@lobehub/lobehub 2.0.0-next.144 → 2.0.0-next.146

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 (41) hide show
  1. package/.cursor/rules/project-introduce.mdc +1 -1
  2. package/.github/workflows/test.yml +3 -0
  3. package/AGENTS.md +4 -0
  4. package/CHANGELOG.md +50 -0
  5. package/apps/desktop/package.json +2 -0
  6. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +8 -12
  7. package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +1 -0
  8. package/apps/desktop/src/main/core/ui/__tests__/Tray.test.ts +2 -2
  9. package/apps/desktop/src/main/utils/__tests__/file-system.test.ts +1 -1
  10. package/apps/desktop/src/main/utils/__tests__/logger.test.ts +7 -7
  11. package/apps/desktop/src/main/utils/next-electron-rsc.ts +3 -1
  12. package/apps/desktop/src/preload/invoke.test.ts +4 -2
  13. package/apps/desktop/src/preload/routeInterceptor.test.ts +54 -9
  14. package/apps/desktop/src/preload/streamer.test.ts +32 -31
  15. package/changelog/v1.json +18 -0
  16. package/docs/development/database-schema.dbml +2 -1
  17. package/docs/self-hosting/advanced/auth.mdx +21 -10
  18. package/docs/self-hosting/advanced/auth.zh-CN.mdx +21 -10
  19. package/package.json +4 -3
  20. package/packages/database/migrations/0056_update_agent_slug_index.sql +2 -0
  21. package/packages/database/migrations/meta/0056_snapshot.json +8411 -0
  22. package/packages/database/migrations/meta/_journal.json +7 -0
  23. package/packages/database/src/core/migrations.json +11 -2
  24. package/packages/database/src/schemas/agent.ts +2 -3
  25. package/packages/electron-client-ipc/src/events/system.ts +1 -3
  26. package/packages/electron-client-ipc/src/types/system.ts +1 -0
  27. package/src/envs/email.ts +11 -0
  28. package/src/libs/better-auth/email-templates/magic-link.ts +5 -5
  29. package/src/libs/better-auth/email-templates/reset-password.ts +4 -4
  30. package/src/libs/better-auth/email-templates/verification.ts +4 -4
  31. package/src/libs/mcp/__tests__/__snapshots__/index.test.ts.snap +9 -0
  32. package/src/server/services/email/README.md +19 -0
  33. package/src/server/services/email/impls/index.ts +5 -1
  34. package/src/server/services/email/impls/resend/index.ts +120 -0
  35. package/src/server/services/email/index.test.ts +1 -1
  36. package/src/server/services/email/index.ts +9 -1
  37. package/src/server/services/file/impls/index.ts +3 -3
  38. package/src/server/services/file/impls/local.ts +35 -35
  39. package/src/server/services/file/impls/s3.ts +1 -1
  40. package/src/server/services/file/impls/type.ts +11 -11
  41. package/src/server/services/file/index.ts +12 -12
@@ -392,6 +392,13 @@
392
392
  "when": 1764583392443,
393
393
  "tag": "0055_rename_phone_number_to_phone",
394
394
  "breakpoints": true
395
+ },
396
+ {
397
+ "idx": 56,
398
+ "version": "7",
399
+ "when": 1764685643024,
400
+ "tag": "0056_update_agent_slug_index",
401
+ "breakpoints": true
395
402
  }
396
403
  ],
397
404
  "version": "6"
@@ -884,12 +884,12 @@
884
884
  "\nCREATE INDEX IF NOT EXISTS \"account_userId_idx\" ON \"accounts\" USING btree (\"user_id\");\n",
885
885
  "\nCREATE INDEX IF NOT EXISTS \"auth_session_userId_idx\" ON \"auth_sessions\" USING btree (\"user_id\");\n",
886
886
  "\nCREATE INDEX IF NOT EXISTS \"verification_identifier_idx\" ON \"verifications\" USING btree (\"identifier\");\n",
