feat: manually mirror opencoze's code from bytedance

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

View File

@@ -0,0 +1,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 React, { useMemo } from 'react';
import { IconCozFocus } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { type Span } from '@coze-arch/bot-api/workflow_api';
import { getStrFromSpan } from '../../utils';
export const FocusButton: React.FC<{
span: Span;
onClick: (span: Span) => void;
}> = ({ span, onClick }) => {
const nodeId = useMemo(
() => getStrFromSpan(span, 'workflow_node_id'),
[span],
);
if (!nodeId) {
return null;
}
return (
<IconButton
icon={<IconCozFocus />}
size="mini"
onClick={() => onClick(span)}
/>
);
};

View File

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

View File

@@ -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 React, { useMemo } from 'react';
import { clsx } from 'clsx';
import { I18n } from '@coze-arch/i18n';
import {
IconCozCheckMarkCircleFillPalette,
IconCozCrossCircleFillPalette,
} from '@coze-arch/coze-design/icons';
import { Tag } from '@coze-arch/coze-design';
interface StatusTagProps {
status?: number;
className?: string;
type?: 'normal' | 'icon';
}
export const StatusIcon: React.FC<{ status?: number; className?: string }> = ({
status,
className,
}) =>
status === 0 ? (
<IconCozCheckMarkCircleFillPalette
className={clsx(className, 'coz-fg-hglt-green')}
/>
) : (
<IconCozCrossCircleFillPalette
className={clsx(className, 'coz-fg-hglt-red')}
/>
);
export const StatusTag: React.FC<StatusTagProps> = ({
status,
className,
type = 'normal',
}) => {
const children = useMemo(() => {
if (type === 'icon') {
return null;
}
return status === 0
? I18n.t('debug_asyn_task_task_status_success')
: I18n.t('debug_asyn_task_task_status_failed');
}, [status, type]);
return (
<Tag
prefixIcon={
status === 0 ? (
<IconCozCheckMarkCircleFillPalette />
) : (
<IconCozCrossCircleFillPalette />
)
}
color={status === 0 ? 'green' : 'red'}
className={className}
size="mini"
>
{children}
</Tag>
);
};

View File

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

View File

@@ -0,0 +1,12 @@
.pay-block {
display: flex;
align-items: center;
column-gap: 2px;
}
.pay-blocks {
display: flex;
align-items: center;
column-gap: 2px;
margin-bottom: 4px;
}

View File

