koishi-plugin-p-draw 1.3.1 → 1.3.3

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.
Files changed (2) hide show
  1. package/index.js +121 -93
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -461,43 +461,56 @@ exports.apply = async function apply(ctx, cfg) {
461
461
  return { ok: true, tasks, firstPosition }
462
462
  }
463
463
 
464
- // 即时反馈的公共部分(队列/生成中 + 张数 + 扣费),返回待发送的行
465
- function feedbackBase(session, { firstPosition, count, totalPrice, isAdmin }) {
466
- const rows = []
467
- if (cfg.queueEnabled) {
468
- rows.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
469
- if (count > 1) rows.push(session.text('.batch-count', [count]))
470
- if (!isAdmin) rows.push(session.text('.charged', [totalPrice]))
471
- } else {
472
- rows.push(session.text('.generating'))
473
- if (count > 1) rows.push(session.text('.batch-count', [count]))
474
- if (!isAdmin) rows.push(session.text('.charged', [totalPrice]))
475
- }
476
- return rows
477
- }
478
-
479
- async function sendNotices(session, notices) {
480
- if (!notices.length) return
481
- try {
482
- await session.send(notices.filter(Boolean).join('\n'))
483
- } catch (e) {
484
- logger.warn(`发送反馈消息失败:${e.message}`)
485
- }
486
- }
487
-
488
- // 发图:引用用户触发指令的原消息,失败回退普通发送
489
- async function sendImagesWithQuote(session, outputs) {
490
- const imageElements = await Promise.all(outputs.map(async (src) => {
464
+ // 即时反馈的公共部分。扣费提醒单独返回,随后以引用消息发送。
465
+ function feedbackBase(session, { firstPosition, count, totalPrice, isAdmin }) {
466
+ const notices = []
467
+ if (cfg.queueEnabled) {
468
+ notices.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
469
+ if (count > 1) notices.push(session.text('.batch-count', [count]))
470
+ } else {
471
+ notices.push(session.text('.generating'))
472
+ if (count > 1) notices.push(session.text('.batch-count', [count]))
473
+ }
474
+ return {
475
+ notices,
476
+ chargeNotice: isAdmin ? '' : session.text('.charged', [totalPrice]),
477
+ }
478
+ }
479
+
480
+ async function sendNotices(session, notices, opts = {}) {
481
+ const content = notices.filter(Boolean).join('\n')
482
+ if (!content) return
483
+ try {
484
+ const message = opts.quote && session.messageId ? h.quote(session.messageId) + content : content
485
+ await session.send(message)
486
+ } catch (e) {
487
+ logger.warn(`发送反馈消息失败:${e.message}`)
488
+ }
489
+ }
490
+
491
+ // 生成图片使用合并转发;每张图后紧跟实际使用的最终提示词,不附原消息引用。
492
+ async function sendImagesAsForward(session, outputs) {
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 || '')
491
498
  const materialized = await materializeImageSource(src)
492
- return Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
493
- }))
494
- try {
495
- await session.send(h.quote(session.messageId) + imageElements.join(''))
496
- } catch (e) {
497
- logger.warn(`发送图片失败(引用):${e.message}`)
498
- await session.send(imageElements)
499
- }
500
- }
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
+ }
507
+ try {
508
+ await session.send(h('figure', nodes))
509
+ } catch (e) {
510
+ logger.warn(`发送转发图片失败:${e.message}`)
511
+ await session.send(fallback)
512
+ }
513
+ }
501
514
 
502
515
  // P 点读改写按用户串行化,避免并发指令互相覆盖余额
503
516
  const userLocks = new Map()
@@ -1186,7 +1199,7 @@ exports.apply = async function apply(ctx, cfg) {
1186
1199
  const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
1187
1200
  const verifyModel = String(cfg.verifyLlmModel || '').trim()
1188
1201
  if (!verifyBaseUrl || !verifyModel) {
1189
- return { ok: true, degraded: true, message: '', verdict: null, outputs: images }
1202
+ return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt }
1190
1203
  }
1191
1204
  const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
1192
1205
  const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
@@ -1294,11 +1307,11 @@ exports.apply = async function apply(ctx, cfg) {
1294
1307
  data = extractVerifyJson(reply)
1295
1308
  } catch (e) {
1296
1309
  logger.warn(`多人视觉校验失败:${e.message}`)
1297
- 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 }
1298
1311
  }
1299
1312
  if (!data) {
1300
1313
  logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
1301
- return { ok: true, degraded: true, message: '', verdict: null, outputs: images }
1314
+ return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt }
1302
1315
  }
1303
1316
  const verdict = verdictFromData(data)
1304
1317
  verdict.skipped = false
@@ -1332,7 +1345,7 @@ exports.apply = async function apply(ctx, cfg) {
1332
1345
  selectedOutputs = best.outputs
1333
1346
  selectedVerdict = best.verdict
1334
1347
  if (!multiAccepted && !cfg.multiSendDegradedCandidate) {
1335
- 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 }
1336
1349
  }