887
- "\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_email_unique') THEN\n UPDATE \"users\" SET \"email\" = NULL WHERE \"email\" = '';\n ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\");\n END IF;\nEND $$;\n",
887
+ "\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_email_unique') THEN\n -- Normalize empty emails so the unique constraint can be created safely\n UPDATE \"users\" SET \"email\" = NULL WHERE \"email\" = '';\n ALTER TABLE \"users\" ADD CONSTRAINT \"users_email_unique\" UNIQUE (\"email\");\n END IF;\nEND $$;\n",
888
888
  "\nDO $$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_phone_number_unique') THEN\n ALTER TABLE \"users\" ADD CONSTRAINT \"users_phone_number_unique\" UNIQUE (\"phone_number\");\n END IF;\nEND $$;\n"
889
889
  ],
890
890
  "bps": true,
891
891
  "folderMillis": 1764579351312,
892
- "hash": "22fb7a65764b1f3e3c1ae2ce95d448685e6a01d1fb2f8c3f925c655f0824c161"
892
+ "hash": "1d2536a9471bb87686b35053f98ba7762259a07c819dc4489bb4f3c7f27a4d8d"
893
893
  },
894
894
  {
895
895
  "sql": [
@@ -901,5 +901,14 @@
901
901
  "bps": true,
902
902
  "folderMillis": 1764583392443,
903
903
  "hash": "6a43d90ee1d2e1e008d1b8206f227aa05b9a2e2a8fc8c31ec608a3716c716a6c"
904
+ },
905
+ {
906
+ "sql": [
907
+ "ALTER TABLE \"agents\" DROP CONSTRAINT \"agents_slug_unique\";",
908
+ "\nCREATE UNIQUE INDEX IF NOT EXISTS \"agents_slug_user_id_unique\" ON \"agents\" USING btree (\"slug\",\"user_id\");\n"
909
+ ],
910
+ "bps": true,
911
+ "folderMillis": 1764685643024,
912
+ "hash": "6e7ac7f964eb03efa3cb0d2fd35ded23e25c3abf955c4c2a51418f8daef54af9"
904
913
  }
905
914
  ]
@@ -28,9 +28,7 @@ export const agents = pgTable(
28
28
  .primaryKey()
29
29
  .$defaultFn(() => idGenerator('agents'))
30
30
  .notNull(),
31
- slug: varchar('slug', { length: 100 })
32
- .$defaultFn(() => randomSlug(4))
33
- .unique(),
31
+ slug: varchar('slug', { length: 100 }).$defaultFn(() => randomSlug(3)),
34
32
  title: varchar('title', { length: 255 }),
35
33
  description: varchar('description', { length: 1000 }),
36
34
  tags: jsonb('tags').$type<string[]>().default([]),
@@ -65,6 +63,7 @@ export const agents = pgTable(
65
63
  },
66
64
  (t) => [
67
65
  uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId),
66
+ uniqueIndex('agents_slug_user_id_unique').on(t.slug, t.userId),
68
67
  index('agents_title_idx').on(t.title),
69
68
  index('agents_description_idx').on(t.description),
70
69
  ],
@@ -1,6 +1,4 @@
1
- import { ThemeAppearance } from 'antd-style';
2
-
3
- import { ElectronAppState, ThemeMode } from '../types';
1
+ import { ElectronAppState, ThemeAppearance, ThemeMode } from '../types';
4
2
 
