Initial commit: R0Installer source, patch generator, API backend and build scripts
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# API 路由: /v3/* -> api/index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^v3/(.*)$ index.php [QSA,L]
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Medoo 数据库框架占位文件
|
||||
*
|
||||
* 请从官方下载完整版本替换此文件:
|
||||
* composer require catfan/medoo
|
||||
*
|
||||
* 或者从 https://medoo.in/ 下载 Medoo.php 放到此目录
|
||||
*
|
||||
* 使用 composer 时请将 require 改为:
|
||||
* require_once __DIR__ . '/vendor/autoload.php';
|
||||
*/
|
||||
|
||||
// 如果已通过 composer 安装,取消下面的注释:
|
||||
// require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// 如果您直接下载了 Medoo.php,它应该在此位置
|
||||
// 此文件仅为提示,请用实际的 Medoo 库替换
|
||||
if (!class_exists('Medoo\Medoo')) {
|
||||
die('请安装 Medoo 数据库框架: composer require catfan/medoo 或从 https://medoo.in/ 下载');
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../db.php';
|
||||
require_once __DIR__ . '/../helpers.php';
|
||||
|
||||
// 登录检查
|
||||
function isLoggedIn() {
|
||||
return isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'login') {
|
||||
$user = $_POST['username'] ?? '';
|
||||
$pass = $_POST['password'] ?? '';
|
||||
if ($user === ADMIN_USER && $pass === ADMIN_PASS) {
|
||||
$_SESSION['admin_logged_in'] = true;
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
$loginError = '用户名或密码错误';
|
||||
}
|
||||
|
||||
// 处理登出
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 未登录显示登录页
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPage($loginError ?? '');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDb();
|
||||
$page = $_GET['page'] ?? 'dashboard';
|
||||
|
||||
// AJAX 操作处理
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax_action'])) {
|
||||
handleAjaxAction($db, $_POST['ajax_action']);
|
||||
}
|
||||
|
||||
// 页面路由
|
||||
showAdminPage($db, $page);
|
||||
|
||||
// ========== AJAX 处理 ==========
|
||||
|
||||
function handleAjaxAction($db, $action) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
switch ($action) {
|
||||
case 'save_project':
|
||||
$projectId = $_POST['project_id'] ?? '';
|
||||
$displayName = $_POST['display_name'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$iconUrl = $_POST['icon_url'] ?? '';
|
||||
$enabled = isset($_POST['enabled']) ? 1 : 0;
|
||||
$configJson = $_POST['config_json'] ?? '{}';
|
||||
$editId = $_POST['edit_id'] ?? '';
|
||||
|
||||
// 验证 JSON
|
||||
$parsed = json_decode($configJson, true);
|
||||
if ($parsed === null && $configJson !== 'null') {
|
||||
echo json_encode(['success' => false, 'message' => 'config_json 不是有效的 JSON']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($editId) {
|
||||
$db->update('projects', [
|
||||
'display_name' => $displayName,
|
||||
'description' => $description,
|
||||
'icon_url' => $iconUrl,
|
||||
'enabled' => $enabled,
|
||||
'config_json' => $configJson,
|
||||
], ['id' => $editId]);
|
||||
} else {
|
||||
$db->insert('projects', [
|
||||
'project_id' => $projectId,
|
||||
'display_name' => $displayName,
|
||||
'description' => $description,
|
||||
'icon_url' => $iconUrl,
|
||||
'enabled' => $enabled,
|
||||
'config_json' => $configJson,
|
||||
]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_project':
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$db->delete('projects', ['id' => $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'save_installer_config':
|
||||
$filenameRules = $_POST['filename_rules'] ?? '[]';
|
||||
$commandRules = $_POST['command_rules'] ?? '[]';
|
||||
$defaultProject = $_POST['default_project'] ?? 'r0_arena';
|
||||
$defaultMode = $_POST['default_mode'] ?? 'full';
|
||||
|
||||
$db->update('installer_config', [
|
||||
'filename_rules' => $filenameRules,
|
||||
'command_rules' => $commandRules,
|
||||
'default_project' => $defaultProject,
|
||||
'default_mode' => $defaultMode,
|
||||
], ['config_key' => 'global']);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'save_version':
|
||||
$projectId = $_POST['project_id'] ?? '';
|
||||
$version = $_POST['version'] ?? '';
|
||||
$downloadUrl = $_POST['download_url'] ?? '';
|
||||
$fileSize = intval($_POST['file_size'] ?? 0);
|
||||
$changelog = $_POST['changelog'] ?? '';
|
||||
$threads = intval($_POST['threads'] ?? 8);
|
||||
$extraDownloads = $_POST['extra_downloads'] ?? '{}';
|
||||
$runtimeUrls = $_POST['runtime_urls'] ?? '{}';
|
||||
$isLatest = isset($_POST['is_latest']) ? 1 : 0;
|
||||
$silentUpdate = isset($_POST['silent_update']) ? 1 : 0;
|
||||
$editId = $_POST['edit_id'] ?? '';
|
||||
|
||||
if ($isLatest) {
|
||||
$db->update('project_versions', ['is_latest' => 0], ['project_id' => $projectId]);
|
||||
}
|
||||
|
||||
if ($editId) {
|
||||
$db->update('project_versions', [
|
||||
'version' => $version,
|
||||
'download_url' => $downloadUrl,
|
||||
'file_size' => $fileSize,
|
||||
'changelog' => $changelog,
|
||||
'threads' => $threads,
|
||||
'extra_downloads' => $extraDownloads,
|
||||
'runtime_urls' => $runtimeUrls,
|
||||
'is_latest' => $isLatest,
|
||||
'silent_update' => $silentUpdate,
|
||||
], ['id' => $editId]);
|
||||
} else {
|
||||
$db->insert('project_versions', [
|
||||
'project_id' => $projectId,
|
||||
'version' => $version,
|
||||
'download_url' => $downloadUrl,
|
||||
'file_size' => $fileSize,
|
||||
'changelog' => $changelog,
|
||||
'threads' => $threads,
|
||||
'extra_downloads' => $extraDownloads,
|
||||
'runtime_urls' => $runtimeUrls,
|
||||
'is_latest' => $isLatest,
|
||||
'silent_update' => $silentUpdate,
|
||||
]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_version':
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$db->delete('project_versions', ['id' => $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'save_patch':
|
||||
$projectId = $_POST['project_id'] ?? '';
|
||||
$fromVersion = $_POST['from_version'] ?? '';
|
||||
$toVersion = $_POST['to_version'] ?? '';
|
||||
$downloadUrl = $_POST['download_url'] ?? '';
|
||||
$fileSize = intval($_POST['file_size'] ?? 0);
|
||||
$changelog = $_POST['changelog'] ?? '';
|
||||
$editId = $_POST['edit_id'] ?? '';
|
||||
|
||||
if ($editId) {
|
||||
$db->update('project_patches', [
|
||||
'from_version' => $fromVersion,
|
||||
'to_version' => $toVersion,
|
||||
'download_url' => $downloadUrl,
|
||||
'file_size' => $fileSize,
|
||||
'changelog' => $changelog,
|
||||
], ['id' => $editId]);
|
||||
} else {
|
||||
$db->insert('project_patches', [
|
||||
'project_id' => $projectId,
|
||||
'from_version' => $fromVersion,
|
||||
'to_version' => $toVersion,
|
||||
'download_url' => $downloadUrl,
|
||||
'file_size' => $fileSize,
|
||||
'changelog' => $changelog,
|
||||
]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
|
||||
case 'delete_patch':
|
||||
$id = $_POST['id'] ?? 0;
|
||||
$db->delete('project_patches', ['id' => $id]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => false, 'message' => '未知操作']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ========== 页面渲染 ==========
|
||||
|
||||
function showLoginPage($error = '') {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>R0Installer 管理后台 - 登录</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e0e0e0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
||||
.login-box { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 40px; width: 360px; }
|
||||
.login-box h1 { color: #4CAF50; font-size: 22px; text-align: center; margin-bottom: 30px; }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
.form-group label { display: block; margin-bottom: 6px; color: #999; font-size: 14px; }
|
||||
.form-group input { width: 100%; padding: 10px 14px; background: #252525; border: 1px solid #444; border-radius: 6px; color: #e0e0e0; font-size: 15px; outline: none; transition: border-color 0.2s; }
|
||||
.form-group input:focus { border-color: #4CAF50; }
|
||||
.btn-login { width: 100%; padding: 12px; background: #4CAF50; color: #fff; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background 0.2s; }
|
||||
.btn-login:hover { background: #45a049; }
|
||||
.error { color: #f44336; text-align: center; margin-bottom: 16px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h1>R0Installer 管理后台</h1>
|
||||
<?php if ($error): ?><div class="error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="login">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-login">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
function showAdminPage($db, $page) {
|
||||
$projects = $db->select('projects', '*');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>R0Installer 管理后台</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e0e0e0; }
|
||||
.layout { display: flex; min-height: 100vh; }
|
||||
.sidebar { width: 220px; background: #1a1a1a; border-right: 1px solid #333; padding: 20px 0; flex-shrink: 0; }
|
||||
.sidebar h2 { color: #4CAF50; font-size: 16px; padding: 0 20px 20px; border-bottom: 1px solid #333; }
|
||||
.sidebar a { display: block; padding: 12px 20px; color: #999; text-decoration: none; font-size: 14px; transition: all 0.2s; }
|
||||
.sidebar a:hover, .sidebar a.active { color: #4CAF50; background: #252525; }
|
||||
.sidebar .logout { position: absolute; bottom: 20px; width: 220px; }
|
||||
.main { flex: 1; padding: 30px; overflow-x: auto; }
|
||||
.main h2 { color: #4CAF50; margin-bottom: 20px; font-size: 20px; }
|
||||
table { width: 100%; border-collapse: collapse; background: #1a1a1a; border-radius: 8px; overflow: hidden; }
|
||||
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #2a2a2a; font-size: 14px; }
|
||||
th { background: #252525; color: #4CAF50; font-weight: 600; }
|
||||
tr:hover { background: #1f1f1f; }
|
||||
.btn { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; transition: opacity 0.2s; }
|
||||
.btn-primary { background: #4CAF50; color: #fff; }
|
||||
.btn-danger { background: #f44336; color: #fff; }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn:hover { opacity: 0.85; }
|
||||
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: flex-start; padding-top: 60px; }
|
||||
.modal-overlay.show { display: flex; }
|
||||
.modal { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 30px; width: 700px; max-height: 80vh; overflow-y: auto; }
|
||||
.modal h3 { color: #4CAF50; margin-bottom: 20px; }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; margin-bottom: 4px; color: #999; font-size: 13px; }
|
||||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 12px; background: #252525; border: 1px solid #444; border-radius: 4px; color: #e0e0e0; font-size: 14px; outline: none; }
|
||||
.form-group textarea { min-height: 200px; font-family: monospace; font-size: 13px; }
|
||||
.form-group input:focus, .form-group textarea:focus { border-color: #4CAF50; }
|
||||
.form-row { display: flex; gap: 16px; }
|
||||
.form-row .form-group { flex: 1; }
|
||||
.form-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
|
||||
.badge-green { background: #1b5e20; color: #4CAF50; }
|
||||
.badge-gray { background: #333; color: #999; }
|
||||
.badge-blue { background: #0d47a1; color: #42a5f5; }
|
||||
.toast { position: fixed; top: 20px; right: 20px; background: #4CAF50; color: #fff; padding: 12px 24px; border-radius: 6px; z-index: 2000; display: none; font-size: 14px; }
|
||||
.tag { display: inline-block; padding: 2px 6px; background: #333; color: #aaa; border-radius: 3px; font-size: 11px; margin-right: 4px; }
|
||||
.checkbox-label { display: flex; align-items: center; gap: 8px; color: #ccc; font-size: 14px; cursor: pointer; }
|
||||
.checkbox-label input[type="checkbox"] { width: 18px; height: 18px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<h2>R0Installer</h2>
|
||||
<a href="?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">仪表盘</a>
|
||||
<a href="?page=projects" class="<?= $page === 'projects' ? 'active' : '' ?>">项目管理</a>
|
||||
<a href="?page=versions" class="<?= $page === 'versions' ? 'active' : '' ?>">版本管理</a>
|
||||
<a href="?page=patches" class="<?= $page === 'patches' ? 'active' : '' ?>">增量包管理</a>
|
||||
<a href="?page=installer" class="<?= $page === 'installer' ? 'active' : '' ?>">安装器配置</a>
|
||||
<a href="?action=logout" style="color:#f44336; margin-top: 40px;">退出登录</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
<?php
|
||||
switch ($page) {
|
||||
case 'projects': renderProjectsPage($db, $projects); break;
|
||||
case 'versions': renderVersionsPage($db, $projects); break;
|
||||
case 'patches': renderPatchesPage($db, $projects); break;
|
||||
case 'installer': renderInstallerPage($db); break;
|
||||
default: renderDashboard($db, $projects); break;
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<script>
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.style.display = 'block';
|
||||
setTimeout(() => t.style.display = 'none', 2000);
|
||||
}
|
||||
|
||||
function openModal(id) {
|
||||
document.getElementById(id).classList.add('show');
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove('show');
|
||||
}
|
||||
|
||||
function submitForm(formId, callback) {
|
||||
const form = document.getElementById(formId);
|
||||
const data = new FormData(form);
|
||||
fetch('index.php', { method: 'POST', body: data })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
showToast('保存成功');
|
||||
setTimeout(() => location.reload(), 500);
|
||||
} else {
|
||||
alert(d.message || '操作失败');
|
||||
}
|
||||
})
|
||||
.catch(e => alert('请求失败: ' + e));
|
||||
}
|
||||
|
||||
function deleteItem(action, id) {
|
||||
if (!confirm('确定要删除吗?')) return;
|
||||
const data = new FormData();
|
||||
data.append('ajax_action', action);
|
||||
data.append('id', id);
|
||||
fetch('index.php', { method: 'POST', body: data })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) { showToast('已删除'); setTimeout(() => location.reload(), 500); }
|
||||
else alert(d.message || '删除失败');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
// ========== 各页面渲染 ==========
|
||||
|
||||
function renderDashboard($db, $projects) {
|
||||
$versionCount = $db->count('project_versions');
|
||||
$patchCount = $db->count('project_patches');
|
||||
?>
|
||||
<h2>仪表盘</h2>
|
||||
<div style="display:flex;gap:20px;margin-bottom:30px;">
|
||||
<div style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;flex:1;text-align:center;">
|
||||
<div style="font-size:36px;color:#4CAF50;font-weight:bold;"><?= count($projects) ?></div>
|
||||
<div style="color:#999;margin-top:8px;">项目总数</div>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;flex:1;text-align:center;">
|
||||
<div style="font-size:36px;color:#42a5f5;font-weight:bold;"><?= $versionCount ?></div>
|
||||
<div style="color:#999;margin-top:8px;">版本总数</div>
|
||||
</div>
|
||||
<div style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;flex:1;text-align:center;">
|
||||
<div style="font-size:36px;color:#ff9800;font-weight:bold;"><?= $patchCount ?></div>
|
||||
<div style="color:#999;margin-top:8px;">增量包总数</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="font-size:16px;margin-bottom:12px;">项目列表</h2>
|
||||
<table>
|
||||
<tr><th>项目ID</th><th>名称</th><th>状态</th></tr>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<tr>
|
||||
<td><code><?= htmlspecialchars($p['project_id']) ?></code></td>
|
||||
<td><?= htmlspecialchars($p['display_name']) ?></td>
|
||||
<td><?= $p['enabled'] ? '<span class="badge badge-green">启用</span>' : '<span class="badge badge-gray">禁用</span>' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
function renderProjectsPage($db, $projects) {
|
||||
?>
|
||||
<h2>项目管理 <button class="btn btn-primary" onclick="openModal('projectModal');document.getElementById('projectForm').reset();document.getElementById('projectEditId').value='';">新增项目</button></h2>
|
||||
<table>
|
||||
<tr><th>ID</th><th>项目ID</th><th>名称</th><th>状态</th><th>更新时间</th><th>操作</th></tr>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<tr>
|
||||
<td><?= $p['id'] ?></td>
|
||||
<td><code><?= htmlspecialchars($p['project_id']) ?></code></td>
|
||||
<td><?= htmlspecialchars($p['display_name']) ?></td>
|
||||
<td><?= $p['enabled'] ? '<span class="badge badge-green">启用</span>' : '<span class="badge badge-gray">禁用</span>' ?></td>
|
||||
<td><?= $p['updated_at'] ?></td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-sm" onclick='editProject(<?= json_encode($p, JSON_UNESCAPED_UNICODE) ?>)'>编辑</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteItem('delete_project', <?= $p['id'] ?>)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
|
||||
<div class="modal-overlay" id="projectModal">
|
||||
<div class="modal">
|
||||
<h3>项目配置</h3>
|
||||
<form id="projectForm">
|
||||
<input type="hidden" name="ajax_action" value="save_project">
|
||||
<input type="hidden" name="edit_id" id="projectEditId">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>项目ID(唯一标识,创建后不可改)</label>
|
||||
<input type="text" name="project_id" id="projectIdInput" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>显示名称</label>
|
||||
<input type="text" name="display_name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<input type="text" name="description">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>图标URL</label>
|
||||
<input type="text" name="icon_url">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label"><input type="checkbox" name="enabled" checked> 启用</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>项目配置 JSON(install / update / runtime_requirements / download_config)</label>
|
||||
<textarea name="config_json" style="min-height:300px;">{}</textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-danger" onclick="closeModal('projectModal')">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitForm('projectForm')">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function editProject(p) {
|
||||
const f = document.getElementById('projectForm');
|
||||
document.getElementById('projectEditId').value = p.id;
|
||||
f.querySelector('[name=project_id]').value = p.project_id;
|
||||
f.querySelector('[name=project_id]').readOnly = true;
|
||||
f.querySelector('[name=display_name]').value = p.display_name;
|
||||
f.querySelector('[name=description]').value = p.description || '';
|
||||
f.querySelector('[name=icon_url]').value = p.icon_url || '';
|
||||
f.querySelector('[name=enabled]').checked = !!p.enabled;
|
||||
try {
|
||||
const cfg = typeof p.config_json === 'string' ? JSON.parse(p.config_json) : p.config_json;
|
||||
f.querySelector('[name=config_json]').value = JSON.stringify(cfg, null, 4);
|
||||
} catch(e) {
|
||||
f.querySelector('[name=config_json]').value = p.config_json || '{}';
|
||||
}
|
||||
openModal('projectModal');
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
function renderVersionsPage($db, $projects) {
|
||||
$filterProject = $_GET['project'] ?? '';
|
||||
$where = $filterProject ? ['project_id' => $filterProject] : [];
|
||||
$where['ORDER'] = ['id' => 'DESC'];
|
||||
$versions = $db->select('project_versions', '*', $where);
|
||||
?>
|
||||
<h2>版本管理 <button class="btn btn-primary" onclick="openModal('versionModal');document.getElementById('versionForm').reset();document.getElementById('versionEditId').value='';">新增版本</button></h2>
|
||||
<div style="margin-bottom:16px;">
|
||||
筛选项目:
|
||||
<a href="?page=versions" class="tag <?= !$filterProject ? 'style=background:#4CAF50;color:#fff' : '' ?>">全部</a>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<a href="?page=versions&project=<?= urlencode($p['project_id']) ?>" class="tag <?= $filterProject === $p['project_id'] ? 'style=background:#4CAF50;color:#fff' : '' ?>"><?= htmlspecialchars($p['display_name']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>ID</th><th>项目</th><th>版本</th><th>最新</th><th>静默更新</th><th>线程数</th><th>创建时间</th><th>操作</th></tr>
|
||||
<?php foreach ($versions as $v): ?>
|
||||
<tr>
|
||||
<td><?= $v['id'] ?></td>
|
||||
<td><code><?= htmlspecialchars($v['project_id']) ?></code></td>
|
||||
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
|
||||
<td><?= $v['is_latest'] ? '<span class="badge badge-green">是</span>' : '<span class="badge badge-gray">否</span>' ?></td>
|
||||
<td><?= !empty($v['silent_update']) ? '<span class="badge badge-blue">静默</span>' : '<span class="badge badge-gray">界面</span>' ?></td>
|
||||
<td><?= $v['threads'] ?></td>
|
||||
<td><?= $v['created_at'] ?></td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-sm" onclick='editVersion(<?= json_encode($v, JSON_UNESCAPED_UNICODE) ?>)'>编辑</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteItem('delete_version', <?= $v['id'] ?>)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
|
||||
<div class="modal-overlay" id="versionModal">
|
||||
<div class="modal">
|
||||
<h3>版本配置</h3>
|
||||
<form id="versionForm">
|
||||
<input type="hidden" name="ajax_action" value="save_version">
|
||||
<input type="hidden" name="edit_id" id="versionEditId">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>项目ID</label>
|
||||
<select name="project_id" required>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<option value="<?= htmlspecialchars($p['project_id']) ?>"><?= htmlspecialchars($p['display_name']) ?> (<?= htmlspecialchars($p['project_id']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>版本号</label>
|
||||
<input type="text" name="version" required placeholder="如 3.0.5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>下载地址 (download_url)</label>
|
||||
<input type="text" name="download_url" required placeholder="https://cdn.r0csgo.com/releases/...">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>文件大小 (bytes)</label>
|
||||
<input type="number" name="file_size" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>下载线程数</label>
|
||||
<input type="number" name="threads" value="8">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>更新日志</label>
|
||||
<textarea name="changelog" style="min-height:60px;"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>额外下载项 JSON (extra_downloads),如 {"download_url_ac_cdn": "..."}</label>
|
||||
<textarea name="extra_downloads" style="min-height:60px;">{}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>运行库下载地址覆盖 JSON (runtime_urls)</label>
|
||||
<textarea name="runtime_urls" style="min-height:60px;">{}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_latest"> 设为最新版本</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label"><input type="checkbox" name="silent_update"> 静默更新(更新到该版本时不弹界面,后台自动下载安装并拉起主程序)</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-danger" onclick="closeModal('versionModal')">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitForm('versionForm')">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function editVersion(v) {
|
||||
const f = document.getElementById('versionForm');
|
||||
document.getElementById('versionEditId').value = v.id;
|
||||
f.querySelector('[name=project_id]').value = v.project_id;
|
||||
f.querySelector('[name=version]').value = v.version;
|
||||
f.querySelector('[name=download_url]').value = v.download_url;
|
||||
f.querySelector('[name=file_size]').value = v.file_size;
|
||||
f.querySelector('[name=threads]').value = v.threads;
|
||||
f.querySelector('[name=changelog]').value = v.changelog || '';
|
||||
f.querySelector('[name=extra_downloads]').value = v.extra_downloads || '{}';
|
||||
f.querySelector('[name=runtime_urls]').value = v.runtime_urls || '{}';
|
||||
f.querySelector('[name=is_latest]').checked = !!v.is_latest;
|
||||
f.querySelector('[name=silent_update]').checked = !!Number(v.silent_update);
|
||||
openModal('versionModal');
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
function renderPatchesPage($db, $projects) {
|
||||
$filterProject = $_GET['project'] ?? '';
|
||||
$where = $filterProject ? ['project_id' => $filterProject] : [];
|
||||
$where['ORDER'] = ['id' => 'DESC'];
|
||||
$patches = $db->select('project_patches', '*', $where);
|
||||
?>
|
||||
<h2>增量包管理 <button class="btn btn-primary" onclick="openModal('patchModal');document.getElementById('patchForm').reset();document.getElementById('patchEditId').value='';">新增增量包</button></h2>
|
||||
<div style="margin-bottom:16px;">
|
||||
筛选项目:
|
||||
<a href="?page=patches" class="tag <?= !$filterProject ? 'style=background:#4CAF50;color:#fff' : '' ?>">全部</a>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<a href="?page=patches&project=<?= urlencode($p['project_id']) ?>" class="tag <?= $filterProject === $p['project_id'] ? 'style=background:#4CAF50;color:#fff' : '' ?>"><?= htmlspecialchars($p['display_name']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>ID</th><th>项目</th><th>从版本</th><th>到版本</th><th>文件大小</th><th>创建时间</th><th>操作</th></tr>
|
||||
<?php foreach ($patches as $p): ?>
|
||||
<tr>
|
||||
<td><?= $p['id'] ?></td>
|
||||
<td><code><?= htmlspecialchars($p['project_id']) ?></code></td>
|
||||
<td><?= htmlspecialchars($p['from_version']) ?></td>
|
||||
<td><?= htmlspecialchars($p['to_version']) ?></td>
|
||||
<td><?= number_format($p['file_size']) ?></td>
|
||||
<td><?= $p['created_at'] ?></td>
|
||||
<td>
|
||||
<button class="btn btn-primary btn-sm" onclick='editPatch(<?= json_encode($p, JSON_UNESCAPED_UNICODE) ?>)'>编辑</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteItem('delete_patch', <?= $p['id'] ?>)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
|
||||
<div class="modal-overlay" id="patchModal">
|
||||
<div class="modal">
|
||||
<h3>增量包配置</h3>
|
||||
<form id="patchForm">
|
||||
<input type="hidden" name="ajax_action" value="save_patch">
|
||||
<input type="hidden" name="edit_id" id="patchEditId">
|
||||
<div class="form-group">
|
||||
<label>项目ID</label>
|
||||
<select name="project_id" required>
|
||||
<?php foreach ($projects as $p): ?>
|
||||
<option value="<?= htmlspecialchars($p['project_id']) ?>"><?= htmlspecialchars($p['display_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>从版本</label>
|
||||
<input type="text" name="from_version" required placeholder="如 3.0.5">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>到版本</label>
|
||||
<input type="text" name="to_version" required placeholder="如 3.0.6">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>下载地址</label>
|
||||
<input type="text" name="download_url" required placeholder="https://cdn.r0csgo.com/patches/...">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>文件大小 (bytes)</label>
|
||||
<input type="number" name="file_size" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>更新日志</label>
|
||||
<textarea name="changelog" style="min-height:60px;"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-danger" onclick="closeModal('patchModal')">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitForm('patchForm')">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function editPatch(p) {
|
||||
const f = document.getElementById('patchForm');
|
||||
document.getElementById('patchEditId').value = p.id;
|
||||
f.querySelector('[name=project_id]').value = p.project_id;
|
||||
f.querySelector('[name=from_version]').value = p.from_version;
|
||||
f.querySelector('[name=to_version]').value = p.to_version;
|
||||
f.querySelector('[name=download_url]').value = p.download_url;
|
||||
f.querySelector('[name=file_size]').value = p.file_size;
|
||||
f.querySelector('[name=changelog]').value = p.changelog || '';
|
||||
openModal('patchModal');
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
function renderInstallerPage($db) {
|
||||
$config = $db->get('installer_config', '*', ['config_key' => 'global']);
|
||||
$filenameRules = $config ? $config['filename_rules'] : '[]';
|
||||
$commandRules = $config ? $config['command_rules'] : '[]';
|
||||
|
||||
// 格式化 JSON 显示
|
||||
$fr = json_decode($filenameRules, true);
|
||||
$cr = json_decode($commandRules, true);
|
||||
$frFormatted = $fr ? json_encode($fr, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '[]';
|
||||
$crFormatted = $cr ? json_encode($cr, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '[]';
|
||||
?>
|
||||
<h2>安装器全局配置</h2>
|
||||
<form id="installerForm" style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;max-width:800px;">
|
||||
<input type="hidden" name="ajax_action" value="save_installer_config">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>默认项目ID</label>
|
||||
<input type="text" name="default_project" value="<?= htmlspecialchars($config['default_project'] ?? 'r0_arena') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>默认安装模式</label>
|
||||
<select name="default_mode">
|
||||
<option value="full" <?= ($config['default_mode'] ?? '') === 'full' ? 'selected' : '' ?>>full(完整安装)</option>
|
||||
<option value="client_only" <?= ($config['default_mode'] ?? '') === 'client_only' ? 'selected' : '' ?>>client_only(仅客户端)</option>
|
||||
<option value="ac_only" <?= ($config['default_mode'] ?? '') === 'ac_only' ? 'selected' : '' ?>>ac_only(仅反作弊)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>文件名匹配规则 (filename_rules) JSON 数组</label>
|
||||
<textarea name="filename_rules" style="min-height:200px;"><?= htmlspecialchars($frFormatted) ?></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>命令行匹配规则 (command_rules) JSON 数组</label>
|
||||
<textarea name="command_rules" style="min-height:150px;"><?= htmlspecialchars($crFormatted) ?></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-primary" onclick="submitForm('installerForm')">保存配置</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "r0/installer-api",
|
||||
"description": "R0Installer 云端配置 API",
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"catfan/medoo": "^2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
// 数据库配置
|
||||
define('DB_TYPE', 'mysql');
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_PORT', 3306);
|
||||
define('DB_NAME', 'installer');
|
||||
define('DB_USER', 'installer');
|
||||
define('DB_PASS', 'your-database-password');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// 管理后台账号密码(硬编码)
|
||||
define('ADMIN_USER', 'admin');
|
||||
define('ADMIN_PASS', 'your-admin-password');
|
||||
|
||||
// API Base URL
|
||||
define('API_BASE', '/v3');
|
||||
@@ -0,0 +1,158 @@
|
||||
-- R0Installer 云端配置数据库
|
||||
-- 使用 MySQL 5.7+ / MariaDB 10.2+
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `r0_installer` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE `r0_installer`;
|
||||
|
||||
-- 项目表
|
||||
CREATE TABLE IF NOT EXISTS `projects` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`project_id` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`display_name` VARCHAR(100) NOT NULL,
|
||||
`description` TEXT,
|
||||
`icon_url` VARCHAR(500) DEFAULT '',
|
||||
`enabled` TINYINT(1) DEFAULT 1,
|
||||
`config_json` JSON NOT NULL,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 安装器全局配置表
|
||||
CREATE TABLE IF NOT EXISTS `installer_config` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`config_key` VARCHAR(50) NOT NULL UNIQUE DEFAULT 'global',
|
||||
`installer_version` VARCHAR(20) DEFAULT '2.0',
|
||||
`min_client_version` VARCHAR(20) DEFAULT '1.0.0.0',
|
||||
`filename_rules` JSON NOT NULL,
|
||||
`command_rules` JSON NOT NULL,
|
||||
`default_project` VARCHAR(50) DEFAULT 'r0_arena',
|
||||
`default_mode` VARCHAR(20) DEFAULT 'full',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 项目版本表
|
||||
CREATE TABLE IF NOT EXISTS `project_versions` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`project_id` VARCHAR(50) NOT NULL,
|
||||
`version` VARCHAR(50) NOT NULL,
|
||||
`download_url` VARCHAR(500) NOT NULL,
|
||||
`file_size` BIGINT DEFAULT 0,
|
||||
`changelog` TEXT,
|
||||
`threads` INT DEFAULT 8,
|
||||
`extra_downloads` JSON DEFAULT NULL,
|
||||
`runtime_urls` JSON DEFAULT NULL,
|
||||
`is_latest` TINYINT(1) DEFAULT 0,
|
||||
`silent_update` TINYINT(1) DEFAULT 0, -- 1=更新到该版本时静默更新(不弹界面),0=显示更新界面
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uk_project_version` (`project_id`, `version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- 已有数据库升级:为旧的 project_versions 表补充 silent_update 列(首次部署可忽略本行;
|
||||
-- MySQL 8 不支持 ADD COLUMN IF NOT EXISTS,若列已存在会报错,可直接跳过执行)。
|
||||
-- ALTER TABLE `project_versions` ADD COLUMN `silent_update` TINYINT(1) DEFAULT 0 AFTER `is_latest`;
|
||||
|
||||
-- 增量更新包表
|
||||
CREATE TABLE IF NOT EXISTS `project_patches` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`project_id` VARCHAR(50) NOT NULL,
|
||||
`from_version` VARCHAR(50) NOT NULL,
|
||||
`to_version` VARCHAR(50) NOT NULL,
|
||||
`download_url` VARCHAR(500) NOT NULL,
|
||||
`file_size` BIGINT DEFAULT 0,
|
||||
`changelog` TEXT,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `uk_patch` (`project_id`, `from_version`, `to_version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ========== 初始数据 ==========
|
||||
|
||||
-- 安装器全局配置
|
||||
INSERT INTO `installer_config` (`config_key`, `installer_version`, `min_client_version`, `filename_rules`, `command_rules`, `default_project`, `default_mode`)
|
||||
VALUES ('global', '2.0', '1.0.0.0',
|
||||
'[{"pattern":"R0ClientInstaller","match_type":"starts_with","case_sensitive":false,"project_id":"r0_arena","install_mode":"client_only","description":"仅安装R0对战平台客户端"},{"pattern":"R0AcInstaller","match_type":"starts_with","case_sensitive":false,"project_id":"r0_arena","install_mode":"ac_only","description":"仅安装R0对战平台环境和反作弊"},{"pattern":"R0Installer","match_type":"starts_with","case_sensitive":false,"project_id":"r0_arena","install_mode":"full","description":"完整安装R0对战平台"}]',
|
||||
'[{"command":"update","project_id":"r0_arena","update_target":"update","description":"R0对战平台主程序更新"},{"command":"update_ac","project_id":"r0_arena","update_target":"update_ac","description":"R0对战平台反作弊更新"}]',
|
||||
'r0_arena', 'full');
|
||||
|
||||
-- R0对战平台项目
|
||||
INSERT INTO `projects` (`project_id`, `display_name`, `description`, `enabled`, `config_json`)
|
||||
VALUES ('r0_arena', 'R0对战平台', 'R0对战平台客户端', 1,
|
||||
'{
|
||||
"install": {
|
||||
"window_title": "R0对战平台安装程序",
|
||||
"steps": [
|
||||
{"step_id": "runtime", "display_name": "环境检查", "type": "runtime_check", "enabled": true},
|
||||
{"step_id": "anticheat", "display_name": "反作弊安装", "type": "download_and_run", "enabled": true, "download_url_field": "download_url_ac_cdn", "temp_filename_template": "r0_guard_v{version}.exe", "installer_args": "", "wait_for_exit": true},
|
||||
{"step_id": "main", "display_name": "主程序安装", "type": "download_and_run", "enabled": true, "download_url_field": "download_url_cdn", "temp_filename_template": "r0_arena_v{version}.exe", "installer_args": "", "wait_for_exit": false}
|
||||
]
|
||||
},
|
||||
"update": {
|
||||
"window_title": "R0对战平台更新程序",
|
||||
"command": "update",
|
||||
"check_api": "/v3/update/check?project=r0_arena&v={0}",
|
||||
"processes_to_close": ["r0_arena", "R0对战平台"],
|
||||
"launch_after_update": "R0对战平台.exe",
|
||||
"temp_filename_template": "r0_arena_v{version}.exe",
|
||||
"hold_file": null,
|
||||
"supports_incremental": true
|
||||
},
|
||||
"update_ac": {
|
||||
"window_title": "R0反作弊更新程序",
|
||||
"command": "update_ac",
|
||||
"check_api": "/v3/update/check?project=r0_guard&v={0}",
|
||||
"processes_to_close": ["R0Guard"],
|
||||
"launch_after_update": "R0Guard.exe",
|
||||
"temp_filename_template": "r0_guard_v{version}.exe",
|
||||
"hold_file": "ac.hold_install",
|
||||
"supports_incremental": true
|
||||
},
|
||||
"runtime_requirements": [
|
||||
{
|
||||
"runtime_id": "vc_redist",
|
||||
"display_name": "VC++运行库",
|
||||
"enabled": true,
|
||||
"always_install": true,
|
||||
"detection": {"type": "registry", "keys": ["SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x64","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\15.0\\\\VC\\\\Runtimes\\\\x86","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\15.0\\\\VC\\\\Runtimes\\\\x64","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\16.0\\\\VC\\\\Runtimes\\\\x86","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\16.0\\\\VC\\\\Runtimes\\\\x64","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\17.0\\\\VC\\\\Runtimes\\\\x86","SOFTWARE\\\\Microsoft\\\\VisualStudio\\\\17.0\\\\VC\\\\Runtimes\\\\x64"], "value_name": "Installed", "expected_value": 1},
|
||||
"download_url_x86": "https://cdn.r0csgo.com/runtime/VC_redist.x86.exe",
|
||||
"download_url_x64": "https://cdn.r0csgo.com/runtime/VC_redist.x64.exe",
|
||||
"temp_filename_template": "VC_redist.{arch}.exe",
|
||||
"install_args": "/install /passive /norestart"
|
||||
},
|
||||
{
|
||||
"runtime_id": "dotnet_desktop",
|
||||
"display_name": ".NET运行库",
|
||||
"enabled": false,
|
||||
"always_install": false,
|
||||
"detection": {"type": "dotnet_runtime", "runtime_name": "Microsoft.WindowsDesktop.App", "version": "8.0.21"},
|
||||
"download_url_x86": "https://cdn.r0csgo.com/runtime/windowsdesktop-runtime-8.0.21-win-x86.exe",
|
||||
"download_url_x64": "https://cdn.r0csgo.com/runtime/windowsdesktop-runtime-8.0.21-win-x64.exe",
|
||||
"temp_filename_template": "windowsdesktop-runtime-{version}-win-{arch}.exe",
|
||||
"install_args": "/install /passive /norestart"
|
||||
},
|
||||
{
|
||||
"runtime_id": "aspnet_core",
|
||||
"display_name": "ASP.NET Core运行库",
|
||||
"enabled": false,
|
||||
"always_install": false,
|
||||
"detection": {"type": "dotnet_runtime", "runtime_name": "Microsoft.AspNetCore.App", "version": "8.0.21"},
|
||||
"download_url_x86": "https://cdn.r0csgo.com/runtime/aspnetcore-runtime-8.0.21-win-x86.exe",
|
||||
"download_url_x64": "https://cdn.r0csgo.com/runtime/aspnetcore-runtime-8.0.21-win-x64.exe",
|
||||
"temp_filename_template": "aspnetcore-runtime-{version}-win-{arch}.exe",
|
||||
"install_args": "/install /passive /norestart"
|
||||
}
|
||||
],
|
||||
"download_config": {
|
||||
"chunk_size": 10485760,
|
||||
"max_threads": 8,
|
||||
"retry_count": 3,
|
||||
"timeout_ms": 30000,
|
||||
"user_agent": "r0_installer"
|
||||
}
|
||||
}');
|
||||
|
||||
-- 示例版本
|
||||
INSERT INTO `project_versions` (`project_id`, `version`, `download_url`, `file_size`, `changelog`, `threads`, `extra_downloads`, `runtime_urls`, `is_latest`)
|
||||
VALUES ('r0_arena', '3.0.5', 'https://cdn.r0csgo.com/releases/r0_arena_v3.0.5.exe', 0, '最新版本', 8,
|
||||
'{"download_url_ac_cdn": "https://cdn.r0csgo.com/releases/r0_guard_v2.1.0.exe"}',
|
||||
'{"VC_REDISTRIB_X86_URL":"https://cdn.r0csgo.com/runtime/VC_redist.x86.exe","VC_REDISTRIB_X64_URL":"https://cdn.r0csgo.com/runtime/VC_redist.x64.exe"}',
|
||||
1);
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/Medoo.php';
|
||||
|
||||
use Medoo\Medoo;
|
||||
|
||||
function getDb() {
|
||||
static $db = null;
|
||||
if ($db === null) {
|
||||
$db = new Medoo([
|
||||
'type' => DB_TYPE,
|
||||
'host' => DB_HOST,
|
||||
'port' => DB_PORT,
|
||||
'database' => DB_NAME,
|
||||
'username' => DB_USER,
|
||||
'password' => DB_PASS,
|
||||
'charset' => DB_CHARSET,
|
||||
]);
|
||||
}
|
||||
return $db;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
function json_response($data, $code = 0, $message = 'success') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
echo json_encode([
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function json_error($message, $code = 5000) {
|
||||
json_response(null, $code, $message);
|
||||
}
|
||||
|
||||
function get_request_path() {
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$pos = strpos($uri, '?');
|
||||
if ($pos !== false) {
|
||||
$uri = substr($uri, 0, $pos);
|
||||
}
|
||||
return rtrim($uri, '/');
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
/**
|
||||
* R0Installer 云端配置 API 入口
|
||||
*
|
||||
* Nginx 配置示例:
|
||||
* location /v3/ {
|
||||
* try_files $uri $uri/ /api/index.php?$query_string;
|
||||
* }
|
||||
*
|
||||
* Apache .htaccess:
|
||||
* RewriteEngine On
|
||||
* RewriteRule ^v3/(.*)$ api/index.php [QSA,L]
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
|
||||
$path = get_request_path();
|
||||
$db = getDb();
|
||||
|
||||
// 路由匹配
|
||||
if ($path === '/v3/config/projects') {
|
||||
handleGetProjects($db);
|
||||
}
|
||||
elseif (preg_match('#^/v3/config/project/([a-zA-Z0-9_]+)$#', $path, $m)) {
|
||||
handleGetProjectConfig($db, $m[1]);
|
||||
}
|
||||
elseif ($path === '/v3/config/installer') {
|
||||
handleGetInstallerConfig($db);
|
||||
}
|
||||
elseif ($path === '/v3/version/index') {
|
||||
$projectId = $_GET['project'] ?? '';
|
||||
handleGetVersionIndex($db, $projectId);
|
||||
}
|
||||
elseif ($path === '/v3/update/check') {
|
||||
$projectId = $_GET['project'] ?? '';
|
||||
$version = $_GET['v'] ?? '';
|
||||
handleUpdateCheck($db, $projectId, $version);
|
||||
}
|
||||
else {
|
||||
json_error('Not Found', 404);
|
||||
}
|
||||
|
||||
// ========== API 处理函数 ==========
|
||||
|
||||
function handleGetProjects($db) {
|
||||
$rows = $db->select('projects', [
|
||||
'project_id', 'display_name', 'description', 'icon_url', 'enabled'
|
||||
]);
|
||||
|
||||
$projects = [];
|
||||
foreach ($rows as $row) {
|
||||
$row['enabled'] = (bool)$row['enabled'];
|
||||
$projects[] = $row;
|
||||
}
|
||||
|
||||
json_response(['projects' => $projects]);
|
||||
}
|
||||
|
||||
function handleGetProjectConfig($db, $projectId) {
|
||||
$row = $db->get('projects', [
|
||||
'project_id', 'display_name', 'config_json'
|
||||
], ['project_id' => $projectId]);
|
||||
|
||||
if (!$row) {
|
||||
json_error('项目不存在', 1001);
|
||||
}
|
||||
|
||||
$config = json_decode($row['config_json'], true);
|
||||
if (!$config) {
|
||||
json_error('项目配置解析失败', 1003);
|
||||
}
|
||||
|
||||
$config['project_id'] = $row['project_id'];
|
||||
$config['display_name'] = $row['display_name'];
|
||||
|
||||
json_response($config);
|
||||
}
|
||||
|
||||
function handleGetInstallerConfig($db) {
|
||||
$row = $db->get('installer_config', '*', ['config_key' => 'global']);
|
||||
|
||||
if (!$row) {
|
||||
json_error('配置不存在', 1003);
|
||||
}
|
||||
|
||||
$filenameRules = json_decode($row['filename_rules'], true) ?: [];
|
||||
$commandRules = json_decode($row['command_rules'], true) ?: [];
|
||||
|
||||
json_response([
|
||||
'installer_version' => $row['installer_version'],
|
||||
'min_client_version' => $row['min_client_version'],
|
||||
'filename_rules' => $filenameRules,
|
||||
'command_rules' => $commandRules,
|
||||
'default_project_id' => $row['default_project'],
|
||||
'default_install_mode' => $row['default_mode'],
|
||||
]);
|
||||
}
|
||||
|
||||
function handleGetVersionIndex($db, $projectId) {
|
||||
if (empty($projectId)) {
|
||||
json_error('缺少 project 参数', 1002);
|
||||
}
|
||||
|
||||
$row = $db->get('project_versions', '*', [
|
||||
'project_id' => $projectId,
|
||||
'is_latest' => 1,
|
||||
]);
|
||||
|
||||
if (!$row) {
|
||||
$row = $db->get('project_versions', '*', [
|
||||
'project_id' => $projectId,
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
json_error('未找到版本信息', 1001);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'project_id' => $row['project_id'],
|
||||
'version' => $row['version'],
|
||||
'download_url_cdn' => $row['download_url'],
|
||||
'threads' => (int)$row['threads'],
|
||||
'changelog' => $row['changelog'] ?? '',
|
||||
];
|
||||
|
||||
$extraDownloads = json_decode($row['extra_downloads'] ?? '{}', true);
|
||||
if ($extraDownloads) {
|
||||
foreach ($extraDownloads as $key => $val) {
|
||||
$data[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
$runtimeUrls = json_decode($row['runtime_urls'] ?? '{}', true);
|
||||
if ($runtimeUrls) {
|
||||
$data['runtime_download_url'] = $runtimeUrls;
|
||||
}
|
||||
|
||||
json_response($data);
|
||||
}
|
||||
|
||||
function handleUpdateCheck($db, $projectId, $currentVersion) {
|
||||
if (empty($projectId)) {
|
||||
json_error('缺少 project 参数', 1002);
|
||||
}
|
||||
if (empty($currentVersion)) {
|
||||
json_error('缺少 v 参数', 1002);
|
||||
}
|
||||
|
||||
$latestRow = $db->get('project_versions', '*', [
|
||||
'project_id' => $projectId,
|
||||
'is_latest' => 1,
|
||||
]);
|
||||
|
||||
if (!$latestRow) {
|
||||
$latestRow = $db->get('project_versions', '*', [
|
||||
'project_id' => $projectId,
|
||||
'ORDER' => ['id' => 'DESC'],
|
||||
'LIMIT' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$latestRow) {
|
||||
json_response([
|
||||
'has_update' => false,
|
||||
'latest_version' => $currentVersion,
|
||||
'current_version' => $currentVersion,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$latestVersion = $latestRow['version'];
|
||||
|
||||
if (version_compare($currentVersion, $latestVersion, '>=')) {
|
||||
json_response([
|
||||
'has_update' => false,
|
||||
'latest_version' => $latestVersion,
|
||||
'current_version' => $currentVersion,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$patches = $db->select('project_patches', '*', [
|
||||
'project_id' => $projectId,
|
||||
'ORDER' => ['id' => 'ASC'],
|
||||
]);
|
||||
|
||||
$chain = buildPatchChain($patches, $currentVersion, $latestVersion);
|
||||
|
||||
// 是否静默更新由“目标版本”(即最新版本)决定,云端按版本控制。
|
||||
$silent = (bool)($latestRow['silent_update'] ?? 0);
|
||||
|
||||
if ($chain && count($chain) === 1) {
|
||||
$p = $chain[0];
|
||||
json_response([
|
||||
'has_update' => true,
|
||||
'latest_version' => $latestVersion,
|
||||
'current_version' => $currentVersion,
|
||||
'update_type' => 'incremental',
|
||||
'download_url' => $p['download_url'],
|
||||
'file_size' => (string)$p['file_size'],
|
||||
'total_size' => (string)$p['file_size'],
|
||||
'changelog' => $p['changelog'] ?? '',
|
||||
'threads' => (int)$latestRow['threads'],
|
||||
'silent' => $silent,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($chain && count($chain) > 1) {
|
||||
$totalSize = 0;
|
||||
$updates = [];
|
||||
foreach ($chain as $p) {
|
||||
$totalSize += (int)$p['file_size'];
|
||||
$updates[] = [
|
||||
'from_version' => $p['from_version'],
|
||||
'to_version' => $p['to_version'],
|
||||
'download_url' => $p['download_url'],
|
||||
'file_size' => (string)$p['file_size'],
|
||||
'changelog' => $p['changelog'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
json_response([
|
||||
'has_update' => true,
|
||||
'latest_version' => $latestVersion,
|
||||
'current_version' => $currentVersion,
|
||||
'update_type' => 'multi_incremental',
|
||||
'total_size' => (string)$totalSize,
|
||||
'changelog' => '多版本增量更新',
|
||||
'threads' => (int)$latestRow['threads'],
|
||||
'incremental_updates' => $updates,
|
||||
'silent' => $silent,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
json_response([
|
||||
'has_update' => true,
|
||||
'latest_version' => $latestVersion,
|
||||
'current_version' => $currentVersion,
|
||||
'update_type' => 'full',
|
||||
'download_url' => $latestRow['download_url'],
|
||||
'file_size' => (string)$latestRow['file_size'],
|
||||
'total_size' => (string)$latestRow['file_size'],
|
||||
'changelog' => $latestRow['changelog'] ?? '',
|
||||
'threads' => (int)$latestRow['threads'],
|
||||
'silent' => $silent,
|
||||
]);
|
||||
}
|
||||
|
||||
function buildPatchChain($patches, $from, $to) {
|
||||
$graph = [];
|
||||
foreach ($patches as $p) {
|
||||
$graph[$p['from_version']][] = $p;
|
||||
}
|
||||
|
||||
$visited = [];
|
||||
$queue = [[$from, []]];
|
||||
|
||||
while (!empty($queue)) {
|
||||
list($current, $path) = array_shift($queue);
|
||||
|
||||
if ($current === $to) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
if (isset($visited[$current])) continue;
|
||||
$visited[$current] = true;
|
||||
|
||||
if (isset($graph[$current])) {
|
||||
foreach ($graph[$current] as $edge) {
|
||||
if (!isset($visited[$edge['to_version']])) {
|
||||
$newPath = $path;
|
||||
$newPath[] = $edge;
|
||||
$queue[] = [$edge['to_version'], $newPath];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user