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:
YungLe
2026-09-02 10:30:42 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 7664ad7149
commit 3cd1fc098c
73 changed files with 1453 additions and 26 deletions
+43 -1
View File
@@ -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")
+38 -4
View File
@@ -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>")
+14 -1
View File
@@ -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(
@@ -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")
+2
View File
@@ -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
View File
@@ -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__ = (
+13
View File
@@ -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 |
+4
View File
@@ -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}>"
+94
View File
@@ -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
+5
View File
@@ -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' }),
+37 -2
View File
@@ -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}
/>
)}
</>
)
}
+22 -1
View File
@@ -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: [] },
],
}
+14
View File
@@ -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": "نسخ",
+2
View File
@@ -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": "في الخدمة",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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": "کپی",
+2
View File
@@ -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": "در حال خدمت",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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": "प्रतिलिपि",
+2
View File
@@ -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": "सेवा में",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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": "コピー",
+2
View File
@@ -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": "サービス中",
+14
View File
@@ -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": "복사",
+2
View File
@@ -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": "서비스 중",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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ă",
+2
View File
@@ -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",
+14
View File
@@ -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": "Копировать",
+2
View File
@@ -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": "На службе",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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": "ลอก",
+2
View File
@@ -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": "ให้บริการ",
+14
View File
@@ -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",
+2
View File
@@ -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",
+14
View File
@@ -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": "Копіювати",
+2
View File
@@ -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": "У службі",
+14
View File
@@ -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",
+2
View File
@@ -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ụ",
+14
View File
@@ -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": "复制",
+2
View File
@@ -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": "已启用",
+14
View File
@@ -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": "複製",
+2
View File
@@ -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 -1
View File
@@ -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,
),