koishi-plugin-p-draw 1.3.3 → 1.3.6

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,21 +488,21 @@ exports.apply = async function apply(ctx, cfg) {
488
488
  }
489
489
  }
490
490
 
491
- // 生成图片使用合并转发;每张图后紧跟实际使用的最终提示词,不附原消息引用。
491
+ // 生成图片使用合并转发;每张图后紧跟实际使用的正负面提示词,不附原消息引用。
492
492
  async function sendImagesAsForward(session, outputs) {
493
493
  const nodes = []
494
494
  const fallback = []
495
495
  for (const output of outputs) {
496
496
  const src = typeof output === 'string' ? output : output.src
497
497
  const prompt = typeof output === 'string' ? '' : String(output.prompt || '')
498
+ const negativePrompt = typeof output === 'string' ? '' : String(output.negativePrompt || '')
498
499
  const materialized = await materializeImageSource(src)
499
500
  const image = Buffer.isBuffer(materialized) ? h.image(materialized) : h.image(src)
500
501
  nodes.push(h('message', image))
501
502
  fallback.push(image)
502
- if (prompt) {
503
- nodes.push(h('message', prompt))
504
- fallback.push(prompt)
505
- }
503
+ const promptText = `Positive:\n${prompt}\n\nNegative:\n${negativePrompt}`
504
+ nodes.push(h('message', promptText))
505
+ fallback.push(promptText)
506
506
  }
507
507
  try {
508
508
  await session.send(h('figure', nodes))
@@ -664,9 +664,10 @@ exports.apply = async function apply(ctx, cfg) {
664
664
  width,
665
665
  height,
666
666
  steps,
667
- cfg: cfgVal,
668
- prompt_id: promptId,
669
- }
667
+ cfg: cfgVal,
668
+ prompt_id: promptId,
669
+ negativePrompt,
670
+ }
670
671
  }
671
672
 
672
673
  // ---------------- LLM 提示词优化 ----------------
@@ -1195,11 +1196,11 @@ exports.apply = async function apply(ctx, cfg) {
1195
1196
 
1196
1197
  // 视觉校验(anima_verify + generation_verifier 移植):对生成的图片跑视觉 LLM,
1197
1198
  // 不合格则用相同提示词重试(最多 multiCandidateCount 张),按多候选规则挑选并返回结果。
1198
- async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet) {
1199
+ async function verifyGeneratedImages(session, images, userRequest, prompt, size, planCount, unet, negativePrompt) {
1199
1200
  const verifyBaseUrl = String(cfg.verifyLlmBaseUrl || '').trim()
1200
1201
  const verifyModel = String(cfg.verifyLlmModel || '').trim()
1201
1202
  if (!verifyBaseUrl || !verifyModel) {
1202
- return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt }
1203
+ return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt, negativePrompt }
1203
1204
  }
1204
1205
  const passScore = Math.max(0, Math.min(10, parseInt(cfg.multiVerifyPassScore) || 6))
1205
1206
  const candidateCount = Math.max(1, Math.min(3, parseInt(cfg.multiCandidateCount) || 2))
@@ -1207,9 +1208,10 @@ exports.apply = async function apply(ctx, cfg) {
1207
1208
  const systemPrompt = buildVerifySystemPrompt(true, planCount)
1208
1209
  const candidates = []
1209
1210
  let lastVerdict = null
1210
- let retries = 0
1211
- let currentImages = images
1212
- let currentPrompt = prompt
1211
+ let retries = 0
1212
+ let currentImages = images
1213
+ let currentPrompt = prompt
1214
+ let currentNegativePrompt = negativePrompt
1213
1215
 
1214
1216
  async function verifyOnce(imgs, userReq) {
1215
1217
  const controller = new AbortController()
@@ -1307,15 +1309,15 @@ exports.apply = async function apply(ctx, cfg) {
1307
1309
  data = extractVerifyJson(reply)
1308
1310
  } catch (e) {
1309
1311
  logger.warn(`多人视觉校验失败:${e.message}`)
1310
- return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt }
1312
+ return { ok: true, degraded: true, message: session.text('.multi-verify-error', [String(e && e.message || e)]), verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
1311
1313
  }
1312
1314
  if (!data) {
1313
1315
  logger.warn(`多人视觉校验返回无法解析:${reply.slice(0, 200)}`)
1314
- return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt }
1316
+ return { ok: true, degraded: true, message: '', verdict: null, outputs: images, prompt: currentPrompt, negativePrompt: currentNegativePrompt }
1315
1317
  }
1316
1318
  const verdict = verdictFromData(data)
1317
1319
  verdict.skipped = false
1318
- candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt })
1320
+ candidates.push({ outputs: currentImages, verdict, prompt: currentPrompt, negativePrompt: currentNegativePrompt })
1319
1321
  selectedOutputs = currentImages
1320
1322
  selectedVerdict = verdict
1321
1323
 
