feat: add github spider md push and async task logging

This commit is contained in:
www
2026-04-19 19:37:53 +08:00
parent e8688443e5
commit fccd2bfe5b
12 changed files with 970 additions and 11 deletions

View File

@@ -6,6 +6,7 @@ namespace app\admin\controller;
use app\admin\BaseController; use app\admin\BaseController;
use app\common\helper\SeoCopyAiRuleConfigHelper; use app\common\helper\SeoCopyAiRuleConfigHelper;
use app\common\helper\SpiderMdConfigHelper;
use app\model\AdminUserModel; use app\model\AdminUserModel;
use app\model\BaoYangRuZhuModel; use app\model\BaoYangRuZhuModel;
use app\model\CaiJiPeiZhiModel; use app\model\CaiJiPeiZhiModel;
@@ -122,6 +123,7 @@ class SystemConfig extends BaseController
public function getSystemConfigList(Request $Request, ?AdminUserModel $AdminUserModel) public function getSystemConfigList(Request $Request, ?AdminUserModel $AdminUserModel)
{ {
SeoCopyAiRuleConfigHelper::ensureSystemConfigDefaults(); SeoCopyAiRuleConfigHelper::ensureSystemConfigDefaults();
SpiderMdConfigHelper::ensureSystemConfigDefaults();
$arrData = [ $arrData = [
"page" => $Request->get('page', 1), "page" => $Request->get('page', 1),

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace app\command;
use app\model\PlanTaskModel;
use think\console\Command;
use think\console\Input;
use think\console\Output;
class DomainSpiderMdPlanTaskSeedCommand extends Command
{
protected function configure()
{
$this->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;
}
}

View File

@@ -23,7 +23,9 @@ class DomainSpiderMdRunHelper
$runs = []; $runs = [];
SpiderMdConfigHelper::ensureSystemConfigDefaults(); SpiderMdConfigHelper::ensureSystemConfigDefaults();
$gitee = self::resolveGiteeConfig(); $gitee = self::resolveGiteeConfig();
$github = self::resolveGithubConfig();
unset($gitee['token']); unset($gitee['token']);
unset($github['token']);
$matches = glob(self::baseRoot() . '/*/*/summary.json'); $matches = glob(self::baseRoot() . '/*/*/summary.json');
if (is_array($matches)) { if (is_array($matches)) {
rsort($matches); rsort($matches);
@@ -39,6 +41,7 @@ class DomainSpiderMdRunHelper
'generated_at' => date(DATE_ATOM), 'generated_at' => date(DATE_ATOM),
'run_root' => self::baseRoot(), 'run_root' => self::baseRoot(),
'gitee' => $gitee, 'gitee' => $gitee,
'github' => $github,
'runtime_config' => self::resolveRuntimeConfig(), 'runtime_config' => self::resolveRuntimeConfig(),
'config_summary' => SpiderMdConfigHelper::buildConfigSummary(), 'config_summary' => SpiderMdConfigHelper::buildConfigSummary(),
'latest_run' => $runs[0] ?? null, 'latest_run' => $runs[0] ?? null,
@@ -50,19 +53,24 @@ class DomainSpiderMdRunHelper
{ {
$config = self::resolveRuntimeConfig($options); $config = self::resolveRuntimeConfig($options);
$giteeConfig = self::resolveGiteeConfig(); $giteeConfig = self::resolveGiteeConfig();
$githubConfig = self::resolveGithubConfig();
self::ensureDir(self::baseRoot()); self::ensureDir(self::baseRoot());
$forceRegenerate = !empty($options['force_regenerate']); $forceRegenerate = !empty($options['force_regenerate']);
$previous = self::findLatestSuccessfulRunSummary(); $previous = self::findLatestSuccessfulRunSummary();
$today = date('Ymd'); $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'] ?? '')); $previousGiteePushStatus = trim((string)($previous['gitee_push_status'] ?? ''));
$previousGithubPushStatus = trim((string)($previous['github_push_status'] ?? ''));
$allowRetryToday = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true); $allowRetryToday = in_array($previousGiteePushStatus, ['failed', 'partial_failed'], true);
$allowRetryToday = $allowRetryToday || in_array($previousGithubPushStatus, ['failed', 'partial_failed'], true);
if ( if (
!$forceRegenerate !$forceRegenerate
&& !empty($previous) && !empty($previous)
&& substr((string)($previous['generated_at'] ?? ''), 0, 10) === date('Y-m-d') && substr((string)($previous['generated_at'] ?? ''), 0, 10) === date('Y-m-d')
&& !$giteeTargetChanged && !$remoteTargetChanged
&& !$allowRetryToday && !$allowRetryToday
) { ) {
self::log($logger, '今日已生成过蜘蛛池 MD跳过本轮如需全新生成请使用强制重生成'); self::log($logger, '今日已生成过蜘蛛池 MD跳过本轮如需全新生成请使用强制重生成');
@@ -72,11 +80,11 @@ class DomainSpiderMdRunHelper
return $previous; return $previous;
} }
if ($forceRegenerate || empty($previous) || $giteeTargetChanged) { if ($forceRegenerate || empty($previous) || $remoteTargetChanged) {
if ($forceRegenerate) { if ($forceRegenerate) {
self::clearAllRuns(); self::clearAllRuns();
} elseif ($giteeTargetChanged) { } elseif ($remoteTargetChanged) {
self::log($logger, '检测到 Gitee 推送目标已变更,本轮按全新生成处理'); self::log($logger, '检测到远端推送目标已变更,本轮按全新生成处理');
} }
$dateDir = self::baseRoot() . '/' . $today; $dateDir = self::baseRoot() . '/' . $today;
self::ensureDir($dateDir); self::ensureDir($dateDir);
@@ -216,11 +224,17 @@ class DomainSpiderMdRunHelper
], ],
'files' => $writtenFiles, 'files' => $writtenFiles,
'gitee' => array_diff_key($giteeConfig, ['token' => true]), 'gitee' => array_diff_key($giteeConfig, ['token' => true]),
'github' => array_diff_key($githubConfig, ['token' => true]),
'gitee_files' => [], 'gitee_files' => [],
'gitee_pushed' => 0, 'gitee_pushed' => 0,
'gitee_push_attempted' => 0, 'gitee_push_attempted' => 0,
'gitee_push_status' => 'pending', 'gitee_push_status' => 'pending',
'gitee_push_error' => '', 'gitee_push_error' => '',
'github_files' => [],
'github_pushed' => 0,
'github_push_attempted' => 0,
'github_push_status' => 'pending',
'github_push_error' => '',
]; ];
file_put_contents( file_put_contents(
@@ -235,6 +249,12 @@ class DomainSpiderMdRunHelper
$summary['gitee_push_attempted'] = (int)($giteePush['attempted'] ?? 0); $summary['gitee_push_attempted'] = (int)($giteePush['attempted'] ?? 0);
$summary['gitee_push_status'] = (string)($giteePush['status'] ?? 'skipped'); $summary['gitee_push_status'] = (string)($giteePush['status'] ?? 'skipped');
$summary['gitee_push_error'] = (string)($giteePush['error'] ?? ''); $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( file_put_contents(
$runRoot . '/gitee-links.json', $runRoot . '/gitee-links.json',
@@ -246,6 +266,16 @@ class DomainSpiderMdRunHelper
'items' => $summary['gitee_files'], 'items' => $summary['gitee_files'],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL ], 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
@@ -1094,6 +1124,213 @@ class DomainSpiderMdRunHelper
return SpiderMdConfigHelper::resolveGiteeConfig(); 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 protected static function readJsonFile(string $path): array
{ {
if (!is_file($path)) { if (!is_file($path)) {

View File

@@ -36,32 +36,62 @@ class SpiderMdConfigHelper
], ],
'SPIDER_MD_GITEE_ENABLED' => [ 'SPIDER_MD_GITEE_ENABLED' => [
'default' => '0', 'default' => '0',
'description' => '蜘蛛池MD是否启用 Gitee 推送', 'description' => '蜘蛛池MD是否启用 Gitee 推送0=关闭1=开启;开启后会把生成的 markdown 与 links.json 同步到 Gitee',
'secret' => false, 'secret' => false,
], ],
'SPIDER_MD_GITEE_OWNER' => [ 'SPIDER_MD_GITEE_OWNER' => [
'default' => '', 'default' => '',
'description' => '蜘蛛池MD Gitee 仓库 owner', 'description' => '蜘蛛池MD Gitee 仓库 owner(仓库所属用户名或组织名,例如 yourname',
'secret' => false, 'secret' => false,
], ],
'SPIDER_MD_GITEE_REPO' => [ 'SPIDER_MD_GITEE_REPO' => [
'default' => '', 'default' => '',
'description' => '蜘蛛池MD Gitee 仓库 repo', 'description' => '蜘蛛池MD Gitee 仓库 repo(仓库名,不带 owner例如 seo-spider-md',
'secret' => false, 'secret' => false,
], ],
'SPIDER_MD_GITEE_BRANCH' => [ 'SPIDER_MD_GITEE_BRANCH' => [
'default' => 'master', 'default' => 'master',
'description' => '蜘蛛池MD Gitee 分支', 'description' => '蜘蛛池MD Gitee 分支(默认 master推送文件会写入这个分支',
'secret' => false, 'secret' => false,
], ],
'SPIDER_MD_GITEE_ROOT' => [ 'SPIDER_MD_GITEE_ROOT' => [
'default' => 'seo-spider-md', 'default' => 'seo-spider-md',
'description' => '蜘蛛池MD Gitee 远程根目录', 'description' => '蜘蛛池MD Gitee 远程根目录(仓库内子目录,例如 seo-spider-md留空表示直接写仓库根目录',
'secret' => false, 'secret' => false,
], ],
'SPIDER_MD_GITEE_TOKEN' => [ 'SPIDER_MD_GITEE_TOKEN' => [
'default' => '', '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, 'secret' => true,
], ],
'SPIDER_MD_RETENTION_DAYS' => [ '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 protected static function readOperationalValue(string $code): string
{ {
return (string)(self::readValueMeta($code)['value'] ?? ''); return (string)(self::readValueMeta($code)['value'] ?? '');

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace app\task\logic;
use app\common\helper\DomainImportAsyncJobHelper;
use app\common\helper\DomainSpiderMdRunHelper;
class DomainImportAsyncTaskLogic
{
public static function handle(array $data = []): void
{
$jobId = trim((string)($data['job_id'] ?? ''));
if ($jobId === '') {
return;
}
$job = DomainImportAsyncJobHelper::readJob($jobId);
if (empty($job)) {
return;
}
try {
DomainImportAsyncJobHelper::updateJob($jobId, [
'status' => 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;
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace app\task\module; namespace app\task\module;
use app\common\helper\VideoMetadataMissingRefreshHelper; use app\common\helper\VideoMetadataMissingRefreshHelper;
use app\common\helper\DomainSpiderMdRunHelper;
use app\model\PlanTaskModel; use app\model\PlanTaskModel;
use app\task\logic\SiteMapLogic; use app\task\logic\SiteMapLogic;
use app\task\logic\VideoSiteMapLogic; use app\task\logic\VideoSiteMapLogic;
@@ -98,6 +99,9 @@ class PlanTask
case 'REFRESH_VIDEO_METADATA_TASK_POOL': case 'REFRESH_VIDEO_METADATA_TASK_POOL':
self::refreshVideoMetadataTaskPool(); self::refreshVideoMetadataTaskPool();
break; 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 * Undocumented function
* *

View File

@@ -18,5 +18,6 @@ return [
'video:metadata:workbench' => 'app\\command\\VideoMetadataMissingWorkbenchCommand', 'video:metadata:workbench' => 'app\\command\\VideoMetadataMissingWorkbenchCommand',
'video:metadata:task-pool' => 'app\\command\\VideoMetadataMissingTaskPoolCommand', 'video:metadata:task-pool' => 'app\\command\\VideoMetadataMissingTaskPoolCommand',
'plan:seed:video-metadata-missing' => 'app\\command\\VideoMetadataMissingPlanTaskSeedCommand', 'plan:seed:video-metadata-missing' => 'app\\command\\VideoMetadataMissingPlanTaskSeedCommand',
'plan:seed:domain-spider-md' => 'app\\command\\DomainSpiderMdPlanTaskSeedCommand',
], ],
]; ];

View File

@@ -77,3 +77,26 @@ GPT 模板专属工作区:
4. 今天这轮收口如果要继续下钻,请先看: 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-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`

View File

@@ -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 进度总体仍符合“修复后进入正向回访期”的预期,但还没有进入理想放量阶段;当前更该继续看日志和入口链路,而不是急着再改一轮模板业务代码。

View File

@@ -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 模板没有看到明确直接影响。

View File

@@ -35,12 +35,15 @@
如果你关心的是 GPT 模板这条专属 SEO 支线,而不是老模板,请先看: 如果你关心的是 GPT 模板这条专属 SEO 支线,而不是老模板,请先看:
- [2026-04-18-GPT模板SEO公共主线汇总.md](/www/wwwroot/diff-maccms/SEONexus/docs/2026-04-18-GPT模板SEO公共主线汇总.md) - [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 模板线当前边界和主线 1. 想快速知道 GPT 模板线当前边界和主线
2. 想知道 GPT 模板专属目录放在哪里 2. 想知道 GPT 模板专属目录放在哪里
3. 想看 GPT 模板阶段性结论的公共回写入口 3. 想看 GPT 模板阶段性结论的公共回写入口
4. 想直接看今天这轮蜘蛛复盘和老模板影响复核结果
如果你接手的是今天这一轮 `PHP + Vue 后台收口``视频缺字段工作台迁移``导入后 Codex / OpenAI 分流``其它服务器同步`,请继续看: 如果你接手的是今天这一轮 `PHP + Vue 后台收口``视频缺字段工作台迁移``导入后 Codex / OpenAI 分流``其它服务器同步`,请继续看:
@@ -52,6 +55,7 @@
1. 需要把今天这一轮收口同步到其它服务器 1. 需要把今天这一轮收口同步到其它服务器
2. 需要分清哪些是 `SEONexus`、哪些是 `SEONexusAdmin` 2. 需要分清哪些是 `SEONexus`、哪些是 `SEONexusAdmin`
3. 需要确认后台真实页面入口现在在 Vue而不是 PHP 直出页 3. 需要确认后台真实页面入口现在在 Vue而不是 PHP 直出页
4. 需要同步今天补上的 `Spider MD 异步任务消费器``GitHub 推送``主表一键复制MD` 这些后台能力
### 1.3 AI文案与视频元数据公共规则 ### 1.3 AI文案与视频元数据公共规则

View File

@@ -258,6 +258,148 @@ curl -L -s 'https://example.com/detail/pinyin-xxx' | rg -o '<title>.*?</title>|<
如果其它服务器也同步了 [ExceptionHandle.php](/www/wwwroot/diff-maccms/SEONexus/code/app/ExceptionHandle.php),下次再出现: 如果其它服务器也同步了 [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] ...`
- `已推送 GitHubpart-001.md`
- 如果失败,会直接显示失败原因
### 10.3 后台复制体验补充
这次 Vue 后台还补了:
1. 主表右侧固定 `一键复制MD`
2. 复制内容只保留 `.md` 直链
3. 不再复制 `summary.json / summary.html`
如果其它服务器更新后发现:
1. 要横向滚很远才能看到按钮
2. 复制出来的是页面链接不是 md 直链
说明 `SEONexusAdmin` 最新代码还没有同步到位。
1. 先记下精确时间 1. 先记下精确时间
- 例如:`2026-04-18 14:23:15 +08:00` - 例如:`2026-04-18 14:23:15 +08:00`
2. 记下完整 URL 2. 记下完整 URL