Implement open knowledge access scope

This commit is contained in:
2026-07-07 18:32:01 +08:00
parent 8d5d36c5eb
commit 03b1810a90
13 changed files with 587 additions and 382 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ElMessage } from "element-plus";
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { computed, onMounted, reactive, ref } from "vue";
import { api, clearToken, getToken, saveToken } from "./services/api";
import type {
@@ -15,15 +15,6 @@ import type {
ModelItem,
} from "./types/api";
type UserKnowledgePermissionRow = {
knowledgeId: number;
name: string;
status: number;
assigned: boolean;
effectiveAt: string;
expiredAt: string;
};
const admin = ref<AdminProfile | null>(null);
const activeMenu = ref("dashboard");
const loading = ref(false);
@@ -44,10 +35,6 @@ const chatDetailOpen = ref(false);
const recordTab = ref("chats");
const editingModelId = ref<number | null>(null);
const editingKnowledgeId = ref<number | null>(null);
const userPermissionOpen = ref(false);
const userPermissionLoading = ref(false);
const permissionUser = ref<AdminUser | null>(null);
const userKnowledgePermissionRows = ref<UserKnowledgePermissionRow[]>([]);
const agentDebugging = ref(false);
const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; content: string }[]>([
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
@@ -111,8 +98,8 @@ const quickModelProviders = [
{
label: "MiniMax",
provider: "minimax",
apiType: "anthropic_messages",
baseUrl: "https://api.minimax.io/anthropic",
apiType: "openai_compatible",
baseUrl: "https://api.minimaxi.com/v1",
authType: "bearer",
apiVersion: "",
temperature: null,
@@ -120,14 +107,8 @@ const quickModelProviders = [
maxToken: null,
contextWindow: null,
models: [
{ label: "MiniMax-Text-01", value: "MiniMax-Text-01" },
{ label: "MiniMax-M3", value: "MiniMax-M3" },
{ label: "MiniMax-M2.7", value: "MiniMax-M2.7" },
{ label: "MiniMax-M2.7-highspeed", value: "MiniMax-M2.7-highspeed" },
{ label: "MiniMax-M2.5", value: "MiniMax-M2.5" },
{ label: "MiniMax-M2.5-highspeed", value: "MiniMax-M2.5-highspeed" },
{ label: "MiniMax-M2.1", value: "MiniMax-M2.1" },
{ label: "MiniMax-M2.1-highspeed", value: "MiniMax-M2.1-highspeed" },
{ label: "MiniMax-M2", value: "MiniMax-M2" },
],
},
] as const;
@@ -177,7 +158,8 @@ const chatFilters = reactive({
keyword: "",
userId: undefined as number | undefined,
status: "",
dateRange: [] as string[],
dateFrom: "",
dateTo: "",
});
const agentForm = reactive({
@@ -266,45 +248,6 @@ async function deleteUser(row: AdminUser) {
await loadCurrentMenu();
}
async function openUserPermissions(row: AdminUser) {
permissionUser.value = row;
userPermissionOpen.value = true;
userPermissionLoading.value = true;
try {
const [knowledgeRows, detail] = await Promise.all([api.knowledge(), api.userKnowledgePermissions(row.id)]);
knowledge.value = knowledgeRows;
const permissionMap = new Map(detail.permissions.map((item) => [item.knowledgeId, item]));
userKnowledgePermissionRows.value = knowledgeRows.map((item) => {
const permission = permissionMap.get(item.id);
return {
knowledgeId: item.id,
name: item.name,
status: item.status,
assigned: Boolean(permission),
effectiveAt: permission?.effectiveAt ?? "",
expiredAt: permission?.expiredAt ?? "",
};
});
} finally {
userPermissionLoading.value = false;
}
}
async function saveUserPermissions() {
if (!permissionUser.value) return;
await api.saveUserKnowledgePermissions(permissionUser.value.id, {
permissions: userKnowledgePermissionRows.value
.filter((item) => item.assigned)
.map((item) => ({
knowledgeId: item.knowledgeId,
effectiveAt: item.effectiveAt || null,
expiredAt: item.expiredAt || null,
})),
});
ElMessage.success("知识库权限已保存");
userPermissionOpen.value = false;
}
async function saveKnowledge() {
if (!knowledgeForm.name || !knowledgeForm.feishuSpaceId || !knowledgeForm.feishuNodeId) {
ElMessage.error("请填写知识库名称、SpaceID 和 NodeID");
@@ -344,7 +287,7 @@ function editKnowledge(row: KnowledgeItem) {
}
async function deleteKnowledge(row: KnowledgeItem) {
if (!window.confirm(`确认删除知识库 ${row.name} 吗?已分配给用户的授权也会失效`)) return;
if (!window.confirm(`确认删除知识库 ${row.name} 吗?删除后所有用户都无法再检索该知识库`)) return;
await api.deleteKnowledge(row.id);
ElMessage.success("知识库已删除");
if (editingKnowledgeId.value === row.id) resetKnowledgeForm();
@@ -404,8 +347,12 @@ function buildModelPayload() {
return payload;
}
const selectedQuickProvider = computed(() => {
return quickModelProviders.find((item) => item.provider === quickModelForm.provider) ?? quickModelProviders[0];
});
async function quickAddModel() {
const provider = selectedQuickProvider();
const provider = selectedQuickProvider.value;
if (!provider) return;
if (!quickModelForm.apiKey) {
ElMessage.error("请填写 API Key");
@@ -440,10 +387,6 @@ async function quickAddModel() {
await loadCurrentMenu();
}
function selectedQuickProvider() {
return quickModelProviders.find((item) => item.provider === quickModelForm.provider) ?? quickModelProviders[0];
}
function applyQuickProvider(provider: string) {
const target = quickModelProviders.find((item) => item.provider === provider);
if (!target) return;
@@ -456,6 +399,21 @@ async function enableModel(id: number) {
await loadCurrentMenu();
}
async function deleteModel(id: number) {
try {
await ElMessageBox.confirm("确认删除该模型?删除后不可恢复。", "删除模型", {
confirmButtonText: "确认删除",
cancelButtonText: "取消",
type: "warning",
});
} catch {
return;
}
await api.deleteModel(id);
ElMessage.success("模型已删除");
await loadCurrentMenu();
}
async function testModel(id: number) {
const result = await api.testModel(id);
if (result.ok) {
@@ -621,7 +579,7 @@ async function searchChats() {
}
async function resetChatFilters() {
Object.assign(chatFilters, { keyword: "", userId: undefined, status: "", dateRange: [] });
Object.assign(chatFilters, { keyword: "", userId: undefined, status: "", dateFrom: "", dateTo: "" });
await searchChats();
}
@@ -639,8 +597,8 @@ function buildChatQuery() {
keyword: chatFilters.keyword,
userId: chatFilters.userId,
status: chatFilters.status,
dateFrom: chatFilters.dateRange?.[0],
dateTo: chatFilters.dateRange?.[1],
dateFrom: chatFilters.dateFrom,
dateTo: chatFilters.dateTo,
};
}
</script>
@@ -717,10 +675,9 @@ function buildChatQuery() {
</el-table-column>
<el-table-column prop="dailyChatUsed" label="已用" width="90" />
<el-table-column prop="lastLoginAt" label="最近登录" width="180" />
<el-table-column label="操作" width="250" fixed="right">
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="saveUser(row)">保存</el-button>
<el-button size="small" type="primary" @click="openUserPermissions(row)">知识库权限</el-button>
<el-button size="small" type="danger" @click="deleteUser(row)">删除</el-button>
</template>
</el-table-column>
@@ -876,9 +833,9 @@ function buildChatQuery() {
</el-select>
</el-form-item>
<el-form-item label="模型">
<el-select v-model="quickModelForm.modelName">
<el-select v-model="quickModelForm.modelName" :key="quickModelForm.provider">
<el-option
v-for="model in selectedQuickProvider()?.models"
v-for="model in selectedQuickProvider.models"
:key="model.value"
:label="model.label"
:value="model.value"
@@ -911,6 +868,7 @@ function buildChatQuery() {
<el-select v-model="modelForm.apiType">
<el-option label="OpenAI 兼容" value="openai_compatible" />
<el-option label="Anthropic Messages" value="anthropic_messages" />
<el-option label="MiniMax 官方" value="minimax" />
</el-select>
</el-form-item>
<el-form-item label="模型名">
@@ -959,6 +917,7 @@ function buildChatQuery() {
<el-button size="small" @click="editModel(row)">编辑</el-button>
<el-button size="small" @click="testModel(row.id)">测试</el-button>
<el-button size="small" type="primary" @click="enableModel(row.id)">启用</el-button>
<el-button size="small" type="danger" @click="deleteModel(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -994,13 +953,19 @@ function buildChatQuery() {
<el-option label="失败" value="FAILED" />
</el-select>
<el-date-picker
v-model="chatFilters.dateRange"
class="record-date-range"
type="datetimerange"
unlink-panels
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DDTHH:mm:ss"
v-model="chatFilters.dateFrom"
class="record-date-item"
type="datetime"
placeholder="开始时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<span class="record-date-sep"></span>
<el-date-picker
v-model="chatFilters.dateTo"
class="record-date-item"
type="datetime"
placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<div class="record-filter-actions">
<el-button type="primary" @click="searchChats">搜索</el-button>
@@ -1047,62 +1012,6 @@ function buildChatQuery() {
</section>
</section>
<el-drawer v-model="userPermissionOpen" size="760px" title="知识库权限分配">
<template v-if="permissionUser">
<section class="permission-summary">
<div>
<span>用户</span>
<strong>{{ permissionUser.name || '-' }}</strong>
</div>
<div>
<span>手机号</span>
<strong>{{ permissionUser.phone }}</strong>
</div>
<div>
<span>每日额度</span>
<strong>{{ permissionUser.dailyChatLimit }}</strong>
</div>
</section>
<el-table v-loading="userPermissionLoading" :data="userKnowledgePermissionRows" stripe>
<el-table-column label="授权" width="90">
<template #default="{ row }">
<el-switch v-model="row.assigned" />
</template>
</el-table-column>
<el-table-column prop="name" label="知识库" min-width="170" />
<el-table-column label="状态" width="90">
<template #default="{ row }">{{ row.status === 1 ? "启用" : "禁用" }}</template>
</el-table-column>
<el-table-column label="生效时间" width="220">
<template #default="{ row }">
<el-date-picker
v-model="row.effectiveAt"
type="datetime"
value-format="YYYY-MM-DDTHH:mm:ss"
placeholder="不限制"
:disabled="!row.assigned"
/>
</template>
</el-table-column>
<el-table-column label="失效时间" width="220">
<template #default="{ row }">
<el-date-picker
v-model="row.expiredAt"
type="datetime"
value-format="YYYY-MM-DDTHH:mm:ss"
placeholder="不限制"
:disabled="!row.assigned"
/>
</template>
</el-table-column>
</el-table>
<div class="drawer-actions">
<el-button type="primary" @click="saveUserPermissions">保存权限</el-button>
<el-button @click="userPermissionOpen = false">取消</el-button>
</div>
</template>
</el-drawer>
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
<template v-if="chatDetail">
<section class="chat-summary">