|
|
@@ -1,74 +1,693 @@
|
|
|
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';
|
|
|
|
|
|
/**
|
|
|
- * AI 辅助服务
|
|
|
+ * 消息角色类型
|
|
|
*/
|
|
|
-export class AIService {
|
|
|
+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;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 页面操作指导结果
|
|
|
+ */
|
|
|
+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;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
- * 生成视频标题
|
|
|
+ * 获取可用模型列表
|
|
|
*/
|
|
|
- async generateTitle(params: {
|
|
|
- description: string;
|
|
|
- platform: string;
|
|
|
- maxLength?: number;
|
|
|
- }): Promise<string[]> {
|
|
|
+ getAvailableModels(): typeof config.ai.models {
|
|
|
+ return this.models;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确保服务可用
|
|
|
+ */
|
|
|
+ private ensureAvailable(): void {
|
|
|
if (!this.client) {
|
|
|
- throw new Error('AI service not configured');
|
|
|
+ throw new Error('AI service not configured. Please set DASHSCOPE_API_KEY environment variable.');
|
|
|
}
|
|
|
-
|
|
|
- const { description, platform, maxLength = 50 } = params;
|
|
|
-
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 核心 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: 'gpt-3.5-turbo',
|
|
|
- messages: [
|
|
|
- {
|
|
|
- role: 'system',
|
|
|
- content: `你是一个专业的自媒体运营专家,擅长为${platform}平台创作吸引人的标题。标题长度不超过${maxLength}个字符。`,
|
|
|
+ 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: `请根据以下视频内容描述,生成5个不同风格的标题:\n${description}`,
|
|
|
+ content: [
|
|
|
+ imageContent,
|
|
|
+ { type: 'text', text: prompt },
|
|
|
+ ],
|
|
|
},
|
|
|
],
|
|
|
- temperature: 0.8,
|
|
|
- max_tokens: 500,
|
|
|
+ max_tokens: maxTokens,
|
|
|
});
|
|
|
-
|
|
|
+
|
|
|
+ const duration = Date.now() - startTime;
|
|
|
const content = response.choices[0]?.message?.content || '';
|
|
|
- const titles = content
|
|
|
- .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);
|
|
|
+
|
|
|
+ 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) {
|
|
|
- logger.error('AI generateTitle error:', error);
|
|
|
- throw 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);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成标签推荐
|
|
|
*/
|
|
|
@@ -78,42 +697,21 @@ export class AIService {
|
|
|
platform: string;
|
|
|
maxTags?: number;
|
|
|
}): Promise<string[]> {
|
|
|
- if (!this.client) {
|
|
|
- throw new Error('AI service not configured');
|
|
|
- }
|
|
|
-
|
|
|
const { title, description, platform, maxTags = 5 } = params;
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await this.client.chat.completions.create({
|
|
|
- model: 'gpt-3.5-turbo',
|
|
|
- messages: [
|
|
|
- {
|
|
|
- role: 'system',
|
|
|
- content: `你是一个专业的自媒体运营专家,擅长为${platform}平台选择热门标签。每个标签不要带#号,用逗号分隔。`,
|
|
|
- },
|
|
|
- {
|
|
|
- role: 'user',
|
|
|
- content: `请根据以下视频信息推荐${maxTags}个相关标签:\n标题:${title}\n${description ? `描述:${description}` : ''}`,
|
|
|
- },
|
|
|
- ],
|
|
|
- temperature: 0.7,
|
|
|
- max_tokens: 200,
|
|
|
- });
|
|
|
-
|
|
|
- const content = response.choices[0]?.message?.content || '';
|
|
|
- const tags = content
|
|
|
- .split(/[,,\n]/)
|
|
|
- .map(tag => tag.trim().replace(/^#/, ''))
|
|
|
- .filter(tag => tag.length > 0);
|
|
|
-
|
|
|
- return tags.slice(0, maxTags);
|
|
|
- } catch (error) {
|
|
|
- logger.error('AI generateTags error:', error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
+
|
|
|
+ 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);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 优化内容描述
|
|
|
*/
|
|
|
@@ -122,37 +720,16 @@ export class AIService {
|
|
|
platform: string;
|
|
|
maxLength?: number;
|
|
|
}): Promise<string> {
|
|
|
- if (!this.client) {
|
|
|
- throw new Error('AI service not configured');
|
|
|
- }
|
|
|
-
|
|
|
const { original, platform, maxLength = 500 } = params;
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await this.client.chat.completions.create({
|
|
|
- model: 'gpt-3.5-turbo',
|
|
|
- messages: [
|
|
|
- {
|
|
|
- role: 'system',
|
|
|
- content: `你是一个专业的自媒体运营专家,擅长为${platform}平台优化视频描述。优化后的描述要吸引人、有互动性,长度不超过${maxLength}个字符。`,
|
|
|
- },
|
|
|
- {
|
|
|
- role: 'user',
|
|
|
- content: `请优化以下视频描述:\n${original}`,
|
|
|
- },
|
|
|
- ],
|
|
|
- temperature: 0.7,
|
|
|
- max_tokens: 600,
|
|
|
- });
|
|
|
-
|
|
|
- const content = response.choices[0]?.message?.content || original;
|
|
|
- return content.slice(0, maxLength);
|
|
|
- } catch (error) {
|
|
|
- logger.error('AI optimizeDescription error:', error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
+
|
|
|
+ const response = await this.chat(
|
|
|
+ `请优化以下视频描述:\n${original}`,
|
|
|
+ `你是一个专业的自媒体运营专家,擅长为${platform}平台优化视频描述。优化后的描述要吸引人、有互动性,长度不超过${maxLength}个字符。直接输出优化后的描述,不要添加其他说明。`
|
|
|
+ );
|
|
|
+
|
|
|
+ return response.slice(0, maxLength);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 生成评论回复
|
|
|
*/
|
|
|
@@ -162,49 +739,28 @@ export class AIService {
|
|
|
context?: string;
|
|
|
tone?: 'friendly' | 'professional' | 'humorous';
|
|
|
}): Promise<string[]> {
|
|
|
- if (!this.client) {
|
|
|
- throw new Error('AI service not configured');
|
|
|
- }
|
|
|
-
|
|
|
const { comment, authorName, context, tone = 'friendly' } = params;
|
|
|
-
|
|
|
+
|
|
|
const toneDesc = {
|
|
|
friendly: '友好亲切',
|
|
|
professional: '专业正式',
|
|
|
humorous: '幽默风趣',
|
|
|
};
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await this.client.chat.completions.create({
|
|
|
- model: 'gpt-3.5-turbo',
|
|
|
- messages: [
|
|
|
- {
|
|
|
- role: 'system',
|
|
|
- content: `你是一个自媒体账号运营人员,需要用${toneDesc[tone]}的语气回复粉丝评论。回复要简洁、有互动性,表达对粉丝的感谢。`,
|
|
|
- },
|
|
|
- {
|
|
|
- role: 'user',
|
|
|
- content: `粉丝"${authorName}"的评论:${comment}\n${context ? `视频内容:${context}` : ''}\n请生成3个不同的回复选项。`,
|
|
|
- },
|
|
|
- ],
|
|
|
- temperature: 0.8,
|
|
|
- max_tokens: 300,
|
|
|
- });
|
|
|
-
|
|
|
- const content = response.choices[0]?.message?.content || '';
|
|
|
- const replies = content
|
|
|
- .split('\n')
|
|
|
- .filter(line => line.trim())
|
|
|
- .map(line => line.replace(/^\d+[\.\、\)]\s*/, '').trim())
|
|
|
- .filter(reply => reply.length > 0);
|
|
|
-
|
|
|
- return replies.slice(0, 3);
|
|
|
- } catch (error) {
|
|
|
- logger.error('AI generateReply error:', error);
|
|
|
- throw error;
|
|
|
- }
|
|
|
+
|
|
|
+ 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);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 推荐最佳发布时间
|
|
|
*/
|
|
|
@@ -213,51 +769,635 @@ export class AIService {
|
|
|
contentType: string;
|
|
|
targetAudience?: string;
|
|
|
}): Promise<{ time: string; reason: string }[]> {
|
|
|
- if (!this.client) {
|
|
|
- throw new Error('AI service not configured');
|
|
|
- }
|
|
|
-
|
|
|
const { platform, contentType, targetAudience } = params;
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
- const response = await this.client.chat.completions.create({
|
|
|
- model: 'gpt-3.5-turbo',
|
|
|
- messages: [
|
|
|
- {
|
|
|
- role: 'system',
|
|
|
- content: '你是一个数据分析专家,熟悉各自媒体平台的用户活跃规律。',
|
|
|
- },
|
|
|
- {
|
|
|
- role: 'user',
|
|
|
- content: `请为${platform}平台的${contentType}类型内容推荐3个最佳发布时间。${targetAudience ? `目标受众:${targetAudience}` : ''}\n请以JSON数组格式返回,每项包含time(HH:mm格式)和reason字段。`,
|
|
|
- },
|
|
|
- ],
|
|
|
- temperature: 0.5,
|
|
|
- max_tokens: 300,
|
|
|
- });
|
|
|
-
|
|
|
- const content = response.choices[0]?.message?.content || '[]';
|
|
|
-
|
|
|
- try {
|
|
|
- // 尝试提取 JSON
|
|
|
- const jsonMatch = content.match(/\[[\s\S]*\]/);
|
|
|
- if (jsonMatch) {
|
|
|
- return JSON.parse(jsonMatch[0]);
|
|
|
- }
|
|
|
- } catch {
|
|
|
- // 解析失败,返回默认推荐
|
|
|
- }
|
|
|
-
|
|
|
+ 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('AI recommendPublishTime error:', error);
|
|
|
- throw 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: `
|
|
|
+小红书平台常见的账号信息位置:
|
|
|
+- 页面左侧或顶部显示创作者信息
|
|
|
+- 侧边栏有"个人中心"或"账号管理"入口`,
|
|
|
+ kuaishou: `
|
|
|
+快手平台常见的账号信息位置:
|
|
|
+- 页面顶部显示用户信息
|
|
|
+- 侧边栏有创作者中心入口`,
|
|
|
+ weixin_video: `
|
|
|
+视频号平台常见的账号信息位置:
|
|
|
+- 页面左上角显示账号名称
|
|
|
+- 侧边栏有账号设置入口`,
|
|
|
+ };
|
|
|
+
|
|
|
+ const platformHint = platformHints[platform] || '';
|
|
|
+
|
|
|
+ const prompt = `请仔细分析这张${platform}平台的网页截图,尝试提取以下账号信息:
|
|
|
+
|
|
|
+1. 账号名称/昵称(这是最重要的信息,请仔细查找页面上的用户名、作者名、昵称等)
|
|
|
+2. 账号ID(如果可见)
|
|
|
+3. 头像描述(如果可见)
|
|
|
+4. 粉丝数量(如果可见)
|
|
|
+5. 作品数量(如果可见)
|
|
|
+6. 其他相关信息
|
|
|
+${platformHint}
|
|
|
+
|
|
|
+重要提示:
|
|
|
+- 账号名称可能显示在页面顶部、侧边栏、头像旁边等位置
|
|
|
+- 如果看到任何类似用户名或昵称的文字,请提取出来
|
|
|
+- 即使只找到账号名称,也请返回 found: true
|
|
|
+
|
|
|
+如果当前页面确实没有显示任何账号信息,请告诉我应该如何操作才能看到账号信息。
|
|
|
+
|
|
|
+请严格按照以下JSON格式返回:
|
|
|
+{
|
|
|
+ "found": true或false(是否找到账号信息,只要找到账号名称就算找到),
|
|
|
+ "accountName": "账号名称/昵称,如果找不到则为null",
|
|
|
+ "accountId": "账号ID,如果找不到则为null",
|
|
|
+ "avatarDescription": "头像描述,如果看不到则为null",
|
|
|
+ "fansCount": "粉丝数量(数字),如果看不到则为null",
|
|
|
+ "worksCount": "作品数量(数字),如果看不到则为null",
|
|
|
+ "otherInfo": "其他相关信息,如果没有则为null",
|
|
|
+ "navigationGuide": "如果没找到账号信息,请描述具体的操作步骤(如:点击左侧菜单的'个人中心'),如果已找到则为null"
|
|
|
+}`;
|
|
|
+
|
|
|
+ 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 {
|
|
|
+ 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,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ found: false,
|
|
|
+ navigationGuide: response,
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('extractAccountInfo error:', error);
|
|
|
+ return {
|
|
|
+ found: false,
|
|
|
+ navigationGuide: '分析失败,请手动查看页面',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取页面操作指导
|
|
|
+ * @param imageBase64 页面截图的 Base64 编码
|
|
|
+ * @param platform 平台名称
|
|
|
+ * @param goal 操作目标(如:"获取账号信息"、"完成登录")
|
|
|
+ * @returns 页面操作指导
|
|
|
+ */
|
|
|
+ async getPageOperationGuide(
|
|
|
+ imageBase64: string,
|
|
|
+ platform: string,
|
|
|
+ goal: string
|
|
|
+ ): Promise<PageOperationGuide> {
|
|
|
+ const prompt = `请分析这张${platform}平台的网页截图,我的目标是:${goal}
|
|
|
+
|
|
|
+请告诉我下一步应该进行什么操作。如果需要点击某个元素,请尽可能提供:
|
|
|
+1. 操作类型(点击、输入、滚动、等待、跳转)
|
|
|
+2. 目标元素的描述(文字内容、位置描述)
|
|
|
+3. 如果是点击操作,估计目标在截图中的大致位置(假设截图尺寸为 1920x1080,给出x,y坐标)
|
|
|
+4. 如果是输入操作,需要输入什么内容
|
|
|
+
|
|
|
+请严格按照以下JSON格式返回:
|
|
|
+{
|
|
|
+ "hasAction": true或false(是否需要执行操作),
|
|
|
+ "actionType": "click"或"input"或"scroll"或"wait"或"navigate"或null,
|
|
|
+ "targetDescription": "目标元素的文字描述",
|
|
|
+ "targetSelector": "可能的CSS选择器,如果能推断的话",
|
|
|
+ "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: '分析失败',
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过 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": "操作说明和当前页面状态分析"
|
|
|
}
|
|
|
|
|
|
-export const aiService = new AIService();
|
|
|
+注意:
|
|
|
+- CSS选择器必须能够唯一定位到目标元素
|
|
|
+- 如果有多个相似元素,使用更具体的选择器(如 :first-child, :nth-child(n))
|
|
|
+- 优先使用 id 选择器,其次是 data-* 属性,再次是唯一的 class
|
|
|
+- 如果页面已完成目标(如已登录成功),返回 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;
|