fix: stabilize spider md github sync
This commit is contained in:
@@ -17,6 +17,11 @@ class DomainSpiderMdRunHelper
|
|||||||
return dirname(__DIR__, 3) . '/storage/spider_md_runs';
|
return dirname(__DIR__, 3) . '/storage/spider_md_runs';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function activeRunRoot(): string
|
||||||
|
{
|
||||||
|
return self::baseRoot() . '/active';
|
||||||
|
}
|
||||||
|
|
||||||
public static function buildSummary(int $limit = 20): array
|
public static function buildSummary(int $limit = 20): array
|
||||||
{
|
{
|
||||||
$limit = max(1, min(100, $limit));
|
$limit = max(1, min(100, $limit));
|
||||||
@@ -26,13 +31,19 @@ class DomainSpiderMdRunHelper
|
|||||||
$github = self::resolveGithubConfig();
|
$github = self::resolveGithubConfig();
|
||||||
unset($gitee['token']);
|
unset($gitee['token']);
|
||||||
unset($github['token']);
|
unset($github['token']);
|
||||||
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
$activeSummaryPath = self::activeRunRoot() . '/summary.json';
|
||||||
if (is_array($matches)) {
|
$activePayload = self::readJsonFile($activeSummaryPath);
|
||||||
rsort($matches);
|
if (!empty($activePayload)) {
|
||||||
foreach (array_slice($matches, 0, $limit) as $summaryPath) {
|
$runs[] = $activePayload;
|
||||||
$payload = self::readJsonFile((string)$summaryPath);
|
} else {
|
||||||
if (!empty($payload)) {
|
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
||||||
$runs[] = $payload;
|
if (is_array($matches)) {
|
||||||
|
rsort($matches);
|
||||||
|
foreach (array_slice($matches, 0, $limit) as $summaryPath) {
|
||||||
|
$payload = self::readJsonFile((string)$summaryPath);
|
||||||
|
if (!empty($payload)) {
|
||||||
|
$runs[] = $payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,54 +66,36 @@ class DomainSpiderMdRunHelper
|
|||||||
$giteeConfig = self::resolveGiteeConfig();
|
$giteeConfig = self::resolveGiteeConfig();
|
||||||
$githubConfig = self::resolveGithubConfig();
|
$githubConfig = self::resolveGithubConfig();
|
||||||
self::ensureDir(self::baseRoot());
|
self::ensureDir(self::baseRoot());
|
||||||
|
self::ensureDir(self::activeRunRoot());
|
||||||
$forceRegenerate = !empty($options['force_regenerate']);
|
$forceRegenerate = !empty($options['force_regenerate']);
|
||||||
$previous = self::findLatestSuccessfulRunSummary();
|
$previous = self::findLatestSuccessfulRunSummary();
|
||||||
$today = date('Ymd');
|
|
||||||
$remoteTargetChanged = !empty($previous)
|
$remoteTargetChanged = !empty($previous)
|
||||||
? (self::giteeTargetChanged($previous, $giteeConfig) || self::githubTargetChanged($previous, $githubConfig))
|
? (self::giteeTargetChanged($previous, $giteeConfig) || self::githubTargetChanged($previous, $githubConfig))
|
||||||
: false;
|
: false;
|
||||||
$previousGiteePushStatus = trim((string)($previous['gitee_push_status'] ?? ''));
|
$previousGiteePushStatus = trim((string)($previous['gitee_push_status'] ?? ''));
|
||||||
$previousGithubPushStatus = trim((string)($previous['github_push_status'] ?? ''));
|
$previousGithubPushStatus = trim((string)($previous['github_push_status'] ?? ''));
|
||||||
$allowRetryToday = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true);
|
$allowRetryPush = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true)
|
||||||
$allowRetryToday = $allowRetryToday || in_array($previousGithubPushStatus, ['failed', 'partial_failed'], true);
|
|| in_array($previousGithubPushStatus, ['failed', 'partial_failed'], true);
|
||||||
|
$forceFullRemoteSync = $forceRegenerate || empty($previous) || $remoteTargetChanged || $allowRetryPush;
|
||||||
|
|
||||||
if (
|
if ($forceRegenerate || empty($previous)) {
|
||||||
!$forceRegenerate
|
|
||||||
&& !empty($previous)
|
|
||||||
&& substr((string)($previous['generated_at'] ?? ''), 0, 10) === date('Y-m-d')
|
|
||||||
&& !$remoteTargetChanged
|
|
||||||
&& !$allowRetryToday
|
|
||||||
) {
|
|
||||||
self::log($logger, '今日已生成过蜘蛛池 MD,跳过本轮;如需全新生成请使用强制重生成');
|
|
||||||
$previous['status'] = 'skipped';
|
|
||||||
$previous['skipped_reason'] = 'already_generated_today';
|
|
||||||
$previous['runtime_config'] = $config;
|
|
||||||
return $previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($forceRegenerate || empty($previous) || $remoteTargetChanged) {
|
|
||||||
if ($forceRegenerate) {
|
if ($forceRegenerate) {
|
||||||
self::clearAllRuns();
|
self::clearAllRuns();
|
||||||
} elseif ($remoteTargetChanged) {
|
|
||||||
self::log($logger, '检测到远端推送目标已变更,本轮按全新生成处理');
|
|
||||||
}
|
}
|
||||||
$dateDir = self::baseRoot() . '/' . $today;
|
|
||||||
self::ensureDir($dateDir);
|
|
||||||
$runId = date('His') . '_spider_md_' . substr(md5(uniqid('', true)), 0, 6);
|
$runId = date('His') . '_spider_md_' . substr(md5(uniqid('', true)), 0, 6);
|
||||||
$runRoot = $dateDir . '/' . $runId;
|
$runRoot = self::activeRunRoot();
|
||||||
self::ensureDir($runRoot);
|
self::ensureDir($runRoot);
|
||||||
self::log($logger, $forceRegenerate ? '开始强制重生成蜘蛛池 MD' : '开始首次生成蜘蛛池 MD');
|
self::log($logger, $forceRegenerate ? '开始强制重生成蜘蛛池 MD' : '开始首次生成蜘蛛池 MD');
|
||||||
} else {
|
} else {
|
||||||
$runId = (string)($previous['run_id'] ?? '');
|
$runId = date('His') . '_spider_md_' . substr(md5(uniqid('', true)), 0, 6);
|
||||||
$runRoot = (string)($previous['run_root'] ?? '');
|
$runRoot = self::activeRunRoot();
|
||||||
if ($runId === '' || $runRoot === '') {
|
|
||||||
throw new \RuntimeException('历史蜘蛛池MD运行目录无效,无法续写');
|
|
||||||
}
|
|
||||||
self::ensureDir($runRoot);
|
self::ensureDir($runRoot);
|
||||||
if ($allowRetryToday) {
|
self::log($logger, '开始增量续写蜘蛛池 MD');
|
||||||
self::log($logger, '检测到上一轮 Gitee 推送失败,本轮继续沿用当前目录并重试推送');
|
if ($remoteTargetChanged) {
|
||||||
} else {
|
self::log($logger, '检测到远端推送目标已变更,本轮保留现有分片,仅执行全量远端同步');
|
||||||
self::log($logger, '开始增量续写蜘蛛池 MD');
|
}
|
||||||
|
if ($allowRetryPush) {
|
||||||
|
self::log($logger, '检测到上一轮远端推送失败,本轮保留现有分片并重试远端同步');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +106,7 @@ class DomainSpiderMdRunHelper
|
|||||||
$existingState = self::loadExistingRunState($runRoot, (int)$config['max_links_per_file']);
|
$existingState = self::loadExistingRunState($runRoot, (int)$config['max_links_per_file']);
|
||||||
$aggregateLinks = [];
|
$aggregateLinks = [];
|
||||||
$writtenFiles = (array)($existingState['files'] ?? []);
|
$writtenFiles = (array)($existingState['files'] ?? []);
|
||||||
|
$changedFiles = [];
|
||||||
$chunkState = [
|
$chunkState = [
|
||||||
'records' => (array)($existingState['pending_records'] ?? []),
|
'records' => (array)($existingState['pending_records'] ?? []),
|
||||||
'max_links_per_file' => (int)$config['max_links_per_file'],
|
'max_links_per_file' => (int)$config['max_links_per_file'],
|
||||||
@@ -120,6 +114,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'generated_at' => date('Y-m-d H:i:s'),
|
'generated_at' => date('Y-m-d H:i:s'),
|
||||||
'run_root' => $runRoot,
|
'run_root' => $runRoot,
|
||||||
'files' => &$writtenFiles,
|
'files' => &$writtenFiles,
|
||||||
|
'changed_files' => &$changedFiles,
|
||||||
'known_urls' => (array)($existingState['known_urls'] ?? []),
|
'known_urls' => (array)($existingState['known_urls'] ?? []),
|
||||||
'reused_last_file' => (string)($existingState['reused_last_file'] ?? ''),
|
'reused_last_file' => (string)($existingState['reused_last_file'] ?? ''),
|
||||||
];
|
];
|
||||||
@@ -155,10 +150,14 @@ class DomainSpiderMdRunHelper
|
|||||||
/** @var DomainModel $domain */
|
/** @var DomainModel $domain */
|
||||||
$domain = $domainRow['domain'];
|
$domain = $domainRow['domain'];
|
||||||
$forgePolicy = $domain->getForgeSeoCfg();
|
$forgePolicy = $domain->getForgeSeoCfg();
|
||||||
|
$existingDetailCount = (int)(($existingState['detail_count_by_host'] ?? [])[$host] ?? 0);
|
||||||
|
$existingForgeCount = (int)(($existingState['forge_count_by_host'] ?? [])[$host] ?? 0);
|
||||||
|
$detailLimit = (int)$config['detail_limit_per_host'];
|
||||||
|
$forgeLimit = (int)$config['forge_limit_per_host'];
|
||||||
$domainProgress[$host] = [
|
$domainProgress[$host] = [
|
||||||
'row' => $domainRow,
|
'row' => $domainRow,
|
||||||
'detail_remaining' => (int)$config['detail_limit_per_host'] > 0 ? (int)$config['detail_limit_per_host'] : null,
|
'detail_remaining' => $detailLimit > 0 ? max(0, $detailLimit - $existingDetailCount) : null,
|
||||||
'forge_remaining' => (int)$config['forge_limit_per_host'] > 0 ? (int)$config['forge_limit_per_host'] : null,
|
'forge_remaining' => $forgeLimit > 0 ? max(0, $forgeLimit - $existingForgeCount) : null,
|
||||||
'forge_per_video' => max(0, (int)($forgePolicy['sitemap_count'] ?? 0)),
|
'forge_per_video' => max(0, (int)($forgePolicy['sitemap_count'] ?? 0)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -216,6 +215,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'max_links_per_file' => (int)$config['max_links_per_file'],
|
'max_links_per_file' => (int)$config['max_links_per_file'],
|
||||||
'config' => $config,
|
'config' => $config,
|
||||||
'mode' => $forceRegenerate || empty($previous) ? 'rebuild' : 'append',
|
'mode' => $forceRegenerate || empty($previous) ? 'rebuild' : 'append',
|
||||||
|
'remote_mode' => 'fixed-root-incremental',
|
||||||
'new_links' => [
|
'new_links' => [
|
||||||
'aggregate_count' => $newAggregateCount,
|
'aggregate_count' => $newAggregateCount,
|
||||||
'detail_count' => $newDetailCount,
|
'detail_count' => $newDetailCount,
|
||||||
@@ -223,6 +223,8 @@ class DomainSpiderMdRunHelper
|
|||||||
'total_links' => $newAggregateCount + $newDetailCount + $newForgeCount,
|
'total_links' => $newAggregateCount + $newDetailCount + $newForgeCount,
|
||||||
],
|
],
|
||||||
'files' => $writtenFiles,
|
'files' => $writtenFiles,
|
||||||
|
'changed_files' => array_values(array_unique($changedFiles)),
|
||||||
|
'full_remote_sync' => $forceFullRemoteSync ? 1 : 0,
|
||||||
'gitee' => array_diff_key($giteeConfig, ['token' => true]),
|
'gitee' => array_diff_key($giteeConfig, ['token' => true]),
|
||||||
'github' => array_diff_key($githubConfig, ['token' => true]),
|
'github' => array_diff_key($githubConfig, ['token' => true]),
|
||||||
'gitee_files' => [],
|
'gitee_files' => [],
|
||||||
@@ -237,11 +239,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'github_push_error' => '',
|
'github_push_error' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents(
|
self::writeSummaryArtifacts($runRoot, $summary);
|
||||||
$runRoot . '/summary.json',
|
|
||||||
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
|
||||||
);
|
|
||||||
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
|
||||||
|
|
||||||
$giteePush = self::pushRunToGitee($summary, $logger);
|
$giteePush = self::pushRunToGitee($summary, $logger);
|
||||||
$summary['gitee_files'] = (array)($giteePush['items'] ?? []);
|
$summary['gitee_files'] = (array)($giteePush['items'] ?? []);
|
||||||
@@ -256,31 +254,13 @@ class DomainSpiderMdRunHelper
|
|||||||
$summary['github_push_status'] = (string)($githubPush['status'] ?? 'skipped');
|
$summary['github_push_status'] = (string)($githubPush['status'] ?? 'skipped');
|
||||||
$summary['github_push_error'] = (string)($githubPush['error'] ?? '');
|
$summary['github_push_error'] = (string)($githubPush['error'] ?? '');
|
||||||
|
|
||||||
file_put_contents(
|
self::writeLinkArtifacts($runRoot, $summary);
|
||||||
$runRoot . '/gitee-links.json',
|
|
||||||
json_encode([
|
|
||||||
'run_id' => $runId,
|
|
||||||
'generated_at' => date(DATE_ATOM),
|
|
||||||
'status' => $summary['gitee_push_status'],
|
|
||||||
'error' => $summary['gitee_push_error'],
|
|
||||||
'items' => $summary['gitee_files'],
|
|
||||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
|
||||||
);
|
|
||||||
file_put_contents(
|
|
||||||
$runRoot . '/github-links.json',
|
|
||||||
json_encode([
|
|
||||||
'run_id' => $runId,
|
|
||||||
'generated_at' => date(DATE_ATOM),
|
|
||||||
'status' => $summary['github_push_status'],
|
|
||||||
'error' => $summary['github_push_error'],
|
|
||||||
'items' => $summary['github_files'],
|
|
||||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
|
||||||
);
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$runRoot . '/summary.json',
|
$runRoot . '/summary.json',
|
||||||
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
);
|
);
|
||||||
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
||||||
|
self::syncMetadataToRemotes($summary, $logger);
|
||||||
|
|
||||||
self::log($logger, '蜘蛛池 MD 生成完成,共 ' . count($writtenFiles) . ' 个文件');
|
self::log($logger, '蜘蛛池 MD 生成完成,共 ' . count($writtenFiles) . ' 个文件');
|
||||||
|
|
||||||
@@ -609,6 +589,7 @@ class DomainSpiderMdRunHelper
|
|||||||
return (string)($item['section_label'] ?? '');
|
return (string)($item['section_label'] ?? '');
|
||||||
}, $state['records']))),
|
}, $state['records']))),
|
||||||
];
|
];
|
||||||
|
$state['changed_files'][] = $fileName;
|
||||||
|
|
||||||
$state['records'] = [];
|
$state['records'] = [];
|
||||||
$state['reused_last_file'] = '';
|
$state['reused_last_file'] = '';
|
||||||
@@ -616,6 +597,11 @@ class DomainSpiderMdRunHelper
|
|||||||
|
|
||||||
protected static function findLatestSuccessfulRunSummary(): array
|
protected static function findLatestSuccessfulRunSummary(): array
|
||||||
{
|
{
|
||||||
|
$activeSummary = self::readJsonFile(self::activeRunRoot() . '/summary.json');
|
||||||
|
if (!empty($activeSummary) && (string)($activeSummary['status'] ?? '') === 'success') {
|
||||||
|
return $activeSummary;
|
||||||
|
}
|
||||||
|
|
||||||
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
$matches = glob(self::baseRoot() . '/*/*/summary.json');
|
||||||
if (!is_array($matches) || empty($matches)) {
|
if (!is_array($matches) || empty($matches)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -674,9 +660,45 @@ class DomainSpiderMdRunHelper
|
|||||||
'aggregate_count' => (int)($summary['aggregate_count'] ?? 0),
|
'aggregate_count' => (int)($summary['aggregate_count'] ?? 0),
|
||||||
'detail_count' => (int)($summary['detail_count'] ?? 0),
|
'detail_count' => (int)($summary['detail_count'] ?? 0),
|
||||||
'forge_count' => (int)($summary['forge_count'] ?? 0),
|
'forge_count' => (int)($summary['forge_count'] ?? 0),
|
||||||
|
'detail_count_by_host' => self::countExistingSectionByHost($files, $pendingRecords, 'detail'),
|
||||||
|
'forge_count_by_host' => self::countExistingSectionByHost($files, $pendingRecords, 'forge'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function countExistingSectionByHost(array $files, array $pendingRecords, string $section): array
|
||||||
|
{
|
||||||
|
$counts = [];
|
||||||
|
foreach ($files as $fileMeta) {
|
||||||
|
$filePath = (string)($fileMeta['file_path'] ?? '');
|
||||||
|
if ($filePath === '' || !is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (self::parseMarkdownChunkRecords($filePath) as $record) {
|
||||||
|
if ((string)($record['section'] ?? '') !== $section) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$host = trim((string)($record['host'] ?? ''));
|
||||||
|
if ($host === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$counts[$host] = (int)($counts[$host] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pendingRecords as $record) {
|
||||||
|
if ((string)($record['section'] ?? '') !== $section) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$host = trim((string)($record['host'] ?? ''));
|
||||||
|
if ($host === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$counts[$host] = (int)($counts[$host] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counts;
|
||||||
|
}
|
||||||
|
|
||||||
protected static function parseMarkdownChunkRecords(string $filePath): array
|
protected static function parseMarkdownChunkRecords(string $filePath): array
|
||||||
{
|
{
|
||||||
$lines = @file($filePath, FILE_IGNORE_NEW_LINES);
|
$lines = @file($filePath, FILE_IGNORE_NEW_LINES);
|
||||||
@@ -822,6 +844,8 @@ class DomainSpiderMdRunHelper
|
|||||||
foreach (glob(self::baseRoot() . '/*') ?: [] as $dateDir) {
|
foreach (glob(self::baseRoot() . '/*') ?: [] as $dateDir) {
|
||||||
self::deletePath((string)$dateDir);
|
self::deletePath((string)$dateDir);
|
||||||
}
|
}
|
||||||
|
self::deletePath(self::activeRunRoot());
|
||||||
|
self::ensureDir(self::activeRunRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function deletePath(string $path): void
|
protected static function deletePath(string $path): void
|
||||||
@@ -955,23 +979,8 @@ class DomainSpiderMdRunHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
$dateDir = basename(dirname((string)($summary['run_root'] ?? '')));
|
|
||||||
if (!preg_match('/^\d{8}$/', $dateDir)) {
|
|
||||||
$dateDir = date('Ymd', strtotime((string)($summary['generated_at'] ?? 'now')));
|
|
||||||
}
|
|
||||||
$baseRemoteDir = trim((string)$config['root'], '/');
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
$baseRemoteDir = trim($baseRemoteDir . '/' . $dateDir . '/' . (string)($summary['run_id'] ?? ''), '/');
|
$localFiles = self::resolveIncrementalLocalFiles($summary);
|
||||||
|
|
||||||
$localFiles = array_merge(
|
|
||||||
array_map(static function (array $file): string {
|
|
||||||
return (string)($file['file_path'] ?? '');
|
|
||||||
}, (array)($summary['files'] ?? [])),
|
|
||||||
[
|
|
||||||
(string)($summary['run_root'] ?? '') . '/summary.json',
|
|
||||||
(string)($summary['run_root'] ?? '') . '/summary.html',
|
|
||||||
(string)($summary['run_root'] ?? '') . '/gitee-links.json',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($localFiles as $filePath) {
|
foreach ($localFiles as $filePath) {
|
||||||
@@ -1007,7 +1016,7 @@ class DomainSpiderMdRunHelper
|
|||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'attempted' => 1,
|
'attempted' => 1,
|
||||||
'error' => '',
|
'error' => '',
|
||||||
'items' => $items,
|
'items' => self::buildRemoteItems($summary, $config, 'gitee'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,23 +1164,8 @@ class DomainSpiderMdRunHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
$dateDir = basename(dirname((string)($summary['run_root'] ?? '')));
|
|
||||||
if (!preg_match('/^\d{8}$/', $dateDir)) {
|
|
||||||
$dateDir = date('Ymd', strtotime((string)($summary['generated_at'] ?? 'now')));
|
|
||||||
}
|
|
||||||
$baseRemoteDir = trim((string)$config['root'], '/');
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
$baseRemoteDir = trim($baseRemoteDir . '/' . $dateDir . '/' . (string)($summary['run_id'] ?? ''), '/');
|
$localFiles = self::resolveIncrementalLocalFiles($summary);
|
||||||
|
|
||||||
$localFiles = array_merge(
|
|
||||||
array_map(static function (array $file): string {
|
|
||||||
return (string)($file['file_path'] ?? '');
|
|
||||||
}, (array)($summary['files'] ?? [])),
|
|
||||||
[
|
|
||||||
(string)($summary['run_root'] ?? '') . '/summary.json',
|
|
||||||
(string)($summary['run_root'] ?? '') . '/summary.html',
|
|
||||||
(string)($summary['run_root'] ?? '') . '/github-links.json',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($localFiles as $filePath) {
|
foreach ($localFiles as $filePath) {
|
||||||
@@ -1207,10 +1201,161 @@ class DomainSpiderMdRunHelper
|
|||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'attempted' => 1,
|
'attempted' => 1,
|
||||||
'error' => '',
|
'error' => '',
|
||||||
'items' => $items,
|
'items' => self::buildRemoteItems($summary, $config, 'github'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function resolveIncrementalLocalFiles(array $summary): array
|
||||||
|
{
|
||||||
|
$changedFileNames = array_values(array_unique(array_filter(array_map(static function ($item): string {
|
||||||
|
return trim((string)$item);
|
||||||
|
}, (array)($summary['changed_files'] ?? [])))));
|
||||||
|
$allFiles = array_values(array_filter(array_map(static function (array $file): string {
|
||||||
|
return (string)($file['file_path'] ?? '');
|
||||||
|
}, (array)($summary['files'] ?? []))));
|
||||||
|
|
||||||
|
if (!empty($summary['full_remote_sync'])) {
|
||||||
|
return $allFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($changedFileNames)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = [];
|
||||||
|
foreach ((array)($summary['files'] ?? []) as $file) {
|
||||||
|
$fileName = trim((string)($file['file_name'] ?? ''));
|
||||||
|
$filePath = (string)($file['file_path'] ?? '');
|
||||||
|
if ($fileName === '' || $filePath === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_array($fileName, $changedFileNames, true)) {
|
||||||
|
$selected[] = $filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildRemoteItems(array $summary, array $config, string $provider): array
|
||||||
|
{
|
||||||
|
$baseRemoteDir = trim((string)($config['root'] ?? ''), '/');
|
||||||
|
$items = [];
|
||||||
|
$fileNames = [];
|
||||||
|
foreach ((array)($summary['files'] ?? []) as $file) {
|
||||||
|
$fileName = trim((string)($file['file_name'] ?? ''));
|
||||||
|
$filePath = (string)($file['file_path'] ?? '');
|
||||||
|
if ($fileName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fileNames[] = [
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'local_path' => $filePath,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$fileNames[] = ['file_name' => 'summary.json', 'local_path' => (string)($summary['run_root'] ?? '') . '/summary.json'];
|
||||||
|
$fileNames[] = ['file_name' => 'summary.html', 'local_path' => (string)($summary['run_root'] ?? '') . '/summary.html'];
|
||||||
|
$fileNames[] = ['file_name' => $provider . '-links.json', 'local_path' => (string)($summary['run_root'] ?? '') . '/' . $provider . '-links.json'];
|
||||||
|
|
||||||
|
foreach ($fileNames as $fileMeta) {
|
||||||
|
$fileName = (string)($fileMeta['file_name'] ?? '');
|
||||||
|
if ($fileName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$remotePath = trim($baseRemoteDir . '/' . $fileName, '/');
|
||||||
|
$items[] = [
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'remote_path' => $remotePath,
|
||||||
|
'html_url' => $provider === 'github'
|
||||||
|
? self::buildGithubHtmlUrl($config, $remotePath)
|
||||||
|
: self::buildGiteeHtmlUrl($config, $remotePath),
|
||||||
|
'download_url' => $provider === 'github'
|
||||||
|
? self::buildGithubRawUrl($config, $remotePath)
|
||||||
|
: self::buildGiteeRawUrl($config, $remotePath),
|
||||||
|
'local_path' => (string)($fileMeta['local_path'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeSummaryArtifacts(string $runRoot, array $summary): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$runRoot . '/summary.json',
|
||||||
|
json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
file_put_contents($runRoot . '/summary.html', self::renderSummaryHtml($summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function writeLinkArtifacts(string $runRoot, array $summary): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$runRoot . '/gitee-links.json',
|
||||||
|
json_encode([
|
||||||
|
'run_id' => (string)($summary['run_id'] ?? ''),
|
||||||
|
'generated_at' => date(DATE_ATOM),
|
||||||
|
'status' => (string)($summary['gitee_push_status'] ?? 'pending'),
|
||||||
|
'error' => (string)($summary['gitee_push_error'] ?? ''),
|
||||||
|
'items' => (array)($summary['gitee_files'] ?? []),
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
file_put_contents(
|
||||||
|
$runRoot . '/github-links.json',
|
||||||
|
json_encode([
|
||||||
|
'run_id' => (string)($summary['run_id'] ?? ''),
|
||||||
|
'generated_at' => date(DATE_ATOM),
|
||||||
|
'status' => (string)($summary['github_push_status'] ?? 'pending'),
|
||||||
|
'error' => (string)($summary['github_push_error'] ?? ''),
|
||||||
|
'items' => (array)($summary['github_files'] ?? []),
|
||||||
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function syncMetadataToRemotes(array $summary, ?callable $logger = null): void
|
||||||
|
{
|
||||||
|
self::syncMetadataToGitee($summary, $logger);
|
||||||
|
self::syncMetadataToGithub($summary, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function syncMetadataToGitee(array $summary, ?callable $logger = null): void
|
||||||
|
{
|
||||||
|
$config = self::resolveGiteeConfig();
|
||||||
|
if (empty($config['enabled']) || empty($config['configured'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
|
foreach (['summary.json', 'summary.html', 'gitee-links.json'] as $fileName) {
|
||||||
|
$filePath = (string)($summary['run_root'] ?? '') . '/' . $fileName;
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$remotePath = trim($baseRemoteDir . '/' . $fileName, '/');
|
||||||
|
self::upsertGiteeFile($config, $remotePath, (string)file_get_contents($filePath), 'spider md metadata update ' . $fileName);
|
||||||
|
self::log($logger, '已更新 Gitee 元数据:' . $fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function syncMetadataToGithub(array $summary, ?callable $logger = null): void
|
||||||
|
{
|
||||||
|
$config = self::resolveGithubConfig();
|
||||||
|
if (empty($config['enabled']) || empty($config['configured'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseRemoteDir = trim((string)$config['root'], '/');
|
||||||
|
foreach (['summary.json', 'summary.html', 'github-links.json'] as $fileName) {
|
||||||
|
$filePath = (string)($summary['run_root'] ?? '') . '/' . $fileName;
|
||||||
|
if (!is_file($filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$remotePath = trim($baseRemoteDir . '/' . $fileName, '/');
|
||||||
|
self::upsertGithubFile($config, $remotePath, (string)file_get_contents($filePath), 'spider md metadata update ' . $fileName);
|
||||||
|
self::log($logger, '已更新 GitHub 元数据:' . $fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected static function upsertGithubFile(array $config, string $remotePath, string $content, string $message): array
|
protected static function upsertGithubFile(array $config, string $remotePath, string $content, string $message): array
|
||||||
{
|
{
|
||||||
$existing = self::githubRequest(
|
$existing = self::githubRequest(
|
||||||
@@ -1255,12 +1400,6 @@ class DomainSpiderMdRunHelper
|
|||||||
if (!function_exists('curl_init')) {
|
if (!function_exists('curl_init')) {
|
||||||
throw new \RuntimeException('当前 PHP 环境缺少 curl 扩展,无法推送 GitHub');
|
throw new \RuntimeException('当前 PHP 环境缺少 curl 扩展,无法推送 GitHub');
|
||||||
}
|
}
|
||||||
|
|
||||||
$curl = curl_init();
|
|
||||||
if ($curl === false) {
|
|
||||||
throw new \RuntimeException('初始化 GitHub 请求失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
$headers = [
|
$headers = [
|
||||||
'Accept: application/vnd.github+json',
|
'Accept: application/vnd.github+json',
|
||||||
'Authorization: Bearer ' . (string)$config['token'],
|
'Authorization: Bearer ' . (string)$config['token'],
|
||||||
@@ -1268,45 +1407,80 @@ class DomainSpiderMdRunHelper
|
|||||||
'User-Agent: SEONexus-SpiderMd',
|
'User-Agent: SEONexus-SpiderMd',
|
||||||
];
|
];
|
||||||
|
|
||||||
curl_setopt_array($curl, [
|
$maxAttempts = 3;
|
||||||
CURLOPT_URL => $url,
|
$lastError = '';
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
|
||||||
CURLOPT_TIMEOUT => 45,
|
$curl = curl_init();
|
||||||
CURLOPT_CUSTOMREQUEST => $method,
|
if ($curl === false) {
|
||||||
CURLOPT_HTTPHEADER => $headers,
|
throw new \RuntimeException('初始化 GitHub 请求失败');
|
||||||
]);
|
}
|
||||||
|
|
||||||
if (!empty($payload)) {
|
curl_setopt_array($curl, [
|
||||||
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 90,
|
||||||
|
CURLOPT_CUSTOMREQUEST => $method,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($payload)) {
|
||||||
|
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = curl_exec($curl);
|
||||||
|
$errno = curl_errno($curl);
|
||||||
|
$error = curl_error($curl);
|
||||||
|
$status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($errno !== 0) {
|
||||||
|
$lastError = 'GitHub 请求失败:' . $error;
|
||||||
|
if ($attempt < $maxAttempts) {
|
||||||
|
usleep(300000 * $attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new \RuntimeException($lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string)$response, true);
|
||||||
|
if ($allowNotFound && $status === 404) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status >= 400) {
|
||||||
|
$message = '';
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$message = trim((string)($decoded['message'] ?? ''));
|
||||||
|
if ($message === '') {
|
||||||
|
$message = json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message = trim((string)$response);
|
||||||
|
}
|
||||||
|
if ($message === '') {
|
||||||
|
$message = 'HTTP ' . $status . ',响应体为空';
|
||||||
|
} else {
|
||||||
|
$message = 'HTTP ' . $status . ',' . $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastError = 'GitHub 请求返回异常:' . $message;
|
||||||
|
if (in_array($status, [429, 500, 502, 503, 504], true) && $attempt < $maxAttempts) {
|
||||||
|
usleep(500000 * $attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new \RuntimeException($lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($decoded) && isset($decoded['content']) && is_array($decoded['content'])) {
|
||||||
|
$decoded['html_url'] = (string)($decoded['content']['html_url'] ?? '');
|
||||||
|
$decoded['download_url'] = (string)($decoded['content']['download_url'] ?? '');
|
||||||
|
$decoded['sha'] = (string)($decoded['content']['sha'] ?? ($decoded['sha'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = curl_exec($curl);
|
throw new \RuntimeException($lastError !== '' ? $lastError : 'GitHub 请求失败:未知异常');
|
||||||
$errno = curl_errno($curl);
|
|
||||||
$error = curl_error($curl);
|
|
||||||
$status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($curl);
|
|
||||||
|
|
||||||
if ($errno !== 0) {
|
|
||||||
throw new \RuntimeException('GitHub 请求失败:' . $error);
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode((string)$response, true);
|
|
||||||
if ($allowNotFound && $status === 404) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status >= 400) {
|
|
||||||
$message = is_array($decoded) ? (string)($decoded['message'] ?? json_encode($decoded, JSON_UNESCAPED_UNICODE)) : (string)$response;
|
|
||||||
throw new \RuntimeException('GitHub 请求返回异常:' . $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($decoded) && isset($decoded['content']) && is_array($decoded['content'])) {
|
|
||||||
$decoded['html_url'] = (string)($decoded['content']['html_url'] ?? '');
|
|
||||||
$decoded['download_url'] = (string)($decoded['content']['download_url'] ?? '');
|
|
||||||
$decoded['sha'] = (string)($decoded['content']['sha'] ?? ($decoded['sha'] ?? ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_array($decoded) ? $decoded : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function buildGithubHtmlUrl(array $config, string $remotePath): string
|
protected static function buildGithubHtmlUrl(array $config, string $remotePath): string
|
||||||
|
|||||||
@@ -100,3 +100,16 @@ GPT 模板专属工作区:
|
|||||||
6. 今天补充的后台体验修正:
|
6. 今天补充的后台体验修正:
|
||||||
- `蜘蛛池MD` 弹窗现在支持主表右侧固定 `一键复制MD` 按钮
|
- `蜘蛛池MD` 弹窗现在支持主表右侧固定 `一键复制MD` 按钮
|
||||||
- 复制内容已恢复为旧习惯:只复制 `.md` 直链,不复制 `summary.json / summary.html`
|
- 复制内容已恢复为旧习惯:只复制 `.md` 直链,不复制 `summary.json / summary.html`
|
||||||
|
7. 今天补充的 GitHub 推送稳定性修复:
|
||||||
|
- 已确认 `Spider MD` 后台显示的“GitHub 部分失败”并不等于全量失败,之前真实原因是 GitHub 中途短暂异常,旧日志未带出 `HTTP 状态码`
|
||||||
|
- `DomainSpiderMdRunHelper` 已补强 GitHub 请求层:
|
||||||
|
- 失败时记录 `HTTP 状态码`
|
||||||
|
- 空响应时明确显示“响应体为空”
|
||||||
|
- 对 `429 / 500 / 502 / 503 / 504` 增加自动重试
|
||||||
|
- 单次 GitHub 请求超时已放宽到 `90s`
|
||||||
|
- 本地 `storage/spider_md_runs` 目录权限已统一修正为 `www:www`,避免 `part-047.md` 这类文件因属主错误导致“还没推 GitHub 就先本地写失败”
|
||||||
|
- 当前已实测:
|
||||||
|
- GitHub 远端目录可一键清空
|
||||||
|
- 本地活动缓存可同步清空
|
||||||
|
- 重跑后 `part-001.md ~ part-047.md + summary.json + summary.html + github-links.json` 已完整成功推送
|
||||||
|
- 当前 Spider MD GitHub 推送的稳定状态应以 `run_id = 202353_spider_md_d28664` 这轮为准
|
||||||
|
|||||||
@@ -238,6 +238,70 @@
|
|||||||
3. 今天先不动 GPT 模板业务代码
|
3. 今天先不动 GPT 模板业务代码
|
||||||
4. 如果明天 `googlebot / bingbot` 仍大面积只打 `robots/category` 且持续 `444`,再单独开一轮 `Nginx 蜘蛛放行策略` 优化
|
4. 如果明天 `googlebot / bingbot` 仍大面积只打 `robots/category` 且持续 `444`,再单独开一轮 `Nginx 蜘蛛放行策略` 优化
|
||||||
|
|
||||||
|
## 10. 今日新增工程修复说明
|
||||||
|
|
||||||
|
今天除了日志判断,还额外处理了 `Spider MD -> GitHub` 这条工程链路,避免后续误把“推送工具异常”当成 SEO 或模板代码异常。
|
||||||
|
|
||||||
|
### 已确认的问题
|
||||||
|
|
||||||
|
1. 前一轮 `GitHub:部分失败` 不等于“没有推上”
|
||||||
|
2. 真实现象是:
|
||||||
|
- `part-001.md ~ part-022.md` 已上传
|
||||||
|
- 后续某次 GitHub 接口中途异常
|
||||||
|
- 旧代码只留下了空报错:`GitHub 请求返回异常:`
|
||||||
|
- 但 `summary.json / summary.html / github-links.json` 元数据又成功更新
|
||||||
|
3. 所以前端会表现成:
|
||||||
|
- GitHub 上已经有一批 md 文件
|
||||||
|
- 后台状态却还是 `partial_failed`
|
||||||
|
|
||||||
|
### 已完成的修复
|
||||||
|
|
||||||
|
1. GitHub 请求层现在会明确记录:
|
||||||
|
- `HTTP 状态码`
|
||||||
|
- 空响应体
|
||||||
|
- 原始异常信息
|
||||||
|
2. 对 `429 / 500 / 502 / 503 / 504` 已增加自动重试
|
||||||
|
3. GitHub 单次请求超时已从 `45s` 放宽到 `90s`
|
||||||
|
4. `storage/spider_md_runs` 目录属主已修正为 `www:www`
|
||||||
|
- 避免出现 `part-047.md: Permission denied` 这种“本地先失败,远端还没开始”的误判
|
||||||
|
|
||||||
|
### 最终实测结果
|
||||||
|
|
||||||
|
修复后再次重跑,成功 run:
|
||||||
|
|
||||||
|
- `run_id = 202353_spider_md_d28664`
|
||||||
|
- `github_push_status = success`
|
||||||
|
- `github_files_count = 50`
|
||||||
|
|
||||||
|
对应含义:
|
||||||
|
|
||||||
|
1. `part-001.md ~ part-047.md`
|
||||||
|
2. `summary.json`
|
||||||
|
3. `summary.html`
|
||||||
|
4. `github-links.json`
|
||||||
|
|
||||||
|
已经全部完整推送成功。
|
||||||
|
|
||||||
|
### 对 SEO 判断的影响
|
||||||
|
|
||||||
|
这条修复属于:
|
||||||
|
|
||||||
|
- 工程稳定性修复
|
||||||
|
- 运维链路修复
|
||||||
|
- 后台日志可读性修复
|
||||||
|
|
||||||
|
不属于:
|
||||||
|
|
||||||
|
- GPT 模板业务层 SEO 结构调整
|
||||||
|
- 影视页面内容策略变化
|
||||||
|
- 蜘蛛放行策略变化
|
||||||
|
|
||||||
|
所以今天的 SEO 主判断不变:
|
||||||
|
|
||||||
|
1. GPT 模板仍以“继续观察蜘蛛回访”为主
|
||||||
|
2. 今天不需要因为 Spider MD 这次异常去大改模板业务代码
|
||||||
|
3. Spider MD 问题已经单独收口,不应再干扰明天蜘蛛日志判断
|
||||||
|
|
||||||
## 10. 一句话总结
|
## 10. 一句话总结
|
||||||
|
|
||||||
今天的 GPT 模板 SEO 进度总体仍符合“修复后进入正向回访期”的预期,但还没有进入理想放量阶段;当前更该继续看日志和入口链路,而不是急着再改一轮模板业务代码。
|
今天的 GPT 模板 SEO 进度总体仍符合“修复后进入正向回访期”的预期,但还没有进入理想放量阶段;当前更该继续看日志和入口链路,而不是急着再改一轮模板业务代码。
|
||||||
|
|||||||
Reference in New Issue
Block a user