Implement open knowledge access scope
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -33,11 +33,13 @@ import "element-plus/theme-chalk/el-switch.css";
|
||||
import "element-plus/theme-chalk/el-table.css";
|
||||
import "element-plus/theme-chalk/el-tabs.css";
|
||||
import { createApp } from "vue";
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
|
||||
import App from "./App.vue";
|
||||
import "./styles.css";
|
||||
|
||||
const app = createApp(App);
|
||||
app.config.globalProperties.$ELEMENT = { locale: zhCn };
|
||||
|
||||
[
|
||||
ElButton,
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
DashboardStats,
|
||||
KnowledgeItem,
|
||||
ModelItem,
|
||||
UserKnowledgePermissionDetail,
|
||||
} from "../types/api";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8100/api";
|
||||
@@ -77,13 +76,6 @@ export const api = {
|
||||
updateUser: (id: number, payload: Record<string, unknown>) =>
|
||||
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
||||
userKnowledgePermissions: (id: number) =>
|
||||
request<UserKnowledgePermissionDetail>(`/admin/user/${id}/knowledge-permissions`),
|
||||
saveUserKnowledgePermissions: (id: number, payload: Record<string, unknown>) =>
|
||||
request<UserKnowledgePermissionDetail>(`/admin/user/${id}/knowledge-permissions`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
knowledge: () => request<KnowledgeItem[]>("/admin/knowledge/list"),
|
||||
createKnowledge: (payload: Record<string, unknown>) =>
|
||||
request<KnowledgeItem>("/admin/knowledge", { method: "POST", body: JSON.stringify(payload) }),
|
||||
@@ -103,6 +95,8 @@ export const api = {
|
||||
request<ModelItem>(`/admin/model/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||
enableModel: (modelId: number) =>
|
||||
request<null>("/admin/model/enable", { method: "POST", body: JSON.stringify({ modelId }) }),
|
||||
deleteModel: (modelId: number) =>
|
||||
request<null>(`/admin/model/${modelId}`, { method: "DELETE" }),
|
||||
testModel: (modelId: number) =>
|
||||
request<{ ok: boolean; message: string; answer: string }>(`/admin/model/${modelId}/test`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -377,42 +377,6 @@ textarea {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.permission-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.permission-summary div {
|
||||
padding: 12px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 8px;
|
||||
background: #f8fbfa;
|
||||
}
|
||||
|
||||
.permission-summary span {
|
||||
display: block;
|
||||
color: #6b7d77;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.permission-summary strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
color: #1f2d2a;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.el-drawer .el-date-editor.el-input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -437,10 +401,16 @@ textarea {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.record-date-range {
|
||||
width: 580px !important;
|
||||
max-width: 100%;
|
||||
flex: 0 0 580px;
|
||||
.record-date-item {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.record-date-sep {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.record-filter-actions {
|
||||
@@ -465,8 +435,10 @@ textarea {
|
||||
|
||||
.chat-summary span {
|
||||
display: block;
|
||||
color: #6b7d77;
|
||||
font-size: 12px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2a6b56;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chat-summary strong {
|
||||
|
||||
@@ -43,17 +43,6 @@ export interface KnowledgeItem {
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface UserKnowledgePermissionItem {
|
||||
knowledgeId: number;
|
||||
effectiveAt?: string | null;
|
||||
expiredAt?: string | null;
|
||||
}
|
||||
|
||||
export interface UserKnowledgePermissionDetail {
|
||||
user: AdminUser;
|
||||
permissions: UserKnowledgePermissionItem[];
|
||||
}
|
||||
|
||||
export interface ModelItem {
|
||||
id: number;
|
||||
provider: string;
|
||||
|
||||
Reference in New Issue
Block a user