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:
TyperBody
2026-09-28 01:56:00 +08:00
parent 9ecb469d13
commit 7eeb0393a6
13 changed files with 309 additions and 69 deletions
@@ -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">
+2
View File
@@ -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)',
+2
View File
@@ -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)',
+2
View File
@@ -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: '重複排除ウィンドウ(秒)',
+2
View File
@@ -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: 'Окно дедупликации (с)',
+2
View File
@@ -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: 'ช่วงเวลาลบรายการซ้ำ (วินาที)',
+2
View File
@@ -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)',
+2
View File
@@ -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: '去重窗口(秒)',
+2
View File
@@ -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: '去重視窗(秒)',