初始提交 - 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,41 @@
# HTML 渲染服务
将存储在 plugin-storage 中的 HTML 页面映射到 8080 端口。
## 功能
- 从 plugin-storage 读取 HTML
- 自动注册路由到 web-toolkit
- 支持动态页面访问
- 页面管理(存储/获取/删除/列出)
## 使用
```python
html_render = plugin_mgr.get("html-render")
# 存储 HTML 页面
html_render.store_html("index", "<h1>Hello World</h1>")
html_render.store_html("about", "<h1>About</h1>")
# 获取页面
html = html_render.get_html("index")
# 列出所有页面
pages = html_render.list_pages() # ["index", "about"]
# 删除页面
html_render.delete_page("about")
```
## 访问
```
http://localhost:8080/ → index 页面
http://localhost:8080/about → about 页面
```
## 依赖
- web-toolkitWeb 服务
- plugin-storageHTML 存储

View File

@@ -0,0 +1,99 @@
"""HTML 渲染服务 - 通过 config.json 配置,统一文件入口"""
import json
from pathlib import Path
from oss.plugin.types import Plugin, register_plugin_type, Response
class HtmlRenderPlugin(Plugin):
"""HTML 渲染插件 - 渲染服务由 html-render 提供"""
def __init__(self):
self.http_api = None
self.storage = None # plugin-storage 入口
self.config = {}
self.root_dir = None # 解析后的网站根目录
def init(self, deps: dict = None):
"""初始化 - 读取 config.json 并解析网站根目录"""
self._load_config()
print(f"[html-render] 配置加载完成: root_dir={self.root_dir}")
def start(self):
"""启动 - 注册路由到 http-api共享配置给 web-toolkit"""
# 注册首页路由
if self.http_api and hasattr(self.http_api, 'router'):
self.http_api.router.get("/", self._serve_html)
print("[html-render] 已注册路由到 http-api")
else:
print("[html-render] http-api 未加载")
# 将配置共享给 web-toolkit通过 plugin-storage 的 DCIM 共享存储)
if self.storage:
shared = self.storage.get_shared()
shared.set_shared("html-render-config", {
"root_dir": str(self.root_dir),
"index_file": self.config.get("index_file", "index.html"),
"static_prefix": self.config.get("static_prefix", "/static"),
})
print("[html-render] 配置已共享到 DCIM")
def stop(self):
"""停止"""
pass
def set_http_api(self, instance):
"""设置 http-api 实例"""
self.http_api = instance
def set_plugin_storage(self, instance):
"""设置 plugin-storage 实例(唯一文件读写入口)"""
self.storage = instance
def _load_config(self):
"""读取 config.json解析根目录"""
config_path = Path("./data/html-render/config.json")
if not config_path.exists():
print("[html-render] 警告: config.json 不存在,使用默认配置")
self.config = {"root_dir": "../website", "index_file": "index.html"}
else:
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
# 解析根目录(相对于 config.json 的路径)
root_relative = self.config.get("root_dir", "../website")
self.root_dir = (config_path.parent / root_relative).resolve()
def _serve_html(self, request):
"""提供 HTML 页面 - 通过 plugin-storage 读取并注入静态资源路径"""
index_file = self.config.get("index_file", "index.html")
if self.storage:
storage = self.storage.get_storage("html-render")
if storage.file_exists(index_file):
content = storage.read_file(index_file)
if content:
# 注入静态资源路径(相对路径 → /website/ 前缀)
content = self._inject_static_paths(content)
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=content
)
return Response(status=404, body="Not Found")
def _inject_static_paths(self, html: str) -> str:
"""将相对静态资源路径替换为 /website/ 前缀"""
import re
# href="css/xxx" → href="/website/css/xxx"
html = re.sub(r'(href\s*=\s*["\'])css/', r'\1/website/css/', html)
# src="js/xxx" → src="/website/js/xxx"
html = re.sub(r'(src\s*=\s*["\'])js/', r'\1/website/js/', html)
# src="logo.svg" → src="/website/logo.svg"
html = re.sub(r'(src\s*=\s*["\'])(?!https?://|/)([\w.-]+\.(svg|png|jpg|gif|ico|webp))', r'\1/website/\2', html)
return html
register_plugin_type("HtmlRenderPlugin", HtmlRenderPlugin)
def New():
return HtmlRenderPlugin()

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"name": "html-render",
"version": "1.0.0",
"author": "Falck",
"description": "HTML 渲染服务 - 提供 8080 端口的 HTML 页面服务",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"html_dir": "./data/html-render"
}
},
"dependencies": ["http-api", "plugin-storage"],
"permissions": ["http-api", "plugin-storage"]
}

View File

@@ -0,0 +1,71 @@
# web-toolkit Web 工具包
提供静态文件服务、模板渲染、路由等 Web 开发工具。
## 功能
- **静态文件服务**:提供 HTML/CSS/JS/图片等静态文件
- **模板引擎**:支持变量替换、条件判断、循环
- **路由管理**:为 HTTP 和 TCP 服务器注册路由
- **自动首页**:自动查找 index.html
## 使用
```python
web = plugin_mgr.get("web-toolkit")
# 设置目录
web.set_static_dir("./public")
web.set_template_dir("./templates")
# 添加自定义路由
web.add_route("GET", "/api/hello", lambda req: {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"message": "Hello"}'
})
# 渲染模板
html = web.render_template("page.html", {"title": "My Page", "items": [1, 2, 3]})
```
## 模板语法
```html
<!-- 变量 -->
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<!-- 条件 -->
{% if show_content %}
<div>{{ content }}</div>
{% endif %}
<!-- 循环 -->
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8080,
"static_dir": "./static",
"template_dir": "./templates",
"index_files": ["index.html", "index.htm"]
}
}
}
```
## 依赖
- http-apiHTTP 服务
- http-tcpTCP HTTP 服务

View File

@@ -0,0 +1,158 @@
"""Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)"""
import json
from pathlib import Path
from oss.plugin.types import Plugin, register_plugin_type, Response
from .router import WebRouter
from .static import StaticFileHandler
from .template import TemplateEngine
class WebToolkitPlugin(Plugin):
"""Web 工具包插件 - 提供网站前端所有服务"""
def __init__(self):
self.router = None
self.static_handler = None
self.template_engine = None
self.http_api = None
self.http_tcp = None
self.storage = None
self.config = {} # 从 config.json 读取
self.root_dir = None
def init(self, deps: dict = None):
"""初始化 - 读取 config.json 配置"""
self.router = WebRouter()
self.template_engine = TemplateEngine()
self._load_config()
self.static_handler = StaticFileHandler(root=str(self.root_dir))
print(f"[web-toolkit] 配置加载完成: root_dir={self.root_dir}")
def start(self):
"""启动"""
# 注册路由到 http-api
if self.http_api:
http_instance = self.http_api
if hasattr(http_instance, "router"):
# 精确路由先注册,参数化路由后注册
http_instance.router.get(
self.config.get("website_prefix", "/website") + "/",
self._serve_website_index
)
http_instance.router.get(
self.config.get("website_prefix", "/website") + "/:path",
self._serve_static
)
http_instance.router.get(
self.config.get("static_prefix", "/static") + "/:path",
self._serve_static
)
# 注册路由到 http-tcp
if self.http_tcp:
tcp_instance = self.http_tcp
if hasattr(tcp_instance, "router"):
tcp_instance.router.get(
self.config.get("website_prefix", "/website") + "/",
self._serve_website_index
)
tcp_instance.router.get(
self.config.get("website_prefix", "/website") + "/:path",
self._serve_static
)
tcp_instance.router.get(
self.config.get("static_prefix", "/static") + "/:path",
self._serve_static
)
print("[web-toolkit] Web 工具包已启动")
def stop(self):
"""停止"""
pass
def set_http_api(self, instance):
"""设置 HTTP API 实例"""
self.http_api = instance
def set_http_tcp(self, instance):
"""设置 HTTP TCP 实例"""
self.http_tcp = instance
def set_plugin_storage(self, instance):
"""设置 plugin-storage 实例(唯一文件读写入口)"""
self.storage = instance
def set_static_dir(self, path: str):
"""设置静态文件目录"""
self.static_handler.set_root(path)
def set_template_dir(self, path: str):
"""设置模板目录"""
template_root = Path(path)
if template_root.exists():
self.template_engine.set_root(str(template_root))
def _load_config(self):
"""读取 config.json解析网站根目录"""
config_path = Path("./data/web-toolkit/config.json")
if not config_path.exists():
print("[web-toolkit] 警告: config.json 不存在,使用默认配置")
self.config = {
"root_dir": "../website",
"index_file": "index.html",
"static_prefix": "/static",
"website_prefix": "/website",
}
else:
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
# 解析根目录(相对于 config.json 的路径)
root_relative = self.config.get("root_dir", "../website")
self.root_dir = (config_path.parent / root_relative).resolve()
# 初始化模板引擎
template_dir = self.config.get("template_dir", "")
if template_dir:
template_path = self.root_dir / template_dir
if template_path.exists():
self.template_engine.set_root(str(template_path))
def _serve_website_index(self, request):
"""提供 website 目录首页"""
index_file = self.config.get("index_file", "index.html")
if self.root_dir:
path = self.root_dir / index_file
if path.exists():
content = path.read_text(encoding="utf-8")
return Response(
status=200,
headers={"Content-Type": "text/html; charset=utf-8"},
body=content
)
return Response(status=404, body="Index file not found")
def _serve_static(self, request):
"""提供静态文件"""
path = request.path
website_prefix = self.config.get("website_prefix", "/website")
static_prefix = self.config.get("static_prefix", "/static")
if path.startswith(website_prefix + "/"):
filename = path[len(website_prefix) + 1:]
elif path.startswith(static_prefix + "/"):
filename = path[len(static_prefix) + 1:]
else:
filename = path.lstrip("/")
if not filename:
return self._serve_website_index(request)
return self.static_handler.serve(filename)
register_plugin_type("WebToolkitPlugin", WebToolkitPlugin)
def New():
return WebToolkitPlugin()

View File

@@ -0,0 +1,21 @@
{
"metadata": {
"name": "web-toolkit",
"version": "1.0.0",
"author": "Falck",
"description": "Web 工具包 - 提供静态文件服务、模板渲染、路由等 Web 开发工具",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8080,
"static_dir": "./static",
"template_dir": "./templates",
"index_files": ["index.html", "index.htm"]
}
},
"dependencies": ["http-api", "http-tcp", "plugin-storage"],
"permissions": ["http-api", "http-tcp", "json-codec", "plugin-storage"]
}

View File

@@ -0,0 +1,63 @@
"""Web 路由器"""
from typing import Callable, Optional, Any
class WebRoute:
"""Web 路由"""
def __init__(self, method: str, path: str, handler: Callable):
self.method = method
self.path = path
self.handler = handler
class WebRouter:
"""Web 路由器"""
def __init__(self):
self.routes: list[WebRoute] = []
def add_route(self, method: str, path: str, handler: Callable):
"""添加路由"""
self.routes.append(WebRoute(method, path, handler))
def get(self, path: str, handler: Callable):
"""GET 路由"""
self.add_route("GET", path, handler)
def post(self, path: str, handler: Callable):
"""POST 路由"""
self.add_route("POST", path, handler)
def put(self, path: str, handler: Callable):
"""PUT 路由"""
self.add_route("PUT", path, handler)
def delete(self, path: str, handler: Callable):
"""DELETE 路由"""
self.add_route("DELETE", path, handler)
def handle(self, request: dict) -> Optional[Any]:
"""处理请求"""
method = request.get("method", "GET")
path = request.get("path", "/")
for route in self.routes:
if route.method == method and self._match(route.path, path):
return route.handler(request)
return None
def _match(self, pattern: str, path: str) -> bool:
"""路径匹配"""
if pattern == path:
return True
if ":" in pattern:
pattern_parts = pattern.strip("/").split("/")
path_parts = path.strip("/").split("/")
if len(pattern_parts) != len(path_parts):
return False
for p, a in zip(pattern_parts, path_parts):
if not p.startswith(":") and p != a:
return False
return True
return False

View File

