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

@ -4,7 +4,32 @@ const path = require("path");
const os = require("os");
const { log, err } = require("../logger");
const { TOOL_HOSTS } = require("../../shared/constants/mitmToolHosts");
const { runElevatedPowerShell, quotePs, isAdmin } = require("../winElevated.js");
const { runElevatedPowerShell, isAdmin } = require("../winElevated.js");
/**
* Atomic-ish write for Windows hosts file with rollback on failure.
* Strategy: write `.new` sibling rename current to `.bak` rename `.new` to target.
* If anything fails mid-way, restore from `.bak`. Same-volume renames are atomic on NTFS.
*/
function atomicWriteHostsWin(target, originalContent, newContent) {
const tmpNew = `${target}.9router.new`;
const tmpBak = `${target}.9router.bak`;
try {
fs.writeFileSync(tmpNew, newContent, "utf8");
try { fs.unlinkSync(tmpBak); } catch { /* none */ }
fs.renameSync(target, tmpBak);
try {
fs.renameSync(tmpNew, target);
} catch (e) {
// Rollback: restore original
try { fs.renameSync(tmpBak, target); } catch { fs.writeFileSync(target, originalContent, "utf8"); }
throw e;
}
try { fs.unlinkSync(tmpBak); } catch { /* best effort */ }
} finally {
try { fs.unlinkSync(tmpNew); } catch { /* already moved or never created */ }
}
}
const IS_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
@ -130,16 +155,13 @@ async function addDNSEntry(tool, sudoPassword) {
try {
if (IS_WIN) {
// Read → trim → append → write (avoids stacked blank lines from Add-Content)
// Read → trim → append → atomic write (Node-side, no CLI size limit)
const current = fs.readFileSync(HOSTS_FILE, "utf8");
const trimmed = current.replace(/[\r\n\s]+$/g, "");
const toAppend = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("\r\n");
const next = `${trimmed}\r\n${toAppend}\r\n`;
const script = `
Set-Content -LiteralPath ${quotePs(HOSTS_FILE)} -Value ${quotePs(next)} -NoNewline
ipconfig /flushdns | Out-Null
`;
await runElevatedPowerShell(script);
atomicWriteHostsWin(HOSTS_FILE, current, next);
await runElevatedPowerShell("ipconfig /flushdns | Out-Null");
} else {
const current = fs.readFileSync(HOSTS_FILE, "utf8");
const trimmed = current.replace(/[\r\n\s]+$/g, "");
@ -175,11 +197,8 @@ async function removeDNSEntry(tool, sudoPassword) {
const current = fs.readFileSync(HOSTS_FILE, "utf8");
const filtered = current.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\r\n");
const next = filtered.replace(/[\r\n\s]+$/g, "") + "\r\n";
const script = `
Set-Content -LiteralPath ${quotePs(HOSTS_FILE)} -Value ${quotePs(next)} -NoNewline
ipconfig /flushdns | Out-Null
`;
await runElevatedPowerShell(script);
atomicWriteHostsWin(HOSTS_FILE, current, next);
await runElevatedPowerShell("ipconfig /flushdns | Out-Null");
} else {
const current = fs.readFileSync(HOSTS_FILE, "utf8");
const filtered = current.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\n");