koishi-plugin-p-draw 1.4.0 → 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 +286 -424
- package/lib/i18n.js +126 -129
- package/lib/media.js +22 -22
- package/lib/workflows.js +169 -138
- package/package.json +1 -1
- package/readme.md +10 -11
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 } = require('url')
|
|
6
|
+
const { pathToFileURL } = require('url')
|
|
7
7
|
|
|
8
8
|
exports.name = 'p-draw'
|
|
9
9
|
|
|
@@ -28,9 +28,9 @@ exports.usage = `
|
|
|
28
28
|
- **联网搜索:** 描述中带 \`联网\` / \`搜索\` / \`查一下\` 等词时,会先联网搜索补充角色设定(需配置 Tavily Key)。
|
|
29
29
|
- **画师组:** \`创建画师组 名称=tags\` \`切换画师组 名称\` \`查看画师组\` \`删除画师组 名称\`
|
|
30
30
|
- **固定角色:** \`添加角色 名称=tags\`
|
|
31
|
-
`;
|
|
31
|
+
`;
|
|
32
32
|
|
|
33
|
-
const { zhCN } = require('./lib/i18n')
|
|
33
|
+
const { zhCN } = require('./lib/i18n')
|
|
34
34
|
exports.Config = Schema.object({
|
|
35
35
|
// ComfyUI 连接
|
|
36
36
|
comfyuiBaseUrl: Schema.string().default('http://127.0.0.1:8188').description('ComfyUI 地址'),
|
|
@@ -81,6 +81,7 @@ exports.Config = Schema.object({
|
|
|
81
81
|
activeArtistPreset: Schema.string().default('').description('启用的画师组名称'),
|
|
82
82
|
defaultArtistTags: Schema.string().default('').description('备用画师 tags'),
|
|
83
83
|
styleTags: Schema.string().default('').description('画风 tags'),
|
|
84
|
+
fixedCharacters: Schema.array(Schema.string()).default([]).description('固定角色(格式:角色名=tags;仅用于兼容旧配置,运行时数据保存在数据库)'),
|
|
84
85
|
|
|
85
86
|
// 队列
|
|
86
87
|
queueEnabled: Schema.boolean().default(true).description('启用生成队列(逐张顺序执行)'),
|
|
@@ -92,7 +93,6 @@ exports.Config = Schema.object({
|
|
|
92
93
|
multiPrice: Schema.number().default(900).description('多人指令(p-draw 多人)单张消耗的 P 点'),
|
|
93
94
|
couponPrice: Schema.number().default(3000).description('提示词优化券单价(P 点/张,购买询问时显示;可自动读取 data/p-shop.json 里的价格覆盖)'),
|
|
94
95
|
couponAskTimeout: Schema.number().default(60).description('提示词优化券确认等待时间(秒)'),
|
|
95
|
-
seriesAskTimeout: Schema.number().default(60).description('连续图 LLM 使用确认等待时间(秒)'),
|
|
96
96
|
adminUsers: Schema.array(Schema.string()).default([]).description('免 P 点管理员用户 ID 列表'),
|
|
97
97
|
outputLogs: Schema.boolean().default(true).description('是否在控制台输出详细日志'),
|
|
98
98
|
|
|
@@ -115,27 +115,27 @@ exports.Config = Schema.object({
|
|
|
115
115
|
'zh-CN': zhCN,
|
|
116
116
|
})
|
|
117
117
|
|
|
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,
|
|
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')
|
|
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')
|
|
139
139
|
exports.apply = async function apply(ctx, cfg) {
|
|
140
140
|
// 注意:不在此处 extend p_system 表 —— 该表由 p-qiandao 等 p 系插件创建。
|
|
141
141
|
// 重复声明同一张表可能导致 Koishi 的 schema 迁移冲突,拖垮签到插件。
|
|
@@ -143,13 +143,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
143
143
|
const logger = ctx.logger('p-draw')
|
|
144
144
|
ctx.i18n.define('zh-CN', zhCN)
|
|
145
145
|
|
|
146
|
-
//
|
|
146
|
+
// 运行时数据持久化到数据库,而不是调用 scope.update
|
|
147
147
|
// 写 koishi.yml:scope.update 会触发插件重载,导致正在生成的图被 dispose(Context has
|
|
148
|
-
// been disposed
|
|
148
|
+
// been disposed),且重复写入时配置文件会被冲掉(曾出现配置整体恢复成默认)。
|
|
149
149
|
try {
|
|
150
150
|
ctx.model.extend('p_draw_config', {
|
|
151
151
|
id: 'unsigned',
|
|
152
152
|
fixed_characters: 'json',
|
|
153
|
+
fixed_characters_migrated: 'boolean',
|
|
153
154
|
artist_presets: 'json',
|
|
154
155
|
active_artist_preset: 'text',
|
|
155
156
|
default_artist_tags: 'text',
|
|
@@ -159,6 +160,16 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
159
160
|
logger.warn(`p_draw_config 表初始化失败:${e.message}`)
|
|
160
161
|
}
|
|
161
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
|
+
|
|
162
173
|
// 用户自选模型偏好(userid -> unet 文件名),持久化在 p_draw_config.user_models
|
|
163
174
|
if (!cfg.userModels || typeof cfg.userModels !== 'object') cfg.userModels = {}
|
|
164
175
|
|
|
@@ -240,14 +251,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
240
251
|
if (objectInfo) {
|
|
241
252
|
const unetList = availableModels(objectInfo, 'UNETLoader', 'unet_name')
|
|
242
253
|
const clipList = availableModels(objectInfo, 'CLIPLoader', 'clip_name')
|
|
243
|
-
const vaeList = availableModels(objectInfo, 'VAELoader', 'vae_name')
|
|
244
|
-
payload.unet_available = unetList.includes(cfg.unetName)
|
|
245
|
-
payload.unet_models = unetList
|
|
246
|
-
payload.clip_available = clipList.includes(cfg.clipName)
|
|
247
|
-
payload.vae_available = vaeList.includes(cfg.vaeName)
|
|
254
|
+
const vaeList = availableModels(objectInfo, 'VAELoader', 'vae_name')
|
|
255
|
+
payload.unet_available = unetList.includes(cfg.unetName)
|
|
256
|
+
payload.unet_models = unetList
|
|
257
|
+
payload.clip_available = clipList.includes(cfg.clipName)
|
|
258
|
+
payload.vae_available = vaeList.includes(cfg.vaeName)
|
|
248
259
|
} else {
|
|
249
|
-
payload.unet_available = undefined
|
|
250
|
-
payload.clip_available = undefined
|
|
260
|
+
payload.unet_available = undefined
|
|
261
|
+
payload.clip_available = undefined
|
|
251
262
|
payload.vae_available = undefined
|
|
252
263
|
}
|
|
253
264
|
return payload
|
|
@@ -290,9 +301,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
290
301
|
const lines = [
|
|
291
302
|
`ComfyUI 状态:在线`,
|
|
292
303
|
`版本:${payload.comfyui_version || '未知'}`,
|
|
293
|
-
`GPU:${payload.gpu || '未知'}(显存 ${payload.vram_total_mb}MB / 空闲 ${payload.vram_free_mb}MB)`,
|
|
294
|
-
`主模型:${cfg.unetName} ${modelStatus(cfg.unetName, payload.unet_available)}`,
|
|
295
|
-
`文本编码器:${cfg.clipName} ${modelStatus(cfg.clipName, payload.clip_available)}`,
|
|
304
|
+
`GPU:${payload.gpu || '未知'}(显存 ${payload.vram_total_mb}MB / 空闲 ${payload.vram_free_mb}MB)`,
|
|
305
|
+
`主模型:${cfg.unetName} ${modelStatus(cfg.unetName, payload.unet_available)}`,
|
|
306
|
+
`文本编码器:${cfg.clipName} ${modelStatus(cfg.clipName, payload.clip_available)}`,
|
|
296
307
|
`VAE:${cfg.vaeName} ${modelStatus(cfg.vaeName, payload.vae_available)}`,
|
|
297
308
|
`可用尺寸:${payload.allowed_sizes.join('、')}`,
|
|
298
309
|
]
|
|
@@ -403,7 +414,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
403
414
|
return String(cfg.styleTags).trim()
|
|
404
415
|
}
|
|
405
416
|
|
|
406
|
-
// P
|
|
417
|
+
// P 点余额预检(单图/多人共用)
|
|
407
418
|
async function precheckPoints(session, USERID, isAdmin, totalPrice) {
|
|
408
419
|
if (isAdmin) return { ok: true }
|
|
409
420
|
const notExists = await isAccountExists(USERID)
|
|
@@ -438,56 +449,58 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
438
449
|
return { ok: true, tasks, firstPosition }
|
|
439
450
|
}
|
|
440
451
|
|
|
441
|
-
//
|
|
442
|
-
function feedbackBase(session, { firstPosition, count
|
|
443
|
-
const notices = []
|
|
444
|
-
if (cfg.queueEnabled) {
|
|
445
|
-
notices.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
|
|
446
|
-
if (count > 1) notices.push(session.text('.batch-count', [count]))
|
|
447
|
-
} else {
|
|
448
|
-
notices.push(session.text('.generating'))
|
|
449
|
-
if (count > 1) notices.push(session.text('.batch-count', [count]))
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
504
|
|
|
492
505
|
// P 点读改写按用户串行化,避免并发指令互相覆盖余额
|
|
493
506
|
const userLocks = new Map()
|
|
@@ -499,17 +512,22 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
499
512
|
}
|
|
500
513
|
|
|
501
514
|
// ---------------- 用户自选模型 ----------------
|
|
502
|
-
// 从 ComfyUI /object_info(10
|
|
515
|
+
// 从 ComfyUI /object_info(10 分钟缓存)读取 UNET 和 checkpoint 模型列表
|
|
503
516
|
async function listUnetModels() {
|
|
504
517
|
try {
|
|
505
518
|
const objectInfo = await getObjectInfoCached()
|
|
506
|
-
const list =
|
|
507
|
-
|
|
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
|
|
508
526
|
} catch (e) { /* ignore */ }
|
|
509
527
|
return []
|
|
510
528
|
}
|
|
511
529
|
|
|
512
|
-
//
|
|
530
|
+
// 解析该用户当前生效的模型:有偏好且仍存在于 ComfyUI 时用偏好,否则回落默认
|
|
513
531
|
async function resolveUnet(USERID) {
|
|
514
532
|
const chosen = cfg.userModels && cfg.userModels[USERID]
|
|
515
533
|
if (!chosen || !String(chosen).trim()) return cfg.unetName
|
|
@@ -559,12 +577,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
559
577
|
|
|
560
578
|
async function runComfyGenerate(prompt, size, overrides) {
|
|
561
579
|
const sizes = parseAllowedSizes()
|
|
562
|
-
const requestedWidth = (size && size[0]) || overrides.width || cfg.width
|
|
563
|
-
const requestedHeight = (size && size[1]) || overrides.height || cfg.height
|
|
564
580
|
const unetName = overrides.unet || cfg.unetName
|
|
565
581
|
// 应用该模型的独立参数覆盖(modelParams),再叠加命令级 overrides(overrides.steps/cfg 优先于模型级)
|
|
566
582
|
const modelSpecific = resolveModelParams(unetName)
|
|
567
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
|
|
568
586
|
// 防御:sampler/scheduler 配置若带尾随空格会导致 ComfyUI 报 "Value not in list",统一 trim
|
|
569
587
|
if (typeof workCfg.samplerName === 'string') workCfg.samplerName = workCfg.samplerName.trim()
|
|
570
588
|
if (typeof workCfg.scheduler === 'string') workCfg.scheduler = workCfg.scheduler.trim()
|
|
@@ -574,7 +592,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
574
592
|
const cfgVal = Number(overrides.cfg) || workCfg.cfg
|
|
575
593
|
const seed = Number(overrides.seed) || crypto.randomInt(1, 2 ** 32 - 1)
|
|
576
594
|
const negativePrompt = joinPromptParts([overrides.negativePrompt || cfg.negativePrompt || ''])
|
|
577
|
-
const promptBody = 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))
|
|
578
596
|
|
|
579
597
|
const clientId = crypto.randomUUID()
|
|
580
598
|
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
@@ -634,10 +652,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
634
652
|
width,
|
|
635
653
|
height,
|
|
636
654
|
steps,
|
|
637
|
-
cfg: cfgVal,
|
|
638
|
-
prompt_id: promptId,
|
|
639
|
-
negativePrompt,
|
|
640
|
-
}
|
|
655
|
+
cfg: cfgVal,
|
|
656
|
+
prompt_id: promptId,
|
|
657
|
+
negativePrompt,
|
|
658
|
+
}
|
|
641
659
|
}
|
|
642
660
|
|
|
643
661
|
// ---------------- LLM 提示词优化 ----------------
|
|
@@ -679,9 +697,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
679
697
|
}
|
|
680
698
|
|
|
681
699
|
// 把命中的固定角色 tags 渲染进优化模板的 {character_rule} 占位符。
|
|
682
|
-
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) {
|
|
683
706
|
const text = String(prompt || '')
|
|
684
|
-
for (const
|
|
707
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
685
708
|
if (name && text.includes(name)) {
|
|
686
709
|
return `用户提到了固定角色「${name}」,其外观 tags 为:${tags} 请优先保留这些特征。`
|
|
687
710
|
}
|
|
@@ -741,7 +764,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
741
764
|
}
|
|
742
765
|
}
|
|
743
766
|
|
|
744
|
-
async function optimizePrompt(session, userPrompt, force = false, precomputedSearch = null) {
|
|
767
|
+
async function optimizePrompt(session, userPrompt, force = false, precomputedSearch = null) {
|
|
745
768
|
if (!cfg.promptOptimizeEnabled && !force) {
|
|
746
769
|
return { ok: true, prompt: userPrompt, reason: 'optimize_disabled' }
|
|
747
770
|
}
|
|
@@ -756,8 +779,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
756
779
|
} else if (wantsWebSearch(userPrompt)) {
|
|
757
780
|
searchBlock = await webSearch(userPrompt)
|
|
758
781
|
}
|
|
759
|
-
const characterRule = buildCharacterRule(userPrompt)
|
|
760
|
-
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}`
|
|
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}`
|
|
761
784
|
const template = (cfg.promptOptimizeTemplate || '').trim() || defaultTemplate
|
|
762
785
|
const searchBlockText = searchBlock
|
|
763
786
|
? `联网搜索参考信息(请尽量依据这些内容补全角色外观与设定):\n${searchBlock}`
|
|
@@ -815,54 +838,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
815
838
|
}
|
|
816
839
|
}
|
|
817
840
|
|
|
818
|
-
function extractSeriesOptimizeJson(text) {
|
|
819
|
-
let raw = String(text || '').trim()
|
|
820
|
-
if (raw.startsWith('```')) raw = (raw.match(/```(?:json)?([\s\S]*?)```/) || [null, raw])[1].trim()
|
|
821
|
-
const start = raw.indexOf('{')
|
|
822
|
-
const end = raw.lastIndexOf('}')
|
|
823
|
-
if (start === -1 || end === -1 || end <= start) return null
|
|
824
|
-
try { return JSON.parse(raw.slice(start, end + 1)) } catch (e) { return null }
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function filterFixedTags(tags, drops) {
|
|
828
|
-
if (!tags) return ''
|
|
829
|
-
const dropKeys = (drops || []).map(d => canonicalTagText(String(d))).filter(Boolean)
|
|
830
|
-
if (!dropKeys.length) return tags
|
|
831
|
-
return splitTags(tags)
|
|
832
|
-
.filter(t => !dropKeys.some(k => k && canonicalTagText(t).includes(k)))
|
|
833
|
-
.join(', ')
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// 连续图专用的阶段优化:LLM 把「角色 + 阶段描述」转成 Danbooru tags,并返回
|
|
837
|
-
// 要从固定角色 tags 中移除的冲突项(如固定 silver hair、阶段变成 black hair)。
|
|
838
|
-
async function optimizeSeriesStage(userPrompt, identity) {
|
|
839
|
-
if (!cfg.llmModel || !cfg.llmBaseUrl) {
|
|
840
|
-
return { ok: false, prompt: userPrompt, drops: [], reason: 'llm_not_configured' }
|
|
841
|
-
}
|
|
842
|
-
const fixedTags = identity && parsePresetList(cfg.fixedCharacters)[identity]
|
|
843
|
-
? parsePresetList(cfg.fixedCharacters)[identity]
|
|
844
|
-
: ''
|
|
845
|
-
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}`
|
|
846
|
-
const rendered = template.replace(/\{theme\}/g, userPrompt)
|
|
847
|
-
try {
|
|
848
|
-
const text = await llmChat({ system: rendered, user: userPrompt, maxTokens: Math.min(parseInt(cfg.llmMaxTokens) || 700, 900) })
|
|
849
|
-
const data = extractSeriesOptimizeJson(text)
|
|
850
|
-
if (data && String(data.stage_tags || '').trim()) {
|
|
851
|
-
const drops = Array.isArray(data.drop_fixed) ? data.drop_fixed.map(String).filter(Boolean) : []
|
|
852
|
-
return { ok: true, prompt: String(data.stage_tags).trim(), drops, reason: '' }
|
|
853
|
-
}
|
|
854
|
-
// JSON 解析失败:把整段文本当作阶段 tags,不剔除固定标签
|
|
855
|
-
return { ok: true, prompt: text, drops: [], reason: '' }
|
|
856
|
-
} catch (e) {
|
|
857
|
-
const reason = String(e && e.message || e)
|
|
858
|
-
logger.warn(`连续图阶段优化失败:${reason}`)
|
|
859
|
-
return { ok: false, prompt: userPrompt, drops: [], reason }
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
841
|
// ---------------- 提示词组装 ----------------
|
|
864
842
|
// fixedOverride:undefined=按名称自动匹配固定角色;'skip'=不注入固定角色;字符串=直接使用该字符串作为固定角色 tags
|
|
865
|
-
function composePrompt(userPrompt, raw, fixedOverride) {
|
|
843
|
+
async function composePrompt(userPrompt, raw, fixedOverride) {
|
|
866
844
|
if (raw) return { prompt: userPrompt, degraded: false }
|
|
867
845
|
const parts = []
|
|
868
846
|
if (cfg.qualityPrefix) parts.push(String(cfg.qualityPrefix).trim())
|
|
@@ -871,7 +849,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
871
849
|
} else if (typeof fixedOverride === 'string') {
|
|
872
850
|
if (String(fixedOverride).trim()) parts.push(String(fixedOverride).trim())
|
|
873
851
|
} else {
|
|
874
|
-
for (const
|
|
852
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
875
853
|
if (name && userPrompt.includes(name)) {
|
|
876
854
|
parts.push(tags)
|
|
877
855
|
break
|
|
@@ -931,7 +909,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
931
909
|
// 多人规划:让 LLM 输出结构化场景 JSON(2-4 人)。
|
|
932
910
|
async function generateMultiPersonPlan(prompt) {
|
|
933
911
|
const mentioned = {}
|
|
934
|
-
for (const
|
|
912
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
935
913
|
if (name && prompt.includes(name)) mentioned[name] = tags
|
|
936
914
|
}
|
|
937
915
|
const planPrompt = buildMultiPersonPlanPrompt(prompt, mentioned)
|
|
@@ -977,7 +955,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
977
955
|
}
|
|
978
956
|
|
|
979
957
|
// 多人最终提示词组装:count/common tags + 角色块 + 互动 + 构图。
|
|
980
|
-
function buildMultiPersonFinalPrompt(plan, prompt) {
|
|
958
|
+
async function buildMultiPersonFinalPrompt(plan, prompt) {
|
|
981
959
|
const aliases = ['Character A', 'Character B', 'Character C', 'Character D']
|
|
982
960
|
const characterCount = plan.characters.length
|
|
983
961
|
const characterRoles = []
|
|
@@ -993,7 +971,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
993
971
|
|
|
994
972
|
const usedFixedNames = new Set()
|
|
995
973
|
const fixedGenders = []
|
|
996
|
-
const configuredChars =
|
|
974
|
+
const configuredChars = Object.fromEntries((await fixedCharacterRows()).map(({ name, tags }) => [name, tags]))
|
|
997
975
|
for (let index = 0; index < plan.characters.length; index++) {
|
|
998
976
|
const character = plan.characters[index]
|
|
999
977
|
let fixedName = ''
|
|
@@ -1162,11 +1140,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1162
1140
|
|
|
1163
1141
|
// 视觉校验(anima_verify + generation_verifier 移植):对生成的图片跑视觉 LLM,
|
|
1164
1142
|
// 不合格则用相同提示词重试(最多 multiCandidateCount 张),按多候选规则挑选并返回结果。
|
|
1165
|
-
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1143
|
+
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1166
1144
|
const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
|
|
1167
1145
|
const verifyModel = String(cfg.verifyLlmModel || '').trim()
|
|
1168
1146
|
if (!verifyBaseUrl || !verifyModel) {
|
|
1169
|
-
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 }
|
|
1170
1148
|
}
|
|
1171
1149
|
const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
|
|
1172
1150
|
const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
|
|
@@ -1174,10 +1152,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1174
1152
|
const systemPrompt = buildVerifySystemPrompt(true, planCount)
|
|
1175
1153
|
const candidates = []
|
|
1176
1154
|
let lastVerdict = null
|
|
1177
|
-
let retries = 0
|
|
1178
|
-
let currentImages = images
|
|
1179
|
-
let currentPrompt = prompt
|
|
1180
|
-
let currentNegativePrompt = negativePrompt
|
|
1155
|
+
let retries = 0
|
|
1156
|
+
let currentImages = images
|
|
1157
|
+
let currentPrompt = prompt
|
|
1158
|
+
let currentNegativePrompt = negativePrompt
|
|
1181
1159
|
|
|
1182
1160
|
async function verifyOnce(imgs, userReq) {
|
|
1183
1161
|
const controller = new AbortController()
|
|
@@ -1275,15 +1253,15 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1275
1253
|
data = extractVerifyJson(reply)
|
|
1276
1254
|
} catch (e) {
|
|
1277
1255
|
logger.warn(`多人视觉校验失败:${e.message}`)
|
|
1278
|
-
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 }
|
|
1279
1257
|
}
|
|
1280
1258
|
if (!data) {
|
|
1281
1259
|
logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
|
|
1282
|
-
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 }
|
|
1283
1261
|
}
|
|
1284
1262
|
const verdict = verdictFromData(data)
|
|
1285
1263
|
verdict.skipped = false
|
|
1286
|
-
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1264
|
+
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1287
1265
|
selectedOutputs = currentImages
|
|
1288
1266
|
selectedVerdict = verdict
|
|
1289
1267
|
|
|
@@ -1297,13 +1275,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1297
1275
|
if (hint) {
|
|
1298
1276
|
currentPrompt = `${userRequest}\n【上次问题,请修正】${hint}`
|
|
1299
1277
|
}
|
|
1300
|
-
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1278
|
+
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1301
1279
|
if (!regen.ok) {
|
|
1302
1280
|
logger.warn(`多人校验重试生成失败:${regen.message}`)
|
|
1303
1281
|
break
|
|
1304
1282
|
}
|
|
1305
|
-
currentImages = regen.outputs
|
|
1306
|
-
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1283
|
+
currentImages = regen.outputs
|
|
1284
|
+
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1307
1285
|
}
|
|
1308
1286
|
|
|
1309
1287
|
// 多候选挑选
|
|
@@ -1314,7 +1292,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1314
1292
|
selectedOutputs = best.outputs
|
|
1315
1293
|
selectedVerdict = best.verdict
|
|
1316
1294
|
if (!multiAccepted && !cfg.multiSendDegradedCandidate) {
|
|
1317
|
-
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 }
|
|
1318
1296
|
}
|
|
1319
1297
|
const noteParts = []
|
|
1320
1298
|
if (multiAccepted) {
|
|
@@ -1323,7 +1301,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1323
1301
|
noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
|
|
1324
1302
|
}
|
|
1325
1303
|
if (retries) noteParts.push(session.text('.multi-verify-failed', [selectedVerdict.issues.length ? ':' + selectedVerdict.issues.join(';').slice(0, 80) : '', retries]))
|
|
1326
|
-
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 }
|
|
1327
1305
|
}
|
|
1328
1306
|
|
|
1329
1307
|
function buildVerifySystemPrompt(multiPerson, planCount) {
|
|
@@ -1336,7 +1314,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1336
1314
|
}
|
|
1337
1315
|
|
|
1338
1316
|
// 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
|
|
1339
|
-
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1317
|
+
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1340
1318
|
async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
|
|
1341
1319
|
const results = []
|
|
1342
1320
|
let successCount = 0
|
|
@@ -1350,7 +1328,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1350
1328
|
const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
|
|
1351
1329
|
if (item.ok && outputs.length) {
|
|
1352
1330
|
successCount += 1
|
|
1353
|
-
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 || '' })
|
|
1354
1332
|
} else {
|
|
1355
1333
|
if (!isAdmin) await refundP(USERID, unitPrice)
|
|
1356
1334
|
if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
|
|
@@ -1407,7 +1385,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1407
1385
|
const plan = planResult.plan
|
|
1408
1386
|
|
|
1409
1387
|
// 组装最终提示词
|
|
1410
|
-
const built = buildMultiPersonFinalPrompt(plan, text)
|
|
1388
|
+
const built = await buildMultiPersonFinalPrompt(plan, text)
|
|
1411
1389
|
if (!built.ok) {
|
|
1412
1390
|
return session.text('.multi-usage') + '\n(多人规划失败:' + built.error + ')'
|
|
1413
1391
|
}
|
|
@@ -1431,10 +1409,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1431
1409
|
// 即时反馈
|
|
1432
1410
|
const notice = []
|
|
1433
1411
|
if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
|
|
1434
|
-
const feedback = feedbackBase(session, { firstPosition, count
|
|
1435
|
-
notice.push(...feedback.notices)
|
|
1436
|
-
await sendNotices(session, notice)
|
|
1437
|
-
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1412
|
+
const feedback = feedbackBase(session, { firstPosition, count })
|
|
1413
|
+
notice.push(...feedback.notices)
|
|
1414
|
+
await sendNotices(session, notice)
|
|
1438
1415
|
|
|
1439
1416
|
// 单张生成 +(可选)视觉校验
|
|
1440
1417
|
const runOne = async (i) => {
|
|
@@ -1444,27 +1421,27 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1444
1421
|
} else {
|
|
1445
1422
|
try { result = await runComfyGenerate(finalPrompt, size, { negativePrompt: multiNegative, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1446
1423
|
}
|
|
1447
|
-
if (!result.ok || !result.outputs || !result.outputs.length) return result
|
|
1448
|
-
if (!cfg.multiVerifyEnabled) {
|
|
1449
|
-
return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, negativePrompt: result.negativePrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
|
|
1450
|
-
}
|
|
1451
|
-
const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet, result.negativePrompt || multiNegative)
|
|
1452
|
-
if (!verified.ok) return { ok: false, message: verified.message }
|
|
1453
|
-
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 || '' }
|
|
1454
1431
|
}
|
|
1455
1432
|
|
|
1456
1433
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
|
|
1457
1434
|
|
|
1458
|
-
// 汇总
|
|
1459
|
-
const allOutputs = []
|
|
1460
|
-
const forwardOutputs = []
|
|
1461
|
-
const notes = []
|
|
1435
|
+
// 汇总
|
|
1436
|
+
const allOutputs = []
|
|
1437
|
+
const forwardOutputs = []
|
|
1438
|
+
const notes = []
|
|
1462
1439
|
const seeds = []
|
|
1463
1440
|
const failures = []
|
|
1464
1441
|
for (const item of results) {
|
|
1465
|
-
if (item.ok) {
|
|
1466
|
-
allOutputs.push(...item.outputs)
|
|
1467
|
-
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 })))
|
|
1468
1445
|
if (item.seed != null) seeds.push(item.seed)
|
|
1469
1446
|
if (item.note) notes.push(item.note)
|
|
1470
1447
|
} else {
|
|
@@ -1472,6 +1449,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1472
1449
|
}
|
|
1473
1450
|
}
|
|
1474
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
|
+
|
|
1475
1460
|
if (!allOutputs.length) {
|
|
1476
1461
|
if (cfg.outputLogs) logger.warn(`多人生成全部失败(${USERID}),已按张退款`)
|
|
1477
1462
|
return session.text('.generate-failed', ['全部失败(已按张退款)'])
|
|
@@ -1479,8 +1464,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1479
1464
|
|
|
1480
1465
|
if (cfg.outputLogs) logger.success(`${USERID} 多人生成成功 ${successCount}/${count} 张`)
|
|
1481
1466
|
|
|
1482
|
-
// 发图:合并转发,不引用原指令
|
|
1483
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
1467
|
+
// 发图:合并转发,不引用原指令
|
|
1468
|
+
await sendImagesAsForward(session, forwardOutputs)
|
|
1484
1469
|
|
|
1485
1470
|
const reply = []
|
|
1486
1471
|
if (count > 1) {
|
|
@@ -1493,173 +1478,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1493
1478
|
return reply.filter(Boolean).join('\n')
|
|
1494
1479
|
}
|
|
1495
1480
|
|
|
1496
|
-
// 连续图/过程图主流程:同一角色多阶段(固定身份 + 阶段描述 + 全阶段共用同一 seed 保证一致性)
|
|
1497
|
-
// 语法:连续 <角色>:<阶段1> → <阶段2> → ... 或 连续 <角色>:<阶段1>|<阶段2>|...
|
|
1498
|
-
// 询问是否使用 LLM 优化,返回 'yes' / 'no' / 'cancel' / null(无法交互或超时)
|
|
1499
|
-
async function askSeriesLLMUse(session) {
|
|
1500
|
-
if (typeof session.prompt !== 'function') return null
|
|
1501
|
-
await session.send(session.text('.series-llm-ask'))
|
|
1502
|
-
const reply = await session.prompt((cfg.seriesAskTimeout || 60) * 1000).catch(() => null)
|
|
1503
|
-
const ans = String((reply && (reply.content != null ? reply.content : reply)) || '').trim()
|
|
1504
|
-
if (/^(是|1|①|用|使用|使用llm|yes|y)$/i.test(ans)) return 'yes'
|
|
1505
|
-
if (/^(否|2|②|不用|不使用|不使用llm|不优化|不用llm|no|n)$/i.test(ans)) return 'no'
|
|
1506
|
-
if (/^(取消|3|③|算了|不生成|cancel|c)$/i.test(ans)) return 'cancel'
|
|
1507
|
-
if (!ans) return null
|
|
1508
|
-
await session.send(session.text('.series-llm-invalid'))
|
|
1509
|
-
return askSeriesLLMUse(session)
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
async function handleGenerateSeries(session, rawText) {
|
|
1513
|
-
const USERID = session.userId
|
|
1514
|
-
const isAdmin = isAdminUser(session)
|
|
1515
|
-
const unet = await resolveUnet(USERID)
|
|
1516
|
-
const price = Math.max(0, parseInt(cfg.price) || 500)
|
|
1517
|
-
|
|
1518
|
-
// 阶段分隔符:箭头 / 管道
|
|
1519
|
-
const STAGE_SEP = /→|➔|➜|←|↔|=>|->|⇒|\|/
|
|
1520
|
-
|
|
1521
|
-
// 尺寸解析(连续图默认横图);固定 seed:全阶段共用,支持 --seed 覆盖(含 --seed: 冒号形式)
|
|
1522
|
-
const seedInfo = parseSeed(String(rawText || ''))
|
|
1523
|
-
const seed = seedInfo.seed != null ? seedInfo.seed : crypto.randomInt(1, 2 ** 32 - 1)
|
|
1524
|
-
const allowed = parseAllowedSizes()
|
|
1525
|
-
const parsedSize = parseGenerationSize(seedInfo.prompt, allowed)
|
|
1526
|
-
if (parsedSize.error) return parsedSize.error
|
|
1527
|
-
let size = parsedSize.size
|
|
1528
|
-
if (!size && allowed.length) {
|
|
1529
|
-
size = allowed.reduce((best, s) => {
|
|
1530
|
-
const a = Math.abs(s[0] / s[1] - 16 / 9)
|
|
1531
|
-
const b = Math.abs(best[0] / best[1] - 16 / 9)
|
|
1532
|
-
return a < b ? s : best
|
|
1533
|
-
})
|
|
1534
|
-
}
|
|
1535
|
-
const sizeCleanedPrompt = parsedSize.prompt
|
|
1536
|
-
|
|
1537
|
-
// 提取身份与阶段文本(角色:阶段1 → 阶段2)
|
|
1538
|
-
let identity = ''
|
|
1539
|
-
let stageText = String(sizeCleanedPrompt || '').trim()
|
|
1540
|
-
const colonMatch = stageText.match(/^(.+?)[::]\s*(.+)$/)
|
|
1541
|
-
if (colonMatch) {
|
|
1542
|
-
identity = colonMatch[1].trim()
|
|
1543
|
-
stageText = colonMatch[2].trim()
|
|
1544
|
-
}
|
|
1545
|
-
const stages = stageText
|
|
1546
|
-
.split(STAGE_SEP)
|
|
1547
|
-
.map(s => s.trim().replace(/^[\s,,、;;::]+|[\s,,、;;::]+$/g, '').replace(/\s+/g, ' '))
|
|
1548
|
-
.filter(Boolean)
|
|
1549
|
-
if (!stages.length) return session.text('.series-usage')
|
|
1550
|
-
|
|
1551
|
-
const maxStages = Math.max(1, parseInt(cfg.batchMax) || 4)
|
|
1552
|
-
const count = Math.min(stages.length, maxStages)
|
|
1553
|
-
const clamped = stages.length > count
|
|
1554
|
-
const stageList = stages.slice(0, count)
|
|
1555
|
-
|
|
1556
|
-
// P 点校验(按总价)
|
|
1557
|
-
const pcheck = await precheckPoints(session, USERID, isAdmin, count * price)
|
|
1558
|
-
if (!pcheck.ok) return pcheck.message
|
|
1559
|
-
|
|
1560
|
-
// ComfyUI 就绪
|
|
1561
|
-
const ready = await ensureComfyuiReady()
|
|
1562
|
-
if (!ready.ok) return ready.message
|
|
1563
|
-
|
|
1564
|
-
// 询问是否使用 LLM 优化:是=用 LLM / 否=直接使用原始描述 / 取消=不生成。
|
|
1565
|
-
// 未配置 LLM 或平台不支持交互式询问时,沿用原有「配置了 LLM 就逐阶段优化」行为。
|
|
1566
|
-
let useLLM = true
|
|
1567
|
-
if (cfg.llmModel && cfg.llmBaseUrl && typeof session.prompt === 'function') {
|
|
1568
|
-
const choice = await askSeriesLLMUse(session)
|
|
1569
|
-
if (choice === 'cancel') return ''
|
|
1570
|
-
if (choice === 'no') useLLM = false
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// 组装各阶段提示词:身份(含固定角色 tags)+ 阶段描述。
|
|
1574
|
-
// 使用 LLM 时逐阶段优化(不受 promptOptimizeEnabled 限制),因为 anima 是
|
|
1575
|
-
// Danbooru-tag 模型,中文阶段描述必须转成 tags 才能体现在画面里;
|
|
1576
|
-
// 用户选择「否」或未配置 LLM 时,直接使用各阶段原始描述(不注入身份前缀)。
|
|
1577
|
-
const fixedChars = parsePresetList(cfg.fixedCharacters)
|
|
1578
|
-
const stagePrompts = []
|
|
1579
|
-
let degradedStages = 0
|
|
1580
|
-
for (const st of stageList) {
|
|
1581
|
-
const base = identity ? `${identity},${st}` : st
|
|
1582
|
-
const result = useLLM
|
|
1583
|
-
? await optimizeSeriesStage(base, identity)
|
|
1584
|
-
: { ok: true, prompt: st, drops: [], reason: 'user_skipped_llm' }
|
|
1585
|
-
if (!result.ok) degradedStages += 1
|
|
1586
|
-
const stageTags = result.prompt || base
|
|
1587
|
-
const drops = result.drops || []
|
|
1588
|
-
// 始终注入固定角色 tags 作为身份锚点(保证角色名/种族/尖耳朵出现),
|
|
1589
|
-
// 阶段描述里被明确改变的外观由 drop 列表剔除,避免被固定默认值拉回。
|
|
1590
|
-
const identityTags = identity ? filterFixedTags(fixedChars[identity] || '', drops) : ''
|
|
1591
|
-
const anchor = stageTags
|
|
1592
|
-
const composed = composePrompt(anchor, false, identityTags)
|
|
1593
|
-
stagePrompts.push(composed.prompt)
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// 扣 P 点(一次性扣除总价)
|
|
1597
|
-
if (!isAdmin) {
|
|
1598
|
-
const saving = await deductP(USERID, count * price)
|
|
1599
|
-
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 连续图已扣除 ${count * price} P 点(${count} 阶段 × ${price},seed=${seed}),余额 ${saving - count * price}`)
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// 队列:预排队全部阶段(先查容量再入队)
|
|
1603
|
-
const queued = await enqueueBatch(count, (i) => runComfyGenerate(stagePrompts[i], size, { seed, unet }), { USERID, isAdmin, totalPrice: count * price })
|
|
1604
|
-
if (!queued.ok) return queued.message
|
|
1605
|
-
const queuedTasks = queued.tasks
|
|
1606
|
-
const firstPosition = queued.firstPosition
|
|
1607
|
-
|
|
1608
|
-
// 即时反馈
|
|
1609
|
-
const notice = []
|
|
1610
|
-
if (clamped) notice.push(session.text('.batch-limit', [count]))
|
|
1611
|
-
if (identity && fixedChars[identity]) notice.push(`已固定角色「${identity}」的身份 tags,各阶段外观将保持一致。`)
|
|
1612
|
-
if (!useLLM) notice.push(session.text('.series-no-llm'))
|
|
1613
|
-
if (degradedStages) notice.push(session.text('.prompt-degraded', ['(连续图阶段优化失败,已使用原始描述)']))
|
|
1614
|
-
const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin })
|
|
1615
|
-
notice.push(...feedback.notices)
|
|
1616
|
-
await sendNotices(session, notice)
|
|
1617
|
-
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1618
|
-
|
|
1619
|
-
// 单阶段生成(共用 seed)
|
|
1620
|
-
const runOne = async (i) => {
|
|
1621
|
-
let result
|
|
1622
|
-
if (cfg.queueEnabled) {
|
|
1623
|
-
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1624
|
-
} else {
|
|
1625
|
-
try { result = await runComfyGenerate(stagePrompts[i], size, { seed, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1626
|
-
}
|
|
1627
|
-
return result
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
|
|
1631
|
-
|
|
1632
|
-
// 汇总
|
|
1633
|
-
const allOutputs = []
|
|
1634
|
-
const forwardOutputs = []
|
|
1635
|
-
const seeds = []
|
|
1636
|
-
const failures = []
|
|
1637
|
-
for (const item of results) {
|
|
1638
|
-
if (item.ok) {
|
|
1639
|
-
allOutputs.push(...item.outputs)
|
|
1640
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '', negativePrompt: item.negativePrompt })))
|
|
1641
|
-
if (item.seed != null) seeds.push(item.seed)
|
|
1642
|
-
} else {
|
|
1643
|
-
failures.push(`第 ${item.i + 1} 阶段:${item.message}`)
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
if (!allOutputs.length) {
|
|
1648
|
-
if (cfg.outputLogs) logger.warn(`连续图全部失败(${USERID}),已按阶段退款`)
|
|
1649
|
-
return session.text('.generate-failed', ['全部失败(已按阶段退款)'])
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
if (cfg.outputLogs) logger.success(`${USERID} 连续图生成成功 ${successCount}/${count} 阶段(seed=${seed})`)
|
|
1653
|
-
|
|
1654
|
-
// 发图:合并转发,不引用原指令
|
|
1655
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
1656
|
-
|
|
1657
|
-
const reply = []
|
|
1658
|
-
reply.push(session.text('.series-ok', [count * price, successCount, seed]))
|
|
1659
|
-
if (failures.length) reply.push(session.text('.batch-partial', [successCount, count, failures.length, failures.join(';')]))
|
|
1660
|
-
return reply.filter(Boolean).join('\n')
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
1481
|
// ---------------- 权限 ----------------
|
|
1664
1482
|
function readableOptimizeReason(reason) {
|
|
1665
1483
|
if (!reason) return ''
|
|
@@ -1741,7 +1559,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1741
1559
|
// 注意:更新数据里不能带主键 id,否则数据库驱动会报 cannot modify primary key
|
|
1742
1560
|
function runtimeState() {
|
|
1743
1561
|
return {
|
|
1744
|
-
fixed_characters: cfg.fixedCharacters || [],
|
|
1745
1562
|
artist_presets: cfg.artistPresets || [],
|
|
1746
1563
|
active_artist_preset: cfg.activeArtistPreset || '',
|
|
1747
1564
|
default_artist_tags: cfg.defaultArtistTags || '',
|
|
@@ -1749,20 +1566,25 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1749
1566
|
}
|
|
1750
1567
|
}
|
|
1751
1568
|
|
|
1752
|
-
|
|
1569
|
+
let legacyRuntimeFixedCharacters = []
|
|
1570
|
+
let fixedCharactersMigrated = false
|
|
1571
|
+
|
|
1572
|
+
// 启动时加载 p_draw_config 中仍归属该表的运行时数据。
|
|
1573
|
+
// fixed_characters 只作为旧版本迁移输入,运行时不再覆盖 cfg.fixedCharacters。
|
|
1753
1574
|
async function loadRuntimeState() {
|
|
1754
1575
|
try {
|
|
1755
1576
|
const rows = await ctx.database.get('p_draw_config', { id: 1 })
|
|
1756
1577
|
const row = rows && rows[0]
|
|
1757
1578
|
if (!row) return
|
|
1758
|
-
if (Array.isArray(row.fixed_characters))
|
|
1579
|
+
if (Array.isArray(row.fixed_characters)) legacyRuntimeFixedCharacters = row.fixed_characters
|
|
1580
|
+
fixedCharactersMigrated = row.fixed_characters_migrated === true
|
|
1759
1581
|
if (Array.isArray(row.artist_presets)) cfg.artistPresets = row.artist_presets
|
|
1760
1582
|
if (row.active_artist_preset) cfg.activeArtistPreset = row.active_artist_preset
|
|
1761
1583
|
if (row.default_artist_tags != null) cfg.defaultArtistTags = row.default_artist_tags
|
|
1762
1584
|
if (row.user_models && typeof row.user_models === 'object') cfg.userModels = row.user_models
|
|
1763
|
-
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} 个)`)
|
|
1764
1586
|
} catch (e) {
|
|
1765
|
-
logger.warn(
|
|
1587
|
+
logger.warn(`读取运行时配置失败(画师组或模型偏好可能未持久化):${e.message}`)
|
|
1766
1588
|
}
|
|
1767
1589
|
}
|
|
1768
1590
|
|
|
@@ -1781,6 +1603,29 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1781
1603
|
}
|
|
1782
1604
|
}
|
|
1783
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
|
+
|
|
1784
1629
|
function normalizeTagText(text) {
|
|
1785
1630
|
const tags = []
|
|
1786
1631
|
for (const tag of String(text || '').split(',')) {
|
|
@@ -1813,13 +1658,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1813
1658
|
return await diagnoseText(session)
|
|
1814
1659
|
}
|
|
1815
1660
|
|
|
1816
|
-
//
|
|
1817
|
-
const seriesMatch = text.match(/^连续\s*(.*)$/)
|
|
1818
|
-
if (seriesMatch) {
|
|
1819
|
-
return await handleGenerateSeries(session, seriesMatch[1].trim())
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// 多人指令:p-draw 多人 <描述>
|
|
1661
|
+
// 多人指令:p-draw 多人 <描述>
|
|
1823
1662
|
const multiMatch = text.match(/^(?:多人|多人生图|双人|三人|群像)\s*(.*)$/)
|
|
1824
1663
|
if (multiMatch) {
|
|
1825
1664
|
return await handleGenerateMulti(session, multiMatch[1].trim())
|
|
@@ -1904,12 +1743,27 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1904
1743
|
if (addCharacter) {
|
|
1905
1744
|
const parsed = parseNameTags(addCharacter[1])
|
|
1906
1745
|
if (!parsed) return session.text('.character-format')
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
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 })
|
|
1910
1750
|
if (cfg.outputLogs) logger.success(`${USERID} 添加固定角色 ${parsed.name}`)
|
|
1911
1751
|
return session.text('.character-created', [parsed.name, parsed.tags])
|
|
1912
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
|
+
}
|
|
1913
1767
|
|
|
1914
1768
|
// 模型切换:p-draw 模型 <名称>(查看)/ p-draw 模型 默认(重置)
|
|
1915
1769
|
const modelMatch = text.match(/^(?:切换)?\s*模型\s*(.*)$/)
|
|
@@ -1944,10 +1798,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1944
1798
|
return await handleGenerate(session, text)
|
|
1945
1799
|
})
|
|
1946
1800
|
|
|
1947
|
-
async function handleGenerate(session, rawText) {
|
|
1948
|
-
const USERID = session.userId
|
|
1949
|
-
const isAdmin = isAdminUser(session)
|
|
1950
|
-
const unet = await resolveUnet(USERID)
|
|
1801
|
+
async function handleGenerate(session, rawText) {
|
|
1802
|
+
const USERID = session.userId
|
|
1803
|
+
const isAdmin = isAdminUser(session)
|
|
1804
|
+
const unet = await resolveUnet(USERID)
|
|
1951
1805
|
if (cfg.outputLogs) {
|
|
1952
1806
|
logger.info(`[p-draw] 请求 userId=${USERID} isAdmin=${isAdmin} adminUsers=${JSON.stringify(cfg.adminUsers || [])} normalizeId=${normalizeId(USERID)}`)
|
|
1953
1807
|
}
|
|
@@ -1961,21 +1815,21 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1961
1815
|
const parsedBatch = parseBatchCount(parsedSize.prompt, cfg.batchMax)
|
|
1962
1816
|
// 固定种子解析(--seed:xxx / --seed xxx / --seed=xxx),并从提示词中剥离
|
|
1963
1817
|
const parsedSeed = parseSeed(parsedBatch.prompt)
|
|
1964
|
-
const text = parsedSeed.prompt
|
|
1965
|
-
const seed = parsedSeed.seed
|
|
1966
|
-
const count = parsedBatch.count
|
|
1818
|
+
const text = parsedSeed.prompt
|
|
1819
|
+
const seed = parsedSeed.seed
|
|
1820
|
+
const count = parsedBatch.count
|
|
1967
1821
|
|
|
1968
1822
|
// P 点校验(按总价 = 张数 × 单价)
|
|
1969
1823
|
// P 点校验(按总价 = 张数 × 单价)
|
|
1970
1824
|
const pcheck = await precheckPoints(session, USERID, isAdmin, count * cfg.price)
|
|
1971
1825
|
if (!pcheck.ok) return pcheck.message
|
|
1972
1826
|
|
|
1973
|
-
// 原样模式
|
|
1974
|
-
const stripped = stripRawPrefix(text)
|
|
1975
|
-
const raw = stripped.raw
|
|
1976
|
-
const promptSections = splitPositiveNegativePrompt(stripped.prompt)
|
|
1977
|
-
const userPrompt = promptSections.positive
|
|
1978
|
-
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
|
|
1979
1833
|
if (!userPrompt) return session.text('.no-prompt')
|
|
1980
1834
|
|
|
1981
1835
|
// ComfyUI 就绪
|
|
@@ -1997,13 +1851,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1997
1851
|
const tokenOpt = !globalOpt && !isAdmin && cfg.llmModel && cfg.llmBaseUrl
|
|
1998
1852
|
if (globalOpt) {
|
|
1999
1853
|
// 全局优化开启:一次优化,整批复用同一提示词
|
|
2000
|
-
const optimized = await optimizePrompt(session, userPrompt, false)
|
|
1854
|
+
const optimized = await optimizePrompt(session, userPrompt, false)
|
|
2001
1855
|
finalPrompt = optimized.prompt
|
|
2002
1856
|
degraded = !optimized.ok
|
|
2003
1857
|
optimizedReason = optimized.reason || ''
|
|
2004
1858
|
} else if (adminOpt) {
|
|
2005
1859
|
// 管理员在全局关闭时也免费优化(不耗券)
|
|
2006
|
-
const optimized = await optimizePrompt(session, userPrompt, true)
|
|
1860
|
+
const optimized = await optimizePrompt(session, userPrompt, true)
|
|
2007
1861
|
finalPrompt = optimized.prompt
|
|
2008
1862
|
degraded = !optimized.ok
|
|
2009
1863
|
optimizedReason = optimized.reason || ''
|
|
@@ -2044,7 +1898,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2044
1898
|
}
|
|
2045
1899
|
// 非按张优化模式:直接拼好整批复用的提示词
|
|
2046
1900
|
if (!perImageOptimize) {
|
|
2047
|
-
const composed = composePrompt(finalPrompt, raw)
|
|
1901
|
+
const composed = await composePrompt(finalPrompt, raw)
|
|
2048
1902
|
finalPrompt = appendInlineProtectedTags(composed.prompt, userPrompt, raw)
|
|
2049
1903
|
degraded = degraded || composed.degraded
|
|
2050
1904
|
}
|
|
@@ -2055,11 +1909,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2055
1909
|
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 已扣除 ${count * cfg.price} P 点(${count} 张 × ${cfg.price}),余额 ${saving - count * cfg.price}`)
|
|
2056
1910
|
}
|
|
2057
1911
|
|
|
2058
|
-
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
2059
|
-
const generationOverrides = Object.assign(
|
|
2060
|
-
{ unet, seed },
|
|
2061
|
-
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
2062
|
-
)
|
|
1912
|
+
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
1913
|
+
const generationOverrides = Object.assign(
|
|
1914
|
+
{ unet, seed },
|
|
1915
|
+
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
1916
|
+
)
|
|
2063
1917
|
|
|
2064
1918
|
// 性能:按张优化(perImageOptimize)时联网搜索只做一次,各图复用同一份结果
|
|
2065
1919
|
const searchCache = perImageOptimize && wantsWebSearch(userPrompt) ? await webSearch(userPrompt) : null
|
|
@@ -2068,11 +1922,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2068
1922
|
const queued = await enqueueBatch(count, async (i) => {
|
|
2069
1923
|
let p = finalPrompt
|
|
2070
1924
|
if (perImageOptimize) {
|
|
2071
|
-
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
2072
|
-
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)
|
|
2073
1927
|
}
|
|
2074
|
-
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
2075
|
-
return { ...generated, prompt: p }
|
|
1928
|
+
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
1929
|
+
return { ...generated, prompt: p }
|
|
2076
1930
|
}, { USERID, isAdmin, totalPrice: count * cfg.price })
|
|
2077
1931
|
if (!queued.ok) return queued.message
|
|
2078
1932
|
const queuedTasks = queued.tasks
|
|
@@ -2095,45 +1949,52 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2095
1949
|
notice.push(session.text('.no-optimize', [reasons[noOptimizeReason] || noOptimizeReason]))
|
|
2096
1950
|
}
|
|
2097
1951
|
if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
|
|
2098
|
-
const feedback = feedbackBase(session, { firstPosition, count
|
|
2099
|
-
notice.push(...feedback.notices)
|
|
2100
|
-
await sendNotices(session, notice)
|
|
2101
|
-
await sendNotices(session, [feedback.chargeNotice], { quote: true })
|
|
1952
|
+
const feedback = feedbackBase(session, { firstPosition, count })
|
|
1953
|
+
notice.push(...feedback.notices)
|
|
1954
|
+
await sendNotices(session, notice)
|
|
2102
1955
|
|
|
2103
1956
|
// 单张生成
|
|
2104
1957
|
const runOne = async (i) => {
|
|
2105
1958
|
let p = finalPrompt
|
|
2106
1959
|
if (perImageOptimize) {
|
|
2107
|
-
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
2108
|
-
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)
|
|
2109
1962
|
}
|
|
2110
1963
|
let result
|
|
2111
1964
|
if (cfg.queueEnabled) {
|
|
2112
1965
|
try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
2113
|
-
} else {
|
|
2114
|
-
try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
2115
|
-
}
|
|
2116
|
-
if (!result.prompt) result.prompt = p
|
|
2117
|
-
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
|
|
2118
1971
|
}
|
|
2119
1972
|
|
|
2120
1973
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, cfg.price, runOne)
|
|
2121
1974
|
|
|
2122
|
-
// 汇总
|
|
2123
|
-
const allOutputs = []
|
|
2124
|
-
const forwardOutputs = []
|
|
2125
|
-
const seeds = []
|
|
2126
|
-
const failures = []
|
|
2127
|
-
for (const item of results) {
|
|
2128
|
-
if (item.ok) {
|
|
2129
|
-
allOutputs.push(...item.outputs)
|
|
2130
|
-
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
2131
|
-
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)
|
|
2132
1985
|
} else {
|
|
2133
1986
|
failures.push(`第 ${item.i + 1} 张:${item.message}`)
|
|
2134
1987
|
}
|
|
2135
1988
|
}
|
|
2136
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
|
+
|
|
2137
1998
|
if (!allOutputs.length) {
|
|
2138
1999
|
if (cfg.outputLogs) logger.warn(`生成全部失败(${USERID}),已按张退款`)
|
|
2139
2000
|
return session.text('.generate-failed', ['全部失败(已按张退款)'])
|
|
@@ -2141,8 +2002,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2141
2002
|
|
|
2142
2003
|
if (cfg.outputLogs) logger.success(`${USERID} 生成成功 ${successCount}/${count} 张`)
|
|
2143
2004
|
|
|
2144
|
-
// 发图:合并转发,不引用原指令
|
|
2145
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
2005
|
+
// 发图:合并转发,不引用原指令
|
|
2006
|
+
await sendImagesAsForward(session, forwardOutputs)
|
|
2146
2007
|
|
|
2147
2008
|
const reply = []
|
|
2148
2009
|
if (count > 1) {
|
|
@@ -2281,8 +2142,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2281
2142
|
return price
|
|
2282
2143
|
}
|
|
2283
2144
|
|
|
2284
|
-
//
|
|
2145
|
+
// 启动时加载配置表,并将旧 fixedCharacters 数据迁入专用表。
|
|
2285
2146
|
await loadRuntimeState()
|
|
2147
|
+
await migrateFixedCharacters()
|
|
2286
2148
|
|
|
2287
2149
|
ctx.on('dispose', () => {
|
|
2288
2150
|
// 清理临时文件
|
|
@@ -2295,5 +2157,5 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2295
2157
|
})
|
|
2296
2158
|
|
|
2297
2159
|
// 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
|
|
2298
|
-
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, sendNotices, sendImagesAsForward, executeBatch }
|
|
2299
|
-
}
|
|
2160
|
+
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, buildChargeNotice, sendNotices, sendImagesAsForward, executeBatch }
|
|
2161
|
+
}
|