restore bootstrap and seo helper chain after rebase drop
This commit is contained in:
12
code/scripts/_bootstrap_cli_app.php
Normal file
12
code/scripts/_bootstrap_cli_app.php
Normal 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;
|
||||
38
code/scripts/domain_bootstrap_batch_template.php
Normal file
38
code/scripts/domain_bootstrap_batch_template.php
Normal 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;
|
||||
79
code/scripts/domain_bootstrap_bundle_index.php
Normal file
79
code/scripts/domain_bootstrap_bundle_index.php
Normal 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;
|
||||
94
code/scripts/domain_bootstrap_env_apply.php
Normal file
94
code/scripts/domain_bootstrap_env_apply.php
Normal 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);
|
||||
}
|
||||
99
code/scripts/domain_bootstrap_import_ai.php
Normal file
99
code/scripts/domain_bootstrap_import_ai.php
Normal 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;
|
||||
196
code/scripts/domain_bootstrap_oncall_run.php
Normal file
196
code/scripts/domain_bootstrap_oncall_run.php
Normal 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);
|
||||
}
|
||||
190
code/scripts/domain_bootstrap_pipeline_run.php
Normal file
190
code/scripts/domain_bootstrap_pipeline_run.php
Normal 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);
|
||||
}
|
||||
90
code/scripts/domain_bootstrap_publish.php
Normal file
90
code/scripts/domain_bootstrap_publish.php
Normal 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;
|
||||
116
code/scripts/domain_bootstrap_register.php
Normal file
116
code/scripts/domain_bootstrap_register.php
Normal 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;
|
||||
262
code/scripts/domain_bootstrap_run.php
Normal file
262
code/scripts/domain_bootstrap_run.php
Normal 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;
|
||||
79
code/scripts/domain_bootstrap_run_index.php
Normal file
79
code/scripts/domain_bootstrap_run_index.php
Normal 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;
|
||||
124
code/scripts/domain_seo_bootstrap_apply.php
Normal file
124
code/scripts/domain_seo_bootstrap_apply.php
Normal 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);
|
||||
77
code/scripts/seo_copy_approved_index.php
Normal file
77
code/scripts/seo_copy_approved_index.php
Normal 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;
|
||||
339
code/scripts/seo_copy_batch_front_verify.php
Normal file
339
code/scripts/seo_copy_batch_front_verify.php
Normal 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;
|
||||
252
code/scripts/seo_copy_batch_import.php
Normal file
252
code/scripts/seo_copy_batch_import.php
Normal 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));
|
||||
}
|
||||
77
code/scripts/seo_copy_batch_prepare.php
Normal file
77
code/scripts/seo_copy_batch_prepare.php
Normal 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;
|
||||
404
code/scripts/seo_copy_batch_publish.php
Normal file
404
code/scripts/seo_copy_batch_publish.php
Normal 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;
|
||||
195
code/scripts/seo_copy_batch_verify.php
Normal file
195
code/scripts/seo_copy_batch_verify.php
Normal 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;
|
||||
769
code/scripts/seo_copy_domain_bootstrap.php
Normal file
769
code/scripts/seo_copy_domain_bootstrap.php
Normal 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);
|
||||
306
code/scripts/seo_copy_domain_bootstrap_batch.php
Normal file
306
code/scripts/seo_copy_domain_bootstrap_batch.php
Normal 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;
|
||||
99
code/scripts/seo_copy_domain_supply_batch.php
Normal file
99
code/scripts/seo_copy_domain_supply_batch.php
Normal 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;
|
||||
85
code/scripts/seo_copy_domain_supply_orchestration_run.php
Normal file
85
code/scripts/seo_copy_domain_supply_orchestration_run.php
Normal 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);
|
||||
49
code/scripts/seo_copy_facts.php
Normal file
49
code/scripts/seo_copy_facts.php
Normal 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;
|
||||
288
code/scripts/seo_copy_forge_output_file_verify.php
Normal file
288
code/scripts/seo_copy_forge_output_file_verify.php
Normal 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);
|
||||
290
code/scripts/seo_copy_forge_output_verify.php
Normal file
290
code/scripts/seo_copy_forge_output_verify.php
Normal 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);
|
||||
365
code/scripts/seo_copy_forge_route_group_verify.php
Normal file
365
code/scripts/seo_copy_forge_route_group_verify.php
Normal 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);
|
||||
280
code/scripts/seo_copy_front_verify.php
Normal file
280
code/scripts/seo_copy_front_verify.php
Normal 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;
|
||||
}
|
||||
26
code/scripts/seo_copy_generation_prepare.php
Normal file
26
code/scripts/seo_copy_generation_prepare.php
Normal 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;
|
||||
23
code/scripts/seo_copy_generation_status.php
Normal file
23
code/scripts/seo_copy_generation_status.php
Normal 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;
|
||||
304
code/scripts/seo_copy_play_route_verify.php
Normal file
304
code/scripts/seo_copy_play_route_verify.php
Normal 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);
|
||||
51
code/scripts/seo_copy_portal_home.php
Normal file
51
code/scripts/seo_copy_portal_home.php
Normal 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;
|
||||
154
code/scripts/seo_copy_prompt.php
Normal file
154
code/scripts/seo_copy_prompt.php
Normal 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;
|
||||
158
code/scripts/seo_copy_readback.php
Normal file
158
code/scripts/seo_copy_readback.php
Normal 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;
|
||||
65
code/scripts/seo_copy_release_index.php
Normal file
65
code/scripts/seo_copy_release_index.php
Normal 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;
|
||||
62
code/scripts/seo_copy_release_portal.php
Normal file
62
code/scripts/seo_copy_release_portal.php
Normal 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;
|
||||
226
code/scripts/seo_copy_release_rollback.php
Normal file
226
code/scripts/seo_copy_release_rollback.php
Normal 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;
|
||||
1172
code/scripts/seo_copy_release_run.php
Normal file
1172
code/scripts/seo_copy_release_run.php
Normal file
File diff suppressed because it is too large
Load Diff
61
code/scripts/seo_copy_release_run_index.php
Normal file
61
code/scripts/seo_copy_release_run_index.php
Normal 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;
|
||||
@@ -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);
|
||||
124
code/scripts/seo_copy_runtime_acceptance_forge_run.php
Normal file
124
code/scripts/seo_copy_runtime_acceptance_forge_run.php
Normal 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);
|
||||
85
code/scripts/seo_copy_runtime_acceptance_host_run.php
Normal file
85
code/scripts/seo_copy_runtime_acceptance_host_run.php
Normal 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);
|
||||
81
code/scripts/seo_copy_runtime_acceptance_run.php
Normal file
81
code/scripts/seo_copy_runtime_acceptance_run.php
Normal 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);
|
||||
217
code/scripts/seo_copy_runtime_acceptance_verify.php
Normal file
217
code/scripts/seo_copy_runtime_acceptance_verify.php
Normal 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);
|
||||
416
code/scripts/seo_copy_runtime_governance_run.php
Normal file
416
code/scripts/seo_copy_runtime_governance_run.php
Normal 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);
|
||||
100
code/scripts/seo_copy_scaffold.php
Normal file
100
code/scripts/seo_copy_scaffold.php
Normal 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";
|
||||
Reference in New Issue
Block a user