⚡ 初始提交 - FutureOSS v1.0 插件化运行时框架
一切皆为插件的开发者工具运行时框架
🧩 核心特性:
- 插件热插拔 (importlib 动态加载)
- 依赖自动解析 (拓扑排序 + 循环检测)
- 企业级稳定 (熔断/降级/重试/隔离)
- 事件驱动 (发布/订阅事件总线)
- 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
71
store/@{Falck}/web-toolkit/README.md
Normal file
71
store/@{Falck}/web-toolkit/README.md
Normal 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-api:HTTP 服务
|
||||
- http-tcp:TCP HTTP 服务
|
||||
BIN
store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc
Normal file
BIN
store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc
Normal file
Binary file not shown.
158
store/@{Falck}/web-toolkit/main.py
Normal file
158
store/@{Falck}/web-toolkit/main.py
Normal 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()
|
||||
21
store/@{Falck}/web-toolkit/manifest.json
Normal file
21
store/@{Falck}/web-toolkit/manifest.json
Normal 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"]
|
||||
}
|
||||
63
store/@{Falck}/web-toolkit/router.py
Normal file
63
store/@{Falck}/web-toolkit/router.py
Normal 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
|
||||
69
store/@{Falck}/web-toolkit/static.py
Normal file
69
store/@{Falck}/web-toolkit/static.py
Normal 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()]
|
||||
144
store/@{Falck}/web-toolkit/template.py
Normal file
144
store/@{Falck}/web-toolkit/template.py
Normal 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)
|
||||
Reference in New Issue
Block a user