Improve wizard mapping and quality guidance
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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="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="12">first_name: "$.input.first_name"
|
||||
<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>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,9 +50,32 @@ 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="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>
|
||||
<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>
|
||||
@@ -47,6 +85,7 @@ status: "$.response.data.status"
|
||||
created_at: "$.response.data.created_at"
|
||||
owner_id: "$.response.data.owner.id"</textarea>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user