restore gpt seo copy pipeline and stabilize seo tkd output

This commit is contained in:
root
2026-04-16 22:45:00 +08:00
parent 09b2137fa6
commit 2a905bbf0f
18 changed files with 3587 additions and 85 deletions

View File

@@ -26,8 +26,10 @@ class SeoCopyFallbackBuilder
{
$strSiteName = self::siteName($arrFacts);
$strScopes = self::joinItems((array)($arrFacts['content_scopes'] ?? []), 3, '影视内容');
$strLanding = $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,适合先看首屏推荐,再按分类、榜单或搜索继续缩小范围。';
return [
'search_landing' => $strLanding,
'intro_text' => $strSiteName . '首页当前覆盖' . $strScopes . '等常见内容入口,建议先看首屏内容卡片,再继续按分类、榜单或搜索缩小范围。',
'intro_meta' => '首页更适合先发现值得点开的内容,说明性提示只做辅助,不建议压住首屏主体内容。',
'guide_cards' => [
@@ -94,8 +96,10 @@ class SeoCopyFallbackBuilder
private static function buildRankIndex(array $arrFacts): array
{
$strPeriods = self::joinItems((array)($arrFacts['rank_periods'] ?? []), 3, '常见榜单周期');
$strLanding = '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定周期,再继续进入具体榜单列表页。';
return [
'search_landing' => $strLanding,
'intro_text' => '当前榜单首页汇总了' . $strPeriods . '等常见周期入口,适合先确定要看的榜单范围再继续下钻。',
'intro_meta' => '如果你更关心具体周期,可以直接进入对应榜单列表页,再继续看详情和播放路径。',
'guide_cards' => [
@@ -111,8 +115,10 @@ class SeoCopyFallbackBuilder
$strPeriod = self::fallback((string)($arrFacts['rank_period_name'] ?? ''), '当前周期');
$strScope = self::fallback((string)($arrFacts['rank_scope_name'] ?? ''), '当前范围');
$intVisible = (int)($arrFacts['page_stats']['visible'] ?? 0);
$strLanding = '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。';
return [
'search_landing' => $strLanding,
'intro_text' => '当前榜单列表页展示的是' . $strScope . '下的' . $strPeriod . '结果,当前页可继续浏览的条目约为' . max(1, $intVisible) . '条。',
'intro_meta' => '如果你想横向对比结果,可以切到其他周期榜单,或者直接进入详情页继续确认内容。',
'guide_cards' => [

View File

@@ -24,7 +24,93 @@ class SeoCopyStore
return trim($strTemplate);
}
return trim(strtr($strTemplate, $arrReplace));
foreach ($arrTokens as $strKey => $mValue) {
$strKey = trim((string)$strKey);
if ($strKey === '' || trim((string)$mValue) !== '') {
continue;
}
$strTemplate = self::stripEmptyTokenArtifacts($strTemplate, $strKey);
}
return self::normalizeInterpolatedText(strtr($strTemplate, $arrReplace));
}
private static function stripEmptyTokenArtifacts(string $strTemplate, string $strKey): string
{
$strToken = preg_quote('{' . $strKey . '}', '/');
$arrLabelMap = [
'video_alias' => ['别名'],
'year' => ['年份'],
'area_name' => ['地区'],
'lang_name' => ['语言'],
'actor_names' => ['演员'],
'director_names' => ['导演'],
'remarks' => ['备注'],
'play_line' => ['播放线路', '线路'],
'episode_name' => ['集数', '期数', '剧集'],
];
foreach ((array)($arrLabelMap[$strKey] ?? []) as $strLabel) {
$strLabel = preg_quote($strLabel, '/');
$arrLabelPatterns = [
'/' . $strLabel . '\s*' . $strToken . '\s*[、,,]/u',
'/[、,,]\s*' . $strLabel . '\s*' . $strToken . '/u',
'/' . $strLabel . '\s*' . $strToken . '/u',
];
foreach ($arrLabelPatterns as $strPattern) {
$strTemplate = preg_replace($strPattern, '', $strTemplate) ?? $strTemplate;
}
}
$arrPatterns = [
// 删掉前置连词 + 空 token例如 “和{remarks}”
'/\s*(?:和|及|与|并|或)\s*' . $strToken . '/u',
// 删掉空 token + 后置连词,例如 “{remarks}和”
'/' . $strToken . '\s*(?:和|及|与|并|或)\s*/u',
// 删掉与空 token 紧邻的顿号/逗号
'/[、,,]\s*' . $strToken . '/u',
'/' . $strToken . '\s*[、,,]/u',
// 默认兜底:直接删掉空 token
'/' . $strToken . '/u',
];
foreach ($arrPatterns as $strPattern) {
$strTemplate = preg_replace($strPattern, '', $strTemplate) ?? $strTemplate;
}
return $strTemplate;
}
private static function normalizeInterpolatedText(string $strValue): string
{
$strValue = preg_replace('/\{[a-z0-9_]+\}/iu', '', $strValue) ?? $strValue;
$strValue = str_replace(['高清高清播放', '高清高清'], ['高清播放', '高清'], $strValue);
$strValue = preg_replace('/\s{2,}/u', ' ', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*[_|]+\s*/u', ' - ', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*-\s*-\s*/u', ' - ', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把|把)([^,。;!?、\\s]{1,24})和([^,。;!?、\\s]{1,24})(等资料|这类线索)/u', '$1$2、$3$4', $strValue) ?? $strValue;
$strValue = preg_replace('/(年份|地区|语言|演员|导演|备注|别名|线路|集数)(?=[A-Za-z0-9])/u', '$1 ', $strValue) ?? $strValue;
$strValue = preg_replace('/(地区|语言|演员|导演|备注|别名)([\p{Han}A-Za-z])/u', '$1 $2', $strValue) ?? $strValue;
$strValue = preg_replace('/地区\s*([^,。;!?、]+?)和语言\s*([^,。;!?、]+)/u', '地区 $1语言 $2', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把)([^,。;!?、\\s]{1,24})、([^,。;!?、\\s]{1,24})(这类线索)/u', '$1$2和$3$4', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把)([^,。;!?、\\s]{1,24})、([^,。;!?、\\s]{1,24})(等资料)/u', '$1$2、$3$4', $strValue) ?? $strValue;
$strValue = preg_replace('/补看(\d{4}[^、,。;!?]*?)、([\p{Han}A-Za-z]{1,20})、([\p{Han}A-Za-z]{1,20})与([\p{Han}A-Za-z·、,]{1,40})/u', '补看 $1、$2、$3以及$4', $strValue) ?? $strValue;
$strValue = preg_replace('/(别名|年份|地区|语言|演员|导演|备注)\s+与/u', '$1与', $strValue) ?? $strValue;
$strValue = preg_replace('/本页,建议/u', '本页建议', $strValue) ?? $strValue;
$strValue = preg_replace('/本页,适合/u', '本页适合', $strValue) ?? $strValue;
$strValue = preg_replace('/本页,方便/u', '本页方便', $strValue) ?? $strValue;
$strValue = preg_replace('/([、,,])(?:\s*\\1)+/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/([(【\[])\s*[、,,\-]+\s*/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*[、,,\-]+\s*([)】\]])/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/(会把|把|将|用|看)、/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/(\d{4})-\d{2}-\d{2}(?:\s+\d{2}:\d{2}:\d{2})?/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s*([,。;!?、])/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/([(【\[])\s+/u', '$1', $strValue) ?? $strValue;
$strValue = preg_replace('/\s+([)】\]])/u', '$1', $strValue) ?? $strValue;
return trim($strValue);
}
public static function buildPageKey(array|string $mValue, string $strFallback = 'index'): string

View File

@@ -4,6 +4,31 @@ namespace app\common\helper;
class SeoWordsHelper
{
/**
* Resolve a stable forge keyword from video seo words.
*/
public static function resolveForgeWord(array $video = [], int $intForgeId = 1): string
{
$intForgeId = max(1, $intForgeId);
$arrWords = array_values(array_filter(array_map(
static fn($mVal): string => trim((string)$mVal),
(array)($video['v_seo_words'] ?? [])
), static fn(string $strVal): bool => $strVal !== ''));
if (!empty($arrWords)) {
$intIndex = ($intForgeId - 1) % count($arrWords);
return $arrWords[$intIndex];
}
$strName = trim((string)($video['v_name'] ?? ''));
if ($strName !== '') {
return $strName;
}
return trim((string)($video['v_name_en'] ?? ''));
}
/**
* 根据 $intRewrite 处理 SeoWords
*/
@@ -22,32 +47,39 @@ class SeoWordsHelper
// 0原序全部
case 0:
$arrSeoWords = $words;
break;
// 1前 6
// 1前 6
case 1:
$arrSeoWords = array_slice($words, 0, 6);
break;
// 2随机 8
// 2随机 8
case 2:
shuffle($words);
$arrSeoWords = array_slice($words, 0, 8);
break;
// 3插年份
// 3插年份
case 3:
$arrSeoWords = self::appendYear($words, $video);
break;
// 4插“在线观看 / 免费 / 高清”
// 4插“在线观看 / 免费 / 高清”
case 4:
$arrSeoWords = self::appendSuffix($words);
break;
// 5重排 + 去重
// 5重排 + 去重
case 5:
shuffle($words);
$arrSeoWords = array_values(array_unique($words));
break;
// 兜底
// 兜底
default:
$arrSeoWords = array_slice($words, 0, 6);
break;
}
$arrNewsSeoWords = [];

View File

@@ -83,7 +83,7 @@ class UrlBuilder
return $this->replacePattern($pattern, [
'strParentCategory' => $strParentCategory,
'strCategory' => $strCategory,
'intPage' => max(1, (int)$intPage),
'intPage' => $this->normalizePageValue($intPage),
]);
}
@@ -226,4 +226,13 @@ class UrlBuilder
}
return $this->trimSlash($pattern);
}
protected function normalizePageValue(int|string $page): string
{
if (is_string($page) && strpos($page, '{page}') !== false) {
return '{page}';
}
return (string)max(1, (int)$page);
}
}