diff --git a/api/core/model_runtime/README.md b/api/core/model_runtime/README.md index a6caa7eb1e..b9d2c55210 100644 --- a/api/core/model_runtime/README.md +++ b/api/core/model_runtime/README.md @@ -18,34 +18,20 @@ This module provides the interface for invoking and authenticating various model - Model provider display - ![image-20231210143654461](./docs/en_US/images/index/image-20231210143654461.png) - - Displays a list of all supported providers, including provider names, icons, supported model types list, predefined model list, configuration method, and credentials form rules, etc. For detailed rule design, see: [Schema](./docs/en_US/schema.md). + Displays a list of all supported providers, including provider names, icons, supported model types list, predefined model list, configuration method, and credentials form rules, etc. - Selectable model list display - ![image-20231210144229650](./docs/en_US/images/index/image-20231210144229650.png) - After configuring provider/model credentials, the dropdown (application orchestration interface/default model) allows viewing of the available LLM list. Greyed out items represent predefined model lists from providers without configured credentials, facilitating user review of supported models. - In addition, this list also returns configurable parameter information and rules for LLM, as shown below: - - ![image-20231210144814617](./docs/en_US/images/index/image-20231210144814617.png) - - These parameters are all defined in the backend, allowing different settings for various parameters supported by different models, as detailed in: [Schema](./docs/en_US/schema.md#ParameterRule). + In addition, this list also returns configurable parameter information and rules for LLM. These parameters are all defined in the backend, allowing different settings for various parameters supported by different models. - Provider/model credential authentication - ![image-20231210151548521](./docs/en_US/images/index/image-20231210151548521.png) - - ![image-20231210151628992](./docs/en_US/images/index/image-20231210151628992.png) - - The provider list returns configuration information for the credentials form, which can be authenticated through Runtime's interface. The first image above is a provider credential DEMO, and the second is a model credential DEMO. + The provider list returns configuration information for the credentials form, which can be authenticated through Runtime's interface. ## Structure -![](./docs/en_US/images/index/image-20231210165243632.png) - Model Runtime is divided into three layers: - The outermost layer is the factory method @@ -60,9 +46,6 @@ Model Runtime is divided into three layers: It offers direct invocation of various model types, predefined model configuration information, getting predefined/remote model lists, model credential authentication methods. Different models provide additional special methods, like LLM's pre-computed tokens method, cost information obtaining method, etc., **allowing horizontal expansion** for different models under the same provider (within supported model types). -## Next Steps +## Documentation -- Add new provider configuration: [Link](./docs/en_US/provider_scale_out.md) -- Add new models for existing providers: [Link](./docs/en_US/provider_scale_out.md#AddModel) -- View YAML configuration rules: [Link](./docs/en_US/schema.md) -- Implement interface methods: [Link](./docs/en_US/interfaces.md) +For detailed documentation on how to add new providers or models, please refer to the [Dify documentation](https://docs.dify.ai/). diff --git a/api/core/model_runtime/README_CN.md b/api/core/model_runtime/README_CN.md index dfe614347a..0a8b56b3fe 100644 --- a/api/core/model_runtime/README_CN.md +++ b/api/core/model_runtime/README_CN.md @@ -18,34 +18,20 @@ - 模型供应商展示 - ![image-20231210143654461](./docs/zh_Hans/images/index/image-20231210143654461.png) - -​ 展示所有已支持的供应商列表,除了返回供应商名称、图标之外,还提供了支持的模型类型列表,预定义模型列表、配置方式以及配置凭据的表单规则等等,规则设计详见:[Schema](./docs/zh_Hans/schema.md)。 + 展示所有已支持的供应商列表,除了返回供应商名称、图标之外,还提供了支持的模型类型列表,预定义模型列表、配置方式以及配置凭据的表单规则等等。 - 可选择的模型列表展示 - ![image-20231210144229650](./docs/zh_Hans/images/index/image-20231210144229650.png) + 配置供应商/模型凭据后,可在此下拉(应用编排界面/默认模型)查看可用的 LLM 列表,其中灰色的为未配置凭据供应商的预定义模型列表,方便用户查看已支持的模型。 -​ 配置供应商/模型凭据后,可在此下拉(应用编排界面/默认模型)查看可用的 LLM 列表,其中灰色的为未配置凭据供应商的预定义模型列表,方便用户查看已支持的模型。 - -​ 除此之外,该列表还返回了 LLM 可配置的参数信息和规则,如下图: - -​ ![image-20231210144814617](./docs/zh_Hans/images/index/image-20231210144814617.png) - -​ 这里的参数均为后端定义,相比之前只有 5 种固定参数,这里可为不同模型设置所支持的各种参数,详见:[Schema](./docs/zh_Hans/schema.md#ParameterRule)。 + 除此之外,该列表还返回了 LLM 可配置的参数信息和规则。这里的参数均为后端定义,相比之前只有 5 种固定参数,这里可为不同模型设置所支持的各种参数。 - 供应商/模型凭据鉴权 - ![image-20231210151548521](./docs/zh_Hans/images/index/image-20231210151548521.png) - -![image-20231210151628992](./docs/zh_Hans/images/index/image-20231210151628992.png) - -​ 供应商列表返回了凭据表单的配置信息,可通过 Runtime 提供的接口对凭据进行鉴权,上图 1 为供应商凭据 DEMO,上图 2 为模型凭据 DEMO。 + 供应商列表返回了凭据表单的配置信息,可通过 Runtime 提供的接口对凭据进行鉴权。 ## 结构 -![](./docs/zh_Hans/images/index/image-20231210165243632.png) - Model Runtime 分三层: - 最外层为工厂方法 @@ -59,8 +45,7 @@ Model Runtime 分三层: 对于供应商/模型凭据,有两种情况 - 如 OpenAI 这类中心化供应商,需要定义如**api_key**这类的鉴权凭据 - - 如[**Xinference**](https://github.com/xorbitsai/inference)这类本地部署的供应商,需要定义如**server_url**这类的地址凭据,有时候还需要定义**model_uid**之类的模型类型凭据,就像下面这样,当在供应商层定义了这些凭据后,就可以在前端页面上直接展示,无需修改前端逻辑。 - ![Alt text](docs/zh_Hans/images/index/image.png) + - 如[**Xinference**](https://github.com/xorbitsai/inference)这类本地部署的供应商,需要定义如**server_url**这类的地址凭据,有时候还需要定义**model_uid**之类的模型类型凭据。当在供应商层定义了这些凭据后,就可以在前端页面上直接展示,无需修改前端逻辑。 当配置好凭据后,就可以通过 DifyRuntime 的外部接口直接获取到对应供应商所需要的**Schema**(凭据表单规则),从而在可以在不修改前端逻辑的情况下,提供新的供应商/模型的支持。 @@ -74,20 +59,6 @@ Model Runtime 分三层: - 模型凭据 (**在供应商层定义**):这是一类不经常变动,一般在配置好后就不会再变动的参数,如 **api_key**、**server_url** 等。在 DifyRuntime 中,他们的参数名一般为**credentials: dict[str, any]**,Provider 层的 credentials 会直接被传递到这一层,不需要再单独定义。 -## 下一步 +## 文档 -### [增加新的供应商配置 👈🏻](./docs/zh_Hans/provider_scale_out.md) - -当添加后,这里将会出现一个新的供应商 - -![Alt text](docs/zh_Hans/images/index/image-1.png) - -### [为已存在的供应商新增模型 👈🏻](./docs/zh_Hans/provider_scale_out.md#%E5%A2%9E%E5%8A%A0%E6%A8%A1%E5%9E%8B) - -当添加后,对应供应商的模型列表中将会出现一个新的预定义模型供用户选择,如 GPT-3.5 GPT-4 ChatGLM3-6b 等,而对于支持自定义模型的供应商,则不需要新增模型。 - -![Alt text](docs/zh_Hans/images/index/image-2.png) - -### [接口的具体实现 👈🏻](./docs/zh_Hans/interfaces.md) - -你可以在这里找到你想要查看的接口的具体实现,以及接口的参数和返回值的具体含义。 +有关如何添加新供应商或模型的详细文档,请参阅 [Dify 文档](https://docs.dify.ai/)。 diff --git a/web/app/components/base/mermaid/index.tsx b/web/app/components/base/mermaid/index.tsx index bf35c8c94c..92fcd5cac9 100644 --- a/web/app/components/base/mermaid/index.tsx +++ b/web/app/components/base/mermaid/index.tsx @@ -8,6 +8,7 @@ import { isMermaidCodeComplete, prepareMermaidCode, processSvgForTheme, + sanitizeMermaidCode, svgToBase64, waitForDOMElement, } from './utils' @@ -71,7 +72,7 @@ const initMermaid = () => { const config: MermaidConfig = { startOnLoad: false, fontFamily: 'sans-serif', - securityLevel: 'loose', + securityLevel: 'strict', flowchart: { htmlLabels: true, useMaxWidth: true, @@ -267,6 +268,8 @@ const Flowchart = (props: FlowchartProps) => { finalCode = prepareMermaidCode(primitiveCode, look) } + finalCode = sanitizeMermaidCode(finalCode) + // Step 2: Render chart const svgGraph = await renderMermaidChart(finalCode, look) @@ -297,9 +300,9 @@ const Flowchart = (props: FlowchartProps) => { const configureMermaid = useCallback((primitiveCode: string) => { if (typeof window !== 'undefined' && isInitialized) { const themeVars = THEMES[currentTheme] - const config: any = { + const config: MermaidConfig = { startOnLoad: false, - securityLevel: 'loose', + securityLevel: 'strict', fontFamily: 'sans-serif', maxTextSize: 50000, gantt: { @@ -325,7 +328,8 @@ const Flowchart = (props: FlowchartProps) => { config.theme = currentTheme === 'dark' ? 'dark' : 'neutral' if (isFlowchart) { - config.flowchart = { + type FlowchartConfigWithRanker = NonNullable & { ranker?: string } + const flowchartConfig: FlowchartConfigWithRanker = { htmlLabels: true, useMaxWidth: true, nodeSpacing: 60, @@ -333,6 +337,7 @@ const Flowchart = (props: FlowchartProps) => { curve: 'linear', ranker: 'tight-tree', } + config.flowchart = flowchartConfig as unknown as MermaidConfig['flowchart'] } if (currentTheme === 'dark') { @@ -531,7 +536,7 @@ const Flowchart = (props: FlowchartProps) => { {isLoading && !svgString && (
- +
{t('common.wait_for_completion', 'Waiting for diagram code to complete...')}
@@ -564,7 +569,7 @@ const Flowchart = (props: FlowchartProps) => { {errMsg && (
- + {errMsg}
diff --git a/web/app/components/base/mermaid/utils.spec.ts b/web/app/components/base/mermaid/utils.spec.ts index 6ea7f17bfa..7a73aa1fc9 100644 --- a/web/app/components/base/mermaid/utils.spec.ts +++ b/web/app/components/base/mermaid/utils.spec.ts @@ -1,4 +1,4 @@ -import { cleanUpSvgCode } from './utils' +import { cleanUpSvgCode, prepareMermaidCode, sanitizeMermaidCode } from './utils' describe('cleanUpSvgCode', () => { it('replaces old-style
tags with the new style', () => { @@ -6,3 +6,54 @@ describe('cleanUpSvgCode', () => { expect(result).toEqual('
test
') }) }) + +describe('sanitizeMermaidCode', () => { + it('removes click directives to prevent link/callback injection', () => { + const unsafeProtocol = ['java', 'script:'].join('') + const input = [ + 'gantt', + 'title Demo', + 'section S1', + 'Task 1 :a1, 2020-01-01, 1d', + `click A href "${unsafeProtocol}alert(location.href)"`, + 'click B call callback()', + ].join('\n') + + const result = sanitizeMermaidCode(input) + + expect(result).toContain('gantt') + expect(result).toContain('Task 1') + expect(result).not.toContain('click A') + expect(result).not.toContain('click B') + expect(result).not.toContain(unsafeProtocol) + }) + + it('removes Mermaid init directives to prevent config overrides', () => { + const input = [ + '%%{init: {"securityLevel":"loose"}}%%', + 'graph TD', + 'A-->B', + ].join('\n') + + const result = sanitizeMermaidCode(input) + + expect(result).toEqual(['graph TD', 'A-->B'].join('\n')) + }) +}) + +describe('prepareMermaidCode', () => { + it('sanitizes click directives in flowcharts', () => { + const unsafeProtocol = ['java', 'script:'].join('') + const input = [ + 'graph TD', + 'A[Click]-->B', + `click A href "${unsafeProtocol}alert(1)"`, + ].join('\n') + + const result = prepareMermaidCode(input, 'classic') + + expect(result).toContain('graph TD') + expect(result).not.toContain('click ') + expect(result).not.toContain(unsafeProtocol) + }) +}) diff --git a/web/app/components/base/mermaid/utils.ts b/web/app/components/base/mermaid/utils.ts index 7e59869de1..e4abed3e44 100644 --- a/web/app/components/base/mermaid/utils.ts +++ b/web/app/components/base/mermaid/utils.ts @@ -2,6 +2,28 @@ export function cleanUpSvgCode(svgCode: string): string { return svgCode.replaceAll('
', '
') } +export const sanitizeMermaidCode = (mermaidCode: string): string => { + if (!mermaidCode || typeof mermaidCode !== 'string') + return '' + + return mermaidCode + .split('\n') + .filter((line) => { + const trimmed = line.trimStart() + + // Mermaid directives can override config; treat as untrusted in chat context. + if (trimmed.startsWith('%%{')) + return false + + // Mermaid click directives can create JS callbacks/links inside rendered SVG. + if (trimmed.startsWith('click ')) + return false + + return true + }) + .join('\n') +} + /** * Prepares mermaid code for rendering by sanitizing common syntax issues. * @param {string} mermaidCode - The mermaid code to prepare @@ -12,10 +34,7 @@ export const prepareMermaidCode = (mermaidCode: string, style: 'classic' | 'hand if (!mermaidCode || typeof mermaidCode !== 'string') return '' - let code = mermaidCode.trim() - - // Security: Sanitize against javascript: protocol in click events (XSS vector) - code = code.replace(/(\bclick\s+\w+\s+")javascript:[^"]*(")/g, '$1#$2') + let code = sanitizeMermaidCode(mermaidCode.trim()) // Convenience: Basic BR replacement. This is a common and safe operation. code = code.replace(//g, '\n') diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 7ce5acb228..8af7fb4c9f 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -4,6 +4,7 @@ import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { produce } from 'immer' import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderParameter, WorkflowToolProviderRequest } from '../types' +import { buildWorkflowOutputParameters } from './utils' import cn from '@/utils/classnames' import Drawer from '@/app/components/base/drawer-plus' import Input from '@/app/components/base/input' @@ -47,7 +48,9 @@ const WorkflowToolAsModal: FC = ({ const [name, setName] = useState(payload.name) const [description, setDescription] = useState(payload.description) const [parameters, setParameters] = useState(payload.parameters) - const outputParameters = useMemo(() => payload.outputParameters, [payload.outputParameters]) + const rawOutputParameters = payload.outputParameters + const outputSchema = payload.tool?.output_schema + const outputParameters = useMemo(() => buildWorkflowOutputParameters(rawOutputParameters, outputSchema), [rawOutputParameters, outputSchema]) const reservedOutputParameters: WorkflowToolProviderOutputParameter[] = [ { name: 'text', diff --git a/web/app/components/tools/workflow-tool/utils.test.ts b/web/app/components/tools/workflow-tool/utils.test.ts new file mode 100644 index 0000000000..fef8c05489 --- /dev/null +++ b/web/app/components/tools/workflow-tool/utils.test.ts @@ -0,0 +1,47 @@ +import { VarType } from '@/app/components/workflow/types' +import type { WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema } from '../types' +import { buildWorkflowOutputParameters } from './utils' + +describe('buildWorkflowOutputParameters', () => { + it('returns provided output parameters when array input exists', () => { + const params: WorkflowToolProviderOutputParameter[] = [ + { name: 'text', description: 'final text', type: VarType.string }, + ] + + const result = buildWorkflowOutputParameters(params, null) + + expect(result).toBe(params) + }) + + it('derives parameters from schema when explicit array missing', () => { + const schema: WorkflowToolProviderOutputSchema = { + type: 'object', + properties: { + answer: { + type: VarType.string, + description: 'AI answer', + }, + attachments: { + type: VarType.arrayFile, + description: 'Supporting files', + }, + unknown: { + type: 'custom', + description: 'Unsupported type', + }, + }, + } + + const result = buildWorkflowOutputParameters(undefined, schema) + + expect(result).toEqual([ + { name: 'answer', description: 'AI answer', type: VarType.string }, + { name: 'attachments', description: 'Supporting files', type: VarType.arrayFile }, + { name: 'unknown', description: 'Unsupported type', type: undefined }, + ]) + }) + + it('returns empty array when no source information is provided', () => { + expect(buildWorkflowOutputParameters(null, null)).toEqual([]) + }) +}) diff --git a/web/app/components/tools/workflow-tool/utils.ts b/web/app/components/tools/workflow-tool/utils.ts new file mode 100644 index 0000000000..80d832fb47 --- /dev/null +++ b/web/app/components/tools/workflow-tool/utils.ts @@ -0,0 +1,28 @@ +import type { WorkflowToolProviderOutputParameter, WorkflowToolProviderOutputSchema } from '../types' +import { VarType } from '@/app/components/workflow/types' + +const validVarTypes = new Set(Object.values(VarType)) + +const normalizeVarType = (type?: string): VarType | undefined => { + if (!type) + return undefined + + return validVarTypes.has(type) ? type as VarType : undefined +} + +export const buildWorkflowOutputParameters = ( + outputParameters: WorkflowToolProviderOutputParameter[] | null | undefined, + outputSchema?: WorkflowToolProviderOutputSchema | null, +): WorkflowToolProviderOutputParameter[] => { + if (Array.isArray(outputParameters)) + return outputParameters + + if (!outputSchema?.properties) + return [] + + return Object.entries(outputSchema.properties).map(([name, schema]) => ({ + name, + description: schema.description, + type: normalizeVarType(schema.type), + })) +}