@lobehub/lobehub 2.0.0-next.290 → 2.0.0-next.292

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.
Files changed (89) hide show
  1. package/.conductor/setup.sh +107 -0
  2. package/.cursor/rules/linear.mdc +53 -0
  3. package/.github/actions/desktop-build-setup/action.yml +29 -0
  4. package/.github/actions/desktop-upload-artifacts/action.yml +46 -0
  5. package/.github/workflows/release-desktop-beta.yml +76 -115
  6. package/.github/workflows/release-desktop-stable.yml +461 -0
  7. package/CHANGELOG.md +68 -0
  8. package/CLAUDE.md +2 -48
  9. package/apps/desktop/dev-app-update.yml +10 -0
  10. package/apps/desktop/electron-builder.mjs +40 -10
  11. package/apps/desktop/electron.vite.config.ts +3 -2
  12. package/apps/desktop/package.json +2 -1
  13. package/apps/desktop/scripts/update-test/README.md +222 -0
  14. package/apps/desktop/scripts/update-test/dev-app-update.local.yml +18 -0
  15. package/apps/desktop/scripts/update-test/generate-manifest.sh +277 -0
  16. package/apps/desktop/scripts/update-test/run-test.sh +105 -0
  17. package/apps/desktop/scripts/update-test/setup.sh +111 -0
  18. package/apps/desktop/scripts/update-test/start-server.sh +70 -0
  19. package/apps/desktop/scripts/update-test/stop-server.sh +33 -0
  20. package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +120 -9
  21. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +17 -1
  22. package/apps/desktop/src/main/env.ts +19 -11
  23. package/apps/desktop/src/main/modules/updater/configs.ts +14 -1
  24. package/changelog/v1.json +21 -0
  25. package/conductor.json +5 -0
  26. package/locales/en-US/chat.json +2 -0
  27. package/locales/en-US/subscription.json +2 -2
  28. package/locales/zh-CN/chat.json +2 -0
  29. package/locales/zh-CN/subscription.json +2 -2
  30. package/package.json +1 -1
  31. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +16 -14
  32. package/packages/electron-client-ipc/src/useWatchBroadcast.ts +10 -4
  33. package/packages/model-bank/src/aiModels/qiniu.ts +6 -6
  34. package/packages/observability-otel/src/node.ts +39 -37
  35. package/scripts/electronWorkflow/mergeMacReleaseFiles.js +22 -8
  36. package/src/app/(backend)/api/version/route.ts +13 -0
  37. package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +2 -1
  38. package/src/app/[variants]/(main)/_layout/index.tsx +2 -1
  39. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +0 -1
  40. package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +5 -5
  41. package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +3 -1
  42. package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +3 -1
  43. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +15 -6
  44. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +68 -23
  45. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +1 -4
  46. package/src/app/[variants]/router/desktopRouter.config.tsx +1 -4
  47. package/src/components/HtmlPreview/PreviewDrawer.tsx +1 -1
  48. package/src/features/ChatInput/ChatInputProvider.tsx +1 -1
  49. package/src/features/Conversation/Messages/Assistant/components/MessageContent.tsx +9 -16
  50. package/src/features/Conversation/Messages/AssistantGroup/components/GroupItem.tsx +12 -2
  51. package/src/features/Conversation/Messages/Task/components/MessageContent.tsx +1 -0
  52. package/src/features/Conversation/Messages/Tool/Tool/index.tsx +10 -1
  53. package/src/features/Conversation/Messages/components/ContentLoading.tsx +64 -0
  54. package/src/features/Conversation/Messages/components/DisplayContent.tsx +4 -2
  55. package/src/features/{ElectronTitlebar/hooks → Electron/navigation}/useNavigationHistory.ts +1 -1
  56. package/src/features/{ElectronTitlebar/NavigationBar/index.tsx → Electron/titlebar/NavigationBar.tsx} +1 -1
  57. package/src/features/{ElectronTitlebar/NavigationBar → Electron/titlebar}/RecentlyViewed.tsx +1 -1
  58. package/src/features/{ElectronTitlebar/index.tsx → Electron/titlebar/TitleBar.tsx} +19 -9
  59. package/src/features/Electron/titlebar/WinControl.tsx +5 -0
  60. package/src/features/Electron/updater/UpdateModal.tsx +299 -0
  61. package/src/features/LibraryModal/AddFilesToKnowledgeBase/index.test.tsx +24 -0
  62. package/src/features/LibraryModal/AddFilesToKnowledgeBase/index.tsx +21 -24
  63. package/src/features/LibraryModal/CreateNew/index.tsx +18 -22
  64. package/src/features/OllamaModelDownloader/index.tsx +3 -3
  65. package/src/features/PluginDevModal/index.tsx +1 -1
  66. package/src/layout/GlobalProvider/AppTheme.tsx +1 -1
  67. package/src/libs/swr/index.ts +17 -23
  68. package/src/locales/default/chat.ts +2 -0
  69. package/src/store/aiInfra/slices/aiProvider/action.ts +68 -1
  70. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +2 -1
  71. package/src/store/chat/slices/portal/action.test.ts +0 -2
  72. package/src/store/chat/slices/portal/action.ts +17 -44
  73. package/src/store/chat/slices/thread/action.test.ts +4 -1
  74. package/src/store/chat/slices/thread/action.ts +6 -1
  75. package/src/components/FunctionModal/createModalHooks.ts +0 -48
  76. package/src/components/FunctionModal/index.ts +0 -1
  77. package/src/components/FunctionModal/style.tsx +0 -44
  78. package/src/features/ElectronTitlebar/UpdateModal.tsx +0 -274
  79. package/src/features/ElectronTitlebar/WinControl/index.tsx +0 -90
  80. /package/src/features/{ElectronTitlebar/Connection/index.tsx → Electron/connection/Connection.tsx} +0 -0
  81. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/ConnectionMode.tsx +0 -0
  82. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/Option.tsx +0 -0
  83. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/RemoteStatus.tsx +0 -0
  84. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/Waiting.tsx +0 -0
  85. /package/src/features/{ElectronTitlebar/Connection → Electron/connection}/WaitingAnim.tsx +0 -0
  86. /package/src/features/{ElectronTitlebar/helpers → Electron/navigation}/routeMetadata.ts +0 -0
  87. /package/src/features/{ElectronTitlebar/hooks → Electron/system}/useWatchThemeUpdate.ts +0 -0
  88. /package/src/features/{ElectronTitlebar → Electron/titlebar}/SimpleTitleBar.tsx +0 -0
  89. /package/src/features/{ElectronTitlebar → Electron/updater}/UpdateNotification.tsx +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect } from 'react';
