Merge branch 'main' into feat/queue-based-graph-engine

This commit is contained in:
-LAN- 2025-09-09 13:33:17 +08:00 committed by GitHub
commit b46858d87d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1385 additions and 58 deletions

View File

@ -8,37 +8,44 @@ from flask_restx import reqparse
from pydantic import BaseModel
from sqlalchemy.orm import Session
from core.file.constants import DEFAULT_SERVICE_API_USER_ID
from extensions.ext_database import db
from libs.login import _get_user
from models.account import Account, Tenant
from models.account import Tenant
from models.model import EndUser
from services.account_service import AccountService
def get_user(tenant_id: str, user_id: str | None) -> Account | EndUser:
def get_user(tenant_id: str, user_id: str | None) -> EndUser:
"""
Get current user
NOTE: user_id is not trusted, it could be maliciously set to any value.
As a result, it could only be considered as an end user id.
"""
try:
with Session(db.engine) as session:
if not user_id:
user_id = "DEFAULT-USER"
user_id = DEFAULT_SERVICE_API_USER_ID
user_model = (
session.query(EndUser)
.where(
EndUser.session_id == user_id,
EndUser.tenant_id == tenant_id,
)
.first()
)
if not user_model:
user_model = EndUser(
tenant_id=tenant_id,
type="service_api",
is_anonymous=user_id == DEFAULT_SERVICE_API_USER_ID,
session_id=user_id,
)
session.add(user_model)
session.commit()
session.refresh(user_model)
if user_id == "DEFAULT-USER":
user_model = session.query(EndUser).where(EndUser.session_id == "DEFAULT-USER").first()
if not user_model:
user_model = EndUser(
tenant_id=tenant_id,
type="service_api",
is_anonymous=True if user_id == "DEFAULT-USER" else False,
session_id=user_id,
)
session.add(user_model)
session.commit()
session.refresh(user_model)
else:
user_model = AccountService.load_user(user_id)
if not user_model:
user_model = session.query(EndUser).where(EndUser.id == user_id).first()
if not user_model:
raise ValueError("user not found")
except Exception:
raise ValueError("user not found")
@ -63,7 +70,7 @@ def get_user_tenant(view: Optional[Callable] = None):
raise ValueError("tenant_id is required")
if not user_id:
user_id = "DEFAULT-USER"
user_id = DEFAULT_SERVICE_API_USER_ID
del kwargs["tenant_id"]
del kwargs["user_id"]

View File

