mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-30 05:16:52 +08:00
feat(operation-trace): collapsible retention policy and assistant trace tool
- Collapse the retention policy section by default, showing a one-line summary when collapsed. - Add the owner/admin-only list_operation_logs assistant tool backed by audit.view. - Enforce per-tool permissions on both tool listing and invocation. - Add a substring search filter (search) to query_logs and the tool so callers can ask narrow questions (plugin name, account, verb) instead of dumping the full history. - Project records to a compact shape so a page stays under the assistant result budget. - Localize the new assistant tool label and retention summary across all locales.
This commit is contained in:
@@ -25,6 +25,10 @@ Do not claim the application has been tested: this experiment has no chat-test o
|
||||
After creation/configuration show the returned resource URL so the user can open the normal editor,
|
||||
upload documents and use its existing debug chat. If an operation failed or its result is unknown,
|
||||
do not repeat a write automatically; explain the result and ask the user to inspect the resource.
|
||||
When the list_operation_logs tool is available (owner / admin only), use it to answer "who changed or
|
||||
viewed what" questions with the recorded before/after fields instead of guessing. Always query it with
|
||||
a narrow filter (search / resource_type / actor) rather than an unfiltered dump, and never repeat the
|
||||
same empty query: if a filter matched nothing, widen it once instead of re-asking.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,129 @@ class ListResources(Arguments):
|
||||
kind: Literal['models', 'embedding_models', 'pipelines', 'knowledge_bases', 'knowledge_engines']
|
||||
|
||||
|
||||
#: Canonical resource types, mirrored from the trace service action table. The
|
||||
#: enum lets the model pick a value that exists instead of guessing a string
|
||||
#: that matches nothing.
|
||||
_RESOURCE_TYPES = (
|
||||
'bot',
|
||||
'adapter',
|
||||
'model_provider',
|
||||
'llm_model',
|
||||
'pipeline',
|
||||
'user',
|
||||
'workspace',
|
||||
'monitoring',
|
||||
'webhook',
|
||||
'api_key',
|
||||
'workspace_settings',
|
||||
'member',
|
||||
'member_invitation',
|
||||
'operation_log',
|
||||
'plugin',
|
||||
'plugin_page',
|
||||
'skill',
|
||||
'knowledge_base',
|
||||
'mcp_server',
|
||||
'runtime',
|
||||
'system',
|
||||
'resource',
|
||||
)
|
||||
|
||||
|
||||
class ListOperationLogs(Arguments):
|
||||
"""Read the Workspace operation trace (owner / admin only)."""
|
||||
|
||||
#: Optional free-text filter is mapped to a substring match so the caller
|
||||
#: can name a plugin, an account or a verb ("install") without knowing the
|
||||
#: stored action key. Prefer this over ``action`` when unsure of the key.
|
||||
search: str | None = Field(default=None, max_length=100)
|
||||
#: Exact stored action key, e.g. ``plugin_install`` (see the action list in
|
||||
#: the tool description). Only use it when the exact key is known.
|
||||
action: str | None = Field(default=None, max_length=64)
|
||||
resource_type: str | None = Field(
|
||||
default=None,
|
||||
max_length=64,
|
||||
description='Optional resource type filter. Valid values: ' + ', '.join(_RESOURCE_TYPES) + '.',
|
||||
)
|
||||
actor: str | None = Field(
|
||||
default=None,
|
||||
max_length=64,
|
||||
description='Optional actor filter: an account UUID or part of the actor name.',
|
||||
)
|
||||
limit: int = Field(
|
||||
default=20,
|
||||
ge=1,
|
||||
le=50,
|
||||
description='Maximum records to return. Keep it small (10-20) for a targeted answer.',
|
||||
)
|
||||
|
||||
|
||||
#: Forensics fields the settings panel needs but an answer does not; dropping
|
||||
#: them keeps a page of records far below the assistant's per-result budget.
|
||||
_COMPACT_CHANGE_FIELDS = 6
|
||||
_COMPACT_CHANGE_CHARS = 160
|
||||
|
||||
|
||||
def _brief(value, limit: int = _COMPACT_CHANGE_CHARS):
|
||||
"""Render a diff value as a bounded string (mirrors the trace service)."""
|
||||
|
||||
if value is None or isinstance(value, (int, float, bool)):
|
||||
return value
|
||||
text = value if isinstance(value, str) else str(value)
|
||||
return text if len(text) <= limit else text[: max(limit - 1, 0)] + '…'
|
||||
|
||||
|
||||
def _compact_operation_records(result: dict) -> dict:
|
||||
"""Project operation records onto the fields a "who did what" answer needs.
|
||||
|
||||
``query_logs`` returns the full panel payload -- hash chain, user agent,
|
||||
payload details -- which is roughly a kilobyte per row. A whole page of
|
||||
those overflows the assistant's per-result budget and arrives as a raw
|
||||
truncated preview the model cannot read, which is why an answer can end up
|
||||
claiming the data is unavailable. Only the actor, the action, the target,
|
||||
the timestamp and the verification verdicts are kept here; the heavy
|
||||
forensic columns stay available through the settings panel export.
|
||||
"""
|
||||
|
||||
def record(row: dict) -> dict:
|
||||
changes = [
|
||||
{
|
||||
'field': change.get('field'),
|
||||
'before': _brief(change.get('before')),
|
||||
'after': _brief(change.get('after')),
|
||||
}
|
||||
for change in (row.get('changes') or [])[:_COMPACT_CHANGE_FIELDS]
|
||||
]
|
||||
return {
|
||||
'id': row.get('id'),
|
||||
'created_at': row.get('created_at'),
|
||||
'actor': row.get('actor_name') or row.get('actor_account_uuid'),
|
||||
'actor_role': row.get('actor_role'),
|
||||
'action': row.get('action'),
|
||||
'action_i18n_key': row.get('action_i18n_key'),
|
||||
'resource_type': row.get('resource_type'),
|
||||
'resource_id': row.get('resource_id'),
|
||||
'summary': row.get('summary'),
|
||||
'changes': changes,
|
||||
'outcome': row.get('outcome'),
|
||||
'http_method': row.get('http_method'),
|
||||
'route': row.get('route'),
|
||||
'integrity_ok': row.get('integrity_ok'),
|
||||
'chain_ok': row.get('chain_ok'),
|
||||
'tampered': row.get('tampered'),
|
||||
}
|
||||
|
||||
records = result.get('records') or []
|
||||
return {
|
||||
'total': result.get('total', len(records)),
|
||||
'returned': len(records),
|
||||
'tampered_count': result.get('tampered_count', 0),
|
||||
'integrity_failed_count': result.get('integrity_failed_count', 0),
|
||||
'chain_failed_count': result.get('chain_failed_count', 0),
|
||||
'records': [record(row) for row in records],
|
||||
}
|
||||
|
||||
|
||||
class PipelineID(Arguments):
|
||||
pipeline_uuid: UUID
|
||||
|
||||
@@ -45,29 +168,55 @@ class CreateKnowledgeBase(CreatePipeline):
|
||||
retrieval_settings: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
#: Each entry is ``(schema, description, writes, extra_permission)``. ``writes``
|
||||
#: selects resource.manage vs resource.view; ``extra_permission`` narrows a tool
|
||||
#: further (e.g. the audit surface needs ``audit.view``, which only the owner
|
||||
#: and admin hold) and both are enforced identically when listing and calling.
|
||||
TOOLS = {
|
||||
'list_resources': (ListResources, 'List existing Workspace resources. Discover IDs before using them.', False),
|
||||
'get_pipeline': (PipelineID, 'Read a Pipeline configuration with secrets redacted.', False),
|
||||
'get_knowledge_schema': (EngineID, 'Get the engine creation and retrieval configuration schemas.', False),
|
||||
'create_pipeline': (CreatePipeline, 'Create an unconnected Pipeline draft. Requires user confirmation.', True),
|
||||
'list_resources': (ListResources, 'List existing Workspace resources. Discover IDs before using them.', False, None),
|
||||
'get_pipeline': (PipelineID, 'Read a Pipeline configuration with secrets redacted.', False, None),
|
||||
'get_knowledge_schema': (EngineID, 'Get the engine creation and retrieval configuration schemas.', False, None),
|
||||
'list_operation_logs': (
|
||||
ListOperationLogs,
|
||||
'Read the Workspace operation trace (who changed or viewed what, with before/after fields). '
|
||||
'Owner and admin only. Use it to answer "who did what" questions about this Workspace.',
|
||||
False,
|
||||
Permission.AUDIT_VIEW,
|
||||
),
|
||||
'create_pipeline': (CreatePipeline, 'Create an unconnected Pipeline draft. Requires user confirmation.', True, None),
|
||||
'configure_pipeline': (
|
||||
ConfigurePipeline,
|
||||
'Set the local-agent model, system prompt and complete knowledge-base binding list. Requires confirmation.',
|
||||
True,
|
||||
None,
|
||||
),
|
||||
'create_knowledge_base': (
|
||||
CreateKnowledgeBase,
|
||||
'Create a knowledge base using an installed engine. Read its schema first. Requires confirmation.',
|
||||
True,
|
||||
None,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _required_permission(writes: bool, extra: Permission | None) -> Permission:
|
||||
"""Return the permission a tool needs beyond the base resource verbs."""
|
||||
|
||||
if extra is not None:
|
||||
return extra
|
||||
return Permission.RESOURCE_MANAGE if writes else Permission.RESOURCE_VIEW
|
||||
|
||||
|
||||
def _authorized(context, name: str) -> bool:
|
||||
_schema, _description, writes, extra = TOOLS[name]
|
||||
return _required_permission(writes, extra).value in context.workspace.permissions
|
||||
|
||||
|
||||
def validate_call(context, name: str, arguments: dict) -> Arguments:
|
||||
if name not in TOOLS:
|
||||
raise ValueError('Unknown management tool')
|
||||
schema, _, writes = TOOLS[name]
|
||||
require_permission(context, Permission.RESOURCE_MANAGE if writes else Permission.RESOURCE_VIEW)
|
||||
schema, _, writes, extra = TOOLS[name]
|
||||
require_permission(context, _required_permission(writes, extra))
|
||||
return schema.model_validate(arguments)
|
||||
|
||||
|
||||
@@ -80,8 +229,8 @@ def tool_definitions(context) -> list[LLMTool]:
|
||||
parameters=schema.model_json_schema(),
|
||||
func=execute_tool,
|
||||
)
|
||||
for name, (schema, description, writes) in TOOLS.items()
|
||||
if not writes or Permission.RESOURCE_MANAGE in context.workspace.permissions
|
||||
for name, (schema, description, _writes, _extra) in TOOLS.items()
|
||||
if _authorized(context, name)
|
||||
]
|
||||
|
||||
|
||||
@@ -101,6 +250,23 @@ async def execute_tool(ap, context, name: str, arguments: dict):
|
||||
fields = ('uuid', 'name', 'description', 'abilities', 'knowledge_engine_plugin_id')
|
||||
resources = [{key: item[key] for key in fields if key in item} for item in resources]
|
||||
return {'total': len(resources), 'items': redact_secrets(resources)}
|
||||
if name == 'list_operation_logs':
|
||||
# Delegates to the operation-trace service, so the assistant shows the
|
||||
# same records and tamper verification as the settings panel, then
|
||||
# compacts them so a full page still fits the per-result budget.
|
||||
result = await ap.workspace_settings_service.query_logs(
|
||||
context.workspace_uuid,
|
||||
limit=args['limit'],
|
||||
search=args['search'],
|
||||
action=args['action'],
|
||||
resource_type=args['resource_type'],
|
||||
actor_account_uuid=args['actor'],
|
||||
)
|
||||
compact = _compact_operation_records(result)
|
||||
# Echo the applied filters so the model can see why a result is empty
|
||||
# and widen a filter instead of repeating an empty targeted query.
|
||||
compact['query'] = {key: args[key] for key in ('search', 'action', 'resource_type', 'actor') if args.get(key)}
|
||||
return compact
|
||||
if name == 'get_pipeline':
|
||||
return await ap.pipeline_service.get_pipeline(context, args['pipeline_uuid'])
|
||||
if name == 'get_knowledge_schema':
|
||||
|
||||
@@ -1653,6 +1653,7 @@ class WorkspaceSettingsService:
|
||||
since: datetime.datetime | None = None,
|
||||
until: datetime.datetime | None = None,
|
||||
integrity: str | None = None,
|
||||
search: str | None = None,
|
||||
) -> dict[str, typing.Any]:
|
||||
"""Return one page of operation records plus a Workspace-wide summary.
|
||||
|
||||
@@ -1664,6 +1665,13 @@ class WorkspaceSettingsService:
|
||||
``integrity`` optionally narrows the listing to the records that failed
|
||||
verification, which lets the panel make its counters actionable instead
|
||||
of decorative.
|
||||
|
||||
``search`` is a case-insensitive substring match over the action,
|
||||
resource type, resource id, actor name and summary. It exists so a
|
||||
caller can ask a narrow question ("install", a plugin name, an account)
|
||||
without pulling the whole history: the exact ``action`` filter compares
|
||||
the stored key, so an approximate term like ``install`` would otherwise
|
||||
return nothing and force a full dump.
|
||||
"""
|
||||
|
||||
resolved_limit = max(1, min(int(limit or DEFAULT_PAGE_SIZE), MAX_PAGE_SIZE))
|
||||
@@ -1681,6 +1689,17 @@ class WorkspaceSettingsService:
|
||||
filters.append(model.resource_type == resource_type)
|
||||
if actor_account_uuid:
|
||||
filters.append(model.actor_account_uuid == actor_account_uuid)
|
||||
if search:
|
||||
needle = f'%{str(search).strip()}%'
|
||||
filters.append(
|
||||
sqlalchemy.or_(
|
||||
model.action.ilike(needle),
|
||||
model.resource_type.ilike(needle),
|
||||
model.resource_id.ilike(needle),
|
||||
model.actor_name.ilike(needle),
|
||||
model.summary.ilike(needle),
|
||||
)
|
||||
)
|
||||
if level is not None:
|
||||
filters.append(model.level == int(level))
|
||||
if since is not None:
|
||||
|
||||
@@ -45,7 +45,9 @@ export default function AssistantToolResult({
|
||||
? result
|
||||
: Array.isArray(data.items)
|
||||
? data.items
|
||||
: null;
|
||||
: Array.isArray(data.records)
|
||||
? data.records
|
||||
: null;
|
||||
const total = typeof data.total === 'number' ? data.total : items?.length;
|
||||
const kind = tool?.arguments.kind;
|
||||
const label =
|
||||
@@ -142,7 +144,13 @@ export default function AssistantToolResult({
|
||||
: {};
|
||||
return (
|
||||
<li key={index} className="truncate">
|
||||
{String(entry.name || entry.uuid || '—')}
|
||||
{String(
|
||||
entry.name ||
|
||||
entry.summary ||
|
||||
entry.action ||
|
||||
entry.uuid ||
|
||||
'—',
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -15,6 +15,11 @@ import { toast } from 'sonner';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Item, ItemContent, ItemMedia, ItemTitle } from '@/components/ui/item';
|
||||
import {
|
||||
@@ -341,6 +346,9 @@ export default function OperationTracePanel({
|
||||
const [retentionDays, setRetentionDays] = useState<number>(30);
|
||||
const [maxRows, setMaxRows] = useState<number>(20000);
|
||||
const [dedupeSeconds, setDedupeSeconds] = useState<number>(60);
|
||||
// Retention is an advanced, rarely-touched policy: it collapses by default so
|
||||
// the capture level and the records list stay the focus of the panel.
|
||||
const [retentionOpen, setRetentionOpen] = useState(false);
|
||||
// Only one record shows its full change detail at a time, so the list stays
|
||||
// scannable: the payload diff is long and is opt-in per row.
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
@@ -631,66 +639,85 @@ export default function OperationTracePanel({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t('operationTrace.retention')}
|
||||
</h3>
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t('operationTrace.retentionDays')}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-28"
|
||||
min={governance?.limits.min_retention_days ?? 1}
|
||||
max={governance?.limits.max_retention_days ?? 3650}
|
||||
value={retentionDays}
|
||||
disabled={!canConfigure}
|
||||
onChange={(event) =>
|
||||
setRetentionDays(Number(event.target.value))
|
||||
}
|
||||
<Collapsible open={retentionOpen} onOpenChange={setRetentionOpen}>
|
||||
<section className="space-y-3">
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-2 text-left">
|
||||
<ChevronDown
|
||||
className={`size-4 shrink-0 text-muted-foreground transition-transform ${
|
||||
retentionOpen ? '' : '-rotate-90'
|
||||
}`}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t('operationTrace.maxRows')}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-32"
|
||||
min={governance?.limits.min_max_rows ?? 100}
|
||||
max={governance?.limits.max_max_rows ?? 500000}
|
||||
value={maxRows}
|
||||
disabled={!canConfigure}
|
||||
onChange={(event) => setMaxRows(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t('operationTrace.dedupeWindow')}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-28"
|
||||
min={governance?.limits.min_dedupe_window_seconds ?? 0}
|
||||
max={governance?.limits.max_dedupe_window_seconds ?? 3600}
|
||||
value={dedupeSeconds}
|
||||
disabled={!canConfigure}
|
||||
onChange={(event) =>
|
||||
setDedupeSeconds(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void saveRetention()}
|
||||
disabled={!canConfigure || saving}
|
||||
>
|
||||
{t('operationTrace.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t('operationTrace.retention')}
|
||||
</h3>
|
||||
{!retentionOpen && (
|
||||
<span className="ml-auto truncate text-xs text-muted-foreground">
|
||||
{t('operationTrace.retentionSummary', {
|
||||
days: retentionDays,
|
||||
rows: maxRows,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="flex flex-wrap items-end gap-2 pt-3">
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t('operationTrace.retentionDays')}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-28"
|
||||
min={governance?.limits.min_retention_days ?? 1}
|
||||
max={governance?.limits.max_retention_days ?? 3650}
|
||||
value={retentionDays}
|
||||
disabled={!canConfigure}
|
||||
onChange={(event) =>
|
||||
setRetentionDays(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t('operationTrace.maxRows')}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-32"
|
||||
min={governance?.limits.min_max_rows ?? 100}
|
||||
max={governance?.limits.max_max_rows ?? 500000}
|
||||
value={maxRows}
|
||||
disabled={!canConfigure}
|
||||
onChange={(event) => setMaxRows(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs">
|
||||
<span className="text-muted-foreground">
|
||||
{t('operationTrace.dedupeWindow')}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-28"
|
||||
min={governance?.limits.min_dedupe_window_seconds ?? 0}
|
||||
max={governance?.limits.max_dedupe_window_seconds ?? 3600}
|
||||
value={dedupeSeconds}
|
||||
disabled={!canConfigure}
|
||||
onChange={(event) =>
|
||||
setDedupeSeconds(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void saveRetention()}
|
||||
disabled={!canConfigure || saving}
|
||||
>
|
||||
{t('operationTrace.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</section>
|
||||
</Collapsible>
|
||||
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -21,6 +21,7 @@ const enUS = {
|
||||
create_knowledge_base: 'Create knowledge base',
|
||||
get_pipeline: 'Read Pipeline',
|
||||
get_knowledge_schema: 'Read knowledge engine schema',
|
||||
list_operation_logs: 'Read operation trace',
|
||||
},
|
||||
resources: {
|
||||
models: 'Find chat models',
|
||||
@@ -2724,6 +2725,7 @@ const enUS = {
|
||||
description: 'Trace admin and owner edits and views, and what changed.',
|
||||
captureLevel: 'Capture level',
|
||||
retention: 'Retention',
|
||||
retentionSummary: 'Keep {{days}} days · up to {{rows}} records',
|
||||
retentionDays: 'Retention days',
|
||||
maxRows: 'Max records',
|
||||
dedupeWindow: 'Dedupe window (s)',
|
||||
|
||||
@@ -21,6 +21,7 @@ const esES = {
|
||||
create_knowledge_base: 'Crear base de conocimiento',
|
||||
get_pipeline: 'Consultar Pipeline',
|
||||
get_knowledge_schema: 'Consultar el esquema del motor de conocimiento',
|
||||
list_operation_logs: 'Consultar la trazabilidad de operaciones',
|
||||
},
|
||||
resources: {
|
||||
models: 'Buscar modelos de chat',
|
||||
@@ -2779,6 +2780,7 @@ const esES = {
|
||||
description: 'Rastrea cambios y consultas de los administradores',
|
||||
captureLevel: 'Nivel de captura',
|
||||
retention: 'Retención',
|
||||
retentionSummary: 'Conservar {{days}} días · hasta {{rows}} registros',
|
||||
retentionDays: 'Días de retención',
|
||||
maxRows: 'Registros máximos',
|
||||
dedupeWindow: 'Ventana de deduplicación (s)',
|
||||
|
||||
@@ -21,6 +21,7 @@ const jaJP = {
|
||||
create_knowledge_base: 'ナレッジベース作成',
|
||||
get_pipeline: 'Pipeline 参照',
|
||||
get_knowledge_schema: 'エンジン設定の参照',
|
||||
list_operation_logs: '操作トレーサビリティの参照',
|
||||
},
|
||||
resources: {
|
||||
models: 'チャットモデル検索',
|
||||
@@ -2742,6 +2743,7 @@ const jaJP = {
|
||||
description: '管理者とオーナーの操作を追跡します。',
|
||||
captureLevel: 'キャプチャレベル',
|
||||
retention: '保持ポリシー',
|
||||
retentionSummary: '{{days}} 日間保持 · 最大 {{rows}} 件',
|
||||
retentionDays: '保持日数',
|
||||
maxRows: '最大レコード数',
|
||||
dedupeWindow: '重複排除ウィンドウ(秒)',
|
||||
|
||||
@@ -21,6 +21,7 @@ const ruRU = {
|
||||
create_knowledge_base: 'Создать базу знаний',
|
||||
get_pipeline: 'Посмотреть Pipeline',
|
||||
get_knowledge_schema: 'Посмотреть схему движка знаний',
|
||||
list_operation_logs: 'Просмотр трассировки операций',
|
||||
},
|
||||
resources: {
|
||||
models: 'Найти чат-модели',
|
||||
@@ -2749,6 +2750,7 @@ const ruRU = {
|
||||
description: 'Отслеживание изменений и просмотров администраторов',
|
||||
captureLevel: 'Уровень записи',
|
||||
retention: 'Хранение',
|
||||
retentionSummary: 'Хранить {{days}} дн. · до {{rows}} записей',
|
||||
retentionDays: 'Дней хранения',
|
||||
maxRows: 'Максимум записей',
|
||||
dedupeWindow: 'Окно дедупликации (с)',
|
||||
|
||||
@@ -21,6 +21,7 @@ const thTH = {
|
||||
create_knowledge_base: 'สร้างฐานความรู้',
|
||||
get_pipeline: 'ดู Pipeline',
|
||||
get_knowledge_schema: 'ดูสคีมาของ knowledge engine',
|
||||
list_operation_logs: 'ดูการติดตามการดำเนินการ',
|
||||
},
|
||||
resources: {
|
||||
models: 'ค้นหาโมเดลแชต',
|
||||
@@ -2676,6 +2677,7 @@ const thTH = {
|
||||
description: 'ติดตามการแก้ไขและการดูของผู้ดูแลและเจ้าของ',
|
||||
captureLevel: 'ระดับการบันทึก',
|
||||
retention: 'นโยบายการเก็บรักษา',
|
||||
retentionSummary: 'เก็บ {{days}} วัน · สูงสุด {{rows}} รายการ',
|
||||
retentionDays: 'จำนวนวันเก็บรักษา',
|
||||
maxRows: 'จำนวนบันทึกสูงสุด',
|
||||
dedupeWindow: 'ช่วงเวลาลบรายการซ้ำ (วินาที)',
|
||||
|
||||
@@ -22,6 +22,7 @@ const viVN = {
|
||||
create_knowledge_base: 'Tạo cơ sở tri thức',
|
||||
get_pipeline: 'Xem Pipeline',
|
||||
get_knowledge_schema: 'Xem lược đồ knowledge engine',
|
||||
list_operation_logs: 'Truy vấn truy vết thao tác',
|
||||
},
|
||||
resources: {
|
||||
models: 'Tìm mô hình trò chuyện',
|
||||
@@ -2711,6 +2712,7 @@ const viVN = {
|
||||
description: 'Theo dõi thao tác sửa và xem của quản trị viên',
|
||||
captureLevel: 'Mức ghi nhận',
|
||||
retention: 'Chính sách lưu trữ',
|
||||
retentionSummary: 'Lưu {{days}} ngày · tối đa {{rows}} bản ghi',
|
||||
retentionDays: 'Số ngày lưu',
|
||||
maxRows: 'Số bản ghi tối đa',
|
||||
dedupeWindow: 'Khoảng khử trùng lặp (giây)',
|
||||
|
||||
@@ -19,6 +19,7 @@ const zhHans = {
|
||||
create_knowledge_base: '创建知识库',
|
||||
get_pipeline: '查看 Pipeline',
|
||||
get_knowledge_schema: '查看知识引擎配置',
|
||||
list_operation_logs: '查询操作溯源',
|
||||
},
|
||||
resources: {
|
||||
models: '查询聊天模型',
|
||||
@@ -2574,6 +2575,7 @@ const zhHans = {
|
||||
description: '追踪管理员与拥有者的修改和查看操作,记录“什么改成了什么”。',
|
||||
captureLevel: '操作等级',
|
||||
retention: '保留策略',
|
||||
retentionSummary: '保留 {{days}} 天 · 最多 {{rows}} 条',
|
||||
retentionDays: '保留天数',
|
||||
maxRows: '最大记录数',
|
||||
dedupeWindow: '去重窗口(秒)',
|
||||
|
||||
@@ -19,6 +19,7 @@ const zhHant = {
|
||||
create_knowledge_base: '建立知識庫',
|
||||
get_pipeline: '查看 Pipeline',
|
||||
get_knowledge_schema: '查看知識引擎設定',
|
||||
list_operation_logs: '查詢操作溯源',
|
||||
},
|
||||
resources: {
|
||||
models: '查詢聊天模型',
|
||||
@@ -2575,6 +2576,7 @@ const zhHant = {
|
||||
description: '追蹤管理員與擁有者的修改與查看操作,記錄「什麼改成了什麼」。',
|
||||
captureLevel: '操作等級',
|
||||
retention: '保留策略',
|
||||
retentionSummary: '保留 {{days}} 天 · 最多 {{rows}} 條',
|
||||
retentionDays: '保留天數',
|
||||
maxRows: '最大記錄數',
|
||||
dedupeWindow: '去重視窗(秒)',
|
||||
|
||||
Reference in New Issue
Block a user