feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
31
frontend/packages/devops/common-modules/.storybook/main.js
Normal file
31
frontend/packages/devops/common-modules/.storybook/main.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
5
frontend/packages/devops/common-modules/.stylelintrc.js
Normal file
5
frontend/packages/devops/common-modules/.stylelintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
34
frontend/packages/devops/common-modules/README.md
Normal file
34
frontend/packages/devops/common-modules/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# @coze-devops/common-modules
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## 目录结构说明
|
||||
|
||||
``` bash
|
||||
├── __tests__
|
||||
├── .storybook
|
||||
├── config
|
||||
├── src
|
||||
│ ├── assets ## 公共静态资源
|
||||
│ │ ├── react.svg
|
||||
│ │ └── rspack.png
|
||||
│ ├── components ## 公共组件
|
||||
│ ├── hooks ## 公共组件
|
||||
│ ├── index.tsx ## 对外统一出口, 导出内容类型可以是:component, hook, util, typing
|
||||
│ ├── modules ## 模块集合:子目录按业务模块划分;另外,index.tsx中导出的资源都是来自于modules目录
|
||||
│ │ └── query-trace
|
||||
│ ├── services ## 请求 api 封装
|
||||
│ ├── styles ## 公共样式
|
||||
│ ├── typings ## 公共类型
|
||||
│ ├── typings.d.ts
|
||||
│ └── utils ## 公共工具库
|
||||
├── stories ## 文档
|
||||
├── .eslintrc.js
|
||||
├── .stylelintrc.js
|
||||
├── OWNERS
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── tsconfig.build.json
|
||||
├── tsconfig.json
|
||||
└── vitest.config.ts
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
11
frontend/packages/devops/common-modules/eslint.config.js
Normal file
11
frontend/packages/devops/common-modules/eslint.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {
|
||||
'@coze-arch/max-line-per-function': 'off',
|
||||
'max-lines-per-function': 'off',
|
||||
'max-lines': 'off',
|
||||
},
|
||||
});
|
||||
70
frontend/packages/devops/common-modules/package.json
Normal file
70
frontend/packages/devops/common-modules/package.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "@coze-devops/common-modules",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "common business modules for devops",
|
||||
"license": "Apache-2.0",
|
||||
"author": "wangke.789@bytedance.com",
|
||||
"maintainers": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./query-trace": "./src/modules/query-trace/index.ts",
|
||||
"./tree": "./src/modules/query-trace/components/tree/index.tsx"
|
||||
},
|
||||
"main": "src/index.tsx",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"query-trace": [
|
||||
"./src/modules/query-trace/index.ts"
|
||||
],
|
||||
"tree": [
|
||||
"./src/modules/query-trace/components/tree/index.tsx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-hooks": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@dagrejs/dagre": "^1.0.2",
|
||||
"@visactor/vgrammar": "0.12.5-alpha.4",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reactflow": "^11.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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目录
|
||||
44
frontend/packages/devops/common-modules/tsconfig.build.json
Normal file
44
frontend/packages/devops/common-modules/tsconfig.build.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
"strictNullChecks": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../arch/bot-api/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-flags/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/bot-hooks/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../arch/i18n/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-icons/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../components/bot-semi/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/stylelint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/devops/common-modules/tsconfig.json
Normal file
15
frontend/packages/devops/common-modules/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
17
frontend/packages/devops/common-modules/tsconfig.misc.json
Normal file
17
frontend/packages/devops/common-modules/tsconfig.misc.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.web.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/globals"],
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
22
frontend/packages/devops/common-modules/vitest.config.ts
Normal file
22
frontend/packages/devops/common-modules/vitest.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { mergeConfig } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: config =>
|
||||
mergeConfig(config, {
|
||||
plugins: [
|
||||
svgr({
|
||||
svgrOptions: {
|
||||
native: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @type { import('@storybook/react').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,5 @@
|
||||
const { defineConfig } = require('@coze-arch/stylelint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
extends: [],
|
||||
});
|
||||
16
frontend/packages/devops/debug/debug-panel/README.md
Normal file
16
frontend/packages/devops/debug/debug-panel/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# @coze-devops/debug-panel
|
||||
|
||||
> Project template for react component with storybook.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] eslint & ts
|
||||
- [x] esm bundle
|
||||
- [x] umd bundle
|
||||
- [x] storybook
|
||||
|
||||
## Commands
|
||||
|
||||
- init: `rush update`
|
||||
- dev: `npm run dev`
|
||||
- build: `npm run build`
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
76
frontend/packages/devops/debug/debug-panel/package.json
Normal file
76
frontend/packages/devops/debug/debug-panel/package.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "@coze-devops/debug-panel",
|
||||
"version": "0.0.1",
|
||||
"description": "coze debug panel",
|
||||
"license": "Apache-2.0",
|
||||
"author": "lukexian.bryce@bytedance.com",
|
||||
"maintainers": [],
|
||||
"main": "src/index.ts",
|
||||
"typings": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-devops/json-link-preview": "workspace:*",
|
||||
"immer": "^10.0.3",
|
||||
"json-bigint": "~1.0.0",
|
||||
"qs": "^6.11.2",
|
||||
"re-resizable": "~6.9.11",
|
||||
"react-json-view": "~1.21.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-env": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-tea": "workspace:*",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-arch/logger": "workspace:*",
|
||||
"@coze-arch/stylelint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@coze-devops/common-modules": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/json-bigint": "~1.0.4",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"tailwindcss": "~3.3.3",
|
||||
"vite-plugin-svgr": "~3.3.0",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@coze-arch/bot-api": "workspace:*",
|
||||
"@coze-arch/bot-env": "workspace:*",
|
||||
"@coze-arch/bot-flags": "workspace:*",
|
||||
"@coze-arch/bot-icons": "workspace:*",
|
||||
"@coze-arch/bot-semi": "workspace:*",
|
||||
"@coze-arch/bot-tea": "workspace:*",
|
||||
"@coze-arch/i18n": "workspace:*",
|
||||
"@coze-devops/common-modules": "workspace:*",
|
||||
"@douyinfe/semi-icons": "^2.36.0",
|
||||
"@douyinfe/semi-illustrations": "^2.36.0",
|
||||
"ahooks": "^3.7.8",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0",
|
||||
"zustand": "^4.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/* stylelint-disable custom-property-pattern */
|
||||
@import '../common/common.module.less';
|
||||
|
||||
.chat-trace-tabs {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
:global {
|
||||
.semi-tabs-content {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.semi-tabs-pane-motion-overlay {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-trace-tabs-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 24px 24px 10px;
|
||||
|
||||
.chat-trace-tabs-bar-tab-bar {
|
||||
cursor: pointer;
|
||||
|
||||
padding: 0 4px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
color: var(--Light-usage-text---color-text-1, rgb(29 28 35 / 80%));
|
||||
|
||||
&.active {
|
||||
color: var(--Light-color-brand---brand-5, #4D53E8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-trace-tab-pane_scroll {
|
||||
.webkit-scrollbar_mixin();
|
||||
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-trace-tree {
|
||||
overflow: visible;
|
||||
height: 100%;
|
||||
padding: 8px 24px 20px;
|
||||
}
|
||||
|
||||
.chat-flamethread {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
padding: 0 10px 20px 24px;
|
||||
}
|
||||
|
||||
.resize-container-chat {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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 PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Resizable } from 're-resizable';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
type CSpan,
|
||||
DataSourceTypeEnum,
|
||||
TraceFlamethread,
|
||||
TraceTree,
|
||||
type InteractionEventHandler,
|
||||
type CTrace,
|
||||
} from '@coze-devops/common-modules/query-trace';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TabsProps } from '@coze-arch/bot-semi/Tabs';
|
||||
import { TabPane, Tabs } from '@coze-arch/bot-semi';
|
||||
|
||||
import { DebugPanelLayout } from '../../../typings';
|
||||
import { useDebugPanelStore } from '../../../store';
|
||||
import { useDebugPanelLayoutConfig } from '../../../hooks/use-debug-panel-layout-config';
|
||||
import {
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO,
|
||||
GraphTabEnum,
|
||||
} from '../../../consts/static';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface PanelChartProps {
|
||||
rootSpan: CTrace;
|
||||
spans: CSpan[];
|
||||
targetDetailSpan?: CSpan;
|
||||
onTargetDetailSpanChange: (detailSpan: CSpan) => void;
|
||||
}
|
||||
|
||||
const ChartResizableArea = (props: PropsWithChildren) => {
|
||||
const { children } = props;
|
||||
const [layoutConfig, setLayoutConfig] = useDebugPanelLayoutConfig();
|
||||
return (
|
||||
<Resizable
|
||||
minHeight={
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO.side[DebugPanelLayout.Chat]
|
||||
.height.min
|
||||
}
|
||||
maxHeight={
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO.side[DebugPanelLayout.Chat]
|
||||
.height.max
|
||||
}
|
||||
minWidth="100%"
|
||||
enable={{
|
||||
bottom: true,
|
||||
}}
|
||||
defaultSize={{
|
||||
height: layoutConfig.side[DebugPanelLayout.Chat],
|
||||
width: '100%',
|
||||
}}
|
||||
// eslint-disable-next-line max-params
|
||||
onResizeStop={(e, d, el, delta) => {
|
||||
setLayoutConfig(config => {
|
||||
config.side[DebugPanelLayout.Chat] += delta.height;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className={classNames(s['resize-container-chat'])}>{children}</div>
|
||||
</Resizable>
|
||||
);
|
||||
};
|
||||
|
||||
export const PanelChart = (props: PanelChartProps) => {
|
||||
const { rootSpan, spans, targetDetailSpan, onTargetDetailSpanChange } = props;
|
||||
const {
|
||||
basicInfo: { spaceID },
|
||||
} = useDebugPanelStore(
|
||||
useShallow(state => ({
|
||||
basicInfo: state.basicInfo,
|
||||
})),
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<GraphTabEnum>(
|
||||
GraphTabEnum.RunTree,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onTargetDetailSpanChange(rootSpan as CSpan);
|
||||
}, [rootSpan.id]);
|
||||
|
||||
const renderTabBar = useCallback<
|
||||
Exclude<TabsProps['renderTabBar'], undefined>
|
||||
>(tabBarProps => {
|
||||
const { activeKey, list } = tabBarProps;
|
||||
|
||||
return (
|
||||
<div className={s['chat-trace-tabs-bar']}>
|
||||
{list?.map(({ tab, itemKey }) => (
|
||||
<div
|
||||
className={classNames(s['chat-trace-tabs-bar-tab-bar'], {
|
||||
[s.active]: activeKey === itemKey,
|
||||
})}
|
||||
key={itemKey}
|
||||
onClick={() => setActiveTab(itemKey as GraphTabEnum)}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onFlamethreadClick: InteractionEventHandler = useCallback(
|
||||
(_, element) => {
|
||||
const { span } = element?.data?.[0]?.extra as { span: CSpan };
|
||||
if (span?.id !== undefined) {
|
||||
onTargetDetailSpanChange(span);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartResizableArea>
|
||||
<Tabs
|
||||
className={s['chat-trace-tabs']}
|
||||
activeKey={activeTab}
|
||||
renderTabBar={renderTabBar}
|
||||
>
|
||||
<TabPane
|
||||
tab={I18n.t('query_run_tree')}
|
||||
itemKey={GraphTabEnum.RunTree}
|
||||
className={s['chat-trace-tab-pane_scroll']}
|
||||
>
|
||||
<TraceTree
|
||||
className={s['chat-trace-tree']}
|
||||
dataSource={{
|
||||
type: DataSourceTypeEnum.SpanData,
|
||||
spanData: spans,
|
||||
}}
|
||||
spaceId={spaceID}
|
||||
selectedSpanId={targetDetailSpan?.id}
|
||||
onSelect={({ node }) => {
|
||||
const { span } = node.extra as { span: CSpan };
|
||||
if (span?.id !== undefined) {
|
||||
onTargetDetailSpanChange(span);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={I18n.t('query_flamethread')}
|
||||
className="h-full overflow-hidden"
|
||||
itemKey={GraphTabEnum.Flamethread}
|
||||
>
|
||||
<div className={s['chat-flamethread']}>
|
||||
<TraceFlamethread
|
||||
dataSource={{
|
||||
type: DataSourceTypeEnum.SpanData,
|
||||
spanData: spans,
|
||||
}}
|
||||
disableViewScroll
|
||||
selectedSpanId={targetDetailSpan?.id}
|
||||
axisLabelSuffix="ms"
|
||||
globalStyle={{
|
||||
height: '100%',
|
||||
}}
|
||||
onClick={onFlamethreadClick}
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</ChartResizableArea>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
@scrollbar-size: 11px;
|
||||
@scrollbar-padding: 2px;
|
||||
@transition-timing-function-standard: cubic-bezier(0.34, 0.69, 0.1, 1);
|
||||
|
||||
.webkit-scrollbar_mixin() {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(125 125 125 / 30%);
|
||||
background-clip: padding-box;
|
||||
border: @scrollbar-padding solid transparent;
|
||||
border-radius: 9999px;
|
||||
|
||||
transition: background .2s @transition-timing-function-standard;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: @scrollbar-size;
|
||||
height: @scrollbar-size;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: rgb(125 125 125 / 60%) !important;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar:hover {
|
||||
width: @scrollbar-size;
|
||||
height: @scrollbar-size;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
.description-container {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: space-between;
|
||||
|
||||
.description-container-box {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 0;
|
||||
|
||||
.description-container-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
|
||||
.description-container-item-key {
|
||||
flex-shrink: 0;
|
||||
color: var(--Light-usage-text---color-text-3, rgb(29 28 35 / 35%));
|
||||
}
|
||||
|
||||
.description-container-item-value {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 0;
|
||||
color: var(--Light-usage-text---color-text-1, rgb(29 28 35 / 80%));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 20px;
|
||||
margin: 20px 0 17px;
|
||||
|
||||
.node-detail-title-left {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--Light-color-black---black, #000);
|
||||
}
|
||||
|
||||
.node-detail-title-right {
|
||||
font-size: 12px;
|
||||
color: var(--Light-usage-text---color-text-2, rgb(29 28 35 / 60%));
|
||||
}
|
||||
}
|
||||
|
||||
.common-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
:global(.semi-typography-action-copied) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.common-text-content {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
color: #6b6b75;
|
||||
}
|
||||
|
||||
.description-container-with-full-line {
|
||||
.description-container-with-full-line-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
|
||||
&:nth-child(2) {
|
||||
min-width: 182px;
|
||||
}
|
||||
|
||||
.description-container-with-full-line-item-key {
|
||||
flex-shrink: 0;
|
||||
color: var(--Light-usage-text---color-text-3, rgb(29 28 35 / 35%));
|
||||
}
|
||||
|
||||
.description-container-with-full-line-item-value {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 0;
|
||||
color: var(--Light-usage-text---color-text-1, rgb(29 28 35 / 80%));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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 classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TextProps } from '@coze-arch/bot-semi/Typography';
|
||||
import { Typography } from '@coze-arch/bot-semi';
|
||||
import { IconCopy } from '@coze-arch/bot-icons';
|
||||
import { IconTick } from '@douyinfe/semi-icons';
|
||||
|
||||
import { textWithFallback } from '../../../utils';
|
||||
import { type FieldCol } from '../../../typings';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface DebugTextProps extends TextProps {
|
||||
text?: string;
|
||||
useCopy?: boolean;
|
||||
}
|
||||
|
||||
export const DebugText = (props: DebugTextProps) => {
|
||||
const { text, useCopy, ...otherProps } = props;
|
||||
|
||||
return (
|
||||
<div className={s['common-container']}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: {
|
||||
className: s['common-text-content'],
|
||||
position: 'bottom',
|
||||
},
|
||||
},
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
{textWithFallback(text)}
|
||||
</Text>
|
||||
{useCopy ? (
|
||||
<Text
|
||||
copyable={{
|
||||
icon: <IconCopy className={s['copy-icon']} />,
|
||||
successTip: <IconTick />,
|
||||
content: text,
|
||||
copyTip: I18n.t('query_detail_tip_copy'),
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface NodeDescriptionProps {
|
||||
cols: FieldCol[];
|
||||
}
|
||||
|
||||
export const NodeDescription = (props: NodeDescriptionProps) => {
|
||||
const { cols } = props;
|
||||
|
||||
return (
|
||||
<div className={s['description-container']}>
|
||||
{cols.map((col, colIndex) => {
|
||||
const { fields } = col;
|
||||
return (
|
||||
<div className={s['description-container-box']} key={colIndex}>
|
||||
{fields.map((field, fieldIndex) => {
|
||||
const { key, value, options } = field;
|
||||
return (
|
||||
<div
|
||||
key={fieldIndex}
|
||||
className={s['description-container-item']}
|
||||
>
|
||||
<div className={s['description-container-item-key']}>
|
||||
{key} :
|
||||
</div>
|
||||
<div className={s['description-container-item-value']}>
|
||||
{value === undefined || typeof value === 'string' ? (
|
||||
<DebugText
|
||||
text={value}
|
||||
useCopy={options?.copyable}
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface NodeDetailTitleProps {
|
||||
text: string;
|
||||
copyContent?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const NodeDetailTitle = (props: NodeDetailTitleProps) => {
|
||||
const { text, copyContent, description } = props;
|
||||
|
||||
return (
|
||||
<div className={s['node-detail-title']}>
|
||||
<Text
|
||||
className={classNames(s['node-detail-title-left'], s['common-text'])}
|
||||
copyable={
|
||||
copyContent
|
||||
? {
|
||||
content: copyContent,
|
||||
icon: <IconCopy className={s['copy-icon']} />,
|
||||
successTip: <IconTick />,
|
||||
copyTip: I18n.t('query_detail_tip_copy'),
|
||||
}
|
||||
: false
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{description ? (
|
||||
<div className={s['node-detail-title-right']}>{description}</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NodeDescriptionWithFullLine = (props: NodeDescriptionProps) => {
|
||||
const { cols } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'gap-x-[18px] flex flex-wrap justify-between',
|
||||
s['description-container-with-full-line'],
|
||||
)}
|
||||
>
|
||||
{cols.map(item => {
|
||||
const { fields } = item;
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
const { value, key, options } = field;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames(
|
||||
s['description-container-with-full-line-item'],
|
||||
{
|
||||
'!w-full': options?.fullLine,
|
||||
'flex-1': !options?.fullLine,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
s['description-container-with-full-line-item-key']
|
||||
}
|
||||
>
|
||||
{key} :
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
s['description-container-with-full-line-item-value']
|
||||
}
|
||||
>
|
||||
{value === undefined || typeof value === 'string' ? (
|
||||
<DebugText
|
||||
text={value}
|
||||
useCopy={options?.copyable}
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
) : (
|
||||
<>{value}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
@import '../common/common.module.less';
|
||||
|
||||
.detail-container {
|
||||
.detail-title-container {
|
||||
padding: 24px 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.detail-border-box {
|
||||
.webkit-scrollbar_mixin();
|
||||
|
||||
overflow: auto;
|
||||
|
||||
max-height: 340px;
|
||||
padding: 8px 4px;
|
||||
|
||||
font-family: Menlo;
|
||||
|
||||
border: 1px solid #1D1C2314;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-border-box_error {
|
||||
border: 1px solid #FF441E;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 12px;
|
||||
color: #1D1C23;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.detail-pagination {
|
||||
margin-bottom: 10px;
|
||||
|
||||
:global(.semi-page-prev),
|
||||
:global(.semi-page-next) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.semi-page-item) {
|
||||
background: transparent;
|
||||
border: 1px solid rgb(29 28 35 / 8%);
|
||||
border-radius: 8px;
|
||||
|
||||
&:global(.semi-page-item-active),
|
||||
&:global(:hover) {
|
||||
font-weight: 600;
|
||||
color: #4D53E8;
|
||||
|
||||
background: #F1F2FD;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-json-container {
|
||||
:global(.string-value) {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.topology-flow {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
padding: 24px 0;
|
||||
|
||||
.topology-flow-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.topology-flow-header-title {
|
||||
margin-right: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1D1C23;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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 @coze-arch/max-line-per-function */
|
||||
import ReactJson from 'react-json-view';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { JsonLinkPreview } from '@coze-devops/json-link-preview';
|
||||
import {
|
||||
type CSpanSingle,
|
||||
type CSPanBatch,
|
||||
type CSpan,
|
||||
TopologyFlow,
|
||||
DataSourceTypeEnum,
|
||||
isBatchSpanType,
|
||||
} from '@coze-devops/common-modules/query-trace';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Divider, Pagination, Tag } from '@coze-arch/bot-semi';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { NodeDescription, NodeDetailTitle } from '../common';
|
||||
import { jsonParse, textWithFallback } from '../../../utils';
|
||||
import { useSpanCols } from '../../../hooks/use-span-cols';
|
||||
import { useBatchSpanCols } from '../../../hooks/use-batch-span-cols';
|
||||
import {
|
||||
REACT_JSON_VIEW_CONFIG,
|
||||
topologyTypeConfig,
|
||||
} from '../../../consts/static';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface PanelDetailProps {
|
||||
botId: string;
|
||||
spaceId: string;
|
||||
spans: CSpan[];
|
||||
targetDetailSpan: CSpan;
|
||||
curBatchPage: number;
|
||||
setCurBatchPage: (curBatchPage: number) => void;
|
||||
}
|
||||
|
||||
export const PanelDetail = (props: PanelDetailProps) => {
|
||||
const {
|
||||
botId,
|
||||
spaceId,
|
||||
spans,
|
||||
targetDetailSpan,
|
||||
curBatchPage,
|
||||
setCurBatchPage,
|
||||
} = props;
|
||||
|
||||
const isBatchNode = useMemo(
|
||||
() => isBatchSpanType(targetDetailSpan.type),
|
||||
[targetDetailSpan],
|
||||
);
|
||||
|
||||
const { spanCols } = useSpanCols({ span: targetDetailSpan });
|
||||
|
||||
const { batchSpanCols } = useBatchSpanCols({
|
||||
span: targetDetailSpan,
|
||||
curBatchIndex: curBatchPage - 1,
|
||||
});
|
||||
|
||||
const batchArea = useMemo(() => {
|
||||
if (!isBatchNode) {
|
||||
return null;
|
||||
}
|
||||
const batchSpan = targetDetailSpan as CSPanBatch;
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeDetailTitle text={I18n.t('query_select_batch')} />
|
||||
<Pagination
|
||||
className={s['detail-pagination']}
|
||||
total={batchSpan.spans.length}
|
||||
pageSize={1}
|
||||
currentPage={curBatchPage}
|
||||
onPageChange={setCurBatchPage}
|
||||
/>
|
||||
<NodeDescription cols={batchSpanCols} />
|
||||
</>
|
||||
);
|
||||
}, [isBatchNode, targetDetailSpan, curBatchPage, batchSpanCols]);
|
||||
|
||||
const inputArea = useMemo(() => {
|
||||
const span = isBatchNode
|
||||
? (targetDetailSpan as CSPanBatch)?.spans[curBatchPage - 1]
|
||||
: (targetDetailSpan as CSpanSingle);
|
||||
|
||||
const inputValue = jsonParse(
|
||||
textWithFallback(span?.extra?.input) as string,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeDetailTitle
|
||||
text={I18n.t('query_detail_title_input')}
|
||||
copyContent={
|
||||
typeof inputValue === 'string'
|
||||
? inputValue
|
||||
: JSON.stringify(inputValue)
|
||||
}
|
||||
/>
|
||||
<div className={s['detail-border-box']}>
|
||||
{typeof inputValue === 'string' ? (
|
||||
<div className={s['detail-text']}>{inputValue}</div>
|
||||
) : (
|
||||
<div className={s['react-json-container']}>
|
||||
{Array.isArray(inputValue) ? (
|
||||
<JsonLinkPreview
|
||||
src={inputValue}
|
||||
bot_id={botId}
|
||||
space_id={spaceId}
|
||||
/>
|
||||
) : (
|
||||
<ReactJson
|
||||
src={inputValue}
|
||||
{...REACT_JSON_VIEW_CONFIG}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}, [isBatchNode, targetDetailSpan, curBatchPage]);
|
||||
|
||||
const outputArea = useMemo(() => {
|
||||
const span = isBatchNode
|
||||
? (targetDetailSpan as CSPanBatch)?.spans[curBatchPage - 1]
|
||||
: (targetDetailSpan as CSpanSingle);
|
||||
|
||||
const { status } = span;
|
||||
const outputValue = jsonParse(
|
||||
textWithFallback(span?.extra?.output) as string,
|
||||
);
|
||||
|
||||
const isError = status === SpanStatus.Error;
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeDetailTitle
|
||||
text={I18n.t('query_detail_title_output')}
|
||||
copyContent={
|
||||
typeof outputValue === 'string'
|
||||
? outputValue
|
||||
: JSON.stringify(outputValue)
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
s['detail-border-box'],
|
||||
isError && s['detail-border-box_error'],
|
||||
)}
|
||||
>
|
||||
{typeof outputValue === 'string' ? (
|
||||
<div className={s['detail-text']}>{outputValue}</div>
|
||||
) : (
|
||||
<div className={s['react-json-container']}>
|
||||
{Array.isArray(outputValue) ? (
|
||||
<JsonLinkPreview
|
||||
src={outputValue}
|
||||
bot_id={botId}
|
||||
space_id={spaceId}
|
||||
/>
|
||||
) : (
|
||||
<ReactJson
|
||||
src={outputValue}
|
||||
{...REACT_JSON_VIEW_CONFIG}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}, [isBatchNode, targetDetailSpan, curBatchPage]);
|
||||
|
||||
const topologyArea = useMemo(
|
||||
() => (
|
||||
<TopologyFlow
|
||||
botId={botId}
|
||||
spaceId={spaceId}
|
||||
dataSource={{
|
||||
type: DataSourceTypeEnum.SpanData,
|
||||
spanData: spans,
|
||||
}}
|
||||
selectedSpanId={targetDetailSpan.id}
|
||||
className={s['topology-flow']}
|
||||
renderHeader={type => (
|
||||
<div className={s['topology-flow-header']}>
|
||||
<div className={s['topology-flow-header-title']}>
|
||||
{I18n.t('analytic_query_detail_topology')}
|
||||
</div>
|
||||
<Tag size="small" style={{ top: 1 }}>
|
||||
{topologyTypeConfig[type]}
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
[botId, spaceId, spans, targetDetailSpan.id],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={s['detail-container']}>
|
||||
<div className={s['detail-title-container']}>
|
||||
{I18n.t('query_node_details')}
|
||||
</div>
|
||||
<NodeDescription cols={spanCols} />
|
||||
<Divider margin={24} />
|
||||
{batchArea}
|
||||
{inputArea}
|
||||
{outputArea}
|
||||
{topologyArea}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
height: 56px;
|
||||
padding: 16px 24px;
|
||||
|
||||
background-color: var(--light-color-grey-grey-0, #f7f7fa);
|
||||
border-bottom: 1px solid var(--light-usage-border-color-border, rgb(29 28 35 / 8%));
|
||||
|
||||
.panel-header-title {
|
||||
flex-shrink: 0;
|
||||
|
||||
margin-right: 24px;
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
color: var(--light-usage-text-color-text-0, #1c1d23);
|
||||
}
|
||||
|
||||
.panel-header-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
width: 0;
|
||||
|
||||
.panel-header-option-icon {
|
||||
color: var(--light-usage-text-color-text-2, rgb(29 28 35 / 60%));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { UIButton } from '@coze-arch/bot-semi';
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface PanelHeaderProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const PanelHeader = (props: PanelHeaderProps) => {
|
||||
const { onClose } = props;
|
||||
return (
|
||||
<div className={s['panel-header']}>
|
||||
<div className={s['panel-header-title']}>
|
||||
{I18n.t('debug_detail_tab')}
|
||||
</div>
|
||||
<div className={s['panel-header-option']}>
|
||||
<UIButton
|
||||
className={s['panel-header-option-icon']}
|
||||
theme="borderless"
|
||||
icon={<IconClose />}
|
||||
size="small"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
|
||||
import { useDebugPanelStore } from '../../store';
|
||||
import { SideDebugPanel } from './side-panel';
|
||||
|
||||
export interface DebugPanelProps {
|
||||
botId: string;
|
||||
spaceID?: string;
|
||||
userID?: string;
|
||||
placement: 'left';
|
||||
currentQueryLogId: string;
|
||||
isShow: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DebugPanel = (props: DebugPanelProps) => {
|
||||
const {
|
||||
botId,
|
||||
spaceID,
|
||||
userID,
|
||||
placement,
|
||||
currentQueryLogId,
|
||||
isShow,
|
||||
onClose,
|
||||
} = props;
|
||||
const { setBasicInfo, setEntranceMessageLogId, setIsPanelShow, resetStore } =
|
||||
useDebugPanelStore();
|
||||
|
||||
useEffect(() => {
|
||||
setBasicInfo({
|
||||
botId,
|
||||
spaceID,
|
||||
userID,
|
||||
placement,
|
||||
});
|
||||
setEntranceMessageLogId(currentQueryLogId);
|
||||
setIsPanelShow(isShow);
|
||||
}, [botId, spaceID, userID, placement, isShow, currentQueryLogId]);
|
||||
|
||||
useEffect(() => {
|
||||
sendTeaEvent(EVENT_NAMES.debug_page_show, {
|
||||
bot_id: botId,
|
||||
workspace_id: spaceID,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onDebugPanelClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
resetStore();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return <SideDebugPanel onClose={onDebugPanelClose} />;
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
@import '../common/common.module.less';
|
||||
|
||||
.query-filter {
|
||||
width: 100%;
|
||||
|
||||
.query-filter-options {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
height: 32px;
|
||||
padding-right: 6px;
|
||||
padding-left: 8px;
|
||||
|
||||
border: 1px solid rgb(29 28 35 / 8%);
|
||||
border-right: none;
|
||||
border-radius: 8px 0 0 8px;
|
||||
|
||||
.query-filter-options-button_active {
|
||||
background: rgb(46 46 56 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.query-filter-select {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
|
||||
.query-filter-select-search-icon {
|
||||
padding: 0 8px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.query-filter-select-tag {
|
||||
overflow: hidden;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.query-execute-status_success {
|
||||
color: var(#1D1C23);
|
||||
background: var(#F0F0F5);
|
||||
}
|
||||
|
||||
.query-execute-status_broken {
|
||||
color: var(#4D53E8);
|
||||
background: var(#F1F2FD);
|
||||
}
|
||||
|
||||
.query-execute-status_error {
|
||||
color: var(#FF441E);
|
||||
background: var(#FFF3EE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.query-filter-select-dropdown {
|
||||
:global(.semi-select-loading-wrapper) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
:global(.semi-select-option-selected .semi-select-option-icon) {
|
||||
color: #4D53E8;
|
||||
}
|
||||
|
||||
.query-filter-select-dropdown-option {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 0;
|
||||
|
||||
font-size: 12px;
|
||||
color: var(rgb(29 28 35 / 60%));
|
||||
|
||||
.query-filter-select-dropdown-option-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-filter-select-dropdown-option-text {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.query-filter-select-dropdown-option-time {
|
||||
color: var(rgb(29 28 35 / 35%));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
:global(.semi-dropdown-item > .semi-icon) {
|
||||
color: #4D53E8;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-tooltip {
|
||||
padding-right: 0;
|
||||
|
||||
:global(.semi-tooltip-content) {
|
||||
overflow-y: auto;
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
max-height: 500px;
|
||||
padding-right: 12px;
|
||||
.webkit-scrollbar_mixin();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, type PropsWithChildren, useRef } from 'react';
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
type CSpanSingle,
|
||||
type CSpan,
|
||||
} from '@coze-devops/common-modules/query-trace';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
InputGroup,
|
||||
Select,
|
||||
Tag,
|
||||
Tooltip,
|
||||
UIButton,
|
||||
} from '@coze-arch/bot-semi';
|
||||
import { IconCalendar, IconFilter, IconSearch } from '@coze-arch/bot-icons';
|
||||
import { type SpanStatus } from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import { getPastWeekDates, getTimeInCurrentTimeZone } from '../../../utils';
|
||||
import {
|
||||
type TargetOverallSpanInfo,
|
||||
type QueryFilterItem,
|
||||
type QueryFilterItemId,
|
||||
} from '../../../typings';
|
||||
import { EXECUTE_STATUS_FILTERING_OPTIONS } from '../../../consts/static';
|
||||
import { SPAN_STATUS_CONFIG_MAP } from '../../../consts/span';
|
||||
import {
|
||||
FILTERING_OPTION_ALL,
|
||||
QUERY_FILTER_DEBOUNCE_TIME,
|
||||
} from '../../../consts';
|
||||
|
||||
import s from './index.module.less';
|
||||
export interface QueryFilterProps {
|
||||
targetDateId?: QueryFilterItemId;
|
||||
targetExecuteStatusId?: QueryFilterItemId;
|
||||
targetOverallSpanInfo?: TargetOverallSpanInfo;
|
||||
enhancedOverallSpans: CSpan[];
|
||||
showLoadMore: boolean;
|
||||
onSelectDate: (dateId: QueryFilterItemId) => void;
|
||||
onSelectExecuteStatus: (executeStatusId: QueryFilterItemId) => void;
|
||||
onFetchQuery: (inputSearch?: string, loadMore?: boolean) => Promise<CSpan[]>;
|
||||
onSelectQuery: (overallSpanInfo: TargetOverallSpanInfo) => void;
|
||||
}
|
||||
|
||||
export interface FilterDropdownProps {
|
||||
dropdownMenuItem: QueryFilterItem[];
|
||||
activeId?: QueryFilterItemId;
|
||||
onSelectActiveId?: (id: QueryFilterItemId) => void;
|
||||
}
|
||||
|
||||
const FilterDropdown = (props: PropsWithChildren<FilterDropdownProps>) => {
|
||||
const { children, activeId, dropdownMenuItem, onSelectActiveId } = props;
|
||||
return (
|
||||
<Dropdown
|
||||
clickToHide
|
||||
position="bottomLeft"
|
||||
showTick
|
||||
contentClassName={s['dropdown-content']}
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
{dropdownMenuItem.map(item => {
|
||||
const { id, name } = item;
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={id}
|
||||
onClick={() => onSelectActiveId?.(id)}
|
||||
active={activeId === id}
|
||||
>
|
||||
{name}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @coze-arch/max-line-per-function
|
||||
export const QueryFilter = (props: QueryFilterProps) => {
|
||||
const {
|
||||
targetDateId,
|
||||
targetExecuteStatusId,
|
||||
targetOverallSpanInfo,
|
||||
enhancedOverallSpans,
|
||||
showLoadMore,
|
||||
onSelectDate,
|
||||
onSelectExecuteStatus,
|
||||
onFetchQuery,
|
||||
onSelectQuery,
|
||||
} = props;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadMoreLoading, setLoadMoreLoading] = useState(false);
|
||||
|
||||
const currentInputSearchRef = useRef<string | undefined>(undefined);
|
||||
|
||||
const checkSelectAll = (targetId?: QueryFilterItemId) =>
|
||||
targetId === FILTERING_OPTION_ALL;
|
||||
|
||||
const onFetchQueryWithLoading: (
|
||||
inputSearch?: string,
|
||||
loadMore?: boolean,
|
||||
) => Promise<void> = async (inputSearch, loadMore) => {
|
||||
const setLoadingFn = loadMore ? setLoadMoreLoading : setLoading;
|
||||
try {
|
||||
setLoadingFn(true);
|
||||
await onFetchQuery(inputSearch, loadMore);
|
||||
} finally {
|
||||
setLoadingFn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSelectedItem: (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
node: Record<string, any>,
|
||||
) => React.ReactNode = node => {
|
||||
const { value, input, span } = node;
|
||||
return value ? (
|
||||
<Tooltip content={input} className={s['custom-tooltip']}>
|
||||
<Tag
|
||||
className={classnames(
|
||||
s['query-filter-select-tag'],
|
||||
s[SPAN_STATUS_CONFIG_MAP[span.status as SpanStatus].className],
|
||||
)}
|
||||
>
|
||||
{input}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderInnerBottomSlot = () => (
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="primary"
|
||||
loading={loadMoreLoading}
|
||||
className="w-full"
|
||||
onClick={() =>
|
||||
onFetchQueryWithLoading(currentInputSearchRef.current, true)
|
||||
}
|
||||
>
|
||||
{I18n.t('query_list_loadmore')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<InputGroup className={s['query-filter']}>
|
||||
<div className={s['query-filter-options']}>
|
||||
<FilterDropdown
|
||||
activeId={targetDateId}
|
||||
onSelectActiveId={onSelectDate}
|
||||
dropdownMenuItem={[FILTERING_OPTION_ALL, ...getPastWeekDates()].map(
|
||||
item => {
|
||||
const queryFilterItem: QueryFilterItem = {
|
||||
id: item,
|
||||
name:
|
||||
item === FILTERING_OPTION_ALL
|
||||
? I18n.t('query_status_all')
|
||||
: item,
|
||||
};
|
||||
return queryFilterItem;
|
||||
},
|
||||
)}
|
||||
>
|
||||
<UIButton
|
||||
theme="borderless"
|
||||
icon={<IconCalendar />}
|
||||
size="small"
|
||||
className={classnames(
|
||||
!checkSelectAll(targetDateId) &&
|
||||
s['query-filter-options-button_active'],
|
||||
)}
|
||||
/>
|
||||
</FilterDropdown>
|
||||
<FilterDropdown
|
||||
activeId={targetExecuteStatusId}
|
||||
onSelectActiveId={onSelectExecuteStatus}
|
||||
dropdownMenuItem={EXECUTE_STATUS_FILTERING_OPTIONS.map(item => ({
|
||||
id: item.id,
|
||||
name: I18n.t(item.name as I18nKeysNoOptionsType),
|
||||
}))}
|
||||
>
|
||||
<UIButton
|
||||
theme="borderless"
|
||||
icon={<IconFilter />}
|
||||
size="small"
|
||||
className={classnames(
|
||||
!checkSelectAll(targetExecuteStatusId) &&
|
||||
s['query-filter-options-button_active'],
|
||||
)}
|
||||
/>
|
||||
</FilterDropdown>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={targetOverallSpanInfo}
|
||||
prefix={<IconSearch className={s['query-filter-select-search-icon']} />}
|
||||
filter
|
||||
remote
|
||||
loading={loading}
|
||||
className={s['query-filter-select']}
|
||||
dropdownClassName={s['query-filter-select-dropdown']}
|
||||
onChangeWithObject
|
||||
onSearch={debounce((value: string) => {
|
||||
const input = value === '' ? undefined : value;
|
||||
currentInputSearchRef.current = input;
|
||||
onFetchQueryWithLoading(input);
|
||||
}, QUERY_FILTER_DEBOUNCE_TIME)}
|
||||
onDropdownVisibleChange={visible => {
|
||||
if (visible) {
|
||||
onFetchQueryWithLoading();
|
||||
}
|
||||
}}
|
||||
onChange={value => {
|
||||
onSelectQuery(value as TargetOverallSpanInfo);
|
||||
}}
|
||||
renderSelectedItem={renderSelectedItem}
|
||||
innerBottomSlot={showLoadMore ? renderInnerBottomSlot() : null}
|
||||
>
|
||||
{enhancedOverallSpans.map(item => {
|
||||
const { status, extra } = item as CSpanSingle;
|
||||
const { dateString } = getTimeInCurrentTimeZone(item.start_time);
|
||||
return (
|
||||
<Select.Option
|
||||
value={extra?.log_id}
|
||||
key={extra?.log_id}
|
||||
input={extra?.input}
|
||||
span={item}
|
||||
>
|
||||
<div className={s['query-filter-select-dropdown-option']}>
|
||||
<div className={s['query-filter-select-dropdown-option-icon']}>
|
||||
{SPAN_STATUS_CONFIG_MAP[status].icon}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={extra?.input}
|
||||
className={s['custom-tooltip']}
|
||||
position="left"
|
||||
>
|
||||
<div
|
||||
className={s['query-filter-select-dropdown-option-text']}
|
||||
>
|
||||
{extra?.input}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="ml-2 font-normal">
|
||||
{/* <span
|
||||
className={s['query-filter-select-dropdown-option-time']}
|
||||
>
|
||||
{timeOffsetString}
|
||||
</span>{' '} */}
|
||||
{dateString}
|
||||
</div>
|
||||
</div>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
@import '../common/common.module.less';
|
||||
|
||||
.side-debug-panel {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 8px 0 rgb(29 28 35 / 6%), 0 0 2px 0 rgb(29 28 35 / 18%);
|
||||
|
||||
.side-debug-panel-divider {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #EFEFF0;
|
||||
}
|
||||
|
||||
.side-debug-panel-no-data {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 0;
|
||||
|
||||
:global(.semi-empty-description) {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--Light-usage-text---color-text-0, #1D1C23);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.side-debug-panel-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 0;
|
||||
|
||||
.side-debug-panel-container-sheet {
|
||||
width: 100%;
|
||||
padding: 24px 24px 4px;
|
||||
}
|
||||
|
||||
.side-debug-panel-container-scroll-box {
|
||||
.webkit-scrollbar_mixin();
|
||||
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
height: 0;
|
||||
|
||||
.side-debug-panel-container-scroll-box-summary {
|
||||
position: relative;
|
||||
|
||||
|
||||
width: 100%;
|
||||
min-height: 145px;
|
||||
padding: 16px 24px 24px;
|
||||
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.side-debug-panel-container-scroll-box-sub-loading {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.side-debug-panel-container-scroll-box-chat {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.side-debug-panel-container-scroll-box-detail {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 24px 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* 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 max-lines-per-function */
|
||||
/* eslint-disable max-params */
|
||||
/* eslint-disable @coze-arch/max-line-per-function */
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { Resizable } from 're-resizable';
|
||||
import qs from 'qs';
|
||||
import { useAsyncEffect } from 'ahooks';
|
||||
import { IllustrationNoResult } from '@douyinfe/semi-illustrations';
|
||||
import { type CSpan } from '@coze-devops/common-modules/query-trace';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Divider, Empty, Spin } from '@coze-arch/bot-semi';
|
||||
import { type SpanStatus, SpanType } from '@coze-arch/bot-api/ob_query_api';
|
||||
import { obQueryApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { PanelSummary } from '../summary';
|
||||
import { QueryFilter } from '../query-filter';
|
||||
import { PanelHeader } from '../header';
|
||||
import { enhanceOriginalSpan, getSpanProp } from '../../../utils/span';
|
||||
import { getDailyTimestampByDate } from '../../../utils';
|
||||
import { DebugPanelLayout } from '../../../typings';
|
||||
import { useDebugPanelStore } from '../../../store';
|
||||
import { useDebugPanelLayoutConfig } from '../../../hooks/use-debug-panel-layout-config';
|
||||
import { DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO } from '../../../consts/static';
|
||||
import {
|
||||
FILTERING_LIMIT,
|
||||
FILTERING_OPTION_ALL,
|
||||
INITIAL_OFFSET,
|
||||
TRACES_ADVANCE_INFO_TIME_BUFFER,
|
||||
} from '../../../consts';
|
||||
import { SpanInfoArea } from './span-info';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
export interface SideDebugPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SideDebugPanel = (props: SideDebugPanelProps) => {
|
||||
const { onClose } = props;
|
||||
const [subAreaLoading, setSubAreaLoading] = useState(false);
|
||||
const [showLoadMore, setShowLoadMore] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {
|
||||
basicInfo: { spaceID = '', botId = '' },
|
||||
isPanelShow,
|
||||
targetDateId,
|
||||
targetExecuteStatusId,
|
||||
targetOverallSpanInfo,
|
||||
enhancedOverallSpans,
|
||||
spanCategory,
|
||||
orgDetailSpans,
|
||||
targetDetailSpan,
|
||||
curBatchPage,
|
||||
entranceMessageLogId,
|
||||
setTargetOverallSpanInfo,
|
||||
onSelectDate,
|
||||
onSelectExecuteStatus,
|
||||
setEnhancedOverallSpans,
|
||||
setOrgDetailSpans,
|
||||
setSpanCategory,
|
||||
setTargetDetailSpan,
|
||||
setCurBatchPage,
|
||||
} = useDebugPanelStore();
|
||||
|
||||
const queryOffsetRef = useRef(INITIAL_OFFSET);
|
||||
|
||||
const [layoutConfig, setLayoutConfig] = useDebugPanelLayoutConfig();
|
||||
|
||||
const handleFetchQuery = async (inputSearch?: string, loadMore?: boolean) => {
|
||||
if (!loadMore) {
|
||||
setShowLoadMore(false);
|
||||
queryOffsetRef.current = INITIAL_OFFSET;
|
||||
}
|
||||
const { data } = await obQueryApi.ListDebugQueries(
|
||||
{
|
||||
spaceID,
|
||||
botID: botId,
|
||||
status:
|
||||
targetExecuteStatusId === FILTERING_OPTION_ALL
|
||||
? undefined
|
||||
: [targetExecuteStatusId as SpanStatus],
|
||||
inputSearch,
|
||||
limit: FILTERING_LIMIT,
|
||||
pageToken:
|
||||
queryOffsetRef.current === INITIAL_OFFSET
|
||||
? undefined
|
||||
: queryOffsetRef.current,
|
||||
...getDailyTimestampByDate(targetDateId),
|
||||
},
|
||||
{
|
||||
paramsSerializer: p => qs.stringify(p, { arrayFormat: 'comma' }),
|
||||
},
|
||||
);
|
||||
queryOffsetRef.current =
|
||||
data?.next_page_token && data.next_page_token !== ''
|
||||
? data.next_page_token
|
||||
: INITIAL_OFFSET;
|
||||
const originSpans = data?.spans ?? [];
|
||||
setShowLoadMore(data?.has_more ?? false);
|
||||
|
||||
if (originSpans.length === 0) {
|
||||
setEnhancedOverallSpans([]);
|
||||
return [];
|
||||
}
|
||||
const {
|
||||
data: { traces_advance_info: traceAdvanceInfo },
|
||||
} = await obQueryApi.BatchGetTracesAdvanceInfo({
|
||||
space_id: spaceID ?? '',
|
||||
bot_id: botId ?? '',
|
||||
traces: originSpans.map(item => {
|
||||
const { trace_id, start_time, latency } = item;
|
||||
return {
|
||||
trace_id,
|
||||
start_time,
|
||||
end_time: String(
|
||||
Number(start_time) +
|
||||
Number(latency) +
|
||||
TRACES_ADVANCE_INFO_TIME_BUFFER,
|
||||
),
|
||||
};
|
||||
}),
|
||||
});
|
||||
const enhancedSpans = enhanceOriginalSpan(originSpans, traceAdvanceInfo);
|
||||
const spans = loadMore
|
||||
? [...enhancedOverallSpans, ...enhancedSpans]
|
||||
: enhancedSpans;
|
||||
setEnhancedOverallSpans(spans);
|
||||
return spans;
|
||||
};
|
||||
|
||||
const handleFetchQueryDetail = async (logId: string) => {
|
||||
const {
|
||||
data: { spans },
|
||||
} = await obQueryApi.GetTraceByLogID({
|
||||
space_id: spaceID,
|
||||
bot_id: botId,
|
||||
log_id: logId,
|
||||
});
|
||||
setOrgDetailSpans(spans);
|
||||
return spans;
|
||||
};
|
||||
|
||||
const handleFetchTracesMetaInfo = async () => {
|
||||
const { data } = await obQueryApi.GetTracesMetaInfo();
|
||||
setSpanCategory(data?.span_category);
|
||||
};
|
||||
|
||||
const selectQueryAuto = (span: CSpan) => {
|
||||
const logId = getSpanProp(span, 'log_id') as string;
|
||||
const input = getSpanProp(span, 'simple_input') as string;
|
||||
const output = getSpanProp(span, 'output') as string;
|
||||
setTargetOverallSpanInfo({
|
||||
value: logId,
|
||||
input,
|
||||
output,
|
||||
span,
|
||||
});
|
||||
};
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (targetOverallSpanInfo) {
|
||||
const { span } = targetOverallSpanInfo;
|
||||
const logId = getSpanProp(span, 'log_id') as string;
|
||||
setSubAreaLoading(true);
|
||||
try {
|
||||
await handleFetchQueryDetail(logId);
|
||||
} finally {
|
||||
setSubAreaLoading(false);
|
||||
}
|
||||
}
|
||||
}, [targetOverallSpanInfo]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if (isPanelShow) {
|
||||
setLoading(true);
|
||||
onSelectDate(FILTERING_OPTION_ALL);
|
||||
onSelectExecuteStatus(FILTERING_OPTION_ALL);
|
||||
if (!spanCategory) {
|
||||
await handleFetchTracesMetaInfo();
|
||||
}
|
||||
// 从某条消息进入
|
||||
if (entranceMessageLogId) {
|
||||
try {
|
||||
const spans = await handleFetchQueryDetail(entranceMessageLogId);
|
||||
const userInputSpan = spans.find(
|
||||
item => item.type === SpanType.UserInput,
|
||||
);
|
||||
if (userInputSpan) {
|
||||
const { trace_id, start_time, latency } = userInputSpan;
|
||||
const {
|
||||
data: { traces_advance_info: traceAdvanceInfo },
|
||||
} = await obQueryApi.BatchGetTracesAdvanceInfo({
|
||||
space_id: spaceID,
|
||||
bot_id: botId,
|
||||
traces: [
|
||||
{
|
||||
trace_id,
|
||||
start_time,
|
||||
end_time: String(
|
||||
Number(start_time) +
|
||||
Number(latency) +
|
||||
TRACES_ADVANCE_INFO_TIME_BUFFER,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const userInputCSpan = enhanceOriginalSpan(
|
||||
[userInputSpan],
|
||||
traceAdvanceInfo,
|
||||
)?.[0];
|
||||
selectQueryAuto(userInputCSpan);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
//直接进入
|
||||
else {
|
||||
try {
|
||||
const spans = await handleFetchQuery();
|
||||
const latestTrace = spans?.[0] as CSpan | undefined;
|
||||
if (latestTrace) {
|
||||
selectQueryAuto(latestTrace);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isPanelShow, entranceMessageLogId]);
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
minWidth={
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO.side[DebugPanelLayout.Overall]
|
||||
.width.min
|
||||
}
|
||||
maxWidth={
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO.side[DebugPanelLayout.Overall]
|
||||
.width.max
|
||||
}
|
||||
enable={{
|
||||
left: true,
|
||||
}}
|
||||
defaultSize={{
|
||||
height: '100%',
|
||||
width: layoutConfig.side[DebugPanelLayout.Overall],
|
||||
}}
|
||||
onResizeStop={(e, d, el, delta) => {
|
||||
setLayoutConfig(config => {
|
||||
config.side[DebugPanelLayout.Overall] += delta.width;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className={s['side-debug-panel']}>
|
||||
<PanelHeader onClose={onClose} />
|
||||
{loading ? (
|
||||
<div className={s['side-debug-panel-no-data']}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={s['side-debug-panel-container']}>
|
||||
<div className={s['side-debug-panel-container-sheet']}>
|
||||
<QueryFilter
|
||||
targetDateId={targetDateId}
|
||||
targetExecuteStatusId={targetExecuteStatusId}
|
||||
targetOverallSpanInfo={targetOverallSpanInfo}
|
||||
enhancedOverallSpans={enhancedOverallSpans}
|
||||
showLoadMore={showLoadMore}
|
||||
onSelectDate={onSelectDate}
|
||||
onSelectExecuteStatus={onSelectExecuteStatus}
|
||||
onSelectQuery={setTargetOverallSpanInfo}
|
||||
onFetchQuery={handleFetchQuery}
|
||||
/>
|
||||
</div>
|
||||
{!targetOverallSpanInfo ? (
|
||||
<div className={s['side-debug-panel-no-data']}>
|
||||
<Empty
|
||||
image={<IllustrationNoResult />}
|
||||
description={I18n.t('query_data_empty')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={s['side-debug-panel-container-scroll-box']}>
|
||||
<Resizable
|
||||
minHeight={
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO.side[
|
||||
DebugPanelLayout.Summary
|
||||
].height.min
|
||||
}
|
||||
maxHeight={
|
||||
DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO.side[
|
||||
DebugPanelLayout.Summary
|
||||
].height.max
|
||||
}
|
||||
minWidth="100%"
|
||||
enable={{
|
||||
bottom: true,
|
||||
}}
|
||||
defaultSize={{
|
||||
height: layoutConfig.side[DebugPanelLayout.Summary],
|
||||
width: '100%',
|
||||
}}
|
||||
onResizeStop={(e, d, el, delta) => {
|
||||
setLayoutConfig(config => {
|
||||
config.side[DebugPanelLayout.Summary] += delta.height;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
s['side-debug-panel-container-scroll-box-summary']
|
||||
}
|
||||
>
|
||||
{targetOverallSpanInfo ? (
|
||||
<PanelSummary
|
||||
targetOverallSpanInfo={targetOverallSpanInfo}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</Resizable>
|
||||
<Divider className={s['side-debug-panel-divider']} />
|
||||
{subAreaLoading ? (
|
||||
<div
|
||||
className={
|
||||
s['side-debug-panel-container-scroll-box-sub-loading']
|
||||
}
|
||||
>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<SpanInfoArea
|
||||
botId={botId}
|
||||
spaceId={spaceID}
|
||||
targetDetailSpan={targetDetailSpan}
|
||||
orgDetailSpans={orgDetailSpans}
|
||||
spanCategory={spanCategory}
|
||||
targetOverallSpanInfo={targetOverallSpanInfo}
|
||||
curBatchPage={curBatchPage}
|
||||
setTargetDetailSpan={setTargetDetailSpan}
|
||||
setCurBatchPage={setCurBatchPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Resizable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 {
|
||||
useSpanTransform,
|
||||
type CSpan,
|
||||
} from '@coze-devops/common-modules/query-trace';
|
||||
import { Divider } from '@coze-arch/bot-semi';
|
||||
import {
|
||||
type TraceAdvanceInfo,
|
||||
type Span,
|
||||
SpanStatus,
|
||||
} from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { PanelDetail } from '../detail';
|
||||
import { PanelChart } from '../chart';
|
||||
import { type TargetOverallSpanInfo } from '../../../typings';
|
||||
import { type SpanCategory } from '../../../store';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface SpanInfoAreaProps {
|
||||
botId: string;
|
||||
spaceId: string;
|
||||
targetDetailSpan?: CSpan;
|
||||
orgDetailSpans?: Span[];
|
||||
spanCategory?: SpanCategory;
|
||||
targetOverallSpanInfo?: TargetOverallSpanInfo;
|
||||
curBatchPage?: number;
|
||||
setTargetDetailSpan: (targetDetailSpan: CSpan) => void;
|
||||
setCurBatchPage: (curBatchPage: number) => void;
|
||||
}
|
||||
|
||||
export const SpanInfoArea = (props: SpanInfoAreaProps) => {
|
||||
const {
|
||||
botId,
|
||||
spaceId,
|
||||
targetDetailSpan,
|
||||
orgDetailSpans,
|
||||
spanCategory,
|
||||
targetOverallSpanInfo,
|
||||
curBatchPage,
|
||||
setTargetDetailSpan,
|
||||
setCurBatchPage,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
status = SpanStatus.Unknown,
|
||||
input_tokens_sum = 0,
|
||||
output_tokens_sum = 0,
|
||||
} = targetOverallSpanInfo?.span ?? {};
|
||||
|
||||
const traceAdvanceInfo: Omit<TraceAdvanceInfo, 'trace_id'> = useMemo(
|
||||
() => ({
|
||||
tokens: {
|
||||
input: input_tokens_sum,
|
||||
output: output_tokens_sum,
|
||||
},
|
||||
status,
|
||||
}),
|
||||
[input_tokens_sum, output_tokens_sum, status],
|
||||
);
|
||||
|
||||
const { rootSpan, spans } = useSpanTransform({
|
||||
orgSpans: orgDetailSpans ?? [],
|
||||
traceAdvanceInfo,
|
||||
spanCategoryMeta: spanCategory,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={s['side-debug-panel-container-scroll-box-chat']}>
|
||||
{orgDetailSpans && spanCategory && targetOverallSpanInfo ? (
|
||||
<PanelChart
|
||||
rootSpan={rootSpan}
|
||||
spans={spans}
|
||||
targetDetailSpan={targetDetailSpan}
|
||||
onTargetDetailSpanChange={detailSpan => {
|
||||
setCurBatchPage(1);
|
||||
setTargetDetailSpan(detailSpan);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Divider className={s['side-debug-panel-divider']} />
|
||||
<div className={s['side-debug-panel-container-scroll-box-detail']}>
|
||||
{targetDetailSpan && curBatchPage ? (
|
||||
<PanelDetail
|
||||
botId={botId}
|
||||
spaceId={spaceId}
|
||||
spans={spans}
|
||||
targetDetailSpan={targetDetailSpan}
|
||||
curBatchPage={curBatchPage}
|
||||
setCurBatchPage={setCurBatchPage}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
.summary-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.summary-title-container-data {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--Light-usage-text---color-text-0, #1d1c23);
|
||||
}
|
||||
|
||||
.summary-title-container-tag {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
|
||||
height: 16px;
|
||||
padding: 0 6px;
|
||||
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-execute-status_success {
|
||||
color: var(--Light-usage-success---color-success, #3ec254);
|
||||
background: var(--Light-color-green---green-1, #d2f3d5);
|
||||
}
|
||||
|
||||
.query-execute-status_broken {
|
||||
color: var(--Light-color-orange---orange-5, #ff9600);
|
||||
background: var(--Light-color-orange---orange-1, #fff1cc);
|
||||
}
|
||||
|
||||
.query-execute-status_error {
|
||||
color: #ff441e;
|
||||
background: var(--Light-color-red---red-1, #ffe0d2);
|
||||
}
|
||||
|
||||
.feedback-button {
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import copy from 'copy-to-clipboard';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from '@coze-arch/logger';
|
||||
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
|
||||
import { UIToast } from '@coze-arch/bot-semi';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import { NodeDescriptionWithFullLine } from '../common';
|
||||
import { fieldHandlers } from '../../../utils/field-item';
|
||||
import { type TargetOverallSpanInfo } from '../../../typings';
|
||||
import { useDebugPanelStore } from '../../../store';
|
||||
import { useTraceCols } from '../../../hooks/use-trace-cols';
|
||||
import { SPAN_STATUS_CONFIG_MAP } from '../../../consts/span';
|
||||
|
||||
import s from './index.module.less';
|
||||
|
||||
interface PanelSummaryProps {
|
||||
targetOverallSpanInfo: TargetOverallSpanInfo;
|
||||
}
|
||||
|
||||
export const PanelSummary = (props: PanelSummaryProps) => {
|
||||
const { targetOverallSpanInfo } = props;
|
||||
const {
|
||||
basicInfo: { botId, userID, spaceID },
|
||||
} = useDebugPanelStore();
|
||||
|
||||
const {
|
||||
span,
|
||||
output,
|
||||
span: { status, latency, input_tokens_sum = 0, output_tokens_sum = 0 },
|
||||
} = targetOverallSpanInfo;
|
||||
|
||||
const { icon, label, className } = SPAN_STATUS_CONFIG_MAP[status];
|
||||
|
||||
const { traceCols } = useTraceCols({ span });
|
||||
|
||||
const handleFeedback = () => {
|
||||
try {
|
||||
const feedbackMsg = [
|
||||
`Logid: ${fieldHandlers.log_id(span).value}`,
|
||||
`UID: ${userID}`,
|
||||
`Botid: ${botId}`,
|
||||
`StartTime: ${fieldHandlers.start_time(span).value}`,
|
||||
`EndTime: ${fieldHandlers.end_time(span).value}`,
|
||||
status === SpanStatus.Error && `ErrorMsg:${output}`,
|
||||
`\n${I18n.t('debug_copy_suggestion')}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
copy(feedbackMsg);
|
||||
UIToast.success({
|
||||
content: I18n.t('debug_copy_success'),
|
||||
});
|
||||
|
||||
sendTeaEvent(EVENT_NAMES.click_debug_panel_feedback_button, {
|
||||
bot_id: botId ?? '',
|
||||
space_id: spaceID ?? '',
|
||||
host: window.location.host,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
eventName: 'fail_to_copy_debug_info',
|
||||
error: error as Error,
|
||||
});
|
||||
UIToast.error(I18n.t('copy_failed'));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className={s['summary-title-container']}>
|
||||
<div className={s['summary-title-container-data']}>
|
||||
{I18n.t('query_latency', {
|
||||
duration: latency,
|
||||
})}
|
||||
ms|
|
||||
{I18n.t('query_tokens_number', {
|
||||
number: input_tokens_sum + output_tokens_sum,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(s['summary-title-container-tag'], s[className])}
|
||||
>
|
||||
{icon}
|
||||
{I18n.t(label as I18nKeysNoOptionsType)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleFeedback}
|
||||
className="ml-2"
|
||||
color="highlight"
|
||||
size="small"
|
||||
>
|
||||
{I18n.t('debug_copy_report')}
|
||||
</Button>
|
||||
</div>
|
||||
<NodeDescriptionWithFullLine cols={traceCols} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
19
frontend/packages/devops/debug/debug-panel/src/consts/env.ts
Normal file
19
frontend/packages/devops/debug/debug-panel/src/consts/env.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const IS_DEV_MODE =
|
||||
(process.env.NODE_ENV as 'production' | 'development' | 'test') ===
|
||||
'development';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 支持筛选的query时间范围
|
||||
*/
|
||||
export const DATE_FILTERING_DAYS_NUMBER = 7;
|
||||
export const FILTERING_OPTION_ALL = 'ALL';
|
||||
/**
|
||||
* query每次加载条数
|
||||
*/
|
||||
export const FILTERING_LIMIT = 30;
|
||||
export const TRACES_ADVANCE_INFO_TIME_BUFFER = 1000;
|
||||
export const TIME_MINUTE = 60;
|
||||
/**
|
||||
* query拉取默认偏移量
|
||||
*/
|
||||
export const INITIAL_OFFSET = '0';
|
||||
export const EMPTY_TEXT = '-';
|
||||
/**
|
||||
* query拉取防抖时间
|
||||
*/
|
||||
export const QUERY_FILTER_DEBOUNCE_TIME = 300;
|
||||
/**
|
||||
* 调试台位置信息localStorage key
|
||||
*/
|
||||
export const DEBUG_PANEL_LAYOUT_KEY = 'coze_debug_panel_layout_config';
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
spanCategoryConfigMap,
|
||||
spanTypeConfigMap,
|
||||
botEnvConfigMap,
|
||||
} from '@coze-devops/common-modules/query-trace';
|
||||
import { IconSuccess, IconError, IconWarningInfo } from '@coze-arch/bot-icons';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { type SpanStatusConfig } from '../typings';
|
||||
|
||||
export const SPAN_TYPE_CONFIG_MAP = spanTypeConfigMap;
|
||||
|
||||
export const SPAN_STATUS_CONFIG_MAP: Record<SpanStatus, SpanStatusConfig> = {
|
||||
[SpanStatus.Success]: {
|
||||
icon: <IconSuccess />,
|
||||
className: 'query-execute-status_success',
|
||||
label: 'query_status_success',
|
||||
},
|
||||
[SpanStatus.Broken]: {
|
||||
icon: <IconWarningInfo />,
|
||||
className: 'query-execute-status_broken',
|
||||
label: 'query_status_broken',
|
||||
},
|
||||
[SpanStatus.Error]: {
|
||||
icon: <IconError />,
|
||||
className: 'query-execute-status_error',
|
||||
label: 'query_status_error',
|
||||
},
|
||||
[SpanStatus.Unknown]: {
|
||||
icon: <IconSuccess />,
|
||||
className: 'query-execute-status_unknown',
|
||||
label: 'query_status_unknown',
|
||||
},
|
||||
};
|
||||
export const SPAN_CATEGORY_CONFIG_MAP = spanCategoryConfigMap;
|
||||
|
||||
export const BOT_ENV_CONFIG_MAP = botEnvConfigMap;
|
||||
118
frontend/packages/devops/debug/debug-panel/src/consts/static.tsx
Normal file
118
frontend/packages/devops/debug/debug-panel/src/consts/static.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 ReactJsonViewProps } from 'react-json-view';
|
||||
|
||||
import { TopoType } from '@coze-arch/bot-api/dp_manage_api';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/debugger_api';
|
||||
|
||||
import {
|
||||
DebugPanelLayout,
|
||||
type DebugPanelLayoutConfig,
|
||||
type DebugPanelLayoutTemplateConfig,
|
||||
type QueryFilterItem,
|
||||
} from '../typings';
|
||||
import { FILTERING_OPTION_ALL } from '.';
|
||||
|
||||
export const EXECUTE_STATUS_FILTERING_OPTIONS: QueryFilterItem[] = [
|
||||
{
|
||||
id: FILTERING_OPTION_ALL,
|
||||
name: 'query_status_all',
|
||||
},
|
||||
{
|
||||
id: SpanStatus.Error,
|
||||
name: 'query_status_failed',
|
||||
},
|
||||
{
|
||||
id: SpanStatus.Success,
|
||||
name: 'query_status_completed',
|
||||
},
|
||||
];
|
||||
|
||||
export enum GraphTabEnum {
|
||||
RunTree = 'RunTree',
|
||||
Flamethread = 'Flamethread',
|
||||
}
|
||||
|
||||
export const DEBUG_PANEL_LAYOUT_DEFAULT_TEMPLATE_INFO: DebugPanelLayoutTemplateConfig =
|
||||
{
|
||||
side: {
|
||||
[DebugPanelLayout.Overall]: {
|
||||
width: {
|
||||
min: 400,
|
||||
max: 800,
|
||||
},
|
||||
height: {},
|
||||
},
|
||||
[DebugPanelLayout.Summary]: {
|
||||
width: {},
|
||||
height: {
|
||||
min: 8,
|
||||
max: 150,
|
||||
},
|
||||
},
|
||||
[DebugPanelLayout.Chat]: {
|
||||
width: {},
|
||||
height: {
|
||||
min: 1,
|
||||
max: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
[DebugPanelLayout.Overall]: {
|
||||
width: {},
|
||||
height: {},
|
||||
},
|
||||
[DebugPanelLayout.Summary]: {
|
||||
width: {},
|
||||
height: {},
|
||||
},
|
||||
[DebugPanelLayout.Chat]: {
|
||||
width: {},
|
||||
height: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DEBUG_PANEL_LAYOUT_DEFAULT_INFO: DebugPanelLayoutConfig = {
|
||||
side: {
|
||||
[DebugPanelLayout.Overall]: 400,
|
||||
[DebugPanelLayout.Summary]: 124,
|
||||
[DebugPanelLayout.Chat]: 280,
|
||||
},
|
||||
bottom: {
|
||||
[DebugPanelLayout.Overall]: 0,
|
||||
[DebugPanelLayout.Summary]: 0,
|
||||
[DebugPanelLayout.Chat]: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const REACT_JSON_VIEW_CONFIG: Partial<ReactJsonViewProps> = {
|
||||
name: false,
|
||||
displayDataTypes: false,
|
||||
indentWidth: 2,
|
||||
iconStyle: 'triangle',
|
||||
enableClipboard: false,
|
||||
collapsed: 5,
|
||||
collapseStringsAfterLength: 300,
|
||||
};
|
||||
|
||||
export const topologyTypeConfig: Record<TopoType, string> = {
|
||||
[TopoType.Agent]: 'Agent',
|
||||
[TopoType.AgentFlow]: 'AgentFlow',
|
||||
[TopoType.Workflow]: 'Workflow',
|
||||
};
|
||||
22
frontend/packages/devops/debug/debug-panel/src/global.d.ts
vendored
Normal file
22
frontend/packages/devops/debug/debug-panel/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: { [key: string]: string };
|
||||
export = resource;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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 {
|
||||
isBatchSpanType,
|
||||
type CSPanBatch,
|
||||
type CSpan,
|
||||
} from '@coze-devops/common-modules/query-trace';
|
||||
import { SpanType } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { fieldHandlers } from '../utils/field-item';
|
||||
import {
|
||||
type FieldCol,
|
||||
type BatchSpanType,
|
||||
type FieldColConfig,
|
||||
} from '../typings';
|
||||
|
||||
const colsConfigForLLMBatchCall: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'tokens',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForPluginToolBatch: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForCodeBatch: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigMap = {
|
||||
[SpanType.LLMBatchCall]: colsConfigForLLMBatchCall,
|
||||
[SpanType.WorkflowLLMBatchCall]: colsConfigForLLMBatchCall,
|
||||
[SpanType.PluginToolBatch]: colsConfigForPluginToolBatch,
|
||||
[SpanType.WorkflowPluginToolBatch]: colsConfigForPluginToolBatch,
|
||||
[SpanType.CodeBatch]: colsConfigForCodeBatch,
|
||||
[SpanType.WorkflowCodeBatch]: colsConfigForCodeBatch,
|
||||
};
|
||||
|
||||
export const useBatchSpanCols = (input: {
|
||||
span?: CSpan;
|
||||
curBatchIndex?: number;
|
||||
}): {
|
||||
batchSpanCols: FieldCol[];
|
||||
} => {
|
||||
const { span, curBatchIndex } = input;
|
||||
const batchSpanCols: FieldCol[] = useMemo(() => {
|
||||
if (!span || curBatchIndex === undefined || !isBatchSpanType(span.type)) {
|
||||
return [];
|
||||
}
|
||||
const subSpan = (span as CSPanBatch).spans[curBatchIndex];
|
||||
if (!subSpan) {
|
||||
return [];
|
||||
}
|
||||
const colsConfig = colsConfigMap[subSpan.type as BatchSpanType];
|
||||
return colsConfig.map(colConfig => {
|
||||
const { fields } = colConfig;
|
||||
return {
|
||||
fields: fields?.map(fieldConfig => {
|
||||
const { name, options } = fieldConfig;
|
||||
return {
|
||||
...fieldHandlers[name](subSpan),
|
||||
options,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [span, curBatchIndex]);
|
||||
|
||||
return {
|
||||
batchSpanCols,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { isJsonString } from '../utils';
|
||||
import { type DebugPanelLayoutConfig } from '../typings';
|
||||
import { DEBUG_PANEL_LAYOUT_DEFAULT_INFO } from '../consts/static';
|
||||
import { DEBUG_PANEL_LAYOUT_KEY } from '../consts';
|
||||
|
||||
export type SetLayoutConfigAction = (value: DebugPanelLayoutConfig) => void;
|
||||
|
||||
export type UseDebugPanelLayoutConfig = () => [
|
||||
DebugPanelLayoutConfig,
|
||||
(input: DebugPanelLayoutConfig | SetLayoutConfigAction) => void,
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取和修改存储在localStorage中的调试台布局数据
|
||||
* @returns UseDebugPanelLayoutConfig
|
||||
*/
|
||||
export const useDebugPanelLayoutConfig: UseDebugPanelLayoutConfig = () => {
|
||||
const initLayoutConfig = () => {
|
||||
const layoutConfigString = localStorage.getItem(DEBUG_PANEL_LAYOUT_KEY);
|
||||
if (layoutConfigString && isJsonString(layoutConfigString)) {
|
||||
return JSON.parse(layoutConfigString) as DebugPanelLayoutConfig;
|
||||
} else {
|
||||
return DEBUG_PANEL_LAYOUT_DEFAULT_INFO;
|
||||
}
|
||||
};
|
||||
|
||||
const layoutConfigRef = useRef<DebugPanelLayoutConfig>(initLayoutConfig());
|
||||
|
||||
const setLayoutConfig = (
|
||||
input: DebugPanelLayoutConfig | SetLayoutConfigAction,
|
||||
) => {
|
||||
const layoutConfig =
|
||||
typeof input === 'function'
|
||||
? produce(layoutConfigRef.current, draft => {
|
||||
input(draft);
|
||||
})
|
||||
: input;
|
||||
layoutConfigRef.current = layoutConfig;
|
||||
window.localStorage.setItem(
|
||||
DEBUG_PANEL_LAYOUT_KEY,
|
||||
JSON.stringify(layoutConfig),
|
||||
);
|
||||
};
|
||||
|
||||
return [layoutConfigRef.current, setLayoutConfig];
|
||||
};
|
||||
@@ -0,0 +1,656 @@
|
||||
/*
|
||||
* 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 max-lines */
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { type CSpan } from '@coze-devops/common-modules/query-trace';
|
||||
import { SpanType } from '@coze-arch/bot-api/ob_query_api';
|
||||
|
||||
import { fieldHandlers } from '../utils/field-item';
|
||||
import { type FieldCol, type FieldColConfig } from '../typings';
|
||||
|
||||
const colsConfigForStart: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'first_response_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'latency_first',
|
||||
},
|
||||
{
|
||||
name: 'tokens',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForInvokeAgent: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'call_type',
|
||||
},
|
||||
{
|
||||
name: 'agent_type',
|
||||
},
|
||||
{
|
||||
name: 'temperature',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
},
|
||||
{
|
||||
name: 'tokens',
|
||||
},
|
||||
{
|
||||
name: 'max_length_resp',
|
||||
},
|
||||
{
|
||||
name: 'dialog_round',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForSwitchAgent: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForLLMCall: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
name: 'call_type',
|
||||
},
|
||||
{
|
||||
name: 'max_length_resp',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'first_response_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'latency_first',
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
},
|
||||
{
|
||||
name: 'tokens',
|
||||
},
|
||||
{
|
||||
name: 'temperature',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForWorkflow: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'tokens',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForWorkflowEnd: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'tokens',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForCode: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForCondition: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForPluginTool: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'call_type',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForKnowledge: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'call_type',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigGeneral: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForCard: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'call_type',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'card_id',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForMessage: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'call_type',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'stream_output',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForBWCondition: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'branch_name',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const colsConfigForBWConnector: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'node_type',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colsConfigForHook: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'end_time',
|
||||
},
|
||||
{
|
||||
name: 'hook_type',
|
||||
},
|
||||
{
|
||||
name: 'agent_id',
|
||||
},
|
||||
{
|
||||
name: 'is_stream',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'status',
|
||||
},
|
||||
{
|
||||
name: 'latency',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
name: 'hook_resp_code',
|
||||
},
|
||||
{
|
||||
name: 'hook_uri',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const colsConfigMap = {
|
||||
[SpanType.UserInput]: colsConfigForStart,
|
||||
[SpanType.ThirdParty]: colsConfigForStart,
|
||||
[SpanType.ScheduledTasks]: colsConfigForStart,
|
||||
[SpanType.OpenDialog]: colsConfigForStart,
|
||||
[SpanType.InvokeAgent]: colsConfigForInvokeAgent,
|
||||
[SpanType.RestartAgent]: colsConfigForSwitchAgent,
|
||||
[SpanType.SwitchAgent]: colsConfigForSwitchAgent,
|
||||
[SpanType.LLMCall]: colsConfigForLLMCall,
|
||||
[SpanType.WorkflowLLMCall]: colsConfigForLLMCall,
|
||||
[SpanType.LLMBatchCall]: colsConfigForLLMCall,
|
||||
[SpanType.WorkflowLLMBatchCall]: colsConfigForLLMCall,
|
||||
[SpanType.Workflow]: colsConfigForWorkflow,
|
||||
[SpanType.WorkflowStart]: colsConfigForWorkflowEnd,
|
||||
[SpanType.WorkflowEnd]: colsConfigForWorkflowEnd,
|
||||
[SpanType.PluginTool]: colsConfigForPluginTool,
|
||||
[SpanType.WorkflowPluginTool]: colsConfigForPluginTool,
|
||||
[SpanType.PluginToolBatch]: colsConfigForPluginTool,
|
||||
[SpanType.WorkflowPluginToolBatch]: colsConfigForPluginTool,
|
||||
[SpanType.Knowledge]: colsConfigForKnowledge,
|
||||
[SpanType.WorkflowKnowledge]: colsConfigForKnowledge,
|
||||
[SpanType.Code]: colsConfigForCode,
|
||||
[SpanType.WorkflowCode]: colsConfigForCode,
|
||||
[SpanType.CodeBatch]: colsConfigForCode,
|
||||
[SpanType.WorkflowCodeBatch]: colsConfigForCode,
|
||||
[SpanType.Condition]: colsConfigForCondition,
|
||||
[SpanType.WorkflowCondition]: colsConfigForCondition,
|
||||
[SpanType.Unknown]: colsConfigGeneral,
|
||||
[SpanType.Chain]: [],
|
||||
[SpanType.Card]: colsConfigForCard,
|
||||
[SpanType.WorkflowMessage]: colsConfigForMessage,
|
||||
[SpanType.Hook]: colsConfigForHook,
|
||||
[SpanType.BWStart]: colsConfigGeneral,
|
||||
[SpanType.BWEnd]: colsConfigGeneral,
|
||||
[SpanType.BWBatch]: colsConfigGeneral,
|
||||
[SpanType.BWLoop]: colsConfigGeneral,
|
||||
[SpanType.BWCondition]: colsConfigForBWCondition,
|
||||
[SpanType.BWLLM]: colsConfigForLLMCall,
|
||||
[SpanType.BWParallel]: colsConfigGeneral,
|
||||
[SpanType.BWScript]: colsConfigGeneral,
|
||||
[SpanType.BWVariable]: colsConfigGeneral,
|
||||
[SpanType.BWCallFlow]: colsConfigGeneral,
|
||||
[SpanType.BWConnector]: colsConfigForBWConnector,
|
||||
};
|
||||
|
||||
export const useSpanCols = (input: {
|
||||
span?: CSpan;
|
||||
}): {
|
||||
spanCols: FieldCol[];
|
||||
} => {
|
||||
const { span } = input;
|
||||
const spanCols: FieldCol[] = useMemo(() => {
|
||||
if (!span) {
|
||||
return [];
|
||||
}
|
||||
const colsConfig = colsConfigMap[span.type];
|
||||
return (
|
||||
colsConfig?.map(colConfig => {
|
||||
const { fields } = colConfig;
|
||||
return {
|
||||
fields: fields?.map(fieldConfig => {
|
||||
const { name, options } = fieldConfig;
|
||||
return {
|
||||
...fieldHandlers[name](span),
|
||||
options,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}) || []
|
||||
);
|
||||
}, [span]);
|
||||
|
||||
return {
|
||||
spanCols,
|
||||
};
|
||||
};
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { type CSpan } from '@coze-devops/common-modules/query-trace';
|
||||
|
||||
import { fieldHandlers } from '../utils/field-item';
|
||||
import { type FieldCol, type FieldColConfig } from '../typings';
|
||||
|
||||
const colsConfigForTrace: FieldColConfig[] = [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'log_id',
|
||||
options: {
|
||||
copyable: true,
|
||||
fullLine: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'start_time',
|
||||
},
|
||||
{
|
||||
name: 'latency_first',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const useTraceCols = (input: {
|
||||
span?: CSpan;
|
||||
}): {
|
||||
traceCols: FieldCol[];
|
||||
} => {
|
||||
const { span } = input;
|
||||
const traceCols: FieldCol[] = useMemo(() => {
|
||||
if (!span) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return colsConfigForTrace.map(colConfig => {
|
||||
const { fields } = colConfig;
|
||||
return {
|
||||
fields: fields?.map(fieldConfig => {
|
||||
const { name, options } = fieldConfig;
|
||||
return {
|
||||
...fieldHandlers[name](span),
|
||||
options,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [span]);
|
||||
|
||||
return {
|
||||
traceCols,
|
||||
};
|
||||
};
|
||||
22
frontend/packages/devops/debug/debug-panel/src/index.ts
Normal file
22
frontend/packages/devops/debug/debug-panel/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 './main.css';
|
||||
|
||||
import { DebugPanel } from './components/debug-panel';
|
||||
export type { DebugPanelProps } from './components/debug-panel';
|
||||
|
||||
export default DebugPanel;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user