mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-30 13:26:49 +08:00
fix(operation-trace): audit every route for misclassified mutations
Systematically cross-checked all 263 route/method pairs against the classifier and fixed every case where a real change could be mislabelled: - POST /skills and PUT /skills/<name> were classified as skill_view (read bucket), so creating or editing a skill was silently unlogged at the mutation level. They now record skill_update. - The public webhook ingress (/bots/<uuid>) and the embedded chat widget (/embed/*) are unauthenticated visitor traffic, not Workspace changes; they are dropped instead of logged as create/resource. - Uploading files and indexing documents (data flowing into a resource) are offered as observations so they never fill the log; deleting a knowledge-base file is destructive and now records file_delete. - A plugin's own config-file edit/delete is a plugin change, not an opaque resource delete; ordered before the /config rule that also matched it. - The bucket now comes from the action table (plus 'a read verb is always a read'), so a write action can no longer be forced into the read bucket. - Added agents and files resource families; localized every new action/resource type in all nine locales.
This commit is contained in:
@@ -393,6 +393,12 @@ ACTION_RULE_TABLE: typing.Final[tuple[ActionRule, ...]] = (
|
||||
bucket='write',
|
||||
resource_type='skill',
|
||||
),
|
||||
ActionRule(
|
||||
action='skill_update',
|
||||
category='extension',
|
||||
bucket='write',
|
||||
resource_type='skill',
|
||||
),
|
||||
ActionRule(
|
||||
action='skill_uninstall',
|
||||
category='extension',
|
||||
@@ -492,6 +498,51 @@ ACTION_RULE_TABLE: typing.Final[tuple[ActionRule, ...]] = (
|
||||
bucket='read',
|
||||
resource_type='resource',
|
||||
),
|
||||
# --- Data-flow helpers (why a read: nothing to trace) ----------------
|
||||
# Uploading a file or indexing a document into a knowledge base is data
|
||||
# flowing into a resource, not a change to the resource's definition. It is
|
||||
# offered as a read verb so the persist gate drops it for every role, the
|
||||
# same way a file view is dropped, keeping the log to definition changes.
|
||||
ActionRule(
|
||||
action='file_view',
|
||||
category='resource',
|
||||
bucket='read',
|
||||
resource_type='file',
|
||||
),
|
||||
ActionRule(
|
||||
action='ingest',
|
||||
category='resource',
|
||||
bucket='read',
|
||||
resource_type='knowledge_base',
|
||||
),
|
||||
# Removing a document from a knowledge base is destructive and worth tracing,
|
||||
# unlike the upload/index that put it there.
|
||||
ActionRule(
|
||||
action='file_delete',
|
||||
category='knowledge',
|
||||
bucket='write',
|
||||
resource_type='knowledge_base',
|
||||
),
|
||||
# --- Platform ingress (external traffic, no Workspace actor) ---------
|
||||
ActionRule(
|
||||
action='ingress',
|
||||
category='runtime',
|
||||
bucket='skip',
|
||||
resource_type='bot',
|
||||
),
|
||||
# --- Provider credential handshake -----------------------------------
|
||||
ActionRule(
|
||||
action='codex_view',
|
||||
category='integration',
|
||||
bucket='read',
|
||||
resource_type='model_provider',
|
||||
),
|
||||
ActionRule(
|
||||
action='codex_authorize',
|
||||
category='integration',
|
||||
bucket='write',
|
||||
resource_type='model_provider',
|
||||
),
|
||||
ActionRule(
|
||||
action='probe',
|
||||
category='system',
|
||||
@@ -508,6 +559,14 @@ ACTION_RULE_TABLE: typing.Final[tuple[ActionRule, ...]] = (
|
||||
bucket='skip',
|
||||
resource_type='assistant_conversation',
|
||||
),
|
||||
# Visitor traffic to an embedded public chat widget: unauthenticated, keyed
|
||||
# only by a bot UUID, never a Workspace actor. Dropped rather than logged.
|
||||
ActionRule(
|
||||
action='embed',
|
||||
category='runtime',
|
||||
bucket='skip',
|
||||
resource_type='bot',
|
||||
),
|
||||
)
|
||||
|
||||
ACTION_RULES_BY_ACTION: typing.Final[dict[str, ActionRule]] = {rule.action: rule for rule in ACTION_RULE_TABLE}
|
||||
@@ -516,57 +575,86 @@ _READ_METHODS: typing.Final = frozenset({'GET', 'HEAD', 'OPTIONS'})
|
||||
|
||||
# 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, delete_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
|
||||
# the method bucket -- read verb, ``DELETE``, or any other verb.
|
||||
# write_action, delete_action, fallback_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>``).
|
||||
#
|
||||
# ``delete_action`` is distinct because several resources register their read
|
||||
# and their destroy on the *same* path (``GET``/``DELETE /plugins/<author>/<name>``,
|
||||
# ``/knowledge/...``, ``/mcp/servers/<name>``). Keying only on fragments made a
|
||||
# ``DELETE`` fall into the write bucket and record an uninstall as a view or an
|
||||
# update, so the destructive operation left no readable trace.
|
||||
_ROUTE_RULES: typing.Final[tuple[tuple[tuple[str, ...], str, str, str | None], ...]] = (
|
||||
# The action is selected by the method bucket: a read verb uses ``read_action``,
|
||||
# ``DELETE`` uses ``delete_action``, a write verb uses ``write_action``. Two
|
||||
# subtlety guards exist because keying only on fragments and then trusting the
|
||||
# bucket silently mislabels real operations:
|
||||
#
|
||||
# * ``write_action`` may be ``None`` for a read-only surface (``/plugins/github``,
|
||||
# the codex auth handshake, the public webhook ingress). A stray non-read verb
|
||||
# then uses ``fallback_action`` instead of being recorded as a mutation.
|
||||
# * the bucket is re-derived from the *actual* method and the resolved action, so
|
||||
# a mismatch can never route a real mutation into the read bucket (which is
|
||||
# dropped at the mutation level) or a page load into the write bucket.
|
||||
_ROUTE_RULES: typing.Final[tuple[tuple[tuple[str, ...], str, str | None, str | None, str], ...]] = (
|
||||
# --- Audit surface itself -------------------------------------------
|
||||
(('/settings/operation-logs/export',), 'export', 'export', None),
|
||||
(('/settings/operation-logs',), 'audit_log_view', 'audit_log_view', None),
|
||||
(('/settings/operation-level',), 'settings_view', 'settings_update', None),
|
||||
(('/settings/governance',), 'settings_view', 'settings_update', None),
|
||||
(('/settings/limits',), 'settings_view', 'settings_update', None),
|
||||
(('/settings/operation-logs/export',), 'export', 'export', None, 'export'),
|
||||
(('/settings/operation-logs',), 'audit_log_view', 'audit_log_view', None, 'audit_log_view'),
|
||||
(('/settings/operation-level',), 'settings_view', 'settings_update', None, 'settings_view'),
|
||||
(('/settings/governance',), 'settings_view', 'settings_update', None, 'settings_view'),
|
||||
(('/settings/limits',), 'settings_view', 'settings_update', None, 'settings_view'),
|
||||
# --- Extension lifecycle: plugins -----------------------------------
|
||||
# ``/plugins/install`` is an install; the bare ``/plugins/<author>/<name>``
|
||||
# is a read when fetched and an uninstall when deleted.
|
||||
(('/plugins/install',), 'plugin_view', 'plugin_install', None),
|
||||
(('/plugins/github',), 'plugin_view', 'plugin_view', None),
|
||||
(('/plugins/', '/config'), 'plugin_view', 'plugin_config', None),
|
||||
(('/plugins/', '/page-api'), 'page_view', 'page_view', None),
|
||||
(('/plugins/', '/upgrade'), 'plugin_view', 'plugin_upgrade', None),
|
||||
(('/plugins/', '/logs'), 'plugin_view', 'plugin_view', None),
|
||||
(('/plugins',), 'plugin_view', 'plugin_view', 'plugin_uninstall'),
|
||||
(('/plugins/install',), 'plugin_view', 'plugin_install', None, 'plugin_view'),
|
||||
(('/plugins/github',), 'plugin_view', None, None, 'plugin_view'),
|
||||
# Editing or deleting a plugin's own config file is a plugin change, not an
|
||||
# opaque resource delete. Must precede the ``/config`` rule below, whose
|
||||
# fragment also matches ``config-files``.
|
||||
(('/plugins/', '/config-files'), 'plugin_view', 'plugin_config', 'plugin_config', 'plugin_view'),
|
||||
(('/plugins/', '/config'), 'plugin_view', 'plugin_config', None, 'plugin_view'),
|
||||
(('/plugins/', '/page-api'), 'page_view', 'page_view', None, 'page_view'),
|
||||
(('/plugins/', '/upgrade'), 'plugin_view', 'plugin_upgrade', None, 'plugin_view'),
|
||||
(('/plugins/', '/logs'), 'plugin_view', 'plugin_view', None, 'plugin_view'),
|
||||
(('/plugins',), 'plugin_view', 'plugin_view', 'plugin_uninstall', 'plugin_view'),
|
||||
# The pipeline extension bindings (plugins / MCP servers / skills) live on
|
||||
# ``/pipelines/<uuid>/extensions``, not on a plugin. Without this rule the
|
||||
# generic fragment below would classify the change as a plugin config edit.
|
||||
# The read keeps the generic ``view`` for backwards-compatible labelling.
|
||||
(('/extensions',), 'view', 'pipeline_extensions_update', None),
|
||||
(('/extensions',), 'view', 'pipeline_extensions_update', None, 'view'),
|
||||
# --- Extension lifecycle: skills ------------------------------------
|
||||
(('/skills/', '/install'), 'skill_view', 'skill_install', None),
|
||||
(('/skills',), 'skill_view', 'skill_view', 'skill_uninstall'),
|
||||
# ``/skills/install/...`` is an install; the bare ``/skills`` collection is
|
||||
# created with POST and the item is rewritten with PUT, so the write verb is
|
||||
# an update -- not ``skill_view``, which used to drop a real skill edit into
|
||||
# the read bucket where a mutation-level Workspace never persisted it.
|
||||
(('/skills/', '/install'), 'skill_view', 'skill_install', None, 'skill_view'),
|
||||
(('/skills',), 'skill_view', 'skill_update', 'skill_uninstall', 'skill_view'),
|
||||
# --- Ingestion helpers (data flowing in, not a definition change) ----
|
||||
# Ordered before the knowledge-base rule so a file upload or an index is not
|
||||
# mislabelled as a knowledge-base definition update.
|
||||
(('/knowledge/', '/files'), 'file_view', 'ingest', 'file_delete', 'file_view'),
|
||||
# --- Knowledge bases & MCP servers ----------------------------------
|
||||
(('/knowledge/',), 'knowledge_base_view', 'knowledge_base_update', 'knowledge_base_delete'),
|
||||
(('/mcp/', '/config'), 'mcp_view', 'mcp_config', None),
|
||||
(('/mcp',), 'mcp_view', 'mcp_config', 'mcp_delete'),
|
||||
(('/knowledge/',), 'knowledge_base_view', 'knowledge_base_update', 'knowledge_base_delete', 'knowledge_base_view'),
|
||||
(('/mcp/', '/config'), 'mcp_view', 'mcp_config', None, 'mcp_view'),
|
||||
(('/mcp',), 'mcp_view', 'mcp_config', 'mcp_delete', 'mcp_view'),
|
||||
# --- Member management ----------------------------------------------
|
||||
(('/members',), 'member_view', 'member_role_update', None),
|
||||
(('/invitations',), 'member_view', 'member_invite', None),
|
||||
(('/members',), 'member_view', 'member_role_update', None, 'member_view'),
|
||||
(('/invitations',), 'member_view', 'member_invite', None, 'member_view'),
|
||||
# --- Agent-assistant session chatter (never traced) ------------------
|
||||
# Placed before the generic verbs so a conversation turn is not recorded as
|
||||
# an opaque ``create`` on a nameless resource.
|
||||
(('/assistant',), 'assistant_session', 'assistant_session', 'assistant_session'),
|
||||
(('/assistant',), 'assistant_session', 'assistant_session', 'assistant_session', 'assistant_session'),
|
||||
# --- Embedded public chat widget (visitor traffic, never traced) ------
|
||||
(('/embed/',), 'embed', 'embed', 'embed', 'embed'),
|
||||
# --- Public inbound webhook ingress (external traffic, never traced) --
|
||||
# ``/bots/<uuid>`` is unauthenticated platform traffic, not a Workspace
|
||||
# mutation; recording it as ``create/resource`` was pure noise.
|
||||
(('/bots/',), 'ingress', 'ingress', 'ingress', 'ingress'),
|
||||
# --- Provider credential handshake (in-flight pairing state) ---------
|
||||
(('/codex/',), 'codex_view', None, 'codex_authorize', 'codex_view'),
|
||||
# --- Ingestion helpers: uploading a document is data flowing in, not a
|
||||
# change to a resource definition. --------------------------------
|
||||
(('/files/',), 'file_view', 'ingest', 'ingest', 'file_view'),
|
||||
# --- Generic resource verbs -----------------------------------------
|
||||
(('/export',), 'export', 'export', None),
|
||||
(('/debug',), 'debug', 'debug', None),
|
||||
(('/execute',), 'execute', 'execute', None),
|
||||
(('/publish',), 'publish', 'publish', None),
|
||||
(('/export',), 'export', 'export', None, 'export'),
|
||||
(('/debug',), 'debug', 'debug', None, 'debug'),
|
||||
(('/execute',), 'execute', 'execute', None, 'execute'),
|
||||
(('/publish',), 'publish', 'publish', None, 'publish'),
|
||||
)
|
||||
|
||||
#: Refines the resource family for the generic verb rules. The route rules above
|
||||
@@ -590,6 +678,11 @@ _RESOURCE_RULES: typing.Final[tuple[tuple[tuple[str, ...], str], ...]] = (
|
||||
(('/monitoring',), 'monitoring'),
|
||||
(('/webhooks',), 'webhook'),
|
||||
(('/apikeys',), 'api_key'),
|
||||
(('/agents',), 'agent'),
|
||||
(('/files/',), 'file'),
|
||||
(('/skills',), 'skill'),
|
||||
(('/extensions',), 'plugin'),
|
||||
(('/assistant',), 'assistant_conversation'),
|
||||
# The sandbox, survey and generic system probes share the ``system`` family.
|
||||
(('/box/',), 'system'),
|
||||
(('/survey',), 'system'),
|
||||
@@ -622,6 +715,35 @@ def _with_resource_type(rule: ActionRule, route: str) -> ActionRule:
|
||||
return dataclasses.replace(rule, resource_type=resource_type)
|
||||
|
||||
|
||||
#: Action names that describe an observation. A mutation can never be persisted
|
||||
#: under one of these: the read bucket is dropped at the mutation level, so a
|
||||
def _action_bucket(action: str, method: str) -> str:
|
||||
"""Return the capture bucket for a resolved action and the actual method.
|
||||
|
||||
The action table is the single source of truth: an action declares whether it
|
||||
is a traceable change (``write``), an observation (``read``), the audit
|
||||
surface itself (``audit``) or noise (``skip``). The only adjustment made here
|
||||
is that a read *method* is always an observation -- so a GET that somehow
|
||||
resolved to a write action still cannot be persisted as a mutation. Because
|
||||
the table now names a real write action for every mutating route, trusting it
|
||||
no longer lets a skill edit hide in the read bucket the way it used to.
|
||||
"""
|
||||
|
||||
bucket = ACTION_RULES_BY_ACTION[action].bucket
|
||||
if bucket in ('audit', 'skip'):
|
||||
return bucket
|
||||
if method in _READ_METHODS:
|
||||
return 'read'
|
||||
return bucket
|
||||
|
||||
|
||||
def _resolve(rule: ActionRule, action: str, method: str, route: str) -> ActionRule:
|
||||
"""Attach the method-derived bucket to a classified action."""
|
||||
|
||||
refined = _with_resource_type(ACTION_RULES_BY_ACTION[action], route)
|
||||
return dataclasses.replace(refined, bucket=_action_bucket(action, method))
|
||||
|
||||
|
||||
def classify(method: str, route: str) -> ActionRule:
|
||||
"""Map one HTTP request to its normalized action rule.
|
||||
|
||||
@@ -634,7 +756,7 @@ def classify(method: str, route: str) -> ActionRule:
|
||||
is_read = upper_method in _READ_METHODS
|
||||
is_delete = upper_method == 'DELETE'
|
||||
|
||||
for fragments, read_action, write_action, delete_action in _ROUTE_RULES:
|
||||
for fragments, read_action, write_action, delete_action, fallback_action in _ROUTE_RULES:
|
||||
if all(fragment in lowered_route for fragment in fragments):
|
||||
if is_read:
|
||||
action = read_action
|
||||
@@ -643,18 +765,22 @@ def classify(method: str, route: str) -> ActionRule:
|
||||
# dedicated destroy action (still a mutation, never a view).
|
||||
action = delete_action or 'delete'
|
||||
else:
|
||||
action = write_action
|
||||
return _with_resource_type(ACTION_RULES_BY_ACTION[action], route)
|
||||
# A read-only surface declares no write action; never invent a
|
||||
# mutation for it, use the observation action instead.
|
||||
action = write_action or fallback_action
|
||||
return _resolve(ACTION_RULES_BY_ACTION[action], action, upper_method, route)
|
||||
|
||||
if is_read:
|
||||
return _with_resource_type(ACTION_RULES_BY_ACTION['view'], route)
|
||||
if is_delete:
|
||||
return _with_resource_type(ACTION_RULES_BY_ACTION['delete'], route)
|
||||
if upper_method == 'POST':
|
||||
return _with_resource_type(ACTION_RULES_BY_ACTION['create'], route)
|
||||
if upper_method in {'PUT', 'PATCH'}:
|
||||
return _with_resource_type(ACTION_RULES_BY_ACTION['update'], route)
|
||||
return _with_resource_type(ACTION_RULES_BY_ACTION['probe'], route)
|
||||
action = 'view'
|
||||
elif is_delete:
|
||||
action = 'delete'
|
||||
elif upper_method == 'POST':
|
||||
action = 'create'
|
||||
elif upper_method in {'PUT', 'PATCH'}:
|
||||
action = 'update'
|
||||
else:
|
||||
action = 'probe'
|
||||
return _resolve(ACTION_RULES_BY_ACTION[action], action, upper_method, route)
|
||||
|
||||
|
||||
def level_cap_for_role(role: str | None) -> int:
|
||||
|
||||
@@ -2798,6 +2798,8 @@ const enUS = {
|
||||
member_invitation: 'Member invitation',
|
||||
operation_log: 'Operation log',
|
||||
assistant_conversation: 'Assistant conversation',
|
||||
agent: 'Agent',
|
||||
file: 'File',
|
||||
plugin: 'Extension',
|
||||
plugin_page: 'Extension page',
|
||||
skill: 'Skill',
|
||||
@@ -2835,6 +2837,14 @@ const enUS = {
|
||||
mcp_view: 'View MCP server',
|
||||
mcp_config: 'Change MCP server',
|
||||
mcp_delete: 'Delete MCP server',
|
||||
skill_update: 'Modify skill',
|
||||
file_view: 'View file',
|
||||
ingest: 'Ingest data',
|
||||
file_delete: 'Delete knowledge base file',
|
||||
ingress: 'External message ingress',
|
||||
codex_view: 'View Codex authorization',
|
||||
codex_authorize: 'Authorize Codex',
|
||||
embed: 'Embedded session',
|
||||
pipeline_extensions_update: 'Update pipeline extension bindings',
|
||||
export: 'Export data',
|
||||
execute: 'Execute task',
|
||||
|
||||
@@ -2853,6 +2853,8 @@ const esES = {
|
||||
member_invitation: 'Invitación de miembro',
|
||||
operation_log: 'Registro de operaciones',
|
||||
assistant_conversation: 'Conversación del asistente',
|
||||
agent: 'Agente',
|
||||
file: 'Archivo',
|
||||
plugin: 'Extensión',
|
||||
plugin_page: 'Página de extensión',
|
||||
skill: 'Habilidad',
|
||||
@@ -2890,6 +2892,14 @@ const esES = {
|
||||
mcp_view: 'Ver servidor MCP',
|
||||
mcp_config: 'Cambiar servidor MCP',
|
||||
mcp_delete: 'Eliminar servidor MCP',
|
||||
skill_update: 'Modificar habilidad',
|
||||
file_view: 'Ver archivo',
|
||||
ingest: 'Importar datos',
|
||||
file_delete: 'Eliminar archivo de la base de conocimiento',
|
||||
ingress: 'Entrada de mensajes externos',
|
||||
codex_view: 'Ver autorización de Codex',
|
||||
codex_authorize: 'Autorizar Codex',
|
||||
embed: 'Sesión incrustada',
|
||||
pipeline_extensions_update:
|
||||
'Actualizar vinculaciones de extensiones del pipeline',
|
||||
export: 'Exportar datos',
|
||||
|
||||
@@ -2816,6 +2816,8 @@ const jaJP = {
|
||||
member_invitation: 'メンバー招待',
|
||||
operation_log: '操作ログ',
|
||||
assistant_conversation: 'アシスタント会話',
|
||||
agent: 'エージェント',
|
||||
file: 'ファイル',
|
||||
plugin: '拡張機能',
|
||||
plugin_page: '拡張ページ',
|
||||
skill: 'スキル',
|
||||
@@ -2853,6 +2855,14 @@ const jaJP = {
|
||||
mcp_view: 'MCP サーバーを閲覧',
|
||||
mcp_config: 'MCP サーバーを変更',
|
||||
mcp_delete: 'MCP サーバーを削除',
|
||||
skill_update: 'スキルを変更',
|
||||
file_view: 'ファイルを表示',
|
||||
ingest: 'データを取り込み',
|
||||
file_delete: 'ナレッジベースのファイルを削除',
|
||||
ingress: '外部メッセージ受信',
|
||||
codex_view: 'Codex 認証を表示',
|
||||
codex_authorize: 'Codex を認証',
|
||||
embed: '埋め込みセッション',
|
||||
pipeline_extensions_update: 'パイプライン拡張の紐付けを変更',
|
||||
export: 'データをエクスポート',
|
||||
execute: 'タスクを実行',
|
||||
|
||||
@@ -2823,6 +2823,8 @@ const ruRU = {
|
||||
member_invitation: 'Приглашение участника',
|
||||
operation_log: 'Журнал операций',
|
||||
assistant_conversation: 'Сессия ассистента',
|
||||
agent: 'Агент',
|
||||
file: 'Файл',
|
||||
plugin: 'Расширение',
|
||||
plugin_page: 'Страница расширения',
|
||||
skill: 'Навык',
|
||||
@@ -2860,6 +2862,14 @@ const ruRU = {
|
||||
mcp_view: 'Просмотр сервера MCP',
|
||||
mcp_config: 'Изменение сервера MCP',
|
||||
mcp_delete: 'Удаление сервера MCP',
|
||||
skill_update: 'Изменение навыка',
|
||||
file_view: 'Просмотр файла',
|
||||
ingest: 'Импорт данных',
|
||||
file_delete: 'Удаление файла базы знаний',
|
||||
ingress: 'Внешний приём сообщений',
|
||||
codex_view: 'Просмотр авторизации Codex',
|
||||
codex_authorize: 'Авторизация Codex',
|
||||
embed: 'Встроенная сессия',
|
||||
pipeline_extensions_update: 'Изменение привязок расширений конвейера',
|
||||
export: 'Экспорт данных',
|
||||
execute: 'Выполнение задачи',
|
||||
|
||||
@@ -2750,6 +2750,8 @@ const thTH = {
|
||||
member_invitation: 'คำเชิญสมาชิก',
|
||||
operation_log: 'บันทึกการดำเนินการ',
|
||||
assistant_conversation: 'บทสนทนาผู้ช่วย',
|
||||
agent: 'เอเจนต์',
|
||||
file: 'ไฟล์',
|
||||
plugin: 'ส่วนขยาย',
|
||||
plugin_page: 'หน้าส่วนขยาย',
|
||||
skill: 'ทักษะ',
|
||||
@@ -2787,6 +2789,14 @@ const thTH = {
|
||||
mcp_view: 'ดูเซิร์ฟเวอร์ MCP',
|
||||
mcp_config: 'แก้ไขเซิร์ฟเวอร์ MCP',
|
||||
mcp_delete: 'ลบเซิร์ฟเวอร์ MCP',
|
||||
skill_update: 'แก้ไขทักษะ',
|
||||
file_view: 'ดูไฟล์',
|
||||
ingest: 'นำเข้าข้อมูล',
|
||||
file_delete: 'ลบไฟล์ฐานความรู้',
|
||||
ingress: 'การรับข้อความภายนอก',
|
||||
codex_view: 'ดูการอนุญาต Codex',
|
||||
codex_authorize: 'อนุญาต Codex',
|
||||
embed: 'เซสชันฝังตัว',
|
||||
pipeline_extensions_update: 'แก้ไขการผูกส่วนขยายของไปป์ไลน์',
|
||||
export: 'ส่งออกข้อมูล',
|
||||
execute: 'รันงาน',
|
||||
|
||||
@@ -2785,6 +2785,8 @@ const viVN = {
|
||||
member_invitation: 'Lời mời thành viên',
|
||||
operation_log: 'Nhật ký thao tác',
|
||||
assistant_conversation: 'Phiên trợ lý',
|
||||
agent: 'Tác nhân',
|
||||
file: 'Tệp',
|
||||
plugin: 'Tiện ích mở rộng',
|
||||
plugin_page: 'Trang tiện ích',
|
||||
skill: 'Kỹ năng',
|
||||
@@ -2822,6 +2824,14 @@ const viVN = {
|
||||
mcp_view: 'Xem máy chủ MCP',
|
||||
mcp_config: 'Thay đổi máy chủ MCP',
|
||||
mcp_delete: 'Xóa máy chủ MCP',
|
||||
skill_update: 'Sửa kỹ năng',
|
||||
file_view: 'Xem tệp',
|
||||
ingest: 'Nhập dữ liệu',
|
||||
file_delete: 'Xóa tệp cơ sở tri thức',
|
||||
ingress: 'Nhận tin nhắn bên ngoài',
|
||||
codex_view: 'Xem ủy quyền Codex',
|
||||
codex_authorize: 'Ủy quyền Codex',
|
||||
embed: 'Phiên nhúng',
|
||||
pipeline_extensions_update: 'Cập nhật liên kết tiện ích của pipeline',
|
||||
export: 'Xuất dữ liệu',
|
||||
execute: 'Chạy tác vụ',
|
||||
|
||||
@@ -2648,6 +2648,8 @@ const zhHans = {
|
||||
member_invitation: '成员邀请',
|
||||
operation_log: '操作日志',
|
||||
assistant_conversation: '助手会话',
|
||||
agent: '智能体',
|
||||
file: '文件',
|
||||
plugin: '扩展',
|
||||
plugin_page: '扩展页面',
|
||||
skill: '技能',
|
||||
@@ -2685,6 +2687,14 @@ const zhHans = {
|
||||
mcp_view: '查看 MCP 服务器',
|
||||
mcp_config: '修改 MCP 服务器',
|
||||
mcp_delete: '删除 MCP 服务器',
|
||||
skill_update: '修改技能',
|
||||
file_view: '查看文件',
|
||||
ingest: '导入数据',
|
||||
file_delete: '删除知识库文件',
|
||||
ingress: '外部消息接入',
|
||||
codex_view: '查看 Codex 授权',
|
||||
codex_authorize: '授权 Codex',
|
||||
embed: '嵌入式会话',
|
||||
pipeline_extensions_update: '修改管道扩展绑定',
|
||||
export: '导出数据',
|
||||
execute: '执行任务',
|
||||
|
||||
@@ -2649,6 +2649,8 @@ const zhHant = {
|
||||
member_invitation: '成員邀請',
|
||||
operation_log: '操作日誌',
|
||||
assistant_conversation: '助手會話',
|
||||
agent: '智慧代理',
|
||||
file: '檔案',
|
||||
plugin: '擴充',
|
||||
plugin_page: '擴充頁面',
|
||||
skill: '技能',
|
||||
@@ -2686,6 +2688,14 @@ const zhHant = {
|
||||
mcp_view: '查看 MCP 伺服器',
|
||||
mcp_config: '修改 MCP 伺服器',
|
||||
mcp_delete: '刪除 MCP 伺服器',
|
||||
skill_update: '修改技能',
|
||||
file_view: '查看檔案',
|
||||
ingest: '匯入資料',
|
||||
file_delete: '刪除知識庫檔案',
|
||||
ingress: '外部訊息接入',
|
||||
codex_view: '查看 Codex 授權',
|
||||
codex_authorize: '授權 Codex',
|
||||
embed: '嵌入式工作階段',
|
||||
pipeline_extensions_update: '修改管道擴展綁定',
|
||||
export: '匯出資料',
|
||||
execute: '執行任務',
|
||||
|
||||
Reference in New Issue
Block a user