refactor: flatten src/AKSRT → src (drop theme-identifier dir)
Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
This commit is contained in:
1 parent
56f1a8175f
commit
8c5b1555c9
285 files changed
+26
-3600
No files matched your search
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Ai;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AISEO {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO AI 功能是否启用。
|
||||
*/
|
||||
public function is_enabled(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return false;
|
||||
}
|
||||
$seo = \AKSRT\Settings\Settings::get_seo_settings();
|
||||
return !empty($seo['seo_enabled']) && !empty($seo['seo_ai_enabled']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已保存实体生成 SEO 三字段(Zibll 形状)。
|
||||
*
|
||||
* @return array{success:bool,data?:array,message?:string}
|
||||
*/
|
||||
public function generate_fields(string $type, int $id): array {
|
||||
if (!$this->is_enabled()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('AI SEO 功能未启用', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT\\Ai\\AI') || !class_exists('AKSRT\\Ai\\AIPrompt')) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('AI 模块未加载', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$ai = AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('AI 服务未启用或未配置', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$type = $type === 'term' ? 'term' : 'post';
|
||||
if ($id <= 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('请先保存后再生成 SEO', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$context = $type === 'term'
|
||||
? $this->build_term_context($id)
|
||||
: $this->build_post_context($id);
|
||||
|
||||
if (is_wp_error($context)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $context->get_error_message(),
|
||||
];
|
||||
}
|
||||
|
||||
$prompt = AIPrompt::get_instance();
|
||||
|
||||
$title = $this->run_field(
|
||||
$ai,
|
||||
$context,
|
||||
$prompt->get_seo_title_system_instruction(),
|
||||
'title'
|
||||
);
|
||||
$keywords = $this->run_field(
|
||||
$ai,
|
||||
$context,
|
||||
$prompt->get_seo_keywords_system_instruction(),
|
||||
'keywords'
|
||||
);
|
||||
$description = $this->run_field(
|
||||
$ai,
|
||||
$context,
|
||||
$prompt->get_seo_description_system_instruction(),
|
||||
'description'
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'title' => $title,
|
||||
'keywords' => $keywords,
|
||||
'description' => $description,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿场景:用传入 title/content 生成(兼容旧 /ai/seo-suggest)。
|
||||
*/
|
||||
public function suggest(string $title, string $content = ''): array {
|
||||
if (!$this->is_enabled()) {
|
||||
return ['success' => false, 'message' => __('AI SEO 功能未启用', 'aksrt')];
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT\\Ai\\AI') || !class_exists('AKSRT\\Ai\\AIPrompt')) {
|
||||
return ['success' => false, 'message' => __('AI 模块未加载', 'aksrt')];
|
||||
}
|
||||
|
||||
$ai = AI::get_instance();
|
||||
if (!$ai->is_available()) {
|
||||
return ['success' => false, 'message' => __('AI 服务未启用', 'aksrt')];
|
||||
}
|
||||
|
||||
$title = trim($title);
|
||||
$content = trim(wp_strip_all_tags($content));
|
||||
if ($title === '' && $content === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => __('无可用内容,请先填写标题或正文', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($title !== '') {
|
||||
$parts[] = '标题: ' . $title;
|
||||
}
|
||||
if ($content !== '') {
|
||||
$content = wp_trim_words($content, 1500, '');
|
||||
$parts[] = "正文:\n" . $content;
|
||||
}
|
||||
$context = implode("\n\n", $parts);
|
||||
$prompt = AIPrompt::get_instance();
|
||||
|
||||
$title_out = $this->run_field($ai, $context, $prompt->get_seo_title_system_instruction(), 'title');
|
||||
$kw_out = $this->run_field($ai, $context, $prompt->get_seo_keywords_system_instruction(), 'keywords');
|
||||
$desc_out = $this->run_field($ai, $context, $prompt->get_seo_description_system_instruction(), 'description');
|
||||
|
||||
$keywords = [];
|
||||
if (!empty($kw_out['content'])) {
|
||||
$keywords = array_values(array_filter(array_map('trim', explode(',', $kw_out['content']))));
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'suggested_title' => (string) ($title_out['content'] ?? ''),
|
||||
'suggested_description' => (string) ($desc_out['content'] ?? ''),
|
||||
'suggested_keywords' => $keywords,
|
||||
'fields' => [
|
||||
'title' => $title_out,
|
||||
'keywords' => $kw_out,
|
||||
'description' => $desc_out,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
public function build_post_context(int $post_id, int $word_limit = 1500) {
|
||||
$post = get_post($post_id);
|
||||
if (!$post) {
|
||||
return new \WP_Error('no_post', __('内容不存在', 'aksrt'));
|
||||
}
|
||||
|
||||
$title = trim((string) $post->post_title);
|
||||
$excerpt = trim((string) $post->post_excerpt);
|
||||
$content = wp_strip_all_tags((string) $post->post_content);
|
||||
$content = trim(preg_replace('/\s+/u', ' ', $content) ?? $content);
|
||||
$content = wp_trim_words($content, max(50, $word_limit), '');
|
||||
|
||||
if ($title === '' && $content === '' && $excerpt === '') {
|
||||
return new \WP_Error('empty_content', __('无可用内容,请先填写标题、摘要或正文并保存', 'aksrt'));
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($title !== '') {
|
||||
$parts[] = '标题: ' . $title;
|
||||
}
|
||||
if ($excerpt !== '') {
|
||||
$parts[] = '摘要: ' . $excerpt;
|
||||
}
|
||||
if ($content !== '') {
|
||||
$parts[] = "正文:\n" . $content;
|
||||
}
|
||||
|
||||
return implode("\n\n", $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
public function build_term_context(int $term_id) {
|
||||
$term = get_term($term_id);
|
||||
if (!$term || is_wp_error($term)) {
|
||||
return new \WP_Error('no_term', __('分类/标签不存在', 'aksrt'));
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if ($term->name !== '') {
|
||||
$parts[] = '标题: ' . $term->name;
|
||||
}
|
||||
if ($term->description !== '') {
|
||||
$parts[] = '描述: ' . wp_strip_all_tags((string) $term->description);
|
||||
}
|
||||
|
||||
if (empty($parts)) {
|
||||
return new \WP_Error('empty_term', __('没有可用的标题或描述', 'aksrt'));
|
||||
}
|
||||
|
||||
return implode("\n\n", $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{content?:string,error?:string}
|
||||
*/
|
||||
private function run_field(AI $ai, string $context, string $system, string $field): array {
|
||||
$result = $ai->generate_text($context, [
|
||||
'system_instruction' => $system,
|
||||
'max_tokens' => $field === 'description' ? 400 : 300,
|
||||
'temperature' => 0.5,
|
||||
]);
|
||||
|
||||
if (empty($result['success'])) {
|
||||
return [
|
||||
'error' => (string) ($result['message'] ?? __('生成失败', 'aksrt')),
|
||||
];
|
||||
}
|
||||
|
||||
$raw = (string) ($result['content'] ?? '');
|
||||
return [
|
||||
'content' => $this->post_process_field($field, $raw),
|
||||
];
|
||||
}
|
||||
|
||||
private function post_process_field(string $field, string $raw): string {
|
||||
$raw = trim($raw);
|
||||
// Strip common wrappers
|
||||
$raw = preg_replace('/^["\'“”]+|["\'“”]+$/u', '', $raw) ?? $raw;
|
||||
$raw = trim($raw);
|
||||
|
||||
if ($field === 'title') {
|
||||
$title = sanitize_text_field($raw);
|
||||
$seo = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_seo_settings() : [];
|
||||
$conn = trim((string) ($seo['seo_connector'] ?? '-')) ?: '-';
|
||||
$site = trim((string) get_bloginfo('name'));
|
||||
if ($site !== '' && $title !== '' && mb_stripos($title, $site) === false) {
|
||||
$title = $title . ' ' . $conn . ' ' . $site;
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
if ($field === 'keywords') {
|
||||
$raw = str_replace(['、', ',', ';', ';', "\n", "\r"], ',', $raw);
|
||||
$parts = array_map('trim', explode(',', $raw));
|
||||
$parts = array_values(array_unique(array_filter($parts, static function ($p) {
|
||||
return $p !== '';
|
||||
})));
|
||||
$parts = array_map('sanitize_text_field', $parts);
|
||||
return implode(',', array_slice($parts, 0, 12));
|
||||
}
|
||||
|
||||
// description
|
||||
$raw = preg_replace('/\s+/u', ' ', $raw) ?? $raw;
|
||||
return sanitize_text_field(trim($raw));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user