@lobehub/lobehub 2.0.0-next.231 → 2.0.0-next.232

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.232](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.231...v2.0.0-next.232)
6
+
7
+ <sup>Released on **2026-01-07**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Correct BrandTextLoading position after removing SSG CSS-in-JS injection.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Correct BrandTextLoading position after removing SSG CSS-in-JS injection, closes [#11312](https://github.com/lobehub/lobe-chat/issues/11312) ([0de4eb8](https://github.com/lobehub/lobe-chat/commit/0de4eb8))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.231](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.230...v2.0.0-next.231)
6
31
 
7
32
  <sup>Released on **2026-01-07**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Correct BrandTextLoading position after removing SSG CSS-in-JS injection."
6
+ ]
7
+ },
8
+ "date": "2026-01-07",
9
+ "version": "2.0.0-next.232"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.231",
3
+ "version": "2.0.0-next.232",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -0,0 +1,81 @@
1
+ .container {
2
+ --brand-text-color: var(--colorText, #1f1f1f);
3
+ --brand-muted-color: var(--colorTextSecondary, #8c8c8c);
4
+ --brand-border-color: var(--colorBorder, #d9d9d9);
5
+ --brand-tag-bg: var(--colorFillTertiary, rgba(0, 0, 0, 0.04));
6
+
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+
12
+ width: 100%;
13
+ height: 100vh;
14
+ height: 100dvh;
15
+ gap: 12px;
16
+ }
17
+
18
+ [data-theme='dark'] .container {
19
+ --brand-text-color: var(--colorText, #f0f0f0);
20
+ --brand-muted-color: var(--colorTextSecondary, #a6a6a6);
21
+ --brand-border-color: var(--colorBorder, #424242);
22
+ --brand-tag-bg: var(--colorFillTertiary, rgba(255, 255, 255, 0.08));
23
+ }
24
+
25
+ .brand {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 12px;
29
+
30
+ opacity: 0.6;
31
+ color: var(--brand-text-color);
32
+ }
33
+
34
+ .brand :global(.lobe-brand-loading) {
35
+ display: block;
36
+ color: inherit;
37
+ }
38
+
39
+ .debug {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: center;
43
+ gap: 4px;
44
+
45
+ padding: 16px;
46
+ }
47
+
48
+ .debugRow {
49
+ display: flex;
50
+ gap: 8px;
51
+ align-items: center;
52
+
53
+ font-size: 12px;
54
+ color: var(--brand-muted-color);
55
+ }
56
+
57
+ .debugRow code {
58
+ font-family:
59
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
60
+ monospace;
61
+ }
62
+
63
+ .debugTag {
64
+ display: inline-flex;
65
+ align-items: center;
66
+
67
+ padding: 2px 8px;
68
+ border: 1px solid var(--brand-border-color);
69
+ border-radius: 6px;
70
+
71
+ background: var(--brand-tag-bg);
72
+ }
73
+
74
+ .debugTag code {
75
+ color: var(--brand-text-color);
76
+ }
77
+
78
+ .debugHint {
79
+ font-size: 12px;
80
+ color: var(--brand-muted-color);
81
+ }
@@ -1,34 +1,41 @@
1
- import { Center, Tag, Text } from '@lobehub/ui';
2
1
  import { BrandLoading, LobeHubText } from '@lobehub/ui/brand';
3
2
 
4
3
  import { isCustomBranding } from '@/const/version';
5
4
 
6
5
  import CircleLoading from '../CircleLoading';
6
+ import styles from './index.module.css';
7
7
 
8
8
  interface BrandTextLoadingProps {
9
9
  debugId: string;
10
10
  }
11
11
 
12
12
  const BrandTextLoading = ({ debugId }: BrandTextLoadingProps) => {
13
- if (isCustomBranding) return <CircleLoading />;
13
+ if (isCustomBranding)
14
+ return (
15
+ <div className={styles.container}>
16
+ <CircleLoading />
17
+ </div>
18
+ );
19
+
20
+ const showDebug = process.env.NODE_ENV === 'development' && debugId;
14
21
 
15
22
  return (
16
- <Center height={'100%'} width={'100%'}>
17
- <BrandLoading size={40} style={{ opacity: 0.6 }} text={LobeHubText} />
18
- {process.env.NODE_ENV === 'development' && debugId && (
19
- <Center gap={4} padding={16}>
20
- <Text code style={{ alignItems: 'center', display: 'flex' }}>
21
- Debug ID:{' '}
22
- <Tag size={'large'}>
23
- <Text code>{debugId}</Text>
24
- </Tag>
25
- </Text>
26
- <Text fontSize={12} type={'secondary'}>
27
- only visible in development
28
- </Text>
29
- </Center>
23
+ <div className={styles.container}>
24
+ <div aria-label="Loading" className={styles.brand} role="status">
25
+ <BrandLoading size={40} text={LobeHubText} />
26
+ </div>
27
+ {showDebug && (
28
+ <div className={styles.debug}>
29
+ <div className={styles.debugRow}>
30
+ <code>Debug ID:</code>
31
+ <span className={styles.debugTag}>
32
+ <code>{debugId}</code>
33
+ </span>
34
+ </div>
35
+ <div className={styles.debugHint}>only visible in development</div>
36
+ </div>
30
37
  )}
31
- </Center>
38
+ </div>
32
39
  );
33
40
  };
34
41
 
@@ -94,3 +94,66 @@ export const createRedisWithPrefix = async (
94
94
  await provider.initialize();
95
95
  return provider;
96
96
  };
97
+
98
+ /**
99
+ * Manages singleton Redis clients per prefix
100
+ */
101
+ class PrefixedRedisManager {
102
+ private static instances = new Map<string, BaseRedisProvider>();
103
+ private static initPromises = new Map<string, Promise<BaseRedisProvider | null>>();
104
+
105
+ static async initialize(config: RedisConfig, prefix: string): Promise<BaseRedisProvider | null> {
106
+ const existing = this.instances.get(prefix);
107
+ if (existing) return existing;
108
+
109
+ const pendingPromise = this.initPromises.get(prefix);
110
+ if (pendingPromise) return pendingPromise;
111
+
112
+ const initPromise = (async () => {
113
+ const provider = createProvider(config, prefix);
114
+ if (!provider) return null;
115
+
116
+ await provider.initialize();
117
+ this.instances.set(prefix, provider);
118
+ return provider;
119
+ })().catch((error) => {
120
+ this.initPromises.delete(prefix);
121
+ throw error;
122
+ });
123
+
124
+ this.initPromises.set(prefix, initPromise);
125
+ return initPromise;
126
+ }
127
+
128
+ static async reset(prefix?: string) {
129
+ if (prefix) {
130
+ const instance = this.instances.get(prefix);
131
+ if (instance) {
132
+ await instance.disconnect();
133
+ this.instances.delete(prefix);
134
+ this.initPromises.delete(prefix);
135
+ }
136
+ } else {
137
+ for (const instance of this.instances.values()) {
138
+ await instance.disconnect();
139
+ }
140
+ this.instances.clear();
141
+ this.initPromises.clear();
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Initialize a singleton Redis client with custom prefix
148
+ *
149
+ * Unlike createRedisWithPrefix, this reuses the same client for each prefix,
150
+ * avoiding connection leaks when called frequently.
151
+ *
152
+ * @param config - Redis config
153
+ * @param prefix - Custom prefix for all keys (e.g., 'aiGeneration')
154
+ * @returns Redis client or null if Redis is disabled
155
+ */
156
+ export const initializeRedisWithPrefix = (config: RedisConfig, prefix: string) =>
157
+ PrefixedRedisManager.initialize(config, prefix);
158
+
159
+ export const resetPrefixedRedisClient = (prefix?: string) => PrefixedRedisManager.reset(prefix);
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
5
  import { AgentModel } from '@/database/models/agent';
6
6
  import { SessionModel } from '@/database/models/session';
7
7
  import { UserModel } from '@/database/models/user';
8
- import { RedisKeys, createRedisWithPrefix } from '@/libs/redis';
8
+ import { RedisKeys, initializeRedisWithPrefix, isRedisEnabled } from '@/libs/redis';
9
9
  import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
10
10
 
11
11
  import { AgentService } from './index';
@@ -43,7 +43,8 @@ vi.mock('@/libs/redis', async (importOriginal) => {
43
43
  const original = await importOriginal<typeof import('@/libs/redis')>();
44
44
  return {
45
45
  ...original,
46
- createRedisWithPrefix: vi.fn(),
46
+ initializeRedisWithPrefix: vi.fn(),
47
+ isRedisEnabled: vi.fn(),
47
48
  };
48
49
  });
49
50
 
@@ -290,7 +291,8 @@ describe('AgentService', () => {
290
291
  const mockRedisClient = { get: mockRedisGet };
291
292
 
292
293
  beforeEach(() => {
293
- vi.mocked(createRedisWithPrefix).mockReset();
294
+ vi.mocked(initializeRedisWithPrefix).mockReset();
295
+ vi.mocked(isRedisEnabled).mockReset();
294
296
  mockRedisGet.mockReset();
295
297
  });
296
298
 
@@ -310,7 +312,8 @@ describe('AgentService', () => {
310
312
 
311
313
  (AgentModel as any).mockImplementation(() => mockAgentModel);
312
314
  (parseAgentConfig as any).mockReturnValue({});
313
- vi.mocked(createRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
315
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
316
+ vi.mocked(initializeRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
314
317
  mockRedisGet.mockResolvedValue(JSON.stringify(welcomeData));
315
318
 
316
319
  const newService = new AgentService(mockDb, mockUserId);
@@ -334,7 +337,7 @@ describe('AgentService', () => {
334
337
 
335
338
  (AgentModel as any).mockImplementation(() => mockAgentModel);
336
339
  (parseAgentConfig as any).mockReturnValue({});
337
- vi.mocked(createRedisWithPrefix).mockResolvedValue(null);
340
+ vi.mocked(isRedisEnabled).mockReturnValue(false);
338
341
 
339
342
  const newService = new AgentService(mockDb, mockUserId);
340
343
  const result = await newService.getAgentConfigById('agent-1');
@@ -343,6 +346,7 @@ describe('AgentService', () => {
343
346
  expect(result?.openingMessage).toBe('Default message');
344
347
  // openingQuestions comes from DEFAULT_AGENT_CONFIG (empty array)
345
348
  expect(result?.openingQuestions).toEqual([]);
349
+ expect(initializeRedisWithPrefix).not.toHaveBeenCalled();
346
350
  });
347
351
 
348
352
  it('should return normal config when Redis key does not exist', async () => {
@@ -357,7 +361,8 @@ describe('AgentService', () => {
357
361
 
358
362
  (AgentModel as any).mockImplementation(() => mockAgentModel);
359
363
  (parseAgentConfig as any).mockReturnValue({});
360
- vi.mocked(createRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
364
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
365
+ vi.mocked(initializeRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
361
366
  mockRedisGet.mockResolvedValue(null);
362
367
 
363
368
  const newService = new AgentService(mockDb, mockUserId);
@@ -381,7 +386,8 @@ describe('AgentService', () => {
381
386
 
382
387
  (AgentModel as any).mockImplementation(() => mockAgentModel);
383
388
  (parseAgentConfig as any).mockReturnValue({});
384
- vi.mocked(createRedisWithPrefix).mockRejectedValue(new Error('Redis connection failed'));
389
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
390
+ vi.mocked(initializeRedisWithPrefix).mockRejectedValue(new Error('Redis connection failed'));
385
391
 
386
392
  const newService = new AgentService(mockDb, mockUserId);
387
393
  const result = await newService.getAgentConfigById('agent-1');
@@ -403,7 +409,8 @@ describe('AgentService', () => {
403
409
 
404
410
  (AgentModel as any).mockImplementation(() => mockAgentModel);
405
411
  (parseAgentConfig as any).mockReturnValue({});
406
- vi.mocked(createRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
412
+ vi.mocked(isRedisEnabled).mockReturnValue(true);
413
+ vi.mocked(initializeRedisWithPrefix).mockResolvedValue(mockRedisClient as any);
407
414
  mockRedisGet.mockResolvedValue('invalid json {');
408
415
 
409
416
  const newService = new AgentService(mockDb, mockUserId);
@@ -3,17 +3,20 @@ import { DEFAULT_AGENT_CONFIG } from '@lobechat/const';
3
3
  import { type LobeChatDatabase } from '@lobechat/database';
4
4
  import { type AgentItem, type LobeAgentConfig } from '@lobechat/types';
5
5
  import { cleanObject, merge } from '@lobechat/utils';
6
+ import debug from 'debug';
6
7
  import type { PartialDeep } from 'type-fest';
7
8
 
8
9
  import { AgentModel } from '@/database/models/agent';
9
10
  import { SessionModel } from '@/database/models/session';
10
11
  import { UserModel } from '@/database/models/user';
11
12
  import { getRedisConfig } from '@/envs/redis';
12
- import { RedisKeyNamespace, RedisKeys, createRedisWithPrefix } from '@/libs/redis';
13
+ import { RedisKeyNamespace, RedisKeys, initializeRedisWithPrefix, isRedisEnabled } from '@/libs/redis';
13
14
  import { getServerDefaultAgentConfig } from '@/server/globalConfig';
14
15
 
15
16
  import { type UpdateAgentResult } from './type';
16
17
 
18
+ const log = debug('lobe-agent:service');
19
+
17
20
  interface AgentWelcomeData {
18
21
  openQuestions: string[];
19
22
  welcomeMessage: string;
@@ -113,7 +116,10 @@ export class AgentService {
113
116
  */
114
117
  private async getAgentWelcomeFromRedis(agentId: string): Promise<AgentWelcomeData | null> {
115
118
  try {
116
- const redis = await createRedisWithPrefix(getRedisConfig(), RedisKeyNamespace.AI_GENERATION);
119
+ const redisConfig = getRedisConfig();
120
+ if (!isRedisEnabled(redisConfig)) return null;
121
+
122
+ const redis = await initializeRedisWithPrefix(redisConfig, RedisKeyNamespace.AI_GENERATION);
117
123
  if (!redis) return null;
118
124
 
119
125
  const key = RedisKeys.aiGeneration.agentWelcome(agentId);
@@ -121,8 +127,9 @@ export class AgentService {
121
127
  if (!value) return null;
122
128
 
123
129
  return JSON.parse(value) as AgentWelcomeData;
124
- } catch {
125
- // Silently fail - Redis errors shouldn't break agent retrieval
130
+ } catch (error) {
131
+ // Log error for observability but don't break agent retrieval
132
+ log('Failed to get agent welcome from Redis for agent %s: %O', agentId, error);
126
133
  return null;
127
134
  }
128
135
  }