Improve wizard mapping and quality guidance
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user