Compare commits
7 Commits
f4626987f1
...
86a7ff1573
| Author | SHA1 | Date | |
|---|---|---|---|
| 86a7ff1573 | |||
| cb7e36ad03 | |||
| 7c60e34de0 | |||
| 38166cb0b8 | |||
| ce70f86000 | |||
| e357f93779 | |||
| 010db1e7bc |
@@ -7,28 +7,47 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# 从组织级Secrets读取,不用在仓库重复配置
|
||||
K3S_HOST: ${{ secrets.K3S_HOST }}
|
||||
K3S_HOST: 121.37.117.181
|
||||
APP_NAME: ${{ gitea.repo_name }}
|
||||
REGISTRY: 你的镜像仓库地址 # 比如 docker.io/你的用户名
|
||||
steps:
|
||||
- name: 拉取代码
|
||||
uses: actions/checkout@v4
|
||||
- uses: gitea/actions/checkout@v4
|
||||
|
||||
# 1. 初始化 Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# 2. 登录镜像仓库(按需)
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PWD }}
|
||||
|
||||
# 3. 构建+推送,启用缓存
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.APP_NAME }}:${{ gitea.sha }}
|
||||
# 缓存配置:推送到镜像仓库
|
||||
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.APP_NAME }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.APP_NAME }}:buildcache,mode=max
|
||||
|
||||
# 4. 核心修改:先上传deploy.yaml到K3s服务器,再执行kubectl
|
||||
- name: SSH部署K3s
|
||||
run: |
|
||||
run:
|
||||
mkdir -p ~/.ssh
|
||||
# 写入组织配置的SSH私钥
|
||||
echo "${{ secrets.K3S_SSH_KEY }}" > k3s.pem
|
||||
echo "${{ secrets.K3S_PEM_KEY }}" > k3s.pem
|
||||
chmod 600 k3s.pem
|
||||
# 调试:验证私钥是否正确写入
|
||||
echo "私钥文件权限:"
|
||||
ls -l k3s.pem
|
||||
echo "私钥头部(仅前5行):"
|
||||
head -5 k3s.pem
|
||||
# 测试连接(会输出服务器主机名和kubectl版本)
|
||||
ssh -i k3s.pem -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@${K3S_HOST} "hostname && kubectl version --client"
|
||||
# 正式执行部署命令
|
||||
# 关键1:把Gitea仓库里的deploy.yaml上传到K3s服务器临时目录(/tmp)
|
||||
# 注意:如果你的deploy.yaml不在仓库根目录,要修改./deploy.yaml为实际路径
|
||||
scp -i k3s.pem -o StrictHostKeyChecking=no ./deploy.yaml root@${K3S_HOST}:/tmp/
|
||||
# 关键2:执行kubectl时指向临时目录的文件,而非不存在的/k8s/
|
||||
ssh -i k3s.pem -o StrictHostKeyChecking=no root@${K3S_HOST} << CMD
|
||||
kubectl apply -f /k8s/deploy.yaml
|
||||
kubectl rollout restart deployment ${APP_NAME}
|
||||
kubectl apply -f /tmp/deploy.yaml
|
||||
kubectl rollout restart deployment ${APP_NAME} -n default
|
||||
# 可选:部署完成后删除临时文件,清理服务器
|
||||
rm -f /tmp/deploy.yaml
|
||||
CMD
|
||||
@@ -26,6 +26,6 @@ COPY ngnix.conf /etc/nginx/conf.d/default.conf
|
||||
# 复制SSL证书
|
||||
COPY ssl/* /etc/nginx/ssl/
|
||||
|
||||
EXPOSE 80 443
|
||||
EXPOSE 443
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface CreateModelParams {
|
||||
modelName: string;
|
||||
/** 与 listType 返回的类型 id 一致,可能为数字或字符串 */
|
||||
modelType: number | string;
|
||||
operatorName?: string;
|
||||
baseUrl: string;
|
||||
httpMethod?: string;
|
||||
headMsg?: string;
|
||||
@@ -122,6 +123,10 @@ export interface CreateModelParams {
|
||||
form: ModelFormEntry[];
|
||||
requestMapping?: Record<string, unknown>;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
responseBody?: Record<string, unknown>;
|
||||
extendMapping?: Record<string, unknown>;
|
||||
responseTokenField?: string;
|
||||
tokenConfig?: Record<string, unknown>;
|
||||
maxConcurrency?: number;
|
||||
queueLimit?: number;
|
||||
timeoutSeconds: number;
|
||||
|
||||
@@ -92,7 +92,14 @@
|
||||
import { ref, reactive, watch, onMounted } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
||||
import { getModelModuleList, addModelModule, getModelTypeList, normalizeModelTypeOptions } from '/@/api/settings/modelConfig/modelModule';
|
||||
import {
|
||||
getModelModuleList,
|
||||
addModelModule,
|
||||
getModelTypeList,
|
||||
normalizeModelTypeOptions,
|
||||
type CreateModelParams,
|
||||
type ModelFormEntry,
|
||||
} from '/@/api/settings/modelConfig/modelModule';
|
||||
import { checkIsSuperAdmin } from '/@/api/system/user/index';
|
||||
import { getApiErrorMessage } from '/@/utils/request';
|
||||
import EditModule from '/@/views/settings/modelConfig/modelModule/component/editModule.vue';
|
||||
@@ -101,7 +108,7 @@ interface ModelItem {
|
||||
id: string;
|
||||
tenantId?: number;
|
||||
modelName: string;
|
||||
modelType: number;
|
||||
modelType: number | string;
|
||||
baseUrl: string;
|
||||
route: string;
|
||||
httpMethod: string;
|
||||
@@ -113,9 +120,10 @@ interface ModelItem {
|
||||
operatorName?: string;
|
||||
responseBody?: Record<string, unknown>;
|
||||
tokenConfig?: Record<string, unknown> | string;
|
||||
form?: any;
|
||||
requestMapping?: any;
|
||||
responseMapping?: any;
|
||||
extendMapping?: Record<string, unknown> | string;
|
||||
form?: ModelFormEntry[] | Record<string, unknown>;
|
||||
requestMapping?: Record<string, unknown>;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
maxConcurrency?: number;
|
||||
queueLimit?: number;
|
||||
timeoutSeconds?: number;
|
||||
@@ -206,13 +214,30 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const getModelTypeName = (type: number) => {
|
||||
const getModelTypeName = (type: number | string) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '推理模型',
|
||||
2: '图片模型',
|
||||
3: '音频模型',
|
||||
};
|
||||
return typeMap[type] || '未知类型';
|
||||
return typeMap[Number(type)] || '未知类型';
|
||||
};
|
||||
|
||||
const parseJsonObjectField = (raw: unknown): Record<string, unknown> => {
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return raw as Record<string, unknown>;
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(raw || '{}');
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const fetchModelList = async () => {
|
||||
@@ -270,7 +295,13 @@ const handleCreatePrivateModel = async () => {
|
||||
creatingModel.value = true;
|
||||
|
||||
const builtInModel = builtInModelToClone.value;
|
||||
const createParams = {
|
||||
const formList: ModelFormEntry[] = Array.isArray(builtInModel.form)
|
||||
? (builtInModel.form as ModelFormEntry[])
|
||||
: Object.entries((builtInModel.form as Record<string, unknown>) || {}).map(([key, value]) => ({
|
||||
key: String(key),
|
||||
value: String(value ?? ''),
|
||||
}));
|
||||
const createParams: CreateModelParams = {
|
||||
modelName: apiKeyForm.modelName,
|
||||
modelType: builtInModel.modelType,
|
||||
operatorName: builtInModel.operatorName || '',
|
||||
@@ -281,9 +312,9 @@ const handleCreatePrivateModel = async () => {
|
||||
enabled: builtInModel.enabled ?? 1,
|
||||
isChatModel: builtInModel.isChatModel || 0,
|
||||
apiKey: apiKeyForm.apiKey,
|
||||
form: builtInModel.form || {},
|
||||
requestMapping: builtInModel.requestMapping || {},
|
||||
responseMapping: builtInModel.responseMapping || {},
|
||||
form: formList,
|
||||
requestMapping: (builtInModel.requestMapping as Record<string, unknown>) || {},
|
||||
responseMapping: (builtInModel.responseMapping as Record<string, unknown>) || {},
|
||||
responseBody: builtInModel.responseBody || {},
|
||||
maxConcurrency: builtInModel.maxConcurrency || 10,
|
||||
queueLimit: builtInModel.queueLimit || 100,
|
||||
@@ -293,8 +324,9 @@ const handleCreatePrivateModel = async () => {
|
||||
retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60,
|
||||
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
||||
remark: builtInModel.remark || '',
|
||||
tokenMapping: builtInModel.tokenMapping || '',
|
||||
tokenConfig: builtInModel.tokenConfig || {},
|
||||
|
||||
extendMapping: parseJsonObjectField(builtInModel.extendMapping),
|
||||
tokenConfig: parseJsonObjectField(builtInModel.tokenConfig),
|
||||
};
|
||||
|
||||
const res: any = await addModelModule(createParams);
|
||||
|
||||
40
src/views/settings/creation/component/SaveWorkflowDialog.vue
Normal file
40
src/views/settings/creation/component/SaveWorkflowDialog.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" :close-on-click-modal="false">
|
||||
<el-form :model="saveForm" label-position="top">
|
||||
<el-form-item label="工作流名称" required>
|
||||
<el-input v-model="saveForm.flowName" placeholder="请输入工作流名称" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作流描述">
|
||||
<el-input v-model="saveForm.description" type="textarea" :rows="4" placeholder="请输入工作流描述(选填)" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="emit('confirm')">{{ confirmText }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
saveForm: { flowName: string; description: string };
|
||||
currentEditingWorkflowId: string | null;
|
||||
saving: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirm'): void;
|
||||
}>();
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (props.currentEditingWorkflowId ? '编辑工作流' : '保存工作流'));
|
||||
const confirmText = computed(() => (props.currentEditingWorkflowId ? '确定更新' : '确定保存'));
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="creation-page" :class="{ 'creation-mode': isCreationMode }">
|
||||
<!-- 左侧面板:工作空间/当前选中元素 Tab切换 -->
|
||||
<div class="panel left">
|
||||
@@ -352,7 +352,13 @@
|
||||
:accept="getCreationFileAccept(field)"
|
||||
:on-change="(file: any) => handleCreationFieldUpload(node, field, file)"
|
||||
>
|
||||
<el-button size="small" type="primary" :loading="isCreationFieldUploading(node, field)" :disabled="isFromWorkspace || isCreationFieldUploading(node, field)">{{ isCreationFieldUploading(node, field) ? '上传中...' : '选择文件' }}</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="isCreationFieldUploading(node, field)"
|
||||
:disabled="isFromWorkspace || isCreationFieldUploading(node, field)"
|
||||
>{{ isCreationFieldUploading(node, field) ? '上传中...' : '选择文件' }}</el-button
|
||||
>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="creation-upload-tags">
|
||||
@@ -360,7 +366,11 @@
|
||||
<span class="creation-upload-tag count">已上传 {{ getCreationFileCountText(node, field) }}</span>
|
||||
</div>
|
||||
<div v-if="getCreationFieldFiles(node, field).length > 0" class="uploaded-files-list creation-upload-list">
|
||||
<div v-for="(uploadedFile, fileIdx) in getCreationFieldFiles(node, field)" :key="fileIdx" class="uploaded-file-item creation-upload-item">
|
||||
<div
|
||||
v-for="(uploadedFile, fileIdx) in getCreationFieldFiles(node, field)"
|
||||
:key="fileIdx"
|
||||
class="uploaded-file-item creation-upload-item"
|
||||
>
|
||||
<span class="file-name">{{ uploadedFile.name }}</span>
|
||||
<el-button
|
||||
type="danger"
|
||||
@@ -439,6 +449,18 @@
|
||||
</div>
|
||||
<div class="meta-actions">
|
||||
<el-button size="small" @click="resetFlow">清空画布</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
:disabled="
|
||||
!selectedElement ||
|
||||
(selectedElement.kind === 'node' &&
|
||||
(selectedElement.properties?.nodeCode === START_NODE_CODE || selectedElement.text === START_NODE_TEXT))
|
||||
"
|
||||
@click="deleteSelectedElement"
|
||||
>
|
||||
删除选中
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="saveWorkflowAction" :loading="saving">保存工作流</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -554,25 +576,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 保存工作流对话框 -->
|
||||
<el-dialog
|
||||
<SaveWorkflowDialog
|
||||
v-model="saveDialogVisible"
|
||||
:title="currentEditingWorkflowId ? '编辑工作流' : '保存工作流'"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="saveForm" label-position="top">
|
||||
<el-form-item label="工作流名称" required>
|
||||
<el-input v-model="saveForm.flowName" placeholder="请输入工作流名称" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作流描述">
|
||||
<el-input v-model="saveForm.description" type="textarea" :rows="4" placeholder="请输入工作流描述(选填)" maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="saveDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmSaveWorkflow" :loading="saving">{{ currentEditingWorkflowId ? '确定更新' : '确定保存' }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
:save-form="saveForm"
|
||||
:current-editing-workflow-id="currentEditingWorkflowId"
|
||||
:saving="saving"
|
||||
@confirm="confirmSaveWorkflow"
|
||||
/>
|
||||
|
||||
<!-- 技能选择器 -->
|
||||
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkill" @confirm="handleSkillConfirm" />
|
||||
@@ -759,7 +769,15 @@
|
||||
<!-- 预览弹窗 -->
|
||||
<el-dialog v-model="previewDialogVisible" title="预览" width="95%" top="2vh" :close-on-click-modal="false" destroy-on-close>
|
||||
<div class="preview-container">
|
||||
<iframe v-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
|
||||
<el-image v-if="previewUrl && previewMode === 'image'" :src="previewUrl" fit="contain" style="width: 100%; height: 100%" />
|
||||
<video
|
||||
v-else-if="previewUrl && previewMode === 'video'"
|
||||
:src="previewUrl"
|
||||
controls
|
||||
style="width: 100%; height: 100%; background: #000"
|
||||
></video>
|
||||
<audio v-else-if="previewUrl && previewMode === 'audio'" :src="previewUrl" controls style="width: 100%"></audio>
|
||||
<iframe v-else-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
|
||||
<el-empty v-else description="无法加载预览内容" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
@@ -776,6 +794,7 @@ import '@logicflow/core/dist/index.css';
|
||||
import '@logicflow/extension/lib/style/index.css';
|
||||
import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
|
||||
import ModelSelector from '/@/components/model/ModelSelector.vue';
|
||||
import SaveWorkflowDialog from './component/SaveWorkflowDialog.vue';
|
||||
import type { SkillItem } from '/@/api/settings/skill';
|
||||
import {
|
||||
downloadToFile,
|
||||
@@ -810,6 +829,7 @@ interface TreeNode {
|
||||
children?: TreeNode[];
|
||||
fileUrl?: string;
|
||||
workflowId?: number | string;
|
||||
fileType?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
interface SelectedState {
|
||||
@@ -881,6 +901,7 @@ const isDraggingMiddleSplitter = ref(false);
|
||||
// 预览相关状态
|
||||
const previewDialogVisible = ref(false);
|
||||
const previewUrl = ref('');
|
||||
const previewMode = ref<'iframe' | 'image' | 'video' | 'audio'>('iframe');
|
||||
// 模型选择器相关状态
|
||||
const showModelSelector = ref(false);
|
||||
const selectedModelData = ref<any>(null);
|
||||
@@ -1252,6 +1273,7 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
||||
label: item.label || `作品${ii + 1}`,
|
||||
nodeType: 'title',
|
||||
fileUrl: item.content,
|
||||
fileType: item.type,
|
||||
workflowId: f.Id,
|
||||
sessionId: f.sessionId,
|
||||
})),
|
||||
@@ -1799,7 +1821,7 @@ const handleCreationFieldUpload = async (node: any, field: any, file: any) => {
|
||||
creationFieldFiles[key].push({ name: file.name, url: fileUrl });
|
||||
creationFormValues[key] = creationFieldFiles[key].map((f) => f.url);
|
||||
ElMessage.success('文件上传成功');
|
||||
} catch (error: any) {
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '文件上传失败');
|
||||
} finally {
|
||||
creationFieldUploading[key] = false;
|
||||
@@ -1945,7 +1967,9 @@ const sendMessage = async () => {
|
||||
value:
|
||||
bodyItem.fieldType === 'fileUpload'
|
||||
? Array.isArray(userVal !== undefined ? userVal : bodyItem.value)
|
||||
? (userVal !== undefined ? userVal : bodyItem.value)
|
||||
? userVal !== undefined
|
||||
? userVal
|
||||
: bodyItem.value
|
||||
: userVal !== undefined
|
||||
? [userVal]
|
||||
: bodyItem.value
|
||||
@@ -2130,13 +2154,13 @@ const handleTreeNodeClick = async (data: TreeNode) => {
|
||||
creationFormValues[fieldKey] = Boolean(field.value);
|
||||
} else {
|
||||
creationFormValues[fieldKey] =
|
||||
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||
? Array.isArray(field.value)
|
||||
? field.value
|
||||
: field.value
|
||||
? [field.value]
|
||||
: []
|
||||
: field.value || '';
|
||||
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||
? Array.isArray(field.value)
|
||||
? field.value
|
||||
: field.value
|
||||
? [field.value]
|
||||
: []
|
||||
: field.value || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2214,13 +2238,13 @@ const handleTreeNodeClick = async (data: TreeNode) => {
|
||||
creationFormValues[fieldKey] = Boolean(field.value);
|
||||
} else {
|
||||
creationFormValues[fieldKey] =
|
||||
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||
? Array.isArray(field.value)
|
||||
? field.value
|
||||
: field.value
|
||||
? [field.value]
|
||||
: []
|
||||
: field.value || '';
|
||||
field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload'
|
||||
? Array.isArray(field.value)
|
||||
? field.value
|
||||
: field.value
|
||||
? [field.value]
|
||||
: []
|
||||
: field.value || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2248,9 +2272,21 @@ const handleTreeNodeClick = async (data: TreeNode) => {
|
||||
};
|
||||
// 预览节点
|
||||
const previewNode = (d: TreeNode) => {
|
||||
if (d.nodeType !== 'html' && d.nodeType !== 'image' && d.nodeType !== 'title') return;
|
||||
if (!d.fileUrl) return ElMessage.warning('当前节点没有可用预览地址');
|
||||
const url = buildAssetUrl(d.fileUrl);
|
||||
if (!url) return ElMessage.warning('当前节点没有可用预览地址');
|
||||
|
||||
const type = String(d.fileType || '').toLowerCase();
|
||||
if (type === 'image') {
|
||||
previewMode.value = 'image';
|
||||
} else if (type === 'video') {
|
||||
previewMode.value = 'video';
|
||||
} else if (type === 'audio') {
|
||||
previewMode.value = 'audio';
|
||||
} else {
|
||||
previewMode.value = 'iframe';
|
||||
}
|
||||
|
||||
previewUrl.value = url;
|
||||
previewDialogVisible.value = true;
|
||||
};
|
||||
@@ -2264,7 +2300,9 @@ const downloadNode = async (d: TreeNode) => {
|
||||
const blob = r instanceof Blob ? r : r?.data;
|
||||
if (!(blob instanceof Blob)) throw new Error('invalid blob');
|
||||
const fileName = d.fileUrl.split('/').pop() || '';
|
||||
const fileExt = fileName.split('.').pop()?.toLowerCase() || 'html';
|
||||
const type = String(d.fileType || '').toLowerCase();
|
||||
const defaultExt = type === 'video' ? 'mp4' : type === 'audio' ? 'mp3' : 'html';
|
||||
const fileExt = fileName.split('.').pop()?.toLowerCase() || defaultExt;
|
||||
const name = decodeURIComponent(fileName || `${d.label}.${fileExt}`);
|
||||
const u = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -2274,7 +2312,7 @@ const downloadNode = async (d: TreeNode) => {
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(u);
|
||||
ElMessage.success('下载成功');
|
||||
ElMessage.success(`下载成功${type ? `(${type})` : ''}`);
|
||||
} catch {
|
||||
// 下载失败由 request 全局提示后端 message
|
||||
}
|
||||
@@ -2406,12 +2444,18 @@ const isHttpExpandTriggerField = (fieldItem: any) => {
|
||||
};
|
||||
const openHttpExpandDialog = (fieldItem: any) => {
|
||||
if (!Array.isArray(fieldItem?.expand)) return;
|
||||
const nodeId = selectedElement.value?.id;
|
||||
if (!nodeId) return;
|
||||
|
||||
currentHttpExpandFields.value = fieldItem.expand;
|
||||
Object.keys(httpExpandFormValues).forEach((k) => delete httpExpandFormValues[k]);
|
||||
Object.keys(httpExpandKeyValuePairs).forEach((k) => delete httpExpandKeyValuePairs[k]);
|
||||
|
||||
fieldItem.expand.forEach((f: any) => {
|
||||
httpExpandFormValues[f.field] = dynamicFormValues[f.field] !== undefined ? dynamicFormValues[f.field] : (f.default ?? '');
|
||||
const expandKey = `${nodeId}_responseType_expand_${f.field}`;
|
||||
httpExpandFormValues[f.field] = dynamicFormValues[expandKey] !== undefined ? dynamicFormValues[expandKey] : '';
|
||||
});
|
||||
|
||||
showHttpExpandDialog.value = true;
|
||||
};
|
||||
const getHttpExpandKeyValuePairs = (field: string) => {
|
||||
@@ -2454,12 +2498,28 @@ const removeHttpExpandKeyValuePair = (field: string, index: number) => {
|
||||
updateHttpExpandKeyValueField(field);
|
||||
};
|
||||
const confirmHttpExpandDialog = () => {
|
||||
const nodeId = selectedElement.value?.id;
|
||||
if (!nodeId) return;
|
||||
|
||||
currentHttpExpandFields.value.forEach((f: any) => {
|
||||
dynamicFormValues[f.field] = httpExpandFormValues[f.field];
|
||||
const expandKey = `${nodeId}_responseType_expand_${f.field}`;
|
||||
dynamicFormValues[expandKey] = httpExpandFormValues[f.field];
|
||||
});
|
||||
|
||||
showHttpExpandDialog.value = false;
|
||||
ElMessage.success('主动拉取参数已保存');
|
||||
};
|
||||
const buildHttpResponseTypeExpandData = (responseTypeField: any, nodeId: string) => {
|
||||
if (!Array.isArray(responseTypeField?.expand) || responseTypeField.expand.length === 0) return [];
|
||||
|
||||
return responseTypeField.expand.map((f: any) => {
|
||||
const expandKey = `${nodeId}_responseType_expand_${f.field}`;
|
||||
return {
|
||||
...f,
|
||||
value: dynamicFormValues[expandKey] !== undefined ? dynamicFormValues[expandKey] : '',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// HTTP请求体配置相关函数
|
||||
// 打开HTTP请求体配置弹窗
|
||||
@@ -3182,12 +3242,14 @@ watch(
|
||||
currentHttpBodyField.value = '';
|
||||
showHttpBodyDialog.value = false;
|
||||
|
||||
// 重置 dynamicFormValues(不删除键,保持响应式)
|
||||
// 重置 dynamicFormValues(不删除固定字段键,动态 expand 键按节点切换清理)
|
||||
for (const key in dynamicFormValues) {
|
||||
if (key.includes('_responseType_expand_')) {
|
||||
delete dynamicFormValues[key];
|
||||
continue;
|
||||
}
|
||||
dynamicFormValues[key] = '';
|
||||
}
|
||||
|
||||
// 获取当前节点的基础表单字段(直接从 nodeSchemaMap 获取,避免响应式延迟)
|
||||
const currentNodeCode = formState.nodeCode;
|
||||
const baseFormFields = nodeSchemaMap.value[currentNodeCode] || [];
|
||||
const baseFieldNames = new Set(baseFormFields.map((f) => f.field));
|
||||
@@ -3208,6 +3270,16 @@ watch(
|
||||
// 其他类型:保持原值
|
||||
dynamicFormValues[fieldConfig.field] = fieldConfig.value;
|
||||
}
|
||||
if (
|
||||
String(e.properties.nodeCode || '').toLowerCase() === 'http' &&
|
||||
fieldConfig.field === 'responseType' &&
|
||||
Array.isArray(fieldConfig.expand)
|
||||
) {
|
||||
fieldConfig.expand.forEach((expandField: any) => {
|
||||
const expandKey = `${e.id}_responseType_expand_${expandField.field}`;
|
||||
dynamicFormValues[expandKey] = expandField.value !== undefined ? expandField.value : '';
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 自定义字段:加载到 customFields
|
||||
const customType = fieldConfig.type === 'upload' ? 'uploadMultiple' : fieldConfig.type || 'input';
|
||||
@@ -3460,6 +3532,11 @@ const applySelected = () => {
|
||||
label: fieldItem.label,
|
||||
value: normalizedValue,
|
||||
required: fieldItem.required || false,
|
||||
...(String(formState.nodeCode || '').toLowerCase() === 'http' && fieldItem.field === 'responseType' && normalizedValue === 'pull'
|
||||
? {
|
||||
expand: buildHttpResponseTypeExpandData(fieldItem, cur.id),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3708,7 +3785,100 @@ const resetFlow = () => {
|
||||
selectedElement.value = null;
|
||||
syncDsl();
|
||||
};
|
||||
// 从后端 DSL 恢复工作流
|
||||
const cleanupReferencesToNode = (deletedNodeId: string) => {
|
||||
const lf = logicFlowInstance.value;
|
||||
if (!lf) return 0;
|
||||
|
||||
const graphData = lf.getGraphData() as { nodes?: Item[] };
|
||||
const nodes = graphData.nodes || [];
|
||||
let affectedCount = 0;
|
||||
|
||||
nodes.forEach((node: any) => {
|
||||
if (node.id === deletedNodeId) return;
|
||||
const props = node.properties || {};
|
||||
const inputSource = Array.isArray(props.inputSource) ? props.inputSource : [];
|
||||
const nextInputSource = inputSource.filter((item: any) => item?.nodeId !== deletedNodeId);
|
||||
|
||||
if (nextInputSource.length === inputSource.length) return;
|
||||
|
||||
affectedCount += 1;
|
||||
const normalizedInputSource = nextInputSource.length > 0 ? nextInputSource : null;
|
||||
lf.setProperties(node.id, {
|
||||
...props,
|
||||
inputSource: normalizedInputSource,
|
||||
});
|
||||
|
||||
if (selectedElement.value?.id === node.id) {
|
||||
selectedElement.value.properties = {
|
||||
...props,
|
||||
inputSource: normalizedInputSource,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return affectedCount;
|
||||
};
|
||||
const getAffectedDownstreamNodeNames = (deletedNodeId: string) => {
|
||||
const lf = logicFlowInstance.value;
|
||||
if (!lf) return [] as string[];
|
||||
|
||||
const graphData = lf.getGraphData() as { nodes?: Item[] };
|
||||
const nodes = graphData.nodes || [];
|
||||
const names: string[] = [];
|
||||
|
||||
nodes.forEach((node: any) => {
|
||||
if (node.id === deletedNodeId) return;
|
||||
const props = node.properties || {};
|
||||
const inputSource = Array.isArray(props.inputSource) ? props.inputSource : [];
|
||||
const referenced = inputSource.some((item: any) => item?.nodeId === deletedNodeId);
|
||||
if (!referenced) return;
|
||||
|
||||
const nodeName = typeof node.text === 'string' ? node.text : node.text?.value || node.id;
|
||||
names.push(String(nodeName));
|
||||
});
|
||||
|
||||
return names;
|
||||
};
|
||||
const deleteSelectedElement = async () => {
|
||||
const lf = logicFlowInstance.value;
|
||||
const cur = selectedElement.value;
|
||||
if (!lf || !cur) return;
|
||||
|
||||
if (cur.kind === 'node' && (cur.properties?.nodeCode === START_NODE_CODE || cur.text === START_NODE_TEXT)) {
|
||||
ElMessage.warning('开始节点不能删除');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let affectedCount = 0;
|
||||
if (cur.kind === 'node') {
|
||||
const affectedNodeNames = getAffectedDownstreamNodeNames(cur.id);
|
||||
if (affectedNodeNames.length > 0) {
|
||||
const previewNames = affectedNodeNames.slice(0, 8);
|
||||
const overflowText = affectedNodeNames.length > 8 ? `\n...等 ${affectedNodeNames.length} 个节点` : '';
|
||||
await ElMessageBox.confirm(
|
||||
`删除该节点将清理以下下级节点中的引用:\n${previewNames.join('、')}${overflowText}`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '继续删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
affectedCount = cleanupReferencesToNode(cur.id);
|
||||
lf.deleteNode(cur.id);
|
||||
} else {
|
||||
lf.deleteEdge(cur.id);
|
||||
}
|
||||
selectedElement.value = null;
|
||||
ElMessage.success(affectedCount > 0 ? `删除成功,已清理 ${affectedCount} 个下级节点引用` : '删除成功');
|
||||
} catch (error) {
|
||||
if (error === 'cancel') return;
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
};// 从后端 DSL 恢复工作流
|
||||
const loadWorkflowFromDsl = (dsl: any) => {
|
||||
const lf = logicFlowInstance.value;
|
||||
if (!lf || !dsl) return;
|
||||
@@ -5456,3 +5626,7 @@ onBeforeUnmount(() => {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -152,9 +152,15 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||
<el-form-item label="Token映射" prop="tokenMapping">
|
||||
<el-input v-model="state.ruleForm.tokenMapping" placeholder="请输入Token映射" clearable></el-input>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
<el-form-item label="附加映射" prop="extendMapping">
|
||||
<el-input
|
||||
v-model="state.ruleForm.extendMapping"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='请输入 JSON 对象,例如:{"\"foo\": \"bar\"}'
|
||||
clearable
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||
@@ -163,7 +169,7 @@
|
||||
v-model="state.ruleForm.tokenConfig"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入 JSON 对象,例如:{promptRate: 1, completionRate: 1}"
|
||||
placeholder='请输入 JSON 对象,例如:{\"promptRate\": 1, \"completionRate\": 1}'
|
||||
clearable
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
@@ -240,9 +246,18 @@
|
||||
<el-dialog v-model="showResponseMappingDialog" title="配置响应映射" width="700px" :close-on-click-modal="false">
|
||||
<div class="mapping-config-container">
|
||||
<div v-for="(field, index) in state.responseMappingFields" :key="index" class="mapping-field-item">
|
||||
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 30%" clearable></el-input>
|
||||
<el-input
|
||||
v-model="field.key"
|
||||
placeholder="请输入字段名 (Key)"
|
||||
style="width: 30%"
|
||||
clearable
|
||||
@input="syncTokenFieldOnKeyChange(index)"
|
||||
></el-input>
|
||||
<span class="separator">=</span>
|
||||
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 30%" clearable></el-input>
|
||||
<el-button :type="field.isTokenField ? 'warning' : 'primary'" :plain="!field.isTokenField" @click="setTokenField(index)" size="small">
|
||||
{{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }}
|
||||
</el-button>
|
||||
<el-button :type="field.isMainBody ? 'success' : 'primary'" :plain="!field.isMainBody" @click="setMainBody(index)" size="small">
|
||||
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
|
||||
</el-button>
|
||||
@@ -271,6 +286,7 @@ import {
|
||||
getModelModuleDetail,
|
||||
getOperatorList,
|
||||
type ModelFormEntry,
|
||||
type CreateModelParams,
|
||||
} from '/@/api/settings/modelConfig/modelModule/index';
|
||||
|
||||
export type ModelTypeOption = { id: number | string; label: string };
|
||||
@@ -334,7 +350,8 @@ const state = reactive({
|
||||
retryQueueMaxSeconds: 60,
|
||||
autoCleanSeconds: 300,
|
||||
remark: '',
|
||||
tokenMapping: '',
|
||||
extendMapping: '{}',
|
||||
responseTokenField: '',
|
||||
tokenConfig: '{}',
|
||||
},
|
||||
rules: {
|
||||
@@ -401,6 +418,27 @@ const state = reactive({
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
extendMapping: [
|
||||
{
|
||||
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(String(value));
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
callback(new Error('附加映射必须为 JSON 对象'));
|
||||
} catch {
|
||||
callback(new Error('附加映射 JSON 格式不正确'));
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
dialog: {
|
||||
isShowDialog: false,
|
||||
@@ -414,7 +452,7 @@ const state = reactive({
|
||||
headers: [] as Array<{ key: string; value: string }>,
|
||||
formFields: [] as Array<{ key: string; value: string }>,
|
||||
requestMappingFields: [] as Array<{ key: string; value: string }>,
|
||||
responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean }>,
|
||||
responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>,
|
||||
mainBodyIndex: -1, // 记录哪一行被设置为返回主体
|
||||
});
|
||||
|
||||
@@ -549,11 +587,28 @@ const confirmRequestMappingFields = () => {
|
||||
|
||||
// 响应映射字段操作
|
||||
const addResponseMappingField = () => {
|
||||
state.responseMappingFields.push({ key: '', value: '', isMainBody: false });
|
||||
state.responseMappingFields.push({ key: '', value: '', isMainBody: false, isTokenField: false });
|
||||
};
|
||||
|
||||
const removeResponseMappingField = (index: number) => {
|
||||
const removed = state.responseMappingFields[index];
|
||||
state.responseMappingFields.splice(index, 1);
|
||||
if (removed?.isTokenField) {
|
||||
state.ruleForm.responseTokenField = '';
|
||||
}
|
||||
};
|
||||
|
||||
const setTokenField = (index: number) => {
|
||||
state.responseMappingFields.forEach((field, i) => {
|
||||
field.isTokenField = i === index;
|
||||
});
|
||||
state.ruleForm.responseTokenField = state.responseMappingFields[index]?.key?.trim?.() || '';
|
||||
};
|
||||
|
||||
const syncTokenFieldOnKeyChange = (index: number) => {
|
||||
const row = state.responseMappingFields[index];
|
||||
if (!row?.isTokenField) return;
|
||||
state.ruleForm.responseTokenField = row.key?.trim?.() || '';
|
||||
};
|
||||
|
||||
// 设置返回主体(单选)
|
||||
@@ -571,9 +626,9 @@ const confirmResponseMappingFields = () => {
|
||||
|
||||
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
|
||||
|
||||
const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean }>) => {
|
||||
if (!rows.length) return [{ key: '', value: '', isMainBody: false }];
|
||||
return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false }));
|
||||
const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>) => {
|
||||
if (!rows.length) return [{ key: '', value: '', isMainBody: false, isTokenField: false }];
|
||||
return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false, isTokenField: row.isTokenField || false }));
|
||||
};
|
||||
|
||||
/** 从 getModel 返回的 data 中取出单条模型对象 */
|
||||
@@ -623,11 +678,10 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||
remark: String(row.remark || ''),
|
||||
tokenMapping: String(row.tokenMapping || ''),
|
||||
tokenConfig:
|
||||
typeof row.tokenConfig === 'string'
|
||||
? row.tokenConfig
|
||||
: JSON.stringify((row.tokenConfig as Record<string, unknown>) || {}, null, 2),
|
||||
extendMapping:
|
||||
typeof row.extendMapping === 'string' ? row.extendMapping : JSON.stringify((row.extendMapping as Record<string, unknown>) || {}, null, 2),
|
||||
responseTokenField: String(row.responseTokenField || ''),
|
||||
tokenConfig: typeof row.tokenConfig === 'string' ? row.tokenConfig : JSON.stringify((row.tokenConfig as Record<string, unknown>) || {}, null, 2),
|
||||
};
|
||||
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
||||
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
||||
@@ -635,6 +689,15 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
||||
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
|
||||
state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
|
||||
|
||||
// 根据 responseTokenField 字段设置计费字段标记(单选)
|
||||
const tokenFieldKey = String(row.responseTokenField || '').trim();
|
||||
if (tokenFieldKey) {
|
||||
const tokenFieldIndex = state.responseMappingFields.findIndex((f) => String(f.key || '').trim() === tokenFieldKey);
|
||||
if (tokenFieldIndex !== -1) {
|
||||
state.responseMappingFields[tokenFieldIndex].isTokenField = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value})
|
||||
if (row.responseBody && typeof row.responseBody === 'object') {
|
||||
const responseBodyKey = Object.keys(row.responseBody)[0];
|
||||
@@ -705,13 +768,14 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
||||
retryQueueMaxSeconds: 60,
|
||||
autoCleanSeconds: 300,
|
||||
remark: '',
|
||||
tokenMapping: '',
|
||||
extendMapping: '{}',
|
||||
responseTokenField: '',
|
||||
tokenConfig: '{}',
|
||||
};
|
||||
state.headers = [{ key: '', value: '' }];
|
||||
state.formFields = [{ key: '', value: '' }];
|
||||
state.requestMappingFields = [{ key: '', value: '' }];
|
||||
state.responseMappingFields = [{ key: '', value: '', isMainBody: false }];
|
||||
state.responseMappingFields = [{ key: '', value: '', isMainBody: false, isTokenField: false }];
|
||||
state.dialog.title = '新增模型配置';
|
||||
state.dialog.submitTxt = '新 增';
|
||||
}
|
||||
@@ -742,7 +806,9 @@ const onSubmit = () => {
|
||||
// 获取被设置为返回主体的字段 {key: value}
|
||||
const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody);
|
||||
const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {};
|
||||
const submitData = {
|
||||
const responseTokenField =
|
||||
state.responseMappingFields.find((f) => f.isTokenField)?.key?.trim() || String(state.ruleForm.responseTokenField || '').trim();
|
||||
const submitData: CreateModelParams = {
|
||||
modelName: state.ruleForm.modelName,
|
||||
modelType: state.ruleForm.modelType as number | string,
|
||||
operatorName: state.ruleForm.operatorName,
|
||||
@@ -753,7 +819,9 @@ const onSubmit = () => {
|
||||
enabled: state.ruleForm.enabled,
|
||||
isChatModel: state.ruleForm.isChatModel,
|
||||
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
|
||||
form: fieldsToObject(state.formFields),
|
||||
form: state.formFields
|
||||
.filter((f) => String(f.key || '').trim() !== '')
|
||||
.map((f) => ({ key: String(f.key).trim(), value: String(f.value ?? '') })),
|
||||
requestMapping,
|
||||
responseMapping,
|
||||
responseBody,
|
||||
@@ -765,7 +833,8 @@ const onSubmit = () => {
|
||||
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
|
||||
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
||||
remark: state.ruleForm.remark || '',
|
||||
tokenMapping: state.ruleForm.tokenMapping || '',
|
||||
extendMapping: parseJsonObjectField(state.ruleForm.extendMapping || '{}', {}),
|
||||
responseTokenField,
|
||||
tokenConfig: parseJsonObjectField(state.ruleForm.tokenConfig || '{}', {}),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user