The overview's models are one compact list (preview, name, node, here under the saved name or missing, size, note, download page). The recipe's own match / apply-match flow, identity badges, advanced path and hash blocks, origin editor and recipe fingerprint are gone; Model Check finds renamed or moved models once the workflow is on the canvas, and saving the recipe again keeps it. ui_recipe_model_matching.js becomes ui_recipe_models.js; ui_recipe_detail.js drops from 776 to 292 lines. Plain wording for the save dialog, empty page and status; no per-node save-to-library buttons on the parameters page. Recipe CSS rules for classes no script uses (107 rules) are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
2.6 KiB
JavaScript
75 lines
2.6 KiB
JavaScript
/** UI-neutral helpers for Workflow Recipe model provenance records. */
|
|
|
|
const MODEL_FILE_PATTERN = /\.(?:safetensors|ckpt|pt|bin|sft)$/i;
|
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/i;
|
|
|
|
function cloneJson(value) {
|
|
if (value === undefined) return undefined;
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function pathVariants(value) {
|
|
if (typeof value !== 'string') return [];
|
|
return [...new Set([
|
|
value,
|
|
value.replace(/\\/g, '/'),
|
|
value.replace(/\//g, '\\'),
|
|
])];
|
|
}
|
|
|
|
function nodeHashKeys(nodeId, value) {
|
|
return pathVariants(value).map((variant) => `${nodeId}_${variant}`);
|
|
}
|
|
|
|
function provenanceMap(workflow) {
|
|
const hashes = workflow?.extra?.anomalous_hashes;
|
|
return hashes && typeof hashes === 'object' && !Array.isArray(hashes) ? hashes : null;
|
|
}
|
|
|
|
export function findWorkflowHashRecord(workflow, nodeId, value) {
|
|
const hashes = provenanceMap(workflow);
|
|
if (!hashes) return null;
|
|
const keys = [
|
|
...nodeHashKeys(nodeId, value),
|
|
...pathVariants(value),
|
|
];
|
|
for (const key of keys) {
|
|
if (Object.prototype.hasOwnProperty.call(hashes, key)) return cloneJson(hashes[key]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function writeNodeHashRecord(hashes, nodeId, value, record) {
|
|
for (const key of nodeHashKeys(nodeId, value)) hashes[key] = cloneJson(record);
|
|
}
|
|
|
|
/** Build node-ID-remapped provenance for appending a partial recipe. */
|
|
export function remapWorkflowHashRecords(workflow, idMap) {
|
|
const remapped = {};
|
|
if (!(idMap instanceof Map) || !provenanceMap(workflow)) return remapped;
|
|
for (const node of workflow?.nodes || []) {
|
|
const newId = idMap.get(String(node?.id));
|
|
if (newId === undefined || !Array.isArray(node?.widgets_values)) continue;
|
|
for (const value of node.widgets_values) {
|
|
if (typeof value !== 'string' || !MODEL_FILE_PATTERN.test(value)) continue;
|
|
const record = findWorkflowHashRecord(workflow, node.id, value);
|
|
if (record !== null) writeNodeHashRecord(remapped, newId, value, record);
|
|
}
|
|
}
|
|
return remapped;
|
|
}
|
|
|
|
/** Merge remapped recipe provenance into the live graph without deleting host data. */
|
|
export function mergeRecipeHashRecords(graph, workflow, idMap) {
|
|
const additions = remapWorkflowHashRecords(workflow, idMap);
|
|
const keys = Object.keys(additions);
|
|
if (!keys.length) return 0;
|
|
if (!graph.extra || typeof graph.extra !== 'object') graph.extra = {};
|
|
if (!graph.extra.anomalous_hashes || typeof graph.extra.anomalous_hashes !== 'object') {
|
|
graph.extra.anomalous_hashes = {};
|
|
}
|
|
Object.assign(graph.extra.anomalous_hashes, additions);
|
|
return keys.length;
|
|
}
|
|
|