@@ -0,0 +1,52 @@
/*
* 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 { Divider, Typography } from '@coze-arch/coze-design';
import styles from './pay-block.module.less';
interface PayBlockProps {
label: string;
value: string;
}
export const PayBlock: React.FC<PayBlockProps> = ({ label, value }) => (
<div className={styles['pay-block']}>
<Typography.Text type="secondary" size="small">
{label}:
</Typography.Text>
<Typography.Text strong size="small">
{value}
</Typography.Text>
</div>
);
interface PayBlocksProps {
options: PayBlockProps[];
}
export const PayBlocks: React.FC<PayBlocksProps> = ({ options }) => (
<div className={styles['pay-blocks']}>
{options.flatMap((item, idx) =>
idx < options.length - 1
? [
<PayBlock key={item.label} {...item} />,
<Divider layout="vertical" margin={4} style={{ height: '10px' }} />,
]
: [<PayBlock key={item.label} {...item} />],
)}
</div>
);

View File

@@ -0,0 +1,32 @@
.trace-detail-panel {
width: 360px;
min-width:360px;
height: 100%;
}
.trace-detail {
padding: 16px;
}
.detail-title {
display: flex;
align-items: center;
column-gap: 4px;
margin-bottom: 4px;
:global .coz-icon-button {
line-height: 0;
}
}
.log-id {
display: flex;
align-items: center;
justify-content: space-between;
:global .coz-icon-button {
line-height: 0;
}
}
.json-viewer {
margin-bottom: 16px;
margin-top: -4px;
}

View File

@@ -0,0 +1,154 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo } from 'react';
import { isUndefined } from 'lodash-es';
import copy from 'copy-to-clipboard';
import { BottomPanel } from '@coze-workflow/test-run-shared';
import { I18n } from '@coze-arch/i18n';
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
import { IconCozCopy } from '@coze-arch/coze-design/icons';
import { Divider, IconButton, Toast, Typography } from '@coze-arch/coze-design';
import { StatusTag } from '../status-tag';
import { FocusButton } from '../focus-button';
import {
formatDuration,
getTokensFromSpan,
getGotoNodeParams,
} from '../../utils';
import { type GotoParams } from '../../types';
import {
MessagePanel,
ObservationModules,
type MessagePanelProps,
} from '../../observation-components';
import { PayBlocks } from './pay-block';
import styles from './trace-detail-panel.module.less';
const ResultViewer: React.FC<
Omit<MessagePanelProps, 'i18nMapping'>
> = props => (
<MessagePanel
{...props}
className={styles['json-viewer']}
i18nMapping={
{
[ObservationModules.INPUT]: {
title: I18n.t('workflow_detail_node_input'),
},
[ObservationModules.OUTPUT]: {
title: I18n.t('workflow_detail_node_output'),
},
} as unknown as MessagePanelProps['i18nMapping']
}
/>
);
interface TraceDetailPanelProps {
span: TraceFrontendSpan;
onClose: () => void;
onGotoNode: (params: GotoParams) => void;
}
export const TraceDetailPanel: React.FC<TraceDetailPanelProps> = ({
span,
onClose,
onGotoNode,
}) => {
const pays = useMemo(() => {
const temp = [
{
label: I18n.t('analytic_query_detail_key_latency'),
value: span.duration
? formatDuration(span.duration as unknown as number)
: '0ms',
},
];
const tokens = getTokensFromSpan(span);
if (!isUndefined(tokens)) {
temp.push({
label: I18n.t('analytic_query_table_title_tokens'),
value: `${tokens}`,
});
}
return temp;
}, [span]);
const handleCopy = () => {
try {
copy(span.log_id || '');
Toast.success({ content: I18n.t('copy_success'), showClose: false });
} catch {
Toast.error(I18n.t('copy_failed'));
}
};
const handleScroll = () => {
onGotoNode(getGotoNodeParams(span));
};
return (
<BottomPanel
header={I18n.t('workflow_running_results')}
onClose={onClose}
className={styles['trace-detail-panel']}
>
<div className={styles['trace-detail']}>
<div className={styles['detail-title']}>
<Typography.Text strong>{span.alias_name}</Typography.Text>
<StatusTag status={span.status_code} />
<FocusButton span={span} onClick={handleScroll} />
</div>
<PayBlocks options={pays} />
{span.log_id ? (
<div className={styles['log-id']}>
<Typography.Text type="secondary" size="small">
LogId: {span.log_id}
</Typography.Text>
<IconButton
icon={<IconCozCopy />}
size="mini"
onClick={handleCopy}
color="secondary"
/>
</div>
) : null}
<Divider margin={16} />
{span.input?.content ? (
<ResultViewer
content={span.input?.content}
category={ObservationModules.INPUT}
jsonViewerProps={{
displayDataTypes: false,
}}
/>
) : null}
{span.output?.content ? (
<ResultViewer
content={span.output?.content}
category={ObservationModules.OUTPUT}
jsonViewerProps={{
displayDataTypes: false,
}}
/>
) : null}
</div>
</BottomPanel>
);
};

View File

@@ -0,0 +1,42 @@
.trace-graph {
display: flex;
height: 100%;
:global .coz-icon-button-mini {
line-height: 0px;
}
}
.graph-part {
width: 50%;
flex-shrink: 1;
flex-grow: 1;
}
.part-tree {
padding: 12px;
overflow-y: auto;
}
.part-chart {
border-left: 1px solid var(--coz-stroke-plus);
}
.trace-charts {
height: 100%;
}
.chart-header {
height: 48px;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--coz-stroke-plus);
.mode-select {
width: 100px;
}
}
.chart-content {
height: calc(100% - 48px);
padding: 4px 8px;
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo, useState } from 'react';
import { clsx } from 'clsx';
import { gotoDebugFlow } from '@coze-workflow/test-run-shared';
import { I18n } from '@coze-arch/i18n';
import {
type Span,
type TraceFrontendSpan,
} from '@coze-arch/bot-api/workflow_api';
import { IconCozExit } from '@coze-arch/coze-design/icons';
import { Typography, Select, IconButton } from '@coze-arch/coze-design';
import { FocusButton } from '../focus-button';
import { getGotoNodeParams } from '../../utils';
import { type GotoParams } from '../../types';
import {
TraceTree,
TraceFlameThread,
spans2SpanNodes,
} from '../../observation-components';
import { useTraceListStore } from '../../contexts';
import { TraceChartsMode } from '../../constants';
import { useTrace } from './use-trace';
import { EmptyTemplate, LoadingTemplate } from './template';
import { TraceTable } from './table';
import css from './graph.module.less';
interface TraceGraphProps {
onOpenDetail: (span: TraceFrontendSpan) => void;
onGotoNode: (params: GotoParams) => void;
}
const MODE_OPTIONS = [
{
label: I18n.t('analytic_query_detail_left_panel_flamethread'),
value: TraceChartsMode.FlameThread,
},
{
label: I18n.t('Starling_filebox_api_list'),
value: TraceChartsMode.Table,
},
];
export const TraceGraph: React.FC<TraceGraphProps> = ({
onOpenDetail,
onGotoNode,
}) => {
const [mode, setMode] = useState(TraceChartsMode.FlameThread);
const { ready, spaceId, isInOp } = useTraceListStore(store => ({
ready: store.ready,
spaceId: store.spaceId,
isInOp: store.isInOp,
}));
const { spans, loading } = useTrace();
const tree = useMemo(() => spans2SpanNodes(spans || ([] as any)), [spans]);
const handleFocusNode = (span: Span) => {
onGotoNode(getGotoNodeParams(span));
};
const jumpToDebugFlow = (span: Span) => {
const params = getGotoNodeParams(span);
gotoDebugFlow(
{
...params,
spaceId,
},
isInOp,
);
};
if (!ready || loading) {
return <LoadingTemplate />;
}
if (!spans) {
return <EmptyTemplate />;
}
return (
<div className={css['trace-graph']}>
<div className={clsx(css['graph-part'], css['part-tree'])}>
<TraceTree
spans={tree.roots}
renderGraphNodeConfig={{
traceTreeCustomRenderer: {
renderExtra: span =>
span.parent_id && span.parent_id !== '0' ? (
<FocusButton span={span} onClick={handleFocusNode} />
) : (
<IconButton
size="mini"
icon={<IconCozExit />}
onClick={e => {
e.stopPropagation();
jumpToDebugFlow(span);
}}
/>
),
},
}}
onSelect={v => {
const span = (v.node.extra as any)?.spanNode;
if (span) {
onOpenDetail(span);
}
}}
/>
</div>
<div className={clsx(css['graph-part'], css['part-chart'])}>
<div className={css['trace-charts']}>
<div className={css['chart-header']}>
<Typography.Text strong fontSize="16px">
{I18n.t('store_bot_detail_title_mobile')}
</Typography.Text>
<Select
size="small"
className={css['mode-select']}
optionList={MODE_OPTIONS}
value={mode}
onChange={(v: any) => setMode(v)}
/>
</div>
<div className={css['chart-content']}>
{mode === TraceChartsMode.Table && <TraceTable spans={spans} />}
{mode === TraceChartsMode.FlameThread && (
<TraceFlameThread spans={spans} />
)}
</div>
</div>
</div>
</div>
);
};

View File

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

View File

@@ -0,0 +1,20 @@
.trace-table {
height: 100%;
overflow-y: auto;
:global {
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
height: 32px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.semi-table-row-head {
font-size: 12px !important;
height: 32px !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
}
.coz-table-wrapper .coz-table-list .semi-table-fixed-header table {
height: 32px;
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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 { isUndefined } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { Table } from '@coze-arch/coze-design';
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
import { formatDuration, getTokensFromSpan } from '../../utils';
import css from './table.module.less';
interface TraceTableProps {
spans: TraceFrontendSpan[];
}
export const TraceTable: React.FC<TraceTableProps> = ({ spans }) => {
const columns = [
{
title: I18n.t('platfrom_trigger_creat_name'),
dataIndex: 'name',
},
{
title: I18n.t('debug_asyn_task_task_status'),
dataIndex: 'status_code',
render: data =>
data === 0
? I18n.t('debug_asyn_task_task_status_success')
: I18n.t('debug_asyn_task_task_status_failed'),
width: 78,
},
{
title: I18n.t('analytic_query_table_title_tokens'),
render: (_, row) => {
const v = getTokensFromSpan(row);
return isUndefined(v) ? '-' : v;
},
width: 78,
},
{
title: I18n.t('db_add_table_field_type_time'),
dataIndex: 'duration',
render: formatDuration,
width: 78,
},
];
return (
<div className={css['trace-table']}>
<Table
tableProps={{
dataSource: spans,
rowKey: 'span_id',
columns,
size: 'small',
}}
/>
</div>
);
};

View File

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

View File

@@ -0,0 +1,32 @@
/*
* 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 { IconCozIllusEmpty } from '@coze-arch/coze-design/illustrations';
import { Spin } from '@coze-arch/coze-design';
import css from './template.module.less';
export const EmptyTemplate = () => (
<div className={css['full-template']}>
<IconCozIllusEmpty width="100px" height="100px" />
</div>
);
export const LoadingTemplate = () => (
<div className={css['full-template']}>
<Spin />
</div>
);

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import dayjs from 'dayjs';
import { useMemoizedFn } from 'ahooks';
import { workflowApi } from '@coze-workflow/base';
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
import { sortSpans } from '../../utils';
import { useTraceListStore } from '../../contexts';
import { MAX_TRACE_TIME } from '../../constants';
export const useTrace = () => {
const [loading, setLoading] = useState(false);
const [spans, setSpans] = useState<TraceFrontendSpan[] | null>(null);
const { span } = useTraceListStore(store => ({
span: store.span,
}));
const fetch = useMemoizedFn(async (logId: string) => {
setLoading(true);
/** 查询日志时,开始结束时间必传,由于用户可查范围为 7 天内,所以直接伪造 7 天时间间隔即可 */
const now = dayjs().endOf('day').valueOf();
const end = dayjs()
.subtract(MAX_TRACE_TIME, 'day')
.startOf('day')
.valueOf();
try {
const { data } = await workflowApi.GetTraceSDK({
log_id: logId,
start_at: end,
end_at: now,
});
if (!data || !data.spans) {
return;
}
const next = sortSpans(data.spans);
setSpans(next);
} finally {
setLoading(false);
}
});
useEffect(() => {
if (span?.log_id) {
fetch(span.log_id);
}
}, [span, fetch]);
return { spans, loading };
};