@ -13,6 +13,7 @@ from sqlalchemy import select, update
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
from core.file.constants import DEFAULT_SERVICE_API_USER_ID
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.datetime_utils import naive_utc_now
@ -271,7 +272,7 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str]
Create or update session terminal based on user ID.
"""
if not user_id:
user_id = "DEFAULT-USER"
user_id = DEFAULT_SERVICE_API_USER_ID
with Session(db.engine, expire_on_commit=False) as session:
end_user = (
@ -290,7 +291,7 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str]
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="service_api",
is_anonymous=user_id == "DEFAULT-USER",
is_anonymous=user_id == DEFAULT_SERVICE_API_USER_ID,
session_id=user_id,
)
session.add(end_user)

View File

@ -9,3 +9,7 @@ FILE_MODEL_IDENTITY = "__dify__file__"
def maybe_file_object(o: Any) -> bool:
return isinstance(o, dict) and o.get("dify_model_identity") == FILE_MODEL_IDENTITY
# The default user ID for service API calls.
DEFAULT_SERVICE_API_USER_ID = "DEFAULT-USER"

View File

@ -5,6 +5,7 @@ import os
import time
from configs import dify_config
from core.file.constants import DEFAULT_SERVICE_API_USER_ID
def get_signed_file_url(upload_file_id: str) -> str:
@ -26,7 +27,7 @@ def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str,
url = f"{base_url}/files/upload/for-plugin"
if user_id is None:
user_id = "DEFAULT-USER"
user_id = DEFAULT_SERVICE_API_USER_ID
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()
@ -42,7 +43,7 @@ def verify_plugin_file_signature(
*, filename: str, mimetype: str, tenant_id: str, user_id: str | None, timestamp: str, nonce: str, sign: str
) -> bool:
if user_id is None:
user_id = "DEFAULT-USER"
user_id = DEFAULT_SERVICE_API_USER_ID
data_to_sign = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{timestamp}|{nonce}"
secret_key = dify_config.SECRET_KEY.encode()

View File

@ -49,7 +49,7 @@ class Dataset(Base):
INDEXING_TECHNIQUE_LIST = ["high_quality", "economy", None]
PROVIDER_LIST = ["vendor", "external", None]
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
id: Mapped[str] = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
tenant_id: Mapped[str] = mapped_column(StringUUID)
name: Mapped[str] = mapped_column(String(255))
description = mapped_column(sa.Text, nullable=True)

144
web/.oxlintrc.json Normal file
View File

@ -0,0 +1,144 @@
{
"plugins": [
"unicorn",
"typescript",
"oxc"
],
"categories": {},
"rules": {
"for-direction": "error",
"no-async-promise-executor": "error",
"no-caller": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "warn",
"no-const-assign": "warn",
"no-constant-binary-expression": "error",
"no-constant-condition": "warn",
"no-control-regex": "warn",
"no-debugger": "warn",
"no-delete-var": "warn",
"no-dupe-class-members": "warn",
"no-dupe-else-if": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-empty-character-class": "warn",
"no-empty-pattern": "warn",
"no-empty-static-block": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extra-boolean-cast": "warn",
"no-func-assign": "warn",
"no-global-assign": "warn",
"no-import-assign": "warn",
"no-invalid-regexp": "warn",
"no-irregular-whitespace": "warn",
"no-loss-of-precision": "warn",
"no-new-native-nonconstructor": "warn",
"no-nonoctal-decimal-escape": "warn",
"no-obj-calls": "warn",
"no-self-assign": "warn",
"no-setter-return": "warn",
"no-shadow-restricted-names": "warn",
"no-sparse-arrays": "warn",
"no-this-before-super": "warn",
"no-unassigned-vars": "warn",
"no-unsafe-finally": "warn",
"no-unsafe-negation": "warn",
"no-unsafe-optional-chaining": "warn",
"no-unused-labels": "warn",
"no-unused-private-class-members": "warn",
"no-unused-vars": "warn",
"no-useless-backreference": "warn",
"no-useless-catch": "error",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-with": "warn",
"require-yield": "warn",
"use-isnan": "warn",
"valid-typeof": "warn",
"oxc/bad-array-method-on-arguments": "warn",
"oxc/bad-char-at-comparison": "warn",
"oxc/bad-comparison-sequence": "warn",
"oxc/bad-min-max-func": "warn",
"oxc/bad-object-literal-comparison": "warn",
"oxc/bad-replace-all-arg": "warn",
"oxc/const-comparisons": "warn",
"oxc/double-comparisons": "warn",
"oxc/erasing-op": "warn",
"oxc/missing-throw": "warn",
"oxc/number-arg-out-of-range": "warn",
"oxc/only-used-in-recursion": "warn",
"oxc/uninvoked-array-callback": "warn",
"typescript/await-thenable": "warn",
"typescript/no-array-delete": "warn",
"typescript/no-base-to-string": "warn",
"typescript/no-confusing-void-expression": "warn",
"typescript/no-duplicate-enum-values": "warn",
"typescript/no-duplicate-type-constituents": "warn",
"typescript/no-extra-non-null-assertion": "warn",
"typescript/no-floating-promises": "warn",
"typescript/no-for-in-array": "warn",
"typescript/no-implied-eval": "warn",
"typescript/no-meaningless-void-operator": "warn",
"typescript/no-misused-new": "warn",
"typescript/no-misused-spread": "warn",
"typescript/no-non-null-asserted-optional-chain": "warn",
"typescript/no-redundant-type-constituents": "warn",
"typescript/no-this-alias": "warn",
"typescript/no-unnecessary-parameter-property-assignment": "warn",
"typescript/no-unsafe-declaration-merging": "warn",
"typescript/no-unsafe-unary-minus": "warn",
"typescript/no-useless-empty-export": "warn",
"typescript/no-wrapper-object-types": "warn",
"typescript/prefer-as-const": "warn",
"typescript/require-array-sort-compare": "warn",
"typescript/restrict-template-expressions": "warn",
"typescript/triple-slash-reference": "warn",
"typescript/unbound-method": "warn",
"unicorn/no-await-in-promise-methods": "warn",
"unicorn/no-empty-file": "warn",
"unicorn/no-invalid-fetch-options": "warn",
"unicorn/no-invalid-remove-event-listener": "warn",
"unicorn/no-new-array": "warn",
"unicorn/no-single-promise-in-promise-methods": "warn",
"unicorn/no-thenable": "warn",
"unicorn/no-unnecessary-await": "warn",
"unicorn/no-useless-fallback-in-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-spread": "warn",
"unicorn/prefer-set-size": "warn",
"unicorn/prefer-string-starts-ends-with": "warn"
},
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
},
"react": {
"formComponents": [],
"linkComponents": []
},
"jsdoc": {
"ignorePrivate": false,
"ignoreInternal": false,
"ignoreReplacesDocs": true,
"overrideReplacesDocs": true,
"augmentsExtendsReplacesDocs": false,
"implementsReplacesDocs": false,
"exemptDestructuredRootsFromChecks": false,
"tagNamePreference": {}
}
},
"env": {
"builtin": true
},
"globals": {},
"ignorePatterns": [
"**/*.js"
]
}

View File

@ -82,7 +82,7 @@ export default function CheckCode() {
<form action="">
<input type='text' className='hidden' />
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>

View File

@ -104,7 +104,7 @@ export default function CheckCode() {
<form action="">
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') || ''} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>

View File

@ -215,7 +215,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
}
if (item.number) {
const convertedNumber = Number(initInputs[item.number.variable]) ?? undefined
const convertedNumber = Number(initInputs[item.number.variable])
return {
...item.number,
default: convertedNumber || item.default || item.number.default,

View File

@ -188,7 +188,7 @@ export const useEmbeddedChatbot = () => {
}
}
if (item.number) {
const convertedNumber = Number(initInputs[item.number.variable]) ?? undefined
const convertedNumber = Number(initInputs[item.number.variable])
return {
...item.number,
default: convertedNumber || item.default || item.number.default,

View File

@ -9,17 +9,34 @@ import { isValidUrl } from './utils'
const Link = ({ node, children, ...props }: any) => {
const { onSend } = useChatContext()
const commonClassName = 'cursor-pointer underline !decoration-primary-700 decoration-dashed'
if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) {
const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1])
return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr>
return <abbr className={commonClassName} onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr>
}
else {
const href = props.href || node.properties?.href
if(!href || !isValidUrl(href))
if (href && /^#[a-zA-Z0-9_\-]+$/.test(href.toString())) {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault()
// scroll to target element if exists within the answer container
const answerContainer = e.currentTarget.closest('.chat-answer-container')
if (answerContainer) {
const targetId = CSS.escape(href.toString().substring(1))
const targetElement = answerContainer.querySelector(`[id="${targetId}"]`)
if (targetElement)
targetElement.scrollIntoView({ behavior: 'smooth' })
}
}
return <a href={href} onClick={handleClick} className={commonClassName}>{children || 'ScrollView'}</a>
}
if (!href || !isValidUrl(href))
return <span>{children}</span>
return <a href={href} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a>
return <a href={href} target="_blank" rel="noopener noreferrer" className={commonClassName}>{children || 'Download'}</a>
}
}

View File

@ -186,12 +186,12 @@ const ParameterItem: FC<ParameterItemProps> = ({
if (parameterRule.type === 'boolean') {
return (
<Radio.Group
className='flex w-[178px] items-center'
className='flex w-[150px] items-center'
value={renderValue as boolean}
onChange={handleRadioChange}
>
<Radio value={true} className='w-[83px]'>True</Radio>
<Radio value={false} className='w-[83px]'>False</Radio>
<Radio value={true} className='w-[70px] px-[18px]'>True</Radio>
<Radio value={false} className='w-[70px] px-[18px]'>False</Radio>
</Radio.Group>
)
}
@ -199,7 +199,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
if (parameterRule.type === 'string' && !parameterRule.options?.length) {
return (
<input
className={cn(isInWorkflow ? 'w-[178px]' : 'w-full', 'system-sm-regular ml-4 flex h-8 appearance-none items-center rounded-lg bg-components-input-bg-normal px-3 text-components-input-text-filled outline-none')}
className={cn(isInWorkflow ? 'w-[150px]' : 'w-full', 'system-sm-regular ml-4 flex h-8 appearance-none items-center rounded-lg bg-components-input-bg-normal px-3 text-components-input-text-filled outline-none')}
value={renderValue as string}
onChange={handleStringInputChange}
/>
@ -270,7 +270,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
parameterRule.help && (
<Tooltip
popupContent={(
<div className='w-[178px] whitespace-pre-wrap'>{parameterRule.help[language] || parameterRule.help.en_US}</div>
<div className='w-[150px] whitespace-pre-wrap'>{parameterRule.help[language] || parameterRule.help.en_US}</div>
)}
popupClassName='mr-1'
triggerClassName='mr-1 w-4 h-4 shrink-0'
@ -280,7 +280,7 @@ const ParameterItem: FC<ParameterItemProps> = ({
</div>
{
parameterRule.type === 'tag' && (
<div className={cn(!isInWorkflow && 'w-[178px]', 'system-xs-regular text-text-tertiary')}>
<div className={cn(!isInWorkflow && 'w-[150px]', 'system-xs-regular text-text-tertiary')}>
{parameterRule?.tagPlaceholder?.[language]}
</div>
)

View File

@ -51,7 +51,7 @@ const nodeDefault: NodeDefault<ListFilterNodeType> = {
if (!errorMessages && !filter_by.conditions[0]?.comparison_operator)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonOperator') })
if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && (item_var_type === VarType.boolean ? !filter_by.conditions[0]?.value === undefined : !filter_by.conditions[0]?.value))
if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && (item_var_type === VarType.boolean ? filter_by.conditions[0]?.value === undefined : !filter_by.conditions[0]?.value))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonValue') })
}

View File

@ -82,7 +82,7 @@ export default function CheckCode() {
<form action="">
<input type='text' className='hidden' />
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>

View File

@ -89,7 +89,7 @@ export default function CheckCode() {
<form action="">
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Input value={code} onChange={e => setVerifyCode(e.target.value)} maxLength={6} className='mt-1' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>

View File

@ -193,15 +193,15 @@ const translation = {
confirm: 'افزودن و مجوزدهی',
timeout: 'مهلت',
sseReadTimeout: 'زمان.out خواندن SSE',
headers: 'عناوین',
timeoutPlaceholder: 'سی',
headers: 'هدرها',
timeoutPlaceholder: '30',
headerKey: 'نام هدر',
headerValue: 'مقدار هدر',
addHeader: 'هدر اضافه کنید',
headerKeyPlaceholder: 'به عنوان مثال، مجوز',
headerValuePlaceholder: 'مثلاً، توکن حامل ۱۲۳',
headerKeyPlaceholder: 'Authorization',
headerValuePlaceholder: 'مثلاً، Bearer 123',
noHeaders: 'هیچ هدر سفارشی پیکربندی نشده است',
headersTip: 'سرفصل‌های اضافی HTTP برای ارسال با درخواست‌های سرور MCP',
headersTip: 'هدرهای HTTP اضافی برای ارسال با درخواست‌های سرور MCP',
maskedHeadersTip: 'مقدارهای هدر به خاطر امنیت مخفی شده‌اند. تغییرات مقادیر واقعی را به‌روزرسانی خواهد کرد.',
},
delete: 'حذف سرور MCP',

View File

@ -176,13 +176,13 @@ const translation = {
serverIdentifierPlaceholder: 'Pengidentifikasi unik, misalnya, my-mcp-server',
serverUrl: 'Server URL',
headers: 'Header',
timeoutPlaceholder: 'tiga puluh',
timeoutPlaceholder: '30',
addHeader: 'Tambahkan Judul',
headerKey: 'Nama Header',
headerValue: 'Nilai Header',
headersTip: 'Header HTTP tambahan untuk dikirim bersama permintaan server MCP',
headerKeyPlaceholder: 'misalnya, Otorisasi',
headerValuePlaceholder: 'misalnya, Token Pengganti 123',
headerKeyPlaceholder: 'Authorization',
headerValuePlaceholder: 'Bearer 123',
noHeaders: 'Tidak ada header kustom yang dikonfigurasi',
maskedHeadersTip: 'Nilai header disembunyikan untuk keamanan. Perubahan akan memperbarui nilai yang sebenarnya.',
},

View File

@ -193,15 +193,15 @@ const translation = {
confirm: 'Dodaj in avtoriziraj',
timeout: 'Časovna omejitev',
sseReadTimeout: 'SSE časovna omejitev branja',
timeoutPlaceholder: 'trideset',
headers: 'Naslovi',
headerKeyPlaceholder: 'npr., Pooblastitev',
timeoutPlaceholder: '30',
headers: 'Glave',
headerKeyPlaceholder: 'npr., Authorization',
headerValue: 'Vrednost glave',
headerKey: 'Ime glave',
addHeader: 'Dodaj naslov',
addHeader: 'Dodaj glavo',
headersTip: 'Dodatni HTTP glavi za poslati z zahtevami MCP strežnika',
headerValuePlaceholder: 'npr., nosilec žeton123',
noHeaders: 'Nobenih prilagojenih glave ni konfiguriranih',
headerValuePlaceholder: 'npr., Bearer žeton123',
noHeaders: 'Nobena prilagojena glava ni konfigurirana',
maskedHeadersTip: 'Vrednosti glave so zakrite zaradi varnosti. Spremembe bodo posodobile dejanske vrednosti.',
},
delete: 'Odstrani strežnik MCP',