Initial commit: R0Installer source, patch generator, API backend and build scripts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 18:03:44 +08:00
commit 6124d6060c
33 changed files with 8389 additions and 0 deletions
+7
View File
@@ -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]
+21
View File
@@ -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/ 下载');
}
+743
View File
@@ -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>项目配置 JSONinstall / 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
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "r0/installer-api",
"description": "R0Installer 云端配置 API",
"require": {
"php": ">=7.4",
"catfan/medoo": "^2.1"
}
}
+16
View File
@@ -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');
+158
View File
@@ -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
View File
@@ -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;
}
+25
View File
@@ -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
View File
@@ -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;
}