feat: workflow application debug chat (#3245)

This commit is contained in:
shaohuzhang1 2025-06-12 21:10:10 +08:00 committed by GitHub
parent c7e30bb098
commit bf630b9d07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 145 additions and 118 deletions

View File

@ -82,7 +82,7 @@ class WorkFlowPostHandler:
index=0)
self.chat_info.append_chat_record(chat_record)
self.chat_info.set_cahce()
self.chat_info.set_cache()
if [ChatUserType.ANONYMOUS_USER.value, ChatUserType.CHAT_USER.value].__contains__(
workflow_body.get('chat_user_type')):
application_public_access_client = (QuerySet(ApplicationChatUserStats)

View File

@ -219,12 +219,17 @@ class ChatSerializers(serializers.Serializer):
other_list = instance.get('other_list')
workspace_id = chat_info.application.workspace_id
chat_record_id = instance.get('chat_record_id')
debug = self.data.get('debug', False)
chat_record = None
history_chat_record = chat_info.chat_record_list
if chat_record_id is not None:
chat_record = self.get_chat_record(chat_info, chat_record_id)
history_chat_record = [r for r in chat_info.chat_record_list if str(r.id) != chat_record_id]
work_flow_manage = WorkflowManage(Flow.new_instance(chat_info.work_flow_version.work_flow),
if not debug:
work_flow = chat_info.work_flow_version.work_flow
else:
work_flow = chat_info.application.work_flow
work_flow_manage = WorkflowManage(Flow.new_instance(work_flow),
{'history_chat_record': history_chat_record, 'question': message,
'chat_id': chat_info.chat_id, 'chat_record_id': str(
uuid.uuid1()) if chat_record is None else chat_record.id,
@ -233,7 +238,7 @@ class ChatSerializers(serializers.Serializer):
'chat_user_id': chat_user_id,
'chat_user_type': chat_user_type,
'workspace_id': workspace_id,
'debug': self.data.get('debug', False)},
'debug': debug},
WorkFlowPostHandler(chat_info),
base_to_response, form_data, image_list, document_list, audio_list,
other_list,
@ -339,12 +344,14 @@ class OpenChatSerializers(serializers.Serializer):
chat_user_type = self.data.get("chat_user_type")
debug = self.data.get("debug")
chat_id = str(uuid.uuid7())
work_flow_version = QuerySet(WorkFlowVersion).filter(application_id=application_id).order_by(
'-create_time')[0:1].first()
if work_flow_version is None:
raise AppApiException(500,
gettext(
"The application has not been published. Please use it after publishing."))
work_flow_version = None
if not debug:
work_flow_version = QuerySet(WorkFlowVersion).filter(application_id=application_id).order_by(
'-create_time')[0:1].first()
if work_flow_version is None:
raise AppApiException(500,
gettext(
"The application has not been published. Please use it after publishing."))
ChatInfo(chat_id, chat_user_id, chat_user_type, [],
[],
application_id,

View File

@ -17,8 +17,8 @@
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@logicflow/core": "^2.0.15",
"@logicflow/extension": "^2.0.20",
"@logicflow/core": "^1.2.27",
"@logicflow/extension": "^1.2.27",
"@vavt/cm-extension": "^1.9.1",
"@vueuse/core": "^13.3.0",
"@wecom/jssdk": "^2.3.1",

View File

@ -22,60 +22,55 @@ const getModel: (
return get(`${prefix}/model`, data, loading)
}
/**
* 使
* @param application_id
* @param loading
* @query { query_text: string, top_number: number, similarity: number }
* @returns
*
* @param loading
* @returns
*/
const getApplicationRerankerModel: (
application_id: string,
loading?: Ref<boolean>,
) => Promise<Result<Array<any>>> = (application_id, loading) => {
const getRerankerModel: (loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (loading) => {
return get(`${prefix}/model`, { model_type: 'RERANKER' }, loading)
}
/**
* 使
* @param application_id
*
* @param loading
* @query { query_text: string, top_number: number, similarity: number }
* @returns
* @returns
*/
const getApplicationSTTModel: (
application_id: string,
loading?: Ref<boolean>,
) => Promise<Result<Array<any>>> = (application_id, loading) => {
const getSTTModel: (loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (loading) => {
return get(`${prefix}/model`, { model_type: 'STT' }, loading)
}
/**
* 使
* @param application_id
*
* @param loading
* @query { query_text: string, top_number: number, similarity: number }
* @returns
*/
const getApplicationTTSModel: (
application_id: string,
loading?: Ref<boolean>,
) => Promise<Result<Array<any>>> = (application_id, loading) => {
const getTTSModel: (loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (loading) => {
return get(`${prefix}/model`, { model_type: 'TTS' }, loading)
}
const getApplicationImageModel: (
application_id: string,
loading?: Ref<boolean>,
) => Promise<Result<Array<any>>> = (application_id, loading) => {
/**
*
* @param loading
* @returns
*/
const getImageModel: (loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (loading) => {
return get(`${prefix}/model`, { model_type: 'IMAGE' }, loading)
}
const getApplicationTTIModel: (
application_id: string,
loading?: Ref<boolean>,
) => Promise<Result<Array<any>>> = (application_id, loading) => {
/**
*
* @param loading
* @returns
*/
const getTTIModel: (loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (loading) => {
return get(`${prefix}/model`, { model_type: 'TTI' }, loading)
}
/**
*
* @param loading
* @returns
*/
const getLLMModel: (loading?: Ref<boolean>) => Promise<Result<Array<any>>> = (loading) => {
return get(`${prefix}/model`, { model_type: 'LLM' }, loading)
}
/**
*
* @param model_id id
@ -182,9 +177,10 @@ export default {
pauseDownload,
getModelParamsForm,
updateModelParamsForm,
getApplicationRerankerModel,
getApplicationSTTModel,
getApplicationTTSModel,
getApplicationImageModel,
getApplicationTTIModel,
getRerankerModel,
getSTTModel,
getTTSModel,
getImageModel,
getTTIModel,
getLLMModel,
}

View File

@ -0,0 +1,31 @@
import type { App } from 'vue'
export default {
install: (app: App) => {
app.directive('resize', {
created(el: any, binding: any) {
// 记录长宽
let width = ''
let height = ''
function getSize() {
const style = (document.defaultView as any).getComputedStyle(el)
// 如果当前长宽和历史长宽不同
if (width !== style.width || height !== style.height) {
// binding.value在这里就是下面的resizeChart函数
binding.value({
width: parseFloat(style.width),
height: parseFloat(style.height)
})
}
width = style.width
height = style.height
}
;(el as any).__vueDomResize__ = setInterval(getSize, 500)
},
unmounted(el: any, binding: any) {
clearInterval((el as any).__vueDomResize__)
}
})
}
}

View File

@ -673,7 +673,7 @@ function getKnowledge() {
function getModel() {
loading.value = true
modelAPI
.getModel({})
.getLLMModel()
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
@ -686,7 +686,7 @@ function getModel() {
function getSTTModel() {
loading.value = true
modelAPI
.getApplicationSTTModel(id)
.getSTTModel()
.then((res: any) => {
sttModelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
@ -699,7 +699,7 @@ function getSTTModel() {
function getTTSModel() {
loading.value = true
modelAPI
.getApplicationTTSModel(id)
.getTTSModel()
.then((res: any) => {
ttsModelOptions.value = groupBy(res?.data, 'provider')
loading.value = false

View File

@ -144,8 +144,8 @@
{
required: true,
message: $t('common.inputPlaceholder'),
trigger: 'blur'
}
trigger: 'blur',
},
]"
>
<el-input v-model="form.title" @blur="form.title = form.title.trim()" />
@ -176,7 +176,7 @@ import { MsgError, MsgConfirm } from '@/utils/message'
import type { FormInstance } from 'element-plus'
import { t } from '@/locales'
const {
params: { id }
params: { id },
} = app.config.globalProperties.$route as any
const height = ref<{
@ -186,14 +186,14 @@ const height = ref<{
}>({
stepContainerHeight: 0,
inputContainerHeight: 0,
outputContainerHeight: 0
outputContainerHeight: 0,
})
const showAnchor = ref<boolean>(false)
const anchorData = ref<any>()
const titleFormRef = ref()
const nodeNameDialogVisible = ref<boolean>(false)
const form = ref<any>({
title: ''
title: '',
})
const condition = computed({
@ -206,7 +206,7 @@ const condition = computed({
}
set(props.nodeModel.properties, 'condition', 'AND')
return true
}
},
})
const showNode = computed({
set: (v) => {
@ -218,7 +218,7 @@ const showNode = computed({
}
set(props.nodeModel.properties, 'showNode', true)
return true
}
},
})
const handleWheel = (event: any) => {
@ -244,7 +244,7 @@ const editName = async (formEl: FormInstance | undefined) => {
if (valid) {
if (
!props.nodeModel.graphModel.nodes?.some(
(node: any) => node.properties.stepName === form.value.title
(node: any) => node.properties.stepName === form.value.title,
)
) {
set(props.nodeModel.properties, 'stepName', form.value.title)
@ -274,7 +274,7 @@ const copyNode = () => {
const deleteNode = () => {
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger'
confirmButtonClass: 'danger',
}).then(() => {
props.nodeModel.graphModel.deleteNode(props.nodeModel.id)
})
@ -295,13 +295,13 @@ function clickNodes(item: any) {
type: item.type,
properties: item.properties,
x: anchorData.value?.x + width / 2 + 200,
y: anchorData.value?.y - item.height
y: anchorData.value?.y - item.height,
})
props.nodeModel.graphModel.addEdge({
type: 'app-edge',
sourceNodeId: props.nodeModel.id,
sourceAnchorId: anchorData.value?.id,
targetNodeId: nodeModel.id
targetNodeId: nodeModel.id,
})
closeNodeMenu()
@ -317,7 +317,7 @@ const nodeFields = computed(() => {
label: field.label,
value: field.value,
globeLabel: `{{${props.nodeModel.properties.stepName}.${field.value}}}`,
globeValue: `{{context['${props.nodeModel.id}'].${field.value}}}`
globeValue: `{{context['${props.nodeModel.id}'].${field.value}}}`,
}
})
return fields
@ -364,4 +364,9 @@ onMounted(() => {
:deep(.el-card) {
overflow: visible;
}
.app-card {
background: #fff;
border-radius: 8px;
box-shadow: 0px 2px 4px 0px rgba(31, 35, 41, 0.12);
}
</style>

View File

@ -11,7 +11,7 @@ import AppEdge from './common/edge'
import Control from './common/NodeControl.vue'
import { baseNodes } from '@/workflow/common/data'
import '@logicflow/extension/lib/style/index.css'
import '@logicflow/core/dist/index.css'
import '@logicflow/core/dist/style/index.css'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { getTeleport } from '@/workflow/common/teleport'
@ -32,11 +32,11 @@ type ShapeItem = {
}
const props = defineProps({
data: Object || null
data: Object || null,
})
const defaultData = {
nodes: [...baseNodes]
nodes: [...baseNodes],
}
const graphData = computed({
get: () => {
@ -48,7 +48,7 @@ const graphData = computed({
},
set: (value) => {
return value
}
},
})
const lf = ref()
@ -67,27 +67,27 @@ const renderGraphData = (data?: any) => {
adjustEdge: false,
adjustEdgeStartAndEnd: false,
background: {
backgroundColor: '#f5f6f7'
backgroundColor: '#f5f6f7',
},
grid: {
size: 10,
type: 'dot',
config: {
color: '#DEE0E3',
thickness: 1
}
thickness: 1,
},
},
keyboard: {
enabled: true
enabled: true,
},
isSilentMode: false,
container: container
container: container,
})
lf.value.setTheme({
bezier: {
stroke: '#afafaf',
strokeWidth: 1
}
strokeWidth: 1,
},
})
lf.value.on('graph:rendered', () => {
flowId.value = lf.value.graphModel.flowId
@ -124,7 +124,7 @@ const onmousedown = (shapeItem: ShapeItem) => {
if (shapeItem.type) {
lf.value.dnd.startDrag({
type: shapeItem.type,
properties: { ...shapeItem.properties }
properties: { ...shapeItem.properties },
})
}
if (shapeItem.callback) {
@ -139,7 +139,7 @@ const addNode = (shapeItem: ShapeItem) => {
type: shapeItem.type,
properties: shapeItem.properties,
x: virtualRectCenterPositionX,
y: virtualRectCenterPositionY - lf.value.graphModel.height / 2
y: virtualRectCenterPositionY - lf.value.graphModel.height / 2,
})
newNode.isSelected = true
newNode.isHovered = true
@ -157,7 +157,7 @@ defineExpose({
addNode,
clearGraphData,
renderGraphData,
render
render,
})
</script>
<style lang="scss">

View File

@ -17,7 +17,7 @@
:rules="{
required: true,
message: $t('views.application.form.aiModel.placeholder'),
trigger: 'change'
trigger: 'change',
}"
>
<template #label>
@ -68,7 +68,7 @@
:rules="{
required: true,
message: $t('views.application.form.prompt.requiredMessage'),
trigger: 'blur'
trigger: 'blur',
}"
>
<template #label>
@ -80,9 +80,7 @@
>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content
>{{ $t('views.application.form.prompt.tooltip') }}
</template>
<template #content>{{ $t('views.application.form.prompt.tooltip') }} </template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
@ -127,9 +125,7 @@
<template #label>
<div class="flex-between w-full">
<div>
<span>{{
$t('views.application.form.reasoningContent.label')
}}</span>
<span>{{ $t('views.application.form.reasoningContent.label') }}</span>
</div>
<el-button
type="primary"
@ -179,6 +175,7 @@ import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import applicationApi from '@/api/application/application'
import modelAPI from '@/api/model/model.ts'
import useStore from '@/stores'
import { isLastNode } from '@/workflow/common/data'
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
@ -213,7 +210,7 @@ const model_change = (model_id?: string) => {
}
}
const {
params: { id }
params: { id },
} = app.config.globalProperties.$route as any
// @ts-ignore
@ -234,8 +231,8 @@ const form = {
model_setting: {
reasoning_content_start: '<think>',
reasoning_content_end: '</think>',
reasoning_content_enable: false
}
reasoning_content_enable: false,
},
}
const chat_data = computed({
@ -245,7 +242,7 @@ const chat_data = computed({
set(props.nodeModel.properties.node_data, 'model_setting', {
reasoning_content_start: '<think>',
reasoning_content_end: '</think>',
reasoning_content_enable: false
reasoning_content_enable: false,
})
}
return props.nodeModel.properties.node_data
@ -257,7 +254,7 @@ const chat_data = computed({
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
},
})
const props = defineProps<{ nodeModel: any }>()
@ -274,7 +271,7 @@ const validate = () => {
function getModel() {
if (id) {
applicationApi.getApplicationModel(id).then((res: any) => {
modelAPI.getLLMModel().then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
})
} else {
@ -302,7 +299,7 @@ function submitReasoningDialog(val: any) {
let model_setting = cloneDeep(props.nodeModel.properties.node_data.model_setting)
model_setting = {
...model_setting,
...val
...val,
}
set(props.nodeModel.properties.node_data, 'model_setting', model_setting)
@ -312,7 +309,7 @@ const mcpServersDialogRef = ref()
function openMcpServersDialog() {
const config = {
mcp_servers: chat_data.value.mcp_servers,
mcp_enable: chat_data.value.mcp_enable
mcp_enable: chat_data.value.mcp_enable,
}
mcpServersDialogRef.value.open(config)
}

View File

@ -15,7 +15,7 @@
:rules="{
message: t('views.application.form.appName.requiredMessage'),
trigger: 'blur',
required: true
required: true,
}"
>
<el-input
@ -86,9 +86,7 @@
<el-form-item>
<template #label>
<div class="flex-between">
<span class="mr-4">{{
$t('views.application.form.voiceInput.label')
}}</span>
<span class="mr-4">{{ $t('views.application.form.voiceInput.label') }}</span>
<div class="flex">
<el-checkbox v-if="form_data.stt_model_enable" v-model="form_data.stt_autosend">{{
$t('views.application.form.voiceInput.autoSend')
@ -115,9 +113,7 @@
<el-form-item>
<template #label>
<div class="flex-between">
<span class="mr-4">{{
$t('views.application.form.voicePlay.label')
}}</span>
<span class="mr-4">{{ $t('views.application.form.voicePlay.label') }}</span>
<div class="flex">
<el-checkbox v-if="form_data.tts_model_enable" v-model="form_data.tts_autoplay">{{
$t('views.application.form.voicePlay.autoPlay')
@ -133,14 +129,8 @@
</template>
<div class="w-full">
<el-radio-group v-model="form_data.tts_type" v-show="form_data.tts_model_enable">
<el-radio
:label="$t('views.application.form.voicePlay.browser')"
value="BROWSER"
/>
<el-radio
:label="$t('views.application.form.voicePlay.tts')"
value="TTS"
/>
<el-radio :label="$t('views.application.form.voicePlay.browser')" value="BROWSER" />
<el-radio :label="$t('views.application.form.voicePlay.tts')" value="TTS" />
</el-radio-group>
</div>
<div class="flex-between w-full">
@ -183,6 +173,7 @@ import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import applicationApi from '@/api/application/application'
import modelAPI from '@/api/model/model.ts'
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message'
import { t } from '@/locales'
import TTSModeParamSettingDialog from '@/views/application/component/TTSModeParamSettingDialog.vue'
@ -191,7 +182,7 @@ import UserInputFieldTable from './component/UserInputFieldTable.vue'
import FileUploadSettingDialog from '@/workflow/nodes/base-node/component/FileUploadSettingDialog.vue'
const {
params: { id }
params: { id },
} = app.config.globalProperties.$route as any
const props = defineProps<{ nodeModel: any }>()
@ -206,7 +197,7 @@ const FileUploadSettingDialogRef = ref<InstanceType<typeof FileUploadSettingDial
const form = {
name: '',
desc: '',
prologue: t('views.application.form.defaultPrologue')
prologue: t('views.application.form.defaultPrologue'),
}
const wheel = (e: any) => {
@ -234,7 +225,7 @@ const form_data = computed({
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
},
})
const baseNodeFormRef = ref<FormInstance>()
@ -247,13 +238,13 @@ const validate = () => {
) {
return Promise.reject({
node: props.nodeModel,
errMessage: t('views.application.form.voicePlay.requiredMessage')
errMessage: t('views.application.form.voicePlay.requiredMessage'),
})
}
if (form_data.value.stt_model_enable && !form_data.value.stt_model_id) {
return Promise.reject({
node: props.nodeModel,
errMessage: t('views.application.form.voiceInput.requiredMessage')
errMessage: t('views.application.form.voiceInput.requiredMessage'),
})
}
return baseNodeFormRef.value?.validate().catch((err) => {
@ -262,13 +253,13 @@ const validate = () => {
}
function getSTTModel() {
applicationApi.getApplicationSTTModel(id).then((res: any) => {
modelAPI.getSTTModel().then((res: any) => {
sttModelOptions.value = groupBy(res?.data, 'provider')
})
}
function getTTSModel() {
applicationApi.getApplicationTTSModel(id).then((res: any) => {
modelAPI.getTTSModel().then((res: any) => {
ttsModelOptions.value = groupBy(res?.data, 'provider')
})
}
@ -316,7 +307,7 @@ const switchFileUpload = () => {
audio: false,
video: false,
other: false,
otherExtensions: ['ppt', 'doc']
otherExtensions: ['ppt', 'doc'],
}
if (form_data.value.file_upload_enable) {