diff --git a/ui/AGENTS.md b/ui/AGENTS.md index dab2290ed2d..825fcaab8c6 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -208,6 +208,8 @@ Keep these behaviors: ## Frontend Implementation Notes - Prefer existing project structure and naming. +- Do not write `aria-label` attributes in project source. Add concise Chinese HTML comments before + buttons to describe their purpose, following `src/components/COMPONENT_README.md`. - Before changing existing behavior, trace the affected flow and dependent requirements, including why the current implementation exists. If the requested change is likely to remove a requirement or introduce a regression, pause before editing, explain the risk, recommend a safer approach, diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 6e1bbd6c3a8..f284b3ae5fc 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -204,3 +204,11 @@ System 资源管理的用户授权维护在 `admin/system/resource-management/re 由接口内部读取,loading 由组件管理。`ResourceAuthorizationDrawer` 内部通过 `isSystemResource()` 选择完整的用户授权 API 对象,作为该抽屉的范围选择例外;用户组仍使用 现有 Workspace 接口,不推测 System 用户组路径。 + +### 关联资源 + +`admin/workspace/related-resources.ts` 维护关联资源查询: +`getResourceDependencies` 查询当前资源依赖的资源,对应后端 `mapping_resource`; +`getResourceDependents` 查询引用当前资源的资源,对应后端 `resource_mapping`。 +前端按关联资源语义命名,后端接口路径保持不变。该文件当前保留旧版请求签名, +尚未完成 v3 请求适配及页面接入。 diff --git a/ui/src/api/admin/core/request.ts b/ui/src/api/admin/core/request.ts index 7c85eaddfff..c1976f42d5c 100644 --- a/ui/src/api/admin/core/request.ts +++ b/ui/src/api/admin/core/request.ts @@ -14,10 +14,6 @@ interface ExportRequestConfig extends AxiosRequestConfig { skipGlobalErrorMessage?: boolean } -interface StreamRequestConfig { - signal?: AbortSignal -} - function setRequestHeaders(config: InternalAxiosRequestConfig) { const { auth, user } = useStore() @@ -256,7 +252,7 @@ export async function downloadRequest( } /** 发送 POST 请求并返回可逐块读取的原始响应。 */ -export function postStream(base: string, path: string, data?: unknown, config?: StreamRequestConfig): Promise { +export function postStream(base: string, path: string, data?: unknown): Promise { const { auth, user } = useStore() const headers: Record = { 'Content-Type': 'application/json' } if (auth.token) { @@ -269,7 +265,6 @@ export function postStream(base: string, path: string, data?: unknown, config?: method: 'POST', headers, body: data === undefined ? undefined : JSON.stringify(data), - signal: config?.signal, }) } diff --git a/ui/src/api/admin/workspace/application/application.ts b/ui/src/api/admin/workspace/application/application.ts index 4112bad256f..58b48d12a96 100644 --- a/ui/src/api/admin/workspace/application/application.ts +++ b/ui/src/api/admin/workspace/application/application.ts @@ -74,8 +74,8 @@ const putApplicationPublish = (applicationId: string) => { } /** 使用指定模型流式生成或优化系统提示词。 */ -const postPromptGenerate = (applicationId: string, modelId: string, payload: PromptGeneratePayload, signal?: AbortSignal) => { - return postStream(ADMIN_API_BASE_PATH, `${getPrefix()}/${applicationId}/model/${modelId}/prompt_generate`, payload, { signal }) +const postPromptGenerate = (applicationId: string, modelId: string, payload: PromptGeneratePayload) => { + return postStream(ADMIN_API_BASE_PATH, `${getPrefix()}/${applicationId}/model/${modelId}/prompt_generate`, payload) } export default { diff --git a/ui/src/api/admin/workspace/related-resources.ts b/ui/src/api/admin/workspace/related-resources.ts new file mode 100644 index 00000000000..05e7598a4ca --- /dev/null +++ b/ui/src/api/admin/workspace/related-resources.ts @@ -0,0 +1,33 @@ +import { Result } from '@/request/Result' +import { get, put, post, del } from '@/request/index' +import type { Ref } from 'vue' +import type { pageRequest } from '@/api/type/common' +const prefix = '/workspace' + +/** 获取引用当前资源的资源。 */ +const getResourceDependents: ( + workspace_id: string, + resource: string, + resource_id: string, + page: pageRequest, + params?: any, + loading?: Ref, +) => Promise> = (workspace_id, resource, resource_id, page, params, loading) => { + return get(`${prefix}/${workspace_id}/resource_mapping/${resource}/${resource_id}/${page.current_page}/${page.page_size}`, params, loading) +} +/** 获取当前资源依赖的资源。 */ +const getResourceDependencies: ( + workspace_id: string, + resource: string, + resource_id: string, + page: pageRequest, + params?: any, + loading?: Ref, +) => Promise> = (workspace_id, resource, resource_id, page, params, loading) => { + return get(`${prefix}/${workspace_id}/mapping_resource/${resource}/${resource_id}/${page.current_page}/${page.page_size}`, params, loading) +} + +export default { + getResourceDependents, + getResourceDependencies, +} diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 876b51c01c5..62580a4931c 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -51,6 +51,8 @@ src/components/ │ │ └── ModelParamsDialog.vue # 模型参数动态表单弹窗 │ ├── workspace-dropdown/ │ │ └── index.vue # 工作空间选择下拉框 +│ ├── related-resources-drawer/ # 关联资源查看抽屉(待完成 v3 接入) +│ │ └── index.vue │ ├── resource-authorization-drawer/ # 资源用户组、用户授权抽屉与权限配置弹窗 │ │ ├── index.vue │ │ ├── UserAuthorization.vue # 按用户查询与授权 @@ -171,6 +173,8 @@ import WorkspaceRelationTags from '@/components/business/workspace-relation-tags ## 组件约定 +- 项目源码不编写 `aria-label` 属性。按钮前添加简短中文 HTML 备注说明实际用途, + 例如 ``;已有用途备注时不重复添加。此规则适用于公共组件、页面和工作流节点。 - 高频、稳定、跨多数页面使用的基础组件放入 `global`。 - 跨页面复用且依赖业务类型、固定业务接口或领域交互的组件放入 `business/`, 由使用方手动导入。业务组件不使用 `Mk` 前缀,也不再按 Workspace 等上级领域增加额外目录。 @@ -1369,3 +1373,10 @@ import WorkspaceRelationTags from '@/components/business/workspace-relation-tags 资源授权抽屉内部公共展示 Props 放在同目录 `types.ts`,权限选项类型从 `RESOURCE_PERMISSION_OPTIONS` 派生;用户和用户组 API Props 分别保留完整 API 对象类型, 用户组组件只接收具备用户组方法的 Workspace API。 + +### RelatedResourcesDrawer + +关联资源查看抽屉,路径为 `business/related-resources-drawer/index.vue`,组件名为 +`RelatedResourcesDrawer`。标题为“关联资源”,操作入口文案统一为“查看关联资源”; +两个方向分别显示“依赖的资源”和“引用此资源的资源”。 +当前保留旧版实现,尚未完成 v3 请求、国际化依赖适配及页面接入。 diff --git a/ui/src/components/business/related-resources-drawer/index.vue b/ui/src/components/business/related-resources-drawer/index.vue new file mode 100644 index 00000000000..32a691c33c0 --- /dev/null +++ b/ui/src/components/business/related-resources-drawer/index.vue @@ -0,0 +1,612 @@ + + + diff --git a/ui/src/components/business/select-application-dialog/index.vue b/ui/src/components/business/select-application-dialog/index.vue index a5b03469a29..6f914bf6000 100644 --- a/ui/src/components/business/select-application-dialog/index.vue +++ b/ui/src/components/business/select-application-dialog/index.vue @@ -101,7 +101,8 @@ defineExpose({ open })

