新增热更新后台管理系统
This commit is contained in:
Binary file not shown.
3242
extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/admin-backend/package-lock.json
generated
Normal file
3242
extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension/admin-backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "cocos-ecs-hotupdate-admin",
|
||||
"version": "1.0.0",
|
||||
"description": "Cocos ECS Extension Hot Update Management Backend",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"migrate": "ts-node src/database/migrate.ts",
|
||||
"simple": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"cors": "^2.8.5",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"adm-zip": "^0.5.10",
|
||||
"crypto": "^1.0.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"node-cron": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node-cron": "^3.0.8",
|
||||
"typescript": "^5.1.6",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cocos ECS Extension - 热更新管理后台</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/element-plus@2.4.4/dist/index.css" rel="stylesheet">
|
||||
<style>
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||
.header { background: #409eff; color: white; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.container { max-width: 1200px; margin: 20px auto; padding: 0 20px; }
|
||||
.card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
||||
.upload-area {
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.upload-area:hover { border-color: #409eff; background: #f0f8ff; }
|
||||
.version-list { max-height: 400px; overflow-y: auto; }
|
||||
.version-item {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.version-info h4 { margin: 0 0 8px 0; color: #303133; }
|
||||
.version-info p { margin: 0; color: #909399; font-size: 14px; }
|
||||
.status-tag {
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status-active { background: #f0f9ff; color: #409eff; }
|
||||
.status-draft { background: #fdf6ec; color: #e6a23c; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
|
||||
.stat-card {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.stat-number { font-size: 32px; font-weight: bold; margin-bottom: 8px; }
|
||||
.stat-label { font-size: 14px; opacity: 0.9; }
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-primary { background: #409eff; color: white; }
|
||||
.btn-primary:hover { background: #337ecc; }
|
||||
.btn-danger { background: #f56c6c; color: white; }
|
||||
.btn-danger:hover { background: #dd6161; }
|
||||
.btn-success { background: #67c23a; color: white; }
|
||||
.btn-success:hover { background: #5daf34; }
|
||||
.plugin-card:hover { border-color: #409eff; background: #f0f8ff; }
|
||||
.plugin-active { border-color: #409eff !important; background: #f0f8ff !important; }
|
||||
.filtered-out { opacity: 0.3; }
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn.btn-default {
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.btn.btn-default:hover {
|
||||
color: #409eff;
|
||||
border-color: #c6e2ff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="header">
|
||||
<h1 style="margin: 0; font-size: 24px;">🚀 Cocos ECS Extension - 热更新管理后台</h1>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- 插件选择器 -->
|
||||
<div class="card">
|
||||
<div style="padding: 20px;">
|
||||
<h2>🔌 插件管理</h2>
|
||||
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
||||
<div
|
||||
v-for="plugin in plugins"
|
||||
:key="plugin.id"
|
||||
@click="selectPlugin(plugin.id)"
|
||||
:class="'plugin-card ' + (currentPlugin === plugin.id ? 'plugin-active' : '')"
|
||||
style="flex: 1; min-width: 250px; padding: 20px; border: 2px solid #e4e7ed; border-radius: 8px; cursor: pointer; transition: all 0.3s;"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="font-size: 32px;">{{ plugin.icon }}</span>
|
||||
<div style="flex: 1;">
|
||||
<h3 style="margin: 0; color: #303133;">{{ plugin.displayName }}</h3>
|
||||
<p style="margin: 4px 0 0 0; color: #909399; font-size: 14px;">{{ plugin.description || '暂无描述' }}</p>
|
||||
<div style="margin: 8px 0 0 0; display: flex; gap: 16px; font-size: 12px;">
|
||||
<span style="color: #67c23a;">{{ plugin.status === 'active' ? '✅ 活跃' : '⏸️ 暂停' }}</span>
|
||||
<span style="color: #409eff;">📦 版本: {{ plugin.versionCount || 0 }}</span>
|
||||
<span style="color: #e6a23c;">📥 下载: {{ plugin.totalDownloads || 0 }}</span>
|
||||
</div>
|
||||
<p style="margin: 4px 0 0 0; color: #c0c4cc; font-size: 12px;">{{ plugin.author || '未知作者' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<div class="card">
|
||||
<div style="padding: 20px;">
|
||||
<h2>📊 系统统计</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.totalVersions }}</div>
|
||||
<div class="stat-label">总版本数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.activeUsers }}</div>
|
||||
<div class="stat-label">活跃用户</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.todayUpdates }}</div>
|
||||
<div class="stat-label">今日更新</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ stats.successRate }}%</div>
|
||||
<div class="stat-label">成功率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传新版本 -->
|
||||
<div class="card">
|
||||
<div style="padding: 20px;">
|
||||
<h2>📦 上传新版本</h2>
|
||||
<div class="upload-area" @click="selectFile">
|
||||
<div v-if="!uploading">
|
||||
<i style="font-size: 48px; color: #c0c4cc;">📁</i>
|
||||
<p style="margin: 16px 0 0 0; color: #606266;">点击或拖拽文件到此处上传插件包</p>
|
||||
<p style="margin: 8px 0 0 0; color: #909399; font-size: 14px;">支持 .zip 格式,最大 100MB</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<i style="font-size: 48px; color: #409eff;">⏳</i>
|
||||
<p style="margin: 16px 0 0 0; color: #409eff;">上传中... {{ uploadProgress }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" ref="fileInput" @change="handleFileSelect" accept=".zip" style="display: none;">
|
||||
|
||||
<div v-if="selectedFile" style="margin-top: 20px; padding: 16px; background: #f5f7fa; border-radius: 4px;">
|
||||
<h4 style="margin: 0 0 12px 0;">版本信息</h4>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
||||
<div>
|
||||
<label>目标插件:</label>
|
||||
<select v-model="versionInfo.pluginId" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
<option v-for="plugin in plugins" :key="plugin.id" :value="plugin.id">
|
||||
{{ plugin.icon }} {{ plugin.displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>版本号:</label>
|
||||
<input v-model="versionInfo.version" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
</div>
|
||||
<div>
|
||||
<label>发布渠道:</label>
|
||||
<select v-model="versionInfo.channel" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
<option value="stable">稳定版</option>
|
||||
<option value="beta">测试版</option>
|
||||
<option value="dev">开发版</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 16px;">
|
||||
<label>更新说明:</label>
|
||||
<textarea v-model="versionInfo.description" rows="3" style="width: 100%; padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;"></textarea>
|
||||
</div>
|
||||
<div style="margin-top: 16px;">
|
||||
<label>
|
||||
<input type="checkbox" v-model="versionInfo.mandatory"> 强制更新
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top: 16px;">
|
||||
<button class="btn btn-primary" @click="uploadVersion" :disabled="uploading">
|
||||
{{ uploading ? '上传中...' : '发布版本' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 版本管理 -->
|
||||
<div class="card">
|
||||
<div style="padding: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2>📋 版本管理 - {{ getCurrentPluginName() }}</h2>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<button class="btn btn-primary" @click="showCreatePlugin = true">+ 创建新项目</button>
|
||||
<select v-model="currentPlugin" @change="filterVersions" style="padding: 8px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
<option value="">全部插件</option>
|
||||
<option v-for="plugin in plugins" :key="plugin.id" :value="plugin.id">
|
||||
{{ plugin.icon }} {{ plugin.displayName }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-success" @click="refreshVersions">刷新列表</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="version-list">
|
||||
<div v-for="version in filteredVersions" :key="version.id" class="version-item">
|
||||
<div class="version-info">
|
||||
<h4>
|
||||
<span style="margin-right: 8px;">{{ getPluginIcon(version.pluginId) }}</span>
|
||||
{{ version.version }} ({{ getChannelName(version.channel) }})
|
||||
<span style="font-size: 12px; color: #909399; margin-left: 8px;">- {{ getPluginDisplayName(version.pluginId) }}</span>
|
||||
</h4>
|
||||
<p>{{ version.description }}</p>
|
||||
<p>发布时间: {{ formatDate(version.release_date) }} | 文件大小: {{ formatSize(version.file_size) }}</p>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span :class="'status-tag status-' + version.status">
|
||||
{{ getStatusName(version.status) }}
|
||||
</span>
|
||||
<button class="btn btn-primary" @click="editVersion(version)">编辑</button>
|
||||
<button class="btn btn-danger" @click="deleteVersion(version)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredVersions.length === 0" style="text-align: center; padding: 40px; color: #909399;">
|
||||
{{ currentPlugin ? '该插件暂无版本数据' : '暂无版本数据' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建项目模态框 -->
|
||||
<div v-if="showCreatePlugin" class="modal-overlay" @click="showCreatePlugin = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>🎯 创建新项目</h3>
|
||||
<button @click="showCreatePlugin = false" class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>项目ID *</label>
|
||||
<input v-model="newPlugin.id" placeholder="例如: my-awesome-plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
<small>项目ID必须是唯一的,建议使用小写字母和短横线</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>项目名称 *</label>
|
||||
<input v-model="newPlugin.name" placeholder="例如: my-awesome-plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>显示名称 *</label>
|
||||
<input v-model="newPlugin.displayName" placeholder="例如: My Awesome Plugin" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>项目描述</label>
|
||||
<textarea v-model="newPlugin.description" rows="3" placeholder="描述你的项目功能和特点..." style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>作者</label>
|
||||
<input v-model="newPlugin.author" placeholder="例如: Your Name" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>仓库地址</label>
|
||||
<input v-model="newPlugin.repository" placeholder="例如: https://github.com/user/repo" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>图标 (Emoji)</label>
|
||||
<input v-model="newPlugin.icon" placeholder="📦" maxlength="2" style="width: 100%; padding: 12px; border: 1px solid #dcdfe6; border-radius: 4px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="showCreatePlugin = false" class="btn btn-default">取消</button>
|
||||
<button @click="createPlugin" class="btn btn-primary" :disabled="creatingPlugin">
|
||||
{{ creatingPlugin ? '创建中...' : '创建项目' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.8/dist/vue.global.js"></script>
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
uploading: false,
|
||||
uploadProgress: 0,
|
||||
selectedFile: null,
|
||||
currentPlugin: 'cocos-ecs-extension', // 当前选中的插件
|
||||
showCreatePlugin: false,
|
||||
creatingPlugin: false,
|
||||
newPlugin: {
|
||||
id: '',
|
||||
name: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
author: '',
|
||||
repository: '',
|
||||
icon: '📦'
|
||||
},
|
||||
plugins: [],
|
||||
versionInfo: {
|
||||
pluginId: 'cocos-ecs-extension',
|
||||
version: '',
|
||||
channel: 'stable',
|
||||
description: '',
|
||||
mandatory: false
|
||||
},
|
||||
stats: {
|
||||
totalVersions: 0,
|
||||
activeUsers: 0,
|
||||
todayUpdates: 0,
|
||||
successRate: 100.0
|
||||
},
|
||||
versions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredVersions() {
|
||||
if (!this.currentPlugin) {
|
||||
return this.versions;
|
||||
}
|
||||
return this.versions.filter(v => v.pluginId === this.currentPlugin);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectPlugin(pluginId) {
|
||||
this.currentPlugin = pluginId;
|
||||
this.versionInfo.pluginId = pluginId;
|
||||
},
|
||||
filterVersions() {
|
||||
// 触发计算属性重新计算
|
||||
},
|
||||
getCurrentPluginName() {
|
||||
if (!this.currentPlugin) return '全部插件';
|
||||
const plugin = this.plugins.find(p => p.id === this.currentPlugin);
|
||||
return plugin ? plugin.displayName : '未知插件';
|
||||
},
|
||||
getPluginIcon(pluginId) {
|
||||
const plugin = this.plugins.find(p => p.id === pluginId);
|
||||
return plugin ? plugin.icon : '📦';
|
||||
},
|
||||
getPluginDisplayName(pluginId) {
|
||||
const plugin = this.plugins.find(p => p.id === pluginId);
|
||||
return plugin ? plugin.displayName : pluginId;
|
||||
},
|
||||
selectFile() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.selectedFile = file;
|
||||
// 自动提取版本号(如果文件名包含版本信息)
|
||||
const match = file.name.match(/(\d+\.\d+\.\d+)/);
|
||||
if (match) {
|
||||
this.versionInfo.version = match[1];
|
||||
}
|
||||
}
|
||||
},
|
||||
async uploadVersion() {
|
||||
if (!this.selectedFile) return;
|
||||
|
||||
this.uploading = true;
|
||||
this.uploadProgress = 0;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('package', this.selectedFile);
|
||||
formData.append('pluginId', this.versionInfo.pluginId);
|
||||
formData.append('version', this.versionInfo.version);
|
||||
formData.append('channel', this.versionInfo.channel);
|
||||
formData.append('description', this.versionInfo.description);
|
||||
formData.append('mandatory', this.versionInfo.mandatory.toString());
|
||||
|
||||
// 模拟上传进度
|
||||
const progressInterval = setInterval(() => {
|
||||
this.uploadProgress = Math.min(this.uploadProgress + Math.random() * 15, 90);
|
||||
}, 200);
|
||||
|
||||
// 调用真实的上传API
|
||||
const response = await fetch('/api/upload/package', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
this.uploadProgress = 100;
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(`版本上传成功!\n插件: ${result.data.pluginId}\n版本: ${result.data.version}\n文件数: ${result.data.filesCount}`);
|
||||
|
||||
// 重置表单
|
||||
this.selectedFile = null;
|
||||
this.versionInfo = {
|
||||
pluginId: this.currentPlugin,
|
||||
version: '',
|
||||
channel: 'stable',
|
||||
description: '',
|
||||
mandatory: false
|
||||
};
|
||||
this.$refs.fileInput.value = '';
|
||||
|
||||
// 刷新数据
|
||||
await this.refreshVersions();
|
||||
await this.refreshStats();
|
||||
} else {
|
||||
alert('上传失败: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('上传失败: ' + error.message);
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
}
|
||||
},
|
||||
async refreshVersions() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.currentPlugin) {
|
||||
params.set('pluginId', this.currentPlugin);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/versions?${params}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.versions = result.data.map(v => ({
|
||||
id: v.id,
|
||||
pluginId: v.pluginId,
|
||||
version: v.version,
|
||||
channel: v.channel,
|
||||
description: v.description,
|
||||
release_date: v.releaseDate,
|
||||
file_size: v.fileSize,
|
||||
status: v.status
|
||||
}));
|
||||
} else {
|
||||
console.error('获取版本列表失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新版本列表失败:', error);
|
||||
}
|
||||
},
|
||||
async refreshStats() {
|
||||
try {
|
||||
const response = await fetch('/api/stats');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.stats = result.data;
|
||||
} else {
|
||||
console.error('获取统计数据失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新统计数据失败:', error);
|
||||
}
|
||||
},
|
||||
editVersion(version) {
|
||||
// 编辑版本
|
||||
console.log('编辑版本:', version);
|
||||
},
|
||||
deleteVersion(version) {
|
||||
if (confirm('确定要删除版本 ' + version.version + ' 吗?')) {
|
||||
// 删除版本
|
||||
console.log('删除版本:', version);
|
||||
}
|
||||
},
|
||||
getChannelName(channel) {
|
||||
const names = { stable: '稳定版', beta: '测试版', dev: '开发版' };
|
||||
return names[channel] || channel;
|
||||
},
|
||||
getStatusName(status) {
|
||||
const names = { active: '已发布', draft: '草稿' };
|
||||
return names[status] || status;
|
||||
},
|
||||
formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleString('zh-CN');
|
||||
},
|
||||
formatSize(bytes) {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return size.toFixed(1) + ' ' + units[unitIndex];
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 加载插件列表
|
||||
await this.loadPlugins();
|
||||
|
||||
// 加载初始数据
|
||||
await this.refreshStats();
|
||||
await this.refreshVersions();
|
||||
},
|
||||
async loadPlugins() {
|
||||
try {
|
||||
const response = await fetch('/api/plugins');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.plugins = result.data.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
description: p.description,
|
||||
author: p.author,
|
||||
repository: p.repository,
|
||||
icon: p.icon,
|
||||
status: p.status,
|
||||
versionCount: p.versionCount || 0,
|
||||
totalDownloads: p.totalDownloads || 0
|
||||
}));
|
||||
|
||||
// 如果没有选中插件且有插件数据,自动选中第一个
|
||||
if (!this.currentPlugin && this.plugins.length > 0) {
|
||||
this.currentPlugin = this.plugins[0].id;
|
||||
this.versionInfo.pluginId = this.plugins[0].id;
|
||||
}
|
||||
} else {
|
||||
console.error('获取插件列表失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载插件列表失败:', error);
|
||||
}
|
||||
},
|
||||
async createPlugin() {
|
||||
if (!this.newPlugin.id || !this.newPlugin.name || !this.newPlugin.displayName) {
|
||||
alert('请填写必要的字段:项目ID、项目名称、显示名称');
|
||||
return;
|
||||
}
|
||||
|
||||
this.creatingPlugin = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/plugins', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.newPlugin)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert(`项目创建成功!\n项目名称: ${result.data.displayName}\n项目ID: ${result.data.id}`);
|
||||
|
||||
// 重置表单
|
||||
this.newPlugin = {
|
||||
id: '',
|
||||
name: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
author: '',
|
||||
repository: '',
|
||||
icon: '📦'
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
this.showCreatePlugin = false;
|
||||
|
||||
// 刷新插件列表
|
||||
await this.loadPlugins();
|
||||
await this.refreshStats();
|
||||
} else {
|
||||
alert('创建失败: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('创建失败: ' + error.message);
|
||||
} finally {
|
||||
this.creatingPlugin = false;
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,283 @@
|
||||
import * as sqlite3 from 'sqlite3';
|
||||
import { promisify } from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
// 数据库实例
|
||||
export let db: sqlite3.Database;
|
||||
|
||||
/**
|
||||
* 插件信息接口
|
||||
*/
|
||||
export interface PluginInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
author: string;
|
||||
repository: string;
|
||||
icon: string;
|
||||
status: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
*/
|
||||
export async function initDatabase(): Promise<void> {
|
||||
const dbPath = path.join(__dirname, '../../data/hotupdate.db');
|
||||
|
||||
// 确保数据目录存在
|
||||
await fs.ensureDir(path.dirname(dbPath));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db = new sqlite3.Database(dbPath, (err: any) => {
|
||||
if (err) {
|
||||
console.error('数据库连接失败:', err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ 数据库连接成功');
|
||||
createTables().then(resolve).catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据表
|
||||
*/
|
||||
async function createTables(): Promise<void> {
|
||||
const run = promisify(db.run.bind(db));
|
||||
|
||||
try {
|
||||
// 插件信息表
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS plugins (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
author TEXT,
|
||||
repository TEXT,
|
||||
icon TEXT,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// 版本信息表 - 添加plugin_id字段
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plugin_id TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
channel TEXT NOT NULL DEFAULT 'stable',
|
||||
release_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT,
|
||||
download_url TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
mandatory BOOLEAN DEFAULT 0,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE,
|
||||
UNIQUE(plugin_id, version, channel)
|
||||
)
|
||||
`);
|
||||
|
||||
// 版本文件表
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS version_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version_id INTEGER NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_hash TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
action TEXT NOT NULL DEFAULT 'update',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (version_id) REFERENCES versions (id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// 用户表
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT DEFAULT 'admin',
|
||||
last_login DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// 更新统计表 - 添加plugin_id字段
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS update_stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
plugin_id TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
user_count INTEGER DEFAULT 0,
|
||||
success_count INTEGER DEFAULT 0,
|
||||
failure_count INTEGER DEFAULT 0,
|
||||
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (plugin_id) REFERENCES plugins (id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// 配置表
|
||||
await run(`
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('✅ 数据表创建成功');
|
||||
|
||||
// 插入默认数据
|
||||
await insertDefaultData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据表创建失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入默认数据
|
||||
*/
|
||||
async function insertDefaultData(): Promise<void> {
|
||||
const run = promisify(db.run.bind(db)) as (sql: string, params?: any[]) => Promise<any>;
|
||||
|
||||
// 插入默认插件
|
||||
const defaultPlugins: PluginInfo[] = [
|
||||
{
|
||||
id: 'cocos-ecs-extension',
|
||||
name: 'cocos-ecs-extension',
|
||||
displayName: 'Cocos ECS Framework',
|
||||
description: '为Cocos Creator提供高性能ECS框架支持',
|
||||
author: 'esengine',
|
||||
repository: 'https://github.com/esengine/ecs-framework',
|
||||
icon: '🎮',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'behaviour-tree-ai',
|
||||
name: 'behaviour-tree-ai',
|
||||
displayName: 'Behaviour Tree AI',
|
||||
description: '智能行为树AI系统,支持可视化编辑',
|
||||
author: 'esengine',
|
||||
repository: 'https://github.com/esengine/ecs-framework/tree/master/thirdparty/BehaviourTree-ai',
|
||||
icon: '🧠',
|
||||
status: 'active'
|
||||
}
|
||||
];
|
||||
|
||||
for (const plugin of defaultPlugins) {
|
||||
try {
|
||||
await run(
|
||||
`INSERT OR IGNORE INTO plugins
|
||||
(id, name, display_name, description, author, repository, icon, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[plugin.id, plugin.name, plugin.displayName, plugin.description,
|
||||
plugin.author, plugin.repository, plugin.icon, plugin.status]
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('插件数据插入失败:', plugin.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 插入默认配置
|
||||
const defaultConfigs = [
|
||||
{ key: 'server_url', value: 'http://localhost:3001', description: '服务器地址' },
|
||||
{ key: 'cdn_url', value: 'https://cdn.earthonline-game.cn', description: 'CDN地址' },
|
||||
{ key: 'auto_backup', value: 'true', description: '自动备份' },
|
||||
{ key: 'max_backup_count', value: '5', description: '最大备份数量' },
|
||||
{ key: 'upload_max_size', value: '100', description: '上传文件最大大小(MB)' }
|
||||
];
|
||||
|
||||
for (const config of defaultConfigs) {
|
||||
try {
|
||||
await run(
|
||||
'INSERT OR IGNORE INTO config (key, value, description) VALUES (?, ?, ?)',
|
||||
[config.key, config.value, config.description]
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('配置插入失败:', config.key);
|
||||
}
|
||||
}
|
||||
|
||||
// 插入默认管理员用户
|
||||
const bcrypt = require('bcryptjs');
|
||||
const defaultPassword = 'admin123';
|
||||
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
|
||||
|
||||
try {
|
||||
await run(
|
||||
'INSERT OR IGNORE INTO users (username, password_hash, role) VALUES (?, ?, ?)',
|
||||
['admin', hashedPassword, 'admin']
|
||||
);
|
||||
console.log('✅ 默认管理员账户创建成功 (用户名: admin, 密码: admin123)');
|
||||
} catch (error) {
|
||||
console.warn('默认用户创建失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 执行查询
|
||||
*/
|
||||
export function query(sql: string, params: any[] = []): Promise<any[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, params, (err: any, rows: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个查询
|
||||
*/
|
||||
export function queryOne(sql: string, params: any[] = []): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, params, (err: any, row: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行更新/插入
|
||||
*/
|
||||
export function execute(sql: string, params: any[] = []): Promise<{ lastID: number; changes: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, function(err: any) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ lastID: (this as any).lastID, changes: (this as any).changes });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库连接
|
||||
*/
|
||||
export function closeDatabase(): void {
|
||||
db.close();
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { query, execute } from '../database';
|
||||
|
||||
export const authRoutes = Router();
|
||||
|
||||
// JWT密钥 - 生产环境应该从环境变量读取
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
const JWT_EXPIRES_IN = '24h';
|
||||
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface AuthRequest extends Request {
|
||||
user?: {
|
||||
id: number;
|
||||
username: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
authRoutes.post('/login', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { username, password }: LoginRequest = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '用户名和密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const users = await query(`
|
||||
SELECT id, username, password_hash, role
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
`, [username]);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: JWT_EXPIRES_IN }
|
||||
);
|
||||
|
||||
// 更新最后登录时间
|
||||
await execute(`
|
||||
UPDATE users
|
||||
SET last_login = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [user.id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '登录失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 验证token
|
||||
*/
|
||||
authRoutes.get('/verify', authenticateToken, (req: AuthRequest, res: Response) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: req.user
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 登出 (清除客户端token即可)
|
||||
*/
|
||||
authRoutes.post('/logout', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登出成功'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
authRoutes.post('/change-password', authenticateToken, async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '当前密码和新密码不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '新密码长度不能少于6位'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
const users = await query(`
|
||||
SELECT password_hash FROM users WHERE id = ?
|
||||
`, [req.user!.id]);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, users[0].password_hash);
|
||||
|
||||
if (!isCurrentPasswordValid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: '当前密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
const saltRounds = 10;
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, saltRounds);
|
||||
|
||||
// 更新密码
|
||||
await execute(`
|
||||
UPDATE users
|
||||
SET password_hash = ?
|
||||
WHERE id = ?
|
||||
`, [hashedNewPassword, req.user!.id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码修改成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '修改密码失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* JWT Token验证中间件
|
||||
*/
|
||||
export function authenticateToken(req: AuthRequest, res: Response, next: any) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: '访问令牌缺失'
|
||||
});
|
||||
}
|
||||
|
||||
jwt.verify(token, JWT_SECRET, (err: any, user: any) => {
|
||||
if (err) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: '访问令牌无效或已过期'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query, execute } from '../database';
|
||||
|
||||
export const configRoutes = Router();
|
||||
|
||||
/**
|
||||
* 获取所有配置
|
||||
*/
|
||||
configRoutes.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const configs = await query(`
|
||||
SELECT key, value, description, updated_at
|
||||
FROM config
|
||||
ORDER BY key
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: configs
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取配置失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取特定配置
|
||||
*/
|
||||
configRoutes.get('/:key', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
|
||||
const config = await query(`
|
||||
SELECT key, value, description, updated_at
|
||||
FROM config
|
||||
WHERE key = ?
|
||||
`, [key]);
|
||||
|
||||
if (config.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '配置不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: config[0]
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取配置失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
configRoutes.put('/:key', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const { value, description } = req.body;
|
||||
|
||||
if (value === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '缺少配置值'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查配置是否存在
|
||||
const existing = await query('SELECT key FROM config WHERE key = ?', [key]);
|
||||
|
||||
if (existing.length === 0) {
|
||||
// 创建新配置
|
||||
await execute(`
|
||||
INSERT INTO config (key, value, description, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`, [key, value, description || '']);
|
||||
} else {
|
||||
// 更新现有配置
|
||||
await execute(`
|
||||
UPDATE config
|
||||
SET value = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = ?
|
||||
`, [value, description || '', key]);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '配置更新成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '更新配置失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*/
|
||||
configRoutes.delete('/:key', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
|
||||
await execute('DELETE FROM config WHERE key = ?', [key]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '配置删除成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '删除配置失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 批量更新配置
|
||||
*/
|
||||
configRoutes.post('/batch', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { configs } = req.body;
|
||||
|
||||
if (!Array.isArray(configs)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '配置数据格式错误'
|
||||
});
|
||||
}
|
||||
|
||||
for (const config of configs) {
|
||||
const { key, value, description } = config;
|
||||
|
||||
if (!key || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查配置是否存在
|
||||
const existing = await query('SELECT key FROM config WHERE key = ?', [key]);
|
||||
|
||||
if (existing.length === 0) {
|
||||
await execute(`
|
||||
INSERT INTO config (key, value, description, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`, [key, value, description || '']);
|
||||
} else {
|
||||
await execute(`
|
||||
UPDATE config
|
||||
SET value = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = ?
|
||||
`, [value, description || '', key]);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '批量配置更新成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量更新配置失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '批量更新配置失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,302 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query, queryOne, execute } from '../database';
|
||||
|
||||
export const pluginRoutes = Router();
|
||||
|
||||
/**
|
||||
* 获取插件列表
|
||||
*/
|
||||
pluginRoutes.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const plugins = await query(`
|
||||
SELECT
|
||||
p.*,
|
||||
COUNT(DISTINCT v.id) as version_count,
|
||||
COALESCE(SUM(us.user_count), 0) as total_downloads,
|
||||
MAX(v.created_at) as latest_version_date
|
||||
FROM plugins p
|
||||
LEFT JOIN versions v ON p.id = v.plugin_id
|
||||
LEFT JOIN update_stats us ON p.id = us.plugin_id
|
||||
GROUP BY p.id
|
||||
ORDER BY p.display_name
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: plugins.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
displayName: p.display_name,
|
||||
description: p.description,
|
||||
author: p.author,
|
||||
repository: p.repository,
|
||||
icon: p.icon,
|
||||
status: p.status,
|
||||
versionCount: p.version_count,
|
||||
totalDownloads: p.total_downloads,
|
||||
latestVersionDate: p.latest_version_date,
|
||||
createdAt: p.created_at
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取插件列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取插件列表失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取特定插件信息
|
||||
*/
|
||||
pluginRoutes.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const plugin = await queryOne(`
|
||||
SELECT
|
||||
p.*,
|
||||
COUNT(DISTINCT v.id) as version_count,
|
||||
COALESCE(SUM(us.user_count), 0) as total_downloads,
|
||||
MAX(v.created_at) as latest_version_date
|
||||
FROM plugins p
|
||||
LEFT JOIN versions v ON p.id = v.plugin_id
|
||||
LEFT JOIN update_stats us ON p.id = us.plugin_id
|
||||
WHERE p.id = ?
|
||||
GROUP BY p.id
|
||||
`, [id]);
|
||||
|
||||
if (!plugin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '插件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
displayName: plugin.display_name,
|
||||
description: plugin.description,
|
||||
author: plugin.author,
|
||||
repository: plugin.repository,
|
||||
icon: plugin.icon,
|
||||
status: plugin.status,
|
||||
versionCount: plugin.version_count,
|
||||
totalDownloads: plugin.total_downloads,
|
||||
latestVersionDate: plugin.latest_version_date,
|
||||
createdAt: plugin.created_at
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取插件信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取插件信息失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建新插件
|
||||
*/
|
||||
pluginRoutes.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id, name, displayName, description, author, repository, icon } = req.body;
|
||||
|
||||
if (!id || !name || !displayName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '缺少必要参数: id, name, displayName'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查插件ID是否已存在
|
||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
||||
|
||||
if (existingPlugin) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '插件ID已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建插件记录
|
||||
await execute(`
|
||||
INSERT INTO plugins
|
||||
(id, name, display_name, description, author, repository, icon)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, [id, name, displayName, description || '', author || '', repository || '', icon || '📦']);
|
||||
|
||||
console.log(`✅ 新插件创建成功: ${displayName} (${id})`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id,
|
||||
name,
|
||||
displayName,
|
||||
description: description || '',
|
||||
author: author || '',
|
||||
repository: repository || '',
|
||||
icon: icon || '📦',
|
||||
status: 'active'
|
||||
},
|
||||
message: '插件创建成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建插件失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '创建插件失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新插件信息
|
||||
*/
|
||||
pluginRoutes.put('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { displayName, description, author, repository, icon, status } = req.body;
|
||||
|
||||
// 检查插件是否存在
|
||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
||||
|
||||
if (!existingPlugin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '插件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新插件信息
|
||||
await execute(`
|
||||
UPDATE plugins
|
||||
SET display_name = ?, description = ?, author = ?, repository = ?, icon = ?, status = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`, [displayName, description, author, repository, icon, status, new Date().toISOString(), id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '插件信息更新成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新插件失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '更新插件失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除插件(软删除)
|
||||
*/
|
||||
pluginRoutes.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查插件是否存在
|
||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
||||
|
||||
if (!existingPlugin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '插件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有版本记录
|
||||
const versionCount = await queryOne('SELECT COUNT(*) as count FROM versions WHERE plugin_id = ?', [id]);
|
||||
|
||||
if (versionCount && versionCount.count > 0) {
|
||||
// 如果有版本记录,只进行软删除
|
||||
await execute(`
|
||||
UPDATE plugins
|
||||
SET status = 'inactive', updated_at = ?
|
||||
WHERE id = ?
|
||||
`, [new Date().toISOString(), id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '插件已停用(因为存在版本记录)'
|
||||
});
|
||||
} else {
|
||||
// 如果没有版本记录,可以直接删除
|
||||
await execute('DELETE FROM plugins WHERE id = ?', [id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '插件删除成功'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('删除插件失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '删除插件失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 激活/停用插件
|
||||
*/
|
||||
pluginRoutes.patch('/:id/status', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!['active', 'inactive'].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '无效的状态值,必须是 active 或 inactive'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查插件是否存在
|
||||
const existingPlugin = await queryOne('SELECT id FROM plugins WHERE id = ?', [id]);
|
||||
|
||||
if (!existingPlugin) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '插件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
await execute(`
|
||||
UPDATE plugins
|
||||
SET status = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`, [status, new Date().toISOString(), id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `插件已${status === 'active' ? '激活' : '停用'}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新插件状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '更新插件状态失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query, queryOne, execute } from '../database';
|
||||
|
||||
export const statsRoutes = Router();
|
||||
|
||||
/**
|
||||
* 获取整体统计信息
|
||||
*/
|
||||
statsRoutes.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// 插件数量统计
|
||||
const pluginStats = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_plugins,
|
||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_plugins,
|
||||
COUNT(CASE WHEN status = 'inactive' THEN 1 END) as inactive_plugins
|
||||
FROM plugins
|
||||
`);
|
||||
|
||||
// 版本数量统计
|
||||
const versionStats = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_versions,
|
||||
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_versions,
|
||||
COUNT(CASE WHEN channel = 'stable' THEN 1 END) as stable_versions,
|
||||
COUNT(CASE WHEN channel = 'beta' THEN 1 END) as beta_versions,
|
||||
COUNT(CASE WHEN mandatory = 1 THEN 1 END) as mandatory_versions
|
||||
FROM versions
|
||||
`);
|
||||
|
||||
// 文件数量统计
|
||||
const fileStats = await query(`
|
||||
SELECT
|
||||
COUNT(*) as total_files,
|
||||
SUM(file_size) as total_size,
|
||||
AVG(file_size) as avg_file_size
|
||||
FROM version_files
|
||||
`);
|
||||
|
||||
// 更新统计
|
||||
const updateStats = await query(`
|
||||
SELECT
|
||||
SUM(user_count) as total_users,
|
||||
SUM(success_count) as total_success,
|
||||
SUM(failure_count) as total_failure
|
||||
FROM update_stats
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
plugins: pluginStats[0] || {},
|
||||
versions: versionStats[0] || {},
|
||||
files: fileStats[0] || {},
|
||||
updates: updateStats[0] || {}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取统计信息失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取插件使用统计
|
||||
*/
|
||||
statsRoutes.get('/plugins', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { limit = 10 } = req.query;
|
||||
|
||||
const pluginStats = await query(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.display_name,
|
||||
p.icon,
|
||||
COUNT(v.id) as version_count,
|
||||
COALESCE(SUM(us.user_count), 0) as total_users,
|
||||
COALESCE(SUM(us.success_count), 0) as total_success,
|
||||
COALESCE(SUM(us.failure_count), 0) as total_failure,
|
||||
MAX(v.created_at) as latest_version_date
|
||||
FROM plugins p
|
||||
LEFT JOIN versions v ON p.id = v.plugin_id AND v.status = 'active'
|
||||
LEFT JOIN update_stats us ON p.id = us.plugin_id
|
||||
WHERE p.status = 'active'
|
||||
GROUP BY p.id, p.display_name, p.icon
|
||||
ORDER BY total_users DESC
|
||||
LIMIT ?
|
||||
`, [Number(limit)]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: pluginStats
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取插件统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取插件统计失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取版本使用统计
|
||||
*/
|
||||
statsRoutes.get('/versions', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { pluginId, days = 30 } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT
|
||||
v.id,
|
||||
v.version,
|
||||
v.channel,
|
||||
v.release_date,
|
||||
p.display_name as plugin_name,
|
||||
COALESCE(us.user_count, 0) as user_count,
|
||||
COALESCE(us.success_count, 0) as success_count,
|
||||
COALESCE(us.failure_count, 0) as failure_count,
|
||||
CASE
|
||||
WHEN us.user_count > 0
|
||||
THEN ROUND((us.success_count * 1.0 / us.user_count) * 100, 2)
|
||||
ELSE 0
|
||||
END as success_rate
|
||||
FROM versions v
|
||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
||||
LEFT JOIN update_stats us ON v.plugin_id = us.plugin_id AND v.version = us.version
|
||||
WHERE v.status = 'active'
|
||||
AND v.release_date >= date('now', '-' || ? || ' days')
|
||||
`;
|
||||
const params: any[] = [Number(days)];
|
||||
|
||||
if (pluginId) {
|
||||
sql += ' AND v.plugin_id = ?';
|
||||
params.push(pluginId);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY v.release_date DESC';
|
||||
|
||||
const versionStats = await query(sql, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: versionStats
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取版本统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取版本统计失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取趋势统计
|
||||
*/
|
||||
statsRoutes.get('/trends', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { period = 'week' } = req.query;
|
||||
|
||||
let dateFormat = '%Y-%m-%d';
|
||||
let dateInterval = '7 days';
|
||||
|
||||
if (period === 'month') {
|
||||
dateFormat = '%Y-%m';
|
||||
dateInterval = '30 days';
|
||||
} else if (period === 'year') {
|
||||
dateFormat = '%Y';
|
||||
dateInterval = '365 days';
|
||||
}
|
||||
|
||||
// 版本发布趋势
|
||||
const versionTrends = await query(`
|
||||
SELECT
|
||||
strftime(?, release_date) as period,
|
||||
COUNT(*) as version_count,
|
||||
COUNT(CASE WHEN channel = 'stable' THEN 1 END) as stable_count,
|
||||
COUNT(CASE WHEN channel = 'beta' THEN 1 END) as beta_count
|
||||
FROM versions
|
||||
WHERE release_date >= date('now', '-' || ? || '')
|
||||
AND status = 'active'
|
||||
GROUP BY strftime(?, release_date)
|
||||
ORDER BY period DESC
|
||||
LIMIT 20
|
||||
`, [dateFormat, dateInterval, dateFormat]);
|
||||
|
||||
// 更新使用趋势
|
||||
const updateTrends = await query(`
|
||||
SELECT
|
||||
strftime(?, last_updated) as period,
|
||||
SUM(user_count) as total_users,
|
||||
SUM(success_count) as total_success,
|
||||
SUM(failure_count) as total_failure
|
||||
FROM update_stats
|
||||
WHERE last_updated >= date('now', '-' || ? || '')
|
||||
GROUP BY strftime(?, last_updated)
|
||||
ORDER BY period DESC
|
||||
LIMIT 20
|
||||
`, [dateFormat, dateInterval, dateFormat]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
versions: versionTrends,
|
||||
updates: updateTrends
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取趋势统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取趋势统计失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 记录更新成功
|
||||
*/
|
||||
statsRoutes.post('/success', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { pluginId, version } = req.body;
|
||||
|
||||
if (!pluginId || !version) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '缺少必要参数: pluginId, version'
|
||||
});
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE update_stats
|
||||
SET success_count = success_count + 1, last_updated = ?
|
||||
WHERE plugin_id = ? AND version = ?
|
||||
`, [new Date().toISOString(), pluginId, version]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新成功记录已保存'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('记录更新成功失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '记录更新成功失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 记录更新失败
|
||||
*/
|
||||
statsRoutes.post('/failure', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { pluginId, version, error: errorMessage } = req.body;
|
||||
|
||||
if (!pluginId || !version) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '缺少必要参数: pluginId, version'
|
||||
});
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE update_stats
|
||||
SET failure_count = failure_count + 1, last_updated = ?
|
||||
WHERE plugin_id = ? AND version = ?
|
||||
`, [new Date().toISOString(), pluginId, version]);
|
||||
|
||||
console.log(`❌ 更新失败记录: ${pluginId} v${version} - ${errorMessage}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新失败记录已保存'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('记录更新失败失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '记录更新失败失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import * as multer from 'multer';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as crypto from 'crypto';
|
||||
import * as AdmZip from 'adm-zip';
|
||||
import { execute, query } from '../database';
|
||||
|
||||
interface MulterRequest extends Request {
|
||||
file?: any;
|
||||
}
|
||||
|
||||
export const uploadRoutes = Router();
|
||||
|
||||
// 配置multer用于文件上传
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const uploadDir = path.join(__dirname, '../../uploads/packages');
|
||||
await fs.ensureDir(uploadDir);
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
// 生成唯一文件名
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 100 * 1024 * 1024 // 100MB限制
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype === 'application/zip' || path.extname(file.originalname).toLowerCase() === '.zip') {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('只允许上传ZIP文件'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 上传插件包
|
||||
*/
|
||||
uploadRoutes.post('/package', upload.single('package'), async (req: MulterRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '未选择文件'
|
||||
});
|
||||
}
|
||||
|
||||
const { pluginId, version, channel, description, mandatory } = req.body;
|
||||
|
||||
if (!pluginId || !version || !channel) {
|
||||
// 删除已上传的文件
|
||||
await fs.remove(req.file.path);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '缺少必要参数: pluginId, version, channel'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📦 上传插件包: ${pluginId} v${version} (${channel})`);
|
||||
|
||||
// 计算文件哈希
|
||||
const fileHash = await calculateFileHash(req.file.path);
|
||||
|
||||
// 分析ZIP文件内容
|
||||
const zipAnalysis = await analyzeZipFile(req.file.path);
|
||||
|
||||
// 生成下载URL
|
||||
const downloadUrl = `/uploads/packages/${req.file.filename}`;
|
||||
|
||||
// 创建版本记录
|
||||
const result = await execute(`
|
||||
INSERT INTO versions
|
||||
(plugin_id, version, channel, description, download_url, file_size, checksum, mandatory, release_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
pluginId,
|
||||
version,
|
||||
channel,
|
||||
description || '',
|
||||
downloadUrl,
|
||||
req.file.size,
|
||||
fileHash,
|
||||
mandatory === 'true' ? 1 : 0,
|
||||
new Date().toISOString()
|
||||
]);
|
||||
|
||||
const versionId = result.lastID;
|
||||
|
||||
// 添加文件记录
|
||||
for (const file of zipAnalysis.files) {
|
||||
await execute(`
|
||||
INSERT INTO version_files (version_id, file_path, file_hash, file_size, action)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [versionId, file.path, file.hash, file.size, 'update']);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: versionId,
|
||||
pluginId,
|
||||
version,
|
||||
channel,
|
||||
description: description || '',
|
||||
downloadUrl,
|
||||
fileSize: req.file.size,
|
||||
checksum: fileHash,
|
||||
mandatory: mandatory === 'true',
|
||||
filesCount: zipAnalysis.files.length,
|
||||
uploadedAt: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// 清理文件
|
||||
if (req.file) {
|
||||
await fs.remove(req.file.path).catch(() => {});
|
||||
}
|
||||
|
||||
console.error('上传失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '上传失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 上传进度查询
|
||||
*/
|
||||
uploadRoutes.get('/progress/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 这里可以实现真正的进度追踪
|
||||
// 目前返回完成状态
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id,
|
||||
progress: 100,
|
||||
status: 'completed'
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('查询上传进度失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '查询上传进度失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取上传历史
|
||||
*/
|
||||
uploadRoutes.get('/history', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { pluginId, limit = 10 } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT v.*, p.display_name as plugin_display_name, p.icon as plugin_icon
|
||||
FROM versions v
|
||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
if (pluginId) {
|
||||
sql += ' AND v.plugin_id = ?';
|
||||
params.push(pluginId);
|
||||
}
|
||||
|
||||
sql += ` ORDER BY v.created_at DESC LIMIT ?`;
|
||||
params.push(Number(limit));
|
||||
|
||||
const uploads = await query(sql, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: uploads.map(u => ({
|
||||
id: u.id,
|
||||
pluginId: u.plugin_id,
|
||||
version: u.version,
|
||||
channel: u.channel,
|
||||
fileSize: u.file_size,
|
||||
uploadedAt: u.created_at,
|
||||
pluginDisplayName: u.plugin_display_name,
|
||||
pluginIcon: u.plugin_icon
|
||||
}))
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取上传历史失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取上传历史失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 计算文件哈希值
|
||||
*/
|
||||
async function calculateFileHash(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
stream.on('data', data => hash.update(data));
|
||||
stream.on('end', () => resolve(hash.digest('hex')));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析ZIP文件内容
|
||||
*/
|
||||
async function analyzeZipFile(filePath: string): Promise<{ files: Array<{ path: string; hash: string; size: number }> }> {
|
||||
const zip = new AdmZip(filePath);
|
||||
const entries = zip.getEntries();
|
||||
|
||||
const files: Array<{ path: string; hash: string; size: number }> = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory) {
|
||||
const content = entry.getData();
|
||||
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
||||
|
||||
files.push({
|
||||
path: entry.entryName,
|
||||
hash: hash,
|
||||
size: entry.header.size
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { files };
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query, queryOne, execute } from '../database';
|
||||
|
||||
export const versionRoutes = Router();
|
||||
|
||||
/**
|
||||
* 获取所有版本列表
|
||||
*/
|
||||
versionRoutes.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { pluginId, channel, limit = 50 } = req.query;
|
||||
|
||||
let sql = `
|
||||
SELECT v.*, p.display_name as plugin_display_name, p.icon as plugin_icon
|
||||
FROM versions v
|
||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
||||
WHERE v.status = 'active'
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
if (pluginId) {
|
||||
sql += ' AND v.plugin_id = ?';
|
||||
params.push(pluginId);
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
sql += ' AND v.channel = ?';
|
||||
params.push(channel);
|
||||
}
|
||||
|
||||
sql += ` ORDER BY v.created_at DESC LIMIT ?`;
|
||||
params.push(Number(limit));
|
||||
|
||||
const versions = await query(sql, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: versions
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取版本列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取版本列表失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取特定版本详情
|
||||
*/
|
||||
versionRoutes.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const version = await query(`
|
||||
SELECT v.*, p.display_name as plugin_display_name, p.icon as plugin_icon
|
||||
FROM versions v
|
||||
LEFT JOIN plugins p ON v.plugin_id = p.id
|
||||
WHERE v.id = ? AND v.status = 'active'
|
||||
`, [id]);
|
||||
|
||||
if (version.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '版本不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取版本文件列表
|
||||
const files = await query(`
|
||||
SELECT * FROM version_files WHERE version_id = ?
|
||||
`, [id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...version[0],
|
||||
files
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取版本详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '获取版本详情失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 删除版本
|
||||
*/
|
||||
versionRoutes.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 软删除
|
||||
await execute(`
|
||||
UPDATE versions SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '版本删除成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('删除版本失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '删除版本失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新版本状态
|
||||
*/
|
||||
versionRoutes.patch('/:id/status', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
if (!['active', 'inactive', 'deleted'].includes(status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '无效的状态值'
|
||||
});
|
||||
}
|
||||
|
||||
await execute(`
|
||||
UPDATE versions SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [status, id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '状态更新成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新版本状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '更新版本状态失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 检查更新 (供客户端使用)
|
||||
*/
|
||||
versionRoutes.post('/check', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { currentVersion, pluginId = 'cocos-ecs-extension', channel = 'stable', platform, editorVersion } = req.body;
|
||||
|
||||
console.log(`📱 插件更新检查: ${pluginId} v${currentVersion} (${channel}) - ${platform}`);
|
||||
|
||||
// 查找最新版本
|
||||
const latestVersion = await queryOne(`
|
||||
SELECT * FROM versions
|
||||
WHERE plugin_id = ? AND channel = ? AND status = 'active'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, [pluginId, channel]);
|
||||
|
||||
if (!latestVersion) {
|
||||
return res.json({
|
||||
hasUpdate: false,
|
||||
message: `暂无 ${pluginId} 的 ${channel} 版本`,
|
||||
currentVersion
|
||||
});
|
||||
}
|
||||
|
||||
// 比较版本号
|
||||
const hasUpdate = isNewerVersion(latestVersion.version, currentVersion);
|
||||
|
||||
if (hasUpdate) {
|
||||
// 记录更新检查
|
||||
await execute(`
|
||||
INSERT OR REPLACE INTO update_stats (plugin_id, version, user_count, last_updated)
|
||||
VALUES (?, ?, COALESCE((SELECT user_count FROM update_stats WHERE plugin_id = ? AND version = ?), 0) + 1, ?)
|
||||
`, [pluginId, latestVersion.version, pluginId, latestVersion.version, new Date().toISOString()]);
|
||||
|
||||
// 获取文件列表
|
||||
const files = await query(`
|
||||
SELECT file_path, file_hash, file_size, action
|
||||
FROM version_files
|
||||
WHERE version_id = ?
|
||||
`, [latestVersion.id]);
|
||||
|
||||
res.json({
|
||||
hasUpdate: true,
|
||||
version: latestVersion.version,
|
||||
releaseDate: latestVersion.release_date,
|
||||
description: latestVersion.description,
|
||||
downloadUrl: latestVersion.download_url,
|
||||
fileSize: latestVersion.file_size,
|
||||
checksum: latestVersion.checksum,
|
||||
mandatory: latestVersion.mandatory === 1,
|
||||
files: files.map(f => ({
|
||||
path: f.file_path,
|
||||
hash: f.file_hash,
|
||||
size: f.file_size,
|
||||
action: f.action
|
||||
}))
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
hasUpdate: false,
|
||||
message: `${pluginId} 当前已是最新版本`,
|
||||
currentVersion
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error);
|
||||
res.status(500).json({
|
||||
hasUpdate: false,
|
||||
error: '检查更新失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建新版本
|
||||
*/
|
||||
versionRoutes.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { pluginId, version, channel, description, downloadUrl, fileSize, checksum, mandatory, files } = req.body;
|
||||
|
||||
// 检查版本是否已存在
|
||||
const existingVersion = await queryOne(`
|
||||
SELECT id FROM versions
|
||||
WHERE plugin_id = ? AND version = ? AND channel = ?
|
||||
`, [pluginId, version, channel]);
|
||||
|
||||
if (existingVersion) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '该版本已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建版本记录
|
||||
const result = await execute(`
|
||||
INSERT INTO versions
|
||||
(plugin_id, version, channel, description, download_url, file_size, checksum, mandatory, release_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [pluginId, version, channel, description, downloadUrl, fileSize, checksum, mandatory ? 1 : 0, new Date().toISOString()]);
|
||||
|
||||
const versionId = result.lastID;
|
||||
|
||||
// 添加文件记录
|
||||
if (files && files.length > 0) {
|
||||
for (const file of files) {
|
||||
await execute(`
|
||||
INSERT INTO version_files (version_id, file_path, file_hash, file_size, action)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [versionId, file.path, file.hash, file.size, file.action || 'update']);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: versionId,
|
||||
pluginId,
|
||||
version,
|
||||
channel,
|
||||
description,
|
||||
downloadUrl,
|
||||
fileSize,
|
||||
checksum,
|
||||
mandatory
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建版本失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '创建版本失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 更新版本信息
|
||||
*/
|
||||
versionRoutes.put('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { description, status, mandatory } = req.body;
|
||||
|
||||
await execute(`
|
||||
UPDATE versions
|
||||
SET description = ?, status = ?, mandatory = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`, [description, status, mandatory ? 1 : 0, new Date().toISOString(), id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '版本信息更新成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新版本失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: '更新版本失败',
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
*/
|
||||
function isNewerVersion(newVersion: string, currentVersion: string): boolean {
|
||||
const parseVersion = (version: string) => {
|
||||
return version.split('.').map(Number);
|
||||
};
|
||||
|
||||
const newParts = parseVersion(newVersion);
|
||||
const currentParts = parseVersion(currentVersion);
|
||||
const maxLength = Math.max(newParts.length, currentParts.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const newPart = newParts[i] || 0;
|
||||
const currentPart = currentParts[i] || 0;
|
||||
|
||||
if (newPart > currentPart) return true;
|
||||
if (newPart < currentPart) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as express from 'express';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import * as cors from 'cors';
|
||||
import * as path from 'path';
|
||||
import { authRoutes } from './routes/auth';
|
||||
import { uploadRoutes } from './routes/upload';
|
||||
import { versionRoutes } from './routes/versions';
|
||||
import { configRoutes } from './routes/config';
|
||||
import { statsRoutes } from './routes/stats';
|
||||
import { initDatabase } from './database';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '100mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '100mb' }));
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||
app.use('/assets', express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// API路由
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/upload', uploadRoutes);
|
||||
app.use('/api/config', configRoutes);
|
||||
app.use('/api/stats', statsRoutes);
|
||||
|
||||
// 热更新客户端API (供Cocos Creator插件使用)
|
||||
app.post('/api/plugin-updates/check', (req: Request, res: Response) => {
|
||||
// 检查插件更新
|
||||
res.json({
|
||||
message: '当前已是最新版本',
|
||||
hasUpdate: false,
|
||||
currentVersion: req.body?.currentVersion || '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
// 管理界面路由
|
||||
app.get('/', (req: Request, res: Response) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
app.get('/admin', (req: Request, res: Response) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error('Error:', err);
|
||||
res.status(500).json({
|
||||
error: '服务器内部错误',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : '请联系管理员'
|
||||
});
|
||||
});
|
||||
|
||||
// 404处理
|
||||
app.use('*', (req: Request, res: Response) => {
|
||||
res.status(404).json({ error: '接口不存在' });
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
async function startServer() {
|
||||
try {
|
||||
// 初始化数据库
|
||||
await initDatabase();
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 热更新管理后台启动成功!`);
|
||||
console.log(`📍 服务地址: http://localhost:${PORT}`);
|
||||
console.log(`📱 管理界面: http://localhost:${PORT}`);
|
||||
console.log(`📱 管理界面(admin): http://localhost:${PORT}/admin`);
|
||||
console.log(`🔗 健康检查: http://localhost:${PORT}/health`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ 服务器启动失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"uploads"
|
||||
]
|
||||
}
|
||||
@@ -14,13 +14,15 @@
|
||||
"dependencies": {
|
||||
"vue": "^3.1.4",
|
||||
"fs-extra": "^10.0.0",
|
||||
"ws": "^8.14.2"
|
||||
"ws": "^8.14.2",
|
||||
"adm-zip": "^0.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cocos/creator-types": "^3.8.6",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/node": "^18.17.1",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"panels": {
|
||||
@@ -98,6 +100,11 @@
|
||||
"path": "i18n:menu.develop/ECS Framework",
|
||||
"label": "ECS 开发工具",
|
||||
"message": "open-panel"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.panel/ECS Framework",
|
||||
"label": "检查更新",
|
||||
"message": "check-plugin-updates"
|
||||
}
|
||||
],
|
||||
"assets": {
|
||||
@@ -177,6 +184,21 @@
|
||||
"check-behavior-tree-installed"
|
||||
]
|
||||
},
|
||||
"check-plugin-updates": {
|
||||
"methods": [
|
||||
"check-plugin-updates"
|
||||
]
|
||||
},
|
||||
"set-hot-update-config": {
|
||||
"methods": [
|
||||
"set-hot-update-config"
|
||||
]
|
||||
},
|
||||
"get-hot-update-config": {
|
||||
"methods": [
|
||||
"get-hot-update-config"
|
||||
]
|
||||
},
|
||||
"open-behavior-tree-docs": {
|
||||
"methods": [
|
||||
"open-behavior-tree-docs"
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* 热更新配置接口
|
||||
*/
|
||||
interface HotUpdateConfig {
|
||||
serverUrl: string;
|
||||
currentVersion: string;
|
||||
updateChannel: 'stable' | 'beta' | 'dev';
|
||||
autoCheck: boolean;
|
||||
checkInterval: number; // 分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息接口
|
||||
*/
|
||||
interface VersionInfo {
|
||||
version: string;
|
||||
releaseDate: string;
|
||||
description: string;
|
||||
downloadUrl: string;
|
||||
fileSize: number;
|
||||
checksum: string;
|
||||
mandatory: boolean; // 是否强制更新
|
||||
files: UpdateFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件接口
|
||||
*/
|
||||
interface UpdateFile {
|
||||
path: string;
|
||||
hash: string;
|
||||
size: number;
|
||||
action: 'add' | 'update' | 'delete';
|
||||
}
|
||||
|
||||
/**
|
||||
* 热更新处理器
|
||||
*/
|
||||
export class HotUpdateHandler {
|
||||
private static readonly CONFIG_FILE = 'hot-update-config.json';
|
||||
private static readonly VERSION_FILE = 'version-info.json';
|
||||
private static readonly EXTENSION_PATH = Editor.Package.getPath('cocos-ecs-extension') || '';
|
||||
|
||||
private static config: HotUpdateConfig;
|
||||
private static updateTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* 初始化热更新系统
|
||||
*/
|
||||
static async initialize(): Promise<void> {
|
||||
console.log('[HotUpdate] 初始化热更新系统...');
|
||||
|
||||
try {
|
||||
await this.loadConfig();
|
||||
await this.startAutoCheck();
|
||||
console.log('[HotUpdate] 热更新系统初始化完成');
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
*/
|
||||
private static async loadConfig(): Promise<void> {
|
||||
const configPath = path.join(this.EXTENSION_PATH, this.CONFIG_FILE);
|
||||
|
||||
try {
|
||||
if (await fs.pathExists(configPath)) {
|
||||
this.config = await fs.readJSON(configPath);
|
||||
} else {
|
||||
// 创建默认配置
|
||||
this.config = {
|
||||
serverUrl: 'https://earthonline-game.cn/api/plugin-updates',
|
||||
currentVersion: this.getCurrentVersion(),
|
||||
updateChannel: 'stable',
|
||||
autoCheck: true,
|
||||
checkInterval: 60 // 60分钟检查一次
|
||||
};
|
||||
await this.saveConfig();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 配置加载失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
*/
|
||||
private static async saveConfig(): Promise<void> {
|
||||
const configPath = path.join(this.EXTENSION_PATH, this.CONFIG_FILE);
|
||||
await fs.writeJSON(configPath, this.config, { spaces: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本
|
||||
*/
|
||||
private static getCurrentVersion(): string {
|
||||
try {
|
||||
const packagePath = path.join(this.EXTENSION_PATH, 'package.json');
|
||||
const packageInfo = fs.readJSONSync(packagePath);
|
||||
return packageInfo.version;
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 无法获取当前版本:', error);
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始自动检查
|
||||
*/
|
||||
private static async startAutoCheck(): Promise<void> {
|
||||
if (!this.config.autoCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即检查一次
|
||||
await this.checkForUpdates(true);
|
||||
|
||||
// 设置定时检查
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
}
|
||||
|
||||
this.updateTimer = setInterval(async () => {
|
||||
await this.checkForUpdates(true);
|
||||
}, this.config.checkInterval * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*/
|
||||
static async checkForUpdates(silent: boolean = false): Promise<VersionInfo | null> {
|
||||
console.log('[HotUpdate] 检查更新中...');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.config.serverUrl}/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
currentVersion: this.config.currentVersion,
|
||||
pluginId: 'cocos-ecs-extension', // 当前插件ID
|
||||
channel: this.config.updateChannel,
|
||||
platform: process.platform,
|
||||
editorVersion: Editor.App.version
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器响应错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const versionInfo: VersionInfo = await response.json();
|
||||
|
||||
if (this.isNewerVersion(versionInfo.version, this.config.currentVersion)) {
|
||||
console.log(`[HotUpdate] 发现新版本: ${versionInfo.version}`);
|
||||
|
||||
if (!silent) {
|
||||
await this.showUpdateDialog(versionInfo);
|
||||
}
|
||||
|
||||
return versionInfo;
|
||||
} else {
|
||||
if (!silent) {
|
||||
Editor.Dialog.info('检查更新', {
|
||||
detail: '当前已是最新版本!'
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 检查更新失败:', error);
|
||||
|
||||
if (!silent) {
|
||||
Editor.Dialog.error('检查更新失败', {
|
||||
detail: `无法连接到更新服务器:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
*/
|
||||
private static isNewerVersion(newVersion: string, currentVersion: string): boolean {
|
||||
const parseVersion = (version: string) => {
|
||||
return version.split('.').map(Number);
|
||||
};
|
||||
|
||||
const newParts = parseVersion(newVersion);
|
||||
const currentParts = parseVersion(currentVersion);
|
||||
const maxLength = Math.max(newParts.length, currentParts.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const newPart = newParts[i] || 0;
|
||||
const currentPart = currentParts[i] || 0;
|
||||
|
||||
if (newPart > currentPart) return true;
|
||||
if (newPart < currentPart) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示更新对话框
|
||||
*/
|
||||
private static async showUpdateDialog(versionInfo: VersionInfo): Promise<void> {
|
||||
const message = `发现新版本 ${versionInfo.version}!\n\n` +
|
||||
`发布时间: ${versionInfo.releaseDate}\n\n` +
|
||||
`更新内容:\n${versionInfo.description}\n\n` +
|
||||
`文件大小: ${this.formatFileSize(versionInfo.fileSize)}`;
|
||||
|
||||
const buttons = versionInfo.mandatory ? ['立即更新'] : ['立即更新', '稍后提醒', '跳过此版本'];
|
||||
|
||||
const result = await Editor.Dialog.info('插件更新', {
|
||||
detail: message,
|
||||
buttons: buttons
|
||||
});
|
||||
|
||||
switch (result.response) {
|
||||
case 0: // 立即更新
|
||||
await this.downloadAndInstallUpdate(versionInfo);
|
||||
break;
|
||||
case 1: // 稍后提醒
|
||||
if (!versionInfo.mandatory) {
|
||||
console.log('[HotUpdate] 用户选择稍后更新');
|
||||
}
|
||||
break;
|
||||
case 2: // 跳过此版本
|
||||
if (!versionInfo.mandatory) {
|
||||
await this.skipVersion(versionInfo.version);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并安装更新
|
||||
*/
|
||||
private static async downloadAndInstallUpdate(versionInfo: VersionInfo): Promise<void> {
|
||||
console.log(`[HotUpdate] 开始下载更新: ${versionInfo.version}`);
|
||||
|
||||
try {
|
||||
// 显示进度对话框
|
||||
const progressDialog = this.showProgressDialog('正在下载更新...');
|
||||
|
||||
// 下载更新包
|
||||
const updatePath = await this.downloadUpdate(versionInfo, (progress) => {
|
||||
// 更新进度
|
||||
console.log(`[HotUpdate] 下载进度: ${progress}%`);
|
||||
});
|
||||
|
||||
progressDialog.detail = '正在验证文件...';
|
||||
|
||||
// 验证文件完整性
|
||||
const isValid = await this.verifyUpdate(updatePath, versionInfo.checksum);
|
||||
if (!isValid) {
|
||||
throw new Error('文件校验失败,更新包可能已损坏');
|
||||
}
|
||||
|
||||
progressDialog.detail = '正在安装更新...';
|
||||
|
||||
// 安装更新
|
||||
await this.installUpdate(updatePath, versionInfo);
|
||||
|
||||
// 更新版本信息
|
||||
this.config.currentVersion = versionInfo.version;
|
||||
await this.saveConfig();
|
||||
|
||||
// 显示安装完成对话框
|
||||
const result = await Editor.Dialog.info('更新完成', {
|
||||
detail: `插件已成功更新到版本 ${versionInfo.version}!\n\n为了使更新生效,需要重启Cocos Creator编辑器。`,
|
||||
buttons: ['立即重启', '稍后重启']
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
this.restartEditor();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 更新失败:', error);
|
||||
Editor.Dialog.error('更新失败', {
|
||||
detail: `更新过程中发生错误:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载更新
|
||||
*/
|
||||
private static async downloadUpdate(versionInfo: VersionInfo, onProgress?: (progress: number) => void): Promise<string> {
|
||||
const response = await fetch(versionInfo.downloadUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers.get('content-length') || '0');
|
||||
let downloadedSize = 0;
|
||||
|
||||
const tempPath = path.join(this.EXTENSION_PATH, 'temp', `update-${versionInfo.version}.zip`);
|
||||
await fs.ensureDir(path.dirname(tempPath));
|
||||
|
||||
const writer = fs.createWriteStream(tempPath);
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
throw new Error('无法创建下载流');
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
writer.write(value);
|
||||
downloadedSize += value.length;
|
||||
|
||||
if (onProgress && totalSize > 0) {
|
||||
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||
onProgress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
writer.end();
|
||||
return tempPath;
|
||||
|
||||
} catch (error) {
|
||||
writer.destroy();
|
||||
await fs.remove(tempPath).catch(() => {}); // 忽略删除错误
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证更新包
|
||||
*/
|
||||
private static async verifyUpdate(filePath: string, expectedChecksum: string): Promise<boolean> {
|
||||
try {
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(fileBuffer);
|
||||
const actualChecksum = hash.digest('hex');
|
||||
|
||||
return actualChecksum === expectedChecksum;
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 文件校验失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装更新
|
||||
*/
|
||||
private static async installUpdate(updatePath: string, versionInfo: VersionInfo): Promise<void> {
|
||||
const extractPath = path.join(this.EXTENSION_PATH, 'temp', 'extract');
|
||||
|
||||
try {
|
||||
// 清理临时目录
|
||||
await fs.remove(extractPath);
|
||||
await fs.ensureDir(extractPath);
|
||||
|
||||
// 解压更新包
|
||||
await this.extractZip(updatePath, extractPath);
|
||||
|
||||
// 备份当前版本
|
||||
const backupPath = path.join(this.EXTENSION_PATH, 'backup', this.config.currentVersion);
|
||||
await this.createBackup(backupPath);
|
||||
|
||||
// 应用更新文件
|
||||
await this.applyUpdateFiles(extractPath, versionInfo.files);
|
||||
|
||||
// 清理临时文件
|
||||
await fs.remove(path.dirname(updatePath));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 安装更新失败:', error);
|
||||
// 尝试恢复备份
|
||||
await this.restoreBackup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压ZIP文件
|
||||
*/
|
||||
private static async extractZip(zipPath: string, extractPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 这里使用node的解压库,您可能需要安装 yauzl 或 adm-zip
|
||||
const AdmZip = require('adm-zip');
|
||||
|
||||
try {
|
||||
const zip = new AdmZip(zipPath);
|
||||
zip.extractAllTo(extractPath, true);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用更新文件
|
||||
*/
|
||||
private static async applyUpdateFiles(extractPath: string, files: UpdateFile[]): Promise<void> {
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(extractPath, file.path);
|
||||
const targetPath = path.join(this.EXTENSION_PATH, file.path);
|
||||
|
||||
try {
|
||||
switch (file.action) {
|
||||
case 'add':
|
||||
case 'update':
|
||||
await fs.ensureDir(path.dirname(targetPath));
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
||||
break;
|
||||
case 'delete':
|
||||
await fs.remove(targetPath);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[HotUpdate] 处理文件失败 ${file.path}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*/
|
||||
private static async createBackup(backupPath: string): Promise<void> {
|
||||
await fs.ensureDir(backupPath);
|
||||
|
||||
const sourceFiles = [
|
||||
'source',
|
||||
'static',
|
||||
'package.json',
|
||||
'README.md'
|
||||
];
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const sourcePath = path.join(this.EXTENSION_PATH, file);
|
||||
const targetPath = path.join(backupPath, file);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await fs.copy(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复备份
|
||||
*/
|
||||
private static async restoreBackup(): Promise<void> {
|
||||
const backupPath = path.join(this.EXTENSION_PATH, 'backup', this.config.currentVersion);
|
||||
|
||||
if (await fs.pathExists(backupPath)) {
|
||||
console.log('[HotUpdate] 正在恢复备份...');
|
||||
|
||||
const backupFiles = await fs.readdir(backupPath);
|
||||
for (const file of backupFiles) {
|
||||
const sourcePath = path.join(backupPath, file);
|
||||
const targetPath = path.join(this.EXTENSION_PATH, file);
|
||||
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过版本
|
||||
*/
|
||||
private static async skipVersion(version: string): Promise<void> {
|
||||
const skipPath = path.join(this.EXTENSION_PATH, 'skipped-versions.json');
|
||||
let skippedVersions: string[] = [];
|
||||
|
||||
if (await fs.pathExists(skipPath)) {
|
||||
skippedVersions = await fs.readJSON(skipPath);
|
||||
}
|
||||
|
||||
if (!skippedVersions.includes(version)) {
|
||||
skippedVersions.push(version);
|
||||
await fs.writeJSON(skipPath, skippedVersions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示进度对话框
|
||||
*/
|
||||
private static showProgressDialog(message: string) {
|
||||
// 这是一个简化版本,实际可能需要创建自定义进度条
|
||||
return {
|
||||
detail: message
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
private static formatFileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启编辑器
|
||||
*/
|
||||
private static restartEditor(): void {
|
||||
// 注意:这个功能需要特殊权限,可能需要用户手动重启
|
||||
console.log('[HotUpdate] 请求重启编辑器');
|
||||
|
||||
try {
|
||||
// 尝试重启编辑器(可能不会成功)
|
||||
Editor.App.quit();
|
||||
} catch (error) {
|
||||
console.warn('[HotUpdate] 无法自动重启编辑器:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置更新配置
|
||||
*/
|
||||
static async setConfig(newConfig: Partial<HotUpdateConfig>): Promise<void> {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
await this.saveConfig();
|
||||
|
||||
// 重新启动自动检查
|
||||
if (this.config.autoCheck) {
|
||||
await this.startAutoCheck();
|
||||
} else if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
static getConfig(): HotUpdateConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
static cleanup(): void {
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
|
||||
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
|
||||
export { PanelHandler } from './PanelHandler';
|
||||
export { HotUpdateHandler } from './HotUpdateHandler';
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
import packageJSON from '../package.json';
|
||||
import { EcsFrameworkHandler, BehaviorTreeHandler, PanelHandler } from './handlers';
|
||||
import { EcsFrameworkHandler, BehaviorTreeHandler, PanelHandler, HotUpdateHandler } from './handlers';
|
||||
import { readJSON } from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { AssetInfo } from '@cocos/creator-types/editor/packages/asset-db/@types/public';
|
||||
@@ -177,6 +177,29 @@ export const methods: { [key: string]: (...any: any) => any } = {
|
||||
throw new Error('文件路径不存在或数据无效');
|
||||
}
|
||||
},
|
||||
|
||||
// ================ 热更新管理 ================
|
||||
/**
|
||||
* 检查插件更新
|
||||
*/
|
||||
'check-plugin-updates'() {
|
||||
return HotUpdateHandler.checkForUpdates(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置热更新配置
|
||||
*/
|
||||
'set-hot-update-config'(...args: any[]) {
|
||||
const config = args.length >= 2 ? args[1] : args[0];
|
||||
return HotUpdateHandler.setConfig(config);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热更新配置
|
||||
*/
|
||||
'get-hot-update-config'() {
|
||||
return HotUpdateHandler.getConfig();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -187,6 +210,11 @@ export const methods: { [key: string]: (...any: any) => any } = {
|
||||
*/
|
||||
export function load() {
|
||||
console.log('[Cocos ECS Extension] 扩展已加载');
|
||||
|
||||
// 初始化热更新系统
|
||||
HotUpdateHandler.initialize().catch(error => {
|
||||
console.error('[Cocos ECS Extension] 热更新初始化失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,4 +223,7 @@ export function load() {
|
||||
*/
|
||||
export function unload() {
|
||||
console.log('[Cocos ECS Extension] 扩展已卸载');
|
||||
|
||||
// 清理热更新资源
|
||||
HotUpdateHandler.cleanup();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title Cocos ECS Extension - 热更新管理后台
|
||||
|
||||
echo.
|
||||
echo ======================================
|
||||
echo 🚀 Cocos ECS Extension 热更新管理后台
|
||||
echo ======================================
|
||||
echo.
|
||||
|
||||
:: 检查Node.js是否安装
|
||||
node --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ 错误: 未检测到Node.js,请先安装Node.js
|
||||
echo 下载地址: https://nodejs.org/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 获取Node.js版本
|
||||
for /f "tokens=1" %%i in ('node --version') do set NODE_VERSION=%%i
|
||||
echo ✅ Node.js版本: %NODE_VERSION%
|
||||
|
||||
:: 检查是否首次运行
|
||||
if not exist "admin-backend\node_modules" (
|
||||
echo.
|
||||
echo 📦 首次运行,正在安装依赖...
|
||||
cd admin-backend
|
||||
call npm install
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ 依赖安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd ..
|
||||
echo ✅ 依赖安装完成
|
||||
)
|
||||
|
||||
:: 启动服务
|
||||
echo.
|
||||
echo 🚀 启动热更新管理后台...
|
||||
echo 📍 管理界面地址: http://localhost:3001
|
||||
echo.
|
||||
echo 💡 提示: 按 Ctrl+C 可停止服务
|
||||
echo.
|
||||
|
||||
cd admin-backend
|
||||
call npm run dev
|
||||
|
||||
pause
|
||||
@@ -7,5 +7,9 @@
|
||||
"node",
|
||||
"@cocos/creator-types/editor",
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"admin-backend/**/*",
|
||||
"admin-desktop/**/*"
|
||||
]
|
||||
}
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "2.1.22",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/multer": "^1.4.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
@@ -574,12 +575,68 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
|
||||
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
||||
},
|
||||
"node_modules/@types/multer": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
|
||||
"integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
|
||||
@@ -589,12 +646,41 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
|
||||
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/node": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"url": "https://github.com/esengine/ecs-framework.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/multer": "^1.4.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user