diff --git a/README.md b/README.md
index bd6423b..86b64d1 100644
--- a/README.md
+++ b/README.md
@@ -36,18 +36,18 @@ FutureOSS/
-所有文档都在本地 `dock/` 目录中:
+完整开发者文档请查阅 [项目 Wiki](https://gitee.com/starlight-apk/feature-oss/wikis):
| 📘 页面 | 📝 内容 |
|:---:|:---|
-| [🎯 项目介绍](./dock/00-项目介绍/) | 什么是 FutureOSS、架构设计、核心概念 |
-| [🚀 快速开始](./dock/01-快速开始/) | 安装、配置、第一次运行 |
-| [🔌 插件开发](./dock/02-插件开发/) | 编写你的第一个插件、事件系统 |
-| [📄 插件文档](./dock/03-插件文档/) | http-api、ws-api、file 插件详解 |
-| [📦 包管理](./dock/04-包管理/) | 安装/卸载/搜索/发布插件 |
-| [⚙️ 配置参考](./dock/05-配置参考/) | 配置参数详解 |
-| [🚢 部署运维](./dock/06-部署运维/) | 本地运行、Docker、生产环境 |
-| [🌟 社区与贡献](./dock/07-社区与贡献/) | 贡献指南、行为准则 |
+| [🎯 项目介绍](https://gitee.com/starlight-apk/feature-oss/wikis/项目介绍) | 什么是 FutureOSS、架构设计、核心概念 |
+| [🚀 快速开始](https://gitee.com/starlight-apk/feature-oss/wikis/快速开始) | 安装、配置、第一次运行 |
+| [🔌 插件开发](https://gitee.com/starlight-apk/feature-oss/wikis/插件开发) | 编写你的第一个插件、事件系统 |
+| [📄 插件文档](https://gitee.com/starlight-apk/feature-oss/wikis/插件文档) | http-api、ws-api、file 插件详解 |
+| [📦 包管理](https://gitee.com/starlight-apk/feature-oss/wikis/包管理) | 安装/卸载/搜索/发布插件 |
+| [⚙️ 配置参考](https://gitee.com/starlight-apk/feature-oss/wikis/配置参考) | 配置参数详解 |
+| [🚢 部署运维](https://gitee.com/starlight-apk/feature-oss/wikis/部署运维) | 本地运行、Docker、生产环境 |
+| [🌟 社区与贡献](https://gitee.com/starlight-apk/feature-oss/wikis/社区与贡献) | 贡献指南、行为准则 |
diff --git a/oss/shared/__init__.py b/oss/shared/__init__.py
new file mode 100644
index 0000000..f2c7d55
--- /dev/null
+++ b/oss/shared/__init__.py
@@ -0,0 +1,4 @@
+"""共享工具模块"""
+from .router import BaseRoute, BaseRouter, match_path, extract_path_params
+
+__all__ = ["BaseRoute", "BaseRouter", "match_path", "extract_path_params"]
diff --git a/oss/shared/__pycache__/router.cpython-313.pyc b/oss/shared/__pycache__/router.cpython-313.pyc
new file mode 100644
index 0000000..f20d836
Binary files /dev/null and b/oss/shared/__pycache__/router.cpython-313.pyc differ
diff --git a/oss/shared/router.py b/oss/shared/router.py
new file mode 100644
index 0000000..172859a
--- /dev/null
+++ b/oss/shared/router.py
@@ -0,0 +1,136 @@
+"""共享路由工具函数"""
+from typing import Callable, Optional, Any
+
+
+class BaseRoute:
+ """路由定义基类"""
+ def __init__(self, method: str, path: str, handler: Callable):
+ self.method = method
+ self.path = path
+ self.handler = handler
+
+
+def match_path(pattern: str, path: str) -> bool:
+ """路径匹配
+
+ 支持:
+ - 精确匹配: /api/users == /api/users
+ - 参数匹配: /api/users/:id 匹配 /api/users/123
+ - 通配符匹配: /api/:path 匹配 /api/users/123/profile
+
+ Args:
+ pattern: 路由模式 (如 /api/users/:id)
+ path: 实际请求路径 (如 /api/users/123)
+
+ Returns:
+ 是否匹配成功
+ """
+ if pattern == path:
+ return True
+
+ if ":" not in pattern:
+ return False
+
+ pattern_parts = pattern.strip("/").split("/")
+ path_parts = path.strip("/").split("/")
+
+ # 如果最后一个 pattern 是 :path(通配符),允许更多路径段
+ last_pattern = pattern_parts[-1]
+ if last_pattern.startswith(":") and len(path_parts) >= len(pattern_parts):
+ # 检查前面的段是否匹配
+ for i, p in enumerate(pattern_parts[:-1]):
+ if i >= len(path_parts):
+ return False
+ if not p.startswith(":") and p != path_parts[i]:
+ return False
+ return True
+
+ # 普通参数匹配,段数必须相同
+ 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
+
+
+def extract_path_params(pattern: str, path: str) -> dict[str, str]:
+ """从路径中提取参数
+
+ Args:
+ pattern: 路由模式 (如 /api/users/:id)
+ path: 实际请求路径 (如 /api/users/123)
+
+ Returns:
+ 参数字典 (如 {"id": "123"})
+ """
+ params = {}
+
+ if ":" not in pattern:
+ return params
+
+ pattern_parts = pattern.strip("/").split("/")
+ path_parts = path.strip("/").split("/")
+
+ for p, a in zip(pattern_parts, path_parts):
+ if p.startswith(":"):
+ param_name = p[1:] # 去掉 :
+ params[param_name] = a
+
+ # 处理通配符 :path
+ last_pattern = pattern_parts[-1]
+ if last_pattern.startswith(":") and len(path_parts) > len(pattern_parts):
+ param_name = last_pattern[1:]
+ # 将剩余的路径段合并
+ remaining = "/".join(path_parts[len(pattern_parts) - 1:])
+ params[param_name] = remaining
+
+ return params
+
+
+class BaseRouter:
+ """路由器基类
+
+ 提供通用的路由注册和匹配功能,子类只需实现 handle() 方法
+ """
+
+ def __init__(self):
+ self.routes: list[BaseRoute] = []
+
+ def add(self, method: str, path: str, handler: Callable):
+ """添加路由"""
+ self.routes.append(BaseRoute(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 find_route(self, method: str, path: str) -> Optional[tuple[BaseRoute, dict[str, str]]]:
+ """查找匹配的路由和路径参数
+
+ Args:
+ method: HTTP 方法
+ path: 请求路径
+
+ Returns:
+ (路由, 路径参数) 或 None
+ """
+ for route in self.routes:
+ if route.method == method and match_path(route.path, path):
+ params = extract_path_params(route.path, path)
+ return route, params
+ return None
diff --git a/static/banner.svg b/static/banner.svg
index 66f457c..81860ae 100644
--- a/static/banner.svg
+++ b/static/banner.svg
@@ -269,32 +269,32 @@
-
所有文档都在本地 dock/ 目录中
+
完整文档请访问项目 Wiki
-
+
-
项目介绍
+
Wiki/项目介绍
-
快速开始
+
Wiki/快速开始
-
插件开发
+
Wiki/插件开发
-
插件文档
+
Wiki/插件文档
-
包管理
+
Wiki/包管理
-
配置参考
+
Wiki/配置参考
-
部署运维
+
Wiki/部署运维
-
社区与贡献
+
Wiki/社区与贡献
diff --git a/store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc
index 7faa2ea..a02fe57 100644
Binary files a/store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc and b/store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc differ
diff --git a/store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc
index d3cd630..0dac3c0 100644
Binary files a/store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc and b/store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc differ
diff --git a/store/@{Falck}/web-toolkit/router.py b/store/@{Falck}/web-toolkit/router.py
index 7fbb571..72b3122 100644
--- a/store/@{Falck}/web-toolkit/router.py
+++ b/store/@{Falck}/web-toolkit/router.py
@@ -1,63 +1,21 @@
"""Web 路由器"""
from typing import Callable, Optional, Any
+from oss.shared.router import BaseRouter, match_path
-class WebRoute:
- """Web 路由"""
- def __init__(self, method: str, path: str, handler: Callable):
- self.method = method
- self.path = path
- self.handler = handler
-
-
-class WebRouter:
+class WebRouter(BaseRouter):
"""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)
+
+ result = self.find_route(method, path)
+ if result:
+ route, params = result
+ # 将路径参数注入到请求中
+ request["path_params"] = params
+ 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
diff --git a/store/@{Falck}/web-toolkit/template.py b/store/@{Falck}/web-toolkit/template.py
index 4b126f8..55f2a39 100644
--- a/store/@{Falck}/web-toolkit/template.py
+++ b/store/@{Falck}/web-toolkit/template.py
@@ -8,9 +8,10 @@ from typing import Any, Optional
class TemplateEngine:
"""简单模板引擎"""
- def __init__(self, root: str = "./templates"):
+ def __init__(self, root: str = "./templates", max_depth: int = 10):
self.root = root
self._cache: dict[str, str] = {}
+ self.max_depth = max_depth
self._ensure_root()
def _ensure_root(self):
@@ -26,7 +27,7 @@ class TemplateEngine:
def render(self, name: str, context: dict[str, Any]) -> str:
"""渲染模板"""
template = self._load_template(name)
- return self._render_template(template, context)
+ return self._render_template(template, context, depth=0)
def _load_template(self, name: str) -> str:
"""加载模板"""
@@ -88,8 +89,22 @@ class TemplateEngine:
self._validate_ast(node.slice, allowed_names))
return False
- def _render_template(self, template: str, context: dict[str, Any]) -> str:
- """渲染模板内容"""
+ def _render_template(self, template: str, context: dict[str, Any], depth: int = 0) -> str:
+ """渲染模板内容
+
+ Args:
+ template: 模板内容
+ context: 上下文变量
+ depth: 当前递归深度
+
+ 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()
@@ -102,14 +117,14 @@ class TemplateEngine:
result = re.sub(r'\{\{(.*?)\}\}', replace_var, template)
# 处理 {% if condition %} ... {% endif %}
- result = self._process_if(result, context)
+ result = self._process_if(result, context, depth)
# 处理 {% for item in list %} ... {% endfor %}
- result = self._process_for(result, context)
+ result = self._process_for(result, context, depth)
return result
- def _process_if(self, template: str, context: dict) -> str:
+ def _process_if(self, template: str, context: dict, depth: int = 0) -> str:
"""处理 if 条件"""
pattern = r'\{%\s*if\s+(.*?)\s*%\}(.*?){%\s*endif\s*%\}'
@@ -118,11 +133,14 @@ class TemplateEngine:
content = match.group(2)
# 安全条件评估
value = self._safe_eval(condition, context)
- return content if value else ""
+ 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) -> str:
+ 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*%\}'
@@ -138,7 +156,8 @@ class TemplateEngine:
result = ""
for item in items:
loop_context = {**context, item_name: item}
- result += self._render_template(content, loop_context)
+ # 递归处理嵌套内容,深度+1
+ result += self._render_template(content, loop_context, depth + 1)
return result
return re.sub(pattern, replace_for, template, flags=re.DOTALL)
diff --git a/store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc b/store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc
index ee0b2dc..0de4be3 100644
Binary files a/store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc and b/store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/http-api/router.py b/store/@{FutureOSS}/http-api/router.py
index 7c5b40b..dcb512d 100644
--- a/store/@{FutureOSS}/http-api/router.py
+++ b/store/@{FutureOSS}/http-api/router.py
@@ -1,72 +1,18 @@
"""路由器 - 路径匹配和处理器分发"""
from typing import Callable, Optional
+from oss.shared.router import BaseRouter, match_path
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)
+class Router(BaseRouter):
+ """HTTP API 路由器"""
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)
+ result = self.find_route(request.method, request.path)
+ if result:
+ route, params = result
+ # 将路径参数注入到请求中
+ request.path_params = params
+ 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
diff --git a/store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc b/store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc
index 33a8bc1..50c6c62 100644
Binary files a/store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc and b/store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc b/store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc
index 7a2d158..6d65f99 100644
Binary files a/store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc and b/store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/http-tcp/router.py b/store/@{FutureOSS}/http-tcp/router.py
index 6f4b66c..31d9581 100644
--- a/store/@{FutureOSS}/http-tcp/router.py
+++ b/store/@{FutureOSS}/http-tcp/router.py
@@ -1,63 +1,21 @@
"""TCP HTTP 路由器"""
from typing import Callable, Optional, Any
+from oss.shared.router import BaseRouter, match_path
-class TcpRoute:
- """TCP HTTP 路由"""
- def __init__(self, method: str, path: str, handler: Callable):
- self.method = method
- self.path = path
- self.handler = handler
-
-
-class TcpRouter:
+class TcpRouter(BaseRouter):
"""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)
+
+ result = self.find_route(method, path)
+ if result:
+ route, params = result
+ # 将路径参数注入到请求中
+ request["path_params"] = params
+ 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
diff --git a/store/@{FutureOSS}/http-tcp/server.py b/store/@{FutureOSS}/http-tcp/server.py
index 1d16515..7598762 100644
--- a/store/@{FutureOSS}/http-tcp/server.py
+++ b/store/@{FutureOSS}/http-tcp/server.py
@@ -77,8 +77,35 @@ class TcpHttpServer:
break
buffer += data
- # 检查 HTTP 请求是否完整
+ # 检查 HTTP 请求头是否完整
if b"\r\n\r\n" in buffer:
+ # 先解析请求头以获取 Content-Length
+ header_end = buffer.find(b"\r\n\r\n")
+ header_text = buffer[:header_end].decode("utf-8", errors="replace")
+
+ # 从请求头中提取 Content-Length
+ content_length = 0
+ for line in header_text.split("\r\n")[1:]:
+ if line.lower().startswith("content-length:"):
+ content_length = int(line.split(":", 1)[1].strip())
+ break
+
+ # 计算 body 起始位置
+ body_start_pos = header_end + 4 # \r\n\r\n
+ body_received = len(buffer) - body_start_pos
+
+ # 等待完整 body
+ if body_received < content_length:
+ # 继续接收剩余数据
+ while body_received < content_length:
+ remaining = content_length - body_received
+ chunk = client.conn.recv(min(4096, remaining))
+ if not chunk:
+ break
+ buffer += chunk
+ body_received += len(chunk)
+
+ # 现在解析完整请求
request = self._parse_request(buffer)
if request:
# 触发请求事件
diff --git a/store/@{FutureOSS}/i18n/__init__.py b/store/@{FutureOSS}/i18n/__init__.py
new file mode 100644
index 0000000..aa29a23
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/__init__.py
@@ -0,0 +1 @@
+"""i18n 国际化多语言支持插件"""
diff --git a/store/@{FutureOSS}/i18n/__pycache__/i18n.cpython-313.pyc b/store/@{FutureOSS}/i18n/__pycache__/i18n.cpython-313.pyc
new file mode 100644
index 0000000..5160e73
Binary files /dev/null and b/store/@{FutureOSS}/i18n/__pycache__/i18n.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/i18n/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/i18n/__pycache__/main.cpython-313.pyc
new file mode 100644
index 0000000..1c8ce20
Binary files /dev/null and b/store/@{FutureOSS}/i18n/__pycache__/main.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/i18n/__pycache__/middleware.cpython-313.pyc b/store/@{FutureOSS}/i18n/__pycache__/middleware.cpython-313.pyc
new file mode 100644
index 0000000..6606d8a
Binary files /dev/null and b/store/@{FutureOSS}/i18n/__pycache__/middleware.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/i18n/i18n.py b/store/@{FutureOSS}/i18n/i18n.py
new file mode 100644
index 0000000..befbaa5
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/i18n.py
@@ -0,0 +1,156 @@
+"""i18n 核心引擎"""
+import json
+import re
+from pathlib import Path
+from typing import Any, Optional
+
+
+class I18nEngine:
+ """国际化引擎"""
+
+ def __init__(self):
+ self._translations: dict[str, dict[str, Any]] = {} # {locale: {key: value}}
+ self._current_locale: str = "zh-CN"
+ self._fallback_locale: str = "en-US"
+ self._supported_locales: list[str] = []
+ self._locales_dir: str = ""
+
+ def load_locales(self, locales_dir: str, locales: list[str]):
+ """加载语言文件
+
+ Args:
+ locales_dir: 语言文件目录路径
+ locales: 支持的语言列表
+ """
+ self._locales_dir = locales_dir
+ self._supported_locales = locales
+ locales_path = Path(locales_dir)
+
+ if not locales_path.exists():
+ locales_path.mkdir(parents=True, exist_ok=True)
+ return
+
+ for locale in locales:
+ locale_file = locales_path / f"{locale}.json"
+ if locale_file.exists():
+ try:
+ content = locale_file.read_text(encoding="utf-8")
+ self._translations[locale] = json.loads(content)
+ except (json.JSONDecodeError, Exception) as e:
+ print(f"[i18n] 加载语言文件失败 {locale_file}: {e}")
+ self._translations[locale] = {}
+
+ def set_locale(self, locale: str):
+ """设置当前语言"""
+ if locale in self._supported_locales:
+ self._current_locale = locale
+
+ def get_locale(self) -> str:
+ """获取当前语言"""
+ return self._current_locale
+
+ def set_fallback(self, locale: str):
+ """设置回退语言"""
+ self._fallback_locale = locale
+
+ def t(self, key: str, locale: Optional[str] = None, **kwargs) -> str:
+ """翻译文本
+
+ Args:
+ key: 翻译键 (支持点号分隔的嵌套路径,如 "user.greeting")
+ locale: 指定语言 (默认使用当前语言)
+ **kwargs: 插值参数
+
+ Returns:
+ 翻译后的文本
+ """
+ target_locale = locale or self._current_locale
+
+ # 尝试从指定语言获取
+ value = self._get_nested(key, self._translations.get(target_locale, {}))
+
+ # 如果未找到,尝试从回退语言获取
+ if value is None and target_locale != self._fallback_locale:
+ value = self._get_nested(key, self._translations.get(self._fallback_locale, {}))
+
+ # 仍未找到,返回键名
+ if value is None:
+ return key
+
+ # 插值处理: {{name}} 或 {name}
+ return self._interpolate(value, kwargs)
+
+ def _get_nested(self, key: str, data: dict) -> Any:
+ """获取嵌套字典值"""
+ keys = key.split(".")
+ current = data
+ for k in keys:
+ if isinstance(current, dict) and k in current:
+ current = current[k]
+ else:
+ return None
+ return current
+
+ def _interpolate(self, text: str, kwargs: dict) -> str:
+ """插值替换: {{name}} 或 {name}"""
+ # 支持 {{name}} 格式
+ result = re.sub(r'\{\{(\w+)\}\}', lambda m: str(kwargs.get(m.group(1), m.group(0))), text)
+ # 支持 {name} 格式 (如果未被 {{}} 替换)
+ result = re.sub(r'\{(\w+)\}', lambda m: str(kwargs.get(m.group(1), m.group(0))), result)
+ return result
+
+ def get_supported_locales(self) -> list[str]:
+ """获取支持的语言列表"""
+ return self._supported_locales
+
+ def is_valid_locale(self, locale: str) -> bool:
+ """检查语言是否有效"""
+ return locale in self._supported_locales
+
+ def detect_locale(self, accept_language: Optional[str] = None,
+ query_lang: Optional[str] = None,
+ cookie_lang: Optional[str] = None) -> str:
+ """检测语言优先级
+
+ Args:
+ accept_language: HTTP Accept-Language 头
+ query_lang: URL 查询参数 ?lang=xx
+ cookie_lang: Cookie 中的语言
+
+ Returns:
+ 检测到的语言代码
+ """
+ # 1. 查询参数优先级最高
+ if query_lang and self.is_valid_locale(query_lang):
+ return query_lang
+
+ # 2. Cookie 次之
+ if cookie_lang and self.is_valid_locale(cookie_lang):
+ return cookie_lang
+
+ # 3. Accept-Language 头
+ if accept_language:
+ # 解析 "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7"
+ languages = []
+ for part in accept_language.split(","):
+ part = part.strip()
+ if ";q=" in part:
+ lang, q = part.split(";q=")
+ languages.append((lang.strip(), float(q)))
+ else:
+ languages.append((part, 1.0))
+
+ # 按权重排序
+ languages.sort(key=lambda x: x[1], reverse=True)
+
+ for lang, _ in languages:
+ # 精确匹配
+ if self.is_valid_locale(lang):
+ return lang
+ # 前缀匹配 (zh 匹配 zh-CN, zh-TW)
+ for supported in self._supported_locales:
+ if supported.startswith(lang + "-") or lang.startswith(supported.split("-")[0] + "-"):
+ return supported
+
+ # 4. 默认语言
+ return self._current_locale
diff --git a/store/@{FutureOSS}/i18n/locales/en-US.json b/store/@{FutureOSS}/i18n/locales/en-US.json
new file mode 100644
index 0000000..959d446
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/locales/en-US.json
@@ -0,0 +1,51 @@
+{
+ "common": {
+ "success": "Success",
+ "error": "Error",
+ "not_found": "Not Found",
+ "forbidden": "Forbidden",
+ "unauthorized": "Unauthorized",
+ "server_error": "Internal Server Error",
+ "bad_request": "Bad Request",
+ "ok": "OK",
+ "cancel": "Cancel",
+ "save": "Save",
+ "delete": "Delete",
+ "edit": "Edit",
+ "create": "Create",
+ "search": "Search",
+ "loading": "Loading...",
+ "no_data": "No Data",
+ "confirm": "Confirm",
+ "back": "Back"
+ },
+ "health": {
+ "status": "Running",
+ "service": "Service",
+ "version": "Version",
+ "uptime": "Uptime"
+ },
+ "api": {
+ "welcome": "Welcome to FutureOSS API",
+ "docs": "API Documentation",
+ "rate_limit": "Rate limit exceeded, please try again later",
+ "invalid_request": "Invalid request parameters",
+ "missing_param": "Missing required parameter: {{param}}",
+ "invalid_param": "Invalid parameter format: {{param}}"
+ },
+ "errors": {
+ "400": "Bad Request",
+ "401": "Please login first",
+ "403": "You don't have permission to perform this action",
+ "404": "The requested resource was not found",
+ "500": "Internal server error, please try again later",
+ "502": "Bad Gateway",
+ "503": "Service temporarily unavailable, please try again later"
+ },
+ "plugin": {
+ "i18n_name": "Internationalization",
+ "i18n_desc": "Provides translation loading, language detection, and HTTP middleware",
+ "locale_changed": "Locale changed to {{locale}}",
+ "locale_not_supported": "Unsupported locale: {{locale}}"
+ }
+}
diff --git a/store/@{FutureOSS}/i18n/locales/zh-CN.json b/store/@{FutureOSS}/i18n/locales/zh-CN.json
new file mode 100644
index 0000000..0f97598
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/locales/zh-CN.json
@@ -0,0 +1,51 @@
+{
+ "common": {
+ "success": "成功",
+ "error": "错误",
+ "not_found": "未找到",
+ "forbidden": "禁止访问",
+ "unauthorized": "未授权",
+ "server_error": "服务器内部错误",
+ "bad_request": "请求格式错误",
+ "ok": "确定",
+ "cancel": "取消",
+ "save": "保存",
+ "delete": "删除",
+ "edit": "编辑",
+ "create": "创建",
+ "search": "搜索",
+ "loading": "加载中...",
+ "no_data": "暂无数据",
+ "confirm": "确认",
+ "back": "返回"
+ },
+ "health": {
+ "status": "运行正常",
+ "service": "服务",
+ "version": "版本",
+ "uptime": "运行时间"
+ },
+ "api": {
+ "welcome": "欢迎使用 FutureOSS API",
+ "docs": "API 文档",
+ "rate_limit": "请求频率过高,请稍后重试",
+ "invalid_request": "无效的请求参数",
+ "missing_param": "缺少必需参数: {{param}}",
+ "invalid_param": "参数格式错误: {{param}}"
+ },
+ "errors": {
+ "400": "请求格式错误",
+ "401": "请先登录",
+ "403": "您没有权限执行此操作",
+ "404": "请求的资源不存在",
+ "500": "服务器内部错误,请稍后重试",
+ "502": "网关错误",
+ "503": "服务暂时不可用,请稍后重试"
+ },
+ "plugin": {
+ "i18n_name": "国际化多语言支持",
+ "i18n_desc": "提供翻译加载、语言检测和 HTTP 中间件功能",
+ "locale_changed": "语言已切换为 {{locale}}",
+ "locale_not_supported": "不支持的语言: {{locale}}"
+ }
+}
diff --git a/store/@{FutureOSS}/i18n/locales/zh-TW.json b/store/@{FutureOSS}/i18n/locales/zh-TW.json
new file mode 100644
index 0000000..f7cddb3
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/locales/zh-TW.json
@@ -0,0 +1,51 @@
+{
+ "common": {
+ "success": "成功",
+ "error": "錯誤",
+ "not_found": "找不到",
+ "forbidden": "禁止存取",
+ "unauthorized": "未授權",
+ "server_error": "伺服器內部錯誤",
+ "bad_request": "請求格式錯誤",
+ "ok": "確定",
+ "cancel": "取消",
+ "save": "儲存",
+ "delete": "刪除",
+ "edit": "編輯",
+ "create": "建立",
+ "search": "搜尋",
+ "loading": "載入中...",
+ "no_data": "暫無資料",
+ "confirm": "確認",
+ "back": "返回"
+ },
+ "health": {
+ "status": "運作正常",
+ "service": "服務",
+ "version": "版本",
+ "uptime": "運行時間"
+ },
+ "api": {
+ "welcome": "歡迎使用 FutureOSS API",
+ "docs": "API 文件",
+ "rate_limit": "請求頻率過高,請稍後重試",
+ "invalid_request": "無效的請求參數",
+ "missing_param": "缺少必要參數: {{param}}",
+ "invalid_param": "參數格式錯誤: {{param}}"
+ },
+ "errors": {
+ "400": "請求格式錯誤",
+ "401": "請先登入",
+ "403": "您沒有權限執行此操作",
+ "404": "請求的資源不存在",
+ "500": "伺服器內部錯誤,請稍後重試",
+ "502": "閘道錯誤",
+ "503": "服務暫時不可用,請稍後重試"
+ },
+ "plugin": {
+ "i18n_name": "國際化多語言支援",
+ "i18n_desc": "提供翻譯載入、語言偵測和 HTTP 中介軟體功能",
+ "locale_changed": "語言已切換為 {{locale}}",
+ "locale_not_supported": "不支援的語言: {{locale}}"
+ }
+}
diff --git a/store/@{FutureOSS}/i18n/main.py b/store/@{FutureOSS}/i18n/main.py
new file mode 100644
index 0000000..ca7f2d9
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/main.py
@@ -0,0 +1,215 @@
+"""i18n 国际化多语言支持插件"""
+import json
+from pathlib import Path
+from oss.plugin.types import Plugin, register_plugin_type
+from .i18n import I18nEngine
+from .middleware import I18nMiddleware
+
+
+class I18nPlugin(Plugin):
+ """i18n 国际化插件"""
+
+ def __init__(self):
+ self.engine = I18nEngine()
+ self.middleware_handler = None
+
+ def meta(self):
+ """插件元数据"""
+ from oss.plugin.types import Metadata, PluginConfig, Manifest
+ return Manifest(
+ metadata=Metadata(
+ name="i18n",
+ version="1.0.0",
+ author="FutureOSS",
+ description="国际化多语言支持 - 提供翻译加载/语言切换/HTTP中间件"
+ ),
+ config=PluginConfig(
+ enabled=True,
+ args={
+ "default_locale": "zh-CN",
+ "fallback_locale": "en-US",
+ "supported_locales": ["zh-CN", "en-US", "zh-TW"]
+ }
+ ),
+ dependencies=[]
+ )
+
+ def init(self, deps: dict = None):
+ """初始化插件
+
+ 加载语言文件并初始化中间件
+ """
+ # 获取插件配置
+ config = {}
+ if deps:
+ config = deps.get("config", {})
+
+ # 默认配置
+ default_locale = config.get("default_locale", "zh-CN")
+ fallback_locale = config.get("fallback_locale", "en-US")
+ supported_locales = config.get("supported_locales", ["zh-CN", "en-US", "zh-TW"])
+ locales_dir = config.get("locales_dir", "locales")
+
+ # 解析 locales_dir 相对路径
+ plugin_dir = Path(__file__).parent
+ full_locales_dir = plugin_dir / locales_dir
+
+ # 设置回退语言
+ self.engine.set_fallback(fallback_locale)
+
+ # 加载语言文件
+ self.engine.load_locales(str(full_locales_dir), supported_locales)
+
+ # 设置默认语言
+ self.engine.set_locale(default_locale)
+
+ # 初始化中间件
+ self.middleware_handler = I18nMiddleware(self.engine, config)
+
+ print(f"[i18n] 已加载语言: {', '.join(supported_locales)}")
+ print(f"[i18n] 默认语言: {default_locale}")
+
+ def start(self):
+ """启动插件
+
+ 注册 API 路由(如果有 http-api 依赖)
+ """
+ # 如果有 http-api 依赖,注册 i18n 相关路由
+ http_api = None
+ if hasattr(self, 'set_http_api'):
+ http_api = getattr(self, '_http_api', None)
+
+ if http_api and hasattr(http_api, 'router'):
+ http_api.router.get("/api/i18n/locales", self._locales_handler)
+ http_api.router.get("/api/i18n/translate", self._translate_handler)
+ http_api.router.post("/api/i18n/locale", self._change_locale_handler)
+ print("[i18n] API 路由已注册")
+
+ def stop(self):
+ """停止插件"""
+ print("[i18n] 插件已停止")
+
+ def health(self) -> bool:
+ """健康检查"""
+ return self.engine is not None
+
+ def stats(self) -> dict:
+ """获取插件统计"""
+ return {
+ "current_locale": self.engine.get_locale(),
+ "supported_locales": self.engine.get_supported_locales(),
+ "loaded_translations": len(self.engine._translations)
+ }
+
+ # ========== 依赖注入 Setter ==========
+
+ def set_http_api(self, http_api):
+ """注入 http-api 依赖"""
+ self._http_api = http_api
+
+ # ========== API 处理器 ==========
+
+ def _locales_handler(self, request):
+ """获取支持的语言列表"""
+ from oss.plugin.types import Response
+ t = getattr(request, 't', self.engine.t)
+
+ locales = []
+ for locale in self.engine.get_supported_locales():
+ locales.append({
+ "code": locale,
+ "name": t(f"plugin.i18n_name", locale=locale)
+ })
+
+ return Response(
+ status=200,
+ body=json.dumps({
+ "current": self.engine.get_locale(),
+ "supported": locales
+ }),
+ headers={"Content-Type": "application/json"}
+ )
+
+ def _translate_handler(self, request):
+ """翻译接口
+
+ GET /api/i18n/translate?key=user.greeting&locale=en-US&name=World
+ """
+ from oss.plugin.types import Response
+ t = getattr(request, 't', self.engine.t)
+
+ # 解析查询参数
+ query = request.path.split("?", 1)[-1] if "?" in request.path else ""
+ params = {}
+ for param in query.split("&"):
+ if "=" in param:
+ key, value = param.split("=", 1)
+ params[key] = value
+
+ key = params.get("key", "")
+ locale = params.get("locale", None)
+
+ if not key:
+ return Response(
+ status=400,
+ body=json.dumps({"error": t("api.missing_param", param="key")}),
+ headers={"Content-Type": "application/json"}
+ )
+
+ # 翻译
+ result = t(key, locale=locale, **params)
+
+ return Response(
+ status=200,
+ body=json.dumps({
+ "key": key,
+ "locale": locale or self.engine.get_locale(),
+ "text": result
+ }),
+ headers={"Content-Type": "application/json"}
+ )
+
+ def _change_locale_handler(self, request):
+ """切换语言接口
+
+ POST /api/i18n/locale
+ Body: {"locale": "en-US"}
+ """
+ from oss.plugin.types import Response
+ t = getattr(request, 't', self.engine.t)
+
+ try:
+ body = json.loads(request.body) if hasattr(request, 'body') and request.body else {}
+ except json.JSONDecodeError:
+ body = {}
+
+ new_locale = body.get("locale", "")
+
+ if not new_locale:
+ return Response(
+ status=400,
+ body=json.dumps({"error": t("api.missing_param", param="locale")}),
+ headers={"Content-Type": "application/json"}
+ )
+
+ if not self.engine.is_valid_locale(new_locale):
+ return Response(
+ status=400,
+ body=json.dumps({"error": t("plugin.locale_not_supported", locale=new_locale)}),
+ headers={"Content-Type": "application/json"}
+ )
+
+ self.engine.set_locale(new_locale)
+
+ return Response(
+ status=200,
+ body=json.dumps({"message": t("plugin.locale_changed", locale=new_locale)}),
+ headers={"Content-Type": "application/json"}
+ )
+
+
+register_plugin_type("I18nPlugin", I18nPlugin)
+
+
+def New():
+ return I18nPlugin()
diff --git a/store/@{FutureOSS}/i18n/manifest.json b/store/@{FutureOSS}/i18n/manifest.json
new file mode 100644
index 0000000..1d63ad7
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/manifest.json
@@ -0,0 +1,23 @@
+{
+ "metadata": {
+ "name": "i18n",
+ "version": "1.0.0",
+ "author": "FutureOSS",
+ "description": "国际化多语言支持 - 提供翻译加载/语言切换/HTTP中间件",
+ "type": "middleware"
+ },
+ "config": {
+ "enabled": true,
+ "args": {
+ "default_locale": "zh-CN",
+ "fallback_locale": "en-US",
+ "locales_dir": "locales",
+ "supported_locales": ["zh-CN", "en-US", "zh-TW"],
+ "auto_detect": true,
+ "cookie_name": "locale",
+ "query_param": "lang"
+ }
+ },
+ "dependencies": [],
+ "permissions": ["lifecycle"]
+}
diff --git a/store/@{FutureOSS}/i18n/middleware.py b/store/@{FutureOSS}/i18n/middleware.py
new file mode 100644
index 0000000..16b1444
--- /dev/null
+++ b/store/@{FutureOSS}/i18n/middleware.py
@@ -0,0 +1,90 @@
+"""i18n HTTP 中间件"""
+import json
+from typing import Optional, Callable
+from oss.plugin.types import Response
+
+
+class I18nMiddleware:
+ """i18n 中间件
+
+ 自动检测语言并注入到请求上下文
+ 检测优先级:
+ 1. URL 查询参数 ?lang=xx
+ 2. Cookie locale=xx
+ 3. Accept-Language 头
+ 4. 默认语言
+ """
+
+ def __init__(self, engine, config: dict = None):
+ self.engine = engine
+ self.cookie_name = (config or {}).get("cookie_name", "locale")
+ self.query_param = (config or {}).get("query_param", "lang")
+
+ def handle(self, request: dict, next_fn: Callable) -> Response:
+ """处理请求
+
+ 1. 检测语言
+ 2. 将语言注入到请求上下文
+ 3. 调用下一个中间件/处理器
+ 4. 可选: 在响应中添加 Content-Language 头
+ """
+ # 解析查询参数
+ query_lang = self._parse_query_param(request.get("query", ""))
+
+ # 解析 Cookie
+ cookie_lang = self._parse_cookie(request.get("headers", {}))
+
+ # 解析 Accept-Language
+ accept_language = request.get("headers", {}).get("Accept-Language",
+ request.get("headers", {}).get("accept-language", ""))
+
+ # 检测语言
+ locale = self.engine.detect_locale(
+ accept_language=accept_language if accept_language else None,
+ query_lang=query_lang,
+ cookie_lang=cookie_lang
+ )
+
+ # 设置当前语言
+ self.engine.set_locale(locale)
+
+ # 注入到请求上下文
+ request["locale"] = locale
+ request["t"] = self.engine.t # 提供翻译函数
+
+ # 调用下一个处理器
+ response = next_fn()
+
+ # 在响应中添加 Content-Language 头
+ if isinstance(response, Response):
+ response.headers["Content-Language"] = locale
+
+ return response
+
+ def _parse_query_param(self, query_string: str) -> Optional[str]:
+ """从查询字符串解析语言参数"""
+ if not query_string:
+ return None
+
+ # 解析 ?lang=xx 或 &lang=xx
+ params = {}
+ for param in query_string.lstrip("?").split("&"):
+ if "=" in param:
+ key, value = param.split("=", 1)
+ params[key.strip()] = value.strip()
+
+ return params.get(self.query_param)
+
+ def _parse_cookie(self, headers: dict) -> Optional[str]:
+ """从 Cookie 解析语言参数"""
+ cookie_header = headers.get("Cookie", headers.get("cookie", ""))
+ if not cookie_header:
+ return None
+
+ cookies = {}
+ for cookie in cookie_header.split(";"):
+ if "=" in cookie:
+ key, value = cookie.split("=", 1)
+ cookies[key.strip()] = value.strip()
+
+ return cookies.get(self.cookie_name)
diff --git a/store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc b/store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc
index 5a94e6e..3211561 100644
Binary files a/store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc and b/store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc differ
diff --git a/store/@{FutureOSS}/ws-api/middleware.py b/store/@{FutureOSS}/ws-api/middleware.py
index 9d3e7de..3eed9a4 100644
--- a/store/@{FutureOSS}/ws-api/middleware.py
+++ b/store/@{FutureOSS}/ws-api/middleware.py
@@ -29,13 +29,16 @@ class WsMiddlewareChain:
async def run(self, client, message) -> Optional[str]:
"""执行中间件链"""
idx = 0
+ current_message = message
- async def next_fn():
- nonlocal idx
+ async def next_fn(msg=None):
+ nonlocal idx, current_message
+ if msg is not None:
+ current_message = msg
if idx < len(self.middlewares):
mw = self.middlewares[idx]
idx += 1
- return await mw.process(client, message, next_fn)
- return message
+ return await mw.process(client, current_message, next_fn)
+ return current_message
return await next_fn()
diff --git a/website/index.html b/website/index.html
index dd5867b..8ed24e5 100644
--- a/website/index.html
+++ b/website/index.html
@@ -32,8 +32,8 @@
2026 · 插件驱动 · 一切皆可扩展