@lobehub/chat 1.133.1 → 1.133.3

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 (130) hide show
  1. package/.cursor/rules/project-introduce.mdc +19 -25
  2. package/.cursor/rules/project-structure.mdc +102 -221
  3. package/.cursor/rules/{rules-attach.mdc → rules-index.mdc} +2 -11
  4. package/.cursor/rules/typescript.mdc +3 -53
  5. package/.vscode/settings.json +2 -1
  6. package/AGENTS.md +33 -54
  7. package/CHANGELOG.md +58 -0
  8. package/CLAUDE.md +1 -26
  9. package/changelog/v1.json +21 -0
  10. package/locales/ar/chat.json +5 -0
  11. package/locales/ar/image.json +7 -0
  12. package/locales/ar/models.json +2 -2
  13. package/locales/bg-BG/chat.json +5 -0
  14. package/locales/bg-BG/image.json +7 -0
  15. package/locales/de-DE/chat.json +5 -0
  16. package/locales/de-DE/image.json +7 -0
  17. package/locales/en-US/chat.json +5 -0
  18. package/locales/en-US/image.json +7 -0
  19. package/locales/es-ES/chat.json +5 -0
  20. package/locales/es-ES/image.json +7 -0
  21. package/locales/es-ES/tool.json +1 -1
  22. package/locales/fa-IR/chat.json +5 -0
  23. package/locales/fa-IR/image.json +7 -0
  24. package/locales/fa-IR/models.json +2 -2
  25. package/locales/fr-FR/chat.json +5 -0
  26. package/locales/fr-FR/image.json +7 -0
  27. package/locales/fr-FR/models.json +2 -2
  28. package/locales/it-IT/chat.json +5 -0
  29. package/locales/it-IT/image.json +7 -0
  30. package/locales/ja-JP/chat.json +5 -0
  31. package/locales/ja-JP/image.json +7 -0
  32. package/locales/ko-KR/chat.json +5 -0
  33. package/locales/ko-KR/image.json +7 -0
  34. package/locales/nl-NL/chat.json +5 -0
  35. package/locales/nl-NL/image.json +7 -0
  36. package/locales/pl-PL/chat.json +5 -0
  37. package/locales/pl-PL/image.json +7 -0
  38. package/locales/pt-BR/chat.json +5 -0
  39. package/locales/pt-BR/image.json +7 -0
  40. package/locales/ru-RU/chat.json +5 -0
  41. package/locales/ru-RU/image.json +7 -0
  42. package/locales/ru-RU/tool.json +1 -1
  43. package/locales/tr-TR/chat.json +5 -0
  44. package/locales/tr-TR/image.json +7 -0
  45. package/locales/tr-TR/models.json +2 -2
  46. package/locales/vi-VN/chat.json +5 -0
  47. package/locales/vi-VN/image.json +7 -0
  48. package/locales/zh-CN/chat.json +5 -0
  49. package/locales/zh-CN/image.json +7 -0
  50. package/locales/zh-TW/chat.json +5 -0
  51. package/locales/zh-TW/image.json +7 -0
  52. package/package.json +4 -5
  53. package/packages/const/package.json +4 -0
  54. package/packages/const/src/currency.ts +2 -0
  55. package/packages/const/src/index.ts +1 -0
  56. package/packages/model-bank/package.json +2 -1
  57. package/packages/model-bank/src/aiModels/google.ts +6 -0
  58. package/packages/model-bank/src/aiModels/openai.ts +6 -22
  59. package/packages/model-bank/src/standard-parameters/index.ts +56 -46
  60. package/packages/model-runtime/package.json +1 -0
  61. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +4 -2
  62. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +12 -2
  63. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +16 -5
  64. package/packages/model-runtime/src/core/streams/anthropic.ts +25 -36
  65. package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +1 -1
  66. package/packages/model-runtime/src/core/streams/google/index.ts +18 -42
  67. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +7 -10
  68. package/packages/model-runtime/src/core/streams/openai/openai.ts +14 -11
  69. package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +11 -5
  70. package/packages/model-runtime/src/core/streams/protocol.ts +25 -6
  71. package/packages/model-runtime/src/core/streams/qwen.ts +2 -2
  72. package/packages/model-runtime/src/core/streams/spark.ts +3 -3
  73. package/packages/model-runtime/src/core/streams/vertex-ai.test.ts +2 -2
  74. package/packages/model-runtime/src/core/streams/vertex-ai.ts +14 -23
  75. package/packages/model-runtime/src/core/usageConverters/anthropic.test.ts +99 -0
  76. package/packages/model-runtime/src/core/usageConverters/anthropic.ts +73 -0
  77. package/packages/model-runtime/src/core/usageConverters/google-ai.test.ts +88 -0
  78. package/packages/model-runtime/src/core/usageConverters/google-ai.ts +55 -0
  79. package/packages/model-runtime/src/core/usageConverters/index.ts +4 -0
  80. package/packages/model-runtime/src/core/usageConverters/openai.test.ts +429 -0
  81. package/packages/model-runtime/src/core/usageConverters/openai.ts +152 -0
  82. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +455 -0
  83. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +293 -0
  84. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +47 -0
  85. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.ts +121 -0
  86. package/packages/model-runtime/src/core/usageConverters/utils/index.ts +11 -0
  87. package/packages/model-runtime/src/core/usageConverters/utils/withUsageCost.ts +19 -0
  88. package/packages/model-runtime/src/index.ts +2 -0
  89. package/packages/model-runtime/src/providers/anthropic/index.ts +48 -1
  90. package/packages/model-runtime/src/providers/google/createImage.ts +11 -2
  91. package/packages/model-runtime/src/providers/google/index.ts +8 -1
  92. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +7 -0
  93. package/packages/model-runtime/src/providers/zhipu/index.ts +3 -1
  94. package/packages/model-runtime/src/types/chat.ts +5 -3
  95. package/packages/model-runtime/src/types/image.ts +20 -9
  96. package/packages/model-runtime/src/utils/getModelPricing.ts +36 -0
  97. package/packages/obervability-otel/package.json +2 -2
  98. package/packages/ssrf-safe-fetch/index.test.ts +343 -0
  99. package/packages/ssrf-safe-fetch/index.ts +37 -0
  100. package/packages/ssrf-safe-fetch/package.json +17 -0
  101. package/packages/ssrf-safe-fetch/vitest.config.mts +10 -0
  102. package/packages/types/src/message/base.ts +43 -17
  103. package/packages/utils/src/client/apiKeyManager.test.ts +70 -0
  104. package/packages/utils/src/client/apiKeyManager.ts +41 -0
  105. package/packages/utils/src/client/index.ts +2 -0
  106. package/packages/utils/src/fetch/fetchSSE.ts +4 -4
  107. package/packages/utils/src/index.ts +1 -0
  108. package/packages/utils/src/toolManifest.ts +2 -1
  109. package/src/app/(backend)/webapi/proxy/route.ts +2 -13
  110. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/default.tsx +2 -0
  111. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +335 -0
  112. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +4 -0
  113. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx +23 -0
  114. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
  115. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +13 -13
  116. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +1 -1
  117. package/src/features/Conversation/components/ChatItem/index.tsx +56 -2
  118. package/src/features/Conversation/components/VirtualizedList/VirtuosoContext.ts +88 -0
  119. package/src/features/Conversation/components/VirtualizedList/index.tsx +15 -1
  120. package/src/locales/default/chat.ts +5 -0
  121. package/src/locales/default/image.ts +7 -0
  122. package/src/server/modules/EdgeConfig/index.ts +1 -1
  123. package/src/server/routers/async/image.ts +9 -1
  124. package/src/services/_auth.ts +12 -12
  125. package/src/services/chat/contextEngineering.ts +2 -3
  126. package/.cursor/rules/backend-architecture.mdc +0 -176
  127. package/.cursor/rules/code-review.mdc +0 -58
  128. package/.cursor/rules/cursor-ux.mdc +0 -32
  129. package/.cursor/rules/define-database-model.mdc +0 -8
  130. package/.cursor/rules/system-role.mdc +0 -31
