225 lines
9.5 KiB
HTML
225 lines
9.5 KiB
HTML
{% extends "admin/base.html" %}
|
||
|
||
{% block main_content %}
|
||
<section class="form-panel">
|
||
<div class="table-head">
|
||
<div>
|
||
<h2>站点资料</h2>
|
||
<div class="table-note">保存后首页、关于页、页脚和友链页中的本站信息会直接读取这里的配置。AI 问答也在这里统一开启和配置。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="site-settings-form" class="form-grid">
|
||
<div class="field">
|
||
<label>站点名称</label>
|
||
<input name="site_name" value="{{ form.site_name }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>短名称</label>
|
||
<input name="site_short_name" value="{{ form.site_short_name }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>站点链接</label>
|
||
<input name="site_url" value="{{ form.site_url }}">
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>站点标题</label>
|
||
<input name="site_title" value="{{ form.site_title }}">
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>站点简介</label>
|
||
<textarea name="site_description">{{ form.site_description }}</textarea>
|
||
</div>
|
||
<div class="field">
|
||
<label>首页主标题</label>
|
||
<input name="hero_title" value="{{ form.hero_title }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>首页副标题</label>
|
||
<input name="hero_subtitle" value="{{ form.hero_subtitle }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>个人名称</label>
|
||
<input name="owner_name" value="{{ form.owner_name }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>个人头衔</label>
|
||
<input name="owner_title" value="{{ form.owner_title }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>头像 URL</label>
|
||
<input name="owner_avatar_url" value="{{ form.owner_avatar_url }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>所在地</label>
|
||
<input name="location" value="{{ form.location }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>GitHub</label>
|
||
<input name="social_github" value="{{ form.social_github }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>Twitter / X</label>
|
||
<input name="social_twitter" value="{{ form.social_twitter }}">
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>Email / mailto</label>
|
||
<input name="social_email" value="{{ form.social_email }}">
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>个人简介</label>
|
||
<textarea name="owner_bio">{{ form.owner_bio }}</textarea>
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>技术栈(每行一个)</label>
|
||
<textarea name="tech_stack">{{ form.tech_stack }}</textarea>
|
||
</div>
|
||
|
||
<div class="field field-wide" style="border-top: 1px solid rgba(148, 163, 184, 0.18); padding-top: 18px; margin-top: 10px;">
|
||
<label style="display:flex; align-items:center; gap:10px;">
|
||
<input type="checkbox" name="ai_enabled" {% if form.ai_enabled %}checked{% endif %}>
|
||
<span>启用前台 AI 问答</span>
|
||
</label>
|
||
<div class="field-hint">关闭后,前台导航不会显示 AI 页面,公开接口也不会对外提供回答。Embedding 已改为后端本地生成,并使用 PostgreSQL 的 pgvector 存储与检索。</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>聊天 Provider</label>
|
||
<input name="ai_provider" value="{{ form.ai_provider }}" placeholder="newapi">
|
||
</div>
|
||
<div class="field">
|
||
<label>聊天 API Base</label>
|
||
<input name="ai_api_base" value="{{ form.ai_api_base }}" placeholder="http://localhost:8317/v1">
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>聊天 API Key</label>
|
||
<input name="ai_api_key" value="{{ form.ai_api_key }}" placeholder="your-api-key-1">
|
||
<div class="field-hint">这里只保存在后端数据库里,前台公开接口不会返回这个字段。当前默认接入本地 NewAPI 网关,未配置时前台仍可做本地检索,但不会生成完整聊天回答。</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>聊天模型</label>
|
||
<input name="ai_chat_model" value="{{ form.ai_chat_model }}" placeholder="gpt-5.4">
|
||
</div>
|
||
<div class="field">
|
||
<label>本地 Embedding</label>
|
||
<input value="{{ form.ai_local_embedding }}" disabled>
|
||
</div>
|
||
<div class="field">
|
||
<label>Top K</label>
|
||
<input type="number" min="1" max="12" name="ai_top_k" value="{{ form.ai_top_k }}">
|
||
</div>
|
||
<div class="field">
|
||
<label>Chunk Size</label>
|
||
<input type="number" min="400" max="4000" step="50" name="ai_chunk_size" value="{{ form.ai_chunk_size }}">
|
||
</div>
|
||
<div class="field field-wide">
|
||
<label>系统提示词</label>
|
||
<textarea name="ai_system_prompt">{{ form.ai_system_prompt }}</textarea>
|
||
</div>
|
||
<div class="field field-wide">
|
||
<div class="table-note">AI 索引状态:已索引 {{ form.ai_chunks_count }} 个片段,最近建立时间 {{ form.ai_last_indexed_at }}。</div>
|
||
<div class="actions">
|
||
<button type="submit" class="btn btn-primary">保存设置</button>
|
||
<button type="button" id="reindex-btn" class="btn">重建 AI 索引</button>
|
||
</div>
|
||
<div class="field-hint" style="margin-top: 10px;">文章内容变化后建议手动重建一次 AI 索引。本地 embedding 使用后端内置 `fastembed` 生成,向量会写入 PostgreSQL 的 `pgvector` 列,并通过 HNSW 索引做相似度检索;聊天回答默认走 `newapi -> /responses -> gpt-5.4`。</div>
|
||
<div id="notice" class="notice"></div>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
{% endblock %}
|
||
|
||
{% block page_scripts %}
|
||
<script>
|
||
const form = document.getElementById("site-settings-form");
|
||
const notice = document.getElementById("notice");
|
||
const reindexBtn = document.getElementById("reindex-btn");
|
||
|
||
function showNotice(message, kind) {
|
||
notice.textContent = message;
|
||
notice.className = "notice show " + (kind === "success" ? "notice-success" : "notice-error");
|
||
}
|
||
|
||
function numericOrNull(value) {
|
||
const parsed = Number(value);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
}
|
||
|
||
form?.addEventListener("submit", async (event) => {
|
||
event.preventDefault();
|
||
|
||
const data = new FormData(form);
|
||
const payload = {
|
||
siteName: data.get("site_name"),
|
||
siteShortName: data.get("site_short_name"),
|
||
siteUrl: data.get("site_url"),
|
||
siteTitle: data.get("site_title"),
|
||
siteDescription: data.get("site_description"),
|
||
heroTitle: data.get("hero_title"),
|
||
heroSubtitle: data.get("hero_subtitle"),
|
||
ownerName: data.get("owner_name"),
|
||
ownerTitle: data.get("owner_title"),
|
||
ownerAvatarUrl: data.get("owner_avatar_url"),
|
||
location: data.get("location"),
|
||
socialGithub: data.get("social_github"),
|
||
socialTwitter: data.get("social_twitter"),
|
||
socialEmail: data.get("social_email"),
|
||
ownerBio: data.get("owner_bio"),
|
||
techStack: String(data.get("tech_stack") || "")
|
||
.split("\n")
|
||
.map((item) => item.trim())
|
||
.filter(Boolean),
|
||
aiEnabled: data.get("ai_enabled") === "on",
|
||
aiProvider: data.get("ai_provider"),
|
||
aiApiBase: data.get("ai_api_base"),
|
||
aiApiKey: data.get("ai_api_key"),
|
||
aiChatModel: data.get("ai_chat_model"),
|
||
aiTopK: numericOrNull(data.get("ai_top_k")),
|
||
aiChunkSize: numericOrNull(data.get("ai_chunk_size")),
|
||
aiSystemPrompt: data.get("ai_system_prompt")
|
||
};
|
||
|
||
try {
|
||
const response = await fetch("/api/site_settings", {
|
||
method: "PATCH",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(await response.text() || "save failed");
|
||
}
|
||
|
||
showNotice("站点信息与 AI 配置已保存。", "success");
|
||
} catch (error) {
|
||
showNotice("保存失败:" + (error?.message || "unknown error"), "error");
|
||
}
|
||
});
|
||
|
||
reindexBtn?.addEventListener("click", async () => {
|
||
reindexBtn.disabled = true;
|
||
reindexBtn.textContent = "正在重建...";
|
||
|
||
try {
|
||
const response = await fetch("/api/ai/reindex", {
|
||
method: "POST"
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(await response.text() || "reindex failed");
|
||
}
|
||
|
||
const data = await response.json();
|
||
showNotice(`AI 索引已重建,当前共有 ${data.indexed_chunks} 个片段。`, "success");
|
||
window.setTimeout(() => window.location.reload(), 900);
|
||
} catch (error) {
|
||
showNotice("重建失败:" + (error?.message || "unknown error"), "error");
|
||
} finally {
|
||
reindexBtn.disabled = false;
|
||
reindexBtn.textContent = "重建 AI 索引";
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock %}
|