Skip to content

Commit 2b7ceed

Browse files
authored
Merge branch 'langgenius:main' into main
2 parents d104121 + 5362f69 commit 2b7ceed

501 files changed

Lines changed: 4963 additions & 2876 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/frontend-testing/assets/component-test.template.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,14 @@ import userEvent from '@testing-library/user-event'
2828

2929
// i18n (automatically mocked)
3030
// WHY: Global mock in web/vitest.setup.ts is auto-loaded by Vitest setup
31-
// No explicit mock needed - it returns translation keys as-is
31+
// The global mock provides: useTranslation, Trans, useMixedTranslation, useGetLanguage
32+
// No explicit mock needed for most tests
33+
//
3234
// Override only if custom translations are required:
33-
// vi.mock('react-i18next', () => ({
34-
// useTranslation: () => ({
35-
// t: (key: string) => {
36-
// const customTranslations: Record<string, string> = {
37-
// 'my.custom.key': 'Custom Translation',
38-
// }
39-
// return customTranslations[key] || key
40-
// },
41-
// }),
35+
// import { createReactI18nextMock } from '@/test/i18n-mock'
36+
// vi.mock('react-i18next', () => createReactI18nextMock({
37+
// 'my.custom.key': 'Custom Translation',
38+
// 'button.save': 'Save',
4239
// }))
4340

4441
// Router (if component uses useRouter, usePathname, useSearchParams)

.claude/skills/frontend-testing/references/mocking.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,29 @@ Modules are not mocked automatically. Use `vi.mock` in test files, or add global
5252
### 1. i18n (Auto-loaded via Global Mock)
5353

5454
A global mock is defined in `web/vitest.setup.ts` and is auto-loaded by Vitest setup.
55-
**No explicit mock needed** for most tests - it returns translation keys as-is.
5655

57-
For tests requiring custom translations, override the mock:
56+
The global mock provides:
57+
58+
- `useTranslation` - returns translation keys with namespace prefix
59+
- `Trans` component - renders i18nKey and components
60+
- `useMixedTranslation` (from `@/app/components/plugins/marketplace/hooks`)
61+
- `useGetLanguage` (from `@/context/i18n`) - returns `'en-US'`
62+
63+
**Default behavior**: Most tests should use the global mock (no local override needed).
64+
65+
**For custom translations**: Use the helper function from `@/test/i18n-mock`:
5866

5967
```typescript
60-
vi.mock('react-i18next', () => ({
61-
useTranslation: () => ({
62-
t: (key: string) => {
63-
const translations: Record<string, string> = {
64-
'my.custom.key': 'Custom translation',
65-
}
66-
return translations[key] || key
67-
},
68-
}),
68+
import { createReactI18nextMock } from '@/test/i18n-mock'
69+
70+
vi.mock('react-i18next', () => createReactI18nextMock({
71+
'my.custom.key': 'Custom translation',
72+
'button.save': 'Save',
6973
}))
7074
```
7175

76+
**Avoid**: Manually defining `useTranslation` mocks that just return the key - the global mock already does this.
77+
7278
### 2. Next.js Router
7379

