diff --git a/base.yaml b/base.yaml index 80f46439..a17d0576 100644 --- a/base.yaml +++ b/base.yaml @@ -3,7 +3,7 @@ name: omnibox-local services: backend: ports: - - "${OBB_PORT:-8000}:${OBB_PORT:-8000}" + - '${OBB_PORT:-8000}:${OBB_PORT:-8000}' env_file: - .env environment: @@ -17,7 +17,14 @@ services: minio: condition: service_healthy healthcheck: - test: [ "CMD", "wget", "-q", "-O-", "http://127.0.0.1:${OBB_PORT:-8000}/api/v1/health" ] + test: + [ + 'CMD', + 'wget', + '-q', + '-O-', + 'http://127.0.0.1:${OBB_PORT:-8000}/api/v1/health', + ] interval: 5s timeout: 3s retries: 5 @@ -32,7 +39,18 @@ services: - POSTGRES_PASSWORD=${OBB_DB_PASSWORD:-omnibox} - POSTGRES_PORT=${OBB_DB_PORT:-5432} healthcheck: - test: [ "CMD", "pg_isready", "-q", "-d", "${OBB_DB_DATABASE:-omnibox}", "-U", "${OBB_DB_USERNAME:-omnibox}", "-p", "${OBB_DB_PORT:-5432}" ] + test: + [ + 'CMD', + 'pg_isready', + '-q', + '-d', + '${OBB_DB_DATABASE:-omnibox}', + '-U', + '${OBB_DB_USERNAME:-omnibox}', + '-p', + '${OBB_DB_PORT:-5432}', + ] interval: 5s timeout: 3s retries: 5 @@ -45,7 +63,7 @@ services: MINIO_ROOT_USER: username MINIO_ROOT_PASSWORD: password healthcheck: - test: [ "CMD", "curl", "-I", "http://127.0.0.1:9000/minio/health/live" ] + test: ['CMD', 'curl', '-I', 'http://127.0.0.1:9000/minio/health/live'] interval: 5s timeout: 3s retries: 5 @@ -79,7 +97,7 @@ services: init: image: alpine/curl:8.10.0 - command: + command: - 'http://backend:${OBB_PORT:-8000}/internal/api/v1/sign-up' - '-H' - 'Content-Type: application/json' diff --git a/package.json b/package.json index b8aed866..7d3c7ee4 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/passport-jwt": "^3.0.13", "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.3", + "@types/ws": "^8.18.1", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", @@ -107,7 +108,8 @@ "rxjs": "^7.8.2", "socket.io": "^4.8.1", "typeorm": "^0.3.27", - "typeorm-naming-strategies": "^4.1.0" + "typeorm-naming-strategies": "^4.1.0", + "ws": "^8.18.3" }, "packageManager": "pnpm@10.17.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92e71c9d..3278c737 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,7 +139,7 @@ importers: version: 6.10.1 openai: specifier: ^4.104.0 - version: 4.104.0 + version: 4.104.0(ws@8.18.3) passport: specifier: ^0.7.0 version: 0.7.0 @@ -170,6 +170,9 @@ importers: typeorm-naming-strategies: specifier: ^4.1.0 version: 4.1.0(typeorm@0.3.27(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@swc/core@1.15.0)(@types/node@22.19.0)(typescript@5.9.3))) + ws: + specifier: ^8.18.3 + version: 8.18.3 devDependencies: '@eslint/eslintrc': specifier: ^3.3.1 @@ -225,6 +228,9 @@ importers: '@types/supertest': specifier: ^6.0.3 version: 6.0.3 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 eslint: specifier: ^9.39.1 version: 9.39.1 @@ -2256,6 +2262,9 @@ packages: '@types/validator@13.15.4': resolution: {integrity: sha512-LSFfpSnJJY9wbC0LQxgvfb+ynbHftFo0tMsFOl/J4wexLnYMmDSPaj2ZyDv3TkfL1UePxPrxOWJfbiRS8mQv7A==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -5968,6 +5977,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -8916,6 +8937,10 @@ snapshots: '@types/validator@13.15.4': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.0 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.34': @@ -12075,7 +12100,7 @@ snapshots: is-wsl: 2.2.0 optional: true - openai@4.104.0: + openai@4.104.0(ws@8.18.3): dependencies: '@types/node': 18.19.130 '@types/node-fetch': 2.6.13 @@ -12084,6 +12109,8 @@ snapshots: form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.7.0 + optionalDependencies: + ws: 8.18.3 transitivePeerDependencies: - encoding @@ -13444,6 +13471,8 @@ snapshots: ws@8.17.1: {} + ws@8.18.3: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/src/auth/wechat/wechat.controller.ts b/src/auth/wechat/wechat.controller.ts index 0c34d28f..946b25dd 100644 --- a/src/auth/wechat/wechat.controller.ts +++ b/src/auth/wechat/wechat.controller.ts @@ -28,8 +28,14 @@ export class WechatController extends SocialController { @Public() @Get('auth-url') - getAuthUrl() { - return this.wechatService.authUrl(); + getAuthUrl( + @Query('isH5') isH5?: boolean, + @Query('source') source?: 'h5' | 'web', + @Query('h5_redirect') h5Redirect?: string, + ) { + // 兼容旧的isH5参数 + const finalSource = source || (isH5 ? 'h5' : 'web'); + return this.wechatService.authUrl(finalSource, h5Redirect); } @Public() diff --git a/src/auth/wechat/wechat.service.ts b/src/auth/wechat/wechat.service.ts index 7bcaa4ff..ca97c117 100644 --- a/src/auth/wechat/wechat.service.ts +++ b/src/auth/wechat/wechat.service.ts @@ -104,8 +104,22 @@ export class WechatService { }; } - async authUrl(): Promise { + async authUrl( + source: 'h5' | 'web' = 'web', + h5Redirect?: string, + ): Promise { const state = await this.socialService.generateState('weixin'); + + // 在state中保存source和h5_redirect信息 + const stateInfo = await this.socialService.getState(state); + if (stateInfo) { + stateInfo['source'] = source; + if (h5Redirect) { + stateInfo['h5_redirect'] = h5Redirect; + } + await this.socialService.updateState(state, stateInfo); + } + return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(this.redirectUri)}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`; } @@ -199,6 +213,8 @@ export class WechatService { sub: wechatUser.id, username: wechatUser.username, }), + source: stateInfo['source'] || 'web', + h5_redirect: stateInfo['h5_redirect'], }; stateInfo.userInfo = returnValue; await this.socialService.updateState(state, stateInfo); @@ -216,6 +232,8 @@ export class WechatService { sub: existingUser.id, username: existingUser.username, }), + source: stateInfo['source'] || 'web', + h5_redirect: stateInfo['h5_redirect'], }; stateInfo.userInfo = returnValue; await this.socialService.updateState(state, stateInfo); @@ -229,6 +247,8 @@ export class WechatService { sub: wechatUser.id, username: wechatUser.username, }), + source: stateInfo['source'] || 'web', + h5_redirect: stateInfo['h5_redirect'], }; stateInfo.userInfo = returnValue; await this.socialService.updateState(state, stateInfo); @@ -264,6 +284,8 @@ export class WechatService { sub: wechatUser.id, username: wechatUser.username, }), + source: stateInfo['source'] || 'web', + h5_redirect: stateInfo['h5_redirect'], }; stateInfo.userInfo = returnValue; await this.socialService.updateState(state, stateInfo); diff --git a/src/main.ts b/src/main.ts index c036108d..3a37d5dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,9 @@ async function bootstrap() { configureApp(app); const configService = app.get(ConfigService); - await app.listen(parseInt(configService.get('OBB_PORT', '8000'))); + const port = parseInt(configService.get('OBB_PORT', '8000')); + + await app.listen(port); } bootstrap().catch(console.error); diff --git a/src/namespace-resources/namespace-resources.controller.ts b/src/namespace-resources/namespace-resources.controller.ts index 5f9de3d1..63bb072b 100644 --- a/src/namespace-resources/namespace-resources.controller.ts +++ b/src/namespace-resources/namespace-resources.controller.ts @@ -163,12 +163,15 @@ export class NamespaceResourcesController { @UserId() userId: string, @Param('namespaceId') namespaceId: string, @Query('limit') limit?: string, + @Query('offset') offset?: string, ): Promise { const take = Number.isFinite(Number(limit)) ? Number(limit) : 10; + const skip = Number.isFinite(Number(offset)) ? Number(offset) : 0; return await this.namespaceResourcesService.recent( namespaceId, userId, take, + skip, ); } diff --git a/src/namespace-resources/namespace-resources.service.ts b/src/namespace-resources/namespace-resources.service.ts index 6ff9663d..61c9da0a 100644 --- a/src/namespace-resources/namespace-resources.service.ts +++ b/src/namespace-resources/namespace-resources.service.ts @@ -443,13 +443,25 @@ export class NamespaceResourcesService { namespaceId: string, userId: string, limit: number = 10, + offset: number = 0, ): Promise { const allVisible = await this.getUserVisibleResources(userId, namespaceId); const sorted = allVisible .filter((r) => r.parentId !== null) + .filter((r) => r.resourceType !== ResourceType.FOLDER) .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); const take = Math.max(1, Math.min(100, limit)); - return sorted.slice(0, take); + const skip = Math.max(0, offset); + const resources = sorted.slice(skip, skip + take); + const firstAttachments = + await this.resourceAttachmentsService.getFirstAttachments( + namespaceId, + resources.map((r) => r.id), + ); + for (const resource of resources) { + resource.firstAttachment = firstAttachments.get(resource.id); + } + return resources; } // Alias for clarity and reuse across modules diff --git a/src/resource-attachments/resource-attachments.service.ts b/src/resource-attachments/resource-attachments.service.ts index a7bc9849..86360323 100644 --- a/src/resource-attachments/resource-attachments.service.ts +++ b/src/resource-attachments/resource-attachments.service.ts @@ -2,7 +2,7 @@ import { Injectable, HttpStatus } from '@nestjs/common'; import { AppException } from 'omniboxd/common/exceptions/app.exception'; import { I18nService } from 'nestjs-i18n'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, EntityManager } from 'typeorm'; +import { Repository, EntityManager, In } from 'typeorm'; import { ResourceAttachment } from 'omniboxd/attachments/entities/resource-attachment.entity'; @Injectable() @@ -130,4 +130,21 @@ export class ResourceAttachmentsService { }, }); } + + async getFirstAttachments( + namespaceId: string, + resourceIds: string[], + ): Promise> { + const attachments = await this.resourceAttachmentRepository.find({ + where: { namespaceId, resourceId: In(resourceIds) }, + order: { id: 'ASC' }, + }); + const firstAttachments = new Map(); + for (const attachment of attachments) { + if (!firstAttachments.has(attachment.resourceId)) { + firstAttachments.set(attachment.resourceId, attachment.attachmentId); + } + } + return firstAttachments; + } } diff --git a/src/resources/dto/resource-meta.dto.ts b/src/resources/dto/resource-meta.dto.ts index f2b69fb2..601e599f 100644 --- a/src/resources/dto/resource-meta.dto.ts +++ b/src/resources/dto/resource-meta.dto.ts @@ -29,8 +29,13 @@ export class ResourceMetaDto { @Expose({ name: 'attrs' }) attrs: Record; + @Expose({ name: 'content' }) + content: string; fileId: string | null; + @Expose({ name: 'first_attachment' }) + firstAttachment?: string; + static fromEntity(resource: Resource) { const dto = new ResourceMetaDto(); dto.id = resource.id; @@ -40,6 +45,12 @@ export class ResourceMetaDto { dto.globalPermission = resource.globalPermission; dto.createdAt = resource.createdAt; dto.updatedAt = resource.updatedAt; + // Remove markdown images ![alt](url) and HTML images + const contentWithoutImages = resource.content + .replace(/!\[.*?\]\(.*?\)/g, '') + .replace(/]*>/gi, '') + .trim(); + dto.content = contentWithoutImages.slice(0, 100); dto.attrs = { ...resource.attrs }; delete dto.attrs.transcript; delete dto.attrs.video_info; diff --git a/src/resources/resources.service.ts b/src/resources/resources.service.ts index 3c5611a5..37860911 100644 --- a/src/resources/resources.service.ts +++ b/src/resources/resources.service.ts @@ -65,6 +65,7 @@ export class ResourcesService { 'fileId', 'createdAt', 'updatedAt', + 'content', ], where: { namespaceId, id: resourceId }, }); @@ -145,6 +146,7 @@ export class ResourcesService { 'createdAt', 'updatedAt', 'attrs', + 'content', ], where: { namespaceId, @@ -191,6 +193,7 @@ export class ResourcesService { 'createdAt', 'updatedAt', 'attrs', + 'content', ], where: { namespaceId, diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 73f3a2b0..378f2604 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -37,6 +37,11 @@ export class UserController { return req.user; } + @Get('wx/profile') + async wxProfile(@UserId() userId: string) { + return await this.userService.find(userId); + } + @Get(':id') async get(@Param('id') id: string) { return await this.userService.find(id); diff --git a/src/websocket/wizard.gateway.ts b/src/websocket/wizard.gateway.ts index 023834f9..5909d6b0 100644 --- a/src/websocket/wizard.gateway.ts +++ b/src/websocket/wizard.gateway.ts @@ -47,11 +47,15 @@ export class WizardGateway implements OnGatewayConnection, OnGatewayDisconnect { private readonly i18n: I18nService, ) {} - // eslint-disable-next-line @typescript-eslint/no-unused-vars - handleConnection(client: Socket) {} + handleConnection(client: Socket) { + this.logger.log(`Socket.IO client connected: ${client.id}`); + this.logger.log(`Namespace: ${client.nsp.name}`); + this.logger.log(`Transport: ${client.conn.transport.name}`); + } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - handleDisconnect(client: Socket) {} + handleDisconnect(client: Socket) { + this.logger.log(`Socket.IO client disconnected: ${client.id}`); + } @UseGuards(WsJwtGuard) @SubscribeMessage('ask')