feat(skills): add Agent Skills management system (#1917)

* feat(skills): add Agent Skills management system

Implement comprehensive skills management feature inspired by agentskills spec:

Backend:
- Add Skill and SkillPipelineBinding database entities
- Add database migration (dbm018) for skills tables
- Implement SkillManager for skill loading, matching, and resolution
- Implement SkillService for CRUD operations
- Add skills API endpoints for skill and pipeline binding management
- Integrate skill index injection into pipeline preprocessor
- Add skill activation detection in LocalAgentRunner

Frontend:
- Add Skills page with listing, search, and type filter
- Add SkillDetailDialog for create/edit with preview
- Add SkillCard and SkillForm components
- Add skills API methods to BackendClient
- Add skills entry to sidebar navigation
- Add i18n translations (en-US, zh-Hans)

Features:
- Support skill and workflow types
- Sub-skill composition via {{INVOKE_SKILL: name}} syntax
- Progressive disclosure (index in prompt, full instructions on activation)
- Pipeline-specific skill bindings with priority

* fix: resolve cherry-pick conflicts for agentskills onto sandbox

- Remove non-existent external_kb service import
- Add skill_mgr mock to localagent sandbox_exec tests
- Keep database version at 24 (sandbox branch's latest)

* feat(skills): upgrade to package-backed skills with sandbox execution

  Evolve the skills system from pure prompt-based to package-backed with
  sandbox tool execution support:

  - Add source_type/package_root/entry_file/skill_tools fields to Skill entity
  - SkillManager loads SKILL.md from local package directories
  - SkillToolLoader as 4th dispatch layer in ToolManager (query-scoped)
  - LocalAgent injects skill tools into use_funcs on skill activation
  - BoxService.execute_skill_tool() runs scripts in sandbox (ro mount, env params)
  - Skill tool names auto-namespaced as skill__{skill}__{tool}
  - API validation for package_root allowlist and entry path traversal
  - Frontend source_type toggle, package_root input, skill_tools editor
  - Migration renumbered to 025 with ALTER TABLE fallback for existing DBs
  - Fix unclosed limitation section in i18n files
  - Fix skills API methods misplaced outside BackendClient class

* fix: test info

* feat(skills): switch skills to package-backed storage and add import tooling
  - skills 从 inline/package 双轨收敛成 package-first
  - instructions 改为写入并读取 SKILL.md
  - 新增本地目录扫描和 GitHub 安装 skill
  - 前端把 skills 整合进 plugins 页,新增 SkillsComponent 和 GitHub 导入弹窗
  - skill form 去掉 source_type / type 筛选,改成目录扫描驱动
  - Box skill tool 挂载模式从 ro 改成 rw
  - 测试和中英文文案同步更新

* feat: simplify langbot skill create and import

* refactor(skills): clean up legacy skill API and harden activation flow

* refactor(skills): remove skill dependency expansion and add skill_get

* fix: lint

* fix: delete

* fix(skills): align tool manager loader initialization

* refactor: remove sandbox execute skill

* fix(skills): hide activation markers and isolate skill activation flow

* refactor(skills): switch skill model to filesystem-backed packages

* refactor(skills): switch skill model to filesystem-backed packages

* refactor(skills): unify runtime skill access around filesystem paths

* refactor(skills): unify runtime skill access around filesystem paths

* feat(skills): align rw package design and fix skill activation, visibility, and lint issues

* refactor(skills): replace rich authoring API with import/reload flow and update
  Box design doc

* feat(box): add sandbox_exec tool loop for local-agent calculations

* feat(box): add host workspace mounting and sandbox_exec guidance

* feat(box): add BoxProfile with resource limits and improved output truncation

  - Implement head+tail output truncation (60/40 split) so LLM sees both
    beginning and final results; add streaming byte-limited reads in backend
    to prevent unbounded memory usage (_MAX_RAW_OUTPUT_BYTES = 1MB)
  - Define BoxProfile model with locked fields and max_timeout_sec clamping
  - Add four built-in profiles: default, offline_readonly, network_basic,
    network_extended with differentiated resource and security constraints
  - Add resource limit fields to BoxSpec (cpus, memory_mb, pids_limit,
    read_only_rootfs) and pass corresponding container CLI flags
    (--cpus, --memory, --pids-limit, --read-only, --tmpfs)
  - Profile loaded from config (box.profile), applied in service layer
    before BoxSpec validation; locked fields cannot be overridden by
    tool-call parameters

* feat(box): add obs

* refactor(box): unify box service lifecycle and local runtime
  management

* refactor(box): remove legacy in-process runtime code and clean up smells

After the architecture settled on always using an independent Box Runtime
service, several pieces of compatibility code and design shortcuts were
left behind. This commit cleans them up:

- Remove `LocalBoxRuntimeClient` and `create_box_runtime_client` from
  production code (moved to test-only helper).
- Remove unused `_clip_bytes` method from backend.
- Remove `__langbot_session_placeholder__` hack by making `BoxSpec.cmd`
  default to empty and validating non-empty only in `runtime.execute()`.
- Extract `get_box_config()` helper to eliminate 5× duplicated config
  access boilerplate.
- Remove `session_id`/`host_path`/`host_path_mode` from the LLM-facing
  tool schema to enforce request-scoped session isolation.
- Fix dual shutdown path: `NativeToolLoader.shutdown()` no longer calls
  `box_service.shutdown()` (handled by `Application.dispose()`).
- Simplify `_assert_session_compatible` with a loop.
- Inline client creation in `BoxRuntimeConnector`.
- Remove redundant `BOX__RUNTIME_URL` env var from docker-compose
  (auto-detected by code).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(box/mcp): integrate MCP stdio with Box sandbox — auto-isolation, dep install, security

  ## Summary

  When Podman/Docker is available, all stdio-mode MCP servers now automatically
  run inside Box containers with dependency installation, path rewriting, and
  lifecycle management. When no container runtime exists, LangBot starts normally
  and stdio MCP falls back to host-direct execution.

  ## What changed

  ### MCP stdio → Box integration (mcp.py)
  - Add `MCPServerBoxConfig` pydantic model for structured box configuration
    with validation and defaults (network, host_path_mode, timeouts, resources)
  - Auto-infer `host_path` from command/args with venv detection: recognizes
    `.venv/bin/python` patterns and walks up to the project root
  - Rewrite host paths to container `/workspace` paths transparently
  - Replace venv python commands with container-native `python`
  - Auto-detect `pyproject.toml`/`setup.py`/`requirements.txt` and run
    `pip install` inside the container before starting the MCP server
  - Copy project to `/tmp` before install to handle read-only mounts
  - Add retry with exponential backoff (3 retries, 2s/4s/8s delays)
  - Add Box managed process health monitoring (poll every 5s)
  - Fix session leak: `_cleanup_box_stdio_session()` now runs in `finally`
    block of `_lifecycle_loop`, covering all exit paths
  - Fix retry logic: `_ready_event` is only set after all retries exhaust
    or on success, not on first failure
  - Enhance `get_runtime_info_dict()` with `box_session_id` and `box_enabled`

  ### Box security (security.py — new)
  - `validate_sandbox_security()` blocks dangerous host paths:
    `/etc`, `/proc`, `/sys`, `/dev`, `/root`, `/boot`, `/run`,
    docker.sock, podman socket
  - Called at the start of `CLISandboxBackend.start_session()`

  ### Box models (models.py)
  - Add `BoxHostMountMode.NONE` — skips volume mount entirely
  - Adjust `validate_host_mount_consistency` to allow arbitrary workdir
    when `host_path_mode=NONE`

  ### Box backend (backend.py)
  - Add `validate_sandbox_security()` call in `start_session()`
  - Add `langbot.box.config_hash` label on containers for drift detection
  - Handle `BoxHostMountMode.NONE` — skip `-v` mount arg
  - Add `cleanup_orphaned_containers()` to base class (no-op default) and
    CLI implementation (single batched `rm -f` command)

  ### Box runtime (runtime.py)
  - Call `cleanup_orphaned_containers()` during `initialize()` to remove
    lingering containers from previous runs

  ### Box service (service.py)
  - Graceful degradation: `initialize()` catches runtime errors and sets
    `available=False` instead of crashing LangBot startup
  - Add `available` property and guard on `execute_sandbox_tool()`
  - Add `skip_host_mount_validation` parameter to `build_spec()` and
    `create_session()` — MCP paths are admin-configured and trusted,
    bypassing `allowed_host_mount_roots` restrictions meant for
    LLM-generated sandbox_exec commands

  ### Default behavior
  - stdio MCP servers automatically use Box when `box_service.available`
    is True (Podman/Docker detected); no explicit `box` config needed
  - When no container runtime exists, falls back to host-direct stdio
  - MCP Box defaults: `network=on` (for pip install), `read_only_rootfs=false`
    (for site-packages), `host_path_mode=ro`, `startup_timeout=120s`

  ### Tests
  - `test_box_security.py`: blocked paths, safe paths, subpath rejection
  - `test_mcp_box_integration.py`: config model, path rewriting, venv
    unwrap, host_path inference, payload building, runtime info, box
    availability check
  - `test_box_service.py`: `BoxHostMountMode.NONE` validation tests

* feat(box/mcp): instance-based orphan cleanup, error classification, session API, and integration tests

  ## Changes

  ### Precise orphan container cleanup
  - Runtime generates a unique instance_id on startup
  - Every container gets a `langbot.box.instance_id` label
  - `cleanup_orphaned_containers()` only removes containers from
    previous instances, preserving containers owned by the current one
  - Containers from older versions (no label) are also cleaned up
  - `cleanup_orphaned_containers` added to `BaseSandboxBackend` as
    a no-op default method, removing hasattr duck-typing

  ### Fine-grained MCP error classification
  - New `MCPSessionErrorPhase` enum with 7 phases: session_create,
    dep_install, process_start, relay_connect, mcp_init, runtime,
    tool_call
  - Each phase in `_init_box_stdio_server()` sets the error phase
    before re-raising, enabling precise failure diagnosis
  - `retry_count` tracked across retry attempts
  - `get_runtime_info_dict()` exposes `error_phase` and `retry_count`

  ### GET /v1/sessions/{id} API
  - `BoxRuntime.get_session()` returns session details including
    managed process info when present
  - `handle_get_session` HTTP handler + route in server.py
  - `BoxRuntimeClient.get_session()` abstract method + remote impl

  ### stdio defaults to Box when runtime is available
  - `_uses_box_stdio()` checks `box_service.available` instead of
    requiring explicit `box` key in server_config
  - `BoxService.initialize()` catches runtime errors gracefully,
    sets `available=False` instead of crashing LangBot startup
  - When no container runtime exists, stdio MCP falls back to
    host-direct execution

  ### Code quality (from /simplify review)
  - Extracted `_VENV_DIRS` / `_VENV_BIN_DIRS` module-level constants
  - Removed dead `_box_network_mode()` method and unused `bc` variable
  - Fixed broken import `from ....box.models` → `from ...box.models`
  - Cached `_resolve_host_path()` result — computed once, passed through
  - Config hash now includes `host_path` field
  - Batched orphan cleanup into single `rm -f` command

  ### Session leak fix
  - `_cleanup_box_stdio_session()` now runs in `_lifecycle_loop`'s
    finally block, covering all exit paths (normal shutdown, error,
    retry, final failure)

  ### Integration tests
  - 6 end-to-end tests covering managed process lifecycle, WebSocket
    stdio bidirectional IO, session cleanup verification, single
    session query, process exit detection, and orphan cleanup safety

* refactor: use rpc

* fix: import

* refactor(box): clean up sandbox subsystem code quality and efficiency

  - Fix O(n²) stderr trimming in runtime.py with running length tracker
  - Remove dead code: RESERVED_CONTAINER_PATHS, _subprocess_wait_task,
    unused config_hash computation, unused imports
  - Deduplicate connection callback in BoxRuntimeConnector, parse URL once
  - Use enum comparison instead of stringly-typed spec.network.value check
  - Replace manual _result_to_dict/_session_to_dict with model_dump()
  - Cache NativeToolLoader tool definition and sandbox system guidance
  - Extract _is_path_under() helper to eliminate duplicated path checks
  - Import SANDBOX_EXEC_TOOL_NAME from native.py instead of redefining
  - Add JSON startswith guard in logging_utils to skip futile json.loads
  - Fix ruff lint errors (F401 unused imports, F841 unused variables)

* fix: ruff

* refactor(sandbox): keep box logic out of pipeline and localagent

  - Move sandbox system-prompt guidance from LocalAgentRunner into
    BoxService.get_system_guidance() so all box domain knowledge stays
    in the box module.
  - Remove standalone logging_utils.py; merge format_result_log() into
    MessageHandler base class alongside cut_str().
  - Strip sandbox-specific JSON parsing from log formatting; tool
    results now use generic truncation.
  - Revert TYPE_CHECKING changes in stage.py and runner.py that were
    unrelated to this feature.
  - Skip two test files affected by a pre-existing circular import
    (runner ↔ app) until the import cycle is resolved in a separate PR.

* refactor(box): move box runtime to langbot-plugin-sdk

  Extract self-contained box runtime modules (actions, backend, client,
  errors, models, runtime, security, server) to langbot-plugin-sdk and
  update all imports to use `langbot_plugin.box.*`. Keep only service
  and
  connector in LangBot core as they depend on the Application context.

  - Update docker-compose to use `langbot_plugin.box.server` entry
  point
  - Update pyproject.toml to use local SDK via `tool.uv.sources`
  - Remove migrated source files and their unit/integration tests
  - Update remaining test imports to match new module paths

* fix: ruff

* fix(box): tighten sandbox exposure and restore box integration coverage

* refactor(types): remove quoted annotations under postponed evaluation

* chore(sandbox): move MCP loader changes to follow-up branch

* refactor(plugins): simplify GitHub install flow to default master archive

* revert(api): restore plugin GitHub import flow in plugins controller

* Improve data-root handling and skill install previews

* Add managed skill authoring tools for local agents

* Refactor the skills UI around sidebar detail pages

* Document why managed skill authoring tools bypass box

* fix: lint

* feat(web): refactor plugin/skill install flows and fix skills page

- Fix sidebar skill icon
- Add skills route and error page component
- Refactor plugin GitHub install from dialog modal to inline card
- Add skill install dropdown menu (create/upload/github) in sidebar
- Wire sidebar → skills page communication via pendingSkillInstallAction context
- Add i18n keys for error page and skill install actions

* fix(web): persist sidebar collapsible section open state on navigation

Sections opened via sub-item navigation now retain their expanded state
when the user switches to a different section, instead of collapsing
because the isActive fallback becomes false.

---------

Co-authored-by: youhuanghe <1051233107@qq.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
fdc310
2026-04-08 16:09:06 +08:00
committed by WangCham
parent fcf74c3b6c
commit 4b8a8c5e31
50 changed files with 6375 additions and 518 deletions

View File

@@ -802,8 +802,15 @@ const enUS = {
selectAll: 'Select All',
enableAllPlugins: 'Enable All Plugins',
enableAllMCPServers: 'Enable All MCP Servers',
enableAllSkills: 'Enable All Skills',
allPluginsEnabled: 'All plugins enabled',
allMCPServersEnabled: 'All MCP servers enabled',
allSkillsEnabled: 'All skills enabled',
skillsTitle: 'Skills',
noSkillsSelected: 'No skills selected',
addSkill: 'Add Skill',
selectSkills: 'Select Skills',
noSkillsAvailable: 'No skills available',
},
debugDialog: {
title: 'Pipeline Chat',
@@ -1241,6 +1248,80 @@ const enUS = {
maxExtensionsReached:
'Maximum number of extensions ({{max}}) reached. Please remove an existing MCP server or plugin before adding a new one.',
},
skills: {
title: 'Skills',
description:
'Create and manage skills that can be activated during conversations',
createSkill: 'Create Skill',
editSkill: 'Edit Skill',
getSkillListError: 'Failed to get skill list: ',
skillName: 'Skill Name',
displayName: 'Skill Name',
displayNamePlaceholder: 'Display name (supports any language)',
skillSlug: 'Directory Name',
skillSlugPlaceholder: 'english-name-only',
skillSlugHelp:
'Used as the skill directory name. Only letters, numbers, hyphens and underscores.',
skillDescription: 'Skill Description',
skillInstructions: 'Instructions',
autoActivate: 'Auto Activate',
saveSuccess: 'Saved successfully',
saveError: 'Save failed: ',
createSuccess: 'Created successfully',
createError: 'Creation failed: ',
deleteSuccess: 'Deleted successfully',
deleteError: 'Delete failed: ',
deleteConfirmation: 'Are you sure you want to delete this skill?',
skillNameRequired: 'Skill name cannot be empty',
skillDescriptionRequired: 'Skill description cannot be empty',
packageRootRequired: 'Package root path cannot be empty',
scan: 'Scan',
scanSuccess: 'Directory scanned successfully',
scanError: 'Failed to scan directory: ',
noSkills: 'No skills configured',
preview: 'Preview',
previewInstructions: 'Preview Instructions',
instructionsPlaceholder: 'Enter skill instructions in Markdown format...',
descriptionPlaceholder:
'A brief description of what this skill does (shown to the LLM)',
packageRoot: 'Package Directory',
packageRootHelp:
'Optional. Only needed when importing an existing skill directory. Leave empty for new skills. Scanning checks the current directory and subdirectories up to 2 levels deep.',
advancedSettings: 'Advanced Settings',
searchSkills: 'Search skills...',
selectSkills: 'Select Skills',
builtin: 'Built-in',
importFromGithub: 'Import from GitHub',
createManually: 'Create Manually',
uploadZip: 'Upload ZIP Package',
uploadZipOnly: 'Only .zip skill packages are supported',
installSuccess: 'Skill installed successfully',
installError: 'Failed to install skill: ',
enterRepoUrl: 'Enter GitHub repository URL',
repoUrlPlaceholder: 'e.g., https://github.com/owner/repo',
fetchingReleases: 'Fetching releases...',
selectRelease: 'Select Release',
noReleasesFound: 'No releases found',
fetchReleasesError: 'Failed to fetch releases: ',
selectAsset: 'Select file to install',
sourceArchive: 'Source code (zip)',
noAssetsFound: 'No installable files available in this release',
fetchAssetsError: 'Failed to fetch assets: ',
backToReleases: 'Back to releases',
backToRepoUrl: 'Back to repository URL',
backToAssets: 'Back to assets',
releaseTag: 'Tag: {{tag}}',
publishedAt: 'Published at: {{date}}',
prerelease: 'Pre-release',
assetSize: 'Size: {{size}}',
confirmInstall: 'Confirm Install',
installing: 'Installing skill...',
loading: 'Loading...',
previewLoadError: 'Failed to load preview',
selectFromSidebar: 'Select a skill from the sidebar',
dangerZone: 'Danger Zone',
dangerZoneDescription: 'Irreversible and destructive actions',
},
wizard: {
sidebarDescription: 'Create a bot with guided steps',
loading: 'Loading wizard...',
@@ -1305,6 +1386,16 @@ const enUS = {
backToWorkbench: 'Back to Workbench',
},
},
errorPage: {
unexpectedError: 'Something went wrong',
unexpectedErrorDescription:
'An unexpected error occurred. Please try again later.',
notFound: 'Page not found',
notFoundDescription:
'The page you are looking for does not exist or has been moved.',
goBack: 'Go Back',
backToHome: 'Back to Home',
},
};
export default enUS;

View File

@@ -1342,6 +1342,15 @@ const esES = {
backToWorkbench: 'Volver al panel de trabajo',
},
},
errorPage: {
unexpectedError: 'Algo salió mal',
unexpectedErrorDescription:
'Ocurrió un error inesperado. Por favor, inténtelo de nuevo más tarde.',
notFound: 'Página no encontrada',
notFoundDescription: 'La página que buscas no existe o ha sido movida.',
goBack: 'Volver',
backToHome: 'Ir al inicio',
},
};
export default esES;