@@ -1329,12 +1331,13 @@ exports.apply = async function apply(ctx, cfg) {
1329
1331
  if (hint) {
1330
1332
  currentPrompt = `${userRequest}\n【上次问题,请修正】${hint}`
1331
1333
  }
1332
- const regen = await runComfyGenerate(currentPrompt, size, { unet })
1334
+ const regen = await runComfyGenerate(currentPrompt, size, { unet, negativePrompt: currentNegativePrompt })
1333
1335
  if (!regen.ok) {
1334
1336
  logger.warn(`多人校验重试生成失败:${regen.message}`)
1335
1337
  break
1336
1338
  }
1337
- currentImages = regen.outputs
1339
+ currentImages = regen.outputs
1340
+ currentNegativePrompt = regen.negativePrompt || currentNegativePrompt
1338
1341
  }
1339
1342
 
1340
1343
  // 多候选挑选
@@ -1345,7 +1348,7 @@ exports.apply = async function apply(ctx, cfg) {
1345
1348
  selectedOutputs = best.outputs
1346
1349
  selectedVerdict = best.verdict
1347
1350
  if (!multiAccepted && !cfg.multiSendDegradedCandidate) {
1348
- return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt }
1351
+ return { ok: false, discarded: true, message: session.text('.multi-verify-discarded'), verdict: selectedVerdict, outputs: [], prompt: best.prompt, negativePrompt: best.negativePrompt }
1349
1352
  }
1350
1353
  const noteParts = []
1351
1354
  if (multiAccepted) {
@@ -1354,7 +1357,7 @@ exports.apply = async function apply(ctx, cfg) {
1354
1357
  noteParts.push(session.text('.multi-verify-degraded', selectedVerdict.issues.length ? '(' + selectedVerdict.issues.join(';').slice(0, 80) + ')' : ''))
1355
1358
  }
1356
1359
  if (retries) noteParts.push(session.text('.multi-verify-failed', [selectedVerdict.issues.length ? ':' + selectedVerdict.issues.join(';').slice(0, 80) : '', retries]))
1357
- return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt }
1360
+ return { ok: true, degraded: false, message: noteParts.join('\n'), verdict: selectedVerdict, outputs: selectedOutputs, prompt: best.prompt, negativePrompt: best.negativePrompt }
1358
1361
  }
1359
1362
 
1360
1363
  function buildVerifySystemPrompt(multiPerson, planCount) {
@@ -1367,7 +1370,7 @@ exports.apply = async function apply(ctx, cfg) {
1367
1370
  }
1368
1371
 
1369
1372
  // 共享批量执行器:一次性扣除总价,逐张生成,单张失败只退该张单价。
1370
- // runOne(i) 需返回 { ok, outputs, seed, prompt, message? };返回数组为多张输出(如视觉校验候选)。
1373
+ // runOne(i) 需返回 { ok, outputs, seed, prompt, negativePrompt, message? };返回数组为多张输出(如视觉校验候选)。
1371
1374
  async function executeBatch(USERID, isAdmin, count, unitPrice, runOne) {
1372
1375
  const results = []
1373
1376
  let successCount = 0
@@ -1381,7 +1384,7 @@ exports.apply = async function apply(ctx, cfg) {
1381
1384
  const outputs = Array.isArray(item.outputs) ? item.outputs : (item.outputs ? [item.outputs] : [])
1382
1385
  if (item.ok && outputs.length) {
1383
1386
  successCount += 1
1384
- results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', note: item.note || '' })
1387
+ results.push({ i, ok: true, outputs, seed: item.seed, prompt: item.prompt || '', negativePrompt: item.negativePrompt || '', note: item.note || '' })
1385
1388
  } else {
1386
1389
  if (!isAdmin) await refundP(USERID, unitPrice)
1387
1390
  if (cfg.outputLogs) logger.warn(`批量第 ${i + 1} 张生成失败(${USERID}):${item.message || '无输出'}`)
@@ -1477,11 +1480,11 @@ exports.apply = async function apply(ctx, cfg) {
1477
1480
  }
1478
1481
  if (!result.ok || !result.outputs || !result.outputs.length) return result
1479
1482
  if (!cfg.multiVerifyEnabled) {
1480
- return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
1483
+ return { ok: true, outputs: result.outputs, seed: result.seed, prompt: finalPrompt, negativePrompt: result.negativePrompt, note: session.text('.multi-degraded', ['(未启用校验或未配置视觉模型)']) }
1481
1484
  }
1482
- const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet)
1485
+ const verified = await verifyGeneratedImages(session, result.outputs, text, finalPrompt, size, plan.characters.length, unet, result.negativePrompt || multiNegative)
1483
1486
  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 || '' }
1487
+ return { ok: true, outputs: verified.outputs, seed: result.seed, prompt: verified.prompt || finalPrompt, negativePrompt: verified.negativePrompt || result.negativePrompt || multiNegative, note: verified.message || '' }
1485
1488
  }
1486
1489
 
1487
1490
  const { results, successCount } = await executeBatch(USERID, isAdmin, count, price, runOne)
@@ -1495,7 +1498,7 @@ exports.apply = async function apply(ctx, cfg) {
1495
1498
  for (const item of results) {
1496
1499
  if (item.ok) {
1497
1500
  allOutputs.push(...item.outputs)
1498
- forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
1501
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
1499
1502
  if (item.seed != null) seeds.push(item.seed)
1500
1503
  if (item.note) notes.push(item.note)
1501
1504
  } else {
@@ -1669,7 +1672,7 @@ exports.apply = async function apply(ctx, cfg) {
1669
1672
  for (const item of results) {
1670
1673
  if (item.ok) {
1671
1674
  allOutputs.push(...item.outputs)
1672
- forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '' })))
1675
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || stagePrompts[item.i] || '', negativePrompt: item.negativePrompt })))
1673
1676
  if (item.seed != null) seeds.push(item.seed)
