@lobehub/chat 1.77.6 → 1.77.8

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 (47) hide show
  1. package/.github/scripts/pr-comment.js +80 -0
  2. package/.github/scripts/pr-release-body.js +59 -0
  3. package/.github/workflows/release-desktop.yml +331 -0
  4. package/.github/workflows/test.yml +1 -1
  5. package/CHANGELOG.md +58 -0
  6. package/changelog/v1.json +21 -0
  7. package/next.config.ts +16 -11
  8. package/package.json +92 -89
  9. package/packages/electron-client-ipc/README.md +48 -0
  10. package/packages/electron-client-ipc/package.json +7 -0
  11. package/packages/electron-client-ipc/src/events/devtools.ts +6 -0
  12. package/packages/electron-client-ipc/src/events/index.ts +13 -0
  13. package/packages/electron-client-ipc/src/index.ts +2 -0
  14. package/packages/electron-client-ipc/src/types/dispatch.ts +10 -0
  15. package/packages/electron-client-ipc/src/types/index.ts +1 -0
  16. package/packages/electron-server-ipc/README.md +1 -1
  17. package/packages/web-crawler/src/crawImpl/search1api.ts +20 -17
  18. package/pnpm-workspace.yaml +1 -0
  19. package/scripts/setup-test-postgres-db.sh +21 -0
  20. package/src/app/desktop/devtools/page.tsx +89 -0
  21. package/src/app/desktop/layout.tsx +31 -0
  22. package/src/app/layout.tsx +11 -0
  23. package/src/app/not-found.tsx +1 -0
  24. package/src/const/desktop.ts +1 -0
  25. package/src/const/version.ts +2 -0
  26. package/src/database/client/db.ts +3 -10
  27. package/src/database/models/__tests__/message.test.ts +97 -26
  28. package/src/database/models/__tests__/session.test.ts +2 -0
  29. package/src/database/models/drizzleMigration.ts +15 -0
  30. package/src/database/models/message.ts +10 -5
  31. package/src/database/models/user.ts +3 -0
  32. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +13 -3
  33. package/src/features/DevPanel/features/FloatPanel.tsx +23 -6
  34. package/src/features/User/UserPanel/index.tsx +10 -6
  35. package/src/libs/trpc/middleware/userAuth.ts +10 -0
  36. package/src/server/routers/tools/__tests__/search.test.ts +1 -0
  37. package/src/server/translation.test.ts +72 -52
  38. package/src/server/translation.ts +2 -11
  39. package/src/services/electron/devtools.ts +9 -0
  40. package/src/styles/electron.ts +14 -0
  41. package/src/tools/web-browsing/Portal/Search/ResultList/SearchItem/index.tsx +3 -8
  42. package/src/tools/web-browsing/Render/Search/SearchResult/ShowMore.tsx +2 -4
  43. package/src/types/electron.ts +11 -0
  44. package/src/utils/electron/dispatch.ts +10 -0
  45. package/tsconfig.json +6 -6
  46. package/vitest.config.ts +3 -1
  47. package/vitest.server.config.ts +7 -3
@@ -1,12 +1,9 @@
1
1
  // @vitest-environment node
2
2
  import { cookies } from 'next/headers';
3
- import * as fs from 'node:fs';
4
- import * as path from 'node:path';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
6
4
 
7
- import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
5
+ import { DEFAULT_LANG } from '@/const/locale';
8
6
  import { normalizeLocale } from '@/locales/resources';
9
- import * as env from '@/utils/env';
10
7
 
11
8
  import { getLocale, translation } from './translation';
12
9
 
@@ -15,15 +12,6 @@ vi.mock('next/headers', () => ({
15
12
  cookies: vi.fn(),
16
13
  }));
17
14
 
