Compare commits

...

11 Commits

Author SHA1 Message Date
Stephen Zhou
7b66bbc35a chore: introduce bulk-suppressions and multithread linting (#31157)
Some checks failed
autofix.ci / autofix (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Main CI Pipeline / Check Changed Files (push) Has been cancelled
Main CI Pipeline / Style Check (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Main CI Pipeline / API Tests (push) Has been cancelled
Main CI Pipeline / Web Tests (push) Has been cancelled
Main CI Pipeline / VDB Tests (push) Has been cancelled
Main CI Pipeline / DB Migration Test (push) Has been cancelled
2026-01-17 19:51:56 +08:00
Pegasus
77366f33a4 feat(web): add loading indicators for infinite scroll pagination (#31110)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
2026-01-17 17:36:07 +08:00
yyh
e3b0918dd9 test(web): add global zustand mock for tests (#31149) 2026-01-17 17:29:13 +08:00
가은 정
fad6fa141d chore: improve accessibility for learn more link (#31120)
Some checks failed
autofix.ci / autofix (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Has been cancelled
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Has been cancelled
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Has been cancelled
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Has been cancelled
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Has been cancelled
Main CI Pipeline / Check Changed Files (push) Has been cancelled
Main CI Pipeline / API Tests (push) Has been cancelled
Main CI Pipeline / Web Tests (push) Has been cancelled
Main CI Pipeline / Style Check (push) Has been cancelled
Main CI Pipeline / VDB Tests (push) Has been cancelled
Main CI Pipeline / DB Migration Test (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Co-authored-by: khmandarrin <jeong-ga-eun@jeong-ga-eun-ui-MacBookAir.local>
2026-01-16 18:12:07 +08:00
Pádraic Slattery
30821fd26c chore: Update outdated GitHub Actions versions (#31114) 2026-01-16 17:56:55 +08:00
Xiangxuan Qu
1a9fdd9a65 refactor: migrate tag list API query parameters to Pydantic (#31097)
Co-authored-by: fghpdf <fghpdf@users.noreply.github.com>
2026-01-16 17:49:52 +08:00
Stream
de610cbf39 fix: call get_text_content() instead of casting to str (#31121)
Signed-off-by: Stream <Stream_2@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 18:41:00 +09:00
yyh
6903c31b84 fix(search-input): retain focus after clearing input (#31107) 2026-01-16 16:22:14 +08:00
盐粒 Yanli
b2cc9b255d chore: Update coding agent workflow for backend (#31093) 2026-01-16 14:28:47 +08:00
XiaoBa
e9f0e1e839 fix(web): replace Response.json with legacy Response constructor for pre-Chrome 105 compatibility(#31091) (#31095)
Co-authored-by: Xiaoba Yu <xb1823725853@gmail.com>
2026-01-16 14:26:23 +08:00
pavior
cd497a8c52 fix(web): use portal for variable picker in code editor (Fixes #31063) (#31066) 2026-01-16 13:31:57 +08:00
41 changed files with 5569 additions and 333 deletions

View File

@@ -16,14 +16,14 @@ jobs:
- name: Check Docker Compose inputs
id: docker-compose-changes
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@v47
with:
files: |
docker/generate_docker_compose
docker/.env.example
docker/docker-compose-template.yaml
docker/docker-compose.yaml
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.11"

View File

@@ -112,7 +112,7 @@ jobs:
context: "web"
steps:
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
path: /tmp/digests
pattern: digests-${{ matrix.context }}-*

View File

@@ -19,7 +19,7 @@ jobs:
github.event.workflow_run.head_branch == 'deploy/agent-dev'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.AGENT_DEV_SSH_HOST }}
username: ${{ secrets.SSH_USER }}

View File

@@ -16,7 +16,7 @@ jobs:
github.event.workflow_run.head_branch == 'deploy/dev'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}

View File

@@ -20,7 +20,7 @@ jobs:
)
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HITL_SSH_HOST }}
username: ${{ secrets.SSH_USER }}

View File

@@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/stale@v5
- uses: actions/stale@v10
with:
days-before-issue-stale: 15
days-before-issue-close: 3

View File

@@ -106,8 +106,9 @@ jobs:
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
run: |
pnpm run lint:report
continue-on-error: true
pnpm run lint:ci
# pnpm run lint:report
# continue-on-error: true
# - name: Annotate Code
# if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request'
@@ -126,11 +127,6 @@ jobs:
working-directory: ./web
run: pnpm run knip
- name: Web build check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
run: pnpm run build
superlinter:
name: SuperLinter
runs-on: ubuntu-latest

View File

@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -366,3 +366,48 @@ jobs:
path: web/coverage
retention-days: 30
if-no-files-found: error
web-build:
name: Web Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Check changed files
id: changed-files
uses: tj-actions/changed-files@v47
with:
files: |
web/**
.github/workflows/web-tests.yml
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: web/package.json
run_install: false
- name: Setup NodeJS
uses: actions/setup-node@v6
if: steps.changed-files.outputs.any_changed == 'true'
with:
node-version: 24
cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml
- name: Web dependencies
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
run: pnpm install --frozen-lockfile
- name: Web build check
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ./web
run: pnpm run build

View File

@@ -12,12 +12,8 @@ The codebase is split into:
## Backend Workflow
- Read `api/AGENTS.md` for details
- Run backend CLI commands through `uv run --project api <command>`.
- Before submission, all backend modifications must pass local checks: `make lint`, `make type-check`, and `uv run --project api --dev dev/pytest/pytest_unit_tests.sh`.
- Use Makefile targets for linting and formatting; `make lint` and `make type-check` cover the required checks.
- Integration tests are CI-only and are not expected to run in the local environment.
## Frontend Workflow

View File

@@ -61,7 +61,8 @@ check:
lint:
@echo "🔧 Running ruff format, check with fixes, import linter, and dotenv-linter..."
@uv run --project api --dev sh -c 'ruff format ./api && ruff check --fix ./api'
@uv run --project api --dev ruff format ./api
@uv run --project api --dev ruff check --fix ./api
@uv run --directory api --dev lint-imports
@uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
@echo "✅ Linting complete"
@@ -73,7 +74,12 @@ type-check:
test:
@echo "🧪 Running backend unit tests..."
@uv run --project api --dev dev/pytest/pytest_unit_tests.sh
@if [ -n "$(TARGET_TESTS)" ]; then \
echo "Target: $(TARGET_TESTS)"; \
uv run --project api --dev pytest $(TARGET_TESTS); \
else \
uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
fi
@echo "✅ Tests complete"
# Build Docker images
@@ -125,7 +131,7 @@ help:
@echo " make check - Check code with ruff"
@echo " make lint - Format, fix, and lint code (ruff, imports, dotenv)"
@echo " make type-check - Run type checking with basedpyright"
@echo " make test - Run backend unit tests"
@echo " make test - Run backend unit tests (or TARGET_TESTS=./api/tests/<target_tests>)"
@echo ""
@echo "Docker Build Targets:"
@echo " make build-web - Build web Docker image"

0
agent-notes/.gitkeep Normal file
View File

View File

@@ -1,62 +1,236 @@
# Agent Skill Index
# API Agent Guide
## Agent Notes (must-check)
Before you start work on any backend file under `api/`, you MUST check whether a related note exists under:
- `agent-notes/<same-relative-path-as-target-file>.md`
Rules:
- **Path mapping**: for a target file `<path>/<name>.py`, the note must be `agent-notes/<path>/<name>.py.md` (same folder structure, same filename, plus `.md`).
- **Before working**:
- If the note exists, read it first and follow any constraints/decisions recorded there.
- If the note conflicts with the current code, or references an "origin" file/path that has been deleted, renamed, or migrated, treat the **code as the single source of truth** and update the note to match reality.
- If the note does not exist, create it with a short architecture/intent summary and any relevant invariants/edge cases.
- **During working**:
- Keep the note in sync as you discover constraints, make decisions, or change approach.
- If you move/rename a file, migrate its note to the new mapped path (and fix any outdated references inside the note).
- Record non-obvious edge cases, trade-offs, and the test/verification plan as you go (not just at the end).
- Keep notes **coherent**: integrate new findings into the relevant sections and rewrite for clarity; avoid append-only “recent fix” / changelog-style additions unless the note is explicitly intended to be a changelog.
- **When finishing work**:
- Update the related note(s) to reflect what changed, why, and any new edge cases/tests.
- If a file is deleted, remove or clearly deprecate the corresponding note so it cannot be mistaken as current guidance.
- Keep notes concise and accurate; they are meant to prevent repeated rediscovery.
## Skill Index
Start with the section that best matches your need. Each entry lists the problems it solves plus key files/concepts so you know what to expect before opening it.
______________________________________________________________________
### Platform Foundations
## Platform Foundations
- **[Infrastructure Overview](agent_skills/infra.md)**\
When to read this:
#### [Infrastructure Overview](agent_skills/infra.md)
- **When to read this**
- You need to understand where a feature belongs in the architecture.
- Youre wiring storage, Redis, vector stores, or OTEL.
- Youre about to add CLI commands or async jobs.\
What it covers: configuration stack (`configs/app_config.py`, remote settings), storage entry points (`extensions/ext_storage.py`, `core/file/file_manager.py`), Redis conventions (`extensions/ext_redis.py`), plugin runtime topology, vector-store factory (`core/rag/datasource/vdb/*`), observability hooks, SSRF proxy usage, and core CLI commands.
- Youre about to add CLI commands or async jobs.
- **What it covers**
- Configuration stack (`configs/app_config.py`, remote settings)
- Storage entry points (`extensions/ext_storage.py`, `core/file/file_manager.py`)
- Redis conventions (`extensions/ext_redis.py`)
- Plugin runtime topology
- Vector-store factory (`core/rag/datasource/vdb/*`)
- Observability hooks
- SSRF proxy usage
- Core CLI commands
- **[Coding Style](agent_skills/coding_style.md)**\
When to read this:
### Plugin & Extension Development
- Youre writing or reviewing backend code and need the authoritative checklist.
- Youre unsure about Pydantic validators, SQLAlchemy session usage, or logging patterns.
- You want the exact lint/type/test commands used in PRs.\
Includes: Ruff & BasedPyright commands, no-annotation policy, session examples (`with Session(db.engine, ...)`), `@field_validator` usage, logging expectations, and the rule set for file size, helpers, and package management.
______________________________________________________________________
## Plugin & Extension Development
- **[Plugin Systems](agent_skills/plugin.md)**\
When to read this:
#### [Plugin Systems](agent_skills/plugin.md)
- **When to read this**
- Youre building or debugging a marketplace plugin.
- You need to know how manifests, providers, daemons, and migrations fit together.\
What it covers: plugin manifests (`core/plugin/entities/plugin.py`), installation/upgrade flows (`services/plugin/plugin_service.py`, CLI commands), runtime adapters (`core/plugin/impl/*` for tool/model/datasource/trigger/endpoint/agent), daemon coordination (`core/plugin/entities/plugin_daemon.py`), and how provider registries surface capabilities to the rest of the platform.
- You need to know how manifests, providers, daemons, and migrations fit together.
- **What it covers**
- Plugin manifests (`core/plugin/entities/plugin.py`)
- Installation/upgrade flows (`services/plugin/plugin_service.py`, CLI commands)
- Runtime adapters (`core/plugin/impl/*` for tool/model/datasource/trigger/endpoint/agent)
- Daemon coordination (`core/plugin/entities/plugin_daemon.py`)
- How provider registries surface capabilities to the rest of the platform
- **[Plugin OAuth](agent_skills/plugin_oauth.md)**\
When to read this:
#### [Plugin OAuth](agent_skills/plugin_oauth.md)
- **When to read this**
- You must integrate OAuth for a plugin or datasource.
- Youre handling credential encryption or refresh flows.\
Topics: credential storage, encryption helpers (`core/helper/provider_encryption.py`), OAuth client bootstrap (`services/plugin/oauth_service.py`, `services/plugin/plugin_parameter_service.py`), and how console/API layers expose the flows.
- Youre handling credential encryption or refresh flows.
- **Topics**
- Credential storage
- Encryption helpers (`core/helper/provider_encryption.py`)
- OAuth client bootstrap (`services/plugin/oauth_service.py`, `services/plugin/plugin_parameter_service.py`)
- How console/API layers expose the flows
______________________________________________________________________
### Workflow Entry & Execution
## Workflow Entry & Execution
#### [Trigger Concepts](agent_skills/trigger.md)
- **[Trigger Concepts](agent_skills/trigger.md)**\
When to read this:
- **When to read this**
- Youre debugging why a workflow didnt start.
- Youre adding a new trigger type or hook.
- You need to trace async execution, draft debugging, or webhook/schedule pipelines.\
Details: Start-node taxonomy, webhook & schedule internals (`core/workflow/nodes/trigger_*`, `services/trigger/*`), async orchestration (`services/async_workflow_service.py`, Celery queues), debug event bus, and storage/logging interactions.
- You need to trace async execution, draft debugging, or webhook/schedule pipelines.
- **Details**
- Start-node taxonomy
- Webhook & schedule internals (`core/workflow/nodes/trigger_*`, `services/trigger/*`)
- Async orchestration (`services/async_workflow_service.py`, Celery queues)
- Debug event bus
- Storage/logging interactions
______________________________________________________________________
## General Reminders
## Additional Notes for Agents
- All skill docs assume you follow the coding style guide—run Ruff/BasedPyright/tests listed there before submitting changes.
- All skill docs assume you follow the coding style rules below—run the lint/type/test commands before submitting changes.
- When you cannot find an answer in these briefs, search the codebase using the paths referenced (e.g., `core/plugin/impl/tool.py`, `services/dataset_service.py`).
- If you run into cross-cutting concerns (tenancy, configuration, storage), check the infrastructure guide first; it links to most supporting modules.
- Keep multi-tenancy and configuration central: everything flows through `configs.dify_config` and `tenant_id`.
- When touching plugins or triggers, consult both the system overview and the specialised doc to ensure you adjust lifecycle, storage, and observability consistently.
## Coding Style
This is the default standard for backend code in this repo. Follow it for new code and use it as the checklist when reviewing changes.
### Linting & Formatting
- Use Ruff for formatting and linting (follow `.ruff.toml`).
- Keep each line under 120 characters (including spaces).
### Naming Conventions
- Use `snake_case` for variables and functions.
- Use `PascalCase` for classes.
- Use `UPPER_CASE` for constants.
### Typing & Class Layout
- Code should usually include type annotations that match the repos current Python version (avoid untyped public APIs and “mystery” values).
- Prefer modern typing forms (e.g. `list[str]`, `dict[str, int]`) and avoid `Any` unless theres a strong reason.
- For classes, declare member variables at the top of the class body (before `__init__`) so the class shape is obvious at a glance:
```python
from datetime import datetime
class Example:
user_id: str
created_at: datetime
def __init__(self, user_id: str, created_at: datetime) -> None:
self.user_id = user_id
self.created_at = created_at
```
### General Rules
- Use Pydantic v2 conventions.
- Use `uv` for Python package management in this repo (usually with `--project api`).
- Prefer simple functions over small “utility classes” for lightweight helpers.
- Avoid implementing dunder methods unless its clearly needed and matches existing patterns.
- Never start long-running services as part of agent work (`uv run app.py`, `flask run`, etc.); running tests is allowed.
- Keep files below ~800 lines; split when necessary.
- Keep code readable and explicit—avoid clever hacks.
### Architecture & Boundaries
- Mirror the layered architecture: controller → service → core/domain.
- Reuse existing helpers in `core/`, `services/`, and `libs/` before creating new abstractions.
- Optimise for observability: deterministic control flow, clear logging, actionable errors.
### Logging & Errors
- Never use `print`; use a module-level logger:
- `logger = logging.getLogger(__name__)`
- Include tenant/app/workflow identifiers in log context when relevant.
- Raise domain-specific exceptions (`services/errors`, `core/errors`) and translate them into HTTP responses in controllers.
- Log retryable events at `warning`, terminal failures at `error`.
### SQLAlchemy Patterns
- Models inherit from `models.base.TypeBase`; do not create ad-hoc metadata or engines.
- Open sessions with context managers:
```python
from sqlalchemy.orm import Session
with Session(db.engine, expire_on_commit=False) as session:
stmt = select(Workflow).where(
Workflow.id == workflow_id,
Workflow.tenant_id == tenant_id,
)
workflow = session.execute(stmt).scalar_one_or_none()
```
- Prefer SQLAlchemy expressions; avoid raw SQL unless necessary.
- Always scope queries by `tenant_id` and protect write paths with safeguards (`FOR UPDATE`, row counts, etc.).
- Introduce repository abstractions only for very large tables (e.g., workflow executions) or when alternative storage strategies are required.
### Storage & External I/O
- Access storage via `extensions.ext_storage.storage`.
- Use `core.helper.ssrf_proxy` for outbound HTTP fetches.
- Background tasks that touch storage must be idempotent, and should log relevant object identifiers.
### Pydantic Usage
- Define DTOs with Pydantic v2 models and forbid extras by default.
- Use `@field_validator` / `@model_validator` for domain rules.
Example:
```python
from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
class TriggerConfig(BaseModel):
endpoint: HttpUrl
secret: str
model_config = ConfigDict(extra="forbid")
@field_validator("secret")
def ensure_secret_prefix(cls, value: str) -> str:
if not value.startswith("dify_"):
raise ValueError("secret must start with dify_")
return value
```
### Generics & Protocols
- Use `typing.Protocol` to define behavioural contracts (e.g., cache interfaces).
- Apply generics (`TypeVar`, `Generic`) for reusable utilities like caches or providers.
- Validate dynamic inputs at runtime when generics cannot enforce safety alone.
### Tooling & Checks
Quick checks while iterating:
- Format: `make format`
- Lint (includes auto-fix): `make lint`
- Type check: `make type-check`
- Targeted tests: `make test TARGET_TESTS=./api/tests/<target_tests>`
Before opening a PR / submitting:
- `make lint`
- `make type-check`
- `make test`
### Controllers & Services
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
- Document non-obvious behaviour with concise comments.
### Miscellaneous
- Use `configs.dify_config` for configuration—never read environment variables directly.
- Maintain tenant awareness end-to-end; `tenant_id` must flow through every layer touching shared resources.
- Queue async work through `services/async_workflow_service`; implement tasks under `tasks/` with explicit queue selection.
- Keep experimental scripts under `dev/`; do not ship them in production builds.

View File

@@ -1,115 +0,0 @@
## Linter
- Always follow `.ruff.toml`.
- Run `uv run ruff check --fix --unsafe-fixes`.
- Keep each line under 100 characters (including spaces).
## Code Style
- `snake_case` for variables and functions.
- `PascalCase` for classes.
- `UPPER_CASE` for constants.
## Rules
- Use Pydantic v2 standard.
- Use `uv` for package management.
- Do not override dunder methods like `__init__`, `__iadd__`, etc.
- Never launch services (`uv run app.py`, `flask run`, etc.); running tests under `tests/` is allowed.
- Prefer simple functions over classes for lightweight helpers.
- Keep files below 800 lines; split when necessary.
- Keep code readable—no clever hacks.
- Never use `print`; log with `logger = logging.getLogger(__name__)`.
## Guiding Principles
- Mirror the projects layered architecture: controller → service → core/domain.
- Reuse existing helpers in `core/`, `services/`, and `libs/` before creating new abstractions.
- Optimise for observability: deterministic control flow, clear logging, actionable errors.
## SQLAlchemy Patterns
- Models inherit from `models.base.Base`; never create ad-hoc metadata or engines.
- Open sessions with context managers:
```python
from sqlalchemy.orm import Session
with Session(db.engine, expire_on_commit=False) as session:
stmt = select(Workflow).where(
Workflow.id == workflow_id,
Workflow.tenant_id == tenant_id,
)
workflow = session.execute(stmt).scalar_one_or_none()
```
- Use SQLAlchemy expressions; avoid raw SQL unless necessary.
- Introduce repository abstractions only for very large tables (e.g., workflow executions) to support alternative storage strategies.
- Always scope queries by `tenant_id` and protect write paths with safeguards (`FOR UPDATE`, row counts, etc.).
## Storage & External IO
- Access storage via `extensions.ext_storage.storage`.
- Use `core.helper.ssrf_proxy` for outbound HTTP fetches.
- Background tasks that touch storage must be idempotent and log the relevant object identifiers.
## Pydantic Usage
- Define DTOs with Pydantic v2 models and forbid extras by default.
- Use `@field_validator` / `@model_validator` for domain rules.
- Example:
```python
from pydantic import BaseModel, ConfigDict, HttpUrl, field_validator
class TriggerConfig(BaseModel):
endpoint: HttpUrl
secret: str
model_config = ConfigDict(extra="forbid")
@field_validator("secret")
def ensure_secret_prefix(cls, value: str) -> str:
if not value.startswith("dify_"):
raise ValueError("secret must start with dify_")
return value
```
## Generics & Protocols
- Use `typing.Protocol` to define behavioural contracts (e.g., cache interfaces).
- Apply generics (`TypeVar`, `Generic`) for reusable utilities like caches or providers.
- Validate dynamic inputs at runtime when generics cannot enforce safety alone.
## Error Handling & Logging
- Raise domain-specific exceptions (`services/errors`, `core/errors`) and translate to HTTP responses in controllers.
- Declare `logger = logging.getLogger(__name__)` at module top.
- Include tenant/app/workflow identifiers in log context.
- Log retryable events at `warning`, terminal failures at `error`.
## Tooling & Checks
- Format/lint: `uv run --project api --dev ruff format ./api` and `uv run --project api --dev ruff check --fix --unsafe-fixes ./api`.
- Type checks: `uv run --directory api --dev basedpyright`.
- Tests: `uv run --project api --dev dev/pytest/pytest_unit_tests.sh`.
- Run all of the above before submitting your work.
## Controllers & Services
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
- Avoid repositories unless necessary; direct SQLAlchemy usage is preferred for typical tables.
- Document non-obvious behaviour with concise comments.
## Miscellaneous
- Use `configs.dify_config` for configuration—never read environment variables directly.
- Maintain tenant awareness end-to-end; `tenant_id` must flow through every layer touching shared resources.
- Queue async work through `services/async_workflow_service`; implement tasks under `tasks/` with explicit queue selection.
- Keep experimental scripts under `dev/`; do not ship them in production builds.

View File

@@ -30,6 +30,11 @@ class TagBindingRemovePayload(BaseModel):
type: Literal["knowledge", "app"] | None = Field(default=None, description="Tag type")
class TagListQueryParam(BaseModel):
type: Literal["knowledge", "app", ""] = Field("", description="Tag type filter")
keyword: str | None = Field(None, description="Search keyword")
register_schema_models(
console_ns,
TagBasePayload,
@@ -43,12 +48,15 @@ class TagListApi(Resource):
@setup_required
@login_required
@account_initialization_required
@console_ns.doc(
params={"type": 'Tag type filter. Can be "knowledge" or "app".', "keyword": "Search keyword for tag name."}
)
@marshal_with(dataset_tag_fields)
def get(self):
_, current_tenant_id = current_account_with_tenant()
tag_type = request.args.get("type", type=str, default="")
keyword = request.args.get("keyword", default=None, type=str)
tags = TagService.get_tags(tag_type, current_tenant_id, keyword)
raw_args = request.args.to_dict()
param = TagListQueryParam.model_validate(raw_args)
tags = TagService.get_tags(param.type, current_tenant_id, param.keyword)
return tags, 200

View File

@@ -71,8 +71,8 @@ class LLMGenerator:
response: LLMResult = model_instance.invoke_llm(
prompt_messages=list(prompts), model_parameters={"max_tokens": 500, "temperature": 1}, stream=False
)
answer = cast(str, response.message.content)
if answer is None:
answer = response.message.get_text_content()
if answer == "":
return ""
try:
result_dict = json.loads(answer)
@@ -184,7 +184,7 @@ class LLMGenerator:
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
rule_config["prompt"] = cast(str, response.message.content)
rule_config["prompt"] = response.message.get_text_content()
except InvokeError as e:
error = str(e)
@@ -237,13 +237,11 @@ class LLMGenerator:
return rule_config
rule_config["prompt"] = cast(str, prompt_content.message.content)
rule_config["prompt"] = prompt_content.message.get_text_content()
if not isinstance(prompt_content.message.content, str):
raise NotImplementedError("prompt content is not a string")
parameter_generate_prompt = parameter_template.format(
inputs={
"INPUT_TEXT": prompt_content.message.content,
"INPUT_TEXT": prompt_content.message.get_text_content(),
},
remove_template_variables=False,
)
@@ -253,7 +251,7 @@ class LLMGenerator:
statement_generate_prompt = statement_template.format(
inputs={
"TASK_DESCRIPTION": instruction,
"INPUT_TEXT": prompt_content.message.content,
"INPUT_TEXT": prompt_content.message.get_text_content(),
},
remove_template_variables=False,
)
@@ -263,7 +261,7 @@ class LLMGenerator:
parameter_content: LLMResult = model_instance.invoke_llm(
prompt_messages=list(parameter_messages), model_parameters=model_parameters, stream=False
)
rule_config["variables"] = re.findall(r'"\s*([^"]+)\s*"', cast(str, parameter_content.message.content))
rule_config["variables"] = re.findall(r'"\s*([^"]+)\s*"', parameter_content.message.get_text_content())
except InvokeError as e:
error = str(e)
error_step = "generate variables"
@@ -272,7 +270,7 @@ class LLMGenerator:
statement_content: LLMResult = model_instance.invoke_llm(
prompt_messages=list(statement_messages), model_parameters=model_parameters, stream=False
)
rule_config["opening_statement"] = cast(str, statement_content.message.content)
rule_config["opening_statement"] = statement_content.message.get_text_content()
except InvokeError as e:
error = str(e)
error_step = "generate conversation opener"
@@ -315,7 +313,7 @@ class LLMGenerator:
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
generated_code = cast(str, response.message.content)
generated_code = response.message.get_text_content()
return {"code": generated_code, "language": code_language, "error": ""}
except InvokeError as e:
@@ -351,7 +349,7 @@ class LLMGenerator:
raise TypeError("Expected LLMResult when stream=False")
response = result
answer = cast(str, response.message.content)
answer = response.message.get_text_content()
return answer.strip()
@classmethod
@@ -375,10 +373,7 @@ class LLMGenerator:
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
)
raw_content = response.message.content
if not isinstance(raw_content, str):
raise ValueError(f"LLM response content must be a string, got: {type(raw_content)}")
raw_content = response.message.get_text_content()
try:
parsed_content = json.loads(raw_content)

56
web/__mocks__/zustand.ts Normal file
View File

@@ -0,0 +1,56 @@
import type * as ZustandExportedTypes from 'zustand'
import { act } from '@testing-library/react'
export * from 'zustand'
const { create: actualCreate, createStore: actualCreateStore }
// eslint-disable-next-line antfu/no-top-level-await
= await vi.importActual<typeof ZustandExportedTypes>('zustand')
export const storeResetFns = new Set<() => void>()
const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreate(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}
export const create = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
return typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried
}) as typeof ZustandExportedTypes.create
const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreateStore(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}
export const createStore = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
return typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried
}) as typeof ZustandExportedTypes.createStore
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn()
})
})
})

View File

@@ -65,15 +65,17 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
<div className="text-xs text-text-secondary">
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
</div>
<div
className="cursor-pointer text-xs font-medium text-text-accent hover:underline"
<a
href={triggerDocUrl}
target="_blank"
rel="noopener noreferrer"
className="block cursor-pointer text-xs font-medium text-text-accent hover:underline"
onClick={(event) => {
event.stopPropagation()
window.open(triggerDocUrl, '_blank')
}}
>
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
</div>
</a>
</div>
), [t, triggerDocUrl])

View File

@@ -3,9 +3,7 @@ import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import useAccessControlStore from '@/context/access-control-store'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode, SubjectType } from '@/models/access-control'
import { defaultSystemFeatures } from '@/types/feature'
import Toast from '../../base/toast'
import AccessControlDialog from './access-control-dialog'
import AccessControlItem from './access-control-item'
@@ -105,22 +103,6 @@ const memberSubject: Subject = {
accountData: baseMember,
} as Subject
const resetAccessControlStore = () => {
useAccessControlStore.setState({
appId: '',
specificGroups: [],
specificMembers: [],
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
selectedGroupsForBreadcrumb: [],
})
}
const resetGlobalStore = () => {
useGlobalPublicStore.setState({
systemFeatures: defaultSystemFeatures,
})
}
beforeAll(() => {
class MockIntersectionObserver {
observe = vi.fn(() => undefined)
@@ -132,9 +114,6 @@ beforeAll(() => {
})
beforeEach(() => {
vi.clearAllMocks()
resetAccessControlStore()
resetGlobalStore()
mockMutateAsync.mockResolvedValue(undefined)
mockUseUpdateAccessMode.mockReturnValue({
isPending: false,

View File

@@ -189,6 +189,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}
</div>
))}
{isFetchingNextPage && <Loading />}
</div>
</>
)}

View File

@@ -21,15 +21,15 @@ export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps)
>
<SkeletonContainer className="h-full">
<SkeletonRow>
<SkeletonRectangle className="h-10 w-10 rounded-lg" />
<SkeletonRectangle className="h-10 w-10 animate-pulse rounded-lg" />
<div className="flex flex-1 flex-col gap-1">
<SkeletonRectangle className="h-4 w-2/3" />
<SkeletonRectangle className="h-3 w-1/3" />
<SkeletonRectangle className="h-4 w-2/3 animate-pulse" />
<SkeletonRectangle className="h-3 w-1/3 animate-pulse" />
</div>
</SkeletonRow>
<div className="mt-4 flex flex-col gap-2">
<SkeletonRectangle className="h-3 w-full" />
<SkeletonRectangle className="h-3 w-4/5" />
<SkeletonRectangle className="h-3 w-full animate-pulse" />
<SkeletonRectangle className="h-3 w-4/5 animate-pulse" />
</div>
</SkeletonContainer>
</div>

View File

@@ -248,6 +248,9 @@ const List = () => {
// No apps - show empty state
return <Empty />
})()}
{isFetchingNextPage && (
<AppCardSkeleton count={3} />
)}
</div>
{isCurrentWorkspaceEditor && (

View File

@@ -1,21 +1,25 @@
'use client'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import './style.css'
type ILoadingProps = {
type?: 'area' | 'app'
className?: string
}
const Loading = (
{ type = 'area' }: ILoadingProps = { type: 'area' },
) => {
const Loading = (props?: ILoadingProps) => {
const { type = 'area', className } = props || {}
const { t } = useTranslation()
return (
<div
className={`flex w-full items-center justify-center ${type === 'app' ? 'h-full' : ''}`}
className={cn(
'flex w-full items-center justify-center',
type === 'app' && 'h-full',
className,
)}
role="status"
aria-live="polite"
aria-label={t('loading', { ns: 'appApi' })}
@@ -37,4 +41,5 @@ const Loading = (
</div>
)
}
export default Loading

View File

@@ -20,6 +20,7 @@ const SearchInput: FC<SearchInputProps> = ({
white,
}) => {
const { t } = useTranslation()
const inputRef = useRef<HTMLInputElement>(null)
const [focus, setFocus] = useState<boolean>(false)
const isComposing = useRef<boolean>(false)
const [compositionValue, setCompositionValue] = useState<string>('')
@@ -36,6 +37,7 @@ const SearchInput: FC<SearchInputProps> = ({
<RiSearchLine className="h-4 w-4 text-components-input-text-placeholder" aria-hidden="true" />
</div>
<input
ref={inputRef}
type="text"
name="query"
className={cn(
@@ -65,14 +67,17 @@ const SearchInput: FC<SearchInputProps> = ({
autoComplete="off"
/>
{value && (
<div
className="group/clear flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center"
<button
type="button"
aria-label={t('operation.clear', { ns: 'common' })}
className="group/clear flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center border-none bg-transparent p-0"
onClick={() => {
onChange('')
inputRef.current?.focus()
}}
>
<RiCloseCircleFill className="h-4 w-4 text-text-quaternary group-hover/clear:text-text-tertiary" />
</div>
</button>
)}
</div>
)

View File

@@ -2,6 +2,7 @@
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import DatasetCard from './dataset-card'
@@ -25,6 +26,7 @@ const Datasets = ({
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
} = useDatasetList({
initialPage: 1,
tag_ids: tags,
@@ -60,6 +62,7 @@ const Datasets = ({
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
))}
{isFetchingNextPage && <Loading />}
<div ref={anchorRef} className="h-0" />
</nav>
</>

View File

@@ -33,6 +33,7 @@ const AppNav = () => {
data: appsData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
} = useInfiniteAppList({
page: 1,
@@ -111,6 +112,7 @@ const AppNav = () => {
createText={t('menus.newApp', { ns: 'common' })}
onCreate={openModal}
onLoadMore={handleLoadMore}
isLoadingMore={isFetchingNextPage}
/>
<CreateAppModal
show={showNewAppDialog}

View File

@@ -23,6 +23,7 @@ const DatasetNav = () => {
data: datasetList,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useDatasetList({
initialPage: 1,
limit: 30,
@@ -93,6 +94,7 @@ const DatasetNav = () => {
createText={t('menus.newDataset', { ns: 'common' })}
onCreate={() => router.push(createRoute)}
onLoadMore={handleLoadMore}
isLoadingMore={isFetchingNextPage}
/>
)
}

View File

@@ -30,6 +30,7 @@ const Nav = ({
createText,
onCreate,
onLoadMore,
isLoadingMore,
isApp,
}: INavProps) => {
const setAppDetail = useAppStore(state => state.setAppDetail)
@@ -81,6 +82,7 @@ const Nav = ({
createText={createText}
onCreate={onCreate}
onLoadMore={onLoadMore}
isLoadingMore={isLoadingMore}
/>
</>
)

View File

@@ -14,6 +14,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import AppIcon from '@/app/components/base/app-icon'
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { cn } from '@/utils/classnames'
@@ -34,9 +35,10 @@ export type INavSelectorProps = {
isApp?: boolean
onCreate: (state: string) => void
onLoadMore?: () => void
isLoadingMore?: boolean
}
const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore }: INavSelectorProps) => {
const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore, isLoadingMore }: INavSelectorProps) => {
const { t } = useTranslation()
const router = useRouter()
const { isCurrentWorkspaceEditor } = useAppContext()
@@ -106,6 +108,11 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
</MenuItem>
))
}
{isLoadingMore && (
<div className="flex justify-center py-2">
<Loading />
</div>
)}
</div>
{!isApp && isCurrentWorkspaceEditor && (
<MenuItem as="div" className="w-full p-1">

View File

@@ -19,6 +19,7 @@ const ListWrapper = ({
marketplaceCollections,
marketplaceCollectionPluginsMap,
isLoading,
isFetchingNextPage,
page,
} = useMarketplaceData()
@@ -53,6 +54,11 @@ const ListWrapper = ({
/>
)
}
{
isFetchingNextPage && (
<Loading className="my-3" />
)
}
</div>
)
}

View File

@@ -33,7 +33,7 @@ export function useMarketplaceData() {
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
const pluginsQuery = useMarketplacePlugins(queryParams)
const { hasNextPage, fetchNextPage, isFetching } = pluginsQuery
const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = pluginsQuery
const handlePageChange = useCallback(() => {
if (hasNextPage && !isFetching)
@@ -50,5 +50,6 @@ export function useMarketplaceData() {
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
page: pluginsQuery.data?.pages.length || 1,
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
isFetchingNextPage,
}
}

View File

@@ -144,17 +144,6 @@ describe('constant.ts - Type Definitions', () => {
// ==================== store.ts Tests ====================
describe('store.ts - Zustand Store', () => {
beforeEach(() => {
// Reset store to initial state
const { setState } = useStore
setState({
tagList: [],
categoryList: [],
showTagManagementModal: false,
showCategoryManagementModal: false,
})
})
describe('Initial State', () => {
it('should have empty tagList initially', () => {
const { result } = renderHook(() => useStore(state => state.tagList))

View File

@@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { useGetLanguage } from '@/context/i18n'
import { renderI18nObject } from '@/i18n-config'
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import Loading from '../../base/loading'
import { PluginSource } from '../types'
import { usePluginPageContext } from './context'
import Empty from './empty'
@@ -107,12 +107,17 @@ const PluginsPanel = () => {
<div className="w-full">
<List pluginList={filteredList || []} />
</div>
{!isLastPage && !isFetching && (
<Button onClick={loadNextPage}>
{t('common.loadMore', { ns: 'workflow' })}
</Button>
{!isLastPage && (
<div className="flex justify-center py-4">
{isFetching
? <Loading className="size-8" />
: (
<Button onClick={loadNextPage}>
{t('common.loadMore', { ns: 'workflow' })}
</Button>
)}
</div>
)}
{isFetching && <div className="system-md-semibold text-text-secondary">{t('detail.loading', { ns: 'appLog' })}</div>}
</div>
)
: (

View File

@@ -134,13 +134,6 @@ describe('BUILTIN_TOOLS_ARRAY', () => {
// Store Tests
// ================================
describe('useReadmePanelStore', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset store state before each test
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
setCurrentPluginDetail()
})
describe('Initial State', () => {
it('should have undefined currentPluginDetail initially', () => {
const { currentPluginDetail } = useReadmePanelStore.getState()
@@ -228,13 +221,6 @@ describe('useReadmePanelStore', () => {
// ReadmeEntrance Component Tests
// ================================
describe('ReadmeEntrance', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset store state
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
setCurrentPluginDetail()
})
// ================================
// Rendering Tests
// ================================
@@ -417,11 +403,6 @@ describe('ReadmeEntrance', () => {
// ================================
describe('ReadmePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset store state
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
setCurrentPluginDetail()
// Reset mock
mockUsePluginReadme.mockReturnValue({
data: null,
isLoading: false,

View File

@@ -5,6 +5,7 @@ import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { cn } from '@/utils/classnames'
@@ -147,7 +148,7 @@ const CodeEditor: FC<Props> = ({
onMount={onEditorMounted}
placeholder={t('common.jinjaEditorPlaceholder', { ns: 'workflow' })!}
/>
{isShowVarPicker && (
{isShowVarPicker && createPortal(
<div
ref={popupRef}
className="w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg"
@@ -164,7 +165,8 @@ const CodeEditor: FC<Props> = ({
onChange={handleSelectVar}
isSupportFileVar={false}
/>
</div>
</div>,
document.body,
)}
</div>
)

5107
web/eslint-suppressions.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,30 +9,15 @@ import difyI18n from './eslint-rules/index.js'
export default antfu(
{
react: {
reactCompiler: true,
overrides: {
'react/no-context-provider': 'off',
'react/no-forward-ref': 'off',
'react/no-use-context': 'off',
'react/prefer-namespace-import': 'error',
// React Compiler rules
// Set to warn for gradual adoption
'react-hooks/config': 'warn',
'react-hooks/error-boundaries': 'warn',
'react-hooks/component-hook-factories': 'warn',
'react-hooks/gating': 'warn',
'react-hooks/globals': 'warn',
'react-hooks/immutability': 'warn',
'react-hooks/preserve-manual-memoization': 'warn',
'react-hooks/purity': 'warn',
'react-hooks/refs': 'warn',
// prefer react-hooks-extra/no-direct-set-state-in-use-effect
'react-hooks/set-state-in-effect': 'off',
'react-hooks/set-state-in-render': 'warn',
'react-hooks/static-components': 'warn',
'react-hooks/unsupported-syntax': 'warn',
'react-hooks/use-memo': 'warn',
'react-hooks/incompatible-library': 'warn',
'react-hooks-extra/no-direct-set-state-in-use-effect': 'error',
},
},
nextjs: true,
@@ -40,7 +25,7 @@ export default antfu(
typescript: {
overrides: {
'ts/consistent-type-definitions': ['error', 'type'],
'ts/no-explicit-any': 'warn',
'ts/no-explicit-any': 'error',
},
},
test: {
@@ -54,6 +39,11 @@ export default antfu(
},
},
},
{
rules: {
'node/prefer-global/process': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
settings: {
@@ -62,32 +52,6 @@ export default antfu(
},
},
},
// downgrade some rules from error to warn for gradual adoption
// we should fix these in following pull requests
{
// @keep-sorted
rules: {
'next/inline-script-id': 'warn',
'no-console': 'warn',
'no-irregular-whitespace': 'warn',
'node/prefer-global/buffer': 'warn',
'node/prefer-global/process': 'warn',
'react/no-create-ref': 'warn',
'react/no-missing-key': 'warn',
'react/no-nested-component-definitions': 'warn',
'regexp/no-dupe-disjunctions': 'warn',
'regexp/no-super-linear-backtracking': 'warn',
'regexp/no-unused-capturing-group': 'warn',
'regexp/no-useless-assertions': 'warn',
'regexp/no-useless-quantifier': 'warn',
'style/multiline-ternary': 'warn',
'test/no-identical-title': 'warn',
'test/prefer-hooks-in-order': 'warn',
'ts/no-empty-object-type': 'warn',
'unicorn/prefer-number-properties': 'warn',
'unused-imports/no-unused-vars': 'warn',
},
},
storybook.configs['flat/recommended'],
...pluginQuery.configs['flat/recommended'],
// sonar
@@ -178,19 +142,19 @@ export default antfu(
},
},
// dify i18n namespace migration
{
files: ['**/*.ts', '**/*.tsx'],
ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'],
plugins: {
'dify-i18n': difyI18n,
},
rules: {
// 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
'dify-i18n/no-as-any-in-t': 'error',
// 'dify-i18n/no-legacy-namespace-prefix': 'error',
// 'dify-i18n/require-ns-option': 'error',
},
},
// {
// files: ['**/*.ts', '**/*.tsx'],
// ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'],
// plugins: {
// 'dify-i18n': difyI18n,
// },
// rules: {
// // 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
// 'dify-i18n/no-as-any-in-t': 'error',
// // 'dify-i18n/no-legacy-namespace-prefix': 'error',
// // 'dify-i18n/require-ns-option': 'error',
// },
// },
// i18n JSON validation rules
{
files: ['i18n/**/*.json'],

View File

@@ -28,6 +28,7 @@
"build:docker": "next build && node scripts/optimize-standalone.js",
"start": "node ./scripts/copy-and-start.mjs",
"lint": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
"lint:ci": "pnpm lint --concurrency 3",
"lint:fix": "pnpm lint --fix",
"lint:quiet": "pnpm lint --quiet",
"lint:complexity": "pnpm lint --rule 'complexity: [error, {max: 15}]' --quiet",

View File

@@ -24,8 +24,12 @@ export type FetchOptionType = Omit<RequestInit, 'body'> & {
}
const afterResponse204: AfterResponseHook = async (_request, _options, response) => {
if (response.status === 204)
return Response.json({ result: 'success' })
if (response.status === 204) {
return new Response(JSON.stringify({ result: 'success' }), {
status: 200,
headers: { 'Content-Type': ContentType.json },
})
}
}
export type ResponseError = {

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2015",
"target": "es2022",
"jsx": "preserve",
"lib": [
"dom",

View File

@@ -85,6 +85,10 @@ afterEach(() => {
// mock next/image to avoid width/height requirements for data URLs
vi.mock('next/image')
// mock zustand - auto-resets all stores after each test
// Based on official Zustand testing guide: https://zustand.docs.pmnd.rs/guides/testing
vi.mock('zustand')
// mock react-i18next
vi.mock('react-i18next', async () => {
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')