⚡ 初始提交 - FutureOSS v1.0 插件化运行时框架
一切皆为插件的开发者工具运行时框架
🧩 核心特性:
- 插件热插拔 (importlib 动态加载)
- 依赖自动解析 (拓扑排序 + 循环检测)
- 企业级稳定 (熔断/降级/重试/隔离)
- 事件驱动 (发布/订阅事件总线)
- 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
30
store/@{FutureOSS}/circuit-breaker/README.md
Normal file
30
store/@{FutureOSS}/circuit-breaker/README.md
Normal 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("熔断器已触发")
|
||||
```
|
||||
Binary file not shown.
70
store/@{FutureOSS}/circuit-breaker/main.py
Normal file
70
store/@{FutureOSS}/circuit-breaker/main.py
Normal 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()
|
||||
17
store/@{FutureOSS}/circuit-breaker/manifest.json
Normal file
17
store/@{FutureOSS}/circuit-breaker/manifest.json
Normal 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": []
|
||||
}
|
||||
39
store/@{FutureOSS}/dependency/README.md
Normal file
39
store/@{FutureOSS}/dependency/README.md
Normal 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"]
|
||||
}
|
||||
```
|
||||
BIN
store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
138
store/@{FutureOSS}/dependency/main.py
Normal file
138
store/@{FutureOSS}/dependency/main.py
Normal 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 依赖 B,B 依赖 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()
|
||||
15
store/@{FutureOSS}/dependency/manifest.json
Normal file
15
store/@{FutureOSS}/dependency/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "dependency",
|
||||
"version": "1.0.0",
|
||||
"author": "FutureOSS",
|
||||
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
|
||||
"type": "core"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
32
store/@{FutureOSS}/hot-reload/README.md
Normal file
32
store/@{FutureOSS}/hot-reload/README.md
Normal 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()`
|
||||
BIN
store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
196
store/@{FutureOSS}/hot-reload/main.py
Normal file
196
store/@{FutureOSS}/hot-reload/main.py
Normal 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()
|
||||
18
store/@{FutureOSS}/hot-reload/manifest.json
Normal file
18
store/@{FutureOSS}/hot-reload/manifest.json
Normal 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"]
|
||||
}
|
||||
53
store/@{FutureOSS}/http-api/README.md
Normal file
53
store/@{FutureOSS}/http-api/README.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-api/__pycache__/server.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-api/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
58
store/@{FutureOSS}/http-api/events.py
Normal file
58
store/@{FutureOSS}/http-api/events.py
Normal 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"
|
||||
68
store/@{FutureOSS}/http-api/main.py
Normal file
68
store/@{FutureOSS}/http-api/main.py
Normal 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()
|
||||
18
store/@{FutureOSS}/http-api/manifest.json
Normal file
18
store/@{FutureOSS}/http-api/manifest.json
Normal 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"]
|
||||
}
|
||||
57
store/@{FutureOSS}/http-api/middleware.py
Normal file
57
store/@{FutureOSS}/http-api/middleware.py
Normal 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()
|
||||
72
store/@{FutureOSS}/http-api/router.py
Normal file
72
store/@{FutureOSS}/http-api/router.py
Normal 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
|
||||
110
store/@{FutureOSS}/http-api/server.py
Normal file
110
store/@{FutureOSS}/http-api/server.py
Normal 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
|
||||
51
store/@{FutureOSS}/http-tcp/README.md
Normal file
51
store/@{FutureOSS}/http-tcp/README.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-tcp/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
21
store/@{FutureOSS}/http-tcp/events.py
Normal file
21
store/@{FutureOSS}/http-tcp/events.py
Normal 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"
|
||||
34
store/@{FutureOSS}/http-tcp/main.py
Normal file
34
store/@{FutureOSS}/http-tcp/main.py
Normal 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()
|
||||
18
store/@{FutureOSS}/http-tcp/manifest.json
Normal file
18
store/@{FutureOSS}/http-tcp/manifest.json
Normal 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": []
|
||||
}
|
||||
53
store/@{FutureOSS}/http-tcp/middleware.py
Normal file
53
store/@{FutureOSS}/http-tcp/middleware.py
Normal 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()
|
||||
63
store/@{FutureOSS}/http-tcp/router.py
Normal file
63
store/@{FutureOSS}/http-tcp/router.py
Normal 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
|
||||
193
store/@{FutureOSS}/http-tcp/server.py
Normal file
193
store/@{FutureOSS}/http-tcp/server.py
Normal 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())
|
||||
83
store/@{FutureOSS}/json-codec/README.md
Normal file
83
store/@{FutureOSS}/json-codec/README.md
Normal 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}")
|
||||
```
|
||||
BIN
store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
161
store/@{FutureOSS}/json-codec/main.py
Normal file
161
store/@{FutureOSS}/json-codec/main.py
Normal 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()
|
||||
15
store/@{FutureOSS}/json-codec/manifest.json
Normal file
15
store/@{FutureOSS}/json-codec/manifest.json
Normal 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": []
|
||||
}
|
||||
30
store/@{FutureOSS}/lifecycle/README.md
Normal file
30
store/@{FutureOSS}/lifecycle/README.md
Normal 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()
|
||||
```
|
||||
BIN
store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
150
store/@{FutureOSS}/lifecycle/main.py
Normal file
150
store/@{FutureOSS}/lifecycle/main.py
Normal 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()
|
||||
15
store/@{FutureOSS}/lifecycle/manifest.json
Normal file
15
store/@{FutureOSS}/lifecycle/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "lifecycle",
|
||||
"version": "1.0.0",
|
||||
"author": "FutureOSS",
|
||||
"description": "生命周期管理 - 管理插件的状态转换和钩子",
|
||||
"type": "core"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
43
store/@{FutureOSS}/pkg/README.md
Normal file
43
store/@{FutureOSS}/pkg/README.md
Normal 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
|
||||
```
|
||||
BIN
store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
201
store/@{FutureOSS}/pkg/main.py
Normal file
201
store/@{FutureOSS}/pkg/main.py
Normal 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()
|
||||
18
store/@{FutureOSS}/pkg/manifest.json
Normal file
18
store/@{FutureOSS}/pkg/manifest.json
Normal 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": []
|
||||
}
|
||||
77
store/@{FutureOSS}/plugin-bridge/README.md
Normal file
77
store/@{FutureOSS}/plugin-bridge/README.md
Normal 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()
|
||||
```
|
||||
Binary file not shown.
203
store/@{FutureOSS}/plugin-bridge/main.py
Normal file
203
store/@{FutureOSS}/plugin-bridge/main.py
Normal 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()
|
||||
15
store/@{FutureOSS}/plugin-bridge/manifest.json
Normal file
15
store/@{FutureOSS}/plugin-bridge/manifest.json
Normal 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"]
|
||||
}
|
||||
16
store/@{FutureOSS}/plugin-loader/README.md
Normal file
16
store/@{FutureOSS}/plugin-loader/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# plugin-loader 插件加载器
|
||||
|
||||
核心插件,负责扫描、加载和管理所有其他插件。
|
||||
|
||||
## 功能
|
||||
|
||||
- 自动扫描 `store/` 和 `./data/pkg/` 目录
|
||||
- 动态加载 `main.py` 并调用 `New()` 获取实例
|
||||
- 解析 `manifest.json` 获取插件元数据
|
||||
- 自动扫描插件能力(AST 分析)
|
||||
- 按依赖关系排序加载顺序
|
||||
- 关联能力提供者与消费者
|
||||
|
||||
## 使用
|
||||
|
||||
无需手动使用,框架启动时自动加载。
|
||||
Binary file not shown.
609
store/@{FutureOSS}/plugin-loader/main.py
Normal file
609
store/@{FutureOSS}/plugin-loader/main.py
Normal 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()
|
||||
19
store/@{FutureOSS}/plugin-loader/manifest.json
Normal file
19
store/@{FutureOSS}/plugin-loader/manifest.json
Normal 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": ["*"]
|
||||
}
|
||||
72
store/@{FutureOSS}/plugin-storage/README.md
Normal file
72
store/@{FutureOSS}/plugin-storage/README.md
Normal 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"}
|
||||
```
|
||||
Binary file not shown.
350
store/@{FutureOSS}/plugin-storage/main.py
Normal file
350
store/@{FutureOSS}/plugin-storage/main.py
Normal 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()
|
||||
17
store/@{FutureOSS}/plugin-storage/manifest.json
Normal file
17
store/@{FutureOSS}/plugin-storage/manifest.json
Normal 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": ["*"]
|
||||
}
|
||||
50
store/@{FutureOSS}/ws-api/README.md
Normal file
50
store/@{FutureOSS}/ws-api/README.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
23
store/@{FutureOSS}/ws-api/events.py
Normal file
23
store/@{FutureOSS}/ws-api/events.py
Normal 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"
|
||||
30
store/@{FutureOSS}/ws-api/main.py
Normal file
30
store/@{FutureOSS}/ws-api/main.py
Normal 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()
|
||||
18
store/@{FutureOSS}/ws-api/manifest.json
Normal file
18
store/@{FutureOSS}/ws-api/manifest.json
Normal 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": []
|
||||
}
|
||||
41
store/@{FutureOSS}/ws-api/middleware.py
Normal file
41
store/@{FutureOSS}/ws-api/middleware.py
Normal 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()
|
||||
39
store/@{FutureOSS}/ws-api/router.py
Normal file
39
store/@{FutureOSS}/ws-api/router.py
Normal 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})
|
||||
125
store/@{FutureOSS}/ws-api/server.py
Normal file
125
store/@{FutureOSS}/ws-api/server.py
Normal 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())
|
||||
Reference in New Issue
Block a user