View File

@@ -1312,6 +1312,16 @@ const jaJP = {
backToWorkbench: 'ワークベンチに戻る',
},
},
errorPage: {
unexpectedError: 'エラーが発生しました',
unexpectedErrorDescription:
'予期しないエラーが発生しました。しばらくしてからもう一度お試しください。',
notFound: 'ページが見つかりません',
notFoundDescription:
'お探しのページは存在しないか、移動された可能性があります。',
goBack: '戻る',
backToHome: 'ホームに戻る',
},
};
export default jaJP;

View File

@@ -1284,6 +1284,15 @@ const thTH = {
backToWorkbench: 'กลับไปหน้าทำงาน',
},
},
errorPage: {
unexpectedError: 'เกิดข้อผิดพลาด',
unexpectedErrorDescription:
'เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่อีกครั้งในภายหลัง',
notFound: 'ไม่พบหน้า',
notFoundDescription: 'หน้าที่คุณกำลังมองหาไม่มีอยู่หรือถูกย้ายแล้ว',
goBack: 'ย้อนกลับ',
backToHome: 'กลับหน้าหลัก',
},
};
export default thTH;

View File

@@ -1305,6 +1305,16 @@ const viVN = {
backToWorkbench: 'Quay lại bàn làm việc',
},
},
errorPage: {
unexpectedError: 'Đã xảy ra lỗi',
unexpectedErrorDescription:
'Đã xảy ra lỗi không mong muốn. Vui lòng thử lại sau.',
notFound: 'Không tìm thấy trang',
notFoundDescription:
'Trang bạn tìm kiếm không tồn tại hoặc đã được di chuyển.',
goBack: 'Quay lại',
backToHome: 'Về trang chủ',
},
};
export default viVN;

