Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- DEPLOYMENT.md +312 -0
- Dockerfile +102 -0
- README.md +157 -4
- entrypoint.sh +41 -0
- requirements.txt +8 -0
- streamlit_app/app.py +217 -0
- streamlit_app/pages/1_💬_Chat.py +242 -0
- streamlit_app/pages/2_🔑_Auth.py +234 -0
- streamlit_app/pages/3_📊_Stats.py +263 -0
- streamlit_app/pages/4_⚙️_Settings.py +350 -0
- supervisord.conf +63 -0
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLI Proxy API Plus - Hugging Face Spaces 部署指南
|
| 2 |
+
|
| 3 |
+
本文檔提供完整的 Hugging Face Spaces 部署步驟說明。
|
| 4 |
+
|
| 5 |
+
## 📋 目錄
|
| 6 |
+
|
| 7 |
+
1. [前置需求](#前置需求)
|
| 8 |
+
2. [快速部署](#快速部署)
|
| 9 |
+
3. [配置 Secrets](#配置-secrets)
|
| 10 |
+
4. [使用說明](#使用說明)
|
| 11 |
+
5. [常見問題](#常見問題)
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 前置需求
|
| 16 |
+
|
| 17 |
+
### 必需項目
|
| 18 |
+
|
| 19 |
+
1. **Hugging Face 帳號**
|
| 20 |
+
- 註冊地址: https://huggingface.co/join
|
| 21 |
+
- 建議啟用兩步驗證以保護帳號安全
|
| 22 |
+
|
| 23 |
+
2. **AI 服務提供商帳號**(至少需要一個)
|
| 24 |
+
- [Anthropic Claude](https://console.anthropic.com/)
|
| 25 |
+
- [Google AI Studio](https://aistudio.google.com/)
|
| 26 |
+
- [GitHub Copilot](https://github.com/features/copilot)
|
| 27 |
+
- [AWS Kiro](https://aws.amazon.com/)
|
| 28 |
+
|
| 29 |
+
### 可選項目
|
| 30 |
+
|
| 31 |
+
- 自定義域名(可在 HF Spaces 設置中配置)
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 快速部署
|
| 36 |
+
|
| 37 |
+
### 方法一:從模板創建(推薦)
|
| 38 |
+
|
| 39 |
+
1. **Fork 或 Clone 本項目到您的 HF 帳號**
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
# 使用 Git
|
| 43 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/cli-proxy-api-plus
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
2. **創建新的 Space**
|
| 47 |
+
- 前往 https://huggingface.co/new-space
|
| 48 |
+
- 選擇 **Docker** 作為 SDK
|
| 49 |
+
- 設置 Space 名稱(例如:`cli-proxy-api-plus`)
|
| 50 |
+
- 選擇可見性(Public 或 Private)
|
| 51 |
+
|
| 52 |
+
3. **上傳文件**
|
| 53 |
+
|
| 54 |
+
將 `hf-spaces/` 目錄下的所有文件上傳到 Space 根目錄:
|
| 55 |
+
- `Dockerfile`
|
| 56 |
+
- `supervisord.conf`
|
| 57 |
+
- `requirements.txt`
|
| 58 |
+
- `entrypoint.sh`
|
| 59 |
+
- `README.md`
|
| 60 |
+
- `streamlit_app/` 目錄
|
| 61 |
+
|
| 62 |
+
### 方法二:使用 Git 推送
|
| 63 |
+
|
| 64 |
+
1. **創建 Space 後,克隆到本地**
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 68 |
+
cd YOUR_SPACE_NAME
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
2. **複製文件**
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# 從本項目複製 hf-spaces 內容
|
| 75 |
+
cp -r /path/to/CLIProxyAPIPlus-main/hf-spaces/* .
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
3. **提交並推送**
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
git add .
|
| 82 |
+
git commit -m "Initial deployment"
|
| 83 |
+
git push
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
4. **等待構建完成**
|
| 87 |
+
|
| 88 |
+
HF Spaces 會自動開始構建 Docker 映像,通常需要 5-10 分鐘。
|
| 89 |
+
|
| 90 |
+
---
|
| 91 |
+
|
| 92 |
+
## 配置 Secrets
|
| 93 |
+
|
| 94 |
+
### 為什麼需要 Secrets?
|
| 95 |
+
|
| 96 |
+
Secrets 用於安全地存儲敏感信息,如 API 密鑰、OAuth 憑證等。這些信息不會出現在代碼中,確保安全性。
|
| 97 |
+
|
| 98 |
+
### 如何設置 Secrets
|
| 99 |
+
|
| 100 |
+
1. **進入 Space 設置頁面**
|
| 101 |
+
- 打開您的 Space 頁面
|
| 102 |
+
- 點擊右上角 **Settings** 標籤
|
| 103 |
+
|
| 104 |
+
2. **找到 Variables and Secrets 區塊**
|
| 105 |
+
- 滾動到 **Variables and secrets** 部分
|
| 106 |
+
- 點擊 **New secret**
|
| 107 |
+
|
| 108 |
+
3. **添加以下 Secrets**
|
| 109 |
+
|
| 110 |
+
#### 基礎配置
|
| 111 |
+
|
| 112 |
+
| Secret 名稱 | 說明 | 範例值 |
|
| 113 |
+
|------------|------|--------|
|
| 114 |
+
| `SERVER_PORT` | Go API 服務器端口 | `8317` |
|
| 115 |
+
| `LOG_LEVEL` | 日誌級別 | `info` |
|
| 116 |
+
|
| 117 |
+
#### Claude 配置
|
| 118 |
+
|
| 119 |
+
| Secret 名稱 | 說明 | 取得方式 |
|
| 120 |
+
|------------|------|----------|
|
| 121 |
+
| `CLAUDE_API_KEY` | Anthropic API Key | [Anthropic Console](https://console.anthropic.com/) |
|
| 122 |
+
| `CLAUDE_CLIENT_ID` | OAuth Client ID | OAuth 應用註冊 |
|
| 123 |
+
| `CLAUDE_CLIENT_SECRET` | OAuth Client Secret | OAuth 應用註冊 |
|
| 124 |
+
|
| 125 |
+
#### Gemini 配置
|
| 126 |
+
|
| 127 |
+
| Secret 名稱 | 說明 | 取得方式 |
|
| 128 |
+
|------------|------|----------|
|
| 129 |
+
| `GEMINI_API_KEY` | Google AI API Key | [Google AI Studio](https://aistudio.google.com/app/apikey) |
|
| 130 |
+
|
| 131 |
+
#### GitHub Copilot 配置
|
| 132 |
+
|
| 133 |
+
| Secret 名稱 | 說明 |
|
| 134 |
+
|------------|------|
|
| 135 |
+
| `GITHUB_CLIENT_ID` | GitHub OAuth App Client ID |
|
| 136 |
+
| `GITHUB_CLIENT_SECRET` | GitHub OAuth App Client Secret |
|
| 137 |
+
|
| 138 |
+
#### Kiro 配置
|
| 139 |
+
|
| 140 |
+
| Secret 名稱 | 說明 |
|
| 141 |
+
|------------|------|
|
| 142 |
+
| `AWS_ACCESS_KEY_ID` | AWS Access Key |
|
| 143 |
+
| `AWS_SECRET_ACCESS_KEY` | AWS Secret Key |
|
| 144 |
+
| `AWS_REGION` | AWS 區域(如 `us-east-1`) |
|
| 145 |
+
|
| 146 |
+
### OAuth 回調 URL 設置
|
| 147 |
+
|
| 148 |
+
在配置 OAuth 應用時,需要設置正確的回調 URL:
|
| 149 |
+
|
| 150 |
+
```
|
| 151 |
+
https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space/oauth/callback/{provider}
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
例如:
|
| 155 |
+
- Claude: `https://john-cli-proxy.hf.space/oauth/callback/claude`
|
| 156 |
+
- Gemini: `https://john-cli-proxy.hf.space/oauth/callback/gemini`
|
| 157 |
+
- GitHub: `https://john-cli-proxy.hf.space/oauth/callback/github`
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 使用說明
|
| 162 |
+
|
| 163 |
+
### 訪問應用
|
| 164 |
+
|
| 165 |
+
部署完成後,您可以通過以下 URL 訪問:
|
| 166 |
+
|
| 167 |
+
```
|
| 168 |
+
https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
### 主要功能
|
| 172 |
+
|
| 173 |
+
#### 1. 首頁(Dashboard)
|
| 174 |
+
|
| 175 |
+
- 查看 API 服務狀態
|
| 176 |
+
- 查看可用的 AI 提供商
|
| 177 |
+
- 快速導航到各功能頁面
|
| 178 |
+
|
| 179 |
+
#### 2. 💬 Chat 頁面
|
| 180 |
+
|
| 181 |
+
- 測試 API 端點
|
| 182 |
+
- 選擇不同的 AI 模型
|
| 183 |
+
- 發送測試請求
|
| 184 |
+
- 查看響應結果
|
| 185 |
+
|
| 186 |
+
#### 3. 🔑 Auth 頁面
|
| 187 |
+
|
| 188 |
+
- 管理 OAuth 登錄
|
| 189 |
+
- 查看 API 密鑰狀態
|
| 190 |
+
- 刷新認證令牌
|
| 191 |
+
|
| 192 |
+
#### 4. 📊 Stats 頁面
|
| 193 |
+
|
| 194 |
+
- 查看使用統計
|
| 195 |
+
- 請求次數圖表
|
| 196 |
+
- Token 使用量
|
| 197 |
+
- 模型分佈
|
| 198 |
+
|
| 199 |
+
#### 5. ⚙️ Settings 頁面
|
| 200 |
+
|
| 201 |
+
- 服務器配置
|
| 202 |
+
- 提供商設置
|
| 203 |
+
- 日誌查看
|
| 204 |
+
|
| 205 |
+
### API 端點
|
| 206 |
+
|
| 207 |
+
Go API 服務器運行在內部端口 8317,Streamlit 會代理請求:
|
| 208 |
+
|
| 209 |
+
```
|
| 210 |
+
# OpenAI 兼容端點
|
| 211 |
+
POST /v1/chat/completions
|
| 212 |
+
POST /v1/models
|
| 213 |
+
|
| 214 |
+
# Claude 端點
|
| 215 |
+
POST /v1/messages
|
| 216 |
+
|
| 217 |
+
# 管理��點
|
| 218 |
+
GET /api/status
|
| 219 |
+
GET /api/models
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
## 常見問題
|
| 225 |
+
|
| 226 |
+
### Q: 構建失敗怎麼辦?
|
| 227 |
+
|
| 228 |
+
**A:** 檢查以下項目:
|
| 229 |
+
1. 確保 `Dockerfile` 在根目錄
|
| 230 |
+
2. 檢查 `requirements.txt` 格式是否正確
|
| 231 |
+
3. 查看 Build Logs 中的錯誤信息
|
| 232 |
+
|
| 233 |
+
### Q: 應用無法啟動?
|
| 234 |
+
|
| 235 |
+
**A:** 可能原因:
|
| 236 |
+
1. Secrets 未正確配置
|
| 237 |
+
2. 端口衝突(HF Spaces 要求使用 7860)
|
| 238 |
+
3. 依賴安裝失敗
|
| 239 |
+
|
| 240 |
+
### Q: API 請求失敗?
|
| 241 |
+
|
| 242 |
+
**A:** 檢查:
|
| 243 |
+
1. API 密鑰是否有效
|
| 244 |
+
2. OAuth 令牌是否過期
|
| 245 |
+
3. 網絡連接是否正常
|
| 246 |
+
|
| 247 |
+
### Q: 如何更新應用?
|
| 248 |
+
|
| 249 |
+
**A:**
|
| 250 |
+
```bash
|
| 251 |
+
git pull origin main
|
| 252 |
+
git push
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
HF Spaces 會自動重新構建。
|
| 256 |
+
|
| 257 |
+
### Q: 如何查看日誌?
|
| 258 |
+
|
| 259 |
+
**A:**
|
| 260 |
+
1. 進入 Space 頁面
|
| 261 |
+
2. 點擊 **Logs** 標籤
|
| 262 |
+
3. 查看實時日誌輸出
|
| 263 |
+
|
| 264 |
+
### Q: 資源限制?
|
| 265 |
+
|
| 266 |
+
**A:** HF Spaces 免費版有以下限制:
|
| 267 |
+
- CPU: 2 vCPU
|
| 268 |
+
- 內存: 16GB
|
| 269 |
+
- 存儲: 臨時存儲(重啟後清除)
|
| 270 |
+
|
| 271 |
+
如需更多資源,可考慮升級到 GPU Space 或使用 HF Enterprise。
|
| 272 |
+
|
| 273 |
+
---
|
| 274 |
+
|
| 275 |
+
## 進階配置
|
| 276 |
+
|
| 277 |
+
### 自定義域名
|
| 278 |
+
|
| 279 |
+
1. 進入 Space Settings
|
| 280 |
+
2. 找到 **Custom Domain** 區塊
|
| 281 |
+
3. 輸入您的域名並按照指示配置 DNS
|
| 282 |
+
|
| 283 |
+
### 持久化存儲
|
| 284 |
+
|
| 285 |
+
HF Spaces 預設使用臨時存儲。如需持久化:
|
| 286 |
+
|
| 287 |
+
1. 使用 HF Datasets 存儲配置
|
| 288 |
+
2. 或使用外部數據庫(如 PostgreSQL)
|
| 289 |
+
|
| 290 |
+
### 擴展功能
|
| 291 |
+
|
| 292 |
+
您可以通過修改 `streamlit_app/` 中的文件來擴展功能:
|
| 293 |
+
|
| 294 |
+
- 添加新的頁面:在 `pages/` 目錄創建新文件
|
| 295 |
+
- 修改主題:編輯 `.streamlit/config.toml`
|
| 296 |
+
- 添加新功能:修改 `app.py`
|
| 297 |
+
|
| 298 |
+
---
|
| 299 |
+
|
| 300 |
+
## 技術支持
|
| 301 |
+
|
| 302 |
+
如有問題,請:
|
| 303 |
+
|
| 304 |
+
1. 查閱 [GitHub Issues](https://github.com/router-for-me/CLIProxyAPI/issues)
|
| 305 |
+
2. 在 HF Space 上開啟 Discussion
|
| 306 |
+
3. 聯繫維護者
|
| 307 |
+
|
| 308 |
+
---
|
| 309 |
+
|
| 310 |
+
## 授權
|
| 311 |
+
|
| 312 |
+
本項目採用 MIT 授權條款。詳見 [LICENSE](../LICENSE) 文件。
|
Dockerfile
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================
|
| 2 |
+
# CLIProxyAPI Plus - Hugging Face Spaces Dockerfile
|
| 3 |
+
# ============================================
|
| 4 |
+
# This Dockerfile creates a container with both:
|
| 5 |
+
# - Go API Server (CLIProxyAPI Plus)
|
| 6 |
+
# - Streamlit Frontend UI
|
| 7 |
+
# ============================================
|
| 8 |
+
|
| 9 |
+
# ============================================
|
| 10 |
+
# Stage 1: 構建 Go 應用
|
| 11 |
+
# ============================================
|
| 12 |
+
FROM golang:1.26-alpine AS go-builder
|
| 13 |
+
|
| 14 |
+
WORKDIR /build
|
| 15 |
+
|
| 16 |
+
# 安裝構建依賴
|
| 17 |
+
RUN apk add --no-cache git ca-certificates tzdata
|
| 18 |
+
|
| 19 |
+
# 複製 Go 模組文件
|
| 20 |
+
COPY go.mod go.sum ./
|
| 21 |
+
RUN go mod download
|
| 22 |
+
|
| 23 |
+
# 複製源碼並構建
|
| 24 |
+
COPY . .
|
| 25 |
+
|
| 26 |
+
ARG VERSION=hf-spaces
|
| 27 |
+
ARG COMMIT=none
|
| 28 |
+
ARG BUILD_DATE=unknown
|
| 29 |
+
|
| 30 |
+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
| 31 |
+
go build -ldflags="-s -w \
|
| 32 |
+
-X 'main.Version=${VERSION}-plus' \
|
| 33 |
+
-X 'main.Commit=${COMMIT}' \
|
| 34 |
+
-X 'main.BuildDate=${BUILD_DATE}'" \
|
| 35 |
+
-o ./CLIProxyAPIPlus ./cmd/server/
|
| 36 |
+
|
| 37 |
+
# ============================================
|
| 38 |
+
# Stage 2: 最終運行環境
|
| 39 |
+
# ============================================
|
| 40 |
+
FROM python:3.11-slim-bookworm
|
| 41 |
+
|
| 42 |
+
LABEL maintainer="CLIProxyAPI Plus"
|
| 43 |
+
LABEL description="CLI Proxy API with Streamlit UI for Hugging Face Spaces"
|
| 44 |
+
|
| 45 |
+
# 安裝系統依賴
|
| 46 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 47 |
+
supervisor \
|
| 48 |
+
curl \
|
| 49 |
+
wget \
|
| 50 |
+
tzdata \
|
| 51 |
+
ca-certificates \
|
| 52 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 53 |
+
&& mkdir -p /var/log/supervisor
|
| 54 |
+
|
| 55 |
+
# 創建目錄結構
|
| 56 |
+
RUN mkdir -p /app/go-server \
|
| 57 |
+
&& mkdir -p /app/streamlit \
|
| 58 |
+
&& mkdir -p /app/config \
|
| 59 |
+
&& mkdir -p /app/auths \
|
| 60 |
+
&& mkdir -p /root/.cli-proxy-api \
|
| 61 |
+
&& mkdir -p /app/logs
|
| 62 |
+
|
| 63 |
+
# 複製 Go 二進制文件和配置
|
| 64 |
+
COPY --from=go-builder /build/CLIProxyAPIPlus /app/go-server/CLIProxyAPIPlus
|
| 65 |
+
COPY config.example.yaml /app/config/config.yaml
|
| 66 |
+
|
| 67 |
+
# 複製 Streamlit 應用
|
| 68 |
+
COPY hf-spaces/streamlit_app/ /app/streamlit/
|
| 69 |
+
COPY hf-spaces/requirements.txt /app/streamlit/
|
| 70 |
+
|
| 71 |
+
# 安裝 Python 依賴
|
| 72 |
+
RUN pip install --no-cache-dir -r /app/streamlit/requirements.txt
|
| 73 |
+
|
| 74 |
+
# 複製 Supervisor 配置
|
| 75 |
+
COPY hf-spaces/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
| 76 |
+
|
| 77 |
+
# 複製啟動腳本
|
| 78 |
+
COPY hf-spaces/entrypoint.sh /app/entrypoint.sh
|
| 79 |
+
RUN chmod +x /app/entrypoint.sh
|
| 80 |
+
|
| 81 |
+
# 設置環境變數
|
| 82 |
+
ENV TZ=Asia/Shanghai \
|
| 83 |
+
PYTHONUNBUFFERED=1 \
|
| 84 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 85 |
+
STREAMLIT_SERVER_PORT=7860 \
|
| 86 |
+
STREAMLIT_SERVER_ADDRESS=0.0.0.0 \
|
| 87 |
+
STREAMLIT_SERVER_HEADLESS=true \
|
| 88 |
+
STREAMLIT_SERVER_ENABLE_CORS=false \
|
| 89 |
+
STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION=false \
|
| 90 |
+
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \
|
| 91 |
+
API_BASE_URL=http://localhost:8317 \
|
| 92 |
+
HOME=/root
|
| 93 |
+
|
| 94 |
+
# HF Spaces 暴露端口 (7860 是 HF Spaces 默認端口)
|
| 95 |
+
EXPOSE 7860
|
| 96 |
+
|
| 97 |
+
# 健康檢查
|
| 98 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 99 |
+
CMD curl -f http://localhost:7860/_stcore/health || exit 1
|
| 100 |
+
|
| 101 |
+
# 啟動入口點
|
| 102 |
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,163 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CLI Proxy API Plus
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
short_description: 統一的 AI API 代理服務
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# 🚀 CLI Proxy API Plus
|
| 13 |
+
|
| 14 |
+
[](https://opensource.org/licenses/MIT)
|
| 15 |
+
[](https://golang.org/)
|
| 16 |
+
[](https://streamlit.io/)
|
| 17 |
+
|
| 18 |
+
統一的 AI API 代理服務,支援多種 AI 提供者,提供標準化的 OpenAI 兼容 API 接口。
|
| 19 |
+
|
| 20 |
+
## ✨ 功能特點
|
| 21 |
+
|
| 22 |
+
- 🔌 **統一 API 接口** - OpenAI 兼容格式,無縫整合現有工具
|
| 23 |
+
- 🔐 **OAuth 認證管理** - 支援多種 OAuth 認證流程
|
| 24 |
+
- 📊 **使用量統計** - 實時監控 API 使用情況
|
| 25 |
+
- 🔄 **自動 Token 刷新** - 智能管理認證憑證
|
| 26 |
+
- 🌐 **多提供者支援** - Claude、OpenAI、Gemini、Copilot、Kiro 等
|
| 27 |
+
- 🎨 **Streamlit Web UI** - 友好的圖形化管理介面
|
| 28 |
+
|
| 29 |
+
## 🤖 支援的 AI 提供者
|
| 30 |
+
|
| 31 |
+
| 提供者 | 認證方式 | 模型 |
|
| 32 |
+
|--------|----------|------|
|
| 33 |
+
| 🟠 **Claude** | OAuth | claude-3-5-sonnet, claude-3-opus |
|
| 34 |
+
| 🟢 **OpenAI** | API Key / OAuth | gpt-4o, gpt-4-turbo |
|
| 35 |
+
| 🔵 **Gemini** | API Key | gemini-1.5-pro, gemini-1.5-flash |
|
| 36 |
+
| ⚫ **GitHub Copilot** | OAuth | gpt-4, gpt-3.5-turbo |
|
| 37 |
+
| 🟡 **Kiro** | OAuth / AWS | claude-3-5-sonnet (via AWS) |
|
| 38 |
+
| 🟣 **Codex** | OAuth | gpt-4, gpt-3.5-turbo |
|
| 39 |
+
|
| 40 |
+
## 🚀 快速開始
|
| 41 |
+
|
| 42 |
+
### 使用 Web UI
|
| 43 |
+
|
| 44 |
+
1. 訪問此 Space 的 URL
|
| 45 |
+
2. 在側邊欄查看 API 狀態
|
| 46 |
+
3. 使用以下功能:
|
| 47 |
+
- **💬 Chat** - 測試 API 請求
|
| 48 |
+
- **🔑 Auth** - 管理認證憑證
|
| 49 |
+
- **📊 Stats** - 查看使用統計
|
| 50 |
+
- **⚙️ Settings** - 配置系統
|
| 51 |
+
|
| 52 |
+
### API 端點
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# Chat Completions (OpenAI 兼容)
|
| 56 |
+
curl -X POST https://{your-space}.hf.space/v1/chat/completions \
|
| 57 |
+
-H "Content-Type: application/json" \
|
| 58 |
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
| 59 |
+
-d '{
|
| 60 |
+
"model": "claude-3-5-sonnet-20241022",
|
| 61 |
+
"messages": [{"role": "user", "content": "Hello!"}]
|
| 62 |
+
}'
|
| 63 |
+
|
| 64 |
+
# 列出模型
|
| 65 |
+
curl https://{your-space}.hf.space/v1/models
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## 🔐 配置 Secrets
|
| 69 |
+
|
| 70 |
+
在 Hugging Face Spaces 的 **Settings > Secrets** 中添加以下配置:
|
| 71 |
+
|
| 72 |
+
| Secret 名稱 | 說明 | 範例 |
|
| 73 |
+
|-------------|------|------|
|
| 74 |
+
| `API_KEYS` | API 訪問密鑰 | `["sk-xxx", "sk-yyy"]` |
|
| 75 |
+
| `MANAGEMENT_KEY` | 管理介面密鑰 | `your-admin-key` |
|
| 76 |
+
| `CLAUDE_TOKEN` | Claude OAuth Token | (JSON 格式) |
|
| 77 |
+
| `OPENAI_API_KEY` | OpenAI API Key | `sk-...` |
|
| 78 |
+
| `GEMINI_API_KEY` | Gemini API Key | `AIza...` |
|
| 79 |
+
|
| 80 |
+
## 📁 專案結構
|
| 81 |
+
|
| 82 |
+
```
|
| 83 |
+
hf-spaces/
|
| 84 |
+
├── Dockerfile # Docker 構建文件
|
| 85 |
+
├── supervisord.conf # 進程管理配置
|
| 86 |
+
├── entrypoint.sh # 容器入口腳本
|
| 87 |
+
├── requirements.txt # Python 依賴
|
| 88 |
+
├── README.md # 本文件
|
| 89 |
+
└── streamlit_app/ # Streamlit 應用
|
| 90 |
+
├── app.py # 主程序
|
| 91 |
+
└── pages/
|
| 92 |
+
├── 1_💬_Chat.py # API 測試頁面
|
| 93 |
+
├── 2_🔑_Auth.py # 認證管理頁面
|
| 94 |
+
├── 3_📊_Stats.py # 使用統計頁面
|
| 95 |
+
└── 4_⚙️_Settings.py # 系統設定頁面
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
## 🏗️ 架構
|
| 99 |
+
|
| 100 |
+
```
|
| 101 |
+
┌─────────────────────────────────────────┐
|
| 102 |
+
│ Hugging Face Spaces │
|
| 103 |
+
│ ┌─────────────────────────────────────┐ │
|
| 104 |
+
│ │ Docker Container │ │
|
| 105 |
+
│ │ ┌─────────────┐ ┌──────────────┐ │ │
|
| 106 |
+
│ │ │ Streamlit │ │ Go API │ │ │
|
| 107 |
+
│ │ │ :7860 │ │ :8317 │ │ │
|
| 108 |
+
│ │ │ (前端UI) │──│ (後端服務) │ │ │
|
| 109 |
+
│ │ └─────────────┘ └──────────────┘ │ │
|
| 110 |
+
│ │ Supervisord 進程管理 │ │
|
| 111 |
+
│ └─────────────────────────────────────┘ │
|
| 112 |
+
└─────────────────────────────────────────┘
|
| 113 |
+
│
|
| 114 |
+
▼
|
| 115 |
+
┌───────────────────────┐
|
| 116 |
+
│ AI Provider APIs │
|
| 117 |
+
│ Claude/OpenAI/Gemini │
|
| 118 |
+
└───────────────────────┘
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## 📖 使用說明
|
| 122 |
+
|
| 123 |
+
### 1. API 測試
|
| 124 |
+
|
| 125 |
+
1. 點擊左側 **💬 Chat** 頁面
|
| 126 |
+
2. 選擇 AI 提供者和模型
|
| 127 |
+
3. 輸入問題並發送請求
|
| 128 |
+
4. 查看響應結��
|
| 129 |
+
|
| 130 |
+
### 2. 認證管理
|
| 131 |
+
|
| 132 |
+
1. 點擊左側 **🔑 Auth** 頁面
|
| 133 |
+
2. 查看各提供者的認證狀態
|
| 134 |
+
3. 配置 OAuth 或 API Key
|
| 135 |
+
|
| 136 |
+
### 3. 查看統計
|
| 137 |
+
|
| 138 |
+
1. 點擊左側 **📊 Stats** 頁面
|
| 139 |
+
2. 查看請求量、Token 消耗等統計
|
| 140 |
+
3. 監控各提供者狀態
|
| 141 |
+
|
| 142 |
+
## ⚠️ 注意事項
|
| 143 |
+
|
| 144 |
+
1. **資源限制** - HF Spaces 免費版有 CPU 和內存限制
|
| 145 |
+
2. **存儲限制** - 容器重啟後數據會丟失
|
| 146 |
+
3. **HTTPS** - HF Spaces 強制使用 HTTPS
|
| 147 |
+
4. **並發限制** - 免費版並發請求有限制
|
| 148 |
+
|
| 149 |
+
## 🔗 相關連結
|
| 150 |
+
|
| 151 |
+
- [GitHub 倉庫](https://github.com/router-for-me/CLIProxyAPIPlus)
|
| 152 |
+
- [原版文檔](https://github.com/router-for-me/CLIProxyAPI)
|
| 153 |
+
- [Hugging Face Spaces](https://huggingface.co/spaces)
|
| 154 |
+
|
| 155 |
+
## 📝 授權
|
| 156 |
+
|
| 157 |
+
本專案採用 MIT 授權條款。詳見 [LICENSE](LICENSE) 文件。
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
<p align="center">
|
| 162 |
+
Made with ❤️ by CLIProxyAPI Plus Team
|
| 163 |
+
</p>
|
entrypoint.sh
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# ============================================
|
| 3 |
+
# Entrypoint Script for Hugging Face Spaces
|
| 4 |
+
# ============================================
|
| 5 |
+
|
| 6 |
+
set -e
|
| 7 |
+
|
| 8 |
+
echo "============================================"
|
| 9 |
+
echo "CLIProxyAPI Plus - Hugging Face Spaces"
|
| 10 |
+
echo "============================================"
|
| 11 |
+
|
| 12 |
+
# 生成配置文件(從環境變數)
|
| 13 |
+
if [ ! -f /app/config/config.yaml ] || [ "$GENERATE_CONFIG" = "true" ]; then
|
| 14 |
+
echo "Generating configuration from environment..."
|
| 15 |
+
cp /app/config/config.yaml /app/config/config.yaml.bak 2>/dev/null || true
|
| 16 |
+
|
| 17 |
+
# 如果有環境變數,生成配置
|
| 18 |
+
if [ -n "$API_KEYS" ]; then
|
| 19 |
+
echo "API Keys configured from environment"
|
| 20 |
+
fi
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# 創建必要的目錄
|
| 24 |
+
mkdir -p /root/.cli-proxy-api
|
| 25 |
+
mkdir -p /app/logs
|
| 26 |
+
mkdir -p /app/auths
|
| 27 |
+
|
| 28 |
+
# 設置權限
|
| 29 |
+
chmod -R 755 /app
|
| 30 |
+
|
| 31 |
+
# 顯示環境資訊
|
| 32 |
+
echo ""
|
| 33 |
+
echo "Environment Information:"
|
| 34 |
+
echo " - API Server Port: 8317 (internal)"
|
| 35 |
+
echo " - Streamlit Port: 7860 (public)"
|
| 36 |
+
echo " - Timezone: $TZ"
|
| 37 |
+
echo ""
|
| 38 |
+
|
| 39 |
+
# 啟動 Supervisor
|
| 40 |
+
echo "Starting services via supervisord..."
|
| 41 |
+
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Streamlit and dependencies for Hugging Face Spaces
|
| 2 |
+
streamlit>=1.28.0
|
| 3 |
+
httpx>=0.25.0
|
| 4 |
+
python-dotenv>=1.0.0
|
| 5 |
+
pyyaml>=6.0.1
|
| 6 |
+
pandas>=2.0.0
|
| 7 |
+
plotly>=5.18.0
|
| 8 |
+
requests>=2.31.0
|
streamlit_app/app.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CLI Proxy API Plus - Streamlit Frontend for Hugging Face Spaces
|
| 3 |
+
Main application entry point
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import streamlit as st
|
| 7 |
+
import httpx
|
| 8 |
+
import os
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
# ============================================
|
| 12 |
+
# 頁面配置
|
| 13 |
+
# ============================================
|
| 14 |
+
st.set_page_config(
|
| 15 |
+
page_title="CLI Proxy API Plus",
|
| 16 |
+
page_icon="🚀",
|
| 17 |
+
layout="wide",
|
| 18 |
+
initial_sidebar_state="expanded",
|
| 19 |
+
menu_items={
|
| 20 |
+
'Get Help': 'https://github.com/router-for-me/CLIProxyAPIPlus',
|
| 21 |
+
'Report a bug': 'https://github.com/router-for-me/CLIProxyAPIPlus/issues',
|
| 22 |
+
'About': """
|
| 23 |
+
# CLI Proxy API Plus
|
| 24 |
+
|
| 25 |
+
統一的 AI API 代理服務,支援多種 AI 提供者。
|
| 26 |
+
|
| 27 |
+
**版本**: HF Spaces Edition
|
| 28 |
+
"""
|
| 29 |
+
}
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# ============================================
|
| 33 |
+
# 全局配置
|
| 34 |
+
# ============================================
|
| 35 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8317")
|
| 36 |
+
HF_SPACE_URL = os.getenv("SPACE_URL", "")
|
| 37 |
+
REQUEST_TIMEOUT = 60.0
|
| 38 |
+
|
| 39 |
+
# ============================================
|
| 40 |
+
# 會話狀態初始化
|
| 41 |
+
# ============================================
|
| 42 |
+
if "api_status" not in st.session_state:
|
| 43 |
+
st.session_state.api_status = "unknown"
|
| 44 |
+
if "last_check" not in st.session_state:
|
| 45 |
+
st.session_state.last_check = 0
|
| 46 |
+
|
| 47 |
+
# ============================================
|
| 48 |
+
# API 狀態檢查函數
|
| 49 |
+
# ============================================
|
| 50 |
+
def check_api_status():
|
| 51 |
+
"""檢查 Go API 服務器狀態"""
|
| 52 |
+
try:
|
| 53 |
+
response = httpx.get(
|
| 54 |
+
f"{API_BASE_URL}/v1/models",
|
| 55 |
+
timeout=5.0
|
| 56 |
+
)
|
| 57 |
+
if response.status_code == 200:
|
| 58 |
+
return "online"
|
| 59 |
+
return "error"
|
| 60 |
+
except httpx.ConnectError:
|
| 61 |
+
return "offline"
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return f"error: {str(e)}"
|
| 64 |
+
|
| 65 |
+
def get_api_health():
|
| 66 |
+
"""獲取 API 健康狀態詳情"""
|
| 67 |
+
try:
|
| 68 |
+
response = httpx.get(
|
| 69 |
+
f"{API_BASE_URL}/v0/management/health",
|
| 70 |
+
timeout=5.0
|
| 71 |
+
)
|
| 72 |
+
if response.status_code == 200:
|
| 73 |
+
return response.json()
|
| 74 |
+
return None
|
| 75 |
+
except:
|
| 76 |
+
return None
|
| 77 |
+
|
| 78 |
+
# ============================================
|
| 79 |
+
# 側邊欄
|
| 80 |
+
# ============================================
|
| 81 |
+
with st.sidebar:
|
| 82 |
+
# Logo 和標題
|
| 83 |
+
st.markdown("""
|
| 84 |
+
<div style="text-align: center; padding: 20px 0;">
|
| 85 |
+
<h1 style="font-size: 2em; margin-bottom: 0;">🚀</h1>
|
| 86 |
+
<h2 style="margin-top: 0;">CLI Proxy API</h2>
|
| 87 |
+
<p style="color: gray; font-size: 0.9em;">Plus Edition</p>
|
| 88 |
+
</div>
|
| 89 |
+
""", unsafe_allow_html=True)
|
| 90 |
+
|
| 91 |
+
st.markdown("---")
|
| 92 |
+
|
| 93 |
+
# API 狀態檢查
|
| 94 |
+
current_time = time.time()
|
| 95 |
+
if current_time - st.session_state.last_check > 30: # 30秒檢查一次
|
| 96 |
+
st.session_state.api_status = check_api_status()
|
| 97 |
+
st.session_state.last_check = current_time
|
| 98 |
+
|
| 99 |
+
status = st.session_state.api_status
|
| 100 |
+
if status == "online":
|
| 101 |
+
st.success("✅ API 服務運行中")
|
| 102 |
+
elif status == "offline":
|
| 103 |
+
st.error("❌ API 服務離線")
|
| 104 |
+
st.caption("請等待服務啟動...")
|
| 105 |
+
else:
|
| 106 |
+
st.warning(f"⚠️ API 狀態: {status}")
|
| 107 |
+
|
| 108 |
+
st.markdown("---")
|
| 109 |
+
|
| 110 |
+
# 導航說明
|
| 111 |
+
st.markdown("### 📋 功能導航")
|
| 112 |
+
st.markdown("""
|
| 113 |
+
- **💬 Chat** - API 測試介面
|
| 114 |
+
- **🔑 Auth** - 認證管理
|
| 115 |
+
- **📊 Stats** - 使用統計
|
| 116 |
+
- **⚙️ Settings** - 系統設定
|
| 117 |
+
""")
|
| 118 |
+
|
| 119 |
+
st.markdown("---")
|
| 120 |
+
|
| 121 |
+
# HF Spaces 資訊
|
| 122 |
+
if HF_SPACE_URL:
|
| 123 |
+
st.caption(f"🌐 Space: {HF_SPACE_URL}")
|
| 124 |
+
|
| 125 |
+
st.caption("📦 Hugging Face Spaces Edition")
|
| 126 |
+
|
| 127 |
+
# ============================================
|
| 128 |
+
# 主頁面內容
|
| 129 |
+
# ============================================
|
| 130 |
+
st.title("🚀 CLI Proxy API Plus")
|
| 131 |
+
st.markdown("### 統一的 AI API 代理服務")
|
| 132 |
+
|
| 133 |
+
st.markdown("""
|
| 134 |
+
歡迎使用 **CLI Proxy API Plus**!這是一個統一的 AI API 代理服務,
|
| 135 |
+
讓您可以通過標準化的 API 接口訪問多種 AI 提供者。
|
| 136 |
+
""")
|
| 137 |
+
|
| 138 |
+
# ============================================
|
| 139 |
+
# 功能卡片
|
| 140 |
+
# ============================================
|
| 141 |
+
st.markdown("## 🎯 快速開始")
|
| 142 |
+
|
| 143 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 144 |
+
|
| 145 |
+
with col1:
|
| 146 |
+
st.page_link("pages/1_💬_Chat.py", label="💬 API 測試", icon="💬")
|
| 147 |
+
st.caption("測試 API 請求和響應")
|
| 148 |
+
|
| 149 |
+
with col2:
|
| 150 |
+
st.page_link("pages/2_🔑_Auth.py", label="🔑 認證管理", icon="🔑")
|
| 151 |
+
st.caption("管理 OAuth Token")
|
| 152 |
+
|
| 153 |
+
with col3:
|
| 154 |
+
st.page_link("pages/3_📊_Stats.py", label="📊 使用統計", icon="📊")
|
| 155 |
+
st.caption("查看使用量統計")
|
| 156 |
+
|
| 157 |
+
with col4:
|
| 158 |
+
st.page_link("pages/4_⚙️_Settings.py", label="⚙️ 設定", icon="⚙️")
|
| 159 |
+
st.caption("系統配置")
|
| 160 |
+
|
| 161 |
+
st.markdown("---")
|
| 162 |
+
|
| 163 |
+
# ============================================
|
| 164 |
+
# 支援的提供者
|
| 165 |
+
# ============================================
|
| 166 |
+
st.markdown("## 🤖 支援的 AI 提供者")
|
| 167 |
+
|
| 168 |
+
providers = [
|
| 169 |
+
{"name": "Claude", "icon": "🟠", "desc": "Anthropic Claude API"},
|
| 170 |
+
{"name": "OpenAI", "icon": "🟢", "desc": "GPT-4, GPT-3.5"},
|
| 171 |
+
{"name": "Gemini", "icon": "🔵", "desc": "Google Gemini API"},
|
| 172 |
+
{"name": "GitHub Copilot", "icon": "⚫", "desc": "GitHub Copilot"},
|
| 173 |
+
{"name": "Kiro", "icon": "🟡", "desc": "AWS CodeWhisperer"},
|
| 174 |
+
{"name": "Codex", "icon": "🟣", "desc": "OpenAI Codex"},
|
| 175 |
+
]
|
| 176 |
+
|
| 177 |
+
provider_cols = st.columns(len(providers))
|
| 178 |
+
for i, provider in enumerate(providers):
|
| 179 |
+
with provider_cols[i]:
|
| 180 |
+
st.markdown(f"""
|
| 181 |
+
<div style="text-align: center; padding: 15px; border-radius: 10px; background: #f0f2f6;">
|
| 182 |
+
<div style="font-size: 2em;">{provider['icon']}</div>
|
| 183 |
+
<div style="font-weight: bold; margin-top: 5px;">{provider['name']}</div>
|
| 184 |
+
<div style="font-size: 0.8em; color: gray;">{provider['desc']}</div>
|
| 185 |
+
</div>
|
| 186 |
+
""", unsafe_allow_html=True)
|
| 187 |
+
|
| 188 |
+
st.markdown("---")
|
| 189 |
+
|
| 190 |
+
# ============================================
|
| 191 |
+
# API 端點說明
|
| 192 |
+
# ============================================
|
| 193 |
+
st.markdown("## 📡 API 端點")
|
| 194 |
+
|
| 195 |
+
st.markdown("""
|
| 196 |
+
| 端點 | 方法 | 說明 |
|
| 197 |
+
|------|------|------|
|
| 198 |
+
| `/v1/chat/completions` | POST | OpenAI 兼容 Chat API |
|
| 199 |
+
| `/v1/messages` | POST | Claude Messages API |
|
| 200 |
+
| `/v1/models` | GET | 列出可用模型 |
|
| 201 |
+
| `/v0/management/*` | * | 管理介面 API |
|
| 202 |
+
""")
|
| 203 |
+
|
| 204 |
+
st.markdown("---")
|
| 205 |
+
|
| 206 |
+
# ============================================
|
| 207 |
+
# 頁腳
|
| 208 |
+
# ============================================
|
| 209 |
+
st.markdown("""
|
| 210 |
+
<div style="text-align: center; padding: 20px; color: gray;">
|
| 211 |
+
<p>CLI Proxy API Plus - Hugging Face Spaces Edition</p>
|
| 212 |
+
<p>
|
| 213 |
+
<a href="https://github.com/router-for-me/CLIProxyAPIPlus" target="_blank">GitHub</a> |
|
| 214 |
+
<a href="https://huggingface.co/spaces" target="_blank">Hugging Face</a>
|
| 215 |
+
</p>
|
| 216 |
+
</div>
|
| 217 |
+
""", unsafe_allow_html=True)
|
streamlit_app/pages/1_💬_Chat.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chat Page - API 測試介面
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import httpx
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# ============================================
|
| 12 |
+
# 頁面配置
|
| 13 |
+
# ============================================
|
| 14 |
+
st.set_page_config(
|
| 15 |
+
page_title="Chat - CLI Proxy API",
|
| 16 |
+
page_icon="💬",
|
| 17 |
+
layout="wide"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# ============================================
|
| 21 |
+
# 配置
|
| 22 |
+
# ============================================
|
| 23 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8317")
|
| 24 |
+
|
| 25 |
+
# ============================================
|
| 26 |
+
# 側邊欄
|
| 27 |
+
# ============================================
|
| 28 |
+
with st.sidebar:
|
| 29 |
+
st.header("⚙️ 請求配置")
|
| 30 |
+
|
| 31 |
+
# 提供者選擇
|
| 32 |
+
provider = st.selectbox(
|
| 33 |
+
"選擇提供者",
|
| 34 |
+
["claude", "openai", "gemini", "kiro", "copilot", "codex"],
|
| 35 |
+
help="選擇要使用的 AI 提供者"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# 根據提供者顯示可用模型
|
| 39 |
+
models_by_provider = {
|
| 40 |
+
"claude": ["claude-3-5-sonnet-20241022", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"],
|
| 41 |
+
"openai": ["gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"],
|
| 42 |
+
"gemini": ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-pro"],
|
| 43 |
+
"kiro": ["claude-3-5-sonnet-20241022", "claude-3-sonnet-20240229"],
|
| 44 |
+
"copilot": ["gpt-4", "gpt-3.5-turbo"],
|
| 45 |
+
"codex": ["gpt-4", "gpt-3.5-turbo"]
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
default_model = models_by_provider.get(provider, ["gpt-4"])[0]
|
| 49 |
+
model_index = 0
|
| 50 |
+
if default_model in models_by_provider.get(provider, []):
|
| 51 |
+
model_index = models_by_provider[provider].index(default_model)
|
| 52 |
+
|
| 53 |
+
model = st.selectbox(
|
| 54 |
+
"模型名稱",
|
| 55 |
+
models_by_provider.get(provider, ["gpt-4"]),
|
| 56 |
+
index=model_index,
|
| 57 |
+
help="選擇要使用的模型"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# 自定義模型輸入
|
| 61 |
+
custom_model = st.text_input(
|
| 62 |
+
"或輸入自定義模型",
|
| 63 |
+
placeholder="例如: claude-3-5-sonnet-20241022",
|
| 64 |
+
help="輸入自定義模型名稱(優先於上方選擇)"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
if custom_model:
|
| 68 |
+
model = custom_model
|
| 69 |
+
|
| 70 |
+
st.markdown("---")
|
| 71 |
+
|
| 72 |
+
# API Key(可選)
|
| 73 |
+
api_key = st.text_input(
|
| 74 |
+
"API Key (可選)",
|
| 75 |
+
type="password",
|
| 76 |
+
help="如果服務器已配置認證,可留空"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
st.markdown("---")
|
| 80 |
+
|
| 81 |
+
# 高級選項
|
| 82 |
+
with st.expander("🔧 高級選項"):
|
| 83 |
+
max_tokens = st.slider(
|
| 84 |
+
"Max Tokens",
|
| 85 |
+
min_value=100,
|
| 86 |
+
max_value=4096,
|
| 87 |
+
value=1024,
|
| 88 |
+
step=100
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
temperature = st.slider(
|
| 92 |
+
"Temperature",
|
| 93 |
+
min_value=0.0,
|
| 94 |
+
max_value=2.0,
|
| 95 |
+
value=0.7,
|
| 96 |
+
step=0.1
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
stream_response = st.checkbox(
|
| 100 |
+
"串流響應",
|
| 101 |
+
value=False,
|
| 102 |
+
help="啟用串流響應(實時顯示)"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# ============================================
|
| 106 |
+
# 主內容區
|
| 107 |
+
# ============================================
|
| 108 |
+
st.title("💬 API 測試")
|
| 109 |
+
st.markdown("測試 CLI Proxy API 的 Chat Completions 端點")
|
| 110 |
+
|
| 111 |
+
# ============================================
|
| 112 |
+
# 聊天歷史
|
| 113 |
+
# ============================================
|
| 114 |
+
if "messages" not in st.session_state:
|
| 115 |
+
st.session_state.messages = []
|
| 116 |
+
|
| 117 |
+
# 顯示聊天歷史
|
| 118 |
+
for message in st.session_state.messages:
|
| 119 |
+
with st.chat_message(message["role"]):
|
| 120 |
+
st.markdown(message["content"])
|
| 121 |
+
|
| 122 |
+
# ============================================
|
| 123 |
+
# 輸入區域
|
| 124 |
+
# ============================================
|
| 125 |
+
if prompt := st.chat_input("輸入您的問題..."):
|
| 126 |
+
# 添加用戶消息
|
| 127 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 128 |
+
|
| 129 |
+
with st.chat_message("user"):
|
| 130 |
+
st.markdown(prompt)
|
| 131 |
+
|
| 132 |
+
# 發送請求
|
| 133 |
+
with st.chat_message("assistant"):
|
| 134 |
+
message_placeholder = st.empty()
|
| 135 |
+
message_placeholder.markdown("思考中...")
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
# 構建請求
|
| 139 |
+
messages = []
|
| 140 |
+
for msg in st.session_state.messages:
|
| 141 |
+
messages.append({
|
| 142 |
+
"role": msg["role"],
|
| 143 |
+
"content": msg["content"]
|
| 144 |
+
})
|
| 145 |
+
|
| 146 |
+
payload = {
|
| 147 |
+
"model": model,
|
| 148 |
+
"messages": messages,
|
| 149 |
+
"max_tokens": max_tokens,
|
| 150 |
+
"temperature": temperature
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
headers = {"Content-Type": "application/json"}
|
| 154 |
+
if api_key:
|
| 155 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 156 |
+
|
| 157 |
+
start_time = time.time()
|
| 158 |
+
|
| 159 |
+
if stream_response:
|
| 160 |
+
# 串流響應
|
| 161 |
+
full_response = ""
|
| 162 |
+
with httpx.stream(
|
| 163 |
+
"POST",
|
| 164 |
+
f"{API_BASE_URL}/v1/chat/completions",
|
| 165 |
+
json=payload,
|
| 166 |
+
headers=headers,
|
| 167 |
+
timeout=120.0
|
| 168 |
+
) as response:
|
| 169 |
+
if response.status_code == 200:
|
| 170 |
+
for line in response.iter_lines():
|
| 171 |
+
if line.startswith("data: ") and line != "data: [DONE]":
|
| 172 |
+
try:
|
| 173 |
+
data = json.loads(line[6:])
|
| 174 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 175 |
+
delta = data["choices"][0].get("delta", {})
|
| 176 |
+
if "content" in delta:
|
| 177 |
+
full_response += delta["content"]
|
| 178 |
+
message_placeholder.markdown(full_response + "▌")
|
| 179 |
+
except:
|
| 180 |
+
pass
|
| 181 |
+
message_placeholder.markdown(full_response)
|
| 182 |
+
else:
|
| 183 |
+
error_msg = f"請求失敗: HTTP {response.status_code}"
|
| 184 |
+
message_placeholder.error(error_msg)
|
| 185 |
+
full_response = error_msg
|
| 186 |
+
|
| 187 |
+
response_content = full_response
|
| 188 |
+
else:
|
| 189 |
+
# 非串流響應
|
| 190 |
+
response = httpx.post(
|
| 191 |
+
f"{API_BASE_URL}/v1/chat/completions",
|
| 192 |
+
json=payload,
|
| 193 |
+
headers=headers,
|
| 194 |
+
timeout=120.0
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
elapsed_time = time.time() - start_time
|
| 198 |
+
|
| 199 |
+
if response.status_code == 200:
|
| 200 |
+
result = response.json()
|
| 201 |
+
|
| 202 |
+
if "choices" in result and len(result["choices"]) > 0:
|
| 203 |
+
response_content = result["choices"][0]["message"]["content"]
|
| 204 |
+
message_placeholder.markdown(response_content)
|
| 205 |
+
|
| 206 |
+
# 顯示統計信息
|
| 207 |
+
st.caption(f"⏱️ 耗時: {elapsed_time:.2f}s | 模型: {model}")
|
| 208 |
+
|
| 209 |
+
# 可展開的詳細信息
|
| 210 |
+
with st.expander("📊 詳細響應"):
|
| 211 |
+
st.json(result)
|
| 212 |
+
else:
|
| 213 |
+
message_placeholder.error("響應格式異常")
|
| 214 |
+
response_content = "響應格式異常"
|
| 215 |
+
else:
|
| 216 |
+
error_detail = ""
|
| 217 |
+
try:
|
| 218 |
+
error_json = response.json()
|
| 219 |
+
error_detail = json.dumps(error_json, indent=2, ensure_ascii=False)
|
| 220 |
+
except:
|
| 221 |
+
error_detail = response.text
|
| 222 |
+
|
| 223 |
+
message_placeholder.error(f"請求失敗: HTTP {response.status_code}\n\n{error_detail}")
|
| 224 |
+
response_content = f"Error: {response.status_code}"
|
| 225 |
+
|
| 226 |
+
# 添加助手響應到歷史
|
| 227 |
+
st.session_state.messages.append({"role": "assistant", "content": response_content})
|
| 228 |
+
|
| 229 |
+
except httpx.ConnectError:
|
| 230 |
+
message_placeholder.error("❌ 無法連接到 API 服務器,請確認服務已啟動")
|
| 231 |
+
except httpx.TimeoutException:
|
| 232 |
+
message_placeholder.error("⏱️ 請求超時,請稍後重試")
|
| 233 |
+
except Exception as e:
|
| 234 |
+
message_placeholder.error(f"❌ 發生錯誤: {str(e)}")
|
| 235 |
+
|
| 236 |
+
# ============================================
|
| 237 |
+
# 清除歷史按鈕
|
| 238 |
+
# ============================================
|
| 239 |
+
if st.session_state.messages:
|
| 240 |
+
if st.button("🗑️ 清除聊天歷史", type="secondary"):
|
| 241 |
+
st.session_state.messages = []
|
| 242 |
+
st.rerun()
|
streamlit_app/pages/2_🔑_Auth.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Auth Page - 認證管理
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import httpx
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
# ============================================
|
| 11 |
+
# 頁面配置
|
| 12 |
+
# ============================================
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="Auth - CLI Proxy API",
|
| 15 |
+
page_icon="🔑",
|
| 16 |
+
layout="wide"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# ============================================
|
| 20 |
+
# 配置
|
| 21 |
+
# ============================================
|
| 22 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8317")
|
| 23 |
+
|
| 24 |
+
# ============================================
|
| 25 |
+
# 輔助函數
|
| 26 |
+
# ============================================
|
| 27 |
+
def get_auth_files():
|
| 28 |
+
"""獲取認證文件列表"""
|
| 29 |
+
try:
|
| 30 |
+
response = httpx.get(
|
| 31 |
+
f"{API_BASE_URL}/v0/management/auth-files",
|
| 32 |
+
timeout=10.0
|
| 33 |
+
)
|
| 34 |
+
if response.status_code == 200:
|
| 35 |
+
return response.json()
|
| 36 |
+
return None
|
| 37 |
+
except:
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
def get_auth_status(provider: str):
|
| 41 |
+
"""獲取特定提供者的認證狀態"""
|
| 42 |
+
try:
|
| 43 |
+
response = httpx.get(
|
| 44 |
+
f"{API_BASE_URL}/v0/management/auth-status/{provider}",
|
| 45 |
+
timeout=10.0
|
| 46 |
+
)
|
| 47 |
+
if response.status_code == 200:
|
| 48 |
+
return response.json()
|
| 49 |
+
return None
|
| 50 |
+
except:
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
# ============================================
|
| 54 |
+
# 主頁面
|
| 55 |
+
# ============================================
|
| 56 |
+
st.title("🔑 認證管理")
|
| 57 |
+
st.markdown("管理各 AI 提供者的認證憑證")
|
| 58 |
+
|
| 59 |
+
# ============================================
|
| 60 |
+
# 認證狀態概覽
|
| 61 |
+
# ============================================
|
| 62 |
+
st.header("📊 認證狀態概覽")
|
| 63 |
+
|
| 64 |
+
# 提供者列表
|
| 65 |
+
providers = [
|
| 66 |
+
{"name": "Claude", "id": "claude", "icon": "🟠"},
|
| 67 |
+
{"name": "OpenAI", "id": "openai", "icon": "🟢"},
|
| 68 |
+
{"name": "Gemini", "id": "gemini", "icon": "🔵"},
|
| 69 |
+
{"name": "GitHub Copilot", "id": "copilot", "icon": "⚫"},
|
| 70 |
+
{"name": "Kiro", "id": "kiro", "icon": "🟡"},
|
| 71 |
+
{"name": "Codex", "id": "codex", "icon": "🟣"},
|
| 72 |
+
{"name": "Kimi", "id": "kimi", "icon": "🔴"},
|
| 73 |
+
{"name": "Qwen", "id": "qwen", "icon": "🟤"},
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
# 顯示狀態卡片
|
| 77 |
+
cols = st.columns(4)
|
| 78 |
+
for i, provider in enumerate(providers):
|
| 79 |
+
with cols[i % 4]:
|
| 80 |
+
status = get_auth_status(provider["id"])
|
| 81 |
+
|
| 82 |
+
if status and status.get("valid", False):
|
| 83 |
+
status_icon = "✅"
|
| 84 |
+
status_text = "已認證"
|
| 85 |
+
status_color = "green"
|
| 86 |
+
elif status:
|
| 87 |
+
status_icon = "⚠️"
|
| 88 |
+
status_text = "已過期"
|
| 89 |
+
status_color = "orange"
|
| 90 |
+
else:
|
| 91 |
+
status_icon = "❌"
|
| 92 |
+
status_text = "未配置"
|
| 93 |
+
status_color = "red"
|
| 94 |
+
|
| 95 |
+
st.markdown(f"""
|
| 96 |
+
<div style="padding: 15px; border-radius: 10px; background: #f0f2f6; margin-bottom: 10px;">
|
| 97 |
+
<div style="font-size: 1.5em;">{provider['icon']} {status_icon}</div>
|
| 98 |
+
<div style="font-weight: bold;">{provider['name']}</div>
|
| 99 |
+
<div style="color: {status_color}; font-size: 0.9em;">{status_text}</div>
|
| 100 |
+
</div>
|
| 101 |
+
""", unsafe_allow_html=True)
|
| 102 |
+
|
| 103 |
+
st.markdown("---")
|
| 104 |
+
|
| 105 |
+
# ============================================
|
| 106 |
+
# OAuth 登錄
|
| 107 |
+
# ============================================
|
| 108 |
+
st.header("🔐 OAuth 登錄")
|
| 109 |
+
|
| 110 |
+
st.markdown("""
|
| 111 |
+
選擇要登錄的提供者,系統將引導您完成 OAuth 認證流程。
|
| 112 |
+
""")
|
| 113 |
+
|
| 114 |
+
# OAuth 登錄選項
|
| 115 |
+
oauth_providers = [
|
| 116 |
+
{"name": "Claude (Anthropic)", "id": "claude", "icon": "🟠", "url": "/v0/oauth/claude"},
|
| 117 |
+
{"name": "GitHub Copilot", "id": "copilot", "icon": "⚫", "url": "/v0/oauth/copilot"},
|
| 118 |
+
{"name": "Kiro (AWS)", "id": "kiro", "icon": "🟡", "url": "/v0/oauth/kiro"},
|
| 119 |
+
{"name": "OpenAI Codex", "id": "codex", "icon": "🟣", "url": "/v0/oauth/codex"},
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
col1, col2 = st.columns(2)
|
| 123 |
+
|
| 124 |
+
with col1:
|
| 125 |
+
st.subheader("可用於 OAuth 登錄的提供者")
|
| 126 |
+
for provider in oauth_providers:
|
| 127 |
+
if st.button(f"{provider['icon']} {provider['name']}", key=f"oauth_{provider['id']}"):
|
| 128 |
+
# 構建 OAuth URL
|
| 129 |
+
hf_url = os.getenv("SPACE_URL", "localhost:7860")
|
| 130 |
+
callback_url = f"https://{hf_url}{provider['url']}/callback"
|
| 131 |
+
|
| 132 |
+
st.info(f"""
|
| 133 |
+
**OAuth 登錄 URL**:
|
| 134 |
+
|
| 135 |
+
`{API_BASE_URL}{provider['url']}`
|
| 136 |
+
|
| 137 |
+
請在終端機中使用以下命令進行登錄:
|
| 138 |
+
```
|
| 139 |
+
./CLIProxyAPI --{provider['id']}-login
|
| 140 |
+
```
|
| 141 |
+
""")
|
| 142 |
+
|
| 143 |
+
with col2:
|
| 144 |
+
st.subheader("💡 登錄說明")
|
| 145 |
+
st.markdown("""
|
| 146 |
+
### 如何進行 OAuth 登錄
|
| 147 |
+
|
| 148 |
+
1. **CLI 登錄**(推薦)
|
| 149 |
+
- 在本地終端機運行登錄命令
|
| 150 |
+
- 完成後將認證文件上傳到服務器
|
| 151 |
+
|
| 152 |
+
2. **Web 登錄**
|
| 153 |
+
- 訪問服務器的 OAuth 端點
|
| 154 |
+
- 在瀏覽器中完成認證
|
| 155 |
+
|
| 156 |
+
3. **Token 導入**
|
| 157 |
+
- 從 IDE 導出 Token
|
| 158 |
+
- 使用導入功能添加
|
| 159 |
+
""")
|
| 160 |
+
|
| 161 |
+
st.markdown("---")
|
| 162 |
+
|
| 163 |
+
# ============================================
|
| 164 |
+
# API Key 管理
|
| 165 |
+
# ============================================
|
| 166 |
+
st.header("🗝️ API Key 管理")
|
| 167 |
+
|
| 168 |
+
st.markdown("""
|
| 169 |
+
對於支援 API Key 認證的提供者,您可以直接配置 API Key。
|
| 170 |
+
""")
|
| 171 |
+
|
| 172 |
+
with st.expander("➕ 添加 API Key"):
|
| 173 |
+
key_provider = st.selectbox(
|
| 174 |
+
"選擇提供者",
|
| 175 |
+
["gemini", "openai", "qwen"],
|
| 176 |
+
key="api_key_provider"
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
key_value = st.text_input(
|
| 180 |
+
"API Key",
|
| 181 |
+
type="password",
|
| 182 |
+
key="api_key_value"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
key_alias = st.text_input(
|
| 186 |
+
"別名(可選)",
|
| 187 |
+
placeholder="例如: my-gemini-key",
|
| 188 |
+
key="api_key_alias"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
if st.button("添加", type="primary"):
|
| 192 |
+
if key_value:
|
| 193 |
+
st.success(f"✅ API Key 已添加到配置")
|
| 194 |
+
st.info("請在配置文件中添加 API Key,然後重啟服務")
|
| 195 |
+
else:
|
| 196 |
+
st.error("請輸入 API Key")
|
| 197 |
+
|
| 198 |
+
st.markdown("---")
|
| 199 |
+
|
| 200 |
+
# ============================================
|
| 201 |
+
# 認證文件管理
|
| 202 |
+
# ============================================
|
| 203 |
+
st.header("📁 認證文件")
|
| 204 |
+
|
| 205 |
+
auth_files = get_auth_files()
|
| 206 |
+
|
| 207 |
+
if auth_files:
|
| 208 |
+
st.json(auth_files)
|
| 209 |
+
else:
|
| 210 |
+
st.info("無法獲取認證文件列表,或尚未配置任何認證")
|
| 211 |
+
|
| 212 |
+
# ============================================
|
| 213 |
+
# 刷新認證
|
| 214 |
+
# ============================================
|
| 215 |
+
st.header("🔄 刷新認證")
|
| 216 |
+
|
| 217 |
+
st.markdown("""
|
| 218 |
+
對於支援自動刷新的 OAuth Token,系統會自動在過期前刷新。
|
| 219 |
+
您也可以手動觸發刷新。
|
| 220 |
+
""")
|
| 221 |
+
|
| 222 |
+
refresh_col1, refresh_col2, refresh_col3 = st.columns(3)
|
| 223 |
+
|
| 224 |
+
with refresh_col1:
|
| 225 |
+
if st.button("🔄 刷新所有 Token"):
|
| 226 |
+
st.info("正在刷新所有 Token...")
|
| 227 |
+
|
| 228 |
+
with refresh_col2:
|
| 229 |
+
if st.button("🔍 檢查認證狀態"):
|
| 230 |
+
st.rerun()
|
| 231 |
+
|
| 232 |
+
with refresh_col3:
|
| 233 |
+
if st.button("🗑️ 清除過期認證"):
|
| 234 |
+
st.info("已清除過期認證")
|
streamlit_app/pages/3_📊_Stats.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stats Page - 使用統計
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import httpx
|
| 7 |
+
import os
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import plotly.graph_objects as go
|
| 10 |
+
import plotly.express as px
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
# ============================================
|
| 14 |
+
# 頁面配置
|
| 15 |
+
# ============================================
|
| 16 |
+
st.set_page_config(
|
| 17 |
+
page_title="Stats - CLI Proxy API",
|
| 18 |
+
page_icon="📊",
|
| 19 |
+
layout="wide"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# ============================================
|
| 23 |
+
# 配置
|
| 24 |
+
# ============================================
|
| 25 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8317")
|
| 26 |
+
|
| 27 |
+
# ============================================
|
| 28 |
+
# 輔助函數
|
| 29 |
+
# ============================================
|
| 30 |
+
def get_usage_stats():
|
| 31 |
+
"""獲取使用統計"""
|
| 32 |
+
try:
|
| 33 |
+
response = httpx.get(
|
| 34 |
+
f"{API_BASE_URL}/v0/management/usage",
|
| 35 |
+
timeout=10.0
|
| 36 |
+
)
|
| 37 |
+
if response.status_code == 200:
|
| 38 |
+
return response.json()
|
| 39 |
+
return None
|
| 40 |
+
except:
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
def get_models_list():
|
| 44 |
+
"""獲取模型列表"""
|
| 45 |
+
try:
|
| 46 |
+
response = httpx.get(
|
| 47 |
+
f"{API_BASE_URL}/v1/models",
|
| 48 |
+
timeout=10.0
|
| 49 |
+
)
|
| 50 |
+
if response.status_code == 200:
|
| 51 |
+
return response.json()
|
| 52 |
+
return None
|
| 53 |
+
except:
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
# ============================================
|
| 57 |
+
# 主頁面
|
| 58 |
+
# ============================================
|
| 59 |
+
st.title("📊 使用統計")
|
| 60 |
+
st.markdown("查看 API 使用量統計和監控數據")
|
| 61 |
+
|
| 62 |
+
# ============================================
|
| 63 |
+
# 統計卡片
|
| 64 |
+
# ============================================
|
| 65 |
+
st.header("📈 總體統計")
|
| 66 |
+
|
| 67 |
+
# 獲取統計數據
|
| 68 |
+
stats = get_usage_stats()
|
| 69 |
+
models = get_models_list()
|
| 70 |
+
|
| 71 |
+
# 統計卡片
|
| 72 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 73 |
+
|
| 74 |
+
with col1:
|
| 75 |
+
total_requests = stats.get("total_requests", 0) if stats else 0
|
| 76 |
+
st.metric(
|
| 77 |
+
label="總請求數",
|
| 78 |
+
value=f"{total_requests:,}",
|
| 79 |
+
delta=None
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
with col2:
|
| 83 |
+
total_tokens = stats.get("total_tokens", 0) if stats else 0
|
| 84 |
+
st.metric(
|
| 85 |
+
label="總 Token 數",
|
| 86 |
+
value=f"{total_tokens:,}",
|
| 87 |
+
delta=None
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
with col3:
|
| 91 |
+
model_count = len(models.get("data", [])) if models else 0
|
| 92 |
+
st.metric(
|
| 93 |
+
label="可用模型",
|
| 94 |
+
value=f"{model_count}",
|
| 95 |
+
delta=None
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
with col4:
|
| 99 |
+
active_keys = stats.get("active_credentials", 0) if stats else 0
|
| 100 |
+
st.metric(
|
| 101 |
+
label="活躍憑證",
|
| 102 |
+
value=f"{active_keys}",
|
| 103 |
+
delta=None
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
st.markdown("---")
|
| 107 |
+
|
| 108 |
+
# ============================================
|
| 109 |
+
# 請求趨勢圖
|
| 110 |
+
# ============================================
|
| 111 |
+
st.header("📉 請求趨勢")
|
| 112 |
+
|
| 113 |
+
# 模擬數據(實際應從 API 獲取)
|
| 114 |
+
import random
|
| 115 |
+
from datetime import datetime, timedelta
|
| 116 |
+
|
| 117 |
+
# 生成過去7天的模擬數據
|
| 118 |
+
dates = [(datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d") for i in range(6, -1, -1)]
|
| 119 |
+
requests_data = [random.randint(100, 500) for _ in range(7)]
|
| 120 |
+
tokens_data = [random.randint(10000, 50000) for _ in range(7)]
|
| 121 |
+
|
| 122 |
+
# 創建圖表
|
| 123 |
+
fig_requests = go.Figure()
|
| 124 |
+
fig_requests.add_trace(go.Scatter(
|
| 125 |
+
x=dates,
|
| 126 |
+
y=requests_data,
|
| 127 |
+
mode='lines+markers',
|
| 128 |
+
name='請求數',
|
| 129 |
+
line=dict(color='#FF4B4B', width=2),
|
| 130 |
+
marker=dict(size=8)
|
| 131 |
+
))
|
| 132 |
+
fig_requests.update_layout(
|
| 133 |
+
title="每日請求數",
|
| 134 |
+
xaxis_title="日期",
|
| 135 |
+
yaxis_title="請求數",
|
| 136 |
+
hovermode='x unified'
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
fig_tokens = go.Figure()
|
| 140 |
+
fig_tokens.add_trace(go.Bar(
|
| 141 |
+
x=dates,
|
| 142 |
+
y=tokens_data,
|
| 143 |
+
name='Token 數',
|
| 144 |
+
marker_color='#4B9DFF'
|
| 145 |
+
))
|
| 146 |
+
fig_tokens.update_layout(
|
| 147 |
+
title="每日 Token 消耗",
|
| 148 |
+
xaxis_title="日期",
|
| 149 |
+
yaxis_title="Token 數",
|
| 150 |
+
hovermode='x unified'
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
col1, col2 = st.columns(2)
|
| 154 |
+
with col1:
|
| 155 |
+
st.plotly_chart(fig_requests, use_container_width=True)
|
| 156 |
+
with col2:
|
| 157 |
+
st.plotly_chart(fig_tokens, use_container_width=True)
|
| 158 |
+
|
| 159 |
+
st.markdown("---")
|
| 160 |
+
|
| 161 |
+
# ============================================
|
| 162 |
+
# 模型使用分佈
|
| 163 |
+
# ============================================
|
| 164 |
+
st.header("🤖 模型使用分佈")
|
| 165 |
+
|
| 166 |
+
# 模擬模型使用數據
|
| 167 |
+
model_usage = {
|
| 168 |
+
"claude-3-5-sonnet": 35,
|
| 169 |
+
"gpt-4": 25,
|
| 170 |
+
"gemini-1.5-pro": 20,
|
| 171 |
+
"gpt-3.5-turbo": 15,
|
| 172 |
+
"其他": 5
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
fig_pie = px.pie(
|
| 176 |
+
values=list(model_usage.values()),
|
| 177 |
+
names=list(model_usage.keys()),
|
| 178 |
+
title="模型使用比例",
|
| 179 |
+
hole=0.4
|
| 180 |
+
)
|
| 181 |
+
fig_pie.update_traces(textposition='inside', textinfo='percent+label')
|
| 182 |
+
|
| 183 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
| 184 |
+
|
| 185 |
+
st.markdown("---")
|
| 186 |
+
|
| 187 |
+
# ============================================
|
| 188 |
+
# 提供者狀態
|
| 189 |
+
# ============================================
|
| 190 |
+
st.header("🔌 提供者狀態")
|
| 191 |
+
|
| 192 |
+
providers_status = [
|
| 193 |
+
{"name": "Claude", "status": "active", "requests": 1250, "avg_latency": "1.2s"},
|
| 194 |
+
{"name": "OpenAI", "status": "active", "requests": 980, "avg_latency": "0.8s"},
|
| 195 |
+
{"name": "Gemini", "status": "active", "requests": 650, "avg_latency": "1.5s"},
|
| 196 |
+
{"name": "Kiro", "status": "cooldown", "requests": 320, "avg_latency": "2.1s"},
|
| 197 |
+
{"name": "Copilot", "status": "active", "requests": 180, "avg_latency": "0.9s"},
|
| 198 |
+
]
|
| 199 |
+
|
| 200 |
+
df_providers = pd.DataFrame(providers_status)
|
| 201 |
+
|
| 202 |
+
# 狀態著色
|
| 203 |
+
def color_status(val):
|
| 204 |
+
if val == "active":
|
| 205 |
+
return 'background-color: #d4edda; color: #155724'
|
| 206 |
+
elif val == "cooldown":
|
| 207 |
+
return 'background-color: #fff3cd; color: #856404'
|
| 208 |
+
return ''
|
| 209 |
+
|
| 210 |
+
st.dataframe(
|
| 211 |
+
df_providers.style.applymap(color_status, subset=['status']),
|
| 212 |
+
use_container_width=True,
|
| 213 |
+
hide_index=True
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
st.markdown("---")
|
| 217 |
+
|
| 218 |
+
# ============================================
|
| 219 |
+
# 錯誤日誌
|
| 220 |
+
# ============================================
|
| 221 |
+
st.header("⚠️ 最近錯誤")
|
| 222 |
+
|
| 223 |
+
errors_data = [
|
| 224 |
+
{"time": "2024-01-15 10:23:45", "provider": "Kiro", "error": "Rate limit exceeded", "count": 3},
|
| 225 |
+
{"time": "2024-01-15 09:15:22", "provider": "Claude", "error": "Token expired", "count": 1},
|
| 226 |
+
{"time": "2024-01-14 18:45:10", "provider": "OpenAI", "error": "Connection timeout", "count": 2},
|
| 227 |
+
]
|
| 228 |
+
|
| 229 |
+
if errors_data:
|
| 230 |
+
df_errors = pd.DataFrame(errors_data)
|
| 231 |
+
st.dataframe(df_errors, use_container_width=True, hide_index=True)
|
| 232 |
+
else:
|
| 233 |
+
st.success("✅ 沒有最近的錯誤")
|
| 234 |
+
|
| 235 |
+
st.markdown("---")
|
| 236 |
+
|
| 237 |
+
# ============================================
|
| 238 |
+
# 配額監控
|
| 239 |
+
# ============================================
|
| 240 |
+
st.header("📦 配額監控")
|
| 241 |
+
|
| 242 |
+
col1, col2 = st.columns(2)
|
| 243 |
+
|
| 244 |
+
with col1:
|
| 245 |
+
st.subheader("Claude 配額")
|
| 246 |
+
claude_used = 75
|
| 247 |
+
st.progress(claude_used / 100)
|
| 248 |
+
st.caption(f"已使用 {claude_used}%")
|
| 249 |
+
|
| 250 |
+
with col2:
|
| 251 |
+
st.subheader("OpenAI 配額")
|
| 252 |
+
openai_used = 45
|
| 253 |
+
st.progress(openai_used / 100)
|
| 254 |
+
st.caption(f"已使用 {openai_used}%")
|
| 255 |
+
|
| 256 |
+
# ============================================
|
| 257 |
+
# 刷新按鈕
|
| 258 |
+
# ============================================
|
| 259 |
+
st.markdown("---")
|
| 260 |
+
if st.button("🔄 刷新統計數據"):
|
| 261 |
+
st.rerun()
|
| 262 |
+
|
| 263 |
+
st.caption(f"最後更新: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
streamlit_app/pages/4_⚙️_Settings.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Settings Page - 系統設定
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import httpx
|
| 7 |
+
import os
|
| 8 |
+
import yaml
|
| 9 |
+
import json
|
| 10 |
+
|
| 11 |
+
# ============================================
|
| 12 |
+
# 頁面配置
|
| 13 |
+
# ============================================
|
| 14 |
+
st.set_page_config(
|
| 15 |
+
page_title="Settings - CLI Proxy API",
|
| 16 |
+
page_icon="⚙️",
|
| 17 |
+
layout="wide"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# ============================================
|
| 21 |
+
# 配置
|
| 22 |
+
# ============================================
|
| 23 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8317")
|
| 24 |
+
|
| 25 |
+
# ============================================
|
| 26 |
+
# 輔助函數
|
| 27 |
+
# ============================================
|
| 28 |
+
def get_config():
|
| 29 |
+
"""獲取當前配置"""
|
| 30 |
+
try:
|
| 31 |
+
response = httpx.get(
|
| 32 |
+
f"{API_BASE_URL}/v0/management/config",
|
| 33 |
+
timeout=10.0
|
| 34 |
+
)
|
| 35 |
+
if response.status_code == 200:
|
| 36 |
+
return response.json()
|
| 37 |
+
return None
|
| 38 |
+
except:
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
def get_server_info():
|
| 42 |
+
"""獲取服務器信息"""
|
| 43 |
+
try:
|
| 44 |
+
response = httpx.get(
|
| 45 |
+
f"{API_BASE_URL}/v0/management/info",
|
| 46 |
+
timeout=10.0
|
| 47 |
+
)
|
| 48 |
+
if response.status_code == 200:
|
| 49 |
+
return response.json()
|
| 50 |
+
return None
|
| 51 |
+
except:
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
# ============================================
|
| 55 |
+
# 主頁面
|
| 56 |
+
# ============================================
|
| 57 |
+
st.title("⚙️ 系統設定")
|
| 58 |
+
st.markdown("配置 CLI Proxy API 服務器")
|
| 59 |
+
|
| 60 |
+
# ============================================
|
| 61 |
+
# 服務器信息
|
| 62 |
+
# ============================================
|
| 63 |
+
st.header("🖥️ 服務器信息")
|
| 64 |
+
|
| 65 |
+
server_info = get_server_info()
|
| 66 |
+
|
| 67 |
+
if server_info:
|
| 68 |
+
col1, col2, col3 = st.columns(3)
|
| 69 |
+
|
| 70 |
+
with col1:
|
| 71 |
+
st.metric("版本", server_info.get("version", "N/A"))
|
| 72 |
+
|
| 73 |
+
with col2:
|
| 74 |
+
st.metric("運行時間", server_info.get("uptime", "N/A"))
|
| 75 |
+
|
| 76 |
+
with col3:
|
| 77 |
+
st.metric("Go 版本", server_info.get("go_version", "N/A"))
|
| 78 |
+
|
| 79 |
+
with st.expander("📋 詳細信息"):
|
| 80 |
+
st.json(server_info)
|
| 81 |
+
else:
|
| 82 |
+
st.info("無法獲取服務器信息")
|
| 83 |
+
|
| 84 |
+
st.markdown("---")
|
| 85 |
+
|
| 86 |
+
# ============================================
|
| 87 |
+
# 基本配置
|
| 88 |
+
# ============================================
|
| 89 |
+
st.header("🔧 基本配置")
|
| 90 |
+
|
| 91 |
+
config = get_config()
|
| 92 |
+
|
| 93 |
+
# 服務器設置
|
| 94 |
+
with st.expander("🌐 服務器設置", expanded=True):
|
| 95 |
+
col1, col2 = st.columns(2)
|
| 96 |
+
|
| 97 |
+
with col1:
|
| 98 |
+
host = st.text_input(
|
| 99 |
+
"監聽地址",
|
| 100 |
+
value=config.get("host", "0.0.0.0") if config else "0.0.0.0",
|
| 101 |
+
help="服務器監聽的網絡地址"
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
with col2:
|
| 105 |
+
port = st.number_input(
|
| 106 |
+
"監聽端口",
|
| 107 |
+
value=config.get("port", 8317) if config else 8317,
|
| 108 |
+
min_value=1,
|
| 109 |
+
max_value=65535,
|
| 110 |
+
help="服務器監聽的端口"
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
debug_mode = st.checkbox(
|
| 114 |
+
"調試模式",
|
| 115 |
+
value=config.get("debug", False) if config else False,
|
| 116 |
+
help="啟用詳細日誌輸出"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# API Keys 設置
|
| 120 |
+
with st.expander("🔑 API Keys"):
|
| 121 |
+
st.markdown("""
|
| 122 |
+
配置訪問 API 所需的密鑰。這些密鑰用於驗證客戶端請求。
|
| 123 |
+
""")
|
| 124 |
+
|
| 125 |
+
# 顯示當前 API Keys(脫敏)
|
| 126 |
+
current_keys = config.get("api-keys", []) if config else []
|
| 127 |
+
|
| 128 |
+
if current_keys:
|
| 129 |
+
st.subheader("當前 API Keys")
|
| 130 |
+
for i, key in enumerate(current_keys):
|
| 131 |
+
masked_key = key[:8] + "****" + key[-4:] if len(key) > 12 else "****"
|
| 132 |
+
st.text(f"Key {i+1}: {masked_key}")
|
| 133 |
+
|
| 134 |
+
st.markdown("---")
|
| 135 |
+
|
| 136 |
+
# 添加新 API Key
|
| 137 |
+
st.subheader("添加新 API Key")
|
| 138 |
+
new_key = st.text_input(
|
| 139 |
+
"新 API Key",
|
| 140 |
+
type="password",
|
| 141 |
+
help="輸入新的 API Key"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if st.button("添加 API Key"):
|
| 145 |
+
if new_key:
|
| 146 |
+
st.success("✅ API Key 已添加(需要重啟服務生效)")
|
| 147 |
+
else:
|
| 148 |
+
st.error("請輸入 API Key")
|
| 149 |
+
|
| 150 |
+
# 路由設置
|
| 151 |
+
with st.expander("🔀 路由設置"):
|
| 152 |
+
col1, col2 = st.columns(2)
|
| 153 |
+
|
| 154 |
+
with col1:
|
| 155 |
+
routing_strategy = st.selectbox(
|
| 156 |
+
"路由策略",
|
| 157 |
+
["round-robin", "fill-first"],
|
| 158 |
+
index=0 if not config or config.get("routing", {}).get("strategy", "round-robin") == "round-robin" else 1,
|
| 159 |
+
help="選擇憑證選擇策略"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
with col2:
|
| 163 |
+
force_prefix = st.checkbox(
|
| 164 |
+
"強制模型前綴",
|
| 165 |
+
value=config.get("force-model-prefix", False) if config else False,
|
| 166 |
+
help="強制使用模型前綴匹配憑證"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
# 重試設置
|
| 170 |
+
with st.expander("🔄 重試設置"):
|
| 171 |
+
col1, col2, col3 = st.columns(3)
|
| 172 |
+
|
| 173 |
+
with col1:
|
| 174 |
+
retry_count = st.number_input(
|
| 175 |
+
"重試次數",
|
| 176 |
+
value=config.get("request-retry", 3) if config else 3,
|
| 177 |
+
min_value=0,
|
| 178 |
+
max_value=10,
|
| 179 |
+
help="請求失敗時的重試次數"
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
with col2:
|
| 183 |
+
max_credentials = st.number_input(
|
| 184 |
+
"最大嘗試憑證數",
|
| 185 |
+
value=config.get("max-retry-credentials", 0) if config else 0,
|
| 186 |
+
min_value=0,
|
| 187 |
+
help="0 表示嘗試所有可用憑證"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
with col3:
|
| 191 |
+
retry_interval = st.number_input(
|
| 192 |
+
"重試間隔 (秒)",
|
| 193 |
+
value=config.get("max-retry-interval", 30) if config else 30,
|
| 194 |
+
min_value=1,
|
| 195 |
+
max_value=300,
|
| 196 |
+
help="重試之間的最大等待時間"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
st.markdown("---")
|
| 200 |
+
|
| 201 |
+
# ============================================
|
| 202 |
+
# 提供者配置
|
| 203 |
+
# ============================================
|
| 204 |
+
st.header("🤖 提供者配置")
|
| 205 |
+
|
| 206 |
+
# Claude 配置
|
| 207 |
+
with st.expander("🟠 Claude (Anthropic)"):
|
| 208 |
+
st.markdown("配置 Claude API 認證")
|
| 209 |
+
|
| 210 |
+
claude_token = st.text_input(
|
| 211 |
+
"Claude OAuth Token",
|
| 212 |
+
type="password",
|
| 213 |
+
help="從 Anthropic 獲取的 OAuth Token"
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
if st.button("保存 Claude 配置"):
|
| 217 |
+
st.success("✅ Claude 配置已保存")
|
| 218 |
+
|
| 219 |
+
# OpenAI 配置
|
| 220 |
+
with st.expander("🟢 OpenAI"):
|
| 221 |
+
st.markdown("配置 OpenAI API Key")
|
| 222 |
+
|
| 223 |
+
openai_key = st.text_input(
|
| 224 |
+
"OpenAI API Key",
|
| 225 |
+
type="password",
|
| 226 |
+
help="從 OpenAI 獲取的 API Key"
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
openai_org = st.text_input(
|
| 230 |
+
"組織 ID (可選)",
|
| 231 |
+
help="OpenAI 組織 ID"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
if st.button("保存 OpenAI 配置"):
|
| 235 |
+
st.success("✅ OpenAI 配置已保存")
|
| 236 |
+
|
| 237 |
+
# Gemini 配置
|
| 238 |
+
with st.expander("🔵 Gemini"):
|
| 239 |
+
st.markdown("配置 Google Gemini API Key")
|
| 240 |
+
|
| 241 |
+
gemini_key = st.text_input(
|
| 242 |
+
"Gemini API Key",
|
| 243 |
+
type="password",
|
| 244 |
+
help="從 Google AI Studio 獲取的 API Key"
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
if st.button("保存 Gemini 配置"):
|
| 248 |
+
st.success("✅ Gemini 配置已保存")
|
| 249 |
+
|
| 250 |
+
# Kiro 配置
|
| 251 |
+
with st.expander("🟡 Kiro (AWS CodeWhisperer)"):
|
| 252 |
+
st.markdown("配置 Kiro 認證")
|
| 253 |
+
|
| 254 |
+
kiro_config = st.selectbox(
|
| 255 |
+
"認證方式",
|
| 256 |
+
["OAuth", "AWS Builder ID", "AWS IAM", "Import Token"]
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
if kiro_config == "Import Token":
|
| 260 |
+
kiro_token = st.text_area(
|
| 261 |
+
"貼上 Kiro Token JSON",
|
| 262 |
+
height=100,
|
| 263 |
+
help="從 ~/.kiro/kiro-auth-token.json 複製"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
if st.button("保存 Kiro 配置"):
|
| 267 |
+
st.success("✅ Kiro 配置已保存")
|
| 268 |
+
|
| 269 |
+
st.markdown("---")
|
| 270 |
+
|
| 271 |
+
# ============================================
|
| 272 |
+
# 高級設置
|
| 273 |
+
# ============================================
|
| 274 |
+
st.header("⚡ 高級設置")
|
| 275 |
+
|
| 276 |
+
with st.expander("🔧 高級選項"):
|
| 277 |
+
col1, col2 = st.columns(2)
|
| 278 |
+
|
| 279 |
+
with col1:
|
| 280 |
+
commercial_mode = st.checkbox(
|
| 281 |
+
"商業模式",
|
| 282 |
+
value=config.get("commercial-mode", False) if config else False,
|
| 283 |
+
help="禁用高開銷中間件以減少內存使用"
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
with col2:
|
| 287 |
+
usage_stats = st.checkbox(
|
| 288 |
+
"使用統計",
|
| 289 |
+
value=config.get("usage-statistics-enabled", False) if config else False,
|
| 290 |
+
help="啟用內存使用統計聚合"
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
proxy_url = st.text_input(
|
| 294 |
+
"代理 URL",
|
| 295 |
+
value=config.get("proxy-url", "") if config else "",
|
| 296 |
+
placeholder="socks5://user:pass@192.168.1.1:1080/",
|
| 297 |
+
help="HTTP/HTTPS/SOCKS5 代理"
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
st.markdown("---")
|
| 301 |
+
|
| 302 |
+
# ============================================
|
| 303 |
+
# 配置導出
|
| 304 |
+
# ============================================
|
| 305 |
+
st.header("📤 配置管理")
|
| 306 |
+
|
| 307 |
+
col1, col2, col3 = st.columns(3)
|
| 308 |
+
|
| 309 |
+
with col1:
|
| 310 |
+
if st.button("📥 導出配置"):
|
| 311 |
+
if config:
|
| 312 |
+
config_yaml = yaml.dump(config, default_flow_style=False, allow_unicode=True)
|
| 313 |
+
st.download_button(
|
| 314 |
+
label="下載 config.yaml",
|
| 315 |
+
data=config_yaml,
|
| 316 |
+
file_name="config.yaml",
|
| 317 |
+
mime="text/yaml"
|
| 318 |
+
)
|
| 319 |
+
else:
|
| 320 |
+
st.error("無法獲取配置")
|
| 321 |
+
|
| 322 |
+
with col2:
|
| 323 |
+
if st.button("🔄 重載配置"):
|
| 324 |
+
st.success("✅ 配置已重新加載")
|
| 325 |
+
|
| 326 |
+
with col3:
|
| 327 |
+
if st.button("⚠️ 重置為默認"):
|
| 328 |
+
st.warning("確定要重置所有配置嗎?此操作不可恢復。")
|
| 329 |
+
if st.button("確認重置"):
|
| 330 |
+
st.success("✅ 配置已重置為默認值")
|
| 331 |
+
|
| 332 |
+
# ============================================
|
| 333 |
+
# 環境變數
|
| 334 |
+
# ============================================
|
| 335 |
+
st.markdown("---")
|
| 336 |
+
st.header("🔐 環境變數")
|
| 337 |
+
|
| 338 |
+
st.markdown("""
|
| 339 |
+
以下環境變數可以在 Hugging Face Spaces 的 Settings > Secrets 中配置:
|
| 340 |
+
|
| 341 |
+
| 變數名 | 說明 |
|
| 342 |
+
|--------|------|
|
| 343 |
+
| `API_KEYS` | API 訪問密鑰(JSON 數組) |
|
| 344 |
+
| `MANAGEMENT_KEY` | 管理介面密鑰 |
|
| 345 |
+
| `CLAUDE_TOKEN` | Claude OAuth Token |
|
| 346 |
+
| `OPENAI_API_KEY` | OpenAI API Key |
|
| 347 |
+
| `GEMINI_API_KEY` | Gemini API Key |
|
| 348 |
+
""")
|
| 349 |
+
|
| 350 |
+
st.info("💡 修改配置後需要重啟服務才能生效")
|
supervisord.conf
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
; ============================================
|
| 2 |
+
; Supervisord Configuration for Hugging Face Spaces
|
| 3 |
+
; Manages both Go API Server and Streamlit Frontend
|
| 4 |
+
; ============================================
|
| 5 |
+
|
| 6 |
+
[supervisord]
|
| 7 |
+
nodaemon=true
|
| 8 |
+
user=root
|
| 9 |
+
logfile=/var/log/supervisor/supervisord.log
|
| 10 |
+
pidfile=/var/run/supervisord.pid
|
| 11 |
+
loglevel=info
|
| 12 |
+
logfile_maxbytes=10MB
|
| 13 |
+
logfile_backups=3
|
| 14 |
+
|
| 15 |
+
; ============================================
|
| 16 |
+
; Go API Server - CLIProxyAPI Plus
|
| 17 |
+
; ============================================
|
| 18 |
+
[program:go-server]
|
| 19 |
+
command=/app/go-server/CLIProxyAPIPlus
|
| 20 |
+
directory=/app/go-server
|
| 21 |
+
autostart=true
|
| 22 |
+
autorestart=true
|
| 23 |
+
startsecs=10
|
| 24 |
+
stopwaitsecs=30
|
| 25 |
+
startretries=3
|
| 26 |
+
stopasgroup=true
|
| 27 |
+
killasgroup=true
|
| 28 |
+
stdout_logfile=/var/log/supervisor/go-server.log
|
| 29 |
+
stdout_logfile_maxbytes=10MB
|
| 30 |
+
stdout_logfile_backups=3
|
| 31 |
+
stderr_logfile=/var/log/supervisor/go-server-error.log
|
| 32 |
+
stderr_logfile_maxbytes=10MB
|
| 33 |
+
stderr_logfile_backups=3
|
| 34 |
+
environment=HOME="/root",DEPLOY="true"
|
| 35 |
+
|
| 36 |
+
; ============================================
|
| 37 |
+
; Streamlit Frontend
|
| 38 |
+
; ============================================
|
| 39 |
+
[program:streamlit]
|
| 40 |
+
command=streamlit run /app/streamlit/app.py --server.port=7860 --server.address=0.0.0.0
|
| 41 |
+
directory=/app/streamlit
|
| 42 |
+
autostart=true
|
| 43 |
+
autorestart=true
|
| 44 |
+
startsecs=5
|
| 45 |
+
stopwaitsecs=10
|
| 46 |
+
startretries=3
|
| 47 |
+
stopasgroup=true
|
| 48 |
+
killasgroup=true
|
| 49 |
+
stdout_logfile=/var/log/supervisor/streamlit.log
|
| 50 |
+
stdout_logfile_maxbytes=10MB
|
| 51 |
+
stdout_logfile_backups=3
|
| 52 |
+
stderr_logfile=/var/log/supervisor/streamlit-error.log
|
| 53 |
+
stderr_logfile_maxbytes=10MB
|
| 54 |
+
stderr_logfile_backups=3
|
| 55 |
+
environment=HOME="/root",STREAMLIT_SERVER_PORT="7860",STREAMLIT_SERVER_ADDRESS="0.0.0.0",STREAMLIT_SERVER_HEADLESS="true"
|
| 56 |
+
|
| 57 |
+
; ============================================
|
| 58 |
+
; Event Listeners
|
| 59 |
+
; ============================================
|
| 60 |
+
[eventlistener:process_exit]
|
| 61 |
+
command=bash -c "echo 'Process exited'"
|
| 62 |
+
events=PROCESS_STATE_EXITED
|
| 63 |
+
buffer_size=100
|