create-pardx-scaffold 0.1.0 → 0.1.2

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 (75) hide show
  1. package/package.json +1 -1
  2. package/template/.cursor/worktrees.json +37 -0
  3. package/template/.dockerignore +49 -0
  4. package/template/.mcp.json +26 -0
  5. package/template/.nvmrc +1 -0
  6. package/template/CLAUDE.md +85 -0
  7. package/template/apps/api/libs/domain/services/index.ts +7 -0
  8. package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/ip-info.module.ts +2 -0
  9. package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/ip-info.service.ts +2 -0
  10. package/template/apps/api/libs/infra/clients/internal/file-storage/dto/file.dto.ts +1 -1
  11. package/template/apps/api/libs/infra/shared-services/email/email.module.ts +0 -2
  12. package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +1 -1
  13. package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +1 -1
  14. package/template/apps/api/libs/infra/shared-services/sms/sms.module.ts +0 -2
  15. package/template/apps/api/package.json +15 -15
  16. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  17. package/template/apps/api/src/app.module.ts +1 -1
  18. package/template/apps/web/.env.example +6 -4
  19. package/template/apps/web/components/error-boundary.tsx +166 -0
  20. package/template/apps/web/components/index.ts +10 -0
  21. package/template/apps/web/components.json +20 -0
  22. package/template/apps/web/config.ts +115 -0
  23. package/template/apps/web/eslint.config.mjs +4 -0
  24. package/template/apps/web/lib/api/avatar-upload.ts +1 -0
  25. package/template/apps/web/lib/api/contracts/client.ts +51 -30
  26. package/template/apps/web/lib/api/contracts/hooks/index.ts +0 -3
  27. package/template/apps/web/lib/api/contracts/hooks/notification.ts +42 -124
  28. package/template/apps/web/lib/api.ts +24 -1
  29. package/template/apps/web/lib/dynamic-import.tsx +121 -0
  30. package/template/apps/web/lib/logger.ts +113 -0
  31. package/template/apps/web/lib/upload/api.ts +37 -105
  32. package/template/apps/web/lib/upload/batch-uploader.ts +7 -74
  33. package/template/apps/web/lib/upload/uploader.ts +10 -74
  34. package/template/apps/web/locales/zh-CN/assessment.json +5 -0
  35. package/template/apps/web/locales/zh-CN/chat.json +6 -0
  36. package/template/apps/web/locales/zh-CN/common.json +38 -0
  37. package/template/apps/web/locales/zh-CN/creative.json +5 -0
  38. package/template/apps/web/locales/zh-CN/daily-challenge.json +6 -0
  39. package/template/apps/web/locales/zh-CN/errors.json +16 -0
  40. package/template/apps/web/locales/zh-CN/forms.json +18 -0
  41. package/template/apps/web/locales/zh-CN/memory.json +5 -0
  42. package/template/apps/web/locales/zh-CN/navigation.json +12 -0
  43. package/template/apps/web/locales/zh-CN/recommendation.json +5 -0
  44. package/template/apps/web/locales/zh-CN/recruitment.json +5 -0
  45. package/template/apps/web/locales/zh-CN/settings.json +7 -0
  46. package/template/apps/web/locales/zh-CN/subscription.json +6 -0
  47. package/template/apps/web/locales/zh-CN/validation.json +8 -0
  48. package/template/apps/web/package.json +14 -15
  49. package/template/apps/web/postcss.config.mjs +1 -0
  50. package/template/apps/web/proxy.ts +102 -0
  51. package/template/apps/web/public/logo.svg +21 -0
  52. package/template/apps/web/vitest.config.ts +69 -0
  53. package/template/apps/web/vitest.setup.ts +80 -0
  54. package/template/package.json +7 -7
  55. package/template/packages/constants/package.json +3 -1
  56. package/template/packages/constants/tsconfig.build.esm.json +8 -0
  57. package/template/packages/contracts/package.json +2 -2
  58. package/template/packages/contracts/src/schemas/uploader.schema.ts +33 -10
  59. package/template/packages/ui/.storybook/main.ts +28 -0
  60. package/template/packages/ui/.storybook/preview.ts +40 -0
  61. package/template/packages/ui/eslint.config.js +3 -0
  62. package/template/packages/ui/package.json +15 -2
  63. package/template/packages/ui/src/components/button.stories.tsx +171 -0
  64. package/template/packages/ui/src/styles/globals.css +1 -1
  65. package/template/packages/ui/tsconfig.json +1 -1
  66. package/template/packages/utils/package.json +2 -2
  67. package/template/packages/utils/tsconfig.build.esm.json +8 -0
  68. package/template/packages/validators/package.json +1 -1
  69. package/template/pnpm-lock.yaml +2263 -999
  70. package/template/scripts/export-scaffold-for-create.js +65 -0
  71. package/template/apps/api/libs/infra/utils/download.ts +0 -21
  72. package/template/apps/web/lib/api/client.ts +0 -649
  73. package/template/apps/web/lib/audio-buffer-queue.ts +0 -273
  74. package/template/apps/web/lib/upload/folder-utils.ts +0 -295
  75. /package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/index.ts +0 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Frontend Logger
