From 4cbe470089afe0aae69023737cbbfd1dfc7c89ad Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 19 Dec 2025 12:56:56 +0800 Subject: [PATCH] Feat: Display error messages from intermediate nodes of the webhook. #10427 (#11954) ### What problem does this PR solve? Feat: Remove HMAC from the webhook #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/copy-to-clipboard.tsx | 9 + web/src/components/originui/number-input.tsx | 8 + web/src/components/ragflow-form.tsx | 10 +- web/src/constants/agent.tsx | 2 +- web/src/hooks/use-agent-request.ts | 75 +++- web/src/interfaces/database/agent.ts | 7 + web/src/interfaces/request/agent.ts | 5 + web/src/pages/agent/constant/index.tsx | 12 +- web/src/pages/agent/form/begin-form/index.tsx | 1 + web/src/pages/agent/form/begin-form/schema.ts | 11 +- .../form/begin-form/use-handle-mode-change.ts | 4 +- .../agent/form/begin-form/webhook/auth.tsx | 50 +-- .../begin-form/webhook/dynamic-request.tsx | 30 +- .../agent/form/begin-form/webhook/index.tsx | 48 ++- .../begin-form/webhook/request-schema.tsx | 37 +- .../form/begin-form/webhook/response.tsx | 2 +- .../agent/form/components/json-viewer.tsx | 21 ++ web/src/pages/agent/hooks/use-is-webhook.ts | 10 + web/src/pages/agent/index.tsx | 24 +- web/src/pages/agent/log-sheet/data.ts | 243 ------------- .../agent/log-sheet/tool-timeline-item.tsx | 7 +- .../agent/log-sheet/workflow-timeline.tsx | 22 +- web/src/pages/agent/utils.ts | 16 +- web/src/pages/agent/webhook-sheet/index.tsx | 94 +++++ .../pages/agent/webhook-sheet/timeline.tsx | 337 ++++++++++++++++++ web/src/services/agent-service.ts | 8 + web/src/utils/api.ts | 3 + 27 files changed, 737 insertions(+), 359 deletions(-) create mode 100644 web/src/pages/agent/form/components/json-viewer.tsx create mode 100644 web/src/pages/agent/hooks/use-is-webhook.ts delete mode 100644 web/src/pages/agent/log-sheet/data.ts create mode 100644 web/src/pages/agent/webhook-sheet/index.tsx create mode 100644 web/src/pages/agent/webhook-sheet/timeline.tsx diff --git a/web/src/components/copy-to-clipboard.tsx b/web/src/components/copy-to-clipboard.tsx index 8d4c436a2..50db68435 100644 --- a/web/src/components/copy-to-clipboard.tsx +++ b/web/src/components/copy-to-clipboard.tsx @@ -25,3 +25,12 @@ const CopyToClipboard = ({ text }: Props) => { }; export default CopyToClipboard; + +export function CopyToClipboardWithText({ text }: { text: string }) { + return ( +
+ {text} + +
+ ); +} diff --git a/web/src/components/originui/number-input.tsx b/web/src/components/originui/number-input.tsx index 8017d32c7..535c1e8b3 100644 --- a/web/src/components/originui/number-input.tsx +++ b/web/src/components/originui/number-input.tsx @@ -7,6 +7,7 @@ interface NumberInputProps { onChange?: (value: number) => void; height?: number | string; min?: number; + max?: number; } const NumberInput: React.FC = ({ @@ -15,6 +16,7 @@ const NumberInput: React.FC = ({ onChange, height, min = 0, + max = Infinity, }) => { const [value, setValue] = useState(() => { return initialValue ?? 0; @@ -34,6 +36,9 @@ const NumberInput: React.FC = ({ }; const handleIncrement = () => { + if (value > max - 1) { + return; + } setValue(value + 1); onChange?.(value + 1); }; @@ -41,6 +46,9 @@ const NumberInput: React.FC = ({ const handleChange = (e: React.ChangeEvent) => { const newValue = Number(e.target.value); if (!isNaN(newValue)) { + if (newValue > max) { + return; + } setValue(newValue); onChange?.(newValue); } diff --git a/web/src/components/ragflow-form.tsx b/web/src/components/ragflow-form.tsx index d7a94fb37..e1e3b832a 100644 --- a/web/src/components/ragflow-form.tsx +++ b/web/src/components/ragflow-form.tsx @@ -7,7 +7,11 @@ import { } from '@/components/ui/form'; import { cn } from '@/lib/utils'; import { ReactNode, cloneElement, isValidElement } from 'react'; -import { ControllerRenderProps, useFormContext } from 'react-hook-form'; +import { + ControllerRenderProps, + UseControllerProps, + useFormContext, +} from 'react-hook-form'; type RAGFlowFormItemProps = { name: string; @@ -18,7 +22,7 @@ type RAGFlowFormItemProps = { required?: boolean; labelClassName?: string; className?: string; -}; +} & Pick, 'rules'>; export function RAGFlowFormItem({ name, @@ -29,11 +33,13 @@ export function RAGFlowFormItem({ required = false, labelClassName, className, + rules, }: RAGFlowFormItemProps) { const form = useFormContext(); return ( ( { + const { id } = useParams(); + const [currentWebhookId, setCurrentWebhookId] = useState(''); + const [currentNextSinceTs, setCurrentNextSinceTs] = useState(0); + const [shouldPoll, setShouldPoll] = useState(autoStart); + + const { + data, + isFetching: loading, + refetch, + } = useQuery({ + queryKey: [AgentApiAction.FetchWebhookTrace, id], + refetchOnReconnect: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchIntervalInBackground: false, + gcTime: 0, + enabled: !!id && shouldPoll, + queryFn: async () => { + if (!id) return {}; + + const payload: IAgentWebhookTraceRequest = + {} as IAgentWebhookTraceRequest; + + if (currentNextSinceTs) { + payload['since_ts'] = currentNextSinceTs; + } + + if (currentWebhookId) { + payload['webhook_id'] = currentWebhookId; + } + + const { data } = await fetchWebhookTrace(id, payload); + + const result = data.data ?? {}; + + if (result.webhook_id && result.webhook_id !== currentWebhookId) { + setCurrentWebhookId(result.webhook_id); + } + + if ( + currentNextSinceTs === 0 && + result.next_since_ts && + result.next_since_ts !== currentNextSinceTs + ) { + setCurrentNextSinceTs(result.next_since_ts); + } + + if (result.finished) { + setShouldPoll(false); + } + + return result; + }, + refetchInterval: shouldPoll ? 3000 : false, + }); + + return { + data, + loading, + refetch, + isPolling: shouldPoll, + currentWebhookId, + currentNextSinceTs, + }; +}; diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index f1a658fef..ebdf65836 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -291,3 +291,10 @@ export interface GlobalVariableType { description: string; type: string; } + +export interface IWebhookTrace { + webhook_id: null; + events: any[]; + next_since_ts: number; + finished: boolean; +} diff --git a/web/src/interfaces/request/agent.ts b/web/src/interfaces/request/agent.ts index 1c6ee8b88..24e5257c8 100644 --- a/web/src/interfaces/request/agent.ts +++ b/web/src/interfaces/request/agent.ts @@ -2,3 +2,8 @@ export interface IDebugSingleRequestBody { component_id: string; params: Record; } + +export interface IAgentWebhookTraceRequest { + since_ts: number; // From the first request for return + webhook_id: string; // Each external request generates a unique webhook ID. +} diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index dfe183d2f..9731fbf3d 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -1050,12 +1050,18 @@ export enum WebhookSecurityAuthType { Token = 'token', Basic = 'basic', Jwt = 'jwt', - Hmac = 'hmac', } -export const RateLimitPerList = ['minute', 'hour', 'day']; +export enum WebhookRateLimitPer { + Second = 'second', + Minute = 'minute', + Hour = 'hour', + Day = 'day', +} -export const WebhookMaxBodySize = ['10MB', '50MB', '100MB', '1000MB']; +export const RateLimitPerList = Object.values(WebhookRateLimitPer); + +export const WebhookMaxBodySize = ['1MB', '5MB', '10MB']; export enum WebhookRequestParameters { File = VariableType.File, diff --git a/web/src/pages/agent/form/begin-form/index.tsx b/web/src/pages/agent/form/begin-form/index.tsx index 785b8ea57..aca44e79a 100644 --- a/web/src/pages/agent/form/begin-form/index.tsx +++ b/web/src/pages/agent/form/begin-form/index.tsx @@ -43,6 +43,7 @@ function BeginForm({ node }: INextOperatorForm) { const form = useForm({ defaultValues: values, resolver: zodResolver(BeginFormSchema), + mode: 'onChange', }); useWatchFormChange(node?.id, form); diff --git a/web/src/pages/agent/form/begin-form/schema.ts b/web/src/pages/agent/form/begin-form/schema.ts index 07511b6de..33fb586e7 100644 --- a/web/src/pages/agent/form/begin-form/schema.ts +++ b/web/src/pages/agent/form/begin-form/schema.ts @@ -1,4 +1,4 @@ -import { WebhookAlgorithmList } from '@/constants/agent'; +import { WebhookJWTAlgorithmList } from '@/constants/agent'; import { z } from 'zod'; export const BeginFormSchema = z.object({ @@ -30,7 +30,14 @@ export const BeginFormSchema = z.object({ max_body_size: z.string(), jwt: z .object({ - algorithm: z.string().default(WebhookAlgorithmList[0]).optional(), + algorithm: z.string().default(WebhookJWTAlgorithmList[0]).optional(), + required_claims: z.array(z.object({ value: z.string() })), + }) + .optional(), + hmac: z + .object({ + header: z.string().optional(), + secret: z.string().optional(), }) .optional(), }) diff --git a/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts b/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts index 93b2722f3..416fbd484 100644 --- a/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts +++ b/web/src/pages/agent/form/begin-form/use-handle-mode-change.ts @@ -2,11 +2,11 @@ import { useCallback } from 'react'; import { UseFormReturn } from 'react-hook-form'; import { AgentDialogueMode, - RateLimitPerList, WebhookContentType, WebhookExecutionMode, WebhookMaxBodySize, WebhookMethod, + WebhookRateLimitPer, WebhookSecurityAuthType, } from '../../constant'; @@ -14,7 +14,7 @@ const initialFormValuesMap = { methods: [WebhookMethod.Get], schema: {}, 'security.auth_type': WebhookSecurityAuthType.Basic, - 'security.rate_limit.per': RateLimitPerList[0], + 'security.rate_limit.per': WebhookRateLimitPer.Second, 'security.rate_limit.limit': 10, 'security.max_body_size': WebhookMaxBodySize[0], 'response.status': 200, diff --git a/web/src/pages/agent/form/begin-form/webhook/auth.tsx b/web/src/pages/agent/form/begin-form/webhook/auth.tsx index 4a739b491..c1ba78208 100644 --- a/web/src/pages/agent/form/begin-form/webhook/auth.tsx +++ b/web/src/pages/agent/form/begin-form/webhook/auth.tsx @@ -1,16 +1,15 @@ import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; import { Input } from '@/components/ui/input'; -import { WebhookAlgorithmList } from '@/constants/agent'; +import { WebhookJWTAlgorithmList } from '@/constants/agent'; import { WebhookSecurityAuthType } from '@/pages/agent/constant'; import { buildOptions } from '@/utils/form'; import { useCallback } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { DynamicStringForm } from '../../components/dynamic-string-form'; -const AlgorithmOptions = buildOptions(WebhookAlgorithmList); - -const RequiredClaimsOptions = buildOptions(['exp', 'sub']); +const AlgorithmOptions = buildOptions(WebhookJWTAlgorithmList); export function Auth() { const { t } = useTranslation(); @@ -88,38 +87,10 @@ export function Auth() { > - - - - - ), - [t], - ); - - const renderHmacAuth = useCallback( - () => ( - <> - - - - - - - - - + > ), [t], @@ -129,11 +100,14 @@ export function Auth() { [WebhookSecurityAuthType.Token]: renderTokenAuth, [WebhookSecurityAuthType.Basic]: renderBasicAuth, [WebhookSecurityAuthType.Jwt]: renderJwtAuth, - [WebhookSecurityAuthType.Hmac]: renderHmacAuth, [WebhookSecurityAuthType.None]: () => null, }; - return AuthMap[ - (authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType - ](); + return ( +
+ {AuthMap[ + (authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType + ]()} +
+ ); } diff --git a/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx b/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx index 3e912e36a..2124f5e18 100644 --- a/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx +++ b/web/src/pages/agent/form/begin-form/webhook/dynamic-request.tsx @@ -6,15 +6,10 @@ import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { buildOptions } from '@/utils/form'; import { loader } from '@monaco-editor/react'; -import { omit } from 'lodash'; import { X } from 'lucide-react'; import { ReactNode } from 'react'; -import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; -import { - TypesWithArray, - WebhookContentType, - WebhookRequestParameters, -} from '../../../constant'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { TypesWithArray, WebhookRequestParameters } from '../../../constant'; import { DynamicFormHeader } from '../../components/dynamic-fom-header'; loader.config({ paths: { vs: '/vs' } }); @@ -28,16 +23,9 @@ type SelectKeysProps = { requiredField?: string; nodeId?: string; isObject?: boolean; + operatorList: WebhookRequestParameters[]; }; -function buildParametersOptions(isObject: boolean) { - const list = isObject - ? WebhookRequestParameters - : omit(WebhookRequestParameters, ['File']); - - return buildOptions(list); -} - export function DynamicRequest({ name, label, @@ -45,15 +33,9 @@ export function DynamicRequest({ keyField = 'key', operatorField = 'type', requiredField = 'required', - isObject = false, + operatorList, }: SelectKeysProps) { const form = useFormContext(); - const contentType = useWatch({ - name: 'content_types', - control: form.control, - }); - const isFormDataContentType = - contentType === WebhookContentType.MultipartFormData; const { fields, remove, append } = useFieldArray({ name: name, @@ -94,9 +76,7 @@ export function DynamicRequest({ onChange={(val) => { field.onChange(val); }} - options={buildParametersOptions( - isObject && isFormDataContentType, - )} + options={buildOptions(operatorList)} > )} diff --git a/web/src/pages/agent/form/begin-form/webhook/index.tsx b/web/src/pages/agent/form/begin-form/webhook/index.tsx index 8bcae0f29..5f1509b65 100644 --- a/web/src/pages/agent/form/begin-form/webhook/index.tsx +++ b/web/src/pages/agent/form/begin-form/webhook/index.tsx @@ -1,17 +1,20 @@ import { Collapse } from '@/components/collapse'; -import CopyToClipboard from '@/components/copy-to-clipboard'; +import { CopyToClipboardWithText } from '@/components/copy-to-clipboard'; +import NumberInput from '@/components/originui/number-input'; import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; -import { Input } from '@/components/ui/input'; import { MultiSelect } from '@/components/ui/multi-select'; import { Textarea } from '@/components/ui/textarea'; import { buildOptions } from '@/utils/form'; +import { useCallback } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useParams } from 'umi'; import { RateLimitPerList, WebhookMaxBodySize, WebhookMethod, + WebhookRateLimitPer, WebhookSecurityAuthType, } from '../../../constant'; import { DynamicStringForm } from '../../components/dynamic-string-form'; @@ -21,18 +24,32 @@ import { WebhookResponse } from './response'; const RateLimitPerOptions = buildOptions(RateLimitPerList); +const RequestLimitMap = { + [WebhookRateLimitPer.Second]: 100, + [WebhookRateLimitPer.Minute]: 1000, + [WebhookRateLimitPer.Hour]: 10000, + [WebhookRateLimitPer.Day]: 100000, +}; + export function WebHook() { const { t } = useTranslation(); const { id } = useParams(); + const form = useFormContext(); + + const rateLimitPer = useWatch({ + name: 'security.rate_limit.per', + control: form.control, + }); + + const getLimitRateLimitPerMax = useCallback((rateLimitPer: string) => { + return RequestLimitMap[rateLimitPer as keyof typeof RequestLimitMap] ?? 100; + }, []); const text = `${location.protocol}//${location.host}/api/v1/webhook/${id}`; return ( <> -
- {text} - -
+ {(field) => ( - + - + {(field) => ( + { + field.onChange(val); + form.setValue( + 'security.rate_limit.limit', + getLimitRateLimitPerMax(val), + ); + }} + > + )} { + return isFormDataContentType + ? [ + WebhookRequestParameters.String, + WebhookRequestParameters.Number, + WebhookRequestParameters.Boolean, + WebhookRequestParameters.File, + ] + : [ + WebhookRequestParameters.String, + WebhookRequestParameters.Number, + WebhookRequestParameters.Boolean, + ]; + }, [isFormDataContentType]); return ( {t('flow.webhook.schema')}}> @@ -23,14 +50,20 @@ export function WebhookRequestSchema() { diff --git a/web/src/pages/agent/form/begin-form/webhook/response.tsx b/web/src/pages/agent/form/begin-form/webhook/response.tsx index 44591d5f1..a3813bd8e 100644 --- a/web/src/pages/agent/form/begin-form/webhook/response.tsx +++ b/web/src/pages/agent/form/begin-form/webhook/response.tsx @@ -49,7 +49,7 @@ export function WebhookResponse() { name="response.body_template" label={t('flow.webhook.bodyTemplate')} > - + )} diff --git a/web/src/pages/agent/form/components/json-viewer.tsx b/web/src/pages/agent/form/components/json-viewer.tsx new file mode 100644 index 000000000..3a6d1b558 --- /dev/null +++ b/web/src/pages/agent/form/components/json-viewer.tsx @@ -0,0 +1,21 @@ +import JsonView from 'react18-json-view'; + +export function JsonViewer({ + data, + title, +}: { + data: Record; + title: string; +}) { + return ( +
+
{title}
+ +
+ ); +} diff --git a/web/src/pages/agent/hooks/use-is-webhook.ts b/web/src/pages/agent/hooks/use-is-webhook.ts new file mode 100644 index 000000000..297a511f0 --- /dev/null +++ b/web/src/pages/agent/hooks/use-is-webhook.ts @@ -0,0 +1,10 @@ +import { AgentDialogueMode, BeginId } from '../constant'; +import useGraphStore from '../store'; + +export function useIsWebhookMode() { + const getNode = useGraphStore((state) => state.getNode); + + const beginNode = getNode(BeginId); + + return beginNode?.data.form?.mode === AgentDialogueMode.Webhook; +} diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index b0d2f6f15..02a9fd646 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -46,6 +46,7 @@ import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log'; import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query'; import { useIsPipeline } from './hooks/use-is-pipeline'; +import { useIsWebhookMode } from './hooks/use-is-webhook'; import { useRunDataflow } from './hooks/use-run-dataflow'; import { useSaveGraph, @@ -58,6 +59,7 @@ import { SettingDialog } from './setting-dialog'; import useGraphStore from './store'; import { useAgentHistoryManager } from './use-agent-history-manager'; import { VersionDialog } from './version-dialog'; +import WebhookSheet from './webhook-sheet'; function AgentDropdownMenuItem({ children, @@ -110,6 +112,7 @@ export default function Agent() { useShowEmbedModal(); const { navigateToAgentLogs } = useNavigatePage(); const time = useWatchAgentChange(chatDrawerVisible); + const isWebhookMode = useIsWebhookMode(); // pipeline @@ -119,6 +122,12 @@ export default function Agent() { showModal: showPipelineRunSheet, } = useSetModalState(); + const { + visible: webhookTestSheetVisible, + hideModal: hideWebhookTestSheet, + showModal: showWebhookTestSheet, + } = useSetModalState(); + const { visible: pipelineLogSheetVisible, showModal: showPipelineLogSheet, @@ -172,12 +181,20 @@ export default function Agent() { }); const handleButtonRunClick = useCallback(() => { - if (isPipeline) { + if (isWebhookMode) { + showWebhookTestSheet(); + } else if (isPipeline) { handleRunPipeline(); } else { handleRunAgent(); } - }, [handleRunAgent, handleRunPipeline, isPipeline]); + }, [ + handleRunAgent, + handleRunPipeline, + isPipeline, + isWebhookMode, + showWebhookTestSheet, + ]); const { run: runPipeline, @@ -320,6 +337,9 @@ export default function Agent() { hideModal={hideGlobalParamSheet} > )} + {webhookTestSheetVisible && ( + + )} ); } diff --git a/web/src/pages/agent/log-sheet/data.ts b/web/src/pages/agent/log-sheet/data.ts deleted file mode 100644 index fc8d32323..000000000 --- a/web/src/pages/agent/log-sheet/data.ts +++ /dev/null @@ -1,243 +0,0 @@ -export const list = [ - { - event: 'node_finished', - message_id: 'dce6c0c8466611f08e04047c16ec874f', - created_at: 1749606805, - task_id: 'db68eb0645ab11f0bbdc047c16ec874f', - data: { - inputs: {}, - outputs: { - _elapsed_time: 0.000010083022061735392, - }, - component_id: 'begin', - error: null, - elapsed_time: 0.000010083022061735392, - created_at: 1749606805, - }, - }, - { - event: 'node_finished', - message_id: 'dce6c0c8466611f08e04047c16ec874f', - created_at: 1749606805, - task_id: 'db68eb0645ab11f0bbdc047c16ec874f', - data: { - inputs: { - query: '算法', - }, - outputs: { - formalized_content: - '\nDocument: Mallat算法频3333率混叠原因及其改进模型.pdf \nRelevant fragments as following:\n---\nID: Retrieval:ClearHornetsClap_0\nFig. 6 The improved decomposition model of Ma llat algorithm\n图6 改进的Mallat分解算法模型\ncaj\nG\n→cdj+1\n↑2\nT\ncdj+1\n---\nID: Retrieval:ClearHornetsClap_1\n2 Mallat算法Mallat算法是StéPhanMallat将计算机视觉领域内的多分辨分析思想引入到小波分析中,推导出的小波分析快速算法但在具体使用时,Mallat算法是利用与尺度函数和小波函数相对应的小波低通滤波器H, h和小波高通滤波器G, g对信号进行低通和高通滤波来具体实现的为了叙述方便,在此把尺度函数称为低频子带,小波函数称为次高频子带Mallat算法如下\n---\nID: Retrieval:ClearHornetsClap_2\n4 改进算法模型经过对Mallat算法的深入研究可知,Mallat算法实现过程会不可避免地产生频率混叠现象可是,为什么现在小波分析的Mallat算法还能得到广泛的应用呢?这就是Mallat算法的精妙之处由小波分解算法和重构算法可知,小波分解过程是隔点采样的过程,重构过程是隔点插零的过程,实际上这两个过程都产生频率混叠,但是它们产生混叠的方向正好相反也就是说分解过程产生的混叠又在重构过程中得到了纠正[13 ]限于篇幅隔点插零产生频率混叠现象本文不做详细的讨论不过,这也给如何解决Mallat分解算法产生频率混叠现象提供了一个思路:在利用Mallat算法对信号分解,得到各尺度上小波系数后,再按照重构的方法重构至所需的小波空间系数cdj ,利用该重构系数cdj代替相应尺度上得到的小波系数cdj 来分析该尺度上的信号,这样就能较好地解决频率混叠所带来的影响,以达到预期的目的本文称这种算法为子带信号重构算法经改进的算法模型如图6所示\n---\nID: Retrieval:ClearHornetsClap_3\n关键词:小波分析;Mallat算法;频率混叠中图分类号: TN911. 6   文献标识码: A  文章编号: 10030972 (2007)04051104Reason andM eliorationModel of Generating Frequency A liasing of Ma llat A lgor ithmGUO Chaofeng, L IM e ilian(College of Computer Science &Technology, XuchangUniversity, Xuchang 461000, China)Abstract:Because of the design ofMallatA lgorithm, the phenomenon of frequency aliasing exists in the signal decomposition process. Based on research and analysis ofMallat algorithm, the reasons thatMallat algorithm generates frequency aliasing were found out, and an improved modle that can elim inate efficiently frequency aliasingwas given.\n---\nID: Retrieval:ClearHornetsClap_4\n·应用技术研究·Mallat算率混叠原因及其改进模型郭超峰,李梅莲(许昌学院计算机科学与技术学院,河南许昌461000)摘 要:Mallat算法由于自身设计的原因,在信号分解过程中,存在频率混叠现象在利用小波分析进行信号提取时,这种现象是一个不容忽视的问题. 通过分析Mallat算法,找出了造成Mallat算法产生频率混叠的原因,给出了一个能有效消除频率混叠的改进算法模型\n---\nID: Retrieval:ClearHornetsClap_5\nF ig. 4 Two 1-D miensiona l pagoda decomposition process ofMa llat a lgor ithm重构算法:2fsHz, J表示分解的深度A j [f (t)]=2{∑k h (t -2k)A j+1 [f (t)]+其中: j、J意义与式(2)相同, j =J -1, J -2, …, 0; h, g为小波重构; A j、D j 意义与式(2)相同Mallat二维塔式小波变换的重构过程如图5所示\n---\nID: Retrieval:ClearHornetsClap_6\n1 Mallat算法的频率混叠现象对最高频率为的带限信号进行离散化抽样,如果抽样周期比较大,或者说抽样频率比较小,那么抽样将会导致频率相邻的2个被延拓的频谱发生叠加而互相产生影响,这种现象称为混叠[78 ]下面是一个利用Mallat算法进行信号分解的例子[912 ]\n---\nID: Retrieval:ClearHornetsClap_7\n∑k g (t -2k)D j+1 [f (t)]}, \n', - _references: { - total: 30, - chunks: [ - { - chunk_id: '64fe175ac75330dd', - content_ltks: - 'fig 6 the improv decomposit model of ma llat algorithm 图 6 改进 的 mallat 分解 算法 模型 caj g cdj 12 t cdj 1', - content_with_weight: - 'Fig. 6 The improved decomposition model of Ma llat algorithm\n图6 改进的Mallat分解算法模型\ncaj\nG\n→cdj+1\n↑2\nT\ncdj+1', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-64fe175ac75330dd', - similarity: 0.8437627020510018, - vector_similarity: 0.47920900683667284, - term_similarity: 1, - positions: [[3, 303, 500, 513, 545]], - doc_type_kwd: 'image', - }, - { - chunk_id: 'dad90c5ea1b0945b', - content_ltks: - '2 mallat 算法 mallat 算法 是 st é phanmallat 将 计算机 视觉 领域 内 的 多 分辨 分析 思想 引入 到 小波 分析 中 推导 出 的 小波 分析 快速 算法 但 在 具体 使用 时 mallat 算法 是 利用 与 尺度 函数 和 小波 函数 相对 应 的 小波 低通滤波器 h h 和 小波 高通 滤波器 g g 对 信号 进行 低 通和 高 通滤波 来 具体 实现 的 为了 叙述 方便 在 此 把 尺度 函数 称为 低频 子 带 小 波函数 称为 次 高频 子 带 mallat 算法 如下', - content_with_weight: - '2 Mallat算法Mallat算法是StéPhanMallat将计算机视觉领域内的多分辨分析思想引入到小波分析中,推导出的小波分析快速算法但在具体使用时,Mallat算法是利用与尺度函数和小波函数相对应的小波低通滤波器H, h和小波高通滤波器G, g对信号进行低通和高通滤波来具体实现的为了叙述方便,在此把尺度函数称为低频子带,小波函数称为次高频子带Mallat算法如下', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-dad90c5ea1b0945b', - similarity: 0.8354827046851212, - vector_similarity: 0.4516090156170707, - term_similarity: 1, - positions: [[2, 38, 267, 673, 770]], - doc_type_kwd: '', - }, - { - chunk_id: '28df4d0c894e3201', - content_ltks: - '4 改进 算法 模型 经过 对 mallat 算法 的 深入研究 可知 mallat 算法 实现 过程 会 不可避免 地 产生 频率 混 叠 现象 可是 为什么 现在 小波 分析 的 mallat 算法 还 能 得到 广泛 的 应用 呢 这 就是 mallat 算法 的 精妙 之处 由 小波 分解 算法 和 重构 算法 可知 小波 分解 过程 是 隔 点 采样 的 过程 重构 过程 是 隔 点 插 零 的 过程 实际上 这 两个 过程 都 产生 频率 混 叠 但是 它们 产生 混 叠 的 方向 正好 相反 也就是说 分解 过程 产生 的 混 叠 又 在 重构 过程 中 得到 了 纠正 13 限于 篇幅 隔 点 插 零 产生 频率 混 叠 现象 本文 不 做 详细 的 讨论 不过 这 也 给 如何 解决 mallat 分解 算法 产生 频率 混 叠 现象 提供 了 一个 思路 在 利用 mallat 算法 对 信号 分解 得到 各 尺度 上 小波 系数 后 再 按照 重构 的 方法 重构 至 所需 的 小波 空间 系数 cdj 利用 该 重构 系数 cdj 代替 相应 尺度 上 得到 的 小波 系数 cdj 来 分析 该 尺度 上 的 信号 这样 就 能 较 好地解决 频率 混 叠 所 带来 的 影响 以 达到 预期 的 目的 本文 称 这种 算法 为 子 带 信号 重构 算法 经 改进 的 算法 模型 如 图 6 所示', - content_with_weight: - '4 改进算法模型经过对Mallat算法的深入研究可知,Mallat算法实现过程会不可避免地产生频率混叠现象可是,为什么现在小波分析的Mallat算法还能得到广泛的应用呢?这就是Mallat算法的精妙之处由小波分解算法和重构算法可知,小波分解过程是隔点采样的过程,重构过程是隔点插零的过程,实际上这两个过程都产生频率混叠,但是它们产生混叠的方向正好相反也就是说分解过程产生的混叠又在重构过程中得到了纠正[13 ]限于篇幅隔点插零产生频率混叠现象本文不做详细的讨论不过,这也给如何解决Mallat分解算法产生频率混叠现象提供了一个思路:在利用Mallat算法对信号分解,得到各尺度上小波系数后,再按照重构的方法重构至所需的小波空间系数cdj ,利用该重构系数cdj代替相应尺度上得到的小波系数cdj 来分析该尺度上的信号,这样就能较好地解决频率混叠所带来的影响,以达到预期的目的本文称这种算法为子带信号重构算法经改进的算法模型如图6所示', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-28df4d0c894e3201', - similarity: 0.8322061155631969, - vector_similarity: 0.4406870518773232, - term_similarity: 1, - positions: [ - [3, 285, 518, 257, 272], - [3, 282, 515, 280, 504], - ], - doc_type_kwd: '', - }, - { - chunk_id: 'e79b07acbec9eb61', - content_ltks: - '关键词 小波 分析 mallat 算法 频率 混 叠 中图 分类号 tn911 6 文献 标识码 a 文章 编号 10030972 2007 04051104reason andm eliorationmodel of gener frequenc a lias of ma llat a lgor ithmguo chaofeng l im e ilian colleg of comput scienc technolog xuchangunivers xuchang 461000 china abstract becaus of the design ofmallata lgorithm the phenomenon of frequenc alias exist in the signal decomposit process base on research and analysi ofmallat algorithm the reason thatmallat algorithm gener frequenc alias were found out and an improv modl that can elim inat effici frequenc aliasingwa given', - content_with_weight: - '关键词:小波分析;Mallat算法;频率混叠中图分类号: TN911. 6   文献标识码: A  文章编号: 10030972 (2007)04051104Reason andM eliorationModel of Generating Frequency A liasing of Ma llat A lgor ithmGUO Chaofeng, L IM e ilian(College of Computer Science &Technology, XuchangUniversity, Xuchang 461000, China)Abstract:Because of the design ofMallatA lgorithm, the phenomenon of frequency aliasing exists in the signal decomposition process. Based on research and analysis ofMallat algorithm, the reasons thatMallat algorithm generates frequency aliasing were found out, and an improved modle that can elim inate efficiently frequency aliasingwas given.', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-e79b07acbec9eb61', - similarity: 0.8294912664806687, - vector_similarity: 0.43163755493556244, - term_similarity: 1, - positions: [ - [1, 81, 528, 240, 251], - [1, 81, 528, 255, 266], - [1, 54, 501, 287, 302], - [1, 211, 657, 305, 317], - [1, 113, 560, 319, 331], - [1, 65, 512, 334, 395], - ], - doc_type_kwd: '', - }, - { - chunk_id: '138908de860b111c', - content_ltks: - '应用 技术 研究 mallat 算 率 混 叠 原因 及其 改进 模型 郭 超 峰 李 梅 莲 许昌 学院 计算机科学 与 技术 学院 河南 许昌 461000 摘 要 mallat 算法 由于 自身 设计 的 原因 在 信号 分解 过程 中 存在 频率 混 叠 现象 在 利用 小波 分析 进行 信号 提取 时 这种 现象 是 一个 不容忽视 的 问题 通过 分析 mallat 算法 找出 了 造成 mallat 算法 产生 频率 混 叠 的 原因 给出 了 一个 能 有效 消除 频率 混 叠 的 改进 算法 模型', - content_with_weight: - '·应用技术研究·Mallat算率混叠原因及其改进模型郭超峰,李梅莲(许昌学院计算机科学与技术学院,河南许昌461000)摘 要:Mallat算法由于自身设计的原因,在信号分解过程中,存在频率混叠现象在利用小波分析进行信号提取时,这种现象是一个不容忽视的问题. 通过分析Mallat算法,找出了造成Mallat算法产生频率混叠的原因,给出了一个能有效消除频率混叠的改进算法模型', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-138908de860b111c', - similarity: 0.827624678600734, - vector_similarity: 0.4254155953357798, - term_similarity: 1, - positions: [ - [1, 47, 471, 80, 92], - [1, 75, 500, 112, 138], - [1, 224, 649, 154, 168], - [1, 172, 596, 179, 190], - [1, 63, 487, 196, 237], - ], - doc_type_kwd: '', - }, - { - chunk_id: '77951868ce3d1994', - content_ltks: - 'f ig 4 two 1 d miensiona l pagoda decomposit process ofma llat a lgor ithm 重构 算法 2fshz j 表示 分解 的 深度 a j f t2 k h t 2k a j 1 f t 其中 j j 意义 与 式 2 相同 j j 1 j 20 h g 为 小波 重构 a j d j 意义 与 式 2 相同 mallat 二维 塔式 小波 变换 的 重构 过程 如 图 5 所示', - content_with_weight: - 'F ig. 4 Two 1-D miensiona l pagoda decomposition process ofMa llat a lgor ithm重构算法:2fsHz, J表示分解的深度A j [f (t)]=2{∑k h (t -2k)A j+1 [f (t)]+其中: j、J意义与式(2)相同, j =J -1, J -2, …, 0; h, g为小波重构; A j、D j 意义与式(2)相同Mallat二维塔式小波变换的重构过程如图5所示', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-77951868ce3d1994', - similarity: 0.8263513588843328, - vector_similarity: 0.42117119628110916, - term_similarity: 1, - positions: [ - [2, 284, 514, 754, 765], - [3, 285, 515, 79, 91], - [3, 57, 287, 95, 107], - [3, 38, 268, 126, 167], - ], - doc_type_kwd: '', - }, - { - chunk_id: 'b8076c8ba1598567', - content_ltks: - '1 mallat 算法 的 频率 混 叠 现象 对 最高 频率 为 的 带 限 信号 进行 离散 化 抽样 如果 抽样 周期 比较 大 或者说 抽样 频率 比较 小 那么 抽样 将 会 导致 频率 相邻 的 2 个 被 延拓 的 频谱 发生 叠加 而 互相 产生 影响 这种 现象 称为 混 叠 78 下面 是 一个 利用 mallat 算法 进行 信号 分解 的 例子 912', - content_with_weight: - '1 Mallat算法的频率混叠现象对最高频率为的带限信号进行离散化抽样,如果抽样周期比较大,或者说抽样频率比较小,那么抽样将会导致频率相邻的2个被延拓的频谱发生叠加而互相产生影响,这种现象称为混叠[78 ]下面是一个利用Mallat算法进行信号分解的例子[912 ]', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-b8076c8ba1598567', - similarity: 0.8260278445075276, - vector_similarity: 0.4200928150250923, - term_similarity: 1, - positions: [ - [1, 287, 516, 430, 441], - [1, 284, 513, 448, 520], - ], - doc_type_kwd: '', - }, - { - chunk_id: '537d27bca0af2c0e', - content_ltks: 'k g t 2k d j 1 f t', - content_with_weight: '∑k g (t -2k)D j+1 [f (t)]}, ', - doc_id: 'bf60855c41d911f09504047c16ec874f', - docnm_kwd: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - kb_id: 'fd05dba641bf11f0a713047c16ec874f', - important_kwd: [], - image_id: 'fd05dba641bf11f0a713047c16ec874f-537d27bca0af2c0e', - similarity: 0.8255620344768997, - vector_similarity: 0.4185401149229989, - term_similarity: 1, - positions: [[3, 93, 187, 112, 124]], - doc_type_kwd: 'image', - }, - ], - doc_aggs: [ - { - doc_name: 'Mallat算法频3333率混叠原因及其改进模型.pdf', - doc_id: 'bf60855c41d911f09504047c16ec874f', - count: 8, - }, - ], - }, - }, - component_id: 'Retrieval:ClearHornetsClap', - error: null, - elapsed_time: null, - created_at: 1749606806, - }, - }, - { - event: 'node_finished', - message_id: 'dce6c0c8466611f08e04047c16ec874f', - created_at: 1749606805, - task_id: 'db68eb0645ab11f0bbdc047c16ec874f', - data: { - inputs: {}, - outputs: { - content: null, - structured_output: null, - _elapsed_time: 0.009871692978776991, - }, - component_id: 'Agent:EvilBobcatsWish', - error: null, - elapsed_time: 0.009871692978776991, - created_at: 1749606806, - }, - }, - { - event: 'node_finished', - message_id: 'dce6c0c8466611f08e04047c16ec874f', - created_at: 1749606805, - task_id: 'db68eb0645ab11f0bbdc047c16ec874f', - data: { - inputs: {}, - outputs: { - content: - '您好,根据您提供的知识库内容,以下是关于Mallat算法的一些信息:\n\n1. **Mallat算法的定义**:\n Mallat算法是由Stéphane Mallat将计算机视觉领域的多分辨率分析思想引入到小波分析中推导出的小波分析快速算法。该算法利用与尺度函数和小波函数相对应的小波低通滤波器(记为H, h)和小波高通滤波器(记为G, g)对信号进行低通和高通滤波来实现。\n\n2. **频率混叠现象**:\n 在具体使用时,Mallat算法实现过程中不可避免地会产生频率混叠现象。这种现象是由于小波分解过程是隔点采样的过程,而重构过程是隔点插零的过程,这两个过程都可能产生频率混叠。\n\n3. **改进模型**:\n 通过对Mallat算法的深入研究,找到了造成频率混叠的原因,并提出了一个能有效消除频率混叠的改进模型。这种改进的模型被称为子带信号重构算法。具体来说,在利用Mallat算法对信号分解得到各尺度上的小波系数后,再按照重构的方法重构至所需的小波空间系数cdj,并用该重构系数代替相应尺度上得到的小波系数来分析该尺度上的信号,以较好地解决频率混叠所带来的影响。\n\n4. **应用技术研究**:\n Mallat算法由于自身设计的原因,在信号分解过程中存在频率混叠现象。通过分析Mallat算法找出了造成这种现象的原因,并给出了一个能有效消除这种现象的改进模型。\n\n这些信息提供了对Mallat算法及其相关问题和解决方案的基本理解。如果您有更具体的问题或需要进一步的信息,请随时告知!', - _elapsed_time: 0.0001981810200959444, - }, - component_id: 'Message:PurpleWordsBuy', - error: null, - elapsed_time: 0.0001981810200959444, - created_at: 1749606814, - }, - }, -]; diff --git a/web/src/pages/agent/log-sheet/tool-timeline-item.tsx b/web/src/pages/agent/log-sheet/tool-timeline-item.tsx index c79ba3a6d..ea7a0786e 100644 --- a/web/src/pages/agent/log-sheet/tool-timeline-item.tsx +++ b/web/src/pages/agent/log-sheet/tool-timeline-item.tsx @@ -14,12 +14,9 @@ import { import { cn } from '@/lib/utils'; import { isEmpty } from 'lodash'; import { Operator } from '../constant'; +import { JsonViewer } from '../form/components/json-viewer'; import OperatorIcon, { SVGIconMap } from '../operator-icon'; -import { - JsonViewer, - toLowerCaseStringAndDeleteChar, - typeMap, -} from './workflow-timeline'; +import { toLowerCaseStringAndDeleteChar, typeMap } from './workflow-timeline'; type IToolIcon = | Operator.ArXiv | Operator.GitHub diff --git a/web/src/pages/agent/log-sheet/workflow-timeline.tsx b/web/src/pages/agent/log-sheet/workflow-timeline.tsx index de24f6636..5fdb8c3cc 100644 --- a/web/src/pages/agent/log-sheet/workflow-timeline.tsx +++ b/web/src/pages/agent/log-sheet/workflow-timeline.tsx @@ -24,8 +24,8 @@ import { cn } from '@/lib/utils'; import { t } from 'i18next'; import { get, isEmpty, isEqual, uniqWith } from 'lodash'; import { useCallback, useEffect, useMemo } from 'react'; -import JsonView from 'react18-json-view'; import { Operator } from '../constant'; +import { JsonViewer } from '../form/components/json-viewer'; import { useCacheChatLog } from '../hooks/use-cache-chat-log'; import OperatorIcon from '../operator-icon'; import ToolTimelineItem from './tool-timeline-item'; @@ -37,25 +37,7 @@ type LogFlowTimelineProps = Pick< sendLoading: boolean; isShare?: boolean; }; -export function JsonViewer({ - data, - title, -}: { - data: Record; - title: string; -}) { - return ( -
-
{title}
- -
- ); -} + export const typeMap = { begin: t('flow.logTimeline.begin'), agent: t('flow.logTimeline.agent'), diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index 3221d8ca0..b6397754d 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -34,6 +34,7 @@ import { NodeHandleId, Operator, TypesWithArray, + WebhookSecurityAuthType, } from './constant'; import { BeginFormSchemaType } from './form/begin-form/schema'; import { DataOperationsFormSchemaType } from './form/data-operations-form'; @@ -353,13 +354,20 @@ function transformRequestSchemaToJsonschema( function transformBeginParams(params: BeginFormSchemaType) { if (params.mode === AgentDialogueMode.Webhook) { + const nextSecurity: Record = { + ...params.security, + ip_whitelist: params.security?.ip_whitelist.map((x) => x.value), + }; + if (params.security?.auth_type === WebhookSecurityAuthType.Jwt) { + nextSecurity.jwt = { + ...nextSecurity.jwt, + required_claims: nextSecurity.jwt?.required_claims.map((x) => x.value), + }; + } return { ...params, schema: transformRequestSchemaToJsonschema(params.schema), - security: { - ...params.security, - ip_whitelist: params.security?.ip_whitelist.map((x) => x.value), - }, + security: nextSecurity, }; } diff --git a/web/src/pages/agent/webhook-sheet/index.tsx b/web/src/pages/agent/webhook-sheet/index.tsx new file mode 100644 index 000000000..bfb5f3997 --- /dev/null +++ b/web/src/pages/agent/webhook-sheet/index.tsx @@ -0,0 +1,94 @@ +import { CopyToClipboardWithText } from '@/components/copy-to-clipboard'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useFetchWebhookTrace } from '@/hooks/use-agent-request'; +import { MessageEventType } from '@/hooks/use-send-message'; +import { IModalProps } from '@/interfaces/common'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'umi'; +import { BeginId } from '../constant'; +import { JsonViewer } from '../form/components/json-viewer'; +import { WorkFlowTimeline } from './timeline'; + +type RunSheetProps = IModalProps; + +enum WebhookTraceTabType { + Detail = 'detail', + Tracing = 'tracing', +} + +const WebhookSheet = ({ hideModal }: RunSheetProps) => { + const { t } = useTranslation(); + const { id } = useParams(); + const text = `${location.protocol}//${location.host}/api/v1/webhook_test/${id}`; + + const { data } = useFetchWebhookTrace(true); + + const firstInput = data?.events.find( + (event) => + event.event === MessageEventType.NodeFinished && + event.data.component_id === BeginId, + )?.data.inputs; + + const latestOutput = data?.events?.findLast( + (event) => + event.event === MessageEventType.NodeFinished && + event.data.component_id !== BeginId, + )?.data.outputs; + + return ( + + + + {t('flow.testRun')} + + +
+
Webhook URL:
+ +
+ +
+
+ {data?.finished ? 'SUCCESS' : 'RUNNING'} +
+
+ + + + Detail + + Tracing + + + + + + + + + + +
+
+ ); +}; + +export default WebhookSheet; diff --git a/web/src/pages/agent/webhook-sheet/timeline.tsx b/web/src/pages/agent/webhook-sheet/timeline.tsx new file mode 100644 index 000000000..a7dde455b --- /dev/null +++ b/web/src/pages/agent/webhook-sheet/timeline.tsx @@ -0,0 +1,337 @@ +import HighLightMarkdown from '@/components/highlight-markdown'; +import { + Timeline, + TimelineContent, + TimelineHeader, + TimelineIndicator, + TimelineItem, + TimelineSeparator, +} from '@/components/originui/timeline'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { useFetchMessageTrace } from '@/hooks/use-agent-request'; +import { + INodeData, + INodeEvent, + MessageEventType, +} from '@/hooks/use-send-message'; +import { ITraceData } from '@/interfaces/database/agent'; +import { cn } from '@/lib/utils'; +import { t } from 'i18next'; +import { get, isEmpty, isEqual, uniqWith } from 'lodash'; +import { useCallback, useEffect, useMemo } from 'react'; +import { Operator } from '../constant'; +import { JsonViewer } from '../form/components/json-viewer'; +import { useCacheChatLog } from '../hooks/use-cache-chat-log'; +import ToolTimelineItem from '../log-sheet/tool-timeline-item'; +import OperatorIcon from '../operator-icon'; +type LogFlowTimelineProps = Pick< + ReturnType, + 'currentEventListWithoutMessage' | 'currentMessageId' +> & { + canvasId?: string; + sendLoading: boolean; + isShare?: boolean; +}; + +export const typeMap = { + begin: t('flow.logTimeline.begin'), + agent: t('flow.logTimeline.agent'), + retrieval: t('flow.logTimeline.retrieval'), + message: t('flow.logTimeline.message'), + awaitResponse: t('flow.logTimeline.awaitResponse'), + switch: t('flow.logTimeline.switch'), + iteration: t('flow.logTimeline.iteration'), + categorize: t('flow.logTimeline.categorize'), + code: t('flow.logTimeline.code'), + textProcessing: t('flow.logTimeline.textProcessing'), + tavilySearch: t('flow.logTimeline.tavilySearch'), + tavilyExtract: t('flow.logTimeline.tavilyExtract'), + exeSQL: t('flow.logTimeline.exeSQL'), + google: t('flow.logTimeline.google'), + duckDuckGo: t('flow.logTimeline.google'), + wikipedia: t('flow.logTimeline.wikipedia'), + googleScholar: t('flow.logTimeline.googleScholar'), + arXiv: t('flow.logTimeline.googleScholar'), + pubMed: t('flow.logTimeline.googleScholar'), + gitHub: t('flow.logTimeline.gitHub'), + email: t('flow.logTimeline.email'), + httpRequest: t('flow.logTimeline.httpRequest'), + wenCai: t('flow.logTimeline.wenCai'), + yahooFinance: t('flow.logTimeline.yahooFinance'), + userFillUp: t('flow.logTimeline.userFillUp'), +}; +export const toLowerCaseStringAndDeleteChar = ( + str: string, + char: string = '_', +) => str.toLowerCase().replace(/ /g, '').replaceAll(char, ''); + +// Convert all keys in typeMap to lowercase and output the new typeMap +export const typeMapLowerCase = Object.fromEntries( + Object.entries(typeMap).map(([key, value]) => [ + toLowerCaseStringAndDeleteChar(key), + value, + ]), +); + +function getInputsOrOutputs( + nodeEventList: INodeData[], + field: 'inputs' | 'outputs', +) { + const inputsOrOutputs = nodeEventList.map((x) => get(x, field, {})); + + if (inputsOrOutputs.length < 2) { + return inputsOrOutputs[0] || {}; + } + + return uniqWith(inputsOrOutputs, isEqual); // TODO: Violence should not be used to +} +export const WorkFlowTimeline = ({ + currentEventListWithoutMessage, + currentMessageId, + canvasId, + sendLoading, + isShare, +}: LogFlowTimelineProps) => { + // const getNode = useGraphStore((state) => state.getNode); + + const { + data: traceData, + setMessageId, + setISStopFetchTrace, + } = useFetchMessageTrace(canvasId); + + useEffect(() => { + setMessageId(currentMessageId); + }, [currentMessageId, setMessageId]); + const getNodeName = (nodeId: string) => { + if ('begin' === nodeId) return t('flow.begin'); + return nodeId; + }; + + useEffect(() => { + setISStopFetchTrace(!sendLoading); + }, [sendLoading, setISStopFetchTrace]); + + const startedNodeList = useMemo(() => { + const finish = currentEventListWithoutMessage?.some( + (item) => item.event === MessageEventType.WorkflowFinished, + ); + setISStopFetchTrace(finish || !sendLoading); + const duplicateList = currentEventListWithoutMessage?.filter( + (x) => x.event === MessageEventType.NodeStarted, + ) as INodeEvent[]; + + // Remove duplicate nodes + return duplicateList?.reduce>((pre, cur) => { + if (pre.every((x) => x.data.component_id !== cur.data.component_id)) { + pre.push(cur); + } + return pre; + }, []); + }, [currentEventListWithoutMessage, sendLoading, setISStopFetchTrace]); + + const getElapsedTime = (nodeId: string) => { + if (nodeId === 'begin') { + return ''; + } + const data = currentEventListWithoutMessage?.find((x) => { + return ( + x.data?.component_id === nodeId && + x.event === MessageEventType.NodeFinished + ); + }); + if (!data || data?.data.elapsed_time < 0.000001) { + return ''; + } + return data?.data.elapsed_time || ''; + }; + + const hasTrace = useCallback( + (componentId: string) => { + if (Array.isArray(traceData)) { + return traceData?.some((x) => x.component_id === componentId); + } + return false; + }, + [traceData], + ); + + const filterTrace = useCallback( + (componentId: string) => { + const trace = traceData + ?.filter((x) => x.component_id === componentId) + .reduce((pre, cur) => { + pre.push(...cur.trace); + + return pre; + }, []); + return Array.isArray(trace) ? trace : [{}]; + }, + [traceData], + ); + + const filterFinishedNodeList = useCallback( + (componentId: string) => { + const nodeEventList = currentEventListWithoutMessage + .filter( + (x) => + x.event === MessageEventType.NodeFinished && + (x.data as INodeData)?.component_id === componentId, + ) + .map((x) => x.data); + + return nodeEventList; + }, + [currentEventListWithoutMessage], + ); + + return ( + + {startedNodeList?.map((x, idx) => { + const nodeDataList = filterFinishedNodeList(x.data.component_id); + const finishNodeIds = nodeDataList.map( + (x: INodeData) => x.component_id, + ); + const inputs = getInputsOrOutputs(nodeDataList, 'inputs'); + const outputs = getInputsOrOutputs(nodeDataList, 'outputs'); + const nodeLabel = x.data.component_type; + return ( + <> + + + + + +
+
+
+
+
+ +
+
+
+
+ +
+ + + +
+ + {!isShare && getNodeName(x.data?.component_name)} + {isShare && + (typeMapLowerCase[ + toLowerCaseStringAndDeleteChar( + nodeLabel, + ) as keyof typeof typeMap + ] ?? + nodeLabel)} + + + {getElapsedTime(x.data.component_id) + .toString() + .slice(0, 6)} + {getElapsedTime(x.data.component_id) ? 's' : ''} + + + Online + +
+
+ {!isShare && ( + +
+ {!isShare && ( + <> + + + + + )} +
+
+ )} + {isShare && x.data?.thoughts && ( + +
+
+ + {x.data.thoughts || ''} + +
+
+
+ )} +
+
+
+
+
+ {hasTrace(x.data.component_id) && ( + + )} + + ); + })} +
+ ); +}; diff --git a/web/src/services/agent-service.ts b/web/src/services/agent-service.ts index 2e95d4d1a..2dde15899 100644 --- a/web/src/services/agent-service.ts +++ b/web/src/services/agent-service.ts @@ -2,6 +2,7 @@ import { IAgentLogsRequest, IPipeLineListRequest, } from '@/interfaces/database/agent'; +import { IAgentWebhookTraceRequest } from '@/interfaces/request/agent'; import api from '@/utils/api'; import { registerNextServer } from '@/utils/register-server'; import request from '@/utils/request'; @@ -143,4 +144,11 @@ export const fetchPipeLineList = (params: IPipeLineListRequest) => { return request.get(api.listCanvas, { params: params }); }; +export const fetchWebhookTrace = ( + id: string, + params: IAgentWebhookTraceRequest, +) => { + return request.get(api.fetchWebhookTrace(id), { params: params }); +}; + export default agentService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 817817f36..17dc4e039 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -202,6 +202,9 @@ export default { prompt: `${api_host}/canvas/prompts`, cancelDataflow: (id: string) => `${api_host}/canvas/cancel/${id}`, downloadFile: `${api_host}/canvas/download`, + testWebhook: (id: string) => `${ExternalApi}${api_host}/webhook_test/${id}`, + fetchWebhookTrace: (id: string) => + `${ExternalApi}${api_host}/webhook_trace/${id}`, // mcp server listMcpServer: `${api_host}/mcp_server/list`,