소스 검색

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

tags/2.0.0-beta.1
-LAN- 1 개월 전
부모
커밋
07109846e0
No account linked to committer's email address

+ 13
- 3
.github/workflows/translate-i18n-base-on-english.yml 파일 보기

@@ -67,12 +67,22 @@ jobs:
working-directory: ./web
run: pnpm run auto-gen-i18n ${{ env.FILE_ARGS }}

- name: Generate i18n type definitions
if: env.FILES_CHANGED == 'true'
working-directory: ./web
run: pnpm run gen:i18n-types

- name: Create Pull Request
if: env.FILES_CHANGED == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update i18n files based on en-US changes
title: 'chore: translate i18n files'
body: This PR was automatically created to update i18n files based on changes in en-US locale.
commit-message: Update i18n files and type definitions based on en-US changes
title: 'chore: translate i18n files and update type definitions'
body: |
This PR was automatically created to update i18n files and TypeScript type definitions based on changes in en-US locale.
**Changes included:**
- Updated translation files for all locales
- Regenerated TypeScript type definitions for type safety
branch: chore/automated-i18n-updates

+ 5
- 0
.github/workflows/web-tests.yml 파일 보기

@@ -47,6 +47,11 @@ jobs:
working-directory: ./web
run: pnpm install --frozen-lockfile

- name: Check i18n types synchronization
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
run: pnpm run check:i18n-types

- name: Run tests
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web

+ 7
- 5
api/core/plugin/backwards_invocation/app.py 파일 보기

@@ -2,6 +2,7 @@ from collections.abc import Generator, Mapping
from typing import Optional, Union

from sqlalchemy import select
from sqlalchemy.orm import Session

from controllers.service_api.wraps import create_or_update_end_user_for_user_id
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
@@ -193,11 +194,12 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation):
"""
get the user by user id
"""
stmt = select(EndUser).where(EndUser.id == user_id)
user = db.session.scalar(stmt)
if not user:
stmt = select(Account).where(Account.id == user_id)
user = db.session.scalar(stmt)
with Session(db.engine, expire_on_commit=False) as session:
stmt = select(EndUser).where(EndUser.id == user_id)
user = session.scalar(stmt)
if not user:
stmt = select(Account).where(Account.id == user_id)
user = session.scalar(stmt)

if not user:
raise ValueError("user not found")

+ 41
- 2
web/app/components/workflow/hooks/use-nodes-interactions.ts 파일 보기

@@ -38,6 +38,7 @@ import {
import {
genNewNodeTitleFromOld,
generateNewNode,
getNestedNodePosition,
getNodeCustomTypeByNodeDataType,
getNodesConnectedSourceOrTargetHandleIdsMap,
getTopLeftNodePosition,
@@ -1307,8 +1308,7 @@ export const useNodesInteractions = () => {
})
newChildren.push(newIterationStartNode!)
}

if (nodeToPaste.data.type === BlockEnum.Loop) {
else if (nodeToPaste.data.type === BlockEnum.Loop) {
newLoopStartNode!.parentId = newNode.id;
(newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id

@@ -1318,6 +1318,44 @@ export const useNodesInteractions = () => {
})
newChildren.push(newLoopStartNode!)
}
else {
// single node paste
const selectedNode = nodes.find(node => node.selected)
if (selectedNode) {
const commonNestedDisallowPasteNodes = [
// end node only can be placed outermost layer
BlockEnum.End,
]

// handle disallow paste node
if (commonNestedDisallowPasteNodes.includes(nodeToPaste.data.type))
return

// handle paste to nested block
if (selectedNode.data.type === BlockEnum.Iteration) {
newNode.data.isInIteration = true
newNode.data.iteration_id = selectedNode.data.iteration_id
newNode.parentId = selectedNode.id
newNode.positionAbsolute = {
x: newNode.position.x,
y: newNode.position.y,
}
// set position base on parent node
newNode.position = getNestedNodePosition(newNode, selectedNode)
}
else if (selectedNode.data.type === BlockEnum.Loop) {
newNode.data.isInLoop = true
newNode.data.loop_id = selectedNode.data.loop_id
newNode.parentId = selectedNode.id
newNode.positionAbsolute = {
x: newNode.position.x,
y: newNode.position.y,
}
// set position base on parent node
newNode.position = getNestedNodePosition(newNode, selectedNode)
}
}
}

nodesToPaste.push(newNode)

@@ -1325,6 +1363,7 @@ export const useNodesInteractions = () => {
nodesToPaste.push(...newChildren)
})

