diff --git a/packages/service/common/metrics/client.ts b/packages/service/common/metrics/client.ts index bb867d4b8b..c557ef6622 100644 --- a/packages/service/common/metrics/client.ts +++ b/packages/service/common/metrics/client.ts @@ -1,5 +1,10 @@ -import { configureMetricsFromEnv, disposeMetrics, getMeter } from '@fastgpt-sdk/otel/metrics'; +import { + configureMetricsFromEnv, + disposeMetrics as disposeOtelMetrics, + getMeter +} from '@fastgpt-sdk/otel/metrics'; import { env } from '../../env'; +import { startRuntimeMetrics, stopRuntimeMetrics } from './runtime'; export async function configureMetrics() { await configureMetricsFromEnv({ @@ -7,6 +12,13 @@ export async function configureMetrics() { defaultServiceName: 'fastgpt-client', defaultMeterName: 'fastgpt-client' }); + + startRuntimeMetrics(); } -export { disposeMetrics, getMeter }; +export async function disposeMetrics() { + stopRuntimeMetrics(); + await disposeOtelMetrics(); +} + +export { getMeter }; diff --git a/packages/service/common/metrics/runtime.ts b/packages/service/common/metrics/runtime.ts new file mode 100644 index 0000000000..75dba5cd30 --- /dev/null +++ b/packages/service/common/metrics/runtime.ts @@ -0,0 +1,98 @@ +import type { + BatchObservableCallback, + Meter, + Observable, + ObservableGauge +} from '@opentelemetry/api'; +import { getMeter } from '@fastgpt-sdk/otel/metrics'; + +type RuntimeMetricAttributes = Record; + +type RuntimeObservableSet = { + meter: Meter; + processMemoryRss: ObservableGauge; + processMemoryHeapUsed: ObservableGauge; + processMemoryHeapTotal: ObservableGauge; + processMemoryExternal: ObservableGauge; + processMemoryArrayBuffers: ObservableGauge; + processUptime: ObservableGauge; +}; + +const prefix = 'fastgpt.runtime.process'; + +let runtimeMetricsRegistered = false; +let runtimeMeter: Meter | undefined; +let runtimeObservables: Observable[] = []; +let runtimeMetricsCallback: BatchObservableCallback | undefined; + +function createRuntimeObservables(): RuntimeObservableSet { + const meter = getMeter('fastgpt.runtime'); + + return { + meter, + processMemoryRss: meter.createObservableGauge(`${prefix}.memory.rss`, { + description: 'Resident set size memory used by the current process', + unit: 'By' + }), + processMemoryHeapUsed: meter.createObservableGauge(`${prefix}.memory.heap_used`, { + description: 'V8 heap memory currently used by the current process', + unit: 'By' + }), + processMemoryHeapTotal: meter.createObservableGauge(`${prefix}.memory.heap_total`, { + description: 'Total V8 heap memory allocated for the current process', + unit: 'By' + }), + processMemoryExternal: meter.createObservableGauge(`${prefix}.memory.external`, { + description: 'Memory used by C++ objects bound to JavaScript objects', + unit: 'By' + }), + processMemoryArrayBuffers: meter.createObservableGauge(`${prefix}.memory.array_buffers`, { + description: 'Memory allocated for ArrayBuffer and SharedArrayBuffer instances', + unit: 'By' + }), + processUptime: meter.createObservableGauge(`${prefix}.uptime`, { + description: 'Process uptime', + unit: 's' + }) + }; +} + +export function startRuntimeMetrics() { + if (runtimeMetricsRegistered) return; + + const observables = createRuntimeObservables(); + runtimeMeter = observables.meter; + + runtimeObservables = [ + observables.processMemoryRss, + observables.processMemoryHeapUsed, + observables.processMemoryHeapTotal, + observables.processMemoryExternal, + observables.processMemoryArrayBuffers, + observables.processUptime + ]; + runtimeMetricsCallback = (result) => { + const memoryUsage = process.memoryUsage(); + + result.observe(observables.processMemoryRss, memoryUsage.rss); + result.observe(observables.processMemoryHeapUsed, memoryUsage.heapUsed); + result.observe(observables.processMemoryHeapTotal, memoryUsage.heapTotal); + result.observe(observables.processMemoryExternal, memoryUsage.external); + result.observe(observables.processMemoryArrayBuffers, memoryUsage.arrayBuffers); + result.observe(observables.processUptime, process.uptime()); + }; + + runtimeMeter.addBatchObservableCallback(runtimeMetricsCallback, runtimeObservables); + runtimeMetricsRegistered = true; +} + +export function stopRuntimeMetrics() { + if (!runtimeMetricsRegistered || !runtimeMetricsCallback || !runtimeMeter) return; + + runtimeMeter.removeBatchObservableCallback(runtimeMetricsCallback, runtimeObservables); + + runtimeMetricsRegistered = false; + runtimeMeter = undefined; + runtimeObservables = []; + runtimeMetricsCallback = undefined; +} diff --git a/packages/service/common/middle/entry.ts b/packages/service/common/middle/entry.ts index aab227be7c..6f71636c9c 100644 --- a/packages/service/common/middle/entry.ts +++ b/packages/service/common/middle/entry.ts @@ -13,6 +13,37 @@ export type NextApiHandler = ( res: NextApiResponse ) => unknown | Promise; +function isIdLikeRouteSegment(segment: string) { + return ( + /^\d{4,}$/.test(segment) || + /^[0-9a-f]{24}$/i.test(segment) || + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(segment) || + /^[A-Za-z0-9_-]{16,}$/.test(segment) + ); +} + +function normalizeRouteSegment(segment: string) { + return isIdLikeRouteSegment(segment) ? ':id' : segment; +} + +function parseHeaderNumber(value: string | string[] | undefined) { + const normalized = Array.isArray(value) ? value[0] : value; + if (!normalized) return undefined; + + const parsed = Number(normalized); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function getRequestRoute(url: string) { + const [route = '/'] = url.split('?'); + if (!route || route === '/') return '/'; + + return route + .split('/') + .map((segment) => normalizeRouteSegment(segment)) + .join('/'); +} + export const NextEntry = ({ beforeCallback = [] }: { @@ -28,23 +59,22 @@ export const NextEntry = ({ const responseLogger = getLogger(LogCategories.HTTP.RESPONSE); const url = req.url || ''; + const route = getRequestRoute(url); const method = req.method?.toUpperCase() || ''; const ip = req.headers['x-forwarded-for'] || req.socket?.remoteAddress; const userAgent = req.headers['user-agent']; const contentLength = req.headers['content-length']; + const requestBodySize = parseHeaderNumber(contentLength); return withContext({ requestId }, async () => withActiveSpan( { - name: `http.request ${method || 'UNKNOWN'} ${url || '/'}`, + name: 'http.request', tracerName: 'fastgpt.http', attributes: { - 'fastgpt.request.id': requestId, 'http.request.method': method, - 'url.full': url, - 'client.address': Array.isArray(ip) ? ip.join(',') : ip, - 'user_agent.original': userAgent, - 'http.request.body.size': contentLength + 'http.route': route, + 'http.request.body.size': requestBodySize } }, async (span) => { diff --git a/packages/service/common/tracing/client.ts b/packages/service/common/tracing/client.ts index 1ca91ec5c9..33d9661d38 100644 --- a/packages/service/common/tracing/client.ts +++ b/packages/service/common/tracing/client.ts @@ -29,6 +29,19 @@ export type ActiveSpanOptions = { attributes?: Record; }; +const DEFAULT_PRODUCTION_TRACING_SAMPLE_RATIO = 0.05; +const DEFAULT_NON_PRODUCTION_TRACING_SAMPLE_RATIO = 1; + +function getDefaultTracingSampleRatio() { + if (typeof env.TRACING_OTEL_SAMPLE_RATIO === 'number') { + return env.TRACING_OTEL_SAMPLE_RATIO; + } + + return process.env.NODE_ENV === 'production' + ? DEFAULT_PRODUCTION_TRACING_SAMPLE_RATIO + : DEFAULT_NON_PRODUCTION_TRACING_SAMPLE_RATIO; +} + function normalizeAttributes(attributes?: Record) { if (!attributes) return; @@ -51,7 +64,7 @@ export async function configureTracing() { env, defaultServiceName: 'fastgpt-client', defaultTracerName: 'fastgpt-client', - defaultSampleRatio: env.TRACING_OTEL_SAMPLE_RATIO + defaultSampleRatio: getDefaultTracingSampleRatio() }); } diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index adcfd8a3ff..c27573a273 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -1,5 +1,5 @@ import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { SpanStatusCode } from '@opentelemetry/api'; +import { SpanStatusCode, trace, type Span } from '@opentelemetry/api'; import type { AIChatItemValueItemType, ChatHistoryItemResType, @@ -63,13 +63,13 @@ import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { i18nT } from '../../../../web/i18n/utils'; import { validateFileUrlDomain } from '../../../common/security/fileUrlValidator'; import { classifyEdgesByDFS, findSCCs, isNodeInCycle, getEdgeType } from '../utils/tarjan'; -import { observeWorkflowStep } from '../metrics'; +import { observeWorkflowRun, observeWorkflowStep } from '../metrics'; import { withActiveSpan } from '../../../common/tracing'; - -const logger = getLogger(LogCategories.MODULE.WORKFLOW.DISPATCH); import { delAgentRuntimeStopSign, shouldWorkflowStop } from './workflowStatus'; import { runWithContext } from '../utils/context'; +const logger = getLogger(LogCategories.MODULE.WORKFLOW.DISPATCH); + type Props = Omit< ChatDispatchProps, 'checkIsStopping' | 'workflowDispatchDeep' | 'timezone' | 'externalProvider' @@ -85,6 +85,68 @@ type NodeResponseCompleteType = Omit & { [DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType; }; +type WorkflowObservedStepResult = { + node: RuntimeNodeItemType; + runStatus: 'run'; + result: NodeResponseCompleteType; +}; + +const tracedWorkflowStepTypes = new Set([ + FlowNodeTypeEnum.appModule, + FlowNodeTypeEnum.pluginModule, + FlowNodeTypeEnum.agent, + FlowNodeTypeEnum.chatNode, + FlowNodeTypeEnum.datasetSearchNode, + FlowNodeTypeEnum.classifyQuestion, + FlowNodeTypeEnum.contentExtract, + FlowNodeTypeEnum.queryExtension, + FlowNodeTypeEnum.toolCall, + FlowNodeTypeEnum.httpRequest468, + FlowNodeTypeEnum.lafModule, + FlowNodeTypeEnum.code, + FlowNodeTypeEnum.readFiles, + FlowNodeTypeEnum.tool +]); + +function shouldTraceWorkflowStep(nodeType: FlowNodeTypeEnum) { + return tracedWorkflowStepTypes.has(nodeType); +} + +function getWorkflowStepStatus(result: WorkflowObservedStepResult): 'ok' | 'error' { + return result.result[DispatchNodeResponseKeyEnum.nodeResponse]?.error ? 'error' : 'ok'; +} + +function addWorkflowStepEvent({ + eventName, + nodeType, + mode, + status, + durationMs +}: { + eventName: 'workflow.step.start' | 'workflow.step.end'; + nodeType: FlowNodeTypeEnum; + mode: string; + status?: 'ok' | 'error'; + durationMs?: number; +}) { + const activeSpan = trace.getActiveSpan(); + if (!activeSpan) return; + + const attributes: Record = { + 'fastgpt.workflow.node.type': nodeType, + 'fastgpt.workflow.mode': mode + }; + + if (status) { + attributes['fastgpt.workflow.step.status'] = status; + } + if (typeof durationMs === 'number') { + attributes['fastgpt.workflow.step.duration_ms'] = durationMs; + } + + activeSpan.addEvent(eventName, attributes); +} + // Run workflow type WorkflowUsageProps = RequireOnlyOne<{ usageSource: UsageSourceEnum; @@ -746,277 +808,315 @@ export class WorkflowQueue { runStatus: 'run'; result: NodeResponseCompleteType; }> { + const mode = this.isDebugMode ? 'test' : this.data.mode; const stepMetricAttributes = { - workflowId: this.data.runningAppInfo.id, - workflowName: this.data.runningAppInfo.name, - nodeId: node.nodeId, - nodeName: node.name, nodeType: node.flowNodeType, - mode: this.isDebugMode ? 'test' : this.data.mode + mode }; - return observeWorkflowStep(stepMetricAttributes, () => - withActiveSpan( - { - name: `workflow.step ${node.name || node.nodeId}`, - tracerName: 'fastgpt.workflow', - attributes: { - 'fastgpt.workflow.id': this.data.runningAppInfo.id, - 'fastgpt.workflow.name': this.data.runningAppInfo.name, - 'fastgpt.workflow.node.id': node.nodeId, - 'fastgpt.workflow.node.name': node.name, - 'fastgpt.workflow.node.type': node.flowNodeType, - 'fastgpt.workflow.mode': stepMetricAttributes.mode - } - }, - async (stepSpan) => { - /* Inject data into module input */ - const getNodeRunParams = (node: RuntimeNodeItemType) => { - if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) { - // Format plugin input to object - return node.inputs.reduce>((acc, item) => { - acc[item.key] = valueTypeFormat(item.value, item.valueType); - return acc; - }, {}); + const executeNode = async (stepSpan?: Span): Promise => { + /* Inject data into module input */ + const getNodeRunParams = (node: RuntimeNodeItemType) => { + if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) { + // Format plugin input to object + return node.inputs.reduce>((acc, item) => { + acc[item.key] = valueTypeFormat(item.value, item.valueType); + return acc; + }, {}); + } + + // Dynamic input need to store a key. + const dynamicInput = node.inputs.find( + (item) => item.renderTypeList[0] === FlowNodeInputTypeEnum.addInputParam + ); + const params: Record = dynamicInput + ? { + [dynamicInput.key]: {} } + : {}; - // Dynamic input need to store a key. - const dynamicInput = node.inputs.find( - (item) => item.renderTypeList[0] === FlowNodeInputTypeEnum.addInputParam - ); - const params: Record = dynamicInput - ? { - [dynamicInput.key]: {} - } - : {}; + node.inputs.forEach((input) => { + // Special input, not format + if (input.key === dynamicInput?.key) return; - node.inputs.forEach((input) => { - // Special input, not format - if (input.key === dynamicInput?.key) return; - - // Skip some special key - if ( - [NodeInputKeyEnum.childrenNodeIdList, NodeInputKeyEnum.httpJsonBody].includes( - input.key as NodeInputKeyEnum - ) - ) { - params[input.key] = input.value; - return; - } - - // replace {{$xx.xx$}} and {{xx}} variables - let value = replaceEditorVariable({ - text: input.value, - nodes: this.data.runtimeNodes, - variables: this.data.variables - }); - - // replace reference variables - value = getReferenceVariableValue({ - value, - nodes: this.data.runtimeNodes, - variables: this.data.variables - }); - - // Dynamic input is stored in the dynamic key - if (input.canEdit && dynamicInput && params[dynamicInput.key]) { - params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType); - } - params[input.key] = valueTypeFormat(value, input.valueType); - }); - - return params; - }; - - // push run status messages - if (node.showStatus && !this.data.isToolCall) { - this.data.workflowStreamResponse?.({ - event: SseResponseEventEnum.flowNodeStatus, - data: { - status: 'running', - name: node.name - } - }); + // Skip some special key + if ( + [NodeInputKeyEnum.childrenNodeIdList, NodeInputKeyEnum.httpJsonBody].includes( + input.key as NodeInputKeyEnum + ) + ) { + params[input.key] = input.value; + return; } - const startTime = Date.now(); - // get node running params - const params = getNodeRunParams(node); + // replace {{$xx.xx$}} and {{xx}} variables + let value = replaceEditorVariable({ + text: input.value, + nodes: this.data.runtimeNodes, + variables: this.data.variables + }); - const dispatchData: ModuleDispatchProps> = { - ...this.data, - usagePush: this.usagePush.bind(this), - lastInteractive: this.data.lastInteractive?.entryNodeIds?.includes(node.nodeId) - ? this.data.lastInteractive - : undefined, - variables: this.data.variables, - histories: this.data.histories, - retainDatasetCite: this.data.retainDatasetCite, - node, - runtimeNodes: this.data.runtimeNodes, - runtimeEdges: this.data.runtimeEdges, - params, - mode: this.isDebugMode ? 'test' : this.data.mode - }; + // replace reference variables + value = getReferenceVariableValue({ + value, + nodes: this.data.runtimeNodes, + variables: this.data.variables + }); - // run module - const dispatchRes: NodeResponseType = await (async () => { - if (callbackMap[node.flowNodeType]) { - const targetEdges = this.edgeIndex.bySource.get(node.nodeId) || []; - const errorHandleId = getHandleId(node.nodeId, 'source_catch', 'right'); + // Dynamic input is stored in the dynamic key + if (input.canEdit && dynamicInput && params[dynamicInput.key]) { + params[dynamicInput.key][input.key] = valueTypeFormat(value, input.valueType); + } + params[input.key] = valueTypeFormat(value, input.valueType); + }); - try { - const result = (await callbackMap[node.flowNodeType]( - dispatchData - )) as NodeResponseType; + return params; + }; - if (result.error) { - // Run error and not catch error, skip all edges - if (!node.catchError) { - return { - ...result, - [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map( - (item) => item.sourceHandle - ) - }; - } + // push run status messages + if (node.showStatus && !this.data.isToolCall) { + this.data.workflowStreamResponse?.({ + event: SseResponseEventEnum.flowNodeStatus, + data: { + status: 'running', + name: node.name + } + }); + } + const startTime = Date.now(); - // Catch error, skip unError handle - const skipHandleIds = targetEdges - .filter((item) => item.sourceHandle !== errorHandleId) - .map((item) => item.sourceHandle); + // get node running params + const params = getNodeRunParams(node); - return { - ...result, - [DispatchNodeResponseKeyEnum.skipHandleId]: result[ - DispatchNodeResponseKeyEnum.skipHandleId - ] - ? [ - ...result[DispatchNodeResponseKeyEnum.skipHandleId], - ...skipHandleIds - ].filter(Boolean) - : skipHandleIds - }; - } + const dispatchData: ModuleDispatchProps> = { + ...this.data, + usagePush: this.usagePush.bind(this), + lastInteractive: this.data.lastInteractive?.entryNodeIds?.includes(node.nodeId) + ? this.data.lastInteractive + : undefined, + variables: this.data.variables, + histories: this.data.histories, + retainDatasetCite: this.data.retainDatasetCite, + node, + runtimeNodes: this.data.runtimeNodes, + runtimeEdges: this.data.runtimeEdges, + params, + mode + }; - // Not error - const errorHandle = - targetEdges.find((item) => item.sourceHandle === errorHandleId)?.sourceHandle || - ''; + // run module + const dispatchRes: NodeResponseType = await (async () => { + if (callbackMap[node.flowNodeType]) { + const targetEdges = this.edgeIndex.bySource.get(node.nodeId) || []; + const errorHandleId = getHandleId(node.nodeId, 'source_catch', 'right'); + try { + const result = (await callbackMap[node.flowNodeType](dispatchData)) as NodeResponseType; + + if (result.error) { + // Run error and not catch error, skip all edges + if (!node.catchError) { return { ...result, - [DispatchNodeResponseKeyEnum.skipHandleId]: (result[ - DispatchNodeResponseKeyEnum.skipHandleId - ] - ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], errorHandle] - : [errorHandle] - ).filter(Boolean) - }; - } catch (error) { - // Skip all edges and return error - let skipHandleId = targetEdges.map((item) => item.sourceHandle); - if (node.catchError) { - skipHandleId = skipHandleId.filter((item) => item !== errorHandleId); - } - - return { - [DispatchNodeResponseKeyEnum.nodeResponse]: { - error: getErrText(error) - }, - [DispatchNodeResponseKeyEnum.skipHandleId]: skipHandleId + [DispatchNodeResponseKeyEnum.skipHandleId]: targetEdges.map( + (item) => item.sourceHandle + ) }; } + + // Catch error, skip unError handle + const skipHandleIds = targetEdges + .filter((item) => item.sourceHandle !== errorHandleId) + .map((item) => item.sourceHandle); + + return { + ...result, + [DispatchNodeResponseKeyEnum.skipHandleId]: result[ + DispatchNodeResponseKeyEnum.skipHandleId + ] + ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], ...skipHandleIds].filter( + Boolean + ) + : skipHandleIds + }; } - return {}; - })(); - const nodeResponses = dispatchRes[DispatchNodeResponseKeyEnum.nodeResponses] || []; - // format response data. Add modulename and module type - const formatResponseData: NodeResponseCompleteType['responseData'] = (() => { - if (!dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]) return undefined; + // Not error + const errorHandle = + targetEdges.find((item) => item.sourceHandle === errorHandleId)?.sourceHandle || ''; - const val = { - moduleName: node.name, - moduleType: node.flowNodeType, - moduleLogo: node.avatar, - ...dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse], - id: getNanoid(), - nodeId: node.nodeId, - runningTime: +((Date.now() - startTime) / 1000).toFixed(2) + return { + ...result, + [DispatchNodeResponseKeyEnum.skipHandleId]: (result[ + DispatchNodeResponseKeyEnum.skipHandleId + ] + ? [...result[DispatchNodeResponseKeyEnum.skipHandleId], errorHandle] + : [errorHandle] + ).filter(Boolean) }; - nodeResponses.push(val); - return val; - })(); + } catch (error) { + // Skip all edges and return error + let skipHandleId = targetEdges.map((item) => item.sourceHandle); + if (node.catchError) { + skipHandleId = skipHandleId.filter((item) => item !== errorHandleId); + } - // Response node response - if ( - this.data.apiVersion === 'v2' && - !this.data.isToolCall && - this.isRootRuntime && - nodeResponses.length > 0 - ) { - const filteredResponses = this.data.responseAllData - ? nodeResponses - : filterPublicNodeResponseData({ - nodeRespones: nodeResponses, - responseDetail: this.data.responseDetail - }); - - filteredResponses.forEach((item) => { - this.data.workflowStreamResponse?.({ - event: SseResponseEventEnum.flowNodeResponse, - data: item - }); - }); - } - - // Add output default value - if (dispatchRes.data) { - node.outputs.forEach((item) => { - if (!item.required) return; - if (dispatchRes.data?.[item.key] !== undefined) return; - dispatchRes.data![item.key] = valueTypeFormat(item.defaultValue, item.valueType); - }); - } - - // Update new variables - if (dispatchRes[DispatchNodeResponseKeyEnum.newVariables]) { - this.data.variables = { - ...this.data.variables, - ...dispatchRes[DispatchNodeResponseKeyEnum.newVariables] + return { + [DispatchNodeResponseKeyEnum.nodeResponse]: { + error: getErrText(error) + }, + [DispatchNodeResponseKeyEnum.skipHandleId]: skipHandleId }; } - - // Error - if (dispatchRes?.responseData?.error) { - stepSpan.setAttribute('fastgpt.workflow.step.error', true); - stepSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: String(dispatchRes.responseData.error) - }); - logger.warn('Workflow node returned error', { error: dispatchRes.responseData.error }); - } else { - stepSpan.setStatus({ code: SpanStatusCode.OK }); - } - - if (formatResponseData?.runningTime !== undefined) { - stepSpan.setAttribute( - 'fastgpt.workflow.step.running_time_seconds', - formatResponseData.runningTime - ); - } - - return { - node, - runStatus: 'run', - result: { - ...dispatchRes, - [DispatchNodeResponseKeyEnum.nodeResponse]: formatResponseData - } - }; } - ) + return {}; + })(); + + const nodeResponses = dispatchRes[DispatchNodeResponseKeyEnum.nodeResponses] || []; + // format response data. Add modulename and module type + const formatResponseData: NodeResponseCompleteType['responseData'] = (() => { + if (!dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]) return undefined; + + const val = { + moduleName: node.name, + moduleType: node.flowNodeType, + moduleLogo: node.avatar, + ...dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse], + id: getNanoid(), + nodeId: node.nodeId, + runningTime: +((Date.now() - startTime) / 1000).toFixed(2) + }; + nodeResponses.push(val); + return val; + })(); + + // Response node response + if ( + this.data.apiVersion === 'v2' && + !this.data.isToolCall && + this.isRootRuntime && + nodeResponses.length > 0 + ) { + const filteredResponses = this.data.responseAllData + ? nodeResponses + : filterPublicNodeResponseData({ + nodeRespones: nodeResponses, + responseDetail: this.data.responseDetail + }); + + filteredResponses.forEach((item) => { + this.data.workflowStreamResponse?.({ + event: SseResponseEventEnum.flowNodeResponse, + data: item + }); + }); + } + + // Add output default value + if (dispatchRes.data) { + node.outputs.forEach((item) => { + if (!item.required) return; + if (dispatchRes.data?.[item.key] !== undefined) return; + dispatchRes.data![item.key] = valueTypeFormat(item.defaultValue, item.valueType); + }); + } + + // Update new variables + if (dispatchRes[DispatchNodeResponseKeyEnum.newVariables]) { + this.data.variables = { + ...this.data.variables, + ...dispatchRes[DispatchNodeResponseKeyEnum.newVariables] + }; + } + + // Error + if (dispatchRes?.responseData?.error) { + if (stepSpan) { + stepSpan.setAttribute('fastgpt.workflow.step.error', true); + stepSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: String(dispatchRes.responseData.error) + }); + } + logger.warn('Workflow node returned error', { error: dispatchRes.responseData.error }); + } else if (stepSpan) { + stepSpan.setStatus({ code: SpanStatusCode.OK }); + } + + if (stepSpan && formatResponseData?.runningTime !== undefined) { + stepSpan.setAttribute( + 'fastgpt.workflow.step.running_time_seconds', + formatResponseData.runningTime + ); + } + + return { + node, + runStatus: 'run', + result: { + ...dispatchRes, + [DispatchNodeResponseKeyEnum.nodeResponse]: formatResponseData + } + }; + }; + + if (shouldTraceWorkflowStep(node.flowNodeType)) { + return observeWorkflowStep( + stepMetricAttributes, + () => + withActiveSpan( + { + name: 'workflow.step', + tracerName: 'fastgpt.workflow', + attributes: { + 'fastgpt.workflow.node.type': node.flowNodeType, + 'fastgpt.workflow.mode': mode + } + }, + async (stepSpan) => executeNode(stepSpan) + ), + { + getStatus: getWorkflowStepStatus + } + ); + } + + return observeWorkflowStep( + stepMetricAttributes, + async () => { + const stepStartedAt = Date.now(); + addWorkflowStepEvent({ + eventName: 'workflow.step.start', + nodeType: node.flowNodeType, + mode + }); + + try { + const result = await executeNode(); + + addWorkflowStepEvent({ + eventName: 'workflow.step.end', + nodeType: node.flowNodeType, + mode, + status: getWorkflowStepStatus(result), + durationMs: Date.now() - stepStartedAt + }); + + return result; + } catch (error) { + addWorkflowStepEvent({ + eventName: 'workflow.step.end', + nodeType: node.flowNodeType, + mode, + status: 'error', + durationMs: Date.now() - stepStartedAt + }); + throw error; + } + }, + { + getStatus: getWorkflowStepStatus + } ); } private nodeRunWithSkip(node: RuntimeNodeItemType): { @@ -1426,122 +1526,129 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise { - const startTime = Date.now(); - - await rewriteRuntimeWorkFlow({ - nodes: data.runtimeNodes, - edges: data.runtimeEdges, - lang: data.lang - }); - // Init default value - data.retainDatasetCite = data.retainDatasetCite ?? true; - data.responseDetail = data.responseDetail ?? true; - data.responseAllData = data.responseAllData ?? true; - - // Start process width initInput - const entryNodes = data.runtimeNodes.filter((item) => item.isEntry); - // Reset entry - data.runtimeNodes.forEach((item) => { - // Interactively nodes will use the "isEntry", which does not need to be updated - if ( - item.flowNodeType !== FlowNodeTypeEnum.userSelect && - item.flowNodeType !== FlowNodeTypeEnum.formInput && - item.flowNodeType !== FlowNodeTypeEnum.toolCall - ) { - item.isEntry = false; - } - }); - - const workflowQueue = await new Promise((resolve) => { - logger.info('Workflow run start', { - maxRunTimes: data.maxRunTimes, - appId: data.runningAppInfo.id - }); - const workflowQueue = new WorkflowQueue({ - data, - resolve, - defaultSkipNodeQueue: data.lastInteractive?.skipNodeQueue || data.defaultSkipNodeQueue - }); - - entryNodes.forEach((node) => { - workflowQueue.addActiveNode(node.nodeId); - }); - }); - - // Get interactive node response. - const interactiveResult = (() => { - if (workflowQueue.nodeInteractiveResponse) { - const interactiveAssistant = workflowQueue.handleInteractiveResult({ - entryNodeIds: workflowQueue.nodeInteractiveResponse.entryNodeIds, - interactiveResponse: workflowQueue.nodeInteractiveResponse.interactiveResponse - }); - if (workflowQueue.isRootRuntime) { - workflowQueue.chatAssistantResponse.push(interactiveAssistant); + () => + withActiveSpan( + { + name: isRootRuntime ? 'workflow.run' : 'workflow.child.run', + tracerName: 'fastgpt.workflow', + attributes: { + 'fastgpt.workflow.mode': data.mode, + 'fastgpt.workflow.depth': data.workflowDispatchDeep, + 'fastgpt.workflow.is_root': isRootRuntime, + 'fastgpt.workflow.app_version': data.apiVersion, + 'fastgpt.workflow.is_tool_call': !!data.isToolCall, + 'fastgpt.workflow.node_count': data.runtimeNodes.length, + 'fastgpt.workflow.edge_count': data.runtimeEdges.length } - return interactiveAssistant.interactive; + }, + async (workflowSpan) => { + const startTime = Date.now(); + + await rewriteRuntimeWorkFlow({ + nodes: data.runtimeNodes, + edges: data.runtimeEdges, + lang: data.lang + }); + // Init default value + data.retainDatasetCite = data.retainDatasetCite ?? true; + data.responseDetail = data.responseDetail ?? true; + data.responseAllData = data.responseAllData ?? true; + + // Start process width initInput + const entryNodes = data.runtimeNodes.filter((item) => item.isEntry); + // Reset entry + data.runtimeNodes.forEach((item) => { + // Interactively nodes will use the "isEntry", which does not need to be updated + if ( + item.flowNodeType !== FlowNodeTypeEnum.userSelect && + item.flowNodeType !== FlowNodeTypeEnum.formInput && + item.flowNodeType !== FlowNodeTypeEnum.toolCall + ) { + item.isEntry = false; + } + }); + + const workflowQueue = await new Promise((resolve) => { + logger.info('Workflow run start', { + maxRunTimes: data.maxRunTimes, + appId: data.runningAppInfo.id + }); + const workflowQueue = new WorkflowQueue({ + data, + resolve, + defaultSkipNodeQueue: data.lastInteractive?.skipNodeQueue || data.defaultSkipNodeQueue + }); + + entryNodes.forEach((node) => { + workflowQueue.addActiveNode(node.nodeId); + }); + }); + + // Get interactive node response. + const interactiveResult = (() => { + if (workflowQueue.nodeInteractiveResponse) { + const interactiveAssistant = workflowQueue.handleInteractiveResult({ + entryNodeIds: workflowQueue.nodeInteractiveResponse.entryNodeIds, + interactiveResponse: workflowQueue.nodeInteractiveResponse.interactiveResponse + }); + if (workflowQueue.isRootRuntime) { + workflowQueue.chatAssistantResponse.push(interactiveAssistant); + } + return interactiveAssistant.interactive; + } + })(); + + const durationSeconds = +((Date.now() - startTime) / 1000).toFixed(2); + + workflowSpan.setAttribute('fastgpt.workflow.duration_seconds', durationSeconds); + workflowSpan.setAttribute('fastgpt.workflow.run_times', workflowQueue.workflowRunTimes); + workflowSpan.setAttribute( + 'fastgpt.workflow.has_interactive_response', + !!workflowQueue.nodeInteractiveResponse + ); + workflowSpan.setStatus({ code: SpanStatusCode.OK }); + + if (isRootRuntime) { + data.workflowStreamResponse?.({ + event: SseResponseEventEnum.workflowDuration, + data: { durationSeconds } + }); + } + + return { + flowResponses: workflowQueue.chatResponses, + flowUsages: workflowQueue.chatNodeUsages, + debugResponse: workflowQueue.getDebugResponse(), + workflowInteractiveResponse: interactiveResult, + [DispatchNodeResponseKeyEnum.runTimes]: workflowQueue.workflowRunTimes, + [DispatchNodeResponseKeyEnum.assistantResponses]: mergeAssistantResponseAnswerText( + workflowQueue.chatAssistantResponse + ), + [DispatchNodeResponseKeyEnum.toolResponses]: workflowQueue.toolRunResponse, + [DispatchNodeResponseKeyEnum.newVariables]: runtimeSystemVar2StoreType({ + variables: data.variables, + removeObj: data.externalProvider.externalWorkflowVariables, + userVariablesConfigs: data.chatConfig?.variables + }), + [DispatchNodeResponseKeyEnum.memories]: + Object.keys(workflowQueue.system_memories).length > 0 + ? workflowQueue.system_memories + : undefined, + [DispatchNodeResponseKeyEnum.customFeedbacks]: + workflowQueue.customFeedbackList.length > 0 + ? workflowQueue.customFeedbackList + : undefined, + durationSeconds + }; } - })(); - - const durationSeconds = +((Date.now() - startTime) / 1000).toFixed(2); - - workflowSpan.setAttribute('fastgpt.workflow.duration_seconds', durationSeconds); - workflowSpan.setAttribute('fastgpt.workflow.run_times', workflowQueue.workflowRunTimes); - workflowSpan.setAttribute( - 'fastgpt.workflow.has_interactive_response', - !!workflowQueue.nodeInteractiveResponse - ); - workflowSpan.setStatus({ code: SpanStatusCode.OK }); - - if (isRootRuntime) { - data.workflowStreamResponse?.({ - event: SseResponseEventEnum.workflowDuration, - data: { durationSeconds } - }); - } - - return { - flowResponses: workflowQueue.chatResponses, - flowUsages: workflowQueue.chatNodeUsages, - debugResponse: workflowQueue.getDebugResponse(), - workflowInteractiveResponse: interactiveResult, - [DispatchNodeResponseKeyEnum.runTimes]: workflowQueue.workflowRunTimes, - [DispatchNodeResponseKeyEnum.assistantResponses]: mergeAssistantResponseAnswerText( - workflowQueue.chatAssistantResponse - ), - [DispatchNodeResponseKeyEnum.toolResponses]: workflowQueue.toolRunResponse, - [DispatchNodeResponseKeyEnum.newVariables]: runtimeSystemVar2StoreType({ - variables: data.variables, - removeObj: data.externalProvider.externalWorkflowVariables, - userVariablesConfigs: data.chatConfig?.variables - }), - [DispatchNodeResponseKeyEnum.memories]: - Object.keys(workflowQueue.system_memories).length > 0 - ? workflowQueue.system_memories - : undefined, - [DispatchNodeResponseKeyEnum.customFeedbacks]: - workflowQueue.customFeedbackList.length > 0 - ? workflowQueue.customFeedbackList - : undefined, - durationSeconds - }; + ), + { + getRunTimes: (result) => result[DispatchNodeResponseKeyEnum.runTimes] } ); }; diff --git a/packages/service/core/workflow/metrics.ts b/packages/service/core/workflow/metrics.ts index c9898d72d3..38cdbaf235 100644 --- a/packages/service/core/workflow/metrics.ts +++ b/packages/service/core/workflow/metrics.ts @@ -2,30 +2,28 @@ import { getMeter } from '../../common/metrics'; type MetricAttributeValue = string | number | boolean; type MetricAttributes = Record; +type ObservationStatus = 'ok' | 'error'; + +type ObservationState = { + startedAt: bigint; +}; + +type ObserveMetricOptions = { + getStatus?: (result: T) => ObservationStatus; +}; + +export type WorkflowRunMetricAttributes = { + mode?: string; + isRoot?: boolean; +}; export type WorkflowStepMetricAttributes = { - workflowId?: string; - workflowName?: string; - nodeId: string; - nodeName?: string; nodeType: string; mode?: string; }; -type ProcessSnapshot = { - rss: number; - heapUsed: number; - external: number; - arrayBuffers: number; - cpuUser: number; - cpuSystem: number; -}; - -type StepObservationState = { - startedAt: bigint; - startSnapshot: ProcessSnapshot; - hadOverlapAtStart: boolean; - overlapVersionAtStart: number; +type ObserveWorkflowRunOptions = ObserveMetricOptions & { + getRunTimes?: (result: T) => number | undefined; }; function normalizeAttributes(attributes: Record): MetricAttributes { @@ -42,200 +40,129 @@ function normalizeAttributes(attributes: Record): MetricAttribu return normalized; } -function toMetricAttributes( +function toRunMetricAttributes( + attributes: WorkflowRunMetricAttributes, + extras?: Record +) { + return normalizeAttributes({ + mode: attributes.mode, + is_root: attributes.isRoot, + ...extras + }); +} + +function toStepMetricAttributes( attributes: WorkflowStepMetricAttributes, extras?: Record ) { return normalizeAttributes({ - workflow_id: attributes.workflowId, - workflow_name: attributes.workflowName, - node_id: attributes.nodeId, - node_name: attributes.nodeName, node_type: attributes.nodeType, mode: attributes.mode, ...extras }); } -function takeProcessSnapshot(): ProcessSnapshot { - const memory = process.memoryUsage(); - const cpu = process.cpuUsage(); - +function beginObservation(): ObservationState { return { - rss: memory.rss, - heapUsed: memory.heapUsed, - external: memory.external, - arrayBuffers: memory.arrayBuffers, - cpuUser: cpu.user, - cpuSystem: cpu.system + startedAt: process.hrtime.bigint() }; } -let activeWorkflowStepCount = 0; -let overlapVersion = 0; +function getObservationDurationMs(state: ObservationState) { + return Number(process.hrtime.bigint() - state.startedAt) / 1_000_000; +} -function beginStepObservation(): StepObservationState { - const state: StepObservationState = { - startedAt: process.hrtime.bigint(), - startSnapshot: takeProcessSnapshot(), - hadOverlapAtStart: activeWorkflowStepCount > 0, - overlapVersionAtStart: overlapVersion - }; +async function observeOperation({ + fn, + onStart, + onFinish, + options +}: { + fn: () => Promise | T; + onStart?: () => void; + onFinish: (status: ObservationStatus, result: T | undefined, state: ObservationState) => void; + options?: ObserveMetricOptions; +}): Promise { + const observationState = beginObservation(); + onStart?.(); - activeWorkflowStepCount += 1; - - if (activeWorkflowStepCount > 1) { - overlapVersion += 1; + try { + const result = await fn(); + const status = options?.getStatus?.(result) ?? 'ok'; + onFinish(status, result, observationState); + return result; + } catch (error) { + onFinish('error', undefined, observationState); + throw error; } - - return state; } const meter = getMeter('fastgpt.workflow'); const prefix = 'fastgpt.workflow'; +const runDuration = meter.createHistogram(`${prefix}.run.duration`, { + description: 'Workflow run duration', + unit: 'ms' +}); +const runExecutions = meter.createCounter(`${prefix}.run.count`, { + description: 'Workflow run count' +}); +const runActive = meter.createUpDownCounter(`${prefix}.run.active`, { + description: 'Workflow runs currently executing' +}); +const runTimes = meter.createHistogram(`${prefix}.run.run_times`, { + description: 'Workflow total run times before completion' +}); const stepDuration = meter.createHistogram(`${prefix}.step.duration`, { description: 'Workflow step execution duration', unit: 'ms' }); -const stepExecutions = meter.createCounter(`${prefix}.step.executions`, { +const stepExecutions = meter.createCounter(`${prefix}.step.count`, { description: 'Workflow step execution count' }); -const stepActive = meter.createUpDownCounter(`${prefix}.step.active`, { - description: 'Workflow steps currently executing' -}); -const stepCpuUserTime = meter.createHistogram(`${prefix}.step.cpu.user_time`, { - description: 'Workflow step user CPU time', - unit: 'us' -}); -const stepCpuSystemTime = meter.createHistogram(`${prefix}.step.cpu.system_time`, { - description: 'Workflow step system CPU time', - unit: 'us' -}); -const stepMemoryRssStart = meter.createHistogram(`${prefix}.step.memory.rss_start`, { - description: 'Workflow process RSS memory snapshot at step start', - unit: 'By' -}); -const stepMemoryHeapUsedStart = meter.createHistogram(`${prefix}.step.memory.heap_used_start`, { - description: 'Workflow process heap used memory snapshot at step start', - unit: 'By' -}); -const stepMemoryExternalStart = meter.createHistogram(`${prefix}.step.memory.external_start`, { - description: 'Workflow process external memory snapshot at step start', - unit: 'By' -}); -const stepMemoryArrayBuffersStart = meter.createHistogram( - `${prefix}.step.memory.array_buffers_start`, - { - description: 'Workflow process array buffer memory snapshot at step start', - unit: 'By' - } -); -const stepMemoryRss = meter.createHistogram(`${prefix}.step.memory.rss`, { - description: 'Workflow process RSS memory snapshot at step end', - unit: 'By' -}); -const stepMemoryHeapUsed = meter.createHistogram(`${prefix}.step.memory.heap_used`, { - description: 'Workflow process heap used memory snapshot at step end', - unit: 'By' -}); -const stepMemoryExternal = meter.createHistogram(`${prefix}.step.memory.external`, { - description: 'Workflow process external memory snapshot at step end', - unit: 'By' -}); -const stepMemoryArrayBuffers = meter.createHistogram(`${prefix}.step.memory.array_buffers`, { - description: 'Workflow process array buffer memory snapshot at step end', - unit: 'By' -}); -const stepMemoryRssGrowth = meter.createHistogram(`${prefix}.step.memory.rss_growth`, { - description: 'Workflow process RSS memory growth during non-overlapping step execution', - unit: 'By' -}); -const stepMemoryHeapUsedGrowth = meter.createHistogram(`${prefix}.step.memory.heap_used_growth`, { - description: 'Workflow process heap used memory growth during non-overlapping step execution', - unit: 'By' -}); -const stepMemoryExternalGrowth = meter.createHistogram(`${prefix}.step.memory.external_growth`, { - description: 'Workflow process external memory growth during non-overlapping step execution', - unit: 'By' -}); + +export async function observeWorkflowRun( + attributes: WorkflowRunMetricAttributes, + fn: () => Promise | T, + options?: ObserveWorkflowRunOptions +): Promise { + const baseAttributes = toRunMetricAttributes(attributes); + + return observeOperation({ + fn, + options, + onStart: () => { + runActive.add(1, baseAttributes); + }, + onFinish: (status, result, state) => { + const metricAttributes = toRunMetricAttributes(attributes, { status }); + + runDuration.record(getObservationDurationMs(state), metricAttributes); + runExecutions.add(1, metricAttributes); + + const workflowRunTimes = result ? options?.getRunTimes?.(result) : undefined; + if (typeof workflowRunTimes === 'number' && Number.isFinite(workflowRunTimes)) { + runTimes.record(workflowRunTimes, metricAttributes); + } + + runActive.add(-1, baseAttributes); + } + }); +} export async function observeWorkflowStep( attributes: WorkflowStepMetricAttributes, - fn: () => Promise | T + fn: () => Promise | T, + options?: ObserveMetricOptions ): Promise { - const observationState = beginStepObservation(); - const baseAttributes = toMetricAttributes(attributes); + return observeOperation({ + fn, + options, + onFinish: (status, _result, state) => { + const metricAttributes = toStepMetricAttributes(attributes, { status }); - stepActive.add(1, baseAttributes); - - try { - const result = await fn(); - recordWorkflowStepEnd(attributes, observationState, 'ok', baseAttributes); - return result; - } catch (error) { - recordWorkflowStepEnd(attributes, observationState, 'error', baseAttributes); - throw error; - } -} - -function recordWorkflowStepEnd( - attributes: WorkflowStepMetricAttributes, - observationState: StepObservationState, - status: 'ok' | 'error', - baseAttributes: MetricAttributes -) { - const endSnapshot = takeProcessSnapshot(); - const metricAttributes = toMetricAttributes(attributes, { status }); - const stepOverlap = - observationState.hadOverlapAtStart || observationState.overlapVersionAtStart !== overlapVersion; - const memoryAttributes = toMetricAttributes(attributes, { - status, - memory_scope: 'process', - memory_attribution: stepOverlap ? 'best_effort' : 'exclusive', - step_overlap: stepOverlap + stepDuration.record(getObservationDurationMs(state), metricAttributes); + stepExecutions.add(1, metricAttributes); + } }); - const durationMs = Number(process.hrtime.bigint() - observationState.startedAt) / 1_000_000; - - stepDuration.record(durationMs, metricAttributes); - stepExecutions.add(1, metricAttributes); - stepCpuUserTime.record( - Math.max(0, endSnapshot.cpuUser - observationState.startSnapshot.cpuUser), - metricAttributes - ); - stepCpuSystemTime.record( - Math.max(0, endSnapshot.cpuSystem - observationState.startSnapshot.cpuSystem), - metricAttributes - ); - - stepMemoryRssStart.record(observationState.startSnapshot.rss, memoryAttributes); - stepMemoryHeapUsedStart.record(observationState.startSnapshot.heapUsed, memoryAttributes); - stepMemoryExternalStart.record(observationState.startSnapshot.external, memoryAttributes); - stepMemoryArrayBuffersStart.record(observationState.startSnapshot.arrayBuffers, memoryAttributes); - stepMemoryRss.record(endSnapshot.rss, memoryAttributes); - stepMemoryHeapUsed.record(endSnapshot.heapUsed, memoryAttributes); - stepMemoryExternal.record(endSnapshot.external, memoryAttributes); - stepMemoryArrayBuffers.record(endSnapshot.arrayBuffers, memoryAttributes); - - if (!stepOverlap && endSnapshot.rss > observationState.startSnapshot.rss) { - stepMemoryRssGrowth.record( - endSnapshot.rss - observationState.startSnapshot.rss, - memoryAttributes - ); - } - if (!stepOverlap && endSnapshot.heapUsed > observationState.startSnapshot.heapUsed) { - stepMemoryHeapUsedGrowth.record( - endSnapshot.heapUsed - observationState.startSnapshot.heapUsed, - memoryAttributes - ); - } - if (!stepOverlap && endSnapshot.external > observationState.startSnapshot.external) { - stepMemoryExternalGrowth.record( - endSnapshot.external - observationState.startSnapshot.external, - memoryAttributes - ); - } - - activeWorkflowStepCount = Math.max(0, activeWorkflowStepCount - 1); - stepActive.add(-1, baseAttributes); } diff --git a/packages/web/components/core/app/FileTypeSelector/index.tsx b/packages/web/components/core/app/FileTypeSelector/index.tsx index 5827df2d88..5480171c38 100644 --- a/packages/web/components/core/app/FileTypeSelector/index.tsx +++ b/packages/web/components/core/app/FileTypeSelector/index.tsx @@ -16,6 +16,14 @@ type FileTypeSelectorValue = { customFileExtensionList?: string[]; }; +const fileExtensionTypeTranslationMap = new Map([ + ['canSelectFile', 'app:upload_file_extension_type_canSelectFile'], + ['canSelectImg', 'app:upload_file_extension_type_canSelectImg'], + ['canSelectVideo', 'app:upload_file_extension_type_canSelectVideo'], + ['canSelectAudio', 'app:upload_file_extension_type_canSelectAudio'], + ['canSelectCustomFileExtension', 'app:upload_file_extension_type_canSelectCustomFileExtension'] +]); + export const FileTypeSelectorPanel = ({ value, onChange @@ -190,7 +198,7 @@ export const FileTypeSelectorPanel = ({ onChange={(e) => handleTypeChange(type as FileExtensionKeyType, e.target.checked)} > - {t(`app:upload_file_extension_type_${type}`)} + {t(fileExtensionTypeTranslationMap.get(type as FileExtensionKeyType) || type)} {exts.map((ext) => ext.slice(1)).join('/')} diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index e465283092..1e9f2c46f2 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -466,6 +466,10 @@ "upload_file_extension_type_canSelectCustomFileExtension": "Custom file extension type", "upload_file_extension_type_canSelectCustomFileExtension_placeholder": "file extension name", "upload_file_extension_types": "Supported file types", + "upload_file_extension_type_canSelectAudio": "Audio", + "upload_file_extension_type_canSelectFile": "Document", + "upload_file_extension_type_canSelectImg": "Image", + "upload_file_extension_type_canSelectVideo": "Video", "upload_file_max_amount": "Maximum File Quantity", "upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation", "upload_method": "Upload method", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 6fc02e103d..c2d0c00456 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -468,6 +468,10 @@ "upload_file_extension_types": "支持上传的类型", "upload_file_max_amount": "最大文件数量", "upload_file_max_amount_tip": "单轮对话中最大上传文件数量", + "upload_file_extension_type_canSelectAudio": "音频", + "upload_file_extension_type_canSelectFile": "文档", + "upload_file_extension_type_canSelectImg": "图片", + "upload_file_extension_type_canSelectVideo": "视频", "upload_method": "上传方式", "url_upload": "文件链接", "use_agent_sandbox": "虚拟机", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 69b84e1ca5..689ed1a4dd 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -452,6 +452,10 @@ "upload_file_extension_type_canSelectCustomFileExtension": "自定義文件擴展類型", "upload_file_extension_type_canSelectCustomFileExtension_placeholder": "文件擴展名", "upload_file_extension_types": "支持上傳的類型", + "upload_file_extension_type_canSelectAudio": "音頻", + "upload_file_extension_type_canSelectFile": "文檔", + "upload_file_extension_type_canSelectImg": "圖片", + "upload_file_extension_type_canSelectVideo": "視頻", "upload_file_max_amount": "最大檔案數量", "upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量", "upload_method": "上傳方式", diff --git a/projects/app/next.config.ts b/projects/app/next.config.ts index d3f95683d2..3886a2e48e 100644 --- a/projects/app/next.config.ts +++ b/projects/app/next.config.ts @@ -6,6 +6,8 @@ import withRspack from 'next-rspack'; const withBundleAnalyzer = withBundleAnalyzerInit({ enabled: process.env.ANALYZE === 'true' }); const isDev = process.env.NODE_ENV === 'development'; +const isWebpack = process.env.WEBPACK === '1'; +const isRspack = isDev && !isWebpack; const nextConfig: NextConfig = { basePath: process.env.NEXT_PUBLIC_BASE_URL, @@ -218,8 +220,5 @@ const nextConfig: NextConfig = { } }; -const configWithPluginsExceptWithRspack = withBundleAnalyzer(nextConfig); - -export default isDev - ? withRspack(configWithPluginsExceptWithRspack) - : configWithPluginsExceptWithRspack; +const config = withBundleAnalyzer(nextConfig); +export default isRspack ? withRspack(config) : config; diff --git a/projects/app/package.json b/projects/app/package.json index 18c69c14e1..4f421773f8 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -4,6 +4,7 @@ "private": false, "scripts": { "dev": "NODE_OPTIONS='--max-old-space-size=8192' npm run build:workers && next dev", + "dev:webpack": "NODE_OPTIONS='--max-old-space-size=8192' npm run build:workers && WEBPACK=1 next dev --webpack", "build": "npm run build:workers && next build --debug --webpack", "start": "next start", "build:workers": "npx tsx scripts/build-workers.ts",