@lobehub/lobehub 2.0.0-next.291 → 2.0.0-next.293
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/.conductor/setup.sh +107 -0
- package/.cursor/rules/linear.mdc +53 -0
- package/.github/actions/desktop-build-setup/action.yml +29 -0
- package/.github/actions/desktop-upload-artifacts/action.yml +46 -0
- package/.github/workflows/release-desktop-beta.yml +76 -115
- package/.github/workflows/release-desktop-stable.yml +472 -0
- package/CHANGELOG.md +58 -0
- package/CLAUDE.md +2 -48
- package/apps/desktop/dev-app-update.yml +10 -0
- package/apps/desktop/electron-builder.mjs +40 -10
- package/apps/desktop/electron.vite.config.ts +3 -2
- package/apps/desktop/package.json +2 -1
- package/apps/desktop/scripts/update-test/README.md +222 -0
- package/apps/desktop/scripts/update-test/dev-app-update.local.yml +18 -0
- package/apps/desktop/scripts/update-test/generate-manifest.sh +277 -0
- package/apps/desktop/scripts/update-test/run-test.sh +105 -0
- package/apps/desktop/scripts/update-test/setup.sh +111 -0
- package/apps/desktop/scripts/update-test/start-server.sh +70 -0
- package/apps/desktop/scripts/update-test/stop-server.sh +33 -0
- package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +120 -9
- package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +17 -1
- package/apps/desktop/src/main/env.ts +19 -11
- package/apps/desktop/src/main/modules/updater/configs.ts +14 -1
- package/changelog/v1.json +14 -0
- package/conductor.json +5 -0
- package/locales/en-US/subscription.json +2 -2
- package/locales/zh-CN/subscription.json +2 -2
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +16 -14
- package/packages/const/src/cacheControl.ts +1 -0
- package/packages/electron-client-ipc/src/useWatchBroadcast.ts +10 -4
- package/packages/model-bank/src/aiModels/qiniu.ts +6 -6
- package/packages/observability-otel/src/node.ts +39 -37
- package/scripts/electronWorkflow/mergeMacReleaseFiles.js +22 -8
- package/src/app/(backend)/api/desktop/latest/route.ts +115 -0
- package/src/app/(backend)/api/version/route.ts +13 -0
- package/src/app/(backend)/middleware/validate/createValidator.test.ts +61 -0
- package/src/app/(backend)/middleware/validate/createValidator.ts +79 -0
- package/src/app/(backend)/middleware/validate/index.ts +3 -0
- package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +2 -1
- package/src/app/[variants]/(main)/_layout/index.tsx +2 -1
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +0 -1
- package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +5 -5
- package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +3 -1
- package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +3 -1
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +3 -3
- package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +1 -4
- package/src/app/[variants]/router/desktopRouter.config.tsx +1 -4
- package/src/components/HtmlPreview/PreviewDrawer.tsx +1 -1
- package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +12 -2
- package/src/features/Conversation/Messages/Tool/Tool/index.tsx +10 -1
- package/src/features/{ElectronTitlebar/hooks → Electron/navigation}/useNavigationHistory.ts +1 -1
- package/src/features/{ElectronTitlebar/NavigationBar/index.tsx → Electron/titlebar/NavigationBar.tsx} +1 -1
- package/src/features/{ElectronTitlebar/NavigationBar → Electron/titlebar}/RecentlyViewed.tsx +1 -1
- package/src/features/{ElectronTitlebar/index.tsx → Electron/titlebar/TitleBar.tsx} +19 -9
- package/src/features/Electron/titlebar/WinControl.tsx +5 -0
- package/src/features/Electron/updater/UpdateModal.tsx +299 -0
- package/src/features/LibraryModal/AddFilesToKnowledgeBase/index.test.tsx +24 -0
- package/src/features/LibraryModal/AddFilesToKnowledgeBase/index.tsx +21 -24
- package/src/features/LibraryModal/CreateNew/index.tsx +18 -22
- package/src/features/PluginDevModal/index.tsx +1 -1
- package/src/layout/GlobalProvider/AppTheme.tsx +1 -1
- package/src/libs/swr/index.ts +26 -30
- package/src/server/services/desktopRelease/index.test.ts +65 -0
- package/src/server/services/desktopRelease/index.ts +208 -0
- package/src/store/aiInfra/slices/aiProvider/action.ts +16 -17
- package/src/store/chat/slices/portal/action.test.ts +0 -2
- package/src/store/chat/slices/portal/action.ts +17 -44
- package/src/store/chat/slices/thread/action.test.ts +4 -1
- package/src/store/chat/slices/thread/action.ts +6 -1
- package/src/components/FunctionModal/createModalHooks.ts +0 -48
- package/src/components/FunctionModal/index.ts +0 -1
- package/src/components/FunctionModal/style.tsx +0 -44
- package/src/features/ElectronTitlebar/UpdateModal.tsx +0 -274
- package/src/features/ElectronTitlebar/WinControl/index.tsx +0 -90
- /package/src/features/{ElectronTitlebar/Connection/index.tsx → Electron/connection/Connection.tsx} +0 -0
- /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/ConnectionMode.tsx +0 -0
- /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/Option.tsx +0 -0
- /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/RemoteStatus.tsx +0 -0
- /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/Waiting.tsx +0 -0
- /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/WaitingAnim.tsx +0 -0
- /package/src/features/{ElectronTitlebar/helpers → Electron/navigation}/routeMetadata.ts +0 -0
- /package/src/features/{ElectronTitlebar/hooks → Electron/system}/useWatchThemeUpdate.ts +0 -0
- /package/src/features/{ElectronTitlebar → Electron/titlebar}/SimpleTitleBar.tsx +0 -0
- /package/src/features/{ElectronTitlebar → Electron/updater}/UpdateNotification.tsx +0 -0
package/src/libs/swr/index.ts
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import useSWR, { type SWRHook } from 'swr';
|
|
2
|
-
import useSWRMutation from 'swr/mutation';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* This type of request method is relatively flexible data, which will be triggered on the first time
|
|
4
|
+
* This type of request method is for relatively flexible data, which will be triggered on the first time.
|
|
6
5
|
*
|
|
7
6
|
* Refresh rules have two types:
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* - can be combined with refreshXXX methods to refresh data
|
|
7
|
+
* - When the user refocuses, it will be refreshed outside the 5mins interval.
|
|
8
|
+
* - Can be combined with refreshXXX methods to refresh data.
|
|
11
9
|
*
|
|
12
10
|
* Suitable for messages, topics, sessions, and other data that users will interact with on the client.
|
|
13
|
-
*
|
|
14
|
-
* 这一类请求方法是相对灵活的数据,会在请求时触发
|
|
15
|
-
*
|
|
16
|
-
* 刷新规则有两种:
|
|
17
|
-
* - 当用户重新聚焦时,在 5mins 间隔外会重新刷新一次
|
|
18
|
-
* - 可以搭配 refreshXXX 这样的方法刷新数据
|
|
19
|
-
*
|
|
20
|
-
* 适用于 messages、topics、sessions 等用户会在客户端交互的数据
|
|
21
11
|
*/
|
|
22
12
|
// @ts-ignore
|
|
23
13
|
export const useClientDataSWR: SWRHook = (key, fetch, config) =>
|
|
@@ -47,11 +37,8 @@ export const useClientDataSWR: SWRHook = (key, fetch, config) =>
|
|
|
47
37
|
});
|
|
48
38
|
|
|
49
39
|
/**
|
|
50
|
-
* This type of request method is relatively "
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
* 这一类请求方法是相对"死"的请求模式,只会在第一次请求时触发。
|
|
54
|
-
* 适用于第一次请求,例如 `initUserState`
|
|
40
|
+
* This type of request method is a relatively "static" request mode, which will only be triggered on the first request.
|
|
41
|
+
* Suitable for first time requests like `initUserState`.
|
|
55
42
|
*/
|
|
56
43
|
// @ts-ignore
|
|
57
44
|
export const useOnlyFetchOnceSWR: SWRHook = (key, fetch, config) =>
|
|
@@ -63,18 +50,27 @@ export const useOnlyFetchOnceSWR: SWRHook = (key, fetch, config) =>
|
|
|
63
50
|
});
|
|
64
51
|
|
|
65
52
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
53
|
+
* This type of request method is for action triggers. Must use mutate to trigger the request.
|
|
54
|
+
* Benefits: built-in loading/error states, easy to handle loading/error UI interactions.
|
|
55
|
+
* Components with the same SWR key will automatically share loading state (e.g., create agent button and the + button in header).
|
|
56
|
+
* Very suitable for create operations.
|
|
69
57
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
58
|
+
* Uses fallbackData as empty object so SWR thinks initial data exists.
|
|
59
|
+
* Combined with revalidateOnMount: false, this prevents auto-fetch on mount.
|
|
72
60
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
// @ts-ignore
|
|
62
|
+
export const useActionSWR: SWRHook = (key, fetch, config) =>
|
|
63
|
+
useSWR(key, fetch, {
|
|
64
|
+
// Use empty object as fallback to prevent auto-fetch when cache is empty
|
|
65
|
+
// Combined with revalidateOnMount: false, SWR won't call fetcher on mount
|
|
66
|
+
fallbackData: {},
|
|
67
|
+
refreshWhenHidden: false,
|
|
68
|
+
refreshWhenOffline: false,
|
|
69
|
+
revalidateOnFocus: false,
|
|
70
|
+
revalidateOnMount: false,
|
|
71
|
+
revalidateOnReconnect: false,
|
|
72
|
+
...config,
|
|
73
|
+
});
|
|
78
74
|
|
|
79
75
|
export interface SWRRefreshParams<T, A = (...args: any[]) => any> {
|
|
80
76
|
action: A;
|
|
@@ -85,8 +81,8 @@ export type SWRefreshMethod<T> = <A extends (...args: any[]) => Promise<any>>(
|
|
|
85
81
|
params?: SWRRefreshParams<T, A>,
|
|
86
82
|
) => ReturnType<A>;
|
|
87
83
|
|
|
88
|
-
//
|
|
84
|
+
// Export hook with auto-sync functionality
|
|
89
85
|
export { useClientDataSWRWithSync } from './useClientDataSWRWithSync';
|
|
90
86
|
|
|
91
|
-
//
|
|
87
|
+
// Export scoped mutate (for custom cache provider scenarios)
|
|
92
88
|
export { mutate, setScopedMutate } from './mutate';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DesktopDownloadType,
|
|
5
|
+
resolveDesktopDownload,
|
|
6
|
+
resolveDesktopDownloadFromUrls,
|
|
7
|
+
} from './index';
|
|
8
|
+
|
|
9
|
+
const mockRelease = {
|
|
10
|
+
assets: [
|
|
11
|
+
{
|
|
12
|
+
browser_download_url: 'https://example.com/LobeHub-2.0.0-arm64.dmg',
|
|
13
|
+
name: 'LobeHub-2.0.0-arm64.dmg',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
browser_download_url: 'https://example.com/LobeHub-2.0.0-x64.dmg',
|
|
17
|
+
name: 'LobeHub-2.0.0-x64.dmg',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
browser_download_url: 'https://example.com/LobeHub-2.0.0-setup.exe',
|
|
21
|
+
name: 'LobeHub-2.0.0-setup.exe',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
browser_download_url: 'https://example.com/LobeHub-2.0.0.AppImage',
|
|
25
|
+
name: 'LobeHub-2.0.0.AppImage',
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
published_at: '2026-01-01T00:00:00.000Z',
|
|
29
|
+
tag_name: 'v2.0.0',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('desktopRelease', () => {
|
|
33
|
+
it.each([
|
|
34
|
+
['mac-arm', 'LobeHub-2.0.0-arm64.dmg'],
|
|
35
|
+
['mac-intel', 'LobeHub-2.0.0-x64.dmg'],
|
|
36
|
+
['windows', 'LobeHub-2.0.0-setup.exe'],
|
|
37
|
+
['linux', 'LobeHub-2.0.0.AppImage'],
|
|
38
|
+
] as Array<[DesktopDownloadType, string]>)(
|
|
39
|
+
'resolveDesktopDownload(%s)',
|
|
40
|
+
(type, expectedAssetName) => {
|
|
41
|
+
const resolved = resolveDesktopDownload(mockRelease as any, type);
|
|
42
|
+
expect(resolved?.assetName).toBe(expectedAssetName);
|
|
43
|
+
expect(resolved?.version).toBe('2.0.0');
|
|
44
|
+
expect(resolved?.tag).toBe('v2.0.0');
|
|
45
|
+
expect(resolved?.type).toBe(type);
|
|
46
|
+
expect(resolved?.url).toContain(expectedAssetName);
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
it('resolveDesktopDownloadFromUrls should match basename', () => {
|
|
51
|
+
const resolved = resolveDesktopDownloadFromUrls({
|
|
52
|
+
publishedAt: '2026-01-01T00:00:00.000Z',
|
|
53
|
+
tag: 'v2.0.0',
|
|
54
|
+
type: 'windows',
|
|
55
|
+
urls: [
|
|
56
|
+
'https://releases.example.com/stable/2.0.0/LobeHub-2.0.0-setup.exe?download=1',
|
|
57
|
+
'https://releases.example.com/stable/2.0.0/LobeHub-2.0.0-x64.dmg',
|
|
58
|
+
],
|
|
59
|
+
version: '2.0.0',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(resolved?.assetName).toBe('LobeHub-2.0.0-setup.exe');
|
|
63
|
+
expect(resolved?.url).toContain('setup.exe');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import urlJoin from 'url-join';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
|
|
4
|
+
import { FetchCacheTag } from '@/const/cacheControl';
|
|
5
|
+
|
|
6
|
+
export type DesktopDownloadType = 'linux' | 'mac-arm' | 'mac-intel' | 'windows';
|
|
7
|
+
|
|
8
|
+
export interface DesktopDownloadInfo {
|
|
9
|
+
assetName: string;
|
|
10
|
+
publishedAt?: string;
|
|
11
|
+
tag: string;
|
|
12
|
+
type: DesktopDownloadType;
|
|
13
|
+
url: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type GithubReleaseAsset = {
|
|
18
|
+
browser_download_url: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type GithubRelease = {
|
|
23
|
+
assets: GithubReleaseAsset[];
|
|
24
|
+
published_at?: string;
|
|
25
|
+
tag_name: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type UpdateServerManifestFile = {
|
|
29
|
+
url: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type UpdateServerManifest = {
|
|
33
|
+
files?: UpdateServerManifestFile[];
|
|
34
|
+
path?: string;
|
|
35
|
+
releaseDate?: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const getBasename = (pathname: string) => {
|
|
40
|
+
const cleaned = pathname.split('?')[0] || '';
|
|
41
|
+
const lastSlash = cleaned.lastIndexOf('/');
|
|
42
|
+
return lastSlash >= 0 ? cleaned.slice(lastSlash + 1) : cleaned;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const isAbsoluteUrl = (value: string) => /^https?:\/\//i.test(value);
|
|
46
|
+
|
|
47
|
+
const buildTypeMatchers = (type: DesktopDownloadType) => {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'mac-arm': {
|
|
50
|
+
return [/-arm64\.dmg$/i, /-arm64-mac\.zip$/i, /-arm64\.zip$/i, /\.dmg$/i, /\.zip$/i];
|
|
51
|
+
}
|
|
52
|
+
case 'mac-intel': {
|
|
53
|
+
return [/-x64\.dmg$/i, /-x64-mac\.zip$/i, /-x64\.zip$/i, /\.dmg$/i, /\.zip$/i];
|
|
54
|
+
}
|
|
55
|
+
case 'windows': {
|
|
56
|
+
return [/-setup\.exe$/i, /\.exe$/i];
|
|
57
|
+
}
|
|
58
|
+
case 'linux': {
|
|
59
|
+
return [/\.appimage$/i, /\.deb$/i, /\.rpm$/i, /\.snap$/i, /\.tar\.gz$/i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const resolveDesktopDownloadFromUrls = (options: {
|
|
65
|
+
publishedAt?: string;
|
|
66
|
+
tag: string;
|
|
67
|
+
type: DesktopDownloadType;
|
|
68
|
+
urls: string[];
|
|
69
|
+
version: string;
|
|
70
|
+
}): DesktopDownloadInfo | null => {
|
|
71
|
+
const matchers = buildTypeMatchers(options.type);
|
|
72
|
+
|
|
73
|
+
const matchedUrl = matchers
|
|
74
|
+
.map((matcher) => options.urls.find((url) => matcher.test(getBasename(url))))
|
|
75
|
+
.find(Boolean);
|
|
76
|
+
|
|
77
|
+
if (!matchedUrl) return null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
assetName: getBasename(matchedUrl),
|
|
81
|
+
publishedAt: options.publishedAt,
|
|
82
|
+
tag: options.tag,
|
|
83
|
+
type: options.type,
|
|
84
|
+
url: matchedUrl,
|
|
85
|
+
version: options.version,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const resolveDesktopDownload = (
|
|
90
|
+
release: GithubRelease,
|
|
91
|
+
type: DesktopDownloadType,
|
|
92
|
+
): DesktopDownloadInfo | null => {
|
|
93
|
+
const tag = release.tag_name;
|
|
94
|
+
const version = tag.replace(/^v/i, '');
|
|
95
|
+
const matchers = buildTypeMatchers(type);
|
|
96
|
+
|
|
97
|
+
const matchedAsset = matchers
|
|
98
|
+
.map((matcher) => release.assets.find((asset) => matcher.test(asset.name)))
|
|
99
|
+
.find(Boolean);
|
|
100
|
+
|
|
101
|
+
if (!matchedAsset) return null;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
assetName: matchedAsset.name,
|
|
105
|
+
publishedAt: release.published_at,
|
|
106
|
+
tag,
|
|
107
|
+
type,
|
|
108
|
+
url: matchedAsset.browser_download_url,
|
|
109
|
+
version,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getLatestDesktopReleaseFromGithub = async (options?: {
|
|
114
|
+
owner?: string;
|
|
115
|
+
repo?: string;
|
|
116
|
+
token?: string;
|
|
117
|
+
}): Promise<GithubRelease> => {
|
|
118
|
+
const owner = options?.owner || 'lobehub';
|
|
119
|
+
const repo = options?.repo || 'lobe-chat';
|
|
120
|
+
const token = options?.token || process.env.GITHUB_TOKEN;
|
|
121
|
+
|
|
122
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, {
|
|
123
|
+
headers: {
|
|
124
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
125
|
+
'Accept': 'application/vnd.github+json',
|
|
126
|
+
'User-Agent': 'lobehub-server',
|
|
127
|
+
},
|
|
128
|
+
next: { revalidate: 300, tags: [FetchCacheTag.DesktopRelease] },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const text = await res.text().catch(() => '');
|
|
133
|
+
throw new Error(`GitHub releases/latest request failed: ${res.status} ${text}`.trim());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (await res.json()) as GithubRelease;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const fetchUpdateServerManifest = async (
|
|
140
|
+
baseUrl: string,
|
|
141
|
+
manifestName: string,
|
|
142
|
+
): Promise<UpdateServerManifest> => {
|
|
143
|
+
const res = await fetch(urlJoin(baseUrl, manifestName), {
|
|
144
|
+
next: { revalidate: 300, tags: [FetchCacheTag.DesktopRelease] },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const text = await res.text().catch(() => '');
|
|
149
|
+
throw new Error(`Update server manifest request failed: ${res.status} ${text}`.trim());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const text = await res.text();
|
|
153
|
+
return (parse(text) || {}) as UpdateServerManifest;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const normalizeManifestUrls = (baseUrl: string, manifest: UpdateServerManifest) => {
|
|
157
|
+
const urls: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const file of manifest.files || []) {
|
|
160
|
+
if (!file?.url) continue;
|
|
161
|
+
urls.push(isAbsoluteUrl(file.url) ? file.url : urlJoin(baseUrl, file.url));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (manifest.path) {
|
|
165
|
+
urls.push(isAbsoluteUrl(manifest.path) ? manifest.path : urlJoin(baseUrl, manifest.path));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return urls;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const getStableDesktopReleaseInfoFromUpdateServer = async (options?: {
|
|
172
|
+
baseUrl?: string;
|
|
173
|
+
}): Promise<{ publishedAt?: string; tag: string; urls: string[]; version: string } | null> => {
|
|
174
|
+
const baseUrl =
|
|
175
|
+
options?.baseUrl || process.env.DESKTOP_UPDATE_SERVER_URL || process.env.UPDATE_SERVER_URL;
|
|
176
|
+
if (!baseUrl) return null;
|
|
177
|
+
|
|
178
|
+
const [mac, win, linux] = await Promise.all([
|
|
179
|
+
fetchUpdateServerManifest(baseUrl, 'stable-mac.yml').catch(() => null),
|
|
180
|
+
fetchUpdateServerManifest(baseUrl, 'stable.yml').catch(() => null),
|
|
181
|
+
fetchUpdateServerManifest(baseUrl, 'stable-linux.yml').catch(() => null),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const manifests = [mac, win, linux].filter(Boolean) as UpdateServerManifest[];
|
|
185
|
+
const version = manifests.map((m) => m.version).find(Boolean) || '';
|
|
186
|
+
if (!version) return null;
|
|
187
|
+
|
|
188
|
+
const tag = `v${version.replace(/^v/i, '')}`;
|
|
189
|
+
const publishedAt = manifests.map((m) => m.releaseDate).find(Boolean);
|
|
190
|
+
|
|
191
|
+
const urls = [
|
|
192
|
+
...(mac ? normalizeManifestUrls(baseUrl, mac) : []),
|
|
193
|
+
...(win ? normalizeManifestUrls(baseUrl, win) : []),
|
|
194
|
+
...(linux ? normalizeManifestUrls(baseUrl, linux) : []),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
return { publishedAt, tag, urls, version: version.replace(/^v/i, '') };
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const resolveDesktopDownloadFromUpdateServer = async (options: {
|
|
201
|
+
baseUrl?: string;
|
|
202
|
+
type: DesktopDownloadType;
|
|
203
|
+
}): Promise<DesktopDownloadInfo | null> => {
|
|
204
|
+
const info = await getStableDesktopReleaseInfoFromUpdateServer({ baseUrl: options.baseUrl });
|
|
205
|
+
if (!info) return null;
|
|
206
|
+
|
|
207
|
+
return resolveDesktopDownloadFromUrls({ ...info, type: options.type });
|
|
208
|
+
};
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
type UpdateAiProviderParams,
|
|
30
30
|
} from '@/types/aiProvider';
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
export type ProviderModelListItem = {
|
|
34
33
|
abilities: ModelAbilities;
|
|
35
34
|
approximatePricePerImage?: number;
|
|
@@ -77,10 +76,10 @@ export const normalizeImageModel = async (
|
|
|
77
76
|
const fallbackParametersPromise = model.parameters
|
|
78
77
|
? Promise.resolve<ModelParamsSchema | undefined>(model.parameters)
|
|
79
78
|
: getModelPropertyWithFallback<ModelParamsSchema | undefined>(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
model.id,
|
|
80
|
+
'parameters',
|
|
81
|
+
model.providerId,
|
|
82
|
+
);
|
|
84
83
|
|
|
85
84
|
const modelWithPricing = model as AIImageModelCard;
|
|
86
85
|
const fallbackPricingPromise = modelWithPricing.pricing
|
|
@@ -321,23 +320,23 @@ export const createAiProviderSlice: StateCreator<
|
|
|
321
320
|
aiProviderDetailMap:
|
|
322
321
|
currentDetailConfig && Object.keys(detailUpdates).length > 0
|
|
323
322
|
? {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
...state.aiProviderDetailMap,
|
|
324
|
+
[id]: {
|
|
325
|
+
...currentDetailConfig,
|
|
326
|
+
...detailUpdates,
|
|
327
|
+
},
|
|
328
|
+
}
|
|
330
329
|
: state.aiProviderDetailMap,
|
|
331
330
|
// Update runtime config for selectors
|
|
332
331
|
aiProviderRuntimeConfig:
|
|
333
332
|
currentRuntimeConfig && Object.keys(updates).length > 0
|
|
334
333
|
? {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
334
|
+
...state.aiProviderRuntimeConfig,
|
|
335
|
+
[id]: {
|
|
336
|
+
...currentRuntimeConfig,
|
|
337
|
+
...updates,
|
|
338
|
+
},
|
|
339
|
+
}
|
|
341
340
|
: state.aiProviderRuntimeConfig,
|
|
342
341
|
};
|
|
343
342
|
},
|
|
@@ -203,7 +203,6 @@ describe('chatDockSlice', () => {
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
|
|
207
206
|
describe('closeToolUI', () => {
|
|
208
207
|
it('should pop ToolUI view from stack', () => {
|
|
209
208
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -267,5 +266,4 @@ describe('chatDockSlice', () => {
|
|
|
267
266
|
expect(result.current.showPortal).toBe(true);
|
|
268
267
|
});
|
|
269
268
|
});
|
|
270
|
-
|
|
271
269
|
});
|
|
@@ -42,71 +42,57 @@ export const chatPortalSlice: StateCreator<
|
|
|
42
42
|
[],
|
|
43
43
|
ChatPortalAction
|
|
44
44
|
> = (set, get) => ({
|
|
45
|
-
|
|
46
|
-
|
|
47
45
|
clearPortalStack: () => {
|
|
48
46
|
set({ portalStack: [], showPortal: false }, false, 'clearPortalStack');
|
|
49
47
|
},
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
closeArtifact: () => {
|
|
49
|
+
closeArtifact: () => {
|
|
53
50
|
const { portalStack } = get();
|
|
54
51
|
if (getCurrentViewType(portalStack) === PortalViewType.Artifact) {
|
|
55
52
|
get().popPortalView();
|
|
56
53
|
}
|
|
57
54
|
},
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
closeDocument: () => {
|
|
56
|
+
closeDocument: () => {
|
|
61
57
|
const { portalStack } = get();
|
|
62
58
|
if (getCurrentViewType(portalStack) === PortalViewType.Document) {
|
|
63
59
|
get().popPortalView();
|
|
64
60
|
}
|
|
65
61
|
},
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
closeFilePreview: () => {
|
|
63
|
+
closeFilePreview: () => {
|
|
69
64
|
const { portalStack } = get();
|
|
70
65
|
if (getCurrentViewType(portalStack) === PortalViewType.FilePreview) {
|
|
71
66
|
get().popPortalView();
|
|
72
67
|
}
|
|
73
68
|
},
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
closeMessageDetail: () => {
|
|
70
|
+
closeMessageDetail: () => {
|
|
77
71
|
const { portalStack } = get();
|
|
78
72
|
if (getCurrentViewType(portalStack) === PortalViewType.MessageDetail) {
|
|
79
73
|
get().popPortalView();
|
|
80
74
|
}
|
|
81
75
|
},
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
closeNotebook: () => {
|
|
77
|
+
closeNotebook: () => {
|
|
85
78
|
const { portalStack } = get();
|
|
86
79
|
if (getCurrentViewType(portalStack) === PortalViewType.Notebook) {
|
|
87
80
|
get().popPortalView();
|
|
88
81
|
}
|
|
89
82
|
},
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
closeToolUI: () => {
|
|
84
|
+
closeToolUI: () => {
|
|
95
85
|
const { portalStack } = get();
|
|
96
86
|
if (getCurrentViewType(portalStack) === PortalViewType.ToolUI) {
|
|
97
87
|
get().popPortalView();
|
|
98
88
|
}
|
|
99
89
|
},
|
|
100
90
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
goBack: () => {
|
|
91
|
+
goBack: () => {
|
|
104
92
|
get().popPortalView();
|
|
105
93
|
},
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
goHome: () => {
|
|
95
|
+
goHome: () => {
|
|
110
96
|
set(
|
|
111
97
|
{
|
|
112
98
|
portalStack: [{ type: PortalViewType.Home }],
|
|
@@ -117,45 +103,32 @@ goHome: () => {
|
|
|
117
103
|
);
|
|
118
104
|
},
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// ============== Convenience Methods (using stack operations) ==============
|
|
123
|
-
openArtifact: (artifact) => {
|
|
106
|
+
// ============== Convenience Methods (using stack operations) ==============
|
|
107
|
+
openArtifact: (artifact) => {
|
|
124
108
|
get().pushPortalView({ artifact, type: PortalViewType.Artifact });
|
|
125
109
|
},
|
|
126
110
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
openDocument: (documentId) => {
|
|
111
|
+
openDocument: (documentId) => {
|
|
131
112
|
get().pushPortalView({ documentId, type: PortalViewType.Document });
|
|
132
113
|
},
|
|
133
114
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
openFilePreview: (file) => {
|
|
115
|
+
openFilePreview: (file) => {
|
|
138
116
|
get().pushPortalView({ file, type: PortalViewType.FilePreview });
|
|
139
117
|
},
|
|
140
118
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
openMessageDetail: (messageId) => {
|
|
119
|
+
openMessageDetail: (messageId) => {
|
|
144
120
|
get().pushPortalView({ messageId, type: PortalViewType.MessageDetail });
|
|
145
121
|
},
|
|
146
122
|
|
|
147
|
-
|
|
148
|
-
openNotebook: () => {
|
|
123
|
+
openNotebook: () => {
|
|
149
124
|
get().pushPortalView({ type: PortalViewType.Notebook });
|
|
150
125
|
},
|
|
151
126
|
|
|
152
|
-
|
|
153
|
-
openToolUI: (messageId, identifier) => {
|
|
127
|
+
openToolUI: (messageId, identifier) => {
|
|
154
128
|
get().pushPortalView({ identifier, messageId, type: PortalViewType.ToolUI });
|
|
155
129
|
},
|
|
156
130
|
|
|
157
|
-
|
|
158
|
-
popPortalView: () => {
|
|
131
|
+
popPortalView: () => {
|
|
159
132
|
const { portalStack } = get();
|
|
160
133
|
|
|
161
134
|
if (portalStack.length <= 1) {
|
|
@@ -167,7 +140,7 @@ popPortalView: () => {
|
|
|
167
140
|
},
|
|
168
141
|
|
|
169
142
|
// ============== Core Stack Operations ==============
|
|
170
|
-
pushPortalView: (view) => {
|
|
143
|
+
pushPortalView: (view) => {
|
|
171
144
|
const { portalStack } = get();
|
|
172
145
|
const top = portalStack.at(-1);
|
|
173
146
|
|
|
@@ -150,7 +150,10 @@ describe('thread action', () => {
|
|
|
150
150
|
expect(result.current.threadStartMessageId).toBe('message-id');
|
|
151
151
|
expect(result.current.portalThreadId).toBeUndefined();
|
|
152
152
|
expect(result.current.startToForkThread).toBe(true);
|
|
153
|
-
expect(pushPortalViewSpy).toHaveBeenCalledWith({
|
|
153
|
+
expect(pushPortalViewSpy).toHaveBeenCalledWith({
|
|
154
|
+
type: 'thread',
|
|
155
|
+
startMessageId: 'message-id',
|
|
156
|
+
});
|
|
154
157
|
});
|
|
155
158
|
});
|
|
156
159
|
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// Disable the auto sort key eslint rule to make the code more logic and readable
|
|
3
3
|
import { LOADING_FLAT } from '@lobechat/const';
|
|
4
4
|
import { chainSummaryTitle } from '@lobechat/prompts';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type CreateMessageParams,
|
|
7
|
+
type IThreadType,
|
|
8
|
+
type ThreadItem,
|
|
9
|
+
type UIChatMessage,
|
|
10
|
+
} from '@lobechat/types';
|
|
6
11
|
import isEqual from 'fast-deep-equal';
|
|
7
12
|
import type { SWRResponse } from 'swr';
|
|
8
13
|
import { type StateCreator } from 'zustand/vanilla';
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { App } from 'antd';
|
|
2
|
-
import { type ModalFuncProps } from 'antd/es/modal/interface';
|
|
3
|
-
import { type MutableRefObject, type ReactNode, type RefObject, useRef } from 'react';
|
|
4
|
-
|
|
5
|
-
import { closeIcon, styles } from './style';
|
|
6
|
-
|
|
7
|
-
interface CreateModalProps extends ModalFuncProps {
|
|
8
|
-
content: ReactNode;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface ModalInstance {
|
|
12
|
-
destroy: (...args: any[]) => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type PropsFunc<T = undefined> = (
|
|
16
|
-
instance: MutableRefObject<ModalInstance | undefined>,
|
|
17
|
-
props?: T,
|
|
18
|
-
) => CreateModalProps;
|
|
19
|
-
|
|
20
|
-
const createModal = <T>(params: CreateModalProps | PropsFunc<T>) => {
|
|
21
|
-
const useModal = () => {
|
|
22
|
-
const { modal } = App.useApp();
|
|
23
|
-
const instanceRef = useRef<ModalInstance>(null);
|
|
24
|
-
|
|
25
|
-
const open = (outProps?: T) => {
|
|
26
|
-
const props =
|
|
27
|
-
typeof params === 'function'
|
|
28
|
-
? params(instanceRef as RefObject<ModalInstance>, outProps)
|
|
29
|
-
: params;
|
|
30
|
-
|
|
31
|
-
instanceRef.current = modal.confirm({
|
|
32
|
-
className: styles.content,
|
|
33
|
-
closable: true,
|
|
34
|
-
closeIcon,
|
|
35
|
-
footer: false,
|
|
36
|
-
icon: null,
|
|
37
|
-
wrapClassName: styles.wrap,
|
|
38
|
-
...props,
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return { open };
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
return useModal;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export { createModal };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './createModalHooks';
|