// only handle edge when paste nested block
edges.forEach((edge) => {
const sourceId = idMapping[edge.source]
const targetId = idMapping[edge.target]

+ 7
- 0
web/app/components/workflow/utils/node.ts 파일 보기

@@ -135,6 +135,13 @@ export const getTopLeftNodePosition = (nodes: Node[]) => {
}
}

export const getNestedNodePosition = (node: Node, parentNode: Node) => {
return {
x: node.position.x - parentNode.position.x,
y: node.position.y - parentNode.position.y,
}
}

export const hasRetryNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
}

+ 2
- 0
web/global.d.ts 파일 보기

@@ -8,3 +8,5 @@ declare module '*.mdx' {
let MDXComponent: (props: any) => JSX.Element
export default MDXComponent
}

import './types/i18n'

+ 120
- 0
web/i18n-config/check-i18n-sync.js 파일 보기

@@ -0,0 +1,120 @@
#!/usr/bin/env node

const fs = require('fs')
const path = require('path')
const { camelCase } = require('lodash')

// Import the NAMESPACES array from i18next-config.ts
function getNamespacesFromConfig() {
const configPath = path.join(__dirname, 'i18next-config.ts')
const configContent = fs.readFileSync(configPath, 'utf8')
// Extract NAMESPACES array using regex
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
if (!namespacesMatch) {
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
}
// Parse the namespaces
const namespacesStr = namespacesMatch[1]
const namespaces = namespacesStr
.split(',')
.map(line => line.trim())
.filter(line => line.startsWith("'") || line.startsWith('"'))
.map(line => line.slice(1, -1)) // Remove quotes
return namespaces
}

function getNamespacesFromTypes() {
const typesPath = path.join(__dirname, '../types/i18n.d.ts')
if (!fs.existsSync(typesPath)) {
return null
}
const typesContent = fs.readFileSync(typesPath, 'utf8')
// Extract namespaces from Messages type
const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/)
if (!messagesMatch) {
return null
}
// Parse the properties
const propertiesStr = messagesMatch[1]
const properties = propertiesStr
.split('\n')
.map(line => line.trim())
.filter(line => line.includes(':'))
.map(line => line.split(':')[0].trim())
.filter(prop => prop.length > 0)
return properties
}

