ui: trim community premium frontend ballast

This commit is contained in:
github-ops
2026-05-14 16:19:58 +00:00
parent f50c8fb327
commit 23962f98e3
14 changed files with 90 additions and 2083 deletions
+11 -6
View File
@@ -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 без скрытых переходных допущений.
+1 -1
View File
@@ -121,7 +121,7 @@ tls:
</svg>
<div class="info-callout-body">
<div class="info-callout-title" data-i18n="wizard.step5.live_save_title">Live actions save the draft first</div>
<div class="info-callout-text" data-i18n="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.</div>
<div class="info-callout-text" data-i18n="wizard.step5.live_save_body">Sample uploads, test runs, YAML actions and publish all persist the current draft before calling the backend.</div>
</div>
</div>
+2 -434
View File
@@ -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) {
-47
View File
@@ -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,
};
}());
-330
View File
@@ -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,
};
}());
-302
View File
@@ -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'));
}
-394
View File
@@ -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()) || '—';
}
+59
View File
@@ -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'),
+5 -2
View File
@@ -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';
}
+3 -1
View File
@@ -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);
});
+1 -1
View File
@@ -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', 'коммерческую редакцию')
+3 -562
View File
@@ -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 = `<?xml version="1.0"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="urn:crm"
targetNamespace="urn:crm">
<binding name="LeadBinding" type="tns:LeadPortType">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<operation name="CreateLead">
<soap:operation soapAction="urn:createLead"/>
</operation>
</binding>
<service name="LeadService">
<port name="LeadPort" binding="tns:LeadBinding">
<soap:address location="http://127.0.0.1:${STREAM_FIXTURE_PORT}/soap/leads"/>
</port>
</service>
</definitions>`;
function buildSoapOperationPayload(name) {
return {
name,
display_name: 'Playwright SOAP Lead',
category: 'sales',
protocol: 'soap',
target: {
kind: 'soap',
wsdl_ref: 'sample_wsdl_pending',
service_name: 'Service',
port_name: 'Port',
operation_name: 'Operation',
endpoint_override: `http://127.0.0.1:${STREAM_FIXTURE_PORT}/soap/leads`,
soap_version: 'soap_11',
soap_action: 'urn:createLead',
binding_style: 'document_literal',
headers: [],
metadata: {
input_part_names: ['CreateLeadRequest'],
output_part_names: ['CreateLeadResponse'],
namespaces: ['urn:crm'],
},
},
input_schema: schemaObject({
email: schemaString(true),
}),
output_schema: schemaObject({
id: schemaString(true),
}),
input_mapping: {
rules: [
{
source: '$.mcp.email',
target: '$.request.body.email',
required: true,
},
],
},
output_mapping: {
rules: [
{
source: '$.response.body.id',
target: '$.output.id',
required: true,
},
],
},
execution_config: {
timeout_ms: 1000,
headers: {},
},
tool_description: {
title: 'Playwright SOAP Lead',
description: 'Creates a lead through the local SOAP fixture.',
tags: ['playwright', 'soap'],
examples: [{ input: { email: 'user@example.com' } }],
},
};
}
function buildGrpcSessionOperationPayload(name) {
return {
name,
display_name: 'Playwright gRPC Session',
category: 'streaming',
protocol: 'grpc',
target: {
kind: 'grpc',
server_addr: `http://${GRPC_FIXTURE_BIND}`,
package: 'echo',
service: 'EchoService',
method: 'ServerEcho',
descriptor_ref: 'desc_echo_playwright',
descriptor_set_b64: findEchoDescriptorSetB64(),
},
input_schema: schemaObject({
message: schemaString(true),
}),
output_schema: schemaObject({}),
input_mapping: {
rules: [
{
source: '$.mcp.message',
target: '$.request.grpc.message',
required: true,
},
],
},
output_mapping: { rules: [] },
execution_config: {
timeout_ms: 1000,
headers: {},
streaming: {
mode: 'session',
transport_behavior: 'server_stream',
window_duration_ms: 1000,
poll_interval_ms: 250,
upstream_timeout_ms: 1000,
idle_timeout_ms: 5000,
max_session_lifetime_ms: 60000,
max_items: 1,
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: {
start_tool_name: `${name}_start`,
poll_tool_name: `${name}_poll`,
stop_tool_name: `${name}_stop`,
},
},
},
tool_description: {
title: 'Playwright gRPC Session',
description: 'Streams gRPC echo messages through a bounded MCP session tool family.',
tags: ['playwright', 'streaming', 'session', 'grpc'],
examples: [{ input: { message: 'hello' } }],
},
};
}
function buildRestAsyncJobOperationPayload(name) {
return {
name,
display_name: 'Playwright Async Lead',
category: 'streaming',
protocol: 'rest',
target: {
kind: 'rest',
base_url: `http://127.0.0.1:${STREAM_FIXTURE_PORT}`,
method: 'POST',
path_template: '/crm/leads',
static_headers: {},
},
input_schema: schemaObject({
email: schemaString(true),
}),
output_schema: schemaObject({
id: schemaString(true),
}),
input_mapping: {
rules: [
{
source: '$.mcp.email',
target: '$.request.body.email',
required: true,
},
],
},
output_mapping: {
rules: [
{
source: '$.response.body.id',
target: '$.output.id',
required: true,
},
],
},
execution_config: {
timeout_ms: 2000,
headers: {},
streaming: {
mode: 'async_job',
transport_behavior: 'deferred_result',
poll_interval_ms: 250,
upstream_timeout_ms: 2000,
max_session_lifetime_ms: 300000,
max_bytes: 16384,
aggregation_mode: 'summary_only',
redacted_paths: [],
truncate_item_fields: false,
drop_duplicates: false,
tool_family: {
start_tool_name: `${name}_start`,
status_tool_name: `${name}_status`,
result_tool_name: `${name}_result`,
cancel_tool_name: `${name}_cancel`,
},
},
},
tool_description: {
title: 'Playwright Async Lead',
description: 'Creates a lead through the async job MCP tool family.',
tags: ['playwright', 'streaming', 'async_job'],
examples: [{ input: { email: 'user@example.com' } }],
},
};
}
async function setupPublishedAgent(page, { operationPayload, agentSlug, toolName, toolTitle, toolDescription }) {
const workspace = await getCurrentWorkspace(page);
if (!workspace) {
throw new Error('current workspace was not resolved');
}
const createdOperation = await createOperation(page, workspace.id, operationPayload);
await publishOperation(page, workspace.id, createdOperation.operation_id, createdOperation.version);
const createdAgent = await createAgent(page, workspace.id, {
slug: agentSlug,
display_name: toolTitle,
description: toolDescription,
instructions: {},
tool_selection_policy: {},
});
await saveAgentBindings(page, workspace.id, createdAgent.agent_id, [
{
operation_id: createdOperation.operation_id,
operation_version: createdOperation.version,
tool_name: toolName,
tool_title: toolTitle,
tool_description_override: toolDescription,
enabled: true,
},
]);
await publishAgent(page, workspace.id, createdAgent.agent_id, createdAgent.version);
const key = await createPlatformApiKey(
page,
workspace.id,
createdAgent.agent_id,
uniqueName('playwright_key'),
['read', 'write']
);
return {
workspace,
operationId: createdOperation.operation_id,
agentId: createdAgent.agent_id,
apiKeySecret: key.secret,
};
}
function mcpUrl(workspaceSlug, agentSlug) {
return `http://127.0.0.1:${MCP_PORT}/v1/${workspaceSlug}/${agentSlug}`;
}
async function initializeMcpSession({ workspaceSlug, agentSlug, apiKey }) {
const response = await fetch(mcpUrl(workspaceSlug, agentSlug), {
method: 'POST',
headers: {
Accept: 'application/json, text/event-stream',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2025-11-25',
},
}),
});
if (!response.ok) {
throw new Error(`initialize failed: ${response.status} ${await response.text()}`);
}
const sessionId = response.headers.get('MCP-Session-Id');
if (!sessionId) {
throw new Error('initialize response did not include MCP-Session-Id');
}
const initialized = await fetch(mcpUrl(workspaceSlug, agentSlug), {
method: 'POST',
headers: {
Accept: 'application/json, text/event-stream',
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'MCP-Session-Id': sessionId,
'MCP-Protocol-Version': '2025-11-25',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'notifications/initialized',
params: {},
}),
});
if (initialized.status !== 202) {
throw new Error(`initialized notification failed: ${initialized.status} ${await initialized.text()}`);
}
return {
sessionId,
workspaceSlug,
agentSlug,
apiKey,
};
}
async function mcpToolCall(session, toolName, argumentsValue) {
const response = await fetch(mcpUrl(session.workspaceSlug, session.agentSlug), {
method: 'POST',
headers: {
Accept: 'application/json, text/event-stream',
Authorization: `Bearer ${session.apiKey}`,
'Content-Type': 'application/json',
'MCP-Session-Id': session.sessionId,
'MCP-Protocol-Version': '2025-11-25',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: {
name: toolName,
arguments: argumentsValue || {},
},
}),
});
const payload = await response.json();
if (!response.ok || payload.error) {
throw new Error(`tools/call failed for ${toolName}: ${JSON.stringify(payload)}`);
}
return payload.result.structuredContent;
}
module.exports = {
ADMIN_EMAIL,
ADMIN_PASSWORD,
SOAP_TEST_WSDL,
browserJson,
buildGrpcSessionOperationPayload,
buildRestAsyncJobOperationPayload,
buildRestWindowOperationPayload,
buildSoapOperationPayload,
buildWebsocketWindowOperationPayload,
createAgent,
createOperation,
createPlatformApiKey,
getCurrentWorkspace,
getSession,
initializeMcpSession,
login,
localized,
mcpToolCall,
publishAgent,
publishOperation,
saveAgentBindings,
setupPublishedAgent,
login,
uniqueName,
};
+1 -1
View File
@@ -12,5 +12,5 @@ test('logs and usage pages show seeded data', async ({ page }) => {
await page.goto('/usage');
await expect(page.locator('.page-title')).toHaveText(localized('Usage', 'Использование'));
await expect(page.locator('#chart-bars .chart-col')).toHaveCount(7);
await expect(page.locator('#usage-tbody tr')).toHaveCount(3);
await expect(page.locator('#usage-tbody tr')).toHaveCount(1);
});
+4 -2
View File
@@ -19,7 +19,9 @@ test('community wizard hides premium protocols and explains edition scope', asyn
await expect(page.locator('[data-testid="wizard-protocol-grpc"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-websocket"]')).toBeHidden();
await expect(page.locator('[data-testid="wizard-protocol-soap"]')).toBeHidden();
await expect(page.locator('#wizard-edition-protocol-note')).toContainText(localized('Commercial editions unlock', 'коммерческих редакциях'));
await expect(page.locator('#wizard-edition-protocol-note')).toContainText(
localized('Commercial editions unlock', 'коммерческих редакциях')
);
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('#btn-continue').click();
@@ -28,5 +30,5 @@ test('community wizard hides premium protocols and explains edition scope', asyn
await page.locator('#btn-continue').click();
await expect(page.locator('#wizard-streaming-config-card')).toBeHidden();
await expect(page.locator('#wizard-streaming-capability-note')).toContainText(localized('commercial edition', 'коммерческую редакцию'));
await expect(page.locator('#wizard-streaming-capability-note')).toHaveCount(0);
});