Compare commits

...

4 Commits

Author SHA1 Message Date
autofix-ci[bot]
dc5428ec40 [autofix.ci] apply automated fixes 2026-03-18 13:46:26 +00:00
Stephen Zhou
6e22b4c613 refactor: move to std-semver 2026-03-18 21:42:39 +08:00
QuantumGhost
29c70736dc fix(api): Preserving the content transform logic in fetch_prompt_messages (#33666)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-18 19:41:25 +08:00
Desel72
7c99c9f3e8 fix: sync workflow description and name to MCP server on update (#33637) 2026-03-18 19:04:45 +08:00
15 changed files with 175 additions and 124 deletions

View File

@@ -103,13 +103,13 @@ class AppMCPServerController(Resource):
raise NotFound()
description = payload.description
if description is None:
pass
elif not description:
if description is None or not description:
server.description = app_model.description or ""
else:
server.description = description
server.name = app_model.name
server.parameters = json.dumps(payload.parameters, ensure_ascii=False)
if payload.status:
try:

View File

@@ -256,9 +256,13 @@ def fetch_prompt_messages(
):
continue
prompt_message_content.append(content_item)
if prompt_message_content:
if not prompt_message_content:
continue
if len(prompt_message_content) == 1 and prompt_message_content[0].type == PromptMessageContentType.TEXT:
prompt_message.content = prompt_message_content[0].data
else:
prompt_message.content = prompt_message_content
filtered_prompt_messages.append(prompt_message)
filtered_prompt_messages.append(prompt_message)
elif not prompt_message.is_empty():
filtered_prompt_messages.append(prompt_message)

View File

@@ -0,0 +1,106 @@
from unittest import mock
import pytest
from core.model_manager import ModelInstance
from dify_graph.model_runtime.entities import ImagePromptMessageContent, PromptMessageRole, TextPromptMessageContent
from dify_graph.model_runtime.entities.message_entities import SystemPromptMessage
from dify_graph.nodes.llm import llm_utils
from dify_graph.nodes.llm.entities import LLMNodeChatModelMessage
from dify_graph.nodes.llm.exc import NoPromptFoundError
from dify_graph.runtime import VariablePool
def _fetch_prompt_messages_with_mocked_content(content):
variable_pool = VariablePool.empty()
model_instance = mock.MagicMock(spec=ModelInstance)
prompt_template = [
LLMNodeChatModelMessage(
text="You are a classifier.",
role=PromptMessageRole.SYSTEM,
edition_type="basic",
)
]
with (
mock.patch(
"dify_graph.nodes.llm.llm_utils.fetch_model_schema",
return_value=mock.MagicMock(features=[]),
),
mock.patch(
"dify_graph.nodes.llm.llm_utils.handle_list_messages",
return_value=[SystemPromptMessage(content=content)],
),
mock.patch(
"dify_graph.nodes.llm.llm_utils.handle_memory_chat_mode",
return_value=[],
),
):
return llm_utils.fetch_prompt_messages(
sys_query=None,
sys_files=[],
context=None,
memory=None,
model_instance=model_instance,
prompt_template=prompt_template,
stop=["END"],
memory_config=None,
vision_enabled=False,
vision_detail=ImagePromptMessageContent.DETAIL.HIGH,
variable_pool=variable_pool,
jinja2_variables=[],
template_renderer=None,
)
def test_fetch_prompt_messages_skips_messages_when_all_contents_are_filtered_out():
with pytest.raises(NoPromptFoundError):
_fetch_prompt_messages_with_mocked_content(
[
ImagePromptMessageContent(
format="url",
url="https://example.com/image.png",
mime_type="image/png",
),
]
)
def test_fetch_prompt_messages_flattens_single_text_content_after_filtering_unsupported_multimodal_items():
prompt_messages, stop = _fetch_prompt_messages_with_mocked_content(
[
TextPromptMessageContent(data="You are a classifier."),
ImagePromptMessageContent(
format="url",
url="https://example.com/image.png",
mime_type="image/png",
),
]
)
assert stop == ["END"]
assert prompt_messages == [SystemPromptMessage(content="You are a classifier.")]
def test_fetch_prompt_messages_keeps_list_content_when_multiple_supported_items_remain():
prompt_messages, stop = _fetch_prompt_messages_with_mocked_content(
[
TextPromptMessageContent(data="You are"),
TextPromptMessageContent(data=" a classifier."),
ImagePromptMessageContent(
format="url",
url="https://example.com/image.png",
mime_type="image/png",
),
]
)
assert stop == ["END"]
assert prompt_messages == [
SystemPromptMessage(
content=[
TextPromptMessageContent(data="You are"),
TextPromptMessageContent(data=" a classifier."),
]
)
]

View File

@@ -22,33 +22,6 @@ vi.mock('@/service/plugins', () => ({
checkTaskStatus: vi.fn(),
}))
vi.mock('@/utils/semver', () => ({
compareVersion: (a: string, b: string) => {
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number)
const [aMajor, aMinor = 0, aPatch = 0] = parse(a)
const [bMajor, bMinor = 0, bPatch = 0] = parse(b)
if (aMajor !== bMajor)
return aMajor > bMajor ? 1 : -1
if (aMinor !== bMinor)
return aMinor > bMinor ? 1 : -1
if (aPatch !== bPatch)
return aPatch > bPatch ? 1 : -1
return 0
},
getLatestVersion: (versions: string[]) => {
return versions.sort((a, b) => {
const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number)
const [aMaj, aMin = 0, aPat = 0] = parse(a)
const [bMaj, bMin = 0, bPat = 0] = parse(b)
if (aMaj !== bMaj)
return bMaj - aMaj
if (aMin !== bMin)
return bMin - aMin
return bPat - aPat
})[0]
},
}))
const { useGitHubReleases, useGitHubUpload } = await import(
'@/app/components/plugins/install-plugin/hooks',
)

