116 lines
4.8 KiB
Python
116 lines
4.8 KiB
Python
"""Seed the built-in ``cognitive-situation-map`` public skill.
|
||
|
||
认知态势图技能(cognitive-situation-map)此前只存在于各台机器的运行时
|
||
``skills/public/`` 目录里——该目录被 .gitignore 排除,也不随离线发版的
|
||
git 干净快照搬运,导致新机器部署后技能缺失。_verified_ 技能资产因此与
|
||
本模块同目录存放(``_cognitive_situation_seed_assets/skills/``),启动时
|
||
同步到运行时技能根目录(``skills/public/``)。部署只需更新 ``app`` 和
|
||
``packages``:每次启动会覆盖该内置技能的受管文件,但保留目标目录中
|
||
额外的用户文件;下线状态(extensions_config 里的 ``skills.<name>.enabled``)
|
||
不受影响;public 类技能在 extensions_config 缺省即启用。
|
||
|
||
技能目录只保留渲染时必须读取的 ``assets/runtime/index.html`` 模板。
|
||
样式、脚本、底图和国旗由 harness
|
||
``deerflow/html_runtime/assets/cognitive-situation-map/``(Gateway 静态
|
||
服务端)按需提供,不能再复制进沙箱技能视图;否则一次技能调用会搬运约
|
||
25 MB 的重复资源并阻塞后续模型调用。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
COGNITIVE_SITUATION_SKILL_NAMES: tuple[str, ...] = ("cognitive-situation-map",)
|
||
|
||
_ASSETS_DIR = Path(__file__).parent / "_cognitive_situation_seed_assets"
|
||
_LEGACY_RUNTIME_ENTRIES: tuple[str, ...] = ("app.js", "styles.css", "assets", "data")
|
||
|
||
|
||
def _prune_legacy_runtime_bundle(target: Path) -> int:
|
||
"""Remove only formerly package-managed runtime duplicates.
|
||
|
||
Earlier releases seeded a full browser runtime below the skill itself. The
|
||
Gateway now serves that runtime from the harness package, but ``copytree``
|
||
intentionally preserves extra target files. Prune these known legacy
|
||
paths before syncing so upgrading an existing deployment immediately drops
|
||
the expensive sandbox payload without touching user-authored files.
|
||
"""
|
||
runtime_dir = target / "assets" / "runtime"
|
||
removed = 0
|
||
for name in _LEGACY_RUNTIME_ENTRIES:
|
||
candidate = runtime_dir / name
|
||
if candidate.is_dir():
|
||
shutil.rmtree(candidate)
|
||
removed += 1
|
||
elif candidate.is_file() or candidate.is_symlink():
|
||
candidate.unlink()
|
||
removed += 1
|
||
return removed
|
||
|
||
|
||
def _seed_copy_ignore(source: Path):
|
||
"""Keep obsolete source-tree runtime copies out of the deployed skill.
|
||
|
||
This filter makes the deployment lightweight even when upgrading from a
|
||
source checkout that still contains a legacy bundle. The template itself
|
||
remains in ``assets/runtime/index.html`` because the deterministic renderer
|
||
reads it while composing the small output page.
|
||
"""
|
||
runtime_dir = (source / "assets" / "runtime").resolve()
|
||
|
||
def ignore(directory: str, names: list[str]) -> set[str]:
|
||
if Path(directory).resolve() == runtime_dir:
|
||
return set(names).intersection(_LEGACY_RUNTIME_ENTRIES)
|
||
return set()
|
||
|
||
return ignore
|
||
|
||
|
||
def ensure_cognitive_situation_skills() -> bool:
|
||
"""Synchronize every built-in public cognitive-situation skill into this deployment.
|
||
|
||
Idempotent and non-fatal: package-managed files are synchronized into an
|
||
existing target while unrelated target files remain intact. Any failure is
|
||
logged instead of blocking startup. Returns True when at least one skill
|
||
directory was created or synchronized.
|
||
"""
|
||
seeded = False
|
||
try:
|
||
from deerflow.skills.storage import get_or_new_skill_storage
|
||
|
||
public_dir = Path(get_or_new_skill_storage().get_skills_root_path()) / "public"
|
||
public_dir.mkdir(parents=True, exist_ok=True)
|
||
for skill_name in COGNITIVE_SITUATION_SKILL_NAMES:
|
||
source = _ASSETS_DIR / "skills" / skill_name
|
||
target = public_dir / skill_name
|
||
if not (source / "SKILL.md").is_file():
|
||
logger.error(
|
||
"Missing seed asset for cognitive-situation skill '%s': %s",
|
||
skill_name,
|
||
source,
|
||
)
|
||
continue
|
||
target_existed = target.exists()
|
||
removed = _prune_legacy_runtime_bundle(target) if target_existed else 0
|
||
shutil.copytree(source, target, dirs_exist_ok=True, ignore=_seed_copy_ignore(source))
|
||
logger.info(
|
||
"%s public cognitive-situation skill '%s'%s",
|
||
"Synchronized" if target_existed else "Seeded",
|
||
skill_name,
|
||
f" (removed {removed} legacy runtime entries)" if removed else "",
|
||
)
|
||
seeded = True
|
||
except Exception:
|
||
logger.exception("Failed to seed cognitive-situation public skills")
|
||
return seeded
|
||
|
||
|
||
__all__ = [
|
||
"COGNITIVE_SITUATION_SKILL_NAMES",
|
||
"ensure_cognitive_situation_skills",
|
||
]
|