koishi-plugin-p-draw 1.3.6 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +271 -678
- package/lib/i18n.js +126 -152
- package/lib/media.js +22 -22
- package/lib/parse.js +1 -14
- package/lib/workflows.js +169 -285
- package/package.json +1 -1
- package/readme.md +10 -26
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@ const fs = require('fs')
|
|
|
3
3
|
const fsp = require('fs/promises')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
|
-
const { pathToFileURL
|
|
6
|
+
const { pathToFileURL } = require('url')
|
|
7
7
|
|
|
8
8
|
exports.name = 'p-draw'
|
|
9
9
|
|
|
@@ -28,14 +28,9 @@ exports.usage = `
|
|
|
28
28
|
- **联网搜索:** 描述中带 \`联网\` / \`搜索\` / \`查一下\` 等词时,会先联网搜索补充角色设定(需配置 Tavily Key)。
|
|
29
29
|
- **画师组:** \`创建画师组 名称=tags\` \`切换画师组 名称\` \`查看画师组\` \`删除画师组 名称\`
|
|
30
30
|
- **固定角色:** \`添加角色 名称=tags\`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **③ 取消**:不生成
|
|
35
|
-
- 可加 \`--denoise 0.6\` 单独调整强度;未装 ControlNet/IPAdapter 时自动回退普通 img2img。
|
|
36
|
-
`;
|
|
37
|
-
|
|
38
|
-
const { zhCN } = require('./lib/i18n')
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const { zhCN } = require('./lib/i18n')
|
|
39
34
|
exports.Config = Schema.object({
|
|
40
35
|
// ComfyUI 连接
|
|
41
36
|
comfyuiBaseUrl: Schema.string().default('http://127.0.0.1:8188').description('ComfyUI 地址'),
|
|
@@ -48,7 +43,6 @@ exports.Config = Schema.object({
|
|
|
48
43
|
|
|
49
44
|
// 模型文件
|
|
50
45
|
unetName: Schema.string().default('anima-base-v1.0.safetensors').description('主模型文件名(需与 ComfyUI models/diffusion_models 下的文件名完全一致,含连字符;默认 anima-base-v1.0.safetensors)'),
|
|
51
|
-
i2iUnetName: Schema.string().default('anima-base-v1.0.safetensors').description('「换风格(漫画化)」工作流专用主模型文件名。LLLite 权重按 block 数逐块训练,当前 LLLite 权重为 28-block,必须搭配 28-block 模型(anima-base-v1.0 / anima-aesthetic-v1.1);不要用 40-block 的 Anima-2.9B,否则报 depth_embed slices missing。留空则用主模型 unetName'),
|
|
52
46
|
modelParams: Schema.dict(Schema.object({
|
|
53
47
|
samplerName: Schema.string().description('采样器(如 er_sde / res_2s / dpmpp_2m)'),
|
|
54
48
|
scheduler: Schema.string().description('调度器(如 simple / beta57 / normal)'),
|
|
@@ -87,6 +81,7 @@ exports.Config = Schema.object({
|
|
|
87
81
|
activeArtistPreset: Schema.string().default('').description('启用的画师组名称'),
|
|
88
82
|
defaultArtistTags: Schema.string().default('').description('备用画师 tags'),
|
|
89
83
|
styleTags: Schema.string().default('').description('画风 tags'),
|
|
84
|
+
fixedCharacters: Schema.array(Schema.string()).default([]).description('固定角色(格式:角色名=tags;仅用于兼容旧配置,运行时数据保存在数据库)'),
|
|
90
85
|
|
|
91
86
|
// 队列
|
|
92
87
|
queueEnabled: Schema.boolean().default(true).description('启用生成队列(逐张顺序执行)'),
|
|
@@ -98,20 +93,6 @@ exports.Config = Schema.object({
|
|
|
98
93
|
multiPrice: Schema.number().default(900).description('多人指令(p-draw 多人)单张消耗的 P 点'),
|
|
99
94
|
couponPrice: Schema.number().default(3000).description('提示词优化券单价(P 点/张,购买询问时显示;可自动读取 data/p-shop.json 里的价格覆盖)'),
|
|
100
95
|
couponAskTimeout: Schema.number().default(60).description('提示词优化券确认等待时间(秒)'),
|
|
101
|
-
img2imgDenoise: Schema.number().default(0.55).description('普通以图生图(p-draw i2i)的去噪强度,越小越接近原图(建议 0.4-0.7)'),
|
|
102
|
-
i2iMode: Schema.string().default('ask').description('i2i 模式选择方式:ask=每次询问 / style=直接换风格(漫画化)/ ootd=直接换装换姿势 / plain=普通 img2img'),
|
|
103
|
-
i2iAskTimeout: Schema.number().default(60).description('i2i 模式询问等待时间(秒)'),
|
|
104
|
-
seriesAskTimeout: Schema.number().default(60).description('连续图 LLM 使用确认等待时间(秒)'),
|
|
105
|
-
i2iStyleDenoise: Schema.number().default(0.75).description('换风格模式(漫画化)的去噪强度,越大风格变化越彻底(建议 0.7-0.85)'),
|
|
106
|
-
i2iOotdDenoise: Schema.number().default(0.55).description('换装换姿势模式(保留角色)的去噪强度(建议 0.5-0.6)'),
|
|
107
|
-
i2iControlNetStrength: Schema.number().default(0.7).description('换风格模式的 ControlNet-LLLite 强度,越大构图锁得越死(建议 0.5-0.8;需安装 kohya-ss/ComfyUI-Anima-LLLite 节点与权重)'),
|
|
108
|
-
i2iIPAdapterPath: Schema.string().default('').description('Anima IP-Adapter 模型文件路径(换装换姿势模式保脸用;需安装 comfyui-anima-ipadapter 节点并把模型路径填到这里)'),
|
|
109
|
-
i2iIPAdapterWeight: Schema.number().default(0.8).description('换装换姿势模式的 IP-Adapter 权重,越大角色特征保留越强(建议 0.6-1.0)'),
|
|
110
|
-
controlNetModel: Schema.string().default('').description('Anima ControlNet-LLLite 权重文件名(留空自动检测 anima-lllite 系;需放到 ComfyUI/models/controlnet,如 anima-lllite-lineart-test-1.safetensors)'),
|
|
111
|
-
taggerEnabled: Schema.boolean().default(false).description('i2i 前自动识图(需 ComfyUI 安装 WD14 Tagger 节点与模型;识别出的标签会注入提示词优化)'),
|
|
112
|
-
taggerModel: Schema.string().default('wd-v1-4-convnext-tagger-v2').description('识图模型名(WD14 Tagger 节点里可选模型)'),
|
|
113
|
-
taggerThreshold: Schema.number().default(0.35).description('识图标签置信度阈值'),
|
|
114
|
-
taggerCharacterThreshold: Schema.number().default(0.85).description('识图角色标签置信度阈值'),
|
|
115
96
|
adminUsers: Schema.array(Schema.string()).default([]).description('免 P 点管理员用户 ID 列表'),
|
|
116
97
|
outputLogs: Schema.boolean().default(true).description('是否在控制台输出详细日志'),
|
|
117
98
|
|
|
@@ -134,28 +115,27 @@ exports.Config = Schema.object({
|
|
|
134
115
|
'zh-CN': zhCN,
|
|
135
116
|
})
|
|
136
117
|
|
|
137
|
-
// ------------------------------------------------------------------
|
|
138
|
-
// 纯函数库已拆分到 lib/(解析 / tag 清洗 / 工作流 / Comfy 等待 / 多人规划 / HTTP 客户端)
|
|
139
|
-
// ------------------------------------------------------------------
|
|
140
|
-
const {
|
|
141
|
-
normalizeBaseUrl, escapeRe, parseGenerationSize, parseBatchCount, parseSeed,
|
|
142
|
-
stripRawPrefix, splitPositiveNegativePrompt, parseNameTags, parsePresetList, mergeTagText,
|
|
143
|
-
} = require('./lib/parse')
|
|
144
|
-
const {
|
|
145
|
-
splitTags,
|
|
146
|
-
NO_ARTIST_RE, NO_STYLE_RE,
|
|
147
|
-
} = require('./lib/tags')
|
|
148
|
-
const {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
} = require('./lib/
|
|
152
|
-
const {
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} = require('./lib/
|
|
158
|
-
const { buildComfyClient } = require('./lib/http')
|
|
118
|
+
// ------------------------------------------------------------------
|
|
119
|
+
// 纯函数库已拆分到 lib/(解析 / tag 清洗 / 工作流 / Comfy 等待 / 多人规划 / HTTP 客户端)
|
|
120
|
+
// ------------------------------------------------------------------
|
|
121
|
+
const {
|
|
122
|
+
normalizeBaseUrl, escapeRe, parseGenerationSize, parseBatchCount, parseSeed,
|
|
123
|
+
stripRawPrefix, splitPositiveNegativePrompt, parseNameTags, parsePresetList, mergeTagText,
|
|
124
|
+
} = require('./lib/parse')
|
|
125
|
+
const {
|
|
126
|
+
splitTags, joinPromptParts, mergeNegativePrompts, cleanContentTags, appendInlineProtectedTags,
|
|
127
|
+
NO_ARTIST_RE, NO_STYLE_RE,
|
|
128
|
+
} = require('./lib/tags')
|
|
129
|
+
const {
|
|
130
|
+
buildWorkflow,
|
|
131
|
+
} = require('./lib/workflows')
|
|
132
|
+
const { outputImages, waitComfyResult } = require('./lib/comfy')
|
|
133
|
+
const { materializeImageSource } = require('./lib/media')
|
|
134
|
+
const {
|
|
135
|
+
MULTI_PERSON_NEGATIVE_TAGS, buildMultiPersonPlanPrompt, parseMultiPersonPlan,
|
|
136
|
+
renderMultiPersonCharacter, multiPersonAutoSize,
|
|
137
|
+
} = require('./lib/multi')
|
|
138
|
+
const { buildComfyClient } = require('./lib/http')
|
|
159
139
|
exports.apply = async function apply(ctx, cfg) {
|
|
160
140
|
// 注意:不在此处 extend p_system 表 —— 该表由 p-qiandao 等 p 系插件创建。
|
|
161
141
|
// 重复声明同一张表可能导致 Koishi 的 schema 迁移冲突,拖垮签到插件。
|
|
@@ -163,13 +143,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
163
143
|
const logger = ctx.logger('p-draw')
|
|
164
144
|
ctx.i18n.define('zh-CN', zhCN)
|
|
165
145
|
|
|
166
|
-
//
|
|
146
|
+
// 运行时数据持久化到数据库,而不是调用 scope.update
|
|
167
147
|
// 写 koishi.yml:scope.update 会触发插件重载,导致正在生成的图被 dispose(Context has
|
|
168
|
-
// been disposed
|
|
148
|
+
// been disposed),且重复写入时配置文件会被冲掉(曾出现配置整体恢复成默认)。
|
|
169
149
|
try {
|
|
170
150
|
ctx.model.extend('p_draw_config', {
|
|
171
151
|
id: 'unsigned',
|
|
172
152
|
fixed_characters: 'json',
|
|
153
|
+
fixed_characters_migrated: 'boolean',
|
|
173
154
|
artist_presets: 'json',
|
|
174
155
|
active_artist_preset: 'text',
|
|
175
156
|
default_artist_tags: 'text',
|
|
@@ -179,6 +160,16 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
179
160
|
logger.warn(`p_draw_config 表初始化失败:${e.message}`)
|
|
180
161
|
}
|
|
181
162
|
|
|
163
|
+
try {
|
|
164
|
+
ctx.model.extend('p_draw_fixed_characters', {
|
|
165
|
+
id: 'unsigned',
|
|
166
|
+
name: 'string',
|
|
167
|
+
tags: 'text',
|
|
168
|
+
}, { autoInc: true, unique: ['name'] })
|
|
169
|
+
} catch (e) {
|
|
170
|
+
logger.warn(`p_draw_fixed_characters 表初始化失败:${e.message}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
182
173
|
// 用户自选模型偏好(userid -> unet 文件名),持久化在 p_draw_config.user_models
|
|
183
174
|
if (!cfg.userModels || typeof cfg.userModels !== 'object') cfg.userModels = {}
|
|
184
175
|
|
|
@@ -263,12 +254,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
263
254
|
const vaeList = availableModels(objectInfo, 'VAELoader', 'vae_name')
|
|
264
255
|
payload.unet_available = unetList.includes(cfg.unetName)
|
|
265
256
|
payload.unet_models = unetList
|
|
266
|
-
payload.i2i_unet_available = unetList.includes(String(cfg.i2iUnetName || '').trim() || cfg.unetName)
|
|
267
257
|
payload.clip_available = clipList.includes(cfg.clipName)
|
|
268
258
|
payload.vae_available = vaeList.includes(cfg.vaeName)
|
|
269
259
|
} else {
|
|
270
260
|
payload.unet_available = undefined
|
|
271
|
-
payload.i2i_unet_available = undefined
|
|
272
261
|
payload.clip_available = undefined
|
|
273
262
|
payload.vae_available = undefined
|
|
274
263
|
}
|
|
@@ -314,7 +303,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
314
303
|
`版本:${payload.comfyui_version || '未知'}`,
|
|
315
304
|
`GPU:${payload.gpu || '未知'}(显存 ${payload.vram_total_mb}MB / 空闲 ${payload.vram_free_mb}MB)`,
|
|
316
305
|
`主模型:${cfg.unetName} ${modelStatus(cfg.unetName, payload.unet_available)}`,
|
|
317
|
-
`换风格模型:${cfg.i2iUnetName || cfg.unetName} ${modelStatus(cfg.i2iUnetName || cfg.unetName, payload.i2i_unet_available)}`,
|
|
318
306
|
`文本编码器:${cfg.clipName} ${modelStatus(cfg.clipName, payload.clip_available)}`,
|
|
319
307
|
`VAE:${cfg.vaeName} ${modelStatus(cfg.vaeName, payload.vae_available)}`,
|
|
320
308
|
`可用尺寸:${payload.allowed_sizes.join('、')}`,
|
|
@@ -426,7 +414,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
426
414
|
return String(cfg.styleTags).trim()
|
|
427
415
|
}
|
|
428
416
|
|
|
429
|
-
// P
|
|
417
|
+
// P 点余额预检(单图/多人共用)
|
|
430
418
|
async function precheckPoints(session, USERID, isAdmin, totalPrice) {
|
|
431
419
|
if (isAdmin) return { ok: true }
|
|
432
420
|
const notExists = await isAccountExists(USERID)
|
|
@@ -461,56 +449,58 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
461
449
|
return { ok: true, tasks, firstPosition }
|
|
462
450
|
}
|
|
463
451
|
|
|
464
|
-
//
|
|
465
|
-
function feedbackBase(session, { firstPosition, count
|
|
466
|
-
const notices = []
|
|
467
|
-
if (cfg.queueEnabled) {
|
|
468
|
-
notices.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
|
|
469
|
-
if (count > 1) notices.push(session.text('.batch-count', [count]))
|
|
470
|
-
} else {
|
|
471
|
-
notices.push(session.text('.generating'))
|
|
472
|
-
if (count > 1) notices.push(session.text('.batch-count', [count]))
|
|
473
|
-
}
|
|
474
|
-
return {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
452
|
+
// 即时反馈的公共部分。扣费提醒在生成完成后使用实际 seed 发送。
|
|
453
|
+
function feedbackBase(session, { firstPosition, count }) {
|
|
454
|
+
const notices = []
|
|
455
|
+
if (cfg.queueEnabled) {
|
|
456
|
+
notices.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
|
|
457
|
+
if (count > 1) notices.push(session.text('.batch-count', [count]))
|
|
458
|
+
} else {
|
|
459
|
+
notices.push(session.text('.generating'))
|
|
460
|
+
if (count > 1) notices.push(session.text('.batch-count', [count]))
|
|
461
|
+
}
|
|
462
|
+
return { notices }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function buildChargeNotice({ isAdmin, totalPrice, unetName, seeds }) {
|
|
466
|
+
if (isAdmin || !seeds.length) return ''
|
|
467
|
+
return `已扣除 ${totalPrice} P 点,当前模型:${unetName},--seed=${seeds.join(',')}`
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function sendNotices(session, notices, opts = {}) {
|
|
471
|
+
const content = notices.filter(Boolean).join('\n')
|
|
472
|
+
if (!content) return
|
|
473
|
+
try {
|
|
474
|
+
const message = opts.quote && session.messageId ? h.quote(session.messageId) + content : content
|
|
475
|
+
await session.send(message)
|
|
476
|
+
} catch (e) {
|
|
477
|
+
logger.warn(`发送反馈消息失败:${e.message}`)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 生成图片使用合并转发;每张图后紧跟实际使用的正负面提示词,不附原消息引用。
|
|
482
|
+
async function sendImagesAsForward(session, outputs) {
|
|
483
|
+
const nodes = []
|
|
484
|
+
const fallback = []
|
|
485
|
+
for (const output of outputs) {
|
|
486
|
+
const src = typeof output === 'string' ? output : output.src
|
|
487
|
+
const prompt = typeof output === 'string' ? '' : String(output.prompt || '')
|
|
488
|
+
const negativePrompt = typeof output === 'string' ? '' : String(output.negativePrompt || '')
|
|
489
|
+
const materialized = await materializeImageSource(src)
|
|
490
|
+
const image = Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
|
|
491
|
+
nodes.push(h('message', image))
|
|
492
|
+
fallback.push(image)
|
|
493
|
+
const promptText = `Positive:\n${prompt}\n\nNegative:\n${negativePrompt}`
|
|
494
|
+
nodes.push(h('message', promptText))
|
|
495
|
+
fallback.push(promptText)
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
await session.send(h('figure', nodes))
|
|
499
|
+
} catch (e) {
|
|
500
|
+
logger.warn(`发送转发图片失败:${e.message}`)
|
|
501
|
+
await session.send(fallback)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
514
504
|
|
|
515
505
|
// P 点读改写按用户串行化,避免并发指令互相覆盖余额
|
|
516
506
|
const userLocks = new Map()
|
|
@@ -522,17 +512,22 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
522
512
|
}
|
|
523
513
|
|
|
524
514
|
// ---------------- 用户自选模型 ----------------
|
|
525
|
-
// 从 ComfyUI /object_info(10
|
|
515
|
+
// 从 ComfyUI /object_info(10 分钟缓存)读取 UNET 和 checkpoint 模型列表
|
|
526
516
|
async function listUnetModels() {
|
|
527
517
|
try {
|
|
528
518
|
const objectInfo = await getObjectInfoCached()
|
|
529
|
-
const list =
|
|
530
|
-
|
|
519
|
+
const list = [
|
|
520
|
+
...availableModels(objectInfo, 'UNETLoader', 'unet_name'),
|
|
521
|
+
...availableModels(objectInfo, 'CheckpointLoaderSimple', 'ckpt_name')
|
|
522
|
+
.filter(name => String(name).trim().toLowerCase().replace(/[-_]/g, '~') === 'animagine~xl~3.1.safetensors'),
|
|
523
|
+
]
|
|
524
|
+
const unique = [...new Set(list)]
|
|
525
|
+
if (unique.length) return unique
|
|
531
526
|
} catch (e) { /* ignore */ }
|
|
532
527
|
return []
|
|
533
528
|
}
|
|
534
529
|
|
|
535
|
-
//
|
|
530
|
+
// 解析该用户当前生效的模型:有偏好且仍存在于 ComfyUI 时用偏好,否则回落默认
|
|
536
531
|
async function resolveUnet(USERID) {
|
|
537
532
|
const chosen = cfg.userModels && cfg.userModels[USERID]
|
|
538
533
|
if (!chosen || !String(chosen).trim()) return cfg.unetName
|
|
@@ -582,12 +577,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
582
577
|
|
|
583
578
|
async function runComfyGenerate(prompt, size, overrides) {
|
|
584
579
|
const sizes = parseAllowedSizes()
|
|
585
|
-
const requestedWidth = (size && size[0]) || overrides.width || cfg.width
|
|
586
|
-
const requestedHeight = (size && size[1]) || overrides.height || cfg.height
|
|
587
580
|
const unetName = overrides.unet || cfg.unetName
|
|
588
581
|
// 应用该模型的独立参数覆盖(modelParams),再叠加命令级 overrides(overrides.steps/cfg 优先于模型级)
|
|
589
582
|
const modelSpecific = resolveModelParams(unetName)
|
|
590
583
|
const workCfg = Object.assign({}, cfg, modelSpecific, { unetName })
|
|
584
|
+
const requestedWidth = (size && size[0]) || overrides.width || workCfg.width
|
|
585
|
+
const requestedHeight = (size && size[1]) || overrides.height || workCfg.height
|
|
591
586
|
// 防御:sampler/scheduler 配置若带尾随空格会导致 ComfyUI 报 "Value not in list",统一 trim
|
|
592
587
|
if (typeof workCfg.samplerName === 'string') workCfg.samplerName = workCfg.samplerName.trim()
|
|
593
588
|
if (typeof workCfg.scheduler === 'string') workCfg.scheduler = workCfg.scheduler.trim()
|
|
@@ -597,14 +592,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
597
592
|
const cfgVal = Number(overrides.cfg) || workCfg.cfg
|
|
598
593
|
const seed = Number(overrides.seed) || crypto.randomInt(1, 2 ** 32 - 1)
|
|
599
594
|
const negativePrompt = joinPromptParts([overrides.negativePrompt || cfg.negativePrompt || ''])
|
|
600
|
-
const
|
|
601
|
-
if (i2iImage && cfg.customWorkflowEnabled && cfg.customWorkflowPath) {
|
|
602
|
-
return { ok: false, message: 'i2i(以图生图)暂不支持自定义工作流(customWorkflowEnabled),请关闭后再试。' }
|
|
603
|
-
}
|
|
604
|
-
const i2iOpts = overrides.i2i || {}
|
|
605
|
-
const promptBody = i2iImage
|
|
606
|
-
? buildI2IWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, i2iImage, i2iOpts.mode || 'plain', i2iOpts.denoise != null ? i2iOpts.denoise : null, i2iOpts.caps || null).promptBody
|
|
607
|
-
: buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
595
|
+
const promptBody = buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
608
596
|
|
|
609
597
|
const clientId = crypto.randomUUID()
|
|
610
598
|
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
@@ -664,10 +652,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
664
652
|
width,
|
|
665
653
|
height,
|
|
666
654
|
steps,
|
|
667
|
-
cfg: cfgVal,
|
|
668
|
-
prompt_id: promptId,
|
|
669
|
-
negativePrompt,
|
|
670
|
-
}
|
|
655
|
+
cfg: cfgVal,
|
|
656
|
+
prompt_id: promptId,
|
|
657
|
+
negativePrompt,
|
|
658
|
+
}
|
|
671
659
|
}
|
|
672
660
|
|
|
673
661
|
// ---------------- LLM 提示词优化 ----------------
|
|
@@ -709,9 +697,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
709
697
|
}
|
|
710
698
|
|
|
711
699
|
// 把命中的固定角色 tags 渲染进优化模板的 {character_rule} 占位符。
|
|
712
|
-
function
|
|
700
|
+
async function fixedCharacterRows(query = {}) {
|
|
701
|
+
const rows = await ctx.database.get('p_draw_fixed_characters', query)
|
|
702
|
+
return rows.sort((a, b) => Number(a.id) - Number(b.id))
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function buildCharacterRule(prompt) {
|
|
713
706
|
const text = String(prompt || '')
|
|
714
|
-
for (const
|
|
707
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
715
708
|
if (name && text.includes(name)) {
|
|
716
709
|
return `用户提到了固定角色「${name}」,其外观 tags 为:${tags} 请优先保留这些特征。`
|
|
717
710
|
}
|
|
@@ -771,7 +764,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
771
764
|
}
|
|
772
765
|
}
|
|
773
766
|
|
|
774
|
-
async function optimizePrompt(session, userPrompt, force = false,
|
|
767
|
+
async function optimizePrompt(session, userPrompt, force = false, precomputedSearch = null) {
|
|
775
768
|
if (!cfg.promptOptimizeEnabled && !force) {
|
|
776
769
|
return { ok: true, prompt: userPrompt, reason: 'optimize_disabled' }
|
|
777
770
|
}
|
|
@@ -786,8 +779,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
786
779
|
} else if (wantsWebSearch(userPrompt)) {
|
|
787
780
|
searchBlock = await webSearch(userPrompt)
|
|
788
781
|
}
|
|
789
|
-
const characterRule = buildCharacterRule(userPrompt)
|
|
790
|
-
const defaultTemplate = `你是为图像生成模型编写正面提示词的 AI 画师。\n\n请根据用户的原始要求设计一幅完整、协调、具有视觉吸引力的画面,并将结果输出为英文 Danbooru-style tags。\n\n输出要求:\n- 只输出一行英文 tags,使用英文逗号分隔。\n- 不要输出解释、分析、标题、编号、Markdown、代码块或中文。\n- 不要输出 masterpiece、best quality、score 等质量前缀。\n- 不要输出画师 tags;质量词和画师组会由程序另行拼接。\n- 尽量使用模型容易理解的可见画面描述。\n- 保持用户明确指定的角色、主体、人数、关键服装、动作、表情和道具。\n- 以最终图像协调、精致、有表现力和好看为优先。\n\n角色和动态上下文:\n{character_rule}\n{
|
|
782
|
+
const characterRule = await buildCharacterRule(userPrompt)
|
|
783
|
+
const defaultTemplate = `你是为图像生成模型编写正面提示词的 AI 画师。\n\n请根据用户的原始要求设计一幅完整、协调、具有视觉吸引力的画面,并将结果输出为英文 Danbooru-style tags。\n\n输出要求:\n- 只输出一行英文 tags,使用英文逗号分隔。\n- 不要输出解释、分析、标题、编号、Markdown、代码块或中文。\n- 不要输出 masterpiece、best quality、score 等质量前缀。\n- 不要输出画师 tags;质量词和画师组会由程序另行拼接。\n- 尽量使用模型容易理解的可见画面描述。\n- 保持用户明确指定的角色、主体、人数、关键服装、动作、表情和道具。\n- 以最终图像协调、精致、有表现力和好看为优先。\n\n角色和动态上下文:\n{character_rule}\n{search_block}\n\n用户原始要求:\n{theme}`
|
|
791
784
|
const template = (cfg.promptOptimizeTemplate || '').trim() || defaultTemplate
|
|
792
785
|
const searchBlockText = searchBlock
|
|
793
786
|
? `联网搜索参考信息(请尽量依据这些内容补全角色外观与设定):\n${searchBlock}`
|
|
@@ -796,10 +789,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
796
789
|
.replace(/\{theme\}/g, userPrompt)
|
|
797
790
|
.replace(/\{search_block\}/g, searchBlockText)
|
|
798
791
|
.replace(/\{character_rule\}/g, characterRule)
|
|
799
|
-
.replace(/\{outfit_transfer_rule\}/g, '')
|
|
800
|
-
.replace(/\{reference_rule\}/g, '')
|
|
801
|
-
.replace(/\{img2img_rule\}/g, img2imgRule)
|
|
802
|
-
.replace(/\{sensual_rule\}/g, '')
|
|
803
792
|
.replace(/[ \t]+\n/g, '\n')
|
|
804
793
|
.replace(/\n{3,}/g, '\n\n')
|
|
805
794
|
.trim()
|
|
@@ -849,54 +838,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
849
838
|
}
|
|
850
839
|
}
|
|
851
840
|
|
|
852
|
-
function extractSeriesOptimizeJson(text) {
|
|
853
|
-
let raw = String(text || '').trim()
|
|
854
|
-
if (raw.startsWith('```')) raw = (raw.match(/```(?:json)?([\s\S]*?)```/) || [null, raw])[1].trim()
|
|
855
|
-
const start = raw.indexOf('{')
|
|
856
|
-
const end = raw.lastIndexOf('}')
|
|
857
|
-
if (start === -1 || end === -1 || end <= start) return null
|
|
858
|
-
try { return JSON.parse(raw.slice(start, end + 1)) } catch (e) { return null }
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function filterFixedTags(tags, drops) {
|
|
862
|
-
if (!tags) return ''
|
|
863
|
-
const dropKeys = (drops || []).map(d => canonicalTagText(String(d))).filter(Boolean)
|
|
864
|
-
if (!dropKeys.length) return tags
|
|
865
|
-
return splitTags(tags)
|
|
866
|
-
.filter(t => !dropKeys.some(k => k && canonicalTagText(t).includes(k)))
|
|
867
|
-
.join(', ')
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// 连续图专用的阶段优化:LLM 把「角色 + 阶段描述」转成 Danbooru tags,并返回
|
|
871
|
-
// 要从固定角色 tags 中移除的冲突项(如固定 silver hair、阶段变成 black hair)。
|
|
872
|
-
async function optimizeSeriesStage(userPrompt, identity) {
|
|
873
|
-
if (!cfg.llmModel || !cfg.llmBaseUrl) {
|
|
874
|
-
return { ok: false, prompt: userPrompt, drops: [], reason: 'llm_not_configured' }
|
|
875
|
-
}
|
|
876
|
-
const fixedTags = identity && parsePresetList(cfg.fixedCharacters)[identity]
|
|
877
|
-
? parsePresetList(cfg.fixedCharacters)[identity]
|
|
878
|
-
: ''
|
|
879
|
-
const template = `你是为图像生成模型编写正面提示词的 AI 画师。这是「同一个角色」的连续变化过程中的某一个阶段。\n\n用户给出一行描述:<角色身份>,<本阶段的外貌/状态描述>。\n\n固定角色 tags(身份锚点,包含角色名标签、种族、体型、标志特征,也可能包含发色、瞳色等默认外观):\n${fixedTags || '(无)'}\n\n输出要求:\n- 只输出一个 JSON 对象,不要 Markdown、不要解释、不要其他任何文字:\n{\n "stage_tags": "一行英文 Danbooru-style tags,用于本阶段画面,英文逗号分隔;不含 masterpiece/best quality 等质量前缀,不含画师 tags",\n "drop_fixed": ["要从固定 tags 中移除的标签列表;仅当本阶段描述明确改变了该外观时才列出"]\n}\n- 身份一致性:若用户描述的就是固定角色,stage_tags 必须包含该角色的角色名标签(如 kokkoro_(princess_connect!)),并保留种族、体型、标志特征等「不变的底层身份」。\n- 覆盖规则:本阶段描述明确提到的变化(发色、瞳色、表情、眼神、气质、种族变化等)必须体现在 stage_tags 中,并在 drop_fixed 中列出被替换掉的固定标签(措辞与固定 tags 一致或接近)。\n- 本阶段描述与固定 tags 无冲突时,drop_fixed 为 []。\n\n本阶段描述:\n{theme}`
|
|
880
|
-
const rendered = template.replace(/\{theme\}/g, userPrompt)
|
|
881
|
-
try {
|
|
882
|
-
const text = await llmChat({ system: rendered, user: userPrompt, maxTokens: Math.min(parseInt(cfg.llmMaxTokens) || 700, 900) })
|
|
883
|
-
const data = extractSeriesOptimizeJson(text)
|
|
884
|
-
if (data && String(data.stage_tags || '').trim()) {
|
|
885
|
-
const drops = Array.isArray(data.drop_fixed) ? data.drop_fixed.map(String).filter(Boolean) : []
|
|
886
|
-
return { ok: true, prompt: String(data.stage_tags).trim(), drops, reason: '' }
|
|
887
|
-
}
|
|
888
|
-
// JSON 解析失败:把整段文本当作阶段 tags,不剔除固定标签
|
|
889
|
-
return { ok: true, prompt: text, drops: [], reason: '' }
|
|
890
|
-
} catch (e) {
|
|
891
|
-
const reason = String(e && e.message || e)
|
|
892
|
-
logger.warn(`连续图阶段优化失败:${reason}`)
|
|
893
|
-
return { ok: false, prompt: userPrompt, drops: [], reason }
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
841
|
// ---------------- 提示词组装 ----------------
|
|
898
842
|
// fixedOverride:undefined=按名称自动匹配固定角色;'skip'=不注入固定角色;字符串=直接使用该字符串作为固定角色 tags
|
|
899
|
-
function composePrompt(userPrompt, raw, fixedOverride) {
|
|
843
|
+
async function composePrompt(userPrompt, raw, fixedOverride) {
|
|
900
844
|
if (raw) return { prompt: userPrompt, degraded: false }
|
|
901
845
|
const parts = []
|
|
902
846
|
if (cfg.qualityPrefix) parts.push(String(cfg.qualityPrefix).trim())
|
|
@@ -905,7 +849,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
905
849
|
} else if (typeof fixedOverride === 'string') {
|
|
906
850
|
if (String(fixedOverride).trim()) parts.push(String(fixedOverride).trim())
|
|
907
851
|
} else {
|
|
908
|
-
for (const
|
|
852
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
909
853
|
if (name && userPrompt.includes(name)) {
|
|
910
854
|
parts.push(tags)
|
|
911
855
|
break
|
|
@@ -965,7 +909,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
965
909
|
// 多人规划:让 LLM 输出结构化场景 JSON(2-4 人)。
|
|
966
910
|
async function generateMultiPersonPlan(prompt) {
|
|
967
911
|
const mentioned = {}
|
|
968
|
-
for (const
|
|
912
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
969
913
|
if (name && prompt.includes(name)) mentioned[name] = tags
|
|
970
914
|
}
|
|
971
915
|
const planPrompt = buildMultiPersonPlanPrompt(prompt, mentioned)
|
|
@@ -1011,7 +955,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1011
955
|
}
|
|
1012
956
|
|
|
1013
957
|
// 多人最终提示词组装:count/common tags + 角色块 + 互动 + 构图。
|
|
1014
|
-
function buildMultiPersonFinalPrompt(plan, prompt) {
|
|
958
|
+
async function buildMultiPersonFinalPrompt(plan, prompt) {
|
|
1015
959
|
const aliases = ['Character A', 'Character B', 'Character C', 'Character D']
|
|
1016
960
|
const characterCount = plan.characters.length
|
|
1017
961
|
const characterRoles = []
|
|
@@ -1027,7 +971,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1027
971
|
|
|
1028
972
|
const usedFixedNames = new Set()
|
|
1029
973
|
const fixedGenders = []
|
|
1030
|
-
const configuredChars =
|
|
974
|
+
const configuredChars = Object.fromEntries((await fixedCharacterRows()).map(({ name, tags }) => [name, tags]))
|
|
1031
975
|
for (let index = 0; index < plan.characters.length; index++) {
|
|
1032
976
|
const character = plan.characters[index]
|
|
1033
977
|
let fixedName = ''
|
|
@@ -1196,11 +1140,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1196
1140
|
|
|
1197
1141
|
// 视觉校验(anima_verify + generation_verifier 移植):对生成的图片跑视觉 LLM,
|
|
1198
1142
|
// 不合格则用相同提示词重试(最多 multiCandidateCount 张),按多候选规则挑选并返回结果。
|
|
1199
|
-
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1143
|
+
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1200
1144
|
const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
|
|
1201
1145
|
const verifyModel = String(cfg.verifyLlmModel || '').trim()
|
|
1202
1146
|
if (!verifyBaseUrl || !verifyModel) {
|
|
1203
|
-
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt, negativePrompt }
|
|
1147
|
+
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt, negativePrompt }
|
|
1204
1148
|
}
|
|
1205
1149
|
const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
|
|
1206
1150
|
const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
|
|
@@ -1208,10 +1152,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1208
1152
|
const systemPrompt = buildVerifySystemPrompt(true, planCount)
|
|
1209
1153
|
const candidates = []
|
|
1210
1154
|
let lastVerdict = null
|
|
1211
|
-
let retries = 0
|
|
1212
|
-
let currentImages = images
|
|
1213
|
-
let currentPrompt = prompt
|
|
1214
|
-
let currentNegativePrompt = negativePrompt
|
|
1155
|
+
let retries = 0
|
|
1156
|
+
let currentImages = images
|
|
1157
|
+
let currentPrompt = prompt
|
|
1158
|
+
let currentNegativePrompt = negativePrompt
|
|
1215
1159
|
|
|
1216
1160
|
async function verifyOnce(imgs, userReq) {
|
|
1217
1161
|
const controller = new AbortController()
|
|
@@ -1309,15 +1253,15 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1309
1253
|
data = extractVerifyJson(reply)
|
|
1310
1254
|
} catch (e) {
|
|
1311
1255
|
logger.warn(`多人视觉校验失败:${e.message}`)
|
|
1312
|
-
return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1256
|
+
return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1313
1257
|
}
|
|
1314
1258
|
if (!data) {
|
|
1315
1259
|
logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
|
|
1316
|
-
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1260
|
+
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1317
1261
|
}
|
|
1318
1262
|
const verdict = verdictFromData(data)
|
|
1319
1263
|
verdict.skipped = false
|
|
1320
|
-
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1264
|
+
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1321
1265
|
selectedOutputs = currentImages
|
|
1322
1266
|
selectedVerdict = verdict
|
|
1323
1267
|
|
|
@@ -1331,13 +1275,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1331
1275
|
if (hint) {
|
|
1332
1276
|
currentPrompt = `${userRequest}\n【上次问题,请修正】${hint}`
|
|
1333
1277
|
}
|
|
1334
|
-
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1278
|
+
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1335
1279
|
if (!regen.ok) {
|
|
1336
1280
|
logger.warn(`多人校验重试生成失败:${regen.message}`)
|
|
1337
1281
|
break
|
|
1338
1282
|
}
|
|
1339
|
-
currentImages = regen.outputs
|
|
1340
|
-
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1283
|
+
currentImages = regen.outputs
|
|
1284
|
+
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1341
1285
|
}
|
|
1342
1286
|
|
|
1343
1287
|
// 多候选挑选
|
|
@@ -1348,7 +1292,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1348
1292
|
selectedOutputs = best.outputs
|
|
1349
1293
|
selectedVerdict = best.verdict
|
|
1350
1294
|
if (!multiAccepted && !cfg.multiSendDegradedCandidate) {
|
|
1351
|
-
return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1295
|
+
return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1352
1296
|
}
|
|
1353
1297
|
const noteParts = []
|
|
1354
1298
|
if (multiAccepted) {
|
|
@@ -1357,7 +1301,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1357
1301
|
noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
|
|
1358
1302
|
}
|
|
1359
1303
|
if (retries) noteParts.push(session.text('.multi-verify-failed', [selectedVerdict.issues.length ? ':' + selectedVerdict.issues.join(';').slice(0, 80) : '', retries]))
|
|
1360
|
-
return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1304
|
+
return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1361
1305
|
}
|
|
1362
1306
|
|
|
1363
1307
|
function buildVerifySystemPrompt(multiPerson, planCount) {
|
|
@@ -1370,7 +1314,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1370
1314
|
}
|
|
1371
1315
|
|
|
1372
1316
|
// 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
|
|
1373
|
-
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1317
|
+
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1374
1318
|
async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
|
|
1375
1319
|
const results = []
|
|
1376
1320
|
let successCount = 0
|
|
@@ -1384,7 +1328,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1384
1328
|
const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
|
|
1385
1329
|
if (item.ok && outputs.length) {
|
|
1386
1330
|
successCount += 1
|
|
1387
|
-
results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', negativePrompt: item.negativePrompt || '', note: item.note || '' })
|
|
1331
|
+
results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', negativePrompt: item.negativePrompt || '', note: item.note || '' })
|
|
1388
1332
|
} else {
|
|
1389
1333
|
if (!isAdmin) await refundP(USERID, unitPrice)
|
|
1390
1334
|
if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
|
|
@@ -1441,7 +1385,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1441
1385
|
const plan = planResult.plan
|
|
1442
1386
|
|
|
1443
1387
|
// 组装最终提示词
|
|
1444
|
-
const built = buildMultiPersonFinalPrompt(plan, text)
|
|
1388
|
+
const built = await buildMultiPersonFinalPrompt(plan, text)
|
|
1445
1389
|
if (!built.ok) {
|
|
1446
1390
|
return session.text('.multi-usage') + '\n(多人规划失败:' + built.error + ')'
|
|
1447
1391
|
}
|
|
@@ -1465,10 +1409,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1465
1409
|
// 即时反馈
|
|
1466
1410
|
const notice = []
|
|
1467
1411
|
if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
|
|
1468
|
-
const feedback = feedbackBase(session, { firstPosition, count
|
|
1469
|
-
notice.push(...feedback.notices)
|
|
1470
|
-
await sendNotices(session, notice)
|
|
1471
|
-
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1412
|
+
const feedback = feedbackBase(session, { firstPosition, count })
|
|
1413
|
+
notice.push(...feedback.notices)
|
|
1414
|
+
await sendNotices(session, notice)
|
|
1472
1415
|
|
|
1473
1416
|
// 单张生成 +(可选)视觉校验
|
|
1474
1417
|
const runOne = async (i) => {
|
|
@@ -1478,27 +1421,27 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1478
1421
|
} else {
|
|
1479
1422
|
try { result = await runComfyGenerate(finalPrompt, size, { negativePrompt: multiNegative, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1480
1423
|
}
|
|
1481
|
-
if (!result.ok || !result.outputs || !result.outputs.length) return result
|
|
1482
|
-
if (!cfg.multiVerifyEnabled) {
|
|
1483
|
-
return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, negativePrompt: result.negativePrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
|
|
1484
|
-
}
|
|
1485
|
-
const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet, result.negativePrompt || multiNegative)
|
|
1486
|
-
if (!verified.ok) return { ok: false, message: verified.message }
|
|
1487
|
-
return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, negativePrompt: verified.negativePrompt || result.negativePrompt || multiNegative, note: verified.message || '' }
|
|
1424
|
+
if (!result.ok || !result.outputs || !result.outputs.length) return result
|
|
1425
|
+
if (!cfg.multiVerifyEnabled) {
|
|
1426
|
+
return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, negativePrompt: result.negativePrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
|
|
1427
|
+
}
|
|
1428
|
+
const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet, result.negativePrompt || multiNegative)
|
|
1429
|
+
if (!verified.ok) return { ok: false, message: verified.message }
|
|
1430
|
+
return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, negativePrompt: verified.negativePrompt || result.negativePrompt || multiNegative, note: verified.message || '' }
|
|
1488
1431
|
}
|
|
1489
1432
|
|
|
1490
1433
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
|
|
1491
1434
|
|
|
1492
|
-
// 汇总
|
|
1493
|
-
const allOutputs = []
|
|
1494
|
-
const forwardOutputs = []
|
|
1495
|
-
const notes = []
|
|
1435
|
+
// 汇总
|
|
1436
|
+
const allOutputs = []
|
|
1437
|
+
const forwardOutputs = []
|
|
1438
|
+
const notes = []
|
|
1496
1439
|
const seeds = []
|
|
1497
1440
|
const failures = []
|
|
1498
1441
|
for (const item of results) {
|
|
1499
|
-
if (item.ok) {
|
|
1500
|
-
allOutputs.push(...item.outputs)
|
|
1501
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
1442
|
+
if (item.ok) {
|
|
1443
|
+
allOutputs.push(...item.outputs)
|
|
1444
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
1502
1445
|
if (item.seed != null) seeds.push(item.seed)
|
|
1503
1446
|
if (item.note) notes.push(item.note)
|
|
1504
1447
|
} else {
|
|
@@ -1506,6 +1449,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1506
1449
|
}
|
|
1507
1450
|
}
|
|
1508
1451
|
|
|
1452
|
+
const chargeNotice = buildChargeNotice({
|
|
1453
|
+
isAdmin,
|
|
1454
|
+
totalPrice: successCount * price,
|
|
1455
|
+
unetName: unet,
|
|
1456
|
+
seeds,
|
|
1457
|
+
})
|
|
1458
|
+
await sendNotices(session, [chargeNotice], { quote: true })
|
|
1459
|
+
|
|
1509
1460
|
if (!allOutputs.length) {
|
|
1510
1461
|
if (cfg.outputLogs) logger.warn(`多人生成全部失败(${USERID}),已按张退款`)
|
|
1511
1462
|
return session.text('.generate-failed', ['全部失败(已按张退款)'])
|
|
@@ -1513,8 +1464,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1513
1464
|
|
|
1514
1465
|
if (cfg.outputLogs) logger.success(`${USERID} 多人生成成功 ${successCount}/${count} 张`)
|
|
1515
1466
|
|
|
1516
|
-
// 发图:合并转发,不引用原指令
|
|
1517
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
1467
|
+
// 发图:合并转发,不引用原指令
|
|
1468
|
+
await sendImagesAsForward(session, forwardOutputs)
|
|
1518
1469
|
|
|
1519
1470
|
const reply = []
|
|
1520
1471
|
if (count > 1) {
|
|
@@ -1527,174 +1478,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1527
1478
|
return reply.filter(Boolean).join('\n')
|
|
1528
1479
|
}
|
|
1529
1480
|
|
|
1530
|
-
// 连续图/过程图主流程:同一角色多阶段(固定身份 + 阶段描述 + 全阶段共用同一 seed 保证一致性)
|
|
1531
|
-
// 语法:连续 <角色>:<阶段1> → <阶段2> → ... 或 连续 <角色>:<阶段1>|<阶段2>|...
|
|
1532
|
-
// 询问是否使用 LLM 优化,返回 'yes' / 'no' / 'cancel' / null(无法交互或超时)
|
|
1533
|
-
async function askSeriesLLMUse(session) {
|
|
1534
|
-
if (typeof session.prompt !== 'function') return null
|
|
1535
|
-
await session.send(session.text('.series-llm-ask'))
|
|
1536
|
-
const reply = await session.prompt((cfg.seriesAskTimeout || 60) * 1000).catch(() => null)
|
|
1537
|
-
const ans = String((reply && (reply.content != null ? reply.content : reply)) || '').trim()
|
|
1538
|
-
if (/^(是|1|①|用|使用|使用llm|yes|y)$/i.test(ans)) return 'yes'
|
|
1539
|
-
if (/^(否|2|②|不用|不使用|不使用llm|不优化|不用llm|no|n)$/i.test(ans)) return 'no'
|
|
1540
|
-
if (/^(取消|3|③|算了|不生成|cancel|c)$/i.test(ans)) return 'cancel'
|
|
1541
|
-
if (!ans) return null
|
|
1542
|
-
await session.send(session.text('.series-llm-invalid'))
|
|
1543
|
-
return askSeriesLLMUse(session)
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
async function handleGenerateSeries(session, rawText) {
|
|
1547
|
-
const USERID = session.userId
|
|
1548
|
-
const isAdmin = isAdminUser(session)
|
|
1549
|
-
const unet = await resolveUnet(USERID)
|
|
1550
|
-
const price = Math.max(0, parseInt(cfg.price) || 500)
|
|
1551
|
-
|
|
1552
|
-
// 阶段分隔符:箭头 / 管道
|
|
1553
|
-
const STAGE_SEP = /→|➔|➜|←|↔|=>|->|⇒|\|/
|
|
1554
|
-
|
|
1555
|
-
// 尺寸解析(连续图默认横图);固定 seed:全阶段共用,支持 --seed 覆盖(含 --seed: 冒号形式)
|
|
1556
|
-
const seedInfo = parseSeed(String(rawText || ''))
|
|
1557
|
-
const seed = seedInfo.seed != null ? seedInfo.seed : crypto.randomInt(1, 2 ** 32 - 1)
|
|
1558
|
-
const allowed = parseAllowedSizes()
|
|
1559
|
-
const parsedSize = parseGenerationSize(seedInfo.prompt, allowed)
|
|
1560
|
-
if (parsedSize.error) return parsedSize.error
|
|
1561
|
-
let size = parsedSize.size
|
|
1562
|
-
if (!size && allowed.length) {
|
|
1563
|
-
size = allowed.reduce((best, s) => {
|
|
1564
|
-
const a = Math.abs(s[0] / s[1] - 16 / 9)
|
|
1565
|
-
const b = Math.abs(best[0] / best[1] - 16 / 9)
|
|
1566
|
-
return a < b ? s : best
|
|
1567
|
-
})
|
|
1568
|
-
}
|
|
1569
|
-
const sizeCleanedPrompt = parsedSize.prompt
|
|
1570
|
-
|
|
1571
|
-
// 提取身份与阶段文本(角色:阶段1 → 阶段2)
|
|
1572
|
-
let identity = ''
|
|
1573
|
-
let stageText = String(sizeCleanedPrompt || '').trim()
|
|
1574
|
-
const colonMatch = stageText.match(/^(.+?)[::]\s*(.+)$/)
|
|
1575
|
-
if (colonMatch) {
|
|
1576
|
-
identity = colonMatch[1].trim()
|
|
1577
|
-
stageText = colonMatch[2].trim()
|
|
1578
|
-
}
|
|
1579
|
-
const stages = stageText
|
|
1580
|
-
.split(STAGE_SEP)
|
|
1581
|
-
.map(s => s.trim().replace(/^[\s,,、;;::]+|[\s,,、;;::]+$/g, '').replace(/\s+/g, ' '))
|
|
1582
|
-
.filter(Boolean)
|
|
1583
|
-
if (!stages.length) return session.text('.series-usage')
|
|
1584
|
-
|
|
1585
|
-
const maxStages = Math.max(1, parseInt(cfg.batchMax) || 4)
|
|
1586
|
-
const count = Math.min(stages.length, maxStages)
|
|
1587
|
-
const clamped = stages.length > count
|
|
1588
|
-
const stageList = stages.slice(0, count)
|
|
1589
|
-
|
|
1590
|
-
// P 点校验(按总价)
|
|
1591
|
-
const pcheck = await precheckPoints(session, USERID, isAdmin, count * price)
|
|
1592
|
-
if (!pcheck.ok) return pcheck.message
|
|
1593
|
-
|
|
1594
|
-
// ComfyUI 就绪
|
|
1595
|
-
const ready = await ensureComfyuiReady()
|
|
1596
|
-
if (!ready.ok) return ready.message
|
|
1597
|
-
|
|
1598
|
-
// 询问是否使用 LLM 优化:是=用 LLM / 否=直接使用原始描述 / 取消=不生成。
|
|
1599
|
-
// 未配置 LLM 或平台不支持交互式询问时,沿用原有「配置了 LLM 就逐阶段优化」行为。
|
|
1600
|
-
let useLLM = true
|
|
1601
|
-
if (cfg.llmModel && cfg.llmBaseUrl && typeof session.prompt === 'function') {
|
|
1602
|
-
const choice = await askSeriesLLMUse(session)
|
|
1603
|
-
if (choice === 'cancel') return ''
|
|
1604
|
-
if (choice === 'no') useLLM = false
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
// 组装各阶段提示词:身份(含固定角色 tags)+ 阶段描述。
|
|
1608
|
-
// 使用 LLM 时逐阶段优化(不受 promptOptimizeEnabled 限制),因为 anima 是
|
|
1609
|
-
// Danbooru-tag 模型,中文阶段描述必须转成 tags 才能体现在画面里;
|
|
1610
|
-
// 用户选择「否」或未配置 LLM 时,直接使用各阶段原始描述(不注入身份前缀)。
|
|
1611
|
-
const fixedChars = parsePresetList(cfg.fixedCharacters)
|
|
1612
|
-
const stagePrompts = []
|
|
1613
|
-
let degradedStages = 0
|
|
1614
|
-
for (const st of stageList) {
|
|
1615
|
-
const base = identity ? `${identity},${st}` : st
|
|
1616
|
-
const result = useLLM
|
|
1617
|
-
? await optimizeSeriesStage(base, identity)
|
|
1618
|
-
: { ok: true, prompt: st, drops: [], reason: 'user_skipped_llm' }
|
|
1619
|
-
if (!result.ok) degradedStages += 1
|
|
1620
|
-
const stageTags = result.prompt || base
|
|
1621
|
-
const drops = result.drops || []
|
|
1622
|
-
// 始终注入固定角色 tags 作为身份锚点(保证角色名/种族/尖耳朵出现),
|
|
1623
|
-
// 阶段描述里被明确改变的外观由 drop 列表剔除,避免被固定默认值拉回。
|
|
1624
|
-
const identityTags = identity ? filterFixedTags(fixedChars[identity] || '', drops) : ''
|
|
1625
|
-
const anchor = stageTags
|
|
1626
|
-
const composed = composePrompt(anchor, false, identityTags)
|
|
1627
|
-
stagePrompts.push(composed.prompt)
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// 扣 P 点(一次性扣除总价)
|
|
1631
|
-
if (!isAdmin) {
|
|
1632
|
-
const saving = await deductP(USERID, count * price)
|
|
1633
|
-
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 连续图已扣除 ${count * price} P 点(${count} 阶段 × ${price},seed=${seed}),余额 ${saving - count * price}`)
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// 队列:预排队全部阶段(先查容量再入队)
|
|
1637
|
-
const queued = await enqueueBatch(count, (i) => runComfyGenerate(stagePrompts[i], size, { seed, unet }), { USERID, isAdmin, totalPrice: count * price })
|
|
1638
|
-
if (!queued.ok) return queued.message
|
|
1639
|
-
const queuedTasks = queued.tasks
|
|
1640
|
-
const firstPosition = queued.firstPosition
|
|
1641
|
-
|
|
1642
|
-
// 即时反馈
|
|
1643
|
-
const notice = []
|
|
1644
|
-
if (clamped) notice.push(session.text('.batch-limit', [count]))
|
|
1645
|
-
if (identity && fixedChars[identity]) notice.push(`已固定角色「${identity}」的身份 tags,各阶段外观将保持一致。`)
|
|
1646
|
-
if (!useLLM) notice.push(session.text('.series-no-llm'))
|
|
1647
|
-
if (degradedStages) notice.push(session.text('.prompt-degraded', ['(连续图阶段优化失败,已使用原始描述)']))
|
|
1648
|
-
const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin })
|
|
1649
|
-
notice.push(...feedback.notices)
|
|
1650
|
-
await sendNotices(session, notice)
|
|
1651
|
-
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1652
|
-
|
|
1653
|
-
// 单阶段生成(共用 seed)
|
|
1654
|
-
const runOne = async (i) => {
|
|
1655
|
-
let result
|
|
1656
|
-
if (cfg.queueEnabled) {
|
|
1657
|
-
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1658
|
-
} else {
|
|
1659
|
-
try { result = await runComfyGenerate(stagePrompts[i], size, { seed, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1660
|
-
}
|
|
1661
|
-
result.prompt = stagePrompts[i]
|
|
1662
|
-
return result
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
|
|
1666
|
-
|
|
1667
|
-
// 汇总
|
|
1668
|
-
const allOutputs = []
|
|
1669
|
-
const forwardOutputs = []
|
|
1670
|
-
const seeds = []
|
|
1671
|
-
const failures = []
|
|
1672
|
-
for (const item of results) {
|
|
1673
|
-
if (item.ok) {
|
|
1674
|
-
allOutputs.push(...item.outputs)
|
|
1675
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '', negativePrompt: item.negativePrompt })))
|
|
1676
|
-
if (item.seed != null) seeds.push(item.seed)
|
|
1677
|
-
} else {
|
|
1678
|
-
failures.push(`第 ${item.i + 1} 阶段:${item.message}`)
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
if (!allOutputs.length) {
|
|
1683
|
-
if (cfg.outputLogs) logger.warn(`连续图全部失败(${USERID}),已按阶段退款`)
|
|
1684
|
-
return session.text('.generate-failed', ['全部失败(已按阶段退款)'])
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
if (cfg.outputLogs) logger.success(`${USERID} 连续图生成成功 ${successCount}/${count} 阶段(seed=${seed})`)
|
|
1688
|
-
|
|
1689
|
-
// 发图:合并转发,不引用原指令
|
|
1690
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
1691
|
-
|
|
1692
|
-
const reply = []
|
|
1693
|
-
reply.push(session.text('.series-ok', [count * price, successCount, seed]))
|
|
1694
|
-
if (failures.length) reply.push(session.text('.batch-partial', [successCount, count, failures.length, failures.join(';')]))
|
|
1695
|
-
return reply.filter(Boolean).join('\n')
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
1481
|
// ---------------- 权限 ----------------
|
|
1699
1482
|
function readableOptimizeReason(reason) {
|
|
1700
1483
|
if (!reason) return ''
|
|
@@ -1776,7 +1559,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1776
1559
|
// 注意:更新数据里不能带主键 id,否则数据库驱动会报 cannot modify primary key
|
|
1777
1560
|
function runtimeState() {
|
|
1778
1561
|
return {
|
|
1779
|
-
fixed_characters: cfg.fixedCharacters || [],
|
|
1780
1562
|
artist_presets: cfg.artistPresets || [],
|
|
1781
1563
|
active_artist_preset: cfg.activeArtistPreset || '',
|
|
1782
1564
|
default_artist_tags: cfg.defaultArtistTags || '',
|
|
@@ -1784,20 +1566,25 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1784
1566
|
}
|
|
1785
1567
|
}
|
|
1786
1568
|
|
|
1787
|
-
|
|
1569
|
+
let legacyRuntimeFixedCharacters = []
|
|
1570
|
+
let fixedCharactersMigrated = false
|
|
1571
|
+
|
|
1572
|
+
// 启动时加载 p_draw_config 中仍归属该表的运行时数据。
|
|
1573
|
+
// fixed_characters 只作为旧版本迁移输入,运行时不再覆盖 cfg.fixedCharacters。
|
|
1788
1574
|
async function loadRuntimeState() {
|
|
1789
1575
|
try {
|
|
1790
1576
|
const rows = await ctx.database.get('p_draw_config', { id: 1 })
|
|
1791
1577
|
const row = rows && rows[0]
|
|
1792
1578
|
if (!row) return
|
|
1793
|
-
if (Array.isArray(row.fixed_characters))
|
|
1579
|
+
if (Array.isArray(row.fixed_characters)) legacyRuntimeFixedCharacters = row.fixed_characters
|
|
1580
|
+
fixedCharactersMigrated = row.fixed_characters_migrated === true
|
|
1794
1581
|
if (Array.isArray(row.artist_presets)) cfg.artistPresets = row.artist_presets
|
|
1795
1582
|
if (row.active_artist_preset) cfg.activeArtistPreset = row.active_artist_preset
|
|
1796
1583
|
if (row.default_artist_tags != null) cfg.defaultArtistTags = row.default_artist_tags
|
|
1797
1584
|
if (row.user_models && typeof row.user_models === 'object') cfg.userModels = row.user_models
|
|
1798
|
-
if (cfg.outputLogs) logger.info(`[p-draw] 已加载运行时配置(画师组 ${(cfg.artistPresets || []).length}
|
|
1585
|
+
if (cfg.outputLogs) logger.info(`[p-draw] 已加载运行时配置(画师组 ${(cfg.artistPresets || []).length} 个,模型偏好 ${Object.keys(cfg.userModels || {}).length} 个)`)
|
|
1799
1586
|
} catch (e) {
|
|
1800
|
-
logger.warn(
|
|
1587
|
+
logger.warn(`读取运行时配置失败(画师组或模型偏好可能未持久化):${e.message}`)
|
|
1801
1588
|
}
|
|
1802
1589
|
}
|
|
1803
1590
|
|
|
@@ -1816,6 +1603,29 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1816
1603
|
}
|
|
1817
1604
|
}
|
|
1818
1605
|
|
|
1606
|
+
async function migrateFixedCharacters() {
|
|
1607
|
+
if (fixedCharactersMigrated) return
|
|
1608
|
+
const existingRows = await fixedCharacterRows()
|
|
1609
|
+
const existingNames = new Set(existingRows.map(row => row.name))
|
|
1610
|
+
const legacyEntries = [...(cfg.fixedCharacters || []), ...legacyRuntimeFixedCharacters]
|
|
1611
|
+
for (const entry of legacyEntries) {
|
|
1612
|
+
const parsed = parseNameTags(entry)
|
|
1613
|
+
if (!parsed || existingNames.has(parsed.name)) continue
|
|
1614
|
+
await ctx.database.create('p_draw_fixed_characters', {
|
|
1615
|
+
name: parsed.name,
|
|
1616
|
+
tags: parsed.tags,
|
|
1617
|
+
})
|
|
1618
|
+
existingNames.add(parsed.name)
|
|
1619
|
+
}
|
|
1620
|
+
const existingConfig = await ctx.database.get('p_draw_config', { id: 1 })
|
|
1621
|
+
if (existingConfig && existingConfig[0]) {
|
|
1622
|
+
await ctx.database.set('p_draw_config', { id: 1 }, { fixed_characters_migrated: true })
|
|
1623
|
+
} else {
|
|
1624
|
+
await ctx.database.create('p_draw_config', { id: 1, ...runtimeState(), fixed_characters_migrated: true })
|
|
1625
|
+
}
|
|
1626
|
+
fixedCharactersMigrated = true
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1819
1629
|
function normalizeTagText(text) {
|
|
1820
1630
|
const tags = []
|
|
1821
1631
|
for (const tag of String(text || '').split(',')) {
|
|
@@ -1848,18 +1658,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1848
1658
|
return await diagnoseText(session)
|
|
1849
1659
|
}
|
|
1850
1660
|
|
|
1851
|
-
// 连续图指令:p-draw 连续 <角色>:<阶段1> → <阶段2>
|
|
1852
|
-
const seriesMatch = text.match(/^连续\s*(.*)$/)
|
|
1853
|
-
if (seriesMatch) {
|
|
1854
|
-
return await handleGenerateSeries(session, seriesMatch[1].trim())
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
// 以图生图指令:p-draw i2i <描述>(需同一条消息附带原图)
|
|
1858
|
-
const i2iMatch = text.match(/^i2i\s*(.*)$/i)
|
|
1859
|
-
if (i2iMatch) {
|
|
1860
|
-
return await handleGenerateI2I(session, i2iMatch[1].trim())
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
1661
|
// 多人指令:p-draw 多人 <描述>
|
|
1864
1662
|
const multiMatch = text.match(/^(?:多人|多人生图|双人|三人|群像)\s*(.*)$/)
|
|
1865
1663
|
if (multiMatch) {
|
|
@@ -1945,12 +1743,27 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1945
1743
|
if (addCharacter) {
|
|
1946
1744
|
const parsed = parseNameTags(addCharacter[1])
|
|
1947
1745
|
if (!parsed) return session.text('.character-format')
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
await
|
|
1746
|
+
const tags = normalizeTagText(parsed.tags)
|
|
1747
|
+
const existing = (await fixedCharacterRows({ name: parsed.name }))[0]
|
|
1748
|
+
if (existing) await ctx.database.set('p_draw_fixed_characters', { id: existing.id }, { name: parsed.name, tags })
|
|
1749
|
+
else await ctx.database.create('p_draw_fixed_characters', { name: parsed.name, tags })
|
|
1951
1750
|
if (cfg.outputLogs) logger.success(`${USERID} 添加固定角色 ${parsed.name}`)
|
|
1952
1751
|
return session.text('.character-created', [parsed.name, parsed.tags])
|
|
1953
1752
|
}
|
|
1753
|
+
if (text.match(/^(?:查看|列出|显示)\s*(?:固定)?\s*角色$|^(?:固定)?\s*角色列表$/)) {
|
|
1754
|
+
const rows = await fixedCharacterRows()
|
|
1755
|
+
if (!rows.length) return '固定角色:无'
|
|
1756
|
+
return ['固定角色:', ...rows.map(row => `- ${row.name}:${row.tags}`)].join('\n')
|
|
1757
|
+
}
|
|
1758
|
+
const deleteCharacter = text.match(/^(?:删除|移除)\s*(?:固定)?\s*角色\s*(.*)$/)
|
|
1759
|
+
if (deleteCharacter) {
|
|
1760
|
+
const name = String(deleteCharacter[1]).trim()
|
|
1761
|
+
if (!name) return session.text('.character-delete-format')
|
|
1762
|
+
const existing = (await fixedCharacterRows({ name }))[0]
|
|
1763
|
+
if (!existing) return session.text('.character-not-found', [name])
|
|
1764
|
+
await ctx.database.remove('p_draw_fixed_characters', { id: existing.id })
|
|
1765
|
+
return session.text('.character-deleted', [name])
|
|
1766
|
+
}
|
|
1954
1767
|
|
|
1955
1768
|
// 模型切换:p-draw 模型 <名称>(查看)/ p-draw 模型 默认(重置)
|
|
1956
1769
|
const modelMatch = text.match(/^(?:切换)?\s*模型\s*(.*)$/)
|
|
@@ -1985,230 +1798,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1985
1798
|
return await handleGenerate(session, text)
|
|
1986
1799
|
})
|
|
1987
1800
|
|
|
1988
|
-
|
|
1989
|
-
async function handleGenerateI2I(session, rawText) {
|
|
1990
|
-
if (cfg.customWorkflowEnabled && cfg.customWorkflowPath) {
|
|
1991
|
-
return session.text('.i2i-no-custom-workflow')
|
|
1992
|
-
}
|
|
1993
|
-
const image = await extractImageFromSession(session)
|
|
1994
|
-
if (!image) return session.text('.i2i-no-image')
|
|
1995
|
-
let uploadName
|
|
1996
|
-
try {
|
|
1997
|
-
uploadName = await uploadImageToComfyui(image)
|
|
1998
|
-
} catch (e) {
|
|
1999
|
-
logger.warn(`上传原图失败:${e.message}`)
|
|
2000
|
-
return session.text('.i2i-upload-fail', [e.message])
|
|
2001
|
-
}
|
|
2002
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 原图已上传:${uploadName}`)
|
|
2003
|
-
|
|
2004
|
-
// 能力检测(ControlNet / Anima IP-Adapter)
|
|
2005
|
-
const caps = await detectI2ICapabilities()
|
|
2006
|
-
|
|
2007
|
-
// 处理模式:i2iMode=ask 时交互询问;style/ootd/plain 直接固定
|
|
2008
|
-
let mode = 'plain'
|
|
2009
|
-
const cfgMode = String(cfg.i2iMode || 'ask').toLowerCase()
|
|
2010
|
-
if (cfgMode === 'style' || cfgMode === 'ootd') {
|
|
2011
|
-
mode = cfgMode
|
|
2012
|
-
} else if (cfgMode === 'ask' && typeof session.prompt === 'function') {
|
|
2013
|
-
const chosen = await askI2IMode(session)
|
|
2014
|
-
if (chosen === 'cancel') return session.text('.i2i-mode-cancelled')
|
|
2015
|
-
if (chosen === null) {
|
|
2016
|
-
await session.send(session.text('.i2i-mode-cancelled'))
|
|
2017
|
-
return ''
|
|
2018
|
-
}
|
|
2019
|
-
mode = chosen
|
|
2020
|
-
if (mode === 'style') await session.send(session.text('.i2i-mode-style'))
|
|
2021
|
-
if (mode === 'ootd') await session.send(session.text('.i2i-mode-ootd'))
|
|
2022
|
-
}
|
|
2023
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 模式=${mode} ControlNet=${caps.controlNet.available ? caps.controlNet.model : '-'} IPAdapter=${caps.ipAdapter.available ? 'on' : 'off'}`)
|
|
2024
|
-
|
|
2025
|
-
// 所选模式依赖的节点缺失时给出提示(仍会回退普通 img2img,不中断)
|
|
2026
|
-
const notices = []
|
|
2027
|
-
if (mode === 'style' && (!caps.controlNet.available || !caps.controlNet.model)) {
|
|
2028
|
-
notices.push(session.text('.i2i-no-controlnet'))
|
|
2029
|
-
}
|
|
2030
|
-
if (mode === 'ootd' && (!caps.ipAdapter.available || !String(cfg.i2iIPAdapterPath || '').trim())) {
|
|
2031
|
-
notices.push(session.text('.i2i-no-ipadapter'))
|
|
2032
|
-
}
|
|
2033
|
-
if (notices.length) {
|
|
2034
|
-
try { await session.send(notices.join('\n')) } catch (e) { logger.warn(`发送 i2i 提示失败:${e.message}`) }
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
// 识图:仅当启用、非无优化模式、且已配置 LLM(识图结果会注入提示词优化)时才执行
|
|
2038
|
-
let taggerTags = ''
|
|
2039
|
-
if (cfg.taggerEnabled && cfg.llmModel && cfg.llmBaseUrl && !stripRawPrefix(rawText).raw) {
|
|
2040
|
-
try {
|
|
2041
|
-
taggerTags = await taggerImage(uploadName)
|
|
2042
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i ${session.userId} 识图完成:${taggerTags.slice(0, 120)}${taggerTags.length > 120 ? '...' : ''}`)
|
|
2043
|
-
} catch (e) {
|
|
2044
|
-
logger.warn(`识图失败(继续生图):${e.message}`)
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
return await handleGenerate(session, rawText, uploadName, taggerTags, { mode, caps })
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// 识图预工作流:LoadImage → WD14Tagger|pysssss,读回原图 tags
|
|
2051
|
-
async function taggerImage(uploadName) {
|
|
2052
|
-
const model = String(cfg.taggerModel || 'wd-v1-4-convnext-tagger-v2').trim()
|
|
2053
|
-
const taggerNodeId = '61'
|
|
2054
|
-
const promptBody = {
|
|
2055
|
-
'60': { class_type: 'LoadImage', inputs: { image: uploadName } },
|
|
2056
|
-
[taggerNodeId]: {
|
|
2057
|
-
class_type: 'WD14Tagger|pysssss',
|
|
2058
|
-
inputs: {
|
|
2059
|
-
image: ['60', 0],
|
|
2060
|
-
model,
|
|
2061
|
-
threshold: Number(cfg.taggerThreshold) || 0.35,
|
|
2062
|
-
character_threshold: Number(cfg.taggerCharacterThreshold) || 0.85,
|
|
2063
|
-
replace_underscore: true,
|
|
2064
|
-
trailing_comma: false,
|
|
2065
|
-
exclude_tags: '',
|
|
2066
|
-
},
|
|
2067
|
-
},
|
|
2068
|
-
}
|
|
2069
|
-
const clientId = crypto.randomUUID()
|
|
2070
|
-
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
2071
|
-
const promptId = submit && submit.prompt_id
|
|
2072
|
-
if (!promptId) throw new Error('识图工作流提交失败')
|
|
2073
|
-
const timeoutMs = Math.max(1, parseInt(cfg.timeout) || 300) * 1000
|
|
2074
|
-
const pollMs = Math.max(1, parseInt(cfg.pollInterval) || 2) * 1000
|
|
2075
|
-
const history = await waitComfyResult(ctx, comfyGet, baseUrl(), promptId, clientId, timeoutMs, pollMs)
|
|
2076
|
-
if (!history) throw new Error('识图超时')
|
|
2077
|
-
const nodeOutput = history.outputs && history.outputs[taggerNodeId]
|
|
2078
|
-
const tags = (nodeOutput && Array.isArray(nodeOutput.tags) ? nodeOutput.tags : []).filter(Boolean)
|
|
2079
|
-
const first = String(tags[0] || '').trim()
|
|
2080
|
-
if (!first) throw new Error('识图未返回标签')
|
|
2081
|
-
return first
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// 从会话消息里取第一张图片并下载为内存 Buffer
|
|
2085
|
-
async function extractImageFromSession(session) {
|
|
2086
|
-
const elements = session.elements || []
|
|
2087
|
-
const img = elements.find((e) => e.type === 'img' || e.type === 'image')
|
|
2088
|
-
if (!img) return null
|
|
2089
|
-
const attrs = img.attrs || img.data || {}
|
|
2090
|
-
const src = String(attrs.src || '')
|
|
2091
|
-
if (!src) return null
|
|
2092
|
-
try {
|
|
2093
|
-
if (/^file:\/\//i.test(src)) {
|
|
2094
|
-
// 修复:Windows 下 file:///K:/... 用 replace 会得到 /K:/...(无法读取),用 fileURLToPath 解析
|
|
2095
|
-
let filePath
|
|
2096
|
-
try {
|
|
2097
|
-
filePath = fileURLToPath(src)
|
|
2098
|
-
} catch (e) {
|
|
2099
|
-
filePath = src.replace(/^file:\/\//i, '')
|
|
2100
|
-
}
|
|
2101
|
-
const buffer = await fsp.readFile(filePath)
|
|
2102
|
-
return { buffer, ext: path.extname(filePath) || '.png' }
|
|
2103
|
-
}
|
|
2104
|
-
if (/^data:/i.test(src)) {
|
|
2105
|
-
const m = src.match(/^data:image\/([a-zA-Z0-9+]+);base64,(.+)$/)
|
|
2106
|
-
if (!m) return null
|
|
2107
|
-
const kind = m[1].toLowerCase()
|
|
2108
|
-
const ext = kind === 'jpeg' ? '.jpg' : kind === 'webp' ? '.webp' : kind === 'png' ? '.png' : '.' + (kind || 'png')
|
|
2109
|
-
return { buffer: Buffer.from(m[2], 'base64'), ext }
|
|
2110
|
-
}
|
|
2111
|
-
const res = await fetch(src)
|
|
2112
|
-
if (!res.ok) return null
|
|
2113
|
-
const buffer = Buffer.from(await res.arrayBuffer())
|
|
2114
|
-
const mime = String(res.headers.get('content-type') || '')
|
|
2115
|
-
const ext = mime.includes('jpeg') ? '.jpg' : mime.includes('webp') ? '.webp' : '.png'
|
|
2116
|
-
return { buffer, ext }
|
|
2117
|
-
} catch (e) {
|
|
2118
|
-
logger.warn(`读取原图失败:${e.message}`)
|
|
2119
|
-
return null
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// 上传图片到 ComfyUI input 目录,返回 ComfyUI 使用的文件名
|
|
2124
|
-
async function uploadImageToComfyui(image) {
|
|
2125
|
-
const filename = `pdraw_i2i_${Date.now()}_${crypto.randomBytes(4).toString('hex')}${image.ext || '.png'}`
|
|
2126
|
-
const form = new FormData()
|
|
2127
|
-
form.append('image', new Blob([image.buffer]), filename)
|
|
2128
|
-
form.append('overwrite', 'true')
|
|
2129
|
-
form.append('type', 'input')
|
|
2130
|
-
const res = await fetch(baseUrl() + '/upload/image', { method: 'POST', body: form })
|
|
2131
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
2132
|
-
const data = await res.json().catch(() => ({}))
|
|
2133
|
-
if (!data || !data.name) throw new Error('ComfyUI 未返回上传文件名')
|
|
2134
|
-
return data.name
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
// 检测 i2i 可用能力:Anima ControlNet-LLLite 节点与权重(comfyui-anima-lllite / kohya-ss/ComfyUI-Anima-LLLite)、
|
|
2138
|
-
// LineArt 预处理器(comfyui_controlnet_aux,可选)与 Anima IP-Adapter 专用节点(comfyui-anima-ipadapter)。
|
|
2139
|
-
// 检测失败全部视为不可用(回退普通 img2img)。
|
|
2140
|
-
// 注意:Qwen-Image InstantX ControlNet 与 Anima(MiniTrainDIT,3584 维)架构不兼容,这里只认 LLLite 权重。
|
|
2141
|
-
async function detectI2ICapabilities() {
|
|
2142
|
-
const caps = { controlNet: { available: false, model: null, preprocessor: 'Canny' }, ipAdapter: { available: false } }
|
|
2143
|
-
let info
|
|
2144
|
-
try {
|
|
2145
|
-
info = await getObjectInfoCached()
|
|
2146
|
-
} catch (e) {
|
|
2147
|
-
return caps
|
|
2148
|
-
}
|
|
2149
|
-
if (!info || typeof info !== 'object') return caps
|
|
2150
|
-
// LineArt 预处理器(comfyui_controlnet_aux):AnimeLineArt 更贴近漫画线条,优先;LineArt 次之;都没有则回退 Canny
|
|
2151
|
-
if (info.AnimeLineArtPreprocessor) {
|
|
2152
|
-
caps.controlNet.preprocessor = 'AnimeLineArtPreprocessor'
|
|
2153
|
-
} else if (info.LineArtPreprocessor) {
|
|
2154
|
-
caps.controlNet.preprocessor = 'LineArtPreprocessor'
|
|
2155
|
-
}
|
|
2156
|
-
// Anima ControlNet-LLLite:lllite_name 从 models/controlnet 目录读取,只挑 anima-lllite 系权重
|
|
2157
|
-
if (info.AnimaLLLiteApply_sdscripts) {
|
|
2158
|
-
const llliteModels = availableModels(info, 'AnimaLLLiteApply_sdscripts', 'lllite_name')
|
|
2159
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i LLLite 节点存在,models/controlnet 文件列表=${JSON.stringify(llliteModels)}`)
|
|
2160
|
-
if (llliteModels.length) {
|
|
2161
|
-
const configured = String(cfg.controlNetModel || '').trim()
|
|
2162
|
-
const norm = (s) => String(s).toLowerCase().replace(/[-_ ]/g, '')
|
|
2163
|
-
const prefers = ['anima-lllite-lineart', 'anima-lllite', 'lllite-lineart', 'lllite']
|
|
2164
|
-
const pool = llliteModels.filter((m) => /lllite/i.test(String(m)))
|
|
2165
|
-
const scored = pool.map((m) => ({ m, s: prefers.reduce((acc, p, i) => acc + (norm(m) === norm(p) ? prefers.length - i : 0), 0) }))
|
|
2166
|
-
scored.sort((a, b) => b.s - a.s)
|
|
2167
|
-
const best = (scored[0] && scored[0].m) || null
|
|
2168
|
-
caps.controlNet.model = configured && llliteModels.includes(configured) ? configured : best
|
|
2169
|
-
caps.controlNet.available = !!caps.controlNet.model
|
|
2170
|
-
}
|
|
2171
|
-
// 换风格专用主模型必须是 28-block(与 LLLite 权重匹配),检测它是否已安装
|
|
2172
|
-
const styleModel = String(cfg.i2iUnetName || '').trim() || cfg.unetName
|
|
2173
|
-
const unetList = availableModels(info, 'UNETLoader', 'unet_name')
|
|
2174
|
-
if (unetList.length && !unetList.includes(styleModel)) {
|
|
2175
|
-
logger.warn(`[p-draw] i2i 换风格专用主模型 ${styleModel} 不在 models/diffusion_models 中(现有:${JSON.stringify(unetList)})。LLLite 权重为 28-block,请安装 anima-base-v1.0 或修改 i2iUnetName 配置,否则换风格会报 depth_embed slices missing`)
|
|
2176
|
-
caps.controlNet.modelMismatch = true
|
|
2177
|
-
}
|
|
2178
|
-
} else if (cfg.outputLogs) {
|
|
2179
|
-
logger.info('[p-draw] i2i 未检测到 AnimaLLLiteApply_sdscripts 节点(请确认已安装 kohya-ss/ComfyUI-Anima-LLLite 并重启 ComfyUI)')
|
|
2180
|
-
}
|
|
2181
|
-
if (info.AnimaIPAdapterLoader && info.AnimaIPAdapterApply && info.AnimaSiglipeEncodeImage) {
|
|
2182
|
-
caps.ipAdapter.available = true
|
|
2183
|
-
}
|
|
2184
|
-
if (cfg.outputLogs) logger.info(`[p-draw] i2i 能力检测:ControlNet(LLLite)=${JSON.stringify(caps.controlNet)} IPAdapter=${JSON.stringify(caps.ipAdapter)}`)
|
|
2185
|
-
return caps
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// 交互式询问 i2i 处理模式,返回 'style' / 'ootd' / 'cancel' / null(超时或无法询问时 null)
|
|
2189
|
-
async function askI2IMode(session, i18nAsk) {
|
|
2190
|
-
if (typeof session.prompt !== 'function') return null
|
|
2191
|
-
await session.send(i18nAsk || session.text('.i2i-mode-ask'))
|
|
2192
|
-
const reply = await session.prompt((cfg.i2iAskTimeout || 60) * 1000).catch(() => null)
|
|
2193
|
-
const ans = String((reply && (reply.content != null ? reply.content : reply)) || '').trim()
|
|
2194
|
-
if (/^(1|①|换风格|风格|漫画|漫画化|style|stylechange)$/i.test(ans)) return 'style'
|
|
2195
|
-
if (/^(2|②|换装|换装换姿势|换衣服|换姿势|ootd|outfit)$/i.test(ans)) return 'ootd'
|
|
2196
|
-
if (/^(3|③|取消|不生成|不要|算了|cancel|no)$/i.test(ans)) return 'cancel'
|
|
2197
|
-
if (!ans) return null
|
|
2198
|
-
await session.send(session.text('.i2i-mode-invalid'))
|
|
2199
|
-
return askI2IMode(session, i18nAsk)
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
async function handleGenerate(session, rawText, i2iImage = null, taggerTags = '', i2iOpts = null) {
|
|
1801
|
+
async function handleGenerate(session, rawText) {
|
|
2203
1802
|
const USERID = session.userId
|
|
2204
1803
|
const isAdmin = isAdminUser(session)
|
|
2205
1804
|
const unet = await resolveUnet(USERID)
|
|
2206
|
-
const i2iRule = i2iImage
|
|
2207
|
-
? '这是以图生图(img2img):原图的构图、主体与色调会被保留。请主要描述你希望发生的变化(风格、服装、表情、场景改造、细节调整等),不要重复描述原图已有的细节。'
|
|
2208
|
-
+ (taggerTags
|
|
2209
|
-
? `\n\n原图内容参考(本地识图自动识别出的标签,用于了解原图已包含的内容;不要照抄全部标签,只参考与用户改动需求相关的部分):\n${taggerTags}`
|
|
2210
|
-
: '')
|
|
2211
|
-
: ''
|
|
2212
1805
|
if (cfg.outputLogs) {
|
|
2213
1806
|
logger.info(`[p-draw] 请求 userId=${USERID} isAdmin=${isAdmin} adminUsers=${JSON.stringify(cfg.adminUsers || [])} normalizeId=${normalizeId(USERID)}`)
|
|
2214
1807
|
}
|
|
@@ -2222,11 +1815,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2222
1815
|
const parsedBatch = parseBatchCount(parsedSize.prompt, cfg.batchMax)
|
|
2223
1816
|
// 固定种子解析(--seed:xxx / --seed xxx / --seed=xxx),并从提示词中剥离
|
|
2224
1817
|
const parsedSeed = parseSeed(parsedBatch.prompt)
|
|
2225
|
-
|
|
2226
|
-
const parsedDenoise = i2iImage ? parseDenoise(parsedSeed.prompt) : { denoise: null, prompt: parsedSeed.prompt }
|
|
2227
|
-
const text = parsedDenoise.prompt
|
|
1818
|
+
const text = parsedSeed.prompt
|
|
2228
1819
|
const seed = parsedSeed.seed
|
|
2229
|
-
const denoise = parsedDenoise.denoise
|
|
2230
1820
|
const count = parsedBatch.count
|
|
2231
1821
|
|
|
2232
1822
|
// P 点校验(按总价 = 张数 × 单价)
|
|
@@ -2234,12 +1824,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2234
1824
|
const pcheck = await precheckPoints(session, USERID, isAdmin, count * cfg.price)
|
|
2235
1825
|
if (!pcheck.ok) return pcheck.message
|
|
2236
1826
|
|
|
2237
|
-
// 原样模式
|
|
2238
|
-
const stripped = stripRawPrefix(text)
|
|
2239
|
-
const raw = stripped.raw
|
|
2240
|
-
const promptSections = splitPositiveNegativePrompt(stripped.prompt)
|
|
2241
|
-
const userPrompt = promptSections.positive
|
|
2242
|
-
const userNegativePrompt = promptSections.negative
|
|
1827
|
+
// 原样模式
|
|
1828
|
+
const stripped = stripRawPrefix(text)
|
|
1829
|
+
const raw = stripped.raw
|
|
1830
|
+
const promptSections = splitPositiveNegativePrompt(stripped.prompt)
|
|
1831
|
+
const userPrompt = promptSections.positive
|
|
1832
|
+
const userNegativePrompt = promptSections.negative
|
|
2243
1833
|
if (!userPrompt) return session.text('.no-prompt')
|
|
2244
1834
|
|
|
2245
1835
|
// ComfyUI 就绪
|
|
@@ -2261,13 +1851,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2261
1851
|
const tokenOpt = !globalOpt && !isAdmin && cfg.llmModel && cfg.llmBaseUrl
|
|
2262
1852
|
if (globalOpt) {
|
|
2263
1853
|
// 全局优化开启:一次优化,整批复用同一提示词
|
|
2264
|
-
const optimized = await optimizePrompt(session, userPrompt, false
|
|
1854
|
+
const optimized = await optimizePrompt(session, userPrompt, false)
|
|
2265
1855
|
finalPrompt = optimized.prompt
|
|
2266
1856
|
degraded = !optimized.ok
|
|
2267
1857
|
optimizedReason = optimized.reason || ''
|
|
2268
1858
|
} else if (adminOpt) {
|
|
2269
1859
|
// 管理员在全局关闭时也免费优化(不耗券)
|
|
2270
|
-
const optimized = await optimizePrompt(session, userPrompt, true
|
|
1860
|
+
const optimized = await optimizePrompt(session, userPrompt, true)
|
|
2271
1861
|
finalPrompt = optimized.prompt
|
|
2272
1862
|
degraded = !optimized.ok
|
|
2273
1863
|
optimizedReason = optimized.reason || ''
|
|
@@ -2308,7 +1898,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2308
1898
|
}
|
|
2309
1899
|
// 非按张优化模式:直接拼好整批复用的提示词
|
|
2310
1900
|
if (!perImageOptimize) {
|
|
2311
|
-
const composed = composePrompt(finalPrompt, raw)
|
|
1901
|
+
const composed = await composePrompt(finalPrompt, raw)
|
|
2312
1902
|
finalPrompt = appendInlineProtectedTags(composed.prompt, userPrompt, raw)
|
|
2313
1903
|
degraded = degraded || composed.degraded
|
|
2314
1904
|
}
|
|
@@ -2319,16 +1909,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2319
1909
|
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 已扣除 ${count * cfg.price} P 点(${count} 张 × ${cfg.price}),余额 ${saving - count * cfg.price}`)
|
|
2320
1910
|
}
|
|
2321
1911
|
|
|
2322
|
-
//
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
: {}
|
|
2326
|
-
|
|
2327
|
-
const generationOverrides = Object.assign(
|
|
2328
|
-
{ unet, seed },
|
|
2329
|
-
i2iRun,
|
|
2330
|
-
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
2331
|
-
)
|
|
1912
|
+
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
1913
|
+
const generationOverrides = Object.assign(
|
|
1914
|
+
{ unet, seed },
|
|
1915
|
+
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
1916
|
+
)
|
|
2332
1917
|
|
|
2333
1918
|
// 性能:按张优化(perImageOptimize)时联网搜索只做一次,各图复用同一份结果
|
|
2334
1919
|
const searchCache = perImageOptimize && wantsWebSearch(userPrompt) ? await webSearch(userPrompt) : null
|
|
@@ -2337,11 +1922,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2337
1922
|
const queued = await enqueueBatch(count, async (i) => {
|
|
2338
1923
|
let p = finalPrompt
|
|
2339
1924
|
if (perImageOptimize) {
|
|
2340
|
-
const optimized = await optimizePrompt(session, userPrompt, true,
|
|
2341
|
-
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
1925
|
+
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
1926
|
+
p = appendInlineProtectedTags((await composePrompt(optimized.prompt || userPrompt, raw)).prompt, userPrompt, raw)
|
|
2342
1927
|
}
|
|
2343
|
-
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
2344
|
-
return { ...generated, prompt: p }
|
|
1928
|
+
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
1929
|
+
return { ...generated, prompt: p }
|
|
2345
1930
|
}, { USERID, isAdmin, totalPrice: count * cfg.price })
|
|
2346
1931
|
if (!queued.ok) return queued.message
|
|
2347
1932
|
const queuedTasks = queued.tasks
|
|
@@ -2364,45 +1949,52 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2364
1949
|
notice.push(session.text('.no-optimize', [reasons[noOptimizeReason] || noOptimizeReason]))
|
|
2365
1950
|
}
|
|
2366
1951
|
if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
|
|
2367
|
-
const feedback = feedbackBase(session, { firstPosition, count
|
|
2368
|
-
notice.push(...feedback.notices)
|
|
2369
|
-
await sendNotices(session, notice)
|
|
2370
|
-
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1952
|
+
const feedback = feedbackBase(session, { firstPosition, count })
|
|
1953
|
+
notice.push(...feedback.notices)
|
|
1954
|
+
await sendNotices(session, notice)
|
|
2371
1955
|
|
|
2372
1956
|
// 单张生成
|
|
2373
1957
|
const runOne = async (i) => {
|
|
2374
1958
|
let p = finalPrompt
|
|
2375
1959
|
if (perImageOptimize) {
|
|
2376
|
-
const optimized = await optimizePrompt(session, userPrompt, true,
|
|
2377
|
-
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
1960
|
+
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
1961
|
+
p = appendInlineProtectedTags((await composePrompt(optimized.prompt || userPrompt, raw)).prompt, userPrompt, raw)
|
|
2378
1962
|
}
|
|
2379
1963
|
let result
|
|
2380
1964
|
if (cfg.queueEnabled) {
|
|
2381
1965
|
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
2382
|
-
} else {
|
|
2383
|
-
try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
2384
|
-
}
|
|
2385
|
-
if (!result.prompt) result.prompt = p
|
|
2386
|
-
return result
|
|
1966
|
+
} else {
|
|
1967
|
+
try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1968
|
+
}
|
|
1969
|
+
if (!result.prompt) result.prompt = p
|
|
1970
|
+
return result
|
|
2387
1971
|
}
|
|
2388
1972
|
|
|
2389
1973
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, cfg.price, runOne)
|
|
2390
1974
|
|
|
2391
|
-
// 汇总
|
|
2392
|
-
const allOutputs = []
|
|
2393
|
-
const forwardOutputs = []
|
|
2394
|
-
const seeds = []
|
|
2395
|
-
const failures = []
|
|
2396
|
-
for (const item of results) {
|
|
2397
|
-
if (item.ok) {
|
|
2398
|
-
allOutputs.push(...item.outputs)
|
|
2399
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
2400
|
-
if (item.seed != null) seeds.push(item.seed)
|
|
1975
|
+
// 汇总
|
|
1976
|
+
const allOutputs = []
|
|
1977
|
+
const forwardOutputs = []
|
|
1978
|
+
const seeds = []
|
|
1979
|
+
const failures = []
|
|
1980
|
+
for (const item of results) {
|
|
1981
|
+
if (item.ok) {
|
|
1982
|
+
allOutputs.push(...item.outputs)
|
|
1983
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
1984
|
+
if (item.seed != null) seeds.push(item.seed)
|
|
2401
1985
|
} else {
|
|
2402
1986
|
failures.push(`第 ${item.i + 1} 张:${item.message}`)
|
|
2403
1987
|
}
|
|
2404
1988
|
}
|
|
2405
1989
|
|
|
1990
|
+
const chargeNotice = buildChargeNotice({
|
|
1991
|
+
isAdmin,
|
|
1992
|
+
totalPrice: successCount * cfg.price,
|
|
1993
|
+
unetName: unet,
|
|
1994
|
+
seeds,
|
|
1995
|
+
})
|
|
1996
|
+
await sendNotices(session, [chargeNotice], { quote: true })
|
|
1997
|
+
|
|
2406
1998
|
if (!allOutputs.length) {
|
|
2407
1999
|
if (cfg.outputLogs) logger.warn(`生成全部失败(${USERID}),已按张退款`)
|
|
2408
2000
|
return session.text('.generate-failed', ['全部失败(已按张退款)'])
|
|
@@ -2410,8 +2002,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2410
2002
|
|
|
2411
2003
|
if (cfg.outputLogs) logger.success(`${USERID} 生成成功 ${successCount}/${count} 张`)
|
|
2412
2004
|
|
|
2413
|
-
// 发图:合并转发,不引用原指令
|
|
2414
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
2005
|
+
// 发图:合并转发,不引用原指令
|
|
2006
|
+
await sendImagesAsForward(session, forwardOutputs)
|
|
2415
2007
|
|
|
2416
2008
|
const reply = []
|
|
2417
2009
|
if (count > 1) {
|
|
@@ -2550,8 +2142,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2550
2142
|
return price
|
|
2551
2143
|
}
|
|
2552
2144
|
|
|
2553
|
-
//
|
|
2145
|
+
// 启动时加载配置表,并将旧 fixedCharacters 数据迁入专用表。
|
|
2554
2146
|
await loadRuntimeState()
|
|
2147
|
+
await migrateFixedCharacters()
|
|
2555
2148
|
|
|
2556
2149
|
ctx.on('dispose', () => {
|
|
2557
2150
|
// 清理临时文件
|
|
@@ -2564,5 +2157,5 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2564
2157
|
})
|
|
2565
2158
|
|
|
2566
2159
|
// 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
|
|
2567
|
-
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice,
|
|
2568
|
-
}
|
|
2160
|
+
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, buildChargeNotice, sendNotices, sendImagesAsForward, executeBatch }
|
|
2161
|
+
}
|