koishi-plugin-p-draw 1.4.0 → 1.5.1
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 +291 -437
- package/lib/i18n.js +124 -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,57 +449,66 @@ 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
|
-
async function sendNotices(session, notices, opts = {}) {
|
|
458
|
-
const content = notices.filter(Boolean).join('\n')
|
|
459
|
-
if (!content) return
|
|
460
|
-
try {
|
|
461
|
-
const message = opts.quote && session.messageId ? h.quote(session.messageId) + content : content
|
|
462
|
-
await session.send(message)
|
|
463
|
-
} catch (e) {
|
|
464
|
-
logger.warn(`发送反馈消息失败:${e.message}`)
|
|
465
|
-
}
|
|
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(',')}`
|
|
466
468
|
}
|
|
467
469
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const src = typeof output === 'string' ? output : output.src
|
|
474
|
-
const prompt = typeof output === 'string' ? '' : String(output.prompt || '')
|
|
475
|
-
const negativePrompt = typeof output === 'string' ? '' : String(output.negativePrompt || '')
|
|
476
|
-
const materialized = await materializeImageSource(src)
|
|
477
|
-
const image = Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
|
|
478
|
-
nodes.push(h('message', image))
|
|
479
|
-
fallback.push(image)
|
|
480
|
-
const promptText = `Positive:\n${prompt}\n\nNegative:\n${negativePrompt}`
|
|
481
|
-
nodes.push(h('message', promptText))
|
|
482
|
-
fallback.push(promptText)
|
|
483
|
-
}
|
|
484
|
-
try {
|
|
485
|
-
await session.send(h('figure', nodes))
|
|
486
|
-
} catch (e) {
|
|
487
|
-
logger.warn(`发送转发图片失败:${e.message}`)
|
|
488
|
-
await session.send(fallback)
|
|
489
|
-
}
|
|
470
|
+
function buildGenerationReply(session, { successCount, count, failures, notes = [] }) {
|
|
471
|
+
const reply = []
|
|
472
|
+
if (notes.length) reply.push(notes.join('\n'))
|
|
473
|
+
if (failures.length) reply.push(session.text('.batch-partial', [successCount, count, failures.length, failures.join(';')]))
|
|
474
|
+
return reply.filter(Boolean).join('\n')
|
|
490
475
|
}
|
|
491
476
|
|
|
477
|
+
async function sendNotices(session, notices, opts = {}) {
|
|
478
|
+
const content = notices.filter(Boolean).join('\n')
|
|
479
|
+
if (!content) return
|
|
480
|
+
try {
|
|
481
|
+
const message = opts.quote && session.messageId ? h.quote(session.messageId) + content : content
|
|
482
|
+
await session.send(message)
|
|
483
|
+
} catch (e) {
|
|
484
|
+
logger.warn(`发送反馈消息失败:${e.message}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 生成图片使用合并转发;每张图后紧跟实际使用的正负面提示词,不附原消息引用。
|
|
489
|
+
async function sendImagesAsForward(session, outputs) {
|
|
490
|
+
const nodes = []
|
|
491
|
+
const fallback = []
|
|
492
|
+
for (const output of outputs) {
|
|
493
|
+
const src = typeof output === 'string' ? output : output.src
|
|
494
|
+
const prompt = typeof output === 'string' ? '' : String(output.prompt || '')
|
|
495
|
+
const negativePrompt = typeof output === 'string' ? '' : String(output.negativePrompt || '')
|
|
496
|
+
const materialized = await materializeImageSource(src)
|
|
497
|
+
const image = Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
|
|
498
|
+
nodes.push(h('message', image))
|
|
499
|
+
fallback.push(image)
|
|
500
|
+
const promptText = `Positive:\n${prompt}\n\nNegative:\n${negativePrompt}`
|
|
501
|
+
nodes.push(h('message', promptText))
|
|
502
|
+
fallback.push(promptText)
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
await session.send(h('figure', nodes))
|
|
506
|
+
} catch (e) {
|
|
507
|
+
logger.warn(`发送转发图片失败:${e.message}`)
|
|
508
|
+
await session.send(fallback)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
492
512
|
// P 点读改写按用户串行化,避免并发指令互相覆盖余额
|
|
493
513
|
const userLocks = new Map()
|
|
494
514
|
function withUserLock(USERID, fn) {
|
|
@@ -499,17 +519,22 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
499
519
|
}
|
|
500
520
|
|
|
501
521
|
// ---------------- 用户自选模型 ----------------
|
|
502
|
-
// 从 ComfyUI /object_info(10
|
|
522
|
+
// 从 ComfyUI /object_info(10 分钟缓存)读取 UNET 和 checkpoint 模型列表
|
|
503
523
|
async function listUnetModels() {
|
|
504
524
|
try {
|
|
505
525
|
const objectInfo = await getObjectInfoCached()
|
|
506
|
-
const list =
|
|
507
|
-
|
|
526
|
+
const list = [
|
|
527
|
+
...availableModels(objectInfo, 'UNETLoader', 'unet_name'),
|
|
528
|
+
...availableModels(objectInfo, 'CheckpointLoaderSimple', 'ckpt_name')
|
|
529
|
+
.filter(name => String(name).trim().toLowerCase().replace(/[-_]/g, '~') === 'animagine~xl~3.1.safetensors'),
|
|
530
|
+
]
|
|
531
|
+
const unique = [...new Set(list)]
|
|
532
|
+
if (unique.length) return unique
|
|
508
533
|
} catch (e) { /* ignore */ }
|
|
509
534
|
return []
|
|
510
535
|
}
|
|
511
536
|
|
|
512
|
-
//
|
|
537
|
+
// 解析该用户当前生效的模型:有偏好且仍存在于 ComfyUI 时用偏好,否则回落默认
|
|
513
538
|
async function resolveUnet(USERID) {
|
|
514
539
|
const chosen = cfg.userModels && cfg.userModels[USERID]
|
|
515
540
|
if (!chosen || !String(chosen).trim()) return cfg.unetName
|
|
@@ -559,12 +584,12 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
559
584
|
|
|
560
585
|
async function runComfyGenerate(prompt, size, overrides) {
|
|
561
586
|
const sizes = parseAllowedSizes()
|
|
562
|
-
const requestedWidth = (size && size[0]) || overrides.width || cfg.width
|
|
563
|
-
const requestedHeight = (size && size[1]) || overrides.height || cfg.height
|
|
564
587
|
const unetName = overrides.unet || cfg.unetName
|
|
565
588
|
// 应用该模型的独立参数覆盖(modelParams),再叠加命令级 overrides(overrides.steps/cfg 优先于模型级)
|
|
566
589
|
const modelSpecific = resolveModelParams(unetName)
|
|
567
590
|
const workCfg = Object.assign({}, cfg, modelSpecific, { unetName })
|
|
591
|
+
const requestedWidth = (size && size[0]) || overrides.width || workCfg.width
|
|
592
|
+
const requestedHeight = (size && size[1]) || overrides.height || workCfg.height
|
|
568
593
|
// 防御:sampler/scheduler 配置若带尾随空格会导致 ComfyUI 报 "Value not in list",统一 trim
|
|
569
594
|
if (typeof workCfg.samplerName === 'string') workCfg.samplerName = workCfg.samplerName.trim()
|
|
570
595
|
if (typeof workCfg.scheduler === 'string') workCfg.scheduler = workCfg.scheduler.trim()
|
|
@@ -574,7 +599,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
574
599
|
const cfgVal = Number(overrides.cfg) || workCfg.cfg
|
|
575
600
|
const seed = Number(overrides.seed) || crypto.randomInt(1, 2 ** 32 - 1)
|
|
576
601
|
const negativePrompt = joinPromptParts([overrides.negativePrompt || cfg.negativePrompt || ''])
|
|
577
|
-
const promptBody = buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
602
|
+
const promptBody = buildWorkflow(workCfg, prompt, negativePrompt, width, height, steps, cfgVal, seed, Boolean(size))
|
|
578
603
|
|
|
579
604
|
const clientId = crypto.randomUUID()
|
|
580
605
|
const submit = await comfyPost('/prompt', { prompt: promptBody, client_id: clientId }, 20000)
|
|
@@ -634,10 +659,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
634
659
|
width,
|
|
635
660
|
height,
|
|
636
661
|
steps,
|
|
637
|
-
cfg: cfgVal,
|
|
638
|
-
prompt_id: promptId,
|
|
639
|
-
negativePrompt,
|
|
640
|
-
}
|
|
662
|
+
cfg: cfgVal,
|
|
663
|
+
prompt_id: promptId,
|
|
664
|
+
negativePrompt,
|
|
665
|
+
}
|
|
641
666
|
}
|
|
642
667
|
|
|
643
668
|
// ---------------- LLM 提示词优化 ----------------
|
|
@@ -679,9 +704,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
679
704
|
}
|
|
680
705
|
|
|
681
706
|
// 把命中的固定角色 tags 渲染进优化模板的 {character_rule} 占位符。
|
|
682
|
-
function
|
|
707
|
+
async function fixedCharacterRows(query = {}) {
|
|
708
|
+
const rows = await ctx.database.get('p_draw_fixed_characters', query)
|
|
709
|
+
return rows.sort((a, b) => Number(a.id) - Number(b.id))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function buildCharacterRule(prompt) {
|
|
683
713
|
const text = String(prompt || '')
|
|
684
|
-
for (const
|
|
714
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
685
715
|
if (name && text.includes(name)) {
|
|
686
716
|
return `用户提到了固定角色「${name}」,其外观 tags 为:${tags} 请优先保留这些特征。`
|
|
687
717
|
}
|
|
@@ -741,7 +771,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
741
771
|
}
|
|
742
772
|
}
|
|
743
773
|
|
|
744
|
-
async function optimizePrompt(session, userPrompt, force = false, precomputedSearch = null) {
|
|
774
|
+
async function optimizePrompt(session, userPrompt, force = false, precomputedSearch = null) {
|
|
745
775
|
if (!cfg.promptOptimizeEnabled && !force) {
|
|
746
776
|
return { ok: true, prompt: userPrompt, reason: 'optimize_disabled' }
|
|
747
777
|
}
|
|
@@ -756,8 +786,8 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
756
786
|
} else if (wantsWebSearch(userPrompt)) {
|
|
757
787
|
searchBlock = await webSearch(userPrompt)
|
|
758
788
|
}
|
|
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}`
|
|
789
|
+
const characterRule = await 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{search_block}\n\n用户原始要求:\n{theme}`
|
|
761
791
|
const template = (cfg.promptOptimizeTemplate || '').trim() || defaultTemplate
|
|
762
792
|
const searchBlockText = searchBlock
|
|
763
793
|
? `联网搜索参考信息(请尽量依据这些内容补全角色外观与设定):\n${searchBlock}`
|
|
@@ -815,54 +845,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
815
845
|
}
|
|
816
846
|
}
|
|
817
847
|
|
|
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
848
|
// ---------------- 提示词组装 ----------------
|
|
864
849
|
// fixedOverride:undefined=按名称自动匹配固定角色;'skip'=不注入固定角色;字符串=直接使用该字符串作为固定角色 tags
|
|
865
|
-
function composePrompt(userPrompt, raw, fixedOverride) {
|
|
850
|
+
async function composePrompt(userPrompt, raw, fixedOverride) {
|
|
866
851
|
if (raw) return { prompt: userPrompt, degraded: false }
|
|
867
852
|
const parts = []
|
|
868
853
|
if (cfg.qualityPrefix) parts.push(String(cfg.qualityPrefix).trim())
|
|
@@ -871,7 +856,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
871
856
|
} else if (typeof fixedOverride === 'string') {
|
|
872
857
|
if (String(fixedOverride).trim()) parts.push(String(fixedOverride).trim())
|
|
873
858
|
} else {
|
|
874
|
-
for (const
|
|
859
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
875
860
|
if (name && userPrompt.includes(name)) {
|
|
876
861
|
parts.push(tags)
|
|
877
862
|
break
|
|
@@ -931,7 +916,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
931
916
|
// 多人规划:让 LLM 输出结构化场景 JSON(2-4 人)。
|
|
932
917
|
async function generateMultiPersonPlan(prompt) {
|
|
933
918
|
const mentioned = {}
|
|
934
|
-
for (const
|
|
919
|
+
for (const { name, tags } of await fixedCharacterRows()) {
|
|
935
920
|
if (name && prompt.includes(name)) mentioned[name] = tags
|
|
936
921
|
}
|
|
937
922
|
const planPrompt = buildMultiPersonPlanPrompt(prompt, mentioned)
|
|
@@ -977,7 +962,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
977
962
|
}
|
|
978
963
|
|
|
979
964
|
// 多人最终提示词组装:count/common tags + 角色块 + 互动 + 构图。
|
|
980
|
-
function buildMultiPersonFinalPrompt(plan, prompt) {
|
|
965
|
+
async function buildMultiPersonFinalPrompt(plan, prompt) {
|
|
981
966
|
const aliases = ['Character A', 'Character B', 'Character C', 'Character D']
|
|
982
967
|
const characterCount = plan.characters.length
|
|
983
968
|
const characterRoles = []
|
|
@@ -993,7 +978,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
993
978
|
|
|
994
979
|
const usedFixedNames = new Set()
|
|
995
980
|
const fixedGenders = []
|
|
996
|
-
const configuredChars =
|
|
981
|
+
const configuredChars = Object.fromEntries((await fixedCharacterRows()).map(({ name, tags }) => [name, tags]))
|
|
997
982
|
for (let index = 0; index < plan.characters.length; index++) {
|
|
998
983
|
const character = plan.characters[index]
|
|
999
984
|
let fixedName = ''
|
|
@@ -1162,11 +1147,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1162
1147
|
|
|
1163
1148
|
// 视觉校验(anima_verify + generation_verifier 移植):对生成的图片跑视觉 LLM,
|
|
1164
1149
|
// 不合格则用相同提示词重试(最多 multiCandidateCount 张),按多候选规则挑选并返回结果。
|
|
1165
|
-
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1150
|
+
async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
|
|
1166
1151
|
const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
|
|
1167
1152
|
const verifyModel = String(cfg.verifyLlmModel || '').trim()
|
|
1168
1153
|
if (!verifyBaseUrl || !verifyModel) {
|
|
1169
|
-
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt, negativePrompt }
|
|
1154
|
+
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt, negativePrompt }
|
|
1170
1155
|
}
|
|
1171
1156
|
const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
|
|
1172
1157
|
const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
|
|
@@ -1174,10 +1159,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1174
1159
|
const systemPrompt = buildVerifySystemPrompt(true, planCount)
|
|
1175
1160
|
const candidates = []
|
|
1176
1161
|
let lastVerdict = null
|
|
1177
|
-
let retries = 0
|
|
1178
|
-
let currentImages = images
|
|
1179
|
-
let currentPrompt = prompt
|
|
1180
|
-
let currentNegativePrompt = negativePrompt
|
|
1162
|
+
let retries = 0
|
|
1163
|
+
let currentImages = images
|
|
1164
|
+
let currentPrompt = prompt
|
|
1165
|
+
let currentNegativePrompt = negativePrompt
|
|
1181
1166
|
|
|
1182
1167
|
async function verifyOnce(imgs, userReq) {
|
|
1183
1168
|
const controller = new AbortController()
|
|
@@ -1275,15 +1260,15 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1275
1260
|
data = extractVerifyJson(reply)
|
|
1276
1261
|
} catch (e) {
|
|
1277
1262
|
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 }
|
|
1263
|
+
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
1264
|
}
|
|
1280
1265
|
if (!data) {
|
|
1281
1266
|
logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
|
|
1282
|
-
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1267
|
+
return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
|
|
1283
1268
|
}
|
|
1284
1269
|
const verdict = verdictFromData(data)
|
|
1285
1270
|
verdict.skipped = false
|
|
1286
|
-
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1271
|
+
candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
|
|
1287
1272
|
selectedOutputs = currentImages
|
|
1288
1273
|
selectedVerdict = verdict
|
|
1289
1274
|
|
|
@@ -1297,13 +1282,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1297
1282
|
if (hint) {
|
|
1298
1283
|
currentPrompt = `${userRequest}\n【上次问题,请修正】${hint}`
|
|
1299
1284
|
}
|
|
1300
|
-
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1285
|
+
const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
|
|
1301
1286
|
if (!regen.ok) {
|
|
1302
1287
|
logger.warn(`多人校验重试生成失败:${regen.message}`)
|
|
1303
1288
|
break
|
|
1304
1289
|
}
|
|
1305
|
-
currentImages = regen.outputs
|
|
1306
|
-
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1290
|
+
currentImages = regen.outputs
|
|
1291
|
+
currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
|
|
1307
1292
|
}
|
|
1308
1293
|
|
|
1309
1294
|
// 多候选挑选
|
|
@@ -1314,7 +1299,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1314
1299
|
selectedOutputs = best.outputs
|
|
1315
1300
|
selectedVerdict = best.verdict
|
|
1316
1301
|
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 }
|
|
1302
|
+
return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1318
1303
|
}
|
|
1319
1304
|
const noteParts = []
|
|
1320
1305
|
if (multiAccepted) {
|
|
@@ -1323,7 +1308,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1323
1308
|
noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
|
|
1324
1309
|
}
|
|
1325
1310
|
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 }
|
|
1311
|
+
return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt, negativePrompt: best.negativePrompt }
|
|
1327
1312
|
}
|
|
1328
1313
|
|
|
1329
1314
|
function buildVerifySystemPrompt(multiPerson, planCount) {
|
|
@@ -1336,7 +1321,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1336
1321
|
}
|
|
1337
1322
|
|
|
1338
1323
|
// 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
|
|
1339
|
-
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1324
|
+
// runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
|
|
1340
1325
|
async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
|
|
1341
1326
|
const results = []
|
|
1342
1327
|
let successCount = 0
|
|
@@ -1350,7 +1335,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1350
1335
|
const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
|
|
1351
1336
|
if (item.ok && outputs.length) {
|
|
1352
1337
|
successCount += 1
|
|
1353
|
-
results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', negativePrompt: item.negativePrompt || '', note: item.note || '' })
|
|
1338
|
+
results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', negativePrompt: item.negativePrompt || '', note: item.note || '' })
|
|
1354
1339
|
} else {
|
|
1355
1340
|
if (!isAdmin) await refundP(USERID, unitPrice)
|
|
1356
1341
|
if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
|
|
@@ -1407,7 +1392,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1407
1392
|
const plan = planResult.plan
|
|
1408
1393
|
|
|
1409
1394
|
// 组装最终提示词
|
|
1410
|
-
const built = buildMultiPersonFinalPrompt(plan, text)
|
|
1395
|
+
const built = await buildMultiPersonFinalPrompt(plan, text)
|
|
1411
1396
|
if (!built.ok) {
|
|
1412
1397
|
return session.text('.multi-usage') + '\n(多人规划失败:' + built.error + ')'
|
|
1413
1398
|
}
|
|
@@ -1431,10 +1416,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1431
1416
|
// 即时反馈
|
|
1432
1417
|
const notice = []
|
|
1433
1418
|
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 })
|
|
1419
|
+
const feedback = feedbackBase(session, { firstPosition, count })
|
|
1420
|
+
notice.push(...feedback.notices)
|
|
1421
|
+
await sendNotices(session, notice)
|
|
1438
1422
|
|
|
1439
1423
|
// 单张生成 +(可选)视觉校验
|
|
1440
1424
|
const runOne = async (i) => {
|
|
@@ -1444,27 +1428,27 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1444
1428
|
} else {
|
|
1445
1429
|
try { result = await runComfyGenerate(finalPrompt, size, { negativePrompt: multiNegative, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1446
1430
|
}
|
|
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 || '' }
|
|
1431
|
+
if (!result.ok || !result.outputs || !result.outputs.length) return result
|
|
1432
|
+
if (!cfg.multiVerifyEnabled) {
|
|
1433
|
+
return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, negativePrompt: result.negativePrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
|
|
1434
|
+
}
|
|
1435
|
+
const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet, result.negativePrompt || multiNegative)
|
|
1436
|
+
if (!verified.ok) return { ok: false, message: verified.message }
|
|
1437
|
+
return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, negativePrompt: verified.negativePrompt || result.negativePrompt || multiNegative, note: verified.message || '' }
|
|
1454
1438
|
}
|
|
1455
1439
|
|
|
1456
1440
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
|
|
1457
1441
|
|
|
1458
|
-
// 汇总
|
|
1459
|
-
const allOutputs = []
|
|
1460
|
-
const forwardOutputs = []
|
|
1461
|
-
const notes = []
|
|
1442
|
+
// 汇总
|
|
1443
|
+
const allOutputs = []
|
|
1444
|
+
const forwardOutputs = []
|
|
1445
|
+
const notes = []
|
|
1462
1446
|
const seeds = []
|
|
1463
1447
|
const failures = []
|
|
1464
1448
|
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 })))
|
|
1449
|
+
if (item.ok) {
|
|
1450
|
+
allOutputs.push(...item.outputs)
|
|
1451
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
1468
1452
|
if (item.seed != null) seeds.push(item.seed)
|
|
1469
1453
|
if (item.note) notes.push(item.note)
|
|
1470
1454
|
} else {
|
|
@@ -1472,6 +1456,14 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1472
1456
|
}
|
|
1473
1457
|
}
|
|
1474
1458
|
|
|
1459
|
+
const chargeNotice = buildChargeNotice({
|
|
1460
|
+
isAdmin,
|
|
1461
|
+
totalPrice: successCount * price,
|
|
1462
|
+
unetName: unet,
|
|
1463
|
+
seeds,
|
|
1464
|
+
})
|
|
1465
|
+
await sendNotices(session, [chargeNotice], { quote: true })
|
|
1466
|
+
|
|
1475
1467
|
if (!allOutputs.length) {
|
|
1476
1468
|
if (cfg.outputLogs) logger.warn(`多人生成全部失败(${USERID}),已按张退款`)
|
|
1477
1469
|
return session.text('.generate-failed', ['全部失败(已按张退款)'])
|
|
@@ -1479,185 +1471,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1479
1471
|
|
|
1480
1472
|
if (cfg.outputLogs) logger.success(`${USERID} 多人生成成功 ${successCount}/${count} 张`)
|
|
1481
1473
|
|
|
1482
|
-
// 发图:合并转发,不引用原指令
|
|
1483
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
1484
|
-
|
|
1485
|
-
const reply = []
|
|
1486
|
-
if (count > 1) {
|
|
1487
|
-
reply.push(session.text('.generate-ok-batch', [count * price, successCount, seeds.join(', ') || '-']))
|
|
1488
|
-
} else {
|
|
1489
|
-
reply.push(session.text('.generate-ok', [price, seeds[0] || '-']))
|
|
1490
|
-
}
|
|
1491
|
-
if (notes.length) reply.push(notes.join('\n'))
|
|
1492
|
-
if (failures.length) reply.push(session.text('.batch-partial', [successCount, count, failures.length, failures.join(';')]))
|
|
1493
|
-
return reply.filter(Boolean).join('\n')
|
|
1494
|
-
}
|
|
1474
|
+
// 发图:合并转发,不引用原指令
|
|
1475
|
+
await sendImagesAsForward(session, forwardOutputs)
|
|
1495
1476
|
|
|
1496
|
-
|
|
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')
|
|
1477
|
+
return buildGenerationReply(session, { successCount, count, failures, notes })
|
|
1661
1478
|
}
|
|
1662
1479
|
|
|
1663
1480
|
// ---------------- 权限 ----------------
|
|
@@ -1741,7 +1558,6 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1741
1558
|
// 注意:更新数据里不能带主键 id,否则数据库驱动会报 cannot modify primary key
|
|
1742
1559
|
function runtimeState() {
|
|
1743
1560
|
return {
|
|
1744
|
-
fixed_characters: cfg.fixedCharacters || [],
|
|
1745
1561
|
artist_presets: cfg.artistPresets || [],
|
|
1746
1562
|
active_artist_preset: cfg.activeArtistPreset || '',
|
|
1747
1563
|
default_artist_tags: cfg.defaultArtistTags || '',
|
|
@@ -1749,20 +1565,25 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1749
1565
|
}
|
|
1750
1566
|
}
|
|
1751
1567
|
|
|
1752
|
-
|
|
1568
|
+
let legacyRuntimeFixedCharacters = []
|
|
1569
|
+
let fixedCharactersMigrated = false
|
|
1570
|
+
|
|
1571
|
+
// 启动时加载 p_draw_config 中仍归属该表的运行时数据。
|
|
1572
|
+
// fixed_characters 只作为旧版本迁移输入,运行时不再覆盖 cfg.fixedCharacters。
|
|
1753
1573
|
async function loadRuntimeState() {
|
|
1754
1574
|
try {
|
|
1755
1575
|
const rows = await ctx.database.get('p_draw_config', { id: 1 })
|
|
1756
1576
|
const row = rows && rows[0]
|
|
1757
1577
|
if (!row) return
|
|
1758
|
-
if (Array.isArray(row.fixed_characters))
|
|
1578
|
+
if (Array.isArray(row.fixed_characters)) legacyRuntimeFixedCharacters = row.fixed_characters
|
|
1579
|
+
fixedCharactersMigrated = row.fixed_characters_migrated === true
|
|
1759
1580
|
if (Array.isArray(row.artist_presets)) cfg.artistPresets = row.artist_presets
|
|
1760
1581
|
if (row.active_artist_preset) cfg.activeArtistPreset = row.active_artist_preset
|
|
1761
1582
|
if (row.default_artist_tags != null) cfg.defaultArtistTags = row.default_artist_tags
|
|
1762
1583
|
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}
|
|
1584
|
+
if (cfg.outputLogs) logger.info(`[p-draw] 已加载运行时配置(画师组 ${(cfg.artistPresets || []).length} 个,模型偏好 ${Object.keys(cfg.userModels || {}).length} 个)`)
|
|
1764
1585
|
} catch (e) {
|
|
1765
|
-
logger.warn(
|
|
1586
|
+
logger.warn(`读取运行时配置失败(画师组或模型偏好可能未持久化):${e.message}`)
|
|
1766
1587
|
}
|
|
1767
1588
|
}
|
|
1768
1589
|
|
|
@@ -1781,6 +1602,29 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1781
1602
|
}
|
|
1782
1603
|
}
|
|
1783
1604
|
|
|
1605
|
+
async function migrateFixedCharacters() {
|
|
1606
|
+
if (fixedCharactersMigrated) return
|
|
1607
|
+
const existingRows = await fixedCharacterRows()
|
|
1608
|
+
const existingNames = new Set(existingRows.map(row => row.name))
|
|
1609
|
+
const legacyEntries = [...(cfg.fixedCharacters || []), ...legacyRuntimeFixedCharacters]
|
|
1610
|
+
for (const entry of legacyEntries) {
|
|
1611
|
+
const parsed = parseNameTags(entry)
|
|
1612
|
+
if (!parsed || existingNames.has(parsed.name)) continue
|
|
1613
|
+
await ctx.database.create('p_draw_fixed_characters', {
|
|
1614
|
+
name: parsed.name,
|
|
1615
|
+
tags: parsed.tags,
|
|
1616
|
+
})
|
|
1617
|
+
existingNames.add(parsed.name)
|
|
1618
|
+
}
|
|
1619
|
+
const existingConfig = await ctx.database.get('p_draw_config', { id: 1 })
|
|
1620
|
+
if (existingConfig && existingConfig[0]) {
|
|
1621
|
+
await ctx.database.set('p_draw_config', { id: 1 }, { fixed_characters_migrated: true })
|
|
1622
|
+
} else {
|
|
1623
|
+
await ctx.database.create('p_draw_config', { id: 1, ...runtimeState(), fixed_characters_migrated: true })
|
|
1624
|
+
}
|
|
1625
|
+
fixedCharactersMigrated = true
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1784
1628
|
function normalizeTagText(text) {
|
|
1785
1629
|
const tags = []
|
|
1786
1630
|
for (const tag of String(text || '').split(',')) {
|
|
@@ -1813,13 +1657,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1813
1657
|
return await diagnoseText(session)
|
|
1814
1658
|
}
|
|
1815
1659
|
|
|
1816
|
-
//
|
|
1817
|
-
const seriesMatch = text.match(/^连续\s*(.*)$/)
|
|
1818
|
-
if (seriesMatch) {
|
|
1819
|
-
return await handleGenerateSeries(session, seriesMatch[1].trim())
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// 多人指令:p-draw 多人 <描述>
|
|
1660
|
+
// 多人指令:p-draw 多人 <描述>
|
|
1823
1661
|
const multiMatch = text.match(/^(?:多人|多人生图|双人|三人|群像)\s*(.*)$/)
|
|
1824
1662
|
if (multiMatch) {
|
|
1825
1663
|
return await handleGenerateMulti(session, multiMatch[1].trim())
|
|
@@ -1904,12 +1742,27 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1904
1742
|
if (addCharacter) {
|
|
1905
1743
|
const parsed = parseNameTags(addCharacter[1])
|
|
1906
1744
|
if (!parsed) return session.text('.character-format')
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
await
|
|
1745
|
+
const tags = normalizeTagText(parsed.tags)
|
|
1746
|
+
const existing = (await fixedCharacterRows({ name: parsed.name }))[0]
|
|
1747
|
+
if (existing) await ctx.database.set('p_draw_fixed_characters', { id: existing.id }, { name: parsed.name, tags })
|
|
1748
|
+
else await ctx.database.create('p_draw_fixed_characters', { name: parsed.name, tags })
|
|
1910
1749
|
if (cfg.outputLogs) logger.success(`${USERID} 添加固定角色 ${parsed.name}`)
|
|
1911
1750
|
return session.text('.character-created', [parsed.name, parsed.tags])
|
|
1912
1751
|
}
|
|
1752
|
+
if (text.match(/^(?:查看|列出|显示)\s*(?:固定)?\s*角色$|^(?:固定)?\s*角色列表$/)) {
|
|
1753
|
+
const rows = await fixedCharacterRows()
|
|
1754
|
+
if (!rows.length) return '固定角色:无'
|
|
1755
|
+
return ['固定角色:', ...rows.map(row => `- ${row.name}:${row.tags}`)].join('\n')
|
|
1756
|
+
}
|
|
1757
|
+
const deleteCharacter = text.match(/^(?:删除|移除)\s*(?:固定)?\s*角色\s*(.*)$/)
|
|
1758
|
+
if (deleteCharacter) {
|
|
1759
|
+
const name = String(deleteCharacter[1]).trim()
|
|
1760
|
+
if (!name) return session.text('.character-delete-format')
|
|
1761
|
+
const existing = (await fixedCharacterRows({ name }))[0]
|
|
1762
|
+
if (!existing) return session.text('.character-not-found', [name])
|
|
1763
|
+
await ctx.database.remove('p_draw_fixed_characters', { id: existing.id })
|
|
1764
|
+
return session.text('.character-deleted', [name])
|
|
1765
|
+
}
|
|
1913
1766
|
|
|
1914
1767
|
// 模型切换:p-draw 模型 <名称>(查看)/ p-draw 模型 默认(重置)
|
|
1915
1768
|
const modelMatch = text.match(/^(?:切换)?\s*模型\s*(.*)$/)
|
|
@@ -1944,10 +1797,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1944
1797
|
return await handleGenerate(session, text)
|
|
1945
1798
|
})
|
|
1946
1799
|
|
|
1947
|
-
async function handleGenerate(session, rawText) {
|
|
1948
|
-
const USERID = session.userId
|
|
1949
|
-
const isAdmin = isAdminUser(session)
|
|
1950
|
-
const unet = await resolveUnet(USERID)
|
|
1800
|
+
async function handleGenerate(session, rawText) {
|
|
1801
|
+
const USERID = session.userId
|
|
1802
|
+
const isAdmin = isAdminUser(session)
|
|
1803
|
+
const unet = await resolveUnet(USERID)
|
|
1951
1804
|
if (cfg.outputLogs) {
|
|
1952
1805
|
logger.info(`[p-draw] 请求 userId=${USERID} isAdmin=${isAdmin} adminUsers=${JSON.stringify(cfg.adminUsers || [])} normalizeId=${normalizeId(USERID)}`)
|
|
1953
1806
|
}
|
|
@@ -1961,21 +1814,21 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1961
1814
|
const parsedBatch = parseBatchCount(parsedSize.prompt, cfg.batchMax)
|
|
1962
1815
|
// 固定种子解析(--seed:xxx / --seed xxx / --seed=xxx),并从提示词中剥离
|
|
1963
1816
|
const parsedSeed = parseSeed(parsedBatch.prompt)
|
|
1964
|
-
const text = parsedSeed.prompt
|
|
1965
|
-
const seed = parsedSeed.seed
|
|
1966
|
-
const count = parsedBatch.count
|
|
1817
|
+
const text = parsedSeed.prompt
|
|
1818
|
+
const seed = parsedSeed.seed
|
|
1819
|
+
const count = parsedBatch.count
|
|
1967
1820
|
|
|
1968
1821
|
// P 点校验(按总价 = 张数 × 单价)
|
|
1969
1822
|
// P 点校验(按总价 = 张数 × 单价)
|
|
1970
1823
|
const pcheck = await precheckPoints(session, USERID, isAdmin, count * cfg.price)
|
|
1971
1824
|
if (!pcheck.ok) return pcheck.message
|
|
1972
1825
|
|
|
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
|
|
1826
|
+
// 原样模式
|
|
1827
|
+
const stripped = stripRawPrefix(text)
|
|
1828
|
+
const raw = stripped.raw
|
|
1829
|
+
const promptSections = splitPositiveNegativePrompt(stripped.prompt)
|
|
1830
|
+
const userPrompt = promptSections.positive
|
|
1831
|
+
const userNegativePrompt = promptSections.negative
|
|
1979
1832
|
if (!userPrompt) return session.text('.no-prompt')
|
|
1980
1833
|
|
|
1981
1834
|
// ComfyUI 就绪
|
|
@@ -1997,13 +1850,13 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
1997
1850
|
const tokenOpt = !globalOpt && !isAdmin && cfg.llmModel && cfg.llmBaseUrl
|
|
1998
1851
|
if (globalOpt) {
|
|
1999
1852
|
// 全局优化开启:一次优化,整批复用同一提示词
|
|
2000
|
-
const optimized = await optimizePrompt(session, userPrompt, false)
|
|
1853
|
+
const optimized = await optimizePrompt(session, userPrompt, false)
|
|
2001
1854
|
finalPrompt = optimized.prompt
|
|
2002
1855
|
degraded = !optimized.ok
|
|
2003
1856
|
optimizedReason = optimized.reason || ''
|
|
2004
1857
|
} else if (adminOpt) {
|
|
2005
1858
|
// 管理员在全局关闭时也免费优化(不耗券)
|
|
2006
|
-
const optimized = await optimizePrompt(session, userPrompt, true)
|
|
1859
|
+
const optimized = await optimizePrompt(session, userPrompt, true)
|
|
2007
1860
|
finalPrompt = optimized.prompt
|
|
2008
1861
|
degraded = !optimized.ok
|
|
2009
1862
|
optimizedReason = optimized.reason || ''
|
|
@@ -2044,7 +1897,7 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2044
1897
|
}
|
|
2045
1898
|
// 非按张优化模式:直接拼好整批复用的提示词
|
|
2046
1899
|
if (!perImageOptimize) {
|
|
2047
|
-
const composed = composePrompt(finalPrompt, raw)
|
|
1900
|
+
const composed = await composePrompt(finalPrompt, raw)
|
|
2048
1901
|
finalPrompt = appendInlineProtectedTags(composed.prompt, userPrompt, raw)
|
|
2049
1902
|
degraded = degraded || composed.degraded
|
|
2050
1903
|
}
|
|
@@ -2055,11 +1908,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2055
1908
|
if (cfg.outputLogs) logger.info(`[p-draw] ${USERID} 已扣除 ${count * cfg.price} P 点(${count} 张 × ${cfg.price}),余额 ${saving - count * cfg.price}`)
|
|
2056
1909
|
}
|
|
2057
1910
|
|
|
2058
|
-
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
2059
|
-
const generationOverrides = Object.assign(
|
|
2060
|
-
{ unet, seed },
|
|
2061
|
-
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
2062
|
-
)
|
|
1911
|
+
// 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
|
|
1912
|
+
const generationOverrides = Object.assign(
|
|
1913
|
+
{ unet, seed },
|
|
1914
|
+
userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
|
|
1915
|
+
)
|
|
2063
1916
|
|
|
2064
1917
|
// 性能:按张优化(perImageOptimize)时联网搜索只做一次,各图复用同一份结果
|
|
2065
1918
|
const searchCache = perImageOptimize && wantsWebSearch(userPrompt) ? await webSearch(userPrompt) : null
|
|
@@ -2068,11 +1921,11 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2068
1921
|
const queued = await enqueueBatch(count, async (i) => {
|
|
2069
1922
|
let p = finalPrompt
|
|
2070
1923
|
if (perImageOptimize) {
|
|
2071
|
-
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
2072
|
-
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
1924
|
+
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
1925
|
+
p = appendInlineProtectedTags((await composePrompt(optimized.prompt || userPrompt, raw)).prompt, userPrompt, raw)
|
|
2073
1926
|
}
|
|
2074
|
-
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
2075
|
-
return { ...generated, prompt: p }
|
|
1927
|
+
const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
|
|
1928
|
+
return { ...generated, prompt: p }
|
|
2076
1929
|
}, { USERID, isAdmin, totalPrice: count * cfg.price })
|
|
2077
1930
|
if (!queued.ok) return queued.message
|
|
2078
1931
|
const queuedTasks = queued.tasks
|
|
@@ -2095,45 +1948,52 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2095
1948
|
notice.push(session.text('.no-optimize', [reasons[noOptimizeReason] || noOptimizeReason]))
|
|
2096
1949
|
}
|
|
2097
1950
|
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 })
|
|
1951
|
+
const feedback = feedbackBase(session, { firstPosition, count })
|
|
1952
|
+
notice.push(...feedback.notices)
|
|
1953
|
+
await sendNotices(session, notice)
|
|
2102
1954
|
|
|
2103
1955
|
// 单张生成
|
|
2104
1956
|
const runOne = async (i) => {
|
|
2105
1957
|
let p = finalPrompt
|
|
2106
1958
|
if (perImageOptimize) {
|
|
2107
|
-
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
2108
|
-
p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
|
|
1959
|
+
const optimized = await optimizePrompt(session, userPrompt, true, searchCache)
|
|
1960
|
+
p = appendInlineProtectedTags((await composePrompt(optimized.prompt || userPrompt, raw)).prompt, userPrompt, raw)
|
|
2109
1961
|
}
|
|
2110
1962
|
let result
|
|
2111
1963
|
if (cfg.queueEnabled) {
|
|
2112
1964
|
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
|
|
1965
|
+
} else {
|
|
1966
|
+
try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
|
|
1967
|
+
}
|
|
1968
|
+
if (!result.prompt) result.prompt = p
|
|
1969
|
+
return result
|
|
2118
1970
|
}
|
|
2119
1971
|
|
|
2120
1972
|
const { results, successCount } = await executeBatch(USERID, isAdmin, count, cfg.price, runOne)
|
|
2121
1973
|
|
|
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)
|
|
1974
|
+
// 汇总
|
|
1975
|
+
const allOutputs = []
|
|
1976
|
+
const forwardOutputs = []
|
|
1977
|
+
const seeds = []
|
|
1978
|
+
const failures = []
|
|
1979
|
+
for (const item of results) {
|
|
1980
|
+
if (item.ok) {
|
|
1981
|
+
allOutputs.push(...item.outputs)
|
|
1982
|
+
forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
|
|
1983
|
+
if (item.seed != null) seeds.push(item.seed)
|
|
2132
1984
|
} else {
|
|
2133
1985
|
failures.push(`第 ${item.i + 1} 张:${item.message}`)
|
|
2134
1986
|
}
|
|
2135
1987
|
}
|
|
2136
1988
|
|
|
1989
|
+
const chargeNotice = buildChargeNotice({
|
|
1990
|
+
isAdmin,
|
|
1991
|
+
totalPrice: successCount * cfg.price,
|
|
1992
|
+
unetName: unet,
|
|
1993
|
+
seeds,
|
|
1994
|
+
})
|
|
1995
|
+
await sendNotices(session, [chargeNotice], { quote: true })
|
|
1996
|
+
|
|
2137
1997
|
if (!allOutputs.length) {
|
|
2138
1998
|
if (cfg.outputLogs) logger.warn(`生成全部失败(${USERID}),已按张退款`)
|
|
2139
1999
|
return session.text('.generate-failed', ['全部失败(已按张退款)'])
|
|
@@ -2141,17 +2001,10 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2141
2001
|
|
|
2142
2002
|
if (cfg.outputLogs) logger.success(`${USERID} 生成成功 ${successCount}/${count} 张`)
|
|
2143
2003
|
|
|
2144
|
-
// 发图:合并转发,不引用原指令
|
|
2145
|
-
await sendImagesAsForward(session, forwardOutputs)
|
|
2004
|
+
// 发图:合并转发,不引用原指令
|
|
2005
|
+
await sendImagesAsForward(session, forwardOutputs)
|
|
2146
2006
|
|
|
2147
|
-
|
|
2148
|
-
if (count > 1) {
|
|
2149
|
-
reply.push(session.text('.generate-ok-batch', [count * cfg.price, successCount, seeds.join(', ') || '-']))
|
|
2150
|
-
} else {
|
|
2151
|
-
reply.push(session.text('.generate-ok', [cfg.price, seeds[0] || '-']))
|
|
2152
|
-
}
|
|
2153
|
-
if (failures.length) reply.push(session.text('.batch-partial', [successCount, count, failures.length, failures.join(';')]))
|
|
2154
|
-
return reply.filter(Boolean).join('\n')
|
|
2007
|
+
return buildGenerationReply(session, { successCount, count, failures })
|
|
2155
2008
|
}
|
|
2156
2009
|
// ---------------- 提示词优化券交互式确认 ----------------
|
|
2157
2010
|
// 仅全局优化关闭 + 非管理员 + 已配置 LLM(tokenOpt 分支)时进入。
|
|
@@ -2281,8 +2134,9 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2281
2134
|
return price
|
|
2282
2135
|
}
|
|
2283
2136
|
|
|
2284
|
-
//
|
|
2137
|
+
// 启动时加载配置表,并将旧 fixedCharacters 数据迁入专用表。
|
|
2285
2138
|
await loadRuntimeState()
|
|
2139
|
+
await migrateFixedCharacters()
|
|
2286
2140
|
|
|
2287
2141
|
ctx.on('dispose', () => {
|
|
2288
2142
|
// 清理临时文件
|
|
@@ -2295,5 +2149,5 @@ exports.apply = async function apply(ctx, cfg) {
|
|
|
2295
2149
|
})
|
|
2296
2150
|
|
|
2297
2151
|
// 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
|
|
2298
|
-
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, sendNotices, sendImagesAsForward, executeBatch }
|
|
2152
|
+
return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, buildChargeNotice, buildGenerationReply, sendNotices, sendImagesAsForward, executeBatch }
|
|
2299
2153
|
}
|