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:
kate522 committed 2026-08-02 22:06:06 +08:00
1 parent 56f1a8175f
commit 8c5b1555c9
285 files changed
+26 -3600

No files matched your search

+281
View File
@@ -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));
}
}