mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-09 19:47:15 +00:00
refactor(skill): move orchestration into core
This commit is contained in:
@@ -352,15 +352,12 @@ SkillStore
|
|||||||
└─ 支持 source_subdir / target_suffix(commit 1aa043f)
|
└─ 支持 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. Core 暂时读取 `box.local.skills_root`,兼容尚未生成 `skills.root` 的持久化配置。
|
||||||
|
2. 新安装默认暂时沿用历史 `./data/box/skills`,避免升级时搬迁已安装 Skill;下一大版本切换到 `./data/skills`。
|
||||||
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。
|
|
||||||
|
|
||||||
### 3.8 Security (`box/security.py`, 52 行)
|
### 3.8 Security (`box/security.py`, 52 行)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import weakref
|
||||||
|
|
||||||
from langbot_plugin.skill_store import (
|
from langbot_plugin.skill_store import (
|
||||||
SkillRevisionMismatchError,
|
SkillRevisionMismatchError,
|
||||||
@@ -23,7 +24,14 @@ class SkillRepository:
|
|||||||
self._local_config = (config.get('box') or {}).get('local') or {}
|
self._local_config = (config.get('box') or {}).get('local') or {}
|
||||||
self._skills_config = config.get('skills') or {}
|
self._skills_config = config.get('skills') or {}
|
||||||
self._store = SkillStore(self._skills_root())
|
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:
|
def _host_root(self) -> str:
|
||||||
configured = str(self._local_config.get('host_root') or './data/box').strip()
|
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)
|
method = getattr(self._store.scoped(namespace), method_name)
|
||||||
return method(*args, **kwargs)
|
return method(*args, **kwargs)
|
||||||
|
|
||||||
async with self._lock:
|
async with self._workspace_lock(namespace):
|
||||||
with blocking_work_scope(f'skill:{namespace}'):
|
with blocking_work_scope(f'skill:{namespace}'):
|
||||||
return await run_blocking_atomic(invoke)
|
return await run_blocking_atomic(invoke)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def _repository(tmp_path) -> SkillRepository:
|
|||||||
'local': {
|
'local': {
|
||||||
'host_root': str(tmp_path / 'box'),
|
'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())
|
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):
|
def test_repository_keeps_old_box_root_only_for_online_upgrade(tmp_path):
|
||||||
app = SimpleNamespace(
|
app = SimpleNamespace(
|
||||||
workspace_service=SimpleNamespace(get_execution_binding=_binding),
|
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)
|
skill = await repository.get_skill(_CONTEXT, 'docs-only', snapshot=True)
|
||||||
assert skill is not None
|
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']
|
assert [item['name'] for item in await repository.list_skills(_CONTEXT)] == ['docs-only']
|
||||||
|
|
||||||
listed = await repository.list_skill_resources(
|
listed = await repository.list_skill_resources(
|
||||||
|
|||||||
Reference in New Issue
Block a user