feat: add websocket wizard foundation
This commit is contained in:
@@ -2,18 +2,18 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/soap-wizard-foundation`
|
||||
### `feat/websocket-wizard-foundation`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
- wizard step 1 exposes SOAP as a selectable protocol
|
||||
- wizard step 3 supports WSDL/XSD upload and backend-driven SOAP service inspection
|
||||
- SOAP service, port and operation selection is serialized into operation target payload
|
||||
- wizard step 1 exposes WebSocket as a selectable protocol
|
||||
- wizard step 3 collects WebSocket endpoint, subprotocols and subscribe/unsubscribe templates
|
||||
- execution config serializes `ProtocolOptions.websocket` runtime tuning
|
||||
|
||||
## Next
|
||||
|
||||
- `feat/websocket-wizard-foundation`
|
||||
- `feat/live-authenticated-staging-entry`
|
||||
|
||||
## Backlog
|
||||
|
||||
|
||||
@@ -76,6 +76,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-card websocket" data-protocol="websocket" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
<div class="protocol-icon-wrap">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 8h16M4 16h16"/>
|
||||
<path d="M7 5l-3 3 3 3M17 13l3 3-3 3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="protocol-card-name" data-i18n="wizard.step1.websocket_name">WebSocket</div>
|
||||
<div class="protocol-card-tagline" data-i18n="wizard.step1.websocket_tagline">Stateful event streams and push-oriented integrations normalized into bounded MCP streaming tools.</div>
|
||||
<div class="protocol-card-tags">
|
||||
<span class="protocol-tag">WS</span>
|
||||
<span class="protocol-tag" data-i18n="wizard.streaming.mode.window">Window</span>
|
||||
<span class="protocol-tag" data-i18n="wizard.streaming.mode.session">Session</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-card soap" data-protocol="soap" role="radio" aria-checked="false" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<div id="step-panel-3-websocket" class="step-pane">
|
||||
<div class="step-panel-header">
|
||||
<div class="step-panel-eyebrow">
|
||||
<div class="step-panel-eyebrow-line"></div>
|
||||
<span data-i18n-html="wizard.step_of">Step 3 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step3.websocket.title">WebSocket subscription</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step3.websocket.subtitle">Define the subscription handshake, the message templates and the connection tuning that Crank should use before collecting bounded WebSocket events.</p>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2.5 8h11M4.5 5l-2 3 2 3M11.5 11l2-3-2-3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step3.websocket.connection_title">Connection</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step3.websocket.connection_subtitle">The upstream base URL from step 2 becomes the WebSocket endpoint unless you override it here.</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
|
||||
</div>
|
||||
<div class="config-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.path">Socket path</label>
|
||||
<input id="websocket-path" class="form-input input-mono" type="text" placeholder="/telemetry">
|
||||
<div class="form-hint" data-i18n="wizard.step3.websocket.path_hint">Appended to the upstream URL from step 2. Leave empty if the upstream URL already points to the full WebSocket endpoint.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.subprotocols">Subprotocols</label>
|
||||
<textarea id="websocket-subprotocols" class="form-textarea code-textarea" rows="3" spellcheck="false" placeholder="graphql-transport-ws telemetry.v1"></textarea>
|
||||
<div class="form-hint" data-i18n="wizard.step3.websocket.subprotocols_hint">One subprotocol per line. Crank offers them during the handshake in the same order.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="12" height="10" rx="1.5"/>
|
||||
<path d="M5 7h6M5 10h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step3.websocket.messages_title">Subscription messages</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step3.websocket.messages_subtitle">Templates are sent after the socket opens. They can reference MCP input through the mapping step.</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
|
||||
</div>
|
||||
<div class="config-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.subscribe_template">Subscribe template</label>
|
||||
<textarea id="websocket-subscribe-template" class="form-textarea code-textarea" rows="6" spellcheck="false" placeholder="{ "type": "subscribe", "topic": "telemetry" }"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.unsubscribe_template">Unsubscribe template</label>
|
||||
<textarea id="websocket-unsubscribe-template" class="form-textarea code-textarea" rows="6" spellcheck="false" placeholder="{ "type": "unsubscribe", "topic": "telemetry" }"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 2.5v2M8 11.5v2M3.8 4.2l1.4 1.4M10.8 11.2l1.4 1.4M2.5 8h2M11.5 8h2M3.8 11.8l1.4-1.4M10.8 4.8l1.4-1.4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step3.websocket.runtime_title">Connection tuning</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step3.websocket.runtime_subtitle">Used by the runtime adapter for heartbeats and reconnects in bounded session-oriented flows.</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
|
||||
</div>
|
||||
<div class="config-card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.heartbeat">Heartbeat interval (ms)</label>
|
||||
<input id="websocket-heartbeat-interval-ms" class="form-input" type="number" min="1" step="1" placeholder="5000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.reconnect_attempts">Reconnect attempts</label>
|
||||
<input id="websocket-reconnect-max-attempts" class="form-input" type="number" min="0" step="1" placeholder="3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.websocket.reconnect_backoff">Reconnect backoff (ms)</label>
|
||||
<input id="websocket-reconnect-backoff-ms" class="form-input" type="number" min="0" step="1" placeholder="250">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -605,6 +605,7 @@ var TRANSLATIONS = {
|
||||
'wizard.step3.rest.label': 'Request config',
|
||||
'wizard.step3.graphql.label': 'GQL operation',
|
||||
'wizard.step3.grpc.label': 'RPC method',
|
||||
'wizard.step3.websocket.label': 'Socket config',
|
||||
'wizard.step3.soap.label': 'WSDL binding',
|
||||
'wizard.step3.rest.title': 'HTTP method & format',
|
||||
'wizard.step3.rest.subtitle': 'Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.',
|
||||
@@ -667,6 +668,23 @@ var TRANSLATIONS = {
|
||||
'wizard.step3.grpc.route_label': 'gRPC route',
|
||||
'wizard.step3.grpc.request_label': 'Request',
|
||||
'wizard.step3.grpc.response_label': 'Response',
|
||||
'wizard.step3.websocket.title': 'WebSocket subscription',
|
||||
'wizard.step3.websocket.subtitle': 'Define the subscription handshake, message templates and runtime tuning that Crank should use before collecting bounded WebSocket events.',
|
||||
'wizard.step3.websocket.connection_title': 'Connection',
|
||||
'wizard.step3.websocket.connection_subtitle': 'The upstream base URL from step 2 becomes the WebSocket endpoint unless you override it here.',
|
||||
'wizard.step3.websocket.path': 'Socket path',
|
||||
'wizard.step3.websocket.path_hint': 'Appended to the upstream URL from step 2. Leave empty when the upstream URL already points to the full WebSocket endpoint.',
|
||||
'wizard.step3.websocket.subprotocols': 'Subprotocols',
|
||||
'wizard.step3.websocket.subprotocols_hint': 'One subprotocol per line. Crank offers them during the handshake in the same order.',
|
||||
'wizard.step3.websocket.messages_title': 'Subscription messages',
|
||||
'wizard.step3.websocket.messages_subtitle': 'Templates are sent after the socket opens. They can reference MCP input through the mapping step.',
|
||||
'wizard.step3.websocket.subscribe_template': 'Subscribe template',
|
||||
'wizard.step3.websocket.unsubscribe_template': 'Unsubscribe template',
|
||||
'wizard.step3.websocket.runtime_title': 'Connection tuning',
|
||||
'wizard.step3.websocket.runtime_subtitle': 'Used by the runtime adapter for heartbeats and reconnects in bounded session-oriented flows.',
|
||||
'wizard.step3.websocket.heartbeat': 'Heartbeat interval (ms)',
|
||||
'wizard.step3.websocket.reconnect_attempts': 'Reconnect attempts',
|
||||
'wizard.step3.websocket.reconnect_backoff': 'Reconnect backoff (ms)',
|
||||
'wizard.step3.soap.title': 'WSDL and SOAP binding',
|
||||
'wizard.step3.soap.subtitle': 'Upload the WSDL/XSD bundle, inspect services and choose the exact service, port and operation that Crank should expose as an MCP tool.',
|
||||
'wizard.step3.soap.wsdl_title': 'Contract files',
|
||||
@@ -1569,6 +1587,7 @@ var TRANSLATIONS = {
|
||||
'wizard.step3.rest.label': 'Конфиг запроса',
|
||||
'wizard.step3.graphql.label': 'GQL операция',
|
||||
'wizard.step3.grpc.label': 'RPC метод',
|
||||
'wizard.step3.websocket.label': 'Конфиг сокета',
|
||||
'wizard.step3.soap.label': 'WSDL binding',
|
||||
'wizard.step3.rest.title': 'HTTP метод и формат',
|
||||
'wizard.step3.rest.subtitle': 'Выберите HTTP-метод, который отправляет эта операция, и настройте content negotiation headers. Crank сериализует аргументы MCP-инструмента в нужный формат request body.',
|
||||
@@ -1631,6 +1650,23 @@ var TRANSLATIONS = {
|
||||
'wizard.step3.grpc.route_label': 'gRPC route',
|
||||
'wizard.step3.grpc.request_label': 'Запрос',
|
||||
'wizard.step3.grpc.response_label': 'Ответ',
|
||||
'wizard.step3.websocket.title': 'WebSocket subscription',
|
||||
'wizard.step3.websocket.subtitle': 'Определите handshake подписки, шаблоны сообщений и runtime-параметры, которые Crank должен использовать перед сбором bounded WebSocket events.',
|
||||
'wizard.step3.websocket.connection_title': 'Соединение',
|
||||
'wizard.step3.websocket.connection_subtitle': 'Base URL upstream-а из шага 2 станет WebSocket endpoint-ом, если вы явно не переопределите его здесь.',
|
||||
'wizard.step3.websocket.path': 'Путь сокета',
|
||||
'wizard.step3.websocket.path_hint': 'Добавляется к upstream URL из шага 2. Оставьте пустым, если upstream URL уже указывает на полный WebSocket endpoint.',
|
||||
'wizard.step3.websocket.subprotocols': 'Subprotocols',
|
||||
'wizard.step3.websocket.subprotocols_hint': 'По одному subprotocol на строку. Crank предложит их в handshake в том же порядке.',
|
||||
'wizard.step3.websocket.messages_title': 'Сообщения подписки',
|
||||
'wizard.step3.websocket.messages_subtitle': 'Шаблоны отправляются после открытия сокета. Они могут ссылаться на MCP input через mapping на следующем шаге.',
|
||||
'wizard.step3.websocket.subscribe_template': 'Шаблон subscribe',
|
||||
'wizard.step3.websocket.unsubscribe_template': 'Шаблон unsubscribe',
|
||||
'wizard.step3.websocket.runtime_title': 'Тюнинг соединения',
|
||||
'wizard.step3.websocket.runtime_subtitle': 'Используется runtime adapter-ом для heartbeat и reconnect в bounded session-oriented flows.',
|
||||
'wizard.step3.websocket.heartbeat': 'Интервал heartbeat (мс)',
|
||||
'wizard.step3.websocket.reconnect_attempts': 'Число reconnect attempts',
|
||||
'wizard.step3.websocket.reconnect_backoff': 'Backoff reconnect-а (мс)',
|
||||
'wizard.step3.soap.title': 'WSDL и SOAP binding',
|
||||
'wizard.step3.soap.subtitle': 'Загрузите WSDL/XSD bundle, получите список сервисов и выберите точный service, port и operation, который Crank должен опубликовать как MCP tool.',
|
||||
'wizard.step3.soap.wsdl_title': 'Файлы контракта',
|
||||
|
||||
@@ -94,6 +94,7 @@ function loadStep(n, callback) {
|
||||
function step3PanelId() {
|
||||
if (wizardProtocol === 'graphql') return 'step-panel-3-graphql';
|
||||
if (wizardProtocol === 'grpc') return 'step-panel-3-grpc';
|
||||
if (wizardProtocol === 'websocket') return 'step-panel-3-websocket';
|
||||
if (wizardProtocol === 'soap') return 'step-panel-3-soap';
|
||||
return 'step-panel-3-rest';
|
||||
}
|
||||
@@ -103,6 +104,7 @@ var STEP3_LABELS = {
|
||||
rest: tKey('wizard.step3.rest.label'),
|
||||
graphql: tKey('wizard.step3.graphql.label'),
|
||||
grpc: tKey('wizard.step3.grpc.label'),
|
||||
websocket: tKey('wizard.step3.websocket.label'),
|
||||
soap: tKey('wizard.step3.soap.label'),
|
||||
};
|
||||
|
||||
@@ -217,6 +219,8 @@ function bindProtocolCards() {
|
||||
? 'graphql'
|
||||
: card.classList.contains('grpc')
|
||||
? 'grpc'
|
||||
: card.classList.contains('websocket')
|
||||
? 'websocket'
|
||||
: card.classList.contains('soap')
|
||||
? 'soap'
|
||||
: 'rest');
|
||||
@@ -1510,6 +1514,32 @@ function parseExecutionConfig(text) {
|
||||
useTls = !!value.tls.verify;
|
||||
}
|
||||
config.protocol_options = { grpc: { use_tls: useTls } };
|
||||
} else if (wizardProtocol === 'websocket') {
|
||||
var websocketProtocolOptions = value.protocol_options && value.protocol_options.websocket
|
||||
? value.protocol_options.websocket
|
||||
: {};
|
||||
var heartbeatInterval = websocketProtocolOptions.heartbeat_interval_ms != null
|
||||
? Number(websocketProtocolOptions.heartbeat_interval_ms)
|
||||
: numericValueOrNull('websocket-heartbeat-interval-ms');
|
||||
var reconnectAttempts = websocketProtocolOptions.reconnect_max_attempts != null
|
||||
? Number(websocketProtocolOptions.reconnect_max_attempts)
|
||||
: numericValueOrNull('websocket-reconnect-max-attempts');
|
||||
var reconnectBackoff = websocketProtocolOptions.reconnect_backoff_ms != null
|
||||
? Number(websocketProtocolOptions.reconnect_backoff_ms)
|
||||
: numericValueOrNull('websocket-reconnect-backoff-ms');
|
||||
var runtimeOptions = {};
|
||||
if (Number.isFinite(heartbeatInterval) && heartbeatInterval > 0) {
|
||||
runtimeOptions.heartbeat_interval_ms = heartbeatInterval;
|
||||
}
|
||||
if (Number.isFinite(reconnectAttempts) && reconnectAttempts >= 0) {
|
||||
runtimeOptions.reconnect_max_attempts = reconnectAttempts;
|
||||
}
|
||||
if (Number.isFinite(reconnectBackoff) && reconnectBackoff >= 0) {
|
||||
runtimeOptions.reconnect_backoff_ms = reconnectBackoff;
|
||||
}
|
||||
if (Object.keys(runtimeOptions).length) {
|
||||
config.protocol_options = { websocket: runtimeOptions };
|
||||
}
|
||||
}
|
||||
|
||||
config.streaming = collectStreamingConfig();
|
||||
@@ -1615,6 +1645,21 @@ function buildTarget() {
|
||||
};
|
||||
}
|
||||
|
||||
if (wizardProtocol === 'websocket') {
|
||||
return {
|
||||
kind: 'websocket',
|
||||
url: joinUrl(upstream.url, textValue('websocket-path')),
|
||||
subprotocols: textareaLines('websocket-subprotocols'),
|
||||
subscribe_message_template: stringValueOrNull('websocket-subscribe-template')
|
||||
? parseStructuredText(textValue('websocket-subscribe-template'))
|
||||
: null,
|
||||
unsubscribe_message_template: stringValueOrNull('websocket-unsubscribe-template')
|
||||
? parseStructuredText(textValue('websocket-unsubscribe-template'))
|
||||
: null,
|
||||
static_headers: parseHeaderMap(upstream.staticHeaders),
|
||||
};
|
||||
}
|
||||
|
||||
var route = textValue('grpc-manual-route') || (document.getElementById('grpc-path-value') ? document.getElementById('grpc-path-value').textContent.trim() : '');
|
||||
var descriptorRef = wizardCurrentVersion && wizardCurrentVersion.target && wizardCurrentVersion.target.kind === 'grpc'
|
||||
? wizardCurrentVersion.target.descriptor_ref
|
||||
@@ -1998,6 +2043,8 @@ function selectProtocol(protocol) {
|
||||
? 'graphql'
|
||||
: card.classList.contains('grpc')
|
||||
? 'grpc'
|
||||
: card.classList.contains('websocket')
|
||||
? 'websocket'
|
||||
: card.classList.contains('soap')
|
||||
? 'soap'
|
||||
: 'rest');
|
||||
@@ -2095,6 +2142,25 @@ function executionConfigToEditorValue(config) {
|
||||
if (config.protocol_options && config.protocol_options.grpc) {
|
||||
value.tls = { verify: !!config.protocol_options.grpc.use_tls };
|
||||
}
|
||||
if (config.protocol_options && config.protocol_options.websocket) {
|
||||
value.protocol_options = value.protocol_options || {};
|
||||
value.protocol_options.websocket = {};
|
||||
if (config.protocol_options.websocket.heartbeat_interval_ms != null) {
|
||||
value.protocol_options.websocket.heartbeat_interval_ms = config.protocol_options.websocket.heartbeat_interval_ms;
|
||||
}
|
||||
if (config.protocol_options.websocket.reconnect_max_attempts != null) {
|
||||
value.protocol_options.websocket.reconnect_max_attempts = config.protocol_options.websocket.reconnect_max_attempts;
|
||||
}
|
||||
if (config.protocol_options.websocket.reconnect_backoff_ms != null) {
|
||||
value.protocol_options.websocket.reconnect_backoff_ms = config.protocol_options.websocket.reconnect_backoff_ms;
|
||||
}
|
||||
if (!Object.keys(value.protocol_options.websocket).length) {
|
||||
delete value.protocol_options.websocket;
|
||||
}
|
||||
if (!Object.keys(value.protocol_options).length) {
|
||||
delete value.protocol_options;
|
||||
}
|
||||
}
|
||||
return window.jsyaml ? window.jsyaml.dump(value, { lineWidth: -1 }) : JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
@@ -2141,6 +2207,30 @@ function prefillWizardFromEdit(detail, versionDocument) {
|
||||
setValue('soap-action', target.soap_action || '');
|
||||
setValue('soap-version', target.soap_version || 'soap_11');
|
||||
setValue('soap-binding-style', target.binding_style || 'document_literal');
|
||||
} else if (target.kind === 'websocket') {
|
||||
prefillUpstream(
|
||||
target.url,
|
||||
target.static_headers || {},
|
||||
versionDocument.execution_config ? versionDocument.execution_config.auth_profile_ref : null
|
||||
);
|
||||
setValue('websocket-path', '');
|
||||
setValue('websocket-subprotocols', (target.subprotocols || []).join('\n'));
|
||||
setValue(
|
||||
'websocket-subscribe-template',
|
||||
target.subscribe_message_template ? JSON.stringify(target.subscribe_message_template, null, 2) : ''
|
||||
);
|
||||
setValue(
|
||||
'websocket-unsubscribe-template',
|
||||
target.unsubscribe_message_template ? JSON.stringify(target.unsubscribe_message_template, null, 2) : ''
|
||||
);
|
||||
var websocketOptions = versionDocument.execution_config
|
||||
&& versionDocument.execution_config.protocol_options
|
||||
&& versionDocument.execution_config.protocol_options.websocket
|
||||
? versionDocument.execution_config.protocol_options.websocket
|
||||
: null;
|
||||
setValue('websocket-heartbeat-interval-ms', websocketOptions && websocketOptions.heartbeat_interval_ms ? websocketOptions.heartbeat_interval_ms : '');
|
||||
setValue('websocket-reconnect-max-attempts', websocketOptions && websocketOptions.reconnect_max_attempts != null ? websocketOptions.reconnect_max_attempts : '');
|
||||
setValue('websocket-reconnect-backoff-ms', websocketOptions && websocketOptions.reconnect_backoff_ms ? websocketOptions.reconnect_backoff_ms : '');
|
||||
}
|
||||
|
||||
setValue('tool-name', detail.name);
|
||||
|
||||
@@ -104,6 +104,28 @@ Protocol-specific runtime tuning хранится отдельно в `ProtocolO
|
||||
8. Выполняет test run.
|
||||
9. Публикует operation как MCP tool или tool family.
|
||||
|
||||
## 7.1. Текущий UI foundation
|
||||
|
||||
Текущий wizard support для WebSocket уже включает:
|
||||
|
||||
- выбор `WebSocket` как protocol family на шаге 1;
|
||||
- step 2 reuse общего upstream/auth selector;
|
||||
- отдельный step 3 для:
|
||||
- socket path override;
|
||||
- subprotocol list;
|
||||
- subscribe message template;
|
||||
- unsubscribe message template;
|
||||
- `heartbeat_interval_ms`;
|
||||
- `reconnect_max_attempts`;
|
||||
- `reconnect_backoff_ms`.
|
||||
|
||||
На текущем этапе wizard сериализует:
|
||||
|
||||
- `Target::Websocket`
|
||||
- `ExecutionConfig.protocol_options.websocket`
|
||||
|
||||
Это дает оператору usable foundation для `window`, `session` и `async_job` flows even before deeper inspection/test-run polish.
|
||||
|
||||
## 8. Поведение runtime
|
||||
|
||||
При выполнении WebSocket operation runtime должен:
|
||||
|
||||
Reference in New Issue
Block a user