feat: add frontend lint checks to CI

- Add ESLint config (eslint.config.js) for ESLint 9
- Add ESLint dependencies to package.json
- Update CI workflow to include:
  - ESLint check
  - Prettier format check
- Fix lint errors:
  - Remove unused catch variable in Logs.tsx
  - Add eslint-disable for intentional useEffect dependency
- Run prettier to format all source files
This commit is contained in:
coso
2025-12-14 10:51:48 +08:00
parent d25ca669db
commit fa69ceeaee
13 changed files with 1988 additions and 199 deletions
+6
View File
@@ -62,6 +62,12 @@ jobs:
- name: TypeScript check
run: npx tsc --noEmit
- name: ESLint check
run: npm run lint
- name: Prettier check
run: npx prettier --check "src/**/*.{ts,tsx,css}"
build-check:
name: Build Check
runs-on: ubuntu-latest
+34
View File
@@ -0,0 +1,34 @@
import js from "@eslint/js";
import tseslint from "@typescript-eslint/eslint-plugin";
import tsparser from "@typescript-eslint/parser";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
export default [
{ ignores: ["dist", "src-tauri", "node_modules"] },
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parser: tsparser,
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
plugins: {
"@typescript-eslint": tseslint,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...tseslint.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "off",
},
},
];
+1402 -2
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -8,7 +8,7 @@
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint": "eslint src --max-warnings 0",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\""
},
"dependencies": {
@@ -31,12 +31,19 @@
"tailwind-merge": "^2.5.4"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@tauri-apps/cli": "^2.0.0",
"@types/node": "^22.9.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.15.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.12.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.14",
+93 -35
View File
@@ -1,5 +1,15 @@
import { useState, useEffect } from "react";
import { Activity, Server, Zap, Clock, Play, Copy, Check, ChevronDown, ChevronUp } from "lucide-react";
import {
Activity,
Server,
Zap,
Clock,
Play,
Copy,
Check,
ChevronDown,
ChevronUp,
} from "lucide-react";
import {
startServer,
stopServer,
@@ -84,9 +94,12 @@ export function Dashboard() {
return `${h}h ${m}m`;
};
const serverUrl = status ? `http://${status.host}:${status.port}` : "http://localhost:3001";
const serverUrl = status
? `http://${status.host}:${status.port}`
: "http://localhost:3001";
const apiKey = config?.server.api_key || "proxycast-key";
const maskedKey = apiKey.length > 8 ? apiKey.slice(0, 4) + "****" + apiKey.slice(-4) : "****";
const maskedKey =
apiKey.length > 8 ? apiKey.slice(0, 4) + "****" + apiKey.slice(-4) : "****";
// 测试端点配置
const testEndpoints = [
@@ -126,12 +139,17 @@ export function Dashboard() {
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 100,
messages: [{ role: "user", content: "What is 1+1? Answer with just the number." }],
messages: [
{
role: "user",
content: "What is 1+1? Answer with just the number.",
},
],
}),
},
];
const runTest = async (endpoint: typeof testEndpoints[0]) => {
const runTest = async (endpoint: (typeof testEndpoints)[0]) => {
setTestResults((prev) => ({
...prev,
[endpoint.id]: { endpoint: endpoint.path, status: "loading" },
@@ -142,7 +160,7 @@ export function Dashboard() {
endpoint.method,
endpoint.path,
endpoint.body,
endpoint.needsAuth // maps to 'auth' parameter
endpoint.needsAuth, // maps to 'auth' parameter
);
// 添加调试日志
@@ -177,7 +195,7 @@ export function Dashboard() {
}
};
const getCurlCommand = (endpoint: typeof testEndpoints[0]) => {
const getCurlCommand = (endpoint: (typeof testEndpoints)[0]) => {
let cmd = `curl -s ${serverUrl}${endpoint.path}`;
if (endpoint.needsAuth) {
cmd += ` \\\n -H "Authorization: Bearer ${apiKey}"`;
@@ -203,15 +221,11 @@ export function Dashboard() {
return <span className="text-xs text-blue-500">...</span>;
}
if (result.status === "success") {
return (
<span className="text-xs text-green-600">
{result.time}ms
</span>
);
return <span className="text-xs text-green-600"> {result.time}ms</span>;
}
return (
<span className="text-xs text-red-500">
{result.httpStatus ? `(${result.httpStatus})` : ''}
{result.httpStatus ? `(${result.httpStatus})` : ""}
</span>
);
};
@@ -231,8 +245,12 @@ export function Dashboard() {
<span className="text-sm text-muted-foreground"></span>
</div>
<div className="mt-2 flex items-center gap-2">
<div className={`h-2 w-2 rounded-full ${status?.running ? "bg-green-500" : "bg-red-500"}`} />
<span className="font-medium">{status?.running ? "运行中" : "已停止"}</span>
<div
className={`h-2 w-2 rounded-full ${status?.running ? "bg-green-500" : "bg-red-500"}`}
/>
<span className="font-medium">
{status?.running ? "运行中" : "已停止"}
</span>
</div>
</div>
@@ -249,7 +267,9 @@ export function Dashboard() {
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground"></span>
</div>
<div className="mt-2 font-medium">{formatUptime(status?.uptime_secs || 0)}</div>
<div className="mt-2 font-medium">
{formatUptime(status?.uptime_secs || 0)}
</div>
</div>
<div className="rounded-lg border bg-card p-4">
@@ -262,7 +282,9 @@ export function Dashboard() {
</div>
{error && (
<div className="rounded-lg border border-red-500 bg-red-50 p-4 text-red-700">{error}</div>
<div className="rounded-lg border border-red-500 bg-red-50 p-4 text-red-700">
{error}
</div>
)}
{/* Server Control */}
@@ -285,8 +307,14 @@ export function Dashboard() {
</button>
</div>
<div className="mt-4 flex items-center gap-4 text-sm text-muted-foreground">
<span>API : <code className="rounded bg-muted px-2 py-1">{serverUrl}</code></span>
<span>API Key: <code className="rounded bg-muted px-2 py-1">{maskedKey}</code></span>
<span>
API :{" "}
<code className="rounded bg-muted px-2 py-1">{serverUrl}</code>
</span>
<span>
API Key:{" "}
<code className="rounded bg-muted px-2 py-1">{maskedKey}</code>
</span>
</div>
</div>
@@ -316,16 +344,25 @@ export function Dashboard() {
const curlCmd = getCurlCommand(endpoint);
return (
<div key={endpoint.id} className="rounded-lg border bg-background">
<div
key={endpoint.id}
className="rounded-lg border bg-background"
>
<div className="flex items-center justify-between p-3">
<div className="flex items-center gap-3">
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
endpoint.method === "GET" ? "bg-green-100 text-green-700" : "bg-blue-100 text-blue-700"
}`}>
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${
endpoint.method === "GET"
? "bg-green-100 text-green-700"
: "bg-blue-100 text-blue-700"
}`}
>
{endpoint.method}
</span>
<span className="font-medium">{endpoint.name}</span>
<code className="text-xs text-muted-foreground">{endpoint.path}</code>
<code className="text-xs text-muted-foreground">
{endpoint.path}
</code>
{getStatusBadge(result)}
</div>
<div className="flex items-center gap-2">
@@ -342,16 +379,24 @@ export function Dashboard() {
</button>
<button
onClick={() => runTest(endpoint)}
disabled={!status?.running || result?.status === "loading"}
disabled={
!status?.running || result?.status === "loading"
}
className="rounded bg-primary/10 px-2 py-1 text-xs font-medium text-primary hover:bg-primary/20 disabled:opacity-50"
>
</button>
<button
onClick={() => setExpandedTest(isExpanded ? null : endpoint.id)}
onClick={() =>
setExpandedTest(isExpanded ? null : endpoint.id)
}
className="rounded p-1.5 hover:bg-muted"
>
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
</div>
</div>
@@ -359,22 +404,35 @@ export function Dashboard() {
{isExpanded && (
<div className="border-t p-3 space-y-3">
<div>
<p className="mb-1 text-xs font-medium text-muted-foreground">curl </p>
<pre className="rounded bg-muted p-2 text-xs overflow-x-auto">{curlCmd}</pre>
<p className="mb-1 text-xs font-medium text-muted-foreground">
curl
</p>
<pre className="rounded bg-muted p-2 text-xs overflow-x-auto">
{curlCmd}
</pre>
</div>
{result?.response && (
<div>
<p className="mb-1 text-xs font-medium text-muted-foreground">
{result.httpStatus && `(HTTP ${result.httpStatus})`}
{" "}
{result.httpStatus && `(HTTP ${result.httpStatus})`}
</p>
<pre className={`rounded p-2 text-xs overflow-x-auto max-h-40 ${
result.status === "success" ? "bg-green-50" : "bg-red-50"
}`}>
<pre
className={`rounded p-2 text-xs overflow-x-auto max-h-40 ${
result.status === "success"
? "bg-green-50"
: "bg-red-50"
}`}
>
{(() => {
try {
return JSON.stringify(JSON.parse(result.response), null, 2);
return JSON.stringify(
JSON.parse(result.response),
null,
2,
);
} catch {
return result.response || '(空响应)';
return result.response || "(空响应)";
}
})()}
</pre>
+18 -8
View File
@@ -33,16 +33,19 @@ export function Logs() {
try {
await clearLogs();
setLogs([]);
} catch (e) {
} catch {
setLogs([]);
}
};
const handleExport = () => {
const content = logs.map(l =>
`[${new Date(l.timestamp).toLocaleString()}] [${l.level.toUpperCase()}] ${l.message}`
).join("\n");
const content = logs
.map(
(l) =>
`[${new Date(l.timestamp).toLocaleString()}] [${l.level.toUpperCase()}] ${l.message}`,
)
.join("\n");
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
@@ -113,14 +116,21 @@ export function Logs() {
<div className="rounded-lg border bg-card">
<div className="max-h-[600px] overflow-auto p-4 font-mono text-sm">
{logs.length === 0 ? (
<p className="text-center text-muted-foreground"></p>
<p className="text-center text-muted-foreground">
</p>
) : (
logs.map((log, i) => (
<div key={i} className={`flex gap-2 py-1 px-2 rounded ${getLevelBg(log.level)}`}>
<div
key={i}
className={`flex gap-2 py-1 px-2 rounded ${getLevelBg(log.level)}`}
>
<span className="text-muted-foreground shrink-0">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span className={`font-medium shrink-0 ${getLevelColor(log.level)}`}>
<span
className={`font-medium shrink-0 ${getLevelColor(log.level)}`}
>
[{log.level.toUpperCase()}]
</span>
<span className="break-all">{log.message}</span>
+42 -20
View File
@@ -3,7 +3,10 @@ import { Cpu, RefreshCw, Copy, Check, Search } from "lucide-react";
import { getAvailableModels, ModelInfo } from "@/hooks/useTauri";
// 模型分组配置
const MODEL_GROUPS: Record<string, { name: string; color: string; models: string[] }> = {
const MODEL_GROUPS: Record<
string,
{ name: string; color: string; models: string[] }
> = {
kiro: {
name: "Kiro Claude",
color: "bg-purple-100 text-purple-700",
@@ -70,7 +73,7 @@ export function Models() {
const fetchModels = async () => {
setLoading(true);
setError(null);
try {
const data = await getAvailableModels();
setModels(data || []);
@@ -78,7 +81,7 @@ export function Models() {
setError(e.toString());
setModels([]);
}
setLoading(false);
};
@@ -90,12 +93,16 @@ export function Models() {
const getModelGroup = (modelId: string): string | null => {
for (const [groupId, group] of Object.entries(MODEL_GROUPS)) {
if (group.models.some(m => modelId.toLowerCase().includes(m.toLowerCase().split("-")[0]))) {
if (
group.models.some((m) =>
modelId.toLowerCase().includes(m.toLowerCase().split("-")[0]),
)
) {
return groupId;
}
}
// 根据 owned_by 判断
const model = models.find(m => m.id === modelId);
const model = models.find((m) => m.id === modelId);
if (model?.owned_by === "anthropic") return "kiro";
if (model?.owned_by === "google") return "gemini";
if (model?.owned_by === "alibaba") return "qwen";
@@ -107,27 +114,33 @@ export function Models() {
if (!groupId || !MODEL_GROUPS[groupId]) return null;
const group = MODEL_GROUPS[groupId];
return (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${group.color}`}>
<span
className={`rounded px-2 py-0.5 text-xs font-medium ${group.color}`}
>
{group.name}
</span>
);
};
// 过滤模型
const filteredModels = models.filter(model => {
const filteredModels = models.filter((model) => {
const matchesSearch = model.id.toLowerCase().includes(search.toLowerCase());
const matchesGroup = !selectedGroup || getModelGroup(model.id) === selectedGroup;
const matchesGroup =
!selectedGroup || getModelGroup(model.id) === selectedGroup;
return matchesSearch && matchesGroup;
});
// 按 provider 分组统计
const groupCounts = models.reduce((acc, model) => {
const group = getModelGroup(model.id);
if (group) {
acc[group] = (acc[group] || 0) + 1;
}
return acc;
}, {} as Record<string, number>);
const groupCounts = models.reduce(
(acc, model) => {
const group = getModelGroup(model.id);
if (group) {
acc[group] = (acc[group] || 0) + 1;
}
return acc;
},
{} as Record<string, number>,
);
return (
<div className="space-y-6">
@@ -169,7 +182,9 @@ export function Models() {
<button
onClick={() => setSelectedGroup(null)}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
!selectedGroup ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
!selectedGroup
? "bg-primary text-primary-foreground"
: "bg-muted hover:bg-muted/80"
}`}
>
({models.length})
@@ -180,9 +195,13 @@ export function Models() {
return (
<button
key={groupId}
onClick={() => setSelectedGroup(selectedGroup === groupId ? null : groupId)}
onClick={() =>
setSelectedGroup(selectedGroup === groupId ? null : groupId)
}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
selectedGroup === groupId ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
selectedGroup === groupId
? "bg-primary text-primary-foreground"
: "bg-muted hover:bg-muted/80"
}`}
>
{group.name} ({count})
@@ -201,7 +220,7 @@ export function Models() {
</span>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
@@ -252,7 +271,10 @@ export function Models() {
<h3 className="mb-2 font-semibold">使</h3>
<div className="space-y-2 text-sm text-muted-foreground">
<p> ID </p>
<p> API 使 <code className="rounded bg-muted px-1">model</code> </p>
<p>
API 使{" "}
<code className="rounded bg-muted px-1">model</code>
</p>
<p> Provider </p>
</div>
</div>
+321 -104
View File
@@ -1,8 +1,19 @@
import { useState, useEffect, useRef } from "react";
import { Check, X, RefreshCw, FolderOpen, AlertCircle, CheckCircle2, Eye, EyeOff, Copy, FileText } from "lucide-react";
import {
reloadCredentials,
refreshKiroToken,
import {
Check,
X,
RefreshCw,
FolderOpen,
AlertCircle,
CheckCircle2,
Eye,
EyeOff,
Copy,
FileText,
} from "lucide-react";
import {
reloadCredentials,
refreshKiroToken,
getKiroCredentials,
getEnvVariables,
getTokenFileHash,
@@ -46,50 +57,92 @@ interface Provider {
}
const defaultProviders: Provider[] = [
{ id: "kiro", name: "Kiro Claude", enabled: true, status: "disconnected", description: "通过 Kiro OAuth 访问 Claude Sonnet 4.5" },
{ id: "gemini", name: "Gemini CLI", enabled: true, status: "disconnected", description: "通过 Gemini CLI OAuth 访问 Gemini 模型" },
{ id: "qwen", name: "通义千问", enabled: true, status: "disconnected", description: "通过 Qwen OAuth 访问通义千问" },
{ id: "openai", name: "OpenAI 自定义", enabled: false, status: "disconnected", description: "自定义 OpenAI 兼容 API" },
{ id: "claude", name: "Claude 自定义", enabled: false, status: "disconnected", description: "自定义 Claude API" },
{
id: "kiro",
name: "Kiro Claude",
enabled: true,
status: "disconnected",
description: "通过 Kiro OAuth 访问 Claude Sonnet 4.5",
},
{
id: "gemini",
name: "Gemini CLI",
enabled: true,
status: "disconnected",
description: "通过 Gemini CLI OAuth 访问 Gemini 模型",
},
{
id: "qwen",
name: "通义千问",
enabled: true,
status: "disconnected",
description: "通过 Qwen OAuth 访问通义千问",
},
{
id: "openai",
name: "OpenAI 自定义",
enabled: false,
status: "disconnected",
description: "自定义 OpenAI 兼容 API",
},
{
id: "claude",
name: "Claude 自定义",
enabled: false,
status: "disconnected",
description: "自定义 Claude API",
},
];
export function Providers() {
const [providers, setProviders] = useState<Provider[]>(defaultProviders);
const [activeProvider, setActiveProvider] = useState<string>("kiro");
// Kiro state
const [kiroStatus, setKiroStatus] = useState<KiroCredentialStatus | null>(null);
const [kiroStatus, setKiroStatus] = useState<KiroCredentialStatus | null>(
null,
);
const [kiroEnvVars, setKiroEnvVars] = useState<EnvVariable[]>([]);
const kiroHashRef = useRef<string>("");
// Gemini state
const [geminiStatus, setGeminiStatus] = useState<GeminiCredentialStatus | null>(null);
const [geminiStatus, setGeminiStatus] =
useState<GeminiCredentialStatus | null>(null);
const [geminiEnvVars, setGeminiEnvVars] = useState<EnvVariable[]>([]);
const geminiHashRef = useRef<string>("");
// Qwen state
const [qwenStatus, setQwenStatus] = useState<QwenCredentialStatus | null>(null);
const [qwenStatus, setQwenStatus] = useState<QwenCredentialStatus | null>(
null,
);
const [qwenEnvVars, setQwenEnvVars] = useState<EnvVariable[]>([]);
const qwenHashRef = useRef<string>("");
// OpenAI Custom state
const [openaiStatus, setOpenaiStatus] = useState<OpenAICustomStatus | null>(null);
const [openaiStatus, setOpenaiStatus] = useState<OpenAICustomStatus | null>(
null,
);
const [openaiApiKey, setOpenaiApiKey] = useState("");
const [openaiBaseUrl, setOpenaiBaseUrl] = useState("");
// Claude Custom state
const [claudeStatus, setClaudeStatus] = useState<ClaudeCustomStatus | null>(null);
const [claudeStatus, setClaudeStatus] = useState<ClaudeCustomStatus | null>(
null,
);
const [claudeApiKey, setClaudeApiKey] = useState("");
const [claudeBaseUrl, setClaudeBaseUrl] = useState("");
// Default provider state
const [defaultProvider, setDefaultProviderState] = useState<string>("kiro");
// Common state
const [showEnv, setShowEnv] = useState(false);
const [showValues, setShowValues] = useState(false);
const [loading, setLoading] = useState<string | null>(null);
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
const [message, setMessage] = useState<{
type: "success" | "error";
text: string;
} | null>(null);
const [copied, setCopied] = useState<string | null>(null);
useEffect(() => {
@@ -101,7 +154,7 @@ export function Providers() {
} catch (e) {
console.error("Failed to get default provider:", e);
}
await loadKiroStatus();
await loadGeminiStatus();
await loadQwenStatus();
@@ -119,6 +172,7 @@ export function Providers() {
const interval = setInterval(checkFileChanges, 5000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const checkFileChanges = async () => {
@@ -128,33 +182,46 @@ export function Providers() {
kiroHashRef.current = kiroResult.new_hash;
if (kiroResult.changed && kiroResult.reloaded) {
await loadKiroStatus();
setMessage({ type: "success", text: "[Kiro] 检测到凭证文件变化,已自动重新加载" });
setMessage({
type: "success",
text: "[Kiro] 检测到凭证文件变化,已自动重新加载",
});
setTimeout(() => setMessage(null), 5000);
}
} catch (e) {
console.error("Kiro check error:", e);
}
// Check Gemini
try {
const geminiResult = await checkAndReloadGeminiCredentials(geminiHashRef.current);
const geminiResult = await checkAndReloadGeminiCredentials(
geminiHashRef.current,
);
geminiHashRef.current = geminiResult.new_hash;
if (geminiResult.changed && geminiResult.reloaded) {
await loadGeminiStatus();
setMessage({ type: "success", text: "[Gemini] 检测到凭证文件变化,已自动重新加载" });
setMessage({
type: "success",
text: "[Gemini] 检测到凭证文件变化,已自动重新加载",
});
setTimeout(() => setMessage(null), 5000);
}
} catch (e) {
console.error("Gemini check error:", e);
}
// Check Qwen
try {
const qwenResult = await checkAndReloadQwenCredentials(qwenHashRef.current);
const qwenResult = await checkAndReloadQwenCredentials(
qwenHashRef.current,
);
qwenHashRef.current = qwenResult.new_hash;
if (qwenResult.changed && qwenResult.reloaded) {
await loadQwenStatus();
setMessage({ type: "success", text: "[Qwen] 检测到凭证文件变化,已自动重新加载" });
setMessage({
type: "success",
text: "[Qwen] 检测到凭证文件变化,已自动重新加载",
});
setTimeout(() => setMessage(null), 5000);
}
} catch (e) {
@@ -167,9 +234,13 @@ export function Providers() {
const status = await getKiroCredentials();
setKiroStatus(status);
setKiroEnvVars(await getEnvVariables());
setProviders(prev => prev.map(p =>
p.id === "kiro" ? { ...p, status: status.loaded ? "connected" : "disconnected" } : p
));
setProviders((prev) =>
prev.map((p) =>
p.id === "kiro"
? { ...p, status: status.loaded ? "connected" : "disconnected" }
: p,
),
);
} catch (e) {
console.error("Failed to load Kiro status:", e);
}
@@ -180,9 +251,13 @@ export function Providers() {
const status = await getGeminiCredentials();
setGeminiStatus(status);
setGeminiEnvVars(await getGeminiEnvVariables());
setProviders(prev => prev.map(p =>
p.id === "gemini" ? { ...p, status: status.loaded ? "connected" : "disconnected" } : p
));
setProviders((prev) =>
prev.map((p) =>
p.id === "gemini"
? { ...p, status: status.loaded ? "connected" : "disconnected" }
: p,
),
);
} catch (e) {
console.error("Failed to load Gemini status:", e);
}
@@ -193,9 +268,13 @@ export function Providers() {
const status = await getQwenCredentials();
setQwenStatus(status);
setQwenEnvVars(await getQwenEnvVariables());
setProviders(prev => prev.map(p =>
p.id === "qwen" ? { ...p, status: status.loaded ? "connected" : "disconnected" } : p
));
setProviders((prev) =>
prev.map((p) =>
p.id === "qwen"
? { ...p, status: status.loaded ? "connected" : "disconnected" }
: p,
),
);
} catch (e) {
console.error("Failed to load Qwen status:", e);
}
@@ -206,9 +285,20 @@ export function Providers() {
const status = await getOpenAICustomStatus();
setOpenaiStatus(status);
setOpenaiBaseUrl(status.base_url);
setProviders(prev => prev.map(p =>
p.id === "openai" ? { ...p, status: status.enabled && status.has_api_key ? "connected" : "disconnected", enabled: status.enabled } : p
));
setProviders((prev) =>
prev.map((p) =>
p.id === "openai"
? {
...p,
status:
status.enabled && status.has_api_key
? "connected"
: "disconnected",
enabled: status.enabled,
}
: p,
),
);
} catch (e) {
console.error("Failed to load OpenAI Custom status:", e);
}
@@ -219,9 +309,20 @@ export function Providers() {
const status = await getClaudeCustomStatus();
setClaudeStatus(status);
setClaudeBaseUrl(status.base_url);
setProviders(prev => prev.map(p =>
p.id === "claude" ? { ...p, status: status.enabled && status.has_api_key ? "connected" : "disconnected", enabled: status.enabled } : p
));
setProviders((prev) =>
prev.map((p) =>
p.id === "claude"
? {
...p,
status:
status.enabled && status.has_api_key
? "connected"
: "disconnected",
enabled: status.enabled,
}
: p,
),
);
} catch (e) {
console.error("Failed to load Claude Custom status:", e);
}
@@ -278,7 +379,7 @@ export function Providers() {
await setOpenAICustomConfig(
openaiApiKey || null,
openaiBaseUrl || null,
true
true,
);
await loadOpenAICustomStatus();
setMessage({ type: "success", text: "[OpenAI] 配置保存成功!" });
@@ -294,7 +395,7 @@ export function Providers() {
await setClaudeCustomConfig(
claudeApiKey || null,
claudeBaseUrl || null,
true
true,
);
await loadClaudeCustomStatus();
setMessage({ type: "success", text: "[Claude] 配置保存成功!" });
@@ -305,7 +406,9 @@ export function Providers() {
};
const toggleProvider = (id: string) => {
setProviders(prev => prev.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p));
setProviders((prev) =>
prev.map((p) => (p.id === id ? { ...p, enabled: !p.enabled } : p)),
);
};
const handleSetDefaultProvider = async (providerId: string) => {
@@ -313,7 +416,10 @@ export function Providers() {
try {
await setDefaultProvider(providerId);
setDefaultProviderState(providerId);
setMessage({ type: "success", text: `默认 Provider 已切换为: ${getProviderName(providerId)}` });
setMessage({
type: "success",
text: `默认 Provider 已切换为: ${getProviderName(providerId)}`,
});
} catch (e: any) {
setMessage({ type: "error", text: `切换失败: ${e.toString()}` });
}
@@ -322,12 +428,18 @@ export function Providers() {
const getProviderName = (id: string) => {
switch (id) {
case "kiro": return "Kiro Claude";
case "gemini": return "Gemini CLI";
case "qwen": return "通义千问";
case "openai": return "OpenAI 自定义";
case "claude": return "Claude 自定义";
default: return id;
case "kiro":
return "Kiro Claude";
case "gemini":
return "Gemini CLI";
case "qwen":
return "通义千问";
case "openai":
return "OpenAI 自定义";
case "claude":
return "Claude 自定义";
default:
return id;
}
};
@@ -338,21 +450,32 @@ export function Providers() {
};
const copyAllEnv = (vars: EnvVariable[]) => {
navigator.clipboard.writeText(vars.map(v => `${v.key}=${v.value}`).join("\n"));
navigator.clipboard.writeText(
vars.map((v) => `${v.key}=${v.value}`).join("\n"),
);
setCopied("all");
setTimeout(() => setCopied(null), 2000);
};
const getStatusColor = (status: Provider["status"]) => {
switch (status) {
case "connected": return "bg-green-500";
case "error": return "bg-red-500";
case "loading": return "bg-yellow-500 animate-pulse";
default: return "bg-gray-400";
case "connected":
return "bg-green-500";
case "error":
return "bg-red-500";
case "loading":
return "bg-yellow-500 animate-pulse";
default:
return "bg-gray-400";
}
};
const currentEnvVars = activeProvider === "kiro" ? kiroEnvVars : activeProvider === "gemini" ? geminiEnvVars : qwenEnvVars;
const currentEnvVars =
activeProvider === "kiro"
? kiroEnvVars
: activeProvider === "gemini"
? geminiEnvVars
: qwenEnvVars;
return (
<div className="space-y-6">
@@ -362,27 +485,43 @@ export function Providers() {
</div>
{message && (
<div className={`flex items-center gap-2 rounded-lg border p-3 text-sm ${
message.type === "success" ? "border-green-500 bg-green-50 text-green-700" : "border-red-500 bg-red-50 text-red-700"
}`}>
{message.type === "success" ? <CheckCircle2 className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
<div
className={`flex items-center gap-2 rounded-lg border p-3 text-sm ${
message.type === "success"
? "border-green-500 bg-green-50 text-green-700"
: "border-red-500 bg-red-50 text-red-700"
}`}
>
{message.type === "success" ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
{message.text}
</div>
)}
{/* Provider Tabs */}
<div className="flex gap-2 border-b overflow-x-auto">
{["kiro", "gemini", "qwen", "openai", "claude"].map(id => (
{["kiro", "gemini", "qwen", "openai", "claude"].map((id) => (
<button
key={id}
onClick={() => setActiveProvider(id)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px whitespace-nowrap ${
activeProvider === id
? "border-primary text-primary"
activeProvider === id
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
{id === "kiro" ? "Kiro Claude" : id === "gemini" ? "Gemini CLI" : id === "qwen" ? "通义千问" : id === "openai" ? "OpenAI 自定义" : "Claude 自定义"}
{id === "kiro"
? "Kiro Claude"
: id === "gemini"
? "Gemini CLI"
: id === "qwen"
? "通义千问"
: id === "openai"
? "OpenAI 自定义"
: "Claude 自定义"}
</button>
))}
</div>
@@ -395,7 +534,8 @@ export function Providers() {
<div>
<span className="text-muted-foreground">:</span>
<code className="ml-2 rounded bg-muted px-2 py-0.5 text-xs break-all">
{kiroStatus?.creds_path || "~/.aws/sso/cache/kiro-auth-token.json"}
{kiroStatus?.creds_path ||
"~/.aws/sso/cache/kiro-auth-token.json"}
</code>
</div>
<div>
@@ -404,13 +544,17 @@ export function Providers() {
</div>
<div>
<span className="text-muted-foreground">Access Token:</span>
<span className={`ml-2 ${kiroStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${kiroStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}
>
{kiroStatus?.has_access_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
<div>
<span className="text-muted-foreground">Refresh Token:</span>
<span className={`ml-2 ${kiroStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${kiroStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}
>
{kiroStatus?.has_refresh_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
@@ -429,7 +573,9 @@ export function Providers() {
disabled={loading !== null || !kiroStatus?.has_refresh_token}
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading === "refresh-kiro" ? "animate-spin" : ""}`} />
<RefreshCw
className={`h-4 w-4 ${loading === "refresh-kiro" ? "animate-spin" : ""}`}
/>
Token
</button>
<button
@@ -456,19 +602,25 @@ export function Providers() {
</div>
<div>
<span className="text-muted-foreground">Token :</span>
<span className={`ml-2 ${geminiStatus?.is_valid ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${geminiStatus?.is_valid ? "text-green-600" : "text-red-500"}`}
>
{geminiStatus?.is_valid ? "✓ 有效" : "✗ 无效/过期"}
</span>
</div>
<div>
<span className="text-muted-foreground">Access Token:</span>
<span className={`ml-2 ${geminiStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${geminiStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}
>
{geminiStatus?.has_access_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
<div>
<span className="text-muted-foreground">Refresh Token:</span>
<span className={`ml-2 ${geminiStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${geminiStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}
>
{geminiStatus?.has_refresh_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
@@ -487,7 +639,9 @@ export function Providers() {
disabled={loading !== null || !geminiStatus?.has_refresh_token}
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading === "refresh-gemini" ? "animate-spin" : ""}`} />
<RefreshCw
className={`h-4 w-4 ${loading === "refresh-gemini" ? "animate-spin" : ""}`}
/>
Token
</button>
<button
@@ -514,19 +668,25 @@ export function Providers() {
</div>
<div>
<span className="text-muted-foreground">Token :</span>
<span className={`ml-2 ${qwenStatus?.is_valid ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${qwenStatus?.is_valid ? "text-green-600" : "text-red-500"}`}
>
{qwenStatus?.is_valid ? "✓ 有效" : "✗ 无效/过期"}
</span>
</div>
<div>
<span className="text-muted-foreground">Access Token:</span>
<span className={`ml-2 ${qwenStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${qwenStatus?.has_access_token ? "text-green-600" : "text-red-500"}`}
>
{qwenStatus?.has_access_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
<div>
<span className="text-muted-foreground">Refresh Token:</span>
<span className={`ml-2 ${qwenStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}>
<span
className={`ml-2 ${qwenStatus?.has_refresh_token ? "text-green-600" : "text-red-500"}`}
>
{qwenStatus?.has_refresh_token ? "✓ 已加载" : "✗ 未加载"}
</span>
</div>
@@ -545,7 +705,9 @@ export function Providers() {
disabled={loading !== null || !qwenStatus?.has_refresh_token}
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading === "refresh-qwen" ? "animate-spin" : ""}`} />
<RefreshCw
className={`h-4 w-4 ${loading === "refresh-qwen" ? "animate-spin" : ""}`}
/>
Token
</button>
<button
@@ -565,7 +727,9 @@ export function Providers() {
<h3 className="mb-3 font-semibold">OpenAI </h3>
<div className="mb-4 space-y-4">
<div>
<label className="block text-sm text-muted-foreground mb-1">API Key</label>
<label className="block text-sm text-muted-foreground mb-1">
API Key
</label>
<input
type="password"
value={openaiApiKey}
@@ -575,7 +739,9 @@ export function Providers() {
/>
</div>
<div>
<label className="block text-sm text-muted-foreground mb-1">Base URL</label>
<label className="block text-sm text-muted-foreground mb-1">
Base URL
</label>
<input
type="text"
value={openaiBaseUrl}
@@ -586,7 +752,11 @@ export function Providers() {
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">:</span>
<span className={openaiStatus?.has_api_key ? "text-green-600" : "text-red-500"}>
<span
className={
openaiStatus?.has_api_key ? "text-green-600" : "text-red-500"
}
>
{openaiStatus?.has_api_key ? "✓ 已配置" : "✗ 未配置"}
</span>
</div>
@@ -607,7 +777,9 @@ export function Providers() {
<h3 className="mb-3 font-semibold">Claude </h3>
<div className="mb-4 space-y-4">
<div>
<label className="block text-sm text-muted-foreground mb-1">API Key</label>
<label className="block text-sm text-muted-foreground mb-1">
API Key
</label>
<input
type="password"
value={claudeApiKey}
@@ -617,7 +789,9 @@ export function Providers() {
/>
</div>
<div>
<label className="block text-sm text-muted-foreground mb-1">Base URL</label>
<label className="block text-sm text-muted-foreground mb-1">
Base URL
</label>
<input
type="text"
value={claudeBaseUrl}
@@ -628,7 +802,11 @@ export function Providers() {
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">:</span>
<span className={claudeStatus?.has_api_key ? "text-green-600" : "text-red-500"}>
<span
className={
claudeStatus?.has_api_key ? "text-green-600" : "text-red-500"
}
>
{claudeStatus?.has_api_key ? "✓ 已配置" : "✗ 未配置"}
</span>
</div>
@@ -653,24 +831,37 @@ export function Providers() {
onClick={() => setShowValues(!showValues)}
className="flex items-center gap-1 rounded px-2 py-1 text-xs hover:bg-muted"
>
{showValues ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
{showValues ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
{showValues ? "隐藏值" : "显示值"}
</button>
<button
onClick={() => copyAllEnv(currentEnvVars)}
className="flex items-center gap-1 rounded px-2 py-1 text-xs hover:bg-muted"
>
{copied === "all" ? <CheckCircle2 className="h-3 w-3 text-green-500" /> : <Copy className="h-3 w-3" />}
{copied === "all" ? (
<CheckCircle2 className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
</div>
</div>
{currentEnvVars.length === 0 ? (
<p className="text-sm text-muted-foreground"></p>
<p className="text-sm text-muted-foreground">
</p>
) : (
<div className="space-y-2 font-mono text-sm">
{currentEnvVars.map((v) => (
<div key={v.key} className="flex items-center gap-2 rounded bg-muted p-2">
<div
key={v.key}
className="flex items-center gap-2 rounded bg-muted p-2"
>
<span className="text-blue-600 shrink-0">{v.key}</span>
<span>=</span>
<span className="flex-1 truncate text-muted-foreground">
@@ -680,7 +871,11 @@ export function Providers() {
onClick={() => copyValue(v.key, v.value)}
className="rounded p-1 hover:bg-background shrink-0"
>
{copied === v.key ? <CheckCircle2 className="h-3 w-3 text-green-500" /> : <Copy className="h-3 w-3" />}
{copied === v.key ? (
<CheckCircle2 className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
</div>
))}
@@ -694,26 +889,37 @@ export function Providers() {
<div className="flex items-center justify-between">
<h3 className="font-semibold">Provider </h3>
<span className="text-sm text-muted-foreground">
: <span className="font-medium text-primary">{getProviderName(defaultProvider)}</span>
:{" "}
<span className="font-medium text-primary">
{getProviderName(defaultProvider)}
</span>
</span>
</div>
{providers.map((provider) => (
<div
key={provider.id}
<div
key={provider.id}
className={`flex items-center justify-between rounded-lg border bg-card p-4 transition-all ${
defaultProvider === provider.id ? "border-primary ring-1 ring-primary" : ""
defaultProvider === provider.id
? "border-primary ring-1 ring-primary"
: ""
}`}
>
<div className="flex items-center gap-4">
<div className={`h-3 w-3 rounded-full ${getStatusColor(provider.status)}`} />
<div
className={`h-3 w-3 rounded-full ${getStatusColor(provider.status)}`}
/>
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium">{provider.name}</h3>
{defaultProvider === provider.id && (
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"></span>
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
</span>
)}
</div>
<p className="text-sm text-muted-foreground">{provider.description}</p>
<p className="text-sm text-muted-foreground">
{provider.description}
</p>
</div>
</div>
<div className="flex items-center gap-2">
@@ -724,24 +930,34 @@ export function Providers() {
className="rounded-lg border px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50"
title="设为默认"
>
{loading === `default-${provider.id}` ? "切换中..." : "设为默认"}
{loading === `default-${provider.id}`
? "切换中..."
: "设为默认"}
</button>
)}
{(provider.id === "kiro" || provider.id === "gemini" || provider.id === "qwen") && (
{(provider.id === "kiro" ||
provider.id === "gemini" ||
provider.id === "qwen") && (
<button
onClick={() => handleRefreshToken(provider.id)}
disabled={loading !== null}
className="rounded p-2 hover:bg-muted"
title="刷新 Token"
>
<RefreshCw className={`h-4 w-4 ${loading === `refresh-${provider.id}` ? "animate-spin" : ""}`} />
<RefreshCw
className={`h-4 w-4 ${loading === `refresh-${provider.id}` ? "animate-spin" : ""}`}
/>
</button>
)}
<button
onClick={() => toggleProvider(provider.id)}
className={`rounded-full p-1 ${provider.enabled ? "bg-green-100 text-green-600" : "bg-gray-100 text-gray-400"}`}
>
{provider.enabled ? <Check className="h-4 w-4" /> : <X className="h-4 w-4" />}
{provider.enabled ? (
<Check className="h-4 w-4" />
) : (
<X className="h-4 w-4" />
)}
</button>
</div>
</div>
@@ -749,7 +965,8 @@ export function Providers() {
</div>
<p className="text-xs text-muted-foreground">
💡 5
💡 5
</p>
</div>
);
+32 -6
View File
@@ -56,7 +56,9 @@ export function Settings() {
</div>
{message && (
<div className={`rounded-lg border p-3 text-sm ${message.includes('失败') ? 'border-red-500 bg-red-50 text-red-700' : 'border-green-500 bg-green-50 text-green-700'}`}>
<div
className={`rounded-lg border p-3 text-sm ${message.includes("失败") ? "border-red-500 bg-red-50 text-red-700" : "border-green-500 bg-green-50 text-green-700"}`}
>
{message}
</div>
)}
@@ -67,7 +69,12 @@ export function Settings() {
<input
type="text"
value={config.server.host}
onChange={(e) => setConfig({ ...config, server: { ...config.server, host: e.target.value } })}
onChange={(e) =>
setConfig({
...config,
server: { ...config.server, host: e.target.value },
})
}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
/>
</div>
@@ -78,7 +85,13 @@ export function Settings() {
type="number"
value={config.server.port}
onChange={(e) =>
setConfig({ ...config, server: { ...config.server, port: parseInt(e.target.value) || 3001 } })
setConfig({
...config,
server: {
...config.server,
port: parseInt(e.target.value) || 3001,
},
})
}
className="w-full rounded-lg border bg-background px-3 py-2 text-sm"
/>
@@ -91,7 +104,12 @@ export function Settings() {
<input
type={showApiKey ? "text" : "password"}
value={config.server.api_key}
onChange={(e) => setConfig({ ...config, server: { ...config.server, api_key: e.target.value } })}
onChange={(e) =>
setConfig({
...config,
server: { ...config.server, api_key: e.target.value },
})
}
className="w-full rounded-lg border bg-background px-3 py-2 pr-20 text-sm"
/>
<div className="absolute right-2 top-1/2 flex -translate-y-1/2 gap-1">
@@ -101,7 +119,11 @@ export function Settings() {
className="rounded p-1 hover:bg-muted"
title={showApiKey ? "隐藏" : "显示"}
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
<button
type="button"
@@ -109,7 +131,11 @@ export function Settings() {
className="rounded p-1 hover:bg-muted"
title="复制"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</div>
+8 -2
View File
@@ -1,4 +1,10 @@
import { LayoutDashboard, Server, Settings, ScrollText, Cpu } from "lucide-react";
import {
LayoutDashboard,
Server,
Settings,
ScrollText,
Cpu,
} from "lucide-react";
import { cn } from "@/lib/utils";
type Page = "dashboard" | "providers" | "models" | "settings" | "logs";
@@ -32,7 +38,7 @@ export function Sidebar({ currentPage, onNavigate }: SidebarProps) {
"flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
currentPage === item.id
? "bg-primary text-primary-foreground"
: "hover:bg-muted"
: "hover:bg-muted",
)}
>
<item.icon className="h-4 w-4" />
+20 -18
View File
@@ -111,7 +111,7 @@ export async function testApi(
method: string,
path: string,
body: string | null,
auth: boolean
auth: boolean,
): Promise<TestResult> {
return invoke("test_api", { method, path, body, auth });
}
@@ -150,11 +150,12 @@ export interface CheckResult {
reloaded: boolean;
}
export async function checkAndReloadCredentials(lastHash: string): Promise<CheckResult> {
export async function checkAndReloadCredentials(
lastHash: string,
): Promise<CheckResult> {
return invoke("check_and_reload_credentials", { last_hash: lastHash });
}
// ============ Gemini Provider ============
export interface GeminiCredentialStatus {
@@ -186,11 +187,12 @@ export async function getGeminiTokenFileHash(): Promise<string> {
return invoke("get_gemini_token_file_hash");
}
export async function checkAndReloadGeminiCredentials(lastHash: string): Promise<CheckResult> {
export async function checkAndReloadGeminiCredentials(
lastHash: string,
): Promise<CheckResult> {
return invoke("check_and_reload_gemini_credentials", { last_hash: lastHash });
}
// ============ Qwen Provider ============
export interface QwenCredentialStatus {
@@ -222,11 +224,12 @@ export async function getQwenTokenFileHash(): Promise<string> {
return invoke("get_qwen_token_file_hash");
}
export async function checkAndReloadQwenCredentials(lastHash: string): Promise<CheckResult> {
export async function checkAndReloadQwenCredentials(
lastHash: string,
): Promise<CheckResult> {
return invoke("check_and_reload_qwen_credentials", { last_hash: lastHash });
}
// ============ OpenAI Custom Provider ============
export interface OpenAICustomStatus {
@@ -242,12 +245,12 @@ export async function getOpenAICustomStatus(): Promise<OpenAICustomStatus> {
export async function setOpenAICustomConfig(
apiKey: string | null,
baseUrl: string | null,
enabled: boolean
enabled: boolean,
): Promise<string> {
return invoke("set_openai_custom_config", {
api_key: apiKey,
base_url: baseUrl,
enabled
return invoke("set_openai_custom_config", {
api_key: apiKey,
base_url: baseUrl,
enabled,
});
}
@@ -266,16 +269,15 @@ export async function getClaudeCustomStatus(): Promise<ClaudeCustomStatus> {
export async function setClaudeCustomConfig(
apiKey: string | null,
baseUrl: string | null,
enabled: boolean
enabled: boolean,
): Promise<string> {
return invoke("set_claude_custom_config", {
api_key: apiKey,
base_url: baseUrl,
enabled
return invoke("set_claude_custom_config", {
api_key: apiKey,
base_url: baseUrl,
enabled,
});
}
// ============ Models ============
export interface ModelInfo {
+3 -2
View File
@@ -47,7 +47,8 @@
}
body {
@apply bg-background text-foreground;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
}
+1 -1
View File
@@ -6,5 +6,5 @@ import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
</React.StrictMode>,
);