1337
1350
  const noteParts = []
1338
1351
  if (multiAccepted) {
@@ -1341,7 +1354,7 @@ exports.apply = async function apply(ctx, cfg) {
1341
1354
  noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
1342
1355
  }
1343
1356
  if (retries) noteParts.push(session.text('.multi-verify-failed', [selectedVerdict.issues.length ? ':' + selectedVerdict.issues.join(';').slice(0, 80) : '', retries]))
1344
- 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 }
1345
1358
  }
1346
1359
 
1347
1360
  function buildVerifySystemPrompt(multiPerson, planCount) {
@@ -1354,7 +1367,7 @@ exports.apply = async function apply(ctx, cfg) {
1354
1367
  }
1355
1368
 
1356
1369
  // 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
1357
- // runOne(i) 需返回 { ok, outputs, seed, message? };返回数组为多张输出(如视觉校验候选)。
1370
+ // runOne(i) 需返回 { ok, outputs, seed, prompt, message? };返回数组为多张输出(如视觉校验候选)。
1358
1371
  async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
1359
1372
  const results = []
1360
1373
  let successCount = 0
@@ -1368,7 +1381,7 @@ exports.apply = async function apply(ctx, cfg) {
1368
1381
  const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
1369
1382
  if (item.ok && outputs.length) {
1370
1383
  successCount += 1
1371
- 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 || '' })
1372
1385
  } else {
1373
1386
  if (!isAdmin) await refundP(USERID, unitPrice)
1374
1387
  if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
@@ -1449,8 +1462,10 @@ exports.apply = async function apply(ctx, cfg) {
1449
1462
  // 即时反馈
1450
1463
  const notice = []
1451
1464
  if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
1452
- notice.push(...feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin }))
1453
- await sendNotices(session, notice)
1465
+ const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin })
1466
+ notice.push(...feedback.notices)
1467
+ await sendNotices(session, notice)
1468
+ await sendNotices(session, [feedback.chargeNotice], { quote: true })
1454
1469
 
1455
1470
  // 单张生成 +(可选)视觉校验
1456
1471
  const runOne = async (i) => {
@@ -1460,25 +1475,27 @@ exports.apply = async function apply(ctx, cfg) {
1460
1475
  } else {
1461
1476
  try { result = await runComfyGenerate(finalPrompt, size, { negativePrompt: multiNegative, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1462
1477
  }
1463
- if (!result.ok || !result.outputs || !result.outputs.length) return result
1464
- if (!cfg.multiVerifyEnabled) {
1465
- return { ok: true, outputs: result.outputs, seed: result.seed, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
1466
- }
1467
- const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet)
1468
- if (!verified.ok) return { ok: false, message: verified.message }
1469
- 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 || '' }
1470
1485
  }
1471
1486
 
1472
1487
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
1473
1488
 
1474
- // 汇总
1475
- const allOutputs = []
1476
- const notes = []
1489
+ // 汇总
1490
+ const allOutputs = []
1491
+ const forwardOutputs = []
1492
+ const notes = []
1477
1493
  const seeds = []
1478
1494
  const failures = []
1479
1495
  for (const item of results) {
1480
- if (item.ok) {
1481
- 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 })))
1482
1499
  if (item.seed != null) seeds.push(item.seed)
1483
1500
  if (item.note) notes.push(item.note)
1484
1501
  } else {
@@ -1493,8 +1510,8 @@ exports.apply = async function apply(ctx, cfg) {
1493
1510
 
1494
1511
  if (cfg.outputLogs) logger.success(`${USERID} 多人生成成功 ${successCount}/${count} 张`)
1495
1512
 
1496
- // 发图:引用用户触发指令的原消息
1497
- await sendImagesWithQuote(session, allOutputs)
1513
+ // 发图:合并转发,不引用原指令
1514
+ await sendImagesAsForward(session, forwardOutputs)
1498
1515
 
1499
1516
  const reply = []
1500
1517
  if (count > 1) {
@@ -1625,29 +1642,34 @@ exports.apply = async function apply(ctx, cfg) {
1625
1642
  if (identity && fixedChars[identity]) notice.push(`已固定角色「${identity}」的身份 tags,各阶段外观将保持一致。`)
1626
1643
  if (!useLLM) notice.push(session.text('.series-no-llm'))
1627
1644
  if (degradedStages) notice.push(session.text('.prompt-degraded', ['(连续图阶段优化失败,已使用原始描述)']))
1628
- notice.push(...feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin }))
1629
- await sendNotices(session, notice)
1645
+ const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin })
1646
+ notice.push(...feedback.notices)
1647
+ await sendNotices(session, notice)
1648
+ await sendNotices(session, [feedback.chargeNotice], { quote: true })
1630
1649
 
1631
1650
  // 单阶段生成(共用 seed)
