diff --git a/.claude/skills/repo-core-di/SKILL.md b/.claude/skills/repo-core-di/SKILL.md new file mode 100644 index 0000000..08bb077 --- /dev/null +++ b/.claude/skills/repo-core-di/SKILL.md @@ -0,0 +1,340 @@ +--- +name: repo-core-di +description: "@repo/core 依赖注入框架使用指南。当需要创建注入器、注册提供者、使用装饰器(@Injectable, @Inject, @Optional等)、处理生命周期(OnInit, OnDestroy)、或解决循环依赖时使用此技能。适用于:(1) 创建和配置注入器层次结构 (2) 注册各类提供者(Value/Class/Factory/Lazy) (3) 使用参数装饰器控制注入行为 (4) 实现服务生命周期管理 (5) 使用 InjectionToken 和 ForwardRef" +--- + +# @repo/core 依赖注入框架 + +## 核心概念 + +@repo/core 提供企业级依赖注入框架,支持层次化注入器、多种提供者类型、生命周期管理。 + +### 注入器层次结构 + +``` +Root (根注入器) + └── Platform (平台注入器) + └── Application (应用注入器) + └── Feature (特性注入器) +``` + +## 快速开始 + +### 创建注入器 + +```typescript +import { EnvironmentInjector, Injectable, InjectionToken } from '@repo/core' + +// 定义类型安全的令牌 +const API_URL = new InjectionToken('API_URL') + +// 创建根注入器 +const rootInjector = EnvironmentInjector.createRootInjector([ + { provide: API_URL, useValue: 'https://api.example.com' } +]) + +// 创建应用注入器(继承根注入器) +const appInjector = EnvironmentInjector.createApplicationInjector([ + UserService, + { provide: LoggerService, useClass: LoggerService } +]) + +// 初始化(执行所有 @OnInit) +await appInjector.init() + +// 获取服务 +const userService = appInjector.get(UserService) +``` + +### 使用 @Injectable 装饰器 + +```typescript +import { Injectable } from '@repo/core' + +// providedIn: 'root' - 单例,自动注册到根注入器 +@Injectable({ providedIn: 'root' }) +class ConfigService { + apiUrl = 'https://api.example.com' +} + +// providedIn: 'auto' - 不自动注册到根注入器,可在任何注入器中被按需解析 +// 推荐用于请求级服务 +@Injectable({ providedIn: 'auto' }) +class RequestScopedService { + // 每次请求创建新实例 +} + +// 使用工厂函数 +@Injectable({ + providedIn: 'root', + useFactory: (config: ConfigService) => new ApiClient(config.apiUrl), + deps: [ConfigService] +}) +class ApiClient { + constructor(public baseUrl: string) {} +} +``` + +## 提供者类型 + +### 值提供者 (ValueProvider) + +```typescript +const API_URL = new InjectionToken('API_URL') +const CONFIG_TOKEN = new InjectionToken('CONFIG_TOKEN') + +{ provide: API_URL, useValue: 'https://api.example.com' } +{ provide: CONFIG_TOKEN, useValue: { debug: true, timeout: 5000 } } +``` + +### 类提供者 (ClassProvider) + +```typescript +{ provide: UserService, useClass: UserService } +{ provide: Logger, useClass: ConsoleLogger } // 接口映射到实现 +``` + +### 工厂提供者 (FactoryProvider) + +```typescript +{ + provide: DatabaseConnection, + useFactory: (config: ConfigService) => { + return new DatabaseConnection(config.dbUrl) + }, + deps: [ConfigService] +} +``` + +### 别名提供者 (ExistingProvider) + +```typescript +const LOGGER_TOKEN = new InjectionToken('Logger') + +{ provide: LOGGER_TOKEN, useExisting: LoggerService } +``` + +### 延迟提供者 (LazyProvider) + +```typescript +const LAZY_CONFIG = new InjectionToken('LAZY_CONFIG') + +// 延迟类 - 首次访问时才实例化 +{ provide: HeavyService, useLazyClass: HeavyService } + +// 延迟工厂 +{ provide: LAZY_CONFIG, useLazyFactory: () => loadConfig(), deps: [] } +``` + +### 多值提供者 (Multi Provider) + +```typescript +{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } +{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true } + +// 获取时返回数组 +const interceptors = injector.get(HTTP_INTERCEPTORS) // [AuthInterceptor, LoggingInterceptor] +``` + +## 参数装饰器 + +### @Inject - 显式指定注入令牌 + +```typescript +const API_URL = new InjectionToken('API_URL') + +class UserController { + constructor( + @Inject(UserService) private userService: UserService, + @Inject(API_URL) private apiUrl: string + ) {} +} +``` + +### @Optional - 可选注入 + +```typescript +class NotificationService { + constructor( + @Optional(EmailService) private email?: EmailService // 不存在时为 null + ) {} +} +``` + +### @Self - 仅在当前注入器查找 + +```typescript +class ChildComponent { + constructor( + @Self(LocalService) private local: LocalService // 不查找父注入器 + ) {} +} +``` + +### @SkipSelf - 跳过当前注入器 + +```typescript +class ChildService { + constructor( + @SkipSelf(ParentService) private parent: ParentService // 从父注入器开始查找 + ) {} +} +``` + +### @Host - 在宿主注入器查找 + +```typescript +class DirectiveService { + constructor( + @Host(HostService) private host: HostService + ) {} +} +``` + +## 生命周期管理 + +### @OnInit 装饰器 + +```typescript +import { Injectable, OnInit } from '@repo/core' + +@Injectable({ providedIn: 'root' }) +class DatabaseService implements OnInit { + private connection: Connection + + @OnInit() + async onInit() { + this.connection = await this.connect() + console.log('Database connected') + } + + private async connect() { + // 连接逻辑 + } +} +``` + +### OnDestroy 接口 + +```typescript +import { Injectable, OnDestroy } from '@repo/core' + +@Injectable({ providedIn: 'root' }) +class CacheService implements OnDestroy { + async onDestroy() { + await this.flush() + console.log('Cache flushed') + } +} + +// 销毁注入器时自动调用 +await injector.destroy() +``` + +### APP_INITIALIZER - 应用初始化器 + +```typescript +import { APP_INITIALIZER, InjectionToken } from '@repo/core' + +const appInjector = EnvironmentInjector.createApplicationInjector([ + { + provide: APP_INITIALIZER, + useValue: { + provide: new InjectionToken('DB_INIT'), + deps: [ConfigService], + init: async () => { + await initDatabase() + } + }, + multi: true + }, + { + provide: APP_INITIALIZER, + useValue: { + provide: new InjectionToken('CACHE_INIT'), + deps: [new InjectionToken('DB_INIT')], // 依赖 DB_INIT + init: async () => { + await initCache() + } + }, + multi: true + } +]) + +// 按依赖顺序执行初始化器 +await appInjector.init() +``` + +## InjectionToken + +```typescript +import { InjectionToken } from '@repo/core' + +// 创建类型安全的令牌 +const API_URL = new InjectionToken('API_URL') +const CONFIG = new InjectionToken('CONFIG') +const LOGGER_LEVEL = new InjectionToken('LOGGER_LEVEL') + +// 注册 +const injector = EnvironmentInjector.createRootInjector([ + { provide: API_URL, useValue: 'https://api.example.com' }, + { provide: CONFIG, useValue: { debug: true } } +]) + +// 获取(类型安全) +const url: string = injector.get(API_URL) +const config: AppConfig = injector.get(CONFIG) +``` + +## ForwardRef - 解决循环依赖 + +```typescript +import { Injectable, Inject, forwardRef } from '@repo/core' + +@Injectable({ providedIn: 'root' }) +class ServiceA { + constructor( + @Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB + ) {} +} + +@Injectable({ providedIn: 'root' }) +class ServiceB { + constructor( + @Inject(forwardRef(() => ServiceA)) private serviceA: ServiceA + ) {} +} +``` + +## 便捷工厂函数 + +```typescript +import { + createRootInjector, + createPlatformInjector, + createApplicationInjector, + createFeatureInjector +} from '@repo/core' + +// 简化创建 +const root = createRootInjector([...providers]) +const platform = createPlatformInjector([...providers]) +const app = createApplicationInjector([...providers]) +const feature = createFeatureInjector([...providers], parentInjector) +``` + +## 最佳实践 + +1. **使用 providedIn: 'root'** - 大多数服务应该是单例 +2. **避免循环依赖** - 使用 ForwardRef 仅作为最后手段 +3. **使用 InjectionToken** - 为非类类型创建类型安全令牌 +4. **实现 OnDestroy** - 清理资源(连接、订阅等) +5. **使用 APP_INITIALIZER** - 管理复杂的初始化顺序 +6. **延迟加载** - 对重型服务使用 useLazyClass/useLazyFactory + +## 参考文档 + +- 详细 API 参考:见 [references/api.md](references/api.md) +- 提供者类型详解:见 [references/providers.md](references/providers.md) +- 请求级注入器:见 [references/request-scoped-injector.md](references/request-scoped-injector.md) - HTTP 请求隔离、CURRENT_USER 注入、Controller 生命周期 +- 错误处理系统:见 [references/errors.md](references/errors.md) - 预定义错误类、ErrorFactory、ErrorSerializer diff --git a/.claude/skills/repo-core-di/references/api.md b/.claude/skills/repo-core-di/references/api.md new file mode 100644 index 0000000..4b8304d --- /dev/null +++ b/.claude/skills/repo-core-di/references/api.md @@ -0,0 +1,228 @@ +# @repo/core 依赖注入 API 参考 + +## EnvironmentInjector + +核心注入器类,管理依赖的注册和解析。 + +### 静态方法 + +```typescript +// 创建根注入器 - 最顶层,无父注入器 +static createRootInjector(providers: Provider[]): EnvironmentInjector + +// 创建平台注入器 - 继承根注入器 +static createPlatformInjector(providers: Provider[]): EnvironmentInjector + +// 创建应用注入器 - 继承平台注入器 +static createApplicationInjector(providers: Provider[]): EnvironmentInjector + +// 创建特性注入器 - 需要指定父注入器 +static createFeatureInjector( + providers: Provider[], + parent: Injector +): EnvironmentInjector + +// 创建带自动提供者解析的注入器 +static createWithAutoProviders( + providers: Provider[], + parent: Injector | null, + scope: InjectorScope +): EnvironmentInjector +``` + +### 实例方法 + +```typescript +// 获取服务实例 +get(token: InjectionTokenType, defaultValue?: T): T + +// 动态注册提供者 +set(providers: Provider[]): void + +// 初始化 - 执行所有 @OnInit 和 APP_INITIALIZER +async init(): Promise + +// 销毁 - 执行所有 OnDestroy +async destroy(): Promise +``` + +## InjectionToken + +类型安全的注入令牌。 + +```typescript +class InjectionToken { + constructor(description: string) + toString(): string +} + +// 使用示例 +const API_URL = new InjectionToken('API_URL') +const CONFIG = new InjectionToken('CONFIG') +``` + +## HostAttributeToken + +宿主属性令牌,用于获取宿主元素属性。 + +```typescript +class HostAttributeToken { + constructor(attributeName: string, defaultValue?: T) +} + +// 使用示例 +const dataAttr = new HostAttributeToken('data-value', 'default') +``` + +## 装饰器 + +### @Injectable(options?) + +标记类为可注入服务。 + +```typescript +interface InjectableOptions { + providedIn?: 'root' | 'platform' | 'application' | 'feature' | 'auto' | null + useFactory?: (...args: any[]) => any + deps?: any[] +} + +@Injectable({ providedIn: 'root' }) +class MyService {} +``` + +### @Inject(token) + +显式指定注入令牌。 + +```typescript +constructor(@Inject(TOKEN) value: string) {} +``` + +### @Optional(token) + +可选注入,不存在时返回 null。 + +```typescript +constructor(@Optional(OptionalService) service?: OptionalService) {} +``` + +### @Self(token) + +仅在当前注入器查找。 + +```typescript +constructor(@Self(LocalService) service: LocalService) {} +``` + +### @SkipSelf(token) + +跳过当前注入器,从父注入器开始查找。 + +```typescript +constructor(@SkipSelf(ParentService) service: ParentService) {} +``` + +### @Host(token) + +在宿主注入器中查找。 + +```typescript +constructor(@Host(HostService) service: HostService) {} +``` + +### @OnInit() + +标记初始化方法。 + +```typescript +@OnInit() +async onInit() { + await this.initialize() +} +``` + +## 生命周期接口 + +### OnInit + +```typescript +interface OnInit { + onInit(): Promise +} +``` + +### OnDestroy + +```typescript +interface OnDestroy { + onDestroy(): Promise | void +} +``` + +## ForwardRef + +解决循环依赖。 + +```typescript +// 创建前向引用 +function forwardRef(fn: () => T): ForwardRef + +// 检查是否为前向引用 +function isForwardRef(ref: any): boolean + +// 解析前向引用 +function resolveForwardRef(ref: T | ForwardRef): T +``` + +## 内部标志位 + +用于性能优化的位标志。 + +```typescript +enum InternalInjectFlags { + Default = 0, + Optional = 1 << 0, // 1 + SkipSelf = 1 << 1, // 2 + Self = 1 << 2, // 4 + Host = 1 << 3, // 8 +} + +// 工具函数 +function combineInjectFlags(...flags: InternalInjectFlags[]): number +function hasFlag(flags: number, flag: InternalInjectFlags): boolean +function convertInjectOptionsToFlags(options: InjectOptions): number +``` + +## 类型定义 + +### InjectionTokenType + +```typescript +type InjectionTokenType = + | InjectionToken + | HostAttributeToken + | Type + | AbstractType + | StringToken + | SymbolToken + | Function + | ForwardRef> +``` + +### InjectOptions + +```typescript +interface InjectOptions { + optional?: boolean + skipSelf?: boolean + self?: boolean + host?: boolean +} +``` + +### InjectorScope + +```typescript +type InjectorScope = 'root' | 'platform' | 'application' | 'feature' | 'auto' +``` diff --git a/.claude/skills/repo-core-di/references/controller.md b/.claude/skills/repo-core-di/references/controller.md new file mode 100644 index 0000000..458c5b1 --- /dev/null +++ b/.claude/skills/repo-core-di/references/controller.md @@ -0,0 +1,371 @@ +--- +name: repo-core-controller +description: "@repo/core HTTP 控制器装饰器使用指南。当需要创建 REST API 端点、处理 HTTP 请求参数、实现权限控制、或使用 SSE 流式响应时使用此技能。适用于:(1) 使用 @Controller 和 HTTP 方法装饰器(@Get, @Post, @Put, @Delete, @Patch) (2) 使用参数装饰器(@Body, @Query, @Param, @Header) (3) 实现权限和会话验证(@RequirePermissions, @RequireSession) (4) 创建 SSE 端点(@Sse) (5) 添加 API 文档(@ApiDescription)" +--- + +# @repo/core HTTP 控制器 + +## 核心概念 + +@repo/core 提供声明式 HTTP 控制器装饰器,支持路由定义、参数绑定、权限控制和 SSE 流式响应。 + +## 快速开始 + +### 创建控制器 + +```typescript +import { Controller, Get, Post, Body, Param, Injectable } from '@repo/core' +import { z } from 'zod' + +@Controller('/api/users') +class UserController { + constructor(private userService: UserService) {} + + @Get('/list') + async getUsers() { + return this.userService.findAll() + } + + @Get('/:id') + async getUser(@Param('id') id: string) { + return this.userService.findById(id) + } + + @Post('/create', z.object({ + name: z.string(), + email: z.string().email() + })) + async createUser(@Body() data: { name: string; email: string }) { + return this.userService.create(data) + } +} +``` + +## HTTP 方法装饰器 + +### @Get(path) + +```typescript +@Get('/users') +async getUsers() { } + +@Get('/users/:id') +async getUser(@Param('id') id: string) { } + +@Get('/search') +async search(@Query('q') query: string) { } +``` + +### @Post(path, schema?, contentType?) + +```typescript +// 基本用法 +@Post('/users') +async createUser(@Body() data: CreateUserDto) { } + +// 带 Zod 验证 +@Post('/users', z.object({ + name: z.string().min(1), + email: z.string().email() +})) +async createUser(@Body() data: { name: string; email: string }) { } + +// 指定 Content-Type +@Post('/upload', schema, 'multipart/form-data') +async upload(@Body() data: FormData) { } +``` + +### @Put(path, schema?) + +```typescript +@Put('/users/:id', z.object({ + name: z.string().optional(), + email: z.string().email().optional() +})) +async updateUser( + @Param('id') id: string, + @Body() data: Partial +) { } +``` + +### @Delete(path) + +```typescript +@Delete('/users/:id') +async deleteUser(@Param('id') id: string) { } +``` + +### @Patch(path, schema?) + +```typescript +@Patch('/users/:id/status', z.object({ + status: z.enum(['active', 'inactive']) +})) +async updateStatus( + @Param('id') id: string, + @Body('status') status: string +) { } +``` + +### @Sse(path) - Server-Sent Events + +```typescript +@Sse('/events') +async *streamEvents() { + while (true) { + yield { data: { timestamp: Date.now() } } + await sleep(1000) + } +} + +@Sse('/notifications/:userId') +async *userNotifications(@Param('userId') userId: string) { + const stream = this.notificationService.subscribe(userId) + for await (const notification of stream) { + yield { event: 'notification', data: notification } + } +} +``` + +## 参数装饰器 + +### @Body(key?, schema?) + +```typescript +// 获取整个请求体 +@Post('/users') +async create(@Body() data: CreateUserDto) { } + +// 获取特定字段 +@Post('/users') +async create(@Body('name') name: string) { } + +// 带验证 +@Post('/users') +async create(@Body('email', z.string().email()) email: string) { } +``` + +### @Query(key?, schema?) + +```typescript +// 获取查询参数 +@Get('/search') +async search(@Query('q') query: string) { } + +// 带默认值和验证 +@Get('/list') +async list( + @Query('page', z.coerce.number().default(1)) page: number, + @Query('limit', z.coerce.number().default(10)) limit: number +) { } +``` + +### @Param(key) + +```typescript +@Get('/users/:id') +async getUser(@Param('id') id: string) { } + +@Get('/projects/:projectId/tasks/:taskId') +async getTask( + @Param('projectId') projectId: string, + @Param('taskId') taskId: string +) { } +``` + +### @Header(key) + +```typescript +@Get('/protected') +async protected(@Header('authorization') auth: string) { } + +@Post('/webhook') +async webhook( + @Header('x-signature') signature: string, + @Body() payload: any +) { } +``` + +### @Session() + +```typescript +@Get('/profile') +async getProfile(@Session() session: UserSession) { + return this.userService.findById(session.userId) +} +``` + +### @Req() / @Res() + +```typescript +@Get('/download') +async download(@Req() req: Request, @Res() res: Response) { + // 直接访问原始请求/响应对象 + res.setHeader('Content-Disposition', 'attachment') + return fileStream +} +``` + +### @Headers() + +```typescript +@Post('/webhook') +async webhook(@Headers() headers: Record) { + const signature = headers['x-signature'] + // ... +} +``` + +## 权限控制 + +### @RequireSession() + +```typescript +@Controller('/api/account') +class AccountController { + @Get('/profile') + @RequireSession() + async getProfile(@Session() session: UserSession) { + return this.userService.findById(session.userId) + } +} +``` + +### @RequirePermissions(permissions, mode?) + +```typescript +// 需要单个权限 +@Delete('/users/:id') +@RequirePermissions({ user: ['delete'] }) +async deleteUser(@Param('id') id: string) { } + +// 需要多个权限 (AND 模式 - 全部满足) +@Put('/projects/:id') +@RequirePermissions({ project: ['read', 'update'] }, 'AND') +async updateProject(@Param('id') id: string) { } + +// OR 模式 - 满足任一即可 +@Get('/reports') +@RequirePermissions({ report: ['read'], admin: ['access'] }, 'OR') +async getReports() { } +``` + +### 权限资源类型 + +```typescript +// 可用的权限资源和操作 +const permissions = { + activity: ['create', 'read', 'update', 'delete'], + project: ['create', 'read', 'list', 'update', 'delete', 'run'], + template: ['create', 'read', 'list', 'update', 'delete', 'run'], + user: ['read', 'update', 'delete'], + admin: ['access', 'manage'], + report: ['read', 'export'] +} +``` + +## API 文档 + +### @ApiDescription(description, tags?) + +```typescript +@Controller('/api/users') +class UserController { + @Get('/list') + @ApiDescription('获取用户列表', ['Users', 'Admin']) + async getUsers() { } + + @Post('/create') + @ApiDescription('创建新用户', ['Users']) + async createUser(@Body() data: CreateUserDto) { } +} +``` + +## 完整示例 + +```typescript +import { + Controller, Get, Post, Put, Delete, Sse, + Body, Query, Param, Header, Session, + RequireSession, RequirePermissions, ApiDescription, + Injectable +} from '@repo/core' +import { z } from 'zod' + +const CreateUserSchema = z.object({ + name: z.string().min(1).max(100), + email: z.string().email(), + role: z.enum(['user', 'admin']).default('user') +}) + +const UpdateUserSchema = CreateUserSchema.partial() + +@Controller('/api/users') +class UserController { + constructor( + private userService: UserService, + private notificationService: NotificationService + ) {} + + @Get('/list') + @RequireSession() + @ApiDescription('获取用户列表', ['Users']) + async list( + @Query('page', z.coerce.number().default(1)) page: number, + @Query('limit', z.coerce.number().default(20)) limit: number, + @Query('search') search?: string + ) { + return this.userService.findAll({ page, limit, search }) + } + + @Get('/:id') + @RequireSession() + @ApiDescription('获取用户详情', ['Users']) + async getById(@Param('id') id: string) { + return this.userService.findById(id) + } + + @Post('/create', CreateUserSchema) + @RequirePermissions({ user: ['create'] }) + @ApiDescription('创建用户', ['Users', 'Admin']) + async create(@Body() data: z.infer) { + return this.userService.create(data) + } + + @Put('/:id', UpdateUserSchema) + @RequirePermissions({ user: ['update'] }) + @ApiDescription('更新用户', ['Users', 'Admin']) + async update( + @Param('id') id: string, + @Body() data: z.infer + ) { + return this.userService.update(id, data) + } + + @Delete('/:id') + @RequirePermissions({ user: ['delete'] }) + @ApiDescription('删除用户', ['Users', 'Admin']) + async delete(@Param('id') id: string) { + return this.userService.delete(id) + } + + @Sse('/notifications') + @RequireSession() + @ApiDescription('用户通知流', ['Users', 'Realtime']) + async *notifications(@Session() session: UserSession) { + const stream = this.notificationService.subscribe(session.userId) + for await (const notification of stream) { + yield { event: 'notification', data: notification } + } + } +} +``` + +## 最佳实践 + +1. **使用 Zod 验证** - 始终为 POST/PUT/PATCH 请求定义 schema +2. **分层权限** - 使用 @RequireSession 和 @RequirePermissions 组合 +3. **API 文档** - 为所有端点添加 @ApiDescription +4. **参数验证** - 使用 @Query 和 @Body 的 schema 参数 +5. **SSE 清理** - 确保 SSE 生成器正确处理客户端断开 +6. **控制器单一职责** - 每个控制器处理一个资源类型 diff --git a/.claude/skills/repo-core-di/references/errors.md b/.claude/skills/repo-core-di/references/errors.md new file mode 100644 index 0000000..25b1b1c --- /dev/null +++ b/.claude/skills/repo-core-di/references/errors.md @@ -0,0 +1,450 @@ +--- +name: repo-core-errors +description: "@repo/core 错误处理系统使用指南。当需要创建自定义错误、处理 HTTP 错误响应、序列化错误信息、或使用错误工厂时使用此技能。适用于:(1) 使用预定义错误类(ValidationError, NotFoundError, UnauthorizedError等) (2) 创建业务逻辑错误 (3) 使用 ErrorFactory 快速创建错误 (4) 序列化错误用于 API 响应 (5) 实现全局错误处理" +--- + +# @repo/core 错误处理系统 + +## 核心概念 + +@repo/core 提供完整的错误处理系统,包括预定义错误类、错误工厂、错误序列化和全局错误处理器。 + +## 错误类层次结构 + +``` +Error (原生) +└── AppError (基础应用错误) + ├── NoRetryError (不可重试错误) + ├── HTTP 错误 + │ ├── ValidationError (400) + │ ├── UnauthorizedError (401) + │ ├── ForbiddenError (403) + │ ├── NotFoundError (404) + │ ├── ConflictError (409) + │ ├── UnprocessableEntityError (422) + │ ├── RateLimitError (429) + │ ├── InternalError (500) + │ └── ServiceUnavailableError (503) + └── 业务错误 + ├── DatabaseError + ├── ExternalServiceError + ├── ConfigurationError + └── BusinessRuleError +``` + +## 快速开始 + +### 抛出错误 + +```typescript +import { + ValidationError, + NotFoundError, + UnauthorizedError, + errorFactory // 注意:使用小写的实例 +} from '@repo/core' + +// 直接使用错误类 +throw new NotFoundError('User', '123') // "User with identifier '123' not found" + +// 使用错误工厂实例 +throw errorFactory.notFound('User', '123') +throw errorFactory.validation('Invalid email format', { email: 'Invalid format' }) +``` + +### 处理错误 + +```typescript +import { ErrorSerializer, ErrorHandler } from '@repo/core' + +try { + await userService.findById(id) +} catch (error) { + // 序列化错误用于 API 响应 + const serialized = ErrorSerializer.serialize(error) + return Response.json(serialized, { status: serialized.statusCode }) +} +``` + +## HTTP 错误类 + +### ValidationError (400) + +```typescript +// 基本用法 +throw new ValidationError('Invalid input') + +// 带字段错误信息 +throw new ValidationError('Validation failed', { + email: 'Invalid email format', + age: 'Must be a positive number' +}) + +// 使用工厂 +throw errorFactory.validation('Email is required', { email: 'Required field' }) +``` + +### UnauthorizedError (401) + +```typescript +// 未认证(使用默认消息) +throw new UnauthorizedError() // "Authentication required" + +// 自定义消息 +throw new UnauthorizedError('Token expired') + +// 使用工厂 +throw errorFactory.unauthorized('Invalid credentials') +``` + +### ForbiddenError (403) + +```typescript +// 无权限(使用默认消息) +throw new ForbiddenError() // "Access forbidden" + +// 自定义消息 +throw new ForbiddenError('Cannot access this resource') + +// 使用工厂 +throw errorFactory.forbidden('Insufficient permissions') +``` + +### NotFoundError (404) + +```typescript +// 基本用法 - 只有资源类型 +throw new NotFoundError('User') // "User not found" + +// 带资源标识符 +throw new NotFoundError('User', '123') // "User with identifier '123' not found" + +// 使用工厂 - 推荐 +throw errorFactory.notFound('User', '123') +``` + +### ConflictError (409) + +```typescript +// 资源冲突 +throw new ConflictError('Email already exists') + +// 带冲突详情 +throw new ConflictError('Duplicate entry', { + field: 'email', + value: 'user@example.com' +}) + +// 使用工厂 +throw errorFactory.conflict('Email already registered', { email: 'user@example.com' }) +``` + +### UnprocessableEntityError (422) + +```typescript +// 业务规则验证失败 +throw new UnprocessableEntityError('Cannot process request', { + reason: 'Insufficient balance' +}) + +// 使用工厂 +throw errorFactory.unprocessable('Cannot process request', { reason: 'Insufficient balance' }) +``` + +### RateLimitError (429) + +```typescript +// 速率限制(使用默认消息) +throw new RateLimitError() // "Rate limit exceeded" + +// 自定义消息和重试时间 +throw new RateLimitError('Too many requests', 60) // 60秒后重试 + +// 使用工厂 +throw errorFactory.rateLimit('Too many requests', 60) +``` + +### InternalError (500) + +```typescript +// 内部错误 +throw new InternalError('Unexpected error occurred') + +// 包装原始错误 +throw new InternalError('Database operation failed', originalError) + +// 使用工厂 +throw errorFactory.internal('Something went wrong', originalError) +``` + +### ServiceUnavailableError (503) + +```typescript +// 服务不可用(使用默认消息) +throw new ServiceUnavailableError() // "Service temporarily unavailable" + +// 自定义消息和重试时间 +throw new ServiceUnavailableError('Service temporarily unavailable', 300) + +// 使用工厂 +throw errorFactory.unavailable('Service down for maintenance', 300) +``` + +## 业务错误类 + +### DatabaseError + +```typescript +import { DatabaseError } from '@repo/core' + +// 基本用法 +throw new DatabaseError('Connection failed') + +// 带操作信息 +throw new DatabaseError('Query timeout', 'SELECT') + +// 带原始错误 +throw new DatabaseError('Connection failed', 'connect', originalError) + +// 使用工厂 +throw errorFactory.database('Query timeout', 'SELECT', originalError) +``` + +### ExternalServiceError + +```typescript +import { ExternalServiceError } from '@repo/core' + +// 基本用法 +throw new ExternalServiceError('stripe', 'Payment failed') + +// 带原始错误 +throw new ExternalServiceError('stripe', 'Payment gateway error', gatewayError) + +// 使用工厂 +throw errorFactory.externalService('Stripe', 'Payment failed', originalError) +``` + +### ConfigurationError + +```typescript +import { ConfigurationError } from '@repo/core' + +// 基本用法 +throw new ConfigurationError('Missing required configuration') + +// 带配置键 +throw new ConfigurationError('Missing required configuration', 'DATABASE_URL') + +// 使用工厂 +throw errorFactory.configuration('DATABASE_URL is not set', 'DATABASE_URL') +``` + +### BusinessRuleError + +```typescript +import { BusinessRuleError } from '@repo/core' + +// 基本用法 +throw new BusinessRuleError('Cannot delete active subscription') + +// 带规则标识 +throw new BusinessRuleError('Cannot delete active subscription', 'SUBSCRIPTION_ACTIVE') + +// 带额外元数据 +throw new BusinessRuleError('Insufficient balance', 'BALANCE_CHECK', { + required: 100, + available: 50 +}) + +// 使用工厂 +throw errorFactory.businessRule('Insufficient balance for withdrawal', 'BALANCE_CHECK', { amount: 100 }) +``` + +## errorFactory + +错误工厂是一个 `@Injectable()` 服务实例,提供便捷的错误创建方法。 + +```typescript +import { errorFactory } from '@repo/core' + +// HTTP 错误 +errorFactory.validation(message, fields?) +errorFactory.unauthorized(message?) +errorFactory.forbidden(message?) +errorFactory.notFound(resource, identifier?) +errorFactory.conflict(message, meta?) +errorFactory.unprocessable(message, meta?) +errorFactory.rateLimit(message?, retryAfter?) +errorFactory.internal(message, originalError?) +errorFactory.unavailable(message?, retryAfter?) + +// 业务错误 +errorFactory.database(message, operation?, originalError?) +errorFactory.externalService(service, message, originalError?) +errorFactory.configuration(message, configKey?) +errorFactory.businessRule(message, rule?, meta?) +``` + +> **注意**: `errorFactory` 是预创建的实例(小写),可以直接导入使用。 +> 如果需要依赖注入,可以注入 `ErrorFactory` 类。 + +## ErrorSerializer + +错误序列化器用于将错误转换为 API 响应格式。 + +```typescript +import { ErrorSerializer } from '@repo/core' + +// 序列化错误 +const serialized = ErrorSerializer.serialize(error) +// { +// message: 'User not found', +// name: 'NotFoundError', +// type: 'NOT_FOUND', +// statusCode: 404, +// response: { userId: '123' } +// } + +// 包含堆栈信息(开发环境) +const withStack = ErrorSerializer.serialize(error, true) + +// 提取最深层错误 +const deepest = ErrorSerializer.extractDeepestError(wrappedError) + +// 获取完整错误描述 +const description = ErrorSerializer.getFullDescription(error) +``` + +### SerializedError 结构 + +```typescript +interface SerializedError { + message: string + name: string + type?: string + statusCode?: number + response?: any + stack?: string + cause?: SerializedError + originalError?: any +} +``` + +## ErrorHandler + +全局错误处理器服务。 + +```typescript +import { ErrorHandler, Injectable } from '@repo/core' + +@Injectable({ providedIn: 'root' }) +class MyErrorHandler extends ErrorHandler { + handle(error: unknown) { + // 记录错误 + console.error('Error occurred:', error) + + // 发送到监控服务 + this.reportToMonitoring(error) + + // 调用父类处理 + super.handle(error) + } + + private reportToMonitoring(error: unknown) { + // Sentry, DataDog 等 + } +} + +// 注册自定义处理器 +const injector = EnvironmentInjector.createRootInjector([ + { provide: ErrorHandler, useClass: MyErrorHandler } +]) +``` + +## 完整示例 + +```typescript +import { + Controller, Get, Post, Body, Param, + Injectable, + NotFoundError, ValidationError, ForbiddenError, + errorFactory, ErrorSerializer +} from '@repo/core' + +@Controller('/api/orders') +class OrderController { + constructor( + private orderService: OrderService, + private userService: UserService + ) {} + + @Get('/:id') + async getOrder(@Param('id') id: string) { + const order = await this.orderService.findById(id) + if (!order) { + throw errorFactory.notFound('Order', id) + } + return order + } + + @Post('/create') + async createOrder(@Body() data: CreateOrderDto) { + // 验证 + if (!data.items?.length) { + throw new ValidationError('Order must have at least one item', { + items: 'At least one item required' + }) + } + + // 业务规则检查 + const user = await this.userService.findById(data.userId) + if (!user) { + throw errorFactory.notFound('User', data.userId) + } + + if (user.status === 'suspended') { + throw new ForbiddenError('Suspended users cannot create orders') + } + + // 检查库存 + for (const item of data.items) { + const available = await this.checkInventory(item.productId) + if (available < item.quantity) { + throw errorFactory.businessRule( + `Insufficient inventory for product ${item.productId}`, + 'INVENTORY_CHECK' + ) + } + } + + return this.orderService.create(data) + } +} + +// 全局错误处理中间件 +async function errorMiddleware(ctx: Context, next: Next) { + try { + await next() + } catch (error) { + const serialized = ErrorSerializer.serialize( + error, + process.env.NODE_ENV === 'development' + ) + + ctx.status = serialized.statusCode || 500 + ctx.body = serialized + } +} +``` + +## 最佳实践 + +1. **使用具体错误类** - 不要只用通用 Error,使用语义化错误类 +2. **提供上下文** - 错误消息应包含足够的调试信息 +3. **使用 errorFactory** - 保持错误创建的一致性 +4. **序列化响应** - 使用 ErrorSerializer 确保 API 响应格式统一 +5. **区分环境** - 生产环境不暴露堆栈信息 +6. **错误链** - 使用 originalError 参数保留原始错误上下文 +7. **自定义处理器** - 集成监控和日志系统 + diff --git a/.claude/skills/repo-core-di/references/providers.md b/.claude/skills/repo-core-di/references/providers.md new file mode 100644 index 0000000..f8ef5ed --- /dev/null +++ b/.claude/skills/repo-core-di/references/providers.md @@ -0,0 +1,291 @@ +# @repo/core 提供者类型详解 + +## 提供者类型概览 + +| 类型 | 用途 | 示例 | +|------|------|------| +| ValueProvider | 提供静态值 | 配置、常量 | +| ClassProvider | 提供类实例 | 服务类 | +| FactoryProvider | 通过工厂函数创建 | 需要动态配置的服务 | +| ExistingProvider | 别名映射 | 接口到实现的映射 | +| ConstructorProvider | 类本身作为令牌 | 简化注册 | +| LazyClassProvider | 延迟实例化类 | 重型服务 | +| LazyFactoryProvider | 延迟调用工厂 | 按需加载配置 | + +## ValueProvider + +直接提供值,不进行实例化。 + +```typescript +interface ValueProvider { + provide: InjectionTokenType + useValue: T + multi?: boolean +} + +// 示例 - 始终使用 InjectionToken 确保类型安全 +const API_URL = new InjectionToken('API_URL') +const CONFIG_TOKEN = new InjectionToken('CONFIG_TOKEN') +const VERSION = new InjectionToken('VERSION') + +{ provide: API_URL, useValue: 'https://api.example.com' } +{ provide: CONFIG_TOKEN, useValue: { debug: true, timeout: 5000 } } +{ provide: VERSION, useValue: '1.0.0' } +``` + +**适用场景**: +- 配置值 +- 常量 +- 已创建的实例 +- 简单对象 + +## ClassProvider + +通过构造函数创建实例。 + +```typescript +interface ClassProvider { + provide: InjectionTokenType + useClass: Type + multi?: boolean +} + +// 示例 +{ provide: UserService, useClass: UserService } +{ provide: Logger, useClass: ConsoleLogger } +{ provide: IAuthService, useClass: JwtAuthService } +``` + +**适用场景**: +- 服务类 +- 接口到实现的映射 +- 替换默认实现 + +## FactoryProvider + +通过工厂函数创建实例。 + +```typescript +interface FactoryProvider { + provide: InjectionTokenType + useFactory: (...deps: any[]) => T + deps?: InjectionTokenType[] + multi?: boolean +} + +// 示例 +{ + provide: DatabaseConnection, + useFactory: (config: ConfigService) => { + return new DatabaseConnection({ + host: config.dbHost, + port: config.dbPort, + database: config.dbName + }) + }, + deps: [ConfigService] +} + +// 异步工厂 +{ + provide: RemoteConfig, + useFactory: async (http: HttpClient) => { + const config = await http.get('/config') + return new RemoteConfig(config) + }, + deps: [HttpClient] +} +``` + +**适用场景**: +- 需要复杂初始化逻辑 +- 依赖运行时配置 +- 条件创建实例 +- 异步初始化 + +## ExistingProvider + +创建到另一个令牌的别名。 + +```typescript +interface ExistingProvider { + provide: InjectionTokenType + useExisting: InjectionTokenType + multi?: boolean +} + +// 示例 - 使用 InjectionToken 确保类型安全 +const LOGGER_TOKEN = new InjectionToken('Logger') + +{ provide: LOGGER_TOKEN, useExisting: LoggerService } +{ provide: AbstractLogger, useExisting: ConsoleLogger } +``` + +**适用场景**: +- 为同一服务提供多个令牌 +- 向后兼容 +- 接口别名 + +## ConstructorProvider + +类本身作为令牌和实现。 + +```typescript +interface ConstructorProvider { + provide: Type + multi?: boolean +} + +// 示例 +{ provide: UserService } +// 等价于 +{ provide: UserService, useClass: UserService } +``` + +**适用场景**: +- 简化注册 +- 类名即令牌 + +## LazyClassProvider + +延迟实例化,首次访问时才创建。 + +```typescript +interface LazyClassProvider { + provide: InjectionTokenType + useLazyClass: Type + multi?: boolean +} + +// 示例 +{ provide: HeavyAnalyticsService, useLazyClass: HeavyAnalyticsService } +{ provide: ReportGenerator, useLazyClass: ReportGenerator } +``` + +**适用场景**: +- 重型服务(大量初始化开销) +- 可能不会使用的服务 +- 优化启动时间 + +## LazyFactoryProvider + +延迟调用工厂函数。 + +```typescript +interface LazyFactoryProvider { + provide: InjectionTokenType + useLazyFactory: (...deps: any[]) => T + deps?: InjectionTokenType[] + multi?: boolean +} + +// 示例 - 使用 InjectionToken 确保类型安全 +const LAZY_CONFIG = new InjectionToken('LAZY_CONFIG') + +{ + provide: LAZY_CONFIG, + useLazyFactory: async () => { + const response = await fetch('/api/config') + return response.json() + }, + deps: [] +} +``` + +**适用场景**: +- 按需加载配置 +- 延迟网络请求 +- 条件初始化 + +## Multi Provider + +允许多个提供者注册到同一令牌。 + +```typescript +// 注册多个拦截器 +{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } +{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true } +{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true } + +// 获取时返回数组 +const interceptors = injector.get(HTTP_INTERCEPTORS) +// [AuthInterceptor实例, LoggingInterceptor实例, ErrorInterceptor实例] +``` + +**适用场景**: +- 插件系统 +- 中间件/拦截器 +- 事件处理器 +- 验证器集合 + +## 提供者选择决策树 + +``` +需要提供什么? +├── 静态值/配置 → ValueProvider +├── 类实例 +│ ├── 简单实例化 → ClassProvider 或 ConstructorProvider +│ ├── 需要复杂初始化 → FactoryProvider +│ ├── 延迟加载 → LazyClassProvider +│ └── 别名/映射 → ExistingProvider +├── 动态值 +│ ├── 同步创建 → FactoryProvider +│ └── 延迟创建 → LazyFactoryProvider +└── 多个实现 → 任意类型 + multi: true +``` + +## 常见模式 + +### 环境配置 + +```typescript +import { InjectionToken } from '@repo/core' + +// 定义类型安全的令牌 +const ENV = new InjectionToken('ENV') +const API_URL = new InjectionToken('API_URL') + +const providers = [ + { provide: ENV, useValue: process.env.NODE_ENV }, + { + provide: API_URL, + useFactory: (env: string) => { + return env === 'production' + ? 'https://api.prod.com' + : 'https://api.dev.com' + }, + deps: [ENV] + } +] + +// 获取时类型安全 +const apiUrl: string = injector.get(API_URL) +``` + +### 条件提供者 + +```typescript +const providers = [ + process.env.USE_MOCK + ? { provide: UserService, useClass: MockUserService } + : { provide: UserService, useClass: RealUserService } +] +``` + +### 工厂链 + +```typescript +const providers = [ + { provide: ConfigService, useClass: ConfigService }, + { + provide: DatabaseService, + useFactory: (config: ConfigService) => new DatabaseService(config), + deps: [ConfigService] + }, + { + provide: UserRepository, + useFactory: (db: DatabaseService) => new UserRepository(db), + deps: [DatabaseService] + } +] +``` diff --git a/.claude/skills/repo-core-di/references/request-scoped-injector.md b/.claude/skills/repo-core-di/references/request-scoped-injector.md new file mode 100644 index 0000000..b399264 --- /dev/null +++ b/.claude/skills/repo-core-di/references/request-scoped-injector.md @@ -0,0 +1,364 @@ +# 请求级注入器 (Request-Scoped Injector) + +## 核心概念 + +在 HTTP 请求处理中,每个请求需要独立的依赖注入上下文,以便: +1. 隔离请求间的状态 +2. 注入请求特定的数据(当前用户、请求上下文等) +3. 请求结束后自动清理资源 + +## 架构模式 + +### 完整注入器层次结构 + +``` +Root Injector (scope: 'root', 全局单例) + │ + ├── 全局服务 (ConfigService, DatabaseService...) + │ + └── Platform Injector (scope: 'platform', 全局单例, 可选) + │ + └── Application Injector (scope: 'application', 可选) + │ + └── Feature/Request Injector (scope: 'feature', 请求级) + │ + ├── 请求上下文 (BETTER_AUTH_CONTEXT) + ├── 当前用户 (CURRENT_USER) + ├── Feature Providers + └── Controller 实例 +``` + +### 简化模式(常用) + +``` +Root Injector (应用级单例) + │ + ├── 全局服务 (ConfigService, DatabaseService...) + │ + └── Request Injector (请求级,生命周期 = 请求) + │ + ├── 请求上下文 (BETTER_AUTH_CONTEXT) + ├── 当前用户 (CURRENT_USER) + ├── Feature Providers + └── Controller 实例 +``` + +## 实现模式 + +### 1. 定义请求级令牌 + +```typescript +import { InjectionToken } from '@repo/core' + +// 请求上下文令牌 +const BETTER_AUTH_CONTEXT = new InjectionToken('BETTER_AUTH_CONTEXT') + +// 当前用户令牌 +const CURRENT_USER = new InjectionToken('CURRENT_USER') + +// 请求对象令牌 +const REQUEST = new InjectionToken('REQUEST') + +// 响应对象令牌 +const RESPONSE = new InjectionToken('RESPONSE') +``` + +### 2. 创建请求级注入器 + +```typescript +import { createInjector, root, FEATURE_PROVIDERS } from '@repo/core' + +function createEndpointHandler(ControllerClass, methodName) { + return async (ctx: RequestContext) => { + // 获取全局注册的 feature providers + const providers = root.get(FEATURE_PROVIDERS, []) + + // 创建请求级注入器 + const reqInjector = createInjector([ + // 注入请求上下文 + { provide: BETTER_AUTH_CONTEXT, useValue: ctx.context }, + + // 注入当前用户(延迟加载) + { + provide: CURRENT_USER, + useFactory: async () => { + const session = await getSessionFromCtx(ctx) + return session?.user + } + }, + + // 注入原始请求/响应 + { provide: REQUEST, useValue: ctx.request }, + { provide: RESPONSE, useValue: ctx.response }, + + // 展开 feature providers + ...providers.flat(), + ], root, 'feature') // parent = root, scope = 'feature' + + // 从请求级注入器获取 Controller 实例 + const instance = reqInjector.get(ControllerClass) + + // 执行方法 + const result = await instance[methodName](...args) + + return result + } +} +``` + +### 3. 在服务中使用请求级依赖 + +```typescript +import { Injectable, Inject } from '@repo/core' + +// 注意:providedIn: 'auto' 表示不自动注册到根注入器 +// 由请求级注入器在运行时创建实例 +@Injectable({ providedIn: 'auto' }) +class UserService { + constructor( + // CURRENT_USER 可能为 undefined,使用 optional: true 避免注入失败 + @Inject(CURRENT_USER, { optional: true }) private currentUser: User | undefined, + @Inject(BETTER_AUTH_CONTEXT) private authContext: AuthContext + ) {} + + async getCurrentUserProfile() { + if (!this.currentUser) { + throw new UnauthorizedError('Not logged in') + } + return this.userRepository.findById(this.currentUser.id) + } +} +``` + +> **重要**: +> - `providedIn: 'auto'` - 推荐用于请求级服务,可在任何注入器中被自动解析 +> - `providedIn: null` - 不会被自动解析,必须手动注册 +> - `providedIn: 'feature'` - 只能在 `scope: 'feature'` 的注入器中被解析 + +### 4. Controller 使用请求级服务 + +```typescript +@Injectable({ providedIn: 'auto' }) +@Controller('/api/users') +class UserController { + constructor( + private userService: UserService, // 请求级实例 + @Inject(CURRENT_USER, { optional: true }) private currentUser: User | undefined + ) {} + + @Get('/me') + @RequireSession() + async getProfile() { + return this.userService.getCurrentUserProfile() + } +} +``` + +## FEATURE_PROVIDERS 机制 + +用于在应用启动时注册需要在每个请求中可用的提供者。 + +```typescript +import { FEATURE_PROVIDERS, root } from '@repo/core' + +// 注册 feature providers +root.set([ + { + provide: FEATURE_PROVIDERS, + useValue: [ + { provide: UserRepository, useClass: UserRepository }, + { provide: OrderRepository, useClass: OrderRepository }, + ], + multi: true + } +]) + +// 在请求处理时,这些 providers 会被注入到请求级注入器 +``` + +## 生命周期对比 + +| 作用域 | 生命周期 | 示例 | +|--------|----------|------| +| root | 应用启动 → 应用关闭 | ConfigService, DatabasePool | +| platform | 应用启动 → 应用关闭 | 跨应用共享服务 | +| application | 应用启动 → 应用关闭 | 应用级服务 | +| feature (请求级) | 请求开始 → 请求结束 | Controller, 请求上下文 | + +### 注入器生命周期钩子 + +```typescript +// OnDestroy - 注入器销毁时调用 +@Injectable({ providedIn: 'auto' }) +class ResourceService { + private connection: Connection + + async onDestroy() { + // 清理资源 + await this.connection.close() + } +} + +// 销毁注入器会自动调用所有实例的 onDestroy +await reqInjector.destroy() +``` + +> **注意**: +> - 销毁后的注入器不能再获取服务,会抛出"注入器已销毁"错误 +> - 重复销毁注入器是安全的(幂等操作) + +## 最佳实践 + +### 1. 明确服务作用域 + +```typescript +// 全局单例 - 无状态或共享状态 +@Injectable({ providedIn: 'root' }) +class ConfigService { } + +// 请求级 - 依赖请求上下文,使用 'auto' 不自动注册 +@Injectable({ providedIn: 'auto' }) +class RequestScopedService { + constructor(@Inject(CURRENT_USER, { optional: true }) private user: User | undefined) { } +} +``` + +### 2. 避免在单例中注入请求级依赖 + +```typescript +// ⚠️ 问题:单例服务注入请求级依赖 +// 如果使用 optional: true,不会报错,但 user 始终为 null +@Injectable({ providedIn: 'root' }) +class ProblematicService { + constructor(@Inject(CURRENT_USER, { optional: true }) private user: User | null) { } + // user 永远是 null,因为根注入器没有 CURRENT_USER +} + +// ❌ 错误:单例服务注入必需的请求级依赖会抛出错误 +@Injectable({ providedIn: 'root' }) +class BadService { + constructor(@Inject(CURRENT_USER) private user: User) { } // 抛出错误! +} + +// ✅ 正确:使用 'auto' 作用域,让服务在请求级注入器中实例化 +@Injectable({ providedIn: 'auto' }) +class GoodService { + constructor(@Inject(CURRENT_USER, { optional: true }) private user: User | undefined) { } +} +``` + +> **行为说明**: +> - `providedIn: 'root'` + `optional: true` 的请求级依赖 → 依赖值为 `null`,不报错 +> - `providedIn: 'root'` + 必需的请求级依赖 → 抛出错误 +> - `providedIn: 'auto'` → 在请求级注入器中实例化,可正确获取请求级依赖 + +### 3. 使用工厂延迟加载 + +```typescript +// 延迟加载当前用户,避免不必要的数据库查询 +{ + provide: CURRENT_USER, + useFactory: async () => { + const session = await getSessionFromCtx(ctx) + return session?.user + } +} +``` + +### 4. 请求隔离保证 + +每个请求创建独立的注入器实例,确保: +- 请求 A 的用户数据不会泄露到请求 B +- 并发请求互不干扰 +- 请求结束后资源自动释放 + +```typescript +// 请求 1 +const reqInjector1 = createInjector([...], root, 'feature') +const user1 = reqInjector1.get(CURRENT_USER) // User A + +// 请求 2 (并发) +const reqInjector2 = createInjector([...], root, 'feature') +const user2 = reqInjector2.get(CURRENT_USER) // User B + +// user1 !== user2,完全隔离 +``` + +## 完整示例 + +```typescript +import { + Injectable, Inject, Controller, Get, Post, Body, + InjectionToken, createInjector, root, FEATURE_PROVIDERS, + RequireSession +} from '@repo/core' + +// 1. 定义令牌 +const AUTH_CONTEXT = new InjectionToken('AUTH_CONTEXT') +const CURRENT_USER = new InjectionToken('CURRENT_USER') + +// 2. 请求级服务 +@Injectable({ providedIn: 'auto' }) +class OrderService { + constructor( + @Inject(CURRENT_USER, { optional: true }) private currentUser: User | undefined, + private orderRepository: OrderRepository + ) {} + + async getMyOrders() { + if (!this.currentUser) throw new UnauthorizedError() + return this.orderRepository.findByUserId(this.currentUser.id) + } + + async createOrder(data: CreateOrderDto) { + if (!this.currentUser) throw new UnauthorizedError() + return this.orderRepository.create({ + ...data, + userId: this.currentUser.id + }) + } +} + +// 3. Controller +@Injectable({ providedIn: 'auto' }) +@Controller('/api/orders') +class OrderController { + constructor(private orderService: OrderService) {} + + @Get('/my') + @RequireSession() + async getMyOrders() { + return this.orderService.getMyOrders() + } + + @Post('/create') + @RequireSession() + async createOrder(@Body() data: CreateOrderDto) { + return this.orderService.createOrder(data) + } +} + +// 4. 请求处理器 +function handleRequest(ControllerClass, methodName) { + return async (ctx) => { + const providers = root.get(FEATURE_PROVIDERS, []) + + const reqInjector = createInjector([ + { provide: AUTH_CONTEXT, useValue: ctx.context }, + { + provide: CURRENT_USER, + useFactory: async () => { + const session = await getSessionFromCtx(ctx) + return session?.user + } + }, + ...providers.flat(), + ], root, 'feature') + + const controller = reqInjector.get(ControllerClass) + const args = await injectParameters(ctx) + + return controller[methodName](...args) + } +} +``` diff --git a/.claude/skills/repo-sdk/SKILL.md b/.claude/skills/repo-sdk/SKILL.md new file mode 100644 index 0000000..1b9b943 --- /dev/null +++ b/.claude/skills/repo-sdk/SKILL.md @@ -0,0 +1,280 @@ +--- +name: repo-sdk +description: "@repo/sdk 使用指南。当需要调用 Loomart 平台 API、实现项目管理、模板运行、AI 生成、文件处理、社交互动等功能时使用此技能。适用于:(1) 服务端实现 Controller 接口 (2) 客户端调用 API (3) 理解接口组合使用场景 (4) 数据验证和类型定义" +--- + +# @repo/sdk 使用指南 + +## 架构概述 + +SDK 采用**三层分离架构**: + +``` +@repo/sdk +├── controllers/ # 26 个业务控制器(API 契约定义) +├── schemas/ # Zod 数据验证 Schema +├── types/ # TypeScript 类型定义 +└── client/ # 客户端集成(Better Auth 插件 + API 生成器) +``` + +**核心依赖**: +- `@repo/core`:装饰器系统(@Controller, @Post, @Get, @Body, @Query 等) +- `zod`:数据验证 +- `reflect-metadata`:元数据反射 + +## 推荐调用方式:root.get (DI 风格) + +**强烈推荐使用 `root.get` 方式**,保证完整的类型安全: + +```typescript +import { root } from '@repo/core' +import { ProjectController, TemplateController, AigcController } from '@repo/sdk' +import type { CreateProjectInput, Project } from '@repo/sdk' + +// 获取控制器(完整类型推导) +const projectCtrl = root.get(ProjectController) +const templateCtrl = root.get(TemplateController) +const aigcCtrl = root.get(AigcController) + +// 调用方法(参数和返回值都有类型提示) +const projects = await projectCtrl.list({ page: 1, limit: 10 }) +const project = await projectCtrl.create({ title: '新项目', content: {} }) +``` + +### 其他调用方式 + +
+Better Auth 插件风格 + +```typescript +import { createAuthClient } from 'better-auth/client' +import { createSkerClientPlugin } from '@repo/sdk' + +const auth = createAuthClient({ + baseURL: 'http://localhost:3000', + plugins: [createSkerClientPlugin()] +}) + +const projects = await auth.sker.project.list({ page: 1, limit: 10 }) +``` +
+ +
+API 函数风格 + +```typescript +import { generateApiFunctions, client } from '@repo/sdk' + +client.setConfig({ + baseUrl: 'http://localhost:3000', + auth: async () => localStorage.getItem('token') +}) + +const apis = generateApiFunctions() +const projects = await apis.postLoomartProjectList({ page: 1, limit: 10 }) +``` +
+ +## 功能模块导航 + +| 模块 | 控制器 | 主要功能 | +|------|--------|----------| +| 项目管理 | ProjectController | 创建、列表、更新、删除、转移项目 | +| 模板管理 | TemplateController | 创建、列表、运行模板工作流 | +| 模板生成 | TemplateGenerationController | 生成记录管理 | +| 社交互动 | TemplateSocialController | 点赞、收藏、评论、分享 | +| AI 生成 | AigcController | 图像/视频生成任务 | +| 聊天 | ChatController | AI 对话 | +| 文件处理 | FileController | 上传、压缩、格式转换 | +| 分类标签 | CategoryController, TagController | 分类和标签管理 | +| 消息系统 | MessageController | 用户消息通知 | +| 公告 | AnnouncementController | 系统公告 | +| 权限 | PermissionController, RoleController | 权限和角色管理 | +| 支付 | AlipayController, AdminBalanceController | 支付和余额管理 | + +## 核心组合使用场景 + +### 场景 1:模板运行完整流程 + +```typescript +// 1. 获取模板详情 +const template = await templateCtrl.get(templateId) + +// 2. 运行模板(返回 generationId) +const { generationId } = await templateCtrl.run({ + templateId, + formData: { prompt: '生成一只猫' } +}) + +// 3. 轮询任务状态 +let status = await aigcCtrl.getTaskStatus(taskId) +while (status.status === 'PENDING' || status.status === 'PROCESSING') { + await sleep(2000) + status = await aigcCtrl.getTaskStatus(taskId) +} + +// 4. 处理生成结果(可选:转换格式) +if (status.result_url) { + const webp = await fileCtrl.convertToWebp({ + videoUrl: status.result_url, + quality: 80, + loop: true + }) +} +``` + +### 场景 2:模板社交互动 + +```typescript +// 1. 获取模板列表(带社交状态) +const { templates } = await templateCtrl.list({ page: 1, limit: 20 }) + +// 2. 批量检查点赞/收藏状态 +const likedStatus = await socialCtrl.checkLikedBatch({ + templateIds: templates.map(t => t.id) +}) + +// 3. 点赞操作 +await socialCtrl.like({ templateId }) + +// 4. 获取评论 +const comments = await socialCtrl.getComments({ templateId, page: 1 }) + +// 5. 发表评论 +await socialCtrl.createComment({ + templateId, + content: '很棒的模板!' +}) +``` + +### 场景 3:项目管理与标签 + +```typescript +// 1. 创建项目 +const project = await projectCtrl.create({ + title: '我的项目', + content: { /* 项目数据 */ } +}) + +// 2. 创建用户标签 +const tag = await projectTagCtrl.create({ name: '收藏夹' }) + +// 3. 给项目添加标签 +await projectTagCtrl.addToProject({ + projectId: project.id, + tagId: tag.id +}) + +// 4. 按标签筛选项目 +const { projects } = await projectCtrl.list({ + page: 1, + tagId: tag.id +}) +``` + +## 参考文档 + +- **控制器 API 详情**:查阅 [references/controllers.md](references/controllers.md) +- **组合使用场景**:查阅 [references/workflows.md](references/workflows.md) +- **客户端集成指南**:查阅 [references/client-usage.md](references/client-usage.md) + +## 权限装饰器 + +```typescript +// 需要登录 +@RequireSession() + +// 需要特定权限(AND 模式) +@RequirePermissions({ project: ['create', 'update'] }) + +// 需要任一权限(OR 模式) +@RequirePermissions({ project: ['run'], template: ['run'] }, 'OR') +``` + +## 复用 Zod Schema 和类型(推荐) + +**强烈推荐复用 SDK 导出的 Schema 和类型**,确保前后端数据一致性: + +### 导入类型 + +```typescript +// 从 @repo/sdk 导入类型(推荐) +import type { + // 项目 + Project, CreateProjectInput, ListProjectsInput, ListProjectsResult, + // 模板 + TemplateDetail, RunTemplateInput, ListTemplatesInput, + // AI 生成 + AigcModel, SubmitTaskBody, GetTaskStatusResult, + // 消息 + Message, ListMessagesInput, UnreadCountResult +} from '@repo/sdk' +``` + +### 复用 Zod Schema 验证 + +```typescript +import { + CreateProjectSchema, + ListProjectsSchema, + RunTemplateSchema, + SubmitTaskBodySchema +} from '@repo/sdk' + +// 验证用户输入 +function validateInput(data: unknown) { + return CreateProjectSchema.parse(data) // 抛出错误或返回类型安全的数据 +} + +// 安全验证(不抛错) +function safeValidate(data: unknown) { + const result = CreateProjectSchema.safeParse(data) + if (!result.success) { + console.error('验证失败:', result.error.flatten()) + return null + } + return result.data +} +``` + +### 服务端实现复用类型 + +```typescript +import { Injectable } from '@repo/core' +import { ProjectController } from '@repo/sdk' +import type { Project, CreateProjectInput, ListProjectsInput, ListProjectsResult } from '@repo/sdk' + +@Injectable() +export class ProjectControllerImpl implements ProjectController { + // 参数和返回值类型直接复用 SDK 导出的类型 + async create(data: CreateProjectInput): Promise { + // 实现... + } + + async list(body: ListProjectsInput): Promise { + // 实现... + } +} +``` + +### 前端表单复用 Schema + +```typescript +import { CreateProjectSchema } from '@repo/sdk' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' + +type FormData = z.infer + +function CreateProjectForm() { + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(CreateProjectSchema) + }) + + const onSubmit = async (data: FormData) => { + const projectCtrl = root.get(ProjectController) + await projectCtrl.create(data) // 类型完全匹配 + } +} +``` diff --git a/.claude/skills/repo-sdk/references/client-usage.md b/.claude/skills/repo-sdk/references/client-usage.md new file mode 100644 index 0000000..0d5fa7e --- /dev/null +++ b/.claude/skills/repo-sdk/references/client-usage.md @@ -0,0 +1,619 @@ +# 客户端使用指南 + +本文档详细介绍 @repo/sdk 的客户端调用方式及其配置方法。 + +## 目录 + +1. [推荐方式:root.get (DI 风格)](#1-推荐方式rootget-di-风格) +2. [复用 Schema 和类型](#2-复用-schema-和类型) +3. [Better Auth 插件风格](#3-better-auth-插件风格) +4. [API 函数风格](#4-api-函数风格) +5. [服务端实现](#5-服务端实现) +6. [错误处理](#6-错误处理) + +--- + +## 1. 推荐方式:root.get (DI 风格) + +**强烈推荐使用此方式**,保证完整的类型安全和最佳开发体验。 + +### 基本用法 + +```typescript +import { root } from '@repo/core' +import { + ProjectController, + TemplateController, + TemplateSocialController, + AigcController, + FileController +} from '@repo/sdk' +import type { CreateProjectInput, Project, ListProjectsResult } from '@repo/sdk' + +// 获取控制器实例(完整类型推导) +const projectCtrl = root.get(ProjectController) +const templateCtrl = root.get(TemplateController) +const socialCtrl = root.get(TemplateSocialController) +const aigcCtrl = root.get(AigcController) +const fileCtrl = root.get(FileController) + +// 调用方法(参数和返回值都有完整类型提示) +const projects: ListProjectsResult = await projectCtrl.list({ page: 1, limit: 10 }) +const project: Project = await projectCtrl.create({ title: '新项目', content: {} }) +const template = await templateCtrl.get('template-id') +await socialCtrl.like({ templateId: 'xxx' }) +``` + +### 优势 + +1. **完整类型安全**:参数和返回值都有精确的类型推导 +2. **IDE 支持**:自动补全、跳转定义、重构支持 +3. **可测试性**:可以轻松 mock Controller 进行单元测试 +4. **前后端一致**:服务端和客户端使用相同的接口定义 +5. **灵活性**:可以自定义代理实现(如添加缓存、日志等) + +### 前置条件 + +需要先初始化客户端插件(注册 HTTP 代理): + +```typescript +import { createAuthClient } from 'better-auth/client' +import { createSkerClientPlugin } from '@repo/sdk' + +// 初始化(会自动将 Controller 替换为 HTTP 代理) +const auth = createAuthClient({ + baseURL: 'http://localhost:3000', + plugins: [createSkerClientPlugin()] +}) +``` + +### 自定义代理(高级) + +```typescript +import { root } from '@repo/core' +import { ProjectController } from '@repo/sdk' +import type { ListProjectsInput, ListProjectsResult } from '@repo/sdk' + +// 注册带缓存的自定义实现 +root.set([{ + provide: ProjectController, + useFactory: () => ({ + list: async (input: ListProjectsInput): Promise => { + const cacheKey = `projects-${JSON.stringify(input)}` + const cached = cache.get(cacheKey) + if (cached) return cached + + const result = await originalProxy.list(input) + cache.set(cacheKey, result) + return result + }, + // ... 其他方法 + }) +}]) +``` + +--- + +## 2. 复用 Schema 和类型 + +**强烈推荐复用 SDK 导出的 Zod Schema 和 TypeScript 类型**,确保前后端数据一致性。 + +### 导入类型(推荐) + +```typescript +// 始终从 @repo/sdk 导入类型,不要自己定义 +import type { + // 项目相关 + Project, + CreateProjectInput, + ListProjectsInput, + ListProjectsResult, + UpdateProjectInput, + TransferProjectInput, + + // 模板相关 + TemplateDetail, + CreateTemplateInput, + RunTemplateInput, + ListTemplatesInput, + ListTemplatesResult, + + // AI 生成 + AigcModel, + SubmitTaskBody, + SubmitTaskResult, + GetTaskStatusResult, + + // 社交 + LikeTemplateInput, + CreateCommentInput, + TemplateComment, + GetCommentsResponse, + + // 消息 + Message, + ListMessagesInput, + UnreadCountResult, + + // 文件 + FileUploadResponse, + VideoCompressRequest, + ConvertToWebpRequest +} from '@repo/sdk' +``` + +### 复用 Zod Schema 验证 + +```typescript +import { + CreateProjectSchema, + ListProjectsSchema, + RunTemplateSchema, + SubmitTaskBodySchema, + CreateCommentSchema +} from '@repo/sdk' + +// 验证用户输入(抛出错误) +function validateInput(data: unknown) { + return CreateProjectSchema.parse(data) +} + +// 安全验证(不抛错,返回结果对象) +function safeValidate(data: unknown) { + const result = CreateProjectSchema.safeParse(data) + if (!result.success) { + console.error('验证失败:', result.error.flatten()) + return null + } + return result.data // 类型安全的数据 +} + +// 部分验证(用于更新操作) +const PartialProjectSchema = CreateProjectSchema.partial() +function validatePartialUpdate(data: unknown) { + return PartialProjectSchema.parse(data) +} +``` + +### 前端表单集成 + +```typescript +import { CreateProjectSchema } from '@repo/sdk' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' +import { root } from '@repo/core' +import { ProjectController } from '@repo/sdk' + +// 从 Schema 推导表单类型 +type FormData = z.infer + +function CreateProjectForm() { + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(CreateProjectSchema) // 复用 SDK Schema + }) + + const onSubmit = async (data: FormData) => { + const projectCtrl = root.get(ProjectController) + const project = await projectCtrl.create(data) // 类型完全匹配 + console.log('创建成功:', project.id) + } + + return ( +
+ + {errors.title && {errors.title.message}} + {/* ... */} +
+ ) +} +``` + +### 服务端实现复用类型 + +```typescript +import { Injectable } from '@repo/core' +import { ProjectController } from '@repo/sdk' +// 直接复用 SDK 导出的类型 +import type { + Project, + CreateProjectInput, + ListProjectsInput, + ListProjectsResult, + UpdateProjectInput +} from '@repo/sdk' + +@Injectable() +export class ProjectControllerImpl implements ProjectController { + constructor( + private readonly db: DatabaseService, + private readonly auth: AuthService + ) {} + + // 参数和返回值类型直接复用 SDK 类型 + async create(data: CreateProjectInput): Promise { + const userId = this.auth.getCurrentUserId() + return this.db.project.create({ + data: { ...data, userId } + }) + } + + async list(body: ListProjectsInput): Promise { + // 实现... + } + + async update(body: UpdateProjectInput): Promise { + // 实现... + } +} +``` + +--- + +## 3. Better Auth 插件风格 + +**适用场景**:已使用 Better Auth 进行身份认证的项目 + +### 安装和配置 + +```typescript +import { createAuthClient } from 'better-auth/client' +import { createSkerClientPlugin } from '@repo/sdk' + +// 创建带 Sker 插件的 Auth 客户端 +const auth = createAuthClient({ + baseURL: 'http://localhost:3000', + plugins: [createSkerClientPlugin()] +}) + +export { auth } +``` + +### 调用方式 + +```typescript +// 项目管理 +const projects = await auth.sker.project.list({ page: 1, limit: 10 }) +const project = await auth.sker.project.create({ title: '新项目', content: {} }) +const detail = await auth.sker.project.get('project-id') + +// 模板管理 +const templates = await auth.sker.template.list({ page: 1 }) +const { generationId } = await auth.sker.template.run({ templateId: 'xxx', formData: {} }) + +// 社交功能 +await auth.sker.templateSocial.like({ templateId: 'xxx' }) +const comments = await auth.sker.templateSocial.getComments({ templateId: 'xxx' }) + +// AI 生成 +const models = await auth.sker.aigc.getModels('image') +const { task_id } = await auth.sker.aigc.submitTask({ model_name: 'xxx', prompt: 'xxx' }) + +// 文件上传 +const formData = new FormData() +formData.append('file', file) +const { data: fileUrl } = await auth.sker.file.uploadS3(formData) +``` + +### 插件工作原理 + +`createSkerClientPlugin()` 做了以下事情: + +1. **注册 HTTP 代理**:将所有 Controller 替换为 HTTP 调用代理 +2. **提供 Better Auth 风格 API**:通过 `auth.sker.xxx` 访问 +3. **自动处理认证**:使用 Better Auth 的 session 进行认证 +4. **生成路径映射**:自动从 Controller 元数据生成路由 + +```typescript +// 插件内部实现简化版 +export function createSkerClientPlugin(): BetterAuthClientPlugin { + return { + id: 'sker', + getActions: ($fetch) => { + // 注册代理到 DI 容器 + registerControllerProxies($fetch) + // 返回 Better Auth 风格的 actions + return buildBetterAuthActions(controllers, $fetch) + }, + pathMethods: generatePathMethods(controllers) + } +} +``` + +--- + +## 4. API 函数风格 + +**推荐场景**:不使用 Better Auth、需要独立的 API 客户端 + +### 配置客户端 + +```typescript +import { client, generateApiFunctions } from '@repo/sdk' + +// 配置基础 URL 和认证 +client.setConfig({ + baseUrl: 'http://localhost:3000', + auth: async () => { + // 返回认证 token + return localStorage.getItem('token') || '' + } +}) + +// 生成所有 API 函数 +const apis = generateApiFunctions() +``` + +### 调用方式 + +函数名格式:`{httpMethod}{Path}`(驼峰命名) + +```typescript +// POST /loomart/project/list -> postLoomartProjectList +const projects = await apis.postLoomartProjectList({ page: 1, limit: 10 }) + +// GET /loomart/template/get -> getLoomartTemplateGet +const template = await apis.getLoomartTemplateGet({ id: 'template-id' }) + +// POST /loomart/template/run -> postLoomartTemplateRun +const { generationId } = await apis.postLoomartTemplateRun({ + templateId: 'xxx', + formData: {} +}) + +// GET /loomart/aigc/models -> getLoomartAigcModels +const models = await apis.getLoomartAigcModels({ category: 'image' }) + +// POST /loomart/file/upload-s3 -> postLoomartFileUploadS3 +const formData = new FormData() +formData.append('file', file) +const result = await apis.postLoomartFileUploadS3(formData) +``` + +### 函数名映射规则 + +| 路由 | HTTP 方法 | 函数名 | +|------|-----------|--------| +| /loomart/project/create | POST | postLoomartProjectCreate | +| /loomart/project/list | POST | postLoomartProjectList | +| /loomart/project/get | GET | getLoomartProjectGet | +| /loomart/template/run | POST | postLoomartTemplateRun | +| /loomart/aigc/task/submit | POST | postLoomartAigcTaskSubmit | +| /loomart/aigc/task/status | GET | getLoomartAigcTaskStatus | +| /loomart/file/upload-s3 | POST | postLoomartFileUploadS3 | + +### 动态认证 + +```typescript +client.setConfig({ + baseUrl: 'http://localhost:3000', + auth: async (authContext) => { + // 可以根据上下文动态获取 token + if (authContext?.refreshToken) { + // 刷新 token + const newToken = await refreshAccessToken(authContext.refreshToken) + return newToken + } + return localStorage.getItem('token') || '' + } +}) +``` + +--- + +## 5. 服务端实现 + +**场景**:在 Hono/Express 等后端框架中实现 Controller + +### 注册控制器实现 + +```typescript +import { root } from '@repo/core' +import { ProjectController } from '@repo/sdk' +import { ProjectControllerImpl } from './impl/project.controller.impl' + +// 注册实现类 +root.set([ + { + provide: ProjectController, + useClass: ProjectControllerImpl + } +]) +``` + +### 实现类示例 + +```typescript +import { Injectable } from '@repo/core' +import { ProjectController } from '@repo/sdk' +import type { + Project, + CreateProjectInput, + ListProjectsInput, + ListProjectsResult +} from '@repo/sdk' + +@Injectable() +export class ProjectControllerImpl implements ProjectController { + constructor( + private readonly db: DatabaseService, + private readonly auth: AuthService + ) {} + + async create(data: CreateProjectInput): Promise { + const userId = this.auth.getCurrentUserId() + + const project = await this.db.project.create({ + data: { + ...data, + userId + } + }) + + return project + } + + async list(body: ListProjectsInput): Promise { + const userId = this.auth.getCurrentUserId() + + const [projects, total] = await Promise.all([ + this.db.project.findMany({ + where: { userId, isDeleted: false }, + skip: ((body.page || 1) - 1) * (body.limit || 20), + take: body.limit || 20, + orderBy: { [body.sortBy || 'createdAt']: body.sortOrder || 'desc' } + }), + this.db.project.count({ where: { userId, isDeleted: false } }) + ]) + + return { + projects, + total, + page: body.page || 1, + limit: body.limit || 20 + } + } + + async get(id: string): Promise { + const project = await this.db.project.findUnique({ where: { id } }) + if (!project) throw new Error('Project not found') + return project + } + + async update(body: UpdateProjectInput): Promise { + return this.db.project.update({ + where: { id: body.id }, + data: body + }) + } + + async delete(body: { id: string }): Promise<{ message: string }> { + await this.db.project.update({ + where: { id: body.id }, + data: { isDeleted: true } + }) + return { message: 'Project deleted' } + } + + async transfer(body: TransferProjectInput): Promise { + // 实现转移逻辑... + } +} +``` + +### 与 Hono 集成 + +```typescript +import { Hono } from 'hono' +import { root, CONTROLLES } from '@repo/core' + +const app = new Hono() + +// 自动注册所有控制器路由 +const controllers = root.get(CONTROLLES, []) +for (const controller of controllers) { + // 使用 @repo/core 的路由注册逻辑 + registerControllerRoutes(app, controller) +} + +export default app +``` + +--- + +## 6. 错误处理 + +### 客户端错误处理 + +```typescript +import { ProjectController } from '@repo/sdk' +import { root } from '@repo/core' + +const projectCtrl = root.get(ProjectController) + +try { + const project = await projectCtrl.get('non-existent-id') +} catch (error) { + if (error.message.includes('API error')) { + // API 返回的错误 + console.error('API 错误:', error.message) + } else { + // 网络或其他错误 + console.error('请求失败:', error) + } +} +``` + +### 统一错误处理 + +```typescript +// 创建带错误处理的包装器 +function withErrorHandling Promise>(fn: T): T { + return (async (...args: Parameters) => { + try { + return await fn(...args) + } catch (error: any) { + // 统一错误处理 + if (error.message.includes('401')) { + // 未认证,跳转登录 + window.location.href = '/login' + return + } + if (error.message.includes('403')) { + // 无权限 + throw new Error('您没有权限执行此操作') + } + if (error.message.includes('404')) { + throw new Error('资源不存在') + } + throw error + } + }) as T +} + +// 使用 +const safeList = withErrorHandling(projectCtrl.list.bind(projectCtrl)) +const projects = await safeList({ page: 1 }) +``` + +--- + +## 最佳实践总结 + +### 推荐做法 + +| 场景 | 推荐方式 | +|------|----------| +| 调用 API | `root.get(Controller)` - 完整类型安全 | +| 定义类型 | 从 `@repo/sdk` 导入,不要自己定义 | +| 验证数据 | 复用 SDK 导出的 Zod Schema | +| 表单验证 | 使用 `zodResolver(Schema)` | +| 服务端实现 | `implements Controller` + 复用类型 | + +### 避免做法 + +```typescript +// ❌ 不要自己定义类型 +interface MyProject { + id: string; + title: string; +} + +// ✅ 从 SDK 导入类型 +import type { Project } from '@repo/sdk' + +// ❌ 不要自己写验证逻辑 +if (!data.title || data.title.length < 1) { + throw new Error('标题不能为空') +} + +// ✅ 复用 SDK Schema +import { CreateProjectSchema } from '@repo/sdk' +CreateProjectSchema.parse(data) + +// ❌ 不要使用 any 类型 +const projectCtrl: any = root.get(ProjectController) + +// ✅ 让 TypeScript 自动推导 +const projectCtrl = root.get(ProjectController) // 类型自动推导 +``` diff --git a/.claude/skills/repo-sdk/references/controllers.md b/.claude/skills/repo-sdk/references/controllers.md new file mode 100644 index 0000000..f7ac0ce --- /dev/null +++ b/.claude/skills/repo-sdk/references/controllers.md @@ -0,0 +1,691 @@ +# 控制器 API 参考 + +## 目录 + +1. [项目管理](#项目管理) +2. [模板管理](#模板管理) +3. [模板生成记录](#模板生成记录) +4. [社交互动](#社交互动) +5. [AI 生成](#ai-生成) +6. [聊天](#聊天) +7. [文件处理](#文件处理) +8. [分类管理](#分类管理) +9. [标签管理](#标签管理) +10. [项目标签](#项目标签) +11. [消息系统](#消息系统) +12. [公告系统](#公告系统) +13. [权限管理](#权限管理) +14. [角色管理](#角色管理) +15. [支付相关](#支付相关) + +--- + +## 项目管理 + +**控制器**: `ProjectController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /project/create | 创建新项目 | project:create | +| POST | /project/list | 获取用户项目列表 | project:list | +| GET | /project/get | 获取单个项目 | project:read | +| POST | /project/update | 更新项目信息 | project:update | +| POST | /project/delete | 删除项目 | project:delete | +| POST | /project/transfer | 转移/复制项目所有权 | project:update OR project:create | + +### 类型定义 + +```typescript +// 创建项目 +interface CreateProjectInput { + title: string; + titleEn?: string; + description?: string; + descriptionEn?: string; + content: any; + sourceTemplateId?: string; +} + +// 列表查询 +interface ListProjectsInput { + page?: number; + limit?: number; + tagId?: string; + search?: string; + sortBy?: 'createdAt' | 'updatedAt' | 'title'; + sortOrder?: 'asc' | 'desc'; +} + +// 项目响应 +interface Project { + id: string; + userId: string; + title: string; + titleEn: string; + description?: string; + descriptionEn?: string; + resultUrl?: string; + content: any; + sourceTemplateId?: string; + isDeleted: boolean; + createdAt: Date; + updatedAt: Date; + tags?: UserTagBasic[]; +} + +// 转移项目 +interface TransferProjectInput { + projectId: string; + targetUserId: string; + mode: 'transfer' | 'copy'; +} +``` + +--- + +## 模板管理 + +**控制器**: `TemplateController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /template/create | 创建新模板 | - | +| POST | /template/list | 获取模板列表 | - | +| GET | /template/get | 获取单个模板 | - | +| POST | /template/update | 更新模板 | - | +| POST | /template/delete | 删除模板 | - | +| POST | /template/run | 运行模板工作流 | - | +| POST | /template/rerun | 根据生成记录重新运行 | - | + +### 类型定义 + +```typescript +// 模板详情 +interface TemplateDetail { + id: string; + userId: string; + title: string; + titleEn: string; + description: string; + descriptionEn: string; + coverImageUrl: string; + previewUrl: string; + watermarkedPreviewUrl?: string; + webpPreviewUrl?: string; + webpHighPreviewUrl?: string; + content: any; + formSchema?: any; + sortOrder: number; + viewCount: number; + useCount: number; + likeCount: number; + favoriteCount: number; + shareCount: number; + commentCount: number; + costPrice?: number; + price?: number; + aspectRatio: string; + status: string; + isDeleted: boolean; + createdAt: Date; + updatedAt: Date; + isLiked?: boolean; + isFavorited?: boolean; +} + +// 运行模板 +interface RunTemplateInput { + templateId: string; + formData?: Record; + projectId?: string; +} + +// 列表查询 +interface ListTemplatesInput { + page?: number; + limit?: number; + categoryId?: string; + tagId?: string; + status?: string; + search?: string; + sortBy?: 'sortOrder' | 'likeCount' | 'useCount' | 'viewCount' | 'createdAt'; + sortOrder?: 'asc' | 'desc'; +} +``` + +--- + +## 模板生成记录 + +**控制器**: `TemplateGenerationController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /template-generation/list | 获取生成记录列表 | template:run | +| GET | /template-generation/get | 获取单个记录 | template:run | +| POST | /template-generation/update | 更新记录 | template:run | +| POST | /template-generation/delete | 删除记录 | template:run | +| POST | /template-generation/batch-delete | 批量删除 | template:run | + +### 类型定义 + +```typescript +interface TemplateGeneration { + id: string; + userId: string; + templateId: string; + projectId?: string; + status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'; + formData?: any; + resultUrl?: string; + errorMessage?: string; + createdAt: Date; + updatedAt: Date; +} + +interface ListTemplateGenerationsInput { + page?: number; + limit?: number; + templateId?: string; + projectId?: string; + status?: string; +} +``` + +--- + +## 社交互动 + +**控制器**: `TemplateSocialController` +**路由前缀**: `/loomart/template` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /like | 点赞模板 | template:run | +| POST | /unlike | 取消点赞 | template:run | +| GET | /check-liked | 检查是否已点赞 | template:run | +| POST | /check-liked-batch | 批量检查点赞状态 | template:run | +| POST | /favorite | 收藏模板 | template:run | +| POST | /unfavorite | 取消收藏 | template:run | +| GET | /check-favorited | 检查是否已收藏 | template:run | +| POST | /check-favorited-batch | 批量检查收藏状态 | template:run | +| POST | /favorites | 获取用户收藏列表 | template:run | +| POST | /likes | 获取用户喜欢列表 | template:run | +| POST | /share | 转发模板 | template:run | +| POST | /comment/create | 创建评论 | template:run | +| POST | /comment/delete | 删除评论 | template:run | +| GET | /comments | 获取评论列表 | template:run | +| GET | /comment/replies | 获取评论回复 | template:run | + +### 类型定义 + +```typescript +// 点赞/收藏 +interface LikeTemplateInput { templateId: string; } +interface FavoriteTemplateInput { templateId: string; } + +// 批量检查 +interface CheckLikedBatchInput { templateIds: string[]; } +interface CheckLikedBatchResponse { + results: Record; +} + +// 评论 +interface CreateCommentInput { + templateId: string; + content: string; + parentId?: string; // 回复评论时使用 +} + +interface TemplateComment { + id: string; + userId: string; + templateId: string; + content: string; + parentId?: string; + isDeleted: boolean; + createdAt: Date; + user?: { id: string; name: string; image?: string; }; + replies?: TemplateComment[]; + replyCount?: number; +} + +// 获取评论 +interface GetCommentsInput { + templateId: string; + page?: number; + limit?: number; +} +``` + +--- + +## AI 生成 + +**控制器**: `AigcController` +**路由前缀**: `/loomart/aigc` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| GET | /models | 获取支持的模型列表 | project:run OR template:run | +| POST | /task/submit | 提交图像/视频生成任务 | project:run OR template:run | +| GET | /task/status | 获取任务状态 | project:run OR template:run | + +### 类型定义 + +```typescript +// 模型信息 +interface AigcModel { + model_name: string; + description: string; + description_en: string; + supported_ar: string[]; // 支持的宽高比 + supported_count: string[]; // 支持的生成数量 + supported_duration: string[]; // 支持的时长(视频) + supported_resolution: string[]; + mode: string; // 'image' | 'video' + model_provider: string; + tags: string[]; + tags_en: string[]; + provider: string; + display_name: string; +} + +// 提交任务 +interface SubmitTaskBody { + mode?: string; + model_name: string; + prompt: string; + aspect_ratio?: string; + webhook_flag?: boolean; + watermark?: boolean; + extra?: string; + img_url?: string; + img_list?: any[]; + duration?: string; + resolution?: string; +} + +// 任务状态 +interface GetTaskStatusResult { + task_id: string; + status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'; + progress?: number; + result_url?: string; + error_message?: string; +} +``` + +--- + +## 聊天 + +**控制器**: `ChatController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /chat | 发送聊天消息 | project:run OR template:run | +| GET | /chat/models | 获取模型列表 | project:run OR template:run | + +### 类型定义 + +```typescript +interface ChatRequest { + prompt: string; + model_name: string; + img_list?: string[]; + video_url?: string[]; + stream?: boolean; + tools?: string; + tool_choice?: string; +} + +interface ChatResponse { + data: string; + status: boolean; + msg: string; + usage?: any; +} +``` + +--- + +## 文件处理 + +**控制器**: `FileController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | 内容类型 | +|------|------|------|------|---------| +| POST | /file/upload-s3 | 上传文件到S3 | RequireSession | multipart/form-data | +| POST | /file/compress-video | 压缩视频 | project:run OR template:run | - | +| POST | /file/convert-to-ani | 转换为ANI格式 | project:run OR template:run | - | +| POST | /file/convert-to-webp | 转换为WebP格式 | project:run OR template:run | - | +| POST | /file/convert-to-watermark | 添加水印 | project:run OR template:run | - | +| POST | /file/upload-and-convert-ani | 上传并转换ANI | project:run OR template:run | multipart/form-data | + +### 类型定义 + +```typescript +// 文件上传响应 +interface FileUploadResponse { + status: boolean; + msg: string; + data: string; // 文件URL +} + +// 视频压缩 +interface VideoCompressRequest { + videoUrl: string; + quality: 'low' | 'medium' | 'high'; +} + +// 转换为 WebP +interface ConvertToWebpRequest { + videoUrl: string; + compressionLevel: number; // 0-6 + quality: number; // 0-100 + loop: boolean; + resolution: string; + fps: number; + mode?: string; +} + +// 添加水印 +interface ConvertToWatermarkRequest { + videoUrl: string; + watermarkUrl: string; + position?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'center'; +} +``` + +--- + +## 分类管理 + +**控制器**: `CategoryController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /category/create | 创建分类 | category:create | +| POST | /category/list | 获取分类列表 | - | +| GET | /category/get | 获取单个分类 | - | +| GET | /category/get-by-name | 按名称获取分类 | - | +| GET | /category/get-by-id | 按ID获取分类及模板 | - | +| POST | /category/list-with-tags | 获取分类及标签列表 | - | +| POST | /category/update | 更新分类 | category:update | +| POST | /category/delete | 删除分类 | category:delete | +| POST | /category/batch-update-sort-order | 批量更新排序 | category:update | +| POST | /category/batch-update-tag-sort-order | 批量更新标签排序 | category:update | +| GET | /category/stats | 获取分类统计 | category:read | + +### 类型定义 + +```typescript +interface Category { + id: string; + name: string; + nameEn: string; + description?: string; + descriptionEn?: string; + coverImageUrl?: string; + sortOrder: number; + isDeleted: boolean; + createdAt: Date; + updatedAt: Date; + tags?: Tag[]; + templates?: TemplateDetail[]; +} + +interface CreateCategoryInput { + name: string; + nameEn?: string; + description?: string; + descriptionEn?: string; + coverImageUrl?: string; + sortOrder?: number; +} +``` + +--- + +## 标签管理 + +**控制器**: `TagController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /tags/create | 创建标签 | tag:create | +| POST | /tags/list | 获取标签列表 | tag:list | +| GET | /tags/get | 获取单个标签 | tag:read | +| GET | /tags/get-by-name | 按名称获取标签 | tag:read | +| GET | /tags/get-by-category-tag-id | 按CategoryTag ID获取 | - | +| POST | /tags/update | 更新标签 | tag:update | +| POST | /tags/delete | 删除标签 | tag:delete | +| POST | /tags/update-category-tag | 更新分类标签关联 | tag:update | +| POST | /tags/batch-update-sort-order | 批量更新排序 | tag:update | + +### 类型定义 + +```typescript +interface Tag { + id: string; + name: string; + nameEn: string; + description?: string; + sortOrder: number; + isDeleted: boolean; + createdAt: Date; + updatedAt: Date; +} + +interface CreateTagInput { + name: string; + nameEn?: string; + description?: string; + sortOrder?: number; +} +``` + +--- + +## 项目标签 + +**控制器**: `ProjectTagController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /project-tags/create | 创建用户标签 | - | +| GET | /project-tags/list | 获取标签列表 | - | +| POST | /project-tags/update | 更新标签 | - | +| POST | /project-tags/delete | 删除标签 | - | +| POST | /project-tags/add-to-project | 添加标签到项目 | - | +| POST | /project-tags/remove-from-project | 从项目移除标签 | - | +| POST | /project-tags/set-project-tags | 设置项目标签 | - | + +### 类型定义 + +```typescript +interface UserTag { + id: string; + userId: string; + name: string; + color?: string; + sortOrder: number; + createdAt: Date; + updatedAt: Date; +} + +interface CreateUserTagInput { + name: string; + color?: string; +} + +interface SetProjectTagsInput { + projectId: string; + tagIds: string[]; +} +``` + +--- + +## 消息系统 + +**控制器**: `MessageController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /messages/list | 获取消息列表 | RequireSession | +| GET | /messages/get | 获取单条消息 | RequireSession | +| POST | /messages/mark-read | 标记消息已读 | RequireSession | +| POST | /messages/batch-mark-read | 批量标记已读 | RequireSession | +| POST | /messages/delete | 删除消息 | RequireSession | +| GET | /messages/unread-count | 获取未读数量 | RequireSession | + +### 类型定义 + +```typescript +interface Message { + id: string; + userId: string; + type: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING'; + title: string; + content: string; + data?: any; + link?: string; + priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW'; + expiresAt?: Date; + isRead: boolean; + readAt?: Date; + isDeleted: boolean; + deletedAt?: Date; + createdAt: Date; + updatedAt: Date; +} + +interface ListMessagesInput { + page?: number; + limit?: number; + type?: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING'; + isRead?: boolean; +} + +interface UnreadCountResult { + total: number; + byType: { + SYSTEM: number; + ACTIVITY: number; + BILLING: number; + MARKETING: number; + }; +} +``` + +--- + +## 公告系统 + +**控制器**: `AnnouncementController` +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| POST | /announcements/create | 创建公告 | announcement:create | +| POST | /announcements/list | 获取公告列表 | - | +| GET | /announcements/get | 获取单条公告 | - | +| POST | /announcements/update | 更新公告 | announcement:update | +| POST | /announcements/delete | 删除公告 | announcement:delete | +| POST | /announcements/mark-read | 标记已读 | RequireSession | +| GET | /announcements/unread-count | 获取未读数量 | RequireSession | + +--- + +## 权限管理 + +**控制器**: `PermissionController` +**路由前缀**: `/loomart/admin/permissions` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| GET | /permissions/list | 获取权限列表 | user:permissions | +| POST | /permissions/create | 创建权限 | user:permissions | +| PATCH | /permissions/get | 更新权限 | user:permissions | +| DELETE | /permissions/delete | 删除权限 | user:permissions | + +### 类型定义 + +```typescript +interface Permission { + id: string; + resource: string; + action: string; + description?: string; + createdAt: Date; + updatedAt: Date; +} +``` + +--- + +## 角色管理 + +**控制器**: `RoleController` +**路由前缀**: `/loomart/admin` + +| 方法 | 路由 | 功能 | 权限 | +|------|------|------|------| +| GET | /roles/list | 获取角色列表 | role:list | +| POST | /roles/create | 创建角色 | role:create | +| PATCH | /roles/update | 更新角色基本信息 | role:update | +| PUT | /roles/update/permissions | 更新角色权限 | role:update | +| DELETE | /roles/delete | 删除角色 | role:delete | + +### 类型定义 + +```typescript +interface RoleWithPermissions { + id: string; + name: string; + displayName: string; + description?: string; + isSystem: boolean; + scope: string; + organizationId?: string; + createdAt: Date; + updatedAt: Date; + permissions: Permission[]; +} +``` + +--- + +## 支付相关 + +### AlipayController + +**路由前缀**: `/loomart` + +| 方法 | 路由 | 功能 | +|------|------|------| +| POST | /alipay/app-pay | 支付宝APP支付下单 | +| POST | /alipay/auth-info | 获取支付宝授权字符串 | +| POST | /alipay/webhook | 支付宝异步通知回调 | +| POST | /credits/pre-recharge | 积分预充值 | + +### AdminBalanceController + +**路由前缀**: `/loomart/admin-balance` + +| 方法 | 路由 | 功能 | +|------|------|------| +| POST | /add-user-balance | 管理员给用户充值 | +| POST | /deduct-user-balance | 管理员扣除用户余额 | +| POST | /get-user-balance | 查询用户当前余额 | diff --git a/.claude/skills/repo-sdk/references/workflows.md b/.claude/skills/repo-sdk/references/workflows.md new file mode 100644 index 0000000..9d1925d --- /dev/null +++ b/.claude/skills/repo-sdk/references/workflows.md @@ -0,0 +1,1010 @@ +# 接口组合使用场景 + +本文档详细描述 @repo/sdk 中各接口的组合使用场景,帮助理解业务流程中接口之间的依赖关系。 + +> **推荐**:所有示例都使用 `root.get(Controller)` 方式调用,并复用 SDK 导出的类型。 + +## 目录 + +1. [模板运行完整流程](#1-模板运行完整流程) +2. [模板社交互动流程](#2-模板社交互动流程) +3. [项目管理与标签流程](#3-项目管理与标签流程) +4. [文件上传与处理流程](#4-文件上传与处理流程) +5. [分类标签管理流程](#5-分类标签管理流程) +6. [消息通知处理流程](#6-消息通知处理流程) +7. [用户收藏夹管理流程](#7-用户收藏夹管理流程) +8. [AI 内容生成流程](#8-ai-内容生成流程) +9. [权限角色管理流程](#9-权限角色管理流程) + +--- + +## 1. 模板运行完整流程 + +**场景**:用户选择模板,填写表单,生成内容,保存到项目 + +**涉及控制器**: +- `TemplateController` +- `AigcController` +- `FileController` +- `TemplateGenerationController` +- `ProjectController` + +### 流程图 + +``` +用户选择模板 + ↓ +TemplateController.get(templateId) + ↓ +用户填写表单(根据 formSchema) + ↓ +TemplateController.run({ templateId, formData, projectId? }) + ↓ 返回 generationId +轮询任务状态 + ↓ +AigcController.getTaskStatus(taskId) + ↓ status === 'COMPLETED' +获取生成结果 + ↓ +FileController.convertToWebp({ videoUrl }) [可选:格式转换] + ↓ +ProjectController.update({ id, resultUrl }) [保存到项目] +``` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { + TemplateController, + AigcController, + FileController, + ProjectController +} from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + TemplateDetail, + Project, + GetTaskStatusResult, + ConvertToWebpRequest +} from '@repo/sdk' + +async function runTemplateWorkflow(templateId: string, formData: Record) { + // 使用 root.get 获取控制器(类型安全) + const templateCtrl = root.get(TemplateController) + const aigcCtrl = root.get(AigcController) + const fileCtrl = root.get(FileController) + const projectCtrl = root.get(ProjectController) + + // 1. 获取模板详情(了解表单结构) + const template: TemplateDetail = await templateCtrl.get(templateId) + console.log('模板表单结构:', template.formSchema) + + // 2. 创建项目(可选,用于保存结果) + const project: Project = await projectCtrl.create({ + title: `基于 ${template.title} 的创作`, + content: { templateId, formData }, + sourceTemplateId: templateId + }) + + // 3. 运行模板 + const { generationId } = await templateCtrl.run({ + templateId, + formData, + projectId: project.id + }) + + // 4. 轮询任务状态 + let taskStatus: GetTaskStatusResult = await aigcCtrl.getTaskStatus(generationId) + while (taskStatus.status === 'PENDING' || taskStatus.status === 'PROCESSING') { + await new Promise(resolve => setTimeout(resolve, 2000)) + taskStatus = await aigcCtrl.getTaskStatus(generationId) + console.log(`进度: ${taskStatus.progress || 0}%`) + } + + if (taskStatus.status === 'FAILED') { + throw new Error(`生成失败: ${taskStatus.error_message}`) + } + + // 5. 处理生成结果(转换为 WebP 格式) + const webpResult = await fileCtrl.convertToWebp({ + videoUrl: taskStatus.result_url!, + quality: 80, + compressionLevel: 4, + loop: true, + resolution: '720p', + fps: 15 + }) + + // 6. 更新项目结果 + await projectCtrl.update({ + id: project.id, + resultUrl: webpResult.data + }) + + return { + project, + generationId, + resultUrl: webpResult.data + } +} +``` + +### 重新运行场景 + +```typescript +import { root } from '@repo/core' +import { TemplateController } from '@repo/sdk' + +// 基于历史生成记录重新运行 +async function rerunFromHistory(generationId: string) { + const templateCtrl = root.get(TemplateController) + + // 直接使用历史记录的参数重新运行 + const { generationId: newId } = await templateCtrl.rerun({ generationId }) + + // 继续轮询新任务... + return newId +} +``` + +--- + +## 2. 模板社交互动流程 + +**场景**:用户浏览模板列表,点赞、收藏、评论 + +**涉及控制器**: +- `TemplateController` +- `TemplateSocialController` + +### 流程图 + +``` +获取模板列表 + ↓ +TemplateController.list({ page, limit }) + ↓ +批量检查社交状态 + ↓ +TemplateSocialController.checkLikedBatch({ templateIds }) +TemplateSocialController.checkFavoritedBatch({ templateIds }) + ↓ +用户交互 + ↓ +点赞: TemplateSocialController.like({ templateId }) +收藏: TemplateSocialController.favorite({ templateId }) +评论: TemplateSocialController.createComment({ templateId, content }) +``` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { TemplateController, TemplateSocialController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + TemplateDetail, + ListTemplatesResult, + CheckLikedBatchResponse, + CheckFavoritedBatchResponse, + TemplateComment, + GetCommentsResponse +} from '@repo/sdk' + +// 获取模板列表并附加社交状态 +async function getTemplatesWithSocialStatus(page: number = 1) { + const templateCtrl = root.get(TemplateController) + const socialCtrl = root.get(TemplateSocialController) + + // 1. 获取模板列表 + const { templates, total }: ListTemplatesResult = await templateCtrl.list({ + page, + limit: 20, + sortBy: 'likeCount', + sortOrder: 'desc' + }) + + const templateIds = templates.map(t => t.id) + + // 2. 批量检查点赞和收藏状态 + const [likedStatus, favoritedStatus]: [CheckLikedBatchResponse, CheckFavoritedBatchResponse] = await Promise.all([ + socialCtrl.checkLikedBatch({ templateIds }), + socialCtrl.checkFavoritedBatch({ templateIds }) + ]) + + // 3. 合并状态到模板数据 + const templatesWithStatus = templates.map(template => ({ + ...template, + isLiked: likedStatus.results[template.id] || false, + isFavorited: favoritedStatus.results[template.id] || false + })) + + return { templates: templatesWithStatus, total } +} + +// 点赞/取消点赞 +async function toggleLike(templateId: string, isCurrentlyLiked: boolean): Promise { + const socialCtrl = root.get(TemplateSocialController) + + if (isCurrentlyLiked) { + await socialCtrl.unlike({ templateId }) + } else { + await socialCtrl.like({ templateId }) + } + + return !isCurrentlyLiked +} + +// 获取评论列表(含回复) +async function getCommentsWithReplies(templateId: string): Promise { + const socialCtrl = root.get(TemplateSocialController) + + // 1. 获取顶级评论(自动包含前3条回复) + const { comments, total } = await socialCtrl.getComments({ + templateId, + page: 1, + limit: 20 + }) + + // 2. 对于回复数超过3的评论,加载更多回复 + for (const comment of comments) { + if (comment.replyCount && comment.replyCount > 3) { + const { replies } = await socialCtrl.getCommentReplies({ + commentId: comment.id, + page: 1, + limit: 50 + }) + comment.replies = replies + } + } + + return { comments, total } +} + +// 发表评论或回复 +async function postComment(templateId: string, content: string, parentId?: string): Promise { + const socialCtrl = root.get(TemplateSocialController) + + const comment = await socialCtrl.createComment({ + templateId, + content, + parentId // 如果是回复,传入父评论ID + }) + + return comment +} +``` + +--- + +## 3. 项目管理与标签流程 + +**场景**:用户创建项目,使用标签分类管理 + +**涉及控制器**: +- `ProjectController` +- `ProjectTagController` + +### 流程图 + +``` +创建用户标签 + ↓ +ProjectTagController.create({ name, color }) + ↓ +创建项目 + ↓ +ProjectController.create({ title, content }) + ↓ +给项目添加标签 + ↓ +ProjectTagController.addToProject({ projectId, tagId }) + ↓ +按标签筛选项目 + ↓ +ProjectController.list({ tagId }) +``` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { ProjectController, ProjectTagController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + Project, + CreateProjectInput, + ListProjectsResult, + UserTag, + TransferProjectResult +} from '@repo/sdk' + +// 初始化用户标签系统 +async function initUserTags(): Promise { + const tagCtrl = root.get(ProjectTagController) + + // 创建默认标签 + const defaultTags = [ + { name: '收藏夹', color: '#FF6B6B' }, + { name: '工作', color: '#4ECDC4' }, + { name: '个人', color: '#45B7D1' }, + { name: '灵感', color: '#96CEB4' } + ] + + const tags = await Promise.all( + defaultTags.map(tag => tagCtrl.create(tag)) + ) + + return tags +} + +// 创建项目并分类 +async function createProjectWithTags( + projectData: CreateProjectInput, + tagIds: string[] +): Promise { + const projectCtrl = root.get(ProjectController) + const tagCtrl = root.get(ProjectTagController) + + // 1. 创建项目 + const project: Project = await projectCtrl.create(projectData) + + // 2. 批量设置标签 + await tagCtrl.setProjectTags({ + projectId: project.id, + tagIds + }) + + return project +} + +// 获取用户所有标签及项目数量 +async function getTagsWithProjectCount(): Promise> { + const tagCtrl = root.get(ProjectTagController) + const projectCtrl = root.get(ProjectController) + + // 1. 获取所有标签 + const tags = await tagCtrl.list() + + // 2. 获取每个标签下的项目数量 + const tagsWithCount = await Promise.all( + tags.map(async tag => { + const { total } = await projectCtrl.list({ tagId: tag.id, limit: 1 }) + return { ...tag, projectCount: total } + }) + ) + + return tagsWithCount +} + +// 项目转移(复制到其他用户) +async function copyProjectToUser(projectId: string, targetUserId: string): Promise { + const projectCtrl = root.get(ProjectController) + + const result = await projectCtrl.transfer({ + projectId, + targetUserId, + mode: 'copy' // 'transfer' 会移动所有权,'copy' 会复制 + }) + + return result +} +``` + +--- + +## 4. 文件上传与处理流程 + +**场景**:用户上传文件,进行格式转换和处理 + +**涉及控制器**: +- `FileController` +- `FFmpegController`(视频合成) + +### 流程图 + +``` +用户选择文件 + ↓ +FileController.uploadS3(formData) + ↓ 返回文件URL +选择处理方式 + ↓ +├─ 压缩视频: FileController.compressVideo({ videoUrl, quality }) +├─ 转WebP: FileController.convertToWebp({ videoUrl, ... }) +├─ 转ANI: FileController.convertToAni({ videoUrl }) +├─ 加水印: FileController.convertToWatermark({ videoUrl, watermarkUrl }) +└─ 视频合成: FFmpegController.concat({ videoUrls }) +``` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { FileController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + FileUploadResponse, + VideoCompressRequest, + ConvertToWebpRequest, + ConvertToWatermarkRequest +} from '@repo/sdk' + +// 上传文件 +async function uploadFile(file: File): Promise { + const fileCtrl = root.get(FileController) + + const formData = new FormData() + formData.append('file', file) + + const result: FileUploadResponse = await fileCtrl.uploadS3(formData) + + if (!result.status) { + throw new Error(result.msg) + } + + return result.data // 文件URL +} + +// 完整的视频处理流程 +async function processVideo(file: File, options: { + compress?: boolean; + addWatermark?: boolean; + watermarkUrl?: string; + convertToWebp?: boolean; +}): Promise { + const fileCtrl = root.get(FileController) + + // 1. 上传原始文件 + let videoUrl = await uploadFile(file) + + // 2. 压缩视频(可选) + if (options.compress) { + const compressed = await fileCtrl.compressVideo({ + videoUrl, + quality: 'medium' + }) + videoUrl = compressed.data + } + + // 3. 添加水印(可选) + if (options.addWatermark && options.watermarkUrl) { + const watermarked = await fileCtrl.convertToWatermark({ + videoUrl, + watermarkUrl: options.watermarkUrl, + position: 'bottomRight' + }) + videoUrl = watermarked.data + } + + // 4. 转换为 WebP(可选) + if (options.convertToWebp) { + const webp = await fileCtrl.convertToWebp({ + videoUrl, + quality: 80, + compressionLevel: 4, + loop: true, + resolution: '720p', + fps: 15 + }) + videoUrl = webp.data + } + + return videoUrl +} + +// 一键上传并转换为 ANI 格式 +async function uploadAndConvertToAni(file: File): Promise { + const fileCtrl = root.get(FileController) + + const formData = new FormData() + formData.append('file', file) + + // 使用组合接口,一步完成上传和转换 + const result = await fileCtrl.uploadAndConvertToAni(formData) + + return result.data +} +``` + +--- + +## 5. 分类标签管理流程 + +**场景**:管理员管理模板分类和标签体系 + +**涉及控制器**: +- `CategoryController` +- `TagController` + +### 流程图 + +``` +创建分类 + ↓ +CategoryController.create({ name, nameEn }) + ↓ +创建标签 + ↓ +TagController.create({ name, nameEn }) + ↓ +关联标签到分类 + ↓ +TagController.updateCategoryTag({ tagId, categoryId, sortOrder }) + ↓ +调整排序 + ↓ +CategoryController.batchUpdateSortOrder({ items }) +CategoryController.batchUpdateTagSortOrder({ categoryId, items }) +``` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { CategoryController, TagController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + Category, + Tag, + ListCategoriesWithTagsResult +} from '@repo/sdk' + +// 创建完整的分类标签体系 +async function setupCategorySystem(): Promise<{ categories: Category[]; tags: Tag[] }> { + const categoryCtrl = root.get(CategoryController) + const tagCtrl = root.get(TagController) + + // 1. 创建分类 + const categories: Category[] = await Promise.all([ + categoryCtrl.create({ name: '热门推荐', nameEn: 'Hot', sortOrder: 1 }), + categoryCtrl.create({ name: '视频生成', nameEn: 'Video', sortOrder: 2 }), + categoryCtrl.create({ name: '图片生成', nameEn: 'Image', sortOrder: 3 }) + ]) + + // 2. 创建标签 + const tags: Tag[] = await Promise.all([ + tagCtrl.create({ name: '人物', nameEn: 'Portrait' }), + tagCtrl.create({ name: '风景', nameEn: 'Landscape' }), + tagCtrl.create({ name: '动漫', nameEn: 'Anime' }), + tagCtrl.create({ name: '写实', nameEn: 'Realistic' }) + ]) + + // 3. 关联标签到分类 + const videoCategory = categories[1] + for (let i = 0; i < tags.length; i++) { + await tagCtrl.updateCategoryTag({ + tagId: tags[i].id, + categoryId: videoCategory.id, + sortOrder: i + 1 + }) + } + + return { categories, tags } +} + +// 获取分类及其标签(用于前端导航) +async function getCategoryNavigation(): Promise { + const categoryCtrl = root.get(CategoryController) + + const { categories }: ListCategoriesWithTagsResult = await categoryCtrl.listWithTags({ + page: 1, + limit: 100 + }) + + return categories +} + +// 获取分类下的模板(带分页) +async function getCategoryTemplates(categoryId: string, page: number = 1) { + const categoryCtrl = root.get(CategoryController) + + const category: Category = await categoryCtrl.getById( + categoryId, + page, + 20, + 'sortOrder', + 'asc' + ) + + return { + category, + templates: category.templates || [] + } +} + +// 批量调整分类排序 +async function reorderCategories(items: Array<{ id: string; sortOrder: number }>): Promise { + const categoryCtrl = root.get(CategoryController) + + await categoryCtrl.batchUpdateSortOrder({ items }) +} +``` + +--- + +## 6. 消息通知处理流程 + +**场景**:用户查看和管理消息通知 + +**涉及控制器**: +- `MessageController` +- `AnnouncementController` + +### 流程图 + +``` +获取未读数量 + ↓ +MessageController.getUnreadCount() +AnnouncementController.getUnreadCount() + ↓ +获取消息列表 + ↓ +MessageController.list({ type, isRead }) + ↓ +标记已读 + ↓ +MessageController.markRead({ id }) +MessageController.batchMarkRead({ ids }) +``` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { MessageController, AnnouncementController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + Message, + UnreadCountResult, + ListMessagesResult +} from '@repo/sdk' + +// 获取所有未读通知数量 +async function getAllUnreadCounts() { + const messageCtrl = root.get(MessageController) + const announcementCtrl = root.get(AnnouncementController) + + const [messageUnread, announcementUnread]: [UnreadCountResult, number] = await Promise.all([ + messageCtrl.getUnreadCount(), + announcementCtrl.getUnreadCount() + ]) + + return { + messages: messageUnread, + announcements: announcementUnread, + total: messageUnread.total + announcementUnread + } +} + +// 获取分类消息 +async function getMessagesByType(type: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING'): Promise { + const messageCtrl = root.get(MessageController) + + const { messages, total } = await messageCtrl.list({ + type, + page: 1, + limit: 50 + }) + + return { messages, total } +} + +// 一键已读所有消息 +async function markAllAsRead(): Promise { + const messageCtrl = root.get(MessageController) + + // 获取所有未读消息 + const { messages } = await messageCtrl.list({ + isRead: false, + limit: 1000 + }) + + if (messages.length === 0) return + + // 批量标记已读 + await messageCtrl.batchMarkRead({ + ids: messages.map(m => m.id) + }) +} + +// 消息中心完整数据 +async function getMessageCenter() { + const messageCtrl = root.get(MessageController) + const announcementCtrl = root.get(AnnouncementController) + + const [ + unreadCount, + systemMessages, + activityMessages, + billingMessages, + announcements + ] = await Promise.all([ + messageCtrl.getUnreadCount(), + messageCtrl.list({ type: 'SYSTEM', limit: 10 }), + messageCtrl.list({ type: 'ACTIVITY', limit: 10 }), + messageCtrl.list({ type: 'BILLING', limit: 10 }), + announcementCtrl.list({ page: 1, limit: 5 }) + ]) + + return { + unreadCount, + systemMessages: systemMessages.messages, + activityMessages: activityMessages.messages, + billingMessages: billingMessages.messages, + announcements: announcements.announcements + } +} +``` + +--- + +## 7. 用户收藏夹管理流程 + +**场景**:用户管理收藏的模板和喜欢的内容 + +**涉及控制器**: +- `TemplateSocialController` +- `TemplateController` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { TemplateSocialController, TemplateController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + TemplateDetail, + GetUserFavoritesResponse, + GetUserLikesResponse +} from '@repo/sdk' + +// 获取用户收藏列表(带模板详情) +async function getUserFavorites(page: number = 1): Promise<{ templates: TemplateDetail[]; total: number }> { + const socialCtrl = root.get(TemplateSocialController) + const templateCtrl = root.get(TemplateController) + + // 1. 获取收藏列表 + const { favorites, total }: GetUserFavoritesResponse = await socialCtrl.getUserFavorites({ + page, + limit: 20 + }) + + // 2. 获取模板详情 + const templates: TemplateDetail[] = await Promise.all( + favorites.map(fav => templateCtrl.get(fav.templateId)) + ) + + return { templates, total } +} + +// 获取用户喜欢列表 +async function getUserLikes(page: number = 1): Promise { + const socialCtrl = root.get(TemplateSocialController) + + const { likes, total } = await socialCtrl.getUserLikes({ + page, + limit: 20 + }) + + return { likes, total } +} + +// 收藏/取消收藏 +async function toggleFavorite(templateId: string, isCurrentlyFavorited: boolean): Promise { + const socialCtrl = root.get(TemplateSocialController) + + if (isCurrentlyFavorited) { + await socialCtrl.unfavorite({ templateId }) + } else { + await socialCtrl.favorite({ templateId }) + } + + return !isCurrentlyFavorited +} +``` + +--- + +## 8. AI 内容生成流程 + +**场景**:直接使用 AI 模型生成图片或视频 + +**涉及控制器**: +- `AigcController` +- `ChatController` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { AigcController, ChatController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + AigcModel, + GetModelsResult, + SubmitTaskBody, + GetTaskStatusResult, + ChatRequest, + ChatResponse +} from '@repo/sdk' + +// 获取可用模型列表 +async function getAvailableModels(category?: 'image' | 'video'): Promise> { + const aigcCtrl = root.get(AigcController) + + const { models }: GetModelsResult = await aigcCtrl.getModels(category) + + // 按提供商分组 + const groupedModels = models.reduce((acc, model) => { + const provider = model.provider + if (!acc[provider]) acc[provider] = [] + acc[provider].push(model) + return acc + }, {} as Record) + + return groupedModels +} + +// 生成图片 +async function generateImage(prompt: string, modelName: string, options?: { + aspectRatio?: string; + watermark?: boolean; +}): Promise { + const aigcCtrl = root.get(AigcController) + + // 1. 提交任务 + const { task_id } = await aigcCtrl.submitTask({ + mode: 'image', + model_name: modelName, + prompt, + aspect_ratio: options?.aspectRatio || '1:1', + watermark: options?.watermark ?? false + }) + + // 2. 轮询状态 + let status: GetTaskStatusResult = await aigcCtrl.getTaskStatus(task_id) + while (status.status === 'PENDING' || status.status === 'PROCESSING') { + await new Promise(resolve => setTimeout(resolve, 2000)) + status = await aigcCtrl.getTaskStatus(task_id) + } + + if (status.status === 'FAILED') { + throw new Error(status.error_message) + } + + return status.result_url +} + +// AI 对话(支持图片输入) +async function chat(prompt: string, options?: { + modelName?: string; + images?: string[]; + stream?: boolean; +}): Promise { + const chatCtrl = root.get(ChatController) + + const response: ChatResponse = await chatCtrl.chat({ + prompt, + model_name: options?.modelName || 'gpt-4o', + img_list: options?.images, + stream: options?.stream ?? false + }) + + return response.data +} + +// 图生图 +async function imageToImage(imageUrl: string, prompt: string, modelName: string): Promise { + const aigcCtrl = root.get(AigcController) + + const { task_id } = await aigcCtrl.submitTask({ + mode: 'image', + model_name: modelName, + prompt, + img_url: imageUrl + }) + + // 轮询状态 + let status: GetTaskStatusResult = await aigcCtrl.getTaskStatus(task_id) + while (status.status === 'PENDING' || status.status === 'PROCESSING') { + await new Promise(resolve => setTimeout(resolve, 2000)) + status = await aigcCtrl.getTaskStatus(task_id) + } + + return status.result_url +} +``` + +--- + +## 9. 权限角色管理流程 + +**场景**:管理员配置系统权限和角色 + +**涉及控制器**: +- `PermissionController` +- `RoleController` + +### 代码示例 + +```typescript +import { root } from '@repo/core' +import { PermissionController, RoleController } from '@repo/sdk' +// 复用 SDK 导出的类型 +import type { + Permission, + RoleWithPermissions +} from '@repo/sdk' + +// 初始化权限系统 +async function initPermissionSystem(): Promise<{ permissions: Permission[]; roles: RoleWithPermissions[] }> { + const permCtrl = root.get(PermissionController) + const roleCtrl = root.get(RoleController) + + // 1. 创建基础权限 + const permissions: Permission[] = await Promise.all([ + permCtrl.create({ resource: 'project', action: 'create' }), + permCtrl.create({ resource: 'project', action: 'read' }), + permCtrl.create({ resource: 'project', action: 'update' }), + permCtrl.create({ resource: 'project', action: 'delete' }), + permCtrl.create({ resource: 'template', action: 'run' }), + permCtrl.create({ resource: 'category', action: 'create' }), + permCtrl.create({ resource: 'category', action: 'update' }) + ]) + + // 2. 创建角色 + const userRole = await roleCtrl.create({ + name: 'user', + displayName: '普通用户', + description: '基础用户权限' + }) + + const adminRole = await roleCtrl.create({ + name: 'admin', + displayName: '管理员', + description: '管理员权限' + }) + + // 3. 分配权限给角色 + await roleCtrl.updatePermissions({ + roleId: userRole.id, + permissionIds: permissions + .filter(p => p.resource === 'project' || p.action === 'run') + .map(p => p.id) + }) + + await roleCtrl.updatePermissions({ + roleId: adminRole.id, + permissionIds: permissions.map(p => p.id) + }) + + return { permissions, roles: [userRole, adminRole] } +} + +// 获取角色及其权限 +async function getRolesWithPermissions(): Promise { + const roleCtrl = root.get(RoleController) + + const { roles } = await roleCtrl.list() + + return roles +} +``` + +--- + +## 接口依赖关系总结 + +| 主流程 | 依赖接口 | 说明 | +|--------|----------|------| +| 模板运行 | Template → AIGC → File → Project | 完整的内容生成流程 | +| 社交互动 | Template → TemplateSocial | 点赞、收藏、评论 | +| 项目管理 | Project ↔ ProjectTag | 双向关联 | +| 文件处理 | File(多个方法链式调用) | 上传→压缩→转换→水印 | +| 分类标签 | Category ↔ Tag | 多对多关联 | +| 消息通知 | Message + Announcement | 并行获取 | +| AI 生成 | AIGC + Chat | 独立或组合使用 | +| 权限管理 | Permission → Role | 权限分配给角色 | diff --git a/bun.lock b/bun.lock index 7e95861..f0fb462 100644 --- a/bun.lock +++ b/bun.lock @@ -13,8 +13,8 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", - "@repo/core": "1.0.3", - "@repo/sdk": "1.0.14", + "@repo/core": "1.0.4", + "@repo/sdk": "1.0.15", "@shopify/flash-list": "^2.0.0", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.5.3", @@ -655,9 +655,9 @@ "@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="], - "@repo/core": ["@repo/core@1.0.3", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fcore/-/1.0.3/core-1.0.3.tgz", {}, "sha512-lO7rk3hsrtoyewZu7cgwlFqjjhGBx+lw4wxkehfvTsbTWm/tKChq1t6SC+XXNJj/YVwnLp6AH8BOsvR4r1nxyg=="], + "@repo/core": ["@repo/core@1.0.4", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fcore/-/1.0.4/core-1.0.4.tgz", {}, "sha512-uht306Xupvs637Ww4EPbCTHw9Gi0g0xbDZpnZHX0Eldw4xGfwAgW1SqC65qNxT2Wi2qkFeBvSBNdP6fB6CuxQg=="], - "@repo/sdk": ["@repo/sdk@1.0.14", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fsdk/-/1.0.14/sdk-1.0.14.tgz", { "dependencies": { "@repo/core": "1.0.3", "reflect-metadata": "^0.2.1", "zod": "^4.2.1" } }, "sha512-J/j3kRL83X4TtGadjLUGX9ueiROStZ0hM+zCPkKtgk5bMUR8K98HefbWzt1PV8jKBp9n+heckyHWHcL3nKDPwA=="], + "@repo/sdk": ["@repo/sdk@1.0.15", "https://gitea.bowongai.com/api/packages/bowong/npm/%40repo%2Fsdk/-/1.0.15/sdk-1.0.15.tgz", { "dependencies": { "@repo/core": "1.0.4", "reflect-metadata": "^0.2.1", "zod": "^4.2.1" } }, "sha512-QZtMwBQ/SM5m4qFMv63kCktdYhEzpzOrx1HWoQaOgLyV/vnyff5ktRNqmTVkHlPGGZmqlOjJHnpbqV6iQSeoYA=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], diff --git a/components/message/MessageCard.test.tsx b/components/message/MessageCard.test.tsx new file mode 100644 index 0000000..00449cf --- /dev/null +++ b/components/message/MessageCard.test.tsx @@ -0,0 +1,373 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react-native' + +import { MessageCard, getMessageIcon, getMessageConfig, formatRelativeTime } from './MessageCard' + +// Mock react-i18next +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { language: 'zh-CN' }, + }), +})) + +describe('MessageCard Utilities', () => { + describe('getMessageIcon', () => { + it('should return celebration icon for TEMPLATE_GENERATION_SUCCESS', () => { + expect(getMessageIcon('TEMPLATE_GENERATION_SUCCESS')).toBe('🎉') + }) + + it('should return error icon for TEMPLATE_GENERATION_FAILED', () => { + expect(getMessageIcon('TEMPLATE_GENERATION_FAILED')).toBe('❌') + }) + + it('should return thumbs up icon for TEMPLATE_LIKED', () => { + expect(getMessageIcon('TEMPLATE_LIKED')).toBe('👍') + }) + + it('should return star icon for TEMPLATE_FAVORITED', () => { + expect(getMessageIcon('TEMPLATE_FAVORITED')).toBe('⭐') + }) + + it('should return comment icon for TEMPLATE_COMMENTED', () => { + expect(getMessageIcon('TEMPLATE_COMMENTED')).toBe('💬') + }) + + it('should return comment icon for COMMENT_REPLIED', () => { + expect(getMessageIcon('COMMENT_REPLIED')).toBe('💬') + }) + + it('should return money icon for CREDITS_DEDUCTED', () => { + expect(getMessageIcon('CREDITS_DEDUCTED')).toBe('💰') + }) + + it('should return money icon for CREDITS_REFUNDED', () => { + expect(getMessageIcon('CREDITS_REFUNDED')).toBe('💰') + }) + + it('should return money icon for CREDITS_RECHARGED', () => { + expect(getMessageIcon('CREDITS_RECHARGED')).toBe('💰') + }) + + it('should return announcement icon for ANNOUNCEMENT', () => { + expect(getMessageIcon('ANNOUNCEMENT')).toBe('📢') + }) + + it('should return default icon for unknown subType', () => { + expect(getMessageIcon('UNKNOWN_TYPE')).toBe('📩') + }) + + it('should return default icon for undefined subType', () => { + expect(getMessageIcon(undefined)).toBe('📩') + }) + }) + + describe('getMessageConfig', () => { + it('should return showPreview true for TEMPLATE_GENERATION_SUCCESS', () => { + const config = getMessageConfig('TEMPLATE_GENERATION_SUCCESS') + expect(config.showPreview).toBe(true) + expect(config.showAvatar).toBe(false) + }) + + it('should return showAvatar true for TEMPLATE_LIKED', () => { + const config = getMessageConfig('TEMPLATE_LIKED') + expect(config.showAvatar).toBe(true) + expect(config.showPreview).toBe(false) + }) + + it('should return showAvatar true for TEMPLATE_FAVORITED', () => { + const config = getMessageConfig('TEMPLATE_FAVORITED') + expect(config.showAvatar).toBe(true) + }) + + it('should return showAvatar true for TEMPLATE_COMMENTED', () => { + const config = getMessageConfig('TEMPLATE_COMMENTED') + expect(config.showAvatar).toBe(true) + }) + + it('should return showQuote true for COMMENT_REPLIED', () => { + const config = getMessageConfig('COMMENT_REPLIED') + expect(config.showQuote).toBe(true) + expect(config.showAvatar).toBe(true) + }) + + it('should return default config for unknown subType', () => { + const config = getMessageConfig('UNKNOWN_TYPE') + expect(config.showPreview).toBe(false) + expect(config.showAvatar).toBe(false) + expect(config.showQuote).toBe(false) + }) + }) + + describe('formatRelativeTime', () => { + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2026-01-29T12:00:00Z')) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should return "刚刚" for time within 1 minute', () => { + const now = new Date() + const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000) + expect(formatRelativeTime(thirtySecondsAgo)).toBe('刚刚') + }) + + it('should return "X分钟前" for time within 1 hour', () => { + const now = new Date() + const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000) + expect(formatRelativeTime(tenMinutesAgo)).toBe('10分钟前') + }) + + it('should return "X小时前" for time within 24 hours', () => { + const now = new Date() + const threeHoursAgo = new Date(now.getTime() - 3 * 60 * 60 * 1000) + expect(formatRelativeTime(threeHoursAgo)).toBe('3小时前') + }) + + it('should return "昨天 HH:MM" for time within 48 hours', () => { + const now = new Date() + const yesterday = new Date(now.getTime() - 25 * 60 * 60 * 1000) + const result = formatRelativeTime(yesterday) + expect(result).toMatch(/^昨天 \d{2}:\d{2}$/) + }) + + it('should return formatted date for older times', () => { + const now = new Date() + const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) + const result = formatRelativeTime(threeDaysAgo) + expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/) + }) + + it('should handle string date input', () => { + const result = formatRelativeTime('2026-01-29T11:50:00Z') + expect(result).toBe('10分钟前') + }) + }) +}) + +describe('MessageCard Component', () => { + const mockMessage = { + id: 'msg-123', + type: 'ACTIVITY' as const, + title: '收到新点赞', + content: '小明 赞了你的作品「风景照」', + data: { + subType: 'TEMPLATE_LIKED', + likerName: '小明', + likerAvatar: 'https://example.com/avatar.jpg', + templateId: 'tpl-456', + templateTitle: '风景照', + }, + isRead: false, + createdAt: new Date('2026-01-29T11:50:00Z'), + } + + const mockOnPress = jest.fn() + const mockOnDelete = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + jest.useFakeTimers() + jest.setSystemTime(new Date('2026-01-29T12:00:00Z')) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should render message title', () => { + const { getByText } = render( + + ) + expect(getByText('收到新点赞')).toBeTruthy() + }) + + it('should render message content', () => { + const { getByText } = render( + + ) + expect(getByText('小明 赞了你的作品「风景照」')).toBeTruthy() + }) + + it('should render relative time', () => { + const { getByText } = render( + + ) + expect(getByText('10分钟前')).toBeTruthy() + }) + + it('should show unread indicator when isRead is false', () => { + const { getByTestId } = render( + + ) + expect(getByTestId('unread-indicator')).toBeTruthy() + }) + + it('should not show unread indicator when isRead is true', () => { + const readMessage = { ...mockMessage, isRead: true } + const { queryByTestId } = render( + + ) + expect(queryByTestId('unread-indicator')).toBeNull() + }) + + it('should call onPress when card is pressed', () => { + const { getByTestId } = render( + + ) + fireEvent.press(getByTestId('message-card')) + expect(mockOnPress).toHaveBeenCalledWith(mockMessage) + }) + + it('should render message icon based on subType', () => { + const { getByText } = render( + + ) + expect(getByText('👍')).toBeTruthy() + }) + + describe('Avatar Display', () => { + it('should show avatar for TEMPLATE_LIKED messages', () => { + const { getByTestId } = render( + + ) + expect(getByTestId('user-avatar')).toBeTruthy() + }) + + it('should not show avatar for CREDITS_RECHARGED messages', () => { + const billingMessage = { + ...mockMessage, + type: 'BILLING' as const, + data: { subType: 'CREDITS_RECHARGED', credits: 1000 }, + } + const { queryByTestId } = render( + + ) + expect(queryByTestId('user-avatar')).toBeNull() + }) + }) + + describe('Preview Image Display', () => { + it('should show preview image for TEMPLATE_GENERATION_SUCCESS', () => { + const successMessage = { + ...mockMessage, + type: 'SYSTEM' as const, + data: { + subType: 'TEMPLATE_GENERATION_SUCCESS', + webpPreviewUrl: 'https://example.com/preview.webp', + }, + } + const { getByTestId } = render( + + ) + expect(getByTestId('preview-image')).toBeTruthy() + }) + + it('should not show preview image when webpPreviewUrl is missing', () => { + const successMessage = { + ...mockMessage, + type: 'SYSTEM' as const, + data: { subType: 'TEMPLATE_GENERATION_SUCCESS' }, + } + const { queryByTestId } = render( + + ) + expect(queryByTestId('preview-image')).toBeNull() + }) + }) + + describe('Quote Display', () => { + it('should show quote for COMMENT_REPLIED messages', () => { + const replyMessage = { + ...mockMessage, + data: { + subType: 'COMMENT_REPLIED', + originalComment: '太棒了!', + likerAvatar: 'https://example.com/avatar.jpg', + }, + } + const { getByTestId, getByText } = render( + + ) + expect(getByTestId('quote-container')).toBeTruthy() + expect(getByText('太棒了!')).toBeTruthy() + }) + }) + + describe('Swipe to Delete', () => { + it('should call onDelete when delete action is triggered', () => { + const { getByTestId } = render( + + ) + // Simulate swipe delete action + const deleteButton = getByTestId('delete-button') + fireEvent.press(deleteButton) + expect(mockOnDelete).toHaveBeenCalledWith(mockMessage.id) + }) + + it('should not render delete button when onDelete is not provided', () => { + const { queryByTestId } = render( + + ) + expect(queryByTestId('delete-button')).toBeNull() + }) + }) +}) + +describe('MessageCard Types', () => { + it('should accept Message type with all required fields', () => { + const message = { + id: 'msg-123', + type: 'SYSTEM' as const, + title: 'Test Title', + content: 'Test Content', + isRead: false, + createdAt: new Date(), + } + expect(message.id).toBeDefined() + expect(message.type).toBeDefined() + expect(message.title).toBeDefined() + expect(message.content).toBeDefined() + expect(message.isRead).toBeDefined() + expect(message.createdAt).toBeDefined() + }) + + it('should accept Message type with optional data field', () => { + const message = { + id: 'msg-123', + type: 'ACTIVITY' as const, + title: 'Test Title', + content: 'Test Content', + data: { + subType: 'TEMPLATE_LIKED', + likerName: '小明', + }, + isRead: false, + createdAt: new Date(), + } + expect(message.data).toBeDefined() + expect(message.data?.subType).toBe('TEMPLATE_LIKED') + }) + + it('should handle all message types', () => { + const types = ['SYSTEM', 'ACTIVITY', 'BILLING', 'MARKETING'] as const + types.forEach((type) => { + const message = { + id: 'msg-123', + type, + title: 'Test', + content: 'Test', + isRead: false, + createdAt: new Date(), + } + expect(message.type).toBe(type) + }) + }) +}) diff --git a/components/message/MessageCard.tsx b/components/message/MessageCard.tsx new file mode 100644 index 0000000..abab8a5 --- /dev/null +++ b/components/message/MessageCard.tsx @@ -0,0 +1,293 @@ +import React from 'react' +import { + View, + Text, + StyleSheet, + Pressable, + Image, +} from 'react-native' + +export interface MessageData { + subType?: string + templateId?: string + templateTitle?: string + generationId?: string + resultUrl?: string[] + webpPreviewUrl?: string + likerId?: string + likerName?: string + likerAvatar?: string + commentId?: string + credits?: number + balanceAfter?: number + originalComment?: string +} + +export interface Message { + id: string + type: 'SYSTEM' | 'ACTIVITY' | 'BILLING' | 'MARKETING' + title: string + content: string + data?: MessageData + link?: string + priority?: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW' + isRead: boolean + readAt?: string + createdAt: Date | string +} + +interface MessageCardProps { + message: Message + onPress: (message: Message) => void + onDelete?: (id: string) => void +} + +const MESSAGE_TYPE_CONFIG: Record = { + TEMPLATE_GENERATION_SUCCESS: { icon: '🎉', showPreview: true, showAvatar: false, showQuote: false }, + TEMPLATE_GENERATION_FAILED: { icon: '❌', showPreview: false, showAvatar: false, showQuote: false }, + TEMPLATE_LIKED: { icon: '👍', showPreview: false, showAvatar: true, showQuote: false }, + TEMPLATE_FAVORITED: { icon: '⭐', showPreview: false, showAvatar: true, showQuote: false }, + TEMPLATE_COMMENTED: { icon: '💬', showPreview: false, showAvatar: true, showQuote: false }, + COMMENT_REPLIED: { icon: '💬', showPreview: false, showAvatar: true, showQuote: true }, + CREDITS_DEDUCTED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false }, + CREDITS_REFUNDED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false }, + CREDITS_RECHARGED: { icon: '💰', showPreview: false, showAvatar: false, showQuote: false }, + ANNOUNCEMENT: { icon: '📢', showPreview: false, showAvatar: false, showQuote: false }, +} + +const DEFAULT_CONFIG = { icon: '📩', showPreview: false, showAvatar: false, showQuote: false } + +export const getMessageIcon = (subType?: string): string => { + if (!subType) return DEFAULT_CONFIG.icon + return MESSAGE_TYPE_CONFIG[subType]?.icon ?? DEFAULT_CONFIG.icon +} + +export const getMessageConfig = (subType?: string) => { + if (!subType) return DEFAULT_CONFIG + return MESSAGE_TYPE_CONFIG[subType] ?? DEFAULT_CONFIG +} + +export const formatRelativeTime = (date: Date | string): string => { + const now = new Date() + const targetDate = typeof date === 'string' ? new Date(date) : date + const diffMs = now.getTime() - targetDate.getTime() + const diffMinutes = Math.floor(diffMs / (1000 * 60)) + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) + + if (diffMinutes < 1) { + return '刚刚' + } + + if (diffMinutes < 60) { + return `${diffMinutes}分钟前` + } + + if (diffHours < 24) { + return `${diffHours}小时前` + } + + if (diffDays < 2) { + const hours = String(targetDate.getHours()).padStart(2, '0') + const minutes = String(targetDate.getMinutes()).padStart(2, '0') + return `昨天 ${hours}:${minutes}` + } + + const year = targetDate.getFullYear() + const month = String(targetDate.getMonth() + 1).padStart(2, '0') + const day = String(targetDate.getDate()).padStart(2, '0') + const hours = String(targetDate.getHours()).padStart(2, '0') + const minutes = String(targetDate.getMinutes()).padStart(2, '0') + return `${year}-${month}-${day} ${hours}:${minutes}` +} + +export const MessageCard: React.FC = ({ + message, + onPress, + onDelete, +}) => { + const config = getMessageConfig(message.data?.subType) + const icon = getMessageIcon(message.data?.subType) + const showAvatar = config.showAvatar && message.data?.likerAvatar + const showPreview = config.showPreview && message.data?.webpPreviewUrl + const showQuote = config.showQuote && message.data?.originalComment + + const handlePress = () => { + onPress(message) + } + + const handleDelete = () => { + if (onDelete) { + onDelete(message.id) + } + } + + return ( + + {/* Unread Indicator */} + {!message.isRead && ( + + + + )} + + + {/* Left: Icon or Avatar */} + + {showAvatar ? ( + + ) : ( + {icon} + )} + + + {/* Middle: Content */} + + {message.title} + + {message.content} + + + {/* Preview Image */} + {showPreview && ( + + )} + + {/* Quote */} + {showQuote && ( + + + {message.data?.originalComment} + + + )} + + + {formatRelativeTime(message.createdAt)} + + + + {/* Right: Delete Button (if onDelete provided) */} + {onDelete && ( + + 删除 + + )} + + + ) +} + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#16181B', + borderRadius: 16, + padding: 16, + marginBottom: 12, + marginHorizontal: 4, + position: 'relative', + }, + unreadIndicatorContainer: { + position: 'absolute', + top: -4, + right: -2, + width: 16, + height: 16, + borderWidth: 4, + borderColor: '#090A0B', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + zIndex: 1, + }, + unreadIndicator: { + width: 6, + height: 6, + borderRadius: 4, + backgroundColor: '#00FF66', + }, + contentWrapper: { + flexDirection: 'row', + alignItems: 'flex-start', + }, + leftSection: { + marginRight: 12, + }, + icon: { + fontSize: 24, + }, + avatar: { + width: 40, + height: 40, + borderRadius: 20, + }, + middleSection: { + flex: 1, + }, + title: { + color: '#FFFFFF', + fontSize: 15, + fontWeight: '600', + marginBottom: 4, + }, + content: { + color: '#ABABAB', + fontSize: 13, + marginBottom: 8, + lineHeight: 18, + }, + previewImage: { + width: 80, + height: 80, + borderRadius: 8, + marginBottom: 8, + }, + quoteContainer: { + backgroundColor: '#26292E', + borderRadius: 8, + padding: 8, + marginBottom: 8, + }, + quoteText: { + color: '#8A8A8A', + fontSize: 12, + }, + time: { + color: '#8A8A8A', + fontSize: 11, + }, + deleteButton: { + backgroundColor: '#FF3B30', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 6, + marginLeft: 8, + }, + deleteText: { + color: '#FFFFFF', + fontSize: 12, + }, +}) + +export default MessageCard diff --git a/components/message/SwipeToDelete.test.tsx b/components/message/SwipeToDelete.test.tsx new file mode 100644 index 0000000..1b7442c --- /dev/null +++ b/components/message/SwipeToDelete.test.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react-native' + +import { SwipeToDelete } from './SwipeToDelete' + +describe('SwipeToDelete Component', () => { + const mockOnDelete = jest.fn() + const mockChildren = <> + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render children', () => { + const { getByTestId } = render( + + <> + + ) + expect(getByTestId('swipe-container')).toBeTruthy() + }) + + it('should render delete button', () => { + const { getByTestId } = render( + + <> + + ) + expect(getByTestId('swipe-delete-button')).toBeTruthy() + }) + + it('should call onDelete when delete button is pressed', () => { + const { getByTestId } = render( + + <> + + ) + fireEvent.press(getByTestId('swipe-delete-button')) + expect(mockOnDelete).toHaveBeenCalledTimes(1) + }) + + it('should render with custom delete text', () => { + const { getByText } = render( + + <> + + ) + expect(getByText('移除')).toBeTruthy() + }) + + it('should render default delete text when not provided', () => { + const { getByText } = render( + + <> + + ) + expect(getByText('删除')).toBeTruthy() + }) + + it('should be disabled when disabled prop is true', () => { + const { getByTestId } = render( + + <> + + ) + fireEvent.press(getByTestId('swipe-delete-button')) + expect(mockOnDelete).not.toHaveBeenCalled() + }) + + it('should apply custom style to container', () => { + const customStyle = { marginTop: 10 } + const { getByTestId } = render( + + <> + + ) + const container = getByTestId('swipe-container') + expect(container).toBeTruthy() + }) +}) + +describe('SwipeToDelete Accessibility', () => { + const mockOnDelete = jest.fn() + + it('should have accessible delete button', () => { + const { getByTestId } = render( + + <> + + ) + const deleteButton = getByTestId('swipe-delete-button') + expect(deleteButton.props.accessibilityRole).toBe('button') + }) + + it('should have accessibility label on delete button', () => { + const { getByTestId } = render( + + <> + + ) + const deleteButton = getByTestId('swipe-delete-button') + expect(deleteButton.props.accessibilityLabel).toBe('删除') + }) +}) diff --git a/jest.setup.js b/jest.setup.js index d8ffe50..2cb1b12 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -5,29 +5,51 @@ jest.mock('react-native-css-interop', () => ({ // Mock react-native BEFORE any other imports to avoid flow type issues jest.mock('react-native', () => { - const React = require('react') + const mockReact = require('react') + + // Helper to create mock components that render as actual React elements + const mockCreateMockComponent = (mockName) => { + const MockComponent = mockReact.forwardRef(({ children, ...mockProps }, mockRef) => { + return mockReact.createElement(mockName, { ...mockProps, ref: mockRef }, children) + }) + MockComponent.displayName = mockName + return MockComponent + } + + // Create Pressable with onPress support + const MockPressable = mockReact.forwardRef(({ children, onPress, ...mockProps }, mockRef) => { + return mockReact.createElement('Pressable', { ...mockProps, ref: mockRef, onPress, onClick: onPress }, children) + }) + MockPressable.displayName = 'Pressable' + + // Create TouchableOpacity with onPress support + const MockTouchableOpacity = mockReact.forwardRef(({ children, onPress, ...mockProps }, mockRef) => { + return mockReact.createElement('TouchableOpacity', { ...mockProps, ref: mockRef, onPress, onClick: onPress }, children) + }) + MockTouchableOpacity.displayName = 'TouchableOpacity' + return { - RefreshControl: 'RefreshControl', - ScrollView: 'ScrollView', - FlatList: 'FlatList', - View: 'View', - Text: 'Text', - Image: 'Image', - Pressable: 'Pressable', - TouchableOpacity: 'TouchableOpacity', - TextInput: 'TextInput', - ActivityIndicator: 'ActivityIndicator', + RefreshControl: mockCreateMockComponent('RefreshControl'), + ScrollView: mockCreateMockComponent('ScrollView'), + FlatList: mockCreateMockComponent('FlatList'), + View: mockCreateMockComponent('View'), + Text: mockCreateMockComponent('Text'), + Image: mockCreateMockComponent('Image'), + Pressable: MockPressable, + TouchableOpacity: MockTouchableOpacity, + TextInput: mockCreateMockComponent('TextInput'), + ActivityIndicator: mockCreateMockComponent('ActivityIndicator'), Platform: { OS: 'web', - select: (obj) => obj.web || obj.default, + select: (mockObj) => mockObj.web || mockObj.default, }, Animated: { - View: 'Animated.View', - Text: 'Animated.Text', - Image: 'Animated.Image', + View: mockCreateMockComponent('Animated.View'), + Text: mockCreateMockComponent('Animated.Text'), + Image: mockCreateMockComponent('Animated.Image'), Value: class MockValue { - constructor(v) { this._value = v } - setValue(v) { this._value = v } + constructor(mockV) { this._value = mockV } + setValue(mockV) { this._value = mockV } __getValue() { return this._value } interpolate() { return this } }, @@ -38,11 +60,10 @@ jest.mock('react-native', () => { PanResponder: { create: () => ({ panHandlers: {} }), }, - ActivityIndicator: 'ActivityIndicator', - StyleSheet: { create: (styles) => styles }, + StyleSheet: { create: (mockStyles) => mockStyles }, Dimensions: { get: () => ({ width: 375, height: 812 }) }, - StatusBar: 'StatusBar', - SafeAreaView: 'SafeAreaView', + StatusBar: mockCreateMockComponent('StatusBar'), + SafeAreaView: mockCreateMockComponent('SafeAreaView'), useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), NativeModules: { DevMenu: {}, diff --git a/package.json b/package.json index 03f8a58..4f0882f 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", - "@repo/core": "1.0.3", - "@repo/sdk": "1.0.14", + "@repo/core": "1.0.4", + "@repo/sdk": "1.0.15", "@shopify/flash-list": "^2.0.0", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.5.3",