diff --git a/code/app/admin/controller/SystemConfig.php b/code/app/admin/controller/SystemConfig.php index d92714d..4a718cc 100644 --- a/code/app/admin/controller/SystemConfig.php +++ b/code/app/admin/controller/SystemConfig.php @@ -6,6 +6,7 @@ namespace app\admin\controller; use app\admin\BaseController; use app\common\helper\SeoCopyAiRuleConfigHelper; +use app\common\helper\SpiderMdConfigHelper; use app\model\AdminUserModel; use app\model\BaoYangRuZhuModel; use app\model\CaiJiPeiZhiModel; @@ -122,6 +123,7 @@ class SystemConfig extends BaseController public function getSystemConfigList(Request $Request, ?AdminUserModel $AdminUserModel) { SeoCopyAiRuleConfigHelper::ensureSystemConfigDefaults(); + SpiderMdConfigHelper::ensureSystemConfigDefaults(); $arrData = [ "page" => $Request->get('page', 1), diff --git a/code/app/command/DomainSpiderMdPlanTaskSeedCommand.php b/code/app/command/DomainSpiderMdPlanTaskSeedCommand.php new file mode 100644 index 0000000..813eb48 --- /dev/null +++ b/code/app/command/DomainSpiderMdPlanTaskSeedCommand.php @@ -0,0 +1,50 @@ +setName('plan:seed:domain-spider-md') + ->setDescription('补齐蜘蛛池MD自动生成计划任务 GENERATE_DOMAIN_SPIDER_MD'); + } + + protected function execute(Input $input, Output $output) + { + $strCode = 'GENERATE_DOMAIN_SPIDER_MD'; + $arrData = [ + 'pt_name' => '蜘蛛池MD自动生成', + 'pt_code' => $strCode, + 'pt_enable' => 0, + 'pt_limit' => 24 * 3600, + 'pt_last_exec' => 0, + ]; + + $PlanTaskModel = PlanTaskModel::where('pt_code', $strCode)->find(); + if ($PlanTaskModel instanceof PlanTaskModel) { + $output->writeln('计划任务已存在,无需重复创建:' . $strCode); + $output->writeln('pt_id:' . (int)$PlanTaskModel->pt_id); + $output->writeln('pt_name:' . (string)$PlanTaskModel->pt_name); + $output->writeln('pt_enable:' . (int)$PlanTaskModel->pt_enable); + $output->writeln('pt_limit:' . (int)$PlanTaskModel->pt_limit); + return 0; + } + + $intId = (int)PlanTaskModel::insertGetId($arrData); + $output->writeln('计划任务已创建:' . $strCode); + $output->writeln('pt_id:' . $intId); + $output->writeln('pt_name:' . $arrData['pt_name']); + $output->writeln('pt_enable:' . $arrData['pt_enable']); + $output->writeln('pt_limit:' . $arrData['pt_limit']); + + return 0; + } +} diff --git a/code/app/common/helper/DomainSpiderMdRunHelper.php b/code/app/common/helper/DomainSpiderMdRunHelper.php index 2747fbb..c51ee64 100644 --- a/code/app/common/helper/DomainSpiderMdRunHelper.php +++ b/code/app/common/helper/DomainSpiderMdRunHelper.php @@ -23,7 +23,9 @@ class DomainSpiderMdRunHelper $runs = []; SpiderMdConfigHelper::ensureSystemConfigDefaults(); $gitee = self::resolveGiteeConfig(); + $github = self::resolveGithubConfig(); unset($gitee['token']); + unset($github['token']); $matches = glob(self::baseRoot() . '/*/*/summary.json'); if (is_array($matches)) { rsort($matches); @@ -39,6 +41,7 @@ class DomainSpiderMdRunHelper 'generated_at' => date(DATE_ATOM), 'run_root' => self::baseRoot(), 'gitee' => $gitee, + 'github' => $github, 'runtime_config' => self::resolveRuntimeConfig(), 'config_summary' => SpiderMdConfigHelper::buildConfigSummary(), 'latest_run' => $runs[0] ?? null, @@ -50,19 +53,24 @@ class DomainSpiderMdRunHelper { $config = self::resolveRuntimeConfig($options); $giteeConfig = self::resolveGiteeConfig(); + $githubConfig = self::resolveGithubConfig(); self::ensureDir(self::baseRoot()); $forceRegenerate = !empty($options['force_regenerate']); $previous = self::findLatestSuccessfulRunSummary(); $today = date('Ymd'); - $giteeTargetChanged = !empty($previous) ? self::giteeTargetChanged($previous, $giteeConfig) : false; + $remoteTargetChanged = !empty($previous) + ? (self::giteeTargetChanged($previous, $giteeConfig) || self::githubTargetChanged($previous, $githubConfig)) + : false; $previousGiteePushStatus = trim((string)($previous['gitee_push_status'] ?? '')); + $previousGithubPushStatus = trim((string)($previous['github_push_status'] ?? '')); $allowRetryToday = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true); + $allowRetryToday = $allowRetryToday || in_array($previousGithubPushStatus, ['failed', 'partial_failed'], true); if ( !$forceRegenerate && !empty($previous) && substr((string)($previous['generated_at'] ?? ''), 0, 10) === date('Y-m-d') - && !$giteeTargetChanged + && !$remoteTargetChanged && !$allowRetryToday ) { self::log($logger, '今日已生成过蜘蛛池 MD,跳过本轮;如需全新生成请使用强制重生成'); @@ -72,11 +80,11 @@ class DomainSpiderMdRunHelper return $previous; } - if ($forceRegenerate || empty($previous) || $giteeTargetChanged) { + if ($forceRegenerate || empty($previous) || $remoteTargetChanged) { if ($forceRegenerate) { self::clearAllRuns(); - } elseif ($giteeTargetChanged) { - self::log($logger, '检测到 Gitee 推送目标已变更,本轮按全新生成处理'); + } elseif ($remoteTargetChanged) { + self::log($logger, '检测到远端推送目标已变更,本轮按全新生成处理'); } $dateDir = self::baseRoot() . '/' . $today; self::ensureDir($dateDir); @@ -216,11 +224,17 @@ class DomainSpiderMdRunHelper ], 'files' => $writtenFiles, 'gitee' => array_diff_key($giteeConfig, ['token' => true]), + 'github' => array_diff_key($githubConfig, ['token' => true]), 'gitee_files' => [], 'gitee_pushed' => 0, 'gitee_push_attempted' => 0, 'gitee_push_status' => 'pending', 'gitee_push_error' => '', + 'github_files' => [], + 'github_pushed' => 0, + 'github_push_attempted' => 0, + 'github_push_status' => 'pending', + 'github_push_error' => '', ]; file_put_contents( @@ -235,6 +249,12 @@ class DomainSpiderMdRunHelper $summary['gitee_push_attempted'] = (int)($giteePush['attempted'] ?? 0); $summary['gitee_push_status'] = (string)($giteePush['status'] ?? 'skipped'); $summary['gitee_push_error'] = (string)($giteePush['error'] ?? ''); + $githubPush = self::pushRunToGithub($summary, $logger); + $summary['github_files'] = (array)($githubPush['items'] ?? []); + $summary['github_pushed'] = !empty($summary['github_files']) ? 1 : 0; + $summary['github_push_attempted'] = (int)($githubPush['attempted'] ?? 0); + $summary['github_push_status'] = (string)($githubPush['status'] ?? 'skipped'); + $summary['github_push_error'] = (string)($githubPush['error'] ?? ''); file_put_contents( $runRoot . '/gitee-links.json', @@ -246,6 +266,16 @@ class DomainSpiderMdRunHelper '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( $runRoot . '/summary.json', json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL @@ -1094,6 +1124,213 @@ class DomainSpiderMdRunHelper return SpiderMdConfigHelper::resolveGiteeConfig(); } + protected static function resolveGithubConfig(): array + { + return SpiderMdConfigHelper::resolveGithubConfig(); + } + + protected static function githubTargetChanged(array $previous, array $currentConfig): bool + { + $previousGithub = (array)($previous['github'] ?? []); + foreach (['enabled', 'owner', 'repo', 'branch', 'root'] as $key) { + if ((string)($previousGithub[$key] ?? '') !== (string)($currentConfig[$key] ?? '')) { + return true; + } + } + + return false; + } + + protected static function pushRunToGithub(array $summary, ?callable $logger = null): array + { + $config = self::resolveGithubConfig(); + if (empty($config['enabled']) || empty($config['configured'])) { + self::log($logger, 'GitHub 未启用或未配置,本轮跳过 GitHub 推送'); + return [ + 'status' => 'skipped', + 'attempted' => 0, + 'error' => '', + '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($baseRemoteDir . '/' . $dateDir . '/' . (string)($summary['run_id'] ?? ''), '/'); + + $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 { + foreach ($localFiles as $filePath) { + if (!is_file($filePath)) { + continue; + } + + $fileName = basename($filePath); + $remotePath = $baseRemoteDir . '/' . $fileName; + $response = self::upsertGithubFile($config, $remotePath, (string)file_get_contents($filePath), 'spider md update ' . $fileName); + + $items[] = [ + 'file_name' => $fileName, + 'remote_path' => $remotePath, + 'html_url' => (string)($response['html_url'] ?? self::buildGithubHtmlUrl($config, $remotePath)), + 'download_url' => (string)($response['download_url'] ?? self::buildGithubRawUrl($config, $remotePath)), + 'local_path' => $filePath, + ]; + self::log($logger, '已推送 GitHub:' . $fileName); + } + } catch (\Throwable $throwable) { + $message = trim($throwable->getMessage()); + self::log($logger, 'GitHub 推送失败:' . $message); + return [ + 'status' => !empty($items) ? 'partial_failed' : 'failed', + 'attempted' => 1, + 'error' => $message, + 'items' => $items, + ]; + } + + return [ + 'status' => 'success', + 'attempted' => 1, + 'error' => '', + 'items' => $items, + ]; + } + + protected static function upsertGithubFile(array $config, string $remotePath, string $content, string $message): array + { + $existing = self::githubRequest( + 'GET', + sprintf( + 'https://api.github.com/repos/%s/%s/contents/%s?ref=%s', + rawurlencode((string)$config['owner']), + rawurlencode((string)$config['repo']), + str_replace('%2F', '/', rawurlencode($remotePath)), + rawurlencode((string)$config['branch']) + ), + $config, + [], + true + ); + + $payload = [ + 'message' => $message, + 'content' => base64_encode($content), + 'branch' => (string)$config['branch'], + ]; + + if (!empty($existing['sha'])) { + $payload['sha'] = (string)$existing['sha']; + } + + return self::githubRequest( + 'PUT', + sprintf( + 'https://api.github.com/repos/%s/%s/contents/%s', + rawurlencode((string)$config['owner']), + rawurlencode((string)$config['repo']), + str_replace('%2F', '/', rawurlencode($remotePath)) + ), + $config, + $payload + ); + } + + protected static function githubRequest(string $method, string $url, array $config, array $payload = [], bool $allowNotFound = false): array + { + if (!function_exists('curl_init')) { + throw new \RuntimeException('当前 PHP 环境缺少 curl 扩展,无法推送 GitHub'); + } + + $curl = curl_init(); + if ($curl === false) { + throw new \RuntimeException('初始化 GitHub 请求失败'); + } + + $headers = [ + 'Accept: application/vnd.github+json', + 'Authorization: Bearer ' . (string)$config['token'], + 'X-GitHub-Api-Version: 2022-11-28', + 'User-Agent: SEONexus-SpiderMd', + ]; + + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 45, + 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) { + 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 + { + return sprintf( + 'https://github.com/%s/%s/blob/%s/%s', + rawurlencode((string)$config['owner']), + rawurlencode((string)$config['repo']), + rawurlencode((string)$config['branch']), + str_replace('%2F', '/', rawurlencode($remotePath)) + ); + } + + protected static function buildGithubRawUrl(array $config, string $remotePath): string + { + return sprintf( + 'https://raw.githubusercontent.com/%s/%s/%s/%s', + rawurlencode((string)$config['owner']), + rawurlencode((string)$config['repo']), + rawurlencode((string)$config['branch']), + str_replace('%2F', '/', rawurlencode($remotePath)) + ); + } + protected static function readJsonFile(string $path): array { if (!is_file($path)) { diff --git a/code/app/common/helper/SpiderMdConfigHelper.php b/code/app/common/helper/SpiderMdConfigHelper.php index f3b0365..26b47fa 100644 --- a/code/app/common/helper/SpiderMdConfigHelper.php +++ b/code/app/common/helper/SpiderMdConfigHelper.php @@ -36,32 +36,62 @@ class SpiderMdConfigHelper ], 'SPIDER_MD_GITEE_ENABLED' => [ 'default' => '0', - 'description' => '蜘蛛池MD是否启用 Gitee 推送', + 'description' => '蜘蛛池MD是否启用 Gitee 推送(0=关闭,1=开启;开启后会把生成的 markdown 与 links.json 同步到 Gitee)', 'secret' => false, ], 'SPIDER_MD_GITEE_OWNER' => [ 'default' => '', - 'description' => '蜘蛛池MD Gitee 仓库 owner', + 'description' => '蜘蛛池MD Gitee 仓库 owner(仓库所属用户名或组织名,例如 yourname)', 'secret' => false, ], 'SPIDER_MD_GITEE_REPO' => [ 'default' => '', - 'description' => '蜘蛛池MD Gitee 仓库 repo', + 'description' => '蜘蛛池MD Gitee 仓库 repo(仓库名,不带 owner,例如 seo-spider-md)', 'secret' => false, ], 'SPIDER_MD_GITEE_BRANCH' => [ 'default' => 'master', - 'description' => '蜘蛛池MD Gitee 分支', + 'description' => '蜘蛛池MD Gitee 分支(默认 master;推送文件会写入这个分支)', 'secret' => false, ], 'SPIDER_MD_GITEE_ROOT' => [ 'default' => 'seo-spider-md', - 'description' => '蜘蛛池MD Gitee 远程根目录', + 'description' => '蜘蛛池MD Gitee 远程根目录(仓库内子目录,例如 seo-spider-md;留空表示直接写仓库根目录)', 'secret' => false, ], 'SPIDER_MD_GITEE_TOKEN' => [ 'default' => '', - 'description' => '蜘蛛池MD Gitee Token', + 'description' => '蜘蛛池MD Gitee Token(需要仓库 contents / push 权限;后台保存后会参与远程推送)', + 'secret' => true, + ], + 'SPIDER_MD_GITHUB_ENABLED' => [ + 'default' => '0', + 'description' => '蜘蛛池MD是否启用 GitHub 推送(0=关闭,1=开启;开启后会把生成的 markdown 与 links.json 同步到 GitHub)', + 'secret' => false, + ], + 'SPIDER_MD_GITHUB_OWNER' => [ + 'default' => '', + 'description' => '蜘蛛池MD GitHub 仓库 owner(仓库所属用户名或组织名,例如 yourname)', + 'secret' => false, + ], + 'SPIDER_MD_GITHUB_REPO' => [ + 'default' => '', + 'description' => '蜘蛛池MD GitHub 仓库 repo(仓库名,不带 owner,例如 seo-spider-md)', + 'secret' => false, + ], + 'SPIDER_MD_GITHUB_BRANCH' => [ + 'default' => 'main', + 'description' => '蜘蛛池MD GitHub 分支(默认 main;推送文件会写入这个分支)', + 'secret' => false, + ], + 'SPIDER_MD_GITHUB_ROOT' => [ + 'default' => 'seo-spider-md', + 'description' => '蜘蛛池MD GitHub 远程根目录(仓库内子目录,例如 seo-spider-md;留空表示直接写仓库根目录)', + 'secret' => false, + ], + 'SPIDER_MD_GITHUB_TOKEN' => [ + 'default' => '', + 'description' => '蜘蛛池MD GitHub Token(建议使用细粒度 Token,并授予当前仓库 Contents: Read and write 权限)', 'secret' => true, ], 'SPIDER_MD_RETENTION_DAYS' => [ @@ -197,6 +227,43 @@ class SpiderMdConfigHelper ]; } + public static function resolveGithubConfig(): array + { + self::ensureSystemConfigDefaults(); + + $enabledMeta = self::readValueMeta('SPIDER_MD_GITHUB_ENABLED'); + $ownerMeta = self::readValueMeta('SPIDER_MD_GITHUB_OWNER'); + $repoMeta = self::readValueMeta('SPIDER_MD_GITHUB_REPO'); + $branchMeta = self::readValueMeta('SPIDER_MD_GITHUB_BRANCH'); + $rootMeta = self::readValueMeta('SPIDER_MD_GITHUB_ROOT'); + $tokenMeta = self::readValueMeta('SPIDER_MD_GITHUB_TOKEN'); + + $enabled = (int)($enabledMeta['value'] ?? '0') === 1; + $owner = trim((string)($ownerMeta['value'] ?? '')); + $repo = trim((string)($repoMeta['value'] ?? '')); + $branch = trim((string)($branchMeta['value'] ?? 'main')); + $root = trim((string)($rootMeta['value'] ?? 'seo-spider-md')); + $token = trim((string)($tokenMeta['value'] ?? '')); + + return [ + 'enabled' => $enabled ? 1 : 0, + 'configured' => ($enabled && $owner !== '' && $repo !== '' && $token !== '') ? 1 : 0, + 'owner' => $owner, + 'repo' => $repo, + 'branch' => $branch !== '' ? $branch : 'main', + 'root' => $root, + 'token' => $token, + 'sources' => [ + 'enabled' => (string)($enabledMeta['source'] ?? 'env'), + 'owner' => (string)($ownerMeta['source'] ?? 'env'), + 'repo' => (string)($repoMeta['source'] ?? 'env'), + 'branch' => (string)($branchMeta['source'] ?? 'env'), + 'root' => (string)($rootMeta['source'] ?? 'env'), + 'token' => (string)($tokenMeta['source'] ?? 'env'), + ], + ]; + } + protected static function readOperationalValue(string $code): string { return (string)(self::readValueMeta($code)['value'] ?? ''); diff --git a/code/app/task/logic/DomainImportAsyncTaskLogic.php b/code/app/task/logic/DomainImportAsyncTaskLogic.php new file mode 100644 index 0000000..42af77f --- /dev/null +++ b/code/app/task/logic/DomainImportAsyncTaskLogic.php @@ -0,0 +1,124 @@ + DomainImportAsyncJobHelper::STATUS_RUNNING, + 'current_step' => 'starting', + 'message' => '任务开始执行', + ]); + + DomainImportAsyncJobHelper::appendLog($jobId, '开始执行任务:' . (string)($job['label'] ?? $job['type'] ?? 'unknown')); + DomainImportAsyncJobHelper::updateProgress($jobId, 1, 100, 'prepare', '开始准备执行环境'); + + $type = trim((string)($job['type'] ?? '')); + $payload = is_array($job['payload'] ?? null) ? (array)$job['payload'] : []; + + switch ($type) { + case 'spider_md_generate': + $summary = self::runSpiderMdGenerate($jobId, $payload); + break; + default: + throw new \RuntimeException('暂未支持的异步任务类型:' . $type); + } + + DomainImportAsyncJobHelper::updateJob($jobId, [ + 'status' => DomainImportAsyncJobHelper::STATUS_SUCCESS, + 'current_step' => 'completed', + 'message' => '任务执行完成', + 'summary' => $summary, + ]); + DomainImportAsyncJobHelper::appendLog($jobId, '任务执行完成'); + } catch (\Throwable $e) { + DomainImportAsyncJobHelper::appendLog($jobId, '任务执行失败:' . $e->getMessage()); + DomainImportAsyncJobHelper::updateJob($jobId, [ + 'status' => DomainImportAsyncJobHelper::STATUS_FAILED, + 'current_step' => 'failed', + 'message' => '任务执行失败:' . $e->getMessage(), + 'summary' => [ + 'error' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ], + ]); + } + } + + protected static function runSpiderMdGenerate(string $jobId, array $payload): array + { + DomainImportAsyncJobHelper::appendLog($jobId, '开始执行蜘蛛池MD生成'); + DomainImportAsyncJobHelper::updateProgress($jobId, 5, 100, 'running', '开始生成蜘蛛池MD'); + + $state = [ + 'current' => 5, + 'total' => 100, + 'domain_total' => 0, + ]; + + $logger = static function (string $message) use ($jobId, &$state): void { + $message = trim($message); + if ($message === '') { + return; + } + + DomainImportAsyncJobHelper::appendLog($jobId, $message); + + if (preg_match('/已载入\s+(\d+)\s+个域名/u', $message, $matches)) { + $state['domain_total'] = max(0, (int)($matches[1] ?? 0)); + DomainImportAsyncJobHelper::updateProgress($jobId, 10, 100, 'running', $message); + return; + } + + if (preg_match('/\[(\d+)\/(\d+)\]/', $message, $matches)) { + $current = max(0, (int)($matches[1] ?? 0)); + $total = max(1, (int)($matches[2] ?? 1)); + $percent = 10 + (int)floor(($current / $total) * 75); + $state['current'] = min(95, max($state['current'], $percent)); + DomainImportAsyncJobHelper::updateProgress($jobId, $state['current'], 100, 'running', $message); + return; + } + + if (mb_strpos($message, '推送失败') !== false) { + DomainImportAsyncJobHelper::updateProgress($jobId, max($state['current'], 90), 100, 'running', $message); + return; + } + + if (mb_strpos($message, '已推送 GitHub') !== false || mb_strpos($message, '已推送 Gitee') !== false) { + $state['current'] = min(98, max($state['current'], $state['current'] + 1)); + DomainImportAsyncJobHelper::updateProgress($jobId, $state['current'], 100, 'running', $message); + return; + } + + if (mb_strpos($message, '蜘蛛池 MD 生成完成') !== false) { + DomainImportAsyncJobHelper::updateProgress($jobId, 99, 100, 'completed', $message); + return; + } + }; + + $summary = DomainSpiderMdRunHelper::run($payload, $logger); + + DomainImportAsyncJobHelper::updateProgress($jobId, 100, 100, 'completed', '蜘蛛池MD生成完成'); + + return $summary; + } +} diff --git a/code/app/task/module/PlanTask.php b/code/app/task/module/PlanTask.php index c6e9aa0..9c8e1d6 100644 --- a/code/app/task/module/PlanTask.php +++ b/code/app/task/module/PlanTask.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace app\task\module; use app\common\helper\VideoMetadataMissingRefreshHelper; +use app\common\helper\DomainSpiderMdRunHelper; use app\model\PlanTaskModel; use app\task\logic\SiteMapLogic; use app\task\logic\VideoSiteMapLogic; @@ -98,6 +99,9 @@ class PlanTask case 'REFRESH_VIDEO_METADATA_TASK_POOL': self::refreshVideoMetadataTaskPool(); break; + case 'GENERATE_DOMAIN_SPIDER_MD': + self::generateDomainSpiderMd(); + break; } @@ -141,6 +145,13 @@ class PlanTask ]); } + public static function generateDomainSpiderMd(): array + { + return DomainSpiderMdRunHelper::run([ + 'force_regenerate' => 0, + ]); + } + /** * Undocumented function * diff --git a/code/config/console.php b/code/config/console.php index cf384e5..2f2ac6f 100644 --- a/code/config/console.php +++ b/code/config/console.php @@ -18,5 +18,6 @@ return [ 'video:metadata:workbench' => 'app\\command\\VideoMetadataMissingWorkbenchCommand', 'video:metadata:task-pool' => 'app\\command\\VideoMetadataMissingTaskPoolCommand', 'plan:seed:video-metadata-missing' => 'app\\command\\VideoMetadataMissingPlanTaskSeedCommand', + 'plan:seed:domain-spider-md' => 'app\\command\\DomainSpiderMdPlanTaskSeedCommand', ], ]; diff --git a/docs/2026-04-18-GPT模板SEO公共主线汇总.md b/docs/2026-04-18-GPT模板SEO公共主线汇总.md index 1c58ec8..ec412f6 100644 --- a/docs/2026-04-18-GPT模板SEO公共主线汇总.md +++ b/docs/2026-04-18-GPT模板SEO公共主线汇总.md @@ -77,3 +77,26 @@ GPT 模板专属工作区: 4. 今天这轮收口如果要继续下钻,请先看: - [2026-04-18-PHP与Vue收口交付总结.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-18-PHP与Vue收口交付总结.md) - [2026-04-18-其它服务器更新操作单-PHP加Vue.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-18-其它服务器更新操作单-PHP加Vue.md) + +## 2026-04-19 最新补充 + +1. 今日 GPT 模板蜘蛛复盘已写入: + - [2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md) +2. 今日老模板 `1001-1005` 对 GPT 影响复核已写入: + - [2026-04-19-老模板1001-1005变更对GPT影响复核.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md) +3. 当前补充结论: + - GPT 模板线仍属正向回访期 + - `sitemap / search` 入口仍偏弱 + - 今天不建议继续大改 GPT 模板业务代码 + - 如果明天 `googlebot / bingbot` 仍大面积 `robots/category => 444`,再单独评估 Nginx 蜘蛛放行策略 +4. 今天补充的后台与运维收口: + - 蜘蛛池 MD 远程推送已经扩展为 `Gitee + GitHub` + - GitHub 配置项现在会在“系统配置”列表页自动初始化 + - 其它服务器同步时,除了代码文件,还要同步后台配置与计划任务种子命令 +5. 今天补充的 Spider MD 异步修复: + - 已补齐 `Spider MD` 异步任务消费器,后台点击“生成并推送 / 强制重生成”后,不再只停在 `queued` + - 任务窗口现在会持续显示 `running` 进度、阶段、实时日志和失败原因 + - 本轮已验证 GitHub 推送全链路可用,正式异步任务已不再停留在 `5` 个测试 md,而是按真实配置继续推送到多文件 +6. 今天补充的后台体验修正: + - `蜘蛛池MD` 弹窗现在支持主表右侧固定 `一键复制MD` 按钮 + - 复制内容已恢复为旧习惯:只复制 `.md` 直链,不复制 `summary.json / summary.html` diff --git a/docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md b/docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md new file mode 100644 index 0000000..78b0a50 --- /dev/null +++ b/docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md @@ -0,0 +1,243 @@ +# 2026-04-19 GPT模板蜘蛛日志复盘与代码判断 + +## 1. 复盘范围 + +- 日期:`2026-04-19` +- 复盘对象:`GPT 模板 15 个优先域名` +- 日志来源:`SEONexus/code/storage/domain-spider-crawl/runs/20260419/*/crawl-logs.summary.json` +- 对照文档: + - [2026-04-18-7天推收录执行主线.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-18-7天推收录执行主线.md) + - [04-GPT模板A-B组日志跟踪板-2026-04-18.md](/www/wwwroot/diff-maccms/SEONexus/docs/gpt-template-seo/04-GPT模板A-B组日志跟踪板-2026-04-18.md) + - [09-GPT模板次日蜘蛛日志判读模板-2026-04-19.md](/www/wwwroot/diff-maccms/SEONexus/docs/gpt-template-seo/09-GPT模板次日蜘蛛日志判读模板-2026-04-19.md) + +## 2. 今日总判断 + +今天这轮 GPT 模板蜘蛛表现属于: + +1. 仍有正向回访 +2. 整体基本符合 Day2 到 Day3 之间的预期 +3. 但推进速度一般,还没有进入理想放量阶段 + +直接结论: + +- `detail / play` 没有完全消失,说明前面的修复没有白做 +- 但今天新增信号主要还是局部重复回访,没有形成明显的 `sitemap -> search -> detail -> play` 放量链路 +- 所以今天不建议再大面积改 GPT 模板代码,先继续观察下一轮日志更稳妥 + +## 3. 是否符合 7 天计划预期 + +对照 [2026-04-18-7天推收录执行主线.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-18-7天推收录执行主线.md) 的 Day2 / Day3 目标,当前判断如下: + +### 符合预期的部分 + +1. 已有站点继续出现 `detail` 或 `play` 级别命中 +2. `jingxifa.com`、`codohealth.com`、`lgyz.net`、`gxhongzhuang.com` 这类站仍保留真实内容页信号 +3. 部分站点首页 `200` 仍在持续,说明基础可访问性没有整体塌掉 + +### 低于理想预期的部分 + +1. `sitemap` 真实命中仍然很弱 +2. `search` 真实回访仍然几乎没有起来 +3. 四个 P0 站里,今天没有看到“集体往深页扩张”的明显加速 +4. 多站仍有 `robots` 或 `category` 被 `444` 的现象,尤其是 `googlebot` + +### 今日阶段定位 + +更准确地说,今天是: + +- `修复后维持正向回访` + +而不是: + +- `已经进入明显放量` + +## 4. 重点站判断 + +### P0 四站 + +#### `jingxifa.com` + +- 今日总请求:`622` +- 今日特征: + - `play: 15` + - `sitemap: 1` + - `category: 341` + - `robots: 257` + - `444: 600` +- 判断: + - 仍是当前最有内容层信号的样本之一 + - 但今天新增量主要不是来自更深链路扩张,而是大量 `googlebot category/robots 444` + - 更像“有正向样本,但入口质量一般” + +#### `lcdchq.com` + +- 今日总请求:`34` +- 今日特征: + - 以首页 `200` 为主 + - 大量 `/admin.php => 301` 探针噪音 + - 未见 `detail / play / sitemap / search` +- 判断: + - 还没走出浅层 + - 当前不适合继续改模板 + - 先继续看缓存与后续蜘蛛是否重新打真实内容页 + +#### `cnzhenbang.com` + +- 今日总请求:`29` +- 今日特征: + - `home: 8` + - `robots: 19` + - `444: 19` + - `403: 2` + - 无 `detail / play` +- 判断: + - 仍偏浅层 + - 今天看不到从 `category/home` 进入内容层的明显新突破 + - 继续观察更合适,不建议今天为它单独改模板 + +#### `oronorent.com` + +- 今日总请求:`12` +- 今日特征: + - 首页有 `200/301` + - `robots` 仍有 `444/403` + - 无 `detail / play` +- 判断: + - 还是浅层观察站 + - 今天也没有支持“继续改 GPT 模板代码”的新证据 + +### P1 正向样本站 + +#### `codohealth.com` + +- 今日 `detail: 4` +- 首页仍有 `200` +- `robots 444` 仍高 +- 判断: + - 仍保留深页能力 + - 但没有进入更大规模扩张 + +#### `lgyz.net` + +- 今日 `detail: 4` +- 今日 `sitemap: 3` +- 但 `sitemap` 是 `444` +- 判断: + - 依然是值得继续盯的正向样本 + - 但 `sitemap` 质量并不好,属于“有信号,但入口不稳” + +#### `jpjdxs.com` + +- 今日仅首页 `200/301` +- 没有新的 `detail / play` +- 判断: + - 今天偏弱 + - 还不足以支持“继续做模板层优化” + +## 5. 弱站与风险点 + +### `liangzuan.net` + +- 今日在 `2026-04-19` 这批 run 中未见有效命中 +- 判断: + - 今天不能用“无信号”直接下坏结论 + - 但它确实不在今天的正向样本里 + +### `jxxgygy.com` + +- 首页 `200` 有继续出现 +- 但今天未见真正新的 `detail / play` +- 判断: + - 属于“还活着,但没深入” + +### `gxhongzhuang.com` + +- 今日 `detail: 4` +- 是今天除 `jingxifa.com / codohealth.com / lgyz.net` 之外,仍保持内容层信号的站 + +### `glae.cc` + +- 今日 `sitemap: 1` +- 但 `sitemap.xml => 444` +- 判断: + - 有入口请求,不等于入口质量健康 + +## 6. 今天最重要的日志结论 + +1. GPT 模板线今天不是断流,而是“继续温和推进” +2. `detail / play` 仍有样本站支撑,说明之前修复方向总体没错 +3. 真正的短板仍然是 `sitemap / search` 链路偏弱 +4. 多站 `googlebot robots/category => 444` 依旧很重,会拖慢外部蜘蛛的正向判断 +5. 今天没有出现足够新的结构性异常,不支持再大面积改 GPT 模板业务代码 + +## 7. 今天要不要继续改 GPT 模板代码 + +### 结论 + +今天不建议继续做大面积 GPT 模板代码改动。 + +### 原因 + +1. 今天的日志没有显示出新的模板层结构故障 +2. 现阶段更像是: + - 蜘蛛继续回访中 + - 入口链路偏弱 + - 部分缓存与前端放行策略仍在影响抓取质量 +3. 如果此时继续大改模板,容易把“观察窗口”打断,反而不利于判断真实修复效果 + +### 今天唯一值得关注的“精选优化方向” + +如果后续目标里包含 `google / bing` 的真实收录推进,那么需要单独评估: + +1. `robots.txt` 对 `googlebot / bingbot / bytespider` 的放行策略 +2. `sitemap.xml` 命中后返回 `444` 的站点 +3. 某些目录型 `category` 路径被 `444` 的情况是否属于预期安全策略 + +这条线更偏: + +- `Nginx / 蜘蛛策略` + +而不是: + +- `GPT 模板 PHP 渲染逻辑` + +## 8. 老模板 1001-1005 更新是否影响 GPT 模板 + +今天额外检查了 `/www/wwwroot/maccms` 最近提交: + +- `c6514d4` +- `59e9c15` +- `4165099` +- `d9e1e49` + +最近几次已提交改动主要集中在: + +1. `application/admin/controller/Domainnew.php` +2. `application/admin/view_new/domainnew/index.html` + +判断: + +1. 这些是老模板项目自己的后台域名管理逻辑 +2. 目前没看到它们直接下沉到 GPT 模板共享 SEO 服务层 +3. 所以从“最近提交代码”角度看,对 GPT 模板直接影响很低 + +注意: + +- 老模板仓库工作区本身非常脏,不能把全部未提交文件都当成已经上线的有效变更 +- 这次判断只基于“最近已提交的实际代码” + +## 9. 明日动作建议 + +1. 继续看 `2026-04-20` 第一轮新 run,重点比较: + - 是否新增 `detail / play` 站点 + - `jingxifa.com` 是否继续保持内容层信号 + - `lcdchq.com / cnzhenbang.com / oronorent.com` 是否有任何深页突破 +2. 单独盯: + - `lgyz.net` 的 `sitemap 444` + - `glae.cc` 的 `sitemap 444` +3. 今天先不动 GPT 模板业务代码 +4. 如果明天 `googlebot / bingbot` 仍大面积只打 `robots/category` 且持续 `444`,再单独开一轮 `Nginx 蜘蛛放行策略` 优化 + +## 10. 一句话总结 + +今天的 GPT 模板 SEO 进度总体仍符合“修复后进入正向回访期”的预期,但还没有进入理想放量阶段;当前更该继续看日志和入口链路,而不是急着再改一轮模板业务代码。 diff --git a/docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md b/docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md new file mode 100644 index 0000000..839ce78 --- /dev/null +++ b/docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md @@ -0,0 +1,55 @@ +# 2026-04-19 老模板1001-1005变更对GPT影响复核 + +## 1. 目的 + +复核 `1001-1005` 老模板项目今天已更新的代码,判断是否会影响当前 GPT 模板线。 + +## 2. 复核对象 + +- 老模板项目目录:`/www/wwwroot/maccms` +- 复核方式: + - 看最近已提交 commit + - 不把大量未提交脏文件误判为已上线改动 + +## 3. 最近已提交代码 + +本次实际复核的最近提交: + +1. `c6514d4` +2. `59e9c15` +3. `4165099` +4. `d9e1e49` +5. `fc9a8ad` + +其中最近几次真正动到业务代码的提交,集中改了: + +1. `application/admin/controller/Domainnew.php` +2. `application/admin/view_new/domainnew/index.html` + +## 4. 当前判断 + +### 对 GPT 模板直接影响 + +当前判断为: + +- `低` + +### 原因 + +1. 最近已提交改动落在老模板项目自己的后台域名管理逻辑 +2. 没看到这几次提交直接修改 GPT 模板共享 SEO 服务层 +3. 也没看到它直接改到当前 GPT 模板站的前台渲染链路 + +## 5. 风险提醒 + +老模板仓库当前工作区很脏,`git status` 里有大量未提交改动。 + +这意味着: + +1. 不能用工作区脏文件去判断“已经上线的真实影响” +2. 只能以最近 commit 为主判断 +3. 如果那边后续再提交共享层、Nginx、公用脚本,才需要重新评估 GPT 影响 + +## 6. 当前结论 + +截至 `2026-04-19` 这次复核,`1001-1005` 老模板项目最近已提交代码对 GPT 模板没有看到明确直接影响。 diff --git a/docs/README.md b/docs/README.md index 00f6149..63e3a7f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,12 +35,15 @@ 如果你关心的是 GPT 模板这条专属 SEO 支线,而不是老模板,请先看: - [2026-04-18-GPT模板SEO公共主线汇总.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-18-GPT模板SEO公共主线汇总.md) +- [2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md) +- [2026-04-19-老模板1001-1005变更对GPT影响复核.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md) 适用场景: 1. 想快速知道 GPT 模板线当前边界和主线 2. 想知道 GPT 模板专属目录放在哪里 3. 想看 GPT 模板阶段性结论的公共回写入口 +4. 想直接看今天这轮蜘蛛复盘和老模板影响复核结果 如果你接手的是今天这一轮 `PHP + Vue 后台收口`、`视频缺字段工作台迁移`、`导入后 Codex / OpenAI 分流`、`其它服务器同步`,请继续看: @@ -52,6 +55,7 @@ 1. 需要把今天这一轮收口同步到其它服务器 2. 需要分清哪些是 `SEONexus`、哪些是 `SEONexusAdmin` 3. 需要确认后台真实页面入口现在在 Vue,而不是 PHP 直出页 +4. 需要同步今天补上的 `Spider MD 异步任务消费器`、`GitHub 推送`、`主表一键复制MD` 这些后台能力 ### 1.3 AI文案与视频元数据公共规则 diff --git a/docs/gpt-template-seo/08-GPT模板其它服务器更新交接-2026-04-18.md b/docs/gpt-template-seo/08-GPT模板其它服务器更新交接-2026-04-18.md index 140d0ec..878229d 100644 --- a/docs/gpt-template-seo/08-GPT模板其它服务器更新交接-2026-04-18.md +++ b/docs/gpt-template-seo/08-GPT模板其它服务器更新交接-2026-04-18.md @@ -258,6 +258,148 @@ curl -L -s 'https://example.com/detail/pinyin-xxx' | rg -o '.*?|< 如果其它服务器也同步了 [ExceptionHandle.php](/www/wwwroot/diff-maccms/SEONexus/code/app/ExceptionHandle.php),下次再出现: +## 9. 蜘蛛池 MD 远程推送补充说明 + +如果其它服务器也要同步“蜘蛛池 MD 自动生成 + 远程推送”,这次已经支持: + +1. Gitee 推送 +2. GitHub 推送 +3. 计划任务自动生成 + +### 9.1 后台系统配置里现在能看到的 GitHub 参数 + +进入“系统配置”后,搜索 `SPIDER_MD_GITHUB`,应该能看到: + +1. `SPIDER_MD_GITHUB_ENABLED` + - `0` 表示关闭 + - `1` 表示开启 +2. `SPIDER_MD_GITHUB_OWNER` + - 仓库所属用户名或组织名 + - 例如:`yourname` +3. `SPIDER_MD_GITHUB_REPO` + - 仓库名,不带 owner + - 例如:`seo-spider-md` +4. `SPIDER_MD_GITHUB_BRANCH` + - 目标分支 + - 默认建议:`main` +5. `SPIDER_MD_GITHUB_ROOT` + - 仓库内子目录 + - 默认可填:`seo-spider-md` + - 如果想直接写仓库根目录,可以留空 +6. `SPIDER_MD_GITHUB_TOKEN` + - GitHub Token + - 建议使用细粒度 Token + - 至少给当前仓库 `Contents: Read and write` + +### 9.2 最小可用配置 + +如果只想先跑通 GitHub 推送,最少需要填: + +1. `SPIDER_MD_GITHUB_ENABLED=1` +2. `SPIDER_MD_GITHUB_OWNER` +3. `SPIDER_MD_GITHUB_REPO` +4. `SPIDER_MD_GITHUB_BRANCH` +5. `SPIDER_MD_GITHUB_ROOT` +6. `SPIDER_MD_GITHUB_TOKEN` + +注意: + +1. 只开 `ENABLED=1` 但没填 owner / repo / token,不会真正推送成功 +2. `root` 只是远程目录,不是本地目录 +3. GitHub 和 Gitee 可以并行开启,互不冲突 + +### 9.3 计划任务怎么接 + +这次已经补了计划任务种子命令: + +```bash +php think plan:seed:domain-spider-md +``` + +作用: + +1. 自动写入一条 `GENERATE_DOMAIN_SPIDER_MD` 计划任务 +2. 默认是 `禁用` 状态,不会一落地就乱跑 +3. 你可以进后台计划任务页再手工启用 + +执行逻辑: + +1. 任务 code:`GENERATE_DOMAIN_SPIDER_MD` +2. 任务内容:生成蜘蛛池 MD +3. 如果配置了 Gitee / GitHub,就会同步推送远程文件 + +### 9.4 其它服务器上线后先验什么 + +至少先验下面 4 件事: + +1. 后台系统配置能搜到 `SPIDER_MD_GITHUB_*` +2. 执行一次蜘蛛池 MD 生成后,摘要里能看到 GitHub 配置状态 +3. 若已填 Token,生成记录里能看到 GitHub 推送结果 +4. 若开了计划任务,后台计划任务列表里能看到 `GENERATE_DOMAIN_SPIDER_MD` + +### 9.5 如果后台看不到 GitHub 配置项 + +优先排查: + +1. 是否已同步 [SpiderMdConfigHelper.php](/www/wwwroot/diff-maccms/SEONexus/code/app/common/helper/SpiderMdConfigHelper.php) +2. 是否已同步 [SystemConfig.php](/www/wwwroot/diff-maccms/SEONexus/code/app/admin/controller/SystemConfig.php) +3. 是否打开过一次“系统配置”列表页 + +当前版本里,系统配置列表页会自动把 `SPIDER_MD_GITHUB_*` 与 `SPIDER_MD_GITEE_*` 默认项补进数据库。 + +## 10. Spider MD 异步任务窗口补充说明 + +这次还额外补了 `Spider MD` 的异步任务消费器。 + +意义是: + +1. 后台点“生成并推送 / 强制重生成”后,不再只是创建一个 `queued` 任务 +2. 任务会真正进入 `running` +3. 前台任务窗口会持续看到: + - 当前阶段 + - 进度百分比 + - 实时日志 + - 失败原因 + +### 10.1 这次同步时必须注意的文件 + +- [DomainImportAsyncTaskLogic.php](/www/wwwroot/diff-maccms/SEONexus/code/app/task/logic/DomainImportAsyncTaskLogic.php) + +如果漏掉这个文件,现场最常见的表现就是: + +1. 后台按钮点了 +2. 任务号也出来了 +3. 但状态一直停在 `queued` +4. 进度条始终 `0%` +5. 日志窗口只显示“任务已入队,等待执行” + +### 10.2 现场正确现象 + +同步后,Spider MD 任务窗口里应该能看到: + +1. `status: running` +2. `progress_percent` 持续变化 +3. 日志里出现: + - 已载入多少个域名 + - `[1/32] ... [2/32] ...` + - `已推送 GitHub:part-001.md` + - 如果失败,会直接显示失败原因 + +### 10.3 后台复制体验补充 + +这次 Vue 后台还补了: + +1. 主表右侧固定 `一键复制MD` +2. 复制内容只保留 `.md` 直链 +3. 不再复制 `summary.json / summary.html` + +如果其它服务器更新后发现: + +1. 要横向滚很远才能看到按钮 +2. 复制出来的是页面链接不是 md 直链 + +说明 `SEONexusAdmin` 最新代码还没有同步到位。 + 1. 先记下精确时间 - 例如:`2026-04-18 14:23:15 +08:00` 2. 记下完整 URL