fix: 修复抖音内容审核适配器HTTP请求Observable问题

- 导入firstValueFrom从rxjs处理HttpService返回的Observable
- 使用await firstValueFrom包装httpService.post调用
- 修复响应数据访问,使用response.data获取实际数据
This commit is contained in:
imeepos 2025-09-08 13:39:19 +08:00
parent 6cd2e75d15
commit 39f2ecc4f5
1 changed files with 25 additions and 97 deletions

View File

@ -3,12 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Repository } from 'typeorm';
import Client, {
CensorImageRequest,
CensorImageResponse,
OauthClientTokenRequest,
} from '@open-dy/open_api_sdk';
import CredentialClient from '@open-dy/open_api_credential';
import { firstValueFrom } from 'rxjs';
import { BaseContentAdapter } from './base-content.adapter';
import { ContentAuditLogEntity } from '../entities/content-audit-log.entity';
import { PlatformType } from '../../entities/platform-user.entity';
@ -20,7 +15,7 @@ import {
RiskLevel,
AuditSuggestion,
} from '../interfaces/content-moderation.interface';
import { DouyinAuthClient } from './DouYinClient';
@Injectable()
export class DouyinContentAdapter extends BaseContentAdapter {
platform = PlatformType.BYTEDANCE;
@ -32,13 +27,10 @@ export class DouyinContentAdapter extends BaseContentAdapter {
grantType: string;
};
private douyinClient: Client;
private accessToken: string | null = null;
private tokenExpiresAt: Date | null = null;
constructor(
protected readonly httpService: HttpService,
protected readonly configService: ConfigService,
protected readonly douyinAuthClient: DouyinAuthClient,
@InjectRepository(ContentAuditLogEntity)
protected readonly auditLogRepository: Repository<ContentAuditLogEntity>,
) {
@ -51,11 +43,6 @@ export class DouyinContentAdapter extends BaseContentAdapter {
appId: this.configService.get('BYTEDANCE_APP_ID') || '',
grantType: this.configService.get('BYTEDANCE_GRANT_TYPE') || 'client_credential',
};
this.douyinClient = new Client({
clientKey: this.douyinConfig.clientKey,
clientSecret: this.douyinConfig.clientSecret,
});
}
async auditImage(auditData: ImageAuditRequest): Promise<ContentAuditResult> {
@ -170,96 +157,37 @@ export class DouyinContentAdapter extends BaseContentAdapter {
* 访
*/
private async getAccessToken(): Promise<string> {
// 检查token是否有效
if (this.accessToken && this.tokenExpiresAt && new Date() < this.tokenExpiresAt) {
return this.accessToken;
}
// 获取新的token
const params = new OauthClientTokenRequest({
clientKey: this.douyinConfig.clientKey,
clientSecret: this.douyinConfig.clientSecret,
grantType: this.douyinConfig.grantType,
});
const response = await this.douyinClient.oauthClientToken(params);
if (response.errNo && response.errNo !== 0) {
throw new Error(`获取访问令牌失败: ${response.errMsg || '未知错误'}`);
}
// 存储token和过期时间
this.accessToken = response.accessToken || '';
if (response.expiresIn) {
// 提前5分钟刷新token
const expiresIn = parseInt(response.expiresIn.toString()) - 300;
this.tokenExpiresAt = new Date(Date.now() + expiresIn * 1000);
}
if (!this.accessToken) {
throw new Error('获取访问令牌失败: 返回的accessToken为空');
}
return this.accessToken;
return this.douyinAuthClient.getAccessToken().then(res => res.accessToken)
}
/**
* token
*/
private async ensureValidToken(): Promise<string> {
if (!this.accessToken || !this.tokenExpiresAt || new Date() >= this.tokenExpiresAt) {
return this.getAccessToken();
}
return this.accessToken;
}
/**
* token获取功能 -
*/
async testTokenAcquisition(): Promise<{
success: boolean;
accessToken?: string;
expiresAt?: Date;
error?: string;
}> {
try {
const token = await this.getAccessToken();
return {
success: true,
accessToken: token.substring(0, 10) + '...', // 只显示前10位
expiresAt: this.tokenExpiresAt || undefined,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '未知错误',
};
}
}
/**
* API (使SDK v3)
* {
"app_id": "ttxxxxxxxxxxxxxxxx",
"access_token": "0d495e15563015e3f599c742384f546cac4ce63911464106af8094a0581bae7386dcff77b1b9b6fc4c16b69c9048ba2a2846c7ae8d8f07aa8b84a52bcb4d560a5b8724d99f8816600b5xxxxxxxxxx",
"image_data": "MGQ0OTVlMTU1NjMwMTVlM2Y1OTljNzQyMzg0ZjU0NmNhYzRjZTYzOTExNDY0MTA2YWY4MDk0YTA1ODFiYWU3Mzg2ZGNmZjc3YjFiOWI2ZmM0YzE2YjY5YzkwNDhiYTJhMjg0NmM3YWU4ZDhmMDdhYThiODRhNTJiY2I0ZDU2MGE1Yjg3MjRkOTlmODgxNjYwMGI1eHh4eHh4eHh4eA=="
}
*/
private async callDouyinAuditAPI(
auditData: ImageAuditRequest,
): Promise<ContentAuditResult> {
// 确保有有效的access token
const accessToken = await this.ensureValidToken();
const params = new CensorImageRequest({
accessToken: accessToken,
appId: this.douyinConfig.appId,
image: auditData.imageUrl,
imageData: auditData.imageBase64, // base64编码的图片数据可选
});
const response = await this.douyinClient.censorImage(params);
if (response.errNo && response.errNo !== 0) {
throw new Error(`抖音审核API错误: ${response.errMsg || '未知错误'}`);
}
return this.parseDouyinSDKResponse(response, auditData.taskId || '');
const accessToken = await this.getAccessToken();
const url = `https://open.douyin.com/api/apps/v1/censor/image/`
const response = await firstValueFrom(
this.httpService.post(url, {
app_id: this.douyinConfig.appId,
image: auditData.imageUrl
}, {
headers: {
[`access_token`]: accessToken,
[`content-type`]: `application/json`
}
})
);
console.log({ response: response.data });
return this.parseDouyinSDKResponse(response.data, auditData.taskId || '');
}
/**
@ -285,7 +213,7 @@ export class DouyinContentAdapter extends BaseContentAdapter {
* SDK响应
*/
private parseDouyinSDKResponse(
responseData: CensorImageResponse,
responseData: any,
taskId: string,
): ContentAuditResult {
// SDK v3的响应格式不同需要根据实际返回结果进行适配