feat: 移除backend-old目录中的废弃文件
移除以下文件: - .dockerignore 和 .gitignore 配置文件 - A2A_Multiagent_change.md 设计文档 - COMMUNICATION.md 通讯信息文档 - Dockerfile 构建配置 - LICENSE 许可证文件 这些文件属于旧版本后端代码,不再需要维护。
This commit is contained in:
@ -1,262 +0,0 @@
|
||||
"""Import no-credential Hermes Agent skills into Beaver."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from beaver.skills.catalog.utils import parse_frontmatter, strip_frontmatter
|
||||
from beaver.skills.specs import SkillSpec, SkillSpecStore, SkillVersion
|
||||
from beaver.skills.specs.serialization import canonical_hash, normalize_frontmatter, summarize_skill_content
|
||||
|
||||
|
||||
HERMES_REPO_URL = "https://github.com/NousResearch/hermes-agent"
|
||||
|
||||
_CREDENTIAL_PATTERNS = [
|
||||
re.compile(pattern, re.IGNORECASE)
|
||||
for pattern in [
|
||||
r"\bapi[_ -]?key\b",
|
||||
r"\boauth\b",
|
||||
r"\bbearer\s+token\b",
|
||||
r"\baccess[_ -]?token\b",
|
||||
r"\bclient[_ -]?secret\b",
|
||||
r"\bsecret\b",
|
||||
r"\bcredential",
|
||||
r"\bspotify\b",
|
||||
r"\bdiscord\b",
|
||||
r"\bfeishu\b",
|
||||
r"\bhome\s*assistant\b",
|
||||
r"\bfal\b",
|
||||
r"\bopenrouter\b",
|
||||
r"\bwandb\b",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class HermesMigrationService:
|
||||
store: SkillSpecStore
|
||||
manifest_path: Path | None = None
|
||||
included_tools: list[dict[str, Any]] = field(default_factory=list)
|
||||
skipped_tools: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
def migrate(
|
||||
self,
|
||||
repo_path: str | Path,
|
||||
*,
|
||||
include_optional: bool = True,
|
||||
dry_run: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
repo = Path(repo_path)
|
||||
if not repo.exists():
|
||||
raise ValueError(f"Hermes repository not found: {repo}")
|
||||
skill_files = self._discover_skill_files(repo, include_optional=include_optional)
|
||||
included: list[dict[str, Any]] = []
|
||||
skipped: list[dict[str, Any]] = []
|
||||
for skill_file in skill_files:
|
||||
result = self._migrate_skill(repo, skill_file, dry_run=dry_run)
|
||||
if result["status"] in {"included", "unchanged"}:
|
||||
included.append(result)
|
||||
else:
|
||||
skipped.append(result)
|
||||
manifest = {
|
||||
"source": "hermes-agent",
|
||||
"repo_url": HERMES_REPO_URL,
|
||||
"repo_path": str(repo),
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"dry_run": dry_run,
|
||||
"included": included,
|
||||
"skipped": skipped,
|
||||
"tools": self._tool_manifest(),
|
||||
}
|
||||
path = self.manifest_path or (self.store.workspace / "hermes_migration_manifest.json")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return manifest
|
||||
|
||||
def _discover_skill_files(self, repo: Path, *, include_optional: bool) -> list[Path]:
|
||||
roots = [repo / "skills"]
|
||||
if include_optional:
|
||||
roots.append(repo / "optional-skills")
|
||||
files: list[Path] = []
|
||||
for root in roots:
|
||||
if root.exists():
|
||||
files.extend(sorted(root.glob("**/SKILL.md")))
|
||||
return files
|
||||
|
||||
def _migrate_skill(self, repo: Path, skill_file: Path, *, dry_run: bool) -> dict[str, Any]:
|
||||
relative = skill_file.relative_to(repo)
|
||||
content = skill_file.read_text(encoding="utf-8")
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
skill_name = _safe_skill_name(str(frontmatter.get("name") or skill_file.parent.name))
|
||||
if not skill_name:
|
||||
return _skip(relative, "unsafe_skill_name")
|
||||
credential_reason = _credential_reason(content)
|
||||
if credential_reason:
|
||||
return _skip(relative, credential_reason, skill_name=skill_name)
|
||||
normalized = normalize_frontmatter(
|
||||
{
|
||||
**frontmatter,
|
||||
"name": skill_name,
|
||||
"description": frontmatter.get("description") or skill_name,
|
||||
}
|
||||
)
|
||||
rendered = _render_skill_content(normalized, body)
|
||||
content_hash = canonical_hash(rendered)
|
||||
existing = self.store.read_published_skill(skill_name)
|
||||
existing_spec = self.store.get_skill_spec(skill_name)
|
||||
if existing is not None and existing.version.content_hash == content_hash:
|
||||
return {
|
||||
"status": "unchanged",
|
||||
"skill_name": skill_name,
|
||||
"version": existing.version.version,
|
||||
"path": str(relative),
|
||||
"reason": "same_content_hash",
|
||||
}
|
||||
next_version = self._next_version(skill_name)
|
||||
if dry_run:
|
||||
return {
|
||||
"status": "included",
|
||||
"skill_name": skill_name,
|
||||
"version": next_version,
|
||||
"path": str(relative),
|
||||
"dry_run": True,
|
||||
}
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
skill_version = SkillVersion(
|
||||
skill_name=skill_name,
|
||||
version=next_version,
|
||||
content_hash=content_hash,
|
||||
summary_hash=canonical_hash(strip_frontmatter(rendered).strip()),
|
||||
created_at=now,
|
||||
created_by="hermes_migration",
|
||||
change_reason=f"Import Hermes skill {relative}",
|
||||
parent_version=existing.version.version if existing is not None else None,
|
||||
review_state="published",
|
||||
frontmatter=normalized,
|
||||
summary=summarize_skill_content(body),
|
||||
tool_hints=self.store._extract_tool_hints(normalized),
|
||||
provenance={
|
||||
"source": "hermes-agent",
|
||||
"repo_url": HERMES_REPO_URL,
|
||||
"repo_path": str(repo),
|
||||
"relative_path": str(relative),
|
||||
},
|
||||
)
|
||||
self.store.write_skill_version(skill_version, rendered)
|
||||
self._copy_supporting_files(skill_file.parent, skill_name, next_version)
|
||||
spec = existing_spec or SkillSpec(
|
||||
name=skill_name,
|
||||
display_name=skill_name,
|
||||
description=str(normalized.get("description") or skill_name),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
current_version=next_version,
|
||||
status="active",
|
||||
tags=[],
|
||||
owners=["hermes-agent"],
|
||||
source_kind="hermes-agent",
|
||||
lineage=[],
|
||||
)
|
||||
spec.current_version = next_version
|
||||
spec.updated_at = now
|
||||
spec.status = "active"
|
||||
spec.source_kind = "hermes-agent"
|
||||
if "hermes-agent" not in spec.owners:
|
||||
spec.owners.append("hermes-agent")
|
||||
self.store.write_skill_spec(spec)
|
||||
self.store.set_current_version(skill_name, next_version)
|
||||
published = self.store.read_index("published")
|
||||
if skill_name not in published:
|
||||
published.append(skill_name)
|
||||
self.store.update_index("published", published)
|
||||
return {
|
||||
"status": "included",
|
||||
"skill_name": skill_name,
|
||||
"version": next_version,
|
||||
"path": str(relative),
|
||||
}
|
||||
|
||||
def _copy_supporting_files(self, source_dir: Path, skill_name: str, version: str) -> None:
|
||||
target_root = self.store.root / skill_name / "versions" / version
|
||||
for source in sorted(source_dir.rglob("*")):
|
||||
if not source.is_file() or source.name == "SKILL.md" or source.is_symlink():
|
||||
continue
|
||||
relative = source.relative_to(source_dir)
|
||||
if any(part in {"", ".", ".."} for part in relative.parts):
|
||||
continue
|
||||
target = target_root / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, target)
|
||||
|
||||
def _next_version(self, skill_name: str) -> str:
|
||||
versions = [item for item in self.store.list_versions(skill_name) if item.startswith("v")]
|
||||
numbers = [int(item[1:]) for item in versions if item[1:].isdigit()]
|
||||
return f"v{(max(numbers) if numbers else 0) + 1:04d}"
|
||||
|
||||
def _tool_manifest(self) -> dict[str, list[dict[str, Any]]]:
|
||||
included = self.included_tools or [
|
||||
{"name": "todo", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "clarify", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "delegate", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "spawn", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "skills_list", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "skill_manage", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "terminal", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "process", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "patch", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "write_file", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "web_fetch", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "web_search", "reason": "implemented_builtin_no_api"},
|
||||
{"name": "execute_code", "reason": "implemented_builtin_no_api"},
|
||||
]
|
||||
skipped = self.skipped_tools or [
|
||||
{"name": "spotify", "reason": "requires_oauth"},
|
||||
{"name": "discord", "reason": "requires_external_token"},
|
||||
{"name": "feishu", "reason": "requires_external_token"},
|
||||
{"name": "home_assistant", "reason": "requires_external_service_credentials"},
|
||||
{"name": "fal_image_generation", "reason": "requires_api_key"},
|
||||
{"name": "remote_web_providers", "reason": "requires_api_key_or_oauth"},
|
||||
]
|
||||
return {"included": included, "skipped": skipped}
|
||||
|
||||
|
||||
def _credential_reason(content: str) -> str | None:
|
||||
for pattern in _CREDENTIAL_PATTERNS:
|
||||
if pattern.search(content):
|
||||
return "requires_external_credentials"
|
||||
return None
|
||||
|
||||
|
||||
def _safe_skill_name(value: str) -> str:
|
||||
cleaned = value.strip().replace(" ", "-")
|
||||
if not cleaned or cleaned in {".", ".."} or "/" in cleaned or "\\" in cleaned:
|
||||
return ""
|
||||
if not re.fullmatch(r"[A-Za-z0-9_.-]+", cleaned):
|
||||
return ""
|
||||
return cleaned
|
||||
|
||||
|
||||
def _skip(relative: Path, reason: str, *, skill_name: str | None = None) -> dict[str, Any]:
|
||||
result = {"status": "skipped", "path": str(relative), "reason": reason}
|
||||
if skill_name:
|
||||
result["skill_name"] = skill_name
|
||||
return result
|
||||
|
||||
|
||||
def _render_skill_content(frontmatter: dict[str, Any], body: str) -> str:
|
||||
lines = ["---"]
|
||||
for key, value in normalize_frontmatter(frontmatter).items():
|
||||
if isinstance(value, list):
|
||||
lines.append(f"{key}:")
|
||||
for item in value:
|
||||
lines.append(f" - {item}")
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.extend(["---", "", body.strip()])
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
@ -1,208 +0,0 @@
|
||||
"""Import legacy and staged skills into the Beaver SkillSpecStore."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from beaver.skills.catalog.utils import parse_frontmatter, strip_frontmatter
|
||||
from beaver.skills.specs import SkillSpec, SkillSpecStore, SkillVersion
|
||||
from beaver.skills.specs.serialization import canonical_hash, normalize_frontmatter, summarize_skill_content
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SkillMigrationService:
|
||||
store: SkillSpecStore
|
||||
repo_root: Path | None = None
|
||||
|
||||
def migrate_all(self) -> dict[str, Any]:
|
||||
included: list[dict[str, Any]] = []
|
||||
skipped: list[dict[str, Any]] = []
|
||||
for path in self._backend_old_skills():
|
||||
self._migrate_skill_file(path, "backend-old", included, skipped)
|
||||
for path in self._staged_skills():
|
||||
self._migrate_skill_file(path, "stevenli-staged", included, skipped)
|
||||
for path in self._skill_zips():
|
||||
self._migrate_zip(path, included, skipped)
|
||||
manifest = {
|
||||
"generated_at": _now(),
|
||||
"workspace": str(self.store.workspace),
|
||||
"included": included,
|
||||
"skipped": skipped,
|
||||
}
|
||||
manifest_path = self.store.workspace / "skill_migration_manifest.json"
|
||||
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return manifest
|
||||
|
||||
def _backend_old_skills(self) -> list[Path]:
|
||||
root = self._repo_root() / "app-instance" / "backend-old" / "nanobot" / "skills"
|
||||
if not root.exists():
|
||||
return []
|
||||
return sorted(root.glob("*/SKILL.md"))
|
||||
|
||||
def _staged_skills(self) -> list[Path]:
|
||||
root = self.store.workspace / "state" / "skill-reviews"
|
||||
if not root.exists():
|
||||
return []
|
||||
return sorted(root.glob("*/staged/*/SKILL.md"))
|
||||
|
||||
def _skill_zips(self) -> list[Path]:
|
||||
root = self.store.workspace / "skills"
|
||||
if not root.exists():
|
||||
return []
|
||||
return sorted(root.glob("*.zip"))
|
||||
|
||||
def _repo_root(self) -> Path:
|
||||
if self.repo_root is not None:
|
||||
return self.repo_root
|
||||
return Path(__file__).resolve().parents[4]
|
||||
|
||||
def _migrate_skill_file(self, path: Path, source: str, included: list[dict[str, Any]], skipped: list[dict[str, Any]]) -> None:
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
result = self._publish_content(content, source=source, source_path=str(path))
|
||||
included.append(result)
|
||||
except Exception as exc:
|
||||
skipped.append({"source": source, "source_path": str(path), "reason": str(exc)})
|
||||
|
||||
def _migrate_zip(self, path: Path, included: list[dict[str, Any]], skipped: list[dict[str, Any]]) -> None:
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(path.read_bytes()), "r") as archive:
|
||||
entries = [info for info in archive.infolist() if not info.is_dir()]
|
||||
skill_entry = _find_skill_entry(entries)
|
||||
content = archive.read(skill_entry).decode("utf-8", errors="replace")
|
||||
result = self._publish_content(content, source="stevenli-zip", source_path=str(path))
|
||||
skill_name = result["skill_name"]
|
||||
version = result["version"]
|
||||
top = Path(skill_entry).parts[0] if len(Path(skill_entry).parts) == 2 else ""
|
||||
for info in entries:
|
||||
raw = info.filename.replace("\\", "/")
|
||||
if raw == skill_entry or raw.startswith("/") or "__MACOSX" in Path(raw).parts:
|
||||
continue
|
||||
parts = Path(raw).parts
|
||||
rel_parts = parts[1:] if top and parts and parts[0] == top else parts
|
||||
if not rel_parts or any(part in {"", ".", ".."} for part in rel_parts):
|
||||
continue
|
||||
target = self.store.root / skill_name / "versions" / version / "/".join(rel_parts)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(archive.read(info))
|
||||
included.append(result)
|
||||
except Exception as exc:
|
||||
skipped.append({"source": "stevenli-zip", "source_path": str(path), "reason": str(exc)})
|
||||
|
||||
def _publish_content(self, content: str, *, source: str, source_path: str) -> dict[str, Any]:
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
skill_name = _safe_name(str(frontmatter.get("name") or Path(source_path).parent.name))
|
||||
if not skill_name:
|
||||
raise ValueError("unsafe or missing skill name")
|
||||
normalized = normalize_frontmatter(
|
||||
{
|
||||
**frontmatter,
|
||||
"name": skill_name,
|
||||
"description": frontmatter.get("description") or skill_name,
|
||||
}
|
||||
)
|
||||
rendered = _render_skill_content(normalized, body)
|
||||
content_hash = canonical_hash(rendered)
|
||||
existing = self.store.read_published_skill(skill_name)
|
||||
if existing is not None and existing.version.content_hash == content_hash:
|
||||
return {
|
||||
"status": "unchanged",
|
||||
"skill_name": skill_name,
|
||||
"version": existing.version.version,
|
||||
"source": source,
|
||||
"source_path": source_path,
|
||||
}
|
||||
version_id = self._next_version(skill_name)
|
||||
now = _now()
|
||||
skill_version = SkillVersion(
|
||||
skill_name=skill_name,
|
||||
version=version_id,
|
||||
content_hash=content_hash,
|
||||
summary_hash=canonical_hash(strip_frontmatter(rendered).strip()),
|
||||
created_at=now,
|
||||
created_by="migration",
|
||||
change_reason=f"Import skill from {source}",
|
||||
parent_version=existing.version.version if existing is not None else None,
|
||||
review_state="published",
|
||||
frontmatter=normalized,
|
||||
summary=summarize_skill_content(body),
|
||||
tool_hints=self.store._extract_tool_hints(normalized),
|
||||
provenance={"source": source, "source_path": source_path, "imported_at": now},
|
||||
)
|
||||
self.store.write_skill_version(skill_version, rendered)
|
||||
spec = self.store.get_skill_spec(skill_name) or SkillSpec(
|
||||
name=skill_name,
|
||||
display_name=skill_name,
|
||||
description=str(normalized.get("description") or skill_name),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
current_version=version_id,
|
||||
status="active",
|
||||
tags=[],
|
||||
owners=["migration"],
|
||||
source_kind=source,
|
||||
lineage=[],
|
||||
)
|
||||
spec.current_version = version_id
|
||||
spec.updated_at = now
|
||||
spec.status = "active"
|
||||
spec.source_kind = source
|
||||
if "migration" not in spec.owners:
|
||||
spec.owners.append("migration")
|
||||
self.store.write_skill_spec(spec)
|
||||
self.store.set_current_version(skill_name, version_id)
|
||||
published = self.store.read_index("published")
|
||||
if skill_name not in published:
|
||||
published.append(skill_name)
|
||||
self.store.update_index("published", published)
|
||||
return {"status": "included", "skill_name": skill_name, "version": version_id, "source": source, "source_path": source_path}
|
||||
|
||||
def _next_version(self, skill_name: str) -> str:
|
||||
versions = [item for item in self.store.list_versions(skill_name) if item.startswith("v")]
|
||||
numbers = [int(item[1:]) for item in versions if item[1:].isdigit()]
|
||||
return f"v{(max(numbers) if numbers else 0) + 1:04d}"
|
||||
|
||||
|
||||
def _find_skill_entry(entries: list[zipfile.ZipInfo]) -> str:
|
||||
candidates = []
|
||||
for info in entries:
|
||||
raw = info.filename.replace("\\", "/")
|
||||
parts = Path(raw).parts
|
||||
if raw.startswith("/") or any(part in {"", ".", ".."} for part in parts):
|
||||
raise ValueError(f"unsafe archive entry: {info.filename}")
|
||||
if parts and parts[-1] == "SKILL.md" and len(parts) in (1, 2):
|
||||
candidates.append(raw)
|
||||
if not candidates:
|
||||
raise ValueError("zip has no root SKILL.md")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def _safe_name(value: str) -> str:
|
||||
cleaned = value.strip().replace(" ", "-")
|
||||
if not cleaned or cleaned in {".", ".."} or "/" in cleaned or "\\" in cleaned:
|
||||
return ""
|
||||
return cleaned if re.fullmatch(r"[A-Za-z0-9_.-]+", cleaned) else ""
|
||||
|
||||
|
||||
def _render_skill_content(frontmatter: dict[str, Any], body: str) -> str:
|
||||
lines = ["---"]
|
||||
for key, value in normalize_frontmatter(frontmatter).items():
|
||||
if isinstance(value, list):
|
||||
lines.append(f"{key}:")
|
||||
for item in value:
|
||||
lines.append(f" - {item}")
|
||||
else:
|
||||
lines.append(f"{key}: {value}")
|
||||
lines.extend(["---", "", body.strip()])
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
Reference in New Issue
Block a user