# v0.4.55 (2026-05-18)

## Features
- Xiaomi MiMo Token Plan: region selector (Singapore / China / Europe) — keys are cluster-specific
- Antigravity: risk confirmation dialog before first connection
- Gemini CLI: surface upstream retry delay on 429 errors

## Fixes
- MITM: cannot kill process on macOS under sudo (lsof not found in PATH)
- Stream: false-positive stall timeout on Claude reasoning / Kiro responses
- Tunnel: cannot re-enable after disable (stuck state)
- Tunnel: cloudflared error messages now include log tail for easier debugging
- Language switcher: applies selected locale immediately on close (#1234)
- Antigravity OAuth: metadata now matches the official client

## Improvements
- Gemini CLI: bump engine to 0.34.0
- Re-hide `qwen` (OAuth EOL) and `iflow` (not ready) providers
This commit is contained in:
decolua
2026-05-18 16:26:35 +07:00
parent 90a47c3f29
commit 613a0a819a
12 changed files with 125 additions and 47 deletions

View File

@@ -305,6 +305,8 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
return new Promise((resolve, reject) => {
let resolved = false;
// Keep a small tail of raw cloudflared logs to surface real failure causes
let logTail = "";
function getQuickTunnelUrlFromLog(message) {
// cloudflared logs may contain "api.trycloudflare.com" as well,
@@ -326,13 +328,14 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
if (resolved) return;
resolved = true;
cleanup();
reject(new Error("Quick tunnel timed out"));
reject(new Error(`Quick tunnel timed out. Last log: ${logTail.slice(-800) || "(empty)"}`));
}, 90000);
let lastUrl = null;
const handleLog = (data) => {
const msg = data.toString();
logTail = (logTail + msg).slice(-4000);
const tunnelUrl = getQuickTunnelUrlFromLog(msg);
if (!tunnelUrl) return;
@@ -370,17 +373,18 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
cloudflaredProcess = null;
clearPid();
console.log(`[Tunnel] cloudflared exit code=${code} signal=${signal}`);
if (logTail) console.log(`[Tunnel] cloudflared log tail:\n${logTail.slice(-1500)}`);
if (!resolved) {
resolved = true;
clearTimeout(timeout);
cleanup();
// Provide more helpful error messages for common exit codes
const tail = logTail.slice(-600).trim() || "(empty)";
if (code === 1) {
reject(new Error(`cloudflared exited with code ${code}. This often means: (1) the tunnel token is invalid or expired, (2) network connectivity issues, or (3) cloudflared cannot reach the local server.`));
reject(new Error(`cloudflared quick tunnel exited (code 1). Common causes: (1) outbound port 7844 (TCP/UDP) blocked, (2) TryCloudflare service issue, (3) cannot reach 127.0.0.1:${localPort}, (4) protocol (http2/quic) blocked by network. Last log: ${tail}`));
} else if (code === 2) {
reject(new Error(`cloudflared exited with code ${code}. Check that arguments are correct.`));
reject(new Error(`cloudflared exited (code 2). Bad arguments. Last log: ${tail}`));
} else {
reject(new Error(`cloudflared exited with code ${code}`));
reject(new Error(`cloudflared exited (code ${code}). Last log: ${tail}`));
}
return;
}

View File

@@ -1,5 +1,5 @@
import crypto from "crypto";
import { loadState, saveState, generateShortId } from "./state.js";
import { loadState, saveState, generateShortId, clearPid } from "./state.js";
import { spawnQuickTunnel, killCloudflared, isCloudflaredRunning, setUnexpectedExitHandler } from "./cloudflared.js";
import { startFunnel, stopFunnel, isTailscaleRunning, isTailscaleRunningStrict, isTailscaleLoggedIn, startLogin, startDaemonWithPassword, provisionCert } from "./tailscale.js";
import { getSettings, updateSettings } from "@/lib/localDb";
@@ -127,12 +127,15 @@ export async function enableTunnel(localPort = 20128) {
await updateSettings({ tunnelEnabled: true, tunnelUrl });
console.log(`[Tunnel] registered shortId=${shortId} publicUrl=${publicUrl}`);
// Verify direct tunnel URL first (avoid CDN false positive on publicUrl)
await waitForHealth(tunnelUrl, token);
console.log("[Tunnel] direct URL healthy");
// Then verify public URL (DNS propagated through worker)
// Verify publicUrl first (worker route is reliable; direct *.trycloudflare.com DNS may lag)
await waitForHealth(publicUrl, token);
console.log("[Tunnel] public URL healthy");
// Direct tunnel probe is best-effort: DNS for *.trycloudflare.com can be slow/blocked on some networks
if (!(await probeUrlAlive(tunnelUrl))) {
console.warn("[Tunnel] direct URL not reachable yet, continuing via publicUrl");
} else {
console.log("[Tunnel] direct URL healthy");
}
// Prime reachable cache so UI shows correct state immediately
tunnelReachable.value = true;
@@ -151,15 +154,21 @@ export async function enableTunnel(localPort = 20128) {
export async function disableTunnel() {
console.log("[Tunnel] disable");
// Abort any in-flight enable so it cannot resurrect state after we clear it
tunnelSvc.cancelToken.cancelled = true;
setUnexpectedExitHandler(null);
killCloudflared(tunnelSvc.activeLocalPort);
try { killCloudflared(tunnelSvc.activeLocalPort); } catch (e) { console.warn(`[Tunnel] kill warn: ${e.message}`); }
clearPid();
const state = loadState();
if (state) saveState({ shortId: state.shortId, machineId: state.machineId, tunnelUrl: null });
await updateSettings({ tunnelEnabled: false, tunnelUrl: "" });
tunnelReachable.value = false; tunnelReachable.url = null; tunnelReachable.fetchedAt = Date.now();
// Force-clear flags so a subsequent enable is not blocked by a stuck spawnInProgress
tunnelSvc.spawnInProgress = false;
tunnelSvc.activeLocalPort = null;
return { success: true };
}