初始提交 - FutureOSS v1.0 插件化运行时框架

一切皆为插件的开发者工具运行时框架

🧩 核心特性:
  - 插件热插拔 (importlib 动态加载)
  - 依赖自动解析 (拓扑排序 + 循环检测)
  - 企业级稳定 (熔断/降级/重试/隔离)
  - 事件驱动 (发布/订阅事件总线)
  - 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
Falck
2026-04-06 09:57:10 +08:00
commit 76147bae94
174 changed files with 15626 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# plugin-storage 插件存储
为所有插件提供隔离的键值存储服务。
## 功能
- **隔离存储**:每个插件有独立的命名空间
- **持久化**:数据自动保存到 JSON 文件
- **线程安全**:支持并发访问
- **共享访问**:通过 plugin-bridge 可跨插件访问
## 基本使用
```python
storage_plugin = plugin_mgr.get("plugin-storage")
# 获取插件的隔离存储
storage = storage_plugin.get_storage("my-plugin")
# 设置值
storage.set("key", "value")
storage.set("config", {"theme": "dark", "lang": "zh"})
# 获取值
value = storage.get("key")
config = storage.get("config", default={})
# 检查键
if storage.has("key"):
print("存在")
# 删除
storage.delete("key")
# 批量设置
storage.set_many({"a": 1, "b": 2, "c": 3})
# 获取所有数据
all_data = storage.get_all()
# 清空
storage.clear()
```
## 通过 plugin-bridge 访问
```python
bridge = plugin_mgr.get("plugin-bridge")
shared_storage = bridge.storage # 假设 bridge 集成了 storage
# 获取其他插件的存储(需要权限)
other_storage = shared_storage.get_plugin_storage("other-plugin")
data = other_storage.get("some_key")
```
## 存储位置
```
./data/storage/
├── plugin-a/
│ └── data.json
├── plugin-b/
│ └── data.json
└── ...
```
## 元信息
```python
meta = storage.get_meta()
# {"plugin": "my-plugin", "keys": 5, "path": "./data/storage/my-plugin"}
```

View File

