1 line
20 KiB
JSON
1 line
20 KiB
JSON
{"repo": "afjf123/gh-repo-clone-lioensky-vcptoolbox", "files": {"Plugin/DMXDoubaoGen/DoubaoGen.js": {"10182c50d3d0f903ab9932a96022dd01f605a043": {"t": 1785768923.0086474, "n": 18733, "h": "989dc03f5088e447", "c": "#!/usr/bin/env node\r\nimport axios from \"axios\";\r\nimport fs from 'fs/promises';\r\nimport path from 'path';\r\nimport { v4 as uuidv4 } from 'uuid'; // For generating unique filenames\r\nimport { fileURLToPath } from 'url'; // Needed for file:// URLs\r\nimport mime from 'mime-types'; // To determine MIME type from file path\r\n// VolcEngine SDK (if needed for signing or other functionalities - to be determined)\r\n// import * as VolcEngineSDK from '@volcengine/openapi-sdk'; // Example, actual import may vary\r\n\r\n// --- Configuration (from environment variables set by Plugin.js) ---\r\nconst VOLCENGINE_API_KEY = process.env.VOLCENGINE_API_KEY; // API Key for the image generation service (e.g., DMXAPI). Ensure this is set in config.env.\r\nconst PROJECT_BASE_PATH = process.env.PROJECT_BASE_PATH;\r\nconst SERVER_PORT = process.env.SERVER_PORT;\r\nconst IMAGESERVER_IMAGE_KEY = process.env.IMAGESERVER_IMAGE_KEY; // Key for our own image server\r\nconst VAR_HTTP_URL = process.env.VarHttpUrl; // Read VarHttpUrl from env\r\nconst VAR_HTTPS_URL = process.env.VarHttpsUrl; // Read VarHttps Url from env\r\n\r\n// API specific configurations for DMXAPI\r\nconst DMX_API_CONFIG = {\r\n BASE_URL: 'https://www.dmxapi.cn', // New API Host\r\n IMAGE_GENERATION_ENDPOINT: '/v1/images/generations', // New API Endpoint\r\n MODEL_ID: \"doubao-seedream-4-0-250828\", // New unified model\r\n DEFAULT_PARAMS: {\r\n n: 1, // Number of images to generate, typically fixed at 1 for this API\r\n }\r\n};\r\n\r\n// Helper to validate input arguments\r\nfunction isValidDoubaoGenArgs(args) {\r\n if (!args || typeof args !== 'object' || !args.command) return false;\r\n\r\n // Common validation\r\n if (typeof args.prompt !== 'string' || !args.prompt.trim()) return false;\r\n if (args.seed !== undefined && (typeof args.seed !== 'number' || !Number.isInteger(args.seed) || args.seed < 0)) return false;\r\n\r\n // Command-specific validation\r\n if (args.command === 'DoubaoGenerateImage') {\r\n if (typeof args.resolution !== 'string') return false;\r\n const parts = args.resolution.split('x');\r\n if (parts.length !== 2) return false;\r\n const width = parseInt(parts[0], 10);\r\n const height = parseInt(parts[1], 10);\r\n if (isNaN(width) || isNaN(height)) return false;\r\n if (width < 512 || width > 2048 || height < 512 || height > 2048) return false;\r\n\r\n } else if (args.command === 'DoubaoEditImage') {\r\n if (typeof args.image !== 'string' || !args.image.trim()) return false;\r\n if (typeof args.resolution !== 'string') return false; // Can be \"adaptive\" or \"WxH\"\r\n if (args.guidance_scale !== undefined) {\r\n const scale = parseFloat(args.guidance_scale);\r\n if (isNaN(scale) || scale < 0 || scale > 10) return false;\r\n }\r\n \r\n } else if (args.command === 'DoubaoComposeImage') {\r\n if (typeof args.prompt !== 'string' || !args.prompt.trim()) return false;\r\n \r\n // Find at least one image parameter\r\n const imageKeys = Object.keys(args).filter(k => k.startsWith('image_'));\r\n if (imageKeys.length === 0) return false;\r\n\r\n if (typeof args.resolution !== 'string') return false; // Can be \"adaptive\" or \"WxH\"\r\n if (args.guidance_scale !== undefined) {\r\n const scale = parseFloat(args.guidance_scale);\r\n if (isNaN(scale) || scale < 0 || scale > 10) return false;\r\n }\r\n\r\n } else {\r\n return false; // Unknown command\r\n }\r\n \r\n return true;\r\n}\r\n\r\n// --- Assuming Bearer Token Authentication ---\r\nasync function signRequest() {\r\n if (!VOLCENGINE_API_KEY) {\r\n // This warning is important if the key is missing\r\n console.warn(\"[DoubaoGen Plugin] WARN: API_KEY (e.g., VOLCENGINE_API_KEY in config.env) is not set. Authentication will likely fail.\");\r\n // Decide if to throw an error or let the API call fail\r\n // throw new Error(\"DoubaoGen Plugin Error: API_KEY (e.g., VOLCENGINE_API_KEY in config.env) is required for authentication.\");\r\n }\r\n return {\r\n 'Authorization': `Bearer ${VOLCENGINE_API_KEY}`,\r\n 'Content-Type': 'application/json',\r\n 'Accept': 'application/json', // Added based on DMXAPI example\r\n 'User-Agent': 'DMXAPI/1.0.0 (https://www.dmxapi.cn)' // Added based on DMXAPI example\r\n };\r\n}\r\n\r\n// --- Helper function to process the 'image' parameter ---\r\nasync function getImageData(imageUrl, imageBase64) {\r\n // Priority to imageBase64 if provided (on retry from file fetch)\r\n if (imageBase64) {\r\n // The provided data is a full Data URI, e.g., \"data:image/png;base64,...\"\r\n // The API expects this full string.\r\n return imageBase64;\r\n }\r\n\r\n if (!imageUrl) {\r\n return null;\r\n }\r\n\r\n // Handle Data URI\r\n if (imageUrl.startsWith('data:image/')) {\r\n return imageUrl;\r\n }\r\n\r\n // Handle public https URL\r\n if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\r\n return imageUrl;\r\n }\r\n\r\n // Handle local file URL\r\n if (imageUrl.startsWith('file://')) {\r\n const filePath = fileURLToPath(imageUrl);\r\n try {\r\n const buffer = await fs.readFile(filePath);\r\n const mimeType = mime.lookup(filePath) || 'application/octet-stream';\r\n \r\n // Check file size (10MB limit)\r\n if (buffer.length > 10 * 1024 * 1024) {\r\n throw new Error(\"Image size exceeds the 10MB limit.\");\r\n }\r\n\r\n const base64Image = buffer.toString('base64');\r\n // Construct the Data URI format required by the API\r\n return `data:${mimeType};base64,${base64Image}`;\r\n } catch (e) {\r\n if (e.code === 'ENOENT') {\r\n // Hyper-Stack-Trace: File not found locally, request remote fetch.\r\n const structuredError = new Error(`File not found locally, requesting remote fetch for: ${imageUrl}`);\r\n structuredError.code = 'FILE_NOT_FOUND_LOCALLY';\r\n structuredError.fileUrl = imageUrl;\r\n throw structuredError;\r\n } else {\r\n throw new Error(`Error reading local file: ${e.message}`);\r\n }\r\n }\r\n }\r\n\r\n // If the format is unrecognized, treat it as an error.\r\n throw new Error(`Unsupported image format or protocol. Please use an https:// or file:// URL.`);\r\n}\r\n\r\n\r\nasync function generateImageAndSave(args) {\r\n // Check for essential environment variables\r\n if (!VOLCENGINE_API_KEY) {\r\n throw new Error(\"DoubaoGen Plugin Error: API_KEY (e.g., VOLCENGINE_API_KEY in config.env) environment variable is required.\");\r\n }\r\n if (!PROJECT_BASE_PATH) {\r\n throw new Error(\"DoubaoGen Plugin Error: PROJECT_BASE_PATH environment variable is required for saving images.\");\r\n }\r\n if (!SERVER_PORT) {\r\n throw new Error(\"DoubaoGen Plugin Error: SERVER_PORT environment variable is required for constructing image URL.\");\r\n }\r\n if (!IMAGESERVER_IMAGE_KEY) {\r\n throw new Error(\"DoubaoGen Plugin Error: IMAGESERVER_IMAGE_KEY environment variable is required for constructing image URL.\");\r\n }\r\n if (!VAR_HTTP_URL) {\r\n throw new Error(\"DoubaoGen Plugin Error: VarHttpUrl environment variable is required for constructing image URL.\");\r\n }\r\n\r\n const command = args.command;\r\n if (!command) {\r\n throw new Error(`DoubaoGen Plugin Error: 'command' not specified in arguments.`);\r\n }\r\n\r\n if (!isValidDoubaoGenArgs(args)) {\r\n throw new Error(`DoubaoGen Plugin Error: Invalid arguments for command '${command}': ${JSON.stringify(args)}.`);\r\n }\r\n\r\n // --- Image Data Processing (only for EditImage command) ---\r\n let imageData = null;\r\n let imagesData = [];\r\n\r\n if (command === 'DoubaoEditImage') {\r\n imageData = await getImageData(args.image, args.image_base64);\r\n if (!imageData) {\r\n // This case should ideally not be hit if validation is correct, but as a safeguard.\r\n throw new Error(\"DoubaoGen Plugin Error: 'image' parameter is required and must be processed for the DoubaoEditImage command.\");\r\n }\r\n } else if (command === 'DoubaoComposeImage') {\r\n const imageKeys = Object.keys(args).filter(k => k.startsWith('image_'));\r\n \r\n const indices = imageKeys.map(k => {\r\n const num = k.split('_').pop();\r\n return isNaN(num) ? 0 : parseInt(num, 10);\r\n }).filter(n => n > 0);\r\n\r\n if (indices.length === 0) {\r\n throw new Error(\"DoubaoGen Plugin Error: For DoubaoComposeImage, at least one 'image_N' or 'image_base64_N' (N>0) parameter is required.\");\r\n }\r\n const maxIndex = Math.max(...indices);\r\n\r\n for (let i = 1; i <= maxIndex; i++) {\r\n const imageUrlKey = `image_${i}`;\r\n const imageBase64Key = `image_base64_${i}`;\r\n \r\n const imageUrl = args[imageUrlKey];\r\n const imageBase64 = args[imageBase64Key];\r\n\r\n if (!imageUrl && !imageBase64) {\r\n throw new Error(`DoubaoGen Plugin Error: Image parameters are not continuous. Missing 'image_${i}' or 'image_base64_${i}'.`);\r\n }\r\n\r\n try {\r\n const processedImage = await getImageData(imageUrl, imageBase64);\r\n imagesData.push(processedImage);\r\n } catch (e) {\r\n if (e.code === 'FILE_NOT_FOUND_LOCALLY') {\r\n const enhancedError = new Error(`In multi-image composition, image ${i} (parameter: ${imageUrlKey}) was not found locally and requires remote fetching.`);\r\n enhancedError.code = 'FILE_NOT_FOUND_LOCALLY';\r\n enhancedError.fileUrl = e.fileUrl;\r\n enhancedError.failedParameter = imageUrlKey;\r\n throw enhancedError;\r\n }\r\n throw new Error(`Error processing image ${i} ('${imageUrlKey}'): ${e.message}`);\r\n }\r\n }\r\n }\r\n\r\n // --- Payload Construction ---\r\n const payload = {\r\n model: DMX_API_CONFIG.MODEL_ID,\r\n prompt: args.prompt,\r\n n: DMX_API_CONFIG.DEFAULT_PARAMS.n,\r\n size: args.resolution,\r\n watermark: false\r\n };\r\n\r\n if (command === 'DoubaoEditImage') {\r\n payload.image = imageData; // For single image edit\r\n if (args.guidance_scale !== undefined) {\r\n payload.guidance_scale = args.guidance_scale;\r\n }\r\n if (args.resolution.toLowerCase() === 'adaptive') {\r\n payload.size = 'adaptive';\r\n }\r\n } else if (command === 'DoubaoComposeImage') {\r\n payload.images = imagesData; // For multiple image composition\r\n if (args.guidance_scale !== undefined) {\r\n payload.guidance_scale = args.guidance_scale;\r\n }\r\n if (args.resolution.toLowerCase() === 'adaptive') {\r\n payload.size = 'adaptive';\r\n }\r\n }\r\n \r\n if (args.seed !== undefined) {\r\n payload.seed = args.seed; // Add seed if provided\r\n }\r\n\r\n // console.error(`[DoubaoGen Plugin] Sending payload to DMXAPI: ${JSON.stringify(payload)}`);\r\n\r\n // --- Bearer Token Authentication ---\r\n const headers = await signRequest();\r\n\r\n const dmxApiAxiosInstance = axios.create({\r\n baseURL: DMX_API_CONFIG.BASE_URL,\r\n headers: headers,\r\n timeout: 120000 // 120 second timeout, image generation can be slow\r\n });\r\n \r\n const fullApiUrl = DMX_API_CONFIG.IMAGE_GENERATION_ENDPOINT;\r\n\r\n const response = await dmxApiAxiosInstance.post(\r\n fullApiUrl,\r\n payload\r\n );\r\n\r\n // console.error(`[DoubaoGen Plugin] Received response from DMXAPI: ${JSON.stringify(response.data)}`);\r\n\r\n // Parse the response (assuming OpenAI API V1 compatible structure for images)\r\n // response.data usually contains { created: ..., data: [ { url: ... } or { b64_json: ... } ] }\r\n // If DMXAPI has a different response structure, this part needs adjustment.\r\n let generatedImageUrlOrBase64;\r\n const responseData = response.data?.data?.[0];\r\n\r\n if (responseData?.url) { // Check for a URL\r\n generatedImageUrlOrBase64 = responseData.url;\r\n } else if (responseData?.b64_json) { // Check for base64 data\r\n generatedImageUrlOrBase64 = responseData.b64_json;\r\n // TODO: VolcEngine might specify the image format if returning b64_json (e.g., 'png', 'jpeg')\r\n // For now, we default to 'png' later if extension not found, but API might provide this.\r\n }\r\n\r\n if (!generatedImageUrlOrBase64) {\r\n throw new Error(\"DoubaoGen Plugin Error: Failed to extract image data/URL from DMXAPI response. Response: \" + JSON.stringify(response.data));\r\n }\r\n\r\n let imageBuffer;\r\n let imageExtension = 'png'; // Default extension, adjust if VolcEngine specifies or returns different types\r\n\r\n if (generatedImageUrlOrBase64.startsWith('http')) {\r\n // If it's a URL, download it\r\n const imageResponse = await axios({\r\n method: 'get',\r\n url: generatedImageUrlOrBase64,\r\n responseType: 'arraybuffer',\r\n timeout: 60000 // 60 second timeout for image download\r\n });\r\n imageBuffer = imageResponse.data;\r\n const contentType = imageResponse.headers['content-type'];\r\n if (contentType && contentType.startsWith('image/')) {\r\n imageExtension = contentType.split('/')[1];\r\n } else {\r\n const urlExtMatch = generatedImageUrlOrBase64.match(/\\.([^.?]+)(?:[?#]|$)/);\r\n if (urlExtMatch && urlExtMatch[1]) {\r\n imageExtension = urlExtMatch[1];\r\n }\r\n }\r\n } else {\r\n // If it's base64 data\r\n imageBuffer = Buffer.from(generatedImageUrlOrBase64, 'base64');\r\n // TODO: Determine image type from DMXAPI response if possible, otherwise default (e.g., 'png')\r\n // For example, if API returns content-type for base64 or an explicit format field.\r\n }\r\n \r\n const generatedFileName = `${uuidv4()}.${imageExtension}`;\r\n const doubaoGenImageDir = path.join(PROJECT_BASE_PATH, 'image', 'doubaogen'); // Dir name can be kept or changed\r\n const localImageServerPath = path.join(doubaoGenImageDir, generatedFileName);\r\n\r\n await fs.mkdir(doubaoGenImageDir, { recursive: true });\r\n await fs.writeFile(localImageServerPath, imageBuffer);\r\n // console.error(`[DoubaoGen Plugin] Image saved to: ${localImageServerPath}`);\r\n\r\n const relativeServerPathForUrl = path.join('doubaogen', generatedFileName).replace(/\\\\\\\\/g, '/');\r\n const accessibleImageUrl = `${VAR_HTTP_URL}:${SERVER_PORT}/pw=${IMAGESERVER_IMAGE_KEY}/images/${relativeServerPathForUrl}`;\r\n // const accessibleImageUrl = `${VAR_HTTPS_URL}/pw=${IMAGESERVER_IMAGE_KEY}/images/${relativeServerPathForUrl}`;\r\n\r\n const altText = args.prompt ? args.prompt.substring(0, 80) + (args.prompt.length > 80 ? \"...\" : \"\") : (generatedFileName || \"生成的图片\");\r\n const imageHtml = `<img src=\"${accessibleImageUrl}\" alt=\"${altText}\" width=\"300\">`;\r\n const successMessage = `图片已成功生成!`;\r\n const aiInstructions = `图片已成功生成!\\n\\n` +\r\n `详细信息:\\n` +\r\n `- 图片URL: ${accessibleImageUrl}\\n` +\r\n `- 服务器路径: image/doubaogen/${generatedFileName}\\n` +\r\n `- 文件名: ${generatedFileName}\\n\\n` +\r\n `请务必使用以下HTML <img> 标签将图片直接展示给用户 (您可以调整width属性,建议200-500像素):\\n` +\r\n `${imageHtml}\\n`;\r\n\r\n const base64Image = imageBuffer.toString('base64');\r\n const imageMimeType = `image/${imageExtension}`;\r\n\r\n // Attempt to get the seed from the API response, fallback to payload or N/A\r\n const responseSeed = response.data?.data?.[0]?.seed;\r\n const payloadSeed = payload.seed;\r\n const finalSeed = responseSeed !== undefined ? responseSeed : (payloadSeed !== undefined ? payloadSeed : 'N/A');\r\n\r\n\r\n const result = {\r\n content: [\r\n {\r\n type: 'text',\r\n text: `图片已成功生成!\\n- 提示词: ${args.prompt}\\n- 分辨率: ${args.resolution}\\n- Seed: ${finalSeed}\\n- 可访问URL: ${accessibleImageUrl}\\n请将生成好的图片转发给用户哦。`\r\n },\r\n {\r\n type: 'image_url',\r\n image_url: {\r\n url: `data:${imageMimeType};base64,${base64Image}`\r\n }\r\n }\r\n ],\r\n details: { // Keep details for logging or other purposes if needed\r\n serverPath: `image/doubaogen/${generatedFileName}`,\r\n fileName: generatedFileName,\r\n prompt: args.prompt,\r\n resolution: args.resolution,\r\n seed: finalSeed,\r\n imageUrl: accessibleImageUrl\r\n }\r\n };\r\n\r\n return result;\r\n}\r\n\r\nasync function main() {\r\n let inputChunks = [];\r\n process.stdin.setEncoding('utf8');\r\n\r\n for await (const chunk of process.stdin) {\r\n inputChunks.push(chunk);\r\n }\r\n const inputData = inputChunks.join('');\r\n let parsedArgs;\r\n\r\n try {\r\n if (!inputData.trim()) {\r\n console.log(JSON.stringify({ status: \"error\", error: \"DoubaoGen Plugin Error: No input data received from stdin.\" }));\r\n process.exit(1);\r\n return;\r\n }\r\n parsedArgs = JSON.parse(inputData);\r\n const resultObject = await generateImageAndSave(parsedArgs);\r\n // The result from the plugin should be a string for Plugin.js to parse, so we stringify our result object.\r\n console.log(JSON.stringify({ status: \"success\", result: resultObject }));\r\n } catch (e) {\r\n // Handle Hyper-Stack-Trace for remote file fetching\r\n if (e.code === 'FILE_NOT_FOUND_LOCALLY') {\r\n const errorPayload = {\r\n status: \"error\",\r\n code: e.code,\r\n error: e.message,\r\n fileUrl: e.fileUrl\r\n };\r\n if (e.failedParameter) {\r\n errorPayload.failedParameter = e.failedParameter;\r\n }\r\n console.log(JSON.stringify(errorPayload));\r\n } else {\r\n let detailedError = e.message || \"Unknown error in DoubaoGen plugin\";\r\n if (e.response && e.response.data) {\r\n // If the API returned a specific error message, include it.\r\n detailedError += ` - API Response: ${JSON.stringify(e.response.data)}`;\r\n } else if (e.request) {\r\n detailedError += ` - No response received from API.`;\r\n }\r\n const finalErrorMessage = detailedError.startsWith(\"DoubaoGen Plugin Error:\") ? detailedError : `DoubaoGen Plugin Error: ${detailedError}`;\r\n console.log(JSON.stringify({ status: \"error\", error: finalErrorMessage }));\r\n }\r\n process.exit(1);\r\n }\r\n}\r\n\r\nmain();\r\n"}}}} |