mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-22 13:10:50 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
];
|
||||
Generated
+1402
-2
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -6,5 +6,5 @@ import "./index.css";
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user