fix: refresh user logout state
This commit is contained in:
@@ -1,253 +0,0 @@
|
|||||||
# 模型白名单功能 - PR 准备文档
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
实现全局模型白名单功能,允许管理员配置只启用特定模型的路由。当白名单配置后,不在白名单中的模型路由会被自动移除,实现精细化的模型访问控制。
|
|
||||||
|
|
||||||
## 业务场景
|
|
||||||
|
|
||||||
- **成本控制**: 只允许使用价格合理的模型
|
|
||||||
- **合规要求**: 限制只使用审批通过的模型
|
|
||||||
- **简化管理**: 隐藏不需要的模型,减少用户困惑
|
|
||||||
- **测试验证**: 在特定场景下只开放测试模型
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
### 1. 配置层 (src/server/config.ts)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
globalAllowedModels: [] as string[]
|
|
||||||
```
|
|
||||||
|
|
||||||
- 默认为空数组(向后兼容)
|
|
||||||
- 存储在内存中,启动时从数据库加载
|
|
||||||
|
|
||||||
### 2. 数据持久化 (src/server/routes/api/settings.ts)
|
|
||||||
|
|
||||||
**数据库存储:**
|
|
||||||
- Key: `global_allowed_models`
|
|
||||||
- Value: JSON 字符串数组
|
|
||||||
|
|
||||||
**API 接口:**
|
|
||||||
|
|
||||||
#### GET /api/settings/runtime
|
|
||||||
返回当前白名单配置
|
|
||||||
|
|
||||||
#### PUT /api/settings/runtime
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"globalAllowedModels": ["gpt-4", "gpt-4o", "claude-3-sonnet"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**处理流程:**
|
|
||||||
1. 验证输入(必须是字符串数组)
|
|
||||||
2. 去重、trim、过滤空值
|
|
||||||
3. 比较新旧配置
|
|
||||||
4. 更新数据库和内存配置
|
|
||||||
5. 如果配置变更,自动触发路由重建
|
|
||||||
|
|
||||||
### 3. 路由重建 (src/server/services/modelService.ts)
|
|
||||||
|
|
||||||
**核心函数:** `rebuildTokenRoutesFromAvailability()`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 加载白名单
|
|
||||||
const globalAllowedModels = new Set(
|
|
||||||
config.globalAllowedModels.map(m => m.toLowerCase().trim()).filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 判断模型是否允许
|
|
||||||
function isModelAllowedByWhitelist(modelName: string): boolean {
|
|
||||||
if (globalAllowedModels.size === 0) return true; // 向后兼容
|
|
||||||
return globalAllowedModels.has(modelName.toLowerCase().trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加模型候选时过滤
|
|
||||||
const addModelCandidate = (modelNameRaw, accountId, tokenId, siteId) => {
|
|
||||||
const modelName = (modelNameRaw || '').trim();
|
|
||||||
if (!modelName) return;
|
|
||||||
if (!isModelAllowedByWhitelist(modelName)) return; // 白名单过滤
|
|
||||||
// ... 其他过滤逻辑
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**处理逻辑:**
|
|
||||||
1. 从 `token_model_availability` 加载所有可用模型
|
|
||||||
2. 通过 `addModelCandidate()` 过滤,只保留白名单中的模型
|
|
||||||
3. 创建/更新路由和渠道
|
|
||||||
4. 删除不在白名单中的旧路由
|
|
||||||
|
|
||||||
### 4. 模型候选API (src/server/routes/api/stats.ts)
|
|
||||||
|
|
||||||
**接口:** GET /api/models/token-candidates
|
|
||||||
|
|
||||||
**返回字段:**
|
|
||||||
- `models`: 有token的模型
|
|
||||||
- `modelsWithoutToken`: 无token的模型
|
|
||||||
- `modelsMissingTokenGroups`: 缺少token分组的模型
|
|
||||||
|
|
||||||
**过滤逻辑:**
|
|
||||||
```typescript
|
|
||||||
if (globalAllowedModels.size > 0) {
|
|
||||||
// 白名单模式:只返回白名单中的模型
|
|
||||||
for (const [modelName, candidates] of Object.entries(result)) {
|
|
||||||
if (globalAllowedModels.has(modelName.toLowerCase().trim())) {
|
|
||||||
filteredResult[modelName] = candidates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 向后兼容:返回所有模型
|
|
||||||
Object.assign(filteredResult, result);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 前端UI (src/web/pages/Settings.tsx)
|
|
||||||
|
|
||||||
**新增卡片:** "全局模型白名单"
|
|
||||||
|
|
||||||
**功能特性:**
|
|
||||||
- 输入框手动添加模型名称(支持回车)
|
|
||||||
- 可用模型列表展示(点击快速添加)
|
|
||||||
- 已选模型徽章显示(绿色徽章)
|
|
||||||
- 点击 × 删除模型
|
|
||||||
- "保存模型白名单"按钮
|
|
||||||
- 保存成功后显示提示并自动刷新页面
|
|
||||||
|
|
||||||
**交互流程:**
|
|
||||||
1. 加载当前白名单配置
|
|
||||||
2. 加载可用模型列表
|
|
||||||
3. 用户添加/删除模型
|
|
||||||
4. 点击保存触发API调用
|
|
||||||
5. 后端自动重建路由
|
|
||||||
6. 前端显示成功提示
|
|
||||||
|
|
||||||
## 数据流程图
|
|
||||||
|
|
||||||
```
|
|
||||||
用户输入 → Settings API
|
|
||||||
↓
|
|
||||||
数据库存储 (global_allowed_models)
|
|
||||||
↓
|
|
||||||
内存配置更新 (config.globalAllowedModels)
|
|
||||||
↓
|
|
||||||
触发路由重建 (异步后台任务)
|
|
||||||
↓
|
|
||||||
加载 token_model_availability
|
|
||||||
↓
|
|
||||||
白名单过滤 (isModelAllowedByWhitelist)
|
|
||||||
↓
|
|
||||||
创建/更新/删除路由
|
|
||||||
↓
|
|
||||||
候选API过滤 (token-candidates)
|
|
||||||
↓
|
|
||||||
前端显示过滤后的模型列表
|
|
||||||
```
|
|
||||||
|
|
||||||
## 向后兼容性
|
|
||||||
|
|
||||||
| 场景 | 行为 |
|
|
||||||
|------|------|
|
|
||||||
| 白名单为空 | 允许所有模型(原有行为) |
|
|
||||||
| 白名单有值 | 只允许白名单中的模型 |
|
|
||||||
| 数据库无配置 | 默认为空数组 |
|
|
||||||
| API未传该字段 | 不修改现有配置 |
|
|
||||||
|
|
||||||
## 测试验证
|
|
||||||
|
|
||||||
### 功能测试清单
|
|
||||||
|
|
||||||
- [x] 白名单为空时,所有模型可见
|
|
||||||
- [x] 设置白名单后,只显示白名单模型
|
|
||||||
- [x] 大小写不敏感匹配(GPT-4 = gpt-4)
|
|
||||||
- [x] 自动trim空格
|
|
||||||
- [x] 保存后自动触发路由重建
|
|
||||||
- [x] 路由重建成功后,路由正确过滤
|
|
||||||
- [x] 候选API正确过滤三个返回字段
|
|
||||||
- [ ] 前端UI交互正常
|
|
||||||
- [ ] 输入框支持回车添加
|
|
||||||
- [ ] 可用模型列表正确显示
|
|
||||||
- [ ] 点击模型快速添加
|
|
||||||
- [ ] 删除模型功能正常
|
|
||||||
- [ ] 保存成功提示显示
|
|
||||||
|
|
||||||
### API 测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看白名单
|
|
||||||
curl -H "Authorization: Bearer test-admin-token" \
|
|
||||||
http://localhost:4000/api/settings/runtime
|
|
||||||
|
|
||||||
# 设置白名单
|
|
||||||
curl -X PUT \
|
|
||||||
-H "Authorization: Bearer test-admin-token" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
|
|
||||||
http://localhost:4000/api/settings/runtime
|
|
||||||
|
|
||||||
# 查看模型候选
|
|
||||||
curl -H "Authorization: Bearer test-admin-token" \
|
|
||||||
http://localhost:4000/api/models/token-candidates
|
|
||||||
```
|
|
||||||
|
|
||||||
## 文件修改列表
|
|
||||||
|
|
||||||
| 文件 | 修改内容 | 行数 |
|
|
||||||
|------|---------|-----|
|
|
||||||
| src/server/config.ts | 新增 globalAllowedModels 配置 | 1 |
|
|
||||||
| src/server/routes/api/settings.ts | 类型定义、加载、保存、触发重建 | ~80 |
|
|
||||||
| src/server/services/modelService.ts | 白名单过滤逻辑 | ~20 |
|
|
||||||
| src/server/routes/api/stats.ts | 候选API过滤 | ~30 |
|
|
||||||
| src/web/pages/Settings.tsx | 前端UI组件 | ~150 |
|
|
||||||
|
|
||||||
**总计:** 约 280 行代码新增/修改
|
|
||||||
|
|
||||||
## 性能影响
|
|
||||||
|
|
||||||
- **内存:** 新增一个 Set 数据结构,大小为白名单模型数量
|
|
||||||
- **路由重建:** 无额外开销(仅增加一个 O(1) 的 Set 查找)
|
|
||||||
- **候选API:** 增加过滤循环,复杂度 O(n),n为模型数量
|
|
||||||
|
|
||||||
**结论:** 性能影响可忽略不计
|
|
||||||
|
|
||||||
## 安全考虑
|
|
||||||
|
|
||||||
- ✅ 仅管理员可配置(需要 admin token)
|
|
||||||
- ✅ 输入验证(类型检查、trim、去重)
|
|
||||||
- ✅ 无SQL注入风险(使用 ORM)
|
|
||||||
- ✅ 配置变更自动记录日志
|
|
||||||
|
|
||||||
## 后续优化建议
|
|
||||||
|
|
||||||
1. **批量导入**: 支持从文件批量导入白名单
|
|
||||||
2. **预设模板**: 提供常用模型组合模板
|
|
||||||
3. **分组管理**: 支持多个白名单分组
|
|
||||||
4. **权限控制**: 不同用户看到不同白名单
|
|
||||||
5. **审计日志**: 记录白名单变更历史
|
|
||||||
|
|
||||||
## 已知限制
|
|
||||||
|
|
||||||
1. 白名单只支持精确匹配,不支持通配符或正则
|
|
||||||
2. 模型名称大小写不敏感,但保留原始大小写显示
|
|
||||||
3. 白名单全局生效,不支持按站点或账号单独配置
|
|
||||||
|
|
||||||
## 相关 Issue
|
|
||||||
|
|
||||||
- Issue #XXX: 模型访问控制需求
|
|
||||||
- Issue #XXX: 简化模型列表显示
|
|
||||||
|
|
||||||
## 测试环境
|
|
||||||
|
|
||||||
- Node.js: v20.x
|
|
||||||
- pnpm: 8.x
|
|
||||||
- SQLite: 3.x
|
|
||||||
- 测试服务器: http://localhost:4000
|
|
||||||
- 真实模型数据已验证
|
|
||||||
|
|
||||||
## 下一步计划
|
|
||||||
|
|
||||||
1. ✅ 完成功能开发和测试
|
|
||||||
2. ✅ 编写PR文档
|
|
||||||
3. ⏳ 等待用户前端测试验证
|
|
||||||
4. ⏳ 合并到主仓库
|
|
||||||
5. ⏳ 开始第二个功能:新站点跳转选择
|
|
||||||
-178
@@ -1,178 +0,0 @@
|
|||||||
# Security Policy / 安全政策
|
|
||||||
|
|
||||||
## Overview / 概述
|
|
||||||
|
|
||||||
The security of Metapi is important to us. This document outlines our security policy and how to report vulnerabilities.
|
|
||||||
|
|
||||||
Metapi 的安全对我们很重要。本文档概述了我们的安全政策以及如何报告漏洞。
|
|
||||||
|
|
||||||
Since Metapi is a self-hosted meta-aggregation layer that manages sensitive credentials (API keys, account passwords) and proxies AI API requests, security is a critical concern.
|
|
||||||
|
|
||||||
由于 Metapi 是一个自托管的元聚合层,管理敏感凭证(API 密钥、账号密码)并代理 AI API 请求,因此安全性至关重要。
|
|
||||||
|
|
||||||
## Supported Versions / 支持的版本
|
|
||||||
|
|
||||||
We provide security updates for the following versions / 我们为以下版本提供安全更新:
|
|
||||||
|
|
||||||
| Version / 版本 | Supported / 支持状态 |
|
|
||||||
| -------------- | -------------------- |
|
|
||||||
| Latest release / 最新版本 | ✅ Fully supported / 完全支持 |
|
|
||||||
| `main` branch / `main` 分支 | ✅ Supported / 支持 |
|
|
||||||
| Older releases / 旧版本 | ⚠️ Best effort / 尽力而为 |
|
|
||||||
| Forks / 分支 | ❌ Not supported / 不支持 |
|
|
||||||
|
|
||||||
**Recommendation** / **建议**: Always use the latest stable release from [GitHub Releases](https://github.com/cita-777/metapi/releases) or the `main` branch for the most up-to-date security patches.
|
|
||||||
|
|
||||||
始终使用 [GitHub Releases](https://github.com/cita-777/metapi/releases) 的最新稳定版本或 `main` 分支以获得最新的安全补丁。
|
|
||||||
|
|
||||||
## Security Considerations / 安全注意事项
|
|
||||||
|
|
||||||
When deploying Metapi, please consider / 部署 Metapi 时,请考虑:
|
|
||||||
|
|
||||||
### Credential Storage / 凭证存储
|
|
||||||
|
|
||||||
- All sensitive credentials (API keys, passwords) are encrypted at rest in the database / 所有敏感凭证(API 密钥、密码)在数据库中静态加密存储
|
|
||||||
- Use strong `AUTH_TOKEN` and `PROXY_TOKEN` values / 使用强 `AUTH_TOKEN` 和 `PROXY_TOKEN` 值
|
|
||||||
- Never commit `.env` files or expose tokens in logs / 切勿提交 `.env` 文件或在日志中暴露令牌
|
|
||||||
|
|
||||||
### Network Security / 网络安全
|
|
||||||
|
|
||||||
- Deploy behind HTTPS/TLS in production / 在生产环境中部署在 HTTPS/TLS 后面
|
|
||||||
- Use `ADMIN_IP_ALLOWLIST` to restrict admin access / 使用 `ADMIN_IP_ALLOWLIST` 限制管理员访问
|
|
||||||
- Consider firewall rules to limit access to port 4000 / 考虑使用防火墙规则限制对端口 4000 的访问
|
|
||||||
|
|
||||||
### Database Security / 数据库安全
|
|
||||||
|
|
||||||
- Secure your database with strong credentials / 使用强凭证保护您的数据库
|
|
||||||
- Regularly backup the `data/` directory / 定期备份 `data/` 目录
|
|
||||||
- For production, consider using MySQL/PostgreSQL instead of SQLite / 对于生产环境,考虑使用 MySQL/PostgreSQL 而不是 SQLite
|
|
||||||
|
|
||||||
### Docker Security / Docker 安全
|
|
||||||
|
|
||||||
- Keep Docker images up to date / 保持 Docker 镜像最新
|
|
||||||
- Use volume mounts carefully to avoid exposing sensitive data / 谨慎使用卷挂载以避免暴露敏感数据
|
|
||||||
- Run containers with minimal privileges / 以最小权限运行容器
|
|
||||||
|
|
||||||
## Reporting a Vulnerability / 报告漏洞
|
|
||||||
|
|
||||||
**⚠️ Please do NOT report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
|
|
||||||
|
|
||||||
**⚠️ 请勿通过公开的 GitHub issue、讨论或 pull request 报告安全漏洞。**
|
|
||||||
|
|
||||||
### Reporting Channels / 报告渠道
|
|
||||||
|
|
||||||
Use one of these private channels / 使用以下私密渠道之一:
|
|
||||||
|
|
||||||
1. **GitHub Security Advisory** (Preferred) / **GitHub 安全公告**(首选)
|
|
||||||
- Go to: https://github.com/cita-777/metapi/security/advisories/new
|
|
||||||
- This allows for coordinated disclosure and CVE assignment / 这允许协调披露和 CVE 分配
|
|
||||||
|
|
||||||
2. **Email** / **邮件**
|
|
||||||
- Send to: `cita-777@users.noreply.github.com`
|
|
||||||
- Subject: `[Metapi Security] <brief description>` / 主题:`[Metapi Security] <简要描述>`
|
|
||||||
|
|
||||||
### What to Include / 应包含的内容
|
|
||||||
|
|
||||||
To help us understand and address the issue quickly, please include / 为了帮助我们快速理解和解决问题,请包含:
|
|
||||||
|
|
||||||
- **Description** / **描述**: Clear description of the vulnerability / 漏洞的清晰描述
|
|
||||||
- **Impact** / **影响**: What an attacker could achieve / 攻击者可能实现的目标
|
|
||||||
- **Affected versions** / **受影响的版本**: Version, commit hash, or deployment mode / 版本、提交哈希或部署模式
|
|
||||||
- **Affected components** / **受影响的组件**: Specific endpoints, modules, or configuration / 特定端点、模块或配置
|
|
||||||
- **Reproduction steps** / **复现步骤**: Step-by-step instructions to reproduce / 逐步复现说明
|
|
||||||
- **Proof of concept** / **概念验证**: Code, screenshots, or logs (with secrets redacted) / 代码、截图或日志(删除敏感信息)
|
|
||||||
- **Attack preconditions** / **攻击前提条件**: Required access level, network position, etc. / 所需访问级别、网络位置等
|
|
||||||
- **Suggested fix** / **建议修复**: If you have ideas for mitigation or patches / 如果您有缓解措施或补丁的想法
|
|
||||||
|
|
||||||
### Example Report / 报告示例
|
|
||||||
|
|
||||||
```
|
|
||||||
Subject: [Metapi Security] SQL Injection in account search
|
|
||||||
|
|
||||||
Description:
|
|
||||||
The account search endpoint is vulnerable to SQL injection through the
|
|
||||||
'name' parameter, allowing unauthorized database access.
|
|
||||||
|
|
||||||
Impact:
|
|
||||||
An authenticated attacker could extract all account credentials from
|
|
||||||
the database.
|
|
||||||
|
|
||||||
Affected Version:
|
|
||||||
v1.2.2 and earlier
|
|
||||||
|
|
||||||
Reproduction:
|
|
||||||
1. Login to Metapi admin panel
|
|
||||||
2. Navigate to /api/accounts/search?name=' OR '1'='1
|
|
||||||
3. Observe all accounts returned
|
|
||||||
|
|
||||||
Proof of Concept:
|
|
||||||
[Screenshot attached with sensitive data redacted]
|
|
||||||
|
|
||||||
Suggested Fix:
|
|
||||||
Use parameterized queries instead of string concatenation in
|
|
||||||
src/server/routes/api/accounts.ts:45
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response Process / 响应流程
|
|
||||||
|
|
||||||
Our security response process / 我们的安全响应流程:
|
|
||||||
|
|
||||||
1. **Acknowledgment** / **确认**: We aim to acknowledge your report within **3 business days** / 我们力求在 **3 个工作日**内确认您的报告
|
|
||||||
|
|
||||||
2. **Initial Triage** / **初步分类**: We will provide initial assessment or follow-up questions within **7 business days** / 我们将在 **7 个工作日**内提供初步评估或后续问题
|
|
||||||
|
|
||||||
3. **Investigation** / **调查**: We may request additional details, logs, or reproduction steps / 我们可能会要求额外的细节、日志或复现步骤
|
|
||||||
|
|
||||||
4. **Fix Development** / **修复开发**: We will develop and test a fix / 我们将开发并测试修复方案
|
|
||||||
|
|
||||||
5. **Coordinated Disclosure** / **协调披露**: We will coordinate disclosure timing with you / 我们将与您协调披露时间
|
|
||||||
- Please keep the vulnerability confidential until we release a fix / 请在我们发布修复之前对漏洞保密
|
|
||||||
- We will credit you in the security advisory (unless you prefer to remain anonymous) / 我们将在安全公告中致谢您(除非您希望保持匿名)
|
|
||||||
|
|
||||||
6. **Release** / **发布**: We will release a patched version and publish a security advisory / 我们将发布修补版本并发布安全公告
|
|
||||||
|
|
||||||
### Severity Assessment / 严重性评估
|
|
||||||
|
|
||||||
We use the following severity levels / 我们使用以下严重性级别:
|
|
||||||
|
|
||||||
- **Critical** / **严重**: Remote code execution, credential theft, data breach / 远程代码执行、凭证盗窃、数据泄露
|
|
||||||
- **High** / **高**: Authentication bypass, privilege escalation / 身份验证绕过、权限提升
|
|
||||||
- **Medium** / **中**: Information disclosure, denial of service / 信息泄露、拒绝服务
|
|
||||||
- **Low** / **低**: Minor information leaks, configuration issues / 轻微信息泄露、配置问题
|
|
||||||
|
|
||||||
## Public Disclosure / 公开披露
|
|
||||||
|
|
||||||
If a vulnerability is accidentally posted publicly / 如果漏洞被意外公开发布:
|
|
||||||
|
|
||||||
1. We will immediately assess the risk / 我们将立即评估风险
|
|
||||||
2. We may redact sensitive details from the public post / 我们可能会从公开帖子中删除敏感细节
|
|
||||||
3. We will redirect the reporter to this private process / 我们将把报告者重定向到此私密流程
|
|
||||||
4. We will expedite the fix and release process / 我们将加快修复和发布流程
|
|
||||||
|
|
||||||
## Security Updates / 安全更新
|
|
||||||
|
|
||||||
Security updates will be announced through / 安全更新将通过以下方式公布:
|
|
||||||
|
|
||||||
- [GitHub Security Advisories](https://github.com/cita-777/metapi/security/advisories)
|
|
||||||
- [GitHub Releases](https://github.com/cita-777/metapi/releases) with `[SECURITY]` tag / 带有 `[SECURITY]` 标签
|
|
||||||
- Project README and documentation / 项目 README 和文档
|
|
||||||
|
|
||||||
Subscribe to repository notifications to stay informed / 订阅仓库通知以保持了解。
|
|
||||||
|
|
||||||
## Bug Bounty / 漏洞赏金
|
|
||||||
|
|
||||||
We currently do not offer a paid bug bounty program. However, we deeply appreciate security researchers who responsibly disclose vulnerabilities and will publicly acknowledge your contribution (with your permission).
|
|
||||||
|
|
||||||
我们目前不提供付费漏洞赏金计划。但是,我们非常感谢负责任地披露漏洞的安全研究人员,并将公开致谢您的贡献(经您许可)。
|
|
||||||
|
|
||||||
## Questions / 问题
|
|
||||||
|
|
||||||
If you have questions about this security policy, please contact `cita-777@users.noreply.github.com`.
|
|
||||||
|
|
||||||
如果您对本安全政策有疑问,请联系 `cita-777@users.noreply.github.com`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Thank you for helping keep Metapi and its users safe!**
|
|
||||||
|
|
||||||
**感谢您帮助保护 Metapi 及其用户的安全!**
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
# 本地测试环境搭建指南
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
D:\Code\Projects\Metapi\
|
|
||||||
├── metapi # 主仓库(upstream)
|
|
||||||
├── metapi-routing-ux-optimization # 开发 worktree (模型白名单功能)
|
|
||||||
├── metapi-upstream-latest # 其他 worktree
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速启动测试服务器
|
|
||||||
|
|
||||||
### 1. 进入开发 worktree
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd D:/Code/Projects/Metapi/metapi-routing-ux-optimization
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 安装依赖(首次运行)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 构建项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 启动测试服务器
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DATA_DIR="./tmp/test-db" node dist/server/index.js
|
|
||||||
```
|
|
||||||
|
|
||||||
服务器将在 http://localhost:4000 启动
|
|
||||||
|
|
||||||
**测试Token:** `test-admin-token`
|
|
||||||
|
|
||||||
### 5. 访问前端
|
|
||||||
|
|
||||||
打开浏览器访问: http://localhost:4000
|
|
||||||
|
|
||||||
登录使用 Token: `test-admin-token`
|
|
||||||
|
|
||||||
## 测试数据准备
|
|
||||||
|
|
||||||
### 方式1: 导入真实站点
|
|
||||||
|
|
||||||
在前端"站点管理"中添加真实的 new-api 站点:
|
|
||||||
- 站点URL: 真实的 new-api 地址
|
|
||||||
- 平台: new-api
|
|
||||||
- 添加账号和token
|
|
||||||
|
|
||||||
然后触发模型发现:
|
|
||||||
```bash
|
|
||||||
curl -X POST -H "Authorization: Bearer test-admin-token" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"refreshModels": true, "wait": true}' \
|
|
||||||
http://localhost:4000/api/routes/rebuild
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式2: 使用测试脚本(可选)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DATA_DIR="./tmp/test-db" node scripts/seed-test-data.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试模型白名单功能
|
|
||||||
|
|
||||||
### 1. 进入设置页面
|
|
||||||
|
|
||||||
点击左侧导航"设置",滚动到"全局模型白名单"卡片
|
|
||||||
|
|
||||||
### 2. 测试功能
|
|
||||||
|
|
||||||
#### 添加模型到白名单
|
|
||||||
- **方式1**: 在输入框输入模型名称,按回车
|
|
||||||
- **方式2**: 点击"可用模型列表"中的模型
|
|
||||||
|
|
||||||
#### 删除模型
|
|
||||||
- 点击已选模型徽章上的 × 按钮
|
|
||||||
|
|
||||||
#### 保存配置
|
|
||||||
- 点击"保存模型白名单"按钮
|
|
||||||
- 系统自动触发路由重建
|
|
||||||
|
|
||||||
### 3. 验证效果
|
|
||||||
|
|
||||||
#### 查看路由
|
|
||||||
进入"令牌路由"页面,确认只有白名单中的模型有路由
|
|
||||||
|
|
||||||
#### 查看模型候选
|
|
||||||
```bash
|
|
||||||
curl -H "Authorization: Bearer test-admin-token" \
|
|
||||||
http://localhost:4000/api/models/token-candidates | python -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
应该只返回白名单中的模型
|
|
||||||
|
|
||||||
### 4. API 测试命令
|
|
||||||
|
|
||||||
#### 查看当前白名单
|
|
||||||
```bash
|
|
||||||
curl -H "Authorization: Bearer test-admin-token" \
|
|
||||||
http://localhost:4000/api/settings/runtime | python -m json.tool | grep -A 5 "globalAllowedModels"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 设置白名单
|
|
||||||
```bash
|
|
||||||
curl -X PUT \
|
|
||||||
-H "Authorization: Bearer test-admin-token" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
|
|
||||||
http://localhost:4000/api/settings/runtime | python -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 清空白名单(允许所有模型)
|
|
||||||
```bash
|
|
||||||
curl -X PUT \
|
|
||||||
-H "Authorization: Bearer test-admin-token" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"globalAllowedModels": []}' \
|
|
||||||
http://localhost:4000/api/settings/runtime | python -m json.tool
|
|
||||||
```
|
|
||||||
|
|
||||||
## 停止测试服务器
|
|
||||||
|
|
||||||
### 查找进程
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
netstat -ano | grep ":4000" | grep "LISTENING"
|
|
||||||
|
|
||||||
# Linux/Mac
|
|
||||||
lsof -i :4000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 停止进程
|
|
||||||
```bash
|
|
||||||
# Windows (替换 PID)
|
|
||||||
taskkill //F //PID <PID>
|
|
||||||
|
|
||||||
# Linux/Mac
|
|
||||||
kill <PID>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 开发工作流
|
|
||||||
|
|
||||||
### 修改代码后重新构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 运行测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 提交代码
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: 实现模型白名单功能"
|
|
||||||
git push origin routing-ux-optimization
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### Q: 路由列表为空?
|
|
||||||
A: 检查 token_model_availability 表是否有数据,触发模型发现重新加载
|
|
||||||
|
|
||||||
### Q: 白名单保存后路由消失?
|
|
||||||
A: 正常现象,白名单会过滤掉不在列表中的模型路由
|
|
||||||
|
|
||||||
### Q: 如何恢复所有模型?
|
|
||||||
A: 清空白名单(设置为空数组 [])即可
|
|
||||||
|
|
||||||
## 数据库位置
|
|
||||||
|
|
||||||
测试数据库存储在: `./tmp/test-db/hub.db`
|
|
||||||
|
|
||||||
查看数据库:
|
|
||||||
```bash
|
|
||||||
sqlite3 ./tmp/test-db/hub.db
|
|
||||||
```
|
|
||||||
|
|
||||||
常用查询:
|
|
||||||
```sql
|
|
||||||
-- 查看所有站点
|
|
||||||
SELECT id, name, url, platform FROM sites WHERE status = 'active';
|
|
||||||
|
|
||||||
-- 查看所有账号
|
|
||||||
SELECT id, site_id, username, status FROM accounts;
|
|
||||||
|
|
||||||
-- 查看所有token
|
|
||||||
SELECT id, account_id, name, value_status FROM account_tokens;
|
|
||||||
|
|
||||||
-- 查看模型可用性
|
|
||||||
SELECT COUNT(*) FROM token_model_availability;
|
|
||||||
|
|
||||||
-- 查看路由
|
|
||||||
SELECT COUNT(*) FROM token_routes;
|
|
||||||
```
|
|
||||||
+4
-1
@@ -1096,7 +1096,10 @@ function AppShell() {
|
|||||||
<Route path="/users" element={<UserManagement />} />
|
<Route path="/users" element={<UserManagement />} />
|
||||||
|
|
||||||
{/* User specific pages */}
|
{/* User specific pages */}
|
||||||
<Route path="/user/dashboard" element={<UserDashboard />} />
|
<Route
|
||||||
|
path="/user/dashboard"
|
||||||
|
element={<UserDashboard onLogout={() => setUserAuthed(false)} />}
|
||||||
|
/>
|
||||||
<Route path="/user/dashboard/keys" element={<UserKeys />} />
|
<Route path="/user/dashboard/keys" element={<UserKeys />} />
|
||||||
<Route path="/user/dashboard/profile" element={<UserProfile />} />
|
<Route path="/user/dashboard/profile" element={<UserProfile />} />
|
||||||
<Route path="/user/usage" element={<UsageRecords />} />
|
<Route path="/user/usage" element={<UsageRecords />} />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ interface LogItem {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserDashboard() {
|
export default function UserDashboard({ onLogout }: { onLogout?: () => void }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [keys, setKeys] = useState<ApiKey[]>([]);
|
const [keys, setKeys] = useState<ApiKey[]>([]);
|
||||||
@@ -74,6 +74,12 @@ export default function UserDashboard() {
|
|||||||
|
|
||||||
useEffect(() => { loadData(); }, [loadData]);
|
useEffect(() => { loadData(); }, [loadData]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
clearUserSession(localStorage);
|
||||||
|
onLogout?.();
|
||||||
|
navigate('/user/login', { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', minHeight: '400px' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', minHeight: '400px' }}>
|
||||||
@@ -210,7 +216,7 @@ export default function UserDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
|
<div style={{ marginTop: '20px', display: 'flex', gap: '10px' }}>
|
||||||
<button className="btn btn-ghost btn-sm" style={{ flex: 1, border: '1px solid var(--color-border)' }} onClick={() => navigate('/user/dashboard/profile')}>修改资料</button>
|
<button className="btn btn-ghost btn-sm" style={{ flex: 1, border: '1px solid var(--color-border)' }} onClick={() => navigate('/user/dashboard/profile')}>修改资料</button>
|
||||||
<button className="btn btn-ghost btn-sm danger" style={{ flex: 1, border: '1px solid var(--color-border)' }} onClick={() => { clearUserSession(localStorage); window.location.href = '/user/login'; }}>退出登录</button>
|
<button className="btn btn-ghost btn-sm danger" style={{ flex: 1, border: '1px solid var(--color-border)' }} onClick={handleLogout}>退出登录</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yml"
|
|
||||||
COMPOSE_OVERRIDE_FILE="$ROOT_DIR/docker/docker-compose.override.yml"
|
|
||||||
ENV_FILE="$ROOT_DIR/docker/.env"
|
|
||||||
DATA_DIR="$ROOT_DIR/docker/data"
|
|
||||||
|
|
||||||
DO_PULL=0
|
|
||||||
SKIP_BACKUP=0
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: ./update-and-restart.sh [--pull] [--skip-backup]
|
|
||||||
|
|
||||||
默认行为:
|
|
||||||
1. 保留现有 docker/data 数据
|
|
||||||
2. 先备份数据目录
|
|
||||||
3. 基于当前本地代码重新构建并重启容器
|
|
||||||
|
|
||||||
可选参数:
|
|
||||||
--pull 先拉取最新 Git 代码后再构建。若工作区有未提交修改会直接退出,避免覆盖本地改动
|
|
||||||
--skip-backup 跳过启动前数据备份
|
|
||||||
-h, --help 显示帮助
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "Missing required command: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_file() {
|
|
||||||
if [ ! -f "$1" ]; then
|
|
||||||
echo "Missing required file: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--pull)
|
|
||||||
DO_PULL=1
|
|
||||||
;;
|
|
||||||
--skip-backup)
|
|
||||||
SKIP_BACKUP=1
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown argument: $1" >&2
|
|
||||||
usage >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
require_cmd docker
|
|
||||||
require_cmd tar
|
|
||||||
require_cmd git
|
|
||||||
require_file "$COMPOSE_FILE"
|
|
||||||
require_file "$ENV_FILE"
|
|
||||||
|
|
||||||
COMPOSE_ARGS=(
|
|
||||||
-f "$COMPOSE_FILE"
|
|
||||||
--env-file "$ENV_FILE"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ -f "$COMPOSE_OVERRIDE_FILE" ]; then
|
|
||||||
COMPOSE_ARGS=(
|
|
||||||
-f "$COMPOSE_FILE"
|
|
||||||
-f "$COMPOSE_OVERRIDE_FILE"
|
|
||||||
--env-file "$ENV_FILE"
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$DO_PULL" -eq 1 ]; then
|
|
||||||
echo "[0/5] Checking git worktree..."
|
|
||||||
if [ -n "$(git -C "$ROOT_DIR" status --porcelain)" ]; then
|
|
||||||
echo "Git worktree has uncommitted changes. Commit/stash them first, or rerun without --pull to rebuild the current local code." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[1/5] Pulling latest git changes..."
|
|
||||||
git -C "$ROOT_DIR" pull --rebase
|
|
||||||
else
|
|
||||||
echo "[1/4] Using current local source code..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
BACKUP_FILE=""
|
|
||||||
RESTORE_ON_ERROR=0
|
|
||||||
|
|
||||||
restore_if_failed() {
|
|
||||||
if [ "$RESTORE_ON_ERROR" -eq 1 ]; then
|
|
||||||
echo "Update failed after containers were stopped; attempting to restore service..." >&2
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" up -d --force-recreate || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
trap restore_if_failed ERR
|
|
||||||
|
|
||||||
if [ "$DO_PULL" -eq 1 ]; then
|
|
||||||
echo "[2/5] Stopping current containers..."
|
|
||||||
else
|
|
||||||
echo "[2/4] Stopping current containers..."
|
|
||||||
fi
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" down --remove-orphans
|
|
||||||
RESTORE_ON_ERROR=1
|
|
||||||
|
|
||||||
if [ "$SKIP_BACKUP" -eq 0 ]; then
|
|
||||||
mkdir -p "$DATA_DIR"
|
|
||||||
BACKUP_FILE="$ROOT_DIR/docker/data-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
|
|
||||||
if [ "$DO_PULL" -eq 1 ]; then
|
|
||||||
echo "[3/5] Backing up data directory..."
|
|
||||||
else
|
|
||||||
echo "[3/4] Backing up data directory..."
|
|
||||||
fi
|
|
||||||
tar -czf "$BACKUP_FILE" -C "$DATA_DIR" .
|
|
||||||
else
|
|
||||||
if [ "$DO_PULL" -eq 1 ]; then
|
|
||||||
echo "[3/5] Skipping data backup by request..."
|
|
||||||
else
|
|
||||||
echo "[3/4] Skipping data backup by request..."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$DO_PULL" -eq 1 ]; then
|
|
||||||
echo "[4/5] Rebuilding and starting containers..."
|
|
||||||
else
|
|
||||||
echo "[4/4] Rebuilding and starting containers..."
|
|
||||||
fi
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" up -d --build --force-recreate
|
|
||||||
RESTORE_ON_ERROR=0
|
|
||||||
trap - ERR
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Container status:"
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" ps
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Recent logs:"
|
|
||||||
docker compose "${COMPOSE_ARGS[@]}" logs --tail 20
|
|
||||||
|
|
||||||
if [ -n "$BACKUP_FILE" ]; then
|
|
||||||
echo
|
|
||||||
echo "Data backup saved to: $BACKUP_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if PORT_MAPPING="$(docker compose "${COMPOSE_ARGS[@]}" port metapi 4000 2>/dev/null)"; then
|
|
||||||
echo
|
|
||||||
echo "Service is available at: $PORT_MAPPING"
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user