1674
1677
  } else {
1675
1678
  failures.push(`第 ${item.i + 1} 阶段:${item.message}`)
@@ -2231,10 +2234,12 @@ exports.apply = async function apply(ctx, cfg) {
2231
2234
  const pcheck = await precheckPoints(session, USERID, isAdmin, count * cfg.price)
2232
2235
  if (!pcheck.ok) return pcheck.message
2233
2236
 
2234
- // 原样模式
2235
- const stripped = stripRawPrefix(text)
2236
- const raw = stripped.raw
2237
- const userPrompt = stripped.prompt
2237
+ // 原样模式
2238
+ const stripped = stripRawPrefix(text)
2239
+ const raw = stripped.raw
2240
+ const promptSections = splitPositiveNegativePrompt(stripped.prompt)
2241
+ const userPrompt = promptSections.positive
2242
+ const userNegativePrompt = promptSections.negative
2238
2243
  if (!userPrompt) return session.text('.no-prompt')
2239
2244
 
2240
2245
  // ComfyUI 就绪
@@ -2315,9 +2320,15 @@ exports.apply = async function apply(ctx, cfg) {
2315
2320
  }
2316
2321
 
2317
2322
  // i2i 运行参数:模式(style/ootd/plain)、--denoise 覆盖值、检测到的能力
2318
- const i2iRun = i2iImage
2319
- ? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
2320
- : {}
2323
+ const i2iRun = i2iImage
2324
+ ? { i2iImage, i2i: { mode: (i2iOpts && i2iOpts.mode) || 'plain', denoise: denoise != null ? denoise : null, caps: (i2iOpts && i2iOpts.caps) || null } }
2325
+ : {}
2326
+ // 用户手写了 negative: 区块时,默认负面词仍保留;用户 tag 只补充未出现的部分。
2327
+ const generationOverrides = Object.assign(
2328
+ { unet, seed },
2329
+ i2iRun,
2330
+ userNegativePrompt ? { negativePrompt: mergeNegativePrompts(cfg.negativePrompt, userNegativePrompt) } : {},
2331
+ )
2321
2332
 
2322
2333
  // 性能:按张优化(perImageOptimize)时联网搜索只做一次,各图复用同一份结果
2323
2334
  const searchCache = perImageOptimize && wantsWebSearch(userPrompt) ? await webSearch(userPrompt) : null
@@ -2329,7 +2340,7 @@ exports.apply = async function apply(ctx, cfg) {
2329
2340
  const optimized = await optimizePrompt(session, userPrompt, true, i2iRule, searchCache)
2330
2341
  p = appendInlineProtectedTags(composePrompt(optimized.prompt || userPrompt, raw).prompt, userPrompt, raw)
2331
2342
  }
2332
- const generated = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun))
2343
+ const generated = await runComfyGenerate(p, parsedSize.size, generationOverrides)
2333
2344
  return { ...generated, prompt: p }
2334
2345
  }, { USERID, isAdmin, totalPrice: count * cfg.price })
2335
2346
  if (!queued.ok) return queued.message
@@ -2369,7 +2380,7 @@ exports.apply = async function apply(ctx, cfg) {
2369
2380
  if (cfg.queueEnabled) {
2370
2381
  try { result = await queuedTasks[i] } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2371
2382
  } else {
2372
- try { result = await runComfyGenerate(p, parsedSize.size, Object.assign({ unet, seed }, i2iRun)) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2383
+ try { result = await runComfyGenerate(p, parsedSize.size, generationOverrides) } catch (e) { result = { ok: false, message: `生成失败:${e.message}` } }
2373
2384
  }
2374
2385
  if (!result.prompt) result.prompt = p
2375
2386
  return result
@@ -2385,7 +2396,7 @@ exports.apply = async function apply(ctx, cfg) {
2385
2396
  for (const item of results) {
2386
2397
  if (item.ok) {
2387
2398
  allOutputs.push(...item.outputs)
2388
- forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt })))
2399
+ forwardOutputs.push(...item.outputs.map(src => ({ src, prompt: item.prompt || finalPrompt, negativePrompt: item.negativePrompt })))
2389
2400
  if (item.seed != null) seeds.push(item.seed)
2390
2401
  } else {
2391
2402
  failures.push(`第 ${item.i + 1} 张:${item.message}`)
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.3",
4
+ "version": "1.3.6",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",