Files
DemonGatanjieu-Anomalous_Mo…/web/modules/recipe_provenance.js
T
DemonGatanjieuandClaude Opus 5.5 a93ed042d2 Slim the recipe detail and leave model recovery to Model Check
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>
2026-10-03 16:41:51 +08:00

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;
}