Accept gateway root URLs like https://llm.example.com and auto-append /v1 so NewAPI-style providers work; update Extension AI help text accordingly.
317 lines
12 KiB
Vue
317 lines
12 KiB
Vue
<template>
|
||
<div class="aksrt-page-container">
|
||
<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.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>主题 Provider 配置(回退链路)</h3>
|
||
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
|
||
<div class="aksrt-form-group">
|
||
<label>
|
||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
||
</label>
|
||
</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>
|
||
</select>
|
||
</div>
|
||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||
<label>API Key</label>
|
||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
||
</div>
|
||
<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://llm.example.com/v1">
|
||
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
|
||
</div>
|
||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||
<label>模型</label>
|
||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
||
</div>
|
||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||
<label>最大 Token 数</label>
|
||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
||
</div>
|
||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
||
<label>每分钟请求限制</label>
|
||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
||
</div>
|
||
<div class="aksrt-form-actions">
|
||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
||
{{ saving ? '保存中...' : '保存设置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="aksrt-ai__form-card">
|
||
<h3>已接通能力</h3>
|
||
<table class="aksrt-table aksrt-table--sm">
|
||
<thead>
|
||
<tr>
|
||
<th>能力</th>
|
||
<th>入口</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="feat in features" :key="feat.key">
|
||
<td>{{ feat.name }}</td>
|
||
<td>{{ feat.scene }}</td>
|
||
<td>
|
||
<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>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
|
||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, reactive, computed, onMounted } from 'vue';
|
||
import PageHeader from '@/components/PageHeader.vue';
|
||
import api from '@/api';
|
||
|
||
export default {
|
||
name: 'ExtensionAI',
|
||
components: { PageHeader },
|
||
setup() {
|
||
const status = ref({
|
||
enabled: false,
|
||
provider: 'none',
|
||
configured: false,
|
||
model: '',
|
||
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({
|
||
enabled: false,
|
||
provider: 'none',
|
||
api_key: '',
|
||
api_base: 'https://api.openai.com/v1',
|
||
model: 'gpt-3.5-turbo',
|
||
max_tokens: 1000,
|
||
rate_limit: 10,
|
||
});
|
||
|
||
const saving = ref(false);
|
||
const loadError = ref('');
|
||
|
||
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;
|
||
status.value = {
|
||
enabled: !!data.enabled,
|
||
provider: data.provider || 'none',
|
||
configured: !!data.configured,
|
||
model: data.model || '',
|
||
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 (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
||
form.max_tokens = data.max_tokens;
|
||
}
|
||
}
|
||
|
||
async function loadStatus() {
|
||
loadError.value = '';
|
||
try {
|
||
const json = await api.getAiStatus();
|
||
hydrateFromStatus(json.data || {});
|
||
} catch (e) {
|
||
console.error('Load AI status failed:', e);
|
||
loadError.value = e?.message || '加载 AI 状态失败';
|
||
}
|
||
}
|
||
|
||
async function saveSettings() {
|
||
saving.value = true;
|
||
try {
|
||
const payload = {
|
||
enabled: form.enabled,
|
||
provider: form.provider,
|
||
api_base: form.api_base,
|
||
model: form.model,
|
||
max_tokens: form.max_tokens,
|
||
rate_limit: form.rate_limit,
|
||
};
|
||
// Only send api_key when the user entered a new one.
|
||
if (form.api_key && form.api_key !== '***') {
|
||
payload.api_key = form.api_key;
|
||
}
|
||
const json = await api.updateAiSettings(payload);
|
||
alert(json.message || '设置已保存');
|
||
await loadStatus();
|
||
} catch (e) {
|
||
console.error('Save failed:', e);
|
||
alert(e?.message || '保存失败');
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(() => loadStatus());
|
||
|
||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.aksrt-ai__status {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.aksrt-ai__status-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
border: 1px solid #e8e8e8;
|
||
}
|
||
.aksrt-ai__status-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
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__form-card {
|
||
background: #fff;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 8px;
|
||
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__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 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>
|