Files
leaudit-platform-frontend/docs/new-dify/dify_frontend_integration.md
T
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

36 KiB
Raw Blame History

Dify 模块前端对接文档

版本:1.0 更新时间:2025-12-06 适用前端:Vue 3 + TypeScript


目录

  1. 概述
  2. 权限体系
  3. 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

前端权限检查

// 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 接口清单

基础配置

// 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

// 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
}

使用示例:

<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

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

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

export async function deleteConversation(conversationId: string): Promise<void> {
  await request.delete(DIFY_API.CONVERSATION_DELETE(conversationId))
}

使用示例:

<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

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

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: 全部知识库
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
}

使用示例:

<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 (仅省级管理员)

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

export async function getAvailableAreas(): Promise<string[]> {
  const response = await request.get(DIFY_API.AREA_DATASETS_AREAS)
  return response.data.data
}

4. 创建知识库绑定

权限要求: dify:dataset:manage (仅省级管理员)

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 (仅省级管理员)

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 (仅省级管理员)

export async function deleteDatasetBinding(id: number): Promise<void> {
  await request.delete(DIFY_API.AREA_DATASETS_DETAIL(id))
}

7. 检查知识库访问权限

权限要求: dify:dataset:read

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
}

使用示例:

// 在访问 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 获取用户权限:

// 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. 路由守卫

// 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. 按钮级权限控制

// 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)

使用示例:

<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

// 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
  }
}

使用示例:

<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 服务器错误 显示系统错误提示

统一错误处理

// 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

权限错误特殊处理

// 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 '操作失败'
}

完整代码示例

知识库管理页面完整实现

<!-- 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 初始版本,包含完整的前端对接说明