├── frontend ├── src │ ├── vite-env.d.ts │ ├── components │ │ ├── ToggleThemeButton.tsx │ │ └── Footer.tsx │ ├── theme │ │ ├── types.ts │ │ └── index.ts │ ├── routes.tsx │ ├── entry-server.tsx │ ├── utils │ │ └── storage.ts │ ├── entry-client.tsx │ ├── context │ │ └── ThemeContextProvider.tsx │ ├── pages │ │ ├── AuthCallbackPage.tsx │ │ └── HomePage.tsx │ ├── App.tsx │ ├── hooks │ │ └── useAuth.ts │ └── assets │ │ └── logos │ │ └── index.tsx ├── vite-env.d.ts ├── tsconfig.json ├── uno.config.ts ├── vite.config.mts └── index.html ├── drizzle ├── 0002_grey_captain_midlands.sql ├── 0000_little_mordo.sql ├── meta │ ├── _journal.json │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ └── 0002_snapshot.json └── 0001_brainy_stranger.sql ├── drizzle.config.ts ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── tsconfig.node.json ├── worker-configuration.d.ts ├── src ├── types.ts ├── custom.d.ts ├── routes │ ├── api.ts │ ├── feature.ts │ └── config.ts ├── validators │ ├── post.schema.ts │ ├── feature.schema.ts │ └── config.schema.ts ├── ambient.d.ts ├── middleware │ └── auth.ts ├── db │ └── schema.ts ├── config │ ├── seo.ts │ └── defaultModelMappings.ts ├── utils │ ├── encryption.ts │ └── htmlTemplate.ts ├── services │ └── modelMappingService.ts └── index.ts ├── env.example ├── scripts └── generateHtml.ts ├── common └── validators │ ├── auth.schema.ts │ ├── config.schema.ts │ └── claude.schema.ts ├── tsconfig.json ├── LICENSE ├── wrangler.jsonc ├── docs ├── TROUBLESHOOTING.md ├── INSTALLATION.md ├── SEO_GUIDE.md ├── DEPLOYMENT.md ├── API_GUIDE.md ├── DEVELOPMENT.md └── THEMING.md ├── package.json ├── .gitignore ├── README.md ├── README_EN.md └── .cursor ├── rules └── global.mdc └── trace └── debugging-ssr-error.md /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /drizzle/0002_grey_captain_midlands.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `users` ADD `provider_base_url` text; -------------------------------------------------------------------------------- /frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.json" { 4 | const value: any; 5 | export default value; 6 | } 7 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | schema: "./src/db/schema.ts", 5 | out: "./drizzle", 6 | dialect: "sqlite", 7 | }); 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | node_modules 3 | dist 4 | .wrangler 5 | migrations 6 | 7 | # Ignore lockfiles 8 | package-lock.json 9 | pnpm-lock.yaml 10 | yarn.lock 11 | bun.lockb 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 120, 4 | "proseWrap": "never", 5 | "overrides": [ 6 | { 7 | "files": ["*.json", "*.jsonc"], 8 | "options": { 9 | "trailingComma": "none" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[json]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[jsonc]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "editor.tabSize": 2 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "types": ["node"] 8 | }, 9 | "include": ["drizzle.config.ts", "frontend/vite.config.mts", "frontend/uno.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler on Tue Jul 30 2024 10:00:00 GMT+0000 (Coordinated Universal Time) 2 | // by running `wrangler types` 3 | 4 | interface Env { 5 | DB: D1Database; 6 | ASSETS: Fetcher; 7 | NODE_ENV: "development" | "production" | "test"; 8 | VITE_PORT: string; 9 | } 10 | -------------------------------------------------------------------------------- /drizzle/0000_little_mordo.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `features` ( 2 | `id` integer PRIMARY KEY NOT NULL, 3 | `key` text NOT NULL, 4 | `name` text NOT NULL, 5 | `description` text NOT NULL, 6 | `enabled` integer DEFAULT false NOT NULL 7 | ); 8 | --> statement-breakpoint 9 | CREATE UNIQUE INDEX `features_key_idx` ON `features` (`key`); -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["vite/client"], 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"], 8 | "@frontend/*": ["./src/*"] 9 | } 10 | }, 11 | "include": ["src/**/*", "vite.config.mts", "uno.config.ts"], 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export type Bindings = { 4 | DB: D1Database; 5 | ASSETS: Fetcher; 6 | NODE_ENV: "development" | "production" | "test"; 7 | VITE_PORT: string; 8 | // GitHub OAuth 配置 9 | GITHUB_CLIENT_ID: string; 10 | GITHUB_CLIENT_SECRET: string; 11 | // 加密密钥 12 | ENCRYPTION_KEY: string; 13 | // 应用基础 URL 14 | APP_BASE_URL: string; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetUno, presetIcons } from "unocss"; 2 | 3 | export default defineConfig({ 4 | presets: [ 5 | presetUno(), 6 | presetIcons({ 7 | scale: 1.2, 8 | warn: true, 9 | }), 10 | ], 11 | // UnoCSS a11y properties are not supported by MUI, so we disable them. 12 | // https://github.com/unocss/unocss/issues/2103 13 | configDeps: ["./uno.config.ts"], 14 | blocklist: ["sr-only"], 15 | }); 16 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # AI 代理服务环境变量配置示例 2 | # 复制此文件为 .env.vars 并填入实际值作为本地配置(线上环境配置请通过 Cloud Flare 控制台进行) 3 | 4 | # GitHub OAuth 配置 5 | # 在 https://github.com/settings/applications/new 创建新的 OAuth App 6 | GITHUB_CLIENT_ID=your_github_client_id 7 | GITHUB_CLIENT_SECRET=your_github_client_secret 8 | 9 | # 加密密钥(用于加密存储的 API 密钥) 10 | # 生成一个 32 字符的随机字符串 11 | ENCRYPTION_KEY=your_32_character_encryption_key_here 12 | 13 | # 应用基础 URL 14 | # 开发环境 15 | APP_BASE_URL=http://localhost:8787 16 | # 生产环境示例 17 | # APP_BASE_URL=https://your-domain.pages.dev 18 | -------------------------------------------------------------------------------- /frontend/src/components/ToggleThemeButton.tsx: -------------------------------------------------------------------------------- 1 | import Brightness4Icon from "@mui/icons-material/Brightness4"; 2 | import Brightness7Icon from "@mui/icons-material/Brightness7"; 3 | import { IconButton } from "@mui/material"; 4 | import { useAppTheme } from "../context/ThemeContextProvider"; 5 | 6 | export function ToggleThemeButton() { 7 | const { themeMode, toggleTheme } = useAppTheme(); 8 | return ( 9 | 10 | {themeMode === "dark" ? : } 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1751593595151, 9 | "tag": "0000_little_mordo", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "6", 15 | "when": 1755261724552, 16 | "tag": "0001_brainy_stranger", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "6", 22 | "when": 1755347396798, 23 | "tag": "0002_grey_captain_midlands", 24 | "breakpoints": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /frontend/src/theme/types.ts: -------------------------------------------------------------------------------- 1 | import "@mui/material/styles"; 2 | 3 | declare module "@mui/material/styles" { 4 | interface Theme { 5 | pageBackground: string; 6 | appBar: { 7 | background: string; 8 | }; 9 | glass: { 10 | background: string; 11 | }; 12 | hero: { 13 | background: string; 14 | }; 15 | } 16 | // allow configuration using `createTheme` 17 | interface ThemeOptions { 18 | pageBackground?: string; 19 | appBar?: { 20 | background?: string; 21 | }; 22 | glass?: { 23 | background?: string; 24 | }; 25 | hero?: { 26 | background?: string; 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.html" { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare module "*vite/manifest.json" { 7 | const manifest: { 8 | [key: string]: { 9 | file: string; 10 | css?: string[]; 11 | isEntry?: boolean; 12 | }; 13 | }; 14 | export default manifest; 15 | } 16 | 17 | declare module "../frontend/dist/manifest.json" { 18 | const manifest: { 19 | [key: string]: { 20 | file: string; 21 | css?: string[]; 22 | isEntry?: boolean; 23 | }; 24 | }; 25 | export default manifest; 26 | } 27 | 28 | declare module "../frontend/dist/index.html" { 29 | const content: string; 30 | export default content; 31 | } 32 | -------------------------------------------------------------------------------- /scripts/generateHtml.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | /** 3 | * 根据SEO配置生成开发环境的HTML模板 4 | * 使用方法:pnpm tsx scripts/generateHtml.ts 5 | */ 6 | 7 | import { writeFileSync } from "fs"; 8 | import { join } from "path"; 9 | import { generateHtmlTemplate } from "../src/utils/htmlTemplate"; 10 | 11 | // 生成开发环境的HTML模板 12 | function generateDevelopmentHtml() { 13 | const htmlContent = generateHtmlTemplate({ 14 | path: "/", 15 | content: "", 16 | cssFiles: [], 17 | jsFiles: ["/src/entry-client.tsx"], 18 | }); 19 | 20 | const outputPath = join(__dirname, "../frontend/index.html"); 21 | writeFileSync(outputPath, htmlContent, "utf-8"); 22 | 23 | console.log("✅ Generated frontend/index.html successfully!"); 24 | console.log("📄 File location:", outputPath); 25 | } 26 | 27 | // 执行生成 28 | generateDevelopmentHtml(); 29 | -------------------------------------------------------------------------------- /common/validators/auth.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "@hono/zod-openapi"; 2 | 3 | export const ErrorSchema = z.object({ 4 | code: z.number().openapi({ example: 404 }), 5 | message: z.string().openapi({ example: "User not found" }), 6 | }); 7 | 8 | export const GitHubCallbackSchema = z.object({ 9 | code: z.string(), 10 | state: z.string().optional(), 11 | }); 12 | 13 | export const UserInfoSchema = z.object({ 14 | id: z.string(), 15 | username: z.string(), 16 | email: z.string().nullable(), 17 | avatarUrl: z.string().nullable(), 18 | apiKey: z.string(), 19 | createdAt: z.string(), 20 | }); 21 | 22 | export const AuthResponseSchema = z.object({ 23 | success: z.boolean(), 24 | message: z.string(), 25 | data: z 26 | .object({ 27 | user: UserInfoSchema, 28 | sessionToken: z.string(), 29 | }) 30 | .optional(), 31 | }); 32 | -------------------------------------------------------------------------------- /frontend/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import App from "./App"; 3 | import HomePage from "./pages/HomePage"; 4 | import { DashboardPage } from "./pages/DashboardPage"; 5 | import { AuthCallbackPage } from "./pages/AuthCallbackPage"; 6 | 7 | /** 8 | * 路由配置 9 | * 10 | * 这是唯一的路由定义文件,被客户端和服务端入口共享使用。 11 | * 添加新路由时,只需要在这里修改即可。 12 | * 13 | * @example 14 | * // 添加新页面: 15 | * 1. 导入页面组件:import AboutPage from "./pages/AboutPage"; 16 | * 2. 添加路由:} /> 17 | */ 18 | export const AppRoutes = () => ( 19 | 20 | }> 21 | } /> 22 | } /> 23 | } /> 24 | {/* 在这里添加新的路由 */} 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/routes/api.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIHono } from "@hono/zod-openapi"; 2 | import * as drizzleSchema from "../db/schema"; 3 | import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1"; 4 | 5 | import features from "./feature"; 6 | import auth from "./auth"; 7 | import config from "./config"; 8 | import { Bindings } from "../types"; 9 | 10 | type Variables = { 11 | db: DrizzleD1Database; 12 | }; 13 | 14 | const api = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>() 15 | // Add DB middleware to all API routes 16 | .use("*", async (c, next) => { 17 | const db = drizzle(c.env.DB, { schema: drizzleSchema }); 18 | c.set("db", db); 19 | await next(); 20 | }) 21 | // Register API routes using chaining 22 | .route("/features", features) 23 | .route("/auth", auth) 24 | .route("/config", config); 25 | 26 | export type ApiRoutes = typeof api; 27 | 28 | export default api; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["ESNext", "DOM"], 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "react", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowSyntheticDefaultImports": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "allowJs": false, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": ["./src/*"], 22 | "@frontend/*": ["./frontend/src/*"], 23 | "@common/*": ["./common/*"] 24 | }, 25 | "types": ["@cloudflare/workers-types", "vite/client"], 26 | "allowImportingTsExtensions": true 27 | }, 28 | "include": ["src", "frontend/src", "common"], 29 | "exclude": ["node_modules", "dist", "frontend/dist"] 30 | } 31 | -------------------------------------------------------------------------------- /src/validators/post.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "@hono/zod-openapi"; 2 | 3 | export const PostSchema = z 4 | .object({ 5 | id: z.number().openapi({ 6 | example: 1, 7 | }), 8 | title: z.string().openapi({ 9 | example: "My First Post", 10 | }), 11 | content: z.string().openapi({ 12 | example: "This is the content of my first post.", 13 | }), 14 | }) 15 | .openapi("Post"); 16 | 17 | export const CreatePostSchema = z.object({ 18 | title: z.string().max(50), 19 | content: z.string().max(1000), 20 | }); 21 | 22 | export const PathParamsSchema = z.object({ 23 | id: z 24 | .string() 25 | .regex(/^\d+$/) 26 | .openapi({ 27 | param: { 28 | name: "id", 29 | in: "path", 30 | }, 31 | example: "123", 32 | }), 33 | }); 34 | 35 | export const ErrorSchema = z.object({ 36 | code: z.number().openapi({ 37 | example: 400, 38 | }), 39 | message: z.string().openapi({ 40 | example: "Bad Request", 41 | }), 42 | }); 43 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | // This file is for ambient type declarations. 2 | // It's used to tell TypeScript about modules that are not part of the main 3 | // compilation unit, but will be available at runtime. 4 | 5 | /** 6 | * Declares the shape of the Vite manifest file. 7 | * This allows us to import `../../frontend/dist/manifest.json` without TypeScript errors, 8 | * even though the file only exists after the frontend has been built. 9 | */ 10 | declare module "*manifest.json" { 11 | const manifest: { 12 | [key: string]: { 13 | file: string; 14 | css?: string[]; 15 | isEntry?: boolean; 16 | src?: string; 17 | }; 18 | }; 19 | export default manifest; 20 | } 21 | 22 | /** 23 | * Declares the shape of the server entry module from the frontend build. 24 | * This allows us to import `../../frontend/dist/server/entry-server.js` without TypeScript errors. 25 | * It specifies that the module has a `render` function with a specific signature. 26 | */ 27 | declare module "*/entry-server.js" { 28 | export function render(opts: { url: string }): Promise<{ html: string }>; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@mui/material"; 2 | import "./types.ts"; 3 | 4 | export const lightTheme = createTheme({ 5 | palette: { 6 | mode: "light", 7 | }, 8 | pageBackground: "linear-gradient(180deg, #ffffff 0%, #eaf2f8 100%)", 9 | appBar: { 10 | background: "rgba(255, 255, 255, 0.75)", 11 | }, 12 | glass: { 13 | background: "rgba(255, 255, 255, 0.7)", 14 | }, 15 | hero: { 16 | background: `radial-gradient(ellipse 80% 50% at 50% -20%, rgba(0, 128, 255, 0.2), hsla(0, 0%, 100%, 0))`, 17 | }, 18 | }); 19 | 20 | export const darkTheme = createTheme({ 21 | palette: { 22 | mode: "dark", 23 | background: { 24 | default: "#121212", 25 | paper: "#1e1e1e", 26 | }, 27 | }, 28 | pageBackground: "linear-gradient(180deg, #121212 0%, #1a1a2e 100%)", 29 | appBar: { 30 | background: "rgba(20, 20, 22, 0.75)", 31 | }, 32 | glass: { 33 | background: "rgba(20, 20, 22, 0.7)", 34 | }, 35 | hero: { 36 | background: `radial-gradient(ellipse 80% 50% at 50% -20%, rgba(120, 119, 198, 0.3), hsla(0, 0%, 100%, 0))`, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /frontend/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMServer from "react-dom/server"; 3 | import { StaticRouter } from "react-router-dom/server"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import { AppThemeProvider } from "./context/ThemeContextProvider"; 6 | import { AppRoutes } from "./routes"; 7 | 8 | /** 9 | * 服务端渲染入口 10 | * 11 | * @param path - 请求的路径 12 | * @returns 渲染后的 HTML 字符串和其他数据 13 | */ 14 | export function render(path: string) { 15 | const queryClient = new QueryClient({ 16 | defaultOptions: { 17 | queries: { 18 | staleTime: Infinity, // 服务端不需要重新获取数据 19 | gcTime: Infinity, 20 | }, 21 | }, 22 | }); 23 | 24 | const html = ReactDOMServer.renderToString( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | , 34 | ); 35 | 36 | return { html }; 37 | } 38 | -------------------------------------------------------------------------------- /src/validators/feature.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "@hono/zod-openapi"; 2 | 3 | export const FeatureSchema = z 4 | .object({ 5 | id: z.number().openapi({ 6 | example: 1, 7 | }), 8 | key: z.string().openapi({ 9 | example: "dark-mode-toggle", 10 | }), 11 | name: z.string().openapi({ 12 | example: "Dark Mode Toggle", 13 | }), 14 | description: z.string().openapi({ 15 | example: "Enable or disable the dark mode toggle button in the UI.", 16 | }), 17 | enabled: z.boolean().openapi({ 18 | example: true, 19 | }), 20 | }) 21 | .openapi("Feature"); 22 | 23 | export const UpdateFeatureSchema = z.object({ 24 | enabled: z.boolean(), 25 | }); 26 | 27 | export const PathParamsSchema = z.object({ 28 | key: z.string().openapi({ 29 | param: { 30 | name: "key", 31 | in: "path", 32 | }, 33 | example: "dark-mode-toggle", 34 | }), 35 | }); 36 | 37 | export const ErrorSchema = z.object({ 38 | code: z.number().openapi({ 39 | example: 400, 40 | }), 41 | message: z.string().openapi({ 42 | example: "Bad Request", 43 | }), 44 | }); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KroMiose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/cloudflare/wrangler/main/config-schema.json", 3 | "name": "claude-code-nexus", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2024-07-29", 6 | "compatibility_flags": ["nodejs_compat"], 7 | "assets": { 8 | "binding": "ASSETS", 9 | "directory": "./dist/client" 10 | }, 11 | "d1_databases": [ 12 | { 13 | "binding": "DB", 14 | "database_name": "claude-code-nexus-local", 15 | "database_id": "local", 16 | "migrations_dir": "drizzle" 17 | } 18 | ], 19 | "vars": { 20 | "NODE_ENV": "development", 21 | "VITE_PORT": "5173", 22 | "APP_BASE_URL": "http://localhost:8787" 23 | }, 24 | "env": { 25 | "production": { 26 | "assets": { 27 | "binding": "ASSETS", 28 | "directory": "./dist/client" 29 | }, 30 | "d1_databases": [ 31 | { 32 | "binding": "DB", 33 | "database_name": "claude-code-nexus", 34 | "database_id": "18df19cb-8329-4911-9c02-fffe47d65c7b", 35 | "migrations_dir": "drizzle" 36 | } 37 | ], 38 | "vars": { 39 | "NODE_ENV": "production", 40 | "VITE_PORT": "5173", 41 | "APP_BASE_URL": "https://claude.nekro.ai" 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | // 检查是否在浏览器环境中 2 | const isBrowser = typeof window !== "undefined"; 3 | 4 | // 安全的 localStorage 操作工具 5 | export const safeLocalStorage = { 6 | getItem: (key: string): string | null => { 7 | if (!isBrowser) return null; 8 | try { 9 | return localStorage.getItem(key); 10 | } catch (error) { 11 | console.error("localStorage.getItem error:", error); 12 | return null; 13 | } 14 | }, 15 | setItem: (key: string, value: string): void => { 16 | if (!isBrowser) return; 17 | try { 18 | localStorage.setItem(key, value); 19 | // 触发自定义事件通知其他组件 localStorage 已更改 20 | if (key === "auth_token") { 21 | window.dispatchEvent(new Event("auth_token_changed")); 22 | } 23 | } catch (error) { 24 | console.error("localStorage.setItem error:", error); 25 | } 26 | }, 27 | removeItem: (key: string): void => { 28 | if (!isBrowser) return; 29 | try { 30 | localStorage.removeItem(key); 31 | // 触发自定义事件通知其他组件 localStorage 已更改 32 | if (key === "auth_token") { 33 | window.dispatchEvent(new Event("auth_token_changed")); 34 | } 35 | } catch (error) { 36 | console.error("localStorage.removeItem error:", error); 37 | } 38 | }, 39 | }; 40 | 41 | // 检查是否在浏览器环境中的便捷函数 42 | export { isBrowser }; 43 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Container, Link, Typography, Divider } from "@mui/material"; 2 | 3 | export const Footer = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | 18 | © {new Date().getFullYear()} Claude Code Nexus. 为开发者提供最佳的 Claude API 代理服务. 19 | 20 | 21 | 28 | GitHub 29 | 30 | 31 | Powered by Cloudflare 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { createMiddleware } from "hono/factory"; 2 | import { and, eq, gt } from "drizzle-orm"; 3 | import { userSessions, users } from "../db/schema"; 4 | import type { Bindings } from "../types"; 5 | import type { DrizzleD1Database } from "drizzle-orm/d1"; 6 | import * as drizzleSchema from "../db/schema"; 7 | 8 | type Variables = { 9 | db: DrizzleD1Database; 10 | user: typeof drizzleSchema.users.$inferSelect; 11 | }; 12 | 13 | export const authMiddleware = createMiddleware<{ 14 | Bindings: Bindings; 15 | Variables: Variables; 16 | }>(async (c, next) => { 17 | const authHeader = c.req.header("Authorization"); 18 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 19 | return c.json({ success: false, message: "认证令牌缺失" }, 401); 20 | } 21 | 22 | const sessionToken = authHeader.substring(7); 23 | const db = c.get("db"); 24 | 25 | const session = await db.query.userSessions.findFirst({ 26 | where: and(eq(userSessions.sessionToken, sessionToken), gt(userSessions.expiresAt, new Date())), 27 | }); 28 | 29 | if (!session) { 30 | return c.json({ success: false, message: "认证令牌无效或已过期" }, 401); 31 | } 32 | 33 | const user = await db.query.users.findFirst({ 34 | where: eq(users.id, session.userId), 35 | }); 36 | 37 | if (!user) { 38 | return c.json({ success: false, message: "用户不存在" }, 401); 39 | } 40 | 41 | c.set("user", user); 42 | await next(); 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import "vite/modulepreload-polyfill"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import { BrowserRouter as OriginalBrowserRouter } from "react-router-dom"; 5 | import "uno.css"; 6 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 7 | import { AppThemeProvider } from "./context/ThemeContextProvider"; 8 | import { AppRoutes } from "./routes"; 9 | 10 | // CJS/ESM interop fix for react-router-dom 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | const BrowserRouter = (OriginalBrowserRouter as any).default ?? OriginalBrowserRouter; 13 | 14 | const queryClient = new QueryClient({ 15 | defaultOptions: { 16 | queries: { 17 | staleTime: 1000 * 60 * 5, // 5 minutes 18 | gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime) 19 | }, 20 | }, 21 | }); 22 | 23 | const AppComponent = ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | const rootElement = document.getElementById("root")!; 36 | 37 | // 在开发环境使用 createRoot,在生产环境使用 hydrateRoot 进行 SSR 38 | if (import.meta.env.DEV) { 39 | ReactDOM.createRoot(rootElement).render(AppComponent); 40 | } else { 41 | ReactDOM.hydrateRoot(rootElement, AppComponent); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import unocss from "unocss/vite"; 4 | import { resolve } from "path"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig(({ mode }) => { 8 | // 正确加载环境变量 - 从项目根目录加载 9 | const env = loadEnv(mode, resolve(__dirname, ".."), ""); 10 | 11 | // 配置变量 12 | const DEV_PORT = parseInt(env.VITE_PORT || "5173"); 13 | const API_PORT = env.VITE_API_PORT || "8787"; 14 | const API_HOST = env.VITE_API_HOST || "localhost"; 15 | 16 | console.log(`🚀 Frontend dev server will run on port: ${DEV_PORT}`); 17 | console.log(`📡 API proxy target: http://${API_HOST}:${API_PORT}`); 18 | 19 | return { 20 | root: "frontend", 21 | plugins: [react(), unocss()], 22 | build: { 23 | outDir: "../dist/client", 24 | manifest: true, 25 | }, 26 | 27 | // 开发服务器配置 28 | server: { 29 | port: DEV_PORT, 30 | host: true, // 允许外部访问 31 | proxy: { 32 | // 代理 API 请求到后端服务器 33 | "/api": { 34 | target: `http://${API_HOST}:${API_PORT}`, 35 | changeOrigin: true, 36 | secure: false, 37 | }, 38 | }, 39 | }, 40 | 41 | resolve: { 42 | alias: { 43 | "@frontend": resolve(__dirname, "src"), 44 | "@": resolve(__dirname, "src"), 45 | }, 46 | }, 47 | 48 | ssr: { 49 | noExternal: [ 50 | "react-router-dom", 51 | "@mui/material", 52 | "@mui/system", 53 | "@mui/icons-material", 54 | "@emotion/react", 55 | "@emotion/styled", 56 | "react-i18next", 57 | "i18next", 58 | ], 59 | }, 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "d9f15a02-4279-4217-8e61-2c26ad20a08a", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "features": { 8 | "name": "features", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "key": { 18 | "name": "key", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "name": { 25 | "name": "name", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | }, 31 | "description": { 32 | "name": "description", 33 | "type": "text", 34 | "primaryKey": false, 35 | "notNull": true, 36 | "autoincrement": false 37 | }, 38 | "enabled": { 39 | "name": "enabled", 40 | "type": "integer", 41 | "primaryKey": false, 42 | "notNull": true, 43 | "autoincrement": false, 44 | "default": false 45 | } 46 | }, 47 | "indexes": { 48 | "features_key_idx": { 49 | "name": "features_key_idx", 50 | "columns": [ 51 | "key" 52 | ], 53 | "isUnique": true 54 | } 55 | }, 56 | "foreignKeys": {}, 57 | "compositePrimaryKeys": {}, 58 | "uniqueConstraints": {}, 59 | "checkConstraints": {} 60 | } 61 | }, 62 | "views": {}, 63 | "enums": {}, 64 | "_meta": { 65 | "schemas": {}, 66 | "tables": {}, 67 | "columns": {} 68 | }, 69 | "internal": { 70 | "indexes": {} 71 | } 72 | } -------------------------------------------------------------------------------- /drizzle/0001_brainy_stranger.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `user_model_config` ( 2 | `id` text PRIMARY KEY NOT NULL, 3 | `user_id` text NOT NULL, 4 | `use_system_mapping` integer DEFAULT true NOT NULL, 5 | `custom_haiku` text, 6 | `custom_sonnet` text, 7 | `custom_opus` text, 8 | `created_at` integer NOT NULL, 9 | `updated_at` integer NOT NULL, 10 | FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade 11 | ); 12 | --> statement-breakpoint 13 | CREATE UNIQUE INDEX `user_model_config_user_id_unique` ON `user_model_config` (`user_id`);--> statement-breakpoint 14 | CREATE INDEX `user_model_config_user_id_idx` ON `user_model_config` (`user_id`);--> statement-breakpoint 15 | CREATE TABLE `user_sessions` ( 16 | `id` text PRIMARY KEY NOT NULL, 17 | `user_id` text NOT NULL, 18 | `session_token` text NOT NULL, 19 | `expires_at` integer NOT NULL, 20 | `created_at` integer NOT NULL, 21 | FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade 22 | ); 23 | --> statement-breakpoint 24 | CREATE UNIQUE INDEX `user_sessions_session_token_unique` ON `user_sessions` (`session_token`);--> statement-breakpoint 25 | CREATE INDEX `user_sessions_session_token_idx` ON `user_sessions` (`session_token`);--> statement-breakpoint 26 | CREATE INDEX `user_sessions_user_id_idx` ON `user_sessions` (`user_id`);--> statement-breakpoint 27 | CREATE TABLE `users` ( 28 | `id` text PRIMARY KEY NOT NULL, 29 | `github_id` text NOT NULL, 30 | `username` text NOT NULL, 31 | `email` text, 32 | `avatar_url` text, 33 | `api_key` text NOT NULL, 34 | `encrypted_provider_api_key` text, 35 | `created_at` integer DEFAULT (strftime('%s', 'now')) NOT NULL, 36 | `updated_at` integer DEFAULT (strftime('%s', 'now')) NOT NULL 37 | ); 38 | --> statement-breakpoint 39 | CREATE UNIQUE INDEX `users_github_id_unique` ON `users` (`github_id`);--> statement-breakpoint 40 | CREATE UNIQUE INDEX `users_api_key_unique` ON `users` (`api_key`);--> statement-breakpoint 41 | CREATE INDEX `users_github_id_idx` ON `users` (`github_id`);--> statement-breakpoint 42 | CREATE INDEX `users_api_key_idx` ON `users` (`api_key`); -------------------------------------------------------------------------------- /frontend/src/context/ThemeContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useMemo, useContext, ReactNode } from "react"; 2 | import { ThemeProvider as MuiThemeProvider, createTheme, type PaletteMode } from "@mui/material"; 3 | import { lightTheme, darkTheme } from "../theme"; 4 | import { useCallback } from "react"; 5 | 6 | type AppThemeContextType = { 7 | themeMode: PaletteMode; 8 | toggleTheme: () => void; 9 | }; 10 | 11 | const AppThemeContext = createContext({ 12 | themeMode: "dark", 13 | toggleTheme: () => {}, 14 | }); 15 | 16 | export const useAppTheme = () => useContext(AppThemeContext); 17 | 18 | const isBrowser = typeof window !== "undefined"; 19 | 20 | export const AppThemeProvider = ({ children }: { children: ReactNode }) => { 21 | const [themeMode, setThemeMode] = useState(() => { 22 | if (!isBrowser) return "dark"; 23 | try { 24 | const storedMode = localStorage.getItem("themeMode"); 25 | if (storedMode) { 26 | return storedMode as PaletteMode; 27 | } 28 | return "dark"; // Default to dark mode 29 | } catch (error) { 30 | // If localStorage is not available (e.g., in SSR or private mode), default to dark 31 | return "dark"; 32 | } 33 | }); 34 | 35 | const toggleTheme = useCallback(() => { 36 | setThemeMode((prevMode: PaletteMode) => { 37 | const newMode = prevMode === "light" ? "dark" : "light"; 38 | if (isBrowser) { 39 | try { 40 | localStorage.setItem("themeMode", newMode); 41 | } catch (error) { 42 | // Handle potential errors if localStorage is not available 43 | console.error("Failed to save theme mode to localStorage", error); 44 | } 45 | } 46 | return newMode; 47 | }); 48 | }, []); 49 | 50 | const theme = useMemo(() => (themeMode === "light" ? lightTheme : darkTheme), [themeMode]); 51 | 52 | return ( 53 | 54 | {children} 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /common/validators/config.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { createRoute, OpenAPIHono } from "@hono/zod-openapi"; 3 | 4 | const zodOpenAPIRequest = (req: any) => req; 5 | const zodOpenAPIResponse = (res: any) => res; 6 | 7 | export const ProviderConfigSchema = z.object({ 8 | baseUrl: z.string().url("请输入有效的 URL"), // 仅用于前端显示,不存储 9 | apiKey: z.string().min(1, "API Key 不能为空"), 10 | }); 11 | 12 | // 固定的三个模型映射配置 13 | export const ModelMappingConfigSchema = z.object({ 14 | haiku: z.string().min(1, "Haiku模型不能为空"), 15 | sonnet: z.string().min(1, "Sonnet模型不能为空"), 16 | opus: z.string().min(1, "Opus模型不能为空"), 17 | }); 18 | 19 | // 用户模型配置 20 | export const UserModelConfigSchema = z.object({ 21 | useSystemMapping: z.boolean(), // true=使用系统默认,false=使用自定义 22 | customMapping: ModelMappingConfigSchema.optional(), // 自定义映射配置 23 | }); 24 | 25 | export const UpdateUserConfigSchema = z.object({ 26 | provider: ProviderConfigSchema.optional(), 27 | modelConfig: UserModelConfigSchema.optional(), 28 | }); 29 | 30 | export const UserConfigSchema = z.object({ 31 | provider: ProviderConfigSchema, 32 | modelConfig: UserModelConfigSchema, 33 | }); 34 | 35 | // --- OpenAPI Routes --- 36 | 37 | export const getUserConfigRoute = createRoute({ 38 | method: "get", 39 | path: "/", 40 | summary: "获取用户配置", 41 | responses: { 42 | 200: zodOpenAPIResponse({ 43 | description: "成功获取用户配置", 44 | schema: UserConfigSchema, 45 | }), 46 | }, 47 | }); 48 | 49 | export const updateUserConfigRoute = createRoute({ 50 | method: "put", 51 | path: "/", 52 | summary: "更新用户配置", 53 | request: zodOpenAPIRequest({ 54 | body: { 55 | content: { 56 | "application/json": { 57 | schema: UpdateUserConfigSchema, 58 | }, 59 | }, 60 | }, 61 | }), 62 | responses: { 63 | 200: zodOpenAPIResponse({ 64 | description: "成功更新用户配置", 65 | schema: UserConfigSchema, 66 | }), 67 | }, 68 | }); 69 | 70 | export const resetMappingsRoute = createRoute({ 71 | method: "post", 72 | path: "/reset", 73 | summary: "重置模型映射到系统默认配置", 74 | responses: { 75 | 200: zodOpenAPIResponse({ 76 | description: "成功重置映射配置", 77 | schema: UserConfigSchema, 78 | }), 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /src/validators/config.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { 3 | createRoute, 4 | OpenAPIHono, 5 | } from "@hono/zod-openapi"; 6 | 7 | const zodOpenAPIRequest = (req: any) => req; 8 | const zodOpenAPIResponse = (res: any) => res; 9 | 10 | export const ProviderConfigSchema = z.object({ 11 | name: z.string().min(1, "提供商名称不能为空").max(50, "名称不能超过50个字符"), 12 | baseUrl: z.string().url("请输入有效的 URL"), 13 | apiKey: z.string().min(1, "API Key 不能为空"), 14 | }); 15 | 16 | export const ModelMappingSchema = z.object({ 17 | id: z.string(), 18 | keyword: z.string().min(1, "关键字不能为空").max(50, "关键字不能超过50个字符"), 19 | targetModel: z.string().min(1, "目标模型不能为空").max(100, "目标模型名称不能超过100个字符"), 20 | isEnabled: z.boolean(), 21 | }); 22 | 23 | export const UpdateUserConfigSchema = z.object({ 24 | provider: ProviderConfigSchema.optional(), 25 | mappings: z.array(ModelMappingSchema.omit({ id: true })).optional(), 26 | }); 27 | 28 | export const UserConfigSchema = z.object({ 29 | provider: ProviderConfigSchema.nullable(), 30 | mappings: z.array(ModelMappingSchema), 31 | }); 32 | 33 | // --- OpenAPI Routes --- 34 | 35 | export const getUserConfigRoute = createRoute({ 36 | method: "get", 37 | path: "/config", 38 | summary: "获取用户配置", 39 | responses: { 40 | 200: zodOpenAPIResponse({ 41 | description: "成功获取用户配置", 42 | schema: UserConfigSchema, 43 | }), 44 | }, 45 | }); 46 | 47 | export const updateUserConfigRoute = createRoute({ 48 | method: "put", 49 | path: "/config", 50 | summary: "更新用户配置", 51 | request: zodOpenAPIRequest({ 52 | body: { 53 | content: { 54 | "application/json": { 55 | schema: UpdateUserConfigSchema, 56 | }, 57 | }, 58 | }, 59 | }), 60 | responses: { 61 | 200: zodOpenAPIResponse({ 62 | description: "成功更新用户配置", 63 | schema: UserConfigSchema, 64 | }), 65 | }, 66 | }); 67 | 68 | export const getModelsRoute = createRoute({ 69 | method: "get", 70 | path: "/config/models", 71 | summary: "获取远程模型列表", 72 | responses: { 73 | 200: zodOpenAPIResponse({ 74 | description: "成功获取模型列表", 75 | schema: z.array(z.object({ id: z.string(), name: z.string() })), 76 | }), 77 | 400: zodOpenAPIResponse({ 78 | description: "配置错误", 79 | schema: z.object({ success: z.boolean(), message: z.string() }), 80 | }), 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /src/routes/feature.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; 2 | import { ErrorSchema, FeatureSchema, PathParamsSchema, UpdateFeatureSchema } from "../validators/feature.schema"; 3 | import { eq } from "drizzle-orm"; 4 | import { DrizzleD1Database } from "drizzle-orm/d1"; 5 | import * as schema from "../db/schema"; 6 | import { features } from "../db/schema"; 7 | 8 | const GetAllFeaturesRoute = createRoute({ 9 | method: "get", 10 | path: "/", 11 | responses: { 12 | 200: { 13 | content: { 14 | "application/json": { 15 | schema: z.array(FeatureSchema), 16 | }, 17 | }, 18 | description: "Retrieve all features", 19 | }, 20 | }, 21 | }); 22 | 23 | const UpdateFeatureRoute = createRoute({ 24 | method: "patch", 25 | path: "/{key}", 26 | request: { 27 | params: PathParamsSchema, 28 | body: { 29 | content: { 30 | "application/json": { 31 | schema: UpdateFeatureSchema, 32 | }, 33 | }, 34 | }, 35 | }, 36 | responses: { 37 | 200: { 38 | content: { 39 | "application/json": { 40 | schema: FeatureSchema, 41 | }, 42 | }, 43 | description: "Returns the updated feature", 44 | }, 45 | 404: { 46 | content: { 47 | "application/json": { 48 | schema: ErrorSchema, 49 | }, 50 | }, 51 | description: "Feature not found", 52 | }, 53 | }, 54 | }); 55 | 56 | const app = new OpenAPIHono<{ 57 | Variables: { 58 | db: DrizzleD1Database; 59 | }; 60 | }>() 61 | .openapi(GetAllFeaturesRoute, async (c) => { 62 | const db = c.get("db"); 63 | const allFeatures = await db.select().from(features).orderBy(features.id); 64 | return c.json(allFeatures); 65 | }) 66 | .openapi(UpdateFeatureRoute, async (c) => { 67 | const db = c.get("db"); 68 | const { key } = c.req.valid("param"); 69 | const { enabled } = c.req.valid("json"); 70 | 71 | const [updatedFeature] = await db.update(features).set({ enabled }).where(eq(features.key, key)).returning(); 72 | 73 | if (!updatedFeature) { 74 | return c.json( 75 | { 76 | code: 404, 77 | message: "Feature not found", 78 | }, 79 | 404, 80 | ); 81 | } 82 | 83 | const result = FeatureSchema.parse(updatedFeature); 84 | return c.json(result, 200); 85 | }); 86 | 87 | export type ApiRoutes = typeof app; 88 | 89 | export default app; 90 | -------------------------------------------------------------------------------- /frontend/src/pages/AuthCallbackPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, CircularProgress, Typography } from "@mui/material"; 2 | import { useEffect } from "react"; 3 | import { useLocation, useNavigate } from "react-router-dom"; 4 | import { useQueryClient } from "@tanstack/react-query"; 5 | import { useAuth } from "../hooks/useAuth"; 6 | import { AuthResponseSchema, UserInfoSchema } from "../../../common/validators/auth.schema"; 7 | import { useRef } from "react"; 8 | import { z } from "zod"; 9 | import { safeLocalStorage } from "../utils/storage"; 10 | 11 | export function AuthCallbackPage() { 12 | const navigate = useNavigate(); 13 | const location = useLocation(); 14 | const queryClient = useQueryClient(); 15 | const { refetch } = useAuth(); 16 | const isProcessing = useRef(false); 17 | 18 | useEffect(() => { 19 | const handleAuthCallback = async () => { 20 | if (isProcessing.current) { 21 | return; 22 | } 23 | isProcessing.current = true; 24 | 25 | const params = new URLSearchParams(location.search); 26 | const code = params.get("code"); 27 | const state = params.get("state"); 28 | 29 | if (!code || !state) { 30 | navigate("/"); 31 | return; 32 | } 33 | 34 | try { 35 | const response = await fetch(`/api/auth/github/callback?code=${code}&state=${state}`); 36 | const data: z.infer = await response.json(); 37 | 38 | if (data.success && data.data) { 39 | // 1. 设置 token 到 localStorage (会触发 auth_token_changed 事件) 40 | safeLocalStorage.setItem("auth_token", data.data.sessionToken); 41 | // 2. 设置用户数据到 QueryClient 缓存 42 | queryClient.setQueryData>(["auth", "user"], data.data.user); 43 | // 3. 触发 refetch 以确保状态同步 44 | await refetch(); 45 | // 4. 导航到控制台页面 46 | navigate("/dashboard"); 47 | } else { 48 | console.error("Authentication failed:", data.message); 49 | navigate("/"); 50 | } 51 | } catch (error) { 52 | console.error("Error during auth callback:", error); 53 | navigate("/"); 54 | } 55 | }; 56 | 57 | handleAuthCallback(); 58 | }, [location, navigate, queryClient, refetch]); 59 | 60 | return ( 61 | 70 | 71 | 正在验证您的身份,请稍候... 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /docs/TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # 🔧 故障排除指南 2 | 3 | 本指南收集了 NekroEdge 开发和部署过程中的常见问题及解决方案。 4 | 5 | ## 🚨 开发环境问题 6 | 7 | ### 启动失败 8 | 9 | #### 问题:`pnpm dev` 启动失败 10 | 11 | **错误信息**: `Module not found` 或 `Cannot resolve dependency` 12 | 13 | **解决方案**: 14 | 15 | ```bash 16 | # 1. 清理依赖缓存 17 | rm -rf node_modules pnpm-lock.yaml 18 | pnpm install 19 | 20 | # 2. 检查 Node.js 版本 21 | node --version # 需要 >= 18 22 | 23 | # 3. 检查 pnpm 版本 24 | pnpm --version # 需要 >= 8 25 | ``` 26 | 27 | ### 热重载问题 28 | 29 | #### 问题:热重载不工作 30 | 31 | **症状**: 修改代码后浏览器不自动更新 32 | 33 | **解决方案**: 34 | 35 | 1. **确认访问地址**: 必须使用 `localhost:5173`,不是 `8787` 36 | 2. **检查 WebSocket 连接**: 在浏览器控制台查看是否有连接错误 37 | 3. **重启开发服务器**: 38 | ```bash 39 | # Ctrl+C 停止服务器 40 | pnpm dev # 重新启动 41 | ``` 42 | 4. **检查防火墙设置**: 确保 5173 端口未被阻止 43 | 44 | ### 数据库问题 45 | 46 | #### 问题:数据库连接失败 47 | 48 | **错误信息**: `D1_ERROR` 或 `Database not found` 49 | 50 | **解决方案**: 51 | 52 | ```bash 53 | # 1. 重新初始化本地数据库 54 | rm -rf .wrangler/state 55 | pnpm db:migrate 56 | 57 | # 2. 检查数据库配置 58 | cat wrangler.jsonc # 确认配置正确 59 | 60 | # 3. 手动创建迁移 61 | pnpm db:generate 62 | pnpm db:migrate 63 | ``` 64 | 65 | #### 问题:迁移文件冲突 66 | 67 | **解决方案**: 68 | 69 | ```bash 70 | # 1. 备份数据 71 | cp .wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite backup/ 72 | 73 | # 2. 重置迁移 74 | rm -rf drizzle/* 75 | pnpm db:generate 76 | 77 | # 3. 重新应用迁移 78 | pnpm db:migrate 79 | ``` 80 | 81 | ## 🏗️ 构建问题 82 | 83 | ### Vite 构建错误 84 | 85 | #### 问题:模块解析失败 86 | 87 | **错误信息**: `Failed to resolve import` 或 `Cannot find module` 88 | 89 | **解决方案**: 90 | 91 | ```typescript 92 | // frontend/vite.config.mts 93 | export default defineConfig({ 94 | ssr: { 95 | noExternal: [ 96 | "react-router-dom", 97 | "@mui/material", 98 | "@mui/system", 99 | "@mui/icons-material", 100 | "@emotion/react", 101 | "@emotion/styled", 102 | // 添加导致问题的模块 103 | ], 104 | }, 105 | }); 106 | ``` 107 | 108 | ### Wrangler 构建错误 109 | 110 | #### 问题:esbuild 打包失败 111 | 112 | **错误信息**: `Build failed` 或 `Transform failed` 113 | 114 | **解决方案**: 115 | 116 | ```jsonc 117 | // wrangler.jsonc 118 | { 119 | "build": { 120 | "command": "pnpm build", 121 | }, 122 | "compatibility_flags": ["nodejs_compat"], 123 | "node_compat": true, 124 | } 125 | ``` 126 | 127 | ### 报告问题 128 | 129 | 如果以上方法都无法解决问题,请: 130 | 131 | 1. **收集信息**: 132 | - 错误信息截图 133 | - 相关日志输出 134 | - 系统环境信息 135 | - 复现步骤 136 | 137 | 2. **提交 Issue**: 138 | - [GitHub Issues](https://github.com/KroMiose/nekro-edge-template/issues) 139 | - 提供详细的问题描述和环境信息 140 | 141 | 3. **社区讨论**: 142 | - [GitHub Discussions](https://github.com/KroMiose/nekro-edge-template/discussions) 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-code-nexus", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently \"npm:dev:backend\" \"npm:dev:frontend\"", 7 | "dev:backend": "wrangler dev", 8 | "dev:frontend": "vite --config frontend/vite.config.mts", 9 | "build": "pnpm db:migrate:prod && pnpm generate:html && pnpm build:client && pnpm build:server", 10 | "build:client": "vite build --config frontend/vite.config.mts", 11 | "build:server": "vite build --ssr src/entry-server.tsx --outDir ../dist/server --config frontend/vite.config.mts", 12 | "generate:html": "tsx scripts/generateHtml.ts", 13 | "deploy": "pnpm build && wrangler deploy --env production", 14 | "serve:prod": "pnpm build && wrangler dev --env production", 15 | "test": "vitest", 16 | "cf-typegen": "wrangler types", 17 | "db:generate": "drizzle-kit generate", 18 | "db:migrate": "wrangler d1 migrations apply DB --local", 19 | "db:migrate:prod": "wrangler d1 migrations apply DB --env production --remote", 20 | "db:studio": "drizzle-kit studio", 21 | "db:check": "drizzle-kit check", 22 | "db:seed": "wrangler d1 execute DB --local --file=./drizzle/seed.sql", 23 | "db:seed:prod": "wrangler d1 execute DB --remote --file=./drizzle/seed.sql", 24 | "typecheck": "tsc --noEmit && tsc -p frontend/tsconfig.json --noEmit", 25 | "lint": "tsx", 26 | "format": "eslint . --fix" 27 | }, 28 | "devDependencies": { 29 | "@cloudflare/vitest-pool-workers": "^0.7.5", 30 | "@cloudflare/workers-types": "^4.20250320.0", 31 | "@hono/vite-cloudflare-pages": "^0.4.2", 32 | "@types/node": "^20.14.9", 33 | "@types/react": "^18.3.3", 34 | "@types/react-dom": "^18.3.0", 35 | "@vitejs/plugin-react": "^4.3.1", 36 | "@vitejs/plugin-react-swc": "^3.10.2", 37 | "concurrently": "^8.2.2", 38 | "drizzle-kit": "^0.31.4", 39 | "prettier": "^3.3.2", 40 | "rimraf": "^5.0.7", 41 | "tsx": "^4.20.3", 42 | "typescript": "^5.5.2", 43 | "unocss": "^0.61.0", 44 | "vite": "^5.3.1", 45 | "vitest": "~3.0.7", 46 | "wrangler": "^4.30.0" 47 | }, 48 | "dependencies": { 49 | "@emotion/react": "^11.14.0", 50 | "@emotion/styled": "^11.14.1", 51 | "@hono/swagger-ui": "^0.5.1", 52 | "@hono/zod-openapi": "0.18.0", 53 | "@mui/icons-material": "^5.17.1", 54 | "@mui/material": "^5.17.1", 55 | "@mui/system": "^7.1.1", 56 | "@paralleldrive/cuid2": "^2.2.2", 57 | "@tanstack/react-query": "^5.81.2", 58 | "@unocss/reset": "^66.3.2", 59 | "drizzle-orm": "^0.44.2", 60 | "drizzle-zod": "^0.8.2", 61 | "framer-motion": "^12.23.0", 62 | "hono": "4.4.0", 63 | "i18next": "^25.2.1", 64 | "i18next-browser-languagedetector": "^8.2.0", 65 | "react": "^18.3.1", 66 | "react-dom": "^18.3.1", 67 | "react-i18next": "^15.5.3", 68 | "react-router-dom": "6.23.1", 69 | "zod": "^3.22.4" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.prod 99 | .env.development.local 100 | .env.test.local 101 | .env.production.local 102 | .env.local 103 | 104 | # parcel-bundler cache (https://parceljs.org/) 105 | 106 | .cache 107 | .parcel-cache 108 | 109 | # Next.js build output 110 | 111 | .next 112 | out 113 | 114 | # Nuxt.js build / generate output 115 | 116 | .nuxt 117 | dist 118 | 119 | # Gatsby files 120 | 121 | .cache/ 122 | 123 | # Comment in the public line in if your project uses Gatsby and not Next.js 124 | 125 | # https://nextjs.org/blog/next-9-1#public-directory-support 126 | 127 | # public 128 | 129 | # vuepress build output 130 | 131 | .vuepress/dist 132 | 133 | # vuepress v2.x temp and cache directory 134 | 135 | .temp 136 | .cache 137 | 138 | # Docusaurus cache and generated files 139 | 140 | .docusaurus 141 | 142 | # Serverless directories 143 | 144 | .serverless/ 145 | 146 | # FuseBox cache 147 | 148 | .fusebox/ 149 | 150 | # DynamoDB Local files 151 | 152 | .dynamodb/ 153 | 154 | # TernJS port file 155 | 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | 160 | .vscode-test 161 | 162 | # yarn v2 163 | 164 | .yarn/cache 165 | .yarn/unplugged 166 | .yarn/build-state.yml 167 | .yarn/install-state.gz 168 | .pnp.\* 169 | 170 | # wrangler project 171 | 172 | .dev.vars 173 | .wrangler/ 174 | 175 | # Local dev database 176 | local.db 177 | *.db 178 | 179 | # Article 180 | articles/ 181 | -------------------------------------------------------------------------------- /src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { sqliteTable, text, integer, unique, index } from "drizzle-orm/sqlite-core"; 2 | import { createId } from "@paralleldrive/cuid2"; 3 | import { sql } from "drizzle-orm"; 4 | 5 | // 保留原有的 features 表 6 | export const features = sqliteTable( 7 | "features", 8 | { 9 | id: integer("id").primaryKey(), 10 | key: text("key").notNull(), 11 | name: text("name").notNull(), 12 | description: text("description").notNull(), 13 | enabled: integer("enabled", { mode: "boolean" }).notNull().default(false), 14 | }, 15 | (table) => [unique("features_key_idx").on(table.key)], 16 | ); 17 | 18 | // AI 代理服务相关表结构 19 | 20 | // 用户表 21 | export const users = sqliteTable( 22 | "users", 23 | { 24 | id: text("id") 25 | .primaryKey() 26 | .$defaultFn(() => createId()), 27 | githubId: text("github_id").notNull().unique(), 28 | username: text("username").notNull(), 29 | email: text("email"), 30 | avatarUrl: text("avatar_url"), 31 | apiKey: text("api_key") 32 | .notNull() 33 | .unique() 34 | .$defaultFn(() => `ak-${createId()}`), 35 | encryptedProviderApiKey: text("encrypted_provider_api_key"), 36 | providerBaseUrl: text("provider_base_url"), 37 | createdAt: integer("created_at", { mode: "timestamp" }) 38 | .notNull() 39 | .default(sql`(strftime('%s', 'now'))`), 40 | updatedAt: integer("updated_at", { mode: "timestamp" }) 41 | .notNull() 42 | .default(sql`(strftime('%s', 'now'))`), 43 | }, 44 | (table) => [index("users_github_id_idx").on(table.githubId), index("users_api_key_idx").on(table.apiKey)], 45 | ); 46 | 47 | // 用户会话表 48 | export const userSessions = sqliteTable( 49 | "user_sessions", 50 | { 51 | id: text("id") 52 | .primaryKey() 53 | .$defaultFn(() => createId()), 54 | userId: text("user_id") 55 | .notNull() 56 | .references(() => users.id, { onDelete: "cascade" }), 57 | sessionToken: text("session_token").notNull().unique(), 58 | expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), 59 | createdAt: integer("created_at", { mode: "timestamp" }) 60 | .notNull() 61 | .$defaultFn(() => new Date()), 62 | }, 63 | (table) => [ 64 | index("user_sessions_session_token_idx").on(table.sessionToken), 65 | index("user_sessions_user_id_idx").on(table.userId), 66 | ], 67 | ); 68 | 69 | // 用户模型映射配置表 - 存储用户的映射模式和自定义配置 70 | export const userModelConfig = sqliteTable( 71 | "user_model_config", 72 | { 73 | id: text("id") 74 | .primaryKey() 75 | .$defaultFn(() => createId()), 76 | userId: text("user_id") 77 | .notNull() 78 | .unique() 79 | .references(() => users.id, { onDelete: "cascade" }), 80 | useSystemMapping: integer("use_system_mapping", { mode: "boolean" }).notNull().default(true), // true=使用系统默认映射,false=使用自定义映射 81 | // 自定义映射配置(JSON格式存储三个固定映射) 82 | customHaiku: text("custom_haiku"), // 自定义haiku映射 83 | customSonnet: text("custom_sonnet"), // 自定义sonnet映射 84 | customOpus: text("custom_opus"), // 自定义opus映射 85 | createdAt: integer("created_at", { mode: "timestamp" }) 86 | .notNull() 87 | .$defaultFn(() => new Date()), 88 | updatedAt: integer("updated_at", { mode: "timestamp" }) 89 | .notNull() 90 | .$defaultFn(() => new Date()), 91 | }, 92 | (table) => [index("user_model_config_user_id_idx").on(table.userId)], 93 | ); 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Claude Code Nexus 2 | 3 | [English](README_EN.md) | 中文版 4 | 5 | > 🤖 **一个 Claude API 代理服务平台 - 让 Claude Code CLI 无缝兼容任何 OpenAI API 服务** 6 | 7 | [![部署状态](https://img.shields.io/badge/部署-在线-brightgreen)](https://claude.nekro.ai/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![QQ群1]()](https://qm.qq.com/q/eT30LxDcSA) [![QQ群2]()](https://qm.qq.com/q/ZQ6QHdkXu0) [![Discord](https://img.shields.io/badge/Discord-加入频道-5865F2?style=flat-square&logo=discord)](https://discord.gg/eMsgwFnxUB) 8 | 9 | **Claude Code Nexus** 是一个部署在 Cloudflare 上的高性能 AI 代理服务平台。它专为 [Claude Code CLI](https://github.com/claude-code/cli) 设计,通过一个兼容层,让你可以将 Claude Code 的请求无缝转发到**任何 OpenAI 兼容的 API 服务**,例如 OneAPI、Azure OpenAI、本地的 Ollama,或是其他任何遵循 OpenAI 规范的 LLM 服务。 10 | 11 | ## ✨ 核心价值 12 | 13 | - **🔓 供应商解锁**: 不再被锁定在单一的 AI 服务提供商。你可以自由选择性价比最高、性能最好的服务。 14 | - **🔌 无缝兼容**: 兼容 Claude Messages API,包括流式响应 (SSE)、工具使用 (Tool Use) 和多模态输入。 15 | - **🎯 智能模型映射**: 在网页上轻松配置模型映射规则(例如,将 `claude-3-haiku` 映射到 `gpt-4o-mini`),Claude Code CLI 无需任何改动。 16 | - **🔐 安全可靠**: API Key 在数据库中加密存储,用户数据严格隔离。 17 | - **🚀 全球加速**: 基于 Cloudflare 的全球网络,为你的 AI 应用提供低延迟、高可用的访问体验。 18 | - **🌍 开源可控**: 项目完全开源,你可以自行部署、修改和扩展,数据和服务完全由你掌控。 19 | 20 | ## 🚀 快速开始 (3步) 21 | 22 | ### 1. 登录 & 获取 API Key 23 | 24 | 访问 **[https://claude.nekro.ai/](https://claude.nekro.ai/)**,使用你的 GitHub 账户登录。系统会自动为你生成一个专属的 API Key。 25 | 26 | ### 2. 配置你的后端服务 27 | 28 | 在控制台中,配置你的 OpenAI 兼容 API 服务。你需要提供: 29 | 30 | - **Base URL**: 你的 API 服务地址 (例如: `https://api.oneapi.com`) 31 | - **API Key**: 你的 API 服务密钥 32 | 33 | ### 3. 在 Claude Code 中使用 34 | 35 | 在你的终端中设置以下环境变量: 36 | 37 | ```bash 38 | # 1. 设置你的专属 API Key 39 | export ANTHROPIC_API_KEY="ak-your-nexus-key" 40 | 41 | # 2. 设置代理服务地址 42 | export ANTHROPIC_BASE_URL="https://claude.nekro.ai" 43 | 44 | # 3. 正常使用 Claude Code! 45 | claude 46 | ``` 47 | 48 | 🎉 **完成!** 现在你的 Claude Code CLI 已经通过 Claude Code Nexus 代理,使用你自己的后端服务了。 49 | 50 | ## 🛠️ 技术栈 51 | 52 | - **后端**: [Hono](https://hono.dev/) on [Cloudflare Workers](https://workers.cloudflare.com/) - 轻量、快速的边缘计算后端。 53 | - **前端**: [React](https://react.dev/) + [Vite](https://vitejs.dev/) on [Cloudflare Pages](https://pages.cloudflare.com/) - 现代、高效的前端开发体验。 54 | - **数据库**: [Cloudflare D1](https://developers.cloudflare.com/d1/) + [Drizzle ORM](https://orm.drizzle.team/) - 类型安全的无服务器 SQL 数据库。 55 | - **UI**: [Material-UI](https://mui.com/) - 成熟、美观的 React 组件库。 56 | - **认证**: GitHub OAuth。 57 | 58 | ## 📚 文档 59 | 60 | 我们提供了完整的项目需求和实现细节文档: 61 | 62 | - [**项目需求文档 (PRD)**](./REQUIREMENTS.md) - 深入了解项目的设计理念、功能架构和技术实现细节。 63 | 64 | ## 🔗 相关项目 65 | 66 | 如果您正在寻找一款高扩展性的 AI Agent 框架,我们推荐您关注我们的另一个项目: 67 | 68 | **[Nekro Agent](https://github.com/KroMiose/nekro-agent)** - 一个集代码执行能力与高度可扩展性为一体的多人跨平台聊天机器人框架。支持沙盒驱动、可视化界面、高扩展性插件系统,原生支持 QQ、Discord、Minecraft、B站直播等多种平台。如果您需要构建智能聊天机器人或自动化 Agent 系统,Nekro Agent 将是您的理想选择。 69 | 70 | --- 71 | 72 | ## 🤝 参与贡献 73 | 74 | 欢迎通过以下方式参与项目: 75 | 76 | - 🐛 **报告问题**: [在 GitHub Issues 中提交 Bug](https://github.com/KroMiose/claude-code-nexus/issues) 77 | - 💡 **提出建议**: [在 GitHub Discussions 中分享你的想法](https://github.com/KroMiose/claude-code-nexus/discussions) 78 | - ⭐ 如果你觉得这个项目对你有帮助,请给一个 **Star**! 79 | 80 | ## 📄 许可证 81 | 82 | 本项目基于 [MIT License](./LICENSE) 开源。 83 | 84 | ## ⭐ Star 趋势 85 | 86 | ![Star History Chart](https://api.star-history.com/svg?repos=KroMiose/claude-code-nexus&type=Date) 87 | -------------------------------------------------------------------------------- /src/config/seo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 集中化的SEO配置文件 3 | * 其他开发者只需要修改这一个文件即可完成所有SEO配置 4 | */ 5 | 6 | export interface SEOConfig { 7 | // 基础信息 8 | siteName: string; 9 | siteUrl: string; 10 | title: string; 11 | description: string; 12 | keywords: string[]; 13 | author: string; 14 | language: string; 15 | 16 | // 社交媒体 17 | ogImage: string; 18 | twitterHandle?: string; 19 | 20 | // 品牌色彩 21 | themeColor: string; 22 | 23 | // 页面配置 24 | pages: { 25 | [path: string]: { 26 | title?: string; 27 | description?: string; 28 | keywords?: string[]; 29 | changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"; 30 | priority?: number; 31 | }; 32 | }; 33 | } 34 | 35 | /** 36 | * 默认SEO配置 37 | * 🎯 用户只需要修改这个配置对象即可完成整站SEO设置 38 | */ 39 | export const seoConfig: SEOConfig = { 40 | // 🌟 基础网站信息(必须修改) 41 | siteName: "Claude Code Nexus", 42 | siteUrl: "https://claude.nekro.ai", 43 | title: "Claude Code Nexus - 自由切换后端的 Claude Code CLI 代理平台", 44 | description: 45 | "一个开源的 Claude API 代理服务平台,让您的 Claude Code CLI 无缝兼容任何 OpenAI API 服务,如 OneAPI、Azure OpenAI 或本地 Ollama。提供多用户隔离、图形化配置和开源自部署能力。", 46 | keywords: [ 47 | "Claude Code", 48 | "Claude API", 49 | "OpenAI", 50 | "API Proxy", 51 | "API Gateway", 52 | "OneAPI", 53 | "Ollama", 54 | "Anthropic", 55 | "Cloudflare", 56 | "Hono", 57 | "React", 58 | "开源", 59 | // 兼容模型供应商 60 | "Gemini", 61 | "通义千问", 62 | "Qwen", 63 | "豆包", 64 | "Kimi", 65 | "Moonshot AI", 66 | "智谱清言", 67 | "Zhipu AI", 68 | "ChatGLM", 69 | "百度千帆", 70 | "Baidu Qianfan", 71 | "科大讯飞", 72 | "Spark", 73 | "百川", 74 | "Baichuan", 75 | "腾讯混元", 76 | "Hunyuan", 77 | "商汤日日新", 78 | "SenseNova", 79 | ], 80 | author: "Claude Code Nexus Team", 81 | language: "zh-CN", 82 | 83 | // 🎨 社交媒体和品牌 84 | ogImage: "/og-image.png", // 建议在 public 目录下创建一个 og-image.png 85 | themeColor: "#4A90E2", // Claude-like blue color 86 | 87 | // 📄 页面级配置 88 | pages: { 89 | "/": { 90 | title: "Claude Code Nexus - 首页 | 兼容 OpenAI 的 Claude API 代理", 91 | description: 92 | "了解如何使用 Claude Code Nexus 将您的 Claude Code CLI 连接到任何 OpenAI 兼容的 API 服务,实现模型自由、降低成本。", 93 | changefreq: "monthly", 94 | priority: 1.0, 95 | }, 96 | "/dashboard": { 97 | title: "控制台 - Claude Code Nexus", 98 | description: "管理您的 API Key、配置后端 OpenAI 服务地址、自定义模型映射规则。", 99 | changefreq: "yearly", 100 | priority: 0.5, 101 | }, 102 | }, 103 | }; 104 | 105 | /** 106 | * 生成页面的完整标题 107 | */ 108 | export function generatePageTitle(path: string): string { 109 | const pageConfig = seoConfig.pages[path]; 110 | return pageConfig?.title || `${seoConfig.title} | ${seoConfig.siteName}`; 111 | } 112 | 113 | /** 114 | * 生成页面描述 115 | */ 116 | export function generatePageDescription(path: string): string { 117 | const pageConfig = seoConfig.pages[path]; 118 | return pageConfig?.description || seoConfig.description; 119 | } 120 | 121 | /** 122 | * 生成页面关键词 123 | */ 124 | export function generatePageKeywords(path: string): string { 125 | const pageConfig = seoConfig.pages[path]; 126 | const keywords = pageConfig?.keywords || seoConfig.keywords; 127 | return keywords.join(","); 128 | } 129 | 130 | /** 131 | * 生成完整的页面URL 132 | */ 133 | export function generatePageUrl(path: string): string { 134 | return `${seoConfig.siteUrl}${path === "/" ? "" : path}`; 135 | } 136 | -------------------------------------------------------------------------------- /docs/INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # 📋 安装配置指南 2 | 3 | 本指南将详细介绍 NekroEdge 模板的安装、配置和初始化过程。 4 | 5 | ## 📋 系统要求 6 | 7 | ### 必需环境 8 | 9 | - **Node.js** >= 18.0.0 10 | - **pnpm** >= 8.0.0 (推荐) 或 npm >= 9.0.0 11 | - **Git** (用于版本控制) 12 | 13 | ### 推荐工具 14 | 15 | - **VS Code** + TypeScript 扩展 16 | - **Chrome/Edge** (用于调试) 17 | - **Cloudflare Account** (用于部署) 18 | 19 | ## 🚀 完整安装流程 20 | 21 | ### 1. 获取项目 22 | 23 | #### 方式一:使用 GitHub 模板 (🌟 强烈推荐) 24 | 25 | 1. 访问 [NekroEdge 模板页面](https://github.com/KroMiose/nekro-edge-template) 26 | 2. 点击绿色的 **"Use this template"** 按钮 27 | 3. 选择 **"Create a new repository"** 28 | 4. 填写你的仓库名称和描述 29 | 5. 选择仓库可见性(公开/私有) 30 | 6. 点击 **"Create repository"** 31 | 32 | ```bash 33 | # 克隆你新创建的仓库 34 | git clone https://github.com/YOUR_USERNAME/your-project-name.git 35 | cd your-project-name 36 | ``` 37 | 38 | > 💡 **为什么推荐这种方式?** 39 | > 40 | > - 自动创建独立的 Git 历史 41 | > - 保持与原模板的松耦合关系 42 | > - 方便后续获取模板更新 43 | > - 符合 GitHub 的最佳实践 44 | 45 | #### 方式二:Fork 仓库 (适合贡献代码) 46 | 47 | 如果你计划向原模板贡献代码,可以选择 Fork: 48 | 49 | 1. 在 [GitHub 模板页面](https://github.com/KroMiose/nekro-edge-template) 点击 **"Fork"** 50 | 2. 克隆你的 Fork 51 | 52 | ```bash 53 | git clone https://github.com/YOUR_USERNAME/nekro-edge-template.git your-project-name 54 | cd your-project-name 55 | ``` 56 | 57 | ### 2. 安装依赖 58 | 59 | ```bash 60 | # 使用 pnpm (推荐,更快更省空间) 61 | pnpm install 62 | 63 | # 或使用 npm 64 | npm install 65 | ``` 66 | 67 | ### 3. 初始化数据库 68 | 69 | ```bash 70 | # 生成数据库迁移文件 (如果需要) 71 | pnpm db:generate 72 | 73 | # 运行数据库迁移,创建表结构 74 | pnpm db:migrate 75 | 76 | # 可选:打开数据库管理界面查看 77 | pnpm db:studio 78 | ``` 79 | 80 | ### 4. 启动开发环境 81 | 82 | ```bash 83 | # 启动开发服务器 84 | pnpm dev 85 | ``` 86 | 87 | ### 5. 验证安装 88 | 89 | 访问以下地址确认安装成功: 90 | 91 | - ✅ **前端**: http://localhost:5173 - 应显示项目首页 92 | - ✅ **API**: http://localhost:8787/api/posts - 应返回 JSON 数据 93 | - ✅ **文档**: http://localhost:8787/api/doc - 应显示 Swagger 文档 94 | 95 | ## ⚙️ 环境变量配置 96 | 97 | ### 创建环境配置文件 98 | 99 | 在项目根目录创建 `.env` 文件: 100 | 101 | ```bash 102 | # 前端开发服务器配置 103 | VITE_PORT=5173 104 | 105 | # API 服务器配置 106 | VITE_API_HOST=localhost 107 | VITE_API_PORT=8787 108 | 109 | # 开发环境标识 110 | NODE_ENV=development 111 | 112 | # 可选:数据库调试 113 | DB_DEBUG=true 114 | ``` 115 | 116 | ### 配置说明 117 | 118 | | 变量名 | 说明 | 默认值 | 示例 | 119 | | --------------- | ------------------ | ------------- | ------------ | 120 | | `VITE_PORT` | 前端开发服务器端口 | `5173` | `3000` | 121 | | `VITE_API_HOST` | API 服务器主机 | `localhost` | `127.0.0.1` | 122 | | `VITE_API_PORT` | API 服务器端口 | `8787` | `8000` | 123 | | `NODE_ENV` | 环境标识 | `development` | `production` | 124 | | `DB_DEBUG` | 数据库调试日志 | `false` | `true` | 125 | 126 | ## 🔧 开发工具配置 127 | 128 | ### VS Code 推荐扩展 129 | 130 | 创建 `.vscode/extensions.json`: 131 | 132 | ```json 133 | { 134 | "recommendations": [ 135 | "bradlc.vscode-tailwindcss", 136 | "esbenp.prettier-vscode", 137 | "ms-vscode.vscode-typescript-next", 138 | "formulahendry.auto-rename-tag", 139 | "ms-vscode.vscode-json" 140 | ] 141 | } 142 | ``` 143 | 144 | ## 🚨 常见安装问题 145 | 146 | ### 端口冲突 147 | 148 | 如果 5173 或 8787 端口被占用: 149 | 150 | ```bash 151 | # 修改 .env.vars 文件中的端口 152 | VITE_PORT=3000 153 | VITE_API_PORT=8000 154 | ``` 155 | 156 | ### 数据库连接失败 157 | 158 | ```bash 159 | # 清理并重新初始化数据库 160 | rm -rf .wrangler 161 | pnpm db:migrate 162 | ``` 163 | 164 | ### 热重载不工作 165 | 166 | 1. 确认从正确端口访问 (5173 不是 8787) 167 | 2. 检查防火墙是否阻止 WebSocket 连接 168 | 3. 尝试重启开发服务器 169 | 170 | ```bash 171 | # 停止服务器 (Ctrl+C) 然后重启 172 | pnpm dev 173 | ``` 174 | 175 | ## 🔄 下一步 176 | 177 | 安装完成后,建议阅读: 178 | 179 | - [⚙️ 开发指南](./DEVELOPMENT.md) - 了解日常开发工作流 180 | - [🎨 主题定制指南](./THEMING.md) - 自定义应用外观 181 | - [🔌 API 开发指南](./API_GUIDE.md) - 创建后端功能 182 | 183 | ## 💡 小贴士 184 | 185 | - 推荐使用 **pnpm** 而非 npm,速度更快且节省磁盘空间 186 | - 开发时优先使用 **5173 端口**,享受热重载功能 187 | - 遇到问题首先查看 **控制台日志**,大部分错误信息很明确 188 | - 定期运行 `pnpm type-check` 确保类型安全 189 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Claude Code Nexus - 首页 | 兼容 OpenAI 的 Claude API 代理 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/config/defaultModelMappings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 固定的三个模型映射配置 3 | * 4 | * Claude 只支持这三个固定的模型系列:haiku、sonnet、opus 5 | * 用户可以选择使用系统默认映射或配置自定义映射 6 | */ 7 | 8 | export interface ModelMappingRule { 9 | keyword: string; 10 | description: string; 11 | } 12 | 13 | export interface ModelMappingConfig { 14 | haiku: string; 15 | sonnet: string; 16 | opus: string; 17 | } 18 | 19 | // 预设的API供应商配置 20 | export interface ApiProvider { 21 | name: string; 22 | baseUrl: string; 23 | description: string; 24 | } 25 | 26 | export const PRESET_API_PROVIDERS: ApiProvider[] = [ 27 | { 28 | name: "NekroAI 中转", 29 | baseUrl: "https://api.nekro.ai/v1", 30 | description: "Nekro AI 中转服务", 31 | }, 32 | { 33 | name: "谷歌Gemini", 34 | baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", 35 | description: "Google Gemini API 服务", 36 | }, 37 | { 38 | name: "通义千问", 39 | baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", 40 | description: "阿里云通义千问 API 服务", 41 | }, 42 | { 43 | name: "豆包", 44 | baseUrl: "https://ark.cn-beijing.volces.com/api/v3", 45 | description: "字节跳动豆包 API 服务", 46 | }, 47 | { 48 | name: "Kimi", 49 | baseUrl: "https://api.moonshot.cn/v1", 50 | description: "月之暗面 Kimi API 服务", 51 | }, 52 | { 53 | name: "智谱清言", 54 | baseUrl: "https://open.bigmodel.cn/api/paas/v4", 55 | description: "智谱AI 清言 API 服务", 56 | }, 57 | { 58 | name: "百度千帆", 59 | baseUrl: "https://qianfan.baidubce.com/v2", 60 | description: "百度千帆大模型 API 服务", 61 | }, 62 | { 63 | name: "科大讯飞", 64 | baseUrl: "https://spark-api-open.xf-yun.com/v1", 65 | description: "科大讯飞星火 API 服务", 66 | }, 67 | { 68 | name: "百川", 69 | baseUrl: "https://api.baichuan-ai.com/v1", 70 | description: "百川智能 API 服务", 71 | }, 72 | { 73 | name: "腾讯混元", 74 | baseUrl: "https://api.hunyuan.cloud.tencent.com/v1", 75 | description: "腾讯混元大模型 API 服务", 76 | }, 77 | { 78 | name: "商汤日日新", 79 | baseUrl: "https://api.sensenova.cn/compatible-mode/v1", 80 | description: "商汤科技日日新 API 服务", 81 | }, 82 | ]; 83 | 84 | // 固定的三个模型规则 85 | export const FIXED_MODEL_RULES: ModelMappingRule[] = [ 86 | { 87 | keyword: "haiku", 88 | description: "轻量级模型,在快速响应和简单任务中使用", 89 | }, 90 | { 91 | keyword: "sonnet", 92 | description: "平衡性能的通用模型,在大多数场景中使用", 93 | }, 94 | { 95 | keyword: "opus", 96 | description: "高性能模型,在复杂推理编码任务中使用", 97 | }, 98 | ]; 99 | 100 | // 系统默认映射配置 101 | export const DEFAULT_MAPPING_CONFIG: ModelMappingConfig = { 102 | haiku: "gemini-2.5-flash-nothinking", 103 | sonnet: "gemini-2.5-pro", 104 | opus: "gemini-2.5-pro", 105 | }; 106 | 107 | // 默认的 API 配置 108 | export const DEFAULT_API_CONFIG = { 109 | baseUrl: "https://api.nekro.ai/v1", 110 | name: "Nekro AI", 111 | }; 112 | 113 | /** 114 | * 获取系统默认映射配置 115 | */ 116 | export function getDefaultMappingConfig(): ModelMappingConfig { 117 | return { ...DEFAULT_MAPPING_CONFIG }; 118 | } 119 | 120 | /** 121 | * 根据模型关键词查找目标模型 122 | * @param keyword 模型关键词 123 | * @param config 映射配置 124 | * @returns 目标模型名称,如果没有找到则返回原关键词 125 | */ 126 | export function findTargetModel(keyword: string, config: ModelMappingConfig): string { 127 | const normalizedKeyword = keyword.toLowerCase(); 128 | 129 | // 精确匹配或包含关键词 130 | for (const rule of FIXED_MODEL_RULES) { 131 | if (normalizedKeyword.includes(rule.keyword)) { 132 | return config[rule.keyword as keyof ModelMappingConfig]; 133 | } 134 | } 135 | 136 | // 如果没有匹配到,返回原关键词 137 | return keyword; 138 | } 139 | 140 | /** 141 | * 验证映射配置是否完整 142 | * @param config 映射配置 143 | * @returns 是否有效 144 | */ 145 | export function isValidMappingConfig(config: any): config is ModelMappingConfig { 146 | return ( 147 | config && 148 | typeof config.haiku === "string" && 149 | config.haiku.length > 0 && 150 | typeof config.sonnet === "string" && 151 | config.sonnet.length > 0 && 152 | typeof config.opus === "string" && 153 | config.opus.length > 0 154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /src/routes/config.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIHono } from "@hono/zod-openapi"; 2 | import { and, eq, gt } from "drizzle-orm"; 3 | import { users, userSessions } from "../db/schema"; 4 | import { encryptApiKey, decryptApiKey } from "../utils/encryption"; 5 | import { 6 | getUserConfigRoute, 7 | updateUserConfigRoute, 8 | resetMappingsRoute, 9 | UpdateUserConfigSchema, 10 | } from "@common/validators/config.schema"; 11 | import { ModelMappingService } from "../services/modelMappingService"; 12 | import { authMiddleware } from "../middleware/auth"; 13 | import type { Bindings } from "../types"; 14 | import type { DrizzleD1Database } from "drizzle-orm/d1"; 15 | import * as drizzleSchema from "../db/schema"; 16 | 17 | type Variables = { 18 | db: DrizzleD1Database; 19 | user: typeof drizzleSchema.users.$inferSelect; 20 | }; 21 | 22 | const app = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>(); 23 | 24 | app.use("/*", authMiddleware); 25 | 26 | app 27 | .openapi(getUserConfigRoute, async (c) => { 28 | const user = c.get("user"); 29 | const db = c.get("db"); 30 | const mappingService = new ModelMappingService(db); 31 | 32 | const modelConfig = await mappingService.getUserModelConfig(user.id); 33 | 34 | const providerApiKey = user.encryptedProviderApiKey 35 | ? await decryptApiKey(user.encryptedProviderApiKey, c.env.ENCRYPTION_KEY) 36 | : null; 37 | 38 | // 始终返回默认baseUrl和用户的apiKey 39 | const defaultApiConfig = mappingService.getDefaultApiConfig(); 40 | 41 | return c.json({ 42 | provider: { 43 | baseUrl: user.providerBaseUrl || defaultApiConfig.baseUrl, 44 | apiKey: providerApiKey || "", 45 | }, 46 | modelConfig, 47 | }); 48 | }) 49 | .openapi(updateUserConfigRoute, async (c) => { 50 | const user = c.get("user"); 51 | const db = c.get("db"); 52 | const mappingService = new ModelMappingService(db); 53 | const { provider, modelConfig } = await c.req.json(); 54 | 55 | if (provider) { 56 | const updateData: { 57 | encryptedProviderApiKey?: string; 58 | providerBaseUrl?: string; 59 | updatedAt: Date; 60 | } = { 61 | updatedAt: new Date(), 62 | }; 63 | 64 | if (provider.apiKey) { 65 | updateData.encryptedProviderApiKey = await encryptApiKey( 66 | provider.apiKey, 67 | c.env.ENCRYPTION_KEY, 68 | ); 69 | } 70 | if (provider.baseUrl) { 71 | updateData.providerBaseUrl = provider.baseUrl; 72 | } 73 | 74 | await db.update(users).set(updateData).where(eq(users.id, user.id)); 75 | } 76 | 77 | if (modelConfig) { 78 | await mappingService.updateUserModelConfig(user.id, modelConfig); 79 | } 80 | 81 | const updatedUser = await db.query.users.findFirst({ where: eq(users.id, user.id) }); 82 | const updatedModelConfig = await mappingService.getUserModelConfig(user.id); 83 | const decryptedApiKey = updatedUser?.encryptedProviderApiKey 84 | ? await decryptApiKey(updatedUser.encryptedProviderApiKey, c.env.ENCRYPTION_KEY) 85 | : null; 86 | 87 | const defaultApiConfig = mappingService.getDefaultApiConfig(); 88 | 89 | return c.json({ 90 | provider: { 91 | baseUrl: updatedUser?.providerBaseUrl || defaultApiConfig.baseUrl, 92 | apiKey: decryptedApiKey || "", 93 | }, 94 | modelConfig: updatedModelConfig, 95 | }); 96 | }) 97 | 98 | .openapi(resetMappingsRoute, async (c) => { 99 | const user = c.get("user"); 100 | const db = c.get("db"); 101 | const mappingService = new ModelMappingService(db); 102 | 103 | const modelConfig = await mappingService.resetToSystemMapping(user.id); 104 | 105 | const providerApiKey = user.encryptedProviderApiKey 106 | ? await decryptApiKey(user.encryptedProviderApiKey, c.env.ENCRYPTION_KEY) 107 | : null; 108 | 109 | const defaultApiConfig = mappingService.getDefaultApiConfig(); 110 | 111 | return c.json({ 112 | provider: { 113 | baseUrl: defaultApiConfig.baseUrl, 114 | apiKey: providerApiKey || "", 115 | }, 116 | modelConfig, 117 | }); 118 | }); 119 | 120 | export default app; 121 | -------------------------------------------------------------------------------- /src/utils/encryption.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * API 密钥加密工具函数 3 | * 使用 Web Crypto API 提供安全的加密/解密功能 4 | */ 5 | 6 | // 将字符串转换为 ArrayBuffer 7 | function stringToArrayBuffer(str: string): ArrayBuffer { 8 | const encoder = new TextEncoder(); 9 | const uint8Array = encoder.encode(str); 10 | const buffer = new ArrayBuffer(uint8Array.length); 11 | const view = new Uint8Array(buffer); 12 | view.set(uint8Array); 13 | return buffer; 14 | } 15 | 16 | // 将 ArrayBuffer 转换为字符串 17 | function arrayBufferToString(buffer: ArrayBuffer): string { 18 | const decoder = new TextDecoder(); 19 | return decoder.decode(buffer); 20 | } 21 | 22 | // 将 ArrayBuffer 转换为 Base64 字符串 23 | function arrayBufferToBase64(buffer: ArrayBuffer): string { 24 | const bytes = new Uint8Array(buffer); 25 | let binary = ""; 26 | for (let i = 0; i < bytes.byteLength; i++) { 27 | binary += String.fromCharCode(bytes[i]); 28 | } 29 | return btoa(binary); 30 | } 31 | 32 | // 将 Base64 字符串转换为 ArrayBuffer 33 | function base64ToArrayBuffer(base64: string): ArrayBuffer { 34 | const binary = atob(base64); 35 | const bytes = new Uint8Array(binary.length); 36 | for (let i = 0; i < binary.length; i++) { 37 | bytes[i] = binary.charCodeAt(i); 38 | } 39 | return bytes.buffer; 40 | } 41 | 42 | // 从密钥字符串生成 CryptoKey 43 | async function getKey(keyString: string): Promise { 44 | const keyData = stringToArrayBuffer(keyString); 45 | 46 | // 如果密钥长度不是32字节,则进行哈希处理 47 | let keyBuffer: ArrayBuffer; 48 | if (keyData.byteLength !== 32) { 49 | keyBuffer = await crypto.subtle.digest("SHA-256", keyData); 50 | } else { 51 | keyBuffer = keyData; 52 | } 53 | 54 | return await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); 55 | } 56 | 57 | /** 58 | * 加密 API 密钥 59 | * @param plaintext 明文 API 密钥 60 | * @param keyString 加密密钥 61 | * @returns 加密后的 Base64 字符串(包含 IV) 62 | */ 63 | export async function encryptApiKey(plaintext: string, keyString: string): Promise { 64 | try { 65 | const key = await getKey(keyString); 66 | const iv = crypto.getRandomValues(new Uint8Array(12)); // AES-GCM 使用 12 字节 IV 67 | const plaintextBuffer = stringToArrayBuffer(plaintext); 68 | 69 | const encryptedBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, plaintextBuffer); 70 | 71 | // 将 IV 和加密数据合并 72 | const combined = new Uint8Array(iv.length + encryptedBuffer.byteLength); 73 | combined.set(iv); 74 | combined.set(new Uint8Array(encryptedBuffer), iv.length); 75 | 76 | return arrayBufferToBase64(combined.buffer); 77 | } catch (error) { 78 | console.error("加密失败:", error); 79 | throw new Error("API 密钥加密失败"); 80 | } 81 | } 82 | 83 | /** 84 | * 解密 API 密钥 85 | * @param encryptedData 加密的 Base64 字符串 86 | * @param keyString 解密密钥 87 | * @returns 解密后的明文 API 密钥 88 | */ 89 | export async function decryptApiKey(encryptedData: string, keyString: string): Promise { 90 | try { 91 | const key = await getKey(keyString); 92 | const combinedBuffer = base64ToArrayBuffer(encryptedData); 93 | 94 | // 分离 IV 和加密数据 95 | const iv = combinedBuffer.slice(0, 12); 96 | const encryptedBuffer = combinedBuffer.slice(12); 97 | 98 | const decryptedBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, encryptedBuffer); 99 | 100 | return arrayBufferToString(decryptedBuffer); 101 | } catch (error) { 102 | console.error("解密失败:", error); 103 | throw new Error("API 密钥解密失败"); 104 | } 105 | } 106 | 107 | /** 108 | * 为前端显示生成掩码 API 密钥 109 | * @param apiKey 完整的 API 密钥 110 | * @returns 掩码后的 API 密钥 111 | */ 112 | export function maskApiKey(apiKey: string): string { 113 | if (apiKey.length <= 8) { 114 | return "*".repeat(apiKey.length); 115 | } 116 | 117 | const start = apiKey.substring(0, 4); 118 | const end = apiKey.substring(apiKey.length - 4); 119 | const middle = "*".repeat(Math.max(4, apiKey.length - 8)); 120 | 121 | return `${start}${middle}${end}`; 122 | } 123 | 124 | /** 125 | * 生成用户专属的 API 密钥 126 | * @returns 随机生成的 API 密钥 127 | */ 128 | export function generateUserApiKey(): string { 129 | const array = new Uint8Array(32); 130 | crypto.getRandomValues(array); 131 | return "ak-" + Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(""); 132 | } 133 | -------------------------------------------------------------------------------- /src/services/modelMappingService.ts: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm"; 2 | import { userModelConfig } from "../db/schema"; 3 | import { 4 | getDefaultMappingConfig, 5 | findTargetModel as findTargetModelFromConfig, 6 | type ModelMappingConfig, 7 | FIXED_MODEL_RULES, 8 | DEFAULT_API_CONFIG, 9 | } from "../config/defaultModelMappings"; 10 | import type { DrizzleD1Database } from "drizzle-orm/d1"; 11 | import * as drizzleSchema from "../db/schema"; 12 | import { createId } from "@paralleldrive/cuid2"; 13 | 14 | export interface UserModelConfigData { 15 | useSystemMapping: boolean; 16 | customMapping?: ModelMappingConfig; 17 | } 18 | 19 | /** 20 | * 模型映射服务 - 重构为模式切换 21 | */ 22 | export class ModelMappingService { 23 | constructor(private db: DrizzleD1Database) {} 24 | 25 | /** 26 | * 获取用户的模型配置 27 | * @param userId 用户ID 28 | * @returns 用户模型配置 29 | */ 30 | async getUserModelConfig(userId: string): Promise { 31 | const config = await this.db.query.userModelConfig.findFirst({ 32 | where: eq(userModelConfig.userId, userId), 33 | }); 34 | 35 | if (!config) { 36 | // 返回默认配置:使用系统映射 37 | return { 38 | useSystemMapping: true, 39 | customMapping: undefined, 40 | }; 41 | } 42 | 43 | const customMapping = 44 | config.customHaiku && config.customSonnet && config.customOpus 45 | ? { 46 | haiku: config.customHaiku, 47 | sonnet: config.customSonnet, 48 | opus: config.customOpus, 49 | } 50 | : undefined; 51 | 52 | return { 53 | useSystemMapping: config.useSystemMapping, 54 | customMapping, 55 | }; 56 | } 57 | 58 | /** 59 | * 更新用户模型配置 60 | * @param userId 用户ID 61 | * @param configData 新的配置数据 62 | */ 63 | async updateUserModelConfig(userId: string, configData: UserModelConfigData): Promise { 64 | const existingConfig = await this.db.query.userModelConfig.findFirst({ 65 | where: eq(userModelConfig.userId, userId), 66 | }); 67 | 68 | const updateData = { 69 | useSystemMapping: configData.useSystemMapping, 70 | customHaiku: configData.customMapping?.haiku || null, 71 | customSonnet: configData.customMapping?.sonnet || null, 72 | customOpus: configData.customMapping?.opus || null, 73 | updatedAt: new Date(), 74 | }; 75 | 76 | if (existingConfig) { 77 | await this.db.update(userModelConfig).set(updateData).where(eq(userModelConfig.userId, userId)); 78 | } else { 79 | await this.db.insert(userModelConfig).values({ 80 | id: createId(), 81 | userId, 82 | ...updateData, 83 | createdAt: new Date(), 84 | }); 85 | } 86 | } 87 | 88 | /** 89 | * 重置用户映射到系统默认配置 90 | * @param userId 用户ID 91 | * @returns 重置后的配置 92 | */ 93 | async resetToSystemMapping(userId: string): Promise { 94 | const resetConfig: UserModelConfigData = { 95 | useSystemMapping: true, 96 | customMapping: undefined, 97 | }; 98 | 99 | await this.updateUserModelConfig(userId, resetConfig); 100 | return resetConfig; 101 | } 102 | 103 | /** 104 | * 获取当前生效的映射配置 105 | * @param userId 用户ID 106 | * @returns 当前生效的映射配置 107 | */ 108 | async getEffectiveMappingConfig(userId: string): Promise { 109 | const userConfig = await this.getUserModelConfig(userId); 110 | 111 | if (userConfig.useSystemMapping || !userConfig.customMapping) { 112 | return getDefaultMappingConfig(); 113 | } 114 | 115 | return userConfig.customMapping; 116 | } 117 | 118 | /** 119 | * 根据模型名称查找映射的目标模型 120 | * @param userId 用户ID 121 | * @param modelName 模型名称 122 | * @returns 映射的目标模型名称 123 | */ 124 | async findTargetModel(userId: string, modelName: string): Promise { 125 | const effectiveConfig = await this.getEffectiveMappingConfig(userId); 126 | return findTargetModelFromConfig(modelName, effectiveConfig); 127 | } 128 | 129 | /** 130 | * 获取固定的模型规则(用于前端显示) 131 | * @returns 固定的三个模型规则 132 | */ 133 | getFixedModelRules() { 134 | return FIXED_MODEL_RULES; 135 | } 136 | 137 | /** 138 | * 获取默认API配置 139 | * @returns 默认API配置 140 | */ 141 | getDefaultApiConfig() { 142 | return DEFAULT_API_CONFIG; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /docs/SEO_GUIDE.md: -------------------------------------------------------------------------------- 1 | # 🎯 SEO 配置指南 2 | 3 | > **一分钟配置,全站 SEO 优化完成!** 4 | 5 | 本模板提供了**集中化、类型安全**的 SEO 配置系统,您只需要修改一个文件即可完成整站的 SEO 设置。 6 | 7 | ## 📖 快速开始 8 | 9 | ### 1. 修改 SEO 配置 10 | 11 | 打开 `src/config/seo.ts` 文件,根据您的项目信息修改配置: 12 | 13 | ```typescript 14 | export const seoConfig: SEOConfig = { 15 | // 🌟 基础网站信息(必须修改) 16 | siteName: "Your App Name", 17 | siteUrl: "https://your-domain.com", 18 | title: "Your App - 简短描述", 19 | description: "详细描述您的应用功能和特色...", 20 | keywords: ["关键词1", "关键词2", "关键词3"], 21 | author: "Your Team Name", 22 | language: "zh-CN", // 或 "en-US" 23 | 24 | // 🎨 社交媒体和品牌 25 | ogImage: "/og-image.png", // 1200x630 像素 26 | twitterHandle: "your_twitter", // 可选 27 | themeColor: "#your-brand-color", 28 | 29 | // 📄 页面级配置 30 | pages: { 31 | "/": { 32 | title: "首页标题 - 品牌名", 33 | changefreq: "weekly", 34 | priority: 1.0, 35 | }, 36 | "/about": { 37 | title: "关于我们 - 品牌名", 38 | description: "关于页面的描述...", 39 | changefreq: "monthly", 40 | priority: 0.8, 41 | }, 42 | }, 43 | }; 44 | ``` 45 | 46 | ### 2. 生成 HTML 模板 47 | 48 | 修改配置后,运行以下命令更新 HTML 模板: 49 | 50 | ```bash 51 | pnpm generate:html 52 | ``` 53 | 54 | ### 3. 部署 55 | 56 | ```bash 57 | pnpm deploy 58 | ``` 59 | 60 | 就这么简单!🎉 61 | 62 | ## 🔧 高级配置 63 | 64 | ### 页面级别的 SEO 设置 65 | 66 | 每个页面可以有独立的 SEO 配置: 67 | 68 | ```typescript 69 | pages: { 70 | "/products": { 71 | title: "产品列表 - 我的商城", 72 | description: "查看我们的全部产品,包括...", 73 | keywords: ["产品", "商城", "购买"], 74 | changefreq: "daily", 75 | priority: 0.9, 76 | }, 77 | "/blog": { 78 | title: "博客 - 我的网站", 79 | description: "最新的技术文章和行业资讯", 80 | changefreq: "weekly", 81 | priority: 0.7, 82 | } 83 | } 84 | ``` 85 | 86 | ### 动态页面的 SEO(高级) 87 | 88 | 如果您需要为动态路由设置 SEO,可以在渲染时动态生成: 89 | 90 | ```typescript 91 | // 在您的路由组件中 92 | import { generatePageTitle, generatePageDescription } from "@/config/seo"; 93 | 94 | // 动态设置页面标题 95 | document.title = generatePageTitle(`/blog/${postId}`); 96 | ``` 97 | 98 | ## 📊 自动生成的功能 99 | 100 | 配置完成后,系统会自动为您生成: 101 | 102 | ### ✅ Meta 标签 103 | 104 | - 完整的 SEO meta 标签 105 | - Open Graph 标签(Facebook、LinkedIn 分享) 106 | - Twitter Card 标签 107 | - 结构化数据(JSON-LD) 108 | 109 | ### ✅ 搜索引擎优化 110 | 111 | - 自动生成 `robots.txt` 112 | - 自动生成 `sitemap.xml` 113 | - 规范化 URL(canonical) 114 | 115 | ### ✅ 社交媒体优化 116 | 117 | - 分享卡片预览 118 | - 品牌一致性 119 | - 移动端优化 120 | 121 | ## 🎨 社交媒体图片 122 | 123 | 创建一张 **1200x630 像素**的图片,命名为 `og-image.png`,放在 `frontend/public/` 目录下。 124 | 125 | **图片内容建议:** 126 | 127 | - 您的 Logo 128 | - 应用名称 129 | - 简短的标语 130 | - 品牌色彩 131 | 132 | ## 🔍 SEO 检查清单 133 | 134 | 部署后,使用以下工具验证 SEO 效果: 135 | 136 | ### 必做检查 137 | 138 | - [ ] **Google Rich Results Test**: https://search.google.com/test/rich-results 139 | - [ ] **Facebook Sharing Debugger**: https://developers.facebook.com/tools/debug/ 140 | - [ ] **Twitter Card Validator**: https://cards-dev.twitter.com/validator 141 | 142 | ### 可选检查 143 | 144 | - [ ] 访问 `your-domain.com/robots.txt` 145 | - [ ] 访问 `your-domain.com/sitemap.xml` 146 | - [ ] 在不同设备上测试分享效果 147 | 148 | ## 📈 最佳实践 149 | 150 | ### 标题优化 151 | 152 | ```typescript 153 | // ✅ 好的标题格式 154 | title: "具体功能 - 品牌名 | 核心价值"; 155 | 156 | // ❌ 避免 157 | title: "欢迎来到我们的网站"; 158 | ``` 159 | 160 | ### 描述优化 161 | 162 | ```typescript 163 | // ✅ 好的描述 164 | description: "使用我们的工具可以帮您快速完成 X 任务,支持 Y 功能,已有 10000+ 用户选择。"; 165 | 166 | // ❌ 避免 167 | description: "这是一个很棒的网站"; 168 | ``` 169 | 170 | ### 关键词选择 171 | 172 | ```typescript 173 | // ✅ 精准相关的关键词 174 | keywords: ["任务管理", "团队协作", "项目管理工具", "在线办公"]; 175 | 176 | // ❌ 避免堆砌 177 | keywords: ["好用", "强大", "完美", "最佳"]; 178 | ``` 179 | 180 | ## 🚀 模板架构优势 181 | 182 | ### 🎯 集中化配置 183 | 184 | - **单一数据源**:所有 SEO 配置在一个文件中 185 | - **类型安全**:TypeScript 确保配置正确性 186 | - **零重复**:HTML 模板统一生成,避免重复代码 187 | 188 | ### 🔄 自动化流程 189 | 190 | - **构建时生成**:HTML 模板在构建时自动更新 191 | - **动态内容**:robots.txt 和 sitemap.xml 实时生成 192 | - **缓存优化**:SEO 文件包含合理的缓存策略 193 | 194 | ### 🎨 开发友好 195 | 196 | - **即时更新**:修改配置后立即生效 197 | - **文档完整**:每个配置项都有清晰说明 198 | - **易于扩展**:可轻松添加新页面的 SEO 配置 199 | 200 | ## 🤔 常见问题 201 | 202 | ### Q: 如何为新页面添加 SEO 配置? 203 | 204 | A: 在 `seoConfig.pages` 中添加对应路径的配置即可。 205 | 206 | ### Q: 修改配置后需要重新部署吗? 207 | 208 | A: 是的,需要运行 `pnpm generate:html && pnpm deploy`。 209 | 210 | ### Q: 如何测试 SEO 效果? 211 | 212 | A: 使用文档中提到的在线工具,或在社交媒体平台测试分享效果。 213 | 214 | ### Q: 可以为不同语言设置不同的 SEO 吗? 215 | 216 | A: 目前单语言支持,多语言 SEO 需要额外的配置。 217 | 218 | --- 219 | 220 | **🎉 现在您的网站已经具备了企业级的 SEO 配置!** 221 | -------------------------------------------------------------------------------- /docs/DEPLOYMENT.md: -------------------------------------------------------------------------------- 1 | # 📦 部署指南 2 | 3 | 本指南将详细介绍如何将 Claude Code Nexus AI 代理服务部署到 Cloudflare Pages & Workers 生产环境。 4 | 5 | > 🤖 **特别说明**: 本项目是一个完整的 AI 代理服务,需要配置 GitHub OAuth、API 密钥加密等特殊环境变量。 6 | 7 | ## 🚀 部署前准备 8 | 9 | ### 1. 准备 Cloudflare 账户 10 | 11 | - 注册 [Cloudflare 账户](https://dash.cloudflare.com/sign-up) 12 | - 确保账户已验证邮箱 13 | - 准备一个域名 (可选,Cloudflare 会提供子域名) 14 | 15 | ### 2. 环境变量准备 16 | 17 | 在部署前,您需要准备以下关键环境变量: 18 | 19 | #### GitHub OAuth 应用配置 20 | 21 | 1. 访问 [GitHub Developer Settings](https://github.com/settings/developers) 22 | 2. 点击 **"New OAuth App"** 23 | 3. 填写应用信息: 24 | - **Application name**: `Claude Code Nexus`(或您的应用名称) 25 | - **Homepage URL**: `https://your-domain.com`(您的实际域名) 26 | - **Authorization callback URL**: `https://your-domain.com/auth/callback` 27 | 4. 创建后记录 **Client ID** 和 **Client Secret** 28 | 29 | #### 加密密钥生成 30 | 31 | ```bash 32 | # 生成 256 位的加密密钥(用于 API 密钥加密) 33 | node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" 34 | ``` 35 | 36 | ### 3. 准备代码仓库 37 | 38 | ```bash 39 | # 确保代码已推送到 Git 仓库 (GitHub/GitLab) 40 | git add . 41 | git commit -m "Ready for deployment" 42 | git push origin main 43 | ``` 44 | 45 | ### 4. 本地环境测试 46 | 47 | 在部署前,建议先在本地测试完整配置: 48 | 49 | ```bash 50 | # 复制环境变量模板 51 | cp env.example .env 52 | 53 | # 编辑 .env 文件,填入实际值 54 | nano .env 55 | ``` 56 | 57 | `.env` 文件示例: 58 | 59 | ```bash 60 | # GitHub OAuth 配置 61 | GITHUB_CLIENT_ID=your_github_client_id 62 | GITHUB_CLIENT_SECRET=your_github_client_secret 63 | 64 | # 加密密钥(32字节十六进制字符串) 65 | ENCRYPTION_KEY=your_generated_256_bit_key_in_hex 66 | 67 | # 应用基础 URL(本地开发时为 localhost) 68 | APP_BASE_URL=http://localhost:8787 69 | ``` 70 | 71 | ```bash 72 | # 启动本地开发服务器测试 73 | pnpm dev 74 | 75 | # 测试 GitHub OAuth 登录功能 76 | # 访问 http://localhost:8787 并尝试登录 77 | ``` 78 | 79 | ## 🗄️ 生产数据库配置 80 | 81 | ### 1. 创建生产数据库 82 | 83 | ```bash 84 | # 创建生产 D1 数据库 85 | npx wrangler d1 create your-prod-db-name 86 | 87 | # 示例输出: 88 | # ✅ Successfully created DB 'your-prod-db-name' 89 | # 90 | # [[d1_databases]] 91 | # binding = "DB" 92 | # database_name = "your-prod-db-name" 93 | # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 94 | ``` 95 | 96 | ### 2. 更新配置文件 97 | 98 | 将上面的输出信息更新到 `wrangler.jsonc`: 99 | 100 | ```jsonc 101 | { 102 | "env": { 103 | "production": { 104 | "d1_databases": [ 105 | { 106 | "binding": "DB", 107 | "database_name": "your-prod-db-name", // 👈 替换这里 108 | "database_id": "your-database-id", // 👈 替换这里 109 | "migrations_dir": "drizzle", 110 | }, 111 | ], 112 | "vars": { 113 | "NODE_ENV": "production", 114 | "APP_BASE_URL": "https://your-app-name.pages.dev", // 👈 替换为实际域名 115 | }, 116 | "assets": { 117 | "binding": "ASSETS", 118 | "directory": "./dist/client", 119 | }, 120 | }, 121 | }, 122 | } 123 | ``` 124 | 125 | > 📝 **注意**: `GITHUB_CLIENT_ID`、`GITHUB_CLIENT_SECRET` 和 `ENCRYPTION_KEY` 等敏感变量应该在 Cloudflare Pages Dashboard 中设置。 126 | 127 | ### 3. 运行生产数据库迁移 128 | 129 | ```bash 130 | # 应用数据库迁移到生产环境 131 | pnpm db:migrate:prod 132 | 133 | # 验证迁移成功 134 | npx wrangler d1 execute your-prod-db-name --env production --command "SELECT name FROM sqlite_master WHERE type='table';" 135 | ``` 136 | 137 | ## 🌐 Cloudflare Pages 部署 138 | 139 | ### 方式一:通过 Dashboard 部署 140 | 141 | #### 1. 连接 Git 仓库 142 | 143 | 1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/) 144 | 2. 进入 **Workers & Pages** → **Pages** 145 | 3. 点击 **"Create a project"** 146 | 4. 选择 **"Connect to Git"** 147 | 5. 授权并选择你的 Git 仓库 148 | 149 | #### 2. 配置构建设置 150 | 151 | 在部署配置页面设置: 152 | 153 | | 配置项 | 值 | 154 | | ------------ | -------------------------------------- | 155 | | **项目名称** | `your-app-name` | 156 | | **生产分支** | `main` | 157 | | **构建命令** | `pnpm build` | 158 | | **部署命令** | `npx wrangler deploy --env production` | 159 | | **根目录** | `/` | 160 | 161 | #### 3. 设置环境变量 162 | 163 | 在 **Settings** → **Environment variables** 中添加以下环境变量: 164 | 165 | #### 🔐 必需的环境变量(生产环境) 166 | 167 | ```bash 168 | # 基础配置 169 | NODE_ENV=production 170 | VITE_PORT=5173 171 | 172 | # GitHub OAuth 配置(从步骤2获取) 173 | GITHUB_CLIENT_ID=your_github_client_id 174 | GITHUB_CLIENT_SECRET=your_github_client_secret 175 | 176 | # API 密钥加密(从步骤2生成) 177 | ENCRYPTION_KEY=your_generated_256_bit_key_in_hex 178 | 179 | # 应用基础 URL(替换为您的实际域名) 180 | APP_BASE_URL=https://your-app-name.pages.dev 181 | ``` 182 | 183 | > ⚠️ **安全提醒**: 184 | > 185 | > - `GITHUB_CLIENT_SECRET` 和 `ENCRYPTION_KEY` 是敏感信息,务必保密 186 | > - 在 Cloudflare Pages 中设置的环境变量会自动加密存储 187 | > - `APP_BASE_URL` 必须与 GitHub OAuth 应用配置的域名一致 188 | 189 | #### 4. 配置兼容性标志 190 | 191 | 在 **Settings** → **Functions** 中设置: 192 | 193 | - **Compatibility date**: `2024-07-29` 194 | - **Compatibility flags**: `nodejs_compat` 195 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # Claude Code Nexus 2 | 3 | [中文版](README.md) | English 4 | 5 | > 🤖 **A Claude API Proxy Service Platform - Seamlessly Compatible with Any OpenAI API Service for Claude Code CLI** 6 | 7 | [![Deployment Status](https://img.shields.io/badge/Deployment-Online-brightgreen)](https://claude.nekro.ai/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![QQ Group 1]()](https://qm.qq.com/q/eT30LxDcSA) [![QQ Group 2]()](https://qm.qq.com/q/ZQ6QHdkXu0) [![Discord](https://img.shields.io/badge/Discord-Join_Channel-5865F2?style=flat-square&logo=discord)](https://discord.gg/eMsgwFnxUB) 8 | 9 | **Claude Code Nexus** is a high-performance AI proxy service platform deployed on Cloudflare. It's specifically designed for [Claude Code CLI](https://github.com/claude-code/cli), providing a compatibility layer that allows you to seamlessly forward Claude Code requests to **any OpenAI-compatible API service**, such as OneAPI, Azure OpenAI, local Ollama, or any other LLM service that follows the OpenAI specification. 10 | 11 | ## ✨ Core Value 12 | 13 | - **🔓 Vendor Unlock**: No longer locked to a single AI service provider. You can freely choose the service with the best cost-performance ratio and performance. 14 | - **🔌 Seamless Compatibility**: Compatible with Claude Messages API, including streaming responses (SSE), tool use (Tool Use), and multimodal input. 15 | - **🎯 Smart Model Mapping**: Easily configure model mapping rules on the web (e.g., map `claude-3-haiku` to `gpt-4o-mini`), with no changes needed in Claude Code CLI. 16 | - **🔐 Secure & Reliable**: API Keys are encrypted and stored in the database, with strict user data isolation. 17 | - **🚀 Global Acceleration**: Based on Cloudflare's global network, providing low-latency, high-availability access for your AI applications. 18 | - **🌍 Open Source & Controllable**: The project is completely open source, allowing you to deploy, modify, and extend it yourself, with complete control over your data and services. 19 | 20 | ## 🚀 Quick Start (3 Steps) 21 | 22 | ### 1. Login & Get API Key 23 | 24 | Visit **[https://claude.nekro.ai/](https://claude.nekro.ai/)** and log in with your GitHub account. The system will automatically generate a dedicated API Key for you. 25 | 26 | ### 2. Configure Your Backend Service 27 | 28 | In the console, configure your OpenAI-compatible API service. You'll need to provide: 29 | 30 | - **Base URL**: Your API service address (e.g., `https://api.oneapi.com`) 31 | - **API Key**: Your API service secret key 32 | 33 | ### 3. Use in Claude Code 34 | 35 | Set the following environment variables in your terminal: 36 | 37 | ```bash 38 | # 1. Set your dedicated API Key 39 | export ANTHROPIC_API_KEY="ak-your-nexus-key" 40 | 41 | # 2. Set the proxy service address 42 | export ANTHROPIC_BASE_URL="https://claude.nekro.ai" 43 | 44 | # 3. Use Claude Code normally! 45 | claude 46 | ``` 47 | 48 | 🎉 **Done!** Now your Claude Code CLI is proxied through Claude Code Nexus, using your own backend service. 49 | 50 | ## 🛠️ Tech Stack 51 | 52 | - **Backend**: [Hono](https://hono.dev/) on [Cloudflare Workers](https://workers.cloudflare.com/) - Lightweight, fast edge computing backend. 53 | - **Frontend**: [React](https://react.dev/) + [Vite](https://vitejs.dev/) on [Cloudflare Pages](https://pages.cloudflare.com/) - Modern, efficient frontend development experience. 54 | - **Database**: [Cloudflare D1](https://developers.cloudflare.com/d1/) + [Drizzle ORM](https://orm.drizzle.team/) - Type-safe serverless SQL database. 55 | - **UI**: [Material-UI](https://mui.com/) - Mature, beautiful React component library. 56 | - **Authentication**: GitHub OAuth. 57 | 58 | ## 📚 Documentation 59 | 60 | We provide comprehensive project requirements and implementation details: 61 | 62 | - [**Project Requirements Document (PRD)**](./REQUIREMENTS.md) - Deep dive into the project's design philosophy, functional architecture, and technical implementation details. 63 | 64 | ## 🔗 Related Projects 65 | 66 | If you're looking for a highly extensible AI Agent framework, we recommend checking out our other project: 67 | 68 | **[Nekro Agent](https://github.com/KroMiose/nekro-agent)** - A multi-platform chat robot framework that combines code execution capabilities with high extensibility. Features sandbox-driven execution, visual interface, highly extensible plugin system, and native support for QQ, Discord, Minecraft, Bilibili Live, and other platforms. If you need to build intelligent chatbots or automated Agent systems, Nekro Agent will be your ideal choice. 69 | 70 | --- 71 | 72 | ## 🤝 Contributing 73 | 74 | Welcome to participate in the project through the following ways: 75 | 76 | - 🐛 **Report Issues**: [Submit bugs in GitHub Issues](https://github.com/KroMiose/claude-code-nexus/issues) 77 | - 💡 **Suggest Ideas**: [Share your thoughts in GitHub Discussions](https://github.com/KroMiose/claude-code-nexus/discussions) 78 | - ⭐ If you find this project helpful, please give it a **Star**! 79 | 80 | ## 📄 License 81 | 82 | This project is licensed under the [MIT License](./LICENSE). 83 | 84 | ## ⭐ Star Trend 85 | 86 | ![Star History Chart](https://api.star-history.com/svg?repos=KroMiose/claude-code-nexus&type=Date) 87 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | Box, 4 | Button, 5 | Container, 6 | Toolbar, 7 | Typography, 8 | CssBaseline, 9 | useTheme, 10 | Avatar, 11 | Menu, 12 | MenuItem, 13 | IconButton, 14 | CircularProgress, 15 | } from "@mui/material"; 16 | import { 17 | AccountCircle as AccountIcon, 18 | Dashboard as DashboardIcon, 19 | ExitToApp as LogoutIcon, 20 | GitHub as GitHubIcon, 21 | } from "@mui/icons-material"; 22 | import { Link as RouterLink, Outlet, useLocation } from "react-router-dom"; 23 | import { useState } from "react"; 24 | import { Footer } from "./components/Footer"; 25 | import { NekroEdgeLogo } from "./assets/logos"; 26 | import { ToggleThemeButton } from "./components/ToggleThemeButton"; 27 | import { useAuth } from "./hooks/useAuth"; 28 | 29 | function App() { 30 | const location = useLocation(); 31 | const theme = useTheme(); 32 | const { user, isAuthenticated, isLoading, login, logout } = useAuth(); 33 | const [anchorEl, setAnchorEl] = useState(null); 34 | 35 | const handleMenuOpen = (event: React.MouseEvent) => { 36 | setAnchorEl(event.currentTarget); 37 | }; 38 | 39 | const handleMenuClose = () => { 40 | setAnchorEl(null); 41 | }; 42 | 43 | const handleLogout = () => { 44 | logout(); 45 | handleMenuClose(); 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 61 | 62 | 63 | 74 | 75 | 85 | Claude Code Nexus 86 | 87 | 88 | 89 | 90 | 91 | {/* 导航链接 */} 92 | 93 | {isAuthenticated && ( 94 | <> 95 | 107 | 108 | )} 109 | 110 | 111 | 112 | {/* 用户认证区域 */} 113 | {isLoading ? ( 114 | 115 | ) : isAuthenticated ? ( 116 | <> 117 | 126 | 127 | {user?.username?.charAt(0).toUpperCase()} 128 | 129 | 130 | 138 | 139 | 140 | 控制台 141 | 142 | 143 | 144 | 登出 145 | 146 | 147 | 148 | ) : ( 149 | 152 | )} 153 | 154 | 155 | 156 | 157 | 158 | 159 |