├── index.tsx ├── docs └── assets │ ├── admin-login.png │ ├── batch-add-panel.png │ ├── user-interface.png │ ├── session-isolation-input.png │ ├── account-management-panel.png │ └── service-configuration-panel.png ├── vercel.json ├── src ├── vite-env.d.ts ├── utils │ ├── randomId.ts │ └── apiConstants.ts ├── hooks │ ├── useAdminAuth.ts │ ├── useConfiguredWorkerUrl.ts │ └── useApi.ts ├── components │ ├── LoadingIndicator.tsx │ ├── Toast.tsx │ ├── EmailCard.tsx │ ├── admin │ │ ├── AdminTabs.tsx │ │ ├── AdminLoginForm.tsx │ │ ├── AdminBatchAddTab.tsx │ │ ├── AdminActionForm.tsx │ │ └── AccountManagementTab.tsx │ ├── Modal.tsx │ └── ConfigPanel.tsx ├── main.tsx ├── contexts │ ├── ToastContext.tsx │ ├── WorkerUrlContext.tsx │ └── AuthContext.tsx ├── types │ └── index.ts ├── views │ ├── AdminView.tsx │ └── UserView.tsx └── App.tsx ├── metadata.json ├── vite.config.ts ├── package.json ├── tsconfig.json ├── LICENSE ├── index.html ├── .gitignore ├── README_zh.md ├── README.md └── index.css /index.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/admin-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f14XuanLv/fuclaude-pool-manager-ui/HEAD/docs/assets/admin-login.png -------------------------------------------------------------------------------- /docs/assets/batch-add-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f14XuanLv/fuclaude-pool-manager-ui/HEAD/docs/assets/batch-add-panel.png -------------------------------------------------------------------------------- /docs/assets/user-interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f14XuanLv/fuclaude-pool-manager-ui/HEAD/docs/assets/user-interface.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /docs/assets/session-isolation-input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f14XuanLv/fuclaude-pool-manager-ui/HEAD/docs/assets/session-isolation-input.png -------------------------------------------------------------------------------- /docs/assets/account-management-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f14XuanLv/fuclaude-pool-manager-ui/HEAD/docs/assets/account-management-panel.png -------------------------------------------------------------------------------- /docs/assets/service-configuration-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f14XuanLv/fuclaude-pool-manager-ui/HEAD/docs/assets/service-configuration-panel.png -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | 2 | interface ImportMetaEnv { 3 | readonly VITE_WORKER_URL?: string; 4 | // more env variables... 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } -------------------------------------------------------------------------------- /src/utils/randomId.ts: -------------------------------------------------------------------------------- 1 | export const generateRandomId = (prefix: string = 'rand_'): string => { 2 | return `${prefix}${Date.now().toString(36)}${Math.random().toString(36).substring(2, 9)}`; 3 | }; 4 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FuClaude Pool Manager UI", 3 | "description": "A web interface for managing and using a FuClaude Pool Manager worker on Cloudflare.", 4 | "requestFramePermissions": [] 5 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { fileURLToPath, URL } from 'node:url'; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | '@': fileURLToPath(new URL('.', import.meta.url)), 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/hooks/useAdminAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { AuthContext } from '../contexts/AuthContext'; 3 | 4 | export const useAdminAuth = () => { 5 | const context = useContext(AuthContext); 6 | if (context === undefined) { 7 | throw new Error('useAdminAuth must be used within an AuthProvider'); 8 | } 9 | return context; 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/apiConstants.ts: -------------------------------------------------------------------------------- 1 | export const API_PATHS = { 2 | // User endpoints 3 | GET_EMAILS: '/api/emails', 4 | LOGIN: '/api/login', 5 | 6 | // Admin endpoints 7 | ADMIN_LOGIN: '/api/admin/login', 8 | ADMIN_LIST: '/api/admin/list', 9 | ADMIN_ADD: '/api/admin/add', 10 | ADMIN_UPDATE: '/api/admin/update', 11 | ADMIN_DELETE: '/api/admin/delete', 12 | ADMIN_BATCH: '/api/admin/batch', 13 | }; -------------------------------------------------------------------------------- /src/components/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LoadingIndicatorProps { 4 | message?: string; 5 | } 6 | 7 | const LoadingIndicator: React.FC = ({ message = "加载中..." }) => { 8 | return ( 9 |
10 | {message} 11 |
12 | ); 13 | }; 14 | 15 | export default LoadingIndicator; 16 | -------------------------------------------------------------------------------- /src/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ToastProps { 4 | message: string; 5 | type: 'success' | 'error' | 'info'; 6 | } 7 | 8 | const Toast: React.FC = ({ message, type }) => { 9 | const typeClass = `toast-${type}`; 10 | return ( 11 |
16 | {message} 17 |
18 | ); 19 | }; 20 | 21 | export default Toast; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuclaude-pool-manager-ui", 3 | "private": true, 4 | "version": "0.1.3", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^19.1.0", 13 | "react-dom": "^19.1.0", 14 | "react-hook-form": "^7.52.1" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.14.0", 18 | "@types/react": "^18.3.3", 19 | "@types/react-dom": "^18.3.0", 20 | "typescript": "~5.7.2", 21 | "vite": "^6.2.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/EmailCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface EmailCardProps { 4 | email: string; 5 | onClick: () => void; 6 | } 7 | 8 | const EmailCard: React.FC = ({ email, onClick }) => { 9 | return ( 10 |
{ 16 | if (e.key === 'Enter' || e.key === ' ') onClick(); 17 | }} 18 | aria-label={`Login with ${email}`} 19 | > 20 | {email} 21 |
22 | ); 23 | }; 24 | 25 | export default EmailCard; 26 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | // Styles are imported via index.html's tag or could be imported here in a Vite setup 5 | // import './index.css'; // If you move index.css to src/ and want Vite to handle it 6 | 7 | const container = document.getElementById('root'); 8 | if (container) { 9 | const root = createRoot(container); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | } else { 16 | console.error("Root container not found. Ensure you have an element with id='root' in your HTML."); 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "experimentalDecorators": true, 5 | "useDefineForClassFields": false, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "allowJs": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true, 25 | 26 | "paths": { 27 | "@/*" : ["./*"] 28 | } 29 | }, 30 | "include": ["src", "vite.config.ts", "src/vite-env.d.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/admin/AdminTabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type AdminTabKey = 'manage' | 'batch_add'; 4 | 5 | interface AdminTabsProps { 6 | activeTab: AdminTabKey; 7 | onTabChange: (tab: AdminTabKey) => void; 8 | } 9 | 10 | const tabLabels: Record = { 11 | manage: '账户管理', 12 | batch_add: '批量添加', 13 | }; 14 | 15 | const AdminTabs: React.FC = ({ activeTab, onTabChange }) => { 16 | return ( 17 |
18 | {(Object.keys(tabLabels) as AdminTabKey[]).map((tab) => ( 19 | 30 | ))} 31 |
32 | ); 33 | }; 34 | 35 | export default AdminTabs; 36 | -------------------------------------------------------------------------------- /src/contexts/ToastContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useCallback, ReactNode } from 'react'; 2 | import { ToastMessage as ToastMessageType } from '../types'; // Renamed to avoid conflict 3 | import Toast from '../components/Toast'; 4 | 5 | interface ToastContextType { 6 | showToast: (message: string, type: 'success' | 'error' | 'info') => void; 7 | } 8 | 9 | export const ToastContext = createContext(undefined); 10 | 11 | export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 12 | const [toast, setToast] = useState(null); 13 | 14 | const showToast = useCallback((message: string, type: 'success' | 'error' | 'info') => { 15 | setToast({ id: Date.now(), message, type }); 16 | setTimeout(() => setToast(null), 3000); 17 | }, []); 18 | 19 | return ( 20 | 21 | {children} 22 | {toast && } 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 f14XuanLv 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. -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useRef } from 'react'; 2 | 3 | interface ModalProps { 4 | isOpen: boolean; 5 | onClose: () => void; 6 | title: string; 7 | children: ReactNode; 8 | } 9 | 10 | const Modal: React.FC = ({ isOpen, onClose, title, children }) => { 11 | const mouseDownTarget = useRef(null); 12 | 13 | if (!isOpen) return null; 14 | 15 | const handleMouseDown = (e: React.MouseEvent) => { 16 | mouseDownTarget.current = e.target; 17 | }; 18 | 19 | const handleMouseUp = (e: React.MouseEvent) => { 20 | if (e.target === e.currentTarget && mouseDownTarget.current === e.target) { 21 | onClose(); 22 | } 23 | mouseDownTarget.current = null; 24 | }; 25 | 26 | return ( 27 |
32 |
38 | 39 | {children} 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default Modal; 46 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FuClaude Pool Manager 8 | 9 | 10 | 16 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/contexts/WorkerUrlContext.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { createContext, useState, ReactNode, Dispatch, SetStateAction, useEffect } from 'react'; 3 | import useConfiguredWorkerUrl from '../hooks/useConfiguredWorkerUrl'; 4 | 5 | interface WorkerUrlContextType { 6 | workerUrl: string | null; // Allow null 7 | setWorkerUrl: (url: string) => void; 8 | resetWorkerUrl: () => void; 9 | exampleWorkerUrl: string; 10 | } 11 | 12 | export const WorkerUrlContext = createContext(undefined); 13 | 14 | export const WorkerUrlProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 15 | const { initialWorkerUrl, exampleWorkerUrl } = useConfiguredWorkerUrl(); 16 | const [currentWorkerUrl, setCurrentWorkerUrl] = useState(initialWorkerUrl); 17 | 18 | useEffect(() => { 19 | if (initialWorkerUrl) { 20 | setCurrentWorkerUrl(initialWorkerUrl); 21 | } 22 | }, [initialWorkerUrl]); 23 | 24 | const handleSetWorkerUrl = (newUrl: string) => { 25 | const urlToSet = newUrl.trim() === '' ? exampleWorkerUrl : newUrl; 26 | localStorage.setItem('workerUrl', urlToSet); 27 | setCurrentWorkerUrl(urlToSet); 28 | }; 29 | 30 | const handleResetWorkerUrl = () => { 31 | localStorage.removeItem('workerUrl'); 32 | window.location.reload(); 33 | }; 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface EmailSkMapEntry { 3 | index: number; 4 | email: string; 5 | sk_preview: string; 6 | } 7 | 8 | export interface AdminBatchAction { 9 | action: 'add' | 'delete'; 10 | email: string; 11 | sk?: string; 12 | } 13 | 14 | export interface AdminBatchResultItem { 15 | email: string; 16 | status: string; 17 | reason?: string; 18 | } 19 | 20 | export type ToastMessage = { 21 | id: number; 22 | message: string; 23 | type: 'success' | 'error' | 'info'; 24 | }; 25 | 26 | export type LoginPayload = { 27 | mode: 'random' | 'specific'; 28 | email?: string; 29 | unique_name?: string; 30 | expires_in?: number; 31 | }; 32 | 33 | export type LoginResponse = { 34 | login_url: string; 35 | warning?: string; 36 | }; 37 | 38 | export interface AdminRequestBase { 39 | admin_password: string; 40 | } 41 | 42 | export interface AdminLoginPayload extends LoginPayload, AdminRequestBase {} 43 | 44 | export type AdminAddPayload = { 45 | email: string; 46 | sk: string; 47 | } & AdminRequestBase; 48 | 49 | export type AdminUpdatePayload = { 50 | email: string; 51 | new_email?: string; 52 | new_sk?: string; 53 | } & AdminRequestBase; 54 | 55 | export type AdminDeletePayload = { 56 | email: string; 57 | } & AdminRequestBase; 58 | 59 | export type AdminBatchPayload = { 60 | actions: AdminBatchAction[]; 61 | } & AdminRequestBase; 62 | 63 | export type AdminApiResponse = { 64 | message: string; 65 | }; 66 | 67 | export type AdminBatchApiResponse = AdminApiResponse & { 68 | results: AdminBatchResultItem[]; 69 | }; -------------------------------------------------------------------------------- /src/components/admin/AdminLoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, FormEvent, useContext } from 'react'; 2 | import { useAdminAuth } from '../../hooks/useAdminAuth'; 3 | import LoadingIndicator from '../LoadingIndicator'; 4 | 5 | const AdminLoginForm: React.FC = () => { 6 | const { login, tempAdminPassword, setTempAdminPassword, authLoading, authError, clearAuthError } = useAdminAuth(); 7 | 8 | const handleSubmit = async (e: FormEvent) => { 9 | e.preventDefault(); 10 | clearAuthError(); 11 | await login(tempAdminPassword); 12 | }; 13 | 14 | return ( 15 |
16 |

管理员登录

17 |
18 |
19 | 20 | setTempAdminPassword(e.target.value)} 25 | placeholder="输入管理员密码" 26 | required 27 | aria-required="true" 28 | aria-describedby={authError ? "admin-auth-error" : undefined} 29 | /> 30 |
31 | {authLoading && } 32 | {authError && } 33 | 36 | 37 |
38 | ); 39 | }; 40 | 41 | export default AdminLoginForm; 42 | -------------------------------------------------------------------------------- /src/views/AdminView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import AdminLoginForm from '../components/admin/AdminLoginForm'; 3 | import AdminTabs, { AdminTabKey } from '../components/admin/AdminTabs'; 4 | import { useAdminAuth } from '../hooks/useAdminAuth'; 5 | import AdminBatchAddTab from '../components/admin/AdminBatchAddTab'; 6 | import AccountManagementTab from '../components/admin/AccountManagementTab'; 7 | 8 | const AdminView: React.FC = () => { 9 | const { isAdminAuthenticated, logout } = useAdminAuth(); 10 | const [activeTab, setActiveTab] = useState('manage'); 11 | 12 | const handleActionSuccess = () => { 13 | // This function might need to be passed down to the management tab 14 | // to trigger a refresh within it. For now, it's a placeholder. 15 | }; 16 | 17 | if (!isAdminAuthenticated) { 18 | return ( 19 |
20 |

管理后台

21 | 22 |
23 | ); 24 | } 25 | 26 | return ( 27 |
28 |

管理后台

29 |
30 | 管理员已登录。 31 | 34 |
35 | 36 | {activeTab === 'manage' && } 37 | {activeTab === 'batch_add' && setActiveTab('manage')} />} 38 |
39 | ); 40 | }; 41 | 42 | export default AdminView; -------------------------------------------------------------------------------- /src/hooks/useConfiguredWorkerUrl.ts: -------------------------------------------------------------------------------- 1 | 2 | import { useState, useEffect } from 'react'; 3 | 4 | const EXAMPLE_WORKER_URL = 'https://..workers.dev'; 5 | 6 | const useConfiguredWorkerUrl = () => { 7 | // Start with null, indicating the URL is not yet determined. 8 | const [determinedInitialUrl, setDeterminedInitialUrl] = useState(null); 9 | 10 | useEffect(() => { 11 | // 1. Check localStorage (user override) 12 | const storedUrl = localStorage.getItem('workerUrl'); 13 | if (storedUrl && storedUrl !== EXAMPLE_WORKER_URL) { 14 | setDeterminedInitialUrl(storedUrl); 15 | return; 16 | } 17 | 18 | // 2. Check Vite environment variable (VITE_WORKER_URL) 19 | const viteEnvUrl = import.meta.env.VITE_WORKER_URL; 20 | if (viteEnvUrl && (viteEnvUrl.startsWith('http://') || viteEnvUrl.startsWith('https://'))) { 21 | setDeterminedInitialUrl(viteEnvUrl); 22 | localStorage.setItem('workerUrl', viteEnvUrl); // Persist if found 23 | return; 24 | } 25 | 26 | // 3. Check window preconfigured URL (from index.html script) 27 | const preconfiguredUrlFromWindow = (window as any).__PRECONFIGURED_WORKER_URL__; 28 | if ( 29 | preconfiguredUrlFromWindow && 30 | preconfiguredUrlFromWindow !== '%%PLACEHOLDER_WORKER_URL%%' && 31 | (preconfiguredUrlFromWindow.startsWith('http://') || preconfiguredUrlFromWindow.startsWith('https://')) 32 | ) { 33 | setDeterminedInitialUrl(preconfiguredUrlFromWindow); 34 | localStorage.setItem('workerUrl', preconfiguredUrlFromWindow); // Persist if found 35 | return; 36 | } 37 | 38 | // 4. If nothing is found, set it to the example URL so the user can configure it. 39 | setDeterminedInitialUrl(EXAMPLE_WORKER_URL); 40 | // Also save the example to local storage so it doesn't run this logic again. 41 | localStorage.setItem('workerUrl', EXAMPLE_WORKER_URL); 42 | 43 | }, []); // Runs once on mount 44 | 45 | return { initialWorkerUrl: determinedInitialUrl, exampleWorkerUrl: EXAMPLE_WORKER_URL }; 46 | }; 47 | 48 | export default useConfiguredWorkerUrl; -------------------------------------------------------------------------------- /src/hooks/useApi.ts: -------------------------------------------------------------------------------- 1 | 2 | import { useState, useCallback, useContext } from 'react'; 3 | import { WorkerUrlContext } from '../contexts/WorkerUrlContext'; 4 | import { ToastContext } from '../contexts/ToastContext'; 5 | 6 | interface ApiState { 7 | data: R | null; 8 | isLoading: boolean; 9 | error: string | null; 10 | } 11 | 12 | // Ensure T includes potential admin_password if it's an admin payload 13 | function useApi() { 14 | const workerUrlCtx = useContext(WorkerUrlContext); 15 | const toastCtx = useContext(ToastContext); 16 | 17 | if (!workerUrlCtx || !toastCtx) { 18 | throw new Error('useApi must be used within WorkerUrlProvider and ToastProvider'); 19 | } 20 | const { workerUrl } = workerUrlCtx; 21 | const { showToast } = toastCtx; 22 | 23 | const [apiState, setApiState] = useState>({ 24 | data: null, 25 | isLoading: false, 26 | error: null, 27 | }); 28 | 29 | const callApi = useCallback( 30 | async (endpoint: string, method: string = 'GET', body?: T): Promise => { 31 | if (!workerUrl) { 32 | const err = new Error("Worker URL is not configured yet."); 33 | setApiState({ data: null, isLoading: false, error: err.message }); 34 | // Do not show toast here, as this is an expected initial state 35 | return Promise.reject(err); 36 | } 37 | 38 | setApiState({ data: null, isLoading: true, error: null }); 39 | const url = `${workerUrl.replace(/\/$/, '')}${endpoint}`; 40 | const headers: HeadersInit = { 'Content-Type': 'application/json' }; 41 | 42 | try { 43 | const response = await fetch(url, { 44 | method, 45 | headers, 46 | body: body && method !== 'GET' ? JSON.stringify(body) : undefined, 47 | }); 48 | const responseData = await response.json(); 49 | if (!response.ok) { 50 | throw new Error(responseData.error || `请求失败,状态码: ${response.status}`); 51 | } 52 | setApiState({ data: responseData, isLoading: false, error: null }); 53 | return responseData; 54 | } catch (err: any) { 55 | setApiState({ data: null, isLoading: false, error: err.message }); 56 | showToast(err.message, "error"); 57 | throw err; 58 | } 59 | }, 60 | [workerUrl, showToast] 61 | ); 62 | 63 | return { ...apiState, callApi, clearError: () => setApiState((prev: ApiState) => ({...prev, error: null})) }; 64 | } 65 | 66 | export default useApi; -------------------------------------------------------------------------------- /src/components/ConfigPanel.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState, useContext, useEffect } from 'react'; 3 | import { WorkerUrlContext } from '../contexts/WorkerUrlContext'; 4 | import { ToastContext } from '../contexts/ToastContext'; 5 | 6 | interface ConfigPanelProps { 7 | onClose: () => void; 8 | } 9 | 10 | const ConfigPanel: React.FC = ({ onClose }) => { 11 | const workerUrlCtx = useContext(WorkerUrlContext); 12 | const toastCtx = useContext(ToastContext); 13 | 14 | if (!workerUrlCtx || !toastCtx) { 15 | throw new Error('ConfigPanel must be used within WorkerUrlProvider and ToastProvider'); 16 | } 17 | 18 | const { workerUrl, setWorkerUrl, resetWorkerUrl, exampleWorkerUrl } = workerUrlCtx; 19 | const { showToast } = toastCtx; 20 | const [tempUrl, setTempUrl] = useState(workerUrl || ''); 21 | 22 | useEffect(() => { 23 | setTempUrl(workerUrl || ''); 24 | }, [workerUrl]); 25 | 26 | const handleSave = () => { 27 | if (!tempUrl || tempUrl.trim() === '') { 28 | showToast('Worker URL 不能为空', 'error'); 29 | return; 30 | } 31 | if (!tempUrl.startsWith('http://') && !tempUrl.startsWith('https://')) { 32 | showToast('Worker URL 必须以 http:// 或 https:// 开头', 'error'); 33 | return; 34 | } 35 | setWorkerUrl(tempUrl); 36 | showToast('Worker URL 已保存!', 'success'); 37 | onClose(); 38 | }; 39 | 40 | return ( 41 |
42 |

服务配置

43 |
44 | 45 | setTempUrl(e.target.value)} 50 | placeholder={exampleWorkerUrl} 51 | aria-describedby="workerUrlHint1 workerUrlHint2" 52 | /> 53 |
54 |