View File

@@ -768,8 +768,15 @@ const zhHans = {
selectAll: '全选',
enableAllPlugins: '启用所有插件',
enableAllMCPServers: '启用所有 MCP 服务器',
enableAllSkills: '启用所有技能',
allPluginsEnabled: '已启用所有插件',
allMCPServersEnabled: '已启用所有 MCP 服务器',
allSkillsEnabled: '已启用所有技能',
skillsTitle: '技能',
noSkillsSelected: '未选择任何技能',
addSkill: '添加技能',
selectSkills: '选择技能',
noSkillsAvailable: '暂无可用技能',
},
debugDialog: {
title: '流水线对话',
@@ -1187,6 +1194,77 @@ const zhHans = {
maxExtensionsReached:
'已达到扩展数量上限({{max}}个)。请先删除已有的 MCP 服务器或插件后再添加新的。',
},
skills: {
title: '技能',
description: '创建和管理可在对话中激活的技能',
createSkill: '创建技能',
editSkill: '编辑技能',
getSkillListError: '获取技能列表失败:',
skillName: '技能名称',
displayName: '技能名称',
displayNamePlaceholder: '显示名称(支持中文)',
skillSlug: '目录名称',
skillSlugPlaceholder: 'english-name-only',
skillSlugHelp: '用作技能目录名,仅支持英文字母、数字、连字符和下划线。',
skillDescription: '技能描述',
skillInstructions: '指令内容',
autoActivate: '自动激活',
saveSuccess: '保存成功',
saveError: '保存失败:',
createSuccess: '创建成功',
createError: '创建失败:',
deleteSuccess: '删除成功',
deleteError: '删除失败:',
deleteConfirmation: '你确定要删除这个技能吗?',
skillNameRequired: '技能名称不能为空',
skillDescriptionRequired: '技能描述不能为空',
packageRootRequired: '技能目录不能为空',
scan: '扫描',
scanSuccess: '目录扫描成功',
scanError: '扫描目录失败:',
noSkills: '暂未配置任何技能',
preview: '预览',
previewInstructions: '预览指令',
instructionsPlaceholder: '使用 Markdown 格式输入技能指令...',
descriptionPlaceholder: '简短描述此技能的功能(会展示给 LLM',
packageRoot: '技能目录',
packageRootHelp:
'非必填。仅在导入已有技能目录时需要填写,新建技能可留空。扫描会检查当前目录及两级子目录。',
advancedSettings: '高级设置',
searchSkills: '搜索技能...',
selectSkills: '选择技能',
builtin: '内置',
importFromGithub: '从 GitHub 导入',
createManually: '手动创建',
uploadZip: '上传 ZIP 包',
uploadZipOnly: '仅支持 .zip 技能包',
installSuccess: '技能安装成功',
installError: '安装技能失败:',
enterRepoUrl: '输入 GitHub 仓库地址',
repoUrlPlaceholder: '例如 https://github.com/owner/repo',
fetchingReleases: '正在获取发布版本...',
selectRelease: '选择发布版本',
noReleasesFound: '未找到发布版本',
fetchReleasesError: '获取发布版本失败:',
selectAsset: '选择要安装的文件',
sourceArchive: '源码包 (zip)',
noAssetsFound: '此版本暂无可安装的文件',
fetchAssetsError: '获取文件列表失败:',
backToReleases: '返回版本列表',
backToRepoUrl: '返回仓库地址',
backToAssets: '返回文件列表',
releaseTag: '标签:{{tag}}',
publishedAt: '发布于:{{date}}',
prerelease: '预发布',
assetSize: '大小:{{size}}',
confirmInstall: '确认安装',
installing: '正在安装技能...',
loading: '加载中...',
previewLoadError: '加载预览失败',
selectFromSidebar: '从侧边栏选择一个技能',
dangerZone: '危险区域',
dangerZoneDescription: '不可逆的操作',
},
wizard: {
sidebarDescription: '通过引导步骤创建机器人',
loading: '正在加载向导...',
@@ -1247,6 +1325,14 @@ const zhHans = {
backToWorkbench: '返回工作台',
},
},
errorPage: {
unexpectedError: '出错了',
unexpectedErrorDescription: '发生了意外错误,请稍后重试。',
notFound: '页面未找到',
notFoundDescription: '你访问的页面不存在或已被移动。',
goBack: '返回上页',
backToHome: '返回首页',
},
};
export default zhHans;

View File

@@ -1247,6 +1247,14 @@ const zhHant = {
backToWorkbench: '返回工作台',
},
},
errorPage: {
unexpectedError: '出錯了',
unexpectedErrorDescription: '發生了意外錯誤,請稍後重試。',
notFound: '頁面未找到',
notFoundDescription: '你訪問的頁面不存在或已被移動。',
goBack: '返回上頁',
backToHome: '返回首頁',
},
};
export default zhHant;