refactor(skill): move orchestration into core

This commit is contained in:
huanghuoguoguo
2026-09-06 12:37:09 +08:00
parent cba842fbee
commit 2fe4b117a4
3 changed files with 24 additions and 11 deletions
+4 -7
View File
@@ -352,15 +352,12 @@ SkillStore
└─ 支持 source_subdir / target_suffixcommit 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 行)
+10 -2
View File
@@ -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)
+10 -2
View File
@@ -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(