function main() {
try {
console.log('🔍 Checking i18n types synchronization...')
// Get namespaces from config
const configNamespaces = getNamespacesFromConfig()
console.log(`📦 Found ${configNamespaces.length} namespaces in config`)
// Convert to camelCase for comparison
const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort()
// Get namespaces from type definitions
const typeNamespaces = getNamespacesFromTypes()
if (!typeNamespaces) {
console.error('❌ Type definitions file not found or invalid')
console.error(' Run: pnpm run gen:i18n-types')
process.exit(1)
}
console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`)
const typeCamelCase = typeNamespaces.sort()
// Compare arrays
const configSet = new Set(configCamelCase)
const typeSet = new Set(typeCamelCase)
// Find missing in types
const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns))
// Find extra in types
const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns))
let hasErrors = false
if (missingInTypes.length > 0) {
hasErrors = true
console.error('❌ Missing in type definitions:')
missingInTypes.forEach(ns => console.error(` - ${ns}`))
}
if (extraInTypes.length > 0) {
hasErrors = true
console.error('❌ Extra in type definitions:')
extraInTypes.forEach(ns => console.error(` - ${ns}`))
}
if (hasErrors) {
console.error('\n💡 To fix synchronization issues:')
console.error(' Run: pnpm run gen:i18n-types')
process.exit(1)
}
console.log('✅ i18n types are synchronized')
} catch (error) {
console.error('❌ Error:', error.message)
process.exit(1)
}
}

if (require.main === module) {
main()
}

+ 135
- 0
web/i18n-config/generate-i18n-types.js 파일 보기

@@ -0,0 +1,135 @@
#!/usr/bin/env node

const fs = require('fs')
const path = require('path')
const { camelCase } = require('lodash')

// Import the NAMESPACES array from i18next-config.ts
function getNamespacesFromConfig() {
const configPath = path.join(__dirname, 'i18next-config.ts')
const configContent = fs.readFileSync(configPath, 'utf8')
// Extract NAMESPACES array using regex
const namespacesMatch = configContent.match(/const NAMESPACES = \[([\s\S]*?)\]/)
if (!namespacesMatch) {
throw new Error('Could not find NAMESPACES array in i18next-config.ts')
}
// Parse the namespaces
const namespacesStr = namespacesMatch[1]
const namespaces = namespacesStr
.split(',')
.map(line => line.trim())
.filter(line => line.startsWith("'") || line.startsWith('"'))
.map(line => line.slice(1, -1)) // Remove quotes
return namespaces
}

function generateTypeDefinitions(namespaces) {
const header = `// TypeScript type definitions for Dify's i18next configuration
// This file is auto-generated. Do not edit manually.
// To regenerate, run: pnpm run gen:i18n-types
import 'react-i18next'

// Extract types from translation files using typeof import pattern`

// Generate individual type definitions
const typeDefinitions = namespaces.map(namespace => {
const typeName = camelCase(namespace).replace(/^\w/, c => c.toUpperCase()) + 'Messages'
return `type ${typeName} = typeof import('../i18n/en-US/${namespace}').default`
}).join('\n')

// Generate Messages interface
const messagesInterface = `
// Complete type structure that matches i18next-config.ts camelCase conversion
export type Messages = {
${namespaces.map(namespace => {
const camelCased = camelCase(namespace)
const typeName = camelCase(namespace).replace(/^\w/, c => c.toUpperCase()) + 'Messages'
return ` ${camelCased}: ${typeName};`
}).join('\n')}
}`

const utilityTypes = `
// Utility type to flatten nested object keys into dot notation
type FlattenKeys<T> = T extends object
? {
[K in keyof T]: T[K] extends object
? \`\${K & string}.\${FlattenKeys<T[K]> & string}\`
: \`\${K & string}\`
}[keyof T]
: never

export type ValidTranslationKeys = FlattenKeys<Messages>`

const moduleDeclarations = `
// Extend react-i18next with Dify's type structure
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: Messages;
};
}
}

// Extend i18next for complete type safety
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: Messages;
};
}
}`

return [header, typeDefinitions, messagesInterface, utilityTypes, moduleDeclarations].join('\n\n')
}

function main() {
const args = process.argv.slice(2)
const checkMode = args.includes('--check')
try {
console.log('📦 Generating i18n type definitions...')
// Get namespaces from config
const namespaces = getNamespacesFromConfig()
console.log(`✅ Found ${namespaces.length} namespaces`)
// Generate type definitions
const typeDefinitions = generateTypeDefinitions(namespaces)
const outputPath = path.join(__dirname, '../types/i18n.d.ts')
if (checkMode) {
// Check mode: compare with existing file
if (!fs.existsSync(outputPath)) {
console.error('❌ Type definitions file does not exist')
process.exit(1)
}
const existingContent = fs.readFileSync(outputPath, 'utf8')
if (existingContent.trim() !== typeDefinitions.trim()) {
console.error('❌ Type definitions are out of sync')
console.error(' Run: pnpm run gen:i18n-types')
process.exit(1)
}
console.log('✅ Type definitions are in sync')
} else {
// Generate mode: write file
fs.writeFileSync(outputPath, typeDefinitions)
console.log(`✅ Generated type definitions: ${outputPath}`)
}
} catch (error) {
console.error('❌ Error:', error.message)
process.exit(1)
}
}

if (require.main === module) {
main()
}

+ 2
- 0
web/package.json 파일 보기

@@ -35,6 +35,8 @@
"uglify-embed": "node ./bin/uglify-embed",
"check-i18n": "node ./i18n-config/check-i18n.js",
"auto-gen-i18n": "node ./i18n-config/auto-gen-i18n.js",
"gen:i18n-types": "node ./i18n-config/generate-i18n-types.js",
"check:i18n-types": "node ./i18n-config/check-i18n-sync.js",
"test": "jest",
"test:watch": "jest --watch",
"storybook": "storybook dev -p 6006",

+ 96
- 0
web/types/i18n.d.ts 파일 보기

@@ -0,0 +1,96 @@
// TypeScript type definitions for Dify's i18next configuration
// This file is auto-generated. Do not edit manually.
// To regenerate, run: pnpm run gen:i18n-types
import 'react-i18next'

// Extract types from translation files using typeof import pattern

type AppAnnotationMessages = typeof import('../i18n/en-US/app-annotation').default
type AppApiMessages = typeof import('../i18n/en-US/app-api').default
type AppDebugMessages = typeof import('../i18n/en-US/app-debug').default
type AppLogMessages = typeof import('../i18n/en-US/app-log').default
type AppOverviewMessages = typeof import('../i18n/en-US/app-overview').default
type AppMessages = typeof import('../i18n/en-US/app').default
type BillingMessages = typeof import('../i18n/en-US/billing').default
type CommonMessages = typeof import('../i18n/en-US/common').default
type CustomMessages = typeof import('../i18n/en-US/custom').default
type DatasetCreationMessages = typeof import('../i18n/en-US/dataset-creation').default
type DatasetDocumentsMessages = typeof import('../i18n/en-US/dataset-documents').default
type DatasetHitTestingMessages = typeof import('../i18n/en-US/dataset-hit-testing').default
type DatasetSettingsMessages = typeof import('../i18n/en-US/dataset-settings').default
type DatasetMessages = typeof import('../i18n/en-US/dataset').default
type EducationMessages = typeof import('../i18n/en-US/education').default
type ExploreMessages = typeof import('../i18n/en-US/explore').default
type LayoutMessages = typeof import('../i18n/en-US/layout').default
type LoginMessages = typeof import('../i18n/en-US/login').default
type OauthMessages = typeof import('../i18n/en-US/oauth').default
type PluginTagsMessages = typeof import('../i18n/en-US/plugin-tags').default
type PluginMessages = typeof import('../i18n/en-US/plugin').default
type RegisterMessages = typeof import('../i18n/en-US/register').default
type RunLogMessages = typeof import('../i18n/en-US/run-log').default
type ShareMessages = typeof import('../i18n/en-US/share').default
type TimeMessages = typeof import('../i18n/en-US/time').default
type ToolsMessages = typeof import('../i18n/en-US/tools').default
type WorkflowMessages = typeof import('../i18n/en-US/workflow').default

// Complete type structure that matches i18next-config.ts camelCase conversion
export type Messages = {
appAnnotation: AppAnnotationMessages;
appApi: AppApiMessages;
appDebug: AppDebugMessages;
appLog: AppLogMessages;
appOverview: AppOverviewMessages;
app: AppMessages;
billing: BillingMessages;
common: CommonMessages;
custom: CustomMessages;
datasetCreation: DatasetCreationMessages;
datasetDocuments: DatasetDocumentsMessages;
datasetHitTesting: DatasetHitTestingMessages;
datasetSettings: DatasetSettingsMessages;
dataset: DatasetMessages;
education: EducationMessages;
explore: ExploreMessages;
layout: LayoutMessages;
login: LoginMessages;
oauth: OauthMessages;
pluginTags: PluginTagsMessages;
plugin: PluginMessages;
register: RegisterMessages;
runLog: RunLogMessages;
share: ShareMessages;
time: TimeMessages;
tools: ToolsMessages;
workflow: WorkflowMessages;
}

// Utility type to flatten nested object keys into dot notation
type FlattenKeys<T> = T extends object
? {
[K in keyof T]: T[K] extends object
? `${K & string}.${FlattenKeys<T[K]> & string}`
: `${K & string}`
}[keyof T]
: never

export type ValidTranslationKeys = FlattenKeys<Messages>

// Extend react-i18next with Dify's type structure
declare module 'react-i18next' {
type CustomTypeOptions = {
defaultNS: 'translation';
resources: {
translation: Messages;
};
}
}

// Extend i18next for complete type safety
declare module 'i18next' {
type CustomTypeOptions = {
defaultNS: 'translation';
resources: {
translation: Messages;
};
}
}

Loading…
취소
저장