proofdb/app/service/AdminConsole/MaintenanceScriptService.php
2026-05-08 00:05:51 +08:00

149 lines
5.1 KiB
PHP

<?php
namespace app\service\AdminConsole;
use RuntimeException;
class MaintenanceScriptService
{
private const ARG_PATTERN = '/^--[A-Za-z0-9_]+(?:=[A-Za-z0-9._:@\/-]+)?$/';
public function list(): array
{
$docs = new AdminDocService();
$items = [];
foreach ($this->definitions() as $definition) {
$item = $definition;
try {
$doc = $docs->readScriptDoc($definition['doc_name']);
$item['doc_title'] = $doc['title'];
$item['doc_html'] = $doc['html'];
$item['doc_content'] = $doc['content'];
} catch (RuntimeException) {
$item['doc_title'] = null;
$item['doc_html'] = null;
$item['doc_content'] = null;
}
$items[] = $item;
}
return $items;
}
public function describe(string $name): array
{
$definitions = $this->definitions();
if (!isset($definitions[$name])) {
throw new RuntimeException('Script is not allowed.');
}
foreach ($this->list() as $item) {
if ($item['name'] === $name) {
return $item;
}
}
throw new RuntimeException('Script metadata not found.');
}
public function run(string $name, array $args = []): array
{
$definitions = $this->definitions();
if (!isset($definitions[$name])) {
throw new RuntimeException('Script is not allowed.');
}
$script = $definitions[$name];
$scriptPath = base_path('scripts/' . $script['file']);
if (!is_file($scriptPath)) {
throw new RuntimeException('Script file not found.');
}
$safeArgs = [];
foreach ($args as $arg) {
$arg = trim((string) $arg);
if ($arg === '') {
continue;
}
if (!preg_match(self::ARG_PATTERN, $arg)) {
throw new RuntimeException('Only --key=value style arguments are allowed.');
}
$safeArgs[] = $arg;
}
$command = array_merge([PHP_BINARY, $scriptPath], $safeArgs);
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptors, $pipes, base_path());
if (!is_resource($process)) {
throw new RuntimeException('Failed to start script process.');
}
fclose($pipes[0]);
$stdout = (string) stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stderr = (string) stream_get_contents($pipes[2]);
fclose($pipes[2]);
$exitCode = proc_close($process);
return [
'script_name' => $name,
'command' => array_merge(['php', 'scripts/' . $script['file']], $safeArgs),
'exit_code' => $exitCode,
'stdout' => $stdout,
'stderr' => $stderr,
'ok' => $exitCode === 0,
];
}
private function definitions(): array
{
return [
'setup_database' => [
'name' => 'setup_database',
'file' => 'setup_database.php',
'label' => '初始化数据库',
'description' => '创建或补齐 archives、chunks 相关表结构与索引。',
'doc_name' => 'setup_database.md',
'args_hint' => '无参数',
],
'setup_opensearch' => [
'name' => 'setup_opensearch',
'file' => 'setup_opensearch.php',
'label' => '初始化 OpenSearch',
'description' => '创建或补齐 proofdb_chunks 索引与 mapping。',
'doc_name' => 'setup_opensearch.md',
'args_hint' => '无参数',
],
'reindex_opensearch' => [
'name' => 'reindex_opensearch',
'file' => 'reindex_opensearch.php',
'label' => '重建 OpenSearch 索引',
'description' => '把 PostgreSQL 中已向量化的数据重新写入 OpenSearch。',
'doc_name' => 'reindex_opensearch.md',
'args_hint' => '--archive_uid=01...',
],
'backfill_archive_content' => [
'name' => 'backfill_archive_content',
'file' => 'backfill_archive_content.php',
'label' => '回填 archive content',
'description' => '从 raw 或 chunks 回填 archives.content。',
'doc_name' => 'backfill_archive_content.md',
'args_hint' => '--archive_uid=01...',
],
'setup_admin_users' => [
'name' => 'setup_admin_users',
'file' => 'setup_admin_users.php',
'label' => '初始化管理员用户',
'description' => '创建 admin_users 表并写入或更新管理员账号。',
'doc_name' => 'setup_admin_users.md',
'args_hint' => '--username=admin --password=secret',
],
];
}
}