源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,599 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=protected-access
|
||||
"""CloudPaw Plugin for QwenPaw.
|
||||
|
||||
Provides Alibaba Cloud deployment orchestration capabilities:
|
||||
- Built-in agents (Orchestration + IaC Code + Executor + Verifier)
|
||||
- Custom skills (alicloud_cli, terraform, etc.)
|
||||
- Custom tools (proposal_choice, manage_prd)
|
||||
- API router (interaction)
|
||||
- IaC Code ACP integration (iac-code agent via ACP protocol)
|
||||
|
||||
Uses the plugin startup hook to inject all components into the main system.
|
||||
See README.md for detailed implementation notes.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("qwenpaw").getChild("plugin.cloudpaw")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin path initialization (must run before any router/tool imports)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _init_plugin_path() -> None:
|
||||
"""Ensure the plugin directory is on sys.path.
|
||||
|
||||
This eliminates the cold-start window where absolute imports like
|
||||
``from modules.a2a.client_manager`` would fail because ``constants.py``
|
||||
(which does ``sys.path.insert``) has not been imported yet.
|
||||
"""
|
||||
plugin_dir = str(Path(__file__).parent)
|
||||
if plugin_dir not in sys.path:
|
||||
sys.path.insert(0, plugin_dir)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Skill installation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _install_plugin_skills() -> None:
|
||||
"""Copy plugin skills into the shared skill pool."""
|
||||
from .constants import PLUGIN_DIR, _PLUGIN_SKILLS
|
||||
|
||||
try:
|
||||
from qwenpaw.agents.skill_system import (
|
||||
get_skill_pool_dir,
|
||||
ensure_skill_pool_initialized,
|
||||
)
|
||||
except ImportError:
|
||||
logger.error(
|
||||
"Cannot import skill_system; skill installation skipped",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
ensure_skill_pool_initialized()
|
||||
except Exception as exc:
|
||||
logger.warning("Skill pool init failed: %s", exc)
|
||||
|
||||
pool_dir = get_skill_pool_dir()
|
||||
skills_src = PLUGIN_DIR / "skills"
|
||||
|
||||
for skill_name in _PLUGIN_SKILLS:
|
||||
src = skills_src / skill_name
|
||||
dst = pool_dir / skill_name
|
||||
if not src.exists():
|
||||
logger.warning("Plugin skill source missing: %s", src)
|
||||
continue
|
||||
if dst.exists():
|
||||
logger.debug("Skill %s already in pool, updating...", skill_name)
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst)
|
||||
logger.info("Installed plugin skill to pool: %s", skill_name)
|
||||
|
||||
_update_pool_manifest(pool_dir)
|
||||
|
||||
|
||||
def _update_pool_manifest(pool_dir: Path) -> None:
|
||||
"""Update skill.json manifest to include newly installed skills."""
|
||||
from .constants import _PLUGIN_SKILLS
|
||||
|
||||
manifest_path = pool_dir / "skill.json"
|
||||
try:
|
||||
if manifest_path.exists():
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
else:
|
||||
manifest = {"skills": {}, "builtin_skill_names": []}
|
||||
|
||||
skills = manifest.setdefault("skills", {})
|
||||
for skill_name in _PLUGIN_SKILLS:
|
||||
skill_dir = pool_dir / skill_name
|
||||
if not skill_dir.exists():
|
||||
continue
|
||||
if skill_name not in skills:
|
||||
skills[skill_name] = {
|
||||
"source": "plugin:cloudpaw",
|
||||
"protected": False,
|
||||
}
|
||||
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to update pool manifest: %s", exc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Default environment variable provisioning
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_DEFAULT_ENV_KEYS = (
|
||||
"ALIBABA_CLOUD_ACCESS_KEY_ID",
|
||||
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
|
||||
"ALIBABA_CLOUD_REGION_ID",
|
||||
)
|
||||
|
||||
_DEFAULT_ENV_VALUES: dict[str, str] = {
|
||||
"ALIBABA_CLOUD_REGION_ID": "cn-hangzhou",
|
||||
}
|
||||
|
||||
|
||||
def _ensure_default_env_vars() -> None:
|
||||
"""Ensure required env var keys always appear in the QwenPaw console.
|
||||
|
||||
For each key in ``_DEFAULT_ENV_KEYS``:
|
||||
- If already in envs.json → keep as-is (user may have edited it).
|
||||
- If absent from envs.json but present in ``os.environ`` → copy the
|
||||
system value into envs.json so it is visible in the console.
|
||||
- If absent from both → create a blank placeholder.
|
||||
"""
|
||||
try:
|
||||
from qwenpaw.envs import load_envs, save_envs
|
||||
except ImportError:
|
||||
logger.warning("Cannot import qwenpaw.envs; env provisioning skipped")
|
||||
return
|
||||
|
||||
envs = load_envs()
|
||||
changed = False
|
||||
for key in _DEFAULT_ENV_KEYS:
|
||||
if key not in envs:
|
||||
fallback = _DEFAULT_ENV_VALUES.get(key, "")
|
||||
envs[key] = os.environ.get(key, fallback)
|
||||
changed = True
|
||||
if changed:
|
||||
save_envs(envs)
|
||||
logger.info(
|
||||
"Provisioned default env keys into envs.json: %s",
|
||||
[k for k in _DEFAULT_ENV_KEYS if k in envs],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A2A client manager initialization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _init_a2a_manager() -> None:
|
||||
"""Initialize the A2A client manager singleton.
|
||||
|
||||
This ensures the manager is ready when a2a_discover / a2a_call
|
||||
tools are invoked. The actual connections are created lazily.
|
||||
"""
|
||||
try:
|
||||
from .modules.a2a.client_manager import get_a2a_manager
|
||||
|
||||
get_a2a_manager()
|
||||
logger.info("A2A client manager initialized")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to initialize A2A client manager: %s", e)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Alibaba Cloud CLI (aliyun) detection and auto-install
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_INSTALL_PS1_PATH = (
|
||||
Path(__file__).parent / "scripts" / "Install-CLI-Windows.ps1"
|
||||
)
|
||||
|
||||
|
||||
def _get_aliyun_cli_tgz_url() -> str | None:
|
||||
"""Return the tgz download URL for aliyun CLI (manual fallback)."""
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if system == "Darwin":
|
||||
return (
|
||||
"https://aliyuncli.alicdn.com/"
|
||||
"aliyun-cli-macosx-latest-universal.tgz"
|
||||
)
|
||||
elif system == "Linux":
|
||||
if machine in ("aarch64", "arm64"):
|
||||
return (
|
||||
"https://aliyuncli.alicdn.com/"
|
||||
"aliyun-cli-linux-latest-arm64.tgz"
|
||||
)
|
||||
return "https://aliyuncli.alicdn.com/aliyun-cli-linux-latest-amd64.tgz"
|
||||
return None
|
||||
|
||||
|
||||
# -- Installation strategies (tried in priority order per platform) ----------
|
||||
|
||||
|
||||
def _install_via_bash_script() -> bool:
|
||||
"""Install via the official install.sh one-liner (macOS & Linux)."""
|
||||
if platform.system() not in ("Darwin", "Linux"):
|
||||
return False
|
||||
try:
|
||||
logger.info("Installing aliyun CLI via official install.sh script")
|
||||
subprocess.run(
|
||||
[
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
"$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)",
|
||||
],
|
||||
check=True,
|
||||
timeout=180,
|
||||
)
|
||||
if shutil.which("aliyun") is not None:
|
||||
logger.info("aliyun CLI installed via official bash script")
|
||||
return True
|
||||
logger.warning("install.sh finished but aliyun not found on PATH")
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.warning("Official install.sh failed: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def _install_via_homebrew() -> bool:
|
||||
"""Install via Homebrew (macOS only)."""
|
||||
if platform.system() != "Darwin":
|
||||
return False
|
||||
if shutil.which("brew") is None:
|
||||
logger.info("Homebrew not available, skipping brew install")
|
||||
return False
|
||||
try:
|
||||
logger.info("Installing aliyun CLI via Homebrew")
|
||||
subprocess.run(
|
||||
["brew", "install", "aliyun-cli"],
|
||||
check=True,
|
||||
timeout=300,
|
||||
)
|
||||
if shutil.which("aliyun") is not None:
|
||||
logger.info("aliyun CLI installed via Homebrew")
|
||||
return True
|
||||
logger.warning("brew install succeeded but aliyun not found on PATH")
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.warning("Homebrew install failed: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def _install_via_tgz() -> bool:
|
||||
"""Download tgz and install manually (macOS / Linux fallback)."""
|
||||
url = _get_aliyun_cli_tgz_url()
|
||||
if url is None:
|
||||
logger.warning(
|
||||
"Unsupported platform for tgz install: %s/%s",
|
||||
platform.system(),
|
||||
platform.machine(),
|
||||
)
|
||||
return False
|
||||
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
archive_path = os.path.join(tmp_dir, "aliyun-cli.tgz")
|
||||
logger.info("Downloading aliyun CLI from %s", url)
|
||||
subprocess.run(
|
||||
["curl", "-fsSL", url, "-o", archive_path],
|
||||
check=True,
|
||||
timeout=120,
|
||||
)
|
||||
subprocess.run(
|
||||
["tar", "-xzf", archive_path, "-C", tmp_dir],
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
aliyun_bin = os.path.join(tmp_dir, "aliyun")
|
||||
if not os.path.isfile(aliyun_bin):
|
||||
logger.error("aliyun binary not found after extraction")
|
||||
return False
|
||||
|
||||
install_dir = "/usr/local/bin"
|
||||
dest = os.path.join(install_dir, "aliyun")
|
||||
try:
|
||||
shutil.move(aliyun_bin, dest)
|
||||
except PermissionError:
|
||||
subprocess.run(
|
||||
["sudo", "mv", aliyun_bin, dest],
|
||||
check=True,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
os.chmod(dest, 0o755)
|
||||
logger.info("aliyun CLI installed to %s", dest)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to install aliyun CLI via tgz: %s", exc)
|
||||
return False
|
||||
finally:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def _install_via_powershell() -> bool:
|
||||
"""Install on Windows using the official PowerShell script."""
|
||||
if platform.system() != "Windows":
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.info("Installing aliyun CLI via PowerShell script")
|
||||
subprocess.run(
|
||||
[
|
||||
"powershell.exe",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(_INSTALL_PS1_PATH),
|
||||
],
|
||||
check=True,
|
||||
timeout=180,
|
||||
)
|
||||
|
||||
install_dir = os.path.join(
|
||||
os.environ.get("LOCALAPPDATA", os.path.expanduser("~")),
|
||||
"AliyunCLI",
|
||||
)
|
||||
aliyun_exe = os.path.join(install_dir, "aliyun.exe")
|
||||
if os.path.isfile(aliyun_exe):
|
||||
current_path = os.environ.get("PATH", "")
|
||||
if install_dir not in current_path:
|
||||
os.environ["PATH"] = current_path + os.pathsep + install_dir
|
||||
logger.info("aliyun CLI installed to %s", aliyun_exe)
|
||||
return True
|
||||
|
||||
if shutil.which("aliyun") is not None:
|
||||
logger.info("aliyun CLI installed via PowerShell script")
|
||||
return True
|
||||
|
||||
logger.warning("PowerShell script finished but aliyun not found")
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.warning("PowerShell install failed: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def _install_aliyun_cli_blocking() -> bool:
|
||||
"""Download and install aliyun CLI. Returns True on success.
|
||||
|
||||
Installation priority per platform:
|
||||
macOS : bash script → Homebrew → tgz
|
||||
Linux : bash script → tgz
|
||||
Windows : PowerShell script
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
return _install_via_powershell()
|
||||
|
||||
strategies: list[tuple[str, Callable[[], bool]]] = [
|
||||
("bash script", _install_via_bash_script),
|
||||
]
|
||||
if system == "Darwin":
|
||||
strategies.append(("Homebrew", _install_via_homebrew))
|
||||
strategies.append(("tgz", _install_via_tgz))
|
||||
|
||||
for name, fn in strategies:
|
||||
logger.info("Trying aliyun CLI install via %s...", name)
|
||||
if fn():
|
||||
return True
|
||||
logger.info("Install via %s did not succeed, trying next...", name)
|
||||
|
||||
logger.warning("All aliyun CLI installation methods failed")
|
||||
return False
|
||||
|
||||
|
||||
async def _ensure_aliyun_cli() -> None:
|
||||
"""Check if aliyun CLI is available; install if missing."""
|
||||
if shutil.which("aliyun") is not None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["aliyun", "version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
)
|
||||
version = result.stdout.strip() or "unknown"
|
||||
logger.info("aliyun CLI already installed: %s", version)
|
||||
except Exception:
|
||||
logger.info("aliyun CLI found on PATH")
|
||||
return
|
||||
|
||||
logger.info("aliyun CLI not found, attempting auto-install...")
|
||||
success = await asyncio.to_thread(_install_aliyun_cli_blocking)
|
||||
if success:
|
||||
logger.info("aliyun CLI auto-install completed")
|
||||
else:
|
||||
logger.warning(
|
||||
"aliyun CLI auto-install failed. Install manually: "
|
||||
"https://help.aliyun.com/zh/cli/",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin loader monkeypatch for uninstall cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _patch_plugin_loader_unload() -> None:
|
||||
"""Monkeypatch PluginLoader.unload_plugin to clean up CloudPaw agents
|
||||
when the plugin is uninstalled.
|
||||
"""
|
||||
try:
|
||||
from qwenpaw.plugins.loader import PluginLoader
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"Cannot import PluginLoader; uninstall patch skipped",
|
||||
)
|
||||
return
|
||||
|
||||
if getattr(PluginLoader.unload_plugin, "_cloudpaw_patched", False):
|
||||
logger.debug("unload_plugin already patched; skip")
|
||||
return
|
||||
|
||||
_original_unload_plugin = PluginLoader.unload_plugin
|
||||
|
||||
async def _patched_unload_plugin(
|
||||
self,
|
||||
plugin_id: str,
|
||||
delete_files: bool = False,
|
||||
) -> None:
|
||||
if plugin_id == "cloudpaw":
|
||||
logger.info(
|
||||
"[CloudPaw] Uninstall detected, cleaning up agents...",
|
||||
)
|
||||
try:
|
||||
from .agents_setup import uninstall_agents
|
||||
|
||||
uninstall_agents()
|
||||
logger.info("[CloudPaw] Agent cleanup completed")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"[CloudPaw] Agent cleanup failed: %s",
|
||||
exc,
|
||||
)
|
||||
|
||||
return await _original_unload_plugin(self, plugin_id, delete_files)
|
||||
|
||||
_patched_unload_plugin._cloudpaw_patched = True
|
||||
_patched_unload_plugin._original = _original_unload_plugin
|
||||
PluginLoader.unload_plugin = _patched_unload_plugin
|
||||
logger.info("[CloudPaw] Patched PluginLoader.unload_plugin")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CloudPawPlugin:
|
||||
"""CloudPaw plugin entry point."""
|
||||
|
||||
def register(self, api):
|
||||
"""Register all CloudPaw components via startup hook."""
|
||||
logger.info("CloudPawPlugin.register() called")
|
||||
|
||||
# Ensure plugin dir is on sys.path BEFORE any router/tool imports.
|
||||
_init_plugin_path()
|
||||
|
||||
# Inject synthetic modules BEFORE route registration so that
|
||||
# routers_setup.py can import InteractionManager. This must
|
||||
# happen early because on cold restart sys.modules is empty.
|
||||
from .injectors import inject_interaction_module
|
||||
|
||||
inject_interaction_module()
|
||||
logger.info("CloudPaw: injected synthetic modules")
|
||||
|
||||
# Register HTTP routers via the official PluginApi — no manual
|
||||
# app mounting needed. The registry already has the FastAPI app
|
||||
# set via set_plugin_http_app() before load_all_plugins().
|
||||
try:
|
||||
from .routers_setup import build_plugin_routers
|
||||
|
||||
routers = build_plugin_routers()
|
||||
logger.info(
|
||||
"CloudPaw: got %d HTTP routers: %s",
|
||||
len(routers),
|
||||
[(r.prefix, p) for r, p in routers],
|
||||
)
|
||||
for router, prefix in routers:
|
||||
logger.info(
|
||||
"CloudPaw: registering router at prefix '/api%s'",
|
||||
prefix,
|
||||
)
|
||||
api.register_http_router(router, prefix=prefix)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to register HTTP routers: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Register A2A mode via official PluginApi.
|
||||
# This registers /a2a as a slash command via SlashCommandRegistry.
|
||||
try:
|
||||
from .a2a_mode import A2AMode
|
||||
|
||||
api.register_mode(A2AMode)
|
||||
logger.info("CloudPaw: registered A2A mode")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to register A2A mode: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
api.register_startup_hook(
|
||||
hook_name="cloudpaw_init",
|
||||
callback=self._on_startup,
|
||||
priority=50,
|
||||
)
|
||||
api.register_shutdown_hook(
|
||||
hook_name="cloudpaw_cleanup",
|
||||
callback=self._on_shutdown,
|
||||
priority=50,
|
||||
)
|
||||
logger.info("CloudPaw plugin registered hooks")
|
||||
|
||||
async def _on_startup(self):
|
||||
"""Initialize all CloudPaw components on application startup."""
|
||||
from .agents_setup import ensure_builtin_agents
|
||||
from .hooks import (
|
||||
setup_tool_and_prompt_hooks,
|
||||
setup_mission_hooks,
|
||||
setup_acp_auto_approve,
|
||||
)
|
||||
|
||||
logger.info("CloudPaw plugin starting up...")
|
||||
|
||||
logger.info("[CloudPaw] Ensuring default environment variables...")
|
||||
_ensure_default_env_vars()
|
||||
|
||||
logger.info("[CloudPaw] Installing skills to pool...")
|
||||
_install_plugin_skills()
|
||||
|
||||
logger.info("[CloudPaw] Registering built-in agents...")
|
||||
ensure_builtin_agents()
|
||||
|
||||
logger.info("[CloudPaw] Setting up tool and prompt hooks...")
|
||||
setup_tool_and_prompt_hooks()
|
||||
|
||||
logger.info("[CloudPaw] Setting up ACP permission auto-approve...")
|
||||
setup_acp_auto_approve()
|
||||
|
||||
logger.info("[CloudPaw] Setting up mission mode hooks...")
|
||||
setup_mission_hooks()
|
||||
|
||||
logger.info("[CloudPaw] Initializing A2A client manager...")
|
||||
_init_a2a_manager()
|
||||
|
||||
logger.info("[CloudPaw] Checking aliyun CLI availability...")
|
||||
await _ensure_aliyun_cli()
|
||||
|
||||
logger.info(
|
||||
"[CloudPaw] Patching plugin loader for uninstall cleanup...",
|
||||
)
|
||||
_patch_plugin_loader_unload()
|
||||
|
||||
logger.info("CloudPaw plugin startup complete")
|
||||
|
||||
async def _on_shutdown(self):
|
||||
"""Cleanup on application shutdown."""
|
||||
logger.info("CloudPaw plugin shutting down...")
|
||||
try:
|
||||
from .modules.a2a.client_manager import shutdown_a2a_manager
|
||||
|
||||
await shutdown_a2a_manager()
|
||||
logger.info("[CloudPaw] A2A client manager shut down")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to shut down A2A manager: %s", e)
|
||||
|
||||
|
||||
plugin = CloudPawPlugin()
|
||||
Reference in New Issue
Block a user