From 2fe4b117a44ca7106e1eb7cad718772cd5b84408 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <60681390+huanghuoguoguo@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:37:09 +0800 Subject: [PATCH] refactor(skill): move orchestration into core --- docs/review/box-architecture.md | 11 ++++------- src/langbot/pkg/skill/repository.py | 12 ++++++++++-- tests/unit_tests/test_skill_repository.py | 12 ++++++++++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/review/box-architecture.md b/docs/review/box-architecture.md index b2bc3462f..0a07abd30 100644 --- a/docs/review/box-architecture.md +++ b/docs/review/box-architecture.md @@ -352,15 +352,12 @@ SkillStore └─ 支持 source_subdir / target_suffix(commit 1aa043f) ``` -GitHub 安装路径由 Core HTTP 层下载归档,再交给 SkillRepository。Skill 文件位于独立的 `skills.root`,执行时由 Core 组装成通用只读 `BoxMountSpec` 并挂载到 `/workspace/.skills/`。Box 的正常模型、客户端和 Runtime 不包含 `skill_name`、Skill CRUD、revision 或 `SKILL.md` 语义。 +GitHub 安装路径由 Core HTTP 层下载归档,再交给 SkillRepository。Skill 文件位于独立的 `skills.root`,执行时由 Core 组装成通用只读 `BoxMountSpec` 并挂载到 `/workspace/.skills/`。Box 的模型、客户端和 Runtime 不包含 `skill_name`、Skill CRUD、revision 或 `SKILL.md` 语义。Core 与 Box Runtime SDK 按同一发布单元同步升级;Box 不保留旧 Skill RPC,也会拒绝旧的 Skill-aware payload 字段。 -滚动升级只保留一个隔离桥:`box/legacy_skill_compat.py` 让旧 Core 暂时调用新 Box,并把旧 `skill_name` 转为普通只读 mount。部署顺序必须先升级 Box、再升级 Core。该模块有 `TODO(next-major)`,下一大版本删除;正常架构不依赖它。 +仍保留的兼容仅用于已有数据的在线升级,不用于混版本协议: -下一大版本的删除清单(当前均有 `TODO(next-major)`): - -1. 删除 SDK `box/legacy_skill_compat.py` 及 Server 中唯一的注册/转换钩子。 -2. 删除 Core 对 `box.local.skills_root` 的配置 fallback。 -3. 新安装默认从历史 `./data/box/skills` 切到 `./data/skills`;届时 Box 部署只需只读访问 Core 明确下发的通用 artifact root。 +1. Core 暂时读取 `box.local.skills_root`,兼容尚未生成 `skills.root` 的持久化配置。 +2. 新安装默认暂时沿用历史 `./data/box/skills`,避免升级时搬迁已安装 Skill;下一大版本切换到 `./data/skills`。 ### 3.8 Security (`box/security.py`, 52 行) diff --git a/src/langbot/pkg/skill/repository.py b/src/langbot/pkg/skill/repository.py index 9f73ba6e9..86e0ba6ee 100644 --- a/src/langbot/pkg/skill/repository.py +++ b/src/langbot/pkg/skill/repository.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import os +import weakref from langbot_plugin.skill_store import ( SkillRevisionMismatchError, @@ -23,7 +24,14 @@ class SkillRepository: self._local_config = (config.get('box') or {}).get('local') or {} self._skills_config = config.get('skills') or {} self._store = SkillStore(self._skills_root()) - self._lock = asyncio.Lock() + self._locks: weakref.WeakValueDictionary[str, asyncio.Lock] = weakref.WeakValueDictionary() + + def _workspace_lock(self, namespace: str) -> asyncio.Lock: + lock = self._locks.get(namespace) + if lock is None: + lock = asyncio.Lock() + self._locks[namespace] = lock + return lock def _host_root(self) -> str: configured = str(self._local_config.get('host_root') or './data/box').strip() @@ -101,7 +109,7 @@ class SkillRepository: method = getattr(self._store.scoped(namespace), method_name) return method(*args, **kwargs) - async with self._lock: + async with self._workspace_lock(namespace): with blocking_work_scope(f'skill:{namespace}'): return await run_blocking_atomic(invoke) diff --git a/tests/unit_tests/test_skill_repository.py b/tests/unit_tests/test_skill_repository.py index 783dfa09a..9878878b6 100644 --- a/tests/unit_tests/test_skill_repository.py +++ b/tests/unit_tests/test_skill_repository.py @@ -34,7 +34,7 @@ def _repository(tmp_path) -> SkillRepository: 'local': { 'host_root': str(tmp_path / 'box'), }, - } + }, } ), ) @@ -47,6 +47,14 @@ def test_repository_prefers_standalone_skill_root(tmp_path): assert repository._store.root == str((tmp_path / 'skill-store').resolve()) +def test_repository_locks_are_workspace_scoped(tmp_path): + repository = _repository(tmp_path) + + first = repository._workspace_lock('workspace-a') + assert repository._workspace_lock('workspace-a') is first + assert repository._workspace_lock('workspace-b') is not first + + def test_repository_keeps_old_box_root_only_for_online_upgrade(tmp_path): app = SimpleNamespace( workspace_service=SimpleNamespace(get_execution_binding=_binding), @@ -89,7 +97,7 @@ async def test_repository_crud_and_reads_do_not_require_box(tmp_path): skill = await repository.get_skill(_CONTEXT, 'docs-only', snapshot=True) assert skill is not None - assert skill['revision'].startswith('sha256:') + assert skill['revision'].startswith('stat-v1:') assert [item['name'] for item in await repository.list_skills(_CONTEXT)] == ['docs-only'] listed = await repository.list_skill_resources(