create-pardx-scaffold 0.1.1 → 0.1.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.
- package/package.json +1 -1
- package/template/.cursor/worktrees.json +37 -0
- package/template/.dockerignore +49 -0
- package/template/.mcp.json +26 -0
- package/template/.nvmrc +1 -0
- package/template/CLAUDE.md +85 -0
- package/template/apps/api/libs/domain/services/index.ts +7 -0
- package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/ip-info.module.ts +2 -0
- package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/ip-info.service.ts +2 -0
- package/template/apps/api/libs/infra/clients/internal/file-storage/dto/file.dto.ts +1 -1
- package/template/apps/api/libs/infra/common/ts-rest/response.helper.ts +9 -1
- package/template/apps/api/libs/infra/shared-services/email/email.module.ts +0 -2
- package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +1 -1
- package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +1 -1
- package/template/apps/api/libs/infra/shared-services/sms/sms.module.ts +0 -2
- package/template/apps/api/libs/infra/shared-services/uploader/uploader.service.ts +4 -4
- package/template/apps/api/package.json +15 -15
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/src/app.module.ts +5 -1
- package/template/apps/api/src/modules/uploader/uploader.controller.ts +305 -0
- package/template/apps/api/src/modules/uploader/uploader.module.ts +17 -0
- package/template/apps/web/.env.example +6 -4
- package/template/apps/web/components/error-boundary.tsx +166 -0
- package/template/apps/web/components/index.ts +10 -0
- package/template/apps/web/components.json +20 -0
- package/template/apps/web/config.ts +115 -0
- package/template/apps/web/eslint.config.mjs +4 -0
- package/template/apps/web/lib/api/avatar-upload.ts +1 -0
- package/template/apps/web/lib/api/contracts/client.ts +51 -30
- package/template/apps/web/lib/api/contracts/hooks/index.ts +0 -3
- package/template/apps/web/lib/api/contracts/hooks/notification.ts +42 -124
- package/template/apps/web/lib/api.ts +24 -1
- package/template/apps/web/lib/dynamic-import.tsx +121 -0
- package/template/apps/web/lib/logger.ts +113 -0
- package/template/apps/web/lib/upload/api.ts +37 -105
- package/template/apps/web/lib/upload/batch-uploader.ts +7 -74
- package/template/apps/web/lib/upload/uploader.ts +10 -74
- package/template/apps/web/locales/zh-CN/assessment.json +5 -0
- package/template/apps/web/locales/zh-CN/chat.json +6 -0
- package/template/apps/web/locales/zh-CN/common.json +38 -0
- package/template/apps/web/locales/zh-CN/creative.json +5 -0
- package/template/apps/web/locales/zh-CN/daily-challenge.json +6 -0
- package/template/apps/web/locales/zh-CN/errors.json +16 -0
- package/template/apps/web/locales/zh-CN/forms.json +18 -0
- package/template/apps/web/locales/zh-CN/memory.json +5 -0
- package/template/apps/web/locales/zh-CN/navigation.json +12 -0
- package/template/apps/web/locales/zh-CN/recommendation.json +5 -0
- package/template/apps/web/locales/zh-CN/recruitment.json +5 -0
- package/template/apps/web/locales/zh-CN/settings.json +7 -0
- package/template/apps/web/locales/zh-CN/subscription.json +6 -0
- package/template/apps/web/locales/zh-CN/validation.json +8 -0
- package/template/apps/web/package.json +14 -15
- package/template/apps/web/postcss.config.mjs +1 -0
- package/template/apps/web/proxy.ts +102 -0
- package/template/apps/web/public/logo.svg +21 -0
- package/template/apps/web/vitest.config.ts +69 -0
- package/template/apps/web/vitest.setup.ts +80 -0
- package/template/package.json +7 -7
- package/template/packages/constants/package.json +3 -1
- package/template/packages/constants/tsconfig.build.esm.json +8 -0
- package/template/packages/contracts/package.json +2 -2
- package/template/packages/contracts/src/schemas/uploader.schema.ts +33 -10
- package/template/packages/ui/.storybook/main.ts +28 -0
- package/template/packages/ui/.storybook/preview.ts +40 -0
- package/template/packages/ui/eslint.config.js +3 -0
- package/template/packages/ui/package.json +15 -2
- package/template/packages/ui/src/components/button.stories.tsx +171 -0
- package/template/packages/ui/src/styles/globals.css +1 -1
- package/template/packages/ui/tsconfig.json +1 -1
- package/template/packages/utils/package.json +2 -2
- package/template/packages/utils/tsconfig.build.esm.json +8 -0
- package/template/packages/validators/package.json +1 -1
- package/template/pnpm-lock.yaml +2263 -999
- package/template/scripts/export-scaffold-for-create.js +65 -0
- package/template/apps/api/libs/infra/utils/download.ts +0 -21
- package/template/apps/web/lib/api/client.ts +0 -649
- package/template/apps/web/lib/audio-buffer-queue.ts +0 -273
- package/template/apps/web/lib/upload/folder-utils.ts +0 -295
- /package/template/apps/api/libs/{infra/shared-services → domain/services}/ip-info/index.ts +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { Controller, Req, VERSION_NEUTRAL } from '@nestjs/common';
|
|
2
|
+
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
|
|
3
|
+
import { uploaderContract as c } from '@repo/contracts/api';
|
|
4
|
+
import { success } from '@/common/ts-rest/response.helper';
|
|
5
|
+
import { UploaderService } from '@app/shared-services/uploader';
|
|
6
|
+
import { FileSourceService } from '@app/db';
|
|
7
|
+
import { FileStorageService } from '@app/shared-services/file-storage';
|
|
8
|
+
import { ConfigService } from '@nestjs/config';
|
|
9
|
+
import { AppConfig } from '@/config/validation';
|
|
10
|
+
import fileUtil from '@/utils/file.util';
|
|
11
|
+
import ipUtil from '@/utils/ip.util';
|
|
12
|
+
import { FileBucketVendor } from '@prisma/client';
|
|
13
|
+
import { AuthenticatedRequest } from '@app/auth';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 文件上传控制器
|
|
17
|
+
*
|
|
18
|
+
* 提供文件上传相关的 API 端点,包括:
|
|
19
|
+
* - 获取上传凭证(私有/公开/缩略图)
|
|
20
|
+
* - 分片上传初始化和凭证获取
|
|
21
|
+
* - 上传完成确认
|
|
22
|
+
* - 上传取消
|
|
23
|
+
*
|
|
24
|
+
* @version VERSION_NEUTRAL
|
|
25
|
+
*
|
|
26
|
+
* 版本控制说明:
|
|
27
|
+
* - VERSION_NEUTRAL 表示该控制器为"版本中立"
|
|
28
|
+
* - 接受任何版本的请求,或者没有版本头的请求
|
|
29
|
+
* - 客户端调用时无需提供 `x-api-version` 请求头
|
|
30
|
+
* - 适用于上传接口这类稳定、不需要版本迭代的基础设施接口
|
|
31
|
+
*
|
|
32
|
+
* 如需版本控制,可改为:
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // 指定版本(需要 x-api-version: 1 请求头)
|
|
36
|
+
* @Controller({ version: '1' })
|
|
37
|
+
*
|
|
38
|
+
* // 支持多版本
|
|
39
|
+
* @Controller({ version: ['1', '2'] })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
@Controller({
|
|
43
|
+
/**
|
|
44
|
+
* VERSION_NEUTRAL: 版本中立模式
|
|
45
|
+
*
|
|
46
|
+
* 在 Header 版本控制模式下(x-api-version):
|
|
47
|
+
* - 接受任何版本号的请求
|
|
48
|
+
* - 接受没有版本头的请求
|
|
49
|
+
* - 不会因版本不匹配而拒绝请求
|
|
50
|
+
*
|
|
51
|
+
* 适用场景:
|
|
52
|
+
* - 基础设施接口(上传、健康检查等)
|
|
53
|
+
* - 不需要版本迭代的稳定接口
|
|
54
|
+
* - 需要向后兼容的公共接口
|
|
55
|
+
*/
|
|
56
|
+
version: VERSION_NEUTRAL,
|
|
57
|
+
})
|
|
58
|
+
export class UploaderController {
|
|
59
|
+
private appConfig: AppConfig;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly uploaderService: UploaderService,
|
|
63
|
+
private readonly fileSourceDb: FileSourceService,
|
|
64
|
+
private readonly fileStorageService: FileStorageService,
|
|
65
|
+
private readonly configService: ConfigService,
|
|
66
|
+
) {
|
|
67
|
+
this.appConfig = configService.get<AppConfig>('app');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@TsRestHandler(c.getPrivateThumbToken)
|
|
71
|
+
async getPrivateThumbToken(@Req() req: AuthenticatedRequest) {
|
|
72
|
+
return tsRestHandler(c.getPrivateThumbToken, async ({ body }) => {
|
|
73
|
+
const userId = req.userId;
|
|
74
|
+
const ip = ipUtil.extractIp(req);
|
|
75
|
+
|
|
76
|
+
this.uploaderService.checkValidateAndReturnSignatureData(
|
|
77
|
+
userId,
|
|
78
|
+
body as any,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = await this.uploaderService.uploadThumbToken(
|
|
82
|
+
userId,
|
|
83
|
+
body as any,
|
|
84
|
+
ip,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return success({
|
|
88
|
+
token: result.token,
|
|
89
|
+
key: result.key,
|
|
90
|
+
fileId: result.key,
|
|
91
|
+
bucket: result.bucket,
|
|
92
|
+
url: `${result.domain}/${result.key}`,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@TsRestHandler(c.initMultipart)
|
|
98
|
+
async initMultipart(@Req() req: AuthenticatedRequest) {
|
|
99
|
+
return tsRestHandler(c.initMultipart, async ({ body }) => {
|
|
100
|
+
const userId = req.userId;
|
|
101
|
+
const ip = ipUtil.extractIp(req);
|
|
102
|
+
|
|
103
|
+
const signatureData =
|
|
104
|
+
this.uploaderService.checkValidateAndReturnSignatureData(
|
|
105
|
+
userId,
|
|
106
|
+
body as any,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const vendor =
|
|
110
|
+
body.vendor ?? (this.appConfig.defaultVendor as FileBucketVendor);
|
|
111
|
+
const bucket = await this.fileStorageService.getBucketString(
|
|
112
|
+
body.bucket,
|
|
113
|
+
ip,
|
|
114
|
+
false,
|
|
115
|
+
body.locale,
|
|
116
|
+
vendor,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const ext = fileUtil.getFileExtension(body.filename);
|
|
120
|
+
const key = await this.fileStorageService.formatNewKeyString(
|
|
121
|
+
'private',
|
|
122
|
+
ext,
|
|
123
|
+
bucket,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Create file source record
|
|
127
|
+
const fileSource = await this.fileSourceDb.create({
|
|
128
|
+
key,
|
|
129
|
+
bucket,
|
|
130
|
+
vendor,
|
|
131
|
+
fsize: body.fsize,
|
|
132
|
+
mimeType: fileUtil.getMimeType(body.filename),
|
|
133
|
+
ext,
|
|
134
|
+
sha256: body.sha256 || signatureData.sha256,
|
|
135
|
+
isUploaded: false,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Get multipart upload ID
|
|
139
|
+
const uploadId = await this.fileStorageService.getMultipartUploadId(
|
|
140
|
+
vendor,
|
|
141
|
+
bucket,
|
|
142
|
+
key,
|
|
143
|
+
ip,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Get presigned URL for first part
|
|
147
|
+
const token = await this.fileStorageService.getPresignedUrl(
|
|
148
|
+
vendor,
|
|
149
|
+
bucket,
|
|
150
|
+
{ uploadId, key, partNumber: 1 },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return success({
|
|
154
|
+
token,
|
|
155
|
+
key,
|
|
156
|
+
fileId: fileSource.id,
|
|
157
|
+
bucket,
|
|
158
|
+
url: uploadId,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@TsRestHandler(c.getMultipartToken)
|
|
164
|
+
async getMultipartToken(@Req() req: AuthenticatedRequest) {
|
|
165
|
+
return tsRestHandler(c.getMultipartToken, async ({ body }) => {
|
|
166
|
+
const userId = req.userId;
|
|
167
|
+
const ip = ipUtil.extractIp(req);
|
|
168
|
+
|
|
169
|
+
this.uploaderService.checkValidateAndReturnSignatureData(
|
|
170
|
+
userId,
|
|
171
|
+
body as any,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const result = await this.uploaderService.getUploaderPresignedUrl(
|
|
175
|
+
body as any,
|
|
176
|
+
ip,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const defaultBucket = await this.fileStorageService.getDefaultBucket();
|
|
180
|
+
|
|
181
|
+
return success({
|
|
182
|
+
token: result.token,
|
|
183
|
+
key: result.fileKey,
|
|
184
|
+
fileId: body.key || result.fileKey,
|
|
185
|
+
bucket: body.bucket || defaultBucket,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@TsRestHandler(c.getPrivateToken)
|
|
191
|
+
async getPrivateToken(@Req() req: AuthenticatedRequest) {
|
|
192
|
+
return tsRestHandler(c.getPrivateToken, async ({ body }) => {
|
|
193
|
+
const userId = req.userId;
|
|
194
|
+
const ip = ipUtil.extractIp(req);
|
|
195
|
+
|
|
196
|
+
const signatureData =
|
|
197
|
+
this.uploaderService.checkValidateAndReturnSignatureData(
|
|
198
|
+
userId,
|
|
199
|
+
body as any,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const vendor =
|
|
203
|
+
body.vendor ?? (this.appConfig.defaultVendor as FileBucketVendor);
|
|
204
|
+
const bucket = await this.fileStorageService.getBucketString(
|
|
205
|
+
body.bucket,
|
|
206
|
+
ip,
|
|
207
|
+
false,
|
|
208
|
+
body.locale,
|
|
209
|
+
vendor,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const ext = fileUtil.getFileExtension(body.filename);
|
|
213
|
+
const key = await this.fileStorageService.formatNewKeyString(
|
|
214
|
+
'private',
|
|
215
|
+
ext,
|
|
216
|
+
bucket,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Create file source record
|
|
220
|
+
const fileSource = await this.fileSourceDb.create({
|
|
221
|
+
key,
|
|
222
|
+
bucket,
|
|
223
|
+
vendor,
|
|
224
|
+
fsize: body.fsize,
|
|
225
|
+
mimeType: fileUtil.getMimeType(body.filename),
|
|
226
|
+
ext,
|
|
227
|
+
sha256: body.sha256 || signatureData.sha256,
|
|
228
|
+
isUploaded: false,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Get upload token with callback
|
|
232
|
+
const result = await this.uploaderService.uploadTokenWithCallback(
|
|
233
|
+
vendor,
|
|
234
|
+
bucket,
|
|
235
|
+
key,
|
|
236
|
+
ip,
|
|
237
|
+
body.locale,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const config = await this.fileStorageService.getFileServiceConfig(
|
|
241
|
+
vendor,
|
|
242
|
+
bucket,
|
|
243
|
+
ip,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return success({
|
|
247
|
+
token: result.token,
|
|
248
|
+
key: result.fileKey,
|
|
249
|
+
fileId: fileSource.id,
|
|
250
|
+
bucket,
|
|
251
|
+
url: `${config.domain}/${result.fileKey}`,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@TsRestHandler(c.abort)
|
|
257
|
+
async abort(@Req() req: AuthenticatedRequest) {
|
|
258
|
+
return tsRestHandler(c.abort, async ({ body }) => {
|
|
259
|
+
const userId = req.userId;
|
|
260
|
+
|
|
261
|
+
this.uploaderService.checkValidateAndReturnSignatureData(userId, {
|
|
262
|
+
signature: body.signature,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Mark file as deleted
|
|
266
|
+
await this.fileSourceDb.update({ id: body.fileId }, { isDeleted: true });
|
|
267
|
+
|
|
268
|
+
return success({ success: true });
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@TsRestHandler(c.complete)
|
|
273
|
+
async complete(@Req() req: AuthenticatedRequest) {
|
|
274
|
+
return tsRestHandler(c.complete, async ({ body }) => {
|
|
275
|
+
const userId = req.userId;
|
|
276
|
+
|
|
277
|
+
this.uploaderService.checkValidateAndReturnSignatureData(userId, {
|
|
278
|
+
signature: body.signature,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Get file source and mark as uploaded
|
|
282
|
+
const fileSource = await this.fileSourceDb.update(
|
|
283
|
+
{ id: body.fileId },
|
|
284
|
+
{ isUploaded: true },
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const config = await this.fileStorageService.getFileServiceConfig(
|
|
288
|
+
fileSource.vendor,
|
|
289
|
+
fileSource.bucket,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return success({
|
|
293
|
+
id: fileSource.id,
|
|
294
|
+
key: fileSource.key,
|
|
295
|
+
bucket: fileSource.bucket,
|
|
296
|
+
fsize: fileSource.fsize,
|
|
297
|
+
mimeType: fileSource.mimeType,
|
|
298
|
+
ext: fileSource.ext,
|
|
299
|
+
sha256: fileSource.sha256,
|
|
300
|
+
isUploaded: fileSource.isUploaded,
|
|
301
|
+
url: `${config.domain}/${fileSource.key}`,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { UploaderController } from './uploader.controller';
|
|
4
|
+
import { UploaderModule as UploaderServiceModule } from '@app/shared-services/uploader';
|
|
5
|
+
import { FileStorageServiceModule } from '@app/shared-services/file-storage';
|
|
6
|
+
import { FileSourceModule } from '@app/db';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [
|
|
10
|
+
ConfigModule,
|
|
11
|
+
UploaderServiceModule,
|
|
12
|
+
FileStorageServiceModule,
|
|
13
|
+
FileSourceModule,
|
|
14
|
+
],
|
|
15
|
+
controllers: [UploaderController],
|
|
16
|
+
})
|
|
17
|
+
export class UploaderModule {}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
# API
|
|
2
|
-
NEXT_PUBLIC_API_BASE_URL=http://localhost:
|
|
1
|
+
# API 配置
|
|
2
|
+
NEXT_PUBLIC_API_BASE_URL=http://localhost:13100/api
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
NEXT_PUBLIC_BRAND_NAME='Pardx.ai'
|
|
5
|
+
NEXT_PUBLIC_BRAND_LOGO='/logo.svg'
|
|
6
|
+
NEXT_PUBLIC_BRAND_TITLE=PardxAI - Multi-Agent Content Creation & Knowledge Management Platform
|
|
7
|
+
NEXT_PUBLIC_BRAND_DESCRIPTION=PardxAI is an AI-powered multi-agent platform for content creation, knowledge management, and intelligent operations. Features include AI writing, evolutionary knowledge base, smart recommendations, AI recruitment agents, and meeting intelligence.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Component, type ReactNode, type ErrorInfo } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Error Boundary Props
|
|
7
|
+
*/
|
|
8
|
+
interface ErrorBoundaryProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/** 自定义错误回退 UI */
|
|
11
|
+
fallback?: ReactNode;
|
|
12
|
+
/** 错误回调函数 */
|
|
13
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
14
|
+
/** 是否显示重试按钮 */
|
|
15
|
+
showRetry?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error Boundary State
|
|
20
|
+
*/
|
|
21
|
+
interface ErrorBoundaryState {
|
|
22
|
+
hasError: boolean;
|
|
23
|
+
error?: Error;
|
|
24
|
+
errorInfo?: ErrorInfo;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 默认错误回退 UI
|
|
29
|
+
*/
|
|
30
|
+
function DefaultErrorFallback({
|
|
31
|
+
error,
|
|
32
|
+
onRetry,
|
|
33
|
+
showRetry = true,
|
|
34
|
+
}: {
|
|
35
|
+
error?: Error;
|
|
36
|
+
onRetry?: () => void;
|
|
37
|
+
showRetry?: boolean;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex min-h-[200px] flex-col items-center justify-center p-8 text-center">
|
|
41
|
+
<div className="mb-4 text-6xl">😵</div>
|
|
42
|
+
<h2 className="mb-2 text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
43
|
+
出错了
|
|
44
|
+
</h2>
|
|
45
|
+
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
|
46
|
+
{error?.message || '页面发生了意外错误'}
|
|
47
|
+
</p>
|
|
48
|
+
{showRetry && onRetry && (
|
|
49
|
+
<button
|
|
50
|
+
onClick={onRetry}
|
|
51
|
+
className="rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700"
|
|
52
|
+
>
|
|
53
|
+
重试
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* React Error Boundary 组件
|
|
62
|
+
*
|
|
63
|
+
* 用于捕获子组件树中的 JavaScript 错误,记录错误并显示回退 UI
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* <ErrorBoundary fallback={<CustomError />}>
|
|
68
|
+
* <MyComponent />
|
|
69
|
+
* </ErrorBoundary>
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export class ErrorBoundary extends Component<
|
|
73
|
+
ErrorBoundaryProps,
|
|
74
|
+
ErrorBoundaryState
|
|
75
|
+
> {
|
|
76
|
+
constructor(props: ErrorBoundaryProps) {
|
|
77
|
+
super(props);
|
|
78
|
+
this.state = { hasError: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
82
|
+
return { hasError: true, error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
86
|
+
// 记录错误信息
|
|
87
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
88
|
+
|
|
89
|
+
// 更新状态以包含错误信息
|
|
90
|
+
this.setState({ errorInfo });
|
|
91
|
+
|
|
92
|
+
// 调用错误回调
|
|
93
|
+
this.props.onError?.(error, errorInfo);
|
|
94
|
+
|
|
95
|
+
// 在生产环境中,可以发送到错误追踪服务
|
|
96
|
+
if (process.env.NODE_ENV === 'production') {
|
|
97
|
+
// TODO: 发送到错误追踪服务 (如 Sentry)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
handleRetry = () => {
|
|
102
|
+
this.setState({ hasError: false, error: undefined, errorInfo: undefined });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
render() {
|
|
106
|
+
if (this.state.hasError) {
|
|
107
|
+
// 如果提供了自定义 fallback,使用它
|
|
108
|
+
if (this.props.fallback) {
|
|
109
|
+
return this.props.fallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 否则使用默认错误 UI
|
|
113
|
+
return (
|
|
114
|
+
<DefaultErrorFallback
|
|
115
|
+
error={this.state.error}
|
|
116
|
+
onRetry={this.handleRetry}
|
|
117
|
+
showRetry={this.props.showRetry}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this.props.children;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 页面级 Error Boundary
|
|
128
|
+
* 用于包裹整个页面,提供更友好的错误提示
|
|
129
|
+
*/
|
|
130
|
+
export function PageErrorBoundary({ children }: { children: ReactNode }) {
|
|
131
|
+
return (
|
|
132
|
+
<ErrorBoundary
|
|
133
|
+
showRetry
|
|
134
|
+
onError={(error, errorInfo) => {
|
|
135
|
+
// 页面级错误可以记录更多上下文
|
|
136
|
+
console.error('Page error:', {
|
|
137
|
+
error: error.message,
|
|
138
|
+
stack: error.stack,
|
|
139
|
+
componentStack: errorInfo.componentStack,
|
|
140
|
+
});
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{children}
|
|
144
|
+
</ErrorBoundary>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 组件级 Error Boundary
|
|
150
|
+
* 用于包裹单个组件,错误不会影响其他组件
|
|
151
|
+
*/
|
|
152
|
+
export function ComponentErrorBoundary({
|
|
153
|
+
children,
|
|
154
|
+
fallback,
|
|
155
|
+
}: {
|
|
156
|
+
children: ReactNode;
|
|
157
|
+
fallback?: ReactNode;
|
|
158
|
+
}) {
|
|
159
|
+
return (
|
|
160
|
+
<ErrorBoundary fallback={fallback} showRetry={false}>
|
|
161
|
+
{children}
|
|
162
|
+
</ErrorBoundary>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default ErrorBoundary;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "../../packages/ui/src/styles/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"iconLibrary": "lucide",
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"hooks": "@/hooks",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"utils": "@repo/ui/lib/utils",
|
|
18
|
+
"ui": "@repo/ui/components"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// API 配置
|
|
2
|
+
// API 基础地址从环境变量读取
|
|
3
|
+
// 在 .env.local 文件中设置以下环境变量:
|
|
4
|
+
// - NEXT_PUBLIC_API_BASE_URL: 登录、上传文件、测评等接口的基础地址
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 获取 API 基础地址(用于登录、上传文件、测评等)
|
|
8
|
+
*/
|
|
9
|
+
const getApiBaseUrl = (): string => {
|
|
10
|
+
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
|
|
11
|
+
|
|
12
|
+
if (!baseUrl) {
|
|
13
|
+
// Use default value during build time or development
|
|
14
|
+
const defaultUrl = 'http://localhost:3100/api';
|
|
15
|
+
if (
|
|
16
|
+
process.env.NODE_ENV === 'development' ||
|
|
17
|
+
process.env.NEXT_PHASE === 'phase-production-build'
|
|
18
|
+
) {
|
|
19
|
+
return defaultUrl;
|
|
20
|
+
}
|
|
21
|
+
// At runtime in production, use default if not set
|
|
22
|
+
console.warn('NEXT_PUBLIC_API_BASE_URL 未设置,使用默认值', defaultUrl);
|
|
23
|
+
return defaultUrl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return baseUrl;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取品牌名称
|
|
31
|
+
*/
|
|
32
|
+
const getBrandName = (): string => {
|
|
33
|
+
const brandName = process.env.NEXT_PUBLIC_BRAND_NAME;
|
|
34
|
+
return brandName || 'Pardx.AI';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取品牌 Logo 路径
|
|
39
|
+
*/
|
|
40
|
+
const getBrandLogo = (): string => {
|
|
41
|
+
const brandLogo = process.env.NEXT_PUBLIC_BRAND_LOGO;
|
|
42
|
+
return brandLogo || '/logo.svg';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 获取品牌完整标题(用于页面标题)
|
|
47
|
+
*
|
|
48
|
+
* 产品定位:多智能体驱动的内容创作与运营平台
|
|
49
|
+
* 核心能力:
|
|
50
|
+
* - AI 内容创作(智能写作、创意生成)
|
|
51
|
+
* - 知识库管理(进化型知识库、智能提取、质量评估)
|
|
52
|
+
* - 智能推荐(向量检索、协同过滤)
|
|
53
|
+
* - 招聘面试(AI 招聘 Agent、简历解析、智能匹配)
|
|
54
|
+
* - 会议管理(实时转写、知识提取、纪要生成)
|
|
55
|
+
* - 多智能体协作(AG-UI/Agno)
|
|
56
|
+
*/
|
|
57
|
+
const getBrandTitle = (): string => {
|
|
58
|
+
const brandTitle = process.env.NEXT_PUBLIC_BRAND_TITLE;
|
|
59
|
+
return brandTitle || 'PardxAI - Multi-Agent Content Creation ';
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 获取品牌描述(用于页面描述)
|
|
64
|
+
*
|
|
65
|
+
* 描述应包含:
|
|
66
|
+
* - 产品定位(多智能体平台)
|
|
67
|
+
* - 核心功能(内容创作、知识管理、智能推荐、招聘面试)
|
|
68
|
+
* - 技术优势(AI 驱动、多智能体协作)
|
|
69
|
+
* - 目标用户(企业团队、内容创作者、HR 团队)
|
|
70
|
+
*/
|
|
71
|
+
const getBrandDescription = (): string => {
|
|
72
|
+
const brandDescription = process.env.NEXT_PUBLIC_BRAND_DESCRIPTION;
|
|
73
|
+
return (
|
|
74
|
+
brandDescription ||
|
|
75
|
+
'PardxAI is an AI-powered multi-agent platform for content creation.'
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const API_CONFIG = {
|
|
80
|
+
// API 基础地址(用于登录、上传文件、测评等)
|
|
81
|
+
baseUrl: getApiBaseUrl(),
|
|
82
|
+
|
|
83
|
+
// API 端点路径 这些断点是不适用ts-rest-api的,用于登录和校验权限的断点,如果未来需要使用ts-rest-api,则将这些断点迁移到ts-rest-api中
|
|
84
|
+
endpoints: {
|
|
85
|
+
// 登录端点
|
|
86
|
+
login: '/sign/in/mobile/password',
|
|
87
|
+
|
|
88
|
+
// Token 刷新端点
|
|
89
|
+
refreshToken: '/sign/refresh/token',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 品牌配置
|
|
95
|
+
* 可通过环境变量覆盖:
|
|
96
|
+
* - NEXT_PUBLIC_BRAND_NAME: 品牌名称(默认: "Pardx.AI")
|
|
97
|
+
* - NEXT_PUBLIC_BRAND_LOGO: Logo 路径(默认: "/logo.svg")
|
|
98
|
+
* - NEXT_PUBLIC_BRAND_TITLE: 页面标题(默认: "PardxAI - Multi-Agent Content Creation")
|
|
99
|
+
* - NEXT_PUBLIC_BRAND_DESCRIPTION: 页面描述(默认: "PardxAI is an AI-powered multi-agent platform for content creation.")
|
|
100
|
+
*
|
|
101
|
+
* 产品定位说明:
|
|
102
|
+
* PardxAI 是一个多智能体驱动的内容创作与运营平台,提供以下核心能力:
|
|
103
|
+
* 1. AI 内容创作:智能写作、创意生成、多模态内容创作
|
|
104
|
+
* 2. 知识库管理:进化型知识库系统、智能知识提取、质量评估、版本控制、相似度检测与合并
|
|
105
|
+
* 3. 智能推荐:基于向量检索和协同过滤的知识推荐系统
|
|
106
|
+
* 4. 招聘面试:AI 招聘 Agent、简历解析、JD 分析、人才匹配、智能面试
|
|
107
|
+
* 5. 会议管理:实时转写、知识提取、智能纪要生成
|
|
108
|
+
* 6. 多智能体协作:AG-UI/Agno 集成,支持多智能体协同工作
|
|
109
|
+
*/
|
|
110
|
+
export const BRAND_CONFIG = {
|
|
111
|
+
name: getBrandName(),
|
|
112
|
+
logo: getBrandLogo(),
|
|
113
|
+
title: getBrandTitle(),
|
|
114
|
+
description: getBrandDescription(),
|
|
115
|
+
};
|