3
+ import { useEffect, useLayoutEffect, useRef } from 'react';
4
4
 
5
5
  import { MainBroadcastEventKey, MainBroadcastParams } from './events';
6
6
 
@@ -21,11 +21,17 @@ export const useWatchBroadcast = <T extends MainBroadcastEventKey>(
21
21
  event: T,
22
22
  handler: (data: MainBroadcastParams<T>) => void,
23
23
  ) => {
24
+ const handlerRef = useRef<typeof handler>(handler);
25
+
26
+ useLayoutEffect(() => {
27
+ handlerRef.current = handler;
28
+ }, [handler]);
29
+
24
30
  useEffect(() => {
25
31
  if (!window.electron) return;
26
32
 
27
- const listener = (e: any, data: MainBroadcastParams<T>) => {
28
- handler(data);
33
+ const listener = (_e: any, data: MainBroadcastParams<T>) => {
34
+ handlerRef.current(data);
29
35
  };
30
36
 
31
37
  window.electron.ipcRenderer.on(event, listener);
@@ -33,5 +39,5 @@ export const useWatchBroadcast = <T extends MainBroadcastEventKey>(
33
39
  return () => {
34
40
  window.electron.ipcRenderer.removeListener(event, listener);
35
41
  };
36
- }, []);
42
+ }, [event]);
37
43
  };
@@ -21,7 +21,7 @@ const qiniuChatModels: AIChatModelCard[] = [
21
21
  },
22
22
  contextWindowTokens: 65_536,
23
23
  description:
