perf: paginate admin audit lists
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
|
||||||
import AdminLoginView from "./components/AdminLoginView.vue";
|
import AdminLoginView from "./components/AdminLoginView.vue";
|
||||||
|
import AdminPagination from "./components/AdminPagination.vue";
|
||||||
import KnowledgeManagementView from "./components/KnowledgeManagementView.vue";
|
import KnowledgeManagementView from "./components/KnowledgeManagementView.vue";
|
||||||
import TableRowActions from "./components/TableRowActions.vue";
|
import TableRowActions from "./components/TableRowActions.vue";
|
||||||
import { systemSettingDefinitions, systemSettingSections, type SystemSettingValue } from "./config/systemSettings";
|
import { systemSettingDefinitions, systemSettingSections, type SystemSettingValue } from "./config/systemSettings";
|
||||||
@@ -55,6 +56,14 @@ const chatDetailOpen = ref(false);
|
|||||||
const selectedAiLog = ref<AiLogRecord | null>(null);
|
const selectedAiLog = ref<AiLogRecord | null>(null);
|
||||||
const aiLogDetailOpen = ref(false);
|
const aiLogDetailOpen = ref(false);
|
||||||
const recordTab = ref("chats");
|
const recordTab = ref("chats");
|
||||||
|
const pagers = reactive({
|
||||||
|
users: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
chats: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
aiLogs: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
operationLogs: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
retrievals: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
attention: { page: 1, pageSize: 20, total: 0 },
|
||||||
|
});
|
||||||
const editingModelId = ref<number | null>(null);
|
const editingModelId = ref<number | null>(null);
|
||||||
const editingKnowledgeId = ref<number | null>(null);
|
const editingKnowledgeId = ref<number | null>(null);
|
||||||
const batchNodeToken = ref("");
|
const batchNodeToken = ref("");
|
||||||
@@ -210,6 +219,10 @@ const agentForm = reactive({
|
|||||||
question: "",
|
question: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(recordTab, async (tab) => {
|
||||||
|
if (activeMenu.value === "records") await loadRecordTab(tab);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!getToken()) return;
|
if (!getToken()) return;
|
||||||
try {
|
try {
|
||||||
@@ -268,10 +281,9 @@ async function loadCurrentMenu() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
if (activeMenu.value === "dashboard") await loadDashboard();
|
if (activeMenu.value === "dashboard") await loadDashboard();
|
||||||
if (activeMenu.value === "users") users.value = await api.users(userKeyword.value);
|
if (activeMenu.value === "users") await loadUsers(pagers.users.page, pagers.users.pageSize);
|
||||||
if (activeMenu.value === "knowledge") knowledge.value = await api.knowledge();
|
|
||||||
if (activeMenu.value === "prompt") {
|
if (activeMenu.value === "prompt") {
|
||||||
const [prompt, modelRows, knowledgeRows] = await Promise.all([api.prompt(), api.models(), api.knowledge()]);
|
const [prompt, modelRows, knowledgeRows] = await Promise.all([api.prompt(), api.models(), api.knowledgeOptions()]);
|
||||||
promptContent.value = prompt.promptContent;
|
promptContent.value = prompt.promptContent;
|
||||||
models.value = modelRows;
|
models.value = modelRows;
|
||||||
knowledge.value = knowledgeRows;
|
knowledge.value = knowledgeRows;
|
||||||
@@ -285,18 +297,51 @@ async function loadCurrentMenu() {
|
|||||||
configs.value = await api.configs();
|
configs.value = await api.configs();
|
||||||
hydrateSystemSettings();
|
hydrateSystemSettings();
|
||||||
}
|
}
|
||||||
if (activeMenu.value === "records") {
|
if (activeMenu.value === "records") await loadRecordTab(recordTab.value);
|
||||||
chats.value = await api.chats(buildChatQuery());
|
if (activeMenu.value === "retrievals") { const [logs, retention] = await Promise.all([api.retrievalLogs({ page: pagers.retrievals.page, pageSize: pagers.retrievals.pageSize }), api.retrievalRetention()]); retrievalLogs.value = logs.items; Object.assign(pagers.retrievals, { page: logs.page, pageSize: logs.pageSize, total: logs.total }); retentionDays.value = retention.retentionDays; retentionState.value = retention; }
|
||||||
aiLogs.value = await api.aiLogs();
|
if (activeMenu.value === "attention") await loadAttention(pagers.attention.page, pagers.attention.pageSize);
|
||||||
operationLogs.value = await api.operationLogs();
|
|
||||||
}
|
|
||||||
if (activeMenu.value === "retrievals") { const [logs, retention] = await Promise.all([api.retrievalLogs(), api.retrievalRetention()]); retrievalLogs.value = logs; retentionDays.value = retention.retentionDays; retentionState.value = retention; }
|
|
||||||
if (activeMenu.value === "attention") attentionRecords.value = await api.attentionList();
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadUsers(page = 1, pageSize = pagers.users.pageSize) {
|
||||||
|
const result = await api.users({ keyword: userKeyword.value, page, pageSize });
|
||||||
|
users.value = result.items;
|
||||||
|
Object.assign(pagers.users, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRecordTab(tab = recordTab.value) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (tab === "chats") {
|
||||||
|
const result = await api.chats({ ...buildChatQuery(), page: pagers.chats.page, pageSize: pagers.chats.pageSize });
|
||||||
|
chats.value = result.items; Object.assign(pagers.chats, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
} else if (tab === "aiLogs") {
|
||||||
|
const result = await api.aiLogs({ page: pagers.aiLogs.page, pageSize: pagers.aiLogs.pageSize });
|
||||||
|
aiLogs.value = result.items; Object.assign(pagers.aiLogs, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
} else {
|
||||||
|
const result = await api.operationLogs({ page: pagers.operationLogs.page, pageSize: pagers.operationLogs.pageSize });
|
||||||
|
operationLogs.value = result.items; Object.assign(pagers.operationLogs, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
}
|
||||||
|
} finally { loading.value = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeRecordPage(tab: "chats" | "aiLogs" | "operationLogs", page: number, pageSize: number) {
|
||||||
|
Object.assign(pagers[tab], { page, pageSize });
|
||||||
|
await loadRecordTab(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRetrievals(page = 1, pageSize = pagers.retrievals.pageSize) {
|
||||||
|
const result = await api.retrievalLogs({ page, pageSize });
|
||||||
|
retrievalLogs.value = result.items; Object.assign(pagers.retrievals, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAttention(page = 1, pageSize = pagers.attention.pageSize) {
|
||||||
|
const result = await api.attentionList({ page, pageSize });
|
||||||
|
attentionRecords.value = result.items; Object.assign(pagers.attention, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
}
|
||||||
|
|
||||||
async function saveUser(row: AdminUser) {
|
async function saveUser(row: AdminUser) {
|
||||||
await api.updateUser(row.id, { status: row.status, dailyChatLimit: row.dailyChatLimit });
|
await api.updateUser(row.id, { status: row.status, dailyChatLimit: row.dailyChatLimit });
|
||||||
ElMessage.success("用户已更新");
|
ElMessage.success("用户已更新");
|
||||||
@@ -766,7 +811,7 @@ async function openRetrievalDetail(row: RetrievalLogItem) {
|
|||||||
async function updateAttentionStatus(row: AttentionItem, status: string) {
|
async function updateAttentionStatus(row: AttentionItem, status: string) {
|
||||||
await api.updateAttention(row.id, status);
|
await api.updateAttention(row.id, status);
|
||||||
ElMessage.success("人工关注状态已更新");
|
ElMessage.success("人工关注状态已更新");
|
||||||
attentionRecords.value = await api.attentionList();
|
await loadAttention(pagers.attention.page, pagers.attention.pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAttentionRecord(row: AttentionItem) {
|
async function deleteAttentionRecord(row: AttentionItem) {
|
||||||
@@ -779,7 +824,7 @@ async function deleteAttentionRecord(row: AttentionItem) {
|
|||||||
}
|
}
|
||||||
await api.deleteAttention(row.id);
|
await api.deleteAttention(row.id);
|
||||||
ElMessage.success("人工关注记录已删除");
|
ElMessage.success("人工关注记录已删除");
|
||||||
attentionRecords.value = await api.attentionList();
|
await loadAttention(pagers.attention.page, pagers.attention.pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function attentionPriorityLabel(priority: string) {
|
function attentionPriorityLabel(priority: string) {
|
||||||
@@ -796,7 +841,8 @@ async function clearFeishuCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function searchChats() {
|
async function searchChats() {
|
||||||
chats.value = await api.chats(buildChatQuery());
|
pagers.chats.page = 1;
|
||||||
|
await loadRecordTab("chats");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetChatFilters() {
|
async function resetChatFilters() {
|
||||||
@@ -900,7 +946,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
<template v-if="activeMenu === 'users'">
|
<template v-if="activeMenu === 'users'">
|
||||||
<div class="page-head inline">
|
<div class="page-head inline">
|
||||||
<div><h2>用户管理</h2><p>维护学员名单;只有名单内启用学员可以登录用户端。</p></div>
|
<div><h2>用户管理</h2><p>维护学员名单;只有名单内启用学员可以登录用户端。</p></div>
|
||||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadCurrentMenu" />
|
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadUsers(1)" />
|
||||||
</div>
|
</div>
|
||||||
<section class="student-tools">
|
<section class="student-tools">
|
||||||
<div class="student-tool-panel">
|
<div class="student-tool-panel">
|
||||||
@@ -972,6 +1018,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pagers.users.page" :page-size="pagers.users.pageSize" :total="pagers.users.total" @change="loadUsers" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="activeMenu === 'knowledge'">
|
<template v-if="activeMenu === 'knowledge'">
|
||||||
@@ -1260,6 +1307,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
<el-table-column prop="createdAt" label="时间" width="180" />
|
<el-table-column prop="createdAt" label="时间" width="180" />
|
||||||
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openRetrievalDetail(row)">详情</el-button></template></el-table-column>
|
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openRetrievalDetail(row)">详情</el-button></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pagers.retrievals.page" :page-size="pagers.retrievals.pageSize" :total="pagers.retrievals.total" @change="loadRetrievals" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="activeMenu === 'attention'">
|
<template v-if="activeMenu === 'attention'">
|
||||||
@@ -1274,6 +1322,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
<el-table-column prop="createdAt" label="创建时间" width="180" />
|
||||||
<el-table-column label="操作" width="130" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openChatSession(row.sessionId)">原会话</el-button><el-button link type="danger" @click="deleteAttentionRecord(row)">删除</el-button></template></el-table-column>
|
<el-table-column label="操作" width="130" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openChatSession(row.sessionId)">原会话</el-button><el-button link type="danger" @click="deleteAttentionRecord(row)">删除</el-button></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pagers.attention.page" :page-size="pagers.attention.pageSize" :total="pagers.attention.total" @change="loadAttention" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="activeMenu === 'records'">
|
<template v-if="activeMenu === 'records'">
|
||||||
@@ -1307,7 +1356,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
<el-button @click="exportChats">导出</el-button>
|
<el-button @click="exportChats">导出</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="chats" stripe>
|
<el-table v-loading="loading" :data="chats" stripe>
|
||||||
<el-table-column prop="id" label="会话ID" width="90" />
|
<el-table-column prop="id" label="会话ID" width="90" />
|
||||||
<el-table-column prop="userPhone" label="手机号" width="140" />
|
<el-table-column prop="userPhone" label="手机号" width="140" />
|
||||||
<el-table-column prop="userName" label="用户" width="120" />
|
<el-table-column prop="userName" label="用户" width="120" />
|
||||||
@@ -1320,9 +1369,10 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pagers.chats.page" :page-size="pagers.chats.pageSize" :total="pagers.chats.total" @change="(page, pageSize) => changeRecordPage('chats', page, pageSize)" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="AI 请求" name="aiLogs">
|
<el-tab-pane label="AI 请求" name="aiLogs">
|
||||||
<el-table :data="aiLogs" stripe>
|
<el-table v-loading="loading" :data="aiLogs" stripe>
|
||||||
<el-table-column prop="sessionId" label="会话ID" width="90" />
|
<el-table-column prop="sessionId" label="会话ID" width="90" />
|
||||||
<el-table-column prop="modelName" label="模型" width="160" />
|
<el-table-column prop="modelName" label="模型" width="160" />
|
||||||
<el-table-column prop="knowledgeIds" label="知识库" width="120" />
|
<el-table-column prop="knowledgeIds" label="知识库" width="120" />
|
||||||
@@ -1342,14 +1392,16 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pagers.aiLogs.page" :page-size="pagers.aiLogs.pageSize" :total="pagers.aiLogs.total" @change="(page, pageSize) => changeRecordPage('aiLogs', page, pageSize)" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="操作日志" name="operationLogs">
|
<el-tab-pane label="操作日志" name="operationLogs">
|
||||||
<el-table :data="operationLogs" stripe>
|
<el-table v-loading="loading" :data="operationLogs" stripe>
|
||||||
<el-table-column prop="module" label="模块" />
|
<el-table-column prop="module" label="模块" />
|
||||||
<el-table-column prop="action" label="动作" />
|
<el-table-column prop="action" label="动作" />
|
||||||
<el-table-column prop="result" label="结果" />
|
<el-table-column prop="result" label="结果" />
|
||||||
<el-table-column prop="createdAt" label="时间" />
|
<el-table-column prop="createdAt" label="时间" />
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pagers.operationLogs.page" :page-size="pagers.operationLogs.pageSize" :total="pagers.operationLogs.total" @change="(page, pageSize) => changeRecordPage('operationLogs', page, pageSize)" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{ page: number; pageSize: number; total: number }>();
|
||||||
|
const emit = defineEmits<{ change: [page: number, pageSize: number] }>();
|
||||||
|
|
||||||
|
function changePage(page: number, pageSize: number) { emit("change", page, pageSize); }
|
||||||
|
function changeSize(pageSize: number) { emit("change", 1, pageSize); }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="admin-pagination">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
:current-page="page"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="changePage($event, pageSize)"
|
||||||
|
@size-change="changeSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
|
||||||
import { api } from "../services/api";
|
import { api } from "../services/api";
|
||||||
import TableRowActions from "./TableRowActions.vue";
|
import TableRowActions from "./TableRowActions.vue";
|
||||||
|
import AdminPagination from "./AdminPagination.vue";
|
||||||
import type {
|
import type {
|
||||||
KnowledgeDetail,
|
KnowledgeDetail,
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
@@ -34,6 +35,7 @@ const resolvingNode = ref(false);
|
|||||||
const nodeResolved = ref(false);
|
const nodeResolved = ref(false);
|
||||||
const sourceTitle = ref("");
|
const sourceTitle = ref("");
|
||||||
const filter = reactive({ keyword: "", type: "", open: "", source: "", pending: "" });
|
const filter = reactive({ keyword: "", type: "", open: "", source: "", pending: "" });
|
||||||
|
const pager = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
feishuSpaceId: "",
|
feishuSpaceId: "",
|
||||||
@@ -46,22 +48,19 @@ const form = reactive({
|
|||||||
const editForm = reactive({ id: 0, name: "", knowledgeType: "general", reviewMode: "manual" });
|
const editForm = reactive({ id: 0, name: "", knowledgeType: "general", reviewMode: "manual" });
|
||||||
const batchForm = reactive({ knowledgeType: "", reviewMode: "" });
|
const batchForm = reactive({ knowledgeType: "", reviewMode: "" });
|
||||||
|
|
||||||
const filteredRows = computed(() => rows.value.filter((item) => {
|
|
||||||
if (filter.keyword && !`${item.name}${item.sourceTitle || ""}`.includes(filter.keyword)) return false;
|
|
||||||
if (filter.type && item.knowledgeType !== filter.type) return false;
|
|
||||||
if (filter.open && String(item.status) !== filter.open) return false;
|
|
||||||
if (filter.source && item.sourceStatus !== filter.source) return false;
|
|
||||||
if (filter.pending === "yes" && !item.pendingVersionId) return false;
|
|
||||||
if (filter.pending === "no" && item.pendingVersionId) return false;
|
|
||||||
return true;
|
|
||||||
}));
|
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
|
|
||||||
async function load() {
|
async function load(page = pager.page, pageSize = pager.pageSize) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
[rows.value, migration.value] = await Promise.all([api.knowledge(), api.migrationSummary()]);
|
const result = await api.knowledge({
|
||||||
|
keyword: filter.keyword, knowledgeType: filter.type, openStatus: filter.open,
|
||||||
|
sourceStatus: filter.source, pending: filter.pending === "" ? undefined : filter.pending === "yes",
|
||||||
|
page, pageSize,
|
||||||
|
});
|
||||||
|
rows.value = result.items;
|
||||||
|
Object.assign(pager, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||||
|
if (!migration.value) migration.value = await api.migrationSummary();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(errorMessage(error, "知识库列表加载失败"));
|
ElMessage.error(errorMessage(error, "知识库列表加载失败"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -429,11 +428,11 @@ async function confirmAction(
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="knowledge-filters">
|
<div class="knowledge-filters">
|
||||||
<el-input v-model="filter.keyword" placeholder="搜索后台名称或飞书标题" clearable />
|
<el-input v-model="filter.keyword" placeholder="搜索后台名称或飞书标题" clearable @change="load(1)" />
|
||||||
<el-select v-model="filter.type" placeholder="知识库类型" clearable><el-option label="课程" value="course" /><el-option label="答疑" value="qa" /><el-option label="通用" value="general" /></el-select>
|
<el-select v-model="filter.type" placeholder="知识库类型" clearable @change="load(1)"><el-option label="课程" value="course" /><el-option label="答疑" value="qa" /><el-option label="通用" value="general" /></el-select>
|
||||||
<el-select v-model="filter.open" placeholder="开放状态" clearable><el-option label="已开放" value="1" /><el-option label="已关闭" value="0" /></el-select>
|
<el-select v-model="filter.open" placeholder="开放状态" clearable @change="load(1)"><el-option label="已开放" value="1" /><el-option label="已关闭" value="0" /></el-select>
|
||||||
<el-select v-model="filter.source" placeholder="源状态" clearable><el-option label="正常" value="normal" /><el-option label="源异常" value="error" /></el-select>
|
<el-select v-model="filter.source" placeholder="源状态" clearable @change="load(1)"><el-option label="正常" value="normal" /><el-option label="源异常" value="error" /></el-select>
|
||||||
<el-select v-model="filter.pending" placeholder="待发布版本" clearable><el-option label="有待发布版本" value="yes" /><el-option label="无待发布版本" value="no" /></el-select>
|
<el-select v-model="filter.pending" placeholder="待发布版本" clearable @change="load(1)"><el-option label="有待发布版本" value="yes" /><el-option label="无待发布版本" value="no" /></el-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="knowledge-batch-bar">
|
<div class="knowledge-batch-bar">
|
||||||
@@ -441,7 +440,7 @@ async function confirmAction(
|
|||||||
<el-button :disabled="selectedRows.length === 0" @click="openBatchEdit">批量修改类型/审核方式</el-button>
|
<el-button :disabled="selectedRows.length === 0" @click="openBatchEdit">批量修改类型/审核方式</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="filteredRows" stripe @selection-change="selectedRows = $event">
|
<el-table v-loading="loading" :data="rows" stripe @selection-change="selectedRows = $event">
|
||||||
<el-table-column type="selection" width="48" />
|
<el-table-column type="selection" width="48" />
|
||||||
<el-table-column label="知识库" min-width="240"><template #default="{ row }"><strong>{{ row.name }}</strong><small class="table-subline">{{ row.sourceTitle || "尚未同步源标题" }}</small></template></el-table-column>
|
<el-table-column label="知识库" min-width="240"><template #default="{ row }"><strong>{{ row.name }}</strong><small class="table-subline">{{ row.sourceTitle || "尚未同步源标题" }}</small></template></el-table-column>
|
||||||
<el-table-column label="类型/审核" width="150"><template #default="{ row }">{{ typeLabel(row.knowledgeType) }}<small class="table-subline">{{ row.reviewMode === 'manual' ? '人工审核' : '允许自动发布' }}</small></template></el-table-column>
|
<el-table-column label="类型/审核" width="150"><template #default="{ row }">{{ typeLabel(row.knowledgeType) }}<small class="table-subline">{{ row.reviewMode === 'manual' ? '人工审核' : '允许自动发布' }}</small></template></el-table-column>
|
||||||
@@ -450,6 +449,7 @@ async function confirmAction(
|
|||||||
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
<el-table-column prop="lastSyncedAt" label="最近同步" width="180" />
|
||||||
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row, 'review')">同步</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item command="versions">查看版本</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" :disabled="row.lifecycleStatus !== 'archived'" divided>删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
<el-table-column label="操作" width="220" fixed="right"><template #default="{ row }"><TableRowActions><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :disabled="row.lifecycleStatus === 'archived'" :loading="operatingId === row.id" @click="sync(row, 'review')">同步</el-button><el-dropdown trigger="click" :teleported="true" @command="handleMore($event, row)"><el-button link type="primary">更多</el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="detail">查看详情</el-dropdown-item><el-dropdown-item command="preview">进入 Agent 预览</el-dropdown-item><el-dropdown-item command="versions">查看版本</el-dropdown-item><el-dropdown-item v-if="row.lifecycleStatus === 'active'" command="archive" divided>归档</el-dropdown-item><el-dropdown-item v-else command="restore" divided>恢复</el-dropdown-item><el-dropdown-item command="delete" :disabled="row.lifecycleStatus !== 'archived'" divided>删除</el-dropdown-item></el-dropdown-menu></template></el-dropdown></TableRowActions></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<AdminPagination :page="pager.page" :page-size="pager.pageSize" :total="pager.total" @change="load" />
|
||||||
|
|
||||||
<el-drawer v-model="detailOpen" size="76%" title="知识库详情" destroy-on-close>
|
<el-drawer v-model="detailOpen" size="76%" title="知识库详情" destroy-on-close>
|
||||||
<template v-if="detail">
|
<template v-if="detail">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ElInput,
|
ElInput,
|
||||||
ElInputNumber,
|
ElInputNumber,
|
||||||
ElOption,
|
ElOption,
|
||||||
|
ElPagination,
|
||||||
ElProgress,
|
ElProgress,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSwitch,
|
ElSwitch,
|
||||||
@@ -44,6 +45,7 @@ import "element-plus/theme-chalk/el-message.css";
|
|||||||
import "element-plus/theme-chalk/el-message-box.css";
|
import "element-plus/theme-chalk/el-message-box.css";
|
||||||
import "element-plus/theme-chalk/el-option.css";
|
import "element-plus/theme-chalk/el-option.css";
|
||||||
import "element-plus/theme-chalk/el-overlay.css";
|
import "element-plus/theme-chalk/el-overlay.css";
|
||||||
|
import "element-plus/theme-chalk/el-pagination.css";
|
||||||
import "element-plus/theme-chalk/el-progress.css";
|
import "element-plus/theme-chalk/el-progress.css";
|
||||||
import "element-plus/theme-chalk/el-popper.css";
|
import "element-plus/theme-chalk/el-popper.css";
|
||||||
import "element-plus/theme-chalk/el-scrollbar.css";
|
import "element-plus/theme-chalk/el-scrollbar.css";
|
||||||
@@ -79,6 +81,7 @@ app.config.globalProperties.$ELEMENT = { locale: zhCn };
|
|||||||
ElInput,
|
ElInput,
|
||||||
ElInputNumber,
|
ElInputNumber,
|
||||||
ElOption,
|
ElOption,
|
||||||
|
ElPagination,
|
||||||
ElProgress,
|
ElProgress,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSwitch,
|
ElSwitch,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type {
|
|||||||
ModelItem,
|
ModelItem,
|
||||||
SystemConfigItem,
|
SystemConfigItem,
|
||||||
UserImportResult,
|
UserImportResult,
|
||||||
|
PageResult,
|
||||||
} from "../types/api";
|
} from "../types/api";
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "/api";
|
||||||
@@ -101,7 +102,7 @@ export const api = {
|
|||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return request<DashboardStats>(`/admin/dashboard${qs ? "?" + qs : ""}`);
|
return request<DashboardStats>(`/admin/dashboard${qs ? "?" + qs : ""}`);
|
||||||
},
|
},
|
||||||
users: (keyword = "") => request<AdminUser[]>(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`),
|
users: (query: { keyword?: string; page?: number; pageSize?: number } = {}) => request<PageResult<AdminUser>>(`/admin/user/list${queryString(query)}`),
|
||||||
createUser: (payload: Record<string, unknown>) =>
|
createUser: (payload: Record<string, unknown>) =>
|
||||||
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
|
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
|
||||||
importUsers: (students: Record<string, unknown>[]) =>
|
importUsers: (students: Record<string, unknown>[]) =>
|
||||||
@@ -115,7 +116,8 @@ export const api = {
|
|||||||
updateUser: (id: number, payload: Record<string, unknown>) =>
|
updateUser: (id: number, payload: Record<string, unknown>) =>
|
||||||
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||||
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
||||||
knowledge: () => request<KnowledgeItem[]>("/admin/knowledge/list"),
|
knowledge: (query: Record<string, unknown> = {}) => request<PageResult<KnowledgeItem>>(`/admin/knowledge/list${queryString(query)}`),
|
||||||
|
knowledgeOptions: () => request<KnowledgeItem[]>("/admin/knowledge/options"),
|
||||||
resolveKnowledgeNode: (nodeId: string) => request<{ nodeId: string; spaceId: string; sourceTitle: string; name: string; remark: string }>("/admin/knowledge/resolve-node", { method: "POST", body: JSON.stringify({ nodeId }) }),
|
resolveKnowledgeNode: (nodeId: string) => request<{ nodeId: string; spaceId: string; sourceTitle: string; name: string; remark: string }>("/admin/knowledge/resolve-node", { method: "POST", body: JSON.stringify({ nodeId }) }),
|
||||||
createKnowledge: (payload: Record<string, unknown>) =>
|
createKnowledge: (payload: Record<string, unknown>) =>
|
||||||
request<KnowledgeItem>("/admin/knowledge", { method: "POST", body: JSON.stringify(payload) }),
|
request<KnowledgeItem>("/admin/knowledge", { method: "POST", body: JSON.stringify(payload) }),
|
||||||
@@ -175,21 +177,21 @@ export const api = {
|
|||||||
configs: () => request<SystemConfigItem[]>("/admin/config"),
|
configs: () => request<SystemConfigItem[]>("/admin/config"),
|
||||||
saveConfig: (payload: Record<string, unknown>) =>
|
saveConfig: (payload: Record<string, unknown>) =>
|
||||||
request<SystemConfigItem>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }),
|
request<SystemConfigItem>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }),
|
||||||
chats: (query: ChatRecordQuery = {}) => request<ChatRecord[]>(`/admin/chat/list${queryString(query)}`),
|
chats: (query: ChatRecordQuery = {}) => request<PageResult<ChatRecord>>(`/admin/chat/list${queryString(query)}`),
|
||||||
chatDetail: (sessionId: number) => request<ChatDetail>(`/admin/chat/${sessionId}`),
|
chatDetail: (sessionId: number) => request<ChatDetail>(`/admin/chat/${sessionId}`),
|
||||||
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
|
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
|
||||||
aiLogs: (query: { sessionId?: number; userId?: number; status?: string } = {}) =>
|
aiLogs: (query: { sessionId?: number; userId?: number; status?: string; page?: number; pageSize?: number } = {}) =>
|
||||||
request<AiLogRecord[]>(`/admin/ai-log/list${queryString(query)}`),
|
request<PageResult<AiLogRecord>>(`/admin/ai-log/list${queryString(query)}`),
|
||||||
aiLogDetail: (id: number) => request<AiLogRecord>(`/admin/ai-log/${id}`),
|
aiLogDetail: (id: number) => request<AiLogRecord>(`/admin/ai-log/${id}`),
|
||||||
operationLogs: () => request<Record<string, unknown>[]>("/admin/log/list"),
|
operationLogs: (query: { module?: string; page?: number; pageSize?: number } = {}) => request<PageResult<Record<string, unknown>>>(`/admin/log/list${queryString(query)}`),
|
||||||
retrievalLogs: () => request<RetrievalLogItem[]>("/admin/retrieval-log/list"),
|
retrievalLogs: (query: { page?: number; pageSize?: number } = {}) => request<PageResult<RetrievalLogItem>>(`/admin/retrieval-log/list${queryString(query)}`),
|
||||||
estimateRetrievalCleanup: (before: string) => request<{ before: string; estimatedCount: number }>("/admin/retrieval-log/cleanup/estimate", { method: "POST", body: JSON.stringify({ before }) }),
|
estimateRetrievalCleanup: (before: string) => request<{ before: string; estimatedCount: number }>("/admin/retrieval-log/cleanup/estimate", { method: "POST", body: JSON.stringify({ before }) }),
|
||||||
cleanupRetrievalLogs: (before: string) => request<{ before: string; deleted: number }>("/admin/retrieval-log/cleanup", { method: "POST", body: JSON.stringify({ before }) }),
|
cleanupRetrievalLogs: (before: string) => request<{ before: string; deleted: number }>("/admin/retrieval-log/cleanup", { method: "POST", body: JSON.stringify({ before }) }),
|
||||||
retrievalRetention: () => request<{ enabled: boolean; retentionDays: number | null; lastRunAt: string | null; lastResult: string | null }>("/admin/retrieval-log/retention"),
|
retrievalRetention: () => request<{ enabled: boolean; retentionDays: number | null; lastRunAt: string | null; lastResult: string | null }>("/admin/retrieval-log/retention"),
|
||||||
saveRetrievalRetention: (retentionDays: number | null) => request<{ enabled: boolean; retentionDays: number | null }>("/admin/retrieval-log/retention", { method: "PUT", body: JSON.stringify({ retentionDays }) }),
|
saveRetrievalRetention: (retentionDays: number | null) => request<{ enabled: boolean; retentionDays: number | null }>("/admin/retrieval-log/retention", { method: "PUT", body: JSON.stringify({ retentionDays }) }),
|
||||||
storageStats: (refresh = false) => request<Record<string, any>>(`/admin/dashboard/storage${refresh ? "?refresh=true" : ""}`),
|
storageStats: (refresh = false) => request<Record<string, any>>(`/admin/dashboard/storage${refresh ? "?refresh=true" : ""}`),
|
||||||
retrievalLogDetail: (id: number) => request<Record<string, unknown>>(`/admin/retrieval-log/${id}`),
|
retrievalLogDetail: (id: number) => request<Record<string, unknown>>(`/admin/retrieval-log/${id}`),
|
||||||
attentionList: () => request<AttentionItem[]>("/admin/attention/list"),
|
attentionList: (query: { page?: number; pageSize?: number } = {}) => request<PageResult<AttentionItem>>(`/admin/attention/list${queryString(query)}`),
|
||||||
updateAttention: (id: number, status: string, note?: string) =>
|
updateAttention: (id: number, status: string, note?: string) =>
|
||||||
request<AttentionItem>(`/admin/attention/${id}`, { method: "PUT", body: JSON.stringify({ status, note }) }),
|
request<AttentionItem>(`/admin/attention/${id}`, { method: "PUT", body: JSON.stringify({ status, note }) }),
|
||||||
deleteAttention: (id: number) => request<null>(`/admin/attention/${id}`, { method: "DELETE" }),
|
deleteAttention: (id: number) => request<null>(`/admin/attention/${id}`, { method: "DELETE" }),
|
||||||
|
|||||||
@@ -726,6 +726,11 @@ textarea {
|
|||||||
.table-row-actions { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; white-space: nowrap; }
|
.table-row-actions { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; white-space: nowrap; }
|
||||||
.table-row-actions .el-button + .el-button { margin-left: 0; }
|
.table-row-actions .el-button + .el-button { margin-left: 0; }
|
||||||
.table-row-actions .el-button { min-width: 48px; height: 32px; white-space: nowrap; }
|
.table-row-actions .el-button { min-width: 48px; height: 32px; white-space: nowrap; }
|
||||||
|
.admin-pagination { display: flex; justify-content: flex-end; align-items: center; min-height: 48px; padding: 14px 0 2px; overflow-x: auto; }
|
||||||
|
.admin-pagination .el-pagination { flex-wrap: nowrap; white-space: nowrap; }
|
||||||
|
.admin-pagination .el-pager li,
|
||||||
|
.admin-pagination .btn-prev,
|
||||||
|
.admin-pagination .btn-next { min-width: 32px; height: 32px; border-radius: 6px; }
|
||||||
.tool-trace-node { border: 1px solid #dfe8e5; border-radius: 8px; background: #fff; overflow: hidden; }
|
.tool-trace-node { border: 1px solid #dfe8e5; border-radius: 8px; background: #fff; overflow: hidden; }
|
||||||
.tool-trace-node summary { cursor: pointer; display: flex; align-items: center; gap: 10px; padding: 12px; list-style: none; }
|
.tool-trace-node summary { cursor: pointer; display: flex; align-items: center; gap: 10px; padding: 12px; list-style: none; }
|
||||||
.tool-trace-node summary span { margin-left: auto; color: #667a73; font-size: 12px; }
|
.tool-trace-node summary span { margin-left: auto; color: #667a73; font-size: 12px; }
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ export interface ApiResponse<T> {
|
|||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageResult<T> {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminProfile {
|
export interface AdminProfile {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -175,7 +182,6 @@ export interface KnowledgeSyncJob {
|
|||||||
export interface MigrationSummary {
|
export interface MigrationSummary {
|
||||||
total: number;
|
total: number;
|
||||||
counts: Record<string, number>;
|
counts: Record<string, number>;
|
||||||
items: KnowledgeItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeBatchUpdateResult {
|
export interface KnowledgeBatchUpdateResult {
|
||||||
@@ -278,4 +284,6 @@ export interface ChatRecordQuery {
|
|||||||
status?: string;
|
status?: string;
|
||||||
dateFrom?: string;
|
dateFrom?: string;
|
||||||
dateTo?: string;
|
dateTo?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.schemas.knowledge import AttentionUpdateRequest
|
|||||||
from app.services.admin_service import OperationLogService
|
from app.services.admin_service import OperationLogService
|
||||||
from app.models.logs import LogRetentionPolicy
|
from app.models.logs import LogRetentionPolicy
|
||||||
from app.services.maintenance_service import MaintenanceService
|
from app.services.maintenance_service import MaintenanceService
|
||||||
|
from app.api.pagination import page_result
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -48,8 +49,9 @@ def retrieval_logs(
|
|||||||
query = query.where(KnowledgeRetrievalLog.status == statusValue)
|
query = query.where(KnowledgeRetrievalLog.status == statusValue)
|
||||||
if attentionCreated is not None:
|
if attentionCreated is not None:
|
||||||
query = query.where(KnowledgeRetrievalLog.attention_created == (1 if attentionCreated else 0))
|
query = query.where(KnowledgeRetrievalLog.attention_created == (1 if attentionCreated else 0))
|
||||||
query = query.offset((page - 1) * pageSize).limit(pageSize)
|
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||||
return api_success([_retrieval_dict(item, detail=False) for item in db.scalars(query).all()])
|
rows = db.scalars(query.offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||||
|
return api_success(page_result([_retrieval_dict(item, detail=False) for item in rows], total=total, page=page, page_size=pageSize))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/retrieval-log/cleanup/estimate")
|
@router.post("/retrieval-log/cleanup/estimate")
|
||||||
@@ -133,15 +135,19 @@ def retrieval_detail(
|
|||||||
def attention_list(
|
def attention_list(
|
||||||
priority: str = Query(default=""),
|
priority: str = Query(default=""),
|
||||||
statusValue: str = Query(default="", alias="status"),
|
statusValue: str = Query(default="", alias="status"),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=20, ge=10, le=100),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
query = select(HumanAttentionRecord).order_by(HumanAttentionRecord.id.desc()).limit(300)
|
query = select(HumanAttentionRecord)
|
||||||
if priority:
|
if priority:
|
||||||
query = query.where(HumanAttentionRecord.priority == priority)
|
query = query.where(HumanAttentionRecord.priority == priority)
|
||||||
if statusValue:
|
if statusValue:
|
||||||
query = query.where(HumanAttentionRecord.status == statusValue)
|
query = query.where(HumanAttentionRecord.status == statusValue)
|
||||||
return api_success([_attention_dict(item) for item in db.scalars(query).all()])
|
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
|
||||||
|
rows = db.scalars(query.order_by(HumanAttentionRecord.id.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||||
|
return api_success(page_result([_attention_dict(item) for item in rows], total=total, page=page, page_size=pageSize))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/attention/{attention_id}")
|
@router.put("/attention/{attention_id}")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -26,13 +26,42 @@ from app.schemas.admin import KnowledgeSaveRequest
|
|||||||
from app.services.admin_service import OperationLogService
|
from app.services.admin_service import OperationLogService
|
||||||
from app.services.feishu_service import FeishuKnowledgeService
|
from app.services.feishu_service import FeishuKnowledgeService
|
||||||
from app.services.knowledge_catalog_cache_service import KnowledgeCatalogCacheService
|
from app.services.knowledge_catalog_cache_service import KnowledgeCatalogCacheService
|
||||||
|
from app.api.pagination import page_result
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/knowledge/list")
|
@router.get("/knowledge/list")
|
||||||
def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
def list_knowledge(
|
||||||
items = db.scalars(select(Knowledge).order_by(Knowledge.id.desc())).all()
|
keyword: str = Query(default=""),
|
||||||
|
knowledgeType: str = Query(default=""),
|
||||||
|
openStatus: int | None = Query(default=None),
|
||||||
|
sourceStatus: str = Query(default=""),
|
||||||
|
pending: bool | None = Query(default=None),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=20, ge=10, le=100),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
|
) -> dict:
|
||||||
|
query = select(Knowledge)
|
||||||
|
if keyword:
|
||||||
|
query = query.where((Knowledge.name.like(f"%{keyword}%")) | (Knowledge.source_title.like(f"%{keyword}%")))
|
||||||
|
if knowledgeType:
|
||||||
|
query = query.where(Knowledge.knowledge_type == knowledgeType)
|
||||||
|
if openStatus is not None:
|
||||||
|
query = query.where(Knowledge.status == openStatus)
|
||||||
|
if sourceStatus:
|
||||||
|
query = query.where(Knowledge.source_status == sourceStatus)
|
||||||
|
if pending is not None:
|
||||||
|
query = query.where(Knowledge.pending_version_id.is_not(None) if pending else Knowledge.pending_version_id.is_(None))
|
||||||
|
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
|
||||||
|
items = db.scalars(query.order_by(Knowledge.id.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||||
|
return api_success(page_result([_knowledge_dict(item) for item in items], total=total, page=page, page_size=pageSize))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/knowledge/options")
|
||||||
|
def knowledge_options(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||||
|
items = db.scalars(select(Knowledge).where(Knowledge.lifecycle_status == "active").order_by(Knowledge.id.desc())).all()
|
||||||
return api_success([_knowledge_dict(item) for item in items])
|
return api_success([_knowledge_dict(item) for item in items])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -367,11 +367,11 @@ def migration_summary(
|
|||||||
db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)
|
db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)
|
||||||
) -> dict:
|
) -> dict:
|
||||||
_require_super_admin(current_admin)
|
_require_super_admin(current_admin)
|
||||||
rows = db.scalars(select(Knowledge).order_by(Knowledge.id)).all()
|
rows = db.execute(
|
||||||
counts: dict[str, int] = {}
|
select(Knowledge.migration_status, func.count(Knowledge.id)).group_by(Knowledge.migration_status)
|
||||||
for item in rows:
|
).all()
|
||||||
counts[item.migration_status] = counts.get(item.migration_status, 0) + 1
|
counts = {migration_status: count for migration_status, count in rows}
|
||||||
return api_success({"total": len(rows), "counts": counts, "items": [_knowledge_detail(item) for item in rows]})
|
return api_success({"total": sum(counts.values()), "counts": counts})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/knowledge-migration/run")
|
@router.post("/knowledge-migration/run")
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
from sqlalchemy import exists, or_, select
|
from sqlalchemy import exists, func, or_, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, load_only
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.dependencies import get_current_admin
|
from app.core.dependencies import get_current_admin
|
||||||
@@ -18,6 +18,7 @@ from app.models.admin import Admin
|
|||||||
from app.models.chat import ChatMessage, ChatSession
|
from app.models.chat import ChatMessage, ChatSession
|
||||||
from app.models.logs import AiRequestLog, OperationLog
|
from app.models.logs import AiRequestLog, OperationLog
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.api.pagination import page_result
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -29,15 +30,15 @@ def chat_list(
|
|||||||
status: str = Query(default=""),
|
status: str = Query(default=""),
|
||||||
dateFrom: datetime | None = Query(default=None),
|
dateFrom: datetime | None = Query(default=None),
|
||||||
dateTo: datetime | None = Query(default=None),
|
dateTo: datetime | None = Query(default=None),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=20, ge=10, le=100),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
rows = db.execute(
|
query = _chat_query(keyword=keyword, user_id=userId, status=status, date_from=dateFrom, date_to=dateTo)
|
||||||
_chat_query(keyword=keyword, user_id=userId, status=status, date_from=dateFrom, date_to=dateTo)
|
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||||
.order_by(ChatSession.updated_at.desc())
|
rows = db.execute(query.order_by(ChatSession.updated_at.desc()).offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||||
.limit(200)
|
return api_success(page_result([_chat_row_dict(session, user) for session, user in rows], total=total, page=page, page_size=pageSize))
|
||||||
).all()
|
|
||||||
return api_success([_chat_row_dict(session, user) for session, user in rows])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/chat/export")
|
@router.get("/chat/export")
|
||||||
@@ -118,14 +119,20 @@ def chat_detail(
|
|||||||
@router.get("/log/list")
|
@router.get("/log/list")
|
||||||
def operation_logs(
|
def operation_logs(
|
||||||
module: str = Query(default=""),
|
module: str = Query(default=""),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=20, ge=10, le=100),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
query = select(OperationLog).order_by(OperationLog.created_at.desc()).limit(200)
|
query = select(OperationLog)
|
||||||
if module:
|
if module:
|
||||||
query = query.where(OperationLog.module == module)
|
query = query.where(OperationLog.module == module)
|
||||||
logs = db.scalars(query).all()
|
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
|
||||||
return api_success(
|
logs = db.scalars(
|
||||||
|
query.options(load_only(OperationLog.id, OperationLog.admin_id, OperationLog.module, OperationLog.action, OperationLog.target_id, OperationLog.result, OperationLog.created_at))
|
||||||
|
.order_by(OperationLog.created_at.desc()).offset((page - 1) * pageSize).limit(pageSize)
|
||||||
|
).all()
|
||||||
|
return api_success(page_result(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": item.id,
|
"id": item.id,
|
||||||
@@ -137,8 +144,8 @@ def operation_logs(
|
|||||||
"createdAt": item.created_at,
|
"createdAt": item.created_at,
|
||||||
}
|
}
|
||||||
for item in logs
|
for item in logs
|
||||||
]
|
], total=total, page=page, page_size=pageSize
|
||||||
)
|
))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ai-log/list")
|
@router.get("/ai-log/list")
|
||||||
@@ -146,18 +153,28 @@ def ai_logs(
|
|||||||
sessionId: int | None = Query(default=None),
|
sessionId: int | None = Query(default=None),
|
||||||
userId: int | None = Query(default=None),
|
userId: int | None = Query(default=None),
|
||||||
status: str = Query(default=""),
|
status: str = Query(default=""),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=20, ge=10, le=100),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
query = select(AiRequestLog).order_by(AiRequestLog.created_at.desc()).limit(200)
|
query = select(AiRequestLog)
|
||||||
if sessionId is not None:
|
if sessionId is not None:
|
||||||
query = query.where(AiRequestLog.session_id == sessionId)
|
query = query.where(AiRequestLog.session_id == sessionId)
|
||||||
if userId is not None:
|
if userId is not None:
|
||||||
query = query.where(AiRequestLog.user_id == userId)
|
query = query.where(AiRequestLog.user_id == userId)
|
||||||
if status:
|
if status:
|
||||||
query = query.where(AiRequestLog.status == status)
|
query = query.where(AiRequestLog.status == status)
|
||||||
logs = db.scalars(query).all()
|
total = db.scalar(select(func.count()).select_from(query.subquery())) or 0
|
||||||
return api_success([_ai_log_dict(item) for item in logs])
|
logs = db.scalars(
|
||||||
|
query.options(load_only(
|
||||||
|
AiRequestLog.id, AiRequestLog.session_id, AiRequestLog.message_id, AiRequestLog.user_id,
|
||||||
|
AiRequestLog.model_name, AiRequestLog.knowledge_ids, AiRequestLog.retrieve_count,
|
||||||
|
AiRequestLog.input_token, AiRequestLog.output_token, AiRequestLog.total_token,
|
||||||
|
AiRequestLog.cost_ms, AiRequestLog.status, AiRequestLog.error_message, AiRequestLog.created_at,
|
||||||
|
)).order_by(AiRequestLog.created_at.desc()).offset((page - 1) * pageSize).limit(pageSize)
|
||||||
|
).all()
|
||||||
|
return api_success(page_result([_ai_log_dict(item, include_chunks=False) for item in logs], total=total, page=page, page_size=pageSize))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ai-log/{log_id}")
|
@router.get("/ai-log/{log_id}")
|
||||||
@@ -241,7 +258,7 @@ def _message_dict(message: ChatMessage) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False) -> dict:
|
def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False, include_chunks: bool = True) -> dict:
|
||||||
data = {
|
data = {
|
||||||
"id": log.id,
|
"id": log.id,
|
||||||
"sessionId": log.session_id,
|
"sessionId": log.session_id,
|
||||||
@@ -250,7 +267,7 @@ def _ai_log_dict(log: AiRequestLog, *, include_prompt: bool = False) -> dict:
|
|||||||
"modelName": log.model_name,
|
"modelName": log.model_name,
|
||||||
"knowledgeIds": log.knowledge_ids,
|
"knowledgeIds": log.knowledge_ids,
|
||||||
"retrieveCount": log.retrieve_count,
|
"retrieveCount": log.retrieve_count,
|
||||||
"retrievedChunks": _parse_retrieved_chunks(log.retrieved_chunks),
|
"retrievedChunks": _parse_retrieved_chunks(log.retrieved_chunks) if include_chunks else [],
|
||||||
"inputToken": log.input_token,
|
"inputToken": log.input_token,
|
||||||
"outputToken": log.output_token,
|
"outputToken": log.output_token,
|
||||||
"totalToken": log.total_token,
|
"totalToken": log.total_token,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile,
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from openpyxl import Workbook, load_workbook
|
from openpyxl import Workbook, load_workbook
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
@@ -19,6 +19,7 @@ from app.models.ai_config import SystemConfig
|
|||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportItem, AdminUserImportRequest, AdminUserUpdateRequest
|
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportItem, AdminUserImportRequest, AdminUserUpdateRequest
|
||||||
from app.services.admin_service import OperationLogService
|
from app.services.admin_service import OperationLogService
|
||||||
|
from app.api.pagination import page_result
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ STUDENT_TEMPLATE_HEADERS = ["手机号", "姓名", "昵称", "每日聊天额度
|
|||||||
@router.get("/user/list")
|
@router.get("/user/list")
|
||||||
def list_users(
|
def list_users(
|
||||||
keyword: str = Query(default=""),
|
keyword: str = Query(default=""),
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
pageSize: int = Query(default=20, ge=10, le=100),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: Admin = Depends(get_current_admin),
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -35,8 +38,9 @@ def list_users(
|
|||||||
if keyword:
|
if keyword:
|
||||||
like = f"%{keyword}%"
|
like = f"%{keyword}%"
|
||||||
query = query.where((User.phone.like(like)) | (User.name.like(like)))
|
query = query.where((User.phone.like(like)) | (User.name.like(like)))
|
||||||
users = db.scalars(query.limit(200)).all()
|
total = db.scalar(select(func.count()).select_from(query.order_by(None).subquery())) or 0
|
||||||
return api_success([_user_dict(user) for user in users])
|
users = db.scalars(query.offset((page - 1) * pageSize).limit(pageSize)).all()
|
||||||
|
return api_success(page_result([_user_dict(user) for user in users], total=total, page=page, page_size=pageSize))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/user")
|
@router.post("/user")
|
||||||
|
|||||||
7
ai_knowledge_base_v2/apps/backend/app/api/pagination.py
Normal file
7
ai_knowledge_base_v2/apps/backend/app/api/pagination.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def page_result(items: list[Any], *, total: int, page: int, page_size: int) -> dict[str, Any]:
|
||||||
|
return {"items": items, "total": total, "page": page, "pageSize": page_size}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from app.api.admin_agent_records import attention_list, retrieval_logs
|
||||||
|
from app.api.admin_records import ai_logs
|
||||||
|
from app.api.admin_users import list_users
|
||||||
|
from app.models import Base
|
||||||
|
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
|
||||||
|
from app.models.logs import AiRequestLog
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
def _database() -> Session:
|
||||||
|
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
return Session(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_list_uses_database_pagination():
|
||||||
|
with _database() as db:
|
||||||
|
db.add_all([
|
||||||
|
User(id=i + 1, phone=f"1380000{i:04d}", name=f"学员{i}", daily_chat_limit=10)
|
||||||
|
for i in range(25)
|
||||||
|
])
|
||||||
|
db.commit()
|
||||||
|
response = list_users(keyword="", page=2, pageSize=10, db=db, current_admin=object())
|
||||||
|
assert response["data"]["total"] == 25
|
||||||
|
assert response["data"]["page"] == 2
|
||||||
|
assert len(response["data"]["items"]) == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_ai_log_page_does_not_load_large_detail_fields():
|
||||||
|
with _database() as db:
|
||||||
|
db.add_all([
|
||||||
|
AiRequestLog(status="success", prompt="p" * 5000, retrieved_chunks='[{"content":"large"}]')
|
||||||
|
for _ in range(23)
|
||||||
|
])
|
||||||
|
db.commit()
|
||||||
|
response = ai_logs(sessionId=None, userId=None, status="", page=2, pageSize=10, db=db, current_admin=object())
|
||||||
|
assert response["data"]["total"] == 23
|
||||||
|
assert len(response["data"]["items"]) == 10
|
||||||
|
assert all(item["retrievedChunks"] == [] for item in response["data"]["items"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieval_and_attention_lists_are_paginated():
|
||||||
|
with _database() as db:
|
||||||
|
for index in range(21):
|
||||||
|
db.add(KnowledgeRetrievalLog(
|
||||||
|
question=f"q{index}", catalog_snapshot="[]", catalog_hash=str(index),
|
||||||
|
safety_rule_version="v1", status="success",
|
||||||
|
))
|
||||||
|
db.add(HumanAttentionRecord(
|
||||||
|
session_id=index + 1, message_id=index + 1, user_id=index + 1,
|
||||||
|
trigger_message="test", problem_summary="test", trigger_reason="test",
|
||||||
|
priority="normal", status="pending",
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
retrieval = retrieval_logs(
|
||||||
|
sessionId=None, knowledgeId=None, knowledgeCalled=None, statusValue="", attentionCreated=None,
|
||||||
|
page=3, pageSize=10, db=db, current_admin=object(),
|
||||||
|
)["data"]
|
||||||
|
attention = attention_list(priority="", statusValue="", page=3, pageSize=10, db=db, current_admin=object())["data"]
|
||||||
|
assert retrieval["total"] == attention["total"] == 21
|
||||||
|
assert len(retrieval["items"]) == len(attention["items"]) == 1
|
||||||
Reference in New Issue
Block a user