restore bootstrap and seo helper chain after rebase drop

This commit is contained in:
root
2026-04-15 14:00:19 +08:00
parent 887e95286f
commit 0794c903d7
147 changed files with 43977 additions and 646 deletions

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use think\App;
require dirname(__DIR__) . '/vendor/autoload.php';
$app = new App(dirname(__DIR__) . '/');
$app->initialize();
return $app;

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBatchTemplateHelper.php';
use app\common\helper\DomainBootstrapBatchTemplateHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_batch_template.php [--output-root=/abs/path]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_batch_template.php --output-root=/tmp/domain-bootstrap-batch-template\n";
}
$args = $argv;
array_shift($args);
$outputRoot = dirname(__DIR__) . '/public/_admin_templates/domain-bootstrap-batch';
foreach ($args as $arg) {
if (str_starts_with($arg, '--output-root=')) {
$outputRoot = rtrim(trim(substr($arg, strlen('--output-root='))), '/');
continue;
}
}
try {
$result = DomainBootstrapBatchTemplateHelper::writeTemplate($outputRoot);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBundleIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
use app\common\helper\DomainBootstrapBundleIndexHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_bundle_index.php --scan-root=/abs/path [--output-root=/abs/path] [--portal-root=/abs/path] [--allow-missing=1]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_bundle_index.php --scan-root=/tmp/domain-bundles --output-root=/tmp/domain-bundles-index --portal-root=public/_seo_copy_release\n";
}
$args = $argv;
array_shift($args);
$scanRoot = '';
$outputRoot = '';
$portalRoot = '';
$allowMissing = false;
foreach ($args as $arg) {
if (str_starts_with($arg, '--scan-root=')) {
$scanRoot = trim(substr($arg, strlen('--scan-root=')));
continue;
}
if (str_starts_with($arg, '--output-root=')) {
$outputRoot = trim(substr($arg, strlen('--output-root=')));
continue;
}
if (str_starts_with($arg, '--portal-root=')) {
$portalRoot = trim(substr($arg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($arg, '--allow-missing=')) {
$allowMissing = in_array(strtolower(trim(substr($arg, strlen('--allow-missing=')))), ['1', 'true', 'yes', 'on'], true);
continue;
}
}
if ($scanRoot === '') {
printUsage();
exit(1);
}
if ($outputRoot === '') {
$outputRoot = rtrim($scanRoot, '/') . '/_index';
}
try {
$summary = DomainBootstrapBundleIndexHelper::buildSummary($scanRoot, $allowMissing);
$artifacts = DomainBootstrapBundleIndexHelper::writeArtifacts($outputRoot, $summary);
$portalArtifacts = [];
if ($portalRoot !== '' && strtolower($portalRoot) !== 'off') {
$portalArtifacts = DomainBootstrapBundleIndexHelper::writeArtifacts(rtrim($portalRoot, '/') . '/bootstrap', $summary);
SeoCopyPortalHomeHelper::writeArtifacts($portalRoot, SeoCopyPortalHomeHelper::buildSummary($portalRoot));
}
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
echo json_encode([
'scan_root' => $scanRoot,
'output_root' => $outputRoot,
'bundles_count' => (int)($summary['bundles_count'] ?? 0),
'json' => $artifacts['json'] ?? '',
'html' => $artifacts['html'] ?? '',
'portal_root' => $portalRoot,
'portal_json' => $portalArtifacts['json'] ?? '',
'portal_html' => $portalArtifacts['html'] ?? '',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapEnvTemplateHelper.php';
use app\common\helper\DomainBootstrapEnvTemplateHelper;
function domainBootstrapEnvApplyUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_env_apply.php [--template=baseline|testing-safe|production-safe|/abs/path] [--env-file=.env] [--dry-run=1] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_env_apply.php --template=testing-safe --env-file=.env --dry-run=1 --format=text\n";
echo " php scripts/domain_bootstrap_env_apply.php --template=production-safe --env-file=.env --dry-run=0 --format=text\n";
}
function domainBootstrapEnvApplyParseBool(string $strValue): bool
{
return in_array(strtolower(trim($strValue)), ['1', 'true', 'yes', 'on'], true);
}
$arrArgs = $argv;
array_shift($arrArgs);
$arrOptions = [
'template' => 'baseline',
'env_file' => dirname(__DIR__) . '/.env',
'dry_run' => true,
'format' => 'json',
'code_root' => dirname(__DIR__),
'source' => 'cli',
'record_latest' => true,
];
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--template=')) {
$arrOptions['template'] = trim(substr($strArg, strlen('--template=')));
continue;
}
if (str_starts_with($strArg, '--env-file=')) {
$arrOptions['env_file'] = trim(substr($strArg, strlen('--env-file=')));
continue;
}
if (str_starts_with($strArg, '--dry-run=')) {
$arrOptions['dry_run'] = domainBootstrapEnvApplyParseBool(substr($strArg, strlen('--dry-run=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$arrOptions['format'] = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
try {
$arrSummary = DomainBootstrapEnvTemplateHelper::apply($arrOptions);
if (($arrOptions['format'] ?? 'json') === 'text') {
echo 'status=' . (string)($arrSummary['status'] ?? '') . PHP_EOL;
echo 'template=' . (string)($arrSummary['template'] ?? '') . PHP_EOL;
echo 'template_path=' . (string)($arrSummary['template_path'] ?? '') . PHP_EOL;
echo 'env_file=' . (string)($arrSummary['env_file'] ?? '') . PHP_EOL;
echo 'parsed_assignment_count=' . (int)($arrSummary['parsed_assignment_count'] ?? 0) . PHP_EOL;
echo 'ignored_assignment_count=' . (int)($arrSummary['ignored_assignment_count'] ?? 0) . PHP_EOL;
echo 'changed_count=' . (int)($arrSummary['changed_count'] ?? 0) . PHP_EOL;
echo 'added_count=' . (int)($arrSummary['added_count'] ?? 0) . PHP_EOL;
echo 'updated_count=' . (int)($arrSummary['updated_count'] ?? 0) . PHP_EOL;
foreach ((array)($arrSummary['changes'] ?? []) as $intIndex => $arrChange) {
$strPrefix = 'change' . ($intIndex + 1);
echo $strPrefix . '_key=' . (string)($arrChange['key'] ?? '') . PHP_EOL;
echo $strPrefix . '_type=' . (string)($arrChange['type'] ?? '') . PHP_EOL;
echo $strPrefix . '_old=' . (string)($arrChange['old'] ?? '') . PHP_EOL;
echo $strPrefix . '_new=' . (string)($arrChange['new'] ?? '') . PHP_EOL;
}
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
} catch (Throwable $throwable) {
if (($arrOptions['format'] ?? 'json') === 'text') {
fwrite(STDERR, 'error=' . $throwable->getMessage() . PHP_EOL);
domainBootstrapEnvApplyUsage();
exit(1);
}
echo json_encode([
'status' => 'error',
'message' => $throwable->getMessage(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit(1);
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapImportHelper.php';
use app\common\helper\DomainBootstrapImportHelper;
function domainBootstrapImportAiPrintUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_import_ai.php <bundle-root|bundle-file> <ai-result-dir> [--format=json|text] [--index-root=/abs/path] [--portal-root=/abs/path|off]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_import_ai.php /tmp/domain-bootstrap/chuanjiafeng-compact /tmp/ai-results --format=text\n";
}
function domainBootstrapImportAiRenderText(array $arrSummary): string
{
$arrLines = [
'status=' . (string)($arrSummary['status'] ?? ''),
'bundle_root=' . (string)($arrSummary['bundle_root'] ?? ''),
'source_dir=' . (string)($arrSummary['source_dir'] ?? ''),
'target_root=' . (string)($arrSummary['target_root'] ?? ''),
'imported_count=' . (int)($arrSummary['imported_count'] ?? 0),
'skipped_count=' . (int)($arrSummary['skipped_count'] ?? 0),
'message=' . (string)($arrSummary['message'] ?? ''),
];
foreach ((array)($arrSummary['bundle_artifacts'] ?? []) as $strKey => $mValue) {
if (is_array($mValue)) {
continue;
}
$arrLines[] = 'artifact_' . $strKey . '=' . (string)$mValue;
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 2) {
domainBootstrapImportAiPrintUsage();
exit(1);
}
$strInput = '';
$strSourceDir = '';
$strFormat = 'json';
$strIndexRoot = '';
$strPortalRoot = '';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if (str_starts_with($strArg, '--index-root=')) {
$strIndexRoot = trim(substr($strArg, strlen('--index-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if ($strInput === '') {
$strInput = trim($strArg);
continue;
}
if ($strSourceDir === '') {
$strSourceDir = trim($strArg);
}
}
if ($strInput === '' || $strSourceDir === '') {
domainBootstrapImportAiPrintUsage();
exit(1);
}
try {
$arrSummary = DomainBootstrapImportHelper::runBundleImport($strInput, $strSourceDir, [
'index_root' => $strIndexRoot,
'portal_root' => $strPortalRoot,
]);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
exit(1);
}
if ($strFormat === 'text') {
echo domainBootstrapImportAiRenderText($arrSummary);
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapApplyHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBundleIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapDutyHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapExecutionHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapHandoffHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapOncallHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapOncallService.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapPublishHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRegisterHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunService.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyBatchPrepareHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseRunHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyFactsBuilder.php';
use app\common\helper\DomainBootstrapOncallService;
use think\App;
function domainBootstrapOncallPrintUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_oncall_run.php [--host=example.com] [--owner=站点入库] [--priority=P1] [--limit=2] [--dry-run=1] [--allow-attention=0] [--allow-prepare=0] [--allow-release-dry-run=0] [--allow-publish=0] [--bundle-root=/abs/path] [--run-root=/abs/path] [--portal-root=/abs/path] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_oncall_run.php --dry-run=1 --format=text\n";
echo " php scripts/domain_bootstrap_oncall_run.php --limit=2 --dry-run=1 --format=text\n";
echo " php scripts/domain_bootstrap_oncall_run.php --host=demo.example.com --dry-run=0 --format=text\n";
}
function domainBootstrapOncallParseBool(string $strValue): bool
{
return in_array(strtolower(trim($strValue)), ['1', 'true', 'yes', 'on'], true);
}
$arrArgs = $argv;
array_shift($arrArgs);
$arrOptions = [
'host' => '',
'owner' => '',
'priority' => '',
'limit' => 1,
'dry_run' => true,
'allow_attention' => false,
'allow_prepare' => false,
'allow_release_dry_run' => false,
'allow_publish' => false,
'bundle_root' => dirname(__DIR__) . '/storage/domain_bootstrap_bundles',
'run_root' => dirname(__DIR__) . '/storage/domain_bootstrap_runs',
'portal_root' => dirname(__DIR__) . '/public/_seo_copy_release',
'format' => 'json',
];
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--host=')) {
$arrOptions['host'] = trim(substr($strArg, strlen('--host=')));
continue;
}
if (str_starts_with($strArg, '--owner=')) {
$arrOptions['owner'] = trim(substr($strArg, strlen('--owner=')));
continue;
}
if (str_starts_with($strArg, '--priority=')) {
$arrOptions['priority'] = strtoupper(trim(substr($strArg, strlen('--priority='))));
continue;
}
if (str_starts_with($strArg, '--limit=')) {
$arrOptions['limit'] = max(1, (int)trim(substr($strArg, strlen('--limit='))));
continue;
}
if (str_starts_with($strArg, '--dry-run=')) {
$arrOptions['dry_run'] = domainBootstrapOncallParseBool(substr($strArg, strlen('--dry-run=')));
continue;
}
if (str_starts_with($strArg, '--allow-attention=')) {
$arrOptions['allow_attention'] = domainBootstrapOncallParseBool(substr($strArg, strlen('--allow-attention=')));
continue;
}
if (str_starts_with($strArg, '--allow-prepare=')) {
$arrOptions['allow_prepare'] = domainBootstrapOncallParseBool(substr($strArg, strlen('--allow-prepare=')));
continue;
}
if (str_starts_with($strArg, '--allow-release-dry-run=')) {
$arrOptions['allow_release_dry_run'] = domainBootstrapOncallParseBool(substr($strArg, strlen('--allow-release-dry-run=')));
continue;
}
if (str_starts_with($strArg, '--allow-publish=')) {
$arrOptions['allow_publish'] = domainBootstrapOncallParseBool(substr($strArg, strlen('--allow-publish=')));
continue;
}
if (str_starts_with($strArg, '--bundle-root=')) {
$arrOptions['bundle_root'] = rtrim(trim(substr($strArg, strlen('--bundle-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$arrOptions['run_root'] = rtrim(trim(substr($strArg, strlen('--run-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$arrOptions['portal_root'] = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$arrOptions['format'] = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
try {
(new App())->initialize();
$arrDispatch = DomainBootstrapOncallService::dispatch($arrOptions);
if ((int)($arrDispatch['processed_count'] ?? 0) <= 0) {
throw new RuntimeException('No matching oncall entry found.');
}
$arrExecutions = array_values(array_filter((array)($arrDispatch['executions'] ?? []), 'is_array'));
if (($arrOptions['format'] ?? 'json') === 'text') {
echo 'processed_count=' . (int)($arrDispatch['processed_count'] ?? 0) . PHP_EOL;
echo 'open_count_before=' . (int)($arrDispatch['open_count_before'] ?? 0) . PHP_EOL;
echo 'open_count_after=' . (int)($arrDispatch['open_count_after'] ?? 0) . PHP_EOL;
echo 'latest_host_after=' . (string)($arrDispatch['latest_host_after'] ?? '') . PHP_EOL;
echo 'latest_todo_after=' . (string)($arrDispatch['latest_todo_after'] ?? '') . PHP_EOL;
foreach ($arrExecutions as $intIndex => $arrExecution) {
$strPrefix = 'step' . ($intIndex + 1);
echo $strPrefix . '_host=' . (string)($arrExecution['host'] ?? '') . PHP_EOL;
echo $strPrefix . '_priority=' . (string)($arrExecution['priority_label'] ?? '') . PHP_EOL;
echo $strPrefix . '_owner=' . (string)($arrExecution['owner_label'] ?? '') . PHP_EOL;
echo $strPrefix . '_todo=' . (string)($arrExecution['todo_title'] ?? '') . PHP_EOL;
echo $strPrefix . '_status=' . (string)($arrExecution['execution_status'] ?? 'unknown') . PHP_EOL;
if (($arrExecution['run_json'] ?? '') !== '') {
echo $strPrefix . '_run_json=' . (string)($arrExecution['run_json'] ?? '') . PHP_EOL;
}
if (($arrExecution['prepare_out_dir'] ?? '') !== '') {
echo $strPrefix . '_prepare_out_dir=' . (string)($arrExecution['prepare_out_dir'] ?? '') . PHP_EOL;
}
if (($arrExecution['release_run_status'] ?? '') !== '') {
echo $strPrefix . '_release_run_status=' . (string)($arrExecution['release_run_status'] ?? '') . PHP_EOL;
}
if (($arrExecution['release_run_dir'] ?? '') !== '') {
echo $strPrefix . '_release_run_dir=' . (string)($arrExecution['release_run_dir'] ?? '') . PHP_EOL;
}
if (($arrExecution['release_run_summary_path'] ?? '') !== '') {
echo $strPrefix . '_release_run_summary_path=' . (string)($arrExecution['release_run_summary_path'] ?? '') . PHP_EOL;
}
if (($arrExecution['publish_run_status'] ?? '') !== '') {
echo $strPrefix . '_publish_run_status=' . (string)($arrExecution['publish_run_status'] ?? '') . PHP_EOL;
}
if (($arrExecution['publish_run_dir'] ?? '') !== '') {
echo $strPrefix . '_publish_run_dir=' . (string)($arrExecution['publish_run_dir'] ?? '') . PHP_EOL;
}
if (($arrExecution['publish_run_summary_path'] ?? '') !== '') {
echo $strPrefix . '_publish_run_summary_path=' . (string)($arrExecution['publish_run_summary_path'] ?? '') . PHP_EOL;
}
if (($arrExecution['message'] ?? '') !== '') {
echo $strPrefix . '_message=' . (string)($arrExecution['message'] ?? '') . PHP_EOL;
}
if (($arrExecution['recommended_command'] ?? '') !== '') {
echo $strPrefix . '_recommended_command=' . (string)($arrExecution['recommended_command'] ?? '') . PHP_EOL;
}
}
$boolBlockedOnly = !empty($arrExecutions) && count(array_filter($arrExecutions, static function (array $arrExecution): bool {
return in_array((string)($arrExecution['execution_status'] ?? ''), ['blocked_attention', 'blocked_prepare', 'blocked_publish', 'unsupported_stage'], true);
})) === count($arrExecutions);
exit($boolBlockedOnly ? 2 : 0);
}
echo json_encode([
'selected_host' => (string)($arrDispatch['selected_host'] ?? ''),
'selected_priority' => (string)($arrDispatch['selected_priority'] ?? ''),
'selected_owner' => (string)($arrDispatch['selected_owner'] ?? ''),
'selected_todo' => (string)($arrDispatch['selected_todo'] ?? ''),
'result' => $arrDispatch,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
} catch (Throwable $throwable) {
if (($arrOptions['format'] ?? 'json') === 'text') {
fwrite(STDERR, 'error=' . $throwable->getMessage() . PHP_EOL);
domainBootstrapOncallPrintUsage();
exit(1);
}
echo json_encode([
'status' => 'error',
'message' => $throwable->getMessage(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit(1);
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapApplyHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapBundleIndexHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapCompletedHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapDutyHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapExecutionHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapHandoffHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapImportHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapOncallHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapOncallService.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapPipelineHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapPublishHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapRegisterHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunIndexHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunService.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopyBatchImportHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopyBatchPrepareHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseRunHelper.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopyFactsBuilder.php';
use app\common\helper\DomainBootstrapPipelineHelper;
use think\App;
function domainBootstrapPipelinePrintUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_pipeline_run.php [--host=example.com] [--step-limit=6] [--dry-run=1] [--allow-attention=0] [--allow-prepare=1] [--allow-import=0] [--ai-result-dir=/abs/path] [--ai-result-root=/abs/path] [--allow-release-dry-run=0] [--allow-publish=0] [--bundle-root=/abs/path] [--run-root=/abs/path] [--portal-root=/abs/path] [--pipeline-run-root=/abs/path] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_pipeline_run.php --step-limit=6 --dry-run=1 --allow-prepare=1 --format=text\n";
echo " php scripts/domain_bootstrap_pipeline_run.php --host=example.com --allow-import=1 --ai-result-dir=/tmp/ai-results --allow-release-dry-run=1 --format=text\n";
echo " php scripts/domain_bootstrap_pipeline_run.php --host=demo.example.com --step-limit=3 --dry-run=0 --format=text\n";
}
function domainBootstrapPipelineParseBool(string $strValue): bool
{
return in_array(strtolower(trim($strValue)), ['1', 'true', 'yes', 'on'], true);
}
$arrArgs = $argv;
array_shift($arrArgs);
$arrOptions = [
'host' => '',
'owner' => '',
'priority' => '',
'step_limit' => 6,
'dry_run' => true,
'allow_attention' => false,
'allow_prepare' => true,
'allow_import' => false,
'ai_result_dir' => '',
'ai_result_root' => '',
'allow_release_dry_run' => false,
'allow_publish' => false,
'bundle_root' => dirname(__DIR__) . '/storage/domain_bootstrap_bundles',
'run_root' => dirname(__DIR__) . '/storage/domain_bootstrap_runs',
'portal_root' => dirname(__DIR__) . '/public/_seo_copy_release',
'pipeline_run_root' => dirname(__DIR__) . '/storage/domain_bootstrap_pipeline_runs',
'format' => 'json',
];
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--host=')) {
$arrOptions['host'] = trim(substr($strArg, strlen('--host=')));
continue;
}
if (str_starts_with($strArg, '--owner=')) {
$arrOptions['owner'] = trim(substr($strArg, strlen('--owner=')));
continue;
}
if (str_starts_with($strArg, '--priority=')) {
$arrOptions['priority'] = strtoupper(trim(substr($strArg, strlen('--priority='))));
continue;
}
if (str_starts_with($strArg, '--step-limit=')) {
$arrOptions['step_limit'] = max(1, (int)trim(substr($strArg, strlen('--step-limit='))));
continue;
}
if (str_starts_with($strArg, '--dry-run=')) {
$arrOptions['dry_run'] = domainBootstrapPipelineParseBool(substr($strArg, strlen('--dry-run=')));
continue;
}
if (str_starts_with($strArg, '--allow-attention=')) {
$arrOptions['allow_attention'] = domainBootstrapPipelineParseBool(substr($strArg, strlen('--allow-attention=')));
continue;
}
if (str_starts_with($strArg, '--allow-prepare=')) {
$arrOptions['allow_prepare'] = domainBootstrapPipelineParseBool(substr($strArg, strlen('--allow-prepare=')));
continue;
}
if (str_starts_with($strArg, '--allow-import=')) {
$arrOptions['allow_import'] = domainBootstrapPipelineParseBool(substr($strArg, strlen('--allow-import=')));
continue;
}
if (str_starts_with($strArg, '--ai-result-dir=')) {
$arrOptions['ai_result_dir'] = rtrim(trim(substr($strArg, strlen('--ai-result-dir='))), '/');
continue;
}
if (str_starts_with($strArg, '--ai-result-root=')) {
$arrOptions['ai_result_root'] = rtrim(trim(substr($strArg, strlen('--ai-result-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--allow-release-dry-run=')) {
$arrOptions['allow_release_dry_run'] = domainBootstrapPipelineParseBool(substr($strArg, strlen('--allow-release-dry-run=')));
continue;
}
if (str_starts_with($strArg, '--allow-publish=')) {
$arrOptions['allow_publish'] = domainBootstrapPipelineParseBool(substr($strArg, strlen('--allow-publish=')));
continue;
}
if (str_starts_with($strArg, '--bundle-root=')) {
$arrOptions['bundle_root'] = rtrim(trim(substr($strArg, strlen('--bundle-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$arrOptions['run_root'] = rtrim(trim(substr($strArg, strlen('--run-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$arrOptions['portal_root'] = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--pipeline-run-root=')) {
$arrOptions['pipeline_run_root'] = rtrim(trim(substr($strArg, strlen('--pipeline-run-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--format=')) {
$arrOptions['format'] = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
try {
(new App())->initialize();
$arrSummary = DomainBootstrapPipelineHelper::run($arrOptions);
if (($arrOptions['format'] ?? 'json') === 'text') {
echo 'status=' . (string)($arrSummary['status'] ?? '') . PHP_EOL;
echo 'stop_reason=' . (string)($arrSummary['stop_reason'] ?? '') . PHP_EOL;
echo 'processed_count=' . (int)($arrSummary['processed_count'] ?? 0) . PHP_EOL;
echo 'open_count_before=' . (int)($arrSummary['open_count_before'] ?? 0) . PHP_EOL;
echo 'open_count_after=' . (int)($arrSummary['open_count_after'] ?? 0) . PHP_EOL;
echo 'latest_host_after=' . (string)($arrSummary['latest_host_after'] ?? '') . PHP_EOL;
echo 'latest_todo_after=' . (string)($arrSummary['latest_todo_after'] ?? '') . PHP_EOL;
echo 'latest_completed_host=' . (string)($arrSummary['latest_completed_host'] ?? '') . PHP_EOL;
echo 'pipeline_json=' . (string)(($arrSummary['artifacts'] ?? [])['json'] ?? '') . PHP_EOL;
foreach ((array)($arrSummary['steps'] ?? []) as $intIndex => $arrStep) {
$strPrefix = 'step' . ($intIndex + 1);
echo $strPrefix . '_host=' . (string)($arrStep['host'] ?? '') . PHP_EOL;
echo $strPrefix . '_stage=' . (string)($arrStep['handoff_stage_label'] ?? '') . PHP_EOL;
echo $strPrefix . '_todo=' . (string)($arrStep['todo_title'] ?? '') . PHP_EOL;
echo $strPrefix . '_status=' . (string)($arrStep['execution_status'] ?? '') . PHP_EOL;
if (($arrStep['import_status'] ?? '') !== '') {
echo $strPrefix . '_import_status=' . (string)($arrStep['import_status'] ?? '') . PHP_EOL;
}
if (($arrStep['import_source_dir'] ?? '') !== '') {
echo $strPrefix . '_import_source_dir=' . (string)($arrStep['import_source_dir'] ?? '') . PHP_EOL;
}
if (($arrStep['message'] ?? '') !== '') {
echo $strPrefix . '_message=' . (string)($arrStep['message'] ?? '') . PHP_EOL;
}
if (($arrStep['recommended_command'] ?? '') !== '') {
echo $strPrefix . '_recommended_command=' . (string)($arrStep['recommended_command'] ?? '') . PHP_EOL;
}
}
exit(((int)($arrSummary['processed_count'] ?? 0)) > 0 ? 0 : 2);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
} catch (Throwable $throwable) {
if (($arrOptions['format'] ?? 'json') === 'text') {
fwrite(STDERR, 'error=' . $throwable->getMessage() . PHP_EOL);
domainBootstrapPipelinePrintUsage();
exit(1);
}
echo json_encode([
'status' => 'error',
'message' => $throwable->getMessage(),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit(1);
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/app/common/helper/DomainBootstrapPublishHelper.php';
use app\common\helper\DomainBootstrapPublishHelper;
function domainBootstrapPublishPrintUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_publish.php <bundle-root|bundle-file> [--target-root=/abs/path] [--index-root=/abs/path] [--portal-root=/abs/path|off] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_publish.php /tmp/domain-bootstrap/chuanjiafeng-compact --format=text\n";
}
function domainBootstrapPublishRenderText(array $arrSummary): string
{
$arrLines = [
'status=' . (string)($arrSummary['status'] ?? ''),
'bundle_root=' . (string)($arrSummary['bundle_root'] ?? ''),
'published_count=' . (int)($arrSummary['published_count'] ?? 0),
'run_dir=' . (string)($arrSummary['run_dir'] ?? ''),
'run_summary_path=' . (string)($arrSummary['run_summary_path'] ?? ''),
'target_root=' . (string)($arrSummary['target_root'] ?? ''),
'message=' . (string)($arrSummary['message'] ?? ''),
];
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
domainBootstrapPublishPrintUsage();
exit(1);
}
$strInput = '';
$strFormat = 'json';
$strTargetRoot = '';
$strIndexRoot = '';
$strPortalRoot = '';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = trim(substr($strArg, strlen('--target-root=')));
continue;
}
if (str_starts_with($strArg, '--index-root=')) {
$strIndexRoot = trim(substr($strArg, strlen('--index-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if ($strInput === '') {
$strInput = trim($strArg);
}
}
if ($strInput === '') {
domainBootstrapPublishPrintUsage();
exit(1);
}
try {
$arrSummary = DomainBootstrapPublishHelper::runBundlePublish($strInput, [
'target_root' => $strTargetRoot,
'index_root' => $strIndexRoot,
'portal_root' => $strPortalRoot,
]);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
exit(1);
}
if ($strFormat === 'text') {
echo domainBootstrapPublishRenderText($arrSummary);
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapExecutionHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRegisterHelper.php';
use app\common\helper\DomainBootstrapExecutionHelper;
use app\common\helper\DomainBootstrapRegisterHelper;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_register.php <site-register.sample.json> [--dry-run=1] [--format=json|text] [--index-root=/abs/path] [--portal-root=/abs/path|off] [--write-result=1]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_register.php /tmp/seo-domain-bootstrap/site-register.sample.json --dry-run=1\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strRegisterPath = '';
$boolDryRun = false;
$strFormat = 'json';
$strIndexRoot = '';
$strPortalRoot = '';
$boolWriteResult = true;
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--dry-run=')) {
$boolDryRun = in_array(strtolower(trim(substr($strArg, strlen('--dry-run=')))), ['1', 'true', 'yes', 'on'], true);
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if (str_starts_with($strArg, '--index-root=')) {
$strIndexRoot = trim(substr($strArg, strlen('--index-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--write-result=')) {
$boolWriteResult = in_array(strtolower(trim(substr($strArg, strlen('--write-result=')))), ['1', 'true', 'yes', 'on'], true);
continue;
}
if ($strRegisterPath === '') {
$strRegisterPath = trim($strArg);
}
}
if ($strRegisterPath === '' || !is_file($strRegisterPath)) {
fwrite(STDERR, "Register file not found: {$strRegisterPath}\n");
printUsage();
exit(1);
}
try {
(new App())->initialize();
$arrPayload = DomainBootstrapRegisterHelper::loadRegisterPayload($strRegisterPath);
$arrSummary = $boolDryRun
? DomainBootstrapRegisterHelper::inspectPayload($arrPayload)
: DomainBootstrapRegisterHelper::createDomain($arrPayload);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
exit(1);
}
$arrOutput = [
'register_path' => $strRegisterPath,
'dry_run' => $boolDryRun,
'status' => $arrSummary['status'] ?? 'unknown',
'message' => $arrSummary['message'] ?? '',
'domain_id' => (int)($arrSummary['domain_id'] ?? 0),
'found' => (bool)($arrSummary['found'] ?? false),
'template_id' => (int)($arrSummary['template_id'] ?? 0),
'template_found' => (bool)($arrSummary['template_found'] ?? false),
'template_code' => (string)($arrSummary['template_code'] ?? ''),
'create_fields' => (int)($arrSummary['create_fields'] ?? 0),
'payload' => $arrSummary['payload'] ?? [],
];
if ($boolWriteResult) {
$strBundleRoot = DomainBootstrapExecutionHelper::inferBundleRoot($strRegisterPath);
if ($strBundleRoot !== '') {
$arrResult = $arrOutput + ['executed_at' => date(DATE_ATOM)];
$arrOutput['bundle_artifacts'] = DomainBootstrapExecutionHelper::persistResult(
$strBundleRoot,
'register',
$arrResult,
$strIndexRoot !== '' ? $strIndexRoot : null,
$strPortalRoot !== '' ? $strPortalRoot : null
);
}
}
if ($strFormat === 'text') {
foreach ($arrOutput as $strKey => $mValue) {
if (is_array($mValue)) {
echo $strKey . ': ' . json_encode($mValue, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
continue;
}
echo $strKey . ': ' . (is_bool($mValue) ? ($mValue ? 'true' : 'false') : $mValue) . PHP_EOL;
}
exit(0);
}
echo json_encode($arrOutput, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapApplyHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapExecutionHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRegisterHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunHelper.php';
use app\common\helper\DomainBootstrapApplyHelper;
use app\common\helper\DomainBootstrapExecutionHelper;
use app\common\helper\DomainBootstrapRegisterHelper;
use app\common\helper\DomainBootstrapRunHelper;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_run.php <bundle-root|site-register.sample.json|site-bootstrap.sample.json> [--dry-run=1] [--register=1] [--apply=1] [--run-root=/abs/path] [--index-root=/abs/path] [--portal-root=/abs/path|off] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_run.php /tmp/domain-bootstrap-bundles/chuanjiafeng-compact --dry-run=1 --portal-root=public/_seo_copy_release --format=text\n";
}
function parseBool(string $strValue): bool
{
return in_array(strtolower(trim($strValue)), ['1', 'true', 'yes', 'on'], true);
}
function runRegisterStep(string $strRegisterPath, bool $boolDryRun, ?string $strIndexRoot, ?string $strPortalRoot): array
{
$arrPayload = DomainBootstrapRegisterHelper::loadRegisterPayload($strRegisterPath);
$arrSummary = $boolDryRun
? DomainBootstrapRegisterHelper::inspectPayload($arrPayload)
: DomainBootstrapRegisterHelper::createDomain($arrPayload);
$arrOutput = [
'register_path' => $strRegisterPath,
'dry_run' => $boolDryRun,
'status' => $arrSummary['status'] ?? 'unknown',
'message' => $arrSummary['message'] ?? '',
'domain_id' => (int)($arrSummary['domain_id'] ?? 0),
'found' => (bool)($arrSummary['found'] ?? false),
'template_id' => (int)($arrSummary['template_id'] ?? 0),
'template_found' => (bool)($arrSummary['template_found'] ?? false),
'template_code' => (string)($arrSummary['template_code'] ?? ''),
'create_fields' => (int)($arrSummary['create_fields'] ?? 0),
'payload' => $arrSummary['payload'] ?? [],
'executed_at' => date(DATE_ATOM),
];
$strBundleRoot = DomainBootstrapExecutionHelper::inferBundleRoot($strRegisterPath);
if ($strBundleRoot !== '') {
$arrOutput['bundle_artifacts'] = DomainBootstrapExecutionHelper::persistResult(
$strBundleRoot,
'register',
$arrOutput,
$strIndexRoot,
$strPortalRoot
);
}
return $arrOutput;
}
function runApplyStep(string $strApplyPath, bool $boolDryRun, ?string $strIndexRoot, ?string $strPortalRoot): array
{
$arrPayload = DomainBootstrapApplyHelper::loadBootstrapPayload($strApplyPath);
$arrSummary = $boolDryRun
? DomainBootstrapApplyHelper::inspectPayload($arrPayload)
: DomainBootstrapApplyHelper::applyPayload($arrPayload);
$arrOutput = [
'bootstrap_path' => $strApplyPath,
'dry_run' => $boolDryRun,
'status' => $arrSummary['status'] ?? 'unknown',
'message' => $arrSummary['message'] ?? '',
'domain_id' => (int)($arrSummary['domain_id'] ?? 0),
'found' => (bool)($arrSummary['found'] ?? false),
'changed_fields' => (int)($arrSummary['changed_fields'] ?? 0),
'diff' => $arrSummary['diff'] ?? [],
'payload' => $arrSummary['payload'] ?? [],
'executed_at' => date(DATE_ATOM),
];
$strBundleRoot = DomainBootstrapExecutionHelper::inferBundleRoot($strApplyPath);
if ($strBundleRoot !== '') {
$arrOutput['bundle_artifacts'] = DomainBootstrapExecutionHelper::persistResult(
$strBundleRoot,
'apply',
$arrOutput,
$strIndexRoot,
$strPortalRoot
);
}
return $arrOutput;
}
function inferOverallStatus(array $arrRegisterResult, array $arrApplyResult, bool $boolDryRun): string
{
$strRegisterStatus = (string)($arrRegisterResult['status'] ?? '');
$strApplyStatus = (string)($arrApplyResult['status'] ?? '');
if ($boolDryRun) {
if (
in_array($strRegisterStatus, ['', 'domain_exists', 'ready_to_create'], true)
&& in_array($strApplyStatus, ['', 'dry_run', 'noop', 'domain_not_found'], true)
) {
return 'dry_run_completed';
}
return 'dry_run_needs_attention';
}
if (
in_array($strRegisterStatus, ['', 'created', 'domain_exists'], true)
&& in_array($strApplyStatus, ['', 'applied', 'noop'], true)
) {
return 'completed';
}
return 'needs_attention';
}
$arrArgs = $argv;
array_shift($arrArgs);
$strInput = '';
$boolDryRun = true;
$boolRunRegister = true;
$boolRunApply = true;
$strRunRoot = dirname(__DIR__) . '/storage/domain_bootstrap_runs';
$strIndexRoot = '';
$strPortalRoot = '';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--dry-run=')) {
$boolDryRun = parseBool(substr($strArg, strlen('--dry-run=')));
continue;
}
if (str_starts_with($strArg, '--register=')) {
$boolRunRegister = parseBool(substr($strArg, strlen('--register=')));
continue;
}
if (str_starts_with($strArg, '--apply=')) {
$boolRunApply = parseBool(substr($strArg, strlen('--apply=')));
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = rtrim(trim(substr($strArg, strlen('--run-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--index-root=')) {
$strIndexRoot = rtrim(trim(substr($strArg, strlen('--index-root='))), '/');
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strInput === '') {
$strInput = trim($strArg);
}
}
if ($strInput === '' || (!$boolRunRegister && !$boolRunApply)) {
printUsage();
exit(1);
}
$strBundleRoot = DomainBootstrapRunHelper::resolveBundleRoot($strInput);
if ($strBundleRoot === '') {
fwrite(STDERR, "Bundle root not found from input: {$strInput}\n");
printUsage();
exit(1);
}
$arrBundleSummary = DomainBootstrapRunHelper::loadBundleSummary($strBundleRoot);
if (empty($arrBundleSummary)) {
fwrite(STDERR, "bootstrap-summary.json not found in bundle: {$strBundleRoot}\n");
exit(1);
}
$arrPaths = DomainBootstrapRunHelper::extractBundlePaths($arrBundleSummary, $strBundleRoot);
$strHost = (string)($arrBundleSummary['host'] ?? '');
$strIndexRoot = $strIndexRoot !== '' ? $strIndexRoot : (string)($arrBundleSummary['bundle_index']['scan_root'] ?? '');
$strPortalRoot = $strPortalRoot !== '' ? $strPortalRoot : (string)($arrBundleSummary['bundle_portal']['root'] ?? '');
$arrRegisterResult = [];
$arrApplyResult = [];
$arrErrors = [];
try {
(new App())->initialize();
if ($boolRunRegister) {
$arrRegisterResult = runRegisterStep(
$arrPaths['register_sample'],
$boolDryRun,
$strIndexRoot !== '' ? $strIndexRoot : null,
$strPortalRoot !== '' ? $strPortalRoot : null
);
}
if ($boolRunApply) {
$arrApplyResult = runApplyStep(
$arrPaths['apply_sample'],
$boolDryRun,
$strIndexRoot !== '' ? $strIndexRoot : null,
$strPortalRoot !== '' ? $strPortalRoot : null
);
}
} catch (Throwable $throwable) {
$arrErrors[] = $throwable->getMessage();
}
$arrRunSummary = [
'input' => $strInput,
'bundle_root' => $strBundleRoot,
'summary_path' => $arrPaths['summary_path'],
'host' => $strHost,
'dry_run' => $boolDryRun,
'run_register' => $boolRunRegister,
'run_apply' => $boolRunApply,
'register_result' => $arrRegisterResult,
'apply_result' => $arrApplyResult,
'errors' => $arrErrors,
'executed_at' => date(DATE_ATOM),
];
$arrRunSummary['overall_status'] = !empty($arrErrors)
? 'failed'
: inferOverallStatus($arrRegisterResult, $arrApplyResult, $boolDryRun);
$arrRunArtifacts = DomainBootstrapRunHelper::writeRunSummary($strRunRoot, $strHost, $arrRunSummary);
$arrRunSummary['run_artifacts'] = $arrRunArtifacts;
DomainBootstrapRunHelper::persistBundleRunMeta($strBundleRoot, $arrRunArtifacts, $arrRunSummary);
if ($strFormat === 'text') {
echo 'overall_status=' . $arrRunSummary['overall_status'] . PHP_EOL;
echo 'host=' . $strHost . PHP_EOL;
echo 'bundle_root=' . $strBundleRoot . PHP_EOL;
echo 'dry_run=' . ($boolDryRun ? 'yes' : 'no') . PHP_EOL;
echo 'register_status=' . (string)($arrRegisterResult['status'] ?? '') . PHP_EOL;
echo 'apply_status=' . (string)($arrApplyResult['status'] ?? '') . PHP_EOL;
echo 'run_json=' . (string)($arrRunArtifacts['json'] ?? '') . PHP_EOL;
if (!empty($arrErrors)) {
foreach ($arrErrors as $strError) {
echo 'error=' . $strError . PHP_EOL;
}
}
exit(!empty($arrErrors) ? 1 : 0);
}
echo json_encode($arrRunSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
use app\common\helper\DomainBootstrapRunIndexHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_bootstrap_run_index.php --scan-root=/abs/path [--output-root=/abs/path] [--portal-root=/abs/path] [--allow-missing=1]\n\n";
echo "Example:\n";
echo " php scripts/domain_bootstrap_run_index.php --scan-root=storage/domain_bootstrap_runs --portal-root=public/_seo_copy_release\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strScanRoot = '';
$strOutputRoot = '';
$strPortalRoot = '';
$boolAllowMissing = false;
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--scan-root=')) {
$strScanRoot = trim(substr($strArg, strlen('--scan-root=')));
continue;
}
if (str_starts_with($strArg, '--output-root=')) {
$strOutputRoot = trim(substr($strArg, strlen('--output-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--allow-missing=')) {
$boolAllowMissing = in_array(strtolower(trim(substr($strArg, strlen('--allow-missing=')))), ['1', 'true', 'yes', 'on'], true);
continue;
}
}
if ($strScanRoot === '') {
printUsage();
exit(1);
}
if ($strOutputRoot === '') {
$strOutputRoot = rtrim($strScanRoot, '/') . '/_index';
}
try {
$arrSummary = DomainBootstrapRunIndexHelper::buildSummary($strScanRoot, $boolAllowMissing);
$arrArtifacts = DomainBootstrapRunIndexHelper::writeArtifacts($strOutputRoot, $arrSummary);
$arrPortalArtifacts = [];
if ($strPortalRoot !== '' && strtolower($strPortalRoot) !== 'off') {
$arrPortalArtifacts = DomainBootstrapRunIndexHelper::writeArtifacts(rtrim($strPortalRoot, '/') . '/bootstrap/runs', $arrSummary);
SeoCopyPortalHomeHelper::writeArtifacts($strPortalRoot, SeoCopyPortalHomeHelper::buildSummary($strPortalRoot));
}
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
echo json_encode([
'scan_root' => $strScanRoot,
'output_root' => $strOutputRoot,
'runs_count' => (int)($arrSummary['runs_count'] ?? 0),
'json' => $arrArtifacts['json'] ?? '',
'html' => $arrArtifacts['html'] ?? '',
'portal_root' => $strPortalRoot,
'portal_json' => $arrPortalArtifacts['json'] ?? '',
'portal_html' => $arrPortalArtifacts['html'] ?? '',
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapExecutionHelper.php';
use app\common\helper\DomainBootstrapExecutionHelper;
use app\common\helper\DomainBootstrapApplyHelper;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/domain_seo_bootstrap_apply.php <site-bootstrap.sample.json> [--dry-run=1] [--format=json|text] [--index-root=/abs/path] [--portal-root=/abs/path|off] [--write-result=1]\n\n";
echo "Example:\n";
echo " php scripts/domain_seo_bootstrap_apply.php /tmp/seo-domain-bootstrap/site-bootstrap.sample.json --dry-run=1\n";
}
function toJson(array $data): string
{
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
function renderText(array $summary): string
{
$lines = [
'status=' . ($summary['status'] ?? ''),
'domain=' . ($summary['payload']['d_domain'] ?? ''),
'found=' . (($summary['found'] ?? false) ? 'yes' : 'no'),
'dry_run=' . (($summary['dry_run'] ?? false) ? 'yes' : 'no'),
'changed_fields=' . count((array)($summary['diff'] ?? [])),
];
foreach ((array)($summary['diff'] ?? []) as $field => $values) {
$lines[] = '- ' . $field;
}
if (!empty($summary['message'])) {
$lines[] = 'message=' . $summary['message'];
}
return implode(PHP_EOL, $lines) . PHP_EOL;
}
$args = $argv;
array_shift($args);
if (empty($args)) {
printUsage();
exit(1);
}
$bootstrapPath = '';
$dryRun = false;
$format = 'json';
$indexRoot = '';
$portalRoot = '';
$writeResult = true;
foreach ($args as $arg) {
if (str_starts_with($arg, '--dry-run=')) {
$dryRun = in_array(strtolower(trim(substr($arg, strlen('--dry-run=')))), ['1', 'true', 'yes', 'on'], true);
continue;
}
if (str_starts_with($arg, '--format=')) {
$format = strtolower(trim(substr($arg, strlen('--format='))));
continue;
}
if (str_starts_with($arg, '--index-root=')) {
$indexRoot = trim(substr($arg, strlen('--index-root=')));
continue;
}
if (str_starts_with($arg, '--portal-root=')) {
$portalRoot = trim(substr($arg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($arg, '--write-result=')) {
$writeResult = in_array(strtolower(trim(substr($arg, strlen('--write-result=')))), ['1', 'true', 'yes', 'on'], true);
continue;
}
if ($bootstrapPath === '') {
$bootstrapPath = trim($arg);
}
}
if ($bootstrapPath === '' || !is_file($bootstrapPath)) {
fwrite(STDERR, "Bootstrap file not found: {$bootstrapPath}\n");
printUsage();
exit(1);
}
try {
$payload = DomainBootstrapApplyHelper::loadBootstrapPayload($bootstrapPath);
} catch (\Throwable $Throwable) {
fwrite(STDERR, $Throwable->getMessage() . PHP_EOL);
exit(1);
}
(new App())->initialize();
$summary = [
'bootstrap_path' => $bootstrapPath,
'dry_run' => $dryRun,
];
$summary = array_merge(
$summary,
$dryRun ? DomainBootstrapApplyHelper::inspectPayload($payload) : DomainBootstrapApplyHelper::applyPayload($payload)
);
if ($writeResult) {
$bundleRoot = DomainBootstrapExecutionHelper::inferBundleRoot($bootstrapPath);
if ($bundleRoot !== '') {
$summary['bundle_artifacts'] = DomainBootstrapExecutionHelper::persistResult(
$bundleRoot,
'apply',
$summary + ['executed_at' => date(DATE_ATOM)],
$indexRoot !== '' ? $indexRoot : null,
$portalRoot !== '' ? $portalRoot : null
);
}
}
echo $format === 'text' ? renderText($summary) : toJson($summary);

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyApprovedManifestHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
use app\common\helper\SeoCopyApprovedManifestHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_approved_index.php [--approved-dir=/abs/path] [--output-root=/abs/path] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_approved_index.php --approved-dir=data/seo_copy_jobs/approved --output-root=public/_seo_copy_release/approved\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strApprovedDir = dirname(__DIR__) . '/data/seo_copy_jobs/approved';
$strOutputRoot = $strApprovedDir;
$strPortalRoot = '';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--approved-dir=')) {
$strApprovedDir = trim(substr($strArg, strlen('--approved-dir=')));
continue;
}
if (str_starts_with($strArg, '--output-root=')) {
$strOutputRoot = trim(substr($strArg, strlen('--output-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
try {
$arrSummary = SeoCopyApprovedManifestHelper::buildSummary($strApprovedDir, true);
$boolPortalMode = $strPortalRoot !== '' && str_starts_with(rtrim($strOutputRoot, '/'), rtrim($strPortalRoot, '/') . '/approved');
SeoCopyApprovedManifestHelper::writeArtifacts($strOutputRoot, $arrSummary, $boolPortalMode);
if ($strPortalRoot === '' && basename(rtrim($strOutputRoot, '/')) === 'approved') {
$strPortalRoot = dirname(rtrim($strOutputRoot, '/'));
}
if ($strPortalRoot !== '') {
SeoCopyPortalHomeHelper::writeArtifacts($strPortalRoot, SeoCopyPortalHomeHelper::buildSummary($strPortalRoot));
}
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
if ($strFormat === 'text') {
echo SeoCopyApprovedManifestHelper::renderTextSummary($arrSummary);
exit(0);
}
echo json_encode([
'approved_dir' => realpath($strApprovedDir) ?: $strApprovedDir,
'output_root' => realpath($strOutputRoot) ?: $strOutputRoot,
'approved_index_json' => rtrim($strOutputRoot, '/') . '/approved-index.json',
'approved_index_html' => rtrim($strOutputRoot, '/') . '/approved-index.html',
'manifests' => (int)($arrSummary['manifests_count'] ?? 0),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,339 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SiteStyle.php';
require dirname(__DIR__) . '/app/common/helper/UrlBuilder.php';
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
use app\common\helper\SiteStyle;
use app\common\helper\UrlBuilder;
use app\model\DomainModel;
use app\model\VideoModel;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_batch_front_verify.php <manifest.json> [--target-root=/abs/path] [--base-url=http://127.0.0.1:8910] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_batch_front_verify.php data/seo_copy_jobs/examples/chuanjiafeng-batch.json --base-url=http://127.0.0.1:8910 --format=text\n";
}
function ensureAppInitialized(): void
{
static $boolInitialized = false;
if ($boolInitialized) {
return;
}
(new App())->initialize();
$boolInitialized = true;
}
function buildScenePath(string $strHost, string $strScene, array $arrPageParts): string
{
ensureAppInitialized();
/** @var DomainModel|null $DomainRow */
$DomainRow = app(DomainModel::class)->where('d_domain', $strHost)->find();
if (empty($DomainRow)) {
return '';
}
$TpStyle = SiteStyle::getConfig($DomainRow, $strHost);
$UrlBuilder = new UrlBuilder($TpStyle);
if (in_array($strScene, ['detail', 'forge', 'play'], true)) {
$intVideoId = (int)($arrPageParts[0] ?? 0);
$arrVideo = VideoModel::getInstance()->findOne(['v_id' => $intVideoId], [
'typeMap' => [
'root' => 'array',
'document' => 'array',
'array' => 'array',
],
'projection' => [
'_id' => 0,
'v_name_en' => 1,
],
]) ?? [];
$strSlug = trim((string)($arrVideo['v_name_en'] ?? ''));
return match ($strScene) {
'detail' => $UrlBuilder->detail($strSlug, $intVideoId),
'forge' => $UrlBuilder->detailForge($strSlug, $intVideoId, max(1, (int)($arrPageParts[1] ?? 1))),
'play' => $UrlBuilder->play($strSlug, $intVideoId, (string)($arrPageParts[1] ?? ''), max(1, (int)($arrPageParts[2] ?? 1))),
default => '',
};
}
return match ($strScene) {
'home' => $UrlBuilder->home(),
'category_index' => $UrlBuilder->categoryParent((string)($arrPageParts[0] ?? '')),
'category_list' => $UrlBuilder->categoryChild((string)($arrPageParts[0] ?? ''), (string)($arrPageParts[1] ?? ''), max(1, (int)($arrPageParts[2] ?? 1))),
'search' => $UrlBuilder->searchResult((string)($arrPageParts[0] ?? '')),
'rank_index' => $UrlBuilder->rankIndex(),
'rank_list' => $UrlBuilder->rankList((string)($arrPageParts[0] ?? 'daily')),
default => '',
};
}
function buildNeedles(string $strScene, array $arrData): array
{
return match ($strScene) {
'home',
'category_index',
'category_list',
'search',
'rank_index',
'rank_list' => array_values(array_filter([
trim((string)($arrData['intro_text'] ?? '')),
trim((string)($arrData['intro_meta'] ?? '')),
trim((string)($arrData['guide_cards'][0]['title'] ?? '')),
])),
'detail' => array_values(array_filter([
trim((string)($arrData['detail_body_lead'] ?? '')),
trim((string)($arrData['detail_play_link_lead'] ?? '')),
])),
'forge' => array_values(array_filter([
trim((string)($arrData['forge_detail_note'] ?? '')),
trim((string)($arrData['forge_body_lead'] ?? '')),
])),
'play' => array_values(array_filter([
trim((string)($arrData['play_intro'] ?? '')),
trim((string)($arrData['play_body_lead'] ?? '')),
])),
default => [],
};
}
function fetchPage(string $strUrl, string $strHost): array
{
$arrHeaders = [
'Host: ' . $strHost,
'Connection: close',
];
$Context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", $arrHeaders),
'ignore_errors' => true,
'timeout' => 15,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
$strBody = @file_get_contents($strUrl, false, $Context);
$arrResponseHeaders = $http_response_header ?? [];
$intStatus = 0;
foreach ($arrResponseHeaders as $strHeaderLine) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})/i', $strHeaderLine, $arrMatches)) {
$intStatus = (int)($arrMatches[1] ?? 0);
}
}
return [
'status' => $intStatus,
'body' => $strBody === false ? '' : $strBody,
];
}
function normalizeForMatch(string $strValue): string
{
$strValue = html_entity_decode($strValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$strValue = strip_tags($strValue);
$strValue = str_replace(
["\r", "\n", "\t", '“', '”', '', '', ' '],
[' ', ' ', ' ', '"', '"', "'", "'", ' '],
$strValue
);
$strValue = preg_replace('/\s+/u', ' ', $strValue);
return trim((string)$strValue);
}
function verifyItem(string $strHost, string $strScene, array $arrPageParts, string $strTargetRoot, string $strBaseUrl): array
{
$strPageKey = SeoCopySchema::buildScenePageKey($strScene, $arrPageParts);
$arrData = SeoCopyStore::getPageDataFromRoot($strTargetRoot, $strHost, $strScene, $strPageKey);
$strPath = buildScenePath($strHost, $strScene, $arrPageParts);
if (empty($arrData)) {
return [
'host' => $strHost,
'scene' => $strScene,
'page_parts' => $arrPageParts,
'page_key' => $strPageKey,
'path' => $strPath,
'http_status' => 0,
'all_matched' => false,
'reason' => 'missing_seo_copy_data',
'checks' => [],
];
}
if ($strPath === '') {
return [
'host' => $strHost,
'scene' => $strScene,
'page_parts' => $arrPageParts,
'page_key' => $strPageKey,
'path' => '',
'http_status' => 0,
'all_matched' => false,
'reason' => 'unresolved_path',
'checks' => [],
];
}
$strUrl = rtrim($strBaseUrl, '/') . $strPath;
$arrResponse = fetchPage($strUrl, $strHost);
$strNormalizedBody = normalizeForMatch((string)($arrResponse['body'] ?? ''));
$arrNeedles = buildNeedles($strScene, $arrData);
$arrChecks = [];
foreach ($arrNeedles as $intIndex => $strNeedle) {
$arrChecks[] = [
'label' => 'needle_' . ($intIndex + 1),
'matched' => $strNeedle !== '' && str_contains($strNormalizedBody, normalizeForMatch($strNeedle)),
'preview' => mb_substr($strNeedle, 0, 120),
];
}
return [
'host' => $strHost,
'scene' => $strScene,
'page_parts' => $arrPageParts,
'page_key' => $strPageKey,
'path' => $strPath,
'http_status' => (int)($arrResponse['status'] ?? 0),
'all_matched' => (int)($arrResponse['status'] ?? 0) === 200 && !in_array(false, array_column($arrChecks, 'matched'), true),
'reason' => '',
'checks' => $arrChecks,
];
}
function renderTextSummary(array $arrSummary): string
{
$arrLines = [
'manifest: ' . $arrSummary['manifest'],
'target_root: ' . $arrSummary['target_root'],
'base_url: ' . $arrSummary['base_url'],
'total: ' . $arrSummary['total'],
'passed: ' . $arrSummary['passed'],
'failed: ' . $arrSummary['failed'],
];
if (!empty($arrSummary['items'])) {
$arrLines[] = 'items:';
foreach ($arrSummary['items'] as $arrItem) {
$arrLines[] = '- ' . ($arrItem['all_matched'] ? '[ok] ' : '[fail] ')
. $arrItem['host'] . ' / ' . $arrItem['scene'] . ' / ' . $arrItem['page_key']
. ' / status=' . $arrItem['http_status']
. ($arrItem['reason'] !== '' ? ' / reason=' . $arrItem['reason'] : '');
}
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strManifestPath = '';
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
$strBaseUrl = 'http://127.0.0.1:8910';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = substr($strArg, strlen('--target-root='));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(substr($strArg, strlen('--format=')));
continue;
}
if ($strManifestPath === '') {
$strManifestPath = $strArg;
}
}
if ($strManifestPath === '' || !is_file($strManifestPath)) {
fwrite(STDERR, "Manifest file not found: {$strManifestPath}\n");
printUsage();
exit(1);
}
$strManifestJson = (string)file_get_contents($strManifestPath);
$arrManifest = json_decode($strManifestJson, true);
if (!is_array($arrManifest)) {
fwrite(STDERR, "Manifest must be a JSON array.\n");
exit(1);
}
$arrItems = [];
$intPassed = 0;
$intFailed = 0;
foreach ($arrManifest as $arrItem) {
if (!is_array($arrItem)) {
continue;
}
$strHost = trim((string)($arrItem['host'] ?? ''));
$strScene = trim((string)($arrItem['scene'] ?? ''));
$arrPageParts = array_values((array)($arrItem['page_parts'] ?? []));
if ($strHost === '' || $strScene === '' || !in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
continue;
}
$arrResult = verifyItem($strHost, $strScene, $arrPageParts, $strTargetRoot, $strBaseUrl);
$arrItems[] = $arrResult;
if ($arrResult['all_matched']) {
$intPassed++;
} else {
$intFailed++;
}
}
$arrSummary = [
'manifest' => realpath($strManifestPath) ?: $strManifestPath,
'target_root' => $strTargetRoot,
'base_url' => $strBaseUrl,
'total' => count($arrItems),
'passed' => $intPassed,
'failed' => $intFailed,
'items' => $arrItems,
];
if ($strFormat === 'text') {
echo renderTextSummary($arrSummary);
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require_once dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
use app\common\helper\SeoCopySchema;
function seoCopyBatchImportPrintUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_batch_import.php <source-dir> [--target-root=/abs/path]\n\n";
echo "Expected source structure:\n";
echo " <source-dir>/<host-dir>/<scene>/<page_key>.json\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_batch_import.php /tmp/ai-results\n";
}
function seoCopyBatchImportNormalizeGuideCards(mixed $mValue): array
{
if (!is_array($mValue)) {
return [];
}
$arrOut = [];
foreach ($mValue as $arrCard) {
if (!is_array($arrCard)) {
continue;
}
$strTitle = trim((string)($arrCard['title'] ?? ''));
$strText = trim((string)($arrCard['text'] ?? ''));
$strHref = trim((string)($arrCard['href'] ?? ''));
if ($strTitle === '' && $strText === '') {
continue;
}
$arrItem = [
'title' => $strTitle,
'text' => $strText,
];
if ($strHref !== '') {
$arrItem['href'] = $strHref;
}
$arrOut[] = $arrItem;
}
return $arrOut;
}
function seoCopyBatchImportNormalizePayload(string $strScene, array $arrPayload): array
{
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
$arrOut = [];
foreach ($arrTemplate as $strKey => $mTemplateVal) {
if ($strKey === 'guide_cards') {
$arrOut[$strKey] = seoCopyBatchImportNormalizeGuideCards($arrPayload[$strKey] ?? []);
continue;
}
$arrOut[$strKey] = trim((string)($arrPayload[$strKey] ?? ''));
}
return $arrOut;
}
function seoCopyBatchImportValidatePayload(string $strScene, array $arrPayload): array
{
$arrErrors = [];
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
foreach ($arrTemplate as $strKey => $mTemplateVal) {
if (!array_key_exists($strKey, $arrPayload)) {
$arrErrors[] = "Missing field: {$strKey}";
continue;
}
if ($strKey === 'guide_cards') {
if (!is_array($arrPayload[$strKey]) || count($arrPayload[$strKey]) === 0) {
$arrErrors[] = 'guide_cards must be a non-empty array';
}
continue;
}
if (!is_string($arrPayload[$strKey])) {
$arrErrors[] = "{$strKey} must be a string";
}
}
return $arrErrors;
}
function seoCopyBatchImportExecute(string $strSourceDir, string $strTargetRoot): array
{
$strSourceDir = rtrim($strSourceDir, '/');
$strTargetRoot = rtrim($strTargetRoot, '/');
if ($strSourceDir === '' || !is_dir($strSourceDir)) {
throw new RuntimeException('Source directory not found: ' . $strSourceDir);
}
$arrSummary = [
'source_dir' => realpath($strSourceDir) ?: $strSourceDir,
'target_root' => $strTargetRoot,
'imported' => [],
'skipped' => [],
];
$Iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($strSourceDir, FilesystemIterator::SKIP_DOTS));
foreach ($Iterator as $FileInfo) {
/** @var SplFileInfo $FileInfo */
if (!$FileInfo->isFile() || strtolower($FileInfo->getExtension()) !== 'json') {
continue;
}
if (preg_match('/\.facts\.json$/i', $FileInfo->getFilename()) === 1) {
continue;
}
$strRelative = str_replace('\\', '/', substr($FileInfo->getPathname(), strlen($strSourceDir) + 1));
$arrSegments = explode('/', $strRelative);
if (count($arrSegments) < 3) {
$arrSummary['skipped'][] = [
'file' => $FileInfo->getPathname(),
'reason' => 'invalid_path_structure',
];
continue;
}
$strHostDir = $arrSegments[0];
$strScene = $arrSegments[1];
$strFilename = $arrSegments[count($arrSegments) - 1];
$strPageKey = preg_replace('/\.json$/i', '', $strFilename);
if (!in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
$arrSummary['skipped'][] = [
'file' => $FileInfo->getPathname(),
'reason' => 'unsupported_scene',
];
continue;
}
$strJson = (string)file_get_contents($FileInfo->getPathname());
$arrPayload = json_decode($strJson, true);
if (!is_array($arrPayload)) {
$arrSummary['skipped'][] = [
'file' => $FileInfo->getPathname(),
'reason' => 'invalid_json',
];
continue;
}
$arrNormalized = seoCopyBatchImportNormalizePayload($strScene, $arrPayload);
$arrErrors = seoCopyBatchImportValidatePayload($strScene, $arrNormalized);
if (!empty($arrErrors)) {
$arrSummary['skipped'][] = [
'file' => $FileInfo->getPathname(),
'reason' => 'validation_failed',
'errors' => $arrErrors,
];
continue;
}
$strTargetDir = $strTargetRoot . '/' . $strHostDir . '/' . $strScene;
if (!is_dir($strTargetDir) && !mkdir($strTargetDir, 0777, true) && !is_dir($strTargetDir)) {
$arrSummary['skipped'][] = [
'file' => $FileInfo->getPathname(),
'reason' => 'mkdir_failed',
];
continue;
}
$strTargetPath = $strTargetDir . '/' . $strPageKey . '.json';
file_put_contents($strTargetPath, json_encode($arrNormalized, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
$arrSummary['imported'][] = [
'scene' => $strScene,
'page_key' => $strPageKey,
'target_path' => $strTargetPath,
];
}
$arrSummary['imported_count'] = count((array)$arrSummary['imported']);
$arrSummary['skipped_count'] = count((array)$arrSummary['skipped']);
return $arrSummary;
}
function seoCopyBatchImportParseCliArgs(array $arrArgs): array
{
$strSourceDir = '';
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = trim(substr($strArg, strlen('--target-root=')));
continue;
}
if ($strSourceDir === '') {
$strSourceDir = trim($strArg);
}
}
return [
'source_dir' => $strSourceDir,
'target_root' => $strTargetRoot,
];
}
function seoCopyBatchImportCliMain(array $argv): int
{
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
seoCopyBatchImportPrintUsage();
return 1;
}
$arrOptions = seoCopyBatchImportParseCliArgs($arrArgs);
$strSourceDir = (string)($arrOptions['source_dir'] ?? '');
if ($strSourceDir === '' || !is_dir($strSourceDir)) {
fwrite(STDERR, "Source directory not found: {$strSourceDir}\n");
seoCopyBatchImportPrintUsage();
return 1;
}
try {
$arrSummary = seoCopyBatchImportExecute(
$strSourceDir,
(string)($arrOptions['target_root'] ?? '')
);
} catch (Throwable $throwable) {
fwrite(STDERR, $throwable->getMessage() . PHP_EOL);
return 1;
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
return 0;
}
if (PHP_SAPI === 'cli' && realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
exit(seoCopyBatchImportCliMain($argv));
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyBatchPrepareHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyFactsBuilder.php';
use app\common\helper\SeoCopyBatchPrepareHelper;
use app\common\helper\SeoCopyFactsBuilder;
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_batch_prepare.php <manifest.json|manifest-name> [--approved-dir=/abs/path] [--out-dir=/abs/path]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_batch_prepare.php data/seo_copy_jobs/examples/chuanjiafeng-batch.json\n";
echo " php scripts/seo_copy_batch_prepare.php chuanjiafeng-core-batch1 --approved-dir=data/seo_copy_jobs/approved\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strManifestPath = '';
$strApprovedDir = dirname(__DIR__) . '/data/seo_copy_jobs/approved';
$strOutDir = '';
$boolOutDirCustom = false;
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--approved-dir=')) {
$strApprovedDir = substr($strArg, strlen('--approved-dir='));
continue;
}
if (str_starts_with($strArg, '--out-dir=')) {
$strOutDir = substr($strArg, strlen('--out-dir='));
$boolOutDirCustom = true;
continue;
}
if ($strManifestPath === '') {
$strManifestPath = $strArg;
}
}
if ($strManifestPath === '' || !is_file($strManifestPath)) {
$strManifestPath = SeoCopyBatchPrepareHelper::resolveManifestPath($strManifestPath, $strApprovedDir);
}
if ($strManifestPath === '' || !is_file($strManifestPath)) {
fwrite(STDERR, "Manifest file not found: {$strManifestPath}\n");
printUsage();
exit(1);
}
(new App())->initialize();
if (!$boolOutDirCustom) {
$strOutDir = dirname(__DIR__) . '/storage/seo_copy_batch/' . date('Ymd_His') . '_' . SeoCopyBatchPrepareHelper::buildReleaseTagFromManifestPath($strManifestPath);
}
$arrOutput = SeoCopyBatchPrepareHelper::prepareManifest($strManifestPath, [
'approved_dir' => $strApprovedDir,
'out_dir' => $strOutDir,
]);
echo json_encode($arrOutput, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,404 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyApprovedManifestHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleasePortalHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseViewHelper.php';
use app\common\helper\SeoCopyApprovedManifestHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyReleaseIndexHelper;
use app\common\helper\SeoCopyReleasePortalHelper;
use app\common\helper\SeoCopyReleaseViewHelper;
use app\common\helper\SeoCopyStore;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_batch_publish.php <manifest.json> --source-root=/abs/path [--target-root=/abs/path] [--dry-run=1] [--skip-existing=1] [--release-tag=tag] [--log-dir=/abs/path] [--approved-dir=/abs/path]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_batch_publish.php /tmp/seo-front-manifest.json --source-root=/tmp/seo-front-dst\n";
echo " php scripts/seo_copy_batch_publish.php data/seo_copy_jobs/examples/chuanjiafeng-batch.json --source-root=/tmp/seo-approved --dry-run=1\n";
}
function normalizeGuideCards(mixed $mValue): array
{
if (!is_array($mValue)) {
return [];
}
$arrOut = [];
foreach ($mValue as $arrCard) {
if (!is_array($arrCard)) {
continue;
}
$strTitle = trim((string)($arrCard['title'] ?? ''));
$strText = trim((string)($arrCard['text'] ?? ''));
$strHref = trim((string)($arrCard['href'] ?? ''));
if ($strTitle === '' && $strText === '') {
continue;
}
$arrItem = [
'title' => $strTitle,
'text' => $strText,
];
if ($strHref !== '') {
$arrItem['href'] = $strHref;
}
$arrOut[] = $arrItem;
}
return $arrOut;
}
function normalizePayload(string $strScene, array $arrPayload): array
{
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
$arrOut = [];
foreach ($arrTemplate as $strKey => $mTemplateVal) {
if ($strKey === 'guide_cards') {
$arrOut[$strKey] = normalizeGuideCards($arrPayload[$strKey] ?? []);
continue;
}
$arrOut[$strKey] = trim((string)($arrPayload[$strKey] ?? ''));
}
return $arrOut;
}
function validatePayload(string $strScene, array $arrPayload): array
{
$arrErrors = [];
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
foreach ($arrTemplate as $strKey => $mTemplateVal) {
if (!array_key_exists($strKey, $arrPayload)) {
$arrErrors[] = "Missing field: {$strKey}";
continue;
}
if ($strKey === 'guide_cards') {
if (!is_array($arrPayload[$strKey]) || count($arrPayload[$strKey]) === 0) {
$arrErrors[] = 'guide_cards must be a non-empty array';
}
continue;
}
if (!is_string($arrPayload[$strKey])) {
$arrErrors[] = "{$strKey} must be a string";
continue;
}
if (trim($arrPayload[$strKey]) === '') {
$arrErrors[] = "{$strKey} must be a non-empty string";
}
}
return $arrErrors;
}
function buildReleaseTag(string $strReleaseTag): string
{
$strReleaseTag = strtolower(trim($strReleaseTag));
$strReleaseTag = preg_replace('/[^a-z0-9\\-_]+/', '-', $strReleaseTag);
$strReleaseTag = trim((string)$strReleaseTag, '-_');
return $strReleaseTag !== '' ? $strReleaseTag : date('Ymd_His');
}
function buildManifestDigest(array $arrManifest): string
{
return sha1(json_encode($arrManifest, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
function writeJsonFile(string $strPath, array $arrData): void
{
$strDir = dirname($strPath);
if (!is_dir($strDir) && !mkdir($strDir, 0777, true) && !is_dir($strDir)) {
throw new RuntimeException('Failed to create directory: ' . $strDir);
}
file_put_contents($strPath, json_encode($arrData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
}
function writeRawFile(string $strPath, string $strContent): void
{
$strDir = dirname($strPath);
if (!is_dir($strDir) && !mkdir($strDir, 0777, true) && !is_dir($strDir)) {
throw new RuntimeException('Failed to create directory: ' . $strDir);
}
file_put_contents($strPath, $strContent);
}
function resolvePortalRoot(): string
{
$strPortalRoot = trim((string)getenv('SEO_COPY_RELEASE_PORTAL_ROOT'));
if (in_array(strtolower($strPortalRoot), ['0', 'false', 'off', 'disable', 'disabled'], true)) {
return '';
}
return $strPortalRoot !== '' ? $strPortalRoot : dirname(__DIR__) . '/public/_seo_copy_release';
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strManifestPath = '';
$strSourceRoot = '';
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
$boolDryRun = false;
$boolSkipExisting = false;
$strReleaseTag = '';
$strLogDir = dirname(__DIR__) . '/storage/seo_copy_publish_logs';
$strApprovedDir = dirname(__DIR__) . '/data/seo_copy_jobs/approved';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--source-root=')) {
$strSourceRoot = substr($strArg, strlen('--source-root='));
continue;
}
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = substr($strArg, strlen('--target-root='));
continue;
}
if (str_starts_with($strArg, '--dry-run=')) {
$boolDryRun = in_array(strtolower(substr($strArg, strlen('--dry-run='))), ['1', 'true', 'yes'], true);
continue;
}
if (str_starts_with($strArg, '--skip-existing=')) {
$boolSkipExisting = in_array(strtolower(substr($strArg, strlen('--skip-existing='))), ['1', 'true', 'yes'], true);
continue;
}
if (str_starts_with($strArg, '--release-tag=')) {
$strReleaseTag = substr($strArg, strlen('--release-tag='));
continue;
}
if (str_starts_with($strArg, '--log-dir=')) {
$strLogDir = substr($strArg, strlen('--log-dir='));
continue;
}
if (str_starts_with($strArg, '--approved-dir=')) {
$strApprovedDir = substr($strArg, strlen('--approved-dir='));
continue;
}
if ($strManifestPath === '') {
$strManifestPath = $strArg;
}
}
if ($strManifestPath === '' || !is_file($strManifestPath)) {
fwrite(STDERR, "Manifest file not found: {$strManifestPath}\n");
printUsage();
exit(1);
}
if ($strSourceRoot === '' || !is_dir($strSourceRoot)) {
fwrite(STDERR, "Source root not found: {$strSourceRoot}\n");
printUsage();
exit(1);
}
$strManifestJson = (string)file_get_contents($strManifestPath);
$arrManifest = json_decode($strManifestJson, true);
if (!is_array($arrManifest)) {
fwrite(STDERR, "Manifest must be a JSON array.\n");
exit(1);
}
$strReleaseTag = buildReleaseTag($strReleaseTag);
$strManifestDigest = buildManifestDigest($arrManifest);
$strManifestArchivePath = rtrim($strApprovedDir, '/') . '/' . $strReleaseTag . '.manifest.json';
$strPublishLogPath = rtrim($strLogDir, '/') . '/' . date('Ymd') . '/' . date('His') . '_' . $strReleaseTag . '.json';
$strBackupRoot = rtrim($strLogDir, '/') . '/' . date('Ymd') . '/backups/' . $strReleaseTag;
$strLatestIndexPath = rtrim($strLogDir, '/') . '/latest-index.json';
$strPortalRoot = resolvePortalRoot();
$arrSummary = [
'log_type' => 'publish',
'created_at' => date('c'),
'manifest' => realpath($strManifestPath) ?: $strManifestPath,
'manifest_digest' => $strManifestDigest,
'source_root' => realpath($strSourceRoot) ?: $strSourceRoot,
'target_root' => $strTargetRoot,
'dry_run' => $boolDryRun,
'skip_existing' => $boolSkipExisting,
'release_tag' => $strReleaseTag,
'manifest_archive_path' => $strManifestArchivePath,
'publish_log_path' => $strPublishLogPath,
'backup_root' => $strBackupRoot,
'latest_index_path' => $strLatestIndexPath,
'portal_root' => $strPortalRoot,
'portal_index_path' => $strPortalRoot !== '' ? rtrim($strPortalRoot, '/') . '/latest-index.html' : '',
'published_count' => 0,
'skipped_count' => 0,
'published' => [],
'skipped' => [],
];
foreach ($arrManifest as $intIndex => $arrItem) {
if (!is_array($arrItem)) {
continue;
}
$strHost = trim((string)($arrItem['host'] ?? ''));
$strScene = trim((string)($arrItem['scene'] ?? ''));
$arrPageParts = array_values((array)($arrItem['page_parts'] ?? []));
if ($strHost === '' || $strScene === '' || !in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'reason' => 'invalid_manifest_item',
];
continue;
}
$strPageKey = SeoCopySchema::buildScenePageKey($strScene, $arrPageParts);
$strSourcePath = SeoCopyStore::resolvePagePathFromRoot($strSourceRoot, $strHost, $strScene, $strPageKey);
$strTargetPath = SeoCopyStore::resolvePagePathFromRoot($strTargetRoot, $strHost, $strScene, $strPageKey);
if ($strSourcePath === '' || !is_file($strSourcePath)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'reason' => 'source_missing',
];
continue;
}
if ($boolSkipExisting && $strTargetPath !== '' && is_file($strTargetPath)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'reason' => 'target_exists',
];
continue;
}
$arrPayload = SeoCopyStore::getPageDataFromRoot($strSourceRoot, $strHost, $strScene, $strPageKey);
if (empty($arrPayload)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'reason' => 'source_unreadable',
];
continue;
}
$arrNormalized = normalizePayload($strScene, $arrPayload);
$arrErrors = validatePayload($strScene, $arrNormalized);
if (!empty($arrErrors)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'reason' => 'validation_failed',
'errors' => $arrErrors,
];
continue;
}
if (!$boolDryRun) {
$strTargetDir = dirname($strTargetPath);
if (!is_dir($strTargetDir) && !mkdir($strTargetDir, 0777, true) && !is_dir($strTargetDir)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'reason' => 'mkdir_failed',
];
continue;
}
$strAction = is_file($strTargetPath) ? 'update' : 'create';
$strBackupPath = '';
if ($strAction === 'update') {
$strRelativeBackup = ltrim(str_replace(rtrim($strTargetRoot, '/'), '', $strTargetPath), '/');
$strBackupPath = rtrim($strBackupRoot, '/') . '/' . $strRelativeBackup;
writeRawFile($strBackupPath, (string)file_get_contents($strTargetPath));
}
file_put_contents($strTargetPath, json_encode($arrNormalized, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
} else {
$strAction = is_file($strTargetPath) ? 'update' : 'create';
$strBackupPath = $strAction === 'update'
? rtrim($strBackupRoot, '/') . '/' . ltrim(str_replace(rtrim($strTargetRoot, '/'), '', $strTargetPath), '/')
: '';
}
$arrSummary['published'][] = [
'index' => $intIndex,
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'source_path' => $strSourcePath,
'target_path' => $strTargetPath,
'action' => $strAction,
'backup_path' => $strBackupPath,
'sha1' => sha1(json_encode($arrNormalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
];
}
$arrSummary['published_count'] = count($arrSummary['published']);
$arrSummary['skipped_count'] = count($arrSummary['skipped']);
if (!$boolDryRun) {
writeJsonFile($strManifestArchivePath, [
'release_tag' => $strReleaseTag,
'created_at' => date('c'),
'source_manifest' => realpath($strManifestPath) ?: $strManifestPath,
'manifest_digest' => $strManifestDigest,
'items' => $arrManifest,
]);
$arrApprovedSummary = SeoCopyApprovedManifestHelper::buildSummary($strApprovedDir, true);
SeoCopyApprovedManifestHelper::writeArtifacts($strApprovedDir, $arrApprovedSummary);
if ($strPortalRoot !== '') {
SeoCopyApprovedManifestHelper::writeArtifacts(rtrim($strPortalRoot, '/') . '/approved', $arrApprovedSummary);
}
}
writeJsonFile($strPublishLogPath, $arrSummary);
$arrLatestSummary = SeoCopyReleaseIndexHelper::buildSummary($strLogDir, true);
SeoCopyReleaseIndexHelper::writeSummary($strLatestIndexPath, $arrLatestSummary);
SeoCopyReleaseViewHelper::writeArtifacts($strLogDir, $arrLatestSummary);
if ($strPortalRoot !== '') {
SeoCopyReleasePortalHelper::writeArtifacts($strPortalRoot, $arrLatestSummary);
SeoCopyPortalHomeHelper::writeArtifacts($strPortalRoot, SeoCopyPortalHomeHelper::buildSummary($strPortalRoot));
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_batch_verify.php <target-root> [--host=host-dir] [--scene=scene] [--format=json|text]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_batch_verify.php /tmp/seo-import-dst\n";
echo " php scripts/seo_copy_batch_verify.php /tmp/seo-import-dst --host=chuanjiafeng-net --scene=play --format=text\n";
}
function summarizeFields(string $strScene, array $arrData): array
{
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
$arrMissing = [];
$arrEmpty = [];
foreach ($arrTemplate as $strField => $mTemplateVal) {
if (!array_key_exists($strField, $arrData)) {
$arrMissing[] = $strField;
continue;
}
if ($strField === 'guide_cards') {
if (!is_array($arrData[$strField]) || count($arrData[$strField]) === 0) {
$arrEmpty[] = $strField;
}
continue;
}
if (trim((string)$arrData[$strField]) === '') {
$arrEmpty[] = $strField;
}
}
return [
'missing_fields' => $arrMissing,
'empty_fields' => $arrEmpty,
];
}
function renderTextSummary(array $arrSummary): string
{
$arrLines = [
'target_root: ' . $arrSummary['target_root'],
'total_files: ' . $arrSummary['total_files'],
'checked_files: ' . $arrSummary['checked_files'],
'readable_files: ' . $arrSummary['readable_files'],
];
if (!empty($arrSummary['by_scene'])) {
$arrLines[] = 'by_scene:';
foreach ($arrSummary['by_scene'] as $strScene => $arrSceneSummary) {
$arrLines[] = '- ' . $strScene . ': total=' . $arrSceneSummary['total'] . ', readable=' . $arrSceneSummary['readable'];
}
}
if (!empty($arrSummary['issues'])) {
$arrLines[] = 'issues:';
foreach ($arrSummary['issues'] as $arrIssue) {
$arrLines[] = '- ' . $arrIssue['host'] . '/' . $arrIssue['scene'] . '/' . $arrIssue['page_key'];
if (!empty($arrIssue['missing_fields'])) {
$arrLines[] = ' missing: ' . implode(', ', $arrIssue['missing_fields']);
}
if (!empty($arrIssue['empty_fields'])) {
$arrLines[] = ' empty: ' . implode(', ', $arrIssue['empty_fields']);
}
}
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strTargetRoot = '';
$strHostFilter = '';
$strSceneFilter = '';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--host=')) {
$strHostFilter = trim(substr($strArg, strlen('--host=')));
continue;
}
if (str_starts_with($strArg, '--scene=')) {
$strSceneFilter = trim(substr($strArg, strlen('--scene=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(substr($strArg, strlen('--format=')));
continue;
}
if ($strTargetRoot === '') {
$strTargetRoot = $strArg;
}
}
if ($strTargetRoot === '' || !is_dir($strTargetRoot)) {
fwrite(STDERR, "Target root not found: {$strTargetRoot}\n");
printUsage();
exit(1);
}
$arrSummary = [
'target_root' => realpath($strTargetRoot) ?: $strTargetRoot,
'total_files' => 0,
'checked_files' => 0,
'readable_files' => 0,
'by_scene' => [],
'issues' => [],
];
$Iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($strTargetRoot, FilesystemIterator::SKIP_DOTS));
foreach ($Iterator as $FileInfo) {
/** @var SplFileInfo $FileInfo */
if (!$FileInfo->isFile() || strtolower($FileInfo->getExtension()) !== 'json') {
continue;
}
$arrSummary['total_files']++;
$strRelative = str_replace('\\', '/', substr($FileInfo->getPathname(), strlen(rtrim($strTargetRoot, '/')) + 1));
$arrSegments = explode('/', $strRelative);
if (count($arrSegments) < 3) {
continue;
}
$strHostDir = $arrSegments[0];
$strScene = $arrSegments[1];
$strPageKey = preg_replace('/\.json$/i', '', $arrSegments[count($arrSegments) - 1]);
if ($strHostFilter !== '' && $strHostFilter !== $strHostDir) {
continue;
}
if ($strSceneFilter !== '' && $strSceneFilter !== $strScene) {
continue;
}
if (!in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
continue;
}
$arrSummary['checked_files']++;
$arrSummary['by_scene'][$strScene] = $arrSummary['by_scene'][$strScene] ?? [
'total' => 0,
'readable' => 0,
];
$arrSummary['by_scene'][$strScene]['total']++;
$arrData = SeoCopyStore::getPageDataFromRoot($strTargetRoot, $strHostDir, $strScene, $strPageKey);
$arrFieldSummary = summarizeFields($strScene, $arrData);
$boolReadable = !empty($arrData) && empty($arrFieldSummary['missing_fields']);
if ($boolReadable) {
$arrSummary['readable_files']++;
$arrSummary['by_scene'][$strScene]['readable']++;
}
if (!$boolReadable || !empty($arrFieldSummary['empty_fields'])) {
$arrSummary['issues'][] = [
'host' => $strHostDir,
'scene' => $strScene,
'page_key' => $strPageKey,
'missing_fields' => $arrFieldSummary['missing_fields'],
'empty_fields' => $arrFieldSummary['empty_fields'],
];
}
}
if ($strFormat === 'text') {
echo renderTextSummary($arrSummary);
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,769 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapApplyHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBundleIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRegisterHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapRunService.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyBatchPrepareHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyFactsBuilder.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyFallbackBuilder.php';
use app\common\helper\DomainBootstrapApplyHelper;
use app\common\helper\DomainBootstrapBundleIndexHelper;
use app\common\helper\DomainBootstrapRegisterHelper;
use app\common\helper\DomainBootstrapRunService;
use app\common\helper\SeoCopyPortalHomeHelper;
use app\common\helper\SeoCopyFactsBuilder;
use app\model\DomainModel;
use app\common\helper\SeoCopyBatchPrepareHelper;
use app\common\helper\SeoCopyFallbackBuilder;
use app\common\helper\SeoCopySchema;
use think\App;
function bootstrapUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_domain_bootstrap.php <host> \\\n";
echo " --detail-id=<id> --detail-slug=<slug> --search-keyword=<keyword> \\\n";
echo " --category-parent=<parent> --category-child=<child> [--play-type=<type>] \\\n";
echo " [--play-index=1] [--forge-index=1] [--rank-slug=daily] [--manifest-name=<name>] \\\n";
echo " [--match-type=exact|wildcard] [--parent-domain=<root-domain>] [--seed-scope=domain|host] \\\n";
echo " [--strategy-profile=standard|traffic|expand|closed] \\\n";
echo " [--canonical-mode=strict|relaxed] [--play-index-mode=noindex_follow|index_follow] \\\n";
echo " [--robots-policy=default|index_follow|noindex_follow|noindex_nofollow] \\\n";
echo " [--structured-policy=full|compact|minimal] \\\n";
echo " [--template-id=<id>] [--site-name=<name>] [--site-keywords=<keywords>] [--site-description=<desc>] \\\n";
echo " [--index-root=/abs/path] \\\n";
echo " [--portal-root=/abs/path|off] \\\n";
echo " [--bootstrap-run=off|dry_run|apply] [--run-root=/abs/path] \\\n";
echo " [--target-root=/abs/path] [--approved-dir=/abs/path] [--bundle-root=/abs/path] \\\n";
echo " [--base-url=https://host] [--force]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_domain_bootstrap.php demo.example.com \\\n";
echo " --detail-id=154229 --detail-slug=xiang-feng-bu-shi-jiu-shi-ren \\\n";
echo " --search-keyword=相逢 --category-parent=dian-ying --category-child=shao-shi-dian-ying \\\n";
echo " --play-type=douban --rank-slug=daily\n";
}
function ensureDir(string $dir): void
{
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new RuntimeException('Failed to create directory: ' . $dir);
}
}
function writeJsonIfNeeded(string $path, array $data, bool $force): string
{
if (is_file($path) && !$force) {
return 'skipped';
}
ensureDir(dirname($path));
file_put_contents(
$path,
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
);
return is_file($path) ? 'written' : 'failed';
}
function writeTextFile(string $path, string $content): void
{
ensureDir(dirname($path));
file_put_contents($path, $content);
}
function exportJson(array $data): string
{
return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
function sh(string $value): string
{
return escapeshellarg($value);
}
function buildDomainBootstrapPayload(
string $host,
string $matchType,
string $parentDomain,
string $seedScope,
string $strategyProfile,
?string $canonicalMode,
?string $playIndexMode,
?string $robotsPolicy,
?string $structuredPolicy
): array {
$normalizedMatchType = DomainModel::normalizeMatchType($matchType);
$normalizedParentDomain = DomainModel::normalizeParentDomain($parentDomain);
$normalizedSeedScope = DomainModel::normalizeSeedScope($seedScope);
$normalizedDomain = DomainModel::normalizeStoredDomain($host, $normalizedMatchType);
$arrSeoCfgPayload = [
'strategy_profile' => $strategyProfile,
];
if ($canonicalMode !== null && $canonicalMode !== '') {
$arrSeoCfgPayload['canonical_mode'] = $canonicalMode;
}
if ($playIndexMode !== null && $playIndexMode !== '') {
$arrSeoCfgPayload['play_index_mode'] = $playIndexMode;
}
if ($robotsPolicy !== null && $robotsPolicy !== '') {
$arrSeoCfgPayload['robots_policy'] = $robotsPolicy;
}
if ($structuredPolicy !== null && $structuredPolicy !== '') {
$arrSeoCfgPayload['structured_data'] = [
'policy' => $structuredPolicy,
];
}
$normalizedSeoCfg = DomainModel::normalizeSeoCfg($arrSeoCfgPayload);
return [
'd_domain' => $normalizedDomain,
'd_match_type' => $normalizedMatchType,
'd_parent_domain' => $normalizedParentDomain,
'd_seed_scope' => $normalizedSeedScope,
'd_seo_cfg' => $normalizedSeoCfg,
'_notes' => [
'导入前请补齐 t_id、d_name、关键词、描述等站点基础字段。',
'wildcard 模式建议同步确认父域名和 seed_scope避免多子域共用错误 seed。',
'本文件只负责域名 SEO 策略与匹配参数,不会替代离线补料 JSON 与 approved manifest。',
],
];
}
function buildSiteApplyPreview(array $arrPayload): array
{
try {
(new App())->initialize();
return DomainBootstrapApplyHelper::inspectPayload($arrPayload);
} catch (\Throwable $Throwable) {
return [
'status' => 'preview_unavailable',
'message' => $Throwable->getMessage(),
'found' => false,
'diff' => [],
'changed_fields' => 0,
'payload' => $arrPayload,
];
}
}
function buildSiteRegisterPayload(
string $host,
array $arrSiteStrategy,
int $intTemplateId,
string $strSiteName,
string $strSiteKeywords,
string $strSiteDescription
): array {
$strDefaultSiteName = trim($strSiteName);
if ($strDefaultSiteName === '') {
$strDefaultSiteName = DomainModel::normalizeHost($host);
}
$strDefaultKeywords = trim($strSiteKeywords);
if ($strDefaultKeywords === '') {
$strDefaultKeywords = $strDefaultSiteName;
}
$strDefaultDescription = trim($strSiteDescription);
if ($strDefaultDescription === '') {
$strDefaultDescription = $strDefaultSiteName . '内容整理与推荐。';
}
return [
'd_domain' => (string)($arrSiteStrategy['d_domain'] ?? $host),
'd_match_type' => (string)($arrSiteStrategy['d_match_type'] ?? DomainModel::MATCH_TYPE_EXACT),
'd_parent_domain' => (string)($arrSiteStrategy['d_parent_domain'] ?? ''),
'd_seed_scope' => (string)($arrSiteStrategy['d_seed_scope'] ?? DomainModel::SEED_SCOPE_DOMAIN),
'd_seo_cfg' => (array)($arrSiteStrategy['d_seo_cfg'] ?? []),
't_id' => $intTemplateId,
'd_name' => $strDefaultSiteName,
'd_keywords' => $strDefaultKeywords,
'd_description' => $strDefaultDescription,
'd_index_title' => $strDefaultSiteName . '影视内容推荐',
'd_index_keywords' => $strDefaultKeywords,
'd_index_description' => $strDefaultDescription,
'd_statis' => '',
'd_logo_type' => 0,
'd_text_logo' => $strDefaultSiteName,
'd_img_logo' => '',
'd_content_encode' => 0,
'info_id' => 0,
'd_baidu_token' => '',
'_notes' => [
'本文件用于创建 domain 记录,模板配置 t_cfg 会在创建时自动从模板表读取。',
'如目标域名已存在register dry-run 会返回 domain_exists后续直接执行 SEO 策略 apply 即可。',
'建议在正式 apply 前先检查 site_name、template_id 和首页三要素文案。',
],
];
}
function buildSiteRegisterPreview(array $arrPayload): array
{
try {
(new App())->initialize();
return DomainBootstrapRegisterHelper::inspectPayload($arrPayload);
} catch (\Throwable $Throwable) {
return [
'status' => 'preview_unavailable',
'message' => $Throwable->getMessage(),
'found' => false,
'domain_id' => 0,
'template_id' => (int)($arrPayload['t_id'] ?? 0),
'template_found' => false,
'create_fields' => 0,
'payload' => $arrPayload,
];
}
}
function renderCommandsScript(array $summary, string $codeRoot, string $manifestArg): string
{
$runDryRun = "php scripts/domain_bootstrap_run.php "
. sh((string)($summary['bundle_root'] ?? '.'))
. " --dry-run=1 --format=text";
$runApply = "php scripts/domain_bootstrap_run.php "
. sh((string)($summary['bundle_root'] ?? '.'))
. " --dry-run=0 --format=text";
$applyDryRun = "php scripts/domain_seo_bootstrap_apply.php "
. sh((string)($summary['site_bootstrap_path'] ?? ($summary['bundle_files']['site_bootstrap'] ?? 'site-bootstrap.sample.json')))
. " --dry-run=1 --format=text";
$registerDryRun = "php scripts/domain_bootstrap_register.php "
. sh((string)($summary['site_register_path'] ?? ($summary['bundle_files']['site_register'] ?? 'site-register.sample.json')))
. " --dry-run=1 --format=text";
$register = "php scripts/domain_bootstrap_register.php "
. sh((string)($summary['site_register_path'] ?? ($summary['bundle_files']['site_register'] ?? 'site-register.sample.json')))
. " --format=text";
$apply = "php scripts/domain_seo_bootstrap_apply.php "
. sh((string)($summary['site_bootstrap_path'] ?? ($summary['bundle_files']['site_bootstrap'] ?? 'site-bootstrap.sample.json')))
. " --format=text";
$prepare = "php scripts/seo_copy_batch_prepare.php "
. sh($manifestArg)
. " --approved-dir=" . sh((string)$summary['approved_dir']);
$dryRun = "php scripts/seo_copy_release_run.php "
. sh($manifestArg)
. " --approved-dir=" . sh((string)$summary['approved_dir'])
. " --source-root=" . sh((string)$summary['target_root'])
. " --base-url=" . sh((string)$summary['base_url'])
. " --dry-run=1 --format=text";
$publish = "php scripts/seo_copy_release_run.php "
. sh($manifestArg)
. " --approved-dir=" . sh((string)$summary['approved_dir'])
. " --source-root=" . sh((string)$summary['target_root'])
. " --base-url=" . sh((string)$summary['base_url'])
. " --format=text";
return implode(PHP_EOL, [
'#!/usr/bin/env bash',
'set -euo pipefail',
'',
'cd ' . sh($codeRoot),
'',
'# 1. 推荐先走高层 run 入口',
$runDryRun,
'# ' . $runApply,
'',
'# 2. 如需拆步排查,再分别执行 register/apply',
$registerDryRun,
'# ' . $register,
'',
$applyDryRun,
'# ' . $apply,
'',
'# 3. 生成 facts + prompt',
$prepare,
'',
'# 4. 跑 dry-run 门禁',
$dryRun,
'',
'# 5. dry-run 通过后正式发布',
'# ' . $publish,
'',
]);
}
function renderRunbookMarkdown(array $summary, string $manifestArg, string $commandsPath): string
{
$items = array_map(static function (array $item): string {
return '- `' . ($item['scene'] ?? '') . '` -> `' . ($item['page_key'] ?? '') . '`';
}, (array)($summary['items'] ?? []));
return implode(PHP_EOL, [
'# 新域名离线补料启动包',
'',
'## 基本信息',
'',
'- host: `' . $summary['host'] . '`',
'- host_dir: `' . $summary['host_dir'] . '`',
'- manifest: `' . $summary['manifest'] . '`',
'- target_root: `' . $summary['target_root'] . '`',
'- approved_dir: `' . $summary['approved_dir'] . '`',
'- base_url: `' . $summary['base_url'] . '`',
'- items_count: `' . $summary['items_count'] . '`',
'- site_register: `' . ($summary['bundle_files']['site_register'] ?? '') . '`',
'- site_register_preview: `' . ($summary['bundle_files']['site_register_preview'] ?? '') . '`',
'- site_bootstrap: `' . ($summary['bundle_files']['site_bootstrap'] ?? ($summary['site_bootstrap_path'] ?? '')) . '`',
'- site_apply_preview: `' . ($summary['bundle_files']['site_apply_preview'] ?? '') . '`',
'- structured_policy: `' . ($summary['site_strategy']['d_seo_cfg']['structured_data']['policy'] ?? '') . '`',
'- reference_mode: `' . ($summary['site_strategy']['d_seo_cfg']['structured_data']['reference_mode'] ?? '') . '`',
'- strategy_profile: `' . ($summary['site_strategy']['d_seo_cfg']['strategy_profile'] ?? '') . '`',
'- forge_profile: `' . ($summary['site_strategy']['d_seo_cfg']['forge']['exposure_profile'] ?? '') . '`',
'- register_status: `' . (($summary['site_register_preview']['status'] ?? 'unknown')) . '`',
'- register_template_id: `' . (($summary['site_register_preview']['template_id'] ?? 0)) . '`',
'- preview_status: `' . (($summary['site_apply_preview']['status'] ?? 'unknown')) . '`',
'- preview_changed_fields: `' . (($summary['site_apply_preview']['changed_fields'] ?? 0)) . '`',
'',
'## 本次生成的 scene',
'',
...$items,
'',
'## 建议先应用的域名策略',
'',
'- `match_type`: `' . ($summary['site_strategy']['d_match_type'] ?? '') . '`',
'- `seed_scope`: `' . ($summary['site_strategy']['d_seed_scope'] ?? '') . '`',
'- `strategy_profile`: `' . ($summary['site_strategy']['d_seo_cfg']['strategy_profile'] ?? '') . '`',
'- `canonical_mode`: `' . ($summary['site_strategy']['d_seo_cfg']['canonical_mode'] ?? '') . '`',
'- `play_index_mode`: `' . ($summary['site_strategy']['d_seo_cfg']['play_index_mode'] ?? '') . '`',
'- `robots_policy`: `' . ($summary['site_strategy']['d_seo_cfg']['robots_policy'] ?? '') . '`',
'- `forge.exposure_profile`: `' . ($summary['site_strategy']['d_seo_cfg']['forge']['exposure_profile'] ?? '') . '`',
'',
'## 下一步',
'',
'1. 优先用 `domain_bootstrap_run.php` 串行执行 `register + apply` dry-run',
'2. dry-run 无误后,再用同一个 run 入口执行正式 `register/apply`',
'3. 如遇边界问题,再退回 `domain_bootstrap_register.php / domain_seo_bootstrap_apply.php` 分步排查',
'4. 补全或改写生成出的 JSON',
'5. 跑 `prepare`',
'6. 跑 `dry-run release_run`',
'7. 通过后再正式 `publish`',
'',
'## 推荐命令',
'',
'- manifest 名称:`' . $manifestArg . '`',
'- 命令脚本:`' . $commandsPath . '`',
'',
'也可以直接执行:',
'',
'```bash',
$summary['next_commands']['run_dry_run'] ?? '',
'# ' . ($summary['next_commands']['run'] ?? ''),
$summary['next_commands']['register_dry_run'] ?? '',
'# ' . ($summary['next_commands']['register'] ?? ''),
$summary['next_commands']['apply_dry_run'] ?? '',
'# ' . ($summary['next_commands']['apply'] ?? ''),
$summary['next_commands']['prepare'] ?? '',
$summary['next_commands']['dry_run'] ?? '',
'# ' . ($summary['next_commands']['publish'] ?? ''),
'```',
'',
]);
}
$args = $argv;
array_shift($args);
if (empty($args)) {
bootstrapUsage();
exit(1);
}
$host = '';
$detailId = '';
$detailSlug = '';
$searchKeyword = '';
$categoryParent = '';
$categoryChild = '';
$playType = 'douban';
$playIndex = '1';
$forgeIndex = '1';
$rankSlug = 'daily';
$manifestName = '';
$targetRoot = dirname(__DIR__) . '/data/seo_copy';
$approvedDir = dirname(__DIR__) . '/data/seo_copy_jobs/approved';
$bundleRoot = '';
$baseUrl = '';
$matchType = DomainModel::MATCH_TYPE_EXACT;
$parentDomain = '';
$seedScope = DomainModel::SEED_SCOPE_DOMAIN;
$strategyProfile = DomainModel::SEO_STRATEGY_PROFILE_STANDARD;
$canonicalMode = null;
$playIndexMode = null;
$robotsPolicy = null;
$structuredPolicy = null;
$templateId = 0;
$siteName = '';
$siteKeywords = '';
$siteDescription = '';
$indexRoot = '';
$portalRoot = '';
$bootstrapRunMode = 'off';
$runRoot = '';
$force = false;
$customTargetRoot = false;
$customApprovedDir = false;
foreach ($args as $arg) {
if ($arg === '--force') {
$force = true;
continue;
}
if (str_starts_with($arg, '--detail-id=')) {
$detailId = trim(substr($arg, strlen('--detail-id=')));
continue;
}
if (str_starts_with($arg, '--detail-slug=')) {
$detailSlug = trim(substr($arg, strlen('--detail-slug=')));
continue;
}
if (str_starts_with($arg, '--search-keyword=')) {
$searchKeyword = trim(substr($arg, strlen('--search-keyword=')));
continue;
}
if (str_starts_with($arg, '--category-parent=')) {
$categoryParent = trim(substr($arg, strlen('--category-parent=')));
continue;
}
if (str_starts_with($arg, '--category-child=')) {
$categoryChild = trim(substr($arg, strlen('--category-child=')));
continue;
}
if (str_starts_with($arg, '--play-type=')) {
$playType = trim(substr($arg, strlen('--play-type=')));
continue;
}
if (str_starts_with($arg, '--play-index=')) {
$playIndex = trim(substr($arg, strlen('--play-index=')));
continue;
}
if (str_starts_with($arg, '--forge-index=')) {
$forgeIndex = trim(substr($arg, strlen('--forge-index=')));
continue;
}
if (str_starts_with($arg, '--rank-slug=')) {
$rankSlug = trim(substr($arg, strlen('--rank-slug=')));
continue;
}
if (str_starts_with($arg, '--manifest-name=')) {
$manifestName = trim(substr($arg, strlen('--manifest-name=')));
continue;
}
if (str_starts_with($arg, '--match-type=')) {
$matchType = trim(substr($arg, strlen('--match-type=')));
continue;
}
if (str_starts_with($arg, '--parent-domain=')) {
$parentDomain = trim(substr($arg, strlen('--parent-domain=')));
continue;
}
if (str_starts_with($arg, '--seed-scope=')) {
$seedScope = trim(substr($arg, strlen('--seed-scope=')));
continue;
}
if (str_starts_with($arg, '--strategy-profile=')) {
$strategyProfile = trim(substr($arg, strlen('--strategy-profile=')));
continue;
}
if (str_starts_with($arg, '--canonical-mode=')) {
$canonicalMode = trim(substr($arg, strlen('--canonical-mode=')));
continue;
}
if (str_starts_with($arg, '--play-index-mode=')) {
$playIndexMode = trim(substr($arg, strlen('--play-index-mode=')));
continue;
}
if (str_starts_with($arg, '--robots-policy=')) {
$robotsPolicy = trim(substr($arg, strlen('--robots-policy=')));
continue;
}
if (str_starts_with($arg, '--structured-policy=')) {
$structuredPolicy = trim(substr($arg, strlen('--structured-policy=')));
continue;
}
if (str_starts_with($arg, '--template-id=')) {
$templateId = (int)trim(substr($arg, strlen('--template-id=')));
continue;
}
if (str_starts_with($arg, '--site-name=')) {
$siteName = trim(substr($arg, strlen('--site-name=')));
continue;
}
if (str_starts_with($arg, '--site-keywords=')) {
$siteKeywords = trim(substr($arg, strlen('--site-keywords=')));
continue;
}
if (str_starts_with($arg, '--site-description=')) {
$siteDescription = trim(substr($arg, strlen('--site-description=')));
continue;
}
if (str_starts_with($arg, '--index-root=')) {
$indexRoot = rtrim(trim(substr($arg, strlen('--index-root='))), '/');
continue;
}
if (str_starts_with($arg, '--portal-root=')) {
$portalRoot = rtrim(trim(substr($arg, strlen('--portal-root='))), '/');
continue;
}
if (str_starts_with($arg, '--bootstrap-run=')) {
$bootstrapRunMode = trim(substr($arg, strlen('--bootstrap-run=')));
continue;
}
if (str_starts_with($arg, '--run-root=')) {
$runRoot = rtrim(trim(substr($arg, strlen('--run-root='))), '/');
continue;
}
if (str_starts_with($arg, '--target-root=')) {
$targetRoot = rtrim(trim(substr($arg, strlen('--target-root='))), '/');
$customTargetRoot = true;
continue;
}
if (str_starts_with($arg, '--approved-dir=')) {
$approvedDir = rtrim(trim(substr($arg, strlen('--approved-dir='))), '/');
$customApprovedDir = true;
continue;
}
if (str_starts_with($arg, '--bundle-root=')) {
$bundleRoot = rtrim(trim(substr($arg, strlen('--bundle-root='))), '/');
continue;
}
if (str_starts_with($arg, '--base-url=')) {
$baseUrl = trim(substr($arg, strlen('--base-url=')));
continue;
}
if ($host === '') {
$host = trim($arg);
}
}
if (
$host === '' ||
$detailId === '' ||
$detailSlug === '' ||
$searchKeyword === '' ||
$categoryParent === '' ||
$categoryChild === ''
) {
bootstrapUsage();
exit(1);
}
$bootstrapRunMode = trim(strtolower($bootstrapRunMode));
if (!in_array($bootstrapRunMode, ['off', 'dry_run', 'apply'], true)) {
fwrite(STDERR, "Invalid --bootstrap-run value: {$bootstrapRunMode}\n");
bootstrapUsage();
exit(1);
}
if ($bundleRoot !== '') {
if (!$customTargetRoot) {
$targetRoot = $bundleRoot . '/data/seo_copy';
}
if (!$customApprovedDir) {
$approvedDir = $bundleRoot . '/data/seo_copy_jobs/approved';
}
}
if ($bootstrapRunMode !== 'off' && $bundleRoot === '') {
fwrite(STDERR, "--bootstrap-run requires --bundle-root so the script can persist run artifacts.\n");
exit(1);
}
$hostDir = SeoCopyBatchPrepareHelper::normalizeHostDir($host);
$manifestTag = $manifestName !== ''
? preg_replace('/[^a-zA-Z0-9\-_]+/', '-', trim($manifestName))
: $hostDir . '-core-batch1';
$manifestFilename = str_ends_with($manifestTag, '.manifest.json') ? $manifestTag : $manifestTag . '.manifest.json';
$manifestPath = rtrim($approvedDir, '/') . '/' . $manifestFilename;
$baseUrl = $baseUrl !== '' ? $baseUrl : 'https://' . $host;
$codeRoot = dirname(__DIR__);
$manifestArg = preg_replace('/\.manifest\.json$/', '', $manifestFilename);
$manifestArg = preg_replace('/\.json$/', '', (string)$manifestArg);
$siteStrategy = buildDomainBootstrapPayload(
$host,
$matchType,
$parentDomain,
$seedScope,
$strategyProfile,
$canonicalMode,
$playIndexMode,
$robotsPolicy,
$structuredPolicy
);
$siteRegister = buildSiteRegisterPayload(
$host,
$siteStrategy,
$templateId,
$siteName,
$siteKeywords,
$siteDescription
);
$siteRegisterPreview = buildSiteRegisterPreview($siteRegister);
$siteApplyPreview = buildSiteApplyPreview($siteStrategy);
$items = [
['scene' => 'home', 'page_parts' => ['index']],
['scene' => 'category_index', 'page_parts' => [$categoryParent]],
['scene' => 'category_list', 'page_parts' => [$categoryParent, $categoryChild]],
['scene' => 'search', 'page_parts' => [$searchKeyword]],
['scene' => 'rank_index', 'page_parts' => ['index']],
['scene' => 'rank_list', 'page_parts' => [$rankSlug]],
['scene' => 'detail', 'page_parts' => [$detailId]],
['scene' => 'forge', 'page_parts' => [$detailId, $forgeIndex]],
['scene' => 'play', 'page_parts' => [$detailId, $playType, $playIndex]],
];
$manifest = [];
$fileResults = [];
foreach ($items as $item) {
$scene = $item['scene'];
$pageParts = $item['page_parts'];
$pageKey = SeoCopySchema::buildScenePageKey($scene, $pageParts);
$facts = SeoCopyFactsBuilder::build($host, $scene, $pageParts);
$template = SeoCopyFallbackBuilder::build($scene, $facts);
$targetPath = rtrim($targetRoot, '/') . '/' . $hostDir . '/' . $scene . '/' . $pageKey . '.json';
$status = writeJsonIfNeeded($targetPath, $template, $force);
$manifest[] = [
'host' => $host,
'scene' => $scene,
'page_parts' => $pageParts,
];
$fileResults[] = [
'scene' => $scene,
'page_key' => $pageKey,
'target_path' => $targetPath,
'status' => $status,
];
}
$manifestStatus = writeJsonIfNeeded($manifestPath, $manifest, $force);
$summary = [
'host' => $host,
'host_dir' => $hostDir,
'manifest' => $manifestPath,
'manifest_status' => $manifestStatus,
'target_root' => $targetRoot,
'approved_dir' => $approvedDir,
'bundle_root' => $bundleRoot,
'base_url' => $baseUrl,
'items_count' => count($manifest),
'items' => $fileResults,
'site_register' => $siteRegister,
'site_register_preview' => $siteRegisterPreview,
'site_strategy' => $siteStrategy,
'site_apply_preview' => $siteApplyPreview,
'next_commands' => [
'run_dry_run' => 'php scripts/domain_bootstrap_run.php ' . ($bundleRoot !== '' ? $bundleRoot : '.') . ' --dry-run=1 --format=text',
'run' => 'php scripts/domain_bootstrap_run.php ' . ($bundleRoot !== '' ? $bundleRoot : '.') . ' --dry-run=0 --format=text',
'register_dry_run' => 'php scripts/domain_bootstrap_register.php ' . ($bundleRoot !== '' ? $bundleRoot . '/site-register.sample.json' : './site-register.sample.json') . ' --dry-run=1 --format=text',
'register' => 'php scripts/domain_bootstrap_register.php ' . ($bundleRoot !== '' ? $bundleRoot . '/site-register.sample.json' : './site-register.sample.json') . ' --format=text',
'apply_dry_run' => 'php scripts/domain_seo_bootstrap_apply.php ' . ($bundleRoot !== '' ? $bundleRoot . '/site-bootstrap.sample.json' : './site-bootstrap.sample.json') . ' --dry-run=1 --format=text',
'apply' => 'php scripts/domain_seo_bootstrap_apply.php ' . ($bundleRoot !== '' ? $bundleRoot . '/site-bootstrap.sample.json' : './site-bootstrap.sample.json') . ' --format=text',
'prepare' => 'php scripts/seo_copy_batch_prepare.php ' . $manifestArg . ' --approved-dir=' . $approvedDir,
'dry_run' => 'php scripts/seo_copy_release_run.php ' . $manifestArg . ' --approved-dir=' . $approvedDir . ' --source-root=' . $targetRoot . ' --base-url=' . $baseUrl . ' --dry-run=1 --format=text',
'publish' => 'php scripts/seo_copy_release_run.php ' . $manifestArg . ' --approved-dir=' . $approvedDir . ' --source-root=' . $targetRoot . ' --base-url=' . $baseUrl . ' --format=text',
],
];
if ($bundleRoot !== '') {
ensureDir($bundleRoot);
$summaryPath = $bundleRoot . '/bootstrap-summary.json';
$commandsPath = $bundleRoot . '/commands.sh';
$runbookPath = $bundleRoot . '/README.md';
$manifestCopyPath = $bundleRoot . '/manifest.copy.json';
$siteRegisterPath = $bundleRoot . '/site-register.sample.json';
$siteRegisterPreviewPath = $bundleRoot . '/site-register.preview.json';
$siteBootstrapPath = $bundleRoot . '/site-bootstrap.sample.json';
$siteApplyPreviewPath = $bundleRoot . '/site-apply.preview.json';
$summary['site_register_path'] = $siteRegisterPath;
$summary['site_bootstrap_path'] = $siteBootstrapPath;
$summary['bundle_files'] = [
'summary' => $summaryPath,
'manifest_copy' => $manifestCopyPath,
'site_register' => $siteRegisterPath,
'site_register_preview' => $siteRegisterPreviewPath,
'site_bootstrap' => $siteBootstrapPath,
'site_apply_preview' => $siteApplyPreviewPath,
'commands' => $commandsPath,
'runbook' => $runbookPath,
];
writeTextFile($manifestCopyPath, exportJson($manifest));
writeTextFile($siteRegisterPath, exportJson($siteRegister));
writeTextFile($siteRegisterPreviewPath, exportJson($siteRegisterPreview));
writeTextFile($siteBootstrapPath, exportJson($siteStrategy));
writeTextFile($siteApplyPreviewPath, exportJson($siteApplyPreview));
writeTextFile($commandsPath, renderCommandsScript($summary, $codeRoot, $manifestArg));
@chmod($commandsPath, 0755);
writeTextFile($runbookPath, renderRunbookMarkdown($summary, $manifestArg, $commandsPath));
writeTextFile($summaryPath, exportJson($summary));
if ($indexRoot !== '') {
$arrBundleIndexSummary = DomainBootstrapBundleIndexHelper::buildSummary($indexRoot, true);
$arrBundleIndexArtifacts = DomainBootstrapBundleIndexHelper::writeArtifacts(rtrim($indexRoot, '/') . '/_index', $arrBundleIndexSummary);
$summary['bundle_index'] = [
'scan_root' => $indexRoot,
'bundles_count' => (int)($arrBundleIndexSummary['bundles_count'] ?? 0),
'json' => $arrBundleIndexArtifacts['json'] ?? '',
'html' => $arrBundleIndexArtifacts['html'] ?? '',
];
if ($portalRoot !== '' && strtolower($portalRoot) !== 'off') {
$arrPortalArtifacts = DomainBootstrapBundleIndexHelper::writeArtifacts(rtrim($portalRoot, '/') . '/bootstrap', $arrBundleIndexSummary);
SeoCopyPortalHomeHelper::writeArtifacts($portalRoot, SeoCopyPortalHomeHelper::buildSummary($portalRoot));
$summary['bundle_portal'] = [
'root' => $portalRoot,
'json' => $arrPortalArtifacts['json'] ?? '',
'html' => $arrPortalArtifacts['html'] ?? '',
];
}
writeTextFile($summaryPath, exportJson($summary));
}
if ($bootstrapRunMode !== '' && $bootstrapRunMode !== 'off') {
$boolDryRun = $bootstrapRunMode !== 'apply';
$arrRunSummary = DomainBootstrapRunService::runBundle($bundleRoot, [
'dry_run' => $boolDryRun,
'run_root' => $runRoot !== '' ? $runRoot : ($codeRoot . '/storage/domain_bootstrap_runs'),
'index_root' => $indexRoot !== '' ? $indexRoot : null,
'portal_root' => $portalRoot !== '' ? $portalRoot : null,
'run_register' => true,
'run_apply' => true,
]);
$summary['bootstrap_run_mode'] = $bootstrapRunMode;
$summary['bootstrap_run'] = $arrRunSummary;
$arrLatestBundleSummary = json_decode((string)file_get_contents($summaryPath), true);
if (is_array($arrLatestBundleSummary)) {
$summary = array_merge($summary, $arrLatestBundleSummary);
$summary['bootstrap_run_mode'] = $bootstrapRunMode;
$summary['bootstrap_run'] = $arrRunSummary;
}
writeTextFile($summaryPath, exportJson($summary));
}
}
echo exportJson($summary);

View File

@@ -0,0 +1,306 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBundleIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyBatchPrepareHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBatchTemplateHelper.php';
use app\common\helper\DomainBootstrapBatchTemplateHelper;
use app\common\helper\DomainBootstrapBundleIndexHelper;
use app\common\helper\SeoCopyBatchPrepareHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
use PhpOffice\PhpSpreadsheet\IOFactory;
function printBatchUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_domain_bootstrap_batch.php <template.xlsx> [--batch-root=/abs/path] [--index-root=/abs/path] [--portal-root=/abs/path|off] [--bootstrap-run=off|dry_run|apply] [--run-root=/abs/path] [--force]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_domain_bootstrap_batch.php /tmp/domain-bootstrap-batch-template/domain-bootstrap-batch-template.xlsx --batch-root=/tmp/domain-bootstrap-batch --index-root=/tmp/domain-bootstrap-bundles --portal-root=public/_seo_copy_release\n";
}
function ensureDirBatch(string $dir): void
{
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new RuntimeException('Failed to create directory: ' . $dir);
}
}
function hostToDir(string $host): string
{
return SeoCopyBatchPrepareHelper::normalizeHostDir($host);
}
function normalizeSheetRows(string $path): array
{
$spreadsheet = IOFactory::load($path);
$sheet = $spreadsheet->getSheet(0);
$rows = $sheet->toArray(null, true, true, false);
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
if (empty($rows)) {
return [];
}
$headers = array_map(static fn ($v): string => trim((string)$v), (array)array_shift($rows));
$normalized = [];
foreach ($rows as $row) {
$assoc = [];
$hasData = false;
foreach ($headers as $index => $header) {
if ($header === '') {
continue;
}
$value = trim((string)($row[$index] ?? ''));
$assoc[$header] = $value;
if ($value !== '') {
$hasData = true;
}
}
if ($hasData) {
$normalized[] = $assoc;
}
}
return $normalized;
}
function buildSingleCommand(array $row, string $bundleRoot, string $indexRoot, string $portalRoot, string $bootstrapRun, string $runRoot, bool $force): string
{
$codeRoot = dirname(__DIR__);
$parts = [
'php',
escapeshellarg($codeRoot . '/scripts/seo_copy_domain_bootstrap.php'),
escapeshellarg((string)$row['host']),
'--detail-id=' . escapeshellarg((string)$row['detail_id']),
'--detail-slug=' . escapeshellarg((string)$row['detail_slug']),
'--search-keyword=' . escapeshellarg((string)$row['search_keyword']),
'--category-parent=' . escapeshellarg((string)$row['category_parent']),
'--category-child=' . escapeshellarg((string)$row['category_child']),
'--bundle-root=' . escapeshellarg($bundleRoot),
'--index-root=' . escapeshellarg($indexRoot),
'--portal-root=' . escapeshellarg($portalRoot),
'--bootstrap-run=' . escapeshellarg($bootstrapRun),
];
$optionalMap = [
'play_type' => '--play-type=',
'play_index' => '--play-index=',
'forge_index' => '--forge-index=',
'rank_slug' => '--rank-slug=',
'match_type' => '--match-type=',
'parent_domain' => '--parent-domain=',
'seed_scope' => '--seed-scope=',
'strategy_profile' => '--strategy-profile=',
'template_id' => '--template-id=',
'site_name' => '--site-name=',
'site_keywords' => '--site-keywords=',
'site_description' => '--site-description=',
'base_url' => '--base-url=',
];
foreach ($optionalMap as $key => $prefix) {
$value = trim((string)($row[$key] ?? ''));
if ($value !== '') {
$parts[] = $prefix . escapeshellarg($value);
}
}
if ($runRoot !== '') {
$parts[] = '--run-root=' . escapeshellarg($runRoot);
}
if ($force) {
$parts[] = '--force';
}
return implode(' ', $parts);
}
function renderBatchSummaryHtml(array $summary): string
{
$rows = [];
foreach ((array)($summary['items'] ?? []) as $item) {
$rows[] = '<tr>'
. '<td>' . htmlspecialchars((string)($item['host'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
. '<td>' . htmlspecialchars((string)($item['bundle_root'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
. '<td>' . htmlspecialchars((string)($item['strategy_profile'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
. '<td>' . htmlspecialchars((string)($item['status'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
. '<td>' . htmlspecialchars((string)($item['message'] ?? ''), ENT_QUOTES, 'UTF-8') . '</td>'
. '<td><code>' . htmlspecialchars((string)($item['command'] ?? ''), ENT_QUOTES, 'UTF-8') . '</code></td>'
. '</tr>';
}
return implode(PHP_EOL, [
'<!doctype html>',
'<html lang="zh-CN">',
'<head>',
'<meta charset="utf-8">',
'<title>Domain Bootstrap Batch Summary</title>',
'<style>body{font-family:Arial,sans-serif;padding:24px;}table{border-collapse:collapse;width:100%;}th,td{border:1px solid #ddd;padding:8px;text-align:left;vertical-align:top;}th{background:#f6f6f6;}code{word-break:break-all;white-space:pre-wrap;}</style>',
'</head>',
'<body>',
'<h1>Domain Bootstrap Batch Summary</h1>',
'<p>template: <code>' . htmlspecialchars((string)($summary['template_path'] ?? ''), ENT_QUOTES, 'UTF-8') . '</code></p>',
'<p>batch_root: <code>' . htmlspecialchars((string)($summary['batch_root'] ?? ''), ENT_QUOTES, 'UTF-8') . '</code></p>',
'<p>processed: <strong>' . (int)($summary['processed_count'] ?? 0) . '</strong> / success: <strong>' . (int)($summary['success_count'] ?? 0) . '</strong> / failed: <strong>' . (int)($summary['failed_count'] ?? 0) . '</strong></p>',
'<table><thead><tr><th>Host</th><th>Bundle Root</th><th>Strategy</th><th>Status</th><th>Message</th><th>Command</th></tr></thead><tbody>',
...$rows,
'</tbody></table>',
'</body>',
'</html>',
]) . PHP_EOL;
}
$args = $argv;
array_shift($args);
$templatePath = '';
$batchRoot = dirname(__DIR__) . '/storage/domain_bootstrap_batch';
$indexRoot = dirname(__DIR__) . '/storage/domain_bootstrap_bundles';
$portalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$bootstrapRun = 'off';
$runRoot = '';
$force = false;
foreach ($args as $arg) {
if ($arg === '--force') {
$force = true;
continue;
}
if (str_starts_with($arg, '--batch-root=')) {
$batchRoot = rtrim(trim(substr($arg, strlen('--batch-root='))), '/');
continue;
}
if (str_starts_with($arg, '--index-root=')) {
$indexRoot = rtrim(trim(substr($arg, strlen('--index-root='))), '/');
continue;
}
if (str_starts_with($arg, '--portal-root=')) {
$portalRoot = rtrim(trim(substr($arg, strlen('--portal-root='))), '/');
continue;
}
if (str_starts_with($arg, '--bootstrap-run=')) {
$bootstrapRun = trim(substr($arg, strlen('--bootstrap-run=')));
continue;
}
if (str_starts_with($arg, '--run-root=')) {
$runRoot = rtrim(trim(substr($arg, strlen('--run-root='))), '/');
continue;
}
if ($templatePath === '') {
$templatePath = trim($arg);
}
}
if ($templatePath === '') {
printBatchUsage();
exit(1);
}
$bootstrapRun = strtolower(trim($bootstrapRun));
if (!in_array($bootstrapRun, ['off', 'dry_run', 'apply'], true)) {
fwrite(STDERR, "Invalid --bootstrap-run value: {$bootstrapRun}\n");
printBatchUsage();
exit(1);
}
if (!is_file($templatePath)) {
fwrite(STDERR, "Template file not found: {$templatePath}\n");
exit(1);
}
ensureDirBatch($batchRoot);
ensureDirBatch($indexRoot);
if ($portalRoot !== '' && strtolower($portalRoot) !== 'off') {
ensureDirBatch($portalRoot);
}
$rows = normalizeSheetRows($templatePath);
$summaryItems = [];
$successCount = 0;
$failedCount = 0;
foreach ($rows as $row) {
$host = trim((string)($row['host'] ?? ''));
if ($host === '') {
continue;
}
$hostDir = hostToDir($host);
$bundleName = trim((string)($row['bundle_name'] ?? ''));
$bundleRoot = $batchRoot . '/' . ($bundleName !== '' ? $bundleName : $hostDir);
$command = buildSingleCommand($row, $bundleRoot, $indexRoot, $portalRoot !== '' ? $portalRoot : 'off', $bootstrapRun, $runRoot, $force);
$output = [];
$exitCode = 0;
exec($command . ' 2>&1', $output, $exitCode);
$joined = trim(implode(PHP_EOL, $output));
$decoded = json_decode($joined, true);
$status = $exitCode === 0 ? 'success' : 'failed';
$message = $exitCode === 0 ? 'bundle_generated' : ($joined !== '' ? $joined : 'command_failed');
if (is_array($decoded)) {
$message = (string)($decoded['manifest_status'] ?? $message);
}
if ($exitCode === 0) {
$successCount++;
} else {
$failedCount++;
}
$summaryItems[] = [
'host' => $host,
'bundle_root' => $bundleRoot,
'strategy_profile' => (string)($row['strategy_profile'] ?? ''),
'status' => $status,
'message' => $message,
'command' => $command,
'exit_code' => $exitCode,
'output' => $output,
];
}
$bundleIndexSummary = DomainBootstrapBundleIndexHelper::buildSummary($indexRoot, true);
$bundleIndexArtifacts = DomainBootstrapBundleIndexHelper::writeArtifacts(rtrim($indexRoot, '/') . '/_index', $bundleIndexSummary);
$portalArtifacts = [];
if ($portalRoot !== '' && strtolower($portalRoot) !== 'off') {
$portalArtifacts = DomainBootstrapBundleIndexHelper::writeArtifacts(rtrim($portalRoot, '/') . '/bootstrap', $bundleIndexSummary);
SeoCopyPortalHomeHelper::writeArtifacts($portalRoot, SeoCopyPortalHomeHelper::buildSummary($portalRoot));
}
$summary = [
'template_path' => $templatePath,
'batch_root' => $batchRoot,
'index_root' => $indexRoot,
'portal_root' => $portalRoot,
'bootstrap_run' => $bootstrapRun,
'processed_count' => count($summaryItems),
'success_count' => $successCount,
'failed_count' => $failedCount,
'items' => $summaryItems,
'bundle_index' => [
'bundles_count' => (int)($bundleIndexSummary['bundles_count'] ?? 0),
'json' => $bundleIndexArtifacts['json'] ?? '',
'html' => $bundleIndexArtifacts['html'] ?? '',
],
'bundle_portal' => [
'json' => $portalArtifacts['json'] ?? '',
'html' => $portalArtifacts['html'] ?? '',
],
];
$summaryJsonPath = rtrim($batchRoot, '/') . '/batch.summary.json';
$summaryHtmlPath = rtrim($batchRoot, '/') . '/batch.summary.html';
file_put_contents($summaryJsonPath, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
file_put_contents($summaryHtmlPath, renderBatchSummaryHtml($summary));
$summary['summary_json'] = $summaryJsonPath;
$summary['summary_html'] = $summaryHtmlPath;
file_put_contents($summaryJsonPath, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainBatchImportTemplateHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainBootstrapBatchTemplateHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainSupplyBatchTemplateHelper.php';
require dirname(__DIR__) . '/app/common/helper/DomainSupplyBatchHelper.php';
use app\common\helper\DomainSupplyBatchHelper;
function printSupplyBatchUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_domain_supply_batch.php <template.xlsx> [--output-root=/abs/path] [--run-bootstrap=0|1] [--batch-root=/abs/path] [--index-root=/abs/path] [--portal-root=/abs/path|off] [--bootstrap-run=off|dry_run|apply] [--run-root=/abs/path] [--force]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_domain_supply_batch.php /tmp/domain-supply-batch-template/domain-supply-batch-template.xlsx --output-root=/tmp/domain-supply-batch --run-bootstrap=1 --portal-root=off\n";
}
$args = $argv;
array_shift($args);
$templatePath = '';
$outputRoot = dirname(__DIR__) . '/storage/domain_supply_batch';
$runBootstrap = false;
$batchRoot = '';
$indexRoot = dirname(__DIR__) . '/storage/domain_bootstrap_bundles';
$indexRootProvided = false;
$portalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$bootstrapRun = 'off';
$runRoot = '';
$force = false;
foreach ($args as $arg) {
if ($arg === '--force') {
$force = true;
continue;
}
if (str_starts_with($arg, '--output-root=')) {
$outputRoot = rtrim(trim(substr($arg, strlen('--output-root='))), '/');
continue;
}
if (str_starts_with($arg, '--run-bootstrap=')) {
$value = strtolower(trim(substr($arg, strlen('--run-bootstrap='))));
$runBootstrap = in_array($value, ['1', 'true', 'yes', 'on'], true);
continue;
}
if (str_starts_with($arg, '--batch-root=')) {
$batchRoot = rtrim(trim(substr($arg, strlen('--batch-root='))), '/');
continue;
}
if (str_starts_with($arg, '--index-root=')) {
$indexRoot = rtrim(trim(substr($arg, strlen('--index-root='))), '/');
$indexRootProvided = true;
continue;
}
if (str_starts_with($arg, '--portal-root=')) {
$portalRoot = rtrim(trim(substr($arg, strlen('--portal-root='))), '/');
continue;
}
if (str_starts_with($arg, '--bootstrap-run=')) {
$bootstrapRun = trim(substr($arg, strlen('--bootstrap-run=')));
continue;
}
if (str_starts_with($arg, '--run-root=')) {
$runRoot = rtrim(trim(substr($arg, strlen('--run-root='))), '/');
continue;
}
if (in_array($arg, ['-h', '--help'], true)) {
printSupplyBatchUsage();
exit(0);
}
if ($templatePath === '') {
$templatePath = trim($arg);
}
}
if ($templatePath === '') {
printSupplyBatchUsage();
exit(1);
}
if (!is_file($templatePath)) {
fwrite(STDERR, "Template file not found: {$templatePath}\n");
exit(1);
}
$summary = DomainSupplyBatchHelper::process($templatePath, [
'output_root' => $outputRoot,
'run_bootstrap' => $runBootstrap,
'batch_root' => $batchRoot,
'index_root' => $indexRootProvided ? $indexRoot : '',
'portal_root' => $portalRoot,
'bootstrap_run' => $bootstrapRun,
'run_root' => $runRoot,
'force' => $force,
]);
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/DomainSupplyRunIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
use app\common\helper\DomainSupplyRunIndexHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_domain_supply_orchestration_run.php [--run-root=/abs/path] [--portal-root=/abs/path] [--format=json|text]\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strRunRoot = dirname(__DIR__) . '/public/_admin_templates/domain-supply-runs';
$strPortalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = trim(substr($strArg, strlen('--run-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if (in_array($strArg, ['-h', '--help'], true)) {
printUsage();
exit(0);
}
}
$arrOpsSummary = SeoCopyPortalHomeHelper::buildSummary($strPortalRoot);
$arrSupplySummary = DomainSupplyRunIndexHelper::buildSummary($strRunRoot, 20, $arrOpsSummary);
$arrOrchestration = (array)($arrSupplySummary['orchestration_summary'] ?? []);
$arrPlan = (array)($arrOrchestration['plan'] ?? []);
$arrResult = [
'generated_at' => date('c'),
'status' => in_array((string)($arrPlan['label'] ?? ''), ['先回看任务', '先回看运行', '先排异常'], true) ? 'needs_attention' : 'success',
'hosts_count' => (int)($arrOrchestration['hosts_count'] ?? 0),
'failed_hosts_count' => count(array_filter((array)($arrSupplySummary['host_totals'] ?? []), static function (array $arrHost): bool {
return (int)($arrHost['failed_count'] ?? 0) > 0;
})),
'latest_run' => (array)($arrSupplySummary['latest_run'] ?? []),
'latest_failed_host' => (array)($arrSupplySummary['latest_failed_host'] ?? []),
'plan' => $arrPlan,
'buckets' => array_values(array_filter((array)($arrOrchestration['buckets'] ?? []), 'is_array')),
];
if ($strFormat === 'text') {
echo 'status: ' . (string)($arrResult['status'] ?? '') . PHP_EOL;
echo 'hosts_count: ' . (int)($arrResult['hosts_count'] ?? 0) . PHP_EOL;
echo 'failed_hosts_count: ' . (int)($arrResult['failed_hosts_count'] ?? 0) . PHP_EOL;
echo 'latest_run: ' . (string)(((array)($arrResult['latest_run'] ?? []))['run_id'] ?? '') . PHP_EOL;
echo 'plan_label: ' . (string)(((array)($arrResult['plan'] ?? []))['label'] ?? '') . PHP_EOL;
echo 'plan_title: ' . (string)(((array)($arrResult['plan'] ?? []))['title'] ?? '') . PHP_EOL;
echo 'plan_lead_host: ' . (string)(((array)($arrResult['plan'] ?? []))['lead_host'] ?? '') . PHP_EOL;
echo 'plan_bucket: ' . (string)(((array)($arrResult['plan'] ?? []))['bucket_key'] ?? '') . PHP_EOL;
foreach ((array)($arrResult['buckets'] ?? []) as $arrBucket) {
if (!is_array($arrBucket)) {
continue;
}
echo '- ' . (string)($arrBucket['key'] ?? '')
. ' count=' . (int)($arrBucket['count'] ?? 0)
. ' lead=' . (string)($arrBucket['lead_host'] ?? '')
. ' ops=' . (string)($arrBucket['resolved_ops_panel'] ?? '') . '/' . (string)($arrBucket['resolved_host_console_panel'] ?? '')
. PHP_EOL;
}
} else {
echo json_encode($arrResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
exit(((string)($arrResult['status'] ?? '')) === 'success' ? 0 : 1);

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyFactsBuilder.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
use app\common\helper\SeoCopyFactsBuilder;
use app\common\helper\SeoCopySchema;
use think\App;
function printUsage(): void
{
$strScenes = implode(', ', SeoCopySchema::getSupportedScenes());
echo "Usage:\n";
echo " php scripts/seo_copy_facts.php <host> <scene> <page-part> [page-part ...]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_facts.php chuanjiafeng.net home index\n";
echo " php scripts/seo_copy_facts.php chuanjiafeng.net category_list dian-ying shao-shi-dian-ying 1\n";
echo " php scripts/seo_copy_facts.php chuanjiafeng.net search 相逢 1\n";
echo " php scripts/seo_copy_facts.php chuanjiafeng.net detail 154229\n";
echo " php scripts/seo_copy_facts.php chuanjiafeng.net play 154229 douban 1\n\n";
echo "Supported scenes:\n";
echo " {$strScenes}\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 3) {
printUsage();
exit(1);
}
$strHost = (string)array_shift($arrArgs);
$strScene = (string)array_shift($arrArgs);
if (!in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
fwrite(STDERR, "Unsupported scene: {$strScene}\n");
printUsage();
exit(1);
}
(new App())->initialize();
$arrFacts = SeoCopyFactsBuilder::build($strHost, $strScene, $arrArgs);
echo json_encode($arrFacts, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,288 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SiteStyle.php';
require dirname(__DIR__) . '/app/common/helper/UrlBuilder.php';
use app\common\helper\SiteStyle;
use app\common\helper\UrlBuilder;
use app\model\DomainModel;
use app\model\VideoModel;
use app\task\logic\VideoSiteMapLogic;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_forge_output_file_verify.php <host> <video-id> [--expand-sitemap-count=2] [--format=json|text]\n";
}
function ensureAppInitialized(): void
{
static $boolInitialized = false;
if ($boolInitialized) {
return;
}
(new App())->initialize();
$boolInitialized = true;
}
function renderText(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'video_id: ' . $arrSummary['video_id'],
'slug: ' . $arrSummary['slug'],
'default_json_count: ' . $arrSummary['default_json_count'],
'expanded_json_count: ' . $arrSummary['expanded_json_count'],
'default_xml_entries: ' . $arrSummary['default_xml_entries'],
'expanded_xml_entries: ' . $arrSummary['expanded_xml_entries'],
'all_passed: ' . ($arrSummary['all_passed'] ? 'yes' : 'no'),
'checks:',
];
foreach ($arrSummary['checks'] as $arrCheck) {
$arrLines[] = '- ' . ($arrCheck['passed'] ? '[ok] ' : '[fail] ') . $arrCheck['label'] . ': ' . $arrCheck['detail'];
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
function buildProbeLogic(DomainModel $DomainModel, array $arrVideo, string $strSiteMapPath, array $arrUrlFamily): VideoSiteMapLogic
{
return new class($DomainModel, $arrVideo, $strSiteMapPath, $arrUrlFamily) extends VideoSiteMapLogic {
protected DomainModel $ProbeDomainModel;
protected array $ProbeVideo;
protected array $ProbeUrlFamily;
public function __construct(DomainModel $DomainModel, array $arrVideo, string $strSiteMapPath, array $arrUrlFamily)
{
parent::__construct();
$this->ProbeDomainModel = $DomainModel;
$this->ProbeVideo = $arrVideo;
$this->ProbeUrlFamily = $arrUrlFamily;
$this->arrDomainModel = [$DomainModel];
$this->strSiteMapPath = rtrim($strSiteMapPath, '/') . '/';
$this->intPageSize = 1;
$this->intBatchSize = 1;
}
public function fetchVideosByBatch(int $intStart, int $intCount): \Generator
{
yield $this->ProbeVideo;
}
public function getDomain(): array
{
return [$this->ProbeDomainModel];
}
public function getGptArrUrlTmp($DomainModel)
{
return $this->ProbeUrlFamily;
}
};
}
function collectFileSummary(string $strRootDir, string $strHost): array
{
$strDomainDir = rtrim($strRootDir, '/') . '/' . $strHost;
$strXmlFile = $strDomainDir . '/sitemap-videos-1.xml';
$strTxtFile = $strDomainDir . '/sitemap-videos-1.txt';
$strJsonFile = $strDomainDir . '/video-list-1.json';
$mXmlRaw = is_file($strXmlFile) ? file_get_contents($strXmlFile) : '';
$mTxtRaw = is_file($strTxtFile) ? file_get_contents($strTxtFile) : '';
$mJsonRaw = is_file($strJsonFile) ? file_get_contents($strJsonFile) : '';
$strXml = is_string($mXmlRaw) ? $mXmlRaw : '';
$strTxt = is_string($mTxtRaw) ? $mTxtRaw : '';
$strJson = is_string($mJsonRaw) ? $mJsonRaw : '';
preg_match_all('#<loc>([^<]+)</loc>#u', $strXml, $arrXmlMatches);
$arrXmlLocs = array_values(array_filter(array_map('trim', $arrXmlMatches[1] ?? [])));
$arrTxtLocs = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $strTxt) ?: [])));
preg_match_all('#"href"\s*:\s*"([^"]+)"#u', $strJson, $arrJsonMatches);
$arrJsonHrefs = array_values(array_filter(array_map(static function ($strHref) {
return stripcslashes(trim((string)$strHref));
}, $arrJsonMatches[1] ?? [])));
return [
'xml_file' => $strXmlFile,
'txt_file' => $strTxtFile,
'json_file' => $strJsonFile,
'xml_locs' => $arrXmlLocs,
'txt_locs' => $arrTxtLocs,
'json_hrefs' => $arrJsonHrefs,
];
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 2) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intExpandedSitemapCount = 2;
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--expand-sitemap-count=')) {
$intExpandedSitemapCount = max(0, (int)substr($strArg, strlen('--expand-sitemap-count=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
}
if ($strHost === '' || $intVideoId <= 0) {
printUsage();
exit(1);
}
ensureAppInitialized();
/** @var DomainModel|null $DomainRow */
$DomainRow = app(DomainModel::class)->where('d_domain', $strHost)->find();
if (empty($DomainRow)) {
fwrite(STDERR, "Domain not found: {$strHost}\n");
exit(1);
}
$arrVideo = VideoModel::getInstance()->findOne(['v_id' => $intVideoId], [
'typeMap' => [
'root' => 'array',
'document' => 'array',
'array' => 'array',
],
'projection' => [
'_id' => 0,
'v_id' => 1,
'v_name' => 1,
'v_name_en' => 1,
'v_seo_words' => 1,
],
]) ?? [];
$strSlug = trim((string)($arrVideo['v_name_en'] ?? ''));
if ($strSlug === '') {
fwrite(STDERR, "Video slug empty: {$intVideoId}\n");
exit(1);
}
$TpStyle = SiteStyle::getConfig($DomainRow, $strHost);
$UrlBuilder = new UrlBuilder($TpStyle);
$arrUrlFamily = (array)($TpStyle['template_cfg']['url_family'] ?? []);
$strDetailPath = $UrlBuilder->detail($strSlug, $intVideoId);
$arrExpectedDefaultUrls = ['https://www.' . $strHost . $strDetailPath];
$arrExpectedExpandedUrls = $arrExpectedDefaultUrls;
for ($intForgeId = 1; $intForgeId <= $intExpandedSitemapCount; $intForgeId++) {
$arrExpectedExpandedUrls[] = 'https://www.' . $strHost . $UrlBuilder->detailForge($strSlug, $intVideoId, $intForgeId);
}
$strTmpRoot = rtrim(sys_get_temp_dir(), '/') . '/forge-output-file-verify-' . date('YmdHis') . '-' . substr(md5($strHost . '|' . $intVideoId), 0, 6);
$strDefaultRoot = $strTmpRoot . '/default';
$strExpandedRoot = $strTmpRoot . '/expanded';
@mkdir($strDefaultRoot, 0755, true);
@mkdir($strExpandedRoot, 0755, true);
$DefaultDomain = $DomainRow->clone();
$arrDefaultSeoCfg = $DefaultDomain->getSeoCfg();
$arrDefaultSeoCfg['forge']['enable'] = false;
$arrDefaultSeoCfg['forge']['count'] = 0;
$arrDefaultSeoCfg['forge']['entry_count'] = 0;
$arrDefaultSeoCfg['forge']['sitemap_count'] = 0;
$DefaultDomain->setAttr('d_seo_cfg', $arrDefaultSeoCfg);
$DefaultLogic = buildProbeLogic($DefaultDomain, $arrVideo, $strDefaultRoot, $arrUrlFamily);
$ExpandedDomain = $DomainRow->clone();
$arrExpandedSeoCfg = $ExpandedDomain->getSeoCfg();
$arrExpandedSeoCfg['forge']['enable'] = true;
$arrExpandedSeoCfg['forge']['sitemap_count'] = $intExpandedSitemapCount;
$ExpandedDomain->setAttr('d_seo_cfg', $arrExpandedSeoCfg);
$ExpandedLogic = buildProbeLogic($ExpandedDomain, $arrVideo, $strExpandedRoot, $arrUrlFamily);
$GenerateMapInfo = new ReflectionMethod(VideoSiteMapLogic::class, 'generateMapInfo');
$GenerateMapInfo->setAccessible(true);
$GenerateMapInfo->invoke($DefaultLogic, 1);
$DefaultLogic->generateVideoJsonByPage(1);
$GenerateMapInfo->invoke($ExpandedLogic, 1);
$ExpandedLogic->generateVideoJsonByPage(1);
$arrDefaultFiles = collectFileSummary($strDefaultRoot, $strHost);
$arrExpandedFiles = collectFileSummary($strExpandedRoot, $strHost);
$arrDefaultJsonHrefs = (array)($arrDefaultFiles['json_hrefs'] ?? []);
$arrExpandedJsonHrefs = (array)($arrExpandedFiles['json_hrefs'] ?? []);
$arrChecks = [];
$arrChecks[] = [
'label' => 'default_xml_only_detail',
'passed' => $arrDefaultFiles['xml_locs'] === $arrExpectedDefaultUrls,
'detail' => json_encode($arrDefaultFiles['xml_locs'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'default_txt_only_detail',
'passed' => $arrDefaultFiles['txt_locs'] === $arrExpectedDefaultUrls,
'detail' => json_encode($arrDefaultFiles['txt_locs'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'default_json_only_detail',
'passed' => $arrDefaultJsonHrefs === $arrExpectedDefaultUrls,
'detail' => json_encode($arrDefaultJsonHrefs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'expanded_xml_matches_expected',
'passed' => $arrExpandedFiles['xml_locs'] === $arrExpectedExpandedUrls,
'detail' => json_encode($arrExpandedFiles['xml_locs'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'expanded_txt_matches_expected',
'passed' => $arrExpandedFiles['txt_locs'] === $arrExpectedExpandedUrls,
'detail' => json_encode($arrExpandedFiles['txt_locs'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'expanded_json_matches_expected',
'passed' => $arrExpandedJsonHrefs === $arrExpectedExpandedUrls,
'detail' => json_encode($arrExpandedJsonHrefs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$boolAllPassed = !in_array(false, array_column($arrChecks, 'passed'), true);
$arrSummary = [
'host' => $strHost,
'video_id' => $intVideoId,
'slug' => $strSlug,
'default_root' => $strDefaultRoot,
'expanded_root' => $strExpandedRoot,
'default_json_count' => count($arrDefaultJsonHrefs),
'expanded_json_count' => count($arrExpandedJsonHrefs),
'default_xml_entries' => count($arrDefaultFiles['xml_locs']),
'expanded_xml_entries' => count($arrExpandedFiles['xml_locs']),
'checks' => $arrChecks,
'all_passed' => $boolAllPassed,
'verified_at' => date(DATE_ATOM),
];
if ($strFormat === 'text') {
echo renderText($arrSummary);
exit($boolAllPassed ? 0 : 1);
}
echo json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
exit($boolAllPassed ? 0 : 1);

View File

@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SiteStyle.php';
require dirname(__DIR__) . '/app/common/helper/UrlBuilder.php';
use app\common\helper\SiteStyle;
use app\common\helper\UrlBuilder;
use app\model\DomainModel;
use app\model\VideoModel;
use app\task\logic\VideoSiteMapLogic;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_forge_output_verify.php <host> <video-id> [--expand-sitemap-count=2] [--expand-entry-count=4] [--base-url=https://host] [--format=json|text]\n";
}
function ensureAppInitialized(): void
{
static $boolInitialized = false;
if ($boolInitialized) {
return;
}
(new App())->initialize();
$boolInitialized = true;
}
function fetchPage(string $strUrl, string $strHost): array
{
$Context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", [
'Host: ' . $strHost,
'Connection: close',
]),
'ignore_errors' => true,
'timeout' => 20,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
$strBody = @file_get_contents($strUrl, false, $Context);
$arrHeaders = $http_response_header ?? [];
$intStatus = 0;
foreach ($arrHeaders as $strHeaderLine) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})/i', $strHeaderLine, $arrMatches)) {
$intStatus = (int)($arrMatches[1] ?? 0);
}
}
return [
'status' => $intStatus,
'body' => $strBody === false ? '' : $strBody,
];
}
function extractForgeEntryIds(string $strDetailPath, string $strBody): int
{
$arrMatches = [];
$strEscapedDetailPath = preg_quote($strDetailPath, '#');
// 兼容常见 detail_forge 链接样式:
// 1) /detail/slug/153511-1
// 2) /detail/slug/153511/1
// 3) /detail/slug-153511-1 或其他常见变体
$strPattern = '#href="[^"]*' . $strEscapedDetailPath
. '(?:-(?P<dashId>\d+)|/(?P<slashId>\d+)|\.(?P<extId>\d+))?"#u';
if (!preg_match_all($strPattern, $strBody, $arrMatches)) {
return 0;
}
$arrEntryIds = [];
foreach ($arrMatches['dashId'] ?? [] as $strDashId) {
$strDashId = (string)$strDashId;
if ($strDashId !== '' && ctype_digit($strDashId)) {
$arrEntryIds[(int)$strDashId] = true;
}
}
foreach ($arrMatches['slashId'] ?? [] as $strSlashId) {
$strSlashId = (string)$strSlashId;
if ($strSlashId !== '' && ctype_digit($strSlashId)) {
$arrEntryIds[(int)$strSlashId] = true;
}
}
foreach ($arrMatches['extId'] ?? [] as $strExtId) {
$strExtId = (string)$strExtId;
if ($strExtId !== '' && ctype_digit($strExtId)) {
$arrEntryIds[(int)$strExtId] = true;
}
}
return count($arrEntryIds);
}
function renderText(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'video_id: ' . $arrSummary['video_id'],
'slug: ' . $arrSummary['slug'],
'entry_count_current: ' . $arrSummary['entry_count_current'],
'entry_links_detected: ' . $arrSummary['entry_links_detected'],
'sitemap_count_current: ' . $arrSummary['sitemap_count_current'],
'current_output_kinds: ' . implode(',', $arrSummary['current_output_kinds']),
'expanded_sitemap_count: ' . $arrSummary['expanded_sitemap_count'],
'expanded_output_kinds: ' . implode(',', $arrSummary['expanded_output_kinds']),
'all_passed: ' . ($arrSummary['all_passed'] ? 'yes' : 'no'),
'checks:',
];
foreach ($arrSummary['checks'] as $arrCheck) {
$arrLines[] = '- ' . ($arrCheck['passed'] ? '[ok] ' : '[fail] ') . $arrCheck['label'] . ': ' . $arrCheck['detail'];
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 2) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intExpandedSitemapCount = 2;
$intExpandedEntryCount = 4;
$strBaseUrl = '';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--expand-sitemap-count=')) {
$intExpandedSitemapCount = max(0, (int)substr($strArg, strlen('--expand-sitemap-count=')));
continue;
}
if (str_starts_with($strArg, '--expand-entry-count=')) {
$intExpandedEntryCount = max(0, (int)substr($strArg, strlen('--expand-entry-count=')));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
}
if ($strHost === '' || $intVideoId <= 0) {
printUsage();
exit(1);
}
ensureAppInitialized();
/** @var DomainModel|null $DomainRow */
$DomainRow = app(DomainModel::class)->where('d_domain', $strHost)->find();
if (empty($DomainRow)) {
fwrite(STDERR, "Domain not found: {$strHost}\n");
exit(1);
}
$strBaseUrl = $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost);
$arrVideo = VideoModel::getInstance()->findOne(['v_id' => $intVideoId], [
'typeMap' => [
'root' => 'array',
'document' => 'array',
'array' => 'array',
],
'projection' => [
'_id' => 0,
'v_id' => 1,
'v_name' => 1,
'v_name_en' => 1,
'v_seo_words' => 1,
],
]) ?? [];
$strSlug = trim((string)($arrVideo['v_name_en'] ?? ''));
if ($strSlug === '') {
fwrite(STDERR, "Video slug empty: {$intVideoId}\n");
exit(1);
}
$Logic = app(VideoSiteMapLogic::class);
$arrUrlFamily = $Logic->getGptArrUrlTmp($DomainRow);
$TpStyle = SiteStyle::getConfig($DomainRow, $strHost);
$UrlBuilder = new UrlBuilder($TpStyle);
$ReflectionMethod = new ReflectionMethod(VideoSiteMapLogic::class, 'buildVideoOutputRows');
$ReflectionMethod->setAccessible(true);
$arrCurrentRows = $ReflectionMethod->invoke($Logic, $DomainRow, $arrUrlFamily, $arrVideo);
$DomainProbe = $DomainRow->clone();
$arrProbeSeoCfg = $DomainProbe->getSeoCfg();
$arrProbeSeoCfg['forge']['enable'] = true;
$arrProbeSeoCfg['forge']['entry_count'] = $intExpandedEntryCount;
$arrProbeSeoCfg['forge']['sitemap_count'] = $intExpandedSitemapCount;
$DomainProbe->setAttr('d_seo_cfg', $arrProbeSeoCfg);
$arrExpandedRows = $ReflectionMethod->invoke($Logic, $DomainProbe, $arrUrlFamily, $arrVideo);
$strDetailPath = $UrlBuilder->detail($strSlug, $intVideoId);
$arrDetailResponse = fetchPage($strBaseUrl . $strDetailPath, $strHost);
$intEntryLinksDetected = extractForgeEntryIds($strDetailPath, (string)$arrDetailResponse['body']);
$arrCurrentKinds = array_values(array_map(static fn(array $arrRow) => (string)($arrRow['kind'] ?? ''), $arrCurrentRows));
$arrExpandedKinds = array_values(array_map(static fn(array $arrRow) => (string)($arrRow['kind'] ?? ''), $arrExpandedRows));
$arrChecks = [];
$arrChecks[] = [
'label' => 'detail_entry_count_matches_config',
'passed' => $intEntryLinksDetected === $DomainRow->getForgeEntryDisplayCount(),
'detail' => $intEntryLinksDetected . ' / expected=' . $DomainRow->getForgeEntryDisplayCount(),
];
$arrChecks[] = [
'label' => 'current_output_default_detail_only',
'passed' => $DomainRow->getForgeSitemapCount() === 0
? count($arrCurrentRows) === 1 && (($arrCurrentRows[0]['kind'] ?? '') === 'detail')
: count($arrCurrentRows) >= 1,
'detail' => json_encode($arrCurrentKinds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'expanded_output_contains_expected_rows',
'passed' => count($arrExpandedRows) >= (1 + $intExpandedSitemapCount),
'detail' => 'count=' . count($arrExpandedRows) . ' / expected_at_least=' . (1 + $intExpandedSitemapCount),
];
foreach ($arrExpandedRows as $intIndex => $arrRow) {
$strHref = (string)($arrRow['href'] ?? '');
$strPath = parse_url($strHref, PHP_URL_PATH) ?: '';
$arrResolved = $UrlBuilder->resolvePagePath($strPath, ['detail_forge', 'detail']);
$strExpectedPage = (($arrRow['kind'] ?? 'detail') === 'forge') ? 'detail_forge' : 'detail';
$arrChecks[] = [
'label' => 'expanded_row_' . $intIndex . '_resolve_page',
'passed' => (string)($arrResolved['page_key'] ?? '') === $strExpectedPage,
'detail' => $strPath . ' => ' . (string)($arrResolved['page_key'] ?? '[miss]'),
];
}
$boolAllPassed = !in_array(false, array_column($arrChecks, 'passed'), true);
$arrSummary = [
'host' => $strHost,
'video_id' => $intVideoId,
'slug' => $strSlug,
'entry_count_current' => $DomainRow->getForgeEntryDisplayCount(),
'entry_links_detected' => $intEntryLinksDetected,
'sitemap_count_current' => $DomainRow->getForgeSitemapCount(),
'current_output_kinds' => $arrCurrentKinds,
'expanded_entry_count' => $intExpandedEntryCount,
'expanded_sitemap_count' => $intExpandedSitemapCount,
'expanded_output_kinds' => $arrExpandedKinds,
'current_rows' => $arrCurrentRows,
'expanded_rows' => $arrExpandedRows,
'checks' => $arrChecks,
'all_passed' => $boolAllPassed,
'verified_at' => date(DATE_ATOM),
];
if ($strFormat === 'text') {
echo renderText($arrSummary);
exit($boolAllPassed ? 0 : 1);
}
echo json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
exit($boolAllPassed ? 0 : 1);

View File

@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SiteStyle.php';
require dirname(__DIR__) . '/app/common/helper/UrlBuilder.php';
use app\common\helper\SeoCopyStore;
use app\common\helper\SiteStyle;
use app\common\helper\UrlBuilder;
use app\model\DomainModel;
use app\model\VideoModel;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_forge_route_group_verify.php <host> <video-id> <forge-id> [--base-url=https://host] [--expected-group=video|film|detail_forge_v2] [--target-root=/abs/path] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_forge_route_group_verify.php chuanjiafeng.net 154124 1 --format=text\n";
}
function ensureAppInitialized(): void
{
static $boolInitialized = false;
if ($boolInitialized) {
return;
}
(new App())->initialize();
$boolInitialized = true;
}
function fetchPage(string $strUrl, string $strHost): array
{
$arrHeaders = [
'Host: ' . $strHost,
'Connection: close',
];
$Context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", $arrHeaders),
'ignore_errors' => true,
'timeout' => 20,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
$strBody = @file_get_contents($strUrl, false, $Context);
$arrResponseHeaders = $http_response_header ?? [];
$intStatus = 0;
foreach ($arrResponseHeaders as $strHeaderLine) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})/i', $strHeaderLine, $arrMatches)) {
$intStatus = (int)($arrMatches[1] ?? 0);
}
}
return [
'status' => $intStatus,
'body' => $strBody === false ? '' : $strBody,
'headers' => $arrResponseHeaders,
];
}
function normalizeForMatch(string $strValue): string
{
$strValue = html_entity_decode($strValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$strValue = strip_tags($strValue);
$strValue = str_replace(
["\r", "\n", "\t", '“', '”', '', '', ' '],
[' ', ' ', ' ', '"', '"', "'", "'", ' '],
$strValue
);
$strValue = preg_replace('/\s+/u', ' ', $strValue);
return trim((string)$strValue);
}
function extractLeadingRouteToken(string $strPattern): string
{
$strPattern = ltrim(trim($strPattern), '/');
if ($strPattern === '') {
return '';
}
if (!preg_match('/^([^\/\{-]+)/', $strPattern, $arrMatches)) {
return '';
}
return trim((string)($arrMatches[1] ?? ''));
}
function renderText(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'video_id: ' . $arrSummary['video_id'],
'forge_id: ' . $arrSummary['forge_id'],
'slug: ' . $arrSummary['slug'],
'forge_group: ' . $arrSummary['forge_group'],
'expected_group: ' . $arrSummary['expected_group'],
'detail_path: ' . $arrSummary['detail_path'],
'forge_path: ' . $arrSummary['forge_path'],
'family_pattern: ' . $arrSummary['family_pattern'],
'family_route: ' . $arrSummary['family_route'],
'detail_resolved_page: ' . ($arrSummary['detail_resolved_page'] ?: '-'),
'forge_resolved_page: ' . ($arrSummary['forge_resolved_page'] ?: '-'),
'detail_status: ' . $arrSummary['detail_status'],
'forge_status: ' . $arrSummary['forge_status'],
'all_passed: ' . ($arrSummary['all_passed'] ? 'yes' : 'no'),
'checks:',
];
foreach ($arrSummary['checks'] as $arrCheck) {
$arrLines[] = '- ' . ($arrCheck['passed'] ? '[ok] ' : '[fail] ') . $arrCheck['label'] . ': ' . $arrCheck['detail'];
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 3) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intForgeId = 0;
$strBaseUrl = '';
$strExpectedGroup = '';
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--expected-group=')) {
$strExpectedGroup = trim(substr($strArg, strlen('--expected-group=')));
continue;
}
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = trim(substr($strArg, strlen('--target-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
if ($intForgeId === 0) {
$intForgeId = max(1, (int)$strArg);
continue;
}
}
if ($strHost === '' || $intVideoId <= 0 || $intForgeId <= 0) {
printUsage();
exit(1);
}
ensureAppInitialized();
/** @var DomainModel|null $DomainRow */
$DomainRow = app(DomainModel::class)->where('d_domain', $strHost)->find();
if (empty($DomainRow)) {
fwrite(STDERR, "Domain not found: {$strHost}\n");
exit(1);
}
$strBaseUrl = $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost);
$arrForgeSeoCfg = $DomainRow->getForgeSeoCfg();
$strActualGroup = DomainModel::normalizeForgeRouteGroup((string)($arrForgeSeoCfg['route_group'] ?? DomainModel::FORGE_ROUTE_GROUP_DEFAULT));
$strExpectedGroup = $strExpectedGroup !== ''
? DomainModel::normalizeForgeRouteGroup($strExpectedGroup)
: $strActualGroup;
$TpStyle = SiteStyle::getConfig($DomainRow, $strHost);
$UrlBuilder = new UrlBuilder($TpStyle);
$arrVideo = VideoModel::getInstance()->findOne(['v_id' => $intVideoId], [
'typeMap' => [
'root' => 'array',
'document' => 'array',
'array' => 'array',
],
'projection' => [
'_id' => 0,
'v_name_en' => 1,
],
]) ?? [];
$strSlug = trim((string)($arrVideo['v_name_en'] ?? ''));
$strDetailPath = $UrlBuilder->detail($strSlug, $intVideoId);
$strForgePath = $UrlBuilder->detailForge($strSlug, $intVideoId, $intForgeId);
$arrDetailParsed = $UrlBuilder->parseDetailPath($strDetailPath);
$arrForgeParsed = $UrlBuilder->parseDetailForgePath($strForgePath);
$arrDetailResolved = $UrlBuilder->resolvePagePath($strDetailPath, ['detail_forge', 'detail', 'play']);
$arrForgeResolved = $UrlBuilder->resolvePagePath($strForgePath, ['detail_forge', 'detail', 'play']);
$strDetailRoundTrip = !empty($arrDetailParsed)
? $UrlBuilder->detail((string)($arrDetailParsed['strPinyin'] ?? ''), (int)($arrDetailParsed['intVId'] ?? 0))
: '';
$strForgeRoundTrip = !empty($arrForgeParsed)
? $UrlBuilder->detailForge(
(string)($arrForgeParsed['strPinyin'] ?? ''),
(int)($arrForgeParsed['intVId'] ?? 0),
(int)($arrForgeParsed['intVForgeId'] ?? 0)
)
: '';
$arrDetailForgeFamily = (array)($TpStyle['template_cfg']['url_family']['detail_forge'] ?? []);
$strFamilyPattern = (string)($arrDetailForgeFamily['pattern'] ?? '');
$strFamilyRoute = (string)($arrDetailForgeFamily['routes'][0] ?? '');
$strFamilyPrefix = extractLeadingRouteToken($strFamilyPattern);
$strExpectedPrefix = DomainModel::resolveForgeRouteGroupPrefix($strExpectedGroup);
$arrDetailResponse = fetchPage($strBaseUrl . $strDetailPath, $strHost);
$arrForgeResponse = fetchPage($strBaseUrl . $strForgePath, $strHost);
$strRawDetailBody = (string)($arrDetailResponse['body'] ?? '');
$strRawForgeBody = (string)($arrForgeResponse['body'] ?? '');
$strNormalizedDetailBody = normalizeForMatch((string)($arrDetailResponse['body'] ?? ''));
$strNormalizedForgeBody = normalizeForMatch((string)($arrForgeResponse['body'] ?? ''));
$arrForgeSeoCopy = SeoCopyStore::getPageDataFromRoot(
$strTargetRoot,
$strHost,
'forge',
$intVideoId . '-forge-' . $intForgeId
);
$arrChecks = [];
$arrChecks[] = [
'label' => 'group_matches_expected',
'passed' => $strActualGroup === $strExpectedGroup,
'detail' => $strActualGroup . ' / expected=' . $strExpectedGroup,
];
$arrChecks[] = [
'label' => 'family_prefix_matches_group',
'passed' => $strExpectedPrefix === '' ? $strFamilyPrefix !== '' : $strFamilyPrefix === $strExpectedPrefix,
'detail' => $strFamilyPrefix . ' / expected_prefix=' . ($strExpectedPrefix !== '' ? $strExpectedPrefix : '[keep-family]'),
];
$arrChecks[] = [
'label' => 'detail_reverse_parse',
'passed' => !empty($arrDetailParsed)
&& (int)($arrDetailParsed['intVId'] ?? 0) === $intVideoId
&& (string)($arrDetailParsed['strPinyin'] ?? '') === $strSlug,
'detail' => json_encode($arrDetailParsed, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'forge_reverse_parse',
'passed' => !empty($arrForgeParsed)
&& (int)($arrForgeParsed['intVId'] ?? 0) === $intVideoId
&& (int)($arrForgeParsed['intVForgeId'] ?? 0) === $intForgeId
&& (string)($arrForgeParsed['strPinyin'] ?? '') === $strSlug,
'detail' => json_encode($arrForgeParsed, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'detail_resolve_page',
'passed' => (string)($arrDetailResolved['page_key'] ?? '') === 'detail',
'detail' => (string)($arrDetailResolved['page_key'] ?? '[miss]'),
];
$arrChecks[] = [
'label' => 'forge_resolve_page',
'passed' => (string)($arrForgeResolved['page_key'] ?? '') === 'detail_forge',
'detail' => (string)($arrForgeResolved['page_key'] ?? '[miss]'),
];
$arrChecks[] = [
'label' => 'detail_round_trip',
'passed' => $strDetailRoundTrip === $strDetailPath,
'detail' => $strDetailRoundTrip . ' / expected=' . $strDetailPath,
];
$arrChecks[] = [
'label' => 'forge_round_trip',
'passed' => $strForgeRoundTrip === $strForgePath,
'detail' => $strForgeRoundTrip . ' / expected=' . $strForgePath,
];
$arrChecks[] = [
'label' => 'detail_status_200',
'passed' => (int)($arrDetailResponse['status'] ?? 0) === 200,
'detail' => 'status=' . (int)($arrDetailResponse['status'] ?? 0),
];
$arrChecks[] = [
'label' => 'forge_status_200',
'passed' => (int)($arrForgeResponse['status'] ?? 0) === 200,
'detail' => 'status=' . (int)($arrForgeResponse['status'] ?? 0),
];
$arrChecks[] = [
'label' => 'detail_contains_forge_link',
'passed' => str_contains($strRawDetailBody, $strForgePath),
'detail' => $strForgePath,
];
$arrChecks[] = [
'label' => 'forge_contains_self_path',
'passed' => str_contains($strRawForgeBody, $strForgePath),
'detail' => $strForgePath,
];
if (!empty($arrForgeSeoCopy)) {
foreach ([
'forge_detail_note',
'forge_body_lead',
] as $strField) {
$strNeedle = trim((string)($arrForgeSeoCopy[$strField] ?? ''));
if ($strNeedle === '') {
continue;
}
$arrChecks[] = [
'label' => 'forge_copy_' . $strField,
'passed' => str_contains($strNormalizedForgeBody, normalizeForMatch($strNeedle)),
'detail' => mb_substr($strNeedle, 0, 80),
];
}
}
$boolAllPassed = !in_array(false, array_column($arrChecks, 'passed'), true);
$arrSummary = [
'host' => $strHost,
'video_id' => $intVideoId,
'forge_id' => $intForgeId,
'slug' => $strSlug,
'forge_group' => $strActualGroup,
'expected_group' => $strExpectedGroup,
'detail_path' => $strDetailPath,
'forge_path' => $strForgePath,
'family_pattern' => $strFamilyPattern,
'family_route' => $strFamilyRoute,
'detail_resolved_page' => (string)($arrDetailResolved['page_key'] ?? ''),
'forge_resolved_page' => (string)($arrForgeResolved['page_key'] ?? ''),
'detail_reverse_parse' => $arrDetailParsed,
'forge_reverse_parse' => $arrForgeParsed,
'detail_status' => (int)($arrDetailResponse['status'] ?? 0),
'forge_status' => (int)($arrForgeResponse['status'] ?? 0),
'checks' => $arrChecks,
'all_passed' => $boolAllPassed,
'verified_at' => date(DATE_ATOM),
];
if ($strFormat === 'text') {
echo renderText($arrSummary);
exit($boolAllPassed ? 0 : 1);
}
echo json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
exit($boolAllPassed ? 0 : 1);

View File

@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SiteStyle.php';
require dirname(__DIR__) . '/app/common/helper/UrlBuilder.php';
use app\common\helper\SiteStyle;
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
use app\common\helper\UrlBuilder;
use app\model\DomainModel;
use app\model\VideoModel;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_front_verify.php <host> <scene> <page-part> [page-part ...] [--target-root=/abs/path] [--path=/target/url] [--base-url=http://127.0.0.1:8910] [--format=json|text]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_front_verify.php chuanjiafeng.net search 相逢 --target-root=/tmp/seo-import-dst --base-url=http://127.0.0.1:8910 --format=text\n";
echo " php scripts/seo_copy_front_verify.php chuanjiafeng.net play 154229 douban 1 --path=/m3u8/154229-douban-1 --base-url=http://127.0.0.1:8910 --format=text\n";
}
function buildScenePath(string $strHost, string $strScene, array $arrPageParts): string
{
static $boolInitialized = false;
if (!$boolInitialized) {
(new App())->initialize();
$boolInitialized = true;
}
/** @var DomainModel|null $DomainRow */
$DomainRow = app(DomainModel::class)->where('d_domain', $strHost)->find();
if (empty($DomainRow)) {
return '';
}
$TpStyle = SiteStyle::getConfig($DomainRow, $strHost);
$UrlBuilder = new UrlBuilder($TpStyle);
if (in_array($strScene, ['detail', 'forge', 'play'], true)) {
$intVideoId = (int)($arrPageParts[0] ?? 0);
$arrVideo = VideoModel::getInstance()->findOne(['v_id' => $intVideoId], [
'typeMap' => [
'root' => 'array',
'document' => 'array',
'array' => 'array',
],
'projection' => [
'_id' => 0,
'v_name_en' => 1,
],
]) ?? [];
$strSlug = trim((string)($arrVideo['v_name_en'] ?? ''));
return match ($strScene) {
'detail' => $UrlBuilder->detail($strSlug, $intVideoId),
'forge' => $UrlBuilder->detailForge($strSlug, $intVideoId, max(1, (int)($arrPageParts[1] ?? 1))),
'play' => $UrlBuilder->play($strSlug, $intVideoId, (string)($arrPageParts[1] ?? ''), max(1, (int)($arrPageParts[2] ?? 1))),
default => '',
};
}
return match ($strScene) {
'home' => $UrlBuilder->home(),
'category_index' => $UrlBuilder->categoryParent((string)($arrPageParts[0] ?? '')),
'category_list' => $UrlBuilder->categoryChild((string)($arrPageParts[0] ?? ''), (string)($arrPageParts[1] ?? ''), max(1, (int)($arrPageParts[2] ?? 1))),
'search' => $UrlBuilder->searchResult((string)($arrPageParts[0] ?? '')),
'rank_index' => $UrlBuilder->rankIndex(),
'rank_list' => $UrlBuilder->rankList((string)($arrPageParts[0] ?? 'daily')),
default => '',
};
}
function buildNeedles(string $strScene, array $arrData): array
{
return match ($strScene) {
'home',
'category_index',
'category_list',
'search',
'rank_index',
'rank_list' => array_values(array_filter([
trim((string)($arrData['intro_text'] ?? '')),
trim((string)($arrData['intro_meta'] ?? '')),
trim((string)($arrData['faq_content'] ?? '')),
trim((string)($arrData['guide_cards'][0]['title'] ?? '')),
])),
'detail' => array_values(array_filter([
trim((string)($arrData['detail_body_lead'] ?? '')),
trim((string)($arrData['detail_play_link_lead'] ?? '')),
])),
'forge' => array_values(array_filter([
trim((string)($arrData['forge_detail_note'] ?? '')),
trim((string)($arrData['forge_body_lead'] ?? '')),
])),
'play' => array_values(array_filter([
trim((string)($arrData['play_intro'] ?? '')),
trim((string)($arrData['play_body_lead'] ?? '')),
])),
default => [],
};
}
function fetchPage(string $strUrl, string $strHost): array
{
$arrHeaders = [
'Host: ' . $strHost,
'Connection: close',
];
$Context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", $arrHeaders),
'ignore_errors' => true,
'timeout' => 15,
],
]);
$strBody = @file_get_contents($strUrl, false, $Context);
$arrResponseHeaders = $http_response_header ?? [];
$intStatus = 0;
foreach ($arrResponseHeaders as $strHeaderLine) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})/i', $strHeaderLine, $arrMatches)) {
$intStatus = (int)($arrMatches[1] ?? 0);
}
}
return [
'status' => $intStatus,
'body' => $strBody === false ? '' : $strBody,
'headers' => $arrResponseHeaders,
];
}
function normalizeForMatch(string $strValue): string
{
$strValue = html_entity_decode($strValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$strValue = strip_tags($strValue);
$strValue = str_replace(
["\r", "\n", "\t", '“', '”', '', '', ' '],
[' ', ' ', ' ', '"', '"', "'", "'", ' '],
$strValue
);
$strValue = preg_replace('/\s+/u', ' ', $strValue);
return trim((string)$strValue);
}
function renderTextSummary(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'scene: ' . $arrSummary['scene'],
'page_key: ' . $arrSummary['page_key'],
'path: ' . $arrSummary['path'],
'status: ' . $arrSummary['http_status'],
'all_matched: ' . ($arrSummary['all_matched'] ? 'yes' : 'no'),
];
$arrLines[] = 'needles:';
foreach ($arrSummary['checks'] as $arrCheck) {
$arrLines[] = '- ' . ($arrCheck['matched'] ? '[ok] ' : '[miss] ') . $arrCheck['label'] . ': ' . $arrCheck['preview'];
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strHost = '';
$strScene = '';
$arrPageParts = [];
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
$strPath = '';
$strBaseUrl = 'http://127.0.0.1:8910';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = substr($strArg, strlen('--target-root='));
continue;
}
if (str_starts_with($strArg, '--path=')) {
$strPath = substr($strArg, strlen('--path='));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(substr($strArg, strlen('--format=')));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($strScene === '') {
$strScene = trim($strArg);
continue;
}
$arrPageParts[] = $strArg;
}
if ($strHost === '' || $strScene === '' || !in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
printUsage();
exit(1);
}
$strPageKey = SeoCopySchema::buildScenePageKey($strScene, $arrPageParts);
$arrData = SeoCopyStore::getPageDataFromRoot($strTargetRoot, $strHost, $strScene, $strPageKey);
if (empty($arrData)) {
fwrite(STDERR, "No readable seo_copy data found for {$strScene}/{$strPageKey} under {$strTargetRoot}\n");
exit(1);
}
if ($strPath === '') {
$strPath = buildScenePath($strHost, $strScene, $arrPageParts);
}
if ($strPath === '') {
fwrite(STDERR, "Unable to resolve path automatically for scene {$strScene}; please pass --path=/your/url\n");
exit(1);
}
$arrNeedles = buildNeedles($strScene, $arrData);
$strUrl = $strBaseUrl . $strPath;
$arrResponse = fetchPage($strUrl, $strHost);
$strBody = (string)($arrResponse['body'] ?? '');
$intStatus = (int)($arrResponse['status'] ?? 0);
$strNormalizedBody = normalizeForMatch($strBody);
$arrChecks = [];
foreach ($arrNeedles as $intIndex => $strNeedle) {
$boolMatched = $strNeedle !== '' && str_contains($strNormalizedBody, normalizeForMatch($strNeedle));
$arrChecks[] = [
'label' => 'needle_' . ($intIndex + 1),
'matched' => $boolMatched,
'preview' => mb_substr($strNeedle, 0, 120),
];
}
$arrSummary = [
'host' => $strHost,
'scene' => $strScene,
'page_parts' => $arrPageParts,
'page_key' => $strPageKey,
'target_root' => $strTargetRoot,
'path' => $strPath,
'url' => $strUrl,
'http_status' => $intStatus,
'all_matched' => $intStatus === 200 && !in_array(false, array_column($arrChecks, 'matched'), true),
'checks' => $arrChecks,
];
if ($strFormat === 'text') {
echo renderTextSummary($arrSummary);
} else {
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
use app\common\helper\SeoCopyGenerationHelper;
use app\model\DomainModel;
require dirname(__DIR__) . '/vendor/autoload.php';
(new think\App())->initialize();
$strHost = trim((string)($argv[1] ?? ''));
if ($strHost === '') {
fwrite(STDERR, "Usage: php scripts/seo_copy_generation_prepare.php <host>\n");
exit(1);
}
DomainModel::flushAllDomianOnCache();
$DomainModel = DomainModel::getDomainOnCacheByDomain($strHost);
if (!$DomainModel instanceof DomainModel) {
fwrite(STDERR, "Domain not found: {$strHost}\n");
exit(1);
}
$arrResult = SeoCopyGenerationHelper::initializeForDomain($DomainModel);
echo json_encode($arrResult, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL;

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use app\common\helper\SeoCopyGenerationHelper;
require dirname(__DIR__) . '/vendor/autoload.php';
(new think\App())->initialize();
$strHost = trim((string)($argv[1] ?? ''));
if ($strHost === '') {
fwrite(STDERR, "Usage: php scripts/seo_copy_generation_status.php <host>\n");
exit(1);
}
$arrState = SeoCopyGenerationHelper::readState($strHost);
if (empty($arrState)) {
fwrite(STDERR, "No state found for {$strHost}\n");
exit(1);
}
echo json_encode($arrState, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL;

View File

@@ -0,0 +1,304 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SiteStyle.php';
require dirname(__DIR__) . '/app/common/helper/UrlBuilder.php';
use app\common\helper\SiteStyle;
use app\common\helper\UrlBuilder;
use app\model\DomainModel;
use app\model\VideoModel;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_play_route_verify.php <host> <video-id> [--play-type=default] [--play-index=1] [--base-url=https://host] [--format=json|text]\n";
}
function ensureAppInitialized(): void
{
static $boolInitialized = false;
if ($boolInitialized) {
return;
}
(new App())->initialize();
$boolInitialized = true;
}
function fetchPage(string $strUrl, string $strHost): array
{
$Context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => implode("\r\n", [
'Host: ' . $strHost,
'Connection: close',
]),
'ignore_errors' => true,
'timeout' => 20,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
$strBody = @file_get_contents($strUrl, false, $Context);
$arrHeaders = $http_response_header ?? [];
$intStatus = 0;
foreach ($arrHeaders as $strHeaderLine) {
if (preg_match('/^HTTP\/\S+\s+(\d{3})/i', $strHeaderLine, $arrMatches)) {
$intStatus = (int)($arrMatches[1] ?? 0);
}
}
return [
'status' => $intStatus,
'body' => $strBody === false ? '' : $strBody,
];
}
function normalizeForMatch(string $strValue): string
{
$strValue = html_entity_decode($strValue, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$strValue = strip_tags($strValue);
$strValue = preg_replace('/\s+/u', ' ', str_replace(["\r", "\n", "\t"], ' ', $strValue));
return trim((string)$strValue);
}
function hasPlayParamMatch(
array $arrParsed,
string $strSlug,
int $intVideoId,
string $strPlayType,
int $intPlayIndex
): bool {
if ((int)($arrParsed['intVId'] ?? 0) !== $intVideoId
|| (string)($arrParsed['strPlayType'] ?? '') !== $strPlayType
|| (int)($arrParsed['intPlayIndex'] ?? 0) !== $intPlayIndex
) {
return false;
}
if (array_key_exists('strPinyin', $arrParsed)) {
return (string)($arrParsed['strPinyin'] ?? '') === $strSlug;
}
return true;
}
function renderText(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'video_id: ' . $arrSummary['video_id'],
'slug: ' . $arrSummary['slug'],
'play_type: ' . $arrSummary['play_type'],
'play_index: ' . $arrSummary['play_index'],
'detail_path: ' . $arrSummary['detail_path'],
'play_path: ' . $arrSummary['play_path'],
'play_resolved_page: ' . ($arrSummary['play_resolved_page'] ?: '-'),
'detail_status: ' . $arrSummary['detail_status'],
'play_status: ' . $arrSummary['play_status'],
'all_passed: ' . ($arrSummary['all_passed'] ? 'yes' : 'no'),
'checks:',
];
foreach ($arrSummary['checks'] as $arrCheck) {
$arrLines[] = '- ' . ($arrCheck['passed'] ? '[ok] ' : '[fail] ') . $arrCheck['label'] . ': ' . $arrCheck['detail'];
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 2) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$strRequestedPlayType = '';
$intRequestedPlayIndex = 1;
$strBaseUrl = '';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--play-type=')) {
$strRequestedPlayType = trim(substr($strArg, strlen('--play-type=')));
continue;
}
if (str_starts_with($strArg, '--play-index=')) {
$intRequestedPlayIndex = max(1, (int)substr($strArg, strlen('--play-index=')));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
}
if ($strHost === '' || $intVideoId <= 0) {
printUsage();
exit(1);
}
ensureAppInitialized();
/** @var DomainModel|null $DomainRow */
$DomainRow = app(DomainModel::class)->where('d_domain', $strHost)->find();
if (empty($DomainRow)) {
fwrite(STDERR, "Domain not found: {$strHost}\n");
exit(1);
}
$strBaseUrl = $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost);
$arrVideo = VideoModel::getInstance()->findOne(['v_id' => $intVideoId], [
'typeMap' => [
'root' => 'array',
'document' => 'array',
'array' => 'array',
],
'projection' => [
'_id' => 0,
'v_id' => 1,
'v_name' => 1,
'v_name_en' => 1,
'v_play_url' => 1,
],
]) ?? [];
$strSlug = trim((string)($arrVideo['v_name_en'] ?? ''));
$arrPlayGroups = is_array($arrVideo['v_play_url'] ?? null) ? (array)$arrVideo['v_play_url'] : [];
if ($strSlug === '' || empty($arrPlayGroups)) {
fwrite(STDERR, "Video slug/play groups missing: {$intVideoId}\n");
exit(1);
}
$strPlayType = $strRequestedPlayType !== '' ? $strRequestedPlayType : (string)array_key_first($arrPlayGroups);
$arrCurrentPlayGroup = (array)($arrPlayGroups[$strPlayType] ?? []);
if (empty($arrCurrentPlayGroup)) {
$strPlayType = (string)array_key_first($arrPlayGroups);
$arrCurrentPlayGroup = (array)($arrPlayGroups[$strPlayType] ?? []);
}
$intPlayIndex = max(1, min($intRequestedPlayIndex, count($arrCurrentPlayGroup)));
$arrCurrentEpisode = (array)($arrCurrentPlayGroup[$intPlayIndex - 1] ?? []);
$strEpisodeName = trim((string)($arrCurrentEpisode['name'] ?? ''));
if ($strEpisodeName === '') {
$strEpisodeName = '第' . $intPlayIndex . '集';
}
$TpStyle = SiteStyle::getConfig($DomainRow, $strHost);
$UrlBuilder = new UrlBuilder($TpStyle);
$strDetailPath = $UrlBuilder->detail($strSlug, $intVideoId);
$strPlayPath = $UrlBuilder->play($strSlug, $intVideoId, $strPlayType, $intPlayIndex);
$arrPlayParsed = $UrlBuilder->parsePlayPath($strPlayPath);
$arrPlayResolved = $UrlBuilder->resolvePagePath($strPlayPath, ['play', 'detail_forge', 'detail']);
$strPlayRoundTrip = !empty($arrPlayParsed)
? $UrlBuilder->play(
(string)($arrPlayParsed['strPinyin'] ?? ''),
(int)($arrPlayParsed['intVId'] ?? 0),
(string)($arrPlayParsed['strPlayType'] ?? ''),
(int)($arrPlayParsed['intPlayIndex'] ?? 0)
)
: '';
$arrDetailResponse = fetchPage($strBaseUrl . $strDetailPath, $strHost);
$arrPlayResponse = fetchPage($strBaseUrl . $strPlayPath, $strHost);
$strRawDetailBody = (string)($arrDetailResponse['body'] ?? '');
$strRawPlayBody = (string)($arrPlayResponse['body'] ?? '');
$strNormalizedPlayBody = normalizeForMatch($strRawPlayBody);
$arrChecks = [];
$arrChecks[] = [
'label' => 'play_reverse_parse',
'passed' => !empty($arrPlayParsed)
&& hasPlayParamMatch($arrPlayParsed, $strSlug, $intVideoId, $strPlayType, $intPlayIndex),
'detail' => json_encode($arrPlayParsed, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$arrChecks[] = [
'label' => 'play_resolve_page',
'passed' => (string)($arrPlayResolved['page_key'] ?? '') === 'play',
'detail' => (string)($arrPlayResolved['page_key'] ?? '[miss]'),
];
$arrChecks[] = [
'label' => 'play_round_trip',
'passed' => $strPlayRoundTrip === $strPlayPath,
'detail' => $strPlayRoundTrip . ' / expected=' . $strPlayPath,
];
$arrChecks[] = [
'label' => 'detail_contains_play_link',
'passed' => str_contains($strRawDetailBody, $strPlayPath),
'detail' => $strPlayPath,
];
$arrChecks[] = [
'label' => 'detail_status_200',
'passed' => (int)($arrDetailResponse['status'] ?? 0) === 200,
'detail' => 'status=' . (int)($arrDetailResponse['status'] ?? 0),
];
$arrChecks[] = [
'label' => 'play_status_200',
'passed' => (int)($arrPlayResponse['status'] ?? 0) === 200,
'detail' => 'status=' . (int)($arrPlayResponse['status'] ?? 0),
];
$arrChecks[] = [
'label' => 'play_contains_self_path',
'passed' => str_contains($strRawPlayBody, $strPlayPath),
'detail' => $strPlayPath,
];
$arrChecks[] = [
'label' => 'play_contains_episode_name',
'passed' => str_contains($strNormalizedPlayBody, normalizeForMatch($strEpisodeName)),
'detail' => $strEpisodeName,
];
$boolAllPassed = !in_array(false, array_column($arrChecks, 'passed'), true);
$arrSummary = [
'host' => $strHost,
'video_id' => $intVideoId,
'slug' => $strSlug,
'play_type' => $strPlayType,
'play_index' => $intPlayIndex,
'episode_name' => $strEpisodeName,
'detail_path' => $strDetailPath,
'play_path' => $strPlayPath,
'play_resolved_page' => (string)($arrPlayResolved['page_key'] ?? ''),
'play_reverse_parse' => $arrPlayParsed,
'detail_status' => (int)($arrDetailResponse['status'] ?? 0),
'play_status' => (int)($arrPlayResponse['status'] ?? 0),
'checks' => $arrChecks,
'all_passed' => $boolAllPassed,
'verified_at' => date(DATE_ATOM),
];
if ($strFormat === 'text') {
echo renderText($arrSummary);
exit($boolAllPassed ? 0 : 1);
}
echo json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
exit($boolAllPassed ? 0 : 1);

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
use app\common\helper\SeoCopyPortalHomeHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_portal_home.php [--portal-root=/abs/path] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_portal_home.php --portal-root=public/_seo_copy_release --format=text\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strPortalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
try {
$arrSummary = SeoCopyPortalHomeHelper::buildSummary($strPortalRoot);
SeoCopyPortalHomeHelper::writeArtifacts($strPortalRoot, $arrSummary);
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
echo json_encode([
'portal_root' => realpath($strPortalRoot) ?: $strPortalRoot,
'index_html' => rtrim($strPortalRoot, '/') . '/index.html',
'index_json' => rtrim($strPortalRoot, '/') . '/index.json',
'releases' => (int)(($arrSummary['release_portal'] ?? [])['releases_count'] ?? 0),
'approved_manifests' => (int)(($arrSummary['approved_portal'] ?? [])['manifests_count'] ?? 0),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyFactsBuilder.php';
use app\common\helper\SeoCopyFactsBuilder;
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
use think\App;
function printUsage(): void
{
$strScenes = implode(', ', SeoCopySchema::getSupportedScenes());
echo "Usage:\n";
echo " php scripts/seo_copy_prompt.php <host> <scene> <page-part> [page-part ...] [--facts-file=/abs/path.json] [--facts-json='{}'] [--format=markdown|json]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_prompt.php chuanjiafeng.net home index\n";
echo " php scripts/seo_copy_prompt.php chuanjiafeng.net search 相逢 --facts-json='{\"keyword\":\"相逢\",\"result_total\":315}'\n";
echo " php scripts/seo_copy_prompt.php chuanjiafeng.net detail 154229 --facts-file=/tmp/detail-facts.json\n\n";
echo "Supported scenes:\n";
echo " {$strScenes}\n";
}
function parseFacts(array $arrOptions): array
{
if (($arrOptions['auto-facts'] ?? '0') === '1') {
return ['__AUTO_FACTS__' => true];
}
if (!empty($arrOptions['facts-json'])) {
$arrData = json_decode((string)$arrOptions['facts-json'], true);
if (!is_array($arrData)) {
fwrite(STDERR, "Invalid --facts-json, must be valid JSON object.\n");
exit(1);
}
return $arrData;
}
if (!empty($arrOptions['facts-file'])) {
$strPath = (string)$arrOptions['facts-file'];
if (!is_file($strPath)) {
fwrite(STDERR, "Facts file not found: {$strPath}\n");
exit(1);
}
$strJson = (string)file_get_contents($strPath);
$arrData = json_decode($strJson, true);
if (!is_array($arrData)) {
fwrite(STDERR, "Facts file must contain valid JSON object.\n");
exit(1);
}
return $arrData;
}
return [];
}
$arrArgs = $argv;
array_shift($arrArgs);
$arrOptions = [
'format' => 'markdown',
];
$arrParts = [];
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--')) {
$arrTmp = explode('=', substr($strArg, 2), 2);
$strKey = $arrTmp[0] ?? '';
$strVal = $arrTmp[1] ?? '1';
$arrOptions[$strKey] = $strVal;
continue;
}
$arrParts[] = $strArg;
}
if (count($arrParts) < 3) {
printUsage();
exit(1);
}
$strHost = (string)array_shift($arrParts);
$strScene = (string)array_shift($arrParts);
if (!in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
fwrite(STDERR, "Unsupported scene: {$strScene}\n");
printUsage();
exit(1);
}
$strPageKey = SeoCopySchema::buildScenePageKey($strScene, $arrParts);
$arrEnvelope = SeoCopySchema::buildGenerationEnvelope($strScene);
$arrFacts = parseFacts($arrOptions);
if (($arrFacts['__AUTO_FACTS__'] ?? false) === true) {
(new App())->initialize();
$arrFacts = SeoCopyFactsBuilder::build($strHost, $strScene, $arrParts);
}
$arrPromptData = [
'host' => $strHost,
'scene' => $strScene,
'page_key' => $strPageKey,
'page_parts' => $arrParts,
'target_path' => dirname(__DIR__) . '/data/seo_copy/' . SeoCopyStore::buildPageKey($strHost, 'host') . '/' . $strScene . '/' . $strPageKey . '.json',
'rules' => $arrEnvelope['rules'],
'fact_hints' => $arrEnvelope['fact_hints'],
'template' => $arrEnvelope['template'],
'facts' => $arrFacts,
];
if (($arrOptions['format'] ?? 'markdown') === 'json') {
echo json_encode($arrPromptData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit(0);
}
$strTemplateJson = json_encode($arrEnvelope['template'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$strFactsJson = json_encode(!empty($arrFacts) ? $arrFacts : (object)[], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$strRulesJson = json_encode($arrEnvelope['rules'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$strFactHintsJson = json_encode($arrEnvelope['fact_hints'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "# SEO Copy Prompt" . PHP_EOL . PHP_EOL;
echo "## Target" . PHP_EOL;
echo "- host: {$strHost}" . PHP_EOL;
echo "- scene: {$strScene}" . PHP_EOL;
echo "- page_key: {$strPageKey}" . PHP_EOL;
echo "- target_path: {$arrPromptData['target_path']}" . PHP_EOL . PHP_EOL;
echo "## Instruction" . PHP_EOL;
echo "你是站群 SEO 内容补料助手。请基于给定 facts 生成一个 JSON 对象,只输出 JSON不要输出解释、Markdown、代码块。" . PHP_EOL;
echo "要求:" . PHP_EOL;
echo "- 必须严格遵守 scene rules 和 template 字段结构。" . PHP_EOL;
echo "- 只能改写和组织已知事实,不要编造演员、年份、剧情、线路、榜单数据。" . PHP_EOL;
echo "- 文案目标是提升页面可读性、解释力和 SEO 覆盖,但不要堆砌关键词。" . PHP_EOL;
echo "- 文风要自然、克制、站内导航型,避免“全网最全”“免费观看”等夸张承诺。" . PHP_EOL;
echo "- 输出必须能直接写入目标 JSON 文件。" . PHP_EOL . PHP_EOL;
echo "## Scene Rules" . PHP_EOL;
echo $strRulesJson . PHP_EOL . PHP_EOL;
echo "## Fact Hints" . PHP_EOL;
echo ($strFactHintsJson !== false ? $strFactHintsJson : '{}') . PHP_EOL . PHP_EOL;
echo "## Facts" . PHP_EOL;
echo ($strFactsJson !== false ? $strFactsJson : '{}') . PHP_EOL . PHP_EOL;
echo "## Return JSON Template" . PHP_EOL;
echo ($strTemplateJson !== false ? $strTemplateJson : '{}') . PHP_EOL;

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_readback.php <host> <scene> <page-part> [page-part ...] [--target-root=/abs/path] [--format=json|text]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_readback.php chuanjiafeng.net play 154229 douban 1\n";
echo " php scripts/seo_copy_readback.php chuanjiafeng.net search 相逢 --format=text\n";
}
function buildFieldSummary(array $arrTemplate, array $arrData): array
{
$arrSummary = [];
foreach ($arrTemplate as $strField => $mTemplateVal) {
$mValue = $arrData[$strField] ?? null;
$arrItem = [
'field' => $strField,
'present' => array_key_exists($strField, $arrData),
'type' => gettype($mValue),
];
if ($strField === 'guide_cards') {
$arrCards = is_array($mValue) ? $mValue : [];
$arrItem['count'] = count($arrCards);
$arrItem['preview'] = array_slice(array_map(static function (array $arrCard): array {
return [
'title' => trim((string)($arrCard['title'] ?? '')),
'text' => trim((string)($arrCard['text'] ?? '')),
'href' => trim((string)($arrCard['href'] ?? '')),
];
}, array_filter($arrCards, 'is_array')), 0, 2);
} else {
$strValue = trim((string)$mValue);
$arrItem['length'] = mb_strlen($strValue);
$arrItem['preview'] = mb_substr($strValue, 0, 120);
}
$arrSummary[] = $arrItem;
}
return $arrSummary;
}
function renderTextSummary(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'scene: ' . $arrSummary['scene'],
'page_key: ' . $arrSummary['page_key'],
'target_root: ' . $arrSummary['target_root'],
'resolved_path: ' . $arrSummary['resolved_path'],
'exists: ' . ($arrSummary['exists'] ? 'yes' : 'no'),
];
if (!empty($arrSummary['missing_fields'])) {
$arrLines[] = 'missing_fields: ' . implode(', ', $arrSummary['missing_fields']);
}
$arrLines[] = 'fields:';
foreach ($arrSummary['fields'] as $arrField) {
if ($arrField['field'] === 'guide_cards') {
$arrLines[] = '- ' . $arrField['field'] . ': count=' . ($arrField['count'] ?? 0);
foreach ((array)($arrField['preview'] ?? []) as $arrCard) {
$arrLines[] = ' • ' . ($arrCard['title'] ?: '(no title)') . ' | ' . ($arrCard['text'] ?: '(no text)');
}
continue;
}
$arrLines[] = '- ' . $arrField['field'] . ': len=' . ($arrField['length'] ?? 0) . ' | ' . ($arrField['preview'] ?? '');
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strHost = '';
$strScene = '';
$arrPageParts = [];
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = substr($strArg, strlen('--target-root='));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(substr($strArg, strlen('--format=')));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($strScene === '') {
$strScene = trim($strArg);
continue;
}
$arrPageParts[] = $strArg;
}
if ($strHost === '' || $strScene === '' || !in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
printUsage();
exit(1);
}
$strPageKey = SeoCopySchema::buildScenePageKey($strScene, $arrPageParts);
$strResolvedPath = SeoCopyStore::resolvePagePathFromRoot($strTargetRoot, $strHost, $strScene, $strPageKey);
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
$arrData = SeoCopyStore::getPageDataFromRoot($strTargetRoot, $strHost, $strScene, $strPageKey);
$arrMissingFields = [];
foreach (array_keys($arrTemplate) as $strField) {
if (!array_key_exists($strField, $arrData)) {
$arrMissingFields[] = $strField;
}
}
$arrSummary = [
'host' => $strHost,
'scene' => $strScene,
'page_parts' => $arrPageParts,
'page_key' => $strPageKey,
'target_root' => $strTargetRoot,
'resolved_path' => $strResolvedPath,
'exists' => $strResolvedPath !== '' && is_file($strResolvedPath),
'missing_fields' => $arrMissingFields,
'fields' => buildFieldSummary($arrTemplate, $arrData),
];
if ($strFormat === 'text') {
echo renderTextSummary($arrSummary);
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseIndexHelper.php';
use app\common\helper\SeoCopyReleaseIndexHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_release_index.php [--log-dir=/abs/path] [--format=json|text] [--out=/abs/path] [--allow-missing-log-dir=1]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_release_index.php --log-dir=storage/seo_copy_publish_logs --format=text\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strLogDir = dirname(__DIR__) . '/storage/seo_copy_publish_logs';
$strFormat = 'json';
$strOutPath = '';
$boolAllowMissing = false;
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--log-dir=')) {
$strLogDir = trim(substr($strArg, strlen('--log-dir=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(substr($strArg, strlen('--format=')));
continue;
}
if (str_starts_with($strArg, '--out=')) {
$strOutPath = trim(substr($strArg, strlen('--out=')));
continue;
}
if (str_starts_with($strArg, '--allow-missing-log-dir=')) {
$boolAllowMissing = in_array(strtolower(substr($strArg, strlen('--allow-missing-log-dir='))), ['1', 'true', 'yes'], true);
continue;
}
}
try {
$arrSummary = SeoCopyReleaseIndexHelper::buildSummary($strLogDir, $boolAllowMissing);
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
if ($strOutPath !== '') {
SeoCopyReleaseIndexHelper::writeSummary($strOutPath, $arrSummary);
}
if ($strFormat === 'text') {
echo SeoCopyReleaseIndexHelper::renderTextSummary($arrSummary);
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleasePortalHelper.php';
use app\common\helper\SeoCopyReleaseIndexHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
use app\common\helper\SeoCopyReleasePortalHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_release_portal.php [--log-dir=/abs/path] [--output-root=/abs/path] [--allow-missing-log-dir=1]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_release_portal.php --log-dir=storage/seo_copy_publish_logs --output-root=public/_seo_copy_release\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strLogDir = dirname(__DIR__) . '/storage/seo_copy_publish_logs';
$strOutputRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$boolAllowMissing = false;
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--log-dir=')) {
$strLogDir = trim(substr($strArg, strlen('--log-dir=')));
continue;
}
if (str_starts_with($strArg, '--output-root=')) {
$strOutputRoot = trim(substr($strArg, strlen('--output-root=')));
continue;
}
if (str_starts_with($strArg, '--allow-missing-log-dir=')) {
$boolAllowMissing = in_array(strtolower(substr($strArg, strlen('--allow-missing-log-dir='))), ['1', 'true', 'yes'], true);
continue;
}
}
try {
$arrSummary = SeoCopyReleaseIndexHelper::buildSummary($strLogDir, $boolAllowMissing);
SeoCopyReleasePortalHelper::writeArtifacts($strOutputRoot, $arrSummary);
SeoCopyPortalHomeHelper::writeArtifacts($strOutputRoot, SeoCopyPortalHomeHelper::buildSummary($strOutputRoot));
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
echo json_encode([
'log_dir' => realpath($strLogDir) ?: $strLogDir,
'output_root' => realpath($strOutputRoot) ?: $strOutputRoot,
'latest_index_html' => rtrim($strOutputRoot, '/') . '/latest-index.html',
'latest_index_json' => rtrim($strOutputRoot, '/') . '/latest-index.json',
'releases' => count((array)($arrSummary['releases'] ?? [])),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleasePortalHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseViewHelper.php';
use app\common\helper\SeoCopyReleaseIndexHelper;
use app\common\helper\SeoCopyPortalHomeHelper;
use app\common\helper\SeoCopyReleasePortalHelper;
use app\common\helper\SeoCopyReleaseViewHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_release_rollback.php <publish-log.json> [--dry-run=1]\n";
echo " php scripts/seo_copy_release_rollback.php --release-tag=tag [--log-dir=/abs/path] [--dry-run=1]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_release_rollback.php storage/seo_copy_publish_logs/20260404/172023_nightly.json\n";
}
function writeRawFile(string $strPath, string $strContent): void
{
$strDir = dirname($strPath);
if (!is_dir($strDir) && !mkdir($strDir, 0777, true) && !is_dir($strDir)) {
throw new RuntimeException('Failed to create directory: ' . $strDir);
}
file_put_contents($strPath, $strContent);
}
function writeJsonFile(string $strPath, array $arrData): void
{
$strDir = dirname($strPath);
if (!is_dir($strDir) && !mkdir($strDir, 0777, true) && !is_dir($strDir)) {
throw new RuntimeException('Failed to create directory: ' . $strDir);
}
file_put_contents($strPath, json_encode($arrData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
}
function resolvePortalRoot(): string
{
$strPortalRoot = trim((string)getenv('SEO_COPY_RELEASE_PORTAL_ROOT'));
if (in_array(strtolower($strPortalRoot), ['0', 'false', 'off', 'disable', 'disabled'], true)) {
return '';
}
return $strPortalRoot !== '' ? $strPortalRoot : dirname(__DIR__) . '/public/_seo_copy_release';
}
function findPublishLogByReleaseTag(string $strLogDir, string $strReleaseTag): string
{
if ($strLogDir === '' || !is_dir($strLogDir) || $strReleaseTag === '') {
return '';
}
$arrMatches = [];
$Iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($strLogDir, FilesystemIterator::SKIP_DOTS));
foreach ($Iterator as $FileInfo) {
/** @var SplFileInfo $FileInfo */
if (!$FileInfo->isFile() || strtolower($FileInfo->getExtension()) !== 'json') {
continue;
}
$strPath = str_replace('\\', '/', $FileInfo->getPathname());
if (str_contains($strPath, '/backups/')) {
continue;
}
if (str_ends_with($strPath, '_' . $strReleaseTag . '.json')) {
$arrMatches[] = $strPath;
}
}
sort($arrMatches, SORT_STRING);
return !empty($arrMatches) ? (string)end($arrMatches) : '';
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strLogPath = '';
$boolDryRun = false;
$strReleaseTag = '';
$strLogDir = dirname(__DIR__) . '/storage/seo_copy_publish_logs';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--dry-run=')) {
$boolDryRun = in_array(strtolower(substr($strArg, strlen('--dry-run='))), ['1', 'true', 'yes'], true);
continue;
}
if (str_starts_with($strArg, '--release-tag=')) {
$strReleaseTag = trim(substr($strArg, strlen('--release-tag=')));
continue;
}
if (str_starts_with($strArg, '--log-dir=')) {
$strLogDir = trim(substr($strArg, strlen('--log-dir=')));
continue;
}
if ($strLogPath === '') {
$strLogPath = $strArg;
}
}
if ($strLogPath === '' && $strReleaseTag !== '') {
$strLogPath = findPublishLogByReleaseTag($strLogDir, $strReleaseTag);
}
if ($strLogPath === '' || !is_file($strLogPath)) {
fwrite(STDERR, "Publish log not found: {$strLogPath}\n");
printUsage();
exit(1);
}
$strLogJson = (string)file_get_contents($strLogPath);
$arrLog = json_decode($strLogJson, true);
if (!is_array($arrLog) || !is_array($arrLog['published'] ?? null)) {
fwrite(STDERR, "Invalid publish log format.\n");
exit(1);
}
$arrSummary = [
'log_type' => 'rollback',
'created_at' => date('c'),
'publish_log' => realpath($strLogPath) ?: $strLogPath,
'release_tag' => (string)($arrLog['release_tag'] ?? ''),
'dry_run' => $boolDryRun,
'restored' => [],
'skipped' => [],
];
for ($intIndex = count($arrLog['published']) - 1; $intIndex >= 0; $intIndex--) {
$arrItem = $arrLog['published'][$intIndex];
if (!is_array($arrItem)) {
continue;
}
$strTargetPath = (string)($arrItem['target_path'] ?? '');
$strAction = (string)($arrItem['action'] ?? '');
$strBackupPath = (string)($arrItem['backup_path'] ?? '');
if ($strTargetPath === '' || $strAction === '') {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'reason' => 'invalid_log_item',
];
continue;
}
if ($strAction === 'update') {
if ($strBackupPath === '' || !is_file($strBackupPath)) {
$arrSummary['skipped'][] = [
'index' => $intIndex,
'target_path' => $strTargetPath,
'reason' => 'missing_backup',
];
continue;
}
if (!$boolDryRun) {
writeRawFile($strTargetPath, (string)file_get_contents($strBackupPath));
}
$arrSummary['restored'][] = [
'index' => $intIndex,
'target_path' => $strTargetPath,
'mode' => 'restore_backup',
'backup_path' => $strBackupPath,
];
continue;
}
if ($strAction === 'create') {
if (!$boolDryRun && is_file($strTargetPath)) {
unlink($strTargetPath);
}
$arrSummary['restored'][] = [
'index' => $intIndex,
'target_path' => $strTargetPath,
'mode' => 'delete_created_file',
'backup_path' => '',
];
continue;
}
$arrSummary['skipped'][] = [
'index' => $intIndex,
'target_path' => $strTargetPath,
'reason' => 'unsupported_action',
];
}
$arrSummary['restored_count'] = count($arrSummary['restored']);
$arrSummary['skipped_count'] = count($arrSummary['skipped']);
$strRollbackLogPath = rtrim($strLogDir, '/') . '/' . date('Ymd') . '/rollback_' . date('His') . '_' . ($arrSummary['release_tag'] ?: 'unknown') . '.json';
$strPortalRoot = resolvePortalRoot();
$arrSummary['rollback_log_path'] = $strRollbackLogPath;
$arrSummary['latest_index_path'] = rtrim($strLogDir, '/') . '/latest-index.json';
$arrSummary['portal_root'] = $strPortalRoot;
$arrSummary['portal_index_path'] = $strPortalRoot !== '' ? rtrim($strPortalRoot, '/') . '/latest-index.html' : '';
writeJsonFile($strRollbackLogPath, $arrSummary);
$arrLatestSummary = SeoCopyReleaseIndexHelper::buildSummary($strLogDir, true);
SeoCopyReleaseIndexHelper::writeSummary($arrSummary['latest_index_path'], $arrLatestSummary);
SeoCopyReleaseViewHelper::writeArtifacts($strLogDir, $arrLatestSummary);
if ($strPortalRoot !== '') {
SeoCopyReleasePortalHelper::writeArtifacts($strPortalRoot, $arrLatestSummary);
SeoCopyPortalHomeHelper::writeArtifacts($strPortalRoot, SeoCopyPortalHomeHelper::buildSummary($strPortalRoot));
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyReleaseRunIndexHelper.php';
use app\common\helper\SeoCopyReleaseRunIndexHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_release_run_index.php [--runs-root=/abs/path] [--output-root=/abs/path] [--format=json|text]\n\n";
echo "Example:\n";
echo " php scripts/seo_copy_release_run_index.php --runs-root=storage/seo_copy_release_runs --output-root=public/_seo_copy_release/runs --format=text\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strRunsRoot = dirname(__DIR__) . '/storage/seo_copy_release_runs';
$strOutputRoot = dirname(__DIR__) . '/public/_seo_copy_release/runs';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--runs-root=')) {
$strRunsRoot = trim(substr($strArg, strlen('--runs-root=')));
continue;
}
if (str_starts_with($strArg, '--output-root=')) {
$strOutputRoot = trim(substr($strArg, strlen('--output-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
try {
$arrSummary = SeoCopyReleaseRunIndexHelper::buildSummary($strRunsRoot, true);
SeoCopyReleaseRunIndexHelper::writeArtifacts($strOutputRoot, $arrSummary);
} catch (Throwable $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
printUsage();
exit(1);
}
if ($strFormat === 'text') {
echo json_encode([
'runs_root' => realpath($strRunsRoot) ?: $strRunsRoot,
'output_root' => realpath($strOutputRoot) ?: $strOutputRoot,
'runs' => (int)($arrSummary['runs_count'] ?? 0),
'latest_run' => (string)(($arrSummary['latest_run'] ?? [])['run_name'] ?? ''),
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit(0);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyRuntimeAcceptanceHelper.php';
use app\common\helper\SeoCopyRuntimeAcceptanceHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_runtime_acceptance_failed_hosts_run.php [--host=liangzuan.net] [--category=route_runtime|play_runtime|file_output] [--limit=5] [--run-root=/abs/path] [--portal-root=/abs/path|off] [--format=json|text]\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$strHost = '';
$strCategory = '';
$intLimit = 10;
$strRunRoot = dirname(__DIR__) . '/storage/seo_copy_runtime_acceptance_runs';
$strPortalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--host=')) {
$strHost = trim(substr($strArg, strlen('--host=')));
continue;
}
if (str_starts_with($strArg, '--category=')) {
$strCategory = trim(substr($strArg, strlen('--category=')));
continue;
}
if (str_starts_with($strArg, '--limit=')) {
$intLimit = max(1, (int)substr($strArg, strlen('--limit=')));
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = trim(substr($strArg, strlen('--run-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
}
$arrResult = SeoCopyRuntimeAcceptanceHelper::runFailedHosts([
'host' => $strHost,
'category' => $strCategory,
'limit' => $intLimit,
'run_root' => $strRunRoot,
'portal_root' => $strPortalRoot,
]);
if ($strFormat === 'text') {
echo 'status: ' . (string)($arrResult['status'] ?? '') . PHP_EOL;
echo 'host_filter: ' . (string)($arrResult['host'] ?? '') . PHP_EOL;
echo 'category_filter: ' . (string)($arrResult['category'] ?? '') . PHP_EOL;
echo 'failed_hosts_count: ' . (int)($arrResult['failed_hosts_count'] ?? 0) . PHP_EOL;
echo 'processed_count: ' . (int)($arrResult['processed_count'] ?? 0) . PHP_EOL;
echo 'passed_count: ' . (int)($arrResult['passed_count'] ?? 0) . PHP_EOL;
echo 'failed_count: ' . (int)($arrResult['failed_count'] ?? 0) . PHP_EOL;
foreach ((array)($arrResult['hosts'] ?? []) as $arrHost) {
if (!is_array($arrHost)) {
continue;
}
echo '- ' . (string)($arrHost['host'] ?? '')
. ' status=' . (string)($arrHost['status'] ?? '')
. ' samples=' . (int)($arrHost['configured_samples'] ?? 0)
. ' processed=' . (int)($arrHost['processed_count'] ?? 0)
. ' passed=' . (int)($arrHost['passed_count'] ?? 0)
. ' failed=' . (int)($arrHost['failed_count'] ?? 0)
. PHP_EOL;
}
} else {
echo json_encode($arrResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
exit(((int)($arrResult['failed_count'] ?? 0)) === 0 ? 0 : 1);

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyRuntimeAcceptanceHelper.php';
use app\common\helper\SeoCopyRuntimeAcceptanceHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_runtime_acceptance_forge_run.php <host> [<video-id>] [--forge-id=1] [--base-url=https://host] [--run-root=/abs/path] [--portal-root=/abs/path|off] [--target-root=/abs/path] [--format=json|text]\n";
}
function renderText(array $arrSummary): string
{
$arrLines = [
'host: ' . (string)($arrSummary['host'] ?? ''),
'status: ' . (string)($arrSummary['status'] ?? ''),
'configured_samples: ' . (int)($arrSummary['configured_samples'] ?? 0),
];
if (!empty($arrSummary['selected_sample'])) {
$arrLines[] = 'selected_sample: video=' . (int)($arrSummary['selected_sample']['video_id'] ?? 0)
. ', forge=' . (int)($arrSummary['selected_sample']['forge_id'] ?? 0);
}
if (!empty($arrSummary['run']) && is_array($arrSummary['run'])) {
$arrLines[] = 'run_name: ' . (string)($arrSummary['run']['run_name'] ?? '');
$arrLines[] = 'all_passed: ' . (!empty($arrSummary['run']['all_passed']) ? 'yes' : 'no');
foreach ((array)($arrSummary['run']['checks'] ?? []) as $arrCheck) {
if (!is_array($arrCheck)) {
continue;
}
$arrLines[] = '- ' . (!empty($arrCheck['passed']) ? '[ok] ' : '[fail] ')
. (string)($arrCheck['label'] ?? '')
. ': ' . (string)($arrCheck['detail'] ?? '');
}
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 1) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intForgeId = 1;
$strBaseUrl = '';
$strRunRoot = dirname(__DIR__) . '/storage/seo_copy_runtime_acceptance_runs';
$strPortalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$strTargetRoot = dirname(__DIR__) . '/data/seo_copy';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--forge-id=')) {
$intForgeId = max(1, (int)substr($strArg, strlen('--forge-id=')));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = trim(substr($strArg, strlen('--run-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = trim(substr($strArg, strlen('--target-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = max(0, (int)$strArg);
continue;
}
}
if ($strHost === '') {
printUsage();
exit(1);
}
$arrResult = $intVideoId > 0
? SeoCopyRuntimeAcceptanceHelper::run($strHost, $intVideoId, [
'forge_id' => $intForgeId,
'base_url' => $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost),
'run_root' => $strRunRoot,
'portal_root' => $strPortalRoot,
'target_root' => $strTargetRoot,
])
: SeoCopyRuntimeAcceptanceHelper::runForgeConfiguredSample($strHost, [
'base_url' => $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost),
'run_root' => $strRunRoot,
'portal_root' => $strPortalRoot,
'target_root' => $strTargetRoot,
]);
if ($strFormat === 'text') {
echo renderText($arrResult);
} else {
echo json_encode($arrResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
exit(!empty($arrResult['run']['all_passed'] ?? $arrResult['all_passed'] ?? false) ? 0 : 1);

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyRuntimeAcceptanceHelper.php';
use app\common\helper\SeoCopyRuntimeAcceptanceHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_runtime_acceptance_host_run.php <host> [--base-url=https://host] [--run-root=/abs/path] [--portal-root=/abs/path|off] [--format=json|text]\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 1) {
printUsage();
exit(1);
}
$strHost = '';
$strBaseUrl = '';
$strRunRoot = dirname(__DIR__) . '/storage/seo_copy_runtime_acceptance_runs';
$strPortalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = trim(substr($strArg, strlen('--run-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
}
}
if ($strHost === '') {
printUsage();
exit(1);
}
$arrResult = SeoCopyRuntimeAcceptanceHelper::runHostConfiguredSamples($strHost, [
'base_url' => $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost),
'run_root' => $strRunRoot,
'portal_root' => $strPortalRoot,
]);
if ($strFormat === 'text') {
echo 'host: ' . (string)($arrResult['host'] ?? '') . PHP_EOL;
echo 'status: ' . (string)($arrResult['status'] ?? '') . PHP_EOL;
echo 'configured_samples: ' . (int)($arrResult['configured_samples'] ?? 0) . PHP_EOL;
echo 'processed_count: ' . (int)($arrResult['processed_count'] ?? 0) . PHP_EOL;
echo 'passed_count: ' . (int)($arrResult['passed_count'] ?? 0) . PHP_EOL;
echo 'failed_count: ' . (int)($arrResult['failed_count'] ?? 0) . PHP_EOL;
foreach ((array)($arrResult['targets'] ?? []) as $arrTarget) {
if (!is_array($arrTarget)) {
continue;
}
echo '- ' . (string)($arrTarget['host'] ?? '')
. ' video=' . (int)($arrTarget['video_id'] ?? 0)
. ' forge=' . (int)($arrTarget['forge_id'] ?? 0)
. ' status=' . (string)($arrTarget['status'] ?? '')
. PHP_EOL;
}
} else {
echo json_encode($arrResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
}
exit(((int)($arrResult['failed_count'] ?? 0)) === 0 ? 0 : 1);

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyRuntimeAcceptanceHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyRuntimeAcceptanceIndexHelper.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyPortalHomeHelper.php';
use app\common\helper\SeoCopyPortalHomeHelper;
use app\common\helper\SeoCopyRuntimeAcceptanceHelper;
use app\common\helper\SeoCopyRuntimeAcceptanceIndexHelper;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_runtime_acceptance_run.php <host> <video-id> [--forge-id=1] [--base-url=https://host] [--run-root=/abs/path] [--portal-root=/abs/path|off] [--format=json|text]\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 2) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intForgeId = 1;
$strBaseUrl = '';
$strRunRoot = dirname(__DIR__) . '/storage/seo_copy_runtime_acceptance_runs';
$strPortalRoot = dirname(__DIR__) . '/public/_seo_copy_release';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--forge-id=')) {
$intForgeId = max(1, (int)substr($strArg, strlen('--forge-id=')));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = trim(substr($strArg, strlen('--run-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
}
if ($strHost === '' || $intVideoId <= 0) {
printUsage();
exit(1);
}
$arrResult = SeoCopyRuntimeAcceptanceHelper::run($strHost, $intVideoId, [
'forge_id' => $intForgeId,
'base_url' => $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost),
'run_root' => $strRunRoot,
'portal_root' => $strPortalRoot,
]);
echo json_encode($arrResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit(!empty($arrResult['all_passed']) ? 0 : 1);

View File

@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_runtime_acceptance_verify.php <host> <video-id> [--forge-id=1] [--base-url=https://host] [--format=json|text]\n";
}
function runChildScript(string $strScript, array $arrArgs = []): array
{
$arrCommand = array_merge([PHP_BINARY, $strScript], $arrArgs);
$arrDescriptor = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$Process = proc_open($arrCommand, $arrDescriptor, $arrPipes, dirname(__DIR__));
if (!is_resource($Process)) {
return [
'ok' => false,
'exit_code' => 1,
'stdout' => '',
'stderr' => 'proc_open failed',
'json' => null,
];
}
fclose($arrPipes[0]);
$strStdout = stream_get_contents($arrPipes[1]);
$strStderr = stream_get_contents($arrPipes[2]);
fclose($arrPipes[1]);
fclose($arrPipes[2]);
$intExitCode = proc_close($Process);
$arrJson = json_decode($strStdout, true);
if (!is_array($arrJson)) {
$intJsonStart = strpos($strStdout, '{');
if ($intJsonStart !== false) {
$strJsonPayload = substr($strStdout, $intJsonStart);
$arrJson = json_decode($strJsonPayload, true);
}
}
return [
'ok' => $intExitCode === 0,
'exit_code' => $intExitCode,
'stdout' => is_string($strStdout) ? $strStdout : '',
'stderr' => is_string($strStderr) ? $strStderr : '',
'json' => is_array($arrJson) ? $arrJson : null,
];
}
function renderText(array $arrSummary): string
{
$arrLines = [
'host: ' . $arrSummary['host'],
'video_id: ' . $arrSummary['video_id'],
'forge_id: ' . $arrSummary['forge_id'],
'all_passed: ' . ($arrSummary['all_passed'] ? 'yes' : 'no'),
'suites:',
];
foreach ($arrSummary['suites'] as $strSuiteKey => $arrSuite) {
$arrLines[] = '- ' . (($arrSuite['passed'] ?? false) ? '[ok] ' : '[fail] ') . $strSuiteKey
. ': exit=' . (int)($arrSuite['exit_code'] ?? 1)
. ', checks=' . (int)($arrSuite['check_count'] ?? 0)
. ', passed=' . (int)($arrSuite['passed_count'] ?? 0)
. ', failed=' . (int)($arrSuite['failed_count'] ?? 0);
if (!empty($arrSuite['summary'])) {
$arrLines[] = ' ' . $arrSuite['summary'];
}
}
$arrLines[] = 'checks:';
foreach ($arrSummary['checks'] as $arrCheck) {
$arrLines[] = '- ' . ($arrCheck['passed'] ? '[ok] ' : '[fail] ') . $arrCheck['label'] . ': ' . $arrCheck['detail'];
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (count($arrArgs) < 2) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intForgeId = 1;
$strBaseUrl = '';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--forge-id=')) {
$intForgeId = max(1, (int)substr($strArg, strlen('--forge-id=')));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
}
if ($strHost === '' || $intVideoId <= 0) {
printUsage();
exit(1);
}
$strBaseUrl = $strBaseUrl !== '' ? $strBaseUrl : ('https://' . $strHost);
$strScriptsDir = __DIR__;
$arrSuiteDefs = [
'forge_route_group' => [
'script' => $strScriptsDir . '/seo_copy_forge_route_group_verify.php',
'args' => [$strHost, (string)$intVideoId, (string)$intForgeId, '--base-url=' . $strBaseUrl, '--format=json'],
'summary' => static function (array $arrJson): string {
return 'group=' . (string)($arrJson['forge_group'] ?? '-')
. ', detail=' . (string)($arrJson['detail_path'] ?? '-')
. ', forge=' . (string)($arrJson['forge_path'] ?? '-');
},
],
'forge_output_runtime' => [
'script' => $strScriptsDir . '/seo_copy_forge_output_verify.php',
'args' => [$strHost, (string)$intVideoId, '--base-url=' . $strBaseUrl, '--format=json'],
'summary' => static function (array $arrJson): string {
return 'entry=' . (int)($arrJson['entry_links_detected'] ?? 0)
. ', current=' . implode(',', (array)($arrJson['current_output_kinds'] ?? []))
. ', expanded=' . implode(',', (array)($arrJson['expanded_output_kinds'] ?? []));
},
],
'play_runtime' => [
'script' => $strScriptsDir . '/seo_copy_play_route_verify.php',
'args' => [$strHost, (string)$intVideoId, '--base-url=' . $strBaseUrl, '--format=json'],
'summary' => static function (array $arrJson): string {
return 'play=' . (string)($arrJson['play_path'] ?? '-')
. ', type=' . (string)($arrJson['play_type'] ?? '-')
. ', index=' . (int)($arrJson['play_index'] ?? 0);
},
],
'forge_output_files' => [
'script' => $strScriptsDir . '/seo_copy_forge_output_file_verify.php',
'args' => [$strHost, (string)$intVideoId, '--format=json'],
'summary' => static function (array $arrJson): string {
return 'xml=' . (int)($arrJson['expanded_xml_entries'] ?? 0)
. ', json=' . (int)($arrJson['expanded_json_count'] ?? 0);
},
],
];
$arrSuites = [];
$arrChecks = [];
foreach ($arrSuiteDefs as $strSuiteKey => $arrSuiteDef) {
$arrChild = runChildScript((string)$arrSuiteDef['script'], (array)$arrSuiteDef['args']);
$arrJson = is_array($arrChild['json']) ? $arrChild['json'] : [];
$arrChildChecks = is_array($arrJson['checks'] ?? null) ? (array)$arrJson['checks'] : [];
$intCheckCount = count($arrChildChecks);
$intPassedCount = count(array_filter($arrChildChecks, static fn(array $arrCheck): bool => !empty($arrCheck['passed'])));
$intFailedCount = max(0, $intCheckCount - $intPassedCount);
$boolPassed = $arrChild['ok'] && !empty($arrJson) && !empty($arrJson['all_passed']);
$arrSuites[$strSuiteKey] = [
'passed' => $boolPassed,
'exit_code' => (int)($arrChild['exit_code'] ?? 1),
'check_count' => $intCheckCount,
'passed_count' => $intPassedCount,
'failed_count' => $intFailedCount,
'summary' => !empty($arrJson) && is_callable($arrSuiteDef['summary']) ? $arrSuiteDef['summary']($arrJson) : trim((string)($arrChild['stderr'] ?? '')),
'stderr' => (string)($arrChild['stderr'] ?? ''),
'json' => $arrJson,
];
$arrChecks[] = [
'label' => $strSuiteKey . '_all_passed',
'passed' => $boolPassed,
'detail' => 'exit=' . (int)($arrChild['exit_code'] ?? 1) . ', failed_checks=' . $intFailedCount,
];
}
$boolAllPassed = !in_array(false, array_column($arrChecks, 'passed'), true);
$arrSummary = [
'host' => $strHost,
'video_id' => $intVideoId,
'forge_id' => $intForgeId,
'suites' => $arrSuites,
'checks' => $arrChecks,
'all_passed' => $boolAllPassed,
'verified_at' => date(DATE_ATOM),
];
if ($strFormat === 'text') {
echo renderText($arrSummary);
exit($boolAllPassed ? 0 : 1);
}
echo json_encode($arrSummary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
exit($boolAllPassed ? 0 : 1);

View File

@@ -0,0 +1,416 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopyRuntimeAcceptanceHelper.php';
use app\common\helper\SeoCopyRuntimeAcceptanceHelper;
use app\model\DomainModel;
use think\App;
function printUsage(): void
{
echo "Usage:\n";
echo " php scripts/seo_copy_runtime_governance_run.php <host> [<video-id>] [--forge-id=1] [--manifest=/abs/path] [--base-url=http://127.0.0.1:8910] [--target-root=/abs/path] [--run-root=/abs/path] [--portal-root=/abs/path|off] [--format=json|text]\n";
}
function ensureAppInitialized(): void
{
static $boolInitialized = false;
if ($boolInitialized) {
return;
}
(new App())->initialize();
$boolInitialized = true;
}
function runChildScript(string $strScript, array $arrArgs = []): array
{
$arrCommand = array_merge([PHP_BINARY, $strScript], $arrArgs);
$arrDescriptor = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$Process = proc_open($arrCommand, $arrDescriptor, $arrPipes, dirname(__DIR__));
if (!is_resource($Process)) {
return [
'ok' => false,
'exit_code' => 1,
'stdout' => '',
'stderr' => 'proc_open failed',
'json' => null,
];
}
fclose($arrPipes[0]);
$strStdout = stream_get_contents($arrPipes[1]);
$strStderr = stream_get_contents($arrPipes[2]);
fclose($arrPipes[1]);
fclose($arrPipes[2]);
$intExitCode = proc_close($Process);
$arrJson = json_decode($strStdout, true);
if (!is_array($arrJson)) {
$intJsonStart = strpos($strStdout, '{');
if ($intJsonStart !== false) {
$arrJson = json_decode(substr($strStdout, $intJsonStart), true);
}
}
return [
'ok' => $intExitCode === 0,
'exit_code' => $intExitCode,
'stdout' => (string)$strStdout,
'stderr' => (string)$strStderr,
'json' => is_array($arrJson) ? $arrJson : null,
];
}
function normalizeHost(string $strHost): string
{
return DomainModel::normalizeHost($strHost);
}
function resolvePath(string $strPath, string $strBaseDir): string
{
$strPath = trim($strPath);
if ($strPath === '') {
return '';
}
if (str_starts_with($strPath, '/') || preg_match('/^[A-Za-z]:[\\\\\\/]/', $strPath) === 1) {
return $strPath;
}
$strResolved = realpath($strBaseDir . '/' . ltrim($strPath, '/\\'));
if ($strResolved !== false) {
return $strResolved;
}
return rtrim($strBaseDir, '/') . '/' . ltrim($strPath, '/\\');
}
function buildGovernanceMatrix(string $strHostFilter = '', string $strMatchTypeFilter = ''): array
{
ensureAppInitialized();
$arrRows = (new DomainModel())->select()->toArray();
$arrItems = [];
$arrTotals = [
'total' => 0,
'exact' => 0,
'wildcard' => 0,
'seed_domain' => 0,
'seed_host' => 0,
'runtime_ready' => 0,
'runtime_missing' => 0,
];
foreach ($arrRows as $arrRow) {
if (!is_array($arrRow)) {
continue;
}
$strDomain = normalizeHost((string)($arrRow['d_domain'] ?? ''));
if ($strHostFilter !== '' && $strHostFilter !== $strDomain) {
continue;
}
$strMatchType = DomainModel::normalizeMatchType((string)($arrRow['d_match_type'] ?? DomainModel::MATCH_TYPE_EXACT));
if ($strMatchTypeFilter !== '' && $strMatchTypeFilter !== $strMatchType) {
continue;
}
$strParentDomain = DomainModel::normalizeParentDomain((string)($arrRow['d_parent_domain'] ?? ''));
$strSeedScope = DomainModel::normalizeSeedScope((string)($arrRow['d_seed_scope'] ?? DomainModel::SEED_SCOPE_DOMAIN));
$arrSeoCfg = DomainModel::normalizeSeoCfg((array)($arrRow['d_seo_cfg'] ?? []), null);
$arrRuntimeSamples = (array)($arrSeoCfg['runtime_acceptance']['samples'] ?? []);
$boolRuntimeReady = !empty($arrSeoCfg['runtime_acceptance']['enable']) && !empty($arrRuntimeSamples);
$strDiagnosticHost = $strMatchType === DomainModel::MATCH_TYPE_WILDCARD && $strParentDomain !== ''
? 'governance.' . $strParentDomain
: $strDomain;
$arrResolved = DomainModel::resolveDomainContextByHost($strDiagnosticHost);
$arrItems[] = [
'host' => $strDomain,
'stored_domain' => DomainModel::normalizeStoredDomain($strDomain, $strMatchType),
'match_type' => $strMatchType,
'parent_domain' => $strParentDomain,
'seed_scope' => $strSeedScope,
'runtime_acceptance_enabled' => !empty($arrSeoCfg['runtime_acceptance']['enable']),
'runtime_samples_count' => count($arrRuntimeSamples),
'runtime_samples' => array_values(array_map(static function ($arrSample): array {
return [
'video_id' => (int)($arrSample['video_id'] ?? 0),
'forge_id' => (int)($arrSample['forge_id'] ?? 0),
];
}, $arrRuntimeSamples)),
'diagnostic_host' => $strDiagnosticHost,
'resolved_match_type' => (string)($arrResolved['match_type'] ?? ''),
'resolved_match_reason' => (string)($arrResolved['match_reason'] ?? ''),
'resolved_seed_scope' => (string)($arrResolved['seed_scope'] ?? ''),
'resolved_from_cache' => !empty($arrResolved['resolved_from_cache']),
'resolved_lookup_state' => (string)($arrResolved['resolved_lookup_state'] ?? ''),
];
$arrTotals['total']++;
$arrTotals[$strMatchType] = (int)($arrTotals[$strMatchType] ?? 0) + 1;
$arrTotals[$strSeedScope === DomainModel::SEED_SCOPE_HOST ? 'seed_host' : 'seed_domain']++;
$arrTotals[$boolRuntimeReady ? 'runtime_ready' : 'runtime_missing']++;
}
return [
'generated_at' => date(DATE_ATOM),
'host_filter' => $strHostFilter,
'match_type_filter' => $strMatchTypeFilter,
'totals' => $arrTotals,
'items' => $arrItems,
'sample_hosts' => array_values(array_filter(array_map(static fn(array $arrItem) => (string)($arrItem['host'] ?? ''), $arrItems))),
];
}
function renderMatrixText(array $arrMatrix): string
{
$arrLines = [
'matrix_total: ' . (int)($arrMatrix['totals']['total'] ?? 0),
'exact_count: ' . (int)($arrMatrix['totals']['exact'] ?? 0),
'wildcard_count: ' . (int)($arrMatrix['totals']['wildcard'] ?? 0),
'runtime_ready_count: ' . (int)($arrMatrix['totals']['runtime_ready'] ?? 0),
'runtime_missing_count: ' . (int)($arrMatrix['totals']['runtime_missing'] ?? 0),
'items:',
];
foreach (array_slice((array)($arrMatrix['items'] ?? []), 0, 20) as $arrItem) {
$arrLines[] = '- ' . ($arrItem['match_type'] ?? '')
. ' | ' . ($arrItem['host'] ?? '')
. ' | parent=' . ($arrItem['parent_domain'] ?? '-')
. ' | seed=' . ($arrItem['seed_scope'] ?? '-')
. ' | runtime=' . (!empty($arrItem['runtime_acceptance_enabled']) ? 'ready' : 'missing')
. ' | diag=' . ($arrItem['resolved_match_reason'] ?? '-')
. ' | cache=' . (!empty($arrItem['resolved_from_cache']) ? 'hit' : 'miss');
}
return implode(PHP_EOL, $arrLines) . PHP_EOL;
}
function renderSummaryText(array $arrSummary): string
{
$arrLines = [
'host: ' . (string)($arrSummary['host'] ?? ''),
'video_id: ' . (int)($arrSummary['video_id'] ?? 0),
'forge_id: ' . (int)($arrSummary['forge_id'] ?? 0),
'manifest: ' . (string)($arrSummary['manifest'] ?? ''),
'base_url: ' . (string)($arrSummary['base_url'] ?? ''),
'all_passed: ' . (!empty($arrSummary['all_passed']) ? 'yes' : 'no'),
'matrix:',
trim((string)($arrSummary['matrix_text'] ?? '')),
'page_inspection:',
trim((string)($arrSummary['page_text'] ?? '')),
'runtime_acceptance:',
trim((string)($arrSummary['runtime_text'] ?? '')),
'diagnostics:',
trim((string)($arrSummary['diagnostics_text'] ?? '')),
];
return implode(PHP_EOL, array_filter($arrLines, static fn(string $strLine): bool => $strLine !== '')) . PHP_EOL;
}
$arrArgs = $argv;
array_shift($arrArgs);
if (empty($arrArgs)) {
printUsage();
exit(1);
}
$strHost = '';
$intVideoId = 0;
$intForgeId = 1;
$strBaseDir = dirname(__DIR__);
$strWorkingDir = getcwd() ?: $strBaseDir;
$strManifest = $strBaseDir . '/data/seo_copy_jobs/examples/chuanjiafeng-batch.json';
$strTargetRoot = $strBaseDir . '/data/seo_copy';
$strBaseUrl = 'http://127.0.0.1:8910';
$strRunRoot = $strBaseDir . '/storage/seo_copy_runtime_acceptance_runs';
$strPortalRoot = $strBaseDir . '/public/_seo_copy_release';
$strFormat = 'json';
foreach ($arrArgs as $strArg) {
if (str_starts_with($strArg, '--forge-id=')) {
$intForgeId = max(1, (int)substr($strArg, strlen('--forge-id=')));
continue;
}
if (str_starts_with($strArg, '--manifest=')) {
$strManifest = trim(substr($strArg, strlen('--manifest=')));
continue;
}
if (str_starts_with($strArg, '--target-root=')) {
$strTargetRoot = trim(substr($strArg, strlen('--target-root=')));
continue;
}
if (str_starts_with($strArg, '--base-url=')) {
$strBaseUrl = rtrim(substr($strArg, strlen('--base-url=')), '/');
continue;
}
if (str_starts_with($strArg, '--run-root=')) {
$strRunRoot = trim(substr($strArg, strlen('--run-root=')));
continue;
}
if (str_starts_with($strArg, '--portal-root=')) {
$strPortalRoot = trim(substr($strArg, strlen('--portal-root=')));
continue;
}
if (str_starts_with($strArg, '--format=')) {
$strFormat = strtolower(trim(substr($strArg, strlen('--format='))));
continue;
}
if ($strHost === '') {
$strHost = trim($strArg);
continue;
}
if ($intVideoId === 0) {
$intVideoId = (int)$strArg;
continue;
}
}
if ($strHost === '') {
printUsage();
exit(1);
}
$strManifest = resolvePath($strManifest, $strWorkingDir);
$strTargetRoot = resolvePath($strTargetRoot, $strWorkingDir);
$strRunRoot = resolvePath($strRunRoot, $strWorkingDir);
$strPortalRoot = $strPortalRoot === 'off' ? 'off' : resolvePath($strPortalRoot, $strWorkingDir);
ensureAppInitialized();
$arrMatrix = buildGovernanceMatrix($strHost, '');
$arrDiagnostics = DomainModel::resolveDomainContextByHost($strHost);
$arrRuntimeSummary = $intVideoId > 0
? SeoCopyRuntimeAcceptanceHelper::verify($strHost, $intVideoId, [
'forge_id' => $intForgeId,
'base_url' => $strBaseUrl,
'target_root' => $strTargetRoot,
])
: SeoCopyRuntimeAcceptanceHelper::runHostConfiguredSamples($strHost, [
'base_url' => $strBaseUrl,
'run_root' => $strRunRoot,
'portal_root' => $strPortalRoot,
'target_root' => $strTargetRoot,
]);
$arrPageResult = ['ok' => false, 'exit_code' => 1, 'stdout' => '', 'stderr' => '', 'json' => null];
if ($strManifest !== '' && is_file($strManifest)) {
$arrPageResult = runChildScript(
dirname(__DIR__) . '/scripts/seo_copy_batch_front_verify.php',
[$strManifest, '--target-root=' . $strTargetRoot, '--base-url=' . $strBaseUrl, '--format=json']
);
}
$arrPageJson = is_array($arrPageResult['json'] ?? null) ? (array)$arrPageResult['json'] : [];
$arrPageItems = array_values(array_filter((array)($arrPageJson['items'] ?? []), 'is_array'));
$intPagePassed = count(array_filter($arrPageItems, static fn(array $arrItem): bool => !empty($arrItem['all_matched'])));
$intPageFailed = max(0, count($arrPageItems) - $intPagePassed);
$arrChecks = [];
$arrChecks[] = [
'label' => 'matrix_has_rows',
'passed' => (int)($arrMatrix['totals']['total'] ?? 0) > 0,
'detail' => 'rows=' . (int)($arrMatrix['totals']['total'] ?? 0),
];
$arrChecks[] = [
'label' => 'matrix_has_runtime_ready',
'passed' => (int)($arrMatrix['totals']['runtime_ready'] ?? 0) > 0,
'detail' => 'runtime_ready=' . (int)($arrMatrix['totals']['runtime_ready'] ?? 0),
];
$arrChecks[] = [
'label' => 'page_inspection_all_matched',
'passed' => !empty($arrPageJson) && !empty($arrPageJson['total']) && $intPageFailed === 0,
'detail' => 'passed=' . $intPagePassed . ', failed=' . $intPageFailed,
];
$arrChecks[] = [
'label' => 'runtime_acceptance_passed',
'passed' => !empty($arrRuntimeSummary['all_passed']),
'detail' => !empty($arrRuntimeSummary['all_passed']) ? 'passed' : 'failed',
];
$arrChecks[] = [
'label' => 'diagnostics_resolved',
'passed' => !empty($arrDiagnostics) && (string)($arrDiagnostics['match_reason'] ?? '') !== 'miss',
'detail' => json_encode([
'match_type' => $arrDiagnostics['match_type'] ?? '',
'match_reason' => $arrDiagnostics['match_reason'] ?? '',
'seed_scope' => $arrDiagnostics['seed_scope'] ?? '',
'resolved_from_cache' => $arrDiagnostics['resolved_from_cache'] ?? false,
'resolved_lookup_state' => $arrDiagnostics['resolved_lookup_state'] ?? '',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
];
$boolAllPassed = !in_array(false, array_column($arrChecks, 'passed'), true);
$arrSummary = [
'host' => $strHost,
'video_id' => $intVideoId,
'forge_id' => $intForgeId,
'manifest' => $strManifest,
'base_url' => $strBaseUrl,
'target_root' => $strTargetRoot,
'matrix' => $arrMatrix,
'diagnostics' => $arrDiagnostics,
'page_inspection' => $arrPageJson,
'runtime_acceptance' => $arrRuntimeSummary,
'checks' => $arrChecks,
'all_passed' => $boolAllPassed,
'verified_at' => date(DATE_ATOM),
];
$arrSummary['matrix_text'] = renderMatrixText($arrMatrix);
$arrSummary['page_text'] = trim((string)($arrPageResult['stdout'] ?? ''));
$arrRuntimeTextLines = [];
if (!empty($arrRuntimeSummary['suites']) && is_array($arrRuntimeSummary['suites'])) {
$arrRuntimeTextLines[] = 'status: ' . (!empty($arrRuntimeSummary['all_passed']) ? 'passed' : 'failed');
foreach ((array)$arrRuntimeSummary['suites'] as $strSuiteKey => $arrSuiteSummary) {
if (!is_array($arrSuiteSummary)) {
continue;
}
$arrRuntimeTextLines[] = $strSuiteKey . ': ' . (!empty($arrSuiteSummary['passed']) ? 'passed' : 'failed')
. ' | checks=' . (int)($arrSuiteSummary['check_count'] ?? 0)
. ' | passed=' . (int)($arrSuiteSummary['passed_count'] ?? 0)
. ' | failed=' . (int)($arrSuiteSummary['failed_count'] ?? 0);
}
} else {
$arrRuntimeTextLines = [
'status: ' . (string)($arrRuntimeSummary['status'] ?? ''),
'configured_samples: ' . (int)($arrRuntimeSummary['configured_samples'] ?? 0),
'processed_count: ' . (int)($arrRuntimeSummary['processed_count'] ?? ($arrRuntimeSummary['runs_count'] ?? 0)),
'passed_count: ' . (int)($arrRuntimeSummary['passed_count'] ?? 0),
'failed_count: ' . (int)($arrRuntimeSummary['failed_count'] ?? 0),
];
}
$arrSummary['runtime_text'] = implode(PHP_EOL, $arrRuntimeTextLines);
$arrSummary['diagnostics_text'] = implode(PHP_EOL, [
'requested_host: ' . (string)($arrDiagnostics['requested_host'] ?? ''),
'source_domain: ' . (string)($arrDiagnostics['source_domain'] ?? ''),
'parent_domain: ' . (string)($arrDiagnostics['parent_domain'] ?? ''),
'match_type: ' . (string)($arrDiagnostics['match_type'] ?? ''),
'match_reason: ' . (string)($arrDiagnostics['match_reason'] ?? ''),
'seed_scope: ' . (string)($arrDiagnostics['seed_scope'] ?? ''),
'seed_host: ' . (string)($arrDiagnostics['seed_host'] ?? ''),
'resolved_from_cache: ' . (!empty($arrDiagnostics['resolved_from_cache']) ? 'yes' : 'no'),
'resolved_lookup_state: ' . (string)($arrDiagnostics['resolved_lookup_state'] ?? ''),
]);
if ($strFormat === 'text') {
echo renderSummaryText($arrSummary);
exit($boolAllPassed ? 0 : 1);
}
echo json_encode($arrSummary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
exit($boolAllPassed ? 0 : 1);

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/app/common/helper/SeoCopyStore.php';
require dirname(__DIR__) . '/app/common/helper/SeoCopySchema.php';
use app\common\helper\SeoCopySchema;
use app\common\helper\SeoCopyStore;
function printUsage(): void
{
$scenes = implode(', ', SeoCopySchema::getSupportedScenes());
echo "Usage:\n";
echo " php scripts/seo_copy_scaffold.php <host> <scene> <page-part> [page-part ...] [--force]\n\n";
echo "Examples:\n";
echo " php scripts/seo_copy_scaffold.php chuanjiafeng.net home index\n";
echo " php scripts/seo_copy_scaffold.php chuanjiafeng.net search 相逢\n";
echo " php scripts/seo_copy_scaffold.php chuanjiafeng.net category_list dian-ying shao-shi-dian-ying\n\n";
echo "Supported scenes:\n";
echo " {$scenes}\n";
}
$arrArgs = $argv;
array_shift($arrArgs);
$boolForce = false;
$arrParts = [];
foreach ($arrArgs as $strArg) {
if ($strArg === '--force') {
$boolForce = true;
continue;
}
$arrParts[] = $strArg;
}
if (count($arrParts) < 3) {
printUsage();
exit(1);
}
$strHost = (string)array_shift($arrParts);
$strScene = (string)array_shift($arrParts);
if (!in_array($strScene, SeoCopySchema::getSupportedScenes(), true)) {
fwrite(STDERR, "Unsupported scene: {$strScene}\n");
printUsage();
exit(1);
}
$strPageKey = SeoCopySchema::buildScenePageKey($strScene, $arrParts);
$arrTemplate = SeoCopySchema::getSceneTemplate($strScene);
if (empty($arrTemplate)) {
fwrite(STDERR, "No template defined for scene: {$strScene}\n");
exit(1);
}
$strHostDir = strtolower(trim($strHost));
$strHostDir = preg_replace('/[^a-z0-9\\-_]+/', '-', $strHostDir);
$strHostDir = trim((string)$strHostDir, '-_');
$strSceneDir = strtolower(trim($strScene));
$strSceneDir = preg_replace('/[^a-z0-9\\-_]+/', '-', $strSceneDir);
$strSceneDir = trim((string)$strSceneDir, '-_');
if ($strHostDir === '' || $strSceneDir === '' || $strPageKey === '') {
fwrite(STDERR, "Invalid host / scene / page key.\n");
exit(1);
}
$strDir = dirname(__DIR__) . '/data/seo_copy/' . $strHostDir . '/' . $strSceneDir;
$strPath = $strDir . '/' . $strPageKey . '.json';
if (is_file($strPath) && !$boolForce) {
echo "Skip existing file: {$strPath}\n";
exit(0);
}
if (!is_dir($strDir) && !mkdir($strDir, 0777, true) && !is_dir($strDir)) {
fwrite(STDERR, "Failed to create directory: {$strDir}\n");
exit(1);
}
$strJson = json_encode($arrTemplate, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($strJson === false) {
fwrite(STDERR, "Failed to encode template.\n");
exit(1);
}
$strJson .= PHP_EOL;
if (file_put_contents($strPath, $strJson) === false) {
fwrite(STDERR, "Failed to write file: {$strPath}\n");
exit(1);
}
echo "Generated: {$strPath}\n";