Make Connect button actually verify the API, not just TCP reachability
probe-url now accepts a type param (llm/stt/tts) and checks service- specific endpoints: LLM → /v1/models with data[] key, STT → /health then /v1/models, TTS → /health then /voices endpoints. Random websites and wrong services are now rejected. Connect passes the card's section type; success toast shows which endpoint responded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
838445ba07
commit
214f2c61cf
44
server.py
44
server.py
@ -3498,18 +3498,48 @@ async def restart_local_container(name: str):
|
||||
|
||||
|
||||
@app.get("/api/probe-url")
|
||||
async def probe_url(url: str):
|
||||
"""Server-side reachability check — avoids browser CORS restrictions."""
|
||||
async def probe_url(url: str, type: str = ""):
|
||||
"""Server-side API probe — checks service-specific endpoints and validates JSON responses."""
|
||||
base = url.rstrip("/")
|
||||
hdrs = {"User-Agent": "TTS-Voice-Creator/probe"}
|
||||
|
||||
# Ordered list of (path, json_key_that_must_exist_or_None)
|
||||
if type == "llm":
|
||||
checks = [("/v1/models", "data"), ("/api/tags", "models"), ("/api/version", None)]
|
||||
elif type == "stt":
|
||||
checks = [("/health", None), ("/v1/models", "data"), ("/v1/audio/transcriptions", None)]
|
||||
elif type == "tts":
|
||||
checks = [("/health", None), ("/v1/audio/voices", None), ("/speakers", None), ("/voices", None)]
|
||||
else:
|
||||
checks = [("", None)]
|
||||
|
||||
last_err = "No response"
|
||||
for path, json_key in checks:
|
||||
try:
|
||||
r = requests.get(url, timeout=5, allow_redirects=True,
|
||||
headers={"User-Agent": "TTS-Voice-Creator/probe"})
|
||||
return {"ok": True, "status": r.status_code}
|
||||
r = requests.get(base + path, timeout=5, headers=hdrs, allow_redirects=True)
|
||||
if r.status_code >= 500:
|
||||
last_err = f"HTTP {r.status_code} on {path or '/'}"
|
||||
continue
|
||||
if r.status_code >= 400 and path:
|
||||
continue # try next endpoint
|
||||
# If we expect a specific JSON key, verify it
|
||||
if json_key:
|
||||
try:
|
||||
data = r.json()
|
||||
if json_key not in data:
|
||||
last_err = f"Unexpected response from {path} (missing '{json_key}')"
|
||||
continue
|
||||
except Exception:
|
||||
last_err = f"{path} returned non-JSON (HTTP {r.status_code})"
|
||||
continue
|
||||
return {"ok": True, "status": r.status_code, "endpoint": path or "/"}
|
||||
except requests.exceptions.ConnectionError:
|
||||
return {"ok": False, "error": "Connection refused"}
|
||||
except requests.exceptions.Timeout:
|
||||
return {"ok": False, "error": "Timeout"}
|
||||
return {"ok": False, "error": "Timeout (5 s)"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
last_err = str(e)
|
||||
return {"ok": False, "error": last_err}
|
||||
|
||||
|
||||
@app.post("/api/tts/restart-flags/clear")
|
||||
|
||||
@ -7187,7 +7187,8 @@ function renderLocalContainers(containers) {
|
||||
btn.disabled = true; btn.textContent = 'Connecting…';
|
||||
try {
|
||||
if (window.probeUrl) {
|
||||
const d = await window.probeUrl(rawUrl);
|
||||
const type = card ? cardType(card) : '';
|
||||
const d = await window.probeUrl(rawUrl, type);
|
||||
if (d.ok) {
|
||||
btn.textContent = '✓ Connected'; btn.className = 'llm-local-ping dc-connect-btn ok';
|
||||
if (card) card.classList.add('llm-local-card-online');
|
||||
@ -7310,12 +7311,20 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl
|
||||
return raw.replace(/^(https?:\/\/)0\.0\.0\.0([\/:])/, '$1host.docker.internal$2');
|
||||
}
|
||||
|
||||
async function probeUrl(rawUrl) {
|
||||
async function probeUrl(rawUrl, type = '') {
|
||||
const url = normalizeProbeUrl(rawUrl);
|
||||
const r = await fetch('/api/probe-url?' + new URLSearchParams({ url }));
|
||||
const params = { url };
|
||||
if (type) params.type = type;
|
||||
const r = await fetch('/api/probe-url?' + new URLSearchParams(params));
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function cardType(card) {
|
||||
const page = card.closest('[data-page]');
|
||||
if (page) return page.dataset.page; // 'llm', 'stt', 'tts'
|
||||
return '';
|
||||
}
|
||||
|
||||
function applyCardState(card, key, connected, failed) {
|
||||
card.classList.toggle('llm-local-card-online', connected);
|
||||
card.classList.toggle('llm-local-card-offline', !connected && !!failed);
|
||||
@ -7353,9 +7362,14 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Connecting…';
|
||||
try {
|
||||
const d = await probeUrl(rawUrl);
|
||||
const type = cardType(card);
|
||||
const d = await probeUrl(rawUrl, type);
|
||||
applyCardState(card, key, d.ok, !d.ok);
|
||||
if (!d.ok) toast('Cannot reach ' + normalizeProbeUrl(rawUrl) + ': ' + (d.error || 'No response'), 'error');
|
||||
if (d.ok) {
|
||||
toast(`✓ ${type.toUpperCase() || 'Service'} reachable — ${d.endpoint}`, 'success');
|
||||
} else {
|
||||
toast('Cannot reach ' + normalizeProbeUrl(rawUrl) + ': ' + (d.error || 'No response'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
applyCardState(card, key, false, true);
|
||||
toast('Probe failed: ' + e.message, 'error');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user