@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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/components/Loading/BrandTextLoading/index.module.css +81 -0
- package/src/components/Loading/BrandTextLoading/index.tsx +24 -17
- package/src/libs/redis/manager.ts +63 -0
- package/src/server/services/agent/index.test.ts +15 -8
- package/src/server/services/agent/index.ts +11 -4
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
|
+
[](#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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
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)
|
|
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
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</
|
|
29
|
-
</
|
|
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
|
-
</
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
}
|