mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-28 20:36:42 +08:00
fix(operation-trace): correct capture scope, broaden coverage, polish panel
- Capture scope: treat the ``audit`` bucket (viewing the log / tracing settings) as a read-level observation instead of recording it from the mutation level. At "mutations only" the log no longer fills with GET page views; views are recorded only at the read level. - Coverage: classify plugin lifecycle (install / upgrade / config / page), skills, knowledge bases and MCP servers, with dedicated actions and resource types, so those operations are traced instead of falling into the generic resource bucket. Route rules now support multi-fragment AND matching and per-verb read/write actions. - Panel: move "Download logs" next to "Refresh" in the toolbar. - Verification: count hash mismatches and broken links separately instead of collapsing both into one "tampered" signal. - i18n: add the new action labels plus the split verification labels in all eight locales.
This commit is contained in:
@@ -252,6 +252,74 @@ ACTION_RULE_TABLE: typing.Final[tuple[ActionRule, ...]] = (
|
||||
bucket='read',
|
||||
resource_type='member',
|
||||
),
|
||||
# --- Extension lifecycle (plugins, pages, skills, MCP) --------------
|
||||
ActionRule(
|
||||
action='plugin_view',
|
||||
category='extension',
|
||||
bucket='read',
|
||||
resource_type='plugin',
|
||||
),
|
||||
ActionRule(
|
||||
action='plugin_config',
|
||||
category='extension',
|
||||
bucket='write',
|
||||
resource_type='plugin',
|
||||
),
|
||||
ActionRule(
|
||||
action='plugin_install',
|
||||
category='extension',
|
||||
bucket='write',
|
||||
resource_type='plugin',
|
||||
),
|
||||
ActionRule(
|
||||
action='plugin_upgrade',
|
||||
category='extension',
|
||||
bucket='write',
|
||||
resource_type='plugin',
|
||||
),
|
||||
ActionRule(
|
||||
action='page_view',
|
||||
category='extension',
|
||||
bucket='read',
|
||||
resource_type='plugin_page',
|
||||
),
|
||||
ActionRule(
|
||||
action='skill_view',
|
||||
category='extension',
|
||||
bucket='read',
|
||||
resource_type='skill',
|
||||
),
|
||||
ActionRule(
|
||||
action='skill_install',
|
||||
category='extension',
|
||||
bucket='write',
|
||||
resource_type='skill',
|
||||
),
|
||||
# --- Knowledge & MCP -------------------------------------------------
|
||||
ActionRule(
|
||||
action='knowledge_base_view',
|
||||
category='knowledge',
|
||||
bucket='read',
|
||||
resource_type='knowledge_base',
|
||||
),
|
||||
ActionRule(
|
||||
action='knowledge_base_update',
|
||||
category='knowledge',
|
||||
bucket='write',
|
||||
resource_type='knowledge_base',
|
||||
),
|
||||
ActionRule(
|
||||
action='mcp_view',
|
||||
category='integration',
|
||||
bucket='read',
|
||||
resource_type='mcp_server',
|
||||
),
|
||||
ActionRule(
|
||||
action='mcp_config',
|
||||
category='integration',
|
||||
bucket='write',
|
||||
resource_type='mcp_server',
|
||||
),
|
||||
# --- Generic resource verbs -----------------------------------------
|
||||
ActionRule(
|
||||
action='export',
|
||||
@@ -313,20 +381,43 @@ ACTION_RULES_BY_ACTION: typing.Final[dict[str, ActionRule]] = {rule.action: rule
|
||||
|
||||
_READ_METHODS: typing.Final = frozenset({'GET', 'HEAD', 'OPTIONS'})
|
||||
|
||||
# Route fragments, evaluated in order, that refine the generic verb mapping.
|
||||
# Longer, more specific fragments must precede their prefixes.
|
||||
_ROUTE_RULES: typing.Final[tuple[tuple[str, str], ...]] = (
|
||||
('/settings/operation-logs/export', 'export'),
|
||||
('/settings/operation-logs', 'audit_log_view'),
|
||||
('/settings/operation-level', 'settings_update'),
|
||||
('/settings/governance', 'settings_update'),
|
||||
('/settings/limits', 'settings_update'),
|
||||
('/members', 'member_view'),
|
||||
('/invitations', 'member_invite'),
|
||||
('/export', 'export'),
|
||||
('/debug', 'debug'),
|
||||
('/execute', 'execute'),
|
||||
('/publish', 'publish'),
|
||||
# Route rules evaluated in order; the first match wins, so the most specific
|
||||
# rule must precede its prefix. Each rule is ``(fragments, read_action,
|
||||
# write_action)``: every fragment must appear in the lowered route (a tuple
|
||||
# expresses an AND, which lets ``/plugins/<author>/<name>/config`` be told
|
||||
# apart from ``/plugins/<author>/<name>``), and the action is selected by
|
||||
# whether the method is a read verb.
|
||||
_ROUTE_RULES: typing.Final[tuple[tuple[tuple[str, ...], str, str], ...]] = (
|
||||
# --- Audit surface itself -------------------------------------------
|
||||
(('/settings/operation-logs/export',), 'export', 'export'),
|
||||
(('/settings/operation-logs',), 'audit_log_view', 'audit_log_view'),
|
||||
(('/settings/operation-level',), 'settings_view', 'settings_update'),
|
||||
(('/settings/governance',), 'settings_view', 'settings_update'),
|
||||
(('/settings/limits',), 'settings_view', 'settings_update'),
|
||||
# --- Extension lifecycle: plugins -----------------------------------
|
||||
(('/plugins/install',), 'plugin_view', 'plugin_install'),
|
||||
(('/plugins/github',), 'plugin_view', 'plugin_view'),
|
||||
(('/plugins/', '/config'), 'plugin_view', 'plugin_config'),
|
||||
(('/plugins/', '/page-api'), 'page_view', 'page_view'),
|
||||
(('/plugins/', '/upgrade'), 'plugin_view', 'plugin_upgrade'),
|
||||
(('/plugins/', '/logs'), 'plugin_view', 'plugin_view'),
|
||||
(('/plugins',), 'plugin_view', 'plugin_view'),
|
||||
(('/extensions',), 'plugin_view', 'plugin_config'),
|
||||
# --- Extension lifecycle: skills ------------------------------------
|
||||
(('/skills/', '/install'), 'skill_view', 'skill_install'),
|
||||
(('/skills',), 'skill_view', 'skill_view'),
|
||||
# --- Knowledge bases & MCP servers ----------------------------------
|
||||
(('/knowledge/',), 'knowledge_base_view', 'knowledge_base_update'),
|
||||
(('/mcp/', '/config'), 'mcp_view', 'mcp_config'),
|
||||
(('/mcp',), 'mcp_view', 'mcp_config'),
|
||||
# --- Member management ----------------------------------------------
|
||||
(('/members',), 'member_view', 'member_role_update'),
|
||||
(('/invitations',), 'member_view', 'member_invite'),
|
||||
# --- Generic resource verbs -----------------------------------------
|
||||
(('/export',), 'export', 'export'),
|
||||
(('/debug',), 'debug', 'debug'),
|
||||
(('/execute',), 'execute', 'execute'),
|
||||
(('/publish',), 'publish', 'publish'),
|
||||
)
|
||||
|
||||
|
||||
@@ -339,22 +430,13 @@ def classify(method: str, route: str) -> ActionRule:
|
||||
|
||||
upper_method = (method or 'GET').upper()
|
||||
lowered_route = (route or '').lower()
|
||||
is_read = upper_method in _READ_METHODS
|
||||
|
||||
route_action: str | None = None
|
||||
for fragment, candidate in _ROUTE_RULES:
|
||||
if fragment in lowered_route:
|
||||
route_action = candidate
|
||||
break
|
||||
for fragments, read_action, write_action in _ROUTE_RULES:
|
||||
if all(fragment in lowered_route for fragment in fragments):
|
||||
return ACTION_RULES_BY_ACTION[read_action if is_read else write_action]
|
||||
|
||||
if route_action is not None:
|
||||
if route_action == 'settings_update' and upper_method in _READ_METHODS:
|
||||
return ACTION_RULES_BY_ACTION['settings_view']
|
||||
if route_action == 'member_view' and upper_method not in _READ_METHODS:
|
||||
# POST /members/<id> style routes are writes on the member family.
|
||||
return ACTION_RULES_BY_ACTION['member_role_update']
|
||||
return ACTION_RULES_BY_ACTION[route_action]
|
||||
|
||||
if upper_method in _READ_METHODS:
|
||||
if is_read:
|
||||
return ACTION_RULES_BY_ACTION['view']
|
||||
if upper_method == 'POST':
|
||||
return ACTION_RULES_BY_ACTION['create']
|
||||
@@ -380,13 +462,19 @@ def role_may_configure(role: str | None) -> bool:
|
||||
|
||||
|
||||
def bucket_allows(bucket: str, effective_level: int) -> bool:
|
||||
"""Return whether a capture bucket should be persisted at this level."""
|
||||
"""Return whether a capture bucket should be persisted at this level.
|
||||
|
||||
``write`` buckets are recorded from the mutation level upward. ``read``
|
||||
and ``audit`` are both *observation* buckets: viewing a resource and
|
||||
viewing the audit surface itself are reads, so a mutation-level Workspace
|
||||
must not fill its log with page views. They are only persisted once the
|
||||
Workspace opts into the read level.
|
||||
"""
|
||||
|
||||
if effective_level <= OPERATION_LEVEL_NONE:
|
||||
return False
|
||||
if bucket == 'read':
|
||||
if bucket in ('read', 'audit'):
|
||||
return effective_level >= OPERATION_LEVEL_READ
|
||||
# ``write`` and ``audit`` are both recorded from mutation level upward.
|
||||
return effective_level >= OPERATION_LEVEL_MUTATION
|
||||
|
||||
|
||||
@@ -1212,7 +1300,12 @@ class WorkspaceSettingsService:
|
||||
'total': total,
|
||||
'limit': resolved_limit,
|
||||
'offset': resolved_offset,
|
||||
# Report the two failure modes separately so the panel can tell a
|
||||
# content edit (integrity) apart from a dropped link (chain) instead
|
||||
# of collapsing both into a single "tampered" signal.
|
||||
'tampered_count': sum(1 for record in records if record.get('tampered')),
|
||||
'integrity_failed_count': sum(1 for record in records if not record.get('integrity_ok')),
|
||||
'chain_failed_count': sum(1 for record in records if not record.get('chain_ok')),
|
||||
}
|
||||
|
||||
def _serialize_log(self, row: typing.Any, *, previous_row: typing.Any | None = None) -> dict[str, typing.Any]:
|
||||
|
||||
@@ -323,6 +323,19 @@ export default function OperationTracePanel({
|
||||
)}
|
||||
{t('operationTrace.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void downloadLogs()}
|
||||
disabled={exporting}
|
||||
>
|
||||
{exporting ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="size-3.5" />
|
||||
)}
|
||||
{t('operationTrace.export')}
|
||||
</Button>
|
||||
</PanelToolbar>
|
||||
|
||||
<PanelBody className="space-y-6">
|
||||
@@ -429,13 +442,22 @@ export default function OperationTracePanel({
|
||||
{t('operationTrace.records')}
|
||||
</h3>
|
||||
<Badge variant="secondary">{total}</Badge>
|
||||
{(page?.tampered_count ?? 0) > 0 && (
|
||||
<Badge variant="destructive">
|
||||
{t('operationTrace.tamperedCount', {
|
||||
count: page?.tampered_count ?? 0,
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
{(page?.tampered_count ?? 0) > 0 &&
|
||||
(page?.integrity_failed_count ?? 0) > 0 && (
|
||||
<Badge variant="destructive">
|
||||
{t('operationTrace.integrityFailedCount', {
|
||||
count: page?.integrity_failed_count ?? 0,
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
{(page?.tampered_count ?? 0) > 0 &&
|
||||
(page?.chain_failed_count ?? 0) > 0 && (
|
||||
<Badge variant="destructive">
|
||||
{t('operationTrace.chainFailedCount', {
|
||||
count: page?.chain_failed_count ?? 0,
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto flex flex-wrap items-center gap-2">
|
||||
<Select
|
||||
@@ -490,19 +512,6 @@ export default function OperationTracePanel({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void downloadLogs()}
|
||||
disabled={exporting}
|
||||
>
|
||||
{exporting ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="size-3.5" />
|
||||
)}
|
||||
{t('operationTrace.export')}
|
||||
</Button>
|
||||
<Select
|
||||
value={query.actor ?? ALL_VALUE}
|
||||
onValueChange={(value) =>
|
||||
|
||||
@@ -76,6 +76,10 @@ export interface OperationLogPage {
|
||||
offset: number;
|
||||
/** How many records on this page failed hash or chain verification. */
|
||||
tampered_count: number;
|
||||
/** Records whose stored hash no longer matches their content. */
|
||||
integrity_failed_count: number;
|
||||
/** Records whose predecessor link is broken. */
|
||||
chain_failed_count: number;
|
||||
}
|
||||
|
||||
export interface OperationLogFilters {
|
||||
|
||||
@@ -2753,6 +2753,8 @@ const enUS = {
|
||||
tamperedBadge: 'Possibly tampered',
|
||||
verifiedBadge: 'Verified',
|
||||
tamperedCount: '{{count}} records failed verification',
|
||||
integrityFailedCount: '{{count}} hash mismatches',
|
||||
chainFailedCount: '{{count}} broken links',
|
||||
levels: {
|
||||
off: {
|
||||
label: 'Tracing off',
|
||||
@@ -2783,6 +2785,17 @@ const enUS = {
|
||||
member_role_update: 'Update member role',
|
||||
member_remove: 'Remove member',
|
||||
member_view: 'View members',
|
||||
plugin_view: 'View extension pages',
|
||||
plugin_config: 'Change extension config',
|
||||
plugin_install: 'Install extension',
|
||||
plugin_upgrade: 'Upgrade extension',
|
||||
page_view: 'View extension page',
|
||||
skill_view: 'View skill',
|
||||
skill_install: 'Install skill',
|
||||
knowledge_base_view: 'View knowledge base',
|
||||
knowledge_base_update: 'Change knowledge base',
|
||||
mcp_view: 'View MCP server',
|
||||
mcp_config: 'Change MCP server',
|
||||
export: 'Export data',
|
||||
execute: 'Execute task',
|
||||
debug: 'Debug task',
|
||||
|
||||
@@ -2808,6 +2808,8 @@ const esES = {
|
||||
tamperedBadge: 'Posible manipulación',
|
||||
verifiedBadge: 'Verificado',
|
||||
tamperedCount: '{{count}} registros fallaron la verificación',
|
||||
integrityFailedCount: '{{count}} hashes no coinciden',
|
||||
chainFailedCount: '{{count}} enlaces rotos',
|
||||
levels: {
|
||||
off: {
|
||||
label: 'Sin trazabilidad',
|
||||
@@ -2838,6 +2840,17 @@ const esES = {
|
||||
member_role_update: 'Actualizar rol del miembro',
|
||||
member_remove: 'Eliminar miembro',
|
||||
member_view: 'Ver miembros',
|
||||
plugin_view: 'Ver páginas de extensión',
|
||||
plugin_config: 'Cambiar configuración de extensión',
|
||||
plugin_install: 'Instalar extensión',
|
||||
plugin_upgrade: 'Actualizar extensión',
|
||||
page_view: 'Ver página de extensión',
|
||||
skill_view: 'Ver habilidad',
|
||||
skill_install: 'Instalar habilidad',
|
||||
knowledge_base_view: 'Ver base de conocimiento',
|
||||
knowledge_base_update: 'Cambiar base de conocimiento',
|
||||
mcp_view: 'Ver servidor MCP',
|
||||
mcp_config: 'Cambiar servidor MCP',
|
||||
export: 'Exportar datos',
|
||||
execute: 'Ejecutar tarea',
|
||||
debug: 'Depurar tarea',
|
||||
|
||||
@@ -2771,6 +2771,8 @@ const jaJP = {
|
||||
tamperedBadge: '改ざんの可能性',
|
||||
verifiedBadge: '検証済み',
|
||||
tamperedCount: '{{count}} 件が検証に失敗',
|
||||
integrityFailedCount: '{{count}} 件のハッシュ不一致',
|
||||
chainFailedCount: '{{count}} 件のリンク断絶',
|
||||
levels: {
|
||||
off: {
|
||||
label: 'トレース無効',
|
||||
@@ -2801,6 +2803,17 @@ const jaJP = {
|
||||
member_role_update: 'メンバーのロールを変更',
|
||||
member_remove: 'メンバーを削除',
|
||||
member_view: 'メンバーを閲覧',
|
||||
plugin_view: '拡張ページを閲覧',
|
||||
plugin_config: '拡張設定を変更',
|
||||
plugin_install: '拡張をインストール',
|
||||
plugin_upgrade: '拡張をアップグレード',
|
||||
page_view: '拡張ページを閲覧',
|
||||
skill_view: 'スキルを閲覧',
|
||||
skill_install: 'スキルをインストール',
|
||||
knowledge_base_view: 'ナレッジベースを閲覧',
|
||||
knowledge_base_update: 'ナレッジベースを変更',
|
||||
mcp_view: 'MCP サーバーを閲覧',
|
||||
mcp_config: 'MCP サーバーを変更',
|
||||
export: 'データをエクスポート',
|
||||
execute: 'タスクを実行',
|
||||
debug: 'タスクをデバッグ',
|
||||
|
||||
@@ -2778,6 +2778,8 @@ const ruRU = {
|
||||
tamperedBadge: 'Возможна подмена',
|
||||
verifiedBadge: 'Проверено',
|
||||
tamperedCount: 'Записей с ошибкой проверки: {{count}}',
|
||||
integrityFailedCount: 'Несовпадение хэша: {{count}}',
|
||||
chainFailedCount: 'Разрыв связи: {{count}}',
|
||||
levels: {
|
||||
off: {
|
||||
label: 'Трассировка выключена',
|
||||
@@ -2808,6 +2810,17 @@ const ruRU = {
|
||||
member_role_update: 'Изменение роли участника',
|
||||
member_remove: 'Удаление участника',
|
||||
member_view: 'Просмотр участников',
|
||||
plugin_view: 'Просмотр страниц расширения',
|
||||
plugin_config: 'Изменение настроек расширения',
|
||||
plugin_install: 'Установка расширения',
|
||||
plugin_upgrade: 'Обновление расширения',
|
||||
page_view: 'Просмотр страницы расширения',
|
||||
skill_view: 'Просмотр навыка',
|
||||
skill_install: 'Установка навыка',
|
||||
knowledge_base_view: 'Просмотр базы знаний',
|
||||
knowledge_base_update: 'Изменение базы знаний',
|
||||
mcp_view: 'Просмотр сервера MCP',
|
||||
mcp_config: 'Изменение сервера MCP',
|
||||
export: 'Экспорт данных',
|
||||
execute: 'Выполнение задачи',
|
||||
debug: 'Отладка задачи',
|
||||
|
||||
@@ -2705,6 +2705,8 @@ const thTH = {
|
||||
tamperedBadge: 'อาจถูกแก้ไข',
|
||||
verifiedBadge: 'ตรวจสอบผ่าน',
|
||||
tamperedCount: 'มี {{count}} บันทึกที่ตรวจสอบไม่ผ่าน',
|
||||
integrityFailedCount: 'แฮชไม่ตรงกัน {{count}} รายการ',
|
||||
chainFailedCount: 'ลิงก์ขาด {{count}} รายการ',
|
||||
levels: {
|
||||
off: {
|
||||
label: 'ปิดการติดตาม',
|
||||
@@ -2735,6 +2737,17 @@ const thTH = {
|
||||
member_role_update: 'แก้ไขบทบาทสมาชิก',
|
||||
member_remove: 'ลบสมาชิก',
|
||||
member_view: 'ดูสมาชิก',
|
||||
plugin_view: 'ดูหน้าส่วนขยาย',
|
||||
plugin_config: 'แก้ไขการตั้งค่าส่วนขยาย',
|
||||
plugin_install: 'ติดตั้งส่วนขยาย',
|
||||
plugin_upgrade: 'อัปเกรดส่วนขยาย',
|
||||
page_view: 'ดูหน้าส่วนขยาย',
|
||||
skill_view: 'ดูทักษะ',
|
||||
skill_install: 'ติดตั้งทักษะ',
|
||||
knowledge_base_view: 'ดูฐานความรู้',
|
||||
knowledge_base_update: 'แก้ไขฐานความรู้',
|
||||
mcp_view: 'ดูเซิร์ฟเวอร์ MCP',
|
||||
mcp_config: 'แก้ไขเซิร์ฟเวอร์ MCP',
|
||||
export: 'ส่งออกข้อมูล',
|
||||
execute: 'รันงาน',
|
||||
debug: 'ดีบักงาน',
|
||||
|
||||
@@ -2740,6 +2740,8 @@ const viVN = {
|
||||
tamperedBadge: 'Có thể bị sửa',
|
||||
verifiedBadge: 'Đã xác minh',
|
||||
tamperedCount: '{{count}} bản ghi không vượt qua kiểm tra',
|
||||
integrityFailedCount: '{{count}} mã băm không khớp',
|
||||
chainFailedCount: '{{count}} liên kết bị đứt',
|
||||
levels: {
|
||||
off: {
|
||||
label: 'Tắt truy vết',
|
||||
@@ -2770,6 +2772,17 @@ const viVN = {
|
||||
member_role_update: 'Cập nhật vai trò thành viên',
|
||||
member_remove: 'Xóa thành viên',
|
||||
member_view: 'Xem thành viên',
|
||||
plugin_view: 'Xem trang tiện ích',
|
||||
plugin_config: 'Thay đổi cấu hình tiện ích',
|
||||
plugin_install: 'Cài đặt tiện ích',
|
||||
plugin_upgrade: 'Nâng cấp tiện ích',
|
||||
page_view: 'Xem trang tiện ích',
|
||||
skill_view: 'Xem kỹ năng',
|
||||
skill_install: 'Cài đặt kỹ năng',
|
||||
knowledge_base_view: 'Xem cơ sở tri thức',
|
||||
knowledge_base_update: 'Thay đổi cơ sở tri thức',
|
||||
mcp_view: 'Xem máy chủ MCP',
|
||||
mcp_config: 'Thay đổi máy chủ MCP',
|
||||
export: 'Xuất dữ liệu',
|
||||
execute: 'Chạy tác vụ',
|
||||
debug: 'Gỡ lỗi tác vụ',
|
||||
|
||||
@@ -2602,6 +2602,8 @@ const zhHans = {
|
||||
tamperedBadge: '可能被篡改',
|
||||
verifiedBadge: '校验通过',
|
||||
tamperedCount: '{{count}} 条记录校验异常',
|
||||
integrityFailedCount: '{{count}} 条哈希不匹配',
|
||||
chainFailedCount: '{{count}} 条链路断裂',
|
||||
levels: {
|
||||
off: {
|
||||
label: '关闭溯源',
|
||||
@@ -2632,6 +2634,17 @@ const zhHans = {
|
||||
member_role_update: '修改成员角色',
|
||||
member_remove: '移除成员',
|
||||
member_view: '查看成员',
|
||||
plugin_view: '查看扩展页面',
|
||||
plugin_config: '修改扩展配置',
|
||||
plugin_install: '安装扩展',
|
||||
plugin_upgrade: '升级扩展',
|
||||
page_view: '查看扩展页面',
|
||||
skill_view: '查看技能',
|
||||
skill_install: '安装技能',
|
||||
knowledge_base_view: '查看知识库',
|
||||
knowledge_base_update: '修改知识库',
|
||||
mcp_view: '查看 MCP 服务器',
|
||||
mcp_config: '修改 MCP 服务器',
|
||||
export: '导出数据',
|
||||
execute: '执行任务',
|
||||
debug: '调试任务',
|
||||
|
||||
@@ -2603,6 +2603,8 @@ const zhHant = {
|
||||
tamperedBadge: '可能被竄改',
|
||||
verifiedBadge: '驗證通過',
|
||||
tamperedCount: '{{count}} 筆記錄驗證異常',
|
||||
integrityFailedCount: '{{count}} 筆雜湊不符',
|
||||
chainFailedCount: '{{count}} 筆鏈結中斷',
|
||||
levels: {
|
||||
off: {
|
||||
label: '關閉溯源',
|
||||
@@ -2633,6 +2635,17 @@ const zhHant = {
|
||||
member_role_update: '修改成員角色',
|
||||
member_remove: '移除成員',
|
||||
member_view: '查看成員',
|
||||
plugin_view: '查看擴充頁面',
|
||||
plugin_config: '修改擴充設定',
|
||||
plugin_install: '安裝擴充',
|
||||
plugin_upgrade: '升級擴充',
|
||||
page_view: '查看擴充頁面',
|
||||
skill_view: '查看技能',
|
||||
skill_install: '安裝技能',
|
||||
knowledge_base_view: '查看知識庫',
|
||||
knowledge_base_update: '修改知識庫',
|
||||
mcp_view: '查看 MCP 伺服器',
|
||||
mcp_config: '修改 MCP 伺服器',
|
||||
export: '匯出資料',
|
||||
execute: '執行任務',
|
||||
debug: '偵錯任務',
|
||||
|
||||
Reference in New Issue
Block a user