7480
```typescript

.github/workflows/style.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ jobs:
110110
working-directory: ./web
111111
run: pnpm run type-check:tsgo
112112

113+
- name: Web dead code check
114+
if: steps.changed-files.outputs.any_changed == 'true'
115+
working-directory: ./web
116+
run: pnpm run knip
117+
118+
- name: Web build check
119+
if: steps.changed-files.outputs.any_changed == 'true'
120+
working-directory: ./web
121+
run: pnpm run build
122+
113123
superlinter:
114124
name: SuperLinter
115125
runs-on: ubuntu-latest

api/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ S3_ACCESS_KEY=your-access-key
101101
S3_SECRET_KEY=your-secret-key
102102
S3_REGION=your-region
103103

104+
# Workflow run and Conversation archive storage (S3-compatible)
105+
ARCHIVE_STORAGE_ENABLED=false
106+
ARCHIVE_STORAGE_ENDPOINT=
107+
ARCHIVE_STORAGE_ARCHIVE_BUCKET=
108+
ARCHIVE_STORAGE_EXPORT_BUCKET=
109+
ARCHIVE_STORAGE_ACCESS_KEY=
110+
ARCHIVE_STORAGE_SECRET_KEY=
111+
ARCHIVE_STORAGE_REGION=auto
112+
104113
# Azure Blob Storage configuration
105114
AZURE_BLOB_ACCOUNT_NAME=your-account-name
106115
AZURE_BLOB_ACCOUNT_KEY=your-account-key
@@ -493,6 +502,8 @@ LOG_FILE_BACKUP_COUNT=5
493502
LOG_DATEFORMAT=%Y-%m-%d %H:%M:%S
494503
# Log Timezone
495504
LOG_TZ=UTC
505+
# Log output format: text or json
506+
LOG_OUTPUT_FORMAT=text
496507
# Log format
497508
LOG_FORMAT=%(asctime)s,%(msecs)d %(levelname)-2s [%(filename)s:%(lineno)d] %(req_id)s %(message)s
498509

api/.ruff.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
exclude = ["migrations/*"]
1+
exclude = [
2+
"migrations/*",
3+
".git",
4+
".git/**",
5+
]
26
line-length = 120
37

48
[format]

api/app_factory.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import time
33

44
from opentelemetry.trace import get_current_span
5+
from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID
56

67
from configs import dify_config
78
from contexts.wrapper import RecyclableContextVar
9+
from core.logging.context import init_request_context
810
from dify_app import DifyApp
911

1012
logger = logging.getLogger(__name__)
@@ -25,28 +27,35 @@ def create_flask_app_with_configs() -> DifyApp:
2527
# add before request hook
2628
@dify_app.before_request
2729
def before_request():
28-
# add an unique identifier to each request
30+
# Initialize logging context for this request
31+
init_request_context()
2932
RecyclableContextVar.increment_thread_recycles()
3033

31-
# add after request hook for injecting X-Trace-Id header from OpenTelemetry span context
34+
# add after request hook for injecting trace headers from OpenTelemetry span context
35+
# Only adds headers when OTEL is enabled and has valid context
3236
@dify_app.after_request
33-
def add_trace_id_header(response):
37+
def add_trace_headers(response):
3438
try:
3539
span = get_current_span()
3640
ctx = span.get_span_context() if span else None
37-
if ctx and ctx.is_valid:
38-
trace_id_hex = format(ctx.trace_id, "032x")
39-
# Avoid duplicates if some middleware added it
40-
if "X-Trace-Id" not in response.headers:
41-
response.headers["X-Trace-Id"] = trace_id_hex
41+
42+
if not ctx or not ctx.is_valid:
43+
return response
44+
45+
# Inject trace headers from OTEL context
46+
if ctx.trace_id != INVALID_TRACE_ID and "X-Trace-Id" not in response.headers:
47+
response.headers["X-Trace-Id"] = format(ctx.trace_id, "032x")
48+
if ctx.span_id != INVALID_SPAN_ID and "X-Span-Id" not in response.headers:
49+
response.headers["X-Span-Id"] = format(ctx.span_id, "016x")
50+
4251
except Exception:
4352
# Never break the response due to tracing header injection
44-
logger.warning("Failed to add trace ID to response header", exc_info=True)
53+
logger.warning("Failed to add trace headers to response", exc_info=True)
4554
return response
4655

4756
# Capture the decorator's return value to avoid pyright reportUnusedFunction
4857
_ = before_request
49-
_ = add_trace_id_header
58+
_ = add_trace_headers
5059

5160
return dify_app
5261

api/configs/extra/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from configs.extra.archive_config import ArchiveStorageConfig
12
from configs.extra.notion_config import NotionConfig
23
from configs.extra.sentry_config import SentryConfig
34

45

56
class ExtraServiceConfig(
67
# place the configs in alphabet order
8+
ArchiveStorageConfig,
79
NotionConfig,
810
SentryConfig,
911
):
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from pydantic import Field
2+
from pydantic_settings import BaseSettings
3+
4+
5+
class ArchiveStorageConfig(BaseSettings):
6+
"""
7+
Configuration settings for workflow run logs archiving storage.
8+
"""
9+
10+
ARCHIVE_STORAGE_ENABLED: bool = Field(
11+
description="Enable workflow run logs archiving to S3-compatible storage",
12+
default=False,
13+
)
14+
15+
ARCHIVE_STORAGE_ENDPOINT: str | None = Field(
16+
description="URL of the S3-compatible storage endpoint (e.g., 'https://storage.example.com')",
17+
default=None,
18+
)
19+
20+
ARCHIVE_STORAGE_ARCHIVE_BUCKET: str | None = Field(
21+
description="Name of the bucket to store archived workflow logs",
22+
default=None,
23+
)
24+
25+
ARCHIVE_STORAGE_EXPORT_BUCKET: str | None = Field(
26+
description="Name of the bucket to store exported workflow runs",
27+
default=None,
28+
)
29+
30+
ARCHIVE_STORAGE_ACCESS_KEY: str | None = Field(
31+
description="Access key ID for authenticating with storage",
32+
default=None,
33+
)
34+
35+
ARCHIVE_STORAGE_SECRET_KEY: str | None = Field(
36+
description="Secret access key for authenticating with storage",
37+
default=None,
38+
)
39+
40+
ARCHIVE_STORAGE_REGION: str = Field(
41+
description="Region for storage (use 'auto' if the provider supports it)",
42+
default="auto",
43+
)

api/configs/feature/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,11 @@ class LoggingConfig(BaseSettings):
587587
default="INFO",
588588
)
589589

590+
LOG_OUTPUT_FORMAT: Literal["text", "json"] = Field(
591+
description="Log output format: 'text' for human-readable, 'json' for structured JSON logs.",
592+
default="text",
593+
)
594+
590595
LOG_FILE: str | None = Field(
591596
description="File path for log output.",
592597
default=None,

api/controllers/common/fields.py

Lines changed: 59 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,59 @@
1-
from flask_restx import Api, Namespace, fields
2-
3-
from libs.helper import AppIconUrlField
4-
5-
parameters__system_parameters = {
6-
"image_file_size_limit": fields.Integer,
7-
"video_file_size_limit": fields.Integer,
8-
"audio_file_size_limit": fields.Integer,
9-
"file_size_limit": fields.Integer,
10-
"workflow_file_upload_limit": fields.Integer,
11-
}
12-
13-
14-
def build_system_parameters_model(api_or_ns: Api | Namespace):
15-
"""Build the system parameters model for the API or Namespace."""
16-
return api_or_ns.model("SystemParameters", parameters__system_parameters)
17-
18-
19-
parameters_fields = {
20-
"opening_statement": fields.String,
21-
"suggested_questions": fields.Raw,
22-
"suggested_questions_after_answer": fields.Raw,
23-
"speech_to_text": fields.Raw,
24-
"text_to_speech": fields.Raw,
25-
"retriever_resource": fields.Raw,
26-
"annotation_reply": fields.Raw,
27-
"more_like_this": fields.Raw,
28-
"user_input_form": fields.Raw,
29-
"sensitive_word_avoidance": fields.Raw,
30-
"file_upload": fields.Raw,
31-
"system_parameters": fields.Nested(parameters__system_parameters),
32-
}
33-
34-
35-
def build_parameters_model(api_or_ns: Api | Namespace):
36-
"""Build the parameters model for the API or Namespace."""
37-
copied_fields = parameters_fields.copy()
38-
copied_fields["system_parameters"] = fields.Nested(build_system_parameters_model(api_or_ns))
39-
return api_or_ns.model("Parameters", copied_fields)
40-
41-
42-
site_fields = {
43-
"title": fields.String,
44-
"chat_color_theme": fields.String,
45-
"chat_color_theme_inverted": fields.Boolean,
46-
"icon_type": fields.String,
47-
"icon": fields.String,
48-
"icon_background": fields.String,
49-
"icon_url": AppIconUrlField,
50-
"description": fields.String,
51-
"copyright": fields.String,
52-
"privacy_policy": fields.String,
53-
"custom_disclaimer": fields.String,
54-
"default_language": fields.String,
55-
"show_workflow_steps": fields.Boolean,
56-
"use_icon_as_answer_icon": fields.Boolean,
57-
}
58-
59-
60-
def build_site_model(api_or_ns: Api | Namespace):
61-
"""Build the site model for the API or Namespace."""
62-
return api_or_ns.model("Site", site_fields)
1+
from __future__ import annotations
2+
3+
from typing import Any, TypeAlias
4+
5+
from pydantic import BaseModel, ConfigDict, computed_field
6+
7+
from core.file import helpers as file_helpers
8+
from models.model import IconType
9+
10+
JSONValue: TypeAlias = str | int | float | bool | None | dict[str, Any] | list[Any]
11+
JSONObject: TypeAlias = dict[str, Any]
12+
13+
14+
class SystemParameters(BaseModel):
15+
image_file_size_limit: int
16+
video_file_size_limit: int
17+
audio_file_size_limit: int
18+
file_size_limit: int
19+
workflow_file_upload_limit: int
20+
21+
22+
class Parameters(BaseModel):
23+
opening_statement: str | None = None
24+
suggested_questions: list[str]
25+
suggested_questions_after_answer: JSONObject
26+
speech_to_text: JSONObject
27+
text_to_speech: JSONObject
28+
retriever_resource: JSONObject
29+
annotation_reply: JSONObject
30+
more_like_this: JSONObject
31+
user_input_form: list[JSONObject]
32+
sensitive_word_avoidance: JSONObject
33+
file_upload: JSONObject
34+
system_parameters: SystemParameters
35+
36+
37+
class Site(BaseModel):
38+
model_config = ConfigDict(from_attributes=True)
39+
40+
title: str
41+
chat_color_theme: str | None = None
42+
chat_color_theme_inverted: bool
43+
icon_type: str | None = None
44+
icon: str | None = None
45+
icon_background: str | None = None
46+
description: str | None = None
47+
copyright: str | None = None
48+
privacy_policy: str | None = None
49+
custom_disclaimer: str | None = None
50+
default_language: str
51+
show_workflow_steps: bool
52+
use_icon_as_answer_icon: bool
53+
54+
@computed_field(return_type=str | None) # type: ignore
55+
@property
56+
def icon_url(self) -> str | None:
57+
if self.icon and self.icon_type == IconType.IMAGE:
58+
return file_helpers.get_signed_file_url(self.icon)
59+
return None

0 commit comments

Comments
 (0)