feat(ai): 打通 SEO 闭环与投稿写作辅助
对齐 Zibll 的 AI 生成 SEO 流程:统一 WP AI Client/主题 Provider 双后端、文章/分类 SEO 框与生成回填、投稿摘要/标签建议,并收敛扩展页为连接配置。
This commit is contained in:
@@ -6,7 +6,10 @@ export const extensionsApiMethods = {
|
||||
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
||||
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
||||
|
||||
/* AI 辅助(兼容页) */
|
||||
/* AI 辅助 */
|
||||
getAiStatus() { return this.request('/ai/status') },
|
||||
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
|
||||
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
|
||||
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
|
||||
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
|
||||
}
|
||||
|
||||
@@ -64,17 +64,15 @@ export const extensionsSection = {
|
||||
title: '系统工具',
|
||||
desc: '额外运维工具与扩展能力。',
|
||||
}),
|
||||
// AI 能力属停用候选(stream-a/§5.9),不进入核心 schema:使用自有 /ai/* REST 端点,
|
||||
// 不声明 canonicalDomain/section,避免 SchemaPage 契约指向不存在的 schema section。
|
||||
customPage({
|
||||
key: 'extensions-ai',
|
||||
name: 'ExtensionAI',
|
||||
label: 'AI 辅助(兼容)',
|
||||
label: 'AI 连接配置',
|
||||
route: '/extensions/ai',
|
||||
component: 'ExtensionAI',
|
||||
status: 'available',
|
||||
title: 'AI 辅助(兼容)',
|
||||
desc: '兼容保留 AI 内容辅助能力,不作为主线扩展能力入口。',
|
||||
title: 'AI 连接配置',
|
||||
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,65 +1,63 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="AI 辅助(兼容)" desc="兼容保留的 AI 内容辅助能力,包括 SEO 建议、摘要生成、标签建议等。" />
|
||||
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
|
||||
|
||||
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
|
||||
<strong>加载失败:</strong>{{ loadError }}
|
||||
</div>
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<div class="aksrt-ai__status">
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.enabled ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
||||
<i :class="status.enabled ? 'fas fa-robot' : 'fas fa-robot'"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">AI 状态</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.enabled ? '已启用' : '已关闭' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.configured ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">API Key</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.configured ? '已配置' : '未配置' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
||||
<i class="fas fa-brain"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">模型</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.model || '未设置' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
||||
<i class="fas fa-robot"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">可用状态</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fas fa-plug"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">WP AI Client</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
||||
<i class="fas fa-key"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">主题 Provider</div>
|
||||
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-card">
|
||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
||||
<i class="fas fa-route"></i>
|
||||
</div>
|
||||
<div class="aksrt-ai__status-info">
|
||||
<div class="aksrt-ai__status-label">当前后端</div>
|
||||
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<div class="aksrt-ai__form-card">
|
||||
<h3>基本配置</h3>
|
||||
<h3>主题 Provider 配置(回退链路)</h3>
|
||||
<p class="aksrt-form-hint">当站点已配置 WordPress AI Connectors 时,生成会优先走核心客户端;以下配置作为兼容回退。</p>
|
||||
<div class="aksrt-form-group">
|
||||
<label>
|
||||
<input type="checkbox" v-model="form.enabled"> 启用 AI 内容辅助
|
||||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
||||
</label>
|
||||
<p class="aksrt-form-hint">开启后可在投稿、编辑等场景使用 AI 辅助功能</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group">
|
||||
<label>AI Provider</label>
|
||||
<select v-model="form.provider" class="aksrt-select">
|
||||
<option value="none">不使用</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openai">OpenAI 兼容接口</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
@@ -70,7 +68,6 @@
|
||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
||||
<label>API Base URL</label>
|
||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://api.openai.com/v1">
|
||||
<p class="aksrt-form-hint">可替换为兼容 OpenAI 接口的第三方服务</p>
|
||||
</div>
|
||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||||
<label>模型</label>
|
||||
@@ -91,49 +88,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能说明 -->
|
||||
<div class="aksrt-ai__form-card">
|
||||
<h3>支持的 AI 功能</h3>
|
||||
<h3>已接通能力</h3>
|
||||
<table class="aksrt-table aksrt-table--sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>功能</th>
|
||||
<th>说明</th>
|
||||
<th>使用场景</th>
|
||||
<th>能力</th>
|
||||
<th>入口</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="feat in features" :key="feat.key">
|
||||
<td>{{ feat.name }}</td>
|
||||
<td>{{ feat.desc }}</td>
|
||||
<td>{{ feat.scene }}</td>
|
||||
<td>
|
||||
<span class="aksrt-badge" :class="status.available ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
||||
{{ status.available ? '可用' : '未启用' }}
|
||||
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
||||
{{ feat.ready ? '可用' : '未就绪' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
|
||||
</div>
|
||||
|
||||
<!-- 重要提示 -->
|
||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
||||
<h3><i class="fas fa-exclamation-triangle"></i> 重要说明</h3>
|
||||
<ul>
|
||||
<li>AI 生成的内容仅作为<strong>建议参考</strong>,不会自动应用到线上内容</li>
|
||||
<li>所有 AI 输出需要<strong>人工审核确认</strong>后才能发布</li>
|
||||
<li>AI 默认关闭,需管理员显式启用并配置 API Key</li>
|
||||
<li>API Key 不会回显到前端,不会通过 <code>wp_localize_script</code> 透出</li>
|
||||
<li>内置速率限制,防止滥用</li>
|
||||
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
|
||||
<li>优先使用 WordPress AI Connectors;主题 API Key 为兼容回退</li>
|
||||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import PageHeader from '@/components/PageHeader.vue';
|
||||
import api from '@/api';
|
||||
|
||||
@@ -149,6 +141,11 @@ export default {
|
||||
api_key: '',
|
||||
rate_limit: 10,
|
||||
available: false,
|
||||
backend: 'none',
|
||||
wp_ai_available: false,
|
||||
theme_available: false,
|
||||
seo_ai_enabled: false,
|
||||
write_assist: false,
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
@@ -164,12 +161,20 @@ export default {
|
||||
const saving = ref(false);
|
||||
const loadError = ref('');
|
||||
|
||||
const features = [
|
||||
{ key: 'seo', name: 'SEO 建议', desc: '生成标题、描述、关键词建议', scene: '文章编辑' },
|
||||
{ key: 'summary', name: '摘要生成', desc: '自动生成文章摘要', scene: '文章编辑、投稿' },
|
||||
{ key: 'tags', name: '标签建议', desc: '根据内容推荐标签', scene: '文章编辑、投稿' },
|
||||
{ key: 'excerpt', name: '摘要建议', desc: '生成吸引人的列表摘要', scene: '文章编辑' },
|
||||
];
|
||||
const backendLabel = computed(() => {
|
||||
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
|
||||
return map[status.value.backend] || status.value.backend || '无';
|
||||
});
|
||||
|
||||
const features = computed(() => {
|
||||
const avail = !!status.value.available;
|
||||
const seoReady = avail && !!status.value.seo_ai_enabled;
|
||||
return [
|
||||
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
|
||||
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
|
||||
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
|
||||
];
|
||||
});
|
||||
|
||||
function hydrateFromStatus(data) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
@@ -181,18 +186,20 @@ export default {
|
||||
api_key: '',
|
||||
rate_limit: data.rate_limit || 10,
|
||||
available: !!data.available,
|
||||
backend: data.backend || 'none',
|
||||
wp_ai_available: !!data.wp_ai_available,
|
||||
theme_available: !!data.theme_available,
|
||||
seo_ai_enabled: !!data.seo_ai_enabled,
|
||||
write_assist: !!data.write_assist,
|
||||
};
|
||||
form.enabled = !!data.enabled;
|
||||
form.provider = data.provider || 'none';
|
||||
form.model = data.model || 'gpt-3.5-turbo';
|
||||
form.rate_limit = data.rate_limit || 10;
|
||||
if (data.api_base) {
|
||||
form.api_base = data.api_base;
|
||||
}
|
||||
if (data.api_base) form.api_base = data.api_base;
|
||||
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
||||
form.max_tokens = data.max_tokens;
|
||||
}
|
||||
// 不回显 api_key
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
@@ -210,7 +217,6 @@ export default {
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = { ...form };
|
||||
// 如果密钥为空且已配置,发送掩码(后端应忽略 *** 保留原密钥)
|
||||
if (!payload.api_key && status.value.configured) {
|
||||
payload.api_key = '***';
|
||||
}
|
||||
@@ -227,7 +233,7 @@ export default {
|
||||
|
||||
onMounted(() => loadStatus());
|
||||
|
||||
return { status, form, saving, loadError, features, saveSettings };
|
||||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -257,31 +263,12 @@ export default {
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
.aksrt-ai__status-icon--ok {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.aksrt-ai__status-icon--warn {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.aksrt-ai__status-icon--off {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.aksrt-ai__status-icon--info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
.aksrt-ai__status-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.aksrt-ai__status-label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
|
||||
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
|
||||
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
|
||||
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
|
||||
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: #1a1a2e; }
|
||||
.aksrt-ai__status-label { font-size: 12px; color: #888; }
|
||||
.aksrt-ai__form-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
@@ -289,41 +276,32 @@ export default {
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.aksrt-ai__form-card h3 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.aksrt-form-hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
|
||||
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
.aksrt-ai__load-error {
|
||||
background: #fdecea;
|
||||
border-color: #f5c2c0;
|
||||
color: #842029;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.aksrt-ai__notice {
|
||||
background: #fff8e1;
|
||||
border-color: #ffe082;
|
||||
}
|
||||
.aksrt-ai__notice h3 {
|
||||
color: #e65100;
|
||||
}
|
||||
.aksrt-ai__notice ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.aksrt-ai__notice li {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
|
||||
.aksrt-ai__notice h3 { color: #e65100; }
|
||||
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
|
||||
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
|
||||
.aksrt-ai__notice code {
|
||||
background: rgba(0,0,0,0.06);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
|
||||
.aksrt-badge--success { background: #d4edda; color: #155724; }
|
||||
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
|
||||
.aksrt-table { width: 100%; border-collapse: collapse; }
|
||||
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
||||
.aksrt-form-group { margin-bottom: 14px; }
|
||||
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
|
||||
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
|
||||
.aksrt-button--primary { background: #1677ff; color: #fff; }
|
||||
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
|
||||
</style>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user