dsh-comfyui-rp 0.2.3 → 0.2.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/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.2.5
4
+
5
+ - **修「只收到时间戳、正文没跟着来」**:自动投递前先等目标会话空闲(最多 15 秒)。有些宿主(例如带自动续写的 Tavern)会让会话一直有事在跑,这时 `queue` 投递的正文会被排到下一轮,而当前这一轮模型只看到时间上下文注入的那三行。
6
+ - 等不到空闲就改用 `steer`,把正文塞进正在跑的这一轮,至少模型能看到内容;任务记录里新增 `deliveryMode` 字段,日志里也会写「目标会话忙,正文改用 steer 投递」。
7
+ - 顺带在任务 / 日志里带上等待时长,方便判断是不是宿主一直在忙。
8
+
9
+ ## 0.2.4
10
+
11
+ - **纯黑图误判修复**:判定前先看体积——真·纯黑 PNG 只有 20 多 KB,超过 **120 KB** 的图直接不参与判定。之前有正常图(约 1 MB)在「原地替换后立刻复查」时被判成黑图,导致同一张图被连续替换 3 次。
12
+ - **判黑要两次都黑**:第一次判黑后会隔 0.9 秒、绕过浏览器缓存再验一次,两次都黑才自动替换,避免一次绘制的偶发全黑。
13
+ - **判定可追溯**:每次判定都会把统计(体积、暗像素比例、平均亮度、亮点比例、不透明覆盖)写进 Host 日志,「日志」页能直接看到;出现误判时能一眼定位是哪条规则触发。
14
+
3
15
  ## 0.2.3
4
16
 
5
17
  - **自动补图可以随时停下**:补图进行中,图片框左下菜单的按钮会变成「终止」(之前因为补图是内部任务,菜单里根本看不到它),点一下清空队列并中止正在跑的那一单;全屏和工作台的「终止」也一样。右侧栏顶部还有一条「停止自动补图」。下一次新任务开始时自动复位。
package/lib/api.js CHANGED
@@ -1293,6 +1293,21 @@ export function createApi({ store, jobs, hub, getConfig, ctx, sc, routePath, ver
1293
1293
  }));
1294
1294
  }
1295
1295
 
1296
+ if (action === 'black-check' && method === 'POST') {
1297
+ const b = body || {};
1298
+ const stats = [
1299
+ b.darkRatio !== undefined ? `暗 ${(Number(b.darkRatio) * 100).toFixed(2)}%` : '',
1300
+ b.meanLum !== undefined ? `平均亮度 ${Number(b.meanLum).toFixed(1)}` : '',
1301
+ b.brightRatio !== undefined ? `亮点 ${(Number(b.brightRatio) * 100).toFixed(2)}%` : '',
1302
+ b.coverage !== undefined ? `不透明 ${Math.round(Number(b.coverage) * 100)}%` : '',
1303
+ ].filter(Boolean).join(' · ');
1304
+ const skippedText = b.skipped === 'bytes' ? '跳过(体积太大,不可能是纯黑)' : (b.skipped ? `跳过(${b.skipped})` : '');
1305
+ const head = b.black === true ? '判为纯黑图' : (skippedText || '不是黑图');
1306
+ hub.pushLog(b.black === true ? 'warn' : 'info',
1307
+ `黑图检测:${head} · ${b.fileName || b.imageId || '?'}${b.bytes ? ` · ${b.bytes} B` : ''}${stats ? ` · ${stats}` : ''}`);
1308
+ return sendJson(res, 200, ok({ logged: true }));
1309
+ }
1310
+
1296
1311
  if (action === 'logs' && method === 'GET') {
1297
1312
  return sendJson(res, 200, ok({ logs: hub.logs.slice(-300) }));
1298
1313
  }
