feat: add STT support, Gemini TTS, and expand usage tracking

- Speech-to-Text: full pipeline with sttCore handler, /v1/audio/transcriptions
  endpoint, sttConfig for OpenAI, Gemini, Groq, Deepgram, AssemblyAI,
  HuggingFace, NVIDIA Parakeet; new 9router-stt skill
- Gemini TTS: add gemini provider with 30 prebuilt voices and TTS_PROVIDER_CONFIG
- Usage: implement GLM (intl/cn) and MiniMax (intl/cn) quota fetchers; refactor
  Gemini CLI usage to use retrieveUserQuota with per-model buckets
- Disabled models: lowdb-backed disabledModelsDb + /api/models/disabled route
- Header search: reusable Zustand store (headerSearchStore) wired into Header
- CLI tools: add Claude Cowork tool card and cowork-settings API
- Providers: introduce mediaPriority sorting in getProvidersByKind, add
  Kimi K2.6, reorder hermes, drop qwen STT kind
- UI: expand media-providers/[kind]/[id] page (+314), enhance OAuthModal,
  ModelSelectModal, ProviderTopology, ProxyPools, ProviderLimits
- Assets: refresh provider PNGs (alicode, byteplus, cloudflare-ai, nvidia,
  ollama, vertex, volcengine-ark) and add aws-polly, fal-ai, jina-ai, recraft,
  runwayml, stability-ai, topaz, black-forest-labs
This commit is contained in:
decolua
2026-05-05 10:32:59 +07:00
parent bfb7d42164
commit d4bc42e1f5
67 changed files with 2930 additions and 234 deletions

View File

@@ -119,41 +119,133 @@ let codexProxyServer = null;
let codexProxyTimeout = null;
const CODEX_PROXY_TIMEOUT_MS = 300000; // 5 minutes
const CODEX_PORT = 1455;
// Pending exchange sessions keyed by state — used by server-side exchange mode
const pendingExchanges = new Map();
/**
* Start a proxy server on Codex fixed port (1455) that redirects callback to the app port.
* Returns { success: true } if started, or { success: false } if port is busy.
* Register a pending exchange session for server-side mode.
* Modal client calls this before opening popup.
*/
export function registerCodexSession({ state, codeVerifier, redirectUri }) {
if (!state || !codeVerifier || !redirectUri) return false;
pendingExchanges.set(state, {
codeVerifier,
redirectUri,
status: "pending",
createdAt: Date.now(),
});
return true;
}
/**
* Read session status (modal polls this).
*/
export function getCodexSessionStatus(state) {
return pendingExchanges.get(state) || null;
}
/**
* Clear a session (called after modal consumes status).
*/
export function clearCodexSession(state) {
pendingExchanges.delete(state);
}
function renderCodexResultPage(success, message) {
const color = success ? "#22c55e" : "#ef4444";
const icon = success ? "✓" : "✗";
const title = success ? "Authentication Successful" : "Authentication Failed";
return `<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>${title}</title>
<style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#f5f5f5}.c{text-align:center;padding:2rem;background:#fff;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1)}.i{color:${color};font-size:3rem}h1{margin:1rem 0}p{color:#666}</style>
</head><body><div class="c"><div class="i">${icon}</div><h1>${title}</h1><p>${message}</p><p>Closing in <span id="cd">3</span>s...</p>
<script>let n=3;const c=document.getElementById("cd");const t=setInterval(()=>{n--;c.textContent=n;if(n<=0){clearInterval(t);window.close();}},1000);</script>
</div></body></html>`;
}
/**
* Start Codex proxy on fixed port 1455.
* Mode A (server-side): if any session was registered, proxy auto-exchanges + saves DB.
* Mode B (channel fallback): if no session, proxy 302 redirects to app port for legacy channel-based flow.
*/
export function startCodexProxy(appPort) {
return new Promise((resolve) => {
// Already running
if (codexProxyServer) {
resolve({ success: true });
return;
}
const CODEX_PORT = 1455;
const server = http.createServer((req, res) => {
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname === "/callback" || url.pathname === "/auth/callback") {
// Redirect to app port with all query params preserved
const redirectUrl = `http://localhost:${appPort}/callback${url.search}`;
res.writeHead(302, { Location: redirectUrl });
res.end();
// Auto-close after redirect
stopCodexProxy();
if (url.pathname !== "/callback" && url.pathname !== "/auth/callback") {
res.writeHead(404);
res.end("Not found");
return;
}
res.writeHead(404);
res.end("Not found");
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const errorParam = url.searchParams.get("error");
const session = state ? pendingExchanges.get(state) : null;
// Mode A: server-side exchange (session registered)
if (session) {
try {
if (errorParam) {
throw new Error(url.searchParams.get("error_description") || errorParam);
}
if (!code) throw new Error("No authorization code received");
// Lazy import to avoid circular deps
const { exchangeTokens } = await import("../providers.js");
const { createProviderConnection } = await import("@/models");
const tokenData = await exchangeTokens(
"codex",
code,
session.redirectUri,
session.codeVerifier,
state
);
const connection = await createProviderConnection({
provider: "codex",
authType: "oauth",
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
: null,
testStatus: "active",
});
session.status = "done";
session.connectionId = connection.id;
session.email = connection.email;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(true, "You can close this window."));
} catch (err) {
session.status = "error";
session.error = err.message;
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(renderCodexResultPage(false, err.message));
} finally {
stopCodexProxy();
}
return;
}
// Mode B: legacy channel fallback — 302 redirect to app /callback
const redirectUrl = `http://localhost:${appPort}/callback${url.search}`;
res.writeHead(302, { Location: redirectUrl });
res.end();
stopCodexProxy();
});
server.listen(CODEX_PORT, "127.0.0.1", () => {
codexProxyServer = server;
// Auto-cleanup after timeout
codexProxyTimeout = setTimeout(() => stopCodexProxy(), CODEX_PROXY_TIMEOUT_MS);
resolve({ success: true });
});