27aff59152
新增地区-知识库绑定管理功能,支持增删改查操作 - 添加 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>
1400 lines
36 KiB
Markdown
1400 lines
36 KiB
Markdown
# 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 | 初始版本,包含完整的前端对接说明 |
|