@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
|
@@ -21,7 +21,7 @@ const qiniuChatModels: AIChatModelCard[] = [
|
|
|
21
21
|
},
|
|
22
22
|
contextWindowTokens: 65_536,
|
|
23
23
|
description:
|
|
24
|
-
|
|
24
|
+
'DeepSeek R1 is DeepSeek’s latest open model with very strong reasoning, matching OpenAI’s o1 on math, programming, and reasoning tasks.',
|
|
25
25
|
displayName: 'DeepSeek R1',
|
|
26
26
|
enabled: true,
|
|
27
27
|
id: 'deepseek-r1',
|
|
@@ -34,7 +34,7 @@ const qiniuChatModels: AIChatModelCard[] = [
|
|
|
34
34
|
search: true,
|
|
35
35
|
},
|
|
36
36
|
contextWindowTokens: 204_800,
|
|
37
|
-
description:
|
|
37
|
+
description:
|
|
38
38
|
'MiniMax-M2.1 is a lightweight, cutting-edge large language model optimized for coding, proxy workflows, and modern application development, providing cleaner, more concise output and faster perceptual response times.',
|
|
39
39
|
displayName: 'MiniMax M2.1',
|
|
40
40
|
enabled: true,
|
|
@@ -89,7 +89,7 @@ const qiniuChatModels: AIChatModelCard[] = [
|
|
|
89
89
|
displayName: 'LongCat Flash Chat',
|
|
90
90
|
enabled: true,
|
|
91
91
|
id: 'meituan/longcat-flash-chat',
|
|
92
|
-
maxOutput:
|
|
92
|
+
maxOutput: 65_536,
|
|
93
93
|
pricing: {
|
|
94
94
|
currency: 'CNY',
|
|
95
95
|
units: [
|
|
@@ -111,8 +111,8 @@ const qiniuChatModels: AIChatModelCard[] = [
|
|
|
111
111
|
search: true,
|
|
112
112
|
},
|
|
113
113
|
contextWindowTokens: 200_000,
|
|
114
|
-
description:
|
|
115
|
-
|
|
114
|
+
description:
|
|
115
|
+
"GLM-4.7 is Zhipu's latest flagship model, offering improved general capabilities, simpler and more natural replies, and a more immersive writing experience.",
|
|
116
116
|
displayName: 'GLM-4.7',
|
|
117
117
|
enabled: true,
|
|
118
118
|
id: 'z-ai/glm-4.7',
|
|
@@ -138,7 +138,7 @@ const qiniuChatModels: AIChatModelCard[] = [
|
|
|
138
138
|
search: true,
|
|
139
139
|
},
|
|
140
140
|
contextWindowTokens: 200_000,
|
|
141
|
-
description:
|
|
141
|
+
description:
|
|
142
142
|
'The flagship model of Zhipu, GLM-4.6, surpasses its predecessor in all aspects of advanced coding, long text processing, reasoning, and intelligent agent capabilities.',
|
|
143
143
|
displayName: 'GLM-4.6',
|
|
144
144
|
enabled: true,
|
|
@@ -5,7 +5,7 @@ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
|
5
5
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
6
6
|
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
7
7
|
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
|
|
8
|
-
import {
|
|
8
|
+
import { DetectedResourceAttributes, resourceFromAttributes } from '@opentelemetry/resources';
|
|
9
9
|
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
10
10
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
11
11
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
@@ -16,48 +16,42 @@ export function attributesForVercel(): DetectedResourceAttributes {
|
|
|
16
16
|
// Vercel.
|
|
17
17
|
// https://vercel.com/docs/projects/environment-variables/system-environment-variables
|
|
18
18
|
// Vercel Env set as top level attribute for simplicity. One of 'production', 'preview' or 'development'.
|
|
19
|
-
env: process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV,
|
|
19
|
+
'env': process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV,
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
process.env.VERCEL_BRANCH_URL ||
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"vercel.project_id": process.env.VERCEL_PROJECT_ID || undefined,
|
|
31
|
-
"vercel.region": process.env.VERCEL_REGION,
|
|
32
|
-
"vercel.runtime": process.env.NEXT_RUNTIME || "nodejs",
|
|
33
|
-
"vercel.sha":
|
|
34
|
-
process.env.VERCEL_GIT_COMMIT_SHA ||
|
|
35
|
-
process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
21
|
+
'vercel.branch_host':
|
|
22
|
+
process.env.VERCEL_BRANCH_URL || process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL || undefined,
|
|
23
|
+
'vercel.deployment_id': process.env.VERCEL_DEPLOYMENT_ID || undefined,
|
|
24
|
+
'vercel.host': process.env.VERCEL_URL || process.env.NEXT_PUBLIC_VERCEL_URL || undefined,
|
|
25
|
+
'vercel.project_id': process.env.VERCEL_PROJECT_ID || undefined,
|
|
26
|
+
'vercel.region': process.env.VERCEL_REGION,
|
|
27
|
+
'vercel.runtime': process.env.NEXT_RUNTIME || 'nodejs',
|
|
28
|
+
'vercel.sha':
|
|
29
|
+
process.env.VERCEL_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
|
|
36
30
|
|
|
37
31
|
'service.version': process.env.VERCEL_DEPLOYMENT_ID,
|
|
38
|
-
}
|
|
32
|
+
};
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
export function attributesForNodejs(): DetectedResourceAttributes {
|
|
42
36
|
return {
|
|
43
37
|
// Node.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
38
|
+
'node.ci': process.env.CI ? true : undefined,
|
|
39
|
+
'node.env': process.env.NODE_ENV,
|
|
40
|
+
};
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
export function attributesForEnv(): DetectedResourceAttributes {
|
|
50
44
|
return {
|
|
51
45
|
...attributesForVercel(),
|
|
52
46
|
...attributesForNodejs(),
|
|
53
|
-
}
|
|
47
|
+
};
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
export function attributesCommon(): DetectedResourceAttributes {
|
|
57
51
|
return {
|
|
58
52
|
[ATTR_SERVICE_NAME]: 'lobe-chat',
|
|
59
53
|
...attributesForEnv(),
|
|
60
|
-
}
|
|
54
|
+
};
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
function debugLogLevelFromString(level?: string | null): DiagLogLevel | undefined {
|
|
@@ -69,26 +63,38 @@ function debugLogLevelFromString(level?: string | null): DiagLogLevel | undefine
|
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
switch (level.toLowerCase()) {
|
|
72
|
-
case 'none':
|
|
66
|
+
case 'none': {
|
|
73
67
|
return DiagLogLevel.NONE;
|
|
74
|
-
|
|
68
|
+
}
|
|
69
|
+
case 'error': {
|
|
75
70
|
return DiagLogLevel.ERROR;
|
|
76
|
-
|
|
71
|
+
}
|
|
72
|
+
case 'warn': {
|
|
77
73
|
return DiagLogLevel.WARN;
|
|
78
|
-
|
|
74
|
+
}
|
|
75
|
+
case 'info': {
|
|
79
76
|
return DiagLogLevel.INFO;
|
|
80
|
-
|
|
77
|
+
}
|
|
78
|
+
case 'debug': {
|
|
81
79
|
return DiagLogLevel.DEBUG;
|
|
82
|
-
|
|
80
|
+
}
|
|
81
|
+
case 'verbose': {
|
|
83
82
|
return DiagLogLevel.VERBOSE;
|
|
84
|
-
|
|
83
|
+
}
|
|
84
|
+
case 'all': {
|
|
85
85
|
return DiagLogLevel.ALL;
|
|
86
|
-
|
|
86
|
+
}
|
|
87
|
+
default: {
|
|
87
88
|
return undefined;
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
export function register(options?: {
|
|
93
|
+
export function register(options?: {
|
|
94
|
+
debug?: true | DiagLogLevel;
|
|
95
|
+
name?: string;
|
|
96
|
+
version?: string;
|
|
97
|
+
}) {
|
|
92
98
|
const attributes = attributesCommon();
|
|
93
99
|
|
|
94
100
|
if (typeof options?.name !== 'undefined') {
|
|
@@ -102,11 +108,7 @@ export function register(options?: { debug?: true | DiagLogLevel; name?: string;
|
|
|
102
108
|
|
|
103
109
|
diag.setLogger(
|
|
104
110
|
new DiagConsoleLogger(),
|
|
105
|
-
!!levelFromEnv
|
|
106
|
-
? levelFromEnv
|
|
107
|
-
: options?.debug === true
|
|
108
|
-
? DiagLogLevel.DEBUG
|
|
109
|
-
: options?.debug,
|
|
111
|
+
!!levelFromEnv ? levelFromEnv : options?.debug === true ? DiagLogLevel.DEBUG : options?.debug,
|
|
110
112
|
);
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -4,7 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
|
|
6
6
|
// 配置
|
|
7
|
-
|
|
7
|
+
// Support both stable-mac.yml (stable channel) and latest-mac.yml (fallback)
|
|
8
|
+
const STABLE_outputFileName = 'stable-mac.yml';
|
|
9
|
+
const LATEST_outputFileName = 'latest-mac.yml';
|
|
8
10
|
const RELEASE_DIR = path.resolve('release');
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -85,11 +87,23 @@ async function main() {
|
|
|
85
87
|
const releaseFiles = fs.readdirSync(RELEASE_DIR);
|
|
86
88
|
console.log(`📂 Files in release directory: ${releaseFiles.join(', ')}`);
|
|
87
89
|
|
|
88
|
-
// 2. 查找所有 latest-mac*.yml 文件
|
|
89
|
-
|
|
90
|
+
// 2. 查找所有 stable-mac*.yml 和 latest-mac*.yml 文件
|
|
91
|
+
// Prioritize stable-mac*.yml, fallback to latest-mac*.yml
|
|
92
|
+
const stableMacYmlFiles = releaseFiles.filter(
|
|
93
|
+
(f) => f.startsWith('stable-mac') && f.endsWith('.yml'),
|
|
94
|
+
);
|
|
95
|
+
const latestMacYmlFiles = releaseFiles.filter(
|
|
90
96
|
(f) => f.startsWith('latest-mac') && f.endsWith('.yml'),
|
|
91
97
|
);
|
|
92
|
-
|
|
98
|
+
|
|
99
|
+
// Use stable files if available, otherwise use latest
|
|
100
|
+
const macYmlFiles = stableMacYmlFiles.length > 0 ? stableMacYmlFiles : latestMacYmlFiles;
|
|
101
|
+
const outputFileName =
|
|
102
|
+
stableMacYmlFiles.length > 0 ? STABLE_outputFileName : LATEST_outputFileName;
|
|
103
|
+
|
|
104
|
+
console.log(`🔍 Found stable macOS YAML files: ${stableMacYmlFiles.join(', ') || 'none'}`);
|
|
105
|
+
console.log(`🔍 Found latest macOS YAML files: ${latestMacYmlFiles.join(', ') || 'none'}`);
|
|
106
|
+
console.log(`🔍 Using files: ${macYmlFiles.join(', ')} -> ${outputFileName}`);
|
|
93
107
|
|
|
94
108
|
if (macYmlFiles.length === 0) {
|
|
95
109
|
console.log('⚠️ No macOS YAML files found, skipping merge');
|
|
@@ -115,7 +129,7 @@ async function main() {
|
|
|
115
129
|
} else if (platform === 'both') {
|
|
116
130
|
console.log(`✅ Found already merged file: ${fileName}`);
|
|
117
131
|
// 如果已经是合并后的文件,直接复制为最终文件
|
|
118
|
-
writeLocalFile(path.join(RELEASE_DIR,
|
|
132
|
+
writeLocalFile(path.join(RELEASE_DIR, outputFileName), content);
|
|
119
133
|
return;
|
|
120
134
|
} else {
|
|
121
135
|
console.log(`⚠️ Unknown platform type: ${platform} in ${fileName}`);
|
|
@@ -136,13 +150,13 @@ async function main() {
|
|
|
136
150
|
|
|
137
151
|
if (x64Files.length === 0) {
|
|
138
152
|
console.log('⚠️ No x64 files found, using ARM64 only');
|
|
139
|
-
writeLocalFile(path.join(RELEASE_DIR,
|
|
153
|
+
writeLocalFile(path.join(RELEASE_DIR, outputFileName), arm64Files[0].content);
|
|
140
154
|
return;
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
if (arm64Files.length === 0) {
|
|
144
158
|
console.log('⚠️ No ARM64 files found, using x64 only');
|
|
145
|
-
writeLocalFile(path.join(RELEASE_DIR,
|
|
159
|
+
writeLocalFile(path.join(RELEASE_DIR, outputFileName), x64Files[0].content);
|
|
146
160
|
return;
|
|
147
161
|
}
|
|
148
162
|
|
|
@@ -154,7 +168,7 @@ async function main() {
|
|
|
154
168
|
const mergedContent = mergeYamlFiles(x64File.yaml, arm64File.yaml);
|
|
155
169
|
|
|
156
170
|
// 6. 保存合并后的文件
|
|
157
|
-
const mergedFilePath = path.join(RELEASE_DIR,
|
|
171
|
+
const mergedFilePath = path.join(RELEASE_DIR, outputFileName);
|
|
158
172
|
writeLocalFile(mergedFilePath, mergedContent);
|
|
159
173
|
|
|
160
174
|
// 7. 验证合并结果
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { zodValidator } from '@/app/(backend)/middleware/validate';
|
|
6
|
+
import {
|
|
7
|
+
type DesktopDownloadType,
|
|
8
|
+
getLatestDesktopReleaseFromGithub,
|
|
9
|
+
getStableDesktopReleaseInfoFromUpdateServer,
|
|
10
|
+
resolveDesktopDownload,
|
|
11
|
+
resolveDesktopDownloadFromUrls,
|
|
12
|
+
} from '@/server/services/desktopRelease';
|
|
13
|
+
|
|
14
|
+
const log = debug('api-route:desktop:latest');
|
|
15
|
+
|
|
16
|
+
const SupportedTypes = ['mac-arm', 'mac-intel', 'windows', 'linux'] as const;
|
|
17
|
+
|
|
18
|
+
const truthyStringToBoolean = z.preprocess((value) => {
|
|
19
|
+
if (!value) return undefined;
|
|
20
|
+
if (typeof value === 'boolean') return value;
|
|
21
|
+
if (typeof value !== 'string') return undefined;
|
|
22
|
+
|
|
23
|
+
const v = value.trim().toLowerCase();
|
|
24
|
+
if (!v) return undefined;
|
|
25
|
+
|
|
26
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'y';
|
|
27
|
+
}, z.boolean());
|
|
28
|
+
|
|
29
|
+
const downloadTypeSchema = z.preprocess((value) => {
|
|
30
|
+
if (typeof value !== 'string') return value;
|
|
31
|
+
return value;
|
|
32
|
+
}, z.enum(SupportedTypes));
|
|
33
|
+
|
|
34
|
+
const querySchema = z
|
|
35
|
+
.object({
|
|
36
|
+
asJson: truthyStringToBoolean.optional(),
|
|
37
|
+
as_json: truthyStringToBoolean.optional(),
|
|
38
|
+
type: downloadTypeSchema.optional(),
|
|
39
|
+
})
|
|
40
|
+
.strip()
|
|
41
|
+
.transform((value) => ({
|
|
42
|
+
asJson: value.as_json ?? value.asJson ?? false,
|
|
43
|
+
type: value.type,
|
|
44
|
+
}))
|
|
45
|
+
.superRefine((value, ctx) => {
|
|
46
|
+
if (!value.asJson && !value.type) {
|
|
47
|
+
ctx.addIssue({
|
|
48
|
+
code: z.ZodIssueCode.custom,
|
|
49
|
+
message: '`type` is required when `as_json` is false',
|
|
50
|
+
path: ['type'],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const GET = zodValidator(querySchema)(async (req, _context, query) => {
|
|
56
|
+
try {
|
|
57
|
+
const { asJson, type } = query;
|
|
58
|
+
|
|
59
|
+
const stableInfo = await getStableDesktopReleaseInfoFromUpdateServer();
|
|
60
|
+
|
|
61
|
+
if (!type) {
|
|
62
|
+
if (stableInfo) {
|
|
63
|
+
return NextResponse.json({
|
|
64
|
+
links: {
|
|
65
|
+
'linux': resolveDesktopDownloadFromUrls({ ...stableInfo, type: 'linux' }),
|
|
66
|
+
'mac-arm': resolveDesktopDownloadFromUrls({ ...stableInfo, type: 'mac-arm' }),
|
|
67
|
+
'mac-intel': resolveDesktopDownloadFromUrls({ ...stableInfo, type: 'mac-intel' }),
|
|
68
|
+
'windows': resolveDesktopDownloadFromUrls({ ...stableInfo, type: 'windows' }),
|
|
69
|
+
},
|
|
70
|
+
tag: stableInfo.tag,
|
|
71
|
+
version: stableInfo.version,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const release = await getLatestDesktopReleaseFromGithub();
|
|
76
|
+
const resolveOne = (t: DesktopDownloadType) => resolveDesktopDownload(release, t);
|
|
77
|
+
|
|
78
|
+
return NextResponse.json({
|
|
79
|
+
links: {
|
|
80
|
+
'linux': resolveOne('linux'),
|
|
81
|
+
'mac-arm': resolveOne('mac-arm'),
|
|
82
|
+
'mac-intel': resolveOne('mac-intel'),
|
|
83
|
+
'windows': resolveOne('windows'),
|
|
84
|
+
},
|
|
85
|
+
tag: release.tag_name,
|
|
86
|
+
version: release.tag_name.replace(/^v/i, ''),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const s3Resolved = stableInfo ? resolveDesktopDownloadFromUrls({ ...stableInfo, type }) : null;
|
|
91
|
+
if (s3Resolved) {
|
|
92
|
+
if (asJson) return NextResponse.json(s3Resolved);
|
|
93
|
+
return NextResponse.redirect(s3Resolved.url, { status: 302 });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const release = await getLatestDesktopReleaseFromGithub();
|
|
97
|
+
const resolved = resolveDesktopDownload(release, type);
|
|
98
|
+
if (!resolved) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: 'No matched asset for type', supportedTypes: SupportedTypes, type },
|
|
101
|
+
{ status: 404 },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (asJson) return NextResponse.json(resolved);
|
|
106
|
+
|
|
107
|
+
return NextResponse.redirect(resolved.url, { status: 302 });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
log('Failed to resolve latest desktop download: %O', e);
|
|
110
|
+
return NextResponse.json(
|
|
111
|
+
{ error: 'Failed to resolve latest desktop download' },
|
|
112
|
+
{ status: 500 },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
import pkg from '../../../../../package.json';
|
|
4
|
+
|
|
5
|
+
export interface VersionResponseData {
|
|
6
|
+
version: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
return NextResponse.json({
|
|
11
|
+
version: pkg.version,
|
|
12
|
+
} satisfies VersionResponseData);
|
|
13
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
import { createValidator } from './createValidator';
|
|
6
|
+
|
|
7
|
+
describe('createValidator', () => {
|
|
8
|
+
it('should validate query for GET and pass parsed data to handler', async () => {
|
|
9
|
+
const validate = createValidator({
|
|
10
|
+
errorStatus: 422,
|
|
11
|
+
stopOnFirstError: true,
|
|
12
|
+
omitNotShapeField: true,
|
|
13
|
+
});
|
|
14
|
+
const schema = z.object({ type: z.enum(['a', 'b']) });
|
|
15
|
+
|
|
16
|
+
const handler = validate(schema)(async (_req: Request, _ctx: unknown, data: any) => {
|
|
17
|
+
return new Response(JSON.stringify({ ok: true, data }), { status: 200 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const res = await handler(new NextRequest('https://example.com/api?type=a'));
|
|
21
|
+
expect(res.status).toBe(200);
|
|
22
|
+
expect(await res.json()).toEqual({ ok: true, data: { type: 'a' } });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return 422 with one issue when stopOnFirstError', async () => {
|
|
26
|
+
const validate = createValidator({
|
|
27
|
+
errorStatus: 422,
|
|
28
|
+
stopOnFirstError: true,
|
|
29
|
+
omitNotShapeField: true,
|
|
30
|
+
});
|
|
31
|
+
const schema = z.object({
|
|
32
|
+
foo: z.string().min(2),
|
|
33
|
+
type: z.enum(['a', 'b']),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const handler = validate(schema)(async () => new Response('ok'));
|
|
37
|
+
const res = await handler(new NextRequest('https://example.com/api?foo=x&type=c'));
|
|
38
|
+
expect(res.status).toBe(422);
|
|
39
|
+
const body = await res.json();
|
|
40
|
+
expect(body.error).toBe('Invalid request');
|
|
41
|
+
expect(Array.isArray(body.issues)).toBe(true);
|
|
42
|
+
expect(body.issues).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should omit unknown fields when omitNotShapeField enabled', async () => {
|
|
46
|
+
const validate = createValidator({
|
|
47
|
+
errorStatus: 422,
|
|
48
|
+
stopOnFirstError: true,
|
|
49
|
+
omitNotShapeField: true,
|
|
50
|
+
});
|
|
51
|
+
const schema = z.object({ type: z.enum(['a', 'b']) });
|
|
52
|
+
|
|
53
|
+
const handler = validate(schema)(async (_req: Request, _ctx: unknown, data: any) => {
|
|
54
|
+
return new Response(JSON.stringify(data), { status: 200 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const res = await handler(new NextRequest('https://example.com/api?type=a&extra=1'));
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
expect(await res.json()).toEqual({ type: 'a' });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export interface ValidatorOptions {
|
|
5
|
+
errorStatus?: number;
|
|
6
|
+
omitNotShapeField?: boolean;
|
|
7
|
+
stopOnFirstError?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type InferInput<TSchema extends z.ZodTypeAny> = z.input<TSchema>;
|
|
11
|
+
type InferOutput<TSchema extends z.ZodTypeAny> = z.output<TSchema>;
|
|
12
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
13
|
+
|
|
14
|
+
const getRequestInput = async (req: Request): Promise<Record<string, unknown>> => {
|
|
15
|
+
const method = req.method?.toUpperCase?.() ?? 'GET';
|
|
16
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
17
|
+
return Object.fromEntries(new URL(req.url).searchParams.entries());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const contentType = req.headers.get('content-type') || '';
|
|
21
|
+
if (contentType.includes('application/json')) {
|
|
22
|
+
try {
|
|
23
|
+
return (await req.json()) as any;
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return (await (req as any).json?.()) as any;
|
|
31
|
+
} catch {
|
|
32
|
+
return Object.fromEntries(new URL(req.url).searchParams.entries());
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const applyOptionsToSchema = <TSchema extends z.ZodTypeAny>(
|
|
37
|
+
schema: TSchema,
|
|
38
|
+
options: ValidatorOptions,
|
|
39
|
+
): z.ZodTypeAny => {
|
|
40
|
+
if (!options.omitNotShapeField) return schema;
|
|
41
|
+
if (schema instanceof z.ZodObject) return schema.strip();
|
|
42
|
+
return schema;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const createValidator =
|
|
46
|
+
(options: ValidatorOptions = {}) =>
|
|
47
|
+
<TSchema extends z.ZodTypeAny>(schema: TSchema) => {
|
|
48
|
+
const errorStatus = options.errorStatus ?? 422;
|
|
49
|
+
const effectiveSchema = applyOptionsToSchema(schema, options) as z.ZodType<
|
|
50
|
+
InferOutput<TSchema>
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
return <TReq extends NextRequest, TContext>(
|
|
54
|
+
handler: (
|
|
55
|
+
req: TReq,
|
|
56
|
+
context: TContext,
|
|
57
|
+
data: InferOutput<TSchema>,
|
|
58
|
+
) => MaybePromise<Response>,
|
|
59
|
+
) =>
|
|
60
|
+
async (req: TReq, context?: TContext) => {
|
|
61
|
+
const input = (await getRequestInput(req)) as InferInput<TSchema>;
|
|
62
|
+
const result = effectiveSchema.safeParse(input);
|
|
63
|
+
|
|
64
|
+
if (!result.success) {
|
|
65
|
+
const issues = options.stopOnFirstError
|
|
66
|
+
? result.error.issues.slice(0, 1)
|
|
67
|
+
: result.error.issues;
|
|
68
|
+
return NextResponse.json({ error: 'Invalid request', issues }, { status: errorStatus });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return handler(req, context as TContext, result.data);
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const zodValidator = createValidator({
|
|
76
|
+
errorStatus: 422,
|
|
77
|
+
omitNotShapeField: true,
|
|
78
|
+
stopOnFirstError: true,
|
|
79
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
3
4
|
import { Center, Flexbox, Text } from '@lobehub/ui';
|
|
4
5
|
import { Divider } from 'antd';
|
|
5
6
|
import { cx } from 'antd-style';
|
|
6
7
|
import type { FC, PropsWithChildren } from 'react';
|
|
7
8
|
|
|
8
|
-
import
|
|
9
|
+
import SimpleTitleBar from '@/features/Electron/titlebar/SimpleTitleBar';
|
|
9
10
|
import LangButton from '@/features/User/UserPanel/LangButton';
|
|
10
11
|
import ThemeButton from '@/features/User/UserPanel/ThemeButton';
|
|
11
12
|
import { useIsDark } from '@/hooks/useIsDark';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
3
4
|
import { Flexbox } from '@lobehub/ui';
|
|
4
5
|
import { cx } from 'antd-style';
|
|
5
6
|
import dynamic from 'next/dynamic';
|
|
@@ -12,7 +13,7 @@ import Loading from '@/components/Loading/BrandTextLoading';
|
|
|
12
13
|
import { isDesktop } from '@/const/version';
|
|
13
14
|
import { BANNER_HEIGHT } from '@/features/AlertBanner/CloudBanner';
|
|
14
15
|
import DesktopNavigationBridge from '@/features/DesktopNavigationBridge';
|
|
15
|
-
import TitleBar
|
|
16
|
+
import TitleBar from '@/features/Electron/titlebar/TitleBar';
|
|
16
17
|
import HotkeyHelperPanel from '@/features/HotkeyHelperPanel';
|
|
17
18
|
import NavPanel from '@/features/NavPanel';
|
|
18
19
|
import { useFeedbackModal } from '@/hooks/useFeedbackModal';
|
|
@@ -168,11 +168,11 @@ const CronJobDetailPage = memo(() => {
|
|
|
168
168
|
(current) =>
|
|
169
169
|
current
|
|
170
170
|
? {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
...current,
|
|
172
|
+
...payload,
|
|
173
|
+
executionConditions: payload.executionConditions ?? null,
|
|
174
|
+
...(updatedAt ? { updatedAt } : null),
|
|
175
|
+
}
|
|
176
176
|
: current,
|
|
177
177
|
false,
|
|
178
178
|
);
|
|
@@ -32,7 +32,9 @@ const ThreadHydration = memo(() => {
|
|
|
32
32
|
// should open portal automatically when portalThread is set
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
if (!!portalThread && !useChatStore.getState().showPortal) {
|
|
35
|
-
useChatStore
|
|
35
|
+
useChatStore
|
|
36
|
+
.getState()
|
|
37
|
+
.pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
|
|
36
38
|
}
|
|
37
39
|
}, [portalThread]);
|
|
38
40
|
|
|
@@ -32,7 +32,9 @@ const ThreadHydration = memo(() => {
|
|
|
32
32
|
// should open portal automatically when portalThread is set
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
if (!!portalThread && !useChatStore.getState().showPortal) {
|
|
35
|
-
useChatStore
|
|
35
|
+
useChatStore
|
|
36
|
+
.getState()
|
|
37
|
+
.pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
|
|
36
38
|
}
|
|
37
39
|
}, [portalThread]);
|
|
38
40
|
|
|
@@ -186,9 +186,9 @@ const Checker = memo<ConnectionCheckerProps>(
|
|
|
186
186
|
style={
|
|
187
187
|
pass
|
|
188
188
|
? {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
borderColor: cssVar.colorSuccess,
|
|
190
|
+
color: cssVar.colorSuccess,
|
|
191
|
+
}
|
|
192
192
|
: undefined
|
|
193
193
|
}
|
|
194
194
|
>
|
|
@@ -259,10 +259,7 @@ export const mobileRoutes: RouteConfig[] = [
|
|
|
259
259
|
{
|
|
260
260
|
children: [
|
|
261
261
|
{
|
|
262
|
-
element: dynamicElement(
|
|
263
|
-
() => import('../../share/t/[id]'),
|
|
264
|
-
'Mobile > Share > Topic',
|
|
265
|
-
),
|
|
262
|
+
element: dynamicElement(() => import('../../share/t/[id]'), 'Mobile > Share > Topic'),
|
|
266
263
|
path: ':id',
|
|
267
264
|
},
|
|
268
265
|
],
|
|
@@ -403,10 +403,7 @@ export const desktopRoutes: RouteConfig[] = [
|
|
|
403
403
|
{
|
|
404
404
|
children: [
|
|
405
405
|
{
|
|
406
|
-
element: dynamicElement(
|
|
407
|
-
() => import('../share/t/[id]'),
|
|
408
|
-
'Desktop > Share > Topic',
|
|
409
|
-
),
|
|
406
|
+
element: dynamicElement(() => import('../share/t/[id]'), 'Desktop > Share > Topic'),
|
|
410
407
|
path: ':id',
|
|
411
408
|
},
|
|
412
409
|
],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
|
|
1
2
|
import { exportFile } from '@lobechat/utils/client';
|
|
2
3
|
import { Block, Button, Flexbox, Highlighter, Segmented } from '@lobehub/ui';
|
|
3
4
|
import { Drawer } from 'antd';
|
|
@@ -7,7 +8,6 @@ import { memo, useCallback, useState } from 'react';
|
|
|
7
8
|
import { useTranslation } from 'react-i18next';
|
|
8
9
|
|
|
9
10
|
import { isDesktop } from '@/const/version';
|
|
10
|
-
import { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
|
|
11
11
|
|
|
12
12
|
const styles = createStaticStyles(({ css }) => ({
|
|
13
13
|
container: css`
|