diff --git a/package.json b/package.json index 19db4de..9b7c6c8 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "rss-parser": "^3.13.0", "rxjs": "^7.8.1", "telegraf": "^4.16.3", - "twitter-api-v2": "^1.29.0" + "twitter-api-v2": "^1.29.0", + "uuid": "^14.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8529fcc..12cefbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,9 @@ importers: twitter-api-v2: specifier: ^1.29.0 version: 1.29.0 + uuid: + specifier: ^14.0.0 + version: 14.0.0 devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -4493,6 +4496,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -9569,6 +9576,8 @@ snapshots: uuid@11.1.0: {} + uuid@14.0.0: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: diff --git a/src/app.controller.ts b/src/app.controller.ts index 07d03bd..fbd8e31 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -205,9 +205,13 @@ export class AppController { } @Get('/tw/read') - async getTwitterMe(@Query('url') url: string, @Query('via') via: string = 'api') { + async getTwitterMe( + @Query('url') url: string, + @Query('via') via: string = 'api', + @Query('nocache') nocache: number = 0, + ) { if (via === 'browser') { - return this.xReaderService.readXPostViaBrowserV2(url) + return this.xReaderService.readXPostViaBrowserV2(url, 0, !nocache); } return this.xReaderService.readXPostViaApi(url) } diff --git a/src/common/utils/token-calculator.ts b/src/common/utils/token-calculator.ts index 1ce8867..90241ce 100644 --- a/src/common/utils/token-calculator.ts +++ b/src/common/utils/token-calculator.ts @@ -20,7 +20,7 @@ const TOKENS_PER_CHAR: Record = { * Target character length theo platform + buffer để AI có không gian "thở". */ const PLATFORM_TARGET_CHARS: Record = { - [Platform.X]: { min: 180, max: 280, buffer: 1.3 }, + [Platform.X]: { min: 50, max: 220, buffer: 1.3 }, [Platform.FACEBOOK]: { min: 400, max: 1200, buffer: 1.5 }, }; @@ -47,7 +47,7 @@ export function calculateLengthBudget( // Safe minimums để đảm bảo không bị cắt const SAFE_MIN: Record = { - [Platform.X]: 300, // X post không bao giờ < 300 tokens + [Platform.X]: 200, // X post không bao giờ < 300 tokens [Platform.FACEBOOK]: 800, // FB không bao giờ < 800 tokens }; return { diff --git a/src/modules/content-writer/comment-writer.processor.ts b/src/modules/content-writer/comment-writer.processor.ts index f00312b..90d58c2 100644 --- a/src/modules/content-writer/comment-writer.processor.ts +++ b/src/modules/content-writer/comment-writer.processor.ts @@ -9,8 +9,10 @@ import {Context, Telegraf} from "telegraf"; import {Logger} from "@nestjs/common"; import {QuoteWriterService} from "./services/quote-writer.service"; import {GenerateQuoteDto} from "./dto/generate-quote.dto"; -import {TextUtil} from "../../common/utils/text.util"; import {isEmpty} from "lodash"; +import {SqsPostService} from "../sqs-module/sqs.post.service"; +import {_getTweetIdFromUrl, rand} from "../../shared/helper"; +import {XCacheService} from "../x-cache/x-cache.service"; @Processor('comment_writer_queue') @@ -22,8 +24,8 @@ export class CommentWriterProcessor extends WorkerHost { private readonly commentWriterService: CommentWriterService, private readonly xreader: XReaderService, @InjectBot() private readonly bot: Telegraf, - //@InjectQueue('comment_writer_completed_queue') private readonly aiCommentWriteCompletedQueue: Queue, - + private readonly sqsPostService: SqsPostService, + private readonly xCache: XCacheService, ) { super(); } @@ -47,11 +49,71 @@ export class CommentWriterProcessor extends WorkerHost { const topic = summary || title; let pgPostCreateDto!: PostCreateInput; - console.log(`CommentWriterProcessor_processing_${job.name}`); + this.logger.log(`CommentWriterProcessor_processing_${job.name}`); + this.logger.debug(job.data) switch (job.name) { - case 'generate_comment_twitter': { + case 'GENERATE_AUTO_COMMENT_TWITTER': { + let {tweetUrl, tweetText, angle, language, tone, x_username} = job.data; + if (isEmpty(tweetUrl) && isEmpty(tweetText)) { + this.logger.error(`Job thiếu payload`); + await this.bot.telegram.sendMessage(telegramChatId, `Job thiếu payload`) + return {}; + } - const xpost = await this.xreader.readXPost(url); + //Kiểm tra đã comment chưa ? + const tweetId = _getTweetIdFromUrl(tweetUrl); + const hadCmt = await this.xCache.hasComment(tweetId, x_username); + if (hadCmt) { + this.logger.error(`Đã comment rồi`); + await this.bot.telegram.sendMessage(telegramChatId, `Đã comment rồi`) + return {} + } + + + if (isEmpty(tweetText)) { + const xpost = await this.xreader.readXPost(tweetUrl); + tweetText = xpost.text || ''; + if (!language) { + language = xpost.lang || 'en'; + } + } + if (isEmpty(tweetText)) { + this.logger.error(`Tweet text = Empty`); + await this.bot.telegram.sendMessage(telegramChatId, `Tweet text = Empty`) + return {}; + } + const dto = { + originalPost: tweetText, + angle, + language: language, + tone, + }; + this.logger.debug(`==> Bắt đầu viết auto comment: ${tweetUrl}`, dto); + + const res = await this.commentWriterService.generateComment(dto, false); + this.logger.debug(`==> Đã viết auto comment xong:`, dto, res); + + await this.sqsPostService.sendMessage(x_username, { + // ...res, + telegramChatId, + content: res.comment, + tweetUrl, + job_type: 'X_POSTER_REPLY', + type: 'X_POSTER_REPLY' + }, + rand(42, 84) + ); + await this.xCache.setAlreadyComment(tweetId, x_username); + + this.logger.log(`Đã gửi comment auto sang queue thành công.`); + await this.bot.telegram.sendMessage(telegramChatId, ` + Đã gửi comment auto sang queue thành công. M:${res.model}- T:${dto?.tone} A:${dto?.angle} `) + + break; + } + + case 'generate_comment_twitter': { + const xpost: any = await this.xreader.readXPost(url); const dto: GenerateCommentDto = { originalPost: xpost.text, @@ -81,7 +143,7 @@ export class CommentWriterProcessor extends WorkerHost { // }, {attempts: 1, backoff: 5000, removeOnComplete: true,}); await this.bot.telegram.sendMessage(telegramChatId, ` - Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}`); + Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}\ntransVi: ${aiWriterResult.commentTransVi}`); // const _url = url.indexOf('?s=20') > -1 ? url : `${url}?s=20`; // console.log({_url}); @@ -144,7 +206,7 @@ export class CommentWriterProcessor extends WorkerHost { const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; await this.bot.telegram.sendMessage(telegramChatId || adminChatId, ` - Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model} } + Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model}\ndich:${aiWriterResult.commentTransVi} `); await this.bot.telegram.sendMessage(telegramChatId || adminChatId, @@ -186,7 +248,7 @@ export class CommentWriterProcessor extends WorkerHost { } case 'generate_quote_twitter': { this.logger.debug('===>generate_quote_twitter:', url); - const xpost = await this.xreader.readXPost(url, xreaderMode, telegramChatId); + const xpost: any = await this.xreader.readXPost(url, xreaderMode, telegramChatId); const originalAuthor = `${xpost.author} ${xpost.handle}`; const dto: GenerateQuoteDto = { @@ -284,9 +346,7 @@ export class CommentWriterProcessor extends WorkerHost { this.logger.log({aiWriterResult}); const sendId = telegramChatId || process.env.TELEGRAM_ADMIN_ID || 0; - await this.bot.telegram.sendMessage(sendId, ` - Đã viết quote xong ...\nmodel: ${aiWriterResult.model} - type: ${aiWriterResult.quoteType} - `); + // let finalQuote = aiWriterResult.quote; @@ -294,13 +354,18 @@ export class CommentWriterProcessor extends WorkerHost { finalQuote += `\n\nQuote:"${dto.originalPost}\n\n${dto.originalAuthor}` } //xoá url trong bài vì tốn 0,2$ cho bài có url - const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote) + // const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote) const X_USERS = process.env.TWITTER_USERNAMES!.split(','); - + await this.bot.telegram.sendMessage(sendId, ` + Đã viết quote xong ... + model: ${aiWriterResult.model} + type: ${aiWriterResult.quoteType} + dịch: ${aiWriterResult.quoteTransVi} + `); await this.bot.telegram.sendMessage( sendId, - finalQuoteCleanUrl, + finalQuote, { // parse_mode: 'Markdown', reply_markup: { diff --git a/src/modules/content-writer/content-writer.controller.ts b/src/modules/content-writer/content-writer.controller.ts index 7570665..2b46573 100644 --- a/src/modules/content-writer/content-writer.controller.ts +++ b/src/modules/content-writer/content-writer.controller.ts @@ -5,12 +5,16 @@ import {CommentWriterService} from './services/comment-writer.service'; import {GenerateContentDto} from './dto/generate-content.dto'; import {GenerateCommentDto} from './dto/generate-comment.dto'; import {IAIGrokProvider} from "./interfaces/ai-provider.interface"; +import {AiTranslatorDto} from "./dto/ai-translator.dto"; +import {TranslatorService} from "./services/translator.service"; +import {GenerateCommentAdapterExtDto} from "./dto/generate-comment-adapter-ext.dto"; @Controller('content-writer') export class ContentWriterController { constructor( private readonly writerService: ContentWriterService, private readonly commentService: CommentWriterService, + private readonly translatorService: TranslatorService, ) { } @@ -29,12 +33,22 @@ export class ContentWriterController { } @Post('comment') - generateComment(@Body() dto: GenerateCommentDto) { - return this.commentService.generateComment(dto); + generateComment(@Body() dto: GenerateCommentAdapterExtDto) { + const dto0: GenerateCommentDto = { + ...dto, + language: dto.lang, + originalPost: dto.tweet_text + } + return this.commentService.generateComment(dto0); } @Post('comment/variants') generateCommentVariants(@Body() dto: GenerateCommentDto) { // return this.commentService.generateVariants(dto, 3); } + + @Post('ai-translate') + aiTranslator(@Body() dto: AiTranslatorDto) { + return this.translatorService.translator(dto); + } } diff --git a/src/modules/content-writer/content-writer.module.ts b/src/modules/content-writer/content-writer.module.ts index d3ee94c..2e4167f 100644 --- a/src/modules/content-writer/content-writer.module.ts +++ b/src/modules/content-writer/content-writer.module.ts @@ -1,9 +1,8 @@ -import {Global, Module} from '@nestjs/common'; +import {Module} from '@nestjs/common'; import {AIService} from "../../shared/ai.service"; import {ContentWriterProcessor} from "./content.writer.processor"; import {PgPostService} from "../../shared/pg.post.service"; import {BullModule} from "@nestjs/bullmq"; -import {PublishPageService} from "../social-api/publish.page.service"; import {SocialModule} from "../social-api/social.module"; import {ContentWriterController} from "./content-writer.controller"; import {AIProviderFactory} from "./providers/ai-provider.factory"; @@ -22,6 +21,8 @@ import {LengthStrategyService} from "./services/length-strategy.service"; import {QuoteWriterService} from "./services/quote-writer.service"; import {SqsPostService} from "../sqs-module/sqs.post.service"; import {ContentSafetyService} from "./services/content-safety.service"; +import {TranslatorService} from "./services/translator.service"; +import {GoogleProvider} from "./providers/google.provider"; @Module({ imports: [ @@ -43,6 +44,7 @@ import {ContentSafetyService} from "./services/content-safety.service"; OpenAIProvider, DeepSeekProvider, GrokProvider, + GoogleProvider, AIProviderFactory, ProviderRouterService, CommentWriterProcessor, @@ -51,8 +53,9 @@ import {ContentSafetyService} from "./services/content-safety.service"; QuoteWriterService, SqsPostService, ContentSafetyService, + TranslatorService, ], - exports: [GrokProvider, ContentWriterService], + exports: [GrokProvider, ContentWriterService, TranslatorService], }) export class ContentWriterModule { diff --git a/src/modules/content-writer/content-writer.service.ts b/src/modules/content-writer/content-writer.service.ts index 0a13d00..6f79971 100644 --- a/src/modules/content-writer/content-writer.service.ts +++ b/src/modules/content-writer/content-writer.service.ts @@ -14,6 +14,8 @@ import {AccountTier} from "./enum/account-tier.enum"; import {ConfigService} from "@nestjs/config"; import {LengthStrategyService} from "./services/length-strategy.service"; import {calculateTokenBudget} from "../../common/utils/token-calculator"; +import {AiTranslatorDto} from "./dto/ai-translator.dto"; +import {buildReviewerPrompt, LANGUAGE_NAMES} from "./prompts/templates"; @Injectable() export class ContentWriterService { @@ -33,6 +35,10 @@ export class ContentWriterService { async getGrokAI() { return this.factory.get('grok'); } + async getGptAI() { + return this.factory.get('openai'); + } + async generate( dto: GenerateContentDto, diff --git a/src/modules/content-writer/content.writer.processor.ts b/src/modules/content-writer/content.writer.processor.ts index a288b05..2543597 100644 --- a/src/modules/content-writer/content.writer.processor.ts +++ b/src/modules/content-writer/content.writer.processor.ts @@ -14,14 +14,20 @@ import {ContentStyle} from "./enum/style.enum"; import {ContentTone} from "./enum/tone.enum"; import {XStrategy} from "../social-api/x-router.service"; import {SqsPostService} from "../sqs-module/sqs.post.service"; +import {TranslatorService} from "./services/translator.service"; +import {CommentWriterService} from "./services/comment-writer.service"; +import {XReaderService} from "../x-reader/x-reader.service"; +import {Logger} from "@nestjs/common"; @Processor('content_writer_queue') export class ContentWriterProcessor extends WorkerHost { + private readonly logger = new Logger("ContentWriterProcessor"); constructor( private aiService: AIService, private readonly writerService: ContentWriterService, private readonly styleDetectorService: StyleDetectorService, private readonly pgPostService: PgPostService, + private readonly translatorService: TranslatorService, @InjectBot() private readonly bot: Telegraf, // private readonly managerService: ManagerService, @InjectQueue('content_writer_completed_queue') private readonly fbContentCompletedQueue: Queue, @@ -44,7 +50,8 @@ export class ContentWriterProcessor extends WorkerHost { enableReview, language, tone, - postLength + postLength, + style, } const aiWriterResult = await this.writerService.generate(dto, false, 'openai', 'deepseek'); // console.log({aiWriterResult}); @@ -79,7 +86,6 @@ export class ContentWriterProcessor extends WorkerHost { } break; } - case 'generate_post_telegram': { isAutoPublish = true; const topicLen = topic.length; @@ -161,11 +167,24 @@ export class ContentWriterProcessor extends WorkerHost { // let finalContent = aiWriterResult.content; const post = await this.pgPostService.createPost(pgPostCreateDto); if (!isAutoPublish) { + let contentTransVi = ''; + let contentTransModel = ''; + if (language !== 'vi') { + const resTranslate = await this.translatorService.translator({ + text: pgPostCreateDto.content, + target_lang: "vi", + target_model: 'google' + }) + contentTransVi = resTranslate.content; + contentTransModel = resTranslate.model; + } await this.fbContentCompletedQueue.add('generate_post_completed', { id: post.id, name: 'generate_post_completed', needConfirm: 1, content: pgPostCreateDto.content, + contentTransVi, + contentTransModel, autoPublish: false, telegramChatId, xSubmitProvider: post.id % 2 === 0 ? XStrategy.BROWSER_ONLY : XStrategy.API_ONLY, //cứ 3post api, có 1 post browser @@ -180,7 +199,7 @@ export class ContentWriterProcessor extends WorkerHost { content: pgPostCreateDto.content, autoPublish, telegramChatId, - publishTo: ['x', 'fb'], + publishTo: [ 'fb'], xSubmitProvider: post.id % 3 === 0 ? XStrategy.API_FIRST : XStrategy.BROWSER_ONLY, //cứ 3post api, có 1 post browser }) } diff --git a/src/modules/content-writer/dto/ai-translator.dto.ts b/src/modules/content-writer/dto/ai-translator.dto.ts new file mode 100644 index 0000000..5e8afae --- /dev/null +++ b/src/modules/content-writer/dto/ai-translator.dto.ts @@ -0,0 +1,18 @@ +// dto/generate-content.dto.ts +import {IsOptional, IsString, MaxLength} from 'class-validator'; +import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface"; +import * as aiProviderFactory from "../providers/ai-provider.factory"; + +export class AiTranslatorDto { + @IsString() + @MaxLength(5000) + text: string; // chủ đề / input thô từ user + + @IsString() + target_lang: languagePromptInterface.Language; // 'vi' | 'en' ... default 'en' + + @IsString() + @IsOptional() + target_model?: aiProviderFactory.ProviderName; + +} diff --git a/src/modules/content-writer/dto/generate-comment-adapter-ext.dto.ts b/src/modules/content-writer/dto/generate-comment-adapter-ext.dto.ts new file mode 100644 index 0000000..8049618 --- /dev/null +++ b/src/modules/content-writer/dto/generate-comment-adapter-ext.dto.ts @@ -0,0 +1,28 @@ +// dto/generate-comment.dto.ts +import { IsEnum, IsString, IsOptional, MaxLength } from 'class-validator'; +import { ContentTone } from '../enum/tone.enum'; +import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface"; +import {AngleEnum} from "../enum/angle.enum"; +import {Transform} from "class-transformer"; + +export class GenerateCommentAdapterExtDto { + @IsString() + @MaxLength(5000) + tweet_text: string; // nội dung bài X gốc + + @IsOptional() + @IsString() + @Transform(({ value }) => value?.trim().toLowerCase()) + angle?: AngleEnum; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny" + + @IsString() + lang: languagePromptInterface.Language; + + @IsOptional() + @Transform(({ value }) => value?.trim().toLowerCase()) + tone?: ContentTone; + + @IsOptional() + @IsString() + persona?: string; // "crypto trader", "news analyst"... +} diff --git a/src/modules/content-writer/dto/generate-comment.dto.ts b/src/modules/content-writer/dto/generate-comment.dto.ts index 26709ae..717186f 100644 --- a/src/modules/content-writer/dto/generate-comment.dto.ts +++ b/src/modules/content-writer/dto/generate-comment.dto.ts @@ -6,7 +6,7 @@ import {AngleEnum} from "../enum/angle.enum"; export class GenerateCommentDto { @IsString() - @MaxLength(3000) + @MaxLength(5000) originalPost: string; // nội dung bài X gốc @IsOptional() diff --git a/src/modules/content-writer/enum/angle.enum.ts b/src/modules/content-writer/enum/angle.enum.ts index cb5cafa..85d0dab 100644 --- a/src/modules/content-writer/enum/angle.enum.ts +++ b/src/modules/content-writer/enum/angle.enum.ts @@ -1,6 +1,7 @@ import {ContentTone} from "./tone.enum"; export enum AngleEnum { + NATURAL = 'natural', AGREE = 'agree', CHALLENGE = 'challenge', ADD_INFO = 'add_info', @@ -33,6 +34,7 @@ export function isEMPATHYToneAngle(angle: AngleEnum): boolean { } export const ANGLE_HINTS: Record = { + [AngleEnum.NATURAL]: 'natural reaction.', [AngleEnum.AGREE]: 'I agree and would like to add a small point to support my argument.', [AngleEnum.CHALLENGE]: 'Disagree or add further nuance', [AngleEnum.ADD_INFO]: 'additional useful related information', @@ -61,6 +63,7 @@ export const get_ANGLE_HINTS_TELEGRAM_BUTTON=(tone:ContentTone)=>{ return buttons; } export const DEFAULT_ANGLE_HINTS_TELEGRAM_BUTTON: Partial> = { + [AngleEnum.NATURAL]: {text: 'phản ứng tự nhiên'}, [AngleEnum.AGREE]: {text: 'Đồng ý'}, [AngleEnum.CHALLENGE]: {text: 'Không đồng ý'}, [AngleEnum.ADD_INFO]: {text: 'thêm thông tin liên quan hữu ích'}, diff --git a/src/modules/content-writer/prompts/comment.templates.ts b/src/modules/content-writer/prompts/comment.templates.ts index 803d907..147e2cd 100644 --- a/src/modules/content-writer/prompts/comment.templates.ts +++ b/src/modules/content-writer/prompts/comment.templates.ts @@ -70,7 +70,8 @@ export function buildCommentPrompt(params: { : ''; const budget = calculateLengthBudget(Platform.X, params.language); - const angleInstruction = getAngleHint(params.angle!, params.language, '- ANGLE:'); + const angle = params.angle ? params.angle : AngleEnum.NATURAL; + const angleInstruction = getAngleHint(angle, params.language, '- ANGLE:'); console.debug({toneInstruction, angleInstruction}); @@ -92,5 +93,8 @@ export function buildCommentPrompt(params: { `- Output ONLY the reply text.`, ].filter(Boolean).join('\n'); + params.angle = angle; + params.tone = tone; + return {system: system, user}; } diff --git a/src/modules/content-writer/prompts/empathy-angles.ts b/src/modules/content-writer/prompts/empathy-angles.ts index a042d28..78c963e 100644 --- a/src/modules/content-writer/prompts/empathy-angles.ts +++ b/src/modules/content-writer/prompts/empathy-angles.ts @@ -294,11 +294,11 @@ export function getEmpathyAngleInstruction( } export function getAngleHint(angle: AngleEnum, language: Language, prefix = '- '): string { - if (!angle) return ''; + if (!angle) return prefix + ANGLE_HINTS[AngleEnum.NATURAL]; if (isEMPATHYToneAngle(angle)) { return getEmpathyAngleInstruction(angle, language) } - const agHint = ANGLE_HINTS[language]; + const agHint = ANGLE_HINTS[angle]; return agHint ? `${prefix} ${agHint}` : ``; diff --git a/src/modules/content-writer/providers/ai-provider.factory.ts b/src/modules/content-writer/providers/ai-provider.factory.ts index 8ab3c78..4116486 100644 --- a/src/modules/content-writer/providers/ai-provider.factory.ts +++ b/src/modules/content-writer/providers/ai-provider.factory.ts @@ -4,8 +4,9 @@ import {OpenAIProvider} from './openai.provider'; import {DeepSeekProvider} from './deepseek.provider'; import {IAIProvider} from '../interfaces/ai-provider.interface'; import {GrokProvider} from "./grok.provider"; +import {GoogleProvider} from "./google.provider"; -export type ProviderName = 'openai' | 'deepseek' | 'grok'; +export type ProviderName = 'openai' | 'deepseek' | 'grok' | 'google'; @Injectable() export class AIProviderFactory { @@ -13,6 +14,7 @@ export class AIProviderFactory { private readonly openai: OpenAIProvider, private readonly deepseek: DeepSeekProvider, private readonly grok: GrokProvider, + private readonly google: GoogleProvider, ) { } @@ -24,6 +26,8 @@ export class AIProviderFactory { return this.deepseek; case 'grok': return this.grok; + case 'google': + return this.google; default: throw new Error(`Unknown AI provider: ${name}`); } diff --git a/src/modules/content-writer/providers/deepseek.provider.ts b/src/modules/content-writer/providers/deepseek.provider.ts index f090a38..49c2342 100644 --- a/src/modules/content-writer/providers/deepseek.provider.ts +++ b/src/modules/content-writer/providers/deepseek.provider.ts @@ -20,6 +20,7 @@ export class DeepSeekProvider implements IAIProvider { baseURL: 'https://api.deepseek.com/v1', }); this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-chat'); + // this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-v4-pro'); } async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise { diff --git a/src/modules/content-writer/providers/google.provider.ts b/src/modules/content-writer/providers/google.provider.ts new file mode 100644 index 0000000..45bacae --- /dev/null +++ b/src/modules/content-writer/providers/google.provider.ts @@ -0,0 +1,57 @@ +// providers/grok.provider.ts +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; +import { IAIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../interfaces/ai-provider.interface'; +import {ChatCompletionTool} from "openai/resources/chat/completions/completions"; + + +@Injectable() +export class GoogleProvider implements IAIProvider { + readonly name = 'google'; + private readonly logger = new Logger(GoogleProvider.name); + private client: OpenAI; + private defaultModel: string; + + constructor(private config: ConfigService) { + // this.client = new OpenAI({ + // apiKey: this.config.get('XAI_API_KEY'), + // baseURL: 'https://api.x.ai/v1', + // }); + this.client = new OpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: this.config.get('OPEN_ROUTER_API_KEY') + }); + // grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn + // this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini'); + // this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast'); + // this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash'); // rẻ + this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash-lite'); // rẻ +// rẻ + + } + + async getModelName() { + return this.defaultModel; + } + + async complete( + messages: AIMessage[], + options: AICompletionOptions = {} + + ): Promise { + const model = options.model ?? this.defaultModel; + const res = await this.client.chat.completions.create({ + model, + messages, + temperature: options.temperature ?? 0.8, + max_tokens: options.maxTokens ?? 400, + }); + + return { + content: res.choices[0]?.message?.content?.trim() ?? '', + tokensUsed: res.usage?.total_tokens ?? 0, + model, + }; + } +} diff --git a/src/modules/content-writer/providers/grok.provider.ts b/src/modules/content-writer/providers/grok.provider.ts index 0e9b8b0..631bba9 100644 --- a/src/modules/content-writer/providers/grok.provider.ts +++ b/src/modules/content-writer/providers/grok.provider.ts @@ -18,17 +18,12 @@ export class GrokProvider implements IAIProvider { private defaultModel: string; constructor(private config: ConfigService) { - // this.client = new OpenAI({ - // apiKey: this.config.get('XAI_API_KEY'), - // baseURL: 'https://api.x.ai/v1', - // }); this.client = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: this.config.get('OPEN_ROUTER_API_KEY') }); - // grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn - // this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini'); - this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast'); + this.defaultModel = this.config.get('OPENAI_MODEL', 'x-ai/grok-4.3'); + } async getModelName() { diff --git a/src/modules/content-writer/providers/openai.provider.ts b/src/modules/content-writer/providers/openai.provider.ts index 50e1595..1e74e17 100644 --- a/src/modules/content-writer/providers/openai.provider.ts +++ b/src/modules/content-writer/providers/openai.provider.ts @@ -41,7 +41,7 @@ export class OpenAIProvider implements IAIProvider { } async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise { - console.log(`OpenAIProvider_complete`); + console.log(`OpenAIProvider_complete_begin...`); const model = options.model ?? this.defaultModel; try { diff --git a/src/modules/content-writer/services/comment-writer.service.ts b/src/modules/content-writer/services/comment-writer.service.ts index e22a488..e4fb52d 100644 --- a/src/modules/content-writer/services/comment-writer.service.ts +++ b/src/modules/content-writer/services/comment-writer.service.ts @@ -4,6 +4,8 @@ import {AIProviderFactory} from '../providers/ai-provider.factory'; import {GenerateCommentDto} from '../dto/generate-comment.dto'; import {buildCommentPrompt} from '../prompts/comment.templates'; import {ProviderRouterService} from "./provider-router.service"; +import {TranslatorService} from "./translator.service"; +import {getUuid4} from "../../../shared/helper"; @Injectable() export class CommentWriterService { @@ -12,10 +14,27 @@ export class CommentWriterService { constructor( private factory: AIProviderFactory, private router: ProviderRouterService, + private translatorService: TranslatorService ) { } - async generateComment(dto: GenerateCommentDto) { + // async generateComment() {} + + async generateComment( + dto: GenerateCommentDto, + allowTranslateToVi = true + ): Promise<{ + commentTransVi?: string; + commentTransModel?: string; + comment: string; + tokensUsed: number; + tokenTranslatorUsed?: number; + model: string; + language: "en" | "vi" | "ja" | "ko" | "cn", + input?: GenerateCommentDto, + uid:string, + }> { + // const uid = getUuid4(); const decision = this.router.route({ language: dto.language, contentType: 'comment', @@ -29,13 +48,7 @@ export class CommentWriterService { const provider = this.factory.get(decision.writer); this.logger.log(`==> Comment routing: ${decision.reason} ==>`); // console.log({dto}) - const {system, user} = buildCommentPrompt({ - originalPost: dto.originalPost, - angle: dto.angle, - language: dto.language, - persona: dto.persona, - tone: dto.tone, - }); + const {system, user} = buildCommentPrompt(dto); this.logger.debug({dto, system, user}) @@ -54,12 +67,35 @@ export class CommentWriterService { // Clean output: bỏ quotes nếu AI lỡ wrap const cleaned = res.content.replace(/^["""']|["""']$/g, '').trim(); + this.logger.debug({ + post: dto.originalPost, + output: cleaned + }); + //transla if language != vi + let commentTransVi = ''; + let commentTransModel = ''; + let tokenTranslatorUsed = 0; + if (allowTranslateToVi && dto.language !== 'vi') { + const resT = await this.translatorService.translator({ + text: cleaned, + target_lang: 'vi', + target_model: 'google' + }) + commentTransVi = resT.content; + commentTransModel = resT.model; + tokenTranslatorUsed = resT.tokensUsed; + } return { + commentTransVi, + commentTransModel, comment: cleaned, tokensUsed: res.tokensUsed, + tokenTranslatorUsed, model: res.model, language: dto.language, + input: dto, + uid: '', }; } diff --git a/src/modules/content-writer/services/provider-router.service.ts b/src/modules/content-writer/services/provider-router.service.ts index f6069ad..b55ffdb 100644 --- a/src/modules/content-writer/services/provider-router.service.ts +++ b/src/modules/content-writer/services/provider-router.service.ts @@ -5,12 +5,13 @@ import {ProviderName} from '../providers/ai-provider.factory'; import {Language} from "../../../common/interfaces/language.prompt.interface"; import {ContentTone, isEdgyTone} from "../enum/tone.enum"; import {AngleEnum} from "../enum/angle.enum"; +import {getRandomElement} from "../../../shared/helper"; interface ProviderPair { writer: ProviderName; reviewer: ProviderName; } -export type ContentType = 'post' | 'comment'; +export type ContentType = 'post' | 'comment' | 'translation'; interface RoutingDecision { writer: ProviderName; reviewer: ProviderName; @@ -55,6 +56,24 @@ export class ProviderRouterService { }): RoutingDecision { const { language, contentType, style, tone } = params; + if(contentType ==='translation') { + if (language === 'cn') { + // Default EN + return { + writer: 'deepseek', + reviewer: 'deepseek', + useXEnrichment: false, + reason: 'CN default: GPT reliable', + }; + } + return { + writer: getRandomElement([ 'openai',]), + reviewer: 'openai', + useXEnrichment: false, + reason: 'EN default: GPT reliable', + }; + } + if (tone === ContentTone.EMPATHETIC) { return { writer: 'openai', // warmest voice, less "AI-ish" @@ -69,7 +88,7 @@ export class ProviderRouterService { if (tone && isEdgyTone(tone)) { if (language === 'en') { return { - writer: 'grok', + writer: getRandomElement(['openai', 'google', 'grok']), reviewer: 'deepseek', useXEnrichment: false, reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`, @@ -77,14 +96,14 @@ export class ProviderRouterService { } if (tone === ContentTone.SPICY) { return { - writer: 'openai', + writer: getRandomElement(['deepseek', 'google', 'openai',]), reviewer: 'deepseek', useXEnrichment: false, reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`, }; } return { - writer: 'deepseek', + writer: getRandomElement(['deepseek', 'google',]), reviewer: 'deepseek', useXEnrichment: false, reason: `Edgy tone (${tone}) ${language}: DeepSeek (less refusal)`, @@ -96,9 +115,9 @@ export class ProviderRouterService { // Breaking news EN -> Grok (real-time + X-native) if (style === ContentStyle.BREAKING_NEWS) { return { - writer: 'grok', + writer: 'google', reviewer: 'deepseek', - useXEnrichment: true, + useXEnrichment: false, reason: 'EN breaking news: Grok has real-time X context', }; } @@ -106,7 +125,7 @@ export class ProviderRouterService { // Comment EN casual/witty -> Grok if (contentType === 'comment' && tone !== 'professional') { return { - writer: 'grok', + writer: 'google', reviewer: 'deepseek', useXEnrichment: false, reason: 'EN casual comment: Grok sounds most human on X', diff --git a/src/modules/content-writer/services/quote-writer.service.ts b/src/modules/content-writer/services/quote-writer.service.ts index 9b65a87..0d63cf9 100644 --- a/src/modules/content-writer/services/quote-writer.service.ts +++ b/src/modules/content-writer/services/quote-writer.service.ts @@ -14,6 +14,7 @@ import {ContentTone, isEdgyTone} from '../enum/tone.enum'; import {calculateTokenBudget} from "../../../common/utils/token-calculator"; import {QuoteType} from "../enum/quote-type.enum"; import {ContentSafetyService} from "./content-safety.service"; +import {TranslatorService} from "./translator.service"; @Injectable() export class QuoteWriterService { @@ -25,10 +26,11 @@ export class QuoteWriterService { private lengthStrategy: LengthStrategyService, private reviewer: ReviewerService, private config: ConfigService, - private safety: ContentSafetyService + private safety: ContentSafetyService, + private translatorService: TranslatorService ) {} - async generateQuote(dto: GenerateQuoteDto, _retryCount = 0) { + async generateQuote(dto: GenerateQuoteDto, _retryCount = 0 , allowTranslateToVi =true) { const MAX_RETRIES = 2; if (_retryCount >= MAX_RETRIES) { @@ -145,6 +147,21 @@ export class QuoteWriterService { ); } + //transla if language != vi + let quoteTransVi = ''; + let quoteTransModel = ''; + let tokenTranslatorUsed = 0; + if (allowTranslateToVi && dto.language !== 'vi') { + const resT = await this.translatorService.translator({ + text: quote, + target_lang: 'vi', + target_model: 'google' + }) + quoteTransVi = resT.content; + quoteTransModel = resT.model; + tokenTranslatorUsed = resT.tokensUsed; + } + return { quote, quoteType, @@ -153,6 +170,9 @@ export class QuoteWriterService { reviewNotes, tokensUsed: totalTokens, model: modelUsed, + quoteTransVi, + quoteTransModel, + tokenTranslatorUsed }; } diff --git a/src/modules/content-writer/services/style-detector.service.ts b/src/modules/content-writer/services/style-detector.service.ts index a71aa0d..b5f2e20 100644 --- a/src/modules/content-writer/services/style-detector.service.ts +++ b/src/modules/content-writer/services/style-detector.service.ts @@ -59,8 +59,8 @@ export class StyleDetectorService { } detectLanguageFromTelegramAutoContent(text: string): Language { - if (/nhật[ _]bản/i.test(text)) return "ja"; - if (/#hàn_quốc/i.test(text)) return "ko"; + // if (/nhật[ _]bản/i.test(text)) return "ja"; + // if (/#hàn_quốc/i.test(text)) return "ko"; // return getLanguageByJSTTime(); // diff --git a/src/modules/content-writer/services/translator.service.ts b/src/modules/content-writer/services/translator.service.ts new file mode 100644 index 0000000..d1a655d --- /dev/null +++ b/src/modules/content-writer/services/translator.service.ts @@ -0,0 +1,68 @@ +import {AiTranslatorDto} from "../dto/ai-translator.dto"; +import {Language} from "../../../common/interfaces/language.prompt.interface"; +import {LANGUAGE_NAMES} from "../prompts/templates"; +import {Injectable, Logger} from "@nestjs/common"; +import {AIProviderFactory, ProviderName} from "../providers/ai-provider.factory"; +import {ProviderRouterService} from "./provider-router.service"; + +@Injectable() +export class TranslatorService { + private readonly logger = new Logger(TranslatorService.name); + + constructor( + private factory: AIProviderFactory, + private router: ProviderRouterService, + ) { + } + async translator(dto: AiTranslatorDto) { + this.logger.log(`Translating ...`); + const targetLanguage = LANGUAGE_NAMES[dto.target_lang] || LANGUAGE_NAMES['en']; + const systemPrompt = +`You are an expert X (Twitter) translator. +Rules: +- Translate naturally and fluently. +- Preserve the original tone, emotion, and internet culture. +- Keep the post concise and punchy like an actual X post. +- Preserve slang, memes, sarcasm, and crypto terminology. +- Do NOT over-formalize the translation. +- Keep token names, ticker symbols, usernames, hashtags, and project names unchanged. +- Preserve emojis, line breaks, and formatting. +- Do not add explanations, notes, or extra commentary. +- Avoid robotic or textbook-style translation. +- Output ONLY the translated text.`; + const USER_PROMPTS_HINT: Record = { + 'en':'Translate to English:', + 'cn':'Translate to Chinese:', + 'vi':'Translate to Vietnamese:', + 'ja':'Translate to Japanese:', + 'ko':'Translate to Korean:', + } + const userPrompt = [ + `[Target Language: ${targetLanguage}] + IMPORTANT: Your previous answer violated language rules.`, + USER_PROMPTS_HINT[dto.target_lang], + dto.text + ].filter(Boolean).join('\n'); + + if(!dto.target_model) { + // 🧭 Smart routing + const decision = this.router.route({ + language: dto.target_lang, + contentType: 'translation', + }); + dto.target_model = decision.writer; + } + const provider = this.factory.get(dto.target_model); + const draft = await provider.complete( + [ + {role: 'system', content: systemPrompt}, + {role: 'user', content: userPrompt}, + ], + { temperature: 0.1}, + ); + this.logger.debug(`===> ${draft.model} đã dich xong!`); + this.logger.debug(`===> ${draft.content}`); + + return draft; + } +} \ No newline at end of file diff --git a/src/modules/manager/manager.processor.ts b/src/modules/manager/manager.processor.ts index 0fb9a60..ea873fa 100644 --- a/src/modules/manager/manager.processor.ts +++ b/src/modules/manager/manager.processor.ts @@ -7,6 +7,7 @@ import {Context, Telegraf} from "telegraf"; import {_JsonToStr} from "../../shared/helper"; import {ContentWriterService} from "../content-writer/content-writer.service"; import {GenerateContentDto} from "../content-writer/dto/generate-content.dto"; +import {TranslatorService} from "../content-writer/services/translator.service"; @Processor('manager_task_queue') export class ManagerProcessor extends WorkerHost { @@ -15,6 +16,7 @@ export class ManagerProcessor extends WorkerHost { constructor( private aiService: AIService, private contentWriterService: ContentWriterService, + private translatorService: TranslatorService, @InjectBot() private readonly bot: Telegraf, ) { super(); @@ -24,7 +26,7 @@ export class ManagerProcessor extends WorkerHost { console.log(`ManagerProcessor ==> process => ${job.name}`); console.log('job_data', job.data); - let {rawData, style, telegramChatId} = job.data; + let {rawData, style, telegramChatId, targetLanguage, text} = job.data; switch (job.name) { case 'analyze_news': @@ -54,6 +56,23 @@ export class ManagerProcessor extends WorkerHost { return {status: 'completed'}; } + case 'translator_text': { + // Gọi AI phân tích + const returnQuestion = await this.translatorService.translator({ + text, + target_lang: targetLanguage + }); + // console.log(analysis.result); + console.log('telegramChatId:', telegramChatId); + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `${returnQuestion.content}`, + { + parse_mode: 'Markdown', + } + ); + + return {status: 'completed'}; + } case 'enrich_x_context': { const dto: GenerateContentDto = { topic: rawData, diff --git a/src/modules/manager/manager.service.ts b/src/modules/manager/manager.service.ts index 825038c..77aabdb 100644 --- a/src/modules/manager/manager.service.ts +++ b/src/modules/manager/manager.service.ts @@ -3,6 +3,7 @@ import {Injectable} from '@nestjs/common'; import {InjectQueue} from '@nestjs/bullmq'; import {Queue} from 'bullmq'; import {QuoteType} from "../content-writer/enum/quote-type.enum"; +import {Language} from "../../common/interfaces/language.prompt.interface"; @Injectable() export class ManagerService { @@ -77,6 +78,18 @@ export class ManagerService { }, {attempts: 1, backoff: 5000}) } + async handleTranslatorText(text: string, targetLanguage: Language, telegramChatId: number = 0) { + await this.managerTaskQueue.add('translator_text', { + text, + targetLanguage, + telegramChatId + }, {attempts: 1, backoff: 5000}); + // // 2. Đẩy việc cho AI-C tìm video TikTok + // await this.ttQueue.add('find_video', { + // keyword: trendData.keyword, + // }, {attempts: 2}); + } + async handleAskQues(question: string, telegramChatId: number = 0) { await this.managerTaskQueue.add('asking', { rawData: question, @@ -133,6 +146,7 @@ export class ManagerService { telegramChatId: undefined, }, ) { + console.log('manualTriggerWriteWithReview', dto); // 1. Đẩy việc cho AI-B viết bài FB const manualtriggerresult = await this.fbQueue.add('generate_post_ver2', { title: dto.keyword, @@ -159,6 +173,34 @@ export class ManagerService { } } + async triggerAutoGenerateCommentTwitter( + TwitterUrl: string, + x_username: string, + preferLanguage?: string, + telegramChatId?:number + ) { + await this.commentQueue.add('GENERATE_AUTO_COMMENT_TWITTER', { + tweetUrl: TwitterUrl, + language: preferLanguage, + telegramChatId, + x_username: x_username || 'realflashkaze', + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + //console.log(manualtriggerresult); + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + async manualTriggerCommentLinkTwitter(TwitterUrl, preferLanguage = 'en', telegramChatId = '') { await this.commentQueue.add('generate_comment_twitter', { url: TwitterUrl, diff --git a/src/modules/sqs-module/sqs.post.service.ts b/src/modules/sqs-module/sqs.post.service.ts index 0f698de..f44f5a3 100644 --- a/src/modules/sqs-module/sqs.post.service.ts +++ b/src/modules/sqs-module/sqs.post.service.ts @@ -1,40 +1,49 @@ // post.service.ts -import {Injectable} from '@nestjs/common'; +import {Injectable, Logger} from '@nestjs/common'; import {SQS_QUEUES_NAME, SqsService} from './sqs.service'; @Injectable() export class SqsPostService { + private readonly logger = new Logger('SqsPostService'); + constructor(private readonly sqs: SqsService) { } - async sendMessage(username: string, data) { + async sendMessage(username: string, data, delaySeconds = 0) { console.log(`SqsPostService -> sendMessage ${username} `); switch (username) { case 'realflashkaze': { - return this.postFlashKaze(data) + await this.postFlashKaze(data, delaySeconds); + console.log(`SqsPostService -> sendMessage done ${username} `); + break; } case 'echcomvuive': { - return this.postEchCom(data) + await this.postEchCom(data, delaySeconds); + console.log(`SqsPostService -> sendMessage done ${username} `); + break; + } + default: { + this.logger.error(`SqsPostService -> K tim thay username=${username} `); break; } } } - async postFlashKaze(data: any): Promise { + async postFlashKaze(data: any, delaySeconds = 0): Promise { console.log(`SqsPostService_postFlashKaze`) return this.sqs.enqueue( SQS_QUEUES_NAME.REALFLASHKAZE!, data, { jobId: `acc1-${Date.now()}`, - delaySeconds: Math.floor(Math.random() * 30), + delaySeconds: delaySeconds > 0 ? delaySeconds : Math.floor(Math.random() * 30), }, ); } - async postEchCom(data: any) { + async postEchCom(data: any, delaySeconds = 0) { console.log(`SqsPostService_postEchCom`) await this.sqs.enqueue( @@ -42,7 +51,7 @@ export class SqsPostService { data, { jobId: `acc2-${Date.now()}`, - delaySeconds: Math.floor(Math.random() * 30), + delaySeconds: delaySeconds > 0 ? delaySeconds : Math.floor(Math.random() * 30), }, ); } diff --git a/src/modules/telegram/telegram.content.writer.completed.processor.ts b/src/modules/telegram/telegram.content.writer.completed.processor.ts index faa9379..709acc1 100644 --- a/src/modules/telegram/telegram.content.writer.completed.processor.ts +++ b/src/modules/telegram/telegram.content.writer.completed.processor.ts @@ -29,38 +29,15 @@ export class TelegramContentWriterCompletedProcessor extends WorkerHost { console.log('TelegramProcessor_facebook_content_writer_completed_process=>posting') //console.log(fbContent); if (job.name === 'generate_post_completed') { - const {id, needConfirm, content, autoPublish, telegramChatId, xSubmitProvider} = job.data; + const { + id, needConfirm, content, autoPublish, telegramChatId, xSubmitProvider, + contentTransVi, + contentTransModel, + } = job.data; console.log({id, needConfirm, autoPublish, xSubmitProvider}) const X_USERS = process.env.TWITTER_USERNAMES!.split(','); console.log({X_USERS}); - //console.log({job}) - // if (autoPublish) { - // // await this.publishPageService.publishTwitter(id); - // // await this.publishPageService.publishToFacebook(id); - // const postId = toNumber(id); - // await this.bot.telegram.sendMessage(adminChatId, '🤖Đã viết xong tin auto:'); - // - // try { - // await Promise.allSettled([ - // this.publishPageService.publishToFacebook(postId).then(() => { - // this.bot.telegram.sendMessage(adminChatId, `Đăng bài lên FB số ${postId} thành công`); - // }).catch(err => { - // this.bot.telegram.sendMessage(adminChatId, `PublishToFacebook error ${err.message}`); - // - // }), - // this.publishPageService.publishTwitter(postId, xSubmitProvider).then((resp) => { - // this.bot.telegram.sendMessage(adminChatId, `Đăng bài lên X số ${postId} thành công via ${xSubmitProvider}, ${resp?.url}`); - // }).catch(err => { - // this.bot.telegram.sendMessage(adminChatId, `PublishToX error ${err.message}`); - // }), - // ] - // ) - // } catch (e) { - // await this.bot.telegram.sendMessage(adminChatId, `❌ Lỗi đăng bài: ${e.message}`); - // } - // return; - // } - await this.bot.telegram.sendMessage(telegramChatId || adminChatId, '🤖Đã viết xong:'); + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `🤖Đã viết xong: Dich: ${contentTransVi}`); await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `${content}\n\n`, { diff --git a/src/modules/telegram/telegram.module.ts b/src/modules/telegram/telegram.module.ts index 808d052..7b53021 100644 --- a/src/modules/telegram/telegram.module.ts +++ b/src/modules/telegram/telegram.module.ts @@ -38,7 +38,7 @@ import {SqsPostService} from "../sqs-module/sqs.post.service"; QuoteWizard, Comment2Wizard, XImageUploadService, - SqsPostService + SqsPostService, ], }) export class TelegramModule implements OnModuleInit{ diff --git a/src/modules/telegram/telegram.updates.ts b/src/modules/telegram/telegram.updates.ts index 39e773d..db33556 100644 --- a/src/modules/telegram/telegram.updates.ts +++ b/src/modules/telegram/telegram.updates.ts @@ -1,4 +1,4 @@ -import {Action, Command, Ctx, On, Start, Update} from 'nestjs-telegraf'; +import {Action, Command, Ctx, Hears, Message, On, Start, Update} from 'nestjs-telegraf'; import {Context, Scenes} from 'telegraf'; import {HttpException, Injectable} from "@nestjs/common"; import {ManagerService} from "../manager/manager.service"; @@ -18,6 +18,8 @@ import {TextUtil} from "../../common/utils/text.util"; import {XStrategy} from "../social-api/x-router.service"; import {XCacheService} from "../x-cache/x-cache.service"; import {SqsPostService} from "../sqs-module/sqs.post.service"; +import {PgPostService} from "../../shared/pg.post.service"; +import {Language} from "../../common/interfaces/language.prompt.interface"; @Injectable() @Update() @@ -30,7 +32,8 @@ export class TelegramUpdates { private readonly trendsService: TrendsService, private readonly xUpload: XImageUploadService, private readonly cacheService: XCacheService, - private readonly sqsPostService: SqsPostService + private readonly sqsPostService: SqsPostService, + private readonly pgPost: PgPostService, ) { } @@ -110,14 +113,79 @@ export class TelegramUpdates { async onWizardCommand(@Ctx() ctx: Scenes.SceneContext): Promise { await ctx.scene.enter(WIZARD_COMMENT_SCENE_ID); } + @Command('chatid') + async onGetChatId(ctx: Context) { + // @ts-ignore + ctx.reply(ctx.chat.id) + } + + @Command('atc') + async onAutoCommentCommand(@Ctx() ctx: Context): Promise { + //@ts-ignore + let {command, payload} = ctx; + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} LinkX`); + return; + } + await this.sharedAutoComment(ctx, payload); + } + @Action('action_auto_comment') + async onDownloadAction(@Ctx() ctx: Context) { + await ctx.answerCbQuery(); // Tắt trạng thái loading trên nút bấm + + // Ép kiểu để ép TypeScript nhận diện đúng đối tượng tin nhắn chữ + // @ts-ignore + const message = ctx.callbackQuery.message as any; + + // 💡 LẤY ĐƯỢC LUÔN: Đây chính là nội dung text của tin nhắn chứa nút bấm này! + const textInMessage = message.text; + + // await ctx.reply(`Tôi đọc được chữ từ tin nhắn cũ: ${textInMessage}`); + + await this.sharedAutoComment(ctx, textInMessage); + } + + private async sharedAutoComment(ctx: Context, linkX:string): Promise { + // console.log(ctx); + // @ts-ignore + if (!linkX) { + await ctx.reply(`Thiếu LinkX`); + return; + } + const detectSendLinkX = TextUtil.detectLinkX(linkX); + if (detectSendLinkX.hasLinkX) { + // @ts-ignore + linkX = detectSendLinkX.url; + } else { + await ctx.reply('Không có link X'); + return; + } + const chatId = ctx.chat?.id; + + await this.managerService.triggerAutoGenerateCommentTwitter( + linkX, + 'realflashkaze', + 'en', + chatId, + ); + await ctx.reply('Chờ xử lý ...'); + } + @Command([ 'comvi', + 'cvi', + 'cvil', 'comen', + 'cen', 'comja', + 'cja', 'comjal', + 'cjal', 'comko', + 'cko', 'comcn', + 'ccn', ]) async onCommentAsTextCommand(@Ctx() ctx: Scenes.SceneContext): Promise { @@ -125,16 +193,19 @@ export class TelegramUpdates { let xpostreader = 'api' // @ts-ignore let {command, payload} = ctx; - if (['comvi', 'comvn', 'com_vi'].includes(command)) { + if (['cvi', 'cvil', 'comvi', 'comvn', 'com_vi'].includes(command)) { language = 'vi'; + if (['cvil', 'comvil'].includes(command)) { + xpostreader = 'browser' + } } if (['comko', 'comkr', 'com_ko'].includes(command)) { language = 'ko'; } - if (['comja', 'com_ja',].includes(command)) { + if (['comja', 'com_ja', 'cja', 'cjal'].includes(command)) { language = 'ja'; } - if (['comjal', 'comjalong' ].includes(command)) { + if (['comjal', 'comjalong', 'cjal'].includes(command)) { language = 'ja'; xpostreader = 'browser'; } @@ -155,7 +226,7 @@ export class TelegramUpdates { const match = _linkX.match(/status\/(\d+)/); if (match) { //nêu match => get content x - const xpost = await this.xReaderService.readXPost(_linkX, xpostreader, chatId); + const xpost: any = await this.xReaderService.readXPost(_linkX, xpostreader, chatId); console.log('==> content text:' + xpost.text); payload = xpost.text; tweetId = xpost.tweetId; @@ -176,11 +247,15 @@ export class TelegramUpdates { 'quote_vi', 'quotevi', 'quotevn', + 'quotevnl', + 'qvnl', 'quote_en', 'quoteen', 'quote_ja', 'quoteja', 'quotejal', + 'qjal', + 'qja', 'quote_jp', 'quotejp', 'quote_ko', @@ -191,15 +266,17 @@ export class TelegramUpdates { // @ts-ignore let {command, payload} = ctx; let xreaderMode = 'api'; - if(['quotejal'].includes(command)) { - command = 'quote_ja'; - xreaderMode='browser'; + + if (['quotevnl', 'qvnl', 'quotejal', 'qjal'].includes(command)) { + xreaderMode = 'browser'; } - if (['quote_jp', 'quotejp', 'quoteja'].includes(command)) { + + if (['quote_jp', 'quotejp', 'quoteja', 'qja', 'quotejal'].includes(command)) { command = 'quote_ja'; } - if (['quote_vn', 'quotevi', 'quotevn'].includes(command)) { + if (['quote_vn', 'quotevi', 'quotevn', 'quotevnl', 'qvnl'].includes(command)) { command = 'quote_vi'; + } if (['quoteen'].includes(command)) { command = 'quote_en'; @@ -329,8 +406,13 @@ export class TelegramUpdates { const detectSendLinkX = TextUtil.detectLinkX(payload); if (detectSendLinkX.hasLinkX) { const xpost = await this.xReaderService.readXPost(detectSendLinkX.url); + // @ts-ignore payload = xpost.text; } + if (!payload) { + await ctx.reply(`reader không được`); + return; + } } await ctx.scene.enter(WIZARD_WRITER_SCENE_ID, { @@ -354,8 +436,13 @@ export class TelegramUpdates { const postNo = 1 * payload; await ctx.reply(`Đang đẩy bài ${payload} lên Facebook, Twitter`); + const post = await this.pgPost.getById(postNo); + if (!post) { + await ctx.reply('No found'); + return; + } await ctx.reply( - `🤖 **Vui lòng chọn kênh**`, + post.content!, { parse_mode: 'Markdown', reply_markup: { @@ -429,6 +516,45 @@ export class TelegramUpdates { await ctx.reply(`vui lòng đưa chờ phân tích ....`); } + @Command(['tvi', 'tja', 'ten', 'tko', 'tcn']) + async onTranslator(ctx: Context) { + // @ts-ignore + const {command, payload} = ctx; + let lang: Language = 'vi' + switch (command) { + case 'tvi': { + lang = 'vi'; + break; + } + case 'tja': { + lang = 'ja'; + break; + } + case 'ten': { + lang = 'en'; + break; + } + case 'tko': { + lang = 'ko'; + break; + } + case 'tcn': { + lang = 'cn'; + break; + } + + } + + if (!payload) { + await ctx.reply(`vui lòng đưa text, ví dụ: /${command} Hot news today in japan ?`); + return; + } + console.log(ctx.chat); + await this.managerService.handleTranslatorText(payload, lang, ctx.chat?.id); + await ctx.reply(`vui lòng chờ ....`); + } + + @Command(['xreader', 'xrbr']) async onXReader(ctx: Context) { // @ts-ignore @@ -441,7 +567,7 @@ export class TelegramUpdates { const content = await this.xReaderService.readXPost( payload, - ['xreader_browser','xrbr'].includes(command) ? 'browser' : 'any' + ['xreader_browser', 'xrbr'].includes(command) ? 'browser' : 'any' ).catch((err) => { ctx.reply(err.message); return err; @@ -483,7 +609,7 @@ export class TelegramUpdates { const callbackData = (ctx.callbackQuery as any).data; const social = callbackData.split('_')[1]; const postId = _toNum(callbackData.split('_')[2]); - const xUsername = callbackData.split('_')[3]; + let xUsername = callbackData.split('_')[3]; await ctx.answerCbQuery(`Đang đẩy bài lên Fb,X ${xUsername}...`); @@ -501,7 +627,7 @@ export class TelegramUpdates { case 'twitter': allowTw = 1; publishTo = ['x']; - twStrageryPost=XStrategy.BROWSER_FIRST + twStrageryPost = XStrategy.BROWSER_FIRST break; case 'twitterbrowser': allowTw = 1; @@ -511,6 +637,7 @@ export class TelegramUpdates { case 'facebook': allowFb = 1; publishTo = ['fb']; + xUsername = 'realflashkaze'; break; } @@ -682,8 +809,8 @@ export class TelegramUpdates { ) // const r = await this.publishPageService.relyX(messageText, tweetId) //@ts-ignore - await ctx.editMessageText(`✅ ** Đã gửi tin sang queue ${xUsername} ID bài viết: ${tweetId}`); - // await ctx.reply(`✅ Đã gửi tin sang queue ${xUsername}!, ID bài viết: ${tweetId}`); + await ctx.editMessageText(`✅ ** Đã gửi tin sang queue ${xUsername} ID bài viết: ${tweetId}`); + // await ctx.reply(`✅ Đã gửi tin sang queue ${xUsername}!, ID bài viết: ${tweetId}`); // else { // //@ts-ignore // await ctx.reply(`❌ Lỗi reply: ${r.error} `); @@ -818,9 +945,21 @@ export class TelegramUpdates { } } - @Command('chatid') - async onGetChatId(ctx: Context) { - // @ts-ignore - ctx.reply(ctx.chat.id) + // Pattern này sẽ bắt tất cả các link dạng ://x.com... hoặc ://twitter.com... + @Hears(/https?:\/\/(www\.)?(x\.com|twitter\.com)\/[a-zA-DR-Z0-9_]+/i) + async handleXLink(@Ctx() ctx: Context, @Message('text') text: string) { + // text ở đây chính là toàn bộ đoạn tin nhắn chứa link x.com + + // Ví dụ xử lý: Trích xuất phần ID hoặc convert link để preview (như fixupx.com) + // const fixedLink = text.replace(/x\.com|twitter\.com/i, 'fixupx.com'); + + await ctx.reply(text, { + reply_markup: { + inline_keyboard: [ + [{ text: '📥 Chạy lệnh auto cmt', callback_data: 'action_auto_comment' }] + ] + } + }); + //return `Tôi đã tìm thấy link X! Đây là bản preview: ${fixedLink}`; } } diff --git a/src/modules/x-cache/x-cache.service.ts b/src/modules/x-cache/x-cache.service.ts index 8b772fa..6ee29da 100644 --- a/src/modules/x-cache/x-cache.service.ts +++ b/src/modules/x-cache/x-cache.service.ts @@ -1,5 +1,6 @@ import {Inject, Injectable} from "@nestjs/common"; import {Cache, CACHE_MANAGER} from "@nestjs/cache-manager"; +import {_toNum} from "../../shared/helper"; @Injectable() export class XCacheService { @@ -21,8 +22,17 @@ export class XCacheService { return data; } + async setAlreadyComment(tweetId: string, xUsername: string) { + await this.setCachedKey(`comment_tweetId:${xUsername}:${tweetId}`, 1, 4 * 3600); + return {tweetId, xUsername}; + } + async hasComment(tweetId: string, xUsername: string) { + const v = await this.getCachedData(`comment_tweetId:${xUsername}:${tweetId}`); + return 1 === _toNum(v); + } + async setCacheTweetUrlById(tweetId, tweetUrl) { - await this.setCachedKey(`x_tweetId:${tweetId}`, tweetUrl, 24*3600); + await this.setCachedKey(`x_tweetId:${tweetId}`, tweetUrl, 24 * 3600); return {tweetId, tweetUrl}; } @@ -30,6 +40,26 @@ export class XCacheService { return await this.getCachedData(`x_tweetId:${tweetId}`) as string; } + async setCacheTweetContentById(tweetId, content) { + console.log('==> setCacheTweetContentById'); + await this.setCachedKey(`x_tweet_content:${tweetId}`, content, 12 * 3600); + return {tweetId, console}; + } + + async getCacheTweetContentById(tweetId): Promise | undefined> { + return await this.getCachedData(`x_tweet_content:${tweetId}`); + } + async incrCountCollectNewsapi() { //this.cacheManager.c } diff --git a/src/modules/x-reader/x-reader.service.ts b/src/modules/x-reader/x-reader.service.ts index 0682bc4..f01d04b 100644 --- a/src/modules/x-reader/x-reader.service.ts +++ b/src/modules/x-reader/x-reader.service.ts @@ -1,5 +1,5 @@ import {chromium} from 'playwright'; -import {Injectable} from "@nestjs/common"; +import {Injectable, Logger} from "@nestjs/common"; import axios from "axios"; import {XCacheService} from "../x-cache/x-cache.service"; import {InjectBot} from "nestjs-telegraf"; @@ -7,13 +7,15 @@ import {Context, Telegraf} from "telegraf"; @Injectable() export class XReaderService { + private logger = new Logger('XReaderService'); + constructor( private readonly cacheService: XCacheService, @InjectBot() private readonly bot: Telegraf, ) { } - async readXPostViaBrowserV2(url, telegramChatId = 0) { + async readXPostViaBrowserV2(url, telegramChatId = 0, useCache = true) { // Normalize URL console.log(`[X] XReaderService -> readXPostViaBrowserV2...`); // @ts-ignore @@ -25,6 +27,22 @@ export class XReaderService { if (!match) throw new Error('URL X không hợp lệ'); const tweetId = match[1]; console.log({tweetId, url}); + + // + if (useCache) { + this.logger.debug('Find from cache ...') + const fCacheData = await this.cacheService.getCacheTweetContentById(tweetId); + if (fCacheData) { + this.logger.log(`[X] XReaderService ->found data cache, return.`); + return { + ...fCacheData, + isCached: true, + }; + } + this.logger.log(`[X] XReaderService -> no cache, start browser...`); + } + + const browser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--disable-blink-features=AutomationControlled'] @@ -85,6 +103,7 @@ export class XReaderService { // Text const textEl = root.querySelector('[data-testid="tweetText"]'); const text = textEl?.innerText || ''; + const lang = textEl?.getAttribute('lang') || ''; console.log(`==> 4`); // User info const userEl = root.querySelector('[data-testid="User-Name"]'); @@ -120,6 +139,7 @@ export class XReaderService { console.log(`==> 8`); return { text, + lang, author, handle, time, @@ -221,6 +241,12 @@ export class XReaderService { await browser.close(); if (!data) throw new Error(`Không parse được tweet ${tweetId} (có thể bị ẩn/xoá)`); + + if (data) { + if (data.text) { + await this.cacheService.setCacheTweetContentById(tweetId, data) + } + } return data; } catch (err) { try { @@ -341,7 +367,7 @@ export class XReaderService { } } - async readXPost(url, crawlerType: string = 'any', telegramChatId = 0) { + async readXPost(url, crawlerType: string = 'any', telegramChatId = 0, useCache=true) { console.log(`[X] XReaderService -> readXPost...`); url = url.replace('twitter.com', 'x.com').split('?')[0]; const match = url.match(/status\/(\d+)/); @@ -350,14 +376,14 @@ export class XReaderService { console.log({tweetId, url}); await this.cacheService.setCacheTweetUrlById(tweetId, url); if (crawlerType === 'browser') { - return await this.readXPostViaBrowserV2(url, telegramChatId); + return await this.readXPostViaBrowserV2(url, telegramChatId, useCache); } try { console.log('[X] Thử syndication API...'); return await this.readXPostViaApi(url); } catch (err) { console.log(`[X] API fail (${err.message}), fallback sang browser...`); - return await this.readXPostViaBrowserV2(url, telegramChatId); + return await this.readXPostViaBrowserV2(url, telegramChatId, useCache); } } } diff --git a/src/shared/helper.ts b/src/shared/helper.ts index a9812c7..79a781a 100644 --- a/src/shared/helper.ts +++ b/src/shared/helper.ts @@ -66,4 +66,21 @@ export const getLanguageByJSTTime = () => { } return 'en'; +} + +export function getRandomElement(array: T[]): T { + const index = Math.floor(Math.random() * array.length); + return array[index]; +} + +export function getUuid4() { + const uuidv4 = require('uuid'); + + return uuidv4(); // Generates a random version 4 UUID +} +export function _getTweetIdFromUrl(xUrl: string) { + xUrl = xUrl.replace('twitter.com', 'x.com').split('?')[0]; + const match = xUrl.match(/status\/(\d+)/); + if (!match) throw new Error('getTweetIdFromUrl: URL X không hợp lệ'); + return match[1]; } \ No newline at end of file diff --git a/src/shared/pg.post.service.ts b/src/shared/pg.post.service.ts index ef3c0bc..3737f4f 100644 --- a/src/shared/pg.post.service.ts +++ b/src/shared/pg.post.service.ts @@ -13,6 +13,10 @@ export class PgPostService { }); } + async getById(id: number): Promise { + return this.post({id}) + } + async createPost(data: Prisma.PostCreateInput): Promise { return this.prisma.post.create({ data,