55 | 您可以在 Cloudflare 中为此 Worker 配置自定义域名,并在此处填入。例如: `https://claude-pool.yourdomain.com` 56 |

57 |

58 | 对于Vite部署 (如Vercel), 推荐设置环境变量 `VITE_WORKER_URL` 来预设此值。 59 |

60 |
61 | 64 | 67 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default ConfigPanel; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Snowpack dependency directory (https://snowpack.dev/) 43 | web_modules/ 44 | 45 | # TypeScript cache 46 | *.tsbuildinfo 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Microbundle cache 55 | .rpt2_cache/ 56 | .rts2_cache_cjs/ 57 | .rts2_cache_es/ 58 | .rts2_cache_umd/ 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variable files 70 | .env 71 | .env.development.local 72 | .env.test.local 73 | .env.production.local 74 | .env.local 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Docusaurus cache and generated files 98 | .docusaurus 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .yarn/install-state.gz 120 | .pnp.* 121 | 122 | # Vite 123 | dist 124 | .vite 125 | 126 | # Lockfiles 127 | package-lock.json 128 | yarn.lock 129 | pnpm-lock.yaml 130 | -------------------------------------------------------------------------------- /src/components/admin/AdminBatchAddTab.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, FormEvent, useContext } from 'react'; 2 | import useApi from '../../hooks/useApi'; 3 | import { AdminBatchPayload, AdminBatchApiResponse, AdminBatchAction } from '../../types'; 4 | import LoadingIndicator from '../LoadingIndicator'; 5 | import { ToastContext } from '../../contexts/ToastContext'; 6 | import { useAdminAuth } from '../../hooks/useAdminAuth'; 7 | import { API_PATHS } from '../../utils/apiConstants'; 8 | 9 | interface AdminBatchAddTabProps { 10 | onActionSuccess?: () => void; 11 | } 12 | 13 | const AdminBatchAddTab: React.FC = ({ onActionSuccess }) => { 14 | const [plainText, setPlainText] = useState(''); 15 | const { callApi, isLoading, error } = useApi(); 16 | const toastCtx = useContext(ToastContext); 17 | const { adminPassword } = useAdminAuth(); 18 | 19 | const handleSubmit = async (e: FormEvent) => { 20 | e.preventDefault(); 21 | if (!toastCtx || !adminPassword) { 22 | toastCtx?.showToast("管理员未登录或会话已过期。", "error"); 23 | return; 24 | } 25 | 26 | const lines = plainText.split('\n').filter(line => line.trim() !== ''); 27 | const actions: AdminBatchAction[] = []; 28 | let parseError = false; 29 | 30 | for (const line of lines) { 31 | const parts = line.split(',').map(p => p.trim()); 32 | if (parts.length === 2 && parts[0] && parts[1]) { 33 | actions.push({ action: 'add', email: parts[0], sk: parts[1] }); 34 | } else { 35 | toastCtx.showToast(`格式错误: "${line}"。应为 "email,sk" 格式。`, "error"); 36 | parseError = true; 37 | break; 38 | } 39 | } 40 | 41 | if (parseError || actions.length === 0) { 42 | return; 43 | } 44 | 45 | try { 46 | const payload: AdminBatchPayload = { actions, admin_password: adminPassword }; 47 | const data = await callApi(API_PATHS.ADMIN_BATCH, 'POST', payload); 48 | toastCtx.showToast(data.message, "success"); 49 | setPlainText(''); 50 | onActionSuccess?.(); 51 | } catch (e) { 52 | // error handled by useApi 53 | } 54 | }; 55 | 56 | return ( 57 |
58 |

批量添加账户

59 |

每行输入一个账户,格式为 email,sk。例如:

60 |
user1@example.com,sk-abc...
user2@example.com,sk-def...
61 |
62 |
63 | 64 |