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
+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;
}