@@ -0,0 +1,69 @@
"""静态文件处理器"""
import os
import mimetypes
from pathlib import Path
from typing import Optional, Any
from oss.plugin.types import Response
class StaticFileHandler:
"""静态文件处理器"""
def __init__(self, root: str = "./static"):
self.root = root
self._ensure_root()
def _ensure_root(self):
"""确保静态目录存在"""
Path(self.root).mkdir(parents=True, exist_ok=True)
def set_root(self, path: str):
"""设置静态文件根目录"""
self.root = path
self._ensure_root()
def serve(self, filename: str) -> Optional[Response]:
"""提供静态文件"""
file_path = Path(self.root) / filename
# 安全检查:防止目录遍历
try:
file_path.resolve().relative_to(Path(self.root).resolve())
except ValueError:
return Response(status=403, body="Forbidden")
if not file_path.exists() or not file_path.is_file():
return Response(status=404, body="File not found")
# 检测 MIME 类型
content_type, _ = mimetypes.guess_type(str(file_path))
if not content_type:
content_type = "application/octet-stream"
# 读取文件内容
try:
if content_type.startswith("text/") or content_type in (
"application/json", "application/javascript", "application/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 reading file: {e}")
def list_files(self) -> list[str]:
"""列出静态文件"""
root_path = Path(self.root)
if not root_path.exists():
return []
return [f.name for f in root_path.iterdir() if f.is_file()]

View File

@@ -0,0 +1,144 @@
"""模板引擎"""
import re
import ast
from pathlib import Path
from typing import Any, Optional
class TemplateEngine:
"""简单模板引擎"""
def __init__(self, root: str = "./templates"):
self.root = root
self._cache: dict[str, str] = {}
self._ensure_root()
def _ensure_root(self):
"""确保模板目录存在"""
Path(self.root).mkdir(parents=True, exist_ok=True)
def set_root(self, path: str):
"""设置模板根目录"""
self.root = path
self._ensure_root()
self._cache.clear()
def render(self, name: str, context: dict[str, Any]) -> str:
"""渲染模板"""
template = self._load_template(name)
return self._render_template(template, context)
def _load_template(self, name: str) -> str:
"""加载模板"""
if name in self._cache:
return self._cache[name]
template_path = Path(self.root) / name
if not template_path.exists():
raise FileNotFoundError(f"模板不存在: {name}")
content = template_path.read_text(encoding="utf-8")
self._cache[name] = content
return content
def _safe_eval(self, expression: str, context: dict) -> Any:
"""安全评估表达式(仅允许简单的属性访问和比较)"""
# 只允许访问 context 中的变量
# 支持的运算符: and, or, not, ==, !=, <, >, <=, >=, in
# 不允许函数调用、导入、属性访问等
# 使用 AST 解析并验证
try:
tree = ast.parse(expression, mode='eval')
except SyntaxError:
return False
# 验证 AST 节点
if not self._validate_ast(tree.body[0].value, set(context.keys())):
return False
# 在受限环境中评估
try:
return eval(expression, {"__builtins__": {}}, context)
except Exception:
return False
def _validate_ast(self, node: ast.AST, allowed_names: set) -> bool:
"""验证 AST 只包含安全的操作"""
if isinstance(node, ast.Name):
return node.id in allowed_names or node.id in ('True', 'False', 'None')
elif isinstance(node, ast.Constant):
return True
elif isinstance(node, ast.BoolOp):
return all(self._validate_ast(v, allowed_names) for v in node.values)
elif isinstance(node, ast.Compare):
return (self._validate_ast(node.left, allowed_names) and
all(self._validate_ast(c, allowed_names) for c in node.comparators))
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
return self._validate_ast(node.operand, allowed_names)
elif isinstance(node, ast.Attribute):
# 不允许属性访问(防止绕过安全限制)
return False
elif isinstance(node, ast.Call):
# 不允许函数调用
return False
elif isinstance(node, ast.Subscript):
# 允许简单的索引访问
return (self._validate_ast(node.value, allowed_names) and
self._validate_ast(node.slice, allowed_names))
return False
def _render_template(self, template: str, context: dict[str, Any]) -> str:
"""渲染模板内容"""
# 替换 {{ variable }}
def replace_var(match):
var_name = match.group(1).strip()
value = context.get(var_name, "")
if isinstance(value, (dict, list)):
import json
return json.dumps(value, ensure_ascii=False)
return str(value)
result = re.sub(r'\{\{(.*?)\}\}', replace_var, template)
# 处理 {% if condition %} ... {% endif %}
result = self._process_if(result, context)
# 处理 {% for item in list %} ... {% endfor %}
result = self._process_for(result, context)
return result
def _process_if(self, template: str, context: dict) -> str:
"""处理 if 条件"""
pattern = r'\{%\s*if\s+(.*?)\s*%\}(.*?){%\s*endif\s*%\}'
def replace_if(match):
condition = match.group(1).strip()
content = match.group(2)
# 安全条件评估
value = self._safe_eval(condition, context)
return content if value else ""
return re.sub(pattern, replace_if, template, flags=re.DOTALL)
def _process_for(self, template: str, context: dict) -> str:
"""处理 for 循环"""
pattern = r'\{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%\}(.*?){%\s*endfor\s*%\}'
def replace_for(match):
item_name = match.group(1)
list_name = match.group(2)
content = match.group(3)
items = context.get(list_name, [])
if not isinstance(items, list):
return ""
result = ""
for item in items:
loop_context = {**context, item_name: item}
result += self._render_template(content, loop_context)
return result
return re.sub(pattern, replace_for, template, flags=re.DOTALL)

View File

@@ -0,0 +1,30 @@
# circuit-breaker 熔断器
为插件提供熔断能力,防止级联失败。
## 功能
- 失败计数熔断
- 状态:`closed``open``half-open`
- 可配置失败阈值
- 自动恢复机制
## 状态机
```
closed (正常) → open (熔断) → half-open (半开) → closed (恢复)
```
## 使用
```python
# 检查是否有熔断能力
if "circuit_breaker" in capabilities:
breaker = extensions["_circuit_breaker_provider"]
cb = breaker.create("my-plugin", threshold=5)
try:
result = cb.call(risky_function, arg1, arg2)
except Exception:
print("熔断器已触发")
```

View File

@@ -0,0 +1,70 @@
"""熔断插件 - 为插件提供熔断能力"""
from oss.plugin.types import Plugin, register_plugin_type
class CircuitBreakerProvider:
"""熔断能力提供者"""
def __init__(self):
self.breakers: dict[str, "CircuitBreaker"] = {}
def create(self, name: str, threshold: int = 5) -> "CircuitBreaker":
breaker = CircuitBreaker(name, threshold)
self.breakers[name] = breaker
return breaker
def get(self, name: str):
return self.breakers.get(name)
class CircuitBreaker:
"""熔断器"""
def __init__(self, name: str, threshold: int = 5):
self.name = name
self.threshold = threshold
self.failures = 0
self.state = "closed" # closed, open, half-open
def call(self, func, *args, **kwargs):
if self.state == "open":
raise Exception(f"熔断器 '{self.name}' 已打开")
try:
result = func(*args, **kwargs)
self.failures = 0
self.state = "closed"
return result
except Exception as e:
self.failures += 1
if self.failures >= self.threshold:
self.state = "open"
raise e
class CircuitBreakerPlugin(Plugin):
"""熔断插件"""
def __init__(self):
self.provider = CircuitBreakerProvider()
def init(self, deps: dict = None):
pass
def start(self):
pass
def stop(self):
pass
def get_provider(self):
return self.provider
# 注册类型
register_plugin_type("CircuitBreakerProvider", CircuitBreakerProvider)
register_plugin_type("CircuitBreaker", CircuitBreaker)
def New():
return CircuitBreakerPlugin()

View File

@@ -0,0 +1,17 @@
{
"metadata": {
"name": "circuit-breaker",
"version": "1.0.0",
"author": "FutureOSS",
"description": "熔断器 - 为插件提供熔断能力",
"type": "extension"
},
"config": {
"enabled": true,
"args": {
"default_threshold": 5
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,39 @@
# dependency 依赖解析
插件依赖关系管理,使用拓扑排序确定加载顺序。
## 功能
- 拓扑排序Kahn 算法)
- 循环依赖检测DFS
- 缺失依赖检测
- 自动按依赖顺序加载插件
## 使用
```python
dep = dependency_plugin
# 添加插件及其依赖
dep.add_plugin("plugin-a", ["plugin-b", "plugin-c"])
dep.add_plugin("plugin-b", [])
dep.add_plugin("plugin-c", ["plugin-b"])
# 解析依赖顺序
order = dep.resolve() # 返回 ["plugin-b", "plugin-c", "plugin-a"]
# 检查缺失依赖
missing = dep.get_missing_deps()
# 获取加载顺序
order = dep.get_order()
```
## manifest.json 声明
```json
{
"metadata": {...},
"dependencies": ["lifecycle", "circuit-breaker"]
}
```

View File

@@ -0,0 +1,138 @@
"""依赖解析插件 - 拓扑排序 + 循环依赖检测"""
from typing import Any, Optional
from oss.plugin.types import Plugin, register_plugin_type
class DependencyError(Exception):
"""依赖错误"""
pass
class DependencyResolver:
"""依赖解析器"""
def __init__(self):
self.graph: dict[str, list[str]] = {} # 插件名 -> 依赖列表
def add_plugin(self, name: str, dependencies: list[str]):
"""添加插件及其依赖"""
self.graph[name] = dependencies
def resolve(self) -> list[str]:
"""解析依赖,返回拓扑排序后的插件列表
例如A 依赖 BB 依赖 C
图: A -> [B], B -> [C], C -> []
结果: [C, B, A] (先启动没有依赖的,再启动依赖它们的)
"""
# 检测循环依赖
self._detect_cycles()
# 拓扑排序 (Kahn 算法 - 反向)
# in_degree[name] = name 依赖的插件数量
in_degree: dict[str, int] = {name: 0 for name in self.graph}
# 反向图: who_depends_on[dep] = [name1, name2, ...] (谁依赖 dep)
who_depends_on: dict[str, list[str]] = {name: [] for name in self.graph}
for name, deps in self.graph.items():
for dep in deps:
if dep in in_degree:
in_degree[name] += 1 # name 依赖 dep所以 name 的入度 +1
who_depends_on[dep].append(name) # dep 被 name 依赖
# 从没有依赖的插件开始
queue = [name for name, degree in in_degree.items() if degree == 0]
result = []
while queue:
node = queue.pop(0)
result.append(node)
# node 已启动,减少依赖它的插件的入度
for dependent in who_depends_on.get(node, []):
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
if len(result) != len(self.graph):
raise DependencyError("无法解析依赖,可能存在循环依赖")
return result
def _detect_cycles(self):
"""检测循环依赖"""
visited = set()
rec_stack = set()
def dfs(node: str) -> bool:
visited.add(node)
rec_stack.add(node)
for dep in self.graph.get(node, []):
if dep not in visited:
if dfs(dep):
return True
elif dep in rec_stack:
raise DependencyError(f"检测到循环依赖: {node} -> {dep}")
rec_stack.remove(node)
return False
for node in self.graph:
if node not in visited:
if dfs(node):
raise DependencyError(f"检测到循环依赖涉及: {node}")
def get_missing(self) -> list[str]:
"""获取缺失的依赖"""
all_deps = set()
for deps in self.graph.values():
all_deps.update(deps)
all_plugins = set(self.graph.keys())
return list(all_deps - all_plugins)
class DependencyPlugin(Plugin):
"""依赖解析插件"""
def __init__(self):
self.resolver = DependencyResolver()
self.plugin_deps: dict[str, list[str]] = {}
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动"""
pass
def stop(self):
"""停止"""
pass
def add_plugin(self, name: str, dependencies: list[str]):
"""添加插件及其依赖"""
self.plugin_deps[name] = dependencies
self.resolver.add_plugin(name, dependencies)
def resolve(self) -> list[str]:
"""解析依赖顺序"""
return self.resolver.resolve()
def get_missing_deps(self) -> list[str]:
"""获取缺失的依赖"""
return self.resolver.get_missing()
def get_order(self) -> list[str]:
"""获取插件加载顺序"""
return self.resolve()
# 注册类型
register_plugin_type("DependencyResolver", DependencyResolver)
register_plugin_type("DependencyError", DependencyError)
def New():
return DependencyPlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "dependency",
"version": "1.0.0",
"author": "FutureOSS",
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,32 @@
# hot-reload 热插拔
运行时加载、卸载、更新插件,无需重启服务。
## 功能
- 运行时加载新插件
- 运行时卸载插件
- 运行时更新插件(热重载)
- 自动监听文件变化(可选)
- 模块缓存清理
## 使用
```python
from pathlib import Path
# 加载新插件
hot_reload.load_plugin(Path("store/@{Author/new-plugin"))
# 卸载插件
hot_reload.unload_plugin("plugin-name")
# 更新插件
hot_reload.reload_plugin("plugin-name", Path("store/@{Author/plugin-name"))
```
## 注意事项
- 插件必须实现 `init()`, `start()`, `stop()`
- 卸载时会调用 `stop()`
- 更新时先 `stop()``init()` + `start()`

View File

@@ -0,0 +1,196 @@
"""热插拔插件 - 运行时加载/卸载/更新插件"""
import sys
import time
import threading
from pathlib import Path
from typing import Any, Optional, Callable
from oss.plugin.types import Plugin, register_plugin_type
class HotReloadError(Exception):
"""热插拔错误"""
pass
class FileWatcher:
"""文件监听器"""
def __init__(self, watch_dirs: list[str], extensions: list[str], on_change: Callable):
self.watch_dirs = [Path(d) for d in watch_dirs]
self.extensions = extensions
self.on_change = on_change
self._running = False
self._thread: Optional[threading.Thread] = None
self._file_times: dict[str, float] = {}
self._scan_files()
def _scan_files(self):
"""扫描当前文件及其修改时间"""
for watch_dir in self.watch_dirs:
if watch_dir.exists():
for f in watch_dir.rglob("*"):
if f.is_file() and f.suffix in self.extensions:
self._file_times[str(f)] = f.stat().st_mtime
def start(self):
"""开始监听"""
self._running = True
self._thread = threading.Thread(target=self._watch_loop, daemon=True)
self._thread.start()
def stop(self):
"""停止监听"""
self._running = False
if self._thread:
self._thread.join(timeout=5)
def _watch_loop(self):
"""监听循环"""
while self._running:
changed = []
current_files = {}
for watch_dir in self.watch_dirs:
if watch_dir.exists():
for f in watch_dir.rglob("*"):
if f.is_file() and f.suffix in self.extensions:
fpath = str(f)
mtime = f.stat().st_mtime
current_files[fpath] = mtime
# 新文件或修改过
if fpath not in self._file_times:
changed.append(("new", f))
elif mtime > self._file_times[fpath]:
changed.append(("modified", f))
# 检查删除的文件
for fpath in self._file_times:
if fpath not in current_files:
changed.append(("deleted", Path(fpath)))
if changed:
self._file_times = current_files
self.on_change(changed)
time.sleep(1)
class HotReloadPlugin(Plugin):
"""热插拔插件"""
def __init__(self):
self.plugin_loader_instance = None
self.watcher: Optional[FileWatcher] = None
self.watch_dirs: list[str] = []
self.watch_extensions: list[str] = [".py", ".json"]
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动 - 自动开始监听默认目录"""
if not self.watch_dirs:
# 默认监听 store 目录
self.watch_dirs = ["store"]
self.start_watching()
def stop(self):
"""停止"""
if self.watcher:
self.watcher.stop()
def set_plugin_loader(self, plugin_loader):
"""设置插件加载器实例"""
self.plugin_loader_instance = plugin_loader
def set_watch_dirs(self, dirs: list[str]):
"""设置监听目录"""
self.watch_dirs = dirs
def start_watching(self):
"""开始监听文件变化"""
if self.watch_dirs and self.plugin_loader_instance:
self.watcher = FileWatcher(
self.watch_dirs,
self.watch_extensions,
self._on_file_change
)
self.watcher.start()
def _on_file_change(self, changes: list[tuple[str, Path]]):
"""文件变化回调"""
for change_type, fpath in changes:
# 只关心 main.py 和 manifest.json 的变化
if fpath.name not in ("main.py", "manifest.json"):
continue
plugin_dir = fpath.parent
plugin_name = plugin_dir.name
try:
if change_type == "new":
self.load_plugin(plugin_dir)
elif change_type == "modified":
self.reload_plugin(plugin_name, plugin_dir)
elif change_type == "deleted":
self.unload_plugin(plugin_name)
except Exception as e:
print(f"[hot-reload] 处理变化失败: {e}")
def load_plugin(self, plugin_dir: Path) -> bool:
"""运行时加载插件"""
try:
plugin_name = plugin_dir.name
if plugin_name in self.plugin_loader_instance.plugins:
raise HotReloadError(f"插件已存在: {plugin_name}")
self.plugin_loader_instance.load(plugin_dir)
info = self.plugin_loader_instance.plugins[plugin_name]
instance = info["instance"]
instance.init()
instance.start()
return True
except Exception as e:
raise HotReloadError(f"加载插件失败: {e}")
def unload_plugin(self, plugin_name: str) -> bool:
"""运行时卸载插件"""
try:
if plugin_name not in self.plugin_loader_instance.plugins:
raise HotReloadError(f"插件不存在: {plugin_name}")
info = self.plugin_loader_instance.plugins[plugin_name]
instance = info["instance"]
instance.stop()
# 从模块缓存中移除
module = info.get("module")
if module and module.__name__ in sys.modules:
del sys.modules[module.__name__]
del self.plugin_loader_instance.plugins[plugin_name]
return True
except Exception as e:
raise HotReloadError(f"卸载插件失败: {e}")
def reload_plugin(self, plugin_name: str, plugin_dir: Path) -> bool:
"""运行时更新插件"""
try:
# 先卸载
self.unload_plugin(plugin_name)
# 再加载
return self.load_plugin(plugin_dir)
except Exception as e:
raise HotReloadError(f"更新插件失败: {e}")
# 注册类型
register_plugin_type("HotReloadError", HotReloadError)
register_plugin_type("FileWatcher", FileWatcher)
def New():
return HotReloadPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "hot-reload",
"version": "1.0.0",
"author": "FutureOSS",
"description": "热插拔 - 运行时加载/卸载/更新插件",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"watch_dirs": ["store", "./data/pkg"],
"watch_extensions": [".py", ".json"]
}
},
"dependencies": [],
"permissions": ["plugin-loader"]
}

