@diegosouzacdv/jev-browser-mcp 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -55
- package/config/ui-testing.json +10 -2
- package/docs/jev-browser-mcp.md +126 -55
- package/mcp_servers/jev-browser-npm/src/accessibility.mjs +18 -9
- package/mcp_servers/jev-browser-npm/src/config.mjs +28 -7
- package/mcp_servers/jev-browser-npm/src/flow.mjs +736 -314
- package/mcp_servers/jev-browser-npm/src/server.mjs +82 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ versão em produção, use o número explícito no argumento do pacote:
|
|
|
17
17
|
"mcpServers": {
|
|
18
18
|
"jev-browser": {
|
|
19
19
|
"command": "npx",
|
|
20
|
-
"args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.6.
|
|
20
|
+
"args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.6.2"],
|
|
21
21
|
"env": {
|
|
22
22
|
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}",
|
|
23
23
|
"JEV_BROWSER_MODE": "computer"
|
|
@@ -32,7 +32,7 @@ um secret manager ou pelo ambiente do processo; não grave a chave no arquivo
|
|
|
32
32
|
de configuração. Para instalar no projeto Node do próprio harness:
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
npm install @diegosouzacdv/jev-browser-mcp@0.6.
|
|
35
|
+
npm install @diegosouzacdv/jev-browser-mcp@0.6.2
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Se o harness executa o MCP repetidamente nesta máquina, instale uma versão
|
|
@@ -40,7 +40,7 @@ fixa globalmente para evitar a resolução e o download feitos pelo `npx` em cad
|
|
|
40
40
|
inicialização:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
npm install --global @diegosouzacdv/jev-browser-mcp@0.6.
|
|
43
|
+
npm install --global @diegosouzacdv/jev-browser-mcp@0.6.2
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Depois configure o servidor MCP com `command: "jev-browser-mcp"` e `args: []`.
|
|
@@ -49,7 +49,7 @@ Para atualizar, rode `npm install --global
|
|
|
49
49
|
|
|
50
50
|
O padrão `computer` exige Chrome ou Edge instalado. Se optar por `harness`,
|
|
51
51
|
configure `JEV_BROWSER_MODE=harness` antes de instalar o navegador gerenciado
|
|
52
|
-
pelo Playwright com `npx --yes @diegosouzacdv/jev-browser-mcp@0.6.
|
|
52
|
+
pelo Playwright com `npx --yes @diegosouzacdv/jev-browser-mcp@0.6.2 --install-browser`.
|
|
53
53
|
|
|
54
54
|
O padrão é `computer`: o pacote abre o Chrome/Edge instalado e usa um perfil
|
|
55
55
|
persistente exclusivo em `browser.computer_user_data_dir`. No modo `harness`, a
|
|
@@ -107,11 +107,17 @@ a página inicial, captura o snapshot acessível e pede ao Jev que escolha um
|
|
|
107
107
|
plano. Em seguida, executa o plano inteiro na mesma sessão e confere o
|
|
108
108
|
resultado esperado na tela.
|
|
109
109
|
|
|
110
|
-
Cada plano pode usar:
|
|
111
|
-
|
|
112
|
-
- `
|
|
113
|
-
|
|
114
|
-
`
|
|
110
|
+
Cada plano pode usar:
|
|
111
|
+
|
|
112
|
+
- `navigate` para mudar de página no meio do fluxo. Com a continuidade ligada,
|
|
113
|
+
um `initial_url` de mesma origem diferente da URL ativa produz erro claro; use
|
|
114
|
+
`navigate` ou `continue_from_current_page: false` para escolher o destino;
|
|
115
|
+
- `click`, `type`, `clear`, `hover`, `select_option`, `press`, `assert_*` e upload com
|
|
116
|
+
papel/nome acessível ou um localizador: `label`, `placeholder`, `title`,
|
|
117
|
+
`text`, `test_id` ou `selector`;
|
|
118
|
+
- `name_match: "exact" | "contains" | "regex"` para controlar a comparação do
|
|
119
|
+
nome acessível; regex rejeita construções com backtracking excessivo. O papel
|
|
120
|
+
`switch` funciona em cliques, verificações e asserções;
|
|
115
121
|
- `near: {"text":"..."}` para localizar o controle logo depois de um texto,
|
|
116
122
|
`within: {"row_containing":"..."}` para limitar por trecho e
|
|
117
123
|
`within: {"row_containing_exact":"..."}` para exigir um elemento com o
|
|
@@ -128,25 +134,34 @@ Cada plano pode usar:
|
|
|
128
134
|
- `selector` com CSS ou XPath como último recurso. O MCP identifica essa escolha
|
|
129
135
|
em `warnings`; não é permitido enviar JavaScript nem coordenadas;
|
|
130
136
|
- `timeout_seconds` opcional em cada etapa, limitado pela configuração central;
|
|
131
|
-
- `type` com `mode: "keys"` para digitar sequencialmente em campos com máscara,
|
|
132
|
-
`blur: true` para desfocar o campo e `sensitive: false` para permitir que o
|
|
133
|
-
valor apareça nas evidências.
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
- `type` com `mode: "keys"` para digitar sequencialmente em campos com máscara,
|
|
138
|
+
`blur: true` para desfocar o campo e `sensitive: false` para permitir que o
|
|
139
|
+
valor apareça nas evidências. `text_env` lê o valor sensível de uma variável
|
|
140
|
+
de ambiente do processo MCP, sem colocá-lo no plano. Nesse modo, `\n` envia
|
|
141
|
+
Enter e `\t` envia Tab;
|
|
142
|
+
- `wait_for_text`, `wait_for_value`, `wait_for_enabled` e `wait_for_condition`
|
|
143
|
+
(`url`, `network_idle`, `hidden`, `text_hidden` ou `angular_idle`). `wait` recebe
|
|
136
144
|
`ms` para uma pausa curta e limitada. `angular_idle` aguarda requisições
|
|
137
145
|
`$http` e digest do AngularJS. `wait_for_text` aceita `fail_on: {"role":"alert"}` para
|
|
138
146
|
interromper a espera assim que um alerta visível aparecer e incluir seu texto
|
|
139
147
|
no erro. Se o alerta contiver o texto esperado, a etapa passa; caso contrário,
|
|
140
|
-
o alerta interrompe a espera. `
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
o alerta interrompe a espera. `url` aguarda a URL conter `url_contains`.
|
|
149
|
+
`network_idle` aceita esse filtro e aguarda uma janela de silêncio usando
|
|
150
|
+
`ready_stable_ms`; WebSocket, EventSource e polling não relacionado não
|
|
151
|
+
bloqueiam a espera. Se não surgir requisição correspondente, o passo conclui
|
|
152
|
+
após a janela de silêncio;
|
|
153
|
+
- `press` (o alias legado `press_key` também é aceito) com `PageDown`, `PageUp`,
|
|
154
|
+
`Home`, `End`, setas, `Enter`, `Escape`, `Tab`, `Shift+Tab`, `Space`,
|
|
155
|
+
`Backspace` ou `Delete`. `clear` limpa um campo sem exigir texto. Sem alvo,
|
|
156
|
+
`press` envia a tecla ao elemento focado; com alvo, usa o localizador informado;
|
|
145
157
|
- `assert_text`, `assert_value`, `assert_visible`, `assert_hidden` e
|
|
146
158
|
`assert_enabled`; `assert_text`
|
|
147
159
|
pode receber apenas `role` quando a região, como `alert`, não tem nome acessível;
|
|
148
|
-
- `upload_file` para input rotulado, botão que abre o seletor de arquivo ou
|
|
149
|
-
dropzone;
|
|
160
|
+
- `upload_file` para input rotulado, botão que abre o seletor de arquivo ou
|
|
161
|
+
dropzone;
|
|
162
|
+
- `target_page: "popup"` para continuar no popup aberto com `expect_popup: true`;
|
|
163
|
+
locators semânticos também atravessam open Shadow DOM. Shadow DOM fechado não
|
|
164
|
+
é acessível ao Playwright;
|
|
150
165
|
- `audit_accessibility` com axe-core para WCAG 2.1 A/AA;
|
|
151
166
|
- `like_comment` e `unlike_comment`, que localizam uma linha pelo autor e texto,
|
|
152
167
|
não repetem uma reação já no estado pedido e distinguem `Curtir` de
|
|
@@ -211,11 +226,12 @@ com `within: {"row_containing_exact":"{pedido}"}`, e use uma etapa
|
|
|
211
226
|
`{"action":"extract","role":"cell","name":"{pedido}","within":{"row_containing_exact":"{pedido}"},"as":"documento"}`.
|
|
212
227
|
A etapa seguinte pode localizar o mesmo documento com `name: "{documento}"`.
|
|
213
228
|
|
|
214
|
-
Em `wait_for_condition` com `condition: "hidden"`, use qualquer um desses
|
|
215
|
-
localizadores, `near` ou `within`; `text_hidden` recebe o texto direto. Para
|
|
216
|
-
aguardar o fim de uma chamada específica, use `condition: "network_idle"` com
|
|
217
|
-
`url_contains
|
|
218
|
-
|
|
229
|
+
Em `wait_for_condition` com `condition: "hidden"`, use qualquer um desses
|
|
230
|
+
localizadores, `near` ou `within`; `text_hidden` recebe o texto direto. Para
|
|
231
|
+
aguardar o fim de uma chamada específica, use `condition: "network_idle"` com
|
|
232
|
+
`url_contains`. O filtro considera somente a atividade correspondente e termina
|
|
233
|
+
após a janela configurada sem atividade; também termina caso nenhuma requisição
|
|
234
|
+
correspondente apareça. Para validar status e corpo, prefira `assert_network`.
|
|
219
235
|
|
|
220
236
|
Quando vários controles têm o mesmo papel e nome, `within` limita a busca a um
|
|
221
237
|
container acessível único, como uma linha, card ou diálogo. `index` escolhe uma
|
|
@@ -411,15 +427,17 @@ validadores.
|
|
|
411
427
|
|
|
412
428
|
`browser.mode` aceita `harness` ou `computer`; o padrão é `computer`:
|
|
413
429
|
|
|
414
|
-
- `harness` usa Chrome headless e contexto isolado, adequado a execuções do
|
|
415
|
-
harness e CI
|
|
430
|
+
- `harness` usa Chrome headless e contexto isolado, adequado a execuções do
|
|
431
|
+
harness e CI. O processo MCP conserva browser, contexto e página entre
|
|
432
|
+
chamadas; cookies, local storage e estado da página permanecem enquanto o
|
|
433
|
+
processo estiver ativo. Encerre o processo para descartar o contexto isolado.
|
|
416
434
|
- `computer` abre o Chrome ou Edge instalado em modo visível e usa o diretório
|
|
417
435
|
persistente `browser.computer_user_data_dir`, separado por navegador. Não
|
|
418
436
|
reutiliza o perfil pessoal já aberto. Faça login uma vez nesse perfil; os
|
|
419
437
|
cookies permanecem nele entre chamadas.
|
|
420
438
|
|
|
421
|
-
`browser.max_flow_steps` limita
|
|
422
|
-
`browser.max_text_entry_chars` limita cada valor digitado. O resultado contém
|
|
439
|
+
`browser.max_flow_steps` limita os passos de cada plano candidato; os planos
|
|
440
|
+
não são somados. `browser.max_text_entry_chars` limita cada valor digitado. O resultado contém
|
|
423
441
|
`status`, o plano escolhido, as ações executadas, a última captura acessível e
|
|
424
442
|
se o critério esperado apareceu. Quando existem asserções explícitas, `status`
|
|
425
443
|
também pode ser `passed` com todas elas satisfeitas; `assertions_passed` registra
|
|
@@ -457,9 +475,12 @@ O `run_browser_flow` aceita `options` com `fast_path`, `snapshot_scope`,
|
|
|
457
475
|
`auto_angular_idle`, `login_url_contains`, `login_text`, `capture_console_errors`,
|
|
458
476
|
`capture_network_errors`, `capture_network_error_bodies`, `ready_timeout_seconds`,
|
|
459
477
|
`ready_network_idle`, `ready_stable_ms`, `ready_text`, `continue_from_current_page`,
|
|
460
|
-
`reuse_page`, `step_timeout_seconds`, `max_flow_steps`, `return_snapshot`,
|
|
461
|
-
`screenshot_on_failure`, `
|
|
462
|
-
`
|
|
478
|
+
`reuse_page`, `step_timeout_seconds`, `max_flow_steps`, `return_snapshot`,
|
|
479
|
+
`screenshot_on_failure`, `screenshot_on_success`, `trace_on_failure`,
|
|
480
|
+
`trace_on_success`, `record_video`, `snapshot_include_hidden`, `stop_on_expected`,
|
|
481
|
+
`dry_run`, `confirmation_token`, `report_path`, `permissions`, `fake_media`,
|
|
482
|
+
`viewport`, `mobile`, `device_scale_factor`, `locale`, `timezone_id`,
|
|
483
|
+
`color_scheme`, `geolocation` e `allow_mutations`. Sem override,
|
|
463
484
|
os padrões são lidos de `jev_browser_mcp` em
|
|
464
485
|
`config/ui-testing.json`: um plano candidato pula a chamada Decisions;
|
|
465
486
|
captura de console e screenshot de falha ficam ligadas; trace, rede e bloqueio
|
|
@@ -471,26 +492,33 @@ chamadas na mesma origem, não repete `page.goto` e consulta um snapshot atual
|
|
|
471
492
|
sem esperar novamente por `load`/`networkidle`. As etapas continuam aguardando
|
|
472
493
|
seus próprios alvos e condições. `reuse_page` segue como alias legado; defina
|
|
473
494
|
`continue_from_current_page: false` para iniciar cada chamada em `initial_url`.
|
|
474
|
-
Se a página atual e `initial_url` tiverem origens diferentes, o MCP navega para
|
|
475
|
-
`initial_url`. Se forem da mesma origem
|
|
476
|
-
|
|
477
|
-
`
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
495
|
+
Se a página atual e `initial_url` tiverem origens diferentes, o MCP navega para
|
|
496
|
+
`initial_url`. Se forem da mesma origem mas URLs completas diferentes, o MCP
|
|
497
|
+
recusa continuar silenciosamente e pede uma etapa `navigate` ou
|
|
498
|
+
`continue_from_current_page: false`. `snapshot_scope` aceita `body`, `main` ou
|
|
499
|
+
`dialog`; se `main` não existir, o snapshot usa `body`.
|
|
500
|
+
|
|
501
|
+
`block_trackers: true` bloqueia analytics/trackers e os tipos de recurso
|
|
502
|
+
configurados, mas preserva `media` quando há permissão de microfone ou
|
|
503
|
+
`fake_media`. Fontes ficam habilitadas por padrão para preservar ícones e
|
|
504
|
+
glyphs; `block_fonts: true` bloqueia fontes separadamente. `busy_selectors`
|
|
505
|
+
complementa `aria-busy="true"` ao aguardar overlays; elementos ocultos ou fora
|
|
506
|
+
da tela com esse atributo não seguram a prontidão da página.
|
|
483
507
|
`auto_angular_idle` aguarda AngularJS depois de cliques e digitação quando a
|
|
484
508
|
página expõe o injector. `login_url_contains` e `login_text` substituem a
|
|
485
509
|
detecção padrão de autenticação. `local_only: true` recusa qualquer etapa que
|
|
486
|
-
precisaria chamar o provedor Jev; use exatamente um plano com `fast_path: true`.
|
|
510
|
+
precisaria chamar o provedor Jev; use exatamente um plano com `fast_path: true`.
|
|
511
|
+
`allow_mutations: true` pula a confirmação humana somente para URLs em
|
|
512
|
+
`localhost`, `127.0.0.1` ou `::1`; use esse modo apenas em ambientes locais
|
|
513
|
+
controlados.
|
|
487
514
|
`step_timeout_seconds` define o timeout padrão por etapa e cada etapa pode
|
|
488
515
|
substituí-lo com `timeout_seconds`. `max_flow_steps` reduz o limite por chamada,
|
|
489
516
|
sem superar o teto da configuração. `return_snapshot` aceita `full`, `diff` ou
|
|
490
517
|
`none`; `diff` retorna linhas acessíveis novas desde o snapshot inicial.
|
|
491
518
|
|
|
492
|
-
Com `capture_console_errors` e `capture_network_errors`, o retorno traz
|
|
493
|
-
`console_errors` e `network_failures`, limitados em quantidade e tamanho.
|
|
519
|
+
Com `capture_console_errors` e `capture_network_errors`, o retorno traz
|
|
520
|
+
`console_errors` e `network_failures`, limitados em quantidade e tamanho.
|
|
521
|
+
Mensagens de erro idênticas do console são agrupadas em uma entrada com `count`.
|
|
494
522
|
Respostas HTTP 4xx/5xx e requisições que falham são listadas; query strings,
|
|
495
523
|
fragmentos, valores de formulário e nomes de arquivo são removidos ou
|
|
496
524
|
sanitizados. `capture_network_error_bodies: true` lê somente respostas JSON
|
|
@@ -506,9 +534,49 @@ compartilhe os arquivos somente se o teste permitir.
|
|
|
506
534
|
|
|
507
535
|
Screenshots por etapa usam `screenshot: true` no próprio passo; o MCP os grava
|
|
508
536
|
depois que a ação termina. `options.report_path` pode apontar para `.md` ou
|
|
509
|
-
JUnit `.xml` dentro da raiz de artefatos. O relatório inclui a versão, tempos,
|
|
510
|
-
localizadores resolvidos, respostas verificadas por `assert_network`, falhas de
|
|
511
|
-
rede e caminhos dos screenshots, para anexar a um PR ou card.
|
|
537
|
+
JUnit `.xml` dentro da raiz de artefatos. O relatório inclui a versão, tempos,
|
|
538
|
+
localizadores resolvidos, respostas verificadas por `assert_network`, falhas de
|
|
539
|
+
rede e caminhos dos screenshots, para anexar a um PR ou card.
|
|
540
|
+
|
|
541
|
+
`screenshot_on_success` grava a captura final em fluxos aprovados;
|
|
542
|
+
`trace_on_success` salva trace também em sucesso e `record_video` grava vídeo
|
|
543
|
+
WebM. Fluxos que digitam texto sensível suprimem screenshots, traces e vídeos
|
|
544
|
+
para não registrar credenciais. `performance_metrics` inclui métricas do
|
|
545
|
+
navegador quando disponíveis. `jev_browser_mcp.browser.max_tool_response_bytes`
|
|
546
|
+
impõe um teto global de bytes na resposta JSON da ferramenta; se necessário,
|
|
547
|
+
evidências volumosas são reduzidas e marcadas com `output_truncated: true`.
|
|
548
|
+
|
|
549
|
+
Para testar microfone, conceda permissão no escopo da chamada:
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
{
|
|
553
|
+
"options": {
|
|
554
|
+
"permissions": ["microphone"],
|
|
555
|
+
"fake_media": "fixtures/resposta-aluno.wav",
|
|
556
|
+
"block_trackers": true
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`fake_media` exige um WAV RIFF válido dentro de `JEV_BROWSER_UPLOAD_ROOT` e
|
|
562
|
+
respeita os mesmos limites de upload. O MCP adiciona as opções de dispositivo
|
|
563
|
+
falso do Chromium e concede permissão ao contexto. Sem esse arquivo, a
|
|
564
|
+
permissão de microfone usa o dispositivo autorizado pelo browser; no modo
|
|
565
|
+
`computer`, o sistema operacional ainda pode pedir acesso ao dispositivo.
|
|
566
|
+
|
|
567
|
+
As opções de emulação aceitam `viewport: {"width": 390, "height": 844}`,
|
|
568
|
+
`mobile: true`, `device_scale_factor`, `locale`, `timezone_id`,
|
|
569
|
+
`color_scheme: "dark"` e `geolocation: {"latitude": -23.55, "longitude": -46.63}`.
|
|
570
|
+
Informar geolocalização concede também a permissão `geolocation`.
|
|
571
|
+
|
|
572
|
+
`audit_accessibility` aceita `scope` com seletor CSS e `fail_on_impact` com
|
|
573
|
+
`minor`, `moderate`, `serious` ou `critical`. O padrão é `minor`; violações
|
|
574
|
+
abaixo do impacto escolhido continuam no relatório, mas não bloqueiam o passo.
|
|
575
|
+
`violations_count` e `blocking_violations_count` separam total e bloqueadoras.
|
|
576
|
+
|
|
577
|
+
Os cliques aguardam `DOMContentLoaded` quando acionam navegação completa e
|
|
578
|
+
tratam a destruição do contexto durante a troca de documento como navegação em
|
|
579
|
+
andamento, sem repetir o clique.
|
|
512
580
|
|
|
513
581
|
Se o Chrome/Edge não iniciar porque o perfil persistente já está aberto, o erro
|
|
514
582
|
identifica o PID que o mantém ocupado quando o sistema consegue associar o
|
|
@@ -565,18 +633,21 @@ sob demanda podem excedê-la. O MCP devolve `total_ms`, `browser_session_ms`,
|
|
|
565
633
|
para localizar o custo. O teto observado em um fluxo sintético local anterior
|
|
566
634
|
foi 6,411 s; isso não mede o Instagram nem garante o mesmo tempo em outros sites.
|
|
567
635
|
|
|
568
|
-
O snapshot inicial, o fluxo, o resultado esperado e as descrições dos planos
|
|
569
|
-
são enviados ao endpoint Decisions quando há mais de uma opção.
|
|
570
|
-
|
|
571
|
-
|
|
636
|
+
O snapshot inicial, o fluxo, o resultado esperado e as descrições dos planos
|
|
637
|
+
são enviados ao endpoint Decisions quando há mais de uma opção. Antes do envio,
|
|
638
|
+
valores dos controles são substituídos e hrefs têm query string e fragmento
|
|
639
|
+
removidos; padrões de PII também são mascarados. Com fast-path, um plano não
|
|
640
|
+
gera chamada remota. Os passos, valores digitados, valores esperados pelas
|
|
641
|
+
asserções, caminhos e conteúdo dos arquivos não são enviados.
|
|
572
642
|
Não inclua segredos em `flow`, `expected_outcome` ou nas descrições. A resposta
|
|
573
643
|
retorna o plano escolhido quando houver decisão, custo/confiança do provedor
|
|
574
644
|
quando disponíveis e o snapshot final sanitizado. O fluxo passa quando o texto
|
|
575
645
|
esperado aparece no snapshot ou quando todas as asserções declaradas passam.
|
|
576
646
|
|
|
577
|
-
`network_idle` é uma espera limitada e opcional
|
|
578
|
-
|
|
579
|
-
|
|
647
|
+
`network_idle` é uma espera limitada e opcional. Polling e conexões contínuas
|
|
648
|
+
não relacionadas a `url_contains` não bloqueiam o modo filtrado; prefira
|
|
649
|
+
`wait_for_text` ou `assert_network` quando houver sinal de interface ou uma
|
|
650
|
+
resposta HTTP específica.
|
|
580
651
|
|
|
581
652
|
Consulte a [introdução do Jev](https://docs.typesafe.ai/introduction) e o
|
|
582
653
|
[tutorial da API Decisions no OpenRouter](https://openrouter.ai/docs/guides/community/jev-tutorial)
|
package/config/ui-testing.json
CHANGED
|
@@ -38,7 +38,14 @@
|
|
|
38
38
|
"visibility_poll_interval_ms_default": 50,
|
|
39
39
|
"capture_network_error_bodies_default": false,
|
|
40
40
|
"max_network_error_body_bytes": 65536,
|
|
41
|
-
"max_network_error_message_chars": 300,
|
|
41
|
+
"max_network_error_message_chars": 300,
|
|
42
|
+
"max_network_history_events": 4096,
|
|
43
|
+
"max_tool_response_bytes": 131072,
|
|
44
|
+
"max_viewport_width": 3840,
|
|
45
|
+
"max_viewport_height": 2160,
|
|
46
|
+
"accessibility_fail_on_impact_default": "minor",
|
|
47
|
+
"record_video_width": 1280,
|
|
48
|
+
"record_video_height": 720,
|
|
42
49
|
"max_upload_files": 5,
|
|
43
50
|
"max_upload_path_chars": 4096,
|
|
44
51
|
"max_upload_file_bytes": 10485760,
|
|
@@ -53,7 +60,8 @@
|
|
|
53
60
|
"max_frame_snapshots": 5,
|
|
54
61
|
"fast_path_single_plan_default": true,
|
|
55
62
|
"screenshot_on_failure_default": true,
|
|
56
|
-
"trace_on_failure_default": false,
|
|
63
|
+
"trace_on_failure_default": false,
|
|
64
|
+
"trace_on_success_default": false,
|
|
57
65
|
"capture_console_errors_default": true,
|
|
58
66
|
"capture_network_errors_default": false,
|
|
59
67
|
"block_trackers_default": false,
|
package/docs/jev-browser-mcp.md
CHANGED
|
@@ -17,7 +17,7 @@ versão em produção, use o número explícito no argumento do pacote:
|
|
|
17
17
|
"mcpServers": {
|
|
18
18
|
"jev-browser": {
|
|
19
19
|
"command": "npx",
|
|
20
|
-
"args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.6.
|
|
20
|
+
"args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.6.2"],
|
|
21
21
|
"env": {
|
|
22
22
|
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}",
|
|
23
23
|
"JEV_BROWSER_MODE": "computer"
|
|
@@ -32,7 +32,7 @@ um secret manager ou pelo ambiente do processo; não grave a chave no arquivo
|
|
|
32
32
|
de configuração. Para instalar no projeto Node do próprio harness:
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
|
-
npm install @diegosouzacdv/jev-browser-mcp@0.6.
|
|
35
|
+
npm install @diegosouzacdv/jev-browser-mcp@0.6.2
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Se o harness executa o MCP repetidamente nesta máquina, instale uma versão
|
|
@@ -40,7 +40,7 @@ fixa globalmente para evitar a resolução e o download feitos pelo `npx` em cad
|
|
|
40
40
|
inicialização:
|
|
41
41
|
|
|
42
42
|
```sh
|
|
43
|
-
npm install --global @diegosouzacdv/jev-browser-mcp@0.6.
|
|
43
|
+
npm install --global @diegosouzacdv/jev-browser-mcp@0.6.2
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Depois configure o servidor MCP com `command: "jev-browser-mcp"` e `args: []`.
|
|
@@ -49,7 +49,7 @@ Para atualizar, rode `npm install --global
|
|
|
49
49
|
|
|
50
50
|
O padrão `computer` exige Chrome ou Edge instalado. Se optar por `harness`,
|
|
51
51
|
configure `JEV_BROWSER_MODE=harness` antes de instalar o navegador gerenciado
|
|
52
|
-
pelo Playwright com `npx --yes @diegosouzacdv/jev-browser-mcp@0.6.
|
|
52
|
+
pelo Playwright com `npx --yes @diegosouzacdv/jev-browser-mcp@0.6.2 --install-browser`.
|
|
53
53
|
|
|
54
54
|
O padrão é `computer`: o pacote abre o Chrome/Edge instalado e usa um perfil
|
|
55
55
|
persistente exclusivo em `browser.computer_user_data_dir`. No modo `harness`, a
|
|
@@ -107,11 +107,17 @@ a página inicial, captura o snapshot acessível e pede ao Jev que escolha um
|
|
|
107
107
|
plano. Em seguida, executa o plano inteiro na mesma sessão e confere o
|
|
108
108
|
resultado esperado na tela.
|
|
109
109
|
|
|
110
|
-
Cada plano pode usar:
|
|
111
|
-
|
|
112
|
-
- `
|
|
113
|
-
|
|
114
|
-
`
|
|
110
|
+
Cada plano pode usar:
|
|
111
|
+
|
|
112
|
+
- `navigate` para mudar de página no meio do fluxo. Com a continuidade ligada,
|
|
113
|
+
um `initial_url` de mesma origem diferente da URL ativa produz erro claro; use
|
|
114
|
+
`navigate` ou `continue_from_current_page: false` para escolher o destino;
|
|
115
|
+
- `click`, `type`, `clear`, `hover`, `select_option`, `press`, `assert_*` e upload com
|
|
116
|
+
papel/nome acessível ou um localizador: `label`, `placeholder`, `title`,
|
|
117
|
+
`text`, `test_id` ou `selector`;
|
|
118
|
+
- `name_match: "exact" | "contains" | "regex"` para controlar a comparação do
|
|
119
|
+
nome acessível; regex rejeita construções com backtracking excessivo. O papel
|
|
120
|
+
`switch` funciona em cliques, verificações e asserções;
|
|
115
121
|
- `near: {"text":"..."}` para localizar o controle logo depois de um texto,
|
|
116
122
|
`within: {"row_containing":"..."}` para limitar por trecho e
|
|
117
123
|
`within: {"row_containing_exact":"..."}` para exigir um elemento com o
|
|
@@ -128,25 +134,34 @@ Cada plano pode usar:
|
|
|
128
134
|
- `selector` com CSS ou XPath como último recurso. O MCP identifica essa escolha
|
|
129
135
|
em `warnings`; não é permitido enviar JavaScript nem coordenadas;
|
|
130
136
|
- `timeout_seconds` opcional em cada etapa, limitado pela configuração central;
|
|
131
|
-
- `type` com `mode: "keys"` para digitar sequencialmente em campos com máscara,
|
|
132
|
-
`blur: true` para desfocar o campo e `sensitive: false` para permitir que o
|
|
133
|
-
valor apareça nas evidências.
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
- `type` com `mode: "keys"` para digitar sequencialmente em campos com máscara,
|
|
138
|
+
`blur: true` para desfocar o campo e `sensitive: false` para permitir que o
|
|
139
|
+
valor apareça nas evidências. `text_env` lê o valor sensível de uma variável
|
|
140
|
+
de ambiente do processo MCP, sem colocá-lo no plano. Nesse modo, `\n` envia
|
|
141
|
+
Enter e `\t` envia Tab;
|
|
142
|
+
- `wait_for_text`, `wait_for_value`, `wait_for_enabled` e `wait_for_condition`
|
|
143
|
+
(`url`, `network_idle`, `hidden`, `text_hidden` ou `angular_idle`). `wait` recebe
|
|
136
144
|
`ms` para uma pausa curta e limitada. `angular_idle` aguarda requisições
|
|
137
145
|
`$http` e digest do AngularJS. `wait_for_text` aceita `fail_on: {"role":"alert"}` para
|
|
138
146
|
interromper a espera assim que um alerta visível aparecer e incluir seu texto
|
|
139
147
|
no erro. Se o alerta contiver o texto esperado, a etapa passa; caso contrário,
|
|
140
|
-
o alerta interrompe a espera. `
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
o alerta interrompe a espera. `url` aguarda a URL conter `url_contains`.
|
|
149
|
+
`network_idle` aceita esse filtro e aguarda uma janela de silêncio usando
|
|
150
|
+
`ready_stable_ms`; WebSocket, EventSource e polling não relacionado não
|
|
151
|
+
bloqueiam a espera. Se não surgir requisição correspondente, o passo conclui
|
|
152
|
+
após a janela de silêncio;
|
|
153
|
+
- `press` (o alias legado `press_key` também é aceito) com `PageDown`, `PageUp`,
|
|
154
|
+
`Home`, `End`, setas, `Enter`, `Escape`, `Tab`, `Shift+Tab`, `Space`,
|
|
155
|
+
`Backspace` ou `Delete`. `clear` limpa um campo sem exigir texto. Sem alvo,
|
|
156
|
+
`press` envia a tecla ao elemento focado; com alvo, usa o localizador informado;
|
|
145
157
|
- `assert_text`, `assert_value`, `assert_visible`, `assert_hidden` e
|
|
146
158
|
`assert_enabled`; `assert_text`
|
|
147
159
|
pode receber apenas `role` quando a região, como `alert`, não tem nome acessível;
|
|
148
|
-
- `upload_file` para input rotulado, botão que abre o seletor de arquivo ou
|
|
149
|
-
dropzone;
|
|
160
|
+
- `upload_file` para input rotulado, botão que abre o seletor de arquivo ou
|
|
161
|
+
dropzone;
|
|
162
|
+
- `target_page: "popup"` para continuar no popup aberto com `expect_popup: true`;
|
|
163
|
+
locators semânticos também atravessam open Shadow DOM. Shadow DOM fechado não
|
|
164
|
+
é acessível ao Playwright;
|
|
150
165
|
- `audit_accessibility` com axe-core para WCAG 2.1 A/AA;
|
|
151
166
|
- `like_comment` e `unlike_comment`, que localizam uma linha pelo autor e texto,
|
|
152
167
|
não repetem uma reação já no estado pedido e distinguem `Curtir` de
|
|
@@ -211,11 +226,12 @@ com `within: {"row_containing_exact":"{pedido}"}`, e use uma etapa
|
|
|
211
226
|
`{"action":"extract","role":"cell","name":"{pedido}","within":{"row_containing_exact":"{pedido}"},"as":"documento"}`.
|
|
212
227
|
A etapa seguinte pode localizar o mesmo documento com `name: "{documento}"`.
|
|
213
228
|
|
|
214
|
-
Em `wait_for_condition` com `condition: "hidden"`, use qualquer um desses
|
|
215
|
-
localizadores, `near` ou `within`; `text_hidden` recebe o texto direto. Para
|
|
216
|
-
aguardar o fim de uma chamada específica, use `condition: "network_idle"` com
|
|
217
|
-
`url_contains
|
|
218
|
-
|
|
229
|
+
Em `wait_for_condition` com `condition: "hidden"`, use qualquer um desses
|
|
230
|
+
localizadores, `near` ou `within`; `text_hidden` recebe o texto direto. Para
|
|
231
|
+
aguardar o fim de uma chamada específica, use `condition: "network_idle"` com
|
|
232
|
+
`url_contains`. O filtro considera somente a atividade correspondente e termina
|
|
233
|
+
após a janela configurada sem atividade; também termina caso nenhuma requisição
|
|
234
|
+
correspondente apareça. Para validar status e corpo, prefira `assert_network`.
|
|
219
235
|
|
|
220
236
|
Quando vários controles têm o mesmo papel e nome, `within` limita a busca a um
|
|
221
237
|
container acessível único, como uma linha, card ou diálogo. `index` escolhe uma
|
|
@@ -411,15 +427,17 @@ validadores.
|
|
|
411
427
|
|
|
412
428
|
`browser.mode` aceita `harness` ou `computer`; o padrão é `computer`:
|
|
413
429
|
|
|
414
|
-
- `harness` usa Chrome headless e contexto isolado, adequado a execuções do
|
|
415
|
-
harness e CI
|
|
430
|
+
- `harness` usa Chrome headless e contexto isolado, adequado a execuções do
|
|
431
|
+
harness e CI. O processo MCP conserva browser, contexto e página entre
|
|
432
|
+
chamadas; cookies, local storage e estado da página permanecem enquanto o
|
|
433
|
+
processo estiver ativo. Encerre o processo para descartar o contexto isolado.
|
|
416
434
|
- `computer` abre o Chrome ou Edge instalado em modo visível e usa o diretório
|
|
417
435
|
persistente `browser.computer_user_data_dir`, separado por navegador. Não
|
|
418
436
|
reutiliza o perfil pessoal já aberto. Faça login uma vez nesse perfil; os
|
|
419
437
|
cookies permanecem nele entre chamadas.
|
|
420
438
|
|
|
421
|
-
`browser.max_flow_steps` limita
|
|
422
|
-
`browser.max_text_entry_chars` limita cada valor digitado. O resultado contém
|
|
439
|
+
`browser.max_flow_steps` limita os passos de cada plano candidato; os planos
|
|
440
|
+
não são somados. `browser.max_text_entry_chars` limita cada valor digitado. O resultado contém
|
|
423
441
|
`status`, o plano escolhido, as ações executadas, a última captura acessível e
|
|
424
442
|
se o critério esperado apareceu. Quando existem asserções explícitas, `status`
|
|
425
443
|
também pode ser `passed` com todas elas satisfeitas; `assertions_passed` registra
|
|
@@ -457,9 +475,12 @@ O `run_browser_flow` aceita `options` com `fast_path`, `snapshot_scope`,
|
|
|
457
475
|
`auto_angular_idle`, `login_url_contains`, `login_text`, `capture_console_errors`,
|
|
458
476
|
`capture_network_errors`, `capture_network_error_bodies`, `ready_timeout_seconds`,
|
|
459
477
|
`ready_network_idle`, `ready_stable_ms`, `ready_text`, `continue_from_current_page`,
|
|
460
|
-
`reuse_page`, `step_timeout_seconds`, `max_flow_steps`, `return_snapshot`,
|
|
461
|
-
`screenshot_on_failure`, `
|
|
462
|
-
`
|
|
478
|
+
`reuse_page`, `step_timeout_seconds`, `max_flow_steps`, `return_snapshot`,
|
|
479
|
+
`screenshot_on_failure`, `screenshot_on_success`, `trace_on_failure`,
|
|
480
|
+
`trace_on_success`, `record_video`, `snapshot_include_hidden`, `stop_on_expected`,
|
|
481
|
+
`dry_run`, `confirmation_token`, `report_path`, `permissions`, `fake_media`,
|
|
482
|
+
`viewport`, `mobile`, `device_scale_factor`, `locale`, `timezone_id`,
|
|
483
|
+
`color_scheme`, `geolocation` e `allow_mutations`. Sem override,
|
|
463
484
|
os padrões são lidos de `jev_browser_mcp` em
|
|
464
485
|
`config/ui-testing.json`: um plano candidato pula a chamada Decisions;
|
|
465
486
|
captura de console e screenshot de falha ficam ligadas; trace, rede e bloqueio
|
|
@@ -471,26 +492,33 @@ chamadas na mesma origem, não repete `page.goto` e consulta um snapshot atual
|
|
|
471
492
|
sem esperar novamente por `load`/`networkidle`. As etapas continuam aguardando
|
|
472
493
|
seus próprios alvos e condições. `reuse_page` segue como alias legado; defina
|
|
473
494
|
`continue_from_current_page: false` para iniciar cada chamada em `initial_url`.
|
|
474
|
-
Se a página atual e `initial_url` tiverem origens diferentes, o MCP navega para
|
|
475
|
-
`initial_url`. Se forem da mesma origem
|
|
476
|
-
|
|
477
|
-
`
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
495
|
+
Se a página atual e `initial_url` tiverem origens diferentes, o MCP navega para
|
|
496
|
+
`initial_url`. Se forem da mesma origem mas URLs completas diferentes, o MCP
|
|
497
|
+
recusa continuar silenciosamente e pede uma etapa `navigate` ou
|
|
498
|
+
`continue_from_current_page: false`. `snapshot_scope` aceita `body`, `main` ou
|
|
499
|
+
`dialog`; se `main` não existir, o snapshot usa `body`.
|
|
500
|
+
|
|
501
|
+
`block_trackers: true` bloqueia analytics/trackers e os tipos de recurso
|
|
502
|
+
configurados, mas preserva `media` quando há permissão de microfone ou
|
|
503
|
+
`fake_media`. Fontes ficam habilitadas por padrão para preservar ícones e
|
|
504
|
+
glyphs; `block_fonts: true` bloqueia fontes separadamente. `busy_selectors`
|
|
505
|
+
complementa `aria-busy="true"` ao aguardar overlays; elementos ocultos ou fora
|
|
506
|
+
da tela com esse atributo não seguram a prontidão da página.
|
|
483
507
|
`auto_angular_idle` aguarda AngularJS depois de cliques e digitação quando a
|
|
484
508
|
página expõe o injector. `login_url_contains` e `login_text` substituem a
|
|
485
509
|
detecção padrão de autenticação. `local_only: true` recusa qualquer etapa que
|
|
486
|
-
precisaria chamar o provedor Jev; use exatamente um plano com `fast_path: true`.
|
|
510
|
+
precisaria chamar o provedor Jev; use exatamente um plano com `fast_path: true`.
|
|
511
|
+
`allow_mutations: true` pula a confirmação humana somente para URLs em
|
|
512
|
+
`localhost`, `127.0.0.1` ou `::1`; use esse modo apenas em ambientes locais
|
|
513
|
+
controlados.
|
|
487
514
|
`step_timeout_seconds` define o timeout padrão por etapa e cada etapa pode
|
|
488
515
|
substituí-lo com `timeout_seconds`. `max_flow_steps` reduz o limite por chamada,
|
|
489
516
|
sem superar o teto da configuração. `return_snapshot` aceita `full`, `diff` ou
|
|
490
517
|
`none`; `diff` retorna linhas acessíveis novas desde o snapshot inicial.
|
|
491
518
|
|
|
492
|
-
Com `capture_console_errors` e `capture_network_errors`, o retorno traz
|
|
493
|
-
`console_errors` e `network_failures`, limitados em quantidade e tamanho.
|
|
519
|
+
Com `capture_console_errors` e `capture_network_errors`, o retorno traz
|
|
520
|
+
`console_errors` e `network_failures`, limitados em quantidade e tamanho.
|
|
521
|
+
Mensagens de erro idênticas do console são agrupadas em uma entrada com `count`.
|
|
494
522
|
Respostas HTTP 4xx/5xx e requisições que falham são listadas; query strings,
|
|
495
523
|
fragmentos, valores de formulário e nomes de arquivo são removidos ou
|
|
496
524
|
sanitizados. `capture_network_error_bodies: true` lê somente respostas JSON
|
|
@@ -506,9 +534,49 @@ compartilhe os arquivos somente se o teste permitir.
|
|
|
506
534
|
|
|
507
535
|
Screenshots por etapa usam `screenshot: true` no próprio passo; o MCP os grava
|
|
508
536
|
depois que a ação termina. `options.report_path` pode apontar para `.md` ou
|
|
509
|
-
JUnit `.xml` dentro da raiz de artefatos. O relatório inclui a versão, tempos,
|
|
510
|
-
localizadores resolvidos, respostas verificadas por `assert_network`, falhas de
|
|
511
|
-
rede e caminhos dos screenshots, para anexar a um PR ou card.
|
|
537
|
+
JUnit `.xml` dentro da raiz de artefatos. O relatório inclui a versão, tempos,
|
|
538
|
+
localizadores resolvidos, respostas verificadas por `assert_network`, falhas de
|
|
539
|
+
rede e caminhos dos screenshots, para anexar a um PR ou card.
|
|
540
|
+
|
|
541
|
+
`screenshot_on_success` grava a captura final em fluxos aprovados;
|
|
542
|
+
`trace_on_success` salva trace também em sucesso e `record_video` grava vídeo
|
|
543
|
+
WebM. Fluxos que digitam texto sensível suprimem screenshots, traces e vídeos
|
|
544
|
+
para não registrar credenciais. `performance_metrics` inclui métricas do
|
|
545
|
+
navegador quando disponíveis. `jev_browser_mcp.browser.max_tool_response_bytes`
|
|
546
|
+
impõe um teto global de bytes na resposta JSON da ferramenta; se necessário,
|
|
547
|
+
evidências volumosas são reduzidas e marcadas com `output_truncated: true`.
|
|
548
|
+
|
|
549
|
+
Para testar microfone, conceda permissão no escopo da chamada:
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
{
|
|
553
|
+
"options": {
|
|
554
|
+
"permissions": ["microphone"],
|
|
555
|
+
"fake_media": "fixtures/resposta-aluno.wav",
|
|
556
|
+
"block_trackers": true
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`fake_media` exige um WAV RIFF válido dentro de `JEV_BROWSER_UPLOAD_ROOT` e
|
|
562
|
+
respeita os mesmos limites de upload. O MCP adiciona as opções de dispositivo
|
|
563
|
+
falso do Chromium e concede permissão ao contexto. Sem esse arquivo, a
|
|
564
|
+
permissão de microfone usa o dispositivo autorizado pelo browser; no modo
|
|
565
|
+
`computer`, o sistema operacional ainda pode pedir acesso ao dispositivo.
|
|
566
|
+
|
|
567
|
+
As opções de emulação aceitam `viewport: {"width": 390, "height": 844}`,
|
|
568
|
+
`mobile: true`, `device_scale_factor`, `locale`, `timezone_id`,
|
|
569
|
+
`color_scheme: "dark"` e `geolocation: {"latitude": -23.55, "longitude": -46.63}`.
|
|
570
|
+
Informar geolocalização concede também a permissão `geolocation`.
|
|
571
|
+
|
|
572
|
+
`audit_accessibility` aceita `scope` com seletor CSS e `fail_on_impact` com
|
|
573
|
+
`minor`, `moderate`, `serious` ou `critical`. O padrão é `minor`; violações
|
|
574
|
+
abaixo do impacto escolhido continuam no relatório, mas não bloqueiam o passo.
|
|
575
|
+
`violations_count` e `blocking_violations_count` separam total e bloqueadoras.
|
|
576
|
+
|
|
577
|
+
Os cliques aguardam `DOMContentLoaded` quando acionam navegação completa e
|
|
578
|
+
tratam a destruição do contexto durante a troca de documento como navegação em
|
|
579
|
+
andamento, sem repetir o clique.
|
|
512
580
|
|
|
513
581
|
Se o Chrome/Edge não iniciar porque o perfil persistente já está aberto, o erro
|
|
514
582
|
identifica o PID que o mantém ocupado quando o sistema consegue associar o
|
|
@@ -565,18 +633,21 @@ sob demanda podem excedê-la. O MCP devolve `total_ms`, `browser_session_ms`,
|
|
|
565
633
|
para localizar o custo. O teto observado em um fluxo sintético local anterior
|
|
566
634
|
foi 6,411 s; isso não mede o Instagram nem garante o mesmo tempo em outros sites.
|
|
567
635
|
|
|
568
|
-
O snapshot inicial, o fluxo, o resultado esperado e as descrições dos planos
|
|
569
|
-
são enviados ao endpoint Decisions quando há mais de uma opção.
|
|
570
|
-
|
|
571
|
-
|
|
636
|
+
O snapshot inicial, o fluxo, o resultado esperado e as descrições dos planos
|
|
637
|
+
são enviados ao endpoint Decisions quando há mais de uma opção. Antes do envio,
|
|
638
|
+
valores dos controles são substituídos e hrefs têm query string e fragmento
|
|
639
|
+
removidos; padrões de PII também são mascarados. Com fast-path, um plano não
|
|
640
|
+
gera chamada remota. Os passos, valores digitados, valores esperados pelas
|
|
641
|
+
asserções, caminhos e conteúdo dos arquivos não são enviados.
|
|
572
642
|
Não inclua segredos em `flow`, `expected_outcome` ou nas descrições. A resposta
|
|
573
643
|
retorna o plano escolhido quando houver decisão, custo/confiança do provedor
|
|
574
644
|
quando disponíveis e o snapshot final sanitizado. O fluxo passa quando o texto
|
|
575
645
|
esperado aparece no snapshot ou quando todas as asserções declaradas passam.
|
|
576
646
|
|
|
577
|
-
`network_idle` é uma espera limitada e opcional
|
|
578
|
-
|
|
579
|
-
|
|
647
|
+
`network_idle` é uma espera limitada e opcional. Polling e conexões contínuas
|
|
648
|
+
não relacionadas a `url_contains` não bloqueiam o modo filtrado; prefira
|
|
649
|
+
`wait_for_text` ou `assert_network` quando houver sinal de interface ou uma
|
|
650
|
+
resposta HTTP específica.
|
|
580
651
|
|
|
581
652
|
Consulte a [introdução do Jev](https://docs.typesafe.ai/introduction) e o
|
|
582
653
|
[tutorial da API Decisions no OpenRouter](https://openrouter.ai/docs/guides/community/jev-tutorial)
|