feat: 更新项目名称为 One API on Workers

This commit is contained in:
Jeff Gu committed 2026-04-09 10:03:02 +08:00
1 parent 6071f18f7a
commit 1ef5502083
11 files changed
+122 -134

No files matched your search

+6 -6
View File
@@ -1,4 +1,4 @@
# AWSL One API
# One API on Workers
一个基于 Cloudflare Workers 的 OpenAI API 代理服务,支持多渠道管理、Token 管理和使用量统计。
@@ -22,7 +22,7 @@ flowchart LR
end
subgraph CF["Cloudflare Workers"]
B[AWSL One API]
B[One API on Workers]
D[(Cloudflare D1<br/>Database)]
end
@@ -41,7 +41,7 @@ flowchart LR
<summary><h2>📁 项目结构</h2></summary>
```text
awsl-one-api/
one-api-workers/
├── src/ # 源代码目录
│ ├── admin/ # 管理接口
│ │ ├── channel_api.ts # 渠道管理 API
@@ -93,7 +93,7 @@ bun install
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "awsl-one-api",
"name": "one-api-workers",
"main": "src/index.ts",
"compatibility_date": "2025-04-28",
"workers_dev": false,
@@ -120,7 +120,7 @@ bun install
```jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "awsl-one-api",
"name": "one-api-workers",
"main": "src/index.ts",
"compatibility_date": "2025-04-28",
"workers_dev": false,
@@ -134,7 +134,7 @@ bun install
3. 创建 Cloudflare D1 数据库:
```bash
wrangler d1 create awsl-one-api
wrangler d1 create one-api-workers
```
4. 在 Cloudflare Dashboard 中手动绑定生产域名:
+2 -2
View File
@@ -1,6 +1,6 @@
# AWSL One API Frontend
# One API on Workers Frontend
Modern React + Vite + TypeScript frontend for AWSL One API management platform.
Modern React + Vite + TypeScript frontend for One API on Workers management platform.
## Features
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>☁️</text></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Awsl One API - 管理控制台</title>
<title>One API on Workers - 管理控制台</title>
</head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -77,7 +77,7 @@ export function AppLayout({ children }: AppLayoutProps) {
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center">
<Zap className="h-3.5 w-3.5 text-white" />
</div>
<span className="font-semibold text-sm tracking-tight">Awsl One API</span>
<span className="font-semibold text-sm tracking-tight">One API on Workers</span>
</div>
<div className="h-9 w-9" />
</header>
+95 -107
View File
@@ -1,5 +1,5 @@
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { cn } from '@/lib/utils'
import { Link, useLocation, useNavigate } from "react-router-dom";
import { cn } from "@/lib/utils";
import {
Link as LinkIcon,
Key,
@@ -13,30 +13,30 @@ import {
PanelLeftClose,
PanelLeftOpen,
Zap,
} from 'lucide-react'
import { useAuthStore } from '@/store/auth'
import { useState } from 'react'
} from "lucide-react";
import { useAuthStore } from "@/store/auth";
import { useState } from "react";
interface NavItem {
title: string
href: string
icon: React.ComponentType<{ className?: string }>
title: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
}
const navItems: NavItem[] = [
{ title: '渠道管理', href: '/channels', icon: LinkIcon },
{ title: '令牌管理', href: '/tokens', icon: Key },
{ title: '定价管理', href: '/pricing', icon: DollarSign },
{ title: 'API 测试', href: '/api-test', icon: TestTube2 },
]
{ title: "渠道管理", href: "/channels", icon: LinkIcon },
{ title: "令牌管理", href: "/tokens", icon: Key },
{ title: "定价管理", href: "/pricing", icon: DollarSign },
{ title: "API 测试", href: "/api-test", icon: TestTube2 },
];
interface SidebarProps {
className?: string
onNavigate?: () => void
onClose?: () => void
collapsed?: boolean
onToggleCollapse?: () => void
showCollapseToggle?: boolean
className?: string;
onNavigate?: () => void;
onClose?: () => void;
collapsed?: boolean;
onToggleCollapse?: () => void;
showCollapseToggle?: boolean;
}
export function Sidebar({
@@ -47,89 +47,84 @@ export function Sidebar({
onToggleCollapse,
showCollapseToggle = false,
}: SidebarProps) {
const location = useLocation()
const navigate = useNavigate()
const { logout } = useAuthStore()
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme')
const isDark = savedTheme === 'dark' || (!savedTheme && document.documentElement.classList.contains('dark'))
const location = useLocation();
const navigate = useNavigate();
const { logout } = useAuthStore();
const [theme, setTheme] = useState<"light" | "dark">(() => {
if (typeof window !== "undefined") {
const savedTheme = localStorage.getItem("theme");
const isDark = savedTheme === "dark" || (!savedTheme && document.documentElement.classList.contains("dark"));
if (isDark) {
document.documentElement.classList.add('dark')
document.documentElement.classList.add("dark");
}
return isDark ? 'dark' : 'light'
return isDark ? "dark" : "light";
}
return 'light'
})
return "light";
});
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
if (newTheme === 'dark') {
document.documentElement.classList.add('dark')
const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
if (newTheme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove('dark')
document.documentElement.classList.remove("dark");
}
}
};
const handleLogout = () => {
logout()
navigate('/', { replace: true })
onNavigate?.()
}
logout();
navigate("/", { replace: true });
onNavigate?.();
};
const NavLink = ({ item }: { item: NavItem }) => {
const isActive = location.pathname === item.href
const isActive = location.pathname === item.href;
return (
<Link
to={item.href}
onClick={() => onNavigate?.()}
title={collapsed ? item.title : undefined}
className={cn(
'group relative flex items-center gap-3 px-3 py-2 rounded-lg text-[13px] font-medium transition-all duration-150',
collapsed && 'justify-center px-0 w-10 h-10 mx-auto',
"group relative flex items-center gap-3 px-3 py-2 rounded-lg text-[13px] font-medium transition-all duration-150",
collapsed && "justify-center px-0 w-10 h-10 mx-auto",
isActive
? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/80'
? "bg-primary text-primary-foreground shadow-sm shadow-primary/20"
: "text-muted-foreground hover:text-foreground hover:bg-muted/80",
)}
>
<item.icon className={cn(
"h-[17px] w-[17px] flex-shrink-0",
!isActive && "group-hover:scale-105 transition-transform duration-150"
)} />
<item.icon
className={cn(
"h-[17px] w-[17px] flex-shrink-0",
!isActive && "group-hover:scale-105 transition-transform duration-150",
)}
/>
{!collapsed && <span>{item.title}</span>}
</Link>
)
}
);
};
return (
<aside
className={cn(
"bg-card border-r flex flex-col h-full transition-all duration-300 ease-out",
collapsed ? "w-[68px]" : "w-60",
className
className,
)}
>
{/* Header */}
<div className={cn(
"h-14 flex items-center border-b px-4",
collapsed ? "justify-center px-0" : "justify-between"
)}>
<div
className={cn("h-14 flex items-center border-b px-2", collapsed ? "justify-center px-0" : "justify-between")}
>
{collapsed ? (
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center">
<Zap className="h-4 w-4 text-white" />
</div>
) : (
<>
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center shadow-md shadow-primary/15">
<Zap className="h-4 w-4 text-white" />
</div>
<div className="flex flex-col">
<span className="font-semibold text-[14px] tracking-tight leading-tight">Awsl One API</span>
<span className="text-[10px] text-muted-foreground leading-tight">AI Gateway</span>
</div>
<div className="w-full flex items-center justify-center gap-2.5">
<span className="font-semibold text-[14px] tracking-tight leading-tight">One API on Workers</span>
</div>
{onClose && (
<button
@@ -144,29 +139,6 @@ export function Sidebar({
)}
</div>
{/* Collapse Toggle */}
{showCollapseToggle && (
<div className={cn("px-3 py-1.5 hidden lg:flex", collapsed && "px-0 justify-center")}>
<button
type="button"
onClick={onToggleCollapse}
className={cn(
"flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[11px] font-medium text-muted-foreground/70 hover:text-muted-foreground hover:bg-muted/50 transition-colors",
collapsed ? "w-10 h-8 p-0 justify-center" : "w-full"
)}
>
{collapsed ? (
<PanelLeftOpen className="h-3.5 w-3.5" />
) : (
<>
<PanelLeftClose className="h-3.5 w-3.5" />
<span>收起</span>
</>
)}
</button>
</div>
)}
{/* Navigation */}
<nav className="flex-1 px-2.5 py-3 overflow-y-auto scrollbar-thin">
<div className="space-y-1">
@@ -179,31 +151,24 @@ export function Sidebar({
{/* Footer */}
<div className={cn("p-2.5 border-t", collapsed && "p-2")}>
{/* Action Buttons */}
<div className={cn(
"flex gap-1",
collapsed ? "flex-col items-center" : "mb-2"
)}>
<div className={cn("flex gap-1 mb-2", collapsed ? "flex-col items-center" : "")}>
<button
type="button"
title={collapsed ? (theme === 'dark' ? '浅色模式' : '深色模式') : undefined}
title={collapsed ? (theme === "dark" ? "浅色模式" : "深色模式") : undefined}
className={cn(
"flex items-center justify-center rounded-md transition-colors duration-150",
collapsed
? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium",
)}
onClick={toggleTheme}
>
{theme === 'dark' ? (
<Sun className="h-3.5 w-3.5" />
) : (
<Moon className="h-3.5 w-3.5" />
)}
{!collapsed && <span>{theme === 'dark' ? '浅色' : '深色'}</span>}
{theme === "dark" ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
{!collapsed && <span>{theme === "dark" ? "浅色" : "深色"}</span>}
</button>
<a
href="https://github.com/Tokinx/awsl-one-api"
href="https://github.com/Tokinx/one-api-workers"
target="_blank"
rel="noopener noreferrer"
title={collapsed ? "GitHub" : undefined}
@@ -211,22 +176,45 @@ export function Sidebar({
"flex items-center justify-center rounded-md transition-colors duration-150",
collapsed
? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium"
: "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium",
)}
>
<Github className="h-3.5 w-3.5" />
{!collapsed && <span>GitHub</span>}
</a>
{/* Collapse Toggle */}
{showCollapseToggle && (
<div className={cn("hidden lg:flex", collapsed && "px-0 justify-center")}>
<button
type="button"
onClick={onToggleCollapse}
className={cn(
"flex items-center justify-center rounded-md transition-colors duration-150",
collapsed ? "w-10 h-9 hover:bg-muted text-muted-foreground hover:text-foreground" : "flex-1 h-8 gap-1.5 px-2.5 hover:bg-muted/70 text-muted-foreground hover:text-foreground text-[11px] font-medium",
)}
>
{collapsed ? (
<PanelLeftOpen className="h-3.5 w-3.5" />
) : (
<>
<PanelLeftClose className="h-3.5 w-3.5" />
<span>收起</span>
</>
)}
</button>
</div>
)}
</div>
{/* Logout Button */}
<button
type="button"
title={collapsed ? '退出登录' : undefined}
title={collapsed ? "退出登录" : undefined}
className={cn(
'flex items-center justify-center gap-2 w-full rounded-lg text-[13px] font-medium transition-all duration-150',
collapsed ? 'h-9' : 'h-9 px-3',
'bg-destructive/8 text-destructive hover:bg-destructive/15'
"flex items-center justify-center gap-2 w-full rounded-lg text-[13px] font-medium transition-all duration-150",
collapsed ? "h-9" : "h-9 px-3",
"bg-destructive/8 text-destructive hover:bg-destructive/15",
)}
onClick={handleLogout}
>
@@ -235,5 +223,5 @@ export function Sidebar({
</button>
</div>
</aside>
)
);
}
+1 -1
View File
@@ -62,7 +62,7 @@ export function Dashboard() {
</div>
<h1 className="text-4xl md:text-5xl lg:text-[3.5rem] font-bold tracking-tight leading-[1.1] mb-5">
<span className="bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent">
Awsl One API
One API on Workers
</span>
</h1>
<p className="text-lg md:text-xl text-muted-foreground leading-relaxed mb-8 max-w-2xl">
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "awsl-ai-proxy",
"name": "one-api-workers",
"workspaces": [
"frontend"
],
+1 -1
View File
@@ -81,7 +81,7 @@ const app = new Hono<HonoCustomType>()
const openapi = fromHono(app, {
schema: {
info: {
title: 'Awsl One API',
title: 'One API on Workers',
version: '1.0.0',
}
},
+4 -4
View File
@@ -174,10 +174,10 @@ assert_http "azure responses stream → 200 + SSE" "200" "event:" "$R"
echo ""
echo "--- Quota check ---"
npx wrangler d1 execute awsl-one-api --local --command "UPDATE api_token SET usage = 99999999 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
npx wrangler d1 execute one-api-workers --local --command "UPDATE api_token SET usage = 99999999 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
R=$(do_post -H "Authorization: Bearer sk-mock-all" "$API/v1/chat/completions" -H "Content-Type: application/json" -d '{"model":"gpt-4o","messages":[]}')
assert_http "quota exceeded → 402" "402" "Quota exceeded" "$R"
npx wrangler d1 execute awsl-one-api --local --command "UPDATE api_token SET usage = 0 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
npx wrangler d1 execute one-api-workers --local --command "UPDATE api_token SET usage = 0 WHERE key = 'sk-mock-all'" > /dev/null 2>&1
# ---- Error recovery ----
echo ""
@@ -197,10 +197,10 @@ echo "========================================="
echo ""
echo "Cleaning up test data..."
for key in test-openai test-azure test-claude test-c2o test-responses test-azure-resp; do
npx wrangler d1 execute awsl-one-api --local --command "DELETE FROM channel_config WHERE key = '$key'" > /dev/null 2>&1
npx wrangler d1 execute one-api-workers --local --command "DELETE FROM channel_config WHERE key = '$key'" > /dev/null 2>&1
done
for key in sk-mock-all sk-mock-openai sk-mock-quota; do
npx wrangler d1 execute awsl-one-api --local --command "DELETE FROM api_token WHERE key = '$key'" > /dev/null 2>&1
npx wrangler d1 execute one-api-workers --local --command "DELETE FROM api_token WHERE key = '$key'" > /dev/null 2>&1
done
echo "Cleanup done."
+9 -9
View File
@@ -7,30 +7,30 @@ MOCK="http://localhost:9999"
echo "Setting up test channels and tokens..."
# Channel: openai type
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-openai', '{\"name\":\"test-openai\",\"type\":\"openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o\":\"gpt-4o\",\"gpt-4\":\"gpt-4\"}}')"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-openai', '{\"name\":\"test-openai\",\"type\":\"openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o\":\"gpt-4o\",\"gpt-4\":\"gpt-4\"}}')"
# Channel: azure-openai type
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure', '{\"name\":\"test-azure\",\"type\":\"azure-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2024-02-01\",\"deployment_mapper\":{\"gpt-4o-azure\":\"gpt-4o-deployment\"}}')"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure', '{\"name\":\"test-azure\",\"type\":\"azure-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2024-02-01\",\"deployment_mapper\":{\"gpt-4o-azure\":\"gpt-4o-deployment\"}}')"
# Channel: claude type
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-claude', '{\"name\":\"test-claude\",\"type\":\"claude\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-3-opus\":\"claude-3-opus-20240229\"}}')"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-claude', '{\"name\":\"test-claude\",\"type\":\"claude\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-3-opus\":\"claude-3-opus-20240229\"}}')"
# Channel: claude-to-openai type
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-c2o', '{\"name\":\"test-c2o\",\"type\":\"claude-to-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-via-openai\":\"claude-3-opus\"}}')"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-c2o', '{\"name\":\"test-c2o\",\"type\":\"claude-to-openai\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"claude-via-openai\":\"claude-3-opus\"}}')"
# Channel: openai-responses type
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-responses', '{\"name\":\"test-responses\",\"type\":\"openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o-resp\":\"gpt-4o\"}}')"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-responses', '{\"name\":\"test-responses\",\"type\":\"openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"deployment_mapper\":{\"gpt-4o-resp\":\"gpt-4o\"}}')"
# Channel: azure-openai-responses type
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure-resp', '{\"name\":\"test-azure-resp\",\"type\":\"azure-openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2025-01-01\",\"deployment_mapper\":{\"gpt-4o-azure-resp\":\"gpt-4o-deployment\"}}')"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO channel_config (key, value) VALUES ('test-azure-resp', '{\"name\":\"test-azure-resp\",\"type\":\"azure-openai-responses\",\"endpoint\":\"$MOCK\",\"api_key\":\"mock-key\",\"api_version\":\"2025-01-01\",\"deployment_mapper\":{\"gpt-4o-azure-resp\":\"gpt-4o-deployment\"}}')"
# Token: access to all test channels only
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-all', '{\"name\":\"mock-all\",\"channel_keys\":[\"test-openai\",\"test-azure\",\"test-claude\",\"test-c2o\",\"test-responses\",\"test-azure-resp\"],\"total_quota\":99999999}', 0)"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-all', '{\"name\":\"mock-all\",\"channel_keys\":[\"test-openai\",\"test-azure\",\"test-claude\",\"test-c2o\",\"test-responses\",\"test-azure-resp\"],\"total_quota\":99999999}', 0)"
# Token: limited to openai only
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-openai', '{\"name\":\"mock-openai\",\"channel_keys\":[\"test-openai\"],\"total_quota\":99999999}', 0)"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-openai', '{\"name\":\"mock-openai\",\"channel_keys\":[\"test-openai\"],\"total_quota\":99999999}', 0)"
# Token: quota almost full
npx wrangler d1 execute awsl-one-api --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-quota', '{\"name\":\"mock-quota\",\"channel_keys\":[\"test-openai\"],\"total_quota\":100}', 99)"
npx wrangler d1 execute one-api-workers --local --command "INSERT OR REPLACE INTO api_token (key, value, usage) VALUES ('sk-mock-quota', '{\"name\":\"mock-quota\",\"channel_keys\":[\"test-openai\"],\"total_quota\":100}', 99)"
echo "Test data setup complete!"
Vendored
+1 -1
View File
@@ -71,7 +71,7 @@ type Usage = {
cached_tokens?: number,
}
type AwslKeyPayload = {
type oawKeyPayload = {
channel_key: string;
multipler: number | undefined | null;
}