mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-04-26 13:31:00 +00:00
fix(mitm): harden packaged runtime and privileged command execution
Compile MITM utilities as NodeNext ESM for prepublish builds, copy the CommonJS MITM server into standalone artifacts, and resolve MITM data paths without relying on Next.js aliases at runtime. Replace shell-interpolated setup and elevated command flows with argument-based spawn and execFile helpers for database setup, DNS edits, certificate install flows, and Tailscale sudo execution. Also tighten the Electron production CSP, update provider icon loading to use direct @lobehub/icons imports with local fallbacks, and pin dependency configuration to avoid unused peer installs and known audit findings.
This commit is contained in:
parent
0e15988233
commit
1f962a2167
18 changed files with 816 additions and 7516 deletions
4
.npmrc
Normal file
4
.npmrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# @lobehub/icons declares UI peers that are not needed by our deep icon imports.
|
||||
# Keeping peer auto-install disabled prevents npm from pulling @lobehub/ui/mermaid
|
||||
# back into the tree and reopening npm audit findings for unused packages.
|
||||
legacy-peer-deps=true
|
||||
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -2,7 +2,16 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- _No unreleased changes._
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(mitm):** Compile MITM utilities as NodeNext ESM during prepublish, copy the CommonJS MITM server into the standalone artifact, and resolve MITM data paths without relying on Next.js aliases in packaged runtime.
|
||||
- **fix(electron):** Harden the production desktop CSP by removing `unsafe-eval` outside development and adding object, base URI, form action, frame ancestor, and worker restrictions.
|
||||
- **fix(cli):** Replace shell-interpolated setup and privileged command execution paths with argument-based `spawn`/`execFile` helpers for database setup, Tailscale sudo commands, MITM DNS edits, and certificate install/uninstall flows.
|
||||
- **fix(ui):** Keep provider icons resilient by using direct `@lobehub/icons` components first, then local PNG/SVG fallbacks, avoiding the `@lobehub/ui` peer runtime in the dashboard.
|
||||
|
||||
### 📦 Dependencies
|
||||
|
||||
- **deps:** Update `@lobehub/icons` to `5.5.4`, add explicit `react-is@19.2.5` for Recharts, pin npm installs to skip unused peer auto-installs, and override Electron's transitive `@xmldom/xmldom` to `0.9.10` so audit findings stay closed.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -280,14 +280,22 @@ function installUpdate() {
|
|||
// ── Content Security Policy (#15) ──────────────────────────
|
||||
function setupContentSecurityPolicy() {
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
const scriptSrc = isDev
|
||||
? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
|
||||
: "script-src 'self' 'unsafe-inline'";
|
||||
const csp = [
|
||||
"default-src 'self'",
|
||||
`connect-src 'self' http://localhost:* ws://localhost:* https://*.omniroute.online https://*.omniroute.dev`,
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||
scriptSrc,
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||
"font-src 'self' https://fonts.gstatic.com data:",
|
||||
"img-src 'self' data: blob: https:",
|
||||
"media-src 'self'",
|
||||
"worker-src 'self' blob:",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
].join("; ");
|
||||
|
||||
callback({
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@
|
|||
"electron": "^41.2.0",
|
||||
"electron-builder": "^26.8.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "^0.9.10"
|
||||
},
|
||||
"build": {
|
||||
"appId": "online.omniroute.desktop",
|
||||
"productName": "OmniRoute",
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const nextConfig = {
|
|||
"util",
|
||||
"process",
|
||||
],
|
||||
transpilePackages: ["@omniroute/open-sse"],
|
||||
transpilePackages: ["@omniroute/open-sse", "@lobehub/icons"],
|
||||
allowedDevOrigins: ["localhost", "127.0.0.1", "192.168.*"],
|
||||
typescript: {
|
||||
// TODO: Re-enable after fixing all sub-component useTranslations scope issues
|
||||
|
|
|
|||
7265
package-lock.json
generated
7265
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -106,7 +106,7 @@
|
|||
"system-info": "node scripts/system-info.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lobehub/icons": "^5.0.1",
|
||||
"@lobehub/icons": "^5.5.4",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@swc/helpers": "0.5.21",
|
||||
|
|
@ -133,6 +133,7 @@
|
|||
"pino-pretty": "^13.1.3",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
"react-is": "^19.2.5",
|
||||
"recharts": "^3.7.0",
|
||||
"selfsigned": "^5.5.0",
|
||||
"tsx": "^4.21.0",
|
||||
|
|
@ -199,6 +200,7 @@
|
|||
"lodash-es": "^4.18.1",
|
||||
"dompurify": "^3.4.0",
|
||||
"path-to-regexp": "^8.4.0",
|
||||
"postcss": "^8.5.10",
|
||||
"hono": "^4.12.14",
|
||||
"@hono/node-server": "^1.19.13",
|
||||
"react": "$react",
|
||||
|
|
|
|||
|
|
@ -4,19 +4,23 @@ import { spawn } from "node:child_process";
|
|||
|
||||
const env = { ...process.env };
|
||||
|
||||
await exec("npx next build --experimental-build-mode generate");
|
||||
await exec("npx", ["next", "build", "--experimental-build-mode", "generate"]);
|
||||
|
||||
// launch application
|
||||
await exec(process.argv.slice(2).join(" "));
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (!command) {
|
||||
throw new Error("Missing command to launch after database setup");
|
||||
}
|
||||
await exec(command, args);
|
||||
|
||||
function exec(command) {
|
||||
const child = spawn(command, { shell: true, stdio: "inherit", env });
|
||||
function exec(command, args = []) {
|
||||
const child = spawn(command, args, { stdio: "inherit", env });
|
||||
return new Promise((resolve, reject) => {
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`${command} failed rc=${code}`));
|
||||
reject(new Error(`${[command, ...args].join(" ")} failed rc=${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -331,10 +331,17 @@ if (existsSync(mitmSrc)) {
|
|||
// Write a temporary tsconfig.json targeting the mitm directory
|
||||
const mitmTsconfig = {
|
||||
compilerOptions: {
|
||||
target: "ES2020",
|
||||
module: "CommonJS",
|
||||
target: "ES2022",
|
||||
module: "NodeNext",
|
||||
moduleResolution: "NodeNext",
|
||||
outDir: mitmDest,
|
||||
rootDir: mitmSrc,
|
||||
strict: false,
|
||||
noImplicitAny: false,
|
||||
strictNullChecks: false,
|
||||
noEmitOnError: true,
|
||||
allowImportingTsExtensions: true,
|
||||
rewriteRelativeImportExtensions: true,
|
||||
ignoreDeprecations: "6.0",
|
||||
resolveJsonModule: true,
|
||||
esModuleInterop: true,
|
||||
|
|
@ -352,6 +359,10 @@ if (existsSync(mitmSrc)) {
|
|||
|
||||
try {
|
||||
execSync("npx tsc -p tsconfig.mitm.tmp.json", { cwd: ROOT, stdio: "inherit" });
|
||||
const mitmServerSrc = join(mitmSrc, "server.cjs");
|
||||
if (existsSync(mitmServerSrc)) {
|
||||
cpSync(mitmServerSrc, join(mitmDest, "server.cjs"));
|
||||
}
|
||||
console.log(" ✅ MITM utilities compiled to app/src/mitm/");
|
||||
} catch (err: any) {
|
||||
console.warn(" ⚠️ MITM compile warning (non-fatal):", err.message);
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { promisify } from "util";
|
|||
import { getSettings, updateSettings } from "@/lib/db/settings";
|
||||
import { resolveDataDir } from "@/lib/dataPaths";
|
||||
import { getRuntimePorts } from "@/lib/runtime/ports";
|
||||
import { execWithPassword } from "@/mitm/dns/dnsConfig";
|
||||
import { getCachedPassword, setCachedPassword } from "@/mitm/manager";
|
||||
import { execFileWithPassword } from "@/mitm/systemCommands";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
|
@ -442,7 +442,7 @@ async function runSudoShell(command: string, password: string) {
|
|||
if (!normalizedPassword) {
|
||||
throw new Error("Sudo password required");
|
||||
}
|
||||
await execWithPassword(`sudo -S sh -c ${shellEscape(command)}`, normalizedPassword);
|
||||
await execFileWithPassword("sudo", ["-S", "sh", "-c", command], normalizedPassword);
|
||||
}
|
||||
|
||||
export async function startTailscaleDaemon({
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { resolveDataDir } from "@/lib/dataPaths";
|
||||
import { resolveMitmDataDir } from "../dataDir.ts";
|
||||
|
||||
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
|
||||
|
||||
/**
|
||||
* Generate self-signed SSL certificate using selfsigned (pure JS, no openssl needed)
|
||||
*/
|
||||
export async function generateCert() {
|
||||
const certDir = path.join(resolveDataDir(), "mitm");
|
||||
export async function generateCert(): Promise<{ key: string; cert: string }> {
|
||||
const certDir = path.join(resolveMitmDataDir(), "mitm");
|
||||
const keyPath = path.join(certDir, "server.key");
|
||||
const certPath = path.join(certDir, "server.crt");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +1,64 @@
|
|||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import { exec } from "child_process";
|
||||
import { execWithPassword } from "../dns/dnsConfig";
|
||||
import {
|
||||
execFileText,
|
||||
execFileWithPassword,
|
||||
getErrorMessage,
|
||||
quotePowerShell,
|
||||
runElevatedPowerShell,
|
||||
} from "../systemCommands.ts";
|
||||
|
||||
const IS_WIN = process.platform === "win32";
|
||||
|
||||
// Get SHA1 fingerprint from cert file using Node.js crypto
|
||||
function getCertFingerprint(certPath) {
|
||||
function getCertFingerprint(certPath: string): string {
|
||||
const pem = fs.readFileSync(certPath, "utf-8");
|
||||
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, ""), "base64");
|
||||
return crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g).join(":");
|
||||
const pairs = crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g);
|
||||
if (!pairs) {
|
||||
throw new Error(`Unable to compute certificate fingerprint for ${certPath}`);
|
||||
}
|
||||
return pairs.join(":");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certificate is already installed in system store
|
||||
*/
|
||||
export async function checkCertInstalled(certPath) {
|
||||
export async function checkCertInstalled(certPath: string): Promise<boolean> {
|
||||
if (IS_WIN) {
|
||||
return checkCertInstalledWindows(certPath);
|
||||
}
|
||||
return checkCertInstalledMac(certPath);
|
||||
}
|
||||
|
||||
function checkCertInstalledMac(certPath) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const fingerprint = getCertFingerprint(certPath);
|
||||
exec(
|
||||
`security find-certificate -a -Z /Library/Keychains/System.keychain | grep -i "${fingerprint}"`,
|
||||
(error) => {
|
||||
resolve(!error);
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
async function checkCertInstalledMac(certPath: string): Promise<boolean> {
|
||||
try {
|
||||
const fingerprint = getCertFingerprint(certPath);
|
||||
const output = await execFileText("security", [
|
||||
"find-certificate",
|
||||
"-a",
|
||||
"-Z",
|
||||
"/Library/Keychains/System.keychain",
|
||||
]);
|
||||
return output.toUpperCase().includes(fingerprint);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function checkCertInstalledWindows(certPath) {
|
||||
return new Promise((resolve) => {
|
||||
// Check Root store for our cert by subject name
|
||||
exec("certutil -store Root daily-cloudcode-pa.googleapis.com", (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
async function checkCertInstalledWindows(_certPath: string): Promise<boolean> {
|
||||
try {
|
||||
await execFileText("certutil", ["-store", "Root", "daily-cloudcode-pa.googleapis.com"]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install SSL certificate to system trust store
|
||||
*/
|
||||
export async function installCert(sudoPassword, certPath) {
|
||||
export async function installCert(sudoPassword: string, certPath: string): Promise<void> {
|
||||
if (!fs.existsSync(certPath)) {
|
||||
throw new Error(`Certificate file not found: ${certPath}`);
|
||||
}
|
||||
|
|
@ -68,41 +76,46 @@ export async function installCert(sudoPassword, certPath) {
|
|||
}
|
||||
}
|
||||
|
||||
async function installCertMac(sudoPassword, certPath) {
|
||||
const command = `sudo -S security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
|
||||
async function installCertMac(sudoPassword: string, certPath: string): Promise<void> {
|
||||
try {
|
||||
await execWithPassword(command, sudoPassword);
|
||||
await execFileWithPassword(
|
||||
"sudo",
|
||||
[
|
||||
"-S",
|
||||
"security",
|
||||
"add-trusted-cert",
|
||||
"-d",
|
||||
"-r",
|
||||
"trustRoot",
|
||||
"-k",
|
||||
"/Library/Keychains/System.keychain",
|
||||
certPath,
|
||||
],
|
||||
sudoPassword
|
||||
);
|
||||
console.log(`✅ Installed certificate to system keychain: ${certPath}`);
|
||||
} catch (error) {
|
||||
const msg = error.message?.includes("canceled")
|
||||
const message = getErrorMessage(error);
|
||||
const msg = message.includes("canceled")
|
||||
? "User canceled authorization"
|
||||
: "Certificate install failed";
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
async function installCertWindows(certPath) {
|
||||
// Use PowerShell elevated to add cert to Root store and capture exit code
|
||||
const psScript = `
|
||||
$proc = Start-Process certutil -ArgumentList '-addstore','Root','${certPath.replace(/'/g, "''")}' -Verb RunAs -Wait -PassThru;
|
||||
async function installCertWindows(certPath: string): Promise<void> {
|
||||
await runElevatedPowerShell(`
|
||||
$certPath = ${quotePowerShell(certPath)};
|
||||
$proc = Start-Process certutil -ArgumentList @('-addstore','Root',$certPath) -Verb RunAs -Wait -PassThru;
|
||||
if ($proc.ExitCode -ne 0) { throw "certutil exited with code $($proc.ExitCode)" }
|
||||
`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`powershell -Command "${psScript.replace(/\n/g, " ")}"`, (error) => {
|
||||
if (error) {
|
||||
reject(new Error(`Failed to install certificate: ${error.message}`));
|
||||
} else {
|
||||
console.log(`✅ Installed certificate to Windows Root store`);
|
||||
resolve(void 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
`);
|
||||
console.log(`✅ Installed certificate to Windows Root store`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall SSL certificate from system store
|
||||
*/
|
||||
export async function uninstallCert(sudoPassword, certPath) {
|
||||
export async function uninstallCert(sudoPassword: string, certPath: string): Promise<void> {
|
||||
const isInstalled = await checkCertInstalled(certPath);
|
||||
if (!isInstalled) {
|
||||
console.log("Certificate not found in system store");
|
||||
|
|
@ -116,27 +129,31 @@ export async function uninstallCert(sudoPassword, certPath) {
|
|||
}
|
||||
}
|
||||
|
||||
async function uninstallCertMac(sudoPassword, certPath) {
|
||||
async function uninstallCertMac(sudoPassword: string, certPath: string): Promise<void> {
|
||||
const fingerprint = getCertFingerprint(certPath).replace(/:/g, "");
|
||||
const command = `sudo -S security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
|
||||
try {
|
||||
await execWithPassword(command, sudoPassword);
|
||||
await execFileWithPassword(
|
||||
"sudo",
|
||||
[
|
||||
"-S",
|
||||
"security",
|
||||
"delete-certificate",
|
||||
"-Z",
|
||||
fingerprint,
|
||||
"/Library/Keychains/System.keychain",
|
||||
],
|
||||
sudoPassword
|
||||
);
|
||||
console.log("✅ Uninstalled certificate from system keychain");
|
||||
} catch (err) {
|
||||
throw new Error("Failed to uninstall certificate");
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallCertWindows() {
|
||||
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','daily-cloudcode-pa.googleapis.com' -Verb RunAs -Wait`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`powershell -Command "${psCommand}"`, (error) => {
|
||||
if (error) {
|
||||
reject(new Error(`Failed to uninstall certificate: ${error.message}`));
|
||||
} else {
|
||||
console.log("✅ Uninstalled certificate from Windows Root store");
|
||||
resolve(void 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
async function uninstallCertWindows(): Promise<void> {
|
||||
await runElevatedPowerShell(`
|
||||
$proc = Start-Process certutil -ArgumentList @('-delstore','Root','daily-cloudcode-pa.googleapis.com') -Verb RunAs -Wait -PassThru;
|
||||
if ($proc.ExitCode -ne 0) { throw "certutil exited with code $($proc.ExitCode)" }
|
||||
`);
|
||||
console.log("✅ Uninstalled certificate from Windows Root store");
|
||||
}
|
||||
|
|
|
|||
41
src/mitm/dataDir.ts
Normal file
41
src/mitm/dataDir.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
const APP_NAME = "omniroute";
|
||||
|
||||
function fallbackHomeDir(): string {
|
||||
const envHome = process.env.HOME || process.env.USERPROFILE;
|
||||
return typeof envHome === "string" && envHome.trim() ? path.resolve(envHome) : os.tmpdir();
|
||||
}
|
||||
|
||||
function safeHomeDir(): string {
|
||||
try {
|
||||
return os.homedir();
|
||||
} catch {
|
||||
return fallbackHomeDir();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeConfiguredPath(dir: unknown): string | null {
|
||||
if (typeof dir !== "string") return null;
|
||||
const trimmed = dir.trim();
|
||||
return trimmed ? path.resolve(trimmed) : null;
|
||||
}
|
||||
|
||||
export function resolveMitmDataDir(): string {
|
||||
const configured = normalizeConfiguredPath(process.env.DATA_DIR);
|
||||
if (configured) return configured;
|
||||
|
||||
const homeDir = safeHomeDir();
|
||||
if (process.platform === "win32") {
|
||||
const appData = process.env.APPDATA || path.join(homeDir, "AppData", "Roaming");
|
||||
return path.join(appData, APP_NAME);
|
||||
}
|
||||
|
||||
const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME);
|
||||
if (xdgConfigHome) {
|
||||
return path.join(xdgConfigHome, APP_NAME);
|
||||
}
|
||||
|
||||
return path.join(homeDir, `.${APP_NAME}`);
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import { exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
execFileWithPassword,
|
||||
getErrorMessage,
|
||||
quotePowerShell,
|
||||
runElevatedPowerShell,
|
||||
} from "../systemCommands.ts";
|
||||
|
||||
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
|
||||
const IS_WIN = process.platform === "win32";
|
||||
|
|
@ -8,46 +13,22 @@ const HOSTS_FILE = IS_WIN
|
|||
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
|
||||
: "/etc/hosts";
|
||||
|
||||
/**
|
||||
* Execute command with sudo password via stdin (macOS/Linux only)
|
||||
*/
|
||||
export function execWithPassword(command, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Command failed: ${error.message}\n${stderr}`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
child.stdin.write(`${password}\n`);
|
||||
child.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute elevated command on Windows via PowerShell RunAs
|
||||
*/
|
||||
function execElevatedWindows(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const psScript = `
|
||||
$proc = Start-Process cmd -ArgumentList '/c','${command.replace(/'/g, "''")}' -Verb RunAs -Wait -PassThru;
|
||||
if ($proc.ExitCode -ne 0) { throw "Elevated command exited with code $($proc.ExitCode)" }
|
||||
`;
|
||||
exec(`powershell -Command "${psScript.replace(/\n/g, " ")}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Elevated command failed: ${error.message}\n${stderr}`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
const REMOVE_HOSTS_ENTRY_SCRIPT = `
|
||||
const fs = require("fs");
|
||||
const filePath = process.argv[1];
|
||||
const targetHost = process.argv[2];
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const filtered = content.split(/\\r?\\n/).filter((line) => {
|
||||
const parts = line.trim().split(/\\s+/).filter(Boolean);
|
||||
return !(parts.length >= 2 && parts.includes(targetHost));
|
||||
});
|
||||
fs.writeFileSync(filePath, filtered.join("\\n").replace(/\\n*$/, "\\n"));
|
||||
`;
|
||||
|
||||
/**
|
||||
* Check if DNS entry already exists
|
||||
*/
|
||||
export function checkDNSEntry() {
|
||||
export function checkDNSEntry(): boolean {
|
||||
try {
|
||||
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const lines = hostsContent.split(/\r?\n/);
|
||||
|
|
@ -63,7 +44,7 @@ export function checkDNSEntry() {
|
|||
/**
|
||||
* Add DNS entry to hosts file
|
||||
*/
|
||||
export async function addDNSEntry(sudoPassword) {
|
||||
export async function addDNSEntry(sudoPassword: string): Promise<void> {
|
||||
if (checkDNSEntry()) {
|
||||
console.log(`DNS entry for ${TARGET_HOST} already exists`);
|
||||
return;
|
||||
|
|
@ -73,22 +54,27 @@ export async function addDNSEntry(sudoPassword) {
|
|||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Windows: use elevated echo >> hosts
|
||||
await execElevatedWindows(`echo ${entry} >> "${HOSTS_FILE}"`);
|
||||
await runElevatedPowerShell(
|
||||
`Add-Content -LiteralPath ${quotePowerShell(HOSTS_FILE)} -Value ${quotePowerShell(entry)}`
|
||||
);
|
||||
} else {
|
||||
const command = `echo "${entry}" | sudo -S tee -a ${HOSTS_FILE} > /dev/null`;
|
||||
await execWithPassword(command, sudoPassword);
|
||||
await execFileWithPassword(
|
||||
"sudo",
|
||||
["-S", "tee", "-a", HOSTS_FILE],
|
||||
sudoPassword,
|
||||
`${entry}\n`
|
||||
);
|
||||
}
|
||||
console.log(`✅ Added DNS entry: ${entry}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add DNS entry: ${error.message}`);
|
||||
throw new Error(`Failed to add DNS entry: ${getErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove DNS entry from hosts file
|
||||
*/
|
||||
export async function removeDNSEntry(sudoPassword) {
|
||||
export async function removeDNSEntry(sudoPassword: string): Promise<void> {
|
||||
if (!checkDNSEntry()) {
|
||||
console.log(`DNS entry for ${TARGET_HOST} does not exist`);
|
||||
return;
|
||||
|
|
@ -96,21 +82,25 @@ export async function removeDNSEntry(sudoPassword) {
|
|||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Windows: read, filter, write back via elevated PowerShell
|
||||
const psScript = `(Get-Content '${HOSTS_FILE}') | Where-Object { $_ -notmatch '${TARGET_HOST}' } | Set-Content '${HOSTS_FILE}'`;
|
||||
const psCommand = `Start-Process powershell -ArgumentList '-Command','${psScript.replace(/'/g, "''")}' -Verb RunAs -Wait`;
|
||||
await new Promise((resolve, reject) => {
|
||||
exec(`powershell -Command "${psCommand}"`, (error) => {
|
||||
if (error) reject(new Error(`Failed to remove DNS entry: ${error.message}`));
|
||||
else resolve(void 0);
|
||||
});
|
||||
});
|
||||
await runElevatedPowerShell(`
|
||||
$hostsFile = ${quotePowerShell(HOSTS_FILE)};
|
||||
$targetHost = ${quotePowerShell(TARGET_HOST)};
|
||||
$lines = Get-Content -LiteralPath $hostsFile;
|
||||
$filtered = $lines | Where-Object {
|
||||
$parts = ($_ -split '\\s+') | Where-Object { $_ };
|
||||
-not (($parts.Length -ge 2) -and ($parts -contains $targetHost))
|
||||
};
|
||||
Set-Content -LiteralPath $hostsFile -Value $filtered;
|
||||
`);
|
||||
} else {
|
||||
const command = `sudo -S sed -i '' '/${TARGET_HOST}/d' ${HOSTS_FILE}`;
|
||||
await execWithPassword(command, sudoPassword);
|
||||
await execFileWithPassword(
|
||||
"sudo",
|
||||
["-S", process.execPath, "-e", REMOVE_HOSTS_ENTRY_SCRIPT, HOSTS_FILE, TARGET_HOST],
|
||||
sudoPassword
|
||||
);
|
||||
}
|
||||
console.log(`✅ Removed DNS entry for ${TARGET_HOST}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to remove DNS entry: ${error.message}`);
|
||||
throw new Error(`Failed to remove DNS entry: ${getErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
import { spawn } from "child_process";
|
||||
import { spawn, type ChildProcess } from "child_process";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { resolveDataDir } from "@/lib/dataPaths";
|
||||
import { addDNSEntry, removeDNSEntry } from "./dns/dnsConfig";
|
||||
import { generateCert } from "./cert/generate";
|
||||
import { installCert } from "./cert/install";
|
||||
import { resolveMitmDataDir } from "./dataDir.ts";
|
||||
import { addDNSEntry, removeDNSEntry } from "./dns/dnsConfig.ts";
|
||||
import { generateCert } from "./cert/generate.ts";
|
||||
import { installCert } from "./cert/install.ts";
|
||||
|
||||
// Store server process
|
||||
let serverProcess = null;
|
||||
let serverPid = null;
|
||||
let serverProcess: ChildProcess | null = null;
|
||||
let serverPid: number | null = null;
|
||||
|
||||
// Module-scoped password cache (not exposed on globalThis).
|
||||
// Cleared automatically when the MITM proxy is stopped.
|
||||
let _cachedPassword = null;
|
||||
export function getCachedPassword() {
|
||||
let _cachedPassword: string | null = null;
|
||||
export function getCachedPassword(): string | null {
|
||||
return _cachedPassword;
|
||||
}
|
||||
export function setCachedPassword(pwd) {
|
||||
export function setCachedPassword(pwd: string | null | undefined): void {
|
||||
_cachedPassword = pwd || null;
|
||||
}
|
||||
export function clearCachedPassword() {
|
||||
export function clearCachedPassword(): void {
|
||||
_cachedPassword = null;
|
||||
}
|
||||
|
||||
const PID_FILE = path.join(resolveDataDir(), "mitm", ".mitm.pid");
|
||||
const PID_FILE = path.join(resolveMitmDataDir(), "mitm", ".mitm.pid");
|
||||
const MITM_SERVER_URL = new URL("./server.cjs", import.meta.url);
|
||||
const urlPath =
|
||||
process.platform === "win32" && MITM_SERVER_URL.pathname.startsWith("/")
|
||||
|
|
@ -34,7 +34,7 @@ const cwdPath = path.join(process.cwd(), "src", "mitm", "server.cjs");
|
|||
const MITM_SERVER_PATH = fs.existsSync(cwdPath) ? cwdPath : urlPath;
|
||||
|
||||
// Check if a PID is alive
|
||||
function isProcessAlive(pid) {
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
|
|
@ -46,7 +46,12 @@ function isProcessAlive(pid) {
|
|||
/**
|
||||
* Get MITM status
|
||||
*/
|
||||
export async function getMitmStatus() {
|
||||
export async function getMitmStatus(): Promise<{
|
||||
running: boolean;
|
||||
pid: number | null;
|
||||
dnsConfigured: boolean;
|
||||
certExists: boolean;
|
||||
}> {
|
||||
// Check in-memory process first, then fallback to PID file
|
||||
let running = serverProcess !== null && !serverProcess.killed;
|
||||
let pid = serverPid;
|
||||
|
|
@ -78,7 +83,7 @@ export async function getMitmStatus() {
|
|||
}
|
||||
|
||||
// Check cert
|
||||
const certDir = path.join(resolveDataDir(), "mitm");
|
||||
const certDir = path.join(resolveMitmDataDir(), "mitm");
|
||||
const certExists = fs.existsSync(path.join(certDir, "server.crt"));
|
||||
|
||||
return { running, pid, dnsConfigured, certExists };
|
||||
|
|
@ -89,14 +94,17 @@ export async function getMitmStatus() {
|
|||
* @param {string} apiKey - OmniRoute API key
|
||||
* @param {string} sudoPassword - Sudo password for DNS/cert operations
|
||||
*/
|
||||
export async function startMitm(apiKey, sudoPassword) {
|
||||
export async function startMitm(
|
||||
apiKey: string,
|
||||
sudoPassword: string
|
||||
): Promise<{ running: true; pid: number | null }> {
|
||||
// Check if already running
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
throw new Error("MITM proxy is already running");
|
||||
}
|
||||
|
||||
// 1. Generate SSL certificate if not exists
|
||||
const certPath = path.join(resolveDataDir(), "mitm", "server.crt");
|
||||
const certPath = path.join(resolveMitmDataDir(), "mitm", "server.crt");
|
||||
if (!fs.existsSync(certPath)) {
|
||||
console.log("Generating SSL certificate...");
|
||||
await generateCert();
|
||||
|
|
@ -121,21 +129,24 @@ export async function startMitm(apiKey, sudoPassword) {
|
|||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
serverPid = serverProcess.pid;
|
||||
const proc = serverProcess;
|
||||
serverPid = proc.pid ?? null;
|
||||
|
||||
// Save PID to file
|
||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
||||
if (serverPid !== null) {
|
||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
||||
}
|
||||
|
||||
// Log server output
|
||||
serverProcess.stdout.on("data", (data) => {
|
||||
proc.stdout?.on("data", (data) => {
|
||||
console.log(`[MITM Server] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr.on("data", (data) => {
|
||||
proc.stderr?.on("data", (data) => {
|
||||
console.error(`[MITM Server Error] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
proc.on("exit", (code) => {
|
||||
console.log(`MITM server exited with code ${code}`);
|
||||
serverProcess = null;
|
||||
serverPid = null;
|
||||
|
|
@ -149,7 +160,7 @@ export async function startMitm(apiKey, sudoPassword) {
|
|||
});
|
||||
|
||||
// Wait and verify server actually started
|
||||
const started = await new Promise((resolve) => {
|
||||
const started = await new Promise<boolean>((resolve) => {
|
||||
let resolved = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
|
|
@ -158,7 +169,7 @@ export async function startMitm(apiKey, sudoPassword) {
|
|||
}
|
||||
}, 2000);
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
proc.on("exit", () => {
|
||||
clearTimeout(timeout);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
|
|
@ -167,7 +178,7 @@ export async function startMitm(apiKey, sudoPassword) {
|
|||
});
|
||||
|
||||
// Check stderr for error messages
|
||||
serverProcess.stderr.on("data", (data) => {
|
||||
proc.stderr?.on("data", (data) => {
|
||||
const msg = data.toString().trim();
|
||||
if (msg.includes("Port") && msg.includes("already in use")) {
|
||||
clearTimeout(timeout);
|
||||
|
|
@ -193,7 +204,7 @@ export async function startMitm(apiKey, sudoPassword) {
|
|||
* Stop MITM proxy
|
||||
* @param {string} sudoPassword - Sudo password for DNS cleanup
|
||||
*/
|
||||
export async function stopMitm(sudoPassword) {
|
||||
export async function stopMitm(sudoPassword: string): Promise<{ running: false; pid: null }> {
|
||||
// 1. Kill server process (in-memory or from PID file)
|
||||
const proc = serverProcess;
|
||||
if (proc && !proc.killed) {
|
||||
|
|
|
|||
96
src/mitm/systemCommands.ts
Normal file
96
src/mitm/systemCommands.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { execFile, spawn } from "child_process";
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export function execFileText(command: string, args: string[]): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(command, args, { encoding: "utf8" }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Command failed: ${getErrorMessage(error)}\n${stderr}`));
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function execFileWithPassword(
|
||||
command: string,
|
||||
args: string[],
|
||||
password: string,
|
||||
stdinAfterPassword = ""
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
|
||||
const settle = (error: Error | null) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
};
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
settle(new Error(`Command failed: ${getErrorMessage(error)}\n${stderr}`));
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
settle(null);
|
||||
return;
|
||||
}
|
||||
settle(new Error(`Command failed with code ${code}\n${stderr}`));
|
||||
});
|
||||
|
||||
child.stdin?.write(`${password}\n${stdinAfterPassword}`);
|
||||
child.stdin?.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function quotePowerShell(value: string): string {
|
||||
return `'${value.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
export function runPowerShell(script: string): Promise<string> {
|
||||
return execFileText("powershell", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
script,
|
||||
]);
|
||||
}
|
||||
|
||||
export function runElevatedPowerShell(script: string): Promise<string> {
|
||||
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
||||
const wrapper = `
|
||||
$proc = Start-Process powershell -ArgumentList @(
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-EncodedCommand',
|
||||
'${encoded}'
|
||||
) -Verb RunAs -Wait -PassThru;
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
throw "Elevated command exited with code $($proc.ExitCode)"
|
||||
}
|
||||
`;
|
||||
return runPowerShell(wrapper);
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* ProviderIcon — Renders a provider logo using @lobehub/icons with PNG fallback.
|
||||
* ProviderIcon — Renders a provider logo using @lobehub/icons with static asset fallbacks.
|
||||
*
|
||||
* Strategy (#529):
|
||||
* 1. Try @lobehub/icons ProviderIcon (130+ providers, React components)
|
||||
* 1. Try @lobehub/icons direct icon components (no @lobehub/ui peer runtime)
|
||||
* 2. Fall back to /providers/{id}.png (existing static assets)
|
||||
* 3. Fall back to /providers/{id}.svg (SVG assets)
|
||||
* 4. Fall back to a generic AI icon
|
||||
|
|
@ -14,100 +14,10 @@
|
|||
* <ProviderIcon providerId="anthropic" size={28} type="color" />
|
||||
*/
|
||||
|
||||
import { memo, useState, Component, type ReactNode } from "react";
|
||||
import { createElement, memo, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { ProviderIcon as LobehubProviderIcon } from "@lobehub/icons";
|
||||
|
||||
const LOBEHUB_PROVIDER_MAP: Record<string, string> = {
|
||||
openai: "openai",
|
||||
anthropic: "anthropic",
|
||||
claude: "anthropic",
|
||||
gemini: "google",
|
||||
"gemini-cli": "gemini",
|
||||
google: "google",
|
||||
"google-pse-search": "google",
|
||||
deepseek: "deepseek",
|
||||
groq: "groq",
|
||||
mistral: "mistral",
|
||||
codestral: "mistral",
|
||||
cohere: "cohere",
|
||||
perplexity: "perplexity",
|
||||
"perplexity-search": "perplexity",
|
||||
"perplexity-web": "perplexity",
|
||||
xai: "xai",
|
||||
grok: "xai",
|
||||
"grok-web": "xai",
|
||||
together: "together",
|
||||
fireworks: "fireworks",
|
||||
"fireworks-ai": "fireworks",
|
||||
cerebras: "cerebras",
|
||||
huggingface: "huggingface",
|
||||
"hugging-face": "huggingface",
|
||||
openrouter: "openrouter",
|
||||
"open-router": "openrouter",
|
||||
ollama: "ollama",
|
||||
"ollama-cloud": "ollama",
|
||||
minimax: "minimax",
|
||||
"minimax-cn": "minimax",
|
||||
qwen: "qwen",
|
||||
alibaba: "qwen",
|
||||
moonshot: "moonshot",
|
||||
kimi: "moonshot",
|
||||
"kimi-coding": "kimi",
|
||||
"kimi-coding-apikey": "kimi",
|
||||
baidu: "baidu",
|
||||
ernie: "baidu",
|
||||
spark: "iflytek",
|
||||
"zhipu-ai": "zhipu",
|
||||
zhipu: "zhipu",
|
||||
glm: "zhipu",
|
||||
glmt: "zhipu",
|
||||
lmsys: "lmsys",
|
||||
"stability-ai": "stability",
|
||||
stability: "stability",
|
||||
replicate: "replicate",
|
||||
ai21: "ai21",
|
||||
nvidia: "nvidia",
|
||||
cloudflare: "cloudflare",
|
||||
"cloudflare-ai": "cloudflare",
|
||||
"amazon-q": "bedrock",
|
||||
"aws-bedrock": "bedrock",
|
||||
bedrock: "bedrock",
|
||||
azure: "azure",
|
||||
"azure-openai": "azure",
|
||||
copilot: "githubcopilot",
|
||||
"github-copilot": "githubcopilot",
|
||||
github: "github",
|
||||
mistralai: "mistral",
|
||||
"azure-ai": "azureai",
|
||||
baseten: "baseten",
|
||||
"black-forest-labs": "bfl",
|
||||
deepinfra: "deepinfra",
|
||||
"fal-ai": "fal",
|
||||
"featherless-ai": "featherless",
|
||||
friendliai: "friendli",
|
||||
"glm-cn": "zhipu",
|
||||
"inference-net": "inference",
|
||||
"jina-ai": "jina",
|
||||
"lambda-ai": "lambda",
|
||||
"lm-studio": "lmstudio",
|
||||
"meta-llama": "meta",
|
||||
"muse-spark-web": "meta",
|
||||
"nous-research": "nousresearch",
|
||||
novita: "novita",
|
||||
sambanova: "sambanova",
|
||||
"searchapi-search": "searchapi",
|
||||
snowflake: "snowflake",
|
||||
upstage: "upstage",
|
||||
"v0-vercel": "v0",
|
||||
"vercel-ai-gateway": "vercelaigateway",
|
||||
"vertex-partner": "vertexai",
|
||||
vllm: "vllm",
|
||||
volcengine: "volcengine",
|
||||
watsonx: "ibm",
|
||||
"xiaomi-mimo": "xiaomimimo",
|
||||
xinference: "xinference",
|
||||
};
|
||||
import { getLobeProviderIcon } from "./lobeProviderIcons";
|
||||
|
||||
interface ProviderIconProps {
|
||||
providerId: string;
|
||||
|
|
@ -117,30 +27,6 @@ interface ProviderIconProps {
|
|||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/** Error boundary to catch Lobehub component render errors gracefully. */
|
||||
class LobehubErrorBoundary extends Component<
|
||||
{ children: ReactNode; onError: () => void },
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch() {
|
||||
this.props.onError();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) return null;
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function GenericProviderIcon({ size }: { size: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ flex: "none" }}>
|
||||
|
|
@ -280,23 +166,27 @@ const ProviderIcon = memo(function ProviderIcon({
|
|||
style,
|
||||
}: ProviderIconProps) {
|
||||
const normalizedId = providerId.toLowerCase();
|
||||
const lobehubId = LOBEHUB_PROVIDER_MAP[normalizedId] ?? null;
|
||||
const lobeIcon = getLobeProviderIcon(normalizedId, type);
|
||||
const hasPng = KNOWN_PNGS.has(normalizedId);
|
||||
const hasSvg = KNOWN_SVGS.has(normalizedId);
|
||||
|
||||
const [useLobehub, setUseLobehub] = useState(lobehubId !== null);
|
||||
const [usePng, setUsePng] = useState(hasPng);
|
||||
const [useSvg, setUseSvg] = useState(!hasPng && hasSvg);
|
||||
const [failedAssets, setFailedAssets] = useState<Record<string, true>>({});
|
||||
const pngKey = `${normalizedId}:png`;
|
||||
const svgKey = `${normalizedId}:svg`;
|
||||
const usePng = !lobeIcon && hasPng && !failedAssets[pngKey];
|
||||
const useSvg = !lobeIcon && hasSvg && !failedAssets[svgKey] && (!hasPng || failedAssets[pngKey]);
|
||||
|
||||
if (useLobehub && lobehubId) {
|
||||
if (lobeIcon) {
|
||||
return (
|
||||
<span
|
||||
className={className}
|
||||
style={{ display: "inline-flex", alignItems: "center", ...style }}
|
||||
>
|
||||
<LobehubErrorBoundary onError={() => setUseLobehub(false)}>
|
||||
<LobehubProviderIcon provider={lobehubId} size={size} type={type} />
|
||||
</LobehubErrorBoundary>
|
||||
{createElement(lobeIcon, {
|
||||
"aria-label": providerId,
|
||||
size,
|
||||
style: { flex: "none" },
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -314,8 +204,7 @@ const ProviderIcon = memo(function ProviderIcon({
|
|||
height={size}
|
||||
style={{ objectFit: "contain" }}
|
||||
onError={() => {
|
||||
setUsePng(false);
|
||||
setUseSvg(hasSvg);
|
||||
setFailedAssets((current) => ({ ...current, [pngKey]: true }));
|
||||
}}
|
||||
unoptimized
|
||||
/>
|
||||
|
|
@ -335,7 +224,7 @@ const ProviderIcon = memo(function ProviderIcon({
|
|||
width={size}
|
||||
height={size}
|
||||
style={{ objectFit: "contain" }}
|
||||
onError={() => setUseSvg(false)}
|
||||
onError={() => setFailedAssets((current) => ({ ...current, [svgKey]: true }))}
|
||||
unoptimized
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
404
src/shared/components/lobeProviderIcons.ts
Normal file
404
src/shared/components/lobeProviderIcons.ts
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
import type { CSSProperties, ElementType } from "react";
|
||||
|
||||
import Ai21MonoIcon from "@lobehub/icons/es/Ai21/components/Mono";
|
||||
import AlibabaColorIcon from "@lobehub/icons/es/Alibaba/components/Color";
|
||||
import AlibabaMonoIcon from "@lobehub/icons/es/Alibaba/components/Mono";
|
||||
import AnthropicMonoIcon from "@lobehub/icons/es/Anthropic/components/Mono";
|
||||
import AntigravityColorIcon from "@lobehub/icons/es/Antigravity/components/Color";
|
||||
import AntigravityMonoIcon from "@lobehub/icons/es/Antigravity/components/Mono";
|
||||
import AssemblyAIColorIcon from "@lobehub/icons/es/AssemblyAI/components/Color";
|
||||
import AssemblyAIMonoIcon from "@lobehub/icons/es/AssemblyAI/components/Mono";
|
||||
import AutomaticColorIcon from "@lobehub/icons/es/Automatic/components/Color";
|
||||
import AutomaticMonoIcon from "@lobehub/icons/es/Automatic/components/Mono";
|
||||
import AwsColorIcon from "@lobehub/icons/es/Aws/components/Color";
|
||||
import AwsMonoIcon from "@lobehub/icons/es/Aws/components/Mono";
|
||||
import AzureColorIcon from "@lobehub/icons/es/Azure/components/Color";
|
||||
import AzureMonoIcon from "@lobehub/icons/es/Azure/components/Mono";
|
||||
import AzureAIColorIcon from "@lobehub/icons/es/AzureAI/components/Color";
|
||||
import AzureAIMonoIcon from "@lobehub/icons/es/AzureAI/components/Mono";
|
||||
import BaiduColorIcon from "@lobehub/icons/es/Baidu/components/Color";
|
||||
import BaiduMonoIcon from "@lobehub/icons/es/Baidu/components/Mono";
|
||||
import BailianColorIcon from "@lobehub/icons/es/Bailian/components/Color";
|
||||
import BailianMonoIcon from "@lobehub/icons/es/Bailian/components/Mono";
|
||||
import BasetenMonoIcon from "@lobehub/icons/es/Baseten/components/Mono";
|
||||
import BedrockColorIcon from "@lobehub/icons/es/Bedrock/components/Color";
|
||||
import BedrockMonoIcon from "@lobehub/icons/es/Bedrock/components/Mono";
|
||||
import BflMonoIcon from "@lobehub/icons/es/Bfl/components/Mono";
|
||||
import CerebrasColorIcon from "@lobehub/icons/es/Cerebras/components/Color";
|
||||
import CerebrasMonoIcon from "@lobehub/icons/es/Cerebras/components/Mono";
|
||||
import ClaudeCodeColorIcon from "@lobehub/icons/es/ClaudeCode/components/Color";
|
||||
import ClaudeCodeMonoIcon from "@lobehub/icons/es/ClaudeCode/components/Mono";
|
||||
import ClineMonoIcon from "@lobehub/icons/es/Cline/components/Mono";
|
||||
import CloudflareColorIcon from "@lobehub/icons/es/Cloudflare/components/Color";
|
||||
import CloudflareMonoIcon from "@lobehub/icons/es/Cloudflare/components/Mono";
|
||||
import CodexColorIcon from "@lobehub/icons/es/Codex/components/Color";
|
||||
import CodexMonoIcon from "@lobehub/icons/es/Codex/components/Mono";
|
||||
import CohereColorIcon from "@lobehub/icons/es/Cohere/components/Color";
|
||||
import CohereMonoIcon from "@lobehub/icons/es/Cohere/components/Mono";
|
||||
import ComfyUIColorIcon from "@lobehub/icons/es/ComfyUI/components/Color";
|
||||
import ComfyUIMonoIcon from "@lobehub/icons/es/ComfyUI/components/Mono";
|
||||
import CursorMonoIcon from "@lobehub/icons/es/Cursor/components/Mono";
|
||||
import DbrxColorIcon from "@lobehub/icons/es/Dbrx/components/Color";
|
||||
import DbrxMonoIcon from "@lobehub/icons/es/Dbrx/components/Mono";
|
||||
import DeepInfraColorIcon from "@lobehub/icons/es/DeepInfra/components/Color";
|
||||
import DeepInfraMonoIcon from "@lobehub/icons/es/DeepInfra/components/Mono";
|
||||
import DeepSeekColorIcon from "@lobehub/icons/es/DeepSeek/components/Color";
|
||||
import DeepSeekMonoIcon from "@lobehub/icons/es/DeepSeek/components/Mono";
|
||||
import ElevenLabsMonoIcon from "@lobehub/icons/es/ElevenLabs/components/Mono";
|
||||
import ExaColorIcon from "@lobehub/icons/es/Exa/components/Color";
|
||||
import ExaMonoIcon from "@lobehub/icons/es/Exa/components/Mono";
|
||||
import FalColorIcon from "@lobehub/icons/es/Fal/components/Color";
|
||||
import FalMonoIcon from "@lobehub/icons/es/Fal/components/Mono";
|
||||
import FeatherlessColorIcon from "@lobehub/icons/es/Featherless/components/Color";
|
||||
import FeatherlessMonoIcon from "@lobehub/icons/es/Featherless/components/Mono";
|
||||
import FireworksColorIcon from "@lobehub/icons/es/Fireworks/components/Color";
|
||||
import FireworksMonoIcon from "@lobehub/icons/es/Fireworks/components/Mono";
|
||||
import FriendliMonoIcon from "@lobehub/icons/es/Friendli/components/Mono";
|
||||
import GeminiColorIcon from "@lobehub/icons/es/Gemini/components/Color";
|
||||
import GeminiMonoIcon from "@lobehub/icons/es/Gemini/components/Mono";
|
||||
import GeminiCLIColorIcon from "@lobehub/icons/es/GeminiCLI/components/Color";
|
||||
import GeminiCLIMonoIcon from "@lobehub/icons/es/GeminiCLI/components/Mono";
|
||||
import GithubCopilotMonoIcon from "@lobehub/icons/es/GithubCopilot/components/Mono";
|
||||
import GoogleColorIcon from "@lobehub/icons/es/Google/components/Color";
|
||||
import GoogleMonoIcon from "@lobehub/icons/es/Google/components/Mono";
|
||||
import GrokMonoIcon from "@lobehub/icons/es/Grok/components/Mono";
|
||||
import GroqMonoIcon from "@lobehub/icons/es/Groq/components/Mono";
|
||||
import HuggingFaceColorIcon from "@lobehub/icons/es/HuggingFace/components/Color";
|
||||
import HuggingFaceMonoIcon from "@lobehub/icons/es/HuggingFace/components/Mono";
|
||||
import HyperbolicColorIcon from "@lobehub/icons/es/Hyperbolic/components/Color";
|
||||
import HyperbolicMonoIcon from "@lobehub/icons/es/Hyperbolic/components/Mono";
|
||||
import IBMMonoIcon from "@lobehub/icons/es/IBM/components/Mono";
|
||||
import InferenceMonoIcon from "@lobehub/icons/es/Inference/components/Mono";
|
||||
import JinaMonoIcon from "@lobehub/icons/es/Jina/components/Mono";
|
||||
import KiloCodeMonoIcon from "@lobehub/icons/es/KiloCode/components/Mono";
|
||||
import KimiColorIcon from "@lobehub/icons/es/Kimi/components/Color";
|
||||
import KimiMonoIcon from "@lobehub/icons/es/Kimi/components/Mono";
|
||||
import LambdaMonoIcon from "@lobehub/icons/es/Lambda/components/Mono";
|
||||
import LmStudioMonoIcon from "@lobehub/icons/es/LmStudio/components/Mono";
|
||||
import MetaColorIcon from "@lobehub/icons/es/Meta/components/Color";
|
||||
import MetaMonoIcon from "@lobehub/icons/es/Meta/components/Mono";
|
||||
import MetaAIColorIcon from "@lobehub/icons/es/MetaAI/components/Color";
|
||||
import MetaAIMonoIcon from "@lobehub/icons/es/MetaAI/components/Mono";
|
||||
import MinimaxColorIcon from "@lobehub/icons/es/Minimax/components/Color";
|
||||
import MinimaxMonoIcon from "@lobehub/icons/es/Minimax/components/Mono";
|
||||
import MistralColorIcon from "@lobehub/icons/es/Mistral/components/Color";
|
||||
import MistralMonoIcon from "@lobehub/icons/es/Mistral/components/Mono";
|
||||
import MoonshotMonoIcon from "@lobehub/icons/es/Moonshot/components/Mono";
|
||||
import MorphColorIcon from "@lobehub/icons/es/Morph/components/Color";
|
||||
import MorphMonoIcon from "@lobehub/icons/es/Morph/components/Mono";
|
||||
import NanoBananaColorIcon from "@lobehub/icons/es/NanoBanana/components/Color";
|
||||
import NanoBananaMonoIcon from "@lobehub/icons/es/NanoBanana/components/Mono";
|
||||
import NebiusMonoIcon from "@lobehub/icons/es/Nebius/components/Mono";
|
||||
import NousResearchMonoIcon from "@lobehub/icons/es/NousResearch/components/Mono";
|
||||
import NovitaColorIcon from "@lobehub/icons/es/Novita/components/Color";
|
||||
import NovitaMonoIcon from "@lobehub/icons/es/Novita/components/Mono";
|
||||
import NvidiaColorIcon from "@lobehub/icons/es/Nvidia/components/Color";
|
||||
import NvidiaMonoIcon from "@lobehub/icons/es/Nvidia/components/Mono";
|
||||
import OllamaMonoIcon from "@lobehub/icons/es/Ollama/components/Mono";
|
||||
import OpenAIMonoIcon from "@lobehub/icons/es/OpenAI/components/Mono";
|
||||
import OpenClawColorIcon from "@lobehub/icons/es/OpenClaw/components/Color";
|
||||
import OpenClawMonoIcon from "@lobehub/icons/es/OpenClaw/components/Mono";
|
||||
import OpenCodeMonoIcon from "@lobehub/icons/es/OpenCode/components/Mono";
|
||||
import OpenRouterMonoIcon from "@lobehub/icons/es/OpenRouter/components/Mono";
|
||||
import PerplexityColorIcon from "@lobehub/icons/es/Perplexity/components/Color";
|
||||
import PerplexityMonoIcon from "@lobehub/icons/es/Perplexity/components/Mono";
|
||||
import PoeColorIcon from "@lobehub/icons/es/Poe/components/Color";
|
||||
import PoeMonoIcon from "@lobehub/icons/es/Poe/components/Mono";
|
||||
import QoderColorIcon from "@lobehub/icons/es/Qoder/components/Color";
|
||||
import QoderMonoIcon from "@lobehub/icons/es/Qoder/components/Mono";
|
||||
import QwenColorIcon from "@lobehub/icons/es/Qwen/components/Color";
|
||||
import QwenMonoIcon from "@lobehub/icons/es/Qwen/components/Mono";
|
||||
import RecraftMonoIcon from "@lobehub/icons/es/Recraft/components/Mono";
|
||||
import ReplicateMonoIcon from "@lobehub/icons/es/Replicate/components/Mono";
|
||||
import RunwayMonoIcon from "@lobehub/icons/es/Runway/components/Mono";
|
||||
import SambaNovaColorIcon from "@lobehub/icons/es/SambaNova/components/Color";
|
||||
import SambaNovaMonoIcon from "@lobehub/icons/es/SambaNova/components/Mono";
|
||||
import SearchApiMonoIcon from "@lobehub/icons/es/SearchApi/components/Mono";
|
||||
import SiliconCloudColorIcon from "@lobehub/icons/es/SiliconCloud/components/Color";
|
||||
import SiliconCloudMonoIcon from "@lobehub/icons/es/SiliconCloud/components/Mono";
|
||||
import SnowflakeColorIcon from "@lobehub/icons/es/Snowflake/components/Color";
|
||||
import SnowflakeMonoIcon from "@lobehub/icons/es/Snowflake/components/Mono";
|
||||
import StabilityColorIcon from "@lobehub/icons/es/Stability/components/Color";
|
||||
import StabilityMonoIcon from "@lobehub/icons/es/Stability/components/Mono";
|
||||
import TavilyColorIcon from "@lobehub/icons/es/Tavily/components/Color";
|
||||
import TavilyMonoIcon from "@lobehub/icons/es/Tavily/components/Mono";
|
||||
import TogetherColorIcon from "@lobehub/icons/es/Together/components/Color";
|
||||
import TogetherMonoIcon from "@lobehub/icons/es/Together/components/Mono";
|
||||
import TopazLabsMonoIcon from "@lobehub/icons/es/TopazLabs/components/Mono";
|
||||
import UpstageColorIcon from "@lobehub/icons/es/Upstage/components/Color";
|
||||
import UpstageMonoIcon from "@lobehub/icons/es/Upstage/components/Mono";
|
||||
import V0MonoIcon from "@lobehub/icons/es/V0/components/Mono";
|
||||
import VeniceColorIcon from "@lobehub/icons/es/Venice/components/Color";
|
||||
import VeniceMonoIcon from "@lobehub/icons/es/Venice/components/Mono";
|
||||
import VercelMonoIcon from "@lobehub/icons/es/Vercel/components/Mono";
|
||||
import VertexAIColorIcon from "@lobehub/icons/es/VertexAI/components/Color";
|
||||
import VertexAIMonoIcon from "@lobehub/icons/es/VertexAI/components/Mono";
|
||||
import VllmColorIcon from "@lobehub/icons/es/Vllm/components/Color";
|
||||
import VllmMonoIcon from "@lobehub/icons/es/Vllm/components/Mono";
|
||||
import VolcengineColorIcon from "@lobehub/icons/es/Volcengine/components/Color";
|
||||
import VolcengineMonoIcon from "@lobehub/icons/es/Volcengine/components/Mono";
|
||||
import VoyageColorIcon from "@lobehub/icons/es/Voyage/components/Color";
|
||||
import VoyageMonoIcon from "@lobehub/icons/es/Voyage/components/Mono";
|
||||
import WorkersAIColorIcon from "@lobehub/icons/es/WorkersAI/components/Color";
|
||||
import WorkersAIMonoIcon from "@lobehub/icons/es/WorkersAI/components/Mono";
|
||||
import XAIMonoIcon from "@lobehub/icons/es/XAI/components/Mono";
|
||||
import XiaomiMiMoMonoIcon from "@lobehub/icons/es/XiaomiMiMo/components/Mono";
|
||||
import XinferenceColorIcon from "@lobehub/icons/es/Xinference/components/Color";
|
||||
import XinferenceMonoIcon from "@lobehub/icons/es/Xinference/components/Mono";
|
||||
import ZAIMonoIcon from "@lobehub/icons/es/ZAI/components/Mono";
|
||||
import ZhipuColorIcon from "@lobehub/icons/es/Zhipu/components/Color";
|
||||
import ZhipuMonoIcon from "@lobehub/icons/es/Zhipu/components/Mono";
|
||||
|
||||
type LobeIconComponent = ElementType<{
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
size?: number | string;
|
||||
style?: CSSProperties;
|
||||
}>;
|
||||
|
||||
type LobeIconEntry = {
|
||||
color?: LobeIconComponent;
|
||||
mono: LobeIconComponent;
|
||||
};
|
||||
|
||||
const LOBE_ICON_COMPONENTS = {
|
||||
Ai21: { mono: Ai21MonoIcon },
|
||||
Alibaba: { mono: AlibabaMonoIcon, color: AlibabaColorIcon },
|
||||
Anthropic: { mono: AnthropicMonoIcon },
|
||||
Antigravity: { mono: AntigravityMonoIcon, color: AntigravityColorIcon },
|
||||
AssemblyAI: { mono: AssemblyAIMonoIcon, color: AssemblyAIColorIcon },
|
||||
Automatic: { mono: AutomaticMonoIcon, color: AutomaticColorIcon },
|
||||
Aws: { mono: AwsMonoIcon, color: AwsColorIcon },
|
||||
Azure: { mono: AzureMonoIcon, color: AzureColorIcon },
|
||||
AzureAI: { mono: AzureAIMonoIcon, color: AzureAIColorIcon },
|
||||
Baidu: { mono: BaiduMonoIcon, color: BaiduColorIcon },
|
||||
Bailian: { mono: BailianMonoIcon, color: BailianColorIcon },
|
||||
Baseten: { mono: BasetenMonoIcon },
|
||||
Bedrock: { mono: BedrockMonoIcon, color: BedrockColorIcon },
|
||||
Bfl: { mono: BflMonoIcon },
|
||||
Cerebras: { mono: CerebrasMonoIcon, color: CerebrasColorIcon },
|
||||
ClaudeCode: { mono: ClaudeCodeMonoIcon, color: ClaudeCodeColorIcon },
|
||||
Cline: { mono: ClineMonoIcon },
|
||||
Cloudflare: { mono: CloudflareMonoIcon, color: CloudflareColorIcon },
|
||||
Codex: { mono: CodexMonoIcon, color: CodexColorIcon },
|
||||
Cohere: { mono: CohereMonoIcon, color: CohereColorIcon },
|
||||
ComfyUI: { mono: ComfyUIMonoIcon, color: ComfyUIColorIcon },
|
||||
Cursor: { mono: CursorMonoIcon },
|
||||
Dbrx: { mono: DbrxMonoIcon, color: DbrxColorIcon },
|
||||
DeepInfra: { mono: DeepInfraMonoIcon, color: DeepInfraColorIcon },
|
||||
DeepSeek: { mono: DeepSeekMonoIcon, color: DeepSeekColorIcon },
|
||||
ElevenLabs: { mono: ElevenLabsMonoIcon },
|
||||
Exa: { mono: ExaMonoIcon, color: ExaColorIcon },
|
||||
Fal: { mono: FalMonoIcon, color: FalColorIcon },
|
||||
Featherless: { mono: FeatherlessMonoIcon, color: FeatherlessColorIcon },
|
||||
Fireworks: { mono: FireworksMonoIcon, color: FireworksColorIcon },
|
||||
Friendli: { mono: FriendliMonoIcon },
|
||||
Gemini: { mono: GeminiMonoIcon, color: GeminiColorIcon },
|
||||
GeminiCLI: { mono: GeminiCLIMonoIcon, color: GeminiCLIColorIcon },
|
||||
GithubCopilot: { mono: GithubCopilotMonoIcon },
|
||||
Google: { mono: GoogleMonoIcon, color: GoogleColorIcon },
|
||||
Grok: { mono: GrokMonoIcon },
|
||||
Groq: { mono: GroqMonoIcon },
|
||||
HuggingFace: { mono: HuggingFaceMonoIcon, color: HuggingFaceColorIcon },
|
||||
Hyperbolic: { mono: HyperbolicMonoIcon, color: HyperbolicColorIcon },
|
||||
IBM: { mono: IBMMonoIcon },
|
||||
Inference: { mono: InferenceMonoIcon },
|
||||
Jina: { mono: JinaMonoIcon },
|
||||
KiloCode: { mono: KiloCodeMonoIcon },
|
||||
Kimi: { mono: KimiMonoIcon, color: KimiColorIcon },
|
||||
Lambda: { mono: LambdaMonoIcon },
|
||||
LmStudio: { mono: LmStudioMonoIcon },
|
||||
Meta: { mono: MetaMonoIcon, color: MetaColorIcon },
|
||||
MetaAI: { mono: MetaAIMonoIcon, color: MetaAIColorIcon },
|
||||
Minimax: { mono: MinimaxMonoIcon, color: MinimaxColorIcon },
|
||||
Mistral: { mono: MistralMonoIcon, color: MistralColorIcon },
|
||||
Moonshot: { mono: MoonshotMonoIcon },
|
||||
Morph: { mono: MorphMonoIcon, color: MorphColorIcon },
|
||||
NanoBanana: { mono: NanoBananaMonoIcon, color: NanoBananaColorIcon },
|
||||
Nebius: { mono: NebiusMonoIcon },
|
||||
NousResearch: { mono: NousResearchMonoIcon },
|
||||
Novita: { mono: NovitaMonoIcon, color: NovitaColorIcon },
|
||||
Nvidia: { mono: NvidiaMonoIcon, color: NvidiaColorIcon },
|
||||
Ollama: { mono: OllamaMonoIcon },
|
||||
OpenAI: { mono: OpenAIMonoIcon },
|
||||
OpenClaw: { mono: OpenClawMonoIcon, color: OpenClawColorIcon },
|
||||
OpenCode: { mono: OpenCodeMonoIcon },
|
||||
OpenRouter: { mono: OpenRouterMonoIcon },
|
||||
Perplexity: { mono: PerplexityMonoIcon, color: PerplexityColorIcon },
|
||||
Poe: { mono: PoeMonoIcon, color: PoeColorIcon },
|
||||
Qoder: { mono: QoderMonoIcon, color: QoderColorIcon },
|
||||
Qwen: { mono: QwenMonoIcon, color: QwenColorIcon },
|
||||
Recraft: { mono: RecraftMonoIcon },
|
||||
Replicate: { mono: ReplicateMonoIcon },
|
||||
Runway: { mono: RunwayMonoIcon },
|
||||
SambaNova: { mono: SambaNovaMonoIcon, color: SambaNovaColorIcon },
|
||||
SearchApi: { mono: SearchApiMonoIcon },
|
||||
SiliconCloud: { mono: SiliconCloudMonoIcon, color: SiliconCloudColorIcon },
|
||||
Snowflake: { mono: SnowflakeMonoIcon, color: SnowflakeColorIcon },
|
||||
Stability: { mono: StabilityMonoIcon, color: StabilityColorIcon },
|
||||
Tavily: { mono: TavilyMonoIcon, color: TavilyColorIcon },
|
||||
Together: { mono: TogetherMonoIcon, color: TogetherColorIcon },
|
||||
TopazLabs: { mono: TopazLabsMonoIcon },
|
||||
Upstage: { mono: UpstageMonoIcon, color: UpstageColorIcon },
|
||||
V0: { mono: V0MonoIcon },
|
||||
Venice: { mono: VeniceMonoIcon, color: VeniceColorIcon },
|
||||
Vercel: { mono: VercelMonoIcon },
|
||||
VertexAI: { mono: VertexAIMonoIcon, color: VertexAIColorIcon },
|
||||
Vllm: { mono: VllmMonoIcon, color: VllmColorIcon },
|
||||
Volcengine: { mono: VolcengineMonoIcon, color: VolcengineColorIcon },
|
||||
Voyage: { mono: VoyageMonoIcon, color: VoyageColorIcon },
|
||||
WorkersAI: { mono: WorkersAIMonoIcon, color: WorkersAIColorIcon },
|
||||
XAI: { mono: XAIMonoIcon },
|
||||
XiaomiMiMo: { mono: XiaomiMiMoMonoIcon },
|
||||
Xinference: { mono: XinferenceMonoIcon, color: XinferenceColorIcon },
|
||||
ZAI: { mono: ZAIMonoIcon },
|
||||
Zhipu: { mono: ZhipuMonoIcon, color: ZhipuColorIcon },
|
||||
} satisfies Record<string, LobeIconEntry>;
|
||||
|
||||
const LOBE_PROVIDER_ALIASES = {
|
||||
ai21: "Ai21",
|
||||
alibaba: "Alibaba",
|
||||
alicode: "Alibaba",
|
||||
"alicode-intl": "Alibaba",
|
||||
"amazon-q": "Aws",
|
||||
anthropic: "Anthropic",
|
||||
antigravity: "Antigravity",
|
||||
assemblyai: "AssemblyAI",
|
||||
"aws-polly": "Aws",
|
||||
azure: "Azure",
|
||||
"azure-ai": "AzureAI",
|
||||
"azure-openai": "AzureAI",
|
||||
baidu: "Baidu",
|
||||
"bailian-coding-plan": "Bailian",
|
||||
baseten: "Baseten",
|
||||
bedrock: "Bedrock",
|
||||
bfl: "Bfl",
|
||||
"black-forest-labs": "Bfl",
|
||||
cerebras: "Cerebras",
|
||||
claude: "ClaudeCode",
|
||||
cline: "Cline",
|
||||
cloudflare: "Cloudflare",
|
||||
"cloudflare-ai": "WorkersAI",
|
||||
codestral: "Mistral",
|
||||
codex: "Codex",
|
||||
cohere: "Cohere",
|
||||
comfyui: "ComfyUI",
|
||||
copilot: "GithubCopilot",
|
||||
cursor: "Cursor",
|
||||
databricks: "Dbrx",
|
||||
deepinfra: "DeepInfra",
|
||||
deepseek: "DeepSeek",
|
||||
elevenlabs: "ElevenLabs",
|
||||
exa: "Exa",
|
||||
"exa-search": "Exa",
|
||||
fal: "Fal",
|
||||
"fal-ai": "Fal",
|
||||
featherless: "Featherless",
|
||||
"featherless-ai": "Featherless",
|
||||
fireworks: "Fireworks",
|
||||
"fireworks-ai": "Fireworks",
|
||||
friendli: "Friendli",
|
||||
friendliai: "Friendli",
|
||||
gemini: "Gemini",
|
||||
"gemini-cli": "GeminiCLI",
|
||||
github: "GithubCopilot",
|
||||
"github-copilot": "GithubCopilot",
|
||||
glm: "Zhipu",
|
||||
"glm-cn": "Zhipu",
|
||||
glmt: "Zhipu",
|
||||
"google-pse-search": "Google",
|
||||
grok: "Grok",
|
||||
"grok-web": "Grok",
|
||||
groq: "Groq",
|
||||
"hugging-face": "HuggingFace",
|
||||
huggingface: "HuggingFace",
|
||||
hyperbolic: "Hyperbolic",
|
||||
ibm: "IBM",
|
||||
"inference-net": "Inference",
|
||||
jina: "Jina",
|
||||
"jina-ai": "Jina",
|
||||
kilocode: "KiloCode",
|
||||
kimi: "Kimi",
|
||||
"kimi-coding": "Kimi",
|
||||
"kimi-coding-apikey": "Kimi",
|
||||
lambda: "Lambda",
|
||||
"lambda-ai": "Lambda",
|
||||
"lm-studio": "LmStudio",
|
||||
lmstudio: "LmStudio",
|
||||
"meta-llama": "Meta",
|
||||
minimax: "Minimax",
|
||||
"minimax-cn": "Minimax",
|
||||
mistral: "Mistral",
|
||||
mistralai: "Mistral",
|
||||
moonshot: "Moonshot",
|
||||
morph: "Morph",
|
||||
"muse-spark-web": "MetaAI",
|
||||
nanobanana: "NanoBanana",
|
||||
nebius: "Nebius",
|
||||
"nous-research": "NousResearch",
|
||||
nousresearch: "NousResearch",
|
||||
novita: "Novita",
|
||||
nvidia: "Nvidia",
|
||||
ollama: "Ollama",
|
||||
"ollama-cloud": "Ollama",
|
||||
openai: "OpenAI",
|
||||
openclaw: "OpenClaw",
|
||||
opencode: "OpenCode",
|
||||
"opencode-go": "OpenCode",
|
||||
"opencode-zen": "OpenCode",
|
||||
"open-router": "OpenRouter",
|
||||
openrouter: "OpenRouter",
|
||||
perplexity: "Perplexity",
|
||||
"perplexity-search": "Perplexity",
|
||||
"perplexity-web": "Perplexity",
|
||||
poe: "Poe",
|
||||
qoder: "Qoder",
|
||||
qwen: "Qwen",
|
||||
recraft: "Recraft",
|
||||
replicate: "Replicate",
|
||||
runwayml: "Runway",
|
||||
sambanova: "SambaNova",
|
||||
sdwebui: "Automatic",
|
||||
searchapi: "SearchApi",
|
||||
"searchapi-search": "SearchApi",
|
||||
siliconflow: "SiliconCloud",
|
||||
snowflake: "Snowflake",
|
||||
stability: "Stability",
|
||||
"stability-ai": "Stability",
|
||||
tavily: "Tavily",
|
||||
"tavily-search": "Tavily",
|
||||
together: "Together",
|
||||
topaz: "TopazLabs",
|
||||
upstage: "Upstage",
|
||||
v0: "V0",
|
||||
"v0-vercel": "V0",
|
||||
venice: "Venice",
|
||||
"vercel-ai-gateway": "Vercel",
|
||||
vertex: "VertexAI",
|
||||
"vertex-partner": "VertexAI",
|
||||
vertexai: "VertexAI",
|
||||
vllm: "Vllm",
|
||||
volcengine: "Volcengine",
|
||||
voyage: "Voyage",
|
||||
"voyage-ai": "Voyage",
|
||||
watsonx: "IBM",
|
||||
"workers-ai": "WorkersAI",
|
||||
workersai: "WorkersAI",
|
||||
xai: "XAI",
|
||||
"xiaomi-mimo": "XiaomiMiMo",
|
||||
xiaomimimo: "XiaomiMiMo",
|
||||
xinference: "Xinference",
|
||||
zai: "ZAI",
|
||||
zhipu: "Zhipu",
|
||||
} satisfies Record<string, keyof typeof LOBE_ICON_COMPONENTS>;
|
||||
|
||||
export function getLobeProviderIcon(
|
||||
providerId: string,
|
||||
type: "mono" | "color" = "color"
|
||||
): LobeIconComponent | null {
|
||||
const iconKey = LOBE_PROVIDER_ALIASES[providerId.toLowerCase()];
|
||||
if (!iconKey) return null;
|
||||
|
||||
const entry = LOBE_ICON_COMPONENTS[iconKey];
|
||||
return type === "color" && entry.color ? entry.color : entry.mono;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue