feat: add prompt history management
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
|
||||
import { api } from "../services/api";
|
||||
import type { AgentDebugResult, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
|
||||
import AdminPagination from "./AdminPagination.vue";
|
||||
|
||||
const props = defineProps<{ previewKnowledgeId?: number | null }>();
|
||||
const emit = defineEmits<{ consumedPreview: [] }>();
|
||||
|
||||
const loading = ref(false);
|
||||
const promptSaving = ref(false);
|
||||
const historyLoading = ref(false);
|
||||
const agentDebugging = ref(false);
|
||||
const activeConfigTab = ref("prompt");
|
||||
const prompt = ref<PromptDetail | null>(null);
|
||||
const promptContent = ref("");
|
||||
const savedPromptContent = ref("");
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const knowledge = ref<KnowledgeItem[]>([]);
|
||||
const history = ref<PromptHistoryItem[]>([]);
|
||||
const historyPager = reactive({ page: 1, pageSize: 10, total: 0 });
|
||||
const historyDetailOpen = ref(false);
|
||||
const selectedHistory = ref<PromptDetail | null>(null);
|
||||
const historyDetailLoading = ref(false);
|
||||
const agentDebugTrace = ref<Record<string, any>[]>([]);
|
||||
const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string }[]>([
|
||||
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
|
||||
]);
|
||||
|
||||
const agentForm = reactive({
|
||||
modelId: undefined as number | undefined,
|
||||
knowledgeIds: [] as number[],
|
||||
temperature: 0.2 as number | null,
|
||||
topP: null as number | null,
|
||||
topK: null as number | null,
|
||||
presencePenalty: null as number | null,
|
||||
frequencyPenalty: null as number | null,
|
||||
maxToken: 1024 as number | null,
|
||||
question: "",
|
||||
});
|
||||
|
||||
const promptDirty = computed(() => promptContent.value !== savedPromptContent.value);
|
||||
const selectedModelName = computed(() => {
|
||||
const model = models.value.find((item) => item.id === agentForm.modelId);
|
||||
return model?.displayName || model?.modelName || "未选择模型";
|
||||
});
|
||||
const selectedKnowledgeSummary = computed(() => {
|
||||
if (!agentForm.knowledgeIds.length) return "未选择知识库";
|
||||
if (agentForm.knowledgeIds.length === 1) {
|
||||
return knowledge.value.find((item) => item.id === agentForm.knowledgeIds[0])?.name || "1 个知识库";
|
||||
}
|
||||
return `${agentForm.knowledgeIds.length} 个知识库`;
|
||||
});
|
||||
|
||||
onMounted(load);
|
||||
|
||||
watch(() => props.previewKnowledgeId, (knowledgeId) => {
|
||||
if (knowledgeId && knowledge.value.length) applyPreviewKnowledge(knowledgeId);
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [promptResult, modelRows, knowledgeRows] = await Promise.all([
|
||||
api.prompt(), api.models(), api.knowledgeOptions(),
|
||||
]);
|
||||
applyPrompt(promptResult);
|
||||
models.value = modelRows;
|
||||
knowledge.value = knowledgeRows;
|
||||
agentForm.modelId = modelRows.find((item) => item.enabled === 1)?.id ?? modelRows[0]?.id;
|
||||
agentForm.knowledgeIds = knowledgeRows
|
||||
.filter((item) => item.status === 1 && item.lifecycleStatus === "active")
|
||||
.map((item) => item.id);
|
||||
if (props.previewKnowledgeId) applyPreviewKnowledge(props.previewKnowledgeId);
|
||||
await loadHistory();
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "Agent 配置加载失败"));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyPreviewKnowledge(knowledgeId: number) {
|
||||
if (knowledge.value.some((item) => item.id === knowledgeId)) {
|
||||
agentForm.knowledgeIds = [knowledgeId];
|
||||
activeConfigTab.value = "debug";
|
||||
}
|
||||
emit("consumedPreview");
|
||||
}
|
||||
|
||||
function applyPrompt(value: PromptDetail) {
|
||||
prompt.value = value;
|
||||
promptContent.value = value.promptContent;
|
||||
savedPromptContent.value = value.promptContent;
|
||||
}
|
||||
|
||||
async function savePrompt() {
|
||||
if (!promptContent.value.trim()) return ElMessage.warning("主提示词不能为空");
|
||||
promptSaving.value = true;
|
||||
try {
|
||||
applyPrompt(await api.savePrompt(promptContent.value.trim()));
|
||||
ElMessage.success("主提示词已保存为新版本");
|
||||
await loadHistory(1);
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "主提示词保存失败"));
|
||||
} finally {
|
||||
promptSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetPrompt() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
"恢复默认提示词会新增一个版本,现有历史不会被删除。确认继续?",
|
||||
"恢复默认提示词",
|
||||
{ type: "warning", confirmButtonText: "确认恢复", cancelButtonText: "取消" },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
promptSaving.value = true;
|
||||
try {
|
||||
applyPrompt(await api.resetPrompt());
|
||||
ElMessage.success("已恢复默认提示词并保存为新版本");
|
||||
await loadHistory(1);
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "恢复默认提示词失败"));
|
||||
} finally {
|
||||
promptSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory(page = historyPager.page, pageSize = historyPager.pageSize) {
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const result = await api.promptHistory({ page, pageSize });
|
||||
history.value = result.items;
|
||||
Object.assign(historyPager, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "提示词历史加载失败"));
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openHistory(item: PromptHistoryItem) {
|
||||
historyDetailOpen.value = true;
|
||||
historyDetailLoading.value = true;
|
||||
selectedHistory.value = null;
|
||||
try {
|
||||
selectedHistory.value = await api.promptHistoryDetail(item.id);
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "提示词版本加载失败"));
|
||||
historyDetailOpen.value = false;
|
||||
} finally {
|
||||
historyDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreHistory(id: number) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认把版本 #${id} 恢复为当前主提示词?系统会新增一个恢复版本,不会覆盖历史。`,
|
||||
"恢复历史版本",
|
||||
{ type: "warning", confirmButtonText: "确认恢复", cancelButtonText: "取消" },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
promptSaving.value = true;
|
||||
try {
|
||||
applyPrompt(await api.restorePrompt(id));
|
||||
historyDetailOpen.value = false;
|
||||
activeConfigTab.value = "prompt";
|
||||
ElMessage.success(`版本 #${id} 已恢复为当前主提示词`);
|
||||
await loadHistory(1);
|
||||
} catch (error) {
|
||||
ElMessage.error(errorMessage(error, "历史版本恢复失败"));
|
||||
} finally {
|
||||
promptSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreSelectedHistory() {
|
||||
if (selectedHistory.value?.id) await restoreHistory(selectedHistory.value.id);
|
||||
}
|
||||
|
||||
async function debugAgent() {
|
||||
if (!agentForm.modelId) return ElMessage.error("请选择调试模型");
|
||||
if (!agentForm.question.trim()) return ElMessage.error("请输入调试问题");
|
||||
if (!promptContent.value.trim()) return ElMessage.error("主提示词不能为空");
|
||||
const question = agentForm.question.trim();
|
||||
agentPreviewMessages.value.push({ role: "user", content: question });
|
||||
agentForm.question = "";
|
||||
agentDebugging.value = true;
|
||||
try {
|
||||
const result: AgentDebugResult = await api.debugAgent({
|
||||
promptContent: promptContent.value,
|
||||
modelId: agentForm.modelId,
|
||||
knowledgeIds: agentForm.knowledgeIds,
|
||||
question,
|
||||
temperature: agentForm.temperature,
|
||||
topP: agentForm.topP,
|
||||
topK: agentForm.topK,
|
||||
presencePenalty: agentForm.presencePenalty,
|
||||
frequencyPenalty: agentForm.frequencyPenalty,
|
||||
maxToken: agentForm.maxToken,
|
||||
});
|
||||
if (!result.ok) {
|
||||
agentPreviewMessages.value.push({ role: "assistant", content: result.message });
|
||||
return ElMessage.error(result.message);
|
||||
}
|
||||
agentPreviewMessages.value.push({ role: "assistant", content: result.answer });
|
||||
agentDebugTrace.value = result.retrievalTrace || [];
|
||||
ElMessage.success(result.message);
|
||||
} catch (error) {
|
||||
const message = errorMessage(error, "Agent 调试失败");
|
||||
agentPreviewMessages.value.push({ role: "assistant", content: message });
|
||||
ElMessage.error(message);
|
||||
} finally {
|
||||
agentDebugging.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAgentPreview() {
|
||||
agentPreviewMessages.value = [{ role: "assistant", content: "预览已清空,可以继续发起新的 Agent 调试。" }];
|
||||
agentDebugTrace.value = [];
|
||||
}
|
||||
|
||||
function agentMessageParts(content: string) {
|
||||
const matched = content.match(/<think>([\s\S]*?)<\/think>/i);
|
||||
if (!matched) return { reasoning: "", answer: content };
|
||||
return {
|
||||
reasoning: matched[1].trim(),
|
||||
answer: content.replace(matched[0], "").trim() || "模型未返回正式回答。",
|
||||
};
|
||||
}
|
||||
|
||||
function changeTypeLabel(value?: string) {
|
||||
return ({ save: "手动保存", reset: "恢复默认", restore: "历史恢复", default: "系统默认" } as Record<string, string>)[value || ""] || "保存";
|
||||
}
|
||||
|
||||
function changeTypeTag(value?: string) {
|
||||
if (value === "reset") return "warning";
|
||||
if (value === "restore") return "success";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function formatDate(value?: string | null) {
|
||||
if (!value) return "尚未保存";
|
||||
return new Date(value).toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown, fallback: string) {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-head agent-page-head">
|
||||
<div>
|
||||
<h2>Agent 管理</h2>
|
||||
<p>维护主提示词及历史版本,配置本次调试参数,并验证真实检索与回答效果。</p>
|
||||
</div>
|
||||
<div class="agent-current-summary">
|
||||
<el-tag type="success">当前版本 {{ prompt?.id ? `#${prompt.id}` : "系统默认" }}</el-tag>
|
||||
<span>{{ prompt?.updatedByName || "系统默认" }} · {{ formatDate(prompt?.updatedAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-loading="loading" class="agent-workbench agent-workbench-v2">
|
||||
<div class="agent-config-panel agent-control-panel">
|
||||
<el-tabs v-model="activeConfigTab" class="agent-config-tabs">
|
||||
<el-tab-pane label="主提示词" name="prompt">
|
||||
<div class="agent-section-intro">
|
||||
<div><h3>当前主提示词</h3><p>保存、恢复默认或恢复历史版本时都会新增版本,历史记录不会被覆盖。</p></div>
|
||||
<el-tag v-if="promptDirty" type="warning">有未保存修改</el-tag>
|
||||
</div>
|
||||
<el-input v-model="promptContent" class="agent-prompt-editor" type="textarea" :rows="18" resize="vertical" placeholder="请输入 Agent 主提示词" />
|
||||
<div class="agent-editor-meta"><span>{{ promptContent.length }} 字符</span><span>调试会立即使用编辑框中的内容</span></div>
|
||||
<div class="actions agent-primary-actions">
|
||||
<el-button type="primary" :loading="promptSaving" :disabled="!promptDirty" @click="savePrompt">保存为新版本</el-button>
|
||||
<el-button :loading="promptSaving" @click="resetPrompt">恢复默认提示词</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="调试配置" name="debug">
|
||||
<div class="agent-section-intro"><div><h3>本次调试配置</h3><p>配置只作用于后台预览,不会修改模型和知识库的正式状态。</p></div></div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="调试模型">
|
||||
<el-select v-model="agentForm.modelId" placeholder="选择已接入模型">
|
||||
<el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="调试知识库">
|
||||
<el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库">
|
||||
<el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-collapse class="agent-advanced-settings">
|
||||
<el-collapse-item title="高级生成参数" name="generation">
|
||||
<div class="agent-config-grid">
|
||||
<el-form-item label="Temperature"><el-input-number v-model="agentForm.temperature" :min="0" :max="2" :step="0.1" /></el-form-item>
|
||||
<el-form-item label="Top P"><el-input-number v-model="agentForm.topP" :min="0" :max="1" :step="0.05" /></el-form-item>
|
||||
<el-form-item label="Top K"><el-input-number v-model="agentForm.topK" :min="1" :max="1000" /></el-form-item>
|
||||
<el-form-item label="Max Tokens"><el-input-number v-model="agentForm.maxToken" :min="1" :max="100000" /></el-form-item>
|
||||
<el-form-item label="Presence Penalty"><el-input-number v-model="agentForm.presencePenalty" :min="-2" :max="2" :step="0.1" /></el-form-item>
|
||||
<el-form-item label="Frequency Penalty"><el-input-number v-model="agentForm.frequencyPenalty" :min="-2" :max="2" :step="0.1" /></el-form-item>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="历史版本" name="history">
|
||||
<div class="agent-section-intro"><div><h3>主提示词历史</h3><p>查看完整内容或恢复任意版本。恢复操作会作为一个新版本记录。</p></div></div>
|
||||
<div v-loading="historyLoading" class="prompt-history-list">
|
||||
<el-empty v-if="history.length === 0" description="还没有已保存的提示词版本" :image-size="64" />
|
||||
<article v-for="item in history" :key="item.id" class="prompt-history-item" :class="{ current: item.isCurrent }">
|
||||
<div class="prompt-history-head">
|
||||
<strong>版本 #{{ item.id }}</strong>
|
||||
<el-tag v-if="item.isCurrent" type="success" size="small">当前</el-tag>
|
||||
<el-tag :type="changeTypeTag(item.changeType)" size="small">{{ changeTypeLabel(item.changeType) }}</el-tag>
|
||||
<span>{{ item.updatedByName }} · {{ formatDate(item.updatedAt) }}</span>
|
||||
</div>
|
||||
<p>{{ item.preview || "空内容" }}</p>
|
||||
<div class="prompt-history-foot">
|
||||
<span>{{ item.charCount }} 字符<span v-if="item.sourcePromptId"> · 来源 #{{ item.sourcePromptId }}</span></span>
|
||||
<div><el-button link type="primary" @click="openHistory(item)">查看完整内容</el-button><el-button v-if="!item.isCurrent" link type="primary" @click="restoreHistory(item.id)">恢复此版本</el-button></div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<AdminPagination :page="historyPager.page" :page-size="historyPager.pageSize" :total="historyPager.total" @change="loadHistory" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<aside class="agent-preview-panel">
|
||||
<div class="agent-preview-head">
|
||||
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }}</small></div>
|
||||
<div class="agent-preview-head-actions"><span>{{ selectedModelName }}</span><el-button link @click="clearAgentPreview">清空</el-button></div>
|
||||
</div>
|
||||
<div class="agent-preview-chat">
|
||||
<article v-for="(message, index) in agentPreviewMessages" :key="`${message.role}-${index}`" class="agent-preview-message-row" :class="message.role">
|
||||
<div class="agent-preview-avatar">{{ message.role === "user" ? "你" : "AI" }}</div>
|
||||
<div class="agent-preview-bubble">
|
||||
<strong>{{ message.role === "user" ? "你" : "Agent" }}</strong>
|
||||
<template v-if="message.role === 'assistant'">
|
||||
<details v-if="agentMessageParts(message.content).reasoning" class="agent-preview-thinking"><summary>思考过程</summary><pre>{{ agentMessageParts(message.content).reasoning }}</pre></details>
|
||||
<pre>{{ agentMessageParts(message.content).answer }}</pre>
|
||||
</template>
|
||||
<pre v-else>{{ message.content }}</pre>
|
||||
</div>
|
||||
</article>
|
||||
<article v-if="agentDebugging" class="agent-preview-message-row assistant thinking"><div class="agent-preview-avatar">AI</div><div class="agent-preview-bubble"><strong>Agent</strong><pre>正在思考中...</pre></div></article>
|
||||
</div>
|
||||
<div class="agent-preview-composer">
|
||||
<el-input v-model="agentForm.question" type="textarea" :rows="2" resize="none" placeholder="向 Agent 发送测试问题;Enter 发送,Shift+Enter 换行" @keydown.enter.exact.prevent="debugAgent" />
|
||||
<el-button type="primary" :loading="agentDebugging" @click="debugAgent">发送</el-button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside class="agent-trace-panel agent-trace-wide">
|
||||
<div class="agent-preview-head"><div><h3>检索追踪</h3><small>按调用顺序查看请求、响应、耗时与错误</small></div><span>本轮 {{ agentDebugTrace.length }} 次 Tool 调用</span></div>
|
||||
<div class="agent-trace-list">
|
||||
<el-empty v-if="agentDebugTrace.length === 0" description="发送问题后查看目录、检索与章节回读" :image-size="64" />
|
||||
<details v-for="(step, index) in agentDebugTrace" :key="index" class="tool-trace-node">
|
||||
<summary><strong>{{ index + 1 }}. {{ step.tool }}</strong><el-tag size="small" :type="step.status === 'failed' ? 'danger' : 'success'">{{ step.status || '完成' }}</el-tag><span>{{ step.durationMs ?? 0 }} ms</span></summary>
|
||||
<h5>请求</h5><pre>{{ JSON.stringify(step.request ?? {}, null, 2) }}</pre>
|
||||
<h5>响应</h5><pre>{{ step.response == null ? '返回为空' : JSON.stringify(step.response, null, 2) }}</pre>
|
||||
<el-alert v-if="step.error" :title="step.error" type="error" :closable="false" />
|
||||
</details>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="historyDetailOpen" width="min(820px, 92vw)" title="主提示词历史版本" destroy-on-close>
|
||||
<div v-loading="historyDetailLoading" class="prompt-history-detail">
|
||||
<template v-if="selectedHistory">
|
||||
<div class="prompt-history-detail-meta"><el-tag :type="changeTypeTag(selectedHistory.changeType)">{{ changeTypeLabel(selectedHistory.changeType) }}</el-tag><strong>版本 #{{ selectedHistory.id }}</strong><span>{{ selectedHistory.updatedByName }} · {{ formatDate(selectedHistory.updatedAt) }} · {{ selectedHistory.charCount }} 字符</span></div>
|
||||
<pre>{{ selectedHistory.promptContent }}</pre>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer><el-button @click="historyDetailOpen = false">关闭</el-button><el-button v-if="selectedHistory?.id !== prompt?.id" type="primary" :loading="promptSaving" @click="restoreSelectedHistory">恢复为当前主提示词</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user