初始提交 - 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,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)