koishi-plugin-p-draw 1.3.0 → 1.3.2

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 (3) hide show
  1. package/index.js +61 -48
  2. package/lib/media.js +22 -0
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -150,6 +150,7 @@ const {
150
150
  animaOotdI2IWorkflow, buildI2IWorkflow, customWorkflow,
151
151
  } = require('./lib/workflows')
152
152
  const { outputImages, waitComfyResult } = require('./lib/comfy')
153
+ const { materializeImageSource } = require('./lib/media')
153
154
  const {
154
155
  MULTI_PERSON_NEGATIVE_TAGS, buildMultiPersonPlanPrompt, parseMultiPersonPlan,
155
156
  renderMultiPersonCharacter, multiPersonAutoSize,
@@ -460,40 +461,46 @@ exports.apply = async function apply(ctx, cfg) {
460
461
  return { ok: true, tasks, firstPosition }
461
462
  }
462
463
 
463
- // 即时反馈的公共部分(队列/生成中 + 张数 + 扣费),返回待发送的行
464
- function feedbackBase(session, { firstPosition, count, totalPrice, isAdmin }) {
465
- const rows = []
466
- if (cfg.queueEnabled) {
467
- rows.push(session.text('.queued', [firstPosition, cfg.queueMaxRequests || '∞']))
468
- if (count > 1) rows.push(session.text('.batch-count', [count]))
469
- if (!isAdmin) rows.push(session.text('.charged', [totalPrice]))
470
- } else {
471
- rows.push(session.text('.generating'))
472
- if (count > 1) rows.push(session.text('.batch-count', [count]))
473
- if (!isAdmin) rows.push(session.text('.charged', [totalPrice]))
474
- }
475
- return rows
476
- }
477
-
478
- async function sendNotices(session, notices) {
479
- if (!notices.length) return
480
- try {
481
- await session.send(notices.filter(Boolean).join('\n'))
482
- } catch (e) {
483
- logger.warn(`发送反馈消息失败:${e.message}`)
484
- }
485
- }
486
-
487
- // 发图:引用用户触发指令的原消息,失败回退普通发送
488
- async function sendImagesWithQuote(session, outputs) {
489
- const imageElements = outputs.map(src => h.image(src))
490
- try {
491
- await session.send(h.quote(session.messageId) + imageElements.join(''))
492
- } catch (e) {
493
- logger.warn(`发送图片失败(引用):${e.message}`)
494
- await session.send(imageElements)
495
- }
496
- }
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 imageElements = await Promise.all(outputs.map(async (src) => {
494
+ const materialized = await materializeImageSource(src)
495
+ return Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
496
+ }))
497
+ try {
498
+ await session.send(h('figure', imageElements))
499
+ } catch (e) {
500
+ logger.warn(`发送转发图片失败:${e.message}`)
501
+ await session.send(imageElements)
502
+ }
503
+ }
497
504
 
498
505
  // P 点读改写按用户串行化,避免并发指令互相覆盖余额
499
506
  const userLocks = new Map()
@@ -1445,8 +1452,10 @@ exports.apply = async function apply(ctx, cfg) {
1445
1452
  // 即时反馈
1446
1453
  const notice = []
1447
1454
  if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
1448
- notice.push(...feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin }))
1449
- await sendNotices(session, notice)
1455
+ const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin })
1456
+ notice.push(...feedback.notices)
1457
+ await sendNotices(session, notice)
1458
+ await sendNotices(session, [feedback.chargeNotice], { quote: true })
1450
1459
 
1451
1460
  // 单张生成 +(可选)视觉校验
1452
1461
  const runOne = async (i) => {
@@ -1489,8 +1498,8 @@ exports.apply = async function apply(ctx, cfg) {
1489
1498
 
1490
1499
  if (cfg.outputLogs) logger.success(`${USERID} 多人生成成功 ${successCount}/${count} 张`)
1491
1500
 
1492
- // 发图:引用用户触发指令的原消息
1493
- await sendImagesWithQuote(session, allOutputs)
1501
+ // 发图:合并转发,不引用原指令
1502
+ await sendImagesAsForward(session, allOutputs)
1494
1503
 
1495
1504
  const reply = []
1496
1505
  if (count > 1) {
@@ -1621,8 +1630,10 @@ exports.apply = async function apply(ctx, cfg) {
1621
1630
  if (identity && fixedChars[identity]) notice.push(`已固定角色「${identity}」的身份 tags,各阶段外观将保持一致。`)
1622
1631
  if (!useLLM) notice.push(session.text('.series-no-llm'))
1623
1632
  if (degradedStages) notice.push(session.text('.prompt-degraded', ['(连续图阶段优化失败,已使用原始描述)']))
1624
- notice.push(...feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin }))
1625
- await sendNotices(session, notice)
1633
+ const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * price, isAdmin })
1634
+ notice.push(...feedback.notices)
1635
+ await sendNotices(session, notice)
1636
+ await sendNotices(session, [feedback.chargeNotice], { quote: true })
1626
1637
 
