1. Modelo de nomes do bridge
O app expõe três estilos de bridge:
- Objetos nativos
Cake*. São os objetos reais registrados pelo Android comaddJavascriptInterface. - Aliases
Dt*. Existem para compatibilidade com temas já existentes. - 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:
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:
// --- 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 </head> quando o HTML contém uma tag head. Se o HTML não tiver </head>, o bridge é colocado no começo do documento.
2.1 Cada objeto nativo pode não existir
Nesta versão, todo objeto Cake* só é registrado no WebView se a camada nativa autorizar aquele nome (SecureOnlineNative.isBridgeAllowed). Um APK pode ser publicado com parte do bridge desativada.
Consequências práticas para o tema:
- Um objeto bloqueado fica
undefined, e o aliasDt*correspondente também, porque o alias é criado comowindow.DtX = window.DtX || window.CakeX. CakeApp.*é injetado sempre, mas os métodos dele chamam os objetos nativos diretamente. Se o objeto estiver bloqueado, a chamada lança erro.- Por isso, sempre use
try/catche checagem de tipo, como nos helpers da seção 1, tanto paraDt*quanto paraCakeApp.*.
3. Esqueleto rápido de tema
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Tema OnlineConfig</title>
</head>
<body>
<button id="serverButton">Selecionar servidor</button>
<div id="usernameBox" data-auth-field>
<input id="username" placeholder="Usuário">
</div>
<div id="passwordBox" data-auth-field>
<input id="password" type="password" placeholder="Senha">
</div>
<div id="uuidBox" data-auth-field style="display:none">
<input id="uuid" placeholder="UUID V2Ray">
</div>
<button id="connectButton">Conectar</button>
<div id="status">Desconectado</div>
<script>
const $ = (id) => document.getElementById(id);
function safeJson(text, fallback) {
try { return JSON.parse(text || ''); } catch (e) { return fallback; }
}
function getState() {
return window.DtGetVpnState?.execute?.() || 'DISCONNECTED';
}
function getCurrentConfig() {
return safeJson(window.DtGetDefaultConfig?.execute?.() || '{}', {});
}
function loadSavedAuth() {
const username = window.DtUsername?.get?.() || '';
const password = window.DtPassword?.get?.() || '';
const uuid = window.DtUuid?.get?.() || '';
if (username.toLowerCase() !== 'locked') $('username').value = username;
if (password.toLowerCase() !== 'locked') $('password').value = password;
if (uuid.toLowerCase() !== 'locked') $('uuid').value = uuid;
}
function saveAuth() {
window.DtUsername?.set?.($('username').value);
window.DtPassword?.set?.($('password').value);
window.DtUuid?.set?.($('uuid').value);
}
function updateStatus(state = getState()) {
$('status').textContent = state;
$('connectButton').textContent = state === 'CONNECTED' ? 'Desconectar' : 'Conectar';
}
$('connectButton').onclick = () => {
saveAuth();
if (getState() === 'CONNECTED') {
window.DtExecuteVpnStop?.execute?.();
} else {
window.DtExecuteVpnStart?.execute?.();
}
};
window.DtVpnStateListener = function(state) {
updateStatus(state);
};
document.addEventListener('DOMContentLoaded', () => {
loadSavedAuth();
updateStatus();
});
</script>
</body>
</html>
4. Helpers principais CakeApp
O app injeta esse objeto para facilitar o código dos temas.
CakeApp.vpn
CakeApp.vpn.getState(); // retorna string
CakeApp.vpn.start(); // inicia/dispara o comando da VPN
CakeApp.vpn.stop(); // para a VPN
Aliases equivalentes:
DtGetVpnState.execute();
DtExecuteVpnStart.execute();
DtExecuteVpnStop.execute();
CakeApp.configs
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:
DtGetConfigs.execute();
DtGetDefaultConfig.execute();
DtSetConfig.execute(id);
CakeApp.auth
CakeApp.auth.getUsername();
CakeApp.auth.setUsername(value);
CakeApp.auth.getPassword();
CakeApp.auth.setPassword(value);
CakeApp.auth.getUuid();
CakeApp.auth.setUuid(value);
Aliases equivalentes:
DtUsername.get();
DtUsername.set(value);
DtPassword.get();
DtPassword.set(value);
DtUuid.get();
DtUuid.set(value);
CakeApp.errors
CakeApp.errors.getLast(); // retorna string JSON com o último erro de conexão
Aliases equivalentes:
DtGetLastConnectionError.execute();
DtGetConnectionError.execute();
CakeApp.system
CakeApp.system.openUrl(url);
CakeApp.system.getStatusBarHeight();
CakeApp.system.getNavigationBarHeight();
Aliases equivalentes:
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 string vazia quando a config selecionada não aceita usuário manual. |
DtUsername.set(value) |
string | void | Sempre guarda o valor no cache manual do OnlineConfig. Só grava nas preferências do perfil se credenciais manuais forem aceitas. |
DtPassword.get() |
sem argumentos | string | Retorna a senha manual salva, ou string vazia quando não for aceita. |
DtPassword.set(value) |
string | void | Mesmo comportamento de DtUsername.set: cache sempre, gravação somente se aceito. |
DtUuid.get() |
sem argumentos | string | Retorna o UUID manual V2Ray/Xray salvo, ou string vazia quando a config selecionada não aceita UUID manual. |
DtUuid.set(value) |
string | void | Sempre guarda o UUID no cache manual. Só grava nas preferências se o perfil aceitar UUID manual. |
Importante nesta versão: o bridge não retorna mais o texto Locked. Quando o input manual não é aceito, get() retorna string vazia ("").
O que decide se o input manual é aceito:
| Getter/Setter | Aceito quando |
|---|---|
DtUsername, DtPassword |
O payload do perfil veio sem username/password (o app marca credenciais manuais como obrigatórias) ou o perfil do catálogo declara auth.username: true ou auth.password: true. |
DtUuid |
O perfil é Xray/V2Ray (tunnelType 6) sem UUID no payload ou o perfil do catálogo declara auth.v2ray_uuid: true. |
Como o cache manual é sempre gravado, DtUsername.set() / DtPassword.set() / DtUuid.set() chamados antes da seleção do servidor continuam valendo: quando o perfil for aplicado e pedir credencial manual, o app reaproveita o último valor em cache.
O helper de limpeza abaixo continua recomendado, porque trata string vazia, null e também o antigo Locked de temas migrados de outros apps:
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. |
Atenção: o estado nativo RECONECTANDO também é mapeado para CONNECTING. Nesta versão, a reconexão automática é silenciosa e mantém a VPN/TUN ativa, então CONNECTING depois de um CONNECTED não significa que o usuário perdeu a sessão nem que o tema deve limpar a tela. Veja a seção 23.
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.
function getLastConnectionError() {
try {
return JSON.parse(DtGetLastConnectionError.execute() || '{}');
} catch (e) {
return {};
}
}
Formato retornado:
{
"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_HTTP_101 |
HTTP 101, normalmente Switching Protocols/WebSocket aceito. |
Mostrar proxy/payload aceito e continuar aguardando autenticação/conexão. |
PROXY_HTTP_200, PROXY_HTTP_2xx |
HTTP 200 até 299, exceto 101. O código real entra no final, exemplo PROXY_HTTP_200. |
Mostrar que o proxy respondeu com sucesso. |
PROXY_REDIRECT_301, PROXY_REDIRECT_302, PROXY_REDIRECT_3xx |
HTTP 300 até 399. O código real entra no final, exemplo PROXY_REDIRECT_302. |
Mostrar que o bughost/proxy redirecionou a conexão. |
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/503/504, Bad Gateway, Service Unavailable, Gateway Timeout 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:
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_HTTP_101: 'Proxy aceitou WebSocket/101 Switching Protocols.',
PROXY_HTTP_200: 'Proxy respondeu 200 OK.',
PROXY_REDIRECT_302: 'Proxy/bughost redirecionou a conexão.',
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:
{username}
{user}
{password}
{uuid}
{hwid}
Exemplo de campo no perfil:
{
"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:
{
"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 já convertida para pixels CSS (dp). Use direto em px no CSS, sem dividir por devicePixelRatio. Retorna 0 se o Android não expor a dimensão. |
DtGetNavigationBarHeight.execute() |
sem argumentos | string numérica | Altura da navigation bar nativa em pixels CSS (dp), mesma regra acima. |
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:
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:
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:
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:
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.
window.DtVpnStateListener = function(state) {
updateStatusBadge(state);
};
window.dtVpnStoppedSuccessListener = function() {}
Chamado depois que uma parada foi solicitada e o estado mapeado vira DISCONNECTED.
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.
window.DtUpdatePayloadSuccessListener = function(message) {
reloadServerList();
};
window.DtUpdatePayloadErrorListener = function(message) {}
Chamado quando a atualização do catálogo falha.
window.DtUpdatePayloadErrorListener = function(message) {
alert(message || 'Falha ao atualizar');
};
window.dtCheckUserStartedListener = function() {}
Chamado antes da requisição nativa de check-user começar.
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.
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.
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:
window.DtConnectionErrorListener = function(error) {};
window.dtAuthErrorListener = function(error) {};
window.DtAuthErrorListener = function(error) {};
O argumento já chega como objeto JavaScript, não como string JSON.
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.
[
{
"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.
{
"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.
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.passwordeauth.v2ray_uuidvindos deDtGetDefaultConfig.execute(). Esta é a fonte principal nesta versão.modedo perfil, para perfis baseados em UUID (V2RAY,XRAY,VMESS,VLESS).- Os getters
DtUsername.get(),DtPassword.get()eDtUuid.get()apenas como fonte de valor inicial. Eles retornam string vazia quando o input não é aceito, e string vazia também é o retorno normal de um input aceito mas ainda não preenchido, então getter vazio não serve como sinal de "campo bloqueado".
HTML recomendado:
<div class="input-group" data-auth-field id="username-group">
<input id="username" name="username" placeholder="Usuário">
</div>
<div class="input-group" data-auth-field id="password-group">
<input id="password" name="password" type="password" placeholder="Senha">
</div>
<div class="input-group" data-auth-field id="uuid-group">
<input id="v2ray_uuid" name="v2ray_uuid" placeholder="UUID V2Ray">
</div>
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:
{ "mode": "V2RAY" }
{ "mode": "XRAY" }
{ "mode": "VMESS" }
{ "mode": "VLESS" }
{ "mode": "V2RAY - VMESS" }
Mesmo assim, o formato mais limpo para configs baseadas em UUID continua sendo:
{
"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.
Cuidado com XHTTP: não use nomes como XRAY XHTTP no mode do catálogo para perfis XHTTP + SSH, porque o helper abaixo detecta xray no texto e mostraria o campo UUID em um perfil que na verdade usa usuário/senha. Use XHTTP, SSH_XHTTP ou XHTTP_SSH. Veja a seção 22.
function isLocked(value) {
// Compatibilidade: esta versão retorna string vazia, versões/apps antigos retornavam 'Locked'.
const text = String(value ?? '').trim();
return text === '' || text.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();
if (mode.includes('xhttp')) return false; // XHTTP + SSH usa usuário/senha
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 || {};
// Decida pela config, não pelos getters: nesta versão eles retornam string
// vazia tanto para campo não aceito quanto para campo aceito e ainda vazio.
const uuidRequired =
auth.v2ray_uuid === true ||
auth.manual_v2ray_uuid === true ||
isUuidCatalogMode(config);
const sshRequired = !uuidRequired && (
auth.username === true ||
auth.manual_username === true ||
auth.password === true ||
auth.manual_password === true ||
// Fallback: perfil sem bloco auth mas com valor manual já salvo/cacheado.
!isLocked(bridgeGet('DtUsername')) ||
!isLocked(bridgeGet('DtPassword'))
);
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';
}
Se o perfil não tem bloco auth no catálogo e nada foi salvo ainda, nenhum dos dois sinais aparece. Nesse caso, prefira mostrar usuário/senha por padrão para modos SSH (SSH_DIRECT, SSH_PROXY, SSH_SSL, SSL_PROXY, DNSTT, XHTTP) em vez de esconder tudo. Um tema que esconde todos os inputs deixa o usuário sem como conectar.
10. Iniciando e parando a conexão
Padrão básico:
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:
[
{ "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:03:59": "Proxy Status [PROXY_SSL]: HTTP/1.1 101 Switching Protocols" },
{ "14:04:00": "Proxy Status [PROXY_SSL]: HTTP/1.1 302 Found" },
{ "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 [PROXY_SSL]: HTTP/1.1 101 ...Proxy Status [PROXY_SSL]: HTTP/1.1 200 ...Proxy Status [PROXY_SSL]: HTTP/1.1 302 ...Proxy Status [SSH_DIRECT]: HTTP/1.1 101 ...Proxy Status [SSH_DIRECT]: HTTP/1.1 302 ...HTTP/1.0,HTTP/1.1ou qualquer primeira linha que comece comHTTP/HTTP 100,101,200,301,302,400,401,403,404,407,429,500,502,503,504- mensagens como
Unauthorized,Proxy Authentication Required,Forbidden,Bad Request,Too Many Requests,Bad Gateway,Service UnavailableeGateway Timeout
A diferença importante é o PROXY_SSL: em Proxy + Payload SSL (SSL_PROXY / SSH_SSL_PROXY), o app agora registra a primeira linha HTTP real recebida do proxy com o formato Proxy Status [PROXY_SSL]: HTTP/.... Antes, esse caminho podia trocar 101 por 200 internamente ou lançar erro antes do WebView receber o status correto. Agora o tema consegue ver o código real, como 101, 200, 302, 407, 500, etc.
A outra diferença é o SSH_DIRECT: quando o campo de servidor SSH é usado como proxy/bughost e o servidor real está dentro do payload, o app 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:
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:
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.
11.1 Limites e o que não aparece em DtGetLogs
| Buffer | Tamanho nesta versão | Conteúdo |
|---|---|---|
Feed exposto ao tema (DtGetLogs.execute()) |
200 entradas mais recentes | Mudanças de estado, banner do servidor e linhas de proxy/HTTP. |
| Buffer nativo de log do serviço | 10.000 entradas (antes 1.000) | Log completo do túnel, visível em DtShowLoggerDialog.execute() e no logcat. |
O corte do buffer nativo também mudou: antes o app apagava um bloco grande de entradas de uma vez, o que parecia log truncado. Agora ele remove uma entrada por vez ao passar do limite.
Linhas que não entram no feed do tema, mesmo estando no log nativo:
- Mensagens de transporte XHTTP, como
Iniciando XHTTP (TLS)...,XHTTP proto=h2eXHTTP downlink stopped: .... VPN lost. Reconnecting... <motivo>e as mensagens de reconexão automática.UDP forward is off: only TCP and DNS are carried by the tunnel.Ignoring unsupported DNS resolver (IPv4 required): <valor>.
Para essas, o tema deve usar o estado (DtVpnStateListener) e o diálogo nativo (DtShowLoggerDialog.execute()). Se você precisa mostrar progresso de XHTTP na UI do tema, baseie-se em CONNECTING → AUTH → CONNECTED, não em texto de log.
11.2 Depurando um tema com logcat
Como a tela do app é um WebView, o log interno nem sempre é acessível durante o desenvolvimento. Nesta versão, toda entrada do log nativo também é espelhada no logcat com a tag VOIDPRO, já sem as tags HTML de formatação:
adb logcat -s VOIDPRO
O nível é mapeado assim: erro do túnel vira E, aviso vira W, debug/verbose vira D e o restante vira I. Isso é útil para conferir se a sua chamada de bridge realmente disparou a ação nativa que você esperava.
11.3 Como extrair o código HTTP do proxy no tema
Quando o app recebe uma linha HTTP/..., o classificador do bridge também atualiza DtGetLastConnectionError.execute() e dispara dtConnectionErrorListener(error) quando o status for relevante para o tema.
Exemplos de códigos normalizados:
| Linha recebida no log | Código normalizado esperado |
|---|---|
Proxy Status [PROXY_SSL]: HTTP/1.1 101 Switching Protocols |
PROXY_HTTP_101 |
Proxy Status [PROXY_SSL]: HTTP/1.1 200 OK |
PROXY_HTTP_200 |
Proxy Status [PROXY_SSL]: HTTP/1.1 302 Found |
PROXY_REDIRECT_302 |
Proxy Status [PROXY_SSL]: HTTP/1.1 400 Bad Request |
PROXY_BAD_REQUEST |
Proxy Status [PROXY_SSL]: HTTP/1.1 401 Unauthorized |
LOGIN_INVALID |
Proxy Status [PROXY_SSL]: HTTP/1.1 407 Proxy Authentication Required |
LOGIN_INVALID |
Proxy Status [PROXY_SSL]: HTTP/1.1 403 Forbidden |
PROXY_FORBIDDEN |
Proxy Status [PROXY_SSL]: HTTP/1.1 429 Too Many Requests |
PROXY_RATE_LIMITED |
Proxy Status [PROXY_SSL]: HTTP/1.1 500 Internal Server Error |
PROXY_SERVER_ERROR |
Proxy Status [PROXY_SSL]: HTTP/1.1 502 Bad Gateway |
PROXY_SERVER_ERROR |
Proxy Status [PROXY_SSL]: HTTP/1.1 503 Service Unavailable |
PROXY_SERVER_ERROR |
Proxy Status [PROXY_SSL]: HTTP/1.1 504 Gateway Timeout |
PROXY_SERVER_ERROR |
Helper simples para pegar o último status HTTP dos logs:
function getLatestProxyHttpStatus() {
let rows = [];
try { rows = JSON.parse(DtGetLogs.execute() || '[]'); } catch (e) {}
for (let i = rows.length - 1; i >= 0; i--) {
const message = String(Object.values(rows[i] || {})[0] || '');
const match = message.match(/Proxy Status \[([^\]]+)\]:\s*(HTTP\/\S+\s+(\d{3})[^<]*)/i);
if (match) {
return {
source: match[1],
line: match[2].trim(),
code: Number(match[3])
};
}
}
return null;
}
Exemplo de UI para Proxy + Payload SSL:
window.dtLogsUpdatedListener = function(logsJson) {
const status = getLatestProxyHttpStatus();
if (!status) return;
if (status.source === 'PROXY_SSL') {
renderProxyStatus(`Proxy SSL respondeu ${status.code}`);
} else if (status.source === 'HTTP_PROXY') {
renderProxyStatus(`Proxy HTTP respondeu ${status.code}`);
} else if (status.source === 'SSH_DIRECT') {
renderProxyStatus(`SSH_DIRECT/proxy respondeu ${status.code}`);
}
};
12. Modal de perfil / check-user
Fluxo recomendado de perfil:
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:
{
"username": "utest678",
"count_connections": 1,
"expiration_date": "25/04/2026",
"expiration_days": 0,
"limit_connections": 1
}
13. Botão de atualizar configs
Use:
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.
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:
.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:
{
"title": "My VPN",
"selectedGroupId": "tim",
"selectedProfileId": "tim_ssl",
"appLogoUrl": "https://example.com/logo.png",
"globalCustomLayoutHtml": "<!doctype html>...",
"groups": []
}
Grupos contêm perfis:
{
"id": "tim",
"name": "TIM",
"color": "#0055ff",
"sorter": 1,
"profiles": []
}
Perfis contêm:
{
"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().
O catálogo também aceita um formato sem grupos. Se o JSON não tiver groups mas tiver profiles na raiz, o app cria automaticamente um grupo único com id: "default" e nome vindo de groupName. O tema continua recebendo grupos normalmente em DtGetConfigs.execute().
{
"title": "My VPN",
"groupName": "Servidores",
"profiles": []
}
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.
O app reconhece um JSON como payload de settings quando ele contém pelo menos uma destas chaves: serverHost, server_host, sshServer, mode, tunnelType, config_v2ray, xrayConfig, xhttpPath, xhttp_path, xhttp ou payload. Um payload XHTTP que só traga xhttp: { ... } já é reconhecido.
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. |
slowDnsMode |
dnsMode |
Modo SlowDNS. Padrão udp quando ausente. |
localPort |
sshLocalPort, local_port |
Porta SOCKS local. Padrão 1080 quando ausente. |
useDefaultPayload |
defaultPayload, use_default_payload |
Usar payload padrão. Padrão true. |
tlsMode |
tlsVersion, tls12 |
Modo/versão TLS. |
publicKey |
pubKey, customPubkey |
Chave pública customizada. |
xhttpPath |
xhttp_path, xhttp.path |
Path XHTTP. Padrão /xhttp quando ausente. |
xhttpHost |
xhttp_host, xhttp.host |
Host/:authority HTTP do XHTTP. |
xhttpTls |
xhttp_tls, xhttp.tls |
TLS do XHTTP. Padrão true. |
Campos de VPN/DNS/UDP também aceitos no payload de settings:
| Campo settings | Aliases | Padrão | Usado para |
|---|---|---|---|
dnsForward |
vpn.dnsForward |
true |
Encaminhar DNS pelo túnel. |
dnsResolver |
dns1, vpn.dnsResolver |
8.8.8.8 |
Resolver DNS primário. |
dnsResolverSecondary |
dns2, vpn.dnsResolverSecondary |
8.8.4.4 |
Resolver DNS secundário. |
udpForward |
vpn.udpForward |
true |
Encaminhar UDP pelo túnel. Com false, o túnel carrega só TCP e DNS. |
udpResolver |
udpResolverHost, vpn.udpResolver |
127.0.0.1:7300 |
Endpoint do resolvedor UDP. |
disableIpv6Tunnel |
ipv6Disabled, vpn.disableIpv6Tunnel |
true |
Desativar IPv6 no TUN. |
Os resolvers precisam ser IPv4. Um valor IPv6 é ignorado e o app registra Ignoring unsupported DNS resolver (IPv4 required): <valor> no log nativo.
O payload de settings aceita objetos aninhados, que são achatados para os campos acima. O valor de raiz tem prioridade sobre o aninhado:
| Objeto aninhado | Mapeia para |
|---|---|
server: { host, port } |
serverHost, serverPort |
auth: { username, password, v2ray_uuid, uuid } |
username, password, xrayUuid |
proxy: { host, port } |
proxyHost, proxyPort |
xhttp: { path, host, tls } |
xhttpPath, xhttpHost, xhttpTls |
slowDns: { ns, nameserver, dns, dnsServer, key, dnsMode } |
campos SlowDNS |
vpn: { dnsForward, dnsResolver, dnsResolverSecondary, udpForward, udpResolver, disableIpv6Tunnel } |
campos de VPN/DNS/UDP |
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. |
7, XHTTP, SSH_XHTTP, XHTTP_SSH |
XHTTP + SSH. Novo nesta versão, veja a seção 22. |
Qualquer outro valor cai em SSH_DIRECT (1). Um mode escrito errado no painel não gera erro: o perfil simplesmente conecta como SSH direto.
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:
"auth": { "v2ray_uuid": true }
Valor UUID no payload de settings:
"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:
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:
const configs = JSON.parse(DtGetConfigs.execute());
Bom:
let configs = [];
try { configs = JSON.parse(DtGetConfigs.execute() || '[]'); } catch (e) {}
Erro: jogar o retorno cru dos getters nos inputs
Ruim:
usernameInput.value = DtUsername.get();
Bom:
usernameInput.value = cleanBridgeValue(DtUsername.get());
cleanBridgeValue cobre os três casos: null, string vazia desta versão e o antigo Locked de temas migrados.
Erro: usar getter vazio como "campo bloqueado"
Nesta versão, DtUsername.get(), DtPassword.get() e DtUuid.get() retornam string vazia tanto quando o input não é aceito quanto quando ele é aceito e ainda está vazio.
Ruim:
if (!DtUuid.get()) hideUuidField();
Bom:
const current = OnlineBridge.getCurrentConfig();
if (!(current.auth?.v2ray_uuid || isUuidCatalogMode(current))) hideUuidField();
Erro: não tratar erros classificados do bridge
Ruim:
window.DtVpnStateListener = function(state) {
if (state === 'AUTH_FAILED') alert('Falha');
};
Bom:
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:
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:
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.
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{}. - Funciona quando um objeto do bridge não existe, porque o APK pode ter aquele nome desativado.
- Não mostra
Lockednem string vazia crua para o usuário. - Consegue fazer parse seguro de JSON vazio ou inválido.
- Escuta
DtVpnStateListener. - Escuta
dtLogsUpdatedListenerse o tema mostra logs/proxy-status em tempo real. - Escuta
dtConnectionErrorListenerou 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.passwordeauth.v2ray_uuid. - O campo UUID aparece para configs com
auth.v2ray_uuid: true; se o catálogo usarmode: "VMESS"ou"VLESS", o helper do tema trata esses nomes. - Perfis XHTTP mostram usuário/senha, não UUID.
- Não trata
CONNECTINGdepois deCONNECTEDcomo sessão perdida, porque a reconexão automática é silenciosa. - Não depende de texto de log para mostrar progresso de XHTTP.
- 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
DtGetPingResultretornar 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/V6/V7
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 incluem Proxy Status [HTTP_PROXY], Proxy Status [PROXY_SSL] e Proxy Status [SSH_DIRECT]. |
| Proxy + Payload SSL | logs + erro classificado | Em SSL_PROXY / SSH_SSL_PROXY, o app preserva o status real do proxy, incluindo 101, 200, 302, 407, 500, etc., usando a origem PROXY_SSL. |
| Códigos HTTP normalizados | DtGetLastConnectionError.execute() + dtConnectionErrorListener(error) |
Status 101 vira PROXY_HTTP_101, status 2xx vira PROXY_HTTP_<codigo> e redirects 3xx viram PROXY_REDIRECT_<codigo>. |
| 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; o tema decide a visibilidade, incluindo v2ray, xray, vmess e vless quando necessário. |
Novidades específicas da V7:
| Item | Bridge/callback | O que muda para o tema |
|---|---|---|
| Modo XHTTP + SSH | DtGetConfigs.execute() / DtGetDefaultConfig.execute() (mode) |
Novo tunnelType 7. O catálogo pode trazer mode: "XHTTP", "SSH_XHTTP" ou "XHTTP_SSH". Pede usuário/senha, nunca UUID. Veja a seção 22. |
| Campos XHTTP no payload | payload de settings | xhttpPath (padrão /xhttp), xhttpHost, xhttpTls (padrão ligado), além do objeto xhttp: { path, host, tls }. |
| Reconexão silenciosa | DtVpnStateListener |
Reconexão automática mantém VPN/TUN/porta SOCKS local ativas e reporta CONNECTING. O tema não deve limpar a tela nem tratar como desconexão. Veja a seção 23. |
| Trava de sessão | nenhum bridge novo | Servidor, porta, SNI, Host, Path e TLS do XHTTP são congelados no início da sessão. Mudança de perfil durante o túnel ativo não afeta a reconexão. |
dtVpnStoppedSuccessListener |
callback | Continua disparando só quando a parada foi pedida pelo tema, nunca em reconexão automática. |
Getters de auth sem Locked |
DtUsername.get(), DtPassword.get(), DtUuid.get() |
Retornam string vazia quando o input manual não é aceito. A visibilidade deve vir de auth.* e do mode. |
| Alturas de barra em px CSS | DtGetStatusBarHeight, DtGetNavigationBarHeight |
Já vêm divididas pela densidade da tela. Use direto em px no CSS. |
| Campos de UDP/DNS no payload | payload de settings | udpForward, udpResolver, dnsForward, dnsResolver, dnsResolverSecondary, disableIpv6Tunnel, com o objeto vpn: { ... }. |
| Log maior + logcat | DtGetLogs.execute(), DtShowLoggerDialog.execute() |
Feed do tema com as 200 entradas recentes, buffer nativo com 10.000, e todo o log espelhado no logcat com a tag VOIDPRO. |
| Bridge opcional por APK | todos os objetos Cake*/Dt* |
Um objeto pode não ser registrado. Sempre proteja as chamadas. Veja a seção 2.1. |
Exemplo completo para tema tratar logs e erros:
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_HTTP_101: 'Proxy aceitou 101 Switching Protocols.',
PROXY_HTTP_200: 'Proxy respondeu 200 OK.',
PROXY_REDIRECT_302: 'Proxy/bughost redirecionou a conexão.',
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);
}
22. Modo XHTTP + SSH (novo)
O app passou a suportar um transporte XHTTP (SplitHTTP) sobre HTTP/2 para o túnel SSH. Ele é o tunnelType 7 e aparece na tela nativa com o rótulo XHTTP + SSH.
Para o tema, XHTTP é um modo SSH: usa usuário e senha, e não usa UUID.
22.1 Como o painel declara um perfil XHTTP
No perfil do catálogo, use um destes valores de mode:
{ "mode": "XHTTP" }
{ "mode": "SSH_XHTTP" }
{ "mode": "XHTTP_SSH" }
{ "tunnelType": 7 }
E declare o auth normalmente:
{
"id": "xhttp_1",
"name": "Servidor XHTTP",
"description": "XHTTP + SSH via CDN",
"mode": "XHTTP",
"methodName": "XHTTP",
"auth": {
"username": true,
"password": true,
"v2ray_uuid": false
}
}
Não escreva mode misturando nomes de Xray, como XRAY XHTTP. Helpers de tema que procuram xray/v2ray no texto do mode mostrariam o campo UUID em um perfil que precisa de usuário/senha.
22.2 Campos de payload usados pelo XHTTP
| Campo | Aliases | Padrão | Significado |
|---|---|---|---|
serverHost |
sshServer, server_host |
— | IP/host que o app realmente conecta. |
serverPort |
sshPort, server_port |
— | Porta do servidor XHTTP. |
xhttpPath |
xhttp_path, xhttp.path |
/xhttp |
Path do inbound XHTTP. |
xhttpHost |
xhttp_host, xhttp.host |
vazio | Cabeçalho Host/:authority HTTP. Normalmente a borda da CDN. |
xhttpTls |
xhttp_tls, xhttp.tls |
true |
TLS ligado. Envie false para XHTTP em texto claro. |
sni |
sslSni, customSni |
vazio | SNI TLS. Com TLS ligado, é também o host da URL, o que permite domain fronting com bug host. |
Exemplo de payload de settings:
{
"mode": "XHTTP",
"serverHost": "203.0.113.10",
"serverPort": 443,
"username": "",
"password": "",
"sni": "bughost.operadora.com.br",
"xhttp": {
"path": "/xhttp",
"host": "edge.cdn.exemplo.com",
"tls": true
}
}
Com username e password vazios, o app marca credenciais manuais como obrigatórias, então o tema deve mostrar os inputs de usuário/senha.
22.3 O que o tema vê durante uma conexão XHTTP
O transporte XHTTP não publica linhas no feed de logs do tema. O que o tema recebe é a sequência normal de estados:
CONNECTING -> AUTH -> CONNECTED
As mensagens de diagnóstico do XHTTP ficam no log nativo e no logcat (adb logcat -s VOIDPRO):
| Mensagem | Significado |
|---|---|
Iniciando XHTTP (TLS)... / Iniciando XHTTP (sem TLS)... |
Abertura da sessão XHTTP. |
XHTTP proto=h2 |
Protocolo negociado. http/1.1 aqui indica que a CDN não passou HTTP/2 e o handshake tende a travar. |
XHTTP download rejected: HTTP <código> |
O servidor/CDN recusou o downlink. |
XHTTP downlink stopped: <motivo> |
O stream de download terminou. |
XHTTP uplink stopped: <motivo> |
O canal de upload terminou. |
Limitação conhecida nesta versão: essas mensagens de rejeição do XHTTP usam o formato HTTP <código>, sem o prefixo HTTP/1.1, então o classificador de erro não as converte em códigos PROXY_*. Uma recusa de XHTTP normalmente chega ao tema como falha de conexão genérica, sem code específico em DtGetLastConnectionError.execute(). Se o tema precisa distinguir esse caso, oriente o usuário pelo estado e pelo diálogo nativo de logs.
22.4 Configuração travada durante o túnel
Servidor, porta, SNI, Host, Path e TLS do XHTTP são fotografados no início da sessão do serviço. Enquanto o túnel estiver ativo:
- Trocar de perfil com
DtSetConfig.execute(id)altera a seleção salva, mas não muda o transporte da sessão em andamento nem o que a reconexão automática vai usar. - Para aplicar um perfil novo de verdade, pare a VPN (
DtExecuteVpnStop.execute()), espereDISCONNECTEDe só então inicie de novo.
Padrão recomendado no tema:
function applyProfileAndReconnect(profileId) {
const state = OnlineBridge.getState();
OnlineBridge.selectConfig(profileId);
if (state === 'DISCONNECTED') {
refreshCurrentServer();
return;
}
window.dtVpnStoppedSuccessListener = function() {
window.dtVpnStoppedSuccessListener = null;
refreshCurrentServer();
OnlineBridge.start();
};
OnlineBridge.stop();
}
23. Reconexão silenciosa
Quando a conexão SSH/XHTTP cai sozinha, o app não derruba mais a VPN. Ele mantém ativos o VpnService, a interface TUN, as rotas, o processo tun2socks e a porta SOCKS local, e substitui apenas a conexão que falhou.
O que isso significa para o tema:
| Situação | Estado reportado | O que o tema deve fazer |
|---|---|---|
| Queda e reconexão automática | CONNECTING (estado nativo RECONECTANDO) |
Mostrar "reconectando" e manter a tela conectada. Não limpar dados, não pedir login de novo. |
| Rede indisponível durante a reconexão | NO_NETWORK |
Mostrar aviso de rede. A sessão pode voltar sozinha quando a rede retornar. |
| Reconexão concluída | CONNECTED |
Voltar ao estado conectado normal. |
| Usuário parou pelo tema | DISCONNECTING e depois DISCONNECTED, mais dtVpnStoppedSuccessListener() |
Aí sim resetar a UI. |
Regras práticas:
dtVpnStoppedSuccessListenersó dispara quando a parada foi pedida pelo bridge. Use esse callback, e nãoDISCONNECTEDpuro, como sinal de "o usuário desconectou".- Não use
CONNECTINGcomo gatilho para zerar contador de tempo, limpar log ou reabrir a tela de login. Guarde o estado anterior e trate a transiçãoCONNECTED -> CONNECTINGcomo reconexão. - Notificação, vibração e som ficam suprimidos durante a reconexão automática. O tema também não deve emitir alerta a cada tentativa.
- Callbacks atrasados de conexões antigas são descartados pelo app, então o tema não recebe eventos duplicados de uma sessão já substituída.
Exemplo de tratamento:
let lastState = 'DISCONNECTED';
window.DtVpnStateListener = function(state) {
const wasOnline = lastState === 'CONNECTED' || lastState === 'AUTH';
const reconnecting = wasOnline && state === 'CONNECTING';
if (reconnecting) {
renderStatus('Reconectando...');
} else {
renderVpnState(state);
}
if (state === 'CONNECTED') {
renderStatus('Conectado');
}
lastState = state;
};
window.dtVpnStoppedSuccessListener = function() {
lastState = 'DISCONNECTED';
resetUi();
};
24. UDP, DNS e IPv6
O payload do perfil controla o que o túnel carrega. Os padrões, quando o campo não vem no payload:
| Campo | Padrão |
|---|---|
dnsForward |
true |
dnsResolver |
8.8.8.8 |
dnsResolverSecondary |
8.8.4.4 |
udpForward |
true |
udpResolver |
127.0.0.1:7300 |
disableIpv6Tunnel |
true |
Pontos que aparecem em suporte de tema:
- Com
udpForward: false, o túnel carrega só TCP e DNS. Jogos e chamadas de voz que dependem de UDP não funcionam nesse perfil, e o log nativo registraUDP forward is off: only TCP and DNS are carried by the tunnel. - Os resolvers precisam ser IPv4. Um endereço IPv6 é ignorado, com a linha
Ignoring unsupported DNS resolver (IPv4 required): <valor>no log nativo. - O tema não tem bridge para ler ou alterar esses campos. Eles vêm do painel. Se o tema mostra "UDP ativo", isso precisa vir de informação do seu próprio painel, não do bridge.