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; } /** * 工具调用结果 */ 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 { 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, callback: StreamCallback ): Promise { 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 { 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 { return this.chat(prompt, systemPrompt, this.models.fast); } /** * 推理对话 - 使用推理模型(适合复杂逻辑问题) */ async reasoningChat(prompt: string, systemPrompt?: string): Promise { return this.chat(prompt, systemPrompt, this.models.reasoning); } /** * 代码生成/分析 - 使用代码模型 */ async codeChat(prompt: string, systemPrompt?: string): Promise { 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 { 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) => Promise, maxIterations: number = 10 ): Promise { 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 { 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 { 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 { 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( prompt: string, schema: string, systemPrompt?: string ): Promise { 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 { 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 { 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 { 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 { 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 { return this.quickChat( `请将以下内容总结为不超过${maxLength}字的摘要:\n${content}`, '你是一个专业的文字编辑,擅长提炼核心内容。直接输出摘要,不要添加任何前缀。' ); } /** * 文本翻译 */ async translate( text: string, targetLang: string = '英文', sourceLang?: string ): Promise { const prompt = sourceLang ? `请将以下${sourceLang}文本翻译成${targetLang}:\n${text}` : `请将以下文本翻译成${targetLang}:\n${text}`; return this.chat(prompt, '你是一个专业翻译,请提供准确、自然的翻译。直接输出翻译结果。'); } /** * 关键词提取 */ async extractKeywords(text: string, count: number = 5): Promise { 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 { 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 { // 根据平台提供更具体的提示 const platformHints: Record = { 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 { // 截取 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 { // 根据目标添加特定的识别提示 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 { 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>/gi, ''); // 移除 style 标签及内容 simplified = simplified.replace(/)<[^<]*)*<\/style>/gi, ''); // 移除 HTML 注释 simplified = simplified.replace(//g, ''); // 移除 svg 内容(保留标签) simplified = simplified.replace(/]*>[\s\S]*?<\/svg>/gi, ''); // 移除 noscript simplified = simplified.replace(/)<[^<]*)*<\/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(/]*>([\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 { 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 { 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;