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' => '无参数', ], 'reembed_chunks' => [ 'name' => 'reembed_chunks', 'file' => 'reembed_chunks.php', 'label' => '重新生成向量', 'description' => '对 chunks 重新执行 embedding,支持 resume 与 --reset。', 'doc_name' => 'reembed_chunks.md', 'args_hint' => '--archive_uid=01... 或 --reset', ], 'reindex_opensearch' => [ 'name' => 'reindex_opensearch', 'file' => 'reindex_opensearch.php', 'label' => '重建 OpenSearch 索引', 'description' => '把 PostgreSQL 中已向量化的数据重新写入 OpenSearch。', 'doc_name' => 'reindex_opensearch.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', ], ]; } }