feat: 支持工作流插件复用与试运行
- 新增工作流插件类型、发布快照同步、实时可用性与下线影响检查 - 收口绑定候选、分类权限、间接环路校验与运行态优雅降级 - 补齐管理端工作流插件配置、详情与试运行界面及定向测试
This commit is contained in:
@@ -75,6 +75,9 @@
|
||||
"submitDeleteApprovalConfirm": "Delete the current workflow?",
|
||||
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
|
||||
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
|
||||
"offlineImpactBoundPluginsIntro": "This workflow is currently bound to the following plugins:",
|
||||
"offlineImpactBoundPluginsFooter": "After offline approval succeeds, these plugins will automatically become unavailable and show the reason in plugin management.",
|
||||
"offlineImpactBoundMixedFooter": "After offline approval succeeds, the system will remove the workflow from bots and mark the related plugins as unavailable.",
|
||||
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
||||
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
||||
"check": "Check",
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"type": "Type",
|
||||
"typeHttp": "HTTP Plugin",
|
||||
"typeWorkflow": "Workflow Plugin",
|
||||
"baseUrl": "BaseUrl",
|
||||
"workflowId": "Bound workflow",
|
||||
"workflowTitle": "Workflow title",
|
||||
"authType": "AuthType",
|
||||
"created": "Created",
|
||||
"icon": "Icon",
|
||||
@@ -20,7 +24,8 @@
|
||||
"placeholder": {
|
||||
"name": "Please enter plugin name",
|
||||
"description": "Please enter plugin description",
|
||||
"categorize": "Please enter categorize"
|
||||
"categorize": "Please enter categorize",
|
||||
"workflow": "Please select a published workflow"
|
||||
},
|
||||
"button": {
|
||||
"addPlugin": "Add Plugin",
|
||||
@@ -29,5 +34,11 @@
|
||||
},
|
||||
"toolsManagement": "Tools Management",
|
||||
"searchUsers": "Search Users",
|
||||
"parameterValue": "ParameterValue"
|
||||
"parameterValue": "ParameterValue",
|
||||
"workflow": "Workflow",
|
||||
"workflowPluginHint": "Workflow plugins mirror the published snapshot of the target workflow. Availability is evaluated in real time against workflow permissions and approval status.",
|
||||
"workflowPluginUnavailable": "This workflow plugin is unavailable",
|
||||
"workflowSnapshotSynced": "Published snapshot synced",
|
||||
"reasonMessage": "Reason",
|
||||
"onlyPublishedWorkflow": "Only published workflows that you can currently access are selectable."
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"debugStatus": "DebugStatus",
|
||||
"englishName": "EnglishName",
|
||||
"createPluginTool": "Create tool",
|
||||
"systemManaged": "System synced",
|
||||
"pluginToolEdit": {
|
||||
"basicInfo": "Basic Info",
|
||||
"configureInputParameters": "Configure input parameters",
|
||||
@@ -21,11 +22,16 @@
|
||||
"toolPath": "Tool path",
|
||||
"requestMethod": "RequestMethod",
|
||||
"runResult": "Run result",
|
||||
"run": "run"
|
||||
"run": "run",
|
||||
"workflowTarget": "Target workflow",
|
||||
"unavailableHint": "The bound workflow is currently unavailable, so execution will not be started.",
|
||||
"runWorkflowStepsEmpty": "After starting a trial run, each node execution result will be shown here.",
|
||||
"workflowStepsPending": "The trial run has started. Waiting for node execution details..."
|
||||
},
|
||||
"parameterName": "Name",
|
||||
"parameterDescription": "Description",
|
||||
"parameterType": "Type",
|
||||
"direction": "Direction",
|
||||
"inputMethod": "InputMethod",
|
||||
"required": "Required",
|
||||
"defaultValue": "DefaultValue",
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
|
||||
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
|
||||
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
|
||||
"offlineImpactBoundPluginsIntro": "当前工作流被以下插件绑定:",
|
||||
"offlineImpactBoundPluginsFooter": "下线审批通过后,这些插件会自动变为不可用,并在插件页展示对应原因。",
|
||||
"offlineImpactBoundMixedFooter": "下线审批通过后,系统会自动从聊天助手中解绑该工作流,同时让相关插件进入不可用状态。",
|
||||
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"check": "检查",
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"type": "类型",
|
||||
"typeHttp": "HTTP 插件",
|
||||
"typeWorkflow": "工作流插件",
|
||||
"baseUrl": "基础URL",
|
||||
"workflowId": "绑定工作流",
|
||||
"workflowTitle": "工作流名称",
|
||||
"authType": "认证方式",
|
||||
"created": "创建时间",
|
||||
"icon": "图标地址",
|
||||
@@ -20,7 +24,8 @@
|
||||
"placeholder": {
|
||||
"name": "请输入插件名称",
|
||||
"description": "请输入插件描述",
|
||||
"categorize": "请选择分类"
|
||||
"categorize": "请选择分类",
|
||||
"workflow": "请选择已发布工作流"
|
||||
},
|
||||
"button": {
|
||||
"addPlugin": "新增插件",
|
||||
@@ -29,5 +34,11 @@
|
||||
},
|
||||
"toolsManagement": "工具管理",
|
||||
"searchUsers": "搜索用户",
|
||||
"parameterValue": "参数值"
|
||||
"parameterValue": "参数值",
|
||||
"workflow": "工作流",
|
||||
"workflowPluginHint": "工作流插件会自动镜像目标工作流的已发布快照,插件可用性会实时跟随工作流权限和审批状态变化。",
|
||||
"workflowPluginUnavailable": "当前工作流插件不可用",
|
||||
"workflowSnapshotSynced": "已同步发布快照",
|
||||
"reasonMessage": "不可用原因",
|
||||
"onlyPublishedWorkflow": "仅支持选择已发布且当前可访问的工作流。"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"debugStatus": "调试状态【0失败 1成功】",
|
||||
"englishName": "英文名称",
|
||||
"createPluginTool": "创建工具",
|
||||
"systemManaged": "系统同步",
|
||||
"pluginToolEdit": {
|
||||
"basicInfo": "基本信息",
|
||||
"configureInputParameters": "配置输入参数",
|
||||
@@ -21,11 +22,16 @@
|
||||
"toolPath": "工具路径",
|
||||
"requestMethod": "请求方法",
|
||||
"runResult": "运行结果",
|
||||
"run": "运行"
|
||||
"run": "运行",
|
||||
"workflowTarget": "目标工作流",
|
||||
"unavailableHint": "当前绑定工作流不可用,本次不会发起执行。",
|
||||
"runWorkflowStepsEmpty": "开始试运行后,这里会展示每个节点的执行结果。",
|
||||
"workflowStepsPending": "试运行已发起,正在等待节点执行信息..."
|
||||
},
|
||||
"parameterName": "参数名称",
|
||||
"parameterDescription": "参数描述",
|
||||
"parameterType": "参数类型",
|
||||
"direction": "方向",
|
||||
"inputMethod": "传入方法",
|
||||
"required": "是否必填",
|
||||
"defaultValue": "默认值",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { Plus, Remove } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
@@ -22,15 +24,28 @@ import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const embeddingLlmList = ref<any>([]);
|
||||
const rerankerLlmList = ref<any>([]);
|
||||
const categoryList = ref<any[]>([]);
|
||||
interface headersType {
|
||||
interface HeaderItem {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
const authTypeList = ref<headersType[]>([
|
||||
|
||||
interface WorkflowCandidate {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const categoryList = ref<any[]>([]);
|
||||
const workflowCandidates = ref<WorkflowCandidate[]>([]);
|
||||
const tempAddHeaders = ref<HeaderItem[]>([]);
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
|
||||
const authTypeList = [
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
@@ -39,79 +54,211 @@ const authTypeList = ref<headersType[]>([
|
||||
label: 'Service token / ApiKey',
|
||||
value: 'apiKey',
|
||||
},
|
||||
]);
|
||||
onMounted(() => {
|
||||
api.get('/api/v1/plugin/modelList?supportEmbed=true').then((res) => {
|
||||
embeddingLlmList.value = res.data;
|
||||
});
|
||||
api
|
||||
.get('/api/v1/plugin/modelList?supportRerankerLlmList=true')
|
||||
.then((res) => {
|
||||
rerankerLlmList.value = res.data;
|
||||
});
|
||||
api.get('/api/v1/pluginCategory/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
categoryList.value = res.data;
|
||||
}
|
||||
});
|
||||
});
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const tempAddHeaders = ref<headersType[]>([]);
|
||||
const entity = ref<any>({
|
||||
alias: '',
|
||||
categoryIds: [],
|
||||
deptId: '',
|
||||
icon: '',
|
||||
title: '',
|
||||
authType: 'none',
|
||||
description: '',
|
||||
englishName: '',
|
||||
headers: '',
|
||||
position: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
];
|
||||
|
||||
const pluginTypeOptions = [
|
||||
{
|
||||
label: $t('plugin.typeHttp'),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: $t('plugin.typeWorkflow'),
|
||||
value: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const isWorkflowType = computed(
|
||||
() => Number(entity.value.type || 1) === 2,
|
||||
);
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
description: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
baseUrl: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!isWorkflowType.value && !value) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
workflowId: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (isWorkflowType.value && !value) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
authType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
tokenKey: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
tokenValue: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!isWorkflowType.value && !value) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
position: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (
|
||||
!isWorkflowType.value &&
|
||||
entity.value.authType === 'apiKey' &&
|
||||
!value
|
||||
) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
tokenKey: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (
|
||||
!isWorkflowType.value &&
|
||||
entity.value.authType === 'apiKey' &&
|
||||
!value
|
||||
) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
tokenValue: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (
|
||||
!isWorkflowType.value &&
|
||||
entity.value.authType === 'apiKey' &&
|
||||
!value
|
||||
) {
|
||||
callback(new Error($t('message.required')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => entity.value.type,
|
||||
(type) => {
|
||||
if (Number(type || 1) === 2) {
|
||||
entity.value.baseUrl = '';
|
||||
entity.value.authType = 'none';
|
||||
entity.value.position = '';
|
||||
entity.value.tokenKey = '';
|
||||
entity.value.tokenValue = '';
|
||||
tempAddHeaders.value = [];
|
||||
return;
|
||||
}
|
||||
entity.value.workflowId = '';
|
||||
entity.value.workflowTitle = '';
|
||||
entity.value.available = true;
|
||||
entity.value.reasonMessage = '';
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadCategories(), loadWorkflowCandidates()]);
|
||||
});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
tempAddHeaders.value = [];
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
if (row.headers) {
|
||||
tempAddHeaders.value = JSON.parse(row.headers);
|
||||
}
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
function createDefaultEntity() {
|
||||
return {
|
||||
alias: '',
|
||||
authType: 'none',
|
||||
available: true,
|
||||
baseUrl: '',
|
||||
categoryIds: [],
|
||||
deptId: '',
|
||||
description: '',
|
||||
englishName: '',
|
||||
headers: '',
|
||||
icon: '',
|
||||
name: '',
|
||||
pluginType: 1,
|
||||
position: '',
|
||||
reasonMessage: '',
|
||||
title: '',
|
||||
tokenKey: '',
|
||||
tokenValue: '',
|
||||
type: 1,
|
||||
workflowId: '',
|
||||
workflowTitle: '',
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
const res = await api.get('/api/v1/pluginCategory/list');
|
||||
if (res.errorCode === 0) {
|
||||
categoryList.value = res.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkflowCandidates(keyword: string = '') {
|
||||
const res = await api.get('/api/v1/plugin/workflowCandidates', {
|
||||
params: {
|
||||
keyword,
|
||||
},
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
workflowCandidates.value = Array.isArray(res.data) ? res.data : [];
|
||||
ensureCurrentWorkflowOption();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCurrentWorkflowOption() {
|
||||
if (!entity.value.workflowId || !entity.value.workflowTitle) {
|
||||
return;
|
||||
}
|
||||
const exists = workflowCandidates.value.some(
|
||||
(item) => String(item.id) === String(entity.value.workflowId),
|
||||
);
|
||||
if (!exists) {
|
||||
workflowCandidates.value.unshift({
|
||||
id: entity.value.workflowId,
|
||||
title: entity.value.workflowTitle,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog(row: any) {
|
||||
tempAddHeaders.value = row.headers ? JSON.parse(row.headers) : [];
|
||||
isAdd.value = !row.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
...row,
|
||||
authType: row.authType || 'none',
|
||||
categoryIds: row.categoryIds?.map((item: any) => item.id) || [],
|
||||
type: Number(row.type || row.pluginType || 1),
|
||||
};
|
||||
ensureCurrentWorkflowOption();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -131,83 +278,86 @@ async function syncPluginCategories(pluginId: string, categoryIds: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePayload() {
|
||||
const plainEntity = { ...entity.value };
|
||||
const categoryIds = [...(plainEntity.categoryIds || [])];
|
||||
delete plainEntity.categoryIds;
|
||||
if (isWorkflowType.value) {
|
||||
plainEntity.baseUrl = '';
|
||||
plainEntity.headers = [];
|
||||
plainEntity.authType = 'none';
|
||||
plainEntity.position = '';
|
||||
plainEntity.tokenKey = '';
|
||||
plainEntity.tokenValue = '';
|
||||
}
|
||||
return {
|
||||
payload: {
|
||||
...plainEntity,
|
||||
headers: isWorkflowType.value ? [] : [...tempAddHeaders.value],
|
||||
},
|
||||
categoryIds,
|
||||
};
|
||||
}
|
||||
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
const plainEntity = { ...entity.value };
|
||||
const plainHeaders = [...tempAddHeaders.value];
|
||||
const categoryIds = [...(plainEntity.categoryIds || [])];
|
||||
delete plainEntity.categoryIds;
|
||||
if (isAdd.value) {
|
||||
api
|
||||
.post('/api/v1/plugin/plugin/save', {
|
||||
...plainEntity,
|
||||
headers: plainHeaders,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.errorCode === 0) {
|
||||
const pluginId =
|
||||
res.data?.id || plainEntity.id || entity.value.id;
|
||||
if (!pluginId) {
|
||||
throw new Error('插件保存成功,但未返回插件ID');
|
||||
}
|
||||
await syncPluginCategories(pluginId, categoryIds);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success($t('message.saveOkMessage'));
|
||||
emit('reload');
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||
})
|
||||
.finally(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.post('/api/v1/plugin/plugin/update', {
|
||||
...plainEntity,
|
||||
headers: plainHeaders,
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.errorCode === 0) {
|
||||
await syncPluginCategories(entity.value.id, categoryIds);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
emit('reload');
|
||||
} else {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||
})
|
||||
.finally(() => {
|
||||
btnLoading.value = false;
|
||||
});
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
const { payload, categoryIds } = normalizePayload();
|
||||
const requestUrl = isAdd.value
|
||||
? '/api/v1/plugin/plugin/save'
|
||||
: '/api/v1/plugin/plugin/update';
|
||||
try {
|
||||
const res = await api.post(requestUrl, payload);
|
||||
if (res.errorCode !== 0) {
|
||||
ElMessage.error(res.message);
|
||||
return;
|
||||
}
|
||||
const pluginId = res.data?.id || payload.id || entity.value.id;
|
||||
if (!pluginId) {
|
||||
throw new Error('插件保存成功,但未返回插件ID');
|
||||
}
|
||||
await syncPluginCategories(pluginId, categoryIds);
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success(
|
||||
isAdd.value ? $t('message.saveOkMessage') : $t('message.updateOkMessage'),
|
||||
);
|
||||
emit('reload');
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || $t('message.saveFailMessage'));
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
saveForm.value?.resetFields();
|
||||
isAdd.value = true;
|
||||
tempAddHeaders.value = [];
|
||||
entity.value = {};
|
||||
entity.value = createDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function addHeader() {
|
||||
tempAddHeaders.value.push({
|
||||
label: '',
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
|
||||
function removeHeader(index: number) {
|
||||
tempAddHeaders.value.splice(index, 1);
|
||||
}
|
||||
|
||||
function handleWorkflowChange(value: string) {
|
||||
const target = workflowCandidates.value.find(
|
||||
(item) => String(item.id) === String(value),
|
||||
);
|
||||
entity.value.workflowTitle = target?.title || '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -237,15 +387,22 @@ function removeHeader(index: number) {
|
||||
>
|
||||
<UploadAvatar v-model="entity.icon" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="type" :label="$t('plugin.type')">
|
||||
<ElSelect v-model="entity.type" :disabled="!isAdd">
|
||||
<ElOption
|
||||
v-for="item in pluginTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="name" :label="$t('plugin.name')">
|
||||
<ElInput
|
||||
v-model.trim="entity.name"
|
||||
:placeholder="$t('plugin.placeholder.name')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||||
<ElInput v-model.trim="entity.baseUrl" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="description" :label="$t('plugin.description')">
|
||||
<ElInput
|
||||
v-model.trim="entity.description"
|
||||
@@ -270,61 +427,108 @@ function removeHeader(index: number) {
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="Headers" label="Headers">
|
||||
<div
|
||||
class="headers-container-reduce flex flex-row gap-4"
|
||||
v-for="(item, index) in tempAddHeaders"
|
||||
:key="index"
|
||||
>
|
||||
<div class="head-con-content flex flex-row gap-4">
|
||||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||||
<ElIcon size="20" @click="removeHeader" style="cursor: pointer">
|
||||
<Remove />
|
||||
</ElIcon>
|
||||
<template v-if="isWorkflowType">
|
||||
<ElAlert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb-4"
|
||||
:title="$t('plugin.workflowPluginHint')"
|
||||
/>
|
||||
<ElFormItem prop="workflowId" :label="$t('plugin.workflowId')">
|
||||
<ElSelect
|
||||
v-model="entity.workflowId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="loadWorkflowCandidates"
|
||||
:placeholder="$t('plugin.placeholder.workflow')"
|
||||
@change="handleWorkflowChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in workflowCandidates"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div class="form-helper-text">
|
||||
{{ $t('plugin.onlyPublishedWorkflow') }}
|
||||
</div>
|
||||
</div>
|
||||
<ElButton @click="addHeader" class="addHeadersBtn">
|
||||
<ElIcon size="18" style="margin-right: 4px">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}headers
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="authType" :label="$t('plugin.authType')">
|
||||
<ElSelect v-model="entity.authType">
|
||||
<ElOption
|
||||
v-for="item in authTypeList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="position"
|
||||
:label="$t('plugin.position')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElRadioGroup v-model="entity.position">
|
||||
<ElRadio value="headers">headers</ElRadio>
|
||||
<ElRadio value="query">query</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="tokenKey"
|
||||
:label="$t('plugin.tokenKey')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="tokenValue"
|
||||
:label="$t('plugin.tokenValue')"
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenValue" />
|
||||
</ElFormItem>
|
||||
</ElFormItem>
|
||||
<ElAlert
|
||||
v-if="!entity.available && entity.reasonMessage"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="$t('plugin.workflowPluginUnavailable')"
|
||||
:description="entity.reasonMessage"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElFormItem prop="baseUrl" :label="$t('plugin.baseUrl')">
|
||||
<ElInput v-model.trim="entity.baseUrl" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="Headers" label="Headers">
|
||||
<div
|
||||
v-for="(item, index) in tempAddHeaders"
|
||||
:key="index"
|
||||
class="headers-container-reduce flex flex-row gap-4"
|
||||
>
|
||||
<div class="head-con-content flex flex-row gap-4">
|
||||
<ElInput v-model.trim="item.label" placeholder="header name" />
|
||||
<ElInput v-model.trim="item.value" placeholder="header value" />
|
||||
<ElIcon
|
||||
size="20"
|
||||
style="cursor: pointer"
|
||||
@click="removeHeader(index)"
|
||||
>
|
||||
<Remove />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton class="addHeadersBtn" @click="addHeader">
|
||||
<ElIcon size="18" style="margin-right: 4px">
|
||||
<Plus />
|
||||
</ElIcon>
|
||||
{{ $t('button.add') }}headers
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="authType" :label="$t('plugin.authType')">
|
||||
<ElSelect v-model="entity.authType">
|
||||
<ElOption
|
||||
v-for="item in authTypeList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value || ''"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
prop="position"
|
||||
:label="$t('plugin.position')"
|
||||
>
|
||||
<ElRadioGroup v-model="entity.position">
|
||||
<ElRadio value="headers">headers</ElRadio>
|
||||
<ElRadio value="query">query</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
prop="tokenKey"
|
||||
:label="$t('plugin.tokenKey')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenKey" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="entity.authType === 'apiKey'"
|
||||
prop="tokenValue"
|
||||
:label="$t('plugin.tokenValue')"
|
||||
>
|
||||
<ElInput v-model.trim="entity.tokenValue" />
|
||||
</ElFormItem>
|
||||
</template>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
@@ -346,4 +550,11 @@ function removeHeader(index: number) {
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-helper-text {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
@@ -94,6 +95,10 @@ const actions: ActionButton[] = [
|
||||
},
|
||||
},
|
||||
];
|
||||
const pluginTypeTagMap = {
|
||||
1: $t('plugin.typeHttp'),
|
||||
2: $t('plugin.typeWorkflow'),
|
||||
};
|
||||
const categoryList = ref<PluginCategory[]>([]);
|
||||
const controlBtns = [
|
||||
{
|
||||
@@ -265,11 +270,26 @@ const handleClickCategory = (item: PluginCategory) => {
|
||||
title-field="title"
|
||||
icon-field="icon"
|
||||
desc-field="description"
|
||||
tag-field="type"
|
||||
:tag-map="pluginTypeTagMap"
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
:default-icon="defaultPluginIcon"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElTag
|
||||
v-if="item.type === 2 && item.available === false"
|
||||
type="danger"
|
||||
effect="light"
|
||||
round
|
||||
>
|
||||
{{
|
||||
item.reasonMessage || $t('plugin.workflowPluginUnavailable')
|
||||
}}
|
||||
</ElTag>
|
||||
</template>
|
||||
</CardPage>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
|
||||
@@ -9,12 +9,14 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => [],
|
||||
editable: false,
|
||||
isEditOutput: false,
|
||||
payloadMode: 'plugin',
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
export interface TreeTableNode {
|
||||
key: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
method?: 'Body' | 'Header' | 'Path' | 'Query';
|
||||
@@ -29,6 +31,7 @@ interface Props {
|
||||
modelValue?: TreeTableNode[];
|
||||
editable?: boolean;
|
||||
isEditOutput?: boolean;
|
||||
payloadMode?: 'plugin' | 'workflow';
|
||||
}
|
||||
|
||||
const data = ref<TreeTableNode[]>([]);
|
||||
@@ -48,8 +51,12 @@ watch(
|
||||
);
|
||||
|
||||
// 计算缩进宽度
|
||||
const getNodeKey = (record?: Partial<TreeTableNode>): string => {
|
||||
return String(record?.key || record?.id || '');
|
||||
};
|
||||
|
||||
const getIndentWidth = (record: TreeTableNode): number => {
|
||||
const level = String(record.key).split('-').length - 1;
|
||||
const level = getNodeKey(record).split('-').length - 1;
|
||||
const indentSize = 20;
|
||||
return level > 0 ? level * indentSize : 0;
|
||||
};
|
||||
@@ -65,7 +72,7 @@ const onExpand = (_row: TreeTableNode, expandedRows: TreeTableNode[]) => {
|
||||
};
|
||||
|
||||
// 验证字段
|
||||
const validateFields = (): boolean => {
|
||||
const validatePluginFields = (): boolean => {
|
||||
const newErrors: Record<
|
||||
string,
|
||||
Partial<Record<keyof TreeTableNode, string>>
|
||||
@@ -75,6 +82,7 @@ const validateFields = (): boolean => {
|
||||
const checkNode = (node: TreeTableNode): boolean => {
|
||||
const { name, description, method, type } = node;
|
||||
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
||||
const nodeKey = getNodeKey(node);
|
||||
|
||||
if (!name?.trim()) {
|
||||
nodeErrors.name = $t('message.cannotBeEmpty.name');
|
||||
@@ -96,8 +104,8 @@ const validateFields = (): boolean => {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (Object.keys(nodeErrors).length > 0) {
|
||||
newErrors[node.key] = nodeErrors;
|
||||
if (nodeKey && Object.keys(nodeErrors).length > 0) {
|
||||
newErrors[nodeKey] = nodeErrors;
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
@@ -117,17 +125,118 @@ const validateFields = (): boolean => {
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const isBlankValue = (value: unknown): boolean => {
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length === 0;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return Object.keys(value as Record<string, unknown>).length === 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const normalizeNodeType = (node: TreeTableNode): string => {
|
||||
return String(node.type || '').trim();
|
||||
};
|
||||
|
||||
const parseWorkflowNodeValue = (node: TreeTableNode): any => {
|
||||
const type = normalizeNodeType(node);
|
||||
if (node.children?.length) {
|
||||
if (type === 'Object') {
|
||||
return buildWorkflowPayload(node.children);
|
||||
}
|
||||
if (type.includes('Array')) {
|
||||
return parseArrayValue(node);
|
||||
}
|
||||
}
|
||||
if (type.includes('Array')) {
|
||||
return parseArrayValue(node);
|
||||
}
|
||||
return node.defaultValue;
|
||||
};
|
||||
|
||||
const parseArrayValue = (node: TreeTableNode): any[] => {
|
||||
if (Array.isArray(node.defaultValue as any)) {
|
||||
return node.defaultValue as any[];
|
||||
}
|
||||
const raw = String(node.defaultValue || '').trim();
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [parsed];
|
||||
} catch {
|
||||
return [raw];
|
||||
}
|
||||
};
|
||||
|
||||
const buildWorkflowPayload = (nodes: TreeTableNode[]): Record<string, any> => {
|
||||
const payload: Record<string, any> = {};
|
||||
for (const node of nodes) {
|
||||
if (!node?.name) {
|
||||
continue;
|
||||
}
|
||||
payload[node.name] = parseWorkflowNodeValue(node);
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const validateWorkflowFields = (): boolean => {
|
||||
const newErrors: Record<
|
||||
string,
|
||||
Partial<Record<keyof TreeTableNode, string>>
|
||||
> = {};
|
||||
let isValid = true;
|
||||
|
||||
const checkNode = (node: TreeTableNode) => {
|
||||
const nodeKey = getNodeKey(node);
|
||||
const nodeErrors: Partial<Record<keyof TreeTableNode, string>> = {};
|
||||
const value = parseWorkflowNodeValue(node);
|
||||
|
||||
if (node.required && isBlankValue(value)) {
|
||||
nodeErrors.defaultValue = $t('message.required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (nodeKey && Object.keys(nodeErrors).length > 0) {
|
||||
newErrors[nodeKey] = nodeErrors;
|
||||
}
|
||||
|
||||
node.children?.forEach(checkNode);
|
||||
};
|
||||
|
||||
data.value.forEach((node) => {
|
||||
checkNode(node);
|
||||
});
|
||||
errors.value = newErrors;
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// 判断是否为根节点
|
||||
const isRootNode = (record: TreeTableNode): boolean => {
|
||||
return !record.key.includes('-');
|
||||
return !getNodeKey(record).includes('-');
|
||||
};
|
||||
|
||||
// 提交参数
|
||||
const handleSubmitParams = () => {
|
||||
if (!validateFields()) {
|
||||
const valid =
|
||||
props.payloadMode === 'workflow'
|
||||
? validateWorkflowFields()
|
||||
: validatePluginFields();
|
||||
if (valid !== true) {
|
||||
ElMessage.error($t('message.completeForm'));
|
||||
return;
|
||||
}
|
||||
if (props.payloadMode === 'workflow') {
|
||||
return buildWorkflowPayload(data.value);
|
||||
}
|
||||
return data.value;
|
||||
};
|
||||
|
||||
@@ -145,7 +254,7 @@ interface Emits {
|
||||
<div class="tree-table-container">
|
||||
<ElTable
|
||||
:data="data"
|
||||
row-key="key"
|
||||
:row-key="getNodeKey"
|
||||
:border="true"
|
||||
size="default"
|
||||
:expand-row-keys="expandedKeys"
|
||||
@@ -160,7 +269,7 @@ interface Emits {
|
||||
<template #default="{ row }">
|
||||
<div class="name-cell">
|
||||
<div
|
||||
v-if="!props.editable"
|
||||
v-if="!props.editable || props.payloadMode === 'workflow'"
|
||||
:style="{ paddingLeft: `${getIndentWidth(row)}px` }"
|
||||
>
|
||||
{{ row.name || '' }}
|
||||
@@ -175,11 +284,11 @@ interface Emits {
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="errors[row.key]?.name"
|
||||
v-if="errors[getNodeKey(row)]?.name"
|
||||
class="error-message"
|
||||
:style="{ marginLeft: `${getIndentWidth(row)}px` }"
|
||||
>
|
||||
{{ errors[row.key]?.name }}
|
||||
{{ errors[getNodeKey(row)]?.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,12 +304,19 @@ interface Emits {
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type === 'Object'"></span>
|
||||
<span v-else-if="!props.editable">{{ row.defaultValue || '' }}</span>
|
||||
<ElInput
|
||||
v-else
|
||||
v-model="row.defaultValue"
|
||||
@input="handleDataChange"
|
||||
:disabled="!props.editable"
|
||||
/>
|
||||
<div v-else class="value-input-wrapper">
|
||||
<ElInput
|
||||
v-model="row.defaultValue"
|
||||
@input="handleDataChange"
|
||||
:disabled="!props.editable"
|
||||
/>
|
||||
<div
|
||||
v-if="errors[getNodeKey(row)]?.defaultValue"
|
||||
class="error-message"
|
||||
>
|
||||
{{ errors[getNodeKey(row)]?.defaultValue }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
@@ -243,6 +359,12 @@ interface Emits {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.value-input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { nextTick, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
import { sortNodes } from '@easyflow/utils';
|
||||
|
||||
import { VideoPlay } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElMenu, ElMenuItem } from 'element-plus';
|
||||
import { ElAlert, ElButton, ElMenu, ElMenuItem, ElMessage } from 'element-plus';
|
||||
import { JsonViewer } from 'vue3-json-viewer';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PluginRunParams from '#/views/ai/plugin/PluginRunParams.vue';
|
||||
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||
|
||||
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
|
||||
|
||||
@@ -26,42 +28,159 @@ watch(
|
||||
},
|
||||
);
|
||||
const dialogVisible = ref(false);
|
||||
const openDialog = () => {
|
||||
getPluginToolInfo();
|
||||
runResultResponse.value = null;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const runTitle = ref('');
|
||||
const runResult = ref('');
|
||||
const inputDataParams = ref<any>(null);
|
||||
const runResultResponse = ref<any>(null);
|
||||
function getPluginToolInfo() {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/search', {
|
||||
aiPluginToolId: props.pluginToolId,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
runTitle.value = `${res.data.aiPlugin.title} - ${res.data.data.name} ${$t(
|
||||
'pluginItem.inputData',
|
||||
)}`;
|
||||
runResult.value = `${$t('pluginItem.pluginToolEdit.runResult')}`;
|
||||
inputDataParams.value = JSON.parse(res.data.data.inputData || '[]');
|
||||
}
|
||||
});
|
||||
}
|
||||
const runParamsRef = ref();
|
||||
const runLoading = ref(false);
|
||||
const pluginAvailable = ref(true);
|
||||
const pluginReasonMessage = ref('');
|
||||
const isWorkflowPlugin = ref(false);
|
||||
const workflowId = ref<null | string>(null);
|
||||
const workflowSnapshot = ref<any>(null);
|
||||
const workflowNodeJson = ref<any[]>([]);
|
||||
const pollingNodes = ref<any[]>([]);
|
||||
const executeId = ref('');
|
||||
const pollingData = ref<any>({ nodes: {} });
|
||||
const initSignal = ref(false);
|
||||
const pollingTimer = ref<null | ReturnType<typeof setInterval>>(null);
|
||||
const activeIndex = ref('1');
|
||||
const dialogContentKey = ref(0);
|
||||
const dialogPreparing = ref(false);
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
function handleSelect(index: string) {
|
||||
activeIndex.value = index;
|
||||
|
||||
async function openDialog() {
|
||||
if (dialogPreparing.value) {
|
||||
return;
|
||||
}
|
||||
dialogPreparing.value = true;
|
||||
resetDialogState();
|
||||
const ready = await getPluginToolInfo();
|
||||
dialogPreparing.value = false;
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
dialogContentKey.value += 1;
|
||||
await nextTick();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
const runParamsRef = ref();
|
||||
const runLoading = ref(false);
|
||||
|
||||
function resetExecutionState() {
|
||||
stopPolling();
|
||||
runResultResponse.value = null;
|
||||
runLoading.value = false;
|
||||
executeId.value = '';
|
||||
pollingData.value = { nodes: {} };
|
||||
initSignal.value = !initSignal.value;
|
||||
activeIndex.value = '1';
|
||||
}
|
||||
|
||||
function resetDialogState() {
|
||||
resetExecutionState();
|
||||
runTitle.value = '';
|
||||
runResult.value = '';
|
||||
inputDataParams.value = [];
|
||||
pluginAvailable.value = true;
|
||||
pluginReasonMessage.value = '';
|
||||
isWorkflowPlugin.value = false;
|
||||
workflowId.value = null;
|
||||
workflowSnapshot.value = null;
|
||||
workflowNodeJson.value = [];
|
||||
pollingNodes.value = [];
|
||||
}
|
||||
|
||||
async function getPluginToolInfo() {
|
||||
try {
|
||||
const res = await api.post('/api/v1/pluginItem/tool/search', {
|
||||
aiPluginToolId: props.pluginToolId,
|
||||
});
|
||||
if (res.errorCode !== 0 || !res.data) {
|
||||
ElMessage.error(res?.message || '加载试运行信息失败');
|
||||
return false;
|
||||
}
|
||||
runTitle.value = `${res.data.aiPlugin.title} - ${res.data.data.name} ${$t(
|
||||
'pluginItem.inputData',
|
||||
)}`;
|
||||
runResult.value = `${$t('pluginItem.pluginToolEdit.runResult')}`;
|
||||
inputDataParams.value = JSON.parse(res.data.data.inputData || '[]');
|
||||
pluginAvailable.value = res.data.aiPlugin?.available !== false;
|
||||
pluginReasonMessage.value = res.data.aiPlugin?.reasonMessage || '';
|
||||
isWorkflowPlugin.value = Number(res.data.aiPlugin?.type || 1) === 2;
|
||||
workflowId.value = res.data.aiPlugin?.workflowId
|
||||
? String(res.data.aiPlugin.workflowId)
|
||||
: null;
|
||||
workflowSnapshot.value = res.data.workflowSnapshot || null;
|
||||
hydrateWorkflowNodes(workflowSnapshot.value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ElMessage.error(buildErrorResult(error).error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateWorkflowNodes(snapshot: any) {
|
||||
workflowNodeJson.value = [];
|
||||
pollingNodes.value = [];
|
||||
const content = snapshot?.content;
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const workflowContent = JSON.parse(content);
|
||||
workflowNodeJson.value = sortNodes(workflowContent) || [];
|
||||
pollingNodes.value = Array.isArray(workflowContent?.nodes)
|
||||
? workflowContent.nodes.map((node: any) => ({
|
||||
nodeId: node.id,
|
||||
nodeName: node?.data?.title || node.id,
|
||||
}))
|
||||
: [];
|
||||
} catch (error) {
|
||||
console.error('解析工作流插件快照失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
function buildErrorResult(error: any) {
|
||||
const responseData = error?.response?.data ?? {};
|
||||
return {
|
||||
error:
|
||||
responseData?.error ||
|
||||
responseData?.message ||
|
||||
error?.message ||
|
||||
'试运行失败',
|
||||
};
|
||||
}
|
||||
|
||||
function buildUnavailableResult() {
|
||||
runResultResponse.value = {
|
||||
skipped: true,
|
||||
reasonMessage:
|
||||
pluginReasonMessage.value ||
|
||||
$t('pluginItem.pluginToolEdit.unavailableHint'),
|
||||
};
|
||||
}
|
||||
|
||||
function handleSubmitRun() {
|
||||
if (!pluginAvailable.value) {
|
||||
buildUnavailableResult();
|
||||
return;
|
||||
}
|
||||
const runParams = runParamsRef.value?.handleSubmitParams?.();
|
||||
if (runParams === null || runParams === undefined) {
|
||||
return;
|
||||
}
|
||||
if (isWorkflowPlugin.value) {
|
||||
handleWorkflowSubmit(runParams);
|
||||
return;
|
||||
}
|
||||
handleHttpSubmit(runParams);
|
||||
}
|
||||
|
||||
function handleHttpSubmit(runParams: any) {
|
||||
runLoading.value = true;
|
||||
const runParams = runParamsRef.value.handleSubmitParams();
|
||||
api
|
||||
.post('/api/v1/pluginItem/test', {
|
||||
pluginToolId: props.pluginToolId,
|
||||
@@ -72,9 +191,160 @@ function handleSubmitRun() {
|
||||
runResultResponse.value = res.data;
|
||||
activeIndex.value = '2';
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
runResultResponse.value = buildErrorResult(error);
|
||||
})
|
||||
.finally(() => {
|
||||
runLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function handleWorkflowSubmit(runParams: any) {
|
||||
if (!workflowId.value) {
|
||||
runResultResponse.value = {
|
||||
error: '当前插件未绑定有效工作流,无法试运行。',
|
||||
};
|
||||
return;
|
||||
}
|
||||
resetExecutionState();
|
||||
runLoading.value = true;
|
||||
api
|
||||
.post('/api/v1/pluginItem/testAsync', {
|
||||
pluginToolId: props.pluginToolId,
|
||||
inputData: JSON.stringify(runParams),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0 && res.data) {
|
||||
executeId.value = String(res.data);
|
||||
pollingData.value = {
|
||||
executeId: executeId.value,
|
||||
status: 1,
|
||||
nodes: {},
|
||||
};
|
||||
runResultResponse.value = pollingData.value;
|
||||
activeIndex.value = '2';
|
||||
startPolling(executeId.value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
runResultResponse.value = buildErrorResult(error);
|
||||
})
|
||||
.finally(() => {
|
||||
runLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function startPolling(nextExecuteId: string) {
|
||||
stopPolling();
|
||||
pollingTimer.value = setInterval(() => {
|
||||
executePolling(nextExecuteId);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingTimer.value) {
|
||||
clearInterval(pollingTimer.value);
|
||||
pollingTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function executePolling(nextExecuteId: string) {
|
||||
api
|
||||
.post('/api/v1/pluginItem/testChainStatus', {
|
||||
executeId: nextExecuteId,
|
||||
nodes: pollingNodes.value,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
const nextData = {
|
||||
...res.data,
|
||||
nodes: res.data?.nodes || {},
|
||||
};
|
||||
pollingData.value = nextData;
|
||||
runResultResponse.value = nextData;
|
||||
if (nextData.status !== 1) {
|
||||
stopPolling();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
stopPolling();
|
||||
runResultResponse.value = buildErrorResult(error);
|
||||
});
|
||||
}
|
||||
|
||||
function resumeChain(payload: any) {
|
||||
if (!executeId.value) {
|
||||
return;
|
||||
}
|
||||
api
|
||||
.post('/api/v1/pluginItem/testResume', {
|
||||
executeId: executeId.value,
|
||||
confirmParams: payload?.confirmParams || {},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
startPolling(executeId.value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
runResultResponse.value = buildErrorResult(error);
|
||||
});
|
||||
}
|
||||
|
||||
function showWorkflowSteps() {
|
||||
return isWorkflowPlugin.value === true;
|
||||
}
|
||||
|
||||
function showWorkflowStepList() {
|
||||
return showWorkflowSteps();
|
||||
}
|
||||
|
||||
function showWorkflowResultAlert() {
|
||||
return (
|
||||
showWorkflowSteps() &&
|
||||
(!!runResultResponse.value?.error || !!runResultResponse.value?.skipped)
|
||||
);
|
||||
}
|
||||
|
||||
function workflowResultAlertType() {
|
||||
return runResultResponse.value?.skipped ? 'warning' : 'error';
|
||||
}
|
||||
|
||||
function workflowResultAlertMessage() {
|
||||
return (
|
||||
runResultResponse.value?.reasonMessage ||
|
||||
runResultResponse.value?.error ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function showWorkflowStepsEmpty() {
|
||||
return showWorkflowSteps() && workflowNodeJson.value.length === 0;
|
||||
}
|
||||
|
||||
function showHttpResultEmpty() {
|
||||
return (
|
||||
showWorkflowSteps() !== true &&
|
||||
activeIndex.value === '2' &&
|
||||
!runResultResponse.value
|
||||
);
|
||||
}
|
||||
|
||||
function handleSelect(index: string) {
|
||||
activeIndex.value = index;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
stopPolling();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -83,27 +353,37 @@ function handleSubmitRun() {
|
||||
width="80%"
|
||||
align-center
|
||||
:title="$t('pluginItem.pluginToolEdit.trialRun')"
|
||||
:before-close="() => (dialogVisible = false)"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<div class="run-test-container">
|
||||
<div :key="dialogContentKey" class="run-test-container">
|
||||
<div class="run-test-params">
|
||||
<div class="run-title-style">
|
||||
{{ runTitle }}
|
||||
</div>
|
||||
<ElAlert
|
||||
v-if="!pluginAvailable"
|
||||
class="mb-4"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="$t('pluginItem.pluginToolEdit.unavailableHint')"
|
||||
:description="pluginReasonMessage"
|
||||
/>
|
||||
<div>
|
||||
<PluginRunParams
|
||||
v-model="inputDataParams"
|
||||
:editable="true"
|
||||
:is-edit-output="true"
|
||||
ref="runParamsRef"
|
||||
v-model="inputDataParams"
|
||||
:editable="pluginAvailable"
|
||||
:is-edit-output="true"
|
||||
:payload-mode="isWorkflowPlugin ? 'workflow' : 'plugin'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="run-test-result">
|
||||
<div class="run-title-style">
|
||||
{{ runResult }}
|
||||
{{ showWorkflowSteps() ? $t('aiWorkflow.steps') : runResult }}
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!showWorkflowSteps()">
|
||||
<ElMenu
|
||||
:default-active="activeIndex"
|
||||
class="el-menu-demo"
|
||||
@@ -116,32 +396,58 @@ function handleSubmitRun() {
|
||||
</ElMenu>
|
||||
</div>
|
||||
<div class="run-res-json">
|
||||
<JsonViewer
|
||||
v-if="activeIndex === '1'"
|
||||
:value="inputDataParams || {}"
|
||||
copyable
|
||||
:expand-depth="Infinity"
|
||||
:theme="themeMode"
|
||||
/>
|
||||
<JsonViewer
|
||||
v-if="activeIndex === '2'"
|
||||
:value="runResultResponse || {}"
|
||||
copyable
|
||||
:expand-depth="Infinity"
|
||||
:theme="themeMode"
|
||||
/>
|
||||
<template v-if="showWorkflowSteps()">
|
||||
<ElAlert
|
||||
v-if="showWorkflowResultAlert()"
|
||||
class="run-result-alert"
|
||||
:type="workflowResultAlertType()"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="workflowResultAlertMessage()"
|
||||
/>
|
||||
<WorkflowSteps
|
||||
v-if="showWorkflowStepList()"
|
||||
:workflow-id="workflowId"
|
||||
:node-json="workflowNodeJson"
|
||||
:init-signal="initSignal"
|
||||
:polling-data="pollingData"
|
||||
@resume="resumeChain"
|
||||
/>
|
||||
<div v-if="showWorkflowStepsEmpty()" class="run-result-placeholder">
|
||||
{{ $t('pluginItem.pluginToolEdit.runWorkflowStepsEmpty') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<JsonViewer
|
||||
v-if="activeIndex === '1'"
|
||||
:value="inputDataParams || {}"
|
||||
copyable
|
||||
:expand-depth="Infinity"
|
||||
:theme="themeMode"
|
||||
/>
|
||||
<JsonViewer
|
||||
v-if="activeIndex === '2' && runResultResponse"
|
||||
:value="runResultResponse || {}"
|
||||
copyable
|
||||
:expand-depth="Infinity"
|
||||
:theme="themeMode"
|
||||
/>
|
||||
<div v-if="showHttpResultEmpty()" class="run-result-placeholder">
|
||||
{{ $t('common.noDataAvailable') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">
|
||||
<ElButton @click="closeDialog">
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="VideoPlay"
|
||||
@click="handleSubmitRun"
|
||||
:loading="runLoading"
|
||||
@click="handleSubmitRun"
|
||||
>
|
||||
{{ $t('pluginItem.pluginToolEdit.run') }}
|
||||
</ElButton>
|
||||
@@ -163,21 +469,34 @@ function handleSubmitRun() {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.run-res-json {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.run-test-result {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
.run-res-json {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.run-result-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.run-result-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
padding: 24px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--el-fill-color-extra-light);
|
||||
}
|
||||
|
||||
.run-title-style {
|
||||
@@ -186,30 +505,6 @@ function handleSubmitRun() {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editable-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.name-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name-input-wrapper .el-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell.first-column div) {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
@@ -14,23 +14,19 @@ import {
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PluginInputAndOutParams from '#/views/ai/plugin/PluginInputAndOutParams.vue';
|
||||
import PluginRunTestModal from '#/views/ai/plugin/PluginRunTestModal.vue';
|
||||
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const toolId = ref<string>((route.query.id as string) || '');
|
||||
|
||||
onMounted(() => {
|
||||
if (!toolId.value) {
|
||||
return;
|
||||
}
|
||||
getPluginToolInfo();
|
||||
});
|
||||
const pluginToolInfo = ref<any>({
|
||||
name: '',
|
||||
englishName: '',
|
||||
@@ -41,23 +37,12 @@ const pluginToolInfo = ref<any>({
|
||||
const pluginInfo = ref<any>({});
|
||||
const pluginInputData = ref<any[]>([]);
|
||||
const pluginOutputData = ref<any[]>([]);
|
||||
|
||||
function getPluginToolInfo() {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/search', {
|
||||
aiPluginToolId: toolId.value,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
pluginToolInfo.value = res.data.data;
|
||||
pluginInfo.value = res.data.aiPlugin;
|
||||
pluginInputData.value = JSON.parse(res.data.data.inputData || '[]');
|
||||
pluginOutputData.value = JSON.parse(res.data.data.outputData || '[]');
|
||||
}
|
||||
});
|
||||
}
|
||||
const pluginInputParamsEditable = ref(false);
|
||||
const pluginOutputParamsEditable = ref(false);
|
||||
const isWorkflowPlugin = ref(false);
|
||||
const pluginUnavailable = ref(false);
|
||||
const pluginUnavailableReason = ref('');
|
||||
const workflowSnapshot = ref<any>(null);
|
||||
|
||||
const pluginBasicCollapse = ref({
|
||||
title: $t('pluginItem.pluginToolEdit.basicInfo'),
|
||||
@@ -74,33 +59,12 @@ const pluginBasicCollapseOutputParams = ref({
|
||||
isOpen: false,
|
||||
isEdit: false,
|
||||
});
|
||||
|
||||
const pluginInputParamsRef = ref();
|
||||
const pluginOutputParamsRef = ref();
|
||||
const handleClickHeader = (index: number) => {
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isOpen = !pluginBasicCollapse.value.isOpen;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isOpen =
|
||||
!pluginBasicCollapseInputParams.value.isOpen;
|
||||
const saveForm = ref();
|
||||
const runTestRef = ref();
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isOpen =
|
||||
!pluginBasicCollapseOutputParams.value.isOpen;
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
|
||||
const back = () => {
|
||||
router.back();
|
||||
};
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
requestMethod: [
|
||||
@@ -125,118 +89,7 @@ const rules = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const saveForm = ref();
|
||||
const updatePluginTool = (index: number) => {
|
||||
if (index === 1) {
|
||||
if (!saveForm.value) return;
|
||||
saveForm.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
updatePluginToolInfo(index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updatePluginToolInfo(index);
|
||||
}
|
||||
};
|
||||
const updatePluginToolInfo = (index: number) => {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/update', {
|
||||
id: toolId.value,
|
||||
name: pluginToolInfo.value.name,
|
||||
englishName: pluginToolInfo.value.englishName,
|
||||
description: pluginToolInfo.value.description,
|
||||
basePath: pluginToolInfo.value.basePath,
|
||||
requestMethod: pluginToolInfo.value.requestMethod,
|
||||
inputData: JSON.stringify(pluginInputData.value),
|
||||
outputData: JSON.stringify(pluginOutputData.value),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleEdit = (index: number) => {
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = true;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = true;
|
||||
pluginBasicCollapseInputParams.value.isOpen = true;
|
||||
pluginInputParamsEditable.value = true;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = true;
|
||||
pluginBasicCollapseOutputParams.value.isOpen = true;
|
||||
pluginOutputParamsEditable.value = true;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
const handleSave = (index: number) => {
|
||||
if (index === 2) {
|
||||
try {
|
||||
// 调用校验方法,若抛异常则进入 catch
|
||||
pluginInputParamsRef.value.handleSubmitParams();
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (index === 3) {
|
||||
try {
|
||||
pluginOutputParamsRef.value.handleSubmitParams();
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pluginInputParamsEditable.value = false;
|
||||
updatePluginTool(index);
|
||||
};
|
||||
const handleCancel = (index: number) => {
|
||||
getPluginToolInfo();
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||
pluginInputParamsEditable.value = false;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||
pluginOutputParamsEditable.value = false;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
};
|
||||
const requestMethodOptions = [
|
||||
{
|
||||
label: 'POST',
|
||||
@@ -259,30 +112,259 @@ const requestMethodOptions = [
|
||||
value: 'PATCH',
|
||||
},
|
||||
];
|
||||
const runTestRef = ref();
|
||||
const handleOpenRunModal = () => {
|
||||
|
||||
onMounted(() => {
|
||||
if (!toolId.value) {
|
||||
return;
|
||||
}
|
||||
getPluginToolInfo();
|
||||
});
|
||||
|
||||
const workflowParameterRows = computed(() => [
|
||||
...flattenParameterRows(pluginInputData.value, 'input'),
|
||||
...flattenParameterRows(pluginOutputData.value, 'output'),
|
||||
]);
|
||||
|
||||
function getPluginToolInfo() {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/search', {
|
||||
aiPluginToolId: toolId.value,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
pluginToolInfo.value = res.data.data;
|
||||
pluginInfo.value = res.data.aiPlugin;
|
||||
pluginInputData.value = JSON.parse(res.data.data.inputData || '[]');
|
||||
pluginOutputData.value = JSON.parse(res.data.data.outputData || '[]');
|
||||
isWorkflowPlugin.value = Number(res.data.aiPlugin?.type || 1) === 2;
|
||||
pluginUnavailable.value = res.data.aiPlugin?.available === false;
|
||||
pluginUnavailableReason.value = res.data.aiPlugin?.reasonMessage || '';
|
||||
workflowSnapshot.value = res.data.workflowSnapshot || null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleClickHeader(index: number) {
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isOpen = !pluginBasicCollapse.value.isOpen;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isOpen =
|
||||
!pluginBasicCollapseInputParams.value.isOpen;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isOpen =
|
||||
!pluginBasicCollapseOutputParams.value.isOpen;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function back() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
function updatePluginTool(index: number) {
|
||||
if (index === 1) {
|
||||
if (!saveForm.value) {
|
||||
return;
|
||||
}
|
||||
saveForm.value.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
updatePluginToolInfo(index);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
updatePluginToolInfo(index);
|
||||
}
|
||||
|
||||
function updatePluginToolInfo(index: number) {
|
||||
api
|
||||
.post('/api/v1/pluginItem/tool/update', {
|
||||
id: toolId.value,
|
||||
name: pluginToolInfo.value.name,
|
||||
englishName: pluginToolInfo.value.englishName,
|
||||
description: pluginToolInfo.value.description,
|
||||
basePath: pluginToolInfo.value.basePath,
|
||||
requestMethod: pluginToolInfo.value.requestMethod,
|
||||
inputData: JSON.stringify(pluginInputData.value),
|
||||
outputData: JSON.stringify(pluginOutputData.value),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = false;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleEdit(index: number) {
|
||||
if (isWorkflowPlugin.value) {
|
||||
return;
|
||||
}
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = true;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = true;
|
||||
pluginBasicCollapseInputParams.value.isOpen = true;
|
||||
pluginInputParamsEditable.value = true;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = true;
|
||||
pluginBasicCollapseOutputParams.value.isOpen = true;
|
||||
pluginOutputParamsEditable.value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave(index: number) {
|
||||
if (index === 2) {
|
||||
pluginInputParamsRef.value.handleSubmitParams();
|
||||
}
|
||||
if (index === 3) {
|
||||
pluginOutputParamsRef.value.handleSubmitParams();
|
||||
}
|
||||
pluginInputParamsEditable.value = false;
|
||||
updatePluginTool(index);
|
||||
}
|
||||
|
||||
function handleCancel(index: number) {
|
||||
getPluginToolInfo();
|
||||
switch (index) {
|
||||
case 1: {
|
||||
pluginBasicCollapse.value.isEdit = false;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
pluginBasicCollapseInputParams.value.isEdit = false;
|
||||
pluginInputParamsEditable.value = false;
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
pluginBasicCollapseOutputParams.value.isEdit = false;
|
||||
pluginOutputParamsEditable.value = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenRunModal() {
|
||||
runTestRef.value.openDialog();
|
||||
};
|
||||
}
|
||||
|
||||
function flattenParameterRows(
|
||||
parameters: any[] = [],
|
||||
direction: 'input' | 'output',
|
||||
parentPath = '',
|
||||
): any[] {
|
||||
return parameters.flatMap((item) => {
|
||||
const currentName = String(item?.name || '').trim();
|
||||
const currentPath = currentName
|
||||
? parentPath
|
||||
? `${parentPath}.${currentName}`
|
||||
: currentName
|
||||
: parentPath || '-';
|
||||
const currentRow = {
|
||||
key: String(item?.id || item?.key || currentPath),
|
||||
direction,
|
||||
name: currentPath,
|
||||
type: String(item?.dataType || item?.type || '-'),
|
||||
};
|
||||
const children = Array.isArray(item?.children)
|
||||
? flattenParameterRows(item.children, direction, currentPath)
|
||||
: [];
|
||||
return [currentRow, ...children];
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="accordion-container">
|
||||
<div class="controls-header">
|
||||
<ElButton @click="back" :icon="Back">
|
||||
<ElButton :icon="Back" @click="back">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" :icon="VideoPlay" @click="handleOpenRunModal">
|
||||
{{ $t('pluginItem.pluginToolEdit.trialRun') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<!-- 折叠面板列表 -->
|
||||
<div class="accordion-list">
|
||||
<!-- 基本信息-->
|
||||
<template v-if="isWorkflowPlugin">
|
||||
<div class="workflow-tool-layout">
|
||||
<section class="workflow-tool-card">
|
||||
<div class="workflow-tool-card__header">
|
||||
<div class="workflow-tool-card__title">
|
||||
{{ $t('pluginItem.inputData') }} / {{ $t('pluginItem.outputData') }}
|
||||
</div>
|
||||
</div>
|
||||
<ElTable
|
||||
:data="workflowParameterRows"
|
||||
border
|
||||
class="workflow-tool-table"
|
||||
empty-text="-"
|
||||
>
|
||||
<ElTableColumn
|
||||
prop="direction"
|
||||
:label="$t('pluginItem.direction')"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.direction === 'input' ? $t('pluginItem.inputData') : $t('pluginItem.outputData') }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="name"
|
||||
:label="$t('pluginItem.parameterName')"
|
||||
min-width="280"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="type"
|
||||
:label="$t('pluginItem.parameterType')"
|
||||
min-width="160"
|
||||
/>
|
||||
</ElTable>
|
||||
</section>
|
||||
|
||||
<section class="workflow-tool-card">
|
||||
<div class="workflow-tool-card__header">
|
||||
<div class="workflow-tool-card__title">
|
||||
快照
|
||||
</div>
|
||||
</div>
|
||||
<WorkflowApprovalSnapshotPreview
|
||||
:title="workflowSnapshot?.title || pluginInfo.workflowTitle || pluginToolInfo.name"
|
||||
:description="''"
|
||||
:content="workflowSnapshot?.content || ''"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="accordion-list">
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{ 'accordion-item--active': pluginBasicCollapse.isOpen }"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="handleClickHeader(1)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
@@ -295,37 +377,38 @@ const handleOpenRunModal = () => {
|
||||
</div>
|
||||
<h3 class="accordion-title">{{ pluginBasicCollapse.title }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!isWorkflowPlugin">
|
||||
<ElButton
|
||||
@click.stop="handleEdit(1)"
|
||||
type="primary"
|
||||
v-if="!pluginBasicCollapse.isEdit"
|
||||
type="primary"
|
||||
@click.stop="handleEdit(1)"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleCancel(1)"
|
||||
v-if="pluginBasicCollapse.isEdit"
|
||||
@click.stop="handleCancel(1)"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleSave(1)"
|
||||
type="primary"
|
||||
v-if="pluginBasicCollapse.isEdit"
|
||||
type="primary"
|
||||
@click.stop="handleSave(1)"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElTag v-else type="info" effect="plain">
|
||||
{{ $t('pluginItem.systemManaged') }}
|
||||
</ElTag>
|
||||
</div>
|
||||
|
||||
<!-- 面板内容 -->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{ 'accordion-content--open': pluginBasicCollapse.isOpen }"
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<!--编辑基本信息-->
|
||||
<div v-show="pluginBasicCollapse.isEdit">
|
||||
<div class="plugin-tool-info-edit-container">
|
||||
<ElForm
|
||||
@@ -381,7 +464,6 @@ const handleOpenRunModal = () => {
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
<!--显示基本信息-->
|
||||
<div
|
||||
v-show="!pluginBasicCollapse.isEdit"
|
||||
class="plugin-tool-info-view-container"
|
||||
@@ -402,32 +484,39 @@ const handleOpenRunModal = () => {
|
||||
</div>
|
||||
<div>{{ pluginToolInfo.description }}</div>
|
||||
</div>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div v-if="isWorkflowPlugin" class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.pluginToolEdit.toolPath') }}:
|
||||
{{ $t('pluginItem.pluginToolEdit.workflowTarget') }}:
|
||||
</div>
|
||||
<div>{{ pluginInfo.baseUrl }}{{ pluginToolInfo.basePath }}</div>
|
||||
<div>{{ pluginInfo.workflowTitle || '-' }}</div>
|
||||
</div>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.pluginToolEdit.requestMethod') }}:
|
||||
<template v-else>
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.pluginToolEdit.toolPath') }}:
|
||||
</div>
|
||||
<div>{{ pluginInfo.baseUrl }}{{ pluginToolInfo.basePath }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ pluginToolInfo.requestMethod }}
|
||||
<div class="plugin-tool-view-item">
|
||||
<div class="view-item-title">
|
||||
{{ $t('pluginItem.pluginToolEdit.requestMethod') }}:
|
||||
</div>
|
||||
<div>
|
||||
{{ pluginToolInfo.requestMethod }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输入参数-->
|
||||
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{
|
||||
'accordion-item--active': pluginBasicCollapseInputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="handleClickHeader(2)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
@@ -445,31 +534,30 @@ const handleOpenRunModal = () => {
|
||||
{{ pluginBasicCollapseInputParams.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!isWorkflowPlugin">
|
||||
<ElButton
|
||||
@click.stop="handleEdit(2)"
|
||||
type="primary"
|
||||
v-if="!pluginBasicCollapseInputParams.isEdit"
|
||||
type="primary"
|
||||
@click.stop="handleEdit(2)"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleCancel(2)"
|
||||
v-if="pluginBasicCollapseInputParams.isEdit"
|
||||
@click.stop="handleCancel(2)"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleSave(2)"
|
||||
type="primary"
|
||||
v-if="pluginBasicCollapseInputParams.isEdit"
|
||||
type="primary"
|
||||
@click.stop="handleSave(2)"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--输入参数-->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{
|
||||
@@ -486,14 +574,13 @@ const handleOpenRunModal = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输出参数-->
|
||||
|
||||
<div
|
||||
class="accordion-item"
|
||||
:class="{
|
||||
'accordion-item--active': pluginBasicCollapseOutputParams.isOpen,
|
||||
}"
|
||||
>
|
||||
<!-- 面板头部 -->
|
||||
<div class="accordion-header" @click="handleClickHeader(3)">
|
||||
<div class="column-header-container">
|
||||
<div
|
||||
@@ -511,31 +598,30 @@ const handleOpenRunModal = () => {
|
||||
{{ pluginBasicCollapseOutputParams.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!isWorkflowPlugin">
|
||||
<ElButton
|
||||
@click.stop="handleEdit(3)"
|
||||
type="primary"
|
||||
v-if="!pluginBasicCollapseOutputParams.isEdit"
|
||||
type="primary"
|
||||
@click.stop="handleEdit(3)"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleCancel(3)"
|
||||
v-if="pluginBasicCollapseOutputParams.isEdit"
|
||||
@click.stop="handleCancel(3)"
|
||||
>
|
||||
{{ $t('button.cancel') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click.stop="handleSave(3)"
|
||||
type="primary"
|
||||
v-if="pluginBasicCollapseOutputParams.isEdit"
|
||||
type="primary"
|
||||
@click.stop="handleSave(3)"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--输出参数-->
|
||||
<div
|
||||
class="accordion-content"
|
||||
:class="{
|
||||
@@ -544,8 +630,8 @@ const handleOpenRunModal = () => {
|
||||
>
|
||||
<div class="accordion-content-inner">
|
||||
<PluginInputAndOutParams
|
||||
v-model="pluginOutputData"
|
||||
ref="pluginOutputParamsRef"
|
||||
v-model="pluginOutputData"
|
||||
:editable="pluginOutputParamsEditable"
|
||||
:is-edit-output="true"
|
||||
/>
|
||||
@@ -553,211 +639,133 @@ const handleOpenRunModal = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 试运行模态框-->
|
||||
<PluginRunTestModal ref="runTestRef" :plugin-tool-id="toolId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.accordion-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-container {
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.controls-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: 30px;
|
||||
font-size: 1.1rem;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 控制面板样式 */
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: #3498db;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
/* 折叠面板列表 */
|
||||
.accordion-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflow-tool-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflow-tool-card {
|
||||
padding: 20px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.workflow-tool-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workflow-tool-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.workflow-tool-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.accordion-item:hover {
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
.accordion-item--active {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
padding: 18px 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: hsl(var(--background));
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
padding-left: 12px;
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
.column-header-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accordion-icon {
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
transition: transform 0.3s ease;
|
||||
display: inline-flex;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.accordion-icon--rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.accordion-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
background: hsl(var(--background));
|
||||
transition: max-height 0.4s ease;
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.2s ease;
|
||||
}
|
||||
|
||||
.accordion-content--open {
|
||||
max-height: 2000px;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.accordion-content-inner {
|
||||
padding: 20px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.accordion-content-inner p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.column-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 0;
|
||||
padding: 0 20px 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plugin-tool-info-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.plugin-tool-view-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.view-item-title {
|
||||
width: 70px;
|
||||
|
||||
/* text-align: right; */
|
||||
|
||||
/* margin-right: 12px; */
|
||||
min-width: 108px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -25,6 +25,10 @@ const props = defineProps({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
pluginType: {
|
||||
default: 1,
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
const router = useRouter();
|
||||
defineExpose({
|
||||
@@ -100,7 +104,7 @@ const pluginToolReload = () => {
|
||||
{{ $t('button.edit') }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElDropdown v-if="Number(props.pluginType || 1) !== 2">
|
||||
<ElButton link :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { markRaw, ref } from 'vue';
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Back, Plus } from '@element-plus/icons-vue';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PluginToolTable from '#/views/ai/plugin/PluginToolTable.vue';
|
||||
|
||||
@@ -13,26 +14,49 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const pluginId = ref<string>((route.query.id as string) || '');
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'back',
|
||||
text: $t('button.back'),
|
||||
icon: markRaw(Back),
|
||||
data: { action: 'back' },
|
||||
},
|
||||
{
|
||||
key: 'createTool',
|
||||
text: $t('pluginItem.createPluginTool'),
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'createTool' },
|
||||
},
|
||||
];
|
||||
const handleSearch = (params: any) => {
|
||||
const pluginInfo = ref<any>({});
|
||||
const pluginToolRef = ref();
|
||||
|
||||
const headerButtons = computed<any[]>(() => {
|
||||
const buttons: any[] = [
|
||||
{
|
||||
key: 'back',
|
||||
text: $t('button.back'),
|
||||
icon: markRaw(Back),
|
||||
data: { action: 'back' },
|
||||
},
|
||||
];
|
||||
if (Number(pluginInfo.value?.type || 1) !== 2) {
|
||||
buttons.push({
|
||||
key: 'createTool',
|
||||
text: $t('pluginItem.createPluginTool'),
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'createTool' },
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadPluginInfo();
|
||||
});
|
||||
|
||||
async function loadPluginInfo() {
|
||||
if (!pluginId.value) {
|
||||
return;
|
||||
}
|
||||
const res = await api.get(`/api/v1/plugin/detail?id=${pluginId.value}`);
|
||||
if (res.errorCode === 0) {
|
||||
pluginInfo.value = res.data || {};
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(params: any) {
|
||||
pluginToolRef.value.handleSearch(params);
|
||||
};
|
||||
const handleButtonClick = (event: any) => {
|
||||
// 根据按钮 key 执行不同操作
|
||||
}
|
||||
|
||||
function handleButtonClick(event: any) {
|
||||
switch (event.key) {
|
||||
case 'back': {
|
||||
router.push({ path: '/ai/plugin' });
|
||||
@@ -43,8 +67,7 @@ const handleButtonClick = (event: any) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const pluginToolRef = ref();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -56,7 +79,11 @@ const pluginToolRef = ref();
|
||||
/>
|
||||
|
||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||
<PluginToolTable :plugin-id="pluginId" ref="pluginToolRef" />
|
||||
<PluginToolTable
|
||||
ref="pluginToolRef"
|
||||
:plugin-id="pluginId"
|
||||
:plugin-type="pluginInfo.type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,8 +8,10 @@ export interface OfflineImpactBinding {
|
||||
export interface OfflineImpactCheck {
|
||||
canProceed: boolean;
|
||||
hasBotBindings: boolean;
|
||||
hasPluginBindings: boolean;
|
||||
hasWorkflowUsages: boolean;
|
||||
botBindings: OfflineImpactBinding[];
|
||||
pluginBindings: OfflineImpactBinding[];
|
||||
workflowUsages: OfflineImpactBinding[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@@ -605,9 +605,25 @@ function handlePluginNodeUpdate(chooseId: any) {
|
||||
})
|
||||
.then((res) => {
|
||||
pageLoading.value = false;
|
||||
updatePluginNode.value(res.data);
|
||||
updatePluginNode.value(decoratePluginNodeData(res.data));
|
||||
});
|
||||
}
|
||||
function decoratePluginNodeData(data: any) {
|
||||
if (!data || Number(data.pluginType || 1) !== 2) {
|
||||
return data;
|
||||
}
|
||||
const statusText =
|
||||
data.available === false
|
||||
? data.reasonMessage || $t('plugin.workflowPluginUnavailable')
|
||||
: $t('plugin.workflowSnapshotSynced');
|
||||
return {
|
||||
...data,
|
||||
pluginName: data.pluginName
|
||||
? `${data.pluginName} · ${statusText}`
|
||||
: statusText,
|
||||
pluginStatusText: statusText,
|
||||
};
|
||||
}
|
||||
function onAsyncExecute(info: any) {
|
||||
chainInfo.value = info;
|
||||
}
|
||||
@@ -628,7 +644,7 @@ function onAsyncExecute(info: any) {
|
||||
:title="$t('menus.ai.plugin')"
|
||||
width="730"
|
||||
ref="pluginSelectRef"
|
||||
page-url="/api/v1/plugin/page"
|
||||
page-url="/api/v1/plugin/page?availableOnly=true"
|
||||
:has-parent="true"
|
||||
single-select
|
||||
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
CardPrimaryAction,
|
||||
} from '#/components/page/CardList.vue';
|
||||
|
||||
import { computed, markRaw, onMounted, ref } from 'vue';
|
||||
import { computed, h, markRaw, onMounted, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@easyflow/access';
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
@@ -360,14 +360,48 @@ async function submitOfflineAction(row: any) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sections = [];
|
||||
if (impactRes.data?.hasBotBindings) {
|
||||
sections.push(
|
||||
buildOfflineImpactMessage(
|
||||
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
||||
impactRes.data.botBindings,
|
||||
impactRes.data?.hasPluginBindings
|
||||
? undefined
|
||||
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (impactRes.data?.hasPluginBindings) {
|
||||
sections.push(
|
||||
buildOfflineImpactMessage(
|
||||
$t('aiWorkflow.offlineImpactBoundPluginsIntro'),
|
||||
impactRes.data.pluginBindings,
|
||||
impactRes.data?.hasBotBindings
|
||||
? undefined
|
||||
: $t('aiWorkflow.offlineImpactBoundPluginsFooter'),
|
||||
),
|
||||
);
|
||||
}
|
||||
const impactMessage =
|
||||
sections.length > 0
|
||||
? h('div', [
|
||||
...sections,
|
||||
h(
|
||||
'p',
|
||||
{
|
||||
style: 'margin-top: 12px;',
|
||||
},
|
||||
impactRes.data?.hasBotBindings && impactRes.data?.hasPluginBindings
|
||||
? $t('aiWorkflow.offlineImpactBoundMixedFooter')
|
||||
: impactRes.data?.hasPluginBindings
|
||||
? $t('aiWorkflow.offlineImpactBoundPluginsFooter')
|
||||
: $t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||
),
|
||||
])
|
||||
: $t('aiWorkflow.submitOfflineApprovalConfirm');
|
||||
await ElMessageBox.confirm(
|
||||
impactRes.data?.hasBotBindings
|
||||
? buildOfflineImpactMessage(
|
||||
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
||||
impactRes.data.botBindings,
|
||||
$t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||
)
|
||||
: $t('aiWorkflow.submitOfflineApprovalConfirm'),
|
||||
impactMessage,
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
|
||||
@@ -4,11 +4,12 @@ import type { FormInstance } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import { ElButton, ElForm, ElFormItem, ElMessage } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import ShowJson from '#/components/json/ShowJson.vue';
|
||||
import { $t } from '#/locales';
|
||||
import WorkflowFormItem from '#/views/ai/workflow/components/WorkflowFormItem.vue';
|
||||
|
||||
interface Props {
|
||||
workflowId: any;
|
||||
@@ -47,19 +48,10 @@ function submit() {
|
||||
<template>
|
||||
<div>
|
||||
<ElForm label-position="top" ref="singleRunForm" :model="runParams">
|
||||
<ElFormItem
|
||||
v-for="(item, idx) in node?.data.parameters"
|
||||
:prop="item.name"
|
||||
:key="idx"
|
||||
:label="item.description || item.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ElInput
|
||||
v-if="item.formType === 'input' || !item.formType"
|
||||
v-model="runParams[item.name]"
|
||||
:placeholder="item.formPlaceholder"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<WorkflowFormItem
|
||||
v-model:run-params="runParams"
|
||||
:parameters="node?.data.parameters || []"
|
||||
/>
|
||||
<ElFormItem>
|
||||
<ElButton
|
||||
type="primary"
|
||||
|
||||
@@ -29,16 +29,64 @@ export interface WorkflowStepsProps {
|
||||
nodeJson: any;
|
||||
initSignal?: boolean;
|
||||
pollingData?: any;
|
||||
expandAll?: boolean;
|
||||
}
|
||||
const props = defineProps<WorkflowStepsProps>();
|
||||
const emit = defineEmits(['resume']);
|
||||
const nodes = ref<any[]>([]);
|
||||
const nodeStatusMap = ref<Record<string, any>>({});
|
||||
const isChainError = ref(false);
|
||||
const activeName = ref<any>(props.expandAll ? [] : '');
|
||||
const confirmParams = ref<any>({});
|
||||
// 定义一个对象来存储所有的 form 实例,key 为 node.key
|
||||
const formRefs = ref<Record<string, FormInstance>>({});
|
||||
const confirmBtnLoading = ref(false);
|
||||
const chainErrMsg = ref('');
|
||||
|
||||
function shouldAutoExpandStatus(status: unknown) {
|
||||
return [1, 5, 20, 21].includes(Number(status));
|
||||
}
|
||||
|
||||
function isExpandedNode(nodeKey: string) {
|
||||
if (Array.isArray(activeName.value)) {
|
||||
return activeName.value.includes(nodeKey);
|
||||
}
|
||||
return activeName.value === nodeKey;
|
||||
}
|
||||
|
||||
function hasNodePayloadChanged(previous: any, current: any) {
|
||||
if (previous === current) {
|
||||
return false;
|
||||
}
|
||||
return JSON.stringify(previous ?? null) !== JSON.stringify(current ?? null);
|
||||
}
|
||||
|
||||
function hasNodeStateChanged(previous: any, current: any) {
|
||||
if (!previous) {
|
||||
return true;
|
||||
}
|
||||
if ((previous?.status || null) !== (current?.status || null)) {
|
||||
return true;
|
||||
}
|
||||
if ((previous?.message || '') !== (current?.message || '')) {
|
||||
return true;
|
||||
}
|
||||
if (hasNodePayloadChanged(previous?.result, current?.result)) {
|
||||
return true;
|
||||
}
|
||||
return hasNodePayloadChanged(
|
||||
previous?.suspendForParameters,
|
||||
current?.suspendForParameters,
|
||||
);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.pollingData,
|
||||
(newVal) => {
|
||||
const nodes = newVal.nodes;
|
||||
if (!newVal) {
|
||||
return;
|
||||
}
|
||||
const currentNodes = newVal.nodes || {};
|
||||
if (newVal.status === 21) {
|
||||
isChainError.value = true;
|
||||
chainErrMsg.value = newVal.message;
|
||||
@@ -46,9 +94,20 @@ watch(
|
||||
if (![20, 21].includes(newVal.status)) {
|
||||
confirmBtnLoading.value = false;
|
||||
}
|
||||
for (const nodeId in nodes) {
|
||||
nodeStatusMap.value[nodeId] = nodes[nodeId];
|
||||
if (nodes[nodeId].status === 5) {
|
||||
for (const nodeId in currentNodes) {
|
||||
const previousNodeState = nodeStatusMap.value[nodeId];
|
||||
const currentNodeState = currentNodes[nodeId];
|
||||
const previousStatus = previousNodeState?.status;
|
||||
const currentStatus = currentNodeState?.status;
|
||||
if (!hasNodeStateChanged(previousNodeState, currentNodeState)) {
|
||||
continue;
|
||||
}
|
||||
nodeStatusMap.value[nodeId] = currentNodeState;
|
||||
if (
|
||||
!props.expandAll &&
|
||||
previousStatus !== currentStatus &&
|
||||
shouldAutoExpandStatus(currentStatus)
|
||||
) {
|
||||
activeName.value = nodeId;
|
||||
}
|
||||
}
|
||||
@@ -62,14 +121,17 @@ watch(
|
||||
isChainError.value = false;
|
||||
confirmBtnLoading.value = false;
|
||||
chainErrMsg.value = '';
|
||||
activeName.value = props.expandAll ? [] : '';
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.nodeJson,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
nodes.value = [...newVal];
|
||||
}
|
||||
const nextNodes = Array.isArray(newVal) ? [...newVal] : [];
|
||||
nodes.value = nextNodes;
|
||||
activeName.value = props.expandAll
|
||||
? nextNodes.map((node: any) => node.key)
|
||||
: '';
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -79,18 +141,12 @@ const displayNodes = computed(() => {
|
||||
...nodeStatusMap.value[node.key],
|
||||
}));
|
||||
});
|
||||
const activeName = ref('1');
|
||||
const confirmParams = ref<any>({});
|
||||
// 定义一个对象来存储所有的 form 实例,key 为 node.key
|
||||
const formRefs = ref<Record<string, FormInstance>>({});
|
||||
// 动态设置 Ref 的辅助函数
|
||||
const setFormRef = (el: any, key: string) => {
|
||||
if (el) {
|
||||
formRefs.value[key] = el as FormInstance;
|
||||
}
|
||||
};
|
||||
const confirmBtnLoading = ref(false);
|
||||
const chainErrMsg = ref('');
|
||||
function getSelectMode(ops: any) {
|
||||
return ops.formType || 'radio';
|
||||
}
|
||||
@@ -124,7 +180,11 @@ function handleConfirm(node: any) {
|
||||
<div class="mb-1">
|
||||
<ElAlert v-if="chainErrMsg" :title="chainErrMsg" type="error" />
|
||||
</div>
|
||||
<ElCollapse v-model="activeName" accordion expand-icon-position="left">
|
||||
<ElCollapse
|
||||
v-model="activeName"
|
||||
:accordion="!props.expandAll"
|
||||
expand-icon-position="left"
|
||||
>
|
||||
<ElCollapseItem
|
||||
v-for="node in displayNodes"
|
||||
:key="node.key"
|
||||
@@ -165,64 +225,66 @@ function handleConfirm(node: any) {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="node.original.type === 'confirmNode'" class="p-2.5">
|
||||
<div class="mb-2 text-[16px] font-bold">
|
||||
{{ node.original.data.message }}
|
||||
</div>
|
||||
<ElForm
|
||||
:ref="(el) => setFormRef(el, node.key)"
|
||||
label-position="top"
|
||||
:model="confirmParams"
|
||||
>
|
||||
<template
|
||||
v-for="(ops, idx) in node.suspendForParameters"
|
||||
:key="idx"
|
||||
<template v-if="isExpandedNode(node.key)">
|
||||
<div v-if="node.original.type === 'confirmNode'" class="p-2.5">
|
||||
<div class="mb-2 text-[16px] font-bold">
|
||||
{{ node.original.data.message }}
|
||||
</div>
|
||||
<ElForm
|
||||
:ref="(el) => setFormRef(el, node.key)"
|
||||
label-position="top"
|
||||
:model="confirmParams"
|
||||
>
|
||||
<div class="header-container" v-if="ops.formType !== 'confirm'">
|
||||
<div class="blue-bar"> </div>
|
||||
<span>{{ ops.formLabel || $t('message.confirmItem') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="description-container"
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
<template
|
||||
v-for="(ops, idx) in node.suspendForParameters"
|
||||
:key="idx"
|
||||
>
|
||||
{{ ops.formDescription }}
|
||||
</div>
|
||||
<ElFormItem
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
:prop="ops.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ConfirmItem
|
||||
v-if="getSelectMode(ops) === 'radio'"
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
<ConfirmItemMulti
|
||||
v-else
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem v-if="node.suspendForParameters?.length > 0">
|
||||
<div class="flex justify-end">
|
||||
<ElButton
|
||||
:disabled="confirmBtnLoading"
|
||||
type="primary"
|
||||
@click="handleConfirm(node)"
|
||||
<div class="header-container" v-if="ops.formType !== 'confirm'">
|
||||
<div class="blue-bar"> </div>
|
||||
<span>{{ ops.formLabel || $t('message.confirmItem') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="description-container"
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ShowJson :value="node.result || node.message" />
|
||||
</div>
|
||||
{{ ops.formDescription }}
|
||||
</div>
|
||||
<ElFormItem
|
||||
v-if="ops.formType !== 'confirm'"
|
||||
:prop="ops.name"
|
||||
:rules="[{ required: true, message: $t('message.required') }]"
|
||||
>
|
||||
<ConfirmItem
|
||||
v-if="getSelectMode(ops) === 'radio'"
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
<ConfirmItemMulti
|
||||
v-else
|
||||
v-model="confirmParams[ops.name]"
|
||||
:selection-data-type="ops.contentType || 'text'"
|
||||
:selection-data="ops.enums"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem v-if="node.suspendForParameters?.length > 0">
|
||||
<div class="flex justify-end">
|
||||
<ElButton
|
||||
:disabled="confirmBtnLoading"
|
||||
type="primary"
|
||||
@click="handleConfirm(node)"
|
||||
>
|
||||
{{ $t('button.confirm') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ShowJson :value="node.result || node.message" />
|
||||
</div>
|
||||
</template>
|
||||
</ElCollapseItem>
|
||||
</ElCollapse>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
let textareaEl = $state<HTMLTextAreaElement | null>(null);
|
||||
let highlightEl = $state<HTMLDivElement | null>(null);
|
||||
let scrollbarWidth = $state(0);
|
||||
let triggerObject: any;
|
||||
let isFocused = $state(false);
|
||||
let isComposing = $state(false);
|
||||
@@ -49,6 +50,17 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
localValue;
|
||||
if (mode !== 'textarea') {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
updateScrollbarWidth();
|
||||
});
|
||||
});
|
||||
|
||||
const paramCandidates = $derived(flattenParameterCandidates(parameters));
|
||||
const paramNames = $derived(paramCandidates.map((item) => item.name));
|
||||
const unresolvedParamSet = $derived.by(() => {
|
||||
@@ -129,9 +141,48 @@
|
||||
highlightEl.scrollLeft = el.scrollLeft;
|
||||
};
|
||||
|
||||
const updateScrollbarWidth = () => {
|
||||
const el = textareaEl;
|
||||
if (!el || mode !== 'textarea') {
|
||||
scrollbarWidth = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const style = getComputedStyle(el);
|
||||
const borderLeft = parseFloat(style.borderLeftWidth || '0');
|
||||
const borderRight = parseFloat(style.borderRightWidth || '0');
|
||||
const nextWidth = Math.max(0, el.offsetWidth - el.clientWidth - borderLeft - borderRight);
|
||||
scrollbarWidth = Number.isFinite(nextWidth) ? nextWidth : 0;
|
||||
};
|
||||
|
||||
const syncEditorMetrics = () => {
|
||||
syncScroll();
|
||||
updateScrollbarWidth();
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (mode !== 'textarea' || !textareaEl) {
|
||||
scrollbarWidth = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = typeof ResizeObserver !== 'undefined'
|
||||
? new ResizeObserver(() => {
|
||||
updateScrollbarWidth();
|
||||
})
|
||||
: null;
|
||||
|
||||
updateScrollbarWidth();
|
||||
resizeObserver?.observe(textareaEl);
|
||||
|
||||
return () => {
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
localValue = ((event.target as HTMLInputElement | HTMLTextAreaElement).value || '') as string;
|
||||
syncScroll();
|
||||
syncEditorMetrics();
|
||||
emitInput(event);
|
||||
};
|
||||
|
||||
@@ -158,7 +209,7 @@
|
||||
newEditorEl.focus();
|
||||
newEditorEl.setSelectionRange(result.cursor, result.cursor);
|
||||
}
|
||||
syncScroll();
|
||||
syncEditorMetrics();
|
||||
triggerObject?.hide?.();
|
||||
};
|
||||
|
||||
@@ -310,7 +361,7 @@
|
||||
editorEl.focus();
|
||||
editorEl.setSelectionRange(deleteRange.start, deleteRange.start);
|
||||
}
|
||||
syncScroll();
|
||||
syncEditorMetrics();
|
||||
rest.onkeydown?.(event);
|
||||
return;
|
||||
}
|
||||
@@ -339,7 +390,7 @@
|
||||
editorEl.focus();
|
||||
editorEl.setSelectionRange(tokenRange.start, tokenRange.start);
|
||||
}
|
||||
syncScroll();
|
||||
syncEditorMetrics();
|
||||
rest.onkeydown?.(event);
|
||||
};
|
||||
|
||||
@@ -400,6 +451,7 @@
|
||||
<div
|
||||
class="param-token-editor-highlight {mode === 'input' ? 'single-line' : 'multi-line'}"
|
||||
bind:this={highlightEl}
|
||||
style={`--param-token-scrollbar-width: ${scrollbarWidth}px;`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{@html highlightedHtml}
|
||||
@@ -571,7 +623,9 @@
|
||||
inset: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
padding: var(--param-token-padding-y) var(--param-token-padding-right) var(--param-token-padding-y)
|
||||
padding: var(--param-token-padding-y)
|
||||
calc(var(--param-token-padding-right) + var(--param-token-scrollbar-width, 0px))
|
||||
var(--param-token-padding-y)
|
||||
var(--param-token-padding-left);
|
||||
box-sizing: border-box;
|
||||
color: var(--tf-text-primary);
|
||||
@@ -621,6 +675,8 @@
|
||||
|
||||
.param-token-textarea {
|
||||
overflow: auto;
|
||||
display: block;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.param-token-action {
|
||||
|
||||
Reference in New Issue
Block a user