mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
优化: 替换第三方CDN资源以提升加载速度
- 将所有第三方资源从 bootcdn 迁移到 cdnjs.cloudflare.com - 移除 SRI 完整性校验以避免哈希值不匹配问题 - 添加 DNS 预取和预连接以加速资源加载 - 调整脚本加载顺序,确保依赖关系正确 - 保持所有库版本号不变 (Vue 3.3.4, Element Plus 2.4.4, Chart.js 4.4.0) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
24
.github/workflows/auto-release.yml
vendored
24
.github/workflows/auto-release.yml
vendored
@@ -127,18 +127,32 @@ jobs:
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Update VERSION file
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
# 更新 VERSION 文件
|
||||
echo "${{ steps.next_version.outputs.new_version }}" > VERSION
|
||||
|
||||
# 检查是否有更改
|
||||
if git diff --quiet VERSION; then
|
||||
echo "VERSION file already up to date"
|
||||
else
|
||||
git add VERSION
|
||||
echo "Updated VERSION file to ${{ steps.next_version.outputs.new_version }}"
|
||||
fi
|
||||
|
||||
- name: Update CHANGELOG.md
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
# 生成完整的 CHANGELOG
|
||||
git cliff --config .github/cliff.toml --output CHANGELOG.md
|
||||
|
||||
# 提交 CHANGELOG 更新
|
||||
if git diff --quiet CHANGELOG.md; then
|
||||
echo "No changes to CHANGELOG.md"
|
||||
# 提交 CHANGELOG 和 VERSION 更新
|
||||
if git diff --quiet CHANGELOG.md VERSION; then
|
||||
echo "No changes to CHANGELOG.md or VERSION"
|
||||
else
|
||||
git add CHANGELOG.md
|
||||
git commit -m "chore: update CHANGELOG.md for ${{ steps.next_version.outputs.new_tag }} [skip ci]"
|
||||
git add CHANGELOG.md VERSION
|
||||
git commit -m "chore: update CHANGELOG.md and VERSION for ${{ steps.next_version.outputs.new_tag }} [skip ci]"
|
||||
git push origin main
|
||||
fi
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ pnpm-debug.log*
|
||||
data/
|
||||
!data/.gitkeep
|
||||
|
||||
# Redis data directory
|
||||
redis_data/
|
||||
|
||||
# Logs directory
|
||||
logs/
|
||||
*.log
|
||||
|
||||
@@ -36,9 +36,8 @@ services:
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro
|
||||
command: redis-server /usr/local/etc/redis/redis.conf
|
||||
- ./redis_data:/data
|
||||
command: redis-server --save 60 1 --appendonly yes --appendfsync everysec
|
||||
networks:
|
||||
- claude-relay-network
|
||||
healthcheck:
|
||||
@@ -104,8 +103,6 @@ services:
|
||||
- monitoring
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
driver: local
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Claude Code Headers 动态管理功能
|
||||
|
||||
## 概述
|
||||
|
||||
该功能自动捕获和管理不同 Claude 账号使用的 Claude Code 客户端 headers,实现版本动态跟踪和避免风控。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **自动捕获**: 从 `/api` 网关的成功请求中自动捕获 Claude Code headers
|
||||
2. **版本管理**: 根据 user-agent 中的版本号智能更新,只保留最新版本
|
||||
3. **账号隔离**: 每个 Claude 账号独立存储 headers,避免版本混用
|
||||
4. **智能降级**: OpenAI 转发时优先使用捕获的 headers,无数据时使用默认值
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 1. Headers 捕获(claudeRelayService.js)
|
||||
- 在请求成功(200/201)后自动捕获客户端 headers
|
||||
- 提取 Claude Code 特定的 headers(x-stainless-*, x-app, user-agent 等)
|
||||
- 根据版本号决定是否更新存储
|
||||
|
||||
### 2. Headers 存储(Redis)
|
||||
- Key: `claude_code_headers:{accountId}`
|
||||
- 数据结构:
|
||||
```json
|
||||
{
|
||||
"headers": {
|
||||
"x-stainless-retry-count": "0",
|
||||
"x-stainless-timeout": "60",
|
||||
"x-stainless-lang": "js",
|
||||
"x-stainless-package-version": "0.55.1",
|
||||
"x-stainless-os": "Windows",
|
||||
"x-stainless-arch": "x64",
|
||||
"x-stainless-runtime": "node",
|
||||
"x-stainless-runtime-version": "v20.19.2",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"x-app": "cli",
|
||||
"user-agent": "claude-cli/1.0.57 (external, cli)",
|
||||
"accept-language": "*",
|
||||
"sec-fetch-mode": "cors"
|
||||
},
|
||||
"version": "1.0.57",
|
||||
"updatedAt": "2025-01-22T10:00:00.000Z"
|
||||
}
|
||||
```
|
||||
- TTL: 7天自动过期
|
||||
|
||||
### 3. Headers 使用(openaiClaudeRoutes.js)
|
||||
- OpenAI 格式转发时,根据选定的 Claude 账号获取对应的 headers
|
||||
- 自动添加完整的 beta headers 以支持 Claude Code 功能
|
||||
|
||||
## API 端点
|
||||
|
||||
### 查看所有账号的 headers
|
||||
```
|
||||
GET /admin/claude-code-headers
|
||||
```
|
||||
|
||||
响应示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"accountId": "account_123",
|
||||
"accountName": "Claude Account 1",
|
||||
"version": "1.0.57",
|
||||
"userAgent": "claude-cli/1.0.57 (external, cli)",
|
||||
"updatedAt": "2025-01-22T10:00:00.000Z",
|
||||
"headers": { ... }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 清除账号的 headers
|
||||
```
|
||||
DELETE /admin/claude-code-headers/:accountId
|
||||
```
|
||||
|
||||
## 默认 Headers
|
||||
|
||||
当账号没有捕获到 headers 时,使用以下默认值:
|
||||
- claude-cli/1.0.57 (external, cli)
|
||||
- x-stainless-package-version: 0.55.1
|
||||
- 其他必要的 Claude Code headers
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **版本一致性**: 确保同一账号使用相同版本的 headers,避免触发风控
|
||||
2. **自动更新**: 系统会自动使用更高版本的 headers 更新存储
|
||||
3. **Beta Headers**: OpenAI 转发时自动添加必要的 beta headers:
|
||||
- oauth-2025-04-20
|
||||
- claude-code-20250219
|
||||
- interleaved-thinking-2025-05-14
|
||||
- fine-grained-tool-streaming-2025-05-14
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Headers 未被捕获
|
||||
- 检查请求是否成功(200/201 状态码)
|
||||
- 确认请求包含有效的 user-agent(含 claude-cli)
|
||||
|
||||
### 版本未更新
|
||||
- 系统只接受更高版本的 headers
|
||||
- 检查新版本号是否确实高于当前存储版本
|
||||
|
||||
### OpenAI 转发仍报错
|
||||
- 检查 beta headers 是否正确配置
|
||||
- 确认账号已有存储的 headers 或默认值可用
|
||||
27
src/app.js
27
src/app.js
@@ -122,10 +122,35 @@ class Application {
|
||||
]);
|
||||
|
||||
const memory = process.memoryUsage();
|
||||
|
||||
// 获取版本号:优先使用环境变量,其次VERSION文件,再次package.json,最后使用默认值
|
||||
let version = process.env.APP_VERSION || process.env.VERSION;
|
||||
if (!version) {
|
||||
try {
|
||||
// 尝试从VERSION文件读取
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const versionFile = path.join(__dirname, '..', 'VERSION');
|
||||
if (fs.existsSync(versionFile)) {
|
||||
version = fs.readFileSync(versionFile, 'utf8').trim();
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略错误,继续尝试其他方式
|
||||
}
|
||||
}
|
||||
if (!version) {
|
||||
try {
|
||||
const packageJson = require('../package.json');
|
||||
version = packageJson.version;
|
||||
} catch (error) {
|
||||
version = '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
service: 'claude-relay-service',
|
||||
version: '1.0.0',
|
||||
version: version,
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
memory: {
|
||||
|
||||
@@ -463,9 +463,9 @@ const securityMiddleware = (req, res, next) => {
|
||||
if (req.path.startsWith('/web') || req.path === '/') {
|
||||
res.setHeader('Content-Security-Policy', [
|
||||
'default-src \'self\'',
|
||||
'script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' https://unpkg.com https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net',
|
||||
'style-src \'self\' \'unsafe-inline\' https://cdn.tailwindcss.com https://cdnjs.cloudflare.com',
|
||||
'font-src \'self\' https://cdnjs.cloudflare.com',
|
||||
'script-src \'self\' \'unsafe-inline\' \'unsafe-eval\' https://unpkg.com https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://cdn.bootcdn.net',
|
||||
'style-src \'self\' \'unsafe-inline\' https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://cdn.bootcdn.net',
|
||||
'font-src \'self\' https://cdnjs.cloudflare.com https://cdn.bootcdn.net',
|
||||
'img-src \'self\' data:',
|
||||
'connect-src \'self\'',
|
||||
'frame-ancestors \'none\'',
|
||||
|
||||
@@ -9,6 +9,9 @@ const oauthHelper = require('../utils/oauthHelper');
|
||||
const CostCalculator = require('../utils/costCalculator');
|
||||
const pricingService = require('../services/pricingService');
|
||||
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService');
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -1607,4 +1610,188 @@ router.delete('/claude-code-headers/:accountId', authenticateAdmin, async (req,
|
||||
}
|
||||
});
|
||||
|
||||
// 🔄 版本检查
|
||||
router.get('/check-updates', authenticateAdmin, async (req, res) => {
|
||||
// 读取当前版本
|
||||
const versionPath = path.join(__dirname, '../../VERSION');
|
||||
let currentVersion = '1.0.0';
|
||||
try {
|
||||
currentVersion = fs.readFileSync(versionPath, 'utf8').trim();
|
||||
} catch (err) {
|
||||
logger.warn('⚠️ Could not read VERSION file:', err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// 从缓存获取
|
||||
const cacheKey = 'version_check_cache';
|
||||
const cached = await redis.getClient().get(cacheKey);
|
||||
|
||||
if (cached && !req.query.force) {
|
||||
const cachedData = JSON.parse(cached);
|
||||
const cacheAge = Date.now() - cachedData.timestamp;
|
||||
|
||||
// 缓存有效期1小时
|
||||
if (cacheAge < 3600000) {
|
||||
// 实时计算 hasUpdate,不使用缓存的值
|
||||
const hasUpdate = compareVersions(currentVersion, cachedData.latest) < 0;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: cachedData.latest,
|
||||
hasUpdate: hasUpdate, // 实时计算,不用缓存
|
||||
releaseInfo: cachedData.releaseInfo,
|
||||
cached: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 请求 GitHub API
|
||||
const githubRepo = 'wei-shaw/claude-relay-service';
|
||||
const response = await axios.get(
|
||||
`https://api.github.com/repos/${githubRepo}/releases/latest`,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'Claude-Relay-Service'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
const release = response.data;
|
||||
const latestVersion = release.tag_name.replace(/^v/, '');
|
||||
|
||||
// 比较版本
|
||||
const hasUpdate = compareVersions(currentVersion, latestVersion) < 0;
|
||||
|
||||
const releaseInfo = {
|
||||
name: release.name,
|
||||
body: release.body,
|
||||
publishedAt: release.published_at,
|
||||
htmlUrl: release.html_url
|
||||
};
|
||||
|
||||
// 缓存结果(不缓存 hasUpdate,因为它应该实时计算)
|
||||
await redis.getClient().set(cacheKey, JSON.stringify({
|
||||
latest: latestVersion,
|
||||
releaseInfo,
|
||||
timestamp: Date.now()
|
||||
}), 'EX', 3600); // 1小时过期
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: latestVersion,
|
||||
hasUpdate,
|
||||
releaseInfo,
|
||||
cached: false
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// 改进错误日志记录
|
||||
const errorDetails = {
|
||||
message: error.message || 'Unknown error',
|
||||
code: error.code,
|
||||
response: error.response ? {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
data: error.response.data
|
||||
} : null,
|
||||
request: error.request ? 'Request was made but no response received' : null
|
||||
};
|
||||
|
||||
logger.error('❌ Failed to check for updates:', errorDetails.message);
|
||||
|
||||
// 处理 404 错误 - 仓库或版本不存在
|
||||
if (error.response && error.response.status === 404) {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: currentVersion,
|
||||
hasUpdate: false,
|
||||
releaseInfo: {
|
||||
name: 'No releases found',
|
||||
body: 'The GitHub repository has no releases yet.',
|
||||
publishedAt: new Date().toISOString(),
|
||||
htmlUrl: '#'
|
||||
},
|
||||
warning: 'GitHub repository has no releases'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 如果是网络错误,尝试返回缓存的数据
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
|
||||
const cacheKey = 'version_check_cache';
|
||||
const cached = await redis.getClient().get(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
const cachedData = JSON.parse(cached);
|
||||
// 实时计算 hasUpdate
|
||||
const hasUpdate = compareVersions(currentVersion, cachedData.latest) < 0;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: cachedData.latest,
|
||||
hasUpdate: hasUpdate, // 实时计算
|
||||
releaseInfo: cachedData.releaseInfo,
|
||||
cached: true,
|
||||
warning: 'Using cached data due to network error'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 其他错误返回当前版本信息
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
current: currentVersion,
|
||||
latest: currentVersion,
|
||||
hasUpdate: false,
|
||||
releaseInfo: {
|
||||
name: 'Update check failed',
|
||||
body: `Unable to check for updates: ${error.message || 'Unknown error'}`,
|
||||
publishedAt: new Date().toISOString(),
|
||||
htmlUrl: '#'
|
||||
},
|
||||
error: true,
|
||||
warning: error.message || 'Failed to check for updates'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 版本比较函数
|
||||
function compareVersions(current, latest) {
|
||||
const parseVersion = (v) => {
|
||||
const parts = v.split('.').map(Number);
|
||||
return {
|
||||
major: parts[0] || 0,
|
||||
minor: parts[1] || 0,
|
||||
patch: parts[2] || 0
|
||||
};
|
||||
};
|
||||
|
||||
const currentV = parseVersion(current);
|
||||
const latestV = parseVersion(latest);
|
||||
|
||||
if (currentV.major !== latestV.major) {
|
||||
return currentV.major - latestV.major;
|
||||
}
|
||||
if (currentV.minor !== latestV.minor) {
|
||||
return currentV.minor - latestV.minor;
|
||||
}
|
||||
return currentV.patch - latestV.patch;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
145
web/admin/app.js
145
web/admin/app.js
@@ -255,6 +255,20 @@ const app = createApp({
|
||||
cancelText: '取消',
|
||||
onConfirm: null,
|
||||
onCancel: null
|
||||
},
|
||||
|
||||
// 版本管理相关
|
||||
versionInfo: {
|
||||
current: '', // 当前版本
|
||||
latest: '', // 最新版本
|
||||
hasUpdate: false, // 是否有更新
|
||||
checkingUpdate: false, // 正在检查更新
|
||||
lastChecked: null, // 上次检查时间
|
||||
releaseInfo: null, // 最新版本的发布信息
|
||||
githubRepo: 'wei-shaw/claude-relay-service', // GitHub仓库
|
||||
showReleaseNotes: false, // 是否显示发布说明
|
||||
autoCheckInterval: null, // 自动检查定时器
|
||||
noUpdateMessage: false // 显示"已是最新版"提醒
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -295,6 +309,9 @@ const app = createApp({
|
||||
// 加载当前用户信息
|
||||
this.loadCurrentUser();
|
||||
|
||||
// 加载版本信息
|
||||
this.loadCurrentVersion();
|
||||
|
||||
// 初始化日期筛选器和图表数据
|
||||
this.initializeDateFilter();
|
||||
|
||||
@@ -321,6 +338,10 @@ const app = createApp({
|
||||
|
||||
beforeUnmount() {
|
||||
this.cleanupCharts();
|
||||
// 清理版本检查定时器
|
||||
if (this.versionInfo.autoCheckInterval) {
|
||||
clearInterval(this.versionInfo.autoCheckInterval);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -1326,6 +1347,130 @@ const app = createApp({
|
||||
}
|
||||
},
|
||||
|
||||
// 版本管理相关方法
|
||||
async loadCurrentVersion() {
|
||||
try {
|
||||
const response = await fetch('/health');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.version) {
|
||||
// 从健康检查端点获取当前版本
|
||||
this.versionInfo.current = data.version;
|
||||
|
||||
// 检查更新
|
||||
await this.checkForUpdates();
|
||||
|
||||
// 设置自动检查更新(每小时检查一次)
|
||||
this.versionInfo.autoCheckInterval = setInterval(() => {
|
||||
this.checkForUpdates();
|
||||
}, 3600000); // 1小时
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading current version:', error);
|
||||
this.versionInfo.current = '未知';
|
||||
}
|
||||
},
|
||||
|
||||
async checkForUpdates() {
|
||||
if (this.versionInfo.checkingUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.versionInfo.checkingUpdate = true;
|
||||
|
||||
try {
|
||||
// 使用后端接口检查更新
|
||||
const response = await fetch('/admin/check-updates', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const data = result.data;
|
||||
|
||||
this.versionInfo.current = data.current;
|
||||
this.versionInfo.latest = data.latest;
|
||||
this.versionInfo.hasUpdate = data.hasUpdate;
|
||||
this.versionInfo.releaseInfo = data.releaseInfo;
|
||||
this.versionInfo.lastChecked = new Date();
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('versionInfo', JSON.stringify({
|
||||
current: data.current,
|
||||
latest: data.latest,
|
||||
lastChecked: this.versionInfo.lastChecked,
|
||||
hasUpdate: data.hasUpdate,
|
||||
releaseInfo: data.releaseInfo
|
||||
}));
|
||||
|
||||
// 如果没有更新,显示提醒
|
||||
if (!data.hasUpdate) {
|
||||
this.versionInfo.noUpdateMessage = true;
|
||||
// 3秒后自动隐藏提醒
|
||||
setTimeout(() => {
|
||||
this.versionInfo.noUpdateMessage = false;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
if (data.cached && data.warning) {
|
||||
console.warn('Version check warning:', data.warning);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
|
||||
// 尝试从localStorage读取缓存的版本信息
|
||||
const cached = localStorage.getItem('versionInfo');
|
||||
if (cached) {
|
||||
const cachedInfo = JSON.parse(cached);
|
||||
this.versionInfo.current = cachedInfo.current || this.versionInfo.current;
|
||||
this.versionInfo.latest = cachedInfo.latest;
|
||||
this.versionInfo.hasUpdate = cachedInfo.hasUpdate;
|
||||
this.versionInfo.releaseInfo = cachedInfo.releaseInfo;
|
||||
this.versionInfo.lastChecked = new Date(cachedInfo.lastChecked);
|
||||
}
|
||||
} finally {
|
||||
this.versionInfo.checkingUpdate = false;
|
||||
}
|
||||
},
|
||||
|
||||
compareVersions(current, latest) {
|
||||
// 比较语义化版本号
|
||||
const parseVersion = (v) => {
|
||||
const parts = v.split('.').map(Number);
|
||||
return {
|
||||
major: parts[0] || 0,
|
||||
minor: parts[1] || 0,
|
||||
patch: parts[2] || 0
|
||||
};
|
||||
};
|
||||
|
||||
const currentV = parseVersion(current);
|
||||
const latestV = parseVersion(latest);
|
||||
|
||||
if (currentV.major !== latestV.major) {
|
||||
return currentV.major - latestV.major;
|
||||
}
|
||||
if (currentV.minor !== latestV.minor) {
|
||||
return currentV.minor - latestV.minor;
|
||||
}
|
||||
return currentV.patch - latestV.patch;
|
||||
},
|
||||
|
||||
formatVersionDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
// 用户菜单相关方法
|
||||
openChangePasswordModal() {
|
||||
this.userMenuOpen = false;
|
||||
|
||||
@@ -4,16 +4,33 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude Relay Service - 管理后台</title>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
|
||||
<!-- 预连接到CDN域名,加速资源加载 -->
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
|
||||
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
||||
|
||||
<!-- 使用更快的CDN资源,保持版本一致 -->
|
||||
<!-- Vue 3.3.4 (必须先加载,不使用defer) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.3.4/vue.global.prod.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<!-- Element Plus -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/index.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/index.full.min.js"></script>
|
||||
|
||||
<!-- Chart.js 4.4.0 (独立库,可以延迟加载) -->
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Element Plus 2.4.4 (依赖Vue,所以在Vue之后加载) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/index.min.css" crossorigin="anonymous">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/index.full.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Element Plus 中文语言包 -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/locale/zh-cn.min.js"></script>
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-plus/2.4.4/locale/zh-cn.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Font Awesome 6.5.1 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<link rel="stylesheet" href="/web/style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -79,7 +96,24 @@
|
||||
<i class="fas fa-cloud text-xl text-gray-700"></i>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center min-h-[48px]">
|
||||
<h1 class="text-2xl font-bold text-white header-title leading-tight">Claude Relay Service</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-2xl font-bold text-white header-title leading-tight">Claude Relay Service</h1>
|
||||
<!-- 版本信息 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400 font-mono">v{{ versionInfo.current || '...' }}</span>
|
||||
<!-- 更新提示 -->
|
||||
<a
|
||||
v-if="versionInfo.hasUpdate"
|
||||
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 bg-green-500 border border-green-600 rounded-full text-xs text-white hover:bg-green-600 transition-colors animate-pulse"
|
||||
title="有新版本可用"
|
||||
>
|
||||
<i class="fas fa-arrow-up text-[10px]"></i>
|
||||
<span>新版本</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-tight mt-0.5">管理后台</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,10 +131,54 @@
|
||||
<!-- 悬浮菜单 -->
|
||||
<div
|
||||
v-if="userMenuOpen"
|
||||
class="absolute right-0 top-full mt-2 w-48 bg-white rounded-xl shadow-xl border border-gray-200 py-2 user-menu-dropdown"
|
||||
class="absolute right-0 top-full mt-2 w-56 bg-white rounded-xl shadow-xl border border-gray-200 py-2 user-menu-dropdown"
|
||||
style="z-index: 999999;"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 版本信息 -->
|
||||
<div class="px-4 py-3 border-b border-gray-100">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500">当前版本</span>
|
||||
<span class="font-mono text-gray-700">v{{ versionInfo.current || '...' }}</span>
|
||||
</div>
|
||||
<div v-if="versionInfo.hasUpdate" class="mt-2">
|
||||
<div class="flex items-center justify-between text-sm mb-2">
|
||||
<span class="text-green-600 font-medium">
|
||||
<i class="fas fa-arrow-up mr-1"></i>有新版本
|
||||
</span>
|
||||
<span class="font-mono text-green-600">v{{ versionInfo.latest }}</span>
|
||||
</div>
|
||||
<a
|
||||
:href="versionInfo.releaseInfo?.htmlUrl || '#'"
|
||||
target="_blank"
|
||||
class="block w-full text-center px-3 py-1.5 bg-green-500 text-white text-sm rounded-lg hover:bg-green-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-external-link-alt mr-1"></i>查看更新
|
||||
</a>
|
||||
</div>
|
||||
<div v-else-if="versionInfo.checkingUpdate" class="mt-2 text-center text-xs text-gray-500">
|
||||
<i class="fas fa-spinner fa-spin mr-1"></i>检查更新中...
|
||||
</div>
|
||||
<div v-else class="mt-2 text-center">
|
||||
<!-- 已是最新版提醒 -->
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="versionInfo.noUpdateMessage" key="message" class="px-3 py-1.5 bg-green-100 border border-green-200 rounded-lg inline-block">
|
||||
<p class="text-xs text-green-700 font-medium">
|
||||
<i class="fas fa-check-circle mr-1"></i>当前已是最新版本
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
key="button"
|
||||
@click="checkForUpdates()"
|
||||
class="text-xs text-blue-500 hover:text-blue-700 transition-colors"
|
||||
>
|
||||
<i class="fas fa-sync-alt mr-1"></i>检查更新
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="openChangePasswordModal"
|
||||
class="w-full px-4 py-3 text-left text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-3"
|
||||
|
||||
@@ -433,4 +433,30 @@ body::before {
|
||||
.modal-scroll-content {
|
||||
max-height: calc(85vh - 120px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 版本更新提醒动画 */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 用户菜单下拉框优化 */
|
||||
.user-menu-dropdown {
|
||||
min-width: 240px;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
Reference in New Issue
Block a user