1563 lines
51 KiB
Markdown
1563 lines
51 KiB
Markdown
## 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 `</head>` quando o HTML contém uma tag `head`. Se o HTML não tiver `</head>`, o bridge é colocado no começo do documento.
|
|
|
|
---
|
|
|
|
## 3. Esqueleto rápido de tema
|
|
|
|
```html
|
|
<!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`
|
|
|
|
```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_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:
|
|
|
|
```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_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:
|
|
|
|
```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
|
|
<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:
|
|
|
|
```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: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.1` ou qualquer primeira linha que comece com `HTTP/`
|
|
- `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 Unavailable` e `Gateway 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:
|
|
|
|
```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.
|
|
|
|
|
|
### 11.1 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:
|
|
|
|
```js
|
|
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:
|
|
|
|
```js
|
|
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:
|
|
|
|
```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": "<!doctype html>...",
|
|
"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/V6
|
|
|
|
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` 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_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);
|
|
}
|
|
```
|