1632
1651
  const runOne = async (i) => {
1633
1652
  let result
1634
- if (cfg.queueEnabled) {
1635
- try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1636
- } else {
1637
- try { result = await runComfyGenerate(stagePrompts[i], size, { seed, unet }) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
1638
- }
1639
- 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
1640
1660
  }
1641
1661
 
1642
1662
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
1643
1663
 
1644
- // 汇总
1645
- const allOutputs = []
1646
- const seeds = []
1664
+ // 汇总
1665
+ const allOutputs = []
1666
+ const forwardOutputs = []
1667
+ const seeds = []
1647
1668
  const failures = []
1648
1669
  for (const item of results) {
1649
- if (item.ok) {
1650
- 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] || '' })))
1651
1673
  if (item.seed != null) seeds.push(item.seed)
1652
1674
  } else {
1653
1675
  failures.push(`第 ${item.i + 1} 阶段:${item.message}`)
@@ -1661,8 +1683,8 @@ exports.apply = async function apply(ctx, cfg) {
1661
1683
 
1662
1684
  if (cfg.outputLogs) logger.success(`${USERID} 连续图生成成功 ${successCount}/${count} 阶段(seed=${seed})`)
1663
1685
 
1664
- // 发图:引用用户触发指令的原消息
1665
- await sendImagesWithQuote(session, allOutputs)
1686
+ // 发图:合并转发,不引用原指令
1687
+ await sendImagesAsForward(session, forwardOutputs)
1666
1688
 
1667
1689
  const reply = []
1668
1690
  reply.push(session.text('.series-ok', [count * price, successCount, seed]))
@@ -2307,7 +2329,8 @@ exports.apply = async function apply(ctx, cfg) {
2307
2329
  const optimized = await optimizePrompt(session, userPrompt, true, i2iRule, searchCache)
2308
2330
  p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
2309
2331
  }
2310
- return runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun))
2332
+ const generated = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun))
2333
+ return { ...generated, prompt: p }
2311
2334
  }, { USERID, isAdmin, totalPrice: count * cfg.price })
2312
2335
  if (!queued.ok) return queued.message
2313
2336
  const queuedTasks = queued.tasks
@@ -2330,8 +2353,10 @@ exports.apply = async function apply(ctx, cfg) {
2330
2353
  notice.push(session.text('.no-optimize', [reasons[noOptimizeReason] || noOptimizeReason]))
2331
2354
  }
2332
2355
  if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
2333
- notice.push(...feedbackBase(session, { firstPosition, count, totalPrice: count * cfg.price, isAdmin }))
2334
- await sendNotices(session, notice)
2356
+ const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * cfg.price, isAdmin })
2357
+ notice.push(...feedback.notices)
2358
+ await sendNotices(session, notice)
2359
+ await sendNotices(session, [feedback.chargeNotice], { quote: true })
2335
2360
 
2336
2361
  // 单张生成
2337
2362
  const runOne = async (i) => {
@@ -2343,22 +2368,25 @@ exports.apply = async function apply(ctx, cfg) {
2343
2368
  let result
2344
2369
  if (cfg.queueEnabled) {
2345
2370
  try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2346
- } else {
2347
- try { result = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun)) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2348
- }
2349
- return result
2371
+ } else {
2372
+ try { result = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun)) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2373
+ }
2374
+ if (!result.prompt) result.prompt = p
2375
+ return result
2350
2376
  }
2351
2377
 
2352
2378
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, cfg.price, runOne)
2353
2379
 
2354
- // 汇总
2355
- const allOutputs = []
2356
- const seeds = []
2357
- const failures = []
2358
- for (const item of results) {
2359
- if (item.ok) {
2360
- allOutputs.push(...item.outputs)
2361
- if (item.seed != null) seeds.push(item.seed)
2380
+ // 汇总
2381
+ const allOutputs = []
2382
+ const forwardOutputs = []
2383
+ const seeds = []
2384
+ const failures = []
2385
+ for (const item of results) {
2386
+ if (item.ok) {
2387
+ allOutputs.push(...item.outputs)
2388
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
2389
+ if (item.seed != null) seeds.push(item.seed)
2362
2390
  } else {
2363
2391
  failures.push(`第 ${item.i + 1} 张:${item.message}`)
2364
2392
  }
@@ -2371,8 +2399,8 @@ exports.apply = async function apply(ctx, cfg) {
2371
2399
 
2372
2400
  if (cfg.outputLogs) logger.success(`${USERID} 生成成功 ${successCount}/${count} 张`)
2373
2401
 
2374
- // 发图:引用用户触发指令的原消息
2375
- await sendImagesWithQuote(session, allOutputs)
2402
+ // 发图:合并转发,不引用原指令
2403
+ await sendImagesAsForward(session, forwardOutputs)
2376
2404
 
2377
2405
  const reply = []
2378
2406
  if (count > 1) {
@@ -2525,5 +2553,5 @@ exports.apply = async function apply(ctx, cfg) {
2525
2553
  })
2526
2554
 
2527
2555
  // 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
2528
- return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise }
2529
- }
2556
+ return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise, sendNotices, sendImagesAsForward, executeBatch }
2557
+ }
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.1",
4
+ "version": "1.3.3",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",