Compare commits

..
Author SHA1 Message Date
crazywoola 0652f3d0aa fix: lint 2025-03-26 11:11:30 +08:00
Hanqing Zhao 803947c1ae Add and modify jp translation 2025-03-26 10:54:56 +08:00
JyongandGitHub 58c62f0a34 fix full-doc mode document doesn't reindex after enable or un_archive (#16737) 2025-03-25 16:26:14 +08:00
JyongandGitHub 2174225259 fix milvus filter search (#16725) 2025-03-25 16:22:43 +08:00
Wu TianweiandGitHub 6a0f1dad7f fix: update htmlContent type and wrap button in div for manualClose (#16716) 2025-03-25 14:58:42 +08:00
DavidandGitHub 40cadab8a6 fix: fix icon generation error on Windows platform (#16254) 2025-03-25 14:54:49 +08:00
6157f57872 feat: Add OceanBase hybrid search features (#16652)
Co-authored-by: 李远军 <4842@9ji.com>
Co-authored-by: yourchanges <yourchanges@gmail.com>
2025-03-25 14:32:00 +08:00
zhangkun-21andGitHub c4bb07184d Update prompts.py:Fix conversation title language for Italian conversations (#16696) 2025-03-25 14:29:08 +08:00
诗浓andGitHub af5d6ca27d fix: regex expressions distorted in code block (#16690) 2025-03-25 14:28:35 +08:00
JyongandGitHub 78f2ec8f32 mark weight type as optional (#16701) 2025-03-25 14:19:26 +08:00
kenwoodjwandGitHub a113356695 fix: pgvector metadata filter (#16688)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-03-25 11:34:33 +08:00
Yongtao HuangandGitHub 43753c8e9a Fix function's name mismatch (#16681) 2025-03-25 10:25:15 +08:00
fc8c765215 Fix/vdb lindorm (#16660)
Co-authored-by: jiangzhijie <jiangzhijie.jzj@alibaba-inc.com>
2025-03-25 09:19:06 +08:00
JyongandGitHub 86a1859d02 Metadata variable value fix (#16665) 2025-03-25 09:07:11 +08:00
GuanMuandGitHub 360986f38d Feat add childchunk api (#16094) 2025-03-24 20:58:28 +08:00
JyongandGitHub 1be0d26c1f fix metadata filter not affect in keyword-search and fulltext-search (#16644) 2025-03-24 18:35:16 +08:00
-LAN-andGitHub c167a1f4f4 chore: bump the package version to 1.1.3 (#16612)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 17:59:54 +08:00
lenboandGitHub 5eb0ca9b9d fix: fix inner API workspace Account.query error. (#16630) 2025-03-24 17:52:50 +08:00
Wu TianweiandGitHub 6e26ed2bb7 fix: update retrieval configuration to correctly handle reranking mod… (#16641) 2025-03-24 17:47:56 +08:00
-LAN-andGitHub 058d9c3525 chore: update release trigger to include all tags in build-push workflow (#16631)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 17:17:55 +08:00
KVOJJJinandGitHub b247fbb2ef Fix: style of sidebar with in mobile (#16629) 2025-03-24 16:30:23 +08:00
KVOJJJinandGitHub bc6f122364 Fix: style issue of app detail panel in jp (#16620) 2025-03-24 15:54:00 +08:00
KVOJJJinandGitHub 815d77856d Fix: show feedback status in conversation (#16615) 2025-03-24 15:20:18 +08:00
zxhlyhandGitHub 05eaef84bb fix: cancel marketplace debounced search when clear search keywords (#16614) 2025-03-24 15:17:12 +08:00
770c461a8f feat: add openGauss PQ acceleration feature (#16432)
Co-authored-by: chenhuan <huan.chen0728@foxmail>
2025-03-24 15:16:40 +08:00
JoelandGitHub 16b6ffd915 fix: sanitizer svg to avoid xss (#16606) 2025-03-24 14:36:07 +08:00
Wu TianweiandGitHub 9701b573e0 feat: add datasets detail context and provider for improved data vali… (#16451) 2025-03-24 14:30:26 +08:00
L8ngandGitHub 83cd14104d feat: datasets openapi list segements support paged resp (#16603) 2025-03-24 14:27:31 +08:00
KVOJJJinandGitHub e2988acc2f Fix: web app sidebar cannot close when long title conversation existed (#16593) 2025-03-24 12:51:21 +08:00
JoelandGitHub cea4669b76 fix: transition in simple select causes page crash (#16587) 2025-03-24 11:07:16 +08:00
zxhlyhandGitHub 17b4d4c7b2 fix: workflow if-else node variable tag style (#16583) 2025-03-24 11:06:10 +08:00
Ryosei KarakiandGitHub e95f0fcceb chore: enable eslint cache (#16570) 2025-03-24 10:17:54 +08:00
Wu TianweiandGitHub 6d5d6f0f24 fix: update app mode display text for advanced-chat type (#16578) 2025-03-24 10:17:04 +08:00
7ce8faf176 fix: fix variable-aggregator cannot pass node check in group mode (#16439)
Co-authored-by: crazywoola <427733928@qq.com>
2025-03-24 09:49:33 +08:00
f31e3313b0 feat: Make the logic of APP filtering and creation the same (#16079)
Co-authored-by: 刘江波 <jiangbo721@163.com>
2025-03-23 09:40:15 +08:00
Yingchun LaiandGitHub f6ac98a37d fix: fix the app max_active_requests been overwritten bug (#16513) 2025-03-23 09:34:23 +08:00
NFishandGitHub f8e7e301cd fix: update chatbot doc link at create app page (#16479) 2025-03-23 09:27:16 +08:00
dotdotdotandGitHub 35bafb3235 fix: the error occurring when passing inputs on iOS devices and some … (#16534) 2025-03-23 09:26:35 +08:00
LiuBodongandGitHub ae5d2ecf48 fix:weight_type missing when create document in dataset (#16503) 2025-03-22 20:21:57 +08:00
YeuolyandGitHub 1907d2a90a fix: optimize query for expired workflow runs by adding date filter and limiting results (#16491) 2025-03-22 11:17:21 +08:00
JaydenZhouandGitHub 4448a54cc1 use REDIS_PORT to replace 6379 in celery config (#16182) 2025-03-21 21:34:07 +08:00
YeuolyandGitHub bfc0d606dc feat: cleanup free tenants expired data like messages/conversations/workflow_runs/workflow_node_executions (#16490) 2025-03-21 21:30:35 +08:00
kenwoodjwandGitHub 3306228840 fix: workflow file add related-id in iteration node (#16255)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-03-21 20:57:02 +08:00
Yingchun LaiandGitHub d7e00ae691 feat: Skip Redis operations if RateLimit is disabled (#12226) 2025-03-21 19:55:27 +08:00
-LAN-andGitHub 0e2e2db3fa refactor: add OpikDataTrace instance builder. (#16444)
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 18:09:39 +08:00
91 changed files with 2384 additions and 592 deletions
+2 -2
View File
@@ -6,8 +6,8 @@ on:
- "main"
- "deploy/dev"
- "deploy/enterprise"
release:
types: [published]
tags:
- "*"
concurrency:
group: build-push-${{ github.head_ref || github.run_id }}
+4 -3
View File
@@ -26,9 +26,6 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60
# Refresh token expiration time in days
REFRESH_TOKEN_EXPIRE_DAYS=30
# celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
# redis configuration
REDIS_HOST=localhost
REDIS_PORT=6379
@@ -50,6 +47,9 @@ REDIS_USE_CLUSTERS=false
REDIS_CLUSTERS=
REDIS_CLUSTERS_PASSWORD=
# celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
# PostgreSQL database configuration
DB_USERNAME=postgres
DB_PASSWORD=difyai123456
@@ -297,6 +297,7 @@ OCEANBASE_VECTOR_USER=root@test
OCEANBASE_VECTOR_PASSWORD=difyai123456
OCEANBASE_VECTOR_DATABASE=test
OCEANBASE_MEMORY_LIMIT=6G
OCEANBASE_ENABLE_HYBRID_SEARCH=false
# openGauss configuration
OPENGAUSS_HOST=127.0.0.1
+21
View File
@@ -26,6 +26,7 @@ from models.dataset import Document as DatasetDocument
from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
from models.provider import Provider, ProviderModel
from services.account_service import RegisterService, TenantService
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
from services.plugin.data_migration import PluginDataMigration
from services.plugin.plugin_migration import PluginMigration
@@ -792,3 +793,23 @@ def install_plugins(input_file: str, output_file: str, workers: int):
PluginMigration.install_plugins(input_file, output_file, workers)
click.echo(click.style("Install plugins completed.", fg="green"))
@click.command("clear-free-plan-tenant-expired-logs", help="Clear free plan tenant expired logs.")
@click.option("--days", prompt=True, help="The days to clear free plan tenant expired logs.", default=30)
@click.option("--batch", prompt=True, help="The batch size to clear free plan tenant expired logs.", default=100)
@click.option(
"--tenant_ids",
prompt=True,
multiple=True,
help="The tenant ids to clear free plan tenant expired logs.",
)
def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[str]):
"""
Clear free plan tenant expired logs.
"""
click.echo(click.style("Starting clear free plan tenant expired logs.", fg="white"))
ClearFreePlanTenantExpiredLogs.process(days, batch, tenant_ids)
click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green"))
@@ -33,3 +33,9 @@ class OceanBaseVectorConfig(BaseSettings):
description="Name of the OceanBase Vector database to connect to",
default=None,
)
OCEANBASE_ENABLE_HYBRID_SEARCH: bool = Field(
description="Enable hybrid search features (requires OceanBase >= 4.3.5.1). Set to false for compatibility "
"with older versions",
default=False,
)
@@ -43,3 +43,8 @@ class OpenGaussConfig(BaseSettings):
description="Max connection of the OpenGauss database",
default=5,
)
OPENGAUSS_ENABLE_PQ: bool = Field(
description="Enable openGauss PQ acceleration feature",
default=False,
)
+1 -1
View File
@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field(
description="Dify version",
default="1.1.2",
default="1.1.3",
)
COMMIT_SHA: str = Field(
+9 -2
View File
@@ -50,7 +50,15 @@ class AppListApi(Resource):
parser.add_argument(
"mode",
type=str,
choices=["chat", "workflow", "agent-chat", "channel", "all"],
choices=[
"completion",
"chat",
"advanced-chat",
"workflow",
"agent-chat",
"channel",
"all",
],
default="all",
location="args",
required=False,
@@ -130,7 +138,6 @@ class AppApi(Resource):
parser.add_argument("icon_type", type=str, location="json")
parser.add_argument("icon", type=str, location="json")
parser.add_argument("icon_background", type=str, location="json")
parser.add_argument("max_active_requests", type=int, location="json")
parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
args = parser.parse_args()
+2 -2
View File
@@ -646,7 +646,6 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.BAIDU
| VectorType.VIKINGDB
| VectorType.UPSTASH
| VectorType.OCEANBASE
):
return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
case (
@@ -664,6 +663,7 @@ class DatasetRetrievalSettingApi(Resource):
| VectorType.COUCHBASE
| VectorType.MILVUS
| VectorType.OPENGAUSS
| VectorType.OCEANBASE
):
return {
"retrieval_method": [
@@ -692,7 +692,6 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.BAIDU
| VectorType.VIKINGDB
| VectorType.UPSTASH
| VectorType.OCEANBASE
):
return {"retrieval_method": [RetrievalMethod.SEMANTIC_SEARCH.value]}
case (
@@ -708,6 +707,7 @@ class DatasetRetrievalSettingMockApi(Resource):
| VectorType.PGVECTOR
| VectorType.LINDORM
| VectorType.OPENGAUSS
| VectorType.OCEANBASE
):
return {
"retrieval_method": [
@@ -6,6 +6,7 @@ from controllers.console.wraps import setup_required
from controllers.inner_api import api
from controllers.inner_api.wraps import enterprise_inner_api_only
from events.tenant_event import tenant_was_created
from extensions.ext_database import db
from models.account import Account
from services.account_service import TenantService
@@ -19,7 +20,7 @@ class EnterpriseWorkspace(Resource):
parser.add_argument("owner_email", type=str, required=True, location="json")
args = parser.parse_args()
account = Account.query.filter_by(email=args["owner_email"]).first()
account = db.session.query(Account).filter_by(email=args["owner_email"]).first()
if account is None:
return {"message": "owner account not found."}, 404
+224 -22
View File
@@ -1,3 +1,4 @@
from flask import request
from flask_login import current_user # type: ignore
from flask_restful import marshal, reqparse # type: ignore
from werkzeug.exceptions import NotFound
@@ -13,10 +14,20 @@ from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelType
from extensions.ext_database import db
from fields.segment_fields import segment_fields
from models.dataset import Dataset, DocumentSegment
from fields.segment_fields import child_chunk_fields, segment_fields
from models.dataset import Dataset
from services.dataset_service import DatasetService, DocumentService, SegmentService
from services.entities.knowledge_entities.knowledge_entities import SegmentUpdateArgs
from services.errors.chunk import (
ChildChunkDeleteIndexError,
ChildChunkIndexingError,
)
from services.errors.chunk import (
ChildChunkDeleteIndexError as ChildChunkDeleteIndexServiceError,
)
from services.errors.chunk import (
ChildChunkIndexingError as ChildChunkIndexingServiceError,
)
class SegmentApi(DatasetApiResource):
@@ -70,10 +81,12 @@ class SegmentApi(DatasetApiResource):
return {"error": "Segments is required"}, 400
def get(self, tenant_id, dataset_id, document_id):
"""Create single segment."""
"""Get segments."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
page = request.args.get("page", default=1, type=int)
limit = request.args.get("limit", default=20, type=int)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
@@ -107,19 +120,23 @@ class SegmentApi(DatasetApiResource):
status_list = args["status"]
keyword = args["keyword"]
query = DocumentSegment.query.filter(
DocumentSegment.document_id == str(document_id), DocumentSegment.tenant_id == current_user.current_tenant_id
segments, total = SegmentService.get_segments(
document_id=document_id,
tenant_id=current_user.current_tenant_id,
status_list=args["status"],
keyword=args["keyword"],
)
if status_list:
query = query.filter(DocumentSegment.status.in_(status_list))
response = {
"data": marshal(segments, segment_fields),
"doc_form": document.doc_form,
"total": total,
"has_more": len(segments) == limit,
"limit": limit,
"page": page,
}
if keyword:
query = query.where(DocumentSegment.content.ilike(f"%{keyword}%"))
total = query.count()
segments = query.order_by(DocumentSegment.position).all()
return {"data": marshal(segments, segment_fields), "doc_form": document.doc_form, "total": total}, 200
return response, 200
class DatasetSegmentApi(DatasetApiResource):
@@ -138,9 +155,8 @@ class DatasetSegmentApi(DatasetApiResource):
if not document:
raise NotFound("Document not found.")
# check segment
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
SegmentService.delete_segment(segment, document, dataset)
@@ -179,9 +195,7 @@ class DatasetSegmentApi(DatasetApiResource):
raise ProviderNotInitializeError(ex.description)
# check segment
segment_id = str(segment_id)
segment = DocumentSegment.query.filter(
DocumentSegment.id == str(segment_id), DocumentSegment.tenant_id == current_user.current_tenant_id
).first()
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
@@ -190,12 +204,200 @@ class DatasetSegmentApi(DatasetApiResource):
parser.add_argument("segment", type=dict, required=False, nullable=True, location="json")
args = parser.parse_args()
SegmentService.segment_create_args_validate(args["segment"], document)
segment = SegmentService.update_segment(SegmentUpdateArgs(**args["segment"]), segment, document, dataset)
return {"data": marshal(segment, segment_fields), "doc_form": document.doc_form}, 200
updated_segment = SegmentService.update_segment(
SegmentUpdateArgs(**args["segment"]), segment, document, dataset
)
return {"data": marshal(updated_segment, segment_fields), "doc_form": document.doc_form}, 200
class ChildChunkApi(DatasetApiResource):
"""Resource for child chunks."""
@cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
def post(self, tenant_id, dataset_id, document_id, segment_id):
"""Create child chunk."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
# check embedding model setting
if dataset.indexing_technique == "high_quality":
try:
model_manager = ModelManager()
model_manager.get_model_instance(
tenant_id=current_user.current_tenant_id,
provider=dataset.embedding_model_provider,
model_type=ModelType.TEXT_EMBEDDING,
model=dataset.embedding_model,
)
except LLMBadRequestError:
raise ProviderNotInitializeError(
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
# validate args
parser = reqparse.RequestParser()
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
child_chunk = SegmentService.create_child_chunk(args.get("content"), segment, document, dataset)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
def get(self, tenant_id, dataset_id, document_id, segment_id):
"""Get child chunks."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
parser = reqparse.RequestParser()
parser.add_argument("limit", type=int, default=20, location="args")
parser.add_argument("keyword", type=str, default=None, location="args")
parser.add_argument("page", type=int, default=1, location="args")
args = parser.parse_args()
page = args["page"]
limit = min(args["limit"], 100)
keyword = args["keyword"]
child_chunks = SegmentService.get_child_chunks(segment_id, document_id, dataset_id, page, limit, keyword)
return {
"data": marshal(child_chunks.items, child_chunk_fields),
"total": child_chunks.total,
"total_pages": child_chunks.pages,
"page": page,
"limit": limit,
}, 200
class DatasetChildChunkApi(DatasetApiResource):
"""Resource for updating child chunks."""
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
def delete(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id):
"""Delete child chunk."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check document
document_id = str(document_id)
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
# check segment
segment_id = str(segment_id)
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
# check child chunk
child_chunk_id = str(child_chunk_id)
child_chunk = SegmentService.get_child_chunk_by_id(
child_chunk_id=child_chunk_id, tenant_id=current_user.current_tenant_id
)
if not child_chunk:
raise NotFound("Child chunk not found.")
try:
SegmentService.delete_child_chunk(child_chunk, dataset)
except ChildChunkDeleteIndexServiceError as e:
raise ChildChunkDeleteIndexError(str(e))
return {"result": "success"}, 200
@cloud_edition_billing_resource_check("vector_space", "dataset")
@cloud_edition_billing_knowledge_limit_check("add_segment", "dataset")
def patch(self, tenant_id, dataset_id, document_id, segment_id, child_chunk_id):
"""Update child chunk."""
# check dataset
dataset_id = str(dataset_id)
tenant_id = str(tenant_id)
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# get document
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# get segment
segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_user.current_tenant_id)
if not segment:
raise NotFound("Segment not found.")
# get child chunk
child_chunk = SegmentService.get_child_chunk_by_id(
child_chunk_id=child_chunk_id, tenant_id=current_user.current_tenant_id
)
if not child_chunk:
raise NotFound("Child chunk not found.")
# validate args
parser = reqparse.RequestParser()
parser.add_argument("content", type=str, required=True, nullable=False, location="json")
args = parser.parse_args()
try:
child_chunk = SegmentService.update_child_chunk(
args.get("content"), child_chunk, segment, document, dataset
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return {"data": marshal(child_chunk, child_chunk_fields)}, 200
api.add_resource(SegmentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments")
api.add_resource(
DatasetSegmentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>"
)
api.add_resource(
ChildChunkApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks"
)
api.add_resource(
DatasetChildChunkApi,
"/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/segments/<uuid:segment_id>/child_chunks/<uuid:child_chunk_id>",
)
@@ -27,6 +27,9 @@ class RateLimit:
def __init__(self, client_id: str, max_active_requests: int):
self.max_active_requests = max_active_requests
# must be called after max_active_requests is set
if self.disabled():
return
if hasattr(self, "initialized"):
return
self.initialized = True
@@ -37,6 +40,8 @@ class RateLimit:
self.flush_cache(use_local_value=True)
def flush_cache(self, use_local_value=False):
if self.disabled():
return
self.last_recalculate_time = time.time()
# flush max active requests
if use_local_value or not redis_client.exists(self.max_active_requests_key):
@@ -59,18 +64,18 @@ class RateLimit:
redis_client.hdel(self.active_requests_key, *timeout_requests)
def enter(self, request_id: Optional[str] = None) -> str:
if self.disabled():
return RateLimit._UNLIMITED_REQUEST_ID
if time.time() - self.last_recalculate_time > RateLimit._ACTIVE_REQUESTS_COUNT_FLUSH_INTERVAL:
self.flush_cache()
if self.max_active_requests <= 0:
return RateLimit._UNLIMITED_REQUEST_ID
if not request_id:
request_id = RateLimit.gen_request_key()
active_requests_count = redis_client.hlen(self.active_requests_key)
if active_requests_count >= self.max_active_requests:
raise AppInvokeQuotaExceededError(
"Too many requests. Please try again later. The current maximum "
"concurrent requests allowed is {}.".format(self.max_active_requests)
f"Too many requests. Please try again later. The current maximum concurrent requests allowed "
f"for {self.client_id} is {self.max_active_requests}."
)
redis_client.hset(self.active_requests_key, request_id, str(time.time()))
return request_id
@@ -80,6 +85,9 @@ class RateLimit:
return
redis_client.hdel(self.active_requests_key, request_id)
def disabled(self):
return self.max_active_requests <= 0
@staticmethod
def gen_request_key() -> str:
return str(uuid.uuid4())
+1
View File
@@ -49,6 +49,7 @@ class FileAttribute(StrEnum):
TRANSFER_METHOD = "transfer_method"
URL = "url"
EXTENSION = "extension"
RELATED_ID = "related_id"
class ArrayFileAttribute(StrEnum):
+2
View File
@@ -34,6 +34,8 @@ def get_attr(*, file: File, attr: FileAttribute):
return file.remote_url
case FileAttribute.EXTENSION:
return file.extension
case FileAttribute.RELATED_ID:
return file.related_id
def to_prompt_message_content(
+1 -1
View File
@@ -187,7 +187,7 @@ class IndexingRunner:
},
)
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
child_chunks = document_segment.child_chunks
child_chunks = document_segment.get_child_chunks()
if child_chunks:
child_documents = []
for child_chunk in child_chunks:
+1 -1
View File
@@ -1,6 +1,6 @@
# Written by YORKI MINAKO🤡, Edited by Xiaoyi
CONVERSATION_TITLE_PROMPT = """You need to decompose the user's input into "subject" and "intention" in order to accurately figure out what the user's input language actually is.
Notice: the language type user use could be diverse, which can be English, Chinese, Español, Arabic, Japanese, French, and etc.
Notice: the language type user use could be diverse, which can be English, Chinese, Italian, Español, Arabic, Japanese, French, and etc.
MAKE SURE your output is the SAME language as the user's input!
Your output is restricted only to: (Input language) Intention + Subject(short as possible)
Your output MUST be a valid JSON.
+8 -2
View File
@@ -33,7 +33,6 @@ from core.ops.entities.trace_entity import (
)
from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace
from core.ops.langsmith_trace.langsmith_trace import LangSmithDataTrace
from core.ops.opik_trace.opik_trace import OpikDataTrace
from core.ops.utils import get_message_data
from extensions.ext_database import db
from extensions.ext_storage import storage
@@ -41,6 +40,13 @@ from models.model import App, AppModelConfig, Conversation, Message, MessageFile
from models.workflow import WorkflowAppLog, WorkflowRun
from tasks.ops_trace_task import process_trace_tasks
def build_opik_trace_instance(config: OpikConfig):
from core.ops.opik_trace.opik_trace import OpikDataTrace
return OpikDataTrace(config)
provider_config_map: dict[str, dict[str, Any]] = {
TracingProviderEnum.LANGFUSE.value: {
"config_class": LangfuseConfig,
@@ -58,7 +64,7 @@ provider_config_map: dict[str, dict[str, Any]] = {
"config_class": OpikConfig,
"secret_keys": ["api_key"],
"other_keys": ["project", "url", "workspace"],
"trace_instance": OpikDataTrace,
"trace_instance": lambda config: build_opik_trace_instance(config),
},
}
+5 -1
View File
@@ -97,6 +97,7 @@ class RetrievalService:
all_documents=all_documents,
retrieval_method=retrieval_method,
exceptions=exceptions,
document_ids_filter=document_ids_filter,
)
)
concurrent.futures.wait(futures, timeout=30, return_when=concurrent.futures.ALL_COMPLETED)
@@ -222,6 +223,7 @@ class RetrievalService:
all_documents: list,
retrieval_method: str,
exceptions: list,
document_ids_filter: Optional[list[str]] = None,
):
with flask_app.app_context():
try:
@@ -231,7 +233,9 @@ class RetrievalService:
vector_processor = Vector(dataset=dataset)
documents = vector_processor.search_by_full_text(cls.escape_query_for_search(query), top_k=top_k)
documents = vector_processor.search_by_full_text(
cls.escape_query_for_search(query), top_k=top_k, document_ids_filter=document_ids_filter
)
if documents:
if (
reranking_model
@@ -102,8 +102,6 @@ class LindormVectorStore(BaseVector):
if response["errors"]:
for item in response["items"]:
print(f"{item['index']['status']}: {item['index']['error']['type']}")
else:
self.refresh()
def get_ids_by_metadata_field(self, key: str, value: str):
query: dict[str, Any] = {
@@ -167,7 +165,7 @@ class LindormVectorStore(BaseVector):
if not all(isinstance(x, float) for x in query_vector):
raise ValueError("All elements in query_vector should be floats")
top_k = kwargs.get("top_k", 10)
top_k = kwargs.get("top_k", 3)
document_ids_filter = kwargs.get("document_ids_filter")
filters = []
if document_ids_filter:
@@ -210,7 +208,7 @@ class LindormVectorStore(BaseVector):
must_not = kwargs.get("must_not")
should = kwargs.get("should")
minimum_should_match = kwargs.get("minimum_should_match", 0)
top_k = kwargs.get("top_k", 10)
top_k = kwargs.get("top_k", 3)
filters = kwargs.get("filter", [])
document_ids_filter = kwargs.get("document_ids_filter")
if document_ids_filter:
@@ -295,7 +293,7 @@ class LindormVectorStore(BaseVector):
def default_text_mapping(dimension: int, method_name: str, **kwargs: Any) -> dict:
excludes_from_source = kwargs.get("excludes_from_source")
excludes_from_source = kwargs.get("excludes_from_source", False)
analyzer = kwargs.get("analyzer", "ik_max_word")
text_field = kwargs.get("text_field", Field.CONTENT_KEY.value)
engine = kwargs["engine"]
@@ -356,12 +354,12 @@ def default_text_mapping(dimension: int, method_name: str, **kwargs: Any) -> dic
if excludes_from_source:
# e.g. {"excludes": ["vector_field"]}
mapping["mappings"]["_source"] = {"excludes": excludes_from_source}
mapping["mappings"]["_source"] = {"excludes": [vector_field]}
if using_ugc and method_name == "ivfpq":
mapping["settings"]["index"]["knn_routing"] = True
mapping["settings"]["index"]["knn.offline.construction"] = True
elif using_ugc and method_name == "hnsw" or using_ugc and method_name == "flat":
elif (using_ugc and method_name == "hnsw") or (using_ugc and method_name == "flat"):
mapping["settings"]["index"]["knn_routing"] = True
return mapping
@@ -458,7 +456,7 @@ def default_vector_search_query(
"query": {"knn": {vector_field: {"vector": query_vector, "k": k}}},
}
if filters is not None:
if filters is not None and len(filters) > 0:
# when using filter, transform filter from List[Dict] to Dict as valid format
filter_dict = {"bool": {"must": filters}} if len(filters) > 1 else filters[0]
search_query["query"]["knn"][vector_field]["filter"] = filter_dict # filter should be Dict
@@ -231,8 +231,8 @@ class MilvusVector(BaseVector):
document_ids_filter = kwargs.get("document_ids_filter")
filter = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
filter = f'metadata["document_id"] in ({document_ids})'
document_ids = ", ".join(f'"{id}"' for id in document_ids_filter)
filter = f'metadata["document_id"] in [{document_ids}]'
results = self._client.search(
collection_name=self._collection_name,
data=[query_vector],
@@ -259,7 +259,7 @@ class MilvusVector(BaseVector):
filter = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
filter = f'metadata["document_id"] in ({document_ids})'
filter = f'metadata["document_id"] in [{document_ids}]'
results = self._client.search(
collection_name=self._collection_name,
@@ -31,6 +31,7 @@ class OceanBaseVectorConfig(BaseModel):
user: str
password: str
database: str
enable_hybrid_search: bool = False
@model_validator(mode="before")
@classmethod
@@ -57,6 +58,7 @@ class OceanBaseVector(BaseVector):
password=self._config.password,
db_name=self._config.database,
)
self._hybrid_search_enabled = self._check_hybrid_search_support() # Check if hybrid search is supported
def get_type(self) -> str:
return VectorType.OCEANBASE
@@ -98,6 +100,16 @@ class OceanBaseVector(BaseVector):
columns=cols,
vidxs=vidx_params,
)
try:
if self._hybrid_search_enabled:
self._client.perform_raw_text_sql(f"""ALTER TABLE {self._collection_name}
ADD FULLTEXT INDEX fulltext_index_for_col_text (text) WITH PARSER ik""")
except Exception as e:
raise Exception(
"Failed to add fulltext index to the target table, your OceanBase version must be 4.3.5.1 or above "
+ "to support fulltext index and vector index in the same table",
e,
)
vals = []
params = self._client.perform_raw_text_sql("SHOW PARAMETERS LIKE '%ob_vector_memory_limit_percentage%'")
for row in params:
@@ -116,6 +128,27 @@ class OceanBaseVector(BaseVector):
)
redis_client.set(collection_exist_cache_key, 1, ex=3600)
def _check_hybrid_search_support(self) -> bool:
"""
Check if the current OceanBase version supports hybrid search.
Returns True if the version is >= 4.3.5.1, otherwise False.
"""
if not self._config.enable_hybrid_search:
return False
try:
from packaging import version
# return OceanBase_CE 4.3.5.1 (r101000042025031818-bxxxx) (Built Mar 18 2025 18:13:36)
result = self._client.perform_raw_text_sql("SELECT @@version_comment AS version")
ob_full_version = result.fetchone()[0]
ob_version = ob_full_version.split()[1]
logger.debug("Current OceanBase version is %s", ob_version)
return version.parse(ob_version).base_version >= version.parse("4.3.5.1").base_version
except Exception as e:
logger.warning(f"Failed to check OceanBase version: {str(e)}. Disabling hybrid search.")
return False
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
ids = self._get_uuids(documents)
for id, doc, emb in zip(ids, documents, embeddings):
@@ -130,7 +163,7 @@ class OceanBaseVector(BaseVector):
)
def text_exists(self, id: str) -> bool:
cur = self._client.get(table_name=self._collection_name, id=id)
cur = self._client.get(table_name=self._collection_name, ids=id)
return bool(cur.rowcount != 0)
def delete_by_ids(self, ids: list[str]) -> None:
@@ -139,9 +172,12 @@ class OceanBaseVector(BaseVector):
self._client.delete(table_name=self._collection_name, ids=ids)
def get_ids_by_metadata_field(self, key: str, value: str) -> list[str]:
from sqlalchemy import text
cur = self._client.get(
table_name=self._collection_name,
where_clause=f"metadata->>'$.{key}' = '{value}'",
ids=None,
where_clause=[text(f"metadata->>'$.{key}' = '{value}'")],
output_column_name=["id"],
)
return [row[0] for row in cur]
@@ -151,36 +187,84 @@ class OceanBaseVector(BaseVector):
self.delete_by_ids(ids)
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
return []
if not self._hybrid_search_enabled:
return []
try:
top_k = kwargs.get("top_k", 5)
if not isinstance(top_k, int) or top_k <= 0:
raise ValueError("top_k must be a positive integer")
document_ids_filter = kwargs.get("document_ids_filter")
where_clause = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
where_clause = f" AND metadata->>'$.document_id' IN ({document_ids})"
full_sql = f"""SELECT metadata, text, MATCH (text) AGAINST (:query) AS score
FROM {self._collection_name}
WHERE MATCH (text) AGAINST (:query) > 0
{where_clause}
ORDER BY score DESC
LIMIT {top_k}"""
with self._client.engine.connect() as conn:
with conn.begin():
from sqlalchemy import text
result = conn.execute(text(full_sql), {"query": query})
rows = result.fetchall()
docs = []
for row in rows:
metadata_str, _text, score = row
try:
metadata = json.loads(metadata_str)
except json.JSONDecodeError:
print(f"Invalid JSON metadata: {metadata_str}")
metadata = {}
metadata["score"] = score
docs.append(Document(page_content=_text, metadata=metadata))
return docs
except Exception as e:
logger.warning(f"Failed to fulltext search: {str(e)}.")
return []
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
document_ids_filter = kwargs.get("document_ids_filter")
where_clause = None
_where_clause = None
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
where_clause = f"metadata->>'$.document_id' in ({document_ids})"
from sqlalchemy import text
_where_clause = [text(where_clause)]
ef_search = kwargs.get("ef_search", self._hnsw_ef_search)
if ef_search != self._hnsw_ef_search:
self._client.set_ob_hnsw_ef_search(ef_search)
self._hnsw_ef_search = ef_search
topk = kwargs.get("top_k", 10)
cur = self._client.ann_search(
table_name=self._collection_name,
vec_column_name="vector",
vec_data=query_vector,
topk=topk,
distance_func=func.l2_distance,
output_column_names=["text", "metadata"],
with_dist=True,
where_clause=where_clause,
)
try:
cur = self._client.ann_search(
table_name=self._collection_name,
vec_column_name="vector",
vec_data=query_vector,
topk=topk,
distance_func=func.l2_distance,
output_column_names=["text", "metadata"],
with_dist=True,
where_clause=_where_clause,
)
except Exception as e:
raise Exception("Failed to search by vector. ", e)
docs = []
for text, metadata, distance in cur:
for _text, metadata, distance in cur:
metadata = json.loads(metadata)
metadata["score"] = 1 - distance / math.sqrt(2)
docs.append(
Document(
page_content=text,
page_content=_text,
metadata=metadata,
)
)
@@ -25,6 +25,7 @@ class OpenGaussConfig(BaseModel):
database: str
min_connection: int
max_connection: int
enable_pq: bool = False # Enable PQ acceleration
@model_validator(mode="before")
@classmethod
@@ -57,6 +58,11 @@ CREATE TABLE IF NOT EXISTS {table_name} (
);
"""
SQL_CREATE_INDEX_PQ = """
CREATE INDEX IF NOT EXISTS embedding_{table_name}_pq_idx ON {table_name}
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64, enable_pq=on, pq_m={pq_m});
"""
SQL_CREATE_INDEX = """
CREATE INDEX IF NOT EXISTS embedding_cosine_{table_name}_idx ON {table_name}
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
@@ -68,6 +74,7 @@ class OpenGauss(BaseVector):
super().__init__(collection_name)
self.pool = self._create_connection_pool(config)
self.table_name = f"embedding_{collection_name}"
self.pq_enabled = config.enable_pq
def get_type(self) -> str:
return VectorType.OPENGAUSS
@@ -97,7 +104,26 @@ class OpenGauss(BaseVector):
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
dimension = len(embeddings[0])
self._create_collection(dimension)
return self.add_texts(texts, embeddings)
self.add_texts(texts, embeddings)
self._create_index(dimension)
def _create_index(self, dimension: int):
index_cache_key = f"vector_index_{self._collection_name}"
lock_name = f"{index_cache_key}_lock"
with redis_client.lock(lock_name, timeout=60):
index_exist_cache_key = f"vector_index_{self._collection_name}"
if redis_client.get(index_exist_cache_key):
return
with self._get_cursor() as cur:
if dimension <= 2000:
if self.pq_enabled:
cur.execute(SQL_CREATE_INDEX_PQ.format(table_name=self.table_name, pq_m=int(dimension / 4)))
cur.execute("SET hnsw_earlystop_threshold = 320")
if not self.pq_enabled:
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
redis_client.set(index_exist_cache_key, 1, ex=3600)
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
values = []
@@ -211,8 +237,6 @@ class OpenGauss(BaseVector):
with self._get_cursor() as cur:
cur.execute(SQL_CREATE_TABLE.format(table_name=self.table_name, dimension=dimension))
if dimension <= 2000:
cur.execute(SQL_CREATE_INDEX.format(table_name=self.table_name))
redis_client.set(collection_exist_cache_key, 1, ex=3600)
@@ -236,5 +260,6 @@ class OpenGaussFactory(AbstractVectorFactory):
database=dify_config.OPENGAUSS_DATABASE or "dify",
min_connection=dify_config.OPENGAUSS_MIN_CONNECTION,
max_connection=dify_config.OPENGAUSS_MAX_CONNECTION,
enable_pq=dify_config.OPENGAUSS_ENABLE_PQ or False,
),
)
@@ -177,7 +177,7 @@ class PGVector(BaseVector):
where_clause = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
where_clause = f" WHERE metadata->>'document_id' in ({document_ids}) "
where_clause = f" WHERE meta->>'document_id' in ({document_ids}) "
with self._get_cursor() as cur:
cur.execute(
@@ -205,7 +205,7 @@ class PGVector(BaseVector):
where_clause = ""
if document_ids_filter:
document_ids = ", ".join(f"'{id}'" for id in document_ids_filter)
where_clause = f" AND metadata->>'document_id' in ({document_ids}) "
where_clause = f" AND meta->>'document_id' in ({document_ids}) "
if self.pg_bigm:
cur.execute("SET pg_bigm.similarity_limit TO 0.000001")
cur.execute(
+9 -2
View File
@@ -610,7 +610,11 @@ class DatasetRetrieval:
if dataset.indexing_technique == "economy":
# use keyword table query
documents = RetrievalService.retrieve(
retrieval_method="keyword_search", dataset_id=dataset.id, query=query, top_k=top_k
retrieval_method="keyword_search",
dataset_id=dataset.id,
query=query,
top_k=top_k,
document_ids_filter=document_ids_filter,
)
if documents:
all_documents.extend(documents)
@@ -896,7 +900,10 @@ class DatasetRetrieval:
return str(inputs.get(key, f"{{{{{key}}}}}"))
pattern = re.compile(r"\{\{(\w+)\}\}")
return pattern.sub(replacer, text)
output = pattern.sub(replacer, text)
if isinstance(output, str):
output = re.sub(r"[\r\n\t]+", " ", output).strip()
return output
def _automatic_metadata_filter_func(
self, dataset_ids: list, query: str, tenant_id: str, user_id: str, metadata_model_config: ModelConfig
@@ -1,5 +1,6 @@
import json
import logging
import re
import time
from collections import defaultdict
from collections.abc import Mapping, Sequence
@@ -360,8 +361,13 @@ class KnowledgeRetrievalNode(LLMNode):
if isinstance(expected_value, str):
expected_value = self.graph_runtime_state.variable_pool.convert_template(
expected_value
).text
).value[0]
if expected_value.value_type == "number":
expected_value = expected_value.value
elif expected_value.value_type == "string":
expected_value = re.sub(r"[\r\n\t]+", " ", expected_value.text).strip()
else:
raise ValueError("Invalid expected metadata value type")
filters = self._process_metadata_filter_func(
condition.comparison_operator, metadata_name, expected_value, filters
)
+2
View File
@@ -4,6 +4,7 @@ from dify_app import DifyApp
def init_app(app: DifyApp):
from commands import (
add_qdrant_index,
clear_free_plan_tenant_expired_logs,
convert_to_agent_apps,
create_tenant,
extract_plugins,
@@ -34,6 +35,7 @@ def init_app(app: DifyApp):
extract_unique_plugins,
install_plugins,
old_metadata_migration,
clear_free_plan_tenant_expired_logs,
]
for cmd in cmds_to_register:
app.cli.add_command(cmd)
+1
View File
@@ -24,6 +24,7 @@ vector_setting_fields = {
}
weighted_score_fields = {
"weight_type": fields.String,
"keyword_setting": fields.Nested(keyword_setting_fields),
"vector_setting": fields.Nested(vector_setting_fields),
}
+17
View File
@@ -720,6 +720,23 @@ class DocumentSegment(db.Model): # type: ignore[name-defined]
else:
return []
def get_child_chunks(self):
process_rule = self.document.dataset_process_rule
if process_rule.mode == "hierarchical":
rules = Rule(**process_rule.rules_dict)
if rules.parent_mode:
child_chunks = (
db.session.query(ChildChunk)
.filter(ChildChunk.segment_id == self.id)
.order_by(ChildChunk.position.asc())
.all()
)
return child_chunks or []
else:
return []
else:
return []
@property
def sign_content(self):
return self.get_sign_content()
+27
View File
@@ -838,6 +838,33 @@ class Conversation(db.Model): # type: ignore[name-defined]
def in_debug_mode(self):
return self.override_model_configs is not None
def to_dict(self):
return {
"id": self.id,
"app_id": self.app_id,
"app_model_config_id": self.app_model_config_id,
"model_provider": self.model_provider,
"override_model_configs": self.override_model_configs,
"model_id": self.model_id,
"mode": self.mode,
"name": self.name,
"summary": self.summary,
"inputs": self.inputs,
"introduction": self.introduction,
"system_instruction": self.system_instruction,
"system_instruction_tokens": self.system_instruction_tokens,
"status": self.status,
"invoke_from": self.invoke_from,
"from_source": self.from_source,
"from_end_user_id": self.from_end_user_id,
"from_account_id": self.from_account_id,
"read_at": self.read_at,
"read_account_id": self.read_account_id,
"dialogue_count": self.dialogue_count,
"created_at": self.created_at,
"updated_at": self.updated_at,
}
class Message(db.Model): # type: ignore[name-defined]
__tablename__ = "messages"
@@ -15,11 +15,11 @@ from services.feature_service import FeatureService
@app.celery.task(queue="dataset")
def send_document_clean_notify_task():
def mail_clean_document_notify_task():
"""
Async Send document clean notify mail
Usage: send_document_clean_notify_task.delay()
Usage: mail_clean_document_notify_task.delay()
"""
if not mail.is_inited():
return
+6 -7
View File
@@ -9,7 +9,6 @@ from flask_sqlalchemy.pagination import Pagination
from configs import dify_config
from constants.model_template import default_app_templates
from core.agent.entities import AgentToolEntity
from core.app.features.rate_limiting import RateLimit
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelPropertyKey, ModelType
@@ -37,9 +36,13 @@ class AppService:
filters = [App.tenant_id == tenant_id, App.is_universal == False]
if args["mode"] == "workflow":
filters.append(App.mode.in_([AppMode.WORKFLOW.value, AppMode.COMPLETION.value]))
filters.append(App.mode == AppMode.WORKFLOW.value)
elif args["mode"] == "completion":
filters.append(App.mode == AppMode.COMPLETION.value)
elif args["mode"] == "chat":
filters.append(App.mode.in_([AppMode.CHAT.value, AppMode.ADVANCED_CHAT.value]))
filters.append(App.mode == AppMode.CHAT.value)
elif args["mode"] == "advanced-chat":
filters.append(App.mode == AppMode.ADVANCED_CHAT.value)
elif args["mode"] == "agent-chat":
filters.append(App.mode == AppMode.AGENT_CHAT.value)
elif args["mode"] == "channel":
@@ -222,7 +225,6 @@ class AppService:
"""
app.name = args.get("name")
app.description = args.get("description", "")
app.max_active_requests = args.get("max_active_requests")
app.icon_type = args.get("icon_type", "emoji")
app.icon = args.get("icon")
app.icon_background = args.get("icon_background")
@@ -231,9 +233,6 @@ class AppService:
app.updated_at = datetime.now(UTC).replace(tzinfo=None)
db.session.commit()
if app.max_active_requests is not None:
rate_limit = RateLimit(app.id, app.max_active_requests)
rate_limit.flush_cache(use_local_value=True)
return app
def update_app_name(self, app: App, name: str) -> App:
@@ -0,0 +1,313 @@
import datetime
import json
import logging
import time
from concurrent.futures import ThreadPoolExecutor
import click
from flask import Flask, current_app
from sqlalchemy.orm import Session
from configs import dify_config
from core.model_runtime.utils.encoders import jsonable_encoder
from extensions.ext_database import db
from extensions.ext_storage import storage
from models.account import Tenant
from models.model import App, Conversation, Message
from models.workflow import WorkflowNodeExecution, WorkflowRun
from services.billing_service import BillingService
logger = logging.getLogger(__name__)
class ClearFreePlanTenantExpiredLogs:
@classmethod
def process_tenant(cls, flask_app: Flask, tenant_id: str, days: int, batch: int):
with flask_app.app_context():
apps = db.session.query(App).filter(App.tenant_id == tenant_id).all()
app_ids = [app.id for app in apps]
while True:
with Session(db.engine).no_autoflush as session:
messages = (
session.query(Message)
.filter(
Message.app_id.in_(app_ids),
Message.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(messages) == 0:
break
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/messages/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(
[message.to_dict() for message in messages],
),
).encode("utf-8"),
)
message_ids = [message.id for message in messages]
# delete messages
session.query(Message).filter(
Message.id.in_(message_ids),
).delete(synchronize_session=False)
session.commit()
click.echo(
click.style(
f"[{datetime.datetime.now()}] Processed {len(message_ids)} messages for tenant {tenant_id} "
)
)
while True:
with Session(db.engine).no_autoflush as session:
conversations = (
session.query(Conversation)
.filter(
Conversation.app_id.in_(app_ids),
Conversation.updated_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(conversations) == 0:
break
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/conversations/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(
[conversation.to_dict() for conversation in conversations],
),
).encode("utf-8"),
)
conversation_ids = [conversation.id for conversation in conversations]
session.query(Conversation).filter(
Conversation.id.in_(conversation_ids),
).delete(synchronize_session=False)
session.commit()
click.echo(
click.style(
f"[{datetime.datetime.now()}] Processed {len(conversation_ids)}"
f" conversations for tenant {tenant_id}"
)
)
while True:
with Session(db.engine).no_autoflush as session:
workflow_node_executions = (
session.query(WorkflowNodeExecution)
.filter(
WorkflowNodeExecution.tenant_id == tenant_id,
WorkflowNodeExecution.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(workflow_node_executions) == 0:
break
# save workflow node executions
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/workflow_node_executions/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(workflow_node_executions),
).encode("utf-8"),
)
workflow_node_execution_ids = [
workflow_node_execution.id for workflow_node_execution in workflow_node_executions
]
# delete workflow node executions
session.query(WorkflowNodeExecution).filter(
WorkflowNodeExecution.id.in_(workflow_node_execution_ids),
).delete(synchronize_session=False)
session.commit()
click.echo(
click.style(
f"[{datetime.datetime.now()}] Processed {len(workflow_node_execution_ids)}"
f" workflow node executions for tenant {tenant_id}"
)
)
while True:
with Session(db.engine).no_autoflush as session:
workflow_runs = (
session.query(WorkflowRun)
.filter(
WorkflowRun.tenant_id == tenant_id,
WorkflowRun.created_at < datetime.datetime.now() - datetime.timedelta(days=days),
)
.limit(batch)
.all()
)
if len(workflow_runs) == 0:
break
# save workflow runs
storage.save(
f"free_plan_tenant_expired_logs/"
f"{tenant_id}/workflow_runs/{datetime.datetime.now().strftime('%Y-%m-%d')}"
f"-{time.time()}.json",
json.dumps(
jsonable_encoder(
[workflow_run.to_dict() for workflow_run in workflow_runs],
),
).encode("utf-8"),
)
workflow_run_ids = [workflow_run.id for workflow_run in workflow_runs]
# delete workflow runs
session.query(WorkflowRun).filter(
WorkflowRun.id.in_(workflow_run_ids),
).delete(synchronize_session=False)
session.commit()
@classmethod
def process(cls, days: int, batch: int, tenant_ids: list[str]):
"""
Clear free plan tenant expired logs.
"""
click.echo(click.style("Clearing free plan tenant expired logs", fg="white"))
ended_at = datetime.datetime.now()
started_at = datetime.datetime(2023, 4, 3, 8, 59, 24)
current_time = started_at
with Session(db.engine) as session:
total_tenant_count = session.query(Tenant.id).count()
click.echo(click.style(f"Total tenant count: {total_tenant_count}", fg="white"))
handled_tenant_count = 0
thread_pool = ThreadPoolExecutor(max_workers=10)
def process_tenant(flask_app: Flask, tenant_id: str) -> None:
try:
if (
not dify_config.BILLING_ENABLED
or BillingService.get_info(tenant_id)["subscription"]["plan"] == "sandbox"
):
# only process sandbox tenant
cls.process_tenant(flask_app, tenant_id, days, batch)
except Exception:
logger.exception(f"Failed to process tenant {tenant_id}")
finally:
nonlocal handled_tenant_count
handled_tenant_count += 1
if handled_tenant_count % 100 == 0:
click.echo(
click.style(
f"[{datetime.datetime.now()}] "
f"Processed {handled_tenant_count} tenants "
f"({(handled_tenant_count / total_tenant_count) * 100:.1f}%), "
f"{handled_tenant_count}/{total_tenant_count}",
fg="green",
)
)
futures = []
if tenant_ids:
for tenant_id in tenant_ids:
futures.append(
thread_pool.submit(
process_tenant,
current_app._get_current_object(), # type: ignore[attr-defined]
tenant_id,
)
)
else:
while current_time < ended_at:
click.echo(
click.style(f"Current time: {current_time}, Started at: {datetime.datetime.now()}", fg="white")
)
# Initial interval of 1 day, will be dynamically adjusted based on tenant count
interval = datetime.timedelta(days=1)
# Process tenants in this batch
with Session(db.engine) as session:
# Calculate tenant count in next batch with current interval
# Try different intervals until we find one with a reasonable tenant count
test_intervals = [
datetime.timedelta(days=1),
datetime.timedelta(hours=12),
datetime.timedelta(hours=6),
datetime.timedelta(hours=3),
datetime.timedelta(hours=1),
]
for test_interval in test_intervals:
tenant_count = (
session.query(Tenant.id)
.filter(Tenant.created_at.between(current_time, current_time + test_interval))
.count()
)
if tenant_count <= 100:
interval = test_interval
break
else:
# If all intervals have too many tenants, use minimum interval
interval = datetime.timedelta(hours=1)
# Adjust interval to target ~100 tenants per batch
if tenant_count > 0:
# Scale interval based on ratio to target count
interval = min(
datetime.timedelta(days=1), # Max 1 day
max(
datetime.timedelta(hours=1), # Min 1 hour
interval * (100 / tenant_count), # Scale to target 100
),
)
batch_end = min(current_time + interval, ended_at)
rs = (
session.query(Tenant.id)
.filter(Tenant.created_at.between(current_time, batch_end))
.order_by(Tenant.created_at)
)
tenants = []
for row in rs:
tenant_id = str(row.id)
try:
tenants.append(tenant_id)
except Exception:
logger.exception(f"Failed to process tenant {tenant_id}")
continue
futures.append(
thread_pool.submit(
process_tenant,
current_app._get_current_object(), # type: ignore[attr-defined]
tenant_id,
)
)
current_time = batch_end
# wait for all threads to finish
for future in futures:
future.result()
+82
View File
@@ -2140,6 +2140,88 @@ class SegmentService:
query = query.where(ChildChunk.content.ilike(f"%{keyword}%"))
return query.paginate(page=page, per_page=limit, max_per_page=100, error_out=False)
@classmethod
def get_child_chunk_by_id(cls, child_chunk_id: str, tenant_id: str) -> Optional[ChildChunk]:
"""Get a child chunk by its ID."""
result = ChildChunk.query.filter(ChildChunk.id == child_chunk_id, ChildChunk.tenant_id == tenant_id).first()
return result if isinstance(result, ChildChunk) else None
@classmethod
def get_segments(
cls, document_id: str, tenant_id: str, status_list: list[str] | None = None, keyword: str | None = None
):
"""Get segments for a document with optional filtering."""
query = DocumentSegment.query.filter(
DocumentSegment.document_id == document_id, DocumentSegment.tenant_id == tenant_id
)
if status_list:
query = query.filter(DocumentSegment.status.in_(status_list))
if keyword:
query = query.filter(DocumentSegment.content.ilike(f"%{keyword}%"))
segments = query.order_by(DocumentSegment.position.asc()).all()
total = len(segments)
return segments, total
@classmethod
def update_segment_by_id(
cls, tenant_id: str, dataset_id: str, document_id: str, segment_id: str, segment_data: dict, user_id: str
) -> tuple[DocumentSegment, Document]:
"""Update a segment by its ID with validation and checks."""
# check dataset
dataset = db.session.query(Dataset).filter(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
if not dataset:
raise NotFound("Dataset not found.")
# check user's model setting
DatasetService.check_dataset_model_setting(dataset)
# check document
document = DocumentService.get_document(dataset_id, document_id)
if not document:
raise NotFound("Document not found.")
# check embedding model setting if high quality
if dataset.indexing_technique == "high_quality":
try:
model_manager = ModelManager()
model_manager.get_model_instance(
tenant_id=user_id,
provider=dataset.embedding_model_provider,
model_type=ModelType.TEXT_EMBEDDING,
model=dataset.embedding_model,
)
except LLMBadRequestError:
raise ValueError(
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
)
except ProviderTokenNotInitError as ex:
raise ValueError(ex.description)
# check segment
segment = DocumentSegment.query.filter(
DocumentSegment.id == segment_id, DocumentSegment.tenant_id == user_id
).first()
if not segment:
raise NotFound("Segment not found.")
# validate and update segment
cls.segment_create_args_validate(segment_data, document)
updated_segment = cls.update_segment(SegmentUpdateArgs(**segment_data), segment, document, dataset)
return updated_segment, document
@classmethod
def get_segment_by_id(cls, segment_id: str, tenant_id: str) -> Optional[DocumentSegment]:
"""Get a segment by its ID."""
result = DocumentSegment.query.filter(
DocumentSegment.id == segment_id, DocumentSegment.tenant_id == tenant_id
).first()
return result if isinstance(result, DocumentSegment) else None
class DatasetCollectionBindingService:
@classmethod
@@ -95,7 +95,7 @@ class WeightKeywordSetting(BaseModel):
class WeightModel(BaseModel):
weight_type: str
weight_type: Optional[str] = None
vector_setting: Optional[WeightVectorSetting] = None
keyword_setting: Optional[WeightKeywordSetting] = None
+1 -1
View File
@@ -59,7 +59,7 @@ def add_document_to_index_task(dataset_document_id: str):
},
)
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
child_chunks = segment.child_chunks
child_chunks = segment.get_child_chunks()
if child_chunks:
child_documents = []
for child_chunk in child_chunks:
+1 -1
View File
@@ -130,7 +130,7 @@ def deal_dataset_vector_index_task(dataset_id: str, action: str):
},
)
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
child_chunks = segment.child_chunks
child_chunks = segment.get_child_chunks()
if child_chunks:
child_documents = []
for child_chunk in child_chunks:
+1 -1
View File
@@ -63,7 +63,7 @@ def enable_segment_to_index_task(segment_id: str):
index_processor = IndexProcessorFactory(dataset_document.doc_form).init_index_processor()
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
child_chunks = segment.child_chunks
child_chunks = segment.get_child_chunks()
if child_chunks:
child_documents = []
for child_chunk in child_chunks:
+1 -1
View File
@@ -67,7 +67,7 @@ def enable_segments_to_index_task(segment_ids: list, dataset_id: str, document_i
)
if dataset_document.doc_form == IndexType.PARENT_CHILD_INDEX:
child_chunks = segment.child_chunks
child_chunks = segment.get_child_chunks()
if child_chunks:
child_documents = []
for child_chunk in child_chunks:
+2
View File
@@ -554,6 +554,7 @@ OCEANBASE_VECTOR_PASSWORD=difyai123456
OCEANBASE_VECTOR_DATABASE=test
OCEANBASE_CLUSTER_NAME=difyai
OCEANBASE_MEMORY_LIMIT=6G
OCEANBASE_ENABLE_HYBRID_SEARCH=false
# opengauss configurations, only available when VECTOR_STORE is `opengauss`
OPENGAUSS_HOST=opengauss
@@ -563,6 +564,7 @@ OPENGAUSS_PASSWORD=Dify@123
OPENGAUSS_DATABASE=dify
OPENGAUSS_MIN_CONNECTION=1
OPENGAUSS_MAX_CONNECTION=5
OPENGAUSS_ENABLE_PQ=false
# Upstash Vector configuration, only available when VECTOR_STORE is `upstash`
UPSTASH_VECTOR_URL=https://xxx-vector.upstash.io
+9 -6
View File
@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.1.2
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@@ -29,7 +29,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.1.2
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@@ -53,7 +53,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.1.2
image: langgenius/dify-web:1.1.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -110,7 +110,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations
@@ -373,7 +373,8 @@ services:
# OceanBase vector database
oceanbase:
image: quay.io/oceanbase/oceanbase-ce:4.3.3.0-100000142024101215
image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818
container_name: oceanbase
profiles:
- oceanbase
restart: always
@@ -386,7 +387,9 @@ services:
OB_SYS_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456}
OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456}
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OB_SERVER_IP: '127.0.0.1'
MODE: MINI
ports:
- "${OCEANBASE_VECTOR_PORT:-2881}:2881"
# Oracle vector database
oracle:
+1 -1
View File
@@ -43,7 +43,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations
+11 -6
View File
@@ -252,6 +252,7 @@ x-shared-env: &shared-api-worker-env
OCEANBASE_VECTOR_DATABASE: ${OCEANBASE_VECTOR_DATABASE:-test}
OCEANBASE_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OCEANBASE_MEMORY_LIMIT: ${OCEANBASE_MEMORY_LIMIT:-6G}
OCEANBASE_ENABLE_HYBRID_SEARCH: ${OCEANBASE_ENABLE_HYBRID_SEARCH:-false}
OPENGAUSS_HOST: ${OPENGAUSS_HOST:-opengauss}
OPENGAUSS_PORT: ${OPENGAUSS_PORT:-6600}
OPENGAUSS_USER: ${OPENGAUSS_USER:-postgres}
@@ -259,6 +260,7 @@ x-shared-env: &shared-api-worker-env
OPENGAUSS_DATABASE: ${OPENGAUSS_DATABASE:-dify}
OPENGAUSS_MIN_CONNECTION: ${OPENGAUSS_MIN_CONNECTION:-1}
OPENGAUSS_MAX_CONNECTION: ${OPENGAUSS_MAX_CONNECTION:-5}
OPENGAUSS_ENABLE_PQ: ${OPENGAUSS_ENABLE_PQ:-false}
UPSTASH_VECTOR_URL: ${UPSTASH_VECTOR_URL:-https://xxx-vector.upstash.io}
UPSTASH_VECTOR_TOKEN: ${UPSTASH_VECTOR_TOKEN:-dify}
UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
@@ -432,7 +434,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.1.2
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@@ -459,7 +461,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.1.2
image: langgenius/dify-api:1.1.3
restart: always
environment:
# Use the shared environment variables.
@@ -483,7 +485,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.1.2
image: langgenius/dify-web:1.1.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -540,7 +542,7 @@ services:
# The DifySandbox
sandbox:
image: langgenius/dify-sandbox:0.2.10
image: langgenius/dify-sandbox:0.2.11
restart: always
environment:
# The DifySandbox configurations
@@ -803,7 +805,8 @@ services:
# OceanBase vector database
oceanbase:
image: quay.io/oceanbase/oceanbase-ce:4.3.3.0-100000142024101215
image: oceanbase/oceanbase-ce:4.3.5.1-101000042025031818
container_name: oceanbase
profiles:
- oceanbase
restart: always
@@ -816,7 +819,9 @@ services:
OB_SYS_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456}
OB_TENANT_PASSWORD: ${OCEANBASE_VECTOR_PASSWORD:-difyai123456}
OB_CLUSTER_NAME: ${OCEANBASE_CLUSTER_NAME:-difyai}
OB_SERVER_IP: '127.0.0.1'
MODE: MINI
ports:
- "${OCEANBASE_VECTOR_PORT:-2881}:2881"
# Oracle vector database
oracle:
+3
View File
@@ -8,6 +8,7 @@ import { useDebounceFn } from 'ahooks'
import {
RiApps2Line,
RiExchange2Line,
RiFile4Line,
RiMessage3Line,
RiRobot3Line,
} from '@remixicon/react'
@@ -81,6 +82,8 @@ const Apps = () => {
{ value: 'all', text: t('app.types.all'), icon: <RiApps2Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'chat', text: t('app.types.chatbot'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'agent-chat', text: t('app.types.agent'), icon: <RiRobot3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'advanced-chat', text: t('app.types.advanced'), icon: <RiMessage3Line className='mr-1 h-[14px] w-[14px]' /> },
{ value: 'workflow', text: t('app.types.workflow'), icon: <RiExchange2Line className='mr-1 h-[14px] w-[14px]' /> },
]
@@ -961,6 +961,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='status' type='string' key='status'>
Search status, completed
</Property>
<Property name='page' type='string' key='page'>
Page number (optional)
</Property>
<Property name='limit' type='string' key='limit'>
Number of items returned, default 20, range 1-100 (optional)
</Property>
</Properties>
</Col>
<Col sticky>
@@ -1004,7 +1010,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"error": null,
"stopped_at": null
}],
"doc_form": "text_model"
"doc_form": "text_model",
"has_more": false,
"limit": 20,
"total": 9,
"page": 1
}
```
</CodeGroup>
@@ -1148,6 +1158,276 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks'
method='POST'
title='Create Child Chunk'
name='#create_child_chunk'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
Segment ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='content' type='string' key='content'>
Child chunk content
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"content": "Child chunk content"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
"content": "Child chunk content"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": {
"id": "",
"segment_id": "",
"content": "Child chunk content",
"word_count": 25,
"tokens": 0,
"index_node_id": "",
"index_node_hash": "",
"status": "completed",
"created_by": "",
"created_at": 1695312007,
"indexing_at": 1695312007,
"completed_at": 1695312007,
"error": null,
"stopped_at": null
}
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks'
method='GET'
title='Get Child Chunks'
name='#get_child_chunks'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
Segment ID
</Property>
</Properties>
### Query
<Properties>
<Property name='keyword' type='string' key='keyword'>
Search keyword (optional)
</Property>
<Property name='page' type='integer' key='page'>
Page number (optional, default: 1)
</Property>
<Property name='limit' type='integer' key='limit'>
Items per page (optional, default: 20, max: 100)
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks?page=1&limit=20' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks?page=1&limit=20' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": [{
"id": "",
"segment_id": "",
"content": "Child chunk content",
"word_count": 25,
"tokens": 0,
"index_node_id": "",
"index_node_hash": "",
"status": "completed",
"created_by": "",
"created_at": 1695312007,
"indexing_at": 1695312007,
"completed_at": 1695312007,
"error": null,
"stopped_at": null
}],
"total": 1,
"total_pages": 1,
"page": 1,
"limit": 20
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}'
method='DELETE'
title='Delete Child Chunk'
name='#delete_child_chunk'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
Segment ID
</Property>
<Property name='child_chunk_id' type='string' key='child_chunk_id'>
Child Chunk ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"result": "success"
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}'
method='PATCH'
title='Update Child Chunk'
name='#update_child_chunk'
/>
<Row>
<Col>
### Params
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
Segment ID
</Property>
<Property name='child_chunk_id' type='string' key='child_chunk_id'>
Child Chunk ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='content' type='string' key='content'>
Child chunk content
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="PATCH"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}"
targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"content": "Updated child chunk content"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
"content": "Updated child chunk content"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": {
"id": "",
"segment_id": "",
"content": "Updated child chunk content",
"word_count": 25,
"tokens": 0,
"index_node_id": "",
"index_node_hash": "",
"status": "completed",
"created_by": "",
"created_at": 1695312007,
"indexing_at": 1695312007,
"completed_at": 1695312007,
"error": null,
"stopped_at": null
}
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/upload-file'
method='GET'
@@ -1694,4 +1974,4 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</tr>
</tbody>
</table>
<div className="pb-4" />
<div className="pb-4" />
@@ -961,6 +961,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Property name='status' type='string' key='status'>
搜索状态,completed
</Property>
<Property name='page' type='string' key='page'>
页码,可选
</Property>
<Property name='limit' type='string' key='limit'>
返回条数,可选,默认 20,范围 1-100
</Property>
</Properties>
</Col>
<Col sticky>
@@ -1004,7 +1010,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"error": null,
"stopped_at": null
}],
"doc_form": "text_model"
"doc_form": "text_model",
"has_more": false,
"limit": 20,
"total": 9,
"page": 1
}
```
</CodeGroup>
@@ -1149,6 +1159,310 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks'
method='POST'
title='新增文档子分段'
name='#create_child_chunk'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
分段 ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='content' type='string' key='content'>
子分段内容
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"content": "子分段内容"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
"content": "子分段内容"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": {
"id": "",
"segment_id": "",
"content": "子分段内容",
"word_count": 25,
"tokens": 0,
"index_node_id": "",
"index_node_hash": "",
"status": "completed",
"created_by": "",
"created_at": 1695312007,
"indexing_at": 1695312007,
"completed_at": 1695312007,
"error": null,
"stopped_at": null
}
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks'
method='GET'
title='查询文档子分段'
name='#get_child_chunks'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
分段 ID
</Property>
</Properties>
### Query
<Properties>
<Property name='keyword' type='string' key='keyword'>
搜索关键词(选填)
</Property>
<Property name='page' type='integer' key='page'>
页码(选填,默认1
</Property>
<Property name='limit' type='integer' key='limit'>
每页数量(选填,默认20,最大100)
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks?page=1&limit=20' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks?page=1&limit=20' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": [{
"id": "",
"segment_id": "",
"content": "子分段内容",
"word_count": 25,
"tokens": 0,
"index_node_id": "",
"index_node_hash": "",
"status": "completed",
"created_by": "",
"created_at": 1695312007,
"indexing_at": 1695312007,
"completed_at": 1695312007,
"error": null,
"stopped_at": null
}],
"total": 1,
"total_pages": 1,
"page": 1,
"limit": 20
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}'
method='DELETE'
title='删除文档子分段'
name='#delete_child_chunk'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
分段 ID
</Property>
<Property name='child_chunk_id' type='string' key='child_chunk_id'>
子分段 ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"result": "success"
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Row>
<Col>
### 错误信息
<Properties>
<Property name='code' type='string' key='code'>
返回的错误代码
</Property>
</Properties>
<Properties>
<Property name='status' type='number' key='status'>
返回的错误状态
</Property>
</Properties>
<Properties>
<Property name='message' type='string' key='message'>
返回的错误信息
</Property>
</Properties>
</Col>
<Col>
<CodeGroup title="Example">
```json {{ title: 'Response' }}
{
"code": "no_file_uploaded",
"message": "Please upload your file.",
"status": 400
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}'
method='PATCH'
title='更新文档子分段'
name='#update_child_chunk'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
分段 ID
</Property>
<Property name='child_chunk_id' type='string' key='child_chunk_id'>
子分段 ID
</Property>
</Properties>
### Request Body
<Properties>
<Property name='content' type='string' key='content'>
子分段内容
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="PATCH"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}"
targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"content": "更新的子分段内容"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request PATCH '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{
"content": "更新的子分段内容"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": {
"id": "",
"segment_id": "",
"content": "更新的子分段内容",
"word_count": 25,
"tokens": 0,
"index_node_id": "",
"index_node_hash": "",
"status": "completed",
"created_by": "",
"created_at": 1695312007,
"indexing_at": 1695312007,
"completed_at": 1695312007,
"error": null,
"stopped_at": null
}
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/upload-file'
method='GET'
+3 -3
View File
@@ -212,7 +212,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
<div className='flex w-full'>
<div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
</div>
)
}
@@ -234,7 +234,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
/>
<div className='flex w-full grow flex-col items-start justify-center'>
<div className='system-md-semibold w-full truncate text-text-secondary'>{appDetail.name}</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
</div>
</div>
{/* description */}
@@ -242,7 +242,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
<div className='system-xs-regular text-text-tertiary'>{appDetail.description}</div>
)}
{/* operations */}
<div className='flex items-center gap-1 self-stretch'>
<div className='flex flex-wrap items-center gap-1 self-stretch'>
<Button
size={'small'}
variant={'secondary'}
@@ -651,7 +651,13 @@ const Configuration: FC = () => {
syncToPublishedConfig(config)
setPublishedConfig(config)
const retrievalConfig = getMultipleRetrievalConfig(modelConfig.dataset_configs, datasets, datasets, {
const retrievalConfig = getMultipleRetrievalConfig({
...modelConfig.dataset_configs,
reranking_model: modelConfig.dataset_configs.reranking_model && {
provider: modelConfig.dataset_configs.reranking_model.reranking_provider_name,
model: modelConfig.dataset_configs.reranking_model.reranking_model_name,
},
}, datasets, datasets, {
provider: currentRerankProvider?.provider,
model: currentRerankModel?.model,
})
@@ -661,8 +667,8 @@ const Configuration: FC = () => {
...retrievalConfig,
...(retrievalConfig.reranking_model ? {
reranking_model: {
...retrievalConfig.reranking_model,
reranking_provider_name: correctModelProvider(modelConfig.dataset_configs.reranking_model.reranking_provider_name),
reranking_model_name: retrievalConfig.reranking_model.model,
reranking_provider_name: correctModelProvider(retrievalConfig.reranking_model.provider),
},
} : {}),
})
@@ -313,7 +313,7 @@ function AppPreview({ mode }: { mode: AppMode }) {
'chat': {
title: t('app.types.chatbot'),
description: t('app.newApp.chatbotUserDescription'),
link: 'https://docs.dify.ai/guides/application-orchestrate/conversation-application?fallback=true',
link: 'https://docs.dify.ai/guides/application-orchestrate#application_type',
},
'advanced-chat': {
title: t('app.types.advanced'),
@@ -78,7 +78,7 @@ const Sidebar = ({ isPanel }: Props) => {
return (
<div className={cn(
'flex grow flex-col',
'flex w-full grow flex-col',
isPanel && 'rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-bg shadow-lg',
)}>
<div className={cn(
@@ -81,13 +81,13 @@ const Operation: FC<OperationProps> = ({
const operationWidth = useMemo(() => {
let width = 0
if (!isOpeningStatement)
width += 28
width += 26
if (!isOpeningStatement && showPromptLog)
width += 102 + 8
width += 28 + 8
if (!isOpeningStatement && config?.text_to_speech?.enabled)
width += 33
width += 26
if (!isOpeningStatement && config?.supportAnnotation && config?.annotation_reply?.enabled)
width += 56 + 8
width += 26
if (config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement)
width += 60 + 8
if (config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement)
@@ -140,7 +140,7 @@ const Operation: FC<OperationProps> = ({
)}
</div>
)}
{!isOpeningStatement && config?.supportFeedback && onFeedback && (
{!isOpeningStatement && config?.supportFeedback && !localFeedback?.rating && onFeedback && (
<div className='ml-1 hidden items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex'>
{!localFeedback?.rating && (
<>
@@ -152,6 +152,10 @@ const Operation: FC<OperationProps> = ({
</ActionButton>
</>
)}
</div>
)}
{!isOpeningStatement && config?.supportFeedback && localFeedback?.rating && onFeedback && (
<div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
{localFeedback?.rating === 'like' && (
<ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
<RiThumbUpLine className='h-4 w-4' />
+2 -1
View File
@@ -13,8 +13,9 @@ async function decodeBase64AndDecompress(base64String: string) {
async function getProcessedInputsFromUrlParams(): Promise<Record<string, any>> {
const urlParams = new URLSearchParams(window.location.search)
const inputs: Record<string, any> = {}
const entriesArray = Array.from(urlParams.entries())
await Promise.all(
urlParams.entries().map(async ([key, value]) => {
entriesArray.map(async ([key, value]) => {
inputs[key] = await decodeBase64AndDecompress(decodeURIComponent(value))
}),
)
+3 -2
View File
@@ -1,9 +1,10 @@
import path from 'node:path'
import { access, appendFile, mkdir, open, readdir, rm, writeFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { parseXml } from '@rgrove/parse-xml'
import { camelCase, template } from 'lodash-es'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const generateDir = async (currentPath) => {
try {
@@ -105,7 +106,7 @@ const generateImageComponent = async (entry, pathList) => {
}
`.trim())
await writeFile(path.resolve(currentPath, `${fileName}.module.css`), `${componentCSSRender({ assetPath: path.join('~@/app/components/base/icons/assets', ...pathList.slice(2), entry) })}\n`)
await writeFile(path.resolve(currentPath, `${fileName}.module.css`), `${componentCSSRender({ assetPath: path.posix.join('~@/app/components/base/icons/assets', ...pathList.slice(2), entry) })}\n`)
const componentRender = template(`
// GENERATE BY script
+21 -19
View File
@@ -27,6 +27,7 @@ import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
import { Theme } from '@/types/app'
import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames'
import SVGRenderer from './svg-gallery'
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
const capitalizationLanguageNameMap: Record<string, string> = {
@@ -65,12 +66,22 @@ const preprocessLaTeX = (content: string) => {
if (typeof content !== 'string')
return content
return flow([
const codeBlockRegex = /```[\s\S]*?```/g
const codeBlocks = content.match(codeBlockRegex) || []
let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER')
processedContent = flow([
(str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\[(.*?)\\\]/gs, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
])(content)
])(processedContent)
codeBlocks.forEach((block) => {
processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block)
})
return processedContent
}
const preprocessThinkTag = (content: string) => {
@@ -136,14 +147,13 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }: any) =>
</div>
)
}
// Attention: SVGRenderer has xss vulnerability
// else if (language === 'svg' && isSVG) {
// return (
// <ErrorBoundary>
// <SVGRenderer content={content} />
// </ErrorBoundary>
// )
// }
else if (language === 'svg' && isSVG) {
return (
<ErrorBoundary>
<SVGRenderer content={content} />
</ErrorBoundary>
)
}
else {
return (
<SyntaxHighlighter
@@ -240,19 +250,11 @@ const Link = ({ node, ...props }: any) => {
}
}
function escapeSVGTags(htmlString: string): string {
return htmlString.replace(/(<svg[\s\S]*?>)([\s\S]*?)(<\/svg>)/gi, (match: string, openTag: string, innerContent: string, closeTag: string): string => {
return openTag.replace(/</g, '&lt;').replace(/>/g, '&gt;')
+ innerContent.replace(/</g, '&lt;').replace(/>/g, '&gt;')
+ closeTag.replace(/</g, '&lt;').replace(/>/g, '&gt;')
})
}
export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) {
const latexContent = flow([
preprocessThinkTag,
preprocessLaTeX,
])(escapeSVGTags(props.content))
])(props.content)
return (
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
+3 -3
View File
@@ -9,7 +9,7 @@ export type HtmlContentProps = {
type IPopover = {
className?: string
htmlContent: React.ReactNode<HtmlContentProps>
htmlContent: React.ReactNode
popupClassName?: string
trigger?: 'click' | 'hover'
position?: 'bottom' | 'br' | 'bl'
@@ -90,7 +90,7 @@ export default function CustomPopover({
>
{({ close }) => (
<div
className={cn('w-fit min-w-[130px] overflow-hidden rounded-lg bg-components-panel-bg shadow-lg ring-1 ring-black ring-opacity-5', popupClassName)}
className={cn('w-fit min-w-[130px] overflow-hidden rounded-lg bg-components-panel-bg shadow-lg ring-1 ring-black/5', popupClassName)}
{...(trigger !== 'hover'
? {}
: {
@@ -99,7 +99,7 @@ export default function CustomPopover({
})
}
>
{cloneElement(htmlContent as React.ReactNode<HtmlContentProps>, {
{cloneElement(htmlContent as React.ReactElement, {
onClose: () => onMouseLeave(open),
...(manualClose
? {
+36 -44
View File
@@ -1,7 +1,7 @@
'use client'
import type { FC } from 'react'
import React, { Fragment, useEffect, useState } from 'react'
import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
import React, { useEffect, useState } from 'react'
import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid'
import Badge from '../badge/index'
import { RiCheckLine } from '@remixicon/react'
@@ -238,48 +238,40 @@ const SimpleSelect: FC<ISelectProps> = ({
)}
{!disabled && (
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<ListboxOption
key={item.value}
className={
classNames(
'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary',
optionClassName,
)
}
value={item}
disabled={disabled}
>
{({ /* active, */ selected }) => (
<>
{renderOption
? renderOption({ item, selected })
: (<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{selected && !hideChecked && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
)}
>
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</Transition>
<ListboxOptions className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur backdrop-blur-sm py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<ListboxOption
key={item.value}
className={
classNames(
'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary',
optionClassName,
)
}
value={item}
disabled={disabled}
>
{({ /* active, */ selected }) => (
<>
{renderOption
? renderOption({ item, selected })
: (<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{selected && !hideChecked && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
)}
>
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
)}
</div>
</Listbox>
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import { SVG } from '@svgdotjs/svg.js'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
import DOMPurify from 'dompurify'
export const SVGRenderer = ({ content }: { content: string }) => {
const svgRef = useRef<HTMLDivElement>(null)
@@ -44,7 +45,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
const rootElement = draw.svg(content)
const rootElement = draw.svg(DOMPurify.sanitize(content))
rootElement.click(() => {
setImagePreview(svgToDataURL(svgElement as Element))
@@ -103,13 +103,16 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
manualClose
trigger='click'
htmlContent={
<button
type='button'
className='system-md-regular flex w-full items-center rounded-lg px-2 py-1.5 text-text-secondary'
onClick={showBatchModal}
>
{t('datasetDocuments.list.action.batchAdd')}
</button>
// need to wrapper the button with div when manualClose is true
<div className='w-full p-1'>
<button
type='button'
className='system-md-regular flex w-full items-center rounded-lg px-2 py-1.5 text-text-secondary'
onClick={showBatchModal}
>
{t('datasetDocuments.list.action.batchAdd')}
</button>
</div>
}
btnElement={
<div className='flex items-center justify-center' >
@@ -143,6 +143,7 @@ export const MarketplaceContextProvider = ({
resetPlugins,
queryPlugins,
queryPluginsWithDebounced,
cancelQueryPluginsWithDebounced,
isLoading: isPluginsLoading,
} = useMarketplacePlugins()
@@ -209,12 +210,13 @@ export const MarketplaceContextProvider = ({
const handleQuery = useCallback((debounced?: boolean) => {
if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) {
cancelQueryPluginsWithDebounced()
handleQueryMarketplaceCollectionsAndPlugins()
return
}
handleQueryPlugins(debounced)
}, [handleQueryMarketplaceCollectionsAndPlugins, handleQueryPlugins])
}, [handleQueryMarketplaceCollectionsAndPlugins, handleQueryPlugins, cancelQueryPluginsWithDebounced])
const handleSearchPluginTextChange = useCallback((text: string) => {
setSearchPluginText(text)
@@ -89,7 +89,7 @@ export const useMarketplacePlugins = () => {
handleUpdatePlugins(pluginsSearchParams)
}, [handleUpdatePlugins])
const { run: queryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams: PluginsSearchParams) => {
const { run: queryPluginsWithDebounced, cancel: cancelQueryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams: PluginsSearchParams) => {
handleUpdatePlugins(pluginsSearchParams)
}, {
wait: 500,
@@ -101,6 +101,7 @@ export const useMarketplacePlugins = () => {
resetPlugins,
queryPlugins,
queryPluginsWithDebounced,
cancelQueryPluginsWithDebounced,
isLoading: isPending,
}
}
+4
View File
@@ -556,6 +556,10 @@ export const FILE_STRUCT: Var[] = [
variable: 'url',
type: VarType.string,
},
{
variable: 'related_id',
type: VarType.string,
},
]
export const DEFAULT_FILE_UPLOAD_SETTING = {
@@ -0,0 +1,53 @@
import type { FC } from 'react'
import { createContext, useCallback, useEffect, useRef } from 'react'
import { createDatasetsDetailStore } from './store'
import type { CommonNodeType, Node } from '../types'
import { BlockEnum } from '../types'
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
import { fetchDatasets } from '@/service/datasets'
type DatasetsDetailStoreApi = ReturnType<typeof createDatasetsDetailStore>
type DatasetsDetailContextType = DatasetsDetailStoreApi | undefined
export const DatasetsDetailContext = createContext<DatasetsDetailContextType>(undefined)
type DatasetsDetailProviderProps = {
nodes: Node[]
children: React.ReactNode
}
const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({
nodes,
children,
}) => {
const storeRef = useRef<DatasetsDetailStoreApi>()
if (!storeRef.current)
storeRef.current = createDatasetsDetailStore()
const updateDatasetsDetail = useCallback(async (datasetIds: string[]) => {
const { data: datasetsDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } })
if (datasetsDetail && datasetsDetail.length > 0)
storeRef.current!.getState().updateDatasetsDetail(datasetsDetail)
}, [])
useEffect(() => {
if (!storeRef.current) return
const knowledgeRetrievalNodes = nodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
const allDatasetIds = knowledgeRetrievalNodes.reduce<string[]>((acc, node) => {
return Array.from(new Set([...acc, ...(node.data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids]))
}, [])
if (allDatasetIds.length === 0) return
updateDatasetsDetail(allDatasetIds)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<DatasetsDetailContext.Provider value={storeRef.current!}>
{children}
</DatasetsDetailContext.Provider>
)
}
export default DatasetsDetailProvider
@@ -0,0 +1,38 @@
import { useContext } from 'react'
import { createStore, useStore } from 'zustand'
import type { DataSet } from '@/models/datasets'
import { DatasetsDetailContext } from './provider'
import produce from 'immer'
type DatasetsDetailStore = {
datasetsDetail: Record<string, DataSet>
updateDatasetsDetail: (datasetsDetail: DataSet[]) => void
}
export const createDatasetsDetailStore = () => {
return createStore<DatasetsDetailStore>((set, get) => ({
datasetsDetail: {},
updateDatasetsDetail: (datasets: DataSet[]) => {
const oldDatasetsDetail = get().datasetsDetail
const datasetsDetail = datasets.reduce<Record<string, DataSet>>((acc, dataset) => {
acc[dataset.id] = dataset
return acc
}, {})
// Merge new datasets detail into old one
const newDatasetsDetail = produce(oldDatasetsDetail, (draft) => {
Object.entries(datasetsDetail).forEach(([key, value]) => {
draft[key] = value
})
})
set({ datasetsDetail: newDatasetsDetail })
},
}))
}
export const useDatasetsDetailStore = <T>(selector: (state: DatasetsDetailStore) => T): T => {
const store = useContext(DatasetsDetailContext)
if (!store)
throw new Error('Missing DatasetsDetailContext.Provider in the tree')
return useStore(store, selector)
}
+1 -1
View File
@@ -160,7 +160,7 @@ const Header: FC = () => {
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
if (handleCheckBeforePublish()) {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
title: params?.title || '',
releaseNotes: params?.releaseNotes || '',
@@ -1,10 +1,12 @@
import {
useCallback,
useMemo,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import type {
CommonNodeType,
Edge,
Node,
} from '../types'
@@ -27,6 +29,10 @@ import { useGetLanguage } from '@/context/i18n'
import type { AgentNodeType } from '../nodes/agent/types'
import { useStrategyProviders } from '@/service/use-strategy'
import { canFindTool } from '@/utils'
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
import type { DataSet } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation()
@@ -37,6 +43,24 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const { data: strategyProviders } = useStrategyProviders()
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
const getCheckData = useCallback((data: CommonNodeType<{}>) => {
let checkData = data
if (data.type === BlockEnum.KnowledgeRetrieval) {
const datasetIds = (data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids
const _datasets = datasetIds.reduce<DataSet[]>((acc, id) => {
if (datasetsDetail[id])
acc.push(datasetsDetail[id])
return acc
}, [])
checkData = {
...data,
_datasets,
} as CommonNodeType<KnowledgeRetrievalNodeType>
}
return checkData
}, [datasetsDetail])
const needWarningNodes = useMemo(() => {
const list = []
@@ -75,7 +99,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
if (node.type === CUSTOM_NODE) {
const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid)
const checkData = getCheckData(node.data)
const { errorMessage } = nodesExtraData[node.data.type].checkValid(checkData, t, moreDataForCheckValid)
if (errorMessage || !validNodes.find(n => n.id === node.id)) {
list.push({
@@ -109,7 +134,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
}
return list
}, [nodes, edges, isChatMode, buildInTools, customTools, workflowTools, language, nodesExtraData, t, strategyProviders])
}, [nodes, edges, isChatMode, buildInTools, customTools, workflowTools, language, nodesExtraData, t, strategyProviders, getCheckData])
return needWarningNodes
}
@@ -125,8 +150,31 @@ export const useChecklistBeforePublish = () => {
const store = useStoreApi()
const nodesExtraData = useNodesExtraData()
const { data: strategyProviders } = useStrategyProviders()
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
const updateTime = useRef(0)
const handleCheckBeforePublish = useCallback(() => {
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
let checkData = data
if (data.type === BlockEnum.KnowledgeRetrieval) {
const datasetIds = (data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids
const datasetsDetail = datasets.reduce<Record<string, DataSet>>((acc, dataset) => {
acc[dataset.id] = dataset
return acc
}, {})
const _datasets = datasetIds.reduce<DataSet[]>((acc, id) => {
if (datasetsDetail[id])
acc.push(datasetsDetail[id])
return acc
}, [])
checkData = {
...data,
_datasets,
} as CommonNodeType<KnowledgeRetrievalNodeType>
}
return checkData
}, [])
const handleCheckBeforePublish = useCallback(async () => {
const {
getNodes,
edges,
@@ -141,6 +189,24 @@ export const useChecklistBeforePublish = () => {
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
return false
}
// Before publish, we need to fetch datasets detail, in case of the settings of datasets have been changed
const knowledgeRetrievalNodes = nodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
const allDatasetIds = knowledgeRetrievalNodes.reduce<string[]>((acc, node) => {
return Array.from(new Set([...acc, ...(node.data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids]))
}, [])
let datasets: DataSet[] = []
if (allDatasetIds.length > 0) {
updateTime.current = updateTime.current + 1
const currUpdateTime = updateTime.current
const { data: datasetsDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: allDatasetIds } })
if (datasetsDetail && datasetsDetail.length > 0) {
// avoid old data to overwrite the new data
if (currUpdateTime < updateTime.current)
return false
datasets = datasetsDetail
updateDatasetsDetail(datasetsDetail)
}
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
@@ -161,7 +227,8 @@ export const useChecklistBeforePublish = () => {
}
}
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(node.data, t, moreDataForCheckValid)
const checkData = getCheckData(node.data, datasets)
const { errorMessage } = nodesExtraData[node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
if (errorMessage) {
notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` })
@@ -185,7 +252,7 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders])
}, [store, isChatMode, notify, t, buildInTools, customTools, workflowTools, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData])
return {
handleCheckBeforePublish,
+8 -5
View File
@@ -99,6 +99,7 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import Confirm from '@/app/components/base/confirm'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { fetchFileUploadConfig } from '@/service/common'
import DatasetsDetailProvider from './datasets-detail-store/provider'
const nodeTypes = {
[CUSTOM_NODE]: CustomNode,
@@ -448,11 +449,13 @@ const WorkflowWrap = memo(() => {
nodes={nodesData}
edges={edgesData} >
<FeaturesProvider features={initialFeatures}>
<Workflow
nodes={nodesData}
edges={edgesData}
viewport={data?.graph.viewport}
/>
<DatasetsDetailProvider nodes={nodesData}>
<Workflow
nodes={nodesData}
edges={edgesData}
viewport={data?.graph.viewport}
/>
</DatasetsDetailProvider>
</FeaturesProvider>
</WorkflowHistoryProvider>
</ReactFlowProvider>
@@ -100,5 +100,5 @@ export const TRANSFER_METHOD = [
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method', 'related_id']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')
@@ -32,8 +32,8 @@ const ConditionVarSelector = ({
crossAxis: 0,
}}
>
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
<div className="cursor-pointer">
<PortalToFollowElemTrigger asChild onClick={() => onOpenChange(!open)}>
<div className="w-full cursor-pointer">
<VariableTag
valueSelector={valueSelector}
varType={varType}
@@ -78,11 +78,11 @@ const ConditionValue = ({
<div className='flex h-6 items-center rounded-md bg-workflow-block-parma-bg px-1'>
{!isEnvVar && !isChatVar && <Variable02 className={cn('mr-1 h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
{isEnvVar && <Env className='mr-1 h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
<div
className={cn(
'ml-0.5 shrink-0 truncate text-xs font-medium text-text-accent',
'ml-0.5 shrink-[2] truncate text-xs font-medium text-text-accent',
!notHasValue && 'max-w-[70px]',
isException && 'text-text-warning',
)}
@@ -98,7 +98,7 @@ const ConditionValue = ({
</div>
{
!notHasValue && (
<div className='truncate text-xs text-text-secondary' title={formatValue}>{isSelect ? selectName : formatValue}</div>
<div className='shrink-[3] truncate text-xs text-text-secondary' title={formatValue}>{isSelect ? selectName : formatValue}</div>
)
}
</div>
@@ -1,33 +1,30 @@
import { type FC, useEffect, useRef, useState } from 'react'
import { type FC, useEffect, useState } from 'react'
import React from 'react'
import type { KnowledgeRetrievalNodeType } from './types'
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
import type { NodeProps } from '@/app/components/workflow/types'
import { fetchDatasets } from '@/service/datasets'
import type { DataSet } from '@/models/datasets'
import { useDatasetsDetailStore } from '../../datasets-detail-store/store'
const Node: FC<NodeProps<KnowledgeRetrievalNodeType>> = ({
data,
}) => {
const [selectedDatasets, setSelectedDatasets] = useState<DataSet[]>([])
const updateTime = useRef(0)
useEffect(() => {
(async () => {
updateTime.current = updateTime.current + 1
const currUpdateTime = updateTime.current
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
if (data.dataset_ids?.length > 0) {
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: data.dataset_ids } })
// avoid old data overwrite new data
if (currUpdateTime < updateTime.current)
return
setSelectedDatasets(dataSetsWithDetail)
}
else {
setSelectedDatasets([])
}
})()
}, [data.dataset_ids])
useEffect(() => {
if (data.dataset_ids?.length > 0) {
const dataSetsWithDetail = data.dataset_ids.reduce<DataSet[]>((acc, id) => {
if (datasetsDetail[id])
acc.push(datasetsDetail[id])
return acc
}, [])
setSelectedDatasets(dataSetsWithDetail)
}
else {
setSelectedDatasets([])
}
}, [data.dataset_ids, datasetsDetail])
if (!selectedDatasets.length)
return null
@@ -10,6 +10,7 @@ import type {
DataSet,
MetadataInDoc,
RerankingModeEnum,
WeightedScoreEnum,
} from '@/models/datasets'
export type MultipleRetrievalConfig = {
@@ -21,6 +22,7 @@ export type MultipleRetrievalConfig = {
}
reranking_mode?: RerankingModeEnum
weights?: {
weight_type: WeightedScoreEnum
vector_setting: {
vector_weight: number
embedding_provider_name: string
@@ -41,6 +41,7 @@ import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-s
import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { useDatasetsDetailStore } from '../../datasets-detail-store/store'
const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@@ -49,6 +50,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
const startNode = getBeforeNodesInSameBranch(id).find(node => node.data.type === BlockEnum.Start)
const startNodeId = startNode?.id
const { inputs, setInputs: doSetInputs } = useNodeCrud<KnowledgeRetrievalNodeType>(id, payload)
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
const inputRef = useRef(inputs)
@@ -218,15 +220,12 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
(async () => {
const inputs = inputRef.current
const datasetIds = inputs.dataset_ids
let _datasets = selectedDatasets
if (datasetIds?.length > 0) {
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } as any })
_datasets = dataSetsWithDetail
setSelectedDatasets(dataSetsWithDetail)
}
const newInputs = produce(inputs, (draft) => {
draft.dataset_ids = datasetIds
draft._datasets = _datasets
})
setInputs(newInputs)
setSelectedDatasetsLoaded(true)
@@ -256,7 +255,6 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
} = getSelectedDatasetsMode(newDatasets)
const newInputs = produce(inputs, (draft) => {
draft.dataset_ids = newDatasets.map(d => d.id)
draft._datasets = newDatasets
if (payload.retrieval_mode === RETRIEVE_TYPE.multiWay && newDatasets.length > 0) {
const multipleRetrievalConfig = draft.multiple_retrieval_config
@@ -266,6 +264,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
})
}
})
updateDatasetsDetail(newDatasets)
setInputs(newInputs)
setSelectedDatasets(newDatasets)
@@ -275,7 +274,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
|| allExternal
)
setRerankModelOpen(true)
}, [inputs, setInputs, payload.retrieval_mode, selectedDatasets, currentRerankModel, currentRerankProvider])
}, [inputs, setInputs, payload.retrieval_mode, selectedDatasets, currentRerankModel, currentRerankProvider, updateDatasetsDetail])
const filterVar = useCallback((varPayload: Var) => {
return varPayload.type === VarType.string
@@ -83,7 +83,7 @@ export const TRANSFER_METHOD = [
{ value: TransferMethod.remote_url, i18nKey: 'url' },
]
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method', 'related_id']
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')
export default nodeDefault
@@ -22,16 +22,33 @@ const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
},
checkValid(payload: VariableAssignerNodeType, t: any) {
let errorMessages = ''
const { variables } = payload
if (!variables || variables.length === 0)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.variableAssigner.title`) })
if (!errorMessages) {
const { variables, advanced_settings } = payload
const { group_enabled = false, groups = [] } = advanced_settings || {}
// enable group
const validateVariables = (variables: any[], field: string) => {
variables.forEach((variable) => {
if (!variable || variable.length === 0)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.errorMsg.fields.variableValue`) })
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(field) })
})
}
if (group_enabled) {
if (!groups || groups.length === 0) {
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.variableAssigner.title`) })
}
else if (!errorMessages) {
groups.forEach((group) => {
validateVariables(group.variables || [], `${i18nPrefix}.errorMsg.fields.variableValue`)
})
}
}
else {
if (!variables || variables.length === 0)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.variableAssigner.title`) })
else if (!errorMessages)
validateVariables(variables, `${i18nPrefix}.errorMsg.fields.variableValue`)
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
+1
View File
@@ -339,6 +339,7 @@ export type RunFile = {
transfer_method: TransferMethod[]
url?: string
upload_file_id?: string
related_id?: string
}
export type WorkflowRunningData = {
+1
View File
@@ -70,6 +70,7 @@ const translation = {
noHitHistory: 'ヒット履歴はありません',
},
hitHistoryTable: {
question: '質問',
query: 'クエリ',
match: '一致',
response: '応答',
+1 -2
View File
@@ -26,10 +26,9 @@ const translation = {
appDeleteFailed: 'アプリの削除に失敗しました',
join: 'コミュニティに参加する',
communityIntro:
'さまざまなチャンネルでチームメンバーや貢献者、開発者と議論します。',
'さまざまなチャンネルでチームメンバーや貢献者、開発者と議論します。',
roadmap: 'ロードマップを見る',
newApp: {
// this comment is to recreate PR
startFromBlank: '最初から作成',
startFromTemplate: 'テンプレートから作成',
captionAppType: 'どのタイプのアプリを作成しますか?',
+6
View File
@@ -101,18 +101,22 @@ const translation = {
plans: {
sandbox: {
name: 'Sandbox',
for: '核心能力的免费试用',
description: 'コア機能を無料で試す',
},
professional: {
name: 'Professional',
for: '核心能力的免费试用',
description: '独立した開発者/小規模チーム向け',
},
team: {
name: 'Team',
for: '核心能力的免费试用',
description: '中規模チーム向け',
},
community: {
name: 'Community',
for: '核心能力的免费试用',
description: '個人ユーザー、小規模チーム、または非営利プロジェクト向け',
price: '無料',
btnText: 'コミュニティを始めましょう',
@@ -125,6 +129,7 @@ const translation = {
},
premium: {
name: 'Premium',
for: '核心能力的免费试用',
description: '中規模の組織やチーム向け',
price: 'スケーラブル',
priceTip: 'クラウドマーケットプレイスに基づく',
@@ -140,6 +145,7 @@ const translation = {
},
enterprise: {
name: 'Enterprise',
for: '核心能力的免费试用',
description: 'エンタープライズグレードのセキュリティ、コンプライアンス、拡張性、制御、およびより高度な機能を必要とする企業向け',
price: 'カスタム',
priceTip: '年間契約のみ',
+12
View File
@@ -26,6 +26,8 @@ const translation = {
lineBreak: '改行',
sure: '確認済み',
download: 'ダウンロード',
downloadSuccess: '下载完毕',
downloadFailed: '下载失败,请稍后重试。',
delete: '削除',
settings: '設定',
setup: 'セットアップ',
@@ -152,12 +154,22 @@ const translation = {
workspace: 'ワークスペース',
createWorkspace: 'ワークスペースを作成',
helpCenter: 'ヘルプ',
support: '支持',
compliance: '合规',
communityFeedback: 'フィードバック',
roadmap: 'ロードマップ',
community: 'コミュニティ',
about: 'Difyについて',
logout: 'ログアウト',
},
compliance: {
soc2Type1: 'SOC 2 Type I Report',
soc2Type2: 'SOC 2 Type II Report',
iso27001: 'ISO 27001:2022 Certification',
gdpr: 'GDPR DPA',
sandboxUpgradeTooltip: '仅适用于 Professional 或 Team 版计划。',
professionalUpgradeTooltip: '仅适用于 Team 版计划或以上。',
},
settings: {
accountGroup: 'アカウント',
workplaceGroup: 'ワークスペース',
+2
View File
@@ -1,6 +1,8 @@
const translation = {
custom: 'カスタマイズ',
upgradeTip: {
title: '升级您的计划',
des: '升级您的计划来定制您的品牌。',
prefix: 'プランをアップグレードして',
suffix: 'ブランドをカスタマイズしましょう。',
},
+25 -25
View File
@@ -1,16 +1,14 @@
const translation = {
steps: {
header: {
creation: 'ナレッジの作成',
update: 'データの追加',
fallbackRoute: '知識',
fallbackRoute: 'ナレッジベース',
},
one: 'データソース',
two: 'テキスト進行中',
three: '実行と完成',
},
error: {
unavailable: 'このナレッジは利用できません',
unavailable: 'このナレッジベースは利用できません',
},
firecrawl: {
configFirecrawl: '🔥Firecrawlの設定',
@@ -50,11 +48,11 @@ const translation = {
connect: '接続する',
cancel: 'キャンセル',
button: '次へ',
emptyDatasetCreation: '空のナレッジを作成します',
emptyDatasetCreation: '空のナレッジベースを作成します',
modal: {
title: '空のナレッジを作成',
tip: '空のナレッジにはドキュメントが含まれず、いつでもドキュメントをアップロードできます。',
input: 'ナレッジ',
title: '空のナレッジベースを作成',
tip: '空のナレッジベースにはドキュメントが含まれず、いつでもドキュメントをアップロードできます。',
input: 'ナレッジベースの名称',
placeholder: '入力してください',
nameNotEmpty: '名前は空にできません',
nameLengthInvalid: '名前は1〜40文字である必要があります',
@@ -63,13 +61,23 @@ const translation = {
failed: '作成に失敗しました',
},
website: {
chooseProvider: 'プロバイダーを選択する',
fireCrawlNotConfigured: 'Firecrawlが設定されていません',
fireCrawlNotConfiguredDescription: 'Firecrawl を使用するには、Firecrawl の API キーを設定してください。',
jinaReaderNotConfigured: 'Jina Reader が設定されていません',
jinaReaderNotConfiguredDescription: '無料のAPIキーを入力して、Jina Readerを設定します。',
configure: '設定',
configureFirecrawl: '配置 Firecrawl',
configureJinaReader: '配置 Jina Reader',
run: '実行',
firecrawlTitle: '🔥Firecrawlを使っでウエブコンテンツを抽出',
firecrawlDoc: 'Firecrawlドキュメント',
firecrawlDocLink: 'https://docs.dify.ai/guides/knowledge-base/sync-from-website',
jinaReaderTitle: 'サイト全体をMarkdownに変換する',
jinaReaderDoc: 'Jina Readerの詳細',
jinaReaderDocLink: 'https://jina.ai/reader',
useSitemap: 'sitemap(サイトマップ)を使用する',
useSitemapTooltip: 'サイトマップに沿ってサイトをクロールします。そうでない場合、Jina Readerはページの関連性に基づいて繰り返しクロールし、ページ数は少なくなりますが、高品質のページが得られます。',
options: 'オプション',
crawlSubPage: 'サブページをクロールする',
limit: '制限',
@@ -85,14 +93,6 @@ const translation = {
scrapTimeInfo: '{{time}} 秒以内に合計 {{total}} ページをスクレイピングしました',
preview: 'プレビュー',
maxDepthTooltip: '入力されたURLを基にしたクローリング作業での設定可能な最大深度について説明します。深度0は入力されたURL自体のページを対象としたスクレイピングを意味します。深度1では、元のURLの直下にあるページ(URLに続く最初の"/"以降の内容)もスクレイピングの対象になります。この深度は指定した数値まで増加させることができ、それに応じてスクレイピングの範囲も広がっていきます。',
jinaReaderDocLink: 'https://jina.ai/reader',
useSitemap: 'サイトマップを使用する',
jinaReaderNotConfigured: 'Jina Reader が設定されていません',
jinaReaderDoc: 'Jina Readerの詳細',
jinaReaderTitle: 'サイト全体をMarkdownに変換する',
chooseProvider: 'プロバイダーを選択する',
jinaReaderNotConfiguredDescription: '無料のAPIキーを入力してJina Readerを設定します。',
useSitemapTooltip: 'サイトマップに沿ってサイトをクロールします。そうでない場合、Jina Readerはページの関連性に基づいて繰り返しクロールし、ページ数は少なくなりますが、高品質のページが得られます。',
},
},
stepTwo: {
@@ -112,7 +112,7 @@ const translation = {
fullDoc: '全文',
fullDocTip: 'ドキュメント全体を親チャンクとして使用し、直接検索します。パフォーマンス上の理由から、10000トークンを超えるテキストは自動的に切り捨てられます。',
separator: 'チャンク識別子',
separatorPlaceholder: '例えば改行(\\\\n)や特殊なセパレータ(例:「***」)',
separatorPlaceholder: '例えば改行(\\n\\n)や特殊なセパレータ(例:「***」)',
maxLength: '最大チャンク長',
overlap: 'チャンクのオーバーラップ',
overlapTip: 'チャンクのオーバーラップを設定することで、それらの間の意味的な関連性を維持し、検索効果を向上させることができます。最大チャンクサイズの10%〜25%を設定することをおすすめします。',
@@ -155,7 +155,7 @@ const translation = {
sideTipTitle: 'なぜチャンクと前処理が必要なのか',
sideTipP1: 'テキストデータを処理する際、チャンクとクリーニングは2つの重要な前処理ステップです。',
sideTipP2: 'セグメンテーションは長いテキストを段落に分割し、モデルがより理解しやすくします。これにより、モデルの結果の品質と関連性が向上します。',
sideTipP3: 'クリーニングは不要な文字や書式を削除し、ナレッジをよりクリーンで解析しやすいものにします。',
sideTipP3: 'クリーニングは不要な文字や書式を削除し、ナレッジベースをよりクリーンで解析しやすいものにします。',
sideTipP4: '適切なチャンクとクリーニングはモデルのパフォーマンスを向上させ、より正確で価値のある結果を提供します。',
previewTitle: 'プレビュー',
previewTitleButton: 'プレビュー',
@@ -165,7 +165,7 @@ const translation = {
characters: '文字',
indexSettingTip: 'インデックス方法を変更するには、',
retrievalSettingTip: '検索方法を変更するには、',
datasetSettingLink: 'ナレッジ設定',
datasetSettingLink: 'ナレッジベース設定',
separatorTip: '区切り文字は、テキストを区切るために使用される文字です。\\n\\n と \\n は、段落と行を区切るために一般的に使用される区切り記号です。カンマ (\\n\\n,\\n) と組み合わせると、最大チャンク長を超えると、段落は行で区切られます。自分で定義した特別な区切り文字を使用することもできます(例:***)。',
maxLengthCheck: 'チャンクの最大長は {{limit}} 未満にする必要があります',
previewChunkTip: 'プレビューを読み込むには、左側の \'チャンクをプレビュー\' ボタンをクリックしてください',
@@ -179,17 +179,17 @@ const translation = {
parentChildChunkDelimiterTip: '区切り文字とは、テキストを分割するために使用される文字です。\\n は、親チャンクを小さな子チャンクに分割する際におすすめです。独自の区切り文字も使用できます。',
},
stepThree: {
creationTitle: '🎉 ナレッジが作成されました',
creationContent: 'ナレッジの名前は自動的に設定されましたが、いつでも変更できます。',
label: 'ナレッジ名',
creationTitle: '🎉 ナレッジベースが作成されました',
creationContent: 'ナレッジベースの名前は自動的に設定されましたが、自由に変更できます。',
label: 'ナレッジベース名',
additionTitle: '🎉 ドキュメントがアップロードされました',
additionP1: 'ドキュメントはナレッジにアップロードされました',
additionP2: '、ナレッジのドキュメントリストで見つけることができます。',
additionP1: 'ドキュメントはナレッジベースにアップロードされました',
additionP2: '、ナレッジベースのドキュメントリストで見つけることができます。',
stop: '処理を停止',
resume: '処理を再開',
navTo: 'ドキュメントに移動',
sideTipTitle: '次は何ですか',
sideTipContent: 'ドキュメントのインデックスが完了したら、ナレッジをアプリケーションのコンテキストとして統合することができます。プロンプトオーケストレーションページでコンテキスト設定を見つけることができます。また、独立したChatGPTインデックスプラグインとしてリリースすることもできます。',
sideTipContent: 'ドキュメントのインデックスが完了したら、ナレッジベースをアプリケーションのコンテキストとして統合することができます。プロンプトオーケストレーションページでコンテキスト設定を見つけることができます。また、独立したChatGPTインデックスプラグインとしてリリースすることもできます。',
modelTitle: '埋め込みを停止してもよろしいですか?',
modelContent: '後で処理を再開する必要がある場合は、中断した場所から続行します。',
modelButtonConfirm: '確認',
+1 -1
View File
@@ -1,7 +1,7 @@
const translation = {
list: {
title: 'ドキュメント',
desc: 'ナレッジのすべてのファイルがここに表示され、ナレッジ全体がDifyの引用やチャットプラグインを介してリンクされるか、インデックス化されることができます。',
desc: 'すべてのファイルがここに表示され、ナレッジベース全体がDifyの引用やチャットプラグインを介してリンクされるか、インデックス化されることができます。',
learnMore: '詳細はこちら',
addFile: 'ファイルを追加',
addPages: 'ページを追加',
+11 -10
View File
@@ -1,15 +1,15 @@
const translation = {
title: 'ナレッジの設定',
desc: 'ここではナレッジのプロパティと動作方法を変更できます。',
title: 'ナレッジベースの設定',
desc: 'ここではナレッジベースのプロパティと動作方法を変更できます。',
form: {
name: 'ナレッジ名',
namePlaceholder: 'ナレッジ名を入力してください',
name: 'ナレッジベース名',
namePlaceholder: 'ナレッジベース名を入力してください',
nameError: '名前は空にできません',
desc: 'ナレッジの説明',
descInfo: 'ナレッジの内容を概説するための明確なテキストの説明を書いてください。この説明は、複数のナレッジから推論を選択する際の基準として使用されます。',
desc: 'ナレッジベースの説明',
descInfo: 'ナレッジベースの内容を概説するための明確なテキストの説明を書いてください。この説明は、複数のナレッジから推論を選択する際の基準として使用されます。',
descPlaceholder: 'このデータセットの内容を記述してください。詳細に記述することで、AIがデータセットの内容に迅速にアクセスできるようになります。空欄の場合、LangGeniusはデフォルトの検索方法を使用します。',
helpText: '適切なデータセットの説明を作成する方法を学びましょう。',
descWrite: '良いナレッジの説明の書き方を学ぶ。',
descWrite: '良いナレッジベースの説明の書き方を学ぶ。',
permissions: '権限',
permissionsOnlyMe: '自分のみ',
permissionsAllMember: 'すべてのチームメンバー',
@@ -26,14 +26,15 @@ const translation = {
embeddingModelTipLink: '設定',
retrievalSetting: {
title: '検索設定',
method: '検索方法',
learnMore: '詳細はこちら',
description: ' 検索方法についての詳細',
longDescription: ' 検索方法についての詳細については、いつでもナレッジの設定で変更できます。',
longDescription: ' 検索方法についての詳細については、いつでもナレッジベースの設定で変更できます。',
},
save: '保存',
externalKnowledgeID: '外部ナレッジID',
externalKnowledgeID: '外部ナレッジベースID',
retrievalSettings: '取得設定',
externalKnowledgeAPI: '外部ナレッジAPI',
externalKnowledgeAPI: '外部ナレッジベースAPI',
indexMethodChangeToEconomyDisabledTip: 'HQからECOへのダウングレードはできません。',
},
}
+82 -34
View File
@@ -1,7 +1,7 @@
const translation = {
knowledge: 'ナレッジ',
knowledge: 'ナレッジベース',
chunkingMode: {
general: '一般',
general: '汎用',
parentChild: '親子',
},
parentMode: {
@@ -10,33 +10,33 @@ const translation = {
},
externalTag: '外部',
externalAPI: '外部API',
externalAPIPanelTitle: '外部ナレッジ連携API',
externalKnowledgeId: '外部ナレッジID',
externalKnowledgeName: '外部ナレッジ名',
externalKnowledgeDescription: 'ナレッジの説明',
externalKnowledgeIdPlaceholder: 'ナレッジIDを入力',
externalAPIPanelTitle: '外部ナレッジベース連携API',
externalKnowledgeId: '外部ナレッジベースID',
externalKnowledgeName: '外部ナレッジベース名',
externalKnowledgeDescription: 'ナレッジベースの説明',
externalKnowledgeIdPlaceholder: 'ナレッジベースIDを入力',
externalKnowledgeNamePlaceholder: 'ナレッジベース名を入力',
externalKnowledgeDescriptionPlaceholder: 'このナレッジベースの説明(任意)',
learnHowToWriteGoodKnowledgeDescription: '効果的なナレッジの説明の書き方',
externalAPIPanelDescription: '外部ナレッジ連携APIは、Dify外のナレッジベースと連携し、そこからナレッジを取得するために使用します。',
externalAPIPanelDocumentation: '外部ナレッジ連携APIの作成方法',
learnHowToWriteGoodKnowledgeDescription: '効果的なナレッジベースの説明の書き方',
externalAPIPanelDescription: '外部ナレッジベース連携APIは、Dify外のナレッジベースと連携し、そこからナレッジベースを取得するために使用します。',
externalAPIPanelDocumentation: '外部ナレッジベース連携APIの作成方法',
localDocs: 'ローカルドキュメント',
documentCount: ' ドキュメント',
wordCount: ' k 単語',
appCount: ' リンクされたアプリ',
createDataset: 'ナレッジを作成',
createNewExternalAPI: '新しい外部ナレッジ連携APIを作成',
noExternalKnowledge: '外部ナレッジ連携APIがありません。ここをクリックして作成してください',
createExternalAPI: '外部ナレッジ連携APIを追加',
editExternalAPIFormTitle: '外部ナレッジ連携APIを編集',
editExternalAPITooltipTitle: '連携中のナレッジ',
createDataset: 'ナレッジベースを作成',
createNewExternalAPI: '新しい外部ナレッジベース連携APIを作成',
noExternalKnowledge: '外部ナレッジベース連携APIがありません。ここをクリックして作成してください',
createExternalAPI: '外部ナレッジベース連携APIを追加',
editExternalAPIFormTitle: '外部ナレッジベース連携APIを編集',
editExternalAPITooltipTitle: '連携中のナレッジベース',
editExternalAPIConfirmWarningContent: {
front: 'この外部ナレッジ連携APIは',
end: '件の外部ナレッジと連携しており、この変更はすべてに適用されます。変更を保存しますか?',
front: 'この外部ナレッジベース連携APIは',
end: '件の外部ナレッジベースと連携しており、この変更はすべてに適用されます。変更を保存しますか?',
},
editExternalAPIFormWarning: {
front: 'この外部APIは',
end: '件の外部ナレッジと連携しています',
end: '件の外部ナレッジベースと連携しています',
},
deleteExternalAPIConfirmWarningContent: {
title: {
@@ -44,13 +44,13 @@ const translation = {
end: 'しますか?',
},
content: {
front: 'この外部ナレッジ連携APIは',
end: '件の外部ナレッジと連携しています。このAPIを削除すると、すべて無効になります。このAPIを削除しますか?',
front: 'この外部ナレッジベース連携APIは',
end: '件の外部ナレッジベースと連携しています。このAPIを削除すると、すべて無効になります。このAPIを削除しますか?',
},
noConnectionContent: 'このAPIを削除しますか?',
},
selectExternalKnowledgeAPI: {
placeholder: '外部ナレッジ連携APIを選択',
placeholder: '外部ナレッジベース連携APIを選択',
},
connectDataset: '外部ナレッジベースと連携',
connectDatasetIntro: {
@@ -58,7 +58,7 @@ const translation = {
content: {
front: '外部ナレッジベースと連携するには、まず外部APIを作成する必要があります。以下の手順を参照し、',
link: '外部APIの作成方法',
end: 'をご確認ください。次に、対応するナレッジIDを左側のフォームに入力してください。すべての情報が正しければ、連携ボタンをクリックすると、自動的にナレッジベースの検索テストに移動します。',
end: 'をご確認ください。次に、対応するナレッジベースIDを左側のフォームに入力してください。すべての情報が正しければ、連携ボタンをクリックすると、自動的にナレッジベースの検索テストに移動します。',
},
learnMore: '詳細はこちら',
},
@@ -70,14 +70,14 @@ const translation = {
helper5: 'をよくお読みください。',
},
createDatasetIntro: '独自のテキストデータをインポートするか、LLMコンテキストの強化のためにWebhookを介してリアルタイムでデータを書き込むことができます。',
deleteDatasetConfirmTitle: 'このナレッジを削除しますか?',
deleteDatasetConfirmTitle: 'このナレッジベースを削除しますか?',
deleteDatasetConfirmContent:
'ナレッジを削除すると元に戻すことはできません。ユーザーはもはやあなた様のナレッジにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
datasetUsedByApp: 'このナレッジは一部のアプリによって使用されています。アプリはこのナレッジを使用できなくなり、すべてのプロンプト設定とログは永久に削除されます。',
datasetDeleted: 'ナレッジが削除されました',
datasetDeleteFailed: 'ナレッジの削除に失敗しました',
'ナレッジベースを削除すると元に戻すことはできません。ユーザーはもはやあなた様のナレッジベースにアクセスできず、すべてのプロンプトの設定とログが永久に削除されます。',
datasetUsedByApp: 'このナレッジベースは一部のアプリによって使用されています。アプリはこのナレッジベースを使用できなくなり、すべてのプロンプト設定とログは永久に削除されます。',
datasetDeleted: 'ナレッジベースが削除されました',
datasetDeleteFailed: 'ナレッジベースの削除に失敗しました',
didYouKnow: 'ご存知ですか?',
intro1: 'ナレッジはDifyアプリケーションに統合することができます',
intro1: 'ナレッジベースはDifyアプリケーションに統合することができます',
intro2: 'コンテキストとして',
intro3: '、',
intro4: 'または',
@@ -85,7 +85,7 @@ const translation = {
intro6: '単体のChatGPTインデックスプラグインとして公開するために',
unavailable: '利用不可',
unavailableTip: '埋め込みモデルが利用できません。デフォルトの埋め込みモデルを設定する必要があります',
datasets: 'ナレッジ',
datasets: 'ナレッジベース',
datasetsApi: 'API ACCESS',
externalKnowledgeForm: {
connect: '連携',
@@ -141,8 +141,8 @@ const translation = {
defaultRetrievalTip: 'デフォルトでは、マルチパス検索が使用されます。複数のナレッジベースから情報を取得した後、再ランキングを行います。',
mixtureHighQualityAndEconomicTip: '高品質なナレッジベースとコスト重視のナレッジベースを混在させるには、Rerankモデルが必要です。',
inconsistentEmbeddingModelTip: '選択されたナレッジベースの埋め込みモデルに一貫性がない場合、Rerankモデルが必要です。',
mixtureInternalAndExternalTip: '内部ナレッジと外部ナレッジを混在させるには、Rerankモデルが必要です。',
allExternalTip: '外部ナレッジのみを使用する場合、Rerankモデルを有効にするかを選択できます。有効にしない場合、検索結果はスコアに基づいてソートされます。異なるナレッジベースで検索戦略が一貫していないと、結果が不正確になる可能性があります。',
mixtureInternalAndExternalTip: '内部と外部ナレッジベースを混在させる場合、Rerankモデルが必要です。',
allExternalTip: '外部ナレッジベースのみを使用する場合、Rerankモデルを有効にするかを選択できます。有効にしない場合、検索結果はスコアに基づいてソートされます。異なるナレッジベースで検索戦略が一貫していないと、結果が不正確になる可能性があります。',
retrievalSettings: '検索設定',
rerankSettings: 'Rerank設定',
weightedScore: {
@@ -166,8 +166,56 @@ const translation = {
cancel: 'キャンセル',
},
preprocessDocument: '{{num}}件のドキュメントを前処理',
allKnowledge: 'すべての知識',
allKnowledgeDescription: 'このワークスペースにすべてのナレッジを表示する場合に選択します。ワークスペースのオーナーのみがすべてのナレッジを管理できます。',
allKnowledge: 'ナレッジベース全体',
allKnowledgeDescription: 'このワークスペースにナレッジベース全体を表示する場合に選択します。ワークスペースのオーナーのみがすべてのナレッジベースを管理できます。',
embeddingModelNotAvailable: 'Embeddingモデル不可用。',
metadata: {
metadata: 'メタデータ',
addMetadata: 'メタデータを追加',
chooseTime: '時間を選択',
createMetadata: {
title: '新規メタデータ',
back: '戻る',
type: 'タイプ',
name: '名称',
namePlaceholder: 'メタデータ名を入力',
},
checkName: {
empty: 'メタデータ名を入力してください',
invalid: 'メタデータ名は小文字、数字、アンダースコアのみを使用し、小文字で始める必要があります',
},
batchEditMetadata: {
editMetadata: 'メタデータを編集',
editDocumentsNum: '{{num}}件のドキュメントを編集',
applyToAllSelectDocument: '選択したすべてのドキュメントに適用',
applyToAllSelectDocumentTip: '上記の編集と新しいメタデータを選択したすべてのドキュメントに自動的に適用します。チェックしない場合、既にメタデータを持つドキュメントにのみ編集が適用されます。',
multipleValue: '複数の値',
},
selectMetadata: {
search: 'メタデータを検索',
newAction: '新規メタデータ',
manageAction: '管理',
},
datasetMetadata: {
description: 'メタデータはドキュメントに関する情報で、ドキュメントの属性を説明するために使用されます。メタデータを活用することで、ドキュメントをより効率的に整理・管理できます。',
addMetaData: 'メタデータを追加',
values: '{{num}}個の値',
disabled: '無効',
rename: '名前変更',
name: '名称',
namePlaceholder: 'メタデータ名',
builtIn: '組み込み',
builtInDescription: '組み込みメタデータはシステムによって事前定義されたメタデータです。ここで組み込みメタデータの表示と管理ができます。',
deleteTitle: '削除の確認',
deleteContent: 'メタデータ「{{name}}」を削除してもよろしいですか?',
},
documentMetadata: {
metadataToolTip: 'メタデータはドキュメントに関する情報で、ドキュメントの属性を説明するために使用されます。メタデータを活用することで、ドキュメントをより効率的に整理・管理できます。',
startLabeling: 'ラベリングを開始',
documentInformation: 'ドキュメント情報',
technicalParameters: '技術パラメータ',
},
},
}
export default translation
+1
View File
@@ -37,6 +37,7 @@ const translation = {
HR: '人事',
Workflow: 'ワークフロー',
Agent: 'エージェント',
Entertainment: 'エンターテイメント',
},
}
+17 -15
View File
@@ -21,7 +21,7 @@ const translation = {
marketplace: 'マーケットプレイスからインストール',
},
noInstalled: 'プラグインはインストールされていません',
notFound: 'プラグインが見つかりませんでした',
notFound: 'プラグインが見つかりません',
},
source: {
github: 'GitHub',
@@ -160,26 +160,28 @@ const translation = {
upgrade: 'インストール',
},
error: {
fetchReleasesError: 'リリースを取得できませんでした。後でもう一度お試しください。',
fetchReleasesError: 'リリースを取得できません。後でもう一度お試しください。',
inValidGitHubUrl: '無効なGitHub URLです。有効なURLを次の形式で入力してください: https://github.com/owner/repo',
noReleasesFound: 'リリースは見つかりませんでした。GitHubリポジトリまたは入力URLを確認してください。',
noReleasesFound: 'リリースは見つかりません。GitHubリポジトリまたは入力URLを確認してください。',
},
marketplace: {
sortOption: {
mostPopular: '最も人気のある',
recentlyUpdated: '最近更新されました',
newlyReleased: '新発売',
firstReleased: '最初にリリースされた',
},
sortBy: '黒い街',
empower: 'AI開発をサポートする',
discover: '探索',
and: 'と',
pluginsResult: '{{num}} 件の結果',
noPluginFound: 'プラグインが見つかりませんでした',
moreFrom: 'マーケットプレイスからのさらなる情報',
difyMarketplace: 'Difyマーケットプレイス',
moreFrom: 'マーケットプレイスからのさらなる情報',
noPluginFound: 'プラグインが見つかりません',
pluginsResult: '{{num}} 件の結果',
sortBy: '並べ替え',
sortOption: {
mostPopular: '人気順',
recentlyUpdated: '最近更新順',
newlyReleased: '新着順',
firstReleased: 'リリース順',
},
viewMore: 'もっと見る',
discover: '発見する',
empower: 'AI開発を強化する',
verifiedTip: 'このプラグインはDifyによって認証されています',
partnerTip: 'このプラグインはDifyのパートナーによって認証されています',
},
task: {
installError: '{{errorLength}} プラグインのインストールに失敗しました。表示するにはクリックしてください。',
+15 -15
View File
@@ -1,31 +1,31 @@
const translation = {
input: '入力',
result: '結果',
detail: '詳細',
tracing: 'トレース',
detail: '詳細情報',
tracing: '実行追跡',
resultPanel: {
status: 'ステータス',
time: '経過時間',
tokens: 'トークンの合計',
time: '処理時間',
tokens: 'トークン総数',
},
meta: {
title: 'メタデータ',
status: 'ステータス',
status: '状態',
version: 'バージョン',
executor: '実行者',
startTime: '開始時',
time: '経過時間',
tokens: 'トークンの合計',
steps: '実行ステップ',
startTime: '開始時',
time: '総処理時間',
tokens: 'トークン総数',
steps: '処理ステップ',
},
resultEmpty: {
title: 'の実行では JSON 形式のみが出力されま',
tipLeft: 'にアクセスしてください',
link: '詳細パネル',
tipRight: '表示します。',
title: '今回の実行ではJSON形式のみが出力されました',
tipLeft: '詳細を確認するには',
link: '詳細情報パネル',
tipRight: 'へ移動してください',
},
circularInvocationTip: '現在のワークフローには、ツール/ノードの循環的な呼び出しがあります。',
actionLogs: 'アクションログ',
actionLogs: '操作ログ',
circularInvocationTip: '現在のワークフローにツール/ノードの循環呼び出しが検出されました',
}
export default translation
+48 -46
View File
@@ -1,72 +1,74 @@
const translation = {
common: {
welcome: '',
appUnavailable: 'アプリ利用できません',
appUnknownError: 'アプリ利用できません',
appUnavailable: 'アプリケーションは利用できません',
appUnknownError: 'アプリケーションは利用できません',
},
chat: {
newChat: '新しいチャット',
pinnedTitle: 'ピン留めされた',
unpinnedTitle: 'チャット',
newChatDefaultName: '新しい会話',
resetChat: '会話をリセット',
newChat: '新チャット',
newChatTip: '新規チャットが開始されています',
chatSettingsTitle: 'チャット設定',
chatFormTip: 'チャット開始後は設定を変更できません',
pinnedTitle: 'ピン留め済み',
unpinnedTitle: 'チャットリスト',
newChatDefaultName: '新規チャット',
resetChat: 'チャットをリセット',
viewChatSettings: '設定を確認',
poweredBy: 'Powered by',
prompt: 'プロンプト',
privatePromptConfigTitle: '会話の設定',
publicPromptConfigTitle: '初期プロンプト',
configStatusDes: '開始前に、会話の設定を変更できます',
configDisabled:
'前回のセッションの設定がこのセッションで使用されました。',
privatePromptConfigTitle: '個別設定',
publicPromptConfigTitle: '共通プロンプト設定',
configStatusDes: '開始前に設定を変更できます',
configDisabled: '前回の設定を適用中です',
startChat: 'チャットを開始',
privacyPolicyLeft:
'アプリ開発者が提供する',
privacyPolicyMiddle:
'プライバシーポリシー',
privacyPolicyRight:
'をお読みください。',
privacyPolicyLeft: '本アプリの',
privacyPolicyMiddle: 'プライバシーポリシー',
privacyPolicyRight: 'に同意の上ご利用ください',
deleteConversation: {
title: '会話を削除する',
content: 'この会話を削除してもよろしいですか?',
title: 'チャットの削除',
content: 'このチャットを削除しますか?',
},
tryToSolve: '解決しようとしています',
temporarySystemIssue: '申し訳ありません、一時的なシステムの問題が発生しました。',
tryToSolve: '問題を解決する',
temporarySystemIssue: 'システムに一時的な問題が発生しています',
},
generation: {
tabs: {
create: '一度だけ実行',
create: '1回実行',
batch: '一括実行',
saved: '保存済み',
},
savedNoData: {
title: 'まだ結果が保存されていません',
description: 'コンテンツ生成を開始し、保存された結果をこちらで見つけてください。',
startCreateContent: 'コンテンツの作成を開始',
title: '保存済みデータがありません',
description: 'コンテンツ生成後に結果がここに表示されます',
startCreateContent: '成を開始',
},
title: 'AI Completion',
queryTitle: 'コンテンツのクエリ',
completionResult: 'Completion 結果',
queryPlaceholder: 'クエリコンテンツを書いてください...',
title: 'AI文章作成',
queryTitle: '入力内容',
completionResult: '生成結果',
queryPlaceholder: '入力してください',
run: '実行',
execution: '処理中',
executions: '{{num}}回実行',
copy: 'コピー',
resultTitle: 'AI Completion',
noData: 'AIはここで必要なものを提供します',
csvUploadTitle: 'CSVファイルをここにドラッグアンドドロップするか',
browse: '参照',
csvStructureTitle: 'CSVファイルは以下の構造に準拠する必要があります:',
downloadTemplate: 'こちらからテンプレートをダウンロード',
field: 'フィールド',
resultTitle: 'AI生成結果',
noData: 'AIがコンテンツを生成します',
csvUploadTitle: 'CSVファイルをドロップするか',
browse: 'ファイルを選択',
csvStructureTitle: 'CSV形式要件:',
downloadTemplate: 'テンプレートを取得',
field: '',
batchFailed: {
info: '{{num}} 回の実行が失敗しました',
retry: '再行',
outputPlaceholder: '出力コンテンツなし',
info: '{{num}}件の失敗',
retry: '再行',
outputPlaceholder: '出力なし',
},
errorMsg: {
empty: 'アップロードされたファイルにコンテンツを入力してください。',
fileStructNotMatch: 'アップロードされたCSVファイルが構造と一致しません。',
emptyLine: '{{rowIndex}} が空です',
invalidLine: '{{rowIndex}}: {{varName}} の値は空にできません',
moreThanMaxLengthLine: '{{rowIndex}}: {{varName}} の値は {{maxLength}} 文字を超えることはできません',
atLeastOne: 'アップロードされたファイルには少なくとも1行の入力が必要です',
empty: 'ファイル内容が空です',
fileStructNotMatch: 'ファイル形式が不正です',
emptyLine: '{{rowIndex}}行目: 内容が空です',
invalidLine: '{{rowIndex}}行目: {{varName}}の入力が必要です',
moreThanMaxLengthLine: '{{rowIndex}}行目: {{varName}}が制限長({{maxLength}})を超過',
atLeastOne: '1行以上のデータが必要です',
},
},
}
+2 -2
View File
@@ -4,7 +4,7 @@ const translation = {
customToolTip: 'Difyカスタムツールの詳細',
type: {
all: 'すべて',
builtIn: '組み込み',
builtIn: 'ツール',
custom: 'カスタム',
workflow: 'ワークフロー',
},
@@ -13,7 +13,7 @@ const translation = {
line2: 'Difyへのツールの貢献に興味があります。',
viewGuide: 'ガイドを見る',
},
author: '著者',
author: '著者',
auth: {
unauthorized: '認証する',
authorized: '認証済み',
+271 -185
View File
@@ -8,6 +8,7 @@ const translation = {
published: '公開済み',
publish: '公開する',
update: '更新',
publishUpdate: '更新を公開',
run: '実行',
running: '実行中',
inRunMode: '実行モード中',
@@ -19,48 +20,50 @@ const translation = {
goBackToEdit: '編集に戻る',
conversationLog: '会話ログ',
features: '機能',
featuresDescription: 'Webアプリのユーザーエクスペリエンスを強化する',
ImageUploadLegacyTip: '開始フォームでファイルタイプ変数作成できるようになりました。まもなく、画像アップロード機能サポート終了いたします。',
fileUploadTip: '画像アップロード機能がファイルアップロード機能にアップグレードされました',
featuresDocLink: '詳細はこちら',
featuresDescription: 'Webアプリの操作性を向上させる機能',
ImageUploadLegacyTip: '開始フォームでファイル変数作成可能になりました。画像アップロード機能は今後サポート終了となります。',
fileUploadTip: '画像アップロード機能がファイルアップロードに拡張されました',
featuresDocLink: '詳細を見る',
debugAndPreview: 'プレビュー',
restart: '再起動',
currentDraft: '現在の下書き',
currentDraftUnpublished: '現在の下書き(未公開)',
latestPublished: '最新公開済み',
latestPublished: '最新公開',
publishedAt: '公開日時',
restore: '復元',
versionHistory: 'バージョン履歴',
exitVersions: 'バージョン履歴を閉じる',
runApp: 'アプリを実行',
batchRunApp: 'バッチでアプリを実行',
openInExplore: '"探索" で開く',
accessAPIReference: 'APIリファレンスにアクセス',
batchRunApp: 'アプリを一括実行',
openInExplore: '探索ページで開く',
accessAPIReference: 'APIリファレンス',
embedIntoSite: 'サイトに埋め込む',
addTitle: 'タイトルを追加...',
addDescription: '説明を追加...',
noVar: '変数なし',
noVar: '変数がありません',
searchVar: '変数を検索',
variableNamePlaceholder: '変数名',
setVarValuePlaceholder: '変数を設定',
needConnectTip: 'このステップは何にも接続されていません',
maxTreeDepth: 'ブランチごとの最大制限は{{depth}}ノードです',
variableNamePlaceholder: '変数名を入力',
setVarValuePlaceholder: '変数を設定',
needConnectTip: '接続されていないステップがあります',
maxTreeDepth: '1ブランチあたりの最大ノード数:{{depth}}',
needEndNode: '終了ブロックを追加する必要があります',
needAnswerNode: '回答ブロックを追加する必要があります',
workflowProcess: 'ワークフロー処理',
notRunning: 'まだ実行されていません',
previewPlaceholder: 'チャットボットのデバッグを開始するには、以下のボックスにコンテンツを入力してください',
previewPlaceholder: '入力欄にテキストを入力してチャットボットのデバッグを開始',
effectVarConfirm: {
title: '変数削除',
title: '変数削除',
content: '他のノードで変数が使用されています。それでも削除しますか?',
},
insertVarTip: 'クイック挿入のために\'/\'キーを押します',
insertVarTip: '"/"キーで変数を挿入',
processData: 'データ処理',
input: '入力',
output: '出力',
jinjaEditorPlaceholder: '変数を挿入するには「/」または「{」を入力してください',
viewOnly: '表示のみ',
jinjaEditorPlaceholder: '「/」または 「{」で変数挿入',
viewOnly: '閲覧のみ',
showRunHistory: '実行履歴を表示',
enableJinja: 'Jinjaテンプレートのサポートを有効にする',
learnMore: '詳細はこちら',
enableJinja: 'Jinjaテンプレートを有効化',
learnMore: '詳細を見る',
copy: 'コピー',
duplicate: '複製',
addBlock: 'ブロックを追加',
@@ -68,44 +71,44 @@ const translation = {
pointerMode: 'ポインターモード',
handMode: 'ハンドモード',
model: 'モデル',
workflowAsTool: 'ツールとしてのワークフロー',
workflowAsTool: 'ワークフローをツールどして公開する',
configureRequired: '設定が必要',
configure: '設定',
manageInTools: 'ツールで管理',
workflowAsToolTip: 'ワークフロー更新後ツールの再設定が必要です',
manageInTools: 'ツールページで管理',
workflowAsToolTip: 'ワークフロー更新後ツールの再設定が必要です',
viewDetailInTracingPanel: '詳細を表示',
syncingData: 'データ同期中,数秒程度で終わります。',
syncingData: 'データ同期中。。。',
importDSL: 'DSLをインポート',
importDSLTip: '現在のドラフトは上書きされますので、インポートする際は、事前にワークフローをバックアップしてエクスポートいただきますよう、お願い申し上げます。',
backupCurrentDraft: '現在のドラフトをバックアップ',
importDSLTip: '現在の下書きは上書きされますインポート前にワークフローをエクスポートしてバックアップしてください',
backupCurrentDraft: '現在の下書きをバックアップ',
chooseDSL: 'DSL(yml)ファイルを選択',
overwriteAndImport: 'オーバライトとインポート',
overwriteAndImport: '上書きしてインポート',
importFailure: 'インポート失敗',
importWarning: '注意事項',
importWarningDetails: 'DSLバージョンの違いにより機能に影響が出る可能性があります',
importSuccess: 'インポート成功',
parallelRun: '並列実行',
parallelTip: {
click: {
title: 'クリック',
desc: '追加する',
desc: '追加',
},
drag: {
title: 'ドラッグ',
desc: '接続するには',
desc: '接続',
},
limit: '並列処理は {{num}} ブランチに制限されています。',
depthLimit: '{{num}}レイヤーの平行ネストレイヤーの制限',
limit: '並列処理可能ブランチ数:{{num}}',
depthLimit: '並列ネスト最大階層数:{{num}}',
},
parallelRun: 'パラレルラン',
disconnect: '切る',
jumpToNode: 'このノードにジャンプします',
disconnect: '接続解除',
jumpToNode: 'このノードに移動',
addParallelNode: '並列ノードを追加',
parallel: '並列',
branch: 'ブランチ',
importWarning: '注意',
importWarningDetails: 'DSL のバージョンの違いが特定の機能に影響を与える場合があります',
onFailure: '失敗時',
addFailureBranch: '失敗ブランチを追加',
noHistory: '履歴なし',
loadMore: 'より多くのワークフローをロードする',
loadMore: 'さらに読み込む',
noHistory: '履歴がありません',
},
env: {
envPanelTitle: '環境変数',
@@ -115,36 +118,36 @@ const translation = {
title: '環境変数を追加',
editTitle: '環境変数を編集',
type: 'タイプ',
name: '名',
namePlaceholder: '変数名',
name: '変数名',
namePlaceholder: '変数名を入力',
value: '値',
valuePlaceholder: '変数値',
secretTip: 'このような機密情報やデータは、定義に使用され、DSLの設定は情報漏洩を防ぐために特別に構成されています。',
valuePlaceholder: '変数値を入力',
secretTip: 'この変数は機密情報やデータを定義するために使用されます。DSL をエクスポートするときに漏洩防止メカニズムを設定されます。',
},
export: {
title: 'シークレット環境変数をエクスポートしますか?',
checkbox: 'シークレット値をエクスポート',
checkbox: 'シークレット値を含む',
ignore: 'DSLをエクスポート',
export: 'シークレット値を含むDSLをエクスポート',
export: 'シークレット値付きでエクスポート',
},
},
chatVariable: {
panelTitle: '会話変数',
panelDescription: '会話変数は、LLMが記憶すべき対話情報を保存するために使用されます。この情報には、対話の履歴、アップロードされたファイルユーザーの好みなどが含まれます。読み書きが可能です。',
docLink: '詳しくはドキュメントをご覧ください。',
panelDescription: '対話情報を保存・管理(会話履歴/ファイル/ユーザー設定など)。書き換えができます。',
docLink: '詳ドキュメント',
button: '変数を追加',
modal: {
title: '会話変数を追加',
editTitle: '会話変数を編集',
name: '名',
namePlaceholder: '変数名',
name: '変数名',
namePlaceholder: '変数名を入力',
type: 'タイプ',
value: 'デフォルト値',
valuePlaceholder: 'デフォルト値、設定しない場合は空白にしでください',
description: '説明',
descriptionPlaceholder: '変数の説明',
editInJSON: 'JSONで編集する',
oneByOne: '次々に追加する',
descriptionPlaceholder: '変数の説明を入力',
editInJSON: 'JSONで編集',
oneByOne: '個別追加',
editInForm: 'フォームで編集',
arrayValue: '値',
addArrayValue: '値を追加',
@@ -152,19 +155,19 @@ const translation = {
objectType: 'タイプ',
objectValue: 'デフォルト値',
},
storedContent: '保存されたコンテンツ',
updatedAt: '更新日は',
storedContent: '保存内容',
updatedAt: '最終更新:',
},
changeHistory: {
title: '変更履歴',
placeholder: 'まだ何も変更ていません',
placeholder: 'まだ何も変更されていません',
clearHistory: '履歴をクリア',
hint: 'ヒント',
hintText: '編集アクションは変更履歴に記録され、このセッションの間にデバイスに保存されます。エディターを終了すると、この履歴は消去されます。',
stepBackward_one: '{{count}} ステップ後退',
stepBackward_other: '{{count}} ステップ後退',
stepForward_one: '{{count}} ステップ進',
stepForward_other: '{{count}} ステップ進',
hintText: 'エディターでの編集操作は、エディターを離れるまで、お使いのデバイスに記録されます。この履歴は、エディターを離れると消去されます。',
stepBackward_one: '{{count}} ステップ戻る',
stepBackward_other: '{{count}} ステップ戻る',
stepForward_one: '{{count}} ステップ進',
stepForward_other: '{{count}} ステップ進',
sessionStart: 'セッション開始',
currentState: '現在の状態',
nodeTitleChange: 'ブロックのタイトルが変更されました',
@@ -175,16 +178,17 @@ const translation = {
nodePaste: 'ブロックが貼り付けられました',
nodeDelete: 'ブロックが削除されました',
nodeAdd: 'ブロックが追加されました',
nodeResize: 'ブロックがリサイズされました',
noteAdd: 'ノートが追加されました',
noteChange: 'ノートが変更されました',
noteDelete: 'ノートが削除されました',
edgeDelete: 'ブロックが切断されました',
nodeResize: 'ブロックサイズが変更されました',
noteAdd: '注釈が追加されました',
noteChange: '注釈が変更されました',
noteDelete: '注釈が削除されました',
edgeDelete: 'ブロックの接続が解除されました',
},
errorMsg: {
fieldRequired: '{{field}}は必須です',
fieldRequired: '{{field}} は必須です',
rerankModelRequired: 'Rerank モデルが設定されていません',
authRequired: '認証が必要です',
invalidJson: '{{field}}は無効です',
invalidJson: '{{field}} は無効な JSON です',
fields: {
variable: '変数名',
variableValue: '変数値',
@@ -193,33 +197,33 @@ const translation = {
rerankModel: 'Rerankモデル',
visionVariable: 'ビジョン変数',
},
invalidVariable: '無効な変数',
rerankModelRequired: 'モデルの再ランク付けをオンにする前に、モデルが設定で正常に構成されていることを確認してください。',
invalidVariable: '無効な変数です',
noValidTool: '{{field}} に利用可能なツールがありません',
toolParameterRequired: '{{field}}: パラメータ [{{param}}] は必須です',
noValidTool: '{{field}} 有効なツールが選択されていません',
},
singleRun: {
testRun: 'テスト実行',
startRun: '実行開始',
startRun: '実行開始',
running: '実行中',
testRunIteration: 'テスト実行イテレーション',
testRunIteration: 'テスト実行イテレーション',
testRunLoop: 'テスト実行(ループ)',
back: '戻る',
iteration: 'イテレーション',
loop: 'ループ',
},
tabs: {
'searchBlock': 'ブロック検索',
'searchBlock': 'ブロック検索',
'blocks': 'ブロック',
'searchTool': '検索ツール',
'searchTool': 'ツール検索',
'tools': 'ツール',
'allTool': 'すべて',
'workflowTool': 'ワークフロー',
'builtInTool': '組み込み',
'customTool': 'カスタム',
'question-understand': '質問の理解',
'workflowTool': 'ワークフロー',
'question-understand': '問題理解',
'logic': 'ロジック',
'transform': '変換',
'utilities': 'ユーティリティ',
'noResult': '一致するものが見つかりませんでした',
'utilities': 'ツール',
'noResult': '該当なし',
'plugin': 'プラグイン',
'agent': 'エージェント戦略',
},
@@ -228,10 +232,10 @@ const translation = {
'end': '終了',
'answer': '回答',
'llm': 'LLM',
'knowledge-retrieval': '知識取得',
'knowledge-retrieval': '知識検索',
'question-classifier': '質問分類器',
'if-else': 'IF/ELSE',
'code': 'コード',
'code': 'コード実行',
'template-transform': 'テンプレート',
'http-request': 'HTTPリクエスト',
'variable-assigner': '変数代入器',
@@ -239,54 +243,64 @@ const translation = {
'assigner': '変数代入',
'iteration-start': 'イテレーション開始',
'iteration': 'イテレーション',
'parameter-extractor': 'パラメータ抽出',
'document-extractor': 'テキスト抽出ツール',
'parameter-extractor': 'パラメータ抽出',
'document-extractor': 'テキスト抽出',
'list-operator': 'リスト処理',
'agent': 'エージェント',
'loop-start': 'ループ開始',
'loop': 'ループ',
},
blocksAbout: {
'start': 'ワークフロー開始に必要なパラメータを定義します',
'end': 'ワークフローの終了と結果のタイプを定義します',
'answer': 'チャット会話の応答内容を定義します',
'llm': '大規模言語モデルを呼び出して質問回答したり、自然言語処理したりします',
'knowledge-retrieval': 'ユーザー質問に関連するテキストコンテンツを知識からクエリできるようにします',
'question-classifier': 'ユーザーの質問の分類条件を定義し、LLM分類記述に基づいて会話がどのように進行するかを定義できます',
'if-else': 'IF/ELSE条件に基づいてワークフローを2つのブランチに分割できます',
'code': 'カスタムロジックを実装するためにPythonまたはNodeJSコードを実行します',
'template-transform': 'Jinjaテンプレート構文を使用してデータを文字列に変換します',
'http-request': 'HTTPプロトコル経由でサーバーリクエストを送信できます',
'variable-assigner': '複数のブランチの変数を1つの変数に集約し、下流ノードに対して統一された設定を行います。',
'assigner': '変数代入ノードは、書き込み可能な変数(例えば、会話変数)に値を割り当てるために使用されます。',
'variable-aggregator': '複数のブランチの変数を1つの変数に集約し、下流ノードに対して統一された設定を行います。',
'iteration': 'リストオブジェクトに対して複数のステップを実行し、すべての結果出力されるまで繰り返します。',
'parameter-extractor': '自然言語からツールの呼び出しやHTTPリクエストのための構造化されたパラメーターを抽出するためにLLMを使用します。',
'document-extractor': 'アップロードされたドキュメントを LLM で簡単に理解できるテキストのコンテンツに解析するために使用されます。',
'list-operator': '配列のコンテンツをフィルタリングまたはソートするために使用されます。',
'agent': '大規模言語モデルを呼び出して質問に答えたり自然言語を処理したりする',
'start': 'ワークフロー開始時の初期パラメータを定義します',
'end': 'ワークフローの終了条件と結果のタイプを定義します',
'answer': 'チャットダイアログの返答内容を定義します',
'llm': '大規模言語モデルを呼び出して質問回答自然言語処理を実行します',
'knowledge-retrieval': 'ナレッジベースからユーザー質問に関連するテキストを検索します',
'question-classifier': '質問の分類条件を定義し、LLM分類に基づいて対話フローを制御します',
'if-else': 'if/else条件でワークフローを2つの分岐に分割ます',
'code': 'Python/NodeJSコードを実行してカスタムロジックを実装します',
'template-transform': 'Jinjaテンプレート構文データを文字列に変換します',
'http-request': 'HTTPリクエストを送信できます',
'variable-assigner': '複数分岐の変数を集約し、下流ノードの設定を統一します。',
'assigner': '書き込み可能な変数(例会話変数)への値の割り当てを行います。',
'variable-aggregator': '複数分岐の変数を集約し、下流ノードの設定を統一します。',
'iteration': 'リスト要素に対して反復処理を実行し全結果出力します。',
'loop': '終了条件達成まで、または最大反復回数までロジックを繰り返します。',
'parameter-extractor': '自然言語から構造化パラメータを抽出し、後続処理で利用します。',
'document-extractor': 'アップロード文書をLLM処理用に最適化されたテキストに変換します。',
'list-operator': '配列のフィルタリングやソート処理を行います。',
'agent': '大規模言語モデルを活用した質問応答や自然言語処理を実行します。',
},
operator: {
zoomIn: '拡大',
zoomOut: '縮小',
zoomTo50: '50%にズーム',
zoomTo100: '100%にズーム',
zoomToFit: 'フィットにズーム',
zoomTo50: '50%サイズ',
zoomTo100: '等倍表示',
zoomToFit: '画面に合わせる',
},
variableReference: {
noAvailableVars: '利用可能な変数がありません',
noVarsForOperation: 'この操作に割り当て可能な変数が存在しません。',
noAssignedVars: '割り当て可能な変数がありません',
assignedVarsDescription: '書き込み可能な変数(例:',
conversationVars: '会話変数',
},
panel: {
userInputField: 'ユーザー入力フィールド',
changeBlock: 'ブロックを変更',
userInputField: 'ユーザー入力',
changeBlock: 'ノード変更',
helpLink: 'ヘルプリンク',
about: '情報',
createdBy: '作成者 ',
about: '詳細',
createdBy: '作成者',
nextStep: '次のステップ',
addNextStep: 'このワークフローで次のブロックを追加',
selectNextStep: '次のブロックを選択',
runThisStep: 'このステップ実行',
addNextStep: 'このワークフローで次ノードを追加',
selectNextStep: '次ノード選択',
runThisStep: 'このステップ実行',
checklist: 'チェックリスト',
checklistTip: '公開する前にすべての問題が解決されていることを確認してください',
checklistResolved: 'すべての問題が解決されました',
organizeBlocks: 'ブロックを整理',
checklistTip: '公開前に全ての項目を確認してください',
checklistResolved: '全てのチェックが完了しました',
organizeBlocks: 'ノード整理',
change: '変更',
optional: '(オプション)',
optional: '(任意)',
},
nodes: {
common: {
@@ -295,54 +309,54 @@ const translation = {
memory: {
memory: 'メモリ',
memoryTip: 'チャットメモリ設定',
windowSize: 'ウィンドウサイズ',
windowSize: 'メモリウィンドウサイズ',
conversationRoleName: '会話ロール名',
user: 'ユーザー接頭辞',
assistant: 'アシスタント接頭辞',
},
memories: {
title: 'メモリ',
tip: 'チャットメモリ',
tip: 'チャットの記憶管理',
builtIn: '組み込み',
},
errorHandle: {
title: '例外処理',
tip: 'ノード例外発生時の処理ポリシーを設定',
none: {
title: '処理なし',
desc: '例外発生して処理されない場合、ノードは実行を停止します',
desc: '例外発生時に処理を停止',
},
defaultValue: {
title: 'デフォルト値',
desc: '例外発生した場合は、デフォルト出力コンテンツを指定します。',
tip: '例外発生した場合は、以下の値を返します。',
inLog: 'ノード例外デフォルト値に従って出力します。',
output: '出力デフォルト値',
desc: '例外発生時のデフォルト出力',
tip: '例外発生時に返される値:',
inLog: 'ノード例外 - デフォルト値を出力',
output: 'デフォルト値出力',
},
failBranch: {
title: 'エラーブランチ',
customize: 'キャンバスに移動して、エラーブランチのロジックをカスタマイズします。',
inLog: '例外が発生した場合は、エラーしたブランチを自動的に実行します。ノード出力は、エラータイプとエラーメッセージを返し、それらをダウンストリームに渡します。',
desc: '例外発生した場合は、エラーブランチを実行します',
customizeTip: 'エラーブランチがアクティブになっても、ノードによってスローされた例外はプロセスを終了させません。代わりに、事前定義された エラーブランチが自動的に実行されるため、エラーメッセージ、レポート、修正アクション、またはスキップアクションを柔軟に提供できます。',
title: '例外分岐',
desc: '例外発生時に分岐を実行',
customize: '失敗分岐ロジックをカスタマイズ',
customizeTip: '例外発生時、失敗分岐でエラー処理を柔軟に設定可能(エラーログ表示/修復処理/操作スキップ等)',
inLog: 'ノード例外 - 失敗分岐を実行。エラー情報を下流に伝播',
},
partialSucceeded: {
tip: 'プロセスに{{num}}ノード異常に動作していますので、トレースに移動してログを確認してください。',
tip: '{{num}}個のノード異常発生。ログはトレース画面で確認可能',
},
title: 'エラー処理',
tip: 'ノードが例外を検出したときにトリガーされる例外処理戦略。',
},
retry: {
retry: '再試行',
retryOnFailure: '失敗時再試行',
maxRetries: '最大試行回数',
retryOnFailure: '失敗時再試行',
maxRetries: '最大試行回数',
retryInterval: '再試行間隔',
retrying: '再試行。。。',
retryFailed: '再試行に失敗しました',
retryTimes: '失敗時 {{times}}回再試行',
retrying: '再試行中...',
retrySuccessful: '再試行成功',
retryFailed: '再試行失敗',
retryFailedTimes: '{{times}}回再試行失敗',
times: '回',
ms: 'ms',
retryTimes: '失敗時に{{times}}回再試行',
retrySuccessful: '再試行に成功しました',
retries: '{{num}} 回の再試行',
retryFailedTimes: '{{times}}回の再試行が失敗しました',
ms: 'ミリ秒',
retries: '再試行回数: {{num}}',
},
},
start: {
@@ -353,17 +367,17 @@ const translation = {
query: 'ユーザー入力',
memories: {
des: '会話履歴',
type: 'メッセージタイプ',
type: 'メッセージ種別',
content: 'メッセージ内容',
},
files: 'ファイルリスト',
files: 'ファイル一覧',
},
noVarTip: 'ワークフローで使用できる入力を設定します',
noVarTip: '入力設定はワークフロー内で利用可能',
},
end: {
outputs: '出力',
outputs: '出力設定',
output: {
type: '出力タイプ',
type: '出力形式',
variable: '出力変数',
},
type: {
@@ -373,75 +387,106 @@ const translation = {
},
},
answer: {
answer: '答',
answer: '答',
outputVars: '出力変数',
},
llm: {
model: 'モデル',
model: 'AIモデル',
variables: '変数',
context: 'コンテキスト',
contextTooltip: 'コンテキストとして知識をインポートできます',
notSetContextInPromptTip: 'コンテキスト機能を有効にするには、PROMPTにコンテキスト変数を記してください',
contextTooltip: 'ナレッジベースをコンテキストとして利用',
notSetContextInPromptTip: 'コンテキスト利用時はプロンプトに変数を記してください',
prompt: 'プロンプト',
addMessage: 'メッセージ追加',
roleDescription: {
system: '話の高レベルな命令を与えます',
user: 'モデルへの指示、クエリ、またはテキストベースの入力を提供します',
assistant: 'ユーザーメッセージに基づいてモデルの応答',
system: '話の基本動作を定義',
user: '指示/質問を入力',
assistant: 'ユーザー入力への応答',
},
addMessage: 'メッセージを追加',
vision: 'ビジョン',
files: 'ファイル',
resolution: {
name: '解像度',
high: '高',
low: '低',
high: '高',
low: '低',
},
outputVars: {
output: 'コンテンツを生成',
usage: 'モデル使用情報',
output: '生成内容',
usage: 'モデル使用',
},
singleRun: {
variable: '変数',
},
sysQueryInUser: 'ユーザーメッセージにsys.queryが必要です',
sysQueryInUser: 'ユーザーメッセージにsys.queryを含めてください',
},
knowledgeRetrieval: {
queryVariable: 'クエリ変数',
knowledge: 'ナレッジ',
queryVariable: '検索変数',
knowledge: 'ナレッジベース',
outputVars: {
output: '検索されたセグメント化されたデータ',
content: 'セグメント化されたコンテンツ',
title: 'セグメント化されたタイトル',
icon: 'セグメント化されたアイコン',
url: 'セグメント化されたURL',
metadata: 'その他のメタデータ',
output: '検索結果セグメント',
content: 'セグメント内容',
title: 'セグメントタイトル',
icon: 'セグメントアイコン',
url: 'セグメントURL',
metadata: 'メタデータ',
},
metadata: {
title: 'メタデータフィルタ',
tip: 'タグ/カテゴリ等の属性で検索を絞り込み',
options: {
disabled: {
title: '無効',
subTitle: 'フィルタリング不使用',
},
automatic: {
title: '自動生成',
subTitle: '検索履歴からフィルタ条件を自動生成',
desc: 'Query Variable(検索変数)に基づきフィルタ条件を自動生成',
},
manual: {
title: '手動設定',
subTitle: 'メタデータの条件を手動で追加',
},
},
panel: {
title: 'メタデータのフィルタ条件',
conditions: '条件一覧',
add: '条件追加',
search: 'メタデータ検索',
placeholder: '値を入力',
datePlaceholder: '日付選択...',
select: '変数選択...',
},
},
},
http: {
inputVars: '入力変数',
api: 'API',
apiPlaceholder: 'URLを入力、「/」を入力して変数を挿入',
notStartWithHttp: 'APIはhttp://またはhttps://で始まる必要があります',
apiPlaceholder: 'URLを入力(変数使用時は"/"を入力)',
extractListPlaceholder: 'リスト番号を入力(変数使用時は"/"を入力)',
notStartWithHttp: 'APIは http:// または https:// で始まってください',
key: 'キー',
type: 'タイプ',
value: '値',
bulkEdit: '一括編集',
keyValueEdit: 'キー-値の編集',
keyValueEdit: 'キーバリュー編集',
headers: 'ヘッダー',
params: 'パラメータ',
body: 'ボディ',
binaryFileVariable: 'バイナリファイル変数',
outputVars: {
body: 'レスポンスコンテンツ',
statusCode: 'レスポンスステータスコード',
headers: 'レスポンスヘッダーリストJSON',
files: 'ファイルリスト',
headers: 'レスポンスヘッダJSON',
files: 'ファイル一覧',
},
authorization: {
'authorization': '認証',
'authorizationType': '認証タイプ',
'no-auth': 'なし',
'api-key': 'APIキー',
'auth-type': '認証タイプ',
'basic': '基本',
'auth-type': 'API認証タイプ',
'basic': 'ベーシック',
'bearer': 'Bearer',
'custom': 'カスタム',
'api-key-title': 'APIキー',
@@ -449,19 +494,16 @@ const translation = {
},
insertVarPlaceholder: '変数を挿入するには\'/\'を入力してください',
timeout: {
title: 'タイムアウト',
title: 'タイムアウト設定',
connectLabel: '接続タイムアウト',
connectPlaceholder: '接続タイムアウトを秒で入力',
connectPlaceholder: '接続タイムアウト(秒)',
readLabel: '読み取りタイムアウト',
readPlaceholder: '読み取りタイムアウトを秒で入力',
readPlaceholder: '読み取りタイムアウト(秒)',
writeLabel: '書き込みタイムアウト',
writePlaceholder: '書き込みタイムアウトを秒で入力',
writePlaceholder: '書き込みタイムアウト(秒)',
},
type: 'タイプ',
binaryFileVariable: 'バイナリファイル変数',
extractListPlaceholder: 'リスト項目のインデックスを入力し、変数を挿入 \'/\' と入力します',
curl: {
title: 'cURLからインポート',
title: 'cURLからインポート',
placeholder: 'ここにcURL文字列を貼り付けます',
},
},
@@ -653,6 +695,25 @@ const translation = {
MaxParallelismDesc: '最大並列処理は、1 回の反復で同時に実行されるタスクの数を制御するために使用されます。',
answerNodeWarningDesc: '並列モードの警告: 応答ノード、会話変数の割り当て、およびイテレーション内の永続的な読み取り/書き込み操作により、例外が発生する可能性があります。',
},
loop: {
deleteTitle: 'ループノードを削除しますか?',
deleteDesc: 'ループノードを削除すると、全ての子ノードが削除されます。',
input: '入力',
output: '出力変数',
loop_one: '{{count}}回',
loop_other: '{{count}}回',
currentLoop: '現在のループ',
breakCondition: 'ループ終了条件',
breakConditionTip: 'ループ内の変数やセッション変数を参照し、終了条件を設定できます。',
loopMaxCount: '最大ループ回数',
loopMaxCountError: '最大ループ回数は1から{{maxCount}}の範囲で正しく入力してください。',
errorResponseMethod: 'エラー対応方法',
ErrorMethod: {
operationTerminated: 'エラー時に処理を終了',
continueOnError: 'エラーを無視して継続',
removeAbnormalOutput: '異常出力を除外',
},
},
note: {
addNote: 'コメントを追加',
editor: {
@@ -771,12 +832,37 @@ const translation = {
tracing: {
stopBy: '{{user}}によって停止',
},
variableReference: {
noVarsForOperation: '選択した操作で代入できる変数はありません。',
noAvailableVars: '使用可能な変数がありません',
noAssignedVars: '使用可能な代入変数がありません',
assignedVarsDescription: '代入変数は、次のような書き込み可能な変数である必要があります。',
conversationVars: '会話変数',
versionHistory: {
title: 'バージョン',
currentDraft: '現在の下書き',
latest: '最新版',
filter: {
all: 'すべて',
onlyYours: '自分のみ',
onlyShowNamedVersions: '名前付きバージョンのみ',
reset: 'リセット',
empty: '該当するバージョンがありません',
},
defaultName: '名称未設定',
nameThisVersion: 'バージョン名を付ける',
editVersionInfo: 'バージョン情報を編集',
editField: {
title: 'タイトル',
releaseNotes: 'リリースノート',
titleLengthLimit: 'タイトルは{{limit}}文字以内で入力してください',
releaseNotesLengthLimit: 'リリースノートは{{limit}}文字以内で入力してください',
},
releaseNotesPlaceholder: '変更内容を入力してください',
restorationTip: 'バージョンを復元すると、現在の下書きが上書きされます',
deletionTip: '削除したデータは復元できません。よろしいですか?',
action: {
restoreSuccess: '復元が完了しました',
restoreFailure: '復元に失敗しました',
deleteSuccess: '削除が完了しました',
deleteFailure: '削除に失敗しました',
updateSuccess: '更新が完了しました',
updateFailure: '更新に失敗しました',
},
},
}
+2
View File
@@ -1,6 +1,7 @@
import type { AgentStrategy, ModelModeType, RETRIEVE_TYPE, ToolItem, TtsAutoPlay } from '@/types/app'
import type {
RerankingModeEnum,
WeightedScoreEnum,
} from '@/models/datasets'
import type { FileUpload } from '@/app/components/base/features/types'
import type {
@@ -165,6 +166,7 @@ export type DatasetConfigs = {
}
reranking_mode?: RerankingModeEnum
weights?: {
weight_type: WeightedScoreEnum
vector_setting: {
vector_weight: number
embedding_provider_name: string
+7 -6
View File
@@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "1.1.2",
"version": "1.1.3",
"private": true,
"engines": {
"node": ">=18.18.0"
@@ -9,10 +9,10 @@
"dev": "cross-env NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"start": "cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cross-env PORT=$npm_config_port HOSTNAME=$npm_config_host node .next/standalone/server.js",
"lint": "pnpm eslint",
"lint": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
"fix": "next lint --fix",
"eslint-fix": "eslint --fix",
"eslint-fix-only-show-error": "eslint --fix --quiet",
"eslint-fix": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix",
"eslint-fix-only-show-error": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache --fix --quiet",
"prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky",
"gen-icons": "node ./app/components/base/icons/script.mjs",
"uglify-embed": "node ./bin/uglify-embed",
@@ -61,6 +61,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"decimal.js": "^10.4.3",
"dompurify": "^3.2.4",
"echarts": "^5.5.1",
"echarts-for-react": "^3.0.2",
"elkjs": "^0.9.3",
@@ -132,10 +133,10 @@
},
"devDependencies": {
"@antfu/eslint-config": "^4.1.1",
"@eslint/js": "^9.20.0",
"@chromatic-com/storybook": "^3.1.0",
"@eslint-react/eslint-plugin": "^1.15.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.20.0",
"@faker-js/faker": "^9.0.3",
"@next/eslint-plugin-next": "^15.2.3",
"@rgrove/parse-xml": "^4.1.0",
@@ -174,11 +175,11 @@
"code-inspector-plugin": "^0.18.1",
"cross-env": "^7.0.3",
"eslint": "^9.20.1",
"eslint-config-next": "^15.0.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-storybook": "^0.11.2",
"eslint-plugin-tailwindcss": "^3.18.0",
"eslint-config-next": "^15.0.0",
"husky": "^9.1.6",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
+7 -4
View File
@@ -121,6 +121,9 @@ importers:
decimal.js:
specifier: ^10.4.3
version: 10.4.3
dompurify:
specifier: ^3.2.4
version: 3.2.4
echarts:
specifier: ^5.5.1
version: 5.5.1
@@ -4299,8 +4302,8 @@ packages:
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
engines: {node: '>= 4'}
dompurify@3.2.3:
resolution: {integrity: sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==}
dompurify@3.2.4:
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
@@ -13070,7 +13073,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.3:
dompurify@3.2.4:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -15688,7 +15691,7 @@ snapshots:
d3-sankey: 0.12.3
dagre-d3-es: 7.0.11
dayjs: 1.11.13
dompurify: 3.2.3
dompurify: 3.2.4
katex: 0.16.21
khroma: 2.1.0
lodash-es: 4.17.21