dsh-comfyui-gateway 0.4.3 → 0.4.7
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/README.md +77 -9
- package/client.js +265 -105
- package/lib/config.js +2 -2
- package/lib/gateway.js +7 -1
- package/lib/instances.js +62 -38
- package/lib/library.js +74 -2
- package/lib/pages.js +172 -14
- package/lib/remote.js +30 -1
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -51,12 +51,14 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
restart: '重启',
|
|
52
52
|
remove: '解绑',
|
|
53
53
|
controlNone: '不受网关托管(只能派发,不能启停)',
|
|
54
|
-
controlSpawn: '
|
|
55
|
-
controlSsh: '远程托管:通过 ssh
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
controlSpawn: '本机托管:网关启动/停止这个 bat 文件',
|
|
55
|
+
controlSsh: '远程托管:通过 ssh 启停(已停用)',
|
|
56
|
+
batPath: 'bat 文件',
|
|
57
|
+
batHint: '填 .bat 的完整路径,网关启动时执行它;停止时连同它的子进程一起杀掉。',
|
|
58
|
+
batPlaceholder: 'E:\\ComfyUI\\启动.bat',
|
|
59
|
+
batMissing: '填一个存在的 .bat 文件路径',
|
|
60
|
+
batPicked: '已选文件',
|
|
61
|
+
cwd: '工作目录(可空,默认 bat 所在目录)',
|
|
60
62
|
apply: '应用',
|
|
61
63
|
probe: '刷新状态',
|
|
62
64
|
healthy: '在线',
|
|
@@ -95,8 +97,8 @@ window.__ModuleLoader__.load({
|
|
|
95
97
|
advancedHint: '填了启动命令,面板上的“启动/停止”才会真正作用于本机进程。',
|
|
96
98
|
controlType: '托管方式',
|
|
97
99
|
noneOpt: '不托管',
|
|
98
|
-
spawnOpt: '
|
|
99
|
-
sshOpt: 'SSH
|
|
100
|
+
spawnOpt: '启动 bat 文件',
|
|
101
|
+
sshOpt: 'SSH 远程(旧配置)',
|
|
100
102
|
applyFirst: '先保存实例修改',
|
|
101
103
|
changed: '有未保存的修改',
|
|
102
104
|
refresh: '自动刷新 2s',
|
|
@@ -117,6 +119,13 @@ window.__ModuleLoader__.load({
|
|
|
117
119
|
noSessions: '当前无人登录',
|
|
118
120
|
revokeAll: '踢掉全部会话',
|
|
119
121
|
remotePort: '远程端口',
|
|
122
|
+
remotePortHint: '只服务浏览器;改完点“重启网关”生效。',
|
|
123
|
+
instancePort: '端口',
|
|
124
|
+
editPort: '改端口',
|
|
125
|
+
savePort: '保存端口',
|
|
126
|
+
portRange: '端口 1–65535',
|
|
127
|
+
portSame: '两个端口不能相同',
|
|
128
|
+
baseUrlHint: 'http://主机:端口,不带尾斜杠',
|
|
120
129
|
gatewayPort: '网关端口',
|
|
121
130
|
sectionRemote: '远程控制',
|
|
122
131
|
sectionRemoteHint: '另一台电脑/手机访问的页面:账号、等级、配对码。',
|
|
@@ -184,12 +193,14 @@ window.__ModuleLoader__.load({
|
|
|
184
193
|
restart: 'Restart',
|
|
185
194
|
remove: 'Unbind',
|
|
186
195
|
controlNone: 'Not owned (dispatch only)',
|
|
187
|
-
controlSpawn: 'Host-owned: the gateway starts and stops
|
|
188
|
-
controlSsh: 'Remote-owned over ssh',
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
controlSpawn: 'Host-owned: the gateway starts and stops this bat file',
|
|
197
|
+
controlSsh: 'Remote-owned over ssh (retired)',
|
|
198
|
+
batPath: 'Bat file',
|
|
199
|
+
batHint: 'Full path to a .bat file. The gateway runs it on start and kills its whole tree on stop.',
|
|
200
|
+
batPlaceholder: 'E:\\ComfyUI\\start.bat',
|
|
201
|
+
batMissing: 'Point at an existing .bat file',
|
|
202
|
+
batPicked: 'Picked',
|
|
203
|
+
cwd: 'Working directory (optional, defaults to the bat folder)',
|
|
193
204
|
apply: 'Apply',
|
|
194
205
|
probe: 'Refresh',
|
|
195
206
|
healthy: 'online',
|
|
@@ -229,7 +240,7 @@ window.__ModuleLoader__.load({
|
|
|
229
240
|
controlType: 'Control',
|
|
230
241
|
noneOpt: 'None',
|
|
231
242
|
spawnOpt: 'Local command',
|
|
232
|
-
sshOpt: 'SSH remote',
|
|
243
|
+
sshOpt: 'SSH remote (legacy)',
|
|
233
244
|
applyFirst: 'Save instance changes first',
|
|
234
245
|
changed: 'unsaved changes',
|
|
235
246
|
refresh: 'auto refresh 2s',
|
|
@@ -250,6 +261,13 @@ window.__ModuleLoader__.load({
|
|
|
250
261
|
noSessions: 'Nobody is logged in',
|
|
251
262
|
revokeAll: 'Revoke all sessions',
|
|
252
263
|
remotePort: 'Remote port',
|
|
264
|
+
remotePortHint: 'Serves browsers only; press Restart gateway to apply.',
|
|
265
|
+
instancePort: 'Port',
|
|
266
|
+
editPort: 'Edit port',
|
|
267
|
+
savePort: 'Save port',
|
|
268
|
+
portRange: 'Port 1–65535',
|
|
269
|
+
portSame: 'The two ports must differ',
|
|
270
|
+
baseUrlHint: 'http://host:port, no trailing slash',
|
|
253
271
|
gatewayPort: 'Gateway port',
|
|
254
272
|
sectionRemote: 'Remote console',
|
|
255
273
|
sectionRemoteHint: 'The page other machines open: accounts, tiers, pairing code.',
|
|
@@ -376,6 +394,47 @@ window.__ModuleLoader__.load({
|
|
|
376
394
|
return data;
|
|
377
395
|
}
|
|
378
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Status rows and writable rows are not the same shape, and mixing them up is
|
|
399
|
+
* what made the 托管方式 dropdown snap back to 不托管:
|
|
400
|
+
*
|
|
401
|
+
* status row: { control: 'spawn', controlDetail: { type:'spawn', command, cwd } }
|
|
402
|
+
* writable: { control: { type:'spawn', command, cwd } }
|
|
403
|
+
*
|
|
404
|
+
* The card seeds its draft from the status row, so `draft.control` is the
|
|
405
|
+
* *string* "spawn" and `draft.control.type` is undefined. The select then
|
|
406
|
+
* reads undefined → falls back to 'none', and writing `{...draft.control}`
|
|
407
|
+
* spreads a string, producing `{ type: 'ssh' }` with no command at all. After
|
|
408
|
+
* the next 2 s poll the entry is unchanged on the server, the draft is
|
|
409
|
+
* reseeded from the status row, and the choice visibly jumps back.
|
|
410
|
+
*
|
|
411
|
+
* One converter, used by the card and by every full-list write, so there is
|
|
412
|
+
* exactly one place that knows the difference.
|
|
413
|
+
*/
|
|
414
|
+
function writableInstance(entry) {
|
|
415
|
+
const detail = entry.controlDetail;
|
|
416
|
+
const control =
|
|
417
|
+
detail !== undefined && detail !== null && typeof detail === 'object'
|
|
418
|
+
? { ...detail }
|
|
419
|
+
: { type: typeof entry.control === 'string' && entry.control !== '' ? entry.control : 'none' };
|
|
420
|
+
// A status row always reports a detail object; keep the bat path even when
|
|
421
|
+
// the type is 'none' so switching back and forth does not lose it.
|
|
422
|
+
return {
|
|
423
|
+
id: entry.id,
|
|
424
|
+
label: entry.label,
|
|
425
|
+
baseUrl: entry.baseUrl,
|
|
426
|
+
weight: entry.weight ?? 1,
|
|
427
|
+
maxConcurrent: entry.maxConcurrent ?? 1,
|
|
428
|
+
enabled: entry.enabled !== false,
|
|
429
|
+
autoStart: entry.autoStart === true,
|
|
430
|
+
control: {
|
|
431
|
+
type: control.type === 'script' ? 'script' : 'none',
|
|
432
|
+
batPath: typeof control.batPath === 'string' ? control.batPath : '',
|
|
433
|
+
cwd: typeof control.cwd === 'string' ? control.cwd : '',
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
379
438
|
const STATE_LABEL = {
|
|
380
439
|
queued: 'queued',
|
|
381
440
|
running: 'running',
|
|
@@ -411,11 +470,33 @@ window.__ModuleLoader__.load({
|
|
|
411
470
|
/** One instance card: identity, live numbers, and its process controls. */
|
|
412
471
|
function InstanceCard({ entry, t, onAction, onPatch, onRemove, busy }) {
|
|
413
472
|
const [open, setOpen] = useState(false);
|
|
414
|
-
|
|
415
|
-
|
|
473
|
+
// Seed from the *writable* shape, never from the status row: see
|
|
474
|
+
// writableInstance() for why spreading a status row loses the choice.
|
|
475
|
+
const [draft, setDraft] = useState(() => writableInstance(entry));
|
|
476
|
+
// The URL is the one field a person actually needs to fix from the card
|
|
477
|
+
// head (a port moved, a host changed), so it gets its own inline editor
|
|
478
|
+
// with its own save. Everything else lives behind "advanced".
|
|
479
|
+
const [editingUrl, setEditingUrl] = useState(false);
|
|
480
|
+
const [urlDraft, setUrlDraft] = useState(String(entry.baseUrl ?? ''));
|
|
481
|
+
const [urlError, setUrlError] = useState(null);
|
|
482
|
+
/**
|
|
483
|
+
* Reseed only when the *saved* configuration actually changed.
|
|
484
|
+
*
|
|
485
|
+
* Depending on `entry` reseeds on every poll: the status payload is rebuilt
|
|
486
|
+
* every 2 s, so `entry` is a new object each tick, the effect fires, and the
|
|
487
|
+
* draft is thrown away — the dropdown visibly snaps back to 不托管 even
|
|
488
|
+
* though nobody saved anything. The signature below changes only when the
|
|
489
|
+
* server really has different values (after Apply, or an external edit).
|
|
490
|
+
*/
|
|
491
|
+
const savedSignature = JSON.stringify(writableInstance(entry));
|
|
492
|
+
useEffect(() => {
|
|
493
|
+
setDraft(JSON.parse(savedSignature));
|
|
494
|
+
}, [savedSignature]);
|
|
495
|
+
// The URL field is seeded when the editor opens, not on every status
|
|
496
|
+
// poll: polling would overwrite what the person is typing.
|
|
416
497
|
const dirty = useMemo(
|
|
417
|
-
() => JSON.stringify(draft) !==
|
|
418
|
-
[draft,
|
|
498
|
+
() => JSON.stringify(draft) !== savedSignature,
|
|
499
|
+
[draft, savedSignature],
|
|
419
500
|
);
|
|
420
501
|
const device = (entry.devices ?? [])[0];
|
|
421
502
|
const vramText =
|
|
@@ -423,6 +504,42 @@ window.__ModuleLoader__.load({
|
|
|
423
504
|
? `${Math.round((device.vramTotal - (device.vramFree ?? 0)) / 1048576)} / ${Math.round(device.vramTotal / 1048576)} MiB`
|
|
424
505
|
: '—';
|
|
425
506
|
const owned = entry.control !== 'none';
|
|
507
|
+
/**
|
|
508
|
+
* A bare port number is accepted as a shorthand: "8190" keeps the host
|
|
509
|
+
* the instance already has, which is what somebody changing a port means.
|
|
510
|
+
*/
|
|
511
|
+
const normalizeUrl = (raw) => {
|
|
512
|
+
const text = String(raw ?? '').trim();
|
|
513
|
+
if (text === '') return '';
|
|
514
|
+
if (/^\d+$/.test(text)) {
|
|
515
|
+
const host = /^[a-z]+:\/\/([^/:]+)/i.exec(String(entry.baseUrl ?? ''))?.[1];
|
|
516
|
+
return host === undefined ? '' : `http://${host}:${text}`;
|
|
517
|
+
}
|
|
518
|
+
const withScheme = /^https?:\/\//i.test(text) ? text : `http://${text}`;
|
|
519
|
+
try {
|
|
520
|
+
const url = new URL(withScheme);
|
|
521
|
+
const port = Number(url.port);
|
|
522
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) return '';
|
|
523
|
+
return `${url.protocol}//${url.host}`;
|
|
524
|
+
} catch {
|
|
525
|
+
return '';
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const saveUrl = () => {
|
|
529
|
+
const next = normalizeUrl(urlDraft);
|
|
530
|
+
if (next === '') {
|
|
531
|
+
setUrlError(t('portRange'));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
setUrlError(null);
|
|
535
|
+
setEditingUrl(false);
|
|
536
|
+
// Only identity + the field being edited travel: the status entry
|
|
537
|
+
// carries display-only keys (`control` is the string "spawn" there,
|
|
538
|
+
// while the writable shape wants `control.type`). Spreading the whole
|
|
539
|
+
// entry would send the display string back and wipe the process
|
|
540
|
+
// settings.
|
|
541
|
+
onPatch({ id: entry.id, baseUrl: next });
|
|
542
|
+
};
|
|
426
543
|
return h(
|
|
427
544
|
'div',
|
|
428
545
|
{ className: 'cgw-card' },
|
|
@@ -431,13 +548,51 @@ window.__ModuleLoader__.load({
|
|
|
431
548
|
{ className: 'cgw-card-head' },
|
|
432
549
|
h('span', { className: `cgw-dot cgw-dot--${entry.healthy ? 'ok' : entry.starting ? 'warn' : 'bad'}` }),
|
|
433
550
|
h('span', { className: 'cgw-section-title' }, entry.label || entry.id),
|
|
434
|
-
|
|
551
|
+
editingUrl
|
|
552
|
+
? h(
|
|
553
|
+
'span',
|
|
554
|
+
{ className: 'cgw-row' },
|
|
555
|
+
h('input', {
|
|
556
|
+
className: 'cgw-input cgw-mono',
|
|
557
|
+
style: { width: 190 },
|
|
558
|
+
placeholder: t('baseUrlHint'),
|
|
559
|
+
value: urlDraft,
|
|
560
|
+
onChange: (event) => setUrlDraft(event.target.value),
|
|
561
|
+
}),
|
|
562
|
+
h(
|
|
563
|
+
'button',
|
|
564
|
+
{ className: 'cgw-btn cgw-btn--primary', disabled: busy, onClick: () => saveUrl() },
|
|
565
|
+
t('savePort'),
|
|
566
|
+
),
|
|
567
|
+
h(
|
|
568
|
+
'button',
|
|
569
|
+
{ className: 'cgw-btn cgw-btn--ghost', disabled: busy, onClick: () => setEditingUrl(false) },
|
|
570
|
+
'×',
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
: h('span', { className: 'cgw-meta cgw-mono' }, entry.baseUrl),
|
|
435
574
|
h('span', { className: 'cgw-grow' }),
|
|
436
575
|
entry.starting ? h('span', { className: 'cgw-meta' }, t('starting')) : null,
|
|
437
576
|
entry.stopping ? h('span', { className: 'cgw-meta' }, t('stopping')) : null,
|
|
577
|
+
h(
|
|
578
|
+
'button',
|
|
579
|
+
{
|
|
580
|
+
className: 'cgw-btn cgw-btn--ghost',
|
|
581
|
+
disabled: busy || editingUrl,
|
|
582
|
+
title: t('baseUrlHint'),
|
|
583
|
+
onClick: () => {
|
|
584
|
+
setUrlDraft(String(entry.baseUrl ?? ''));
|
|
585
|
+
setUrlError(null);
|
|
586
|
+
setEditingUrl(true);
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
t('editPort'),
|
|
590
|
+
),
|
|
438
591
|
h('span', { className: 'cgw-badge' }, entry.healthy ? t('healthy') : t('unhealthy')),
|
|
439
592
|
entry.pid ? h('span', { className: 'cgw-badge cgw-mono' }, `pid ${entry.pid}`) : null,
|
|
440
593
|
),
|
|
594
|
+
urlError === null ? null : h('div', { className: 'cgw-error' }, urlError),
|
|
595
|
+
|
|
441
596
|
h(
|
|
442
597
|
'div',
|
|
443
598
|
{ className: 'cgw-kv' },
|
|
@@ -534,8 +689,10 @@ window.__ModuleLoader__.load({
|
|
|
534
689
|
setDraft({ ...draft, control: { ...(draft.control ?? {}), type: event.target.value } }),
|
|
535
690
|
},
|
|
536
691
|
h('option', { value: 'none' }, t('noneOpt')),
|
|
537
|
-
h('option', { value: '
|
|
538
|
-
|
|
692
|
+
h('option', { value: 'script' }, t('spawnOpt')),
|
|
693
|
+
// Legacy rows that still say 'ssh' keep an option so the
|
|
694
|
+
// select does not show a blank line; saving normalizes it.
|
|
695
|
+
draft.control?.type === 'ssh' ? h('option', { value: 'ssh' }, t('sshOpt')) : null,
|
|
539
696
|
),
|
|
540
697
|
),
|
|
541
698
|
(draft.control?.type ?? 'none') === 'none'
|
|
@@ -543,31 +700,31 @@ window.__ModuleLoader__.load({
|
|
|
543
700
|
: h(
|
|
544
701
|
'div',
|
|
545
702
|
{ style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
703
|
+
h(
|
|
704
|
+
'div',
|
|
705
|
+
{ className: 'cgw-row' },
|
|
706
|
+
h('span', { className: 'cgw-meta' }, t('batPath')),
|
|
707
|
+
h('input', {
|
|
708
|
+
className: 'cgw-input cgw-input--wide cgw-mono',
|
|
709
|
+
placeholder: t('batPlaceholder'),
|
|
710
|
+
value: draft.control?.batPath ?? '',
|
|
711
|
+
onChange: (event) =>
|
|
712
|
+
setDraft({ ...draft, control: { ...(draft.control ?? {}), batPath: event.target.value } }),
|
|
713
|
+
}),
|
|
714
|
+
),
|
|
715
|
+
h('div', { className: 'cgw-meta' }, t('batHint')),
|
|
716
|
+
h(
|
|
717
|
+
'div',
|
|
718
|
+
{ className: 'cgw-row' },
|
|
719
|
+
h('span', { className: 'cgw-meta' }, t('cwd')),
|
|
720
|
+
h('input', {
|
|
721
|
+
className: 'cgw-input cgw-input--wide cgw-mono',
|
|
722
|
+
placeholder: t('cwd'),
|
|
723
|
+
value: draft.control?.cwd ?? '',
|
|
724
|
+
onChange: (event) =>
|
|
725
|
+
setDraft({ ...draft, control: { ...(draft.control ?? {}), cwd: event.target.value } }),
|
|
726
|
+
}),
|
|
727
|
+
),
|
|
571
728
|
),
|
|
572
729
|
h(
|
|
573
730
|
'div',
|
|
@@ -594,7 +751,7 @@ window.__ModuleLoader__.load({
|
|
|
594
751
|
}
|
|
595
752
|
|
|
596
753
|
/** Remote-access control: the pairing code, accounts, sessions, leases. */
|
|
597
|
-
function RemoteCard({ t, status, busy, act }) {
|
|
754
|
+
function RemoteCard({ t, status, busy, act, portDraft, onPortDraft, onSavePort }) {
|
|
598
755
|
const [code, setCode] = useState(null);
|
|
599
756
|
const [name, setName] = useState('');
|
|
600
757
|
const [password, setPassword] = useState('');
|
|
@@ -718,6 +875,23 @@ window.__ModuleLoader__.load({
|
|
|
718
875
|
h('span', { className: 'cgw-meta' }, remote.listening ? t('remoteOn') : t('remoteOff')),
|
|
719
876
|
),
|
|
720
877
|
remote.enabled === true ? null : h('div', { className: 'cgw-meta' }, t('remoteOffHint')),
|
|
878
|
+
h(
|
|
879
|
+
'div',
|
|
880
|
+
{ className: 'cgw-row' },
|
|
881
|
+
h('span', { className: 'cgw-meta' }, t('remotePort')),
|
|
882
|
+
h('input', {
|
|
883
|
+
className: 'cgw-input cgw-input--num',
|
|
884
|
+
value: portDraft,
|
|
885
|
+
onChange: (event) => onPortDraft(String(event.target.value).replace(/[^0-9]/g, '')),
|
|
886
|
+
}),
|
|
887
|
+
h(
|
|
888
|
+
'button',
|
|
889
|
+
{ className: 'cgw-btn', disabled: busy, onClick: () => void onSavePort() },
|
|
890
|
+
t('save'),
|
|
891
|
+
),
|
|
892
|
+
h('span', { className: 'cgw-grow' }),
|
|
893
|
+
h('span', { className: 'cgw-meta' }, t('remotePortHint')),
|
|
894
|
+
),
|
|
721
895
|
h('div', { className: 'cgw-meta' }, t('requireCodeHint')),
|
|
722
896
|
h(
|
|
723
897
|
'div',
|
|
@@ -947,80 +1121,66 @@ window.__ModuleLoader__.load({
|
|
|
947
1121
|
|
|
948
1122
|
const instanceAction = (action, id) => act('/comfyui-gateway/instances', { action, id });
|
|
949
1123
|
|
|
1124
|
+
/** Every full-list write goes through the one shape converter. */
|
|
1125
|
+
const writableList = () => (status?.instances ?? []).map((entry) => writableInstance(entry));
|
|
1126
|
+
|
|
950
1127
|
/** Apply one instance patch: replace that row, keep the rest verbatim. */
|
|
951
1128
|
const patchInstance = (patch) => {
|
|
952
|
-
const
|
|
953
|
-
const raw = current.map((entry) => ({
|
|
954
|
-
id: entry.id,
|
|
955
|
-
label: entry.label,
|
|
956
|
-
baseUrl: entry.baseUrl,
|
|
957
|
-
weight: entry.weight ?? 1,
|
|
958
|
-
maxConcurrent: entry.maxConcurrent ?? 1,
|
|
959
|
-
enabled: entry.enabled !== false,
|
|
960
|
-
autoStart: entry.autoStart === true,
|
|
961
|
-
control: entry.controlDetail ?? { type: entry.control === 'none' ? 'none' : entry.control },
|
|
962
|
-
}));
|
|
1129
|
+
const raw = writableList();
|
|
963
1130
|
const index = raw.findIndex((entry) => entry.id === patch.id);
|
|
964
1131
|
if (index < 0) return;
|
|
965
|
-
|
|
1132
|
+
// Merge into the converted row, so a patch carrying only `{id, baseUrl}`
|
|
1133
|
+
// still sends the row's real control block instead of dropping it.
|
|
1134
|
+
raw[index] = { ...raw[index], ...patch, id: patch.id, control: patch.control ?? raw[index].control };
|
|
966
1135
|
return act('/comfyui-gateway/instances', { action: 'apply', instances: raw });
|
|
967
1136
|
};
|
|
968
1137
|
|
|
969
1138
|
const addInstance = async () => {
|
|
970
1139
|
const url = newUrl.trim();
|
|
971
1140
|
if (url === '') return;
|
|
972
|
-
const
|
|
973
|
-
const raw = current.map((entry) => ({
|
|
974
|
-
id: entry.id,
|
|
975
|
-
label: entry.label,
|
|
976
|
-
baseUrl: entry.baseUrl,
|
|
977
|
-
weight: entry.weight ?? 1,
|
|
978
|
-
maxConcurrent: entry.maxConcurrent ?? 1,
|
|
979
|
-
enabled: entry.enabled !== false,
|
|
980
|
-
autoStart: entry.autoStart === true,
|
|
981
|
-
control: entry.controlDetail ?? { type: entry.control === 'none' ? 'none' : entry.control },
|
|
982
|
-
}));
|
|
1141
|
+
const raw = writableList();
|
|
983
1142
|
raw.push({ baseUrl: url, enabled: true, maxConcurrent: 1, weight: 1, control: { type: 'none' } });
|
|
984
1143
|
const data = await act('/comfyui-gateway/instances', { action: 'apply', instances: raw });
|
|
985
1144
|
if (data?.ok !== false) setNewUrl('');
|
|
986
1145
|
};
|
|
987
1146
|
|
|
988
1147
|
const removeInstance = (id) => {
|
|
989
|
-
const raw = (
|
|
990
|
-
.filter((entry) => entry.id !== id)
|
|
991
|
-
.map((entry) => ({
|
|
992
|
-
id: entry.id,
|
|
993
|
-
label: entry.label,
|
|
994
|
-
baseUrl: entry.baseUrl,
|
|
995
|
-
weight: entry.weight ?? 1,
|
|
996
|
-
maxConcurrent: entry.maxConcurrent ?? 1,
|
|
997
|
-
enabled: entry.enabled !== false,
|
|
998
|
-
autoStart: entry.autoStart === true,
|
|
999
|
-
control: entry.controlDetail ?? { type: entry.control === 'none' ? 'none' : entry.control },
|
|
1000
|
-
}));
|
|
1148
|
+
const raw = writableList().filter((entry) => entry.id !== id);
|
|
1001
1149
|
return act('/comfyui-gateway/instances', { action: 'apply', instances: raw });
|
|
1002
1150
|
};
|
|
1003
1151
|
|
|
1004
1152
|
const toggleDynamic = (on) => act('/comfyui-gateway/pool', { action: 'dynamic', enabled: on });
|
|
1005
1153
|
|
|
1006
|
-
/**
|
|
1007
|
-
|
|
1008
|
-
|
|
1154
|
+
/**
|
|
1155
|
+
* Each port is saved from the card that owns it: the gateway port from
|
|
1156
|
+
* the gateway card, the remote port from the remote card. Splitting the
|
|
1157
|
+
* form is the whole point — a person editing the console port is looking
|
|
1158
|
+
* at console settings, not at the gateway listener. They still share one
|
|
1159
|
+
* validation because they must not collide.
|
|
1160
|
+
*/
|
|
1161
|
+
const commitPort = async (port) => {
|
|
1009
1162
|
const remotePort = Number(draftRemotePort);
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1163
|
+
const gatewayPort = Number(draftPort);
|
|
1164
|
+
const other = port === gatewayPort ? remotePort : gatewayPort;
|
|
1165
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
1166
|
+
setNotice({ kind: 'error', text: t('portRange') });
|
|
1167
|
+
return;
|
|
1015
1168
|
}
|
|
1016
|
-
if (port ===
|
|
1017
|
-
setNotice({ kind: 'error', text: '
|
|
1169
|
+
if (Number.isFinite(other) && port === other) {
|
|
1170
|
+
setNotice({ kind: 'error', text: t('portSame') });
|
|
1018
1171
|
return;
|
|
1019
1172
|
}
|
|
1020
|
-
|
|
1173
|
+
if (port === gatewayPort) {
|
|
1174
|
+
await act('/comfyui-gateway/settings', { listen: { port } });
|
|
1175
|
+
} else {
|
|
1176
|
+
await act('/comfyui-gateway/settings', { remote: { port } });
|
|
1177
|
+
}
|
|
1021
1178
|
setNotice({ kind: 'info', text: t('remoteRestart') });
|
|
1022
1179
|
};
|
|
1023
1180
|
|
|
1181
|
+
const saveGatewayPort = () => commitPort(Number(draftPort));
|
|
1182
|
+
const saveRemotePort = () => commitPort(Number(draftRemotePort));
|
|
1183
|
+
|
|
1024
1184
|
|
|
1025
1185
|
|
|
1026
1186
|
const restartGateway = async () => {
|
|
@@ -1091,7 +1251,15 @@ window.__ModuleLoader__.load({
|
|
|
1091
1251
|
h('span', { className: 'cgw-group-title' }, t('sectionRemote')),
|
|
1092
1252
|
h('span', { className: 'cgw-meta' }, t('sectionRemoteHint')),
|
|
1093
1253
|
),
|
|
1094
|
-
h(RemoteCard, {
|
|
1254
|
+
h(RemoteCard, {
|
|
1255
|
+
t,
|
|
1256
|
+
status,
|
|
1257
|
+
busy,
|
|
1258
|
+
act,
|
|
1259
|
+
portDraft: draftRemotePort,
|
|
1260
|
+
onPortDraft: setDraftRemotePort,
|
|
1261
|
+
onSavePort: saveRemotePort,
|
|
1262
|
+
}),
|
|
1095
1263
|
),
|
|
1096
1264
|
h(
|
|
1097
1265
|
'div',
|
|
@@ -1145,16 +1313,8 @@ window.__ModuleLoader__.load({
|
|
|
1145
1313
|
onChange: (event) => setDraftPort(event.target.value.replace(/[^0-9]/g, '')),
|
|
1146
1314
|
},
|
|
1147
1315
|
),
|
|
1148
|
-
h('
|
|
1149
|
-
h(
|
|
1150
|
-
'input',
|
|
1151
|
-
{
|
|
1152
|
-
className: 'cgw-input cgw-input--num',
|
|
1153
|
-
value: draftRemotePort,
|
|
1154
|
-
onChange: (event) => setDraftRemotePort(event.target.value.replace(/[^0-9]/g, '')),
|
|
1155
|
-
},
|
|
1156
|
-
),
|
|
1157
|
-
h('button', { className: 'cgw-btn', disabled: busy, onClick: () => void savePorts() }, t('save')),
|
|
1316
|
+
h('button', { className: 'cgw-btn', disabled: busy, onClick: () => void saveGatewayPort() }, t('save')),
|
|
1317
|
+
h('span', { className: 'cgw-grow' }),
|
|
1158
1318
|
h('code', { className: 'cgw-mono cgw-meta' }, String(status.settings?.listen?.host ?? '0.0.0.0')),
|
|
1159
1319
|
),
|
|
1160
1320
|
h(
|
package/lib/config.js
CHANGED
|
@@ -85,7 +85,7 @@ export function defaultSettings() {
|
|
|
85
85
|
maxConcurrent: 1,
|
|
86
86
|
enabled: true,
|
|
87
87
|
autoStart: false,
|
|
88
|
-
control: { type: 'none',
|
|
88
|
+
control: { type: 'none', batPath: '', cwd: '', stopTimeoutMs: 15000, readyTimeoutMs: 300000 },
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
id: 'p8189',
|
|
@@ -96,7 +96,7 @@ export function defaultSettings() {
|
|
|
96
96
|
maxConcurrent: 1,
|
|
97
97
|
enabled: true,
|
|
98
98
|
autoStart: false,
|
|
99
|
-
control: { type: 'none',
|
|
99
|
+
control: { type: 'none', batPath: '', cwd: '', stopTimeoutMs: 15000, readyTimeoutMs: 300000 },
|
|
100
100
|
},
|
|
101
101
|
],
|
|
102
102
|
};
|
package/lib/gateway.js
CHANGED
|
@@ -483,7 +483,13 @@ export class GatewayServer {
|
|
|
483
483
|
maxConcurrent: runtime.instance.maxConcurrent,
|
|
484
484
|
weight: runtime.instance.weight,
|
|
485
485
|
control: runtime.instance.control.type,
|
|
486
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* The full control block, so the panel can edit batPath/cwd.
|
|
488
|
+
*
|
|
489
|
+
* `control` above is the *type string* — the panel must never spread it
|
|
490
|
+
* into an object (that is what made the 托管方式 dropdown snap back to
|
|
491
|
+
* 不托管: `{...'spawn'}` yields `{}` with no type). Read this one instead.
|
|
492
|
+
*/
|
|
487
493
|
controlDetail: runtime.instance.control,
|
|
488
494
|
pid: runtime.pid,
|
|
489
495
|
starting: runtime.starting,
|