dsh-plugin-image-workbench 1.1.10 → 1.1.12
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/package.json +1 -1
- package/server/app.py +14 -0
- package/server/pool.py +141 -9
- package/server/static/index.html +118 -3
package/package.json
CHANGED
package/server/app.py
CHANGED
|
@@ -517,6 +517,20 @@ async def api_pool_refresh():
|
|
|
517
517
|
return JSONResponse(POOL.snapshot())
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
# 并行数:默认 3,可调,上限 = 可用账号数。调完立即生效(不重启)
|
|
521
|
+
class WorkersBody(BaseModel):
|
|
522
|
+
workers: int = 0
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@app.post("/api/pool/workers")
|
|
526
|
+
async def api_pool_workers(body: WorkersBody):
|
|
527
|
+
res = await POOL.set_workers(body.workers)
|
|
528
|
+
# 带一份最新快照回去,前端不用再等下一轮轮询
|
|
529
|
+
res["snapshot"] = {"workers": POOL.workers, "workers_max": POOL.workers_cap(),
|
|
530
|
+
"queued": POOL.snapshot()["queued"]}
|
|
531
|
+
return JSONResponse(res)
|
|
532
|
+
|
|
533
|
+
|
|
520
534
|
@app.get("/api/pool/fails")
|
|
521
535
|
async def api_pool_fails(days: int = 7):
|
|
522
536
|
"""账号失败账本:今天每个号失败几次、最近几天各失败几次。
|
package/server/pool.py
CHANGED
|
@@ -35,9 +35,17 @@ STATE_FILE = BASE / "workbench" / "pending_jobs.json"
|
|
|
35
35
|
DEFAULT_POOL_DIR = Path(os.environ.get(
|
|
36
36
|
"DSH_IMAGE_POOL_DIR", str(Path.home() / ".dsh" / "chatgpt2api-image")))
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
38
|
+
# 并行数(worker 个数)现在由用户自己定:默认 3,写进 config.local.json 的 workers。
|
|
39
|
+
# 上限 = 有效账号数(可用账号),一个号同时只出一张图;账号明细读不到时退回默认值,
|
|
40
|
+
# 不会因为一次探测失败就把用户设置判成非法。绝对硬顶防止手抖填出四位数。
|
|
41
|
+
WORKERS_DEFAULT = 3
|
|
42
|
+
WORKERS_ABS_MAX = 40
|
|
43
|
+
# 兼容旧引用(环境变量仍可给个初值)
|
|
44
|
+
WORKERS = max(1, min(WORKERS_ABS_MAX,
|
|
45
|
+
int(os.environ.get("WORKBENCH_WORKERS", WORKERS_DEFAULT) or WORKERS_DEFAULT)))
|
|
46
|
+
# 队列里的「提前下班」令牌:并行上限下调时塞进队列,worker 取到它就退出。
|
|
47
|
+
# 用令牌而不是 cancel 任务,是为了不打断正在出图的那张(cancel 会连带杀掉在途请求)
|
|
48
|
+
RETIRE_TOKEN = "__wb_retire__"
|
|
41
49
|
IMAGE_TIMEOUT = 300 # 实测单张 45~62 秒,留足余量
|
|
42
50
|
TEXT_TIMEOUT = 180
|
|
43
51
|
# 失败不中断任务:重新排队就是换号(号池按 round-robin 取号),最多换 3 轮。
|
|
@@ -333,6 +341,8 @@ def load_config() -> dict:
|
|
|
333
341
|
"edits_style": "auto", # auto | json | multipart | off
|
|
334
342
|
"image_response_format": "auto", # auto | b64_json | url
|
|
335
343
|
"monitor_mode": "auto", # auto | pool | single
|
|
344
|
+
# 并行出图数(worker 个数):默认 3,上限由「可用账号数」决定
|
|
345
|
+
"workers": WORKERS,
|
|
336
346
|
}
|
|
337
347
|
if CONFIG_FILE.exists():
|
|
338
348
|
try:
|
|
@@ -342,6 +352,39 @@ def load_config() -> dict:
|
|
|
342
352
|
return cfg
|
|
343
353
|
|
|
344
354
|
|
|
355
|
+
def save_config(updates: dict) -> dict:
|
|
356
|
+
"""把若干键合并写回 config.local.json(原子替换),返回写后的完整配置。
|
|
357
|
+
|
|
358
|
+
只做浅合并:别的键原样留着,不会因为存一个并行数就把接口配置抹掉。
|
|
359
|
+
"""
|
|
360
|
+
cfg: dict = {}
|
|
361
|
+
if CONFIG_FILE.exists():
|
|
362
|
+
try:
|
|
363
|
+
got = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
364
|
+
if isinstance(got, dict):
|
|
365
|
+
cfg = got
|
|
366
|
+
except Exception as e:
|
|
367
|
+
log(f"config.local.json 读取失败(准备覆盖写): {e}")
|
|
368
|
+
cfg.update(updates or {})
|
|
369
|
+
try:
|
|
370
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
371
|
+
tmp = CONFIG_FILE.with_suffix(".json.tmp")
|
|
372
|
+
tmp.write_text(json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
373
|
+
tmp.replace(CONFIG_FILE)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
log(f"config.local.json 写入失败: {mask(e)[:160]}")
|
|
376
|
+
return cfg
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def workers_from_cfg() -> int:
|
|
380
|
+
"""启动时的并行数:配置里存的值优先,非法值一律退回默认 3。"""
|
|
381
|
+
try:
|
|
382
|
+
n = int(load_config().get("workers") or WORKERS_DEFAULT)
|
|
383
|
+
except Exception:
|
|
384
|
+
n = WORKERS_DEFAULT
|
|
385
|
+
return max(1, min(WORKERS_ABS_MAX, n))
|
|
386
|
+
|
|
387
|
+
|
|
345
388
|
def _read_pool_user_key(cfg: dict) -> str:
|
|
346
389
|
"""从号池目录里的 user-api-keys.json 读第一个 sk- 用户 Key(仅号池模式用)。"""
|
|
347
390
|
p = pool_home(cfg) / "user-api-keys.json"
|
|
@@ -512,6 +555,11 @@ class PoolManager:
|
|
|
512
555
|
self.queue: asyncio.Queue[str] = asyncio.Queue()
|
|
513
556
|
self.running: dict[str, dict] = {}
|
|
514
557
|
self._workers: list[asyncio.Task] = []
|
|
558
|
+
# 并行数:用户可调,持久化在 config.local.json 的 workers
|
|
559
|
+
self.workers: int = workers_from_cfg()
|
|
560
|
+
self._wid_seq: int = 0 # worker 编号只增不减,日志里对得上
|
|
561
|
+
self._retire_pending: int = 0 # 队列里还没被取走的「提前下班」令牌数
|
|
562
|
+
self._workers_lock = asyncio.Lock()
|
|
515
563
|
# 正在执行的单个任务:手动停止时只取消它,不杀 worker,队列才能继续接新活
|
|
516
564
|
self._job_tasks: dict[str, asyncio.Task] = {}
|
|
517
565
|
self._lock = asyncio.Lock()
|
|
@@ -539,13 +587,80 @@ class PoolManager:
|
|
|
539
587
|
await self.refresh_env_ids()
|
|
540
588
|
await self.refresh_accounts(force=True)
|
|
541
589
|
self._restore()
|
|
542
|
-
|
|
543
|
-
|
|
590
|
+
self.workers = workers_from_cfg()
|
|
591
|
+
for _ in range(self.workers):
|
|
592
|
+
self._spawn_worker()
|
|
544
593
|
self._acct_task = asyncio.create_task(self._account_refresh_loop())
|
|
545
594
|
p = provider_cfg()
|
|
546
|
-
log(f"本地执行层启动:{
|
|
595
|
+
log(f"本地执行层启动:{self.workers} 个 worker,接口 {p['base_url']}"
|
|
547
596
|
f"({p['name']}),已同步 {len(self.accounts)} 个账号")
|
|
548
597
|
|
|
598
|
+
def _spawn_worker(self) -> asyncio.Task:
|
|
599
|
+
self._wid_seq += 1
|
|
600
|
+
t = asyncio.create_task(self._worker(self._wid_seq))
|
|
601
|
+
self._workers.append(t)
|
|
602
|
+
return t
|
|
603
|
+
|
|
604
|
+
# ---------- 并行数(用户可调) ----------
|
|
605
|
+
def valid_accounts(self) -> int:
|
|
606
|
+
"""有效账号数 = 能出图的号(可用 + 正在出图中)。
|
|
607
|
+
|
|
608
|
+
「正在出图」也是有效账号,不能因为它在忙就从上限里扣掉:否则一边出图
|
|
609
|
+
一边上限往下掉,用户刚设好的并行数会被自己压回去。
|
|
610
|
+
限流 / 已下线 / 未绑定 / 待验证 / 异常这些才算无效。
|
|
611
|
+
"""
|
|
612
|
+
if self.accounts:
|
|
613
|
+
return sum(1 for a in self.accounts
|
|
614
|
+
if a.get("status") in ("ready", "busy"))
|
|
615
|
+
# 账号明细拿不到时退回号池自报的可用数
|
|
616
|
+
try:
|
|
617
|
+
return int(self.pool_stat.get("active") or 0)
|
|
618
|
+
except Exception:
|
|
619
|
+
return 0
|
|
620
|
+
|
|
621
|
+
def workers_cap(self) -> int:
|
|
622
|
+
"""并行上限:不超过有效账号数(一个号同时只出一张图)。"""
|
|
623
|
+
n = self.valid_accounts()
|
|
624
|
+
if n <= 0:
|
|
625
|
+
# 账号明细暂时读不到时不收紧,免得一次探测失败就把用户的设置判成非法
|
|
626
|
+
return max(WORKERS_DEFAULT, self.workers)
|
|
627
|
+
return max(1, min(n, WORKERS_ABS_MAX))
|
|
628
|
+
|
|
629
|
+
async def set_workers(self, n: int) -> dict:
|
|
630
|
+
"""改并行数:多的立刻拉起来,少的发「下班令牌」等它把手上的图出完再退。"""
|
|
631
|
+
async with self._workers_lock:
|
|
632
|
+
cap = self.workers_cap()
|
|
633
|
+
try:
|
|
634
|
+
want = int(n)
|
|
635
|
+
except Exception:
|
|
636
|
+
return {"ok": False, "workers": self.workers, "max": cap,
|
|
637
|
+
"message": "并行数必须是整数"}
|
|
638
|
+
if want < 1:
|
|
639
|
+
return {"ok": False, "workers": self.workers, "max": cap,
|
|
640
|
+
"message": "并行数至少 1"}
|
|
641
|
+
target = min(want, cap)
|
|
642
|
+
old = self.workers
|
|
643
|
+
# 已经退出的 worker 任务从列表里清掉,别让列表越堆越长
|
|
644
|
+
self._workers = [t for t in self._workers if not t.done()]
|
|
645
|
+
if target > old:
|
|
646
|
+
for _ in range(target - old):
|
|
647
|
+
self._spawn_worker()
|
|
648
|
+
elif target < old:
|
|
649
|
+
for _ in range(old - target):
|
|
650
|
+
self._retire_pending += 1
|
|
651
|
+
await self.queue.put(RETIRE_TOKEN)
|
|
652
|
+
self.workers = target
|
|
653
|
+
save_config({"workers": target})
|
|
654
|
+
if target != old:
|
|
655
|
+
log(f"并行数 {old} → {target}"
|
|
656
|
+
+ (f"(请求 {want},按有效账号 {cap} 收紧)" if target != want else ""))
|
|
657
|
+
msg = f"并行上限已设为 {target}"
|
|
658
|
+
if want > cap:
|
|
659
|
+
msg = f"最多 {cap} 个:并行数不能超过有效账号数,已按 {cap} 执行"
|
|
660
|
+
return {"ok": True, "workers": target, "max": cap, "requested": want,
|
|
661
|
+
"prev": old, "clamped": want > cap, "message": msg}
|
|
662
|
+
|
|
663
|
+
|
|
549
664
|
# ---------- 任务落盘 / 恢复 ----------
|
|
550
665
|
def _persist(self) -> None:
|
|
551
666
|
"""把「还没结束」的任务写进磁盘。写完原子替换,进程被杀也不会写出半个文件。"""
|
|
@@ -868,7 +983,7 @@ class PoolManager:
|
|
|
868
983
|
|
|
869
984
|
账号状态只在这时候才会变,监控和后台同步据此决定刷多快。
|
|
870
985
|
"""
|
|
871
|
-
return bool(self.running) or self.queue.qsize() > 0
|
|
986
|
+
return bool(self.running) or (self.queue.qsize() - self._retire_pending) > 0
|
|
872
987
|
|
|
873
988
|
async def refresh_accounts_throttled(self, busy_interval: float = 3.0,
|
|
874
989
|
idle_interval: float = 20.0) -> None:
|
|
@@ -1003,8 +1118,12 @@ class PoolManager:
|
|
|
1003
1118
|
"busy": len(self.running),
|
|
1004
1119
|
# 「现在有活干」:正在出图或还有排队。别和 pool_stat 里的 active(可用账号数)搞混
|
|
1005
1120
|
"running_now": self.is_busy(),
|
|
1006
|
-
"workers":
|
|
1007
|
-
"
|
|
1121
|
+
"workers": self.workers,
|
|
1122
|
+
"workers_max": self.workers_cap(),
|
|
1123
|
+
"workers_default": WORKERS_DEFAULT,
|
|
1124
|
+
"valid_accounts": self.valid_accounts(),
|
|
1125
|
+
# 队列里混着「下班令牌」,排队数只算真任务
|
|
1126
|
+
"queued": max(0, self.queue.qsize() - self._retire_pending),
|
|
1008
1127
|
"running": running,
|
|
1009
1128
|
"current": current,
|
|
1010
1129
|
"rr_index": self._rr_index,
|
|
@@ -1034,11 +1153,17 @@ class PoolManager:
|
|
|
1034
1153
|
'''
|
|
1035
1154
|
stopped: list[str] = []
|
|
1036
1155
|
dropped = 0
|
|
1156
|
+
retire_lost = 0
|
|
1037
1157
|
while True:
|
|
1038
1158
|
try:
|
|
1039
1159
|
jid = self.queue.get_nowait()
|
|
1040
1160
|
except asyncio.QueueEmpty:
|
|
1041
1161
|
break
|
|
1162
|
+
if jid == RETIRE_TOKEN:
|
|
1163
|
+
# 下班令牌不是任务,别当洪水清掉:并行数下调的意图还得兑现
|
|
1164
|
+
retire_lost += 1
|
|
1165
|
+
self.queue.task_done()
|
|
1166
|
+
continue
|
|
1042
1167
|
job = self.jobs.get(jid)
|
|
1043
1168
|
if job and job.get("status") in ("queued", "running"):
|
|
1044
1169
|
job.update({"status": "cancelled", "error": reason,
|
|
@@ -1046,6 +1171,8 @@ class PoolManager:
|
|
|
1046
1171
|
stopped.append(jid)
|
|
1047
1172
|
dropped += 1
|
|
1048
1173
|
self.queue.task_done()
|
|
1174
|
+
for _ in range(retire_lost):
|
|
1175
|
+
await self.queue.put(RETIRE_TOKEN)
|
|
1049
1176
|
# 先改状态再 cancel:_run_job 的 finally 才不会写历史或安排重试
|
|
1050
1177
|
running = []
|
|
1051
1178
|
for jid, task in list(self._job_tasks.items()):
|
|
@@ -1083,6 +1210,11 @@ class PoolManager:
|
|
|
1083
1210
|
while True:
|
|
1084
1211
|
job_id = await self.queue.get()
|
|
1085
1212
|
try:
|
|
1213
|
+
if job_id == RETIRE_TOKEN:
|
|
1214
|
+
# 并行上限被下调:把手上的活干完再走,不打断在途请求
|
|
1215
|
+
self._retire_pending = max(0, self._retire_pending - 1)
|
|
1216
|
+
log(f"worker {wid}: 退出(并行数下调)")
|
|
1217
|
+
return
|
|
1086
1218
|
job = self.jobs.get(job_id)
|
|
1087
1219
|
if not job or job.get("status") == "cancelled":
|
|
1088
1220
|
continue
|
package/server/static/index.html
CHANGED
|
@@ -457,6 +457,24 @@
|
|
|
457
457
|
.mon-kpi span{display:block;font-size:10.5px;color:var(--ink-3)}
|
|
458
458
|
.mon-kpi b{font-size:17px;letter-spacing:.4px}
|
|
459
459
|
.mon-kpi b.ok{color:var(--ok)} .mon-kpi b.warn{color:var(--warn)} .mon-kpi b.err{color:var(--err)}
|
|
460
|
+
/* 并行上限控制条:夹在 KPI 和账号表之间,一行放下,窄屏自动换行 */
|
|
461
|
+
.mon-wbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
|
|
462
|
+
border:1px solid var(--line);border-radius:11px;padding:6px 10px;background:#fbfaff;margin:0 0 2px}
|
|
463
|
+
.mon-wbar .wlbl{font-size:11px;color:var(--ink-3)}
|
|
464
|
+
.mon-wbar .wctl{display:flex;align-items:center;gap:1px;border:1px solid var(--line);
|
|
465
|
+
background:#fff;border-radius:9px;padding:2px}
|
|
466
|
+
.mon-wbar .wbtn{width:22px;height:22px;border:0;background:transparent;color:var(--ink-2);
|
|
467
|
+
font-size:14px;line-height:1;border-radius:7px;cursor:pointer;padding:0}
|
|
468
|
+
.mon-wbar .wbtn:hover:not(:disabled){background:var(--chip);color:var(--primary)}
|
|
469
|
+
.mon-wbar .wbtn:disabled{opacity:.35;cursor:not-allowed}
|
|
470
|
+
.mon-wbar #monW{width:42px;height:22px;border:0;background:transparent;text-align:center;
|
|
471
|
+
font:600 13px/1 inherit;color:var(--primary);outline:none;padding:0}
|
|
472
|
+
.mon-wbar #monW::-webkit-outer-spin-button,.mon-wbar #monW::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
|
473
|
+
.mon-wbar #monW:focus{background:var(--chip);border-radius:7px}
|
|
474
|
+
.mon-wbar .whint{font-size:10.5px;color:var(--ink-3);line-height:1.5}
|
|
475
|
+
.mon-wbar .whint.ok{color:var(--ok)}
|
|
476
|
+
.mon-wbar .whint.err{color:var(--err)}
|
|
477
|
+
.mon-wbar .whint.warn{color:var(--warn)}
|
|
460
478
|
.mon-sec{display:flex;align-items:center;gap:8px;margin:12px 0 6px;font-size:12px}
|
|
461
479
|
.mon-sec span{font-size:11px;color:var(--ink-3)}
|
|
462
480
|
.mon-sec .spacer{flex:1}
|
|
@@ -985,6 +1003,16 @@
|
|
|
985
1003
|
<button class="btn ghost mini" id="monClose" type="button">关闭</button>
|
|
986
1004
|
</div>
|
|
987
1005
|
<div class="mon-sum" id="monSum"></div>
|
|
1006
|
+
<!-- 并行上限:默认 3,可自行加减。上限跟着有效账号数走(在出图的号也算),超了自动压回来 -->
|
|
1007
|
+
<div class="mon-wbar">
|
|
1008
|
+
<span class="wlbl">并行上限</span>
|
|
1009
|
+
<div class="wctl">
|
|
1010
|
+
<button type="button" class="wbtn" id="monWDown" title="减 1" aria-label="并行上限减 1">−</button>
|
|
1011
|
+
<input id="monW" type="number" min="1" step="1" inputmode="numeric" aria-label="并行上限">
|
|
1012
|
+
<button type="button" class="wbtn" id="monWUp" title="加 1" aria-label="并行上限加 1">+</button>
|
|
1013
|
+
</div>
|
|
1014
|
+
<span class="whint" id="monWHint">默认 3,最多不超过有效账号数</span>
|
|
1015
|
+
</div>
|
|
988
1016
|
<div class="mon-sec"><b>账号状态</b><span id="monAccHint"></span><span class="spacer"></span><span id="monCheckHint"></span></div>
|
|
989
1017
|
<div class="mon-box">
|
|
990
1018
|
<table class="mon">
|
|
@@ -3916,8 +3944,92 @@ async function stopAllJobs(){
|
|
|
3916
3944
|
const ST_LABEL = {ready:'可用', busy:'出图中', limited:'限流', unhealthy:'已下线', unbound:'未绑定', pending:'待验证', error:'异常'};
|
|
3917
3945
|
let monTimer = null;
|
|
3918
3946
|
|
|
3919
|
-
|
|
3920
|
-
|
|
3947
|
+
/* ---------- 并行上限:默认 3,用户可调,封顶 = 有效账号数(在出图的号也算有效) ---------- */
|
|
3948
|
+
let _wBusy = false, _wLast = null, _wHintTimer = null;
|
|
3949
|
+
|
|
3950
|
+
function syncWorkersCtl(d){
|
|
3951
|
+
_wLast = d;
|
|
3952
|
+
const inp = $('#monW'); if (!inp) return;
|
|
3953
|
+
const cap = Math.max(1, Number(d.workers_max || d.workers || 3));
|
|
3954
|
+
const cur = Math.max(1, Number(d.workers || 3));
|
|
3955
|
+
const valid = Number(d.valid_accounts || 0);
|
|
3956
|
+
inp.max = cap;
|
|
3957
|
+
// 正在输入或正在提交时不覆盖,免得把用户打到一半的数字顶掉
|
|
3958
|
+
if (document.activeElement !== inp && !_wBusy){
|
|
3959
|
+
inp.value = cur;
|
|
3960
|
+
inp.dataset.saved = cur;
|
|
3961
|
+
}
|
|
3962
|
+
$('#monWDown').disabled = cur <= 1;
|
|
3963
|
+
$('#monWUp').disabled = cur >= cap;
|
|
3964
|
+
if (_wHintTimer) return; // 刚保存过,提示文字留给它自己过期
|
|
3965
|
+
const hint = $('#monWHint');
|
|
3966
|
+
const over = cur > cap;
|
|
3967
|
+
hint.className = 'whint' + (over ? ' warn' : '');
|
|
3968
|
+
hint.textContent = over
|
|
3969
|
+
? `当前 ${cur} 个,已超过有效账号 ${cap} 个,建议调低`
|
|
3970
|
+
: `默认 3,最多 ${cap} 个${valid ? `(有效账号 ${valid} 个)` : ''}`;
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
function flashWorkersHint(ms){
|
|
3974
|
+
clearTimeout(_wHintTimer);
|
|
3975
|
+
_wHintTimer = setTimeout(()=>{
|
|
3976
|
+
_wHintTimer = null;
|
|
3977
|
+
if (_wLast) syncWorkersCtl(_wLast);
|
|
3978
|
+
}, ms);
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
async function saveWorkers(v){
|
|
3982
|
+
const inp = $('#monW'), hint = $('#monWHint');
|
|
3983
|
+
const n = Math.round(Number(v));
|
|
3984
|
+
if (!Number.isFinite(n) || n < 1){
|
|
3985
|
+
hint.className = 'whint err'; hint.textContent = '并行数至少 1';
|
|
3986
|
+
flashWorkersHint(2600); return;
|
|
3987
|
+
}
|
|
3988
|
+
if (_wBusy) return;
|
|
3989
|
+
_wBusy = true;
|
|
3990
|
+
hint.className = 'whint'; hint.textContent = '保存中…';
|
|
3991
|
+
try{
|
|
3992
|
+
const r = await fetch('/api/pool/workers', {method:'POST',
|
|
3993
|
+
headers:{'Content-Type':'application/json'}, body: JSON.stringify({workers:n})});
|
|
3994
|
+
const d = await r.json();
|
|
3995
|
+
if (!r.ok) throw new Error(d.detail || ('HTTP ' + r.status));
|
|
3996
|
+
inp.value = d.workers;
|
|
3997
|
+
inp.dataset.saved = d.workers;
|
|
3998
|
+
hint.className = 'whint ' + (d.clamped ? 'warn' : 'ok');
|
|
3999
|
+
hint.textContent = d.message || ('并行上限已设为 ' + d.workers);
|
|
4000
|
+
flashWorkersHint(4200);
|
|
4001
|
+
}catch(e){
|
|
4002
|
+
hint.className = 'whint err';
|
|
4003
|
+
hint.textContent = '没存上:' + ((e && e.message) || e);
|
|
4004
|
+
flashWorkersHint(5200);
|
|
4005
|
+
}finally{
|
|
4006
|
+
_wBusy = false;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
function bindWorkersCtl(){
|
|
4011
|
+
const inp = $('#monW'); if (!inp || inp._bound) return;
|
|
4012
|
+
inp._bound = 1;
|
|
4013
|
+
const step = (delta) => {
|
|
4014
|
+
const v = Math.round(Number(inp.value) || 0) + delta;
|
|
4015
|
+
inp.value = v;
|
|
4016
|
+
saveWorkers(v);
|
|
4017
|
+
};
|
|
4018
|
+
$('#monWUp').onclick = () => step(1);
|
|
4019
|
+
$('#monWDown').onclick = () => step(-1);
|
|
4020
|
+
inp.addEventListener('change', () => {
|
|
4021
|
+
// 回车已经存过一遍,失焦时别再发一次
|
|
4022
|
+
if (Number(inp.value) === Number(inp.dataset.saved || 0)) return;
|
|
4023
|
+
saveWorkers(inp.value);
|
|
4024
|
+
});
|
|
4025
|
+
inp.addEventListener('keydown', (e) => {
|
|
4026
|
+
if (e.key === 'Enter'){ e.preventDefault(); saveWorkers(inp.value); inp.blur(); }
|
|
4027
|
+
if (e.key === 'Escape'){ e.preventDefault(); inp.blur(); }
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
function kpi(label, val, cls, tip){
|
|
4032
|
+
return `<div class="mon-kpi"${tip ? ` title="${tip}"` : ''}><span>${label}</span><b class="${cls || ''}">${val}</b></div>`;
|
|
3921
4033
|
}
|
|
3922
4034
|
|
|
3923
4035
|
function agoText(ts){
|
|
@@ -3945,11 +4057,13 @@ function renderMonitor(d){
|
|
|
3945
4057
|
const kTotal = accs.length ? accs.filter(a=>a.env_id).length : Number(poolS.total || 0);
|
|
3946
4058
|
$('#monSum').innerHTML =
|
|
3947
4059
|
kpi('账号可用', `${kReady}/${kTotal}`, kReady ? 'ok' : 'err')
|
|
3948
|
-
+ kpi('并行任务', `${run.length}/${d.workers || 1}`, run.length ? 'warn' : ''
|
|
4060
|
+
+ kpi('并行任务', `${run.length}/${d.workers || 1}`, run.length ? 'warn' : '',
|
|
4061
|
+
'并行上限可调:改下面那条「并行上限」的 − / + 或直接填数字,最多不超过有效账号数')
|
|
3949
4062
|
+ kpi('排队', d.queued || 0, d.queued ? 'warn' : '')
|
|
3950
4063
|
+ kpi('今日成功', hist.today_done || 0, 'ok')
|
|
3951
4064
|
+ kpi('今日失败', hist.today_error || 0, hist.today_error ? 'err' : '')
|
|
3952
4065
|
+ kpi('历史总量', hist.total || 0, '');
|
|
4066
|
+
syncWorkersCtl(d);
|
|
3953
4067
|
|
|
3954
4068
|
let lastUsedAcc = null;
|
|
3955
4069
|
for (const a of accs){
|
|
@@ -4073,6 +4187,7 @@ async function refreshMonitor(){
|
|
|
4073
4187
|
}
|
|
4074
4188
|
|
|
4075
4189
|
async function openMonitor(){
|
|
4190
|
+
bindWorkersCtl();
|
|
4076
4191
|
$('#monitorDlg').showModal();
|
|
4077
4192
|
await refreshMonitor();
|
|
4078
4193
|
scheduleMonitor();
|