dsh-local-sd 0.0.0-stage → 0.4.1
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/LICENSE +21 -0
- package/README.md +304 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +722 -0
- package/lib/index.js +1936 -0
- package/package.json +61 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// dsh-local-sd — browser half (client plugin bundle).
|
|
2
|
+
//
|
|
3
|
+
// Loaded by dsh-client-modules at /plugins/dsh-local-sd/client.js and executed
|
|
4
|
+
// through the vendored cordis Loader's lazy-CJS module table
|
|
5
|
+
// (window.__ModuleLoader__.load). The factory body is plain CJS with require()
|
|
6
|
+
// resolved against the shell's module table. The `id` below MUST equal the
|
|
7
|
+
// package name, or the shell reports "loaded without registering".
|
|
8
|
+
//
|
|
9
|
+
// This bundle carries one thing: 设置 → 本地绘图 (a `settings.section` page).
|
|
10
|
+
// It exists because the whole point of the plugin is "install it and it works",
|
|
11
|
+
// and that means the Forge directory must be pickable with a button instead of
|
|
12
|
+
// hand-editing $DSH_HOME/local-sd.json.
|
|
13
|
+
//
|
|
14
|
+
// All state lives in the host half. The page is a thin form over
|
|
15
|
+
// POST /local-sd/api — { method: "get" | "save" | "detect" | "browse" |
|
|
16
|
+
// "start" | "stop" } — so the JSON file stays the single source of truth and a
|
|
17
|
+
// hand edit still takes effect.
|
|
18
|
+
|
|
19
|
+
window.__ModuleLoader__.load({
|
|
20
|
+
id: "dsh-local-sd",
|
|
21
|
+
factory: (require) => {
|
|
22
|
+
var module = { exports: {} };
|
|
23
|
+
var exports = module.exports;
|
|
24
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
var React = require("react");
|
|
26
|
+
var h = React.createElement;
|
|
27
|
+
|
|
28
|
+
exports.inject = ["slots"];
|
|
29
|
+
exports.apply = function apply(ctx) {
|
|
30
|
+
var API_URL = "/local-sd/api";
|
|
31
|
+
// Text used to locate the settings-nav cell when painting the plugin
|
|
32
|
+
// glyph: the shell's navIcon map is keyed by section id and offers no
|
|
33
|
+
// icon field to register.
|
|
34
|
+
var NAV_LABEL = "本地绘图";
|
|
35
|
+
|
|
36
|
+
/* ── stylesheet ─────────────────────────────────────────────── */
|
|
37
|
+
ctx.effect(function() {
|
|
38
|
+
var style = document.createElement("style");
|
|
39
|
+
style.textContent = `
|
|
40
|
+
.lsd-root{display:flex;flex-direction:column;gap:16px;padding:4px 2px 28px;font-size:13px;color:var(--dsw-alias-label-primary,inherit)}
|
|
41
|
+
.lsd-head{display:flex;flex-direction:column;gap:5px}
|
|
42
|
+
.lsd-title{margin:0;font-size:16px;font-weight:600}
|
|
43
|
+
.lsd-desc{margin:0;font-size:12px;line-height:1.65;color:var(--dsw-alias-label-secondary,rgba(128,128,128,.9))}
|
|
44
|
+
.lsd-card{border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.28));border-radius:12px;padding:14px 16px;background:rgba(128,128,128,.06);display:flex;flex-direction:column;gap:12px}
|
|
45
|
+
.lsd-card-head{display:flex;align-items:center;gap:9px}
|
|
46
|
+
.lsd-dot{width:8px;height:8px;border-radius:50%;background:rgba(128,128,128,.6);flex:none}
|
|
47
|
+
.lsd-dot.on{background:var(--dsw-alias-state-success-primary,#2ea043)}
|
|
48
|
+
.lsd-dot.warn{background:var(--dsw-alias-state-warning-primary,#d29922)}
|
|
49
|
+
.lsd-card-title{font-size:14px;font-weight:600}
|
|
50
|
+
.lsd-field{display:flex;flex-direction:column;gap:6px}
|
|
51
|
+
.lsd-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
|
|
52
|
+
.lsd-hint{font-size:11px;line-height:1.6;color:var(--dsw-alias-label-secondary,rgba(128,128,128,.9))}
|
|
53
|
+
.lsd-row{display:flex;gap:8px;align-items:center}
|
|
54
|
+
.lsd-input{flex:1;min-width:0;box-sizing:border-box;padding:7px 10px;font-size:12px;font-family:inherit;
|
|
55
|
+
color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-bg-base,transparent);
|
|
56
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.32));border-radius:8px;outline:none}
|
|
57
|
+
.lsd-input:focus{border-color:var(--dsw-alias-brand-primary,#4a9eff)}
|
|
58
|
+
.lsd-input.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
|
|
59
|
+
.lsd-btn{flex:none;padding:7px 13px;font-size:12px;font-family:inherit;cursor:pointer;white-space:nowrap;
|
|
60
|
+
color:var(--dsw-alias-label-primary,inherit);background:rgba(128,128,128,.14);
|
|
61
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.32));border-radius:8px}
|
|
62
|
+
.lsd-btn:hover:not(:disabled){background:rgba(128,128,128,.24)}
|
|
63
|
+
.lsd-btn:disabled{opacity:.5;cursor:default}
|
|
64
|
+
.lsd-btn.primary{color:#fff;background:var(--dsw-alias-brand-primary,#4a9eff);border-color:transparent}
|
|
65
|
+
.lsd-btn.primary:hover:not(:disabled){filter:brightness(1.08)}
|
|
66
|
+
.lsd-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:10px}
|
|
67
|
+
.lsd-stat{border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.22));border-radius:10px;
|
|
68
|
+
padding:9px 12px;background:var(--dsw-alias-bg-base,transparent);display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
69
|
+
.lsd-stat-k{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--dsw-alias-label-secondary,rgba(128,128,128,.9))}
|
|
70
|
+
.lsd-stat-v{font-size:12px;font-weight:500;word-break:break-all}
|
|
71
|
+
.lsd-toggle{display:flex;align-items:center;gap:8px;font-size:12px;cursor:pointer;user-select:none}
|
|
72
|
+
.lsd-toggle input{width:15px;height:15px;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#4a9eff)}
|
|
73
|
+
.lsd-msg{font-size:12px;line-height:1.6;padding:8px 11px;border-radius:8px;white-space:pre-wrap;word-break:break-word}
|
|
74
|
+
.lsd-msg.ok{color:var(--dsw-alias-state-success-primary,#2ea043);background:rgba(46,160,67,.1)}
|
|
75
|
+
.lsd-msg.err{color:var(--dsw-alias-state-error-primary,#f85149);background:rgba(248,81,73,.1)}
|
|
76
|
+
.lsd-msg.info{color:var(--dsw-alias-label-secondary,rgba(128,128,128,.95));background:rgba(128,128,128,.1)}
|
|
77
|
+
.lsd-found{display:flex;flex-direction:column;gap:6px;max-height:200px;overflow:auto}
|
|
78
|
+
.lsd-found-item{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:7px 10px;
|
|
79
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.24));border-radius:8px;font-size:11px;
|
|
80
|
+
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}
|
|
81
|
+
.lsd-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center;padding-top:2px}
|
|
82
|
+
.lsd-footer{font-size:11px;color:var(--dsw-alias-label-secondary,rgba(128,128,128,.75));word-break:break-all}
|
|
83
|
+
/* ── in-page directory browser ──────────────────────────────────────── */
|
|
84
|
+
.lsd-browser{display:flex;flex-direction:column;gap:9px;padding:11px;border-radius:10px;
|
|
85
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.3));background:rgba(128,128,128,.07)}
|
|
86
|
+
.lsd-browser-head{display:flex;gap:7px;align-items:center}
|
|
87
|
+
.lsd-browser-path{flex:1;min-width:0}
|
|
88
|
+
.lsd-drives{display:flex;gap:5px;flex-wrap:wrap}
|
|
89
|
+
.lsd-drive{flex:none;padding:3px 9px;font-size:11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
90
|
+
cursor:pointer;color:var(--dsw-alias-label-primary,inherit);background:rgba(128,128,128,.14);
|
|
91
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.3));border-radius:6px}
|
|
92
|
+
.lsd-drive:hover:not(:disabled){background:rgba(128,128,128,.26)}
|
|
93
|
+
.lsd-drive:disabled{opacity:.5;cursor:default}
|
|
94
|
+
.lsd-browser-list{display:flex;flex-direction:column;gap:2px;max-height:230px;overflow:auto;
|
|
95
|
+
border-radius:8px;background:var(--dsw-alias-bg-base,rgba(0,0,0,.1));padding:4px}
|
|
96
|
+
.lsd-entry{text-align:left;padding:5px 9px;font-size:12px;font-family:inherit;cursor:pointer;
|
|
97
|
+
color:var(--dsw-alias-label-primary,inherit);background:transparent;border:none;border-radius:5px;
|
|
98
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
99
|
+
.lsd-entry:hover:not(:disabled){background:rgba(128,128,128,.22)}
|
|
100
|
+
.lsd-entry:disabled{opacity:.5;cursor:default}
|
|
101
|
+
.lsd-browser-foot{display:flex;flex-direction:column;gap:8px;align-items:flex-start}
|
|
102
|
+
.lsd-browser-foot .lsd-actions{padding-top:0}
|
|
103
|
+
/* ── hardware readout ───────────────────────────────────────────────── */
|
|
104
|
+
.lsd-hw{display:flex;flex-direction:column;gap:5px;padding:10px 12px;border-radius:10px;
|
|
105
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.24));background:var(--dsw-alias-bg-base,rgba(128,128,128,.05))}
|
|
106
|
+
.lsd-hw-row{display:flex;gap:9px;align-items:baseline;font-size:12px;flex-wrap:wrap}
|
|
107
|
+
.lsd-hw-k{flex:none;min-width:52px;font-size:11px;color:var(--dsw-alias-label-secondary,rgba(128,128,128,.9))}
|
|
108
|
+
.lsd-hw-v{font-weight:500;word-break:break-word}
|
|
109
|
+
.lsd-hw-v.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
|
|
110
|
+
`;
|
|
111
|
+
document.head.appendChild(style);
|
|
112
|
+
return function() { style.remove(); };
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** POST one command to the host settings API. */
|
|
116
|
+
function callApi(payload) {
|
|
117
|
+
return fetch(API_URL, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: { "content-type": "application/json" },
|
|
120
|
+
body: JSON.stringify(payload)
|
|
121
|
+
}).then(function(r) {
|
|
122
|
+
return r.json().catch(function() {
|
|
123
|
+
return { ok: false, error: "HTTP " + r.status };
|
|
124
|
+
});
|
|
125
|
+
}).catch(function(e) {
|
|
126
|
+
return { ok: false, error: String(e && e.message || e) };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fmtMs(ms) {
|
|
131
|
+
if (typeof ms !== "number" || !isFinite(ms)) return "—";
|
|
132
|
+
if (ms <= 0) return "永不";
|
|
133
|
+
if (ms < 60000) return Math.round(ms / 1000) + " 秒";
|
|
134
|
+
return Math.round(ms / 60000) + " 分钟";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── the settings page ──────────────────────────────────────── */
|
|
138
|
+
|
|
139
|
+
function SettingsPage() {
|
|
140
|
+
var stateS = React.useState(null);
|
|
141
|
+
var state = stateS[0];
|
|
142
|
+
var setState = stateS[1];
|
|
143
|
+
var phaseS = React.useState("loading");
|
|
144
|
+
var phase = phaseS[0];
|
|
145
|
+
var setPhase = phaseS[1];
|
|
146
|
+
var msgS = React.useState(null);
|
|
147
|
+
var msg = msgS[0];
|
|
148
|
+
var setMsg = msgS[1];
|
|
149
|
+
var foundS = React.useState(null);
|
|
150
|
+
var found = foundS[0];
|
|
151
|
+
var setFound = foundS[1];
|
|
152
|
+
// In-page directory browser state. `browser` is null while closed,
|
|
153
|
+
// otherwise the listing the host returned for the current path.
|
|
154
|
+
var browserS = React.useState(null);
|
|
155
|
+
var browser = browserS[0];
|
|
156
|
+
var setBrowser = browserS[1];
|
|
157
|
+
var busyS = React.useState(false);
|
|
158
|
+
var busy = busyS[0];
|
|
159
|
+
var setBusy = busyS[1];
|
|
160
|
+
|
|
161
|
+
// Local form state, seeded from the host snapshot. Kept separate
|
|
162
|
+
// so typing does not round-trip until 保存.
|
|
163
|
+
var forgeDirS = React.useState("");
|
|
164
|
+
var forgeDir = forgeDirS[0];
|
|
165
|
+
var setForgeDir = forgeDirS[1];
|
|
166
|
+
var endpointS = React.useState("");
|
|
167
|
+
var endpoint = endpointS[0];
|
|
168
|
+
var setEndpoint = endpointS[1];
|
|
169
|
+
var enabledS = React.useState(true);
|
|
170
|
+
var enabled = enabledS[0];
|
|
171
|
+
var setEnabled = enabledS[1];
|
|
172
|
+
var startupS = React.useState(240000);
|
|
173
|
+
var startupMs = startupS[0];
|
|
174
|
+
var setStartupMs = startupS[1];
|
|
175
|
+
var idleS = React.useState(300000);
|
|
176
|
+
var idleMs = idleS[0];
|
|
177
|
+
var setIdleMs = idleS[1];
|
|
178
|
+
var stepsS = React.useState(25);
|
|
179
|
+
var steps = stepsS[0];
|
|
180
|
+
var setSteps = stepsS[1];
|
|
181
|
+
var cfgS = React.useState(7);
|
|
182
|
+
var cfg = cfgS[0];
|
|
183
|
+
var setCfg = cfgS[1];
|
|
184
|
+
var wS = React.useState(512);
|
|
185
|
+
var width = wS[0];
|
|
186
|
+
var setWidth = wS[1];
|
|
187
|
+
var hS = React.useState(768);
|
|
188
|
+
var height = hS[0];
|
|
189
|
+
var setHeight = hS[1];
|
|
190
|
+
// Hardware snapshot: { gpu, recommend } once the probe lands.
|
|
191
|
+
var hwS = React.useState(null);
|
|
192
|
+
var hw = hwS[0];
|
|
193
|
+
var setHw = hwS[1];
|
|
194
|
+
var hwPhaseS = React.useState("idle");
|
|
195
|
+
var hwPhase = hwPhaseS[0];
|
|
196
|
+
var setHwPhase = hwPhaseS[1];
|
|
197
|
+
|
|
198
|
+
function adopt(s) {
|
|
199
|
+
setState(s);
|
|
200
|
+
if (!s) return;
|
|
201
|
+
setForgeDir(s.forgeDir || "");
|
|
202
|
+
setEndpoint(s.endpoint || "");
|
|
203
|
+
setEnabled(!!(s.autoStart && s.autoStart.enabled));
|
|
204
|
+
setStartupMs((s.autoStart && s.autoStart.startupTimeoutMs) || 240000);
|
|
205
|
+
setIdleMs((s.autoStart && s.autoStart.idleShutdownMs) || 0);
|
|
206
|
+
setSteps((s.defaults && s.defaults.steps) || 25);
|
|
207
|
+
setCfg((s.defaults && s.defaults.cfg) || 7);
|
|
208
|
+
setWidth((s.defaults && s.defaults.width) || 512);
|
|
209
|
+
setHeight((s.defaults && s.defaults.height) || 768);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
React.useEffect(function() {
|
|
213
|
+
var cancelled = false;
|
|
214
|
+
callApi({ method: "get" }).then(function(res) {
|
|
215
|
+
if (cancelled) return;
|
|
216
|
+
if (res && res.ok) {
|
|
217
|
+
adopt(res.state);
|
|
218
|
+
setPhase("ready");
|
|
219
|
+
} else {
|
|
220
|
+
setPhase("error");
|
|
221
|
+
setMsg({ kind: "err", text: "读取配置失败:" + String(res && res.error || "未知错误") });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return function() { cancelled = true; };
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
/** Probe the GPU. Slow (nvidia-smi + registry walk), so it runs
|
|
228
|
+
* after the page is already on screen. */
|
|
229
|
+
function loadHardware() {
|
|
230
|
+
setHwPhase("loading");
|
|
231
|
+
callApi({ method: "hardware" }).then(function(res) {
|
|
232
|
+
if (res && res.ok && res.hardware) {
|
|
233
|
+
setHw(res.hardware);
|
|
234
|
+
setHwPhase("ready");
|
|
235
|
+
} else {
|
|
236
|
+
setHwPhase("error");
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
React.useEffect(function() {
|
|
242
|
+
var cancelled = false;
|
|
243
|
+
setHwPhase("loading");
|
|
244
|
+
callApi({ method: "hardware" }).then(function(res) {
|
|
245
|
+
if (cancelled) return;
|
|
246
|
+
if (res && res.ok && res.hardware) {
|
|
247
|
+
setHw(res.hardware);
|
|
248
|
+
setHwPhase("ready");
|
|
249
|
+
} else {
|
|
250
|
+
setHwPhase("error");
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return function() { cancelled = true; };
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
/** Copy the recommended values into the form. Nothing is saved
|
|
257
|
+
* until 保存 is pressed — this only fills the inputs. */
|
|
258
|
+
function applyRecommend() {
|
|
259
|
+
if (hw === null || !hw.recommend) return;
|
|
260
|
+
var r = hw.recommend;
|
|
261
|
+
setWidth(r.width);
|
|
262
|
+
setHeight(r.height);
|
|
263
|
+
setSteps(r.steps);
|
|
264
|
+
setCfg(r.cfg);
|
|
265
|
+
setMsg({ kind: "info", text: "已填入推荐值:" + r.width + "×" + r.height + " / " + r.steps + " 步 / CFG " + r.cfg + ",记得点保存" });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function save() {
|
|
269
|
+
setBusy(true);
|
|
270
|
+
setMsg(null);
|
|
271
|
+
return callApi({
|
|
272
|
+
method: "save",
|
|
273
|
+
forgeDir: forgeDir,
|
|
274
|
+
endpoint: endpoint,
|
|
275
|
+
autoStart: {
|
|
276
|
+
enabled: enabled,
|
|
277
|
+
startupTimeoutMs: Number(startupMs) || 240000,
|
|
278
|
+
idleShutdownMs: Number(idleMs) || 0
|
|
279
|
+
},
|
|
280
|
+
defaults: {
|
|
281
|
+
steps: Number(steps) || 25,
|
|
282
|
+
cfg: Number(cfg) || 7,
|
|
283
|
+
width: Number(width) || 512,
|
|
284
|
+
height: Number(height) || 768
|
|
285
|
+
}
|
|
286
|
+
}).then(function(res) {
|
|
287
|
+
setBusy(false);
|
|
288
|
+
if (res && res.ok) {
|
|
289
|
+
adopt(res.state);
|
|
290
|
+
var okLauncher = res.state && res.state.launcher;
|
|
291
|
+
setMsg(okLauncher
|
|
292
|
+
? { kind: "ok", text: "已保存。识别到启动文件:" + okLauncher.launcher }
|
|
293
|
+
: { kind: "warn", text: "已保存,但那个目录里没找到 run.bat / webui.bat / webui.sh。" +
|
|
294
|
+
"\n填 WebUI 解压后的根目录(Forge 是外层那个带 run.bat 的文件夹)。" });
|
|
295
|
+
} else {
|
|
296
|
+
setMsg({ kind: "err", text: "保存失败:" + String(res && res.error || "未知错误") });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Open the in-page browser, starting at whatever is currently typed
|
|
302
|
+
// (or the host's home directory when the box is empty).
|
|
303
|
+
function browse() {
|
|
304
|
+
setBusy(true);
|
|
305
|
+
setMsg(null);
|
|
306
|
+
callApi({ method: "browse", path: forgeDir || "" }).then(function(res) {
|
|
307
|
+
setBusy(false);
|
|
308
|
+
if (res && res.ok && res.listing) {
|
|
309
|
+
setBrowser(res.listing);
|
|
310
|
+
} else {
|
|
311
|
+
setMsg({ kind: "err", text: "打开目录失败:" + String(res && res.error || "未知错误") });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Load one directory into the open browser. */
|
|
317
|
+
function openDir(path) {
|
|
318
|
+
setBusy(true);
|
|
319
|
+
callApi({ method: "browse", path: path }).then(function(res) {
|
|
320
|
+
setBusy(false);
|
|
321
|
+
if (res && res.ok && res.listing) {
|
|
322
|
+
setBrowser(res.listing);
|
|
323
|
+
} else {
|
|
324
|
+
setMsg({ kind: "err", text: "打开目录失败:" + String(res && res.error || "未知错误") });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Commit the browser's current directory to the form field. */
|
|
330
|
+
function useCurrentDir() {
|
|
331
|
+
if (browser === null) return;
|
|
332
|
+
setForgeDir(browser.path);
|
|
333
|
+
setBrowser(null);
|
|
334
|
+
setMsg({ kind: "info", text: "已选:" + browser.path + ",记得点保存" });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function detect() {
|
|
338
|
+
setBusy(true);
|
|
339
|
+
setMsg(null);
|
|
340
|
+
callApi({ method: "detect" }).then(function(res) {
|
|
341
|
+
setBusy(false);
|
|
342
|
+
if (res && res.ok) {
|
|
343
|
+
setFound(res.found || []);
|
|
344
|
+
if (!res.found || res.found.length === 0) {
|
|
345
|
+
setMsg({ kind: "warn", text: "常见位置没扫到。手动填目录吧,就是含 run.bat 的那个文件夹。" });
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
setMsg({ kind: "err", text: "探测失败:" + String(res && res.error || "未知错误") });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function backend(action) {
|
|
354
|
+
setBusy(true);
|
|
355
|
+
setMsg(null);
|
|
356
|
+
callApi({ method: action }).then(function(res) {
|
|
357
|
+
setBusy(false);
|
|
358
|
+
if (res && res.state) adopt(res.state);
|
|
359
|
+
if (res && res.ok) {
|
|
360
|
+
setMsg({ kind: "ok", text: action === "start" ? "后端已就绪" : "后端已关闭,显存释放" });
|
|
361
|
+
} else {
|
|
362
|
+
setMsg({ kind: "err", text: String(res && res.error || "操作失败") });
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (phase === "loading") {
|
|
368
|
+
return h("div", { className: "lsd-root" },
|
|
369
|
+
h("div", { className: "lsd-desc" }, "读取配置中…")
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
var live = state && state.liveEndpoint;
|
|
374
|
+
var launcher = state && state.launcher;
|
|
375
|
+
var dotClass = "lsd-dot" + (live ? " on" : (launcher ? " warn" : ""));
|
|
376
|
+
|
|
377
|
+
return h("div", { className: "lsd-root" },
|
|
378
|
+
h("div", { className: "lsd-head" },
|
|
379
|
+
h("h2", { className: "lsd-title" }, "本地绘图"),
|
|
380
|
+
h("p", { className: "lsd-desc" },
|
|
381
|
+
"把本机的 Stable Diffusion WebUI / Forge 接到 DSH。提示词原样送进采样器,"
|
|
382
|
+
+ "不经过任何云端服务、没有内容过滤。后端按需启动,闲置自动关闭。")
|
|
383
|
+
),
|
|
384
|
+
|
|
385
|
+
// ── status ───────────────────────────────────────────
|
|
386
|
+
h("div", { className: "lsd-card" },
|
|
387
|
+
h("div", { className: "lsd-card-head" },
|
|
388
|
+
h("span", { className: dotClass }),
|
|
389
|
+
h("span", { className: "lsd-card-title" },
|
|
390
|
+
live ? "后端运行中" : (launcher ? "已配置,后端未启动" : "还没配置"))
|
|
391
|
+
),
|
|
392
|
+
h("div", { className: "lsd-grid" },
|
|
393
|
+
h("div", { className: "lsd-stat" },
|
|
394
|
+
h("span", { className: "lsd-stat-k" }, "接口地址"),
|
|
395
|
+
h("span", { className: "lsd-stat-v" }, live || (state && state.endpoint) || "—")
|
|
396
|
+
),
|
|
397
|
+
h("div", { className: "lsd-stat" },
|
|
398
|
+
h("span", { className: "lsd-stat-k" }, "启动文件"),
|
|
399
|
+
h("span", { className: "lsd-stat-v" }, launcher ? launcher.launcher : "未识别")
|
|
400
|
+
),
|
|
401
|
+
h("div", { className: "lsd-stat" },
|
|
402
|
+
h("span", { className: "lsd-stat-k" }, "进程归属"),
|
|
403
|
+
h("span", { className: "lsd-stat-v" }, state && state.owned ? "插件启动" : "非插件启动")
|
|
404
|
+
),
|
|
405
|
+
h("div", { className: "lsd-stat" },
|
|
406
|
+
h("span", { className: "lsd-stat-k" }, "闲置关闭"),
|
|
407
|
+
h("span", { className: "lsd-stat-v" }, fmtMs(idleMs))
|
|
408
|
+
)
|
|
409
|
+
),
|
|
410
|
+
h("div", { className: "lsd-actions" },
|
|
411
|
+
h("button", {
|
|
412
|
+
className: "lsd-btn", disabled: busy,
|
|
413
|
+
onClick: function() { backend("start"); }
|
|
414
|
+
}, "预热后端"),
|
|
415
|
+
h("button", {
|
|
416
|
+
className: "lsd-btn", disabled: busy || !(state && state.owned),
|
|
417
|
+
onClick: function() { backend("stop"); }
|
|
418
|
+
}, "释放显存"),
|
|
419
|
+
h("button", {
|
|
420
|
+
className: "lsd-btn", disabled: busy,
|
|
421
|
+
onClick: function() { callApi({ method: "get" }).then(function(r) { if (r && r.ok) adopt(r.state); }); }
|
|
422
|
+
}, "刷新")
|
|
423
|
+
)
|
|
424
|
+
),
|
|
425
|
+
|
|
426
|
+
// ── directory ────────────────────────────────────────
|
|
427
|
+
h("div", { className: "lsd-card" },
|
|
428
|
+
h("div", { className: "lsd-card-head" },
|
|
429
|
+
h("span", { className: "lsd-card-title" }, "绘图程序目录")
|
|
430
|
+
),
|
|
431
|
+
h("div", { className: "lsd-field" },
|
|
432
|
+
h("label", { className: "lsd-label" }, "Stable Diffusion WebUI / Forge 根目录"),
|
|
433
|
+
h("div", { className: "lsd-row" },
|
|
434
|
+
h("input", {
|
|
435
|
+
className: "lsd-input mono",
|
|
436
|
+
value: forgeDir,
|
|
437
|
+
placeholder: "例如 D:\\AI\\forge\\webui_forge_cu124_torch24",
|
|
438
|
+
onChange: function(e) { setForgeDir(e.target.value); }
|
|
439
|
+
}),
|
|
440
|
+
h("button", { className: "lsd-btn", disabled: busy, onClick: browse }, "浏览…"),
|
|
441
|
+
h("button", { className: "lsd-btn", disabled: busy, onClick: detect }, "自动探测")
|
|
442
|
+
),
|
|
443
|
+
h("span", { className: "lsd-hint" },
|
|
444
|
+
"填解压后带 run.bat 的那一层(Forge 是外层文件夹,不是 system 里面)。"
|
|
445
|
+
+ "也接受直接填 run.bat 的完整路径。Linux / macOS 会自动找 webui.sh。")
|
|
446
|
+
),
|
|
447
|
+
|
|
448
|
+
// ── in-page directory browser ────────────────────
|
|
449
|
+
browser !== null
|
|
450
|
+
? h("div", { className: "lsd-browser" },
|
|
451
|
+
h("div", { className: "lsd-browser-head" },
|
|
452
|
+
h("button", {
|
|
453
|
+
className: "lsd-btn", disabled: busy || browser.parent === null,
|
|
454
|
+
title: browser.parent === null ? "已经是根目录" : browser.parent,
|
|
455
|
+
onClick: function() { if (browser.parent) openDir(browser.parent); }
|
|
456
|
+
}, "↑ 上一层"),
|
|
457
|
+
h("input", {
|
|
458
|
+
className: "lsd-input mono lsd-browser-path",
|
|
459
|
+
value: browser.path,
|
|
460
|
+
onChange: function(e) { setBrowser(Object.assign({}, browser, { path: e.target.value })); },
|
|
461
|
+
onKeyDown: function(e) { if (e.key === "Enter") openDir(e.target.value); }
|
|
462
|
+
}),
|
|
463
|
+
h("button", { className: "lsd-btn", disabled: busy, onClick: function() { openDir(browser.path); } }, "前往")
|
|
464
|
+
),
|
|
465
|
+
|
|
466
|
+
browser.roots && browser.roots.length > 1
|
|
467
|
+
? h("div", { className: "lsd-drives" },
|
|
468
|
+
browser.roots.map(function(drive) {
|
|
469
|
+
return h("button", {
|
|
470
|
+
className: "lsd-drive", key: drive, disabled: busy,
|
|
471
|
+
onClick: function() { openDir(drive); }
|
|
472
|
+
}, drive);
|
|
473
|
+
})
|
|
474
|
+
)
|
|
475
|
+
: null,
|
|
476
|
+
|
|
477
|
+
browser.error
|
|
478
|
+
? h("div", { className: "lsd-msg err" }, browser.error)
|
|
479
|
+
: null,
|
|
480
|
+
|
|
481
|
+
h("div", { className: "lsd-browser-list" },
|
|
482
|
+
browser.entries && browser.entries.length > 0
|
|
483
|
+
? browser.entries.map(function(name) {
|
|
484
|
+
var sep = browser.path.indexOf("\\") >= 0 ? "\\" : "/";
|
|
485
|
+
var child = browser.path.replace(/[\\/]+$/, "") + sep + name;
|
|
486
|
+
return h("button", {
|
|
487
|
+
className: "lsd-entry", key: name, disabled: busy,
|
|
488
|
+
onClick: function() { openDir(child); }
|
|
489
|
+
}, "📁 " + name);
|
|
490
|
+
})
|
|
491
|
+
: h("div", { className: "lsd-hint" }, browser.error ? "" : "这个目录里没有子文件夹")
|
|
492
|
+
),
|
|
493
|
+
|
|
494
|
+
h("div", { className: "lsd-browser-foot" },
|
|
495
|
+
browser.launcher
|
|
496
|
+
? h("span", { className: "lsd-msg ok" },
|
|
497
|
+
"✓ 这个目录里有 " + browser.launcher.launcher + ",就是它了")
|
|
498
|
+
: h("span", { className: "lsd-hint" },
|
|
499
|
+
"这个目录里没有 run.bat / webui.bat / webui.sh,继续往下找或者换一层"),
|
|
500
|
+
h("div", { className: "lsd-actions" },
|
|
501
|
+
h("button", {
|
|
502
|
+
className: "lsd-btn primary", disabled: busy,
|
|
503
|
+
onClick: useCurrentDir
|
|
504
|
+
}, "选这个目录"),
|
|
505
|
+
h("button", {
|
|
506
|
+
className: "lsd-btn", disabled: busy,
|
|
507
|
+
onClick: function() { setBrowser(null); }
|
|
508
|
+
}, "取消")
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
: null,
|
|
513
|
+
|
|
514
|
+
found !== null && found.length > 0
|
|
515
|
+
? h("div", { className: "lsd-found" },
|
|
516
|
+
found.map(function(item, i) {
|
|
517
|
+
return h("div", { className: "lsd-found-item", key: i },
|
|
518
|
+
h("span", null, item.dir),
|
|
519
|
+
h("button", {
|
|
520
|
+
className: "lsd-btn",
|
|
521
|
+
onClick: function() {
|
|
522
|
+
setForgeDir(item.dir);
|
|
523
|
+
setFound(null);
|
|
524
|
+
setMsg({ kind: "info", text: "已选:" + item.dir + ",记得点保存" });
|
|
525
|
+
}
|
|
526
|
+
}, "用这个")
|
|
527
|
+
);
|
|
528
|
+
})
|
|
529
|
+
)
|
|
530
|
+
: null
|
|
531
|
+
),
|
|
532
|
+
|
|
533
|
+
// ── backend behaviour ────────────────────────────────
|
|
534
|
+
h("div", { className: "lsd-card" },
|
|
535
|
+
h("div", { className: "lsd-card-head" },
|
|
536
|
+
h("span", { className: "lsd-card-title" }, "后端行为")
|
|
537
|
+
),
|
|
538
|
+
h("label", { className: "lsd-toggle" },
|
|
539
|
+
h("input", {
|
|
540
|
+
type: "checkbox", checked: enabled,
|
|
541
|
+
onChange: function(e) { setEnabled(e.target.checked); }
|
|
542
|
+
}),
|
|
543
|
+
h("span", null, "后端没开时自动启动它")
|
|
544
|
+
),
|
|
545
|
+
h("div", { className: "lsd-grid" },
|
|
546
|
+
h("div", { className: "lsd-field" },
|
|
547
|
+
h("label", { className: "lsd-label" }, "启动超时(毫秒)"),
|
|
548
|
+
h("input", {
|
|
549
|
+
className: "lsd-input", type: "number", min: 30000, step: 10000,
|
|
550
|
+
value: startupMs,
|
|
551
|
+
onChange: function(e) { setStartupMs(e.target.value); }
|
|
552
|
+
}),
|
|
553
|
+
h("span", { className: "lsd-hint" }, "冷启动要 2~3 分钟,别调太小")
|
|
554
|
+
),
|
|
555
|
+
h("div", { className: "lsd-field" },
|
|
556
|
+
h("label", { className: "lsd-label" }, "闲置多久自动关闭(毫秒)"),
|
|
557
|
+
h("input", {
|
|
558
|
+
className: "lsd-input", type: "number", min: 0, step: 60000,
|
|
559
|
+
value: idleMs,
|
|
560
|
+
onChange: function(e) { setIdleMs(e.target.value); }
|
|
561
|
+
}),
|
|
562
|
+
h("span", { className: "lsd-hint" }, "填 0 = 永不自动关闭")
|
|
563
|
+
),
|
|
564
|
+
h("div", { className: "lsd-field" },
|
|
565
|
+
h("label", { className: "lsd-label" }, "接口地址"),
|
|
566
|
+
h("input", {
|
|
567
|
+
className: "lsd-input mono", value: endpoint,
|
|
568
|
+
onChange: function(e) { setEndpoint(e.target.value); }
|
|
569
|
+
}),
|
|
570
|
+
h("span", { className: "lsd-hint" }, "默认 7860,改过端口才需要动")
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
),
|
|
574
|
+
|
|
575
|
+
// ── defaults ─────────────────────────────────────────
|
|
576
|
+
h("div", { className: "lsd-card" },
|
|
577
|
+
h("div", { className: "lsd-card-head" },
|
|
578
|
+
h("span", { className: "lsd-card-title" }, "默认出图参数")
|
|
579
|
+
),
|
|
580
|
+
|
|
581
|
+
// Hardware readout + one-click recommended values.
|
|
582
|
+
hwPhase === "loading"
|
|
583
|
+
? h("div", { className: "lsd-hint" }, "正在识别显卡…")
|
|
584
|
+
: hwPhase === "error"
|
|
585
|
+
? h("div", { className: "lsd-hint" },
|
|
586
|
+
"没读到显卡信息,按最保守的参数来。可以点「重新识别」再试一次。"
|
|
587
|
+
+ "\n如果一直是这样,多半是 DSH 没重启 —— 插件换了文件但宿主还跑着旧代码。")
|
|
588
|
+
: hw !== null
|
|
589
|
+
? h("div", { className: "lsd-hw" },
|
|
590
|
+
h("div", { className: "lsd-hw-row" },
|
|
591
|
+
h("span", { className: "lsd-hw-k" }, "显卡"),
|
|
592
|
+
h("span", { className: "lsd-hw-v" },
|
|
593
|
+
hw.gpu
|
|
594
|
+
? hw.gpu.name + " " + (hw.gpu.vramMb / 1024).toFixed(1).replace(/\.0$/, "") + " GB 显存"
|
|
595
|
+
: "未识别"
|
|
596
|
+
)
|
|
597
|
+
),
|
|
598
|
+
hw.gpu && !hw.gpu.exact
|
|
599
|
+
? h("div", { className: "lsd-hint" },
|
|
600
|
+
"(显存数值来自 WMI,8 GB 以上的卡可能显示不准,仅供参考)")
|
|
601
|
+
: null,
|
|
602
|
+
// When nothing was found, show what each probe
|
|
603
|
+
// actually returned — a bare "not detected"
|
|
604
|
+
// leaves no way to tell a driver problem from a
|
|
605
|
+
// sandbox one.
|
|
606
|
+
!hw.gpu && hw.tried && hw.tried.length > 0
|
|
607
|
+
? h("div", { className: "lsd-hint" },
|
|
608
|
+
"试过:" + hw.tried.map(function(t) {
|
|
609
|
+
return t.source + (t.ok ? " ✓" : " ✗");
|
|
610
|
+
}).join(" ")
|
|
611
|
+
+ "。全都没结果,就手动填参数吧。")
|
|
612
|
+
: null,
|
|
613
|
+
hw.build
|
|
614
|
+
? h("div", { className: "lsd-footer" }, "插件构建 " + hw.build)
|
|
615
|
+
: null,
|
|
616
|
+
hw.recommend
|
|
617
|
+
? h("div", { className: "lsd-hw-row" },
|
|
618
|
+
h("span", { className: "lsd-hw-k" }, "推荐"),
|
|
619
|
+
h("span", { className: "lsd-hw-v" },
|
|
620
|
+
hw.recommend.width + "×" + hw.recommend.height
|
|
621
|
+
+ " " + hw.recommend.steps + " 步 CFG " + hw.recommend.cfg
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
: null,
|
|
625
|
+
hw.recommend && hw.recommend.note
|
|
626
|
+
? h("div", { className: "lsd-hint" }, hw.recommend.note)
|
|
627
|
+
: null,
|
|
628
|
+
hw.recommend && hw.recommend.launchArgs
|
|
629
|
+
? h("div", { className: "lsd-hw-row" },
|
|
630
|
+
h("span", { className: "lsd-hw-k" }, "启动参数"),
|
|
631
|
+
h("span", { className: "lsd-hw-v mono" }, hw.recommend.launchArgs)
|
|
632
|
+
)
|
|
633
|
+
: null
|
|
634
|
+
)
|
|
635
|
+
: null,
|
|
636
|
+
|
|
637
|
+
hw !== null && hw.recommend
|
|
638
|
+
? h("div", { className: "lsd-actions" },
|
|
639
|
+
h("button", {
|
|
640
|
+
className: "lsd-btn", disabled: busy,
|
|
641
|
+
onClick: applyRecommend
|
|
642
|
+
}, "套用推荐值"),
|
|
643
|
+
h("button", {
|
|
644
|
+
className: "lsd-btn", disabled: busy || hwPhase === "loading",
|
|
645
|
+
onClick: loadHardware
|
|
646
|
+
}, "重新识别")
|
|
647
|
+
)
|
|
648
|
+
: hwPhase !== "loading"
|
|
649
|
+
? h("div", { className: "lsd-actions" },
|
|
650
|
+
h("button", {
|
|
651
|
+
className: "lsd-btn", disabled: busy || hwPhase === "loading",
|
|
652
|
+
onClick: loadHardware
|
|
653
|
+
}, "识别显卡")
|
|
654
|
+
)
|
|
655
|
+
: null,
|
|
656
|
+
|
|
657
|
+
h("div", { className: "lsd-grid" },
|
|
658
|
+
h("div", { className: "lsd-field" },
|
|
659
|
+
h("label", { className: "lsd-label" }, "宽度"),
|
|
660
|
+
h("input", {
|
|
661
|
+
className: "lsd-input", type: "number", min: 64, step: 64, value: width,
|
|
662
|
+
onChange: function(e) { setWidth(e.target.value); }
|
|
663
|
+
})
|
|
664
|
+
),
|
|
665
|
+
h("div", { className: "lsd-field" },
|
|
666
|
+
h("label", { className: "lsd-label" }, "高度"),
|
|
667
|
+
h("input", {
|
|
668
|
+
className: "lsd-input", type: "number", min: 64, step: 64, value: height,
|
|
669
|
+
onChange: function(e) { setHeight(e.target.value); }
|
|
670
|
+
})
|
|
671
|
+
),
|
|
672
|
+
h("div", { className: "lsd-field" },
|
|
673
|
+
h("label", { className: "lsd-label" }, "步数"),
|
|
674
|
+
h("input", {
|
|
675
|
+
className: "lsd-input", type: "number", min: 1, max: 150, value: steps,
|
|
676
|
+
onChange: function(e) { setSteps(e.target.value); }
|
|
677
|
+
})
|
|
678
|
+
),
|
|
679
|
+
h("div", { className: "lsd-field" },
|
|
680
|
+
h("label", { className: "lsd-label" }, "CFG"),
|
|
681
|
+
h("input", {
|
|
682
|
+
className: "lsd-input", type: "number", min: 1, max: 30, step: 0.5, value: cfg,
|
|
683
|
+
onChange: function(e) { setCfg(e.target.value); }
|
|
684
|
+
})
|
|
685
|
+
)
|
|
686
|
+
),
|
|
687
|
+
h("span", { className: "lsd-hint" },
|
|
688
|
+
hw !== null && hw.recommend
|
|
689
|
+
? "按你现在的显卡,上面这组是能跑满又不容易爆显存的组合。手动改也行。"
|
|
690
|
+
: "没识别到显卡时按最保守的来:SD1.5 用 512×768 基本不会爆。")
|
|
691
|
+
),
|
|
692
|
+
|
|
693
|
+
msg !== null
|
|
694
|
+
? h("div", { className: "lsd-msg " + (msg.kind === "warn" ? "info" : msg.kind) }, msg.text)
|
|
695
|
+
: null,
|
|
696
|
+
|
|
697
|
+
h("div", { className: "lsd-actions" },
|
|
698
|
+
h("button", {
|
|
699
|
+
className: "lsd-btn primary", disabled: busy, onClick: save
|
|
700
|
+
}, busy ? "处理中…" : "保存"),
|
|
701
|
+
state
|
|
702
|
+
? h("span", { className: "lsd-footer" }, "配置文件:" + state.configPath)
|
|
703
|
+
: null
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* ── registration ───────────────────────────────────────────── */
|
|
709
|
+
|
|
710
|
+
ctx.slots.inject("settings.section", function() {
|
|
711
|
+
return ctx.slots.register({
|
|
712
|
+
name: "settings.section",
|
|
713
|
+
id: "local-sd",
|
|
714
|
+
// After the shipped pages, next to the other plugin pages.
|
|
715
|
+
order: 210,
|
|
716
|
+
label: NAV_LABEL
|
|
717
|
+
}, SettingsPage);
|
|
718
|
+
});
|
|
719
|
+
};
|
|
720
|
+
return module.exports;
|
|
721
|
+
}
|
|
722
|
+
});
|