5
3
  export interface SystemDispatchEvents {
6
4
  checkSystemAccessibility: () => boolean | undefined;
@@ -25,3 +25,4 @@ export interface UserPathData {
25
25
  }
26
26
 
27
27
  export type ThemeMode = 'auto' | 'dark' | 'light';
28
+ export type ThemeAppearance = 'dark' | 'light' | string;
package/src/envs/email.ts CHANGED
@@ -6,6 +6,9 @@ declare global {
6
6
  // eslint-disable-next-line @typescript-eslint/no-namespace
7
7
  namespace NodeJS {
8
8
  interface ProcessEnv {
9
+ EMAIL_SERVICE_PROVIDER?: string;
10
+ RESEND_API_KEY?: string;
11
+ RESEND_FROM?: string;
9
12
  SMTP_HOST?: string;
10
13
  SMTP_PASS?: string;
11
14
  SMTP_PORT?: string;
@@ -18,6 +21,9 @@ declare global {
18
21
  export const getEmailConfig = () => {
19
22
  return createEnv({
20
23
  server: {
24
+ EMAIL_SERVICE_PROVIDER: z.enum(['nodemailer', 'resend']).optional(),
25
+ RESEND_API_KEY: z.string().optional(),
26
+ RESEND_FROM: z.string().optional(),
21
27
  SMTP_HOST: z.string().optional(),
22
28
  SMTP_PORT: z.coerce.number().optional(),
23
29
  SMTP_SECURE: z.boolean().optional(),
@@ -30,6 +36,11 @@ export const getEmailConfig = () => {
30
36
  SMTP_SECURE: process.env.SMTP_SECURE === 'true',
31
37
  SMTP_USER: process.env.SMTP_USER,
32
38
  SMTP_PASS: process.env.SMTP_PASS,
39
+ EMAIL_SERVICE_PROVIDER: process.env.EMAIL_SERVICE_PROVIDER
40
+ ? process.env.EMAIL_SERVICE_PROVIDER.toLowerCase()
41
+ : undefined,
42
+ RESEND_API_KEY: process.env.RESEND_API_KEY,
43
+ RESEND_FROM: process.env.RESEND_FROM,
33
44
  },
34
45
  });
35
46
  };
@@ -18,7 +18,7 @@ export const getMagicLinkEmailTemplate = (params: { expiresInSeconds: number; ur
18
18
  <head>
19
19
  <meta charset="utf-8">
20
20
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
21
- <title>Sign in to LobeChat</title>
21
+ <title>Sign in to LobeHub</title>
22
22
  </head>
23
23
  <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; color: #1a1a1a;">
24
24
  <!-- Container -->
@@ -28,7 +28,7 @@ export const getMagicLinkEmailTemplate = (params: { expiresInSeconds: number; ur
28
28
  <div style="text-align: center; margin-bottom: 32px;">
29
29
  <div style="display: inline-flex; align-items: center; justify-content: center; background-color: #ffffff; border-radius: 12px; padding: 8px 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
30
30
  <span style="font-size: 24px; line-height: 1; margin-right: 10px;">🤯</span>
31
- <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeChat</span>
31
+ <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeHub</span>
32
32
  </div>
33
33
  </div>
34
34
 
@@ -38,7 +38,7 @@ export const getMagicLinkEmailTemplate = (params: { expiresInSeconds: number; ur
38
38
  <!-- Header -->
39
39
  <div style="text-align: center; margin-bottom: 32px;">
40
40
  <h1 style="color: #111827; font-size: 24px; font-weight: 700; margin: 0 0 12px 0; letter-spacing: -0.5px;">
41
- Sign in to LobeChat
41
+ Sign in to LobeHub
42
42
  </h1>
43
43
  <p style="color: #6b7280; font-size: 16px; margin: 0; line-height: 1.5;">
44
44
  Click the link below to sign in to your account.
@@ -85,14 +85,14 @@ export const getMagicLinkEmailTemplate = (params: { expiresInSeconds: number; ur
85
85
  <!-- Footer -->
86
86
  <div style="text-align: center; margin-top: 32px;">
87
87
  <p style="color: #a1a1aa; font-size: 13px; margin: 0;">
88
- © ${new Date().getFullYear()} LobeChat. All rights reserved.
88
+ © ${new Date().getFullYear()} LobeHub. All rights reserved.
89
89
  </p>
90
90
  </div>
91
91
  </div>
92
92
  </body>
93
93
  </html>
94
94
  `,
95
- subject: 'Your LobeChat sign-in link',
95
+ subject: 'Your LobeHub sign-in link',
96
96
  text: `Use this link to sign in: ${url}\n\nThis link expires in ${expirationText}.`,
97
97
  };
98
98
  };
@@ -22,7 +22,7 @@ export const getResetPasswordEmailTemplate = (params: { url: string }) => {
22
22
  <div style="text-align: center; margin-bottom: 32px;">
23
23
  <div style="display: inline-flex; align-items: center; justify-content: center; background-color: #ffffff; border-radius: 12px; padding: 8px 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
24
24
  <span style="font-size: 24px; line-height: 1; margin-right: 10px;">🤯</span>
25
- <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeChat</span>
25
+ <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeHub</span>
26
26
  </div>
27
27
  </div>
28
28
 
@@ -42,7 +42,7 @@ export const getResetPasswordEmailTemplate = (params: { url: string }) => {
42
42
  <!-- Content -->
43
43
  <div style="color: #374151; font-size: 16px; line-height: 1.6;">
44
44
  <p style="margin: 0 0 24px 0; text-align: center;">
45
- You recently requested to reset your password for your LobeChat account. Click the button below to proceed.
45
+ You recently requested to reset your password for your LobeHub account. Click the button below to proceed.
46
46
  </p>
47
47
 
48
48
  <!-- Button -->
@@ -78,14 +78,14 @@ export const getResetPasswordEmailTemplate = (params: { url: string }) => {
78
78
  <!-- Footer -->
79
79
  <div style="text-align: center; margin-top: 32px;">
80
80
  <p style="color: #a1a1aa; font-size: 13px; margin: 0;">
81
- © ${new Date().getFullYear()} LobeChat. All rights reserved.
81
+ © ${new Date().getFullYear()} LobeHub. All rights reserved.
82
82
  </p>
83
83
  </div>
84
84
  </div>
85
85
  </body>
86
86
  </html>
87
87
  `,
88
- subject: 'Reset Your Password - LobeChat',
88
+ subject: 'Reset Your Password - LobeHub',
89
89
  text: `Reset your password by clicking this link: ${url}`,
90
90
  };
91
91
  };
@@ -33,7 +33,7 @@ export const getVerificationEmailTemplate = (params: {
33
33
  <div style="text-align: center; margin-bottom: 32px;">
34
34
  <div style="display: inline-flex; align-items: center; justify-content: center; background-color: #ffffff; border-radius: 12px; padding: 8px 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
35
35
  <span style="font-size: 24px; line-height: 1; margin-right: 10px;">🤯</span>
36
- <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeChat</span>
36
+ <span style="font-size: 18px; font-weight: 700; color: #000000; letter-spacing: -0.5px;">LobeHub</span>
37
37
  </div>
38
38
  </div>
39
39
 
@@ -55,7 +55,7 @@ export const getVerificationEmailTemplate = (params: {
55
55
  ${userName ? `<p style="margin: 0 0 16px 0;">Hi <strong>${userName}</strong>,</p>` : ''}
56
56
 
57
57
  <p style="margin: 0 0 24px 0;">
58
- Thanks for creating an account with LobeChat. To access your account, please verify your email address by clicking the button below.
58
+ Thanks for creating an account with LobeHub. To access your account, please verify your email address by clicking the button below.
59
59
  </p>
60
60
 
61
61
  <!-- Button -->
@@ -95,14 +95,14 @@ export const getVerificationEmailTemplate = (params: {
95
95
  <!-- Footer -->
96
96
  <div style="text-align: center; margin-top: 32px;">
97
97
  <p style="color: #a1a1aa; font-size: 13px; margin: 0;">
98
- © 2025 LobeChat. All rights reserved.
98
+ © 2025 LobeHub. All rights reserved.
99
99
  </p>
100
100
  </div>
101
101
  </div>
102
102
  </body>
103
103
  </html>
104
104
  `,
105
- subject: 'Verify Your Email - LobeChat',
105
+ subject: 'Verify Your Email - LobeHub',
106
106
  text: `Please verify your email by clicking this link: ${url}\n\nThis link will expire in ${expirationText}.`,
107
107
  };
108
108
  };
@@ -4,6 +4,9 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
4
4
  [
5
5
  {
6
6
  "description": "Echoes back a message with 'Hello' prefix",
7
+ "execution": {
8
+ "taskSupport": "forbidden",
9
+ },
7
10
  "inputSchema": {
8
11
  "$schema": "http://json-schema.org/draft-07/schema#",
9
12
  "additionalProperties": false,
@@ -22,6 +25,9 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
22
25
  },
23
26
  {
24
27
  "description": "Lists all available tools and methods",
28
+ "execution": {
29
+ "taskSupport": "forbidden",
30
+ },
25
31
  "inputSchema": {
26
32
  "$schema": "http://json-schema.org/draft-07/schema#",
27
33
  "properties": {},
@@ -31,6 +37,9 @@ exports[`MCPClient > Stdio Transport > should list tools via stdio 1`] = `
31
37
  },
32
38
  {
33
39
  "description": "Adds two numbers",
40
+ "execution": {
41
+ "taskSupport": "forbidden",
42
+ },
34
43
  "inputSchema": {
35
44
  "$schema": "http://json-schema.org/draft-07/schema#",
36
45
  "additionalProperties": false,
@@ -86,6 +86,25 @@ SMTP_USER=your-username
86
86
  SMTP_PASS=your-password
87
87
  ```
88
88
 
89
+ ### Resend
90
+
91
+ If you prefer Resend, configure the following and initialize the service with `EmailImplType.Resend`:
92
+
93
+ ```bash
94
+ RESEND_API_KEY=your-resend-api-key
95
+ RESEND_FROM=noreply@example.com
96
+ ```
97
+
98
+ `RESEND_FROM` is used when `from` is not provided in the payload.
99
+
100
+ ### Choose Provider by Environment
101
+
102
+ Set `EMAIL_SERVICE_PROVIDER` to `nodemailer` or `resend` to pick the default implementation without changing code:
103
+
104
+ ```bash
105
+ EMAIL_SERVICE_PROVIDER=resend
106
+ ```
107
+
89
108
  ### Using Well-Known Services
90
109
 
91
110
  You can also use well-known email services (Gmail, SendGrid, etc.):
@@ -1,4 +1,5 @@
1
1
  import { NodemailerImpl } from './nodemailer';
2
+ import { ResendImpl } from './resend';
2
3
  import { EmailServiceImpl } from './type';
3
4
 
4
5
  /**
@@ -6,8 +7,8 @@ import { EmailServiceImpl } from './type';
6
7
  */
7
8
  export enum EmailImplType {
8
9
  Nodemailer = 'nodemailer',
10
+ Resend = 'resend',
9
11
  // Future providers can be added here:
10
- // Resend = 'resend',
11
12
  // SendGrid = 'sendgrid',
12
13
  }
13
14
 
@@ -21,6 +22,9 @@ export const createEmailServiceImpl = (
21
22
  case EmailImplType.Nodemailer: {
22
23
  return new NodemailerImpl();
23
24
  }
25
+ case EmailImplType.Resend: {
26
+ return new ResendImpl();
27
+ }
24
28
 
25
29
  default: {
26
30
  return new NodemailerImpl();
@@ -0,0 +1,120 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import debug from 'debug';
3
+ import { Resend } from 'resend';
4
+ import type { CreateEmailOptions } from 'resend';
5
+
6
+ import { emailEnv } from '@/envs/email';
7
+
8
+ import { EmailPayload, EmailResponse, EmailServiceImpl } from '../type';
9
+
10
+ const log = debug('lobe-email:Resend');
11
+
12
+ /**
13
+ * Resend implementation of the email service
14
+ */
15
+ export class ResendImpl implements EmailServiceImpl {
16
+ private client: Resend;
17
+
18
+ constructor() {
19
+ if (!emailEnv.RESEND_API_KEY) {
20
+ throw new Error(
21
+ 'RESEND_API_KEY environment variable is required to use Resend email service. Please configure it in your .env file.',
22
+ );
23
+ }
24
+
25
+ this.client = new Resend(emailEnv.RESEND_API_KEY);
26
+ log('Initialized Resend client');
27
+ }
28
+
29
+ async sendMail(payload: EmailPayload): Promise<EmailResponse> {
30
+ const from = payload.from ?? emailEnv.RESEND_FROM;
31
+ const html = payload.html;
32
+ const text = payload.text;
33
+
34
+ if (!from) {
35
+ throw new TRPCError({
36
+ code: 'PRECONDITION_FAILED',
37
+ message: 'Missing sender address. Provide payload.from or RESEND_FROM environment variable.',
38
+ });
39
+ }
40
+
41
+ if (!html && !text) {
42
+ throw new TRPCError({
43
+ code: 'PRECONDITION_FAILED',
44
+ message: 'Resend requires either html or text content in the email payload.',
45
+ });
46
+ }
47
+
48
+ const attachments = payload.attachments?.map((attachment) => {
49
+ if (attachment.content instanceof Buffer) {
50
+ return {
51
+ ...attachment,
52
+ content: attachment.content.toString('base64'),
53
+ };
54
+ }
55
+
56
+ return attachment;
57
+ });
58
+
59
+ try {
60
+ log('Sending email via Resend: %o', {
61
+ from,
62
+ subject: payload.subject,
63
+ to: payload.to,
64
+ });
65
+
66
+ const emailOptions: CreateEmailOptions = html
67
+ ? {
68
+ attachments,
69
+ from,
70
+ html,
71
+ replyTo: payload.replyTo,
72
+ subject: payload.subject,
73
+ text,
74
+ to: payload.to,
75
+ }
76
+ : {
77
+ attachments,
78
+ from,
79
+ replyTo: payload.replyTo,
80
+ subject: payload.subject,
81
+ text: text!,
82
+ to: payload.to,
83
+ };
84
+
85
+ const { data, error } = await this.client.emails.send(emailOptions);
86
+
87
+ if (error) {
88
+ log.extend('error')('Failed to send email via Resend: %o', error);
89
+ throw new TRPCError({
90
+ cause: error,
91
+ code: 'SERVICE_UNAVAILABLE',
92
+ message: `Failed to send email via Resend: ${error.message}`,
93
+ });
94
+ }
95
+
96
+ if (!data?.id) {
97
+ log.extend('error')('Resend sendMail returned no message id: %o', data);
98
+ throw new TRPCError({
99
+ code: 'SERVICE_UNAVAILABLE',
100
+ message: 'Failed to send email via Resend: missing message id',
101
+ });
102
+ }
103
+
104
+ return {
105
+ messageId: data.id,
106
+ };
107
+ } catch (error) {
108
+ if (error instanceof TRPCError) {
109
+ throw error;
110
+ }
111
+
112
+ log.extend('error')('Unexpected Resend sendMail error: %o', error);
113
+ throw new TRPCError({
114
+ cause: error,
115
+ code: 'SERVICE_UNAVAILABLE',
116
+ message: `Failed to send email via Resend: ${(error as Error).message}`,
117
+ });
118
+ }
119
+ }
120
+ }
@@ -26,7 +26,7 @@ describe('EmailService', () => {
26
26
 
27
27
  describe('constructor', () => {
28
28
  it('should create instance with default email implementation', () => {
29
- expect(createEmailServiceImpl).toHaveBeenCalledWith(undefined);
29
+ expect(createEmailServiceImpl).toHaveBeenCalledWith(EmailImplType.Nodemailer);
30
30
  });
31
31
 
32
32
  it('should create instance with specified implementation type', () => {
@@ -1,3 +1,4 @@
1
+ import { emailEnv } from '@/envs/email';
1
2
 
2
3
  import { EmailImplType, EmailPayload, EmailResponse, createEmailServiceImpl } from './impls';
3
4
  import type { EmailServiceImpl } from './impls';
@@ -10,7 +11,14 @@ export class EmailService {
10
11
  private emailImpl: EmailServiceImpl;
11
12
 
12
13
  constructor(implType?: EmailImplType) {
13
- this.emailImpl = createEmailServiceImpl(implType);
14
+ // Avoid client-side access to server env when executed in browser-like test environments
15
+ const envImplType =
16
+ typeof window === 'undefined'
17
+ ? (emailEnv.EMAIL_SERVICE_PROVIDER as EmailImplType | undefined)
18
+ : undefined;
19
+ const resolvedImplType = implType ?? envImplType ?? EmailImplType.Nodemailer;
20
+
21
+ this.emailImpl = createEmailServiceImpl(resolvedImplType);
14
22
  }
15
23
 
16
24
  /**
@@ -5,11 +5,11 @@ import { S3StaticFileImpl } from './s3';
5
5
  import { FileServiceImpl } from './type';
6
6
 
7
7
  /**
8
- * 创建文件服务模块
9
- * 根据环境自动选择使用S3或桌面本地文件实现
8
+ * Create file service module
9
+ * Automatically selects between S3 or desktop local file implementation based on environment
10
10
  */
11
11
  export const createFileServiceModule = (): FileServiceImpl => {
12
- // 如果在桌面应用环境,使用本地文件实现
12
+ // If in desktop application environment, use local file implementation
13
13
  if (isDesktop) {
14
14
  return new DesktopLocalFileImpl();
15
15
  }