From c4085e9811824c7b716727612ceffd97113e233d Mon Sep 17 00:00:00 2001 From: "xinxin.wu" Date: Wed, 14 Oct 2026 12:38:08 +0800 Subject: [PATCH] feat: user chat and external-retrieval --- ui/src/api/API_README.md | 45 ++++++- .../system/resource-management/chat-user.ts | 30 +++++ .../knowledge/external-retrieval.ts | 16 +++ .../system/shared-resources/chat-user.ts | 30 +++++ .../knowledge/external-retrieval.ts | 16 +++ .../workspace/knowledge/external-retrieval.ts | 17 +++ .../api/admin/workspace/shared/chat-user.ts | 17 +++ .../shared/knowledge/external-retrieval.ts | 17 +++ ui/src/api/types/chat-user.ts | 4 +- ui/src/api/types/index.ts | 1 + .../api/types/knowledge-external-retrieval.ts | 17 +++ ui/src/views/VIEW_README.md | 54 ++++++-- .../application-detail/chat-user/index.vue | 85 +++++++----- .../knowledge-detail/chat-user/index.vue | 85 +++++++----- .../external-retrieval/index.vue | 125 +++++++++++++++++- ui/src/views/system/identity/groups/index.vue | 33 ++++- 16 files changed, 499 insertions(+), 93 deletions(-) create mode 100644 ui/src/api/admin/system/resource-management/chat-user.ts create mode 100644 ui/src/api/admin/system/resource-management/knowledge/external-retrieval.ts create mode 100644 ui/src/api/admin/system/shared-resources/chat-user.ts create mode 100644 ui/src/api/admin/system/shared-resources/knowledge/external-retrieval.ts create mode 100644 ui/src/api/admin/workspace/knowledge/external-retrieval.ts create mode 100644 ui/src/api/admin/workspace/shared/chat-user.ts create mode 100644 ui/src/api/admin/workspace/shared/knowledge/external-retrieval.ts create mode 100644 ui/src/api/types/knowledge-external-retrieval.ts diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 778455644bd..f3a406d0227 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -732,11 +732,25 @@ Workspace 共享知识库详情由 `resourceScope: 'workspace-shared'` 选择上 ### 资源对话用户授权 -`workspace/chat-user.ts` 使用 Admin 请求客户端,通过资源的 `user_group` 查询和保存组自动授权, -通过 `user_group_id/` 查询分页用户及保存逐用户授权,沿用已有接口路径与请求体。 -使用 `ParamsPage`、`ResponsePage`,调用方直接读取已解包数据并管理 loading。 -公共授权类型维护在 `types/chat-user.ts`,经 `@/api/types` 导出。当前由工作空间智能体详情页调用; -仓库内开源后端尚未注册这组资源授权路径,实际联调依赖提供这些接口的后端环境。 +资源对话用户接口按四种范围维护 `chat-user.ts`,同一范围供智能体、知识库页面选用,不与 System 对话端用户管理接口混用: + +- `workspace/chat-user.ts`:`/workspace/{workspaceId}/{resource_type}/{resource_id}`。 +- `workspace/shared/chat-user.ts`:`/system/shared/workspace/{workspaceId}/{resource_type}/{resource_id}`,仅查询。 +- `system/resource-management/chat-user.ts`:`/system/resource/knowledge/{resource_type}/{resource_id}`。 +- `system/shared-resources/chat-user.ts`:`/system/shared/knowledge/{resource_type}/{resource_id}`。 + +后三套沿用 V2 可核对的接口路径并标注 V3 联调 TODO;Workspace 共享在 V2 只核对到 KNOWLEDGE 查询,不能据此认定共享智能体接口已支持。 +System 前缀中的额外 `knowledge` 段暂按 V2 保留,不能依据普通资源接口自行删减。 +资源类型使用 `RESOURCE_TYPE.APPLICATION / KNOWLEDGE` 的大写值;后台按原值读写授权记录,不会将小写转换为大写。 +仓库中的 `system_manage/urls.py` 已注册普通 Workspace 授权路径,其他范围需在提供对应接口的后端环境联调。 + +通过 `user_group` 查询和保存组自动授权,通过 `user_group_id/` 保存逐用户授权, +分页查询在其后追加 `//`;沿用 `ParamsPage`、`ResponsePage` 和原有数组请求体。 +调用方直接读取已解包数据并管理 loading;共享只读 API 不补写方法以凑齐接口集合。 +公共类型维护在 `types/chat-user.ts`,经 `@/api/types` 导出;用户组当前没有已授权人数统计字段,待后台确定字段后补充,不用分页用户数据推算全组人数。 + +当前智能体、知识库对话用户页仅直接调用普通 `workspace/chat-user.ts`,分别传自己的资源类型和路由 ID,暂不接入详情 context。 +其他三个范围的 API 文件保留,但页面暂不选择调用;页面不判断资源范围,当前请求统一使用 Workspace API,其他入口的 API 切换尚未接入。 ### 智能体对话日志 @@ -759,6 +773,27 @@ Web 知识库详情及更新响应的顶层 `doc_strategy` 使用 `DocumentStrat `KnowledgeItem` 声明;非 Web 类型返回 null。文档处理策略设置通过 `putKnowledge` 仅提交 `{ doc_strategy }`,服务端合并到知识库 meta,保留其他来源配置,保存不立即发起同步。 +### 知识库外部检索服务 + +四个知识库范围分别在自己的 `external-retrieval.ts` 中维护相同的查询与保存方法,仅资源前缀不同: + +- Workspace:`/workspace//knowledge/`。 +- Workspace 共享:`/system/shared/workspace//knowledge/`。 +- System 资源管理:`/system/resource/knowledge/`。 +- System 共享资源:`/system/shared/knowledge/`。 + +`getKnowledgeExternalRetrieval(knowledgeId)` 通过 GET `<资源前缀>/external_service` 查询配置; +`putKnowledgeExternalRetrieval(knowledgeId, payload)` 通过 PUT 同一路径保存开关。 +工作空间 ID 在调用时由 API 内读取,四套均使用 Admin 请求封装,不返回占位 null 或回退其他范围。 + +联调 TODO 统一保留在两个方法处:查询返回 `enabled`、`authentication`、`api_url`、`api_example`、`mcp_url`、`mcp_config`; +`api_example` 为后台生成的完整 API 调用示例文本(string),前端仅展示和复制,不拼接请求内容。 +保存只更新 payload 传入的 `enabled` 或 `authentication`,保留未提交字段,并返回包含最新 `api_example` 的完整配置。 +响应与提交类型共用 `types/knowledge-external-retrieval.ts`,通过 `@/api/types` 导入;请求层统一解包 `data`。 +页面已实现加载、开关保存、回显与只读内容复制;接口字段有差异时在各自 API 模块适配,不要求联调人员补写页面交互。 +当前页面仅直接调用普通 `workspace/knowledge/external-retrieval.ts`,暂不接入详情 context; +其他三个范围的 API 文件保留,页面暂不请求并禁用开关、显示待接入提示,不回退普通 Workspace。 + ### 知识库定时同步 `setting/scheduled-sync/` 页面调用设置查询、保存和日志分页接口;保存返回的规范化配置用于回填。 diff --git a/ui/src/api/admin/system/resource-management/chat-user.ts b/ui/src/api/admin/system/resource-management/chat-user.ts new file mode 100644 index 00000000000..a5decd9657f --- /dev/null +++ b/ui/src/api/admin/system/resource-management/chat-user.ts @@ -0,0 +1,30 @@ +import { get, put } from '../../core/request' +import type { ParamsPage, ResponsePage } from '../../core/types' +import type { + ChatUserAuthorization, + ChatUserAuthorizationGroup, + ChatUserAuthorizationResource, + ChatUserGroupAuthorizationPayload, + ChatUserAuthorizationPayload, + Dict, +} from '@/api/types' + +// TODO:V3 联调确认授权前缀;当前沿用 V2 的 /system/resource/knowledge/{resource_type}。 +const getPrefix = (resource: ChatUserAuthorizationResource) => `/system/resource/knowledge/${resource.resource_type}/${resource.resource_id}` + +/** 获取资源的对话用户组及自动授权状态。 */ +const getUserGroupList = (resource: ChatUserAuthorizationResource) => get(`${getPrefix(resource)}/user_group`) + +/** 保存资源的用户组自动授权设置。 */ +const putUserGroupAuthorization = (resource: ChatUserAuthorizationResource, data: ChatUserGroupAuthorizationPayload[]) => + put(`${getPrefix(resource)}/user_group`, data) + +/** 分页查询用户组内的对话用户及资源授权状态。 */ +const getUserGroupUserList = (resource: ChatUserAuthorizationResource, userGroupId: string, page: ParamsPage, query?: Dict) => + get>(`${getPrefix(resource)}/user_group_id/${userGroupId}/${page.currentPage}/${page.pageSize}`, query) + +/** 保存用户组内的对话用户资源授权。 */ +const putUserGroupUser = (resource: ChatUserAuthorizationResource, userGroupId: string, data: ChatUserAuthorizationPayload[]) => + put(`${getPrefix(resource)}/user_group_id/${userGroupId}`, data) + +export default { getUserGroupList, putUserGroupAuthorization, getUserGroupUserList, putUserGroupUser } diff --git a/ui/src/api/admin/system/resource-management/knowledge/external-retrieval.ts b/ui/src/api/admin/system/resource-management/knowledge/external-retrieval.ts new file mode 100644 index 00000000000..2f34850d4ee --- /dev/null +++ b/ui/src/api/admin/system/resource-management/knowledge/external-retrieval.ts @@ -0,0 +1,16 @@ +import { get, put } from '@/api/admin/core/request' +import type { KnowledgeExternalRetrievalPayload, KnowledgeExternalRetrievalSetting } from '@/api/types' + +const getPrefix = (knowledgeId: string) => `/system/resource/knowledge/${knowledgeId}` + +/** 获取知识库外部检索服务配置。 */ +// TODO:联调确认响应包含 enabled、authentication、api_url、api_example、mcp_url、mcp_config。 +// api_example 为后台生成的完整示例文本(string),前端直接展示和复制,不拼接请求内容。 +const getKnowledgeExternalRetrieval = (knowledgeId: string) => get(`${getPrefix(knowledgeId)}/external_service`) + +/** 保存知识库外部检索服务开关。 */ +// TODO:只更新 payload 中的开关,保留未提交字段,并返回包含最新 api_example 的完整配置。 +const putKnowledgeExternalRetrieval = (knowledgeId: string, payload: KnowledgeExternalRetrievalPayload) => + put(`${getPrefix(knowledgeId)}/external_service`, payload) + +export default { getKnowledgeExternalRetrieval, putKnowledgeExternalRetrieval } diff --git a/ui/src/api/admin/system/shared-resources/chat-user.ts b/ui/src/api/admin/system/shared-resources/chat-user.ts new file mode 100644 index 00000000000..8ddf2921544 --- /dev/null +++ b/ui/src/api/admin/system/shared-resources/chat-user.ts @@ -0,0 +1,30 @@ +import { get, put } from '../../core/request' +import type { ParamsPage, ResponsePage } from '../../core/types' +import type { + ChatUserAuthorization, + ChatUserAuthorizationGroup, + ChatUserAuthorizationResource, + ChatUserGroupAuthorizationPayload, + ChatUserAuthorizationPayload, + Dict, +} from '@/api/types' + +// TODO:V3 联调确认授权前缀;当前沿用 V2 的 /system/shared/knowledge/{resource_type}。 +const getPrefix = (resource: ChatUserAuthorizationResource) => `/system/shared/knowledge/${resource.resource_type}/${resource.resource_id}` + +/** 获取资源的对话用户组及自动授权状态。 */ +const getUserGroupList = (resource: ChatUserAuthorizationResource) => get(`${getPrefix(resource)}/user_group`) + +/** 保存资源的用户组自动授权设置。 */ +const putUserGroupAuthorization = (resource: ChatUserAuthorizationResource, data: ChatUserGroupAuthorizationPayload[]) => + put(`${getPrefix(resource)}/user_group`, data) + +/** 分页查询用户组内的对话用户及资源授权状态。 */ +const getUserGroupUserList = (resource: ChatUserAuthorizationResource, userGroupId: string, page: ParamsPage, query?: Dict) => + get>(`${getPrefix(resource)}/user_group_id/${userGroupId}/${page.currentPage}/${page.pageSize}`, query) + +/** 保存用户组内的对话用户资源授权。 */ +const putUserGroupUser = (resource: ChatUserAuthorizationResource, userGroupId: string, data: ChatUserAuthorizationPayload[]) => + put(`${getPrefix(resource)}/user_group_id/${userGroupId}`, data) + +export default { getUserGroupList, putUserGroupAuthorization, getUserGroupUserList, putUserGroupUser } diff --git a/ui/src/api/admin/system/shared-resources/knowledge/external-retrieval.ts b/ui/src/api/admin/system/shared-resources/knowledge/external-retrieval.ts new file mode 100644 index 00000000000..4ad8116526e --- /dev/null +++ b/ui/src/api/admin/system/shared-resources/knowledge/external-retrieval.ts @@ -0,0 +1,16 @@ +import { get, put } from '@/api/admin/core/request' +import type { KnowledgeExternalRetrievalPayload, KnowledgeExternalRetrievalSetting } from '@/api/types' + +const getPrefix = (knowledgeId: string) => `/system/shared/knowledge/${knowledgeId}` + +/** 获取知识库外部检索服务配置。 */ +// TODO:联调确认响应包含 enabled、authentication、api_url、api_example、mcp_url、mcp_config。 +// api_example 为后台生成的完整示例文本(string),前端直接展示和复制,不拼接请求内容。 +const getKnowledgeExternalRetrieval = (knowledgeId: string) => get(`${getPrefix(knowledgeId)}/external_service`) + +/** 保存知识库外部检索服务开关。 */ +// TODO:只更新 payload 中的开关,保留未提交字段,并返回包含最新 api_example 的完整配置。 +const putKnowledgeExternalRetrieval = (knowledgeId: string, payload: KnowledgeExternalRetrievalPayload) => + put(`${getPrefix(knowledgeId)}/external_service`, payload) + +export default { getKnowledgeExternalRetrieval, putKnowledgeExternalRetrieval } diff --git a/ui/src/api/admin/workspace/knowledge/external-retrieval.ts b/ui/src/api/admin/workspace/knowledge/external-retrieval.ts new file mode 100644 index 00000000000..33096a3325d --- /dev/null +++ b/ui/src/api/admin/workspace/knowledge/external-retrieval.ts @@ -0,0 +1,17 @@ +import { get, put } from '../../core/request' +import type { KnowledgeExternalRetrievalPayload, KnowledgeExternalRetrievalSetting } from '@/api/types' +import { getWorkspaceId } from '@/utils/resource-context' + +const getPrefix = (knowledgeId: string) => `/workspace/${getWorkspaceId()}/knowledge/${knowledgeId}` + +/** 获取知识库外部检索服务配置。 */ +// TODO:联调确认响应包含 enabled、authentication、api_url、api_example、mcp_url、mcp_config。 +// api_example 为后台生成的完整示例文本(string),前端直接展示和复制,不拼接请求内容。 +const getKnowledgeExternalRetrieval = (knowledgeId: string) => get(`${getPrefix(knowledgeId)}/external_service`) + +/** 保存知识库外部检索服务开关。 */ +// TODO:只更新 payload 中的开关,保留未提交字段,并返回包含最新 api_example 的完整配置。 +const putKnowledgeExternalRetrieval = (knowledgeId: string, payload: KnowledgeExternalRetrievalPayload) => + put(`${getPrefix(knowledgeId)}/external_service`, payload) + +export default { getKnowledgeExternalRetrieval, putKnowledgeExternalRetrieval } diff --git a/ui/src/api/admin/workspace/shared/chat-user.ts b/ui/src/api/admin/workspace/shared/chat-user.ts new file mode 100644 index 00000000000..a1917f0b644 --- /dev/null +++ b/ui/src/api/admin/workspace/shared/chat-user.ts @@ -0,0 +1,17 @@ +import { get } from '../../core/request' +import type { ParamsPage, ResponsePage } from '../../core/types' +import type { ChatUserAuthorization, ChatUserAuthorizationGroup, ChatUserAuthorizationResource, Dict } from '@/api/types' +import { getWorkspaceId } from '@/utils/resource-context' + +// TODO:V3 联调确认共享授权路径;当前沿用 V2 的大写资源类型,已核对 KNOWLEDGE 查询。 +const getPrefix = (resource: ChatUserAuthorizationResource) => + `/system/shared/workspace/${getWorkspaceId()}/${resource.resource_type}/${resource.resource_id}` + +/** 查询工作空间共享资源的用户组授权状态。 */ +const getUserGroupList = (resource: ChatUserAuthorizationResource) => get(`${getPrefix(resource)}/user_group`) + +/** 分页查询共享资源的组内用户授权;共享范围不提供写接口。 */ +const getUserGroupUserList = (resource: ChatUserAuthorizationResource, userGroupId: string, page: ParamsPage, query?: Dict) => + get>(`${getPrefix(resource)}/user_group_id/${userGroupId}/${page.currentPage}/${page.pageSize}`, query) + +export default { getUserGroupList, getUserGroupUserList } diff --git a/ui/src/api/admin/workspace/shared/knowledge/external-retrieval.ts b/ui/src/api/admin/workspace/shared/knowledge/external-retrieval.ts new file mode 100644 index 00000000000..c2ee375796b --- /dev/null +++ b/ui/src/api/admin/workspace/shared/knowledge/external-retrieval.ts @@ -0,0 +1,17 @@ +import { get, put } from '../../../core/request' +import type { KnowledgeExternalRetrievalPayload, KnowledgeExternalRetrievalSetting } from '@/api/types' +import { getWorkspaceId } from '@/utils/resource-context' + +const getPrefix = (knowledgeId: string) => `/system/shared/workspace/${getWorkspaceId()}/knowledge/${knowledgeId}` + +/** 获取知识库外部检索服务配置。 */ +// TODO:联调确认响应包含 enabled、authentication、api_url、api_example、mcp_url、mcp_config。 +// api_example 为后台生成的完整示例文本(string),前端直接展示和复制,不拼接请求内容。 +const getKnowledgeExternalRetrieval = (knowledgeId: string) => get(`${getPrefix(knowledgeId)}/external_service`) + +/** 保存知识库外部检索服务开关。 */ +// TODO:只更新 payload 中的开关,保留未提交字段,并返回包含最新 api_example 的完整配置。 +const putKnowledgeExternalRetrieval = (knowledgeId: string, payload: KnowledgeExternalRetrievalPayload) => + put(`${getPrefix(knowledgeId)}/external_service`, payload) + +export default { getKnowledgeExternalRetrieval, putKnowledgeExternalRetrieval } diff --git a/ui/src/api/types/chat-user.ts b/ui/src/api/types/chat-user.ts index 5634310ee04..f84207538c7 100644 --- a/ui/src/api/types/chat-user.ts +++ b/ui/src/api/types/chat-user.ts @@ -1,6 +1,6 @@ /** 对话用户 API 与管理页面共用的业务类型。 */ -import { PERIOD_TYPE, QUOTA_TYPE } from '@/api/enums' +import { PERIOD_TYPE, QUOTA_TYPE, RESOURCE_TYPE } from '@/api/enums' export interface ChatUserBase { id: string @@ -104,7 +104,7 @@ export interface BatchSetChatUserQuotaResult { /** 对话用户授权所属的资源。 */ export interface ChatUserAuthorizationResource { resource_id: string - resource_type: string + resource_type: (typeof RESOURCE_TYPE)['APPLICATION' | 'KNOWLEDGE'] } /** 资源内的对话用户组自动授权状态。 */ diff --git a/ui/src/api/types/index.ts b/ui/src/api/types/index.ts index c81acc9f77b..75143806d8b 100644 --- a/ui/src/api/types/index.ts +++ b/ui/src/api/types/index.ts @@ -19,6 +19,7 @@ export type * from './folder' export type * from './tool' export type * from './knowledge' export type * from './knowledge-sync' +export type * from './knowledge-external-retrieval' export type * from './trigger' export type * from './related-resources' export type * from './workflow-version' diff --git a/ui/src/api/types/knowledge-external-retrieval.ts b/ui/src/api/types/knowledge-external-retrieval.ts new file mode 100644 index 00000000000..7dde1d96d4f --- /dev/null +++ b/ui/src/api/types/knowledge-external-retrieval.ts @@ -0,0 +1,17 @@ +import type { Dict } from './common' + +/** 知识库外部检索服务配置。 */ +export interface KnowledgeExternalRetrievalSetting { + enabled: boolean + authentication: boolean + api_url: string + api_example: string + mcp_url: string + mcp_config: Dict +} + +/** 每次只提交发生变化的开关,保留其他配置。 */ +export interface KnowledgeExternalRetrievalPayload { + enabled?: boolean + authentication?: boolean +} diff --git a/ui/src/views/VIEW_README.md b/ui/src/views/VIEW_README.md index 7853b6064b2..63b4bb9bdfd 100644 --- a/ui/src/views/VIEW_README.md +++ b/ui/src/views/VIEW_README.md @@ -487,6 +487,7 @@ Dialog。新增或重命名文件时,应同步更新所有导入和页面功 | `home/index.vue` | Workspace 首页 | | `knowledge/index.vue` | 工作空间知识库目录与知识库卡片页面 | | `knowledge-detail/index.vue` | 知识库详情页面 | +| `knowledge-detail/external-retrieval/index.vue` | 外部检索服务开关、API 地址与调用示例、MCP 配置的只读展示及复制 | | `knowledge-detail/library/tags/index.vue` | 标签分组查询、创建、重命名、单值编辑、删除与关联文档管理 | | `knowledge-detail/workflow-transform/index.vue` | 非工作流知识库转换说明、确认与进入画布 | | `document-detail/index.vue` | 普通 Workspace 文档分段查询、目录定位、批量操作、新增、编辑与详情 | @@ -517,6 +518,10 @@ Dialog。新增或重命名文件时,应同步更新所有导入和页面功 | `trigger/index.vue` | 工作空间触发器查询、新建、编辑及单项和批量启停、删除 | | `tool/index.vue` | 工作空间工具目录与工具卡片页面 | +身份与权限用户组页(`system/identity/groups/index.vue`)的成员顶部搜索仅提供用户名、姓名; +用户来源使用 `MkTableFilter mode="single"` 放在表头,与搜索条件合并后向接口传 `source`。 +筛选变化回到第一页;选择“全部”移除来源条件,保留用户名、姓名搜索。 + ## 备注要求 门户设置的 `components/ButtonEditPortal.vue`、`ButtonPortalAuthSetting.vue` 和 @@ -737,7 +742,7 @@ Workspace API 内部通过 `getWorkspaceId()` 读取当前路由工作空间。 对话用户、外部检索服务。资料库页面统一放在 `knowledge-detail/library/` 下,包含 `document/`、 `image/`、`tags/`;文档专属组件及 Action 随 `document/` 集中维护。其他页面继续放在 `knowledge-detail/` 下的 `recall-test/`、`question/`、`dictionary/`、`chat-user/`、`external-retrieval/`。 -图片及这五个页面暂只展示占位内容。 +对话用户已实现按用户组查询及授权,接口与交互规则见“智能体与知识库对话用户”章节。 容器通过 `knowledge-detail/context.ts` 装配资源范围、知识库 ID、设置所需的完整业务 API 对象和返回列表路由, 并提供只读详情与替换能力。详情入口与 setting 的 API 选择集中在该上下文;library 保持原有接口调用方式。 Action、Drawer、Dialog 继续通过 props 接收完整 API。设置页复用已加载详情,保存成功后 @@ -1346,18 +1351,41 @@ Web 地址按行去空白、去重并校验 HTTP/HTTPS;空选择器提交 `bod 普通详情继续恢复所属文件夹。文档创建者选项在共享范围使用 System `CommonApi`,普通范围使用 Workspace `CommonApi`;范围变化同时清空搜索控件、筛选、创建者选项与分页。 -## 智能体对话用户 - -`application-detail/chat-user/index.vue` 通过 `hideHeader: true` 隐藏详情主区标题栏,负责工作空间智能体的用户组查询、 -用户搜索和分页、自动授权及用户授权保存。内部使用普通 Flex 双栏容器,不嵌套 `MkViewLayout`; -外层 `ResourceDetailLayout` 统一负责标题栏显隐和整体滚动,内部视觉排列与 System 用户组页面对齐。 -侧栏使用 `w-sidebar-expanded` 和 `MkSearchList`,由列表统一管理搜索、选中态、独立滚动和空态。 -右侧展示组名、成员数和自动授权;下方工具栏左侧保存、右侧 `MkComplexSearch`, -用户列表复用 `MkTable` 及其高度约束,无用户组时显示空态,不额外添加主区滚动容器。 -通过 `workspace/chat-user.ts` 请求;当前不接入 `perm`。授权勾选仅维护当前页,保存只提交当前页用户;翻页、筛选或切换组时重新读取服务端授权状态; -自动授权开启时禁用手动授权,切换成功后清空旧勾选并重新读取用户状态。筛选和用户组切换回到第一页, -页面通过 `onMounted` 加载用户组;切换工作空间由顶部入口整页加载,不额外监听路由重置状态, -也不为搜索组件设置工作空间或智能体 key。来源标签复用登录方式枚举与标签,兼容 `OAUTH2` 返回值。 +## 知识库外部检索服务 + +`knowledge-detail/external-retrieval/index.vue` 由四个资源范围共用,复用详情布局的自定义标题栏, +当前仅直接调用普通 `workspace/knowledge/external-retrieval.ts`,不新增 context 配置。 +通过 `isWorkspaceResource()` 限定请求范围;其他三个范围显示待接入提示、禁用开关,不回退普通 Workspace。 +普通 Workspace 提供身份认证及服务启停开关;查询与提交共用内容 loading, +开关仅在保存成功后按完整响应回显,每次只提交变化字段,失败保留原状态。 +API 地址、调用示例及 MCP 配置只读;后两个展示区参考工具 MCP 详情,使用文本域和悬浮复制按钮。 +地址与调用示例均来自接口;调用示例直接绑定 `api_example` 字符串,不在前端拼接或执行检索;MCP 配置直接格式化接口结果。 +四套 API 文件及联调 TODO 保留,均使用真实请求封装,不返回占位 null;其他范围的页面接入待后续处理。 +查询失败不回填配置,不允许在未知配置上保存;页面不伪造配置或保存成功。 + +## 智能体与知识库对话用户 + +`application-detail/chat-user/index.vue` 与 `knowledge-detail/chat-user/index.vue` 分别维护智能体、知识库的用户组和用户授权, +使用对应路由 ID 及 `RESOURCE_TYPE.APPLICATION / KNOWLEDGE`;授权类型必须大写,与后台授权记录及对话端查询一致。 +两页通过 `hideHeader: true` 隐藏详情标题栏,使用普通 Flex 双栏容器,不嵌套 `MkViewLayout`。 +外层 `ResourceDetailLayout` 统一管理整体滚动;侧栏复用 `w-sidebar-expanded`、`MkSearchList` 的搜索、选中和空态。 +“按用户组”列表通过插槽展示名称及自动授权状态;接口尚未返回组已授权人数,手动授权组显示“已授权 —”,不以当前页勾选数代替。 +右侧第一行展示组名和用户查询总数;下方工具栏左侧为默认按用户名查询的 `MkComplexSearch` 及自动授权开关,右侧为保存。 +顶部 `MkComplexSearch` 仅提供用户名、姓名;用户来源只在表头使用 `MkTableFilter mode="single"` 筛选,向接口传 `source`。 +用户名、姓名搜索与表头来源可组合查询,清空一处条件不影响另一处,不额外重置搜索组件。 +筛选变更回到第一页,选择“全部”移除来源条件。保留 `MkTable` 分页及高度约束。 + +两页当前仅直接调用普通 `workspace/chat-user.ts`,分别传自己的资源类型和路由 ID,不配置到详情 context。 +页面不判断资源范围,也不显示范围待接入提示;当前请求均使用 Workspace API,其他入口的 API 切换尚未接入。 +其他范围的 API 文件及联调 TODO 保留,页面接入待后续处理。 +当前不接入 `perm`。授权勾选只维护当前页,保存只提交当前页用户;翻页、筛选或切换组时重新读取服务端状态。 +无修改、无用户或自动授权开启时禁用保存;保存失败保留勾选,成功更新已保存快照并刷新当前用户列表,保留当前组与页码。 +自动授权切换立即保存,成功后更新组状态、清空旧用户与勾选快照,再查询用户;开启时禁用手动授权。 +保存与自动授权切换不重新查询用户组,避免用户刷新依赖无关请求。筛选和切换用户组回到第一页。 +用户组查询、用户查询与提交共用页面 loading;由各操作的最外层管理,内部查询不提前关闭遮罩。 +查询和提交期间页面容器通过 inert 防止交叉操作。 +页面通过 `onMounted` 加载用户组;工作空间切换由顶部入口整页加载,不额外监听路由重置。 +来源标签沿用登录方式枚举与现有“本地创建”文案,兼容 `OAUTH2` 返回值。 ## 智能体对话日志 diff --git a/ui/src/views/application-detail/chat-user/index.vue b/ui/src/views/application-detail/chat-user/index.vue index 8462e2370bf..769b5116597 100644 --- a/ui/src/views/application-detail/chat-user/index.vue +++ b/ui/src/views/application-detail/chat-user/index.vue @@ -3,19 +3,23 @@ import { computed, onMounted, ref } from 'vue' import { useRoute } from 'vue-router' import type { CheckboxValueType } from 'element-plus' import ChatUserApi from '@/api/admin/workspace/chat-user' -import { LOGIN_METHOD } from '@/api/enums' -import type { ChatUserAuthorization, ChatUserAuthorizationGroup, Dict, LoginMethod, OptionItem } from '@/api/types' +import { LOGIN_METHOD, RESOURCE_TYPE } from '@/api/enums' +import type { ChatUserAuthorization, ChatUserAuthorizationGroup, ChatUserAuthorizationResource, Dict, LoginMethod, OptionItem } from '@/api/types' import { LOGIN_METHOD_LABELS } from '@/constants' import MkSearchList from '@/components/mk-search-list/index.vue' import type { ResourceDetailPageProps } from '@/layout/ResourceDetailLayout.vue' import { MsgSuccess } from '@/utils/message' -defineOptions({ name: 'ChatUserListView' }) +defineOptions({ name: 'ApplicationChatUserListView' }) defineProps() defineExpose({ hideHeader: true }) const route = useRoute() -const resource = computed(() => ({ resource_id: String(route.params.applicationId ?? ''), resource_type: 'application' })) +const resource = computed(() => ({ + resource_id: String(route.params.applicationId ?? ''), + resource_type: RESOURCE_TYPE.APPLICATION, +})) + const loading = ref(false) // 用户组查询与切换 @@ -39,6 +43,7 @@ function handleSelectGroup(group: ChatUserAuthorizationGroup) { if (loading.value || currentGroup.value?.id === group.id) return currentGroup.value = group chatUsers.value = [] + savedAuthorization.value = {} paginationConfig.value.currentPage = 1 paginationConfig.value.total = 0 return refreshChatUsers() @@ -46,11 +51,12 @@ function handleSelectGroup(group: ChatUserAuthorizationGroup) { // 用户筛选与分页,仅维护当前页的授权勾选 const chatUsers = ref([]) +const savedAuthorization = ref>({}) const chatUserSearchQuery = ref>() const paginationConfig = ref({ currentPage: 1, pageSize: 20, total: 0 }) const searchFields: OptionItem[] = [ - { label: '姓名', value: 'nick_name' }, { label: '用户名', value: 'username' }, + { label: '姓名', value: 'nick_name' }, ] // 表头用户来源筛选 @@ -74,6 +80,7 @@ function loadChatUsers() { if (!currentGroup.value) return Promise.resolve() return ChatUserApi.getUserGroupUserList(resource.value, currentGroup.value.id, paginationConfig.value, chatUserQuery.value).then((page) => { chatUsers.value = page.records + savedAuthorization.value = Object.fromEntries(page.records.map(({ id, is_auth }) => [id, is_auth])) paginationConfig.value.total = page.total }) } @@ -100,8 +107,12 @@ function handleSourceChange() { const allChecked = computed(() => chatUsers.value.length > 0 && chatUsers.value.every((chatUser) => chatUser.is_auth)) const allIndeterminate = computed(() => !allChecked.value && chatUsers.value.some((chatUser) => chatUser.is_auth)) const authorizationDisabled = computed(() => loading.value || !currentGroup.value || currentGroup.value.is_auth) +const saveDisabled = computed( + () => authorizationDisabled.value || !chatUsers.value.some(({ id, is_auth }) => savedAuthorization.value[id] !== is_auth), +) function handleRowChange(value: CheckboxValueType, chatUser: ChatUserAuthorization) { + if (authorizationDisabled.value) return chatUser.is_auth = value === true } @@ -111,13 +122,15 @@ function handleCheckAll(value: CheckboxValueType) { function handleSave() { const group = currentGroup.value - if (!group || authorizationDisabled.value) return - const userGroupId = group.id + if (!group || saveDisabled.value) return const payload = chatUsers.value.map(({ id, is_auth }) => ({ chat_user_id: id, is_auth })) loading.value = true - return ChatUserApi.putUserGroupUser(resource.value, userGroupId, payload) + return ChatUserApi.putUserGroupUser(resource.value, group.id, payload) .then(() => { + // 先记住已保存结果,列表刷新失败时不重复提示未保存。 + savedAuthorization.value = Object.fromEntries(payload.map(({ chat_user_id, is_auth }) => [chat_user_id, is_auth])) MsgSuccess('保存成功') + return loadChatUsers() }) .finally(() => { loading.value = false @@ -134,9 +147,9 @@ function handleAutoAuthorizationChange(value: string | number | boolean) { .then(() => { group.is_auth = isAuth chatUsers.value = [] - return loadChatUsers().then(() => { - MsgSuccess('保存成功') - }) + savedAuthorization.value = {} + MsgSuccess('保存成功') + return loadChatUsers() }) .finally(() => { loading.value = false @@ -147,38 +160,46 @@ onMounted(() => loadChatUserGroups())