Improve wizard mapping and quality guidance

This commit is contained in:
github-ops
2026-06-23 21:03:25 +00:00
parent 052b9356ec
commit 04f63e690c
7 changed files with 1228 additions and 24 deletions
+95
View File
@@ -13,10 +13,17 @@ function buildToolDescription() {
}
function buildWizardState() {
var snapshot = wizardCurrentVersion && wizardCurrentVersion.snapshot
? wizardCurrentVersion.snapshot
: wizardCurrentVersion;
var existingState = snapshot && snapshot.wizard_state ? snapshot.wizard_state : {};
return {
input_sample: parseStructuredText(textValue('wizard-input-sample')),
output_sample: parseStructuredText(textValue('wizard-output-sample')),
test_input: parseStructuredText(textValue('wizard-test-input')),
import_findings: Array.isArray(existingState.import_findings)
? existingState.import_findings.slice()
: [],
};
}
@@ -24,6 +31,10 @@ function collectWizardPayload() {
var name = textValue('tool-name');
if (!name) throw new Error(tKey('wizard.error.tool_name'));
if (window.CrankWizardMapping && typeof window.CrankWizardMapping.sync === 'function') {
window.CrankWizardMapping.sync();
}
var inputSchemaValue = parseStructuredText(textValue('tool-input-schema'));
var outputSchemaValue = parseStructuredText(textValue('tool-output-schema'));
var inputMappingValue = parseStructuredText(textValue('tool-input-mapping'));
@@ -69,6 +80,7 @@ async function loadOperationForEdit() {
wizardCurrentVersion = draftVersion;
prefillWizardFromEdit(detail, draftVersion);
renderAgentFacingPreview();
renderImportQualityFindings(draftVersion);
}
function setEditModePresentation() {
@@ -111,6 +123,9 @@ function bindWizardLiveActions() {
var yamlInput = document.getElementById('wizard-import-yaml-file');
if (yamlInput) yamlInput.addEventListener('change', handleImportYamlFileSelection);
if (window.CrankWizardMapping && typeof window.CrankWizardMapping.initialize === 'function') {
window.CrankWizardMapping.initialize();
}
bindAgentFacingPreview();
}
@@ -421,6 +436,50 @@ function severityLabel(severity) {
return tKey('wizard.quality.severity_info');
}
function qualityTargetForFinding(finding) {
var code = String(finding && finding.code || '');
var path = String(finding && finding.field_path || '');
if (code.indexOf('tool_name') >= 0 || code.indexOf('weak_tool_name') >= 0) {
return { step: 4, selector: '#tool-name', label: 'Перейти к имени инструмента' };
}
if (code.indexOf('description') >= 0 || path.indexOf('tool_description') >= 0) {
return { step: 4, selector: '#tool-description', label: 'Перейти к описанию' };
}
if (code.indexOf('input') >= 0 || code.indexOf('parameter') >= 0 || path.indexOf('input_schema') >= 0) {
return { step: 4, selector: '#tool-input-schema', label: 'Перейти к входной схеме' };
}
if (code.indexOf('output') >= 0 || code.indexOf('response_schema') >= 0 || path.indexOf('output_schema') >= 0) {
return { step: 4, selector: '#tool-output-schema', label: 'Перейти к схеме ответа' };
}
if (code.indexOf('response_projection') >= 0 || code.indexOf('mapping') >= 0 || path.indexOf('output_mapping') >= 0) {
return { step: 5, selector: '#tool-output-mapping', label: 'Перейти к маппингу ответа' };
}
return null;
}
function focusWizardQualityTarget(target) {
if (!target || !window.CrankWizardShell) return;
var load = typeof window.CrankWizardShell.loadWizardPanels === 'function'
? window.CrankWizardShell.loadWizardPanels([target.step])
: Promise.resolve();
load.then(function() {
if (typeof window.CrankWizardShell.doGoToStep === 'function') {
window.CrankWizardShell.doGoToStep(target.step);
}
window.setTimeout(function() {
var element = document.querySelector(target.selector);
if (!element) return;
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (typeof element.focus === 'function') element.focus();
element.classList.add('quality-focus-target');
window.setTimeout(function() {
element.classList.remove('quality-focus-target');
}, 1600);
}, 80);
});
}
function renderQualityFindings(report) {
var empty = document.getElementById('wizard-quality-empty');
var list = document.getElementById('wizard-quality-findings');
@@ -454,6 +513,19 @@ function renderQualityFindings(report) {
item.appendChild(action);
}
var target = qualityTargetForFinding(finding);
if (target) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'btn-ghost-sm quality-finding-jump';
button.textContent = target.label;
button.addEventListener('click', function(event) {
event.preventDefault();
focusWizardQualityTarget(target);
});
item.appendChild(button);
}
if (finding.field_path) {
var path = document.createElement('div');
path.className = 'quality-finding-path';
@@ -465,6 +537,26 @@ function renderQualityFindings(report) {
});
}
function renderImportQualityFindings(versionDocument) {
var snapshot = versionDocument && versionDocument.snapshot
? versionDocument.snapshot
: versionDocument;
var state = snapshot && snapshot.wizard_state ? snapshot.wizard_state : {};
var findings = Array.isArray(state.import_findings) ? state.import_findings : [];
if (!findings.length) return;
renderQualityFindings({
blocking: findings.some(function(finding) { return finding.severity === 'error'; }),
findings: findings,
});
var empty = document.getElementById('wizard-quality-empty');
if (empty) {
empty.textContent = 'Рекомендации из OpenAPI import. Запустите проверку качества, чтобы пересчитать их по текущему черновику.';
empty.hidden = false;
}
}
async function analyzeWizardQuality() {
if (!wizardWorkspaceId) {
throw new Error(tKey('wizard.error.no_workspace'));
@@ -574,6 +666,9 @@ async function generateDraftFromWizard() {
setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(generated.output_schema), null, 2));
setValue('tool-input-mapping', mappingSetToEditorValue(generated.input_mapping, 'input', wizardProtocol));
setValue('tool-output-mapping', mappingSetToEditorValue(generated.output_mapping, 'output', wizardProtocol));
if (window.CrankWizardMapping && typeof window.CrankWizardMapping.renderFromEditors === 'function') {
window.CrankWizardMapping.renderFromEditors();
}
renderAgentFacingPreview();
showWizardLiveStatus(tKey('wizard.sample.generated'), tKey('wizard.sample.generated_body'));
}
+725
View File
@@ -0,0 +1,725 @@
(function() {
var REQUEST_TARGETS = ['path', 'query', 'headers', 'body'];
var SCALAR_TYPES = ['string', 'number', 'integer', 'boolean', 'null'];
var suspendSync = false;
function field(id) {
return document.getElementById(id);
}
function readText(id) {
var element = field(id);
return element ? element.value.trim() : '';
}
function writeText(id, value) {
var element = field(id);
if (element) element.value = value || '';
}
function parseText(id) {
if (typeof parseStructuredText === 'function') {
return parseStructuredText(readText(id));
}
return JSON.parse(readText(id));
}
function dumpYaml(object) {
return window.jsyaml
? window.jsyaml.dump(object, { lineWidth: -1, noRefs: true })
: JSON.stringify(object, null, 2);
}
function safeJson(id) {
try {
return parseText(id);
} catch (_error) {
return null;
}
}
function scalarType(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
var typeName = typeof value;
if (typeName === 'number') return Number.isInteger(value) ? 'integer' : 'number';
if (typeName === 'boolean') return 'boolean';
if (typeName === 'object') return 'object';
return 'string';
}
function inferJsonSchema(value) {
var typeName = scalarType(value);
if (typeName === 'object') {
var properties = {};
var required = [];
Object.keys(value || {}).forEach(function(name) {
properties[name] = inferJsonSchema(value[name]);
if (value[name] !== null && value[name] !== undefined) required.push(name);
});
var schema = {
type: 'object',
properties: properties,
additionalProperties: false,
};
if (required.length) schema.required = required;
return schema;
}
if (typeName === 'array') {
return {
type: 'array',
items: value.length ? inferJsonSchema(value[0]) : { type: 'string' },
};
}
return { type: typeName };
}
function collectSchemaFields(schema, prefix) {
var result = [];
var node = schema || {};
if (node.type === 'object' && node.properties) {
Object.keys(node.properties).forEach(function(name) {
var child = node.properties[name] || {};
var path = prefix ? prefix + '.' + name : name;
if (child.type === 'object' && child.properties) {
result = result.concat(collectSchemaFields(child, path));
} else {
result.push({
name: path,
required: Array.isArray(node.required) && node.required.indexOf(name) >= 0,
});
}
});
}
return result;
}
function requiredSchemaFields(schema) {
return collectSchemaFields(schema, '').filter(function(item) {
return item.required;
}).map(function(item) {
return item.name;
});
}
function collectJsonLeaves(value, prefix) {
var result = [];
if (Array.isArray(value)) {
if (!value.length) {
if (prefix) result.push({ name: prefix, required: true, array: true });
return result;
}
var samplePath = prefix ? prefix + '[0]' : '[0]';
return collectJsonLeaves(value[0], samplePath).map(function(item) {
item.array = true;
return item;
});
}
if (value && typeof value === 'object') {
Object.keys(value).forEach(function(name) {
var path = prefix ? prefix + '.' + name : name;
var child = value[name];
if (child && typeof child === 'object') {
result = result.concat(collectJsonLeaves(child, path));
} else {
result.push({ name: path, required: child !== null && child !== undefined });
}
});
return result;
}
if (prefix) result.push({ name: prefix, required: true });
return result;
}
function pathParams() {
var path = readText('endpoint-path') || '';
var params = [];
path.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, function(_match, name) {
params.push(name);
return _match;
});
path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, function(_match, name) {
params.push(name);
return _match;
});
return Array.from(new Set(params));
}
function currentMethod() {
var active = document.querySelector('.method-card.active');
return active ? active.dataset.method || 'GET' : 'GET';
}
function defaultTargetKind() {
var method = currentMethod().toUpperCase();
return method === 'GET' || method === 'DELETE' ? 'query' : 'body';
}
function splitRequestTarget(target) {
var value = String(target || '').replace(/^\$\.request\./, '').replace(/^request\./, '');
var parts = value.split('.');
var kind = parts.shift() || defaultTargetKind();
if (REQUEST_TARGETS.indexOf(kind) < 0) kind = defaultTargetKind();
return { kind: kind, name: parts.join('.') };
}
function sourceToField(source) {
return String(source || '')
.replace(/^\$\.mcp\.?/, '')
.replace(/^\$\.input\.?/, '');
}
function responseSourceToPath(source) {
return String(source || '')
.replace(/^\$\.response\.body\.?/, '')
.replace(/^\$\.response\.data\.?/, '');
}
function outputTargetToField(target) {
return String(target || '').replace(/^\$\.output\.?/, '');
}
function mappingEntrySource(entry) {
return entry && typeof entry === 'object' && !Array.isArray(entry)
? entry.source
: entry;
}
function mappingEntryDefault(entry) {
return entry && typeof entry === 'object' && !Array.isArray(entry) && Object.prototype.hasOwnProperty.call(entry, 'default_value')
? entry.default_value
: undefined;
}
function mappingEntryTransform(entry) {
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || entry.transform === undefined || entry.transform === null) {
return '';
}
var value = typeof entry.transform === 'object' ? entry.transform.kind : entry.transform;
value = String(value || '').trim();
return transformKinds().indexOf(value) >= 0 ? value : '';
}
function defaultValueToText(value) {
if (value === undefined) return '';
if (typeof value === 'string') return value;
return JSON.stringify(value);
}
function parseDefaultValue(text) {
var value = String(text || '').trim();
if (!value) return undefined;
if (/^(true|false|null)$/i.test(value) || /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(value) || /^[\[{]/.test(value)) {
try {
return JSON.parse(value);
} catch (_error) {
return value;
}
}
return value;
}
function transformKinds() {
return [
'to_string',
'to_number',
'to_boolean',
'join',
'split',
'wrap_array',
'unwrap_singleton',
];
}
function requestRowsFromEditor() {
var parsed = safeJson('tool-input-mapping') || {};
return Object.keys(parsed).map(function(target) {
var entry = parsed[target];
var source = mappingEntrySource(entry);
var split = splitRequestTarget(target);
return {
input: sourceToField(source) || split.name,
target: split.kind,
apiName: split.name || sourceToField(source),
defaultValue: defaultValueToText(mappingEntryDefault(entry)),
transform: mappingEntryTransform(entry),
};
});
}
function responseRowsFromEditor() {
var parsed = safeJson('tool-output-mapping') || {};
return Object.keys(parsed).map(function(outputField) {
var source = mappingEntrySource(parsed[outputField]);
return {
output: outputField,
responsePath: responseSourceToPath(source) || outputField,
};
});
}
function suggestedRequestRows() {
var rows = [];
pathParams().forEach(function(name) {
rows.push({ input: name, target: 'path', apiName: name });
});
var sample = safeJson('wizard-input-sample');
var fields = [];
if (sample && typeof sample === 'object') {
fields = collectJsonLeaves(sample, '');
}
if (!fields.length) {
fields = collectSchemaFields(safeJson('tool-input-schema'), '');
}
var pathSet = new Set(rows.map(function(row) { return row.input; }));
fields.forEach(function(item) {
if (pathSet.has(item.name)) return;
rows.push({
input: item.name,
target: defaultTargetKind(),
apiName: item.name,
});
});
return rows;
}
function suggestedResponseRows() {
var sample = safeJson('wizard-output-sample');
var fields = sample && typeof sample === 'object'
? collectJsonLeaves(sample, '')
: collectSchemaFields(safeJson('tool-output-schema'), '');
return fields.map(function(item) {
return {
output: outputNameFromPath(item.name),
responsePath: item.name,
};
});
}
function outputNameFromPath(path) {
return String(path || 'value')
.replace(/^\[0\]\.?/, 'item_')
.replace(/\[([0-9]+)\]/g, '_$1')
.replace(/[^A-Za-z0-9_]+/g, '_')
.replace(/^_+|_+$/g, '')
|| 'value';
}
function joinJsonPath(root, relativePath) {
var path = String(relativePath || '').trim();
if (!path) return root;
return path.charAt(0) === '[' ? root + path : root + '.' + path;
}
function makeOption(value, label, selected) {
var option = document.createElement('option');
option.value = value;
option.textContent = label;
option.selected = selected;
return option;
}
function makeInput(value, className, placeholder) {
var input = document.createElement('input');
input.className = className || 'form-input input-mono';
input.value = value === undefined || value === null ? '' : String(value);
input.placeholder = placeholder || '';
input.autocomplete = 'off';
input.spellcheck = false;
input.addEventListener('input', syncVisualMappingsToYaml);
input.addEventListener('change', syncVisualMappingsToYaml);
return input;
}
function makeRemoveButton(row) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'row-btn row-btn-delete mapping-row-remove';
button.title = 'Удалить строку';
button.textContent = '×';
button.addEventListener('click', function() {
row.remove();
syncVisualMappingsToYaml();
});
return button;
}
function renderRequestRows(rows) {
var root = field('wizard-request-mapping-rows');
if (!root) return;
root.replaceChildren();
if (!rows.length) rows = suggestedRequestRows();
rows.forEach(function(row) {
root.appendChild(createRequestRow(row));
});
if (!suspendSync) syncVisualMappingsToYaml();
}
function createRequestRow(row) {
var item = document.createElement('div');
item.className = 'mapping-row request-mapping-row';
item.dataset.mappingRow = 'request';
var input = makeInput(row.input, 'form-input input-mono mapping-source', 'base');
input.dataset.role = 'input';
item.appendChild(input);
var select = document.createElement('select');
select.className = 'form-select mapping-target';
select.dataset.role = 'target';
[
['path', 'Path'],
['query', 'Query'],
['headers', 'Header'],
['body', 'Body'],
].forEach(function(entry) {
select.appendChild(makeOption(entry[0], entry[1], entry[0] === row.target));
});
select.addEventListener('change', syncVisualMappingsToYaml);
item.appendChild(select);
var apiName = makeInput(row.apiName || row.input, 'form-input input-mono mapping-api-name', 'base');
apiName.dataset.role = 'apiName';
item.appendChild(apiName);
var defaultValue = makeInput(row.defaultValue, 'form-input input-mono mapping-default-value', 'по умолчанию');
defaultValue.dataset.role = 'defaultValue';
defaultValue.title = 'Значение по умолчанию, если поле не передано';
item.appendChild(defaultValue);
var transform = document.createElement('select');
transform.className = 'form-select mapping-transform';
transform.dataset.role = 'transform';
transform.appendChild(makeOption('', 'Без преобразования', !row.transform));
transformKinds().forEach(function(kind) {
transform.appendChild(makeOption(kind, kind, kind === row.transform));
});
transform.title = 'Простое преобразование перед отправкой в API';
transform.addEventListener('change', syncVisualMappingsToYaml);
item.appendChild(transform);
item.appendChild(makeRemoveButton(item));
return item;
}
function renderResponseRows(rows) {
var root = field('wizard-response-mapping-rows');
if (!root) return;
root.replaceChildren();
if (!rows.length) rows = suggestedResponseRows();
rows.forEach(function(row) {
root.appendChild(createResponseRow(row));
});
if (!suspendSync) syncVisualMappingsToYaml();
}
function createResponseRow(row) {
var item = document.createElement('div');
item.className = 'mapping-row response-mapping-row';
item.dataset.mappingRow = 'response';
var responsePath = makeInput(row.responsePath, 'form-input input-mono mapping-response-path', 'rates.EUR');
responsePath.dataset.role = 'responsePath';
item.appendChild(responsePath);
var output = makeInput(row.output, 'form-input input-mono mapping-output-field', 'rate');
output.dataset.role = 'output';
item.appendChild(output);
item.appendChild(makeRemoveButton(item));
return item;
}
function valueIn(row, role) {
var input = row.querySelector('[data-role="' + role + '"]');
return input ? input.value.trim() : '';
}
function syncVisualMappingsToYaml() {
var requestRows = document.querySelectorAll('[data-mapping-row="request"]');
if (requestRows.length) {
var requestMapping = {};
requestRows.forEach(function(row) {
var input = valueIn(row, 'input');
var target = valueIn(row, 'target');
var apiName = valueIn(row, 'apiName') || input;
var defaultText = valueIn(row, 'defaultValue');
var transform = valueIn(row, 'transform');
if (!input || !target || !apiName) return;
if (defaultText || transform) {
var entry = { source: '$.input.' + input };
if (defaultText) entry.default_value = parseDefaultValue(defaultText);
if (transform) entry.transform = transform;
requestMapping[target + '.' + apiName] = entry;
return;
}
requestMapping[target + '.' + apiName] = '$.input.' + input;
});
writeText('tool-input-mapping', dumpYaml(requestMapping));
}
var responseRows = document.querySelectorAll('[data-mapping-row="response"]');
if (responseRows.length) {
var responseMapping = {};
responseRows.forEach(function(row) {
var responsePath = valueIn(row, 'responsePath');
var output = valueIn(row, 'output') || outputNameFromPath(responsePath);
if (!responsePath || !output) return;
responseMapping[output] = joinJsonPath('$.response.body', responsePath);
});
writeText('tool-output-mapping', dumpYaml(responseMapping));
}
renderMappingWarnings();
}
function currentRequestMappedInputs() {
return Array.from(document.querySelectorAll('[data-mapping-row="request"]')).map(function(row) {
return valueIn(row, 'input');
}).filter(Boolean);
}
function currentResponseMappedOutputs() {
return Array.from(document.querySelectorAll('[data-mapping-row="response"]')).map(function(row) {
return valueIn(row, 'output');
}).filter(Boolean);
}
function jsonParseWarning(id, label) {
var raw = readText(id);
if (!raw) return null;
try {
parseText(id);
return null;
} catch (error) {
return label + ': JSON не разобран. ' + error.message;
}
}
function buildMappingWarnings() {
var warnings = [];
var inputJsonWarning = jsonParseWarning('wizard-input-sample', 'Пример входных данных');
var outputJsonWarning = jsonParseWarning('wizard-output-sample', 'Пример ответа');
if (inputJsonWarning) warnings.push(inputJsonWarning);
if (outputJsonWarning) warnings.push(outputJsonWarning);
var required = requiredSchemaFields(safeJson('tool-input-schema'));
var mapped = new Set(currentRequestMappedInputs());
var missing = required.filter(function(name) { return !mapped.has(name); });
if (missing.length) {
warnings.push('Обязательные поля входной схемы не отправляются в API: ' + missing.join(', ') + '.');
}
var outputRows = currentResponseMappedOutputs();
if (!outputRows.length) {
warnings.push('Ответ инструмента пустой. Выберите хотя бы одно поле из ответа API.');
}
if (outputRows.length > 12) {
warnings.push('В ответ инструмента выбрано много полей: ' + outputRows.length + '. Лучше вернуть только данные, нужные агенту.');
}
var hasArrayIndex = Array.from(document.querySelectorAll('[data-mapping-row="response"]')).some(function(row) {
return /\[[0-9]+\]/.test(valueIn(row, 'responsePath'));
});
if (hasArrayIndex) {
warnings.push('В ответе выбрано поле из одного элемента массива. Если агенту нужен весь список, верните массив целиком через расширенный YAML-маппинг.');
}
return warnings;
}
function renderMappingWarnings() {
var root = field('wizard-mapping-warnings');
if (!root) return;
var warnings = buildMappingWarnings();
root.replaceChildren();
root.hidden = warnings.length === 0;
warnings.forEach(function(message) {
var item = document.createElement('div');
item.className = 'mapping-warning';
var title = document.createElement('strong');
title.textContent = 'Проверьте маппинг. ';
item.appendChild(title);
item.appendChild(document.createTextNode(message));
root.appendChild(item);
});
}
function renderResponseTree() {
var root = field('wizard-response-json-tree');
if (!root) return;
root.replaceChildren();
var sample = safeJson('wizard-output-sample');
var leaves = sample && typeof sample === 'object' ? collectJsonLeaves(sample, '') : [];
if (!leaves.length) {
var empty = document.createElement('div');
empty.className = 'form-hint';
empty.textContent = 'Добавьте пример ответа JSON, чтобы выбрать поля кликом.';
root.appendChild(empty);
return;
}
leaves.forEach(function(leaf) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'json-tree-node';
button.textContent = leaf.name;
button.addEventListener('click', function() {
var output = outputNameFromPath(leaf.name);
field('wizard-response-mapping-rows').appendChild(createResponseRow({
responsePath: leaf.name,
output: output,
}));
syncVisualMappingsToYaml();
});
root.appendChild(button);
});
}
function formatSamplesAndInferSchemas() {
var input = parseText('wizard-input-sample');
var output = parseText('wizard-output-sample');
writeText('wizard-input-sample', JSON.stringify(input, null, 2));
writeText('wizard-test-input', JSON.stringify(input, null, 2));
writeText('wizard-output-sample', JSON.stringify(output, null, 2));
writeText('tool-input-schema', JSON.stringify(inferJsonSchema(input), null, 2));
writeText('tool-output-schema', JSON.stringify(inferJsonSchema(output), null, 2));
renderRequestRows(suggestedRequestRows());
renderResponseRows(suggestedResponseRows());
renderResponseTree();
if (window.CrankWizardLive && typeof window.CrankWizardLive.renderAgentFacingPreview === 'function') {
window.CrankWizardLive.renderAgentFacingPreview();
}
}
function loadJsonFile(inputId, targetTextareaId, afterLoad) {
var input = field(inputId);
if (!input) return;
var file = input.files && input.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(event) {
try {
var parsed = JSON.parse(event.target.result || '{}');
writeText(targetTextareaId, JSON.stringify(parsed, null, 2));
if (afterLoad) afterLoad();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message, 'JSON не прочитан');
}
} finally {
input.value = '';
}
};
reader.readAsText(file);
}
function bindFileButton(buttonId, inputId) {
var button = field(buttonId);
var input = field(inputId);
if (!button || !input || button.dataset.mappingBound === 'true') return;
button.dataset.mappingBound = 'true';
button.addEventListener('click', function(event) {
event.preventDefault();
input.click();
});
}
function bindOnce(id, eventName, handler) {
var element = field(id);
if (!element || element.dataset.mappingBound === 'true') return;
element.dataset.mappingBound = 'true';
element.addEventListener(eventName, handler);
}
function initializeMappingBuilder() {
bindOnce('wizard-add-request-mapping-row', 'click', function(event) {
event.preventDefault();
field('wizard-request-mapping-rows').appendChild(createRequestRow({
input: '',
target: defaultTargetKind(),
apiName: '',
}));
});
bindOnce('wizard-add-response-mapping-row', 'click', function(event) {
event.preventDefault();
field('wizard-response-mapping-rows').appendChild(createResponseRow({
responsePath: '',
output: '',
}));
});
bindOnce('wizard-auto-request-mapping', 'click', function(event) {
event.preventDefault();
renderRequestRows(suggestedRequestRows());
});
bindOnce('wizard-auto-response-mapping', 'click', function(event) {
event.preventDefault();
renderResponseRows(suggestedResponseRows());
renderResponseTree();
});
bindOnce('wizard-sync-request-mapping', 'click', function(event) {
event.preventDefault();
syncVisualMappingsToYaml();
});
bindOnce('wizard-sync-response-mapping', 'click', function(event) {
event.preventDefault();
syncVisualMappingsToYaml();
});
bindOnce('wizard-format-samples', 'click', function(event) {
event.preventDefault();
formatSamplesAndInferSchemas();
});
bindFileButton('wizard-load-input-sample-file', 'wizard-input-sample-file');
bindFileButton('wizard-load-output-sample-file', 'wizard-output-sample-file');
bindOnce('wizard-input-sample-file', 'change', function() {
loadJsonFile('wizard-input-sample-file', 'wizard-input-sample', function() {
renderRequestRows(suggestedRequestRows());
});
});
bindOnce('wizard-output-sample-file', 'change', function() {
loadJsonFile('wizard-output-sample-file', 'wizard-output-sample', function() {
renderResponseRows(suggestedResponseRows());
renderResponseTree();
});
});
['wizard-input-sample', 'tool-input-schema', 'endpoint-path'].forEach(function(id) {
var element = field(id);
if (!element || element.dataset.mappingRefreshBound === 'true') return;
element.dataset.mappingRefreshBound = 'true';
element.addEventListener('change', function() {
renderRequestRows(requestRowsFromEditor());
renderMappingWarnings();
});
});
['wizard-output-sample', 'tool-output-schema'].forEach(function(id) {
var element = field(id);
if (!element || element.dataset.mappingRefreshBound === 'true') return;
element.dataset.mappingRefreshBound = 'true';
element.addEventListener('change', function() {
renderResponseRows(responseRowsFromEditor());
renderResponseTree();
renderMappingWarnings();
});
});
renderRequestRows(requestRowsFromEditor());
renderResponseRows(responseRowsFromEditor());
renderResponseTree();
renderMappingWarnings();
}
window.CrankWizardMapping = {
initialize: initializeMappingBuilder,
sync: syncVisualMappingsToYaml,
renderFromEditors: function() {
suspendSync = true;
renderRequestRows(requestRowsFromEditor());
renderResponseRows(responseRowsFromEditor());
suspendSync = false;
syncVisualMappingsToYaml();
renderResponseTree();
renderMappingWarnings();
},
formatSamplesAndInferSchemas: formatSamplesAndInferSchemas,
renderWarnings: renderMappingWarnings,
};
})();
+101 -7
View File
@@ -122,26 +122,76 @@ function preserveMappingRuleMetadata(rule, existingRule) {
return rule;
}
function mappingEntrySource(entry) {
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
return entry.source;
}
return entry;
}
function normalizeTransformKind(value) {
if (!value) return null;
var kind = value && typeof value === 'object' ? value.kind : value;
kind = String(kind || '').trim();
var allowed = [
'identity',
'to_string',
'to_number',
'to_boolean',
'join',
'split',
'wrap_array',
'unwrap_singleton',
];
return allowed.indexOf(kind) >= 0 ? kind : null;
}
function applyMappingEntryMetadata(rule, entry, existingRule) {
preserveMappingRuleMetadata(rule, existingRule);
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
return rule;
}
if (Object.prototype.hasOwnProperty.call(entry, 'required')) {
rule.required = entry.required !== false;
}
if (Object.prototype.hasOwnProperty.call(entry, 'default_value')) {
rule.default_value = entry.default_value;
}
if (entry.transform !== undefined && entry.transform !== null) {
var transformKind = normalizeTransformKind(entry.transform);
if (transformKind) rule.transform = { kind: transformKind };
}
['condition', 'notes'].forEach(function(field) {
if (entry[field] !== undefined && entry[field] !== null) {
rule[field] = entry[field];
}
});
return rule;
}
function buildMappingSet(rawValue, mode) {
var value = rawValue || {};
var rules = [];
Object.keys(value).forEach(function(target) {
var source = value[target];
var entry = value[target];
var source = mappingEntrySource(entry);
if (mode === 'input') {
var inputTarget = normalizeInputTarget(target);
rules.push(preserveMappingRuleMetadata({
rules.push(applyMappingEntryMetadata({
source: normalizeInputSource(source),
target: inputTarget,
}, existingMappingRuleByTarget(mode, inputTarget)));
}, entry, existingMappingRuleByTarget(mode, inputTarget)));
return;
}
var outputTarget = normalizeOutputTarget(target);
rules.push(preserveMappingRuleMetadata({
rules.push(applyMappingEntryMetadata({
source: normalizeOutputSource(source),
target: outputTarget,
}, existingMappingRuleByTarget(mode, outputTarget)));
}, entry, existingMappingRuleByTarget(mode, outputTarget)));
});
return { rules: rules };
@@ -313,11 +363,52 @@ function mappingSetToEditorValue(mappingSet, mode, protocol) {
var key = rule.target.indexOf(mappingRootForProtocol(protocol) + '.') === 0
? rule.target.replace(mappingRootForProtocol(protocol) + '.', '')
: rule.target.replace('$.request.', '');
object[key] = rule.source.replace('$.mcp.', '$.input.');
var inputSource = rule.source.replace('$.mcp.', '$.input.');
var inputEntry = { source: inputSource };
var hasMetadata = false;
if (rule.required === false) {
inputEntry.required = false;
hasMetadata = true;
}
if (rule.default_value !== undefined && rule.default_value !== null) {
inputEntry.default_value = rule.default_value;
hasMetadata = true;
}
if (rule.transform && rule.transform.kind) {
inputEntry.transform = rule.transform.kind;
hasMetadata = true;
}
['condition', 'notes'].forEach(function(field) {
if (rule[field] !== undefined && rule[field] !== null) {
inputEntry[field] = rule[field];
hasMetadata = true;
}
});
object[key] = hasMetadata ? inputEntry : inputSource;
return;
}
var outputKey = rule.target.replace('$.output.', '');
object[outputKey] = rule.source;
var outputEntry = { source: rule.source };
var outputHasMetadata = false;
if (rule.required === false) {
outputEntry.required = false;
outputHasMetadata = true;
}
if (rule.default_value !== undefined && rule.default_value !== null) {
outputEntry.default_value = rule.default_value;
outputHasMetadata = true;
}
if (rule.transform && rule.transform.kind) {
outputEntry.transform = rule.transform.kind;
outputHasMetadata = true;
}
['condition', 'notes'].forEach(function(field) {
if (rule[field] !== undefined && rule[field] !== null) {
outputEntry[field] = rule[field];
outputHasMetadata = true;
}
});
object[outputKey] = outputHasMetadata ? outputEntry : rule.source;
});
return window.jsyaml ? window.jsyaml.dump(object, { lineWidth: -1 }) : JSON.stringify(object, null, 2);
}
@@ -397,6 +488,9 @@ function prefillWizardFromEdit(detail, versionDocument) {
setValue('tool-output-mapping', mappingSetToEditorValue(snapshot.output_mapping, 'output', snapshot.protocol || detail.protocol));
setValue('tool-exec-config', executionConfigToEditorValue(snapshot.execution_config || {}));
prefillWizardSamples(snapshot);
if (window.CrankWizardMapping && typeof window.CrankWizardMapping.renderFromEditors === 'function') {
window.CrankWizardMapping.renderFromEditors();
}
var toolNameInput = document.getElementById('tool-name');
if (toolNameInput) toolNameInput.disabled = true;