3
+ *
4
+ * 浏览器兼容的日志库,替代 console.log
5
+ * 支持日志级别控制和生产环境静默
6
+ */
7
+
8
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
9
+
10
+ interface LoggerConfig {
11
+ level: LogLevel;
12
+ prefix?: string;
13
+ enableInProduction?: boolean;
14
+ }
15
+
16
+ const LOG_LEVELS: Record<LogLevel, number> = {
17
+ debug: 0,
18
+ info: 1,
19
+ warn: 2,
20
+ error: 3,
21
+ };
22
+
23
+ class Logger {
24
+ private level: LogLevel;
25
+ private prefix: string;
26
+ private enableInProduction: boolean;
27
+
28
+ constructor(config: Partial<LoggerConfig> = {}) {
29
+ this.level = config.level ?? this.getDefaultLevel();
30
+ this.prefix = config.prefix ?? '[App]';
31
+ this.enableInProduction = config.enableInProduction ?? false;
32
+ }
33
+
34
+ private getDefaultLevel(): LogLevel {
35
+ if (typeof window === 'undefined') {
36
+ return 'info';
37
+ }
38
+ return process.env.NODE_ENV === 'production' ? 'warn' : 'debug';
39
+ }
40
+
41
+ private shouldLog(level: LogLevel): boolean {
42
+ // 在生产环境中,除非明确启用,否则只记录 warn 和 error
43
+ if (
44
+ process.env.NODE_ENV === 'production' &&
45
+ !this.enableInProduction &&
46
+ LOG_LEVELS[level] < LOG_LEVELS.warn
47
+ ) {
48
+ return false;
49
+ }
50
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
51
+ }
52
+
53
+ private formatMessage(level: LogLevel, message: string): string {
54
+ const timestamp = new Date().toISOString();
55
+ return `${timestamp} ${this.prefix} [${level.toUpperCase()}] ${message}`;
56
+ }
57
+
58
+ debug(message: string, ...args: unknown[]): void {
59
+ if (this.shouldLog('debug')) {
60
+ console.debug(this.formatMessage('debug', message), ...args);
61
+ }
62
+ }
63
+
64
+ info(message: string, ...args: unknown[]): void {
65
+ if (this.shouldLog('info')) {
66
+ console.info(this.formatMessage('info', message), ...args);
67
+ }
68
+ }
69
+
70
+ warn(message: string, ...args: unknown[]): void {
71
+ if (this.shouldLog('warn')) {
72
+ console.warn(this.formatMessage('warn', message), ...args);
73
+ }
74
+ }
75
+
76
+ error(message: string, ...args: unknown[]): void {
77
+ if (this.shouldLog('error')) {
78
+ console.error(this.formatMessage('error', message), ...args);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 设置日志级别
84
+ */
85
+ setLevel(level: LogLevel): void {
86
+ this.level = level;
87
+ }
88
+
89
+ /**
90
+ * 创建带有自定义前缀的子 logger
91
+ */
92
+ child(prefix: string): Logger {
93
+ return new Logger({
94
+ level: this.level,
95
+ prefix: `${this.prefix}:${prefix}`,
96
+ enableInProduction: this.enableInProduction,
97
+ });
98
+ }
99
+ }
100
+
101
+ // 默认 logger 实例
102
+ export const logger = new Logger();
103
+
104
+ // 创建模块专用 logger
105
+ export const createLogger = (prefix: string): Logger => {
106
+ return logger.child(prefix);
107
+ };
108
+
109
+ // 导出类型
110
+ export type { LogLevel, LoggerConfig };
111
+ export { Logger };
112
+
113
+ export default logger;
@@ -1,5 +1,13 @@
1
1
  import { uploaderClient } from '../api/contracts/client';
2
2
  import { UploadError, UploadErrorCode } from './errors';
3
+ import type {
4
+ FileSourceResponse,
5
+ TokenResponse,
6
+ UploadMetadata,
7
+ } from '@repo/contracts';
8
+
9
+ // Re-export UploadMetadata type from contracts
10
+ export type { UploadMetadata };
3
11
 
4
12
  // Helper function: extract error message from response (for logging only)
5
13
  function getErrorMsg(body: unknown, defaultMsg: string): string {
@@ -14,87 +22,38 @@ function getErrorMsg(body: unknown, defaultMsg: string): string {
14
22
  return defaultMsg;
15
23
  }
16
24
 
17
- // Type guard for folderId
18
- type FolderId = 'root' | `${string}-${string}-${string}-${string}-${string}`;
19
-
20
- function isValidFolderId(id: string): id is FolderId {
21
- return (
22
- id === 'root' ||
23
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)
24
- );
25
- }
26
-
27
25
  /**
28
- * 上传元数据(用于秒传后处理)
29
- */
30
- export interface UploadMetadata {
31
- jobDescriptionId?: string; // 简历上传使用
32
- meetingId?: string; // 会议录音上传使用
33
- autoParse?: boolean; // 是否自动触发解析
34
- }
35
-
36
- /**
37
- * 获取私有上传 Token
26
+ * 获取私有上传 Token 参数
38
27
  */
39
28
  export interface GetUploadTokenParams {
40
29
  filename: string;
41
30
  signature: string;
42
31
  fsize: number;
43
- folderId: string;
44
- spaceId: string;
45
32
  thumbImg?: string;
46
- domain?: string;
33
+ sha256?: string;
47
34
  metadata?: UploadMetadata;
48
35
  }
49
36
 
50
- export interface FileSystem {
51
- id: string;
52
- name: string;
53
- type: string;
54
- spaceId: string;
55
- parentId: string | null;
56
- fsize: number;
57
- url?: string;
58
- fileKeyId?: string;
59
- origin?: string;
60
- createdAt?: string;
61
- updatedAt?: string;
62
- }
63
-
64
- export interface UploadTokenData {
65
- // 文件不存在时返回的字段
66
- token?: string;
67
- key?: string;
68
- fileId?: string;
69
-
70
- // 文件已存在(秒传)时返回的字段
71
- success?: boolean;
72
- fileSystem?: FileSystem;
73
- space?: Record<string, unknown>;
74
- parent?: Record<string, unknown>;
75
- usage?: Record<string, unknown>;
76
- }
37
+ /**
38
+ * 上传 Token 响应数据
39
+ */
40
+ export type UploadTokenData = TokenResponse;
77
41
 
78
42
  export async function getUploadTokenPrivate(
79
43
  params: GetUploadTokenParams,
80
44
  ): Promise<UploadTokenData> {
81
- if (!isValidFolderId(params.folderId)) {
82
- throw new UploadError(
83
- UploadErrorCode.GET_UPLOAD_TOKEN_FAILED,
84
- 'Invalid folderId format',
85
- );
86
- }
87
-
88
45
  const response = await uploaderClient.getPrivateToken({
89
46
  body: {
90
47
  filename: params.filename,
91
48
  signature: params.signature,
92
49
  fsize: params.fsize,
50
+ sha256: params.sha256,
51
+ metadata: params.metadata,
93
52
  },
94
53
  });
95
54
 
96
55
  if (response.status === 200) {
97
- return response.body.data as UploadTokenData;
56
+ return response.body.data;
98
57
  }
99
58
 
100
59
  console.error(getErrorMsg(response.body, 'Failed to get upload token'));
@@ -102,49 +61,36 @@ export async function getUploadTokenPrivate(
102
61
  }
103
62
 
104
63
  /**
105
- * 初始化分片上传
64
+ * 初始化分片上传参数
106
65
  */
107
66
  export interface InitMultipartUploadParams {
108
67
  signature: string;
109
68
  filename: string;
110
69
  fsize: number;
111
- spaceId: string;
112
- folderId: string;
70
+ sha256?: string;
113
71
  metadata?: UploadMetadata;
114
72
  }
115
73
 
116
- export interface InitMultipartUploadData {
117
- uploadId?: string; // 秒传时可能没有 uploadId
118
- fileId: string;
119
- key: string;
120
- spaceId: string;
121
- // 秒传时可能返回完整的文件信息
122
- success?: boolean;
123
- fileSystem?: FileSystem;
124
- space?: Record<string, unknown>;
125
- parent?: Record<string, unknown>;
126
- }
74
+ /**
75
+ * 初始化分片上传响应数据
76
+ */
77
+ export type InitMultipartUploadData = TokenResponse;
127
78
 
128
79
  export async function initMultipartUpload(
129
80
  params: InitMultipartUploadParams,
130
81
  ): Promise<InitMultipartUploadData> {
131
- if (!isValidFolderId(params.folderId)) {
132
- throw new UploadError(
133
- UploadErrorCode.INIT_MULTIPART_FAILED,
134
- 'Invalid folderId format',
135
- );
136
- }
137
-
138
82
  const response = await uploaderClient.initMultipart({
139
83
  body: {
140
84
  signature: params.signature,
141
85
  filename: params.filename,
142
86
  fsize: params.fsize,
87
+ sha256: params.sha256,
88
+ metadata: params.metadata,
143
89
  },
144
90
  });
145
91
 
146
92
  if (response.status === 200) {
147
- return response.body.data as InitMultipartUploadData;
93
+ return response.body.data;
148
94
  }
149
95
 
150
96
  console.error(getErrorMsg(response.body, 'Failed to init multipart upload'));
@@ -152,7 +98,7 @@ export async function initMultipartUpload(
152
98
  }
153
99
 
154
100
  /**
155
- * 获取分片上传 Token
101
+ * 获取分片上传 Token 参数
156
102
  */
157
103
  export interface GetChunkUploadTokenParams {
158
104
  signature: string;
@@ -160,11 +106,12 @@ export interface GetChunkUploadTokenParams {
160
106
  uploadId: string;
161
107
  partNumber: number;
162
108
  fsize: number;
163
- spaceId: string;
164
- folderId: string;
165
109
  key: string;
166
110
  }
167
111
 
112
+ /**
113
+ * 分片上传 Token 响应数据
114
+ */
168
115
  export interface ChunkUploadTokenData {
169
116
  token: string;
170
117
  }
@@ -192,30 +139,17 @@ export async function getChunkUploadToken(
192
139
  }
193
140
 
194
141
  /**
195
- * 完成上传
142
+ * 完成上传参数
196
143
  */
197
144
  export interface CompleteUploadParams {
198
145
  signature: string;
199
- filename: string;
200
146
  fileId: string;
201
- fsize: number;
202
- spaceId: string;
203
- uploadId?: string;
204
- parts?: Array<{ ETag: string; PartNumber: number }>;
205
- key?: string;
206
- thumbImg?: string;
207
- metadata?: UploadMetadata;
208
147
  }
209
148
 
210
- export interface CompleteUploadData {
211
- data?: {
212
- url?: string;
213
- origin?: string;
214
- fileSystem?: FileSystem;
215
- };
216
- fileSystem?: FileSystem;
217
- success?: boolean;
218
- }
149
+ /**
150
+ * 完成上传响应数据
151
+ */
152
+ export type CompleteUploadData = FileSourceResponse;
219
153
 
220
154
  export async function completeUpload(
221
155
  params: CompleteUploadParams,
@@ -228,7 +162,7 @@ export async function completeUpload(
228
162
  });
229
163
 
230
164
  if (response.status === 200) {
231
- return response.body.data as CompleteUploadData;
165
+ return response.body.data;
232
166
  }
233
167
 
234
168
  console.error(getErrorMsg(response.body, 'Failed to complete upload'));
@@ -236,12 +170,10 @@ export async function completeUpload(
236
170
  }
237
171
 
238
172
  /**
239
- * 取消上传
173
+ * 取消上传参数
240
174
  */
241
175
  export interface AbortUploadParams {
242
176
  signature: string;
243
- uploadId: string;
244
- spaceId: string;
245
177
  fileId: string;
246
178
  }
247
179
 
@@ -29,8 +29,6 @@ export interface BatchUploadProgress {
29
29
  }
30
30
 
31
31
  export interface BatchUploadOptions {
32
- spaceId: string;
33
- folderId: string;
34
32
  files: File[];
35
33
  metadata?: UploadMetadata;
36
34
  onProgress?: (progress: BatchUploadProgress) => void;
@@ -47,39 +45,11 @@ const MAX_CONCURRENT_UPLOADS = 3;
47
45
 
48
46
  /**
49
47
  * 批量上传文件(带并发控制)
50
- * 支持素材空间和简历空间上传
51
48
  */
52
- export function uploadFiles(
53
- options: BatchUploadOptions,
54
- ): Promise<{ success: number; failed: number }>;
55
- /**
56
- * @deprecated 使用 uploadFiles(options) 代替
57
- */
58
- export function uploadFiles(
59
- spaceId: string,
60
- folderId: string,
61
- files: File[],
62
- onProgress?: (progress: BatchUploadProgress) => void,
63
- ): Promise<{ success: number; failed: number }>;
64
49
  export async function uploadFiles(
65
- optionsOrSpaceId: BatchUploadOptions | string,
66
- folderIdArg?: string,
67
- filesArg?: File[],
68
- onProgressArg?: (progress: BatchUploadProgress) => void,
50
+ options: BatchUploadOptions,
69
51
  ): Promise<{ success: number; failed: number }> {
70
- // 兼容旧的调用方式
71
- const options: BatchUploadOptions =
72
- typeof optionsOrSpaceId === 'string'
73
- ? {
74
- spaceId: optionsOrSpaceId,
75
- folderId: folderIdArg!,
76
- files: filesArg!,
77
- onProgress: onProgressArg,
78
- }
79
- : optionsOrSpaceId;
80
-
81
- const { spaceId, folderId, files, metadata, onProgress, onFileComplete } =
82
- options;
52
+ const { files, metadata, onProgress, onFileComplete } = options;
83
53
 
84
54
  if (files.length === 0) {
85
55
  return { success: 0, failed: 0 };
@@ -138,8 +108,6 @@ export async function uploadFiles(
138
108
 
139
109
  const result = await uploadFile({
140
110
  file,
141
- folderId,
142
- spaceId,
143
111
  metadata,
144
112
  callbacks: {
145
113
  onCalculating: (progress) => {
@@ -173,18 +141,14 @@ export async function uploadFiles(
173
141
  updateProgress();
174
142
  },
175
143
  onComplete: (completeResult) => {
176
- // 检查是否为秒传
177
- const isInstant = isInstantUploadResult(completeResult);
178
-
179
- // 从完成结果中提取 fileId(秒传时特别重要)
144
+ // 从完成结果中提取 fileId
180
145
  const completedFileId = getFileIdFromResult(completeResult);
181
146
  if (completedFileId) {
182
147
  fileProgress.fileId = completedFileId;
183
148
  }
184
149
 
185
- fileProgress.status = isInstant ? 'instant' : 'success';
150
+ fileProgress.status = 'success';
186
151
  fileProgress.progress = 100;
187
- fileProgress.isInstantUpload = isInstant;
188
152
  fileProgress.uploadSpeed = undefined;
189
153
  completedCount++;
190
154
  updateProgress();
@@ -211,9 +175,8 @@ export async function uploadFiles(
211
175
  }
212
176
 
213
177
  // 调用文件完成回调
214
- const isInstant = isInstantUploadResult(result);
215
178
  if (fileProgress.fileId) {
216
- onFileComplete?.(file, fileProgress.fileId, result, isInstant);
179
+ onFileComplete?.(file, fileProgress.fileId, result, false);
217
180
  }
218
181
 
219
182
  return { success: true };
@@ -246,41 +209,11 @@ export async function uploadFiles(
246
209
  * 从上传结果中提取文件 ID
247
210
  */
248
211
  function getFileIdFromResult(result: UploadResult): string | null {
249
- if ('fileSystem' in result && result.fileSystem?.id) {
250
- return result.fileSystem.id;
212
+ if ('id' in result && result.id) {
213
+ return result.id;
251
214
  }
252
215
  if ('fileId' in result && result.fileId) {
253
216
  return result.fileId;
254
217
  }
255
- if ('data' in result && result.data?.fileSystem?.id) {
256
- return result.data.fileSystem.id;
257
- }
258
218
  return null;
259
219
  }
260
-
261
- /**
262
- * 检查上传结果是否为秒传
263
- * 秒传的判断条件:
264
- * 1. fileSystem 存在(文件已存在)
265
- * 2. 满足以下任一条件:
266
- * - success === true(普通上传秒传)
267
- * - uploadId 不存在或为空(大文件分片上传秒传)
268
- */
269
- function isInstantUploadResult(result: UploadResult): boolean {
270
- // 必须有 fileSystem 才可能是秒传
271
- if (!('fileSystem' in result) || !result.fileSystem) {
272
- return false;
273
- }
274
-
275
- // 普通上传秒传:success === true
276
- if ('success' in result && result.success === true) {
277
- return true;
278
- }
279
-
280
- // 大文件分片上传秒传:uploadId 不存在或为空
281
- if (!('uploadId' in result) || !result.uploadId) {
282
- return true;
283
- }
284
-
285
- return false;
286
- }