mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
feat: allow knowledge base API keys to be scoped to a single dataset (#37569)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
autofix-ci[bot]
parent
7664ad7149
commit
3cd1fc098c
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Iterable, Mapping
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
@@ -40,6 +41,9 @@ class ApiKeyItem(ResponseModel):
|
||||
id: str
|
||||
type: str
|
||||
token: str
|
||||
# Dataset keys only: the knowledge bases this key is bound to. Empty = the key can
|
||||
# access every dataset in the tenant (default). App keys are always empty.
|
||||
dataset_ids: list[str] = []
|
||||
last_used_at: int | None = None
|
||||
created_at: int | None = None
|
||||
|
||||
@@ -56,6 +60,38 @@ class ApiKeyList(ResponseModel):
|
||||
register_response_schema_models(console_ns, ApiKeyItem, ApiKeyList)
|
||||
|
||||
|
||||
def mask_api_token(token: str) -> str:
|
||||
"""Mask a secret token for list responses.
|
||||
|
||||
Reveal-once: the full secret is only returned by the create endpoint. List
|
||||
endpoints expose just enough (prefix + last 4) to identify a key, never the
|
||||
full value, so an existing key's secret cannot be retrieved after creation.
|
||||
"""
|
||||
if len(token) <= 8:
|
||||
return "***"
|
||||
return f"{token[:5]}...{token[-4:]}"
|
||||
|
||||
|
||||
def build_masked_api_key_list(
|
||||
api_tokens: Iterable[ApiToken],
|
||||
bindings_by_token: Mapping[str, list[str]] | None = None,
|
||||
) -> ApiKeyList:
|
||||
"""Build an ApiKeyList from ORM tokens with their secrets masked.
|
||||
|
||||
``bindings_by_token`` maps an api_token id to the dataset ids it is bound to
|
||||
(from DatasetApiTokenBinding); tokens absent from the map are unbound (empty =
|
||||
access all). App-key lists omit it entirely.
|
||||
"""
|
||||
bindings_by_token = bindings_by_token or {}
|
||||
items: list[ApiKeyItem] = []
|
||||
for api_token in api_tokens:
|
||||
item = ApiKeyItem.model_validate(api_token, from_attributes=True)
|
||||
item.token = mask_api_token(item.token)
|
||||
item.dataset_ids = bindings_by_token.get(str(api_token.id), [])
|
||||
items.append(item)
|
||||
return ApiKeyList(data=items)
|
||||
|
||||
|
||||
def _get_resource(resource_id, tenant_id, resource_model, *, session: Session):
|
||||
resource = session.execute(
|
||||
select(resource_model).filter_by(id=resource_id, tenant_id=tenant_id)
|
||||
@@ -94,7 +130,9 @@ class BaseApiKeyListResource(Resource):
|
||||
getattr(ApiToken, self.resource_id_field) == resource_id,
|
||||
)
|
||||
).all()
|
||||
return ApiKeyList.model_validate({"data": keys}, from_attributes=True)
|
||||
# App and agent keys keep their existing (unmasked) list behavior; reveal-once
|
||||
# masking is scoped to dataset keys, which build their list in datasets.py.
|
||||
return ApiKeyList(data=[ApiKeyItem.model_validate(key, from_attributes=True) for key in keys])
|
||||
|
||||
@edit_permission_required
|
||||
@with_session
|
||||
@@ -271,6 +309,10 @@ class AppApiKeyResource(BaseApiKeyResource):
|
||||
resource_id_field = "app_id"
|
||||
|
||||
|
||||
# Dataset service-API keys are also managed at the workspace level (create with a set of
|
||||
# knowledge bases, list, delete) by DatasetApiKeyApi in
|
||||
# controllers/console/datasets/datasets.py, using DatasetApiTokenBinding for scoping.
|
||||
# The per-dataset routes below remain for callers that key an API token to a single dataset.
|
||||
@console_ns.route("/datasets/<uuid:resource_id>/api-keys")
|
||||
class DatasetApiKeyListResource(BaseApiKeyListResource):
|
||||
@console_ns.doc("get_dataset_api_keys")
|
||||
|
||||
@@ -16,7 +16,7 @@ from controllers.common.fields import ApiBaseUrlResponse, SimpleResultResponse,
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.common.session import with_session
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.apikey import ApiKeyItem, ApiKeyList
|
||||
from controllers.console.apikey import ApiKeyItem, ApiKeyList, build_masked_api_key_list
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||
from controllers.console.wraps import (
|
||||
@@ -51,6 +51,7 @@ from models import Account, ApiToken, App, Dataset, Document, DocumentSegment, U
|
||||
from models.dataset import DatasetPermission, DatasetPermissionEnum, DatasetQuery
|
||||
from models.enums import ApiTokenType, SegmentStatus
|
||||
from models.provider_ids import ModelProviderID
|
||||
from services import dataset_api_key_service
|
||||
from services.api_token_service import ApiTokenCache
|
||||
from services.app_service import AppService
|
||||
from services.dataset_ref_service import DatasetRefService
|
||||
@@ -164,6 +165,12 @@ class IndexingEstimatePayload(BaseModel):
|
||||
return result
|
||||
|
||||
|
||||
class DatasetApiKeyCreatePayload(BaseModel):
|
||||
# Knowledge bases to scope the key to. Absent/empty => the key can access every
|
||||
# dataset in the tenant (default). Declared so the generated client can send it.
|
||||
dataset_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ConsoleDatasetListQuery(BaseModel):
|
||||
page: int = Field(default=1, description="Page number")
|
||||
limit: int = Field(default=20, description="Number of items per page")
|
||||
@@ -335,7 +342,12 @@ class AutoDisableLogsResponse(ResponseModel):
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns, DatasetCreatePayload, DatasetUpdatePayload, IndexingEstimatePayload, ConsoleDatasetListQuery
|
||||
console_ns,
|
||||
DatasetCreatePayload,
|
||||
DatasetUpdatePayload,
|
||||
IndexingEstimatePayload,
|
||||
ConsoleDatasetListQuery,
|
||||
DatasetApiKeyCreatePayload,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
@@ -1108,8 +1120,11 @@ class DatasetApiKeyApi(Resource):
|
||||
keys = session.scalars(
|
||||
select(ApiToken).where(ApiToken.type == self.resource_type, ApiToken.tenant_id == current_tenant_id)
|
||||
).all()
|
||||
return dump_response(ApiKeyList, {"data": keys})
|
||||
token_ids = [str(key.id) for key in keys]
|
||||
bindings_by_token = dataset_api_key_service.list_bindings_by_token(session, token_ids)
|
||||
return dump_response(ApiKeyList, build_masked_api_key_list(keys, bindings_by_token))
|
||||
|
||||
@console_ns.expect(console_ns.models[DatasetApiKeyCreatePayload.__name__])
|
||||
@console_ns.response(200, "API key created successfully", console_ns.models[ApiKeyItem.__name__])
|
||||
@console_ns.response(400, "Maximum keys exceeded")
|
||||
@setup_required
|
||||
@@ -1120,6 +1135,19 @@ class DatasetApiKeyApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str):
|
||||
# Optional list of knowledge bases to scope the key to. Absent/empty => the key
|
||||
# can access every dataset in the tenant (default). Duplicates are de-duplicated.
|
||||
payload = request.get_json(silent=True) or {}
|
||||
raw_dataset_ids = payload.get("dataset_ids") or []
|
||||
if not isinstance(raw_dataset_ids, list) or any(not isinstance(item, str) for item in raw_dataset_ids):
|
||||
console_ns.abort(400, message="dataset_ids must be a list of strings.")
|
||||
dataset_ids = list(dict.fromkeys(raw_dataset_ids))
|
||||
|
||||
if dataset_ids:
|
||||
unknown = dataset_api_key_service.find_unknown_dataset_ids(session, dataset_ids, current_tenant_id)
|
||||
if unknown:
|
||||
console_ns.abort(400, message=f"Unknown knowledge base id(s): {', '.join(unknown)}")
|
||||
|
||||
current_key_count = (
|
||||
session.scalar(
|
||||
select(func.count(ApiToken.id)).where(
|
||||
@@ -1143,7 +1171,13 @@ class DatasetApiKeyApi(Resource):
|
||||
api_token.type = self.resource_type
|
||||
session.add(api_token)
|
||||
session.flush()
|
||||
return dump_response(ApiKeyItem, api_token), 200
|
||||
dataset_api_key_service.bind_datasets(session, api_token.id, dataset_ids)
|
||||
session.flush()
|
||||
|
||||
# Reveal-once: the create response carries the full secret and its bound scope.
|
||||
item = ApiKeyItem.model_validate(api_token, from_attributes=True)
|
||||
item.dataset_ids = dataset_ids
|
||||
return dump_response(ApiKeyItem, item), 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/api-keys/<uuid:api_key_id>")
|
||||
|
||||
@@ -12,7 +12,7 @@ from flask_restx import Resource
|
||||
from flask_restx.utils import merge
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from werkzeug.exceptions import Forbidden, NotFound, ServiceUnavailable, Unauthorized
|
||||
|
||||
from configs import dify_config
|
||||
@@ -30,6 +30,7 @@ from libs.login import current_user
|
||||
from models import Account, Tenant, TenantAccountJoin, TenantStatus
|
||||
from models.dataset import Dataset, RateLimitLog
|
||||
from models.model import ApiToken, App
|
||||
from services import dataset_api_key_service
|
||||
from services.api_token_service import ApiTokenCache, fetch_token_with_single_flight, record_token_usage
|
||||
from services.end_user_service import EndUserService
|
||||
from services.feature_service import FeatureService
|
||||
@@ -325,6 +326,18 @@ def validate_dataset_token[R](view: Callable[..., R]) -> Callable[..., R]:
|
||||
except Exception:
|
||||
logger.exception("Failed to parse dataset_id from positional args")
|
||||
|
||||
# Per-knowledge-base scoping is expressed by DatasetApiTokenBinding rows:
|
||||
# no rows -> the key can reach every dataset in its tenant (default / back-compat)
|
||||
# N rows -> the key is limited to exactly those datasets
|
||||
# A bound key may only call endpoints carrying one of its dataset ids; endpoints
|
||||
# without a dataset id (e.g. list/create datasets) are rejected. The set is queried
|
||||
# per request (not cached) so scope changes take effect immediately.
|
||||
# db.session is Flask-SQLAlchemy's scoped_session proxy; cast so the plain-Session
|
||||
# typed helper accepts it (runtime proxies every Session method through unchanged).
|
||||
bound_dataset_ids = dataset_api_key_service.get_bound_dataset_ids(cast(Session, db.session), api_token.id)
|
||||
if bound_dataset_ids and (not dataset_id or str(dataset_id) not in bound_dataset_ids):
|
||||
raise Forbidden("The API key is not authorized to access this knowledge base.")
|
||||
|
||||
if dataset_id:
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = db.session.scalar(
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"""add dataset_api_token_bindings
|
||||
|
||||
Revision ID: c3f1a9b2e6d4
|
||||
Revises: 5578e028b2f2
|
||||
Create Date: 2026-08-29 12:00:00.000000
|
||||
|
||||
Many-to-many binding table scoping a dataset service-API key to knowledge bases:
|
||||
|
||||
no binding rows for a token → key can access every dataset in its tenant (default)
|
||||
N binding rows for a token → key is limited to exactly those N datasets
|
||||
|
||||
Both foreign keys cascade on delete, so removing a key or a dataset drops its bindings.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import models as models
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c3f1a9b2e6d4"
|
||||
down_revision = "5578e028b2f2"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
"dataset_api_token_bindings",
|
||||
sa.Column("id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("api_token_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("dataset_id", models.types.StringUUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["api_token_id"], ["api_tokens.id"], name=op.f("dataset_api_token_binding_token_fkey"), ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["dataset_id"], ["datasets.id"], name=op.f("dataset_api_token_binding_dataset_fkey"), ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name="dataset_api_token_binding_pkey"),
|
||||
sa.UniqueConstraint("api_token_id", "dataset_id", name="dataset_api_token_binding_unique"),
|
||||
)
|
||||
with op.batch_alter_table("dataset_api_token_bindings", schema=None) as batch_op:
|
||||
batch_op.create_index("dataset_api_token_binding_token_idx", ["api_token_id"], unique=False)
|
||||
batch_op.create_index("dataset_api_token_binding_dataset_idx", ["dataset_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("dataset_api_token_bindings", schema=None) as batch_op:
|
||||
batch_op.drop_index("dataset_api_token_binding_dataset_idx")
|
||||
batch_op.drop_index("dataset_api_token_binding_token_idx")
|
||||
op.drop_table("dataset_api_token_bindings")
|
||||
@@ -77,6 +77,7 @@ from .model import (
|
||||
AppModelConfig,
|
||||
AppStar,
|
||||
Conversation,
|
||||
DatasetApiTokenBinding,
|
||||
DatasetRetrieverResource,
|
||||
DifySetup,
|
||||
EndUser,
|
||||
@@ -204,6 +205,7 @@ __all__ = [
|
||||
"DataSourceApiKeyAuthBinding",
|
||||
"DataSourceOauthBinding",
|
||||
"Dataset",
|
||||
"DatasetApiTokenBinding",
|
||||
"DatasetCollectionBinding",
|
||||
"DatasetKeywordTable",
|
||||
"DatasetPermission",
|
||||
|
||||
+45
-1
@@ -2380,7 +2380,22 @@ class Site(Base):
|
||||
return dify_config.APP_WEB_URL or request.url_root.rstrip("/")
|
||||
|
||||
|
||||
class ApiToken(Base): # bug: this uses setattr so idk the field.
|
||||
class ApiToken(Base):
|
||||
"""API token for the service API.
|
||||
|
||||
Scoping rules:
|
||||
- ``type`` = "app": ``app_id`` points at the app the key serves.
|
||||
- ``type`` = "dataset": ``tenant_id`` is always set. Per-knowledge-base scoping is
|
||||
expressed with ``DatasetApiTokenBinding`` rows (a key with no binding rows can
|
||||
reach every dataset in the tenant — the default and the pre-scoping behavior; a
|
||||
key with binding rows is limited to exactly those datasets). Enforcement lives in
|
||||
``validate_dataset_token`` (controllers/service_api/wraps.py).
|
||||
|
||||
Note: controllers/console/apikey.py assigns the ``*_id`` columns via ``setattr``
|
||||
keyed on ``resource_id_field``, so renaming ``app_id`` requires updating those
|
||||
controllers too.
|
||||
"""
|
||||
|
||||
__tablename__ = "api_tokens"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="api_token_pkey"),
|
||||
@@ -2406,6 +2421,35 @@ class ApiToken(Base): # bug: this uses setattr so idk the field.
|
||||
return result
|
||||
|
||||
|
||||
class DatasetApiTokenBinding(Base):
|
||||
"""Binds a dataset service-API key to a single knowledge base.
|
||||
|
||||
A dataset ``ApiToken`` may have zero or more of these rows:
|
||||
- no rows → the key can access every dataset in its tenant (default / back-compat).
|
||||
- N rows → the key is restricted to exactly those N datasets.
|
||||
|
||||
Both foreign keys cascade on delete, so removing a key or a dataset automatically
|
||||
drops the corresponding bindings (no dangling scope).
|
||||
"""
|
||||
|
||||
__tablename__ = "dataset_api_token_bindings"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="dataset_api_token_binding_pkey"),
|
||||
sa.UniqueConstraint("api_token_id", "dataset_id", name="dataset_api_token_binding_unique"),
|
||||
sa.Index("dataset_api_token_binding_token_idx", "api_token_id"),
|
||||
sa.Index("dataset_api_token_binding_dataset_idx", "dataset_id"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuid4()))
|
||||
api_token_id: Mapped[str] = mapped_column(
|
||||
StringUUID, sa.ForeignKey("api_tokens.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
dataset_id: Mapped[str] = mapped_column(
|
||||
StringUUID, sa.ForeignKey("datasets.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
|
||||
|
||||
class UploadFile(TypeBase):
|
||||
__tablename__ = "upload_files"
|
||||
__table_args__ = (
|
||||
|
||||
@@ -5132,6 +5132,12 @@ Get dataset API keys
|
||||
| 200 | API keys retrieved successfully | **application/json**: [ApiKeyList](#apikeylist)<br> |
|
||||
|
||||
### [POST] /datasets/api-keys
|
||||
#### Request Body
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [DatasetApiKeyCreatePayload](#datasetapikeycreatepayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
@@ -15303,6 +15309,7 @@ Legacy Chat App model config used only for follow-up question generation.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| created_at | integer | | No |
|
||||
| dataset_ids | [ string ], <br>**Default:** | | No |
|
||||
| id | string | | Yes |
|
||||
| last_used_at | integer | | No |
|
||||
| token | string | | Yes |
|
||||
@@ -16801,6 +16808,12 @@ Model class for provider custom model configuration.
|
||||
| dataset | [DatasetResponse](#datasetresponse) | | Yes |
|
||||
| documents | [ [DocumentResponse](#documentresponse) ] | | Yes |
|
||||
|
||||
#### DatasetApiKeyCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| dataset_ids | [ string ] | | No |
|
||||
|
||||
#### DatasetCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@@ -43,6 +43,10 @@ class CachedApiToken(BaseModel):
|
||||
last_used_at: datetime | None
|
||||
created_at: datetime | None
|
||||
|
||||
# Dataset key scoping (DatasetApiTokenBinding) is intentionally NOT cached here:
|
||||
# validate_dataset_token queries the bound dataset ids per request so scope changes
|
||||
# take effect immediately. Unknown fields in older cache entries are ignored.
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"<CachedApiToken id={self.id} type={self.type}>"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Persistence helpers for dataset (knowledge base) API key scoping.
|
||||
|
||||
A dataset API key is scoped to specific knowledge bases through
|
||||
``DatasetApiTokenBinding`` rows:
|
||||
|
||||
- no rows -> the key can access every dataset in its tenant (default / back-compat)
|
||||
- N rows -> the key is restricted to exactly those datasets
|
||||
|
||||
These helpers live in the service layer so controllers and the service-API auth
|
||||
decorator can share the queries without embedding SQLAlchemy in controller code.
|
||||
The caller owns the transaction boundary (commit/flush).
|
||||
"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.dataset import Dataset
|
||||
from models.model import ApiToken, DatasetApiTokenBinding
|
||||
|
||||
|
||||
def get_bound_dataset_ids(session: Session, api_token_id: str) -> set[str]:
|
||||
"""Return the dataset ids a key is scoped to (empty set means unrestricted)."""
|
||||
return {
|
||||
str(dataset_id)
|
||||
for dataset_id in session.scalars(
|
||||
select(DatasetApiTokenBinding.dataset_id).where(DatasetApiTokenBinding.api_token_id == api_token_id)
|
||||
).all()
|
||||
}
|
||||
|
||||
|
||||
def list_bindings_by_token(session: Session, token_ids: Iterable[str]) -> dict[str, list[str]]:
|
||||
"""Group bound dataset ids by api token id for the given tokens."""
|
||||
ids = [str(token_id) for token_id in token_ids]
|
||||
bindings_by_token: dict[str, list[str]] = {}
|
||||
if not ids:
|
||||
return bindings_by_token
|
||||
rows = session.execute(
|
||||
select(DatasetApiTokenBinding.api_token_id, DatasetApiTokenBinding.dataset_id).where(
|
||||
DatasetApiTokenBinding.api_token_id.in_(ids)
|
||||
)
|
||||
).all()
|
||||
for token_id, dataset_id in rows:
|
||||
bindings_by_token.setdefault(str(token_id), []).append(str(dataset_id))
|
||||
return bindings_by_token
|
||||
|
||||
|
||||
def find_unknown_dataset_ids(session: Session, dataset_ids: list[str], tenant_id: str) -> list[str]:
|
||||
"""Return the dataset ids that do not belong to the tenant (preserving order)."""
|
||||
if not dataset_ids:
|
||||
return []
|
||||
existing = {
|
||||
str(dataset_id)
|
||||
for dataset_id in session.scalars(
|
||||
select(Dataset.id).where(Dataset.id.in_(dataset_ids), Dataset.tenant_id == tenant_id)
|
||||
).all()
|
||||
}
|
||||
return [dataset_id for dataset_id in dataset_ids if dataset_id not in existing]
|
||||
|
||||
|
||||
def bind_datasets(session: Session, api_token_id: str, dataset_ids: Iterable[str]) -> None:
|
||||
"""Add one binding row per dataset id. The caller controls the transaction."""
|
||||
for dataset_id in dataset_ids:
|
||||
session.add(DatasetApiTokenBinding(api_token_id=api_token_id, dataset_id=dataset_id))
|
||||
|
||||
|
||||
def delete_keys_scoped_only_to(session: Session, dataset_id: str) -> list[str]:
|
||||
"""Delete dataset API keys whose only binding is ``dataset_id``.
|
||||
|
||||
A deleted knowledge base cascades its bindings away. A key bound solely to it
|
||||
would otherwise drop to zero bindings and silently become unrestricted
|
||||
(access-all) under the "no bindings = access all" rule. To keep scope from
|
||||
escalating, delete those keys (their remaining rows cascade with the token).
|
||||
Keys also bound to other datasets keep those bindings and survive.
|
||||
|
||||
Must run before the dataset row is deleted (so the bindings still exist).
|
||||
Returns the deleted api_token ids; the caller controls the transaction.
|
||||
"""
|
||||
orphan_ids = [
|
||||
str(token_id)
|
||||
for token_id in session.scalars(
|
||||
select(DatasetApiTokenBinding.api_token_id)
|
||||
.where(DatasetApiTokenBinding.dataset_id == dataset_id)
|
||||
.where(
|
||||
DatasetApiTokenBinding.api_token_id.notin_(
|
||||
select(DatasetApiTokenBinding.api_token_id).where(DatasetApiTokenBinding.dataset_id != dataset_id)
|
||||
)
|
||||
)
|
||||
).all()
|
||||
]
|
||||
if orphan_ids:
|
||||
session.execute(delete(ApiToken).where(ApiToken.id.in_(orphan_ids)))
|
||||
return orphan_ids
|
||||
@@ -64,6 +64,7 @@ from models.model import UploadFile
|
||||
from models.provider_ids import ModelProviderID
|
||||
from models.source import DataSourceOauthBinding
|
||||
from models.workflow import Workflow
|
||||
from services import dataset_api_key_service
|
||||
from services.dataset_ref_service import DatasetRef, DatasetRefService, SegmentRef
|
||||
from services.document_indexing_proxy.document_indexing_task_proxy import DocumentIndexingTaskProxy
|
||||
from services.document_indexing_proxy.duplicate_document_indexing_task_proxy import DuplicateDocumentIndexingTaskProxy
|
||||
@@ -1360,6 +1361,10 @@ class DatasetService:
|
||||
|
||||
dataset_was_deleted.send(dataset)
|
||||
|
||||
# Remove any dataset API key scoped only to this knowledge base, so it cannot
|
||||
# silently degrade to unrestricted (access-all) once its last binding is gone.
|
||||
dataset_api_key_service.delete_keys_scoped_only_to(session, str(dataset.id))
|
||||
|
||||
session.delete(dataset)
|
||||
session.commit()
|
||||
return True
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
@@ -12,8 +13,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from models import Account
|
||||
from models.account import AccountStatus, TenantAccountRole
|
||||
from models.enums import ApiTokenType
|
||||
from models.model import ApiToken, App, AppMode
|
||||
from models.dataset import Dataset
|
||||
from models.enums import ApiTokenType, DataSourceType
|
||||
from models.model import ApiToken, App, AppMode, DatasetApiTokenBinding
|
||||
from tests.test_containers_integration_tests.controllers.console.helpers import (
|
||||
authenticate_console_client,
|
||||
create_console_account_and_tenant,
|
||||
@@ -33,6 +35,28 @@ def setup_app(
|
||||
return test_client_with_containers, headers, app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_dataset(
|
||||
db_session_with_containers: Session,
|
||||
test_client_with_containers: FlaskClient,
|
||||
) -> tuple[FlaskClient, dict[str, str], Dataset]:
|
||||
"""Create an authenticated client with a dataset for per-dataset API key tests."""
|
||||
account, tenant = create_console_account_and_tenant(db_session_with_containers)
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant.id,
|
||||
name=f"API Key Dataset {uuid4()}",
|
||||
description="Dataset for API key scoping tests",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=account.id,
|
||||
permission="only_me",
|
||||
provider="vendor",
|
||||
)
|
||||
db_session_with_containers.add(dataset)
|
||||
db_session_with_containers.commit()
|
||||
headers = authenticate_console_client(test_client_with_containers, account)
|
||||
return test_client_with_containers, headers, dataset
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_api_tokens(db_session_with_containers: Session):
|
||||
"""Remove API tokens created during each test."""
|
||||
@@ -184,3 +208,204 @@ class TestAppApiKeyResource:
|
||||
):
|
||||
with pytest.raises(Forbidden):
|
||||
BaseApiKeyResource.delete(resource, "rid", "kid", "tenant-id", non_admin)
|
||||
|
||||
|
||||
class TestDatasetApiKeyListResource:
|
||||
"""Tests for GET/POST /datasets/api-keys with per-knowledge-base scope.
|
||||
|
||||
Scope is expressed by DatasetApiTokenBinding rows: absent/empty ``dataset_ids``
|
||||
on create makes an unrestricted key; a non-empty list binds the key to exactly
|
||||
those knowledge bases.
|
||||
"""
|
||||
|
||||
def _bound_dataset_ids(self, session: Session, api_token_id: str) -> set[str]:
|
||||
return {
|
||||
str(dataset_id)
|
||||
for dataset_id in session.scalars(
|
||||
select(DatasetApiTokenBinding.dataset_id).where(DatasetApiTokenBinding.api_token_id == api_token_id)
|
||||
).all()
|
||||
}
|
||||
|
||||
def test_create_unbound_key(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
db_session_with_containers: Session,
|
||||
) -> None:
|
||||
client, headers, dataset = setup_dataset
|
||||
tenant_id = dataset.tenant_id
|
||||
|
||||
resp = client.post("/console/api/datasets/api-keys", headers=headers)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json is not None
|
||||
assert resp.json["token"].startswith("dataset-")
|
||||
assert resp.json["dataset_ids"] == []
|
||||
api_token = db_session_with_containers.scalar(select(ApiToken).where(ApiToken.id == resp.json["id"]))
|
||||
assert api_token is not None
|
||||
assert api_token.tenant_id == tenant_id
|
||||
assert api_token.type == ApiTokenType.DATASET
|
||||
# No bindings -> unrestricted key.
|
||||
assert self._bound_dataset_ids(db_session_with_containers, resp.json["id"]) == set()
|
||||
|
||||
def test_create_scoped_key_persists_bindings(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
db_session_with_containers: Session,
|
||||
) -> None:
|
||||
client, headers, dataset = setup_dataset
|
||||
dataset_id = dataset.id
|
||||
|
||||
resp = client.post(
|
||||
"/console/api/datasets/api-keys",
|
||||
headers=headers,
|
||||
json={"dataset_ids": [dataset_id]},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json is not None
|
||||
assert resp.json["dataset_ids"] == [dataset_id]
|
||||
assert self._bound_dataset_ids(db_session_with_containers, resp.json["id"]) == {dataset_id}
|
||||
|
||||
def test_create_rejects_dataset_outside_tenant(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
) -> None:
|
||||
"""A dataset id that does not belong to the tenant is rejected with 400."""
|
||||
client, headers, _ = setup_dataset
|
||||
|
||||
resp = client.post(
|
||||
"/console/api/datasets/api-keys",
|
||||
headers=headers,
|
||||
json={"dataset_ids": [str(uuid4())]},
|
||||
)
|
||||
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_list_reports_scope_for_each_key(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
db_session_with_containers: Session,
|
||||
) -> None:
|
||||
client, headers, dataset = setup_dataset
|
||||
dataset_id = dataset.id
|
||||
|
||||
scoped_resp = client.post(
|
||||
"/console/api/datasets/api-keys",
|
||||
headers=headers,
|
||||
json={"dataset_ids": [dataset_id]},
|
||||
)
|
||||
assert scoped_resp.status_code == 200
|
||||
unbound_resp = client.post("/console/api/datasets/api-keys", headers=headers)
|
||||
assert unbound_resp.status_code == 200
|
||||
|
||||
resp = client.get("/console/api/datasets/api-keys", headers=headers)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json is not None
|
||||
assert scoped_resp.json is not None
|
||||
assert unbound_resp.json is not None
|
||||
scopes = {item["id"]: item["dataset_ids"] for item in resp.json["data"]}
|
||||
assert scopes[scoped_resp.json["id"]] == [dataset_id]
|
||||
assert scopes[unbound_resp.json["id"]] == []
|
||||
# reveal-once: the list returns masked tokens, never the full secret.
|
||||
listed = {item["id"]: item["token"] for item in resp.json["data"]}
|
||||
assert listed[scoped_resp.json["id"]] != scoped_resp.json["token"]
|
||||
|
||||
|
||||
class TestDatasetApiKeyResource:
|
||||
"""Tests for DELETE /datasets/api-keys/<api_key_id>."""
|
||||
|
||||
def test_delete_key_removes_bindings(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
db_session_with_containers: Session,
|
||||
) -> None:
|
||||
client, headers, dataset = setup_dataset
|
||||
dataset_id = dataset.id
|
||||
create_resp = client.post(
|
||||
"/console/api/datasets/api-keys",
|
||||
headers=headers,
|
||||
json={"dataset_ids": [dataset_id]},
|
||||
)
|
||||
assert create_resp.json is not None
|
||||
api_key_id = create_resp.json["id"]
|
||||
|
||||
resp = client.delete(f"/console/api/datasets/api-keys/{api_key_id}", headers=headers)
|
||||
|
||||
assert resp.status_code == 204
|
||||
assert db_session_with_containers.scalar(select(ApiToken).where(ApiToken.id == api_key_id)) is None
|
||||
# The binding rows cascade away with the deleted key.
|
||||
remaining = db_session_with_containers.scalars(
|
||||
select(DatasetApiTokenBinding).where(DatasetApiTokenBinding.api_token_id == api_key_id)
|
||||
).all()
|
||||
assert remaining == []
|
||||
|
||||
|
||||
class TestDatasetDeleteCascadesToScopedKeys:
|
||||
"""Deleting a knowledge base must not let a scoped key degrade to access-all."""
|
||||
|
||||
def _create_other_dataset(self, session: Session, tenant_id: str, created_by: str) -> Dataset:
|
||||
other = Dataset(
|
||||
tenant_id=tenant_id,
|
||||
name=f"Other Dataset {uuid4()}",
|
||||
description="Second dataset",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=created_by,
|
||||
permission="only_me",
|
||||
provider="vendor",
|
||||
)
|
||||
session.add(other)
|
||||
session.commit()
|
||||
return other
|
||||
|
||||
def test_deleting_last_bound_dataset_deletes_the_scoped_key(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
db_session_with_containers: Session,
|
||||
) -> None:
|
||||
"""A key bound only to the deleted dataset is removed, never left unrestricted."""
|
||||
client, headers, dataset = setup_dataset
|
||||
dataset_id = dataset.id
|
||||
create_resp = client.post(
|
||||
"/console/api/datasets/api-keys",
|
||||
headers=headers,
|
||||
json={"dataset_ids": [dataset_id]},
|
||||
)
|
||||
assert create_resp.json is not None
|
||||
api_key_id = create_resp.json["id"]
|
||||
|
||||
resp = client.delete(f"/console/api/datasets/{dataset_id}", headers=headers)
|
||||
|
||||
assert resp.status_code == 204
|
||||
assert db_session_with_containers.scalar(select(ApiToken).where(ApiToken.id == api_key_id)) is None
|
||||
|
||||
def test_deleting_one_bound_dataset_keeps_a_multi_scoped_key(
|
||||
self,
|
||||
setup_dataset: tuple[FlaskClient, dict[str, str], Dataset],
|
||||
db_session_with_containers: Session,
|
||||
) -> None:
|
||||
"""A key bound to several datasets survives and just drops the deleted one."""
|
||||
client, headers, dataset = setup_dataset
|
||||
dataset_id = dataset.id
|
||||
other = self._create_other_dataset(db_session_with_containers, dataset.tenant_id, dataset.created_by)
|
||||
other_id = other.id
|
||||
create_resp = client.post(
|
||||
"/console/api/datasets/api-keys",
|
||||
headers=headers,
|
||||
json={"dataset_ids": [dataset_id, other_id]},
|
||||
)
|
||||
assert create_resp.json is not None
|
||||
api_key_id = create_resp.json["id"]
|
||||
|
||||
resp = client.delete(f"/console/api/datasets/{dataset_id}", headers=headers)
|
||||
|
||||
assert resp.status_code == 204
|
||||
# The key survives, still scoped to the remaining dataset only.
|
||||
assert db_session_with_containers.scalar(select(ApiToken).where(ApiToken.id == api_key_id)) is not None
|
||||
remaining = {
|
||||
str(ds_id)
|
||||
for ds_id in db_session_with_containers.scalars(
|
||||
select(DatasetApiTokenBinding.dataset_id).where(DatasetApiTokenBinding.api_token_id == api_key_id)
|
||||
).all()
|
||||
}
|
||||
assert remaining == {other_id}
|
||||
|
||||
@@ -1404,14 +1404,14 @@ class TestDatasetApiKeyApi(_UsesSQLiteSession):
|
||||
mock_key_1 = ApiToken(
|
||||
id="key-1",
|
||||
type="dataset",
|
||||
token="ds-abc",
|
||||
token="dataset-aaaa1111bbbb",
|
||||
last_used_at=None,
|
||||
created_at=None,
|
||||
)
|
||||
mock_key_2 = ApiToken(
|
||||
id="key-2",
|
||||
type="dataset",
|
||||
token="ds-def",
|
||||
token="dataset-cccc2222dddd",
|
||||
last_used_at=None,
|
||||
created_at=None,
|
||||
)
|
||||
@@ -1424,10 +1424,11 @@ class TestDatasetApiKeyApi(_UsesSQLiteSession):
|
||||
response = method(api, session, "tenant-1")
|
||||
assert "data" in response
|
||||
assert len(response["data"]) == 2
|
||||
# reveal-once: the list returns masked tokens, never the full secret
|
||||
assert response["data"][0]["id"] == "key-1"
|
||||
assert response["data"][0]["token"] == "ds-abc"
|
||||
assert response["data"][0]["token"] == "datas...bbbb"
|
||||
assert response["data"][1]["id"] == "key-2"
|
||||
assert response["data"][1]["token"] == "ds-def"
|
||||
assert response["data"][1]["token"] == "datas...dddd"
|
||||
|
||||
def test_post_create_api_key_success(self, app: Flask):
|
||||
api = DatasetApiKeyApi()
|
||||
@@ -1466,6 +1467,72 @@ class TestDatasetApiKeyApi(_UsesSQLiteSession):
|
||||
"custom": "max_keys_exceeded",
|
||||
}
|
||||
|
||||
def test_get_api_keys_include_bound_dataset_ids(self, app: Flask):
|
||||
api = DatasetApiKeyApi()
|
||||
method = unwrap(api.get)
|
||||
mock_key = MagicMock(spec=ApiToken)
|
||||
mock_key.id = "key-1"
|
||||
mock_key.type = "dataset"
|
||||
mock_key.token = "dataset-aaaa1111bbbb"
|
||||
mock_key.last_used_at = None
|
||||
mock_key.created_at = None
|
||||
session = MagicMock()
|
||||
session.scalars.return_value.all.return_value = [mock_key]
|
||||
# Two binding rows for the same key -> the masked list surfaces both dataset ids.
|
||||
session.execute.return_value.all.return_value = [("key-1", "ds-1"), ("key-1", "ds-2")]
|
||||
with app.test_request_context("/"):
|
||||
response = method(api, session, "tenant-1")
|
||||
assert response["data"][0]["dataset_ids"] == ["ds-1", "ds-2"]
|
||||
|
||||
def test_post_create_scoped_key_persists_bindings(self, app: Flask):
|
||||
api = DatasetApiKeyApi()
|
||||
method = unwrap(api.post)
|
||||
mock_token = MagicMock()
|
||||
mock_token.id = "new-key-id"
|
||||
mock_token.last_used_at = None
|
||||
mock_token.created_at = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
|
||||
mock_api_token_cls = MagicMock()
|
||||
mock_api_token_cls.return_value = mock_token
|
||||
mock_api_token_cls.generate_api_key.return_value = "dataset-abc123"
|
||||
session = MagicMock()
|
||||
# Dataset-ownership validation returns exactly the requested ids (all belong to tenant).
|
||||
session.scalars.return_value.all.return_value = ["ds-1", "ds-2"]
|
||||
session.scalar.return_value = 1
|
||||
with (
|
||||
app.test_request_context("/", json={"dataset_ids": ["ds-1", "ds-2", "ds-1"]}),
|
||||
patch("controllers.console.datasets.datasets.ApiToken", mock_api_token_cls),
|
||||
):
|
||||
response, status = method(api, session, "tenant-1")
|
||||
assert status == 200
|
||||
# Duplicates are collapsed and returned in the reveal-once response.
|
||||
assert response["dataset_ids"] == ["ds-1", "ds-2"]
|
||||
# One binding row is added per unique dataset id (plus the token itself).
|
||||
added = [call.args[0] for call in session.add.call_args_list]
|
||||
binding_dataset_ids = [obj.dataset_id for obj in added if obj.__class__.__name__ == "DatasetApiTokenBinding"]
|
||||
assert binding_dataset_ids == ["ds-1", "ds-2"]
|
||||
|
||||
def test_post_rejects_dataset_ids_from_another_tenant(self, app: Flask):
|
||||
api = DatasetApiKeyApi()
|
||||
method = unwrap(api.post)
|
||||
session = MagicMock()
|
||||
# None of the requested ids belong to this tenant.
|
||||
session.scalars.return_value.all.return_value = []
|
||||
with app.test_request_context("/", json={"dataset_ids": ["foreign-ds"]}):
|
||||
with pytest.raises(BadRequest) as exc_info:
|
||||
method(api, session, "tenant-1")
|
||||
assert exc_info.value.code == 400
|
||||
assert "Unknown knowledge base id(s)" in vars(exc_info.value)["data"]["message"]
|
||||
|
||||
def test_post_rejects_non_list_dataset_ids(self, app: Flask):
|
||||
api = DatasetApiKeyApi()
|
||||
method = unwrap(api.post)
|
||||
session = MagicMock()
|
||||
with app.test_request_context("/", json={"dataset_ids": "not-a-list"}):
|
||||
with pytest.raises(BadRequest) as exc_info:
|
||||
method(api, session, "tenant-1")
|
||||
assert exc_info.value.code == 400
|
||||
assert vars(exc_info.value)["data"]["message"] == "dataset_ids must be a list of strings."
|
||||
|
||||
|
||||
class TestDatasetApiDeleteApi(_UsesSQLiteSession):
|
||||
def test_delete_success(self, app: Flask):
|
||||
|
||||
@@ -29,7 +29,7 @@ from models import Account, Tenant, TenantAccountJoin
|
||||
from models.account import TenantAccountRole
|
||||
from models.dataset import Dataset, RateLimitLog
|
||||
from models.enums import ApiTokenType
|
||||
from models.model import ApiToken, App, AppMode, IconType
|
||||
from models.model import ApiToken, App, AppMode, DatasetApiTokenBinding, IconType
|
||||
from tests.unit_tests.config_override import config_overrides_context
|
||||
|
||||
|
||||
@@ -718,6 +718,94 @@ class TestValidateDatasetToken:
|
||||
protected_view(dataset_id=str(uuid.uuid4()))
|
||||
assert "Dataset not found" in str(exc_info.value)
|
||||
|
||||
# Per-knowledge-base scope enforcement (DatasetApiTokenBinding rows):
|
||||
# no rows -> the key reaches every dataset in its tenant (default / back-compat)
|
||||
# N rows -> the key is restricted to exactly those datasets
|
||||
# The "reaches the lookup" tests assert a downstream NotFound (the target dataset is
|
||||
# intentionally not persisted), proving the binding gate let the request through rather
|
||||
# than raising its own Forbidden.
|
||||
|
||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||
def test_unbound_key_is_not_scope_restricted(self, mock_validate_token, app: Flask, sqlite_session: Session):
|
||||
"""A key with no bindings passes the scope gate and proceeds to the dataset lookup."""
|
||||
api_token = _api_token(tenant_id=str(uuid.uuid4()), token_type=ApiTokenType.DATASET)
|
||||
mock_validate_token.return_value = api_token
|
||||
|
||||
@validate_dataset_token
|
||||
def protected_view(**kwargs):
|
||||
return {"success": True}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET", headers={"Authorization": "Bearer test_token"}),
|
||||
patch("controllers.service_api.wraps.db.session", _session_proxy(sqlite_session)),
|
||||
):
|
||||
with pytest.raises(NotFound):
|
||||
protected_view(dataset_id=str(uuid.uuid4()))
|
||||
|
||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||
def test_bound_key_rejects_other_dataset(self, mock_validate_token, app: Flask, sqlite_session: Session):
|
||||
"""A key bound to one dataset is forbidden from reaching a different dataset."""
|
||||
api_token = _api_token(tenant_id=str(uuid.uuid4()), token_type=ApiTokenType.DATASET)
|
||||
mock_validate_token.return_value = api_token
|
||||
sqlite_session.add(DatasetApiTokenBinding(api_token_id=api_token.id, dataset_id=str(uuid.uuid4())))
|
||||
sqlite_session.commit()
|
||||
|
||||
@validate_dataset_token
|
||||
def protected_view(**kwargs):
|
||||
return {"success": True}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET", headers={"Authorization": "Bearer test_token"}),
|
||||
patch("controllers.service_api.wraps.db.session", _session_proxy(sqlite_session)),
|
||||
):
|
||||
with pytest.raises(Forbidden) as exc_info:
|
||||
protected_view(dataset_id=str(uuid.uuid4()))
|
||||
assert "not authorized to access this knowledge base" in str(exc_info.value)
|
||||
|
||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||
def test_bound_key_rejects_endpoint_without_dataset_id(
|
||||
self, mock_validate_token, app: Flask, sqlite_session: Session
|
||||
):
|
||||
"""A scoped key cannot call collection endpoints (list/create) that carry no dataset id."""
|
||||
api_token = _api_token(tenant_id=str(uuid.uuid4()), token_type=ApiTokenType.DATASET)
|
||||
mock_validate_token.return_value = api_token
|
||||
sqlite_session.add(DatasetApiTokenBinding(api_token_id=api_token.id, dataset_id=str(uuid.uuid4())))
|
||||
sqlite_session.commit()
|
||||
|
||||
@validate_dataset_token
|
||||
def protected_view(**kwargs):
|
||||
return {"success": True}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET", headers={"Authorization": "Bearer test_token"}),
|
||||
patch("controllers.service_api.wraps.db.session", _session_proxy(sqlite_session)),
|
||||
):
|
||||
with pytest.raises(Forbidden) as exc_info:
|
||||
protected_view()
|
||||
assert "not authorized to access this knowledge base" in str(exc_info.value)
|
||||
|
||||
@patch("controllers.service_api.wraps.validate_and_get_api_token")
|
||||
def test_bound_key_reaches_allowed_dataset(self, mock_validate_token, app: Flask, sqlite_session: Session):
|
||||
"""A scoped key targeting one of its bound datasets passes the scope gate."""
|
||||
api_token = _api_token(tenant_id=str(uuid.uuid4()), token_type=ApiTokenType.DATASET)
|
||||
mock_validate_token.return_value = api_token
|
||||
allowed_dataset_id = str(uuid.uuid4())
|
||||
sqlite_session.add(DatasetApiTokenBinding(api_token_id=api_token.id, dataset_id=allowed_dataset_id))
|
||||
sqlite_session.commit()
|
||||
|
||||
@validate_dataset_token
|
||||
def protected_view(**kwargs):
|
||||
return {"success": True}
|
||||
|
||||
with (
|
||||
app.test_request_context("/", method="GET", headers={"Authorization": "Bearer test_token"}),
|
||||
patch("controllers.service_api.wraps.db.session", _session_proxy(sqlite_session)),
|
||||
):
|
||||
# The bound dataset is allowed by scope; it is simply not persisted, so the
|
||||
# downstream lookup raises NotFound instead of the scope Forbidden.
|
||||
with pytest.raises(NotFound):
|
||||
protected_view(dataset_id=allowed_dataset_id)
|
||||
|
||||
|
||||
class TestFetchUserArg:
|
||||
"""Test suite for FetchUserArg model"""
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Unit tests for dataset API key scoping helpers."""
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.enums import ApiTokenType
|
||||
from models.model import ApiToken, DatasetApiTokenBinding
|
||||
from services import dataset_api_key_service
|
||||
|
||||
|
||||
def _dataset_token(session: Session) -> ApiToken:
|
||||
token = ApiToken(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=str(uuid.uuid4()),
|
||||
type=ApiTokenType.DATASET,
|
||||
token=f"dataset-{uuid.uuid4().hex}",
|
||||
)
|
||||
session.add(token)
|
||||
return token
|
||||
|
||||
|
||||
class TestDeleteKeysScopedOnlyTo:
|
||||
def test_deletes_key_bound_only_to_the_dataset(self, sqlite_session: Session) -> None:
|
||||
dataset_id = str(uuid.uuid4())
|
||||
token = _dataset_token(sqlite_session)
|
||||
sqlite_session.add(DatasetApiTokenBinding(api_token_id=token.id, dataset_id=dataset_id))
|
||||
sqlite_session.commit()
|
||||
|
||||
deleted = dataset_api_key_service.delete_keys_scoped_only_to(sqlite_session, dataset_id)
|
||||
sqlite_session.commit()
|
||||
|
||||
assert deleted == [token.id]
|
||||
assert sqlite_session.scalar(select(ApiToken).where(ApiToken.id == token.id)) is None
|
||||
|
||||
def test_keeps_key_bound_to_other_datasets(self, sqlite_session: Session) -> None:
|
||||
dataset_id = str(uuid.uuid4())
|
||||
other_dataset_id = str(uuid.uuid4())
|
||||
token = _dataset_token(sqlite_session)
|
||||
sqlite_session.add(DatasetApiTokenBinding(api_token_id=token.id, dataset_id=dataset_id))
|
||||
sqlite_session.add(DatasetApiTokenBinding(api_token_id=token.id, dataset_id=other_dataset_id))
|
||||
sqlite_session.commit()
|
||||
|
||||
deleted = dataset_api_key_service.delete_keys_scoped_only_to(sqlite_session, dataset_id)
|
||||
sqlite_session.commit()
|
||||
|
||||
# The key is bound elsewhere, so it survives and stays scoped.
|
||||
assert deleted == []
|
||||
assert sqlite_session.scalar(select(ApiToken).where(ApiToken.id == token.id)) is not None
|
||||
|
||||
def test_ignores_unrestricted_keys(self, sqlite_session: Session) -> None:
|
||||
"""A key with no bindings (access-all) must never be touched."""
|
||||
dataset_id = str(uuid.uuid4())
|
||||
unbound = _dataset_token(sqlite_session)
|
||||
sqlite_session.commit()
|
||||
|
||||
deleted = dataset_api_key_service.delete_keys_scoped_only_to(sqlite_session, dataset_id)
|
||||
sqlite_session.commit()
|
||||
|
||||
assert deleted == []
|
||||
assert sqlite_session.scalar(select(ApiToken).where(ApiToken.id == unbound.id)) is not None
|
||||
@@ -107,6 +107,7 @@ export type ApiKeyList = {
|
||||
|
||||
export type ApiKeyItem = {
|
||||
created_at?: number | null
|
||||
dataset_ids?: Array<string>
|
||||
id: string
|
||||
last_used_at?: number | null
|
||||
token: string
|
||||
|
||||
@@ -35,6 +35,7 @@ export const zAgentApiStatusPayload = z.object({
|
||||
*/
|
||||
export const zApiKeyItem = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
dataset_ids: z.array(z.string()).optional().default([]),
|
||||
id: z.string(),
|
||||
last_used_at: z.int().nullish(),
|
||||
token: z.string(),
|
||||
|
||||
@@ -1207,6 +1207,7 @@ export type ApiKeyList = {
|
||||
|
||||
export type ApiKeyItem = {
|
||||
created_at?: number | null
|
||||
dataset_ids?: Array<string>
|
||||
id: string
|
||||
last_used_at?: number | null
|
||||
token: string
|
||||
|
||||
@@ -763,6 +763,7 @@ export const zWorkflowRestoreResponse = z.object({
|
||||
*/
|
||||
export const zApiKeyItem = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
dataset_ids: z.array(z.string()).optional().default([]),
|
||||
id: z.string(),
|
||||
last_used_at: z.int().nullish(),
|
||||
token: z.string(),
|
||||
|
||||
@@ -125,6 +125,7 @@ import {
|
||||
zPatchDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdBody,
|
||||
zPatchDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath,
|
||||
zPatchDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponse,
|
||||
zPostDatasetsApiKeysBody,
|
||||
zPostDatasetsApiKeysResponse,
|
||||
zPostDatasetsBatchImportStatusByJobIdBody,
|
||||
zPostDatasetsBatchImportStatusByJobIdPath,
|
||||
@@ -248,6 +249,7 @@ export const post = oc
|
||||
path: '/datasets/api-keys',
|
||||
tags: ['console'],
|
||||
})
|
||||
.input(z.object({ body: zPostDatasetsApiKeysBody }))
|
||||
.output(zPostDatasetsApiKeysResponse)
|
||||
|
||||
export const apiKeys = {
|
||||
|
||||
@@ -70,8 +70,13 @@ export type ApiKeyList = {
|
||||
data: Array<ApiKeyItem>
|
||||
}
|
||||
|
||||
export type DatasetApiKeyCreatePayload = {
|
||||
dataset_ids?: Array<string>
|
||||
}
|
||||
|
||||
export type ApiKeyItem = {
|
||||
created_at?: number | null
|
||||
dataset_ids?: Array<string>
|
||||
id: string
|
||||
last_used_at?: number | null
|
||||
token: string
|
||||
@@ -1155,7 +1160,7 @@ export type GetDatasetsApiKeysResponse =
|
||||
GetDatasetsApiKeysResponses[keyof GetDatasetsApiKeysResponses]
|
||||
|
||||
export type PostDatasetsApiKeysData = {
|
||||
body?: never
|
||||
body: DatasetApiKeyCreatePayload
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/datasets/api-keys'
|
||||
|
||||
@@ -9,11 +9,19 @@ export const zApiBaseUrlResponse = z.object({
|
||||
api_base_url: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
* DatasetApiKeyCreatePayload
|
||||
*/
|
||||
export const zDatasetApiKeyCreatePayload = z.object({
|
||||
dataset_ids: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* ApiKeyItem
|
||||
*/
|
||||
export const zApiKeyItem = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
dataset_ids: z.array(z.string()).optional().default([]),
|
||||
id: z.string(),
|
||||
last_used_at: z.int().nullish(),
|
||||
token: z.string(),
|
||||
@@ -1481,6 +1489,8 @@ export const zGetDatasetsApiBaseInfoResponse = zApiBaseUrlResponse
|
||||
*/
|
||||
export const zGetDatasetsApiKeysResponse = zApiKeyList
|
||||
|
||||
export const zPostDatasetsApiKeysBody = zDatasetApiKeyCreatePayload
|
||||
|
||||
/**
|
||||
* API key created successfully
|
||||
*/
|
||||
|
||||
@@ -70,7 +70,7 @@ vi.mock('@/service/client', () => ({
|
||||
},
|
||||
post: {
|
||||
mutationOptions: () => ({
|
||||
mutationFn: () => apiMocks.createDataset(),
|
||||
mutationFn: (variables: unknown) => apiMocks.createDataset(variables),
|
||||
}),
|
||||
},
|
||||
byApiKeyId: {
|
||||
@@ -231,6 +231,12 @@ describe('ApiKeyModal', () => {
|
||||
|
||||
expect(await screen.findByText('dat...cret-token-123456789')).toBeInTheDocument()
|
||||
expect(apiMocks.listDataset).toHaveBeenCalled()
|
||||
// Dataset keys surface their knowledge-base scope; a key without dataset_ids reads
|
||||
// as scoped to all knowledge bases.
|
||||
expect(
|
||||
screen.getByRole('columnheader', { name: 'appApi.apiKeyModal.scope' }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('appApi.apiKeyModal.scopeAllDatasets')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('creates an app API key through the generated mutation input', async () => {
|
||||
@@ -249,14 +255,18 @@ describe('ApiKeyModal', () => {
|
||||
).toHaveValue('new-app-token-123')
|
||||
})
|
||||
|
||||
it('creates a dataset API key through the generated mutation', async () => {
|
||||
it('creates a workspace dataset API key scoped to all knowledge bases', async () => {
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
||||
await renderModal(datasetScope)
|
||||
|
||||
await user.click(screen.getByText('appApi.apiKeyModal.createNewSecretKey'))
|
||||
|
||||
// Dataset creation first opens the scope dialog; the default "all" scope creates a
|
||||
// key with an empty dataset_ids list.
|
||||
await user.click(await screen.findByRole('button', { name: 'common.operation.create' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.createDataset).toHaveBeenCalledWith()
|
||||
expect(apiMocks.createDataset).toHaveBeenCalledWith({ body: { dataset_ids: [] } })
|
||||
})
|
||||
expect(
|
||||
await screen.findByRole('textbox', { name: 'appApi.apiKeyModal.secretKey' }),
|
||||
|
||||
@@ -27,6 +27,7 @@ import { currentWorkspaceAtom } from '@/context/workspace-state'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { ApiKeyTable } from './api-key-table'
|
||||
import { CreatedApiKeyDialog } from './created-api-key-dialog'
|
||||
import { DatasetScopeDialog } from './dataset-scope-dialog'
|
||||
|
||||
type CreatedApiKey = Pick<ApiKeyItem, 'token'>
|
||||
type ApiKeyScope =
|
||||
@@ -46,6 +47,9 @@ export function ApiKeyModal({ open, canManage, scope, onOpenChange }: ApiKeyModa
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const [deleteKeyId, setDeleteKeyId] = useState<string>()
|
||||
const [createdApiKey, setCreatedApiKey] = useState<CreatedApiKey>()
|
||||
const [scopeDialogOpen, setScopeDialogOpen] = useState(false)
|
||||
// Bumped on each open so the scope dialog remounts with fresh selection state.
|
||||
const [scopeDialogKey, setScopeDialogKey] = useState(0)
|
||||
|
||||
const appApiKeysQuery = useQuery(
|
||||
consoleQuery.apps.byResourceId.apiKeys.get.queryOptions({
|
||||
@@ -110,6 +114,7 @@ export function ApiKeyModal({ open, canManage, scope, onOpenChange }: ApiKeyModa
|
||||
if (!nextOpen) {
|
||||
setDeleteKeyId(undefined)
|
||||
setCreatedApiKey(undefined)
|
||||
setScopeDialogOpen(false)
|
||||
}
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
@@ -125,7 +130,10 @@ export function ApiKeyModal({ open, canManage, scope, onOpenChange }: ApiKeyModa
|
||||
)
|
||||
break
|
||||
case 'dataset':
|
||||
createDatasetApiKey.mutate(undefined, { onSuccess: setCreatedApiKey })
|
||||
// Dataset keys pick a knowledge-base scope before creation; the scope dialog
|
||||
// owns that step and calls handleCreateDatasetKey with the chosen ids.
|
||||
setScopeDialogKey((key) => key + 1)
|
||||
setScopeDialogOpen(true)
|
||||
break
|
||||
case 'environment':
|
||||
createEnvironmentApiKey.mutate(
|
||||
@@ -136,6 +144,19 @@ export function ApiKeyModal({ open, canManage, scope, onOpenChange }: ApiKeyModa
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateDatasetKey = (datasetIds: string[]) => {
|
||||
if (createDatasetApiKey.isPending) return
|
||||
createDatasetApiKey.mutate(
|
||||
{ body: { dataset_ids: datasetIds } },
|
||||
{
|
||||
onSuccess: (apiKey) => {
|
||||
setCreatedApiKey(apiKey)
|
||||
setScopeDialogOpen(false)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!deleteKeyId || isDeleting) return
|
||||
|
||||
@@ -194,7 +215,12 @@ export function ApiKeyModal({ open, canManage, scope, onOpenChange }: ApiKeyModa
|
||||
</div>
|
||||
)}
|
||||
{!!apiKeys?.length && (
|
||||
<ApiKeyTable apiKeys={apiKeys} canManage={canManage} onDeleteRequest={setDeleteKeyId} />
|
||||
<ApiKeyTable
|
||||
apiKeys={apiKeys}
|
||||
canManage={canManage}
|
||||
showScope={scope.type === 'dataset'}
|
||||
onDeleteRequest={setDeleteKeyId}
|
||||
/>
|
||||
)}
|
||||
<div className="flex shrink-0 px-6 py-4">
|
||||
<Button disabled={createDisabled} loading={isCreating} onClick={handleCreate}>
|
||||
@@ -236,6 +262,15 @@ export function ApiKeyModal({ open, canManage, scope, onOpenChange }: ApiKeyModa
|
||||
}}
|
||||
value={createdApiKey?.token ?? ''}
|
||||
/>
|
||||
{scope.type === 'dataset' && (
|
||||
<DatasetScopeDialog
|
||||
key={scopeDialogKey}
|
||||
open={scopeDialogOpen}
|
||||
isCreating={createDatasetApiKey.isPending}
|
||||
onOpenChange={setScopeDialogOpen}
|
||||
onConfirm={handleCreateDatasetKey}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,29 @@ import useTimestamp from '@/hooks/use-timestamp'
|
||||
type ApiKeyTableProps = {
|
||||
apiKeys: Array<ApiKeyItem | EnvironmentApiKey>
|
||||
canManage: boolean
|
||||
// Dataset keys carry a knowledge-base scope; enable the SCOPE column to surface it.
|
||||
showScope?: boolean
|
||||
onDeleteRequest: (apiKeyId: string) => void
|
||||
}
|
||||
|
||||
export function ApiKeyTable({ apiKeys, canManage, onDeleteRequest }: ApiKeyTableProps) {
|
||||
export function ApiKeyTable({
|
||||
apiKeys,
|
||||
canManage,
|
||||
showScope = false,
|
||||
onDeleteRequest,
|
||||
}: ApiKeyTableProps) {
|
||||
const { t } = useTranslation()
|
||||
const { formatTime } = useTimestamp()
|
||||
const maskToken = (token: string) => `${token.slice(0, 3)}...${token.slice(-20)}`
|
||||
|
||||
// Only dataset keys expose dataset_ids; an empty/absent list means the key can access
|
||||
// every knowledge base in the workspace.
|
||||
const scopeLabel = (apiKey: ApiKeyItem | EnvironmentApiKey) => {
|
||||
const datasetIds = (apiKey as { dataset_ids?: string[] }).dataset_ids
|
||||
if (!datasetIds?.length) return t(($) => $['apiKeyModal.scopeAllDatasets'], { ns: 'appApi' })
|
||||
return t(($) => $['apiKeyModal.scopeCount'], { ns: 'appApi', count: datasetIds.length })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-0 overflow-auto border-y border-divider-subtle">
|
||||
<table className="w-full table-fixed text-left system-sm-regular text-text-secondary">
|
||||
@@ -24,6 +39,11 @@ export function ApiKeyTable({ apiKeys, canManage, onDeleteRequest }: ApiKeyTable
|
||||
<th className="w-64 px-6 py-2" scope="col">
|
||||
{t(($) => $['apiKeyModal.secretKey'], { ns: 'appApi' })}
|
||||
</th>
|
||||
{showScope && (
|
||||
<th className="w-40 px-3 py-2" scope="col">
|
||||
{t(($) => $['apiKeyModal.scope'], { ns: 'appApi' })}
|
||||
</th>
|
||||
)}
|
||||
<th className="w-48 px-3 py-2" scope="col">
|
||||
{t(($) => $['apiKeyModal.created'], { ns: 'appApi' })}
|
||||
</th>
|
||||
@@ -39,6 +59,7 @@ export function ApiKeyTable({ apiKeys, canManage, onDeleteRequest }: ApiKeyTable
|
||||
{apiKeys.map((apiKey) => (
|
||||
<tr className="border-b border-divider-regular last:border-b-0" key={apiKey.id}>
|
||||
<td className="truncate px-6 py-2 font-mono">{maskToken(apiKey.token)}</td>
|
||||
{showScope && <td className="truncate px-3 py-2">{scopeLabel(apiKey)}</td>}
|
||||
<td className="truncate px-3 py-2">
|
||||
{formatTime(
|
||||
Number(apiKey.created_at),
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from '@langgenius/dify-ui/dialog'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio-group'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
|
||||
|
||||
type Scope = 'all' | 'specific'
|
||||
type SelectedKb = { id: string; name: string }
|
||||
|
||||
type DatasetScopeDialogProps = {
|
||||
open: boolean
|
||||
isCreating: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
// Emits the knowledge-base ids to scope the new key to. An empty array means the
|
||||
// key can access every knowledge base in the workspace (the "all" scope).
|
||||
onConfirm: (datasetIds: string[]) => void
|
||||
}
|
||||
|
||||
// Scope picker shown when creating a workspace dataset API key. It lets the caller
|
||||
// grant the key access to every knowledge base or to a specific selection, then hands
|
||||
// the chosen ids back to the parent modal, which owns the create mutation.
|
||||
export function DatasetScopeDialog({
|
||||
open,
|
||||
isCreating,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: DatasetScopeDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const [scope, setScope] = useState<Scope>('all')
|
||||
const [selected, setSelected] = useState<SelectedKb[]>([])
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
const [keyword, setKeyword] = useState('')
|
||||
|
||||
// Selection state resets by remounting: the parent bumps this dialog's `key` each time
|
||||
// it opens, so every open starts from these defaults with no reset-in-effect.
|
||||
const { data: datasetsPages } = useInfiniteDatasets({ keyword }, { enabled: open && pickerOpen })
|
||||
const datasets = useMemo(
|
||||
() => (datasetsPages?.pages ?? []).flatMap((page) => page.data),
|
||||
[datasetsPages],
|
||||
)
|
||||
const selectedIds = useMemo(() => new Set(selected.map((kb) => kb.id)), [selected])
|
||||
|
||||
const toggleKb = (id: string, name: string) => {
|
||||
setSelected((prev) =>
|
||||
prev.some((kb) => kb.id === id) ? prev.filter((kb) => kb.id !== id) : [...prev, { id, name }],
|
||||
)
|
||||
}
|
||||
|
||||
const removeKb = (id: string) => setSelected((prev) => prev.filter((kb) => kb.id !== id))
|
||||
|
||||
const canCreate = scope === 'all' || selected.length > 0
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen && isCreating) return
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!canCreate || isCreating) return
|
||||
onConfirm(scope === 'specific' ? selected.map((kb) => kb.id) : [])
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="flex w-140 flex-col overflow-hidden p-0">
|
||||
<div className="flex shrink-0 flex-col gap-1 px-6 pt-6 pr-14 pb-4">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(($) => $['apiKeyModal.addTitle'], { ns: 'appApi' })}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="system-sm-regular text-text-tertiary">
|
||||
{t(($) => $['apiKeyModal.addSubtitle'], { ns: 'appApi' })}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogClose
|
||||
render={
|
||||
<IconButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
size="lg"
|
||||
className="absolute inset-e-6 top-6"
|
||||
disabled={isCreating}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line size-4" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3 px-6 py-4">
|
||||
<div className="system-sm-semibold text-text-secondary">
|
||||
{t(($) => $['apiKeyModal.knowledgeBaseAccess'], { ns: 'appApi' })}
|
||||
</div>
|
||||
<RadioGroup<Scope> value={scope} onValueChange={setScope}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<label htmlFor="api-key-scope-all" className="flex cursor-pointer items-start gap-2">
|
||||
<Radio id="api-key-scope-all" value="all" className="mt-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="system-sm-medium text-text-secondary">
|
||||
{t(($) => $['apiKeyModal.scopeAllDatasets'], { ns: 'appApi' })}
|
||||
</span>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['apiKeyModal.scopeAllDatasetsTip'], { ns: 'appApi' })}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="api-key-scope-specific"
|
||||
className="flex cursor-pointer items-start gap-2"
|
||||
>
|
||||
<Radio id="api-key-scope-specific" value="specific" className="mt-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<span className="system-sm-medium text-text-secondary">
|
||||
{t(($) => $['apiKeyModal.scopeSpecificDatasets'], { ns: 'appApi' })}
|
||||
</span>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['apiKeyModal.scopeSpecificDatasetsTip'], { ns: 'appApi' })}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{scope === 'specific' && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="system-sm-semibold text-text-secondary">
|
||||
{t(($) => $['apiKeyModal.selectedKnowledgeBases'], { ns: 'appApi' })}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 rounded-lg border-[0.5px] border-divider-subtle bg-components-panel-bg p-3">
|
||||
{selected.length === 0 ? (
|
||||
<div className="py-0.5 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['apiKeyModal.noKnowledgeBasesSelected'], { ns: 'appApi' })}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['apiKeyModal.selectedKnowledgeBasesCount'], {
|
||||
ns: 'appApi',
|
||||
count: selected.length,
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selected.map((kb) => (
|
||||
<div
|
||||
key={kb.id}
|
||||
className="flex items-center gap-1 rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-2 py-1"
|
||||
>
|
||||
<span className="max-w-40 truncate system-xs-medium text-text-secondary">
|
||||
{kb.name}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
|
||||
className="i-ri-close-line size-3.5 shrink-0 cursor-pointer border-none bg-transparent p-0 text-text-tertiary hover:text-text-secondary"
|
||||
onClick={() => removeKb(kb.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Popover open={pickerOpen} onOpenChange={setPickerOpen}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="secondary" size="small" className="w-full">
|
||||
<span aria-hidden className="mr-1 i-ri-add-line size-4" />
|
||||
{t(($) => $['apiKeyModal.addKnowledgeBase'], { ns: 'appApi' })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent placement="bottom" sideOffset={4} className="w-80 p-2">
|
||||
<Input
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder={t(($) => $['apiKeyModal.searchKnowledgeBases'], {
|
||||
ns: 'appApi',
|
||||
})}
|
||||
/>
|
||||
<ScrollArea className="mt-2 max-h-60">
|
||||
<div className="flex flex-col">
|
||||
{datasets.map((ds) => (
|
||||
<label
|
||||
key={ds.id}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 hover:bg-state-base-hover"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedIds.has(ds.id)}
|
||||
onCheckedChange={() => toggleKb(ds.id, ds.name)}
|
||||
/>
|
||||
<span className="min-w-0 grow truncate system-sm-regular text-text-secondary">
|
||||
{ds.name}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 justify-end gap-2 px-6 pb-6">
|
||||
<Button variant="secondary" disabled={isCreating} onClick={() => handleOpenChange(false)}>
|
||||
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
||||
</Button>
|
||||
<Button disabled={!canCreate} loading={isCreating} onClick={handleConfirm}>
|
||||
{t(($) => $['operation.create'], { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ApiKeyList } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import type { ReactElement } from 'react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -6,10 +5,10 @@ import { consoleQuery } from '@/service/client'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { ApiSecretKeyButton } from '../shared/api-secret-key-button'
|
||||
|
||||
const appApiKeys: ApiKeyList = {
|
||||
const appApiKeys = {
|
||||
data: [
|
||||
{ id: 'key-1', token: 'app-a', type: 'app', created_at: 1, last_used_at: 1 },
|
||||
{ id: 'key-2', token: 'app-b', type: 'app', created_at: 2, last_used_at: 2 },
|
||||
{ id: 'key-1', token: 'app-a', type: 'app', created_at: 1, last_used_at: 1, dataset_ids: [] },
|
||||
{ id: 'key-2', token: 'app-b', type: 'app', created_at: 2, last_used_at: 2, dataset_ids: [] },
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "حذف هذا المفتاح السري؟",
|
||||
"actionMsg.ok": "موافق",
|
||||
"apiKey": "مفتاح API",
|
||||
"apiKeyModal.addKnowledgeBase": "إضافة قاعدة معرفة",
|
||||
"apiKeyModal.addSubtitle": "أنشئ مفتاح API جديدًا للوصول إلى واجهة برمجة قاعدة المعرفة.",
|
||||
"apiKeyModal.addTitle": "إضافة مفتاح API",
|
||||
"apiKeyModal.apiSecretKey": "مفتاح API",
|
||||
"apiKeyModal.apiSecretKeyTips": "لمنع إساءة استخدام API، قم بحماية مفتاح API الخاص بك. تجنب استخدامه كنص عادي في كود الواجهة الأمامية. :)",
|
||||
"apiKeyModal.createNewSecretKey": "إنشاء مفتاح سري جديد",
|
||||
"apiKeyModal.created": "تم الإنشاء",
|
||||
"apiKeyModal.generateTips": "احتفظ بهذا المفتاح في مكان آمن ويمكن الوصول إليه.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "الوصول إلى قواعد المعرفة",
|
||||
"apiKeyModal.lastUsed": "آخر استخدام",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "لم يتم تحديد أي قواعد معرفة بعد.",
|
||||
"apiKeyModal.scope": "النطاق",
|
||||
"apiKeyModal.scopeAllDatasets": "جميع قواعد المعرفة",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "يمكن لهذا المفتاح الوصول إلى جميع قواعد المعرفة في مساحة العمل.",
|
||||
"apiKeyModal.scopeCount": "{{count}} قاعدة معرفة",
|
||||
"apiKeyModal.scopeSpecificDatasets": "قواعد معرفة محددة",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "يمكن لهذا المفتاح الوصول فقط إلى قواعد المعرفة التي تحددها.",
|
||||
"apiKeyModal.searchKnowledgeBases": "بحث في قواعد المعرفة",
|
||||
"apiKeyModal.secretKey": "المفتاح السري",
|
||||
"apiKeyModal.selectedKnowledgeBases": "قواعد المعرفة المحددة",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "قواعد المعرفة المحددة ({{count}})",
|
||||
"apiServer": "خادم API",
|
||||
"copied": "تم النسخ",
|
||||
"copy": "نسخ",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "إعداد الاسترجاع",
|
||||
"retry": "إعادة المحاولة",
|
||||
"selectExternalKnowledgeAPI.placeholder": "اختر واجهة برمجة تطبيقات معرفة خارجية",
|
||||
"serviceApi.card.addApiKey": "إضافة مفتاح API",
|
||||
"serviceApi.card.apiKey": "مفتاح API",
|
||||
"serviceApi.card.apiReference": "مرجع API",
|
||||
"serviceApi.card.endpoint": "نقطة نهاية واجهة برمجة تطبيقات الخدمة",
|
||||
"serviceApi.card.manageApiKey": "إدارة مفتاح API",
|
||||
"serviceApi.card.title": "واجهة برمجة تطبيقات خدمة الخلفية",
|
||||
"serviceApi.disabled": "معطل",
|
||||
"serviceApi.enabled": "في الخدمة",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Diesen Geheimschlüssel löschen?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "API Schlüssel",
|
||||
"apiKeyModal.addKnowledgeBase": "Wissensdatenbank hinzufügen",
|
||||
"apiKeyModal.addSubtitle": "Erstellen Sie einen neuen API-Schlüssel für den Zugriff auf die Wissensdatenbank-API.",
|
||||
"apiKeyModal.addTitle": "API-Schlüssel hinzufügen",
|
||||
"apiKeyModal.apiSecretKey": "API Schlüssel",
|
||||
"apiKeyModal.apiSecretKeyTips": "Um Missbrauch der API zu verhindern, schützen Sie Ihren API Schlüssel. Vermeiden Sie es, ihn als Klartext im Frontend-Code zu verwenden. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Neuen Geheimschlüssel erstellen",
|
||||
"apiKeyModal.created": "ERSTELLT",
|
||||
"apiKeyModal.generateTips": "Bewahren Sie diesen Schlüssel an einem sicheren und zugänglichen Ort auf.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Zugriff auf Wissensdatenbanken",
|
||||
"apiKeyModal.lastUsed": "ZULETZT VERWENDET",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Noch keine Wissensdatenbanken ausgewählt.",
|
||||
"apiKeyModal.scope": "BEREICH",
|
||||
"apiKeyModal.scopeAllDatasets": "Alle Wissensdatenbanken",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Dieser Schlüssel kann auf alle Wissensdatenbanken im Arbeitsbereich zugreifen.",
|
||||
"apiKeyModal.scopeCount": "{{count}} Wissensdatenbanken",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Bestimmte Wissensdatenbanken",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Dieser Schlüssel kann nur auf die von Ihnen ausgewählten Wissensdatenbanken zugreifen.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Wissensdatenbanken suchen",
|
||||
"apiKeyModal.secretKey": "Geheimschlüssel",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Ausgewählte Wissensdatenbanken",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Ausgewählte Wissensdatenbanken ({{count}})",
|
||||
"apiServer": "API Server",
|
||||
"copied": "Kopiert",
|
||||
"copy": "Kopieren",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Abrufeinstellungen",
|
||||
"retry": "Wiederholen",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Auswählen einer externen Wissens-API",
|
||||
"serviceApi.card.addApiKey": "API-Schlüssel hinzufügen",
|
||||
"serviceApi.card.apiKey": "API-Schlüssel",
|
||||
"serviceApi.card.apiReference": "API Referenz",
|
||||
"serviceApi.card.endpoint": "Service-API-Endpunkt",
|
||||
"serviceApi.card.manageApiKey": "API-Schlüssel verwalten",
|
||||
"serviceApi.card.title": "Backend-Dienst-API",
|
||||
"serviceApi.disabled": "Behindert",
|
||||
"serviceApi.enabled": "Im Dienst",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Delete this secret key?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyModal.addKnowledgeBase": "Add",
|
||||
"apiKeyModal.addSubtitle": "Create a new API key for your workspace.",
|
||||
"apiKeyModal.addTitle": "Add API Key",
|
||||
"apiKeyModal.apiSecretKey": "API Key",
|
||||
"apiKeyModal.apiSecretKeyTips": "To prevent API abuse, protect your API Key. Avoid using it as plain text in front-end code. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Create new Secret key",
|
||||
"apiKeyModal.created": "CREATED",
|
||||
"apiKeyModal.generateTips": "Keep this key in a secure and accessible place.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Knowledge Base Access",
|
||||
"apiKeyModal.lastUsed": "LAST USED",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "No knowledge bases selected",
|
||||
"apiKeyModal.scope": "SCOPE",
|
||||
"apiKeyModal.scopeAllDatasets": "All Knowledge Bases",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Search and retrieve from all available knowledge bases.",
|
||||
"apiKeyModal.scopeCount": "{{count}} knowledge bases",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Specific Knowledge Bases",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Select which knowledge bases to include in search.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Search Knowledge Bases",
|
||||
"apiKeyModal.secretKey": "Secret Key",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Selected Knowledge Bases",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "{{count}} Knowledge Bases",
|
||||
"apiServer": "API Server",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Retrieval Setting",
|
||||
"retry": "Retry",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Choose an External Knowledge API",
|
||||
"serviceApi.card.addApiKey": "Add API Key",
|
||||
"serviceApi.card.apiKey": "API Key",
|
||||
"serviceApi.card.apiReference": "API Reference",
|
||||
"serviceApi.card.endpoint": "Service API Endpoint",
|
||||
"serviceApi.card.manageApiKey": "Manage API Key",
|
||||
"serviceApi.card.title": "Backend service api",
|
||||
"serviceApi.disabled": "Disabled",
|
||||
"serviceApi.enabled": "Enabled",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "¿Eliminar esta clave secreta?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "Clave de API",
|
||||
"apiKeyModal.addKnowledgeBase": "Añadir base de conocimiento",
|
||||
"apiKeyModal.addSubtitle": "Crea una nueva clave API para acceder a la API de la base de conocimiento.",
|
||||
"apiKeyModal.addTitle": "Añadir clave API",
|
||||
"apiKeyModal.apiSecretKey": "Clave de API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Para evitar el abuso de la API, protege tu clave de API. Evita usarla como texto plano en el código del frontend. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Crear nueva clave secreta",
|
||||
"apiKeyModal.created": "CREADA",
|
||||
"apiKeyModal.generateTips": "Guarda esta clave en un lugar seguro y accesible.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Acceso a las bases de conocimiento",
|
||||
"apiKeyModal.lastUsed": "ÚLTIMO USO",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Todavía no se ha seleccionado ninguna base de conocimiento.",
|
||||
"apiKeyModal.scope": "ALCANCE",
|
||||
"apiKeyModal.scopeAllDatasets": "Todas las bases de conocimiento",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Esta clave puede acceder a todas las bases de conocimiento del espacio de trabajo.",
|
||||
"apiKeyModal.scopeCount": "{{count}} bases de conocimiento",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Bases de conocimiento específicas",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Esta clave solo puede acceder a las bases de conocimiento que selecciones.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Buscar bases de conocimiento",
|
||||
"apiKeyModal.secretKey": "Clave secreta",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Bases de conocimiento seleccionadas",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Bases de conocimiento seleccionadas ({{count}})",
|
||||
"apiServer": "Servidor de API",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Configuración de recuperación",
|
||||
"retry": "Reintentar",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Elegir una API de conocimiento externa",
|
||||
"serviceApi.card.addApiKey": "Añadir clave API",
|
||||
"serviceApi.card.apiKey": "Clave API",
|
||||
"serviceApi.card.apiReference": "Referencia de la API",
|
||||
"serviceApi.card.endpoint": "Punto de enlace de la API de servicio",
|
||||
"serviceApi.card.manageApiKey": "Gestionar clave API",
|
||||
"serviceApi.card.title": "API del servicio de backend",
|
||||
"serviceApi.disabled": "Discapacitado",
|
||||
"serviceApi.enabled": "En servicio",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "این کلید مخفی حذف شود؟",
|
||||
"actionMsg.ok": "تایید",
|
||||
"apiKey": "کلید API",
|
||||
"apiKeyModal.addKnowledgeBase": "افزودن پایگاه دانش",
|
||||
"apiKeyModal.addSubtitle": "برای دسترسی به API پایگاه دانش، یک کلید API جدید ایجاد کنید.",
|
||||
"apiKeyModal.addTitle": "افزودن کلید API",
|
||||
"apiKeyModal.apiSecretKey": "کلید API",
|
||||
"apiKeyModal.apiSecretKeyTips": "برای جلوگیری از سوء استفاده از API، از کلید API خود محافظت کنید. از استفاده از آن به صورت متن ساده در کد فرانتاند خودداری کنید. :)",
|
||||
"apiKeyModal.createNewSecretKey": "ایجاد کلید مخفی جدید",
|
||||
"apiKeyModal.created": "ایجاد شده",
|
||||
"apiKeyModal.generateTips": "این کلید را در مکانی امن و قابل دسترس نگه دارید.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "دسترسی به پایگاههای دانش",
|
||||
"apiKeyModal.lastUsed": "آخرین استفاده",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "هنوز هیچ پایگاه دانشی انتخاب نشده است.",
|
||||
"apiKeyModal.scope": "محدوده",
|
||||
"apiKeyModal.scopeAllDatasets": "همه پایگاههای دانش",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "این کلید میتواند به تمام پایگاههای دانش موجود در فضای کاری دسترسی داشته باشد.",
|
||||
"apiKeyModal.scopeCount": "{{count}} پایگاه دانش",
|
||||
"apiKeyModal.scopeSpecificDatasets": "پایگاههای دانش خاص",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "این کلید فقط میتواند به پایگاههای دانشی که انتخاب میکنید دسترسی داشته باشد.",
|
||||
"apiKeyModal.searchKnowledgeBases": "جستجوی پایگاههای دانش",
|
||||
"apiKeyModal.secretKey": "کلید مخفی",
|
||||
"apiKeyModal.selectedKnowledgeBases": "پایگاههای دانش انتخابشده",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "پایگاههای دانش انتخابشده ({{count}})",
|
||||
"apiServer": "سرور API",
|
||||
"copied": "کپی شد",
|
||||
"copy": "کپی",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "تنظیمات بازیابی",
|
||||
"retry": "تلاش مجدد",
|
||||
"selectExternalKnowledgeAPI.placeholder": "یک API دانش خارجی را انتخاب کنید",
|
||||
"serviceApi.card.addApiKey": "افزودن کلید API",
|
||||
"serviceApi.card.apiKey": "کلید API",
|
||||
"serviceApi.card.apiReference": "مرجع API",
|
||||
"serviceApi.card.endpoint": "نقطه انتهایی رابط برنامهنویسی سرویس",
|
||||
"serviceApi.card.manageApiKey": "مدیریت کلید API",
|
||||
"serviceApi.card.title": "رابط برنامهنویسی سرویس پشتیبان",
|
||||
"serviceApi.disabled": "معلول",
|
||||
"serviceApi.enabled": "در حال خدمت",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Supprimer cette clé secrète ?",
|
||||
"actionMsg.ok": "D'accord",
|
||||
"apiKey": "Clé API",
|
||||
"apiKeyModal.addKnowledgeBase": "Ajouter une base de connaissances",
|
||||
"apiKeyModal.addSubtitle": "Créez une nouvelle clé API pour accéder à l'API de la base de connaissances.",
|
||||
"apiKeyModal.addTitle": "Ajouter une clé API",
|
||||
"apiKeyModal.apiSecretKey": "Clé API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Pour prévenir l'abus de l'API, protégez votre clé API. Évitez de l'utiliser comme du texte brut dans le code front-end. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Créer une nouvelle clé secrète",
|
||||
"apiKeyModal.created": "CRÉÉ",
|
||||
"apiKeyModal.generateTips": "Gardez cette clé dans un endroit sûr et accessible.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Accès aux bases de connaissances",
|
||||
"apiKeyModal.lastUsed": "DERNIÈRE UTILISATION",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Aucune base de connaissances sélectionnée pour le moment.",
|
||||
"apiKeyModal.scope": "PORTÉE",
|
||||
"apiKeyModal.scopeAllDatasets": "Toutes les bases de connaissances",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Cette clé peut accéder à toutes les bases de connaissances de l'espace de travail.",
|
||||
"apiKeyModal.scopeCount": "{{count}} bases de connaissances",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Bases de connaissances spécifiques",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Cette clé ne peut accéder qu'aux bases de connaissances que vous sélectionnez.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Rechercher des bases de connaissances",
|
||||
"apiKeyModal.secretKey": "Clé Secrète",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Bases de connaissances sélectionnées",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Bases de connaissances sélectionnées ({{count}})",
|
||||
"apiServer": "Serveur API",
|
||||
"copied": "Copié",
|
||||
"copy": "Copier",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Paramètres de récupération",
|
||||
"retry": "Réessayer",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Choisir une API de connaissances externe",
|
||||
"serviceApi.card.addApiKey": "Ajouter une clé API",
|
||||
"serviceApi.card.apiKey": "Clé API",
|
||||
"serviceApi.card.apiReference": "Référence API",
|
||||
"serviceApi.card.endpoint": "Point de terminaison de l'API",
|
||||
"serviceApi.card.manageApiKey": "Gérer la clé API",
|
||||
"serviceApi.card.title": "API du service backend",
|
||||
"serviceApi.disabled": "désactivé",
|
||||
"serviceApi.enabled": "En service",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "इस गुप्त कुंजी को हटाएं?",
|
||||
"actionMsg.ok": "ठीक",
|
||||
"apiKey": "एपीआई कुंजी",
|
||||
"apiKeyModal.addKnowledgeBase": "नॉलेज बेस जोड़ें",
|
||||
"apiKeyModal.addSubtitle": "नॉलेज बेस API तक पहुंचने के लिए एक नई एपीआई कुंजी बनाएं।",
|
||||
"apiKeyModal.addTitle": "एपीआई कुंजी जोड़ें",
|
||||
"apiKeyModal.apiSecretKey": "एपीआई कुंजी",
|
||||
"apiKeyModal.apiSecretKeyTips": "एपीआई का दुरुपयोग रोकने के लिए, अपनी एपीआई कुंजी की सुरक्षा करें। फ्रंट-एंड कोड में इसे सादे पाठ के रूप में उपयोग करने से बचें। :)",
|
||||
"apiKeyModal.createNewSecretKey": "नई गुप्त कुंजी बनाएँ",
|
||||
"apiKeyModal.created": "बनाई गई",
|
||||
"apiKeyModal.generateTips": "इस कुंजी को एक सुरक्षित और सुलभ स्थान पर रखें।",
|
||||
"apiKeyModal.knowledgeBaseAccess": "नॉलेज बेस एक्सेस",
|
||||
"apiKeyModal.lastUsed": "अंतिम उपयोग",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "अभी तक कोई नॉलेज बेस नहीं चुना गया है।",
|
||||
"apiKeyModal.scope": "दायरा",
|
||||
"apiKeyModal.scopeAllDatasets": "सभी नॉलेज बेस",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "यह कुंजी वर्कस्पेस के सभी नॉलेज बेस तक पहुंच सकती है।",
|
||||
"apiKeyModal.scopeCount": "{{count}} नॉलेज बेस",
|
||||
"apiKeyModal.scopeSpecificDatasets": "विशिष्ट नॉलेज बेस",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "यह कुंजी केवल उन्हीं नॉलेज बेस तक पहुंच सकती है जिन्हें आप चुनते हैं।",
|
||||
"apiKeyModal.searchKnowledgeBases": "नॉलेज बेस खोजें",
|
||||
"apiKeyModal.secretKey": "गुप्त कुंजी",
|
||||
"apiKeyModal.selectedKnowledgeBases": "चयनित नॉलेज बेस",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "चयनित नॉलेज बेस ({{count}})",
|
||||
"apiServer": "एपीआई सर्वर",
|
||||
"copied": "प्रतिलिपि बन गई",
|
||||
"copy": "प्रतिलिपि",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "पुनर्प्राप्ति सेटिंग्स",
|
||||
"retry": "पुनः प्रयास करें",
|
||||
"selectExternalKnowledgeAPI.placeholder": "एक बाहरी ज्ञान API चुनें",
|
||||
"serviceApi.card.addApiKey": "एपीआई कुंजी जोड़ें",
|
||||
"serviceApi.card.apiKey": "एपीआई कुंजी",
|
||||
"serviceApi.card.apiReference": "एपीआई संदर्भ",
|
||||
"serviceApi.card.endpoint": "सेवा एपीआई एंडपॉइंट",
|
||||
"serviceApi.card.manageApiKey": "एपीआई कुंजी प्रबंधित करें",
|
||||
"serviceApi.card.title": "बैकएंड सेवा एपीआई",
|
||||
"serviceApi.disabled": "अक्षम",
|
||||
"serviceApi.enabled": "सेवा में",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Hapus kunci rahasia ini?",
|
||||
"actionMsg.ok": "OKE",
|
||||
"apiKey": "Kunci API",
|
||||
"apiKeyModal.addKnowledgeBase": "Tambah basis pengetahuan",
|
||||
"apiKeyModal.addSubtitle": "Buat API Key baru untuk mengakses API basis pengetahuan.",
|
||||
"apiKeyModal.addTitle": "Tambah API Key",
|
||||
"apiKeyModal.apiSecretKey": "Kunci API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Untuk mencegah penyalahgunaan API, lindungi Kunci API Anda. Hindari menggunakannya sebagai teks biasa dalam kode front-end. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Membuat kunci Rahasia baru",
|
||||
"apiKeyModal.created": "DIBUAT",
|
||||
"apiKeyModal.generateTips": "Simpan kunci ini di tempat yang aman dan mudah diakses.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Akses basis pengetahuan",
|
||||
"apiKeyModal.lastUsed": "TERAKHIR DIGUNAKAN",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Belum ada basis pengetahuan yang dipilih.",
|
||||
"apiKeyModal.scope": "CAKUPAN",
|
||||
"apiKeyModal.scopeAllDatasets": "Semua basis pengetahuan",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Kunci ini dapat mengakses semua basis pengetahuan di ruang kerja.",
|
||||
"apiKeyModal.scopeCount": "{{count}} basis pengetahuan",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Basis pengetahuan tertentu",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Kunci ini hanya dapat mengakses basis pengetahuan yang Anda pilih.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Cari basis pengetahuan",
|
||||
"apiKeyModal.secretKey": "Kunci Rahasia",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Basis pengetahuan yang dipilih",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Basis pengetahuan yang dipilih ({{count}})",
|
||||
"apiServer": "Server API",
|
||||
"copied": "Disalin",
|
||||
"copy": "Salin",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Pengaturan Pengambilan",
|
||||
"retry": "Pengulangan",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Pilih API Pengetahuan Eksternal",
|
||||
"serviceApi.card.addApiKey": "Tambah API Key",
|
||||
"serviceApi.card.apiKey": "Kunci API",
|
||||
"serviceApi.card.apiReference": "Referensi API",
|
||||
"serviceApi.card.endpoint": "Titik Akhir API Layanan",
|
||||
"serviceApi.card.manageApiKey": "Kelola API Key",
|
||||
"serviceApi.card.title": "API layanan backend",
|
||||
"serviceApi.disabled": "Dinonaktifkan",
|
||||
"serviceApi.enabled": "Sedang Beroperasi",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Eliminare questa chiave segreta?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "Chiave API",
|
||||
"apiKeyModal.addKnowledgeBase": "Aggiungi base di conoscenza",
|
||||
"apiKeyModal.addSubtitle": "Crea una nuova Chiave API per accedere all'API delle basi di conoscenza.",
|
||||
"apiKeyModal.addTitle": "Aggiungi Chiave API",
|
||||
"apiKeyModal.apiSecretKey": "Chiave API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Per prevenire l'abuso dell'API, proteggi la tua chiave API. Evita di usarla come testo semplice nel codice front-end. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Crea nuova chiave segreta",
|
||||
"apiKeyModal.created": "CREATA",
|
||||
"apiKeyModal.generateTips": "Conserva questa chiave in un luogo sicuro e accessibile.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Accesso alle basi di conoscenza",
|
||||
"apiKeyModal.lastUsed": "ULTIMO UTILIZZO",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Nessuna base di conoscenza ancora selezionata.",
|
||||
"apiKeyModal.scope": "AMBITO",
|
||||
"apiKeyModal.scopeAllDatasets": "Tutte le basi di conoscenza",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Questa chiave può accedere a tutte le basi di conoscenza nell'area di lavoro.",
|
||||
"apiKeyModal.scopeCount": "{{count}} basi di conoscenza",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Basi di conoscenza specifiche",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Questa chiave può accedere solo alle basi di conoscenza selezionate.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Cerca basi di conoscenza",
|
||||
"apiKeyModal.secretKey": "Chiave Segreta",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Basi di conoscenza selezionate",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Basi di conoscenza selezionate ({{count}})",
|
||||
"apiServer": "Server API",
|
||||
"copied": "Copiato",
|
||||
"copy": "Copia",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Impostazioni di recupero",
|
||||
"retry": "Riprova",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Scegliere un'API di conoscenza esterna",
|
||||
"serviceApi.card.addApiKey": "Aggiungi Chiave API",
|
||||
"serviceApi.card.apiKey": "Chiave API",
|
||||
"serviceApi.card.apiReference": "Riferimento API",
|
||||
"serviceApi.card.endpoint": "Endpoint dell'API di servizio",
|
||||
"serviceApi.card.manageApiKey": "Gestisci Chiave API",
|
||||
"serviceApi.card.title": "API del servizio backend",
|
||||
"serviceApi.disabled": "Disabilitato",
|
||||
"serviceApi.enabled": "In servizio",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "このシークレットキーを削除しますか?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "API キー",
|
||||
"apiKeyModal.addKnowledgeBase": "ナレッジベースを追加",
|
||||
"apiKeyModal.addSubtitle": "ナレッジベースAPIにアクセスするための新しいAPIキーを作成します。",
|
||||
"apiKeyModal.addTitle": "APIキーを追加",
|
||||
"apiKeyModal.apiSecretKey": "API キー",
|
||||
"apiKeyModal.apiSecretKeyTips": "API の悪用を防ぐために、API キーを保護してください。フロントエンドのコードで平文として使用しないでください。:)",
|
||||
"apiKeyModal.createNewSecretKey": "新しいシークレットキーを作成",
|
||||
"apiKeyModal.created": "作成日時",
|
||||
"apiKeyModal.generateTips": "このキーを安全でアクセス可能な場所に保管してください。",
|
||||
"apiKeyModal.knowledgeBaseAccess": "ナレッジベースへのアクセス",
|
||||
"apiKeyModal.lastUsed": "最終使用日時",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "ナレッジベースがまだ選択されていません。",
|
||||
"apiKeyModal.scope": "スコープ",
|
||||
"apiKeyModal.scopeAllDatasets": "すべてのナレッジベース",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "このキーはワークスペース内のすべてのナレッジベースにアクセスできます。",
|
||||
"apiKeyModal.scopeCount": "{{count}}件のナレッジベース",
|
||||
"apiKeyModal.scopeSpecificDatasets": "特定のナレッジベース",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "このキーは選択したナレッジベースにのみアクセスできます。",
|
||||
"apiKeyModal.searchKnowledgeBases": "ナレッジベースを検索",
|
||||
"apiKeyModal.secretKey": "シークレットキー",
|
||||
"apiKeyModal.selectedKnowledgeBases": "選択したナレッジベース",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "選択したナレッジベース({{count}}件)",
|
||||
"apiServer": "API サーバー",
|
||||
"copied": "コピー済み",
|
||||
"copy": "コピー",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "検索設定",
|
||||
"retry": "再試行",
|
||||
"selectExternalKnowledgeAPI.placeholder": "外部ナレッジベース連携 API を選択",
|
||||
"serviceApi.card.addApiKey": "APIキーを追加",
|
||||
"serviceApi.card.apiKey": "APIキー",
|
||||
"serviceApi.card.apiReference": "APIリファレンス",
|
||||
"serviceApi.card.endpoint": "サービスAPIエンドポイント",
|
||||
"serviceApi.card.manageApiKey": "APIキーを管理",
|
||||
"serviceApi.card.title": "バックエンドサービスAPI",
|
||||
"serviceApi.disabled": "無効",
|
||||
"serviceApi.enabled": "サービス中",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "이 비밀 키를 삭제하시겠습니까?",
|
||||
"actionMsg.ok": "확인",
|
||||
"apiKey": "API 키",
|
||||
"apiKeyModal.addKnowledgeBase": "지식 베이스 추가",
|
||||
"apiKeyModal.addSubtitle": "지식 베이스 API에 접근할 새 API 키를 생성합니다.",
|
||||
"apiKeyModal.addTitle": "API 키 추가",
|
||||
"apiKeyModal.apiSecretKey": "API 키",
|
||||
"apiKeyModal.apiSecretKeyTips": "API 키를 보호하여 API 의 남용을 방지하십시오. 프런트엔드 코드에서 평문으로 사용하지 마세요. :)",
|
||||
"apiKeyModal.createNewSecretKey": "새로운 비밀 키 생성",
|
||||
"apiKeyModal.created": "생성 날짜",
|
||||
"apiKeyModal.generateTips": "이 키를 안전하고 접근 가능한 위치에 보관하십시오.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "지식 베이스 접근",
|
||||
"apiKeyModal.lastUsed": "최종 사용 날짜",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "아직 선택된 지식 베이스가 없습니다.",
|
||||
"apiKeyModal.scope": "범위",
|
||||
"apiKeyModal.scopeAllDatasets": "모든 지식 베이스",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "이 키는 작업 공간의 모든 지식 베이스에 접근할 수 있습니다.",
|
||||
"apiKeyModal.scopeCount": "지식 베이스 {{count}}개",
|
||||
"apiKeyModal.scopeSpecificDatasets": "특정 지식 베이스",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "이 키는 선택한 지식 베이스에만 접근할 수 있습니다.",
|
||||
"apiKeyModal.searchKnowledgeBases": "지식 베이스 검색",
|
||||
"apiKeyModal.secretKey": "비밀 키",
|
||||
"apiKeyModal.selectedKnowledgeBases": "선택된 지식 베이스",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "선택된 지식 베이스 ({{count}}개)",
|
||||
"apiServer": "API 서버",
|
||||
"copied": "복사 완료",
|
||||
"copy": "복사",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "검색 설정",
|
||||
"retry": "재시도",
|
||||
"selectExternalKnowledgeAPI.placeholder": "외부 지식 API 선택",
|
||||
"serviceApi.card.addApiKey": "API 키 추가",
|
||||
"serviceApi.card.apiKey": "API 키",
|
||||
"serviceApi.card.apiReference": "API 참고",
|
||||
"serviceApi.card.endpoint": "서비스 API 엔드포인트",
|
||||
"serviceApi.card.manageApiKey": "API 키 관리",
|
||||
"serviceApi.card.title": "백엔드 서비스 API",
|
||||
"serviceApi.disabled": "비활성화됨",
|
||||
"serviceApi.enabled": "서비스 중",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Delete this secret key?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "API Key",
|
||||
"apiKeyModal.addKnowledgeBase": "Kennisbank toevoegen",
|
||||
"apiKeyModal.addSubtitle": "Maak een nieuwe API Key om toegang te krijgen tot de kennisbank-API.",
|
||||
"apiKeyModal.addTitle": "API Key toevoegen",
|
||||
"apiKeyModal.apiSecretKey": "API Key",
|
||||
"apiKeyModal.apiSecretKeyTips": "To prevent API abuse, protect your API Key. Avoid using it as plain text in front-end code. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Create new Secret key",
|
||||
"apiKeyModal.created": "CREATED",
|
||||
"apiKeyModal.generateTips": "Keep this key in a secure and accessible place.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Toegang tot kennisbanken",
|
||||
"apiKeyModal.lastUsed": "LAST USED",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Nog geen kennisbanken geselecteerd.",
|
||||
"apiKeyModal.scope": "BEREIK",
|
||||
"apiKeyModal.scopeAllDatasets": "Alle kennisbanken",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Deze sleutel heeft toegang tot alle kennisbanken in de werkruimte.",
|
||||
"apiKeyModal.scopeCount": "{{count}} kennisbanken",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Specifieke kennisbanken",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Deze sleutel heeft alleen toegang tot de kennisbanken die u selecteert.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Kennisbanken zoeken",
|
||||
"apiKeyModal.secretKey": "Secret Key",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Geselecteerde kennisbanken",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Geselecteerde kennisbanken ({{count}})",
|
||||
"apiServer": "API Server",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Retrieval Setting",
|
||||
"retry": "Retry",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Choose an External Knowledge API",
|
||||
"serviceApi.card.addApiKey": "API Key toevoegen",
|
||||
"serviceApi.card.apiKey": "API Key",
|
||||
"serviceApi.card.apiReference": "API Reference",
|
||||
"serviceApi.card.endpoint": "Service API Endpoint",
|
||||
"serviceApi.card.manageApiKey": "API Key beheren",
|
||||
"serviceApi.card.title": "Backend service api",
|
||||
"serviceApi.disabled": "Disabled",
|
||||
"serviceApi.enabled": "Enabled",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Usunąć ten tajny klucz?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "Klucz API",
|
||||
"apiKeyModal.addKnowledgeBase": "Dodaj bazę wiedzy",
|
||||
"apiKeyModal.addSubtitle": "Utwórz nowy Klucz API, aby uzyskać dostęp do API baz wiedzy.",
|
||||
"apiKeyModal.addTitle": "Dodaj Klucz API",
|
||||
"apiKeyModal.apiSecretKey": "Klucz API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Aby zapobiec nadużyciom API, chron swój klucz API. Unikaj używania go jako zwykłego tekstu w kodzie front-end. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Utwórz nowy tajny klucz",
|
||||
"apiKeyModal.created": "UTWORZONY",
|
||||
"apiKeyModal.generateTips": "Przechowuj ten klucz w bezpiecznym i dostępnym miejscu.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Dostęp do baz wiedzy",
|
||||
"apiKeyModal.lastUsed": "OSTATNIO UŻYWANY",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Nie wybrano jeszcze żadnych baz wiedzy.",
|
||||
"apiKeyModal.scope": "ZAKRES",
|
||||
"apiKeyModal.scopeAllDatasets": "Wszystkie bazy wiedzy",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Ten klucz ma dostęp do wszystkich baz wiedzy w przestrzeni roboczej.",
|
||||
"apiKeyModal.scopeCount": "{{count}} baz wiedzy",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Określone bazy wiedzy",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Ten klucz ma dostęp tylko do wybranych przez Ciebie baz wiedzy.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Szukaj baz wiedzy",
|
||||
"apiKeyModal.secretKey": "Tajny Klucz",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Wybrane bazy wiedzy",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Wybrane bazy wiedzy ({{count}})",
|
||||
"apiServer": "Serwer API",
|
||||
"copied": "Skopiowane",
|
||||
"copy": "Kopiuj",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Ustawienia wyszukiwania",
|
||||
"retry": "Ponów",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Wybieranie interfejsu API wiedzy zewnętrznej",
|
||||
"serviceApi.card.addApiKey": "Dodaj Klucz API",
|
||||
"serviceApi.card.apiKey": "Klucz API",
|
||||
"serviceApi.card.apiReference": "Dokumentacja API",
|
||||
"serviceApi.card.endpoint": "Punkt końcowy API usługi",
|
||||
"serviceApi.card.manageApiKey": "Zarządzaj Kluczem API",
|
||||
"serviceApi.card.title": "Usługa backendowa API",
|
||||
"serviceApi.disabled": "Niepełnosprawny",
|
||||
"serviceApi.enabled": "W serwisie",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Excluir esta chave secreta?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "Chave da API",
|
||||
"apiKeyModal.addKnowledgeBase": "Adicionar base de conhecimento",
|
||||
"apiKeyModal.addSubtitle": "Crie uma nova chave de API para acessar a API de base de conhecimento.",
|
||||
"apiKeyModal.addTitle": "Adicionar Chave de API",
|
||||
"apiKeyModal.apiSecretKey": "Chave da API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Para evitar abuso da API, proteja sua Chave da API. Evite usá-la como texto simples no código front-end. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Criar nova Chave Secreta",
|
||||
"apiKeyModal.created": "CRIADA",
|
||||
"apiKeyModal.generateTips": "Mantenha esta chave em um local seguro e acessível.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Acesso às bases de conhecimento",
|
||||
"apiKeyModal.lastUsed": "ÚLTIMO USO",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Nenhuma base de conhecimento selecionada ainda.",
|
||||
"apiKeyModal.scope": "ESCOPO",
|
||||
"apiKeyModal.scopeAllDatasets": "Todas as bases de conhecimento",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Esta chave pode acessar todas as bases de conhecimento no espaço de trabalho.",
|
||||
"apiKeyModal.scopeCount": "{{count}} bases de conhecimento",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Bases de conhecimento específicas",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Esta chave pode acessar apenas as bases de conhecimento que você selecionar.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Pesquisar bases de conhecimento",
|
||||
"apiKeyModal.secretKey": "Chave Secreta",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Bases de conhecimento selecionadas",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Bases de conhecimento selecionadas ({{count}})",
|
||||
"apiServer": "Servidor da API",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Configurações de Recuperação",
|
||||
"retry": "Tentar novamente",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Escolher uma API de conhecimento externa",
|
||||
"serviceApi.card.addApiKey": "Adicionar Chave de API",
|
||||
"serviceApi.card.apiKey": "Chave de API",
|
||||
"serviceApi.card.apiReference": "Referência da API",
|
||||
"serviceApi.card.endpoint": "Endpoint da API de Serviço",
|
||||
"serviceApi.card.manageApiKey": "Gerenciar Chave de API",
|
||||
"serviceApi.card.title": "API de serviço de backend",
|
||||
"serviceApi.disabled": "Desativado",
|
||||
"serviceApi.enabled": "Em serviço",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Ștergeți această cheie secretă?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "Cheia API",
|
||||
"apiKeyModal.addKnowledgeBase": "Adaugă bază de cunoștințe",
|
||||
"apiKeyModal.addSubtitle": "Creează o nouă cheie API pentru a accesa API-ul bazei de cunoștințe.",
|
||||
"apiKeyModal.addTitle": "Adaugă cheie API",
|
||||
"apiKeyModal.apiSecretKey": "Cheia API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Pentru a preveni abuzul API, protejați-vă Cheia API. Evitați utilizarea ei ca text simplu în codul front-end. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Creează o nouă cheie secretă",
|
||||
"apiKeyModal.created": "CREATĂ",
|
||||
"apiKeyModal.generateTips": "Păstrați această cheie într-un loc sigur și accesibil.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Acces la bazele de cunoștințe",
|
||||
"apiKeyModal.lastUsed": "ULTIMA UTILIZARE",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Nicio bază de cunoștințe selectată încă.",
|
||||
"apiKeyModal.scope": "DOMENIU",
|
||||
"apiKeyModal.scopeAllDatasets": "Toate bazele de cunoștințe",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Această cheie poate accesa toate bazele de cunoștințe din spațiul de lucru.",
|
||||
"apiKeyModal.scopeCount": "{{count}} baze de cunoștințe",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Baze de cunoștințe specifice",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Această cheie poate accesa doar bazele de cunoștințe pe care le selectezi.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Caută baze de cunoștințe",
|
||||
"apiKeyModal.secretKey": "Cheie Secretă",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Baze de cunoștințe selectate",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Baze de cunoștințe selectate ({{count}})",
|
||||
"apiServer": "Server API",
|
||||
"copied": "Copiat",
|
||||
"copy": "Copiază",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Setări de recuperare",
|
||||
"retry": "Reîncercați",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Alegeți un API de cunoștințe extern",
|
||||
"serviceApi.card.addApiKey": "Adaugă cheie API",
|
||||
"serviceApi.card.apiKey": "Cheie API",
|
||||
"serviceApi.card.apiReference": "Referință API",
|
||||
"serviceApi.card.endpoint": "Punct final API de servicii",
|
||||
"serviceApi.card.manageApiKey": "Gestionează cheia API",
|
||||
"serviceApi.card.title": "API pentru serviciul backend",
|
||||
"serviceApi.disabled": "Dezactivat",
|
||||
"serviceApi.enabled": "În serviciu",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Удалить этот секретный ключ?",
|
||||
"actionMsg.ok": "ОК",
|
||||
"apiKey": "API Ключ",
|
||||
"apiKeyModal.addKnowledgeBase": "Добавить базу знаний",
|
||||
"apiKeyModal.addSubtitle": "Создайте новый API ключ для доступа к API базы знаний.",
|
||||
"apiKeyModal.addTitle": "Добавить API ключ",
|
||||
"apiKeyModal.apiSecretKey": "API Ключ",
|
||||
"apiKeyModal.apiSecretKeyTips": "Чтобы предотвратить злоупотребление API, защитите свой API ключ. Избегайте использования его в виде plain-текста во фронтенд-коде. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Создать новый секретный ключ",
|
||||
"apiKeyModal.created": "СОЗДАН",
|
||||
"apiKeyModal.generateTips": "Храните этот ключ в безопасном и доступном месте.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Доступ к базам знаний",
|
||||
"apiKeyModal.lastUsed": "ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Базы знаний еще не выбраны.",
|
||||
"apiKeyModal.scope": "ОБЛАСТЬ",
|
||||
"apiKeyModal.scopeAllDatasets": "Все базы знаний",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Этот ключ может получить доступ ко всем базам знаний в рабочем пространстве.",
|
||||
"apiKeyModal.scopeCount": "{{count}} баз знаний",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Определенные базы знаний",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Этот ключ может получить доступ только к выбранным вами базам знаний.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Поиск баз знаний",
|
||||
"apiKeyModal.secretKey": "Секретный ключ",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Выбранные базы знаний",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Выбранные базы знаний ({{count}})",
|
||||
"apiServer": "API Сервер",
|
||||
"copied": "Скопировано",
|
||||
"copy": "Копировать",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Настройки поиска",
|
||||
"retry": "Повторить попытку",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Выбор API внешних знаний",
|
||||
"serviceApi.card.addApiKey": "Добавить API ключ",
|
||||
"serviceApi.card.apiKey": "API ключ",
|
||||
"serviceApi.card.apiReference": "Справочник API",
|
||||
"serviceApi.card.endpoint": "Конечная точка API сервиса",
|
||||
"serviceApi.card.manageApiKey": "Управление API ключом",
|
||||
"serviceApi.card.title": "API бэкенд-сервиса",
|
||||
"serviceApi.disabled": "Отключено",
|
||||
"serviceApi.enabled": "На службе",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Izbrisati ta skrivni ključ?",
|
||||
"actionMsg.ok": "V redu",
|
||||
"apiKey": "API Ključ",
|
||||
"apiKeyModal.addKnowledgeBase": "Dodaj bazo znanja",
|
||||
"apiKeyModal.addSubtitle": "Ustvarite nov API ključ za dostop do API-ja baze znanja.",
|
||||
"apiKeyModal.addTitle": "Dodaj API ključ",
|
||||
"apiKeyModal.apiSecretKey": "API Ključ",
|
||||
"apiKeyModal.apiSecretKeyTips": "Da bi preprečili zlorabo API-ja, zaščitite svoj API ključ. Izogibajte se uporabi v navadnem besedilu v sprednji kodi. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Ustvari nov skrivni ključ",
|
||||
"apiKeyModal.created": "USTVARJENO",
|
||||
"apiKeyModal.generateTips": "Hranite ta ključ na varnem in dostopnem mestu.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Dostop do baz znanja",
|
||||
"apiKeyModal.lastUsed": "ZADNJA UPORABA",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Nobena baza znanja še ni izbrana.",
|
||||
"apiKeyModal.scope": "OBSEG",
|
||||
"apiKeyModal.scopeAllDatasets": "Vse baze znanja",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Ta ključ lahko dostopa do vseh baz znanja v delovnem prostoru.",
|
||||
"apiKeyModal.scopeCount": "{{count}} baz znanja",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Določene baze znanja",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Ta ključ lahko dostopa samo do baz znanja, ki jih izberete.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Iskanje baz znanja",
|
||||
"apiKeyModal.secretKey": "Skrivni ključ",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Izbrane baze znanja",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Izbrane baze znanja ({{count}})",
|
||||
"apiServer": "API Strežnik",
|
||||
"copied": "Kopirano",
|
||||
"copy": "Kopiraj",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Nastavitve pridobivanja",
|
||||
"retry": "Poskusi znova",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Izberite zunanji API za znanje",
|
||||
"serviceApi.card.addApiKey": "Dodaj API ključ",
|
||||
"serviceApi.card.apiKey": "API ključ",
|
||||
"serviceApi.card.apiReference": "API Referenca",
|
||||
"serviceApi.card.endpoint": "Vhodna točka API storitve",
|
||||
"serviceApi.card.manageApiKey": "Upravljanje API ključa",
|
||||
"serviceApi.card.title": "API storitev za zaledje",
|
||||
"serviceApi.disabled": "Onemogočeno",
|
||||
"serviceApi.enabled": "V storitvi",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "ลบคีย์ลับนี้?",
|
||||
"actionMsg.ok": "ตกลง, ได้",
|
||||
"apiKey": "คีย์ API",
|
||||
"apiKeyModal.addKnowledgeBase": "เพิ่มฐานความรู้",
|
||||
"apiKeyModal.addSubtitle": "สร้างกุญแจ API ใหม่เพื่อเข้าถึง API ของฐานความรู้",
|
||||
"apiKeyModal.addTitle": "เพิ่มกุญแจ API",
|
||||
"apiKeyModal.apiSecretKey": "คีย์ API",
|
||||
"apiKeyModal.apiSecretKeyTips": "เพื่อป้องกันการละเมิด API ให้ปกป้องคีย์ API ของคุณ หลีกเลี่ยงการใช้เป็นข้อความธรรมดาในโค้ดส่วนหน้า :)",
|
||||
"apiKeyModal.createNewSecretKey": "สร้างคีย์ลับใหม่",
|
||||
"apiKeyModal.created": "สร้าง",
|
||||
"apiKeyModal.generateTips": "เก็บกุญแจนี้ไว้ในที่ปลอดภัยและเข้าถึงได้",
|
||||
"apiKeyModal.knowledgeBaseAccess": "การเข้าถึงฐานความรู้",
|
||||
"apiKeyModal.lastUsed": "ใช้ล่าสุด",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "ยังไม่ได้เลือกฐานความรู้",
|
||||
"apiKeyModal.scope": "ขอบเขต",
|
||||
"apiKeyModal.scopeAllDatasets": "ฐานความรู้ทั้งหมด",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "กุญแจนี้สามารถเข้าถึงฐานความรู้ทั้งหมดในพื้นที่ทำงาน",
|
||||
"apiKeyModal.scopeCount": "ฐานความรู้ {{count}} รายการ",
|
||||
"apiKeyModal.scopeSpecificDatasets": "ฐานความรู้ที่ระบุ",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "กุญแจนี้สามารถเข้าถึงเฉพาะฐานความรู้ที่คุณเลือกเท่านั้น",
|
||||
"apiKeyModal.searchKnowledgeBases": "ค้นหาฐานความรู้",
|
||||
"apiKeyModal.secretKey": "กุญแจลับ",
|
||||
"apiKeyModal.selectedKnowledgeBases": "ฐานความรู้ที่เลือก",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "ฐานความรู้ที่เลือก ({{count}})",
|
||||
"apiServer": "เซิร์ฟเวอร์ API",
|
||||
"copied": "คัด ลอก",
|
||||
"copy": "ลอก",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "การตั้งค่าการดึงข้อมูล",
|
||||
"retry": "ลอง",
|
||||
"selectExternalKnowledgeAPI.placeholder": "เลือก API ความรู้ภายนอก",
|
||||
"serviceApi.card.addApiKey": "เพิ่มกุญแจ API",
|
||||
"serviceApi.card.apiKey": "กุญแจ API",
|
||||
"serviceApi.card.apiReference": "เอกสารอ้างอิง API",
|
||||
"serviceApi.card.endpoint": "จุดเชื่อมต่อ API บริการ",
|
||||
"serviceApi.card.manageApiKey": "จัดการกุญแจ API",
|
||||
"serviceApi.card.title": "บริการแบ็กเอนด์ API",
|
||||
"serviceApi.disabled": "ถูกปิดใช้งาน",
|
||||
"serviceApi.enabled": "ให้บริการ",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Bu gizli anahtarı silmek istiyor musunuz?",
|
||||
"actionMsg.ok": "Tamam",
|
||||
"apiKey": "API Anahtarı",
|
||||
"apiKeyModal.addKnowledgeBase": "Bilgi tabanı ekle",
|
||||
"apiKeyModal.addSubtitle": "Bilgi tabanı API'sine erişmek için yeni bir API anahtarı oluşturun.",
|
||||
"apiKeyModal.addTitle": "API Anahtarı Ekle",
|
||||
"apiKeyModal.apiSecretKey": "API Anahtarı",
|
||||
"apiKeyModal.apiSecretKeyTips": "API suiistimalini önlemek için, API Anahtarınızı koruyunuz. Ön uç kodda düz metin olarak kullanmaktan kaçının. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Yeni Gizli Anahtar Oluştur",
|
||||
"apiKeyModal.created": "OLUŞTURULDU",
|
||||
"apiKeyModal.generateTips": "Bu anahtarı güvenli ve erişilebilir bir yerde saklayın.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Bilgi tabanı erişimi",
|
||||
"apiKeyModal.lastUsed": "SON KULLANIM",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Henüz bilgi tabanı seçilmedi.",
|
||||
"apiKeyModal.scope": "KAPSAM",
|
||||
"apiKeyModal.scopeAllDatasets": "Tüm bilgi tabanları",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Bu anahtar çalışma alanındaki tüm bilgi tabanlarına erişebilir.",
|
||||
"apiKeyModal.scopeCount": "{{count}} bilgi tabanı",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Belirli bilgi tabanları",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Bu anahtar yalnızca seçtiğiniz bilgi tabanlarına erişebilir.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Bilgi tabanlarında ara",
|
||||
"apiKeyModal.secretKey": "Gizli Anahtar",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Seçili bilgi tabanları",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Seçili bilgi tabanları ({{count}})",
|
||||
"apiServer": "API Sunucusu",
|
||||
"copied": "Kopyalandı",
|
||||
"copy": "Kopyala",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Geri Alım Ayarı",
|
||||
"retry": "Yeniden Dene",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Bir Harici Bilgi API'si seçin",
|
||||
"serviceApi.card.addApiKey": "API Anahtarı Ekle",
|
||||
"serviceApi.card.apiKey": "API Anahtarı",
|
||||
"serviceApi.card.apiReference": "API Referansı",
|
||||
"serviceApi.card.endpoint": "Hizmet API Uç Noktası",
|
||||
"serviceApi.card.manageApiKey": "API Anahtarını Yönet",
|
||||
"serviceApi.card.title": "Backend servis api",
|
||||
"serviceApi.disabled": "Devre Dışı",
|
||||
"serviceApi.enabled": "Etkin",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Видалити цей секретний ключ?",
|
||||
"actionMsg.ok": "Гаразд",
|
||||
"apiKey": "Ключ API",
|
||||
"apiKeyModal.addKnowledgeBase": "Додати базу знань",
|
||||
"apiKeyModal.addSubtitle": "Створіть новий ключ API для доступу до API бази знань.",
|
||||
"apiKeyModal.addTitle": "Додати ключ API",
|
||||
"apiKeyModal.apiSecretKey": "Ключ API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Щоб запобігти зловживанням API, захистіть свій ключ API. Уникайте його використання як звичайного тексту у front-end коді. :)",
|
||||
"apiKeyModal.createNewSecretKey": "Створити новий секретний ключ",
|
||||
"apiKeyModal.created": "СТВОРЕНО",
|
||||
"apiKeyModal.generateTips": "Зберігайте цей ключ у безпечному та доступному місці.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Доступ до баз знань",
|
||||
"apiKeyModal.lastUsed": "ОСТАННЄ ВИКОРИСТАННЯ",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Бази знань ще не вибрано.",
|
||||
"apiKeyModal.scope": "ОБЛАСТЬ ДІЇ",
|
||||
"apiKeyModal.scopeAllDatasets": "Усі бази знань",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Цей ключ має доступ до всіх баз знань у робочому просторі.",
|
||||
"apiKeyModal.scopeCount": "{{count}} баз знань",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Певні бази знань",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Цей ключ має доступ лише до вибраних вами баз знань.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Пошук баз знань",
|
||||
"apiKeyModal.secretKey": "Секретний ключ",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Вибрані бази знань",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Вибрані бази знань ({{count}})",
|
||||
"apiServer": "API сервер",
|
||||
"copied": "Скопійовано",
|
||||
"copy": "Копіювати",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Налаштування пошуку",
|
||||
"retry": "Повторити спробу",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Виберіть API зовнішніх знань",
|
||||
"serviceApi.card.addApiKey": "Додати ключ API",
|
||||
"serviceApi.card.apiKey": "Ключ API",
|
||||
"serviceApi.card.apiReference": "Посилання на API",
|
||||
"serviceApi.card.endpoint": "Кінцева точка API сервісу",
|
||||
"serviceApi.card.manageApiKey": "Керувати ключем API",
|
||||
"serviceApi.card.title": "API бекенд-сервіс",
|
||||
"serviceApi.disabled": "Вимкнено",
|
||||
"serviceApi.enabled": "У службі",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "Xóa khóa bí mật này?",
|
||||
"actionMsg.ok": "OK",
|
||||
"apiKey": "Khóa API",
|
||||
"apiKeyModal.addKnowledgeBase": "Thêm cơ sở tri thức",
|
||||
"apiKeyModal.addSubtitle": "Tạo khóa API mới để truy cập API cơ sở tri thức.",
|
||||
"apiKeyModal.addTitle": "Thêm khóa API",
|
||||
"apiKeyModal.apiSecretKey": "Khóa API",
|
||||
"apiKeyModal.apiSecretKeyTips": "Để ngăn chặn việc lạm dụng API, hãy bảo vệ Khóa API của bạn. Tránh sử dụng nó dưới dạng văn bản thuần trong mã giao diện người dùng.",
|
||||
"apiKeyModal.createNewSecretKey": "Tạo khóa bí mật mới",
|
||||
"apiKeyModal.created": "ĐÃ TẠO",
|
||||
"apiKeyModal.generateTips": "Hãy lưu giữ khóa này ở nơi an toàn và dễ tiếp cận.",
|
||||
"apiKeyModal.knowledgeBaseAccess": "Quyền truy cập cơ sở tri thức",
|
||||
"apiKeyModal.lastUsed": "SỬ DỤNG LẦN CUỐI",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "Chưa chọn cơ sở tri thức nào.",
|
||||
"apiKeyModal.scope": "PHẠM VI",
|
||||
"apiKeyModal.scopeAllDatasets": "Tất cả cơ sở tri thức",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "Khóa này có thể truy cập tất cả cơ sở tri thức trong không gian làm việc.",
|
||||
"apiKeyModal.scopeCount": "{{count}} cơ sở tri thức",
|
||||
"apiKeyModal.scopeSpecificDatasets": "Cơ sở tri thức cụ thể",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "Khóa này chỉ có thể truy cập những cơ sở tri thức bạn chọn.",
|
||||
"apiKeyModal.searchKnowledgeBases": "Tìm kiếm cơ sở tri thức",
|
||||
"apiKeyModal.secretKey": "Khóa bí mật",
|
||||
"apiKeyModal.selectedKnowledgeBases": "Cơ sở tri thức đã chọn",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "Cơ sở tri thức đã chọn ({{count}})",
|
||||
"apiServer": "Máy chủ API",
|
||||
"copied": "Đã sao chép",
|
||||
"copy": "Sao chép",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "Cài đặt truy xuất",
|
||||
"retry": "Thử lại",
|
||||
"selectExternalKnowledgeAPI.placeholder": "Chọn một API kiến thức bên ngoài",
|
||||
"serviceApi.card.addApiKey": "Thêm khóa API",
|
||||
"serviceApi.card.apiKey": "Khóa API",
|
||||
"serviceApi.card.apiReference": "Tham chiếu API",
|
||||
"serviceApi.card.endpoint": "Điểm cuối API dịch vụ",
|
||||
"serviceApi.card.manageApiKey": "Quản lý khóa API",
|
||||
"serviceApi.card.title": "API dịch vụ backend",
|
||||
"serviceApi.disabled": "Vô hiệu hóa",
|
||||
"serviceApi.enabled": "Đang phục vụ",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "删除此密钥?",
|
||||
"actionMsg.ok": "好的",
|
||||
"apiKey": "API 密钥",
|
||||
"apiKeyModal.addKnowledgeBase": "添加知识库",
|
||||
"apiKeyModal.addSubtitle": "创建一个新的 API 密钥以访问知识库 API。",
|
||||
"apiKeyModal.addTitle": "添加 API 密钥",
|
||||
"apiKeyModal.apiSecretKey": "API 密钥",
|
||||
"apiKeyModal.apiSecretKeyTips": "如果不想你的 API 被滥用,请保护好你的 API Key :) 最佳实践是避免在前端代码中明文引用。",
|
||||
"apiKeyModal.createNewSecretKey": "创建密钥",
|
||||
"apiKeyModal.created": "创建时间",
|
||||
"apiKeyModal.generateTips": "请将此密钥保存在安全且可访问的地方。",
|
||||
"apiKeyModal.knowledgeBaseAccess": "知识库访问权限",
|
||||
"apiKeyModal.lastUsed": "最后使用",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "尚未选择任何知识库。",
|
||||
"apiKeyModal.scope": "范围",
|
||||
"apiKeyModal.scopeAllDatasets": "所有知识库",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "此密钥可以访问工作区中的所有知识库。",
|
||||
"apiKeyModal.scopeCount": "{{count}} 个知识库",
|
||||
"apiKeyModal.scopeSpecificDatasets": "指定知识库",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "此密钥只能访问您选择的知识库。",
|
||||
"apiKeyModal.searchKnowledgeBases": "搜索知识库",
|
||||
"apiKeyModal.secretKey": "密钥",
|
||||
"apiKeyModal.selectedKnowledgeBases": "已选择的知识库",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "已选择的知识库({{count}})",
|
||||
"apiServer": "API 服务器",
|
||||
"copied": "已复制",
|
||||
"copy": "复制",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "召回设置",
|
||||
"retry": "重试",
|
||||
"selectExternalKnowledgeAPI.placeholder": "选择一个外部知识 API",
|
||||
"serviceApi.card.addApiKey": "添加 API 密钥",
|
||||
"serviceApi.card.apiKey": "API 密钥",
|
||||
"serviceApi.card.apiReference": "API 文档",
|
||||
"serviceApi.card.endpoint": "API 端点",
|
||||
"serviceApi.card.manageApiKey": "管理 API 密钥",
|
||||
"serviceApi.card.title": "后端服务 API",
|
||||
"serviceApi.disabled": "已停用",
|
||||
"serviceApi.enabled": "已启用",
|
||||
|
||||
@@ -3,13 +3,27 @@
|
||||
"actionMsg.deleteConfirmTitle": "刪除此金鑰?",
|
||||
"actionMsg.ok": "好的",
|
||||
"apiKey": "API 金鑰",
|
||||
"apiKeyModal.addKnowledgeBase": "新增知識庫",
|
||||
"apiKeyModal.addSubtitle": "建立新的 API 金鑰以存取知識庫 API。",
|
||||
"apiKeyModal.addTitle": "新增 API 金鑰",
|
||||
"apiKeyModal.apiSecretKey": "API 金鑰",
|
||||
"apiKeyModal.apiSecretKeyTips": "如果不想你的 API 被濫用,請保護好你的 API Key :) 最佳實踐是避免在前端程式碼中明文引用。",
|
||||
"apiKeyModal.createNewSecretKey": "建立金鑰",
|
||||
"apiKeyModal.created": "建立時間",
|
||||
"apiKeyModal.generateTips": "請將此金鑰儲存在安全且可訪問的地方。",
|
||||
"apiKeyModal.knowledgeBaseAccess": "知識庫存取權限",
|
||||
"apiKeyModal.lastUsed": "最後使用",
|
||||
"apiKeyModal.noKnowledgeBasesSelected": "尚未選擇任何知識庫。",
|
||||
"apiKeyModal.scope": "範圍",
|
||||
"apiKeyModal.scopeAllDatasets": "所有知識庫",
|
||||
"apiKeyModal.scopeAllDatasetsTip": "此金鑰可以存取工作區中的所有知識庫。",
|
||||
"apiKeyModal.scopeCount": "{{count}} 個知識庫",
|
||||
"apiKeyModal.scopeSpecificDatasets": "指定知識庫",
|
||||
"apiKeyModal.scopeSpecificDatasetsTip": "此金鑰只能存取您選擇的知識庫。",
|
||||
"apiKeyModal.searchKnowledgeBases": "搜尋知識庫",
|
||||
"apiKeyModal.secretKey": "金鑰",
|
||||
"apiKeyModal.selectedKnowledgeBases": "已選擇的知識庫",
|
||||
"apiKeyModal.selectedKnowledgeBasesCount": "已選擇的知識庫({{count}})",
|
||||
"apiServer": "API 伺服器",
|
||||
"copied": "已複製",
|
||||
"copy": "複製",
|
||||
|
||||
@@ -460,9 +460,11 @@
|
||||
"retrievalSettings": "檢索設置",
|
||||
"retry": "重試",
|
||||
"selectExternalKnowledgeAPI.placeholder": "選擇外部知識 API",
|
||||
"serviceApi.card.addApiKey": "新增 API 金鑰",
|
||||
"serviceApi.card.apiKey": "API 金鑰",
|
||||
"serviceApi.card.apiReference": "API 參考",
|
||||
"serviceApi.card.endpoint": "服務 API 端點",
|
||||
"serviceApi.card.manageApiKey": "管理 API 金鑰",
|
||||
"serviceApi.card.title": "後端服務 API",
|
||||
"serviceApi.disabled": "已停用",
|
||||
"serviceApi.enabled": "使用中",
|
||||
|
||||
@@ -928,6 +928,7 @@ describe('consoleQuery app mutation defaults', () => {
|
||||
id: 'app-key-1',
|
||||
token: 'app-token',
|
||||
type: 'app',
|
||||
dataset_ids: [],
|
||||
},
|
||||
{ params: { resource_id: 'app-1' } },
|
||||
undefined,
|
||||
@@ -938,8 +939,9 @@ describe('consoleQuery app mutation defaults', () => {
|
||||
id: 'dataset-key-1',
|
||||
token: 'dataset-token',
|
||||
type: 'dataset',
|
||||
dataset_ids: [],
|
||||
},
|
||||
undefined,
|
||||
{ body: {} },
|
||||
undefined,
|
||||
context,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user