| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758 |
- import OpenAI from 'openai';
- import { config } from '../config/index.js';
- import { logger } from '../utils/logger.js';
- import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
- /**
- * 消息角色类型
- */
- export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
- /**
- * 聊天消息接口
- */
- export interface ChatMessage {
- role: MessageRole;
- content: string;
- name?: string;
- tool_call_id?: string;
- }
- /**
- * 聊天补全选项
- */
- export interface ChatCompletionOptions {
- model?: string;
- messages: ChatMessage[];
- temperature?: number;
- maxTokens?: number;
- topP?: number;
- stream?: boolean;
- tools?: ChatCompletionTool[];
- toolChoice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
- responseFormat?: { type: 'text' | 'json_object' };
- stop?: string | string[];
- seed?: number;
- }
- /**
- * 流式响应回调
- */
- export type StreamCallback = (chunk: string, done: boolean) => void;
- /**
- * 嵌入向量选项
- */
- export interface EmbeddingOptions {
- model?: string;
- input: string | string[];
- dimensions?: number;
- }
- /**
- * 视觉理解选项
- */
- export interface VisionOptions {
- model?: string;
- prompt: string;
- imageUrl?: string;
- imageBase64?: string;
- maxTokens?: number;
- }
- /**
- * 函数定义
- */
- export interface FunctionDefinition {
- name: string;
- description: string;
- parameters: Record<string, unknown>;
- }
- /**
- * 工具调用结果
- */
- export interface ToolCallResult {
- id: string;
- function: {
- name: string;
- arguments: string;
- };
- }
- /**
- * AI 服务响应
- */
- export interface AIResponse {
- content: string;
- toolCalls?: ToolCallResult[];
- usage?: {
- promptTokens: number;
- completionTokens: number;
- totalTokens: number;
- };
- finishReason?: string;
- }
- /**
- * 登录状态分析结果
- */
- export interface LoginStatusAnalysis {
- isLoggedIn: boolean;
- hasVerification: boolean;
- verificationType?: 'captcha' | 'sms' | 'qrcode' | 'face' | 'slider' | 'other';
- verificationDescription?: string;
- pageDescription: string;
- suggestedAction?: string;
- }
- /**
- * 账号信息提取结果
- */
- export interface AccountInfoExtraction {
- found: boolean;
- accountName?: string;
- accountId?: string;
- avatarDescription?: string;
- fansCount?: string;
- worksCount?: string;
- otherInfo?: string;
- navigationGuide?: string;
- navigationSuggestion?: string;
- }
- /**
- * 页面操作指导结果
- */
- export interface PageOperationGuide {
- hasAction: boolean;
- actionType?: 'click' | 'input' | 'scroll' | 'wait' | 'navigate';
- targetDescription?: string;
- targetSelector?: string;
- targetPosition?: { x: number; y: number };
- inputText?: string;
- explanation: string;
- }
- /**
- * 发布状态分析结果
- */
- export interface PublishStatusAnalysis {
- status: 'uploading' | 'processing' | 'success' | 'failed' | 'need_captcha' | 'need_action';
- captchaType?: 'image' | 'sms' | 'slider' | 'other';
- captchaDescription?: string;
- errorMessage?: string;
- nextAction?: {
- actionType: 'click' | 'input' | 'wait';
- targetDescription: string;
- targetSelector?: string;
- };
- pageDescription: string;
- confidence: number; // 0-100 表示 AI 对判断的信心程度
- }
- /**
- * 阿里云百炼千问大模型 AI 服务类
- *
- * 支持功能:
- * - 聊天补全(Chat Completion)
- * - 流式输出(Streaming)
- * - 函数调用(Function Calling)
- * - 视觉理解(Vision)
- * - 文本嵌入(Embeddings)
- * - 多模型支持
- * - 自动重试机制
- */
- export class QwenAIService {
- private client: OpenAI | null = null;
- private models: typeof config.ai.models;
- constructor() {
- this.models = config.ai.models;
- if (config.ai.apiKey) {
- this.client = new OpenAI({
- apiKey: config.ai.apiKey,
- baseURL: config.ai.baseUrl,
- timeout: config.ai.timeout,
- maxRetries: config.ai.maxRetries,
- });
- logger.info('QwenAIService initialized', { baseUrl: config.ai.baseUrl });
- } else {
- logger.warn('QwenAIService: API key not configured');
- }
- }
- /**
- * 检查 AI 服务是否可用
- */
- isAvailable(): boolean {
- return !!this.client;
- }
- /**
- * 获取可用模型列表
- */
- getAvailableModels(): typeof config.ai.models {
- return this.models;
- }
- /**
- * 确保服务可用
- */
- private ensureAvailable(): void {
- if (!this.client) {
- throw new Error('AI service not configured. Please set DASHSCOPE_API_KEY environment variable.');
- }
- }
- // ==================== 核心 API 方法 ====================
- /**
- * 聊天补全 - 基础方法
- * @param options 聊天选项
- * @returns AI 响应
- */
- async chatCompletion(options: ChatCompletionOptions): Promise<AIResponse> {
- this.ensureAvailable();
- const {
- model = this.models.chat,
- messages,
- temperature = 0.7,
- maxTokens = 2000,
- topP = 0.9,
- tools,
- toolChoice,
- responseFormat,
- stop,
- seed,
- } = options;
- const startTime = Date.now();
- const requestId = `chat_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- logger.info(`[AI] ========== Chat Completion Request ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Model: ${model}`);
- logger.info(`[AI] Messages: ${messages.length} 条`);
- logger.info(`[AI] Temperature: ${temperature}, MaxTokens: ${maxTokens}`);
- if (tools) logger.info(`[AI] Tools: ${tools.length} 个函数`);
- if (responseFormat) logger.info(`[AI] Response Format: ${responseFormat.type}`);
- try {
- const response = await this.client!.chat.completions.create({
- model,
- messages: messages as ChatCompletionMessageParam[],
- temperature,
- max_tokens: maxTokens,
- top_p: topP,
- tools,
- tool_choice: toolChoice,
- response_format: responseFormat,
- stop,
- seed,
- });
- const duration = Date.now() - startTime;
- const choice = response.choices[0];
- logger.info(`[AI] ========== Chat Completion Response ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Duration: ${duration}ms`);
- logger.info(`[AI] Finish Reason: ${choice?.finish_reason}`);
- if (response.usage) {
- logger.info(`[AI] Tokens - Prompt: ${response.usage.prompt_tokens}, Completion: ${response.usage.completion_tokens}, Total: ${response.usage.total_tokens}`);
- }
- logger.info(`[AI] Response Length: ${choice?.message?.content?.length || 0} 字符`);
- logger.info(`[AI] ==============================================`);
- return {
- content: choice?.message?.content || '',
- toolCalls: choice?.message?.tool_calls?.map(tc => ({
- id: tc.id,
- function: {
- name: tc.function.name,
- arguments: tc.function.arguments,
- },
- })),
- usage: response.usage ? {
- promptTokens: response.usage.prompt_tokens,
- completionTokens: response.usage.completion_tokens,
- totalTokens: response.usage.total_tokens,
- } : undefined,
- finishReason: choice?.finish_reason || undefined,
- };
- } catch (error) {
- const duration = Date.now() - startTime;
- logger.error(`[AI] ========== Chat Completion Error ==========`);
- logger.error(`[AI] Request ID: ${requestId}`);
- logger.error(`[AI] Duration: ${duration}ms`);
- logger.error(`[AI] Error:`, error);
- logger.error(`[AI] ============================================`);
- throw this.handleError(error);
- }
- }
- /**
- * 流式聊天补全
- * @param options 聊天选项
- * @param callback 流式回调
- * @returns 完整的响应内容
- */
- async chatCompletionStream(
- options: Omit<ChatCompletionOptions, 'stream'>,
- callback: StreamCallback
- ): Promise<string> {
- this.ensureAvailable();
- const {
- model = this.models.chat,
- messages,
- temperature = 0.7,
- maxTokens = 2000,
- topP = 0.9,
- stop,
- } = options;
- const startTime = Date.now();
- const requestId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- logger.info(`[AI] ========== Stream Chat Request ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Model: ${model}`);
- logger.info(`[AI] Messages: ${messages.length} 条`);
- logger.info(`[AI] Temperature: ${temperature}, MaxTokens: ${maxTokens}`);
- try {
- const stream = await this.client!.chat.completions.create({
- model,
- messages: messages as ChatCompletionMessageParam[],
- temperature,
- max_tokens: maxTokens,
- top_p: topP,
- stop,
- stream: true,
- });
- let fullContent = '';
- for await (const chunk of stream) {
- const delta = chunk.choices[0]?.delta?.content || '';
- if (delta) {
- fullContent += delta;
- callback(delta, false);
- }
- }
- callback('', true);
- const duration = Date.now() - startTime;
- logger.info(`[AI] ========== Stream Chat Response ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Duration: ${duration}ms`);
- logger.info(`[AI] Response Length: ${fullContent.length} 字符`);
- logger.info(`[AI] =============================================`);
- return fullContent;
- } catch (error) {
- const duration = Date.now() - startTime;
- logger.error(`[AI] ========== Stream Chat Error ==========`);
- logger.error(`[AI] Request ID: ${requestId}`);
- logger.error(`[AI] Duration: ${duration}ms`);
- logger.error(`[AI] Error:`, error);
- logger.error(`[AI] =========================================`);
- throw this.handleError(error);
- }
- }
- /**
- * 简单对话 - 便捷方法
- * @param prompt 用户提示
- * @param systemPrompt 系统提示(可选)
- * @param model 模型(可选)
- * @returns AI 回复内容
- */
- async chat(prompt: string, systemPrompt?: string, model?: string): Promise<string> {
- const messages: ChatMessage[] = [];
- if (systemPrompt) {
- messages.push({ role: 'system', content: systemPrompt });
- }
- messages.push({ role: 'user', content: prompt });
- const response = await this.chatCompletion({
- model: model || this.models.chat,
- messages,
- });
- return response.content;
- }
- /**
- * 快速对话 - 使用快速模型
- */
- async quickChat(prompt: string, systemPrompt?: string): Promise<string> {
- return this.chat(prompt, systemPrompt, this.models.fast);
- }
- /**
- * 推理对话 - 使用推理模型(适合复杂逻辑问题)
- */
- async reasoningChat(prompt: string, systemPrompt?: string): Promise<string> {
- return this.chat(prompt, systemPrompt, this.models.reasoning);
- }
- /**
- * 代码生成/分析 - 使用代码模型
- */
- async codeChat(prompt: string, systemPrompt?: string): Promise<string> {
- const defaultCodeSystemPrompt = '你是一个专业的编程助手,擅长代码编写、分析和调试。请提供清晰、高效的代码解决方案。';
- return this.chat(prompt, systemPrompt || defaultCodeSystemPrompt, this.models.coder);
- }
- // ==================== 函数调用 ====================
- /**
- * 带函数调用的对话
- * @param messages 消息列表
- * @param functions 函数定义列表
- * @param toolChoice 工具选择策略
- */
- async chatWithFunctions(
- messages: ChatMessage[],
- functions: FunctionDefinition[],
- toolChoice: 'auto' | 'none' | string = 'auto'
- ): Promise<AIResponse> {
- const tools: ChatCompletionTool[] = functions.map(fn => ({
- type: 'function' as const,
- function: {
- name: fn.name,
- description: fn.description,
- parameters: fn.parameters,
- },
- }));
- const choice = toolChoice === 'auto' || toolChoice === 'none'
- ? toolChoice
- : { type: 'function' as const, function: { name: toolChoice } };
- return this.chatCompletion({
- messages,
- tools,
- toolChoice: choice,
- });
- }
- /**
- * 执行函数调用循环
- * @param messages 初始消息
- * @param functions 函数定义
- * @param functionExecutor 函数执行器
- * @param maxIterations 最大迭代次数
- */
- async runFunctionLoop(
- messages: ChatMessage[],
- functions: FunctionDefinition[],
- functionExecutor: (name: string, args: Record<string, unknown>) => Promise<string>,
- maxIterations: number = 10
- ): Promise<string> {
- const conversationMessages = [...messages];
- for (let i = 0; i < maxIterations; i++) {
- const response = await this.chatWithFunctions(conversationMessages, functions);
- if (!response.toolCalls || response.toolCalls.length === 0) {
- return response.content;
- }
- // 添加助手消息(包含工具调用)
- conversationMessages.push({
- role: 'assistant',
- content: response.content || '',
- });
- // 执行每个工具调用
- for (const toolCall of response.toolCalls) {
- try {
- const args = JSON.parse(toolCall.function.arguments);
- const result = await functionExecutor(toolCall.function.name, args);
- conversationMessages.push({
- role: 'tool',
- content: result,
- tool_call_id: toolCall.id,
- });
- } catch (error) {
- conversationMessages.push({
- role: 'tool',
- content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
- tool_call_id: toolCall.id,
- });
- }
- }
- }
- throw new Error('Function loop exceeded maximum iterations');
- }
- // ==================== 视觉理解 ====================
- /**
- * 图像理解
- * @param options 视觉选项
- */
- async analyzeImage(options: VisionOptions): Promise<string> {
- this.ensureAvailable();
- const { model = this.models.vision, prompt, imageUrl, imageBase64, maxTokens = 1000 } = options;
- if (!imageUrl && !imageBase64) {
- throw new Error('Either imageUrl or imageBase64 must be provided');
- }
- const imageContent = imageUrl
- ? { type: 'image_url' as const, image_url: { url: imageUrl } }
- : { type: 'image_url' as const, image_url: { url: `data:image/jpeg;base64,${imageBase64}` } };
- const startTime = Date.now();
- const requestId = `vision_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- const imageSize = imageBase64 ? Math.round(imageBase64.length / 1024) : 0;
- logger.info(`[AI] ========== Vision Analysis Request ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Model: ${model}`);
- logger.info(`[AI] Image: ${imageUrl ? 'URL' : `Base64 (${imageSize}KB)`}`);
- logger.info(`[AI] Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
- logger.info(`[AI] MaxTokens: ${maxTokens}`);
- try {
- const response = await this.client!.chat.completions.create({
- model,
- messages: [
- {
- role: 'user',
- content: [
- imageContent,
- { type: 'text', text: prompt },
- ],
- },
- ],
- max_tokens: maxTokens,
- });
- const duration = Date.now() - startTime;
- const content = response.choices[0]?.message?.content || '';
- logger.info(`[AI] ========== Vision Analysis Response ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Duration: ${duration}ms`);
- logger.info(`[AI] Finish Reason: ${response.choices[0]?.finish_reason}`);
- if (response.usage) {
- logger.info(`[AI] Tokens - Prompt: ${response.usage.prompt_tokens}, Completion: ${response.usage.completion_tokens}, Total: ${response.usage.total_tokens}`);
- }
- logger.info(`[AI] Response Length: ${content.length} 字符`);
- logger.info(`[AI] Response Preview: ${content.substring(0, 150)}${content.length > 150 ? '...' : ''}`);
- logger.info(`[AI] ================================================`);
- return content;
- } catch (error) {
- const duration = Date.now() - startTime;
- logger.error(`[AI] ========== Vision Analysis Error ==========`);
- logger.error(`[AI] Request ID: ${requestId}`);
- logger.error(`[AI] Duration: ${duration}ms`);
- logger.error(`[AI] Error:`, error);
- logger.error(`[AI] =============================================`);
- throw this.handleError(error);
- }
- }
- // ==================== 文本嵌入 ====================
- /**
- * 生成文本嵌入向量
- * @param options 嵌入选项
- */
- async createEmbedding(options: EmbeddingOptions): Promise<number[][]> {
- this.ensureAvailable();
- const { model = this.models.embedding, input, dimensions } = options;
- const startTime = Date.now();
- const requestId = `embed_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- const inputCount = Array.isArray(input) ? input.length : 1;
- logger.info(`[AI] ========== Embedding Request ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Model: ${model}`);
- logger.info(`[AI] Input Count: ${inputCount}`);
- if (dimensions) logger.info(`[AI] Dimensions: ${dimensions}`);
- try {
- const response = await this.client!.embeddings.create({
- model,
- input,
- dimensions,
- });
- const duration = Date.now() - startTime;
- logger.info(`[AI] ========== Embedding Response ==========`);
- logger.info(`[AI] Request ID: ${requestId}`);
- logger.info(`[AI] Duration: ${duration}ms`);
- logger.info(`[AI] Vectors: ${response.data.length}`);
- logger.info(`[AI] Vector Dimension: ${response.data[0]?.embedding?.length || 0}`);
- logger.info(`[AI] ==========================================`);
- return response.data.map(item => item.embedding);
- } catch (error) {
- const duration = Date.now() - startTime;
- logger.error(`[AI] ========== Embedding Error ==========`);
- logger.error(`[AI] Request ID: ${requestId}`);
- logger.error(`[AI] Duration: ${duration}ms`);
- logger.error(`[AI] Error:`, error);
- logger.error(`[AI] ======================================`);
- throw this.handleError(error);
- }
- }
- /**
- * 计算文本相似度
- * @param text1 文本1
- * @param text2 文本2
- * @returns 相似度分数 (0-1)
- */
- async calculateSimilarity(text1: string, text2: string): Promise<number> {
- const embeddings = await this.createEmbedding({ input: [text1, text2] });
- return this.cosineSimilarity(embeddings[0], embeddings[1]);
- }
- // ==================== JSON 结构化输出 ====================
- /**
- * 生成 JSON 结构化响应
- * @param prompt 提示
- * @param schema JSON Schema 描述
- * @param systemPrompt 系统提示
- */
- async generateJSON<T = unknown>(
- prompt: string,
- schema: string,
- systemPrompt?: string
- ): Promise<T> {
- const systemMessage = systemPrompt ||
- `你是一个数据处理助手。请严格按照用户要求的JSON格式输出,不要添加任何额外的说明文字。
- 输出格式要求:${schema}`;
- const response = await this.chatCompletion({
- messages: [
- { role: 'system', content: systemMessage },
- { role: 'user', content: prompt },
- ],
- responseFormat: { type: 'json_object' },
- temperature: 0.3,
- });
- try {
- return JSON.parse(response.content) as T;
- } catch {
- // 尝试提取 JSON
- const jsonMatch = response.content.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
- if (jsonMatch) {
- return JSON.parse(jsonMatch[0]) as T;
- }
- throw new Error('Failed to parse JSON response');
- }
- }
- // ==================== 业务场景方法 ====================
- /**
- * 生成视频标题
- */
- async generateTitle(params: {
- description: string;
- platform: string;
- maxLength?: number;
- }): Promise<string[]> {
- const { description, platform, maxLength = 50 } = params;
- const response = await this.chat(
- `请根据以下视频内容描述,生成5个不同风格的标题:\n${description}`,
- `你是一个专业的自媒体运营专家,擅长为${platform}平台创作吸引人的标题。标题长度不超过${maxLength}个字符。每个标题独占一行,不要添加序号。`
- );
- const titles = response
- .split('\n')
- .filter(line => line.trim())
- .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
- .filter(title => title.length > 0 && title.length <= maxLength);
- return titles.slice(0, 5);
- }
- /**
- * 生成标签推荐
- */
- async generateTags(params: {
- title: string;
- description?: string;
- platform: string;
- maxTags?: number;
- }): Promise<string[]> {
- const { title, description, platform, maxTags = 5 } = params;
- const response = await this.chat(
- `请根据以下视频信息推荐${maxTags}个相关标签:\n标题:${title}\n${description ? `描述:${description}` : ''}`,
- `你是一个专业的自媒体运营专家,擅长为${platform}平台选择热门标签。每个标签不要带#号,用逗号分隔,只输出标签不要其他内容。`
- );
- const tags = response
- .split(/[,,\n]/)
- .map(tag => tag.trim().replace(/^#/, ''))
- .filter(tag => tag.length > 0);
- return tags.slice(0, maxTags);
- }
- /**
- * 优化内容描述
- */
- async optimizeDescription(params: {
- original: string;
- platform: string;
- maxLength?: number;
- }): Promise<string> {
- const { original, platform, maxLength = 500 } = params;
- const response = await this.chat(
- `请优化以下视频描述:\n${original}`,
- `你是一个专业的自媒体运营专家,擅长为${platform}平台优化视频描述。优化后的描述要吸引人、有互动性,长度不超过${maxLength}个字符。直接输出优化后的描述,不要添加其他说明。`
- );
- return response.slice(0, maxLength);
- }
- /**
- * 生成评论回复
- */
- async generateReply(params: {
- comment: string;
- authorName: string;
- context?: string;
- tone?: 'friendly' | 'professional' | 'humorous';
- }): Promise<string[]> {
- const { comment, authorName, context, tone = 'friendly' } = params;
- const toneDesc = {
- friendly: '友好亲切',
- professional: '专业正式',
- humorous: '幽默风趣',
- };
- const response = await this.chat(
- `粉丝"${authorName}"的评论:${comment}\n${context ? `视频内容:${context}` : ''}\n请生成3个不同的回复选项,每个回复独占一行。`,
- `你是一个自媒体账号运营人员,需要用${toneDesc[tone]}的语气回复粉丝评论。回复要简洁、有互动性,表达对粉丝的感谢。`
- );
- const replies = response
- .split('\n')
- .filter(line => line.trim())
- .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
- .filter(reply => reply.length > 0);
- return replies.slice(0, 3);
- }
- /**
- * 推荐最佳发布时间
- */
- async recommendPublishTime(params: {
- platform: string;
- contentType: string;
- targetAudience?: string;
- }): Promise<{ time: string; reason: string }[]> {
- const { platform, contentType, targetAudience } = params;
- try {
- const result = await this.generateJSON<{ time: string; reason: string }[]>(
- `请为${platform}平台的${contentType}类型内容推荐3个最佳发布时间。${targetAudience ? `目标受众:${targetAudience}` : ''}`,
- '返回JSON数组,每项包含time(HH:mm格式)和reason字段',
- '你是一个数据分析专家,熟悉各自媒体平台的用户活跃规律。'
- );
- return Array.isArray(result) ? result : [];
- } catch {
- return [
- { time: '12:00', reason: '午休时间,用户活跃度高' },
- { time: '18:00', reason: '下班时间,通勤路上刷手机' },
- { time: '21:00', reason: '晚间黄金时段,用户放松娱乐' },
- ];
- }
- }
- /**
- * 内容审核
- */
- async moderateContent(content: string): Promise<{
- safe: boolean;
- categories: string[];
- suggestion: string;
- }> {
- try {
- return await this.generateJSON(
- `请审核以下内容是否合规:\n${content}`,
- '返回JSON对象,包含safe(布尔值)、categories(问题类别数组,如空则为[])、suggestion(修改建议)字段',
- '你是一个内容审核专家,需要检查内容是否包含:违法违规、色情低俗、暴力血腥、政治敏感、虚假信息等问题。'
- );
- } catch {
- return { safe: true, categories: [], suggestion: '' };
- }
- }
- /**
- * 内容摘要生成
- */
- async summarize(content: string, maxLength: number = 200): Promise<string> {
- return this.quickChat(
- `请将以下内容总结为不超过${maxLength}字的摘要:\n${content}`,
- '你是一个专业的文字编辑,擅长提炼核心内容。直接输出摘要,不要添加任何前缀。'
- );
- }
- /**
- * 文本翻译
- */
- async translate(
- text: string,
- targetLang: string = '英文',
- sourceLang?: string
- ): Promise<string> {
- const prompt = sourceLang
- ? `请将以下${sourceLang}文本翻译成${targetLang}:\n${text}`
- : `请将以下文本翻译成${targetLang}:\n${text}`;
- return this.chat(prompt, '你是一个专业翻译,请提供准确、自然的翻译。直接输出翻译结果。');
- }
- /**
- * 关键词提取
- */
- async extractKeywords(text: string, count: number = 5): Promise<string[]> {
- const response = await this.chat(
- `请从以下文本中提取${count}个关键词,用逗号分隔:\n${text}`,
- '你是一个文本分析专家。只输出关键词,不要其他内容。'
- );
- return response.split(/[,,]/).map(k => k.trim()).filter(k => k).slice(0, count);
- }
- // ==================== 登录页面分析 ====================
- /**
- * 分析登录页面状态
- * @param imageBase64 页面截图的 Base64 编码
- * @param platform 平台名称
- * @returns 登录状态分析结果
- */
- async analyzeLoginStatus(imageBase64: string, platform: string): Promise<LoginStatusAnalysis> {
- const prompt = `请分析这张${platform}平台的网页截图,判断以下内容:
- 1. 用户是否已经登录成功?(判断依据:是否能看到用户头像、用户名、个人中心入口、创作者后台等已登录状态的元素)
- 2. 页面上是否有验证码或其他二次验证?(如:图形验证码、滑块验证、短信验证码输入框、扫码验证、人脸识别提示等)
- 3. 如果有验证,是什么类型的验证?
- 4. 简要描述当前页面的状态
- 请严格按照以下JSON格式返回:
- {
- "isLoggedIn": true或false,
- "hasVerification": true或false,
- "verificationType": "captcha"或"sms"或"qrcode"或"face"或"slider"或"other"或null,
- "verificationDescription": "验证的具体描述,如果没有验证则为null",
- "pageDescription": "当前页面状态的简要描述",
- "suggestedAction": "建议用户进行的操作,如果不需要则为null"
- }`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 500,
- });
- // 尝试解析 JSON
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- isLoggedIn: Boolean(result.isLoggedIn),
- hasVerification: Boolean(result.hasVerification),
- verificationType: result.verificationType || undefined,
- verificationDescription: result.verificationDescription || undefined,
- pageDescription: result.pageDescription || '无法解析页面状态',
- suggestedAction: result.suggestedAction || undefined,
- };
- }
- // 如果无法解析 JSON,返回默认值
- return {
- isLoggedIn: false,
- hasVerification: false,
- pageDescription: response,
- };
- } catch (error) {
- logger.error('analyzeLoginStatus error:', error);
- return {
- isLoggedIn: false,
- hasVerification: false,
- pageDescription: '分析失败',
- };
- }
- }
- /**
- * 从页面截图中提取账号信息
- * @param imageBase64 页面截图的 Base64 编码
- * @param platform 平台名称
- * @returns 账号信息提取结果
- */
- async extractAccountInfo(imageBase64: string, platform: string): Promise<AccountInfoExtraction> {
- // 根据平台提供更具体的提示
- const platformHints: Record<string, string> = {
- baijiahao: `
- 百家号平台常见的账号信息位置:
- - 页面左上角或侧边栏可能显示作者名称和头像
- - 侧边栏"首页"或"个人中心"菜单可以进入账号信息页面
- - 头部右上角可能有用户头像和下拉菜单
- - 账号设置页面会显示完整的账号信息`,
- douyin: `
- 抖音平台常见的账号信息位置:
- - 页面顶部或右上角显示用户头像和昵称
- - 点击头像可以进入个人主页
- - 侧边栏可能有"我的"或"个人中心"入口`,
- xiaohongshu: `
- 小红书平台常见的账号信息位置和格式:
- - 页面右上角显示账号名称(昵称),通常在头像旁边
- - 账号名称附近或下方会显示"小红书号:xxxxxxx"或"小红书号:xxxxxxx",这是重要的账号ID
- - 小红书号通常是一串字母数字组合,如"ABC123456"
- - "粉丝 X" 或 "粉丝 X/500" 表示粉丝数量
- - 侧边栏有"笔记管理"、"数据看板"等入口
- - 如果看到类似ID格式的字符串(字母+数字组合),很可能就是小红书号`,
- kuaishou: `
- 快手平台常见的账号信息位置:
- - 页面顶部显示用户信息
- - 侧边栏有创作者中心入口`,
- weixin_video: `
- 视频号平台常见的账号信息位置和格式:
- - 页面顶部显示账号名称(如"轻尘网络")
- - 账号名称下方通常显示"视频号ID:xxxxxxx",这个ID是重要信息,请提取冒号后面的字符串
- - "视频 X" 表示作品数量(X是数字)→ 填入 worksCount
- - "关注者 X" 表示粉丝数量(X是数字)→ 填入 fansCount(注意:视频号用"关注者"表示粉丝)
- - 侧边栏有账号设置入口
- - 头像通常是圆形图片,显示在账号名称左侧`,
- };
- const platformHint = platformHints[platform] || '';
- const prompt = `请仔细分析这张${platform}平台的网页截图,尝试提取以下账号信息:
- 1. 账号名称/昵称(这是最重要的信息,请仔细查找页面上的用户名、作者名、昵称等)
- 2. 账号ID(如果可见)
- 3. 头像描述(如果可见)
- 4. 粉丝数量(如果可见)
- 5. 作品数量(如果可见)
- 6. 其他相关信息
- ${platformHint}
- 【重要提示 - 请务必仔细阅读】:
- - 账号名称可能显示在页面顶部、侧边栏、头像旁边等位置
- - 如果看到任何类似用户名或昵称的文字,请提取出来
- - 即使只找到账号名称,也请返回 found: true
- 【关于数据统计 - 请特别注意区分】:
- - "粉丝"/"粉丝数"/"followers"/"关注者" = 关注该账号的人数 → 填入 fansCount
- - "关注"/"关注数"/"following" = 该账号关注的人数 → 这是关注数,不要填入任何字段
- - "作品"/"作品数"/"视频"/"笔记"/"文章"/"posts"/"videos" = 该账号发布的内容数量 → 填入 worksCount
- - "获赞"/"点赞"/"likes" = 获得的点赞数 → 不要填入作品数
- 【各平台特殊说明】:
- - 视频号:页面上"关注者 X"中的X是粉丝数,"视频 X"中的X是作品数,"视频号ID:xxx"中冒号后的xxx是账号ID
- - 抖音:页面上"抖音号:xxx"或"抖音号:xxx"中冒号后的字符串是账号ID
- - 小红书:页面上"小红书号:xxx"或"小红书号:xxx"中冒号后的字符串是账号ID(通常是字母数字组合如ABC123456)
- 【关于弹窗遮挡】:
- - 如果页面有弹窗、对话框、遮罩层等遮挡了主要内容,请在返回结果中说明
- - 如果因为遮挡无法看清账号信息,请设置 found: false,并在 navigationGuide 中说明"页面有弹窗遮挡,请关闭弹窗后重试"
- 【常见错误 - 请避免】:
- - ❌ 不要把"关注数"(following)当成"作品数"
- - ❌ 不要把"获赞数"当成"作品数"
- - ✅ 作品数通常标注为"作品"、"视频"、"笔记"等
- - ✅ 视频号的"关注者"就是粉丝数
- - 如果页面上没有明确显示作品数量,请返回 worksCount: null
- 如果当前页面确实没有显示任何账号信息,请告诉我应该如何操作才能看到账号信息。
- 请严格按照以下JSON格式返回:
- {
- "found": true或false(是否找到账号信息,只要找到账号名称就算找到),
- "accountName": "账号名称/昵称,如果找不到则为null",
- "accountId": "账号ID(如视频号ID:xxx中的xxx、抖音号:xxx中的xxx、小红书号:xxx中的xxx),如果找不到则为null",
- "avatarDescription": "头像描述,如果看不到则为null",
- "fansCount": "粉丝数量(纯数字,如'关注者 1'则填1,'粉丝 100'则填100),如果看不到则为null",
- "worksCount": "作品数量(纯数字,如'视频 4'则填4,'作品 10'则填10),如果看不到或不确定则为null",
- "otherInfo": "其他相关信息,如果没有则为null",
- "navigationGuide": "如果没找到账号信息,请描述具体的操作步骤(如:点击左侧菜单的'个人中心'),如果已找到则为null"
- }`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 600,
- });
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- let jsonStr = jsonMatch[0];
- // 尝试修复常见的 JSON 格式问题
- try {
- // 1. 尝试直接解析
- const result = JSON.parse(jsonStr);
- return {
- found: Boolean(result.found),
- accountName: result.accountName || undefined,
- accountId: result.accountId || undefined,
- avatarDescription: result.avatarDescription || undefined,
- fansCount: result.fansCount || undefined,
- worksCount: result.worksCount || undefined,
- otherInfo: result.otherInfo || undefined,
- navigationGuide: result.navigationGuide || result.navigationSuggestion || undefined,
- };
- } catch {
- // 2. 修复单引号问题:将单引号替换为双引号(注意处理值中的单引号)
- // 先替换属性名的单引号
- jsonStr = jsonStr.replace(/'([^']+)':/g, '"$1":');
- // 替换值的单引号(排除已经是双引号的)
- jsonStr = jsonStr.replace(/:\s*'([^']*)'/g, ': "$1"');
- // 移除末尾多余的逗号
- jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1');
- try {
- const result = JSON.parse(jsonStr);
- return {
- found: Boolean(result.found),
- accountName: result.accountName || undefined,
- accountId: result.accountId || undefined,
- avatarDescription: result.avatarDescription || undefined,
- fansCount: result.fansCount || undefined,
- worksCount: result.worksCount || undefined,
- otherInfo: result.otherInfo || undefined,
- navigationGuide: result.navigationGuide || result.navigationSuggestion || undefined,
- };
- } catch (innerError) {
- logger.error('extractAccountInfo JSON parse failed after fix attempt:', innerError);
- logger.debug('Original JSON:', jsonMatch[0]);
- logger.debug('Fixed JSON:', jsonStr);
- }
- }
- }
- return {
- found: false,
- navigationGuide: response,
- };
- } catch (error) {
- logger.error('extractAccountInfo error:', error);
- return {
- found: false,
- navigationGuide: '分析失败,请手动查看页面',
- };
- }
- }
- /**
- * 从页面截图和 HTML 中提取账号信息(增强版)
- * @param imageBase64 页面截图的 Base64 编码
- * @param html 页面 HTML 内容
- * @param platform 平台名称
- * @returns 账号信息提取结果
- */
- async extractAccountInfoWithHtml(
- imageBase64: string,
- html: string,
- platform: string
- ): Promise<AccountInfoExtraction> {
- // 截取 HTML 的关键部分(避免太长)
- const maxHtmlLength = 8000;
- let htmlSnippet = html;
- if (html.length > maxHtmlLength) {
- // 提取可能包含账号信息的部分
- const patterns = [
- /视频号ID[::]\s*([a-zA-Z0-9_]+)/,
- /finder-uniq-id[^>]*>([^<]+)/,
- /data-clipboard-text="([^"]+)"/,
- /class="[^"]*nickname[^"]*"[^>]*>([^<]+)/,
- /class="[^"]*avatar[^"]*"/,
- ];
-
- // 找到包含关键信息的片段
- let relevantParts: string[] = [];
- for (const pattern of patterns) {
- const match = html.match(pattern);
- if (match) {
- const index = match.index || 0;
- const start = Math.max(0, index - 200);
- const end = Math.min(html.length, index + 500);
- relevantParts.push(html.substring(start, end));
- }
- }
-
- if (relevantParts.length > 0) {
- htmlSnippet = relevantParts.join('\n...\n');
- } else {
- htmlSnippet = html.substring(0, maxHtmlLength);
- }
- }
- const prompt = `请分析以下${platform}平台的网页截图和 HTML 代码,提取账号信息。
- 【HTML 代码片段】
- \`\`\`html
- ${htmlSnippet}
- \`\`\`
- 【提取目标】
- 1. **账号ID**(最重要!):
- - 视频号:查找 "视频号ID:xxx" 或 HTML 中的 data-clipboard-text 属性、finder-uniq-id 元素
- - 示例:sphjl99GV2W1GgN(这种字母数字组合就是视频号ID)
- 2. 账号名称/昵称
- 3. 粉丝数量(视频号显示为"关注者 X")
- 4. 作品数量(视频号显示为"视频 X")
- 【特别注意】
- - 视频号ID 通常是一串字母数字组合,如 "sphjl99GV2W1GgN"
- - 在 HTML 中可能出现在 data-clipboard-text 属性中
- - 或者在 class 为 finder-uniq-id 的元素内
- 请严格按照以下 JSON 格式返回:
- {
- "found": true,
- "accountId": "视频号ID(如 sphjl99GV2W1GgN,不要加前缀)",
- "accountName": "账号名称",
- "fansCount": "粉丝数(纯数字)",
- "worksCount": "作品数(纯数字)"
- }`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 500,
- });
- logger.info(`[AIService] extractAccountInfoWithHtml response:`, response);
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- try {
- const result = JSON.parse(jsonMatch[0]);
- return {
- found: Boolean(result.found),
- accountName: result.accountName || undefined,
- accountId: result.accountId || undefined,
- avatarDescription: result.avatarDescription || undefined,
- fansCount: result.fansCount || undefined,
- worksCount: result.worksCount || undefined,
- otherInfo: result.otherInfo || undefined,
- navigationGuide: result.navigationGuide || undefined,
- };
- } catch (parseError) {
- logger.error('extractAccountInfoWithHtml JSON parse error:', parseError);
- }
- }
- return { found: false };
- } catch (error) {
- logger.error('extractAccountInfoWithHtml error:', error);
- return { found: false };
- }
- }
- /**
- * 获取页面操作指导
- * @param imageBase64 页面截图的 Base64 编码
- * @param platform 平台名称
- * @param goal 操作目标(如:"获取账号信息"、"完成登录")
- * @returns 页面操作指导
- */
- async getPageOperationGuide(
- imageBase64: string,
- platform: string,
- goal: string
- ): Promise<PageOperationGuide> {
- // 根据目标添加特定的识别提示
- let additionalHints = '';
- if (goal.includes('上传') || goal.includes('upload')) {
- additionalHints = `
- 【重要】关于视频上传入口的识别提示:
- - 百家号:上传区域通常是一个虚线边框的大区域,包含云朵图标和"点击上传或将文件拖动入此区域"文字,整个虚线框区域都是可点击的上传入口
- - 微信视频号:
- * 如果在首页,需要找"发表视频"按钮(通常是橙色/红色按钮)
- * 如果在发布页面(标题显示"发表动态"),左侧会有一个带"+"号的矩形上传区域,下方有格式说明文字(如"上传时长8小时内...MP4/H.264格式"),点击这个"+"号区域即可上传
- - 小红书:上传区域通常有"上传视频"文字或拖拽区域
- - 抖音:上传区域通常是一个带有"发布视频"或上传图标的区域
- - 快手:找"上传视频"或拖拽上传区域
- - B站:找"投稿"或上传视频按钮
- 如果页面不是发布页面,需要先找到进入发布页面的入口按钮(如"发表视频"、"发布"、"上传"、"投稿"等)。`;
- }
- const prompt = `请分析这张${platform}平台的网页截图,我的目标是:${goal}
- ${additionalHints}
- 请告诉我下一步应该进行什么操作。仔细观察页面上的所有可点击元素,包括:
- - 带有虚线边框的拖拽上传区域(这是很常见的上传入口)
- - 带有"上传"、"发布"、"发表"、"投稿"等文字的按钮
- - 带有云朵、加号、上传箭头等图标的区域
- 如果需要点击某个元素,请尽可能提供:
- 1. 操作类型(点击、输入、滚动、等待、跳转)
- 2. 目标元素的描述(文字内容、位置描述、视觉特征)
- 3. 如果是点击操作,估计目标在截图中的大致位置(假设截图尺寸为 1920x1080,给出x,y坐标)
- 4. 如果是输入操作,需要输入什么内容
- 请严格按照以下JSON格式返回:
- {
- "hasAction": true或false(是否需要执行操作),
- "actionType": "click"或"input"或"scroll"或"wait"或"navigate"或null,
- "targetDescription": "目标元素的文字描述(如:虚线框上传区域、发表视频按钮等)",
- "targetSelector": "可能的CSS选择器,常见的有:[class*='upload'], [class*='drag'], button:has-text('发布'), button:has-text('上传') 等",
- "targetPosition": {"x": 数字, "y": 数字} 或 null,
- "inputText": "需要输入的文字,如果不需要输入则为null",
- "explanation": "操作说明和原因"
- }`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 500,
- });
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- hasAction: Boolean(result.hasAction),
- actionType: result.actionType || undefined,
- targetDescription: result.targetDescription || undefined,
- targetSelector: result.targetSelector || undefined,
- targetPosition: result.targetPosition || undefined,
- inputText: result.inputText || undefined,
- explanation: result.explanation || '无法解析操作指导',
- };
- }
- return {
- hasAction: false,
- explanation: response,
- };
- } catch (error) {
- logger.error('getPageOperationGuide error:', error);
- return {
- hasAction: false,
- explanation: '分析失败',
- };
- }
- }
- /**
- * 分析视频上传进度
- * @param imageBase64 页面截图的base64
- * @param platform 平台名称
- * @returns 上传进度分析结果
- */
- async analyzeUploadProgress(
- imageBase64: string,
- platform: string
- ): Promise<{
- isUploading: boolean;
- isComplete: boolean;
- isFailed: boolean;
- progress: number | null;
- statusDescription: string;
- }> {
- const prompt = `请分析这张${platform}平台的网页截图,判断视频上传的状态。
- 请仔细观察页面上是否有以下元素:
- 1. 上传进度条(通常显示百分比,如 "50%"、"上传中 75%" 等)
- 2. 上传完成标志(如 "上传成功"、"✓"、绿色勾选图标、"100%"、视频预览画面)
- 3. 上传失败标志(如 "上传失败"、"重试"、红色错误提示)
- 4. 视频处理中的提示(如 "处理中"、"转码中")
- 请严格按照以下JSON格式返回:
- {
- "isUploading": true或false(是否正在上传中,有进度条显示但未完成),
- "isComplete": true或false(是否上传完成,进度达到100%或显示成功标志),
- "isFailed": true或false(是否上传失败),
- "progress": 数字或null(当前上传进度百分比,如果能识别到的话,范围0-100),
- "statusDescription": "当前状态的文字描述"
- }`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 300,
- });
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- isUploading: Boolean(result.isUploading),
- isComplete: Boolean(result.isComplete),
- isFailed: Boolean(result.isFailed),
- progress: typeof result.progress === 'number' ? result.progress : null,
- statusDescription: result.statusDescription || '未知状态',
- };
- }
- return {
- isUploading: false,
- isComplete: false,
- isFailed: false,
- progress: null,
- statusDescription: response,
- };
- } catch (error) {
- logger.error('analyzeUploadProgress error:', error);
- return {
- isUploading: false,
- isComplete: false,
- isFailed: false,
- progress: null,
- statusDescription: '分析失败',
- };
- }
- }
- /**
- * 分析点击发布按钮后的发布进度
- * @param imageBase64 页面截图的base64
- * @param platform 平台名称
- * @returns 发布进度分析结果
- */
- async analyzePublishProgress(
- imageBase64: string,
- platform: string
- ): Promise<{
- isPublishing: boolean;
- isComplete: boolean;
- isFailed: boolean;
- progress: number | null;
- needAction: boolean;
- actionDescription: string | null;
- statusDescription: string;
- }> {
- const prompt = `请分析这张${platform}平台的网页截图,判断点击发布按钮后的发布状态。
- 请仔细观察页面上是否有以下元素:
- 1. 发布/上传进度条(通常显示百分比,如 "发布中 50%"、"上传中 79%"、"正在发布..."、进度圈等)
- 2. 后台上传提示(如 "作品上传中,请勿关闭页面"、"上传完成后将自动发布" + 百分比进度)
- 3. 发布成功标志(如 "发布成功"、"已发布"、绿色勾选图标)
- 4. 发布失败标志(如 "发布失败"、"重试"、红色错误提示)
- 5. 需要处理的弹窗(如确认弹窗、验证码弹窗、协议确认等)
- 6. 正在处理中的提示(如 "视频处理中"、"审核中"、loading动画)
- 【重要】特别注意:
- - 抖音等平台在点击发布后可能会跳转到作品管理页面,但页面右下角会有一个小的上传进度框显示"作品上传中,请勿关闭页面 XX%"
- - 只要页面上有任何上传/发布进度条(无论在页面哪个位置),都应该认为发布尚未完成
- - 只有当进度达到100%且没有任何进度提示时,才算发布完成
- 请严格按照以下JSON格式返回:
- {
- "isPublishing": true或false(是否正在发布/上传中,只要有进度条或loading显示就返回true),
- "isComplete": true或false(是否完全完成,进度100%且无任何上传提示才返回true),
- "isFailed": true或false(是否发布失败),
- "progress": 数字或null(当前进度百分比,仔细查找页面上的百分比数字,范围0-100),
- "needAction": true或false(是否需要用户处理某些操作,如确认弹窗),
- "actionDescription": "需要执行的操作描述,如果不需要操作则为null",
- "statusDescription": "当前状态的文字描述(包括进度信息)"
- }`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 400,
- });
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- isPublishing: Boolean(result.isPublishing),
- isComplete: Boolean(result.isComplete),
- isFailed: Boolean(result.isFailed),
- progress: typeof result.progress === 'number' ? result.progress : null,
- needAction: Boolean(result.needAction),
- actionDescription: result.actionDescription || null,
- statusDescription: result.statusDescription || '未知状态',
- };
- }
- return {
- isPublishing: false,
- isComplete: false,
- isFailed: false,
- progress: null,
- needAction: false,
- actionDescription: null,
- statusDescription: response,
- };
- } catch (error) {
- logger.error('analyzePublishProgress error:', error);
- return {
- isPublishing: false,
- isComplete: false,
- isFailed: false,
- progress: null,
- needAction: false,
- actionDescription: null,
- statusDescription: '分析失败',
- };
- }
- }
- /**
- * 通过 HTML 分析页面并返回操作指导
- * @param html 页面 HTML 内容
- * @param platform 平台名称
- * @param goal 操作目标
- * @returns 页面操作指导(包含精确的 CSS 选择器)
- */
- async analyzeHtmlForOperation(
- html: string,
- platform: string,
- goal: string
- ): Promise<PageOperationGuide> {
- this.ensureAvailable();
- // 简化 HTML,移除不必要的内容,保留关键元素
- const simplifiedHtml = this.simplifyHtml(html);
- const prompt = `你是一个网页自动化助手。我正在${platform}平台上操作,目标是:${goal}
- 以下是当前页面的HTML结构(已简化):
- \`\`\`html
- ${simplifiedHtml}
- \`\`\`
- 请分析这个页面,告诉我下一步应该进行什么操作来达成目标。
- 要求:
- 1. 识别页面当前状态(是否已登录、是否有验证码、是否有弹窗等)
- 2. 找出需要操作的目标元素(按钮、链接、输入框等)
- 3. 提供精确的 CSS 选择器来定位该元素
- 4. 选择器要尽可能唯一和稳定(优先使用 id、data-* 属性、唯一 class)
- 请严格按照以下JSON格式返回:
- {
- "hasAction": true或false,
- "actionType": "click" | "input" | "scroll" | "wait" | null,
- "targetSelector": "精确的CSS选择器,如 #login-btn 或 button[data-action='login'] 或 .login-button",
- "targetDescription": "目标元素的描述",
- "inputText": "如果是输入操作,需要输入的内容,否则为null",
- "explanation": "操作说明和当前页面状态分析"
- }
- 【重要】CSS选择器格式要求:
- - 必须使用标准CSS选择器语法,不要使用jQuery语法
- - 不要使用 :contains()(这是jQuery语法,不是标准CSS)
- - 如果需要按文本匹配,使用 :has-text("文本") 格式,例如:button:has-text("发布")
- - 优先使用 id 选择器,其次是 data-* 属性,再次是唯一的 class
- - 如果有多个相似元素,使用更具体的选择器(如 :first-child, :nth-child(n))
- - 如果页面已完成目标(如已登录成功),返回 hasAction: false`;
- try {
- const response = await this.chat(prompt, undefined, this.models.chat);
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- hasAction: Boolean(result.hasAction),
- actionType: result.actionType || undefined,
- targetDescription: result.targetDescription || undefined,
- targetSelector: result.targetSelector || undefined,
- inputText: result.inputText || undefined,
- explanation: result.explanation || '无法解析操作指导',
- };
- }
- return {
- hasAction: false,
- explanation: response,
- };
- } catch (error) {
- logger.error('analyzeHtmlForOperation error:', error);
- return {
- hasAction: false,
- explanation: '分析失败',
- };
- }
- }
- /**
- * 简化 HTML,移除不必要的内容
- */
- private simplifyHtml(html: string): string {
- let simplified = html;
- // 移除 script 标签及内容
- simplified = simplified.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
- // 移除 style 标签及内容
- simplified = simplified.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
- // 移除 HTML 注释
- simplified = simplified.replace(/<!--[\s\S]*?-->/g, '');
- // 移除 svg 内容(保留标签)
- simplified = simplified.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, '<svg></svg>');
- // 移除 noscript
- simplified = simplified.replace(/<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>/gi, '');
- // 移除 data:image 等 base64 内容
- simplified = simplified.replace(/data:[^"'\s]+/g, 'data:...');
- // 移除过长的属性值(如内联样式)
- simplified = simplified.replace(/style="[^"]{100,}"/gi, 'style="..."');
- // 压缩连续空白
- simplified = simplified.replace(/\s+/g, ' ');
- // 限制总长度(避免 token 超限)
- const maxLength = 30000;
- if (simplified.length > maxLength) {
- // 尝试保留 body 部分
- const bodyMatch = simplified.match(/<body[^>]*>([\s\S]*)<\/body>/i);
- if (bodyMatch) {
- simplified = bodyMatch[1];
- }
- // 如果还是太长,截断
- if (simplified.length > maxLength) {
- simplified = simplified.substring(0, maxLength) + '\n... (HTML 已截断)';
- }
- }
- return simplified.trim();
- }
- // ==================== 发布辅助 ====================
- /**
- * 分析发布页面状态
- * @param imageBase64 页面截图的 Base64 编码
- * @param platform 平台名称
- * @returns 发布状态分析结果
- */
- async analyzePublishStatus(imageBase64: string, platform: string): Promise<PublishStatusAnalysis> {
- const prompt = `请分析这张${platform}平台视频发布页面的截图,判断当前的发布状态。
- 请仔细观察页面,判断:
- 1. 是否正在上传/处理视频(显示进度条、loading 等)
- 2. 是否发布成功(显示成功提示、跳转到作品列表等)
- 3. 是否发布失败(显示错误提示)
- 4. 是否需要输入验证码(图片验证码、滑块验证、短信验证码等)
- 5. 是否需要进行其他操作才能继续发布(如点击发布按钮、确认信息等)
- 请严格按照以下JSON格式返回:
- {
- "status": "uploading" 或 "processing" 或 "success" 或 "failed" 或 "need_captcha" 或 "need_action",
- "captchaType": "image" 或 "sms" 或 "slider" 或 "other" 或 null(仅当 status 为 need_captcha 时填写),
- "captchaDescription": "验证码的具体描述(如:请输入图片中的4位数字)",
- "errorMessage": "错误信息(仅当 status 为 failed 时填写)",
- "nextAction": {
- "actionType": "click" 或 "input" 或 "wait",
- "targetDescription": "需要操作的目标描述",
- "targetSelector": "目标元素的CSS选择器(如果能推断的话)"
- } 或 null,
- "pageDescription": "当前页面状态的详细描述",
- "confidence": 0-100 之间的数字,表示你对这个判断的信心程度
- }
- 注意:
- - 如果看到"发布成功"、"上传完成"等字样,status 应为 "success"
- - 如果看到验证码输入框、滑块验证等,status 应为 "need_captcha"
- - 如果页面显示发布按钮但还未点击,status 应为 "need_action"
- - 如果页面正在加载或显示进度,status 应为 "uploading" 或 "processing"`;
- try {
- const response = await this.analyzeImage({
- imageBase64,
- prompt,
- maxTokens: 600,
- });
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- status: result.status || 'need_action',
- captchaType: result.captchaType || undefined,
- captchaDescription: result.captchaDescription || undefined,
- errorMessage: result.errorMessage || undefined,
- nextAction: result.nextAction || undefined,
- pageDescription: result.pageDescription || '无法解析页面状态',
- confidence: result.confidence || 50,
- };
- }
- return {
- status: 'need_action',
- pageDescription: response,
- confidence: 30,
- };
- } catch (error) {
- logger.error('analyzePublishStatus error:', error);
- return {
- status: 'need_action',
- pageDescription: '分析失败',
- confidence: 0,
- };
- }
- }
- /**
- * 分析发布页面 HTML 并获取操作指导
- * @param html 页面 HTML 内容
- * @param platform 平台名称
- * @param currentStatus 当前发布状态
- * @returns 操作指导
- */
- async analyzePublishPageHtml(
- html: string,
- platform: string,
- currentStatus: string
- ): Promise<PageOperationGuide> {
- const simplifiedHtml = this.simplifyHtml(html);
- const prompt = `你是一个自动化发布助手。我正在${platform}平台上发布视频,当前状态是:${currentStatus}
- 以下是当前页面的HTML结构(已简化):
- \`\`\`html
- ${simplifiedHtml}
- \`\`\`
- 请分析这个页面,告诉我下一步应该进行什么操作来完成发布。
- 要求:
- 1. 如果需要点击"发布"按钮,找到正确的发布按钮
- 2. 如果需要输入验证码,找到验证码输入框
- 3. 如果需要确认/关闭弹窗,找到相应按钮
- 4. 提供精确的 CSS 选择器来定位目标元素
- 请严格按照以下JSON格式返回:
- {
- "hasAction": true或false,
- "actionType": "click" | "input" | "wait" | null,
- "targetSelector": "精确的CSS选择器",
- "targetDescription": "目标元素的描述",
- "inputText": "如果是输入操作,需要输入的内容,否则为null",
- "explanation": "操作说明"
- }`;
- try {
- const response = await this.chat(prompt, undefined, this.models.chat);
- const jsonMatch = response.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const result = JSON.parse(jsonMatch[0]);
- return {
- hasAction: Boolean(result.hasAction),
- actionType: result.actionType || undefined,
- targetDescription: result.targetDescription || undefined,
- targetSelector: result.targetSelector || undefined,
- inputText: result.inputText || undefined,
- explanation: result.explanation || '无法解析操作指导',
- };
- }
- return {
- hasAction: false,
- explanation: response,
- };
- } catch (error) {
- logger.error('analyzePublishPageHtml error:', error);
- return {
- hasAction: false,
- explanation: '分析失败',
- };
- }
- }
- // ==================== 工具方法 ====================
- /**
- * 计算余弦相似度
- */
- private cosineSimilarity(vec1: number[], vec2: number[]): number {
- if (vec1.length !== vec2.length) {
- throw new Error('Vectors must have the same length');
- }
- let dotProduct = 0;
- let norm1 = 0;
- let norm2 = 0;
- for (let i = 0; i < vec1.length; i++) {
- dotProduct += vec1[i] * vec2[i];
- norm1 += vec1[i] * vec1[i];
- norm2 += vec2[i] * vec2[i];
- }
- return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
- }
- /**
- * 错误处理
- */
- private handleError(error: unknown): Error {
- if (error instanceof OpenAI.APIError) {
- const status = error.status;
- const message = error.message;
- switch (status) {
- case 400:
- return new Error(`请求参数错误: ${message}`);
- case 401:
- return new Error('API Key 无效或已过期');
- case 403:
- return new Error('没有权限访问该模型');
- case 404:
- return new Error('请求的模型不存在');
- case 429:
- return new Error('请求频率超限,请稍后重试');
- case 500:
- return new Error('服务器内部错误,请稍后重试');
- default:
- return new Error(`API 错误 (${status}): ${message}`);
- }
- }
- if (error instanceof Error) {
- return error;
- }
- return new Error('未知错误');
- }
- }
- // 导出单例实例
- export const aiService = new QwenAIService();
- // 为了向后兼容,也导出别名
- export const AIService = QwenAIService;
|