ui: trim community premium frontend ballast
This commit is contained in:
@@ -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 без скрытых переходных допущений.
|
||||
|
||||
@@ -121,7 +121,7 @@ tls:
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step5.live_save_title">Live actions save the draft first</div>
|
||||
<div class="info-callout-text" data-i18n="wizard.step5.live_save_body">Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.</div>
|
||||
<div class="info-callout-text" data-i18n="wizard.step5.live_save_body">Sample uploads, test runs, YAML actions and publish all persist the current draft before calling the backend.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+2
-434
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}());
|
||||
@@ -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,
|
||||
};
|
||||
}());
|
||||
@@ -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'));
|
||||
}
|
||||
@@ -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()) || '—';
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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', 'коммерческую редакцию')
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
const { expect } = require('@playwright/test');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const ADMIN_EMAIL = process.env.CRANK_E2E_ADMIN_EMAIL || 'owner@crank.local';
|
||||
const ADMIN_PASSWORD = process.env.CRANK_E2E_ADMIN_PASSWORD || 'change-me-admin-password';
|
||||
const MCP_PORT = Number(process.env.CRANK_E2E_MCP_PORT || 3302);
|
||||
const STREAM_FIXTURE_PORT = Number(process.env.CRANK_E2E_STREAM_FIXTURE_PORT || 3310);
|
||||
const GRPC_FIXTURE_BIND = process.env.CRANK_E2E_GRPC_FIXTURE_BIND || '127.0.0.1:3311';
|
||||
const REPO_ROOT = path.resolve(__dirname, '../../../..');
|
||||
|
||||
let cachedEchoDescriptorSetB64 = null;
|
||||
|
||||
function localized(en, ru) {
|
||||
return new RegExp(`(?:${en}|${ru})`, 'i');
|
||||
@@ -74,577 +65,27 @@ async function getCurrentWorkspace(page) {
|
||||
return membership ? membership.workspace : null;
|
||||
}
|
||||
|
||||
async function createOperation(page, workspaceId, payload) {
|
||||
return browserJson(page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/operations`, payload);
|
||||
}
|
||||
|
||||
async function publishOperation(page, workspaceId, operationId, version = 1) {
|
||||
return browserJson(
|
||||
page,
|
||||
'POST',
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/operations/${encodeURIComponent(operationId)}/publish`,
|
||||
{ version },
|
||||
);
|
||||
}
|
||||
|
||||
async function createPlatformApiKey(page, workspaceId, agentId, name, scopes) {
|
||||
return browserJson(
|
||||
page,
|
||||
'POST',
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/platform-api-keys`,
|
||||
{ name, scopes },
|
||||
);
|
||||
}
|
||||
|
||||
async function createAgent(page, workspaceId, payload) {
|
||||
return browserJson(page, 'POST', `/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents`, payload);
|
||||
}
|
||||
|
||||
async function saveAgentBindings(page, workspaceId, agentId, bindings) {
|
||||
return browserJson(
|
||||
page,
|
||||
'POST',
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/bindings`,
|
||||
bindings,
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
async function publishAgent(page, workspaceId, agentId, version = 1) {
|
||||
return browserJson(
|
||||
page,
|
||||
'POST',
|
||||
`/api/admin/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}/publish`,
|
||||
{ version },
|
||||
);
|
||||
}
|
||||
|
||||
function schemaObject(fields) {
|
||||
return {
|
||||
type: 'object',
|
||||
required: true,
|
||||
fields: fields || {},
|
||||
};
|
||||
}
|
||||
|
||||
function schemaString(required = true) {
|
||||
return {
|
||||
type: 'string',
|
||||
required,
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueName(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function findEchoDescriptorSetB64() {
|
||||
if (cachedEchoDescriptorSetB64) {
|
||||
return cachedEchoDescriptorSetB64;
|
||||
}
|
||||
|
||||
const escapedRoot = REPO_ROOT.replace(/'/g, "'\\''");
|
||||
const descriptorPath = execFileSync(
|
||||
'bash',
|
||||
['-lc', `find '${escapedRoot}/target/debug/build' -path '*/out/echo_descriptor.bin' -print -quit`],
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
|
||||
if (!descriptorPath) {
|
||||
throw new Error('echo_descriptor.bin was not found under target/debug/build');
|
||||
}
|
||||
|
||||
cachedEchoDescriptorSetB64 = fs.readFileSync(descriptorPath).toString('base64');
|
||||
return cachedEchoDescriptorSetB64;
|
||||
}
|
||||
|
||||
function buildRestWindowOperationPayload(name) {
|
||||
return {
|
||||
name,
|
||||
display_name: 'Playwright Window Logs',
|
||||
category: 'streaming',
|
||||
protocol: 'rest',
|
||||
target: {
|
||||
kind: 'rest',
|
||||
base_url: `http://127.0.0.1:${STREAM_FIXTURE_PORT}`,
|
||||
method: 'GET',
|
||||
path_template: '/sse/logs',
|
||||
static_headers: {},
|
||||
},
|
||||
input_schema: schemaObject({
|
||||
window: schemaString(false),
|
||||
}),
|
||||
output_schema: schemaObject({}),
|
||||
input_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.mcp.window',
|
||||
target: '$.request.query.window',
|
||||
required: false,
|
||||
default_value: 'recent',
|
||||
},
|
||||
],
|
||||
},
|
||||
output_mapping: { rules: [] },
|
||||
execution_config: {
|
||||
timeout_ms: 1000,
|
||||
headers: {},
|
||||
streaming: {
|
||||
mode: 'window',
|
||||
transport_behavior: 'server_stream',
|
||||
window_duration_ms: 1000,
|
||||
upstream_timeout_ms: 1000,
|
||||
max_items: 3,
|
||||
max_bytes: 16384,
|
||||
aggregation_mode: 'summary_plus_samples',
|
||||
items_path: '$.items',
|
||||
done_path: '$.done',
|
||||
redacted_paths: [],
|
||||
truncate_item_fields: false,
|
||||
drop_duplicates: false,
|
||||
tool_family: {},
|
||||
},
|
||||
},
|
||||
tool_description: {
|
||||
title: 'Playwright Window Logs',
|
||||
description: 'Collects a bounded SSE log window from the local fixture.',
|
||||
tags: ['playwright', 'streaming', 'window'],
|
||||
examples: [{ input: {} }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildWebsocketWindowOperationPayload(name, overrides = {}) {
|
||||
const streaming = Object.assign({
|
||||
mode: 'window',
|
||||
transport_behavior: 'server_stream',
|
||||
window_duration_ms: 1000,
|
||||
upstream_timeout_ms: 1000,
|
||||
max_items: 3,
|
||||
max_bytes: 16384,
|
||||
aggregation_mode: 'summary_plus_samples',
|
||||
items_path: '$.items',
|
||||
done_path: '$.done',
|
||||
redacted_paths: [],
|
||||
truncate_item_fields: false,
|
||||
drop_duplicates: false,
|
||||
tool_family: {},
|
||||
}, overrides.streaming || {});
|
||||
|
||||
const websocketOptions = Object.assign({
|
||||
heartbeat_interval_ms: 250,
|
||||
reconnect_max_attempts: 1,
|
||||
reconnect_backoff_ms: 10,
|
||||
}, overrides.websocketOptions || {});
|
||||
|
||||
return {
|
||||
name,
|
||||
display_name: 'Playwright WebSocket Window',
|
||||
category: 'streaming',
|
||||
protocol: 'websocket',
|
||||
target: {
|
||||
kind: 'websocket',
|
||||
url: `ws://127.0.0.1:${STREAM_FIXTURE_PORT}/events`,
|
||||
subprotocols: [],
|
||||
subscribe_message_template: null,
|
||||
unsubscribe_message_template: null,
|
||||
static_headers: {},
|
||||
},
|
||||
input_schema: schemaObject({
|
||||
topic: schemaString(false),
|
||||
}),
|
||||
output_schema: schemaObject({}),
|
||||
input_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.mcp.topic',
|
||||
target: '$.request.body.topic',
|
||||
required: false,
|
||||
default_value: 'telemetry',
|
||||
},
|
||||
],
|
||||
},
|
||||
output_mapping: { rules: [] },
|
||||
execution_config: {
|
||||
timeout_ms: 1000,
|
||||
headers: {},
|
||||
protocol_options: {
|
||||
websocket: websocketOptions,
|
||||
},
|
||||
streaming,
|
||||
},
|
||||
tool_description: {
|
||||
title: 'Playwright WebSocket Window',
|
||||
description: 'Collects a bounded WebSocket event window from the local fixture.',
|
||||
tags: ['playwright', 'streaming', 'websocket', 'window'],
|
||||
examples: [{ input: {} }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const SOAP_TEST_WSDL = `<?xml version="1.0"?>
|
||||
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||
xmlns:tns="urn:crm"
|
||||
targetNamespace="urn:crm">
|
||||
<binding name="LeadBinding" type="tns:LeadPortType">
|
||||
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
|
||||
<operation name="CreateLead">
|
||||
<soap:operation soapAction="urn:createLead"/>
|
||||
</operation>
|
||||
</binding>
|
||||
<service name="LeadService">
|
||||
<port name="LeadPort" binding="tns:LeadBinding">
|
||||
<soap:address location="http://127.0.0.1:${STREAM_FIXTURE_PORT}/soap/leads"/>
|
||||
</port>
|
||||
</service>
|
||||
</definitions>`;
|
||||
|
||||
function buildSoapOperationPayload(name) {
|
||||
return {
|
||||
name,
|
||||
display_name: 'Playwright SOAP Lead',
|
||||
category: 'sales',
|
||||
protocol: 'soap',
|
||||
target: {
|
||||
kind: 'soap',
|
||||
wsdl_ref: 'sample_wsdl_pending',
|
||||
service_name: 'Service',
|
||||
port_name: 'Port',
|
||||
operation_name: 'Operation',
|
||||
endpoint_override: `http://127.0.0.1:${STREAM_FIXTURE_PORT}/soap/leads`,
|
||||
soap_version: 'soap_11',
|
||||
soap_action: 'urn:createLead',
|
||||
binding_style: 'document_literal',
|
||||
headers: [],
|
||||
metadata: {
|
||||
input_part_names: ['CreateLeadRequest'],
|
||||
output_part_names: ['CreateLeadResponse'],
|
||||
namespaces: ['urn:crm'],
|
||||
},
|
||||
},
|
||||
input_schema: schemaObject({
|
||||
email: schemaString(true),
|
||||
}),
|
||||
output_schema: schemaObject({
|
||||
id: schemaString(true),
|
||||
}),
|
||||
input_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.mcp.email',
|
||||
target: '$.request.body.email',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
output_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.response.body.id',
|
||||
target: '$.output.id',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
execution_config: {
|
||||
timeout_ms: 1000,
|
||||
headers: {},
|
||||
},
|
||||
tool_description: {
|
||||
title: 'Playwright SOAP Lead',
|
||||
description: 'Creates a lead through the local SOAP fixture.',
|
||||
tags: ['playwright', 'soap'],
|
||||
examples: [{ input: { email: 'user@example.com' } }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildGrpcSessionOperationPayload(name) {
|
||||
return {
|
||||
name,
|
||||
display_name: 'Playwright gRPC Session',
|
||||
category: 'streaming',
|
||||
protocol: 'grpc',
|
||||
target: {
|
||||
kind: 'grpc',
|
||||
server_addr: `http://${GRPC_FIXTURE_BIND}`,
|
||||
package: 'echo',
|
||||
service: 'EchoService',
|
||||
method: 'ServerEcho',
|
||||
descriptor_ref: 'desc_echo_playwright',
|
||||
descriptor_set_b64: findEchoDescriptorSetB64(),
|
||||
},
|
||||
input_schema: schemaObject({
|
||||
message: schemaString(true),
|
||||
}),
|
||||
output_schema: schemaObject({}),
|
||||
input_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.mcp.message',
|
||||
target: '$.request.grpc.message',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
output_mapping: { rules: [] },
|
||||
execution_config: {
|
||||
timeout_ms: 1000,
|
||||
headers: {},
|
||||
streaming: {
|
||||
mode: 'session',
|
||||
transport_behavior: 'server_stream',
|
||||
window_duration_ms: 1000,
|
||||
poll_interval_ms: 250,
|
||||
upstream_timeout_ms: 1000,
|
||||
idle_timeout_ms: 5000,
|
||||
max_session_lifetime_ms: 60000,
|
||||
max_items: 1,
|
||||
max_bytes: 16384,
|
||||
aggregation_mode: 'summary_plus_samples',
|
||||
items_path: '$.items',
|
||||
done_path: '$.done',
|
||||
redacted_paths: [],
|
||||
truncate_item_fields: false,
|
||||
drop_duplicates: false,
|
||||
tool_family: {
|
||||
start_tool_name: `${name}_start`,
|
||||
poll_tool_name: `${name}_poll`,
|
||||
stop_tool_name: `${name}_stop`,
|
||||
},
|
||||
},
|
||||
},
|
||||
tool_description: {
|
||||
title: 'Playwright gRPC Session',
|
||||
description: 'Streams gRPC echo messages through a bounded MCP session tool family.',
|
||||
tags: ['playwright', 'streaming', 'session', 'grpc'],
|
||||
examples: [{ input: { message: 'hello' } }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildRestAsyncJobOperationPayload(name) {
|
||||
return {
|
||||
name,
|
||||
display_name: 'Playwright Async Lead',
|
||||
category: 'streaming',
|
||||
protocol: 'rest',
|
||||
target: {
|
||||
kind: 'rest',
|
||||
base_url: `http://127.0.0.1:${STREAM_FIXTURE_PORT}`,
|
||||
method: 'POST',
|
||||
path_template: '/crm/leads',
|
||||
static_headers: {},
|
||||
},
|
||||
input_schema: schemaObject({
|
||||
email: schemaString(true),
|
||||
}),
|
||||
output_schema: schemaObject({
|
||||
id: schemaString(true),
|
||||
}),
|
||||
input_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.mcp.email',
|
||||
target: '$.request.body.email',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
output_mapping: {
|
||||
rules: [
|
||||
{
|
||||
source: '$.response.body.id',
|
||||
target: '$.output.id',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
execution_config: {
|
||||
timeout_ms: 2000,
|
||||
headers: {},
|
||||
streaming: {
|
||||
mode: 'async_job',
|
||||
transport_behavior: 'deferred_result',
|
||||
poll_interval_ms: 250,
|
||||
upstream_timeout_ms: 2000,
|
||||
max_session_lifetime_ms: 300000,
|
||||
max_bytes: 16384,
|
||||
aggregation_mode: 'summary_only',
|
||||
redacted_paths: [],
|
||||
truncate_item_fields: false,
|
||||
drop_duplicates: false,
|
||||
tool_family: {
|
||||
start_tool_name: `${name}_start`,
|
||||
status_tool_name: `${name}_status`,
|
||||
result_tool_name: `${name}_result`,
|
||||
cancel_tool_name: `${name}_cancel`,
|
||||
},
|
||||
},
|
||||
},
|
||||
tool_description: {
|
||||
title: 'Playwright Async Lead',
|
||||
description: 'Creates a lead through the async job MCP tool family.',
|
||||
tags: ['playwright', 'streaming', 'async_job'],
|
||||
examples: [{ input: { email: 'user@example.com' } }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function setupPublishedAgent(page, { operationPayload, agentSlug, toolName, toolTitle, toolDescription }) {
|
||||
const workspace = await getCurrentWorkspace(page);
|
||||
if (!workspace) {
|
||||
throw new Error('current workspace was not resolved');
|
||||
}
|
||||
|
||||
const createdOperation = await createOperation(page, workspace.id, operationPayload);
|
||||
await publishOperation(page, workspace.id, createdOperation.operation_id, createdOperation.version);
|
||||
|
||||
const createdAgent = await createAgent(page, workspace.id, {
|
||||
slug: agentSlug,
|
||||
display_name: toolTitle,
|
||||
description: toolDescription,
|
||||
instructions: {},
|
||||
tool_selection_policy: {},
|
||||
});
|
||||
|
||||
await saveAgentBindings(page, workspace.id, createdAgent.agent_id, [
|
||||
{
|
||||
operation_id: createdOperation.operation_id,
|
||||
operation_version: createdOperation.version,
|
||||
tool_name: toolName,
|
||||
tool_title: toolTitle,
|
||||
tool_description_override: toolDescription,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
await publishAgent(page, workspace.id, createdAgent.agent_id, createdAgent.version);
|
||||
|
||||
const key = await createPlatformApiKey(
|
||||
page,
|
||||
workspace.id,
|
||||
createdAgent.agent_id,
|
||||
uniqueName('playwright_key'),
|
||||
['read', 'write']
|
||||
);
|
||||
|
||||
return {
|
||||
workspace,
|
||||
operationId: createdOperation.operation_id,
|
||||
agentId: createdAgent.agent_id,
|
||||
apiKeySecret: key.secret,
|
||||
};
|
||||
}
|
||||
|
||||
function mcpUrl(workspaceSlug, agentSlug) {
|
||||
return `http://127.0.0.1:${MCP_PORT}/v1/${workspaceSlug}/${agentSlug}`;
|
||||
}
|
||||
|
||||
async function initializeMcpSession({ workspaceSlug, agentSlug, apiKey }) {
|
||||
const response = await fetch(mcpUrl(workspaceSlug, agentSlug), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json, text/event-stream',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2025-11-25',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`initialize failed: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
|
||||
const sessionId = response.headers.get('MCP-Session-Id');
|
||||
if (!sessionId) {
|
||||
throw new Error('initialize response did not include MCP-Session-Id');
|
||||
}
|
||||
|
||||
const initialized = await fetch(mcpUrl(workspaceSlug, agentSlug), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json, text/event-stream',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'MCP-Session-Id': sessionId,
|
||||
'MCP-Protocol-Version': '2025-11-25',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'notifications/initialized',
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (initialized.status !== 202) {
|
||||
throw new Error(`initialized notification failed: ${initialized.status} ${await initialized.text()}`);
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
workspaceSlug,
|
||||
agentSlug,
|
||||
apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
async function mcpToolCall(session, toolName, argumentsValue) {
|
||||
const response = await fetch(mcpUrl(session.workspaceSlug, session.agentSlug), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json, text/event-stream',
|
||||
Authorization: `Bearer ${session.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'MCP-Session-Id': session.sessionId,
|
||||
'MCP-Protocol-Version': '2025-11-25',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: toolName,
|
||||
arguments: argumentsValue || {},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok || payload.error) {
|
||||
throw new Error(`tools/call failed for ${toolName}: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
return payload.result.structuredContent;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ADMIN_EMAIL,
|
||||
ADMIN_PASSWORD,
|
||||
SOAP_TEST_WSDL,
|
||||
browserJson,
|
||||
buildGrpcSessionOperationPayload,
|
||||
buildRestAsyncJobOperationPayload,
|
||||
buildRestWindowOperationPayload,
|
||||
buildSoapOperationPayload,
|
||||
buildWebsocketWindowOperationPayload,
|
||||
createAgent,
|
||||
createOperation,
|
||||
createPlatformApiKey,
|
||||
getCurrentWorkspace,
|
||||
getSession,
|
||||
initializeMcpSession,
|
||||
login,
|
||||
localized,
|
||||
mcpToolCall,
|
||||
publishAgent,
|
||||
publishOperation,
|
||||
saveAgentBindings,
|
||||
setupPublishedAgent,
|
||||
login,
|
||||
uniqueName,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user