dsh-comfyui 0.4.0 → 0.5.0
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.en.md +3 -1
- package/README.md +3 -1
- package/client/client.js +640 -17
- package/client/client.js.map +1 -1
- package/lib/routes.js +181 -0
- package/lib/skill.js +1 -0
- package/lib/skillpack.d.ts +29 -2
- package/lib/skillpack.js +73 -5
- package/lib/transfer.d.ts +158 -0
- package/lib/transfer.js +460 -0
- package/package.json +3 -2
package/lib/transfer.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow preset transfer: export library workflows (API format) together
|
|
3
|
+
* with their parameters and skill packs into one .zip archive, and import
|
|
4
|
+
* such an archive back as NEW workflows.
|
|
5
|
+
*
|
|
6
|
+
* Why a zip: skill packs carry binary assets, and a zip stores them as-is
|
|
7
|
+
* (no base64 inflation) while keeping the pack layout 1:1 — importing a pack
|
|
8
|
+
* is close to restoring a directory. `preset.json` alone carries everything
|
|
9
|
+
* textual (workflows, parameters, skill file lists), so the panel can
|
|
10
|
+
* analyze a package by reading one small entry before anything is written.
|
|
11
|
+
*
|
|
12
|
+
* Security posture: nothing inside the archive is trusted. Filesystem paths
|
|
13
|
+
* are never taken from zip entry names — the pack root is re-derived from
|
|
14
|
+
* the imported workflow's own slug (`skillSlug(newName, newId)`), and every
|
|
15
|
+
* pack-relative path from preset.json re-passes `parseSkillPath` plus the
|
|
16
|
+
* `writeBytes` containment/cap checks inside the pack store. Import always
|
|
17
|
+
* creates new workflow records, so a hostile package cannot overwrite an
|
|
18
|
+
* existing one; its worst outcome is junk that the user deletes.
|
|
19
|
+
*/
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
|
|
22
|
+
import { validateWorkflow } from './store.js';
|
|
23
|
+
import { parseSkillPath, sizeLimitOf } from './skillpack.js';
|
|
24
|
+
/** Marker plus layout version of the packages this module writes. */
|
|
25
|
+
export const PRESET_FORMAT = 'dsh-comfyui-workflow-preset';
|
|
26
|
+
export const PRESET_VERSION = 1;
|
|
27
|
+
/** Manifest file name inside the archive. */
|
|
28
|
+
const MANIFEST_ENTRY = 'preset.json';
|
|
29
|
+
/** Zip prefix under which skill-pack files are archived, keyed by source id. */
|
|
30
|
+
const SKILL_PREFIX = 'skills/';
|
|
31
|
+
/** Hard cap on the uploaded archive (route-level check before parsing). */
|
|
32
|
+
export const MAX_IMPORT_BYTES = 256 * 1024 * 1024;
|
|
33
|
+
/** Decompression bombs: refuse single entries and totals beyond these. */
|
|
34
|
+
const MAX_ENTRY_BYTES = 64 * 1024 * 1024;
|
|
35
|
+
const MAX_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
36
|
+
/** A package is a curated bundle, not a library dump. */
|
|
37
|
+
const MAX_PACKAGE_WORKFLOWS = 500;
|
|
38
|
+
function fail(error) {
|
|
39
|
+
return { ok: false, error };
|
|
40
|
+
}
|
|
41
|
+
/** Zip entry name for one archived pack file. */
|
|
42
|
+
function packEntry(sourceId, path) {
|
|
43
|
+
return `${SKILL_PREFIX}${sourceId}/${path}`;
|
|
44
|
+
}
|
|
45
|
+
/** Local-time stamp for the download file name (ASCII on purpose: it goes
|
|
46
|
+
* into a Content-Disposition header without RFC 5987 encoding). Milliseconds
|
|
47
|
+
* are included so two exports within the same second never share a name —
|
|
48
|
+
* download managers dedupe on file name too. */
|
|
49
|
+
function stamp() {
|
|
50
|
+
const d = new Date();
|
|
51
|
+
const p = (n, width = 2) => String(n).padStart(width, '0');
|
|
52
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${p(d.getMilliseconds(), 3)}`;
|
|
53
|
+
}
|
|
54
|
+
/** The plugin's own version, recorded in the manifest for troubleshooting.
|
|
55
|
+
* Resolved relative to this file so the published package works; anything
|
|
56
|
+
* unusual just degrades to an empty string. */
|
|
57
|
+
function pluginVersion() {
|
|
58
|
+
try {
|
|
59
|
+
const require = createRequire(import.meta.url);
|
|
60
|
+
const pkg = require('../package.json');
|
|
61
|
+
return typeof pkg.version === 'string' ? pkg.version : '';
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Inflate with memory guards: entries beyond the per-file cap are dropped
|
|
68
|
+
* (and later reported as missing), and once the running total exceeds the
|
|
69
|
+
* package cap nothing more is inflated. */
|
|
70
|
+
function unzipBounded(data) {
|
|
71
|
+
let total = 0;
|
|
72
|
+
return unzipSync(data, {
|
|
73
|
+
filter: (file) => {
|
|
74
|
+
total += file.originalSize;
|
|
75
|
+
return file.originalSize <= MAX_ENTRY_BYTES && total <= MAX_TOTAL_BYTES;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build one preset package from the selected library workflows.
|
|
81
|
+
* @param host - the runtime face (workflow store + skill packs).
|
|
82
|
+
* @param ids - workflow ids to export; unknown ids are skipped with a warning.
|
|
83
|
+
*/
|
|
84
|
+
export async function buildExportPackage(host, ids) {
|
|
85
|
+
if (!Array.isArray(ids))
|
|
86
|
+
return fail('ids 必须是数组');
|
|
87
|
+
const wanted = [...new Set(ids.filter((id) => typeof id === 'string' && id !== ''))];
|
|
88
|
+
if (wanted.length === 0)
|
|
89
|
+
return fail('请先选择要导出的工作流');
|
|
90
|
+
const zip = {};
|
|
91
|
+
const exported = [];
|
|
92
|
+
const warnings = [];
|
|
93
|
+
for (const id of wanted) {
|
|
94
|
+
const stored = await host.getWorkflow(id);
|
|
95
|
+
if (stored === undefined) {
|
|
96
|
+
warnings.push(`工作流不存在,已跳过:${id}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const entry = {
|
|
100
|
+
id: stored.id,
|
|
101
|
+
name: stored.name,
|
|
102
|
+
description: stored.description,
|
|
103
|
+
tags: stored.tags !== undefined && stored.tags.length > 0 ? [...stored.tags] : [],
|
|
104
|
+
parameters: stored.parameters !== undefined && stored.parameters.length > 0 ? stored.parameters : [],
|
|
105
|
+
requireSkill: stored.requireSkill === true,
|
|
106
|
+
skill: null,
|
|
107
|
+
workflow: stored.workflow,
|
|
108
|
+
};
|
|
109
|
+
if (stored.skillDir !== undefined && stored.skillDir !== '') {
|
|
110
|
+
const pack = await host.skillPacks.info(stored.id);
|
|
111
|
+
if (pack === undefined) {
|
|
112
|
+
warnings.push(`技能包目录不可读,未随包导出:${stored.name}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const files = [];
|
|
116
|
+
for (const file of pack.files) {
|
|
117
|
+
const raw = await host.skillPacks.readRaw(stored.id, file.path);
|
|
118
|
+
if (!raw.ok) {
|
|
119
|
+
warnings.push(`技能包文件读取失败,已跳过:${stored.name} / ${file.path}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
zip[packEntry(stored.id, file.path)] = Uint8Array.from(raw.value.bytes);
|
|
123
|
+
files.push({ path: file.path, size: file.size });
|
|
124
|
+
}
|
|
125
|
+
if (files.length > 0) {
|
|
126
|
+
entry.skill = { files, dirs: [...pack.dirs] };
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
warnings.push(`技能包没有可读取的文件,未随包导出:${stored.name}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exported.push(entry);
|
|
134
|
+
}
|
|
135
|
+
if (exported.length === 0)
|
|
136
|
+
return fail(warnings.length > 0 ? warnings.join(';') : '没有可导出的工作流');
|
|
137
|
+
const manifest = {
|
|
138
|
+
format: PRESET_FORMAT,
|
|
139
|
+
version: PRESET_VERSION,
|
|
140
|
+
exportedAt: new Date().toISOString(),
|
|
141
|
+
pluginVersion: pluginVersion(),
|
|
142
|
+
count: exported.length,
|
|
143
|
+
workflows: exported,
|
|
144
|
+
};
|
|
145
|
+
zip[MANIFEST_ENTRY] = strToU8(JSON.stringify(manifest, null, 2));
|
|
146
|
+
const bytes = zipSync(zip);
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
bytes,
|
|
150
|
+
filename: `dsh-comfyui-presets-${stamp()}.zip`,
|
|
151
|
+
count: exported.length,
|
|
152
|
+
names: exported.map((entry) => entry.name),
|
|
153
|
+
warnings,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Structural check of preset.json; returns the normalized manifest or why it was refused. */
|
|
157
|
+
function parseManifest(value) {
|
|
158
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
159
|
+
return 'preset.json 结构不正确';
|
|
160
|
+
const raw = value;
|
|
161
|
+
if (raw.format !== PRESET_FORMAT)
|
|
162
|
+
return '这不是 dsh-comfyui 工作流预设包';
|
|
163
|
+
const version = raw.version;
|
|
164
|
+
if (typeof version !== 'number' || !Number.isInteger(version) || version < 1)
|
|
165
|
+
return '预设包版本号不正确';
|
|
166
|
+
if (version > PRESET_VERSION)
|
|
167
|
+
return `预设包版本过新(v${version}),请升级插件后再导入`;
|
|
168
|
+
const workflowsRaw = raw.workflows;
|
|
169
|
+
if (!Array.isArray(workflowsRaw))
|
|
170
|
+
return '预设包里没有工作流清单';
|
|
171
|
+
if (workflowsRaw.length > MAX_PACKAGE_WORKFLOWS)
|
|
172
|
+
return `预设包包含的工作流过多(上限 ${MAX_PACKAGE_WORKFLOWS} 个)`;
|
|
173
|
+
const workflows = [];
|
|
174
|
+
for (const item of workflowsRaw) {
|
|
175
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
176
|
+
continue;
|
|
177
|
+
const w = item;
|
|
178
|
+
if (typeof w.id !== 'string' || w.id === '')
|
|
179
|
+
continue;
|
|
180
|
+
if (typeof w.name !== 'string' || w.name === '')
|
|
181
|
+
continue;
|
|
182
|
+
if (typeof w.workflow !== 'object' || w.workflow === null || Array.isArray(w.workflow))
|
|
183
|
+
continue;
|
|
184
|
+
workflows.push({
|
|
185
|
+
id: w.id,
|
|
186
|
+
name: w.name,
|
|
187
|
+
description: typeof w.description === 'string' ? w.description : '',
|
|
188
|
+
tags: Array.isArray(w.tags) ? w.tags.filter((tag) => typeof tag === 'string') : [],
|
|
189
|
+
parameters: Array.isArray(w.parameters) ? w.parameters : [],
|
|
190
|
+
requireSkill: w.requireSkill === true,
|
|
191
|
+
skill: parseSkillRef(w.skill),
|
|
192
|
+
workflow: w.workflow,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (workflows.length === 0)
|
|
196
|
+
return '预设包里没有结构完整的工作流';
|
|
197
|
+
return {
|
|
198
|
+
format: PRESET_FORMAT,
|
|
199
|
+
version,
|
|
200
|
+
exportedAt: typeof raw.exportedAt === 'string' ? raw.exportedAt : '',
|
|
201
|
+
pluginVersion: typeof raw.pluginVersion === 'string' ? raw.pluginVersion : '',
|
|
202
|
+
count: workflows.length,
|
|
203
|
+
workflows,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function parseSkillRef(value) {
|
|
207
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
208
|
+
return null;
|
|
209
|
+
const raw = value;
|
|
210
|
+
const filesRaw = raw.files;
|
|
211
|
+
if (!Array.isArray(filesRaw))
|
|
212
|
+
return null;
|
|
213
|
+
const files = [];
|
|
214
|
+
for (const item of filesRaw) {
|
|
215
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
216
|
+
continue;
|
|
217
|
+
const f = item;
|
|
218
|
+
if (typeof f.path !== 'string' || f.path === '')
|
|
219
|
+
continue;
|
|
220
|
+
files.push({ path: f.path, size: typeof f.size === 'number' ? f.size : 0 });
|
|
221
|
+
}
|
|
222
|
+
const dirs = Array.isArray(raw.dirs)
|
|
223
|
+
? raw.dirs.filter((dir) => typeof dir === 'string' && dir !== '')
|
|
224
|
+
: [];
|
|
225
|
+
return files.length > 0 || dirs.length > 0 ? { files, dirs } : null;
|
|
226
|
+
}
|
|
227
|
+
/** Parse and validate one uploaded archive without touching the disk. */
|
|
228
|
+
function readPackage(bytes) {
|
|
229
|
+
if (bytes.length === 0)
|
|
230
|
+
return fail('上传内容为空');
|
|
231
|
+
let files;
|
|
232
|
+
try {
|
|
233
|
+
files = unzipBounded(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
return fail(`无法读取预设包:${error instanceof Error ? error.message : String(error)}`);
|
|
237
|
+
}
|
|
238
|
+
const raw = files[MANIFEST_ENTRY];
|
|
239
|
+
if (raw === undefined)
|
|
240
|
+
return fail(`预设包缺少 ${MANIFEST_ENTRY},不是有效的预设包`);
|
|
241
|
+
let manifest;
|
|
242
|
+
try {
|
|
243
|
+
manifest = JSON.parse(strFromU8(raw));
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return fail(`${MANIFEST_ENTRY} 不是有效的 JSON`);
|
|
247
|
+
}
|
|
248
|
+
const parsed = parseManifest(manifest);
|
|
249
|
+
return typeof parsed === 'string' ? fail(parsed) : { ok: true, manifest: parsed, files };
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Analyze one uploaded package: list the workflows it offers, with per-item
|
|
253
|
+
* warnings for anything that would fail at apply time. No disk writes.
|
|
254
|
+
*/
|
|
255
|
+
export function analyzeImportPackage(bytes) {
|
|
256
|
+
const read = readPackage(bytes);
|
|
257
|
+
if (!read.ok)
|
|
258
|
+
return read;
|
|
259
|
+
const { manifest, files } = read;
|
|
260
|
+
const workflows = manifest.workflows.map((entry, index) => {
|
|
261
|
+
const warnings = [];
|
|
262
|
+
const problem = validateWorkflow(entry.workflow);
|
|
263
|
+
if (problem !== undefined)
|
|
264
|
+
warnings.push(problem);
|
|
265
|
+
let skill = null;
|
|
266
|
+
if (entry.skill !== null) {
|
|
267
|
+
for (const file of entry.skill.files) {
|
|
268
|
+
// Flag the grammar problem at analyze time (visible before anything
|
|
269
|
+
// is written); apply re-checks and skips the same paths regardless.
|
|
270
|
+
if (parseSkillPath(file.path).ok === false)
|
|
271
|
+
warnings.push(`技能包路径不合法:${file.path}`);
|
|
272
|
+
else if (files[packEntry(entry.id, file.path)] === undefined)
|
|
273
|
+
warnings.push(`包内缺少技能包文件:${file.path}`);
|
|
274
|
+
}
|
|
275
|
+
skill = {
|
|
276
|
+
fileCount: entry.skill.files.length,
|
|
277
|
+
totalBytes: entry.skill.files.reduce((sum, file) => sum + file.size, 0),
|
|
278
|
+
required: entry.requireSkill === true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
index,
|
|
283
|
+
id: entry.id,
|
|
284
|
+
name: entry.name,
|
|
285
|
+
description: entry.description,
|
|
286
|
+
tags: entry.tags ?? [],
|
|
287
|
+
paramCount: (entry.parameters ?? []).length,
|
|
288
|
+
skill,
|
|
289
|
+
warnings,
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
analysis: {
|
|
295
|
+
version: manifest.version,
|
|
296
|
+
exportedAt: manifest.exportedAt,
|
|
297
|
+
pluginVersion: manifest.pluginVersion,
|
|
298
|
+
workflows,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Light shape check on parameters: a malformed entry is dropped, a valid one
|
|
303
|
+
* passes through untouched so future fields survive the round trip. */
|
|
304
|
+
function sanitizeParameters(value) {
|
|
305
|
+
if (!Array.isArray(value))
|
|
306
|
+
return [];
|
|
307
|
+
const out = [];
|
|
308
|
+
for (const item of value) {
|
|
309
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
310
|
+
continue;
|
|
311
|
+
const raw = item;
|
|
312
|
+
const valid = typeof raw.id === 'string' && raw.id !== '' &&
|
|
313
|
+
typeof raw.name === 'string' && raw.name !== '' &&
|
|
314
|
+
(raw.type === 'string' || raw.type === 'number' || raw.type === 'boolean') &&
|
|
315
|
+
typeof raw.nodeId === 'string' && raw.nodeId !== '' &&
|
|
316
|
+
typeof raw.inputKey === 'string' && raw.inputKey !== '' &&
|
|
317
|
+
(typeof raw.default === 'string' || typeof raw.default === 'number' || typeof raw.default === 'boolean');
|
|
318
|
+
if (valid)
|
|
319
|
+
out.push(item);
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/** First free variant of a name; the importer never overwrites, so a clash
|
|
324
|
+
* just mints the next suffix. The set also tracks names minted during this
|
|
325
|
+
* run, so two same-named entries in one package cannot collide either. */
|
|
326
|
+
function uniqueName(used, base) {
|
|
327
|
+
const clean = base.trim() === '' ? 'unnamed-workflow' : base.trim();
|
|
328
|
+
if (!used.has(clean)) {
|
|
329
|
+
used.add(clean);
|
|
330
|
+
return clean;
|
|
331
|
+
}
|
|
332
|
+
const marked = `${clean}(导入)`;
|
|
333
|
+
if (!used.has(marked)) {
|
|
334
|
+
used.add(marked);
|
|
335
|
+
return marked;
|
|
336
|
+
}
|
|
337
|
+
for (let i = 2;; i++) {
|
|
338
|
+
const candidate = `${clean}(导入 ${i})`;
|
|
339
|
+
if (!used.has(candidate)) {
|
|
340
|
+
used.add(candidate);
|
|
341
|
+
return candidate;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** Restore one archived skill pack under the freshly created workflow. Files
|
|
346
|
+
* go through the same pack-store paths as a manual edit (grammar, extension
|
|
347
|
+
* whitelist, size caps), so an out-of-spec package fails per file with a
|
|
348
|
+
* reason instead of poisoning the pack. */
|
|
349
|
+
async function importSkillPack(host, newId, sourceId, skill, required, files, warnings) {
|
|
350
|
+
const enabled = await host.skillPacks.enable(newId);
|
|
351
|
+
if (!enabled.ok) {
|
|
352
|
+
warnings.push(`技能包挂载失败:${enabled.error}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// One batched write for the whole pack: the store checks its limits once
|
|
356
|
+
// and enumerates the directory twice (before/after) instead of per file —
|
|
357
|
+
// a 1000-file pack under per-file checks was minutes of filesystem churn
|
|
358
|
+
// and read as a frozen import. Oversized or ungrammatical entries are
|
|
359
|
+
// skipped here with a warning, so one bad file never sinks the batch.
|
|
360
|
+
const entries = [];
|
|
361
|
+
for (const file of skill.files) {
|
|
362
|
+
const parsed = parseSkillPath(file.path);
|
|
363
|
+
if (!parsed.ok) {
|
|
364
|
+
warnings.push(`技能包路径不合法,已跳过:${file.path}`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const entry = files[packEntry(sourceId, file.path)];
|
|
368
|
+
if (entry === undefined) {
|
|
369
|
+
warnings.push(`包内缺少技能包文件:${file.path}`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const bytes = Buffer.from(entry);
|
|
373
|
+
if (bytes.length > sizeLimitOf(parsed.value.file)) {
|
|
374
|
+
warnings.push(`技能包文件超过大小上限,已跳过:${file.path}(${Math.ceil(bytes.length / 1024)} KB)`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
entries.push({ path: file.path, bytes });
|
|
378
|
+
}
|
|
379
|
+
if (entries.length === 0) {
|
|
380
|
+
await host.skillPacks.disable(newId);
|
|
381
|
+
warnings.push('技能包没有一个文件写入成功,已放弃挂载');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const written = await host.skillPacks.importFiles(newId, entries);
|
|
385
|
+
if (!written.ok) {
|
|
386
|
+
await host.skillPacks.disable(newId);
|
|
387
|
+
warnings.push(`技能包还原失败,已放弃挂载:${written.error}`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const dir of skill.dirs) {
|
|
391
|
+
const made = await host.skillPacks.makeDir(newId, dir);
|
|
392
|
+
if (!made.ok)
|
|
393
|
+
warnings.push(`技能包子目录创建失败:${dir}`);
|
|
394
|
+
}
|
|
395
|
+
if (required) {
|
|
396
|
+
const gate = await host.skillPacks.setRequired(newId, true);
|
|
397
|
+
if (!gate.ok)
|
|
398
|
+
warnings.push(`必读标记设置失败:${gate.error}`);
|
|
399
|
+
}
|
|
400
|
+
// One limit tripping on every remaining file reads as an error wall that
|
|
401
|
+
// buries the row and the user's trust with it — keep the first few and
|
|
402
|
+
// summarize the rest. The full list still lands in the host log.
|
|
403
|
+
if (warnings.length > 12) {
|
|
404
|
+
const dropped = warnings.length - 12;
|
|
405
|
+
warnings.length = 12;
|
|
406
|
+
warnings.push(`……其余 ${dropped} 条警告已省略`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Import the selected workflows from one uploaded package. Every workflow
|
|
411
|
+
* becomes a NEW library record (fresh id, name suffixed on clash); skill
|
|
412
|
+
* packs land in freshly slugged directories, so nothing on disk is reused
|
|
413
|
+
* or overwritten.
|
|
414
|
+
* @param selected - manifest indexes to import (as listed by analyze).
|
|
415
|
+
*/
|
|
416
|
+
export async function applyImportPackage(host, bytes, selected) {
|
|
417
|
+
const read = readPackage(bytes);
|
|
418
|
+
if (!read.ok)
|
|
419
|
+
return read;
|
|
420
|
+
const { manifest, files } = read;
|
|
421
|
+
if (!Array.isArray(selected))
|
|
422
|
+
return fail('请先选择要导入的工作流');
|
|
423
|
+
const wanted = [...new Set(selected.filter((index) => typeof index === 'number' && Number.isInteger(index) && index >= 0 && index < manifest.workflows.length))];
|
|
424
|
+
if (wanted.length === 0)
|
|
425
|
+
return fail('请先选择要导入的工作流');
|
|
426
|
+
const usedNames = new Set((await host.listWorkflows()).map((workflow) => workflow.name));
|
|
427
|
+
const results = [];
|
|
428
|
+
let imported = 0;
|
|
429
|
+
for (const index of wanted) {
|
|
430
|
+
const entry = manifest.workflows[index];
|
|
431
|
+
const outcome = { index, name: entry.name, ok: false, warnings: [] };
|
|
432
|
+
const problem = validateWorkflow(entry.workflow);
|
|
433
|
+
if (problem !== undefined) {
|
|
434
|
+
outcome.error = problem;
|
|
435
|
+
results.push(outcome);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const saved = await host.saveWorkflow({
|
|
439
|
+
name: uniqueName(usedNames, entry.name),
|
|
440
|
+
description: entry.description,
|
|
441
|
+
workflow: entry.workflow,
|
|
442
|
+
parameters: sanitizeParameters(entry.parameters),
|
|
443
|
+
tags: entry.tags,
|
|
444
|
+
});
|
|
445
|
+
if (!saved.ok) {
|
|
446
|
+
outcome.error = saved.error;
|
|
447
|
+
results.push(outcome);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
outcome.ok = true;
|
|
451
|
+
outcome.newName = saved.workflow.name;
|
|
452
|
+
outcome.newId = saved.workflow.id;
|
|
453
|
+
imported++;
|
|
454
|
+
if (entry.skill !== null) {
|
|
455
|
+
await importSkillPack(host, saved.workflow.id, entry.id, entry.skill, entry.requireSkill === true, files, outcome.warnings);
|
|
456
|
+
}
|
|
457
|
+
results.push(outcome);
|
|
458
|
+
}
|
|
459
|
+
return { ok: true, results, imported };
|
|
460
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Let the DeepSeek Harness agent smartly drive a local or remote ComfyUI to generate anything, with workflow and asset management panels, per-workflow skill packs, a companion skill and a same-origin media proxy. / 让 DeepSeek Harness 的 Agent 智能驱动本地或远程 ComfyUI 生成任何内容。附带工作流、资产管理面板与技能包管理挂载。配套 skill 与同源媒体代理。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"node": ">=22.19"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@deepseek-ai/schemastery": "^3.18.0"
|
|
52
|
+
"@deepseek-ai/schemastery": "^3.18.0",
|
|
53
|
+
"fflate": "^0.8.3"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"@deepseek-ai/cordis": "^4.0.1",
|