24
- "DeepSeek R1 is DeepSeek’s latest open model with very strong reasoning, matching OpenAI’s o1 on math, programming, and reasoning tasks.",
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: 65536,
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
- 'GLM-4.7 is Zhipu\'s latest flagship model, offering improved general capabilities, simpler and more natural replies, and a more immersive writing experience.',
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 { resourceFromAttributes, DetectedResourceAttributes } from '@opentelemetry/resources';
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
- "vercel.branch_host":
22
- process.env.VERCEL_BRANCH_URL ||
23
- process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL ||
24
- undefined,
25
- "vercel.deployment_id": process.env.VERCEL_DEPLOYMENT_ID || undefined,
26
- "vercel.host":
27
- process.env.VERCEL_URL ||
28
- process.env.NEXT_PUBLIC_VERCEL_URL ||
29
- undefined,
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
- "node.ci": process.env.CI ? true : undefined,
45
- "node.env": process.env.NODE_ENV,
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
- case 'error':
68
+ }
69
+ case 'error': {
75
70
  return DiagLogLevel.ERROR;
76
- case 'warn':
71
+ }
72
+ case 'warn': {
77
73
  return DiagLogLevel.WARN;
78
- case 'info':
74
+ }
75
+ case 'info': {
79
76
  return DiagLogLevel.INFO;
80
- case 'debug':
77
+ }
78
+ case 'debug': {
81
79
  return DiagLogLevel.DEBUG;
82
- case 'verbose':
80
+ }
81
+ case 'verbose': {
83
82
  return DiagLogLevel.VERBOSE;
84
- case 'all':
83
+ }
84
+ case 'all': {
85
85
  return DiagLogLevel.ALL;
86
- default:
86
+ }
87
+ default: {
87
88
  return undefined;
89
+ }
88
90
  }
89
91
  }
90
92
 
91
- export function register(options?: { debug?: true | DiagLogLevel; name?: string; version?: string }) {
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
- const FILE_NAME = 'latest-mac.yml';
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
- const macYmlFiles = releaseFiles.filter(
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
- console.log(`🔍 Found macOS YAML files: ${macYmlFiles.join(', ')}`);
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, FILE_NAME), content);
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, FILE_NAME), arm64Files[0].content);
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, FILE_NAME), x64Files[0].content);
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, FILE_NAME);
171
+ const mergedFilePath = path.join(RELEASE_DIR, outputFileName);
158
172
  writeLocalFile(mergedFilePath, mergedContent);
159
173
 
160
174
  // 7. 验证合并结果
