Files
TanWenyan 27aff59152 feat: 添加知识库配置管理功能
新增地区-知识库绑定管理功能,支持增删改查操作
- 添加 V3 API 路由层:area-datasets 相关接口
- 添加 API 客户端:area-datasets.ts
- 添加自定义 Hook:use-area-dataset-config.ts
- 添加管理组件:area-dataset-config.tsx
- 修复路由冲突问题,删除重复的 .ts 路由文件
- 更新 dataset-manager 页面,添加 Tabs 导航

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-07 23:12:21 +08:00

1400 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dify 模块前端对接文档
> 版本:1.0
> 更新时间:2025-12-06
> 适用前端:Vue 3 + TypeScript
---
## 目录
1. [概述](#概述)
2. [权限体系](#权限体系)
3. [API 接口清单](#api-接口清单)
4. [大模型对话模块](#大模型对话模块)
5. [知识库管理模块](#知识库管理模块)
6. [权限控制实现](#权限控制实现)
7. [错误处理](#错误处理)
8. [完整代码示例](#完整代码示例)
---
## 概述
### 模块结构
```
AI法务助手 /chat-with-llm
├── 大模型对话 /chat-with-llm/chat
│ ├── 使用AI对话 (dify:chat:use)
│ ├── 查看对话历史 (dify:conversation:read)
│ ├── 删除对话 (dify:conversation:delete)
│ └── 消息反馈 (dify:message:feedback)
└── 知识库管理 /chat-with-llm/dataset-manager
├── 查看知识库 (dify:dataset:read)
├── 编辑知识库内容 (dify:dataset:write)
├── 管理知识库绑定 (dify:dataset:manage)
├── 下载文件 (dify:file:read)
└── 上传文件 (dify:file:upload)
```
### 角色权限矩阵
| 功能 | common (普通员工) | admin (市级管理员) | provincial_admin (省级管理员) |
|------|------------------|-------------------|------------------------------|
| 大模型对话 | ✅ | ✅ | ✅ |
| 查看对话历史 | ✅ 仅自己 | ✅ 仅自己 | ✅ 仅自己 |
| 删除对话 | ✅ 仅自己 | ✅ 仅自己 | ✅ 仅自己 |
| 查看知识库 | ✅ 本地区+公共 | ✅ 本地区+公共 | ✅ 全部 |
| 编辑知识库内容 | ❌ | ✅ 仅本地区 | ✅ 全部 |
| 管理知识库绑定 | ❌ | ❌ | ✅ |
| 文件上传/下载 | ✅ | ✅ | ✅ |
---
## 权限体系
### 权限标识 (permission_key)
| 权限标识 | 显示名称 | HTTP方法 | API路径 |
|---------|---------|----------|---------|
| `dify:chat:use` | 使用AI对话 | POST | /api/dify/chat/chat-messages |
| `dify:conversation:read` | 查看对话历史 | GET | /api/dify/chat/conversations |
| `dify:conversation:delete` | 删除对话 | DELETE | /api/dify/chat/conversations/{conversation_id} |
| `dify:message:feedback` | 消息反馈 | POST | /api/dify/chat/messages/{message_id}/feedbacks |
| `dify:dataset:read` | 查看知识库 | GET | /api/v3/dify/area-datasets/my |
| `dify:dataset:write` | 编辑知识库内容 | POST | /api/dify/dataset/documents |
| `dify:dataset:manage` | 管理知识库绑定 | POST | /api/v3/dify/area-datasets |
| `dify:file:read` | 下载文件 | GET | /api/dify/file/{file_id}/file-preview |
| `dify:file:upload` | 上传文件 | POST | /api/dify/chat/files/upload |
### 前端权限检查
```typescript
// stores/permission.ts
import { defineStore } from 'pinia'
interface RoutePermission {
route_path: string
permissions: string[]
}
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [] as RoutePermission[],
routesFlat: [] as RoutePermission[]
}),
actions: {
// 检查是否有某个权限
hasPermission(permissionKey: string): boolean {
return this.routesFlat.some(route =>
route.permissions.includes(permissionKey)
)
},
// 检查是否有某个路由下的权限
hasRoutePermission(routePath: string, permissionKey: string): boolean {
const route = this.routesFlat.find(r => r.route_path === routePath)
return route?.permissions.includes(permissionKey) ?? false
}
}
})
// 使用示例
const permissionStore = usePermissionStore()
// 检查是否可以编辑知识库
if (permissionStore.hasPermission('dify:dataset:write')) {
// 显示编辑按钮
}
// 检查是否可以管理知识库绑定
if (permissionStore.hasPermission('dify:dataset:manage')) {
// 显示管理按钮
}
```
---
## API 接口清单
### 基础配置
```typescript
// api/config.ts
const API_BASE = '/api'
export const DIFY_API = {
// 对话相关
CHAT_MESSAGES: `${API_BASE}/dify/chat/chat-messages`,
CONVERSATIONS: `${API_BASE}/dify/chat/conversations`,
CONVERSATION_MESSAGES: (id: string) => `${API_BASE}/dify/chat/conversations/${id}/messages`,
CONVERSATION_DELETE: (id: string) => `${API_BASE}/dify/chat/conversations/${id}`,
CONVERSATION_RENAME: (id: string) => `${API_BASE}/dify/chat/conversations/${id}/name`,
MESSAGE_FEEDBACK: (id: string) => `${API_BASE}/dify/chat/messages/${id}/feedbacks`,
SUGGESTED_QUESTIONS: (id: string) => `${API_BASE}/dify/chat/messages/${id}/suggested`,
// 文件相关
FILE_UPLOAD: `${API_BASE}/dify/chat/files/upload`,
FILE_PREVIEW: (id: string) => `${API_BASE}/dify/file/${id}/file-preview`,
// 知识库相关 (新增)
AREA_DATASETS_MY: `${API_BASE}/v3/dify/area-datasets/my`,
AREA_DATASETS: `${API_BASE}/v3/dify/area-datasets`,
AREA_DATASETS_DETAIL: (id: number) => `${API_BASE}/v3/dify/area-datasets/${id}`,
AREA_DATASETS_AREAS: `${API_BASE}/v3/dify/area-datasets/areas`,
AREA_DATASETS_CHECK: (datasetId: string) => `${API_BASE}/v3/dify/area-datasets/check/${datasetId}`,
}
```
---
## 大模型对话模块
### 1. 发送对话消息
**权限要求**: `dify:chat:use`
```typescript
// api/dify/chat.ts
interface ChatMessageRequest {
query: string // 用户输入的问题
conversation_id?: string // 对话ID(首次对话不传)
response_mode?: 'streaming' | 'blocking' // 响应模式,默认 streaming
files?: Array<{
type: 'image' | 'document'
transfer_method: 'local_file'
upload_file_id: string
}>
}
interface ChatMessageResponse {
event: string
message_id: string
conversation_id: string
answer: string
created_at: number
}
// 流式对话(推荐)
export async function sendChatMessageStream(
data: ChatMessageRequest,
onMessage: (chunk: string) => void,
onDone: (response: ChatMessageResponse) => void,
onError: (error: Error) => void
) {
const response = await fetch(DIFY_API.CHAT_MESSAGES, {
method: 'POST',
headers: {
'Authorization': `Bearer ${getToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...data,
response_mode: 'streaming'
})
})
if (!response.ok) {
if (response.status === 403) {
onError(new Error('权限不足:您没有使用AI对话的权限'))
return
}
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader!.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
if (data.event === 'message') {
onMessage(data.answer)
} else if (data.event === 'message_end') {
onDone(data)
} else if (data.event === 'error') {
onError(new Error(data.message))
}
}
}
}
}
// 阻塞式对话
export async function sendChatMessage(data: ChatMessageRequest): Promise<ChatMessageResponse> {
const response = await request.post(DIFY_API.CHAT_MESSAGES, {
...data,
response_mode: 'blocking'
})
return response.data
}
```
**使用示例**:
```vue
<template>
<div class="chat-container">
<div class="messages">
<div v-for="msg in messages" :key="msg.id" :class="msg.role">
{{ msg.content }}
</div>
</div>
<div class="input-area">
<input v-model="inputText" @keyup.enter="sendMessage" />
<button @click="sendMessage" :disabled="!canChat">发送</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { sendChatMessageStream } from '@/api/dify/chat'
const permissionStore = usePermissionStore()
const canChat = computed(() => permissionStore.hasPermission('dify:chat:use'))
const inputText = ref('')
const messages = ref<Array<{ id: string, role: string, content: string }>>([])
const conversationId = ref<string>()
const currentAnswer = ref('')
async function sendMessage() {
if (!inputText.value.trim() || !canChat.value) return
const userMessage = inputText.value
inputText.value = ''
// 添加用户消息
messages.value.push({
id: Date.now().toString(),
role: 'user',
content: userMessage
})
// 添加AI消息占位
const aiMessageId = Date.now().toString() + '_ai'
messages.value.push({
id: aiMessageId,
role: 'assistant',
content: ''
})
currentAnswer.value = ''
await sendChatMessageStream(
{
query: userMessage,
conversation_id: conversationId.value
},
// onMessage - 流式接收
(chunk) => {
currentAnswer.value += chunk
const aiMsg = messages.value.find(m => m.id === aiMessageId)
if (aiMsg) aiMsg.content = currentAnswer.value
},
// onDone - 完成
(response) => {
conversationId.value = response.conversation_id
},
// onError - 错误
(error) => {
console.error('对话失败:', error)
ElMessage.error(error.message)
}
)
}
</script>
```
### 2. 获取对话历史列表
**权限要求**: `dify:conversation:read`
```typescript
interface Conversation {
id: string
name: string
created_at: number
updated_at: number
}
interface ConversationListResponse {
data: Conversation[]
has_more: boolean
limit: number
}
export async function getConversations(
limit: number = 20,
last_id?: string
): Promise<ConversationListResponse> {
const params = new URLSearchParams({ limit: limit.toString() })
if (last_id) params.append('last_id', last_id)
const response = await request.get(`${DIFY_API.CONVERSATIONS}?${params}`)
return response.data
}
```
### 3. 获取对话消息详情
**权限要求**: `dify:conversation:read`
```typescript
interface Message {
id: string
conversation_id: string
query: string
answer: string
created_at: number
feedback?: {
rating: 'like' | 'dislike'
}
}
interface MessageListResponse {
data: Message[]
has_more: boolean
limit: number
}
export async function getConversationMessages(
conversationId: string,
limit: number = 20,
first_id?: string
): Promise<MessageListResponse> {
const params = new URLSearchParams({ limit: limit.toString() })
if (first_id) params.append('first_id', first_id)
const response = await request.get(
`${DIFY_API.CONVERSATION_MESSAGES(conversationId)}?${params}`
)
return response.data
}
```
### 4. 删除对话
**权限要求**: `dify:conversation:delete`
```typescript
export async function deleteConversation(conversationId: string): Promise<void> {
await request.delete(DIFY_API.CONVERSATION_DELETE(conversationId))
}
```
**使用示例**:
```vue
<template>
<div class="conversation-item">
<span>{{ conversation.name }}</span>
<button
v-if="canDelete"
@click="handleDelete(conversation.id)"
>
删除
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { deleteConversation } from '@/api/dify/chat'
const permissionStore = usePermissionStore()
const canDelete = computed(() =>
permissionStore.hasPermission('dify:conversation:delete')
)
async function handleDelete(id: string) {
try {
await ElMessageBox.confirm('确定要删除这个对话吗?', '确认删除')
await deleteConversation(id)
ElMessage.success('删除成功')
emit('refresh')
} catch (error: any) {
if (error.response?.status === 403) {
ElMessage.error('您没有删除对话的权限')
} else if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
</script>
```
### 5. 消息反馈(点赞/点踩)
**权限要求**: `dify:message:feedback`
```typescript
interface FeedbackRequest {
rating: 'like' | 'dislike' | null // null 表示取消反馈
content?: string // 反馈内容(可选)
}
export async function submitMessageFeedback(
messageId: string,
feedback: FeedbackRequest
): Promise<void> {
await request.post(DIFY_API.MESSAGE_FEEDBACK(messageId), feedback)
}
```
### 6. 上传文件
**权限要求**: `dify:file:upload`
```typescript
interface FileUploadResponse {
id: string
name: string
size: number
extension: string
mime_type: string
created_at: number
}
export async function uploadFile(file: File): Promise<FileUploadResponse> {
const formData = new FormData()
formData.append('file', file)
const response = await request.post(DIFY_API.FILE_UPLOAD, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
return response.data
}
```
---
## 知识库管理模块
### 1. 获取当前用户可访问的知识库列表
**权限要求**: `dify:dataset:read`
**接口说明**: 根据用户角色自动返回对应数据范围
- `common` / `admin`: 本地区 + 公共知识库
- `provincial_admin`: 全部知识库
```typescript
interface AreaDataset {
id: number
area: string // 地区:梅州、云浮、揭阳、潮州、省级
dataset_id: string // Dify 知识库 ID
dataset_name: string // 知识库名称
dataset_description?: string // 知识库描述
is_default: boolean // 是否为该地区默认知识库
is_public: boolean // 是否公开(省级公共知识库)
sort_order: number // 排序顺序
status: number // 状态:1=启用, 0=禁用
created_at: string
updated_at: string
}
interface MyDatasetsResponse {
data: AreaDataset[]
total: number
user_area: string // 当前用户所属地区
user_role: string // 当前用户角色
}
export async function getMyDatasets(): Promise<MyDatasetsResponse> {
const response = await request.get(DIFY_API.AREA_DATASETS_MY)
return response.data
}
```
**使用示例**:
```vue
<template>
<div class="dataset-list">
<div class="header">
<h3>我的知识库</h3>
<span class="info">
地区: {{ userArea }} | 角色: {{ userRoleLabel }}
</span>
</div>
<div class="datasets">
<div
v-for="dataset in datasets"
:key="dataset.id"
class="dataset-card"
:class="{ 'is-public': dataset.is_public }"
>
<div class="name">
{{ dataset.dataset_name }}
<el-tag v-if="dataset.is_public" type="success" size="small">
公共
</el-tag>
<el-tag v-if="dataset.is_default" type="primary" size="small">
默认
</el-tag>
</div>
<div class="area">{{ dataset.area }}</div>
<div class="description">{{ dataset.dataset_description }}</div>
<div class="actions">
<!-- 编辑按钮需要 dify:dataset:write 权限 -->
<el-button
v-if="canEdit"
size="small"
@click="handleEdit(dataset)"
>
编辑
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { usePermissionStore } from '@/stores/permission'
import { getMyDatasets } from '@/api/dify/dataset'
const permissionStore = usePermissionStore()
const canEdit = computed(() =>
permissionStore.hasPermission('dify:dataset:write')
)
const datasets = ref<AreaDataset[]>([])
const userArea = ref('')
const userRole = ref('')
const userRoleLabel = computed(() => {
const labels: Record<string, string> = {
'common': '普通员工',
'admin': '市级管理员',
'provincial_admin': '省级管理员'
}
return labels[userRole.value] || userRole.value
})
onMounted(async () => {
try {
const res = await getMyDatasets()
datasets.value = res.data
userArea.value = res.user_area
userRole.value = res.user_role
} catch (error: any) {
if (error.response?.status === 403) {
ElMessage.error('您没有查看知识库的权限')
}
}
})
</script>
```
### 2. 获取所有知识库绑定列表(管理员)
**权限要求**: `dify:dataset:manage` (仅省级管理员)
```typescript
interface DatasetListResponse {
data: AreaDataset[]
total: number
page: number
page_size: number
has_more: boolean
}
interface DatasetListParams {
area?: string // 筛选地区
only_enabled?: boolean // 是否只返回启用的,默认 true
page?: number // 页码,默认 1
page_size?: number // 每页数量,默认 20
}
export async function getAllDatasets(
params: DatasetListParams = {}
): Promise<DatasetListResponse> {
const response = await request.get(DIFY_API.AREA_DATASETS, { params })
return response.data
}
```
### 3. 获取可用地区列表
**权限要求**: `dify:dataset:manage`
```typescript
export async function getAvailableAreas(): Promise<string[]> {
const response = await request.get(DIFY_API.AREA_DATASETS_AREAS)
return response.data.data
}
```
### 4. 创建知识库绑定
**权限要求**: `dify:dataset:manage` (仅省级管理员)
```typescript
interface CreateDatasetRequest {
area: string // 地区名称
dataset_id: string // Dify 知识库 ID
dataset_name: string // 知识库名称
dataset_description?: string // 描述
is_default?: boolean // 是否默认,默认 false
is_public?: boolean // 是否公开,默认 false
sort_order?: number // 排序,默认 0
}
export async function createDatasetBinding(
data: CreateDatasetRequest
): Promise<AreaDataset> {
const response = await request.post(DIFY_API.AREA_DATASETS, data)
return response.data.data
}
```
### 5. 更新知识库绑定
**权限要求**: `dify:dataset:manage` (仅省级管理员)
```typescript
interface UpdateDatasetRequest {
dataset_name?: string
dataset_description?: string
is_default?: boolean
is_public?: boolean
sort_order?: number
status?: number // 1=启用, 0=禁用
}
export async function updateDatasetBinding(
id: number,
data: UpdateDatasetRequest
): Promise<AreaDataset> {
const response = await request.put(DIFY_API.AREA_DATASETS_DETAIL(id), data)
return response.data.data
}
```
### 6. 删除知识库绑定
**权限要求**: `dify:dataset:manage` (仅省级管理员)
```typescript
export async function deleteDatasetBinding(id: number): Promise<void> {
await request.delete(DIFY_API.AREA_DATASETS_DETAIL(id))
}
```
### 7. 检查知识库访问权限
**权限要求**: `dify:dataset:read`
```typescript
interface CheckAccessResponse {
has_access: boolean
user_area: string
dataset_id: string
}
export async function checkDatasetAccess(
datasetId: string
): Promise<CheckAccessResponse> {
const response = await request.get(DIFY_API.AREA_DATASETS_CHECK(datasetId))
return response.data
}
```
**使用示例**:
```typescript
// 在访问 Dify 知识库详情之前检查权限
async function viewDatasetDetail(datasetId: string) {
try {
const { has_access } = await checkDatasetAccess(datasetId)
if (!has_access) {
ElMessage.warning('您没有访问该知识库的权限')
return
}
// 继续访问知识库详情...
router.push(`/dataset/${datasetId}`)
} catch (error) {
ElMessage.error('权限检查失败')
}
}
```
---
## 权限控制实现
### 1. 获取用户权限
登录后调用 `/user/routes` 获取用户权限:
```typescript
// api/auth.ts
interface UserRoutesResponse {
user_id: number
username: string
routes: RouteInfo[]
routes_flat: RouteInfo[]
}
export async function getUserRoutes(): Promise<UserRoutesResponse> {
const response = await request.get('/user/routes')
return response.data
}
// 登录后初始化权限
async function initPermissions() {
const data = await getUserRoutes()
const permissionStore = usePermissionStore()
permissionStore.setRoutes(data.routes, data.routes_flat)
}
```
### 2. 路由守卫
```typescript
// router/guards.ts
import { usePermissionStore } from '@/stores/permission'
router.beforeEach(async (to, from, next) => {
const permissionStore = usePermissionStore()
// 检查是否有该路由的访问权限
const hasAccess = permissionStore.routesFlat.some(
route => route.route_path === to.path
)
if (!hasAccess && to.path !== '/403') {
next('/403')
return
}
next()
})
```
### 3. 按钮级权限控制
```typescript
// directives/permission.ts
import { usePermissionStore } from '@/stores/permission'
export const vPermission = {
mounted(el: HTMLElement, binding: { value: string }) {
const permissionStore = usePermissionStore()
const permissionKey = binding.value
if (!permissionStore.hasPermission(permissionKey)) {
el.parentNode?.removeChild(el)
}
}
}
// main.ts
app.directive('permission', vPermission)
```
**使用示例**:
```vue
<template>
<!-- 只有拥有 dify:dataset:write 权限的用户才能看到编辑按钮 -->
<el-button v-permission="'dify:dataset:write'">编辑</el-button>
<!-- 只有省级管理员才能看到管理按钮 -->
<el-button v-permission="'dify:dataset:manage'">管理绑定</el-button>
</template>
```
### 4. 组合式权限 Hook
```typescript
// composables/usePermission.ts
import { computed } from 'vue'
import { usePermissionStore } from '@/stores/permission'
export function usePermission() {
const store = usePermissionStore()
// Dify 对话权限
const canChat = computed(() => store.hasPermission('dify:chat:use'))
const canViewHistory = computed(() => store.hasPermission('dify:conversation:read'))
const canDeleteConversation = computed(() => store.hasPermission('dify:conversation:delete'))
const canFeedback = computed(() => store.hasPermission('dify:message:feedback'))
// Dify 知识库权限
const canViewDataset = computed(() => store.hasPermission('dify:dataset:read'))
const canEditDataset = computed(() => store.hasPermission('dify:dataset:write'))
const canManageDataset = computed(() => store.hasPermission('dify:dataset:manage'))
// Dify 文件权限
const canDownloadFile = computed(() => store.hasPermission('dify:file:read'))
const canUploadFile = computed(() => store.hasPermission('dify:file:upload'))
return {
// 检查任意权限
hasPermission: (key: string) => store.hasPermission(key),
// 对话权限
canChat,
canViewHistory,
canDeleteConversation,
canFeedback,
// 知识库权限
canViewDataset,
canEditDataset,
canManageDataset,
// 文件权限
canDownloadFile,
canUploadFile
}
}
```
**使用示例**:
```vue
<template>
<div class="chat-page">
<!-- 对话输入框 -->
<div v-if="canChat" class="chat-input">
<input v-model="message" />
<el-button @click="send">发送</el-button>
</div>
<div v-else class="no-permission">
您没有使用AI对话的权限
</div>
<!-- 对话历史 -->
<div v-if="canViewHistory" class="history">
<div v-for="conv in conversations" :key="conv.id">
{{ conv.name }}
<el-button
v-if="canDeleteConversation"
@click="deleteConv(conv.id)"
>
删除
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { usePermission } from '@/composables/usePermission'
const {
canChat,
canViewHistory,
canDeleteConversation
} = usePermission()
</script>
```
---
## 错误处理
### HTTP 状态码
| 状态码 | 说明 | 处理方式 |
|--------|------|---------|
| 200 | 成功 | 正常处理 |
| 400 | 请求参数错误 | 显示错误信息 |
| 401 | 未认证 | 跳转登录页 |
| 403 | 权限不足 | 显示权限提示 |
| 404 | 资源不存在 | 显示不存在提示 |
| 500 | 服务器错误 | 显示系统错误提示 |
### 统一错误处理
```typescript
// utils/request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 30000
})
// 请求拦截器
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
response => response,
error => {
const { response } = error
if (response) {
switch (response.status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
router.push('/login')
break
case 403:
// 权限不足
const errorData = response.data
ElMessage.error(errorData?.detail || '权限不足,无法执行此操作')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器错误,请稍后重试')
break
default:
ElMessage.error(response.data?.message || '请求失败')
}
} else {
ElMessage.error('网络错误,请检查网络连接')
}
return Promise.reject(error)
}
)
export default request
```
### 权限错误特殊处理
```typescript
// 403 错误的详细信息格式
interface PermissionError {
error: string // 如 "权限不足: dify:dataset:manage"
detail: string // 如 "您没有执行此操作的权限,请联系管理员"
}
// 解析权限错误
function parsePermissionError(error: any): string {
if (error.response?.status === 403) {
const data = error.response.data as PermissionError
// 提取权限标识
const match = data.error?.match(/权限不足: (.+)/)
if (match) {
const permissionKey = match[1]
const permissionLabels: Record<string, string> = {
'dify:chat:use': '使用AI对话',
'dify:conversation:read': '查看对话历史',
'dify:conversation:delete': '删除对话',
'dify:dataset:read': '查看知识库',
'dify:dataset:write': '编辑知识库',
'dify:dataset:manage': '管理知识库绑定',
'dify:file:read': '下载文件',
'dify:file:upload': '上传文件'
}
return `您没有"${permissionLabels[permissionKey] || permissionKey}"的权限`
}
return data.detail || '权限不足'
}
return '操作失败'
}
```
---
## 完整代码示例
### 知识库管理页面完整实现
```vue
<!-- views/dataset-manager/Index.vue -->
<template>
<div class="dataset-manager">
<el-card>
<template #header>
<div class="card-header">
<span>知识库管理</span>
<div class="header-info">
<el-tag>地区: {{ userArea }}</el-tag>
<el-tag type="info">{{ userRoleLabel }}</el-tag>
</div>
<!-- 只有省级管理员可以新增绑定 -->
<el-button
v-if="canManageDataset"
type="primary"
@click="handleCreate"
>
新增绑定
</el-button>
</div>
</template>
<!-- 筛选区域仅省级管理员可见 -->
<div v-if="canManageDataset" class="filter-area">
<el-select v-model="filterArea" placeholder="选择地区" clearable>
<el-option
v-for="area in areas"
:key="area"
:label="area"
:value="area"
/>
</el-select>
<el-button @click="loadDatasets">查询</el-button>
</div>
<!-- 知识库列表 -->
<el-table :data="datasets" v-loading="loading">
<el-table-column prop="area" label="地区" width="100" />
<el-table-column prop="dataset_name" label="知识库名称" />
<el-table-column prop="dataset_description" label="描述" />
<el-table-column label="标签" width="150">
<template #default="{ row }">
<el-tag v-if="row.is_public" type="success" size="small">
公共
</el-tag>
<el-tag v-if="row.is_default" type="primary" size="small">
默认
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" v-if="canEditDataset">
<template #default="{ row }">
<el-button
v-if="canEditDataset"
size="small"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-if="canManageDataset"
size="small"
type="danger"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页仅管理员视图 -->
<el-pagination
v-if="canManageDataset"
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
@current-change="loadDatasets"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="editingId ? '编辑知识库绑定' : '新增知识库绑定'"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-form-item label="地区" prop="area">
<el-select v-model="form.area" :disabled="!!editingId">
<el-option
v-for="area in areas"
:key="area"
:label="area"
:value="area"
/>
</el-select>
</el-form-item>
<el-form-item label="Dify知识库ID" prop="dataset_id">
<el-input v-model="form.dataset_id" :disabled="!!editingId" />
</el-form-item>
<el-form-item label="知识库名称" prop="dataset_name">
<el-input v-model="form.dataset_name" />
</el-form-item>
<el-form-item label="描述" prop="dataset_description">
<el-input v-model="form.dataset_description" type="textarea" />
</el-form-item>
<el-form-item label="是否公开">
<el-switch v-model="form.is_public" />
<span class="form-tip">公开后所有地区用户可见</span>
</el-form-item>
<el-form-item label="是否默认">
<el-switch v-model="form.is_default" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { usePermission } from '@/composables/usePermission'
import {
getMyDatasets,
getAllDatasets,
getAvailableAreas,
createDatasetBinding,
updateDatasetBinding,
deleteDatasetBinding,
type AreaDataset
} from '@/api/dify/dataset'
// 权限
const { canViewDataset, canEditDataset, canManageDataset } = usePermission()
// 数据
const loading = ref(false)
const datasets = ref<AreaDataset[]>([])
const areas = ref<string[]>([])
const userArea = ref('')
const userRole = ref('')
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const filterArea = ref('')
// 表单
const dialogVisible = ref(false)
const editingId = ref<number | null>(null)
const submitting = ref(false)
const formRef = ref()
const form = ref({
area: '',
dataset_id: '',
dataset_name: '',
dataset_description: '',
is_public: false,
is_default: false,
sort_order: 0
})
const rules = {
area: [{ required: true, message: '请选择地区', trigger: 'change' }],
dataset_id: [{ required: true, message: '请输入Dify知识库ID', trigger: 'blur' }],
dataset_name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }]
}
const userRoleLabel = computed(() => {
const labels: Record<string, string> = {
'common': '普通员工',
'admin': '市级管理员',
'provincial_admin': '省级管理员'
}
return labels[userRole.value] || userRole.value
})
// 加载数据
async function loadDatasets() {
loading.value = true
try {
if (canManageDataset.value) {
// 省级管理员:获取全部
const res = await getAllDatasets({
area: filterArea.value || undefined,
page: page.value,
page_size: pageSize.value
})
datasets.value = res.data
total.value = res.total
} else {
// 其他角色:获取自己可访问的
const res = await getMyDatasets()
datasets.value = res.data
userArea.value = res.user_area
userRole.value = res.user_role
total.value = res.total
}
} catch (error) {
console.error('加载知识库失败:', error)
} finally {
loading.value = false
}
}
// 加载地区列表
async function loadAreas() {
if (canManageDataset.value) {
try {
areas.value = await getAvailableAreas()
} catch (error) {
console.error('加载地区失败:', error)
}
}
}
// 新增
function handleCreate() {
editingId.value = null
form.value = {
area: '',
dataset_id: '',
dataset_name: '',
dataset_description: '',
is_public: false,
is_default: false,
sort_order: 0
}
dialogVisible.value = true
}
// 编辑
function handleEdit(row: AreaDataset) {
editingId.value = row.id
form.value = {
area: row.area,
dataset_id: row.dataset_id,
dataset_name: row.dataset_name,
dataset_description: row.dataset_description || '',
is_public: row.is_public,
is_default: row.is_default,
sort_order: row.sort_order
}
dialogVisible.value = true
}
// 删除
async function handleDelete(row: AreaDataset) {
try {
await ElMessageBox.confirm(
`确定要删除知识库绑定「${row.dataset_name}」吗?`,
'确认删除'
)
await deleteDatasetBinding(row.id)
ElMessage.success('删除成功')
loadDatasets()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
async function handleSubmit() {
try {
await formRef.value?.validate()
submitting.value = true
if (editingId.value) {
await updateDatasetBinding(editingId.value, {
dataset_name: form.value.dataset_name,
dataset_description: form.value.dataset_description,
is_public: form.value.is_public,
is_default: form.value.is_default,
sort_order: form.value.sort_order
})
ElMessage.success('更新成功')
} else {
await createDatasetBinding(form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
loadDatasets()
} catch (error: any) {
if (error.response?.status === 403) {
ElMessage.error('权限不足')
} else if (error.response?.status === 400) {
ElMessage.error(error.response.data?.message || '操作失败')
}
} finally {
submitting.value = false
}
}
// 初始化
onMounted(() => {
loadDatasets()
loadAreas()
})
</script>
<style scoped lang="scss">
.dataset-manager {
padding: 20px;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.header-info {
display: flex;
gap: 10px;
}
}
.filter-area {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.form-tip {
margin-left: 10px;
color: #909399;
font-size: 12px;
}
}
</style>
```
---
## 更新日志
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0 | 2025-12-06 | 初始版本,包含完整的前端对接说明 |