This commit is contained in:
NAME
2026-05-23 06:27:07 +00:00
parent 60dd0730f0
commit 83553be5b3
35 changed files with 763 additions and 126 deletions
+2 -1
View File
@@ -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",
+9
View File
@@ -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:
+6 -2
View File
@@ -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)
}
+2 -2
View File
@@ -20,7 +20,7 @@ const TOKENS_PER_CHAR: Record<Language, number> = {
* Target character length theo platform + buffer để AI có không gian "thở".
*/
const PLATFORM_TARGET_CHARS: Record<Platform, { min: number; max: number; buffer: number }> = {
[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, number> = {
[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 {
@@ -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<Context>,
//@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: {
@@ -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);
}
}
@@ -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 {
@@ -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,
@@ -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<Context>,
// 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
})
}
@@ -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;
}
@@ -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"...
}
@@ -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()
@@ -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, string> = {
[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<Record<AngleEnum, Object>> = {
[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'},
@@ -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};
}
@@ -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}`
: ``;
@@ -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}`);
}
@@ -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<AICompletionResult> {
@@ -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<AICompletionResult> {
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,
};
}
}
@@ -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() {
@@ -41,7 +41,7 @@ export class OpenAIProvider implements IAIProvider {
}
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
console.log(`OpenAIProvider_complete`);
console.log(`OpenAIProvider_complete_begin...`);
const model = options.model ?? this.defaultModel;
try {
@@ -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: '',
};
}
@@ -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',
@@ -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
};
}
@@ -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();
//
@@ -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<Language, string> = {
'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;
}
}
+20 -1
View File
@@ -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<Context>,
) {
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,
+42
View File
@@ -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,
+17 -8
View File
@@ -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<any> {
async postFlashKaze(data: any, delaySeconds = 0): Promise<any> {
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),
},
);
}
@@ -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`,
{
+1 -1
View File
@@ -38,7 +38,7 @@ import {SqsPostService} from "../sqs-module/sqs.post.service";
QuoteWizard,
Comment2Wizard,
XImageUploadService,
SqsPostService
SqsPostService,
],
})
export class TelegramModule implements OnModuleInit{
+160 -21
View File
@@ -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<void> {
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<void> {
//@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<void> {
// 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<void> {
@@ -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}`;
}
}
+31 -1
View File
@@ -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<Partial<{
stats: any
quoted: {}
via: string
tweetId: any
text?: any
author?: any
handle?: any
time?: any
lang?: string
}> | undefined> {
return await this.getCachedData(`x_tweet_content:${tweetId}`);
}
async incrCountCollectNewsapi() {
//this.cacheManager.c
}
+31 -5
View File
@@ -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<Context>,
) {
}
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);
}
}
}
+17
View File
@@ -66,4 +66,21 @@ export const getLanguageByJSTTime = () => {
}
return 'en';
}
export function getRandomElement<T>(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];
}
+4
View File
@@ -13,6 +13,10 @@ export class PgPostService {
});
}
async getById(id: number): Promise<Post | null> {
return this.post({id})
}
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
return this.prisma.post.create({
data,