## 1. Modelo de nomes do bridge O app expõe três estilos de bridge: 1. Objetos nativos `Cake*`. São os objetos reais registrados pelo Android com `addJavascriptInterface`. 2. Aliases `Dt*`. Existem para compatibilidade com temas já existentes. 3. Helpers `CakeApp.*`. São pequenos wrappers JavaScript injetados pelo app. Para novos temas, você pode usar qualquer estilo, mas a abordagem recomendada é: - Use `Dt*` quando quiser máxima compatibilidade com temas existentes. - Use `CakeApp.*` quando quiser código mais limpo. - Sempre proteja as chamadas, porque o tema também pode ser aberto no navegador durante testes. Exemplo de helper seguro: ```js function bridgeExec(name, fallback = '') { try { const obj = window[name]; if (obj && typeof obj.execute === 'function') return obj.execute(); } catch (e) {} return fallback; } function bridgeCommand(name) { try { const obj = window[name]; if (obj && typeof obj.execute === 'function') obj.execute(); } catch (e) {} } function bridgeSet(name, value) { try { const obj = window[name]; if (obj && typeof obj.set === 'function') obj.set(String(value ?? '')); } catch (e) {} } function bridgeGet(name, fallback = '') { try { const obj = window[name]; if (obj && typeof obj.get === 'function') return obj.get(); } catch (e) {} return fallback; } ``` --- ## 2. Comportamento importante do WebView O app ativa JavaScript e DOM storage no WebView do OnlineConfig. O HTML é carregado usando a URL do painel como URL base, então URLs relativas são resolvidas a partir do domínio do painel. Antes de carregar o HTML, o app sanitiza o layout: - Corrige alguns padrões quebrados de regex envolvendo quebras de linha. - Remove qualquer bloco de script que comece neste marcador: ```js // --- AMBIENTE MOCK PARA TESTES --- ``` Não coloque código de produção abaixo desse marcador. O app trata isso como uma seção local de mock/teste e remove essa parte do tema carregado. O app injeta o bridge de compatibilidade antes de `` quando o HTML contém uma tag `head`. Se o HTML não tiver ``, o bridge é colocado no começo do documento. --- ## 3. Esqueleto rápido de tema ```html Tema OnlineConfig
Desconectado
``` --- ## 4. Helpers principais `CakeApp` O app injeta esse objeto para facilitar o código dos temas. ### `CakeApp.vpn` ```js CakeApp.vpn.getState(); // retorna string CakeApp.vpn.start(); // inicia/dispara o comando da VPN CakeApp.vpn.stop(); // para a VPN ``` Aliases equivalentes: ```js DtGetVpnState.execute(); DtExecuteVpnStart.execute(); DtExecuteVpnStop.execute(); ``` ### `CakeApp.configs` ```js CakeApp.configs.list(); // retorna string JSON com grupos/items CakeApp.configs.current(); // retorna string JSON do perfil selecionado CakeApp.configs.select(id); // seleciona um perfil pelo id ``` Aliases equivalentes: ```js DtGetConfigs.execute(); DtGetDefaultConfig.execute(); DtSetConfig.execute(id); ``` ### `CakeApp.auth` ```js CakeApp.auth.getUsername(); CakeApp.auth.setUsername(value); CakeApp.auth.getPassword(); CakeApp.auth.setPassword(value); CakeApp.auth.getUuid(); CakeApp.auth.setUuid(value); ``` Aliases equivalentes: ```js DtUsername.get(); DtUsername.set(value); DtPassword.get(); DtPassword.set(value); DtUuid.get(); DtUuid.set(value); ``` ### `CakeApp.errors` ```js CakeApp.errors.getLast(); // retorna string JSON com o último erro de conexão ``` Aliases equivalentes: ```js DtGetLastConnectionError.execute(); DtGetConnectionError.execute(); ``` ### `CakeApp.system` ```js CakeApp.system.openUrl(url); CakeApp.system.getStatusBarHeight(); CakeApp.system.getNavigationBarHeight(); ``` Aliases equivalentes: ```js DtStartWebViewActivity.execute(url); DtGetStatusBarHeight.execute(); DtGetNavigationBarHeight.execute(); ``` --- ## 5. Tabela completa das APIs do bridge Mapa atual dos objetos injetados: | Objeto nativo `Cake*` | Alias `Dt*` | Método | |---|---|---| | `CakeGetVpnState` | `DtGetVpnState` | `execute()` | | `CakeGetConfigs` | `DtGetConfigs` | `execute()` | | `CakeGetDefaultConfig` | `DtGetDefaultConfig` | `execute()` | | `CakeSetConfig` | `DtSetConfig` | `execute(id)` | | `CakeUsername` | `DtUsername` | `get()`, `set(value)` | | `CakePassword` | `DtPassword` | `get()`, `set(value)` | | `CakeUuid` | `DtUuid` | `get()`, `set(value)` | | `CakeStartAppUpdate` | `DtStartAppUpdate` | `execute()` | | `CakeStartUpdatePayload` | `DtStartUpdatePayload` | `execute()` | | `CakeExecuteVpnStart` | `DtExecuteVpnStart` | `execute()` | | `CakeExecuteVpnStop` | `DtExecuteVpnStop` | `execute()` | | `CakeGetLogs` | `DtGetLogs` | `execute()` | | `CakeGetLastConnectionError` | `DtGetLastConnectionError`, `DtGetConnectionError` | `execute()` | | `CakeGetNetworkDownloadBytes` | `DtGetNetworkDownloadBytes` | `execute()` | | `CakeGetLocalConfigVersion` | `DtGetLocalConfigVersion` | `execute()` | | `CakeShowLoggerDialog` | `DtShowLoggerDialog` | `execute()` | | `CakeStartCheckUser` | `DtStartCheckUser` | `execute()` | | `CakeGetStatusHotSpotService` | `DtGetStatusHotSpotService` | `execute()` | | `CakeStartHotSpotService` | `DtStartHotSpotService` | `execute()` | | `CakeStopHotSpotService` | `DtStopHotSpotService` | `execute()` | | `CakeGetLocalIP` | `DtGetLocalIP` | `execute()` | | `CakeGetNetworkName` | `DtGetNetworkName` | `execute()` | | `CakeGetPingResult` | `DtGetPingResult` | `execute()` | | `CakeGetStatusBarHeight` | `DtGetStatusBarHeight` | `execute()` | | `CakeGetNavigationBarHeight` | `DtGetNavigationBarHeight` | `execute()` | | `CakeStartWebViewActivity` | `DtStartWebViewActivity` | `execute(url)` | | `CakeGetAppConfig` | `DtGetAppConfig` | `execute(label)` | | `CakeIgnoreBatteryOptimizations` | `DtIgnoreBatteryOptimizations` | `execute()` | | `CakeStartApnActivity` | `DtStartApnActivity` | `execute()` | | `CakeCleanApp` | `DtCleanApp` | `execute()` | | `CakeAirplaneState` | `DtAirplaneState` | `execute()` | | `CakeAirplaneActivate` | `DtAirplaneActivate` | `execute()` | | `CakeAirplaneDeactivate` | `DtAirplaneDeactivate` | `execute()` | O helper `CakeApp` injetado nesta versão expõe somente `CakeApp.vpn`, `CakeApp.configs`, `CakeApp.auth`, `CakeApp.errors` e `CakeApp.system`. ### 5.1 APIs de config e perfil | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtGetConfigs.execute()` | sem argumentos | string JSON | Retorna todos os grupos e perfis do OnlineConfig para montar a lista de servidores. | | `DtGetDefaultConfig.execute()` | sem argumentos | string JSON | Retorna o perfil atualmente selecionado. Retorna `{}` se nada estiver selecionado. | | `DtSetConfig.execute(id)` | string id | void | Seleciona um perfil/servidor pelo id e aplica nativamente. | | `DtStartAppUpdate.execute()` | sem argumentos | void | Atualiza o catálogo/layout/cache de configs a partir do painel. | | `DtStartUpdatePayload.execute()` | sem argumentos | void | Mesmo comportamento de `DtStartAppUpdate` nesta versão do app. | | `DtGetLocalConfigVersion.execute()` | sem argumentos | string numérica | Retorna o último valor de timestamp/sincronização do OnlineConfig. Útil como marcador de versão. | ### 5.2 APIs de autenticação | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtUsername.get()` | sem argumentos | string | Retorna o usuário manual salvo, ou `Locked` quando a config selecionada não precisa de credenciais SSH manuais. | | `DtUsername.set(value)` | string | void | Salva o usuário somente se credenciais manuais forem necessárias. Ignorado se estiver bloqueado. | | `DtPassword.get()` | sem argumentos | string | Retorna a senha manual salva, ou `Locked` quando não for necessária. | | `DtPassword.set(value)` | string | void | Salva a senha somente se credenciais manuais forem necessárias. Ignorado se estiver bloqueado. | | `DtUuid.get()` | sem argumentos | string | Retorna o UUID manual V2Ray/Xray salvo, ou `Locked` quando a config selecionada não precisa de UUID manual. | | `DtUuid.set(value)` | string | void | Salva o UUID. Nesta versão do app, escreve o valor do UUID e mantém cache para futuras seleções de config. | Importante: limpe `Locked` antes de colocar valores em inputs. ```js function cleanBridgeValue(value) { const text = String(value ?? '').trim(); return text.toLowerCase() === 'locked' ? '' : text; } ``` ### 5.3 APIs de controle e estado da VPN | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtGetVpnState.execute()` | sem argumentos | string de estado | Lê o estado atual da VPN mapeado para o HTML. | | `DtExecuteVpnStart.execute()` | sem argumentos | void | Inicia/dispara o comando nativo da VPN. | | `DtExecuteVpnStop.execute()` | sem argumentos | void | Para a VPN. | | `DtGetNetworkDownloadBytes.execute()` | sem argumentos | string numérica | Retorna bytes baixados atuais da rede/app para verificação de tráfego. | | `DtGetLogs.execute()` | sem argumentos | string JSON | Retorna logs recentes de status/banner/proxy como array de objetos com timestamp. | | `DtGetLastConnectionError.execute()` | sem argumentos | string JSON | Retorna o último erro classificado de conexão, autenticação ou proxy. Retorna `{}` se não houver erro. | | `DtGetConnectionError.execute()` | sem argumentos | string JSON | Alias de compatibilidade de `DtGetLastConnectionError.execute()`. | | `DtShowLoggerDialog.execute()` | sem argumentos | void | Abre o diálogo nativo de logs. | Possíveis estados de `DtGetVpnState` / `DtVpnStateListener`: | Estado | Significado | |---|---| | `DISCONNECTED` | VPN parada/inativa. | | `CONNECTING` | App iniciando, conectando, resolvendo, atribuindo IP, adicionando rotas ou reconectando. | | `AUTH` | Fase de autenticação SSH/VPN. | | `CONNECTED` | Túnel conectado. | | `DISCONNECTING` | Túnel parando. | | `NO_NETWORK` | Aguardando rede. | | `AUTH_FAILED` | Falha de autenticação. | #### 5.3.1 Erros de conexão/autenticação/proxy O patch expõe um bridge específico para o tema entender por que a conexão falhou, sem precisar depender apenas de texto solto no botão ou no status. ```js function getLastConnectionError() { try { return JSON.parse(DtGetLastConnectionError.execute() || '{}'); } catch (e) { return {}; } } ``` Formato retornado: ```json { "state": "AUTH_FAILED", "code": "LOGIN_INVALID", "message": "Authentication failed", "time": 1777290000000 } ``` Campos: | Campo | Tipo | Significado | |---|---|---| | `state` | string | Estado nativo/mapeado no momento do erro. | | `code` | string | Código normalizado para o tema tratar com UI própria. | | `message` | string | Mensagem original limpa vinda do status/log/banner/proxy. | | `time` | number | Timestamp em milissegundos. | Códigos conhecidos nesta versão: | Código | Quando aparece | Uso recomendado no tema | |---|---|---| | `LOGIN_EXPIRED` | Mensagens contendo acesso expirado/vencido. | Mostrar aviso de renovação. | | `LOGIN_INVALID` | Falha de usuário/senha, HTTP 401 ou texto de login inválido. | Mostrar usuário/senha inválidos. | | `LOGIN_LIMIT` | Mensagens de limite/conexões excedidas. | Mostrar limite de conexões atingido. | | `PROXY_FORBIDDEN` | HTTP 403 ou `Forbidden`. | Mostrar proxy/bughost bloqueado. | | `PROXY_BAD_REQUEST` | HTTP 400 ou `Bad Request`. | Mostrar payload/proxy inválido. | | `PROXY_RATE_LIMITED` | HTTP 429 ou `Too Many Requests`. | Mostrar muitas tentativas/limite temporário. | | `PROXY_SERVER_ERROR` | HTTP 500/502, `Bad Gateway` ou erro interno. | Mostrar falha no proxy/servidor remoto. | | `AUTH_FAILED` | Falha de autenticação sem classificação mais específica. | Mostrar falha genérica de autenticação. | Exemplo de uso direto: ```js function renderLastError() { const error = getLastConnectionError(); if (!error.code) return; const messages = { LOGIN_EXPIRED: 'Seu login expirou. Renove seu acesso.', LOGIN_INVALID: 'Usuário ou senha inválidos.', LOGIN_LIMIT: 'Limite de conexões atingido.', PROXY_FORBIDDEN: 'Proxy/bughost bloqueou a conexão.', PROXY_BAD_REQUEST: 'Payload ou proxy retornou Bad Request.', PROXY_RATE_LIMITED: 'Muitas tentativas. Tente novamente mais tarde.', PROXY_SERVER_ERROR: 'Proxy/servidor remoto retornou erro.', AUTH_FAILED: 'Falha na autenticação.' }; showError(messages[error.code] || error.message || 'Falha na conexão.'); } ``` ### 5.4 APIs de consulta de usuário/conta | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtStartCheckUser.execute()` | sem argumentos | void | Chama o `urlCheckUser` do perfil selecionado e envia o resultado para `dtCheckUserModelListener`. | A URL de check-user suporta estes placeholders: ```txt {username} {user} {password} {uuid} {hwid} ``` Exemplo de campo no perfil: ```json { "urlCheckUser": "https://example.com/checkuser.php?user={username}&uuid={uuid}&hwid={hwid}" } ``` O payload esperado no callback é o que o seu servidor retornar. O app não força um schema fixo. Se a requisição falhar ou se não existir URL, o app envia um JSON fallback parecido com: ```json { "username": "current_user", "expiration_date": "-", "expiration_days": 0, "count_connections": 0, "limit_connections": 0, "message": "" } ``` ### 5.5 APIs de dispositivo/sistema | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtGetLocalIP.execute()` | sem argumentos | string | Retorna o IPv4 local, ou `0.0.0.0`/vazio quando indisponível. | | `DtGetNetworkName.execute()` | sem argumentos | string | Retorna o nome exibido da rede/operadora ativa. | | `DtGetPingResult.execute()` | sem argumentos | string | A versão atual retorna `N/A`. Temas não devem depender disso para ping real. | | `DtGetStatusBarHeight.execute()` | sem argumentos | string numérica | Altura da status bar nativa do Android em pixels. | | `DtGetNavigationBarHeight.execute()` | sem argumentos | string numérica | Altura da navigation bar nativa do Android em pixels. | | `DtStartWebViewActivity.execute(url)` | string URL | void | Abre uma URL externamente/atividade WebView nativa. | | `DtIgnoreBatteryOptimizations.execute()` | sem argumentos | void | Abre as configurações de otimização de bateria. | | `DtStartApnActivity.execute()` | sem argumentos | void | Abre as configurações de APN. | | `DtCleanApp.execute()` | sem argumentos | void | Abre o fluxo nativo de limpar dados/configurações. | | `DtAirplaneState.execute()` | sem argumentos | string | Retorna o estado do modo avião. | | `DtAirplaneActivate.execute()` | sem argumentos | void | Abre configurações do modo avião. Não ativa silenciosamente. | | `DtAirplaneDeactivate.execute()` | sem argumentos | void | Abre configurações do modo avião. Não desativa silenciosamente. | ### 5.6 APIs de proxy local / hotspot O bridge OnlineConfig expõe o serviço de proxy local usando nomes antigos de hotspot para compatibilidade com temas. | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtGetStatusHotSpotService.execute()` | sem argumentos | `RUNNING` ou `STOPPED` | Lê o estado do serviço de proxy local. | | `DtStartHotSpotService.execute()` | sem argumentos | void | Inicia o serviço de proxy local se estiver parado. | | `DtStopHotSpotService.execute()` | sem argumentos | void | Para o serviço de proxy local se estiver rodando. | A porta padrão de proxy usada pelo tema existente é `6821`, mas o bridge em si só expõe iniciar/parar/estado e IP local. Se o tema mostrar um endereço de proxy, monte assim: ```js const proxyAddress = `${DtGetLocalIP.execute()}:6821`; ``` Nesta versão do patch, `DtStartHotSpotService.execute()` não inicia mais o socket diretamente pelo `ProxyServer`. Ele dispara o `ProxyService` com `ACTION_START`, para que o Android mostre uma notificação foreground persistente do proxy, por exemplo: ```txt Proxy ONLINE - 192.168.x.x:6821 ``` Ao chamar `DtStopHotSpotService.execute()`, o app envia `ACTION_STOP` para o mesmo serviço e remove a notificação. Em Android 13+ o app precisa da permissão `POST_NOTIFICATIONS`. O `OnlineConfigWebActivity` tenta pedir essa permissão ao abrir e também antes de iniciar o proxy. Se o usuário negar, o proxy não deve ser considerado iniciado pelo tema. Sempre confirme o status depois do comando: ```js async function startLocalProxyAndRefresh() { DtStartHotSpotService.execute(); setTimeout(() => { const status = DtGetStatusHotSpotService.execute(); renderProxyStatus(status); }, 500); } ``` ### 5.7 APIs de config visual/app | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| | `DtGetAppConfig.execute(label)` | string label | string JSON ou null | Lê labels simples de configuração visual do app. | Labels atuais nesta versão do app: | Label | Retorno atual | |---|---| | `APP_LOGO` | `null` nesta versão do source. | | `APP_BACKGROUND_TYPE` | `{"value":"COLOR"}` | | `APP_BACKGROUND_COLOR` | `{"value":"#121212"}` | | `APP_BACKGROUND_IMAGE` | `{"value":""}` | Uso seguro no tema: ```js function getAppConfigValue(label, fallback = '') { try { const raw = DtGetAppConfig.execute(label); if (!raw) return fallback; const data = JSON.parse(raw); return data && data.value ? data.value : fallback; } catch (e) { return fallback; } } ``` --- ## 6. Callbacks nativos que o tema pode definir Defina estes callbacks em `window` antes ou durante o script principal. O app chama eles com `evaluateJavascript`. ### `window.DtVpnStateListener = function(state) {}` Chamado quando o status nativo da VPN muda e depois que a página termina de carregar. ```js window.DtVpnStateListener = function(state) { updateStatusBadge(state); }; ``` ### `window.dtVpnStoppedSuccessListener = function() {}` Chamado depois que uma parada foi solicitada e o estado mapeado vira `DISCONNECTED`. ```js window.dtVpnStoppedSuccessListener = function() { showToast('Desconectado'); }; ``` ### `window.DtUpdatePayloadSuccessListener = function(message) {}` Chamado depois que o app atualiza o catálogo/layout/cache de configs com sucesso. Nesta versão, a mensagem de sucesso normalmente é `OK`. ```js window.DtUpdatePayloadSuccessListener = function(message) { reloadServerList(); }; ``` ### `window.DtUpdatePayloadErrorListener = function(message) {}` Chamado quando a atualização do catálogo falha. ```js window.DtUpdatePayloadErrorListener = function(message) { alert(message || 'Falha ao atualizar'); }; ``` ### `window.dtCheckUserStartedListener = function() {}` Chamado antes da requisição nativa de check-user começar. ```js window.dtCheckUserStartedListener = function() { showProfileLoading(); }; ``` ### `window.dtCheckUserModelListener = function(modelJson) {}` Chamado quando os dados do check-user estão prontos. `modelJson` é uma string. Faça parse se o endpoint retornar JSON. ```js window.dtCheckUserModelListener = function(modelJson) { let data = {}; try { data = JSON.parse(modelJson); } catch (e) { data = { message: modelJson }; } renderProfile(data); }; ``` ### `window.dtLogsUpdatedListener = function(logsJson) {}` Chamado quando o app adiciona um log visível para o tema. Também existe o alias com inicial maiúscula `window.DtLogsUpdatedListener`. ```js window.dtLogsUpdatedListener = function(logsJson) { let rows = []; try { rows = JSON.parse(logsJson || '[]'); } catch (e) {} const text = rows.map(row => { const time = Object.keys(row)[0] || ''; return `${time} ${row[time] || ''}`; }).join('\n'); renderLogs(text); }; ``` ### `window.dtConnectionErrorListener = function(error) {}` Chamado quando o app classifica um erro de conexão/autenticação/proxy. Também existem estes aliases: ```js window.DtConnectionErrorListener = function(error) {}; window.dtAuthErrorListener = function(error) {}; window.DtAuthErrorListener = function(error) {}; ``` O argumento já chega como objeto JavaScript, não como string JSON. ```js window.dtConnectionErrorListener = function(error) { if (!error || !error.code) return; if (error.code === 'LOGIN_EXPIRED') { showError('Seu login expirou. Renove seu acesso.'); } else if (error.code === 'PROXY_FORBIDDEN') { showError('Proxy/bughost bloqueou a conexão.'); } else { showError(error.message || 'Falha na conexão.'); } }; ``` --- ## 7. JSON de config retornado para o HTML ### 7.1 Estrutura de `DtGetConfigs.execute()` Retorna um array JSON de grupos. Cada grupo tem `items`. ```json [ { "id": "group_1", "name": "TIM", "color": "", "sorter": 1, "items": [ { "id": "profile_1", "name": "TIM Principal", "description": "WebSocket SSL", "icon": "https://example.com/tim.png", "mode": "SSH_PROXY", "methodName": "", "sorter": 1, "status": "ACTIVE", "auth": { "username": true, "password": true, "v2ray_uuid": false, "manual_username": true, "manual_password": true, "manual_v2ray_uuid": false } } ] } ] ``` ### 7.2 Estrutura de `DtGetDefaultConfig.execute()` Retorna o objeto do perfil selecionado, ou `{}` se nada estiver selecionado. ```json { "id": "profile_1", "name": "TIM Principal", "description": "WebSocket SSL", "icon": "https://example.com/tim.png", "mode": "SSH_PROXY", "methodName": "", "auth": { "username": true, "password": true, "v2ray_uuid": false, "manual_username": true, "manual_password": true, "manual_v2ray_uuid": false } } ``` ### 7.3 Significado do `auth` O objeto `auth` informa ao HTML quais inputs manuais o perfil selecionado espera. | Campo auth | Significado | |---|---| | `auth.username` | Mostrar/salvar input de usuário para perfis tipo SSH. | | `auth.password` | Mostrar/salvar input de senha para perfis tipo SSH. | | `auth.v2ray_uuid` | Mostrar/salvar input de UUID para perfis V2Ray/Xray. | | `auth.manual_username` | Duplicado de compatibilidade de `auth.username`. | | `auth.manual_password` | Duplicado de compatibilidade de `auth.password`. | | `auth.manual_v2ray_uuid` | Duplicado de compatibilidade de `auth.v2ray_uuid`. | --- ## 8. Selecionando servidores Monte sua lista de servidores a partir de `DtGetConfigs.execute()`, depois chame `DtSetConfig.execute(id)` quando o usuário selecionar um. ```js function loadConfigs() { try { return JSON.parse(DtGetConfigs.execute() || '[]'); } catch (e) { return []; } } function selectServer(profile) { DtSetConfig.execute(String(profile.id)); refreshCurrentServer(); } function refreshCurrentServer() { const current = JSON.parse(DtGetDefaultConfig.execute() || '{}'); document.querySelector('#serverName').textContent = current.name || 'Selecionar servidor'; updateAuthVisibility(current); } ``` Depois da seleção, o tema deve atualizar a própria UI lendo `DtGetDefaultConfig.execute()` de novo. O bridge atual aplica a config nativamente, mas não injeta um helper automático de visibilidade. --- ## 9. Visibilidade dos campos de autenticação O bridge atual não injeta `CakeApp.applyAuthVisibility()`. O tema deve decidir a visibilidade usando: - `auth.username`, `auth.password` e `auth.v2ray_uuid` vindos de `DtGetDefaultConfig.execute()`. - `DtUsername.get()`, `DtPassword.get()` e `DtUuid.get()`, que retornam `Locked` quando aquele input manual não deve ser editado. HTML recomendado: ```html
``` ### Detecção V2Ray/Xray/VMess/VLess no tema O HTML recebe `mode` do catálogo como texto. Se o painel usa nomes como `VMESS` ou `VLESS` no catálogo, trate esses valores no helper do tema para mostrar o campo UUID. Importante: no payload real de settings aplicado nativamente, o parser do app reconhece `V2RAY`, `XRAY` ou `tunnelType: 6` como modo Xray/V2Ray. Use `VMESS`/`VLESS` como rótulos de catálogo apenas se o helper do tema também tratar esses nomes. O helper abaixo detecta estes formatos como perfis baseados em UUID: ```json { "mode": "V2RAY" } { "mode": "XRAY" } { "mode": "VMESS" } { "mode": "VLESS" } { "mode": "V2RAY - VMESS" } ``` Mesmo assim, o formato mais limpo para configs baseadas em UUID continua sendo: ```json { "id": "v2ray_1", "name": "Servidor V2Ray", "mode": "V2RAY", "auth": { "username": false, "password": false, "v2ray_uuid": true } } ``` ### Helper forte de visibilidade pelo lado do tema Use isto no tema se quiser exibição confiável do UUID para nomes `vmess`, `vless`, `xray` e `v2ray`. ```js function isLocked(value) { return String(value ?? '').trim().toLowerCase() === 'locked'; } function bridgeGet(name) { try { const obj = window[name]; return obj && typeof obj.get === 'function' ? obj.get() : ''; } catch (e) { return ''; } } function bridgeExec(name, fallback = '') { try { const obj = window[name]; return obj && typeof obj.execute === 'function' ? obj.execute() : fallback; } catch (e) { return fallback; } } function isUuidCatalogMode(config) { const mode = String(config?.mode || '').toLowerCase(); return mode.includes('v2ray') || mode.includes('xray') || mode.includes('vmess') || mode.includes('vless'); } function updateAuthVisibility(config = null) { if (!config) { try { config = JSON.parse(bridgeExec('DtGetDefaultConfig', '{}') || '{}'); } catch (e) { config = {}; } } const auth = config?.auth || {}; const uuidLocked = isLocked(bridgeGet('DtUuid')); const usernameLocked = isLocked(bridgeGet('DtUsername')); const passwordLocked = isLocked(bridgeGet('DtPassword')); const uuidRequired = ( auth.v2ray_uuid === true || auth.manual_v2ray_uuid === true || isUuidCatalogMode(config) ) && !uuidLocked; const sshRequired = ( auth.username === true || auth.manual_username === true || auth.password === true || auth.manual_password === true || !usernameLocked || !passwordLocked ) && !uuidRequired; const uuidGroup = document.querySelector('#uuid-group'); const usernameGroup = document.querySelector('#username-group'); const passwordGroup = document.querySelector('#password-group'); if (uuidGroup) uuidGroup.style.display = uuidRequired ? '' : 'none'; if (usernameGroup) usernameGroup.style.display = sshRequired ? '' : 'none'; if (passwordGroup) passwordGroup.style.display = sshRequired ? '' : 'none'; } ``` --- ## 10. Iniciando e parando a conexão Padrão básico: ```js function saveVisibleInputs() { const username = document.querySelector('#username')?.value || ''; const password = document.querySelector('#password')?.value || ''; const uuid = document.querySelector('#v2ray_uuid')?.value || ''; if (username) DtUsername.set(username); if (password) DtPassword.set(password); if (uuid) DtUuid.set(uuid); } function connectOrDisconnect() { const state = DtGetVpnState.execute(); if (state === 'CONNECTED' || state === 'CONNECTING' || state === 'AUTH') { DtExecuteVpnStop.execute(); return; } const current = JSON.parse(DtGetDefaultConfig.execute() || '{}'); if (!current.id) { alert('Selecione um servidor primeiro.'); return; } saveVisibleInputs(); DtExecuteVpnStart.execute(); } ``` Desenvolvedores de temas devem controlar o estado da UI com `DtVpnStateListener`, não apenas com eventos de clique. Mudanças nativas de estado podem acontecer fora do clique do botão. --- ## 11. Logs `DtGetLogs.execute()` retorna um array JSON. Cada item é um objeto com uma chave de timestamp e um valor de mensagem. Exemplo: ```json [ { "14:03:55": "Connecting" }, { "14:03:56": "Authenticating" }, { "14:03:57": "Server Message: welcome" }, { "14:03:58": "Proxy Status [HTTP_PROXY]: HTTP/1.1 101 Switching Protocols" }, { "14:04:01": "Proxy Status [SSH_DIRECT]: HTTP/1.1 302 Found" } ] ``` O patch faz o bridge mostrar também respostas HTTP/proxy relevantes para o tema, incluindo: - `Proxy Status [HTTP_PROXY]: HTTP/1.1 101 ...` - `Proxy Status [HTTP_PROXY]: HTTP/1.1 302 ...` - `Proxy Status [SSH_DIRECT]: HTTP/1.1 101 ...` - `Proxy Status [SSH_DIRECT]: HTTP/1.1 302 ...` - `HTTP/1.1 400`, `401`, `403`, `404`, `429`, `500`, `502` - mensagens como `Unauthorized`, `Forbidden`, `Bad Request`, `Too Many Requests` e `Bad Gateway` A diferença importante é o `SSH_DIRECT`: quando o campo de servidor SSH é usado como proxy/bughost e o servidor real está dentro do payload, o app agora faz uma leitura curta e segura da primeira resposta. Se vier HTTP, ele mostra o status para o HTML. Se vier banner SSH, ele preserva os bytes e não quebra o handshake. Helper de renderização: ```js function readLogs() { let rows = []; try { rows = JSON.parse(DtGetLogs.execute() || '[]'); } catch (e) {} return rows.map(row => { const time = Object.keys(row)[0] || ''; const message = row[time] || ''; return `${time} ${message}`; }).join('\n'); } ``` Listener ao vivo: ```js window.dtLogsUpdatedListener = function(logsJson) { let rows = []; try { rows = JSON.parse(logsJson || '[]'); } catch (e) {} const latest = rows.length ? Object.values(rows[rows.length - 1])[0] : ''; if (latest.includes('Proxy Status [SSH_DIRECT]')) { showProxyStatus(latest); } renderLogs(readLogs()); }; ``` O buffer de logs nativo exposto ao HTML é limitado a entradas recentes de status, mensagens de banner do servidor e mensagens úteis de proxy. Ele não é o logcat/debug completo. --- ## 12. Modal de perfil / check-user Fluxo recomendado de perfil: ```js function openProfile() { showProfileLoading(); DtStartCheckUser.execute(); } window.dtCheckUserStartedListener = function() { showProfileLoading(); }; window.dtCheckUserModelListener = function(modelJson) { let data; try { data = JSON.parse(modelJson); } catch (e) { data = { message: modelJson }; } renderProfile({ username: data.username || '-', expirationDate: data.expiration_date || '-', expirationDays: data.expiration_days ?? '-', connections: `${data.count_connections ?? 0}/${data.limit_connections ?? '-'}`, message: data.message || '' }); }; ``` Formato comum de resposta do seu painel/API check: ```json { "username": "utest678", "count_connections": 1, "expiration_date": "25/04/2026", "expiration_days": 0, "limit_connections": 1 } ``` --- ## 13. Botão de atualizar configs Use: ```js function updateConfigs() { DtStartAppUpdate.execute(); } window.DtUpdatePayloadSuccessListener = function(message) { reloadServerList(); alert('Configs atualizadas'); }; window.DtUpdatePayloadErrorListener = function(message) { alert(message || 'Falha ao atualizar configs'); }; ``` Depois de uma atualização bem-sucedida, o app recarrega o layout atual. Não guarde estado crítico ainda não salvo apenas em memória. --- ## 14. Dimensões de layout e áreas seguras Use as alturas nativas das barras junto com variáveis CSS de safe area para melhor compatibilidade entre dispositivos. ```js function updateInsets() { const status = Number(DtGetStatusBarHeight?.execute?.() || 0); const nav = Number(DtGetNavigationBarHeight?.execute?.() || 0); document.documentElement.style.setProperty('--status-bar-height', `${status}px`); document.documentElement.style.setProperty('--nav-bar-height', `${Math.min(nav, 48)}px`); } window.addEventListener('resize', updateInsets); document.addEventListener('DOMContentLoaded', updateInsets); ``` Exemplo CSS: ```css .header { padding-top: max(var(--status-bar-height, 0px), env(safe-area-inset-top)); } .footer { padding-bottom: max(12px, env(safe-area-inset-bottom)); } ``` --- ## 15. Campos do catálogo OnlineConfig relevantes para temas O endpoint de catálogo do painel é consumido pelo app e depois normalizado para o HTML. Autores de temas normalmente não chamam esse endpoint diretamente, mas conhecer a estrutura de origem ajuda a evitar UI quebrada. O catálogo de nível superior pode conter: ```json { "title": "My VPN", "selectedGroupId": "tim", "selectedProfileId": "tim_ssl", "appLogoUrl": "https://example.com/logo.png", "globalCustomLayoutHtml": "...", "groups": [] } ``` Grupos contêm perfis: ```json { "id": "tim", "name": "TIM", "color": "#0055ff", "sorter": 1, "profiles": [] } ``` Perfis contêm: ```json { "id": "tim_ssl", "name": "TIM SSL", "description": "SSL + payload", "methodName": "SSL", "mode": "SSL_PROXY", "icon": "https://example.com/tim.png", "sorter": 1, "auth": { "username": true, "password": true, "v2ray_uuid": false }, "urlCheckUser": "https://example.com/check?user={username}", "configUrl": "https://example.com/profile/tim_ssl", "configBase64": "...", "cachedSettingsJson": "..." } ``` O HTML recebe os perfis como `items`, não como `profiles`, ao chamar `DtGetConfigs.execute()`. --- ## 16. Campos de payload/settings do perfil que afetam o bridge Quando um perfil é selecionado, o app aplica um arquivo/base64 de config ou um payload JSON de settings. Os campos de settings abaixo importam para o que o bridge HTML vai reportar depois: | Campo settings | Aliases | Usado para | |---|---|---| | `mode` | `tunnelType` | Detecção nativa do tipo de túnel. | | `serverHost` | `sshServer`, `server_host` | Host SSH/servidor. | | `serverPort` | `sshPort`, `server_port` | Porta SSH/servidor. | | `username` | `sshUser`, `user` | Usuário SSH. Vazio significa que credencial manual pode ser necessária. | | `password` | `sshPass`, `pass` | Senha SSH. Vazio significa que credencial manual pode ser necessária. | | `xrayUuid` | `v2ray_uuid`, `uuid` | UUID V2Ray/Xray. Vazio significa que UUID manual pode ser necessário. | | `xrayConfig` | `config_v2ray`, `v2rayConfig` | Config JSON V2Ray/Xray. | | `payload` | `customPayload`, `proxyPayload` | Payload SSH customizado. | | `proxyHost` | `proxy_host` | Host do proxy. | | `proxyPort` | `proxy_port` | Porta do proxy. | | `sni` | `sslSni`, `customSni` | TLS/SNI. | | `slowNameServer` | `nameServer`, `ns`, `nameserver` | Nameserver SlowDNS. | | `slowDnsServer` | `dns`, `dnsServer`, `slowDns` | Servidor DNS SlowDNS. | | `slowDnsKey` | `key`, `slowKey` | Chave SlowDNS. | Valores atuais de tipo de túnel reconhecidos pelo parser de settings do app: | Valor | Significado | |---|---| | `1`, `SSH_DIRECT` | SSH direto. | | `2`, `SSH_PROXY` | SSH proxy. | | `3`, `SSL_DIRECT`, `SSH_SSL` | SSH SSL. | | `4`, `SLOW_DNS`, `DNSTT` | SlowDNS/DNSTT. | | `5`, `SSL_PROXY`, `SSH_SSL_PROXY` | SSH SSL proxy. | | `6`, `V2RAY`, `XRAY` | Xray/V2Ray. | Para perfis V2Ray com UUID, use `mode: "V2RAY"` ou `tunnelType: 6` no payload de settings. Evite enviar boolean `true` como `auth.v2ray_uuid` dentro do payload real de settings. Em settings, `v2ray_uuid` é tratado como alias de valor do UUID. No auth do catálogo, `auth.v2ray_uuid` é boolean. Mantenha os dois conceitos separados: Auth do perfil no catálogo: ```json "auth": { "v2ray_uuid": true } ``` Valor UUID no payload de settings: ```json "xrayUuid": "" ``` --- ## 17. Teste no navegador sem bridge Android Use mocks somente enquanto desenvolve no navegador. Como o app remove a seção mock depois do marcador `// --- AMBIENTE MOCK PARA TESTES ---`, coloque mocks no final do HTML abaixo desse marcador exato, ou use um arquivo local separado somente para teste. Mock mínimo para navegador: ```js if (!window.DtGetVpnState) { window.DtGetVpnState = { execute: () => 'DISCONNECTED' }; window.DtGetConfigs = { execute: () => JSON.stringify([]) }; window.DtGetDefaultConfig = { execute: () => '{}' }; window.DtSetConfig = { execute: (id) => console.log('select', id) }; window.DtUsername = { get: () => '', set: (v) => console.log('username', v) }; window.DtPassword = { get: () => '', set: (v) => console.log('password', v) }; window.DtUuid = { get: () => '', set: (v) => console.log('uuid', v) }; window.DtExecuteVpnStart = { execute: () => console.log('start') }; window.DtExecuteVpnStop = { execute: () => console.log('stop') }; window.DtGetLogs = { execute: () => JSON.stringify([]) }; window.DtGetLastConnectionError = { execute: () => '{}' }; window.DtGetConnectionError = window.DtGetLastConnectionError; window.DtGetStatusHotSpotService = { execute: () => 'STOPPED' }; window.DtStartHotSpotService = { execute: () => console.log('proxy start') }; window.DtStopHotSpotService = { execute: () => console.log('proxy stop') }; } ``` --- ## 18. Erros comuns em temas ### Erro: ler JSON sem `try/catch` Ruim: ```js const configs = JSON.parse(DtGetConfigs.execute()); ``` Bom: ```js let configs = []; try { configs = JSON.parse(DtGetConfigs.execute() || '[]'); } catch (e) {} ``` ### Erro: mostrar `Locked` nos inputs Ruim: ```js usernameInput.value = DtUsername.get(); ``` Bom: ```js usernameInput.value = cleanBridgeValue(DtUsername.get()); ``` ### Erro: não tratar erros classificados do bridge Ruim: ```js window.DtVpnStateListener = function(state) { if (state === 'AUTH_FAILED') alert('Falha'); }; ``` Bom: ```js window.dtConnectionErrorListener = function(error) { if (!error || !error.code) return; showError(error.message || error.code); }; ``` Use `DtGetLastConnectionError.execute()` para recuperar o último erro quando a tela for recriada ou quando o tema carregar depois de uma falha. ### Erro: salvar autenticação somente no conectar O usuário pode trocar de tela ou usar conexão automática. Salve também quando o input mudar: ```js usernameInput.addEventListener('input', () => DtUsername.set(usernameInput.value)); passwordInput.addEventListener('input', () => DtPassword.set(passwordInput.value)); uuidInput.addEventListener('input', () => DtUuid.set(uuidInput.value)); ``` ### Erro: depender do estado do clique em vez do estado nativo Sempre escute: ```js window.DtVpnStateListener = function(state) { renderVpnState(state); }; ``` ### Erro: usar objetos nativos sem verificar se existem Temas podem ser abertos fora do Android para preview. Sempre proteja chamadas do bridge. --- ## 19. Wrapper recomendado para APIs do tema Coloque isto perto do começo de todo layout customizado. ```js const OnlineBridge = { exec(name, fallback = '') { try { const obj = window[name]; if (obj && typeof obj.execute === 'function') return obj.execute(); } catch (e) {} return fallback; }, command(name, value) { try { const obj = window[name]; if (obj && typeof obj.execute === 'function') { if (arguments.length >= 2) obj.execute(String(value)); else obj.execute(); } } catch (e) {} }, get(name, fallback = '') { try { const obj = window[name]; if (obj && typeof obj.get === 'function') return obj.get(); } catch (e) {} return fallback; }, set(name, value) { try { const obj = window[name]; if (obj && typeof obj.set === 'function') obj.set(String(value ?? '')); } catch (e) {} }, jsonFromExec(name, fallback) { try { return JSON.parse(this.exec(name, '') || ''); } catch (e) { return fallback; } }, clean(value) { const text = String(value ?? '').trim(); return text.toLowerCase() === 'locked' ? '' : text; }, getConfigs() { return this.jsonFromExec('DtGetConfigs', []); }, getCurrentConfig() { return this.jsonFromExec('DtGetDefaultConfig', {}); }, selectConfig(id) { this.command('DtSetConfig', id); }, getState() { return this.exec('DtGetVpnState', 'DISCONNECTED'); }, start() { this.command('DtExecuteVpnStart'); }, stop() { this.command('DtExecuteVpnStop'); }, getAuth() { return { username: this.clean(this.get('DtUsername')), password: this.clean(this.get('DtPassword')), uuid: this.clean(this.get('DtUuid')) }; }, saveAuth({ username, password, uuid }) { this.set('DtUsername', username || ''); this.set('DtPassword', password || ''); this.set('DtUuid', uuid || ''); }, readLogs() { return this.jsonFromExec('DtGetLogs', []); }, getLastError() { return this.jsonFromExec('DtGetLastConnectionError', {}); }, getProxyStatus() { return this.exec('DtGetStatusHotSpotService', 'STOPPED'); }, startProxy() { this.command('DtStartHotSpotService'); }, stopProxy() { this.command('DtStopHotSpotService'); } }; ``` --- ## 20. Checklist mínimo para um novo tema HTML OnlineConfig Antes de publicar um tema, verifique: - Funciona quando `DtGetDefaultConfig.execute()` retorna `{}`. - Não mostra `Locked` para o usuário. - Consegue fazer parse seguro de JSON vazio ou inválido. - Escuta `DtVpnStateListener`. - Escuta `dtLogsUpdatedListener` se o tema mostra logs/proxy-status em tempo real. - Escuta `dtConnectionErrorListener` ou lê `DtGetLastConnectionError.execute()` para mostrar erro correto. - Chama `DtSetConfig.execute(id)` antes de iniciar um perfil selecionado. - Salva usuário/senha/UUID antes de conectar. - Trata `auth.username`, `auth.password` e `auth.v2ray_uuid`. - O campo UUID aparece para configs com `auth.v2ray_uuid: true`; se o catálogo usar `mode: "VMESS"` ou `"VLESS"`, o helper do tema trata esses nomes. - Se usar proxy local, confirma `DtGetStatusHotSpotService.execute()` depois de chamar start/stop. - Se usar proxy local em Android 13+, considera que a notificação pode depender da permissão `POST_NOTIFICATIONS`. - Não depende de `DtGetPingResult` retornar ping real. - Não coloca código de produção abaixo do marcador de mock. - Usa APIs nativas de altura das barras ou CSS safe areas para a navegação inferior não ficar cortada. --- ## 21. Resumo dos bridges novos do patch V2/V3/V4/V5 Esta versão do patch adiciona/atualiza estes pontos para temas HTML OnlineConfig: | Item | Bridge/callback | O que muda para o tema | |---|---|---| | Último erro de conexão | `DtGetLastConnectionError.execute()` | Tema consegue saber se foi login expirado, login inválido, limite, proxy bloqueado, erro 400/403/429/500 etc. | | Alias de erro | `DtGetConnectionError.execute()` | Mesmo retorno de `DtGetLastConnectionError.execute()`. | | Helper limpo | `CakeApp.errors.getLast()` | Wrapper `CakeApp` para buscar o último erro. | | Callback de erro | `dtConnectionErrorListener(error)` | Tema recebe o erro ao vivo como objeto JS. | | Callback de auth/compatibilidade | `dtAuthErrorListener(error)` | Alias compatível para temas que tratam erro de autenticação separado. | | Callback de logs | `dtLogsUpdatedListener(logsJson)` | Tema recebe logs recentes sempre que uma mensagem visível é adicionada. | | Status de proxy HTTP | `DtGetLogs.execute()` | Logs agora incluem `Proxy Status [HTTP_PROXY]` e `Proxy Status [SSH_DIRECT]`. | | SSH_DIRECT com payload/proxy | logs + erro classificado | Mesmo em SSH direto, o app tenta detectar resposta HTTP inicial do proxy sem quebrar banner SSH. | | Proxy local foreground | `DtStartHotSpotService.execute()` | Inicia `ProxyService` com notificação foreground em vez de ligar apenas o socket interno. | | Permissão de notificação | abertura do `OnlineConfigWebActivity` e start do proxy | Android 13+ pede `POST_NOTIFICATIONS`; se negar, o tema deve confirmar o status do proxy. | | Detecção UUID no tema | `DtGetDefaultConfig.execute()` + `DtUuid.get()` | O bridge expõe `auth.v2ray_uuid` e `Locked`; o tema decide a visibilidade, incluindo `v2ray`, `xray`, `vmess` e `vless` quando necessário. | Exemplo completo para tema tratar logs e erros: ```js function parseBridgeJson(raw, fallback) { try { return JSON.parse(raw || ''); } catch (e) { return fallback; } } window.dtLogsUpdatedListener = function(logsJson) { const logs = parseBridgeJson(logsJson, []); const text = logs.map(row => { const time = Object.keys(row)[0] || ''; return `${time} ${row[time] || ''}`; }).join('\n'); renderLogs(text); }; window.dtConnectionErrorListener = function(error) { if (!error || !error.code) return; const messageByCode = { LOGIN_EXPIRED: 'Seu login expirou. Renove seu acesso.', LOGIN_INVALID: 'Usuário ou senha inválidos.', LOGIN_LIMIT: 'Limite de conexões atingido.', PROXY_FORBIDDEN: 'Proxy/bughost bloqueou a conexão.', PROXY_BAD_REQUEST: 'Payload ou proxy inválido.', PROXY_RATE_LIMITED: 'Muitas tentativas no proxy.', PROXY_SERVER_ERROR: 'Erro no proxy/servidor remoto.', AUTH_FAILED: 'Falha de autenticação.' }; renderError(messageByCode[error.code] || error.message || 'Falha na conexão.'); }; function restoreLastErrorOnLoad() { const error = parseBridgeJson(DtGetLastConnectionError.execute(), {}); if (error.code) window.dtConnectionErrorListener(error); } ```