diff --git a/package.json b/package.json index 4fb2aab..dbe8ded 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,6 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", - "@open-dy/open_api_credential": "^1.0.0", - "@open-dy/open_api_sdk": "1.1.2", "axios": "^1.11.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", diff --git a/src/content-moderation/adapters/DouYinClient.ts b/src/content-moderation/adapters/DouYinClient.ts index fbac280..9fc5289 100644 --- a/src/content-moderation/adapters/DouYinClient.ts +++ b/src/content-moderation/adapters/DouYinClient.ts @@ -1,46 +1,94 @@ -import { Injectable } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import Client from '@open-dy/open_api_credential'; +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; /** +名称 描述 +HTTP URL +正式地址:https://developer.toutiao.com/api/apps/v2/token +HTTP Method POST -名称 描述 -HTTP URL https://open.douyin.com/oauth/access_token/ -HTTP Method POST +content-type: application/json -content-type: application/x-www-form-urlencoded" body: -- client_key -- client_secret +- appid +- grant_type: client_credential +- secret */ +interface TokenResponse { + err_no: number; + err_tips: string; + data: { + access_token: string; + expires_in: number; + }; +} + +interface AccessTokenResult { + accessToken: string; + expiresIn: number; +} + @Injectable() export class DouyinAuthClient { - private readonly client: Client; - constructor(private config: ConfigService) { - console.log('DouyinAuthClient constructor - creating client with:', { - clientKey: this.config.get('BYTEDANCE_APP_ID') || '', - clientSecret: this.config.get('BYTEDANCE_APP_SECRET') ? '***' : 'empty' - }); - this.client = new Client({ - clientKey: this.config.get('BYTEDANCE_APP_ID') || '', - clientSecret: this.config.get('BYTEDANCE_APP_SECRET') || '', - }); - console.log('DouyinAuthClient constructor - client created:', !!this.client); - console.log('DouyinAuthClient constructor - client.getAccessToken type:', typeof this.client.getAccessToken); + private appid: string; + private secret: string; + private grantType: string = 'client_credential'; + private accessToken: string | null = null; + private tokenExpires: number = 0; + + constructor( + private http: HttpService, + private config: ConfigService, + ) { + this.appid = this.config.get('BYTEDANCE_APP_ID') || ''; + this.secret = this.config.get('BYTEDANCE_APP_SECRET') || ''; + } + + async getAccessToken(): Promise { + // 检查是否有有效的缓存token + if (this.accessToken && Date.now() < this.tokenExpires) { + return { + accessToken: this.accessToken, + expiresIn: this.tokenExpires, + }; } - async getAccessToken() { - console.log('DouyinAuthClient.getAccessToken called'); - console.log('client:', !!this.client); - console.log('client.getAccessToken type:', typeof this.client.getAccessToken); - try { - const result = await this.client.getAccessToken(); - console.log('getAccessToken result:', result); - return result; - } catch (error) { - console.error('getAccessToken error:', error); - throw error; - } + try { + const response = await firstValueFrom( + this.http.post( + 'https://open.douyin.com/oauth/client_token', + { + client_key: this.appid, + grant_type: this.grantType, + client_secret: this.secret, + }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ), + ); + if (response.data.data) { + this.accessToken = response.data.data.access_token; + this.tokenExpires = Date.now() + response.data.data.expires_in * 1000; + return { + accessToken: this.accessToken, + expiresIn: this.tokenExpires, + }; + } else { + throw new Error( + `获取access token失败: ${response.data.err_tips} (错误码: ${response.data.err_no})`, + ); + } + } catch (error) { + console.error('getAccessToken error:', error); + throw new Error( + `DouyinAuthClient获取访问令牌失败: ${error instanceof Error ? error.message : String(error)}`, + ); } -} \ No newline at end of file + } +} diff --git a/src/content-moderation/adapters/douyin-content.adapter.ts b/src/content-moderation/adapters/douyin-content.adapter.ts index 49699c0..278b512 100644 --- a/src/content-moderation/adapters/douyin-content.adapter.ts +++ b/src/content-moderation/adapters/douyin-content.adapter.ts @@ -19,7 +19,7 @@ import { DouyinAuthClient } from './DouYinClient'; @Injectable() export class DouyinContentAdapter extends BaseContentAdapter { platform = PlatformType.BYTEDANCE; - + protected readonly douyinAuthClient: DouyinAuthClient; private douyinConfig: { clientKey: string; clientSecret: string; @@ -30,25 +30,23 @@ export class DouyinContentAdapter extends BaseContentAdapter { constructor( protected readonly httpService: HttpService, protected readonly configService: ConfigService, - protected readonly douyinAuthClient: DouyinAuthClient, + @InjectRepository(ContentAuditLogEntity) protected readonly auditLogRepository: Repository, ) { super(httpService, configService, auditLogRepository); - - // 调试:检查 douyinAuthClient 是否正确注入 - console.log('DouyinContentAdapter constructor - douyinAuthClient:', this.douyinAuthClient); - console.log('douyinAuthClient type:', typeof this.douyinAuthClient); - if (this.douyinAuthClient) { - console.log('douyinAuthClient.getAccessToken type:', typeof this.douyinAuthClient.getAccessToken); - } + this.douyinAuthClient = new DouyinAuthClient( + this.httpService, + this.configService, + ); // 抖音开放平台配置:clientKey/clientSecret 实际使用的是 APP_ID/APP_SECRET this.douyinConfig = { clientKey: this.configService.get('BYTEDANCE_APP_ID') || '', clientSecret: this.configService.get('BYTEDANCE_APP_SECRET') || '', appId: this.configService.get('BYTEDANCE_APP_ID') || '', - grantType: this.configService.get('BYTEDANCE_GRANT_TYPE') || 'client_credential', + grantType: + this.configService.get('BYTEDANCE_GRANT_TYPE') || 'client_credential', }; } @@ -160,24 +158,42 @@ export class DouyinContentAdapter extends BaseContentAdapter { } } + /** + * 将图片URL转换为Base64格式 + */ + private async imageUrlToBase64(imageUrl: string): Promise { + try { + const response = await firstValueFrom( + this.httpService.get(imageUrl, { responseType: 'arraybuffer' }), + ); + + const buffer = Buffer.from(response.data); + const base64String = buffer.toString('base64'); + + return base64String; + } catch (error) { + throw new Error(`转换图片为Base64失败: ${error.message}`); + } + } + /** * 获取访问令牌 */ private async getAccessToken(): Promise { - console.log('getAccessToken called - douyinAuthClient:', this.douyinAuthClient); - console.log('douyinAuthClient type:', typeof this.douyinAuthClient); - if (!this.douyinAuthClient) { throw new Error('DouyinAuthClient 未正确注入'); } - - if (typeof this.douyinAuthClient.getAccessToken !== 'function') { - throw new Error(`DouyinAuthClient.getAccessToken 不是函数,类型: ${typeof this.douyinAuthClient.getAccessToken}`); - } - - return this.douyinAuthClient.getAccessToken().then(res => res.accessToken) - } + if (typeof this.douyinAuthClient.getAccessToken !== 'function') { + throw new Error( + `DouyinAuthClient.getAccessToken 不是函数,类型: ${typeof this.douyinAuthClient.getAccessToken}`, + ); + } + + return this.douyinAuthClient + .getAccessToken() + .then((res) => res.accessToken); + } /** * 调用抖音审核API (使用SDK v3) @@ -192,19 +208,26 @@ export class DouyinContentAdapter extends BaseContentAdapter { ): Promise { // 确保有有效的access token const accessToken = await this.getAccessToken(); - const url = `https://open.douyin.com/api/apps/v1/censor/image/` + const url = `https://open.douyin.com/api/apps/v1/censor/image/`; + // 将图片转化为 base64格式的图片 + const imageData = await this.imageUrlToBase64(auditData.imageUrl); const response = await firstValueFrom( - this.httpService.post(url, { - app_id: this.douyinConfig.appId, - image: auditData.imageUrl - }, { - headers: { - [`access_token`]: accessToken, - [`content-type`]: `application/json` - } - }) + this.httpService.post( + url, + { + app_id: this.douyinConfig.appId, + image_data: imageData, + [`access-token`]: accessToken, + }, + { + headers: { + [`access-token`]: accessToken, + [`content-type`]: `application/json`, + }, + }, + ), ); - console.log({ response: response.data }); + const data = response.data; return this.parseDouyinSDKResponse(response.data, auditData.taskId || ''); } @@ -237,16 +260,16 @@ export class DouyinContentAdapter extends BaseContentAdapter { // SDK v3的响应格式不同,需要根据实际返回结果进行适配 const isHit = responseData.predicts?.some((predict) => predict.hit) || false; + console.log(responseData.predicts) const conclusion = isHit ? 2 : 1; // 2: 不合规, 1: 合规 - const confidence = 80; // SDK v3暂不提供具体置信度,使用默认值 return { taskId: taskId, status: AuditStatus.COMPLETED, // SDK v3直接返回结果 conclusion: this.mapDouyinConclusion(conclusion), - confidence: confidence, + confidence: 100, details: this.mapDouyinSDKDetails(responseData.predicts || []), - riskLevel: this.calculateRiskLevel(conclusion, confidence), + riskLevel: this.calculateRiskLevel(conclusion, 100), suggestion: this.getSuggestion(conclusion), platformData: responseData, timestamp: new Date(), diff --git a/src/content-moderation/content-moderation.module.ts b/src/content-moderation/content-moderation.module.ts index 716df8a..a157311 100644 --- a/src/content-moderation/content-moderation.module.ts +++ b/src/content-moderation/content-moderation.module.ts @@ -22,6 +22,7 @@ import { ContentModerationController } from './controllers/content-moderation.co // 守卫 import { ContentAuditGuard } from './guards/content-audit.guard'; +import { ConfigService } from '@nestjs/config'; @Module({ imports: [ @@ -30,9 +31,12 @@ import { ContentAuditGuard } from './guards/content-audit.guard'; timeout: 30000, maxRedirects: 3, }), - JwtModule.register({ - secret: process.env.JWT_SECRET || 'default-secret', - signOptions: { expiresIn: '1d' }, + JwtModule.registerAsync({ + useFactory: (config: ConfigService) => ({ + secret: config.get('JWT_SECRET'), + signOptions: { expiresIn: '1d' }, + }), + inject: [ConfigService], }), PlatformModule, ], @@ -51,6 +55,11 @@ import { ContentAuditGuard } from './guards/content-audit.guard'; ContentAuditGuard, ], controllers: [ContentModerationController], - exports: [UnifiedContentService, ContentAdapterFactory, ContentAuditGuard, DouyinAuthClient], + exports: [ + UnifiedContentService, + ContentAdapterFactory, + ContentAuditGuard, + DouyinAuthClient, + ], }) export class ContentModerationModule {} diff --git a/src/content-moderation/services/content-adapter.factory.ts b/src/content-moderation/services/content-adapter.factory.ts index 11e8b79..63f11b6 100644 --- a/src/content-moderation/services/content-adapter.factory.ts +++ b/src/content-moderation/services/content-adapter.factory.ts @@ -14,7 +14,6 @@ export class ContentAdapterFactory { constructor( private readonly douyinAdapter: DouyinContentAdapter, - private readonly wechatAdapter: WechatContentAdapter, private readonly enhancedWechatAdapter: EnhancedWechatContentAdapter, ) { // 注册所有可用的审核适配器 diff --git a/src/platform/guards/platform-auth.guard.ts b/src/platform/guards/platform-auth.guard.ts index e3d048d..b8eff4d 100644 --- a/src/platform/guards/platform-auth.guard.ts +++ b/src/platform/guards/platform-auth.guard.ts @@ -28,6 +28,7 @@ export class PlatformAuthGuard implements CanActivate { // 验证平台令牌有效性 const adapter = this.platformAdapterFactory.getAdapter(payload.platform); + const isValid = await adapter.validateToken(token); if (!isValid) { @@ -43,8 +44,21 @@ export class PlatformAuthGuard implements CanActivate { return true; } catch (error) { - console.log({ token }); - throw new UnauthorizedException('令牌验证失败'); + console.error('PlatformAuthGuard - token验证失败:', { + error: error.message, + name: error.name, + tokenPreview: token ? token.substring(0, 20) + '...' : 'no token', + }); + // JWT解析失败的具体错误信息 + if (error.name === 'JsonWebTokenError') { + throw new UnauthorizedException('JWT格式无效: ' + error.message); + } else if (error.name === 'TokenExpiredError') { + throw new UnauthorizedException('JWT已过期'); + } else if (error.name === 'NotBeforeError') { + throw new UnauthorizedException('JWT尚未生效'); + } + + throw new UnauthorizedException('令牌验证失败: ' + error.message); } } diff --git a/test-package.js b/test-package.js index 92ad93b..2dead5a 100644 --- a/test-package.js +++ b/test-package.js @@ -4,7 +4,7 @@ console.log('测试降级后的 @open-dy/open_api_sdk@1.0.0 包...'); try { console.log('1. 尝试导入包...'); - const dyOpenAiSdk = require('@open-dy/open_api_credential'); + const dyOpenAiSdk = require('@open-dy/open_api_sdk'); console.log('✅ 2. 包导入成功!'); console.log('3. 包内容:', typeof dyOpenAiSdk); console.log('4. 包属性:', Object.keys(dyOpenAiSdk || {}));