feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
BIN
frontend/packages/devops/common-modules/src/assets/rspack.png
Normal file
BIN
frontend/packages/devops/common-modules/src/assets/rspack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1 @@
|
||||
公共组件目录
|
||||
@@ -0,0 +1 @@
|
||||
公共hook目录
|
||||
21
frontend/packages/devops/common-modules/src/index.ts
Normal file
21
frontend/packages/devops/common-modules/src/index.ts
Normal 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 {
|
||||
TraceFlamethread,
|
||||
TraceTree,
|
||||
useSpanTransform,
|
||||
} from './modules/query-trace';
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 GlobalStyle,
|
||||
type RectStyle,
|
||||
type LabelStyle,
|
||||
type LabelText,
|
||||
} from './typing';
|
||||
|
||||
export const defaultRectStyle: RectStyle = {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
stroke: '#1D1C2314',
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
hover: {
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
select: {
|
||||
lineWidth: 1,
|
||||
lineDash: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultGlobalStyle: GlobalStyle = {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultDatazoomDecimals = 1;
|
||||
|
||||
export const defaultVisibleRowCount = 6;
|
||||
export const defaultRowHeight = 42;
|
||||
export const defaultVisibleColumnCount = 6; // 13 // 8
|
||||
|
||||
export const defaultLabelStyle: LabelStyle = {
|
||||
position: 'inside-left',
|
||||
fontSize: 12,
|
||||
fill: '#212629',
|
||||
};
|
||||
|
||||
export const defaultLabelText: LabelText = (datum, element, params) =>
|
||||
`${datum.start}-${datum.end}`;
|
||||
|
||||
// xScale的padding(解决hover后rect边框被截断问题)
|
||||
export const scrollbarMargin = 10;
|
||||
export const datazoomHeight = 20;
|
||||
export const datazoomDecimals = 0;
|
||||
export const datazoomPaddingBottom = 18;
|
||||
@@ -0,0 +1,692 @@
|
||||
/*
|
||||
* 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,
|
||||
useEffect,
|
||||
type FC,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { pick, uniqWith } from 'lodash-es';
|
||||
import {
|
||||
View,
|
||||
type ViewSpec,
|
||||
ComponentEnum,
|
||||
GrammarMarkType,
|
||||
type GrammarScaleType,
|
||||
type MarkSpec,
|
||||
type IView,
|
||||
} from '@visactor/vgrammar';
|
||||
|
||||
import type {
|
||||
FlamethreadProps,
|
||||
RectNode,
|
||||
RectStyle,
|
||||
LabelStyle,
|
||||
LabelText,
|
||||
Tooltip,
|
||||
IElement,
|
||||
GlobalStyle,
|
||||
InteractionEventHandler,
|
||||
} from './typing';
|
||||
import {
|
||||
datazoomDecimals,
|
||||
datazoomHeight,
|
||||
datazoomPaddingBottom,
|
||||
defaultGlobalStyle,
|
||||
defaultLabelStyle,
|
||||
defaultLabelText,
|
||||
defaultRectStyle,
|
||||
defaultRowHeight,
|
||||
defaultVisibleColumnCount,
|
||||
scrollbarMargin,
|
||||
} from './config';
|
||||
|
||||
export type {
|
||||
FlamethreadProps,
|
||||
RectNode,
|
||||
RectStyle,
|
||||
LabelStyle,
|
||||
LabelText,
|
||||
Tooltip,
|
||||
IElement,
|
||||
GlobalStyle,
|
||||
InteractionEventHandler,
|
||||
};
|
||||
|
||||
export const Flamethread: FC<FlamethreadProps> = props => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewRef = useRef<IView | null>(null);
|
||||
const [viewSize, setViewSize] = useState({ width: 0, height: 0 });
|
||||
const {
|
||||
flamethreadData,
|
||||
rowHeight = defaultRowHeight,
|
||||
visibleColumnCount = defaultVisibleColumnCount,
|
||||
tooltip,
|
||||
rectStyle: globalRectStyle,
|
||||
labelStyle: _globalLabelStyle,
|
||||
globalStyle: _globalStyle,
|
||||
axisLabelSuffix,
|
||||
labelText,
|
||||
selectedKey,
|
||||
disableViewScroll = false,
|
||||
enableAutoFit = false,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
const genRectStyle = useCallback(
|
||||
(rectStyle?: RectStyle): RectStyle => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
defaultRectStyle.normal,
|
||||
globalRectStyle?.normal,
|
||||
rectStyle?.normal,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
defaultRectStyle.hover,
|
||||
globalRectStyle?.hover,
|
||||
rectStyle?.hover,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
defaultRectStyle.select,
|
||||
globalRectStyle?.select,
|
||||
rectStyle?.select,
|
||||
),
|
||||
}),
|
||||
[globalRectStyle],
|
||||
);
|
||||
|
||||
const genLabelStyle = useCallback(
|
||||
(labelStyle?: LabelStyle): LabelStyle =>
|
||||
Object.assign({}, defaultLabelStyle, _globalLabelStyle, labelStyle),
|
||||
[_globalLabelStyle],
|
||||
);
|
||||
|
||||
const globalLabelStyle = useMemo(
|
||||
() => Object.assign({}, defaultLabelStyle, _globalLabelStyle),
|
||||
[_globalLabelStyle],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- 计算需要
|
||||
const topOffset = datazoomHeight + datazoomPaddingBottom + 8;
|
||||
|
||||
const globalStyle: GlobalStyle = useMemo(
|
||||
() => Object.assign({}, defaultGlobalStyle, _globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
|
||||
const totalRowHeight = useMemo(() => {
|
||||
const rowCount = uniqWith(
|
||||
flamethreadData,
|
||||
(node0: RectNode, node1: RectNode) => node0.rowNo === node1.rowNo,
|
||||
).length;
|
||||
return rowCount * rowHeight;
|
||||
}, [flamethreadData]);
|
||||
|
||||
// 此参数含义: 可视窗口Height / 火焰图Height
|
||||
const yScaleRangeFactor = useMemo(() => {
|
||||
const rowCount = uniqWith(
|
||||
flamethreadData,
|
||||
(node0: RectNode, node1: RectNode) => node0.rowNo === node1.rowNo,
|
||||
).length;
|
||||
|
||||
return rowCount !== 0
|
||||
? ((viewRef.current?.getViewBox().height() || 300) - topOffset) /
|
||||
(rowCount * rowHeight)
|
||||
: 1;
|
||||
}, [flamethreadData, viewSize.height]);
|
||||
|
||||
const spec = useMemo(() => {
|
||||
const orgData = flamethreadData.map(node => {
|
||||
const rectStyle = genRectStyle(node.rectStyle);
|
||||
const labelStyle = genLabelStyle(node.labelStyle);
|
||||
return {
|
||||
...node,
|
||||
rectStyle,
|
||||
labelStyle,
|
||||
};
|
||||
});
|
||||
|
||||
const marks = [
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.axis,
|
||||
id: 'xAxis',
|
||||
scale: 'xScale',
|
||||
axisType: 'line',
|
||||
tickCount: visibleColumnCount,
|
||||
dependency: ['viewBox'],
|
||||
encode: {
|
||||
update: (scale0, elment, params) => {
|
||||
const scale = params.xScale;
|
||||
const range = scale.range() as number[];
|
||||
const tickData = scale.tickData(visibleColumnCount);
|
||||
const dx =
|
||||
tickData.length > 1
|
||||
? // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- 计算需要
|
||||
(range[1] - range[0]) / (tickData.length - 1) / 2
|
||||
: 0;
|
||||
|
||||
return {
|
||||
verticalFactor: -1,
|
||||
x: params.viewBox.x1,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: params.viewBox.width(), y: 0 },
|
||||
tick: { visible: false },
|
||||
label: {
|
||||
style: { dx: -dx },
|
||||
formatMethod: (_value: string) => {
|
||||
const value = Number(_value);
|
||||
// 特化逻辑: 隐藏0刻度
|
||||
if (dx > 0 && value === 0) {
|
||||
return '';
|
||||
}
|
||||
return value !== 0 && axisLabelSuffix !== undefined
|
||||
? `${value}${axisLabelSuffix}`
|
||||
: value;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.grid,
|
||||
tickCount: visibleColumnCount, // vgrammer库的类型写的不严谨,实际是可用的
|
||||
scale: 'xScale',
|
||||
gridType: 'line',
|
||||
gridShape: 'line',
|
||||
dependency: ['viewBox'],
|
||||
// dependency: ["viewBox"],
|
||||
encode: {
|
||||
update: (scale, elment, params) => ({
|
||||
verticalFactor: -1,
|
||||
length: params.viewBox.height() - topOffset,
|
||||
x: params.viewBox.x1,
|
||||
x1: params.viewBox.x2,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: params.viewBox.width(), y: 0 },
|
||||
style: { stroke: '#ccc', lineWidth: 1, lineDash: [] },
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.group,
|
||||
dependency: ['viewBox'],
|
||||
encode: {
|
||||
update: (scale, elment: IElement, params) => ({
|
||||
x: params.viewBox.x1,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
width: params.viewBox.width(),
|
||||
height: params.viewBox.height() - topOffset,
|
||||
clip: true,
|
||||
}),
|
||||
},
|
||||
|
||||
marks: [
|
||||
{
|
||||
type: GrammarMarkType.rect,
|
||||
id: 'rect',
|
||||
from: { data: 'orgData' },
|
||||
groupBy: 'start',
|
||||
key: 'rowNo',
|
||||
encode: {
|
||||
update: {
|
||||
x: { scale: 'xScale', field: 'start' },
|
||||
x1: { scale: 'xScale', field: 'end' },
|
||||
y: { scale: 'yScale', field: 'rowNo', band: 0.07 },
|
||||
// height: { scale: 'yScale', band: 0.86 },
|
||||
height: rowHeight - 4,
|
||||
// height: { scale: "yScale", band: 0.7, offset: 0.15 },
|
||||
fill: (datum, element, params) =>
|
||||
datum?.rectStyle?.normal?.fill,
|
||||
innerBorder: (datum, element, params) => {
|
||||
const { stroke, lineWidth, lineDash } =
|
||||
datum.rectStyle.normal;
|
||||
return {
|
||||
stroke: lineWidth !== 0 ? stroke : null,
|
||||
lineWidth,
|
||||
lineDash,
|
||||
visible: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- 尺寸计算,无须处理
|
||||
distance: lineWidth / 2,
|
||||
};
|
||||
},
|
||||
},
|
||||
hover2: {
|
||||
fill: (datum, element, params) =>
|
||||
datum?.rectStyle?.hover?.fill ??
|
||||
datum?.rectStyle?.normal?.fill,
|
||||
innerBorder: (datum, element, params) => {
|
||||
const { stroke, lineWidth, lineDash } = Object.assign(
|
||||
{},
|
||||
datum?.rectStyle?.normal,
|
||||
datum?.rectStyle?.hover,
|
||||
);
|
||||
return {
|
||||
stroke: lineWidth !== 0 ? stroke : null,
|
||||
lineWidth,
|
||||
lineDash,
|
||||
visible: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- 尺寸计算,无须定义常量
|
||||
distance: lineWidth / 2,
|
||||
};
|
||||
},
|
||||
zIndex: 2,
|
||||
},
|
||||
select2: {
|
||||
fill: (datum, element, params) =>
|
||||
datum?.rectStyle?.select?.fill ??
|
||||
datum?.rectStyle?.normal?.fill,
|
||||
innerBorder: (datum, element, params) => {
|
||||
const { stroke, lineWidth, lineDash } = Object.assign(
|
||||
{},
|
||||
datum?.rectStyle?.normal,
|
||||
datum?.rectStyle?.select,
|
||||
);
|
||||
return {
|
||||
stroke: lineWidth !== 0 ? stroke : null,
|
||||
lineWidth,
|
||||
lineDash,
|
||||
visible: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- 尺寸计算,无须定义常量
|
||||
distance: lineWidth / 2,
|
||||
};
|
||||
},
|
||||
zIndex: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.label,
|
||||
target: 'rect',
|
||||
labelStyle: {
|
||||
position: globalLabelStyle.position,
|
||||
textStyle: {
|
||||
fontSize: globalLabelStyle.fontSize,
|
||||
},
|
||||
animation: false,
|
||||
overlap: {
|
||||
hideOnHit: false,
|
||||
clampForce: false,
|
||||
strategy: [{ type: 'position', position: ['top-left'] }],
|
||||
},
|
||||
},
|
||||
encode: {
|
||||
update: {
|
||||
pickable: false, // vgrammer库的类型写的不严谨
|
||||
text: labelText ?? defaultLabelText,
|
||||
fill: (datum, element, params) => datum?.labelStyle.fill,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.datazoom,
|
||||
id: 'dataZoom',
|
||||
dependency: ['viewBox'],
|
||||
preview: {
|
||||
data: 'table',
|
||||
x: { scale: 'dataZoomXScale', field: ['start', 'end'] },
|
||||
y: { scale: 'dataZoomYScale', field: 'rowNo' },
|
||||
},
|
||||
encode: {
|
||||
update: (scale, elment, params) => ({
|
||||
showDetail: false,
|
||||
x: params.viewBox.x1,
|
||||
y: params.viewBox.y1,
|
||||
size: { width: params.viewBox.width(), height: datazoomHeight },
|
||||
// start: 0,
|
||||
// end: 1,
|
||||
// fill: '#ff0000',
|
||||
minSpan: 0.01,
|
||||
selectedBackgroundStyle: {
|
||||
fill: '#B4BAF6',
|
||||
},
|
||||
brushSelect: false,
|
||||
startHandlerStyle: {
|
||||
symbolType:
|
||||
'M-0.5-2.4h0.9c0.4,0,0.7,0.3,0.7,0.7v3.3c0,0.4-0.3,0.7-0.7,0.7h-0.9c-0.4,0-0.7-0.3-0.7-0.7v-3.3\nC-1.2-2-0.9-2.4-0.5-2.4z M-0.4-1.4L-0.4-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC-0.4-1.4-0.4-1.4-0.4-1.4z M0.3-1.4L0.3-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC0.3-1.4,0.3-1.4,0.3-1.4z;',
|
||||
fill: '#ffffff',
|
||||
scaleX: 1.2,
|
||||
scaleY: 1.2,
|
||||
stroke: '#aeb5be',
|
||||
lineWidth: 1,
|
||||
size: 20,
|
||||
},
|
||||
middleHandlerStyle: {
|
||||
visible: false,
|
||||
},
|
||||
endHandlerStyle: {
|
||||
symbolType:
|
||||
'M-0.5-2.4h0.9c0.4,0,0.7,0.3,0.7,0.7v3.3c0,0.4-0.3,0.7-0.7,0.7h-0.9c-0.4,0-0.7-0.3-0.7-0.7v-3.3\nC-1.2-2-0.9-2.4-0.5-2.4z M-0.4-1.4L-0.4-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC-0.4-1.4-0.4-1.4-0.4-1.4z M0.3-1.4L0.3-1.4c0,0,0,0.1,0,0.1v2.6c0,0.1,0,0.1,0,0.1l0,0c0,0,0-0.1,0-0.1v-2.6\nC0.3-1.4,0.3-1.4,0.3-1.4z;',
|
||||
fill: '#ffffff',
|
||||
scaleX: 1.2,
|
||||
scaleY: 1.2,
|
||||
stroke: '#aeb5be',
|
||||
lineWidth: 1,
|
||||
size: 20,
|
||||
},
|
||||
startTextStyle: {
|
||||
padding: 8,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
lineHeight: '130%',
|
||||
fill: '#606773',
|
||||
// fill: '#ff0000',
|
||||
},
|
||||
formatMethod: (value: number) => value.toFixed(datazoomDecimals),
|
||||
},
|
||||
endTextStyle: {
|
||||
padding: 8,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
lineHeight: '130%',
|
||||
fill: '#606773',
|
||||
},
|
||||
formatMethod: (value: number) => value.toFixed(datazoomDecimals),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
] as MarkSpec[];
|
||||
|
||||
const padding = {
|
||||
top: 3,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
};
|
||||
|
||||
if (yScaleRangeFactor < 1) {
|
||||
marks.unshift({
|
||||
type: GrammarMarkType.component,
|
||||
componentType: ComponentEnum.scrollbar,
|
||||
direction: 'vertical',
|
||||
id: 'verticalScrollbar',
|
||||
dependency: ['viewBox', 'yScale'],
|
||||
encode: {
|
||||
update: (scale, elment, params) => {
|
||||
const { yScale } = params;
|
||||
const curRangeFactor = yScale?.rangeFactor?.() ?? [
|
||||
0,
|
||||
yScaleRangeFactor,
|
||||
];
|
||||
|
||||
return {
|
||||
x: params.viewBox.x2 + scrollbarMargin,
|
||||
y: params.viewBox.y1 + topOffset,
|
||||
height: params.viewBox.height() - topOffset,
|
||||
range: [curRangeFactor[1], curRangeFactor[0]],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
padding.right = 22;
|
||||
}
|
||||
|
||||
const spec0: ViewSpec = {
|
||||
padding,
|
||||
background: globalStyle.background,
|
||||
|
||||
data: [
|
||||
{
|
||||
id: 'orgData',
|
||||
values: orgData,
|
||||
},
|
||||
{
|
||||
id: 'markData',
|
||||
source: 'orgData',
|
||||
},
|
||||
],
|
||||
|
||||
scales: [
|
||||
{
|
||||
id: 'xScale',
|
||||
type: 'linear' as GrammarScaleType,
|
||||
domain: { data: 'markData', field: ['start', 'end'] },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => [0, params.viewBox.width()],
|
||||
nice: true,
|
||||
},
|
||||
{
|
||||
id: 'yScale',
|
||||
type: 'band',
|
||||
domain: { data: 'markData', field: 'rowNo' },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => {
|
||||
const vHeight = params.viewBox.height() - topOffset;
|
||||
const height = yScaleRangeFactor <= 1 ? vHeight : totalRowHeight;
|
||||
|
||||
return [0, height];
|
||||
},
|
||||
padding: 0,
|
||||
round: false,
|
||||
},
|
||||
{
|
||||
id: 'dataZoomXScale',
|
||||
type: 'linear',
|
||||
domain: { data: 'orgData', field: ['start', 'end'] },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => [0, params.viewBox.width()],
|
||||
},
|
||||
{
|
||||
id: 'dataZoomYScale',
|
||||
type: 'band',
|
||||
domain: { data: 'orgData', field: 'rowNo' },
|
||||
dependency: ['viewBox'],
|
||||
range: (scale, params) => [params.viewBox.height(), 0],
|
||||
padding: 0.05,
|
||||
round: true,
|
||||
},
|
||||
],
|
||||
|
||||
marks,
|
||||
};
|
||||
return spec0;
|
||||
}, [
|
||||
flamethreadData,
|
||||
visibleColumnCount,
|
||||
globalLabelStyle.position,
|
||||
globalLabelStyle.fontSize,
|
||||
labelText,
|
||||
yScaleRangeFactor,
|
||||
totalRowHeight,
|
||||
globalStyle.padding,
|
||||
globalStyle.background,
|
||||
genRectStyle,
|
||||
genLabelStyle,
|
||||
axisLabelSuffix,
|
||||
]);
|
||||
|
||||
const updateSelectedKey = useCallback(
|
||||
(view: IView) => {
|
||||
const rectElm = view?.getMarkById('rect');
|
||||
const elements = rectElm?.elements;
|
||||
elements?.forEach(element => {
|
||||
element?.removeState('select2');
|
||||
});
|
||||
elements
|
||||
?.filter(element => {
|
||||
const datum = element.getDatum();
|
||||
return datum.key === selectedKey;
|
||||
})[0]
|
||||
?.addState('select2');
|
||||
},
|
||||
[selectedKey],
|
||||
);
|
||||
|
||||
// 创建/更新view
|
||||
useLayoutEffect(() => {
|
||||
const initializeYScale = (view: IView) => {
|
||||
const yScale = view?.getScaleById('yScale');
|
||||
yScale?.setRangeFactor([0, yScaleRangeFactor]);
|
||||
yScale?.commit();
|
||||
};
|
||||
|
||||
const initializeScale = (view: IView) => {
|
||||
initializeYScale(view);
|
||||
};
|
||||
|
||||
const registerEvent = (view: IView) => {
|
||||
const rectElm = view?.getMarkById('rect');
|
||||
// rect点击事件
|
||||
rectElm?.addEventListener('click', ((event, element) => {
|
||||
onClick?.(event, element);
|
||||
}) as InteractionEventHandler);
|
||||
|
||||
// rect hover高亮
|
||||
view?.interaction('element-highlight', {
|
||||
selector: 'rect',
|
||||
highlightState: 'hover2',
|
||||
});
|
||||
|
||||
view?.interaction('element-highlight', {
|
||||
trigger: 'click',
|
||||
// triggerOff: "view:click",
|
||||
triggerOff: 'swipe',
|
||||
selector: 'rect',
|
||||
highlightState: 'select2',
|
||||
});
|
||||
|
||||
if (!disableViewScroll) {
|
||||
view.interaction('view-scroll', {
|
||||
scaleY: 'yScale',
|
||||
});
|
||||
}
|
||||
|
||||
// rect hover显示tooltip
|
||||
if (tooltip) {
|
||||
view?.interaction('tooltip', {
|
||||
selector: 'rect',
|
||||
...tooltip,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (containerRef.current && !viewRef.current) {
|
||||
const view = new View({
|
||||
autoFit: enableAutoFit,
|
||||
container: containerRef.current,
|
||||
});
|
||||
|
||||
view?.on('change', (...args) => {
|
||||
const event = args[0];
|
||||
const { start, end } = event.detail;
|
||||
const xScale = view.getScaleById('xScale');
|
||||
xScale?.setRangeFactor([start, end]);
|
||||
xScale?.commit();
|
||||
view?.run();
|
||||
});
|
||||
|
||||
view?.on('scrollDrag', e => {
|
||||
const direction = e?.target?.attribute?.direction;
|
||||
if (direction === 'vertical') {
|
||||
const range = e.detail.value;
|
||||
const yScale = view.getScaleById('yScale');
|
||||
yScale?.setRangeFactor(range);
|
||||
yScale?.commit();
|
||||
view.run();
|
||||
}
|
||||
});
|
||||
|
||||
view.parseSpec(spec);
|
||||
initializeScale(view);
|
||||
registerEvent(view);
|
||||
view.run();
|
||||
updateSelectedKey(view);
|
||||
|
||||
view.run();
|
||||
|
||||
viewRef.current = view;
|
||||
} else if (viewRef.current) {
|
||||
const view = viewRef.current;
|
||||
|
||||
view.updateSpec(spec);
|
||||
initializeScale(view);
|
||||
registerEvent(view);
|
||||
view.run({ reuse: false });
|
||||
updateSelectedKey(view);
|
||||
|
||||
view.run();
|
||||
}
|
||||
}, [
|
||||
spec,
|
||||
tooltip,
|
||||
yScaleRangeFactor,
|
||||
onClick,
|
||||
visibleColumnCount,
|
||||
flamethreadData,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewRef.current) {
|
||||
updateSelectedKey(viewRef.current);
|
||||
}
|
||||
}, [selectedKey]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (viewRef.current) {
|
||||
viewRef.current.release();
|
||||
}
|
||||
viewRef.current = null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver(params => {
|
||||
const width = params[0].target.clientWidth;
|
||||
const height = params[0].target.clientHeight;
|
||||
if (width !== undefined && height !== undefined && viewRef.current) {
|
||||
viewRef.current.resize(width, height);
|
||||
setViewSize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...pick(globalStyle, ['width', 'height']),
|
||||
}}
|
||||
ref={containerRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Flamethread;
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 CSSProperties } from 'react';
|
||||
|
||||
import {
|
||||
type InteractionEventHandler,
|
||||
type TooltipSpec,
|
||||
type ViewSpec,
|
||||
type IElement,
|
||||
} from '@visactor/vgrammar';
|
||||
|
||||
export type { IElement, InteractionEventHandler, TooltipSpec };
|
||||
|
||||
export interface RectStyleAttrs {
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
lineWidth?: number;
|
||||
lineDash?: number[];
|
||||
}
|
||||
|
||||
export interface RectStyle {
|
||||
normal?: RectStyleAttrs;
|
||||
hover?: RectStyleAttrs;
|
||||
select?: RectStyleAttrs;
|
||||
}
|
||||
|
||||
export interface LabelStyle {
|
||||
position?: string;
|
||||
fontSize?: number;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
export interface RectNode {
|
||||
key: string;
|
||||
rowNo: number;
|
||||
start: number;
|
||||
end: number;
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: Pick<LabelStyle, 'fill'>;
|
||||
// 其他字段,会透传
|
||||
extra?: unknown;
|
||||
}
|
||||
|
||||
export type Tooltip = Pick<TooltipSpec, 'title' | 'content'>;
|
||||
|
||||
export type GlobalStyle = Pick<CSSProperties, 'width' | 'height'> &
|
||||
Pick<ViewSpec, 'padding' | 'background'>;
|
||||
|
||||
export type LabelText = (
|
||||
datum: RectNode,
|
||||
element: IElement,
|
||||
params: unknown,
|
||||
) => string;
|
||||
|
||||
export interface FlamethreadProps {
|
||||
flamethreadData: RectNode[];
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: LabelStyle;
|
||||
labelText?: LabelText;
|
||||
tooltip?: Tooltip;
|
||||
globalStyle?: GlobalStyle;
|
||||
rowHeight?: number;
|
||||
visibleColumnCount?: number;
|
||||
// valuePerColumn?: number;
|
||||
datazoomDecimals?: number;
|
||||
axisLabelSuffix?: string;
|
||||
selectedKey?: string;
|
||||
disableViewScroll?: boolean;
|
||||
enableAutoFit?: boolean;
|
||||
onClick?: InteractionEventHandler;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 EdgeProps } from 'reactflow';
|
||||
|
||||
import { SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { CommonNode } from '../custom-nodes';
|
||||
import { CommonEdge } from '../custom-edges';
|
||||
import { NodeEdgeCategory } from '.';
|
||||
|
||||
export const CUSTOM_NODES = {
|
||||
[SpanCategory.Unknown]: CommonNode,
|
||||
[SpanCategory.Start]: CommonNode,
|
||||
[SpanCategory.Agent]: CommonNode,
|
||||
[SpanCategory.LLMCall]: CommonNode,
|
||||
[SpanCategory.Workflow]: CommonNode,
|
||||
[SpanCategory.WorkflowStart]: CommonNode,
|
||||
[SpanCategory.WorkflowEnd]: CommonNode,
|
||||
[SpanCategory.Plugin]: CommonNode,
|
||||
[SpanCategory.Knowledge]: CommonNode,
|
||||
[SpanCategory.Code]: CommonNode,
|
||||
[SpanCategory.Condition]: CommonNode,
|
||||
[SpanCategory.Card]: CommonNode,
|
||||
[SpanCategory.Message]: CommonNode,
|
||||
[SpanCategory.Loop]: CommonNode,
|
||||
[SpanCategory.LongTermMemory]: CommonNode,
|
||||
};
|
||||
|
||||
export const CUSTOM_EDGES: Record<NodeEdgeCategory, React.FC<EdgeProps>> = {
|
||||
[NodeEdgeCategory.Common]: CommonEdge,
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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 {
|
||||
IconCozDatabaseFill,
|
||||
IconCozLongTermMemory,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
IconSpanAgent,
|
||||
IconSpanBMBatch,
|
||||
IconSpanBMConnector,
|
||||
IconSpanBMParallel,
|
||||
IconSpanCard,
|
||||
IconSpanCode,
|
||||
IconSpanCondition,
|
||||
IconSpanKnowledge,
|
||||
IconSpanLLMCall,
|
||||
IconSpanMessage,
|
||||
IconSpanPluginTool,
|
||||
IconSpanUnknown,
|
||||
IconSpanHook,
|
||||
IconSpanVar,
|
||||
IconSpanWorkflow,
|
||||
IconSpanWorkflowEnd,
|
||||
IconSpanWorkflowStart,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { SpanCategory, SpanType } from '@coze-arch/bot-api/ob_query_api';
|
||||
import { ResourceType } from '@coze-arch/bot-api/dp_manage_api';
|
||||
|
||||
import {
|
||||
type TopologicalStatusData,
|
||||
type TopologicalLayoutBizData,
|
||||
type TopologicalLayoutCommonData,
|
||||
} from '../typing';
|
||||
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_FONT = '14px SF Pro Display';
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_DEFAULT_WIDTH = 100;
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_HEIGHT = 24;
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_MAX_WIDTH = 200;
|
||||
export const TOPOLOGY_COMMON_NODE_TEXT_ADDITIONAL_WIDTH = 62;
|
||||
export const TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH = 12;
|
||||
export const TOPOLOGY_DEFAULT_NODE_ICON = <IconSpanUnknown />;
|
||||
|
||||
export enum NodeEdgeCategory {
|
||||
Common = 'common',
|
||||
}
|
||||
|
||||
export enum TopologyLayoutDirection {
|
||||
TB = 'TB',
|
||||
LR = 'LR',
|
||||
}
|
||||
|
||||
export const TOPOLOGY_LAYOUT_RECORD: Partial<
|
||||
Record<SpanType, TopologyLayoutDirection>
|
||||
> = {
|
||||
[SpanType.InvokeAgent]: TopologyLayoutDirection.TB,
|
||||
[SpanType.UserInput]: TopologyLayoutDirection.TB,
|
||||
[SpanType.UserInputV2]: TopologyLayoutDirection.TB,
|
||||
[SpanType.Workflow]: TopologyLayoutDirection.LR,
|
||||
};
|
||||
|
||||
export const RESOURCE_TYPE_RECORD: Partial<Record<SpanType, ResourceType>> = {
|
||||
[SpanType.InvokeAgent]: ResourceType.Bot,
|
||||
[SpanType.UserInput]: ResourceType.Bot,
|
||||
[SpanType.UserInputV2]: ResourceType.Bot,
|
||||
[SpanType.Workflow]: ResourceType.Workflow,
|
||||
};
|
||||
|
||||
export enum NodeLayoutCategory {
|
||||
Common,
|
||||
}
|
||||
|
||||
export const TOPOLOGY_LAYOUT_COMMON_MAP: Record<
|
||||
NodeLayoutCategory,
|
||||
TopologicalLayoutCommonData
|
||||
> = {
|
||||
[NodeLayoutCategory.Common]: {
|
||||
height: TOPOLOGY_COMMON_NODE_TEXT_HEIGHT,
|
||||
},
|
||||
};
|
||||
|
||||
export const TOPOLOGY_LAYOUT_BIZ_MAP: Record<
|
||||
SpanCategory,
|
||||
TopologicalLayoutBizData
|
||||
> = {
|
||||
[SpanCategory.Unknown]: {
|
||||
icon: <IconSpanUnknown />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
icon: <IconSpanAgent />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
icon: <IconSpanLLMCall />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
icon: <IconSpanWorkflowEnd />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.Plugin]: {
|
||||
icon: <IconSpanPluginTool />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.Knowledge]: {
|
||||
icon: <IconSpanKnowledge />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
|
||||
[SpanCategory.Code]: {
|
||||
icon: <IconSpanCode />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
icon: <IconSpanCondition />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
icon: <IconSpanCard />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
icon: <IconSpanMessage />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Variable]: {
|
||||
icon: <IconSpanVar />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Database]: {
|
||||
icon: <IconCozDatabaseFill />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.LongTermMemory]: {
|
||||
icon: <IconCozLongTermMemory />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Hook]: {
|
||||
icon: <IconSpanHook />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Batch]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Loop]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Parallel]: {
|
||||
icon: <IconSpanBMParallel />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Script]: {
|
||||
icon: <IconSpanCode />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.CallFlow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
[SpanCategory.Connector]: {
|
||||
icon: <IconSpanBMConnector />,
|
||||
...TOPOLOGY_LAYOUT_COMMON_MAP[NodeLayoutCategory.Common],
|
||||
},
|
||||
};
|
||||
|
||||
export enum TopologyEdgeStatus {
|
||||
STATIC,
|
||||
DYNAMIC,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
export const TOPOLOGY_EDGE_STATUS_MAP: Record<
|
||||
TopologyEdgeStatus,
|
||||
TopologicalStatusData
|
||||
> = {
|
||||
[TopologyEdgeStatus.STATIC]: {
|
||||
edgeColor: '#C8C8CA',
|
||||
nodeClassName: 'common-node-container_static',
|
||||
},
|
||||
[TopologyEdgeStatus.DYNAMIC]: {
|
||||
edgeColor: '#3EC254',
|
||||
nodeClassName: 'common-node-container_dynamic',
|
||||
},
|
||||
[TopologyEdgeStatus.ERROR]: {
|
||||
edgeColor: '#FF441E',
|
||||
nodeClassName: 'common-node-container_error',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
.batch-edge-info-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 2px 4px;
|
||||
|
||||
font-size: 14px;
|
||||
color: rgb(29 28 35 / 80%);
|
||||
|
||||
background: #F7F7FA;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.batch-edge-info-container_error {
|
||||
font-weight: 600;
|
||||
color: #FF441E;
|
||||
background: #FFF3EE;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 EdgeProps,
|
||||
BaseEdge,
|
||||
getBezierPath,
|
||||
Position,
|
||||
EdgeLabelRenderer,
|
||||
} from 'reactflow';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { getTopologyItemStatus } from '../util';
|
||||
import {
|
||||
type TopologicalBatchNodeExecutionInfo,
|
||||
type EdgeData,
|
||||
} from '../typing';
|
||||
import { TOPOLOGY_EDGE_STATUS_MAP } from '../constant';
|
||||
import { type SpanNode } from '../../../utils/cspan-graph';
|
||||
import { checkIsBatchBasicCSpan } from '../../../utils/cspan';
|
||||
import { type CSPanBatch } from '../../../typings/cspan';
|
||||
|
||||
import s from './common.module.less';
|
||||
|
||||
const isVerticalEdge = (position: Position) =>
|
||||
position === Position.Top || position === Position.Bottom;
|
||||
|
||||
const getBatchNodeExecutionInfo = (
|
||||
spanNode?: SpanNode,
|
||||
): TopologicalBatchNodeExecutionInfo => {
|
||||
const batchNodeExecutionInfo: TopologicalBatchNodeExecutionInfo = {
|
||||
isBatch: false,
|
||||
isError: false,
|
||||
errorNumber: 0,
|
||||
totalNumber: 0,
|
||||
};
|
||||
if (!spanNode || !checkIsBatchBasicCSpan(spanNode)) {
|
||||
return batchNodeExecutionInfo;
|
||||
}
|
||||
const { spans, status: batchNodeStatus } = spanNode as CSPanBatch;
|
||||
|
||||
batchNodeExecutionInfo.isBatch = true;
|
||||
batchNodeExecutionInfo.isError = batchNodeStatus === SpanStatus.Error;
|
||||
|
||||
spans.forEach(span => {
|
||||
const { status } = span;
|
||||
batchNodeExecutionInfo.totalNumber++;
|
||||
if (status === SpanStatus.Error) {
|
||||
batchNodeExecutionInfo.errorNumber++;
|
||||
}
|
||||
});
|
||||
|
||||
return batchNodeExecutionInfo;
|
||||
};
|
||||
|
||||
interface BatchEdgeInfoProps {
|
||||
batchNodeExecutionInfo: TopologicalBatchNodeExecutionInfo;
|
||||
}
|
||||
|
||||
const BatchEdgeInfo = (props: BatchEdgeInfoProps) => {
|
||||
const { batchNodeExecutionInfo } = props;
|
||||
const { isError, totalNumber, errorNumber } = batchNodeExecutionInfo;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
s['batch-edge-info-container'],
|
||||
isError && s['batch-edge-info-container_error'],
|
||||
)}
|
||||
>
|
||||
{!isError || totalNumber === errorNumber ? (
|
||||
<>{totalNumber}</>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={I18n.t('analytic_query_detail_topology_tooltip', {
|
||||
errorCount: errorNumber,
|
||||
callCount: totalNumber,
|
||||
})}
|
||||
>
|
||||
{errorNumber}
|
||||
<span style={{ color: '#1D1C23' }}>
|
||||
<span style={{ margin: '0 3px' }}>/</span>
|
||||
{totalNumber}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommonEdge = (props: EdgeProps<EdgeData>) => {
|
||||
const {
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
markerEnd,
|
||||
data,
|
||||
} = props;
|
||||
|
||||
const batchNodeExecutionInfo = useMemo(
|
||||
() => getBatchNodeExecutionInfo(data?.tailDynamicSpanNode),
|
||||
[data?.tailDynamicSpanNode],
|
||||
);
|
||||
|
||||
const topologyEdgeStatus = getTopologyItemStatus(data?.tailDynamicSpanNode);
|
||||
|
||||
// vertical类型线段布局时,采用节点位置进行定位,从而使线段起点和终点定位在节点开始位置
|
||||
const adaptedSourceX = isVerticalEdge(sourcePosition)
|
||||
? data?.layoutInfo?.customSourceX ?? sourceX
|
||||
: sourceX;
|
||||
const adaptedTargetX = isVerticalEdge(targetPosition)
|
||||
? data?.layoutInfo?.customTargetX ?? targetX
|
||||
: targetX;
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: adaptedSourceX,
|
||||
sourceY,
|
||||
targetX: adaptedTargetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{
|
||||
strokeWidth: 2,
|
||||
stroke: TOPOLOGY_EDGE_STATUS_MAP[topologyEdgeStatus].edgeColor,
|
||||
}}
|
||||
/>
|
||||
{batchNodeExecutionInfo.isBatch && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
<BatchEdgeInfo batchNodeExecutionInfo={batchNodeExecutionInfo} />
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 { CommonEdge } from './common';
|
||||
@@ -0,0 +1,47 @@
|
||||
.common-node {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
|
||||
.common-node-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 24px;
|
||||
padding: 2px 4px;
|
||||
|
||||
|
||||
font-size: 14px;
|
||||
color: rgb(29 28 35 / 80%);
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
.common-node-container-text {
|
||||
max-width: 200px;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.common-node-container_static {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.common-node-container_dynamic {
|
||||
background: #EDF9EE;
|
||||
}
|
||||
|
||||
.common-node-container_error {
|
||||
font-weight: 600;
|
||||
color: #FF441E;
|
||||
background: #FFF3EE;
|
||||
}
|
||||
|
||||
|
||||
:global(.react-flow__handle) {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 {
|
||||
Handle,
|
||||
type NodeProps,
|
||||
Position,
|
||||
useUpdateNodeInternals,
|
||||
} from 'reactflow';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useUpdateEffect } from 'ahooks';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
import { SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { getTopologyItemStatus } from '../util';
|
||||
import { type NodeData } from '../typing';
|
||||
import {
|
||||
TOPOLOGY_EDGE_STATUS_MAP,
|
||||
TopologyEdgeStatus,
|
||||
TopologyLayoutDirection,
|
||||
} from '../constant';
|
||||
|
||||
import s from './common.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const CommonNode = (props: NodeProps<NodeData>) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
data: { name, icon, layoutDirection, dynamicSpanNode },
|
||||
} = props;
|
||||
|
||||
// 特化逻辑:动态tracing中没有workflow_start节点,topo中workflow_start节点默认高亮
|
||||
const topologyNodeStatus =
|
||||
Number(type) === SpanCategory.WorkflowStart
|
||||
? TopologyEdgeStatus.DYNAMIC
|
||||
: getTopologyItemStatus(dynamicSpanNode);
|
||||
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
useUpdateEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [layoutDirection]);
|
||||
|
||||
return (
|
||||
<div className={s['common-node']}>
|
||||
<Handle
|
||||
type="target"
|
||||
position={
|
||||
layoutDirection === TopologyLayoutDirection.LR
|
||||
? Position.Left
|
||||
: Position.Top
|
||||
}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={
|
||||
layoutDirection === TopologyLayoutDirection.LR
|
||||
? Position.Right
|
||||
: Position.Bottom
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
s['common-node-container'],
|
||||
s[TOPOLOGY_EDGE_STATUS_MAP[topologyNodeStatus].nodeClassName],
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<Text
|
||||
className={s['common-node-container-text']}
|
||||
ellipsis={{ showTooltip: true }}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 { CommonNode } from './common';
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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 { useReactFlow, useStoreApi } from 'reactflow';
|
||||
import { type RefObject, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useAsyncEffect, useSize, useThrottle } from 'ahooks';
|
||||
import {
|
||||
Env,
|
||||
type ResourceType,
|
||||
type GetTopoInfoReq,
|
||||
type TopoInfo,
|
||||
TopoType,
|
||||
} from '@coze-arch/bot-api/dp_manage_api';
|
||||
import { dpManageApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { buildTraceTree } from '../../utils/cspan-graph';
|
||||
import { DataSourceTypeEnum } from '../../typings/graph';
|
||||
import {
|
||||
type CSpanAttrUserInput,
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpanAttrWorkflow,
|
||||
} from '../../typings/cspan';
|
||||
import {
|
||||
completeDynamicTopologyInfo,
|
||||
extractOriginDynamicNodeMap,
|
||||
filterObjectByKeys,
|
||||
findNearestTopologyRootSpanNode,
|
||||
generateStaticTopologyDataMapKey,
|
||||
generateTopologyMetaInfo,
|
||||
getAllUpstreamTopologyNodeIds,
|
||||
getLayoutedMeta,
|
||||
getNodeResourceId,
|
||||
getTopologyAgentRootType,
|
||||
} from './util';
|
||||
import {
|
||||
type DynamicNodeMap,
|
||||
type DynamicTopologyData,
|
||||
type StaticTopologyDataCache,
|
||||
type UseGenerateTopologyHookData,
|
||||
type TopologicalData,
|
||||
type ProcessedGetTopoInfoReq,
|
||||
} from './typing';
|
||||
import {
|
||||
RESOURCE_TYPE_RECORD,
|
||||
TOPOLOGY_LAYOUT_RECORD,
|
||||
type TopologyLayoutDirection,
|
||||
} from './constant';
|
||||
|
||||
export const useLayoutTopology = (
|
||||
topologicalData: TopologicalData | undefined,
|
||||
): [RefObject<HTMLDivElement>] => {
|
||||
const topologyFlowDomRef = useRef<HTMLDivElement>(null);
|
||||
const topologyFlowBoxSize = useSize(topologyFlowDomRef);
|
||||
const throttledTopologyFlowBoxSize = useThrottle(topologyFlowBoxSize);
|
||||
const { setCenter } = useReactFlow();
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (topologicalData && throttledTopologyFlowBoxSize) {
|
||||
const { nodeInternals } = store.getState();
|
||||
const nodes = Array.from(nodeInternals).map(([, node]) => node);
|
||||
if (nodes.length > 0) {
|
||||
const node = nodes[0];
|
||||
const zoom = 0.7;
|
||||
const { height, width } = throttledTopologyFlowBoxSize;
|
||||
const x = node.position.x + (node.width || 0) / 2 + (width / 2) * zoom;
|
||||
const y =
|
||||
node.position.y + (node.height || 0) / 2 + (height / 2) * zoom;
|
||||
setCenter(x, y, { zoom, duration: 1000 });
|
||||
}
|
||||
}
|
||||
}, [topologicalData, setCenter, store, throttledTopologyFlowBoxSize]);
|
||||
|
||||
return [topologyFlowDomRef];
|
||||
};
|
||||
|
||||
const notShowTopo = (topoInfo: TopoInfo | undefined): topoInfo is undefined => {
|
||||
if (!topoInfo) {
|
||||
return true;
|
||||
}
|
||||
if (!topoInfo.nodes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!topoInfo.topo_type || topoInfo.topo_type === TopoType.AgentFlow) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useGenerateTopology = (
|
||||
data: UseGenerateTopologyHookData,
|
||||
): [boolean, TopologicalData | undefined] => {
|
||||
const { botId, entityId, spaceId, dataSource, selectedSpanId } = data;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [topologicalData, setTopologicalData] = useState<TopologicalData>();
|
||||
|
||||
// 静态topo接口原始数据及计算数据缓存
|
||||
const staticTopologyDataRef = useRef<Record<string, StaticTopologyDataCache>>(
|
||||
{},
|
||||
);
|
||||
// 某个span最近的上游可查询到topo的span节点缓存
|
||||
const nearestTopologyRootSpanMapRef = useRef<DynamicNodeMap>({});
|
||||
|
||||
const resetStatus = () => {
|
||||
setTopologicalData(undefined);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const fetchStaticTopologyData = async (req: GetTopoInfoReq) => {
|
||||
const resp = await dpManageApi.GetTopoInfo({
|
||||
...req,
|
||||
});
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
const getOriginDynamicData = (): DynamicTopologyData | undefined => {
|
||||
const { type, spanData = [] } = dataSource;
|
||||
if (type === DataSourceTypeEnum.SpanData) {
|
||||
const traceTree = buildTraceTree(spanData, false);
|
||||
return extractOriginDynamicNodeMap(traceTree, botId || entityId || '');
|
||||
} else {
|
||||
// TraceId类型暂不实现
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
try {
|
||||
if (!selectedSpanId) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
/**
|
||||
* Step 1
|
||||
* 获取动态tracing tree数据,找到当前选中节点
|
||||
*/
|
||||
const originDynamicData = getOriginDynamicData();
|
||||
const currentSelectedSpanNode =
|
||||
originDynamicData?.originDynamicNodeMap?.[selectedSpanId];
|
||||
|
||||
if (!originDynamicData || !currentSelectedSpanNode) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const getNearestTopologyRootSpanNode = () => {
|
||||
// 命中缓存
|
||||
if (nearestTopologyRootSpanMapRef.current[selectedSpanId]) {
|
||||
return nearestTopologyRootSpanMapRef.current[selectedSpanId];
|
||||
}
|
||||
const node = findNearestTopologyRootSpanNode(currentSelectedSpanNode);
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
nearestTopologyRootSpanMapRef.current[selectedSpanId] = node;
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Step 2
|
||||
* 从当前节点开始向上找到最近的可绘制topo的节点,并请求得到静态原始topo数据
|
||||
*/
|
||||
const nearestTopologyRootSpanNode = getNearestTopologyRootSpanNode() as
|
||||
| CSpanAttrInvokeAgent
|
||||
| CSpanAttrWorkflow
|
||||
| CSpanAttrUserInput
|
||||
| undefined;
|
||||
|
||||
if (!nearestTopologyRootSpanNode) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const { type } = nearestTopologyRootSpanNode;
|
||||
|
||||
// 当前支持InvokeAgent和Workflow类型,分别取bot和workflow的id以及version
|
||||
const getStaticTopologyMetaData = (): Partial<
|
||||
Pick<GetTopoInfoReq, 'resource_id' | 'version'>
|
||||
> => {
|
||||
if (
|
||||
getTopologyAgentRootType().includes(nearestTopologyRootSpanNode.type)
|
||||
) {
|
||||
return {
|
||||
resource_id: botId || entityId || '',
|
||||
version: nearestTopologyRootSpanNode.extra?.bot_version,
|
||||
};
|
||||
} else {
|
||||
const typedNearestTopologyRootSpanNode =
|
||||
nearestTopologyRootSpanNode as CSpanAttrWorkflow;
|
||||
return {
|
||||
resource_id: typedNearestTopologyRootSpanNode.extra?.workflow_id,
|
||||
version: typedNearestTopologyRootSpanNode.extra?.workflow_version,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { resource_id, version } = getStaticTopologyMetaData();
|
||||
|
||||
if (!resource_id || !version) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
const processedGetTopoInfoReq: ProcessedGetTopoInfoReq = {
|
||||
space_id: spaceId,
|
||||
env: Env.Online,
|
||||
resource_type: RESOURCE_TYPE_RECORD[type] as ResourceType,
|
||||
resource_id: resource_id ?? '',
|
||||
version: version ?? '',
|
||||
};
|
||||
const staticTopologyDataMapKey = generateStaticTopologyDataMapKey(
|
||||
processedGetTopoInfoReq,
|
||||
);
|
||||
|
||||
const staticTopologyDataCache = staticTopologyDataRef.current[
|
||||
staticTopologyDataMapKey
|
||||
] as StaticTopologyDataCache | undefined;
|
||||
|
||||
const topoInfo =
|
||||
// 优先从缓存读取
|
||||
staticTopologyDataCache?.topoInfoMap ??
|
||||
(await fetchStaticTopologyData(processedGetTopoInfoReq));
|
||||
|
||||
if (notShowTopo(topoInfo)) {
|
||||
resetStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3
|
||||
* 过滤出当前静态topo节点中需要展示动态调用链路的节点
|
||||
*/
|
||||
const topoMetaInfo =
|
||||
// 优先从缓存读取
|
||||
staticTopologyDataCache?.topoMetaInfo ??
|
||||
generateTopologyMetaInfo(topoInfo);
|
||||
|
||||
const upstreamNodeMap = staticTopologyDataCache?.upstreamNodeMap ?? {};
|
||||
|
||||
// 判断当前节点自身是否有topo信息(即当前所需要展示的topo的根节点)
|
||||
const isSelectedNodeTopologyRoot =
|
||||
currentSelectedSpanNode.id === nearestTopologyRootSpanNode.id;
|
||||
|
||||
const currentSelectedSpanNodeTopoNodeId =
|
||||
topoMetaInfo.nodeIdMap[
|
||||
getNodeResourceId(currentSelectedSpanNode, botId || entityId || '')
|
||||
];
|
||||
|
||||
// 如果当前节点为topo根节点,那么展示所有动态节点信息;
|
||||
// 否则,过滤出当前节点在静态topo中的所有上游节点,只对这些上游节点进行展示
|
||||
const currentDynamicNodeMap = isSelectedNodeTopologyRoot
|
||||
? originDynamicData.dynamicNodeMap
|
||||
: filterObjectByKeys(
|
||||
originDynamicData.dynamicNodeMap,
|
||||
[
|
||||
currentSelectedSpanNodeTopoNodeId,
|
||||
...getAllUpstreamTopologyNodeIds(
|
||||
currentSelectedSpanNodeTopoNodeId,
|
||||
topoMetaInfo.topoGraph,
|
||||
upstreamNodeMap,
|
||||
),
|
||||
].map(nodeId => topoMetaInfo.resourceIdMap[nodeId]),
|
||||
);
|
||||
|
||||
const layoutDirection = TOPOLOGY_LAYOUT_RECORD[
|
||||
type
|
||||
] as TopologyLayoutDirection;
|
||||
|
||||
/**
|
||||
* Step 4
|
||||
* 补齐动态节点信息 & 布局信息到静态topo
|
||||
*/
|
||||
const originalTopologicalData = completeDynamicTopologyInfo(
|
||||
topoInfo,
|
||||
currentDynamicNodeMap,
|
||||
layoutDirection,
|
||||
);
|
||||
|
||||
const layoutTopologicalData = getLayoutedMeta(
|
||||
originalTopologicalData,
|
||||
layoutDirection,
|
||||
);
|
||||
|
||||
// 存入缓存
|
||||
staticTopologyDataRef.current[staticTopologyDataMapKey] = {
|
||||
topoInfoMap: topoInfo,
|
||||
topoMetaInfo,
|
||||
upstreamNodeMap,
|
||||
};
|
||||
|
||||
setTopologicalData(layoutTopologicalData);
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch
|
||||
} catch (e) {
|
||||
resetStatus();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [botId, entityId, spaceId, dataSource, selectedSpanId]);
|
||||
|
||||
useEffect(
|
||||
// 销毁时清空缓存
|
||||
() => () => {
|
||||
staticTopologyDataRef.current = {
|
||||
topoInfoMap: {},
|
||||
upstreamNodeMap: {},
|
||||
};
|
||||
nearestTopologyRootSpanMapRef.current = {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return [loading, topologicalData];
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
.topology-flow {
|
||||
.topology-flow-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:global(.semi-spin-children) {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.topology-flow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.topology-flow-container-flow {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.topology-flow_default {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
padding: 24px;
|
||||
}
|
||||
@@ -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 ReactFlow, { ReactFlowProvider } from 'reactflow';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Spin } from '@coze-arch/bot-semi';
|
||||
|
||||
import { type TopologyFlowProps } from './typing';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useGenerateTopology, useLayoutTopology } from './hook';
|
||||
import { CUSTOM_EDGES, CUSTOM_NODES } from './constant/flow';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const TopologyFlowContent = (props: TopologyFlowProps) => {
|
||||
const { style, className, renderHeader, ...restProps } = props;
|
||||
|
||||
// 计算topo数据
|
||||
const [loading, topologicalData] = useGenerateTopology({
|
||||
...restProps,
|
||||
});
|
||||
|
||||
// 每次topo数据变更后,计算topo布局信息
|
||||
const [topologyFlowDomRef] = useLayoutTopology(topologicalData);
|
||||
|
||||
// 渲染外部自定义header实现(带有业务语义)
|
||||
const topologyHeader = useMemo(() => {
|
||||
if (!renderHeader || !topologicalData) {
|
||||
return null;
|
||||
}
|
||||
return renderHeader(topologicalData.topoType);
|
||||
}, [renderHeader, topologicalData]);
|
||||
|
||||
return topologicalData ? (
|
||||
<div
|
||||
className={classNames(
|
||||
s['topology-flow'],
|
||||
className ?? s['topology-flow_default'],
|
||||
)}
|
||||
style={style}
|
||||
ref={topologyFlowDomRef}
|
||||
>
|
||||
{loading ? (
|
||||
<div className={s['topology-flow-loading']}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<div className={s['topology-flow-container']}>
|
||||
{topologyHeader}
|
||||
<div className={s['topology-flow-container-flow']}>
|
||||
<ReactFlow
|
||||
// @ts-expect-error 使用number类型枚举SpanType作为自定义type,可忽略报错
|
||||
nodes={topologicalData.nodes}
|
||||
edges={topologicalData.edges}
|
||||
nodeTypes={CUSTOM_NODES}
|
||||
edgeTypes={CUSTOM_EDGES}
|
||||
proOptions={{
|
||||
hideAttribution: true,
|
||||
}}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const TopologyFlow = (props: TopologyFlowProps) => (
|
||||
<ReactFlowProvider>
|
||||
<TopologyFlowContent {...props} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default TopologyFlow;
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 Node, type Edge } from 'reactflow';
|
||||
import { type CSSProperties } from 'react';
|
||||
|
||||
import { type SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
import { type LogBizScene } from '@coze-arch/bot-api/ob_data';
|
||||
import {
|
||||
type TopoInfo,
|
||||
type GetTopoInfoReq,
|
||||
type TopoType,
|
||||
} from '@coze-arch/bot-api/dp_manage_api';
|
||||
|
||||
import { type SpanNode } from '../../utils/cspan-graph';
|
||||
import { type DataSource } from '../../typings/graph';
|
||||
import { type TopologyLayoutDirection } from './constant';
|
||||
|
||||
export type ProcessedGetTopoInfoReq = Omit<GetTopoInfoReq, 'Base'>;
|
||||
|
||||
export interface TopologyFlowProps {
|
||||
spaceId: string;
|
||||
botId?: string;
|
||||
entityId?: string;
|
||||
entityType?: LogBizScene;
|
||||
dataSource: DataSource;
|
||||
selectedSpanId?: string;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
renderHeader?: (topologyType: TopoType) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface UseGenerateTopologyHookData {
|
||||
spaceId: string;
|
||||
botId?: string;
|
||||
entityId?: string;
|
||||
entityType?: LogBizScene;
|
||||
dataSource: DataSource;
|
||||
selectedSpanId?: string;
|
||||
}
|
||||
|
||||
// @ts-expect-error 使用number类型枚举SpanType作为自定义type,可忽略报错
|
||||
export type TopologicalNode = Node<NodeData, SpanCategory>;
|
||||
|
||||
export type TopologicalEdge = Edge<EdgeData>;
|
||||
|
||||
export interface NodeData {
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
layoutDirection: TopologyLayoutDirection;
|
||||
dynamicSpanNode?: SpanNode;
|
||||
}
|
||||
|
||||
export interface EdgeData {
|
||||
tailDynamicSpanNode?: SpanNode;
|
||||
layoutInfo?: {
|
||||
customSourceX: number;
|
||||
customTargetX: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type DynamicNodeMap = Record<string, SpanNode>;
|
||||
export type DynamicEdgeMap = Record<string, SpanNode>;
|
||||
|
||||
export interface DynamicTopologyData {
|
||||
dynamicNodeMap: DynamicNodeMap;
|
||||
originDynamicNodeMap: DynamicNodeMap;
|
||||
}
|
||||
|
||||
export interface TopologicalData {
|
||||
topoType: TopoType;
|
||||
nodes: TopologicalNode[];
|
||||
edges: TopologicalEdge[];
|
||||
}
|
||||
|
||||
export interface TopologicalLayoutCommonData {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TopologicalLayoutBizData extends TopologicalLayoutCommonData {
|
||||
icon: React.ReactElement;
|
||||
}
|
||||
|
||||
export interface TopologicalStatusData {
|
||||
edgeColor: string;
|
||||
nodeClassName: string;
|
||||
}
|
||||
|
||||
export interface TopologicalBatchNodeExecutionInfo {
|
||||
isBatch: boolean;
|
||||
isError: boolean;
|
||||
errorNumber: number;
|
||||
totalNumber: number;
|
||||
}
|
||||
|
||||
export interface TopoMetaInfo {
|
||||
topoGraph: Record<string, string[]>;
|
||||
resourceIdMap: Record<string, string>;
|
||||
nodeIdMap: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StaticTopologyDataCache {
|
||||
topoInfoMap?: TopoInfo;
|
||||
topoMetaInfo?: TopoMetaInfo;
|
||||
upstreamNodeMap?: Record<string, string[]>;
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* 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 { MarkerType } from 'reactflow';
|
||||
|
||||
import { getFlags } from '@coze-arch/bot-flags';
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
import {
|
||||
TopoType,
|
||||
type TopoInfo,
|
||||
type Node as TopoNode,
|
||||
} from '@coze-arch/bot-api/dp_manage_api';
|
||||
import Dagre from '@dagrejs/dagre';
|
||||
|
||||
import {
|
||||
getSpanTitle as getDynamicSpanTitle,
|
||||
type SpanNode,
|
||||
} from '../../utils/cspan-graph';
|
||||
import {
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpan,
|
||||
type CSpanAttrWorkflow,
|
||||
type CSpanAttrPluginTool,
|
||||
type CSpanAttrKnowledge,
|
||||
type CSpanAttrCondition,
|
||||
type CSPanBatch,
|
||||
type CSpanAttrLLMCall,
|
||||
} from '../../typings/cspan';
|
||||
import { rootBreakSpanId } from '../../constant';
|
||||
import { spanCategoryConfigMap } from '../../config/cspan';
|
||||
import {
|
||||
type DynamicNodeMap,
|
||||
type DynamicTopologyData,
|
||||
type TopologicalData,
|
||||
type TopologicalNode,
|
||||
type TopologicalEdge,
|
||||
type ProcessedGetTopoInfoReq,
|
||||
type TopoMetaInfo,
|
||||
} from './typing';
|
||||
import {
|
||||
NodeEdgeCategory,
|
||||
TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_ADDITIONAL_WIDTH,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_DEFAULT_WIDTH,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_FONT,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_HEIGHT,
|
||||
TOPOLOGY_COMMON_NODE_TEXT_MAX_WIDTH,
|
||||
TOPOLOGY_DEFAULT_NODE_ICON,
|
||||
TOPOLOGY_EDGE_STATUS_MAP,
|
||||
TOPOLOGY_LAYOUT_BIZ_MAP,
|
||||
TopologyEdgeStatus,
|
||||
type TopologyLayoutDirection,
|
||||
} from './constant';
|
||||
|
||||
const assignRecordIfNotExists = <T extends object>(
|
||||
object: T,
|
||||
key: keyof T,
|
||||
value: T[keyof T],
|
||||
) => {
|
||||
if (!object[key]) {
|
||||
object[key] = value;
|
||||
}
|
||||
};
|
||||
|
||||
export const filterObjectByKeys = <T extends object>(
|
||||
object: T,
|
||||
targetList: string[],
|
||||
) =>
|
||||
Object.keys(object)
|
||||
.filter(key => targetList.includes(key))
|
||||
.reduce<T>((pre, cur) => {
|
||||
pre[cur] = object[cur];
|
||||
return pre;
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
}, {} as T);
|
||||
|
||||
/**
|
||||
* 静态topo数据中resource_id根据span类型的不同和不同的字段映射
|
||||
* @param spanNode 当前span
|
||||
* @param entityId
|
||||
* @returns 与静态topo所映射的id
|
||||
*/
|
||||
export const getNodeResourceId = (
|
||||
spanNode: SpanNode,
|
||||
entityId: string,
|
||||
): string => {
|
||||
const { category, id } = spanNode;
|
||||
const workflowNodeId =
|
||||
// 兼容合并后的Batch节点
|
||||
(spanNode as CSPanBatch).workflow_node_id ??
|
||||
(spanNode as CSpanAttrCondition).extra?.workflow_node_id;
|
||||
if (workflowNodeId) {
|
||||
return workflowNodeId;
|
||||
}
|
||||
switch (category) {
|
||||
case SpanCategory.Agent:
|
||||
case SpanCategory.Start:
|
||||
return (spanNode as CSpanAttrInvokeAgent).extra?.agent_id || entityId;
|
||||
case SpanCategory.Workflow:
|
||||
return (spanNode as CSpanAttrWorkflow).extra?.workflow_id || id;
|
||||
case SpanCategory.Plugin:
|
||||
return (spanNode as CSpanAttrPluginTool).extra?.plugin_id || id;
|
||||
case SpanCategory.Knowledge:
|
||||
return (spanNode as CSpanAttrKnowledge).extra?.knowledge_id || id;
|
||||
case SpanCategory.LLMCall:
|
||||
return (spanNode as CSpanAttrLLMCall).extra?.model || id;
|
||||
default:
|
||||
return id;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateStaticTopologyDataMapKey = (
|
||||
info: ProcessedGetTopoInfoReq,
|
||||
) => {
|
||||
const { space_id, resource_id, version, env, resource_type } = info;
|
||||
return `${space_id}-${resource_id}-${version}-${env}-${resource_type}`;
|
||||
};
|
||||
|
||||
// 单 agent 下 trace 里将去除 SpanType.InvokeAgent 节点,topology root 可能为 SpanType.UserInput
|
||||
export const getTopologyAgentRootType = () => {
|
||||
const FLAGS = getFlags();
|
||||
return FLAGS['bot.devops.use_user_input_as_agent']
|
||||
? [SpanType.InvokeAgent, SpanType.UserInput, SpanType.UserInputV2]
|
||||
: [SpanType.InvokeAgent];
|
||||
};
|
||||
|
||||
export const isTopologyRootSpan = (span: CSpan) => {
|
||||
// 只有基础工作流展示拓扑
|
||||
if (span.type === SpanType.Workflow) {
|
||||
return (span as CSpanAttrWorkflow).extra?.workflow_schema_type === 1;
|
||||
}
|
||||
|
||||
return [...getTopologyAgentRootType()].includes(span.type);
|
||||
};
|
||||
|
||||
export const extractOriginDynamicNodeMap = (
|
||||
spanNode: SpanNode,
|
||||
entityId: string,
|
||||
): DynamicTopologyData => {
|
||||
const dynamicNodeMap: DynamicNodeMap = {};
|
||||
const originDynamicNodeMap: DynamicNodeMap = {};
|
||||
const queue = [spanNode];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentNode = queue.shift() as SpanNode;
|
||||
// 过滤掉broken节点
|
||||
if (currentNode.id === rootBreakSpanId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
originDynamicNodeMap[currentNode.id] = currentNode;
|
||||
assignRecordIfNotExists(
|
||||
dynamicNodeMap,
|
||||
getNodeResourceId(currentNode, entityId),
|
||||
currentNode,
|
||||
);
|
||||
|
||||
for (const childNode of currentNode.children ?? []) {
|
||||
queue.push(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dynamicNodeMap,
|
||||
originDynamicNodeMap,
|
||||
};
|
||||
};
|
||||
|
||||
export const findNearestTopologyRootSpanNode = (
|
||||
spanNode?: SpanNode,
|
||||
): SpanNode | undefined => {
|
||||
let currentSpanNode = spanNode;
|
||||
while (currentSpanNode) {
|
||||
if (isTopologyRootSpan(currentSpanNode)) {
|
||||
return currentSpanNode;
|
||||
}
|
||||
currentSpanNode = currentSpanNode.parent;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function findUserInputRootNode(dynamicNodeMap: DynamicNodeMap) {
|
||||
return Object.values(dynamicNodeMap).find(
|
||||
item =>
|
||||
item.type === SpanType.UserInput || item.type === SpanType.UserInputV2,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态节点填充给对应的静态节点和连线,补全其动态运行信息
|
||||
* @param staticTopoInfo
|
||||
* @param dynamicNodeMap 当前所需要展示的动态节点map
|
||||
* @param layoutDirection
|
||||
* @returns
|
||||
*/
|
||||
export const completeDynamicTopologyInfo = (
|
||||
staticTopoInfo: TopoInfo,
|
||||
dynamicNodeMap: DynamicNodeMap,
|
||||
layoutDirection: TopologyLayoutDirection,
|
||||
): TopologicalData => {
|
||||
const FLAGS = getFlags();
|
||||
const {
|
||||
nodes: staticNodes = [],
|
||||
edges: staticEdges = [],
|
||||
topo_type = TopoType.AgentFlow,
|
||||
} = staticTopoInfo;
|
||||
const nodeInfoMap: Record<
|
||||
string,
|
||||
{
|
||||
node: TopoNode;
|
||||
resourceId: string;
|
||||
}
|
||||
> = {};
|
||||
const nodes: TopologicalNode[] = staticNodes.map(item => {
|
||||
const {
|
||||
node_id = '',
|
||||
resource_id = '',
|
||||
resource_kind,
|
||||
resource_name = '',
|
||||
} = item;
|
||||
nodeInfoMap[node_id] = {
|
||||
node: item,
|
||||
resourceId: resource_id,
|
||||
};
|
||||
|
||||
const typedResourceKind: SpanCategory =
|
||||
resource_kind && resource_kind in SpanCategory
|
||||
? Number(resource_kind)
|
||||
: SpanCategory.Unknown;
|
||||
|
||||
let dynamicSpanNode = dynamicNodeMap[resource_id] as SpanNode | undefined;
|
||||
|
||||
// 特化逻辑:单agent场景下节点信息将不包含agent节点,需要使用userInput替换
|
||||
if (
|
||||
typedResourceKind === SpanCategory.Agent &&
|
||||
!dynamicSpanNode &&
|
||||
FLAGS['bot.devops.use_user_input_as_agent']
|
||||
) {
|
||||
dynamicSpanNode = findUserInputRootNode(dynamicNodeMap) as
|
||||
| SpanNode
|
||||
| undefined;
|
||||
}
|
||||
|
||||
let title = '';
|
||||
if (dynamicSpanNode) {
|
||||
title = getDynamicSpanTitle(dynamicSpanNode);
|
||||
} else {
|
||||
title = getStaticSpanTitle(typedResourceKind, resource_name);
|
||||
}
|
||||
|
||||
return {
|
||||
id: node_id,
|
||||
type: typedResourceKind,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
data: {
|
||||
name: title,
|
||||
icon:
|
||||
TOPOLOGY_LAYOUT_BIZ_MAP[typedResourceKind]?.icon ??
|
||||
TOPOLOGY_DEFAULT_NODE_ICON,
|
||||
dynamicSpanNode,
|
||||
layoutDirection,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const edges: TopologicalEdge[] = staticEdges.map(item => {
|
||||
const { edge_id = '', source_node_id = '', target_node_id = '' } = item;
|
||||
|
||||
const sourceNodeInfo = nodeInfoMap[source_node_id];
|
||||
// 特化逻辑:动态tracing中没有workflow_start节点,默认将其下游的动态节点填入
|
||||
const isWorkflowStartNode =
|
||||
Number(sourceNodeInfo?.node?.resource_kind) ===
|
||||
SpanCategory.WorkflowStart;
|
||||
|
||||
let sourceNode: SpanNode | undefined =
|
||||
dynamicNodeMap[sourceNodeInfo?.resourceId];
|
||||
const targetNode: SpanNode | undefined =
|
||||
dynamicNodeMap[nodeInfoMap[target_node_id]?.resourceId];
|
||||
|
||||
// 特化逻辑:单agent场景下节点信息将不包含agent节点,需要使用userInput替换
|
||||
if (
|
||||
sourceNodeInfo.node.resource_kind === SpanCategory.Agent &&
|
||||
!sourceNode &&
|
||||
FLAGS['bot.devops.use_user_input_as_agent']
|
||||
) {
|
||||
sourceNode = findUserInputRootNode(dynamicNodeMap) as
|
||||
| SpanNode
|
||||
| undefined;
|
||||
}
|
||||
|
||||
let tailDynamicSpanNode: SpanNode | undefined;
|
||||
if (isWorkflowStartNode || (sourceNode && targetNode)) {
|
||||
tailDynamicSpanNode = targetNode;
|
||||
}
|
||||
|
||||
const topologyEdgeStatus = getTopologyItemStatus(tailDynamicSpanNode);
|
||||
return {
|
||||
id: edge_id,
|
||||
source: source_node_id,
|
||||
target: target_node_id,
|
||||
type: NodeEdgeCategory.Common,
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
color: TOPOLOGY_EDGE_STATUS_MAP[topologyEdgeStatus].edgeColor,
|
||||
height: 17,
|
||||
width: 17,
|
||||
},
|
||||
data: {
|
||||
tailDynamicSpanNode,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
topoType: topo_type,
|
||||
};
|
||||
};
|
||||
|
||||
const measureTextCanvas = document.createElement('canvas');
|
||||
const measureTextContext = measureTextCanvas.getContext('2d');
|
||||
|
||||
const getTextWidth = (text: string) => {
|
||||
if (!measureTextContext) {
|
||||
return TOPOLOGY_COMMON_NODE_TEXT_DEFAULT_WIDTH;
|
||||
}
|
||||
measureTextContext.font = TOPOLOGY_COMMON_NODE_TEXT_FONT;
|
||||
return (
|
||||
Math.min(
|
||||
Math.round(measureTextContext.measureText(text).width),
|
||||
TOPOLOGY_COMMON_NODE_TEXT_MAX_WIDTH,
|
||||
) + TOPOLOGY_COMMON_NODE_TEXT_ADDITIONAL_WIDTH
|
||||
);
|
||||
};
|
||||
|
||||
const getStaticSpanTitle = (category: SpanCategory, name: string) => {
|
||||
const typeName = spanCategoryConfigMap[category]?.label ?? '';
|
||||
if (name && name !== typeName) {
|
||||
return `${typeName} ${name}`;
|
||||
} else {
|
||||
return typeName;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 进行对原始topo数据的布局和样式处理
|
||||
* @param originTopologicalData
|
||||
* @param layoutDirection
|
||||
* @returns
|
||||
*/
|
||||
export const getLayoutedMeta = (
|
||||
originTopologicalData: TopologicalData,
|
||||
layoutDirection: TopologyLayoutDirection,
|
||||
): TopologicalData => {
|
||||
const graphInstance = new Dagre.graphlib.Graph().setDefaultEdgeLabel(
|
||||
() => ({}),
|
||||
);
|
||||
graphInstance.setGraph({
|
||||
rankdir: layoutDirection,
|
||||
align: 'UL',
|
||||
});
|
||||
const { edges, nodes, topoType } = originTopologicalData;
|
||||
|
||||
edges.forEach(edge => graphInstance.setEdge(edge.source, edge.target));
|
||||
|
||||
nodes.forEach(node => {
|
||||
const { type = SpanCategory.Unknown, data } = node;
|
||||
const { name } = data;
|
||||
|
||||
graphInstance.setNode(node.id, {
|
||||
label: name,
|
||||
height:
|
||||
TOPOLOGY_LAYOUT_BIZ_MAP[type]?.height ??
|
||||
TOPOLOGY_COMMON_NODE_TEXT_HEIGHT,
|
||||
width: getTextWidth(name),
|
||||
});
|
||||
});
|
||||
|
||||
Dagre.layout(graphInstance);
|
||||
|
||||
// 采集节点的定位信息,用于vertical类型的连线绘制时进行定位
|
||||
const nodeXAxisMap: Record<string, number> = {};
|
||||
|
||||
const layoutNodes: TopologicalNode[] = nodes.map(node => {
|
||||
const { x, y } = graphInstance.node(node.id);
|
||||
|
||||
nodeXAxisMap[node.id] = x;
|
||||
|
||||
return { ...node, position: { x, y } };
|
||||
});
|
||||
|
||||
const layoutEdges: TopologicalEdge[] = edges.map(edge => {
|
||||
const { source, target } = edge;
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
layoutInfo: {
|
||||
customSourceX:
|
||||
nodeXAxisMap[source] + TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH,
|
||||
customTargetX:
|
||||
nodeXAxisMap[target] + TOPOLOGY_COMMON_EDGE_OFFSET_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
topoType,
|
||||
nodes: layoutNodes,
|
||||
edges: layoutEdges,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTopologyItemStatus = (spanNode?: SpanNode) => {
|
||||
if (!spanNode) {
|
||||
return TopologyEdgeStatus.STATIC;
|
||||
}
|
||||
if (spanNode.status === SpanStatus.Error) {
|
||||
return TopologyEdgeStatus.ERROR;
|
||||
}
|
||||
return TopologyEdgeStatus.DYNAMIC;
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用静态topo数据生成记录每个节点的上游节点的graph以及id map
|
||||
* @param topoInfo
|
||||
* @returns graph和map的meta信息
|
||||
*/
|
||||
export const generateTopologyMetaInfo = (topoInfo: TopoInfo): TopoMetaInfo => {
|
||||
const { nodes = [], edges = [] } = topoInfo;
|
||||
const resourceIdMap: Record<string, string> = {};
|
||||
const nodeIdMap: Record<string, string> = {};
|
||||
const topoGraph: Record<string, string[]> = {};
|
||||
for (const { node_id = '', resource_id = '' } of nodes) {
|
||||
resourceIdMap[node_id] = resource_id;
|
||||
nodeIdMap[resource_id] = node_id;
|
||||
}
|
||||
for (const { source_node_id = '', target_node_id = '' } of edges) {
|
||||
if (!topoGraph[target_node_id]) {
|
||||
topoGraph[target_node_id] = [];
|
||||
}
|
||||
topoGraph[target_node_id].push(source_node_id);
|
||||
}
|
||||
return {
|
||||
resourceIdMap,
|
||||
nodeIdMap,
|
||||
topoGraph,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询静态topo图中某个节点的所有上游节点,使用DP降低复杂度
|
||||
* @param selectedNodeId 当前span id
|
||||
* @param topoGraph 存有记录每个节点的上游节点的graph
|
||||
* @param upstreamNodeMap 存有记录某个节点所有上游节点的map
|
||||
* @returns 上游所有节点的id list
|
||||
*/
|
||||
export const getAllUpstreamTopologyNodeIds = (
|
||||
selectedNodeId: string,
|
||||
topoGraph: Record<string, string[]>,
|
||||
upstreamNodeMap: Record<string, string[]>,
|
||||
) => {
|
||||
if (upstreamNodeMap[selectedNodeId]) {
|
||||
return upstreamNodeMap[selectedNodeId];
|
||||
}
|
||||
const upstreamNodeIds = topoGraph[selectedNodeId] ?? [];
|
||||
const allUpstreamNodeIds: string[] = [
|
||||
...upstreamNodeIds,
|
||||
...upstreamNodeIds.flatMap(nodeId =>
|
||||
getAllUpstreamTopologyNodeIds(nodeId, topoGraph, upstreamNodeMap),
|
||||
),
|
||||
];
|
||||
upstreamNodeMap[selectedNodeId] = allUpstreamNodeIds;
|
||||
return allUpstreamNodeIds;
|
||||
};
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 { SpanStatus, SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type LabelStyle, type RectStyle } from '../flamethread';
|
||||
import { type TraceFlamethreadProps } from './typing';
|
||||
|
||||
type DefaultProps = Pick<
|
||||
TraceFlamethreadProps,
|
||||
'rectStyle' | 'labelStyle' | 'rowHeight' | 'globalStyle' | 'datazoomDecimals'
|
||||
>;
|
||||
|
||||
export const defaultProps: DefaultProps = {
|
||||
labelStyle: {
|
||||
position: 'inside-left',
|
||||
fontSize: 12,
|
||||
fill: '#1D1C23CC',
|
||||
},
|
||||
rowHeight: 50,
|
||||
globalStyle: {},
|
||||
datazoomDecimals: 1,
|
||||
};
|
||||
|
||||
interface SpanCategoryConfig {
|
||||
[spanCategory: number]:
|
||||
| {
|
||||
rectStyle?: RectStyle;
|
||||
labelStyle?: LabelStyle;
|
||||
name?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export const spanCategoryConfig: SpanCategoryConfig = {
|
||||
[SpanCategory.Unknown]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
},
|
||||
hover: {
|
||||
fill: '#F0F0F5',
|
||||
},
|
||||
select: {
|
||||
fill: '#C6C6CD',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F1F2FD',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D9DCFA',
|
||||
},
|
||||
select: {
|
||||
fill: '#B4BAF6',
|
||||
},
|
||||
},
|
||||
labelStyle: {},
|
||||
name: 'start',
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F6EFFC',
|
||||
},
|
||||
hover: {
|
||||
fill: '#E9D6F9',
|
||||
},
|
||||
select: {
|
||||
fill: '#D1AEF4',
|
||||
},
|
||||
},
|
||||
name: 'invoke agent',
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F7F7FA',
|
||||
},
|
||||
hover: {
|
||||
fill: '#F0F0F5',
|
||||
},
|
||||
select: {
|
||||
fill: '#C6C6CD',
|
||||
},
|
||||
},
|
||||
name: 'invoke llm',
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#EDF9EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D2F3D5',
|
||||
},
|
||||
select: {
|
||||
fill: '#CFECAC',
|
||||
},
|
||||
},
|
||||
name: 'invoke workflow',
|
||||
},
|
||||
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#EDF9EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D2F3D5',
|
||||
},
|
||||
select: {
|
||||
fill: '#CFECAC',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#EDF9EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D2F3D5',
|
||||
},
|
||||
select: {
|
||||
fill: '#CFECAC',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[SpanCategory.Plugin]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#F1F2FD',
|
||||
},
|
||||
hover: {
|
||||
fill: '#D9DCFA',
|
||||
},
|
||||
select: {
|
||||
fill: '#B4BAF6',
|
||||
},
|
||||
},
|
||||
name: 'invoke plugin',
|
||||
},
|
||||
[SpanCategory.Knowledge]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFEEEF',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFD2D7',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFA5B2',
|
||||
},
|
||||
},
|
||||
name: 'invoke knowledage',
|
||||
},
|
||||
|
||||
[SpanCategory.Code]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#E5F8F7',
|
||||
},
|
||||
hover: {
|
||||
fill: '#C1F2EF',
|
||||
},
|
||||
select: {
|
||||
fill: '#89E5E0',
|
||||
},
|
||||
},
|
||||
name: 'execute code',
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFFAEB',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFF1CC',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFDF99',
|
||||
},
|
||||
},
|
||||
name: 'if condition',
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFFAEB',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFF1CC',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFDF99',
|
||||
},
|
||||
},
|
||||
name: 'card',
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
rectStyle: {
|
||||
normal: {
|
||||
fill: '#FFFAEB',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFF1CC',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFDF99',
|
||||
},
|
||||
},
|
||||
name: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
interface SpanStatusConfig {
|
||||
[spanStatus: string]: {
|
||||
tooltip?: {
|
||||
fill?: string;
|
||||
};
|
||||
rectStyle?: RectStyle;
|
||||
};
|
||||
}
|
||||
|
||||
export const spanStatusConfig: SpanStatusConfig = {
|
||||
[SpanStatus.Success]: {
|
||||
tooltip: {
|
||||
fill: '#3EC254',
|
||||
},
|
||||
rectStyle: {
|
||||
normal: {
|
||||
stroke: '#1D1C2314',
|
||||
},
|
||||
hover: {},
|
||||
select: {},
|
||||
},
|
||||
},
|
||||
[SpanStatus.Error]: {
|
||||
tooltip: {
|
||||
fill: '#FF441E',
|
||||
},
|
||||
rectStyle: {
|
||||
normal: {
|
||||
stroke: '#1D1C2314',
|
||||
fill: '#FFF3EE',
|
||||
},
|
||||
hover: {
|
||||
fill: '#FFE0D2',
|
||||
},
|
||||
select: {
|
||||
fill: '#FFBDA5',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanStatus.Broken]: {
|
||||
tooltip: {
|
||||
fill: '#FF9600',
|
||||
},
|
||||
},
|
||||
[SpanStatus.Unknown]: {
|
||||
tooltip: {
|
||||
fill: '#6B6B75',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const tooltipStyle = {
|
||||
fill: '#212629',
|
||||
shape: {
|
||||
symbolType: 'square',
|
||||
fill: '#212629',
|
||||
size: 5,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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, type FC, useMemo, useState, useCallback } from 'react';
|
||||
|
||||
import { type IElement } from '@visactor/vgrammar';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import Flamethread, {
|
||||
type LabelText,
|
||||
type RectNode,
|
||||
type RectStyle,
|
||||
type Tooltip,
|
||||
} from '../flamethread';
|
||||
import {
|
||||
getSpanDataByTraceId,
|
||||
getSpanTitle,
|
||||
getStatusLabel,
|
||||
} from '../../utils/cspan-graph';
|
||||
import { getTokens } from '../../utils/cspan';
|
||||
import { DataSourceTypeEnum } from '../../typings/graph';
|
||||
import { type CSpan } from '../../typings/cspan';
|
||||
import { spanData2flamethreadData } from './util';
|
||||
import { type TraceFlamethreadProps } from './typing';
|
||||
import { defaultProps, spanStatusConfig, tooltipStyle } from './config';
|
||||
|
||||
const TraceFlamethread: FC<TraceFlamethreadProps> = props => {
|
||||
const [flamethreadData, setFlamethreadData] = useState<RectNode[]>([]);
|
||||
|
||||
const {
|
||||
dataSource: { type: dataType, spanData, traceId },
|
||||
rectStyle: _rectStyle,
|
||||
labelStyle: _labelStyle,
|
||||
globalStyle: _globalStyle,
|
||||
visibleColumnCount,
|
||||
datazoomDecimals = defaultProps.datazoomDecimals,
|
||||
axisLabelSuffix,
|
||||
selectedSpanId,
|
||||
spanTypeConfigMap,
|
||||
spanStatusConfigMap,
|
||||
disableViewScroll,
|
||||
enableAutoFit,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
// 初始化flamethreadData
|
||||
useEffect(() => {
|
||||
if (dataType === DataSourceTypeEnum.SpanData && spanData) {
|
||||
if (spanData?.length === 0 && flamethreadData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rectNodes = spanData2flamethreadData(spanData);
|
||||
setFlamethreadData(rectNodes);
|
||||
} else if (dataType === DataSourceTypeEnum.TraceId && traceId) {
|
||||
const spans = getSpanDataByTraceId(traceId);
|
||||
const rectNodes = spanData2flamethreadData(spans);
|
||||
setFlamethreadData(rectNodes);
|
||||
}
|
||||
}, [dataType, spanData, traceId]);
|
||||
|
||||
const rectStyle = useMemo((): RectStyle => {
|
||||
const defaultRectStyle = defaultProps.rectStyle;
|
||||
return {
|
||||
normal: Object.assign({}, defaultRectStyle?.normal, _rectStyle?.normal),
|
||||
hover: Object.assign({}, defaultRectStyle?.hover, _rectStyle?.hover),
|
||||
select: Object.assign({}, defaultRectStyle?.select, _rectStyle?.select),
|
||||
};
|
||||
}, [_rectStyle]);
|
||||
|
||||
const labelStyle = useMemo(
|
||||
() => Object.assign({}, _labelStyle, defaultProps.labelStyle),
|
||||
[_labelStyle],
|
||||
);
|
||||
|
||||
const globalStyle = useMemo(
|
||||
() => Object.assign({}, _globalStyle, defaultProps.globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
|
||||
const tooltip: Tooltip = useMemo(
|
||||
() => ({
|
||||
title: {
|
||||
value: (datum: RectNode, element: IElement, params) => ({}),
|
||||
},
|
||||
content: (datum: RectNode, element: IElement, params) => {
|
||||
const { span } = (datum.extra ?? {}) as { span: CSpan };
|
||||
if (!span) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { status, latency } = span;
|
||||
const statusConfig = spanStatusConfig[status];
|
||||
|
||||
const tips = [
|
||||
{
|
||||
key: I18n.t('analytic_query_status'),
|
||||
value: getStatusLabel(span, spanStatusConfigMap),
|
||||
},
|
||||
{
|
||||
key: I18n.t('analytic_query_latency'),
|
||||
value: latency ? `${latency}ms` : '-',
|
||||
},
|
||||
];
|
||||
const { input_tokens: inputTokens, output_tokens: outputTokens } =
|
||||
getTokens(span);
|
||||
if (inputTokens !== undefined && outputTokens !== undefined) {
|
||||
tips.push({
|
||||
key: I18n.t('analytic_query_tokens'),
|
||||
value: `${inputTokens + outputTokens}`,
|
||||
});
|
||||
}
|
||||
|
||||
return tips.map(({ key, value }) => ({
|
||||
key: {
|
||||
text: key,
|
||||
fill: tooltipStyle.fill,
|
||||
},
|
||||
value: {
|
||||
text: value ?? '',
|
||||
fill:
|
||||
key === I18n.t('analytic_query_status')
|
||||
? statusConfig?.tooltip?.fill
|
||||
: tooltipStyle.fill,
|
||||
},
|
||||
shape: tooltipStyle.shape,
|
||||
}));
|
||||
},
|
||||
}),
|
||||
[spanStatusConfigMap],
|
||||
);
|
||||
|
||||
const labelText: LabelText = useCallback(
|
||||
(datum: RectNode, element: IElement, params) => {
|
||||
const { span } = (datum.extra ?? {}) as { span: CSpan };
|
||||
return getSpanTitle(span, spanTypeConfigMap);
|
||||
},
|
||||
[spanTypeConfigMap],
|
||||
);
|
||||
|
||||
return flamethreadData ? (
|
||||
<Flamethread
|
||||
flamethreadData={flamethreadData}
|
||||
tooltip={tooltip}
|
||||
rectStyle={rectStyle}
|
||||
labelStyle={labelStyle}
|
||||
globalStyle={globalStyle}
|
||||
labelText={labelText}
|
||||
datazoomDecimals={datazoomDecimals}
|
||||
visibleColumnCount={visibleColumnCount}
|
||||
axisLabelSuffix={axisLabelSuffix}
|
||||
selectedKey={selectedSpanId}
|
||||
disableViewScroll={disableViewScroll}
|
||||
enableAutoFit={enableAutoFit}
|
||||
onClick={onClick}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default TraceFlamethread;
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 FlamethreadProps } from '../flamethread';
|
||||
import { type DataSource } from '../../typings/graph';
|
||||
import {
|
||||
type SpanStatusConfigMap,
|
||||
type SpanTypeConfigMap,
|
||||
} from '../../typings/config';
|
||||
|
||||
export type TraceFlamethreadProps = {
|
||||
dataSource: DataSource;
|
||||
selectedSpanId?: string;
|
||||
spanTypeConfigMap?: SpanTypeConfigMap;
|
||||
spanStatusConfigMap?: SpanStatusConfigMap;
|
||||
} & Pick<
|
||||
FlamethreadProps,
|
||||
| 'rectStyle'
|
||||
| 'labelStyle'
|
||||
| 'globalStyle'
|
||||
| 'rowHeight'
|
||||
| 'visibleColumnCount'
|
||||
| 'datazoomDecimals'
|
||||
| 'axisLabelSuffix'
|
||||
| 'disableViewScroll'
|
||||
| 'enableAutoFit'
|
||||
| 'onClick'
|
||||
>;
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 { sortBy } from 'lodash-es';
|
||||
import { SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type RectNode, type RectStyle } from '../flamethread';
|
||||
import {
|
||||
buildCallTrees,
|
||||
getBreakSpans,
|
||||
getRootSpan,
|
||||
type SpanNode,
|
||||
} from '../../utils/cspan-graph';
|
||||
import { isVisibleSpan } from '../../utils/cspan';
|
||||
import { type CSpan } from '../../typings/cspan';
|
||||
import { spanCategoryConfig, spanStatusConfig } from './config';
|
||||
|
||||
const genRectStyle = (span: CSpan): RectStyle => {
|
||||
const { status, category = SpanCategory.Unknown } = span;
|
||||
const categoryRectStyle = spanCategoryConfig[category]?.rectStyle;
|
||||
const statusRectStyle = spanStatusConfig[status]?.rectStyle;
|
||||
|
||||
return {
|
||||
normal: Object.assign(
|
||||
{},
|
||||
categoryRectStyle?.normal,
|
||||
statusRectStyle?.normal,
|
||||
),
|
||||
hover: Object.assign({}, categoryRectStyle?.hover, statusRectStyle?.hover),
|
||||
select: Object.assign(
|
||||
{},
|
||||
categoryRectStyle?.select,
|
||||
statusRectStyle?.select,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const genRectNode = (info: {
|
||||
span: CSpan;
|
||||
startSpan: CSpan;
|
||||
rowNo: number;
|
||||
}): RectNode => {
|
||||
const { span, startSpan, rowNo } = info;
|
||||
const start = span.start_time - startSpan.start_time;
|
||||
return {
|
||||
key: span.id,
|
||||
rowNo,
|
||||
start,
|
||||
end: start + span.latency,
|
||||
rectStyle: genRectStyle(span),
|
||||
extra: {
|
||||
span,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const spanData2flamethreadData = (spanData: CSpan[]): RectNode[] => {
|
||||
// 1. 根据spans,组装call trees
|
||||
const callTrees = buildCallTrees(spanData);
|
||||
|
||||
// 2. 生成tartSpan
|
||||
const startSpan: SpanNode = getRootSpan(callTrees, false);
|
||||
|
||||
// 3. 获取 break节点(非start的根节点都是breakSpan)
|
||||
const breakSpans: SpanNode[] = getBreakSpans(callTrees, false);
|
||||
|
||||
let rstSpans: SpanNode[] = [];
|
||||
|
||||
// 前序搜索,确保父节点在前
|
||||
const walk = (spans: SpanNode[]) => {
|
||||
rstSpans = rstSpans.concat(spans);
|
||||
spans.forEach(span => {
|
||||
if (span.children) {
|
||||
walk(span.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (startSpan.children) {
|
||||
walk(startSpan.children);
|
||||
}
|
||||
walk(breakSpans);
|
||||
|
||||
// 过滤掉不显示的span节点
|
||||
rstSpans = rstSpans.filter(span => isVisibleSpan(span));
|
||||
|
||||
// 按start_time稳定排序
|
||||
const sortedSpans = sortBy(rstSpans, o => o.start_time);
|
||||
|
||||
// 添加跟节点
|
||||
sortedSpans.unshift(startSpan);
|
||||
|
||||
const rectNodes: RectNode[] = [];
|
||||
sortedSpans.forEach((span, index) => {
|
||||
rectNodes.push(genRectNode({ span, startSpan, rowNo: index }));
|
||||
});
|
||||
|
||||
return rectNodes;
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
IconSpanAgent,
|
||||
IconSpanCard,
|
||||
IconSpanCode,
|
||||
IconSpanCondition,
|
||||
IconSpanKnowledge,
|
||||
IconSpanVar,
|
||||
IconSpanLLMCall,
|
||||
IconSpanHook,
|
||||
IconSpanMessage,
|
||||
IconSpanPluginTool,
|
||||
IconSpanUnknown,
|
||||
IconSpanWorkflow,
|
||||
IconSpanWorkflowEnd,
|
||||
IconSpanWorkflowStart,
|
||||
IconSpanBMConnector,
|
||||
IconSpanBMParallel,
|
||||
IconSpanBMBatch,
|
||||
} from '@coze-arch/bot-icons';
|
||||
import { SpanStatus, SpanCategory } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type LineStyle } from '../tree';
|
||||
import { type TraceTreeProps } from './typing';
|
||||
|
||||
type DefaultProps = Pick<TraceTreeProps, 'lineStyle' | 'globalStyle'>;
|
||||
|
||||
export const defaultProps: DefaultProps = {
|
||||
lineStyle: {
|
||||
normal: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
hover: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
select: {
|
||||
stroke: '#C6C6CD',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type SpanCategoryConfig = Record<
|
||||
number,
|
||||
| {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
|
||||
export const spanCategoryConfig: SpanCategoryConfig = {
|
||||
[SpanCategory.Unknown]: {
|
||||
icon: <IconSpanUnknown />,
|
||||
title: 'Unknown',
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
title: 'Start',
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
icon: <IconSpanAgent />,
|
||||
title: 'Agent',
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
icon: <IconSpanLLMCall />,
|
||||
title: 'Invoke LLM',
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
title: 'Invoke Workflow',
|
||||
},
|
||||
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
icon: <IconSpanWorkflowStart />,
|
||||
title: 'Workflow Start',
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
icon: <IconSpanWorkflowEnd />,
|
||||
title: 'Workflow End',
|
||||
},
|
||||
|
||||
[SpanCategory.Plugin]: {
|
||||
icon: <IconSpanPluginTool />,
|
||||
title: 'Invoke Plugin',
|
||||
},
|
||||
|
||||
[SpanCategory.Knowledge]: {
|
||||
icon: <IconSpanKnowledge />,
|
||||
title: 'Recall Knowledage',
|
||||
},
|
||||
|
||||
[SpanCategory.Code]: {
|
||||
icon: <IconSpanCode />,
|
||||
title: 'Execute Code',
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
icon: <IconSpanCondition />,
|
||||
title: 'If Condition',
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
icon: <IconSpanCard />,
|
||||
title: 'Card',
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
icon: <IconSpanMessage />,
|
||||
title: 'Message',
|
||||
},
|
||||
[SpanCategory.Variable]: {
|
||||
icon: <IconSpanVar />,
|
||||
title: 'Variable',
|
||||
},
|
||||
[SpanCategory.Hook]: {
|
||||
icon: <IconSpanHook />,
|
||||
title: 'Hook',
|
||||
},
|
||||
[SpanCategory.Batch]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
title: 'Batch',
|
||||
},
|
||||
[SpanCategory.Loop]: {
|
||||
icon: <IconSpanBMBatch />,
|
||||
title: 'Loop',
|
||||
},
|
||||
[SpanCategory.Parallel]: {
|
||||
icon: <IconSpanBMParallel />,
|
||||
title: 'Parallel',
|
||||
},
|
||||
[SpanCategory.Script]: {
|
||||
icon: <IconSpanCode />,
|
||||
title: 'Script',
|
||||
},
|
||||
[SpanCategory.CallFlow]: {
|
||||
icon: <IconSpanWorkflow />,
|
||||
title: 'CallFlow',
|
||||
},
|
||||
[SpanCategory.Connector]: {
|
||||
icon: <IconSpanBMConnector />,
|
||||
title: 'Connector',
|
||||
},
|
||||
};
|
||||
|
||||
interface SpanStatusConfig {
|
||||
[spanStatus: string]: {
|
||||
lineStyle?: LineStyle;
|
||||
};
|
||||
}
|
||||
|
||||
export const spanStatusConfig: SpanStatusConfig = {
|
||||
[SpanStatus.Success]: {},
|
||||
[SpanStatus.Error]: {
|
||||
lineStyle: {
|
||||
normal: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
hover: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
select: {
|
||||
stroke: '#FF441E',
|
||||
},
|
||||
},
|
||||
},
|
||||
[SpanStatus.Broken]: {},
|
||||
[SpanStatus.Unknown]: {},
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
.trace-tree {
|
||||
.trace-tree-node {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
|
||||
font-size: 12px;
|
||||
color: #1D1C23;
|
||||
white-space: nowrap;
|
||||
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:global(.selected) {
|
||||
.title {
|
||||
background: #2E2E381F;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.hover) {
|
||||
.title {
|
||||
background: #2E2E3814;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.error) {
|
||||
.title {
|
||||
color: #FF441E;
|
||||
}
|
||||
|
||||
&:global(.selected) {
|
||||
.title {
|
||||
background: #FFE0D2;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.hover) {
|
||||
.title {
|
||||
background: #FFF3EE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
&:global(.disabled) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { SceneType, usePageJumpService } from '@coze-arch/bot-hooks';
|
||||
|
||||
import Tree, { type TreeNode } from '../tree';
|
||||
import { getSpanDataByTraceId } from '../../utils/cspan-graph';
|
||||
import { DataSourceTypeEnum } from '../../typings/graph';
|
||||
import { spanData2treeData } from './util';
|
||||
import { type WorkflowJumpParams, type TraceTreeProps } from './typing';
|
||||
import { defaultProps } from './config';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const TraceTree: FC<TraceTreeProps> = props => {
|
||||
const [treeData, setTreeData] = useState<TreeNode>();
|
||||
const [hoverNodeKey, setHoverNodeKey] = useState('');
|
||||
const { jump } = usePageJumpService();
|
||||
const {
|
||||
dataSource: { type: dataType, spanData, traceId },
|
||||
spaceId,
|
||||
selectedSpanId,
|
||||
spanTypeConfigMap,
|
||||
indentDisabled,
|
||||
lineStyle: _lineStyle,
|
||||
globalStyle: _globalStyle,
|
||||
className,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const lineStyle = useMemo(
|
||||
() => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
defaultProps.lineStyle?.normal,
|
||||
_lineStyle?.normal,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
defaultProps.lineStyle?.select,
|
||||
_lineStyle?.select,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
defaultProps.lineStyle?.hover,
|
||||
_lineStyle?.hover,
|
||||
),
|
||||
}),
|
||||
[_lineStyle],
|
||||
);
|
||||
|
||||
const globalStyle = useMemo(
|
||||
() => Object.assign({}, defaultProps.globalStyle, _globalStyle),
|
||||
[_globalStyle],
|
||||
);
|
||||
const handleJumpToWorkflow = ({
|
||||
workflowID,
|
||||
executeID,
|
||||
workflowNodeID,
|
||||
workflowVersion,
|
||||
subExecuteID,
|
||||
}: WorkflowJumpParams) => {
|
||||
if (!spaceId) {
|
||||
return;
|
||||
}
|
||||
jump(SceneType.BOT__VIEW__WORKFLOW, {
|
||||
workflowID,
|
||||
spaceID: spaceId,
|
||||
botID: '',
|
||||
executeID,
|
||||
workflowNodeID,
|
||||
workflowVersion,
|
||||
subExecuteID,
|
||||
newWindow: true,
|
||||
});
|
||||
};
|
||||
// 初始化flamethreadData
|
||||
useEffect(() => {
|
||||
if (dataType === DataSourceTypeEnum.SpanData && spanData) {
|
||||
if (spanData?.length === 0 && treeData === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const treeNode = spanData2treeData(spanData, spanTypeConfigMap, {
|
||||
spaceId,
|
||||
onHoverChange: setHoverNodeKey,
|
||||
onJumpToWorkflow: handleJumpToWorkflow,
|
||||
});
|
||||
setTreeData(treeNode);
|
||||
} else if (dataType === DataSourceTypeEnum.TraceId && traceId) {
|
||||
const spans = getSpanDataByTraceId(traceId);
|
||||
const treeNode = spanData2treeData(spans, spanTypeConfigMap, {
|
||||
onHoverChange: setHoverNodeKey,
|
||||
onJumpToWorkflow: handleJumpToWorkflow,
|
||||
});
|
||||
setTreeData(treeNode);
|
||||
}
|
||||
}, [dataType, spanData, traceId, spanTypeConfigMap]);
|
||||
|
||||
return treeData ? (
|
||||
<Tree
|
||||
className={classNames(styles['trace-tree'], className)}
|
||||
treeData={treeData}
|
||||
disableDefaultHover={true}
|
||||
hoverKey={hoverNodeKey}
|
||||
selectedKey={selectedSpanId}
|
||||
indentDisabled={indentDisabled}
|
||||
lineStyle={lineStyle}
|
||||
globalStyle={globalStyle}
|
||||
{...restProps}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default TraceTree;
|
||||
@@ -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 TreeProps } from '../tree';
|
||||
import { type DataSource } from '../../typings/graph';
|
||||
import { type SpanTypeConfigMap } from '../../typings/config';
|
||||
|
||||
export type TraceTreeProps = {
|
||||
dataSource: DataSource;
|
||||
spaceId?: string;
|
||||
selectedSpanId?: string;
|
||||
spanTypeConfigMap?: SpanTypeConfigMap;
|
||||
} & Pick<
|
||||
TreeProps,
|
||||
| 'indentDisabled'
|
||||
| 'lineStyle'
|
||||
| 'globalStyle'
|
||||
| 'onSelect'
|
||||
| 'onClick'
|
||||
| 'onMouseMove'
|
||||
| 'onMouseEnter'
|
||||
| 'onMouseLeave'
|
||||
| 'className'
|
||||
>;
|
||||
|
||||
export interface SpanDetail {
|
||||
isCozeWorkflowNode: boolean;
|
||||
workflowLevel: number; // workflow 层级
|
||||
workflowVersion?: string; // 父节点透传给子节点
|
||||
}
|
||||
|
||||
export interface WorkflowJumpParams {
|
||||
workflowID: string;
|
||||
executeID?: string;
|
||||
workflowNodeID?: string;
|
||||
workflowVersion?: string;
|
||||
subExecuteID?: string;
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* 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 ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozFocus } from '@coze-arch/coze-design/icons';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { Tooltip } from '@coze-arch/bot-semi';
|
||||
import { IconpanNodeDamaged } from '@coze-arch/bot-icons';
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type TreeNode, type TreeNodeExtra } from '../tree';
|
||||
import {
|
||||
buildTraceTree,
|
||||
getSpanTitle,
|
||||
type SpanNode,
|
||||
} from '../../utils/cspan-graph';
|
||||
import { getTokens, getSpanProp } from '../../utils/cspan';
|
||||
import { type CSpan } from '../../typings/cspan';
|
||||
import { type SpanTypeConfigMap } from '../../typings/config';
|
||||
import { rootBreakSpanId } from '../../constant';
|
||||
import { type WorkflowJumpParams, type SpanDetail } from './typing';
|
||||
import { spanStatusConfig, spanCategoryConfig } from './config';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
const genTitleRender = ({
|
||||
spanTypeConfigMap,
|
||||
spanInfoMap,
|
||||
spaceId,
|
||||
onHoverChange,
|
||||
onJumpToWorkflow,
|
||||
}: {
|
||||
spanTypeConfigMap?: SpanTypeConfigMap;
|
||||
spanInfoMap?: Record<string, SpanDetail | undefined>;
|
||||
spaceId?: string;
|
||||
onHoverChange?: (key: string) => void;
|
||||
onJumpToWorkflow?: (params: WorkflowJumpParams) => void;
|
||||
}) => {
|
||||
const titleRender = (nodeData: TreeNodeExtra): ReactNode => {
|
||||
const { selected, unindented, hover, key } = nodeData;
|
||||
const { span } = nodeData?.extra as { span: CSpan };
|
||||
const { status, latency, category = SpanCategory.Unknown } = span;
|
||||
const title = getSpanTitle(span, spanTypeConfigMap);
|
||||
|
||||
const isCozeWorkflow =
|
||||
spanInfoMap?.[span.id]?.isCozeWorkflowNode &&
|
||||
Boolean(getSpanProp(span, 'workflow_id')) &&
|
||||
spaceId;
|
||||
const workflowID = getSpanProp(span, 'workflow_id') as string;
|
||||
const workflowVersion =
|
||||
getSpanProp(span, 'workflow_version')?.toString() ||
|
||||
spanInfoMap?.[span.id]?.workflowVersion;
|
||||
|
||||
const handleJumpToWorkflow = () => {
|
||||
const executeID = getSpanProp(span, 'execute_id') as string | undefined;
|
||||
const workflowNodeID = getSpanProp(span, 'workflow_node_id')?.toString();
|
||||
const subExecuteID = getSpanProp(span, 'sub_execute_id')?.toString();
|
||||
|
||||
onJumpToWorkflow?.({
|
||||
workflowID,
|
||||
executeID,
|
||||
workflowVersion,
|
||||
workflowNodeID,
|
||||
subExecuteID,
|
||||
});
|
||||
};
|
||||
|
||||
const { input_tokens: inputTokens, output_tokens: outputTokens } =
|
||||
getTokens(span);
|
||||
let content = '';
|
||||
|
||||
if (inputTokens !== undefined && outputTokens !== undefined) {
|
||||
const tokensStr = inputTokens + outputTokens;
|
||||
content = `${I18n.t('analytic_query_latency')}: ${latency}ms | ${I18n.t(
|
||||
'analytic_query_tokens',
|
||||
)}: ${tokensStr}`;
|
||||
} else {
|
||||
content = `${I18n.t('analytic_query_latency')}: ${latency}ms`;
|
||||
}
|
||||
const config = spanCategoryConfig[category];
|
||||
|
||||
// 虚拟的break的根节点
|
||||
if (span.id === rootBreakSpanId) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['trace-tree-node'], {
|
||||
selected: false,
|
||||
unindented,
|
||||
hover,
|
||||
error: status === SpanStatus.Error,
|
||||
disabled: true,
|
||||
})}
|
||||
>
|
||||
<IconpanNodeDamaged />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Tooltip position="right" content={content} trigger="hover">
|
||||
<div
|
||||
className={classNames(styles['trace-tree-node'], {
|
||||
selected,
|
||||
unindented,
|
||||
hover,
|
||||
error: status === SpanStatus.Error,
|
||||
})}
|
||||
onMouseEnter={() => onHoverChange?.(key)}
|
||||
onMouseLeave={() => onHoverChange?.('')}
|
||||
>
|
||||
{config?.icon}
|
||||
<span className={styles.title}>{title}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{isCozeWorkflow &&
|
||||
workflowID !== undefined &&
|
||||
workflowVersion !== undefined ? (
|
||||
<Tooltip position="top" content={I18n.t('view_workflow_details')}>
|
||||
<Button
|
||||
icon={<IconCozFocus />}
|
||||
style={{ width: 16, height: 16, marginLeft: 4 }}
|
||||
color="secondary"
|
||||
size="mini"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleJumpToWorkflow();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
return titleRender;
|
||||
};
|
||||
|
||||
export const spanData2treeData = (
|
||||
spanData: CSpan[],
|
||||
spanTypeConfigMap?: SpanTypeConfigMap,
|
||||
options?: {
|
||||
spaceId?: string;
|
||||
onHoverChange?: (key: string) => void;
|
||||
onJumpToWorkflow?: (params: WorkflowJumpParams) => void;
|
||||
},
|
||||
): TreeNode | undefined => {
|
||||
const traceTree = buildTraceTree(spanData);
|
||||
const spanInfoMap = getSpanInfoMap(traceTree);
|
||||
|
||||
const walk = (span: SpanNode): TreeNode => {
|
||||
const lineStyle = spanStatusConfig[span.status]?.lineStyle;
|
||||
|
||||
let treeNode: TreeNode = {
|
||||
key: span.id,
|
||||
title: genTitleRender({ spanTypeConfigMap, spanInfoMap, ...options }),
|
||||
selectEnabled: true,
|
||||
indentDisabled: false,
|
||||
lineStyle,
|
||||
zIndex: span.status === SpanStatus.Error ? 1 : 0,
|
||||
extra: {
|
||||
span,
|
||||
},
|
||||
};
|
||||
// breakSpan节点
|
||||
if (span.id === rootBreakSpanId) {
|
||||
treeNode = {
|
||||
...treeNode,
|
||||
selectEnabled: false,
|
||||
indentDisabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
treeNode.children = span.children?.map(childSpan => walk(childSpan)) ?? [];
|
||||
|
||||
return treeNode;
|
||||
};
|
||||
|
||||
return walk(traceTree);
|
||||
};
|
||||
|
||||
export const getSpanInfoMap = (root: SpanNode) => {
|
||||
const spanInfoMap: Record<string, SpanDetail | undefined> = {};
|
||||
|
||||
const bfs = (node: SpanNode) => {
|
||||
// coze workflow 设置 isCozeWorkflowNode
|
||||
if (
|
||||
node.type === SpanType.Workflow &&
|
||||
getSpanProp(node, 'workflow_schema_type') === 1
|
||||
) {
|
||||
const parentLevel = spanInfoMap[node.parent_id]?.workflowLevel || 0;
|
||||
spanInfoMap[node.id] = {
|
||||
isCozeWorkflowNode: true,
|
||||
workflowLevel: parentLevel + 1,
|
||||
workflowVersion:
|
||||
parentLevel <= 1
|
||||
? getSpanProp(node, 'workflow_version')?.toString() ||
|
||||
spanInfoMap[node.parent_id]?.workflowVersion
|
||||
: undefined,
|
||||
};
|
||||
} else {
|
||||
// coze workflow 的子节点设置 isCozeWorkflowNode
|
||||
const { isCozeWorkflowNode, workflowLevel = 0 } =
|
||||
spanInfoMap[node.parent_id] || {};
|
||||
|
||||
if (isCozeWorkflowNode) {
|
||||
spanInfoMap[node.id] = {
|
||||
isCozeWorkflowNode: true,
|
||||
workflowLevel: workflowLevel + 1,
|
||||
workflowVersion:
|
||||
workflowLevel <= 1
|
||||
? spanInfoMap[node.parent_id]?.workflowVersion
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 递归
|
||||
for (const childNode of node.children || []) {
|
||||
bfs(childNode);
|
||||
}
|
||||
};
|
||||
|
||||
bfs(root);
|
||||
|
||||
return spanInfoMap;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 GlobalStyle, type LineStyle } from './typing';
|
||||
|
||||
export const defaultGlobalStyle: GlobalStyle = {
|
||||
indent: 24,
|
||||
verticalInterval: 16,
|
||||
nodeBoxHeight: 16,
|
||||
offsetX: 8,
|
||||
};
|
||||
|
||||
export const defaultLineStyle: LineStyle = {
|
||||
normal: {
|
||||
stroke: '#ccc',
|
||||
strokeDasharray: '[]',
|
||||
strokeWidth: 2,
|
||||
lineRadius: 6,
|
||||
lineGap: 0,
|
||||
},
|
||||
select: {
|
||||
stroke: '#333',
|
||||
},
|
||||
hover: {
|
||||
stroke: '#d25e5a',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
.tree {
|
||||
display: flex;
|
||||
// padding: 10px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
// background-color: #f1f1f1;
|
||||
|
||||
.tree-container {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
|
||||
.tree-path-list {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tree-node-list {
|
||||
position: relative;
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.tree-node-box {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
// .tree-node-icon {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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-magic-numbers -- 本组件中会有很多位置计算的数字,无须处理*/
|
||||
import { type FC, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { isFunction, mergeWith } from 'lodash-es';
|
||||
|
||||
import { flattenTreeData } from './util';
|
||||
import type {
|
||||
TreeProps,
|
||||
TreeNode,
|
||||
TreeNodeExtra,
|
||||
MouseEventParams,
|
||||
Line,
|
||||
LineStyle,
|
||||
GlobalStyle,
|
||||
} from './typing';
|
||||
import { defaultGlobalStyle, defaultLineStyle } from './config';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export type {
|
||||
TreeProps,
|
||||
TreeNode,
|
||||
TreeNodeExtra,
|
||||
MouseEventParams,
|
||||
LineStyle,
|
||||
GlobalStyle,
|
||||
};
|
||||
|
||||
const Tree: FC<TreeProps> = ({
|
||||
treeData,
|
||||
selectedKey,
|
||||
disableDefaultHover,
|
||||
hoverKey: customHoverKey,
|
||||
indentDisabled = false,
|
||||
lineStyle: gLineStyle,
|
||||
globalStyle,
|
||||
className,
|
||||
onMouseMove,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onClick,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [hoverKey, setHoverKey] = useState<string>('');
|
||||
|
||||
const controlledHoverKey = disableDefaultHover ? customHoverKey : hoverKey;
|
||||
|
||||
const { indent, verticalInterval, nodeBoxHeight, offsetX } = useMemo(
|
||||
() =>
|
||||
Object.assign(
|
||||
{},
|
||||
defaultGlobalStyle,
|
||||
globalStyle,
|
||||
) as Required<GlobalStyle>,
|
||||
[globalStyle],
|
||||
);
|
||||
|
||||
/**
|
||||
* 使得指定的selectKey的Line置于顶层。
|
||||
* 通过调整line顺序,来实现z-index效果:key为${selectKey}的line在最上层
|
||||
*/
|
||||
const adjustLineOrder = useCallback(
|
||||
(lines: Line[]): Line[] => {
|
||||
let selectedLine, hoverLine;
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (line.endNode.key === selectedKey) {
|
||||
selectedLine = lines.splice(i, 1)[0];
|
||||
} else if (line.endNode.key === controlledHoverKey) {
|
||||
hoverLine = lines.splice(i, 1)[0];
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// 支持根据zIndex控制高度
|
||||
lines.sort((lineA, lineB) => {
|
||||
const zIndexA = lineA.endNode.zIndex ?? -1;
|
||||
const zIndexB = lineB.endNode.zIndex ?? -1;
|
||||
|
||||
if (zIndexA > zIndexB) {
|
||||
return 1;
|
||||
} else if (zIndexA < zIndexB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedLine) {
|
||||
lines.push(selectedLine);
|
||||
}
|
||||
if (hoverLine) {
|
||||
lines.push(hoverLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
},
|
||||
[selectedKey, controlledHoverKey],
|
||||
);
|
||||
|
||||
const genLineStyle = useCallback(
|
||||
(lineStyle?: LineStyle): LineStyle => ({
|
||||
normal: Object.assign(
|
||||
{},
|
||||
defaultLineStyle?.normal,
|
||||
gLineStyle?.normal,
|
||||
lineStyle?.normal,
|
||||
),
|
||||
select: Object.assign(
|
||||
{},
|
||||
defaultLineStyle?.select,
|
||||
gLineStyle?.select,
|
||||
lineStyle?.select,
|
||||
),
|
||||
hover: Object.assign(
|
||||
{},
|
||||
defaultLineStyle?.hover,
|
||||
gLineStyle?.hover,
|
||||
lineStyle?.hover,
|
||||
),
|
||||
}),
|
||||
[gLineStyle],
|
||||
);
|
||||
|
||||
/**
|
||||
* 根据line信息生成svg path。 colNo, rowNum都从0开始
|
||||
*/
|
||||
const genSvgPath = useCallback(
|
||||
(line: Line): string => {
|
||||
const {
|
||||
startNode: { colNo: startColNo, rowNo: startRowNo },
|
||||
endNode: { colNo: endColNo, rowNo: endRowNo, lineStyle },
|
||||
} = line;
|
||||
|
||||
const { normal: normalLineStyle = {} } = genLineStyle(lineStyle);
|
||||
|
||||
const { lineRadius = 0, lineGap = 0 } = normalLineStyle;
|
||||
const nodeHeight = nodeBoxHeight + verticalInterval;
|
||||
|
||||
// 起始点
|
||||
const startX = startColNo * indent + offsetX;
|
||||
const startY =
|
||||
startRowNo * nodeHeight + (nodeBoxHeight + verticalInterval / 2);
|
||||
|
||||
if (startColNo === endColNo) {
|
||||
// 竖线的长度
|
||||
const lineASize =
|
||||
(endRowNo - startRowNo - 1) * nodeHeight + verticalInterval;
|
||||
// 移动到起始点
|
||||
const moveToStartPoint = `M ${startX} ${startY + lineGap}`;
|
||||
// 竖线
|
||||
const lineA = `L ${startX} ${startY + lineASize}`;
|
||||
return `${moveToStartPoint} ${lineA}`;
|
||||
} else {
|
||||
// 竖线的长度
|
||||
const lineASize =
|
||||
(endRowNo - startRowNo - 1) * nodeHeight +
|
||||
verticalInterval / 2 +
|
||||
nodeHeight / 2 -
|
||||
lineRadius;
|
||||
// 横线的长度
|
||||
const lineBSize =
|
||||
(endColNo - startColNo) * indent - offsetX - lineRadius;
|
||||
// 结束点的坐标
|
||||
const endX = startX + lineBSize + lineRadius;
|
||||
const endY = startY + lineASize + lineRadius;
|
||||
|
||||
// 移动到起始点
|
||||
const moveToStartPoint = `M ${startX} ${startY + lineGap}`;
|
||||
// 竖线
|
||||
const lineA = `L ${startX} ${startY + lineASize}`;
|
||||
// 二次贝塞尔曲线
|
||||
const qbc = `Q ${startX} ${endY} ${startX + lineRadius} ${endY}`;
|
||||
// 横线
|
||||
const lineB = `L ${endX - lineGap} ${endY}`;
|
||||
return `${moveToStartPoint} ${lineA} ${qbc} ${lineB}`;
|
||||
}
|
||||
},
|
||||
[genLineStyle, indent, nodeBoxHeight, offsetX, verticalInterval],
|
||||
);
|
||||
|
||||
const genLineAttrs = useCallback(
|
||||
(nodeKey: string, lineStyle: LineStyle) => {
|
||||
if (controlledHoverKey !== selectedKey) {
|
||||
if (nodeKey === controlledHoverKey) {
|
||||
return mergeWith({}, lineStyle.normal, lineStyle.hover);
|
||||
}
|
||||
if (nodeKey === selectedKey) {
|
||||
return mergeWith({}, lineStyle.normal, lineStyle.select);
|
||||
}
|
||||
return lineStyle.normal;
|
||||
} else {
|
||||
if (nodeKey === controlledHoverKey) {
|
||||
return mergeWith(
|
||||
{},
|
||||
lineStyle.normal,
|
||||
lineStyle.select,
|
||||
lineStyle.hover,
|
||||
);
|
||||
} else {
|
||||
return lineStyle.normal;
|
||||
}
|
||||
}
|
||||
},
|
||||
[controlledHoverKey, selectedKey],
|
||||
);
|
||||
|
||||
const { nodes, lines: orgLines } = flattenTreeData(treeData, {
|
||||
indentDisabled,
|
||||
});
|
||||
const lines = adjustLineOrder(orgLines);
|
||||
|
||||
return (
|
||||
<div className={`${styles.tree} ${className ?? ''}`}>
|
||||
<div
|
||||
className={styles['tree-container']}
|
||||
style={{ marginTop: -verticalInterval / 2 }}
|
||||
>
|
||||
<div className={styles['tree-path-list']}>
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
{lines.map((line, index) => {
|
||||
const path = genSvgPath(line);
|
||||
const { lineStyle } = line.endNode;
|
||||
const lineStyle0 = genLineStyle(lineStyle);
|
||||
const attrs = genLineAttrs(line.endNode.key, lineStyle0);
|
||||
|
||||
return (
|
||||
<path
|
||||
d={path}
|
||||
stroke={attrs?.stroke}
|
||||
strokeWidth={attrs?.strokeWidth}
|
||||
strokeDasharray={attrs?.strokeDasharray}
|
||||
fill="none"
|
||||
// strokeLinecap="round"
|
||||
key={line.endNode.key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
<div className={styles['tree-node-list']}>
|
||||
{nodes.map(node => {
|
||||
const { key, title, selectEnabled = true, colNo } = node;
|
||||
const nodeExtra: TreeNodeExtra = {
|
||||
...node,
|
||||
selected: selectedKey === key,
|
||||
hover: controlledHoverKey === key,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles['tree-node']}
|
||||
style={{
|
||||
paddingTop: verticalInterval / 2,
|
||||
paddingBottom: verticalInterval / 2,
|
||||
}}
|
||||
key={node.key}
|
||||
>
|
||||
<div
|
||||
className={styles['tree-node-box']}
|
||||
style={{
|
||||
marginLeft: colNo * indent,
|
||||
height: nodeBoxHeight,
|
||||
}}
|
||||
onClick={event => {
|
||||
if (selectEnabled) {
|
||||
onSelect?.({ node: nodeExtra });
|
||||
}
|
||||
onClick?.({ event, node: nodeExtra });
|
||||
}}
|
||||
onMouseMove={event => {
|
||||
onMouseMove?.({ event, node: nodeExtra });
|
||||
}}
|
||||
onMouseEnter={event => {
|
||||
if (selectEnabled) {
|
||||
setHoverKey(key);
|
||||
}
|
||||
onMouseEnter?.({
|
||||
event,
|
||||
node: { ...nodeExtra, hover: true },
|
||||
});
|
||||
}}
|
||||
onMouseLeave={event => {
|
||||
if (selectEnabled) {
|
||||
setHoverKey('');
|
||||
}
|
||||
onMouseLeave?.({
|
||||
event,
|
||||
node: { ...nodeExtra, hover: false },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isFunction(title) ? title(nodeExtra) : title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tree;
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 ReactNode, type SVGAttributes } from 'react';
|
||||
|
||||
export type LineAttrs = Pick<
|
||||
SVGAttributes<unknown>,
|
||||
'stroke' | 'strokeDasharray' | 'strokeWidth'
|
||||
> & {
|
||||
lineRadius?: number; // line圆角半径 注意:这个数值不要大于 indent/2
|
||||
lineGap?: number; // line距离box的gap
|
||||
};
|
||||
|
||||
export interface LineStyle {
|
||||
normal?: LineAttrs;
|
||||
select?: LineAttrs;
|
||||
hover?: LineAttrs;
|
||||
}
|
||||
|
||||
// tree-node-box
|
||||
export interface TreeNode {
|
||||
key: string;
|
||||
title: ReactNode | ((nodeData: TreeNodeExtra) => ReactNode);
|
||||
selectEnabled?: boolean; // 默认值 true
|
||||
indentDisabled?: boolean; // 关闭缩进。 仅针对如下场景生效:子节点中的最后一个节点
|
||||
lineStyle?: LineStyle; // 当指定了此属性时,会覆盖全局的lineStyle
|
||||
children?: TreeNode[];
|
||||
linePath?: PathEnum[];
|
||||
zIndex?: number;
|
||||
// 其他字段,会透传
|
||||
extra?: unknown;
|
||||
}
|
||||
|
||||
export enum PathEnum {
|
||||
Hidden = 0,
|
||||
Show = 1,
|
||||
Active = 2,
|
||||
}
|
||||
|
||||
export type TreeNodeExtra = Omit<TreeNode, 'children'> & {
|
||||
colNo: number;
|
||||
rowNo: number;
|
||||
unindented: boolean; // 相对于父节点,是否未缩进
|
||||
selected: boolean; // 是否被选中
|
||||
hover: boolean; // 是否hover
|
||||
};
|
||||
|
||||
// 拉平后的TreeNode信息
|
||||
export type TreeNodeFlatten = Omit<TreeNodeExtra, 'selected' | 'hover'>;
|
||||
|
||||
export interface Line {
|
||||
startNode: TreeNodeFlatten;
|
||||
endNode: TreeNodeFlatten;
|
||||
}
|
||||
|
||||
export interface GlobalStyle {
|
||||
indent?: number; // 父节点和子节点的缩进距离
|
||||
verticalInterval?: number; // node节点的垂直间距
|
||||
nodeBoxHeight?: number; // node-box节点的高度
|
||||
offsetX?: number;
|
||||
}
|
||||
|
||||
export interface MouseEventParams {
|
||||
event: React.MouseEvent<HTMLDivElement>;
|
||||
node: TreeNodeExtra;
|
||||
}
|
||||
|
||||
export interface TreeProps {
|
||||
treeData: TreeNode;
|
||||
selectedKey?: string;
|
||||
hoverKey?: string;
|
||||
disableDefaultHover?: boolean;
|
||||
indentDisabled?: boolean; // 关闭缩进。 仅针对如下场景生效:最后一个节点
|
||||
lineStyle?: LineStyle;
|
||||
globalStyle?: GlobalStyle;
|
||||
className?: string;
|
||||
|
||||
onSelect?: (info: Pick<MouseEventParams, 'node'>) => void;
|
||||
onClick?: (info: MouseEventParams) => void;
|
||||
onMouseMove?: (info: MouseEventParams) => void;
|
||||
onMouseEnter?: (info: MouseEventParams) => void;
|
||||
onMouseLeave?: (info: MouseEventParams) => void;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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 { omit } from 'lodash-es';
|
||||
|
||||
import { type TreeNodeFlatten, type TreeNode, type Line } from './typing';
|
||||
|
||||
/**
|
||||
* 基于TreeData生成:
|
||||
*
|
||||
* @param treeData tree原始数据
|
||||
* @param options.indentDisabled 是否取消缩进。仅针对下述场景有效:异常节点+最后一个节点
|
||||
*
|
||||
* @returns
|
||||
* 1. nodes, 拉平后的node节点信息
|
||||
* 2. lines, 用于将node进行连接
|
||||
*/
|
||||
export const flattenTreeData = (
|
||||
treeData: TreeNode,
|
||||
options: {
|
||||
indentDisabled: boolean;
|
||||
},
|
||||
): { nodes: TreeNodeFlatten[]; lines: Line[] } => {
|
||||
const nodes: TreeNodeFlatten[] = [];
|
||||
const lines: Line[] = [];
|
||||
const walk = (
|
||||
node: TreeNode,
|
||||
nodeColNo: number,
|
||||
fatherNodeFlatten?: TreeNodeFlatten,
|
||||
) => {
|
||||
const nodeFlatten: TreeNodeFlatten = {
|
||||
...omit(node, ['children']),
|
||||
colNo: nodeColNo,
|
||||
rowNo: nodes.length,
|
||||
unindented: fatherNodeFlatten?.colNo === nodeColNo, // 未缩进
|
||||
};
|
||||
nodes.push(nodeFlatten);
|
||||
if (fatherNodeFlatten !== undefined) {
|
||||
lines.push({
|
||||
startNode: fatherNodeFlatten,
|
||||
endNode: nodeFlatten,
|
||||
});
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
const childNodes = node.children;
|
||||
|
||||
childNodes.forEach((childNode, index) => {
|
||||
// 取消缩进。 生效场景:异常节点+最后一个节点
|
||||
const indentDisabled =
|
||||
childNode.indentDisabled ?? options.indentDisabled;
|
||||
if (indentDisabled && childNodes.length - 1 === index) {
|
||||
walk(childNode, nodeColNo, nodeFlatten);
|
||||
} else {
|
||||
walk(childNode, nodeColNo + 1, nodeFlatten);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
walk(treeData, 0);
|
||||
return { nodes, lines };
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* 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 {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
import {
|
||||
SpanCategoryConfigMap,
|
||||
SpanStatusConfigMap,
|
||||
SpanTypeConfigMap,
|
||||
} from '../typings/config';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { StreamingOutputStatus } from '../typings/cspan';
|
||||
|
||||
export const spanTypeConfigMap: SpanTypeConfigMap = {
|
||||
[SpanType.Unknown]: {
|
||||
label: I18n.t('analytic_query_subtype_value_unknown'),
|
||||
},
|
||||
[SpanType.UserInput]: {
|
||||
label: I18n.t('analytic_query_subtype_value_userinput'),
|
||||
},
|
||||
[SpanType.UserInputV2]: {
|
||||
label: I18n.t('analytic_query_subtype_value_userinput'),
|
||||
},
|
||||
[SpanType.ThirdParty]: {
|
||||
label: I18n.t('analytic_query_subtype_value_thirdparty'),
|
||||
},
|
||||
[SpanType.ScheduledTasks]: {
|
||||
label: I18n.t('analytic_query_subtype_value_scheduledtasks'),
|
||||
},
|
||||
[SpanType.OpenDialog]: {
|
||||
label: I18n.t('analytic_query_subtype_value_opendialog'),
|
||||
},
|
||||
[SpanType.InvokeAgent]: {
|
||||
label: I18n.t('analytic_query_subtype_value_invokeagent'),
|
||||
},
|
||||
[SpanType.RestartAgent]: {
|
||||
label: I18n.t('analytic_query_subtype_value_restartagent'),
|
||||
},
|
||||
[SpanType.SwitchAgent]: {
|
||||
label: I18n.t('analytic_query_subtype_value_switchagent'),
|
||||
},
|
||||
[SpanType.LLMCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmcall'),
|
||||
},
|
||||
[SpanType.LLMBatchCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmbatchcall'),
|
||||
},
|
||||
[SpanType.Workflow]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflow'),
|
||||
},
|
||||
[SpanType.WorkflowStart]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflowstart'),
|
||||
},
|
||||
[SpanType.WorkflowEnd]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflowend'),
|
||||
},
|
||||
[SpanType.PluginTool]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintool'),
|
||||
},
|
||||
[SpanType.PluginToolBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintoolbatch'),
|
||||
},
|
||||
[SpanType.Knowledge]: {
|
||||
label: I18n.t('analytic_query_subtype_value_knowledge'),
|
||||
},
|
||||
[SpanType.Code]: {
|
||||
label: I18n.t('analytic_query_subtype_value_code'),
|
||||
},
|
||||
[SpanType.CodeBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_codebatch'),
|
||||
},
|
||||
[SpanType.Condition]: {
|
||||
label: I18n.t('analytic_query_subtype_value_condition'),
|
||||
},
|
||||
[SpanType.Card]: {
|
||||
label: I18n.t('analytic_query_subtype_value_card'),
|
||||
},
|
||||
[SpanType.WorkflowMessage]: {
|
||||
label: I18n.t('analytic_query_subtype_value_workflow_message'),
|
||||
},
|
||||
|
||||
[SpanType.WorkflowLLMCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmcall'),
|
||||
},
|
||||
[SpanType.WorkflowLLMBatchCall]: {
|
||||
label: I18n.t('analytic_query_subtype_value_llmbatchcall'),
|
||||
},
|
||||
[SpanType.WorkflowCode]: {
|
||||
label: I18n.t('analytic_query_subtype_value_code'),
|
||||
},
|
||||
[SpanType.WorkflowCodeBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_codebatch'),
|
||||
},
|
||||
[SpanType.WorkflowCondition]: {
|
||||
label: I18n.t('analytic_query_subtype_value_condition'),
|
||||
},
|
||||
[SpanType.WorkflowPluginTool]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintool'),
|
||||
},
|
||||
[SpanType.WorkflowPluginToolBatch]: {
|
||||
label: I18n.t('analytic_query_subtype_value_plugintoolbatch'),
|
||||
},
|
||||
[SpanType.WorkflowKnowledge]: {
|
||||
label: I18n.t('analytic_query_subtype_value_knowledge'),
|
||||
},
|
||||
[SpanType.Chain]: {},
|
||||
// 特定业务
|
||||
[SpanType.Hook]: {
|
||||
label: I18n.t('analytics_query_invoke', {
|
||||
name: 'Hook',
|
||||
}),
|
||||
},
|
||||
[SpanType.BWStart]: { label: 'BWStart' },
|
||||
[SpanType.BWEnd]: { label: 'BWEnd' },
|
||||
[SpanType.BWBatch]: { label: 'BWBatch' },
|
||||
[SpanType.BWLoop]: { label: 'BWLoop' },
|
||||
[SpanType.BWCondition]: { label: 'BWCondition' },
|
||||
[SpanType.BWLLM]: { label: 'BWLLM' },
|
||||
[SpanType.BWParallel]: { label: 'BWParallel' },
|
||||
[SpanType.BWScript]: { label: 'BWScript' },
|
||||
[SpanType.BWVariable]: { label: 'BWVariable' },
|
||||
[SpanType.BWCallFlow]: { label: 'BWCallFlow' },
|
||||
[SpanType.BWConnector]: { label: 'BWConnector' },
|
||||
};
|
||||
|
||||
export const spanCategoryConfigMap: SpanCategoryConfigMap = {
|
||||
[SpanCategory.Unknown]: {
|
||||
label: I18n.t('analytic_query_type_value_unknown'),
|
||||
},
|
||||
[SpanCategory.Start]: {
|
||||
label: I18n.t('analytic_query_type_value_start'),
|
||||
},
|
||||
[SpanCategory.Agent]: {
|
||||
label: I18n.t('analytic_query_type_value_agent'),
|
||||
},
|
||||
[SpanCategory.LLMCall]: {
|
||||
label: I18n.t('analytic_query_type_value_llmcall'),
|
||||
},
|
||||
[SpanCategory.Workflow]: {
|
||||
label: I18n.t('analytic_query_type_value_workflow'),
|
||||
},
|
||||
[SpanCategory.WorkflowStart]: {
|
||||
label: I18n.t('analytic_query_type_value_workflowstart'),
|
||||
},
|
||||
[SpanCategory.WorkflowEnd]: {
|
||||
label: I18n.t('analytic_query_type_value_workflowend'),
|
||||
},
|
||||
[SpanCategory.Plugin]: {
|
||||
label: I18n.t('analytic_query_type_value_plugin'),
|
||||
},
|
||||
[SpanCategory.Knowledge]: {
|
||||
label: I18n.t('analytic_query_type_value_knowledge'),
|
||||
},
|
||||
[SpanCategory.Code]: {
|
||||
label: I18n.t('analytic_query_type_value_code'),
|
||||
},
|
||||
[SpanCategory.Condition]: {
|
||||
label: I18n.t('analytic_query_type_value_condition'),
|
||||
},
|
||||
[SpanCategory.Card]: {
|
||||
label: I18n.t('analytic_query_type_value_card'),
|
||||
},
|
||||
[SpanCategory.Message]: {
|
||||
label: I18n.t('analytic_query_type_value_message'),
|
||||
},
|
||||
[SpanCategory.Variable]: {
|
||||
label: I18n.t('analytics_query_type_variable'),
|
||||
},
|
||||
[SpanCategory.Hook]: {
|
||||
label: 'Hook',
|
||||
},
|
||||
[SpanCategory.Batch]: {
|
||||
label: 'Batch',
|
||||
},
|
||||
[SpanCategory.Loop]: {
|
||||
label: 'Loop',
|
||||
},
|
||||
[SpanCategory.Parallel]: {
|
||||
label: 'Parallel',
|
||||
},
|
||||
[SpanCategory.Script]: {
|
||||
label: 'Script',
|
||||
},
|
||||
[SpanCategory.CallFlow]: {
|
||||
label: 'CallFlow',
|
||||
},
|
||||
[SpanCategory.Connector]: {
|
||||
label: 'Connector',
|
||||
},
|
||||
};
|
||||
|
||||
export const spanStatusConfigMap: SpanStatusConfigMap = {
|
||||
[SpanStatus.Unknown]: {
|
||||
label: I18n.t('analytic_query_status_unknown'),
|
||||
},
|
||||
[SpanStatus.Success]: {
|
||||
label: I18n.t('analytic_query_status_success'),
|
||||
},
|
||||
[SpanStatus.Error]: {
|
||||
label: I18n.t('analytic_query_status_error'),
|
||||
},
|
||||
[SpanStatus.Broken]: {
|
||||
label: I18n.t('analytic_query_status_broken'),
|
||||
},
|
||||
};
|
||||
|
||||
export const streamingOutputStatusConfigMap: Record<
|
||||
StreamingOutputStatus,
|
||||
{ label?: string } | undefined
|
||||
> = {
|
||||
[StreamingOutputStatus.OPEN]: {
|
||||
label: I18n.t('analytic_streaming_output_status_open'),
|
||||
},
|
||||
[StreamingOutputStatus.CLOSE]: {
|
||||
label: I18n.t('analytic_streaming_output_status_close'),
|
||||
},
|
||||
[StreamingOutputStatus.UNDEFINED]: {},
|
||||
};
|
||||
|
||||
export const botEnvConfigMap: Record<string, { label?: string } | undefined> = {
|
||||
'0': {
|
||||
label: I18n.t('analytic_query_env_value_botmakerdebug'),
|
||||
},
|
||||
'1': {
|
||||
label: I18n.t('analytic_query_env_value_realuser'),
|
||||
},
|
||||
};
|
||||
@@ -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 const virtualStartSpanId = '-10001';
|
||||
export const rootBreakSpanId = '-10002';
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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 {
|
||||
SpanType,
|
||||
type Span,
|
||||
type TraceAdvanceInfo,
|
||||
SpanStatus,
|
||||
SpanCategory,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { spans2CSpans } from '../utils/cspan-transform';
|
||||
import {
|
||||
type SpanNode,
|
||||
buildCallTrees,
|
||||
getRootSpan,
|
||||
} from '../utils/cspan-graph';
|
||||
import { genVirtualStart, getSpanProp } from '../utils/cspan';
|
||||
import {
|
||||
type SpanCategoryMeta,
|
||||
type CSpan,
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSPanBatch,
|
||||
type CTrace,
|
||||
type CSpanAttrUserInput,
|
||||
} from '../typings/cspan';
|
||||
|
||||
// 对根节点追加traceAdvanceInfo信息
|
||||
const appendTraceAdvanceInfo = (
|
||||
spans: CSpan[],
|
||||
traceAdvanceInfo?: Omit<TraceAdvanceInfo, 'trace_id'>,
|
||||
): CSpan[] =>
|
||||
spans.map(span => {
|
||||
// 修改根节点的状态。 根节点的tokens和status以服务端获取的为准
|
||||
if (
|
||||
span.type === SpanType.UserInput ||
|
||||
span.type === SpanType.UserInputV2
|
||||
) {
|
||||
const span0 = span as CSpanAttrUserInput;
|
||||
return {
|
||||
...span0,
|
||||
status: traceAdvanceInfo?.status ?? SpanStatus.Unknown,
|
||||
input_tokens_sum: traceAdvanceInfo?.tokens.input,
|
||||
output_tokens_sum: traceAdvanceInfo?.tokens.output,
|
||||
};
|
||||
} else {
|
||||
return span;
|
||||
}
|
||||
});
|
||||
|
||||
interface TokensSum {
|
||||
input_tokens_sum: number;
|
||||
output_tokens_sum: number;
|
||||
}
|
||||
const appendSpans = (spans: CSpan[], callTrees: SpanNode[]) => {
|
||||
const tokensMap: {
|
||||
[spanId: string]: TokensSum | undefined;
|
||||
} = {};
|
||||
const calculateTokensSum = (span: SpanNode): TokensSum => {
|
||||
const { input_tokens: inputTokens, output_tokens: outputTokens } =
|
||||
getCSpanTokens(span);
|
||||
let inputTokensSumRst = inputTokens;
|
||||
let outputTokensSumRst = outputTokens;
|
||||
|
||||
span.children?.forEach((subSpan: SpanNode) => {
|
||||
const subTokensSum = calculateTokensSum(subSpan);
|
||||
inputTokensSumRst += subTokensSum.input_tokens_sum;
|
||||
outputTokensSumRst += subTokensSum.output_tokens_sum;
|
||||
return span;
|
||||
});
|
||||
const tokensSum = {
|
||||
input_tokens_sum: inputTokensSumRst,
|
||||
output_tokens_sum: outputTokensSumRst,
|
||||
};
|
||||
tokensMap[span.id] = tokensSum;
|
||||
return tokensSum;
|
||||
};
|
||||
|
||||
callTrees.forEach(callTree => {
|
||||
calculateTokensSum(callTree);
|
||||
});
|
||||
|
||||
return spans.map(span => {
|
||||
if (
|
||||
span.type === SpanType.UserInput ||
|
||||
span.type === SpanType.UserInputV2
|
||||
) {
|
||||
// 根节点input_tokens_sum和output_tokens_sum的数值以服务端获取为准,不做计算
|
||||
return span;
|
||||
} else {
|
||||
return {
|
||||
...span,
|
||||
input_tokens_sum: tokensMap[span.id]?.input_tokens_sum,
|
||||
output_tokens_sum: tokensMap[span.id]?.output_tokens_sum,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取CSpan节点的tokens信息
|
||||
const getCSpanTokens = (
|
||||
span: CSpan,
|
||||
): {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
} => {
|
||||
if ('spans' in span) {
|
||||
const spanBatch = span as CSPanBatch;
|
||||
let inputTokensRst = 0;
|
||||
let outputTokensRst = 0;
|
||||
spanBatch.spans.forEach(subSpan => {
|
||||
const inputTokens = subSpan?.extra?.input_tokens;
|
||||
const outputTokens = subSpan?.extra?.output_tokens;
|
||||
if (inputTokens !== undefined) {
|
||||
inputTokensRst += inputTokens;
|
||||
}
|
||||
if (outputTokens !== undefined) {
|
||||
outputTokensRst += outputTokens;
|
||||
}
|
||||
});
|
||||
return {
|
||||
input_tokens: inputTokensRst,
|
||||
output_tokens: outputTokensRst,
|
||||
};
|
||||
} else {
|
||||
// SingleSpan节点
|
||||
return {
|
||||
input_tokens: (getSpanProp(span, 'input_tokens') as number) ?? 0,
|
||||
output_tokens: (getSpanProp(span, 'output_tokens') as number) ?? 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 追加invokeAgentInfo的dialog_round和model字段
|
||||
const appendRootSpan = (info: { rootSpan: CSpan; spans: CSpan[] }): CTrace => {
|
||||
const { rootSpan, spans } = info;
|
||||
const rstSpan: CTrace = rootSpan;
|
||||
let { extra } = rstSpan;
|
||||
|
||||
const invokeAgentSpans = spans.filter(
|
||||
span => span.type === SpanType.InvokeAgent,
|
||||
);
|
||||
if (invokeAgentSpans.length > 0) {
|
||||
const invokeAgentSpan = invokeAgentSpans[0] as CSpanAttrInvokeAgent;
|
||||
extra = {
|
||||
...extra,
|
||||
dialog_round: invokeAgentSpan.extra?.dialog_round,
|
||||
model: invokeAgentSpan.extra?.model,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...rstSpan,
|
||||
extra,
|
||||
};
|
||||
};
|
||||
|
||||
interface UseSpanTransformProps {
|
||||
orgSpans: Span[];
|
||||
traceAdvanceInfo?: Omit<TraceAdvanceInfo, 'trace_id'>;
|
||||
spanCategoryMeta?: SpanCategoryMeta;
|
||||
messageId?: string;
|
||||
}
|
||||
interface UseSpanTransformReturn {
|
||||
rootSpan: CTrace;
|
||||
spans: CSpan[];
|
||||
}
|
||||
|
||||
// start节点不存在时,生成虚拟start节点
|
||||
export const appendVirtualStart = (spans: CSpan[]): CSpan[] => {
|
||||
const startSpans = spans.filter(rootSpan => {
|
||||
const { category } = rootSpan;
|
||||
return category === SpanCategory.Start;
|
||||
});
|
||||
|
||||
// 生成虚拟span
|
||||
if (startSpans.length > 0) {
|
||||
return spans;
|
||||
} else {
|
||||
const virtualStartSpan = genVirtualStart(spans);
|
||||
return spans.concat(virtualStartSpan);
|
||||
}
|
||||
};
|
||||
|
||||
export const useSpanTransform = (
|
||||
props: UseSpanTransformProps,
|
||||
): UseSpanTransformReturn => {
|
||||
const { orgSpans, traceAdvanceInfo, spanCategoryMeta, messageId } = props;
|
||||
|
||||
const rst = useMemo(() => {
|
||||
let spans = spans2CSpans(orgSpans, spanCategoryMeta);
|
||||
// 追加虚拟span
|
||||
spans = appendVirtualStart(spans);
|
||||
|
||||
// 追加traceAdvanceInfo信息
|
||||
spans = appendTraceAdvanceInfo(spans, traceAdvanceInfo);
|
||||
|
||||
// 根据spans,组装call trees
|
||||
let callTrees = buildCallTrees(spans);
|
||||
// 根节点超过 1 个,需要按 message id 过滤
|
||||
if (callTrees.length > 1 && messageId) {
|
||||
callTrees = callTrees.filter(
|
||||
root =>
|
||||
!('extra' in root) ||
|
||||
(root.extra && !('message_id' in root.extra)) ||
|
||||
// 存在 message_id 的情况下,过滤 id 匹配的节点
|
||||
root.extra?.message_id === messageId,
|
||||
);
|
||||
}
|
||||
const rootSpan = getRootSpan(callTrees, false);
|
||||
|
||||
// rootSpan的根节点调整: 追加invokeAgent信息
|
||||
const rootSpanRst = appendRootSpan({
|
||||
rootSpan,
|
||||
spans,
|
||||
});
|
||||
|
||||
const visit = (targetId: string, root: SpanNode): boolean => {
|
||||
if (root.id === targetId) {
|
||||
return true;
|
||||
}
|
||||
return root.children?.some(subRoot => visit(targetId, subRoot)) ?? false;
|
||||
};
|
||||
|
||||
// 过滤掉不在rootSpan中的节点
|
||||
spans = spans.filter(span => visit(span.id, rootSpan));
|
||||
|
||||
// 对spans节点进行调整: spans中workflow节点tokens累加计算
|
||||
const spansRst = appendSpans(spans, callTrees);
|
||||
return {
|
||||
rootSpan: rootSpanRst,
|
||||
spans: spansRst,
|
||||
};
|
||||
}, [orgSpans, traceAdvanceInfo, spanCategoryMeta, messageId]);
|
||||
|
||||
return rst;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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 { default as TraceFlamethread } from './components/trace-flamethread';
|
||||
export { default as TraceTree } from './components/trace-tree';
|
||||
export { default as TopologyFlow } from './components/topology-flow';
|
||||
export {
|
||||
default as Flamethread,
|
||||
type InteractionEventHandler,
|
||||
} from './components/flamethread';
|
||||
export { default as Tree, type MouseEventParams } from './components/tree';
|
||||
export { useSpanTransform } from './hooks/use-span-transform';
|
||||
// Tree和Flamethread的参数类型
|
||||
export { DataSourceTypeEnum } from './typings/graph';
|
||||
|
||||
export {
|
||||
// useSpanTransform相关类型
|
||||
type SpanCategoryMeta,
|
||||
// useSpanTransform 生成的定制span
|
||||
type CSpan,
|
||||
type CTrace,
|
||||
type CSpanSingle,
|
||||
type CSPanBatch,
|
||||
type CSpanAttrUserInput,
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpanAttrRestartAgent,
|
||||
type CSpanAttrSwitchAgent,
|
||||
type CSpanAttrLLMCall,
|
||||
type CSpanAttrLLMBatchCall,
|
||||
type CSpanAttrWorkflow,
|
||||
type CSpanAttrWorkflowEnd,
|
||||
type CSpanAttrCode,
|
||||
type CSpanAttrCodeBatch,
|
||||
type CSpanAttrCondition,
|
||||
type CSpanAttrPluginTool,
|
||||
type CSpanAttrPluginToolBatch,
|
||||
type CSpanAttrKnowledge,
|
||||
type CSpanAttrChain,
|
||||
StreamingOutputStatus,
|
||||
} from './typings/cspan';
|
||||
|
||||
export {
|
||||
spanTypeConfigMap,
|
||||
botEnvConfigMap,
|
||||
spanCategoryConfigMap,
|
||||
streamingOutputStatusConfigMap,
|
||||
} from './config/cspan';
|
||||
|
||||
export {
|
||||
isBatchSpanType,
|
||||
isVisibleSpan,
|
||||
checkIsBatchBasicCSpan,
|
||||
getTokens,
|
||||
getSpanProp,
|
||||
} from './utils/cspan';
|
||||
|
||||
export { span2CSpan } from './utils/cspan-transform';
|
||||
|
||||
export { fieldItemHandlers, type FieldItem } from './utils/field-item-handler';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
interface SpanTypeConfig {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** key: SpanType */
|
||||
export type SpanTypeConfigMap = Record<number, SpanTypeConfig | undefined>;
|
||||
|
||||
interface SpanCategoryConfig {
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** key: SpanCategory */
|
||||
export type SpanCategoryConfigMap = Record<
|
||||
number,
|
||||
SpanCategoryConfig | undefined
|
||||
>;
|
||||
|
||||
interface SpanStatusConfig {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SpanStatusConfigMap {
|
||||
[x: number]: SpanStatusConfig | undefined;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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 Span,
|
||||
type AttrUserInput,
|
||||
type AttrInvokeAgent,
|
||||
type AttrCode,
|
||||
type AttrCodeBatch,
|
||||
type AttrCondition,
|
||||
type AttrKnowledge,
|
||||
type AttrLLMBatchCall,
|
||||
type AttrLLMCall,
|
||||
type AttrPluginTool,
|
||||
type AttrPluginToolBatch,
|
||||
type AttrRestartAgent,
|
||||
type AttrSwitchAgent,
|
||||
type AttrWorkflow,
|
||||
type AttrWorkflowEnd,
|
||||
type SpanCategory,
|
||||
type SpanType,
|
||||
type SpanStatus,
|
||||
type AttrChain,
|
||||
type AttrWorkflowMessage,
|
||||
type AttrCard,
|
||||
type AttrWorkflowLLMCall,
|
||||
type AttrWorkflowCode,
|
||||
type AttrWorkflowCodeBatch,
|
||||
type AttrWorkflowCondition,
|
||||
type AttrWorkflowKnowledge,
|
||||
type AttrWorkflowLLMBatchCall,
|
||||
type AttrWorkflowPluginTool,
|
||||
type AttrWorkflowPluginToolBatch,
|
||||
type AttrBWStart,
|
||||
type AttrBWEnd,
|
||||
type AttrBWBatch,
|
||||
type AttrBWLoop,
|
||||
type AttrBWCondition,
|
||||
type AttrBWLLM,
|
||||
type AttrBWParallel,
|
||||
type AttrBWScript,
|
||||
type AttrBWVariable,
|
||||
type AttrBWCallFlow,
|
||||
type AttrBWConnector,
|
||||
type AttrHook,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
type CSpanCommonProp = Pick<
|
||||
Span,
|
||||
'trace_id' | 'id' | 'parent_id' | 'name' | 'type' | 'status'
|
||||
> & {
|
||||
start_time: number; // 默认为Int64,用起来不方便
|
||||
latency: number; // 默认为Int64,用起来不方便
|
||||
category?: SpanCategory; // 加载Meta失败时才为空
|
||||
input_tokens_sum?: number; // 扩展字段,用于存储子节点的input_tokens之和
|
||||
output_tokens_sum?: number; // 扩展字段,用于存储子节点的output_tokens之和
|
||||
};
|
||||
|
||||
type GenCSpan<T> = CSpanCommonProp & {
|
||||
extra?: T;
|
||||
};
|
||||
|
||||
export type CSpanAttrUserInput = GenCSpan<AttrUserInput>;
|
||||
export type CSpanAttrInvokeAgent = GenCSpan<AttrInvokeAgent>;
|
||||
export type CSpanAttrRestartAgent = GenCSpan<AttrRestartAgent>;
|
||||
export type CSpanAttrSwitchAgent = GenCSpan<AttrSwitchAgent>;
|
||||
export type CSpanAttrLLMCall = GenCSpan<AttrLLMCall>;
|
||||
export type CSpanAttrWorkflowLLMCall = GenCSpan<AttrWorkflowLLMCall>;
|
||||
export type CSpanAttrLLMBatchCall = GenCSpan<AttrLLMBatchCall>;
|
||||
export type CSpanAttrWorkflowLLMBatchCall = GenCSpan<AttrWorkflowLLMBatchCall>;
|
||||
export type CSpanAttrWorkflow = GenCSpan<AttrWorkflow>;
|
||||
export type CSpanAttrWorkflowEnd = GenCSpan<AttrWorkflowEnd>;
|
||||
export type CSpanAttrCode = GenCSpan<AttrCode>;
|
||||
export type CSpanAttrWorkflowCode = GenCSpan<AttrWorkflowCode>;
|
||||
export type CSpanAttrCodeBatch = GenCSpan<AttrCodeBatch>;
|
||||
export type CSpanAttrWorkflowCodeBatch = GenCSpan<AttrWorkflowCodeBatch>;
|
||||
export type CSpanAttrCondition = GenCSpan<AttrCondition>;
|
||||
export type CSpanAttrWorkflowCondition = GenCSpan<AttrWorkflowCondition>;
|
||||
export type CSpanAttrPluginTool = GenCSpan<AttrPluginTool>;
|
||||
export type CSpanAttrWorkflowPluginTool = GenCSpan<AttrWorkflowPluginTool>;
|
||||
export type CSpanAttrPluginToolBatch = GenCSpan<AttrPluginToolBatch>;
|
||||
export type CSpanAttrWorkflowPluginToolBatch =
|
||||
GenCSpan<AttrWorkflowPluginToolBatch>;
|
||||
export type CSpanAttrKnowledge = GenCSpan<AttrKnowledge>;
|
||||
export type CSpanAttrWorkflowKnowledge = GenCSpan<AttrWorkflowKnowledge>;
|
||||
export type CSpanAttrChain = GenCSpan<AttrChain>;
|
||||
export type CSpanAttrCard = GenCSpan<AttrCard>;
|
||||
export type CSpanAttrWorkflowMessage = GenCSpan<AttrWorkflowMessage>;
|
||||
export type CSpanAttrHook = GenCSpan<AttrHook>;
|
||||
export type CSpanAttrBWStart = GenCSpan<AttrBWStart>;
|
||||
export type CSpanAttrBWEnd = GenCSpan<AttrBWEnd>;
|
||||
export type CSpanAttrBWBatch = GenCSpan<AttrBWBatch>;
|
||||
export type CSpanAttrBWLoop = GenCSpan<AttrBWLoop>;
|
||||
export type CSpanAttrBWCondition = GenCSpan<AttrBWCondition>;
|
||||
export type CSpanAttrBWLLM = GenCSpan<AttrBWLLM>;
|
||||
export type CSpanAttrBWParallel = GenCSpan<AttrBWParallel>;
|
||||
export type CSpanAttrBWScript = GenCSpan<AttrBWScript>;
|
||||
export type CSpanAttrBWVariable = GenCSpan<AttrBWVariable>;
|
||||
export type CSpanAttrBWCallFlow = GenCSpan<AttrBWCallFlow>;
|
||||
export type CSpanAttrBWConnector = GenCSpan<AttrBWConnector>;
|
||||
|
||||
export type CSpanSingle =
|
||||
| CSpanAttrUserInput
|
||||
| CSpanAttrInvokeAgent
|
||||
| CSpanAttrRestartAgent
|
||||
| CSpanAttrSwitchAgent
|
||||
| CSpanAttrLLMCall
|
||||
| CSpanAttrLLMBatchCall
|
||||
| CSpanAttrWorkflow
|
||||
| CSpanAttrWorkflowEnd
|
||||
| CSpanAttrCode
|
||||
| CSpanAttrCodeBatch
|
||||
| CSpanAttrCondition
|
||||
| CSpanAttrPluginTool
|
||||
| CSpanAttrPluginToolBatch
|
||||
| CSpanAttrKnowledge
|
||||
| CSpanAttrChain
|
||||
| CSpanAttrCard
|
||||
| CSpanAttrWorkflowMessage
|
||||
| CSpanAttrWorkflowLLMCall
|
||||
| CSpanAttrWorkflowLLMBatchCall
|
||||
| CSpanAttrWorkflowCode
|
||||
| CSpanAttrWorkflowCodeBatch
|
||||
| CSpanAttrWorkflowCondition
|
||||
| CSpanAttrWorkflowPluginTool
|
||||
| CSpanAttrWorkflowPluginToolBatch
|
||||
| CSpanAttrWorkflowKnowledge
|
||||
| CSpanAttrHook
|
||||
| CSpanAttrBWStart
|
||||
| CSpanAttrBWEnd
|
||||
| CSpanAttrBWBatch
|
||||
| CSpanAttrBWLoop
|
||||
| CSpanAttrBWCondition
|
||||
| CSpanAttrBWLLM
|
||||
| CSpanAttrBWParallel
|
||||
| CSpanAttrBWScript
|
||||
| CSpanAttrBWVariable
|
||||
| CSpanAttrBWCallFlow
|
||||
| CSpanAttrBWConnector;
|
||||
|
||||
export type CSpanSingleForBatch =
|
||||
| CSpanAttrLLMBatchCall
|
||||
| CSpanAttrWorkflowLLMBatchCall
|
||||
| CSpanAttrCodeBatch
|
||||
| CSpanAttrWorkflowCodeBatch
|
||||
| CSpanAttrPluginToolBatch
|
||||
| CSpanAttrWorkflowPluginToolBatch;
|
||||
|
||||
export type CSPanBatch = CSpanCommonProp & {
|
||||
spans: CSpanSingleForBatch[];
|
||||
workflow_node_id?: string;
|
||||
};
|
||||
|
||||
export type CSpan = CSpanSingle | CSPanBatch;
|
||||
|
||||
type AttrUserInputExtra = Partial<CSpanAttrUserInput['extra']> & {
|
||||
dialog_round?: number;
|
||||
model?: string;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
};
|
||||
|
||||
export type CTrace = Omit<CSpanAttrUserInput, 'extra' | 'status'> & {
|
||||
status?: SpanStatus;
|
||||
extra?: AttrUserInputExtra;
|
||||
};
|
||||
|
||||
export type SpanCategoryMeta = Record<SpanCategory, SpanType[] | undefined>;
|
||||
|
||||
/** key: SpanCategory */
|
||||
export type SpanCategoryMap = Record<number, SpanCategory>;
|
||||
|
||||
export enum StreamingOutputStatus {
|
||||
OPEN = 'open',
|
||||
CLOSE = 'close',
|
||||
UNDEFINED = 'undefined',
|
||||
}
|
||||
@@ -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 { type CSpan } from './cspan';
|
||||
|
||||
export enum DataSourceTypeEnum {
|
||||
SpanData = 'SpanData',
|
||||
TraceId = 'TraceId',
|
||||
}
|
||||
|
||||
export interface DataSource {
|
||||
// 取值为traceId时,组件会根据traceId查询SpanData
|
||||
type: DataSourceTypeEnum;
|
||||
spanData?: CSpan[]; // type为spanData时,特有字段
|
||||
traceId?: string; // type为traceId时,特有字段
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 { omit } from 'lodash-es';
|
||||
import {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type CSpan } from '../typings/cspan';
|
||||
import { rootBreakSpanId } from '../constant';
|
||||
import {
|
||||
spanStatusConfigMap as defaultSpanStatusConfigMap,
|
||||
spanTypeConfigMap as defaultSpanTypeConfigMap,
|
||||
} from '../config/cspan';
|
||||
import { genVirtualStart, isVisibleSpan } from './cspan';
|
||||
|
||||
export type SpanNode = CSpan & {
|
||||
parent?: SpanNode;
|
||||
children?: SpanNode[];
|
||||
};
|
||||
|
||||
export const getSpanDataByTraceId = (traceId: string): CSpan[] => [];
|
||||
|
||||
// 获取tree的跟节点
|
||||
export const buildCallTrees = (
|
||||
spans: CSpan[],
|
||||
splitBatchSpan = true,
|
||||
): SpanNode[] => {
|
||||
const roots: SpanNode[] = [];
|
||||
|
||||
const map: {
|
||||
[spanId: string]: SpanNode;
|
||||
} = {};
|
||||
|
||||
spans.forEach(span => {
|
||||
const curSpan = { ...span, children: [] };
|
||||
// Batch节点
|
||||
if ('spans' in span && splitBatchSpan) {
|
||||
span.spans.forEach(subSpan => {
|
||||
map[subSpan.id] = curSpan;
|
||||
});
|
||||
} else {
|
||||
const { id: spanId } = span;
|
||||
map[spanId] = curSpan;
|
||||
}
|
||||
});
|
||||
|
||||
spans.forEach(span => {
|
||||
const { id: spanId, parent_id: parentSpanId } = span;
|
||||
const spanNode = map[spanId];
|
||||
const parentSpanNode = map[parentSpanId];
|
||||
|
||||
if (parentSpanId === '' || parentSpanNode === undefined) {
|
||||
roots.push(spanNode);
|
||||
} else {
|
||||
parentSpanNode.children = parentSpanNode.children ?? [];
|
||||
parentSpanNode.children.push(spanNode);
|
||||
spanNode.parent = parentSpanNode;
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
export const getRootSpan = (spans: SpanNode[], needBuildTrees = true) => {
|
||||
const rootSpans = needBuildTrees ? buildCallTrees(spans) : spans;
|
||||
|
||||
const startSpans: SpanNode[] = [];
|
||||
rootSpans.forEach(rootSpan => {
|
||||
const { category } = rootSpan;
|
||||
if (category === SpanCategory.Start) {
|
||||
startSpans.push(rootSpan);
|
||||
}
|
||||
});
|
||||
|
||||
// 无start的场景: 虚拟一个startSpan(供多方使用,火焰图,树状图,详情图,以确保一致);多个startSpans,则取第一个
|
||||
return startSpans.length > 0 ? startSpans[0] : genVirtualStart(rootSpans);
|
||||
};
|
||||
|
||||
export const getBreakSpans = (spans: SpanNode[], needBuildTrees = true) => {
|
||||
const rootSpans = needBuildTrees ? buildCallTrees(spans) : spans;
|
||||
|
||||
const breakSpans: SpanNode[] = [];
|
||||
rootSpans.forEach(rootSpan => {
|
||||
const { category } = rootSpan;
|
||||
if (category !== SpanCategory.Start) {
|
||||
breakSpans.push(rootSpan);
|
||||
}
|
||||
});
|
||||
return breakSpans;
|
||||
};
|
||||
|
||||
export const compareByStartAt = (spanA: SpanNode, spanB: SpanNode) => {
|
||||
const startAtA = spanA.start_time;
|
||||
const startAtB = spanB.start_time;
|
||||
if (startAtA > startAtB) {
|
||||
return 1;
|
||||
} else if (startAtA < startAtB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const compareByEndAt = (spanA: SpanNode, spanB: SpanNode) => {
|
||||
const endAtA = spanA.start_time + spanA.latency;
|
||||
const endAtB = spanB.start_time + spanB.latency;
|
||||
if (endAtA > endAtB) {
|
||||
return 1;
|
||||
} else if (endAtA < endAtB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSpanTitle = (
|
||||
span: CSpan,
|
||||
spanTypeConfigMap = defaultSpanTypeConfigMap,
|
||||
) => {
|
||||
const { type, name = '' } = span;
|
||||
const typeName = spanTypeConfigMap[type]?.label ?? '';
|
||||
if (name && name !== typeName) {
|
||||
return `${typeName} ${name}`;
|
||||
} else {
|
||||
return typeName;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStatusLabel = (
|
||||
span: CSpan,
|
||||
spanStatusConfigMap = defaultSpanStatusConfigMap,
|
||||
) => {
|
||||
const { status } = span;
|
||||
return spanStatusConfigMap[status]?.label ?? '';
|
||||
};
|
||||
|
||||
// start节点不存在时,生成虚拟start节点
|
||||
const getRootBreakSpan = (breakSpans: SpanNode[]): SpanNode => ({
|
||||
id: rootBreakSpanId,
|
||||
parent_id: '',
|
||||
trace_id: '',
|
||||
name: '',
|
||||
type: SpanType.UserInput,
|
||||
status: SpanStatus.Broken,
|
||||
start_time: -1,
|
||||
latency: -1,
|
||||
children: breakSpans,
|
||||
});
|
||||
|
||||
// 根据switchAgent/restartAgent建立父子关系
|
||||
const handleAgent = (spans: SpanNode[]): SpanNode[] => {
|
||||
const getAgent = (startAt: number, agents: SpanNode[]) => {
|
||||
const len = agents.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const curAgent = agents[i];
|
||||
const nextAgent = agents[i + 1];
|
||||
const curEndAt = curAgent.start_time + curAgent.latency;
|
||||
const nextEndAt = nextAgent
|
||||
? nextAgent.start_time + nextAgent.latency
|
||||
: Number.POSITIVE_INFINITY;
|
||||
|
||||
if (startAt >= curEndAt && startAt <= nextEndAt) {
|
||||
return agents[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const agentSpans = spans
|
||||
.filter(
|
||||
span =>
|
||||
span.type === SpanType.SwitchAgent ||
|
||||
span.type === SpanType.RestartAgent,
|
||||
)
|
||||
.sort(compareByEndAt);
|
||||
|
||||
const normalSpans = spans
|
||||
.filter(
|
||||
span =>
|
||||
span.type !== SpanType.SwitchAgent &&
|
||||
span.type !== SpanType.RestartAgent,
|
||||
)
|
||||
.sort(compareByStartAt);
|
||||
|
||||
const rstSpans: SpanNode[] = [];
|
||||
normalSpans.forEach(span => {
|
||||
const agent = getAgent(span.start_time, agentSpans);
|
||||
if (agent) {
|
||||
agent.children = agent.children ?? [];
|
||||
agent.children?.push(span);
|
||||
span.parent = agent;
|
||||
} else {
|
||||
rstSpans.push(span);
|
||||
}
|
||||
});
|
||||
|
||||
return [...rstSpans, ...agentSpans];
|
||||
};
|
||||
|
||||
// 只有特殊的节点类型可以作为根节点
|
||||
const isTreeRootSpanType = (type: SpanType) =>
|
||||
[
|
||||
SpanType.InvokeAgent,
|
||||
SpanType.Workflow,
|
||||
SpanType.LLMBatchCall,
|
||||
SpanType.LLMCall,
|
||||
SpanType.WorkflowLLMCall,
|
||||
SpanType.WorkflowLLMBatchCall,
|
||||
// BlockWise的都放在这里
|
||||
SpanType.BWStart,
|
||||
SpanType.BWEnd,
|
||||
SpanType.BWBatch,
|
||||
SpanType.BWLoop,
|
||||
SpanType.BWCondition,
|
||||
SpanType.BWLLM,
|
||||
SpanType.BWParallel,
|
||||
SpanType.BWScript,
|
||||
SpanType.BWVariable,
|
||||
SpanType.BWCallFlow,
|
||||
SpanType.BWConnector,
|
||||
// 新增类型都支持层级
|
||||
SpanType.Hook,
|
||||
].includes(type);
|
||||
|
||||
// 依据调用树,构建TraceTree
|
||||
const callTree2TraceTree = (rootSpan: SpanNode): SpanNode => {
|
||||
const rstSpans: SpanNode[] = [];
|
||||
const walk = (span: SpanNode) => {
|
||||
span.children?.forEach(subSpan => {
|
||||
const { type } = subSpan;
|
||||
if (isTreeRootSpanType(type)) {
|
||||
rstSpans.push(callTree2TraceTree(subSpan));
|
||||
} else {
|
||||
if (isVisibleSpan(subSpan)) {
|
||||
// 当前节点加入到 rootSpan.children
|
||||
rstSpans.push(omit(subSpan, 'children'));
|
||||
}
|
||||
// 递归子节点(当前节点)。 注意:隐藏的节点类型,也要递归的。 当前节点隐藏,其子节点有可能是显示的
|
||||
walk(subSpan);
|
||||
}
|
||||
});
|
||||
};
|
||||
walk(rootSpan);
|
||||
|
||||
return {
|
||||
...rootSpan,
|
||||
children: handleAgent(rstSpans),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildTraceTree = (spans: SpanNode[], splitBatchSpan?: boolean) => {
|
||||
// 1. 根据spans,组装call trees
|
||||
const callTrees = buildCallTrees(spans, splitBatchSpan);
|
||||
|
||||
// 2. 生成startSpan
|
||||
const startSpan: SpanNode = getRootSpan(callTrees, false);
|
||||
|
||||
// 3. 获取 break节点(非start的根节点都是breakSpan)
|
||||
const breakSpans: SpanNode[] = getBreakSpans(callTrees, false);
|
||||
|
||||
// 4. 根据调用tree,生成PRD中的Tree(即,PRD中的Tree)
|
||||
const treeStartSpan = callTree2TraceTree(startSpan);
|
||||
|
||||
if (breakSpans.length > 0) {
|
||||
// 5. 将所有breakSpans挂载到rootBreakSpan节点下
|
||||
const breakSpan: SpanNode = getRootBreakSpan(breakSpans);
|
||||
// 6. 根据调用tree,生成TraceTree
|
||||
const treeBreakSpan = callTree2TraceTree(breakSpan);
|
||||
|
||||
// 7. 将treeBreakSpan挂在到treeStartSpan下
|
||||
treeStartSpan.children = treeStartSpan.children ?? [];
|
||||
treeStartSpan.children.push(treeBreakSpan);
|
||||
treeBreakSpan.parent = treeStartSpan;
|
||||
}
|
||||
return treeStartSpan;
|
||||
};
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* 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 { pick, uniqBy } from 'lodash-es';
|
||||
import {
|
||||
type Span,
|
||||
SpanStatus,
|
||||
type SpanCategory,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import {
|
||||
type CSpanSingle,
|
||||
type CSpan,
|
||||
type CSPanBatch,
|
||||
type SpanCategoryMeta,
|
||||
type SpanCategoryMap,
|
||||
type CSpanSingleForBatch,
|
||||
} from '../typings/cspan';
|
||||
import { isBatchSpanType } from './cspan';
|
||||
|
||||
const compareByStartAt = (
|
||||
a: CSpanSingleForBatch,
|
||||
b: CSpanSingleForBatch,
|
||||
): number => {
|
||||
if (a.start_time > b.start_time) {
|
||||
return 1;
|
||||
} else if (a.start_time < b.start_time) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const compareByTaskIndex = (
|
||||
a: CSpanSingleForBatch,
|
||||
b: CSpanSingleForBatch,
|
||||
): number => {
|
||||
const taskIndexA = a.extra?.task_index ?? 0;
|
||||
const taskIndexB = a.extra?.task_index ?? 0;
|
||||
if (taskIndexA > taskIndexB) {
|
||||
return 1;
|
||||
} else if (taskIndexA < taskIndexB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const getStartAtForBatch = (spans: CSpanSingleForBatch[]): number => {
|
||||
let startAt = Number.POSITIVE_INFINITY;
|
||||
spans.forEach(span => {
|
||||
startAt = Math.min(span.start_time, startAt);
|
||||
});
|
||||
return startAt;
|
||||
};
|
||||
|
||||
const getLatencyForBatch = (spans: CSpanSingleForBatch[]): number => {
|
||||
let endAt = Number.NEGATIVE_INFINITY;
|
||||
spans.forEach(span => {
|
||||
endAt = Math.max(span.start_time + span.latency, endAt);
|
||||
});
|
||||
const startAt = getStartAtForBatch(spans);
|
||||
return endAt - startAt;
|
||||
};
|
||||
|
||||
const getStatusForBatch = (spans: CSpanSingleForBatch[]): SpanStatus => {
|
||||
let isSuccess = true;
|
||||
spans.forEach(span => {
|
||||
if (span.status === SpanStatus.Error) {
|
||||
isSuccess = false;
|
||||
}
|
||||
});
|
||||
return isSuccess ? SpanStatus.Success : SpanStatus.Error;
|
||||
};
|
||||
|
||||
// spans直接聚合,生成batchSpan
|
||||
const genBatchSpan = function (
|
||||
spans: CSpanSingleForBatch[],
|
||||
spanCategoryMap?: SpanCategoryMap,
|
||||
): CSPanBatch | undefined {
|
||||
if (spans.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// 合法性检查
|
||||
const taskTotal = spans[0].extra?.task_total;
|
||||
const spans0 = spans.filter(curSpan => {
|
||||
const curTaskTotal = curSpan.extra?.task_total;
|
||||
return curTaskTotal !== taskTotal;
|
||||
});
|
||||
// taskTotal不全部相等,数据不合法
|
||||
if (spans0.length > 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...pick(spans[0], ['trace_id', 'id', 'parent_id', 'name', 'type']),
|
||||
category: spanCategoryMap?.[spans[0].type],
|
||||
status: getStatusForBatch(spans),
|
||||
start_time: getStartAtForBatch(spans),
|
||||
latency: getLatencyForBatch(spans),
|
||||
spans: spans.sort(compareByTaskIndex),
|
||||
workflow_node_id: spans[0].extra?.workflow_node_id,
|
||||
};
|
||||
};
|
||||
|
||||
const aggregationBatchSpan = function (
|
||||
spans: CSpanSingleForBatch[],
|
||||
spanCategoryMap?: SpanCategoryMap,
|
||||
) {
|
||||
const batchSpans: CSPanBatch[] = [];
|
||||
|
||||
// 根据 workflowNodeId + type对span进行归类
|
||||
const map: {
|
||||
[key: string]: CSpanSingleForBatch[];
|
||||
} = {};
|
||||
spans.forEach(span => {
|
||||
const { type } = span;
|
||||
const workflowNodeId = span.extra?.workflow_node_id;
|
||||
if (!workflowNodeId) {
|
||||
return;
|
||||
}
|
||||
map[type + workflowNodeId] = map[type + workflowNodeId] ?? [];
|
||||
map[type + workflowNodeId].push(span);
|
||||
});
|
||||
|
||||
// 进一步根据时间+序号进行归类,生成CSpanBatch
|
||||
Object.keys(map).forEach(key => {
|
||||
const workflowSpans = map[key];
|
||||
|
||||
// 排序:时间
|
||||
workflowSpans.sort(compareByStartAt);
|
||||
|
||||
// 根据task_index进行聚合
|
||||
let curTaskIndexs: number[] = [];
|
||||
let curSpans: CSpanSingleForBatch[] = [];
|
||||
workflowSpans.forEach(span => {
|
||||
const taskIndex = span.extra?.task_index;
|
||||
if (taskIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (curTaskIndexs.includes(taskIndex)) {
|
||||
// 序号存在了,则新开启一组
|
||||
const batchSpan = genBatchSpan(curSpans, spanCategoryMap);
|
||||
if (batchSpan) {
|
||||
batchSpans.push(batchSpan);
|
||||
}
|
||||
curTaskIndexs = [];
|
||||
curSpans = [];
|
||||
}
|
||||
curTaskIndexs.push(taskIndex);
|
||||
curSpans.push(span);
|
||||
});
|
||||
|
||||
const batchSpan = genBatchSpan(curSpans, spanCategoryMap);
|
||||
if (batchSpan) {
|
||||
batchSpans.push(batchSpan);
|
||||
}
|
||||
});
|
||||
|
||||
return batchSpans;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理原始Span(将额外节点字段统一到extra)
|
||||
* @param span Span
|
||||
* @returns CSpanSingle
|
||||
*/
|
||||
// eslint-disable-next-line complexity -- 参数过多
|
||||
export const span2CSpan = function (
|
||||
span: Span,
|
||||
spanCategoryMap?: SpanCategoryMap,
|
||||
): CSpanSingle {
|
||||
const cspan: CSpanSingle = {
|
||||
trace_id: span.trace_id,
|
||||
id: span.id,
|
||||
parent_id: span.parent_id,
|
||||
name: span.name,
|
||||
type: span.type,
|
||||
start_time: Number(span.start_time),
|
||||
latency: Number(span.latency),
|
||||
status: span.status,
|
||||
category: spanCategoryMap?.[span.type],
|
||||
};
|
||||
cspan.extra =
|
||||
span.attr_user_input ??
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(span as any).attr_user_input_v2 ??
|
||||
span.attr_invoke_agent ??
|
||||
span.attr_restart_agent ??
|
||||
span.attr_switch_agent ??
|
||||
span.attr_llm_call ??
|
||||
span.attr_workflow_llm_call ??
|
||||
span.attr_llm_batch_call ??
|
||||
span.attr_workflow_llm_batch_call ??
|
||||
span.attr_workflow ??
|
||||
span.attr_workflow_end ??
|
||||
span.attr_code ??
|
||||
span.attr_workflow_code ??
|
||||
span.attr_code_batch ??
|
||||
span.attr_workflow_code_batch ??
|
||||
span.attr_condition ??
|
||||
span.attr_workflow_condition ??
|
||||
span.attr_plugin_tool ??
|
||||
span.attr_workflow_plugin_tool ??
|
||||
span.attr_plugin_tool_batch ??
|
||||
span.attr_workflow_plugin_tool_batch ??
|
||||
span.attr_knowledge ??
|
||||
span.attr_workflow_knowledge ??
|
||||
span.attr_card ??
|
||||
span.attr_workflow_message ??
|
||||
span.attr_chain ??
|
||||
span.attr_hook ??
|
||||
span.attr_bw_start ??
|
||||
span.attr_bw_end ??
|
||||
span.attr_bw_batch ??
|
||||
span.attr_bw_loop ??
|
||||
span.attr_bw_condition ??
|
||||
span.attr_bw_llm ??
|
||||
span.attr_bw_parallel ??
|
||||
span.attr_bw_variable ??
|
||||
span.attr_bw_call_flow ??
|
||||
span.attr_bw_connector ??
|
||||
span.attr_bw_script;
|
||||
return cspan;
|
||||
};
|
||||
|
||||
const genSpanCategoryMap = (
|
||||
spanCategoryMeta: SpanCategoryMeta,
|
||||
): SpanCategoryMap => {
|
||||
const map: SpanCategoryMap = {};
|
||||
Object.keys(spanCategoryMeta).forEach(key => {
|
||||
const spanCategoryValue = Number(key) as SpanCategory;
|
||||
const spanTypes = spanCategoryMeta[spanCategoryValue];
|
||||
spanTypes?.forEach(spanTypeValue => {
|
||||
map[spanTypeValue] = spanCategoryValue;
|
||||
});
|
||||
});
|
||||
return map as SpanCategoryMap;
|
||||
};
|
||||
|
||||
export const spans2CSpans = function (
|
||||
spans: Span[],
|
||||
spanCategoryMeta?: SpanCategoryMeta,
|
||||
): CSpan[] {
|
||||
const spanCategoryMap = spanCategoryMeta
|
||||
? genSpanCategoryMap(spanCategoryMeta)
|
||||
: undefined;
|
||||
|
||||
// 根据span.id进行去重
|
||||
const uniqSpans = uniqBy(spans, 'id');
|
||||
|
||||
// Span -> CSpanSingle
|
||||
const cSpans = uniqSpans.map(span => span2CSpan(span, spanCategoryMap));
|
||||
|
||||
const singleCSpans = cSpans.filter(({ type }) => !isBatchSpanType(type));
|
||||
// CSpanSingle[] -> CSpanBatch[]
|
||||
const batchCSpans = aggregationBatchSpan(
|
||||
cSpans.filter(({ type }) => isBatchSpanType(type)) as CSpanSingleForBatch[],
|
||||
spanCategoryMap,
|
||||
);
|
||||
|
||||
return [...singleCSpans, ...batchCSpans];
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 {
|
||||
SpanCategory,
|
||||
SpanStatus,
|
||||
SpanType,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import {
|
||||
type CTrace,
|
||||
type CSPanBatch,
|
||||
type CSpan,
|
||||
type CSpanSingle,
|
||||
} from '../typings/cspan';
|
||||
import { virtualStartSpanId } from '../constant';
|
||||
|
||||
export const getSpanProp = (span: CSpan | CTrace, key: string) => {
|
||||
if (checkIsBatchBasicCSpan(span)) {
|
||||
const batchSpan = span as CSPanBatch;
|
||||
return (
|
||||
batchSpan[key as keyof CSPanBatch] ??
|
||||
batchSpan.spans[0]?.extra?.[key as keyof CSPanBatch['spans'][0]['extra']]
|
||||
);
|
||||
} else {
|
||||
const singleSpan = span as CSpanSingle;
|
||||
return (
|
||||
singleSpan[key as keyof CSpanSingle] ??
|
||||
singleSpan.extra?.[key as keyof CSpanSingle['extra']]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTokens = (
|
||||
span: CSpan,
|
||||
): {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
} => {
|
||||
if ('spans' in span) {
|
||||
const spanBatch = span as CSPanBatch;
|
||||
let inputTokensSum = 0;
|
||||
let outputTokensSum = 0;
|
||||
spanBatch.spans.forEach(subSpan => {
|
||||
const inputTokens = subSpan?.extra?.input_tokens ?? 0;
|
||||
const outputTokens = subSpan?.extra?.output_tokens ?? 0;
|
||||
inputTokensSum += inputTokens;
|
||||
outputTokensSum += outputTokens;
|
||||
});
|
||||
return {
|
||||
input_tokens: inputTokensSum,
|
||||
output_tokens: outputTokensSum,
|
||||
};
|
||||
} else if (
|
||||
span.type === SpanType.UserInput ||
|
||||
span.type === SpanType.UserInputV2 ||
|
||||
span.type === SpanType.Workflow
|
||||
) {
|
||||
// SingleSpan节点 - Workflow
|
||||
const inputTokens = getSpanProp(span, 'input_tokens_sum') as number;
|
||||
const outputTokens = getSpanProp(span, 'output_tokens_sum') as number;
|
||||
|
||||
return {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
};
|
||||
} else {
|
||||
// SingleSpan节点 - 非workflow节点
|
||||
const inputTokens = getSpanProp(span, 'input_tokens') as number;
|
||||
const outputTokens = getSpanProp(span, 'output_tokens') as number;
|
||||
|
||||
return {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const isBatchSpanType = (type: SpanType): boolean => {
|
||||
const whites = [
|
||||
SpanType.LLMBatchCall,
|
||||
SpanType.WorkflowLLMBatchCall,
|
||||
SpanType.PluginToolBatch,
|
||||
SpanType.WorkflowPluginToolBatch,
|
||||
SpanType.CodeBatch,
|
||||
SpanType.WorkflowCodeBatch,
|
||||
];
|
||||
return whites.includes(type);
|
||||
};
|
||||
|
||||
export const checkIsBatchBasicCSpan = (span: CSpan | CTrace) => 'spans' in span;
|
||||
|
||||
export const isVisibleSpan = (span: CSpan) => {
|
||||
const whites = [
|
||||
SpanType.Unknown,
|
||||
SpanType.UserInput,
|
||||
SpanType.UserInputV2,
|
||||
SpanType.ThirdParty,
|
||||
SpanType.ScheduledTasks,
|
||||
SpanType.OpenDialog,
|
||||
SpanType.InvokeAgent,
|
||||
SpanType.RestartAgent,
|
||||
SpanType.SwitchAgent,
|
||||
SpanType.LLMCall,
|
||||
SpanType.WorkflowLLMCall,
|
||||
SpanType.LLMBatchCall,
|
||||
SpanType.WorkflowLLMBatchCall,
|
||||
SpanType.Workflow,
|
||||
SpanType.WorkflowStart,
|
||||
SpanType.WorkflowEnd,
|
||||
SpanType.PluginTool,
|
||||
SpanType.WorkflowPluginTool,
|
||||
SpanType.PluginToolBatch,
|
||||
SpanType.WorkflowPluginToolBatch,
|
||||
SpanType.Knowledge,
|
||||
SpanType.WorkflowKnowledge,
|
||||
SpanType.Code,
|
||||
SpanType.WorkflowCode,
|
||||
SpanType.CodeBatch,
|
||||
SpanType.WorkflowCodeBatch,
|
||||
SpanType.Condition,
|
||||
SpanType.WorkflowCondition,
|
||||
SpanType.Card,
|
||||
SpanType.WorkflowMessage,
|
||||
SpanType.Hook,
|
||||
SpanType.BWStart,
|
||||
SpanType.BWEnd,
|
||||
SpanType.BWBatch,
|
||||
SpanType.BWLoop,
|
||||
SpanType.BWCondition,
|
||||
SpanType.BWLLM,
|
||||
SpanType.BWParallel,
|
||||
SpanType.BWScript,
|
||||
SpanType.BWVariable,
|
||||
SpanType.BWCallFlow,
|
||||
SpanType.BWConnector,
|
||||
];
|
||||
|
||||
return whites.includes(span.type);
|
||||
};
|
||||
|
||||
export const genVirtualStart = (spans: CSpan[]): CSpan => {
|
||||
let startAt = Number.POSITIVE_INFINITY;
|
||||
let endAt = Number.NEGATIVE_INFINITY;
|
||||
spans.forEach(span => {
|
||||
startAt = Math.min(startAt, span.start_time);
|
||||
endAt = Math.max(endAt, span.start_time + span.latency);
|
||||
});
|
||||
if (
|
||||
startAt === Number.POSITIVE_INFINITY ||
|
||||
endAt === Number.NEGATIVE_INFINITY
|
||||
) {
|
||||
startAt = 0;
|
||||
endAt = 0;
|
||||
}
|
||||
return {
|
||||
id: virtualStartSpanId,
|
||||
parent_id: '',
|
||||
trace_id: '',
|
||||
name: '',
|
||||
type: SpanType.UserInput,
|
||||
category: SpanCategory.Start,
|
||||
status: SpanStatus.Unknown,
|
||||
start_time: startAt,
|
||||
latency: endAt - startAt,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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 ReactNode } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
type CSpanAttrInvokeAgent,
|
||||
type CSpan,
|
||||
type StreamingOutputStatus,
|
||||
} from '../typings/cspan';
|
||||
import {
|
||||
botEnvConfigMap,
|
||||
spanCategoryConfigMap,
|
||||
spanTypeConfigMap,
|
||||
streamingOutputStatusConfigMap,
|
||||
} from '../config/cspan';
|
||||
import { formatTime } from './format-time';
|
||||
import { getSpanProp } from './cspan';
|
||||
|
||||
export interface FieldItem {
|
||||
key?: string | ReactNode;
|
||||
value?: string | number | boolean | ReactNode;
|
||||
}
|
||||
|
||||
const getFieldCategory = (span: CSpan): FieldItem => {
|
||||
const { category } = span;
|
||||
const categoryConfig =
|
||||
category !== undefined ? spanCategoryConfigMap[category] : undefined;
|
||||
return {
|
||||
key: I18n.t('analytic_query_type'),
|
||||
value: categoryConfig?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldType = (span: CSpan): FieldItem => {
|
||||
const { type } = span;
|
||||
const typeConfig = spanTypeConfigMap[type];
|
||||
return {
|
||||
key: I18n.t('analytic_query_subtype'),
|
||||
value: typeConfig?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldOS = (span: CSpan): FieldItem => {
|
||||
const os = getSpanProp(span, 'os');
|
||||
const osVersion = getSpanProp(span, 'os_version');
|
||||
|
||||
return {
|
||||
key: I18n.t('analytic_query_os'),
|
||||
value: os ? `${os}${osVersion ? ` ${osVersion}` : ''}` : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldLatency = (span: CSpan): FieldItem => {
|
||||
const { latency } = span;
|
||||
return {
|
||||
key: I18n.t('analytic_query_latency'),
|
||||
value: latency !== undefined ? `${latency}ms` : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldName = (span: CSpan): FieldItem => {
|
||||
const { name } = span;
|
||||
return {
|
||||
key: I18n.t('analytic_query_name'),
|
||||
value: name,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldOffline = (span: CSpan): FieldItem => {
|
||||
const botEnv = getSpanProp(span, 'bot_env') as string;
|
||||
const botEnvConfig =
|
||||
botEnv !== undefined ? botEnvConfigMap[botEnv] : undefined;
|
||||
return {
|
||||
key: I18n.t('analytic_query_env'),
|
||||
value: botEnvConfig?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldStartTime = (span: CSpan): FieldItem => {
|
||||
const { start_time } = span;
|
||||
return {
|
||||
key: I18n.t('analytic_query_starttime'),
|
||||
value: formatTime(start_time),
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldEndTime = (span: CSpan): FieldItem => {
|
||||
const startAt = span?.start_time;
|
||||
const latency = span?.latency;
|
||||
return {
|
||||
key: I18n.t('analytic_query_endtime'),
|
||||
value:
|
||||
startAt !== undefined && latency !== undefined
|
||||
? formatTime(Number(startAt) + Number(latency))
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldCallType = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_calltype'),
|
||||
value: getSpanProp(span, 'call_type') as string,
|
||||
});
|
||||
|
||||
const getFieldAgentType = (_span: CSpan): FieldItem => {
|
||||
const span = _span as CSpanAttrInvokeAgent;
|
||||
return {
|
||||
key: I18n.t('analytic_query_agenttype'),
|
||||
value: span.extra?.agent_type,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldModel = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_model'),
|
||||
value: getSpanProp(span, 'model') as string,
|
||||
});
|
||||
|
||||
const getFieldTemperature = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_temperature'),
|
||||
value: getSpanProp(span, 'temperature') as string,
|
||||
});
|
||||
|
||||
const getFieldDialogRound = (_span: CSpan): FieldItem => {
|
||||
const span = _span as CSpanAttrInvokeAgent;
|
||||
return {
|
||||
key: I18n.t('analytic_query_diagloground'),
|
||||
value: span.extra?.dialog_round,
|
||||
};
|
||||
};
|
||||
|
||||
const getFieldMaxLengthResp = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_resmaxlen'),
|
||||
value: getSpanProp(span, 'max_length_resp') as string,
|
||||
});
|
||||
|
||||
const getFieldChannel = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_channel'),
|
||||
value: getSpanProp(span, 'channel') as string,
|
||||
});
|
||||
|
||||
const getFieldInputType = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_inputtype'),
|
||||
value: getSpanProp(span, 'query_input_method') as string,
|
||||
});
|
||||
|
||||
const getFieldInput = (span: CSpan): FieldItem => ({
|
||||
key: I18n.t('analytic_query_input'),
|
||||
value: getSpanProp(span, 'input') as string,
|
||||
});
|
||||
|
||||
const getStreamOutput = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'streaming_output') as StreamingOutputStatus;
|
||||
// key 为 starling key
|
||||
return {
|
||||
key: I18n.t('query_stream_output'),
|
||||
value: streamingOutputStatusConfigMap[value]?.label,
|
||||
};
|
||||
};
|
||||
|
||||
const getCardId = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'card_id') as string;
|
||||
return { key: I18n.t('query_card_id'), value };
|
||||
};
|
||||
|
||||
const getBranchName = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'branch_name') as string;
|
||||
return { key: 'branch_name', value };
|
||||
};
|
||||
|
||||
const getNodeType = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'node_type') as string;
|
||||
return { key: 'node_type', value };
|
||||
};
|
||||
|
||||
const getHookType = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'hook_type') as string;
|
||||
return {
|
||||
key: I18n.t('codedev_hook_hook_type'),
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getHookUri = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'hook_uri') as string;
|
||||
return {
|
||||
key: 'Hook Uri',
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getAgentId = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'agent_id') as string;
|
||||
return {
|
||||
key: 'AgentId',
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getHookRespCode = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'hook_resp_code')?.toString() as string;
|
||||
return {
|
||||
key: I18n.t('analytic_query_hook_resp_code'),
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
const getIsStream = (span: CSpan): FieldItem => {
|
||||
const value = getSpanProp(span, 'is_stream')?.toString() as string;
|
||||
return {
|
||||
key: I18n.t('query_stream_output'),
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export const fieldItemHandlers = {
|
||||
category: getFieldCategory,
|
||||
type: getFieldType,
|
||||
os: getFieldOS,
|
||||
latency: getFieldLatency,
|
||||
offline: getFieldOffline,
|
||||
name: getFieldName,
|
||||
start_time: getFieldStartTime,
|
||||
end_time: getFieldEndTime,
|
||||
call_type: getFieldCallType,
|
||||
agent_type: getFieldAgentType,
|
||||
model: getFieldModel,
|
||||
temperature: getFieldTemperature,
|
||||
dialog_round: getFieldDialogRound,
|
||||
max_length_resp: getFieldMaxLengthResp,
|
||||
channel: getFieldChannel,
|
||||
input_type: getFieldInputType,
|
||||
input: getFieldInput,
|
||||
card_id: getCardId,
|
||||
stream_output: getStreamOutput,
|
||||
branch_name: getBranchName,
|
||||
node_type: getNodeType,
|
||||
hook_type: getHookType,
|
||||
hook_uri: getHookUri,
|
||||
agent_id: getAgentId,
|
||||
hook_resp_code: getHookRespCode,
|
||||
is_stream: getIsStream,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 dayjs from 'dayjs';
|
||||
|
||||
export const formatTime = (timestamp?: number | string) =>
|
||||
dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||
@@ -0,0 +1 @@
|
||||
公共API目录, 基于bam接口的二次封装
|
||||
@@ -0,0 +1 @@
|
||||
公共样式目录
|
||||
20
frontend/packages/devops/common-modules/src/typings.d.ts
vendored
Normal file
20
frontend/packages/devops/common-modules/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: { [key: string]: string };
|
||||
export = resource;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
公共类型目录
|
||||
@@ -0,0 +1 @@
|
||||
公共util目录
|
||||
Reference in New Issue
Block a user