fix: 收口管理端页面权限与工作流运行授权

- 页面选项接口改用所属页面权限并返回最小数据视图

- 统一校验工作流引用、租户、状态与定时任务执行主体

- 补充聊天记录权限迁移和权限隔离回归测试
This commit is contained in:
2026-08-07 12:51:21 +08:00
parent 6ad004da9b
commit d244a0404d
55 changed files with 3350 additions and 387 deletions

View File

@@ -78,7 +78,7 @@ export interface AgentChatCapabilityPayload {
}
export function getPublishedAgents() {
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/options', {
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/session/options', {
params: { publishedOnly: true },
});
}
@@ -95,7 +95,7 @@ export function getAgentSession(sessionId: number | string) {
export function getPublishedKnowledges() {
return api.get<RequestResult<AgentChatKnowledgeView[]>>(
'/api/v1/agent/knowledgeOptions',
'/api/v1/agent/session/knowledgeOptions',
);
}

View File

@@ -6,7 +6,7 @@ import pageSource from './index.vue?raw';
describe('管理端智能体聊天历史契约', () => {
it('使用智能体候选接口并保留 assistantId 查询参数', () => {
expect(pageSource).toContain("'/api/v1/agent/options'");
expect(pageSource).toContain("'/api/v1/chatHistory/agentOptions'");
expect(pageSource).not.toContain('/api/v1/bot/list');
expect(pageSource).toContain('label: item.name');
expect(pageSource).toContain('assistantId: query.value.assistantId');

View File

@@ -103,7 +103,9 @@ onMounted(async () => {
async function fetchAgents() {
agentLoading.value = true;
const [error, res] = await tryit(api.get)('/api/v1/agent/options');
const [error, res] = await tryit(api.get)(
'/api/v1/chatHistory/agentOptions',
);
agentLoading.value = false;
if (error || res?.errorCode !== 0) {
agentOptions.value = [];

View File

@@ -19,7 +19,6 @@ import {
} from 'element-plus';
import { api } from '#/api/request';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import { $t } from '#/locales';
@@ -102,6 +101,8 @@ const normalizeEntity = (raw: any = {}) => {
const entity = ref<any>(normalizeEntity(defaultEntity));
const btnLoading = ref(false);
const categoryOptions = ref<any[]>([]);
let categoryOptionsLoaded = false;
const rules = ref({
collectionType: [
{ required: true, message: $t('message.required'), trigger: 'change' },
@@ -145,7 +146,7 @@ const collectionTypeList = [
},
];
function openDialog(row: any = {}) {
async function openDialog(row: any = {}) {
if (row.id) {
isAdd.value = false;
entity.value = normalizeEntity(row);
@@ -154,6 +155,17 @@ function openDialog(row: any = {}) {
entity.value = normalizeEntity(defaultEntity);
}
dialogVisible.value = true;
if (!categoryOptionsLoaded) {
try {
const res = await api.get(
'/api/v1/documentCollectionCategory/visibleList',
);
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
categoryOptionsLoaded = true;
} catch {
categoryOptions.value = [];
}
}
}
async function save() {
@@ -251,10 +263,14 @@ defineExpose({
prop="categoryId"
:label="$t('documentCollection.categoryId')"
>
<DictSelect
v-model="entity.categoryId"
dict-code="aiDocumentCollectionCategory"
/>
<ElSelect v-model="entity.categoryId" clearable filterable>
<ElOption
v-for="category in categoryOptions"
:key="category.id"
:label="category.categoryName"
:value="category.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="visibilityScope"

View File

@@ -375,15 +375,9 @@ const loadModels = async (preferredId?: string) => {
const loadPublishBaseUrl = async () => {
try {
const res = await api.get('/api/v1/sysOption/list', {
params: {
keys: ['chat_publish_base_url'],
},
});
const res = await api.get('/api/v1/model/gatewayConfig');
if (res.errorCode === 0) {
publishBaseUrl.value = String(
res.data?.chat_publish_base_url || '',
).trim();
publishBaseUrl.value = String(res.data?.publishBaseUrl || '').trim();
}
} catch {
publishBaseUrl.value = '';

View File

@@ -1,6 +1,13 @@
<script setup lang="ts">
import type {FormInstance} from 'element-plus';
import {ElForm, ElFormItem, ElInput, ElMessage} from 'element-plus';
import {
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElOption,
ElSelect,
} from 'element-plus';
import {onMounted, ref} from 'vue';
@@ -37,6 +44,8 @@ const dialogVisible = ref(false);
const isAdd = ref(true);
const entity = ref<any>(createDefaultEntity());
const btnLoading = ref(false);
const categoryOptions = ref<any[]>([]);
let categoryOptionsLoaded = false;
const rules = ref({
resourceType: [
{ required: true, message: $t('message.required'), trigger: 'change' },
@@ -57,7 +66,7 @@ const rules = ref({
});
// functions
function openDialog(row: any) {
async function openDialog(row: any) {
isAdd.value = !row?.id;
entity.value = {
...createDefaultEntity(),
@@ -65,6 +74,15 @@ function openDialog(row: any) {
status: row?.status ?? 0,
};
dialogVisible.value = true;
if (!categoryOptionsLoaded) {
try {
const res = await api.get('/api/v1/resourceCategory/visibleList');
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
categoryOptionsLoaded = true;
} catch {
categoryOptions.value = [];
}
}
}
function save() {
saveForm.value?.validate((valid) => {
@@ -158,10 +176,14 @@ function uploadError() {
<ElInput v-model.trim="entity.resourceName" />
</ElFormItem>
<ElFormItem prop="categoryId" :label="$t('aiResource.categoryId')">
<DictSelect
v-model="entity.categoryId"
dict-code="aiResourceCategory"
/>
<ElSelect v-model="entity.categoryId" clearable filterable>
<ElOption
v-for="category in categoryOptions"
:key="category.id"
:label="category.categoryName"
:value="category.id"
/>
</ElSelect>
</ElFormItem>
</ElForm>
</EasyFlowFormModal>

View File

@@ -151,9 +151,7 @@ async function initializeWorkflow() {
}
await Promise.all([
loadCustomNode(),
getLlmList(),
getKnowledgeList(),
getCodeEngineList(),
getDesignerOptions(),
getWorkflowInfo(workflowId.value),
]);
showTinyFlow.value = true;
@@ -574,24 +572,14 @@ function reconcileWorkflowDraftAfterSave(savedContentSignature: string) {
pendingDraftContent = normalizedContent;
persistPendingWorkflowDraft();
}
async function getLlmList() {
return api.get('/api/v1/model/list').then((res) => {
llmList.value = res.data;
});
}
async function getKnowledgeList() {
return api.get('/api/v1/documentCollection/list').then((res) => {
knowledgeList.value = res.data;
});
}
async function getCodeEngineList() {
return api.get('/api/v1/workflow/supportedCodeEngines').then((res) => {
if (
res?.errorCode === 0 &&
Array.isArray(res.data) &&
res.data.length > 0
) {
codeEngineList.value = res.data;
async function getDesignerOptions() {
return api.get('/api/v1/workflow/designer/options').then((res) => {
llmList.value = Array.isArray(res.data?.models) ? res.data.models : [];
knowledgeList.value = Array.isArray(res.data?.knowledges)
? res.data.knowledges
: [];
if (Array.isArray(res.data?.codeEngines) && res.data.codeEngines.length > 0) {
codeEngineList.value = res.data.codeEngines;
}
});
}
@@ -785,7 +773,7 @@ function handleChoose(nodeName: string, value: any) {
function handleWorkflowNodeUpdate(chooseId: any) {
pageLoading.value = true;
api
.get('/api/v1/workflowNode/getChainParams', {
.get('/api/v1/workflow/designer/childWorkflow', {
params: {
currentId: workflowId.value,
workflowId: chooseId,
@@ -799,7 +787,7 @@ function handleWorkflowNodeUpdate(chooseId: any) {
function handlePluginNodeUpdate(chooseId: any) {
pageLoading.value = true;
api
.get('/api/v1/pluginItem/getTinyFlowData', {
.get('/api/v1/workflow/designer/pluginTinyFlow', {
params: {
id: chooseId,
},
@@ -848,7 +836,7 @@ function onAsyncExecute(info: any) {
:title="$t('menus.ai.plugin')"
width="730"
ref="pluginSelectRef"
page-url="/api/v1/plugin/page?availableOnly=true"
page-url="/api/v1/workflow/designer/plugins"
:has-parent="true"
single-select
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"

View File

@@ -201,7 +201,7 @@ const actions: ActionButton[] = [
icon: Share,
text: $t('button.share'),
permission: '/api/v1/workflow/save',
placement: 'inline',
placement: 'menu',
disabled: (row: any) =>
row.publishStatus !== 'PUBLISHED' || sharingWorkflowId.value === row.id,
loading: (row: any) => sharingWorkflowId.value === row.id,

View File

@@ -49,6 +49,8 @@ const createDefaultEntity = () => ({
});
const entity = ref<any>(createDefaultEntity());
const btnLoading = ref(false);
const categoryOptions = ref<any[]>([]);
let categoryOptionsLoaded = false;
const visibilityScopeOptions = computed(() => [
{
label: $t('aiWorkflow.visibilityScopePrivate'),
@@ -88,7 +90,7 @@ const rules = computed(() => ({
}),
}));
// functions
function openDialog(row: any, importMode = false) {
async function openDialog(row: any, importMode = false) {
isImport.value = importMode;
isAdd.value = !row?.id;
entity.value = {
@@ -96,6 +98,15 @@ function openDialog(row: any, importMode = false) {
...row,
};
dialogVisible.value = true;
if (!categoryOptionsLoaded) {
try {
const res = await api.get('/api/v1/workflowCategory/visibleList');
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
categoryOptionsLoaded = true;
} catch {
categoryOptions.value = [];
}
}
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
@@ -227,10 +238,14 @@ function closeDialog() {
<ElInput v-model.trim="entity.title" />
</ElFormItem>
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
<DictSelect
v-model="entity.categoryId"
dict-code="aiWorkFlowCategory"
/>
<ElSelect v-model="entity.categoryId" clearable filterable>
<ElOption
v-for="category in categoryOptions"
:key="category.id"
:label="category.categoryName"
:value="category.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="visibilityScope"

View File

@@ -76,17 +76,12 @@ function dedupeManagedDatasetOptions(options: ManagedDatasetOption[]) {
export async function loadManagedDatasetOptions(): Promise<
ManagedDatasetOption[]
> {
const sourceRes = await api.get('/api/v1/datacenterSource/page', {
params: {
pageNumber: 1,
pageSize: 200,
},
});
const sources = sourceRes.data?.records || [];
const sourceRes = await api.get('/api/v1/workflow/designer/dataSources');
const sources = sourceRes.data || [];
const options: ManagedDatasetOption[] = [];
for (const source of sources) {
try {
const catalogRes = await api.get('/api/v1/datacenterSource/catalogs', {
const catalogRes = await api.get('/api/v1/workflow/designer/catalogs', {
params: {
sourceId: source.id,
},
@@ -94,7 +89,7 @@ export async function loadManagedDatasetOptions(): Promise<
const catalogs = catalogRes.data || [];
for (const catalog of catalogs) {
const tableRes = await api.get(
'/api/v1/datacenterDataset/managedTables',
'/api/v1/workflow/designer/managedTables',
{
params: {
sourceId: source.id,
@@ -176,7 +171,7 @@ export async function loadManagedDatasetSchema(
fields: [],
};
}
const res = await api.get('/api/v1/datacenterDataset/schema', {
const res = await api.get('/api/v1/workflow/designer/schema', {
params: datasetRef,
});
const data = res.data || {};
@@ -188,8 +183,8 @@ export async function loadManagedDatasetSchema(
}))
: [];
return {
tableName: data.table?.tableName || datasetRef.tableName,
tableDesc: data.table?.tableDesc,
tableName: data.tableName || datasetRef.tableName,
tableDesc: data.tableDesc,
fields,
};
}

View File

@@ -28,18 +28,6 @@ interface ProviderOption {
value: string;
}
interface ModelProvider {
key: string;
options?: ProviderOptionExtra;
title: string;
}
interface LlmOption {
extra: Map<string, string | undefined>;
label: string;
value: string;
}
interface SettingsEntity {
chatgpt_api_key: string;
chatgpt_chatPath: string;
@@ -50,18 +38,6 @@ interface SettingsEntity {
}
const providerOptions = ref<ProviderOption[]>(providerList as ProviderOption[]);
const brands = ref<ModelProvider[]>([]);
const llmOptions = ref<LlmOption[]>([]);
// 获取品牌接口数据
function getBrands() {
api.get('/api/v1/modelProvider/list').then((res) => {
if (res.errorCode === 0) {
brands.value = (res.data ?? []) as ModelProvider[];
llmOptions.value = formatLlmList(brands.value);
}
});
}
function getOptions() {
api
.get(
@@ -78,7 +54,6 @@ function getOptions() {
}
onMounted(() => {
getOptions();
getBrands();
});
const entity = ref<SettingsEntity>({
@@ -90,19 +65,6 @@ const entity = ref<SettingsEntity>({
chat_publish_base_url: '',
});
function formatLlmList(data: ModelProvider[]): LlmOption[] {
return data.map((item) => {
const extra = new Map([
['chatPath', item.options?.chatPath],
['llmEndpoint', item.options?.llmEndpoint],
]);
return {
label: item.title,
value: item.key,
extra,
};
});
}
function handleChangeModel(value: string) {
const extra = providerList.find((item) => item.value === value);
entity.value.chatgpt_chatPath = extra?.options?.chatPath ?? '';

View File

@@ -4,7 +4,7 @@ import pageSource from './index.vue?raw';
describe('管理端工作台智能体数据契约', () => {
it('使用 Agent 候选接口和名称字段', () => {
expect(pageSource).toContain("'/api/v1/agent/options'");
expect(pageSource).toContain("'/api/v1/dashboard/agentOptions'");
expect(pageSource).not.toContain('/api/v1/bot/list');
expect(pageSource).toContain('label: item.name');
expect(pageSource).not.toContain('publishedOnly');

View File

@@ -332,7 +332,7 @@ async function loadAssistantOptions() {
try {
const agents =
await requestClient.get<Array<{ id?: number | string; name?: string }>>(
'/api/v1/agent/options',
'/api/v1/dashboard/agentOptions',
);
const nextOptions: AssistantOptionItem[] = [
{ label: '全部智能体', value: '' },

View File

@@ -114,14 +114,14 @@ const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
{ label: $t('approval.assignee.user'), value: 'USER' },
{ label: $t('approval.assignee.dept'), value: 'DEPT' },
];
const ENABLED_DATA_STATUS = 1;
const ASSIGNEE_VISIBLE_TAG_COUNT = 2;
const saveForm = ref<FormInstance>();
const dialogVisible = ref(false);
const isAdd = ref(true);
const btnLoading = ref(false);
const categoryLoaded = ref(false);
const resourceScopeOptionsLoaded = ref(false);
let resourceScopeOptionsRequest: null | Promise<void> = null;
const roleLoaded = ref(false);
const accountLoading = ref(false);
const deptTreeOptions = ref<any[]>([]);
@@ -178,7 +178,7 @@ watch(
return scope;
});
if (resourceType) {
void ensureCategoryOptions();
void ensureResourceScopeOptions();
}
},
);
@@ -212,8 +212,7 @@ async function openDialog(row: any = {}) {
formModel.value = buildDefaultForm();
dialogVisible.value = true;
await Promise.all([
ensureCategoryOptions(),
ensureDeptOptions(),
ensureResourceScopeOptions(),
ensureRoleOptions(),
]);
if (!row?.id) {
@@ -276,40 +275,46 @@ async function openDialog(row: any = {}) {
}
}
async function ensureCategoryOptions() {
if (categoryLoaded.value) {
async function ensureResourceScopeOptions() {
if (resourceScopeOptionsLoaded.value) {
return;
}
const [agentRes, workflowRes, knowledgeRes] = await Promise.all([
api.get('/api/v1/agentCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/workflowCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/documentCollectionCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
]);
categoryOptions.value = {
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
BOT: [],
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
};
categoryLoaded.value = true;
}
async function ensureDeptOptions() {
const res = await api.get('/api/v1/sysDept/list', {
params: {
asTree: true,
sortKey: 'sortNo',
sortType: 'asc',
status: ENABLED_DATA_STATUS,
},
});
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
if (!resourceScopeOptionsRequest) {
resourceScopeOptionsRequest = api
.get('/api/v1/approvalFlow/resourceScopeOptions')
.then((res) => {
const categories = res.data?.categories || {};
categoryOptions.value = {
AGENT: normalizeCategoryOptions(categories.AGENT, 'categoryName'),
BOT: [],
KNOWLEDGE: normalizeCategoryOptions(
categories.KNOWLEDGE,
'categoryName',
),
WORKFLOW: normalizeCategoryOptions(
categories.WORKFLOW,
'categoryName',
),
};
deptTreeOptions.value = Array.isArray(res.data?.departments)
? res.data.departments
: [];
resourceScopeOptionsLoaded.value = true;
})
.catch(() => {
categoryOptions.value = {
AGENT: [],
BOT: [],
KNOWLEDGE: [],
WORKFLOW: [],
};
deptTreeOptions.value = [];
})
.finally(() => {
resourceScopeOptionsRequest = null;
});
}
await resourceScopeOptionsRequest;
}
async function ensureRoleOptions() {

View File

@@ -5,7 +5,15 @@ import { onMounted, ref, watch } from 'vue';
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
import {
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElOption,
ElSelect,
ElTreeSelect,
} from 'element-plus';
import { getCredentialKeyApi } from '#/api';
import { api } from '#/api/request';
@@ -26,6 +34,10 @@ const saveForm = ref<FormInstance>();
// variables
const dialogVisible = ref(false);
const isAdd = ref(true);
const departmentOptions = ref<any[]>([]);
const roleOptions = ref<any[]>([]);
const positionOptions = ref<any[]>([]);
let formOptionsLoaded = false;
function createDefaultEntity() {
return {
deptId: '',
@@ -110,7 +122,7 @@ const rules = ref({
],
});
// functions
function openDialog(row: any) {
async function openDialog(row: any) {
isAdd.value = !row?.id;
entity.value = {
...createDefaultEntity(),
@@ -123,6 +135,23 @@ function openDialog(row: any) {
entity.value.positionIds = [];
}
dialogVisible.value = true;
if (!formOptionsLoaded) {
try {
const res = await api.get('/api/v1/sysAccount/formOptions');
departmentOptions.value = Array.isArray(res.data?.departments)
? res.data.departments
: [];
roleOptions.value = Array.isArray(res.data?.roles) ? res.data.roles : [];
positionOptions.value = Array.isArray(res.data?.positions)
? res.data.positions
: [];
formOptionsLoaded = true;
} catch {
departmentOptions.value = [];
roleOptions.value = [];
positionOptions.value = [];
}
}
}
function save() {
saveForm.value?.validate(async (valid) => {
@@ -197,7 +226,14 @@ watch(
<UploadAvatar v-model="entity.avatar" />
</ElFormItem>
<ElFormItem prop="deptId" :label="$t('sysAccount.deptId')">
<DictSelect v-model="entity.deptId" dict-code="sysDept" />
<ElTreeSelect
v-model="entity.deptId"
:data="departmentOptions"
:props="{ label: 'deptName', value: 'id', children: 'children' }"
check-strictly
clearable
filterable
/>
</ElFormItem>
<ElFormItem prop="loginName" :label="$t('sysAccount.loginName')">
<ElInput v-model.trim="entity.loginName" />
@@ -244,14 +280,24 @@ watch(
:label="$t('sysAccount.roleIds')"
:required="isAdd"
>
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
<ElSelect v-model="entity.roleIds" multiple clearable filterable>
<ElOption
v-for="role in roleOptions"
:key="role.id"
:label="role.roleName"
:value="role.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
<DictSelect
multiple
v-model="entity.positionIds"
dict-code="sysPosition"
/>
<ElSelect v-model="entity.positionIds" multiple clearable filterable>
<ElOption
v-for="position in positionOptions"
:key="position.id"
:label="position.positionName"
:value="position.id"
/>
</ElSelect>
</ElFormItem>
</ElForm>
</EasyFlowFormModal>

View File

@@ -5,7 +5,13 @@ import { ref } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
import {
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElTreeSelect,
} from 'element-plus';
import { api } from '#/api/request';
import DictSelect from '#/components/dict/DictSelect.vue';
@@ -22,6 +28,8 @@ const dialogVisible = ref(false);
const isAdd = ref(true);
const entity = ref<any>(buildDefaultEntity());
const btnLoading = ref(false);
const parentDepartmentOptions = ref<any[]>([]);
let parentDepartmentOptionsLoaded = false;
const rules = ref({
parentId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
@@ -45,13 +53,31 @@ function buildDefaultEntity() {
status: ENABLED_STATUS,
};
}
function openDialog(row: any = {}) {
async function openDialog(row: any = {}) {
isAdd.value = !row.id;
entity.value = {
...buildDefaultEntity(),
...row,
};
dialogVisible.value = true;
if (!parentDepartmentOptionsLoaded) {
try {
const res = await api.get('/api/v1/sysDept/list', {
params: {
asTree: true,
sortKey: 'sortNo',
sortType: 'asc',
},
});
parentDepartmentOptions.value = [
{ id: 0, deptName: $t('sysDept.root'), children: [] },
...(Array.isArray(res.data) ? res.data : []),
];
parentDepartmentOptionsLoaded = true;
} catch {
parentDepartmentOptions.value = [];
}
}
}
async function save() {
if (btnLoading.value) {
@@ -104,11 +130,14 @@ function closeDialog() {
class="easyflow-modal-form easyflow-modal-form--compact"
>
<ElFormItem prop="parentId" :label="$t('sysDept.parentId')">
<DictSelect
<ElTreeSelect
:disabled="entity.deptCode === 'root_dept'"
:extra-options="[{ label: $t('sysDept.root'), value: 0 }]"
v-model="entity.parentId"
dict-code="sysDept"
:data="parentDepartmentOptions"
:props="{ label: 'deptName', value: 'id', children: 'children' }"
check-strictly
clearable
filterable
/>
</ElFormItem>
<ElFormItem prop="deptName" :label="$t('sysDept.deptName')">

View File

@@ -78,7 +78,14 @@ vi.mock('element-plus', () => ({
},
}),
ElInput: defineComponent({ name: 'ElInput', setup: () => () => h('input') }),
ElMessage: { success: vi.fn() },
ElMessage: { error: vi.fn(), success: vi.fn() },
ElOption: defineComponent({ name: 'ElOption', setup: () => () => h('div') }),
ElSelect: defineComponent({
name: 'ElSelect',
setup(_, { slots }) {
return () => h('div', slots.default?.());
},
}),
}));
describe('sys job modal', () => {
@@ -112,7 +119,8 @@ describe('sys job modal', () => {
await flushPromises();
expect(apiMocks.get).toHaveBeenCalledWith(
'/api/v1/workflow/getRunningParameters?id=101',
'/api/v1/sysJob/workflowRunningParameters',
{ params: { id: '101' } },
);
expect(wrapper.get('form').attributes('data-loading')).toBe('false');
expect(wrapper.text()).toContain('所选工作流已不可用,请重新选择');

View File

@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { ElAlert, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
import {
ElAlert,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElOption,
ElSelect,
} from 'element-plus';
import { api } from '#/api/request';
import CronPicker from '#/components/cron/CronPicker.vue';
@@ -39,6 +47,8 @@ const initEntity = {
};
const entity = ref<any>(initEntity);
const btnLoading = ref(false);
const workflowOptions = ref<any[]>([]);
let workflowOptionsLoaded = false;
// 基础验证规则
const baseRules = ref({
jobName: [
@@ -85,8 +95,22 @@ function openDialog(row: any) {
} else {
entity.value = { ...initEntity };
}
void ensureWorkflowOptions();
dialogVisible.value = true;
}
async function ensureWorkflowOptions() {
if (workflowOptionsLoaded) {
return;
}
try {
const res = await api.get('/api/v1/sysJob/workflowOptions');
workflowOptions.value = Array.isArray(res.data) ? res.data : [];
workflowOptionsLoaded = true;
} catch {
workflowOptions.value = [];
ElMessage.error('工作流列表加载失败');
}
}
function save() {
if (btnLoading.value || workflowParamsSubmissionBlocked.value) {
return;
@@ -146,7 +170,9 @@ async function getWorkflowParams(v: any) {
workflowParams.value = [];
workflowParamsLoadError.value = '';
try {
const res = await api.get(`/api/v1/workflow/getRunningParameters?id=${v}`);
const res = await api.get('/api/v1/sysJob/workflowRunningParameters', {
params: { id: v },
});
if (requestId !== workflowParamsRequestId) {
return;
}
@@ -209,11 +235,19 @@ const str = '"param"';
{ required: true, message: $t('message.required'), trigger: 'blur' },
]"
>
<DictSelect
<ElSelect
v-model="entity.jobParams.workflowId"
dict-code="aiWorkFlow"
clearable
filterable
@change="workflowChange"
/>
>
<ElOption
v-for="workflow in workflowOptions"
:key="workflow.id"
:label="workflow.title"
:value="workflow.id"
/>
</ElSelect>
</ElFormItem>
<ElAlert
v-if="workflowParamsLoadError"

View File

@@ -12,6 +12,7 @@ import {
ElMessage,
ElOption,
ElSelect,
ElTreeSelect,
} from 'element-plus';
import { api } from '#/api/request';
@@ -44,6 +45,8 @@ const entity = ref<any>({
remark: '',
});
const btnLoading = ref(false);
const parentMenuOptions = ref<any[]>([]);
let parentMenuOptionsLoaded = false;
const rules = ref({
parentId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
@@ -62,12 +65,26 @@ const rules = ref({
],
});
// functions
function openDialog(row: any) {
async function openDialog(row: any) {
if (row.id) {
isAdd.value = false;
}
entity.value = row;
dialogVisible.value = true;
if (!parentMenuOptionsLoaded) {
try {
const res = await api.get('/api/v1/sysMenu/list', {
params: { asTree: true },
});
parentMenuOptions.value = [
{ id: 0, menuTitle: $t('sysMenu.root'), children: [] },
...(Array.isArray(res.data) ? res.data : []),
];
parentMenuOptionsLoaded = true;
} catch {
parentMenuOptions.value = [];
}
}
}
function save() {
saveForm.value?.validate((valid) => {
@@ -121,10 +138,13 @@ function closeDialog() {
class="easyflow-modal-form easyflow-modal-form--compact"
>
<ElFormItem prop="parentId" :label="$t('sysMenu.parentId')">
<DictSelect
:extra-options="[{ label: $t('sysMenu.root'), value: 0 }]"
<ElTreeSelect
v-model="entity.parentId"
dict-code="sysMenu"
:data="parentMenuOptions"
:props="{ label: 'menuTitle', value: 'id', children: 'children' }"
check-strictly
clearable
filterable
/>
</ElFormItem>
<ElFormItem prop="menuType" :label="$t('sysMenu.menuType')">

View File

@@ -19,7 +19,6 @@ defineExpose({
type ResourceType =
| 'AGENT'
| 'BOT'
| 'KNOWLEDGE'
| 'PLUGIN'
| 'RESOURCE'
@@ -47,7 +46,6 @@ const RESOURCE_SCOPE_GROUPS: Array<{
resourceType: ResourceType;
}> = [
{ resourceType: 'AGENT', label: $t('menus.ai.agents') },
{ resourceType: 'BOT', label: $t('bot.chatAssistant') },
{ resourceType: 'PLUGIN', label: $t('menus.ai.plugin') },
{ resourceType: 'WORKFLOW', label: $t('menus.ai.workflow') },
{ resourceType: 'KNOWLEDGE', label: $t('menus.ai.documentCollection') },
@@ -58,11 +56,11 @@ const saveForm = ref<FormInstance>();
const dialogVisible = ref(false);
const isAdd = ref(true);
const entity = ref<any>(buildDefaultEntity());
const categoryScopeLoaded = ref(false);
const formOptionsLoaded = ref(false);
const menuOptions = ref<any[]>([]);
const categoryScopeEditable = ref(false);
const categoryOptions = ref<Record<ResourceType, CategoryOption[]>>({
AGENT: [],
BOT: [],
KNOWLEDGE: [],
PLUGIN: [],
RESOURCE: [],
@@ -135,7 +133,7 @@ function openDialog(row: any = {}) {
getMenuIds(row.id);
}
getCategoryScopeDetail(row.id);
void ensureCategoryOptions();
void ensureFormOptions();
dialogVisible.value = true;
}
@@ -189,40 +187,32 @@ function getMenuIds(roleId: any) {
});
}
async function ensureCategoryOptions() {
if (categoryScopeLoaded.value) {
async function ensureFormOptions() {
if (formOptionsLoaded.value) {
return;
}
const requests = [
api.get('/api/v1/agentCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/botCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/pluginCategory/list'),
api.get('/api/v1/workflowCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/documentCollectionCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
api.get('/api/v1/resourceCategory/list', {
params: { sortKey: 'sortNo', sortType: 'asc' },
}),
];
const [agentRes, botRes, pluginRes, workflowRes, knowledgeRes, resourceRes] =
await Promise.all(requests);
categoryOptions.value = {
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
PLUGIN: normalizeCategoryOptions(pluginRes.data, 'name'),
RESOURCE: normalizeCategoryOptions(resourceRes.data, 'categoryName'),
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
};
categoryScopeLoaded.value = true;
try {
const res = await api.get('/api/v1/sysRole/formOptions');
const categories = res.data?.categories || {};
menuOptions.value = Array.isArray(res.data?.menus) ? res.data.menus : [];
categoryOptions.value = {
AGENT: normalizeCategoryOptions(categories.AGENT, 'categoryName'),
KNOWLEDGE: normalizeCategoryOptions(categories.KNOWLEDGE, 'categoryName'),
PLUGIN: normalizeCategoryOptions(categories.PLUGIN, 'categoryName'),
RESOURCE: normalizeCategoryOptions(categories.RESOURCE, 'categoryName'),
WORKFLOW: normalizeCategoryOptions(categories.WORKFLOW, 'categoryName'),
};
formOptionsLoaded.value = true;
} catch {
menuOptions.value = [];
categoryOptions.value = {
AGENT: [],
KNOWLEDGE: [],
PLUGIN: [],
RESOURCE: [],
WORKFLOW: [],
};
}
}
function normalizeCategoryOptions(data: any[] = [], labelKey: string) {
@@ -296,18 +286,24 @@ function getCategoryScopeDetail(roleId: number | string) {
categoryScopeEditable.value = !!res.data?.editable;
categoryScopeDetail.value = {
roleId,
scopes: (res.data?.scopes || buildDefaultScopes()).map((item: any) => ({
categoryIds: item.categoryIds || [],
resourceType: item.resourceType,
scopeMode: item.scopeMode || 'CUSTOM',
})),
scopes: (res.data?.scopes || buildDefaultScopes())
.filter((item: any) =>
RESOURCE_SCOPE_GROUPS.some(
(group) => group.resourceType === item.resourceType,
),
)
.map((item: any) => ({
categoryIds: item.categoryIds || [],
resourceType: item.resourceType,
scopeMode: item.scopeMode || 'CUSTOM',
})),
};
syncCategoryTreeCheckedKeys(categoryScopeDetail.value.scopes);
});
}
async function saveCategoryScope(roleId: number | string) {
await ensureCategoryOptions();
await ensureFormOptions();
const scopes = buildScopeItemsFromTree();
categoryScopeDetail.value = {
roleId,
@@ -353,7 +349,7 @@ async function saveCategoryScope(roleId: number | string) {
</ElFormItem>
<ElFormItem :label="$t('sysRole.menuPermission')">
<Tree
data-url="/api/v1/sysMenu/list?asTree=true"
:data="menuOptions"
v-model="entity.menuIds"
:default-props="{
label: 'menuTitle',