Compare commits

..

27 Commits

Author SHA1 Message Date
2c7d1f4cae Merge branch 'feat/agent-v2' into feat/agent-v2-e2e-test 2026-07-09 17:22:02 +08:00
yyh
85b3d62445 Merge branch 'main' into feat/agent-v2 2026-07-09 17:21:24 +08:00
44cda898a4 fix(e2e): use current agent routes 2026-07-09 17:07:29 +08:00
35a495d91d chore: run model test 2026-07-09 16:36:18 +08:00
c6a50b14ec Merge remote-tracking branch 'origin/feat/agent-v2' into feat/agent-v2 2026-07-09 16:33:05 +08:00
3ee9d66aaa Merge commit 'f8fff54682' into feat/agent-v2 2026-07-09 16:32:22 +08:00
9b117b0207 fix(ci): satisfy backend checks 2026-07-09 16:31:59 +08:00
f8fff54682 fix(api): prefer filename extension for tool files 2026-07-09 16:30:25 +08:00
89bc288a4b chore: add e2e trigger 2026-07-09 16:28:19 +08:00
819ecad3f7 Merge remote-tracking branch 'refs/remotes/tmp/main' into fix/pr-38593-ci
# Conflicts:
#	packages/contracts/generated/api/web/types.gen.ts
#	packages/contracts/generated/api/web/zod.gen.ts
2026-07-09 16:17:57 +08:00
65d2b4c1c2 Merge commit 'bca16261ea' into feat/agent-v2 2026-07-09 16:09:54 +08:00
c911d42b31 chore: update generated API contracts 2026-07-09 16:07:23 +08:00
bca16261ea chore(agent): clarify sandbox and skill guidance 2026-07-09 16:03:40 +08:00
f002fa8a98 fix: though fold and icon 2026-07-09 15:55:37 +08:00
43c9fb96ee fix(agent): expose message usage in history (#38596) 2026-07-09 15:49:39 +08:00
79c60ddfe2 [autofix.ci] apply automated fixes 2026-07-09 07:25:42 +00:00
2b0996236f fix(agent): preserve repeated tool call thoughts (#38592) 2026-07-09 15:18:31 +08:00
c192a3c16e fix(agent): use internal URLs for stub file downloads 2026-07-09 15:13:15 +08:00
5bed2666bb fix(agent): pass through model invocation errors (#38584) 2026-07-09 14:14:43 +08:00
1f6e39b0ad chore: agent to agents 2026-07-09 12:44:29 +08:00
5d0da5a27e chore: change agent url 2026-07-09 11:57:02 +08:00
055beabfec fix agent thought history title rendering 2026-07-09 11:57:02 +08:00
fd06801caa fix(agent): ignore workflow agent message deltas (#38582) 2026-07-09 11:56:22 +08:00
37c5f78a7f fix: not render anwser in thought 2026-07-09 11:24:21 +08:00
yyh
093d47cf05 Merge remote-tracking branch 'origin/main' into feat/agent-v2 2026-07-09 09:22:18 +08:00
dac5be487d chore: wire dify agent compose config 2026-07-09 04:06:32 +08:00
12d1998fe4 feat(dify-agent): vendor shellctl 2026-07-08 23:16:48 +08:00
69 changed files with 444 additions and 906 deletions

View File

@ -1,6 +1,10 @@
name: Web Full-Stack E2E
on:
push:
branches:
- feat/agent-v2-e2e-test
workflow_call:
inputs:
run-external-runtime:
@ -59,7 +63,7 @@ jobs:
run: vp run e2e:full
- name: Run external runtime E2E tests
if: ${{ inputs.run-external-runtime }}
if: ${{ inputs.run-external-runtime || github.ref == 'refs/heads/feat/agent-v2-e2e-test' }}
working-directory: ./e2e
env:
E2E_ADMIN_EMAIL: e2e-admin@example.com

View File

@ -125,6 +125,8 @@ All of Dify's offerings come with corresponding APIs, so you could effortlessly
- **Dify for enterprise / organizations<br/>**
We provide additional enterprise-centric features. [Send us an email](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) to discuss your enterprise needs. <br/>
> For startups and small businesses using AWS, check out [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) and deploy it to your own AWS VPC with one click. It's an affordable AMI offering with the option to create apps with custom logo and branding.
## Staying ahead
Star Dify on GitHub and be instantly notified of new releases.

View File

@ -1,12 +1,10 @@
import datetime
import logging
import time
from collections.abc import Callable
from typing import TypedDict
import click
import sqlalchemy as sa
from sqlalchemy.orm import Session, sessionmaker
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
@ -14,7 +12,6 @@ from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpi
from services.retention.conversation.messages_clean_policy import create_message_clean_policy
from services.retention.conversation.messages_clean_service import MessagesCleanService
from services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs import WorkflowRunCleanup
from services.retention.workflow_run.db_retry import run_with_db_retry
from services.retention.workflow_run.tenant_prefix import tenant_prefix_condition
from tasks.remove_app_and_related_data_task import delete_draft_variables_batch
@ -38,12 +35,6 @@ class WorkflowRunArchiveTenantPlan(TypedDict):
unpaid_tenant_ids: list[str]
class WorkflowRunArchivePrefixStats(TypedDict):
tenant_ids: list[str]
workflow_runs: int
workflow_node_executions: int
def _normalize_utc_datetime(value: datetime.datetime) -> datetime.datetime:
if value.tzinfo is None:
return value.replace(tzinfo=datetime.UTC)
@ -66,7 +57,6 @@ def _parse_tenant_prefixes(prefixes: str | None) -> list[str]:
def _get_archive_candidate_tenant_ids_by_prefix(
session: Session,
prefix: str,
*,
start_from: datetime.datetime | None,
@ -85,7 +75,7 @@ def _get_archive_candidate_tenant_ids_by_prefix(
if start_from is not None:
conditions.append(WorkflowRun.created_at >= start_from)
tenant_ids = session.scalars(
tenant_ids = db.session.scalars(
sa.select(WorkflowRun.tenant_id).where(*conditions).distinct().order_by(WorkflowRun.tenant_id)
).all()
return list(tenant_ids)
@ -112,80 +102,8 @@ def _filter_paid_workflow_archive_tenant_ids(tenant_ids: list[str]) -> tuple[lis
return paid_tenant_ids, unpaid_tenant_ids
def _run_archive_command_db_retry[T](operation_name: str, operation: Callable[[], T]) -> T:
return run_with_db_retry(operation_name, operation, logger=logger)
def _get_archive_candidate_tenant_ids_with_retry(
session_maker: sessionmaker[Session],
prefix: str,
*,
start_from: datetime.datetime | None,
end_before: datetime.datetime,
) -> list[str]:
def fetch_tenant_ids() -> list[str]:
with session_maker() as session:
return _get_archive_candidate_tenant_ids_by_prefix(
session,
prefix,
start_from=start_from,
end_before=end_before,
)
return _run_archive_command_db_retry(f"workflow archive tenant resolve for prefix {prefix}", fetch_tenant_ids)
def _get_archive_plan_prefix_stats(
session_maker: sessionmaker[Session],
prefix: str,
*,
start_from: datetime.datetime | None,
end_before: datetime.datetime,
) -> WorkflowRunArchivePrefixStats:
from graphon.enums import WorkflowExecutionStatus
from models.workflow import WorkflowNodeExecutionModel, WorkflowRun
from services.retention.workflow_run.archive_paid_plan_workflow_run import WorkflowRunArchiver
def fetch_prefix_stats() -> WorkflowRunArchivePrefixStats:
with session_maker() as session:
tenant_ids = _get_archive_candidate_tenant_ids_by_prefix(
session,
prefix,
start_from=start_from,
end_before=end_before,
)
run_conditions = [
WorkflowRun.created_at < end_before,
WorkflowRun.status.in_(WorkflowExecutionStatus.ended_values()),
WorkflowRun.type.in_(WorkflowRunArchiver.ARCHIVED_TYPE),
tenant_prefix_condition(WorkflowRun.tenant_id, prefix),
]
if start_from is not None:
run_conditions.append(WorkflowRun.created_at >= start_from)
workflow_runs = (
session.scalar(sa.select(sa.func.count()).select_from(WorkflowRun).where(*run_conditions)) or 0
)
candidate_runs = sa.select(WorkflowRun.id).where(*run_conditions).subquery()
workflow_node_executions = (
session.scalar(
sa.select(sa.func.count())
.select_from(WorkflowNodeExecutionModel)
.join(candidate_runs, WorkflowNodeExecutionModel.workflow_run_id == candidate_runs.c.id)
)
or 0
)
return WorkflowRunArchivePrefixStats(
tenant_ids=tenant_ids,
workflow_runs=workflow_runs,
workflow_node_executions=workflow_node_executions,
)
return _run_archive_command_db_retry(f"workflow archive plan for prefix {prefix}", fetch_prefix_stats)
def _resolve_archive_tenant_ids_from_plan(
*,
session_maker: sessionmaker[Session],
tenant_ids: str | None,
tenant_prefixes: list[str],
start_from: datetime.datetime | None,
@ -204,8 +122,7 @@ def _resolve_archive_tenant_ids_from_plan(
requested_tenant_ids = []
for prefix in tenant_prefixes:
requested_tenant_ids.extend(
_get_archive_candidate_tenant_ids_with_retry(
session_maker,
_get_archive_candidate_tenant_ids_by_prefix(
prefix,
start_from=start_from,
end_before=end_before,
@ -226,21 +143,6 @@ def _resolve_archive_tenant_ids_from_plan(
)
def _safe_remove_scoped_session(context: str) -> None:
try:
db.session.remove()
except Exception:
logger.warning("Ignoring DB scoped-session cleanup error after %s", context, exc_info=True)
try:
db.session.registry.clear()
except Exception:
logger.warning("Ignoring DB scoped-session registry cleanup error after %s", context, exc_info=True)
try:
db.engine.dispose()
except Exception:
logger.warning("Ignoring DB engine dispose error after %s", context, exc_info=True)
def _resolve_archive_time_range(
*,
before_days: int,
@ -447,6 +349,10 @@ def archive_workflow_runs_plan(
supported workflow types, and the requested created_at window. V2 bundle archive
does not maintain per-run archive logs, so this plan reports source-table volume.
"""
from graphon.enums import WorkflowExecutionStatus
from models.workflow import WorkflowNodeExecutionModel, WorkflowRun
from services.retention.workflow_run.archive_paid_plan_workflow_run import WorkflowRunArchiver
before_days, start_from, end_before = _resolve_archive_time_range(
before_days=before_days,
from_days_ago=from_days_ago,
@ -458,25 +364,37 @@ def archive_workflow_runs_plan(
if include_archived:
click.echo(click.style("--include-archived is a no-op for V2 bundle archive plans.", fg="yellow"))
session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
rows: list[WorkflowRunArchivePlanRow] = []
for prefix in _HEX_PREFIXES:
try:
prefix_stats = _get_archive_plan_prefix_stats(
session_maker,
prefix,
start_from=start_from,
end_before=plan_end_before,
)
except Exception as exc:
logger.exception("Failed to build workflow archive plan for prefix %s", prefix)
raise click.ClickException(f"Failed to build workflow archive plan for prefix {prefix}.") from exc
tenant_ids = prefix_stats["tenant_ids"]
workflow_runs = prefix_stats["workflow_runs"]
workflow_node_executions = prefix_stats["workflow_node_executions"]
tenant_ids = _get_archive_candidate_tenant_ids_by_prefix(
prefix,
start_from=start_from,
end_before=plan_end_before,
)
total_tenants = len(tenant_ids)
paid_tenant_ids, unpaid_tenant_ids = _filter_paid_workflow_archive_tenant_ids(tenant_ids)
run_conditions = [
WorkflowRun.created_at < plan_end_before,
WorkflowRun.status.in_(WorkflowExecutionStatus.ended_values()),
WorkflowRun.type.in_(WorkflowRunArchiver.ARCHIVED_TYPE),
tenant_prefix_condition(WorkflowRun.tenant_id, prefix),
]
if start_from is not None:
run_conditions.append(WorkflowRun.created_at >= start_from)
workflow_runs = (
db.session.scalar(sa.select(sa.func.count()).select_from(WorkflowRun).where(*run_conditions)) or 0
)
candidate_runs = sa.select(WorkflowRun.id).where(*run_conditions).subquery()
workflow_node_executions = (
db.session.scalar(
sa.select(sa.func.count())
.select_from(WorkflowNodeExecutionModel)
.join(candidate_runs, WorkflowNodeExecutionModel.workflow_run_id == candidate_runs.c.id)
)
or 0
)
rows.append(
WorkflowRunArchivePlanRow(
tenant_prefix=prefix,
@ -656,18 +574,17 @@ def archive_workflow_runs(
)
)
session_maker = sessionmaker(bind=db.engine, expire_on_commit=False)
try:
tenant_plan = _resolve_archive_tenant_ids_from_plan(
session_maker=session_maker,
tenant_ids=tenant_ids,
tenant_prefixes=parsed_tenant_prefixes,
start_from=start_from,
end_before=plan_end_before,
)
except Exception as exc:
except Exception:
logger.exception("Failed to resolve workflow archive tenant plan")
raise click.ClickException("Failed to resolve workflow archive tenant plan.") from exc
click.echo(click.style("Failed to resolve workflow archive tenant plan.", fg="red"))
return
planned_tenant_ids = tenant_plan["archive_tenant_ids"]
planned_paid_tenant_ids = tenant_plan["paid_tenant_ids"] if planned_tenant_ids is not None else None
@ -699,10 +616,7 @@ def archive_workflow_runs(
dry_run=dry_run,
delete_after_archive=delete_after_archive,
)
try:
summary = archiver.run()
finally:
_safe_remove_scoped_session("archive workflow run command")
summary = archiver.run()
click.echo(
click.style(
f"Summary: processed={summary.total_runs_processed}, archived={summary.runs_archived}, "

View File

@ -333,7 +333,6 @@ class AgentAppGenerator(MessageBasedAppGenerator):
target=self._generate_worker,
kwargs={
"flask_app": current_app._get_current_object(), # type: ignore
"session": db.session(),
"context": context,
"application_generate_entity": application_generate_entity,
"queue_manager": queue_manager,

View File

@ -205,7 +205,6 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
target=self._generate_worker,
kwargs={
"flask_app": current_app._get_current_object(), # type: ignore
"session": db.session(),
"context": context,
"application_generate_entity": application_generate_entity,
"queue_manager": queue_manager,

View File

@ -27,7 +27,7 @@ class ProviderCredentialsCache:
try:
cached_provider_credentials = cached_provider_credentials.decode("utf-8")
cached_provider_credentials = json.loads(cached_provider_credentials)
except (JSONDecodeError, UnicodeDecodeError):
except JSONDecodeError:
return None
return dict(cached_provider_credentials)

View File

@ -24,7 +24,7 @@ class ProviderCredentialsCache(ABC):
try:
cached_credentials = cached_credentials.decode("utf-8")
return dict(json.loads(cached_credentials))
except (JSONDecodeError, UnicodeDecodeError):
except JSONDecodeError:
return None
return None

View File

@ -30,7 +30,7 @@ class ToolParameterCache:
try:
cached_tool_parameter = cached_tool_parameter.decode("utf-8")
cached_tool_parameter = json.loads(cached_tool_parameter)
except (JSONDecodeError, UnicodeDecodeError):
except JSONDecodeError:
return None
return dict(cached_tool_parameter)

View File

@ -103,17 +103,16 @@ class ApiTool(Tool):
elif not isinstance(credentials["api_key_value"], str):
raise ToolProviderCredentialValidationError("api_key_value must be a string")
api_key_value = credentials["api_key_value"]
if "api_key_header_prefix" in credentials:
api_key_header_prefix = credentials["api_key_header_prefix"]
if api_key_header_prefix == "basic" and api_key_value:
api_key_value = f"Basic {api_key_value}"
elif api_key_header_prefix == "bearer" and api_key_value:
api_key_value = f"Bearer {api_key_value}"
if api_key_header_prefix == "basic" and credentials["api_key_value"]:
credentials["api_key_value"] = f"Basic {credentials['api_key_value']}"
elif api_key_header_prefix == "bearer" and credentials["api_key_value"]:
credentials["api_key_value"] = f"Bearer {credentials['api_key_value']}"
elif api_key_header_prefix == "custom":
pass
headers[api_key_header] = api_key_value
headers[api_key_header] = credentials["api_key_value"]
elif credentials["auth_type"] == "api_key_query":
# For query parameter authentication, we don't add anything to headers

View File

@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.16.0-rc1"
version = "1.15.0"
requires-python = "~=3.12.0"
dependencies = [

View File

@ -29,12 +29,12 @@ import hashlib
import json
import logging
import time
from collections.abc import Callable, Sequence
from collections.abc import Sequence
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from enum import Enum
from threading import Lock
from typing import Any, NotRequired, TypedDict, TypeVar, cast
from typing import Any, NotRequired, TypedDict, cast
import click
import pyarrow as pa
@ -70,10 +70,8 @@ from services.retention.workflow_run.constants import (
ARCHIVE_BUNDLE_MANIFEST_NAME,
ARCHIVE_BUNDLE_SCHEMA_VERSION,
)
from services.retention.workflow_run.db_retry import is_retryable_db_disconnect, run_with_db_retry
logger = logging.getLogger(__name__)
T = TypeVar("T")
class TableStatsManifestEntry(TypedDict):
@ -210,8 +208,6 @@ class WorkflowRunArchiver:
"workflow_pause_reasons",
"workflow_trigger_logs",
]
DB_RETRY_ATTEMPTS = 3
DB_RETRY_DELAYS_SECONDS = (1.0, 2.0)
start_from: datetime.datetime | None
end_before: datetime.datetime
@ -459,40 +455,16 @@ class WorkflowRunArchiver:
"""Fetch a batch of workflow runs to archive."""
repo = self._get_workflow_run_repo()
tenant_ids = list(tenant_scope) if tenant_scope is not None else self.tenant_ids or None
return self._run_with_db_retry(
"workflow run batch fetch",
lambda: repo.get_runs_batch_by_time_range(
start_from=self.start_from,
end_before=self.end_before,
last_seen=last_seen,
batch_size=self.batch_size,
run_types=self.ARCHIVED_TYPE,
tenant_ids=tenant_ids,
tenant_prefixes=None if tenant_ids else self.tenant_prefixes or None,
run_shard_index=self.run_shard_index,
run_shard_total=self.run_shard_total,
),
)
@staticmethod
def _is_retryable_db_disconnect(exc: BaseException) -> bool:
return is_retryable_db_disconnect(exc)
@staticmethod
def _safe_rollback(session: Session, bundle_id: str) -> None:
try:
session.rollback()
except Exception:
logger.warning("Failed to rollback archive session for bundle %s", bundle_id, exc_info=True)
def _run_with_db_retry(self, operation_name: str, operation: Callable[[], T]) -> T:
return run_with_db_retry(
operation_name,
operation,
logger=logger,
attempts=self.DB_RETRY_ATTEMPTS,
delays_seconds=self.DB_RETRY_DELAYS_SECONDS,
return repo.get_runs_batch_by_time_range(
start_from=self.start_from,
end_before=self.end_before,
last_seen=last_seen,
batch_size=self.batch_size,
run_types=self.ARCHIVED_TYPE,
tenant_ids=tenant_ids,
tenant_prefixes=None if tenant_ids else self.tenant_prefixes or None,
run_shard_index=self.run_shard_index,
run_shard_total=self.run_shard_total,
)
def _tenant_scan_scopes(self) -> list[list[str] | None]:
@ -560,14 +532,16 @@ class WorkflowRunArchiver:
if self.workers == 1 or len(bundle_groups) == 1:
results: list[ArchiveResult] = []
for bundle_runs in bundle_groups:
results.append(self._archive_bundle_with_retry(session_maker, storage, bundle_runs))
with session_maker() as session:
results.append(self._archive_bundle(session, storage, bundle_runs))
return results
results = []
max_workers = min(self.workers, len(bundle_groups))
def archive_in_worker(bundle_runs: Sequence[WorkflowRun]) -> ArchiveResult:
return self._archive_bundle_with_retry(session_maker, storage, bundle_runs)
with session_maker() as session:
return self._archive_bundle(session, storage, bundle_runs)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(archive_in_worker, bundle_runs) for bundle_runs in bundle_groups]
@ -575,39 +549,6 @@ class WorkflowRunArchiver:
results.append(future.result())
return results
def _archive_bundle_with_retry(
self,
session_maker: sessionmaker[Session],
storage: ArchiveStorage | None,
runs: Sequence[WorkflowRun],
) -> ArchiveResult:
identity = self._build_bundle_identity(runs)
try:
return self._run_with_db_retry(
f"archive workflow run bundle {identity.bundle_id}",
lambda: self._archive_bundle_once(session_maker, storage, runs),
)
except Exception as exc:
logger.exception("Failed to archive workflow run bundle %s after retries", identity.bundle_id)
return ArchiveResult(
bundle_id=identity.bundle_id,
tenant_id=identity.tenant_id,
object_prefix=identity.object_prefix,
run_count=len(runs),
success=False,
error=str(exc),
)
def _archive_bundle_once(
self,
session_maker: sessionmaker[Session],
storage: ArchiveStorage | None,
runs: Sequence[WorkflowRun],
) -> ArchiveResult:
with session_maker() as session:
return self._archive_bundle(session, storage, runs)
def _archive_bundle(
self,
session: Session,
@ -704,12 +645,9 @@ class WorkflowRunArchiver:
result.success = True
except Exception as e:
if self._is_retryable_db_disconnect(e):
self._safe_rollback(session, identity.bundle_id)
raise
logger.exception("Failed to archive workflow run bundle %s", identity.bundle_id)
result.error = str(e)
self._safe_rollback(session, identity.bundle_id)
session.rollback()
result.elapsed_time = time.time() - start_time
return result
@ -856,9 +794,6 @@ class WorkflowRunArchiver:
end_before = self.end_before
if end_before is None:
raise ValueError("archive window end must be set")
formatted_end_before = self._format_window_datetime(end_before)
if formatted_end_before is None:
raise ValueError("archive window end must be set")
return ArchiveManifestDict(
schema_version=ARCHIVE_BUNDLE_SCHEMA_VERSION,
archive_format=ARCHIVE_BUNDLE_FORMAT,
@ -878,7 +813,7 @@ class WorkflowRunArchiver:
archived_at=datetime.datetime.now(datetime.UTC).isoformat(),
campaign_id=self.campaign_id,
archive_window_start=self._format_window_datetime(self.start_from),
archive_window_end=formatted_end_before,
archive_window_end=end_before.isoformat(),
run_shard=identity.shard,
tables=tables,
run_ids=[run.id for run in sorted_runs],

View File

@ -1,66 +0,0 @@
import logging
import time
from collections.abc import Callable
from sqlalchemy.exc import DBAPIError
from sqlalchemy.exc import OperationalError as SQLAlchemyOperationalError
DEFAULT_DB_RETRY_ATTEMPTS = 3
DEFAULT_DB_RETRY_DELAYS_SECONDS = (1.0, 2.0)
_DB_DISCONNECT_PATTERNS = (
"server closed the connection unexpectedly",
"connection already closed",
"closed the connection",
"connection not open",
"terminating connection",
"connection reset",
"broken pipe",
"connection invalidated",
)
def is_retryable_db_disconnect(exc: BaseException) -> bool:
if isinstance(exc, DBAPIError) and exc.connection_invalidated:
return True
if not _is_db_operational_error(exc):
return False
original_exception = exc.orig if isinstance(exc, DBAPIError) else None
message = f"{exc} {original_exception or ''}".lower()
return any(pattern in message for pattern in _DB_DISCONNECT_PATTERNS)
def run_with_db_retry[T](
operation_name: str,
operation: Callable[[], T],
*,
logger: logging.Logger,
attempts: int = DEFAULT_DB_RETRY_ATTEMPTS,
delays_seconds: tuple[float, ...] = DEFAULT_DB_RETRY_DELAYS_SECONDS,
) -> T:
for attempt in range(1, attempts + 1):
try:
return operation()
except Exception as exc:
if not is_retryable_db_disconnect(exc) or attempt == attempts:
raise
delay = delays_seconds[min(attempt - 1, len(delays_seconds) - 1)]
logger.warning(
"Retrying %s after retryable DB disconnect (attempt %s/%s, sleep %.1fs)",
operation_name,
attempt,
attempts,
delay,
exc_info=True,
)
time.sleep(delay)
raise RuntimeError(f"{operation_name} did not complete")
def _is_db_operational_error(exc: BaseException) -> bool:
if isinstance(exc, SQLAlchemyOperationalError):
return True
return exc.__class__.__name__ == "OperationalError" and exc.__class__.__module__.startswith(("psycopg", "psycopg2"))

View File

@ -6,7 +6,7 @@ import uuid
from datetime import UTC, datetime
from typing import TypedDict, cast
from sqlalchemy import select, update
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.db.session_factory import session_factory
@ -912,11 +912,12 @@ class SummaryIndexService:
# Disable summary records (don't delete)
now = naive_utc_now()
session.execute(
update(DocumentSegmentSummary)
.where(DocumentSegmentSummary.id.in_(s.id for s in summaries))
.values(enabled=False, disabled_at=now, disabled_by=disabled_by)
)
for summary in summaries:
summary.enabled = False
summary.disabled_at = now
summary.disabled_by = disabled_by
session.add(summary)
session.commit()
logger.info("Disabled %s summary records for dataset %s", len(summaries), dataset.id)

View File

@ -6,10 +6,8 @@ from unittest.mock import ANY, MagicMock, patch
import pyarrow as pa
import pyarrow.parquet as pq
import pytest
from sqlalchemy.exc import OperationalError
from services.retention.workflow_run.archive_paid_plan_workflow_run import (
ArchiveResult,
ArchiveSummary,
WorkflowRunArchiver,
)
@ -34,30 +32,6 @@ class FakeArchiveStorage:
return sorted(key for key in self.objects if key.startswith(prefix))
def _db_disconnect_error() -> OperationalError:
return OperationalError(
"select 1",
{},
RuntimeError("server closed the connection unexpectedly"),
connection_invalidated=True,
)
def _run(run_id: str = "run-1"):
run = MagicMock()
run.id = run_id
run.tenant_id = "tenant-1"
run.created_at = datetime.datetime(2025, 3, 15, 10, 0, 0)
return run
def _session_context(session):
context = MagicMock()
context.__enter__.return_value = session
context.__exit__.return_value = False
return context
class TestWorkflowRunArchiverInit:
def test_start_from_without_end_before_raises(self):
with pytest.raises(ValueError, match="start_from and end_before must be provided together"):
@ -165,32 +139,6 @@ class TestWorkflowRunArchiverInit:
repo.get_runs_batch_by_time_range.assert_called_once()
assert repo.get_runs_batch_by_time_range.call_args.kwargs["tenant_ids"] == ["tenant-b"]
def test_get_runs_batch_retries_retryable_db_disconnect(self):
repo = MagicMock()
repo.get_runs_batch_by_time_range.side_effect = [_db_disconnect_error(), []]
archiver = WorkflowRunArchiver(workflow_run_repo=repo)
with patch("services.retention.workflow_run.db_retry.time.sleep") as sleep:
runs = archiver._get_runs_batch(None)
assert runs == []
assert repo.get_runs_batch_by_time_range.call_count == 2
sleep.assert_called_once_with(1.0)
def test_get_runs_batch_does_not_retry_non_db_broken_pipe_error(self):
repo = MagicMock()
repo.get_runs_batch_by_time_range.side_effect = RuntimeError("broken pipe")
archiver = WorkflowRunArchiver(workflow_run_repo=repo)
with (
patch("services.retention.workflow_run.db_retry.time.sleep") as sleep,
pytest.raises(RuntimeError, match="broken pipe"),
):
archiver._get_runs_batch(None)
repo.get_runs_batch_by_time_range.assert_called_once()
sleep.assert_not_called()
def test_start_message_includes_shard(self):
archiver = WorkflowRunArchiver(tenant_prefixes=["0"], run_shard_index=1, run_shard_total=4)
@ -403,72 +351,6 @@ class TestDryRunArchive:
assert summary.table_stats["workflow_app_logs"].size_bytes == 32
class TestArchiveDbRetry:
def test_archive_bundle_groups_retries_with_fresh_session(self):
archiver = WorkflowRunArchiver(days=90)
run = _run()
session_maker = MagicMock(
side_effect=[
_session_context(MagicMock(name="session-1")),
_session_context(MagicMock(name="session-2")),
]
)
success = ArchiveResult(
bundle_id=archiver._build_bundle_identity([run]).bundle_id,
tenant_id=run.tenant_id,
object_prefix=archiver._build_bundle_identity([run]).object_prefix,
run_count=1,
success=True,
)
with (
patch.object(archiver, "_archive_bundle", side_effect=[_db_disconnect_error(), success]) as archive_bundle,
patch("services.retention.workflow_run.db_retry.time.sleep") as sleep,
):
results = archiver._archive_bundle_groups(session_maker, MagicMock(), [[run]])
assert results == [success]
assert archive_bundle.call_count == 2
assert session_maker.call_count == 2
sleep.assert_called_once_with(1.0)
def test_archive_bundle_groups_returns_failed_result_after_retry_exhaustion(self):
archiver = WorkflowRunArchiver(days=90)
run = _run()
session_maker = MagicMock(
side_effect=[
_session_context(MagicMock(name="session-1")),
_session_context(MagicMock(name="session-2")),
_session_context(MagicMock(name="session-3")),
]
)
with (
patch.object(archiver, "_archive_bundle", side_effect=[_db_disconnect_error()] * 3) as archive_bundle,
patch("services.retention.workflow_run.db_retry.time.sleep") as sleep,
):
results = archiver._archive_bundle_groups(session_maker, MagicMock(), [[run]])
assert len(results) == 1
assert results[0].success is False
assert "server closed the connection unexpectedly" in (results[0].error or "")
assert archive_bundle.call_count == archiver.DB_RETRY_ATTEMPTS
assert session_maker.call_count == archiver.DB_RETRY_ATTEMPTS
assert sleep.call_count == archiver.DB_RETRY_ATTEMPTS - 1
def test_archive_bundle_uses_safe_rollback_when_failure_rolls_back_badly(self):
archiver = WorkflowRunArchiver(days=90, dry_run=True)
session = MagicMock()
session.rollback.side_effect = RuntimeError("rollback failed")
with patch.object(archiver, "_extract_bundle_data", side_effect=RuntimeError("extract failed")):
result = archiver._archive_bundle(session, None, [_run()])
assert result.success is False
assert result.error == "extract failed"
session.rollback.assert_called_once()
class TestArchiveRunIdempotency:
def _index_payload(self, archiver: WorkflowRunArchiver, run_ids: list[str], run) -> tuple[str, bytes]:
identity = archiver._build_bundle_identity([run])

View File

@ -1,139 +0,0 @@
import datetime
from unittest.mock import MagicMock
import click
import pytest
from sqlalchemy.exc import OperationalError
from commands import retention
def _db_disconnect_error() -> OperationalError:
return OperationalError(
"select 1",
{},
RuntimeError("server closed the connection unexpectedly"),
connection_invalidated=True,
)
def _session_context(session):
context = MagicMock()
context.__enter__.return_value = session
context.__exit__.return_value = False
return context
def test_resolve_archive_tenant_ids_from_plan_uses_explicit_sessions(monkeypatch):
end_before = datetime.datetime(2025, 4, 1, tzinfo=datetime.UTC)
sessions = [MagicMock(name="session-a"), MagicMock(name="session-b")]
session_maker = MagicMock(side_effect=[_session_context(sessions[0]), _session_context(sessions[1])])
calls = []
def get_candidate_tenants(session, prefix, *, start_from, end_before):
calls.append((session, prefix, start_from, end_before))
return [f"{prefix}-paid", f"{prefix}-free"]
monkeypatch.setattr(retention, "_get_archive_candidate_tenant_ids_by_prefix", get_candidate_tenants)
monkeypatch.setattr(
retention,
"_filter_paid_workflow_archive_tenant_ids",
lambda tenant_ids: (["a-paid", "b-paid"], ["a-free", "b-free"]),
)
tenant_plan = retention._resolve_archive_tenant_ids_from_plan(
session_maker=session_maker,
tenant_ids=None,
tenant_prefixes=["a", "b"],
start_from=None,
end_before=end_before,
)
assert tenant_plan["archive_tenant_ids"] == ["a-paid", "b-paid"]
assert tenant_plan["paid_tenant_ids"] == ["a-paid", "b-paid"]
assert tenant_plan["unpaid_tenant_ids"] == ["a-free", "b-free"]
assert calls == [
(sessions[0], "a", None, end_before),
(sessions[1], "b", None, end_before),
]
def test_safe_remove_scoped_session_discards_registry_and_disposes_after_remove_error(monkeypatch):
fake_db = MagicMock()
fake_db.session.remove.side_effect = RuntimeError("server closed the connection unexpectedly")
monkeypatch.setattr(retention, "db", fake_db)
retention._safe_remove_scoped_session("archive workflow run command")
fake_db.session.remove.assert_called_once()
fake_db.session.registry.clear.assert_called_once()
fake_db.engine.dispose.assert_called_once()
def test_archive_command_db_retry_retries_retryable_db_disconnect(monkeypatch):
operation = MagicMock(side_effect=[_db_disconnect_error(), "ok"])
sleep = MagicMock()
monkeypatch.setattr("services.retention.workflow_run.db_retry.time.sleep", sleep)
result = retention._run_archive_command_db_retry("archive plan", operation)
assert result == "ok"
assert operation.call_count == 2
sleep.assert_called_once_with(1.0)
def test_archive_plan_prefix_stats_retries_count_query_with_fresh_session(monkeypatch):
end_before = datetime.datetime(2025, 4, 1, tzinfo=datetime.UTC)
sessions = [MagicMock(name="session-1"), MagicMock(name="session-2")]
sessions[0].scalar.side_effect = _db_disconnect_error()
sessions[1].scalar.side_effect = [7, 9]
session_maker = MagicMock(side_effect=[_session_context(sessions[0]), _session_context(sessions[1])])
sleep = MagicMock()
monkeypatch.setattr(
retention,
"_get_archive_candidate_tenant_ids_by_prefix",
lambda session, prefix, *, start_from, end_before: [f"{prefix}-tenant"],
)
monkeypatch.setattr("services.retention.workflow_run.db_retry.time.sleep", sleep)
stats = retention._get_archive_plan_prefix_stats(
session_maker,
"a",
start_from=None,
end_before=end_before,
)
assert stats["tenant_ids"] == ["a-tenant"]
assert stats["workflow_runs"] == 7
assert stats["workflow_node_executions"] == 9
assert session_maker.call_count == 2
sleep.assert_called_once_with(1.0)
def test_archive_workflow_runs_raises_click_exception_when_tenant_plan_fails(monkeypatch):
fake_db = MagicMock()
monkeypatch.setattr(retention, "db", fake_db)
monkeypatch.setattr(
retention,
"_resolve_archive_tenant_ids_from_plan",
MagicMock(side_effect=RuntimeError("tenant plan failed")),
)
with pytest.raises(click.ClickException, match="Failed to resolve workflow archive tenant plan"):
retention.archive_workflow_runs.callback(
tenant_ids="tenant-1",
tenant_prefixes=None,
before_days=90,
from_days_ago=None,
to_days_ago=None,
start_from=None,
end_before=None,
batch_size=10000,
workers=1,
run_shard_index=None,
run_shard_total=None,
limit=None,
dry_run=True,
delete_after_archive=False,
)

View File

@ -116,12 +116,10 @@ class TestAgentChatAppGeneratorGenerate:
)
thread_obj = mocker.MagicMock()
thread_constructor = mocker.patch(
mocker.patch(
"core.app.apps.agent_chat.app_generator.threading.Thread",
return_value=thread_obj,
)
session = mocker.MagicMock()
mocker.patch("core.app.apps.agent_chat.app_generator.db.session", return_value=session)
mocker.patch(
"core.app.apps.agent_chat.app_generator.AgentChatAppGenerateResponseConverter.convert",
@ -146,7 +144,6 @@ class TestAgentChatAppGeneratorGenerate:
assert result == {"result": "ok"}
assert generate_entity.call_args.kwargs["extras"]["trace_session_id"] == "session-1"
assert thread_constructor.call_args.kwargs["kwargs"]["session"] is session
thread_obj.start.assert_called_once()
def test_generate_without_file_config(self, generator, mocker: MockerFixture):

View File

@ -1,32 +0,0 @@
import json
from pytest_mock import MockerFixture
from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType
def test_model_provider_credentials_cache_get_returns_decoded_dict(mocker: MockerFixture) -> None:
redis_client_mock = mocker.patch("core.helper.model_provider_cache.redis_client")
cache = ProviderCredentialsCache(
tenant_id="tenant",
identity_id="identity",
cache_type=ProviderCredentialsCacheType.PROVIDER,
)
payload = {"api_key": "secret"}
redis_client_mock.get.return_value = json.dumps(payload).encode("utf-8")
assert cache.get() == payload
def test_model_provider_credentials_cache_get_returns_none_for_invalid_utf8(mocker: MockerFixture) -> None:
redis_client_mock = mocker.patch("core.helper.model_provider_cache.redis_client")
cache = ProviderCredentialsCache(
tenant_id="tenant",
identity_id="identity",
cache_type=ProviderCredentialsCacheType.PROVIDER,
)
redis_client_mock.get.return_value = b"\xff"
assert cache.get() is None

View File

@ -1,24 +0,0 @@
import json
from pytest_mock import MockerFixture
from core.helper.provider_cache import ToolProviderCredentialsCache
def test_provider_credentials_cache_get_returns_decoded_dict(mocker: MockerFixture) -> None:
redis_client_mock = mocker.patch("core.helper.provider_cache.redis_client")
cache = ToolProviderCredentialsCache(tenant_id="tenant", provider="provider", credential_id="credential")
payload = {"api_key": "secret"}
redis_client_mock.get.return_value = json.dumps(payload).encode("utf-8")
assert cache.get() == payload
def test_provider_credentials_cache_get_returns_none_for_invalid_utf8(mocker: MockerFixture) -> None:
redis_client_mock = mocker.patch("core.helper.provider_cache.redis_client")
cache = ToolProviderCredentialsCache(tenant_id="tenant", provider="provider", credential_id="credential")
redis_client_mock.get.return_value = b"\xff"
assert cache.get() is None

View File

@ -38,21 +38,6 @@ def test_tool_parameter_cache_get_returns_none_for_invalid_json(mocker: MockerFi
assert cache.get() is None
def test_tool_parameter_cache_get_returns_none_for_invalid_utf8(mocker: MockerFixture) -> None:
redis_client_mock = mocker.patch("core.helper.tool_parameter_cache.redis_client")
cache = ToolParameterCache(
tenant_id="tenant",
provider="provider",
tool_name="tool",
cache_type=ToolParameterCacheType.PARAMETER,
identity_id="identity",
)
redis_client_mock.get.return_value = b"\xff"
assert cache.get() is None
def test_tool_parameter_cache_get_returns_none_when_key_is_missing(mocker: MockerFixture) -> None:
redis_client_mock = mocker.patch("core.helper.tool_parameter_cache.redis_client")
cache = ToolParameterCache(

View File

@ -85,10 +85,6 @@ def test_assembling_request_auth_header_assembly():
assert headers["Authorization"] == "Bearer abc"
tool.runtime.credentials = {"auth_type": "api_key_header", "api_key_header_prefix": "basic", "api_key_value": "abc"}
headers = tool.assembling_request(parameters={})
assert headers["Authorization"] == "Basic abc"
assert tool.runtime.credentials["api_key_value"] == "abc"
headers = tool.assembling_request(parameters={})
assert headers["Authorization"] == "Basic abc"

View File

@ -10,12 +10,9 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
import services.summary_index_service as summary_module
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from models.dataset import DocumentSegmentSummary
from models.enums import SegmentStatus, SummaryStatus
from services.summary_index_service import SummaryIndexService
@ -656,48 +653,32 @@ def test_generate_summaries_for_document_applies_segment_ids_and_only_parent_chu
session.scalars.assert_called()
def test_disable_summaries_for_segments_updates_sqlite_records() -> None:
dataset = SimpleNamespace(id="dataset-1", indexing_technique=IndexTechniqueType.ECONOMY)
engine = create_engine("sqlite+pysqlite:///:memory:")
DocumentSegmentSummary.__table__.create(engine)
summary_rows = [
{
"id": "sum-1",
"dataset_id": dataset.id,
"document_id": "doc-1",
"chunk_id": "seg-1",
"summary_content": "s",
"summary_index_node_id": "n1",
"status": SummaryStatus.COMPLETED,
"enabled": True,
},
{
"id": "sum-2",
"dataset_id": dataset.id,
"document_id": "doc-1",
"chunk_id": "seg-1",
"summary_content": "s",
"summary_index_node_id": None,
"status": SummaryStatus.COMPLETED,
"enabled": True,
},
]
with engine.begin() as connection:
connection.execute(DocumentSegmentSummary.__table__.insert(), summary_rows)
def test_disable_summaries_for_segments_handles_vector_delete_error(monkeypatch: pytest.MonkeyPatch) -> None:
dataset = _dataset()
summary1 = _summary_record(summary_content="s", node_id="n1")
summary2 = _summary_record(summary_content="s", node_id=None)
session_maker = sessionmaker(bind=engine, expire_on_commit=False)
summary_module.session_factory.configure(engine, expire_on_commit=False)
session = MagicMock()
session.scalars.return_value.all.return_value = [summary1, summary2]
monkeypatch.setattr(
summary_module,
"session_factory",
SimpleNamespace(create_session=MagicMock(return_value=_SessionContext(session))),
)
monkeypatch.setattr(
summary_module,
"Vector",
MagicMock(return_value=MagicMock(delete_by_ids=MagicMock(side_effect=RuntimeError("boom")))),
)
monkeypatch.setitem(
sys.modules, "libs.datetime_utils", SimpleNamespace(naive_utc_now=MagicMock(return_value=datetime(2024, 1, 1)))
)
SummaryIndexService.disable_summaries_for_segments(dataset, segment_ids=["seg-1"], disabled_by="u")
with session_maker() as session:
summaries = session.scalars(select(DocumentSegmentSummary).order_by(DocumentSegmentSummary.id)).all()
assert [(summary.id, summary.enabled, summary.disabled_by) for summary in summaries] == [
("sum-1", False, "u"),
("sum-2", False, "u"),
]
assert all(summary.disabled_at is not None for summary in summaries)
assert summary1.enabled is False
assert summary1.disabled_by == "u"
session.commit.assert_called_once()
def test_disable_summaries_for_segments_no_summaries_noop(monkeypatch: pytest.MonkeyPatch) -> None:

4
api/uv.lock generated
View File

@ -1280,7 +1280,7 @@ wheels = [
[[package]]
name = "dify-agent"
version = "1.16.0rc1"
version = "0.1.0"
source = { editable = "../dify-agent" }
dependencies = [
{ name = "anyio" },
@ -1338,7 +1338,7 @@ docs = [
[[package]]
name = "dify-api"
version = "1.16.0rc1"
version = "1.15.0"
source = { virtual = "." }
dependencies = [
{ name = "aliyun-log-python-sdk" },

View File

@ -40,10 +40,8 @@ DIFY_AGENT_STUB_API_BASE_URL=http://localhost:5050/agent-stub
# Optional bind override used only when DIFY_AGENT_STUB_API_BASE_URL uses grpc://.
DIFY_AGENT_STUB_GRPC_BIND_ADDRESS=
# Server-wide root secret used to derive Agent Stub JWE keys.
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY
# Required when DIFY_AGENT_STUB_API_BASE_URL is set; must be unpadded base64url for 32 bytes.
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-base64url-32-byte-secret
# Shared plugin-daemon HTTP client timeouts and limits.
# Plugin-daemon HTTP connect timeout in seconds.

View File

@ -75,9 +75,8 @@ See `.example.env` for the full server settings template.
If you plan to run `dify.shell`, also configure `DIFY_AGENT_SHELLCTL_ENTRYPOINT`
and, when shell jobs need to call back with the `dify-agent` command, set
`DIFY_AGENT_STUB_API_BASE_URL`. The supplied default configs include a
development `DIFY_AGENT_SERVER_SECRET_KEY`, but production deployments should
override it with a unique 32-byte base64url value as documented in `.example.env`.
`DIFY_AGENT_STUB_API_BASE_URL` plus a 32-byte base64url
`DIFY_AGENT_SERVER_SECRET_KEY` as documented in `.example.env`.
## Start the Dify Agent server

View File

@ -42,7 +42,7 @@ also reads `.env` and `dify-agent/.env` when present.
| `DIFY_AGENT_SHELLCTL_AUTH_TOKEN` | empty | Optional bearer token sent to the shellctl server. |
| `DIFY_AGENT_STUB_API_BASE_URL` | empty | Public Agent Stub API base URL reachable from shellctl-managed remote machines. HTTP may be the service root or `/agent-stub`; gRPC must be `grpc://host:port`. Enables `DIFY_AGENT_STUB_*` env injection for user `shell.run` jobs. |
| `DIFY_AGENT_STUB_GRPC_BIND_ADDRESS` | empty | Optional `host:port` bind override used only when `DIFY_AGENT_STUB_API_BASE_URL` uses `grpc://`. |
| `DIFY_AGENT_SERVER_SECRET_KEY` | empty | Security-sensitive server-wide root secret used to derive the JWE encryption key for Agent Stub bearer tokens; required when `DIFY_AGENT_STUB_API_BASE_URL` is set. The supplied default config uses a development value; set a unique unpadded base64url 32-byte secret in production. |
| `DIFY_AGENT_SERVER_SECRET_KEY` | empty | Server-wide root secret used to derive Agent Stub JWE keys; required when `DIFY_AGENT_STUB_API_BASE_URL` is set and must be unpadded base64url for 32 bytes. |
| `DIFY_AGENT_PLUGIN_DAEMON_CONNECT_TIMEOUT` | `10` | Plugin-daemon HTTP connect timeout in seconds. |
| `DIFY_AGENT_PLUGIN_DAEMON_READ_TIMEOUT` | `600` | Plugin-daemon HTTP read timeout in seconds. |
| `DIFY_AGENT_PLUGIN_DAEMON_WRITE_TIMEOUT` | `30` | Plugin-daemon HTTP write timeout in seconds. |
@ -64,11 +64,9 @@ DIFY_AGENT_INNER_API_URL=http://localhost:5001
DIFY_AGENT_INNER_API_KEY=replace-with-dify-inner-api-key-for-plugin
DIFY_AGENT_SHELLCTL_ENTRYPOINT=http://127.0.0.1:5004
DIFY_AGENT_SHELLCTL_AUTH_TOKEN=replace-with-shellctl-token
# Generate with: python -c 'import base64, secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode())'
DIFY_AGENT_STUB_API_BASE_URL=https://agent.example.com/agent-stub
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-base64url-32-byte-secret
```
Run records and event streams use the same retention. Status writes refresh the

View File

@ -56,22 +56,18 @@ with `dify-agent ...`, also enable the Agent Stub:
```env
DIFY_AGENT_STUB_API_BASE_URL=https://agent.example.com/agent-stub
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-base64url-32-byte-secret
```
HTTP `DIFY_AGENT_STUB_API_BASE_URL` may be either the service root or the
explicit `/agent-stub` API root; the server normalizes the service root to
`/agent-stub`. Other HTTP paths are rejected at startup.
The supplied Docker and `.example.env` configs use a development
`DIFY_AGENT_SERVER_SECRET_KEY`. Override it in production with unpadded base64url
text for exactly 32 decoded bytes. One way to generate it is:
`DIFY_AGENT_SERVER_SECRET_KEY` must be unpadded base64url text for exactly 32
decoded bytes. One way to generate it is:
```bash
python -c 'import secrets; print(secrets.token_urlsafe(32))'
python -c 'import base64, secrets; print(base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode())'
```
## Client request shape

View File

@ -1,6 +1,6 @@
[project]
name = "dify-agent"
version = "1.16.0-rc1"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12,<4.0"

2
dify-agent/uv.lock generated
View File

@ -590,7 +590,7 @@ wheels = [
[[package]]
name = "dify-agent"
version = "1.16.0rc1"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },

View File

@ -262,10 +262,7 @@ DIFY_AGENT_INNER_API_KEY=
DIFY_AGENT_SHELLCTL_ENTRYPOINT=http://local_sandbox:5004
DIFY_AGENT_SHELLCTL_AUTH_TOKEN=
DIFY_AGENT_STUB_API_BASE_URL=http://agent_backend:5050/agent-stub
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-base64url-32-byte-secret
# Nginx and Docker Compose
NGINX_SERVER_NAME=_

View File

@ -220,7 +220,7 @@ services:
# API service
api:
<<: *shared-api-worker-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
environment:
MODE: api
SENTRY_DSN: ${API_SENTRY_DSN:-}
@ -267,7 +267,7 @@ services:
# WebSocket service for workflow collaboration.
api_websocket:
<<: *shared-api-worker-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
profiles:
- collaboration
environment:
@ -293,7 +293,7 @@ services:
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
<<: *shared-worker-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
environment:
MODE: worker
SENTRY_DSN: ${API_SENTRY_DSN:-}
@ -339,7 +339,7 @@ services:
# Celery beat for scheduling periodic tasks.
worker_beat:
<<: *shared-worker-beat-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
environment:
MODE: beat
depends_on:
@ -372,7 +372,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.16.0-rc1
image: langgenius/dify-web:1.15.0
restart: always
env_file:
- path: ./envs/core-services/web.env
@ -524,7 +524,7 @@ services:
# Local sandbox for Dify Agent shell workspaces.
local_sandbox:
image: langgenius/dify-agent-local-sandbox:1.16.0-rc1
image: langgenius/dify-agent-local-sandbox:1.15.0
restart: always
environment:
SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}
@ -626,7 +626,7 @@ services:
# Dify Agent backend service.
agent_backend:
image: langgenius/dify-agent-backend:1.16.0-rc1
image: langgenius/dify-agent-backend:1.15.0
restart: always
env_file:
- path: ./envs/core-services/dify-agent.env
@ -646,10 +646,7 @@ services:
DIFY_AGENT_SHELLCTL_ENTRYPOINT: ${DIFY_AGENT_SHELLCTL_ENTRYPOINT:-http://local_sandbox:5004}
DIFY_AGENT_SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}
DIFY_AGENT_STUB_API_BASE_URL: ${DIFY_AGENT_STUB_API_BASE_URL:-http://agent_backend:5050/agent-stub}
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY: ${DIFY_AGENT_SERVER_SECRET_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY}
DIFY_AGENT_SERVER_SECRET_KEY: ${DIFY_AGENT_SERVER_SECRET_KEY:-replace-with-base64url-32-byte-secret}
DIFY_AGENT_SHUTDOWN_GRACE_SECONDS: ${DIFY_AGENT_SHUTDOWN_GRACE_SECONDS:-30}
DIFY_AGENT_RUN_RETENTION_SECONDS: ${DIFY_AGENT_RUN_RETENTION_SECONDS:-259200}
depends_on:

View File

@ -226,7 +226,7 @@ services:
# API service
api:
<<: *shared-api-worker-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
environment:
MODE: api
SENTRY_DSN: ${API_SENTRY_DSN:-}
@ -273,7 +273,7 @@ services:
# WebSocket service for workflow collaboration.
api_websocket:
<<: *shared-api-worker-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
profiles:
- collaboration
environment:
@ -299,7 +299,7 @@ services:
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
<<: *shared-worker-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
environment:
MODE: worker
SENTRY_DSN: ${API_SENTRY_DSN:-}
@ -345,7 +345,7 @@ services:
# Celery beat for scheduling periodic tasks.
worker_beat:
<<: *shared-worker-beat-config
image: langgenius/dify-api:1.16.0-rc1
image: langgenius/dify-api:1.15.0
environment:
MODE: beat
depends_on:
@ -378,7 +378,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.16.0-rc1
image: langgenius/dify-web:1.15.0
restart: always
env_file:
- path: ./envs/core-services/web.env
@ -530,7 +530,7 @@ services:
# Local sandbox for Dify Agent shell workspaces.
local_sandbox:
image: langgenius/dify-agent-local-sandbox:1.16.0-rc1
image: langgenius/dify-agent-local-sandbox:1.15.0
restart: always
environment:
SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}
@ -632,7 +632,7 @@ services:
# Dify Agent backend service.
agent_backend:
image: langgenius/dify-agent-backend:1.16.0-rc1
image: langgenius/dify-agent-backend:1.15.0
restart: always
env_file:
- path: ./envs/core-services/dify-agent.env
@ -652,10 +652,7 @@ services:
DIFY_AGENT_SHELLCTL_ENTRYPOINT: ${DIFY_AGENT_SHELLCTL_ENTRYPOINT:-http://local_sandbox:5004}
DIFY_AGENT_SHELLCTL_AUTH_TOKEN: ${DIFY_AGENT_SHELLCTL_AUTH_TOKEN:-}
DIFY_AGENT_STUB_API_BASE_URL: ${DIFY_AGENT_STUB_API_BASE_URL:-http://agent_backend:5050/agent-stub}
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY: ${DIFY_AGENT_SERVER_SECRET_KEY:-MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY}
DIFY_AGENT_SERVER_SECRET_KEY: ${DIFY_AGENT_SERVER_SECRET_KEY:-replace-with-base64url-32-byte-secret}
DIFY_AGENT_SHUTDOWN_GRACE_SECONDS: ${DIFY_AGENT_SHUTDOWN_GRACE_SECONDS:-30}
DIFY_AGENT_RUN_RETENTION_SECONDS: ${DIFY_AGENT_RUN_RETENTION_SECONDS:-259200}
depends_on:

View File

@ -22,7 +22,4 @@ DIFY_AGENT_INNER_API_KEY=
DIFY_AGENT_SHELLCTL_ENTRYPOINT=http://local_sandbox:5004
DIFY_AGENT_SHELLCTL_AUTH_TOKEN=
DIFY_AGENT_STUB_API_BASE_URL=http://agent_backend:5050/agent-stub
# This is security-sensitive: it derives the JWE encryption key for Agent Stub bearer tokens.
# Replace this development default in production.
# Generate one with: python -c 'import secrets; print(secrets.token_urlsafe(32))'
DIFY_AGENT_SERVER_SECRET_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY
DIFY_AGENT_SERVER_SECRET_KEY=replace-with-base64url-32-byte-secret

View File

@ -6,8 +6,8 @@ CONSOLE_WEB_URL=
SERVICE_API_URL=
TRIGGER_URL=http://localhost
APP_WEB_URL=
# FILES_URL=
# INTERNAL_FILES_URL=
FILES_URL=
INTERNAL_FILES_URL=
LANG=C.UTF-8
LC_ALL=C.UTF-8
PYTHONIOENCODING=utf-8

View File

@ -5,6 +5,8 @@ app:
max_workers: 4
max_requests: 50
worker_timeout: 5
python_path: /opt/python/bin/python3
nodejs_path: /usr/local/bin/node
enable_network: True # please make sure there is no network risk in your environment
allowed_syscalls: # please leave it empty if you have no idea how seccomp works
proxy:

View File

@ -92,6 +92,8 @@
- **مشروع Dify للشركات / المؤسسات</br>**
نحن نوفر ميزات إضافية مركزة على الشركات. [جدول اجتماع معنا](https://cal.com/guchenhe/30min) أو [أرسل لنا بريدًا إلكترونيًا](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) لمناقشة احتياجات الشركات. </br>
> بالنسبة للشركات الناشئة والشركات الصغيرة التي تستخدم خدمات AWS، تحقق من [Dify Premium على AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) ونشرها في شبكتك الخاصة على AWS VPC بنقرة واحدة. إنها عرض AMI بأسعار معقولة مع خيار إنشاء تطبيقات بشعار وعلامة تجارية مخصصة.
## البقاء قدمًا
قم بإضافة نجمة إلى Dify على GitHub وتلق تنبيهًا فوريًا بالإصدارات الجديدة.

View File

@ -129,6 +129,8 @@ LLM ফাংশন কলিং বা ReAct উপর ভিত্তি ক
- **এন্টারপ্রাইজ / প্রতিষ্ঠানের জন্য Dify</br>**
আমরা এন্টারপ্রাইজ/প্রতিষ্ঠান-কেন্দ্রিক সেবা প্রদান করে থাকি । [এই চ্যাটবটের মাধ্যমে আপনার প্রশ্নগুলি আমাদের জন্য লগ করুন।](https://udify.app/chat/22L1zSxg6yW1cWQg) অথবা [আমাদের ইমেল পাঠান](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) আপনার চাহিদা সম্পর্কে আলোচনা করার জন্য। </br>
> AWS ব্যবহারকারী স্টার্টআপ এবং ছোট ব্যবসার জন্য, [AWS মার্কেটপ্লেসে Dify Premium](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) দেখুন এবং এক-ক্লিকের মাধ্যমে এটি আপনার নিজস্ব AWS VPC-তে ডিপ্লয় করুন। এটি একটি সাশ্রয়ী মূল্যের AMI অফার, যাতে কাস্টম লোগো এবং ব্র্যান্ডিং সহ অ্যাপ তৈরির সুবিধা আছে।
## এগিয়ে থাকুন
GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন এবং নতুন রিলিজের খবর তাৎক্ষণিকভাবে পান।

View File

@ -128,6 +128,8 @@ Alle Dify-Angebote kommen mit entsprechenden APIs, sodass Sie Dify mühelos in I
- **Dify für Unternehmen / Organisationen</br>**
Wir bieten zusätzliche, unternehmensspezifische Funktionen. [Über diesen Chatbot können Sie uns Ihre Fragen mitteilen](https://udify.app/chat/22L1zSxg6yW1cWQg) oder [senden Sie uns eine E-Mail](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry), um Ihre unternehmerischen Bedürfnisse zu besprechen. </br>
> Für Startups und kleine Unternehmen, die AWS nutzen, schauen Sie sich [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) an und stellen Sie es mit nur einem Klick in Ihrer eigenen AWS VPC bereit. Es handelt sich um ein erschwingliches AMI-Angebot mit der Option, Apps mit individuellem Logo und Branding zu erstellen.
## Immer einen Schritt voraus
Star Dify auf GitHub und lassen Sie sich sofort über neue Releases benachrichtigen.

View File

@ -103,6 +103,8 @@ Todas las ofertas de Dify vienen con APIs correspondientes, por lo que podrías
- **Dify para Empresas / Organizaciones</br>**
Proporcionamos características adicionales centradas en la empresa. [Envíanos un correo electrónico](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) para discutir las necesidades empresariales. </br>
> Para startups y pequeñas empresas que utilizan AWS, echa un vistazo a [Dify Premium en AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) e impleméntalo en tu propio VPC de AWS con un clic. Es una AMI asequible que ofrece la opción de crear aplicaciones con logotipo y marca personalizados.
## Manteniéndote al tanto
Dale estrella a Dify en GitHub y serás notificado instantáneamente de las nuevas versiones.

View File

@ -103,6 +103,8 @@ Toutes les offres de Dify sont accompagnées d'API correspondantes, vous permett
- **Dify pour les entreprises / organisations</br>**
Nous proposons des fonctionnalités supplémentaires adaptées aux entreprises. [Envoyez-nous un e-mail](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) pour discuter des besoins de l'entreprise. </br>
> Pour les startups et les petites entreprises utilisant AWS, consultez [Dify Premium sur AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) et déployez-le dans votre propre VPC AWS en un clic. C'est une offre AMI abordable avec la possibilité de créer des applications avec un logo et une marque personnalisés.
## Rester en avance
Mettez une étoile à Dify sur GitHub et soyez instantanément informé des nouvelles versions.

View File

@ -130,6 +130,8 @@ Dify की सभी सेवाएँ संबंधित APIs के स
हम अतिरिक्त एंटरप्राइज़-केंद्रित सुविधाएँ प्रदान करते हैं।\
[इस चैटबॉट के माध्यम से हमें अपने प्रश्न भेजें](https://udify.app/chat/22L1zSxg6yW1cWQg) या [हमें ईमेल भेजें](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) ताकि हम एंटरप्राइज़ आवश्यकताओं पर चर्चा कर सकें। <br/>
> AWS का उपयोग करने वाले स्टार्टअप्स और छोटे व्यवसायों के लिए, [AWS Marketplace पर Dify Premium](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) देखें और इसे एक क्लिक में अपने AWS VPC पर डिप्लॉय करें। यह एक किफायती AMI ऑफ़रिंग है, जो आपको कस्टम लोगो और ब्रांडिंग के साथ ऐप्स बनाने की अनुमति देती है।
## आगे बने रहें
GitHub पर Dify को स्टार करें और नए रिलीज़ की सूचना तुरंत प्राप्त करें।

View File

@ -128,6 +128,8 @@ Tutte le offerte di Dify sono dotate di API corrispondenti, permettendovi di int
- **Dify per Aziende / Organizzazioni<br/>**
Offriamo funzionalità aggiuntive specifiche per le aziende. Potete [scriverci via email](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) per discutere le vostre esigenze aziendali. <br/>
> Per startup e piccole imprese che utilizzano AWS, date un'occhiata a [Dify Premium su AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) e distribuitelo con un solo clic nel vostro AWS VPC. Si tratta di un'offerta AMI conveniente con l'opzione di creare app con logo e branding personalizzati.
## Resta Sempre Aggiornato
Mettete una stella a Dify su GitHub e ricevete notifiche immediate sui nuovi rilasci.

View File

@ -104,6 +104,8 @@ LLM Function CallingやReActに基づくエージェントの定義が可能で
- **企業/組織向けのDify</br>**
企業中心の機能を提供しています。[メールを送信](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry)して企業のニーズについて相談してください。 </br>
> AWSを使用しているスタートアップ企業や中小企業の場合は、[AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6)のDify Premiumをチェックして、ワンクリックで自分のAWS VPCにデプロイできます。さらに、手頃な価格のAMIオファリングとして、ロゴやブランディングをカスタマイズしてアプリケーションを作成するオプションがあります。
## 最新の情報を入手
GitHub上でDifyにスターを付けることで、Difyに関する新しいニュースを受け取れます。

View File

@ -97,6 +97,8 @@ Dify의 모든 제품에는 해당 API가 함께 제공되므로 Dify를 자신
- **기업 / 조직을 위한 Dify</br>**
우리는 추가적인 기업 중심 기능을 제공합니다. 잡거나 [이메일 보내기](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry)를 통해 기업 요구 사항을 논의하십시오. </br>
> AWS를 사용하는 스타트업 및 중소기업의 경우 [AWS Marketplace에서 Dify Premium](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6)을 확인하고 한 번의 클릭으로 자체 AWS VPC에 배포하십시오. 맞춤형 로고와 브랜딩이 포함된 앱을 생성할 수 있는 옵션이 포함된 저렴한 AMI 제품입니다.
## 앞서가기
GitHub에서 Dify에 별표를 찍어 새로운 릴리스를 즉시 알림 받으세요.

View File

@ -102,6 +102,8 @@ Todas os recursos do Dify vêm com APIs correspondentes, permitindo que você in
- **Dify para empresas/organizações</br>**
Oferecemos recursos adicionais voltados para empresas. Você pode [falar conosco por e-mail](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) para discutir necessidades empresariais. <br/>
> Para startups e pequenas empresas que utilizam AWS, confira o [Dify Premium no AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) e implemente no seu próprio AWS VPC com um clique. É uma oferta AMI acessível com a opção de criar aplicativos com logotipo e marca personalizados.
## Mantendo-se atualizado
Dê uma estrela no Dify no GitHub e seja notificado imediatamente sobre novos lançamentos.

View File

@ -125,6 +125,8 @@ AVse ponudbe Difyja so opremljene z ustreznimi API-ji, tako da lahko Dify brez t
- **Dify za podjetja/organizacije</br>**
Ponujamo dodatne funkcije, osredotočene na podjetja. Zabeležite svoja vprašanja prek tega klepetalnega robota ali nam pošljite e-pošto, da se pogovorimo o potrebah podjetja. </br>
> Za novoustanovljena podjetja in mala podjetja, ki uporabljajo AWS, si oglejte Dify Premium na AWS Marketplace in ga z enim klikom uvedite v svoj AWS VPC. To je cenovno ugodna ponudba AMI z možnostjo ustvarjanja aplikacij z logotipom in blagovno znamko po meri.
## Staying ahead
Star Dify on GitHub and be instantly notified of new releases.

View File

@ -103,6 +103,8 @@ All of Dify's offerings come with corresponding APIs, so you could effortlessly
- **Dify for Enterprise / Organizations</br>**
We provide additional enterprise-centric features. [Send us an email](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) to discuss enterprise needs. </br>
> For startups and small businesses using AWS, check out [Dify Premium on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) and deploy it to your own AWS VPC with one-click. It's an affordable AMI offering with the option to create apps with custom logo and branding.
## Staying ahead
Star Dify on GitHub and be instantly notified of new releases.

View File

@ -98,6 +98,8 @@ Dify'ın tüm özellikleri ilgili API'lerle birlikte gelir, böylece Dify'ı ken
- **Kurumlar / organizasyonlar için Dify</br>**
Ek kurumsal odaklı özellikler sunuyoruz. Kurumsal ihtiyaçları görüşmek için [bize bir e-posta gönderin](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry). </br>
> AWS kullanan startuplar ve küçük işletmeler için, [AWS Marketplace'deki Dify Premium'a](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) göz atın ve tek tıklamayla kendi AWS VPC'nize dağıtın. Bu, özel logo ve marka ile uygulamalar oluşturma seçeneğine sahip uygun fiyatlı bir AMI teklifdir.
## Güncel Kalma
GitHub'da Dify'a yıldız verin ve yeni sürümlerden anında haberdar olun.

View File

@ -98,6 +98,8 @@ Tất cả các dịch vụ của Dify đều đi kèm với các API tương
- **Dify cho doanh nghiệp / tổ chức</br>**
Chúng tôi cung cấp các tính năng bổ sung tập trung vào doanh nghiệp. [Gửi email cho chúng tôi](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry) để thảo luận về nhu cầu doanh nghiệp. <br/>
> Đối với các công ty khởi nghiệp và doanh nghiệp nhỏ sử dụng AWS, hãy xem [Dify Premium trên AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6) và triển khai nó vào AWS VPC của riêng bạn chỉ với một cú nhấp chuột. Đây là một AMI giá cả phải chăng với tùy chọn tạo ứng dụng với logo và thương hiệu tùy chỉnh.
## Luôn cập nhật
Yêu thích Dify trên GitHub và được thông báo ngay lập tức về các bản phát hành mới.

View File

@ -104,6 +104,8 @@ Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI
- **面向企业/组织的 Dify</br>**
我们提供额外的面向企业的功能。[给我们发送电子邮件](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry)讨论企业需求。 </br>
> 对于使用 AWS 的初创公司和中小型企业,请查看 [AWS Marketplace 上的 Dify 高级版](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6),并使用一键部署到您自己的 AWS VPC。它是一个价格实惠的 AMI 产品,提供了使用自定义徽标和品牌创建应用程序的选项。
## 保持领先
在 GitHub 上给 Dify Star并立即收到新版本的通知。

View File

@ -129,6 +129,8 @@ Dify 的所有功能都提供相應的 API因此您可以輕鬆地將 Dify
- **企業/組織版 Dify</br>**
我們提供額外的企業中心功能。[通過這個聊天機器人記錄您的問題](https://udify.app/chat/22L1zSxg6yW1cWQg)或[發送電子郵件給我們](mailto:business@dify.ai?subject=%5BGitHub%5DBusiness%20License%20Inquiry)討論企業需求。</br>
> 對於使用 AWS 的初創企業和小型企業,請查看 [AWS Marketplace 上的 Dify Premium](https://aws.amazon.com/marketplace/pp/prodview-t22mebxzwjhu6),並一鍵部署到您自己的 AWS VPC。這是一個經濟實惠的 AMI 產品,可選擇使用自定義徽標和品牌創建應用。
## 保持領先
在 GitHub 上為 Dify 加星,即時獲取新版本通知。

View File

@ -151,7 +151,7 @@ When('I open the Agent v2 API Reference', async function (this: DifyWorld) {
const apiReferenceLink = page.getByRole('link', { name: 'API Reference' })
await expect(apiReferenceLink).toBeVisible()
await expect(apiReferenceLink).toHaveAttribute('href', /\/api-reference\/guides\/get-started/)
await expect(apiReferenceLink).toHaveAttribute('href', /\/use-dify\/publish\/developing-with-apis/)
await expect(apiReferenceLink).toHaveAttribute('target', '_blank')
const [apiReferencePage] = await Promise.all([
@ -167,7 +167,7 @@ Then('the Agent v2 API Reference should open in a new tab', async function (this
if (!apiReferencePage)
throw new Error('No Agent v2 API Reference page was opened.')
await expect(apiReferencePage).toHaveURL(/\/api-reference\/guides\/get-started/)
await expect(apiReferencePage).toHaveURL(/developing-with-apis/)
await apiReferencePage.close()
this.agentBuilder.accessPoint.apiReferencePage = undefined
})

View File

@ -298,7 +298,7 @@ describe('CustomizeModal', () => {
})
const way2Link = getAnchorButton(/way2\.operation/i)
expect(way2Link).toHaveAttribute('href', expect.stringContaining('/api-reference/guides/get-started'))
expect(way2Link).toHaveAttribute('href', expect.stringContaining('/use-dify/publish/developing-with-apis'))
expect(way2Link).toHaveAttribute('target', '_blank')
expect(way2Link).toHaveAttribute('rel', 'noopener noreferrer')
})

View File

@ -45,7 +45,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
const docLink = useDocLink()
const isChatApp = mode === AppModeEnum.CHAT || mode === AppModeEnum.ADVANCED_CHAT
const repository = sourceCodeRepository ?? (isChatApp ? 'webapp-conversation' : 'webapp-text-generator')
const apiDocLink = docLink('/api-reference/guides/get-started')
const apiDocLink = docLink('/use-dify/publish/developing-with-apis')
return (
<Dialog open={isShow} onOpenChange={open => !open && onClose()}>

View File

@ -613,7 +613,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('MCP')).toHaveLength(2)
expect(screen.getByText('common.mcpPage.description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#mcp')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/build/mcp')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})
@ -622,7 +622,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('common.settings.swaggerAPIAsTool')).toHaveLength(2)
expect(screen.getByText('common.swaggerAPIAsToolPage.description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#swagger-api')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#custom-tool')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})
@ -665,7 +665,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('workflow.common.workflowAsTool')).toHaveLength(2)
expect(screen.getByText('common.workflowAsToolPage.description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#workflow')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#workflow-tool')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})

View File

@ -36,18 +36,18 @@ type IntegrationsPageProps = {
section?: IntegrationSection
}
const headerDescriptionDocPaths = {
const headerDescriptionDocPaths: Partial<Record<IntegrationSection, string>> = {
'provider': '/use-dify/workspace/model-providers',
'data-source': '/develop-plugin/dev-guides-and-walkthroughs/datasource-plugin#data-source-plugin-types',
'builtin': '/use-dify/workspace/tools',
'custom-tool': '/use-dify/workspace/tools#swagger-api',
'workflow-tool': '/use-dify/workspace/tools#workflow',
'mcp': '/use-dify/workspace/tools#mcp',
'custom-tool': '/use-dify/workspace/tools#custom-tool',
'workflow-tool': '/use-dify/workspace/tools#workflow-tool',
'mcp': '/use-dify/build/mcp',
'custom-endpoint': '/develop-plugin/dev-guides-and-walkthroughs/endpoint',
'trigger': '/develop-plugin/dev-guides-and-walkthroughs/trigger-plugin',
'extension': '/use-dify/workspace/api-extension/api-extension',
'agent-strategy': '/develop-plugin/dev-guides-and-walkthroughs/agent-strategy-plugin',
} satisfies Partial<Record<IntegrationSection, DocPathWithoutLang>>
}
type DescriptionWithLearnMoreProps = {
children: ReactNode
@ -150,7 +150,7 @@ export default function IntegrationsPage({
const headerDescriptionWithLink = headerDescription && headerDescriptionDocPath
? (
<DescriptionWithLearnMore
href={docLink(headerDescriptionDocPath)}
href={docLink(headerDescriptionDocPath as DocPathWithoutLang)}
label={t('modelProvider.learnMore', { ns: 'common' })}
>
{headerDescription}

View File

@ -80,7 +80,7 @@ const NewMCPCard = ({ handleCreate }: Props) => {
showModal,
} = useMCPCreateAction({ handleCreate })
const linkUrl = useMemo(() => docLink('/use-dify/workspace/tools#mcp'), [docLink])
const linkUrl = useMemo(() => docLink('/use-dify/build/mcp'), [docLink])
return (
<>

View File

@ -154,7 +154,7 @@ describe('CustomCreateCard', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
const docLink = screen.getByText('tools.swaggerAPIAsToolTip').closest('a')
expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/cloud/use-dify/workspace/tools#swagger-api')
expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/cloud/use-dify/workspace/tools#custom-tool')
expect(docLink).toHaveAttribute('target', '_blank')
expect(docLink).toHaveAttribute('rel', 'noopener noreferrer')
})

View File

@ -84,7 +84,7 @@ describe('Empty', () => {
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.goToStudio/i })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.learnMore/i })).toHaveAttribute('target', '_blank')
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#workflow')
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#workflow-tool')
})
})

View File

@ -1,5 +1,6 @@
'use client'
import type { CustomCollectionBackend } from '../types'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useState } from 'react'
@ -90,7 +91,7 @@ const Contribute = ({ onRefreshData }: Props) => {
className="min-w-0"
title={t('createSwaggerAPIAsTool', { ns: 'tools' })}
linkText={t('swaggerAPIAsToolTip', { ns: 'tools' })}
linkUrl={docLink('/use-dify/workspace/tools#swagger-api')}
linkUrl={`${docLink('/use-dify/workspace/tools' as DocPathWithoutLang)}#custom-tool`}
onCreate={() => setIsShowEditCustomCollectionModal(true)}
/>
)}

View File

@ -1,4 +1,5 @@
'use client'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowRightLine, RiArrowRightUpLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
@ -94,7 +95,7 @@ const Empty = ({
</Link>
</div>
<Link
href={docLink('/use-dify/workspace/tools#workflow')}
href={`${docLink('/use-dify/workspace/tools' as DocPathWithoutLang)}#workflow-tool`}
target="_blank"
rel="noreferrer"
className="rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px] system-2xs-medium-uppercase text-text-tertiary hover:text-text-accent"

View File

@ -108,8 +108,8 @@ describe('useDocLink', () => {
it('should keep explicit product docs path without adding another product prefix', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/cloud/use-dify/getting-started/introduction' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/getting-started/introduction`)
const url = result.current('/cloud/use-dify/build/mcp' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/build/mcp`)
})
})
@ -122,11 +122,11 @@ describe('useDocLink', () => {
const pathMap: DocPathMap = {
'zh-Hans': '/use-dify/getting-started/introduction',
'en-US': '/use-dify/workspace/tools#mcp',
'en-US': '/use-dify/build/mcp',
}
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/workspace/tools#mcp', pathMap)
const url = result.current('/use-dify/build/mcp', pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/zh/cloud/use-dify/getting-started/introduction`)
})
@ -138,12 +138,12 @@ describe('useDocLink', () => {
const pathMap: DocPathMap = {
'zh-Hans': '/use-dify/getting-started/introduction',
'en-US': '/use-dify/workspace/tools#mcp',
'en-US': '/use-dify/build/mcp',
}
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/workspace/tools#mcp', pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/ja/cloud/use-dify/workspace/tools#mcp`)
const url = result.current('/use-dify/build/mcp', pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/ja/cloud/use-dify/build/mcp`)
})
it('should handle undefined pathMap', () => {
@ -158,16 +158,16 @@ describe('useDocLink', () => {
mockConfig.IS_CLOUD_EDITION = true
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/workspace/tools#mcp')
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/workspace/tools#mcp`)
const url = result.current('/use-dify/build/mcp')
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/build/mcp`)
})
it('should add self-host product prefix for product docs available in both editions outside cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = false
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/workspace/tools#mcp')
expect(url).toBe(`${defaultDocBaseUrl}/en/self-host/use-dify/workspace/tools#mcp`)
const url = result.current('/use-dify/build/mcp')
expect(url).toBe(`${defaultDocBaseUrl}/en/self-host/use-dify/build/mcp`)
})
it('should use the existing cloud docs path for cloud-only product docs outside cloud edition', () => {
@ -254,8 +254,8 @@ describe('useDocLink', () => {
})
})
describe('API reference path handling', () => {
it('should add language prefix for Chinese API reference paths', () => {
describe('API reference path translations', () => {
it('should translate API reference path for Chinese locale', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
@ -263,10 +263,10 @@ describe('useDocLink', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/annotations/create-annotation')
expect(url).toBe(`${defaultDocBaseUrl}/zh/api-reference/annotations/create-annotation`)
expect(url).toBe(`${defaultDocBaseUrl}/api-reference/标注管理/创建标注`)
})
it('should add language prefix for Japanese API reference paths', () => {
it('should translate API reference path for Japanese locale when translation exists', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'ja-JP' },
} as ReturnType<typeof useTranslation>)
@ -274,7 +274,7 @@ describe('useDocLink', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/applications/get-app-info')
expect(url).toBe(`${defaultDocBaseUrl}/ja/api-reference/applications/get-app-info`)
expect(url).toBe(`${defaultDocBaseUrl}/api-reference/アプリケーション設定/アプリケーションの基本情報を取得`)
})
it('should not translate API reference path for English locale', () => {
@ -285,10 +285,22 @@ describe('useDocLink', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/annotations/create-annotation')
expect(url).toBe(`${defaultDocBaseUrl}/en/api-reference/annotations/create-annotation`)
expect(url).toBe(`${defaultDocBaseUrl}/api-reference/annotations/create-annotation`)
})
it('should keep the API reference slug unchanged for non-English locale', () => {
it('should keep original path when no translation exists for non-English locale', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
vi.mocked(getDocLanguage).mockReturnValue('zh')
const { result } = renderHook(() => useDocLink())
// This path has no Japanese translation
const url = result.current('/api-reference/annotations/create-annotation')
expect(url).toBe(`${defaultDocBaseUrl}/api-reference/标注管理/创建标注`)
})
it('should remove language prefix when translation is applied', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
@ -296,29 +308,9 @@ describe('useDocLink', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/annotations/create-annotation')
expect(url).toBe(`${defaultDocBaseUrl}/zh/api-reference/annotations/create-annotation`)
})
it('should keep language prefix for API reference paths', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
vi.mocked(getDocLanguage).mockReturnValue('zh')
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/annotations/create-annotation')
expect(url).toBe(`${defaultDocBaseUrl}/zh/api-reference/annotations/create-annotation`)
})
it('should use the current knowledge API guide path directly', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
vi.mocked(getDocLanguage).mockReturnValue('zh')
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/guides/knowledge')
expect(url).toBe(`${defaultDocBaseUrl}/zh/api-reference/guides/knowledge`)
// Should NOT have /zh/ prefix when translated
expect(url).not.toContain('/zh/')
expect(url).toBe(`${defaultDocBaseUrl}/api-reference/标注管理/创建标注`)
})
it('should not translate non-API-reference paths', () => {
@ -343,9 +335,9 @@ describe('useDocLink', () => {
it('should handle multiple calls with same hook instance', () => {
const { result } = renderHook(() => useDocLink())
const url1 = result.current('/use-dify/getting-started/introduction')
const url2 = result.current('/use-dify/workspace/tools#mcp')
const url2 = result.current('/use-dify/build/mcp')
expect(url1).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/getting-started/introduction`)
expect(url2).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/workspace/tools#mcp`)
expect(url2).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/build/mcp`)
})
})
})

View File

@ -4,7 +4,7 @@ import { useCallback } from 'react'
import { useTranslation } from '#i18n'
import { IS_CLOUD_EDITION } from '@/config'
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
import { docPathProductAvailability } from '@/types/doc-paths'
import { apiReferencePathTranslations, docPathProductAvailability } from '@/types/doc-paths'
export const useLocale = () => {
const { i18n } = useTranslation()
@ -72,9 +72,18 @@ export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathM
const docLanguage = getDocLanguage(locale)
const pathUrl = path || ''
let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl
const languagePrefix = `/${docLanguage}`
let languagePrefix = `/${docLanguage}`
if (!targetPath) {
if (targetPath.startsWith('/api-reference/')) {
languagePrefix = ''
if (docLanguage !== 'en') {
const translatedPath = apiReferencePathTranslations[targetPath]?.[docLanguage]
if (translatedPath) {
targetPath = translatedPath
}
}
}
else if (!targetPath) {
targetPath = getDocHomePath()
}
else {

View File

@ -89,7 +89,7 @@ export function ServiceApiAccessCard({
</span>
</Button>
<a
href={docLink('/api-reference/guides/get-started')}
href={docLink('/use-dify/publish/developing-with-apis')}
target="_blank"
rel="noreferrer"
aria-label={t('agentDetail.access.serviceApi.actions.apiReference')}

View File

@ -315,40 +315,6 @@ describe('useAgentConfigureSync', () => {
})
})
it('should save the latest dirty draft when Configure unmounts before autosave runs', async () => {
const { store, unmount } = renderUseAgentConfigureSync()
act(() => {
store.set(agentComposerDraftAtom, {
...defaultAgentSoulConfigFormState,
prompt: 'Route leave prompt',
})
})
expect(composerPutMutationFn).not.toHaveBeenCalled()
await act(async () => {
unmount()
await Promise.resolve()
})
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
expect(composerPutMutationFn).toHaveBeenCalledWith(expect.objectContaining({
params: {
agent_id: 'agent-1',
},
body: expect.objectContaining({
variant: 'agent_app',
save_strategy: 'save_to_current_version',
agent_soul: expect.objectContaining({
prompt: expect.objectContaining({
system_prompt: 'Route leave prompt',
}),
}),
}),
}))
})
it('should include Agent Soul files when autosaving file changes', async () => {
const { store } = renderUseAgentConfigureSync()

View File

@ -208,6 +208,12 @@ export function useAgentConfigureSync({
})
}, [debouncedSaveDraft, getAgentSoulDraft, store])
useEffect(() => {
return () => {
debouncedSaveDraft.flush?.()
}
}, [debouncedSaveDraft])
useEffect(() => {
const saveDraftWhenPageHidden = () => {
if (document.visibilityState === 'hidden')
@ -226,12 +232,6 @@ export function useAgentConfigureSync({
}
}, [saveDirtyDraftOnPageClose])
useEffect(() => {
return () => {
saveDirtyDraftOnPageClose()
}
}, [saveDirtyDraftOnPageClose])
const publishDraft = useCallback(async () => {
if (publishInFlightRef.current)
return

View File

@ -3,5 +3,5 @@ import { useDocLink } from '@/context/i18n'
export const useDatasetApiAccessUrl = () => {
const docLink = useDocLink()
return docLink('/api-reference/guides/knowledge')
return docLink('/api-reference/knowledge-bases/list-knowledge-bases')
}

View File

@ -1,7 +1,7 @@
{
"name": "dify-web",
"type": "module",
"version": "1.16.0-rc1",
"version": "1.15.0",
"private": true,
"imports": {
"#i18n": {

View File

@ -26,7 +26,6 @@ type NavObject = {
tabs?: NavItem[]
menu?: NavItem[]
versions?: NavItem[]
root?: string
openapi?: string
[key: string]: unknown
}
@ -52,8 +51,14 @@ type OpenAPISpec = {
[key: string]: unknown
}
type Redirect = {
source: string
destination: string
}
type DocsJson = {
navigation?: NavItem
redirects?: Redirect[]
[key: string]: unknown
}
@ -199,9 +204,6 @@ function extractPaths(item: NavItem | undefined, paths: Set<string> = new Set())
}
if (typeof item === 'object') {
if (item.root)
paths.add(item.root)
// Handle pages array
if (item.pages)
extractPaths(item.pages, paths)
@ -316,6 +318,7 @@ function generateTypeDefinitions(
groups: Record<string, Set<string>>,
productAvailability: ProductAvailability,
apiReferencePaths: string[],
apiPathTranslations: Record<string, { zh?: string, ja?: string }>,
): string {
const lines: string[] = [
'// GENERATE BY script',
@ -360,13 +363,13 @@ function generateTypeDefinitions(
// Generate API reference type (English paths only)
if (apiReferencePaths.length > 0) {
const sortedPaths = [...apiReferencePaths].sort()
lines.push('// API Reference endpoint paths')
lines.push('type ApiEndpointReferencePath =')
lines.push('// API Reference paths (English, use apiReferencePathTranslations for other languages)')
lines.push('type ApiReferencePath =')
for (const p of sortedPaths) {
lines.push(` | '${p}'`)
}
lines.push('')
typeNames.push('ApiEndpointReferencePath')
typeNames.push('ApiReferencePath')
}
// Generate base combined type
@ -395,6 +398,23 @@ function generateTypeDefinitions(
lines.push('}')
lines.push('')
// Generate API reference path translations map
lines.push('// API Reference path translations (English -> other languages)')
lines.push('export const apiReferencePathTranslations: Record<string, { zh?: string; ja?: string }> = {')
const sortedEnPaths = Object.keys(apiPathTranslations).sort()
for (const enPath of sortedEnPaths) {
const translations = apiPathTranslations[enPath]
const parts: string[] = []
if (translations!.zh)
parts.push(`zh: '${translations!.zh}'`)
if (translations!.ja)
parts.push(`ja: '${translations!.ja}'`)
if (parts.length > 0)
lines.push(` '${enPath}': { ${parts.join(', ')} },`)
}
lines.push('}')
lines.push('')
return lines.join('\n')
}
@ -418,19 +438,52 @@ async function main(): Promise<void> {
console.log(`Found ${openApiPaths.size} OpenAPI specs to process`)
// Fetch English OpenAPI specs and extract API reference paths.
// API reference URLs are language-prefixed by useDocLink at runtime.
const enApiPaths: string[] = []
// Fetch OpenAPI specs and extract API reference paths with endpoint keys
// Group by OpenAPI file name (without language prefix) to match endpoints across languages
const endpointMapsByLang: Record<string, Map<string, EndpointPathMap>> = {
en: new Map(),
zh: new Map(),
ja: new Map(),
}
for (const openapiPath of openApiPaths) {
// Determine language from path
const langMatch = /^(en|zh|ja)\//.exec(openapiPath)
if (langMatch?.[1] !== 'en')
if (!langMatch)
continue
const lang = langMatch[1]
// Get file name without language prefix (e.g., "api-reference/openapi_knowledge.json")
const fileKey = openapiPath.replace(/^(en|zh|ja)\//, '')
console.log(`Fetching OpenAPI spec: ${openapiPath}`)
const pathMap = await fetchOpenAPIAndExtractPaths(openapiPath)
for (const enPath of pathMap.values())
endpointMapsByLang[lang!]!.set(fileKey, pathMap)
}
// Build English paths and mapping to other languages
const enApiPaths: string[] = []
const apiPathTranslations: Record<string, { zh?: string, ja?: string }> = {}
// Iterate through English endpoint maps
for (const [fileKey, enPathMap] of endpointMapsByLang.en!) {
const zhPathMap = endpointMapsByLang.zh!.get(fileKey)
const jaPathMap = endpointMapsByLang.ja!.get(fileKey)
for (const [endpointKey, enPath] of enPathMap) {
enApiPaths.push(enPath)
const zhPath = zhPathMap?.get(endpointKey)
const jaPath = jaPathMap?.get(endpointKey)
if (zhPath || jaPath) {
apiPathTranslations[enPath] = {}
if (zhPath)
apiPathTranslations[enPath].zh = zhPath
if (jaPath)
apiPathTranslations[enPath].ja = jaPath
}
}
}
// Deduplicate English API paths
@ -438,6 +491,8 @@ async function main(): Promise<void> {
console.log(`Extracted ${uniqueEnApiPaths.length} unique English API reference paths`)
console.log(`Generated ${Object.keys(apiPathTranslations).length} API path translations`)
// Group by section
const { groups, productAvailability } = groupPathsBySection(allPaths)
@ -445,11 +500,7 @@ async function main(): Promise<void> {
console.log(`Found ${Object.keys(productAvailability).length} product-aware paths`)
// Generate TypeScript
const tsContent = generateTypeDefinitions(
groups,
productAvailability,
uniqueEnApiPaths,
)
const tsContent = generateTypeDefinitions(groups, productAvailability, uniqueEnApiPaths, apiPathTranslations)
// Write to file
await writeFile(OUTPUT_PATH, tsContent, 'utf-8')

View File

@ -1,8 +1,8 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
//
// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json
// Generated at: 2026-07-09T10:55:53.618Z
// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/feat/audience-products/docs.json
// Generated at: 2026-06-17T04:42:51.293Z
// Language prefixes
export type DocLanguage = 'en' | 'zh' | 'ja'
@ -13,12 +13,14 @@ type CloudPath =
| '/cloud/use-dify/build/additional-features'
| '/cloud/use-dify/build/agent'
| '/cloud/use-dify/build/chatbot'
| '/cloud/use-dify/build/mcp'
| '/cloud/use-dify/build/orchestrate-node'
| '/cloud/use-dify/build/predefined-error-handling-logic'
| '/cloud/use-dify/build/shortcut-key'
| '/cloud/use-dify/build/text-generator'
| '/cloud/use-dify/build/version-control'
| '/cloud/use-dify/build/workflow-chatflow'
| '/cloud/use-dify/build/workflow-collaboration'
| '/cloud/use-dify/debug/error-type'
| '/cloud/use-dify/debug/history-and-logs'
| '/cloud/use-dify/debug/step-run'
@ -42,6 +44,7 @@ type CloudPath =
| '/cloud/use-dify/knowledge/knowledge-pipeline/upload-files'
| '/cloud/use-dify/knowledge/knowledge-request-rate-limit'
| '/cloud/use-dify/knowledge/manage-knowledge/introduction'
| '/cloud/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api'
| '/cloud/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents'
| '/cloud/use-dify/knowledge/metadata'
| '/cloud/use-dify/knowledge/readme'
@ -82,6 +85,7 @@ type CloudPath =
| '/cloud/use-dify/nodes/variable-aggregator'
| '/cloud/use-dify/nodes/variable-assigner'
| '/cloud/use-dify/publish/README'
| '/cloud/use-dify/publish/developing-with-apis'
| '/cloud/use-dify/publish/publish-mcp'
| '/cloud/use-dify/publish/publish-to-marketplace'
| '/cloud/use-dify/publish/webapp/chatflow-webapp'
@ -106,8 +110,8 @@ type UseDifyPath =
| '/use-dify/build/additional-features'
| '/use-dify/build/agent'
| '/use-dify/build/chatbot'
| '/use-dify/build/mcp'
| '/use-dify/build/new-agent/build'
| '/use-dify/build/new-agent/overview'
| '/use-dify/build/orchestrate-node'
| '/use-dify/build/predefined-error-handling-logic'
| '/use-dify/build/shortcut-key'
@ -138,6 +142,7 @@ type UseDifyPath =
| '/use-dify/knowledge/knowledge-pipeline/upload-files'
| '/use-dify/knowledge/knowledge-request-rate-limit'
| '/use-dify/knowledge/manage-knowledge/introduction'
| '/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api'
| '/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents'
| '/use-dify/knowledge/metadata'
| '/use-dify/knowledge/readme'
@ -178,6 +183,7 @@ type UseDifyPath =
| '/use-dify/nodes/variable-aggregator'
| '/use-dify/nodes/variable-assigner'
| '/use-dify/publish/README'
| '/use-dify/publish/developing-with-apis'
| '/use-dify/publish/publish-mcp'
| '/use-dify/publish/publish-to-marketplace'
| '/use-dify/publish/webapp/chatflow-webapp'
@ -228,42 +234,9 @@ type LearnPath =
type QuickStartPath =
| '/quick-start'
// ApiReference paths
type ApiReferencePath =
| '/api-reference/guides/agent'
| '/api-reference/guides/chat'
| '/api-reference/guides/chatflow'
| '/api-reference/guides/completion'
| '/api-reference/guides/end-user-identity'
| '/api-reference/guides/errors'
| '/api-reference/guides/get-started'
| '/api-reference/guides/human-input-flow'
| '/api-reference/guides/knowledge'
| '/api-reference/guides/streaming'
| '/api-reference/guides/workflow'
// Cli paths
type CliPath =
| '/cli/authenticate'
| '/cli/common-tasks'
| '/cli/install'
| '/cli/integrate-agents/auth-for-agent-deployments'
| '/cli/integrate-agents/error-handling-and-retries-for-agents'
| '/cli/integrate-agents/install-the-difyctl-skill'
| '/cli/integrate-agents/overview'
| '/cli/overview'
| '/cli/quick-start'
| '/cli/reference/apps'
| '/cli/reference/auth-and-contexts'
| '/cli/reference/command-index'
| '/cli/reference/environment-variables'
| '/cli/reference/global-flags'
| '/cli/reference/help'
| '/cli/reference/output-formats-and-exit-codes'
| '/cli/reference/skills'
| '/cli/reference/version'
| '/cli/reference/workspaces'
| '/cli/troubleshooting'
| '/cli/coming-soon'
// DevelopPlugin paths
type DevelopPluginPath =
@ -326,8 +299,8 @@ type SelfHostPath =
| '/self-host/use-dify/build/additional-features'
| '/self-host/use-dify/build/agent'
| '/self-host/use-dify/build/chatbot'
| '/self-host/use-dify/build/mcp'
| '/self-host/use-dify/build/new-agent/build'
| '/self-host/use-dify/build/new-agent/overview'
| '/self-host/use-dify/build/orchestrate-node'
| '/self-host/use-dify/build/predefined-error-handling-logic'
| '/self-host/use-dify/build/shortcut-key'
@ -357,6 +330,7 @@ type SelfHostPath =
| '/self-host/use-dify/knowledge/knowledge-pipeline/readme'
| '/self-host/use-dify/knowledge/knowledge-pipeline/upload-files'
| '/self-host/use-dify/knowledge/manage-knowledge/introduction'
| '/self-host/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api'
| '/self-host/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents'
| '/self-host/use-dify/knowledge/metadata'
| '/self-host/use-dify/knowledge/readme'
@ -397,6 +371,7 @@ type SelfHostPath =
| '/self-host/use-dify/nodes/variable-aggregator'
| '/self-host/use-dify/nodes/variable-assigner'
| '/self-host/use-dify/publish/README'
| '/self-host/use-dify/publish/developing-with-apis'
| '/self-host/use-dify/publish/publish-mcp'
| '/self-host/use-dify/publish/publish-to-marketplace'
| '/self-host/use-dify/publish/webapp/chatflow-webapp'
@ -431,8 +406,8 @@ type DeployPath =
| '/deploy/troubleshooting/storage-and-migration'
| '/deploy/troubleshooting/weaviate-v4-migration'
// API Reference endpoint paths
type ApiEndpointReferencePath =
// API Reference paths (English, use apiReferencePathTranslations for other languages)
type ApiReferencePath =
| '/api-reference/annotations/configure-annotation-reply'
| '/api-reference/annotations/create-annotation'
| '/api-reference/annotations/delete-annotation'
@ -443,11 +418,15 @@ type ApiEndpointReferencePath =
| '/api-reference/applications/get-app-meta'
| '/api-reference/applications/get-app-parameters'
| '/api-reference/applications/get-app-webapp-settings'
| '/api-reference/audio/convert-audio-to-text'
| '/api-reference/audio/convert-text-to-audio'
| '/api-reference/chat-messages/get-next-suggested-questions'
| '/api-reference/chat-messages/send-chat-message'
| '/api-reference/chat-messages/stop-chat-message-generation'
| '/api-reference/chatflows/get-next-suggested-questions'
| '/api-reference/chatflows/get-workflow-run-detail'
| '/api-reference/chatflows/list-workflow-logs'
| '/api-reference/chatflows/send-chat-message'
| '/api-reference/chatflows/stop-chat-message-generation'
| '/api-reference/chatflows/stream-workflow-events'
| '/api-reference/chats/get-next-suggested-questions'
| '/api-reference/chats/send-chat-message'
| '/api-reference/chats/stop-chat-message-generation'
| '/api-reference/chunks/create-child-chunk'
| '/api-reference/chunks/create-chunks'
| '/api-reference/chunks/delete-child-chunk'
@ -457,8 +436,8 @@ type ApiEndpointReferencePath =
| '/api-reference/chunks/list-chunks'
| '/api-reference/chunks/update-child-chunk'
| '/api-reference/chunks/update-chunk'
| '/api-reference/completion-messages/send-completion-message'
| '/api-reference/completion-messages/stop-completion-message-generation'
| '/api-reference/completions/send-completion-message'
| '/api-reference/completions/stop-completion-message-generation'
| '/api-reference/conversations/delete-conversation'
| '/api-reference/conversations/list-conversation-messages'
| '/api-reference/conversations/list-conversation-variables'
@ -473,7 +452,6 @@ type ApiEndpointReferencePath =
| '/api-reference/documents/get-document'
| '/api-reference/documents/get-document-indexing-status'
| '/api-reference/documents/list-documents'
| '/api-reference/documents/update-document'
| '/api-reference/documents/update-document-by-file'
| '/api-reference/documents/update-document-by-text'
| '/api-reference/documents/update-document-status-in-batch'
@ -509,12 +487,14 @@ type ApiEndpointReferencePath =
| '/api-reference/tags/get-knowledge-base-tags'
| '/api-reference/tags/list-knowledge-tags'
| '/api-reference/tags/update-knowledge-tag'
| '/api-reference/workflow-runs/get-workflow-run-detail'
| '/api-reference/workflow-runs/list-workflow-logs'
| '/api-reference/workflow-runs/run-workflow'
| '/api-reference/workflow-runs/run-workflow-by-id'
| '/api-reference/workflow-runs/stop-workflow-task'
| '/api-reference/workflow-runs/stream-workflow-events'
| '/api-reference/tts/convert-audio-to-text'
| '/api-reference/tts/convert-text-to-audio'
| '/api-reference/workflows/get-workflow-run-detail'
| '/api-reference/workflows/list-workflow-logs'
| '/api-reference/workflows/run-workflow'
| '/api-reference/workflows/run-workflow-by-id'
| '/api-reference/workflows/stop-workflow-task'
| '/api-reference/workflows/stream-workflow-events'
// Base path without language prefix
type DocPathWithoutLangBase =
@ -523,12 +503,11 @@ type DocPathWithoutLangBase =
| HomePath
| LearnPath
| QuickStartPath
| ApiReferencePath
| CliPath
| DevelopPluginPath
| SelfHostPath
| DeployPath
| ApiEndpointReferencePath
| ApiReferencePath
// Combined path without language prefix (supports optional #anchor)
export type DocPathWithoutLang =
@ -553,15 +532,15 @@ export const docPathProductAvailability: Record<string, readonly DocsProduct[]>
'/use-dify/build/additional-features': ['cloud', 'self-host'],
'/use-dify/build/agent': ['cloud', 'self-host'],
'/use-dify/build/chatbot': ['cloud', 'self-host'],
'/use-dify/build/mcp': ['cloud', 'self-host'],
'/use-dify/build/new-agent/build': ['self-host'],
'/use-dify/build/new-agent/overview': ['self-host'],
'/use-dify/build/orchestrate-node': ['cloud', 'self-host'],
'/use-dify/build/predefined-error-handling-logic': ['cloud', 'self-host'],
'/use-dify/build/shortcut-key': ['cloud', 'self-host'],
'/use-dify/build/text-generator': ['cloud', 'self-host'],
'/use-dify/build/version-control': ['cloud', 'self-host'],
'/use-dify/build/workflow-chatflow': ['cloud', 'self-host'],
'/use-dify/build/workflow-collaboration': ['self-host'],
'/use-dify/build/workflow-collaboration': ['cloud', 'self-host'],
'/use-dify/debug/error-type': ['cloud', 'self-host'],
'/use-dify/debug/history-and-logs': ['cloud', 'self-host'],
'/use-dify/debug/step-run': ['cloud', 'self-host'],
@ -585,6 +564,7 @@ export const docPathProductAvailability: Record<string, readonly DocsProduct[]>
'/use-dify/knowledge/knowledge-pipeline/upload-files': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-request-rate-limit': ['cloud'],
'/use-dify/knowledge/manage-knowledge/introduction': ['cloud', 'self-host'],
'/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api': ['cloud', 'self-host'],
'/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents': ['cloud', 'self-host'],
'/use-dify/knowledge/metadata': ['cloud', 'self-host'],
'/use-dify/knowledge/readme': ['cloud', 'self-host'],
@ -625,6 +605,7 @@ export const docPathProductAvailability: Record<string, readonly DocsProduct[]>
'/use-dify/nodes/variable-aggregator': ['cloud', 'self-host'],
'/use-dify/nodes/variable-assigner': ['cloud', 'self-host'],
'/use-dify/publish/README': ['cloud', 'self-host'],
'/use-dify/publish/developing-with-apis': ['cloud', 'self-host'],
'/use-dify/publish/publish-mcp': ['cloud', 'self-host'],
'/use-dify/publish/publish-to-marketplace': ['cloud', 'self-host'],
'/use-dify/publish/webapp/chatflow-webapp': ['cloud', 'self-host'],
@ -644,3 +625,94 @@ export const docPathProductAvailability: Record<string, readonly DocsProduct[]>
'/use-dify/workspace/team-members-management': ['cloud', 'self-host'],
'/use-dify/workspace/tools': ['cloud', 'self-host'],
}
// API Reference path translations (English -> other languages)
export const apiReferencePathTranslations: Record<string, { zh?: string; ja?: string }> = {
'/api-reference/annotations/configure-annotation-reply': { zh: '/api-reference/标注管理/配置标注回复', ja: '/api-reference/アノテーション管理/アノテーション返信を設定' },
'/api-reference/annotations/create-annotation': { zh: '/api-reference/标注管理/创建标注', ja: '/api-reference/アノテーション管理/アノテーションを作成' },
'/api-reference/annotations/delete-annotation': { zh: '/api-reference/标注管理/删除标注', ja: '/api-reference/アノテーション管理/アノテーションを削除' },
'/api-reference/annotations/get-annotation-reply-job-status': { zh: '/api-reference/标注管理/查询标注回复配置任务状态', ja: '/api-reference/アノテーション管理/アノテーション返信の初期設定タスクステータスを取得' },
'/api-reference/annotations/list-annotations': { zh: '/api-reference/标注管理/获取标注列表', ja: '/api-reference/アノテーション管理/アノテーションリストを取得' },
'/api-reference/annotations/update-annotation': { zh: '/api-reference/标注管理/更新标注', ja: '/api-reference/アノテーション管理/アノテーションを更新' },
'/api-reference/applications/get-app-info': { zh: '/api-reference/应用配置/获取应用基本信息', ja: '/api-reference/アプリケーション設定/アプリケーションの基本情報を取得' },
'/api-reference/applications/get-app-meta': { zh: '/api-reference/应用配置/获取应用元数据', ja: '/api-reference/アプリケーション設定/アプリケーションのメタ情報を取得' },
'/api-reference/applications/get-app-parameters': { zh: '/api-reference/应用配置/获取应用参数', ja: '/api-reference/アプリケーション設定/アプリケーションのパラメータ情報を取得' },
'/api-reference/applications/get-app-webapp-settings': { zh: '/api-reference/应用配置/获取应用-webapp-设置', ja: '/api-reference/アプリケーション設定/アプリの-webapp-設定を取得' },
'/api-reference/chatflows/get-next-suggested-questions': { zh: '/api-reference/对话流/获取下一轮建议问题列表', ja: '/api-reference/チャットフロー/次の推奨質問を取得' },
'/api-reference/chatflows/get-workflow-run-detail': { zh: '/api-reference/对话流/获取工作流执行情况', ja: '/api-reference/チャットフロー/ワークフロー実行詳細を取得' },
'/api-reference/chatflows/list-workflow-logs': { zh: '/api-reference/对话流/获取工作流日志', ja: '/api-reference/チャットフロー/ワークフローログ一覧を取得' },
'/api-reference/chatflows/send-chat-message': { zh: '/api-reference/对话流/发送对话消息', ja: '/api-reference/チャットフロー/チャットメッセージを送信' },
'/api-reference/chatflows/stop-chat-message-generation': { zh: '/api-reference/对话流/停止响应', ja: '/api-reference/チャットフロー/生成を停止' },
'/api-reference/chatflows/stream-workflow-events': { zh: '/api-reference/对话流/流式获取工作流事件', ja: '/api-reference/チャットフロー/ワークフローイベントをストリーム' },
'/api-reference/chats/get-next-suggested-questions': { zh: '/api-reference/对话消息/获取下一轮建议问题列表', ja: '/api-reference/チャットメッセージ/次の推奨質問を取得' },
'/api-reference/chats/send-chat-message': { zh: '/api-reference/对话消息/发送对话消息', ja: '/api-reference/チャットメッセージ/チャットメッセージを送信' },
'/api-reference/chats/stop-chat-message-generation': { zh: '/api-reference/对话消息/停止响应', ja: '/api-reference/チャットメッセージ/生成を停止' },
'/api-reference/chunks/create-child-chunk': { zh: '/api-reference/分段/创建子分段', ja: '/api-reference/チャンク/子チャンクを作成' },
'/api-reference/chunks/create-chunks': { zh: '/api-reference/分段/向文档添加分段', ja: '/api-reference/チャンク/ドキュメントにチャンクを追加' },
'/api-reference/chunks/delete-child-chunk': { zh: '/api-reference/分段/删除子分段', ja: '/api-reference/チャンク/子チャンクを削除' },
'/api-reference/chunks/delete-chunk': { zh: '/api-reference/分段/删除文档中的分段', ja: '/api-reference/チャンク/ドキュメント内のチャンクを削除' },
'/api-reference/chunks/get-chunk': { zh: '/api-reference/分段/获取文档中的分段详情', ja: '/api-reference/チャンク/ドキュメント内のチャンク詳細を取得' },
'/api-reference/chunks/list-child-chunks': { zh: '/api-reference/分段/获取子分段', ja: '/api-reference/チャンク/子チャンク一覧を取得' },
'/api-reference/chunks/list-chunks': { zh: '/api-reference/分段/从文档获取分段', ja: '/api-reference/チャンク/チャンク一覧を取得' },
'/api-reference/chunks/update-child-chunk': { zh: '/api-reference/分段/更新子分段', ja: '/api-reference/チャンク/子チャンクを更新' },
'/api-reference/chunks/update-chunk': { zh: '/api-reference/分段/更新文档中的分段', ja: '/api-reference/チャンク/ドキュメント内のチャンクを更新' },
'/api-reference/completions/send-completion-message': { zh: '/api-reference/文本生成/发送消息', ja: '/api-reference/完了メッセージ/完了メッセージを送信' },
'/api-reference/completions/stop-completion-message-generation': { zh: '/api-reference/文本生成/停止响应', ja: '/api-reference/完了メッセージ/生成を停止' },
'/api-reference/conversations/delete-conversation': { zh: '/api-reference/会话管理/删除会话', ja: '/api-reference/会話管理/会話を削除' },
'/api-reference/conversations/list-conversation-messages': { zh: '/api-reference/会话管理/获取会话历史消息', ja: '/api-reference/会話管理/会話履歴メッセージ一覧を取得' },
'/api-reference/conversations/list-conversation-variables': { zh: '/api-reference/会话管理/获取对话变量', ja: '/api-reference/会話管理/会話変数の取得' },
'/api-reference/conversations/list-conversations': { zh: '/api-reference/会话管理/获取会话列表', ja: '/api-reference/会話管理/会話一覧を取得' },
'/api-reference/conversations/rename-conversation': { zh: '/api-reference/会话管理/重命名会话', ja: '/api-reference/会話管理/会話の名前を変更' },
'/api-reference/conversations/update-conversation-variable': { zh: '/api-reference/会话管理/更新对话变量', ja: '/api-reference/会話管理/会話変数を更新' },
'/api-reference/documents/create-document-by-file': { zh: '/api-reference/文档/从文件创建文档', ja: '/api-reference/ドキュメント/ファイルからドキュメントを作成' },
'/api-reference/documents/create-document-by-text': { zh: '/api-reference/文档/从文本创建文档', ja: '/api-reference/ドキュメント/テキストからドキュメントを作成' },
'/api-reference/documents/delete-document': { zh: '/api-reference/文档/删除文档', ja: '/api-reference/ドキュメント/ドキュメントを削除' },
'/api-reference/documents/download-document': { zh: '/api-reference/文档/下载文档', ja: '/api-reference/ドキュメント/ドキュメントをダウンロード' },
'/api-reference/documents/download-documents-as-zip': { zh: '/api-reference/文档/批量下载文档zip', ja: '/api-reference/ドキュメント/ドキュメントを一括ダウンロードzip' },
'/api-reference/documents/get-document': { zh: '/api-reference/文档/获取文档详情', ja: '/api-reference/ドキュメント/ドキュメント詳細を取得' },
'/api-reference/documents/get-document-indexing-status': { zh: '/api-reference/文档/获取文档嵌入状态(进度)', ja: '/api-reference/ドキュメント/ドキュメント埋め込みステータス(進捗)を取得' },
'/api-reference/documents/list-documents': { zh: '/api-reference/文档/获取知识库的文档列表', ja: '/api-reference/ドキュメント/ナレッジベースのドキュメントリストを取得' },
'/api-reference/documents/update-document-by-file': { zh: '/api-reference/文档/用文件更新文档', ja: '/api-reference/ドキュメント/ファイルでドキュメントを更新' },
'/api-reference/documents/update-document-by-text': { zh: '/api-reference/文档/用文本更新文档', ja: '/api-reference/ドキュメント/テキストでドキュメントを更新' },
'/api-reference/documents/update-document-status-in-batch': { zh: '/api-reference/文档/批量更新文档状态', ja: '/api-reference/ドキュメント/ドキュメントステータスを一括更新' },
'/api-reference/end-users/get-end-user-info': { zh: '/api-reference/终端用户/获取终端用户', ja: '/api-reference/エンドユーザー/エンドユーザー取得' },
'/api-reference/feedback/list-app-feedbacks': { zh: '/api-reference/消息反馈/获取应用的消息反馈', ja: '/api-reference/メッセージフィードバック/アプリのフィードバック一覧を取得' },
'/api-reference/feedback/submit-message-feedback': { zh: '/api-reference/消息反馈/提交消息反馈', ja: '/api-reference/メッセージフィードバック/メッセージフィードバックを送信' },
'/api-reference/files/download-file': { zh: '/api-reference/文件操作/下载文件', ja: '/api-reference/ファイル操作/ファイルをダウンロード' },
'/api-reference/files/upload-file': { zh: '/api-reference/文件操作/上传文件', ja: '/api-reference/ファイル操作/ファイルをアップロード' },
'/api-reference/human-input/get-human-input-form': { zh: '/api-reference/人工介入/获取人工介入表单', ja: '/api-reference/人間の入力/人間の入力フォームを取得' },
'/api-reference/human-input/submit-human-input-form': { zh: '/api-reference/人工介入/提交人工介入表单', ja: '/api-reference/人間の入力/人間の入力フォームを送信' },
'/api-reference/knowledge-bases/create-an-empty-knowledge-base': { zh: '/api-reference/知识库/创建空知识库', ja: '/api-reference/データセット/空のナレッジベースを作成' },
'/api-reference/knowledge-bases/delete-knowledge-base': { zh: '/api-reference/知识库/删除知识库', ja: '/api-reference/データセット/ナレッジベースを削除' },
'/api-reference/knowledge-bases/get-knowledge-base': { zh: '/api-reference/知识库/获取知识库详情', ja: '/api-reference/データセット/ナレッジベース詳細を取得' },
'/api-reference/knowledge-bases/list-knowledge-bases': { zh: '/api-reference/知识库/获取知识库列表', ja: '/api-reference/データセット/ナレッジベースリストを取得' },
'/api-reference/knowledge-bases/retrieve-chunks-from-a-knowledge-base-/-test-retrieval': { zh: '/api-reference/知识库/从知识库检索分段-/-测试检索', ja: '/api-reference/データセット/ナレッジベースからチャンクを取得-/-テスト検索' },
'/api-reference/knowledge-bases/update-knowledge-base': { zh: '/api-reference/知识库/更新知识库', ja: '/api-reference/データセット/ナレッジベースを更新' },
'/api-reference/knowledge-pipeline/list-datasource-plugins': { zh: '/api-reference/知识流水线/获取数据源插件列表', ja: '/api-reference/ナレッジパイプライン/データソースプラグインリストを取得' },
'/api-reference/knowledge-pipeline/run-datasource-node': { zh: '/api-reference/知识流水线/执行数据源节点', ja: '/api-reference/ナレッジパイプライン/データソースノードを実行' },
'/api-reference/knowledge-pipeline/run-pipeline': { zh: '/api-reference/知识流水线/运行流水线', ja: '/api-reference/ナレッジパイプライン/パイプラインを実行' },
'/api-reference/knowledge-pipeline/upload-pipeline-file': { zh: '/api-reference/知识流水线/上传流水线文件', ja: '/api-reference/ナレッジパイプライン/パイプラインファイルをアップロード' },
'/api-reference/metadata/create-metadata-field': { zh: '/api-reference/元数据/创建元数据字段', ja: '/api-reference/メタデータ/メタデータフィールドを作成' },
'/api-reference/metadata/delete-metadata-field': { zh: '/api-reference/元数据/删除元数据字段', ja: '/api-reference/メタデータ/メタデータフィールドを削除' },
'/api-reference/metadata/get-built-in-metadata-fields': { zh: '/api-reference/元数据/获取内置元数据字段', ja: '/api-reference/メタデータ/組み込みメタデータフィールドを取得' },
'/api-reference/metadata/list-metadata-fields': { zh: '/api-reference/元数据/获取元数据字段列表', ja: '/api-reference/メタデータ/メタデータフィールドリストを取得' },
'/api-reference/metadata/update-built-in-metadata-field': { zh: '/api-reference/元数据/更新内置元数据字段', ja: '/api-reference/メタデータ/組み込みメタデータフィールドを更新' },
'/api-reference/metadata/update-document-metadata-in-batch': { zh: '/api-reference/元数据/批量更新文档元数据', ja: '/api-reference/メタデータ/ドキュメントメタデータを一括更新' },
'/api-reference/metadata/update-metadata-field': { zh: '/api-reference/元数据/更新元数据字段', ja: '/api-reference/メタデータ/メタデータフィールドを更新' },
'/api-reference/models/get-available-models': { zh: '/api-reference/模型/获取可用模型', ja: '/api-reference/モデル/利用可能なモデルを取得' },
'/api-reference/tags/create-knowledge-tag': { zh: '/api-reference/标签/创建知识库标签', ja: '/api-reference/タグ管理/ナレッジベースタグを作成' },
'/api-reference/tags/create-tag-binding': { zh: '/api-reference/标签/绑定标签到知识库', ja: '/api-reference/タグ管理/タグをデータセットにバインド' },
'/api-reference/tags/delete-knowledge-tag': { zh: '/api-reference/标签/删除知识库标签', ja: '/api-reference/タグ管理/ナレッジベースタグを削除' },
'/api-reference/tags/delete-tag-binding': { zh: '/api-reference/标签/解除标签与知识库的绑定', ja: '/api-reference/タグ管理/タグとデータセットのバインドを解除' },
'/api-reference/tags/get-knowledge-base-tags': { zh: '/api-reference/标签/获取知识库绑定的标签', ja: '/api-reference/タグ管理/ナレッジベースにバインドされたタグを取得' },
'/api-reference/tags/list-knowledge-tags': { zh: '/api-reference/标签/获取知识库标签列表', ja: '/api-reference/タグ管理/ナレッジベースタグリストを取得' },
'/api-reference/tags/update-knowledge-tag': { zh: '/api-reference/标签/修改知识库标签', ja: '/api-reference/タグ管理/ナレッジベースタグを変更' },
'/api-reference/tts/convert-audio-to-text': { zh: '/api-reference/语音与文字转换/语音转文字', ja: '/api-reference/音声・テキスト変換/音声をテキストに変換' },
'/api-reference/tts/convert-text-to-audio': { zh: '/api-reference/语音与文字转换/文字转语音', ja: '/api-reference/音声・テキスト変換/テキストを音声に変換' },
'/api-reference/workflows/get-workflow-run-detail': { zh: '/api-reference/工作流/获取工作流执行情况', ja: '/api-reference/ワークフロー/ワークフロー実行詳細を取得' },
'/api-reference/workflows/list-workflow-logs': { zh: '/api-reference/工作流/获取工作流日志', ja: '/api-reference/ワークフロー/ワークフローログ一覧を取得' },
'/api-reference/workflows/run-workflow': { zh: '/api-reference/工作流/执行工作流', ja: '/api-reference/ワークフロー/ワークフローを実行' },
'/api-reference/workflows/run-workflow-by-id': { zh: '/api-reference/工作流/按-id-执行工作流', ja: '/api-reference/ワークフロー/id-でワークフローを実行' },
'/api-reference/workflows/stop-workflow-task': { zh: '/api-reference/工作流/停止工作流任务', ja: '/api-reference/ワークフロー/ワークフロータスクを停止' },
'/api-reference/workflows/stream-workflow-events': { zh: '/api-reference/工作流/流式获取工作流事件', ja: '/api-reference/ワークフロー/ワークフローイベントをストリーム' },
}