@@ -0,0 +1,350 @@
"""插件存储插件入口 - 统一文件读写服务"""
import json
import threading
import mimetypes
import shutil
from pathlib import Path
from typing import Any, Optional, BinaryIO
from datetime import datetime
from oss.plugin.types import Plugin, register_plugin_type, Response
class PluginStorage:
"""插件隔离存储 - 每个插件拥有独立的 data/<plugin_name>/ 目录"""
def __init__(self, plugin_name: str, data_dir: str = "./data"):
self.plugin_name = plugin_name
self.data_dir = Path(data_dir) / plugin_name
self.data_dir.mkdir(parents=True, exist_ok=True)
self._data: dict[str, Any] = {}
self._lock = threading.Lock()
self._load()
# ========== JSON 键值存储 ==========
def _load(self):
"""加载 JSON 存储数据"""
data_file = self.data_dir / "data.json"
if data_file.exists():
try:
with open(data_file, "r", encoding="utf-8") as f:
content = f.read().strip()
if content:
self._data = json.loads(content)
else:
self._data = {}
except (json.JSONDecodeError, IOError) as e:
print(f"[plugin-storage] 加载数据失败 {self.plugin_name}: {e}")
self._data = {}
def _save(self):
"""保存 JSON 存储数据"""
data_file = self.data_dir / "data.json"
with open(data_file, "w", encoding="utf-8") as f:
json.dump(self._data, f, ensure_ascii=False, indent=2)
def get(self, key: str, default: Any = None) -> Any:
"""获取 JSON 值"""
with self._lock:
return self._data.get(key, default)
def set(self, key: str, value: Any):
"""设置 JSON 值"""
with self._lock:
self._data[key] = value
self._save()
def delete(self, key: str) -> bool:
"""删除 JSON 键"""
with self._lock:
if key in self._data:
del self._data[key]
self._save()
return True
return False
def has(self, key: str) -> bool:
"""检查 JSON 键是否存在"""
with self._lock:
return key in self._data
def keys(self) -> list[str]:
"""获取所有 JSON 键"""
with self._lock:
return list(self._data.keys())
def clear(self):
"""清空 JSON 存储"""
with self._lock:
self._data.clear()
self._save()
def size(self) -> int:
"""获取 JSON 存储大小(键数量)"""
with self._lock:
return len(self._data)
def get_all(self) -> dict[str, Any]:
"""获取所有 JSON 数据"""
with self._lock:
return self._data.copy()
def set_many(self, data: dict[str, Any]):
"""批量设置 JSON"""
with self._lock:
self._data.update(data)
self._save()
def get_meta(self) -> dict[str, Any]:
"""获取存储元信息"""
return {
"plugin": self.plugin_name,
"keys": self.size(),
"path": str(self.data_dir),
}
# ========== 文件级别操作 ==========
def read_file(self, path: str, mode: str = "r") -> Optional[str | bytes]:
"""读取插件目录内的文件
Args:
path: 相对于插件数据目录的路径,如 "index.html""templates/home.html"
mode: "r" (文本) 或 "rb" (二进制)
Returns:
文件内容,文件不存在时返回 None
"""
try:
file_path = self._resolve_path(path)
if not file_path.exists() or not file_path.is_file():
return None
with open(file_path, mode, encoding="utf-8" if mode == "r" else None) as f:
return f.read()
except Exception as e:
print(f"[plugin-storage] 读取文件失败 {self.plugin_name}/{path}: {e}")
return None
def write_file(self, path: str, content: str | bytes):
"""写入文件到插件目录
Args:
path: 相对于插件数据目录的路径
content: 文件内容(字符串或字节)
"""
try:
file_path = self._resolve_path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
if isinstance(content, bytes):
with open(file_path, "wb") as f:
f.write(content)
else:
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
except Exception as e:
print(f"[plugin-storage] 写入文件失败 {self.plugin_name}/{path}: {e}")
def delete_file(self, path: str) -> bool:
"""删除插件目录内的文件"""
try:
file_path = self._resolve_path(path)
if file_path.exists():
file_path.unlink()
return True
return False
except Exception as e:
print(f"[plugin-storage] 删除文件失败 {self.plugin_name}/{path}: {e}")
return False
def list_files(self, prefix: str = "") -> list[str]:
"""列出插件目录内的文件
Args:
prefix: 子目录前缀,如 "templates/"""(全部)
Returns:
相对路径列表
"""
try:
search_dir = self._resolve_path(prefix) if prefix else self.data_dir
if not search_dir.exists():
return []
files = []
for f in search_dir.rglob("*"):
if f.is_file():
files.append(str(f.relative_to(self.data_dir)))
return sorted(files)
except Exception:
return []
def file_exists(self, path: str) -> bool:
"""检查文件是否存在"""
try:
file_path = self._resolve_path(path)
return file_path.exists() and file_path.is_file()
except Exception:
return False
def serve_file(self, path: str) -> Response:
"""提供文件服务(返回 HTTP Response
用于插件向外部提供静态文件。
自动检测 MIME 类型,支持文本和二进制文件。
Args:
path: 相对于插件数据目录的路径
Returns:
Response 对象200 成功 / 404 不存在 / 403 安全拦截)
"""
try:
file_path = self._resolve_path(path)
# 安全检查:防止目录遍历
try:
file_path.resolve().relative_to(self.data_dir.resolve())
except ValueError:
return Response(status=403, body="Forbidden: path traversal detected")
if not file_path.exists() or not file_path.is_file():
return Response(status=404, body=f"File not found: {path}")
# 检测 MIME 类型
content_type, _ = mimetypes.guess_type(str(file_path))
if not content_type:
content_type = "application/octet-stream"
# 读取文件内容
if content_type.startswith("text/") or content_type in (
"application/json", "application/javascript", "application/xml",
"text/css", "text/html", "image/svg+xml"
):
content = file_path.read_text(encoding="utf-8")
else:
content = file_path.read_bytes()
return Response(
status=200,
headers={
"Content-Type": content_type,
"Cache-Control": "public, max-age=3600",
},
body=content,
)
except Exception as e:
return Response(status=500, body=f"Error serving file: {e}")
def _resolve_path(self, path: str) -> Path:
"""解析相对于插件数据目录的安全路径"""
return (self.data_dir / path).resolve()
def get_data_dir(self) -> Path:
"""获取插件数据目录绝对路径"""
return self.data_dir.resolve()
class SharedStorage:
"""共享存储(供 plugin-bridge 使用)"""
def __init__(self, storage_manager, shared_dir: Path = None):
self._manager = storage_manager
self._shared_dir = shared_dir or Path("./data/DCIM")
self._shared_dir.mkdir(parents=True, exist_ok=True)
def get_plugin_storage(self, plugin_name: str) -> PluginStorage:
"""获取指定插件的存储空间"""
return self._manager.get_storage(plugin_name)
def get_shared(self, key: str, default: Any = None) -> Any:
"""获取共享存储 (DCIM)"""
shared_file = self._shared_dir / f"{key}.json"
if shared_file.exists():
with open(shared_file, "r", encoding="utf-8") as f:
return json.load(f)
return default
def set_shared(self, key: str, value: Any):
"""设置共享存储 (DCIM)"""
shared_file = self._shared_dir / f"{key}.json"
with open(shared_file, "w", encoding="utf-8") as f:
json.dump(value, f, ensure_ascii=False, indent=2)
def list_storages(self) -> list[str]:
"""列出所有有存储的插件"""
return self._manager.list_storages()
class PluginStoragePlugin(Plugin):
"""插件存储插件 - 所有插件的唯一文件读写入口"""
def __init__(self):
self.storages: dict[str, PluginStorage] = {}
self.shared = None
self.config = {}
self.data_root = Path("./data")
def init(self, deps: dict = None):
"""初始化 - 读取 config.json 配置"""
self._load_config()
def start(self):
"""启动"""
print(f"[plugin-storage] 插件存储服务已启动 (root={self.data_root})")
def stop(self):
"""停止"""
pass
def _load_config(self):
"""读取 config.json 配置"""
config_path = Path("./data/plugin-storage/config.json")
if config_path.exists():
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
self.data_root = Path(self.config.get("data_root", "./data"))
shared_dir_name = self.config.get("shared_dir", "DCIM")
shared_dir = self.data_root / shared_dir_name
else:
print("[plugin-storage] config.json 不存在,使用默认配置")
self.config = {"data_root": "./data", "shared_dir": "DCIM"}
self.data_root = Path("./data")
shared_dir = self.data_root / "DCIM"
self.shared = SharedStorage(self, shared_dir=shared_dir)
def get_storage(self, plugin_name: str) -> PluginStorage:
"""获取插件的隔离存储空间(唯一入口)"""
if plugin_name not in self.storages:
self.storages[plugin_name] = PluginStorage(
plugin_name,
data_dir=str(self.data_root)
)
return self.storages[plugin_name]
def remove_storage(self, plugin_name: str) -> bool:
"""删除插件的存储空间"""
if plugin_name in self.storages:
del self.storages[plugin_name]
data_dir = PluginStorage(plugin_name).data_dir
if data_dir.exists():
shutil.rmtree(data_dir)
return True
return False
def list_storages(self) -> list[str]:
"""列出所有有存储的插件"""
return list(self.storages.keys())
def get_shared(self) -> SharedStorage:
"""获取共享存储接口"""
return self.shared
# 注册类型
register_plugin_type("PluginStorage", PluginStorage)
register_plugin_type("SharedStorage", SharedStorage)
def New():
return PluginStoragePlugin()

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"name": "plugin-storage",
"version": "1.0.0",
"author": "FutureOSS",
"description": "插件存储 - 为所有插件提供隔离的键值存储服务",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"data_dir": "./data/storage"
}
},
"dependencies": [],
"permissions": ["*"]
}