Merge remote-tracking branch 'origin/feature/workflow'
# Conflicts: # .gitea/workflows/deploy.yml
This commit is contained in:
@@ -7,28 +7,47 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
# 从组织级Secrets读取,不用在仓库重复配置
|
K3S_HOST: 121.37.117.181
|
||||||
K3S_HOST: ${{ secrets.K3S_HOST }}
|
|
||||||
APP_NAME: ${{ gitea.repo_name }}
|
APP_NAME: ${{ gitea.repo_name }}
|
||||||
|
REGISTRY: 你的镜像仓库地址 # 比如 docker.io/你的用户名
|
||||||
steps:
|
steps:
|
||||||
- name: 拉取代码
|
- uses: gitea/actions/checkout@v4
|
||||||
uses: 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
|
- name: SSH部署K3s
|
||||||
run: |
|
run:
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
# 写入组织配置的SSH私钥
|
echo "${{ secrets.K3S_PEM_KEY }}" > k3s.pem
|
||||||
echo "${{ secrets.K3S_SSH_KEY }}" > k3s.pem
|
|
||||||
chmod 600 k3s.pem
|
chmod 600 k3s.pem
|
||||||
# 调试:验证私钥是否正确写入
|
# 关键1:把Gitea仓库里的deploy.yaml上传到K3s服务器临时目录(/tmp)
|
||||||
echo "私钥文件权限:"
|
# 注意:如果你的deploy.yaml不在仓库根目录,要修改./deploy.yaml为实际路径
|
||||||
ls -l k3s.pem
|
scp -i k3s.pem -o StrictHostKeyChecking=no ./deploy.yaml root@${K3S_HOST}:/tmp/
|
||||||
echo "私钥头部(仅前5行):"
|
# 关键2:执行kubectl时指向临时目录的文件,而非不存在的/k8s/
|
||||||
head -5 k3s.pem
|
|
||||||
# 测试连接(会输出服务器主机名和kubectl版本)
|
|
||||||
ssh -i k3s.pem -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@${K3S_HOST} "hostname && kubectl version --client"
|
|
||||||
# 正式执行部署命令
|
|
||||||
ssh -i k3s.pem -o StrictHostKeyChecking=no root@${K3S_HOST} << CMD
|
ssh -i k3s.pem -o StrictHostKeyChecking=no root@${K3S_HOST} << CMD
|
||||||
kubectl apply -f /k8s/deploy.yaml
|
kubectl apply -f /tmp/deploy.yaml
|
||||||
kubectl rollout restart deployment ${APP_NAME}
|
kubectl rollout restart deployment ${APP_NAME} -n default
|
||||||
|
# 可选:部署完成后删除临时文件,清理服务器
|
||||||
|
rm -f /tmp/deploy.yaml
|
||||||
CMD
|
CMD
|
||||||
@@ -26,6 +26,6 @@ COPY ngnix.conf /etc/nginx/conf.d/default.conf
|
|||||||
# 复制SSL证书
|
# 复制SSL证书
|
||||||
COPY ssl/* /etc/nginx/ssl/
|
COPY ssl/* /etc/nginx/ssl/
|
||||||
|
|
||||||
EXPOSE 80 443
|
EXPOSE 443
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export interface CreateModelParams {
|
|||||||
modelName: string;
|
modelName: string;
|
||||||
/** 与 listType 返回的类型 id 一致,可能为数字或字符串 */
|
/** 与 listType 返回的类型 id 一致,可能为数字或字符串 */
|
||||||
modelType: number | string;
|
modelType: number | string;
|
||||||
|
operatorName?: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
httpMethod?: string;
|
httpMethod?: string;
|
||||||
headMsg?: string;
|
headMsg?: string;
|
||||||
@@ -122,6 +123,10 @@ export interface CreateModelParams {
|
|||||||
form: ModelFormEntry[];
|
form: ModelFormEntry[];
|
||||||
requestMapping?: Record<string, unknown>;
|
requestMapping?: Record<string, unknown>;
|
||||||
responseMapping?: Record<string, unknown>;
|
responseMapping?: Record<string, unknown>;
|
||||||
|
responseBody?: Record<string, unknown>;
|
||||||
|
extendMapping?: Record<string, unknown>;
|
||||||
|
responseTokenField?: string;
|
||||||
|
tokenConfig?: Record<string, unknown>;
|
||||||
maxConcurrency?: number;
|
maxConcurrency?: number;
|
||||||
queueLimit?: number;
|
queueLimit?: number;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
|
|||||||
@@ -92,7 +92,14 @@
|
|||||||
import { ref, reactive, watch, onMounted } from 'vue';
|
import { ref, reactive, watch, onMounted } from 'vue';
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
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 { checkIsSuperAdmin } from '/@/api/system/user/index';
|
||||||
import { getApiErrorMessage } from '/@/utils/request';
|
import { getApiErrorMessage } from '/@/utils/request';
|
||||||
import EditModule from '/@/views/settings/modelConfig/modelModule/component/editModule.vue';
|
import EditModule from '/@/views/settings/modelConfig/modelModule/component/editModule.vue';
|
||||||
@@ -101,7 +108,7 @@ interface ModelItem {
|
|||||||
id: string;
|
id: string;
|
||||||
tenantId?: number;
|
tenantId?: number;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
modelType: number;
|
modelType: number | string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
route: string;
|
route: string;
|
||||||
httpMethod: string;
|
httpMethod: string;
|
||||||
@@ -113,9 +120,10 @@ interface ModelItem {
|
|||||||
operatorName?: string;
|
operatorName?: string;
|
||||||
responseBody?: Record<string, unknown>;
|
responseBody?: Record<string, unknown>;
|
||||||
tokenConfig?: Record<string, unknown> | string;
|
tokenConfig?: Record<string, unknown> | string;
|
||||||
form?: any;
|
extendMapping?: Record<string, unknown> | string;
|
||||||
requestMapping?: any;
|
form?: ModelFormEntry[] | Record<string, unknown>;
|
||||||
responseMapping?: any;
|
requestMapping?: Record<string, unknown>;
|
||||||
|
responseMapping?: Record<string, unknown>;
|
||||||
maxConcurrency?: number;
|
maxConcurrency?: number;
|
||||||
queueLimit?: number;
|
queueLimit?: number;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
@@ -206,13 +214,30 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const getModelTypeName = (type: number) => {
|
const getModelTypeName = (type: number | string) => {
|
||||||
const typeMap: Record<number, string> = {
|
const typeMap: Record<number, string> = {
|
||||||
1: '推理模型',
|
1: '推理模型',
|
||||||
2: '图片模型',
|
2: '图片模型',
|
||||||
3: '音频模型',
|
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 () => {
|
const fetchModelList = async () => {
|
||||||
@@ -270,7 +295,13 @@ const handleCreatePrivateModel = async () => {
|
|||||||
creatingModel.value = true;
|
creatingModel.value = true;
|
||||||
|
|
||||||
const builtInModel = builtInModelToClone.value;
|
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,
|
modelName: apiKeyForm.modelName,
|
||||||
modelType: builtInModel.modelType,
|
modelType: builtInModel.modelType,
|
||||||
operatorName: builtInModel.operatorName || '',
|
operatorName: builtInModel.operatorName || '',
|
||||||
@@ -281,9 +312,9 @@ const handleCreatePrivateModel = async () => {
|
|||||||
enabled: builtInModel.enabled ?? 1,
|
enabled: builtInModel.enabled ?? 1,
|
||||||
isChatModel: builtInModel.isChatModel || 0,
|
isChatModel: builtInModel.isChatModel || 0,
|
||||||
apiKey: apiKeyForm.apiKey,
|
apiKey: apiKeyForm.apiKey,
|
||||||
form: builtInModel.form || {},
|
form: formList,
|
||||||
requestMapping: builtInModel.requestMapping || {},
|
requestMapping: (builtInModel.requestMapping as Record<string, unknown>) || {},
|
||||||
responseMapping: builtInModel.responseMapping || {},
|
responseMapping: (builtInModel.responseMapping as Record<string, unknown>) || {},
|
||||||
responseBody: builtInModel.responseBody || {},
|
responseBody: builtInModel.responseBody || {},
|
||||||
maxConcurrency: builtInModel.maxConcurrency || 10,
|
maxConcurrency: builtInModel.maxConcurrency || 10,
|
||||||
queueLimit: builtInModel.queueLimit || 100,
|
queueLimit: builtInModel.queueLimit || 100,
|
||||||
@@ -293,8 +324,9 @@ const handleCreatePrivateModel = async () => {
|
|||||||
retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60,
|
retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60,
|
||||||
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
||||||
remark: builtInModel.remark || '',
|
remark: builtInModel.remark || '',
|
||||||
tokenMapping: builtInModel.tokenMapping || '',
|
|
||||||
tokenConfig: builtInModel.tokenConfig || {},
|
extendMapping: parseJsonObjectField(builtInModel.extendMapping),
|
||||||
|
tokenConfig: parseJsonObjectField(builtInModel.tokenConfig),
|
||||||
};
|
};
|
||||||
|
|
||||||
const res: any = await addModelModule(createParams);
|
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 }">
|
<div class="creation-page" :class="{ 'creation-mode': isCreationMode }">
|
||||||
<!-- 左侧面板:工作空间/当前选中元素 Tab切换 -->
|
<!-- 左侧面板:工作空间/当前选中元素 Tab切换 -->
|
||||||
<div class="panel left">
|
<div class="panel left">
|
||||||
@@ -352,7 +352,13 @@
|
|||||||
:accept="getCreationFileAccept(field)"
|
:accept="getCreationFileAccept(field)"
|
||||||
:on-change="(file: any) => handleCreationFieldUpload(node, field, file)"
|
: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>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
<div class="creation-upload-tags">
|
<div class="creation-upload-tags">
|
||||||
@@ -360,7 +366,11 @@
|
|||||||
<span class="creation-upload-tag count">已上传 {{ getCreationFileCountText(node, field) }}</span>
|
<span class="creation-upload-tag count">已上传 {{ getCreationFileCountText(node, field) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getCreationFieldFiles(node, field).length > 0" class="uploaded-files-list creation-upload-list">
|
<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>
|
<span class="file-name">{{ uploadedFile.name }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -439,6 +449,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta-actions">
|
<div class="meta-actions">
|
||||||
<el-button size="small" @click="resetFlow">清空画布</el-button>
|
<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>
|
<el-button type="primary" size="small" @click="saveWorkflowAction" :loading="saving">保存工作流</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,25 +576,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保存工作流对话框 -->
|
<!-- 保存工作流对话框 -->
|
||||||
<el-dialog
|
<SaveWorkflowDialog
|
||||||
v-model="saveDialogVisible"
|
v-model="saveDialogVisible"
|
||||||
:title="currentEditingWorkflowId ? '编辑工作流' : '保存工作流'"
|
:save-form="saveForm"
|
||||||
width="500px"
|
:current-editing-workflow-id="currentEditingWorkflowId"
|
||||||
:close-on-click-modal="false"
|
:saving="saving"
|
||||||
>
|
@confirm="confirmSaveWorkflow"
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 技能选择器 -->
|
<!-- 技能选择器 -->
|
||||||
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkill" @confirm="handleSkillConfirm" />
|
<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>
|
<el-dialog v-model="previewDialogVisible" title="预览" width="95%" top="2vh" :close-on-click-modal="false" destroy-on-close>
|
||||||
<div class="preview-container">
|
<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="无法加载预览内容" />
|
<el-empty v-else description="无法加载预览内容" />
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -776,6 +794,7 @@ import '@logicflow/core/dist/index.css';
|
|||||||
import '@logicflow/extension/lib/style/index.css';
|
import '@logicflow/extension/lib/style/index.css';
|
||||||
import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
|
import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
|
||||||
import ModelSelector from '/@/components/model/ModelSelector.vue';
|
import ModelSelector from '/@/components/model/ModelSelector.vue';
|
||||||
|
import SaveWorkflowDialog from './component/SaveWorkflowDialog.vue';
|
||||||
import type { SkillItem } from '/@/api/settings/skill';
|
import type { SkillItem } from '/@/api/settings/skill';
|
||||||
import {
|
import {
|
||||||
downloadToFile,
|
downloadToFile,
|
||||||
@@ -810,6 +829,7 @@ interface TreeNode {
|
|||||||
children?: TreeNode[];
|
children?: TreeNode[];
|
||||||
fileUrl?: string;
|
fileUrl?: string;
|
||||||
workflowId?: number | string;
|
workflowId?: number | string;
|
||||||
|
fileType?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
interface SelectedState {
|
interface SelectedState {
|
||||||
@@ -881,6 +901,7 @@ const isDraggingMiddleSplitter = ref(false);
|
|||||||
// 预览相关状态
|
// 预览相关状态
|
||||||
const previewDialogVisible = ref(false);
|
const previewDialogVisible = ref(false);
|
||||||
const previewUrl = ref('');
|
const previewUrl = ref('');
|
||||||
|
const previewMode = ref<'iframe' | 'image' | 'video' | 'audio'>('iframe');
|
||||||
// 模型选择器相关状态
|
// 模型选择器相关状态
|
||||||
const showModelSelector = ref(false);
|
const showModelSelector = ref(false);
|
||||||
const selectedModelData = ref<any>(null);
|
const selectedModelData = ref<any>(null);
|
||||||
@@ -1252,6 +1273,7 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
|||||||
label: item.label || `作品${ii + 1}`,
|
label: item.label || `作品${ii + 1}`,
|
||||||
nodeType: 'title',
|
nodeType: 'title',
|
||||||
fileUrl: item.content,
|
fileUrl: item.content,
|
||||||
|
fileType: item.type,
|
||||||
workflowId: f.Id,
|
workflowId: f.Id,
|
||||||
sessionId: f.sessionId,
|
sessionId: f.sessionId,
|
||||||
})),
|
})),
|
||||||
@@ -1945,7 +1967,9 @@ const sendMessage = async () => {
|
|||||||
value:
|
value:
|
||||||
bodyItem.fieldType === 'fileUpload'
|
bodyItem.fieldType === 'fileUpload'
|
||||||
? Array.isArray(userVal !== undefined ? userVal : bodyItem.value)
|
? Array.isArray(userVal !== undefined ? userVal : bodyItem.value)
|
||||||
? (userVal !== undefined ? userVal : bodyItem.value)
|
? userVal !== undefined
|
||||||
|
? userVal
|
||||||
|
: bodyItem.value
|
||||||
: userVal !== undefined
|
: userVal !== undefined
|
||||||
? [userVal]
|
? [userVal]
|
||||||
: bodyItem.value
|
: bodyItem.value
|
||||||
@@ -2248,9 +2272,21 @@ const handleTreeNodeClick = async (data: TreeNode) => {
|
|||||||
};
|
};
|
||||||
// 预览节点
|
// 预览节点
|
||||||
const previewNode = (d: 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);
|
const url = buildAssetUrl(d.fileUrl);
|
||||||
if (!url) return ElMessage.warning('当前节点没有可用预览地址');
|
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;
|
previewUrl.value = url;
|
||||||
previewDialogVisible.value = true;
|
previewDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
@@ -2264,7 +2300,9 @@ const downloadNode = async (d: TreeNode) => {
|
|||||||
const blob = r instanceof Blob ? r : r?.data;
|
const blob = r instanceof Blob ? r : r?.data;
|
||||||
if (!(blob instanceof Blob)) throw new Error('invalid blob');
|
if (!(blob instanceof Blob)) throw new Error('invalid blob');
|
||||||
const fileName = d.fileUrl.split('/').pop() || '';
|
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 name = decodeURIComponent(fileName || `${d.label}.${fileExt}`);
|
||||||
const u = URL.createObjectURL(blob);
|
const u = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -2274,7 +2312,7 @@ const downloadNode = async (d: TreeNode) => {
|
|||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(u);
|
URL.revokeObjectURL(u);
|
||||||
ElMessage.success('下载成功');
|
ElMessage.success(`下载成功${type ? `(${type})` : ''}`);
|
||||||
} catch {
|
} catch {
|
||||||
// 下载失败由 request 全局提示后端 message
|
// 下载失败由 request 全局提示后端 message
|
||||||
}
|
}
|
||||||
@@ -2406,12 +2444,18 @@ const isHttpExpandTriggerField = (fieldItem: any) => {
|
|||||||
};
|
};
|
||||||
const openHttpExpandDialog = (fieldItem: any) => {
|
const openHttpExpandDialog = (fieldItem: any) => {
|
||||||
if (!Array.isArray(fieldItem?.expand)) return;
|
if (!Array.isArray(fieldItem?.expand)) return;
|
||||||
|
const nodeId = selectedElement.value?.id;
|
||||||
|
if (!nodeId) return;
|
||||||
|
|
||||||
currentHttpExpandFields.value = fieldItem.expand;
|
currentHttpExpandFields.value = fieldItem.expand;
|
||||||
Object.keys(httpExpandFormValues).forEach((k) => delete httpExpandFormValues[k]);
|
Object.keys(httpExpandFormValues).forEach((k) => delete httpExpandFormValues[k]);
|
||||||
Object.keys(httpExpandKeyValuePairs).forEach((k) => delete httpExpandKeyValuePairs[k]);
|
Object.keys(httpExpandKeyValuePairs).forEach((k) => delete httpExpandKeyValuePairs[k]);
|
||||||
|
|
||||||
fieldItem.expand.forEach((f: any) => {
|
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;
|
showHttpExpandDialog.value = true;
|
||||||
};
|
};
|
||||||
const getHttpExpandKeyValuePairs = (field: string) => {
|
const getHttpExpandKeyValuePairs = (field: string) => {
|
||||||
@@ -2454,12 +2498,28 @@ const removeHttpExpandKeyValuePair = (field: string, index: number) => {
|
|||||||
updateHttpExpandKeyValueField(field);
|
updateHttpExpandKeyValueField(field);
|
||||||
};
|
};
|
||||||
const confirmHttpExpandDialog = () => {
|
const confirmHttpExpandDialog = () => {
|
||||||
|
const nodeId = selectedElement.value?.id;
|
||||||
|
if (!nodeId) return;
|
||||||
|
|
||||||
currentHttpExpandFields.value.forEach((f: any) => {
|
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;
|
showHttpExpandDialog.value = false;
|
||||||
ElMessage.success('主动拉取参数已保存');
|
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请求体配置相关函数
|
||||||
// 打开HTTP请求体配置弹窗
|
// 打开HTTP请求体配置弹窗
|
||||||
@@ -3182,12 +3242,14 @@ watch(
|
|||||||
currentHttpBodyField.value = '';
|
currentHttpBodyField.value = '';
|
||||||
showHttpBodyDialog.value = false;
|
showHttpBodyDialog.value = false;
|
||||||
|
|
||||||
// 重置 dynamicFormValues(不删除键,保持响应式)
|
// 重置 dynamicFormValues(不删除固定字段键,动态 expand 键按节点切换清理)
|
||||||
for (const key in dynamicFormValues) {
|
for (const key in dynamicFormValues) {
|
||||||
|
if (key.includes('_responseType_expand_')) {
|
||||||
|
delete dynamicFormValues[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
dynamicFormValues[key] = '';
|
dynamicFormValues[key] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前节点的基础表单字段(直接从 nodeSchemaMap 获取,避免响应式延迟)
|
|
||||||
const currentNodeCode = formState.nodeCode;
|
const currentNodeCode = formState.nodeCode;
|
||||||
const baseFormFields = nodeSchemaMap.value[currentNodeCode] || [];
|
const baseFormFields = nodeSchemaMap.value[currentNodeCode] || [];
|
||||||
const baseFieldNames = new Set(baseFormFields.map((f) => f.field));
|
const baseFieldNames = new Set(baseFormFields.map((f) => f.field));
|
||||||
@@ -3208,6 +3270,16 @@ watch(
|
|||||||
// 其他类型:保持原值
|
// 其他类型:保持原值
|
||||||
dynamicFormValues[fieldConfig.field] = fieldConfig.value;
|
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 {
|
} else {
|
||||||
// 自定义字段:加载到 customFields
|
// 自定义字段:加载到 customFields
|
||||||
const customType = fieldConfig.type === 'upload' ? 'uploadMultiple' : fieldConfig.type || 'input';
|
const customType = fieldConfig.type === 'upload' ? 'uploadMultiple' : fieldConfig.type || 'input';
|
||||||
@@ -3460,6 +3532,11 @@ const applySelected = () => {
|
|||||||
label: fieldItem.label,
|
label: fieldItem.label,
|
||||||
value: normalizedValue,
|
value: normalizedValue,
|
||||||
required: fieldItem.required || false,
|
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;
|
selectedElement.value = null;
|
||||||
syncDsl();
|
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 loadWorkflowFromDsl = (dsl: any) => {
|
||||||
const lf = logicFlowInstance.value;
|
const lf = logicFlowInstance.value;
|
||||||
if (!lf || !dsl) return;
|
if (!lf || !dsl) return;
|
||||||
@@ -5456,3 +5626,7 @@ onBeforeUnmount(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -152,9 +152,15 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||||
<el-form-item label="Token映射" prop="tokenMapping">
|
<el-form-item label="附加映射" prop="extendMapping">
|
||||||
<el-input v-model="state.ruleForm.tokenMapping" placeholder="请输入Token映射" clearable></el-input>
|
<el-input
|
||||||
|
v-model="state.ruleForm.extendMapping"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder='请输入 JSON 对象,例如:{"\"foo\": \"bar\"}'
|
||||||
|
clearable
|
||||||
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||||
@@ -163,7 +169,7 @@
|
|||||||
v-model="state.ruleForm.tokenConfig"
|
v-model="state.ruleForm.tokenConfig"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
placeholder="请输入 JSON 对象,例如:{promptRate: 1, completionRate: 1}"
|
placeholder='请输入 JSON 对象,例如:{\"promptRate\": 1, \"completionRate\": 1}'
|
||||||
clearable
|
clearable
|
||||||
></el-input>
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -240,9 +246,18 @@
|
|||||||
<el-dialog v-model="showResponseMappingDialog" title="配置响应映射" width="700px" :close-on-click-modal="false">
|
<el-dialog v-model="showResponseMappingDialog" title="配置响应映射" width="700px" :close-on-click-modal="false">
|
||||||
<div class="mapping-config-container">
|
<div class="mapping-config-container">
|
||||||
<div v-for="(field, index) in state.responseMappingFields" :key="index" class="mapping-field-item">
|
<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>
|
<span class="separator">=</span>
|
||||||
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 30%" clearable></el-input>
|
<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">
|
<el-button :type="field.isMainBody ? 'success' : 'primary'" :plain="!field.isMainBody" @click="setMainBody(index)" size="small">
|
||||||
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
|
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -271,6 +286,7 @@ import {
|
|||||||
getModelModuleDetail,
|
getModelModuleDetail,
|
||||||
getOperatorList,
|
getOperatorList,
|
||||||
type ModelFormEntry,
|
type ModelFormEntry,
|
||||||
|
type CreateModelParams,
|
||||||
} from '/@/api/settings/modelConfig/modelModule/index';
|
} from '/@/api/settings/modelConfig/modelModule/index';
|
||||||
|
|
||||||
export type ModelTypeOption = { id: number | string; label: string };
|
export type ModelTypeOption = { id: number | string; label: string };
|
||||||
@@ -334,7 +350,8 @@ const state = reactive({
|
|||||||
retryQueueMaxSeconds: 60,
|
retryQueueMaxSeconds: 60,
|
||||||
autoCleanSeconds: 300,
|
autoCleanSeconds: 300,
|
||||||
remark: '',
|
remark: '',
|
||||||
tokenMapping: '',
|
extendMapping: '{}',
|
||||||
|
responseTokenField: '',
|
||||||
tokenConfig: '{}',
|
tokenConfig: '{}',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
@@ -401,6 +418,27 @@ const state = reactive({
|
|||||||
trigger: 'blur',
|
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: {
|
dialog: {
|
||||||
isShowDialog: false,
|
isShowDialog: false,
|
||||||
@@ -414,7 +452,7 @@ const state = reactive({
|
|||||||
headers: [] as Array<{ key: string; value: string }>,
|
headers: [] as Array<{ key: string; value: string }>,
|
||||||
formFields: [] as Array<{ key: string; value: string }>,
|
formFields: [] as Array<{ key: string; value: string }>,
|
||||||
requestMappingFields: [] 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, // 记录哪一行被设置为返回主体
|
mainBodyIndex: -1, // 记录哪一行被设置为返回主体
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -549,11 +587,28 @@ const confirmRequestMappingFields = () => {
|
|||||||
|
|
||||||
// 响应映射字段操作
|
// 响应映射字段操作
|
||||||
const addResponseMappingField = () => {
|
const addResponseMappingField = () => {
|
||||||
state.responseMappingFields.push({ key: '', value: '', isMainBody: false });
|
state.responseMappingFields.push({ key: '', value: '', isMainBody: false, isTokenField: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeResponseMappingField = (index: number) => {
|
const removeResponseMappingField = (index: number) => {
|
||||||
|
const removed = state.responseMappingFields[index];
|
||||||
state.responseMappingFields.splice(index, 1);
|
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 ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
|
||||||
|
|
||||||
const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean }>) => {
|
const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>) => {
|
||||||
if (!rows.length) return [{ key: '', value: '', isMainBody: false }];
|
if (!rows.length) return [{ key: '', value: '', isMainBody: false, isTokenField: false }];
|
||||||
return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false }));
|
return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false, isTokenField: row.isTokenField || false }));
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 从 getModel 返回的 data 中取出单条模型对象 */
|
/** 从 getModel 返回的 data 中取出单条模型对象 */
|
||||||
@@ -623,11 +678,10 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
|||||||
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
|
||||||
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
|
||||||
remark: String(row.remark || ''),
|
remark: String(row.remark || ''),
|
||||||
tokenMapping: String(row.tokenMapping || ''),
|
extendMapping:
|
||||||
tokenConfig:
|
typeof row.extendMapping === 'string' ? row.extendMapping : JSON.stringify((row.extendMapping as Record<string, unknown>) || {}, null, 2),
|
||||||
typeof row.tokenConfig === 'string'
|
responseTokenField: String(row.responseTokenField || ''),
|
||||||
? row.tokenConfig
|
tokenConfig: typeof row.tokenConfig === 'string' ? row.tokenConfig : JSON.stringify((row.tokenConfig as Record<string, unknown>) || {}, null, 2),
|
||||||
: JSON.stringify((row.tokenConfig as Record<string, unknown>) || {}, null, 2),
|
|
||||||
};
|
};
|
||||||
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
|
||||||
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
|
||||||
@@ -635,6 +689,15 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
|
|||||||
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
|
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
|
||||||
state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
|
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})
|
// 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value})
|
||||||
if (row.responseBody && typeof row.responseBody === 'object') {
|
if (row.responseBody && typeof row.responseBody === 'object') {
|
||||||
const responseBodyKey = Object.keys(row.responseBody)[0];
|
const responseBodyKey = Object.keys(row.responseBody)[0];
|
||||||
@@ -705,13 +768,14 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
|
|||||||
retryQueueMaxSeconds: 60,
|
retryQueueMaxSeconds: 60,
|
||||||
autoCleanSeconds: 300,
|
autoCleanSeconds: 300,
|
||||||
remark: '',
|
remark: '',
|
||||||
tokenMapping: '',
|
extendMapping: '{}',
|
||||||
|
responseTokenField: '',
|
||||||
tokenConfig: '{}',
|
tokenConfig: '{}',
|
||||||
};
|
};
|
||||||
state.headers = [{ key: '', value: '' }];
|
state.headers = [{ key: '', value: '' }];
|
||||||
state.formFields = [{ key: '', value: '' }];
|
state.formFields = [{ key: '', value: '' }];
|
||||||
state.requestMappingFields = [{ 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.title = '新增模型配置';
|
||||||
state.dialog.submitTxt = '新 增';
|
state.dialog.submitTxt = '新 增';
|
||||||
}
|
}
|
||||||
@@ -742,7 +806,9 @@ const onSubmit = () => {
|
|||||||
// 获取被设置为返回主体的字段 {key: value}
|
// 获取被设置为返回主体的字段 {key: value}
|
||||||
const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody);
|
const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody);
|
||||||
const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {};
|
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,
|
modelName: state.ruleForm.modelName,
|
||||||
modelType: state.ruleForm.modelType as number | string,
|
modelType: state.ruleForm.modelType as number | string,
|
||||||
operatorName: state.ruleForm.operatorName,
|
operatorName: state.ruleForm.operatorName,
|
||||||
@@ -753,7 +819,9 @@ const onSubmit = () => {
|
|||||||
enabled: state.ruleForm.enabled,
|
enabled: state.ruleForm.enabled,
|
||||||
isChatModel: state.ruleForm.isChatModel,
|
isChatModel: state.ruleForm.isChatModel,
|
||||||
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
|
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,
|
requestMapping,
|
||||||
responseMapping,
|
responseMapping,
|
||||||
responseBody,
|
responseBody,
|
||||||
@@ -765,7 +833,8 @@ const onSubmit = () => {
|
|||||||
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
|
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
|
||||||
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
|
||||||
remark: state.ruleForm.remark || '',
|
remark: state.ruleForm.remark || '',
|
||||||
tokenMapping: state.ruleForm.tokenMapping || '',
|
extendMapping: parseJsonObjectField(state.ruleForm.extendMapping || '{}', {}),
|
||||||
|
responseTokenField,
|
||||||
tokenConfig: parseJsonObjectField(state.ruleForm.tokenConfig || '{}', {}),
|
tokenConfig: parseJsonObjectField(state.ruleForm.tokenConfig || '{}', {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user