| |
| import config from '../../lib/config.js' |
| import { MioFunction } from '../../lib/function.js' |
|
|
| |
| const pluginsConfig = config.plugins || {} |
| const earthkConfig = pluginsConfig.earthk || {} |
| const thirdPartyApiKey = earthkConfig.api_key || '' |
| const thirdPartyBindQQ = earthkConfig.bind_qq || '' |
|
|
| export default class drawImage extends MioFunction { |
| constructor() { |
| super({ |
| name: 'drawImage', |
| description: 'Use NovelAI to generate anime-style images using NovelAI-specific prompt format. Provide the final image URL to the user in markdown format: . IMPORTANT: ALWAYS return the final image URL within the markdown format  after generating the image. This function is designed to generate ONLY anime-style images. Prompts containing realistic or photorealistic content are strictly prohibited.', |
| parameters: { |
| type: 'object', |
| properties: { |
| orientation: { |
| type:'string', |
| description: 'Image orientation: horizontal or vertical.', |
| enum: ['horizontal','vertical'], |
| }, |
| prompt: { |
| type:'string', |
| description: 'Positive prompt words describing the image content you want to generate. **The prompt MUST be in English and MUST NOT contain any content intended to generate realistic or photorealistic images.** Use NovelAI\'s tag-based prompt format (comma-separated). Add quality tags like "masterpiece, best quality, ultra-detailed" automatically. Apply weighting using parentheses (e.g., `(keyword:1.2)` for increased emphasis). If the prompt is too short, please add details for backgrounds, hairstyles, clothing, etc. Aim for beautiful, highly detailed results. If the user does not provide the quality tags above, please manually add them. Do NOT specify the source of these additions. Example:"1girl, sparkle \\(honkai: star rail\\), kuroduki \\(pieat\\), (rei \\(sanbonzakura\\):1.2), machi \\(7769\\), meion, :p, from side, from from above, bare shoulders, black gloves, blunt bangs, blush, brown hair, circle facial mark, cleavage, closed mouth, detached sleeves, flower tattoo, fox mask, gloves, hair bell, hair between eyes, hair bow, japanese clothes, index finger raised, large breasts, hand on own face, long hair, looking at viewer, mask on head, red bow, red dress, red eyes, red sleeves, shoulder tattoo, sidelocks, solo, tongue out, twintails, upper body, skindentation, red background, black theme, dark, dutch angle, (wlop:0.5), masterpiece, best quality, good quality, newest, year 2024, year 2023"', |
| }, |
| }, |
| required: ['prompt'], |
| } |
| }) |
| this.func = this.generateImage |
| this.webui_url = 'http://127.0.0.1:7860' |
| this.defaultParams = { |
| sampler_name: 'Euler a', |
| steps: 24, |
| width: 832, |
| height: 1216, |
| cfg_scale: 6, |
| denoising_strength: 0.5, |
| enable_hr: false, |
| hr_upscaler: 'R-ESRGAN 4x+ Anime6B', |
| hr_second_pass_steps: 0, |
| hr_scale: 1.3, |
| negative_prompt: 'modern, recent, old, oldest, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured, long body, lowres, bad anatomy, bad hands, missing fingers, extra fingers, extra digits, fewer digits, cropped, very displeasing, (worst quality, bad quality:1.2), sketch, jpeg artifacts, signature, watermark, username, (censored, bar_censor, mosaic_censor:1.2), simple background, conjoined, bad ai-generated' |
| } |
| this.thirdPartyBaseUrl = 'https://fast-dodo-45.deno.dev' |
|
|
| this.imageRequestCounts = new Map() |
| } |
| async checkLocalSDAvailability() { |
| const timeout = 1000 |
| try { |
| const fetchPromise = fetch(this.webui_url, { |
| method: 'GET', |
| |
| }) |
|
|
| const timeoutPromise = new Promise((_, reject) => { |
| setTimeout(() => { |
| reject(new Error('请求超时')) |
| }, timeout) |
| }) |
|
|
| const response = await Promise.race([fetchPromise, timeoutPromise]) |
|
|
| const available = response.ok |
| logger.info(`本地SD可用性检查: ${available}`) |
| return available |
| } catch (error) { |
| logger.warn(`本地SD不可用: ${error.message}`) |
| return false |
| } |
| } |
| async generateImage(e) { |
| const localSDAvailable = await this.checkLocalSDAvailability() |
| if (localSDAvailable) { |
| logger.info('使用本地Stable Diffusion WebUI.') |
| return this.generateImageLocalSD(e) |
| } else { |
| logger.info('本地Stable Diffusion WebUI不可用,使用第三方服务.') |
| return this.generateImageThirdParty(e) |
| } |
| } |
| async generateImageLocalSD(e) { |
| const { orientation } = e.params |
| const { width, height } = this.defaultParams |
| let newWidth = width |
| let newHeight = height |
|
|
| if (orientation === 'horizontal') { |
| newWidth = height |
| newHeight = width |
| } |
|
|
| const combinedParams = { |
| ...this.defaultParams, |
| prompt: 'masterpiece, best quality, amazing quality, very aesthetic, high resolution, ultra-detailed, absurdres, newest, scenery,' + e.params.prompt + ', BREAK, depth of field, volumetric lighting', |
| width: newWidth, |
| height: newHeight |
| } |
|
|
| try { |
| const response = await fetch(`${this.webui_url}/sdapi/v1/txt2img`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(combinedParams), |
| }) |
| if (!response.ok) { |
| throw new Error(`HTTP错误! 状态: ${response.status}`) |
| } |
| const data = await response.json() |
| logger.debug(`本地SD API 响应状态: ${response.status}`) |
| if (data.images && data.images.length > 0) { |
| const imageBase64 = data.images[0] |
| const imageBuffer = Buffer.from(imageBase64, 'base64') |
| const imageUrl = await this.getImgUrlFromBuffer(e.user.origin, imageBuffer) |
| logger.info('本地SD图像生成成功. URL: ' + imageUrl) |
| return { |
| url: ``, |
| info: data.info |
| } |
| } else { |
| throw new Error('没有生成图像.') |
| } |
| } catch (err) { |
| logger.error('使用本地SD生成图像时出错: ' + err.message) |
| return { error: err.message } |
| } |
| } |
| async generateImageThirdParty(e) { |
| |
| if (!e.user.isAdmin) { |
| const ipAddress = e.user.ip |
| const now = Date.now() |
| let userRequestData = this.imageRequestCounts.get(ipAddress) |
|
|
| if (!userRequestData) { |
| userRequestData = { count: 0, lastResetTime: now } |
| this.imageRequestCounts.set(ipAddress, userRequestData) |
| } |
|
|
| const oneHour = 60 * 60 * 1000 |
| if (now - userRequestData.lastResetTime > oneHour) { |
| |
| userRequestData.count = 0 |
| userRequestData.lastResetTime = now |
| logger.info(`IP ${ipAddress} 的第三方API请求计数已重置.`) |
| } |
|
|
| if (userRequestData.count >= 10) { |
| logger.warn(`IP ${ipAddress} 达到每小时第三方API请求限制 (10次).`) |
| return { |
| success: false, |
| error: 'You have reached the limit of 10 images per hour for the third-party service. Please try again later.' |
| } |
| } |
|
|
| userRequestData.count++ |
| this.imageRequestCounts.set(ipAddress, userRequestData) |
| logger.info(`IP ${ipAddress} 本小时已对第三方API请求 ${userRequestData.count} 张图像.`) |
| } |
|
|
| const { orientation = 'vertical', prompt } = e.params |
| const baseUrl = this.thirdPartyBaseUrl |
| const apikey = thirdPartyApiKey |
| const bindQQ = thirdPartyBindQQ |
| const apiUrl = '/ht2.php?qq=' + bindQQ |
| const url = e.user.origin |
| const recsCheckUrl = '/qx2.php?tk=' + apikey + '&qq=' + bindQQ |
| let recsResponse |
| try { |
| logger.info('检查剩余第三方API调用次数...') |
| recsResponse = await fetch(baseUrl + recsCheckUrl) |
| logger.debug(`第三方剩余次数检查响应: ${recsResponse.status} ${recsResponse.statusText}`) |
| } catch (error) { |
| console.error(`请求剩余次数时发生错误: ${error.message}`) |
| return { |
| success: false, |
| error: '请求系统状态时发生错误,请稍后再试。' |
| } |
| } |
| const recsData = await recsResponse.json() |
| logger.json(recsData) |
| logger.info(`剩余第三方API调用次数: ${recsData.recs}`) |
| if (recsData.recs <= 0) { |
| logger.warn('第三方API调用次数已达到限制.') |
| return { |
| success: false, |
| error: 'The system is currently unable to process the request, please try again later.' |
| } |
| } |
| let changdu, kuandu |
| if (orientation !== 'horizontal') { |
| changdu = 768 |
| kuandu = 512 |
| } else { |
| changdu = 512 |
| kuandu = 768 |
| } |
| const guimo = 7 |
| const moxing = 'Euler a' |
| const pc = '(easynegative:1.1), (verybadimagenegative_v1.3:1), (low quality:1.2), (worst quality:1.2)' |
| const requestBody = { |
| prompt: prompt, |
| width: kuandu, |
| height: changdu, |
| cfg_scale: guimo + 2, |
| sampler: moxing, |
| steps: 23, |
| seed: -1, |
| n_samples: 1, |
| ucPreset: 0, |
| negative_prompt: pc, |
| my: apikey |
| } |
| const maxRetries = 3 |
| const delayBetweenRetries = 2000 |
| for (let attempt = 0; attempt < maxRetries; attempt++) { |
| try { |
| logger.info(`尝试 ${attempt + 1}: 调用第三方API...`) |
| const response = await fetch(baseUrl + apiUrl, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': 'Bearer', |
| }, |
| body: JSON.stringify(requestBody), |
| }) |
| if (!response.ok) { |
| console.error(`尝试 ${attempt + 1}: API请求失败: ${response.status} ${response.statusText}`) |
| const errorText = await response.text() |
| console.error('错误响应:', errorText) |
| logger.warn(`尝试 ${attempt + 1}: 第三方API请求失败: ${response.status} ${errorText}`) |
| if (attempt === maxRetries - 1) { |
| logger.error('达到最大重试次数。第三方服务失败.') |
| return { |
| success: false, |
| error: 'Service is busy, please try again later.', |
| } |
| } |
| } else { |
| const data = await response.json() |
| logger.debug(`第三方API 响应状态: ${response.status}`) |
| const imageBase64 = data.images[0] |
| const imageBuffer = Buffer.from(imageBase64, 'base64') |
| const imageUrl = await this.getImgUrlFromBuffer(url, imageBuffer) |
| logger.info(`尝试 ${attempt + 1}: 第三方图像生成成功. URL: ${imageUrl}`) |
| return { |
| success: true, |
| url: imageUrl, |
| } |
| } |
| } catch (error) { |
| console.error(`尝试 ${attempt + 1}: 发生错误: ${error.message}`) |
| logger.error(`尝试 ${attempt + 1}: 调用第三方API时发生错误: ${error.message}`) |
| if (attempt === maxRetries - 1) { |
| logger.error('达到最大重试次数。由于异常,第三方服务失败.') |
| return { |
| success: false, |
| error: 'Service is busy, please try again later.', |
| } |
| } |
| } |
| await new Promise(resolve => setTimeout(resolve, delayBetweenRetries)) |
| } |
| logger.error('多次重试后,第三方服务仍然失败.') |
| return { success: false, error: 'Failed to generate image after multiple retries.' } |
| } |
| } |