diff --git a/TASKS.md b/TASKS.md
index fb5a7cc..9730db0 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -4,7 +4,7 @@
### `feat/community-finalization`
-Status: in_progress
+Status: done
Goal:
- довести `crank-community` до окончательного самостоятельного состояния как открытой `REST-only` редакции;
@@ -56,15 +56,20 @@ Progress:
- premium protocol toolchain dependencies have been removed from the Community workspace manifest while keeping the `REST-only` workspace green under `cargo check`
- Community backend crates no longer rely on `test = false`; `just test` now runs against a real Community-only test surface on the isolated local test database
- premium protocol and streaming ballast has been removed from Community backend test/dev paths, and Community demo expectations now match the actual `REST-only` demo seed
-- pending:
- - remove dormant premium UI modules, fixtures, and translation strings
- - run a final Community verification pass
+ - dormant premium UI modules, fixtures, and translation strings have been removed, while the remaining Community wizard and admin pages stay green under local Playwright smoke
+ - final Community verification pass completed:
+ - `cargo metadata --no-deps`
+ - `just fmt`
+ - `just check`
+ - `just test`
+ - `apps/ui` build
+ - Community-targeted Playwright smoke
-## Planned
+## Next
### `feat/community-release-hardening`
-Status: ready
+Status: in_progress
Goal:
- закрепить воспроизводимый Community release path и release checklist без скрытых переходных допущений.
diff --git a/apps/ui/html/wizard/step5.html b/apps/ui/html/wizard/step5.html
index 4890f45..445858e 100644
--- a/apps/ui/html/wizard/step5.html
+++ b/apps/ui/html/wizard/step5.html
@@ -121,7 +121,7 @@ tls:
Live actions save the draft first
-
Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.
+
Sample uploads, test runs, YAML actions and publish all persist the current draft before calling the backend.
diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js
index 688dd04..b4781ff 100644
--- a/apps/ui/js/i18n.js
+++ b/apps/ui/js/i18n.js
@@ -651,9 +651,6 @@ var TRANSLATIONS = {
'wizard.step2.path_hint': 'Appended to the upstream base URL. Use {param} for path variables.',
'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.',
'wizard.step3.rest.method_title': 'HTTP method',
@@ -679,81 +676,6 @@ var TRANSLATIONS = {
'wizard.step3.graphql.doc_subtitle': 'GraphQL document with typed variable declarations',
'wizard.step3.graphql.variables_title': 'Variables are mapped in step 5',
'wizard.step3.graphql.variables_body': 'Declare all variables in the query document using the $variable: Type syntax. In step 5 you will map MCP input fields to these variables. Crank serializes the final {"query","variables"} payload automatically.',
- 'wizard.step3.grpc.title': 'RPC method',
- 'wizard.step3.grpc.subtitle': 'Choose how to describe the gRPC service contract. Upload a .proto file, discover methods from a live server via reflection, or enter the route manually. Only unary RPCs are supported.',
- 'wizard.step3.grpc.source_proto': 'Proto file',
- 'wizard.step3.grpc.source_reflection': 'Descriptor set',
- 'wizard.step3.grpc.source_manual': 'Manual',
- 'wizard.step3.grpc.proto_title': 'Proto definition',
- 'wizard.step3.grpc.proto_subtitle': 'Upload or paste your .proto file',
- 'wizard.step3.grpc.drop_title': 'Drop .proto file here',
- 'wizard.step3.grpc.drop_subtitle': 'or click to browse',
- 'wizard.step3.grpc.change_file': 'Change file',
- 'wizard.step3.grpc.paste_content': 'Paste content',
- 'wizard.step3.grpc.parse_proto': 'Parse proto',
- 'wizard.step3.grpc.streaming_excluded': 'Streaming methods excluded',
- 'wizard.step3.grpc.streaming_hidden.one': 'Found and hid {count} streaming method — it is not compatible with MCP tool calls.',
- 'wizard.step3.grpc.streaming_hidden.other': 'Found and hid {count} streaming methods — they are not compatible with MCP tool calls.',
- 'wizard.step3.grpc.select_method': 'Select method',
- 'wizard.step3.grpc.discovery_live_title': 'Descriptor-driven discovery',
- 'wizard.step3.grpc.discovery_live_subtitle': 'Use the saved draft and a descriptor set to discover services through the backend',
- 'wizard.step3.grpc.no_upstream': 'No upstream selected',
- 'wizard.step3.grpc.select_upstream_first': 'Select an upstream in step 2 first',
- 'wizard.step3.grpc.open_tools': 'Open live descriptor tools',
- 'wizard.step3.grpc.connecting': 'Connecting…',
- 'wizard.step3.grpc.live_step5': 'Live descriptor discovery is handled in step 5.',
- 'wizard.step3.grpc.retry': 'Retry',
- 'wizard.step3.grpc.manual_title': 'Method route',
- 'wizard.step3.grpc.manual_subtitle': 'Full gRPC path to the unary method',
- 'wizard.step3.grpc.route': 'gRPC route',
- 'wizard.step3.grpc.route_hint': 'Format: /[package.]ServiceName/MethodName',
- 'wizard.step3.grpc.request_type': 'Request type',
- 'wizard.step3.grpc.response_type': 'Response type',
- 'wizard.step3.grpc.proto_doc_hint': 'Protobuf message name, for documentation.',
- 'wizard.step3.grpc.schema_title': 'Input and output schemas are defined in step 4',
- 'wizard.step3.grpc.schema_body': 'In manual mode you only need the route. The MCP tool input/output schemas — which the LLM uses to call this tool — are configured in the next step regardless of Protobuf definitions.',
- 'wizard.step3.grpc.signature_title': 'Method signature',
- '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',
- 'wizard.step3.soap.wsdl_subtitle': 'WSDL is required. Supporting XSD files are optional and can be uploaded before discovery.',
- 'wizard.step3.soap.choose_wsdl': 'Choose WSDL',
- 'wizard.step3.soap.choose_xsd': 'Choose XSD',
- 'wizard.step3.soap.no_wsdl': 'No WSDL selected',
- 'wizard.step3.soap.no_xsd': 'No XSD selected',
- 'wizard.step3.soap.upload_discover': 'Upload and inspect services',
- 'wizard.step3.soap.binding_title': 'Selected binding',
- 'wizard.step3.soap.binding_subtitle': 'The chosen service, port and operation become the target binding for this operation draft.',
- 'wizard.step3.soap.service_name': 'Service',
- 'wizard.step3.soap.port_name': 'Port',
- 'wizard.step3.soap.operation_name': 'Operation',
- 'wizard.step3.soap.version': 'SOAP version',
- 'wizard.step3.soap.action': 'SOAPAction',
- 'wizard.step3.soap.binding_style': 'Binding style',
- 'wizard.step3.soap.binding.document_literal': 'Document literal',
- 'wizard.step3.soap.binding.rpc_literal': 'RPC literal',
- 'wizard.step3.soap.endpoint_override': 'Endpoint override',
- 'wizard.step3.soap.endpoint_override_hint': 'Defaults to the upstream base URL from step 2. Override only when the binding address differs from the selected upstream.',
'wizard.step4.title': 'Tool config',
'wizard.step4.subtitle': 'Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.',
'wizard.step4.identity_title': 'Tool identity',
@@ -787,7 +709,7 @@ var TRANSLATIONS = {
'wizard.step5.live_title': 'Live validation and publishing',
'wizard.step5.draft_actions': 'Draft actions',
'wizard.step5.live_save_title': 'Live actions save the draft first',
- 'wizard.step5.live_save_body': 'Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.',
+ 'wizard.step5.live_save_body': 'Sample uploads, test runs, YAML actions and publish all persist the current draft before calling the backend.',
'wizard.step5.samples_title': 'Samples and draft generation',
'wizard.step5.samples_subtitle': 'Persist input/output samples, then generate schemas and mappings from real payloads.',
'wizard.step5.input_sample': 'Input sample',
@@ -803,8 +725,6 @@ var TRANSLATIONS = {
'wizard.step5.request_preview': 'Request preview',
'wizard.step5.response_preview': 'Response preview',
'wizard.step5.errors': 'Errors',
- 'wizard.step5.grpc_descriptor_title': 'gRPC descriptor set',
- 'wizard.step5.grpc_descriptor_subtitle': 'Upload a compiled descriptor set and discover unary methods from the saved draft.',
'wizard.step5.choose_descriptor': 'Choose descriptor set',
'wizard.step5.upload_discover': 'Upload and discover services',
'wizard.step5.no_descriptor': 'No descriptor set selected',
@@ -857,20 +777,6 @@ var TRANSLATIONS = {
'wizard.busy.publish': 'Publishing operation…',
'wizard.busy.descriptor': 'Uploading descriptor set…',
'wizard.busy.wsdl': 'Inspecting WSDL…',
- 'wizard.step3.soap.wsdl_selected': 'WSDL selected',
- 'wizard.step3.soap.wsdl_selected_body': 'The WSDL file is staged and will be uploaded with the current draft.',
- 'wizard.step3.soap.xsd_selected': 'XSD selected',
- 'wizard.step3.soap.xsd_selected_body': 'The XSD file is staged and will be uploaded with the current draft.',
- 'wizard.step3.soap.wsdl_uploaded': 'WSDL uploaded',
- 'wizard.step3.soap.xsd_uploaded': 'XSD uploaded',
- 'wizard.step3.soap.services_discovered': '{services} service(s), {ports} port(s) and {operations} operation(s) discovered from the uploaded contract.',
- 'wizard.step3.soap.services_none': 'No SOAP services discovered from the uploaded WSDL yet.',
- 'wizard.step3.soap.applied': 'SOAP binding applied',
- 'wizard.step3.soap.applied_body': 'The selected service, port and operation were applied to the current draft.',
- 'wizard.step3.soap.no_wsdl_error': 'WSDL is required',
- 'wizard.step3.soap.no_wsdl_error_body': 'Choose a WSDL file or reuse an already uploaded draft contract before inspection.',
- 'wizard.step3.soap.discovered': 'SOAP services discovered',
- 'wizard.step3.soap.discovered_body': 'The uploaded WSDL was inspected through the backend and the available bindings are ready to apply.',
'wizard.sample.input_saved': 'Input sample saved',
'wizard.sample.input_saved_body': 'The input sample is now stored against the current draft version.',
'wizard.sample.output_saved': 'Output sample saved',
@@ -919,15 +825,6 @@ var TRANSLATIONS = {
'wizard.yaml.loaded_body': 'The YAML document was loaded into the import editor.',
'wizard.publish.done': 'Operation published',
'wizard.publish.done_body': 'Version {version} is now published and can be bound into agents.',
- 'wizard.descriptor.selected': 'Descriptor set selected',
- 'wizard.descriptor.selected_body': 'Upload the descriptor set to discover unary gRPC methods from the backend.',
- 'wizard.descriptor.no_file': 'No descriptor set selected',
- 'wizard.descriptor.no_file_body': 'Choose a descriptor-set file before uploading it, or save a draft that already has one attached.',
- 'wizard.descriptor.discovered': 'Descriptor services discovered',
- 'wizard.descriptor.discovered_body': 'The backend parsed the descriptor set. Pick a method to apply its route and schemas.',
- 'wizard.descriptor.applied': 'gRPC method applied',
- 'wizard.descriptor.applied_body': 'The route and schemas were filled from the uploaded descriptor set.',
- 'wizard.descriptor.uploaded': 'Descriptor set uploaded',
'wizard.grpc.discovery_title': 'gRPC discovery moved to live descriptor flow',
'wizard.grpc.discovery_body': 'Server reflection is not wired in this deployment. Save the draft, then use the descriptor-set tools in Step 5 to upload a real descriptor set and discover unary methods from the backend.',
'wizard.grpc.no_methods': 'No unary RPC methods found.',
@@ -1732,9 +1629,6 @@ var TRANSLATIONS = {
'wizard.step2.path_hint': 'Добавляется к base URL upstream-а. Используйте {param} для path-параметров.',
'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.',
'wizard.step3.rest.method_title': 'HTTP метод',
@@ -1760,83 +1654,6 @@ var TRANSLATIONS = {
'wizard.step3.graphql.doc_subtitle': 'GraphQL-документ с типизированными объявлениями переменных',
'wizard.step3.graphql.variables_title': 'Переменные мапятся на шаге 5',
'wizard.step3.graphql.variables_body': 'Объявите все переменные в документе запроса в формате $variable: Type. На шаге 5 вы свяжете поля MCP input-а с этими переменными. Crank автоматически сериализует итоговый payload {"query","variables"}.',
- 'wizard.step3.grpc.title': 'RPC метод',
- 'wizard.step3.grpc.subtitle': 'Выберите, как описывать контракт gRPC-сервиса. Загрузите .proto файл, откройте методы через reflection или введите route вручную. Поддерживаются только unary RPC.',
- 'wizard.step3.grpc.source_proto': 'Proto файл',
- 'wizard.step3.grpc.source_reflection': 'Descriptor set',
- 'wizard.step3.grpc.source_manual': 'Вручную',
- 'wizard.step3.grpc.proto_title': 'Proto definition',
- 'wizard.step3.grpc.proto_subtitle': 'Загрузите или вставьте содержимое .proto файла',
- 'wizard.step3.grpc.drop_title': 'Перетащите сюда .proto файл',
- 'wizard.step3.grpc.drop_subtitle': 'или нажмите для выбора',
- 'wizard.step3.grpc.change_file': 'Сменить файл',
- 'wizard.step3.grpc.paste_content': 'Вставить содержимое',
- 'wizard.step3.grpc.parse_proto': 'Разобрать proto',
- 'wizard.step3.grpc.streaming_excluded': 'Стриминговые методы исключены',
- 'wizard.step3.grpc.streaming_hidden.one': 'Найден и скрыт {count} streaming method — он не поддерживается в MCP tool calls.',
- 'wizard.step3.grpc.streaming_hidden.few': 'Найдены и скрыты {count} streaming methods — они не поддерживаются в MCP tool calls.',
- 'wizard.step3.grpc.streaming_hidden.many': 'Найдено и скрыто {count} streaming methods — они не поддерживаются в MCP tool calls.',
- 'wizard.step3.grpc.streaming_hidden.other': 'Найдены и скрыты {count} streaming methods — они не поддерживаются в MCP tool calls.',
- 'wizard.step3.grpc.select_method': 'Выберите метод',
- 'wizard.step3.grpc.discovery_live_title': 'Поиск через descriptor set',
- 'wizard.step3.grpc.discovery_live_subtitle': 'Используйте сохраненный черновик и descriptor set, чтобы открыть сервисы через backend',
- 'wizard.step3.grpc.no_upstream': 'Upstream не выбран',
- 'wizard.step3.grpc.select_upstream_first': 'Сначала выберите upstream на шаге 2',
- 'wizard.step3.grpc.open_tools': 'Открыть live-инструменты descriptor-а',
- 'wizard.step3.grpc.connecting': 'Подключение…',
- 'wizard.step3.grpc.live_step5': 'Live-поиск descriptor-а выполняется на шаге 5.',
- 'wizard.step3.grpc.retry': 'Повторить',
- 'wizard.step3.grpc.manual_title': 'Маршрут метода',
- 'wizard.step3.grpc.manual_subtitle': 'Полный gRPC path до unary-метода',
- 'wizard.step3.grpc.route': 'gRPC route',
- 'wizard.step3.grpc.route_hint': 'Формат: /[package.]ServiceName/MethodName',
- 'wizard.step3.grpc.request_type': 'Тип запроса',
- 'wizard.step3.grpc.response_type': 'Тип ответа',
- 'wizard.step3.grpc.proto_doc_hint': 'Имя Protobuf-сообщения, только для документации.',
- 'wizard.step3.grpc.schema_title': 'Входная и выходная схемы задаются на шаге 4',
- 'wizard.step3.grpc.schema_body': 'В ручном режиме нужен только route. Входная и выходная схемы MCP-инструмента — которые LLM использует при вызове — настраиваются на следующем шаге независимо от Protobuf-определений.',
- 'wizard.step3.grpc.signature_title': 'Сигнатура метода',
- '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': 'Файлы контракта',
- 'wizard.step3.soap.wsdl_subtitle': 'WSDL обязателен. Supporting XSD files можно загрузить дополнительно до discovery.',
- 'wizard.step3.soap.choose_wsdl': 'Выбрать WSDL',
- 'wizard.step3.soap.choose_xsd': 'Выбрать XSD',
- 'wizard.step3.soap.no_wsdl': 'WSDL не выбран',
- 'wizard.step3.soap.no_xsd': 'XSD не выбран',
- 'wizard.step3.soap.upload_discover': 'Загрузить и получить сервисы',
- 'wizard.step3.soap.binding_title': 'Выбранный binding',
- 'wizard.step3.soap.binding_subtitle': 'Выбранные service, port и operation станут target binding для текущего draft.',
- 'wizard.step3.soap.service_name': 'Service',
- 'wizard.step3.soap.port_name': 'Port',
- 'wizard.step3.soap.operation_name': 'Operation',
- 'wizard.step3.soap.version': 'SOAP версия',
- 'wizard.step3.soap.action': 'SOAPAction',
- 'wizard.step3.soap.binding_style': 'Binding style',
- 'wizard.step3.soap.binding.document_literal': 'Document literal',
- 'wizard.step3.soap.binding.rpc_literal': 'RPC literal',
- 'wizard.step3.soap.endpoint_override': 'Endpoint override',
- 'wizard.step3.soap.endpoint_override_hint': 'По умолчанию берется upstream base URL из шага 2. Переопределяйте только если binding address отличается от выбранного upstream.',
'wizard.step4.title': 'Конфиг инструмента',
'wizard.step4.subtitle': 'Задайте имя инструмента, описание для LLM и определите входную/выходную схему. Описание — главный сигнал, по которому модель решает, вызывать ли этот инструмент.',
'wizard.step4.identity_title': 'Идентичность инструмента',
@@ -1870,7 +1687,7 @@ var TRANSLATIONS = {
'wizard.step5.live_title': 'Live-проверка и публикация',
'wizard.step5.draft_actions': 'Действия черновика',
'wizard.step5.live_save_title': 'Live-действия сначала сохраняют черновик',
- 'wizard.step5.live_save_body': 'Загрузка sample-ов, тестовые прогоны, YAML-действия, публикация и gRPC descriptor discovery сначала сохраняют текущий черновик перед вызовом backend-а.',
+ 'wizard.step5.live_save_body': 'Загрузка sample-ов, тестовые прогоны, YAML-действия и публикация сначала сохраняют текущий черновик перед вызовом backend-а.',
'wizard.step5.samples_title': 'Samples и генерация черновика',
'wizard.step5.samples_subtitle': 'Сохраните входные и выходные sample-ы, затем сгенерируйте схемы и маппинг из реальных payload-ов.',
'wizard.step5.input_sample': 'Входной sample',
@@ -1886,8 +1703,6 @@ var TRANSLATIONS = {
'wizard.step5.request_preview': 'Предпросмотр запроса',
'wizard.step5.response_preview': 'Предпросмотр ответа',
'wizard.step5.errors': 'Ошибки',
- 'wizard.step5.grpc_descriptor_title': 'gRPC descriptor set',
- 'wizard.step5.grpc_descriptor_subtitle': 'Загрузите скомпилированный descriptor set и откройте unary-методы из сохраненного черновика.',
'wizard.step5.choose_descriptor': 'Выбрать descriptor set',
'wizard.step5.upload_discover': 'Загрузить и открыть сервисы',
'wizard.step5.no_descriptor': 'Descriptor set не выбран',
@@ -1940,20 +1755,6 @@ var TRANSLATIONS = {
'wizard.busy.publish': 'Публикация операции…',
'wizard.busy.descriptor': 'Загрузка descriptor set…',
'wizard.busy.wsdl': 'Inspection WSDL…',
- 'wizard.step3.soap.wsdl_selected': 'WSDL выбран',
- 'wizard.step3.soap.wsdl_selected_body': 'WSDL staged и будет загружен вместе с текущим draft.',
- 'wizard.step3.soap.xsd_selected': 'XSD выбран',
- 'wizard.step3.soap.xsd_selected_body': 'XSD staged и будет загружен вместе с текущим draft.',
- 'wizard.step3.soap.wsdl_uploaded': 'WSDL загружен',
- 'wizard.step3.soap.xsd_uploaded': 'XSD загружен',
- 'wizard.step3.soap.services_discovered': 'Из загруженного контракта найдено {services} service(s), {ports} port(s) и {operations} operation(s).',
- 'wizard.step3.soap.services_none': 'Из загруженного WSDL пока не найдено ни одного SOAP service.',
- 'wizard.step3.soap.applied': 'SOAP binding применен',
- 'wizard.step3.soap.applied_body': 'Выбранные service, port и operation применены к текущему draft.',
- 'wizard.step3.soap.no_wsdl_error': 'Нужен WSDL',
- 'wizard.step3.soap.no_wsdl_error_body': 'Выберите WSDL или используйте уже загруженный контракт текущего draft перед inspection.',
- 'wizard.step3.soap.discovered': 'SOAP services получены',
- 'wizard.step3.soap.discovered_body': 'Загруженный WSDL был inspected через backend, и доступные bindings готовы к применению.',
'wizard.sample.input_saved': 'Входной sample сохранен',
'wizard.sample.input_saved_body': 'Входной sample теперь сохранен в текущей версии черновика.',
'wizard.sample.output_saved': 'Выходной sample сохранен',
@@ -2002,15 +1803,6 @@ var TRANSLATIONS = {
'wizard.yaml.loaded_body': 'YAML-документ загружен в editor импорта.',
'wizard.publish.done': 'Операция опубликована',
'wizard.publish.done_body': 'Версия {version} опубликована и теперь может быть привязана к агентам.',
- 'wizard.descriptor.selected': 'Descriptor set выбран',
- 'wizard.descriptor.selected_body': 'Загрузите descriptor set, чтобы открыть unary gRPC methods из backend.',
- 'wizard.descriptor.no_file': 'Descriptor set не выбран',
- 'wizard.descriptor.no_file_body': 'Выберите файл descriptor set перед загрузкой или сохраните черновик, у которого он уже прикреплен.',
- 'wizard.descriptor.discovered': 'Сервисы descriptor-а открыты',
- 'wizard.descriptor.discovered_body': 'Backend разобрал descriptor set. Выберите метод, чтобы применить route и схемы.',
- 'wizard.descriptor.applied': 'gRPC метод применен',
- 'wizard.descriptor.applied_body': 'Route и схемы заполнены из загруженного descriptor set.',
- 'wizard.descriptor.uploaded': 'Descriptor set загружен',
'wizard.grpc.discovery_title': 'gRPC discovery переведен в live descriptor flow',
'wizard.grpc.discovery_body': 'Server reflection в этом развертывании не подключен. Сохраните черновик и используйте инструменты descriptor set на шаге 5, чтобы загрузить реальный descriptor set и открыть unary methods из backend.',
'wizard.grpc.no_methods': 'Unary RPC methods не найдены.',
@@ -2166,237 +1958,13 @@ var TRANSLATIONS = {
};
Object.assign(TRANSLATIONS.en, {
- 'wizard.streaming.title': 'Streaming execution',
- 'wizard.streaming.subtitle': 'Bounded stream, session and async job settings for MCP tool families.',
- 'wizard.streaming.execution_mode': 'Execution mode',
- 'wizard.streaming.transport_behavior': 'Transport behavior',
- 'wizard.streaming.aggregation_mode': 'Aggregation mode',
- 'wizard.streaming.window_duration': 'Window duration (ms)',
- 'wizard.streaming.poll_interval': 'Poll interval (ms)',
- 'wizard.streaming.upstream_timeout': 'Upstream timeout (ms)',
- 'wizard.streaming.idle_timeout': 'Idle timeout (ms)',
- 'wizard.streaming.session_lifetime': 'Session lifetime (ms)',
- 'wizard.streaming.max_items': 'Max items',
- 'wizard.streaming.max_bytes': 'Max bytes',
- 'wizard.streaming.max_field_length': 'Max field length',
- 'wizard.streaming.sampling_rate': 'Sampling rate',
- 'wizard.streaming.items_path': 'Items path',
- 'wizard.streaming.summary_path': 'Summary path',
- 'wizard.streaming.done_path': 'Done path',
- 'wizard.streaming.cursor_path': 'Cursor path',
- 'wizard.streaming.status_path': 'Status path',
- 'wizard.streaming.redacted_paths': 'Redacted paths',
- 'wizard.streaming.truncate_item_fields': 'Truncate item fields',
- 'wizard.streaming.drop_duplicates': 'Drop duplicates',
- 'wizard.streaming.tool_family': 'Tool family',
- 'wizard.streaming.start_tool_name': 'Start tool name',
- 'wizard.streaming.poll_tool_name': 'Poll tool name',
- 'wizard.streaming.stop_tool_name': 'Stop tool name',
- 'wizard.streaming.status_tool_name': 'Status tool name',
- 'wizard.streaming.result_tool_name': 'Result tool name',
- 'wizard.streaming.cancel_tool_name': 'Cancel tool name',
- 'wizard.streaming.mode.unary': 'Unary',
- 'wizard.streaming.mode.window': 'Window',
- 'wizard.streaming.mode.session': 'Session',
- 'wizard.streaming.mode.async_job': 'Async job',
- 'wizard.streaming.transport.request_response': 'Request / response',
- 'wizard.streaming.transport.server_stream': 'Server stream',
- 'wizard.streaming.transport.stateful_session': 'Stateful session',
- 'wizard.streaming.transport.deferred_result': 'Deferred result',
- 'wizard.streaming.aggregation.raw_items': 'Raw items',
- 'wizard.streaming.aggregation.summary_only': 'Summary only',
- 'wizard.streaming.aggregation.summary_plus_samples': 'Summary + samples',
- 'wizard.streaming.aggregation.stats': 'Stats',
- 'wizard.streaming.aggregation.latest_state': 'Latest state',
- 'wizard.streaming.help.unary': 'Unary keeps the existing request-response model. Other modes create bounded stream-aware MCP tools.',
- 'wizard.streaming.help.window': 'Window mode collects a bounded slice of data and returns a compact result.',
- 'wizard.streaming.help.session': 'Session mode publishes start/poll/stop tool families backed by persisted stream sessions.',
- 'wizard.streaming.help.async_job': 'Async job mode publishes start/status/result/cancel tools and stores runtime job state.',
- 'stream_sessions.title': 'Stream sessions',
- 'stream_sessions.subtitle': 'Inspect stateful streaming sessions created by generated MCP tool families.',
- 'stream_sessions.refresh': 'Refresh',
- 'stream_sessions.summary': '{count} sessions',
- 'stream_sessions.filter.all_statuses': 'All statuses',
- 'stream_sessions.filter.all_modes': 'All modes',
- 'stream_sessions.loading.title': 'Loading stream sessions…',
- 'stream_sessions.loading.body': 'Fetching persisted session state for the current workspace.',
- 'stream_sessions.error.title': 'Unable to load stream sessions',
- 'stream_sessions.error.workspace': 'Workspace is not selected',
- 'stream_sessions.error.load': 'Failed to load stream sessions',
- 'stream_sessions.empty.title': 'No stream sessions found',
- 'stream_sessions.empty.body': 'Session-mode MCP tools will create entries here after start/poll/stop flows.',
- 'stream_sessions.operation': 'Operation: {operation}',
- 'stream_sessions.meta.created': 'Created',
- 'stream_sessions.meta.last_poll': 'Last poll',
- 'stream_sessions.meta.expires': 'Expires',
- 'stream_sessions.meta.agent': 'Agent',
- 'stream_sessions.cursor': 'Cursor',
- 'stream_sessions.stop': 'Stop session',
- 'stream_sessions.show_details': 'Show details',
- 'stream_sessions.hide_details': 'Hide details',
- 'stream_sessions.toast.stop.title': 'Session stopped',
- 'stream_sessions.toast.stop.body': 'The stream session was stopped.',
- 'stream_sessions.toast.stop_error.title': 'Stop failed',
- 'stream_sessions.toast.stop_error.body': 'Failed to stop stream session',
- 'stream_sessions.status.created': 'Created',
- 'stream_sessions.status.running': 'Running',
- 'stream_sessions.status.failed': 'Failed',
- 'stream_sessions.status.stopped': 'Stopped',
- 'stream_sessions.status.expired': 'Expired',
- 'async_jobs.title': 'Async jobs',
- 'async_jobs.subtitle': 'Inspect deferred jobs started by streaming MCP tool families.',
- 'async_jobs.refresh': 'Refresh',
- 'async_jobs.summary': '{count} jobs',
- 'async_jobs.filter.all_statuses': 'All statuses',
- 'async_jobs.loading.title': 'Loading async jobs…',
- 'async_jobs.loading.body': 'Fetching persisted job state for the current workspace.',
- 'async_jobs.error.title': 'Unable to load async jobs',
- 'async_jobs.error.workspace': 'Workspace is not selected',
- 'async_jobs.error.load': 'Failed to load async jobs',
- 'async_jobs.empty.title': 'No async jobs found',
- 'async_jobs.empty.body': 'Async job MCP tools will create entries here after start/status/result flows.',
- 'async_jobs.operation': 'Operation: {operation}',
- 'async_jobs.meta.created': 'Created',
- 'async_jobs.meta.updated': 'Updated',
- 'async_jobs.meta.finished': 'Finished',
- 'async_jobs.meta.expires': 'Expires',
- 'async_jobs.progress': 'Progress payload',
- 'async_jobs.error_preview': 'Error preview',
- 'async_jobs.result': 'Result payload',
- 'async_jobs.result_loading': 'Loading result…',
- 'async_jobs.result_error': 'Failed to load result',
- 'async_jobs.cancel': 'Cancel job',
- 'async_jobs.show_details': 'Show details',
- 'async_jobs.hide_details': 'Hide details',
- 'async_jobs.toast.cancel.title': 'Job cancelled',
- 'async_jobs.toast.cancel.body': 'The async job was cancelled.',
- 'async_jobs.toast.cancel_error.title': 'Cancel failed',
- 'async_jobs.toast.cancel_error.body': 'Failed to cancel async job',
- 'async_jobs.status.created': 'Created',
- 'async_jobs.status.running': 'Running',
- 'async_jobs.status.completed': 'Completed',
- 'async_jobs.status.failed': 'Failed',
- 'async_jobs.status.cancelled': 'Cancelled',
- 'async_jobs.status.expired': 'Expired',
});
Object.assign(TRANSLATIONS.ru, {
- 'wizard.streaming.title': 'Потоковое выполнение',
- 'wizard.streaming.subtitle': 'Ограниченные настройки стрима, сессий и async job для семейств MCP-инструментов.',
- 'wizard.streaming.execution_mode': 'Режим выполнения',
- 'wizard.streaming.transport_behavior': 'Поведение транспорта',
- 'wizard.streaming.aggregation_mode': 'Режим агрегации',
- 'wizard.streaming.window_duration': 'Длительность окна (мс)',
- 'wizard.streaming.poll_interval': 'Интервал poll (мс)',
- 'wizard.streaming.upstream_timeout': 'Таймаут upstream (мс)',
- 'wizard.streaming.idle_timeout': 'Таймаут простоя (мс)',
- 'wizard.streaming.session_lifetime': 'Время жизни сессии (мс)',
- 'wizard.streaming.max_items': 'Максимум элементов',
- 'wizard.streaming.max_bytes': 'Максимум байт',
- 'wizard.streaming.max_field_length': 'Максимальная длина поля',
- 'wizard.streaming.sampling_rate': 'Частота выборки',
- 'wizard.streaming.items_path': 'Путь к items',
- 'wizard.streaming.summary_path': 'Путь к summary',
- 'wizard.streaming.done_path': 'Путь к done',
- 'wizard.streaming.cursor_path': 'Путь к cursor',
- 'wizard.streaming.status_path': 'Путь к status',
- 'wizard.streaming.redacted_paths': 'Скрываемые пути',
- 'wizard.streaming.truncate_item_fields': 'Обрезать поля элементов',
- 'wizard.streaming.drop_duplicates': 'Удалять дубликаты',
- 'wizard.streaming.tool_family': 'Семейство инструментов',
- 'wizard.streaming.start_tool_name': 'Имя start-инструмента',
- 'wizard.streaming.poll_tool_name': 'Имя poll-инструмента',
- 'wizard.streaming.stop_tool_name': 'Имя stop-инструмента',
- 'wizard.streaming.status_tool_name': 'Имя status-инструмента',
- 'wizard.streaming.result_tool_name': 'Имя result-инструмента',
- 'wizard.streaming.cancel_tool_name': 'Имя cancel-инструмента',
- 'wizard.streaming.mode.unary': 'Unary',
- 'wizard.streaming.mode.window': 'Window',
- 'wizard.streaming.mode.session': 'Session',
- 'wizard.streaming.mode.async_job': 'Async job',
- 'wizard.streaming.transport.request_response': 'Запрос / ответ',
- 'wizard.streaming.transport.server_stream': 'Серверный стрим',
- 'wizard.streaming.transport.stateful_session': 'Состояние сессии',
- 'wizard.streaming.transport.deferred_result': 'Отложенный результат',
- 'wizard.streaming.aggregation.raw_items': 'Сырые элементы',
- 'wizard.streaming.aggregation.summary_only': 'Только summary',
- 'wizard.streaming.aggregation.summary_plus_samples': 'Summary + примеры',
- 'wizard.streaming.aggregation.stats': 'Статистика',
- 'wizard.streaming.aggregation.latest_state': 'Последнее состояние',
- 'wizard.streaming.help.unary': 'Unary сохраняет текущую модель запрос-ответ. Остальные режимы создают ограниченные потоковые MCP-инструменты.',
- 'wizard.streaming.help.window': 'Режим window собирает ограниченный срез данных и возвращает компактный результат.',
- 'wizard.streaming.help.session': 'Режим session публикует семейство start/poll/stop и опирается на сохраненные stream sessions.',
- 'wizard.streaming.help.async_job': 'Режим async job публикует start/status/result/cancel и хранит состояние фоновой задачи.',
- 'stream_sessions.title': 'Потоковые сессии',
- 'stream_sessions.subtitle': 'Просмотр stateful streaming-сессий, созданных сгенерированными семействами MCP-инструментов.',
- 'stream_sessions.refresh': 'Обновить',
- 'stream_sessions.summary': '{count} сессий',
- 'stream_sessions.filter.all_statuses': 'Все статусы',
- 'stream_sessions.filter.all_modes': 'Все режимы',
- 'stream_sessions.loading.title': 'Загрузка потоковых сессий…',
- 'stream_sessions.loading.body': 'Получаем сохраненное состояние сессий для текущего workspace.',
- 'stream_sessions.error.title': 'Не удалось загрузить потоковые сессии',
- 'stream_sessions.error.workspace': 'Workspace не выбран',
- 'stream_sessions.error.load': 'Не удалось загрузить потоковые сессии',
- 'stream_sessions.empty.title': 'Потоковых сессий пока нет',
- 'stream_sessions.empty.body': 'Сессии появятся здесь после вызовов start/poll/stop для session-mode инструментов.',
- 'stream_sessions.operation': 'Операция: {operation}',
- 'stream_sessions.meta.created': 'Создана',
- 'stream_sessions.meta.last_poll': 'Последний poll',
- 'stream_sessions.meta.expires': 'Истекает',
- 'stream_sessions.meta.agent': 'Агент',
- 'stream_sessions.cursor': 'Cursor',
- 'stream_sessions.stop': 'Остановить сессию',
- 'stream_sessions.show_details': 'Показать детали',
- 'stream_sessions.hide_details': 'Скрыть детали',
- 'stream_sessions.toast.stop.title': 'Сессия остановлена',
- 'stream_sessions.toast.stop.body': 'Потоковая сессия остановлена.',
- 'stream_sessions.toast.stop_error.title': 'Не удалось остановить',
- 'stream_sessions.toast.stop_error.body': 'Не удалось остановить потоковую сессию',
- 'stream_sessions.status.created': 'Создана',
- 'stream_sessions.status.running': 'Выполняется',
- 'stream_sessions.status.failed': 'Ошибка',
- 'stream_sessions.status.stopped': 'Остановлена',
- 'stream_sessions.status.expired': 'Истекла',
- 'async_jobs.title': 'Асинхронные задачи',
- 'async_jobs.subtitle': 'Просмотр отложенных задач, запущенных потоковыми семействами MCP-инструментов.',
- 'async_jobs.refresh': 'Обновить',
- 'async_jobs.summary': '{count} задач',
- 'async_jobs.filter.all_statuses': 'Все статусы',
- 'async_jobs.loading.title': 'Загрузка async job…',
- 'async_jobs.loading.body': 'Получаем сохраненное состояние задач для текущего workspace.',
- 'async_jobs.error.title': 'Не удалось загрузить async job',
- 'async_jobs.error.workspace': 'Workspace не выбран',
- 'async_jobs.error.load': 'Не удалось загрузить async job',
- 'async_jobs.empty.title': 'Асинхронных задач пока нет',
- 'async_jobs.empty.body': 'Задачи появятся здесь после вызовов start/status/result для async job инструментов.',
- 'async_jobs.operation': 'Операция: {operation}',
- 'async_jobs.meta.created': 'Создана',
- 'async_jobs.meta.updated': 'Обновлена',
- 'async_jobs.meta.finished': 'Завершена',
- 'async_jobs.meta.expires': 'Истекает',
- 'async_jobs.progress': 'Payload прогресса',
- 'async_jobs.error_preview': 'Предпросмотр ошибки',
- 'async_jobs.result': 'Payload результата',
- 'async_jobs.result_loading': 'Загрузка результата…',
- 'async_jobs.result_error': 'Не удалось загрузить результат',
- 'async_jobs.cancel': 'Отменить задачу',
- 'async_jobs.show_details': 'Показать детали',
- 'async_jobs.hide_details': 'Скрыть детали',
- 'async_jobs.toast.cancel.title': 'Задача отменена',
- 'async_jobs.toast.cancel.body': 'Асинхронная задача отменена.',
- 'async_jobs.toast.cancel_error.title': 'Не удалось отменить',
- 'async_jobs.toast.cancel_error.body': 'Не удалось отменить async job',
- 'async_jobs.status.created': 'Создана',
- 'async_jobs.status.running': 'Выполняется',
- 'async_jobs.status.completed': 'Завершена',
- 'async_jobs.status.failed': 'Ошибка',
- 'async_jobs.status.cancelled': 'Отменена',
- 'async_jobs.status.expired': 'Истекла',
});
function t(key) {
diff --git a/apps/ui/js/stream-test-run.js b/apps/ui/js/stream-test-run.js
deleted file mode 100644
index 9a8e7cd..0000000
--- a/apps/ui/js/stream-test-run.js
+++ /dev/null
@@ -1,47 +0,0 @@
-(function() {
- function currentWorkspaceId() {
- var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
- return workspace ? workspace.id : null;
- }
-
- async function startWindowTest(operationId, payload) {
- if (!window.CrankApi || !operationId) {
- throw new Error('Window test requires an operation id.');
- }
- return window.CrankApi.runOperationTest(currentWorkspaceId(), operationId, payload);
- }
-
- async function startSessionTest(workspaceId, operationId, payload) {
- return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
- }
-
- async function pollSessionTest(workspaceId, sessionId) {
- return window.CrankApi.getStreamSession(workspaceId || currentWorkspaceId(), sessionId);
- }
-
- async function stopSessionTest(workspaceId, sessionId) {
- return window.CrankApi.stopStreamSession(workspaceId || currentWorkspaceId(), sessionId);
- }
-
- async function startAsyncJobTest(workspaceId, operationId, payload) {
- return window.CrankApi.runOperationTest(workspaceId || currentWorkspaceId(), operationId, payload);
- }
-
- async function refreshAsyncJobStatus(workspaceId, jobId) {
- return window.CrankApi.getAsyncJob(workspaceId || currentWorkspaceId(), jobId);
- }
-
- async function loadAsyncJobResult(workspaceId, jobId) {
- return window.CrankApi.getAsyncJobResult(workspaceId || currentWorkspaceId(), jobId);
- }
-
- window.CrankStreamTestRun = {
- startWindowTest: startWindowTest,
- startSessionTest: startSessionTest,
- pollSessionTest: pollSessionTest,
- stopSessionTest: stopSessionTest,
- startAsyncJobTest: startAsyncJobTest,
- refreshAsyncJobStatus: refreshAsyncJobStatus,
- loadAsyncJobResult: loadAsyncJobResult,
- };
-}());
diff --git a/apps/ui/js/streaming-form.js b/apps/ui/js/streaming-form.js
deleted file mode 100644
index c8d0cf1..0000000
--- a/apps/ui/js/streaming-form.js
+++ /dev/null
@@ -1,330 +0,0 @@
-(function() {
- function tKey(key) {
- return typeof t === 'function' ? t(key) : key;
- }
-
- function currentProtocolCapabilities() {
- return window.CrankWizardState.currentProtocolCapabilities();
- }
-
- function text(id) {
- var element = document.getElementById(id);
- return element ? String(element.value || '').trim() : '';
- }
-
- function set(id, value) {
- var element = document.getElementById(id);
- if (element) {
- element.value = value || '';
- }
- }
-
- function number(id) {
- var value = text(id);
- if (!value) return null;
- var parsed = Number(value);
- return Number.isFinite(parsed) ? parsed : null;
- }
-
- function lines(id) {
- var value = text(id);
- return value
- ? value.split('\n').map(function(item) { return item.trim(); }).filter(Boolean)
- : [];
- }
-
- function checked(id) {
- var element = document.getElementById(id);
- return !!(element && element.checked);
- }
-
- function selectedMode() {
- return text('streaming-mode') || 'unary';
- }
-
- function transportOptionsForMode(mode) {
- var capabilities = currentProtocolCapabilities();
- var behaviors = (capabilities.supports_transport_behaviors || []).slice();
- if (mode === 'unary') return behaviors.filter(function(value) { return value === 'request_response'; });
- if (mode === 'window') return behaviors.filter(function(value) {
- return value === 'request_response' || value === 'server_stream';
- });
- if (mode === 'session') return behaviors.filter(function(value) {
- return value === 'server_stream' || value === 'stateful_session';
- });
- if (mode === 'async_job') return behaviors.filter(function(value) {
- return value === 'deferred_result';
- });
- return behaviors;
- }
-
- function modeOptionsForProtocol() {
- var capabilities = currentProtocolCapabilities();
- return (capabilities.supports_execution_modes || ['unary']).filter(function(mode) {
- return transportOptionsForMode(mode).length > 0 || mode === 'unary';
- });
- }
-
- function updateStreamingTransportOptions(preferredValue) {
- var element = document.getElementById('streaming-transport-behavior');
- if (!element) return;
- var options = transportOptionsForMode(selectedMode());
- element.innerHTML = '';
- options.forEach(function(value) {
- var option = document.createElement('option');
- option.value = value;
- option.textContent = tKey('wizard.streaming.transport.' + value);
- element.appendChild(option);
- });
- if (preferredValue && options.indexOf(preferredValue) >= 0) {
- element.value = preferredValue;
- } else if (options.length) {
- element.value = options[0];
- }
- }
-
- function updateStreamingModeOptions(preferredValue) {
- var element = document.getElementById('streaming-mode');
- if (!element) return;
- var options = modeOptionsForProtocol();
- element.innerHTML = '';
- options.forEach(function(value) {
- var option = document.createElement('option');
- option.value = value;
- option.textContent = tKey('wizard.streaming.mode.' + value);
- element.appendChild(option);
- });
- if (preferredValue && options.indexOf(preferredValue) >= 0) {
- element.value = preferredValue;
- } else {
- element.value = options.indexOf('unary') >= 0 ? 'unary' : (options[0] || 'unary');
- }
- updateStreamingTransportOptions();
- }
-
- function ensureStreamingToolFamilyDefaults(mode) {
- var base = text('tool-name') || 'stream_tool';
- if (mode === 'session') {
- if (!text('streaming-start-tool-name')) set('streaming-start-tool-name', base + '_start');
- if (!text('streaming-poll-tool-name')) set('streaming-poll-tool-name', base + '_poll');
- if (!text('streaming-stop-tool-name')) set('streaming-stop-tool-name', base + '_stop');
- }
- if (mode === 'async_job') {
- if (!text('streaming-start-tool-name')) set('streaming-start-tool-name', base + '_start');
- if (!text('streaming-status-tool-name')) set('streaming-status-tool-name', base + '_status');
- if (!text('streaming-result-tool-name')) set('streaming-result-tool-name', base + '_result');
- if (!text('streaming-cancel-tool-name')) set('streaming-cancel-tool-name', base + '_cancel');
- }
- }
-
- function updateStreamingConfigVisibility() {
- var mode = selectedMode();
- var card = document.getElementById('wizard-streaming-config-card');
- var help = document.getElementById('streaming-mode-help');
- var toolFamily = document.getElementById('streaming-tool-family-block');
- var asyncRow = document.getElementById('streaming-async-tool-family-row');
- if (!card) return;
-
- var isUnary = mode === 'unary';
- var isSession = mode === 'session';
- var isAsyncJob = mode === 'async_job';
-
- [
- 'streaming-window-duration-ms',
- 'streaming-poll-interval-ms',
- 'streaming-upstream-timeout-ms',
- 'streaming-idle-timeout-ms',
- 'streaming-session-lifetime-ms',
- 'streaming-max-items',
- 'streaming-max-bytes',
- 'streaming-max-field-length',
- 'streaming-sampling-rate',
- 'streaming-items-path',
- 'streaming-summary-path',
- 'streaming-done-path',
- 'streaming-cursor-path',
- 'streaming-status-path',
- 'streaming-redacted-paths',
- 'streaming-truncate-item-fields',
- 'streaming-drop-duplicates'
- ].forEach(function(id) {
- var node = document.getElementById(id);
- if (!node) return;
- var group = node.closest('.form-group') || node.closest('.checkbox-pill');
- if (group) {
- group.hidden = isUnary;
- }
- });
-
- if (help) {
- help.textContent = isUnary
- ? tKey('wizard.streaming.help.unary')
- : (isSession
- ? tKey('wizard.streaming.help.session')
- : (isAsyncJob
- ? tKey('wizard.streaming.help.async_job')
- : tKey('wizard.streaming.help.window')));
- }
-
- if (toolFamily) toolFamily.hidden = !(isSession || isAsyncJob);
- if (asyncRow) asyncRow.hidden = !isAsyncJob;
-
- ensureStreamingToolFamilyDefaults(mode);
- }
-
- function bindStreamingConfigControls() {
- updateStreamingModeOptions();
- updateStreamingConfigVisibility();
-
- var mode = document.getElementById('streaming-mode');
- if (mode) {
- mode.addEventListener('change', function() {
- updateStreamingTransportOptions();
- updateStreamingConfigVisibility();
- });
- }
- }
-
- function serializeStreamingConfig() {
- if (selectedMode() === 'unary') {
- return null;
- }
-
- var config = {
- mode: selectedMode(),
- transport_behavior: text('streaming-transport-behavior') || 'request_response',
- aggregation_mode: text('streaming-aggregation-mode') || 'summary_plus_samples',
- window_duration_ms: number('streaming-window-duration-ms'),
- poll_interval_ms: number('streaming-poll-interval-ms'),
- upstream_timeout_ms: number('streaming-upstream-timeout-ms'),
- idle_timeout_ms: number('streaming-idle-timeout-ms'),
- max_session_lifetime_ms: number('streaming-session-lifetime-ms'),
- max_items: number('streaming-max-items'),
- max_bytes: number('streaming-max-bytes'),
- max_field_length: number('streaming-max-field-length'),
- sampling_rate: number('streaming-sampling-rate'),
- items_path: text('streaming-items-path') || null,
- summary_path: text('streaming-summary-path') || null,
- done_path: text('streaming-done-path') || null,
- cursor_path: text('streaming-cursor-path') || null,
- status_path: text('streaming-status-path') || null,
- redacted_paths: lines('streaming-redacted-paths'),
- truncate_item_fields: checked('streaming-truncate-item-fields'),
- drop_duplicates: checked('streaming-drop-duplicates'),
- tool_family: {},
- };
-
- if (config.mode === 'session') {
- config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
- config.tool_family.poll_tool_name = text('streaming-poll-tool-name') || null;
- config.tool_family.stop_tool_name = text('streaming-stop-tool-name') || null;
- } else if (config.mode === 'async_job') {
- config.tool_family.start_tool_name = text('streaming-start-tool-name') || null;
- config.tool_family.status_tool_name = text('streaming-status-tool-name') || null;
- config.tool_family.result_tool_name = text('streaming-result-tool-name') || null;
- config.tool_family.cancel_tool_name = text('streaming-cancel-tool-name') || null;
- }
-
- return config;
- }
-
- function validateStreamingConfig(capabilities) {
- var config = serializeStreamingConfig();
- if (!config) {
- return { valid: true, errors: [] };
- }
-
- var errors = [];
- var capability = (capabilities || []).find(function(item) {
- return item.protocol === window.wizardProtocol;
- });
-
- if (capability && Array.isArray(capability.supports_execution_modes)) {
- var supported = capability.supports_execution_modes.map(String);
- if (supported.indexOf(config.mode) === -1) {
- errors.push('Selected execution mode is not supported by the current protocol.');
- }
- }
-
- if (!config.max_items || config.max_items <= 0) {
- errors.push('Max items must be a positive integer.');
- }
-
- if (!config.max_bytes || config.max_bytes <= 0) {
- errors.push('Max bytes must be a positive integer.');
- }
-
- if (config.mode === 'window' && (!config.window_duration_ms || config.window_duration_ms <= 0)) {
- errors.push('Window duration must be set for window mode.');
- }
-
- if (config.mode === 'session' && (!config.poll_interval_ms || config.poll_interval_ms <= 0)) {
- errors.push('Poll interval must be set for session mode.');
- }
-
- if (config.mode === 'async_job' && (!config.max_session_lifetime_ms || config.max_session_lifetime_ms <= 0)) {
- errors.push('Session lifetime must be set for async job mode.');
- }
-
- return {
- valid: errors.length === 0,
- errors: errors,
- config: config,
- };
- }
-
- window.CrankStreamingForm = {
- selectedStreamingMode: selectedMode,
- bindStreamingConfigControls: bindStreamingConfigControls,
- collectStreamingConfig: serializeStreamingConfig,
- applyStreamingConfig: function(streaming) {
- updateStreamingModeOptions(streaming && streaming.mode ? streaming.mode : 'unary');
- if (!streaming) {
- updateStreamingConfigVisibility();
- return;
- }
-
- if (streaming.window_duration_ms != null) set('streaming-window-duration-ms', String(streaming.window_duration_ms));
- if (streaming.poll_interval_ms != null) set('streaming-poll-interval-ms', String(streaming.poll_interval_ms));
- if (streaming.upstream_timeout_ms != null) set('streaming-upstream-timeout-ms', String(streaming.upstream_timeout_ms));
- if (streaming.idle_timeout_ms != null) set('streaming-idle-timeout-ms', String(streaming.idle_timeout_ms));
- if (streaming.max_session_lifetime_ms != null) set('streaming-session-lifetime-ms', String(streaming.max_session_lifetime_ms));
- if (streaming.max_items != null) set('streaming-max-items', String(streaming.max_items));
- if (streaming.max_bytes != null) set('streaming-max-bytes', String(streaming.max_bytes));
- if (streaming.max_field_length != null) set('streaming-max-field-length', String(streaming.max_field_length));
- if (streaming.sampling_rate != null) set('streaming-sampling-rate', String(streaming.sampling_rate));
- set('streaming-items-path', streaming.items_path || '');
- set('streaming-summary-path', streaming.summary_path || '');
- set('streaming-done-path', streaming.done_path || '');
- set('streaming-cursor-path', streaming.cursor_path || '');
- set('streaming-status-path', streaming.status_path || '');
- set('streaming-redacted-paths', (streaming.redacted_paths || []).join('\n'));
- var truncate = document.getElementById('streaming-truncate-item-fields');
- if (truncate) truncate.checked = !!streaming.truncate_item_fields;
- var dedupe = document.getElementById('streaming-drop-duplicates');
- if (dedupe) dedupe.checked = !!streaming.drop_duplicates;
- set('streaming-aggregation-mode', streaming.aggregation_mode || 'summary_plus_samples');
-
- updateStreamingTransportOptions(streaming.transport_behavior);
-
- var toolFamily = streaming.tool_family || {};
- set('streaming-start-tool-name', toolFamily.start_tool_name || '');
- set('streaming-poll-tool-name', toolFamily.poll_tool_name || '');
- set('streaming-stop-tool-name', toolFamily.stop_tool_name || '');
- set('streaming-status-tool-name', toolFamily.status_tool_name || '');
- set('streaming-result-tool-name', toolFamily.result_tool_name || '');
- set('streaming-cancel-tool-name', toolFamily.cancel_tool_name || '');
-
- updateStreamingConfigVisibility();
- },
- serializeStreamingConfig: serializeStreamingConfig,
- updateStreamingModeOptions: updateStreamingModeOptions,
- updateStreamingConfigVisibility: updateStreamingConfigVisibility,
- deserializeStreamingConfig: function(streaming) {
- if (typeof window.applyStreamingConfig === 'function') {
- window.applyStreamingConfig(streaming);
- }
- },
- validateStreamingConfig: validateStreamingConfig,
- };
-}());
diff --git a/apps/ui/js/wizard-artifacts.js b/apps/ui/js/wizard-artifacts.js
deleted file mode 100644
index 03f1d16..0000000
--- a/apps/ui/js/wizard-artifacts.js
+++ /dev/null
@@ -1,302 +0,0 @@
-function downloadTextFile(fileName, content, mimeType) {
- var blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
- var url = URL.createObjectURL(blob);
- var link = document.createElement('a');
- link.href = url;
- link.download = fileName;
- link.click();
- URL.revokeObjectURL(url);
-}
-
-async function exportWizardYaml() {
- await persistCurrentDraft(true);
- var yaml = await window.CrankApi.exportOperation(wizardWorkspaceId, wizardEditId, {
- mode: 'portable',
- version: currentDraftVersion(),
- });
- downloadTextFile((textValue('tool-name') || 'operation') + '.yaml', yaml, 'application/yaml');
- showWizardLiveStatus(tKey('wizard.yaml.exported'), tKey('wizard.yaml.exported_body'));
-}
-
-async function importWizardYaml() {
- if (!wizardWorkspaceId) {
- throw new Error(tKey('wizard.error.no_workspace'));
- }
- var yamlDocument = textValue('wizard-import-yaml-text');
- if (!yamlDocument) {
- showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true);
- return;
- }
- var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
- showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body'));
- window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id);
-}
-
-async function publishWizardOperation() {
- await persistCurrentDraft(true);
- var published = await window.CrankApi.publishOperation(
- wizardWorkspaceId,
- wizardEditId,
- currentDraftVersion()
- );
- await refreshCurrentOperationState();
- showWizardLiveStatus(
- tKey('wizard.publish.done'),
- tfKey('wizard.publish.done_body', { version: published.published_version })
- );
-}
-
-function handleImportYamlFileSelection(event) {
- var file = event.target.files && event.target.files[0];
- if (!file) return;
- var reader = new FileReader();
- reader.onload = function(loadEvent) {
- setValue('wizard-import-yaml-text', loadEvent.target.result || '');
- showWizardLiveStatus(tKey('wizard.yaml.loaded'), tKey('wizard.yaml.loaded_body'));
- };
- reader.readAsText(file);
-}
-
-function handleDescriptorSetSelection(event) {
- var file = event.target.files && event.target.files[0];
- if (!file) return;
- var reader = new FileReader();
- reader.onload = function(loadEvent) {
- wizardDescriptorSetUpload = {
- fileName: file.name,
- bytes: new Uint8Array(loadEvent.target.result),
- };
- var fileNameEl = document.getElementById('wizard-descriptor-set-name');
- if (fileNameEl) fileNameEl.textContent = file.name;
- showWizardLiveStatus(tKey('wizard.descriptor.selected'), tKey('wizard.descriptor.selected_body'));
- };
- reader.readAsArrayBuffer(file);
-}
-
-function handleSoapWsdlSelection(event) {
- var file = event.target.files && event.target.files[0];
- if (!file) return;
- var reader = new FileReader();
- reader.onload = function(loadEvent) {
- wizardSoapWsdlUpload = {
- fileName: file.name,
- bytes: new Uint8Array(loadEvent.target.result),
- };
- var fileNameEl = document.getElementById('wizard-soap-wsdl-name');
- if (fileNameEl) fileNameEl.textContent = file.name;
- showWizardLiveStatus(tKey('wizard.step3.soap.wsdl_selected'), tKey('wizard.step3.soap.wsdl_selected_body'));
- };
- reader.readAsArrayBuffer(file);
-}
-
-function handleSoapXsdSelection(event) {
- var file = event.target.files && event.target.files[0];
- if (!file) return;
- var reader = new FileReader();
- reader.onload = function(loadEvent) {
- wizardSoapXsdUpload = {
- fileName: file.name,
- bytes: new Uint8Array(loadEvent.target.result),
- };
- var fileNameEl = document.getElementById('wizard-soap-xsd-name');
- if (fileNameEl) fileNameEl.textContent = file.name;
- showWizardLiveStatus(tKey('wizard.step3.soap.xsd_selected'), tKey('wizard.step3.soap.xsd_selected_body'));
- };
- reader.readAsArrayBuffer(file);
-}
-
-function renderDescriptorServiceList(services) {
- grpcDescriptorServices = services || [];
- var summaryEl = document.getElementById('wizard-descriptor-services-summary');
- var listEl = document.getElementById('wizard-descriptor-services-list');
- if (!summaryEl || !listEl) return;
-
- var methodCount = grpcDescriptorServices.reduce(function(total, service) {
- return total + ((service.methods || []).length);
- }, 0);
- summaryEl.textContent = grpcDescriptorServices.length
- ? tfKey('wizard.grpc.services_discovered', { services: grpcDescriptorServices.length, methods: methodCount })
- : tKey('wizard.grpc.services_none');
- listEl.innerHTML = '';
-
- grpcDescriptorServices.forEach(function(service, serviceIndex) {
- var group = document.createElement('div');
- group.className = 'config-card';
- group.style.marginBottom = '0';
-
- var body = document.createElement('div');
- body.className = 'config-card-body';
- body.style.padding = '16px';
- body.style.gap = '10px';
-
- var title = document.createElement('div');
- title.className = 'config-card-title';
- title.textContent = (service.package ? service.package + '.' : '') + service.service;
- body.appendChild(title);
-
- (service.methods || []).forEach(function(method, methodIndex) {
- var button = document.createElement('button');
- button.type = 'button';
- button.className = 'btn-ghost-sm';
- button.style.width = '100%';
- button.style.justifyContent = 'space-between';
- button.textContent = method.name;
- button.addEventListener('click', function() {
- applyDiscoveredGrpcMethod(serviceIndex, methodIndex);
- });
- body.appendChild(button);
- });
-
- group.appendChild(body);
- listEl.appendChild(group);
- });
-}
-
-function applyDiscoveredGrpcMethod(serviceIndex, methodIndex) {
- var service = grpcDescriptorServices[serviceIndex];
- var method = service && service.methods ? service.methods[methodIndex] : null;
- if (!service || !method) return;
-
- var route = '/' + (service.package ? service.package + '.' : '') + service.service + '/' + method.name;
- selectGrpcSource('manual');
- setValue('grpc-manual-route', route);
- setValue('grpc-manual-req-type', method.name + 'Request');
- setValue('grpc-manual-res-type', method.name + 'Response');
- grpcManualRouteInput(route);
- grpcManualTypeInput();
- setValue('tool-input-schema', JSON.stringify(crankSchemaToJsonSchema(method.input_schema), null, 2));
- setValue('tool-output-schema', JSON.stringify(crankSchemaToJsonSchema(method.output_schema), null, 2));
- showWizardLiveStatus(tKey('wizard.descriptor.applied'), tKey('wizard.descriptor.applied_body'));
-}
-
-async function uploadDescriptorSetAndDiscover() {
- var hasSavedDescriptor = !!(
- wizardCurrentVersion
- && wizardCurrentVersion.target
- && wizardCurrentVersion.target.kind === 'grpc'
- && wizardCurrentVersion.target.descriptor_ref
- );
-
- if (!wizardDescriptorSetUpload && !hasSavedDescriptor) {
- showWizardLiveStatus(tKey('wizard.descriptor.no_file'), tKey('wizard.descriptor.no_file_body'), true);
- return;
- }
-
- await persistCurrentDraft(true);
-
- var services = await window.CrankApi.listGrpcServices(
- wizardWorkspaceId,
- wizardEditId,
- currentDraftVersion()
- );
- renderDescriptorServiceList(services.services || []);
- showWizardLiveStatus(tKey('wizard.descriptor.discovered'), tKey('wizard.descriptor.discovered_body'));
-}
-
-function renderSoapServiceCatalog(services) {
- soapServiceCatalog = services || [];
- var summaryEl = document.getElementById('wizard-soap-services-summary');
- var listEl = document.getElementById('wizard-soap-services-list');
- if (!summaryEl || !listEl) return;
-
- var portCount = 0;
- var operationCount = 0;
- soapServiceCatalog.forEach(function(service) {
- portCount += (service.ports || []).length;
- (service.ports || []).forEach(function(port) {
- operationCount += (port.operations || []).length;
- });
- });
-
- summaryEl.textContent = soapServiceCatalog.length
- ? tfKey('wizard.step3.soap.services_discovered', {
- services: soapServiceCatalog.length,
- ports: portCount,
- operations: operationCount
- })
- : tKey('wizard.step3.soap.services_none');
- listEl.innerHTML = '';
-
- soapServiceCatalog.forEach(function(service, serviceIndex) {
- var group = document.createElement('div');
- group.className = 'config-card';
- group.style.marginBottom = '0';
-
- var body = document.createElement('div');
- body.className = 'config-card-body';
- body.style.padding = '16px';
- body.style.gap = '10px';
-
- var title = document.createElement('div');
- title.className = 'config-card-title';
- title.textContent = service.service_name;
- body.appendChild(title);
-
- (service.ports || []).forEach(function(port, portIndex) {
- var portLabel = document.createElement('div');
- portLabel.className = 'form-hint';
- portLabel.textContent = port.port_name + ' · ' + port.soap_version;
- body.appendChild(portLabel);
-
- (port.operations || []).forEach(function(operation, operationIndex) {
- var button = document.createElement('button');
- button.type = 'button';
- button.className = 'btn-ghost-sm';
- button.style.width = '100%';
- button.style.justifyContent = 'space-between';
- button.textContent = operation.operation_name;
- button.addEventListener('click', function() {
- applyDiscoveredSoapOperation(serviceIndex, portIndex, operationIndex);
- });
- body.appendChild(button);
- });
- });
-
- group.appendChild(body);
- listEl.appendChild(group);
- });
-}
-
-function applyDiscoveredSoapOperation(serviceIndex, portIndex, operationIndex) {
- var service = soapServiceCatalog[serviceIndex];
- var port = service && service.ports ? service.ports[portIndex] : null;
- var operation = port && port.operations ? port.operations[operationIndex] : null;
- if (!service || !port || !operation) return;
-
- setValue('soap-service-name', service.service_name);
- setValue('soap-port-name', port.port_name);
- setValue('soap-operation-name', operation.operation_name);
- setValue('soap-version', port.soap_version || 'soap_11');
- setValue('soap-action', operation.soap_action || '');
- setValue('soap-binding-style', operation.binding_style || 'document_literal');
- if (!textValue('soap-endpoint-override') && port.endpoint) {
- setValue('soap-endpoint-override', port.endpoint);
- }
-
- showWizardLiveStatus(tKey('wizard.step3.soap.applied'), tKey('wizard.step3.soap.applied_body'));
-}
-
-async function uploadSoapArtifactsAndInspect() {
- var hasSavedWsdl = !!(
- wizardCurrentVersion
- && wizardCurrentVersion.target
- && wizardCurrentVersion.target.kind === 'soap'
- && wizardCurrentVersion.target.wsdl_ref
- && wizardCurrentVersion.target.wsdl_ref !== 'sample_wsdl_pending'
- );
-
- if (!wizardSoapWsdlUpload && !hasSavedWsdl) {
- showWizardLiveStatus(tKey('wizard.step3.soap.no_wsdl_error'), tKey('wizard.step3.soap.no_wsdl_error_body'), true);
- return;
- }
-
- await persistCurrentDraft(true);
-
- var services = await window.CrankApi.listSoapServices(
- wizardWorkspaceId,
- wizardEditId,
- currentDraftVersion()
- );
- renderSoapServiceCatalog(services.services || []);
- showWizardLiveStatus(tKey('wizard.step3.soap.discovered'), tKey('wizard.step3.soap.discovered_body'));
-}
diff --git a/apps/ui/js/wizard-grpc.js b/apps/ui/js/wizard-grpc.js
deleted file mode 100644
index 0d58c72..0000000
--- a/apps/ui/js/wizard-grpc.js
+++ /dev/null
@@ -1,394 +0,0 @@
-function selectGrpcSource(source) {
- grpcSource = source;
- document.querySelectorAll('.grpc-source-btn').forEach(function(btn) {
- btn.classList.toggle('active', btn.dataset.source === source);
- });
- document.getElementById('grpc-src-proto').hidden = source !== 'proto';
- document.getElementById('grpc-src-reflection').hidden = source !== 'reflection';
- document.getElementById('grpc-src-manual').hidden = source !== 'manual';
- var detail = document.getElementById('grpc-method-detail');
- if (detail) detail.hidden = true;
- selectedRpcMethod = null;
-}
-
-function extractBlocks(text, keyword) {
- var blocks = [];
- var re = new RegExp(keyword + '\\s+(\\w+)\\s*\\{', 'g');
- var match;
- while ((match = re.exec(text)) !== null) {
- var name = match[1];
- var start = match.index + match[0].length;
- var depth = 1;
- var i = start;
- while (i < text.length && depth > 0) {
- if (text[i] === '{') depth++;
- else if (text[i] === '}') depth--;
- i++;
- }
- blocks.push({ name: name, body: text.slice(start, i - 1), start: match.index });
- }
- return blocks;
-}
-
-function stripProtoComments(text) {
- text = text.replace(/\/\*[\s\S]*?\*\//g, '');
- text = text.replace(/\/\/[^\n]*/g, '');
- return text;
-}
-
-function parseProto(text) {
- text = stripProtoComments(text);
-
- var pkg = '';
- var pkgMatch = text.match(/\bpackage\s+([\w.]+)\s*;/);
- if (pkgMatch) pkg = pkgMatch[1];
-
- var messages = {};
- extractBlocks(text, 'message').forEach(function(blk) {
- var fields = [];
- var fieldRe = /\b(repeated\s+)?([\w.]+)\s+(\w+)\s*=\s*\d+/g;
- var fm;
- while ((fm = fieldRe.exec(blk.body)) !== null) {
- if (['option', 'reserved', 'extensions', 'oneof'].indexOf(fm[2]) !== -1) continue;
- fields.push({ repeated: !!fm[1], type: fm[2], name: fm[3] });
- }
- messages[blk.name] = fields;
- });
-
- var streamingCount = 0;
- var services = [];
-
- extractBlocks(text, 'service').forEach(function(svc) {
- var methods = [];
- var rpcRe = /\brpc\s+(\w+)\s*\(\s*(stream\s+)?([\w.]+)\s*\)\s*returns\s*\(\s*(stream\s+)?([\w.]+)\s*\)/g;
- var rm;
- while ((rm = rpcRe.exec(svc.body)) !== null) {
- var streamIn = !!rm[2];
- var streamOut = !!rm[4];
- if (streamIn || streamOut) {
- streamingCount += 1;
- } else {
- var reqType = rm[3].split('.').pop();
- var resType = rm[5].split('.').pop();
- methods.push({ name: rm[1], request: reqType, response: resType });
- }
- }
- if (methods.length > 0) {
- services.push({ name: svc.name, methods: methods });
- }
- });
-
- return { package: pkg, services: services, messages: messages, streamingCount: streamingCount };
-}
-
-function renderServiceResults(parsed, ids) {
- protoParsed = parsed;
- selectedRpcMethod = null;
-
- var notice = document.getElementById(ids.notice);
- var countEl = document.getElementById(ids.count);
- if (notice) {
- notice.hidden = parsed.streamingCount <= 0;
- if (countEl && parsed.streamingCount > 0) countEl.textContent = parsed.streamingCount;
- }
-
- var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0);
- var summaryEl = document.getElementById(ids.summary);
- if (summaryEl) {
- var servicesLabel = window.tPlural('wizard.grpc.services_label', parsed.services.length, {
- count: parsed.services.length,
- });
- var methodsLabel = window.tPlural('wizard.grpc.methods_label', totalMethods, {
- count: totalMethods,
- });
- summaryEl.textContent = tfKey('wizard.grpc.services_found', {
- services_label: servicesLabel,
- methods_label: methodsLabel,
- });
- }
-
- var listEl = document.getElementById(ids.list);
- if (!listEl) return;
-
- listEl.innerHTML = '';
- if (parsed.services.length === 0) {
- var emptyDiv = document.createElement('div');
- emptyDiv.className = 'proto-empty';
- emptyDiv.textContent = tKey('wizard.grpc.no_methods');
- listEl.appendChild(emptyDiv);
- } else {
- var svcTmpl = document.getElementById('tmpl-proto-service-group');
- var methodTmpl = document.getElementById('tmpl-proto-method-card');
- parsed.services.forEach(function(svc) {
- var svcNode = svcTmpl.content.cloneNode(true);
- svcNode.querySelector('.proto-service-name').textContent = svc.name;
- var grid = svcNode.querySelector('.proto-method-grid');
- svc.methods.forEach(function(m) {
- var mNode = methodTmpl.content.cloneNode(true);
- var btn = mNode.querySelector('.proto-method-card');
- btn.dataset.service = svc.name;
- btn.dataset.method = m.name;
- btn.dataset.req = m.request;
- btn.dataset.res = m.response;
- mNode.querySelector('.proto-method-name').textContent = m.name;
- mNode.querySelector('.proto-req-type').textContent = m.request;
- mNode.querySelector('.proto-res-type').textContent = m.response;
- grid.appendChild(mNode);
- });
- listEl.appendChild(svcNode);
- });
- }
-
- document.getElementById(ids.view).hidden = false;
- document.getElementById('grpc-method-detail').hidden = true;
-}
-
-function renderParsedProto(parsed) {
- var notice = document.getElementById('proto-streaming-text');
- if (notice && parsed && parsed.streamingCount) {
- notice.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, {
- count: parsed.streamingCount,
- });
- }
- renderServiceResults(parsed, {
- notice: 'proto-streaming-notice', count: 'proto-streaming-count',
- summary: 'proto-services-summary', list: 'proto-services-list', view: 'proto-parsed-view',
- });
-}
-
-function renderReflectionResult(parsed) {
- var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text');
- if (reflectText && parsed && parsed.streamingCount) {
- reflectText.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, {
- count: parsed.streamingCount,
- });
- }
- renderServiceResults(parsed, {
- notice: 'reflect-streaming-notice', count: 'reflect-streaming-count',
- summary: 'reflect-services-summary', list: 'reflect-services-list', view: 'reflect-parsed-view',
- });
-}
-
-function selectRpcMethod(btn) {
- document.querySelectorAll('.proto-method-card').forEach(function(b) { b.classList.remove('active'); });
- btn.classList.add('active');
-
- var svcName = btn.dataset.service;
- var methodName = btn.dataset.method;
- var reqType = btn.dataset.req;
- var resType = btn.dataset.res;
-
- selectedRpcMethod = { service: svcName, method: methodName, request: reqType, response: resType };
-
- var pkg = protoParsed && protoParsed.package ? protoParsed.package + '.' : '';
- var grpcPath = '/' + pkg + svcName + '/' + methodName;
-
- var pathEl = document.getElementById('grpc-path-value');
- if (pathEl) pathEl.textContent = grpcPath;
-
- var sub = document.getElementById('grpc-method-subtitle');
- if (sub) sub.textContent = 'rpc ' + methodName + ' (' + reqType + ') returns (' + resType + ')';
-
- var reqTypeEl = document.getElementById('grpc-req-type');
- var resTypeEl = document.getElementById('grpc-res-type');
- if (reqTypeEl) reqTypeEl.textContent = reqType;
- if (resTypeEl) resTypeEl.textContent = resType;
-
- var sigGrid = document.getElementById('grpc-detail-sig');
- if (sigGrid) sigGrid.hidden = false;
-
- renderFieldList('grpc-req-fields', reqType);
- renderFieldList('grpc-res-fields', resType);
-
- document.getElementById('grpc-method-detail').hidden = false;
-}
-
-function renderFieldList(elId, typeName) {
- var el = document.getElementById(elId);
- if (!el) return;
- var fields = protoParsed && protoParsed.messages[typeName];
- el.innerHTML = '';
- if (!fields || fields.length === 0) {
- var emptyEl = document.createElement('div');
- emptyEl.className = 'proto-field-empty';
- emptyEl.textContent = '';
- emptyEl.appendChild(document.createTextNode(tKey('wizard.grpc.fields_unresolved')));
- emptyEl.appendChild(document.createElement('br'));
- var hint = document.createElement('span');
- hint.style.color = 'var(--text-muted)';
- hint.style.fontSize = '11px';
- hint.textContent = tKey('wizard.grpc.fields_unresolved_body');
- emptyEl.appendChild(hint);
- el.appendChild(emptyEl);
- return;
- }
- var fieldTmpl = document.getElementById('tmpl-proto-field-row');
- fields.forEach(function(f) {
- var node = fieldTmpl.content.cloneNode(true);
- node.querySelector('.proto-field-name').textContent = f.name;
- node.querySelector('.proto-field-type').textContent = (f.repeated ? 'repeated ' : '') + f.type;
- el.appendChild(node);
- });
-}
-
-function handleProtoFile(event) {
- var file = event.target.files[0];
- if (!file) return;
- var reader = new FileReader();
- reader.onload = function(e) {
- wizardProtoUpload = { fileName: file.name, content: e.target.result };
- showProtoFileInfo(file.name, file.size);
- var parsed = parseProto(e.target.result);
- renderParsedProto(parsed);
- };
- reader.readAsText(file);
-}
-
-function handleProtoDrop(event) {
- event.preventDefault();
- var dropzone = document.getElementById('proto-dropzone');
- if (dropzone) dropzone.classList.remove('drag-over');
- var file = event.dataTransfer.files[0];
- if (!file || !file.name.endsWith('.proto')) return;
- var reader = new FileReader();
- reader.onload = function(e) {
- wizardProtoUpload = { fileName: file.name, content: e.target.result };
- showProtoFileInfo(file.name, file.size);
- renderParsedProto(parseProto(e.target.result));
- };
- reader.readAsText(file);
-}
-
-function showProtoFileInfo(name, size) {
- var dropzone = document.getElementById('proto-dropzone');
- var info = document.getElementById('proto-file-info');
- var nameEl = document.getElementById('proto-file-name');
- var sizeEl = document.getElementById('proto-file-size');
- if (dropzone) dropzone.hidden = true;
- if (info) info.hidden = false;
- if (nameEl) nameEl.textContent = name;
- if (sizeEl) sizeEl.textContent = (size / 1024).toFixed(1) + ' KB';
- document.getElementById('proto-paste-area').hidden = true;
- document.getElementById('proto-paste-btn').textContent = 'Paste content';
-}
-
-function resetProto() {
- var dropzone = document.getElementById('proto-dropzone');
- var info = document.getElementById('proto-file-info');
- var parsedView = document.getElementById('proto-parsed-view');
- if (dropzone) dropzone.hidden = false;
- if (info) info.hidden = true;
- if (parsedView) parsedView.hidden = true;
- var fileInput = document.getElementById('proto-file-input');
- if (fileInput) fileInput.value = '';
- protoParsed = null;
- selectedRpcMethod = null;
- wizardProtoUpload = null;
-}
-
-function toggleProtoPaste() {
- var area = document.getElementById('proto-paste-area');
- var btn = document.getElementById('proto-paste-btn');
- if (!area) return;
- var open = !area.hidden;
- area.hidden = open;
- if (btn) btn.textContent = open ? 'Paste content' : 'Cancel paste';
- if (!open) {
- var ta = document.getElementById('proto-paste-input');
- if (ta) setTimeout(function() { ta.focus(); }, 50);
- }
-}
-
-function cancelProtoPaste() {
- var area = document.getElementById('proto-paste-area');
- var btn = document.getElementById('proto-paste-btn');
- if (area) area.hidden = true;
- if (btn) btn.textContent = 'Paste content';
-}
-
-function parseProtoPasted() {
- var ta = document.getElementById('proto-paste-input');
- if (!ta || !ta.value.trim()) return;
- var parsed = parseProto(ta.value);
- wizardProtoUpload = { fileName: 'pasted.proto', content: ta.value };
- document.getElementById('proto-paste-area').hidden = true;
- var dropzone = document.getElementById('proto-dropzone');
- if (dropzone) dropzone.hidden = true;
- var info = document.getElementById('proto-file-info');
- var nameEl = document.getElementById('proto-file-name');
- var sizeEl = document.getElementById('proto-file-size');
- if (info) info.hidden = false;
- if (nameEl) nameEl.textContent = 'pasted.proto';
- if (sizeEl) sizeEl.textContent = (ta.value.length / 1024).toFixed(1) + ' KB';
- renderParsedProto(parsed);
-}
-
-var REFLECTION_MOCK = {
- package: 'acme.v1',
- streamingCount: 2,
- services: [
- { name: 'ContactService', methods: [
- { name: 'GetContact', request: 'GetContactRequest', response: 'Contact' },
- { name: 'CreateContact', request: 'CreateContactRequest', response: 'Contact' },
- { name: 'UpdateContact', request: 'UpdateContactRequest', response: 'Contact' },
- { name: 'DeleteContact', request: 'DeleteContactRequest', response: 'DeleteContactResponse' },
- ]},
- { name: 'CompanyService', methods: [
- { name: 'GetCompany', request: 'GetCompanyRequest', response: 'Company' },
- { name: 'ListCompanies', request: 'ListCompaniesRequest', response: 'ListCompaniesResponse' },
- ]},
- ],
- messages: {
- 'GetContactRequest': [{ type: 'string', name: 'id' }],
- Contact: [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }, { type: 'string', name: 'created_at' }],
- 'CreateContactRequest': [{ type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }],
- 'UpdateContactRequest': [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }],
- 'DeleteContactRequest': [{ type: 'string', name: 'id' }],
- 'DeleteContactResponse': [{ type: 'bool', name: 'success' }],
- 'GetCompanyRequest': [{ type: 'string', name: 'id' }],
- Company: [{ type: 'string', name: 'id' }, { type: 'string', name: 'name' }, { type: 'string', name: 'domain' }, { type: 'int32', name: 'employee_count' }],
- 'ListCompaniesRequest': [{ type: 'int32', name: 'page' }, { type: 'int32', name: 'page_size' }, { type: 'string', name: 'query' }],
- 'ListCompaniesResponse': [{ type: 'Company', name: 'companies', repeated: true }, { type: 'int32', name: 'total' }],
- },
-};
-
-function startReflection() {
- goToStep(5);
- showWizardLiveStatus(
- tKey('wizard.grpc.discovery_title'),
- tKey('wizard.grpc.discovery_body')
- );
-}
-
-function grpcManualRouteInput(val) {
- var detail = document.getElementById('grpc-method-detail');
- var sigGrid = document.getElementById('grpc-detail-sig');
- if (!val || !val.startsWith('/') || val.indexOf('/', 1) === -1) {
- if (detail) detail.hidden = true;
- return;
- }
- var parts = val.replace(/^\//, '').split('/');
- var servicePart = parts[0] || '';
- var methodName = parts[1] || '';
- var serviceName = servicePart.split('.').pop();
-
- var pathEl = document.getElementById('grpc-path-value');
- if (pathEl) pathEl.textContent = val;
-
- var sub = document.getElementById('grpc-method-subtitle');
- if (sub) sub.textContent = serviceName + '/' + methodName;
-
- if (sigGrid) sigGrid.hidden = true;
-
- grpcManualTypeInput();
- if (detail) detail.hidden = false;
-}
-
-function grpcManualTypeInput() {
- var reqInput = document.getElementById('grpc-manual-req-type');
- var resInput = document.getElementById('grpc-manual-res-type');
- var reqTypeEl = document.getElementById('grpc-req-type');
- var resTypeEl = document.getElementById('grpc-res-type');
- if (reqTypeEl) reqTypeEl.textContent = (reqInput && reqInput.value.trim()) || '—';
- if (resTypeEl) resTypeEl.textContent = (resInput && resInput.value.trim()) || '—';
-}
diff --git a/apps/ui/js/wizard-live.js b/apps/ui/js/wizard-live.js
index 3d1a01a..5d31658 100644
--- a/apps/ui/js/wizard-live.js
+++ b/apps/ui/js/wizard-live.js
@@ -271,12 +271,71 @@ function safeStringify(value) {
return JSON.stringify(value, null, 2);
}
+function downloadTextFile(fileName, content, mimeType) {
+ var blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
+ var url = URL.createObjectURL(blob);
+ var link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ link.click();
+ URL.revokeObjectURL(url);
+}
+
function setTextareaValue(id, value) {
var element = document.getElementById(id);
if (!element) return;
element.value = safeStringify(value);
}
+async function exportWizardYaml() {
+ await persistCurrentDraft(true);
+ var yaml = await window.CrankApi.exportOperation(wizardWorkspaceId, wizardEditId, {
+ mode: 'portable',
+ version: currentDraftVersion(),
+ });
+ downloadTextFile((textValue('tool-name') || 'operation') + '.yaml', yaml, 'application/yaml');
+ showWizardLiveStatus(tKey('wizard.yaml.exported'), tKey('wizard.yaml.exported_body'));
+}
+
+async function importWizardYaml() {
+ if (!wizardWorkspaceId) {
+ throw new Error(tKey('wizard.error.no_workspace'));
+ }
+ var yamlDocument = textValue('wizard-import-yaml-text');
+ if (!yamlDocument) {
+ showWizardLiveStatus(tKey('wizard.yaml.none'), tKey('wizard.yaml.none_body'), true);
+ return;
+ }
+ var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
+ showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body'));
+ window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id);
+}
+
+async function publishWizardOperation() {
+ await persistCurrentDraft(true);
+ var published = await window.CrankApi.publishOperation(
+ wizardWorkspaceId,
+ wizardEditId,
+ currentDraftVersion()
+ );
+ await refreshCurrentOperationState();
+ showWizardLiveStatus(
+ tKey('wizard.publish.done'),
+ tfKey('wizard.publish.done_body', { version: published.published_version })
+ );
+}
+
+function handleImportYamlFileSelection(event) {
+ var file = event.target.files && event.target.files[0];
+ if (!file) return;
+ var reader = new FileReader();
+ reader.onload = function(loadEvent) {
+ setValue('wizard-import-yaml-text', loadEvent.target.result || '');
+ showWizardLiveStatus(tKey('wizard.yaml.loaded'), tKey('wizard.yaml.loaded_body'));
+ };
+ reader.readAsText(file);
+}
+
function describeWizardTestResult(result) {
return {
title: result.ok ? tKey('wizard.test.completed') : tKey('wizard.test.failed'),
diff --git a/apps/ui/js/wizard-shell.js b/apps/ui/js/wizard-shell.js
index df864f2..36af988 100644
--- a/apps/ui/js/wizard-shell.js
+++ b/apps/ui/js/wizard-shell.js
@@ -203,7 +203,7 @@
var supported = capabilities && Array.isArray(capabilities.supported_protocols)
? capabilities.supported_protocols.slice()
: ['rest'];
- var hiddenProtocols = [];
+ var knownProtocols = ['rest', 'graphql', 'grpc', 'websocket', 'soap'];
document.querySelectorAll('.protocol-card').forEach(function(card) {
var protocol = card.dataset.protocol || 'rest';
@@ -213,10 +213,13 @@
if (!isSupported) {
card.classList.remove('selected');
card.setAttribute('aria-checked', 'false');
- hiddenProtocols.push(protocol);
}
});
+ var hiddenProtocols = knownProtocols.filter(function(protocol) {
+ return supported.indexOf(protocol) === -1;
+ });
+
if (supported.indexOf(window.wizardProtocol) === -1) {
window.wizardProtocol = supported[0] || 'rest';
}
diff --git a/apps/ui/tests/e2e/agents.spec.js b/apps/ui/tests/e2e/agents.spec.js
index 8adbddc..4851d59 100644
--- a/apps/ui/tests/e2e/agents.spec.js
+++ b/apps/ui/tests/e2e/agents.spec.js
@@ -12,6 +12,8 @@ test('agents page shows demo cards and edit drawer opens', async ({ page }) => {
await page.getByRole('button', { name: localized('New agent', 'Новый агент') }).click();
await expect(page.locator('.drawer-title')).toHaveText(localized('New agent', 'Новый агент'));
await expect(page.locator('.drawer-subtitle')).toContainText(localized('static agent-key boundary', 'статических ключах агента'));
- await expect(page.locator('.form-hint')).toContainText(localized('Keep the slug stable', 'Сохраняйте slug стабильным'));
+ await expect(page.locator('[data-i18n="agents.drawer.slug_hint"]')).toContainText(
+ localized('Keep the slug stable', 'Сохраняйте slug стабильным')
+ );
await expect(page.locator('.drawer')).toContainText(/mcp/i);
});
diff --git a/apps/ui/tests/e2e/api-keys.spec.js b/apps/ui/tests/e2e/api-keys.spec.js
index 8a86d2e..ea15dc8 100644
--- a/apps/ui/tests/e2e/api-keys.spec.js
+++ b/apps/ui/tests/e2e/api-keys.spec.js
@@ -14,7 +14,7 @@ test('api keys page opens create key flow', async ({ page }) => {
await page.goto('/api-keys');
await expect(page.locator('.page-title')).toHaveText(localized('Agent Keys', 'Ключи агентов'));
await expect(page.locator('[data-testid="machine-access-summary"]')).toContainText(
- localized('static AI-agent keys', 'статические ключи AI-агентов')
+ localized('static agent keys', 'статические ключи')
);
await expect(page.locator('#machine-access-note')).toContainText(
localized('commercial edition', 'коммерческую редакцию')
diff --git a/apps/ui/tests/e2e/helpers.js b/apps/ui/tests/e2e/helpers.js
index 41f8450..461d331 100644
--- a/apps/ui/tests/e2e/helpers.js
+++ b/apps/ui/tests/e2e/helpers.js
@@ -1,16 +1,7 @@
const { expect } = require('@playwright/test');
-const fs = require('fs');
-const path = require('path');
-const { execFileSync } = require('child_process');
const ADMIN_EMAIL = process.env.CRANK_E2E_ADMIN_EMAIL || 'owner@crank.local';
const ADMIN_PASSWORD = process.env.CRANK_E2E_ADMIN_PASSWORD || 'change-me-admin-password';
-const MCP_PORT = Number(process.env.CRANK_E2E_MCP_PORT || 3302);
-const STREAM_FIXTURE_PORT = Number(process.env.CRANK_E2E_STREAM_FIXTURE_PORT || 3310);
-const GRPC_FIXTURE_BIND = process.env.CRANK_E2E_GRPC_FIXTURE_BIND || '127.0.0.1:3311';
-const REPO_ROOT = path.resolve(__dirname, '../../../..');
-
-let cachedEchoDescriptorSetB64 = null;
function localized(en, ru) {
return new RegExp(`(?:${en}|${ru})`, 'i');
@@ -74,577 +65,27 @@ async function getCurrentWorkspace(page) {
return membership ? membership.workspace : null;
}
-async function createOperation(page, workspaceId, payload) {
- return browserJson(page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/operations`, payload);
-}
-
-async function publishOperation(page, workspaceId, operationId, version = 1) {
- return browserJson(
- page,
- 'POST',
- `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/operations/${encodeURIComponent(operationId)}/publish`,
- { version },
- );
-}
-
-async function createPlatformApiKey(page, workspaceId, agentId, name, scopes) {
- return browserJson(
- page,
- 'POST',
- `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/platform-api-keys`,
- { name, scopes },
- );
-}
-
async function createAgent(page, workspaceId, payload) {
- return browserJson(page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents`, payload);
-}
-
-async function saveAgentBindings(page, workspaceId, agentId, bindings) {
return browserJson(
page,
'POST',
- `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/bindings`,
- bindings,
+ `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents`,
+ payload,
);
}
-async function publishAgent(page, workspaceId, agentId, version = 1) {
- return browserJson(
- page,
- 'POST',
- `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/publish`,
- { version },
- );
-}
-
-function schemaObject(fields) {
- return {
- type: 'object',
- required: true,
- fields: fields || {},
- };
-}
-
-function schemaString(required = true) {
- return {
- type: 'string',
- required,
- };
-}
-
function uniqueName(prefix) {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}
-function findEchoDescriptorSetB64() {
- if (cachedEchoDescriptorSetB64) {
- return cachedEchoDescriptorSetB64;
- }
-
- const escapedRoot = REPO_ROOT.replace(/'/g, "'\\''");
- const descriptorPath = execFileSync(
- 'bash',
- ['-lc', `find '${escapedRoot}/target/debug/build' -path '*/out/echo_descriptor.bin' -print -quit`],
- { encoding: 'utf8' },
- ).trim();
-
- if (!descriptorPath) {
- throw new Error('echo_descriptor.bin was not found under target/debug/build');
- }
-
- cachedEchoDescriptorSetB64 = fs.readFileSync(descriptorPath).toString('base64');
- return cachedEchoDescriptorSetB64;
-}
-
-function buildRestWindowOperationPayload(name) {
- return {
- name,
- display_name: 'Playwright Window Logs',
- category: 'streaming',
- protocol: 'rest',
- target: {
- kind: 'rest',
- base_url: `http://127.0.0.1:${STREAM_FIXTURE_PORT}`,
- method: 'GET',
- path_template: '/sse/logs',
- static_headers: {},
- },
- input_schema: schemaObject({
- window: schemaString(false),
- }),
- output_schema: schemaObject({}),
- input_mapping: {
- rules: [
- {
- source: '$.mcp.window',
- target: '$.request.query.window',
- required: false,
- default_value: 'recent',
- },
- ],
- },
- output_mapping: { rules: [] },
- execution_config: {
- timeout_ms: 1000,
- headers: {},
- streaming: {
- mode: 'window',
- transport_behavior: 'server_stream',
- window_duration_ms: 1000,
- upstream_timeout_ms: 1000,
- max_items: 3,
- max_bytes: 16384,
- aggregation_mode: 'summary_plus_samples',
- items_path: '$.items',
- done_path: '$.done',
- redacted_paths: [],
- truncate_item_fields: false,
- drop_duplicates: false,
- tool_family: {},
- },
- },
- tool_description: {
- title: 'Playwright Window Logs',
- description: 'Collects a bounded SSE log window from the local fixture.',
- tags: ['playwright', 'streaming', 'window'],
- examples: [{ input: {} }],
- },
- };
-}
-
-function buildWebsocketWindowOperationPayload(name, overrides = {}) {
- const streaming = Object.assign({
- mode: 'window',
- transport_behavior: 'server_stream',
- window_duration_ms: 1000,
- upstream_timeout_ms: 1000,
- max_items: 3,
- max_bytes: 16384,
- aggregation_mode: 'summary_plus_samples',
- items_path: '$.items',
- done_path: '$.done',
- redacted_paths: [],
- truncate_item_fields: false,
- drop_duplicates: false,
- tool_family: {},
- }, overrides.streaming || {});
-
- const websocketOptions = Object.assign({
- heartbeat_interval_ms: 250,
- reconnect_max_attempts: 1,
- reconnect_backoff_ms: 10,
- }, overrides.websocketOptions || {});
-
- return {
- name,
- display_name: 'Playwright WebSocket Window',
- category: 'streaming',
- protocol: 'websocket',
- target: {
- kind: 'websocket',
- url: `ws://127.0.0.1:${STREAM_FIXTURE_PORT}/events`,
- subprotocols: [],
- subscribe_message_template: null,
- unsubscribe_message_template: null,
- static_headers: {},
- },
- input_schema: schemaObject({
- topic: schemaString(false),
- }),
- output_schema: schemaObject({}),
- input_mapping: {
- rules: [
- {
- source: '$.mcp.topic',
- target: '$.request.body.topic',
- required: false,
- default_value: 'telemetry',
- },
- ],
- },
- output_mapping: { rules: [] },
- execution_config: {
- timeout_ms: 1000,
- headers: {},
- protocol_options: {
- websocket: websocketOptions,
- },
- streaming,
- },
- tool_description: {
- title: 'Playwright WebSocket Window',
- description: 'Collects a bounded WebSocket event window from the local fixture.',
- tags: ['playwright', 'streaming', 'websocket', 'window'],
- examples: [{ input: {} }],
- },
- };
-}
-
-const SOAP_TEST_WSDL = `
-