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
+141
View File
@@ -189,6 +189,137 @@
color: var(--text-primary);
}
.mapping-builder-card .config-card-header {
align-items: flex-start;
}
.mapping-table {
display: grid;
gap: 8px;
}
.mapping-row {
display: grid;
grid-template-columns:
minmax(130px, 1fr)
minmax(105px, 0.65fr)
minmax(130px, 1fr)
minmax(120px, 0.8fr)
minmax(150px, 0.9fr)
34px;
gap: 8px;
align-items: center;
padding: 10px;
border: 1px solid var(--border-subtle);
border-radius: 10px;
background: var(--bg-overlay);
}
.response-mapping-row {
grid-template-columns: minmax(160px, 1fr) minmax(140px, 1fr) 34px;
}
.mapping-row-remove {
width: 32px;
height: 32px;
font-size: 18px;
line-height: 1;
}
.mapping-builder-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.advanced-mapping-details {
border: 1px solid var(--border-subtle);
border-radius: 10px;
background: rgba(255, 255, 255, 0.02);
overflow: hidden;
}
.advanced-mapping-details summary {
padding: 10px 12px;
cursor: pointer;
color: var(--text-secondary);
font-size: 13px;
font-weight: 600;
}
.advanced-mapping-details[open] summary {
border-bottom: 1px solid var(--border-subtle);
}
.mapping-response-layout {
display: grid;
grid-template-columns: minmax(220px, 0.85fr) minmax(320px, 1.15fr);
gap: 14px;
align-items: start;
}
.json-tree-picker {
display: grid;
gap: 6px;
max-height: 320px;
overflow: auto;
padding: 10px;
border: 1px solid var(--border-subtle);
border-radius: 10px;
background: var(--bg-overlay);
}
.json-tree-node {
width: 100%;
text-align: left;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
padding: 7px 9px;
cursor: pointer;
}
.json-tree-node:hover {
border-color: var(--border);
background: var(--bg-surface);
color: var(--text-primary);
}
.mapping-warnings {
display: grid;
gap: 8px;
margin-bottom: 20px;
}
.mapping-warning {
padding: 10px 12px;
border: 1px solid rgba(245, 158, 11, 0.34);
border-radius: 10px;
background: rgba(245, 158, 11, 0.1);
color: var(--text-secondary);
font-size: 13px;
line-height: 1.45;
}
.mapping-warning strong {
color: var(--text-primary);
}
@media (max-width: 900px) {
.mapping-row,
.response-mapping-row,
.mapping-response-layout {
grid-template-columns: 1fr;
}
.mapping-row-remove {
width: 100%;
}
}
/* ══════════════════════════════════════════════════
STEP SIDEBAR
══════════════════════════════════════════════════ */
@@ -1527,6 +1658,16 @@
margin-top: 6px;
}
.quality-finding-jump {
margin-top: 8px;
}
.quality-focus-target {
outline: 2px solid rgba(13, 148, 136, 0.85);
box-shadow: 0 0 0 4px rgba(13, 148, 136, 0.14);
transition: box-shadow 0.2s ease, outline-color 0.2s ease;
}
@media (max-width: 760px) {
.agent-preview-summary {
grid-template-columns: 1fr;
+63 -17
View File
@@ -13,20 +13,35 @@
<div class="section-divider-line"></div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-body" style="gap: 0; padding: 0;">
<div class="code-block" style="border-radius: 0; border: none;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / request-fields</span>
</div>
<textarea id="tool-input-mapping" class="form-textarea code-textarea" rows="12">first_name: "$.input.first_name"
<div class="config-card mapping-builder-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div>
<div class="config-card-title">Конструктор API-запроса</div>
<div class="config-card-subtitle">Укажите, куда отправлять поля инструмента: в путь, query-параметры, заголовки или JSON-тело.</div>
</div>
<button id="wizard-add-request-mapping-row" class="btn-ghost-sm" type="button">Добавить поле</button>
</div>
<div class="config-card-body" style="gap: 14px;">
<div class="mapping-table" id="wizard-request-mapping-rows" data-testid="wizard-request-mapping-rows"></div>
<div class="mapping-builder-actions">
<button id="wizard-auto-request-mapping" class="btn-ghost-sm" type="button">Собрать из входного JSON</button>
<button id="wizard-sync-request-mapping" class="btn-ghost-sm" type="button">Обновить YAML</button>
</div>
<details class="advanced-mapping-details">
<summary>Дополнительно: YAML маппинга запроса</summary>
<div class="code-block">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / request-fields</span>
</div>
<textarea id="tool-input-mapping" class="form-textarea code-textarea" rows="10">first_name: "$.input.first_name"
last_name: "$.input.last_name"
email: "$.input.email"
company: "$.input.company"
phone: "$.input.phone"
source: "$.input.source"</textarea>
</div>
</div>
</details>
</div>
</div>
@@ -35,18 +50,42 @@ source: "$.input.source"</textarea>
<div class="section-divider-line"></div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-body" style="gap: 0; padding: 0;">
<div class="code-block" style="border-radius: 0; border: none;">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / response-fields</span>
<div class="config-card mapping-builder-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div>
<div class="config-card-title">Конструктор ответа инструмента</div>
<div class="config-card-subtitle">Выберите поля из ответа API, которые нужно вернуть MCP клиенту.</div>
</div>
<button id="wizard-add-response-mapping-row" class="btn-ghost-sm" type="button">Добавить поле</button>
</div>
<div class="config-card-body" style="gap: 14px;">
<div class="mapping-response-layout">
<div>
<div class="form-label">Поля ответа API</div>
<div id="wizard-response-json-tree" class="json-tree-picker" data-testid="wizard-response-json-tree"></div>
</div>
<textarea id="tool-output-mapping" class="form-textarea code-textarea" rows="10">lead_id: "$.response.data.id"
<div>
<div class="form-label">Поля результата инструмента</div>
<div class="mapping-table" id="wizard-response-mapping-rows" data-testid="wizard-response-mapping-rows"></div>
</div>
</div>
<div class="mapping-builder-actions">
<button id="wizard-auto-response-mapping" class="btn-ghost-sm" type="button">Собрать из ответа JSON</button>
<button id="wizard-sync-response-mapping" class="btn-ghost-sm" type="button">Обновить YAML</button>
</div>
<details class="advanced-mapping-details">
<summary>Дополнительно: YAML маппинга ответа</summary>
<div class="code-block">
<div class="code-toolbar">
<div class="code-dots"><span></span><span></span><span></span></div>
<span class="code-toolbar-label">yaml / response-fields</span>
</div>
<textarea id="tool-output-mapping" class="form-textarea code-textarea" rows="10">lead_id: "$.response.data.id"
status: "$.response.data.status"
created_at: "$.response.data.created_at"
owner_id: "$.response.data.owner.id"</textarea>
</div>
</div>
</details>
</div>
</div>
@@ -103,6 +142,8 @@ tls:
</div>
</div>
<div id="wizard-mapping-warnings" class="mapping-warnings" hidden data-testid="wizard-mapping-warnings"></div>
<div class="config-card" id="agent-facing-preview-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div class="config-card-header-icon">
@@ -196,6 +237,7 @@ tls:
"last_name": "Lovelace",
"email": "ada@example.com"
}</textarea>
<input id="wizard-input-sample-file" type="file" accept=".json,application/json" hidden>
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step5.output_sample">Пример ответа</label>
@@ -203,9 +245,13 @@ tls:
"id": "lead_123",
"status": "created"
}</textarea>
<input id="wizard-output-sample-file" type="file" accept=".json,application/json" hidden>
</div>
</div>
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
<button id="wizard-load-input-sample-file" class="btn-ghost-sm" type="button">Загрузить JSON запроса</button>
<button id="wizard-load-output-sample-file" class="btn-ghost-sm" type="button">Загрузить JSON ответа</button>
<button id="wizard-format-samples" class="btn-ghost-sm" type="button">Проверить и форматировать JSON</button>
<button id="wizard-upload-input-sample" class="btn-primary-sm" type="button" data-i18n="wizard.step5.save_input_sample">Сохранить входной пример</button>
<button id="wizard-upload-output-sample" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.save_output_sample">Сохранить выходной пример</button>
<button id="wizard-generate-draft" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.generate_draft">Пересобрать по примерам</button>
+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;
+2
View File
@@ -41,6 +41,7 @@ const BUNDLES = {
operations: {
files: [
'node_modules/alpinejs/dist/cdn.min.js',
'js/openapi-import.js',
'js/catalog.js',
],
},
@@ -93,6 +94,7 @@ const BUNDLES = {
'js/wizard-shell.js',
'js/wizard-upstreams.js',
'js/wizard-model.js',
'js/wizard-mapping.js',
'js/wizard-live.js',
'js/wizard.js',
'js/nav.js',
+101
View File
@@ -122,6 +122,7 @@ test('wizard loads reusable upstreams from backend', async ({ page }) => {
);
await page.goto('/wizard/');
await expect(page.locator('#step-panel-1')).toBeVisible();
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('.btn-continue').click();
await expect(page.locator('#step-panel-2')).toBeVisible();
@@ -132,6 +133,69 @@ test('wizard loads reusable upstreams from backend', async ({ page }) => {
await expect(page.locator('#upstream-dropdown-list')).not.toContainText('Open Meteo');
});
test('wizard builds visual request mappings from JSON sample and path params', async ({ page }) => {
await login(page);
await page.goto('/wizard/');
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.evaluate(() => window.CrankWizardShell.doGoToStep(2));
await expect(page.locator('#step-panel-2')).toBeVisible();
await page.locator('#endpoint-path').fill('/rates/{date}');
await page.evaluate(() => window.CrankWizardShell.doGoToStep(3));
await expect(page.locator('#step-panel-3-rest')).toBeVisible();
await page.locator('.method-card[data-method="GET"]').click();
await page.evaluate(() => window.CrankWizardShell.doGoToStep(5));
await expect(page.locator('#step-panel-5')).toBeVisible();
await page.locator('#wizard-input-sample').fill(JSON.stringify({
date: '2024-01-01',
base: 'USD',
symbols: 'EUR',
}, null, 2));
await page.locator('#wizard-output-sample').fill(JSON.stringify({
base: 'USD',
rates: {
EUR: 0.91,
},
providers: [
{
name: 'ecb',
priority: 1,
},
],
}, null, 2));
await page.locator('#wizard-format-samples').click();
await expect(page.locator('[data-testid="wizard-request-mapping-rows"] [data-role="input"]').first()).toHaveValue('date');
await expect(page.locator('[data-testid="wizard-request-mapping-rows"] [data-role="input"]').nth(1)).toHaveValue('base');
await expect(page.locator('#tool-input-mapping')).toHaveValue(/path\.date/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.base/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.symbols/);
await expect(page.locator('#tool-input-schema')).toHaveValue(/"date"/);
await expect(page.locator('#tool-output-schema')).toHaveValue(/"rates"/);
await expect(page.locator('[data-testid="wizard-response-json-tree"]')).toContainText('rates.EUR');
await expect(page.locator('[data-testid="wizard-response-json-tree"]')).toContainText('providers[0].name');
await expect(page.locator('#tool-output-mapping')).toHaveValue(/providers_0_name: \$\.response\.body\.providers\[0\]\.name/);
await expect(page.locator('[data-testid="wizard-mapping-warnings"]')).toContainText(/одного элемента массива/);
await page.locator('#wizard-add-request-mapping-row').click();
const defaultRow = page.locator('[data-testid="wizard-request-mapping-rows"] [data-mapping-row="request"]').last();
await defaultRow.locator('[data-role="input"]').fill('group');
await defaultRow.locator('[data-role="target"]').selectOption('query');
await defaultRow.locator('[data-role="apiName"]').fill('group');
await defaultRow.locator('[data-role="defaultValue"]').fill('month');
await defaultRow.locator('[data-role="transform"]').selectOption('to_string');
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.group:/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/default_value: month/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/transform: to_string/);
await page.locator('[data-testid="wizard-request-mapping-rows"] .mapping-row-remove').first().click();
await expect(page.locator('[data-testid="wizard-mapping-warnings"]')).toContainText(/date/);
});
test('wizard edit mode hydrates fields from operation version snapshot', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
@@ -301,6 +365,15 @@ test('wizard edit mode hydrates fields from operation version snapshot', async (
from: 'GBP',
to: 'USD',
},
import_findings: [
{
severity: 'warning',
code: 'openapi_import.weak_tool_description',
message: 'Описание инструмента слишком короткое или техническое.',
suggested_action: 'Уточните, когда агент должен вызывать инструмент.',
field_path: 'GET /rates',
},
],
},
samples: [],
generated_draft: null,
@@ -338,6 +411,13 @@ test('wizard edit mode hydrates fields from operation version snapshot', async (
await expect(page.locator('#wizard-test-input')).toHaveValue(/"from": "GBP"/);
await expect(page.locator('#wizard-test-input')).toHaveValue(/"to": "USD"/);
await expect(page.locator('#wizard-test-input')).not.toHaveValue(/Ada/);
await page.evaluate(() => window.CrankWizardShell.doGoToStep(5));
await expect(page.locator('#wizard-quality-findings')).toContainText('Описание инструмента слишком короткое');
await expect(page.locator('#wizard-quality-findings')).toContainText('GET /rates');
await page.getByRole('button', { name: 'Перейти к описанию' }).click();
await expect(page.locator('#step-panel-4')).toBeVisible();
await expect(page.locator('#tool-description')).toBeFocused();
});
test('wizard shows agent-facing MCP preview from current draft fields', async ({ page }) => {
@@ -669,6 +749,7 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
target: '$.request.query.group',
required: false,
default_value: 'month',
transform: { kind: 'to_string' },
},
],
},
@@ -706,6 +787,15 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
base: 'CHF',
symbols: 'JPY',
},
import_findings: [
{
severity: 'warning',
code: 'openapi_import.weak_tool_description',
message: 'Описание инструмента слишком короткое или техническое.',
suggested_action: 'Уточните описание.',
field_path: 'GET /latest',
},
],
},
samples: [],
generated_draft: null,
@@ -719,6 +809,7 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
await page.goto(`/wizard/?mode=edit&operationId=${operationId}`);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.base/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/path\.date/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/transform: to_string/);
await page.locator('.btn-save-draft').click();
await expect.poll(() => updatePayload).not.toBeNull();
@@ -766,6 +857,15 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
base: 'CHF',
symbols: 'JPY',
},
import_findings: [
{
severity: 'warning',
code: 'openapi_import.weak_tool_description',
message: 'Описание инструмента слишком короткое или техническое.',
suggested_action: 'Уточните описание.',
field_path: 'GET /latest',
},
],
});
expect(updatePayload.input_mapping.rules).toEqual([
{ source: '$.mcp.base', target: '$.request.query.base', required: true },
@@ -776,6 +876,7 @@ test('wizard edit mode preserves explicit request mapping targets on save', asyn
target: '$.request.query.group',
required: false,
default_value: 'month',
transform: { kind: 'to_string' },
},
]);
});