koishi-plugin-p-draw 1.3.2 → 1.3.5

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 CHANGED
@@ -139,10 +139,10 @@ exports.Config = Schema.object({
139
139
  // ------------------------------------------------------------------
140
140
  const {
141
141
  normalizeBaseUrl, escapeRe, parseGenerationSize, parseBatchCount, parseSeed, parseDenoise,
142
- stripRawPrefix, parseNameTags, parsePresetList, mergeTagText,
142
+ stripRawPrefix, splitPositiveNegativePrompt, parseNameTags, parsePresetList, mergeTagText,
143
143
  } = require('./lib/parse')
144
144
  const {
145
- splitTags, canonicalTagText, joinPromptParts, cleanContentTags, appendInlineProtectedTags,
145
+ splitTags, canonicalTagText, joinPromptParts, mergeNegativePrompts, cleanContentTags, appendInlineProtectedTags,
146
146
  NO_ARTIST_RE, NO_STYLE_RE,
147
147
  } = require('./lib/tags')
148
148
  const {
@@ -488,17 +488,27 @@ exports.apply = async function apply(ctx, cfg) {
488
488
  }
489
489
  }
490
490
 
491
- // 生成图片使用合并转发;不附原消息引用。发送失败时回退普通图片消息。
491
+ // 生成图片使用合并转发;每张图后紧跟实际使用的最终提示词,不附原消息引用。
492
492
  async function sendImagesAsForward(session, outputs) {
493
- const imageElements = await Promise.all(outputs.map(async (src) => {
493
+ const nodes = []
494
+ const fallback = []
495
+ for (const output of outputs) {
496
+ const src = typeof output === 'string' ? output : output.src
497
+ const prompt = typeof output === 'string' ? '' : String(output.prompt || '')
494
498
  const materialized = await materializeImageSource(src)
495
- return Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
496
- }))
499
+ const image = Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
500
+ nodes.push(h('message', image))
501
+ fallback.push(image)
502
+ if (prompt) {
503
+ nodes.push(h('message', prompt))
504
+ fallback.push(prompt)
505
+ }
506
+ }
497
507
  try {
498
- await session.send(h('figure', imageElements))
508
+ await session.send(h('figure', nodes))
499
509
  } catch (e) {
500
510
  logger.warn(`发送转发图片失败:${e.message}`)
501
- await session.send(imageElements)
511
+ await session.send(fallback)
502
512
  }
503
513
  }
504
514
 
@@ -1189,7 +1199,7 @@ exports.apply = async function apply(ctx, cfg) {
1189
1199
  const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
1190
1200
  const verifyModel = String(cfg.verifyLlmModel || '').trim()
1191
1201
  if (!verifyBaseUrl || !verifyModel) {
1192
- return { ok: true, degraded: true, message: '', verdict: null, outputs: images }
1202
+ return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt }
1193
1203
  }
1194
1204
  const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
1195
1205
  const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
@@ -1297,11 +1307,11 @@ exports.apply = async function apply(ctx, cfg) {
1297
1307
  data = extractVerifyJson(reply)
1298
1308
  } catch (e) {
1299
1309
  logger.warn(`多人视觉校验失败:${e.message}`)
1300
- return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images }
1310
+ return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt }
1301
1311
  }
1302
1312
  if (!data) {
1303
1313
  logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
1304
- return { ok: true, degraded: true, message: '', verdict: null, outputs: images }
1314
+ return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt }
1305
1315
  }
1306
1316
  const verdict = verdictFromData(data)
1307
1317
  verdict.skipped = false
@@ -1335,7 +1345,7 @@ exports.apply = async function apply(ctx, cfg) {
1335
1345
  selectedOutputs = best.outputs
1336
1346
  selectedVerdict = best.verdict
1337
1347
  if (!multiAccepted && !cfg.multiSendDegradedCandidate) {
1338
- return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [] }
1348
+ return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt }
1339
1349
  }
1340
1350
  const noteParts = []
1341
1351
  if (multiAccepted) {
@@ -1344,7 +1354,7 @@ exports.apply = async function apply(ctx, cfg) {
1344
1354
  noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
1345
1355
  }
1346
1356
  if (retries) noteParts.push(session.text('.multi-verify-failed', [selectedVerdict.issues.length ? ':' + selectedVerdict.issues.join(';').slice(0, 80) : '', retries]))
1347
- return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs }
1357
+ return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt }
1348
1358
  }