View File

@@ -0,0 +1,53 @@
# http-api HTTP API 服务
提供 HTTP RESTful API 服务,支持路由、中间件等功能。
## 功能
- HTTP 服务器GET/POST/PUT/DELETE
- 路由匹配(支持参数路由 `:id`
- 中间件链CORS/日志/限流)
- 分散式布局(每个文件 < 200 行)
## 路由使用
```python
# 在插件中获取 router
http_plugin = plugin_mgr.get("http-api")
router = http_plugin.router
# 添加路由
router.get("/health", lambda req: Response(status=200, body='{"status": "ok"}'))
router.get("/api/users", handle_users)
router.post("/api/users", handle_create_user)
router.get("/api/users/:id", handle_user_by_id)
```
## 中间件
```python
middleware = http_plugin.middleware
# 添加自定义中间件
class MyMiddleware(Middleware):
def process(self, ctx, next_fn):
# 前置处理
resp = next_fn() # 继续执行
# 后置处理
return resp
middleware.add(MyMiddleware())
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8080
}
}
}
```

View File

@@ -0,0 +1,58 @@
"""HTTP 事件系统 - 请求/响应生命周期事件"""
from typing import Callable, Any, Optional
from dataclasses import dataclass, field
@dataclass
class HttpEvent:
"""HTTP 事件"""
type: str # request, response, error, etc
request: Any = None
response: Any = None
error: Exception = None
context: dict[str, Any] = field(default_factory=dict)
class HttpEventBus:
"""HTTP 事件总线"""
def __init__(self):
self._handlers: dict[str, list[Callable]] = {}
def on(self, event_type: str, handler: Callable):
"""订阅事件"""
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def off(self, event_type: str, handler: Callable):
"""取消订阅"""
if event_type in self._handlers:
try:
self._handlers[event_type].remove(handler)
except ValueError:
pass
def emit(self, event: HttpEvent):
"""发布事件"""
handlers = self._handlers.get(event.type, [])
for handler in handlers:
try:
handler(event)
except Exception:
pass
def clear(self):
"""清空所有订阅"""
self._handlers.clear()
# 事件类型常量
EVENT_REQUEST = "http.request"
EVENT_BEFORE_ROUTE = "http.before_route"
EVENT_AFTER_ROUTE = "http.after_route"
EVENT_BEFORE_HANDLER = "http.before_handler"
EVENT_AFTER_HANDLER = "http.after_handler"
EVENT_RESPONSE = "http.response"
EVENT_ERROR = "http.error"
EVENT_COMPLETE = "http.complete"

View File

@@ -0,0 +1,68 @@
"""HTTP API 插件 - 分散式布局"""
import json
from oss.plugin.types import Plugin, register_plugin_type
from .server import HttpServer, Response
from .router import Router
from .middleware import MiddlewareChain
class HttpApiPlugin(Plugin):
"""HTTP API 插件"""
def __init__(self):
self.server = None
self.router = Router()
self.middleware = MiddlewareChain()
def init(self, deps: dict = None):
"""初始化"""
# 注册基础路由
self.router.get("/health", self._health_handler)
self.router.get("/api/server/info", self._server_info_handler)
self.router.get("/api/status", self._status_handler)
self.server = HttpServer(self.router, self.middleware)
def start(self):
"""启动"""
self.server.start()
def stop(self):
"""停止"""
if self.server:
self.server.stop()
def _health_handler(self, request):
"""健康检查"""
return Response(
status=200,
body=json.dumps({"status": "ok", "service": "http-api"}),
headers={"Content-Type": "application/json"}
)
def _server_info_handler(self, request):
"""服务器信息"""
return Response(
status=200,
body=json.dumps({
"name": "FutureOSS HTTP API",
"version": "1.0.0",
"endpoints": ["/health", "/api/server/info", "/api/status"]
}),
headers={"Content-Type": "application/json"}
)
def _status_handler(self, request):
"""状态检查"""
return Response(
status=200,
body=json.dumps({"status": "running", "plugins_loaded": True}),
headers={"Content-Type": "application/json"}
)
register_plugin_type("HttpApiPlugin", HttpApiPlugin)
def New():
return HttpApiPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "http-api",
"version": "1.0.0",
"author": "FutureOSS",
"description": "HTTP API 服务 - 提供 RESTful API 和路由功能",
"type": "protocol"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8080
}
},
"dependencies": [],
"permissions": ["lifecycle", "circuit-breaker"]
}

View File

@@ -0,0 +1,57 @@
"""中间件链 - CORS/日志/限流等"""
from typing import Callable, Optional, Any
from .server import Request, Response
class Middleware:
"""中间件基类"""
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
"""处理请求"""
return None
class CorsMiddleware(Middleware):
"""CORS 中间件"""
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
ctx["response_headers"] = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}
return None
class LoggerMiddleware(Middleware):
"""日志中间件"""
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
req = ctx.get("request")
if req:
print(f"[http-api] {req.method} {req.path}")
return None
class MiddlewareChain:
"""中间件链"""
def __init__(self):
self.middlewares: list[Middleware] = []
self.add(LoggerMiddleware())
self.add(CorsMiddleware())
def add(self, middleware: Middleware):
"""添加中间件"""
self.middlewares.append(middleware)
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
"""执行中间件链"""
idx = 0
def next_fn():
nonlocal idx
if idx < len(self.middlewares):
mw = self.middlewares[idx]
idx += 1
return mw.process(ctx, next_fn)
return None
return next_fn()

View File

@@ -0,0 +1,72 @@
"""路由器 - 路径匹配和处理器分发"""
from typing import Callable, Optional
from .server import Request, Response
class Route:
"""路由定义"""
def __init__(self, method: str, path: str, handler: Callable):
self.method = method
self.path = path
self.handler = handler
class Router:
"""路由器"""
def __init__(self):
self.routes: list[Route] = []
def add(self, method: str, path: str, handler: Callable):
"""添加路由"""
self.routes.append(Route(method, path, handler))
def get(self, path: str, handler: Callable):
"""GET 路由"""
self.add("GET", path, handler)
def post(self, path: str, handler: Callable):
"""POST 路由"""
self.add("POST", path, handler)
def put(self, path: str, handler: Callable):
"""PUT 路由"""
self.add("PUT", path, handler)
def delete(self, path: str, handler: Callable):
"""DELETE 路由"""
self.add("DELETE", path, handler)
def handle(self, request: Request) -> Response:
"""处理请求"""
for route in self.routes:
if route.method == request.method and self._match(route.path, request.path):
return route.handler(request)
return Response(status=404, body='{"error": "Not Found"}')
def _match(self, pattern: str, path: str) -> bool:
"""路径匹配"""
if pattern == path:
return True
if ":" in pattern:
pattern_parts = pattern.strip("/").split("/")
path_parts = path.strip("/").split("/")
# 检查前缀是否匹配
for i, p in enumerate(pattern_parts):
if i >= len(path_parts):
return False
if not p.startswith(":") and p != path_parts[i]:
return False
# 如果最后一个 pattern 是 :path通配符允许更多路径段
last_pattern = pattern_parts[-1]
if last_pattern.startswith(":") and len(path_parts) >= len(pattern_parts):
return True
# 否则必须精确匹配段数
if len(pattern_parts) != len(path_parts):
return False
return True
return False

View File