@@ -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
+ }
@@ -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 { SimpleTitleBar, TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
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, { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
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';
@@ -187,7 +187,6 @@ const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
187
187
  style={{ maxWidth: 300, minWidth: 200 }}
188
188
  value={timezone}
189
189
  />
190
-
191
190
  </Flexbox>
192
191
 
193
192
  {/* Max Executions */}
@@ -168,11 +168,11 @@ const CronJobDetailPage = memo(() => {
168
168
  (current) =>
169
169
  current
170
170
  ? {
171
- ...current,
172
- ...payload,
173
- executionConditions: payload.executionConditions ?? null,
174
- ...(updatedAt ? { updatedAt } : null),
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.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
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.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
35
+ useChatStore
36
+ .getState()
37
+ .pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
36
38
  }
37
39
  }, [portalThread]);
38
40
 
@@ -6,7 +6,7 @@ import { ModelIcon } from '@lobehub/icons';
6
6
  import { Alert, Button, Flexbox, Highlighter, Icon, Select } from '@lobehub/ui';
7
7
  import { cssVar } from 'antd-style';
8
8
  import { Loader2Icon } from 'lucide-react';
9
- import { type ReactNode, memo, useState } from 'react';
9
+ import { type ReactNode, memo, useEffect, useState } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
 
12
12
  import { useProviderName } from '@/hooks/useProviderName';
@@ -58,9 +58,10 @@ const Checker = memo<ConnectionCheckerProps>(
58
58
  ({ model, provider, checkErrorRender: CheckErrorRender, onBeforeCheck, onAfterCheck }) => {
59
59
  const { t } = useTranslation('setting');
60
60
 
61
- const isProviderConfigUpdating = useAiInfraStore(
62
- aiProviderSelectors.isProviderConfigUpdating(provider),
63
- );
61
+ const [isProviderConfigUpdating, updateAiProviderConfig] = useAiInfraStore((s) => [
62
+ aiProviderSelectors.isProviderConfigUpdating(provider)(s),
63
+ s.updateAiProviderConfig,
64
+ ]);
64
65
  const totalModels = useAiInfraStore(aiModelSelectors.aiProviderChatModelListIds);
65
66
 
66
67
  const [loading, setLoading] = useState(false);
@@ -69,6 +70,11 @@ const Checker = memo<ConnectionCheckerProps>(
69
70
 
70
71
  const [error, setError] = useState<ChatMessageError | undefined>();
71
72
 
73
+ // Sync checkModel state when model prop changes
74
+ useEffect(() => {
75
+ setCheckModel(model);
76
+ }, [model]);
77
+
72
78
  const checkConnection = async () => {
73
79
  // Clear previous check results immediately
74
80
  setPass(false);
@@ -131,11 +137,14 @@ const Checker = memo<ConnectionCheckerProps>(
131
137
  <Select
132
138
  listItemHeight={36}
133
139
  onSelect={async (value) => {
134
- // Changing the check model should be a local UI concern only.
135
- // Persisting it to provider config would trigger global refresh/revalidation.
140
+ // Update local state
136
141
  setCheckModel(value);
137
142
  setPass(false);
138
143
  setError(undefined);
144
+
145
+ // Persist the selected model to provider config
146
+ // This allows the model to be retained after page refresh
147
+ await updateAiProviderConfig(provider, { checkModel: value });
139
148
  }}
140
149
  optionRender={({ value }) => {
141
150
  return (
@@ -13,7 +13,7 @@ import {
13
13
  } from '@lobehub/ui';
14
14
  import { Center, Flexbox, Skeleton } from '@lobehub/ui';
15
15
  import { useDebounceFn } from 'ahooks';
16
- import { Switch } from 'antd';
16
+ import { Form as AntdForm, Switch } from 'antd';
17
17
  import { createStaticStyles, cssVar, cx, responsive } from 'antd-style';
18
18
  import { Loader2Icon, LockIcon } from 'lucide-react';
19
19
  import Link from 'next/link';
@@ -150,26 +150,72 @@ const ProviderConfig = memo<ProviderConfigProps>(
150
150
  enabled,
151
151
  isLoading,
152
152
  configUpdating,
153
- enableResponseApi,
154
- isProviderEndpointNotEmpty,
155
- isProviderApiKeyNotEmpty,
153
+ providerRuntimeConfig,
156
154
  ] = useAiInfraStore((s) => [
157
155
  aiProviderSelectors.providerDetailById(id)(s),
158
156
  s.updateAiProviderConfig,
159
157
  aiProviderSelectors.isProviderEnabled(id)(s),
160
158
  aiProviderSelectors.isAiProviderConfigLoading(id)(s),
161
159
  aiProviderSelectors.isProviderConfigUpdating(id)(s),
162
- aiProviderSelectors.isProviderEnableResponseApi(id)(s),
163
- aiProviderSelectors.isActiveProviderEndpointNotEmpty(s),
164
- aiProviderSelectors.isActiveProviderApiKeyNotEmpty(s),
160
+ aiProviderSelectors.providerConfigById(id)(s),
165
161
  ]);
166
162
 
163
+ // Watch form values in real-time to show/hide switches immediately
164
+ // Watch nested form values for endpoints
165
+ const formBaseURL = AntdForm.useWatch(['keyVaults', 'baseURL'], form);
166
+ const formEndpoint = AntdForm.useWatch(['keyVaults', 'endpoint'], form);
167
+ // Watch all possible credential fields for different providers
168
+ const formApiKey = AntdForm.useWatch(['keyVaults', 'apiKey'], form);
169
+ const formAccessKeyId = AntdForm.useWatch(['keyVaults', 'accessKeyId'], form);
170
+ const formSecretAccessKey = AntdForm.useWatch(['keyVaults', 'secretAccessKey'], form);
171
+ const formUsername = AntdForm.useWatch(['keyVaults', 'username'], form);
172
+ const formPassword = AntdForm.useWatch(['keyVaults', 'password'], form);
173
+
174
+ // Check if provider has endpoint and apiKey based on runtime config
175
+ // Fallback to data.keyVaults if runtime config is not yet loaded
176
+ const keyVaults = providerRuntimeConfig?.keyVaults || data?.keyVaults;
177
+ // Use form values first (for immediate update), fallback to stored values
178
+ const isProviderEndpointNotEmpty =
179
+ !!formBaseURL || !!formEndpoint || !!keyVaults?.baseURL || !!keyVaults?.endpoint;
180
+ // Check if any credential is present for different authentication types:
181
+ // - Standard apiKey (OpenAI, Azure, Cloudflare, VertexAI, etc.)
182
+ // - AWS Bedrock credentials (accessKeyId, secretAccessKey)
183
+ // - ComfyUI basic auth (username and password)
184
+ const isProviderApiKeyNotEmpty = !!(
185
+ formApiKey ||
186
+ keyVaults?.apiKey ||
187
+ formAccessKeyId ||
188
+ keyVaults?.accessKeyId ||
189
+ formSecretAccessKey ||
190
+ keyVaults?.secretAccessKey ||
191
+ (formUsername && formPassword) ||
192
+ (keyVaults?.username && keyVaults?.password)
193
+ );
194
+
195
+ // Track the last initialized provider ID to avoid resetting form during edits
196
+ const lastInitializedIdRef = useRef<string | null>(null);
197
+
167
198
  useLayoutEffect(() => {
168
199
  if (isLoading) return;
169
200
 
170
- // set the first time
171
- form.setFieldsValue(data);
172
- }, [isLoading, id, data]);
201
+ // Only initialize form when:
202
+ // 1. First load (lastInitializedIdRef.current === null)
203
+ // 2. Provider ID changed (switching between providers)
204
+ const shouldInitialize = lastInitializedIdRef.current !== id;
205
+ if (!shouldInitialize) return;
206
+
207
+ // Merge data from both sources to ensure all fields are initialized correctly
208
+ // data: contains basic info like apiKey, baseURL, fetchOnClient
209
+ // providerRuntimeConfig: contains nested config like enableResponseApi
210
+ const mergedData = {
211
+ ...data,
212
+ ...(providerRuntimeConfig?.config && { config: providerRuntimeConfig.config }),
213
+ };
214
+
215
+ // Set form values and mark as initialized
216
+ form.setFieldsValue(mergedData);
217
+ lastInitializedIdRef.current = id;
218
+ }, [isLoading, id, data, providerRuntimeConfig, form]);
173
219
 
174
220
  // 标记是否正在进行连接测试
175
221
  const isCheckingConnection = useRef(false);
@@ -298,24 +344,23 @@ const ProviderConfig = memo<ProviderConfigProps>(
298
344
  (defaultShowBrowserRequest ||
299
345
  (showEndpoint && isProviderEndpointNotEmpty) ||
300
346
  (showApiKey && isProviderApiKeyNotEmpty));
301
- const clientFetchItem = showClientFetch && {
302
- children: isLoading ? <SkeletonSwitch /> : <Switch loading={configUpdating} />,
303
- desc: t('providerModels.config.fetchOnClient.desc'),
304
- label: t('providerModels.config.fetchOnClient.title'),
305
- minWidth: undefined,
306
- name: 'fetchOnClient',
307
- };
347
+
348
+ const clientFetchItem = showClientFetch
349
+ ? {
350
+ children: isLoading ? <SkeletonSwitch /> : <Switch loading={configUpdating} />,
351
+ desc: t('providerModels.config.fetchOnClient.desc'),
352
+ label: t('providerModels.config.fetchOnClient.title'),
353
+ minWidth: undefined,
354
+ name: 'fetchOnClient',
355
+ }
356
+ : undefined;
308
357
 
309
358
  const configItems = [
310
359
  ...apiKeyItem,
311
360
  endpointItem,
312
361
  supportResponsesApi
313
362
  ? {
314
- children: isLoading ? (
315
- <Skeleton.Button active />
316
- ) : (
317
- <Switch loading={configUpdating} value={enableResponseApi} />
318
- ),
363
+ children: isLoading ? <Skeleton.Button active /> : <Switch loading={configUpdating} />,
319
364
  desc: t('providerModels.config.responsesApi.desc'),
320
365
  label: t('providerModels.config.responsesApi.title'),
321
366
  minWidth: undefined,
@@ -364,7 +409,7 @@ const ProviderConfig = memo<ProviderConfigProps>(
364
409
 
365
410
  {isCustom && <UpdateProviderInfo />}
366
411
  {canDeactivate && !(ENABLE_BUSINESS_FEATURES && id === 'lobehub') && (
367
- <EnableSwitch id={id} />
412
+ <EnableSwitch id={id} key={id} />
368
413
  )}
369
414
  </Flexbox>
370
415
  ),
@@ -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`
@@ -21,7 +21,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
21
21
  chatInputEditorRef,
22
22
  onMarkdownContentChange,
23
23
  mentionItems,
24
- allowExpand,
24
+ allowExpand = true,
25
25
  }) => {
26
26
  const editor = useEditor();
27
27
  const slashMenuRef = useRef<HTMLDivElement>(null);