feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { InputForm } from './input-form';

View File

@@ -0,0 +1,30 @@
.input-form {
position: absolute;
bottom: 0;
left: 0;
height: calc(100% - 48px);
width: 100%;
background: var(--coz-bg-max);
}
.form-notice {
display: flex;
justify-content: center;
align-items: center;
column-gap: 8px;
height: 32px;
background: var(--coz-mg-hglt);
>span {
font-size: 14px;
}
// semi bug行内元素导致高度有问题不居中
:global .semi-spin-wrapper{
line-height: 0px;
}
}
.form-content {
display: flex;
flex-direction: column;
height: calc(100% - 32px);
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
import { SchemaForm } from '../schema-form';
import { NodeEventInfo } from '../../../../components';
import { useSync } from './use-sync';
import styles from './input-form.module.less';
interface QuestionFormProps {
spaceId: string;
workflowId: string;
executeId: string;
inputEvent?: NodeEvent;
}
export const InputForm: React.FC<QuestionFormProps> = ({
spaceId,
workflowId,
executeId,
inputEvent,
}) => {
useSync(inputEvent);
if (!inputEvent) {
return null;
}
return (
<div className={styles['input-form']}>
<div className={styles['form-notice']}>
<NodeEventInfo event={inputEvent} />
<span>{I18n.t('workflow_testrun_hangup_input')}</span>
</div>
<div className={styles['form-content']}>
<SchemaForm
spaceId={spaceId}
workflowId={workflowId}
executeId={executeId}
inputEvent={inputEvent}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { useMemoizedFn } from 'ahooks';
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
import { useTestRunService } from '../../../../hooks';
export const useSync = (inputEvent: NodeEvent | undefined) => {
const testRunService = useTestRunService();
const eventSync = useMemoizedFn((event: NodeEvent | undefined) => {
// 结束
if (!event) {
testRunService.continueTestRun();
return;
}
testRunService.pauseTestRun();
});
useEffect(() => {
eventSync(inputEvent);
}, [inputEvent, eventSync]);
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { SchemaForm } from './schema-form';

View File

@@ -0,0 +1,18 @@
.schema-form {
height: 100%;
}
.form-content {
height: calc(100% - 56px);
overflow-y: auto;
padding: 16px;
}
.form-footer {
height: 56px;
padding: 0 16px;
display: flex;
align-items: center;
border-top: 1px solid var(--coz-stroke-primary);
>button {
width: 100%;
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useMemo, useRef } from 'react';
import { type Form } from '@formily/core';
import { workflowApi } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { type NodeEvent } from '@coze-arch/bot-api/workflow_api';
import { Button } from '@coze-arch/coze-design';
import { translateSchema } from '../../utils';
import { typeSafeJSONParse, stringifyValue } from '../../../../utils';
import { useTestRunService, useFormSubmitting } from '../../../../hooks';
import { FormCore } from '../../../../components/form-engine';
import styles from './schema-form.module.less';
interface SchemaFormProps {
spaceId: string;
workflowId: string;
executeId: string;
inputEvent: NodeEvent;
}
export const SchemaForm: React.FC<SchemaFormProps> = ({
spaceId,
workflowId,
executeId,
inputEvent,
}) => {
const formRef = useRef<Form<any>>(null);
const submitting = useFormSubmitting(formRef.current);
const testRunService = useTestRunService();
const schema = useMemo(() => {
const data = (typeSafeJSONParse(inputEvent.data) || {}) as any;
const temp = (typeSafeJSONParse(data.content) || []) as any[];
return translateSchema(temp);
}, [inputEvent]);
const handleSubmit = useCallback(async () => {
if (!formRef.current) {
return;
}
try {
const data = await formRef.current.submit();
const text = JSON.stringify(stringifyValue(data));
await workflowApi.WorkFlowTestResume({
workflow_id: workflowId,
space_id: spaceId,
data: text,
event_id: inputEvent.id || '',
execute_id: executeId,
});
} finally {
testRunService.continueTestRun();
}
}, [spaceId, workflowId, executeId, inputEvent, testRunService]);
return (
<div className={styles['schema-form']}>
<div className={styles['form-content']}>
<FormCore ref={formRef} schema={schema} />
</div>
<div className={styles['form-footer']}>
<Button loading={submitting} onClick={handleSubmit}>
{I18n.t('devops_publish_multibranch_Save')}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ViewVariableType } from '@coze-workflow/base';
export const ACCEPT_MAP = {
[ViewVariableType.Image]: ['image/*'],
[ViewVariableType.Doc]: ['.docx', '.doc', '.pdf'],
[ViewVariableType.Audio]: [
'.mp3',
'.wav',
'.aac',
'.flac',
'.ogg',
'.wma',
'.alac',
'.mid',
'.midi',
'.ac3',
'.dsd',
],
[ViewVariableType.Excel]: ['.xls', '.xlsx', '.csv'],
[ViewVariableType.Video]: ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
[ViewVariableType.Zip]: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
[ViewVariableType.Code]: ['.py', '.java', '.c', '.cpp', '.js', '.css'],
[ViewVariableType.Txt]: ['.txt'],
[ViewVariableType.Ppt]: ['.ppt', '.pptx'],
[ViewVariableType.Svg]: ['.svg'],
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { InputForm } from './components/input-form';

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { variableUtils } from '@coze-workflow/variable';
import { ViewVariableType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { ACCEPT_MAP } from './constants';
export const getAccept = (
inputType: ViewVariableType,
availableFileTypes?: ViewVariableType[],
) => {
let accept: string;
const itemType = ViewVariableType.isArrayType(inputType)
? ViewVariableType.getArraySubType(inputType)
: inputType;
if (itemType === ViewVariableType.File) {
if (availableFileTypes?.length) {
accept = availableFileTypes
.map(type => ACCEPT_MAP[type]?.join(','))
.join(',');
} else {
accept = Object.values(ACCEPT_MAP)
.map(items => items.join(','))
.join(',');
}
} else {
accept = (ACCEPT_MAP[itemType] || []).join(',');
}
return accept;
};
const translateCommonField = (
temp: any,
viewVariableType: ViewVariableType,
) => ({
title: temp.name,
type: 'string',
'x-decorator-props': {
tag: ViewVariableType.LabelMap[viewVariableType],
description: temp.description,
},
'x-decorator': 'FormItem',
required: temp.required,
'x-validator': temp.required
? {
required: true,
message: I18n.t('workflow_testset_required_tip', {
param_name: temp.name,
}),
}
: undefined,
});
const translateFileField = (temp: any, viewVariableType: ViewVariableType) => ({
...translateCommonField(temp, viewVariableType),
type: 'string',
'x-component': 'FileUpload',
'x-component-props': {
multiple: ViewVariableType.isArrayType(viewVariableType),
accept: getAccept(viewVariableType),
'data-testid': `workflow.testrun.form.component.${temp.name}`,
fileType: [ViewVariableType.Image, ViewVariableType.ArrayImage].includes(
viewVariableType,
)
? 'image'
: 'object',
},
});
const translateVoiceField = (
temp: any,
viewVariableType: ViewVariableType,
) => ({
...translateCommonField(temp, viewVariableType),
type: 'string',
'x-component': 'VoiceSelect',
'x-component-props': {
'data-testid': `workflow.testrun.form.component.${temp.name}`,
},
});
const translateBooleanField = (
temp: any,
viewVariableType: ViewVariableType,
) => ({
...translateCommonField(temp, viewVariableType),
type: 'boolean',
'x-component': 'Switch',
'x-component-props': {
'data-testid': `workflow.testrun.form.component.${temp.name}`,
},
default: true,
});
const translateNumberField = (
temp: any,
viewVariableType: ViewVariableType,
) => ({
...translateCommonField(temp, viewVariableType),
type: 'number',
'x-component':
viewVariableType === ViewVariableType.Integer
? 'InputInteger'
: 'InputNumber',
'x-component-props': {
'data-testid': `workflow.testrun.form.component.${temp.name}`,
},
});
const translateField = (temp: any) => {
if (!temp || !temp.type) {
return null;
}
const viewVariableType = variableUtils.DTOTypeToViewType(temp.type, {
assistType: temp.assistType || temp.schema?.assistType,
arrayItemType: temp.schema?.type,
});
if (ViewVariableType.isVoiceType(viewVariableType)) {
return translateVoiceField(temp, viewVariableType);
}
if (ViewVariableType.isFileType(viewVariableType)) {
return translateFileField(temp, viewVariableType);
}
if (viewVariableType === ViewVariableType.Boolean) {
return translateBooleanField(temp, viewVariableType);
}
if (
viewVariableType === ViewVariableType.Number ||
viewVariableType === ViewVariableType.Integer
) {
return translateNumberField(temp, viewVariableType);
}
if (viewVariableType === ViewVariableType.Time) {
return translateTimeField(temp, viewVariableType);
}
return {
title: temp.name,
// 一期固定为 string
type: 'string',
'x-decorator-props': {
tag: temp.type,
description: temp.description,
},
'x-component': 'Input',
'x-decorator': 'FormItem',
required: temp.required,
'x-component-props': {
'data-testid': `workflow.testrun.form.component.${temp.name}`,
},
'x-validator': {
required: true,
message: I18n.t('workflow_testset_required_tip', {
param_name: temp.name,
}),
},
};
};
export const translateSchema = (temp: any[]) => {
const root = {
type: 'object',
properties: temp.reduce((prev, cur) => {
const computed = translateField(cur);
if (cur) {
prev[cur.name] = computed;
}
return prev;
}, {}),
};
return root;
};
const translateTimeField = (temp: any, viewVariableType: ViewVariableType) => ({
...translateCommonField(temp, viewVariableType),
type: 'string',
'x-component': 'InputTime',
'x-component-props': {
'data-testid': `workflow.testrun.form.component.${temp.name}`,
},
});

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** 预置的特殊的 key */
export enum LogObjSpecialKey {
Error = '$error',
Warning = '$warning',
}
/** log 中 value 的显示样式类型 */
export enum LogValueStyleType {
Default,
Number,
Boolean,
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext } from 'react';
import { type DataViewerStore } from './create-store';
export const DataViewerContext = createContext<DataViewerStore | null>(null);

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createWithEqualityFn,
type UseBoundStoreWithEqualityFn,
} from 'zustand/traditional';
import { shallow } from 'zustand/shallow';
import { type StoreApi } from 'zustand';
export interface DataViewerState {
// 折叠展开的状态
expand: Record<string, boolean> | null;
setExpand: (key: string, v: boolean) => void;
}
export type DataViewerStore = UseBoundStoreWithEqualityFn<
StoreApi<DataViewerState>
>;
export const createDataViewerStore = () =>
createWithEqualityFn<DataViewerState>(
set => ({
expand: null,
setExpand: (key: string, v: boolean) => {
set(state => ({
expand: {
...state.expand,
[key]: v,
},
}));
},
}),
shallow,
);

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { DataViewerProvider } from './provider';
export { DataViewerContext } from './context';
export { type DataViewerState } from './create-store';

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useMemo } from 'react';
import { type Field } from '../types';
import { createDataViewerStore } from './create-store';
import { DataViewerContext } from './context';
interface DataViewerProviderProps {
fields: Field[];
}
export const DataViewerProvider: React.FC<
React.PropsWithChildren<DataViewerProviderProps>
> = ({ children, fields }) => {
const store = useMemo(() => createDataViewerStore(), []);
// 根只有一项且其可以下钻时,默认展开它
useEffect(() => {
if (
store.getState().expand === null &&
fields.length === 1 &&
fields[0]?.isObj
) {
store.setState({
[fields[0].path.join('.')]: true,
});
}
}, [fields, store]);
return (
<DataViewerContext.Provider value={store}>
{children}
</DataViewerContext.Provider>
);
};

View File

@@ -0,0 +1,26 @@
.json-viewer-wrapper {
border-radius: 6px;
border: 1px solid var(--coz-stroke-primary);
padding: 2px 6px;
width: 100%;
user-select: text;
/** 高度限制 */
max-height: 272px;
min-height: 24px;
overflow-y: scroll;
&::-webkit-scrollbar {
background: transparent;
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: rgba(29, 28, 35, 0.3);
border-radius: 6px;
&:hover {
background: rgba(29, 28, 35, 0.6);
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo } from 'react';
import { noop } from 'lodash-es';
import cls from 'classnames';
import { generateFields } from './utils/generate-field';
import type { JsonValueType } from './types';
import { JsonField } from './json-field';
import { DataViewerProvider } from './context';
import css from './data-viewer.module.less';
export interface DataViewerProps {
/** 支持对象或者纯文本渲染 */
data: JsonValueType;
mdPreview?: boolean;
className?: string;
onPreview?: (value: string, path: string[]) => void;
emptyPlaceholder?: string;
}
export const DataViewer: React.FC<DataViewerProps> = ({
data,
mdPreview = false,
className,
onPreview = noop,
emptyPlaceholder,
}) => {
const fields = useMemo(() => generateFields(data), [data]);
const isTree = useMemo(() => fields.some(field => field.isObj), [fields]);
const isEmpty = fields.length === 0;
if (isEmpty && emptyPlaceholder) {
return (
<div className="text-xs flex items-center justify-center leading-4 coz-fg-dim">
{emptyPlaceholder}
</div>
);
}
return (
<div
className={cls(css['json-viewer-wrapper'], className)}
style={isTree ? { paddingLeft: '12px' } : {}}
draggable
onDragStart={e => {
e.stopPropagation();
e.preventDefault();
}}
>
<DataViewerProvider fields={fields}>
{fields.map(i => (
<JsonField
field={i}
key={i.path.join('.')}
mdPreview={mdPreview}
onPreview={onPreview}
/>
))}
</DataViewerProvider>
</div>
);
};

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useExpand } from './use-expand';
export { useValue } from './use-value';

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { DataViewerContext, type DataViewerState } from '../context';
export const useDataViewerStore = <T>(selector: (s: DataViewerState) => T) => {
const store = useContext(DataViewerContext);
if (!store) {
throw new Error('cant not found DataViewerContext');
}
return store(selector);
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemoizedFn } from 'ahooks';
import { useDataViewerStore } from './use-data-viewer-store';
export const useExpand = (path: string) => {
const { expand, setExpand } = useDataViewerStore(store => ({
expand: !!store.expand?.[path],
setExpand: store.setExpand,
}));
const toggle = useMemoizedFn(() => {
setExpand(path, !expand);
});
return {
expand,
toggle,
};
};

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { isBoolean, isNil, isNumber, isObject, isString } from 'lodash-es';
import { isBigNumber, bigNumberToString } from '../utils/big-number';
import { type Field } from '../types';
import { LogValueStyleType } from '../constants';
export const useValue = (value: Field['value']) => {
const v = useMemo(() => {
if (isNil(value)) {
return {
value: 'null',
type: LogValueStyleType.Default,
};
} else if (isObject(value)) {
// 大数字返回数字类型,值用字符串
if (isBigNumber(value)) {
return {
value: bigNumberToString(value),
type: LogValueStyleType.Number,
};
}
return {
value: '',
type: LogValueStyleType.Default,
};
} else if (isBoolean(value)) {
return {
value: value.toString(),
type: LogValueStyleType.Boolean,
};
} else if (isString(value)) {
return {
value: JSON.stringify(value),
type: LogValueStyleType.Default,
};
} else if (isNumber(value)) {
return {
value,
type: LogValueStyleType.Number,
};
}
return {
value,
type: LogValueStyleType.Default,
};
}, [value]);
return v;
};

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type { JsonValueType } from './types';
export { DataViewer, DataViewerProps } from './data-viewer';
export { LogObjSpecialKey, LogValueStyleType } from './constants';

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useMemo } from 'react';
import { isString, last } from 'lodash-es';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozArrowRight, IconCozEye } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
import { type Field } from '../types';
import { useExpand, useValue } from '../hooks';
import { LogObjSpecialKey, LogValueStyleType } from '../constants';
import { isPreviewMarkdown } from '../../../utils/markdown';
import { useTestRunReporterService } from '../../../../../hooks';
import styles from './json-field.module.less';
const SPACE_WIDTH = 14;
/* JSON 类型数据渲染 */
const FieldValue: React.FC<{
value: Field['value'];
/** 是否是 markdown 格式 */
isMarkdown?: boolean;
onMarkdownPreview?: () => void;
}> = ({ value, isMarkdown, onMarkdownPreview }) => {
const { value: current, type } = useValue(value);
return (
<span className={styles['field-value']}>
<span
data-testid="json-viewer-field-value"
className={cls({
[styles['field-value-number']]: type === LogValueStyleType.Number,
[styles['field-value-boolean']]: type === LogValueStyleType.Boolean,
})}
>
{current}
</span>
{/* 预览 */}
{isMarkdown ? (
<Button
className={styles['value-button']}
size="mini"
color="primary"
icon={<IconCozEye />}
onClick={onMarkdownPreview}
>
{I18n.t('creat_project_use_template_preview')}
</Button>
) : null}
</span>
);
};
const JsonField: React.FC<{
field: Field;
mdPreview: boolean;
onPreview: (value: string, path: string[]) => void;
}> = ({ field, mdPreview, onPreview }) => {
const reporter = useTestRunReporterService();
const { lines, children, path, isObj } = field;
const echoLines = useMemo(() => lines.slice(1), [lines]);
const pathStr = useMemo(() => path.join('.'), [path]);
const isError = useMemo(() => pathStr === LogObjSpecialKey.Error, [pathStr]);
const isWarning = useMemo(
() => pathStr === LogObjSpecialKey.Warning,
[pathStr],
);
const key = useMemo(() => last(path), [path]);
const keyWithColon = useMemo(() => {
if (isError) {
return I18n.t('workflow_detail_testrun_error_front');
}
if (isWarning) {
return I18n.t('workflow_detail_testrun_warning_front');
}
return key ? `${key} : ` : '';
}, [key, isError, isWarning]);
const isCanRenderMarkdown = useMemo(
() => !isObj && !isError && !isWarning && isPreviewMarkdown(field.value),
[isObj, isError, isWarning, field.value],
);
const isRenderMarkdown = useMemo(
() => mdPreview && isCanRenderMarkdown,
[isCanRenderMarkdown, mdPreview],
);
const { expand, toggle } = useExpand(path.join('.'));
const handleMarkdownPreview = () => {
if (isString(field.value)) {
onPreview(field.value, path);
reporter.logOutputMarkdown({ action_type: 'preview' });
}
};
useEffect(() => {
if (isRenderMarkdown) {
reporter.logOutputMarkdown({ action_type: 'render' });
}
}, [isRenderMarkdown]);
return (
<>
<div className={styles['json-viewer-field']}>
<div
className={styles['field-space']}
style={{ width: `${echoLines.length * SPACE_WIDTH}px` }}
/>
<div
data-testid="json-viewer-field-content"
className={cls('field-content', styles['field-content'], {
[styles['is-error']]: isError,
[styles['is-warning']]: isWarning,
})}
onClick={isObj ? toggle : undefined}
>
{isObj ? (
<>
<span
data-testid="json-viewer-json-field-expander"
className={cls('field-icon', styles['field-icon'], {
[styles.expand]: expand,
})}
>
<IconCozArrowRight />
</span>
<span className={cls('field-key', styles['field-key'])}>
{key}
</span>
<span className={cls('field-len', styles['field-len'])}>
{` {${children.length}}`}
</span>
</>
) : (
<>
{keyWithColon ? (
<span className={cls('field-key', styles['field-key'])}>
{keyWithColon}
</span>
) : null}
<FieldValue
value={field.value}
isMarkdown={isRenderMarkdown}
onMarkdownPreview={handleMarkdownPreview}
/>
</>
)}
</div>
</div>
{expand
? children.map(i => (
<JsonField
mdPreview={mdPreview}
onPreview={onPreview}
field={i}
key={i.path.join('.')}
/>
))
: null}
</>
);
};
export { JsonField };

View File

@@ -0,0 +1,79 @@
.json-viewer-field {
font-size: 12px;
font-weight: 400;
line-height: 16px;
display: flex;
.field-content {
flex: 1 1 0;
width: 0;
padding: 2px 0;
user-select: auto;
position: relative;
&.is-error .field-key,
&.is-error .field-value {
color: #FF441E;
}
&.is-warning .field-key,
&.is-warning .field-value {
color: #FF9600;
}
.field-value-number {
color: #03B6D0;
}
.field-value-boolean {
color: #BB2BC9;
}
}
.field-icon {
font-size: 12px;
color: var(--coz-fg-secondary);
margin-right: 2px;
cursor: pointer;
display: inline-flex;
align-items: center;
position: absolute;
top: 4px;
left: -12px;
&.expand>svg {
transform: rotate(90deg);
}
}
.field-block {
padding-left: 14px;
}
.field-key {
color: var(--coz-fg-hglt);
word-break: break-word;
}
.field-value {
overflow-wrap: anywhere;
white-space: pre-wrap;
line-height: 16px;
}
.field-len {
color: var(--coz-fg-dim);
}
}
.field-space {
display: inline-block;
flex-grow: 0;
flex-shrink: 0;
}
.value-button {
margin-left: 4px;
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*******************************************************************************
* log 相关的类型
*/
/** 线可能存在的几种状态 */
export enum LineStatus {
/** 完全隐藏,最后一个父属性嵌套的子属性同列将不会有线 */
Hidden,
/** 完全显示,仅出现在属性相邻的线 */
Visible,
/** 半显示,非相邻的线 */
Half,
/** 最后属性的相邻线 */
Last,
}
/** JsonViewer 中的 value 可能值 */
export type JsonValueType =
| string
| null
| number
| object
| boolean
| undefined;
export interface Field {
/** 使用数组而不是 'a.b.c' 是因为可能存在 key='a.b' 会产生错误嵌套 */
path: string[];
lines: LineStatus[];
/** 这里 value 可能是任意值,这里是不完全枚举 */
value: JsonValueType;
children: Field[];
/** 是否是可下钻的对象(包含数组) */
isObj: boolean;
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import BigNumber from 'bignumber.js';
/**
* 是不是大数字
* @param value
* @returns
*/
export function isBigNumber(value: unknown): value is BigNumber {
return !!(value && value instanceof BigNumber);
}
/**
* 大数字转字符串
* @param value
* @returns
*/
export function bigNumberToString(value: BigNumber): string {
return value.toFixed();
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isObject } from 'lodash-es';
import { LineStatus, type JsonValueType, type Field } from '../types';
import { isBigNumber } from './big-number';
/**
* 通过父元素的线条状态推到子元素的线条状态
*/
const getLineByParent2Child = (pLine: LineStatus): LineStatus => {
switch (pLine) {
/** 表示父节点也是从父父节点下钻而来,此处的子节点只需要把线延续下去即可 */
case LineStatus.Visible:
return LineStatus.Half;
/** 表示父节点是父父节点的最后一个节点,子节点无需再延续,渲染空白即可 */
case LineStatus.Last:
return LineStatus.Hidden;
/** 其他的情况完全继承父节点的线 */
default:
return pLine;
}
};
/**
* 将 object 解析成可以循环渲染的 fields
* 1. 若 object 非复杂类型,则返回长度为 1 的 fields 只渲染一项
* 2. 若 object = {},则返回长度为 0 的 fields渲染层需要做好兜底
*/
const generateFields = (object: JsonValueType): Field[] => {
/** 若 object 非复杂类型 */
if (!isObject(object) || isBigNumber(object)) {
return [
{
path: [],
lines: [],
value: object,
isObj: false,
children: [],
},
];
}
/** 递归计算时缓存一下计算好的线,没别的意义,降低一些时间复杂度 */
const lineMap = new Map<string[], LineStatus[]>();
/** 递归解析 object 为 fields */
const dfs = ($object: object, $parentPath: string[] = []): Field[] => {
// 如果不是对象,直接返回空数组,兜底异常情况
if (!isObject($object)) {
return [];
}
// 如果是大数字,直接返回空数组
if (isBigNumber($object)) {
return [];
}
const parentLines = lineMap.get($parentPath) || [];
const keys = Object.keys($object);
return keys.map((key, idx) => {
const value = $object[key];
const path = $parentPath.concat(key);
const last = idx === keys.length - 1;
/**
* 根据父节点的线推导子节点的线
*/
const lines = parentLines
.map<LineStatus>(getLineByParent2Child)
/**
* 最后拼接上子节点自己的线,最后一个节点和普通的节点有样式区分
*/
.concat(last ? LineStatus.Last : LineStatus.Visible);
lineMap.set(path, lines);
return {
path,
lines,
value,
children: dfs(value, path),
isObj: isObject(value) && !isBigNumber(value),
};
});
};
return dfs(object);
};
export { generateFields };

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { generateFields } from './generate-field';

View File

@@ -0,0 +1,31 @@
.preview-group {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
&.only-one .image-item {
width: 100%;
}
&.columns-5 {
grid-template-columns: repeat(5, 1fr);
}
}
.image-item {
max-height: 280px;
aspect-ratio: 1 / 1; /* 宽高比为1:1 */
display: flex;
align-items: center;
justify-content: center;
background-color: var(--coz-bg-primary);
img {
width: 100%;
height: 100%;
object-fit: fill;
border-radius: 4px;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react';
import cls from 'classnames';
import { useSize } from 'ahooks';
import { ImagePreview, Image } from '@coze-arch/coze-design';
import css from './images-preview.module.less';
interface ImagesPreviewProps {
images: string[];
}
export const ImagesPreview: React.FC<ImagesPreviewProps> = ({ images }) => {
const onlyOne = images.length === 1;
const ref = useRef(null);
const size = useSize(ref);
return (
<div ref={ref}>
<ImagePreview
className={cls(css['preview-group'], {
[css['only-one']]: onlyOne,
[css['columns-5']]: size?.width && size?.width > 420,
})}
getPopupContainer={() => document.body}
>
{images.map((url, index) => (
<Image
key={`${url}_${index}`}
src={url}
className={css['image-item']}
/>
))}
</ImagePreview>
</div>
);
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { LogImages } from './log-images';

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { IconCozArrowBottom } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
import { LogWrap } from '../log-parser/log-wrap';
import { ImagesPreview } from './images-preview';
interface LogImagesProps {
images: string[];
onDownload?: () => void;
}
export const LogImages: React.FC<LogImagesProps> = ({ images, onDownload }) => {
if (!images || !images.length) {
return null;
}
return (
<LogWrap
labelStyle={{
height: '24px',
}}
label={I18n.t('imageflow_output_display')}
copyable={false}
extra={
<Button
icon={<IconCozArrowBottom />}
color="primary"
type="primary"
onClick={onDownload}
size="small"
>
{I18n.t('imageflow_output_display_save')}
</Button>
}
>
<ImagesPreview images={images} />
</LogWrap>
);
};

View File

@@ -0,0 +1,32 @@
.condition-field {
display: flex;
justify-content: space-around;
column-gap: 4px;
}
.field-value {
width: 151px;
}
.field-operator {
width: 82px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.operator-value {
width: 82px;
border-radius: 8px;
border: 1px solid rgba(29, 28, 35, 0.16);
background: #FFF;
padding: 6px;
text-align: center;
}
}
.logic-data {
font-size: 12px;
margin: 4px;
text-align: center;
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { pick } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { Typography } from '@coze-arch/coze-design';
import { DataViewer } from '../data-viewer';
import {
type ConditionData,
type ConditionLog,
ConditionGroup,
} from '../../types';
import { LogWrap } from './log-wrap';
import css from './condition-log-parser.module.less';
export const ConditionField: React.FC<{ condition: ConditionData }> = ({
condition,
}) => {
const { leftData, rightData, operatorData } = condition;
return (
<div className={css['condition-field']}>
<DataViewer data={leftData} className={css['field-value']} />
<div className={css['field-operator']}>
<Typography.Text size="small" className={css['operator-value']}>
{operatorData}
</Typography.Text>
</div>
<DataViewer data={rightData} className={css['field-value']} />
</div>
);
};
const ConditionGroup: React.FC<{
idx: number;
group: ConditionGroup;
}> = ({ idx, group }) => {
const { name, logic, logicData, conditions } = group;
return (
<LogWrap
label={`${I18n.t('workflow_detail_condition_condition')} ${idx + 1}`}
copyTooltip={I18n.t('workflow_detail_title_testrun_copyinput')}
source={{
name,
logic,
conditions: conditions.map(condition =>
pick(condition, ['left', 'right', 'oprator']),
),
}}
>
{conditions.map((condition, cIdx) => (
<>
<ConditionField condition={condition} />
{cIdx < conditions.length - 1 && (
<div className={css['logic-data']}>{logicData}</div>
)}
</>
))}
</LogWrap>
);
};
export const ConditionLogParser: React.FC<{ log: ConditionLog }> = ({
log,
}) => {
const { conditions } = log;
return (
<>
{conditions.map((group, idx) => (
<ConditionGroup group={group} idx={idx} key={idx} />
))}
</>
);
};

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozCopy } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import { useCopy } from '../../../hooks/use-copy';
export const ContentHeader: FC<
PropsWithChildren<{
source: unknown;
className?: string;
}>
> = ({ children, source, className }) => {
const { handleCopy } = useCopy(source);
return (
<div className={classNames('flex items-center mb-1 h-4', className)}>
<div className="font-medium coz-fg-secondary text-xs leading-4">
{children}
</div>
<Tooltip content={I18n.t('workflow_250310_13')}>
<div className="leading-none">
<IconButton
className="ml-0.5"
wrapperClass="leading-[0px]"
size="mini"
icon={<IconCozCopy className="text-xs coz-fg-secondary" />}
color="secondary"
onClick={handleCopy}
/>
</div>
</Tooltip>
</div>
);
};

View File

@@ -0,0 +1,43 @@
.item {
&:not(:last-child) {
border-bottom: 1px solid var(--coz-stroke-primary);
}
}
.header {
padding: 6px;
}
.empty {
border: 1px solid var(--coz-stroke-primary);
}
.content {
max-height: 272px;
min-height: 28px;
overflow-y: auto;
user-select: text;
width: 100%;
border-top: 1px solid var(--coz-stroke-primary);
}
.json-viewer {
border: none;
padding: 0;
}
.group-name {
@apply coz-fg-primary;
font-size: 12px;
font-weight: 500;
line-height: 16px;
}
.icon {
width: 16px;
height: 16px;
border-radius: 3px;
border: 0.5px solid var(--coz-stroke-plus);
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, type FC } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import {
IconCozArrowDown,
IconCozArrowUp,
} from '@coze-arch/coze-design/icons';
import { DataViewer } from '../../data-viewer';
import { type FunctionCallLogItem } from '../../../types';
import { ContentHeader } from './content-header';
import styles from './function-call-panel.module.less';
export const FunctionCallLogPanel: FC<{ item: FunctionCallLogItem }> = ({
item,
}) => {
const [collapsed, setCollapsed] = useState(true);
return (
<div className={styles.item}>
<div
className={classNames(
'flex items-center justify-between px-[5px] h-7',
styles.header,
{
[styles['header-expanded']]: !collapsed,
},
)}
onClick={() => setCollapsed(!collapsed)}
>
<div className="flex items-center">
<img src={item.icon} className={styles.icon} />
<span className="text-xs leading-4 font-medium coz-fg-primary ml-2">
{item.name}
</span>
</div>
{collapsed ? (
<IconCozArrowDown className="text-sm coz-fg-secondary"></IconCozArrowDown>
) : (
<IconCozArrowUp className="text-sm coz-fg-secondary"></IconCozArrowUp>
)}
</div>
{!collapsed ? (
<div className={classNames('p-[6px]', styles.content)}>
{item.inputs ? (
<>
<ContentHeader source={item.inputs}>
{I18n.t('workflow_250310_11', undefined, '输入')}
</ContentHeader>
<DataViewer
data={item.inputs}
className={styles['json-viewer']}
/>
</>
) : null}
<ContentHeader source={item.outputs} className="mt-1.5">
{I18n.t('workflow_250310_12', undefined, '输出')}
</ContentHeader>
<DataViewer data={item.outputs} className={styles['json-viewer']} />
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,6 @@
.container {
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--coz-stroke-primary);
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import { LogWrap } from '../log-wrap';
import { type FunctionCallLog } from '../../../types';
import { FunctionCallLogPanel } from './function-call-panel';
import styles from './index.module.less';
export const FunctionCallLogParser: FC<{ log: FunctionCallLog }> = ({
log,
}) => {
const { items } = log;
return (
<LogWrap
label={I18n.t('workflow_250310_06', undefined, '技能调用')}
source={log.data}
copyable={false}
>
{items.length ? (
<div className={styles.container}>
{items.map(item => (
<>
<FunctionCallLogPanel item={item} />
</>
))}
</div>
) : (
<div className="border-[1px] border-solid coz-stroke-primary h-7 rounded-[6px]"></div>
)}
</LogWrap>
);
};

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { OutputLogParser } from './output-log-parser';
export { ConditionLogParser } from './condition-log-parser';
export { NormalLogParser } from './normal-log-parser';
export { FunctionCallLogParser } from './function-call-log-parser';
export { WorkflowLinkParser } from './workflow-link-parser';

View File

@@ -0,0 +1,27 @@
.flow-log-detail-label {
display: flex;
align-items: center;
font-size: 14px;
margin-bottom: 4px;
column-gap: 4px;
height: 20px;
margin-left: 2px;
.label-text {
color: var(--coz-fg-plus);
font-weight: 500;
word-break: keep-all;
height: 20px;
line-height: 20px;
}
:global {
.coz-icon-button-mini {
line-height: 0px;
}
}
}
.wrapper {
padding: 8px 6px;
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useCallback, useMemo, useState, type ReactNode } from 'react';
import { isObject, toString, isNil } from 'lodash-es';
import copy from 'copy-to-clipboard';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { IconCozCopy, IconCozCheckMark } from '@coze-arch/coze-design/icons';
import { IconButton, Toast, Tooltip } from '@coze-arch/coze-design';
import { type LogValueType } from '../../types';
import styles from './log-wrap.module.less';
const SPACE = 2;
export const LogWrap: React.FC<
React.PropsWithChildren<{
label: string;
copyable?: boolean;
source?: LogValueType;
copyTooltip?: string;
labelExtra?: ReactNode;
extra?: ReactNode;
labelStyle?: React.CSSProperties;
}>
> = ({
label,
copyable = true,
source,
copyTooltip,
children,
labelExtra,
extra,
labelStyle,
}) => {
const [isSuccess, setSuccess] = useState(false);
const innerCopyable = useMemo(
() => copyable && !isNil(source),
[copyable, source],
);
const handleCopy = useCallback(() => {
try {
const text = isObject(source)
? JSON.stringify(source, undefined, SPACE)
: toString(source);
copy(text);
Toast.success({ content: I18n.t('copy_success'), showClose: false });
setSuccess(true);
setTimeout(() => {
setSuccess(false);
}, 1000);
} catch (e) {
logger.error(e);
Toast.error(I18n.t('copy_failed'));
setSuccess(false);
}
}, [source]);
const renderCopyButton = () =>
isSuccess ? (
<Tooltip content={I18n.t('Duplicate_success')}>
<IconButton
className={'w-[20px] h-[20px] p-[2px]'}
size={'mini'}
color={'secondary'}
icon={<IconCozCheckMark color={'rgba(107, 109, 117, 1)'} />}
/>
</Tooltip>
) : (
<Tooltip content={copyTooltip || I18n.t('Copy')}>
<IconButton
className={'!w-[20px] !h-[20px] !p-[2px] !text-[16px]'}
size={'mini'}
color={'secondary'}
onClick={handleCopy}
icon={<IconCozCopy color={'rgba(107, 109, 117, 1)'} />}
/>
</Tooltip>
);
return (
<div>
<div style={labelStyle} className={styles['flow-log-detail-label']}>
<span className={styles['label-text']}>{label}</span>
{innerCopyable ? renderCopyButton() : null}
{labelExtra}
{extra ? <div className="flex flex-1 justify-end">{extra}</div> : null}
</div>
<div>{children}</div>
</div>
);
};

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DataViewer } from '../data-viewer';
import { type BaseLog } from '../../types';
import { LogWrap } from './log-wrap';
export const NormalLogParser: React.FC<{ log: BaseLog }> = ({ log }) => (
<LogWrap label={log.label} source={log.data} copyTooltip={log.copyTooltip}>
<DataViewer data={log.data} emptyPlaceholder={log.emptyPlaceholder} />
</LogWrap>
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { OutputLogParser } from './output-log-parser';

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isEqual, isObject, isUndefined, omit } from 'lodash-es';
import { type LogValueType } from '../../../types';
const REASONING_CONTENT_NAME = 'reasoning_content';
interface isDifferentOutputArgs {
/** 节点输出 */
nodeOutput: LogValueType;
/** 原始输出 */
rawOutput: LogValueType;
/** 是否是大模型节点 */
isLLM?: boolean;
}
/**
* 通用的节点输出判异
*/
const isDifferentCommonOutput = (args: isDifferentOutputArgs) => {
const { nodeOutput, rawOutput } = args;
/**
* case1: rawOutput === undefined
* 可以无须比较,直接返回假
*/
if (isUndefined(rawOutput)) {
return false;
}
const nodeOutputType = typeof nodeOutput;
const rawOutputType = typeof rawOutput;
/** case2: 两者类型不同 */
if (nodeOutputType !== rawOutputType) {
return true;
}
/** case4: 深度比较 */
return !isEqual(nodeOutput, rawOutput);
};
/**
* 大模型节点特有的判断逻辑
*/
const isDifferentLLMOutput = (args: isDifferentOutputArgs) => {
const { nodeOutput } = args;
/** 如果节点输出是对象,则去除系统字段 */
const readNodeOutput = isObject(nodeOutput)
? omit(nodeOutput, [REASONING_CONTENT_NAME])
: nodeOutput;
const isDiffCommon = isDifferentCommonOutput({
...args,
nodeOutput: readNodeOutput,
});
/** 常规判断已经判同,则直接返回 */
if (!isDiffCommon) {
return isDiffCommon;
}
/** 如果不是节点输出不是对象,直接判异 */
if (!isObject(readNodeOutput)) {
return true;
}
const arr = Object.entries(readNodeOutput);
/** 如果排除系统字段,仍然超过多个字段,则无须进一步比较,直接判异 */
if (arr.length !== 1) {
return true;
}
/** 用唯一的值与节点输出做异同判断 */
return isDifferentCommonOutput({
...args,
nodeOutput: arr[0][1],
});
};
/**
* 精细的判断节点输出和原始输出是否相同
*/
export const isDifferentOutput = (
args: isDifferentOutputArgs,
): [boolean, any] => {
/**
* nodeOutput 可能值:
* 1. undefined
* 2. string
* 3. 包涵一个自定义字段、reasoning_content、LogObjSpecialKey
* 4. 包涵多个自定义字段
* rawOutput 可能值:
* 1. undefined
* 2. string
* 4. 任意对象
* 5. 任意数组
*/
try {
const { isLLM } = args;
const result = isLLM
? isDifferentLLMOutput(args)
: isDifferentCommonOutput(args);
return [result, undefined];
} catch (err) {
/** 该函数会深入解析日志结构,不排除出现异常的可能性,出现异常则判异, */
return [true, err];
}
};

View File

@@ -0,0 +1,24 @@
.json-switch {
display: flex;
align-items: center;
column-gap: 8px;
}
.output-log {
display: flex;
flex-direction: column;
row-gap: 4px;
}
.extra {
display: flex;
align-items: center;
column-gap: 8px;
}
.tab {
display: inline-flex;
align-items: center;
column-gap: 2px;
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isObject } from 'lodash-es';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { StandardNodeType } from '@coze-workflow/base/types';
import { I18n } from '@coze-arch/i18n';
import { NodeExeStatus } from '@coze-arch/bot-api/workflow_api';
import { IconCozWarningCircle } from '@coze-arch/coze-design/icons';
import { SegmentTab, Tag, Typography, Tooltip } from '@coze-arch/coze-design';
import { LogWrap } from '../log-wrap';
import { DataViewer } from '../../data-viewer';
import { type OutputLog } from '../../../types';
import { useOutputLog, TabValue } from './use-output-log';
import { SyncOutputToNode } from './sync-output-to-node';
import css from './output-log-parser.module.less';
const MockInfo: React.FC<{ log: OutputLog }> = ({ log }) => {
const { mockInfo } = log;
if (!mockInfo?.isHit) {
return null;
}
return (
<Tag size="mini" style={{ maxWidth: '100px' }}>
<Typography.Text ellipsis={{ showTooltip: true }} size="small">
{I18n.t('mockset')}:{mockInfo?.mockSetName}
</Typography.Text>
</Tag>
);
};
const LLMTabTooltip = () => (
<Tooltip
content={
<>
<Typography.Text fontSize="14px">
{I18n.t('wf_testrun_log_md_llm_diff_tooltip')}
</Typography.Text>
<Typography.Text
fontSize="14px"
link={{
href: '/open/docs/guides/llm_node#f1e97a47',
target: '_blank',
}}
>
&nbsp;{I18n.t('wf_testrun_log_md_llm_diff_tooltip_a')}
</Typography.Text>
</>
}
>
<IconCozWarningCircle />
</Tooltip>
);
export const OutputLogParser: React.FC<{
log: OutputLog;
node?: FlowNodeEntity;
nodeStatus?: NodeExeStatus;
onPreview?: (value: string, path: string[]) => void;
}> = ({ log, node, nodeStatus, onPreview }) => {
const { showRawOutput, tab, data, options, setTab } = useOutputLog(log);
const isLLM = log.nodeType === 'LLM';
const showCodeSync =
node?.flowNodeType === StandardNodeType.Code &&
nodeStatus === NodeExeStatus.Success &&
isObject(log.rawOutput?.data);
const isFinished =
nodeStatus === NodeExeStatus.Success || nodeStatus === NodeExeStatus.Fail;
return (
<LogWrap
label={log.label}
source={data}
copyTooltip={log.copyTooltip}
labelExtra={<MockInfo log={log} />}
extra={
<div className={css.extra}>
{showCodeSync ? (
<SyncOutputToNode
node={node}
output={log.rawOutput?.data as object}
/>
) : null}
</div>
}
>
<div className={css['output-log']}>
{showRawOutput ? (
<SegmentTab
size="small"
value={tab}
onChange={e => {
setTab(e.target.value);
}}
>
{options.map(i => (
<SegmentTab.Tab value={i.value}>
<span className={css.tab}>
{i.label}
{isLLM && i.value === TabValue.RawOutput ? (
<LLMTabTooltip />
) : null}
</span>
</SegmentTab.Tab>
))}
</SegmentTab>
) : null}
<DataViewer
data={data}
mdPreview={isFinished}
onPreview={onPreview}
className="!min-h-[100px]"
/>
</div>
</LogWrap>
);
};

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { isInteger } from 'lodash-es';
import { ViewVariableType } from '@coze-workflow/base/types';
interface BotsParam {
name: string;
type: ViewVariableType;
children?: Array<BotsParam>;
}
export enum ConvertSchemaErrorCode {
MaxDepthExceeded = 0,
ContainsInvalidValue,
}
export class ConvertSchemaError extends Error {
errorType: ConvertSchemaErrorCode;
constructor(errorType: ConvertSchemaErrorCode, message?: string) {
super(message);
this.errorType = errorType;
}
}
export function convertSchema<T extends Object>(
object: T,
maxDepth = 20,
currentDepth = 1,
): BotsParam[] {
if (currentDepth > maxDepth) {
throw new ConvertSchemaError(
ConvertSchemaErrorCode.MaxDepthExceeded,
'Max depth exceeded',
);
}
const paramSchema: BotsParam[] = [];
// if (object === null) {
// throw new ConvertSchemaError(
// ConvertSchemaErrorCode.ContainsInvalidValue,
// 'ContainsInvalidValue',
// );
// }
Object.keys(object).forEach(key => {
const value: unknown = (object as any)[key];
switch (typeof value) {
case 'string':
paramSchema.push({
name: key,
type: ViewVariableType.String,
});
break;
case 'number':
if (isInteger(value)) {
paramSchema.push({
name: key,
type: ViewVariableType.Integer,
});
} else {
paramSchema.push({
name: key,
type: ViewVariableType.Number,
});
}
break;
case 'boolean':
paramSchema.push({
name: key,
type: ViewVariableType.Boolean,
});
break;
case 'object':
if (value === null) {
// omit null values
break;
}
if (Array.isArray(value)) {
if (value.length > 0) {
switch (typeof value[0]) {
case 'string':
paramSchema.push({
name: key,
type: ViewVariableType.ArrayString,
});
break;
case 'number':
if (isInteger(value[0])) {
paramSchema.push({
name: key,
type: ViewVariableType.ArrayInteger,
});
} else {
paramSchema.push({
name: key,
type: ViewVariableType.ArrayNumber,
});
}
break;
case 'boolean':
paramSchema.push({
name: key,
type: ViewVariableType.ArrayBoolean,
});
break;
case 'object':
paramSchema.push({
name: key,
type: ViewVariableType.ArrayObject,
children: convertSchema(value[0], maxDepth, currentDepth + 1),
});
break;
default:
paramSchema.push({
name: key,
type: ViewVariableType.ArrayString,
});
}
} else {
paramSchema.push({
name: key,
type: ViewVariableType.ArrayString,
});
}
} else {
paramSchema.push({
name: key,
type: ViewVariableType.Object,
children: convertSchema(value, maxDepth, currentDepth + 1),
});
}
break;
default:
console.log('value,to default', value);
throw new ConvertSchemaError(
ConvertSchemaErrorCode.ContainsInvalidValue,
'ContainsInvalidValue',
);
}
});
return paramSchema;
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozUpdate } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { useSyncOutput } from './use-sync-output';
import { convertSchema } from './convert';
export const SyncOutputToNode: FC<{
output: object;
node: FlowNodeEntity;
}> = props => {
const { output, node } = props;
const updateOutput = useSyncOutput('/outputs', node);
const handleUpdateOutput = () => {
const outputSchema = convertSchema(output);
updateOutput(outputSchema);
};
return (
<Button
color="highlight"
size="mini"
icon={<IconCozUpdate />}
onClick={handleUpdateOutput}
>
{I18n.t('workflow_code_testrun_sync')}
</Button>
);
};

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { nanoid } from 'nanoid';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
type ViewVariableTreeNode,
type ViewVariableType,
type ValueExpression,
} from '@coze-workflow/variable';
export type InputParams = Array<{
name: string;
input: ValueExpression;
}>;
export type OutputParams = ViewVariableTreeNode[];
export interface ParsedOutput {
name: string;
type: ViewVariableType;
children?: ParsedOutput[];
}
export interface ParsedOutputWithKey extends ParsedOutput {
key?: string;
}
const turnOutputParamsToMapDeep = (outputParams: OutputParams) => {
const outputParamsMap: Record<string, ViewVariableTreeNode> = {};
const recursiveLoopOutputParams = (output: OutputParams) => {
output.forEach(item => {
outputParamsMap[item.name] = item;
if (item.children) {
recursiveLoopOutputParams(item.children);
}
});
};
recursiveLoopOutputParams(outputParams);
return outputParamsMap;
};
const updateOutputWithNewType = (
outputParams: OutputParams,
parsedOutput: ParsedOutput[],
) => {
const oldOutputMap = turnOutputParamsToMapDeep(outputParams);
const loopParsedOutput = (items: ParsedOutput[]) =>
items.map(item => {
const newItem: ParsedOutputWithKey = { ...item };
if (oldOutputMap[item.name]) {
newItem.key = oldOutputMap[item.name].key;
} else {
newItem.key = nanoid();
}
if (newItem.children) {
newItem.children = loopParsedOutput(newItem.children);
}
return newItem;
});
return loopParsedOutput(parsedOutput);
};
export const useSyncOutput = (outputPath: string, node: FlowNodeEntity) => {
// TODO: 改到 effects 中实现 ,依赖节点引擎支持自定义事件触发 effects
const updateOutput = (output: ParsedOutput[]) => {
if (outputPath) {
const formModel =
node?.getData<FlowNodeFormData>(FlowNodeFormData).formModel;
const outputFormItem = formModel?.getFormItemByPath(outputPath);
if (outputFormItem) {
outputFormItem.value = updateOutputWithNewType(
outputFormItem.value,
output,
);
}
}
};
return updateOutput;
};

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useMemo } from 'react';
import { toString } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { type OutputLog } from '../../../types';
import { useTestRunReporterService } from '../../../../../hooks';
import { isDifferentOutput } from './is-different-output';
const CODE_TEXT = {
tabLabel: I18n.t('workflow_detail_testrun_panel_raw_output_code'),
};
const LLM_TEXT = {
tabLabel: I18n.t('workflow_detail_testrun_panel_raw_output_llm'),
};
const DEFAULT_TEXT = {
tabLabel: I18n.t('workflow_detail_testrun_panel_raw_output'),
};
/** 一些特化节点的文案 */
const TEXT = {
Code: CODE_TEXT,
LLM: LLM_TEXT,
};
export enum TabValue {
Output,
RawOutput,
}
export const useOutputLog = (log: OutputLog) => {
const [tab, setTab] = useState(TabValue.Output);
const reporter = useTestRunReporterService();
/** 是否渲染原始输出 */
const showRawOutput = useMemo(() => {
const [result, err] = isDifferentOutput({
nodeOutput: log.data,
rawOutput: log.rawOutput?.data,
isLLM: log.nodeType === 'LLM',
});
reporter.logRawOutputDifference({
is_difference: result,
error_msg: err ? toString(err) : undefined,
log_node_type: log.nodeType,
});
return result;
}, [log]);
const text = useMemo(() => TEXT[log.nodeType] || DEFAULT_TEXT, [log]);
const options = useMemo(
() => [
{
value: TabValue.Output,
label: I18n.t('workflow_detail_testrun_panel_final_output2'),
},
{
value: TabValue.RawOutput,
label: text.tabLabel,
},
],
[text],
);
const data = useMemo(
() => (tab === TabValue.Output ? log.data : log.rawOutput?.data),
[tab, log],
);
return {
showRawOutput,
options,
text,
tab,
data,
setTab,
};
};

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { Typography } from '@coze-arch/coze-design';
import { type WorkflowLinkLogData } from '@/types';
import { type WorkflowLinkLog } from '../../types';
export const WorkflowLinkParser: React.FC<{
log: WorkflowLinkLog;
onOpenWorkflowLink?: (data: WorkflowLinkLogData) => any;
}> = ({ log, onOpenWorkflowLink }) => (
<div className="flex items-center">
<span className="mr-[16px] text-[14px] coz-fg-plus font-medium">
{log.label}
</span>
<Typography.Text
size="small"
link
onClick={() => onOpenWorkflowLink?.(log.data)}
>
{I18n.t('View')}
</Typography.Text>
</div>
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { useMarkdownModal } from './use-markdown-modal';

View File

@@ -0,0 +1,3 @@
.markdown-modal {
min-height: 520px;
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Modal } from '@coze-arch/coze-design';
import { MarkdownBoxViewer } from './markdown-viewer';
import css from './markdown-modal.module.less';
interface MarkdownModalProps {
visible?: boolean;
value: string;
onClose: () => void;
}
export const MarkdownModal: React.FC<MarkdownModalProps> = ({
visible,
value,
onClose,
}) => (
<Modal
visible={visible}
title={I18n.t('creat_project_use_template_preview')}
size="large"
getPopupContainer={() => document.body}
onCancel={onClose}
>
<MarkdownBoxViewer value={value} className={css['markdown-modal']} />
</Modal>
);

View File

@@ -0,0 +1,29 @@
.md-box-viewer :global(.flow-markdown-body ) {
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
color: var(--coz-fg-primary);
}
h1 {
font-size: 22px;
}
h2 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5, h6 {
font-size: 14px;
}
:global(.paragraph-element) {
color: var(--coz-fg-primary);
}
blockquote {
h1, h2, h3, h4, h5, h6, :global(.paragraph-element) {
color: var(--coz-fg-secondary);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import cls from 'classnames';
import { MdBoxLazy } from '@coze-arch/bot-md-box-adapter/lazy';
import css from './markdown-viewer.module.less';
interface MarkdownViewerProps {
value: string;
className?: string;
}
export const MarkdownBoxViewer: React.FC<MarkdownViewerProps> = ({
value,
className,
}) => (
<div className={cls(css['md-box-viewer'], className)}>
<MdBoxLazy markDown={value} />
</div>
);

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo, useState } from 'react';
import { MarkdownModal } from './markdown-modal';
export const useMarkdownModal = () => {
const [visible, setVisible] = useState(false);
const [value, setValue] = useState('');
const open = (nextValue: string) => {
setValue(nextValue);
setVisible(true);
};
const close = () => {
setVisible(false);
setValue('');
};
const modal = useMemo(
() =>
value ? (
<MarkdownModal visible={visible} value={value} onClose={close} />
) : null,
[visible, value],
);
return {
open,
modal,
};
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { NodeStatusBar } from './node-status-bar';

View File

@@ -0,0 +1,52 @@
.node-status-bar {
border: 1px solid var(--coz-stroke-plus);
border-radius: 8px;
background-color: var(--coz-bg-max);
position: absolute;
top: calc(100% + 8px);
left: 0;
width: 100%;
}
.status-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px;
&-opened {
padding-bottom: 0;
}
.status-title {
display: flex;
align-items: center;
column-gap: 8px;
min-width: 0;
:global {
.coz-tag {
height: 20px;
}
.semi-tag-content {
font-weight: 500;
line-height: 16px;
font-size: 12px;
}
.semi-tag-suffix-icon >div{
font-size: 14px;
}
}
}
.status-btns {
display: flex;
align-items: center;
column-gap: 4px;
}
.is-show-detail {
transform: rotate(180deg)
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozArrowDown } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
import { useNodeRender } from '@flowgram-adapter/free-layout-editor';
import styles from './node-status-bar.module.less';
interface NodeStatusBarProps {
header?: React.ReactNode;
defaultShowDetail?: boolean;
hasExecuteResult?: boolean;
needAuth?: boolean;
/**
* 是否包含会话处理
*/
hasConversation?: boolean;
onAuth?: () => void;
onJumpToProjectConversation?: () => void;
extraBtns?: React.ReactNode[];
}
export const NodeStatusBar: React.FC<
React.PropsWithChildren<NodeStatusBarProps>
> = ({
header,
defaultShowDetail,
hasExecuteResult,
needAuth,
onAuth,
hasConversation,
onJumpToProjectConversation,
children,
extraBtns = [],
}) => {
const [showDetail, setShowDetail] = useState(defaultShowDetail);
const { selectNode } = useNodeRender();
const handleAuth = e => {
e.stopPropagation();
selectNode(e);
onAuth?.();
};
const handleToggleShowDetail = e => {
e.stopPropagation();
selectNode(e);
setShowDetail(!showDetail);
};
const handleConversation = e => {
e.stopPropagation();
selectNode(e);
onJumpToProjectConversation?.();
};
return (
<div
className={styles['node-status-bar']}
// 必须要禁止 down 冒泡,防止判定圈选和 node hover不支持多边形
onMouseDown={e => e.stopPropagation()}
// 其他事件统一走点击事件,且也需要阻止冒泡
onClick={handleToggleShowDetail}
>
<div
className={classNames(styles['status-header'], {
[styles['status-header-opened']]: showDetail,
})}
>
<div className={styles['status-title']}>
{header}
{extraBtns.length > 0 ? extraBtns : null}
{needAuth ? (
<Button size="small" color="secondary" onClick={handleAuth}>
{I18n.t('knowledge_feishu_10')}
</Button>
) : null}
{hasConversation ? (
<Button size="small" color="secondary" onClick={handleConversation}>
{I18n.t('workflow_view_data')}
</Button>
) : null}
</div>
<div className={styles['status-btns']}>
{hasExecuteResult ? (
<IconCozArrowDown
className={classNames({
[styles['is-show-detail']]: showDetail,
})}
/>
) : null}
</div>
</div>
{showDetail ? children : null}
</div>
);
};

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { merge } from 'lodash-es';
import { ConditionType } from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
export const conditionRelationField = 'relation';
export const referenceNameField = 'param';
export const conditionField = 'conditionType';
export const compareValueField = 'value';
export const isQuotedField = 'isQuote';
export const quotedField = 'quotedValue';
export const fixedField = 'fixedValue';
export const compareIsQuotedField = `${compareValueField}.${isQuotedField}`;
export const compareQuotedField = `${compareValueField}.${quotedField}`;
export const compareFixedField = `${compareValueField}.${fixedField}`;
export const equalValue = {
[ConditionType.Equal]: () => I18n.t('workflow_detail_condition_select_equal'),
};
export const notEqualValue = {
[ConditionType.NotEqual]: () =>
I18n.t('workflow_detail_condition_select_not_equal'),
};
export const lengthBiggerValue = {
[ConditionType.LengthGt]: () =>
I18n.t('workflow_detail_condition_select_longer'),
};
export const lengthBiggerEqualValue = {
[ConditionType.LengthGtEqual]: () =>
I18n.t('workflow_detail_condition_select_longer_equal'),
};
export const lengthSmallerValue = {
[ConditionType.LengthLt]: () =>
I18n.t('workflow_detail_condition_select_shorter'),
};
export const lengthSmallerEqualValue = {
[ConditionType.LengthLtEqual]: () =>
I18n.t('workflow_detail_condition_select_shorter_equal'),
};
export const includeValue = {
[ConditionType.Contains]: () =>
I18n.t('workflow_detail_condition_select_contain'),
};
export const excludeValue = {
[ConditionType.NotContains]: () =>
I18n.t('workflow_detail_condition_select_not_contain'),
};
export const emptyValue = {
[ConditionType.Null]: () => I18n.t('workflow_detail_condition_select_empty'),
};
export const notEmptyValue = {
[ConditionType.NotNull]: () =>
I18n.t('workflow_detail_condition_select_not_empty'),
};
export const biggerValue = {
[ConditionType.Gt]: () => I18n.t('workflow_detail_condition_select_greater'),
};
export const biggerEqualValue = {
[ConditionType.GtEqual]: () =>
I18n.t('workflow_detail_condition_select_greater_equal'),
};
export const smallerValue = {
[ConditionType.Lt]: () => I18n.t('workflow_detail_condition_select_less'),
};
export const smallerEqualValue = {
[ConditionType.LtEqual]: () =>
I18n.t('workflow_detail_condition_select_less_equal'),
};
export const trueValue = {
[ConditionType.True]: () => I18n.t('workflow_detail_condition_select_true'),
};
export const falseValue = {
[ConditionType.False]: () => I18n.t('workflow_detail_condition_select_false'),
};
// 等于、不等于、长度大于、长度大于等于、长度小于、长度小于等于、包含、不包含、为空、不为空
export const stringConditionValueMap = merge(
{},
equalValue,
notEqualValue,
lengthBiggerValue,
lengthBiggerEqualValue,
lengthSmallerValue,
lengthSmallerEqualValue,
includeValue,
excludeValue,
emptyValue,
notEmptyValue,
);
// 等于、不等于、大于、大于等于、小于、小于等于、为空、不为空
export const intConditionValueMap = merge(
{},
equalValue,
notEqualValue,
emptyValue,
notEmptyValue,
biggerValue,
biggerEqualValue,
smallerValue,
smallerEqualValue,
);
// 等于、不等于、为True、为False、为空、不为空
export const booleanConditionValueMap = merge(
{},
equalValue,
notEqualValue,
emptyValue,
notEmptyValue,
trueValue,
falseValue,
);
// 等于、不等于、大于等于、小于等于、大于、小于、为空、不为空
export const numberConditionValueMap = merge(
{},
equalValue,
notEqualValue,
biggerValue,
biggerEqualValue,
smallerValue,
smallerEqualValue,
emptyValue,
notEmptyValue,
);
// 包含、不包含、为空、不为空
export const objectConditionValueMap = merge(
{},
includeValue,
excludeValue,
emptyValue,
notEmptyValue,
);
// 长度大于、长度大于等于、长度小于、长度小于等于、包含、不包含、为空、不为空
export const arrayConditionValueMap = merge(
{},
lengthBiggerValue,
lengthBiggerEqualValue,
lengthSmallerValue,
lengthSmallerEqualValue,
includeValue,
excludeValue,
emptyValue,
notEmptyValue,
);
// 所有的值的集合
export const totalConditionValueMap = merge(
{},
equalValue,
notEqualValue,
lengthBiggerValue,
lengthBiggerEqualValue,
lengthSmallerValue,
lengthSmallerEqualValue,
includeValue,
excludeValue,
emptyValue,
notEmptyValue,
biggerValue,
biggerEqualValue,
smallerValue,
smallerEqualValue,
trueValue,
falseValue,
);
export const fileConditionValueMap = merge({}, notEmptyValue, emptyValue);
export enum ConditionRightType {
Ref = 'ref',
Literal = 'literal',
}
export enum Logic {
OR = 1,
AND = 2,
}
export const logicTextMap = new Map<number, string>([
[Logic.OR, I18n.t('workflow_detail_condition_or')],
[Logic.AND, I18n.t('workflow_detail_condition_and')],
]);

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
totalConditionValueMap,
ConditionRightType,
logicTextMap,
} from './condition';
/** 日志类型 */
export enum LogType {
/** 输入 */
Input,
/** 输出 */
Output,
/** 批处理数据 */
Batch,
/** Condition */
Condition,
/** 大模型推理过程 */
Reasoning,
/** 大模型Function过程 */
FunctionCall,
/** 子流程跳转连接 */
WorkflowLink,
}
export enum EndTerminalPlan {
Variable = 1,
Text = 2,
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback } from 'react';
import { isObject, toString } from 'lodash-es';
import copy from 'copy-to-clipboard';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
const SPACE = 2;
export function useCopy(source: unknown) {
const handleCopy = useCallback(() => {
try {
const text = isObject(source)
? JSON.stringify(source, undefined, SPACE)
: toString(source);
copy(text);
Toast.success({ content: I18n.t('copy_success'), showClose: false });
} catch (e) {
logger.error(e);
Toast.error(I18n.t('copy_failed'));
}
}, [source]);
return {
handleCopy,
};
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { NodeStatusBar } from './components/node-status-bar';
export { LogImages } from './components/log-images';
export { DataViewer } from './components/data-viewer';
export { useMarkdownModal } from './components/markdown-viewer';
export {
ConditionLogParser,
OutputLogParser,
NormalLogParser,
FunctionCallLogParser,
WorkflowLinkParser,
} from './components/log-parser';
export { LogType } from './constants';
export { generateLog } from './utils/generate-log';
export {
isConditionLog,
isOutputLog,
isReasoningLog,
isFunctionCallLog,
isWorkflowLinkLog,
} from './utils/field';
export { Log, ConditionLog } from './types';

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WorkflowLinkLogData } from '@/types';
import { type LogType } from './constants';
/** log 中的 value 可能值 */
export type LogValueType =
| string
| null
| number
| object
| boolean
| undefined;
/** 通常的日志结构 */
export interface BaseLog {
label: string;
data: LogValueType;
copyTooltip?: string;
type: LogType;
emptyPlaceholder?: string;
}
/**
* condition 的值
*/
export interface ConditionData {
leftData: LogValueType;
rightData: LogValueType;
operatorData: string;
}
export interface ConditionGroup {
conditions: ConditionData[];
name: string;
logic: number;
logicData: string;
}
/** condition 的日志结构 */
export interface ConditionLog {
conditions: ConditionGroup[];
type: LogType.Condition;
}
export interface FunctionCallLogItem {
name: string;
inputs?: Record<string, unknown>;
outputs: string | Record<string, unknown>;
icon: string;
}
export interface FunctionCallLog {
type: LogType.FunctionCall;
items: FunctionCallLogItem[];
copyTooltip?: string;
data: LogValueType;
}
/**
* 输出的日志结构
*/
export interface OutputLog {
label: string;
data: LogValueType;
copyTooltip?: string;
type: LogType.Output;
/** 节点类型 */
nodeType: string;
mockInfo?: {
isHit: boolean;
mockSetName?: string;
};
rawOutput?: {
data: LogValueType;
};
}
export interface WorkflowLinkLog {
type: LogType.WorkflowLink;
label: string;
data: WorkflowLinkLogData;
}
export type Log =
| BaseLog
| ConditionLog
| OutputLog
| FunctionCallLog
| WorkflowLinkLog;

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type Log,
type ConditionLog,
type OutputLog,
type BaseLog,
type FunctionCallLog,
type WorkflowLinkLog,
} from '../types';
import { LogType } from '../constants';
/** 是否是输出日志 */
export const isOutputLog = (log: Log): log is OutputLog =>
log.type === LogType.Output;
/** 是否是 condition 输入 */
export const isConditionLog = (log: Log): log is ConditionLog =>
log.type === LogType.Condition;
/** 是否是大模型推理日志 */
export const isReasoningLog = (log: Log): log is BaseLog =>
log.type === LogType.Reasoning;
export const isFunctionCallLog = (log: Log): log is FunctionCallLog =>
log.type === LogType.FunctionCall;
/** 是否是子流程跳转连接 */
export const isWorkflowLinkLog = (log: Log): log is WorkflowLinkLog =>
log.type === LogType.WorkflowLink;

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { I18n } from '@coze-arch/i18n';
import { type FunctionCallLog, type BaseLog, type Log } from '../types';
import { LogType } from '../constants';
import {
type FunctionCallDetail,
parseFunctionCall,
} from './parse-function-call';
export function generateLLMOutput(
logs: Log[],
responseExtra: Record<string, unknown>,
node?: FlowNodeEntity,
) {
const {
reasoning_content: reasoningContent,
fc_called_detail: fcCalledDetail,
} = responseExtra;
if (reasoningContent) {
const reasoningLog: BaseLog = {
type: LogType.Reasoning,
label: I18n.t('workflow_250217_01'),
data: reasoningContent,
copyTooltip: I18n.t('workflow_detail_title_testrun_copyoutput'),
};
logs.push(reasoningLog);
}
if (fcCalledDetail) {
const reasoningLog: FunctionCallLog = parseFunctionCall(
fcCalledDetail as FunctionCallDetail,
node,
);
logs.push(reasoningLog);
}
}

View File

@@ -0,0 +1,290 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
import { isFunction, isString, isObject } from 'lodash-es';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowNodeData } from '@coze-workflow/nodes';
import type { NodeResult } from '@coze-workflow/base/api';
import { type StandardNodeType } from '@coze-workflow/base';
import { LogObjSpecialKey } from '@coze-common/json-viewer';
import { I18n } from '@coze-arch/i18n';
import { MockHitStatus } from '@coze-arch/bot-api/debugger_api';
import { type Log, type LogValueType, type OutputLog } from '../types';
import {
totalConditionValueMap,
ConditionRightType,
logicTextMap,
EndTerminalPlan,
LogType,
} from '../constants';
import { typeSafeJSONParse } from '../../../utils';
import { generateLLMOutput } from './generate-llm-output';
const enableBigInt = true;
const hasStringOutput = (rawData: unknown) =>
isString(rawData) && rawData.length > 0;
const normalizeConditionInputData = (inputData: any) => {
try {
const { branches, conditions, logic } = inputData;
if (branches) {
return branches;
}
if (conditions) {
return [
{
conditions,
logic,
},
];
}
return [];
} catch (_e) {
return [];
}
};
const generateBatchData = (result: NodeResult): Log => {
const batchData =
typeSafeJSONParse(result.items, {
enableBigInt,
}) || {};
return {
label: I18n.t('workflow_detail_testrun_panel_batch_value'),
data: batchData,
copyTooltip: I18n.t('workflow_detail_title_testrun_copy_batch'),
type: LogType.Batch,
};
};
const generateInput = (logs: Log[], result: NodeResult) => {
const { NodeType: type } = result;
/** input 不可能是 string所以使用 {} 兜底,异常情况直接展示空即可 */
const inputData = (typeSafeJSONParse(result.input, {
emptyValue: result.input,
enableBigInt,
}) || {}) as LogValueType;
/** step 1.1: condition 节点单独处理 */
if (type === 'If') {
const normalizeConditions = normalizeConditionInputData(inputData);
const conditions = normalizeConditions.map(branch => ({
conditions: branch.conditions.map(condition => {
/** 右值不一定存在 */
const { left, right = {}, operator } = condition;
const operatorFn = totalConditionValueMap[operator];
/** 后端的 operator 枚举值通过 i18n 转化为文本 */
const operatorData = isFunction(operatorFn) ? operatorFn() : operator;
const leftData = left?.key ? { [left?.key]: left?.value } : left?.value;
const rightData =
right?.type === ConditionRightType.Ref && right?.key
? { [right?.key]: right?.value }
: right?.value;
return {
...condition,
operatorData,
leftData,
rightData,
};
}),
logic: branch.logic,
logicData: logicTextMap.get(branch.logic),
name: branch.name,
}));
logs.push({ conditions, type: LogType.Condition });
} else {
/** end、Message 节点的 label 不同,输入即输出 */
const isOutputNode = type === 'End' || type === 'Message';
const label = isOutputNode
? I18n.t('workflow_detail_end_output')
: I18n.t('workflow_detail_node_input');
const copyTooltip = isOutputNode
? I18n.t('workflow_detail_end_output_copy')
: I18n.t('workflow_detail_title_testrun_copyinput');
logs.push({
label,
data: inputData,
copyTooltip,
type: isOutputNode ? LogType.Output : LogType.Input,
emptyPlaceholder: I18n.t(
'workflow_testrun_input_form_empty',
undefined,
'本次试运行无需输入',
),
});
}
};
const generateOutput = (
logs: Log[],
result: NodeResult,
node?: FlowNodeEntity,
) => {
const { NodeType: type, errorInfo, errorLevel, extra } = result;
const responseExtra = (typeSafeJSONParse(extra) as any)?.response_extra || {};
const { mock_hit_status: mockHitInfo } = responseExtra;
const { hitStatus: mockHitStatus, mockSetName } =
(typeSafeJSONParse(mockHitInfo) as any) || {};
/**
* case1: output 解析成功,可能是对象或者字符串
* case2: output 解析失败,可能是字符串
* case3: output 为空值,兜底展示空对象
*/
let outputData =
typeSafeJSONParse(result.output, { enableBigInt }) || result.output || {};
/** step 2.1: 处理 rawOutput */
/**
* case1: output 解析成功,可能是对象或者字符串
* case2: output 解析失败,可能是字符串,由于是原始输出空值也视为有意义
*/
const rawData =
typeSafeJSONParse(result.raw_output, { enableBigInt }) || result.raw_output;
/** Code、Llm 节点需要展示 raw */
const textHasRawout = type === 'Text' && hasStringOutput(rawData);
const hasRawOutput =
(type && ['Code', 'LLM', 'Question'].includes(type)) || textHasRawout;
/** step 2.2: 处理 errorInfo */
if (errorInfo) {
const errorData = {
[errorLevel === 'Error'
? LogObjSpecialKey.Error
: LogObjSpecialKey.Warning]: errorInfo,
};
/**
* 错误放到 output 中展示output 的展示优先级最高
* 若 output 为对象则直接 assign error
* 否则 output 需要被赋值到 output 字段并和 error 组成一个对象
*/
outputData = isObject(outputData)
? {
...outputData,
...errorData,
}
: { output: outputData, ...errorData };
}
const finalOutputLog: OutputLog = {
label: I18n.t('workflow_detail_node_output'),
data: outputData,
copyTooltip: I18n.t('workflow_detail_title_testrun_copyoutput'),
nodeType: type || '',
mockInfo: {
isHit: mockHitStatus === MockHitStatus.Success,
mockSetName,
},
rawOutput: hasRawOutput
? {
data: rawData,
}
: undefined,
type: LogType.Output,
};
if (type === 'LLM') {
generateLLMOutput(logs, responseExtra, node);
}
if (type === 'End') {
const isReturnText =
(typeSafeJSONParse((result as unknown as any).extra, {}) as any)
?.response_extra?.terminal_plan === EndTerminalPlan.Text;
if (isReturnText || errorInfo) {
logs.push({
...finalOutputLog,
label: I18n.t('workflow_detail_end_answer'),
copyTooltip: I18n.t('workflow_detail_end_answer_copy'),
});
}
} else if (type === 'Message') {
logs.push({
...finalOutputLog,
label: I18n.t('workflow_detail_end_answer'),
copyTooltip: I18n.t('workflow_detail_end_answer_copy'),
});
} else if (type !== 'Start') {
logs.push(finalOutputLog);
}
};
function getSubWorkflowId(node?: FlowNodeEntity) {
const nodeData = node?.getData<WorkflowNodeData>(WorkflowNodeData);
if (!nodeData) {
return '';
}
return nodeData.getNodeData<StandardNodeType.SubWorkflow>()?.workflow_id;
}
function generateExternalLink(
logs: Log[],
result: NodeResult,
node?: FlowNodeEntity,
) {
const subWorkflowId = node ? getSubWorkflowId(node) : '';
const { NodeType: type, executeId, subExecuteId } = result;
if (type !== 'SubWorkflow') {
return;
}
logs.push({
label: I18n.t('workflow_subwf_jump_detail'),
type: LogType.WorkflowLink,
data: {
workflowId: subWorkflowId,
executeId,
subExecuteId,
},
});
}
export const generateLog = (
result: NodeResult | null | undefined,
node?: FlowNodeEntity,
): {
logs: Log[];
} => {
if (!result) {
return { logs: [] };
}
const { isBatch } = result;
const logs: Log[] = [];
/** step 0: 处理 batch data */
if (isBatch) {
logs.push(generateBatchData(result));
}
/** step 1: 处理 input */
generateInput(logs, result);
/** step 2: 处理 output */
generateOutput(logs, result, node);
/** step 3: 对于子工作流节点,生成额外的跳转链接 */
generateExternalLink(logs, result, node);
return {
logs,
};
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { isOutputLog } from './field';

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import { isString } from 'lodash-es';
/**
* 是否符合渲染为 markdown
* 1. ast > 1 或
* 2. ast = 1 且类型不为普通段落
* 2. ast = 1 且类型为普通段落,但段落中超过两个或者仅有一项但不为 text
*/
export const isPreviewMarkdown = (str: unknown) => {
if (!isString(str)) {
return false;
}
const tree = unified().use(remarkParse).parse(str);
if (tree.children.length > 1) {
return true;
}
if (tree.children.length === 1) {
const [child] = tree.children;
if (child.type !== 'paragraph') {
return true;
} else if (
child.children.length > 1 ||
child.children?.[0]?.type !== 'text'
) {
return true;
}
}
return false;
};

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get, isString, omit } from 'lodash-es';
import JSONBig from 'json-bigint';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
WorkflowNodeData,
LLMNodeDataSkillType,
type LLMNodeDataSkill,
type LLMNodeDataDatasetSkill,
type LLMNodeDataPluginSkill,
type LLMNodeDataWorkflowSkill,
} from '@coze-workflow/nodes';
import { type StandardNodeType } from '@coze-workflow/base/types';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { type FunctionCallLog, type FunctionCallLogItem } from '../types';
import { LogType } from '../constants';
interface FunctionCallDetailItem {
input: string;
output: string;
}
interface FunctionCallDetailInput {
name?: string;
arguments?: Record<string, unknown>;
plugin_id?: number;
plugin_name?: string;
api_id?: number;
api_name?: string;
plugin_type?: number;
}
interface FunctionCallKnowledgeOutput {
chunks: {
slice: string;
score: number;
meta: {
dataset: {
id: string;
name: string;
};
};
}[];
}
interface FunctionCallDetailOutput {
[key: string]: unknown;
}
export interface FunctionCallDetail {
fc_called_list: FunctionCallDetailItem[];
}
function getNodeSkills(node?: FlowNodeEntity): LLMNodeDataSkill[] {
const nodeData = node?.getData(WorkflowNodeData);
if (!nodeData) {
return [];
}
return nodeData.getNodeData<StandardNodeType.LLM>()?.skills || [];
}
function getPluginLogItem(
input: FunctionCallDetailInput,
output: FunctionCallDetailOutput | string,
skills: LLMNodeDataSkill[],
): FunctionCallLogItem {
const nodeSkill = skills.find(
skill =>
skill.type === LLMNodeDataSkillType.Plugin &&
`${skill.apiId}` === `${input.api_id}`,
) as LLMNodeDataPluginSkill;
const pluginName = nodeSkill?.pluginName || input.plugin_name || '';
const apiName = input.api_name;
const logItem: FunctionCallLogItem = {
name: `${pluginName} - ${apiName}`,
inputs: input.arguments || {},
outputs: output || {},
icon: nodeSkill?.icon || '',
};
return logItem;
}
function getWorkflowLogItem(
input: FunctionCallDetailInput,
output: FunctionCallDetailOutput | string,
skills: LLMNodeDataSkill[],
): FunctionCallLogItem {
const nodeSkill = skills.find(
skill =>
skill.type === LLMNodeDataSkillType.Workflow &&
`${skill.pluginId}` === `${input.plugin_id}`,
) as LLMNodeDataWorkflowSkill;
const logItem: FunctionCallLogItem = {
name: nodeSkill?.name || '',
inputs: input?.arguments || {},
outputs: isString(output)
? output
: (output as Record<string, unknown>) || {},
icon: nodeSkill?.icon || '',
};
return logItem;
}
function getDatasetLogItem(
output: FunctionCallKnowledgeOutput,
skills: LLMNodeDataSkill[],
): FunctionCallLogItem | null {
const chunk = get(output, 'chunks[0]');
if (!chunk) {
return null;
}
const oriReq = get(output, 'ori_req');
let inputs;
if (oriReq) {
try {
inputs = JSON.parse(oriReq);
} catch (error) {
logger.error(error);
}
}
const id = get(chunk, 'meta.dataset.id');
const nodeSkill = skills.find(
skill =>
skill.type === LLMNodeDataSkillType.Dataset && `${skill.id}` === `${id}`,
) as LLMNodeDataDatasetSkill;
const name = nodeSkill?.name || get(chunk, 'dataset.name') || '';
const logItem: FunctionCallLogItem = {
name,
outputs: omit(chunk, 'meta'),
inputs,
icon: nodeSkill?.icon || '',
};
return logItem;
}
function parseOutput(
jsonBig: JSONBig,
output: string,
): FunctionCallDetailOutput | string {
try {
return jsonBig.parse(output) as FunctionCallDetailOutput;
// eslint-disable-next-line @coze-arch/no-empty-catch, @coze-arch/use-error-in-catch, no-empty
} catch (e) {}
return output;
}
export function parseFunctionCall(
fcCalledDetail: FunctionCallDetail,
node?: FlowNodeEntity,
): FunctionCallLog {
let items: FunctionCallLogItem[] = [];
const jsonBig = JSONBig({ storeAsString: true });
items = (fcCalledDetail?.fc_called_list || [])
.map(detailItem => {
try {
const input = detailItem.input
? (jsonBig.parse(detailItem.input) as FunctionCallDetailInput)
: {};
const output = detailItem.output
? (parseOutput(jsonBig, detailItem.output) as
| FunctionCallDetailOutput
| string)
: {};
const type = get(output, 'msg_type');
const skills = getNodeSkills(node);
// 知识库类型
if (type === 'knowledge_recall') {
const data: FunctionCallKnowledgeOutput = JSON.parse(
get(output, 'data', '{}') as string,
);
return getDatasetLogItem(data, skills);
}
// 插件类型
if (input?.plugin_type === LLMNodeDataSkillType.Plugin) {
return getPluginLogItem(input, output, skills);
}
// Workflow类型
if (input?.plugin_type === LLMNodeDataSkillType.Workflow) {
return getWorkflowLogItem(input, output, skills);
}
} catch (e) {
logger.error(e);
}
return null;
})
.filter(Boolean) as FunctionCallLogItem[];
return {
type: LogType.FunctionCall,
items,
data: items,
copyTooltip: I18n.t('workflow_250310_13', undefined, '复制'),
};
}

View File

@@ -0,0 +1,11 @@
.group-wrap {
display: flex;
flex-direction: column;
row-gap: 4px;
margin-bottom: 12px;
.title {
display: flex;
align-items: center;
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import css from './base-group-wrap.module.less';
interface BaseGroupWrapProps {
title?: React.ReactNode;
}
export const BaseGroupWrap: React.FC<
React.PropsWithChildren<BaseGroupWrapProps>
> = ({ title, children }) => (
<div className={css['group-wrap']}>
{title ? <div className={css.title}>{title}</div> : null}
{children}
</div>
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ProblemGroup } from './problem-group';

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Tag, Typography } from '@coze-arch/coze-design';
import { NodeItem, LineItem, TextItem } from '../problem-item';
import { type WorkflowProblem, type ProblemItem } from '../../types';
import { BaseGroupWrap } from './base-group-wrap';
interface MyProblemGroupProps {
problems: WorkflowProblem;
isMine?: boolean;
showTitle?: boolean;
onClick: (p: ProblemItem) => void;
}
export const MyProblemGroup: React.FC<MyProblemGroupProps> = ({
problems,
isMine,
showTitle,
onClick,
}) => {
const { node, line } = problems.problems;
return (
<BaseGroupWrap
title={
showTitle ? (
<>
<Typography.Text
strong
fontSize="14px"
className="coz-fg-secondary"
>
{problems.name}
</Typography.Text>
<Tag className="ml-2" size="small" color="primary">
{isMine
? I18n.t('wf_problem_my_tag')
: I18n.t('wf_problem_other_tag')}
</Tag>
</>
) : undefined
}
>
{node.map(i =>
isMine ? (
<NodeItem problem={i} onClick={onClick} />
) : (
<TextItem problem={i} onClick={onClick} />
),
)}
{line.map((i, idx) =>
isMine ? (
<LineItem problem={i} idx={idx} onClick={onClick} />
) : (
<TextItem problem={i} onClick={onClick} />
),
)}
</BaseGroupWrap>
);
};

View File

@@ -0,0 +1,5 @@
.problem-group {
height: 100%;
overflow-y: auto;
padding: 12px;
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ProblemEmpty } from '../problem-panel/empty';
import { type WorkflowProblem, type ProblemItem } from '../../types';
import { MyProblemGroup } from './my-problem-group';
import css from './problem-group.module.less';
interface ProblemGroupProps {
myProblems?: WorkflowProblem;
otherProblems: WorkflowProblem[];
onScroll: (p: ProblemItem) => void;
onJump: (p: ProblemItem, workflowId: string) => void;
}
export const ProblemGroup: React.FC<ProblemGroupProps> = ({
myProblems,
otherProblems,
onScroll,
onJump,
}) => {
const isEmpty = !myProblems && !otherProblems.length;
if (isEmpty) {
return <ProblemEmpty />;
}
return (
<div className={css['problem-group']}>
{myProblems ? (
<MyProblemGroup
problems={myProblems}
showTitle={!!otherProblems.length}
isMine
onClick={onScroll}
/>
) : null}
{otherProblems.map(other => (
<MyProblemGroup
problems={other}
showTitle={true}
onClick={p => onJump(p, other.workflowId)}
/>
))}
</div>
);
};

View File

@@ -0,0 +1,40 @@
.base-item {
display: flex;
column-gap: 12px;
padding: 4px 10px;
}
.text-item {
padding: 8px 10px;
display: flex;
}
.item-icon {
padding-top: 6px;
}
.item-title {
display: flex;
font-size: 16px;
align-items: center;
column-gap: 8px;
}
.item-popover {
font-size: 12px;
color: var(--coz-fg-secondary);
}
.item-info {
line-height: 20px;
}
.base-item-wrap {
border: 0.5px solid var(--coz-stroke-primary);
border-radius: 8px;
box-shadow: var(--coz-shadow-small);
cursor: pointer;
&:hover {
background: rgba(77, 77, 77, 0.05);
}
&:active {
background: rgba(77, 77, 77, 0.15);
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type React from 'react';
import { useCallback } from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Popover, Typography, Tag } from '@coze-arch/coze-design';
import { type ProblemItem } from '../../types';
import styles from './base-item.module.less';
type BaseItemWrapProps = React.HTMLAttributes<HTMLDivElement> & {
className?: string;
};
export const BaseItemWrap: React.FC<
React.PropsWithChildren<BaseItemWrapProps>
> = ({ className, ...props }) => (
<div className={cls(styles['base-item-wrap'], className)} {...props}></div>
);
interface BaseItemProps {
problem: ProblemItem;
title: string;
icon: React.ReactNode;
popover?: React.ReactNode;
onClick: (p: ProblemItem) => void;
}
const { Text } = Typography;
export const BaseItem: React.FC<BaseItemProps> = ({
problem,
title,
icon,
popover,
onClick,
}) => {
const { errorInfo, errorLevel } = problem;
const handleClick = useCallback(() => {
onClick(problem);
}, [problem, onClick]);
return (
<BaseItemWrap className={styles['base-item']} onClick={handleClick}>
<div className={styles['item-icon']}>{icon}</div>
<div className={styles['item-content']}>
<div className={styles['item-title']}>
<Text weight={500}>{title}</Text>
{errorLevel === 'warning' && (
<Tag color="primary">{I18n.t('workflow_exception_ignore_tag')}</Tag>
)}
{popover ? (
<Popover content={popover} position="top">
<IconCozInfoCircle className={styles['item-popover']} />
</Popover>
) : null}
</div>
<div className={styles['item-info']}>
<Text
size="small"
className={
errorLevel === 'error' ? 'coz-fg-hglt-red' : 'coz-fg-hglt-yellow'
}
>
{errorInfo}
</Text>
</div>
</div>
</BaseItemWrap>
);
};
export const TextItem: React.FC<{
problem: ProblemItem;
onClick: (p: ProblemItem) => void;
}> = ({ problem, onClick }) => {
const { errorInfo, errorLevel } = problem;
const handleClick = useCallback(() => {
onClick(problem);
}, [problem, onClick]);
return (
<BaseItemWrap className={styles['text-item']} onClick={handleClick}>
<Text
size="small"
className={
errorLevel === 'error' ? 'coz-fg-hglt-red' : 'coz-fg-hglt-yellow'
}
>
{errorInfo}
</Text>
</BaseItemWrap>
);
};

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { NodeItem } from './node-item';
export { LineItem } from './line-item';
export { TextItem } from './base-item';

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,12 @@
.line-icon {
font-size: 32px;
color: var(--coz-fg-hglt-red);
}
.line-popover {
display: flex;
flex-direction: column;
img {
margin-top: 2px;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozAddNode } from '@coze-arch/coze-design/icons';
import { Typography } from '@coze-arch/coze-design';
import { type ProblemItem } from '../../types';
import i18n from './line-case-i18n.png';
import cn from './line-case-cn.png';
import { BaseItem } from './base-item';
import styles from './line-item.module.less';
interface LineItemProps {
problem: ProblemItem;
idx: number;
onClick: (p: ProblemItem) => void;
}
const LinePopover = () => {
const lang = I18n.getLanguages();
const currentLang = lang[0];
return (
<div className={styles['line-popover']}>
<Typography.Text fontSize="16px">
{I18n.t('workflow_running_results_line_error')}
</Typography.Text>
<img src={['zh-CN', 'zh'].includes(currentLang) ? cn : i18n} />
</div>
);
};
export const LineItem: React.FC<LineItemProps> = ({
problem,
idx,
onClick,
}) => (
<BaseItem
problem={problem}
title={`${I18n.t('workflow_connection_name')}${idx + 1}`}
icon={<IconCozAddNode className={styles['line-icon']} />}
popover={<LinePopover />}
onClick={onClick}
/>
);

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { isEqual } from 'lodash-es';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { usePlayground } from '@flowgram-adapter/free-layout-editor';
import {
WorkflowNodeData,
type CommonNodeData,
type NodeData,
} from '@coze-workflow/nodes';
import { Avatar } from '@coze-arch/coze-design';
import { type ProblemItem } from '../../types';
import { BaseItem } from './base-item';
interface NodeItemProps {
problem: ProblemItem;
onClick: (p: ProblemItem) => void;
}
// 避免节点删除后丢失icon、title信息
const useMetaMemo = (nodeId: string) => {
const [nodeMeta, setNodeMeta] = useState<CommonNodeData>();
const playground = usePlayground();
const node = playground.entityManager.getEntityById<FlowNodeEntity>(nodeId);
const nodeData = node?.getData<WorkflowNodeData>(WorkflowNodeData);
const meta = nodeData?.getNodeData<keyof NodeData>();
useEffect(() => {
if (meta && !isEqual(nodeMeta, meta)) {
setNodeMeta(meta);
}
}, [meta]);
return nodeMeta;
};
export const NodeItem: React.FC<NodeItemProps> = ({ problem, onClick }) => {
const meta = useMetaMemo(problem.nodeId);
return (
<BaseItem
problem={problem}
title={meta?.title || ''}
icon={<Avatar src={meta?.icon} shape="square" size="small" />}
onClick={onClick}
/>
);
};

View File

@@ -0,0 +1,6 @@
.problem-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IconCozIllusDone } from '@coze-arch/coze-design/illustrations';
import styles from './empty.module.less';
export const ProblemEmpty = () => (
<div className={styles['problem-empty']}>
<IconCozIllusDone width="120" height="120" />
</div>
);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ProblemPanel } from './problem-panel';

View File

@@ -0,0 +1,32 @@
.problem-list {
height: 100%;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
row-gap: 8px;
}
.base-panel {
min-width: 300px;
}
.panel-title {
display: flex;
align-items: center;
column-gap: 16px;
.checking {
display: flex;
align-items: center;
font-size: 12px;
color: var(--coz-fg-secondary);
column-gap: 2px;
:global(.semi-spin-wrapper) {
line-height: 14px;
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { Spin } from '@coze-arch/coze-design';
import { ProblemGroup } from '../problem-group';
import { type ProblemItem } from '../../types';
import { useProblems } from '../../hooks/use-problems';
import { BasePanel } from '../../../../components';
import styles from './problem-panel.module.less';
interface ProblemPanelProps {
maxHeight: number;
workflowId: string;
onScroll: (p: ProblemItem) => void;
onJump: (p: ProblemItem, workflowId: string) => void;
onClose: () => void;
}
export const ProblemPanel: React.FC<ProblemPanelProps> = ({
maxHeight,
workflowId,
onScroll,
onJump,
onClose,
}) => {
const { problemsV2, validating } = useProblems(workflowId);
return (
<BasePanel
header={
<div className={styles['panel-title']}>
{I18n.t('card_builder_check_title')}
{validating ? (
<div className={styles.checking}>
<Spin size="small" />
{I18n.t('wf_testrun_problems_loading')}
</div>
) : null}
</div>
}
height={300}
resizable={{
min: 300,
max: maxHeight,
}}
onClose={onClose}
className={styles['base-panel']}
>
<ProblemGroup {...problemsV2} onScroll={onScroll} onJump={onJump} />
</BasePanel>
);
};

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import { uniq } from 'lodash-es';
import {
useValidationServiceStore,
type ValidateError,
type ValidationState,
} from '@coze-workflow/base/services';
import { type WorkflowProblem } from '../types';
const generateErrors2Problems = (errors: ValidationState['errors']) => {
const nodeProblems: ValidateError[] = [];
const lineProblems: ValidateError[] = [];
Object.entries(errors).forEach(([id, list]) => {
const nodeErrors = list.filter(i => i.errorType === 'node');
const lineErrors = list.filter(i => i.errorType === 'line');
// 处理节点错误
const nodeLevelErrors = nodeErrors.filter(
item => item.errorLevel === 'error',
);
const nodeLevelWarnings = nodeErrors.filter(
item => item.errorLevel === 'warning',
);
// errors 优先,其次才显示 warning
const nodeCurrentErrors = nodeLevelErrors.length
? nodeLevelErrors
: nodeLevelWarnings;
if (nodeCurrentErrors.length) {
const nodeProblem: ValidateError = {
nodeId: id,
errorInfo: uniq(nodeCurrentErrors.map(error => error.errorInfo)).join(
';',
),
errorLevel: nodeCurrentErrors[0].errorLevel,
errorType: 'node',
};
nodeProblems.push(nodeProblem);
}
// 处理线错误
if (lineErrors.length) {
lineProblems.push(...lineErrors);
}
});
return {
node: nodeProblems,
line: lineProblems,
};
};
const generateProblemsV2 = (
errors: ValidationState['errorsV2'],
workflowId: string,
) => {
let myProblems: WorkflowProblem | undefined;
const otherProblems: WorkflowProblem[] = [];
Object.entries(errors).forEach(([id, error]) => {
if (!Object.keys(error.errors).length) {
return;
}
const value = {
...error,
problems: generateErrors2Problems(error.errors),
};
if (id === workflowId) {
myProblems = value;
} else {
otherProblems.push(value);
}
});
return {
myProblems,
otherProblems,
};
};
export const useProblems = (workflowId: string) => {
const { errorsV2, validating } = useValidationServiceStore(store => ({
errorsV2: store.errorsV2,
validating: store.validating,
}));
const problemsV2 = useMemo(
() => generateProblemsV2(errorsV2, workflowId),
[errorsV2, workflowId],
);
return { problemsV2, validating };
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { ProblemPanel } from './components/problem-panel';

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FeedbackStatus } from '@flowgram-adapter/free-layout-editor';
import { type WorkflowValidateError } from '@coze-workflow/base/services';
export type WorkflowProblem = WorkflowValidateError & {
problems: {
node: ProblemItem[];
line: ProblemItem[];
};
};
export interface ProblemItem {
// 错误描述
errorInfo: string;
// 错误等级
errorLevel: FeedbackStatus;
// 错误类型: 节点 / 连线
errorType: 'node' | 'line';
// 节点id
nodeId: string;
// 若为连线错误,还需要目标节点来确认这条连线
targetNodeId?: string;
}

View File

@@ -0,0 +1,3 @@
.answer-input {
padding: 0 12px 16px 12px;
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozSendFill } from '@coze-arch/coze-design/icons';
import { Input, IconButton } from '@coze-arch/coze-design';
import { useSendMessage } from '../../hooks';
import styles from './answer-input.module.less';
export const AnswerInput = () => {
const { send, waiting } = useSendMessage();
const [value, setValue] = useState('');
const disabled = value === '' || waiting;
const handleSend = () => {
if (disabled) {
return;
}
send(value);
setValue('');
};
return (
<div className={styles['answer-input']}>
<Input
placeholder={I18n.t(
'workflow_ques_ans_testrun_message_placeholder',
{},
'Send a message',
)}
value={value}
onChange={val => setValue(val)}
onEnterPress={handleSend}
autoFocus
size="large"
suffix={
<IconButton
icon={<IconCozSendFill />}
disabled={disabled}
onClick={handleSend}
color="secondary"
/>
}
/>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { MessageList } from './message-list';

View File

@@ -0,0 +1,18 @@
.message-item {
display: flex;
column-gap: 12px;
}
.message-avatar {
flex-shrink: 0;
width: 32px;
}
.message-main {
display: flex;
flex-direction: column;
row-gap: 4px;
flex-grow: 1;
}
.user-name {
font-size: 12px;
color: var(--coz-fg-secondary);
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Avatar } from '@coze-arch/coze-design';
import { type ReceivedMessage } from '../../types';
import { ContentType, MessageType } from '../../constants';
import userAvatar from './user-avatar.png';
import { TextMessage } from './text-message';
import { OptionMessage } from './option-message';
import { MessageLoading } from './message-loading';
import botAvatar from './bot-avatar.png';
import styles from './message-item.module.less';
const BotAvatar = () => (
<Avatar src={botAvatar} size="small" className={styles['message-avatar']} />
);
const UserAvatar = () => (
<Avatar src={userAvatar} size="small" className={styles['message-avatar']} />
);
interface MessageItemProps {
message: ReceivedMessage;
loading?: boolean;
}
export const MessageItem: React.FC<MessageItemProps> = ({
message,
loading,
}) => {
const { content_type: contentType, type } = message;
return (
<div className={styles['message-item']}>
{type === MessageType.Question ? <BotAvatar /> : <UserAvatar />}
<div className={styles['message-main']}>
<div className={styles['user-name']}>
{type === MessageType.Question
? I18n.t('workflow_ques_ans_testrun_botname', {}, 'Bot')
: I18n.t('workflow_ques_ans_testrun_username', {}, 'User')}
</div>
{loading ? <MessageLoading /> : null}
{!loading && contentType === ContentType.Option && (
<OptionMessage message={message} />
)}
{!loading && contentType === ContentType.Text && (
<TextMessage message={message} />
)}
</div>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More