package/lib/client.js CHANGED
@@ -447,6 +447,7 @@ window.__ModuleLoader__.load({
447
447
  jobs: function (limit) { return req("GET", "jobs?limit=" + (limit || 30)); },
448
448
  job: function (id) { return req("GET", "jobs/" + encodeURIComponent(id)); },
449
449
  abort: function (id) { return req("POST", "jobs/" + encodeURIComponent(id) + "/abort", {}); },
450
+ blackCheck: function (body) { return req("POST", "black-check", body || {}); },
450
451
  delJob: function (id) { return req("DELETE", "jobs/" + encodeURIComponent(id)); },
451
452
  clearJobs: function (keepActive) { return req("POST", "jobs/clear", { keepActive: keepActive !== false }); },
452
453
  images: function (q) { return req("GET", "images" + (q ? "?" + q : "")); },
@@ -3752,29 +3753,62 @@ window.__ModuleLoader__.load({
3752
3753
  && darkRatio >= 0.995 && meanLum <= 8 && brightRatio < 0.001;
3753
3754
  return { black: black, darkRatio: darkRatio, brightRatio: brightRatio, meanLum: meanLum, coverage: coverage, opaque: opaque };
3754
3755
  }
3755
- var isNearlyBlack = function (url) {
3756
+ // 纯黑图 PNG 压缩后极小(832×1216 的真黑图 ≈ 25 KB),超过这个体积不可能是纯黑。
3757
+ // 这条是硬保险:就算画布绘制 / 解码出了岔子,也不会把一张有内容的图当成黑图。
3758
+ var BLACK_MAX_BYTES = 120000;
3759
+ // 返回 judgeNearlyBlack 的统计对象;bust=true 时加个 nc 参数绕过浏览器缓存(确认二次判定用)。
3760
+ var isNearlyBlack = function (url, bust) {
3756
3761
  return new Promise(function (resolve) {
3757
- if (!url || typeof document === "undefined" || typeof Image === "undefined") return resolve(false);
3762
+ if (!url || typeof document === "undefined" || typeof Image === "undefined") return resolve({ black: false });
3758
3763
  var probe = new Image();
3759
3764
  probe.onload = function () {
3760
3765
  try {
3761
- if (!probe.naturalWidth || !probe.naturalHeight) return resolve(false);
3766
+ if (!probe.naturalWidth || !probe.naturalHeight) return resolve({ black: false });
3762
3767
  var w = Math.max(1, Math.min(256, probe.naturalWidth));
3763
3768
  var h = Math.max(1, Math.min(256, probe.naturalHeight));
3764
3769
  var canvas = document.createElement("canvas");
3765
3770
  canvas.width = w;
3766
3771
  canvas.height = h;
3767
3772
  var g = canvas.getContext("2d");
3768
- if (!g) return resolve(false);
3773
+ if (!g) return resolve({ black: false });
3769
3774
  g.drawImage(probe, 0, 0, w, h);
3770
3775
  var d = g.getImageData(0, 0, w, h).data;
3771
- resolve(judgeNearlyBlack(d).black);
3772
- } catch (e) { resolve(false); }
3776
+ resolve(judgeNearlyBlack(d));
3777
+ } catch (e) { resolve({ black: false }); }
3773
3778
  };
3774
- probe.onerror = function () { resolve(false); };
3775
- probe.src = url;
3779
+ probe.onerror = function () { resolve({ black: false }); };
3780
+ probe.src = url + (bust ? (url.indexOf("?") >= 0 ? "&" : "?") + "nc=" + Date.now() : "");
3776
3781
  });
3777
3782
  };
3783
+ // 判定入口:先看体积,再验像素;判黑要**两次都黑**才算数(第二次绕过缓存)。
3784
+ var probeBlackImage = function (im) {
3785
+ var url = im && im.url ? String(im.url) : "";
3786
+ var bytes = im && im.bytes ? Number(im.bytes) : 0;
3787
+ if (!url) return Promise.resolve({ black: false, skipped: "no-url" });
3788
+ if (bytes > BLACK_MAX_BYTES) return Promise.resolve({ black: false, skipped: "bytes", bytes: bytes, url: url });
3789
+ return isNearlyBlack(url).then(function (first) {
3790
+ if (!first.black) return { black: false, stats: first, url: url, bytes: bytes };
3791
+ return new Promise(function (resolve) { setTimeout(resolve, 900); })
3792
+ .then(function () { return isNearlyBlack(url, true); })
3793
+ .then(function (second) {
3794
+ return { black: !!second.black, stats: second, first: first, url: url, bytes: bytes, confirmed: true };
3795
+ });
3796
+ });
3797
+ };
3798
+ // 把判定结果写进 Host 日志(日志页可见),误判时能直接看到当时的统计。
3799
+ var reportBlackCheck = function (im, verdict) {
3800
+ try {
3801
+ var s = verdict && verdict.stats ? verdict.stats : {};
3802
+ api.blackCheck({
3803
+ imageId: im && im.id ? String(im.id) : "",
3804
+ fileName: im && im.fileName ? String(im.fileName) : "",
3805
+ bytes: verdict && verdict.bytes ? verdict.bytes : (im && im.bytes) || 0,
3806
+ black: !!(verdict && verdict.black),
3807
+ skipped: (verdict && verdict.skipped) || "",
3808
+ darkRatio: s.darkRatio, meanLum: s.meanLum, brightRatio: s.brightRatio, coverage: s.coverage,
3809
+ }).catch(function () { /* 诊断日志失败不影响主流程 */ });
3810
+ } catch (e) { /* ignore */ }
3811
+ };
3778
3812
  var blackQueueRef = useRef([]);
3779
3813
  // 自动补图可以随时叫停:队列清空 + 正在跑的那单 abort,并且这一轮不再自动补。
3780
3814
  // blackStopRef 在下一轮新任务完成时复位,避免「点过一次停止后就再也不补」。
@@ -3815,7 +3849,8 @@ window.__ModuleLoader__.load({
3815
3849
  if (blackStopRef.current) return;
3816
3850
  blackQueueRef.current.push({
3817
3851
  id: String(im.id), jobId: im.jobId || (sourceJob && sourceJob.id) || "",
3818
- setIndex: im.setIndex, url: im.url, attempt: attempt || 1, key: key || "", label: label || "",
3852
+ setIndex: im.setIndex, url: im.url, bytes: im.bytes || 0,
3853
+ attempt: attempt || 1, key: key || "", label: label || "",
3819
3854
  });
3820
3855
  syncBlackPending();
3821
3856
  };
@@ -3826,8 +3861,9 @@ window.__ModuleLoader__.load({
3826
3861
  if (blackStopRef.current) { blackQueueRef.current = []; syncBlackPending(); return; }
3827
3862
  blackBusyRef.current = true;
3828
3863
  syncBlackPending();
3829
- isNearlyBlack(next.url).then(function (black) {
3830
- if (!black) return null;
3864
+ probeBlackImage(next).then(function (verdict) {
3865
+ reportBlackCheck(next, verdict);
3866
+ if (!verdict.black) return null;
3831
3867
  if (next.attempt === 1) {
3832
3868
  var nk = String(next.jobId || next.id || "");
3833
3869
  if (!blackNotifiedRef.current.has(nk)) {
@@ -3881,8 +3917,9 @@ window.__ModuleLoader__.load({
3881
3917
  if (!retried || !retried.id || !retried.url) return;
3882
3918
  var slotKey = blackSlotKey(sourceJob, retried);
3883
3919
  var attempt = Math.max(1, Number(blackRetryRef.current[slotKey]) || 1);
3884
- isNearlyBlack(retried.url).then(function (black) {
3885
- if (!black) {
3920
+ probeBlackImage(retried).then(function (verdict) {
3921
+ reportBlackCheck(retried, verdict);
3922
+ if (!verdict.black) {
3886
3923
  delete blackRetryRef.current[slotKey];
3887
3924
  notify("黑图已替换为正常图", "ok");
3888
3925
  // 替换已经落到图片上、也确认不是黑图了,最后再报出图完成。
package/lib/jobs.js CHANGED
@@ -388,6 +388,12 @@ export function createJobsEngine({ ctx, sc, store, hub, getConfig, getActiveSess
388
388
  job.replyText = r.text;
389
389
  job.deliveryMs = r.ms;
390
390
  job.sawOwnPrompt = r.sawOwnPrompt;
391
+ job.deliveryMode = r.deliveryMode || '';
392
+ if (r.deliveryMode === 'steer') {
393
+ // 目标会话当时不空闲(例如宿主在做自动续写),正文改成 steer 塞进当前这一轮;
394
+ // 不用 queue 的话正文会排到下一轮,这一轮模型只看到时间上下文。
395
+ log('warn', `目标会话忙,正文改用 steer 投递(等待空闲 ${r.idleWaited || 0}ms)`, { jobId: job.id });
396
+ }
391
397
  job.interrupted = r.interrupted;
392
398
  job.targetSessionId = r.sessionId || job.targetSessionId;
393
399
  job.promptSource = r.mode;
@@ -1295,6 +1301,7 @@ export function createJobsEngine({ ctx, sc, store, hub, getConfig, getActiveSess
1295
1301
  replyChars: String(job.replyText || '').length,
1296
1302
  deliveryMs: job.deliveryMs,
1297
1303
  sawOwnPrompt: job.sawOwnPrompt,
1304
+ deliveryMode: job.deliveryMode || '',
1298
1305
  promptSource: job.promptSource || '',
1299
1306
  interrupted: job.interrupted,
1300
1307
  ruleId: job.ruleId,
package/lib/meta.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export const PACKAGE_NAME = 'dsh-comfyui-rp';
8
8
  export const PLUGIN_NAME = 'dsh-comfyui-rp';
9
- export const VERSION = '0.2.3';
9
+ export const VERSION = '0.2.5';
10
10
 
11
11
  /** 对外路由前缀。不能带尾斜杠(dsh-host-webserver 的 prefix 匹配规则)。 */
12
12
  export const ROUTE_PATH = '/__airp_comfy';
package/lib/session.js CHANGED
@@ -294,7 +294,19 @@ export async function askLocal(ctx, sc, { sessionId, text, timeoutMs, signal } =
294
294
  `读不到会话的日志位置(session.seq = ${String(session?.seq)})。为避免把上一轮回复当成本次答案,这次不猜。`,
295
295
  'bad-session-state');
296
296
  }
297
+ // 先等目标会话空闲再投递。有些宿主(Tavern 这种带自动续写的)会让会话一直有事在跑,
298
+ // 这时 `queue` 的正文会排到下一轮,而当前这轮模型只看到时间上下文注入的那条
299
+ // ——表现就是「只收到时间戳,正文没跟着来」。
300
+ // 等不到空闲就用 `steer` 把正文塞进正在跑的这一轮,至少模型能看到内容。
301
+ const IDLE_WAIT_MS = 15000;
302
+ const waitBegan = Date.now();
303
+ while (agent.status !== 'idle' && Date.now() - waitBegan < IDLE_WAIT_MS) {
304
+ if (signal && signal.aborted) break;
305
+ await sleep(200);
306
+ }
307
+ const idleWaited = Date.now() - waitBegan;
297
308
  const busyBefore = agent.status !== 'idle';
309
+ const deliveryMode = busyBefore ? 'steer' : 'queue';
298
310
  const expectEnds = busyBefore ? 2 : 1;
299
311
  const started = Date.now();
300
312
 
@@ -321,7 +333,7 @@ export async function askLocal(ctx, sc, { sessionId, text, timeoutMs, signal } =
321
333
  inflightLocal.add(sid);
322
334
  try {
323
335
  await controller.prompt(
324
- { requestId, sessionId: sid, mode: 'queue', content: [{ type: 'text', text: body }] },
336
+ { requestId, sessionId: sid, mode: deliveryMode, content: [{ type: 'text', text: body }] },
325
337
  ac.signal,
326
338
  );
327
339
 
@@ -378,6 +390,8 @@ export async function askLocal(ctx, sc, { sessionId, text, timeoutMs, signal } =
378
390
  turn: got.turn,
379
391
  ms: Date.now() - started,
380
392
  busyBefore,
393
+ deliveryMode,
394
+ idleWaited,
381
395
  sawOwnPrompt,
382
396
  interrupted: got.interrupted,
383
397
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-comfyui-rp",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "ComfyRP Bridge:装在 DSH 里的本地文生图自动化桥。捕获会话正文 → 提示词会话 → 本地 ComfyUI 出图 → 图片回到右侧栏 / 悬浮球 / 图库。不依赖外部桥进程,也不调用 LLM。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",