View File

@@ -16,34 +16,6 @@ vi.mock('@/service/plugins', () => ({
uploadGitHub: (...args: unknown[]) => mockUploadGitHub(...args),
}))
vi.mock('@/utils/semver', () => ({
compareVersion: (a: string, b: string) => {
const parseVersion = (v: string) => v.replace(/^v/, '').split('.').map(Number)
const va = parseVersion(a)
const vb = parseVersion(b)
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
const diff = (va[i] || 0) - (vb[i] || 0)
if (diff > 0)
return 1
if (diff < 0)
return -1
}
return 0
},
getLatestVersion: (versions: string[]) => {
return versions.sort((a, b) => {
const pa = a.replace(/^v/, '').split('.').map(Number)
const pb = b.replace(/^v/, '').split('.').map(Number)
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const diff = (pa[i] || 0) - (pb[i] || 0)
if (diff !== 0)
return diff
}
return 0
}).pop()!
},
}))
const mockFetch = vi.fn()
globalThis.fetch = mockFetch

View File

@@ -5,12 +5,12 @@ import { RiLoader2Line } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { gte } from 'semver'
import Button from '@/app/components/base/button'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { useAppContext } from '@/context/app-context'
import { uninstallPlugin } from '@/service/plugins'
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
import { isEqualOrLaterThanVersion } from '@/utils/semver'
import Card from '../../../card'
import { TaskStatus } from '../../../types'
import checkTaskStatus from '../../base/check-task-status'
@@ -111,13 +111,13 @@ const Installed: FC<Props> = ({
const isDifyVersionCompatible = useMemo(() => {
if (!langGeniusVersionInfo.current_version)
return true
return gte(langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0')
return isEqualOrLaterThanVersion(langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version ?? '0.0.0')
}, [langGeniusVersionInfo.current_version, payload.meta.minimum_dify_version])
return (
<>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
<div className="system-md-regular text-text-secondary">
<div className="text-text-secondary system-md-regular">
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
<p>
<Trans
@@ -127,7 +127,7 @@ const Installed: FC<Props> = ({
/>
</p>
{!isDifyVersionCompatible && (
<p className="system-md-regular flex items-center gap-1 text-text-warning">
<p className="flex items-center gap-1 text-text-warning system-md-regular">
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: payload.meta.minimum_dify_version })}
</p>
)}

View File

@@ -5,11 +5,11 @@ import { RiLoader2Line } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { gte } from 'semver'
import Button from '@/app/components/base/button'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { useAppContext } from '@/context/app-context'
import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, usePluginTaskList, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
import { isEqualOrLaterThanVersion } from '@/utils/semver'
import Card from '../../../card'
// import { RiInformation2Line } from '@remixicon/react'
import { TaskStatus } from '../../../types'
@@ -126,17 +126,17 @@ const Installed: FC<Props> = ({
const isDifyVersionCompatible = useMemo(() => {
if (!pluginDeclaration || !langGeniusVersionInfo.current_version)
return true
return gte(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
return isEqualOrLaterThanVersion(langGeniusVersionInfo.current_version, pluginDeclaration?.manifest.meta.minimum_dify_version ?? '0.0.0')
}, [langGeniusVersionInfo.current_version, pluginDeclaration])
const { canInstall } = useInstallPluginLimit({ ...payload, from: 'marketplace' })
return (
<>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
<div className="system-md-regular text-text-secondary">
<div className="text-text-secondary system-md-regular">
<p>{t(`${i18nPrefix}.readyToInstall`, { ns: 'plugin' })}</p>
{!isDifyVersionCompatible && (
<p className="system-md-regular text-text-warning">
<p className="text-text-warning system-md-regular">
{t('difyVersionNotCompatible', { ns: 'plugin', minimalDifyVersion: pluginDeclaration?.manifest.meta.minimum_dify_version })}
</p>
)}

View File

@@ -11,7 +11,6 @@ import {
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { gte } from 'semver'
import Tooltip from '@/app/components/base/tooltip'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { API_PREFIX } from '@/config'
@@ -20,6 +19,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import useTheme from '@/hooks/use-theme'
import { cn } from '@/utils/classnames'
import { isEqualOrLaterThanVersion } from '@/utils/semver'
import { getMarketplaceUrl } from '@/utils/var'
import Badge from '../../base/badge'
import { Github } from '../../base/icons/src/public/common'
@@ -71,7 +71,7 @@ const PluginItem: FC<Props> = ({
const isDifyVersionCompatible = useMemo(() => {
if (!langGeniusVersionInfo.current_version)
return true
return gte(langGeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0')
return isEqualOrLaterThanVersion(langGeniusVersionInfo.current_version, declarationMeta.minimum_dify_version ?? '0.0.0')
}, [declarationMeta.minimum_dify_version, langGeniusVersionInfo.current_version])
const isDeprecated = useMemo(() => {
@@ -164,8 +164,8 @@ const PluginItem: FC<Props> = ({
/>
{category === PluginCategoryEnum.extension && (
<>
<div className="system-xs-regular mx-2 text-text-quaternary">·</div>
<div className="system-xs-regular flex items-center gap-x-1 overflow-hidden text-text-tertiary">
<div className="mx-2 text-text-quaternary system-xs-regular">·</div>
<div className="flex items-center gap-x-1 overflow-hidden text-text-tertiary system-xs-regular">
<RiLoginCircleLine className="size-3 shrink-0" />
<span
className="truncate"
@@ -183,7 +183,7 @@ const PluginItem: FC<Props> = ({
&& (
<>
<a href={`https://github.com/${meta!.repo}`} target="_blank" className="flex items-center gap-1">
<div className="system-2xs-medium-uppercase text-text-tertiary">{t('from', { ns: 'plugin' })}</div>
<div className="text-text-tertiary system-2xs-medium-uppercase">{t('from', { ns: 'plugin' })}</div>
<div className="flex items-center space-x-0.5 text-text-secondary">
<Github className="h-3 w-3" />
<div className="system-2xs-semibold-uppercase">GitHub</div>
@@ -196,7 +196,7 @@ const PluginItem: FC<Props> = ({
&& (
<>
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target="_blank" className="flex items-center gap-0.5">
<div className="system-2xs-medium-uppercase text-text-tertiary">
<div className="text-text-tertiary system-2xs-medium-uppercase">
{t('from', { ns: 'plugin' })}
{' '}
<span className="text-text-secondary">marketplace</span>
@@ -210,7 +210,7 @@ const PluginItem: FC<Props> = ({
<>
<div className="flex items-center gap-1">
<RiHardDrive3Line className="h-3 w-3 text-text-tertiary" />
<div className="system-2xs-medium-uppercase text-text-tertiary">Local Plugin</div>
<div className="text-text-tertiary system-2xs-medium-uppercase">Local Plugin</div>
</div>
</>
)}
@@ -219,14 +219,14 @@ const PluginItem: FC<Props> = ({
<>
<div className="flex items-center gap-1">
<RiBugLine className="h-3 w-3 text-text-warning" />
<div className="system-2xs-medium-uppercase text-text-warning">Debugging Plugin</div>
<div className="text-text-warning system-2xs-medium-uppercase">Debugging Plugin</div>
</div>
</>
)}
</div>
{/* Deprecated */}
{source === PluginSource.marketplace && enable_marketplace && isDeprecated && (
<div className="system-2xs-medium-uppercase flex shrink-0 items-center gap-x-2">
<div className="flex shrink-0 items-center gap-x-2 system-2xs-medium-uppercase">
<span className="text-text-tertiary">·</span>
<span className="text-text-warning">
{t('deprecated', { ns: 'plugin' })}

View File

@@ -104,20 +104,6 @@ vi.mock('../../install-plugin/install-from-github', () => ({
),
}))
// Mock semver
vi.mock('semver', () => ({
lt: (v1: string, v2: string) => {
const parseVersion = (v: string) => v.split('.').map(Number)
const [major1, minor1, patch1] = parseVersion(v1)
const [major2, minor2, patch2] = parseVersion(v2)
if (major1 !== major2)
return major1 < major2
if (minor1 !== minor2)
return minor1 < minor2
return patch1 < patch2
},
}))
// ================================
// Test Data Factories
// ================================

View File

@@ -4,7 +4,6 @@ import type { Placement } from '@/app/components/base/ui/placement'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { lt } from 'semver'
import Badge from '@/app/components/base/badge'
import {
Popover,
@@ -14,6 +13,7 @@ import {
import useTimestamp from '@/hooks/use-timestamp'
import { useVersionListOfPlugin } from '@/service/use-plugins'
import { cn } from '@/utils/classnames'
import { isEarlierThanVersion } from '@/utils/semver'
type Props = {
disabled?: boolean
@@ -100,7 +100,7 @@ const PluginVersionPicker: FC<Props> = ({
onClick={() => handleSelect({
version: version.version,
unique_identifier: version.unique_identifier,
isDowngrade: lt(version.version, currentVersion),
isDowngrade: isEarlierThanVersion(version.version, currentVersion),
})}
>
<div className="flex grow items-center">

View File

@@ -4976,11 +4976,6 @@
"count": 1
}
},
"app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1
@@ -4997,11 +4992,6 @@
"count": 1
}
},
"app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/plugins/marketplace/description/index.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 9
@@ -5480,9 +5470,6 @@
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 7
},
"ts/no-explicit-any": {
"count": 1
}

View File

@@ -151,9 +151,9 @@
"remark-breaks": "4.0.0",
"remark-directive": "4.0.0",
"scheduler": "0.27.0",
"semver": "7.7.4",
"sharp": "0.34.5",
"sortablejs": "1.15.7",
"std-semver": "1.0.8",
"streamdown": "2.5.0",
"string-ts": "2.3.1",
"tailwind-merge": "2.6.1",
@@ -206,7 +206,6 @@
"@types/react-slider": "1.3.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/react-window": "1.8.8",
"@types/semver": "7.7.1",
"@types/sortablejs": "1.15.9",
"@typescript-eslint/parser": "8.57.1",
"@typescript/native-preview": "7.0.0-dev.20260317.1",

20
web/pnpm-lock.yaml generated
View File

@@ -340,15 +340,15 @@ importers:
scheduler:
specifier: 0.27.0
version: 0.27.0
semver:
specifier: 7.7.4
version: 7.7.4
sharp:
specifier: 0.34.5
version: 0.34.5
sortablejs:
specifier: 1.15.7
version: 1.15.7
std-semver:
specifier: 1.0.8
version: 1.0.8
streamdown:
specifier: 2.5.0
version: 2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -500,9 +500,6 @@ importers:
'@types/react-window':
specifier: 1.8.8
version: 1.8.8
'@types/semver':
specifier: 7.7.1
version: 7.7.1
'@types/sortablejs':
specifier: 1.15.9
version: 1.15.9
@@ -3420,9 +3417,6 @@ packages:
'@types/resolve@1.20.6':
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@types/sortablejs@1.15.9':
resolution: {integrity: sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==}
@@ -7115,6 +7109,10 @@ packages:
std-env@4.0.0:
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
std-semver@1.0.8:
resolution: {integrity: sha512-9SN0XIjBBXCT6ZXXVnScJN4KP2RyFg6B8sEoFlugVHMANysfaEni4LTWlvUQQ/R0wgZl1Ovt9KBQbzn21kHoZA==}
engines: {node: '>=20.19.0'}
storybook@10.2.19:
resolution: {integrity: sha512-UUm5eGSm6BLhkcFP0WbxkmAHJZfVN2ViLpIZOqiIPS++q32VYn+CLFC0lrTYTDqYvaG7i4BK4uowXYujzE4NdQ==}
hasBin: true
@@ -10755,8 +10753,6 @@ snapshots:
'@types/resolve@1.20.6': {}
'@types/semver@7.7.1': {}
'@types/sortablejs@1.15.9': {}
'@types/trusted-types@2.0.7':
@@ -15205,6 +15201,8 @@ snapshots:
std-env@4.0.0: {}
std-semver@1.0.8: {}
storybook@10.2.19(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@storybook/global': 5.0.0

View File

@@ -1,4 +1,4 @@
import { compareVersion, getLatestVersion, isEqualOrLaterThanVersion } from './semver'
import { compareVersion, getLatestVersion, isEarlierThanVersion, isEqualOrLaterThanVersion } from './semver'
describe('semver utilities', () => {
describe('getLatestVersion', () => {
@@ -72,4 +72,24 @@ describe('semver utilities', () => {
expect(isEqualOrLaterThanVersion('1.0.0-alpha', '1.0.0')).toBe(false)
})
})
describe('isEarlierThanVersion', () => {
it('should return true when baseVersion is less than targetVersion', () => {
expect(isEarlierThanVersion('1.0.0', '1.1.0')).toBe(true)
expect(isEarlierThanVersion('1.9.9', '2.0.0')).toBe(true)
expect(isEarlierThanVersion('1.0.0', '1.0.1')).toBe(true)
})
it('should return false when baseVersion is equal to or greater than targetVersion', () => {
expect(isEarlierThanVersion('1.0.0', '1.0.0')).toBe(false)
expect(isEarlierThanVersion('1.1.0', '1.0.0')).toBe(false)
expect(isEarlierThanVersion('1.0.1', '1.0.0')).toBe(false)
})
it('should handle pre-release versions correctly', () => {
expect(isEarlierThanVersion('1.0.0-beta', '1.0.0')).toBe(true)
expect(isEarlierThanVersion('1.0.0-alpha', '1.0.0-beta')).toBe(true)
expect(isEarlierThanVersion('1.0.0', '1.0.0-beta')).toBe(false)
})
})
})

View File

@@ -1,13 +1,19 @@
import semver from 'semver'
import { compare, greaterOrEqual, lessThan, parse } from 'std-semver'
export const getLatestVersion = (versionList: string[]) => {
return semver.rsort(versionList)[0]
return [...versionList].sort((versionA, versionB) => {
return compare(parse(versionB), parse(versionA))
})[0]
}
export const compareVersion = (v1: string, v2: string) => {
return semver.compare(v1, v2)
return compare(parse(v1), parse(v2))
}
export const isEqualOrLaterThanVersion = (baseVersion: string, targetVersion: string) => {
return semver.gte(baseVersion, targetVersion)
return greaterOrEqual(parse(baseVersion), parse(targetVersion))
}
export const isEarlierThanVersion = (baseVersion: string, targetVersion: string) => {
return lessThan(parse(baseVersion), parse(targetVersion))
}