feat: add github spider md push and async task logging
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
50
code/app/command/DomainSpiderMdPlanTaskSeedCommand.php
Normal file
50
code/app/command/DomainSpiderMdPlanTaskSeedCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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'] ?? '');
|
||||||
|
|||||||
124
code/app/task/logic/DomainImportAsyncTaskLogic.php
Normal file
124
code/app/task/logic/DomainImportAsyncTaskLogic.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
243
docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md
Normal file
243
docs/2026-04-19-GPT模板蜘蛛日志复盘与代码判断.md
Normal 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 进度总体仍符合“修复后进入正向回访期”的预期,但还没有进入理想放量阶段;当前更该继续看日志和入口链路,而不是急着再改一轮模板业务代码。
|
||||||
55
docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md
Normal file
55
docs/2026-04-19-老模板1001-1005变更对GPT影响复核.md
Normal 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 模板没有看到明确直接影响。
|
||||||
@@ -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文案与视频元数据公共规则
|
||||||
|
|
||||||
|
|||||||
@@ -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] ...`
|
||||||
|
- `已推送 GitHub:part-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
|
||||||
|
|||||||
Reference in New Issue
Block a user