View File

@@ -0,0 +1,20 @@
.trace-panel-header {
display: flex;
align-items: center;
column-gap: 8px;
height: 100%;
flex-grow: 1;
.trace-title {
font-size: 16px;
font-weight: 500;
color: var(--coz-fg-hglt);
cursor: default;
}
}
.header-tabs {
display: flex;
align-items: center;
column-gap: 16px;
margin-right: 4px;
}

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { BottomPanel } from '@coze-workflow/test-run-shared';
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
import { TraceGraph } from '../trace-graph';
import { type GotoParams } from '../../types';
import { TraceListProvider } from '../../contexts';
import { TraceListPanelHeader } from './header';
interface TraceListPanelProps {
spaceId: string;
workflowId: string;
maxHeight: number;
isInOp?: boolean;
onOpenDetail: (span: TraceFrontendSpan) => void;
onClose: () => void;
onGotoNode: (params: GotoParams) => void;
}
export const TraceListPanel: React.FC<TraceListPanelProps> = ({
spaceId,
workflowId,
isInOp,
maxHeight,
onOpenDetail,
onGotoNode,
onClose,
}) => (
<TraceListProvider spaceId={spaceId} workflowId={workflowId} isInOp={isInOp}>
<BottomPanel
header={<TraceListPanelHeader />}
height={300}
resizable={{
min: 300,
max: maxHeight,
}}
onClose={onClose}
>
<TraceGraph onOpenDetail={onOpenDetail} onGotoNode={onGotoNode} />
</BottomPanel>
</TraceListProvider>
);

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback } from 'react';
import dayjs from 'dayjs';
import { IconCozCalendar } from '@coze-arch/coze-design/icons';
import {
DatePicker as DatePickerCore,
type DatePickerProps as DatePickerCoreProps,
IconButton,
} from '@coze-arch/coze-design';
type DatePickerProps = Pick<DatePickerCoreProps, 'value'> & {
onChange: (v: [Date, Date]) => void;
};
export const DatePicker: React.FC<DatePickerProps> = ({
onChange,
...props
}) => {
const disabledDate = (date?: Date) => {
if (!date) {
return false;
}
const current = date.getTime();
const end = dayjs().endOf('day').valueOf();
const start = dayjs().subtract(6, 'day').startOf('day').valueOf();
return current < start || current > end;
};
const triggerRender = useCallback(
() => (
<IconButton icon={<IconCozCalendar />} color="secondary" size="small" />
),
[],
);
const handleChange = (v: any) => {
onChange(v);
};
return (
<DatePickerCore
type="dateRange"
triggerRender={triggerRender}
disabledDate={disabledDate}
onChange={handleChange}
{...props}
/>
);
};