@@ -0,0 +1,110 @@
"""HTTP 服务器核心"""
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import Any
class Request:
"""请求对象"""
def __init__(self, method, path, headers, body):
self.method = method
self.path = path
self.headers = headers
self.body = body
class Response:
"""响应对象"""
def __init__(self, status=200, headers=None, body=""):
self.status = status
self.headers = headers or {}
self.body = body
class HttpServer:
"""HTTP 服务器"""
def __init__(self, router, middleware, host="0.0.0.0", port=8080):
self.host = host
self.port = port
self.router = router
self.middleware = middleware
self._server = None
self._thread = None
def start(self):
"""启动服务器"""
handler = self._create_handler()
self._server = HTTPServer((self.host, self.port), handler)
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
self._thread.start()
print(f"[http-api] 服务器启动: {self.host}:{self.port}")
def stop(self):
"""停止服务器"""
if self._server:
self._server.shutdown()
print("[http-api] 服务器已停止")
def _create_handler(self):
"""创建请求处理器"""
router = self.router
middleware = self.middleware
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self._handle("GET")
def do_POST(self):
self._handle("POST")
def do_PUT(self):
self._handle("PUT")
def do_DELETE(self):
self._handle("DELETE")
def do_OPTIONS(self):
"""处理 CORS 预检请求"""
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def _handle(self, method):
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length) if content_length else b""
req = Request(
method=method,
path=self.path,
headers=dict(self.headers),
body=body.decode("utf-8")
)
# 执行中间件
ctx = {"request": req, "response": None}
result = middleware.run(ctx)
if result:
self._send_response(result)
return
# 路由匹配
resp = router.handle(req)
self._send_response(resp)
def _send_response(self, resp: Response):
self.send_response(resp.status)
for k, v in resp.headers.items():
self.send_header(k, v)
self.end_headers()
if isinstance(resp.body, str):
self.wfile.write(resp.body.encode("utf-8"))
else:
self.wfile.write(resp.body)
def log_message(self, format, *args):
pass
return Handler

View File