18
- vi.mock('node:fs', () => ({
19
- existsSync: vi.fn(),
20
- readFileSync: vi.fn(),
21
- }));
22
-
23
- vi.mock('node:path', () => ({
24
- join: vi.fn(),
25
- }));
26
-
27
15
  vi.mock('@/const/locale', () => ({
28
16
  DEFAULT_LANG: 'en-US',
29
17
  LOBE_LOCALE_COOKIE: 'LOBE_LOCALE',
@@ -37,6 +25,28 @@ vi.mock('@/utils/env', () => ({
37
25
  isDev: false,
38
26
  }));
39
27
 
28
+ // 模拟动态导入结果
29
+ const mockTranslations = {
30
+ key1: 'Value 1',
31
+ key2: 'Value 2 with {{param}}',
32
+ nested: { key: 'Nested value' },
33
+ };
34
+
35
+ const mockDefaultTranslations = {
36
+ key1: '默认值 1',
37
+ key2: '默认值 2 带 {{param}}',
38
+ nested: { key: '默认嵌套值' },
39
+ };
40
+
41
+ // 重写导入函数
42
+ vi.mock('@/../locales/en-US/common.json', async () => {
43
+ return mockTranslations;
44
+ });
45
+
46
+ vi.mock('@/locales/default/common', async () => {
47
+ return mockDefaultTranslations;
48
+ });
49
+
40
50
  describe('getLocale', () => {
41
51
  const mockCookieStore = {
42
52
  get: vi.fn(),
@@ -61,17 +71,12 @@ describe('getLocale', () => {
61
71
  });
62
72
 
63
73
  describe('translation', () => {
64
- const mockTranslations = {
65
- key1: 'Value 1',
66
- key2: 'Value 2 with {{param}}',
67
- nested: { key: 'Nested value' },
68
- };
69
-
70
74
  beforeEach(() => {
71
75
  vi.clearAllMocks();
72
- (fs.existsSync as any).mockReturnValue(true);
73
- (fs.readFileSync as any).mockReturnValue(JSON.stringify(mockTranslations));
74
- (path.join as any).mockImplementation((...args: any) => args.join('/'));
76
+ // 重置 import 模拟
77
+ vi.doMock('@/../locales/en-US/common.json', async () => {
78
+ return mockTranslations;
79
+ });
75
80
  });
76
81
 
77
82
  it('should return correct translation object', async () => {
@@ -88,43 +93,58 @@ describe('translation', () => {
88
93
  expect(t('nested.key')).toBe('Nested value');
89
94
  });
90
95
 
91
- it('should return key if translation is not found', async () => {
96
+ it('should handle multiple parameters in translation string', async () => {
97
+ // 模拟多参数翻译
98
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
99
+ multiParam: 'Hello {{name}}, you have {{count}} messages',
100
+ }));
101
+
92
102
  const { t } = await translation('common', 'en-US');
93
- expect(t('nonexistent.key')).toBe('nonexistent.key');
103
+ expect(t('multiParam', { name: 'John', count: '5' })).toBe('Hello John, you have 5 messages');
94
104
  });
95
105
 
96
- it('should use fallback language if specified locale file does not exist', async () => {
97
- (fs.existsSync as any).mockReturnValueOnce(false);
98
- await translation('common', 'nonexistent-LANG');
99
- expect(fs.readFileSync).toHaveBeenCalledWith(
100
- expect.stringContaining(`/${DEFAULT_LANG}/common.json`),
101
- 'utf8',
102
- );
106
+ it('should handle different namespaces', async () => {
107
+ // 模拟另一个命名空间
108
+ vi.doMock('@/../locales/en-US/chat.json', async () => ({
109
+ welcome: 'Welcome to the chat',
110
+ }));
111
+
112
+ const { t } = await translation('chat', 'en-US');
113
+ expect(t('welcome')).toBe('Welcome to the chat');
103
114
  });
104
115
 
105
- it('should use zh-CN in dev mode when fallback is needed', async () => {
106
- (fs.existsSync as any).mockReturnValueOnce(false);
107
- (env.isDev as unknown as boolean) = true;
108
- await translation('common', 'nonexistent-LANG');
109
- expect(fs.readFileSync).toHaveBeenCalledWith(
110
- expect.stringContaining('/zh-CN/common.json'),
111
- 'utf8',
112
- );
116
+ it('should handle deep nested objects in translations', async () => {
117
+ // 模拟深层嵌套对象
118
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
119
+ very: {
120
+ deeply: {
121
+ nested: {
122
+ key: 'Found the nested value',
123
+ },
124
+ },
125
+ },
126
+ }));
127
+
128
+ const { t } = await translation('common', 'en-US');
129
+ expect(t('very.deeply.nested.key')).toBe('Found the nested value');
113
130
  });
114
131
 
115
- it('should handle file reading errors', async () => {
116
- const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
117
- (fs.readFileSync as any).mockImplementation(() => {
118
- throw new Error('File read error');
119
- });
132
+ it('should handle empty parameters object', async () => {
133
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
134
+ simpleText: 'Just a simple text',
135
+ }));
120
136
 
121
- const result = await translation('common', 'en-US');
122
- expect(result.t('any.key')).toBe('any.key');
123
- expect(consoleErrorSpy).toHaveBeenCalledWith(
124
- 'Error while reading translation file',
125
- expect.any(Error),
126
- );
137
+ const { t } = await translation('common', 'en-US');
138
+ expect(t('simpleText', {})).toBe('Just a simple text');
139
+ });
127
140
 
128
- consoleErrorSpy.mockRestore();
141
+ it('should handle missing parameters in translation string', async () => {
142
+ vi.doMock('@/../locales/en-US/common.json', async () => ({
143
+ withParam: 'Text with {{param}}',
144
+ }));
145
+
146
+ const { t } = await translation('common', 'en-US');
147
+ // 当缺少参数时应保留占位符
148
+ expect(t('withParam')).toBe('Text with {{param}}');
129
149
  });
130
150
  });
@@ -1,8 +1,6 @@
1
1
  'use server';
2
2
 
3
3
  import { get } from 'lodash-es';
4
- import { existsSync, readFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
4
 
7
5
  import { DEFAULT_LANG } from '@/const/locale';
8
6
  import { Locales, NS, normalizeLocale } from '@/locales/resources';
@@ -17,15 +15,8 @@ export const translation = async (ns: NS = 'common', hl: string) => {
17
15
  let i18ns = {};
18
16
  const lng = await getLocale(hl);
19
17
  try {
20
- let filepath = join(process.cwd(), `locales/${normalizeLocale(lng)}/${ns}.json`);
21
- const isExist = existsSync(filepath);
22
- if (!isExist)
23
- filepath = join(
24
- process.cwd(),
25
- `locales/${normalizeLocale(isDev ? 'zh-CN' : DEFAULT_LANG)}/${ns}.json`,
26
- );
27
- const file = readFileSync(filepath, 'utf8');
28
- i18ns = JSON.parse(file);
18
+ if (isDev && lng === 'zh-CN') i18ns = await import(`@/locales/default/${ns}`);
19
+ i18ns = await import(`@/../locales/${normalizeLocale(lng)}/${ns}.json`);
29
20
  } catch (e) {
30
21
  console.error('Error while reading translation file', e);
31
22
  }
@@ -0,0 +1,9 @@
1
+ import { dispatch } from '@/utils/electron/dispatch';
2
+
3
+ class DevtoolsService {
4
+ async openDevtools(): Promise<void> {
5
+ return dispatch('openDevtools');
6
+ }
7
+ }
8
+
9
+ export const electronDevtoolsService = new DevtoolsService();
@@ -0,0 +1,14 @@
1
+ import { css, cx } from 'antd-style';
2
+
3
+ export const draggable = cx(css`
4
+ -webkit-app-region: drag;
5
+ `);
6
+
7
+ export const nodrag = cx(css`
8
+ -webkit-app-region: no-drag;
9
+ `);
10
+
11
+ export const electronStylish = {
12
+ draggable,
13
+ nodrag,
14
+ };
@@ -16,25 +16,22 @@ const useStyles = createStyles(({ css, token }) => {
16
16
  flex: 1;
17
17
 
18
18
  padding: 8px;
19
+ border-radius: 8px;
19
20
 
20
21
  color: initial;
21
22
 
22
- border-radius: 8px;
23
-
24
23
  &:hover {
25
24
  background: ${token.colorFillTertiary};
26
25
  }
27
26
  `,
28
27
  desc: css`
29
28
  overflow: hidden;
30
-
31
29
  display: -webkit-box;
32
30
  -webkit-box-orient: vertical;
31
+ -webkit-line-clamp: 2;
33
32
 
34
33
  color: ${token.colorTextTertiary};
35
34
  text-overflow: ellipsis;
36
-
37
- -webkit-line-clamp: 2;
38
35
  `,
39
36
  displayLink: css`
40
37
  color: ${token.colorTextQuaternary};
@@ -45,14 +42,12 @@ const useStyles = createStyles(({ css, token }) => {
45
42
  `,
46
43
  url: css`
47
44
  overflow: hidden;
48
- { /* stylelint-disable-line */ }
49
45
  display: -webkit-box;
50
46
  -webkit-box-orient: vertical;
47
+ -webkit-line-clamp: 1;
51
48
 
52
49
  color: ${token.colorTextDescription};
53
50
  text-overflow: ellipsis;
54
-
55
- -webkit-line-clamp: 1;
56
51
  `,
57
52
  };
58
53
  });
@@ -14,12 +14,12 @@ const useStyles = createStyles(({ css, token }) => ({
14
14
 
15
15
  height: 100%;
16
16
  padding: 8px;
17
+ border-radius: 8px;
17
18
 
18
19
  font-size: 12px;
19
20
  color: initial;
20
21
 
21
22
  background: ${token.colorFillQuaternary};
22
- border-radius: 8px;
23
23
 
24
24
  &:hover {
25
25
  background: ${token.colorFillTertiary};
@@ -27,13 +27,11 @@ const useStyles = createStyles(({ css, token }) => ({
27
27
  `,
28
28
  title: css`
29
29
  overflow: hidden;
30
- { /* stylelint-disable-line */ }
31
30
  display: -webkit-box;
32
31
  -webkit-box-orient: vertical;
32
+ -webkit-line-clamp: 2;
33
33
 
34
34
  text-overflow: ellipsis;
35
-
36
- -webkit-line-clamp: 2;
37
35
  `,
38
36
  }));
39
37
 
@@ -0,0 +1,11 @@
1
+ import type { DispatchInvoke } from '@lobechat/electron-client-ipc';
2
+
3
+ export interface IElectronAPI {
4
+ invoke: DispatchInvoke;
5
+ }
6
+
7
+ declare global {
8
+ interface Window {
9
+ electronAPI: IElectronAPI;
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ import { DispatchInvoke } from '@lobechat/electron-client-ipc';
2
+
3
+ /**
4
+ * client 端请求 sketch 端 event 数据的方法
5
+ */
6
+ export const dispatch: DispatchInvoke = async (event, ...data) => {
7
+ if (!window.electronAPI) throw new Error('electronAPI not found');
8
+
9
+ return window.electronAPI.invoke(event, ...data);
10
+ };
package/tsconfig.json CHANGED
@@ -27,16 +27,16 @@
27
27
  }
28
28
  ]
29
29
  },
30
- "exclude": ["node_modules", "public/sw.js"],
30
+ "exclude": ["node_modules", "public/sw.js", "apps/desktop"],
31
31
  "include": [
32
+ "**/*.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
32
36
  "next-env.d.ts",
33
- "vitest.config.ts",
34
37
  "src",
35
38
  "tests",
36
- "**/*.ts",
37
- "**/*.d.ts",
38
- "**/*.tsx",
39
- ".next/types/**/*.ts"
39
+ "vitest.config.ts"
40
40
  ],
41
41
  "ts-node": {
42
42
  "compilerOptions": {
package/vitest.config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { defineConfig } from 'vitest/config';
2
+ import { coverageConfigDefaults, defineConfig } from 'vitest/config';
3
3
 
4
4
  export default defineConfig({
5
5
  optimizeDeps: {
@@ -14,6 +14,8 @@ export default defineConfig({
14
14
  coverage: {
15
15
  all: false,
16
16
  exclude: [
17
+ // https://github.com/lobehub/lobe-chat/pull/7265
18
+ ...coverageConfigDefaults.exclude,
17
19
  '__mocks__/**',
18
20
  // just ignore the migration code
19
21
  // we will use pglite in the future
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { defineConfig } from 'vitest/config';
2
+ import { coverageConfigDefaults, defineConfig } from 'vitest/config';
3
3
 
4
4
  export default defineConfig({
5
5
  test: {
@@ -8,8 +8,12 @@ export default defineConfig({
8
8
  },
9
9
  coverage: {
10
10
  all: false,
11
- exclude: ['src/database/server/core/dbForTest.ts'],
12
- include: ['src/database/server/**/*.ts'],
11
+ exclude: [
12
+ // https://github.com/lobehub/lobe-chat/pull/7265
13
+ ...coverageConfigDefaults.exclude,
14
+ 'src/database/server/core/dbForTest.ts',
15
+ ],
16
+ include: ['src/database/models/**/*.ts', 'src/database/server/**/*.ts'],
13
17
  provider: 'v8',
14
18
  reporter: ['text', 'json', 'lcov', 'text-summary'],
15
19
  reportsDirectory: './coverage/server',