├── .gitignore ├── src ├── types.ts ├── locales │ └── zh.yml ├── utils.ts ├── config.ts └── index.ts ├── tsconfig.json ├── readme.md ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | 3 | node_modules 4 | npm-debug.log 5 | yarn-debug.log 6 | yarn-error.log 7 | tsconfig.tsbuildinfo 8 | 9 | .eslintcache 10 | .DS_Store 11 | .idea 12 | .vscode 13 | TODO.md 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ImageData { 2 | buffer: ArrayBuffer 3 | base64: string 4 | dataUrl: string 5 | } 6 | 7 | export interface parseOnput { 8 | errPath?: string 9 | positive?: Array 10 | uc?: string 11 | } 12 | 13 | // TODO: 接口返回格式 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "target": "es2020", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "composite": true, 9 | "incremental": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node" 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # [koishi-plugin-rryth](https://github.com/MirrorCY/rryth) 2 | 3 | [![downloads](https://img.shields.io/npm/dm/koishi-plugin-rryth?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-rryth) 4 | 5 | 本插件基于 [novelai-bot](https://github.com/koishijs/novelai-bot) 修改完成。 6 | 7 | 得益于 Koishi 的插件化机制,只需配合其他插件即可实现更多功能: 8 | 9 | - 多平台支持 (QQ、Discord、Telegram、开黑啦等) 10 | - 速率限制 (限制每个用户每天可以调用的次数和每次调用的间隔) 11 | - 上下文管理 (限制在哪些群聊中哪些用户可以访问) 12 | - 自动翻译中文关键词(通过接入翻译服务) 13 | - 审查图片是否安全(通过接入审核服务) 14 | 15 | **所以所以快去给 [Koishi](https://github.com/koishijs/koishi) 点个 star 吧!** 16 | 17 | ## 使用教程 18 | 19 | - 安装并启用插件,开箱即用,无需任何配置 20 | - 在沙盒里输入 `rr` 即可看到此插件的帮助啦 21 | 22 | 有任何问题都可以提 issue 我会尽快回复~ 23 | 24 | 没有 Github 账号的小伙伴可以来群里找我 @42 25 | 26 | ![QR](https://simx.elchapo.cn/NovelAI.png)![rryth 人人有图画群聊二维码](https://user-images.githubusercontent.com/37006258/213943065-7286a687-b373-45ad-ab69-5051a5f1aea8.png) 27 | 28 | 29 | ## FOSSA Status 30 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FMirrorCY%2Frryth.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FMirrorCY%2Frryth?ref=badge_large) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shigma & Ninzore , 2022 42 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/locales/zh.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | rryth: 3 | description: 人人有图画计划 2.4.3 4 | usage: |- 5 | 输入 sai 空格提示词即可画画,提示词是用逗号分隔的英文 6 | 示例 sai -r 512x768 -t 28 -c 7 tree, yellow -u green 7 | 本插件基于 novelai-bot 修改完成,旨在提供免费画画方案 8 | 好用的话去点个 star https://github.com/MirrorCY/rryth 9 | 10 | options: 11 | resolution: 设定图片比例(1.5x1.2) 12 | seed: 设置随机种子(随意一个数字) 13 | scale: 提示词相关性(0-20) 14 | strength: 图片修改强度(0-1) 15 | undesired: 反向提示词(加在提示词后) 16 | override: 去除插件附加的提示词 17 | 18 | messages: 19 | expect-prompt: 请输入标签。 20 | expect-image: 请输入图片。 21 | latin-only: 只接受英文输入。 22 | concurrent-jobs: |- 23 | 24 | <>等会再约稿吧,我已经忙不过来了…… 25 | <>是数位板没电了,才…才不是我不想画呢! 26 | <>那你得先教我画画(理直气壮 27 | 28 | waiting: |- 29 | 30 | <>少女绘画中…… 31 | <>在画了在画了 32 | <>你就在此地不要走动,等我给你画一幅 33 | 34 | pending: 在画了在画了,不过前面还有 {0} 个稿…… 35 | file-too-large: 文件体积过大。 36 | unsupported-file-type: 不支持的文件格式。 37 | download-error: 图片解析失败。 38 | unknown-error: 发生未知错误。 39 | response-error: 发生未知错误 ({0})。 40 | empty-response: 服务器返回了空白图片,请稍后重试。 41 | request-failed: 请求失败 ({0}),请稍后重试。 42 | request-timeout: 请求超时。 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koishi-plugin-rryth", 3 | "description": "Generate images by horde", 4 | "version": "2.4.3", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "browser": { 11 | "image-size": false 12 | }, 13 | "author": "42 ", 14 | "contributors": [ 15 | "42 ", 16 | "Lipraty " 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/MirrorCY/rryth.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/MirrorCY/rryth/issues" 25 | }, 26 | "scripts": { 27 | "build": "atsc -b" 28 | }, 29 | "koishi": { 30 | "browser": true, 31 | "service": { 32 | "optional": [ 33 | "translator", 34 | "censor" 35 | ] 36 | }, 37 | "description": { 38 | "zh": "人人有图画计划。无需自行搭建后端的、开箱即用的、免费的、无需登录注册的画画插件" 39 | } 40 | }, 41 | "keywords": [ 42 | "chatbot", 43 | "koishi", 44 | "plugin", 45 | "stablehorde", 46 | "ai", 47 | "paint", 48 | "image", 49 | "generate" 50 | ], 51 | "peerDependencies": { 52 | "koishi": "^4.11.0" 53 | }, 54 | "devDependencies": { 55 | "@koishijs/plugin-help": "^1.2.6", 56 | "@types/node": "^17.0.45", 57 | "atsc": "^1.2.2", 58 | "koishi": "^4.11.1", 59 | "sass": "^1.57.1", 60 | "typescript": "^4.9.4" 61 | }, 62 | "dependencies": { 63 | "image-size": "^1.0.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Context, Dict, pick, Quester } from 'koishi' 2 | import imageSize from 'image-size' 3 | import { ImageData } from './types' 4 | 5 | export interface Size { 6 | width: number 7 | height: number 8 | } 9 | 10 | export function getImageSize(buffer: ArrayBuffer): Size { 11 | if (process.env.KOISHI_ENV === 'browser') { 12 | const blob = new Blob([buffer]) 13 | const image = new Image() 14 | image.src = URL.createObjectURL(blob) 15 | return pick(image, ['width', 'height']) 16 | } else { 17 | return imageSize(Buffer.from(buffer)) 18 | } 19 | } 20 | 21 | export function arrayBufferToBase64(buffer: ArrayBuffer) { 22 | if (process.env.KOISHI_ENV === 'browser') { 23 | let result = '' 24 | const chunk = 8192 25 | for (let index = 0; index < buffer.byteLength; index += chunk) { 26 | result += String.fromCharCode.apply(null, buffer.slice(index, index + chunk)) 27 | } 28 | return btoa(result) 29 | } else { 30 | return Buffer.from(buffer).toString('base64') 31 | } 32 | } 33 | 34 | const MAX_OUTPUT_SIZE = 1048576 35 | const MAX_CONTENT_SIZE = 10485760 36 | const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'] 37 | 38 | export async function download(ctx: Context, url: string, headers = {}): Promise { 39 | if (url.startsWith('data:')) { 40 | const [, type, base64] = url.match(/^data:(image\/\w+);base64,(.*)$/) 41 | if (!ALLOWED_TYPES.includes(type)) { 42 | throw new NetworkError('.unsupported-file-type') 43 | } 44 | const binary = atob(base64) 45 | const result = new Uint8Array(binary.length) 46 | for (let i = 0; i < binary.length; i++) { 47 | result[i] = binary.charCodeAt(i) 48 | } 49 | return { buffer: result, base64, dataUrl: url } 50 | } else { 51 | const head = await ctx.http.head(url, { headers }) 52 | if (+head['content-length'] > MAX_CONTENT_SIZE) { 53 | throw new NetworkError('.file-too-large') 54 | } 55 | const mimetype = head['content-type'] 56 | if (!ALLOWED_TYPES.includes(mimetype)) { 57 | throw new NetworkError('.unsupported-file-type') 58 | } 59 | const buffer = await ctx.http.get(url, { responseType: 'arraybuffer', headers }) 60 | const base64 = arrayBufferToBase64(buffer) 61 | return { buffer, base64, dataUrl: `data:${mimetype};base64,${base64}` } 62 | } 63 | } 64 | 65 | export class NetworkError extends Error { 66 | constructor(message: string, public params = {}) { 67 | super(message) 68 | } 69 | 70 | static catch = (mapping: Dict) => (e: any) => { 71 | if (Quester.isAxiosError(e)) { 72 | const code = e.response?.status 73 | for (const key in mapping) { 74 | if (code === +key) { 75 | throw new NetworkError(mapping[key]) 76 | } 77 | } 78 | } 79 | throw e 80 | } 81 | } 82 | 83 | export interface Size { 84 | width: number 85 | height: number 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { Dict, Schema, Time } from 'koishi' 2 | import { Size } from './utils' 3 | import { parseOnput } from './types' 4 | 5 | const ucPreset = [ 6 | 'nsfw, lowres, bad anatomy, bad hands, text, error, missing fingers', 7 | 'extra digit, fewer digits, cropped, worst quality, low quality', 8 | 'normal quality, jpeg artifacts, signature, watermark, username, blurry', 9 | ].join(', ') 10 | 11 | export interface Options { 12 | enhance: boolean 13 | model: string 14 | resolution: Size 15 | seed: string 16 | scale: number 17 | noise: number 18 | strength: number 19 | } 20 | 21 | export interface PromptConfig { 22 | basePrompt?: string 23 | negativePrompt?: string 24 | forbidden?: string 25 | placement?: 'before' | 'after' 26 | latinOnly?: boolean 27 | nsfw?: boolean 28 | translator?: boolean 29 | } 30 | 31 | export const PromptConfig: Schema = Schema.object({ 32 | basePrompt: Schema.string().role('textarea').description('默认附加的标签。').default('masterpiece, best quality'), 33 | negativePrompt: Schema.string().role('textarea').description('默认附加的反向标签。').default(ucPreset), 34 | forbidden: Schema.string().role('textarea').description('违禁词列表。请求中的违禁词将会被自动删除。').default(''), 35 | placement: Schema.union([ 36 | Schema.const('before' as const).description('置于最前'), 37 | Schema.const('after' as const).description('置于最后'), 38 | ]).description('默认附加标签的位置。').default('after'), 39 | translator: Schema.boolean().description('是否启用自动翻译。').default(false), 40 | latinOnly: Schema.boolean().description('是否只接受英文输入。').default(true), 41 | }).description('输入设置') 42 | 43 | export interface Config extends PromptConfig { 44 | updateInfo?: boolean 45 | token?: string 46 | email?: string 47 | password?: string 48 | maxBatch?: number 49 | anatomy?: boolean 50 | output?: 'default' | 'verbose' | 'minimal' 51 | allowAnlas?: boolean | number 52 | enableUpscale?: boolean 53 | endpoint?: string 54 | headers?: Dict 55 | requestTimeout?: number 56 | recallTimeout?: number 57 | maxConcurrency?: number 58 | weigh?: number 59 | hight?: number 60 | strength?: number 61 | scale?: number 62 | censor?: boolean 63 | } 64 | 65 | export const Config = Schema.intersect([ 66 | 67 | Schema.object({ 68 | weigh: Schema.number().description('默认宽度比').default(1), 69 | hight: Schema.number().description('默认高度比').default(1.3), 70 | strength: Schema.number().description('默认图转图强度').default(0.6), 71 | scale: Schema.number().description('默认提示词相关度').default(11), 72 | censor: Schema.boolean().description('是否启用图像审核。').default(false), 73 | }), 74 | 75 | PromptConfig, 76 | 77 | Schema.object({ 78 | output: Schema.union([ 79 | Schema.const('minimal').description('仅图片'), 80 | Schema.const('default').description('标准输出'), 81 | Schema.const('verbose').description('详细输出'), 82 | ]).description('输出方式。').default('default'), 83 | requestTimeout: Schema.number().role('time').description('当请求超过这个时间时会中止并提示超时。').default(Time.minute), 84 | recallTimeout: Schema.number().role('time').description('图片发送后自动撤回的时间 (设置为 0 以禁用此功能)。').default(0), 85 | maxConcurrency: Schema.number().description('单个频道下的最大并发数量 (设置为 0 以禁用此功能)。').default(0), 86 | }).description('高级设置'), 87 | ]) as Schema 88 | 89 | interface Forbidden { 90 | pattern: string 91 | strict: boolean 92 | } 93 | 94 | export function parseForbidden(input: string) { 95 | return input.trim() 96 | .toLowerCase() 97 | .replace(/,/g, ',') 98 | .split(/(?:,\s*|\s*\n\s*)/g) 99 | .filter(Boolean) 100 | .map((pattern: string) => { 101 | const strict = pattern.endsWith('!') 102 | if (strict) pattern = pattern.slice(0, -1) 103 | pattern = pattern.replace(/[^a-z0-9]+/g, ' ').trim() 104 | return { pattern, strict } 105 | }) 106 | } 107 | 108 | const backslash = /@@__BACKSLASH__@@/g 109 | 110 | export function parseInput(input: string, config: Config, forbidden: Forbidden[], override: boolean): parseOnput { 111 | input = input.toLowerCase() 112 | .replace(/\\\\/g, backslash.source) 113 | .replace(/,/g, ',') 114 | .replace(/(/g, '(') 115 | .replace(/)/g, ')') 116 | 117 | input = input 118 | .split('\\{').map(s => s.replace(/\{/g, '(')).join('\\{') 119 | .split('\\}').map(s => s.replace(/\}/g, ')')).join('\\}') 120 | 121 | input = input 122 | .replace(backslash, '\\') 123 | .replace(/_/g, ' ') 124 | 125 | if (config.latinOnly && /[^\s\w"'“”‘’.,:|\\()\[\]{}-]/.test(input)) { 126 | return { errPath: '.latin-only' } 127 | } 128 | 129 | const negative = [] 130 | const appendToList = (words: string[], input: string) => { 131 | const tags = input.split(/,\s*/g) 132 | if (config.placement === 'before') tags.reverse() 133 | for (let tag of tags) { 134 | tag = tag.trim().toLowerCase() 135 | if (!tag || words.includes(tag)) continue 136 | if (config.placement === 'before') { 137 | words.unshift(tag) 138 | } else { 139 | words.push(tag) 140 | } 141 | } 142 | } 143 | 144 | // extract negative prompts 145 | const capture = input.match(/(,\s*|\s+)(-u\s+|--undesired\s+|negative prompts?:\s*)([\s\S]+)/m) 146 | if (capture?.[3]) { 147 | input = input.slice(0, capture.index).trim() 148 | appendToList(negative, capture[3]) 149 | } 150 | 151 | // remove forbidden words 152 | const positive = input.split(/,\s*/g).filter((word) => { 153 | word = word.replace(/[\x00-\x7f]/g, s => s.replace(/[^0-9a-zA-Z]/, ' ')).replace(/\s+/, ' ').trim() 154 | if (!word) return false 155 | for (const { pattern, strict } of forbidden) { 156 | if (strict && word.split(/\W+/g).includes(pattern)) { 157 | return false 158 | } else if (!strict && word.includes(pattern)) { 159 | return false 160 | } 161 | } 162 | return true 163 | }) 164 | 165 | if (!override) { 166 | appendToList(positive, config.basePrompt) 167 | appendToList(negative, config.negativePrompt) 168 | } 169 | 170 | return { errPath: null, positive: positive, uc: negative.join(', ') } 171 | } 172 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Context, Dict, h, Logger, Quester, Session } from 'koishi' 2 | import { Config, parseForbidden, parseInput } from './config' 3 | import { ImageData } from './types' 4 | import { download, getImageSize, NetworkError, Size } from './utils' 5 | import { } from '@koishijs/translator' 6 | import { } from '@koishijs/plugin-help' 7 | 8 | 9 | export * from './config' 10 | 11 | export const reactive = true 12 | export const name = 'rryth' 13 | 14 | const logger = new Logger(name) 15 | 16 | function handleError(session: Session, err: Error) { 17 | if (Quester.isAxiosError(err)) { 18 | if (err.response?.data) { 19 | logger.error(err.response.data) 20 | return session.text(err.response.data.message) 21 | } 22 | if (err.response?.status === 402) { 23 | return session.text('.unauthorized') 24 | } else if (err.response?.status) { 25 | return session.text('.response-error', [err.response.status]) 26 | } else if (err.code === 'ETIMEDOUT') { 27 | return session.text('.request-timeout') 28 | } else if (err.code) { 29 | return session.text('.request-failed', [err.code]) 30 | } 31 | } 32 | logger.error(err) 33 | return session.text('.unknown-error') 34 | } 35 | 36 | interface Forbidden { 37 | pattern: string 38 | strict: boolean 39 | } 40 | 41 | export function apply(ctx: Context, config: Config) { 42 | ctx.i18n.define('zh', require('./locales/zh')) 43 | 44 | let forbidden: Forbidden[] 45 | const tasks: Dict> = Object.create(null) 46 | const globalTasks = new Set() 47 | 48 | ctx.accept(['forbidden'], (config) => { 49 | forbidden = parseForbidden(config.forbidden) 50 | }, { immediate: true }) 51 | 52 | 53 | const resolution = (source: string): Size => { 54 | const cap = source.match(/^(\d*\.?\d+)[x×](\d*\.?\d+)$/) 55 | if (!cap) throw new Error() 56 | const width = +cap[1] 57 | const height = +cap[2] 58 | return { width, height } 59 | } 60 | 61 | const cmd = ctx.command(`${name} `) 62 | .alias('sai', 'rr') 63 | .userFields(['authority']) 64 | .option('resolution', '-r ', { type: resolution }) 65 | .option('override', '-O') 66 | .option('seed', '-x ') 67 | .option('scale', '-c ') 68 | .option('strength', '-N ') 69 | .option('undesired', '-u ') 70 | .action(async ({ session, options }, input) => { 71 | 72 | if (!input?.trim()) return session.execute(`help ${name}`) 73 | 74 | let imgUrl: string, image: ImageData 75 | input = h.transform(input, { 76 | image(attrs) { 77 | imgUrl = attrs.url 78 | return '' 79 | }, 80 | }) 81 | 82 | if (!input.trim() && !config.basePrompt) { 83 | return session.text('.expect-prompt') 84 | } 85 | 86 | const { errPath, positive, uc } = parseInput(input, config, forbidden, options.override) 87 | let prompt = positive.join(', ') 88 | if (errPath) return session.text(errPath) 89 | 90 | if (config.translator && ctx.translator) { 91 | const zhPromptMap: string[] = prompt.match(/[\u4e00-\u9fa5]+/g) 92 | if (zhPromptMap?.length > 0) { 93 | try { 94 | const translatedMap = (await ctx.translator.translate({ input: zhPromptMap.join(','), target: 'en' })).toLocaleLowerCase().split(',') 95 | zhPromptMap.forEach((t, i) => { 96 | prompt = prompt.replace(t, translatedMap[i]).replace(',', ',') 97 | }) 98 | } catch (err) { 99 | logger.warn(err) 100 | } 101 | } 102 | } 103 | 104 | const seed = options.seed || Math.floor(Math.random() * Math.pow(2, 32)) 105 | 106 | const parameters: Dict = { 107 | seed, 108 | prompt, 109 | uc, 110 | scale: options.scale ?? config.scale ?? 11, 111 | steps: imgUrl ? 50 : 28, 112 | } 113 | 114 | if (imgUrl) { 115 | try { 116 | image = await download(ctx, imgUrl) 117 | } catch (err) { 118 | if (err instanceof NetworkError) { 119 | return session.text(err.message, err.params) 120 | } 121 | logger.error(err) 122 | return session.text('.download-error') 123 | } 124 | 125 | options.resolution ||= getImageSize(image.buffer) 126 | Object.assign(parameters, { 127 | height: options.resolution.height, 128 | width: options.resolution.width, 129 | strength: options.strength ?? config.strength ?? 0.3, 130 | }) 131 | 132 | } else { 133 | options.resolution ||= { height: config.hight, width: config.weigh } 134 | Object.assign(parameters, { 135 | height: options.resolution.height, 136 | width: options.resolution.width, 137 | }) 138 | } 139 | 140 | const id = Math.random().toString(36).slice(2) 141 | if (config.maxConcurrency) { 142 | const store = tasks[session.cid] ||= new Set() 143 | if (store.size >= config.maxConcurrency) { 144 | return session.text('.concurrent-jobs') 145 | } else { 146 | store.add(id) 147 | } 148 | } 149 | 150 | session.send(globalTasks.size 151 | ? session.text('.pending', [globalTasks.size]) 152 | : session.text('.waiting')) 153 | 154 | globalTasks.add(id) 155 | const cleanUp = () => { 156 | tasks[session.cid]?.delete(id) 157 | globalTasks.delete(id) 158 | } 159 | const data = (() => { 160 | const body = { 161 | init_images: image && [image.dataUrl], 162 | prompt: parameters.prompt, 163 | seed: parameters.seed, 164 | negative_prompt: parameters.uc, 165 | cfg_scale: parameters.scale, 166 | width: parameters.width, 167 | height: parameters.height, 168 | denoising_strength: parameters.strength, 169 | steps: parameters.steps, 170 | } 171 | return body 172 | })() 173 | const request = () => ctx.http.axios('https://rryth.elchapo.cn:11000/v2', { 174 | method: 'POST', 175 | timeout: config.requestTimeout, 176 | headers: { 177 | 'api': '42', 178 | ...config.headers, 179 | }, 180 | data, 181 | }).then((res) => { 182 | return res.data.images 183 | }) 184 | 185 | let ret: string[] 186 | while (true) { 187 | try { 188 | ret = await request() 189 | cleanUp() 190 | break 191 | } catch (err) { 192 | cleanUp() 193 | return handleError(session, err) 194 | } 195 | } 196 | 197 | async function getContent() { 198 | const safeImg = config.censor 199 | ? h('censor', h('image', { url: 'data:image/png;base64,' + ret[0] })) 200 | : h('image', { url: 'data:image/png;base64,' + ret[0] }) 201 | const attrs: Dict = { 202 | userId: session.userId, 203 | nickname: session.author?.nickname || session.username, 204 | } 205 | if (config.output === 'minimal') { 206 | return safeImg 207 | } 208 | const result = h('figure') 209 | const lines = [`种子 = ${seed}`] 210 | if (config.output === 'verbose') { 211 | lines.push(`模型 = Anything 3.0`) 212 | lines.push(`提示词相关度 = ${parameters.scale}`) 213 | if (parameters.image) lines.push(`图转图强度 = ${parameters.strength}`) 214 | } 215 | result.children.push(h('message', attrs, lines.join('\n'))) 216 | result.children.push(h('message', attrs, `关键词 = ${prompt}`)) 217 | if (config.output === 'verbose') { 218 | result.children.push(h('message', attrs, `排除关键词 = ${uc}`)) 219 | } 220 | result.children.push(safeImg) 221 | if (config.output === 'verbose') result.children.push(h('message', attrs, `工作站名称 = 42`)) 222 | return result 223 | } 224 | 225 | const ids = await session.send(await getContent()) 226 | 227 | if (config.recallTimeout) { 228 | ctx.setTimeout(() => { 229 | for (const id of ids) { 230 | session.bot.deleteMessage(session.channelId, id) 231 | } 232 | }, config.recallTimeout) 233 | } 234 | }) 235 | } 236 | --------------------------------------------------------------------------------