@@ -2,14 +2,27 @@
2
2
 
3
3
  import { createStyles } from 'antd-style';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { MouseEventHandler, ReactNode, memo, use, useCallback, useMemo } from 'react';
5
+ import {
6
+ MouseEventHandler,
7
+ ReactNode,
8
+ memo,
9
+ use,
10
+ useCallback,
11
+ useEffect,
12
+ useMemo,
13
+ useRef,
14
+ } from 'react';
6
15
  import { useTranslation } from 'react-i18next';
7
16
  import { Flexbox } from 'react-layout-kit';
8
17
 
9
18
  import { HtmlPreviewAction } from '@/components/HtmlPreview';
10
19
  import { isDesktop } from '@/const/version';
11
20
  import ChatItem from '@/features/ChatItem';
12
- import { VirtuosoContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
21
+ import {
22
+ VirtuosoContext,
23
+ removeVirtuosoVisibleItem,
24
+ upsertVirtuosoVisibleItem,
25
+ } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
13
26
  import { useAgentStore } from '@/store/agent';
14
27
  import { agentChatConfigSelectors } from '@/store/agent/selectors';
15
28
  import { useChatStore } from '@/store/chat';
@@ -79,6 +92,7 @@ const Item = memo<ChatListItemProps>(
79
92
  }) => {
80
93
  const { t } = useTranslation('common');
81
94
  const { styles, cx } = useStyles();
95
+ const containerRef = useRef<HTMLDivElement | null>(null);
82
96
 
83
97
  const type = useAgentStore(agentChatConfigSelectors.displayMode);
84
98
  const item = useChatStore(chatSelectors.getMessageById(id), isEqual);
@@ -218,6 +232,44 @@ const Item = memo<ChatListItemProps>(
218
232
  const onChange = useCallback((value: string) => updateMessageContent(id, value), [id]);
219
233
  const virtuosoRef = use(VirtuosoContext);
220
234
 
235
+ useEffect(() => {
236
+ if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') return;
237
+
238
+ const element = containerRef.current;
239
+ if (!element) return;
240
+
241
+ const root = element.closest('[data-virtuoso-scroller]');
242
+ const thresholds = [0, 0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 1];
243
+ const options: any = { threshold: thresholds };
244
+
245
+ if (root instanceof Element) options.root = root;
246
+
247
+ const observer = new IntersectionObserver((entries) => {
248
+ entries.forEach((entry) => {
249
+ if (entry.target !== element) return;
250
+
251
+ if (entry.isIntersecting) {
252
+ const { bottom, top } = entry.intersectionRect;
253
+
254
+ upsertVirtuosoVisibleItem(index, {
255
+ bottom,
256
+ ratio: entry.intersectionRatio,
257
+ top,
258
+ });
259
+ } else {
260
+ removeVirtuosoVisibleItem(index);
261
+ }
262
+ });
263
+ }, options);
264
+
265
+ observer.observe(element);
266
+
267
+ return () => {
268
+ observer.disconnect();
269
+ removeVirtuosoVisibleItem(index);
270
+ };
271
+ }, [index]);
272
+
221
273
  const onDoubleClick = useCallback<MouseEventHandler<HTMLDivElement>>(
222
274
  (e) => {
223
275
  if (!item || disableEditing) return;
@@ -267,7 +319,9 @@ const Item = memo<ChatListItemProps>(
267
319
  {enableHistoryDivider && <History />}
268
320
  <Flexbox
269
321
  className={cx(styles.message, className, isMessageLoading && styles.loading)}
322
+ data-index={index}
270
323
  onContextMenu={onContextMenu}
324
+ ref={containerRef}
271
325
  >
272
326
  <ChatItem
273
327
  actions={actionBar}
@@ -2,3 +2,91 @@ import { RefObject, createContext } from 'react';
2
2
  import { VirtuosoHandle } from 'react-virtuoso';
3
3
 
4
4
  export const VirtuosoContext = createContext<RefObject<VirtuosoHandle | null> | null>(null);
5
+
6
+ type VirtuosoRef = RefObject<VirtuosoHandle | null> | null;
7
+
8
+ let currentVirtuosoRef: VirtuosoRef = null;
9
+ const refListeners = new Set<() => void>();
10
+
11
+ const visibleItems = new Map<number, { bottom: number; ratio: number; top: number }>();
12
+ let currentActiveIndex: number | null = null;
13
+ const activeIndexListeners = new Set<() => void>();
14
+
15
+ const notifyActiveIndex = (next: number | null) => {
16
+ if (currentActiveIndex === next) return;
17
+
18
+ currentActiveIndex = next;
19
+ activeIndexListeners.forEach((listener) => listener());
20
+ };
21
+
22
+ const recalculateActiveIndex = () => {
23
+ if (visibleItems.size === 0) {
24
+ notifyActiveIndex(null);
25
+ return;
26
+ }
27
+
28
+ let candidate: number | null = null;
29
+ let minTop = Infinity;
30
+ let maxRatio = -Infinity;
31
+
32
+ visibleItems.forEach(({ top, ratio }, index) => {
33
+ const shouldUpdate =
34
+ top < minTop ||
35
+ (top === minTop &&
36
+ (ratio > maxRatio || (ratio === maxRatio && index < (candidate ?? Infinity))));
37
+
38
+ if (shouldUpdate) {
39
+ candidate = index;
40
+ minTop = top;
41
+ maxRatio = ratio;
42
+ }
43
+ });
44
+
45
+ notifyActiveIndex(candidate ?? null);
46
+ };
47
+
48
+ export const setVirtuosoGlobalRef = (ref: VirtuosoRef) => {
49
+ currentVirtuosoRef = ref;
50
+ refListeners.forEach((listener) => listener());
51
+ };
52
+
53
+ export const getVirtuosoGlobalRef = () => currentVirtuosoRef;
54
+
55
+ export const subscribeVirtuosoGlobalRef = (listener: () => void) => {
56
+ refListeners.add(listener);
57
+
58
+ return () => {
59
+ refListeners.delete(listener);
60
+ };
61
+ };
62
+
63
+ export const upsertVirtuosoVisibleItem = (
64
+ index: number,
65
+ metrics: { bottom: number; ratio: number; top: number },
66
+ ) => {
67
+ visibleItems.set(index, metrics);
68
+ recalculateActiveIndex();
69
+ };
70
+
71
+ export const removeVirtuosoVisibleItem = (index: number) => {
72
+ if (!visibleItems.delete(index)) return;
73
+
74
+ recalculateActiveIndex();
75
+ };
76
+
77
+ export const resetVirtuosoVisibleItems = () => {
78
+ if (visibleItems.size === 0 && currentActiveIndex === null) return;
79
+
80
+ visibleItems.clear();
81
+ notifyActiveIndex(null);
82
+ };
83
+
84
+ export const getVirtuosoActiveIndex = () => currentActiveIndex;
85
+
86
+ export const subscribeVirtuosoActiveIndex = (listener: () => void) => {
87
+ activeIndexListeners.add(listener);
88
+
89
+ return () => {
90
+ activeIndexListeners.delete(listener);
91
+ };
92
+ };
@@ -10,7 +10,7 @@ import { chatSelectors } from '@/store/chat/selectors';
10
10
 
11
11
  import AutoScroll from '../AutoScroll';
12
12
  import SkeletonList from '../SkeletonList';
13
- import { VirtuosoContext } from './VirtuosoContext';
13
+ import { VirtuosoContext, resetVirtuosoVisibleItems, setVirtuosoGlobalRef } from './VirtuosoContext';
14
14
 
15
15
  interface VirtualizedListProps {
16
16
  dataSource: string[];
@@ -57,6 +57,20 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile, dataSource, itemCo
57
57
  scrollToBottom();
58
58
  }, [id]);
59
59
 
60
+ useEffect(() => {
61
+ setVirtuosoGlobalRef(virtuosoRef);
62
+
63
+ return () => {
64
+ setVirtuosoGlobalRef(null);
65
+ };
66
+ }, [virtuosoRef]);
67
+
68
+ useEffect(() => {
69
+ return () => {
70
+ resetVirtuosoVisibleItems();
71
+ };
72
+ }, []);
73
+
60
74
  // overscan should be 3 times the height of the window
61
75
  const overscan = typeof window !== 'undefined' ? window.innerHeight * 3 : 0;
62
76
 
@@ -154,6 +154,11 @@ export default {
154
154
  total: '总计消耗',
155
155
  },
156
156
  },
157
+ minimap: {
158
+ jumpToMessage: '跳转至第 {{index}} 条消息',
159
+ nextMessage: '下一条消息',
160
+ previousMessage: '上一条消息',
161
+ },
157
162
  newAgent: '新建助手',
158
163
  pin: '置顶',
159
164
  pinOff: '取消置顶',
@@ -30,6 +30,13 @@ export default {
30
30
  prompt: {
31
31
  placeholder: '描述你想要生成的内容',
32
32
  },
33
+ quality: {
34
+ label: '图片质量',
35
+ options: {
36
+ hd: '高清',
37
+ standard: '标准',
38
+ },
39
+ },
33
40
  seed: {
34
41
  label: '种子',
35
42
  random: '随机种子',
@@ -2,7 +2,7 @@ import { EdgeConfigClient, createClient } from '@vercel/edge-config';
2
2
 
3
3
  import { appEnv } from '@/envs/app';
4
4
 
5
- import { EdgeConfigData } from './types';
5
+ import type { EdgeConfigData } from './types';
6
6
 
7
7
  export class EdgeConfig {
8
8
  get client(): EdgeConfigClient {
@@ -134,13 +134,14 @@ export const imageRouter = router({
134
134
  try {
135
135
  const imageGenerationPromise = async (signal: AbortSignal) => {
136
136
  log('Initializing agent runtime for provider: %s', provider);
137
+
137
138
  const agentRuntime = await initModelRuntimeWithUserPayload(provider, ctx.jwtPayload);
138
139
 
139
140
  // Check if operation has been cancelled
140
141
  checkAbortSignal(signal);
141
142
 
142
143
  log('Agent runtime initialized, calling createImage');
143
- const response = await agentRuntime.createImage({
144
+ const response = await agentRuntime.createImage!({
144
145
  model,
145
146
  params: params as unknown as RuntimeImageGenParams,
146
147
  });
@@ -150,6 +151,13 @@ export const imageRouter = router({
150
151
  throw new Error('Create image response is empty');
151
152
  }
152
153
 
154
+ log('Create image response: %O', {
155
+ ...response,
156
+ imageUrl: response.imageUrl?.startsWith('data:')
157
+ ? response.imageUrl.slice(0, IMAGE_URL_PREVIEW_LENGTH) + '...'
158
+ : response.imageUrl,
159
+ });
160
+
153
161
  // Check if operation has been cancelled
154
162
  checkAbortSignal(signal);
155
163
 
@@ -1,17 +1,17 @@
1
- import { ClientSecretPayload } from '@lobechat/types';
2
- import { ModelProvider } from 'model-bank';
3
-
4
- import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
5
- import { isDeprecatedEdition } from '@/const/version';
6
- import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
7
- import { useUserStore } from '@/store/user';
8
- import { keyVaultsConfigSelectors, userProfileSelectors } from '@/store/user/selectors';
1
+ import { LOBE_CHAT_AUTH_HEADER, isDeprecatedEdition } from '@lobechat/const';
9
2
  import {
10
3
  AWSBedrockKeyVault,
11
4
  AzureOpenAIKeyVault,
5
+ ClientSecretPayload,
12
6
  CloudflareKeyVault,
13
7
  OpenAICompatibleKeyVault,
14
- } from '@/types/user/settings';
8
+ } from '@lobechat/types';
9
+ import { clientApiKeyManager } from '@lobechat/utils/client';
10
+ import { ModelProvider } from 'model-bank';
11
+
12
+ import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
13
+ import { useUserStore } from '@/store/user';
14
+ import { keyVaultsConfigSelectors, userProfileSelectors } from '@/store/user/selectors';
15
15
  import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
16
16
 
17
17
  export const getProviderAuthPayload = (
@@ -49,7 +49,7 @@ export const getProviderAuthPayload = (
49
49
 
50
50
  case ModelProvider.Azure: {
51
51
  return {
52
- apiKey: keyVaults.apiKey,
52
+ apiKey: clientApiKeyManager.pick(keyVaults.apiKey),
53
53
 
54
54
  apiVersion: keyVaults.apiVersion,
55
55
  /** @deprecated */
@@ -64,7 +64,7 @@ export const getProviderAuthPayload = (
64
64
 
65
65
  case ModelProvider.Cloudflare: {
66
66
  return {
67
- apiKey: keyVaults?.apiKey,
67
+ apiKey: clientApiKeyManager.pick(keyVaults?.apiKey),
68
68
 
69
69
  baseURLOrAccountID: keyVaults?.baseURLOrAccountID,
70
70
  /** @deprecated */
@@ -73,7 +73,7 @@ export const getProviderAuthPayload = (
73
73
  }
74
74
 
75
75
  default: {
76
- return { apiKey: keyVaults?.apiKey, baseURL: keyVaults?.baseURL };
76
+ return { apiKey: clientApiKeyManager.pick(keyVaults?.apiKey), baseURL: keyVaults?.baseURL };
77
77
  }
78
78
  }
79
79
  };
@@ -1,4 +1,4 @@
1
- import { INBOX_SESSION_ID, isDesktop, isServerMode } from '@lobechat/const';
1
+ import { INBOX_GUIDE_SYSTEMROLE, INBOX_SESSION_ID, isDesktop, isServerMode } from '@lobechat/const';
2
2
  import {
3
3
  type AgentState,
4
4
  ContextEngine,
@@ -16,12 +16,11 @@ import {
16
16
  } from '@lobechat/context-engine';
17
17
  import { historySummaryPrompt } from '@lobechat/prompts';
18
18
  import { ChatMessage, OpenAIChatMessage } from '@lobechat/types';
19
+ import { VARIABLE_GENERATORS } from '@lobechat/utils/client';
19
20
 
20
- import { INBOX_GUIDE_SYSTEMROLE } from '@/const/guide';
21
21
  import { isCanUseFC } from '@/helpers/isCanUseFC';
22
22
  import { getToolStoreState } from '@/store/tool';
23
23
  import { toolSelectors } from '@/store/tool/selectors';
24
- import { VARIABLE_GENERATORS } from '@/utils/client/parserPlaceholder';
25
24
  import { genToolCallingName } from '@/utils/toolCall';
26
25
 
27
26
  import { isCanUseVideo, isCanUseVision } from './helper';
@@ -1,176 +0,0 @@
1
- ---
2
- description:
3
- globs: src/services/**/*,src/database/**/*,src/server/**/*
4
- alwaysApply: false
5
- ---
6
-
7
- # LobeChat 后端技术架构指南
8
-
9
- 本指南旨在阐述 LobeChat 项目的后端分层架构,重点介绍各核心目录的职责以及它们之间的协作方式。
10
-
11
- ## 目录结构映射
12
-
13
- ```
14
- src/
15
- ├── server/
16
- │ ├── routers/ # tRPC API 路由定义
17
- │ └── services/ # 业务逻辑服务层
18
- │ └── */impls/ # 平台特定实现
19
- ├── database/
20
- │ ├── models/ # 数据模型 (单表 CRUD)
21
- │ ├── repositories/ # 仓库层 (复杂查询/聚合)
22
- │ └── schemas/ # Drizzle ORM 表定义
23
- └── services/ # 客户端服务 (调用 tRPC 或直接访问 Model)
24
- ```
25
-
26
- ## 核心架构分层
27
-
28
- LobeChat 的后端设计注重模块化、可测试性和灵活性,以适应不同的运行环境(如浏览器端 PGLite、服务端远程 PostgreSQL 以及 Electron 桌面应用)。
29
-
30
- 其主要分层如下:
31
-
32
- 1. 客户端服务层 (`src/services`):
33
- - 位于 src/services/。
34
- - 这是客户端业务逻辑的核心层,负责封装各种业务操作和数据处理逻辑。
35
- - 环境适配: 根据不同的运行环境,服务层会选择合适的数据访问方式:
36
- - 本地数据库模式: 直接调用 `Model` 层进行数据操作,适用于浏览器 PGLite 和本地 Electron 应用。
37
- - 远程数据库模式: 通过 `tRPC` 客户端调用服务端 API,适用于需要云同步的场景。
38
- - 类型转换: 对于简单的数据类型转换,直接在此层进行类型断言,如 `this.pluginModel.query() as Promise<LobeTool[]>`
39
- - 每个服务模块通常包含 `client.ts`(本地模式)、`server.ts`(远程模式)和 `type.ts`(接口定义)文件,在实现时应该确保本地模式和远程模式业务逻辑实现一致,只是数据库不同。
40
-
41
- 2. API 接口层 (`TRPC`):
42
- - 位于 src/server/routers/
43
- - 使用 `tRPC` 构建类型安全的 API。Router 根据运行时环境(如 Edge Functions, Node.js Lambda)进行组织。
44
- - 负责接收客户端请求,并将其路由到相应的 `Service` 层进行处理。
45
- - 新建 lambda 端点时可以参考 src/server/routers/lambda/\_template.ts
46
-
47
- 3. 仓库层 (`Repositories`):
48
- - 位于 src/database/repositories/。
49
- - 主要处理复杂的跨表查询和数据聚合逻辑,特别是当需要从多个 `Model` 获取数据并进行组合时。
50
- - 与 `Model` 层不同,`Repository` 层专注于复杂的业务查询场景,而不涉及简单的领域模型转换。
51
- - 当业务逻辑涉及多表关联、复杂的数据统计或需要事务处理时,会使用 `Repository` 层。
52
- - 如果数据操作简单(仅涉及单个 `Model`),则通常直接在 `src/services` 层调用 `Model` 并进行简单的类型断言。
53
-
54
- 4. 模型层 (`Models`):
55
- - 位于 src/database/models/ (例如 src/database/models/plugin.ts 和 src/database/models/document.ts)。
56
- - 提供对数据库中各个表(由 src/database/schemas/ 中的 Drizzle ORM schema 定义)的基本 CRUD (创建、读取、更新、删除) 操作和简单的查询能力。
57
- - `Model` 类专注于单个数据表的直接操作,不涉及复杂的领域模型转换,这些转换通常在上层的 `src/services` 中通过类型断言完成。
58
- - model(例如 Topic) 层接口经常需要从对应的 schema 层导入 NewTopic 和 TopicItem
59
- - 创建新的 model 时可以参考 src/database/models/\_template.ts
60
-
61
- 5. 数据库 (`Database`):
62
- - 客户端模式 (浏览器/PWA): 使用 PGLite (基于 WASM 的 PostgreSQL),数据存储在用户浏览器本地。
63
- - 服务端模式 (云部署): 使用远程 PostgreSQL 数据库。
64
- - Electron 桌面应用:
65
- - Electron 客户端会启动一个本地 Node.js 服务。
66
- - 本地服务通过 `tRPC` 与 Electron 的渲染进程通信。
67
- - 数据库选择依赖于是否开启云同步功能:
68
- - 云同步开启: 连接到远程 PostgreSQL 数据库。
69
- - 云同步关闭: 使用 PGLite (通过 Node.js 的 WASM 实现) 在本地存储数据。
70
-
71
- ## 数据流向说明
72
-
73
- ### 浏览器/PWA 模式
74
-
75
- ```
76
- UI (React) → Zustand action -> Client Service → Model Layer → PGLite (本地数据库)
77
- ```
78
-
79
- ### 服务端模式
80
-
81
- ```
82
- UI (React) → Zustand action → Client Service -> TRPC Client → TRPC Routers → Repositories/Models → Remote PostgreSQL
83
- ```
84
-
85
- ### Electron 桌面应用模式
86
-
87
- ```
88
- UI (Electron Renderer) → Zustand action → Client Service -> TRPC Client → 本地 Node.js 服务 → TRPC Routers → Repositories/Models → PGLite/Remote PostgreSQL (取决于云同步设置)
89
- ```
90
-
91
- ## 服务层 (Server Services)
92
-
93
- - 位于 src/server/services/。
94
- - 核心职责是封装独立的、可复用的业务逻辑单元。这些服务应易于测试。
95
- - 平台差异抽象: 一个关键特性是通过其内部的 `impls` 子目录(例如 src/server/services/file/impls 包含 s3.ts 和 local.ts)来抹平不同运行环境带来的差异(例如云端使用 S3 存储,桌面版使用本地文件系统)。这使得上层(如 `tRPC` routers)无需关心底层具体实现。
96
- - 目标是使 `tRPC` router 层的逻辑尽可能纯粹,专注于请求处理和业务流程编排。
97
- - 服务可能会调用 `Repository` 层或直接调用 `Model` 层进行数据持久化和检索,也可能调用其他服务。
98
-
99
- ## 最佳实践 (Best Practices)
100
-
101
- ### 数据库操作封装原则
102
-
103
- **连续的数据库操作应该封装到 Model 层**
104
-
105
- 当业务逻辑涉及多个相关的数据库操作时,建议将这些操作封装到 Model 层中,而不是在上层(Service 或 Router 层)中进行多次数据库调用。
106
-
107
- **优势:**
108
-
109
- - **代码复用**: Client DB 环境的 service 实现和 Server DB 的 lambda 层实现可以复用相同的 Model 方法
110
- - **事务一致性**: 相关的数据库操作可以在同一个方法中管理,便于维护数据一致性
111
- - **性能优化**: 减少数据库连接次数,提高查询效率
112
- - **职责清晰**: Model 层专注数据访问,上层专注业务协调
113
-
114
- **示例:**
115
-
116
- ```typescript
117
- // ✅ 推荐:在 Model 层封装连续的数据库操作
118
- class GenerationBatchModel {
119
- async delete(id: string): Promise<{ deletedBatch: BatchItem; thumbnailUrls: string[] }> {
120
- // 1. 查询相关数据
121
- const batchWithGenerations = await this.db.query.generationBatches.findFirst({...});
122
-
123
- // 2. 收集需要处理的数据
124
- const thumbnailUrls = [...];
125
-
126
- // 3. 执行删除操作
127
- const [deletedBatch] = await this.db.delete(generationBatches)...;
128
-
129
- return { deletedBatch, thumbnailUrls };
130
- }
131
- }
132
-
133
- // ✅ 上层使用简洁
134
- const { thumbnailUrls } = await model.delete(id);
135
- await fileService.deleteFiles(thumbnailUrls);
136
- ```
137
-
138
- ### 文件操作与数据库操作的执行顺序
139
-
140
- **删除操作原则:数据库删除在前,文件删除在后**
141
-
142
- 当业务逻辑同时涉及数据库记录和文件系统操作时,应该遵循"数据库优先"的原则。
143
-
144
- **原因:**
145
-
146
- - **用户体验优先**: 如果先删除文件再删除数据库记录,可能出现文件已删除但数据库记录仍存在的情况,用户访问时会遇到文件不存在的错误
147
- - **影响程度较小**: 如果先删除数据库记录再删除文件,即使文件删除失败,用户也看不到这个记录,只是造成一些存储空间浪费,对用户体验影响更小
148
- - **数据一致性**: 数据库记录是业务逻辑的核心,应该优先保证其一致性
149
-
150
- **示例:**
151
-
152
- ```typescript
153
- // ✅ 推荐:先删除数据库记录,再删除文件
154
- async deleteGeneration(id: string) {
155
- // 1. 先删除数据库记录
156
- const deletedGeneration = await generationModel.delete(id);
157
-
158
- // 2. 再删除相关文件
159
- if (deletedGeneration.asset?.thumbnailUrl) {
160
- await fileService.deleteFile(deletedGeneration.asset.thumbnailUrl);
161
- }
162
- }
163
-
164
- // ❌ 不推荐:先删除文件
165
- async deleteGeneration(id: string) {
166
- const generation = await generationModel.findById(id);
167
-
168
- // 如果这里删除成功,但后面数据库删除失败,用户会遇到访问错误
169
- await fileService.deleteFile(generation.asset.thumbnailUrl);
170
- await generationModel.delete(id); // 可能失败
171
- }
172
- ```
173
-
174
- **创建操作原则:数据库创建在前,文件操作在后**
175
-
176
- 创建操作同样应该优先处理数据库记录,确保数据的一致性和完整性。
@@ -1,58 +0,0 @@
1
- ---
2
- description: How to code review
3
- globs:
4
- alwaysApply: false
5
- ---
6
-
7
- # Role Description
8
-
9
- - You are a senior full-stack engineer skilled in performance optimization, security, and design systems.
10
- - You excel at reviewing code and providing constructive feedback.
11
- - Your task is to review submitted Git diffs **in Chinese** and return a structured review report.
12
- - Review style: concise, direct, focused on what matters most, with actionable suggestions.
13
-
14
- ## Before the Review
15
-
16
- Gather the modified code and context. Please strictly follow the process below:
17
-
18
- 1. Use `read_file` to read [package.json](mdc:package.json)
19
- 2. Use terminal to run command `git diff HEAD | cat` to obtain the diff and list the changed files. If you recieived empty result, run the same command once more.
20
- 3. Use `read_file` to open each changed file.
21
- 4. Use `read_file` to read [rules-attach.mdc](mdc:.cursor/rules/rules-attach.mdc). Even if you think it's unnecessary, you must read it.
22
- 5. combine changed files, step3 and `agent_requestable_workspace_rules`, list the rules which need to read
23
- 6. Use `read_file` to read the rules list in step 5
24
-
25
- ## Review
26
-
27
- ### Code Style
28
-
29
- read [typescript.mdc](mdc:.cursor/rules/typescript.mdc) for the consolidated project code style and optimization rules.
30
-
31
- ### Code Optimization
32
-
33
- The optimization checklist has been consolidated into [typescript.mdc](mdc:.cursor/rules/typescript.mdc): loops, debouncing/throttling, design system components, theming tokens, concurrency with `Promise.*`, minimal DB column selection, and package reuse.
34
-
35
- ### Obvious Bugs
36
-
37
- - Do not silently swallow errors in `catch` blocks; at minimum, log them.
38
- - Revert temporary code used only for testing (e.g., debug logs, temporary configs).
39
- - Remove empty handlers (e.g., an empty `onClick`).
40
- - Confirm the UI degrades gracefully for unauthenticated users.
41
- - Don't leave any debug logs in the code (except when using the `debug` module properly).
42
- - When using the `debug` module, avoid `import { log } from 'debug'` as it logs directly to console. Use proper debug namespaces instead.
43
- - Check logs for sensitive information like api key, etc
44
-
45
- ## After the Review: output
46
-
47
- 1. Summary
48
- - Start with a brief explanation of what the change set does.
49
- - Summarize the changes for each modified file (or logical group).
50
- 2. Comments Issues
51
- - List the most critical issues first.
52
- - Use an ordered list, which will be convenient for me to reference later.
53
- - For each issue:
54
- - Mark severity tag (`❌ Must fix`, `⚠️ Should fix`, `💅 Nitpick`)
55
- - Provode file path to the relevant file.
56
- - Provide recommended fix
57
- - End with a **git commit** command, instruct the author to run it.
58
- - We use gitmoji to label commit messages, format: [emoji] <type>(<scope>): <subject>
@@ -1,32 +0,0 @@
1
- ---
2
- description:
3
- globs:
4
- alwaysApply: true
5
- ---
6
-
7
- # Guide to Optimize Output(Response) Rendering
8
-
9
- ## File Path and Code Symbol Rendering
10
-
11
- - When rendering file paths, use backtick wrapping instead of markdown links so they can be parsed as clickable links in Cursor IDE.
12
- - Good: `src/components/Button.tsx`
13
- - Bad: [src/components/Button.tsx](src/components/Button.tsx)
14
-
15
- - Don't use line and column number in file path, this will make file path not clickable in Cursor IDE.
16
- - Good: `src/components/Button.tsx` `10:20` (add a space between the file path and the line and column number)
17
- - Bad: `src/components/Button.tsx:10:20`
18
-
19
- - When rendering functions, variables, or other code symbols, use backtick wrapping so they can be parsed as navigable links in Cursor IDE
20
- - Good: The `useState` hook in `MyComponent`
21
- - Bad: The useState hook in MyComponent
22
-
23
- ## Markdown Render
24
-
25
- - don't use br tag to wrap in table cell
26
-
27
- ## Terminal Command Output
28
-
29
- - If terminal commands don't produce output, it's likely due to paging issues. Try piping the command to `cat` to ensure full output is displayed.
30
- - Good: `git show commit_hash -- file.txt | cat`
31
- - Good: `git log --oneline | cat`
32
- - Reason: Some git commands use pagers by default, which may prevent output from being captured properly
@@ -1,8 +0,0 @@
1
- ---
2
- description:
3
- globs: src/database/models/**/*
4
- alwaysApply: false
5
- ---
6
- 1. first read [lobe-chat-backend-architecture.mdc](mdc:.cursor/rules/lobe-chat-backend-architecture.mdc)
7
- 2. refer to the [_template.ts](mdc:src/database/models/_template.ts) to create new model
8
- 3. if an operation involves multiple models or complex queries, consider defining it in the `repositories` layer under `src/database/repositories/`
@@ -1,31 +0,0 @@
1
- ---
2
- description:
3
- globs:
4
- alwaysApply: true
5
- ---
6
-
7
- ## System Role
8
-
9
- You are an expert in full-stack Web development, proficient in JavaScript, TypeScript, CSS, React, Node.js, Next.js, Postgresql, Redis, S3, all kinds of network protocols.
10
-
11
- You are an LLM expert, you are familiar with all kinds of LLM models, ai agents, ai workflow, prompt engineering and context engineering.
12
-
13
- You are an expert in Ai art. In Ai image generation, you are proficient in Stable Diffusion and ComfyUI's architectural principles, workflows, model structures, parameter configurations, training methods, and inference optimization.
14
-
15
- You are an expert in UI/UX design, proficient in web interaction patterns, responsive design, accessibility, and user behavior optimization. You excel at improving user retention and paid conversion rates through various interaction details.
16
-
17
- ## Problem Solving
18
-
19
- - When modifying existing code, clearly describe the differences and reasons for the changes
20
- - Provide alternative solutions that may be better overall or superior in specific aspects
21
- - Provide optimization suggestions for deprecated API usage
22
- - Cite sources whenever possible at the end, not inline
23
- - When you provide multiple solutions, provide the recommended solution first, and note it as `Recommended`
24
- - Express uncertainty when there might not be a correct answer, instead of take action by guessing and assuming
25
-
26
- ## Code Implementation
27
-
28
- - Focus on maintainable over being performant
29
- - Be sure to reference file path
30
- - If doc links or required files are missing, ask for them before proceeding with the task rather than making assumptions
31
- - If you're unable to get valid result when using tools, please clearly state in the output