View File

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

View File

@@ -0,0 +1,24 @@
.select-option {
display: flex;
align-items: center;
justify-content: space-between;
column-gap: 8px;
padding: 0 8px;
.time {
width: 128px;
flex-shrink: 0;
}
.title {
display: flex;
align-items: center;
width: 242px;
column-gap: 4px;
.icon {
flex-shrink: 0;
font-size: 12px;
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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 { gotoDebugFlow } from '@coze-workflow/test-run-shared';
import { I18n } from '@coze-arch/i18n';
import { type Span } from '@coze-arch/bot-api/workflow_api';
import { IconCozExit } from '@coze-arch/coze-design/icons';
import {
Typography,
Tag,
IconButton,
type ButtonProps,
} from '@coze-arch/coze-design';
import { StatusIcon } from '../status-tag';
import {
getTimeFromSpan,
isTriggerFromSpan,
getGotoNodeParams,
} from '../../utils';
import { useTraceListStore } from '../../contexts';
import css from './select-option.module.less';
interface SelectOptionProps {
span: Span;
}
export const SelectOption: React.FC<SelectOptionProps> = ({ span }) => {
const time = useMemo(() => getTimeFromSpan(span), [span]);
const isTrigger = useMemo(() => isTriggerFromSpan(span), [span]);
const { spaceId, isInOp } = useTraceListStore(store => ({
spaceId: store.spaceId,
isInOp: store.isInOp,
}));
const jumpToDebugFlow: ButtonProps['onClick'] = e => {
e.stopPropagation();
const params = getGotoNodeParams(span);
gotoDebugFlow(
{
...params,
spaceId,
},
isInOp,
);
};
return (
<div className={css['select-option']}>
<div className={css.title}>
<StatusIcon status={span.status_code} className={css.icon} />
<Typography.Text ellipsis={{ showTooltip: true }}>
{time}
</Typography.Text>
</div>
{isTrigger ? (
<Tag
style={{
color: 'var(--coz-fg-hglt)',
backgroundColor: 'var(--coz-mg-hglt)',
}}
size={'mini'}
>
{I18n.t('workflow_start_trigger_triggername')}
</Tag>
) : null}
<IconButton
size="mini"
icon={<IconCozExit />}
onClick={jumpToDebugFlow}
/>
</div>
);
};

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozFilter } from '@coze-arch/coze-design/icons';
import { Select, IconButton } from '@coze-arch/coze-design';
import { SpanStatus } from '@coze-arch/bot-api/workflow_api';
interface StatusSelectProps {
value: SpanStatus;
onChange: (v: SpanStatus) => void;
}
export const StatusSelect: React.FC<StatusSelectProps> = ({
value,
onChange,
}) => {
const triggerRender = useCallback(
() => (
<IconButton icon={<IconCozFilter />} color="secondary" size="small" />
),
[],
);
return (
<Select
value={value}
triggerRender={triggerRender}
optionList={[
{
value: SpanStatus.Unknown,
label: I18n.t('query_status_all'),
},
{
value: SpanStatus.Fail,
label: I18n.t('query_status_failed'),
},
{
value: SpanStatus.Success,
label: I18n.t('query_status_completed'),
},
]}
onChange={onChange as any}
/>
);
};

View File

@@ -0,0 +1,22 @@
.trace-select {
display: flex;
align-items: center;
}
.trace-filter {
height: 32px;
display: flex;
align-items: center;
column-gap: 4px;
padding: 0 4px;
border: 1px solid var(--coz-stroke-plus);
border-right: 0;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.main-select {
width: 200px;
border-top-left-radius: 0px!important;
border-bottom-left-radius: 0px!important;
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useMemo } from 'react';
import { useMemoizedFn } from 'ahooks';
import { Select } from '@coze-arch/coze-design';
import { getTimeFromSpan } from '../../utils';
import { useTraceListStore } from '../../contexts';
import { useOptions } from './use-options';
import { StatusSelect } from './status-select';
import { SelectOption } from './select-option';
import { DatePicker } from './date-picker';
import css from './trace-select.module.less';
export const TraceSelect: React.FC = () => {
const { span, workflowId, patch } = useTraceListStore(store => ({
span: store.span,
workflowId: store.workflowId,
patch: store.patch,
}));
const {
date,
status,
setStatus,
options,
optionsCacheRef,
fetch,
onDateChange,
} = useOptions(workflowId);
const optionList = useMemo(() => {
const temp = options.map(i => ({
...i,
value: i.log_id,
label: <SelectOption span={i} />,
}));
return temp;
}, [options]);
const handleChange = useCallback(
(v: any) => {
patch({
span:
v && optionsCacheRef.current.has(v)
? optionsCacheRef.current.get(v)
: null,
});
},
[optionsCacheRef, patch],
);
const handleDropdownVisibleChange = useMemoizedFn((v: boolean) => {
if (v) {
fetch();
}
});
return (
<div className={css['trace-select']}>
<div className={css['trace-filter']}>
<DatePicker value={date} onChange={onDateChange} />
<StatusSelect value={status} onChange={setStatus} />
</div>
<Select
className={css['main-select']}
optionList={optionList}
value={span?.log_id}
onChange={handleChange}
renderSelectedItem={e => {
const current = optionsCacheRef.current.get(e.value);
return current ? getTimeFromSpan(current) : '-';
}}
onDropdownVisibleChange={handleDropdownVisibleChange}
/>
</div>
);
};

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
import { useCallback, useEffect, useRef, useState } from 'react';
import dayjs from 'dayjs';
import { useMemoizedFn } from 'ahooks';
import { workflowApi } from '@coze-workflow/base';
import { SpanStatus, type Span } from '@coze-arch/bot-api/workflow_api';
import { useTraceListStore } from '../../contexts';
import { MAX_TRACE_LENGTH, MAX_TRACE_TIME } from '../../constants';
const getDefaultDate = (): [Date, Date] => {
const end = dayjs().endOf('day').toDate();
const start = dayjs()
.subtract(MAX_TRACE_TIME - 1, 'day')
.startOf('day')
.toDate();
return [start, end];
};
export const useOptions = (workflowId: string) => {
const [date, setDate] = useState<[Date, Date]>(getDefaultDate());
const [status, setStatus] = useState<SpanStatus>(SpanStatus.Unknown);
const [options, setOptions] = useState<Span[]>([]);
const optionsCacheRef = useRef(new Map<string, Span>());
const { ready, span, patch } = useTraceListStore(store => ({
span: store.span,
ready: store.ready,
patch: store.patch,
}));
const fetch = useMemoizedFn(async () => {
const searchParams = new URLSearchParams(location.search);
const executeMode = searchParams.get('execute_mode');
const executeId = searchParams.get('execute_id');
const { spans } = await workflowApi.ListRootSpans({
workflow_id: workflowId,
limit: MAX_TRACE_LENGTH,
offset: 0,
start_at: date[0].getTime(),
end_at: date[1].getTime(),
status: status === SpanStatus.Unknown ? undefined : status,
execute_mode: executeMode ? Number(executeMode) : undefined,
});
const next = spans || [];
let maybeInitialSpan = next[0];
if (executeId && !ready && !span) {
try {
const { data } = await workflowApi.GetTraceSDK({
execute_id: executeId,
workflow_id: workflowId,
start_at: date[0].getTime(),
end_at: date[1].getTime(),
});
const first = data?.spans?.[0];
if (first?.log_id) {
maybeInitialSpan = first;
const urlSpan = next.find(i => i.log_id === first.log_id);
if (!urlSpan) {
next.unshift(first);
}
}
// eslint-disable-next-line @coze-arch/no-empty-catch -- 无需报错
} catch {
// 无需报错
}
}
next.forEach(s => {
if (s.log_id) {
optionsCacheRef.current.set(s.log_id, s);
}
});
setOptions(next);
// 如果没有初始化,就初始化一次
if (!ready && !span && maybeInitialSpan) {
patch({ span: maybeInitialSpan });
}
if (!ready) {
patch({ ready: true });
}
});
const handleDateChange = useCallback(
(next: [Date, Date]) => {
const [start, end] = next;
// 时间选择器选择的日期是当天 0 点,需要转化为当天 11 点 59 分 59 秒
setDate([start, dayjs(end).endOf('day').toDate()] as [Date, Date]);
},
[setDate],
);
useEffect(() => {
fetch();
}, [date, status, fetch]);
return {
date,
status,
setStatus,
options,
optionsCacheRef,
fetch,
onDateChange: handleDateChange,
};
};