@@ -0,0 +1,51 @@
# http-tcp HTTP TCP 服务
提供基于 TCP 的 HTTP 协议实现。
## 功能
- 原始 TCP HTTP 服务器
- 路由匹配
- 中间件链(日志/CORS
- 连接管理
- 事件发布(通过 plugin-bridge
## 使用
```python
tcp = plugin_mgr.get("http-tcp")
# 注册路由
tcp.router.get("/api/status", lambda req: {
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": '{"status": "ok"}'
})
# 获取客户端
clients = tcp.server.get_clients()
```
## 事件
```python
bridge = plugin_mgr.get("plugin-bridge")
bus = bridge.event_bus
bus.on("tcp.connect", lambda e: print(f"连接: {e.client.id}"))
bus.on("tcp.http.request", lambda e: print(f"请求: {e.context['request']['path']}"))
bus.on("tcp.disconnect", lambda e: print(f"断开: {e.client.id}"))
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8082
}
}
}
```

View File

@@ -0,0 +1,21 @@
"""HTTP TCP 事件定义"""
from dataclasses import dataclass, field
from typing import Any
@dataclass
class TcpEvent:
"""TCP 事件"""
type: str
client: Any = None
data: bytes = b""
context: dict[str, Any] = field(default_factory=dict)
# 事件类型常量
EVENT_CONNECT = "tcp.connect"
EVENT_DISCONNECT = "tcp.disconnect"
EVENT_DATA = "tcp.data"
EVENT_REQUEST = "tcp.http.request"
EVENT_RESPONSE = "tcp.http.response"
EVENT_ERROR = "tcp.error"

View File

@@ -0,0 +1,34 @@
"""HTTP TCP 插件入口"""
from oss.plugin.types import Plugin, register_plugin_type
from .server import TcpHttpServer
from .router import TcpRouter
from .middleware import TcpMiddlewareChain
class HttpTcpPlugin(Plugin):
"""HTTP TCP 插件"""
def __init__(self):
self.server = None
self.router = TcpRouter()
self.middleware = TcpMiddlewareChain()
def init(self, deps: dict = None):
"""初始化"""
self.server = TcpHttpServer(self.router, self.middleware)
def start(self):
"""启动"""
self.server.start()
def stop(self):
"""停止"""
if self.server:
self.server.stop()
register_plugin_type("HttpTcpPlugin", HttpTcpPlugin)
def New():
return HttpTcpPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "http-tcp",
"version": "1.0.0",
"author": "FutureOSS",
"description": "HTTP TCP 服务 - 基于 TCP 的 HTTP 协议实现",
"type": "protocol"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8082
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,53 @@
"""TCP HTTP 中间件链"""
from typing import Callable, Optional, Any
class TcpMiddleware:
"""TCP 中间件基类"""
def process(self, request: dict, next_fn: Callable) -> Optional[dict]:
"""处理请求"""
return next_fn()
class TcpLogMiddleware(TcpMiddleware):
"""日志中间件"""
def process(self, request, next_fn):
print(f"[http-tcp] {request.get('method')} {request.get('path')}")
return next_fn()
class TcpCorsMiddleware(TcpMiddleware):
"""CORS 中间件"""
def process(self, request, next_fn):
response = next_fn()
if response:
response.setdefault("headers", {})
response["headers"]["Access-Control-Allow-Origin"] = "*"
return response
class TcpMiddlewareChain:
"""TCP 中间件链"""
def __init__(self):
self.middlewares: list[TcpMiddleware] = []
self.add(TcpLogMiddleware())
self.add(TcpCorsMiddleware())
def add(self, middleware: TcpMiddleware):
"""添加中间件"""
self.middlewares.append(middleware)
def run(self, request: dict) -> Optional[dict]:
"""执行中间件链"""
idx = 0
def next_fn():
nonlocal idx
if idx < len(self.middlewares):
mw = self.middlewares[idx]
idx += 1
return mw.process(request, next_fn)
return None
return next_fn()

View File

@@ -0,0 +1,63 @@
"""TCP HTTP 路由器"""
from typing import Callable, Optional, Any
class TcpRoute:
"""TCP HTTP 路由"""
def __init__(self, method: str, path: str, handler: Callable):
self.method = method
self.path = path
self.handler = handler
class TcpRouter:
"""TCP HTTP 路由器"""
def __init__(self):
self.routes: list[TcpRoute] = []
def add(self, method: str, path: str, handler: Callable):
"""添加路由"""
self.routes.append(TcpRoute(method, path, handler))
def get(self, path: str, handler: Callable):
"""GET 路由"""
self.add("GET", path, handler)
def post(self, path: str, handler: Callable):
"""POST 路由"""
self.add("POST", path, handler)
def put(self, path: str, handler: Callable):
"""PUT 路由"""
self.add("PUT", path, handler)
def delete(self, path: str, handler: Callable):
"""DELETE 路由"""
self.add("DELETE", path, handler)
def handle(self, request: dict) -> dict:
"""处理请求"""
method = request.get("method", "GET")
path = request.get("path", "/")
for route in self.routes:
if route.method == method and self._match(route.path, path):
return route.handler(request)
return {"status": 404, "headers": {}, "body": "Not Found"}
def _match(self, pattern: str, path: str) -> bool:
"""路径匹配"""
if pattern == path:
return True
if ":" in pattern:
pattern_parts = pattern.strip("/").split("/")
path_parts = path.strip("/").split("/")
if len(pattern_parts) != len(path_parts):
return False
for p, a in zip(pattern_parts, path_parts):
if not p.startswith(":") and p != a:
return False
return True
return False

View File

@@ -0,0 +1,193 @@
"""TCP HTTP 服务器核心"""
import socket
import threading
import re
from typing import Any, Callable, Optional
from .events import TcpEvent, EVENT_CONNECT, EVENT_DISCONNECT, EVENT_DATA, EVENT_REQUEST, EVENT_RESPONSE
class TcpClient:
"""TCP 客户端连接"""
def __init__(self, conn: socket.socket, address: tuple):
self.conn = conn
self.address = address
self.id = f"{address[0]}:{address[1]}"
def send(self, data: bytes):
"""发送数据"""
self.conn.sendall(data)
def close(self):
"""关闭连接"""
self.conn.close()
class TcpHttpServer:
"""TCP HTTP 服务器"""
def __init__(self, router, middleware, event_bus=None, host="0.0.0.0", port=8082):
self.host = host
self.port = port
self.router = router
self.middleware = middleware
self.event_bus = event_bus
self._server = None
self._thread = None
self._running = False
self._clients: dict[str, TcpClient] = {}
def start(self):
"""启动服务器"""
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._server.bind((self.host, self.port))
self._server.listen(128)
self._running = True
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
self._thread.start()
print(f"[http-tcp] 服务器启动: {self.host}:{self.port}")
def _accept_loop(self):
"""接受连接循环"""
while self._running:
try:
conn, address = self._server.accept()
client = TcpClient(conn, address)
self._clients[client.id] = client
# 触发连接事件
if self.event_bus:
self.event_bus.emit(TcpEvent(type=EVENT_CONNECT, client=client))
# 启动处理线程
t = threading.Thread(target=self._handle_client, args=(client,), daemon=True)
t.start()
except Exception as e:
if self._running:
print(f"[http-tcp] 接受连接失败: {e}")
def _handle_client(self, client: TcpClient):
"""处理客户端请求"""
buffer = b""
try:
while self._running:
data = client.conn.recv(4096)
if not data:
break
buffer += data
# 检查 HTTP 请求是否完整
if b"\r\n\r\n" in buffer:
request = self._parse_request(buffer)
if request:
# 触发请求事件
if self.event_bus:
self.event_bus.emit(TcpEvent(
type=EVENT_REQUEST,
client=client,
context={"request": request}
))
# 路由处理
response = self.router.handle(request)
# 发送响应
response_bytes = self._format_response(response)
client.send(response_bytes)
# 触发响应事件
if self.event_bus:
self.event_bus.emit(TcpEvent(
type=EVENT_RESPONSE,
client=client,
data=response_bytes
))
buffer = b""
except Exception as e:
if self.event_bus:
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": str(e)}))
finally:
del self._clients[client.id]
client.close()
if self.event_bus:
self.event_bus.emit(TcpEvent(type=EVENT_DISCONNECT, client=client))
def _parse_request(self, data: bytes) -> Optional[dict]:
"""解析 HTTP 请求"""
try:
text = data.decode("utf-8", errors="replace")
lines = text.split("\r\n")
if not lines:
return None
# 解析请求行
match = re.match(r'(\w+)\s+(\S+)\s+HTTP/(\d\.\d)', lines[0])
if not match:
return None
method, path, version = match.groups()
# 解析头
headers = {}
body_start = 0
for i, line in enumerate(lines[1:], 1):
if line == "":
body_start = i + 1
break
if ":" in line:
key, value = line.split(":", 1)
headers[key.strip()] = value.strip()
# 解析体
content_length = int(headers.get("Content-Length", 0))
body = "\r\n".join(lines[body_start:]) if body_start else ""
return {
"method": method,
"path": path,
"version": version,
"headers": headers,
"body": body,
}
except Exception:
return None
def _format_response(self, response: dict) -> bytes:
"""格式化 HTTP 响应"""
status = response.get("status", 200)
headers = response.get("headers", {})
body = response.get("body", "")
status_text = {200: "OK", 404: "Not Found", 500: "Internal Server Error"}.get(status, "OK")
response_lines = [
f"HTTP/1.1 {status} {status_text}",
]
if "Content-Type" not in headers:
headers["Content-Type"] = "text/plain; charset=utf-8"
headers["Content-Length"] = str(len(body))
for key, value in headers.items():
response_lines.append(f"{key}: {value}")
response_lines.append("")
response_lines.append(body)
return "\r\n".join(response_lines).encode("utf-8")
def stop(self):
"""停止服务器"""
self._running = False
for client in self._clients.values():
client.close()
if self._server:
self._server.close()
print("[http-tcp] 服务器已停止")
def get_clients(self) -> list[TcpClient]:
"""获取所有客户端"""
return list(self._clients.values())

View File

@@ -0,0 +1,83 @@
# json-codec JSON 编解码器
提供插件间 JSON 数据的编码、解码和验证功能。
## 功能
- **JSON 编码**: Python 对象 → JSON 字符串
- **JSON 解码**: JSON 字符串 → Python 对象
- **Schema 验证**: 验证 JSON 数据结构
- **自定义类型**: 支持注册自定义类型编解码器
## 基本使用
```python
codec = plugin_mgr.get("json-codec")
# 编码
data = {"name": "test", "count": 42}
json_str = codec.encode(data)
# '{"name": "test", "count": 42}'
# 编码(格式化)
json_pretty = codec.encode(data, pretty=True)
# '{\n "name": "test",\n "count": 42\n}'
# 解码
parsed = codec.decode(json_str)
# {"name": "test", "count": 42}
```
## HTTP 响应处理
```python
# 在 http-api 插件中使用
router.get("/api/users", lambda req: Response(
status=200,
headers={"Content-Type": "application/json"},
body=codec.encode({"users": [...]})
))
```
## Schema 验证
```python
# 注册 schema
codec.register_schema("user", {
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "number"}
}
})
# 验证数据
user_data = {"name": "test", "email": "test@example.com"}
is_valid = codec.validate(user_data, "user")
```
## 自定义类型
```python
from datetime import datetime
# 注册自定义编码器
codec.serializer.register_encoder(datetime, lambda dt: dt.isoformat())
# 使用
data = {"created_at": datetime.now()}
json_str = codec.encode(data)
```
## 错误处理
```python
from oss.plugin.types import JsonCodecError
try:
result = codec.decode("invalid json")
except JsonCodecError as e:
print(f"解码失败: {e}")
```

View File

@@ -0,0 +1,161 @@
"""JSON 编解码器 - 插件间 JSON 数据处理"""
import json
from typing import Any, Callable, Optional
from datetime import datetime
from oss.plugin.types import Plugin, register_plugin_type
class JsonCodecError(Exception):
"""JSON 编解码错误"""
pass
class JsonSerializer:
"""JSON 序列化器"""
def __init__(self):
self._custom_encoders: dict[type, Callable] = {}
def register_encoder(self, type_class: type, encoder: Callable):
"""注册自定义类型编码器"""
self._custom_encoders[type_class] = encoder
def encode(self, data: Any, pretty: bool = False) -> str:
"""编码为 JSON 字符串"""
def default_handler(obj):
if isinstance(obj, datetime):
return obj.isoformat()
for type_class, encoder in self._custom_encoders.items():
if isinstance(obj, type_class):
return encoder(obj)
raise TypeError(f"无法序列化类型: {type(obj).__name__}")
if pretty:
return json.dumps(data, ensure_ascii=False, indent=2, default=default_handler)
return json.dumps(data, ensure_ascii=False, default=default_handler)
def encode_to_bytes(self, data: Any) -> bytes:
"""编码为字节"""
return self.encode(data).encode("utf-8")
class JsonDeserializer:
"""JSON 反序列化器"""
def __init__(self):
self._custom_decoders: dict[str, Callable] = {}
def register_decoder(self, type_name: str, decoder: Callable):
"""注册自定义类型解码器"""
self._custom_decoders[type_name] = decoder
def decode(self, text: str) -> Any:
"""解码 JSON 字符串"""
try:
return json.loads(text)
except json.JSONDecodeError as e:
raise JsonCodecError(f"JSON 解码失败: {e}")
def decode_bytes(self, data: bytes) -> Any:
"""解码字节"""
return self.decode(data.decode("utf-8"))
def decode_file(self, path: str) -> Any:
"""解码 JSON 文件"""
with open(path, "r", encoding="utf-8") as f:
return self.decode(f.read())
class JsonValidator:
"""JSON 验证器"""
def __init__(self):
self._schemas: dict[str, dict] = {}
def register_schema(self, name: str, schema: dict):
"""注册 schema"""
self._schemas[name] = schema
def validate(self, data: Any, schema_name: str) -> bool:
"""验证数据是否符合 schema"""
if schema_name not in self._schemas:
raise JsonCodecError(f"未知的 schema: {schema_name}")
return self._check_schema(data, self._schemas[schema_name])
def _check_schema(self, data: Any, schema: dict) -> bool:
"""检查 schema 匹配"""
schema_type = schema.get("type")
if schema_type == "object":
if not isinstance(data, dict):
return False
required = schema.get("required", [])
for field in required:
if field not in data:
return False
properties = schema.get("properties", {})
for key, value in data.items():
if key in properties:
if not self._check_schema(value, properties[key]):
return False
return True
elif schema_type == "array":
if not isinstance(data, list):
return False
items_schema = schema.get("items", {})
return all(self._check_schema(item, items_schema) for item in data)
elif schema_type == "string":
return isinstance(data, str)
elif schema_type == "number":
return isinstance(data, (int, float))
elif schema_type == "boolean":
return isinstance(data, bool)
return True
class JsonCodecPlugin(Plugin):
"""JSON 编解码器插件"""
def __init__(self):
self.serializer = JsonSerializer()
self.deserializer = JsonDeserializer()
self.validator = JsonValidator()
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动"""
print("[json-codec] JSON 编解码器已启动")
def stop(self):
"""停止"""
pass
def encode(self, data: Any, pretty: bool = False) -> str:
"""编码 JSON"""
return self.serializer.encode(data, pretty)
def decode(self, text: str) -> Any:
"""解码 JSON"""
return self.deserializer.decode(text)
def validate(self, data: Any, schema_name: str) -> bool:
"""验证 JSON schema"""
return self.validator.validate(data, schema_name)
def register_schema(self, name: str, schema: dict):
"""注册 schema"""
self.validator.register_schema(name, schema)
# 注册类型
register_plugin_type("JsonSerializer", JsonSerializer)
register_plugin_type("JsonDeserializer", JsonDeserializer)
register_plugin_type("JsonValidator", JsonValidator)
register_plugin_type("JsonCodecError", JsonCodecError)
def New():
return JsonCodecPlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "json-codec",
"version": "1.0.0",
"author": "FutureOSS",
"description": "JSON 编解码器 - 插件间 JSON 数据处理和验证",
"type": "utility"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,30 @@
# lifecycle 生命周期管理
管理插件的状态转换和钩子函数。
## 功能
- 状态机:`pending``running``stopped`
- 支持状态转换验证
- 提供生命周期钩子:
- `before_start`
- `after_start`
- `before_stop`
- `after_stop`
- 支持扩展能力注入
## 状态转换
```
pending → running → stopped
(可重启)
```
## 使用
```python
lc = lifecycle_plugin.create("my-plugin")
lc.on("after_start", lambda: print("started"))
lc.start()
```

View File

@@ -0,0 +1,150 @@
"""生命周期插件 - 管理插件生命周期状态"""
from enum import Enum
from typing import Optional, Callable, Any
from oss.plugin.types import Plugin, register_plugin_type
class LifecycleState(str, Enum):
"""生命周期状态"""
PENDING = "pending"
RUNNING = "running"
STOPPED = "stopped"
class LifecycleError(Exception):
"""生命周期错误"""
pass
class Lifecycle:
"""生命周期管理器"""
VALID_TRANSITIONS = {
LifecycleState.PENDING: [LifecycleState.RUNNING],
LifecycleState.RUNNING: [LifecycleState.STOPPED],
LifecycleState.STOPPED: [LifecycleState.RUNNING],
}
def __init__(self, name: str):
self.name = name
self.state = LifecycleState.PENDING
self._hooks: dict[str, list[Callable]] = {
"before_start": [],
"after_start": [],
"before_stop": [],
"after_stop": [],
}
self._extensions: dict[str, Any] = {} # 扩展能力
def add_extension(self, name: str, extension: Any):
"""添加扩展能力"""
self._extensions[name] = extension
def get_extension(self, name: str) -> Optional[Any]:
"""获取扩展能力"""
return self._extensions.get(name)
def transition(self, target_state: LifecycleState):
"""状态转换"""
if target_state not in self.VALID_TRANSITIONS.get(self.state, []):
raise LifecycleError(
f"插件 '{self.name}' 无法从 {self.state.value} 转换到 {target_state.value}"
)
old_state = self.state
self.state = target_state
def start(self):
"""启动"""
for hook in self._hooks["before_start"]:
hook(self)
self.transition(LifecycleState.RUNNING)
for hook in self._hooks["after_start"]:
hook(self)
def stop(self):
"""停止"""
for hook in self._hooks["before_stop"]:
hook(self)
self.transition(LifecycleState.STOPPED)
for hook in self._hooks["after_stop"]:
hook(self)
def restart(self):
"""重启"""
if self.state == LifecycleState.RUNNING:
self.stop()
self.start()
def on(self, event: str, hook: Callable):
"""注册钩子"""
if event in self._hooks:
self._hooks[event].append(hook)
def is_running(self) -> bool:
return self.state == LifecycleState.RUNNING
def is_stopped(self) -> bool:
return self.state == LifecycleState.STOPPED
def is_pending(self) -> bool:
return self.state == LifecycleState.PENDING
def __repr__(self):
return f"Lifecycle({self.name}, state={self.state.value})"
class LifecyclePlugin(Plugin):
"""生命周期插件"""
def __init__(self):
self.lifecycles: dict[str, Lifecycle] = {}
def init(self, deps: dict = None):
"""初始化"""
pass
def start(self):
"""启动"""
pass
def stop(self):
"""停止"""
pass
def create(self, name: str) -> Lifecycle:
"""创建生命周期"""
lifecycle = Lifecycle(name)
self.lifecycles[name] = lifecycle
return lifecycle
def get(self, name: str) -> Optional[Lifecycle]:
"""获取生命周期"""
return self.lifecycles.get(name)
def start_all(self):
"""启动所有"""
for lc in self.lifecycles.values():
try:
lc.start()
except LifecycleError:
pass
def stop_all(self):
"""停止所有"""
for lc in self.lifecycles.values():
try:
lc.stop()
except LifecycleError:
pass
# 注册类型
register_plugin_type("Lifecycle", Lifecycle)
register_plugin_type("LifecycleState", LifecycleState)
register_plugin_type("LifecycleError", LifecycleError)
def New():
return LifecyclePlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "lifecycle",
"version": "1.0.0",
"author": "FutureOSS",
"description": "生命周期管理 - 管理插件的状态转换和钩子",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,43 @@
# pkg 包管理
插件的搜索、安装、卸载和更新功能。
## 功能
- 从远程仓库搜索插件
- 下载并安装到 `./data/pkg/` 目录
- 卸载已安装的插件
- 更新单个或所有插件
- 维护已安装插件列表
## 使用
```python
pm = pkg_plugin.manager
# 搜索
results = pm.search("keyword")
# 安装
pm.install("plugin-name")
pm.install("plugin-name", version="1.0.0")
# 卸载
pm.uninstall("plugin-name")
# 更新
pm.update() # 更新所有
pm.update("plugin-name") # 更新单个
# 列出已安装
installed = pm.list_installed()
```
## 安装位置
```
./data/pkg/
└── <插件名>/
├── main.py
└── manifest.json
```

View File

@@ -0,0 +1,201 @@
"""包管理插件 - 搜索、安装、卸载、更新插件"""
import os
import json
import shutil
import urllib.request
import urllib.parse
from pathlib import Path
from typing import Any, Optional
from oss.plugin.types import Plugin, register_plugin_type
# 远程仓库地址(可配置)
DEFAULT_REGISTRY = "https://gitee.com/starlight-apk/future-oss-pkg/raw/main"
# 插件安装目录
PKG_DIR = Path("./data/pkg")
class PackageInfo:
"""包信息"""
def __init__(self):
self.name: str = ""
self.version: str = ""
self.author: str = ""
self.description: str = ""
self.download_url: str = ""
self.dependencies: list[str] = []
class PackageManager:
"""包管理器"""
def __init__(self):
self.registry = DEFAULT_REGISTRY
self.index_cache: dict[str, PackageInfo] = {}
self.installed: dict[str, dict[str, Any]] = {}
self._load_installed()
def _load_installed(self):
"""加载已安装的包"""
if not PKG_DIR.exists():
return
for pkg_dir in PKG_DIR.iterdir():
if pkg_dir.is_dir():
manifest = pkg_dir / "manifest.json"
if manifest.exists():
with open(manifest, "r", encoding="utf-8") as f:
self.installed[pkg_dir.name] = json.load(f)
def search(self, query: str = "") -> list[PackageInfo]:
"""搜索可用的包"""
# 从远程仓库获取包索引
index_url = f"{self.registry}/index.json"
try:
with urllib.request.urlopen(index_url, timeout=10) as resp:
index = json.loads(resp.read().decode("utf-8"))
except Exception:
# 本地缓存
return list(self.index_cache.values())
results = []
for pkg_name, pkg_info in index.items():
if not query or query.lower() in pkg_name.lower() or query.lower() in pkg_info.get("description", "").lower():
info = PackageInfo()
info.name = pkg_name
info.version = pkg_info.get("version", "")
info.author = pkg_info.get("author", "")
info.description = pkg_info.get("description", "")
info.download_url = pkg_info.get("download_url", "")
info.dependencies = pkg_info.get("dependencies", [])
results.append(info)
self.index_cache[pkg_name] = info
return results
def install(self, name: str, version: str = "") -> bool:
"""安装包,支持 @{作者/插件名} 格式"""
# 解析输入格式 @{author/plugin} 或直接插件名
author = "FutureOSS" # 默认作者
plugin_name = name
if name.startswith("@{") and "/" in name:
# 解析 @{author/plugin} 格式
inner = name[2:-1] if name.endswith("}") else name[2:]
parts = inner.split("/", 1)
if len(parts) == 2:
author, plugin_name = parts
# 搜索获取下载链接
packages = self.search(plugin_name)
pkg_info = None
for p in packages:
if p.name == plugin_name and p.author == author:
if not version or p.version == version:
pkg_info = p
break
if not pkg_info or not pkg_info.download_url:
# 尝试从远程仓库直接构建 URL
pkg_info = PackageInfo()
pkg_info.name = plugin_name
pkg_info.author = author
pkg_info.version = version or "1.0.0"
pkg_info.download_url = self.registry + "/store/@{" + author + "/" + plugin_name + "}"
# 创建安装目录 @{author/plugin_name}
install_dir = PKG_DIR / ("@{" + author + "/" + plugin_name + "}")
install_dir.mkdir(parents=True, exist_ok=True)
try:
# 下载 manifest.json
manifest_url = f"{pkg_info.download_url}/manifest.json"
with urllib.request.urlopen(manifest_url, timeout=10) as resp:
manifest_data = json.loads(resp.read().decode("utf-8"))
with open(install_dir / "manifest.json", "w", encoding="utf-8") as f:
json.dump(manifest_data, f, ensure_ascii=False, indent=2)
# 下载 main.py
main_url = f"{pkg_info.download_url}/main.py"
with urllib.request.urlopen(main_url, timeout=10) as resp:
main_data = resp.read().decode("utf-8")
with open(install_dir / "main.py", "w", encoding="utf-8") as f:
f.write(main_data)
# 更新已安装列表
full_name = "@{" + author + "/" + plugin_name
self.installed[full_name] = manifest_data
print(f"[pkg] 已安装: {full_name} {manifest_data.get('metadata', {}).get('version', '')}")
return True
except Exception as e:
print(f"[pkg] 安装失败 {name}: {e}")
# 清理失败的安装
if install_dir.exists():
shutil.rmtree(install_dir)
return False
def uninstall(self, name: str) -> bool:
"""卸载包"""
install_dir = PKG_DIR / name
if not install_dir.exists():
print(f"[pkg] 包未安装: {name}")
return False
try:
shutil.rmtree(install_dir)
del self.installed[name]
print(f"[pkg] 已卸载: {name}")
return True
except Exception as e:
print(f"[pkg] 卸载失败 {name}: {e}")
return False
def update(self, name: str = "") -> bool:
"""更新包"""
if name:
# 更新单个包
if name not in self.installed:
print(f"[pkg] 包未安装: {name}")
return False
return self.install(name)
else:
# 更新所有已安装的包
success = True
for pkg_name in list(self.installed.keys()):
if not self.install(pkg_name):
success = False
return success
def list_installed(self) -> dict[str, Any]:
"""列出已安装的包"""
return self.installed
class PkgPlugin(Plugin):
"""包管理插件"""
def __init__(self):
self.manager = PackageManager()
def init(self, deps: dict = None):
"""初始化"""
PKG_DIR.mkdir(parents=True, exist_ok=True)
print("[pkg] 包管理器已初始化")
def start(self):
"""启动"""
print(f"[pkg] 包管理器已启动,已安装 {len(self.manager.installed)} 个包")
def stop(self):
"""停止"""
pass
# 注册类型
register_plugin_type("PackageManager", PackageManager)
register_plugin_type("PackageInfo", PackageInfo)
def New():
return PkgPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "pkg",
"version": "1.0.0",
"author": "FutureOSS",
"description": "包管理 - 插件的搜索、安装、卸载和更新",
"type": "utility"
},
"config": {
"enabled": true,
"args": {
"registry": "https://gitee.com/starlight-apk/future-oss-pkg/raw/main",
"install_dir": "./data/pkg"
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,77 @@
# plugin-bridge 插件桥接器
提供插件间的事件共享、广播、桥接和 RPC 服务调用。
## 功能
- **事件总线**: 插件间共享事件(发布/订阅)
- **广播**: 向多个插件发送消息
- **桥接**: 将不同插件的事件互相映射
- **RPC 服务调用**: 插件 A 调用插件 B 的方法并获取返回值
## 事件总线(发布/订阅 + 解耦)
```python
bridge = plugin_mgr.get("plugin-bridge")
bus = bridge.event_bus
# 订阅事件(发布者和订阅者解耦)
bus.on("http.request", lambda event: print(f"收到请求: {event.payload}"))
# 发布事件
bus.emit(BridgeEvent(
type="http.request",
source_plugin="http-api",
payload={"path": "/api/users"}
))
```
## RPC 服务调用
```python
# 插件 B 注册服务
bridge.services.register("plugin-b", "get_user", lambda user_id: {"id": user_id, "name": "test"})
# 插件 A 调用插件 B 的服务
result = bridge.services.call("plugin-b", "get_user", 123)
print(result) # {"id": 123, "name": "test"}
```
## 广播
```python
broadcast = bridge.broadcast
# 创建频道
broadcast.create_channel("system", ["lifecycle", "metrics"])
# 广播消息
broadcast.broadcast("system", {"action": "shutdown"}, "plugin-loader")
```
## 桥接
```python
bridge_mgr = bridge.bridge
# 创建桥接:将 http-api 的事件映射到 metrics
bridge_mgr.create_bridge(
name="http-to-metrics",
from_plugin="http-api",
to_plugin="metrics",
event_mapping={
"http.request": "metrics.http_request",
"http.error": "metrics.http_error",
}
)
```
## 事件历史
```python
# 查询历史
history = bus.get_history("http.request")
# 清空历史
bus.clear_history()
```

View File

@@ -0,0 +1,203 @@
"""插件桥接器 - 共享事件、广播、桥接"""
from typing import Any, Callable, Optional
from dataclasses import dataclass, field
from oss.plugin.types import Plugin, register_plugin_type
@dataclass
class BridgeEvent:
"""桥接事件"""
type: str
source_plugin: str
payload: Any = None
context: dict[str, Any] = field(default_factory=dict)
class EventBus:
"""事件总线"""
def __init__(self):
self._handlers: dict[str, list[Callable]] = {}
self._history: list[BridgeEvent] = []
def emit(self, event: BridgeEvent):
"""发布事件"""
self._history.append(event)
handlers = self._handlers.get(event.type, [])
wildcard_handlers = self._handlers.get("*", [])
for handler in handlers + wildcard_handlers:
try:
handler(event)
except Exception:
pass
def on(self, event_type: str, handler: Callable):
"""订阅事件"""
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def off(self, event_type: str, handler: Callable):
"""取消订阅"""
if event_type in self._handlers:
try:
self._handlers[event_type].remove(handler)
except ValueError:
pass
def once(self, event_type: str, handler: Callable):
"""仅触发一次"""
def wrapper(event):
self.off(event_type, wrapper)
handler(event)
self.on(event_type, wrapper)
def get_history(self, event_type: str = None) -> list[BridgeEvent]:
"""获取事件历史"""
if event_type:
return [e for e in self._history if e.type == event_type]
return self._history.copy()
def clear_history(self):
"""清空事件历史"""
self._history.clear()
class BroadcastManager:
"""广播管理器"""
def __init__(self, event_bus: EventBus):
self.event_bus = event_bus
self._channels: dict[str, list[str]] = {}
def create_channel(self, name: str, plugins: list[str]):
"""创建广播频道"""
self._channels[name] = plugins
def broadcast(self, channel: str, payload: Any, source_plugin: str = ""):
"""广播到指定频道"""
if channel not in self._channels:
return
event = BridgeEvent(
type=f"broadcast.{channel}",
source_plugin=source_plugin,
payload=payload
)
self.event_bus.emit(event)
def get_channels(self) -> dict[str, list[str]]:
"""获取所有频道"""
return self._channels.copy()
class ServiceRegistry:
"""服务注册表RPC"""
def __init__(self):
self._services: dict[str, dict[str, Callable]] = {}
def register(self, plugin_name: str, service_name: str, handler: Callable):
"""注册服务"""
if plugin_name not in self._services:
self._services[plugin_name] = {}
self._services[plugin_name][service_name] = handler
def unregister(self, plugin_name: str, service_name: str = None):
"""注销服务"""
if plugin_name in self._services:
if service_name:
self._services[plugin_name].pop(service_name, None)
else:
del self._services[plugin_name]
def call(self, plugin_name: str, service_name: str, *args, **kwargs) -> Any:
"""远程调用"""
if plugin_name not in self._services:
raise RuntimeError(f"插件 '{plugin_name}' 未注册服务")
if service_name not in self._services[plugin_name]:
raise RuntimeError(f"插件 '{plugin_name}' 未注册服务 '{service_name}'")
return self._services[plugin_name][service_name](*args, **kwargs)
def list_services(self, plugin_name: str = None) -> dict[str, dict[str, Callable]]:
"""列出服务"""
if plugin_name:
return self._services.get(plugin_name, {}).copy()
return {k: v.copy() for k, v in self._services.items()}
class BridgeManager:
"""桥接管理器"""
def __init__(self, event_bus: EventBus):
self.event_bus = event_bus
self._bridges: dict[str, dict[str, Any]] = {}
def create_bridge(self, name: str, from_plugin: str, to_plugin: str, event_mapping: dict[str, str]):
"""创建桥接:将 from_plugin 的事件映射到 to_plugin"""
self._bridges[name] = {
"from": from_plugin,
"to": to_plugin,
"mapping": event_mapping,
}
# 注册桥接处理器
for src_event, dst_event in event_mapping.items():
def handler(event, dst_event=dst_event):
bridged = BridgeEvent(
type=dst_event,
source_plugin=event.source_plugin,
payload=event.payload,
context={**event.context, "_bridged_from": event.type}
)
self.event_bus.emit(bridged)
self.event_bus.on(src_event, handler)
def remove_bridge(self, name: str):
"""移除桥接"""
if name in self._bridges:
del self._bridges[name]
def get_bridges(self) -> dict[str, dict[str, Any]]:
"""获取所有桥接"""
return self._bridges.copy()
class PluginBridgePlugin(Plugin):
"""插件桥接器插件"""
def __init__(self):
self.event_bus = EventBus()
self.broadcast = None
self.bridge = None
self.services = ServiceRegistry()
self.storage = None # 共享存储接口
def init(self, deps: dict = None):
"""初始化"""
self.broadcast = BroadcastManager(self.event_bus)
self.bridge = BridgeManager(self.event_bus)
def start(self):
"""启动"""
print("[plugin-bridge] 事件总线、广播、桥接、RPC、共享存储已启动")
def stop(self):
"""停止"""
self.event_bus.clear_history()
def set_plugin_storage(self, storage_plugin):
"""设置存储插件引用"""
if storage_plugin:
self.storage = storage_plugin.get_shared()
# 注册类型
register_plugin_type("BridgeEvent", BridgeEvent)
register_plugin_type("EventBus", EventBus)
register_plugin_type("BroadcastManager", BroadcastManager)
register_plugin_type("BridgeManager", BridgeManager)
register_plugin_type("ServiceRegistry", ServiceRegistry)
def New():
return PluginBridgePlugin()

View File

@@ -0,0 +1,15 @@
{
"metadata": {
"name": "plugin-bridge",
"version": "1.0.0",
"author": "FutureOSS",
"description": "插件桥接器 - 共享事件、广播、桥接",
"type": "core"
},
"config": {
"enabled": true,
"args": {}
},
"dependencies": ["plugin-storage"],
"permissions": ["plugin-storage"]
}

View File

@@ -0,0 +1,16 @@
# plugin-loader 插件加载器
核心插件,负责扫描、加载和管理所有其他插件。
## 功能
- 自动扫描 `store/``./data/pkg/` 目录
- 动态加载 `main.py` 并调用 `New()` 获取实例
- 解析 `manifest.json` 获取插件元数据
- 自动扫描插件能力AST 分析)
- 按依赖关系排序加载顺序
- 关联能力提供者与消费者
## 使用
无需手动使用,框架启动时自动加载。

View File

@@ -0,0 +1,609 @@
"""插件加载器插件 - 支持能力扫描和扩展"""
import sys
import json
import importlib.util
from pathlib import Path
from typing import Any, Optional
from oss.plugin.types import Plugin, register_plugin_type
from oss.plugin.capabilities import scan_capabilities
class PluginInfo:
"""插件信息"""
def __init__(self):
self.name: str = ""
self.version: str = ""
self.author: str = ""
self.description: str = ""
self.readme: str = ""
self.config: dict[str, Any] = {}
self.extensions: dict[str, Any] = {}
self.lifecycle: Any = None
self.capabilities: set[str] = set()
self.dependencies: list[str] = []
class PermissionError(Exception):
"""权限错误"""
pass
class PluginProxy:
"""插件代理 - 防止越级访问"""
def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict[str, dict[str, Any]]):
self._plugin_name = plugin_name
self._plugin_instance = plugin_instance
self._allowed_plugins = set(allowed_plugins)
self._all_plugins = all_plugins
def get_plugin(self, name: str) -> Any:
"""获取其他插件实例(带权限检查)"""
if name not in self._allowed_plugins and "*" not in self._allowed_plugins:
raise PermissionError(f"插件 '{self._plugin_name}' 无权访问插件 '{name}'")
if name not in self._all_plugins:
return None
return self._all_plugins[name]["instance"]
def list_plugins(self) -> list[str]:
"""列出有权限访问的插件"""
if "*" in self._allowed_plugins:
return list(self._all_plugins.keys())
return [name for name in self._allowed_plugins if name in self._all_plugins]
def get_capability(self, capability: str) -> Any:
"""获取能力(带权限检查)"""
# 能力访问不需要额外权限,能力注册表会自动处理
return None
def __getattr__(self, name: str):
"""代理其他属性到插件实例"""
return getattr(self._plugin_instance, name)
class CapabilityRegistry:
"""能力注册表"""
def __init__(self, permission_check: bool = True):
self.providers: dict[str, dict[str, Any]] = {}
self.consumers: dict[str, list[str]] = {}
self.permission_check = permission_check
def register_provider(self, capability: str, plugin_name: str, instance: Any):
"""注册能力提供者"""
self.providers[capability] = {
"plugin": plugin_name,
"instance": instance,
}
if capability not in self.consumers:
self.consumers[capability] = []
def register_consumer(self, capability: str, plugin_name: str):
"""注册能力消费者"""
if capability not in self.consumers:
self.consumers[capability] = []
if plugin_name not in self.consumers[capability]:
self.consumers[capability].append(plugin_name)
def get_provider(self, capability: str, requester: str = "", allowed_plugins: list[str] = None) -> Optional[Any]:
"""获取能力提供者实例(带权限检查)"""
if capability not in self.providers:
return None
if self.permission_check and allowed_plugins is not None:
provider_name = self.providers[capability]["plugin"]
if provider_name != requester and provider_name not in allowed_plugins and "*" not in allowed_plugins:
raise PermissionError(f"插件 '{requester}' 无权使用能力 '{capability}'")
return self.providers[capability]["instance"]
def has_capability(self, capability: str) -> bool:
"""检查是否有某个能力"""
return capability in self.providers
def get_consumers(self, capability: str) -> list[str]:
"""获取能力消费者列表"""
return self.consumers.get(capability, [])
class PluginManager:
"""插件管理器"""
def __init__(self, permission_check: bool = True):
self.plugins: dict[str, dict[str, Any]] = {}
self.lifecycle_plugin: Optional[Any] = None
self._dependency_plugin: Optional[Any] = None # dependency 插件引用
self.capability_registry = CapabilityRegistry(permission_check=permission_check)
self.permission_check = permission_check
def set_lifecycle(self, lifecycle_plugin: Any):
"""设置生命周期插件"""
self.lifecycle_plugin = lifecycle_plugin
def _load_manifest(self, plugin_dir: Path) -> dict[str, Any]:
"""加载 manifest.json"""
manifest_file = plugin_dir / "manifest.json"
if not manifest_file.exists():
return {}
with open(manifest_file, "r", encoding="utf-8") as f:
return json.load(f)
def _load_readme(self, plugin_dir: Path) -> str:
"""加载 README.md"""
readme_file = plugin_dir / "README.md"
if not readme_file.exists():
return ""
with open(readme_file, "r", encoding="utf-8") as f:
return f.read()
def _load_config(self, plugin_dir: Path) -> dict[str, Any]:
"""加载 Python 配置文件(带安全措施)"""
config_file = plugin_dir / "config.py"
if not config_file.exists():
return {}
safe_globals = {
"__builtins__": {
"True": True,
"False": False,
"None": None,
"dict": dict,
"list": list,
"str": str,
"int": int,
"float": float,
"bool": bool,
}
}
local_vars = {}
with open(config_file, "r", encoding="utf-8") as f:
code = compile(f.read(), str(config_file), "exec")
exec(code, safe_globals, local_vars)
return {
k: v for k, v in local_vars.items()
if not k.startswith("_") and not callable(v)
}
def _load_extensions(self, plugin_dir: Path) -> dict[str, Any]:
"""加载扩展语法Python 文件)"""
ext_file = plugin_dir / "extensions.py"
if not ext_file.exists():
return {}
safe_globals = {
"__builtins__": {
"True": True,
"False": False,
"None": None,
"dict": dict,
"list": list,
"str": str,
"int": int,
"float": float,
"bool": bool,
}
}
local_vars = {}
with open(ext_file, "r", encoding="utf-8") as f:
code = compile(f.read(), str(ext_file), "exec")
exec(code, safe_globals, local_vars)
return {
k: v for k, v in local_vars.items()
if not k.startswith("_") and not callable(v)
}
def load(self, plugin_dir: Path, use_sandbox: bool = True) -> Optional[Any]:
"""加载单个插件"""
main_file = plugin_dir / "main.py"
if not main_file.exists():
return None
manifest = self._load_manifest(plugin_dir)
readme = self._load_readme(plugin_dir)
config = self._load_config(plugin_dir)
extensions = self._load_extensions(plugin_dir)
# 自动扫描能力
capabilities = scan_capabilities(plugin_dir)
plugin_name = plugin_dir.name
# 清理插件名(去掉 } 后缀)
plugin_name = plugin_dir.name.rstrip("}")
# 解析权限
permissions = manifest.get("permissions", [])
# 沙箱加载
if use_sandbox:
from oss.plugin.loader import PluginLoader as FrameworkLoader
framework_loader = FrameworkLoader(enable_sandbox=True)
result = framework_loader.load_sandbox_plugin(plugin_dir)
if not result:
return None
module = result["module"]
instance = result["instance"]
else:
spec = importlib.util.spec_from_file_location(
f"plugin.{plugin_name}", str(main_file)
)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
if not hasattr(module, "New"):
return None
instance = module.New()
# 创建代理包装器
if self.permission_check and permissions:
instance = PluginProxy(plugin_name, instance, permissions, self.plugins)
info = PluginInfo()
meta = manifest.get("metadata", {})
info.name = meta.get("name", plugin_name)
info.version = meta.get("version", "")
info.author = meta.get("author", "")
info.description = meta.get("description", "")
info.readme = readme
info.config = manifest.get("config", {}).get("args", config)
info.extensions = extensions
info.capabilities = capabilities
info.dependencies = manifest.get("dependencies", [])
# 注册能力
for cap in capabilities:
self.capability_registry.register_provider(cap, plugin_name, instance)
# 创建生命周期
if self.lifecycle_plugin and plugin_name != "lifecycle":
lc = self.lifecycle_plugin.create(plugin_name)
info.lifecycle = lc
self.plugins[plugin_name] = {
"instance": instance,
"module": module,
"info": info,
"permissions": permissions,
}
return instance
def load_all(self, store_dir: str = "store"):
"""加载 store 和 data/pkg 下所有插件(跳过自己)"""
# 检查是否有任何插件存在
has_plugins = self._check_any_plugins(store_dir)
if not has_plugins:
print("[plugin-loader] 未检测到任何插件,自动引导安装...")
self._bootstrap_installation()
# 可选:加载 lifecycle
lifecycle_plugin = None
lifecycle_dir = Path(store_dir) / "@{FutureOSS}" / "lifecycle"
if lifecycle_dir.exists() and (lifecycle_dir / "main.py").exists():
try:
instance = self.load(lifecycle_dir)
if instance:
lifecycle_plugin = instance
self.plugins.pop("lifecycle", None)
except Exception:
pass
# 可选:加载 dependency
dependency_plugin = None
dependency_dir = Path(store_dir) / "@{FutureOSS}" / "dependency"
if dependency_dir.exists() and (dependency_dir / "main.py").exists():
try:
instance = self.load(dependency_dir)
if instance:
dependency_plugin = instance
self._dependency_plugin = instance # 保存引用供拓扑排序使用
self.plugins.pop("dependency", None)
except Exception:
pass
# 加载 lifecycle
if lifecycle_plugin:
self.set_lifecycle(lifecycle_plugin)
# 加载其他插件
self._load_plugins_from_dir(Path(store_dir))
self._load_plugins_from_dir(Path("./data/pkg"))
# 可选:按依赖排序
if dependency_plugin:
self._sort_by_dependencies(dependency_plugin)
def _load_plugins_from_dir(self, store_dir: Path):
"""从指定目录加载插件"""
if not store_dir.exists():
return
# 第一遍:加载所有插件
for author_dir in store_dir.iterdir():
if author_dir.is_dir():
for plugin_dir in author_dir.iterdir():
if plugin_dir.is_dir() and plugin_dir.name not in ("plugin-loader", "lifecycle", "dependency"):
if (plugin_dir / "main.py").exists():
self.load(plugin_dir)
# 第二遍:关联能力
self._link_capabilities()
def _check_any_plugins(self, store_dir: str) -> bool:
"""检查是否存在任何插件"""
store = Path(store_dir)
if store.exists():
for author_dir in store.iterdir():
if author_dir.is_dir():
for plugin_dir in author_dir.iterdir():
if plugin_dir.is_dir() and (plugin_dir / "main.py").exists():
return True
pkg_dir = Path("./data/pkg")
if pkg_dir.exists():
for d in pkg_dir.iterdir():
if d.is_dir() and (d / "main.py").exists():
return True
return False
def _bootstrap_installation(self):
"""引导安装 FutureOSS 官方插件"""
# 加载 pkg 插件
pkg_dir = Path("store/@{FutureOSS}/pkg")
if pkg_dir.exists() and (pkg_dir / "main.py").exists():
try:
pkg_instance = self.load(pkg_dir, use_sandbox=False)
if pkg_instance:
pkg_mgr = pkg_instance.manager
print("[plugin-loader] 正在搜索可用插件...")
results = pkg_mgr.search()
if not results:
print("[plugin-loader] 未找到远程插件")
return
print(f"[plugin-loader] 发现 {len(results)} 个插件,开始安装...")
installed_count = 0
for pkg_info in results:
print(f"[plugin-loader] 安装: {pkg_info.name}")
if pkg_mgr.install(pkg_info.name):
installed_count += 1
if installed_count > 0:
print(f"[plugin-loader] 已安装 {installed_count} 个插件,重新扫描加载...")
# pkg 保留,重新加载其他插件
except Exception as e:
print(f"[plugin-loader] 引导安装失败: {e}")
else:
print("[plugin-loader] pkg 插件不存在,跳过引导安装")
def _sort_by_dependencies(self, dep_plugin):
"""按依赖关系排序"""
if not dep_plugin:
return
# 添加所有插件的依赖
for name, info in self.plugins.items():
deps = info["info"].dependencies
dep_plugin.add_plugin(name, deps)
try:
order = dep_plugin.resolve()
# 重新排序 plugins
sorted_plugins = {}
for name in order:
if name in self.plugins:
sorted_plugins[name] = self.plugins[name]
# 检查是否所有插件都在排序结果中
missing = set(self.plugins.keys()) - set(sorted_plugins.keys())
for name in missing:
sorted_plugins[name] = self.plugins[name]
self.plugins = sorted_plugins
except Exception as e:
print(f"[plugin-loader] 依赖解析失败: {e}")
def _link_capabilities(self):
"""关联能力:带权限检查"""
for plugin_name, info in self.plugins.items():
caps = info["info"].capabilities
allowed = info.get("permissions", [])
for cap in caps:
# 如果这个插件是某个能力的提供者
if self.capability_registry.has_capability(cap):
# 找到所有需要这个能力的消费者
consumers = self.capability_registry.get_consumers(cap)
for consumer_name in consumers:
if consumer_name in self.plugins:
consumer_info = self.plugins[consumer_name]["info"]
consumer_allowed = self.plugins[consumer_name].get("permissions", [])
# 权限检查
try:
provider = self.capability_registry.get_provider(
cap,
requester=consumer_name,
allowed_plugins=consumer_allowed
)
if provider and hasattr(consumer_info, "extensions"):
consumer_info.extensions[f"_{cap}_provider"] = provider
except PermissionError as e:
print(f"[plugin-loader] 权限拒绝: {e}")
def start_all(self):
"""启动所有插件(假设已初始化)"""
# 注入依赖实例
self._inject_dependencies()
# 启动所有插件
for name, info in self.plugins.items():
try:
info["instance"].start()
except Exception as e:
print(f"[plugin-loader] 启动失败 {name}: {e}")
def init_and_start_all(self):
"""初始化并启动所有插件
正确顺序:
1. 注入依赖实例
2. 按拓扑顺序 init() 所有插件
3. 按拓扑顺序 start() 所有插件
"""
print(f"[plugin-loader] init_and_start_all 被调用plugins={len(self.plugins)}")
# 1. 注入依赖实例
self._inject_dependencies()
# 2. 获取拓扑排序
ordered_plugins = self._get_ordered_plugins()
print(f"[plugin-loader] 插件启动顺序: {' -> '.join(ordered_plugins)}")
# 3. 初始化所有插件(跳过 plugin-loader 自己)
print("[plugin-loader] 开始初始化所有插件...")
for name in ordered_plugins:
if "plugin-loader" in name:
continue
info = self.plugins[name]
try:
print(f"[plugin-loader] 初始化: {name}")
info["instance"].init()
except Exception as e:
print(f"[plugin-loader] 初始化失败 {name}: {e}")
# 4. 启动所有插件(跳过 plugin-loader 自己)
print("[plugin-loader] 开始启动所有插件...")
for name in ordered_plugins:
if "plugin-loader" in name:
continue
info = self.plugins[name]
try:
print(f"[plugin-loader] 启动: {name}")
info["instance"].start()
except Exception as e:
print(f"[plugin-loader] 启动失败 {name}: {e}")
def _get_ordered_plugins(self) -> list[str]:
"""获取按依赖排序的插件列表"""
# 如果没有 dependency 插件,直接返回原始顺序
if not hasattr(self, '_dependency_plugin') or not self._dependency_plugin:
return list(self.plugins.keys())
try:
# 使用 dependency 插件解析
order = self._dependency_plugin.resolve()
# 过滤出实际存在的插件
return [name for name in order if name in self.plugins]
except Exception as e:
print(f"[plugin-loader] 依赖解析失败,使用原始顺序: {e}")
return list(self.plugins.keys())
def _inject_dependencies(self):
"""注入插件依赖实例"""
print(f"[plugin-loader] 开始注入依赖,共 {len(self.plugins)} 个插件")
# 构建名称映射(处理 } 后缀问题)
name_map = {}
for name in self.plugins:
clean = name.rstrip("}")
name_map[clean] = name
name_map[clean + "}"] = name
for name, info in self.plugins.items():
instance = info["instance"]
info_obj = info.get("info")
if not info_obj:
continue
deps = info_obj.dependencies
if not deps:
continue
print(f"[plugin-loader] {name} 依赖: {deps}")
for dep_name in deps:
# 使用名称映射查找
actual_dep = name_map.get(dep_name) or name_map.get(dep_name + "}")
if actual_dep and actual_dep in self.plugins:
dep_instance = self.plugins[actual_dep]["instance"]
setter_name = f"set_{dep_name.replace('-', '_')}"
print(f"[plugin-loader] 尝试注入: {name} <- {actual_dep} ({setter_name})")
if hasattr(instance, setter_name):
try:
getattr(instance, setter_name)(dep_instance)
print(f"[plugin-loader] 注入成功: {name} <- {actual_dep}")
except Exception as e:
print(f"[plugin-loader] 注入依赖失败 {name}.{setter_name}: {e}")
else:
print(f"[plugin-loader] 警告: {name} 没有 {setter_name} 方法")
def stop_all(self):
"""停止所有插件"""
for name, info in reversed(list(self.plugins.items())):
try:
info["instance"].stop()
except Exception:
pass
if self.lifecycle_plugin:
self.lifecycle_plugin.stop_all()
def get_info(self, name: str) -> Optional[PluginInfo]:
"""获取插件信息"""
if name in self.plugins:
return self.plugins[name]["info"]
return None
def has_capability(self, capability: str) -> bool:
"""检查系统是否有某个能力"""
return self.capability_registry.has_capability(capability)
def get_capability_provider(self, capability: str) -> Optional[Any]:
"""获取能力提供者"""
return self.capability_registry.get_provider(capability)
class PluginLoaderPlugin(Plugin):
"""插件加载器插件"""
def __init__(self):
self.manager = PluginManager()
self._loaded = False
self._started = False
def init(self, deps: dict = None):
"""加载所有插件"""
if self._loaded:
return
self._loaded = True
print("[plugin-loader] 开始加载插件...")
self.manager.load_all()
def start(self):
"""启动所有插件"""
if self._started:
return
self._started = True
print("[plugin-loader] 启动插件...")
self.manager.init_and_start_all()
def stop(self):
"""停止所有插件"""
print("[plugin-loader] 停止插件...")
self.manager.stop_all()
# 注册类型
register_plugin_type("PluginManager", PluginManager)
register_plugin_type("PluginInfo", PluginInfo)
register_plugin_type("CapabilityRegistry", CapabilityRegistry)
def New():
return PluginLoaderPlugin()

View File

@@ -0,0 +1,19 @@
{
"metadata": {
"name": "plugin-loader",
"version": "1.0.0",
"author": "FutureOSS",
"description": "插件加载器 - 负责扫描、加载和管理所有插件",
"type": "core"
},
"config": {
"enabled": true,
"args": {
"scan_dirs": ["store", "./data/pkg"],
"sandbox_enabled": true,
"permission_check": true
}
},
"dependencies": [],
"permissions": ["*"]
}

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": ["*"]
}

View File

@@ -0,0 +1,50 @@
# ws-api WebSocket API
提供 WebSocket 实时双向通信服务。
## 功能
- WebSocket 服务器
- 路由匹配
- 中间件链(认证/日志)
- 广播消息
- 连接/断开/消息事件
- 与 HTTP 插件集成
## 使用
```python
ws = plugin_mgr.get("ws-api")
# 注册消息路由
ws.router.on_message("/chat", lambda client, msg: client.send({"echo": msg}))
# 广播
ws.server.broadcast({"type": "announcement", "data": "服务器维护通知"})
# 获取客户端列表
clients = ws.server.get_clients()
```
## 事件
```python
# 通过 plugin-bridge 订阅 WS 事件
bridge = plugin_mgr.get("plugin-bridge")
bridge.event_bus.on("ws.connect", lambda event: print(f"新连接: {event.client.path}"))
bridge.event_bus.on("ws.message", lambda event: print(f"消息: {event.message}"))
bridge.event_bus.on("ws.disconnect", lambda event: print(f"断开: {event.client.id}"))
```
## 配置
```json
{
"config": {
"args": {
"host": "0.0.0.0",
"port": 8081
}
}
}
```

View File

@@ -0,0 +1,23 @@
"""WebSocket 事件定义"""
from dataclasses import dataclass, field
from typing import Any, Optional
@dataclass
class WsEvent:
"""WebSocket 事件"""
type: str
client: Any = None
path: str = ""
message: str = ""
context: dict[str, Any] = field(default_factory=dict)
# 事件类型常量
EVENT_CONNECT = "ws.connect"
EVENT_DISCONNECT = "ws.disconnect"
EVENT_MESSAGE = "ws.message"
EVENT_ERROR = "ws.error"
EVENT_SUBSCRIBE = "ws.subscribe"
EVENT_UNSUBSCRIBE = "ws.unsubscribe"
EVENT_BROADCAST = "ws.broadcast"

View File

@@ -0,0 +1,30 @@
"""WebSocket API 插件入口 - 简化版"""
from oss.plugin.types import Plugin, register_plugin_type
class WsApiPlugin(Plugin):
"""WebSocket API 插件"""
def __init__(self):
self._running = False
def init(self, deps: dict = None):
"""初始化"""
print("[ws-api] 初始化完成")
def start(self):
"""启动"""
self._running = True
print("[ws-api] 已启动")
def stop(self):
"""停止"""
self._running = False
print("[ws-api] 已停止")
register_plugin_type("WsApiPlugin", WsApiPlugin)
def New():
return WsApiPlugin()

View File

@@ -0,0 +1,18 @@
{
"metadata": {
"name": "ws-api",
"version": "1.0.0",
"author": "FutureOSS",
"description": "WebSocket API 服务 - 实时双向通信",
"type": "protocol"
},
"config": {
"enabled": true,
"args": {
"host": "0.0.0.0",
"port": 8081
}
},
"dependencies": [],
"permissions": []
}

View File

@@ -0,0 +1,41 @@
"""WebSocket 中间件链"""
from typing import Callable, Optional, Any
class WsMiddleware:
"""WebSocket 中间件基类"""
async def process(self, client: Any, message: str, next_fn: Callable) -> Optional[str]:
"""处理消息"""
return await next_fn()
class AuthMiddleware(WsMiddleware):
"""认证中间件"""
async def process(self, client, message, next_fn):
# 可以在这里验证 token
return await next_fn()
class WsMiddlewareChain:
"""WebSocket 中间件链"""
def __init__(self):
self.middlewares: list[WsMiddleware] = []
def add(self, middleware: WsMiddleware):
"""添加中间件"""
self.middlewares.append(middleware)
async def run(self, client, message) -> Optional[str]:
"""执行中间件链"""
idx = 0
async def next_fn():
nonlocal idx
if idx < len(self.middlewares):
mw = self.middlewares[idx]
idx += 1
return await mw.process(client, message, next_fn)
return message
return await next_fn()

View File

@@ -0,0 +1,39 @@
"""WebSocket 路由器"""
import json
import asyncio
from typing import Callable, Optional, Any
from .server import WsClient
class WsRoute:
"""WebSocket 路由"""
def __init__(self, path: str, handler: Callable):
self.path = path
self.handler = handler
class WsRouter:
"""WebSocket 路由器"""
def __init__(self):
self.routes: dict[str, WsRoute] = {}
def on_message(self, path: str, handler: Callable):
"""注册消息路由"""
self.routes[path] = WsRoute(path, handler)
async def handle(self, client: WsClient, path: str, message: str):
"""处理消息"""
# 精确匹配
if path in self.routes:
await self.routes[path].handler(client, message)
return
# 前缀匹配
for route_path, route in self.routes.items():
if path.startswith(route_path):
await route.handler(client, message)
return
# 无匹配路由
await client.send({"error": "No handler for path", "path": path})

View File

@@ -0,0 +1,125 @@
"""WebSocket 服务器核心"""
import asyncio
import websockets
import threading
import json
from typing import Any, Callable, Optional
from .events import WsEvent, EVENT_CONNECT, EVENT_DISCONNECT, EVENT_MESSAGE
class WsClient:
"""WebSocket 客户端连接"""
def __init__(self, websocket, path: str):
self.websocket = websocket
self.path = path
self.id = id(websocket)
self.closed = False
async def send(self, message: Any):
"""发送消息"""
if not self.closed:
data = json.dumps(message, ensure_ascii=False) if isinstance(message, dict) else str(message)
await self.websocket.send(data)
async def close(self):
"""关闭连接"""
self.closed = True
await self.websocket.close()
class WsServer:
"""WebSocket 服务器"""
def __init__(self, router, middleware, event_bus, host="0.0.0.0", port=8081):
self.host = host
self.port = port
self.router = router
self.middleware = middleware
self.event_bus = event_bus
self._server = None
self._loop = None
self._thread = None
self._clients: dict[int, WsClient] = {}
def start(self):
"""启动服务器"""
self._loop = asyncio.new_event_loop()
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
def _run_loop(self):
"""运行事件循环"""
asyncio.set_event_loop(self._loop)
start_server = websockets.serve(
self._handle_connection,
self.host,
self.port
)
self._loop.run_until_complete(start_server)
self._loop.run_forever()
async def _handle_connection(self, websocket, path=None):
"""处理客户端连接(兼容 websockets 新旧版本)"""
# websockets 16.0+ 只传入 connection 参数
if path is None:
# 新版本:从 websocket.request 获取路径
try:
path = websocket.request.path
except AttributeError:
path = "/"
client = WsClient(websocket, path)
self._clients[client.id] = client
# 触发连接事件
self.event_bus.emit(WsEvent(
type=EVENT_CONNECT,
client=client,
path=path
))
try:
async for message in websocket:
# 触发消息事件
self.event_bus.emit(WsEvent(
type=EVENT_MESSAGE,
client=client,
path=path,
message=message
))
# 路由处理
await self.router.handle(client, path, message)
except websockets.exceptions.ConnectionClosed:
pass
finally:
del self._clients[client.id]
# 触发断开事件
self.event_bus.emit(WsEvent(
type=EVENT_DISCONNECT,
client=client,
path=path
))
def stop(self):
"""停止服务器"""
if self._loop and self._loop.is_running():
self._loop.call_soon_threadsafe(self._loop.stop)
print("[ws-api] 服务器已停止")
def broadcast(self, message: Any, exclude_client: int = None):
"""广播消息"""
async def _broadcast():
for client_id, client in self._clients.items():
if exclude_client and client_id == exclude_client:
continue
await client.send(message)
if self._loop and self._loop.is_running():
asyncio.run_coroutine_threadsafe(_broadcast(), self._loop)
def get_clients(self) -> list[WsClient]:
"""获取所有客户端"""
return list(self._clients.values())