dsh-multimodal-node 0.0.0-stage → 0.3.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/README.md +504 -2
- package/client.js +2817 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +9 -0
- package/index.js +960 -0
- package/lib/apikeys.js +250 -0
- package/lib/assets.js +234 -0
- package/lib/auth.js +315 -0
- package/lib/blender.js +644 -0
- package/lib/chain.js +161 -0
- package/lib/comfy.js +204 -0
- package/lib/config.js +377 -0
- package/lib/gateway.js +974 -0
- package/lib/http.js +119 -0
- package/lib/imagesize.js +92 -0
- package/lib/instances.js +554 -0
- package/lib/library.js +1209 -0
- package/lib/models.js +322 -0
- package/lib/nodeapi.js +491 -0
- package/lib/nodeclient.js +143 -0
- package/lib/pages.js +5788 -0
- package/lib/pool.js +591 -0
- package/lib/promptassist.js +1077 -0
- package/lib/promptlibrary.js +92 -0
- package/lib/remote.js +3348 -0
- package/lib/runindex.js +126 -0
- package/lib/seed.js +118 -0
- package/lib/tagdata.json +1 -0
- package/lib/tiers.js +283 -0
- package/lib/tools.js +230 -0
- package/lib/viewport.js +134 -0
- package/lib/workflows.js +472 -0
- package/lib/ws.js +106 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +62 -4
package/lib/models.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型清单:从**本地磁盘**读,不问实例。
|
|
3
|
+
*
|
|
4
|
+
* 这条判据是 03 号插件用一整轮踩出来的,这里直接继承:
|
|
5
|
+
*
|
|
6
|
+
* - 实例的 `/models` 只列 `folder_paths` 注册过的目录。`grounding-dino`、`insightface`、
|
|
7
|
+
* `reactor`、`yolo` 明明在盘上却永远不出现,于是「按 API 列表做下拉」天生缺项。
|
|
8
|
+
* - 实例按各自答一遍,两张卡共用一个 models 目录时每个文件出现两次。
|
|
9
|
+
* - 设备睡着时实例什么都不答——而那正是有人在整理模型的时候。
|
|
10
|
+
*
|
|
11
|
+
* 一次 `readdir` 在本地答得同样完整,且不依赖服务活着。
|
|
12
|
+
*
|
|
13
|
+
* 扫描根有三个来源,优先级从高到低:
|
|
14
|
+
*
|
|
15
|
+
* 1. 实例自己的 `modelsRoot`(面板上「模型文件夹地址」填的那个)
|
|
16
|
+
* 2. 从实例的启动脚本推断(`<bat 所在目录>/ComfyUI/models`)
|
|
17
|
+
* 3. 全局附加扫描目录 `modelsScan.roots`(一份 ComfyUI 之外散落的模型也能纳入)
|
|
18
|
+
*
|
|
19
|
+
* 三者合并去重。**按实例查**时只用该实例的前两个来源——两个实例的 models 目录不一样时,
|
|
20
|
+
* 「GPU1 的模型列表」里出现 GPU0 独有文件会让人做错判断。
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
23
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
24
|
+
|
|
25
|
+
/** ComfyUI 认的权重扩展名。`.py` / `.json` / `.yaml` 是权重旁边的代码与配置,不是模型。 */
|
|
26
|
+
const MODEL_FILE_EXT = /\.(safetensors|ckpt|pt|pth|bin|gguf|onnx|sft|pkl)$/i;
|
|
27
|
+
|
|
28
|
+
/** 认得出来的 ComfyUI 模型目录名:用来判断「这个地址到底像不像一个 models 根」。 */
|
|
29
|
+
const MODELS_FOLDER_NAMES = new Set([
|
|
30
|
+
'checkpoints', 'loras', 'vae', 'controlnet', 'clip', 'clip_vision', 'unet', 'diffusion_models',
|
|
31
|
+
'text_encoders', 'upscale_models', 'embeddings', 'hypernetworks', 'style_models', 'gligen',
|
|
32
|
+
'photomaker', 'ipadapter', 'instantid', 'insightface', 'reactor', 'yolo', 'grounding-dino',
|
|
33
|
+
'sam', 'sams', 'ultralytics', 'vae_approx', 'model_patches',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/** 磁盘上什么都没有时的兜底清单:至少让下拉有一个正确的目录名可选。 */
|
|
37
|
+
const FALLBACK_FOLDERS = [
|
|
38
|
+
['checkpoints', '大模型 / Checkpoint'],
|
|
39
|
+
['loras', 'LoRA'],
|
|
40
|
+
['vae', 'VAE'],
|
|
41
|
+
['controlnet', 'ControlNet'],
|
|
42
|
+
['clip', 'CLIP'],
|
|
43
|
+
['clip_vision', 'CLIP Vision'],
|
|
44
|
+
['unet', 'UNet / Diffusion'],
|
|
45
|
+
['diffusion_models', 'Diffusion Models'],
|
|
46
|
+
['text_encoders', 'Text Encoders'],
|
|
47
|
+
['upscale_models', '放大模型'],
|
|
48
|
+
['embeddings', 'Embedding'],
|
|
49
|
+
['hypernetworks', 'Hypernetwork'],
|
|
50
|
+
['style_models', 'Style Models'],
|
|
51
|
+
['gligen', 'GLIGEN'],
|
|
52
|
+
['photomaker', 'PhotoMaker'],
|
|
53
|
+
['ipadapter', 'IPAdapter'],
|
|
54
|
+
['instantid', 'InstantID'],
|
|
55
|
+
['insightface', 'InsightFace'],
|
|
56
|
+
['reactor', 'ReActor'],
|
|
57
|
+
['yolo', 'YOLO'],
|
|
58
|
+
['grounding-dino', 'GroundingDINO'],
|
|
59
|
+
['sam', 'SAM'],
|
|
60
|
+
['sams', 'SAM'],
|
|
61
|
+
['ultralytics', 'Ultralytics'],
|
|
62
|
+
['vae_approx', 'VAE Approx'],
|
|
63
|
+
['model_patches', 'Model Patches'],
|
|
64
|
+
['configs', 'Configs'],
|
|
65
|
+
['custom_nodes', 'Custom Nodes'],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** 一个扫描根:路径 + 它是怎么来的(面板要能解释「这个目录为什么会出现」)。 */
|
|
69
|
+
export function rootEntry(path, source, instanceId, label) {
|
|
70
|
+
return { path, source, instanceId: instanceId ?? null, label: label ?? null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 一个实例自己声称的 models 根(不含全局附加目录)。
|
|
75
|
+
*
|
|
76
|
+
* 从**设置**读而不是从运行时读:`sync()` 会在设置变更时重建运行时对象,
|
|
77
|
+
* 而一个只改设置没 sync 的调用方(面板之外的编辑器、脚本)会继续拿到旧的 modelsRoot,
|
|
78
|
+
* 表现为「改了路径但列表没变」。
|
|
79
|
+
*/
|
|
80
|
+
export function instanceModelRoots(settingsValue, instanceId) {
|
|
81
|
+
const roots = [];
|
|
82
|
+
const rows = Array.isArray(settingsValue?.instances) ? settingsValue.instances : [];
|
|
83
|
+
for (const instance of rows) {
|
|
84
|
+
if (instance === null || typeof instance !== 'object') continue;
|
|
85
|
+
if (instance.enabled === false) continue;
|
|
86
|
+
if (instanceId !== undefined && instanceId !== null && instanceId !== '' && instance.id !== instanceId) continue;
|
|
87
|
+
const explicit = String(instance.modelsRoot ?? '').trim();
|
|
88
|
+
if (explicit !== '' && isAbsolute(explicit)) {
|
|
89
|
+
if (!roots.some((row) => row.path === explicit)) {
|
|
90
|
+
roots.push(rootEntry(explicit, 'instance', instance.id, instance.label ?? instance.id));
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const bat = String(instance.control?.batPath ?? '').trim();
|
|
95
|
+
if (bat === '') continue;
|
|
96
|
+
// 便携版 ComfyUI 的固定布局:启动脚本坐在根目录,models 在它下面。
|
|
97
|
+
const inferred = join(dirname(bat), 'ComfyUI', 'models');
|
|
98
|
+
if (!roots.some((row) => row.path === inferred)) {
|
|
99
|
+
roots.push(rootEntry(inferred, 'launcher', instance.id, instance.label ?? instance.id));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return roots;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 全局附加扫描目录(与任何实例无关,纯手工指定的文件夹)。 */
|
|
106
|
+
export function extraModelRoots(settingsValue) {
|
|
107
|
+
const rows = Array.isArray(settingsValue?.modelsScan?.roots) ? settingsValue.modelsScan.roots : [];
|
|
108
|
+
const roots = [];
|
|
109
|
+
for (const raw of rows) {
|
|
110
|
+
const text = String(raw ?? '').trim();
|
|
111
|
+
if (text === '' || !isAbsolute(text)) continue;
|
|
112
|
+
if (roots.some((row) => row.path === text)) continue;
|
|
113
|
+
roots.push(rootEntry(text, 'extra', null, basename(text)));
|
|
114
|
+
}
|
|
115
|
+
return roots;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 所有可解析的扫描根,去重,按来源顺序。
|
|
120
|
+
*
|
|
121
|
+
* 传 `instanceId` 时就只算那个实例的根 + 全局附加目录:一个实例的清单里
|
|
122
|
+
* 混进另一个实例独有目录的内容,会让「哪张卡有哪个模型」这个判断失效。
|
|
123
|
+
*/
|
|
124
|
+
export function localModelRoots(settingsValue, instanceId) {
|
|
125
|
+
const roots = instanceModelRoots(settingsValue, instanceId);
|
|
126
|
+
for (const row of extraModelRoots(settingsValue)) {
|
|
127
|
+
if (!roots.some((entry) => entry.path === row.path)) roots.push(row);
|
|
128
|
+
}
|
|
129
|
+
return roots.map((row) => row.path);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 同上,但带来源信息(面板要显示每个根是从哪来的)。 */
|
|
133
|
+
export function localModelRootEntries(settingsValue, instanceId) {
|
|
134
|
+
const rows = instanceModelRoots(settingsValue, instanceId);
|
|
135
|
+
for (const row of extraModelRoots(settingsValue)) {
|
|
136
|
+
if (!rows.some((entry) => entry.path === row.path)) rows.push(row);
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 这个目录本身能不能当一个 models 根:存在、是目录、至少能列出一次。 */
|
|
142
|
+
export function inspectModelDir(raw) {
|
|
143
|
+
const text = String(raw ?? '').trim();
|
|
144
|
+
if (text === '') return { ok: false, error: '没有填目录', path: '' };
|
|
145
|
+
if (!isAbsolute(text)) return { ok: false, error: '需要绝对路径', path: text };
|
|
146
|
+
const path = resolve(text);
|
|
147
|
+
let entries;
|
|
148
|
+
try {
|
|
149
|
+
entries = readdirSync(path, { withFileTypes: true });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const code = error?.code ?? '';
|
|
152
|
+
if (code === 'ENOENT') return { ok: false, error: '目录不存在', path };
|
|
153
|
+
if (code === 'EACCES' || code === 'EPERM') return { ok: false, error: '没有读取权限', path };
|
|
154
|
+
return { ok: false, error: String(error?.message ?? error), path };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 列子目录时**不过滤 models**。
|
|
158
|
+
*
|
|
159
|
+
* 上一版把 configs/datasets 与 models 一起当成「不是模型目录」跳过,
|
|
160
|
+
* 于是「用户填了 ComfyUI 根而不是 models 目录」这个最常见的错误反而检测不到——
|
|
161
|
+
* 本该出现的「下面还有一层 models」永远不触发。
|
|
162
|
+
* 过滤只该作用于「模型清单」,不该作用于「探测这个地址对不对」。
|
|
163
|
+
*/
|
|
164
|
+
const dirs = [];
|
|
165
|
+
const files = [];
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
dirs.push(entry.name);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (entry.isFile() && MODEL_FILE_EXT.test(entry.name)) files.push(entry.name);
|
|
172
|
+
}
|
|
173
|
+
const looksLikeModels = dirs.some((name) => MODELS_FOLDER_NAMES.has(name)) || files.length > 0;
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
path,
|
|
177
|
+
/** 看起来像不像一个 models 根:有一批已知的子目录,或直接躺着权重文件。 */
|
|
178
|
+
looksLikeModels,
|
|
179
|
+
/** 模型清单用的目录列表:这里才排除 configs/datasets(它们不装模型)。 */
|
|
180
|
+
folders: dirs.filter((name) => name !== 'configs' && name !== 'datasets').sort(),
|
|
181
|
+
allDirs: dirs.sort(),
|
|
182
|
+
looseFiles: files.sort().slice(0, 40),
|
|
183
|
+
/**
|
|
184
|
+
* 填高了时的提示。两种常见形态都要认:
|
|
185
|
+
* .../ComfyUI ← 下面直接是 models
|
|
186
|
+
* .../ComfyUI-aki-v3.2 ← 下面是 ComfyUI,再下面才是 models
|
|
187
|
+
* 只认第一种会让「便携版根目录」这个最常见的填法得不到任何提示。
|
|
188
|
+
*/
|
|
189
|
+
hint: dirs.includes('models')
|
|
190
|
+
? '这个目录下面还有一层 models,可能应该填那一层'
|
|
191
|
+
: dirs.includes('ComfyUI')
|
|
192
|
+
? '这个目录下面有 ComfyUI,再往里一层才是 models(…/ComfyUI/models)'
|
|
193
|
+
: null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 每个根下的子目录名;一个都读不到时回落到内置清单。 */
|
|
198
|
+
export function modelFolders(settingsValue, instanceId) {
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
const rows = [];
|
|
201
|
+
for (const root of localModelRoots(settingsValue, instanceId)) {
|
|
202
|
+
let entries;
|
|
203
|
+
try {
|
|
204
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (!entry.isDirectory()) continue;
|
|
210
|
+
const name = entry.name;
|
|
211
|
+
if (seen.has(name)) continue;
|
|
212
|
+
// ComfyUI 把 configs / datasets 放在权重旁边,它们不装模型。
|
|
213
|
+
if (name === 'configs' || name === 'datasets') continue;
|
|
214
|
+
seen.add(name);
|
|
215
|
+
rows.push([name, name]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (rows.length === 0) return FALLBACK_FOLDERS.map(([id, label]) => [id, label]);
|
|
219
|
+
return rows.sort((left, right) => left[0].localeCompare(right[0]));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 一个目录里的文件,递归,返回 `子目录/文件名`。
|
|
224
|
+
*
|
|
225
|
+
* 只列权重:早期的「全部扩展名」规则让 `grounding-dino/` 里冒出 `GroundingDINO_SwinB.cfg.py`、
|
|
226
|
+
* `bert-base-uncased/` 里冒出 `vocab.txt`,选中就加载失败。白名单是诚实的规则。
|
|
227
|
+
*/
|
|
228
|
+
export function modelsInFolder(settingsValue, folder, instanceId) {
|
|
229
|
+
const name = String(folder ?? '').trim();
|
|
230
|
+
// 目录名是一个路径段:任何能跳出根的东西一律拒绝。
|
|
231
|
+
if (name === '' || name === '.' || name === '..' || /[\\/]/.test(name)) return [];
|
|
232
|
+
const found = new Set();
|
|
233
|
+
for (const root of localModelRoots(settingsValue, instanceId)) {
|
|
234
|
+
const dir = join(root, name);
|
|
235
|
+
const walk = (current, prefix, depth) => {
|
|
236
|
+
// 模型树很浅;这个上限防的是符号链接环把请求挂死。
|
|
237
|
+
if (depth > 6) return;
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
241
|
+
} catch {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
if (entry.isDirectory()) {
|
|
246
|
+
if (/^(\.git|__pycache__)$/i.test(entry.name)) continue;
|
|
247
|
+
walk(join(current, entry.name), `${prefix}${entry.name}/`, depth + 1);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!entry.isFile()) continue;
|
|
251
|
+
// 每个模型目录都带一个零字节的 `put_xxx_here` 说明文件,那不是模型。
|
|
252
|
+
if (/^put_.*_here$/i.test(entry.name)) continue;
|
|
253
|
+
if (!MODEL_FILE_EXT.test(entry.name)) continue;
|
|
254
|
+
found.add(`${prefix}${entry.name}`);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
walk(dir, '', 0);
|
|
258
|
+
}
|
|
259
|
+
return [...found].sort();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 完整清单:目录 → 文件,带每个文件的大小。 */
|
|
263
|
+
export function modelInventory(settingsValue, options = {}) {
|
|
264
|
+
const instanceId = options.instanceId;
|
|
265
|
+
const roots = localModelRoots(settingsValue, instanceId);
|
|
266
|
+
const entries = localModelRootEntries(settingsValue, instanceId);
|
|
267
|
+
const enabledIds = (Array.isArray(settingsValue?.instances) ? settingsValue.instances : [])
|
|
268
|
+
.filter((row) => row !== null && typeof row === 'object' && row.enabled !== false)
|
|
269
|
+
.map((row) => String(row.id ?? ''))
|
|
270
|
+
.filter((id) => id !== '');
|
|
271
|
+
const scoped = instanceId !== undefined && instanceId !== null && instanceId !== '';
|
|
272
|
+
const where = scoped ? `实例 ${instanceId} 没有可读的模型目录(在模型文件夹地址里填一个绝对路径)` : '没有可读的本地模型目录(在实例里填模型文件夹地址,或加一个扫描目录)';
|
|
273
|
+
if (roots.length === 0) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
error: where,
|
|
277
|
+
roots,
|
|
278
|
+
rootEntries: entries,
|
|
279
|
+
folders: [],
|
|
280
|
+
total: 0,
|
|
281
|
+
instanceId: scoped ? instanceId : null,
|
|
282
|
+
instances: enabledIds.map((id) => ({ id, label: id })),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const withSize = options.withSize === true;
|
|
286
|
+
const folders = [];
|
|
287
|
+
for (const [name, label] of modelFolders(settingsValue, instanceId)) {
|
|
288
|
+
const items = modelsInFolder(settingsValue, name, instanceId);
|
|
289
|
+
// 空目录是纯噪声:点不出任何东西。
|
|
290
|
+
if (items.length === 0) continue;
|
|
291
|
+
folders.push({
|
|
292
|
+
name,
|
|
293
|
+
label,
|
|
294
|
+
items: withSize ? items.map((item) => ({ name: item, size: sizeOf(roots, name, item) })) : items,
|
|
295
|
+
instances: scoped ? [instanceId] : enabledIds,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
folders.sort((left, right) => right.items.length - left.items.length);
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
source: 'disk',
|
|
302
|
+
roots,
|
|
303
|
+
rootEntries: entries,
|
|
304
|
+
instanceId: scoped ? instanceId : null,
|
|
305
|
+
instances: enabledIds.map((id) => ({ id, label: id })),
|
|
306
|
+
folders,
|
|
307
|
+
total: folders.reduce((sum, folder) => sum + folder.items.length, 0),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sizeOf(roots, folder, relative) {
|
|
312
|
+
for (const root of roots) {
|
|
313
|
+
try {
|
|
314
|
+
return statSync(join(root, folder, relative)).size;
|
|
315
|
+
} catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export { MODEL_FILE_EXT, MODELS_FOLDER_NAMES, FALLBACK_FOLDERS };
|