1349
1359
 
1350
1360
  function buildVerifySystemPrompt(multiPerson, planCount) {
@@ -1357,7 +1367,7 @@ exports.apply = async function apply(ctx, cfg) {
1357
1367
  }
1358
1368
 
1359
1369
  // 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
1360
- // runOne(i) 需返回 { ok, outputs, seed, message? };返回数组为多张输出(如视觉校验候选)。
1370
+ // runOne(i) 需返回 { ok, outputs, seed, prompt, message? };返回数组为多张输出(如视觉校验候选)。
1361
1371
  async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
1362
1372
  const results = []
1363
1373
  let successCount = 0
@@ -1371,7 +1381,7 @@ exports.apply = async function apply(ctx, cfg) {
1371
1381
  const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
1372
1382
  if (item.ok && outputs.length) {
1373
1383
  successCount += 1
1374
- results.push({ i, ok: true, outputs, seed: item.seed, note: item.note || '' })
1384
+ results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', note: item.note || '' })
1375
1385
  } else {
1376
1386
  if (!isAdmin) await refundP(USERID, unitPrice)
1377
1387
  if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
@@ -1465,25 +1475,27 @@ exports.apply = async function apply(ctx, cfg) {
1465
1475
  } else {
1466
1476
  try { result = await runComfyGenerate(finalPrompt, size, { negativePrompt: multiNegative, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1467
1477
  }
1468
- if (!result.ok || !result.outputs || !result.outputs.length) return result
1469
- if (!cfg.multiVerifyEnabled) {
1470
- return { ok: true, outputs: result.outputs, seed: result.seed, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
1471
- }
1472
- const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet)
1473
- if (!verified.ok) return { ok: false, message: verified.message }
1474
- return { ok: true, outputs: verified.outputs, seed: result.seed, note: verified.message || '' }
1478
+ if (!result.ok || !result.outputs || !result.outputs.length) return result
1479
+ if (!cfg.multiVerifyEnabled) {
1480
+ return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
1481
+ }
1482
+ const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet)
1483
+ if (!verified.ok) return { ok: false, message: verified.message }
1484
+ return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, note: verified.message || '' }
1475
1485
  }
1476
1486
 
1477
1487
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
1478
1488
 
1479
- // 汇总
1480
- const allOutputs = []
1481
- const notes = []
1489
+ // 汇总
1490
+ const allOutputs = []
1491
+ const forwardOutputs = []
1492
+ const notes = []
1482
1493
  const seeds = []
1483
1494
  const failures = []
1484
1495
  for (const item of results) {
1485
- if (item.ok) {
1486
- allOutputs.push(...item.outputs)
1496
+ if (item.ok) {
1497
+ allOutputs.push(...item.outputs)
1498
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
1487
1499
  if (item.seed != null) seeds.push(item.seed)
1488
1500
  if (item.note) notes.push(item.note)
1489
1501
  } else {
@@ -1499,7 +1511,7 @@ exports.apply = async function apply(ctx, cfg) {
1499
1511
  if (cfg.outputLogs) logger.success(`${USERID} 多人生成成功 ${successCount}/${count} 张`)
1500
1512
 
1501
1513
  // 发图:合并转发,不引用原指令
1502
- await sendImagesAsForward(session, allOutputs)
1514
+ await sendImagesAsForward(session, forwardOutputs)
1503
1515
 
1504
1516
  const reply = []
1505
1517
  if (count > 1) {
@@ -1638,23 +1650,26 @@ exports.apply = async function apply(ctx, cfg) {
1638
1650
  // 单阶段生成(共用 seed)
1639
1651
  const runOne = async (i) => {
1640
1652
  let result
1641
- if (cfg.queueEnabled) {
1642
- try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1643
- } else {
1644
- try { result = await runComfyGenerate(stagePrompts[i], size, { seed, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1645
- }
1646
- return result
1653
+ if (cfg.queueEnabled) {
1654
+ try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1655
+ } else {
1656
+ try { result = await runComfyGenerate(stagePrompts[i], size, { seed, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1657
+ }
1658
+ result.prompt = stagePrompts[i]
1659
+ return result
1647
1660
  }
1648
1661
 
1649
1662
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
1650
1663
 
1651
- // 汇总
1652
- const allOutputs = []
1653
- const seeds = []
1664
+ // 汇总
1665
+ const allOutputs = []
1666
+ const forwardOutputs = []
1667
+ const seeds = []
1654
1668
  const failures = []
1655
1669
  for (const item of results) {
1656
- if (item.ok) {
1657
- allOutputs.push(...item.outputs)
1670
+ if (item.ok) {
1671
+ allOutputs.push(...item.outputs)
1672
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '' })))
1658
1673
  if (item.seed != null) seeds.push(item.seed)
1659
1674
  } else {
1660
1675
  failures.push(`第 ${item.i + 1} 阶段:${item.message}`)
@@ -1669,7 +1684,7 @@ exports.apply = async function apply(ctx, cfg) {
1669
1684
  if (cfg.outputLogs) logger.success(`${USERID} 连续图生成成功 ${successCount}/${count} 阶段(seed=${seed})`)
1670
1685
 
1671
1686
  // 发图:合并转发,不引用原指令
1672
- await sendImagesAsForward(session, allOutputs)
1687
+ await sendImagesAsForward(session, forwardOutputs)
1673
1688
 
1674
1689
  const reply = []
1675
1690
  reply.push(session.text('.series-ok', [count * price, successCount, seed]))
@@ -2216,10 +2231,12 @@ exports.apply = async function apply(ctx, cfg) {
2216
2231
  const pcheck = await precheckPoints(session, USERID, isAdmin, count * cfg.price)
2217
2232
  if (!pcheck.ok) return pcheck.message
2218
2233
 
2219
- // 原样模式
2220
- const stripped = stripRawPrefix(text)
2221
- const raw = stripped.raw
2222
- const userPrompt = stripped.prompt
2234
+ // 原样模式
2235
+ const stripped = stripRawPrefix(text)
2236
+ const raw = stripped.raw
2237
+ const promptSections = splitPositiveNegativePrompt(stripped.prompt)
2238
+ const userPrompt = promptSections.positive
2239
+ const userNegativePrompt = promptSections.negative
2223
2240
  if (!userPrompt) return session.text('.no-prompt')
2224
2241
 
2225
2242
  // ComfyUI 就绪
@@ -2300,9 +2317,15 @@ exports.apply = async function apply(ctx, cfg) {
2300
2317
  }
2301
2318
 
2302
2319
  // i2i 运行参数:模式(style/ootd/plain)、--denoise 覆盖值、检测到的能力
2303
- const i2iRun = i2iImage
2304
- ? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
2305
- : {}
2320
+ const i2iRun = i2iImage
2321
+ ? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
2322
+ : {}
2323
+ // 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
2324
+ const generationOverrides = Object.assign(
2325
+ { unet, seed },
2326
+ i2iRun,
2327
+ userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
2328
+ )
2306
2329
 
2307
2330
  // 性能:按张优化(perImageOptimize)时联网搜索只做一次,各图复用同一份结果
2308
2331
  const searchCache = perImageOptimize && wantsWebSearch(userPrompt) ? await webSearch(userPrompt) : null
@@ -2314,7 +2337,8 @@ exports.apply = async function apply(ctx, cfg) {
2314
2337
  const optimized = await optimizePrompt(session, userPrompt, true, i2iRule, searchCache)
2315
2338
  p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
2316
2339
  }
2317
- return runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun))
2340
+ const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
2341
+ return { ...generated, prompt: p }
2318
2342
  }, { USERID, isAdmin, totalPrice: count * cfg.price })
2319
2343
  if (!queued.ok) return queued.message
2320
2344
  const queuedTasks = queued.tasks
@@ -2352,22 +2376,25 @@ exports.apply = async function apply(ctx, cfg) {
2352
2376
  let result
2353
2377
  if (cfg.queueEnabled) {
2354
2378
  try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2355
- } else {
2356
- try { result = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun)) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2357
- }
2358
- return result
2379
+ } else {
2380
+ try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2381
+ }
2382
+ if (!result.prompt) result.prompt = p
2383
+ return result
2359
2384
  }
2360
2385
 
2361
2386
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, cfg.price, runOne)
2362
2387
 
2363
- // 汇总
2364
- const allOutputs = []
2365
- const seeds = []
2366
- const failures = []
2367
- for (const item of results) {
2368
- if (item.ok) {
2369
- allOutputs.push(...item.outputs)
2370
- if (item.seed != null) seeds.push(item.seed)
2388
+ // 汇总
2389
+ const allOutputs = []
2390
+ const forwardOutputs = []
2391
+ const seeds = []
2392
+ const failures = []
2393
+ for (const item of results) {
2394
+ if (item.ok) {
2395
+ allOutputs.push(...item.outputs)
2396
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
2397
+ if (item.seed != null) seeds.push(item.seed)
2371
2398
  } else {
2372
2399
  failures.push(`第 ${item.i + 1} 张:${item.message}`)
2373
2400
  }
@@ -2381,7 +2408,7 @@ exports.apply = async function apply(ctx, cfg) {
2381
2408
  if (cfg.outputLogs) logger.success(`${USERID} 生成成功 ${successCount}/${count} 张`)
2382
2409
 
2383
2410
  // 发图:合并转发,不引用原指令
2384
- await sendImagesAsForward(session, allOutputs)
2411
+ await sendImagesAsForward(session, forwardOutputs)
2385
2412
 
2386
2413
  const reply = []
2387
2414
  if (count > 1) {
@@ -2534,5 +2561,5 @@ exports.apply = async function apply(ctx, cfg) {
2534
2561
  })
2535
2562
 
2536
2563
  // 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
2537
- return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise, sendNotices, sendImagesAsForward }
2564
+ return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise, sendNotices, sendImagesAsForward, executeBatch }
2538
2565
  }
package/lib/parse.js CHANGED
@@ -186,6 +186,19 @@ function stripRawPrefix(prompt) {
186
186
  return { raw: false, prompt: text }
187
187
  }
188
188
 
189
+ // 将用户手写的正负面区块拆开。只识别独立行标记,避免把普通 tag(如 negative space)误判为负面段。
190
+ const NEGATIVE_SECTION_RE = /(?:^|\r?\n)\s*(?:negative(?:\s*prompt)?|negative_prompt|负面(?:提示词|词)?)\s*[::]\s*/i
191
+
192
+ function splitPositiveNegativePrompt(prompt) {
193
+ const text = String(prompt || '').trim()
194
+ const match = NEGATIVE_SECTION_RE.exec(text)
195
+ if (!match) return { positive: text, negative: '' }
196
+ return {
197
+ positive: text.slice(0, match.index).trim(),
198
+ negative: text.slice(match.index + match[0].length).trim(),
199
+ }
200
+ }
201
+
189
202
  function mergeTagText(existing, addition) {
190
203
  const tags = []
191
204
  const seen = new Set()
@@ -259,6 +272,7 @@ module.exports = {
259
272
  parseDenoise,
260
273
  RAW_PREFIXES,
261
274
  stripRawPrefix,
275
+ splitPositiveNegativePrompt,
262
276
  mergeTagText,
263
277
  parseNameTags,
264
278
  parsePresetList,
package/lib/tags.js CHANGED
@@ -236,6 +236,11 @@ function joinPromptParts(parts) {
236
236
  return tags.join(', ')
237
237
  }
238
238
 
239
+ // 默认负面词始终保留,用户手写负面词只补充未出现的 tag。
240
+ function mergeNegativePrompts(defaultPrompt, userPrompt) {
241
+ return joinPromptParts([defaultPrompt, userPrompt])
242
+ }
243
+
239
244
  module.exports = {
240
245
  splitTags,
241
246
  normalizeTagKey,
@@ -258,4 +263,5 @@ module.exports = {
258
263
  isCharacterIdentityTag,
259
264
  cleanContentTags,
260
265
  joinPromptParts,
266
+ mergeNegativePrompts,
261
267
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-p-draw",
3
3
  "description": "p点-绘图,连接本地 ComfyUI 生图。支持自然语言优化、尺寸选择、画师组、固定角色、生成队列,需要配合 p-qiandao 使用",
4
- "version": "1.3.2",
4
+ "version": "1.3.5",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",