修复了一些错误 更新了AI.md(给ai看的)
This commit is contained in:
@@ -1,29 +1,3 @@
|
||||
"""Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)"""
|
||||
import json
|
||||
import sys
|
||||
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 _Log:
|
||||
_TTY = sys.stdout.isatty()
|
||||
_C = {"reset": "\033[0m", "white": "\033[0;37m", "yellow": "\033[1;33m", "blue": "\033[1;34m", "red": "\033[1;31m"}
|
||||
@classmethod
|
||||
def _c(cls, t, c):
|
||||
return f"{cls._C.get(c,'')}{t}{cls._C['reset']}" if cls._TTY else t
|
||||
@classmethod
|
||||
def info(cls, m): print(f"{cls._c('[web-toolkit]', 'white')} {cls._c(m, 'white')}")
|
||||
@classmethod
|
||||
def warn(cls, m): print(f"{cls._c('[web-toolkit]', 'yellow')} {cls._c('⚠', 'yellow')} {cls._c(m, 'yellow')}")
|
||||
@classmethod
|
||||
def error(cls, m): print(f"{cls._c('[web-toolkit]', 'red')} {cls._c('✗', 'red')} {cls._c(m, 'red')}")
|
||||
|
||||
|
||||
class WebToolkitPlugin(Plugin):
|
||||
"""Web 工具包插件 - 提供网站前端所有服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.router = None
|
||||
@@ -32,24 +6,12 @@ class WebToolkitPlugin(Plugin):
|
||||
self.http_api = None
|
||||
self.http_tcp = None
|
||||
self.storage = None
|
||||
self.config = {} # 从 config.json 读取
|
||||
self.root_dir = None
|
||||
self.config = {} 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))
|
||||
_Log.info(f"配置加载完成: 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
|
||||
@@ -63,7 +25,6 @@ class WebToolkitPlugin(Plugin):
|
||||
self._serve_static
|
||||
)
|
||||
|
||||
# 注册路由到 http-tcp
|
||||
if self.http_tcp:
|
||||
tcp_instance = self.http_tcp
|
||||
if hasattr(tcp_instance, "router"):
|
||||
@@ -83,59 +44,17 @@ class WebToolkitPlugin(Plugin):
|
||||
_Log.info("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():
|
||||
_Log.warn("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
|
||||
@@ -149,29 +68,3 @@ class WebToolkitPlugin(Plugin):
|
||||
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 ".." in filename or filename.startswith("/"):
|
||||
return Response(status=403, body="Forbidden")
|
||||
|
||||
if not filename:
|
||||
return self._serve_website_index(request)
|
||||
return self.static_handler.serve(filename)
|
||||
|
||||
|
||||
register_plugin_type("WebToolkitPlugin", WebToolkitPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return WebToolkitPlugin()
|
||||
|
||||
@@ -1,21 +1,2 @@
|
||||
"""Web 路由器"""
|
||||
from typing import Callable, Optional, Any
|
||||
from oss.shared.router import BaseRouter, match_path
|
||||
|
||||
|
||||
class WebRouter(BaseRouter):
|
||||
"""Web 路由器"""
|
||||
|
||||
def handle(self, request: dict) -> Optional[Any]:
|
||||
"""处理请求"""
|
||||
method = request.get("method", "GET")
|
||||
path = request.get("path", "/")
|
||||
|
||||
result = self.find_route(method, path)
|
||||
if result:
|
||||
route, params = result
|
||||
# 将路径参数注入到请求中
|
||||
request["path_params"] = params
|
||||
return route.handler(request)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,68 +1,13 @@
|
||||
"""静态文件处理器"""
|
||||
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 []
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
"""模板引擎"""
|
||||
import re
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class TemplateEngine:
|
||||
"""简单模板引擎"""
|
||||
|
||||
def __init__(self, root: str = "./templates", max_depth: int = 10):
|
||||
self.root = root
|
||||
@@ -15,22 +6,11 @@ class TemplateEngine:
|
||||
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, depth=0)
|
||||
|
||||
def _load_template(self, name: str) -> str:
|
||||
"""加载模板"""
|
||||
if name in self._cache:
|
||||
return self._cache[name]
|
||||
|
||||
@@ -43,24 +23,6 @@ class TemplateEngine:
|
||||
return content
|
||||
|
||||
def _safe_eval(self, expression: str, context: dict) -> Any:
|
||||
"""安全评估表达式(使用 AST 验证,不使用 eval)"""
|
||||
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
|
||||
|
||||
# 使用安全的 AST 解释器,不使用 eval
|
||||
try:
|
||||
return self._eval_ast(tree.body[0].value, context)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _eval_ast(self, node: ast.AST, context: dict) -> Any:
|
||||
"""安全地评估 AST 节点"""
|
||||
if isinstance(node, ast.Constant):
|
||||
return node.value
|
||||
elif isinstance(node, ast.Name):
|
||||
@@ -80,31 +42,6 @@ class TemplateEngine:
|
||||
return False
|
||||
|
||||
def _eval_compare(self, node: ast.Compare, context: dict) -> bool:
|
||||
"""评估比较表达式"""
|
||||
left = self._eval_ast(node.left, context)
|
||||
for op, comp in zip(node.ops, node.comparators):
|
||||
right = self._eval_ast(comp, context)
|
||||
if isinstance(op, ast.Eq):
|
||||
if not (left == right): return False
|
||||
elif isinstance(op, ast.NotEq):
|
||||
if not (left != right): return False
|
||||
elif isinstance(op, ast.Lt):
|
||||
if not (left < right): return False
|
||||
elif isinstance(op, ast.Gt):
|
||||
if not (left > right): return False
|
||||
elif isinstance(op, ast.LtE):
|
||||
if not (left <= right): return False
|
||||
elif isinstance(op, ast.GtE):
|
||||
if not (left >= right): return False
|
||||
elif isinstance(op, ast.In):
|
||||
if not (left in right): return False
|
||||
elif isinstance(op, ast.NotIn):
|
||||
if not (left not in right): return False
|
||||
left = right
|
||||
return True
|
||||
|
||||
def _eval_subscript(self, node: ast.Subscript, context: dict) -> Any:
|
||||
"""评估下标访问"""
|
||||
value = self._eval_ast(node.value, context)
|
||||
key = self._eval_ast(node.slice, context)
|
||||
if isinstance(value, (dict, list, str)):
|
||||
@@ -112,32 +49,6 @@ class TemplateEngine:
|
||||
return None
|
||||
|
||||
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], depth: int = 0) -> str:
|
||||
"""渲染模板内容
|
||||
|
||||
Args:
|
||||
template: 模板内容
|
||||
@@ -146,13 +57,11 @@ class TemplateEngine:
|
||||
|
||||
Raises:
|
||||
RecursionError: 当嵌套深度超过 max_depth 时
|
||||
"""
|
||||
if depth > self.max_depth:
|
||||
raise RecursionError(
|
||||
f"模板嵌套深度超过限制 ({self.max_depth}),可能存在无限递归"
|
||||
)
|
||||
|
||||
# 替换 {{ variable }}
|
||||
def replace_var(match):
|
||||
var_name = match.group(1).strip()
|
||||
value = context.get(var_name, "")
|
||||
@@ -163,32 +72,13 @@ class TemplateEngine:
|
||||
|
||||
result = re.sub(r'\{\{(.*?)\}\}', replace_var, template)
|
||||
|
||||
# 处理 {% if condition %} ... {% endif %}
|
||||
result = self._process_if(result, context, depth)
|
||||
|
||||
# 处理 {% for item in list %} ... {% endfor %}
|
||||
result = self._process_for(result, context, depth)
|
||||
|
||||
return result
|
||||
|
||||
def _process_if(self, template: str, context: dict, depth: int = 0) -> 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)
|
||||
if value:
|
||||
# 递归处理嵌套内容,深度+1
|
||||
return self._render_template(content, context, depth + 1)
|
||||
return ""
|
||||
|
||||
return re.sub(pattern, replace_if, template, flags=re.DOTALL)
|
||||
|
||||
def _process_for(self, template: str, context: dict, depth: int = 0) -> str:
|
||||
"""处理 for 循环"""
|
||||
pattern = r'\{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%\}(.*?){%\s*endfor\s*%\}'
|
||||
|
||||
def replace_for(match):
|
||||
@@ -203,7 +93,6 @@ class TemplateEngine:
|
||||
result = ""
|
||||
for item in items:
|
||||
loop_context = {**context, item_name: item}
|
||||
# 递归处理嵌套内容,深度+1
|
||||
result += self._render_template(content, loop_context, depth + 1)
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user