1627
1638
  // 单阶段生成(共用 seed)
1628
1639
  const runOne = async (i) => {
@@ -1657,8 +1668,8 @@ exports.apply = async function apply(ctx, cfg) {
1657
1668
 
1658
1669
  if (cfg.outputLogs) logger.success(`${USERID} 连续图生成成功 ${successCount}/${count} 阶段(seed=${seed})`)
1659
1670
 
1660
- // 发图:引用用户触发指令的原消息
1661
- await sendImagesWithQuote(session, allOutputs)
1671
+ // 发图:合并转发,不引用原指令
1672
+ await sendImagesAsForward(session, allOutputs)
1662
1673
 
1663
1674
  const reply = []
1664
1675
  reply.push(session.text('.series-ok', [count * price, successCount, seed]))
@@ -2326,8 +2337,10 @@ exports.apply = async function apply(ctx, cfg) {
2326
2337
  notice.push(session.text('.no-optimize', [reasons[noOptimizeReason] || noOptimizeReason]))
2327
2338
  }
2328
2339
  if (parsedBatch.clamped) notice.push(session.text('.batch-limit', [count]))
2329
- notice.push(...feedbackBase(session, { firstPosition, count, totalPrice: count * cfg.price, isAdmin }))
2330
- await sendNotices(session, notice)
2340
+ const feedback = feedbackBase(session, { firstPosition, count, totalPrice: count * cfg.price, isAdmin })
2341
+ notice.push(...feedback.notices)
2342
+ await sendNotices(session, notice)
2343
+ await sendNotices(session, [feedback.chargeNotice], { quote: true })
2331
2344
 
2332
2345
  // 单张生成
2333
2346
  const runOne = async (i) => {
@@ -2367,8 +2380,8 @@ exports.apply = async function apply(ctx, cfg) {
2367
2380
 
2368
2381
  if (cfg.outputLogs) logger.success(`${USERID} 生成成功 ${successCount}/${count} 张`)
2369
2382
 
2370
- // 发图:引用用户触发指令的原消息
2371
- await sendImagesWithQuote(session, allOutputs)
2383
+ // 发图:合并转发,不引用原指令
2384
+ await sendImagesAsForward(session, allOutputs)
2372
2385
 
2373
2386
  const reply = []
2374
2387
  if (count > 1) {
@@ -2521,5 +2534,5 @@ exports.apply = async function apply(ctx, cfg) {
2521
2534
  })
2522
2535
 
2523
2536
  // 暴露内部接口供自动化测试调用(Koishi 忽略 apply 返回值,不影响生产行为)
2524
- return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise }
2525
- }
2537
+ return { couponConfirmFlow, buyCouponsAndConsume, normalizeConfirm, resolveCouponPrice, handleGenerateI2I, extractImageFromSession, uploadImageToComfyui, taggerImage, animaI2IWorkflow, animaStyleI2IWorkflow, animaOotdI2IWorkflow, buildI2IWorkflow, detectI2ICapabilities, parseDenoise, sendNotices, sendImagesAsForward }
2538
+ }
package/lib/media.js ADDED
@@ -0,0 +1,22 @@
1
+ // 消息媒体来源处理。
2
+ // OneBot 不一定能读取机器人进程本地的 file:// 路径,因此发送前转换为 Buffer。
3
+
4
+ const fsp = require('fs/promises')
5
+ const { fileURLToPath } = require('url')
6
+
7
+ async function materializeImageSource(source, readFile = fsp.readFile) {
8
+ if (typeof source !== 'string' || !/^file:\/\//i.test(source)) return source
9
+ let filePath
10
+ try {
11
+ filePath = fileURLToPath(source)
12
+ } catch (e) {
13
+ return source
14
+ }
15
+ try {
16
+ return await readFile(filePath)
17
+ } catch (e) {
18
+ return source
19
+ }
20
+ }
21
+
22
+ module.exports = { materializeImageSource }
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.0",
4
+ "version": "1.3.2",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",