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