智能体

- + + diff --git a/ui/src/components/business/select-knowledge-dialog/index.vue b/ui/src/components/business/select-knowledge-dialog/index.vue index e8cb3f6aa3f..9aa864a287e 100644 --- a/ui/src/components/business/select-knowledge-dialog/index.vue +++ b/ui/src/components/business/select-knowledge-dialog/index.vue @@ -110,7 +110,8 @@ defineExpose({ open }) 所选知识库必须使用相同的 Embedding 模型 - + + diff --git a/ui/src/components/business/select-tool-dialog/index.vue b/ui/src/components/business/select-tool-dialog/index.vue index 985915d70a8..0d5472e43f8 100644 --- a/ui/src/components/business/select-tool-dialog/index.vue +++ b/ui/src/components/business/select-tool-dialog/index.vue @@ -111,7 +111,8 @@ defineExpose({ open })

{{ title }}

- + + diff --git a/ui/src/components/global/mk-form-list/index.vue b/ui/src/components/global/mk-form-list/index.vue index e4cecf238de..402e343252e 100644 --- a/ui/src/components/global/mk-form-list/index.vue +++ b/ui/src/components/global/mk-form-list/index.vue @@ -72,13 +72,13 @@ function removeRow(index: number) { diff --git a/ui/src/components/mk-drag-upload/index.vue b/ui/src/components/mk-drag-upload/index.vue index a0a397c34a7..1f43736d5ff 100644 --- a/ui/src/components/mk-drag-upload/index.vue +++ b/ui/src/components/mk-drag-upload/index.vue @@ -55,8 +55,9 @@ defineExpose({ clearFiles }) {{ formatFileSize(selectedFile.size) }}
- - + + +
diff --git a/ui/src/components/mk-dynamics-form/constructor/items/JsonInputConstructor.vue b/ui/src/components/mk-dynamics-form/constructor/items/JsonInputConstructor.vue index 1beda4df8c6..75ad21206af 100644 --- a/ui/src/components/mk-dynamics-form/constructor/items/JsonInputConstructor.vue +++ b/ui/src/components/mk-dynamics-form/constructor/items/JsonInputConstructor.vue @@ -1,6 +1,6 @@ + + + + diff --git a/ui/src/views/workflow/application/ApplicationWorkflowView.vue b/ui/src/views/workflow/application/ApplicationWorkflowView.vue index 8dff1d1e5a5..81de6d918f3 100644 --- a/ui/src/views/workflow/application/ApplicationWorkflowView.vue +++ b/ui/src/views/workflow/application/ApplicationWorkflowView.vue @@ -241,10 +241,10 @@ onMounted(() => { 调试 发布 - + - diff --git a/ui/src/workflow-canvas/WORKFLOW_README.md b/ui/src/workflow-canvas/WORKFLOW_README.md index 8ec2c6e7db3..8d25c26964c 100644 --- a/ui/src/workflow-canvas/WORKFLOW_README.md +++ b/ui/src/workflow-canvas/WORKFLOW_README.md @@ -39,7 +39,7 @@ src/workflow-canvas/ ### `core/` -`core` 是配置完成后应保持稳定的基础层。`node-container/index.vue` 和 `NodeCascader.vue` 是节点的 +`core` 是配置完成后应保持稳定的基础层。`node-container/index.vue` 提供节点的 通用结构与交互能力,属于核心代码;连线、快捷键、公共校验、Teleport 和节点公共工具也放在 这里。`node-container/` 内的条件和操作下拉组件是节点容器的私有实现,不作为画布可复用组件 单独引用;`edge/` 集中维护边的 LogicFlow 注册配置、模型、视图和删除按钮,普通边由画布入口 @@ -51,7 +51,9 @@ src/workflow-canvas/ `visible-change` 状态,并在节点组件卸载时调用 `reset()`;不要通过 Teleport 到 `body` 或增加 `z-index` 绕过画布的缩放和 SVG 绘制顺序。 -`NodeCascader` 已在内部统一管理下拉层的锚点保护,使用方无需重复接入。 +`component/NodeCascader.vue` 提供上游变量选择,已在内部统一管理下拉层的锚点保护,使用方无需重复接入。 +组件在选值回写及 Vue 更新完成后触发表单校验,清空统一回写空数组;`validateEvent` 默认为 +`true`,设为 `false` 可关闭自动校验。 下拉展开时通过 `document` 捕获阶段的 `pointerdown` 处理外部点击,排除组件内部的输入框和 未 Teleport 的下拉面板;关闭及卸载时移除监听。不要移除节点容器的鼠标事件隔离来恢复外部关闭, 以免操作表单时触发画布拖拽。 @@ -141,7 +143,7 @@ AI 对话、意图识别、问题优化、参数提取、图片理解、视频 节点自身的表单、状态和专属校验留在节点目录;多个节点共享且属于画布基础协议的能力才上移到 `core`。节点应复用 `core/node-container/index.vue`,需要选择上游节点字段时复用 -`core/NodeCascader.vue`。 +`component/NodeCascader.vue`。 复杂节点中的普通表单区块直接放在节点 `index.vue`,避免为简单字段读写增加 Props、Emits 和 中间 computed。独立弹窗、资源选择等具有完整交互边界的能力放在节点目录的 `component/` 下; @@ -173,6 +175,12 @@ AI 对话、意图识别、问题优化、参数提取、图片理解、视频 节点入口深拷贝写回列表并发送原有字段刷新事件,保留显隐条件引用校验。用户输入与接口传参继续 交叉检查参数重名;`UserInputSettingDialog` 独立维护直接展示参数设置,删除字段时由表格清理对应设置。 +基本信息节点的 `component/LongTermSetting.vue` 同时封装长期记忆设置按钮与弹窗, +通过 `v-model` 接收 `LongTermSetting`,模型与供应商选项由节点入口传入。节点入口根据长期记忆 +开关挂载组件;打开时深拷贝配置为草稿,校验通过并保存后仅合并长期记忆配置,取消不修改节点。 +默认来源通过 `defaultModelSetting` 接收保存后的 `LLM` 配置,使用禁用的 `SelectModel` 展示, +未配置默认模型时阻止弹窗保存;自定义来源保留节点自己的模型与参数。 + 基本信息节点的 `component/FileUploadSetting.vue` 同时封装文件上传设置按钮与弹窗, 通过 `v-model` 接收 `FileUploadSettingData`。节点入口根据上传开关挂载组件,并在配置写回后发送 `refreshFileUploadConfig` 刷新开始节点文件变量;弹窗打开时重置并深拷贝草稿,确认时保留上传方式 @@ -202,7 +210,12 @@ ID 数组及 `tool_list`、`skill_tool_list`、`application_list` 回显快照 已关联但缺少详情的资源保留 ID 回退展示。 AI 对话节点的提示词、历史记录、视觉理解和输出思考表单直接在节点入口维护,统一使用全局 -`MdEditorMagnify` 和节点表单样式;AI 提示词生成使用独立弹窗。AI 对话、图片理解和视频理解节点的 +`MdEditorMagnify` 和节点表单样式;`component/PromptGenerate.vue` 同时封装生成按钮与弹窗, +接收 `modelId`、`disabled`、`modelOptions` 和 `providerOptions`,内部读取当前路由的智能体 ID。 +弹窗顶部复用 `SelectModel`,仅修改本次生成使用的模型;主体展示最新结果与主题输入框,支持停止和重新生成。 +重新生成复用上次请求消息,关闭时终止请求并在关闭动画结束后清理会话;点击替换后通过 `replace` 交由节点回写系统提示词。 +生成接口沿用智能体已保存的参数,弹窗不提供独立模型参数设置。 +AI 对话、图片理解和视频理解节点的 输出思考设置统一复用 `component/ThinkingSetting.vue`,后续同类入口也应引用该组件。组件通过 `v-model` 接收 `types.ts` 中的共享配置类型 `ReasoningSettingData`;节点入口维护输出思考开关, 并根据开关用 `v-if` 挂载设置组件。弹窗打开时重置并深拷贝草稿,仅在校验通过并保存后回写, diff --git a/ui/src/workflow-canvas/core/NodeCascader.vue b/ui/src/workflow-canvas/component/NodeCascader.vue similarity index 85% rename from ui/src/workflow-canvas/core/NodeCascader.vue rename to ui/src/workflow-canvas/component/NodeCascader.vue index 23aefe21e26..036e0a7e466 100644 --- a/ui/src/workflow-canvas/core/NodeCascader.vue +++ b/ui/src/workflow-canvas/component/NodeCascader.vue @@ -1,5 +1,6 @@ + + + + diff --git a/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerateDialog.vue b/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerateDialog.vue deleted file mode 100644 index a1279fbbb26..00000000000 --- a/ui/src/workflow-canvas/nodes/ai-chat-node/component/PromptGenerateDialog.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - diff --git a/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue b/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue index 09eec35c0c0..664b76773a2 100644 --- a/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue +++ b/ui/src/workflow-canvas/nodes/ai-chat-node/index.vue @@ -4,13 +4,13 @@ import { cloneDeep } from 'lodash' import type { FormInstance } from 'element-plus' import type { ModelItem, ModelProviderItem } from '@/api/types' import NodeModelSelect from '@/workflow-canvas/component/node-model-select/index.vue' -import NodeCascader from '@/workflow-canvas/core/NodeCascader.vue' +import NodeCascader from '@/workflow-canvas/component/NodeCascader.vue' import NodeContainer from '@/workflow-canvas/core/node-container/index.vue' import { handleNodeWheel, isLastNode } from '@/workflow-canvas/core/utils' import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node' import { useWorkflowStore } from '@/workflow-canvas/store' import { WorkflowMode } from '@/workflow-canvas/types' -import PromptGenerateDialog from './component/PromptGenerateDialog.vue' +import PromptGenerate from './component/PromptGenerate.vue' import ThinkingSetting from '@/workflow-canvas/component/ThinkingSetting.vue' import ResourceSetting from './component/resource-setting/index.vue' import type { AiChatNodeForm } from './types' @@ -26,7 +26,6 @@ const model = getModel() const store = useWorkflowStore(apiType) const formRef = useTemplateRef('formRef') -const promptGenerateDialogRef = useTemplateRef>('promptGenerateDialogRef') const modelOptions = ref([]) const providerOptions = ref([]) @@ -145,9 +144,12 @@ onMounted(() => { - - - + { - - diff --git a/ui/src/workflow-canvas/nodes/application-node/index.vue b/ui/src/workflow-canvas/nodes/application-node/index.vue index 664a5546318..bdba080d1b8 100644 --- a/ui/src/workflow-canvas/nodes/application-node/index.vue +++ b/ui/src/workflow-canvas/nodes/application-node/index.vue @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash' import type { FormInstance } from 'element-plus' import ApplicationApi from '@/api/admin/workspace/application/application' import { APPLICATION_TYPE } from '@/api/enums' -import NodeCascader from '@/workflow-canvas/core/NodeCascader.vue' +import NodeCascader from '@/workflow-canvas/component/NodeCascader.vue' import NodeContainer from '@/workflow-canvas/core/node-container/index.vue' import type { WorkflowNodeModel } from '@/workflow-canvas/core/workflow-node' import { WorkflowMode, WorkflowNodeType } from '@/workflow-canvas/types' diff --git a/ui/src/workflow-canvas/nodes/base-node/component/LongTermSettingDialog.vue b/ui/src/workflow-canvas/nodes/base-node/component/LongTermSetting.vue similarity index 81% rename from ui/src/workflow-canvas/nodes/base-node/component/LongTermSettingDialog.vue rename to ui/src/workflow-canvas/nodes/base-node/component/LongTermSetting.vue index e372c0bedf9..81c68f0b174 100644 --- a/ui/src/workflow-canvas/nodes/base-node/component/LongTermSettingDialog.vue +++ b/ui/src/workflow-canvas/nodes/base-node/component/LongTermSetting.vue @@ -3,14 +3,15 @@ import { computed, ref, useTemplateRef } from 'vue' import { cloneDeep } from 'lodash' import type { FormInstance } from 'element-plus' import SelectModel from '@/components/business/select-model/index.vue' -import type { ModelItem, ModelProviderItem } from '@/api/types' +import type { ModelConfig, ModelItem, ModelProviderItem } from '@/api/types' import type { LongTermSetting } from '../types' -defineOptions({ name: 'BaseNodeLongTermSettingDialog' }) +defineOptions({ name: 'BaseNodeLongTermSetting' }) -defineProps<{ modelOptions: ModelItem[]; providerOptions: ModelProviderItem[] }>() -const emit = defineEmits<{ submit: [setting: LongTermSetting] }>() +const props = defineProps<{ modelOptions: ModelItem[]; providerOptions: ModelProviderItem[]; defaultModelSetting?: ModelConfig }>() +const setting = defineModel({ required: true }) +// 长期记忆设置:打开时创建草稿,保存后回写配置。 const visible = ref(false) const formRef = useTemplateRef('formRef') const formData = ref({ @@ -79,8 +80,9 @@ const scheduleValue = computed>({ }, }) -function open(setting: LongTermSetting) { - formData.value = cloneDeep(setting) +function open() { + formRef.value?.clearValidate() + formData.value = cloneDeep(setting.value) if (!formData.value.long_term_trigger_setting || !Object.keys(formData.value.long_term_trigger_setting).length) { formData.value.long_term_trigger_setting = { rounds: 10 } } @@ -109,31 +111,46 @@ function validateCron(_rule: unknown, value: unknown, callback: (error?: Error) function validateModel(_rule: unknown, _value: unknown, callback: (error?: Error) => void) { if (formData.value.long_term_model_id_type === 'custom' && !formData.value.long_term_model_id) { - callback(new Error('请选择长期记忆模型')) + callback(new Error('请选择 AI 模型')) + return + } + if (formData.value.long_term_model_id_type === 'default' && !props.defaultModelSetting?.model_id) { + callback(new Error('请配置默认模型')) return } callback() } function submit() { - formRef.value?.validate().then(() => { - emit('submit', cloneDeep(formData.value)) + formRef.value?.validate((valid) => { + if (!valid) return + setting.value = cloneDeep(formData.value) visible.value = false }) } - -defineExpose({ open })