diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index e1dd15d7bfd..40e4b31cff6 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -285,7 +285,7 @@ Workspace、System 资源管理和 System 共享资源均在各自 `knowledge/kn 通用 / Web / 飞书 / 工作流创建、导入、删除、重新向量化、Web 同步、关联问题、分词索引、MCP 配置与三种导出。工作流创建在 Workspace 与 System 共享范围均由同一个 Knowledge API 的 `postKnowledgeWorkflow` 提交;工作流转换由各范围的 `knowledge.ts` 维护, -`workspace/knowledge/workflow.ts` 仅维护已创建知识库的画布接口。System 共享范围没有工作空间文件夹、批量接口或独立的知识库工作流 API 文件。复用创建入口 +`workspace/knowledge/workflow.ts` 仅维护已创建知识库的画布接口。System 共享范围没有工作空间文件夹或批量接口。复用创建入口 和卡片 Action 时使用完整 Workspace / System 资源管理 / System 共享知识库 API 的联合类型,由页面显式传入当前范围的 API。 ### 工具代码生成 @@ -329,13 +329,18 @@ putKnowledge 更新通用字段,Workspace 飞书使用 putLarkKnowledge;移 更换向量模型保存后通过 putReEmbeddingKnowledge 提交 embedding。Web 同步的 sync_type 为 incremental/replace/complete, 不自行改写协议值。Web doc_strategy 使用 DocumentStrategy,顶层详情可为 null,保存不立即同步。 -postTransformWorkflow POST `//transform_workflow`,提交 `{ work_flow }`,保留已有文档;不能用工作流保存接口代替。 +postTransformWorkflow POST `//transform_workflow`,提交 `{ work_flow }`,返回布尔值,保留已有文档; +已是工作流类型时拒绝重复转换,默认图复用知识库创建模板,不能用工作流保存接口代替。 知识库 Workflow API 通过 getKnowledgeDetail 的 work_flow 加载,putKnowledgeWorkflow 保存,发布接口单独调用。 默认模型协议使用 DefaultModelSettingPayload,知识库后端已保存 default_model_setting 并在详情回传。 导入/导出工作流专用 `.kbwf`,接口为 workflow/import(FormData file)与 workflow/export, 不能使用包含文档的知识库包接口。正式文档执行用 upload_document,调试用 debug,复用 KnowledgeWorkflowDebugPayload。 -知识库工作流 View 当前仍直接使用 Workspace Knowledge/Workflow API,其他来源仅有导航入口。 +四个范围各自维护 `knowledge/workflow.ts`,协议保持一致,前缀分别为 `/workspace//knowledge`、 +`/system/shared/workspace//knowledge`、`/system/resource/knowledge`、`/system/shared/knowledge`。 +工作流页面按路由 `resourceScope` 选择接口,通过 Props 传给调试、历史和执行记录组件。 +文档页正式导入链路保持原有接口装配。 +新增范围接口仍需部署端提供对应服务,尚未完成联调。 ### 资源用户授权 @@ -410,6 +415,9 @@ Workspace 与 System 资源管理的 `application/application.ts` 提供 `getAcc `//enable_api_key` 发送 PUT,仅提交 `{ enable_api_key: boolean }`,ID 由 URL 传递。 返回 `workspace_id`、`application_id` 与 `enable_api_key`,用于保存后回显;该开关独立于 Web 访问和单个密钥启停。 Workspace 后台已提供此接口;System 路径按资源管理前缀接入,当前仓库无对应后台路由,需部署环境联调确认。 + +#### 访问限制 + 访问限制复用上述 GET/PUT,读取与保存 `access_num`、`white_active`、`white_list`、`authentication` 和 `authentication_value`。提问次数为 0~10000000 的整数;社区版仅编辑认证开关,保留已有 `authentication_value`, 不生成登录配置字段,身份认证读写待后台联调。Xpack 版认证配置使用 `type: 'login'`、`login_value`、 @@ -420,12 +428,16 @@ Workspace 后台已提供此接口;System 路径按资源管理前缀接入, Xpack 版可用登录方式统一调用 `system/chat-management/chat-user-auth.ts` 的 `getChatUserAuthType`, GET `/chat_user/auth/types` 返回 `{ label, value }[]`,两种智能体入口共用。 +#### 展示设置 + 社区版展示设置通过 `putAccessToken` 提交 `language`、`show_source` 与 `show_exec`,不调用 `/setting`。 Xpack 展示设置共用 `ApplicationDisplaySetting`;`ApplicationAccessToken` 兼容可选展示字段,缺失字段由弹窗补齐默认值。 两范围的 `putDisplaySetting` 沿用 V2 的 `//setting` PUT 接口,提交 FormData: `custom_theme`、`float_location` 序列化为 JSON,图片字段提交本次文件,`_url` 保留已有地址; 恢复默认时图片文件与地址均提交空字符串。当前社区版后端未提供该路由,完整保存依赖支持此接口的部署。 +#### API Key + API Key 按三个入口分别维护: - `admin/workspace/application/api-key.ts`:`/workspace//application//application_key`。 @@ -445,12 +457,12 @@ Workspace 与 System 资源管理的 `application/overview.ts` 独立维护概 用户排行按首页协议预接入,以下分页、汇总及导出约定待后台实现,两套 API 均保留 TODO: -| 方法 | GET 路径(相对资源前缀) | 返回 | -| ---- | ----------------------- | ---- | -| `getRanking` | `/application_token_usage//` 或 `/top_questions//` | `ResponsePage` | -| `getTokensAggregation` | `/tokens/aggregation` | 数值 | -| `getChatRecordAggregation` | `/chat_record/aggregation` | 数值 | -| `exportRanking` | `/application_token_usage/export` 或 `/top_questions/export` | Excel 文件 | +| 方法 | GET 路径(相对资源前缀) | 返回 | +| -------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------- | +| `getRanking` | `/application_token_usage//` 或 `/top_questions//` | `ResponsePage` | +| `getTokensAggregation` | `/tokens/aggregation` | 数值 | +| `getChatRecordAggregation` | `/chat_record/aggregation` | 数值 | +| `exportRanking` | `/application_token_usage/export` 或 `/top_questions/export` | Excel 文件 | 排行类型 `ApplicationRankingKind` 为 `userTokens`/`questions`,两者均按用户统计,分别按 Tokens/对话次数降序。 分页返回 `records`、`total`、`current`、`size`;记录沿用首页字段,需提供唯一 `chat_user_id`、 @@ -534,16 +546,6 @@ System 接口由资源管理服务提供;本地开源后端没有对应扩展 前缀 `/system/shared/tool`,只接收资源 ID。两种 System 工具的 getToolListWithShared 查询各自 `/tool_list`,合并 tools/shared_tools;需相应部署支持扩展协议。 -### 知识库工作流转换 - -Workspace、System 资源管理及 System 共享资源的 `knowledge.ts` 提供 -`postTransformWorkflow(knowledgeId, { work_flow })`,沿用 v2 的 POST -`//transform_workflow`,各自使用对应知识库前缀,返回布尔值; -转换页通过可管理范围上下文的 `resourceApi.knowledge` 调用,不固定 Workspace API。 -服务端转换知识库类型并创建工作流, -保留已有文档,已是工作流类型时拒绝重复转换。页面负责确认、loading、成功提示及进入画布。 -默认图复用知识库创建模板,不用保存工作流接口代替转换。 - ### 文档与分段 四范围 document.ts、paragraph.ts 的对象由详情上下文装配。Workspace 与 System 两种范围提供完整维护, diff --git a/ui/src/api/admin/system/resource-management/knowledge/workflow.ts b/ui/src/api/admin/system/resource-management/knowledge/workflow.ts new file mode 100644 index 00000000000..a5a2df35189 --- /dev/null +++ b/ui/src/api/admin/system/resource-management/knowledge/workflow.ts @@ -0,0 +1,90 @@ +import type { ParamsPage, ResponsePage } from '@/api/admin/core/types' +import type LogicFlow from '@logicflow/core' +import { get, post, put, getExportFile } from '@/api/admin/core/request' +import type { + DefaultModelSettingPayload, + Dict, + KnowledgeExecutionRecord, + KnowledgeWorkflowAction, + KnowledgeWorkflowDebugPayload, + KnowledgeWorkflowDetail, + WorkflowStoreTemplate, + WorkflowVersion, + WorkflowVersionPayload, +} from '@/api/types' + +type KnowledgeWorkflowPayload = + | { default_model_setting?: DefaultModelSettingPayload; work_flow: LogicFlow.GraphConfigData; work_flow_template?: never } + | { work_flow_template: WorkflowStoreTemplate; work_flow?: never } + +const getPrefix = () => `/system/resource/knowledge` + +/** 保存知识库工作流。 */ +const putKnowledgeWorkflow = (knowledgeId: string, payload: KnowledgeWorkflowPayload) => { + return put(`${getPrefix()}/${knowledgeId}/workflow`, payload) +} + +/** 发布知识库工作流。 */ +const putKnowledgeWorkflowPublish = (knowledgeId: string) => { + return put(`${getPrefix()}/${knowledgeId}/publish`) +} + +/** 获取数据源节点的动态表单配置。 */ +const getKnowledgeWorkflowFormList = (knowledgeId: string, type: 'local' | 'tool', id: string, node: Dict) => { + return post<{ node: Dict }, Dict[]>(`${getPrefix()}/${knowledgeId}/datasource/${type}/${id}/form_list`, { node }) +} + +/** 提交知识库工作流调试任务。 */ +const postKnowledgeWorkflowDebug = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => { + return post(`${getPrefix()}/${knowledgeId}/debug`, payload) +} + +/** 使用已发布知识库工作流执行正式文档导入。 */ +const postKnowledgeWorkflowImport = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => + post(`${getPrefix()}/${knowledgeId}/upload_document`, payload) + +/** 获取知识库工作流执行详情,供调试轮询和执行记录共用。 */ +const getKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { + return get(`${getPrefix()}/${knowledgeId}/action/${actionId}`) +} + +/** 取消知识库工作流执行任务。 */ +const postCancelKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { + return post(`${getPrefix()}/${knowledgeId}/action/${actionId}/cancel`) +} + +/** 导出知识库工作流文件,不包含知识库文档。 */ +const exportKnowledgeWorkflow = (knowledgeId: string, name: string) => getExportFile(`${name}.kbwf`, `${getPrefix()}/${knowledgeId}/workflow/export`) + +/** 导入知识库工作流文件,覆盖当前草稿并由服务端处理关联工具。 */ +const importKnowledgeWorkflow = (knowledgeId: string, file: File) => { + const payload = new FormData() + payload.append('file', file) + return post(`${getPrefix()}/${knowledgeId}/workflow/import`, payload) +} + +/** 获取知识库发布历史,按发布时间倒序返回完整版本快照。 */ +const getWorkflowVersions = (knowledgeId: string) => get(`${getPrefix()}/${knowledgeId}/knowledge_version`) + +/** 修改知识库历史版本标题和更新说明,更新说明需要服务端支持 description 字段。 */ +const putWorkflowVersion = (knowledgeId: string, versionId: string, data: WorkflowVersionPayload) => + put(`${getPrefix()}/${knowledgeId}/knowledge_version/${versionId}`, data) + +/** 获取知识库工作流执行记录分页,支持发起人和状态筛选。 */ +const getKnowledgeExecutionRecordPage = (knowledgeId: string, page: ParamsPage, query?: Dict) => + get>(`${getPrefix()}/${knowledgeId}/action/${page.currentPage}/${page.pageSize}`, query) + +export default { + importKnowledgeWorkflow, + getKnowledgeExecutionRecordPage, + getWorkflowVersions, + putWorkflowVersion, + exportKnowledgeWorkflow, + putKnowledgeWorkflow, + putKnowledgeWorkflowPublish, + getKnowledgeWorkflowFormList, + postKnowledgeWorkflowDebug, + postKnowledgeWorkflowImport, + getKnowledgeWorkflowAction, + postCancelKnowledgeWorkflowAction, +} diff --git a/ui/src/api/admin/system/shared-resources/knowledge/workflow.ts b/ui/src/api/admin/system/shared-resources/knowledge/workflow.ts new file mode 100644 index 00000000000..0c80d20f80b --- /dev/null +++ b/ui/src/api/admin/system/shared-resources/knowledge/workflow.ts @@ -0,0 +1,90 @@ +import type { ParamsPage, ResponsePage } from '@/api/admin/core/types' +import type LogicFlow from '@logicflow/core' +import { get, post, put, getExportFile } from '@/api/admin/core/request' +import type { + DefaultModelSettingPayload, + Dict, + KnowledgeExecutionRecord, + KnowledgeWorkflowAction, + KnowledgeWorkflowDebugPayload, + KnowledgeWorkflowDetail, + WorkflowStoreTemplate, + WorkflowVersion, + WorkflowVersionPayload, +} from '@/api/types' + +type KnowledgeWorkflowPayload = + | { default_model_setting?: DefaultModelSettingPayload; work_flow: LogicFlow.GraphConfigData; work_flow_template?: never } + | { work_flow_template: WorkflowStoreTemplate; work_flow?: never } + +const getPrefix = () => `/system/shared/knowledge` + +/** 保存知识库工作流。 */ +const putKnowledgeWorkflow = (knowledgeId: string, payload: KnowledgeWorkflowPayload) => { + return put(`${getPrefix()}/${knowledgeId}/workflow`, payload) +} + +/** 发布知识库工作流。 */ +const putKnowledgeWorkflowPublish = (knowledgeId: string) => { + return put(`${getPrefix()}/${knowledgeId}/publish`) +} + +/** 获取数据源节点的动态表单配置。 */ +const getKnowledgeWorkflowFormList = (knowledgeId: string, type: 'local' | 'tool', id: string, node: Dict) => { + return post<{ node: Dict }, Dict[]>(`${getPrefix()}/${knowledgeId}/datasource/${type}/${id}/form_list`, { node }) +} + +/** 提交知识库工作流调试任务。 */ +const postKnowledgeWorkflowDebug = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => { + return post(`${getPrefix()}/${knowledgeId}/debug`, payload) +} + +/** 使用已发布知识库工作流执行正式文档导入。 */ +const postKnowledgeWorkflowImport = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => + post(`${getPrefix()}/${knowledgeId}/upload_document`, payload) + +/** 获取知识库工作流执行详情,供调试轮询和执行记录共用。 */ +const getKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { + return get(`${getPrefix()}/${knowledgeId}/action/${actionId}`) +} + +/** 取消知识库工作流执行任务。 */ +const postCancelKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { + return post(`${getPrefix()}/${knowledgeId}/action/${actionId}/cancel`) +} + +/** 导出知识库工作流文件,不包含知识库文档。 */ +const exportKnowledgeWorkflow = (knowledgeId: string, name: string) => getExportFile(`${name}.kbwf`, `${getPrefix()}/${knowledgeId}/workflow/export`) + +/** 导入知识库工作流文件,覆盖当前草稿并由服务端处理关联工具。 */ +const importKnowledgeWorkflow = (knowledgeId: string, file: File) => { + const payload = new FormData() + payload.append('file', file) + return post(`${getPrefix()}/${knowledgeId}/workflow/import`, payload) +} + +/** 获取知识库发布历史,按发布时间倒序返回完整版本快照。 */ +const getWorkflowVersions = (knowledgeId: string) => get(`${getPrefix()}/${knowledgeId}/knowledge_version`) + +/** 修改知识库历史版本标题和更新说明,更新说明需要服务端支持 description 字段。 */ +const putWorkflowVersion = (knowledgeId: string, versionId: string, data: WorkflowVersionPayload) => + put(`${getPrefix()}/${knowledgeId}/knowledge_version/${versionId}`, data) + +/** 获取知识库工作流执行记录分页,支持发起人和状态筛选。 */ +const getKnowledgeExecutionRecordPage = (knowledgeId: string, page: ParamsPage, query?: Dict) => + get>(`${getPrefix()}/${knowledgeId}/action/${page.currentPage}/${page.pageSize}`, query) + +export default { + importKnowledgeWorkflow, + getKnowledgeExecutionRecordPage, + getWorkflowVersions, + putWorkflowVersion, + exportKnowledgeWorkflow, + putKnowledgeWorkflow, + putKnowledgeWorkflowPublish, + getKnowledgeWorkflowFormList, + postKnowledgeWorkflowDebug, + postKnowledgeWorkflowImport, + getKnowledgeWorkflowAction, + postCancelKnowledgeWorkflowAction, +} diff --git a/ui/src/api/admin/workspace/shared/knowledge/workflow.ts b/ui/src/api/admin/workspace/shared/knowledge/workflow.ts new file mode 100644 index 00000000000..76ad059876b --- /dev/null +++ b/ui/src/api/admin/workspace/shared/knowledge/workflow.ts @@ -0,0 +1,91 @@ +import type { ParamsPage, ResponsePage } from '@/api/admin/core/types' +import type LogicFlow from '@logicflow/core' +import { get, post, put, getExportFile } from '@/api/admin/core/request' +import type { + DefaultModelSettingPayload, + Dict, + KnowledgeExecutionRecord, + KnowledgeWorkflowAction, + KnowledgeWorkflowDebugPayload, + KnowledgeWorkflowDetail, + WorkflowStoreTemplate, + WorkflowVersion, + WorkflowVersionPayload, +} from '@/api/types' +import { getWorkspaceId } from '@/utils/resource-context' + +type KnowledgeWorkflowPayload = + | { default_model_setting?: DefaultModelSettingPayload; work_flow: LogicFlow.GraphConfigData; work_flow_template?: never } + | { work_flow_template: WorkflowStoreTemplate; work_flow?: never } + +const getPrefix = () => `/system/shared/workspace/${getWorkspaceId()}/knowledge` + +/** 保存知识库工作流。 */ +const putKnowledgeWorkflow = (knowledgeId: string, payload: KnowledgeWorkflowPayload) => { + return put(`${getPrefix()}/${knowledgeId}/workflow`, payload) +} + +/** 发布知识库工作流。 */ +const putKnowledgeWorkflowPublish = (knowledgeId: string) => { + return put(`${getPrefix()}/${knowledgeId}/publish`) +} + +/** 获取数据源节点的动态表单配置。 */ +const getKnowledgeWorkflowFormList = (knowledgeId: string, type: 'local' | 'tool', id: string, node: Dict) => { + return post<{ node: Dict }, Dict[]>(`${getPrefix()}/${knowledgeId}/datasource/${type}/${id}/form_list`, { node }) +} + +/** 提交知识库工作流调试任务。 */ +const postKnowledgeWorkflowDebug = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => { + return post(`${getPrefix()}/${knowledgeId}/debug`, payload) +} + +/** 使用已发布知识库工作流执行正式文档导入。 */ +const postKnowledgeWorkflowImport = (knowledgeId: string, payload: KnowledgeWorkflowDebugPayload) => + post(`${getPrefix()}/${knowledgeId}/upload_document`, payload) + +/** 获取知识库工作流执行详情,供调试轮询和执行记录共用。 */ +const getKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { + return get(`${getPrefix()}/${knowledgeId}/action/${actionId}`) +} + +/** 取消知识库工作流执行任务。 */ +const postCancelKnowledgeWorkflowAction = (knowledgeId: string, actionId: string) => { + return post(`${getPrefix()}/${knowledgeId}/action/${actionId}/cancel`) +} + +/** 导出知识库工作流文件,不包含知识库文档。 */ +const exportKnowledgeWorkflow = (knowledgeId: string, name: string) => getExportFile(`${name}.kbwf`, `${getPrefix()}/${knowledgeId}/workflow/export`) + +/** 导入知识库工作流文件,覆盖当前草稿并由服务端处理关联工具。 */ +const importKnowledgeWorkflow = (knowledgeId: string, file: File) => { + const payload = new FormData() + payload.append('file', file) + return post(`${getPrefix()}/${knowledgeId}/workflow/import`, payload) +} + +/** 获取知识库发布历史,按发布时间倒序返回完整版本快照。 */ +const getWorkflowVersions = (knowledgeId: string) => get(`${getPrefix()}/${knowledgeId}/knowledge_version`) + +/** 修改知识库历史版本标题和更新说明,更新说明需要服务端支持 description 字段。 */ +const putWorkflowVersion = (knowledgeId: string, versionId: string, data: WorkflowVersionPayload) => + put(`${getPrefix()}/${knowledgeId}/knowledge_version/${versionId}`, data) + +/** 获取知识库工作流执行记录分页,支持发起人和状态筛选。 */ +const getKnowledgeExecutionRecordPage = (knowledgeId: string, page: ParamsPage, query?: Dict) => + get>(`${getPrefix()}/${knowledgeId}/action/${page.currentPage}/${page.pageSize}`, query) + +export default { + importKnowledgeWorkflow, + getKnowledgeExecutionRecordPage, + getWorkflowVersions, + putWorkflowVersion, + exportKnowledgeWorkflow, + putKnowledgeWorkflow, + putKnowledgeWorkflowPublish, + getKnowledgeWorkflowFormList, + postKnowledgeWorkflowDebug, + postKnowledgeWorkflowImport, + getKnowledgeWorkflowAction, + postCancelKnowledgeWorkflowAction, +} diff --git a/ui/src/api/chat/README.md b/ui/src/api/chat/README.md index dfea401bea9..00fb0355d5d 100644 --- a/ui/src/api/chat/README.md +++ b/ui/src/api/chat/README.md @@ -29,3 +29,8 @@ Chat 业务接口统一调用后端 v3 路由(`/chat/api/v3/...`);文件 公钥取当前认证配置的 `rsa_key`;成功后当前 token 失效,调用方清理认证状态并重新导航。 `getApiKey()` 与 `postApiKey()` 分别读取、重新生成 `/v3/api_key`,返回 `ChatUserApiKey`。 首次读取由后端创建密钥;每个用户仅保留一个,重新生成后旧密钥立即失效。 + +钉钉扫码登录 `getDingTalkCallback` 使用 `/auth/dingtalk`;客户端免密登录 +`getDingTalkOauthCallback` 使用 `/auth/dingtalk/oauth2`,传入 `code` 和可选的 `accessToken`。 +两者返回 `ChatLoginResponse`,由认证 Store 按场景保存 token;接口需部署端提供对应认证能力。 +`/auth/` 与 `/v3/auth/` 下的认证请求返回 401 时直接提示错误,不触发自动重新认证。 diff --git a/ui/src/api/chat/auth.ts b/ui/src/api/chat/auth.ts index 67e857ef413..b84e2790669 100644 --- a/ui/src/api/chat/auth.ts +++ b/ui/src/api/chat/auth.ts @@ -67,6 +67,11 @@ const getDingTalkCallback = (code: string, accessToken?: string) => { return get('/auth/dingtalk', accessToken ? { code, accessToken } : { code }) } +/** 使用钉钉客户端授权码登录;传入 accessToken 时登录到对应应用。 */ +const getDingTalkOauthCallback = (code: string, accessToken?: string) => { + return get('/auth/dingtalk/oauth2', accessToken ? { code, accessToken } : { code }) +} + /** * 获取应用登录验证码;无需验证码时返回空字符串。 * 后端以 `accessToken` 查询参数接收应用令牌,且仅支持应用场景。 @@ -89,6 +94,7 @@ export default { getExternalAuthSetting, getQrCodeSources, getDingTalkCallback, + getDingTalkOauthCallback, getCaptcha, postLogout, } diff --git a/ui/src/api/chat/core/request.ts b/ui/src/api/chat/core/request.ts index a2e0fb217e2..f47337b8938 100644 --- a/ui/src/api/chat/core/request.ts +++ b/ui/src/api/chat/core/request.ts @@ -80,7 +80,7 @@ request.interceptors.response.use( // token 失效时重新认证;认证接口自身的 401(如登录失败)仍按普通错误提示 const requestUrl = error.config?.url ?? '' const status = error.response?.status - if (status === 401 && !requestUrl.startsWith('/v3/auth/')) { + if (status === 401 && !requestUrl.startsWith('/v3/auth/') && !requestUrl.startsWith('/auth/')) { reauthenticate() return Promise.reject(error) } diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index dfc1b209199..5eab41a004f 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -228,40 +228,6 @@ Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载 首次加载或 `reset()` 请求第一页时,组件只显示加载状态;第一页完成且列表为空后才渲染 `empty` 插槽,避免请求过程中短暂显示空状态。已有数据时继续渲染默认插槽,并在触底请求期间保留列表。 -### ReverseInfiniteScroll(对话模块) - -位于 `conversation-panel/core/reverse-infinite-scroll/index.vue`,由对话模块显式导入, -不属于全局组件。内部使用 `el-scrollbar`,父级需要提供受限高度或纵向 Flex 剩余空间, -外层不再额外包裹消息滚动容器。 - -内容留白通过 `contentClass` 设置,例如 `content-class="p-4"`;该类作用于滚动区域内的内容层, -同时覆盖消息和空状态,使滚动条保持贴近容器边缘。`class` 仍用于组件外壳布局。 - -传入 `loading`、`hasMore`、`count` 和 `loadMore: () => Promise`。页码、请求和 -历史数据前插由调用方维护;`loadMore` 在历史数据更新后返回 `true`,失败或没有新增数据返回 -`false`。组件不请求首页;首页由调用方加载,首次出现数据时自动定位到底部,距顶部 60px 时 -串行加载历史。内容不足一屏时继续补页,失败或未新增数据时停止自动补页。 - -默认插槽放消息,`empty` 插槽放欢迎语或空状态,加载期间不显示空状态。 -`prepend` 插槽放消息与空状态共用的前置内容,位于顶部加载提示之后,首次加载期间隐藏; -是否等待历史全部加载后展示由调用方控制。 -首次加载使用整块遮罩;有消息时顶部预留状态行,加载期间使用 `mk-conversation-loading` -展示加载动画。失败后可再次主动上滑重试,流式回复状态由消息列表自行渲染。 -历史消息前插后按滚动高度差补偿阅读位置,不要求消息设置额外的 DOM 属性。 -`count` 清零会重置定位状态,切换会话时应通过 -组件 `key` 重建实例,以隔离未结束的补页与定位操作。 - -底部定位与跟随复用 `conversation-panel/core/scroll.ts` 的 `Scroll`;其他消息容器也从此文件导入, -容器卸载或替换时调用 `destroy()` 清理监听和动画帧。 -公开 `scrollToBottom()` 强制定位到底部、`followBottom()` 仅在用户位于底部附近时跟随。 -聊天列表订阅流式进度后调用 `followBottom()`;只读日志无需订阅。 -`message-list` 和 `log-message-list` 使用该容器,保留各自的消息正文与底部信息;聊天列表 -自行展示流式圆点,开场白放入 `prepend` 插槽,日志空状态放入 `empty` 插槽。日志列表不注入发送能力。 -消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`, -直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新 -`hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。 -Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID 为 `key`,不再包裹额外滚动层。 - ### MkListItem 统一列表行、选中态及悬浮操作区。仅自定义内容时传 `active` 并监听 `click` 即可。 @@ -954,6 +920,42 @@ Workspace API 内部使用 `getWorkspaceId()`,System 共享 API 不接收工 工作空间模型、知识库、应用、工具列表已分别通过 `RelatedResourcesModelAction`、 `RelatedResourcesKnowledgeAction`、`RelatedResourcesApplicationAction`、`RelatedResourcesToolAction` 接入。 +## 对话模块组件 + +### ReverseInfiniteScroll(对话模块) + +位于 `conversation-panel/core/reverse-infinite-scroll/index.vue`,由对话模块显式导入, +不属于全局组件。内部使用 `el-scrollbar`,父级需要提供受限高度或纵向 Flex 剩余空间, +外层不再额外包裹消息滚动容器。 + +内容留白通过 `contentClass` 设置,例如 `content-class="p-4"`;该类作用于滚动区域内的内容层, +同时覆盖消息和空状态,使滚动条保持贴近容器边缘。`class` 仍用于组件外壳布局。 + +传入 `loading`、`hasMore`、`count` 和 `loadMore: () => Promise`。页码、请求和 +历史数据前插由调用方维护;`loadMore` 在历史数据更新后返回 `true`,失败或没有新增数据返回 +`false`。组件不请求首页;首页由调用方加载,首次出现数据时自动定位到底部,距顶部 60px 时 +串行加载历史。内容不足一屏时继续补页,失败或未新增数据时停止自动补页。 + +默认插槽放消息,`empty` 插槽放欢迎语或空状态,加载期间不显示空状态。 +`prepend` 插槽放消息与空状态共用的前置内容,位于顶部加载提示之后,首次加载期间隐藏; +是否等待历史全部加载后展示由调用方控制。 +首次加载使用整块遮罩;有消息时顶部预留状态行,加载期间使用 `mk-conversation-loading` +展示加载动画。失败后可再次主动上滑重试,流式回复状态由消息列表自行渲染。 +历史消息前插后按滚动高度差补偿阅读位置,不要求消息设置额外的 DOM 属性。 +`count` 清零会重置定位状态,切换会话时应通过 +组件 `key` 重建实例,以隔离未结束的补页与定位操作。 + +底部定位与跟随复用 `conversation-panel/core/scroll.ts` 的 `Scroll`;其他消息容器也从此文件导入, +容器卸载或替换时调用 `destroy()` 清理监听和动画帧。 +公开 `scrollToBottom()` 强制定位到底部、`followBottom()` 仅在用户位于底部附近时跟随。 +聊天列表订阅流式进度后调用 `followBottom()`;只读日志无需订阅。 +`message-list` 和 `log-message-list` 使用该容器,保留各自的消息正文与底部信息;聊天列表 +自行展示流式圆点,开场白放入 `prepend` 插槽,日志空状态放入 `empty` 插槽。日志列表不注入发送能力。 +消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`, +直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新 +`hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。 +Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID 为 `key`,不再包裹额外滚动层。 + ## 文档维护 仅在目录、公开接口或使用规则变化时更新对应组件章节;不登记内部函数和修复过程。 diff --git a/ui/src/router/ROUTE_README.md b/ui/src/router/ROUTE_README.md index 34b464510af..8807e9f03fd 100644 --- a/ui/src/router/ROUTE_README.md +++ b/ui/src/router/ROUTE_README.md @@ -19,7 +19,8 @@ Workspace 使用 `AppLayout`,左侧导航根据 `scope: 'workspace'` 对应路由的 `children` 自动生成。 Workspace 根地址为 `/admin/workspace/:workspaceId`。`workspaceId` 只以当前路由参数为准。应用根 -地址和登录成功时进入 `/admin/workspace/default`。顶部 `LayoutHeader` 切换到 System 时将当前 +地址进入 `/admin/workspace/default`。登录成功使用替换导航返回 `redirect` 记录的站内原地址, +没有时进入 `/admin/workspace/default`。顶部 `LayoutHeader` 切换到 System 时将当前 工作空间 ID 保存到 localStorage 的 `workspaceId`;仅在该组件返回 Workspace 时读取缓存, 无缓存时使用用户首个工作空间或 `default`。退出登录时由 Admin `clearToken()` 清除缓存。 其他位置不得读取该缓存,工作空间上下文仍以路由参数为准。若当前用户 @@ -89,7 +90,7 @@ Admin Router 在 `admin/index.ts` 中统一处理导航初始化: 2. `login`、`forgot-password`、`not-found` 为公开路由,不校验登录状态。 3. URL 查询参数包含 `token` 时,先写入 Admin 认证 Store,用于外部认证回跳,然后使用替换 导航清理地址栏中的 token。 -4. 访问非公开路由且没有 token 时跳转到 `login`,并通过 `redirect` 查询参数保留原地址。 +4. 访问非公开路由且没有 token 时跳转到 `login`,并通过 `redirect` 查询参数保留原地址,同时透传钉钉客户端入口参数 `client`、`corpId`。 5. token 有效但当前用户尚未加载时,统一加载平台基础档案和当前用户;专业版或 企业版同时加载服务端外观主题,其他版本恢复默认主题,使刷新页面也能恢复版本、用户和 外观状态。 @@ -132,7 +133,7 @@ Chat 导航守卫(`chat/index.ts`): 6. `enable_auth` 为 false 时获取匿名 token,切换当前场景后放行。匿名认证与登录都携带该场景本地保存的 token,后端据此沿用同一对话用户。401 时 token 只在 Store 中清除并标记为失效,本地存储保留以便 重新认证时携带;退出登录才从本地存储删除。 -7. 否则跳转到对应登录页(`portal-login` 或 `chat-login`),并通过 `redirect` 查询参数保留原地址。 +7. 否则跳转到对应登录页(`portal-login` 或 `chat-login`),并通过 `redirect` 查询参数保留原地址,同时透传钉钉客户端入口参数 `client`、`corpId`。 当前场景只在放行前切换,请求层直接读取 `auth.token`,导航未完成时不会带上目标场景的 token。 @@ -157,6 +158,7 @@ Chat 导航守卫(`chat/index.ts`): | `hidden` | `boolean` | 设置为 `true` 时不显示在导航中 | | `detailMenuVisible` | `(params) => boolean` | 按业务类型过滤详情菜单,不用于权限 | | `canAccess` | `(params) => boolean` | 工作流返回详情时的访问判断 | +| `permission` | `(route: { params }) => boolean` | 菜单与导航守卫共用的访问判断 | | `resource` | `ResourceAuthorizationType` | 系统资源授权页面当前管理的后端资源类型 | Workspace 和 System 资源知识库的详情子路由使用现有 `meta.permission` 访问判断;二级菜单应用相同判断并隐藏空分组。 @@ -199,13 +201,13 @@ System 侧栏优先使用 `route.meta.activeMenu`,未配置时使用当前页 ## Scope 来源判断 -Layout 对 Workspace、System 模式的判断统一维护在 `src/layout/utils.ts`: +Layout 对 Workspace、System 模式的判断统一维护在 `src/layout/utils.ts`。 `scope` 来自 Vue Router 合并后的路由 `meta`。Workspace、System 子页面会分别继承根路由的 `workspace`、`system`。 `getChildRouteList` 属于公共路由工具,继续维护在 `admin/utils.ts`;仅供 Layout 使用的模式判断不要放入 Router。 -路由对象是响应式的,不要在 setup 顶层解构 `route.meta.scope`,否则切换路由后 Scope 不会更新。需要响应路由变化时,应在 `computed` 中读取: +路由对象是响应式的,不要在 setup 顶层解构 `route.meta.scope`,否则切换路由后 Scope 不会更新。需要响应路由变化时,在 `computed` 中读取 `route.meta.scope`。 ## 资源范围判断 diff --git a/ui/src/router/admin/index.ts b/ui/src/router/admin/index.ts index 74c149ee7f5..3d3c5932981 100644 --- a/ui/src/router/admin/index.ts +++ b/ui/src/router/admin/index.ts @@ -41,7 +41,7 @@ router.beforeEach(async (to, from) => { } if (!auth.isAuthenticated) { auth.clearToken() - return { name: 'login', query: { redirect: to.fullPath } } + return { name: 'login', query: { redirect: to.fullPath, client: to.query.client, corpId: to.query.corpId } } } if (!user.userInfo) { await auth.loadAuthBaseProfile() diff --git a/ui/src/router/chat/index.ts b/ui/src/router/chat/index.ts index a2a44481be2..6cd01df4543 100644 --- a/ui/src/router/chat/index.ts +++ b/ui/src/router/chat/index.ts @@ -24,7 +24,7 @@ function isUnknownPortalPath(to: RouteLocationNormalized) { } function getLoginRoute(scope: string, to: RouteLocationNormalized) { - const query = { redirect: to.fullPath } + const query = { redirect: to.fullPath, client: to.query.client, corpId: to.query.corpId } return scope === PORTAL_AUTH_SCOPE ? { name: 'portal-login', query } : { name: 'chat-login', params: { accessToken: scope }, query } } diff --git a/ui/src/stores/admin/auth.ts b/ui/src/stores/admin/auth.ts index 86b7c1fc58a..8ab018ca545 100644 --- a/ui/src/stores/admin/auth.ts +++ b/ui/src/stores/admin/auth.ts @@ -36,9 +36,17 @@ export const useAuthStore = defineStore('auth', { return LoginApi.postLdapLogin(loginRequest).then((response) => this.setToken(response.token)) }, - asyncLoginWithDingTalk(code: string, fromClient = false) { - const loginRequest = fromClient ? ExternalLoginApi.getDingTalkOauthCallback(code) : ExternalLoginApi.getDingTalkCallback(code) - return loginRequest.then((response) => this.setToken(response.token)) + /** 使用钉钉扫码授权码登录。 */ + asyncLoginWithDingTalk(code: string) { + return ExternalLoginApi.getDingTalkCallback(code).then((response) => this.setToken(response.token)) + }, + + /** 使用钉钉客户端授权码登录。 */ + loginWithDingTalkClient(code: string) { + return ExternalLoginApi.getDingTalkOauthCallback(code).then(({ token }) => { + this.setToken(token) + return token + }) }, asyncLoginWithLark(code: string) { diff --git a/ui/src/stores/chat/auth.ts b/ui/src/stores/chat/auth.ts index 8b83be6cc3c..c4ad7c79e12 100644 --- a/ui/src/stores/chat/auth.ts +++ b/ui/src/stores/chat/auth.ts @@ -128,6 +128,15 @@ export const useChatAuthStore = defineStore('chat-auth', { }) }, + /** 使用钉钉客户端授权码进行免密登录。 */ + loginWithDingTalkClient(code: string) { + const scope = this.scope + return ChatAuthApi.getDingTalkOauthCallback(code, getAccessToken(scope)).then(({ token }) => { + this.setToken(token, scope) + return token + }) + }, + /** 退出当前场景的登录状态。 */ logout() { return ChatAuthApi.postLogout().then(() => this.clearToken()) diff --git a/ui/src/utils/UTILS_README.md b/ui/src/utils/UTILS_README.md index 9492aef4144..cc9b5a0e5fa 100644 --- a/ui/src/utils/UTILS_README.md +++ b/ui/src/utils/UTILS_README.md @@ -3,22 +3,7 @@ `src/utils` 只存放大部分页面或多个业务模块都会使用的通用函数。只被个别页面、组件或业务 模块使用的函数,应保留在所属代码附近,不要提前提升为全局工具。 -工具函数按照明确的业务领域或能力拆分文件,例如: - -```text -src/utils/ -├── UTILS_README.md # utils 目录的放置、拆分、命名和注释规则 -├── array.ts # 跨页面复用的数组转换、筛选、去重等处理函数 -├── clipboard.ts # 剪贴板文本复制和成功反馈 -├── icon.ts # 文件后缀识别、类型判断和图标匹配 -├── message.ts # Element Plus 全局消息提示的统一封装 -├── number.ts # 跨页面复用的数字计算、转换和格式化函数 -├── resource-context.ts # 当前路由的资源范围和工作空间上下文判断 -├── time.ts # 跨页面复用的日期时间计算与格式化函数 -├── use-responsive.ts # 需要同步组件状态时使用的响应式屏幕判断 -├── use-sortable.ts # 列表排序、动态容器绑定和可选的独立数据写回 -└── vnode.ts # Vue 插槽 VNode 的可渲染内容判断 -``` +工具按业务领域或能力拆分;现有 common.ts 的兼容工具见下文,新工具不继续向通用文件堆积。 ## 使用约定 diff --git a/ui/src/views/VIEW_README.md b/ui/src/views/VIEW_README.md index adac7aecd76..3581dfed473 100644 --- a/ui/src/views/VIEW_README.md +++ b/ui/src/views/VIEW_README.md @@ -89,12 +89,12 @@ - `ToolCard` 点击工作流进入画布,其他类型与编辑菜单共用页面级 `EditToolForm` 实例, 不依赖下拉菜单挂载或维护逐卡片 Action Ref;`EditToolAction` 复用相同表单调度组件。 表单按类型打开,关闭动画后卸载具体表单;编辑、启停成功局部更新。 + 工具商店列表由页面加载,卡片和版本更新按钮不重复查询。 - 智能体的 Workspace 卡片与 System 资源列表复用门户上下架和导出 Action;门户入口按 `is_portal` 显示“上架到门户/从门户下架”,通过当前范围 API 保存,成功后按响应更新列表局部状态,失败保留原状态。 未上架时仅 `is_publish` 为 true 才显示并允许上架;已上架时保留下架入口,下架不修改发布状态。 导出使用二级菜单,包含“导出为 Skills”和“导出智能体”;Skills 当前调用 mock 接口并明确提示 尚未生成文件,智能体沿用现有文件导出。 - 商店列表由页面加载,卡片和版本更新按钮不重复查询。 - 工具头像由 `MkEditAvatar` 暂存文件,校验后先上传再保存;上传成功后清理暂存文件,避免保存失败后重复上传。 - Python 生成入口由 `CodeGenerate` 选择 Tool/Model API,复用 `GenerateContent`;全屏替换只修改草稿,确认后回写。 - System 模型/工具使用表格,保留既有权限、引用数量删除提示和末页回退;不新增不存在的创建/导入能力。 @@ -111,11 +111,6 @@ ## 详情布局与知识库上下文 -智能体发布渠道位于 `application-detail/access/`,支持企业微信智能机器人、企业微信应用、钉钉、 -公众号、飞书和 Slack。平台字段与后台指引在功能本地 `platforms.ts` 维护;卡片按配置状态显示 -启停开关,保存状态成功后更新显示。配置抽屉按需挂载并在关闭动画后卸载,每次打开重新查询, -保存失败保留草稿。公众号提供认证开关,飞书 Verification Token 可选;回调 URL 只读并支持复制。 - `layout/ResourceDetailLayout.vue` 提供侧栏和固定标题栏。子页声明 `ResourceDetailPageProps`, 普通页使用默认标题;`defineExpose({ customHeader: true })` 配合 Teleport 到 `headerTarget` 自定义标题、搜索及操作;`hideHeader: true` 隐藏整个标题栏且优先于自定义标题。 @@ -134,6 +129,13 @@ 提交入口同时阻止共享写入。System 文档、标签与分段 API 已装配,具体服务能力仍需对应部署支持。 知识库全屏 WorkflowView 的加载、保存、模型查询仍直接使用 Workspace API,其他来源仅完成导航,不能视为范围接入完成。 +### 智能体发布渠道 + +智能体发布渠道位于 `application-detail/access/`,支持企业微信智能机器人、企业微信应用、钉钉、 +公众号、飞书和 Slack。平台字段与后台指引在功能本地 `platforms.ts` 维护;卡片按配置状态显示 +启停开关,保存状态成功后更新显示。配置抽屉按需挂载并在关闭动画后卸载,每次打开重新查询, +保存失败保留草稿。公众号提供认证开关,飞书 Verification Token 可选;回调 URL 只读并支持复制。 + ### 简易智能体设置 `application-detail/setting/` 使用可折叠配置表单和调试预览占位双栏布局,仅模型设置和知识库 @@ -344,6 +346,8 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排 工具使用 debug Drawer、SSE 和执行详情;知识库保留任务轮询与导入 Action。 智能体画布按 Workspace/System 资源管理选择详情、保存、发布、模型和历史 API;DebugPanel 尚未按 System 范围适配。 客户端停止读取不等于服务端停止任务。 +- 知识库工作流入口页按四种资源范围选择工作流 API,通过 `api` Props 向调试、历史与执行记录组件逐层传递;详情查询与画布范围同步路由, + System 模型使用对应范围接口,Workspace 共享暂沿用 Workspace 模型设置接口,画布辅助资源查询尚未适配。 - 知识库导出仅工作流 `.kbwf`;导入确认覆盖后直接上传,不先保存旧草稿,成功重载;取消/失败保留图。 正式导入文档要求已发布;转换使用 Knowledge API,保留已有文档,不能用保存代替转换。 - 返回方法集中在各资源 `navigation.ts`;历史模式先退出预览,编辑态保留保存并退出/直接退出确认。 @@ -354,12 +358,20 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排 ## 其他页面约束 +### 账户入口 + - 头像菜单的系统 API Key 弹窗位于 `layout/avatar-dropdown/ApiKeyDialog.vue`,与同目录的修改密码弹窗一起由菜单入口打开。 顶部 API 文档地址使用 Admin 部署前缀,列表传入完整 System API Key API;与智能体概览分别维护弹窗, 仅复用 `business/api-key-list/` 及其内部编辑抽屉。 + +### 首页统计 + - 首页组件位于 `home/components/`,排行位于 `components/ranking/`。完整 Homepage API 和 workspaceId 由 View 传入。 智能体筛选只影响趋势;日期复用 MkDateRange,默认过去 7 天;占比使用工作空间同期总量,累计活跃人次为每日人数之和。 失败不伪装成零。快捷创建复用资源入口,`fitTriggerWidth` 仅显式开启时测量触发器。 + +### 智能体概览 + - `HomeStatistics` 接收完整的 Workspace Homepage、System Homepage 或 Overview API。 智能体概览通过 `hideHeader: true` 隐藏详情布局标题栏,保留内容内的监控与排行标题。 基本信息直接在概览页 `application-detail/overview/index.vue` 中组织 Web 访问与后端 API 访问卡片; @@ -402,14 +414,23 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排 搜索与日期变更回第一页,切换页签保留日期、清空搜索并回第一页;导出当前筛选下的全部记录。 表格展示排名、用户、当前指标、占比、另一指标及均 Tokens/次,排名跨页连续;搜索不改变同期占比分母。 两类排行均需返回用户 Tokens 和对话次数,不按用户名合并数据;新接口约定待后台对齐,见 API 文档。 + +### 触发器 + - 触发器执行记录的执行时间列使用服务端排序,独立 `orderBy` 经 `toOrderBy` 添加 `ett.` 前缀, 通过查询参数 `order` 传入。切换排序回第一页;搜索、分页和详情跨页保留排序,重新打开抽屉恢复默认排序。 - 触发器列表的下次执行时间、创建时间使用 `order_by` 表头排序,切换时清空选择并回到第一页;搜索、筛选和分页保留排序。 - 触发器表单共用创建/编辑,保留任务 ID 和参数;资源模式固定单任务,完整输入定义不足时补查详情。 保存校验全部折叠任务;参数初始化不覆盖已配置值,移除事件引用时恢复自定义默认值。 + +### 对话用户授权 + - 智能体/知识库对话用户页使用双栏布局和当前页授权;智能体从 context 取 Workspace/System 资源管理 ChatUser API,知识库取四范围 API。 自动授权立即保存,手动保存只提交当前页;请求共用 loading 与 inert,失败保留勾选。 授权人数取 `authorized_count`,不以分页勾选推算。 + +### 智能体对话日志 + - 对话日志筛选、导出使用同组查询;无勾选导出筛选结果,日期清空禁用导出。 对话次数、对话用户、最近对话时间使用 `order_by` 表头排序,用户列映射为 `username`;切换时清空选择并回到第一页,筛选、分页和导出保留排序。 清除策略通过当前范围 ChatLog API 保存、使用弹窗自身 loading;添加知识库固定所选对话 ID,选项使用当前范围 Knowledge/Document API。 @@ -418,12 +439,21 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排 `LogConversation`,隔离分页状态和未结束的请求,重置阅读位置与执行详情。 日志通过 `LogConversation.rightDrawerTarget` 将当前抽屉外壳传给会话布局,执行详情及遮罩 覆盖整个抽屉,包括标题与翻页区;关闭执行详情后返回原日志,详情内容独立滚动。 + +### 外部检索服务 + - 外部检索页从 context 读取四范围 ExternalRetrieval API;Workspace 共享只读,禁用开关并阻止提交。 查询失败展示重新加载入口,不展示默认开关;保存期间使用 loading 与 inert,失败保留已保存配置。 知识库或资源范围变化时重新查询;示例与地址来自响应,只读展示/复制,不在前端拼接。 + +### 门户设置 + - 门户设置已通过 Portal API 持久化,JSON 保存开关、认证及 `cross_domain_list`,FormData 保存名称/Logo。 成功以完整响应回显,保留现有编辑权限;认证选项来自门户配置的 `auth_config.system_options`, 默认登录方式仅展示草稿中已选的 `login_value`;跨域地址按行清理,取消不回写。 + +### 系统管理 + - 用户导入、改密、批量角色/配额流程留在各自业务目录;工作空间成员按用户联动角色行选择, 删除按 `user_relation_id` 去重,不按用户 ID 丢弃角色记录。 - 系统用户列表的创建时间、对话用户列表的 Tokens 使用量和创建时间使用服务端表头排序。 @@ -431,9 +461,17 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排 排序参数见 API 文档。 - 系统操作日志的操作时间使用服务端表头排序,切换后回到第一页,保留搜索及筛选条件。 分页和导出沿用当前排序条件,取消排序不传排序值。 + +### 登录与正式对话 + - 登录页使用 `formEl.validate` 回调和清晰的 LDAP/本地分支,不额外抽象或改写提交控制流程。 Admin 与 Chat 登录页共用 `login/components/LoginModePanel.vue` 及其账号/扫码登录组件, 组件不直接引用某个入口的 Store 或 API;接口、回调地址与登录后跳转由页面传入的 `LoginService` 提供。 +- Admin 与 Chat 登录页通过 `login/dingtalk-client-login.ts` 处理 `client=dingtalk&corpId=...` + 的钉钉客户端免密入口,按需加载 SDK 获取授权码,再调用各入口认证 Store 的 + `loginWithDingTalkClient(code)`;页面统一使用 async/await 完成授权、登录和成功跳转,失败恢复普通登录。 + 客户端免登不在登录服务或扫码组件中触发,避免外观预览发起授权。 + Admin 与 Chat 登录成功统一使用替换导航,优先返回 `redirect` 记录的站内原地址,没有时进入各自场景首页。 - Chat/Portal 页面以 auth token 为面板 key,token 变化重建并加载用户档案;路由认证见路由文档。 会话装配在 conversation-panel,列表 Store 不感知 Router;门户复用 chat 装配,保留各智能体最近会话分组。 账户菜单位于 `conversation-panel/left-sidebar/account-menu/index.vue`,单应用与门户共用;直接调用 Chat API 的 `postLogout()`,成功后清理当前认证场景并重新导航,由守卫处理后续认证。 diff --git a/ui/src/views/chat/login/index.vue b/ui/src/views/chat/login/index.vue index 13d24e048e4..bdb8bac7bee 100644 --- a/ui/src/views/chat/login/index.vue +++ b/ui/src/views/chat/login/index.vue @@ -1,5 +1,6 @@ diff --git a/ui/src/views/login/admin-login-service.ts b/ui/src/views/login/admin-login-service.ts index 70fc6258c0c..eac2b7a1621 100644 --- a/ui/src/views/login/admin-login-service.ts +++ b/ui/src/views/login/admin-login-service.ts @@ -1,6 +1,6 @@ /** Admin 登录页与外观设置登录预览使用的登录服务,对接 Admin 认证接口与 Store。 */ -import { useRouter } from 'vue-router' +import { useRoute, useRouter, type RouteLocationRaw } from 'vue-router' import { v7 as uuidv7 } from 'uuid' import ExternalLoginApi from '@/api/admin/auth/external-login' import LoginApi from '@/api/admin/auth/login' @@ -41,9 +41,17 @@ const getExternalLoginUrl = (authType: LoginMethod): Promise => { } export function useAdminLoginService(): LoginService { + const route = useRoute() const router = useRouter() const { auth } = useStore() + // 与 Chat 一致:登录成功优先返回原地址,没有时进入默认工作空间首页。 + const getRedirectRoute = (): RouteLocationRaw => { + const { redirect } = route.query + if (typeof redirect === 'string' && redirect.startsWith('/')) return redirect + return { name: 'workspace-home', params: { workspaceId: 'default' } } + } + return { get rsaKey() { return auth.baseProfile?.rsa ?? '' @@ -56,7 +64,7 @@ export function useAdminLoginService(): LoginService { loginWithDingTalk: (authCode) => auth.asyncLoginWithDingTalk(authCode), larkRedirectUri: `${window.location.origin}${ADMIN_API_BASE_PATH}/lark`, wecomState: 'fit2cloud-wecom-qr', - onLoginSuccess: () => router.push({ name: 'workspace-home', params: { workspaceId: 'default' } }), + onLoginSuccess: () => router.replace(getRedirectRoute()), forgotPassword: () => void router.push({ name: 'forgot-password' }), } } diff --git a/ui/src/views/login/dingtalk-client-login.ts b/ui/src/views/login/dingtalk-client-login.ts new file mode 100644 index 00000000000..93a16f2e143 --- /dev/null +++ b/ui/src/views/login/dingtalk-client-login.ts @@ -0,0 +1,20 @@ +/** 提供 Admin 与 Chat 登录页使用的钉钉客户端免登授权码。 */ + +import type { LocationQuery } from 'vue-router' +import { MsgError } from '@/utils/message' + +/** 仅在钉钉客户端入口请求授权;失败后保留普通登录入口,API 登录由各入口自行处理。 */ +export async function getDingTalkClientAuthCode(query: LocationQuery): Promise { + const { client, corpId } = query + if (client !== 'dingtalk' || typeof corpId !== 'string' || !corpId.trim()) return + + try { + const { default: dd } = await import('dingtalk-jsapi') + if (dd.env.platform === 'notInDingTalk') return + const { code } = await dd.runtime.permission.requestAuthCode({ corpId }) + if (!code) throw new Error('钉钉未返回授权码') + return code + } catch { + MsgError('钉钉免密授权失败,请使用其他方式登录') + } +} diff --git a/ui/src/views/login/index.vue b/ui/src/views/login/index.vue index 24fd97882df..fbd34da26ca 100644 --- a/ui/src/views/login/index.vue +++ b/ui/src/views/login/index.vue @@ -1,5 +1,7 @@ diff --git a/ui/src/views/workflow/application/components/ButtonAiGenerate.vue b/ui/src/views/workflow/application/components/ButtonAiGenerate.vue index 0ef522e9abe..44a4612f023 100644 --- a/ui/src/views/workflow/application/components/ButtonAiGenerate.vue +++ b/ui/src/views/workflow/application/components/ButtonAiGenerate.vue @@ -26,7 +26,6 @@ function handleOpenAiGenerate() {