mirror of
https://github.com/langgenius/dify.git
synced 2026-07-10 07:06:34 +08:00
Compare commits
27 Commits
main
...
feat/agent
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c7d1f4cae | |||
| 85b3d62445 | |||
| 44cda898a4 | |||
| 35a495d91d | |||
| c6a50b14ec | |||
| 3ee9d66aaa | |||
| 9b117b0207 | |||
| f8fff54682 | |||
| 89bc288a4b | |||
| 819ecad3f7 | |||
| 65d2b4c1c2 | |||
| c911d42b31 | |||
| bca16261ea | |||
| f002fa8a98 | |||
| 43c9fb96ee | |||
| 79c60ddfe2 | |||
| 2b0996236f | |||
| c192a3c16e | |||
| 5bed2666bb | |||
| 1f6e39b0ad | |||
| 5d0da5a27e | |||
| 055beabfec | |||
| fd06801caa | |||
| 37c5f78a7f | |||
| 093d47cf05 | |||
| dac5be487d | |||
| 12d1998fe4 |
6
.github/workflows/web-e2e.yml
vendored
6
.github/workflows/web-e2e.yml
vendored
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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}, "
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "dify-api"
|
||||
version = "1.16.0-rc1"
|
||||
version = "1.15.0"
|
||||
requires-python = "~=3.12.0"
|
||||
|
||||
dependencies = [
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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"))
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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,
|
||||
)
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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(
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
4
api/uv.lock
generated
@ -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" },
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
2
dify-agent/uv.lock
generated
@ -590,7 +590,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dify-agent"
|
||||
version = "1.16.0rc1"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
||||
@ -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=_
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 وتلق تنبيهًا فوريًا بالإصدارات الجديدة.
|
||||
|
||||
@ -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-এ ডিফাইকে স্টার দিয়ে রাখুন এবং নতুন রিলিজের খবর তাৎক্ষণিকভাবে পান।
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 को स्टार करें और नए रिलीज़ की सूचना तुरंत प्राप्त करें।
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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に関する新しいニュースを受け取れます。
|
||||
|
||||
@ -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에 별표를 찍어 새로운 릴리스를 즉시 알림 받으세요.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,并立即收到新版本的通知。
|
||||
|
||||
@ -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 加星,即時獲取新版本通知。
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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()}>
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"type": "module",
|
||||
"version": "1.16.0-rc1",
|
||||
"version": "1.15.0",
|
||||
"private": true,
|
||||
"imports": {
|
||||
"#i18n": {
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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/ワークフロー/ワークフローイベントをストリーム' },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user