feat(ai): 打通 SEO 闭环与投稿写作辅助

对齐 Zibll 的 AI 生成 SEO 流程:统一 WP AI Client/主题 Provider 双后端、文章/分类 SEO 框与生成回填、投稿摘要/标签建议,并收敛扩展页为连接配置。
This commit is contained in:
kate522 committed 2026-07-12 15:36:04 +08:00
1 parent a4cd5276e2
commit b36186da65
14 files changed
+1243 -330

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
/**
* AKSRT Admin AI SEO — generate button for post/term SEO fields
*/
(function () {
'use strict';
if (!window.aksrtAiSeo) return;
var CFG = window.aksrtAiSeo;
var REST = (CFG.restUrl || '/wp-json/aksrt/v1').replace(/\/$/, '');
var NONCE = CFG.nonce || '';
var I18N = CFG.i18n || {};
function notify($btn, msg, type) {
var wrap = $btn.closest('.aksrt-seo-meta-box, td, .form-field, p') || $btn.parentNode;
if (!wrap) return;
var old = wrap.querySelector('.aksrt-ai-seo-msg');
if (old) old.remove();
var el = document.createElement('span');
el.className = 'aksrt-ai-seo-msg ' + (type === 'error' ? 'is-error' : 'is-ok');
el.textContent = msg;
wrap.appendChild(el);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 8000);
}
function fillPost(data) {
var map = {
title: 'aksrt_seo_title',
keywords: 'aksrt_seo_keywords',
description: 'aksrt_seo_description',
};
Object.keys(map).forEach(function (key) {
var field = data[key] || {};
if (!field.content) return;
var input = document.getElementById(map[key]) || document.querySelector('[name="' + map[key] + '"]');
if (input) input.value = field.content;
});
}
function fillTerm(data) {
var map = {
title: '.aksrt-term-seo-title, #aksrt_seo_title',
keywords: '.aksrt-term-seo-keywords, #aksrt_seo_keywords',
description: '.aksrt-term-seo-description, #aksrt_seo_description',
};
Object.keys(map).forEach(function (key) {
var field = data[key] || {};
if (!field.content) return;
var input = document.querySelector(map[key]);
if (input) input.value = field.content;
});
}
function collectErrors(data) {
var errors = [];
['title', 'keywords', 'description'].forEach(function (key) {
if (data[key] && data[key].error) errors.push(data[key].error);
});
return errors;
}
document.addEventListener('click', function (e) {
var btn = e.target.closest('.aksrt-ai-seo-generate');
if (!btn) return;
e.preventDefault();
if (btn.classList.contains('is-loading') || btn.disabled) return;
var type = btn.getAttribute('data-type') || 'post';
var id = parseInt(btn.getAttribute('data-id') || '0', 10);
if (!id) {
notify(btn, I18N.needSave || '请先保存后再生成 SEO', 'error');
return;
}
if (CFG.ai && CFG.ai.available === false) {
notify(btn, I18N.notAvailable || 'AI 未配置或未启用', 'error');
return;
}
var original = btn.textContent;
btn.classList.add('is-loading');
btn.textContent = I18N.generating || '生成中…';
fetch(REST + '/ai/seo-generate', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': NONCE,
},
body: JSON.stringify({ type: type, id: id }),
})
.then(function (r) {
return r.json().then(function (json) {
return { ok: r.ok, json: json };
});
})
.then(function (res) {
btn.classList.remove('is-loading');
btn.textContent = original;
if (!res.ok || !res.json || res.json.success === false) {
var msg =
(res.json && (res.json.message || (res.json.data && res.json.data.message))) ||
I18N.failed ||
'生成失败';
notify(btn, msg, 'error');
return;
}
var data = res.json.data || {};
if (type === 'term') fillTerm(data);
else fillPost(data);
var errors = collectErrors(data);
if (errors.length) {
notify(btn, errors.join(';'), 'error');
} else {
notify(btn, I18N.success || '已填入 SEO 字段,请检查后更新保存', 'ok');
}
})
.catch(function () {
btn.classList.remove('is-loading');
btn.textContent = original;
notify(btn, I18N.failed || '生成失败', 'error');
});
});
})();
+110
View File
@@ -13,6 +13,9 @@
var i18n = (window.aksrtSubmitData && window.aksrtSubmitData.i18n) || {};
var ajaxUrl = (window.aksrtSubmitData && window.aksrtSubmitData.ajaxUrl) || '/wp-admin/admin-ajax.php';
var nonce = (window.aksrtSubmitData && window.aksrtSubmitData.nonce) || '';
var restUrl = ((window.aksrtSubmitData && window.aksrtSubmitData.restUrl) || '/wp-json/aksrt/v1').replace(/\/$/, '');
var restNonce = (window.aksrtSubmitData && window.aksrtSubmitData.restNonce) || '';
var aiCfg = (window.aksrtSubmitData && window.aksrtSubmitData.ai) || {};
var FRONT_SUBMIT = {
form: null,
@@ -31,6 +34,7 @@
this.bindCoverPreview();
this.bindWordCount();
this.bindLeaveWarning();
this.bindAiAssist();
},
/**
@@ -351,6 +355,112 @@
contentEl.addEventListener('input', updateCount);
updateCount();
},
/**
* AI 摘要 / 标签辅助(仅建议回填,不自动提交)
*/
bindAiAssist: function () {
var self = this;
if (!aiCfg.available) return;
var buttons = this.form.querySelectorAll('.aksrt-ai-assist-btn');
if (!buttons.length) return;
buttons.forEach(function (btn) {
btn.hidden = false;
btn.addEventListener('click', function (e) {
e.preventDefault();
var action = btn.getAttribute('data-ai-action');
if (action === 'summary') self.runAiSummary(btn);
if (action === 'tags') self.runAiTags(btn);
});
});
},
getSubmitContent: function () {
var titleEl = document.getElementById('submit-title');
var contentEl = document.getElementById('submit-content');
return {
title: titleEl ? titleEl.value.trim() : '',
content: contentEl ? contentEl.value.trim() : '',
};
},
runAiSummary: function (btn) {
var data = this.getSubmitContent();
if (!data.content) {
alert(i18n.needContent || '请先填写正文内容');
return;
}
this.aiRequest(btn, '/ai/summary-suggest', {
title: data.title,
content: data.content,
max_length: 200,
}, function (payload) {
var summary = payload && payload.summary ? String(payload.summary) : '';
var el = document.getElementById('submit-excerpt');
if (el && summary) {
el.value = summary;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
});
},
runAiTags: function (btn) {
var data = this.getSubmitContent();
if (!data.content && !data.title) {
alert(i18n.needContent || '请先填写正文内容');
return;
}
this.aiRequest(btn, '/ai/tags-suggest', {
title: data.title,
content: data.content,
}, function (payload) {
var tags = payload && Array.isArray(payload.tags) ? payload.tags : [];
var el = document.getElementById('submit-tags');
if (el && tags.length) {
el.value = tags.join(', ');
el.dispatchEvent(new Event('input', { bubbles: true }));
}
});
},
aiRequest: function (btn, path, body, onSuccess) {
if (btn.getAttribute('data-loading') === '1') return;
var original = btn.textContent;
btn.setAttribute('data-loading', '1');
btn.textContent = i18n.aiGenerating || '生成中…';
fetch(restUrl + path, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': restNonce,
},
body: JSON.stringify(body || {}),
})
.then(function (r) {
return r.json().then(function (json) {
return { ok: r.ok, json: json };
});
})
.then(function (res) {
btn.removeAttribute('data-loading');
btn.textContent = original;
if (!res.ok || !res.json || res.json.success === false) {
var msg = (res.json && res.json.message) || i18n.aiFailed || 'AI 生成失败';
alert(msg);
return;
}
onSuccess(res.json.data || {});
})
.catch(function () {
btn.removeAttribute('data-loading');
btn.textContent = original;
alert(i18n.aiFailed || 'AI 生成失败');
});
},
};
if (document.readyState === 'loading') {