From 76147bae942419f391f628993b7f2b9478fcdb67 Mon Sep 17 00:00:00 2001 From: Falck Date: Mon, 6 Apr 2026 09:57:10 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20=E5=88=9D=E5=A7=8B=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=20-=20FutureOSS=20v1.0=20=E6=8F=92=E4=BB=B6=E5=8C=96?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一切皆为插件的开发者工具运行时框架 🧩 核心特性: - 插件热插拔 (importlib 动态加载) - 依赖自动解析 (拓扑排序 + 循环检测) - 企业级稳定 (熔断/降级/重试/隔离) - 事件驱动 (发布/订阅事件总线) - 完整配置 (YAML 配置 + 热重载) --- .dockerignore | 19 + .gitignore | 24 + Dockerfile | 46 ++ LICENSE | 190 +++++ README.md | 106 +++ docker-compose.yml | 43 ++ oss/__init__.py | 2 + oss/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 189 bytes oss/__pycache__/cli.cpython-313.pyc | Bin 0 -> 2365 bytes oss/__pycache__/oss_parser.cpython-313.pyc | Bin 0 -> 16061 bytes oss/cli.py | 56 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 142 bytes oss/config/__pycache__/config.cpython-313.pyc | Bin 0 -> 5864 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 142 bytes oss/logger/__pycache__/logger.cpython-313.pyc | Bin 0 -> 1182 bytes oss/logger/logger.py | 15 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 142 bytes .../__pycache__/capabilities.cpython-313.pyc | Bin 0 -> 4137 bytes .../__pycache__/event_bus.cpython-313.pyc | Bin 0 -> 4337 bytes oss/plugin/__pycache__/loader.cpython-313.pyc | Bin 0 -> 7140 bytes .../__pycache__/manager.cpython-313.pyc | Bin 0 -> 1812 bytes oss/plugin/__pycache__/types.cpython-313.pyc | Bin 0 -> 4996 bytes oss/plugin/capabilities.py | 73 ++ oss/plugin/loader.py | 125 +++ oss/plugin/manager.py | 33 + oss/plugin/types.py | 92 +++ pyproject.toml | 17 + requirements.txt | 3 + start.bat | 146 ++++ start.sh | 180 +++++ static/banner.svg | 421 ++++++++++ store/@{Falck}/html-render/README.md | 41 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 5730 bytes store/@{Falck}/html-render/main.py | 99 +++ store/@{Falck}/html-render/manifest.json | 17 + store/@{Falck}/web-toolkit/README.md | 71 ++ .../__pycache__/main.cpython-313.pyc | Bin 0 -> 8161 bytes .../__pycache__/router.cpython-313.pyc | Bin 0 -> 3599 bytes .../__pycache__/static.cpython-313.pyc | Bin 0 -> 3645 bytes .../__pycache__/template.cpython-313.pyc | Bin 0 -> 8539 bytes store/@{Falck}/web-toolkit/main.py | 158 ++++ store/@{Falck}/web-toolkit/manifest.json | 21 + store/@{Falck}/web-toolkit/router.py | 63 ++ store/@{Falck}/web-toolkit/static.py | 69 ++ store/@{Falck}/web-toolkit/template.py | 144 ++++ store/@{FutureOSS}/circuit-breaker/README.md | 30 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 3650 bytes store/@{FutureOSS}/circuit-breaker/main.py | 70 ++ .../circuit-breaker/manifest.json | 17 + store/@{FutureOSS}/dependency/README.md | 39 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 6728 bytes store/@{FutureOSS}/dependency/main.py | 138 ++++ store/@{FutureOSS}/dependency/manifest.json | 15 + store/@{FutureOSS}/hot-reload/README.md | 32 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 9862 bytes store/@{FutureOSS}/hot-reload/main.py | 196 +++++ store/@{FutureOSS}/hot-reload/manifest.json | 18 + store/@{FutureOSS}/http-api/README.md | 53 ++ .../__pycache__/events.cpython-313.pyc | Bin 0 -> 3008 bytes .../http-api/__pycache__/main.cpython-313.pyc | Bin 0 -> 3317 bytes .../__pycache__/middleware.cpython-313.pyc | Bin 0 -> 3535 bytes .../__pycache__/router.cpython-313.pyc | Bin 0 -> 3868 bytes .../__pycache__/server.cpython-313.pyc | Bin 0 -> 6768 bytes store/@{FutureOSS}/http-api/events.py | 58 ++ store/@{FutureOSS}/http-api/main.py | 68 ++ store/@{FutureOSS}/http-api/manifest.json | 18 + store/@{FutureOSS}/http-api/middleware.py | 57 ++ store/@{FutureOSS}/http-api/router.py | 72 ++ store/@{FutureOSS}/http-api/server.py | 110 +++ store/@{FutureOSS}/http-tcp/README.md | 51 ++ .../__pycache__/events.cpython-313.pyc | Bin 0 -> 1021 bytes .../http-tcp/__pycache__/main.cpython-313.pyc | Bin 0 -> 1835 bytes .../__pycache__/middleware.cpython-313.pyc | Bin 0 -> 3254 bytes .../__pycache__/router.cpython-313.pyc | Bin 0 -> 3633 bytes .../__pycache__/server.cpython-313.pyc | Bin 0 -> 9849 bytes store/@{FutureOSS}/http-tcp/events.py | 21 + store/@{FutureOSS}/http-tcp/main.py | 34 + store/@{FutureOSS}/http-tcp/manifest.json | 18 + store/@{FutureOSS}/http-tcp/middleware.py | 53 ++ store/@{FutureOSS}/http-tcp/router.py | 63 ++ store/@{FutureOSS}/http-tcp/server.py | 193 +++++ store/@{FutureOSS}/json-codec/README.md | 83 ++ .../__pycache__/main.cpython-313.pyc | Bin 0 -> 9507 bytes store/@{FutureOSS}/json-codec/main.py | 161 ++++ store/@{FutureOSS}/json-codec/manifest.json | 15 + store/@{FutureOSS}/lifecycle/README.md | 30 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 7626 bytes store/@{FutureOSS}/lifecycle/main.py | 150 ++++ store/@{FutureOSS}/lifecycle/manifest.json | 15 + store/@{FutureOSS}/pkg/README.md | 43 ++ .../pkg/__pycache__/main.cpython-313.pyc | Bin 0 -> 10514 bytes store/@{FutureOSS}/pkg/main.py | 201 +++++ store/@{FutureOSS}/pkg/manifest.json | 18 + store/@{FutureOSS}/plugin-bridge/README.md | 77 ++ .../__pycache__/main.cpython-313.pyc | Bin 0 -> 11598 bytes store/@{FutureOSS}/plugin-bridge/main.py | 203 +++++ .../@{FutureOSS}/plugin-bridge/manifest.json | 15 + store/@{FutureOSS}/plugin-loader/README.md | 16 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 29841 bytes store/@{FutureOSS}/plugin-loader/main.py | 609 +++++++++++++++ .../@{FutureOSS}/plugin-loader/manifest.json | 19 + store/@{FutureOSS}/plugin-storage/README.md | 72 ++ .../__pycache__/main.cpython-313.pyc | Bin 0 -> 19846 bytes store/@{FutureOSS}/plugin-storage/main.py | 350 +++++++++ .../@{FutureOSS}/plugin-storage/manifest.json | 17 + store/@{FutureOSS}/ws-api/README.md | 50 ++ .../ws-api/__pycache__/events.cpython-313.pyc | Bin 0 -> 1112 bytes .../ws-api/__pycache__/main.cpython-313.pyc | Bin 0 -> 1503 bytes .../__pycache__/middleware.cpython-313.pyc | Bin 0 -> 2611 bytes .../ws-api/__pycache__/router.cpython-313.pyc | Bin 0 -> 2281 bytes .../ws-api/__pycache__/server.cpython-313.pyc | Bin 0 -> 6705 bytes store/@{FutureOSS}/ws-api/events.py | 23 + store/@{FutureOSS}/ws-api/main.py | 30 + store/@{FutureOSS}/ws-api/manifest.json | 18 + store/@{FutureOSS}/ws-api/middleware.py | 41 + store/@{FutureOSS}/ws-api/router.py | 39 + store/@{FutureOSS}/ws-api/server.py | 125 +++ website/architecture.html | 94 +++ website/community/UPDATE_PROFILE.md | 134 ++++ website/community/add-title-system.sql | 25 + website/community/api/auth.php | 286 +++++++ website/community/api/index.php | 112 +++ website/community/api/posts.php | 340 +++++++++ website/community/assets/css/auth.css | 363 +++++++++ website/community/assets/css/community.css | 572 ++++++++++++++ website/community/assets/css/dock-popover.css | 177 +++++ website/community/assets/css/editor.css | 325 ++++++++ website/community/assets/css/post-drawer.css | 316 ++++++++ website/community/assets/js/auth.js | 168 ++++ website/community/assets/js/community.js | 255 +++++++ website/community/assets/js/dock-popover.js | 91 +++ website/community/assets/js/editor.js | 252 ++++++ website/community/assets/js/polling-system.js | 91 +++ website/community/assets/js/post-drawer.js | 236 ++++++ website/community/assets/js/title-updater.js | 108 +++ website/community/edit-profile.php | 388 ++++++++++ website/community/editor.php | 156 ++++ website/community/includes/Database.php | 47 ++ website/community/includes/dock.php | 75 ++ website/community/includes/post-modal.php | 478 ++++++++++++ website/community/index.php | 91 +++ website/community/install.sh | 79 ++ website/community/login.php | 110 +++ website/community/migrate-add-bio.php | 29 + website/community/my-posts.php | 498 ++++++++++++ website/community/post.php | 117 +++ website/community/profile.php | 717 ++++++++++++++++++ website/community/register.php | 142 ++++ website/community/schema.sql | 104 +++ website/community/seed-announcements.sql | 15 + website/css/architecture.css | 110 +++ website/css/code-examples.css | 63 ++ website/css/community.css | 46 ++ website/css/dock.css | 156 ++++ website/css/features.css | 98 +++ website/css/footer.css | 79 ++ website/css/hero.css | 259 +++++++ website/css/main.css | 160 ++++ website/css/page.css | 396 ++++++++++ website/css/plugins.css | 110 +++ website/css/quickstart.css | 65 ++ website/css/stats.css | 52 ++ website/features.html | 91 +++ website/index.html | 83 ++ website/js/animations.js | 35 + website/js/app.js | 16 + website/js/dock.js | 75 ++ website/js/particles.js | 83 ++ website/js/spa.js | 100 +++ website/logo.svg | 32 + website/plugins.html | 113 +++ website/quickstart.html | 72 ++ website/sitemap.xml | 97 +++ website/update.php | 126 +++ 174 files changed, 15626 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 oss/__init__.py create mode 100644 oss/__pycache__/__init__.cpython-313.pyc create mode 100644 oss/__pycache__/cli.cpython-313.pyc create mode 100644 oss/__pycache__/oss_parser.cpython-313.pyc create mode 100644 oss/cli.py create mode 100644 oss/config/__pycache__/__init__.cpython-313.pyc create mode 100644 oss/config/__pycache__/config.cpython-313.pyc create mode 100644 oss/logger/__pycache__/__init__.cpython-313.pyc create mode 100644 oss/logger/__pycache__/logger.cpython-313.pyc create mode 100644 oss/logger/logger.py create mode 100644 oss/plugin/__pycache__/__init__.cpython-313.pyc create mode 100644 oss/plugin/__pycache__/capabilities.cpython-313.pyc create mode 100644 oss/plugin/__pycache__/event_bus.cpython-313.pyc create mode 100644 oss/plugin/__pycache__/loader.cpython-313.pyc create mode 100644 oss/plugin/__pycache__/manager.cpython-313.pyc create mode 100644 oss/plugin/__pycache__/types.cpython-313.pyc create mode 100644 oss/plugin/capabilities.py create mode 100644 oss/plugin/loader.py create mode 100644 oss/plugin/manager.py create mode 100644 oss/plugin/types.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 start.bat create mode 100755 start.sh create mode 100644 static/banner.svg create mode 100644 store/@{Falck}/html-render/README.md create mode 100644 store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc create mode 100644 store/@{Falck}/html-render/main.py create mode 100644 store/@{Falck}/html-render/manifest.json create mode 100644 store/@{Falck}/web-toolkit/README.md create mode 100644 store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc create mode 100644 store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc create mode 100644 store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc create mode 100644 store/@{Falck}/web-toolkit/__pycache__/template.cpython-313.pyc create mode 100644 store/@{Falck}/web-toolkit/main.py create mode 100644 store/@{Falck}/web-toolkit/manifest.json create mode 100644 store/@{Falck}/web-toolkit/router.py create mode 100644 store/@{Falck}/web-toolkit/static.py create mode 100644 store/@{Falck}/web-toolkit/template.py create mode 100644 store/@{FutureOSS}/circuit-breaker/README.md create mode 100644 store/@{FutureOSS}/circuit-breaker/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/circuit-breaker/main.py create mode 100644 store/@{FutureOSS}/circuit-breaker/manifest.json create mode 100644 store/@{FutureOSS}/dependency/README.md create mode 100644 store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/dependency/main.py create mode 100644 store/@{FutureOSS}/dependency/manifest.json create mode 100644 store/@{FutureOSS}/hot-reload/README.md create mode 100644 store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/hot-reload/main.py create mode 100644 store/@{FutureOSS}/hot-reload/manifest.json create mode 100644 store/@{FutureOSS}/http-api/README.md create mode 100644 store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-api/__pycache__/middleware.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-api/__pycache__/server.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-api/events.py create mode 100644 store/@{FutureOSS}/http-api/main.py create mode 100644 store/@{FutureOSS}/http-api/manifest.json create mode 100644 store/@{FutureOSS}/http-api/middleware.py create mode 100644 store/@{FutureOSS}/http-api/router.py create mode 100644 store/@{FutureOSS}/http-api/server.py create mode 100644 store/@{FutureOSS}/http-tcp/README.md create mode 100644 store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-tcp/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-tcp/__pycache__/middleware.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc create mode 100644 store/@{FutureOSS}/http-tcp/events.py create mode 100644 store/@{FutureOSS}/http-tcp/main.py create mode 100644 store/@{FutureOSS}/http-tcp/manifest.json create mode 100644 store/@{FutureOSS}/http-tcp/middleware.py create mode 100644 store/@{FutureOSS}/http-tcp/router.py create mode 100644 store/@{FutureOSS}/http-tcp/server.py create mode 100644 store/@{FutureOSS}/json-codec/README.md create mode 100644 store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/json-codec/main.py create mode 100644 store/@{FutureOSS}/json-codec/manifest.json create mode 100644 store/@{FutureOSS}/lifecycle/README.md create mode 100644 store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/lifecycle/main.py create mode 100644 store/@{FutureOSS}/lifecycle/manifest.json create mode 100644 store/@{FutureOSS}/pkg/README.md create mode 100644 store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/pkg/main.py create mode 100644 store/@{FutureOSS}/pkg/manifest.json create mode 100644 store/@{FutureOSS}/plugin-bridge/README.md create mode 100644 store/@{FutureOSS}/plugin-bridge/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/plugin-bridge/main.py create mode 100644 store/@{FutureOSS}/plugin-bridge/manifest.json create mode 100644 store/@{FutureOSS}/plugin-loader/README.md create mode 100644 store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/plugin-loader/main.py create mode 100644 store/@{FutureOSS}/plugin-loader/manifest.json create mode 100644 store/@{FutureOSS}/plugin-storage/README.md create mode 100644 store/@{FutureOSS}/plugin-storage/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/plugin-storage/main.py create mode 100644 store/@{FutureOSS}/plugin-storage/manifest.json create mode 100644 store/@{FutureOSS}/ws-api/README.md create mode 100644 store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc create mode 100644 store/@{FutureOSS}/ws-api/__pycache__/main.cpython-313.pyc create mode 100644 store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc create mode 100644 store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc create mode 100644 store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc create mode 100644 store/@{FutureOSS}/ws-api/events.py create mode 100644 store/@{FutureOSS}/ws-api/main.py create mode 100644 store/@{FutureOSS}/ws-api/manifest.json create mode 100644 store/@{FutureOSS}/ws-api/middleware.py create mode 100644 store/@{FutureOSS}/ws-api/router.py create mode 100644 store/@{FutureOSS}/ws-api/server.py create mode 100644 website/architecture.html create mode 100644 website/community/UPDATE_PROFILE.md create mode 100644 website/community/add-title-system.sql create mode 100644 website/community/api/auth.php create mode 100644 website/community/api/index.php create mode 100644 website/community/api/posts.php create mode 100644 website/community/assets/css/auth.css create mode 100644 website/community/assets/css/community.css create mode 100644 website/community/assets/css/dock-popover.css create mode 100644 website/community/assets/css/editor.css create mode 100644 website/community/assets/css/post-drawer.css create mode 100644 website/community/assets/js/auth.js create mode 100644 website/community/assets/js/community.js create mode 100644 website/community/assets/js/dock-popover.js create mode 100644 website/community/assets/js/editor.js create mode 100644 website/community/assets/js/polling-system.js create mode 100644 website/community/assets/js/post-drawer.js create mode 100644 website/community/assets/js/title-updater.js create mode 100644 website/community/edit-profile.php create mode 100644 website/community/editor.php create mode 100644 website/community/includes/Database.php create mode 100644 website/community/includes/dock.php create mode 100644 website/community/includes/post-modal.php create mode 100644 website/community/index.php create mode 100644 website/community/install.sh create mode 100644 website/community/login.php create mode 100644 website/community/migrate-add-bio.php create mode 100644 website/community/my-posts.php create mode 100644 website/community/post.php create mode 100644 website/community/profile.php create mode 100644 website/community/register.php create mode 100644 website/community/schema.sql create mode 100644 website/community/seed-announcements.sql create mode 100644 website/css/architecture.css create mode 100644 website/css/code-examples.css create mode 100644 website/css/community.css create mode 100644 website/css/dock.css create mode 100644 website/css/features.css create mode 100644 website/css/footer.css create mode 100644 website/css/hero.css create mode 100644 website/css/main.css create mode 100644 website/css/page.css create mode 100644 website/css/plugins.css create mode 100644 website/css/quickstart.css create mode 100644 website/css/stats.css create mode 100644 website/features.html create mode 100644 website/index.html create mode 100644 website/js/animations.js create mode 100644 website/js/app.js create mode 100644 website/js/dock.js create mode 100644 website/js/particles.js create mode 100644 website/js/spa.js create mode 100644 website/logo.svg create mode 100644 website/plugins.html create mode 100644 website/quickstart.html create mode 100644 website/sitemap.xml create mode 100644 website/update.php diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fdd7cf4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +__pycache__/ +*.pyc +*.pyo +.venv/ +*.egg-info/ +.pytest_cache/ +.git/ +.vscode/ +.cache/ +logs/ +config.yaml +data/pkg/ +data/DCIM/ +data/html-render/*.json +data/web-toolkit/*.json +data/plugin-storage/*.json +*.so +*.dll +*.dylib diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a2de83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.qwen/ +QWEN.md +.vscode/ +bin/ +logs/ +*.log +config.yaml +package.json +.cache/ +data/ + +# Web 运行时文件 +start-web.sh +website/router.php +admin/ + +# 数据库配置(含密码) +website/community/config.php +website/community/.env +admin/includes/config.php + +# 日志 +logs/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e397be2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM python:3.12-slim AS builder + +WORKDIR /app + +# 构建依赖 +COPY pyproject.toml ./ +COPY README.md ./ + +# 安装 Python 依赖 +RUN pip install --no-cache-dir --prefix=/install . 2>/dev/null || \ + pip install --no-cache-dir --prefix=/install --break-system-packages . 2>/dev/null || true + +# 兜底核心依赖 +RUN pip install --no-cache-dir --prefix=/install click pyyaml websockets 2>/dev/null || true + +# ───────────────────────────────────────────────────────── +FROM python:3.12-slim + +LABEL maintainer="Falck " +LABEL description="FutureOSS — 一切皆为插件的开发者工具运行时框架" + +WORKDIR /app + +# 复制依赖 +COPY --from=builder /install /usr/local + +# 复制项目文件 +COPY oss/ ./oss/ +COPY store/ ./store/ +COPY data/ ./data/ +COPY start.sh ./start.sh +COPY pyproject.toml ./pyproject.toml +COPY README.md ./README.md + +# 创建必要目录 +RUN mkdir -p /app/data/html-render /app/data/web-toolkit /app/data/plugin-storage /app/data/DCIM /app/data/pkg /app/logs + +# 暴露端口 +EXPOSE 8080 8081 8082 + +# 健康检查 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# 启动 +CMD ["python", "-m", "oss.cli", "serve"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..571b265 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 Falck + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd6423b --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +
+ FutureOSS Banner +
+ +--- + +
+ +## 📂 项目结构 + +
+ +``` +FutureOSS/ +├── 🚀 pyproject.toml # Python 项目配置 +├── 📋 oss/ # 核心框架包 +│ ├── cli.py # CLI 命令入口 +│ ├── config/ # 配置系统 +│ ├── logger/ # 日志系统 +│ ├── plugin/ # 插件框架 (接口/加载器/管理器) +│ └── server/ # HTTP 服务器 +├── 🧩 store/ # 本地插件仓库 +│ └── @{作者/插件名}/ # 插件目录 (含 manifest.json) +├── 📦 data/ # 运行时数据目录 +│ ├── html-render/ # 网站文件 (通过 config.json 配置) +│ ├── web-toolkit/ # Web 工具配置 +│ ├── plugin-storage/ # 插件存储配置 +│ └── DCIM/ # 共享存储 +├── 🌐 website/ # 官网 + 社区 (PHP) +└── 📖 static/ # 静态资源 (SVG 背景等) +``` + +
+ +## 📖 文档 + +
+ +所有文档都在本地 `dock/` 目录中: + +| 📘 页面 | 📝 内容 | +|:---:|:---| +| [🎯 项目介绍](./dock/00-项目介绍/) | 什么是 FutureOSS、架构设计、核心概念 | +| [🚀 快速开始](./dock/01-快速开始/) | 安装、配置、第一次运行 | +| [🔌 插件开发](./dock/02-插件开发/) | 编写你的第一个插件、事件系统 | +| [📄 插件文档](./dock/03-插件文档/) | http-api、ws-api、file 插件详解 | +| [📦 包管理](./dock/04-包管理/) | 安装/卸载/搜索/发布插件 | +| [⚙️ 配置参考](./dock/05-配置参考/) | 配置参数详解 | +| [🚢 部署运维](./dock/06-部署运维/) | 本地运行、Docker、生产环境 | +| [🌟 社区与贡献](./dock/07-社区与贡献/) | 贡献指南、行为准则 | + +
+ +## 🔗 远程仓库 + +
+ +
+ +| 📦 代码仓库 | 📚 包仓库 | +|:---:|:---:| +| [Gitee](https://gitee.com/starlight-apk/feature-oss) | [Gitee Pkg](https://gitee.com/starlight-apk/future-oss-pkg) | + +
+ +--- + +
+ +## 📜 许可证 + +**[Apache License 2.0](LICENSE)** + +Copyright 2026 Falck + +本项目采用 Apache 2.0 许可证 — 你可以自由使用、修改和分发,需保留版权和许可证声明。 + +--- + +### 📝 作者声明 + +> 本项目采用 Apache 2.0 开源许可证,此为独立于许可证的补充说明: + +- 🚫 **禁止未经作者(Falck)明确书面许可的二次转发、搬运、转载** +- 🚫 **禁止冒充原作者或声称与官方项目存在关联** +- 🚫 **禁止移除、修改或遮盖版权声明、许可证和 NOTICE 文件** +- ✅ **允许个人学习、研究、商业使用(需保留版权和许可证信息)** + +> 此声明不改变 Apache 2.0 许可证的法律效力,仅表达作者的合理期望。 +> 如需特殊授权,请联系作者。 + +
+ +
+ +--- + +

+ ⚡ FutureOSS — 一切皆为插件 +

+ +

+ Made with ❤️ by Falck +

+ +
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..99825a4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + futureoss: + build: + context: . + dockerfile: Dockerfile + container_name: futureoss + restart: unless-stopped + ports: + - "8080:8080" # HTTP API + 网站 + - "8081:8081" # WebSocket + - "8082:8082" # HTTP TCP + volumes: + # 插件热更新(无需重建镜像) + - ./store/@{FutureOSS}:/app/store/@{FutureOSS}:ro + - ./store/@{Falck}:/app/store/@{Falck}:ro + # 数据持久化 + - futureoss-data:/app/data + # 配置可覆盖 + - ./config.yaml:/app/config.yaml:ro + environment: + - TZ=Asia/Shanghai + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + cpus: "1.0" + reservations: + memory: 128M + +volumes: + futureoss-data: + driver: local diff --git a/oss/__init__.py b/oss/__init__.py new file mode 100644 index 0000000..673db08 --- /dev/null +++ b/oss/__init__.py @@ -0,0 +1,2 @@ +"""Future OSS""" +__version__ = "1.0.0" diff --git a/oss/__pycache__/__init__.cpython-313.pyc b/oss/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d862c0db508d3a8e14f02ffa49f39762863f2263 GIT binary patch literal 189 zcmey&%ge<81WS}JWvT<|#~=<2FhUuhd4P=jOk38%vD@&r6r|BsS5tV z!Bwn=dIow1ews|T*yH0<@{{A^Z*j-Rm!%dJXXfX{$FF4g3^M4Jntn!pZmNDsW?rSf zOKNd;Nq&JoP`iG9aj|}Ud}dx|NqoFsLFFwDo80`A(wtPgB6grQkRytDfy4)9Mn=Y) P3<5X!L>sw_Sb-t{fqpO) literal 0 HcmV?d00001 diff --git a/oss/__pycache__/cli.cpython-313.pyc b/oss/__pycache__/cli.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a117f1ff0658dad387ce4a30af1b63bbdca6795 GIT binary patch literal 2365 zcmah~|8G-O6hH61``Wj*>%IWOI@liI7YbWTGD(c$f|*0;=rDa-Lb8RXc5kK9_RahH zKr}HO2DgD`LtR|Vejt8H&`A99gAz#mLH_~N8pmT86W1;LRvacDj0Qi&r*u-R1r|^9o<*b>fJkCO&5F(Vw zOh@{}kb^obUhH#*Br3@;%te}Qm(_V2>?!{!4qt_*u3=YXW7W#$Dz?rVThBvmr5f9M zrOpS`JKUhSo<0H)lK_Uj)fi+*@Cn}UHdr=#4jq&WXFe@Ve|(GW-|y#4cQkrJrFv4! zM5FlYIHV;KDn+jT#gvgqX2P*d3Bn%&Fk>s(w7HjRkRKP}B8b zJe3TLoiYWSDT|wZI$N59q65S_iid>zLK0D6h4ecHca%RW%)$^0(;_>^X?}o@Fslbh z;fB~rEb@ZFcL{sSYh@T>+geodA`A$TI$K(uZM_VrcUR>$SEFA$ike*q`9a}HULihs zS<1kW$MzAaaJB?sTdm$(mD}7P>?z|NPg7a{P*08a`s!Tksjb%6|5gDa(6JSU*4uuZ z5NiRzN)i=EpF6{~!5|kw$%CXgtG5td2QUiH3hgk;`6Z(XZ3{DB7S3LjtC+jzuJW?M zu1zl47!GFs)z>RmKabm$&o*O7K)1d1LFrKU!Q+wuJgjlYxTiA*}c@%_Fj7~v_QNQ zhsO`+$p++pBkO-6>+|H1KguhPApbWZ;^sRZ5Zx2u)8Y3Y|Df}H=Y>sqvU8ER%96nO zz=a)ova3i$(YGjhr#)vq?|APnx?5I35PdhKbqn6+1yAF2Xeu=G%G=@F0yL~Eim1A$ zGg$o4wV|gDeyrQx(;>w1zd|gxEUT;CIN+04QABEiUY2gdMppaKb=ANUTZCXCaCz?g zl~1m$OieD&e?i&a{Rlb{AL96Nlc@1yTIm*9d6n+k0+h4dL+E2D@5E1MtwMQU~@=~yyDpTM~*Q zB5|JSwTw(D8f6yP;2KS5G&-WiqfxpYO{otDQ^1yEoyu%NE`X(j4X->|=rx;}yi`qr zbg3bha4aUMW{g=una;SDM&*cwGb<5RNNo2E)+BEEK+>eWX>G(vsoiuphQX>uXD?Av z;5hC&G~NKu4X7_b>rHs{CUjuqW)Ah9cvNF5tz9)^SVI!uiS? njPnne2A8Y~RbxId>+lGj}taYRC5ex%+&d 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 literal 0 HcmV?d00001 diff --git a/oss/cli.py b/oss/cli.py new file mode 100644 index 0000000..68ee536 --- /dev/null +++ b/oss/cli.py @@ -0,0 +1,56 @@ +"""CLI 入口""" +import click +import signal + +from oss import __version__ +from oss.logger.logger import Logger +from oss.plugin.manager import PluginManager + + +@click.group() +def cli(): + """Future OSS - 一切皆为插件""" + pass + + +@cli.command() +def serve(): + """启动 Future OSS""" + log = Logger() + log.info(f"Future OSS {__version__} 启动") + + plugin_mgr = PluginManager() + plugin_mgr.load() + plugin_mgr.start() + + log.info("就绪") + + def shutdown(sig, frame): + log.info("停止中...") + plugin_mgr.stop() + log.info("已停止") + raise SystemExit(0) + + signal.signal(signal.SIGINT, shutdown) + signal.signal(signal.SIGTERM, shutdown) + + try: + signal.pause() + except AttributeError: + import time + while True: + time.sleep(1) + + +@cli.command() +def version(): + """显示版本""" + click.echo(f"Future OSS {__version__}") + + +def main(): + cli() + + +if __name__ == "__main__": + main() diff --git a/oss/config/__pycache__/__init__.cpython-313.pyc b/oss/config/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90f6dc88d9ce84fcf07a802effff1f71240d327c GIT binary patch literal 142 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 Z=vVkt+1buUX7maJKePJ3@P|&we*nNd37`M~ literal 0 HcmV?d00001 diff --git a/oss/logger/__pycache__/__init__.cpython-313.pyc b/oss/logger/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c977cc351b5f60c01e2241c777571c152955dae GIT binary patch literal 142 zcmey&%ge<81eP+FG8uvNV-N=hKms7}nFUBpWk_exWb|9fP{afh0*T+!)z8S!P1P^S z%&XLQNiEJU$uH3N4-VGPFD};4$xlyDEz*yV&&J=7k8C<-ltSYrq2pz>JPahlwn+1|_$b_$}f z2$X1XCCEbr?L`WqLu&mC5qR+7E!WkP(gt4pzL{CULtX5_e7@h$`_6p7pYQj*smJ3n z1gp^V!z>F3{p6&v~2B(BHM9~R!25CYk(!`V)M5$m%3E8^@Y;*Q~_1kQ1ajCX6 zkI&;;6wkMZ>kG#x96T#U(9bkS;>Wkd=utH^*caI zXc$oeDvO#xB~7HVCJieWMNRHR_hlLhnc}l($Ra*dLl*T}lcv&`(Ugd>$Zgv&bh^#! zc&7jYZCeW;t1G4I+=rdzsfP0BOe#1=uAzv_<1=Z7r;Y(iXb5H&EKL{`TG0hD74BeU zpoExy6dk&izA-R@D*{uB)U@1+#AHX$k5dIcWRZ!7S!y_r1O#Ku&L!{J1wHAS)?~6z zcP3oBm>kfwr0qD#ye~X7y~Rl;o7T9^nuCUVIpI0$M*tq9bu~T{ElV@rl)b8U)nN}E zwv)TpGU*XaqmYU=0W?(879dQ9PON=3oT&W8HV$0U;i{`c{h7=_=I`Ydol={!mT($A z^H1t1)DFg>mfOq9_tvgewfny)>g&Hgc=I2M#`ICo*jMDURz8j6jgeGPI?>EjLab~- zCxpcaDcECPp8G9?Jn*vlFw#cIxJey1Z(6!#LpVyvn4JSha$HK|JmWDA9UM*quns~T zH|v@?l675bj(V={5JLHt_q+E`jq;1<1Ha`?3g8)9lh5wTg3?` z_rc%eevpwTOD9B%!3LQRR%7brpua1$2VUX7io;d!N`fG)p{sjhT)@8&fS>OVhcXlq literal 0 HcmV?d00001 diff --git a/oss/logger/logger.py b/oss/logger/logger.py new file mode 100644 index 0000000..47f44ad --- /dev/null +++ b/oss/logger/logger.py @@ -0,0 +1,15 @@ +"""日志系统 - 空壳,由日志插件提供实际功能""" +class Logger: + """日志记录器(空壳)""" + + def info(self, msg: str, **kwargs): + print(f"[INFO] {msg}") + + def warn(self, msg: str, **kwargs): + print(f"[WARN] {msg}") + + def error(self, msg: str, **kwargs): + print(f"[ERROR] {msg}") + + def debug(self, msg: str, **kwargs): + print(f"[DEBUG] {msg}") diff --git a/oss/plugin/__pycache__/__init__.cpython-313.pyc b/oss/plugin/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..272be63fd6c28f39973403effb02165f3b3f028f GIT binary patch literal 142 zcmey&%ge<81U534G8uvNV-N=hKms7}nFUBpWk_exWb|9fP{afh0*T+!)z8S!P1P^S z%&XLQNiEJU$uH3N4-VGPFD}+E$SFWQ}f#fM~Ll}0ZcDDin7CRmG&~))oU{AZ0 zL`jydAqad8>zsS}opZi(&*9b86PL>g(D-HezVIswfPWYha}WjPNgq-^0xXaKi?Nn5 zOu~@F$1I0&31^HoVvIOUN+i- zyEZ$!y0F9qnJ<3wo7D?T`IWWV4<7#UleG_TuD$=GN5A-S9{f*(z|js260dX0NhI-C z#cAkFCo?AmF_sV{Xq9*+BZ+a$Ko*7-NgVw@VH^hSW5s9t3IGv{BGN&Wu4su`A_lf% zn8@9Q^@Ih0DAo>+ktQ%|VJ$4q5@Fl|L_CT|Z03`aG`)xfOB#RskQZYx;FvwH0T9V; zzd6gxRxm;AE3g*HGHlI8oH1(!QPRZvz$kGLt%=rSZW9@`mf3sE)si$hnVE9`LYZ14 zJ4y&s6Kzpjq~4sBBx^O}7k!{|BdouKF*Vl8+RTWNMsu|!i#ukIViCHuZfa4x8H)kq zXLC`uVpK#u1ta@5GP+Zhk;CL&Wb|UyJ#uUxqfu-ebvgR11)xe#G@E-{l4gZWuA>-d z^#9gf;Xv!pm26GT2M(gMM0A!#b(VD^W8zA(GAj25is_8BmJp^E*=1@a$vQXfaHy*N zR-NfA?Yj6h-S-0k#^nvWTAtqvC1$2pnV)f{jDMKpXl}F#Me^x#%=!4Fs*ml+a}@vz zomium@vaTomY*wo%7_=#%51s2h@~~McQX=o${$dtP%IUtwmNtI-#Kvrxl5q#V{4wx zmh&N2Tb?wm%j_p~(nL^%+HH?eTNdFx6v6#$5pq57>#BLU%c7u8$<-1cZ>U+ocZ zx|CnS9$o(3>Zc!l`KR~u_p{a6i;sS`TyZmd+cw1sVrP09V$$1WMwtx0jIL*PUW}&_ zf_UO6w7)K;ekde(2?9ut3ugrp+Sp7wostyjNavr)F=&emQamFl&~{wnxzh;lF091N z1rlBHq-jXTx%f%GqY>gVuRtq*R*)4L5~(yVLIT+)AkN7OB-5NE^N={hB~L@AAPb_b zaAKT?_TeNa%Om_GBt&is&3Q2)pAnRk&?fQeB!?hmGIa(qz_~;MQmf3`s62Ah2#ZSox|HMsAI(uRMkt?>u*B#fF~;W~^W1z}dBK1vT#3@P+W)cQvZ{{K$GO z@YY?je_)@lyHqoR*fFe>0y-~*644~*|VNz-NUFJW-+kh*^~9sy0=~Rwl7ZT-k|CY zmMl*!OB&s$)32!XD;hmi-SSP{8&bWYlBIBKher47^pHvqY4pejOWm{Q-z3=4weAG9 zfqCLe{Ay|;rMo)syE?Oh=Bthc$IPJzEzj#MU7xjd%{l(=Ync~5_T3EJ2S)HRQ;Vx=XC!8)qfz{w)cAfwSK*AzuLC{7IizKwH=(dX9MkeV7D6By%bmp z^n68vKtI~y24>OmaqrE68v}ZMSgQ{k4&S}_uI}$r{awqn?jO+n1BS!FYlC`QSZxd6 z`sQs(Ydc)&aNP@@>t1g#(>S$oN_Taru3S}`t~AY&Szq%adEI%{vm{o^aZ8)mE6wTH#dR@C(*M4u;ODlEzv-O+o>U)Zq#fg==;AY5QDk1AG z(A2)}G3kj5qV5WQ<_c!rzS(oL=PsNx=n>bcYn0A(t4z1X^lHA|<>}iacgOCG=_65f zB&v-}X!|FMp%FbjPp`tRzPE@$eUzuS5wcPV`RaQZ@a}y4eGJqD?h*d4GZ=aSRve-K z%IMw>e(!x_5LXAC5tpsQ25qreA{CFtqyTyZZE5tAI4K;L(0d1Dlm-#@e-T^@_5c6? literal 0 HcmV?d00001 diff --git a/oss/plugin/__pycache__/event_bus.cpython-313.pyc b/oss/plugin/__pycache__/event_bus.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..902a2b66b6aa53b3c66b30544df3746738ee3a4b GIT binary patch literal 4337 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 literal 0 HcmV?d00001 diff --git a/oss/plugin/__pycache__/types.cpython-313.pyc b/oss/plugin/__pycache__/types.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..905b7c3351e918e851fd1dc9fc6dd5d0577aa52f GIT binary patch literal 4996 zcmb7IeQZqC4{JV4RwO?#7WxJBk zyZ79$bMN`x^Wk~F-$S6BDSeST<0jMC zTiPx6D^%%sQCE;`Ahao>M%)o)lZXpMJi5FVzPb`|>n`BRded_9s!Y9%qeg&-(_v$SR_HmlNH$S%e8tXfxyaf!FNtS{SbdcuO4K62@xaLg%rxVbld#=U z2K>!;0vRU*!Yho8WPsouCIwB*t$<4D?~}#1&vOYMo%t&lkBb`C1lj~cG?A+Cp`cV^ z;x56Uy*_j-d)x?FsmxGlr(yom$_|G1>Ut<^njtQ1hzZ*}IAnKFBbhQSgVr8{`d}dh zjER+Da=%AAwP#HGLhhc2HnCmQuyQmL7qC?+&Z)i&jB&Dfru~&WuMWiT0NGrbyM|~& z?m1vRdgog4-S>;v{>Bge@Pkvent@~DX>BQKSa5PYvWY7MsTbKTz^*$$D{xzIP&<%e z^1vIo>+PQLcIOY>@vfbg*HS<5_zXO!_s@D2`kxp1DyvJ6!tk z?6b}w$FqY&mkASri{F|C71YXa(xtFy2mDMtN#y`5XS^%#czdSho+rh}8^JQa1IRdu z5Guek7IlG2x=7*C>2gGYr|62{vsL+{#RmzGRo&#`kM&i7x^~a{ELpgRGIIu5R{QYS0#(Tx9*B^a45w=~X6|-`t-E_=|B@AlX@{w#}C@#b3YH@D% zW#EjHgGQNLW{CC14!dl>w4=aCLA)8fUYTB$tJ-CebcvQ51>d6VYT_HjVVr z$Jizc5QM@nSdB(gnUocc+O5@7^;TKkFw$HGWSHFR>^|?kx9qtKZx&XK$mf0cIs&6^ zcGs{k?#A~;2{dW;bNGa9qIqpt9$!2x?}WNkw`>Uv4(-AA(r6T~Hx-Y@EQ_X&{nAukyB)I@dO-VOi&mO~yC1O@=b~P-M!*|KwcJcIErSYlK z=oMRL7PIMBfFLldwkwtcYfEi!!Z71B#cXZ7>Zf2jaWJ(B8ugeu7;KE6gWJ;hzFA2U zx)LgyOhfRY8<4z&1aD7MC>o?jT_(zau;RGlT57Ex&wsxfzOo0KDSr1&-9_l0&-b{;{vD0;pY^`+H3_@16-(e*2cYPZNUAz(9MIT z@#sy|3zo)h`}Q#p+Lz57O(kbh#GDTZ$H?sI(TBILgzY9H6FZVN61E(pNi*)K5(YY8 zKt0xj;aLWz3IvgEfw}aDK;Wq}^*am`_o`R<(prffwfDj}6JrXnBuwKSfeZcl9V2qp zh=^-6lr|91)p!*T zdX-lkk5cvAwO*zUBz!!|%}UL92XX^a&HAu+kc7}{ITBStrLcELx4}Y8FLXQBb^w7# z&M?Nh9B#eSsv4KdJvk^tiA;RyfU-dFZlL$0U6=a7TlvAj-0kfB$akq3THbrz>+W{< zP6GPARJ*vrkroADLTOSs{B0oQ@M8u9kzvairaZ&kph2KWP_5=qfmA>!DWO7Oa^1}H z8`0r}Ztwa$GPCnAt53f+H2ubjG9lZqaUkf|&LxrD+=%GSv5JxDkT!&DM3bD-J z0P-Y4!8^5pE#gs6tsN3|h1;R3f~g0_f^BlzUT8xKmK06M7&Cz=N_lT-x z;wI~Ky&X5fBUQs-$lKD{Si<&Kw9T9QaTpprNc7bIN82YqF5b9Sx-?w6`0)WAc07J? z7i0x#>zK14Z~nYy!FS}4}=-b`${`HZ(lx8SW_@3`wFj5^v-m>^fh>G-@HUz9cAn-t59R(p2ZWDDQR-cL}q5?=5L_GUxtp3yo`f7u--Oi<@*Zl`CY#~fx$u>x56lD zSZinDr!B>f#3|Y#c0sla{#r<7k`w{OZwqgGF^I7t5sGCpSQ^?|LezRWYNtuz61@tK(K0Vzv@AlM}E60 sPZ7H22#|#~m#}t@09k00g`PPAWT9CRLf9Mnx-BTQjp*MH_^@01FU~?eu>b%7 literal 0 HcmV?d00001 diff --git a/oss/plugin/capabilities.py b/oss/plugin/capabilities.py new file mode 100644 index 0000000..2cc4068 --- /dev/null +++ b/oss/plugin/capabilities.py @@ -0,0 +1,73 @@ +"""能力扫描器 - 自动扫描插件支持的能力""" +import ast +from pathlib import Path +from typing import Any + + +def scan_capabilities(plugin_dir: Path) -> Any: + """扫描插件目录,自动发现支持的能力""" + capabilities: set[str] = set() + main_file = plugin_dir / "main.py" + + if not main_file.exists(): + return capabilities + + with open(main_file, "r", encoding="utf-8") as f: + source = f.read() + + tree = ast.parse(source) + + # 扫描规则: + # 1. 检查是否导出了特定的类或函数 + # 2. 检查是否有特定的装饰器或标记 + # 3. 检查 import 语句(表示依赖了某个能力) + + for node in ast.walk(tree): + # 检查类定义 + if isinstance(node, ast.ClassDef): + class_name = node.name + # 如果类名包含特定后缀,认为是能力提供者 + if class_name.endswith("Provider"): + cap_name = class_name.replace("Provider", "").lower() + capabilities.add(cap_name) + elif class_name.endswith("Mixin"): + cap_name = class_name.replace("Mixin", "").lower() + capabilities.add(cap_name) + elif class_name.endswith("Support"): + cap_name = class_name.replace("Support", "").lower() + capabilities.add(cap_name) + + # 检查函数定义 + elif isinstance(node, ast.FunctionDef): + func_name = node.name + # 检查是否有能力相关的装饰器 + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name): + if decorator.id.startswith("provides_"): + cap_name = decorator.id.replace("provides_", "") + capabilities.add(cap_name) + elif isinstance(decorator, ast.Attribute): + if decorator.attr.startswith("provides_"): + cap_name = decorator.attr.replace("provides_", "") + capabilities.add(cap_name) + + # 检查 import 语句(表示使用了某个能力) + elif isinstance(node, ast.Import): + for alias in node.names: + if "circuit" in alias.name.lower() or "breaker" in alias.name.lower(): + capabilities.add("circuit_breaker") + elif "retry" in alias.name.lower(): + capabilities.add("retry") + elif "cache" in alias.name.lower(): + capabilities.add("cache") + + elif isinstance(node, ast.ImportFrom): + if node.module: + if "circuit" in node.module.lower() or "breaker" in node.module.lower(): + capabilities.add("circuit_breaker") + elif "retry" in node.module.lower(): + capabilities.add("retry") + elif "cache" in node.module.lower(): + capabilities.add("cache") + + return capabilities diff --git a/oss/plugin/loader.py b/oss/plugin/loader.py new file mode 100644 index 0000000..a1f884c --- /dev/null +++ b/oss/plugin/loader.py @@ -0,0 +1,125 @@ +"""插件加载器 - 加载基础插件(带沙箱隔离)""" +import sys +import importlib.util +from pathlib import Path +from typing import Any, Optional + +from oss.plugin.types import Plugin + + +class Sandbox: + """沙箱隔离""" + + def __init__(self): + self._restricted_builtins = { + "__builtins__": { + "True": True, + "False": False, + "None": None, + "dict": dict, + "list": list, + "str": str, + "int": int, + "float": float, + "bool": bool, + "tuple": tuple, + "set": set, + "len": len, + "range": range, + "enumerate": enumerate, + "zip": zip, + "map": map, + "filter": filter, + "sorted": sorted, + "reversed": reversed, + "min": min, + "max": max, + "sum": sum, + "abs": abs, + "round": round, + "isinstance": isinstance, + "issubclass": issubclass, + "type": type, + "id": id, + "hash": hash, + "repr": repr, + "str": str, + "int": int, + "float": float, + "list": list, + "dict": dict, + "set": set, + "tuple": tuple, + "print": print, + } + } + + def get_safe_globals(self) -> dict: + """获取安全的 globals""" + return dict(self._restricted_builtins) + + +class PluginLoader: + """插件加载器(带沙箱隔离)""" + + def __init__(self, enable_sandbox: bool = True): + self.loaded: dict[str, Any] = {} + self.sandbox = Sandbox() if enable_sandbox else None + + def load_core_plugin(self, plugin_name: str, store_dir: str = "store") -> Optional[dict[str, Any]]: + """加载核心插件(不受沙箱限制)""" + plugin_dir = Path(store_dir) / "@{FutureOSS}" / plugin_name + return self._load_plugin(plugin_name, plugin_dir, use_sandbox=False, allow_relative=True) + + def load_sandbox_plugin(self, plugin_dir: Path) -> Optional[dict[str, Any]]: + """加载沙箱插件""" + plugin_name = plugin_dir.name + result = self._load_plugin(plugin_name, plugin_dir, use_sandbox=True, allow_relative=True) + return result + + def _load_plugin(self, plugin_name: str, plugin_dir: Path, use_sandbox: bool = True, allow_relative: bool = False) -> Optional[dict[str, Any]]: + """加载插件""" + if not (plugin_dir / "main.py").exists(): + return None + + # 清理插件名(去掉 } 等) + clean_name = plugin_name.rstrip("}") + module_name = f"plugin.{clean_name}" + spec = importlib.util.spec_from_file_location(module_name, str(plugin_dir / "main.py")) + module = importlib.util.module_from_spec(spec) + module.__package__ = module_name + module.__path__ = [str(plugin_dir)] # 启用相对导入子模块 + sys.modules[spec.name] = module + + # 沙箱模式:限制内置函数 + if use_sandbox and self.sandbox: + safe_globals = self.sandbox.get_safe_globals() + # 允许导入框架模块 + safe_globals["__builtins__"]["__import__"] = self._safe_import + spec.loader.exec_module(module) + else: + spec.loader.exec_module(module) + + if not hasattr(module, "New"): + return None + + instance = module.New() + self.loaded[clean_name] = { + "instance": instance, + "module": module, + "path": str(plugin_dir), + "name": clean_name, # 存储清理后的名称 + } + return self.loaded[clean_name] + + @staticmethod + def _safe_import(name: str, globals: dict = None, locals: dict = None, fromlist: tuple = (), level: int = 0): + """安全导入:只允许导入框架模块、标准库子集和插件自身模块""" + allowed_prefixes = ("oss.", "json.", "time.", "datetime.", "enum.", "typing.", "dataclasses.", "pathlib.", "mimetypes.", "http.", "threading.", "socket.", "asyncio.", "websockets.", "re.", "urllib.", "shutil.", "string.", "io.", "base64.", "hashlib.", "hmac.", "secrets.", "math.", "random.", "collections.", "functools.", "itertools.", "operator.", "copy.", "pprint.", "textwrap.", "unicodedata.", "struct.", "codecs.", "locale.", "gettext.", "logging.", "warnings.", "contextlib.", "abc.", "atexit.", "traceback.", "linecache.", "tokenize.", "keyword.", "ast.", "dis.", "inspect.", "types.", "__future__.", "importlib.", "pkgutil.", "sys.", "os.", "stat.", "glob.", "tempfile.", "fnmatch.", "csv.", "configparser.", "argparse.", "html.", "xml.", "email.", "mailbox.", "mimetypes.", "binascii.", "zlib.", "gzip.", "bz2.", "lzma.", "zipfile.", "tarfile.", "sqlite3.", "zlib.") + if any(name.startswith(p) for p in allowed_prefixes): + return __import__(name, globals, locals, fromlist, level) + # 允许相对导入(插件自身模块) + if level > 0: + return __import__(name, globals, locals, fromlist, level) + raise ImportError(f"插件不允许导入模块: {name}") + diff --git a/oss/plugin/manager.py b/oss/plugin/manager.py new file mode 100644 index 0000000..38fa86b --- /dev/null +++ b/oss/plugin/manager.py @@ -0,0 +1,33 @@ +"""插件管理器 - 只加载 plugin-loader""" +from typing import Any, Optional + +from oss.plugin.loader import PluginLoader + + +class PluginManager: + """管理基础插件""" + + def __init__(self): + self.loader = PluginLoader() + self.plugin_loader: Optional[Any] = None + + def load(self): + """加载基础插件""" + # 只加载 plugin-loader,其他都是可选的 + pl_info = self.loader.load_core_plugin("plugin-loader") + if pl_info: + self.plugin_loader = pl_info["instance"] + + def start(self): + """启动基础插件""" + if self.plugin_loader: + self.plugin_loader.init() + self.plugin_loader.start() + + def stop(self): + """停止基础插件""" + if self.plugin_loader: + try: + self.plugin_loader.stop() + except Exception: + pass diff --git a/oss/plugin/types.py b/oss/plugin/types.py new file mode 100644 index 0000000..9d503d8 --- /dev/null +++ b/oss/plugin/types.py @@ -0,0 +1,92 @@ +"""插件类型定义""" +from abc import ABC, abstractmethod +from typing import Any, Optional + + +# ========== 插件自定义类型注册 ========== +_plugin_types: dict[str, Any] = {} + + +def register_plugin_type(type_name: str, type_class: Any): + """注册插件自定义类型""" + _plugin_types[type_name] = type_class + + +def get_plugin_type(type_name: str) -> Optional[Any]: + """获取已注册的插件类型""" + return _plugin_types.get(type_name) + + +def list_plugin_types() -> dict[str, Any]: + """列出所有已注册的插件类型""" + return _plugin_types.copy() + + +# ========== HTTP 响应类型 ========== +class Response: + """HTTP 响应对象""" + def __init__(self, status: int = 200, headers: Optional[dict[str, str]] = None, body: str = ""): + self.status = status + self.headers = headers or {} + self.body = body + + +# ========== 插件数据类型 ========== +class Metadata: + """插件元数据""" + def __init__(self, name: str = "", version: str = "", author: str = "", description: str = ""): + self.name = name + self.version = version + self.author = author + self.description = description + + +class PluginConfig: + """插件配置""" + def __init__(self, enabled: bool = True, args: Optional[dict[str, Any]] = None): + self.enabled = enabled + self.args = args or {} + + +class Manifest: + """插件清单""" + def __init__(self, metadata: Optional[Metadata] = None, config: Optional[PluginConfig] = None, dependencies: Optional[list[str]] = None): + self.metadata = metadata or Metadata() + self.config = config or PluginConfig() + self.dependencies = dependencies or [] + + +# ========== 插件基类 ========== +class Plugin(ABC): + """插件基类""" + + @abstractmethod + def init(self, deps: Optional[dict[str, Any]] = None): + """初始化插件""" + ... + + @abstractmethod + def start(self): + """启动插件""" + ... + + @abstractmethod + def stop(self): + """停止插件""" + ... + + def meta(self) -> Manifest: + """获取插件元数据""" + return Manifest() + + def reload(self, config: Optional[dict[str, Any]] = None): + """热重载插件配置""" + pass + + def health(self) -> bool: + """健康检查""" + return True + + def stats(self) -> dict[str, Any]: + """获取插件统计信息""" + return {} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aef25c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "future-oss" +version = "1.0.0" +description = "Future OSS - 一切皆为插件的开发者工具运行时框架" +requires-python = ">=3.10" +dependencies = [ + "click>=8.0", + "pyyaml>=6.0", + "websockets>=12.0", +] + +[project.scripts] +oss = "oss.cli:main" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..552ae57 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +click>=8.0 +pyyaml>=6.0 +websockets>=12.0 diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..048c81d --- /dev/null +++ b/start.bat @@ -0,0 +1,146 @@ +@echo off +chcp 65001 >nul 2>&1 +setlocal enabledelayedexpansion + +:: ═══════════════════════════════════════════════════════════ +:: FutureOSS 启动脚本 — Windows +:: 自动检测 Python / 依赖 / 守护 / 崩溃重启 +:: ═══════════════════════════════════════════════════════════ + +set "RED=[31m" +set "GREEN=[32m" +set "YELLOW=[33m" +set "CYAN=[36m" +set "WHITE=[37m" +set "BOLD=[1m" +set "NC=[0m" + +call :color_echo "BOLD" "CYAN" "" +echo ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ +echo ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗██╔════╝ +echo █████╗ ██████╔╝ ██████╔╝ ██████╔╝ ██║ ██║██║ ███╗ +echo ██╔══╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ██║██║ ██║ +echo ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██████╔╝╚██████╔╝ +echo ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ +call :color_echo "BOLD" "WHITE" "" 一切皆为插件 · 零编译热插拔 +call :color_echo "" "WHITE" "" https://gitee.com/starlight-apk/feature-oss +echo. + +:: ── 目录 ── +cd /d "%~dp0" +set "PYTHON_CMD=" +set "PIP_CMD=" + +:: ═══════════════════════════════════════════════════════════ +:: 1. 检查 Python +:: ═══════════════════════════════════════════════════════════ +call :section "环境检测" + +where python 2>nul && set "PYTHON_CMD=python" || ( + where python3 2>nul && set "PYTHON_CMD=python3" || ( + where py 2>nul && set "PYTHON_CMD=py" || ( + call :color_echo "" "YELLOW" "" [!] 未检测到 Python + echo. + echo 请安装 Python 3.10+ : + echo ^> https://www.python.org/downloads/ + echo. + echo 安装时请勾选 "Add Python to PATH" + echo. + pause + exit /b 1 + ) + ) +) + +for /f "tokens=*" %%i in ('%PYTHON_CMD% --version 2^>^&1') do set "PY_VER=%%i" +call :color_echo "" "GREEN" "" [✓] Python: %PY_VER% + +:: ═══════════════════════════════════════════════════════════ +:: 2. 虚拟环境 & 依赖 +:: ═══════════════════════════════════════════════════════════ +call :section "依赖安装" + +if not exist ".venv" ( + call :color_echo "" "CYAN" "" [i] 创建虚拟环境... + %PYTHON_CMD% -m venv .venv +) + +set "VENV_PYTHON=.venv\Scripts\python.exe" +set "VENV_PIP=.venv\Scripts\pip.exe" + +if exist "pyproject.toml" ( + call :color_echo "" "CYAN" "" [i] 安装项目依赖... + %VENV_PIP% install -e . -q 2>nul +) + +if exist "requirements.txt" ( + call :color_echo "" "CYAN" "" [i] 安装 requirements.txt... + %VENV_PIP% install -r requirements.txt -q 2>nul +) + +:: 核心依赖兜底 +for %%p in (click pyyaml websockets) do ( + %VENV_PYTHON% -c "import %%p" 2>nul || ( + call :color_echo "" "CYAN" "" [i] 安装 %%p ... + %VENV_PIP% install %%p -q 2>nul + ) +) + +call :color_echo "" "GREEN" "" [✓] 依赖就绪 + +:: ═══════════════════════════════════════════════════════════ +:: 3. 确保 data 目录 +:: ═══════════════════════════════════════════════════════════ +if not exist "data\html-render" mkdir "data\html-render" +if not exist "data\web-toolkit" mkdir "data\web-toolkit" +if not exist "data\plugin-storage" mkdir "data\plugin-storage" +if not exist "data\DCIM" mkdir "data\DCIM" +if not exist "data\pkg" mkdir "data\pkg" + +:: ═══════════════════════════════════════════════════════════ +:: 4. 启动 +:: ═══════════════════════════════════════════════════════════ +call :section "启动 FutureOSS" + +set "RESTART_DELAY=3" +set "RESTART_COUNT=0" + +:LOOP + echo. + call :color_echo "" "CYAN" "" [i] 启动服务... + echo. + %VENV_PYTHON% -m oss.cli serve + set "EXIT_CODE=!ERRORLEVEL!" + + if !EXIT_CODE! equ 0 ( + echo. + call :color_echo "" "GREEN" "" [✓] 服务正常退出 + goto :END + ) + + set /a RESTART_COUNT+=1 + echo. + call :color_echo "" "YELLOW" "" [!] 服务异常退出 (code: !EXIT_CODE!),!RESTART_DELAY!s 后重启... (第 !RESTART_COUNT! 次) + timeout /t !RESTART_DELAY! /nobreak >nul + + if !RESTART_DELAY! lss 30 set /a RESTART_DELAY=!RESTART_DELAY!*2 + + goto :LOOP + +:END +echo. +pause +exit /b 0 + +:: ── 辅助函数 ── +:color_echo + if "%~1" neq "" set /p "=^%BOLD%%~2%%NC%" /dev/null; then + echo "$cmd" + return + fi + done + return 1 +} + +PYTHON_CMD=$(find_python || true) + +if [[ -z "$PYTHON_CMD" ]]; then + warn "未检测到 Python,正在自动安装..." + if command -v apt-get &>/dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq python3 python3-pip python3-venv + elif command -v yum &>/dev/null; then + sudo yum install -y python3 python3-pip + elif command -v pacman &>/dev/null; then + sudo pacman -Sy --noconfirm python python-pip + elif command -v brew &>/dev/null; then + brew install python + elif command -v apk &>/dev/null; then + apk add python3 py3-pip + else + err "无法自动安装 Python,请手动安装 Python 3.10+" + exit 1 + fi + PYTHON_CMD=$(find_python || true) + [[ -z "$PYTHON_CMD" ]] && { err "Python 安装失败"; exit 1; } +fi + +PY_VER=$($PYTHON_CMD --version 2>&1) +ok "Python: $PY_VER ($PYTHON_CMD)" + +# ═══════════════════════════════════════════════════════════ +# 2. 虚拟环境 & 依赖 +# ═══════════════════════════════════════════════════════════ +title "📚 依赖安装" + +VENV_DIR=".venv" +if [[ ! -d "$VENV_DIR" ]]; then + info "创建虚拟环境..." + $PYTHON_CMD -m venv "$VENV_DIR" +fi + +source "$VENV_DIR/bin/activate" +PIP_CMD="$VENV_DIR/bin/pip" + +if [[ -f "pyproject.toml" ]]; then + info "安装项目依赖 (pyproject.toml)..." + $PIP_CMD install -e . -q 2>/dev/null || $PIP_CMD install -e . --break-system-packages -q 2>/dev/null || true +fi + +if [[ -f "requirements.txt" ]]; then + info "安装 requirements.txt..." + $PIP_CMD install -r requirements.txt -q 2>/dev/null || true +fi + +# 核心依赖兜底 +for pkg in click pyyaml websockets; do + $PYTHON_CMD -c "import $pkg" 2>/dev/null || { + info "安装 $pkg ..." + $PIP_CMD install "$pkg" -q 2>/dev/null || $PIP_CMD install "$pkg" --break-system-packages -q 2>/dev/null || true + } +done + +ok "依赖就绪" + +# ═══════════════════════════════════════════════════════════ +# 3. 确保 data 目录 +# ═══════════════════════════════════════════════════════════ +mkdir -p data/html-render data/web-toolkit data/plugin-storage data/DCIM data/pkg + +# ═══════════════════════════════════════════════════════════ +# 4. 启动 +# ═══════════════════════════════════════════════════════════ +title "🚀 启动 FutureOSS" + +if $DAEMON; then + title "🔒 守护模式" + LOG_FILE="logs/futureoss.log" + mkdir -p logs + PID_FILE="logs/futureoss.pid" + + if [[ -f "$PID_FILE" ]]; then + OLD_PID=$(cat "$PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + warn "已有进程运行 (PID: $OLD_PID),正在停止..." + kill "$OLD_PID" 2>/dev/null || true + sleep 2 + fi + fi + + nohup $PYTHON_CMD -m oss.cli serve > "$LOG_FILE" 2>&1 & + NEW_PID=$! + echo "$NEW_PID" > "$PID_FILE" + ok "已启动守护进程 (PID: $NEW_PID)" + info "日志: $LOG_FILE" + info "停止: kill $(cat $PID_FILE) 或 bash start.sh stop" + sleep 2 + curl -s http://localhost:8080/health &>/dev/null && ok "服务就绪: http://localhost:8080" || warn "服务启动中,请稍候..." + exit 0 +fi + +# ── 前台模式 + 崩溃自动重启 ── +RESTART_DELAY=3 +MAX_RESTARTS=0 +RESTART_COUNT=0 + +run_server() { + $PYTHON_CMD -m oss.cli serve +} + +while true; do + run_server + EXIT_CODE=$? + + if [[ $EXIT_CODE -eq 0 ]]; then + ok "服务正常退出" + break + fi + + RESTART_COUNT=$((RESTART_COUNT + 1)) + warn "服务异常退出 (code: $EXIT_CODE),${RESTART_DELAY}s 后重启... (第 $RESTART_COUNT 次)" + sleep $RESTART_DELAY + + # 指数退避,最大 30s + if [[ $RESTART_DELAY -lt 30 ]]; then + RESTART_DELAY=$((RESTART_DELAY * 2)) + fi +done + +deactivate 2>/dev/null || true diff --git a/static/banner.svg b/static/banner.svg new file mode 100644 index 0000000..77d35a7 --- /dev/null +++ b/static/banner.svg @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python 3.10+ Apache 2.0 Plugin Driven + + + + FutureOSS + 一切皆为插件的开发者工具运行时框架 + + + + 一个空壳框架,通过插件获得无限能力 + + + + + + + + 一切皆插件 + 协议、中间件、工具 + 所有功能以插件加载 + + + + + + 依赖自动解析 + 拓扑排序 + 循环检测 + 自动识别多级依赖 + + + + + + 企业级稳定 + 熔断、降级、重试 + 隔离 + 资源限制 + + + + + + 事件驱动 + 发布/订阅事件总线 + 40+ 种系统事件 + + + + + 快速开始 + + + + + + + + + $ git clone https://gitee.com/starlight-apk/feature-oss.git + $ cd feature-oss && bash start.sh + ✓ 服务已启动 → http://localhost:8080/ + + + + 核心特性 + + + + + + + 插件热插拔 + + + + + + 依赖自动解析 + + + + + + 事件总线 + + + + + + 完整配置 + + + + + + 协议适配 + + + + + + 熔断降级 + + + + + + 🏗️ 架构设计 + + + + + + 用户层 + + CLI │ HTTP API │ WebSocket │ 社区论坛 + + 插件层 + + http-api + + ws-api + + plugin-storage + + 核心层 + + PluginManager | EventBus | Config | Logger | Loader + + + + 文档 + + + + + + + + + + + + + + + + + + + + + + 所有文档都在本地 dock/ 目录中 + + + + 项目介绍 + + + 快速开始 + + + 插件开发 + + + 插件文档 + + + 包管理 + + + 配置参考 + + + 部署运维 + + + 社区与贡献 + + + + + + + 许可证 + Apache License 2.0 + Copyright 2026 Falck + + + + + Made with by Falck + https://gitee.com/starlight-apk + + + FUTURE OSS + EVERYTHING IS A PLUGIN + + + + diff --git a/store/@{Falck}/html-render/README.md b/store/@{Falck}/html-render/README.md new file mode 100644 index 0000000..144ed17 --- /dev/null +++ b/store/@{Falck}/html-render/README.md @@ -0,0 +1,41 @@ +# HTML 渲染服务 + +将存储在 plugin-storage 中的 HTML 页面映射到 8080 端口。 + +## 功能 + +- 从 plugin-storage 读取 HTML +- 自动注册路由到 web-toolkit +- 支持动态页面访问 +- 页面管理(存储/获取/删除/列出) + +## 使用 + +```python +html_render = plugin_mgr.get("html-render") + +# 存储 HTML 页面 +html_render.store_html("index", "

Hello World

") +html_render.store_html("about", "

About

") + +# 获取页面 +html = html_render.get_html("index") + +# 列出所有页面 +pages = html_render.list_pages() # ["index", "about"] + +# 删除页面 +html_render.delete_page("about") +``` + +## 访问 + +``` +http://localhost:8080/ → index 页面 +http://localhost:8080/about → about 页面 +``` + +## 依赖 + +- web-toolkit:Web 服务 +- plugin-storage:HTML 存储 diff --git a/store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc b/store/@{Falck}/html-render/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6fc9d11cad74f674d4f693816aa2890451dcae6 GIT binary patch literal 5730 zcmb7IYj9J?72YddJ*%+tQ{t7%JtKwwa0L$FC5elbK9^^qhMoS$<@u zS&w&DyJyetp69psk=bk@PzwM1h4=3kLcYcy>7q$>9=!ye(}X9K@C>io#!v=Y^)}U3 zHC1ys#%^P`YN%$bmTI@^sE#8gq=xXCGQw*entHXuhw2?#7}YxT^(tiq*6PX#&GD$L z`tW)f+p@EHJ2yV^@%V+`kDu>N9>2)daZ^2SP2PT;a|isLUXN|B81QpbM|vl2Uis$6 ziHYkM#zuO^&%8c%{nO-;_mh3^O(EwNDV(c|;vjHgLRCCN)x3(byc!Oz;q!ToL%UMNYs<)9 zb}vJ9@@4eA9QwDk_rn zG!O_mJG}HVXh9Twog#APqz3z*z<$9V^7;?iHwxmuP#|c3f#>bGUa+tG%_f)6z3&x! z);;X|U0%N}cu3MaonF5;LRP`wy z9040whgZRHGV*rv(i_PWXF!LOSFb1g&djjw87N+~tZTeg=i+Z0`=9Pz**QM|NBUb78ob)Q*U{k02Go)us zp9Va`In?PLx$*OVQe3RNAj|ve zQch*?LcBic?DYDC@0Gp{sU)))a)rEZXOIe=-h6L>8mjQ(IXf#? zgVQQE#p`d}@O*QN)kM)&B;6jD=n91>t-xPuj}VfKPEnv;f)lry9Gixu;yD399!D=1Z$Z^V9%zXvudk`E>HLd> zJ1_0JxNCSzyrL;q(UgGU=3~uq6BjdaaZ_!~R6EEI)yAJ}j6KVc)>M3xR>akZ)xBXyx;X>cyAJFP2}bx>z+_b;mkdz5QDqTc7)wux86+12Gpw z3mWd38Xs7Qsbp@n!l*4rD-7$WiHEVO!dmDiUp+tm;kzxD36+B(lBEMDWU=5pffI+( z;Y9>fa^O^U%g#L5mv=*^r^zq#rW3QgGkWXfm0JLLGJe1%ue>vM>kR-pvQ1o8;<=>v z`av;%w=hGACRBYUUsuf4W!Cg^*h|FF2xl$fdd)xnw2jlJRghL)3LvD;{LPI>^3>G~ zY{L|IbU*=b-`qHcH$vN`PvJpOGU?P*fVW6tw-iTd(Y$w=NhC8$-ROy`UejD{qh@9K z`#_>FN4&OwhZTNemua=>DbF*HpIKlD)VI*-T?V5Ws`n2WEeEX zS*1-~2idPRlVzr-(^7!5WX|o$sV2)wMlK-7P8CP02nT;H%}wem(#cqj;acD_7X?9> zS3pn({&ZK`>>aLGNG1$IQ1*u;9TmiYuS-z4urex>jLN;7 zL3qJPOs8a)K^+x*kU_cx5n~buMMiA21li5?>1^b;5=g{esCvj)aapu{^=R>$h%sR) zi(9H=mg=}=QOvSPj>9|en%WXeR@_+=E87v-*t=~kzo>uT;ELfVzQ}JF`Ne&Rvfj76 zL(Dl}KYPmfothLhKG2iWiVMa8;|0rrWymnReYE6f5p$xnHeR|cR=R9xf2?$Mykujn zWaDVbrU=_>{>})i?&UXpFJdzLi)!Ke#p=HaS+cWqOV+yZ*ifLncU4y!oWX%8y;|&d}S^{3S9hfO^LQZ~4PF?(D z>g{&{XQdw3vjVl^knj>gymaLKskaBldtROV)>_iI2G0GSx^#nB{4s$9_DCJP#DkC$g5T1{F z1vh}96emr7P(b9+Trnu})-$`G**cL0c4ZSj2+!WcIuoTBF zH8D%gU}M~3i&<=mvg&x*vRK)&p{=83tNL~ST2S^?Wlg+tRjhK=h;FoUUH|6$<%<$k zHHm6_^yzI;ZhNAl7NE|;;s=B+Dh8;NS8`SyDvw#}qxyO|_em_O46w2g3RC|KufxoR zKJ};vD)>l%Uwa@MwW;RyG{|UeO#15%WXSocz&`&ED#&~@s2H%@oG>Pqz%mKLot`NbQdq(1Nhq^KG_8S?KO@iyR2?ew0Bz^ zPiI!h1BFXP>YhQj7szt6$nf{i=TI$OTaCaUJ%2*p`Z2p2I=E z=kVU3=dj1ydD!a?9EQ{qv|78(S!^B8SXGjW3X)n3?}pcb{4C!Aho?MLA}S+ec<9B_ z;<<)5k8R6~Hv@eS`C6asVK`7))ftot>kWtr0deRu{FeyHh)A*_<`@QuNos zDD4`x9Eh+9%YukeHXJLfI3EUXB`?ZI4TGCCAHUJ~UHb+2Mq_X~_Xj${;E7K71mS!+ z?DC~%EKcx8Du#Sszu*tRw9e`52)Lb2il_oU%6Q!&T88ZL`9|xodIl>r1Uc1F^gg-- zD_I;x&a_YClgPUpo_`{+TaE-pSm9?4s!vyl1I4XS9VK6~>mIVKrs7+ZTGPy&E&LC` z_MstPv-;srR2uU`y-HJ!757kY(rjiPF0^Q#eNdC5DI8kxfIu_6;5%%TZEy!S{AUUp zpzKPB-m^hv8Z6bqmqC~Uyepq>=7}YBi*SJAch}wUFCt+Nxo<2VHC9B~3Pl?R`YRZw z_&rb3Ay)8tcPlU?FCdCGC8pc(Gp9&dq(twQgPyV@a-StANxw$X+;h|gvk-@g2pNn# zP%{kkCE4;1V*Dp5`8U}RBO4xrTQi0IU5^R2-v(8LDT)>?iCP=(lEz0`hN+Z~`9Djk Bnil{7 literal 0 HcmV?d00001 diff --git a/store/@{Falck}/html-render/main.py b/store/@{Falck}/html-render/main.py new file mode 100644 index 0000000..5fa509e --- /dev/null +++ b/store/@{Falck}/html-render/main.py @@ -0,0 +1,99 @@ +"""HTML 渲染服务 - 通过 config.json 配置,统一文件入口""" +import json +from pathlib import Path +from oss.plugin.types import Plugin, register_plugin_type, Response + + +class HtmlRenderPlugin(Plugin): + """HTML 渲染插件 - 渲染服务由 html-render 提供""" + + def __init__(self): + self.http_api = None + self.storage = None # plugin-storage 入口 + self.config = {} + self.root_dir = None # 解析后的网站根目录 + + def init(self, deps: dict = None): + """初始化 - 读取 config.json 并解析网站根目录""" + self._load_config() + print(f"[html-render] 配置加载完成: root_dir={self.root_dir}") + + def start(self): + """启动 - 注册路由到 http-api,共享配置给 web-toolkit""" + # 注册首页路由 + if self.http_api and hasattr(self.http_api, 'router'): + self.http_api.router.get("/", self._serve_html) + print("[html-render] 已注册路由到 http-api") + else: + print("[html-render] http-api 未加载") + + # 将配置共享给 web-toolkit(通过 plugin-storage 的 DCIM 共享存储) + if self.storage: + shared = self.storage.get_shared() + shared.set_shared("html-render-config", { + "root_dir": str(self.root_dir), + "index_file": self.config.get("index_file", "index.html"), + "static_prefix": self.config.get("static_prefix", "/static"), + }) + print("[html-render] 配置已共享到 DCIM") + + def stop(self): + """停止""" + pass + + def set_http_api(self, instance): + """设置 http-api 实例""" + self.http_api = instance + + def set_plugin_storage(self, instance): + """设置 plugin-storage 实例(唯一文件读写入口)""" + self.storage = instance + + def _load_config(self): + """读取 config.json,解析根目录""" + config_path = Path("./data/html-render/config.json") + if not config_path.exists(): + print("[html-render] 警告: config.json 不存在,使用默认配置") + self.config = {"root_dir": "../website", "index_file": "index.html"} + else: + with open(config_path, "r", encoding="utf-8") as f: + self.config = json.load(f) + + # 解析根目录(相对于 config.json 的路径) + root_relative = self.config.get("root_dir", "../website") + self.root_dir = (config_path.parent / root_relative).resolve() + + def _serve_html(self, request): + """提供 HTML 页面 - 通过 plugin-storage 读取并注入静态资源路径""" + index_file = self.config.get("index_file", "index.html") + if self.storage: + storage = self.storage.get_storage("html-render") + if storage.file_exists(index_file): + content = storage.read_file(index_file) + if content: + # 注入静态资源路径(相对路径 → /website/ 前缀) + content = self._inject_static_paths(content) + return Response( + status=200, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=content + ) + return Response(status=404, body="Not Found") + + def _inject_static_paths(self, html: str) -> str: + """将相对静态资源路径替换为 /website/ 前缀""" + import re + # href="css/xxx" → href="/website/css/xxx" + html = re.sub(r'(href\s*=\s*["\'])css/', r'\1/website/css/', html) + # src="js/xxx" → src="/website/js/xxx" + html = re.sub(r'(src\s*=\s*["\'])js/', r'\1/website/js/', html) + # src="logo.svg" → src="/website/logo.svg" + html = re.sub(r'(src\s*=\s*["\'])(?!https?://|/)([\w.-]+\.(svg|png|jpg|gif|ico|webp))', r'\1/website/\2', html) + return html + + +register_plugin_type("HtmlRenderPlugin", HtmlRenderPlugin) + + +def New(): + return HtmlRenderPlugin() diff --git a/store/@{Falck}/html-render/manifest.json b/store/@{Falck}/html-render/manifest.json new file mode 100644 index 0000000..71794de --- /dev/null +++ b/store/@{Falck}/html-render/manifest.json @@ -0,0 +1,17 @@ +{ + "metadata": { + "name": "html-render", + "version": "1.0.0", + "author": "Falck", + "description": "HTML 渲染服务 - 提供 8080 端口的 HTML 页面服务", + "type": "utility" + }, + "config": { + "enabled": true, + "args": { + "html_dir": "./data/html-render" + } + }, + "dependencies": ["http-api", "plugin-storage"], + "permissions": ["http-api", "plugin-storage"] +} diff --git a/store/@{Falck}/web-toolkit/README.md b/store/@{Falck}/web-toolkit/README.md new file mode 100644 index 0000000..1141cfa --- /dev/null +++ b/store/@{Falck}/web-toolkit/README.md @@ -0,0 +1,71 @@ +# web-toolkit Web 工具包 + +提供静态文件服务、模板渲染、路由等 Web 开发工具。 + +## 功能 + +- **静态文件服务**:提供 HTML/CSS/JS/图片等静态文件 +- **模板引擎**:支持变量替换、条件判断、循环 +- **路由管理**:为 HTTP 和 TCP 服务器注册路由 +- **自动首页**:自动查找 index.html + +## 使用 + +```python +web = plugin_mgr.get("web-toolkit") + +# 设置目录 +web.set_static_dir("./public") +web.set_template_dir("./templates") + +# 添加自定义路由 +web.add_route("GET", "/api/hello", lambda req: { + "status": 200, + "headers": {"Content-Type": "application/json"}, + "body": '{"message": "Hello"}' +}) + +# 渲染模板 +html = web.render_template("page.html", {"title": "My Page", "items": [1, 2, 3]}) +``` + +## 模板语法 + +```html + +

{{ title }}

+

{{ description }}

+ + +{% if show_content %} +
{{ content }}
+{% endif %} + + +
    +{% for item in items %} +
  • {{ item }}
  • +{% endfor %} +
+``` + +## 配置 + +```json +{ + "config": { + "args": { + "host": "0.0.0.0", + "port": 8080, + "static_dir": "./static", + "template_dir": "./templates", + "index_files": ["index.html", "index.htm"] + } + } +} +``` + +## 依赖 + +- http-api:HTTP 服务 +- http-tcp:TCP HTTP 服务 diff --git a/store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a20a4b965632d1b6a0316522bbcfc25c18e05be GIT binary patch literal 8161 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 JO`UYB{{cHY_vQcq literal 0 HcmV?d00001 diff --git a/store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7faa2ea705cdc3a4b94b24c43ac358a6dc4248df GIT binary patch literal 3599 zcmb_f-ESLJ7Qf^9@Fb4o#BpNB2{ip!TBo&BXi}iCWl;%*LIPx(5~-0!6MJx6rnb2= zrY&tHv;veutELiBUF}L3Ef3C0tENw7g@pJQiWTCHkU&}m=$j#|`nKA0?)WQKDqeae zpL_3b?#DUj{?57A8(wcafs!BlHZ|`iq1*CnSu2%EYlh7~pFS6NmUN_4?QF!Lu|#GGqQiOh5vP&;L2$l&Vj{YKRhge1u_HyPn6Zbm&QIzTK7wqWYUTP?7On+-v+ z_nB6P>LY(SuUHy2QptHMs%784IC4gxi&~kC@pj4@p=b(?WzX5Ns;1H@OI7Xenu)O* z1cyq@ZV>O2a^K)(PdN~}+*S@gdZl9nK=3?MsG-OtI!aa3+G!ouYgg6NnMBUOc899I zlhcgq7oV!Wm7=C)q|$mi1E1Zhn#jbV!J8JPXbJU$uzOY2v@|OfS2fF`sX0*ERMlld z12_aVqjF8)xtQBPd_?XFfr^V4x8=S+C-q?VRXYj3r>!}=-C07Eos@To=!My@c~i~7iOW| zrX><8v)}G%*jWdAaK-?L_sNfP$5qcI&zc;*BZvR~RN=*y-KBv;tMW6<^mw!8s4=tE z33;%!`BdN|7KNMf*Q^06ToIlnm!spyt9j1mp*^|G(Z^u)GS3dDkqu1T0HWS8gHP&~ z?QIwU-5`A5z=LvB@4VExn9CPUU!Sfk$wT{rI`Y}vCO<+KGYiG5EL#W>Q;`Xan-KlwCe$5_{KG!=7H|byTIC5ddN#J6OtCDg(*z)EoItuXoipk%$w*Xm>e)& zv=o?$$-AXMN~rFJ!G?vc@)WEhOd&yA$~0F?9j0rD>>?fl!6!x}mO|FQ_~7TokAAs% z`=?ufb#Au^OlLu7b0d=xiSEPx4irc$TT)MJR(!q@mJ!j3(`RZ!oXMh-V$tIev$d#| zocBWWsXEAR7-uFxKibi1*Ui1ldsp<^r*574X4l&A(bDkIa&Yka#cLM}ZVTaCCwHHp-`86|I;}0-&7rgL*P1zIE;uKzQ zEd?T>x_XUk8Z!0~LS|0j+KhRy1~5usyyB8VL}bi@Yia%K57saL``(9Fj@#TJdKlJz zF(TR`JbY4FsIo@NvUx*K+x&X~w8Q&{TIQLQH6L+1oH_zi96EWchoxq5jNOi}&uW{? z+MH&hdB`&;&Q1Ot0L?sec>4sP=7%7FXCD72&y{*7{?2`eQ}2)PG>%*!MpF;-5>t?O!Ox@ITa& zZxs0UeP|#*@m=WPHDQq}2Se8{T)R*>vKHJ^3hpVs@vjqi2Zn%!{ZCdT5(oi(84>Ar zP-ij{ppXX$?L&b#84aQ6M}dzL8b*QiWVfRemXBk148_wRY$1_~TNFc%;$HyzJOk(8 zAxzO56t5mSh@$bRd=$GVFh!f6gTO~+@Hc@IBNfRdK3m``1ghczR(%=z9eWkoB}Vh? zOSCYLP!yq9z6VFe3TF|DJz&)SJX>pjk*&4ABDaZwe4;|2Dx6*6Kfm}pHYJqt25+6!efSS?98;bkgxp!vUB*nQ<*v+w!x{d;Ks~Uz>G^>S-yy^IC6U|qD*j|#+Hv)p~{eQ;p&q)9P literal 0 HcmV?d00001 diff --git a/store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc b/store/@{Falck}/web-toolkit/__pycache__/static.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ee43f73624c499c90e61744e92179e2d316a3bd GIT binary patch literal 3645 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 Sc)l_3FmO+DUlUfaJ^mYPNh3)B literal 0 HcmV?d00001 diff --git a/store/@{Falck}/web-toolkit/main.py b/store/@{Falck}/web-toolkit/main.py new file mode 100644 index 0000000..c1ae7ff --- /dev/null +++ b/store/@{Falck}/web-toolkit/main.py @@ -0,0 +1,158 @@ +"""Web 工具包 - 路由注册、静态文件服务、前端事件(不负责渲染)""" +import json +from pathlib import Path +from oss.plugin.types import Plugin, register_plugin_type, Response +from .router import WebRouter +from .static import StaticFileHandler +from .template import TemplateEngine + + +class WebToolkitPlugin(Plugin): + """Web 工具包插件 - 提供网站前端所有服务""" + + def __init__(self): + self.router = None + self.static_handler = None + self.template_engine = None + self.http_api = None + self.http_tcp = None + self.storage = None + self.config = {} # 从 config.json 读取 + self.root_dir = None + + def init(self, deps: dict = None): + """初始化 - 读取 config.json 配置""" + self.router = WebRouter() + self.template_engine = TemplateEngine() + self._load_config() + self.static_handler = StaticFileHandler(root=str(self.root_dir)) + print(f"[web-toolkit] 配置加载完成: root_dir={self.root_dir}") + + def start(self): + """启动""" + # 注册路由到 http-api + if self.http_api: + http_instance = self.http_api + if hasattr(http_instance, "router"): + # 精确路由先注册,参数化路由后注册 + http_instance.router.get( + self.config.get("website_prefix", "/website") + "/", + self._serve_website_index + ) + http_instance.router.get( + self.config.get("website_prefix", "/website") + "/:path", + self._serve_static + ) + http_instance.router.get( + self.config.get("static_prefix", "/static") + "/:path", + self._serve_static + ) + + # 注册路由到 http-tcp + if self.http_tcp: + tcp_instance = self.http_tcp + if hasattr(tcp_instance, "router"): + tcp_instance.router.get( + self.config.get("website_prefix", "/website") + "/", + self._serve_website_index + ) + tcp_instance.router.get( + self.config.get("website_prefix", "/website") + "/:path", + self._serve_static + ) + tcp_instance.router.get( + self.config.get("static_prefix", "/static") + "/:path", + self._serve_static + ) + + print("[web-toolkit] Web 工具包已启动") + + def stop(self): + """停止""" + pass + + def set_http_api(self, instance): + """设置 HTTP API 实例""" + self.http_api = instance + + def set_http_tcp(self, instance): + """设置 HTTP TCP 实例""" + self.http_tcp = instance + + def set_plugin_storage(self, instance): + """设置 plugin-storage 实例(唯一文件读写入口)""" + self.storage = instance + + def set_static_dir(self, path: str): + """设置静态文件目录""" + self.static_handler.set_root(path) + + def set_template_dir(self, path: str): + """设置模板目录""" + template_root = Path(path) + if template_root.exists(): + self.template_engine.set_root(str(template_root)) + + def _load_config(self): + """读取 config.json,解析网站根目录""" + config_path = Path("./data/web-toolkit/config.json") + if not config_path.exists(): + print("[web-toolkit] 警告: config.json 不存在,使用默认配置") + self.config = { + "root_dir": "../website", + "index_file": "index.html", + "static_prefix": "/static", + "website_prefix": "/website", + } + else: + with open(config_path, "r", encoding="utf-8") as f: + self.config = json.load(f) + + # 解析根目录(相对于 config.json 的路径) + root_relative = self.config.get("root_dir", "../website") + self.root_dir = (config_path.parent / root_relative).resolve() + + # 初始化模板引擎 + template_dir = self.config.get("template_dir", "") + if template_dir: + template_path = self.root_dir / template_dir + if template_path.exists(): + self.template_engine.set_root(str(template_path)) + + def _serve_website_index(self, request): + """提供 website 目录首页""" + index_file = self.config.get("index_file", "index.html") + if self.root_dir: + path = self.root_dir / index_file + if path.exists(): + content = path.read_text(encoding="utf-8") + return Response( + status=200, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=content + ) + return Response(status=404, body="Index file not found") + + def _serve_static(self, request): + """提供静态文件""" + path = request.path + website_prefix = self.config.get("website_prefix", "/website") + static_prefix = self.config.get("static_prefix", "/static") + + if path.startswith(website_prefix + "/"): + filename = path[len(website_prefix) + 1:] + elif path.startswith(static_prefix + "/"): + filename = path[len(static_prefix) + 1:] + else: + filename = path.lstrip("/") + + if not filename: + return self._serve_website_index(request) + return self.static_handler.serve(filename) + + +register_plugin_type("WebToolkitPlugin", WebToolkitPlugin) + + +def New(): + return WebToolkitPlugin() diff --git a/store/@{Falck}/web-toolkit/manifest.json b/store/@{Falck}/web-toolkit/manifest.json new file mode 100644 index 0000000..5e6ad53 --- /dev/null +++ b/store/@{Falck}/web-toolkit/manifest.json @@ -0,0 +1,21 @@ +{ + "metadata": { + "name": "web-toolkit", + "version": "1.0.0", + "author": "Falck", + "description": "Web 工具包 - 提供静态文件服务、模板渲染、路由等 Web 开发工具", + "type": "utility" + }, + "config": { + "enabled": true, + "args": { + "host": "0.0.0.0", + "port": 8080, + "static_dir": "./static", + "template_dir": "./templates", + "index_files": ["index.html", "index.htm"] + } + }, + "dependencies": ["http-api", "http-tcp", "plugin-storage"], + "permissions": ["http-api", "http-tcp", "json-codec", "plugin-storage"] +} diff --git a/store/@{Falck}/web-toolkit/router.py b/store/@{Falck}/web-toolkit/router.py new file mode 100644 index 0000000..7fbb571 --- /dev/null +++ b/store/@{Falck}/web-toolkit/router.py @@ -0,0 +1,63 @@ +"""Web 路由器""" +from typing import Callable, Optional, Any + + +class WebRoute: + """Web 路由""" + def __init__(self, method: str, path: str, handler: Callable): + self.method = method + self.path = path + self.handler = handler + + +class WebRouter: + """Web 路由器""" + + def __init__(self): + self.routes: list[WebRoute] = [] + + def add_route(self, method: str, path: str, handler: Callable): + """添加路由""" + self.routes.append(WebRoute(method, path, handler)) + + def get(self, path: str, handler: Callable): + """GET 路由""" + self.add_route("GET", path, handler) + + def post(self, path: str, handler: Callable): + """POST 路由""" + self.add_route("POST", path, handler) + + def put(self, path: str, handler: Callable): + """PUT 路由""" + self.add_route("PUT", path, handler) + + def delete(self, path: str, handler: Callable): + """DELETE 路由""" + self.add_route("DELETE", path, handler) + + def handle(self, request: dict) -> Optional[Any]: + """处理请求""" + method = request.get("method", "GET") + path = request.get("path", "/") + + for route in self.routes: + if route.method == method and self._match(route.path, path): + return route.handler(request) + + return None + + def _match(self, pattern: str, path: str) -> bool: + """路径匹配""" + if pattern == path: + return True + if ":" in pattern: + pattern_parts = pattern.strip("/").split("/") + path_parts = path.strip("/").split("/") + if len(pattern_parts) != len(path_parts): + return False + for p, a in zip(pattern_parts, path_parts): + if not p.startswith(":") and p != a: + return False + return True + return False diff --git a/store/@{Falck}/web-toolkit/static.py b/store/@{Falck}/web-toolkit/static.py new file mode 100644 index 0000000..af241df --- /dev/null +++ b/store/@{Falck}/web-toolkit/static.py @@ -0,0 +1,69 @@ +"""静态文件处理器""" +import os +import mimetypes +from pathlib import Path +from typing import Optional, Any + +from oss.plugin.types import Response + + +class StaticFileHandler: + """静态文件处理器""" + + def __init__(self, root: str = "./static"): + self.root = root + self._ensure_root() + + def _ensure_root(self): + """确保静态目录存在""" + Path(self.root).mkdir(parents=True, exist_ok=True) + + def set_root(self, path: str): + """设置静态文件根目录""" + self.root = path + self._ensure_root() + + def serve(self, filename: str) -> Optional[Response]: + """提供静态文件""" + file_path = Path(self.root) / filename + + # 安全检查:防止目录遍历 + try: + file_path.resolve().relative_to(Path(self.root).resolve()) + except ValueError: + return Response(status=403, body="Forbidden") + + if not file_path.exists() or not file_path.is_file(): + return Response(status=404, body="File not found") + + # 检测 MIME 类型 + content_type, _ = mimetypes.guess_type(str(file_path)) + if not content_type: + content_type = "application/octet-stream" + + # 读取文件内容 + try: + if content_type.startswith("text/") or content_type in ( + "application/json", "application/javascript", "application/xml" + ): + content = file_path.read_text(encoding="utf-8") + else: + content = file_path.read_bytes() + + return Response( + status=200, + headers={ + "Content-Type": content_type, + "Cache-Control": "public, max-age=3600", + }, + body=content, + ) + except Exception as e: + return Response(status=500, body=f"Error reading file: {e}") + + def list_files(self) -> list[str]: + """列出静态文件""" + root_path = Path(self.root) + if not root_path.exists(): + return [] + return [f.name for f in root_path.iterdir() if f.is_file()] diff --git a/store/@{Falck}/web-toolkit/template.py b/store/@{Falck}/web-toolkit/template.py new file mode 100644 index 0000000..4b126f8 --- /dev/null +++ b/store/@{Falck}/web-toolkit/template.py @@ -0,0 +1,144 @@ +"""模板引擎""" +import re +import ast +from pathlib import Path +from typing import Any, Optional + + +class TemplateEngine: + """简单模板引擎""" + + def __init__(self, root: str = "./templates"): + self.root = root + self._cache: dict[str, str] = {} + self._ensure_root() + + def _ensure_root(self): + """确保模板目录存在""" + Path(self.root).mkdir(parents=True, exist_ok=True) + + def set_root(self, path: str): + """设置模板根目录""" + self.root = path + self._ensure_root() + self._cache.clear() + + def render(self, name: str, context: dict[str, Any]) -> str: + """渲染模板""" + template = self._load_template(name) + return self._render_template(template, context) + + def _load_template(self, name: str) -> str: + """加载模板""" + if name in self._cache: + return self._cache[name] + + template_path = Path(self.root) / name + if not template_path.exists(): + raise FileNotFoundError(f"模板不存在: {name}") + + content = template_path.read_text(encoding="utf-8") + self._cache[name] = content + return content + + def _safe_eval(self, expression: str, context: dict) -> Any: + """安全评估表达式(仅允许简单的属性访问和比较)""" + # 只允许访问 context 中的变量 + # 支持的运算符: and, or, not, ==, !=, <, >, <=, >=, in + # 不允许函数调用、导入、属性访问等 + + # 使用 AST 解析并验证 + try: + tree = ast.parse(expression, mode='eval') + except SyntaxError: + return False + + # 验证 AST 节点 + if not self._validate_ast(tree.body[0].value, set(context.keys())): + return False + + # 在受限环境中评估 + try: + return eval(expression, {"__builtins__": {}}, context) + except Exception: + return False + + def _validate_ast(self, node: ast.AST, allowed_names: set) -> bool: + """验证 AST 只包含安全的操作""" + if isinstance(node, ast.Name): + return node.id in allowed_names or node.id in ('True', 'False', 'None') + elif isinstance(node, ast.Constant): + return True + elif isinstance(node, ast.BoolOp): + return all(self._validate_ast(v, allowed_names) for v in node.values) + elif isinstance(node, ast.Compare): + return (self._validate_ast(node.left, allowed_names) and + all(self._validate_ast(c, allowed_names) for c in node.comparators)) + elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + return self._validate_ast(node.operand, allowed_names) + elif isinstance(node, ast.Attribute): + # 不允许属性访问(防止绕过安全限制) + return False + elif isinstance(node, ast.Call): + # 不允许函数调用 + return False + elif isinstance(node, ast.Subscript): + # 允许简单的索引访问 + return (self._validate_ast(node.value, allowed_names) and + self._validate_ast(node.slice, allowed_names)) + return False + + def _render_template(self, template: str, context: dict[str, Any]) -> str: + """渲染模板内容""" + # 替换 {{ variable }} + def replace_var(match): + var_name = match.group(1).strip() + value = context.get(var_name, "") + if isinstance(value, (dict, list)): + import json + return json.dumps(value, ensure_ascii=False) + return str(value) + + result = re.sub(r'\{\{(.*?)\}\}', replace_var, template) + + # 处理 {% if condition %} ... {% endif %} + result = self._process_if(result, context) + + # 处理 {% for item in list %} ... {% endfor %} + result = self._process_for(result, context) + + return result + + def _process_if(self, template: str, context: dict) -> str: + """处理 if 条件""" + pattern = r'\{%\s*if\s+(.*?)\s*%\}(.*?){%\s*endif\s*%\}' + + def replace_if(match): + condition = match.group(1).strip() + content = match.group(2) + # 安全条件评估 + value = self._safe_eval(condition, context) + return content if value else "" + + return re.sub(pattern, replace_if, template, flags=re.DOTALL) + + def _process_for(self, template: str, context: dict) -> str: + """处理 for 循环""" + pattern = r'\{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%\}(.*?){%\s*endfor\s*%\}' + + def replace_for(match): + item_name = match.group(1) + list_name = match.group(2) + content = match.group(3) + + items = context.get(list_name, []) + if not isinstance(items, list): + return "" + + result = "" + for item in items: + loop_context = {**context, item_name: item} + result += self._render_template(content, loop_context) + return result + + return re.sub(pattern, replace_for, template, flags=re.DOTALL) diff --git a/store/@{FutureOSS}/circuit-breaker/README.md b/store/@{FutureOSS}/circuit-breaker/README.md new file mode 100644 index 0000000..91cccd9 --- /dev/null +++ b/store/@{FutureOSS}/circuit-breaker/README.md @@ -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("熔断器已触发") +``` diff --git a/store/@{FutureOSS}/circuit-breaker/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/circuit-breaker/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58714dc4a730150487e924cd2cdfc6c12da32d8d GIT binary patch literal 3650 zcmbVP-ESLN6~EUr<8Q|yvC|}N5+~a>xR5xdP1=pNu<5p4Xh|p;S&Gz1Bd;@Yr*&;} zXUtNeqDY7(U3HZTHppmS$`h$pt3^Bjf(2>$3tX}i?x+$Ew554V)*BVU3+LQBo;Y3? zA+F?e@7&LG&i$R=J+4P0K>~$7`-}0Rmyo~XN24fCqrL)-b)phRRH}L|(Tg7DxhOFy zL3+suQKf#O%99>3o5{RSfj+O>r|>=>^!XP(lae#jyC}hGdC^^)AiT%Bs5Dk4h#Kf8 z%$Ex$L*-+z?jLWx`}AiYJYDpHv4ir+Ps zbhd@wEyqDLD=o};w(_Gab(FyhU5ieucY#alMQky)rD!&t{qn5T@$lTEFfOVDW=G9_;$@VfG zjxE?7`!~!w*uX_Zh95WkG&I)971t#TT@8E`q=kuj2YI-pQPOl2nrZ1UVW>^g!_d<# zgl(Bcu#LWC-5_jV!*h6G0t)NEZf_PGYU%dCeia)RI4*{9OLG9k3VGC(xEI^(I=YE`df-1;i}tK`urI)jY_pVca|b0xmAvy>{#Nt-nSS529#L^k~(8lpDa1D*$T@BPAI^ zT;RO|E`pkNqqtPgV>_&A-zjJE&PY_#mJMcFd842giZJZcw54KJ(^wQXu_e=Dwqz76 z)(f9P&A6tSR>m^2TE?=N@kZIwO-;K`gbsK&45&&7j3V$!< zy>|k&zL7f}wcfr}`A(!3AApb#a?kDqJ31AwyWS7xtOI?u1G`G67YNDp5pu;f49%R! zuxu$E@amk98iAnGL6W8;B<%rel+Mv9T98$_*+Ht(OqprK%iqGCg|^2d&!7gI*-SmNNYjSOpQoFVG!JD>}R`1!YEArpx4vG(?tZ(l=+z z#gbkqV?evONsRLE(ZrMA{`%?dpFa6$g_oyde+&Xp z5Q?r%-=41YY=(v&bsel+-#GkX@_usT;IA%K552nCHB$}GY|DW_>|wmGa)tabOT&i?aJw6{*=K&%$-T$_D+w({)<;lX!i9YVS%|FYv3 z9h=ed+Q3V9<#&U%uI|;>|Ggc6`Tye7_5OhwiF_;t&jzKBBd5-<9AmzYR zDF}&2C?_Ywc5MH>i8N?y47M+aK?rz>?5TaWfnI2Sb|>K*3xtx(N;&s|Mex}El9S#r zJVIn3!b_a(Tc1*%h`H1p}%$kB7 z*=s0xcJTimBts@wdVx_m^z#333=;Z_c-cua;MoU~B%iZ04B3O$xORKEt#6yByaJmz zx9kLV)qh9?#UN+M42;ko%odLC zc&QSp#C9PihXtaxuMqWnwnJsi-OLO|0>@qAx#Z4r7 zfO}|YGc;V4hxw6nNk3;GyR>7)adsm};hJ6|L;UX71cQ|CV+ X7ao#WHGbsY4?ZDNbuXnOT-pBsRCfJL literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/circuit-breaker/main.py b/store/@{FutureOSS}/circuit-breaker/main.py new file mode 100644 index 0000000..d660651 --- /dev/null +++ b/store/@{FutureOSS}/circuit-breaker/main.py @@ -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() diff --git a/store/@{FutureOSS}/circuit-breaker/manifest.json b/store/@{FutureOSS}/circuit-breaker/manifest.json new file mode 100644 index 0000000..324bc58 --- /dev/null +++ b/store/@{FutureOSS}/circuit-breaker/manifest.json @@ -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": [] +} diff --git a/store/@{FutureOSS}/dependency/README.md b/store/@{FutureOSS}/dependency/README.md new file mode 100644 index 0000000..11774d1 --- /dev/null +++ b/store/@{FutureOSS}/dependency/README.md @@ -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"] +} +``` diff --git a/store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/dependency/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b7d5ff327bcde14617676818fa50a9fdc3cfb33 GIT binary patch literal 6728 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 literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/dependency/main.py b/store/@{FutureOSS}/dependency/main.py new file mode 100644 index 0000000..dcea002 --- /dev/null +++ b/store/@{FutureOSS}/dependency/main.py @@ -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() diff --git a/store/@{FutureOSS}/dependency/manifest.json b/store/@{FutureOSS}/dependency/manifest.json new file mode 100644 index 0000000..e2ba06b --- /dev/null +++ b/store/@{FutureOSS}/dependency/manifest.json @@ -0,0 +1,15 @@ +{ + "metadata": { + "name": "dependency", + "version": "1.0.0", + "author": "FutureOSS", + "description": "依赖解析 - 拓扑排序 + 循环依赖检测", + "type": "core" + }, + "config": { + "enabled": true, + "args": {} + }, + "dependencies": [], + "permissions": [] +} diff --git a/store/@{FutureOSS}/hot-reload/README.md b/store/@{FutureOSS}/hot-reload/README.md new file mode 100644 index 0000000..79b7df7 --- /dev/null +++ b/store/@{FutureOSS}/hot-reload/README.md @@ -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()` diff --git a/store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/hot-reload/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7dec9f160e98dd385a977d14a0d7f0142c335df GIT binary patch literal 9862 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 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() diff --git a/store/@{FutureOSS}/hot-reload/manifest.json b/store/@{FutureOSS}/hot-reload/manifest.json new file mode 100644 index 0000000..3856e3d --- /dev/null +++ b/store/@{FutureOSS}/hot-reload/manifest.json @@ -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"] +} diff --git a/store/@{FutureOSS}/http-api/README.md b/store/@{FutureOSS}/http-api/README.md new file mode 100644 index 0000000..d5c0c56 --- /dev/null +++ b/store/@{FutureOSS}/http-api/README.md @@ -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 + } + } +} +``` diff --git a/store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc b/store/@{FutureOSS}/http-api/__pycache__/events.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc138947156693d8c5b2b49d79b2c5b607d900c0 GIT binary patch literal 3008 zcmbtW-ESMm5#ReDdHfV9ijiwO5-V4>&WWwsk=saV)hhAHsuEK!PcjOW@Zn4yWg66x z**gZY`T;gUS_ws93ymBF2;jCawIA9#2#^Faf*}8aS-~~6eMpN&QvAk7E>iTVGj~Uq zYVC)j3vhP6IJ@`Tnc3ZmMneR~@bNzsbw440MWHo(9<%dfU{;7qC{a0;f1aD+J{q>rfLUZVO=@NSF~UMMO#Hb2+`EgKE05)G+A8dgIz zqK0WyjnJ4Hr5$RF#?=m*n2XDa>QfM6VtV>a`qQgC7EZz zS>4w2CEc=^uTV5fvw3f~&Ln!mjtR^P$q~u{61>V$p`WNc6?49vUlk64RTXIq6gDrqkF49MGQB@k|Xe<|n2+|ObaDzSQc!y}moG6D`aKg4L%1ef6--F{J z)i97Y6qsmVsu&p-pvH?;!?Kw~4XaW%Era}b__z~7`!M=^bZ~tJtYFDW-rK&#~ya2cy(JRG41HrM5RxZ&X_&NlUvc3i6XXGzp zsu7C5`kh9yyLM_(T^G9>U3+R1i}`hNPosNZO<5dX7x!)S9JqP1-ZKm?(^n2$v91g( zo%^FWO!v1UK0G7Nf_ZmFU_PkAUP67UhPxt%1C=w;uwtLcSNzwk&P)Gq4lAgOEYk`NQlp9G1tUY*<3s_BFl8 zGyDk;r8Jr2WWI_S`q}S(eQ)(Ow@YMXo*n=l^Xrw0Vb0zVXg`Q_0ErC5!l>!AhY?>k zS;sDP%I0I}bqq+2{5hPucJ8fn>)|IF@)L_EFK3oAt0VQu!G;vM99xQAS*S~WcO|(l z$=46vk&Zj~?Scwx$ARh*q-*z`0ZJ^v>hbKIB7_i;sM1c(kRr%5jWRUqFKdP$)L z*yiP{TkkEpuw`Vy1B@E;<(CW=I;)qehJrnZJ_(U_116LU1s30pSfPOBWsL!GA?tao zXFYtV$qUg`Bi{99Y&o{NP>&zJ8$VW$AG>+zPJE>4BawlB-1n2No*lwPQycNFrU3Il zEj-ZWo@3;l(5UbsH!48;!@iLB?jNucg4yAqdKH+JN1*o5^x?7(@WDXt7$mJ5%;abI zB$19#?jhvAkY3*W;MG=YiZ0T^oMBV! z|MW{hWYNvgA>f(RDrh%XakG%CUb2eDe9>l!UErhYE8t~~0|E4t-EWRAkKRr8*OUFf zG=6jOR~K*gttU?`N`LC^T|DI+$ws_;)ps@axsU8gJ4f)ia|GSG(BSwmx`m`^MVVQf=$)rB*_E73|BG44qQv>o{S>bgPF_|mbhf#*bWumwdTlI(9MO4lEV!n8dA?FIY$GFgIyf;uTn$BY&P7SY z566OJ;RmlT!=hLo&ilXx55Iy%m9t7_TFWZmol(?j*6E7RD&tdGMaxdjOe-wm>c+;W zm8_@g+T}JemO1%?l4Z$u$CfhYDzl1uW-6m9EbPikHanGNp7FV<(`Q~#rj`FVKqh^| zeKU>HQ?OUeH!BYWx)R52kl|0r;W|0|2^nbm2_OD~_j4m~c-+X20AdrxFFJd<-oFtb x&2f$-dTY@}YT!2MY;+yGO%jdnCvTIk#$$(XlVl^2uEiR$y|plKKXGr}e*vh~$R7Xz literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/http-api/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa0e51426d3fb08745e6b20303d2996f44c2ab56 GIT binary patch literal 3317 zcmcInT}%{L6uz@FvpYMxu!w-Tpsq@TtGH`htXhrMphnq9tz(}cnXJ1bj4m_lommYw zO(0E-zronh+Qg<YLniCGk4E9 z_ug~9dwwpHWo0sf@m>Bmz0XI;pZKFN#OcZ8F_;Vxg;1h!iYLKQ4!(TC)5=r6RiHwI zD1ss;#8xl$w)&{g`Rz?et$ylv=Dvj78lZs)sU<6jB87?K@8H+-?*25`ApqCk5nS(a zIj}|!6I#~ki3W1Zfw%j>fqjwf`}Rb}`Y%5D@m6GQB!BAU*ud5NPyPAZpX9&oeToa) zq8{emm&tYN2CJf4r*7FARkIF5wR^JKQx3F1N#1SSSw*AAG|Hs?nw2#TON(;MyWh;g z5}1Yd>gjYwJD#LkOIK1iQn-I%OxsN++ki7bItbuFs7K)_uXw1S@L;>Blqq6|x7nk3 z!{o4Vn4><$2Yd;=r+&o`RCc@!C;^xUl_2;lM9Wzj8n->GyN*2d!x^~{kP-}1=u2TB zZfMoCMGE;nv+TODYt2on(k>i;B!^IcQM{1k6hvm?)aY zD&J2Gha$jRF59oSl z_9)E$(_TF6g_lz@X7*|i6h!k|2wcRpWE9pz#1!KMqEyV`-RDnznZJHEf9|s>U%X38 zX6&x1ia6e)8!U0%IAl%*T#lWzb5>haq7|@^@tvB@LaMt+?MfPuYm`-~?n1cwLuQd1 zQY~k3fk6>6x&R5LMbtwV;=C4!g?Ai@&?@*a0RxCt&J7>r;ZiFFq?e3)iNCBra5~T* zIvu*?A8a03xnX$ahI`V+Q7Ks9->F@FW5-C-&f%t=_oQ931eeMN?UBaK!;PEoNn4)O zL^Vp< z5rCpLtptgif({V00e-WEb8&Dq!>EYbz2pzq-4SW=u(bHTRQC^dcff*I*}V#6IFfbb zHIiz!XEVO_T^{@9>!NQ0RE_LQt~J70rB{rKOM!180nY-V{>}ZHM}rmpiPMRZVEu5g z{(f-jI8TC$TrX!A223HRFtoyCfV3BjtirVy_2w%8(7-$juLv>E`sp=_sCd_Lif?2C^#$V4bJBI7`++EQ6s`!KZb-D$19;Z;!a+twO3mzGG&G5*ORkhnp z=Q21Cs_M~PGBYg+sp=t}T6RV^G{Xe3PgT=qN>yn!+AO5?luei8&J9SgAUWAYF$8D? z2&-0|4yLEnq-|6EAUrWGRUINua=0ZLdox9X>0x1Xog@#0#S;QAHa(Ml;`l zDm`NTgv5)>CS3E;KBpN0P@^pb~x+Oym42w_JpT26Q2l4c;R!nCZId;2kbZCezNrUDY|)NbR( z!sI*xoVTQVGw8Kz03KFqg|x-Jo2_&&NU^$FX#P0Qaohva_&W(aCYv6URgXyc5ovtx eZRX?)yPgyHJTo}LRbNmCyYG?aNiWB_eeG{Up1xlI literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/http-api/__pycache__/middleware.cpython-313.pyc b/store/@{FutureOSS}/http-api/__pycache__/middleware.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f3720772ca880255b13dc020290610f03797ac0 GIT binary patch literal 3535 zcmb_eU2Gf25#IaZ@kCP8zePEzP1|zoQ$!I#>^SBL`*W;`$O$6u@#l}; zyRnM$6~U1w)A@Y*<-G35GbP(7n(4eFoHplHIb7!*Z(P4z)-BtS3)6;rb6Np+-rI2)H#X)k$2q<+;)136jg zaD3;DY&Nf7NmG3l3{A>iK-#x?_}h;jeQ?(iGWJ!+YwA~R?ULz8RJY62%rxx<`w2nS zalR%3Ss*!1;mZLSKK$M5k1B6`_5Oz|AN(SAo7@&0(bDsmD6Y{?AQqkqhr^Cy*+r^H zPyOTvWst7VsOrz6v$kD|q)SG$&{lG+H0O9rw3yK?%jw-zeyqupQ6}E?8u>?8-x>@E z?FJH4IvrWl%ydE5G{>iDg<`gx$8kW@E|=5!=1P~QT{5U;=M7Ugi?Hm~v}`d012Jo4 zY)7zcN>M&V#VKBw;y-CW5NANsEIVx*87*zw)OfjU>z1Z51?V2!)rSP3%?KLY8U^wO z`9d6O@Vuvgal9&2)vEmVM1#Qe&KcHy68iK=?XiMa})M$9-g~ZvJm^vSrESk1%+L6RuNmqpV zr%q?^)g#EJ#eC#+K3}{NnW09`Fz27%b@9Az&la=R6IRdY>8wsIr<*qYOq+E%?t}G! zwk>$N569kk&vqlVfQNo#eFF#p@0qds==u8SiN{{?kng&1FR-sB?{D}>ck}TWAaDEW zAZ({0BtuB{B6$kQ(@1s!7~O{pczg?-!Z=03{Am~l2&Yhkmk>@30^QOP*4?@KiCjB& zb^}?i06g}8hf(+Q#avFOTY!XjWRb;rt(Q4j=0>>f{-F$1^=_71<7+_OKunuE5;lzb))jU|3WJ){W$D!VMTO= zoNhaw5;aWQkqQiQj#x_Dvx-Q2LG-4-I08tqP!|Y!;VqyF?KD{nR)`7fIUv_aJ=pVR z?nbU+EC<8ag}?cOe+~3h&Q*nXeed`df4cPSlKTFOw_jYEzSn!~e&Be$tIvI61~&Rl z)ME#5&|DM=K9AJ`4#XkR1rBrw9O&A>fw3zUk8J}Eh7@dTF(A9epS^^7FanM9n1vw; zc~vPTV`^%qDsRf43b4o|sxsTrsdms{&ZqdDu1zpa&ZZ4>^#J^ZWtcXWAoFInA_T3> z@bfBDC`ytf4T=*s`G$PI=fHB$q5JZ1 zbN<%8<(>nd%Lmz;IlfIrGR+c*T@x+AEOi0~;Atez*xSi&_DZ(B54;3ZJ_f;+5AQzw zWwre*F@>kcfbB@>Qb{+nFl9dRUEo=$MavJ4vR!Nk7yI*ZZi9`0!8sSeBla< zM~+}*ud))%g&>LzUxVgGCCnwx@XptuWpnIgzL-hpt%wSZp5o=rMMCop|h^Bt&rBkPvk`PVH=6^#5m=V+oljy%er(oB0VMCNULXZPHLXW#i z_u#MQ?#UtcGw?t}XH4mOayI@BY;cP<#HgE`G1I$sAgbh+fKxcyEkk2TMv-9Dvc3%j zGxVOXMZt5Z;p07{i?6IhX*b%C1D>IZy*OFzTRd~~wFZId(vdsIYa=H&aO|1~1;vyA zs}vl`o+}wZS;n?p|1%Lrt*IPW-9>| literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc b/store/@{FutureOSS}/http-api/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee0b2dc386772ca43f177e7261a59b7e3e34eeff GIT binary patch literal 3868 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 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() diff --git a/store/@{FutureOSS}/http-api/router.py b/store/@{FutureOSS}/http-api/router.py new file mode 100644 index 0000000..7c5b40b --- /dev/null +++ b/store/@{FutureOSS}/http-api/router.py @@ -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 diff --git a/store/@{FutureOSS}/http-api/server.py b/store/@{FutureOSS}/http-api/server.py new file mode 100644 index 0000000..155c798 --- /dev/null +++ b/store/@{FutureOSS}/http-api/server.py @@ -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 diff --git a/store/@{FutureOSS}/http-tcp/README.md b/store/@{FutureOSS}/http-tcp/README.md new file mode 100644 index 0000000..ea22683 --- /dev/null +++ b/store/@{FutureOSS}/http-tcp/README.md @@ -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 + } + } +} +``` diff --git a/store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc b/store/@{FutureOSS}/http-tcp/__pycache__/events.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a42829dd4cd6568a58a63f153fc92afd800e3f97 GIT binary patch literal 1021 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 literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc b/store/@{FutureOSS}/http-tcp/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33a8bc10c7c2afe62e79176296c5d115ef9b7cc1 GIT binary patch literal 3633 zcmb_f-ER|D7Qf^9@Fb4o#QAa@7feV=CM-#}1WIX3w}gb2-2^t1u$G848tkcK){Nad z<8~>at*W*PTM$7a!l;#MsXRCj6+!#Ju2x#@pRlQwjTu$iN~^Tx&275kbk1^d(DFl!Trz6~$_= z9B`xpfnSNy4&nn+><(S`6gvagn~FWVZnSIx2zIwMscA5cW>OSgy{N%@&5Cj{70V>C z-J&S(WYlE&i%(JBPEf;4CUi|t!DqLk#8Odcu!cz~9)Y62tu93|Ow~+871cCp;yfsA zD9Q?YT!%EWztmwhRXj!?dqHc8l!;b31f43|(3;C+l+2MD-{OR}0=&Xlf=vfQ zVrUxx2CfixtZRG0U)`q440~J?j00xu8(QRUcpS5C=jb-(BbjXGPeWrFG?{_AvCQCz zhH?%)YnOfz$`&cr!_t1@C1Xl8X@)x zEmn=iEN|_E(CxrceIPy{--<0aJ=Z)NVsK3ietsl3xjIe7y^a?}w0ldQ64ygsbfg3<@msO8zX1(gr z?Sb!G=p09N@tU~QmF3pOea}u`%s^*O%T`c>Pfa!h^+jZp9DZ{i{Yxc!H+Sb^v zWp}o7T^xLNTB23Y%vF_Sq5V`H*>q-`9ifYPgyJ>v>@>m%Ju&f{iP?#Ys|Ba~RK0|l zmefqmYO7h#LH5E@4fYFU$JRy3*4A=U?VXGUW*4Xisb7y)uM(J7rKt$?gfL`wrm5_z z+`qc(DP!2$y{)rA_xfF6BP<={&9?~AxjAlzbC7yD=Qo(FTg89c8{M)MvuX>L%`c9W589fsWFWj7I!`syCezp z7dWpU1rpH`l#8kvU8n`J#5g^Z9}M z`&ag_Y7Z|wxbTmGjp37p;giLl(4DKduI7GM=sECp+kqc>(!Hl7lD=K}{?*^F`Hufn zJYHrQJaXU-p>Ioq=w&+z9=6#P@uP=TXvg9GH^<;{)f?cYomt0LytLCbAXmQv+gv-c zwmRbf%;#!X3NQ(@!%~NF73}DHW$n3hRc=PcD%=Bh55P+hn3z60$Isw(*iayf%Bxp+ zsv_H;BP4Pb*S1*;Yrs{&At&QZoRfH)i*Su?{{5rP>wkUx@r_xFIZBVg+LID*@$f`S zq@hYD6Q;!`HQi$02cQ|aNKNCtgt;KO9d=E^6o**eva^&ljKmVb>9vAy&pkOG{8B0S#(;U>1p2a5 z{|X$w#Vs+#p1_?ex31*IH+qH&Jwy55es%UyUl{n<`%+0DodF=SO+>l})R~t26mkfm z-6-%DqyZGYDDV+OgD8-m_RZ=9=r6H*2*t}FEH0Lan)HO-JfBJ>DcXSIWko|MYLCuc z*hPU!-N=E!M`!3Kj^U+};NoA+!jluKoNQOCM}Ed$Np$gpSr(ze+yX*_2n}UB5E{z; z0imI%j2g}!#8Jcf0FD|iiB0@ClZ}-K);aTMyDoKm`L1lVM4-yUs!*+VKEcXS9#x(O zcA!}-!Mv1C=yAs=m~troL2@2S_5rsa+7e%Np7Y1(Sr~`BHu^!7ScYL9k-)#n@ERF@ WBJj-a9|?#jZ7(uiKM@f2-v0x#A=q{R literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc b/store/@{FutureOSS}/http-tcp/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a2d158607980cf68c52d0c13a51ab23fa2b91f5 GIT binary patch literal 9849 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 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() diff --git a/store/@{FutureOSS}/http-tcp/router.py b/store/@{FutureOSS}/http-tcp/router.py new file mode 100644 index 0000000..6f4b66c --- /dev/null +++ b/store/@{FutureOSS}/http-tcp/router.py @@ -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 diff --git a/store/@{FutureOSS}/http-tcp/server.py b/store/@{FutureOSS}/http-tcp/server.py new file mode 100644 index 0000000..1d16515 --- /dev/null +++ b/store/@{FutureOSS}/http-tcp/server.py @@ -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()) diff --git a/store/@{FutureOSS}/json-codec/README.md b/store/@{FutureOSS}/json-codec/README.md new file mode 100644 index 0000000..fd22b8d --- /dev/null +++ b/store/@{FutureOSS}/json-codec/README.md @@ -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}") +``` diff --git a/store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/json-codec/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44b40b21984b7242bf086bd852aacc746c8247c1 GIT binary patch literal 9507 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�<>qqfUxp7OBnH)%d$*gx^6^gLoI;4`ZFlE;_?gq=ljQs zlf`vIwrN}OXkF5_8uLCq@9dm%c_v++>5}s4l9d-uob@ zF|_Xnj91+GZ`H}-&ZMg|VeiBb_aq(F{KFmK-X-jHnMm*ZHDmjdu1yL1CTw^z>8MFf zlXQ6!b`R)sZamRecX;x?j`#N}bfpmlXdv09(ilFWJTK|^1+f9aZuqG_0OxtN1_C9YfGZ&75rwG+}j8#{qd-%pF!Flj! z6kS&@3Mce;k0m^8-FEN-;W~|7Y&g7* zZoAh8o}uK0>om9o$zwg842xGFa1{nvr*|1(3Sb7w>tdLEZy{XKi`$UQGzsLxnQYZY}|myw`OYc;;; zBV2D;SmH{{T}tS--@+;CT)gRfOL0Lb5#)+I~V4oPR7 zH_F#1%j<`vPvjLhR;+om0=(Efw&fk5gp@%wBWOgDk+1~`zzDxwLGosX{$b{qKY;tB z4wjCCG%UMExd2TWv8{wxLRw+=0ulRnI4rUSREccoK99j+K;yJ*Y7BSNU;T`8?Y?;>xMUl=pIeB11tz3# zBEYl^OJeY(Hd377E8bVcIA|4h>;= zSfUn_=@2n+qS6u#K-7<6Di(U{GK_Pl%P>zg+(!;iD_|}~1+RpYGv#zo8X0jZ>D-Wz zH*kUmq09fvE}_@QLm)2dJZ=M8MxcS)6BV8^HwZPOcAwVJ0~plrXbXyzM? ziwVNdD29oI7$id>>t?PxW!eNrNEU^f*r7Z}--l1f;FnUMH*BRzTX{k%=UcIiUkz=B z4w?o0Sn!%TKT)Sqv=#iOG(W1U_580>>e1a*DLN-q#_z1Vjt3R?7cTtoHE^-avNF$! zuMAg_y*#;%|GCLFIsvVyzSWljKqgrbu9N0J65D^0wUcD+e~^{8NXuP|Nw5xo2RJBL z@8y;Xo9++*_ga?=o;w7^RRzx;1klsu)M>9MG*L(Nc=BK`y_t= literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/json-codec/main.py b/store/@{FutureOSS}/json-codec/main.py new file mode 100644 index 0000000..9f9dd55 --- /dev/null +++ b/store/@{FutureOSS}/json-codec/main.py @@ -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() diff --git a/store/@{FutureOSS}/json-codec/manifest.json b/store/@{FutureOSS}/json-codec/manifest.json new file mode 100644 index 0000000..9af4d93 --- /dev/null +++ b/store/@{FutureOSS}/json-codec/manifest.json @@ -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": [] +} diff --git a/store/@{FutureOSS}/lifecycle/README.md b/store/@{FutureOSS}/lifecycle/README.md new file mode 100644 index 0000000..b68dfea --- /dev/null +++ b/store/@{FutureOSS}/lifecycle/README.md @@ -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() +``` diff --git a/store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/lifecycle/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c53f17943887792c9cf84cd636070914b4c4a950 GIT binary patch literal 7626 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 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() diff --git a/store/@{FutureOSS}/lifecycle/manifest.json b/store/@{FutureOSS}/lifecycle/manifest.json new file mode 100644 index 0000000..182dc7f --- /dev/null +++ b/store/@{FutureOSS}/lifecycle/manifest.json @@ -0,0 +1,15 @@ +{ + "metadata": { + "name": "lifecycle", + "version": "1.0.0", + "author": "FutureOSS", + "description": "生命周期管理 - 管理插件的状态转换和钩子", + "type": "core" + }, + "config": { + "enabled": true, + "args": {} + }, + "dependencies": [], + "permissions": [] +} diff --git a/store/@{FutureOSS}/pkg/README.md b/store/@{FutureOSS}/pkg/README.md new file mode 100644 index 0000000..4cc3963 --- /dev/null +++ b/store/@{FutureOSS}/pkg/README.md @@ -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 +``` diff --git a/store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/pkg/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f18146757694551e3b305522fcda05a19fd98da6 GIT binary patch literal 10514 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 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() diff --git a/store/@{FutureOSS}/pkg/manifest.json b/store/@{FutureOSS}/pkg/manifest.json new file mode 100644 index 0000000..1fdf373 --- /dev/null +++ b/store/@{FutureOSS}/pkg/manifest.json @@ -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": [] +} diff --git a/store/@{FutureOSS}/plugin-bridge/README.md b/store/@{FutureOSS}/plugin-bridge/README.md new file mode 100644 index 0000000..ef274aa --- /dev/null +++ b/store/@{FutureOSS}/plugin-bridge/README.md @@ -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() +``` diff --git a/store/@{FutureOSS}/plugin-bridge/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/plugin-bridge/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9d82af16c4a31c95d3357edb65d9ce02e7fefbb GIT binary patch literal 11598 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 M8)nRc;AJfQALjAW2mk;8 literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/plugin-bridge/main.py b/store/@{FutureOSS}/plugin-bridge/main.py new file mode 100644 index 0000000..a884001 --- /dev/null +++ b/store/@{FutureOSS}/plugin-bridge/main.py @@ -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() diff --git a/store/@{FutureOSS}/plugin-bridge/manifest.json b/store/@{FutureOSS}/plugin-bridge/manifest.json new file mode 100644 index 0000000..401e89a --- /dev/null +++ b/store/@{FutureOSS}/plugin-bridge/manifest.json @@ -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"] +} diff --git a/store/@{FutureOSS}/plugin-loader/README.md b/store/@{FutureOSS}/plugin-loader/README.md new file mode 100644 index 0000000..652aa52 --- /dev/null +++ b/store/@{FutureOSS}/plugin-loader/README.md @@ -0,0 +1,16 @@ +# plugin-loader 插件加载器 + +核心插件,负责扫描、加载和管理所有其他插件。 + +## 功能 + +- 自动扫描 `store/` 和 `./data/pkg/` 目录 +- 动态加载 `main.py` 并调用 `New()` 获取实例 +- 解析 `manifest.json` 获取插件元数据 +- 自动扫描插件能力(AST 分析) +- 按依赖关系排序加载顺序 +- 关联能力提供者与消费者 + +## 使用 + +无需手动使用,框架启动时自动加载。 diff --git a/store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/plugin-loader/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8268092ccece9444b93ce72dd746f0049592fdb2 GIT binary patch literal 29841 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 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() diff --git a/store/@{FutureOSS}/plugin-loader/manifest.json b/store/@{FutureOSS}/plugin-loader/manifest.json new file mode 100644 index 0000000..70fff14 --- /dev/null +++ b/store/@{FutureOSS}/plugin-loader/manifest.json @@ -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": ["*"] +} diff --git a/store/@{FutureOSS}/plugin-storage/README.md b/store/@{FutureOSS}/plugin-storage/README.md new file mode 100644 index 0000000..2543ab9 --- /dev/null +++ b/store/@{FutureOSS}/plugin-storage/README.md @@ -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"} +``` diff --git a/store/@{FutureOSS}/plugin-storage/__pycache__/main.cpython-313.pyc b/store/@{FutureOSS}/plugin-storage/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09992f1492c6fb880bc23172fd317c063905a57c GIT binary patch literal 19846 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 literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/plugin-storage/main.py b/store/@{FutureOSS}/plugin-storage/main.py new file mode 100644 index 0000000..6953bfd --- /dev/null +++ b/store/@{FutureOSS}/plugin-storage/main.py @@ -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// 目录""" + + 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() diff --git a/store/@{FutureOSS}/plugin-storage/manifest.json b/store/@{FutureOSS}/plugin-storage/manifest.json new file mode 100644 index 0000000..e43fdd8 --- /dev/null +++ b/store/@{FutureOSS}/plugin-storage/manifest.json @@ -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": ["*"] +} diff --git a/store/@{FutureOSS}/ws-api/README.md b/store/@{FutureOSS}/ws-api/README.md new file mode 100644 index 0000000..a136378 --- /dev/null +++ b/store/@{FutureOSS}/ws-api/README.md @@ -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 + } + } +} +``` diff --git a/store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc b/store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..581a6bfa919a26e0031b18d896c9ade9bbf3c1c8 GIT binary patch literal 1112 zcmZ8f&u<$=6rTOz_1f#TowSLn5GpHap+3+Ypobt-kvMLZRM?Uyr5B6QdOZm%oL%!~ zmJ%Z*qaH}bC6`|K54a#sNWec)sD#|PRETisEs~MA@@DN&iIMj0_r34UkN3??eZNR> z{QdTC(TYjPpPF2Z+#F1M2%Zp1fKY>)_l!Lg%smUNn&#F$doKsMJqMf`>6%x_8lmI^>Vt5r4M+ICgCSs*3N$Z=Ioar z|N8#hv!5Qn)Qi+jRpDt8d^K_7G0R(!17_E1Ji91OzwcG=IKlWC0L3F#6r zunm(MU|lEF1iPE-I@Ed-Rcb@N>mti#%z;9t<>#!JF;}y$w??`pTG%we@0Kv{zGG^y zaBeZH_OKnhUQ+o7qWKw*J{ZO76(V1F;!&jDCwsIjZ2Jung)ZpQeo^M0wD)c z%;N;*JY)SNL|_RC3gsY<6B%g#1Y_XgK|wbJz7Ay_w2{j0N1+7WhgxDx=oujkq=cxK zO0K6apf<&isC9vdS{mOyq0j84=U3N%*!lJ9Cer=W^;2aLva(0ig_qUKH`2PE~u8~{*GM493X*F36V&9BE)X^sEjO)UvaJWAPM?m zAY`jv0sT~!_up&e0xZ3E=97b$8pVe*4|Qu` zL6g$0yG`ZI3YwixyQ3;ILhoj{0dChjGy|vTWqWjr(%sg4u$93PE zy^!1R22uk*(ZXCNKudki$+WCbONfli8m3 zRX1S~VLT|KS;-=-cuZ|u&b+1?~FqD5behU(VvA<}tDOTU;&7rZ%Bm*Y7G>-FQ1{lXoFkMz+Q%stZOp2;8 zYv%qSswSeMDrPYviOZxl$-rvZ-biu)fm8k&*{YxSWB2HZcii<$U*5g`OZSs2-AmuQ zhuw{9?e6kd8=rOV(O%C;?Gw$U5rpXk_>DlvKBy=IRJ;`Vb#mI2J_FV}g2*bXGT_MC zkQ_{DfFPz|$%3@1_N#`pC)qi1j>Cu?LEMoJ>QOlgg@*9P(y>$eXcXi}-=kC{6R4iX zyv{ez$C0i~Gz$Q~$5ShUAdEc&wxJ{JM5XCi*M%YusUZ64_%L}w#>kIU zo85JaH?|+V>3A@X^Cz-Sd^64m(b$^xd0NRt8bdcQJ(1W5cUOMue*VLI2^H)y|6hO3 z@yeP3^hDqpQW=}9R5@u&$2Ex4ox`j%Xa{<0ir8jGB(KXuc2;eTfT@i*%~ zUY`3e`wXwZvrnTfu%DtQb4>^}_<%Gv$yU-p1z+P8d>z&Vg!{DSDLJMfEAWCH6lM+}eNNHmpNmPds8cJa9~F`cVN@zM1yb@f*nC zsqR5&xY1|(F4GM5q3QAb%oovq%p)}`{snjmG4j#WBW-qjY|q6vZirImR`G0@LO307 zX+4wwvu5a@rGYvU1gY#c6a}_~caG#~eG@!frJmE&+<%*#^IPoYJM8s`_AWlXa^NAu Y;r=1UcdwlO?)dNQm7dM{a~Xqw0ZyJ@fB*mh literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc b/store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a94e6e22c5950be539b84b9360a43aab4dfd722 GIT binary patch literal 2611 zcmb7FU2GIp6ux)49I7) zF+t%Iq9PFgYRN4SQq-OnO^jRf^2~XpU2})7q9!r40Rm#`HWY<(dI1Sa|F7 z&$oZL{A$u$ zRCpD_p{qsL}0jLaN47o8vzf+JYEF~BgA ztQrBeEIcv;Yr<1B?+bq2|XX_T|Pf5zU%k5=5U3rD*e4E#VAN|6mYpxinV~9iom2nF)TTONY383AeGQhIkfW5_h;^$ zJ5o;c|Ct`7a(YbAsOth&2Pc%n@A<-YTf<^=N`Vc4ItwCT9TsEfV~zOHgn-G&gC1NIjead;(?am2Q<= zx?Hhz{ZP8qZs{&vn7Lc_X=yX>nv2YoECmlwL;)UNK+DHSH;EGiX5JVitQ}?()Ty4Y z5Gza>roV;Tgj}~`ey7c}4N2Sb*uh<;HT0^5-WVp+-Or5(*W@PG8;*Z|^JI}aLCdqt zwQVDO06CByu6Qx*D7kKdI8J+ucMOq1qzeqt%uknYeLP;ywP=`Qogj83ZD>e0Q}Btm zV(S4cv?5fz8?U8OPVn(-ffy6Q!fOqa8*?-x7NdJ;zFGChzbYMmNP<3Sj3Ec<Z z^GW%t&_5U&kc6i^(2A4r6wUx_7QNGB+?(a(h>1HE2BY|6$*~*Z9Icy>%33iFw>L=) zn$CJ~K!$_hq3DqjqcJ`vlsY3Y{v#zF<6YL5Jl2oq(KzUWyrG+J@Z1^%g5fd;T-tZ| zbn4(d_j`GY^cfn);c);~rL*SofU=y~waLgPHN)E6Rcu^Ofv%+h{DW+NqjuRB%BlLP z=I~^5_-e58YHjCquz4!jF&XR_Pfi9qN4e>`<)iyg$fNvpYuoAFU+q3Co%daAyxJO> zYK&Z|i%c(X8HwKINz+U8p@pG38WAFO^sWk42Xl|S!lIXs^UZ7nXnP$E9l2-E zp1wY>_;@h9Qi6gQ*~w}L_}4W*P&>lU2~yRHy9A+*G_=llFAM$hQDX=Fnzjq4#SY5C zAc%%*p3H#QOT6~-EY8p_wi4k*gi6%71{V;JjVzoXHHPj9RD6C`;>2~QjQh}b*3HT_ zV%ynQ&MOn!<_Ju^QbPS`Sa#{xk?cc5X>)KvPn2NUGbe;I)P97T9eI=2eK)gBu!`wq qbpg!sl+x>D<)5T;l61~V0&TlX0PY6>MicjOJ{M@9ZTAR(Ys`Pd-ZN$Z literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc b/store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30732b39d14be7b1adb2a450c3134788ba9294ca GIT binary patch literal 2281 zcmb7F-ER{|5Z^oBmp>94n}m>r#v~Ma4kUay1w>l4fkworM)JubQX^fBeK7})o$j7N zp{k%CLO}unQX^9Jg-Vs0Jkg4(eWm;bm`F`HRP~_+nm32Eiu%@>JKI3SOGmoj?9T4a z&g^{LYEzSs0Lk$OX1_qlA2_KK%pF?&5ZDaS2qhY)@y9sIIh?0FaL+MMLZCuIq+&v% zQj}Jy^2a~GDRt{$hh0MuA!7urCulkKXgn1(&zPtQV^SQfZYQS&Dr*u|^0KPf@(GqIl`Uggfs^GBz8sL}t<$HQf-OpW83_DTOawktDf_ER*J_DxRNimsbQ)6#W2RI|UY zF2PVUyyzlX+Sqo*yR_-GtASOR6EFbpcJ557UQ|-dOcJtWInY>{~8eTjXVt^UKMe=87&#fa1oqMN+YyQjr>qj0an;oBQDeKg%E!Pxf z(VaSpF!&t4)f_n0L!wwn(Nul)1%3#|8Sn-ieL-3urg>D9!be(1qmlX(jX-ld1v}1i zGoGXvfV=YCtF@(2**Y4i%_HRKo6s!au_xb4^C@}VHkhV`fm(pxs4eDT_xoF5Mf|@O zqK$sAG|iw?wuQ62oEj>2JZ(3mjYqf+r9m}j&C{^x9+WM-TUj-cqOo9I1Wd@&TiABsN=wXVpdd!QO1!Io=hE}yx-?cm*b zrTyqa^T?tyQc*@$T1eAdD*>`$U_~L`1otmH2=;xpXJ|KfhZ~9sKOTV5FS|Da-V?Z? zozgwA3&-2Ip+4c>>k^K4a)^D1v>Q$*)c|b;PB$T7cckb!ZAZWsLOT$!aOidf?4ooJ z0{(m`w9yKipvZ)8=aSxj)Z4pa&9@AtX< zZe-1wyNwC8seZ>GTR6>1MO)6WsbbbF*^)IiX%_RYbyztRqiajdpl2b098;Yh7^bkZgWL{EtZU?<7_svENANnj~;tD+FMzxs6M3&j_Mp F;eQM3-l6~i literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc b/store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b55f7f1cba0008864d220346d1ff4a3e401edec6 GIT binary patch literal 6705 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 Y;g+R@Yq(AzyVdvv$1S`;FlWpE4|HZQCIA2c literal 0 HcmV?d00001 diff --git a/store/@{FutureOSS}/ws-api/events.py b/store/@{FutureOSS}/ws-api/events.py new file mode 100644 index 0000000..73100ea --- /dev/null +++ b/store/@{FutureOSS}/ws-api/events.py @@ -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" diff --git a/store/@{FutureOSS}/ws-api/main.py b/store/@{FutureOSS}/ws-api/main.py new file mode 100644 index 0000000..28a03dc --- /dev/null +++ b/store/@{FutureOSS}/ws-api/main.py @@ -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() diff --git a/store/@{FutureOSS}/ws-api/manifest.json b/store/@{FutureOSS}/ws-api/manifest.json new file mode 100644 index 0000000..11f5f8b --- /dev/null +++ b/store/@{FutureOSS}/ws-api/manifest.json @@ -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": [] +} diff --git a/store/@{FutureOSS}/ws-api/middleware.py b/store/@{FutureOSS}/ws-api/middleware.py new file mode 100644 index 0000000..9d3e7de --- /dev/null +++ b/store/@{FutureOSS}/ws-api/middleware.py @@ -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() diff --git a/store/@{FutureOSS}/ws-api/router.py b/store/@{FutureOSS}/ws-api/router.py new file mode 100644 index 0000000..f9b5ed7 --- /dev/null +++ b/store/@{FutureOSS}/ws-api/router.py @@ -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}) diff --git a/store/@{FutureOSS}/ws-api/server.py b/store/@{FutureOSS}/ws-api/server.py new file mode 100644 index 0000000..2a997fc --- /dev/null +++ b/store/@{FutureOSS}/ws-api/server.py @@ -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()) diff --git a/website/architecture.html b/website/architecture.html new file mode 100644 index 0000000..0897094 --- /dev/null +++ b/website/architecture.html @@ -0,0 +1,94 @@ + + + + + + 架构设计 - Future OSS + + + + + + + + + + + + + + + + + +
+ +
+
+ 架构设计 +

插件驱动的分层架构

+

一切皆为插件,框架本身只提供核心能力

+
+ +
+
+
Future OSS 核心
+
+
插件管理
+
事件总线
+
消息总线
+
配置系统
+
+
+
+
+
+
+
协议插件
+
HTTP / WS
+
TCP / gRPC
+
UDP / Unix
+
+
+
工具插件
+
外部进程
+
脚本执行
+
依赖管理
+
+
+
中间件插件
+
限流 / 认证
+
日志 / 压缩
+
缓存 / 重试
+
+
+
通知插件
+
Webhook
+
邮件 / 钉钉
+
Telegram
+
+
+
+
+ +
+

数据流

+
+
1外部请求进入协议插件
+
→
+
2中间件链处理(认证/限流)
+
→
+
3业务插件执行逻辑
+
→
+
4发布事件 + 返回响应
+
+
+
+ + + + + + + + diff --git a/website/community/UPDATE_PROFILE.md b/website/community/UPDATE_PROFILE.md new file mode 100644 index 0000000..6172ace --- /dev/null +++ b/website/community/UPDATE_PROFILE.md @@ -0,0 +1,134 @@ +# 用户个人主页功能更新 + +## 改动概述 + +为用户社区添加了完整的个人主页系统,包括用户信息展示、文章管理、个人简介编辑等功能。 + +## 新增文件 + +### 1. `profile.php` - 用户个人主页 +**功能:** +- 显示用户头像、用户名、角色徽章 +- 显示用户个人简介(bio) +- 显示注册时间和邮箱(仅本人可见) +- 统计数据卡片:文章数、回复数、总浏览、总点赞 +- 最近发表的文章列表(最新 10 篇) +- 最近的回复列表(最新 5 条) +- 支持查看其他用户的主页(通过 `?id=用户ID` 参数) +- 本人访问时显示"编辑资料"按钮 + +**访问方式:** +- `profile.php` - 查看当前登录用户的个人主页 +- `profile.php?id=123` - 查看指定用户的个人主页 + +### 2. `edit-profile.php` - 编辑个人资料 +**功能:** +- 编辑个人简介(bio) +- 实时预览功能 +- 显示用户名和邮箱(只读) +- 保存成功后显示提示信息 + +**访问方式:** +- 从个人主页点击"编辑资料"按钮进入 + +### 3. `my-posts.php` - 我的文章(已更新) +**更新内容:** +- 支持查看其他用户的文章(通过 `?id=用户ID` 参数) +- 只有文章作者才能看到"编辑"和"删除"按钮 +- 页面标题动态显示用户名 + +### 4. `migrate-add-bio.php` - 数据库迁移脚本 +**用途:** +- 为现有的 `users` 表添加 `bio` 字段 +- 检查字段是否已存在,避免重复添加 + +**运行方式:** +```bash +php website/community/migrate-add-bio.php +``` + +## 修改的文件 + +### 1. `includes/dock.php` +**改动:** +- 用户头像链接从 `#` 改为 `profile.php`,点击跳转到个人主页 +- 用户面板新增"个人主页"菜单项 +- 保留"我的文章"菜单项,并显示文章数量徽章 + +### 2. `api/auth.php` +**改动:** +- 新增 `my-post-count` API 端点 +- 用于获取当前登录用户的文章数量 +- 在用户面板中实时显示文章数徽章 + +### 3. `css/dock.css` +**改动:** +- 新增 `.dock-user-avatar` 样式 +- 用户头像图标显示为青色高亮 +- 鼠标悬停时有缩放和背景效果 + +### 4. `assets/css/dock-popover.css` +**改动:** +- 新增 `.popover-menu` 菜单区域样式 +- 新增 `.menu-item` 菜单项样式(带图标、文字、徽章) +- 新增 `.menu-badge` 徽章样式 + +### 5. `assets/js/dock-popover.js` +**改动:** +- 新增 `fetchMyPostCount()` 函数 +- 页面加载时自动获取并显示用户文章数量 +- 保留原有的退登和注销功能 + +### 6. `schema.sql` +**改动:** +- `users` 表新增 `bio TEXT` 字段 +- 用于存储用户的个人简介 + +## 使用方法 + +### 1. 运行数据库迁移 +首次部署需要运行迁移脚本添加 bio 字段: +```bash +cd website/community +php migrate-add-bio.php +``` + +### 2. 访问个人主页 +- 点击左侧 Dock 栏的用户头像图标 +- 或访问 `community/profile.php` + +### 3. 编辑个人简介 +- 在个人主页点击"编辑资料"按钮 +- 或访问 `community/edit-profile.php` + +### 4. 查看我的文章 +- 在用户面板中点击"我的文章" +- 或访问 `community/my-posts.php` + +### 5. 查看其他用户的主页 +- 访问 `community/profile.php?id=用户ID` + +## 技术细节 + +### 数据库查询优化 +- 使用子查询统计回复数,避免 JOIN 导致的性能问题 +- 使用 SUM 聚合函数统计总浏览和总点赞 +- 所有查询都使用参数化查询防止 SQL 注入 + +### 安全性 +- 所有用户输入都经过 `htmlspecialchars()` 转义 +- 只有本人才能编辑和删除自己的文章 +- Session 验证确保用户只能访问自己的数据 + +### 响应式设计 +- 适配桌面端和移动端 +- 小屏幕时自动调整布局和字体大小 +- Dock 栏在移动端自动移到底部 + +## 后续可扩展功能 + +1. **头像上传** - 支持用户上传自定义头像 +2. **关注系统** - 用户可以互相关注 +3. **成就徽章** - 根据用户活跃度颁发徽章 +4. **社交链接** - 添加 GitHub、Twitter 等社交链接 +5. **活动日志** - 记录用户的登录、发帖等活动 diff --git a/website/community/add-title-system.sql b/website/community/add-title-system.sql new file mode 100644 index 0000000..6bba69e --- /dev/null +++ b/website/community/add-title-system.sql @@ -0,0 +1,25 @@ +-- 添加称号系统到 users 表 +ALTER TABLE users ADD COLUMN IF NOT EXISTS title VARCHAR(100) DEFAULT '' COMMENT '用户称号'; + +-- 设置 admin 用户的称号 +UPDATE users SET title = '你猜为什么是DeepSeek' WHERE role = 'admin' AND username = 'admin'; + +-- 创建称号配置表(可选,用于管理称号) +CREATE TABLE IF NOT EXISTS titles ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE COMMENT '称号名称', + color VARCHAR(7) DEFAULT '#06b6d4' COMMENT '称号颜色', + description VARCHAR(255) DEFAULT '' COMMENT '称号描述', + is_admin_only TINYINT(1) DEFAULT 0 COMMENT '是否仅管理员可用', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 插入预设称号 +INSERT IGNORE INTO titles (name, color, description, is_admin_only) VALUES +('你猜为什么是DeepSeek', '#f59e0b', '神秘的管理称号', 1), +('管理员', '#ef4444', '网站管理员', 1), +('版主', '#22c55e', '社区版主', 1), +('活跃用户', '#3b82f6', '经常发帖的活跃用户', 0), +('新手', '#6b7280', '新加入的用户', 0), +('技术达人', '#8b5cf6', '技术方面的大神', 0), +('社区元老', '#f97316', '在很久的时间前就加入社区的用户', 0); diff --git a/website/community/api/auth.php b/website/community/api/auth.php new file mode 100644 index 0000000..63282fe --- /dev/null +++ b/website/community/api/auth.php @@ -0,0 +1,286 @@ + false, 'message' => '请求方法不允许']); + exit; +} + +if ($action === 'login') { + handleLogin(); +} elseif ($action === 'register') { + handleRegister(); +} elseif ($action === 'logout') { + handleLogout(); +} elseif ($action === 'check') { + handleCheck(); +} elseif ($action === 'my-post-count') { + handleMyPostCount(); +} elseif ($action === 'current-user') { + handleCurrentUser(); +} else { + http_response_code(400); + echo json_encode(['success' => false, 'message' => '无效的操作类型']); +} + +/** + * 处理登录 + */ +function handleLogin() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['username']) || empty($input['password'])) { + echo json_encode(['success' => false, 'message' => '用户名和密码不能为空']); + return; + } + + $username = trim($input['username']); + $password = $input['password']; + $remember = $input['remember'] ?? false; + + try { + $db = Database::getInstance(); + + // 查询用户(支持用户名或邮箱登录) + $user = $db->fetchOne( + "SELECT id, username, email, password_hash, role, avatar FROM users WHERE username = ? OR email = ?", + [$username, $username] + ); + + if (!$user) { + echo json_encode(['success' => false, 'message' => '用户名或密码错误']); + return; + } + + // 验证密码 + if (!password_verify($password, $user['password_hash'])) { + echo json_encode(['success' => false, 'message' => '用户名或密码错误']); + return; + } + + // 设置 session + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + $_SESSION['avatar'] = $user['avatar']; + + // 如果勾选记住我,设置更长的 session 生命周期 + if ($remember) { + ini_set('session.gc_maxlifetime', 30 * 24 * 60 * 60); // 30天 + session_set_cookie_params(30 * 24 * 60 * 60); + } + + echo json_encode([ + 'success' => true, + 'message' => '登录成功', + 'user' => [ + 'id' => $user['id'], + 'username' => $user['username'], + 'role' => $user['role'], + 'avatar' => $user['avatar'] + ] + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 处理注册 + */ +function handleRegister() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['username']) || empty($input['email']) || empty($input['password'])) { + echo json_encode(['success' => false, 'message' => '所有字段都不能为空']); + return; + } + + $username = trim($input['username']); + $email = trim($input['email']); + $password = $input['password']; + + // 验证用户名格式 + if (!preg_match('/^[a-zA-Z0-9_]{3,50}$/', $username)) { + echo json_encode(['success' => false, 'message' => '用户名只能包含字母、数字和下划线,长度 3-50 个字符']); + return; + } + + // 验证邮箱格式 + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + echo json_encode(['success' => false, 'message' => '邮箱格式不正确']); + return; + } + + // 验证密码长度 + if (strlen($password) < 6) { + echo json_encode(['success' => false, 'message' => '密码长度至少 6 个字符']); + return; + } + + try { + $db = Database::getInstance(); + + // 检查用户名是否已存在 + $existingUser = $db->fetchOne("SELECT id FROM users WHERE username = ?", [$username]); + if ($existingUser) { + echo json_encode(['success' => false, 'message' => '用户名已被使用']); + return; + } + + // 检查邮箱是否已存在 + $existingEmail = $db->fetchOne("SELECT id FROM users WHERE email = ?", [$email]); + if ($existingEmail) { + echo json_encode(['success' => false, 'message' => '邮箱已被注册']); + return; + } + + // 密码哈希 + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + + // 插入新用户 + $db->query( + "INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'member')", + [$username, $email, $passwordHash] + ); + + $userId = $db->lastInsertId(); + + echo json_encode([ + 'success' => true, + 'message' => '注册成功', + 'user' => [ + 'id' => $userId, + 'username' => $username, + 'role' => 'member' + ] + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 处理登出 + */ +function handleLogout() { + session_destroy(); + echo json_encode(['success' => true, 'message' => '已成功退出']); +} + +/** + * 检查登录状态 + */ +function handleCheck() { + if (isset($_SESSION['user_id'])) { + echo json_encode([ + 'success' => true, + 'logged_in' => true, + 'user' => [ + 'id' => $_SESSION['user_id'], + 'username' => $_SESSION['username'], + 'role' => $_SESSION['role'] ?? 'member', + 'avatar' => $_SESSION['avatar'] ?? '' + ] + ]); + } else { + echo json_encode([ + 'success' => true, + 'logged_in' => false + ]); + } +} + +/** + * 获取用户文章数量 + */ +function handleMyPostCount() { + if (!isset($_SESSION['user_id'])) { + echo json_encode(['success' => false, 'message' => '未登录']); + return; + } + + try { + $db = Database::getInstance(); + $count = $db->fetchOne( + "SELECT COUNT(*) as count FROM posts WHERE user_id = ?", + [$_SESSION['user_id']] + )['count']; + + echo json_encode([ + 'success' => true, + 'count' => (int)$count + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 获取当前登录用户信息(用于轮询) + */ +function handleCurrentUser() { + if (!isset($_SESSION['user_id'])) { + echo json_encode(['success' => false, 'message' => '未登录']); + return; + } + + try { + $db = Database::getInstance(); + $user = $db->fetchOne( + "SELECT id, username, email, avatar, role, title, bio, created_at FROM users WHERE id = ?", + [$_SESSION['user_id']] + ); + + if (!$user) { + echo json_encode(['success' => false, 'message' => '用户不存在']); + return; + } + + // 获取统计数据 + $stats = $db->fetchOne( + "SELECT + (SELECT COUNT(*) FROM posts WHERE user_id = ?) as post_count, + (SELECT COUNT(*) FROM replies WHERE user_id = ?) as reply_count", + [$user['id'], $user['id']] + ); + + echo json_encode([ + 'success' => true, + 'user' => $user, + 'stats' => [ + 'post_count' => (int)$stats['post_count'], + 'reply_count' => (int)$stats['reply_count'] + ], + 'permissions' => [ + 'can_manage_users' => in_array($user['role'], ['admin']), + 'can_manage_posts' => in_array($user['role'], ['admin', 'moderator']), + 'can_pin_posts' => in_array($user['role'], ['admin', 'moderator']), + 'can_lock_posts' => in_array($user['role'], ['admin', 'moderator']), + 'can_delete_any_post' => in_array($user['role'], ['admin']), + 'can_manage_titles' => in_array($user['role'], ['admin']) + ] + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} diff --git a/website/community/api/index.php b/website/community/api/index.php new file mode 100644 index 0000000..d939925 --- /dev/null +++ b/website/community/api/index.php @@ -0,0 +1,112 @@ +fetchAll( + "SELECT p.*, u.username, u.avatar, c.name as category_name + FROM posts p + JOIN users u ON p.user_id = u.id + JOIN categories c ON p.category_id = c.id + WHERE p.category_id = ? + ORDER BY p.is_pinned DESC, p.created_at DESC + LIMIT ? OFFSET ?", + [$categoryId, $limit, $offset] + ); + $total = $db->fetchOne("SELECT COUNT(*) as count FROM posts WHERE category_id = ?", [$categoryId])['count']; + } else { + $posts = $db->fetchAll( + "SELECT p.*, u.username, u.avatar, c.name as category_name + FROM posts p + JOIN users u ON p.user_id = u.id + JOIN categories c ON p.category_id = c.id + ORDER BY p.is_pinned DESC, p.created_at DESC + LIMIT ? OFFSET ?", + [$limit, $offset] + ); + $total = $db->fetchOne("SELECT COUNT(*) as count FROM posts")['count']; + } + + echo json_encode([ + 'posts' => $posts, + 'total' => $total, + 'pages' => ceil($total / $limit) + ]); + break; + + case 'post': + $id = (int)($_GET['id'] ?? 0); + $post = $db->fetchOne( + "SELECT p.*, u.username, u.avatar, u.role, c.name as category_name, c.slug as category_slug + FROM posts p + JOIN users u ON p.user_id = u.id + JOIN categories c ON p.category_id = c.id + WHERE p.id = ?", + [$id] + ); + + if (!$post) { + http_response_code(404); + echo json_encode(['error' => '帖子不存在']); + exit; + } + + // 更新浏览数 + $db->query("UPDATE posts SET views = views + 1 WHERE id = ?", [$id]); + $post['views']++; + + // 获取回复 + $replies = $db->fetchAll( + "SELECT r.*, u.username, u.avatar + FROM replies r + JOIN users u ON r.user_id = u.id + WHERE r.post_id = ? + ORDER BY r.is_solution DESC, r.created_at ASC", + [$id] + ); + + echo json_encode(['post' => $post, 'replies' => $replies]); + break; + + case 'categories': + $categories = $db->fetchAll("SELECT * FROM categories ORDER BY sort_order ASC"); + echo json_encode(['categories' => $categories]); + break; + + case 'stats': + $stats = [ + 'posts' => $db->fetchOne("SELECT COUNT(*) as count FROM posts")['count'], + 'replies' => $db->fetchOne("SELECT COUNT(*) as count FROM replies")['count'], + 'users' => $db->fetchOne("SELECT COUNT(*) as count FROM users")['count'], + 'hot_posts' => $db->fetchAll("SELECT id, title, views, likes FROM posts ORDER BY views DESC LIMIT 5"), + ]; + echo json_encode($stats); + break; + + default: + echo json_encode(['error' => '未知操作']); + } +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} diff --git a/website/community/api/posts.php b/website/community/api/posts.php new file mode 100644 index 0000000..bd4763b --- /dev/null +++ b/website/community/api/posts.php @@ -0,0 +1,340 @@ + false, 'message' => '请求方法不允许']); + exit; +} + +$action = $_GET['action'] ?? ''; + +// 检查登录状态(除了查看操作) +$requireAuth = in_array($action, ['create', 'update', 'delete', 'pin', 'lock']); +if ($requireAuth && !isset($_SESSION['user_id'])) { + echo json_encode(['success' => false, 'message' => '请先登录']); + exit; +} + +switch ($action) { + case 'create': + handleCreatePost(); + break; + case 'update': + handleUpdatePost(); + break; + case 'delete': + handleDeletePost(); + break; + case 'pin': + handlePinPost(); + break; + case 'lock': + handleLockPost(); + break; + default: + http_response_code(400); + echo json_encode(['success' => false, 'message' => '无效的操作类型']); +} + +/** + * 创建帖子 + */ +function handleCreatePost() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['title']) || empty($input['content']) || empty($input['category_id'])) { + echo json_encode(['success' => false, 'message' => '标题、内容和分类不能为空']); + return; + } + + $title = trim($input['title']); + $content = trim($input['content']); + $categoryId = (int)$input['category_id']; + $tags = $input['tags'] ?? []; + + // 验证标题长度 + if (mb_strlen($title) < 5 || mb_strlen($title) > 200) { + echo json_encode(['success' => false, 'message' => '标题长度必须在 5-200 个字符之间']); + return; + } + + // 验证内容长度 + if (mb_strlen($content) < 10) { + echo json_encode(['success' => false, 'message' => '内容至少 10 个字符']); + return; + } + + try { + $db = Database::getInstance(); + $userId = $_SESSION['user_id']; + $userRole = $_SESSION['role'] ?? 'member'; + + // 验证分类是否存在 + $category = $db->fetchOne("SELECT * FROM categories WHERE id = ?", [$categoryId]); + if (!$category) { + echo json_encode(['success' => false, 'message' => '分类不存在']); + return; + } + + // 公告分类:禁止通过 API 发帖(只能通过 SQL 直接插入) + if ($category['slug'] === 'announcements') { + echo json_encode(['success' => false, 'message' => '公告不能通过发帖功能创建,请联系管理员通过数据库添加']); + return; + } + + // 生成 slug + $slug = generateSlug($title); + + // 检查 slug 是否重复 + $existing = $db->fetchOne("SELECT id FROM posts WHERE slug = ?", [$slug]); + if ($existing) { + $slug .= '-' . time(); + } + + // 插入帖子 + $db->query( + "INSERT INTO posts (user_id, category_id, title, slug, content) VALUES (?, ?, ?, ?, ?)", + [$userId, $categoryId, $title, $slug, $content] + ); + + $postId = $db->lastInsertId(); + + // 保存标签 + if (!empty($tags)) { + saveTags($db, $postId, $tags); + } + + echo json_encode([ + 'success' => true, + 'message' => '发帖成功', + 'post_id' => $postId + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 更新帖子 + */ +function handleUpdatePost() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['id']) || empty($input['title']) || empty($input['content'])) { + echo json_encode(['success' => false, 'message' => '标题和内容不能为空']); + return; + } + + $postId = (int)$input['id']; + $title = trim($input['title']); + $content = trim($input['content']); + $tags = $input['tags'] ?? []; + + try { + $db = Database::getInstance(); + $userId = $_SESSION['user_id']; + + // 检查帖子是否存在且属于当前用户 + $post = $db->fetchOne("SELECT user_id FROM posts WHERE id = ?", [$postId]); + if (!$post) { + echo json_encode(['success' => false, 'message' => '帖子不存在']); + return; + } + + // 只有作者可以编辑 + if ($post['user_id'] != $userId) { + echo json_encode(['success' => false, 'message' => '无权编辑此帖子']); + return; + } + + // 更新帖子 + $db->query( + "UPDATE posts SET title = ?, content = ?, updated_at = NOW() WHERE id = ?", + [$title, $content, $postId] + ); + + // 更新标签 + if (!empty($tags)) { + saveTags($db, $postId, $tags); + } + + echo json_encode([ + 'success' => true, + 'message' => '更新成功' + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 删除帖子 + */ +function handleDeletePost() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['id'])) { + echo json_encode(['success' => false, 'message' => '帖子 ID 不能为空']); + return; + } + + $postId = (int)$input['id']; + + try { + $db = Database::getInstance(); + $userId = $_SESSION['user_id']; + $userRole = $_SESSION['role'] ?? 'member'; + + // 检查帖子是否存在 + $post = $db->fetchOne("SELECT user_id FROM posts WHERE id = ?", [$postId]); + if (!$post) { + echo json_encode(['success' => false, 'message' => '帖子不存在']); + return; + } + + // 只有作者或管理员可以删除 + if ($post['user_id'] != $userId && !in_array($userRole, ['admin', 'moderator'])) { + echo json_encode(['success' => false, 'message' => '无权删除此帖子']); + return; + } + + // 删除帖子(外键级联删除回复和点赞) + $db->query("DELETE FROM posts WHERE id = ?", [$postId]); + + echo json_encode([ + 'success' => true, + 'message' => '删除成功' + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 置顶/取消置顶 + */ +function handlePinPost() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['id'])) { + echo json_encode(['success' => false, 'message' => '帖子 ID 不能为空']); + return; + } + + $postId = (int)$input['id']; + $pinned = (bool)($input['pinned'] ?? false); + + try { + $db = Database::getInstance(); + $userRole = $_SESSION['role'] ?? 'member'; + + // 只有管理员或版主可以置顶 + if (!in_array($userRole, ['admin', 'moderator'])) { + echo json_encode(['success' => false, 'message' => '无权置顶帖子']); + return; + } + + $db->query("UPDATE posts SET is_pinned = ? WHERE id = ?", [(int)$pinned, $postId]); + + echo json_encode([ + 'success' => true, + 'message' => $pinned ? '已置顶' : '已取消置顶' + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 锁定/解锁帖子 + */ +function handleLockPost() { + $input = json_decode(file_get_contents('php://input'), true); + + if (empty($input['id'])) { + echo json_encode(['success' => false, 'message' => '帖子 ID 不能为空']); + return; + } + + $postId = (int)$input['id']; + $locked = (bool)($input['locked'] ?? false); + + try { + $db = Database::getInstance(); + $userRole = $_SESSION['role'] ?? 'member'; + + // 只有管理员或版主可以锁定 + if (!in_array($userRole, ['admin', 'moderator'])) { + echo json_encode(['success' => false, 'message' => '无权锁定帖子']); + return; + } + + $db->query("UPDATE posts SET is_locked = ? WHERE id = ?", [(int)$locked, $postId]); + + echo json_encode([ + 'success' => true, + 'message' => $locked ? '已锁定' : '已解锁' + ]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['success' => false, 'message' => '服务器错误:' . $e->getMessage()]); + } +} + +/** + * 保存标签 + */ +function saveTags($db, $postId, $tags) { + // 先删除旧标签关联 + $db->query("DELETE FROM post_tags WHERE post_id = ?", [$postId]); + + foreach ($tags as $tagName) { + $tagName = trim($tagName); + if (empty($tagName)) continue; + + // 查找或创建标签 + $slug = strtolower(preg_replace('/[^a-zA-Z0-9\x{4e00}-\x{9fa5}]/u', '-', $tagName)); + $tag = $db->fetchOne("SELECT id FROM tags WHERE name = ?", [$tagName]); + + if (!$tag) { + $db->query( + "INSERT INTO tags (name, slug) VALUES (?, ?)", + [$tagName, $slug] + ); + $tagId = $db->lastInsertId(); + } else { + $tagId = $tag['id']; + } + + // 关联标签 + $db->query( + "INSERT IGNORE INTO post_tags (post_id, tag_id) VALUES (?, ?)", + [$postId, $tagId] + ); + } +} + +/** + * 生成 slug + */ +function generateSlug($title) { + // 简单处理:移除特殊字符,替换空格为连字符 + $slug = preg_replace('/[^\p{L}\p{N}\s]/u', '', $title); + $slug = preg_replace('/\s+/', '-', $slug); + $slug = mb_strtolower($slug, 'UTF-8'); + return mb_substr($slug, 0, 100); +} diff --git a/website/community/assets/css/auth.css b/website/community/assets/css/auth.css new file mode 100644 index 0000000..a559b24 --- /dev/null +++ b/website/community/assets/css/auth.css @@ -0,0 +1,363 @@ +/* ============================================ + OSS Community 认证页面样式 + 与官网风格保持一致 + ============================================ */ + +/* 认证页面布局 */ +.auth-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + margin-left: 80px; + position: relative; + z-index: 1; +} + +.auth-container { + width: 100%; + max-width: 480px; + position: relative; +} + +/* 认证卡片 */ +.auth-card { + background: rgba(15, 23, 42, 0.8); + backdrop-filter: blur(20px); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 16px; + padding: 40px; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(99, 102, 241, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + animation: cardSlideUp 0.6s ease-out; +} + +@keyframes cardSlideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 认证头部 */ +.auth-header { + text-align: center; + margin-bottom: 32px; +} + +.auth-logo { + width: 64px; + height: 64px; + margin: 0 auto 20px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4); +} + +.auth-logo svg { + width: 32px; + height: 32px; + color: white; +} + +.auth-title { + font-size: 28px; + font-weight: 700; + margin: 0 0 8px 0; + background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.auth-subtitle { + color: #94a3b8; + font-size: 14px; + margin: 0; +} + +/* 表单样式 */ +.auth-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-label { + font-size: 14px; + font-weight: 500; + color: #e2e8f0; +} + +.input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.input-icon { + position: absolute; + left: 14px; + width: 18px; + height: 18px; + color: #64748b; + pointer-events: none; + transition: color 0.2s; +} + +.form-input { + width: 100%; + padding: 12px 14px 12px 44px; + background: rgba(30, 41, 59, 0.6); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 8px; + color: #e2e8f0; + font-size: 14px; + transition: all 0.2s; +} + +.form-input::placeholder { + color: #64748b; +} + +.form-input:focus { + outline: none; + border-color: #6366f1; + background: rgba(30, 41, 59, 0.8); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +.form-input:focus + .input-icon, +.form-input:focus ~ .input-icon { + color: #6366f1; +} + +.input-hint { + font-size: 12px; + color: #64748b; + margin-top: 4px; +} + +/* 切换密码可见性按钮 */ +.toggle-password { + position: absolute; + right: 12px; + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: #64748b; + transition: color 0.2s; +} + +.toggle-password:hover { + color: #6366f1; +} + +.toggle-password svg { + width: 18px; + height: 18px; +} + +/* 表单选项 */ +.form-options { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + color: #94a3b8; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: #6366f1; + cursor: pointer; +} + +.forgot-link { + color: #6366f1; + text-decoration: none; + transition: color 0.2s; +} + +.forgot-link:hover { + color: #8b5cf6; +} + +/* 警告框 */ +.alert { + padding: 12px 16px; + border-radius: 8px; + font-size: 14px; + display: none; +} + +.alert-error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: #fca5a5; +} + +.alert-success { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + color: #86efac; +} + +/* 按钮样式 */ +.btn-auth { + width: 100%; + padding: 14px 24px; + font-size: 16px; + font-weight: 600; + margin-top: 8px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.btn-auth:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-spinner { + width: 20px; + height: 20px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 认证页脚 */ +.auth-footer { + text-align: center; + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid rgba(99, 102, 241, 0.2); + color: #94a3b8; + font-size: 14px; +} + +.auth-link { + color: #6366f1; + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.auth-link:hover { + color: #8b5cf6; + text-decoration: underline; +} + +/* 装饰元素 */ +.auth-decoration { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + overflow: hidden; + z-index: -1; +} + +.deco-circle { + position: absolute; + border-radius: 50%; + background: radial-gradient(circle, rgba(99, 102, 241, 0.3) 0%, transparent 70%); +} + +.deco-circle-1 { + width: 400px; + height: 400px; + top: -100px; + right: -100px; + animation: float 20s ease-in-out infinite; +} + +.deco-circle-2 { + width: 300px; + height: 300px; + bottom: -50px; + left: 10%; + background: radial-gradient(circle, rgba(139, 92, 246, 0.3) 0%, transparent 70%); + animation: float 15s ease-in-out infinite reverse; +} + +.deco-circle-3 { + width: 200px; + height: 200px; + top: 40%; + right: 15%; + background: radial-gradient(circle, rgba(6, 182, 212, 0.2) 0%, transparent 70%); + animation: float 18s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translate(0, 0); + } + 25% { + transform: translate(20px, -20px); + } + 50% { + transform: translate(-10px, 15px); + } + 75% { + transform: translate(15px, 10px); + } +} + +/* 响应式 */ +@media (max-width: 640px) { + .auth-page { + margin-left: 0; + padding: 20px 16px; + } + + .auth-card { + padding: 32px 24px; + } + + .auth-title { + font-size: 24px; + } + + .form-options { + flex-direction: column; + gap: 12px; + align-items: flex-start; + } +} diff --git a/website/community/assets/css/community.css b/website/community/assets/css/community.css new file mode 100644 index 0000000..d3c49e7 --- /dev/null +++ b/website/community/assets/css/community.css @@ -0,0 +1,572 @@ +/* OSS Community 样式 - 已对齐官网视觉规范 */ +:root { + --bg: #030712; + --bg-card: rgba(255, 255, 255, 0.02); + --border: rgba(255, 255, 255, 0.05); + --border-hover: rgba(6, 182, 212, 0.3); + --cyan: #06b6d4; + --cyan-light: #22d3ee; + --text: #fff; + --text-secondary: #9ca3af; + --text-muted: #6b7280; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: 'Inter', sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; + overflow-x: hidden; +} + +/* 全局背景网格 */ +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: + linear-gradient(rgba(6, 182, 212, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(6, 182, 212, 0.03) 1px, transparent 1px); + background-size: 60px 60px; + pointer-events: none; + z-index: -1; +} + +/* 头部导航 */ +.comm-header { + position: sticky; + top: 0; + z-index: 100; + background: rgba(3, 7, 18, 0.8); + backdrop-filter: blur(16px); + border-bottom: 1px solid var(--border); + transition: border-color 0.3s; +} + +.header-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 24px; + height: 64px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.header-left { + display: flex; + align-items: center; + gap: 24px; +} + +.back-link { + color: var(--text-muted); + text-decoration: none; + font-size: 14px; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 4px; +} + +.back-link:hover { + color: var(--cyan-light); + transform: translateX(-4px); +} + +.site-title { + font-size: 18px; + font-weight: 700; + background: linear-gradient(135deg, var(--cyan), #3b82f6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: 0.02em; +} + +.btn { + padding: 10px 20px; + border-radius: 10px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + border: none; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 6px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--cyan), #3b82f6); + color: #fff; + box-shadow: 0 4px 12px rgba(6, 182, 212, 0.1); +} + +.btn-primary:hover { + box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4); + transform: translateY(-2px); +} + +/* 主布局 */ +.comm-main { + max-width: 1200px; + margin: 0 auto; + padding: 24px 100px 24px 24px; /* 右侧为 Dock 留空间 */ + min-height: calc(100vh - 64px); +} + +.comm-container { + display: grid; + grid-template-columns: 260px 1fr; + gap: 32px; + animation: fadeInUp 0.6s ease forwards; + opacity: 0; + transform: translateY(20px); +} + +@keyframes fadeInUp { + to { opacity: 1; transform: translateY(0); } +} + +/* 侧边栏 */ +.comm-sidebar { + position: sticky; + top: 88px; + height: fit-content; +} + +.sidebar-section { + background: rgba(10, 15, 30, 0.6); + border: 1px solid var(--border); + border-radius: 16px; + padding: 20px; + margin-bottom: 20px; + backdrop-filter: blur(8px); + transition: all 0.3s ease; +} + +.sidebar-section:hover { + border-color: rgba(255, 255, 255, 0.1); +} + +.sidebar-section h3 { + font-size: 12px; + font-weight: 700; + color: var(--text-muted); + margin-bottom: 16px; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.category-list { list-style: none; } + +.category-list li { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 10px; + cursor: pointer; + transition: all 0.25s ease; + margin-bottom: 4px; + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; +} + +.category-list li:hover { + background: rgba(255, 255, 255, 0.04); + color: #fff; + transform: translateX(4px); +} + +.category-list li.active { + background: rgba(6, 182, 212, 0.1); + border-left: 3px solid var(--cyan); + color: var(--cyan-light); +} + +.cat-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.7; +} + +.cat-icon svg { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; +} + +.category-list li.active .cat-icon { opacity: 1; color: var(--cyan); } + +.cat-count { + font-size: 11px; + color: var(--text-muted); + background: rgba(255, 255, 255, 0.03); + padding: 2px 8px; + border-radius: 6px; + font-weight: 600; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + text-align: center; +} + +.stat-item { + padding: 10px 4px; + background: rgba(255, 255, 255, 0.02); + border-radius: 8px; +} + +.stat-num { + display: block; + font-size: 20px; + font-weight: 800; + color: var(--cyan-light); + margin-bottom: 4px; +} + +.stat-label { + font-size: 10px; + color: var(--text-muted); + font-weight: 500; + text-transform: uppercase; +} + +/* 内容区 */ +.comm-content { min-width: 0; } + +.content-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border); +} + +.content-header h2 { + font-size: 22px; + font-weight: 700; + color: #fff; + display: flex; + align-items: center; + gap: 10px; +} + +.content-header h2::before { + content: ''; + display: block; + width: 4px; + height: 20px; + background: var(--cyan); + border-radius: 2px; +} + +.sort-options { + display: flex; + gap: 8px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + border-radius: 10px; + padding: 4px; +} + +.sort-btn { + padding: 6px 14px; + border-radius: 8px; + background: transparent; + border: none; + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.sort-btn:hover { color: #fff; } + +.sort-btn.active { + background: rgba(6, 182, 212, 0.15); + color: var(--cyan-light); + box-shadow: 0 2px 6px rgba(6, 182, 212, 0.1); +} + +/* 帖子列表 - 卡片动效 */ +.posts-list { display: flex; flex-direction: column; gap: 16px; } + +.post-card { + background: rgba(10, 15, 30, 0.6); + border: 1px solid var(--border); + border-radius: 16px; + padding: 24px; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + cursor: pointer; + position: relative; + overflow: hidden; + animation: cardEnter 0.5s ease forwards; + opacity: 0; + transform: translateY(20px); +} + +@keyframes cardEnter { + to { opacity: 1; transform: translateY(0); } +} + +.post-card::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + background: linear-gradient(135deg, rgba(6, 182, 212, 0.05), transparent); + opacity: 0; + transition: opacity 0.4s; +} + +.post-card:hover { + border-color: var(--border-hover); + transform: translateY(-4px) scale(1.005); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4), 0 0 12px rgba(6, 182, 212, 0.1); +} + +.post-card:hover::before { opacity: 1; } + +.post-header { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 14px; + position: relative; + z-index: 1; +} + +.post-avatar { + width: 44px; + height: 44px; + border-radius: 12px; + background: linear-gradient(135deg, var(--cyan), #3b82f6); + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 18px; + color: #fff; + box-shadow: 0 4px 10px rgba(6, 182, 212, 0.3); + flex-shrink: 0; +} + +.post-meta { flex: 1; min-width: 0; } + +.post-author { + font-weight: 600; + font-size: 14px; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.post-time { + font-size: 12px; + color: var(--text-muted); +} + +.post-tags { display: flex; gap: 6px; flex-shrink: 0; } + +.tag { + padding: 3px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + background: rgba(6, 182, 212, 0.08); + border: 1px solid rgba(6, 182, 212, 0.15); + color: var(--cyan-light); + text-transform: uppercase; +} + +.pinned-badge { + padding: 3px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; + background: rgba(245, 158, 11, 0.15); + border: 1px solid rgba(245, 158, 11, 0.3); + color: #fbbf24; + display: flex; + align-items: center; + gap: 4px; +} + +.post-title { + font-size: 17px; + font-weight: 700; + margin-bottom: 8px; + display: flex; + align-items: flex-start; + gap: 8px; + position: relative; + z-index: 1; + color: #fff; + transition: color 0.2s; +} + +.post-card:hover .post-title { color: var(--cyan-light); } + +.post-excerpt { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 14px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + position: relative; + z-index: 1; +} + +.post-stats { + display: flex; + gap: 20px; + font-size: 12px; + color: var(--text-muted); + font-weight: 500; + position: relative; + z-index: 1; +} + +.post-stats span { + display: flex; + align-items: center; + gap: 6px; + transition: color 0.2s; +} + +.post-stats svg { + width: 14px; + height: 14px; + opacity: 0.6; +} + +.post-card:hover .post-stats span { color: var(--text-secondary); } + +/* 空状态 / 加载动画 */ +.empty-state { + text-align: center; + padding: 80px 20px; + color: var(--text-muted); + animation: fadeIn 0.5s ease; +} + +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + +.loading-spinner { + width: 32px; + height: 32px; + border: 3px solid rgba(6, 182, 212, 0.1); + border-top-color: var(--cyan); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 0 auto 16px; +} + +@keyframes spin { to { transform: rotate(360deg); } } + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.4; +} + +/* 分页 */ +.pagination { + display: flex; + justify-content: center; + gap: 8px; + margin-top: 32px; + padding-bottom: 32px; +} + +.page-btn { + width: 36px; + height: 36px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; +} + +.page-btn:hover { + background: rgba(6, 182, 212, 0.1); + border-color: var(--border-hover); + color: var(--cyan); + transform: translateY(-2px); +} + +.page-btn.active { + background: var(--cyan); + border-color: var(--cyan); + color: #fff; + box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4); +} + +/* 响应式 */ +@media (max-width: 900px) { + .comm-container { + grid-template-columns: 1fr; + } + .comm-sidebar { + position: static; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + } + .stats-grid { margin-top: 0; } +} + +@media (max-width: 600px) { + .header-container { padding: 0 16px; } + .comm-main { padding: 16px; } + .content-header h2 { font-size: 18px; } + .sort-options { display: none; } + .post-card { padding: 16px; } + .post-title { font-size: 15px; } +} +/* 创建帖子按钮样式 - 区别于普通导航链接 */ +button.dock-item { + background: transparent; + border: none; + cursor: pointer; +} + +.dock-action-btn { + color: #10b981; /* 翡翠绿 - 代表创建/动作 */ + background: rgba(16, 185, 129, 0.08); + border: 1px solid rgba(16, 185, 129, 0.2); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +.dock-action-btn:hover { + background: rgba(16, 185, 129, 0.2); + border-color: #10b981; + color: #34d399; + box-shadow: 0 0 12px rgba(16, 185, 129, 0.4); + transform: scale(1.1); +} diff --git a/website/community/assets/css/dock-popover.css b/website/community/assets/css/dock-popover.css new file mode 100644 index 0000000..3ca1507 --- /dev/null +++ b/website/community/assets/css/dock-popover.css @@ -0,0 +1,177 @@ +/* Dock 用户信息面板样式 */ + +.user-popover { + position: fixed; + z-index: 10001; + width: 260px; + background: rgba(15, 23, 42, 0.95); + backdrop-filter: blur(12px); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + opacity: 0; + visibility: hidden; + transform: translateY(10px) scale(0.98); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; + color: #e2e8f0; + font-family: 'Inter', sans-serif; +} + +.user-popover.active { + opacity: 1; + visibility: visible; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.popover-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + border-bottom: 1px solid rgba(99, 102, 241, 0.1); +} + +.popover-avatar { + width: 44px; + height: 44px; + border-radius: 10px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: 700; + color: white; + flex-shrink: 0; +} + +.popover-info { + flex: 1; + min-width: 0; +} + +.popover-name { + font-size: 14px; + font-weight: 600; + color: #f1f5f9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.popover-role { + font-size: 11px; + color: #94a3b8; + margin-top: 2px; + text-transform: uppercase; + letter-spacing: 0.5px; + background: rgba(255, 255, 255, 0.05); + padding: 2px 6px; + border-radius: 4px; + display: inline-block; +} + +.popover-menu { + padding: 8px; + border-bottom: 1px solid rgba(99, 102, 241, 0.1); +} + +.menu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + color: #e2e8f0; + text-decoration: none; + transition: all 0.2s; + background: transparent; + border: none; + cursor: pointer; + width: 100%; +} + +.menu-item:hover { + background: rgba(99, 102, 241, 0.15); + color: #f1f5f9; +} + +.menu-item svg { + width: 16px; + height: 16px; + flex-shrink: 0; + color: #94a3b8; +} + +.menu-item:hover svg { + color: #c7d2fe; +} + +.menu-item span { + flex: 1; + font-size: 13px; + font-weight: 500; +} + +.menu-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 10px; + background: rgba(99, 102, 241, 0.3); + color: #c7d2fe; + min-width: 24px; + text-align: center; +} + +.popover-footer { + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.popover-btn { + width: 100%; + padding: 8px 12px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + border: none; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.btn-logout { + background: rgba(99, 102, 241, 0.2); + color: #c7d2fe; + border: 1px solid rgba(99, 102, 241, 0.3); +} + +.btn-logout:hover { + background: rgba(99, 102, 241, 0.3); + color: white; +} + +.btn-danger { + background: transparent; + color: #94a3b8; + border: 1px solid rgba(100, 116, 139, 0.2); +} + +.btn-danger:hover { + background: rgba(239, 68, 68, 0.1); + color: #fca5a5; + border-color: rgba(239, 68, 68, 0.3); +} + +.btn-danger svg { + width: 14px; + height: 14px; +} diff --git a/website/community/assets/css/editor.css b/website/community/assets/css/editor.css new file mode 100644 index 0000000..6d81291 --- /dev/null +++ b/website/community/assets/css/editor.css @@ -0,0 +1,325 @@ +/* OSS Community Editor - 优化版两栏布局 */ + +* { margin: 0; padding: 0; box-sizing: border-box; } + +.editor-page { + min-height: 100vh; + background: #0f172a; + display: flex; + flex-direction: column; +} + +.editor-container { + flex: 1; + display: flex; + flex-direction: column; + padding: 24px; + padding-right: 100px; + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +/* 顶部工具栏 */ +.editor-toolbar { + flex-shrink: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 18px; + background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 12px; + margin-bottom: 20px; +} + +.toolbar-left { display: flex; align-items: center; gap: 12px; } + +.back-btn { + display: flex; align-items: center; gap: 6px; + color: #94a3b8; text-decoration: none; font-size: 14px; font-weight: 500; + transition: color 0.2s; +} +.back-btn:hover { color: #e2e8f0; } +.back-btn svg { width: 18px; height: 18px; } + +.toolbar-title { + font-size: 16px; font-weight: 700; + background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; background-clip: text; +} + +.toolbar-right { display: flex; gap: 8px; } + +.btn { + display: flex; align-items: center; gap: 6px; + padding: 8px 16px; border-radius: 8px; font-size: 13px; + font-weight: 600; cursor: pointer; border: none; transition: all 0.2s; +} +.btn svg { width: 16px; height: 16px; } + +.btn-outline { + background: rgba(100, 116, 139, 0.2); + color: #94a3b8; border: 1px solid rgba(100, 116, 139, 0.3); +} +.btn-outline:hover { background: rgba(100, 116, 139, 0.3); color: #e2e8f0; } + +.btn-primary { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); +} +.btn-primary:hover { box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4); transform: translateY(-1px); } + +/* 标题输入 */ +.editor-header { + flex-shrink: 0; + margin-bottom: 20px; +} + +.title-input { + width: 100%; + padding: 12px 16px; + background: rgba(30, 41, 59, 0.6); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 10px; + color: #e2e8f0; + font-size: 24px; + font-weight: 700; + transition: all 0.2s; +} +.title-input:focus { + outline: none; border-color: #6366f1; + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} +.title-input::placeholder { color: #64748b; font-weight: 400; } + +/* 两栏主工作区 */ +.editor-workspace { + display: grid; + grid-template-columns: 1fr 280px; + gap: 20px; + flex: 1; + min-height: calc(100vh - 280px); +} + +/* 编辑器面板 */ +.editor-panel { + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 12px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.panel-header { + flex-shrink: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + background: rgba(30, 41, 59, 0.4); + border-bottom: 1px solid rgba(99, 102, 241, 0.1); +} + +.panel-title { + font-size: 12px; font-weight: 600; color: #94a3b8; + text-transform: uppercase; letter-spacing: 0.05em; +} + +.panel-actions { display: flex; gap: 4px; } + +.md-btn { + padding: 6px 8px; + background: transparent; border: 1px solid transparent; border-radius: 6px; + color: #64748b; cursor: pointer; transition: all 0.2s; +} +.md-btn:hover { + background: rgba(99, 102, 241, 0.1); + border-color: rgba(99, 102, 241, 0.2); + color: #e2e8f0; +} +.md-btn svg { width: 16px; height: 16px; } + +.editor-textarea { + flex: 1; + width: 100%; + padding: 18px; + background: transparent; + border: none; + color: #e2e8f0; + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + line-height: 1.8; + resize: none; + overflow-y: auto; + tab-size: 2; + min-height: 600px; +} +.editor-textarea:focus { outline: none; } +.editor-textarea::placeholder { color: #475569; } + +/* 右侧栏 */ +.sidebar-panel { + display: flex; + flex-direction: column; + gap: 16px; +} + +.sidebar-section { + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 12px; + padding: 16px; +} + +.sidebar-title { + font-size: 12px; + font-weight: 600; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 12px; +} + +.meta-select-large { + width: 100%; + padding: 8px 12px; + background: rgba(30, 41, 59, 0.6); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 8px; + color: #e2e8f0; + font-size: 13px; +} +.meta-select-large:focus { outline: none; border-color: #6366f1; } + +/* 标签管理 */ +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} + +.tag-item { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background: rgba(99, 102, 241, 0.15); + border: 1px solid rgba(99, 102, 241, 0.3); + border-radius: 14px; + font-size: 12px; + font-weight: 500; + color: #c7d2fe; +} + +.tag-remove { + background: none; border: none; color: #94a3b8; + cursor: pointer; font-size: 16px; line-height: 1; padding: 0; + width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; +} +.tag-remove:hover { color: #ef4444; } + +.tag-input { + width: 100%; + padding: 6px 10px; + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 6px; + color: #e2e8f0; + font-size: 12px; + margin-bottom: 10px; +} +.tag-input:focus { outline: none; border-color: #6366f1; } + +.tags-suggestions { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.tag-suggestion { + padding: 3px 10px; + background: rgba(100, 116, 139, 0.15); + border: 1px solid rgba(100, 116, 139, 0.2); + border-radius: 12px; + font-size: 11px; + color: #94a3b8; + cursor: pointer; + transition: all 0.2s; +} +.tag-suggestion:hover { + background: rgba(99, 102, 241, 0.1); + border-color: rgba(99, 102, 241, 0.2); + color: #c7d2fe; +} + +/* Markdown 帮助 */ +.markdown-help { + display: flex; + flex-direction: column; + gap: 6px; +} + +.help-item { + font-size: 12px; + color: #94a3b8; + line-height: 1.5; +} + +.help-item code { + display: inline-block; + background: rgba(30, 41, 59, 0.6); + padding: 2px 6px; + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: #f472b6; + margin-right: 6px; + min-width: 90px; +} + +/* Toast 提示 */ +.toast { + position: fixed; bottom: 16px; right: 16px; + padding: 10px 14px; border-radius: 8px; + display: flex; align-items: center; gap: 6px; + font-size: 13px; font-weight: 500; + z-index: 10000; animation: toastSlideIn 0.3s ease-out; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); +} +@keyframes toastSlideIn { + from { opacity: 0; transform: translateX(100px); } + to { opacity: 1; transform: translateX(0); } +} +.toast svg { width: 16px; height: 16px; } +.toast-success { background: rgba(34, 197, 94, 0.15); border: 1px solid rgba(34, 197, 94, 0.3); color: #86efac; } +.toast-error { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); color: #fca5a5; } + +/* 响应式 */ +@media (max-width: 1024px) { + .editor-container { padding: 20px; } + .editor-workspace { grid-template-columns: 1fr; } + .sidebar-panel { + flex-direction: row; + flex-wrap: wrap; + } + .sidebar-section { flex: 1; min-width: 200px; } +} + +@media (max-width: 768px) { + .editor-page { margin-left: 0; } + .editor-container { padding: 16px; padding-bottom: 80px; } + .editor-toolbar { + flex-direction: column; + gap: 12px; + padding: 12px; + } + .toolbar-left, .toolbar-right { width: 100%; justify-content: center; } + .title-input { font-size: 18px; padding: 10px 14px; } + .editor-workspace { min-height: auto; } + .sidebar-panel { flex-direction: column; } + .editor-textarea { min-height: 400px; } +} diff --git a/website/community/assets/css/post-drawer.css b/website/community/assets/css/post-drawer.css new file mode 100644 index 0000000..32779dd --- /dev/null +++ b/website/community/assets/css/post-drawer.css @@ -0,0 +1,316 @@ +/* OSS Community - 文章抽屉样式 */ + +/* 遮罩层 */ +.post-drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + z-index: 9998; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.post-drawer-overlay.active { + opacity: 1; + visibility: visible; +} + +/* 抽屉容器 */ +.post-drawer { + position: fixed; + left: 50%; + bottom: 0; + transform: translateX(-50%) translateY(100%); + width: 90%; + max-width: 900px; + max-height: 85vh; + background: #0f172a; + border: 1px solid rgba(99, 102, 241, 0.2); + border-bottom: none; + border-radius: 20px 20px 0 0; + box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.5); + z-index: 9999; + display: flex; + flex-direction: column; + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); + overflow: hidden; +} + +.post-drawer.active { + transform: translateX(-50%) translateY(0); +} + +/* 抽屉顶部标题栏 */ +.post-drawer-header { + flex-shrink: 0; + padding: 24px 28px 20px; + border-bottom: 1px solid rgba(99, 102, 241, 0.1); + background: rgba(15, 23, 42, 0.95); + position: relative; +} + +.post-drawer-close { + position: absolute; + top: 16px; + right: 16px; + width: 36px; + height: 36px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(30, 41, 59, 0.6); + color: #94a3b8; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.post-drawer-close:hover { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.3); + color: #fca5a5; + transform: scale(1.05); +} + +.post-drawer-close svg { + width: 18px; + height: 18px; +} + +.post-drawer-title { + font-size: 22px; + font-weight: 700; + color: #e2e8f0; + margin: 0 0 12px 0; + padding-right: 50px; + line-height: 1.3; +} + +.post-drawer-meta { + display: flex; + align-items: center; + gap: 12px; + color: #64748b; + font-size: 13px; +} + +.post-drawer-avatar { + width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 14px; + color: white; + flex-shrink: 0; +} + +.post-drawer-user-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.post-drawer-username { + color: #e2e8f0; + font-weight: 600; + font-size: 13px; +} + +.post-drawer-date { + color: #64748b; + font-size: 12px; +} + +.post-drawer-category { + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 8px; + background: rgba(99, 102, 241, 0.2); + color: #c7d2fe; + margin-left: auto; +} + +/* 抽屉内容区域 */ +.post-drawer-body { + flex: 1; + overflow-y: auto; + padding: 28px; +} + +.post-drawer-content { + color: #cbd5e1; + font-size: 15px; + line-height: 1.8; +} + +.post-drawer-content h1, +.post-drawer-content h2, +.post-drawer-content h3 { + color: #e2e8f0; + margin: 1.2em 0 0.6em; + font-weight: 700; +} + +.post-drawer-content h1 { font-size: 1.8em; border-bottom: 2px solid rgba(99, 102, 241, 0.3); padding-bottom: 0.3em; } +.post-drawer-content h2 { font-size: 1.4em; border-bottom: 1px solid rgba(99, 102, 241, 0.2); padding-bottom: 0.2em; } +.post-drawer-content h3 { font-size: 1.2em; } + +.post-drawer-content p { + color: #cbd5e1; + line-height: 1.8; + margin: 0.8em 0; +} + +.post-drawer-content code { + background: rgba(30, 41, 59, 0.6); + padding: 2px 6px; + border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.9em; + color: #f472b6; +} + +.post-drawer-content pre { + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(99, 102, 241, 0.2); + border-radius: 8px; + padding: 16px; + overflow-x: auto; + margin: 0.8em 0; +} + +.post-drawer-content pre code { + background: transparent; + padding: 0; + color: #e2e8f0; +} + +.post-drawer-content blockquote { + border-left: 4px solid #6366f1; + margin: 0.8em 0; + padding: 12px 16px; + background: rgba(99, 102, 241, 0.05); + border-radius: 0 8px 8px 0; + color: #94a3b8; +} + +.post-drawer-content ul, +.post-drawer-content ol { + padding-left: 24px; + color: #cbd5e1; +} + +.post-drawer-content li { + margin: 0.4em 0; +} + +.post-drawer-content a { + color: #6366f1; + text-decoration: none; +} + +.post-drawer-content a:hover { + text-decoration: underline; +} + +.post-drawer-content img { + max-width: 100%; + border-radius: 8px; + margin: 0.8em 0; +} + +.post-drawer-content hr { + border: none; + height: 1px; + background: rgba(99, 102, 241, 0.2); + margin: 1.5em 0; +} + +/* 底部统计栏 */ +.post-drawer-footer { + flex-shrink: 0; + padding: 16px 28px; + border-top: 1px solid rgba(99, 102, 241, 0.1); + background: rgba(15, 23, 42, 0.95); + display: flex; + gap: 24px; + color: #64748b; + font-size: 13px; + font-weight: 500; +} + +.post-drawer-stat { + display: flex; + align-items: center; + gap: 6px; +} + +.post-drawer-stat svg { + width: 16px; + height: 16px; + opacity: 0.7; +} + +/* 滚动条样式 */ +.post-drawer-body::-webkit-scrollbar { + width: 6px; +} + +.post-drawer-body::-webkit-scrollbar-track { + background: transparent; +} + +.post-drawer-body::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.3); + border-radius: 3px; +} + +.post-drawer-body::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.5); +} + +/* 响应式 */ +@media (max-width: 768px) { + .post-drawer { + width: 100%; + max-width: none; + max-height: 90vh; + border-radius: 16px 16px 0 0; + } + + .post-drawer-header { + padding: 20px 20px 16px; + } + + .post-drawer-title { + font-size: 18px; + } + + .post-drawer-body { + padding: 20px; + } + + .post-drawer-content { + font-size: 14px; + } + + .post-drawer-footer { + padding: 14px 20px; + gap: 16px; + } + + .post-drawer-close { + top: 12px; + right: 12px; + width: 32px; + height: 32px; + } +} diff --git a/website/community/assets/js/auth.js b/website/community/assets/js/auth.js new file mode 100644 index 0000000..727b127 --- /dev/null +++ b/website/community/assets/js/auth.js @@ -0,0 +1,168 @@ +/** + * OSS Community 认证页面 JS + * 处理登录/注册表单提交 + */ + +// 切换密码可见性 +function togglePassword(fieldId = 'password') { + const input = document.getElementById(fieldId); + const icon = document.getElementById(`eyeIcon-${fieldId}`) || document.getElementById('eyeIcon'); + + if (input.type === 'password') { + input.type = 'text'; + icon.innerHTML = ` + + `; + } else { + input.type = 'password'; + icon.innerHTML = ` + + + `; + } +} + +// 显示错误消息 +function showError(message) { + const errorEl = document.getElementById('errorMessage'); + const successEl = document.getElementById('successMessage'); + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + if (successEl) { + successEl.style.display = 'none'; + } +} + +// 显示成功消息 +function showSuccess(message) { + const successEl = document.getElementById('successMessage'); + const errorEl = document.getElementById('errorMessage'); + if (successEl) { + successEl.textContent = message; + successEl.style.display = 'block'; + } + if (errorEl) { + errorEl.style.display = 'none'; + } +} + +// 隐藏消息 +function hideMessages() { + const errorEl = document.getElementById('errorMessage'); + const successEl = document.getElementById('successMessage'); + if (errorEl) errorEl.style.display = 'none'; + if (successEl) successEl.style.display = 'none'; +} + +// 设置按钮加载状态 +function setButtonLoading(loading) { + const btn = document.getElementById('submitBtn'); + const text = btn.querySelector('.btn-text'); + const spinner = btn.querySelector('.btn-spinner'); + + if (loading) { + btn.disabled = true; + text.style.display = 'none'; + spinner.style.display = 'block'; + } else { + btn.disabled = false; + text.style.display = 'inline'; + spinner.style.display = 'none'; + } +} + +// 登录表单 +const loginForm = document.getElementById('loginForm'); +if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideMessages(); + setButtonLoading(true); + + const formData = { + username: document.getElementById('username').value.trim(), + password: document.getElementById('password').value, + remember: document.getElementById('remember').checked + }; + + try { + const response = await fetch('api/auth.php?action=login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (result.success) { + showSuccess('登录成功!正在跳转...'); + setTimeout(() => { + window.location.href = 'index.php'; + }, 1000); + } else { + showError(result.message || '登录失败,请检查用户名和密码'); + } + } catch (error) { + showError('网络错误,请稍后重试'); + } finally { + setButtonLoading(false); + } + }); +} + +// 注册表单 +const registerForm = document.getElementById('registerForm'); +if (registerForm) { + registerForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideMessages(); + setButtonLoading(true); + + const password = document.getElementById('password').value; + const confirmPassword = document.getElementById('confirmPassword').value; + + // 前端验证 + if (password !== confirmPassword) { + showError('两次输入的密码不一致'); + setButtonLoading(false); + return; + } + + if (password.length < 6) { + showError('密码长度至少 6 个字符'); + setButtonLoading(false); + return; + } + + const formData = { + username: document.getElementById('username').value.trim(), + email: document.getElementById('email').value.trim(), + password: password + }; + + try { + const response = await fetch('api/auth.php?action=register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (result.success) { + showSuccess('注册成功!正在跳转到登录页...'); + setTimeout(() => { + window.location.href = 'login.php'; + }, 1500); + } else { + showError(result.message || '注册失败,请稍后重试'); + } + } catch (error) { + showError('网络错误,请稍后重试'); + } finally { + setButtonLoading(false); + } + }); +} diff --git a/website/community/assets/js/community.js b/website/community/assets/js/community.js new file mode 100644 index 0000000..1fa9709 --- /dev/null +++ b/website/community/assets/js/community.js @@ -0,0 +1,255 @@ +/** + * OSS Community Module + * Handles rendering of categories, stats, and posts list. + * Designed to be SPA-friendly (re-initializable). + */ + +// Global state variables +let currentPage = 1; +let currentCategory = ''; +let currentSort = 'latest'; + +// Expose initialization function globally for SPA Router +window.initCommunity = function() { + // Check if we are deep-linked (e.g. ?page=2) + const params = new URLSearchParams(window.location.search); + if(params.has('page')) currentPage = parseInt(params.get('page')); + + // Fetch and Render Data + loadCategories(); + loadStats(); + loadPosts(); + + // Bind Events (to the new DOM elements created by SPA) + bindCommunityEvents(); +}; + +function bindCommunityEvents() { + const catList = document.getElementById('categoryList'); + if (catList) { + catList.addEventListener('click', e => { + const li = e.target.closest('li'); + if (!li) return; + + document.querySelectorAll('.category-list li').forEach(el => el.classList.remove('active')); + li.classList.add('active'); + + currentCategory = li.dataset.id || ''; + currentPage = 1; + document.getElementById('currentCategory').textContent = li.querySelector('span:nth-child(2)').textContent + '帖子'; + loadPosts(); + }); + } + + const sortOptions = document.querySelector('.sort-options'); + if (sortOptions) { + sortOptions.addEventListener('click', e => { + if (!e.target.classList.contains('sort-btn')) return; + document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active')); + e.target.classList.add('active'); + currentSort = e.target.dataset.sort; + currentPage = 1; + loadPosts(); + }); + } +} + +// --- Data Loading Functions --- + +const API = './api/index.php'; // Relative to community/ directory + +async function loadCategories() { + try { + const res = await fetch(`${API}?action=categories`); + const data = await res.json(); + + const list = document.getElementById('categoryList'); + if (!list) return; + + // Keep the "All" item (first child) if it exists + const allItem = list.firstElementChild; + list.innerHTML = ''; + if (allItem) list.appendChild(allItem); + + data.categories.forEach(cat => { + const li = document.createElement('li'); + li.dataset.id = cat.id; + li.innerHTML = `${getIconSvg(cat.icon)}${cat.name}`; + list.appendChild(li); + }); + } catch (e) { + console.error('Failed to load categories', e); + } +} + +async function loadStats() { + try { + const res = await fetch(`${API}?action=stats`); + const data = await res.json(); + + animateValue('statPosts', 0, data.posts, 1000); + animateValue('statReplies', 0, data.replies, 1000); + animateValue('statUsers', 0, data.users, 1000); + const countAll = document.getElementById('countAll'); + if (countAll) countAll.textContent = data.posts; + } catch (e) { + console.error('Failed to load stats', e); + } +} + +async function loadPosts() { + const list = document.getElementById('postsList'); + if (!list) return; + + list.innerHTML = '

正在连接节点...

'; + + try { + const params = new URLSearchParams({ action: 'posts', page: currentPage }); + if (currentCategory) params.append('category_id', currentCategory); + + const res = await fetch(`${API}?${params}`); + const data = await res.json(); + + list.innerHTML = ''; + + if (data.posts.length === 0) { + list.innerHTML = ` +
+
📭
+

暂无帖子

+

成为第一个发帖的人吧!

+
+ `; + return; + } + + data.posts.forEach((post, index) => { + const card = document.createElement('div'); + card.className = 'post-card'; + card.dataset.postId = post.id; + card.style.animationDelay = `${index * 0.1}s`; + + card.innerHTML = ` +
+
${post.username[0].toUpperCase()}
+
+
${escapeHtml(post.username)}
+
${timeAgo(post.created_at)}
+
+
+ ${post.category_name} + ${post.is_pinned ? ' 置顶' : ''} +
+
+
${escapeHtml(post.title)}
+
${escapeHtml(post.content.substring(0, 150))}...
+
+ + + ${post.views} + + + + ${post.likes} + + + + ${post.reply_count || 0} + +
+ `; + list.appendChild(card); + }); + + renderPagination(data.pages); + } catch (e) { + list.innerHTML = '

加载失败,请稍后重试

'; + } +} + +function renderPagination(pages) { + const container = document.getElementById('pagination'); + if (!container) return; + + container.innerHTML = ''; + if (pages <= 1) return; + + for (let i = 1; i <= pages; i++) { + const btn = document.createElement('button'); + btn.className = `page-btn ${i === currentPage ? 'active' : ''}`; + btn.textContent = i; + btn.onclick = () => { + currentPage = i; + loadPosts(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + container.appendChild(btn); + } +} + +// --- Utilities --- + +function animateValue(id, start, end, duration) { + const obj = document.getElementById(id); + if (!obj) return; + let startTimestamp = null; + const step = (timestamp) => { + if (!startTimestamp) startTimestamp = timestamp; + const progress = Math.min((timestamp - startTimestamp) / duration, 1); + obj.textContent = Math.floor(progress * (end - start) + start); + if (progress < 1) { + window.requestAnimationFrame(step); + } + }; + window.requestAnimationFrame(step); +} + +function timeAgo(dateStr) { + const now = new Date(); + const date = new Date(dateStr); + const seconds = Math.floor((now - date) / 1000); + const intervals = [ + [31536000, '年'], [2592000, '个月'], [604800, '周'], + [86400, '天'], [3600, '小时'], [60, '分钟'] + ]; + for (const [secondsCount, label] of intervals) { + const count = Math.floor(seconds / secondsCount); + if (count >= 1) return `${count}${label}前`; + } + return '刚刚'; +} + +function escapeHtml(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +function getIconSvg(name) { + const icons = { + megaphone: '', + question: '', + chat: '', + puzzle: '', + bug: '' + }; + return icons[name] || icons['chat']; +} + +// showCreateModal 函数已移至 post-modal.php 中定义 +// 此处仅保留兼容性封装 +window.showCreateModal = window.showCreateModal || function() { + if (typeof window.showCreatePostModal === 'function') { + window.showCreatePostModal(); + } else { + console.warn('发帖模态框未加载'); + } +}; + +// Auto-init if not loaded via SPA (Hard refresh case) +document.addEventListener('DOMContentLoaded', () => { + if (typeof AppRouter === 'undefined') { + window.initCommunity(); + } +}); \ No newline at end of file diff --git a/website/community/assets/js/dock-popover.js b/website/community/assets/js/dock-popover.js new file mode 100644 index 0000000..4d4940e --- /dev/null +++ b/website/community/assets/js/dock-popover.js @@ -0,0 +1,91 @@ +document.addEventListener('DOMContentLoaded', () => { + const trigger = document.getElementById('dockUserMenuBtn'); + const popover = document.getElementById('dockUserMenu'); + const logoutBtn = document.getElementById('logoutBtn'); + const deleteBtn = document.getElementById('deleteAccountBtn'); + const myPostCount = document.getElementById('myPostCount'); + + // 获取用户文章数量 + async function fetchMyPostCount() { + if (!myPostCount) return; + try { + const res = await fetch('api/auth.php?action=my-post-count'); + if (res.ok) { + const data = await res.json(); + if (data.success) { + myPostCount.textContent = data.count; + } + } + } catch (err) { + console.error('Failed to fetch post count:', err); + } + } + + // 页面加载时获取文章数量 + fetchMyPostCount(); + + if (trigger && popover) { + // 点击图标切换面板 + trigger.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + const isActive = popover.classList.contains('active'); + if (isActive) { + popover.classList.remove('active'); + } else { + // 计算位置:在图标右侧 + const rect = trigger.getBoundingClientRect(); + // Dock 通常在左侧,我们定位在图标右边,稍微向上偏移一点居中 + popover.style.left = `${rect.right + 16}px`; + popover.style.top = `${rect.top + 10}px`; + popover.classList.add('active'); + } + }); + + // 点击外部关闭 + document.addEventListener('click', (e) => { + if (!popover.contains(e.target) && !trigger.contains(e.target)) { + popover.classList.remove('active'); + } + }); + + // 退出登录逻辑 + if (logoutBtn) { + logoutBtn.addEventListener('click', async () => { + try { + const originalText = logoutBtn.innerHTML; + logoutBtn.innerHTML = ' 退出中...'; + logoutBtn.disabled = true; + + // 使用相对路径,因为 JS 可能在子目录中运行 + // 注意:这里假设 api/auth.php 相对于当前页面路径可用 + // 在 community/index.php 中,api/ 是同级目录 + const res = await fetch('api/auth.php?action=logout'); + + if (res.ok) { + // 退出成功,刷新页面 + window.location.reload(); + } else { + logoutBtn.innerHTML = originalText; + logoutBtn.disabled = false; + alert('退出失败,请重试'); + } + } catch (err) { + console.error('Logout error:', err); + logoutBtn.innerHTML = '退出失败'; + logoutBtn.disabled = false; + } + }); + } + + // 注销账户逻辑 + if (deleteBtn) { + deleteBtn.addEventListener('click', () => { + if (confirm('确定要注销(永久删除)此账户吗?\n此操作不可撤销,所有数据将被清除。')) { + alert('该功能暂未开放,请联系管理员。'); + } + }); + } + } +}); diff --git a/website/community/assets/js/editor.js b/website/community/assets/js/editor.js new file mode 100644 index 0000000..3bc90d3 --- /dev/null +++ b/website/community/assets/js/editor.js @@ -0,0 +1,252 @@ +/** + * OSS Community Editor JS + * Markdown 编辑器、实时预览、表单提交 + */ + +document.addEventListener('DOMContentLoaded', () => { + initEditor(); + initToolbar(); + initTags(); + initForm(); +}); + +// 初始化编辑器 +function initEditor() { + const textarea = document.getElementById('postContent'); + const titleInput = document.getElementById('postTitle'); + + // Tab 键支持 + if (textarea) { + textarea.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + e.preventDefault(); + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 2; + } + }); + } +} + +// 初始化工具栏 +function initToolbar() { + const buttons = document.querySelectorAll('.md-btn'); + const textarea = document.getElementById('postContent'); + + if (!textarea) return; + + buttons.forEach(btn => { + btn.addEventListener('click', () => { + const action = btn.dataset.md; + insertMarkdown(textarea, action); + }); + }); +} + +function insertMarkdown(textarea, action) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selected = textarea.value.substring(start, end); + let insertion = ''; + + switch (action) { + case 'bold': + insertion = `**${selected || '粗体文本'}**`; + break; + case 'italic': + insertion = `*${selected || '斜体文本'}*`; + break; + case 'heading': + insertion = `\n## ${selected || '标题'}\n`; + break; + case 'quote': + insertion = `\n> ${selected || '引用文本'}\n`; + break; + case 'code': + insertion = selected.includes('\n') ? `\n\`\`\`\n${selected || '代码块'}\n\`\`\`\n` : `\`${selected || '行内代码'}\``; + break; + case 'link': + insertion = `[${selected || '链接文本'}](url)`; + break; + case 'list': + insertion = `\n- ${selected || '列表项'}\n`; + break; + } + + textarea.value = textarea.value.substring(0, start) + insertion + textarea.value.substring(end); + textarea.focus(); + textarea.selectionStart = textarea.selectionEnd = start + insertion.length; +} + +// 初始化标签 +function initTags() { + const tagInput = document.getElementById('tagInput'); + const tagsContainer = document.getElementById('tagsContainer'); + + if (!tagInput || !tagsContainer) return; + + tagInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const tagName = tagInput.value.trim(); + if (tagName) { + addTag(tagName); + tagInput.value = ''; + } + } + }); +} + +function addTag(name) { + const tagsContainer = document.getElementById('tagsContainer'); + if (!tagsContainer) return; + + // 检查是否已存在 + const existing = tagsContainer.querySelectorAll('.tag-item'); + for (const tag of existing) { + if (tag.textContent.trim().replace('×', '').trim() === name) { + return; + } + } + + const tagEl = document.createElement('span'); + tagEl.className = 'tag-item'; + tagEl.innerHTML = ` + ${name} + + `; + tagsContainer.appendChild(tagEl); +} + +// 初始化表单 +function initForm() { + const form = document.getElementById('postEditorForm'); + const saveBtn = document.getElementById('savePostBtn'); + + if (!form || !saveBtn) return; + + saveBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + const postId = document.getElementById('editPostId').value; + const title = document.getElementById('postTitle').value.trim(); + const content = document.getElementById('postContent').value.trim(); + const categoryId = document.getElementById('postCategory').value; + + // 收集标签 + const tags = []; + document.querySelectorAll('#tagsContainer .tag-item').forEach(tag => { + const name = tag.textContent.replace('×', '').trim(); + if (name) tags.push(name); + }); + + // 验证 + if (!title) { + showError('请输入帖子标题'); + return; + } + if (title.length < 5) { + showError('标题至少 5 个字符'); + return; + } + if (!content) { + showError('请输入帖子内容'); + return; + } + if (content.length < 10) { + showError('内容至少 10 个字符'); + return; + } + if (!categoryId) { + showError('请选择分类'); + return; + } + + // 提交 + setSaveButtonLoading(true); + + const formData = { + title: title, + content: content, + category_id: categoryId, + tags: tags + }; + + if (postId) { + formData.id = parseInt(postId); + } + + const action = postId ? 'update' : 'create'; + const url = `api/posts.php?action=${action}`; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (result.success) { + showSuccess(postId ? '更新成功!' : '发布成功!正在跳转...'); + setTimeout(() => { + window.location.href = `post.php?id=${result.post_id || postId}`; + }, 1000); + } else { + showError(result.message || '操作失败'); + } + } catch (error) { + showError('网络错误,请稍后重试'); + } finally { + setSaveButtonLoading(false); + } + }); +} + +function showSuccess(message) { + const toast = document.getElementById('successToast'); + const msgEl = document.getElementById('successMessage'); + if (toast && msgEl) { + msgEl.textContent = message; + toast.style.display = 'flex'; + setTimeout(() => { toast.style.display = 'none'; }, 3000); + } +} + +function showError(message) { + const toast = document.getElementById('errorToast'); + const msgEl = document.getElementById('errorMessage'); + if (toast && msgEl) { + msgEl.textContent = message; + toast.style.display = 'flex'; + setTimeout(() => { toast.style.display = 'none'; }, 4000); + } +} + +function setSaveButtonLoading(loading) { + const btn = document.getElementById('savePostBtn'); + if (!btn) return; + + if (loading) { + btn.disabled = true; + btn.style.opacity = '0.6'; + btn.innerHTML = ` + + + + + 处理中... + `; + } else { + btn.disabled = false; + btn.style.opacity = '1'; + btn.innerHTML = ` + + + + ${document.getElementById('editPostId').value ? '保存修改' : '发布帖子'} + `; + } +} diff --git a/website/community/assets/js/polling-system.js b/website/community/assets/js/polling-system.js new file mode 100644 index 0000000..2f93251 --- /dev/null +++ b/website/community/assets/js/polling-system.js @@ -0,0 +1,91 @@ +/** + * OSS Community - 实时轮询系统 + * 每2秒从数据库刷新所有数据 + */ + +class CommunityPollingSystem { + constructor() { + this.interval = 2000; + this.timer = null; + this.isRunning = false; + this.listeners = { user: [], posts: [], stats: [], categories: [] }; + this.cache = { user: null, posts: null, stats: null, categories: null }; + } + + start() { + if (this.isRunning) return; + this.isRunning = true; + this._fetchAll(); + this.timer = setInterval(() => this._fetchAll(), this.interval); + } + + stop() { + clearInterval(this.timer); + this.isRunning = false; + this.timer = null; + } + + on(event, callback) { + if (this.listeners[event]) this.listeners[event].push(callback); + } + + async _fetch(url) { + try { + const res = await fetch(url); + return res.ok ? await res.json() : null; + } catch { return null; } + } + + async _fetchAll() { + // 并行请求所有接口 + const [userData, postsData, statsData, catsData] = await Promise.all([ + this._fetch('api/auth.php?action=current-user'), + this._fetch('api/index.php?action=posts'), + this._fetch('api/index.php?action=stats'), + this._fetch('api/index.php?action=categories') + ]); + + // 用户数据 + if (userData && userData.success) { + const changed = JSON.stringify(userData) !== JSON.stringify(this.cache.user); + this.cache.user = userData; + if (changed) this._notify('user', userData); + } + + // 帖子数据(含 views, likes, replies) + if (postsData && postsData.posts) { + const changed = JSON.stringify(postsData.posts) !== JSON.stringify(this.cache.posts); + this.cache.posts = postsData.posts; + if (changed) this._notify('posts', postsData); + } + + // 统计数据(含 hot_posts) + if (statsData) { + const changed = JSON.stringify(statsData) !== JSON.stringify(this.cache.stats); + this.cache.stats = statsData; + if (changed) this._notify('stats', statsData); + } + + // 分类 + if (catsData && catsData.categories) { + const changed = JSON.stringify(catsData.categories) !== JSON.stringify(this.cache.categories); + this.cache.categories = catsData.categories; + if (changed) this._notify('categories', catsData); + } + } + + _notify(event, data) { + (this.listeners[event] || []).forEach(fn => { + try { fn(data); } catch(e) {} + }); + } +} + +window.Polling = new CommunityPollingSystem(); + +document.addEventListener('DOMContentLoaded', () => { + const dock = document.getElementById('dock'); + if (dock && dock.dataset.loggedIn === '1') { + window.Polling.start(); + } +}); diff --git a/website/community/assets/js/post-drawer.js b/website/community/assets/js/post-drawer.js new file mode 100644 index 0000000..3e181f6 --- /dev/null +++ b/website/community/assets/js/post-drawer.js @@ -0,0 +1,236 @@ +/** + * OSS Community - 文章抽屉交互逻辑 + */ +document.addEventListener('DOMContentLoaded', () => { + initPostDrawer(); +}); + +let drawerInitialized = false; +let currentDrawerPostId = null; +let drawerPollTimer = null; + +function initPostDrawer() { + if (drawerInitialized) return; + drawerInitialized = true; + + createDrawerElements(); + + // 全局点击事件委托 + document.addEventListener('click', (e) => { + const postCard = e.target.closest('.post-card'); + if (postCard && postCard.dataset.postId) { + e.preventDefault(); + e.stopPropagation(); + openPostDrawer(postCard.dataset.postId); + } + }); + + const overlay = document.getElementById('postDrawerOverlay'); + if (overlay) overlay.addEventListener('click', closePostDrawer); + + const closeBtn = document.getElementById('postDrawerClose'); + if (closeBtn) closeBtn.addEventListener('click', closePostDrawer); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closePostDrawer(); + }); + + // 监听轮询数据更新抽屉内的 views/likes + if (window.Polling) { + window.Polling.on('posts', (data) => { + if (!data.posts || !currentDrawerPostId) return; + const post = data.posts.find(p => p.id == currentDrawerPostId); + if (post) updateDrawerStats(post); + }); + } +} + +function createDrawerElements() { + if (document.getElementById('postDrawerOverlay')) return; + + // 创建遮罩层 + const overlay = document.createElement('div'); + overlay.id = 'postDrawerOverlay'; + overlay.className = 'post-drawer-overlay'; + + // 创建抽屉 + const drawer = document.createElement('div'); + drawer.id = 'postDrawer'; + drawer.className = 'post-drawer'; + drawer.innerHTML = ` +
+ +

+
+
+
+
+
+
+ `; + + document.body.appendChild(overlay); + document.body.appendChild(drawer); + + // 绑定关闭按钮 + document.getElementById('postDrawerClose').addEventListener('click', closePostDrawer); +} + +async function openPostDrawer(postId) { + const drawer = document.getElementById('postDrawer'); + const overlay = document.getElementById('postDrawerOverlay'); + const titleEl = document.getElementById('postDrawerTitle'); + const metaEl = document.getElementById('postDrawerMeta'); + const contentEl = document.getElementById('postDrawerContent'); + const footerEl = document.getElementById('postDrawerFooter'); + + if (!drawer || !overlay) return; + + // 显示加载状态 + titleEl.textContent = '加载中...'; + metaEl.innerHTML = ''; + contentEl.innerHTML = '
正在加载文章内容...
'; + footerEl.innerHTML = ''; + + // 显示抽屉(先滑入,再加载内容) + overlay.classList.add('active'); + drawer.classList.add('active'); + document.body.style.overflow = 'hidden'; + + try { + const response = await fetch(`api/index.php?action=post&id=${postId}`); + if (!response.ok) throw new Error('加载失败'); + + const data = await response.json(); + if (!data.post) throw new Error('帖子不存在'); + + const post = data.post; + const replies = data.replies || []; + + currentDrawerPostId = post.id; + + // 更新标题和元信息 + titleEl.textContent = post.title; + metaEl.innerHTML = ` +
${post.username.charAt(0).toUpperCase()}
+
+
${escapeHtml(post.username)}
+
${formatDate(post.created_at)}
+
+ ${escapeHtml(post.category_name)} + `; + + // 更新内容(使用 marked 解析 Markdown) + if (typeof marked !== 'undefined') { + contentEl.innerHTML = marked.parse(post.content); + } else { + contentEl.innerHTML = escapeHtml(post.content).replace(/\n/g, '
'); + } + + // 更新底部统计 + footerEl.innerHTML = ` +
+ + ${post.views} 浏览 +
+
+ + ${post.likes} 点赞 +
+
+ + ${replies.length} 回复 +
+ `; + + // 如果有回复,在内容下方显示 + if (replies.length > 0) { + contentEl.innerHTML += ` +
+

回复 (${replies.length})

+ ${replies.map(reply => ` +
+
+
+ ${reply.username.charAt(0).toUpperCase()} +
+ ${escapeHtml(reply.username)} + ${formatDate(reply.created_at)} +
+
${escapeHtml(reply.content).replace(/\n/g, '
')}
+
+ `).join('')} + `; + } + + } catch (error) { + console.error('Failed to load post:', error); + contentEl.innerHTML = ` +
+ + + + + +

加载失败

+

${error.message || '未知错误,请稍后重试'}

+
+ `; + } +} + +function closePostDrawer() { + const drawer = document.getElementById('postDrawer'); + const overlay = document.getElementById('postDrawerOverlay'); + + if (drawer) drawer.classList.remove('active'); + if (overlay) overlay.classList.remove('active'); + document.body.style.overflow = ''; + currentDrawerPostId = null; +} + +function updateDrawerStats(post) { + const footerEl = document.getElementById('postDrawerFooter'); + if (!footerEl || !currentDrawerPostId) return; + + footerEl.innerHTML = ` +
+ + ${post.views} 浏览 +
+
+ + ${post.likes} 点赞 +
+
+ + ${post.reply_count || 0} 回复 +
+ `; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function formatDate(dateString) { + const date = new Date(dateString); + const now = new Date(); + const diff = now - date; + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (minutes < 1) return '刚刚'; + if (minutes < 60) return `${minutes} 分钟前`; + if (hours < 24) return `${hours} 小时前`; + if (days < 7) return `${days} 天前`; + return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); +} diff --git a/website/community/assets/js/title-updater.js b/website/community/assets/js/title-updater.js new file mode 100644 index 0000000..ab4e0ae --- /dev/null +++ b/website/community/assets/js/title-updater.js @@ -0,0 +1,108 @@ +/** + * OSS Community - 轮询数据实时更新 + */ + +document.addEventListener('DOMContentLoaded', () => { + if (!window.Polling) return; + + // ========== 用户数据(称号、权限) ========== + let prevTitle = '', prevPerms = {}; + window.Polling.on('user', (data) => { + if (!data.success || !data.user) return; + const { title, role } = data.user; + + // 更新称号徽章 + if (title !== prevTitle) { + prevTitle = title; + const avatar = document.getElementById('dockUserAvatar'); + if (avatar) { + const name = avatar.dataset.tooltip.split(' - ')[0] || avatar.dataset.tooltip; + avatar.dataset.tooltip = title ? `${name} - ${title}` : name; + } + let badge = document.querySelector('.user-title-badge'); + if (title) { + if (!badge) { badge = document.createElement('span'); badge.className = 'user-title-badge'; if (avatar) avatar.appendChild(badge); } + badge.textContent = title; + } else if (badge) badge.remove(); + } + + // 权限控制 + if (data.permissions && JSON.stringify(data.permissions) !== JSON.stringify(prevPerms)) { + prevPerms = data.permissions; + document.querySelectorAll('.admin-only').forEach(el => el.style.display = prevPerms.can_manage_users ? '' : 'none'); + document.querySelectorAll('.moderator-only').forEach(el => el.style.display = prevPerms.can_manage_posts ? '' : 'none'); + } + }); + + // ========== 帖子数据实时更新 views/likes/replies ========== + window.Polling.on('posts', (data) => { + if (!data.posts || !Array.isArray(data.posts)) return; + + data.posts.forEach(post => { + // 更新帖子卡片上的浏览、点赞、回复数 + const cards = document.querySelectorAll(`.post-card[data-post-id="${post.id}"]`); + cards.forEach(card => { + const statsEls = card.querySelectorAll('.post-stats span'); + if (statsEls.length >= 3) { + // views + const viewsSvg = ''; + // likes + const likesSvg = ''; + // replies + const repliesSvg = ''; + + statsEls[0].innerHTML = viewsSvg + ` ${post.views}`; + statsEls[1].innerHTML = likesSvg + ` ${post.likes}`; + statsEls[2].innerHTML = repliesSvg + ` ${post.reply_count || 0}`; + } + }); + + // 更新个人主页帖子列表的 stats + const myListItems = document.querySelectorAll(`.my-post-item[data-post-id="${post.id}"] .my-post-meta`); + myListItems.forEach(meta => { + const spans = meta.querySelectorAll('span'); + if (spans.length >= 4) { + spans[1].textContent = `👁️ ${post.views} 浏览`; + spans[2].textContent = `❤️ ${post.likes} 点赞`; + spans[3].textContent = `💬 ${post.reply_count || 0} 回复`; + } + }); + }); + }); + + // ========== 统计数字实时更新 ========== + window.Polling.on('stats', (data) => { + if (data.posts !== undefined) animateNumber('statPosts', data.posts); + if (data.replies !== undefined) animateNumber('statReplies', data.replies); + if (data.users !== undefined) animateNumber('statUsers', data.users); + const countAll = document.getElementById('countAll'); + if (countAll) countAll.textContent = data.posts || 0; + + // 更新热门帖子 views(如果有这个区域) + if (data.hot_posts && Array.isArray(data.hot_posts)) { + data.hot_posts.forEach(hp => { + const hotItems = document.querySelectorAll(`.hot-post-item[data-post-id="${hp.id}"]`); + hotItems.forEach(item => { + const viewsEl = item.querySelector('.hot-views'); + if (viewsEl) viewsEl.textContent = hp.views; + }); + }); + } + }); + + // ========== 分类更新 ========== + window.Polling.on('categories', (data) => { + if (!data.categories) return; + const countAll = document.getElementById('countAll'); + if (countAll) countAll.textContent = data.posts || data.categories.length; + }); +}); + +// 数字动画 +function animateNumber(id, target) { + const el = document.getElementById(id); + if (!el) return; + const current = parseInt(el.textContent) || 0; + if (current === target) return; + el.textContent = target; +} diff --git a/website/community/edit-profile.php b/website/community/edit-profile.php new file mode 100644 index 0000000..db2df30 --- /dev/null +++ b/website/community/edit-profile.php @@ -0,0 +1,388 @@ +fetchOne( + "SELECT id, username, email, bio FROM users WHERE id = ?", + [$userId] +); + +$success = $_GET['success'] ?? ''; +$error = $_GET['error'] ?? ''; + +// 处理表单提交 +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $bio = trim($_POST['bio'] ?? ''); + + try { + $db->query( + "UPDATE users SET bio = ? WHERE id = ?", + [$bio, $userId] + ); + header('Location: edit-profile.php?success=1'); + exit; + } catch (Exception $e) { + $error = '保存失败:' . $e->getMessage(); + } +} +?> + + + + + + 编辑资料 - OSS Community + + + + + + + + + + + + +
+
+

编辑个人资料

+

更新您的个人简介和公开信息

+
+ + +
+ ✓ 资料已成功更新 +
+ + + +
+ ✗ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + 取消 + + +
+
+ +
+

预览

+
+
+
+ +
+
+
+
+
+
+
+ 暂无简介' ?> +
+
+
+
+ + + + + + diff --git a/website/community/editor.php b/website/community/editor.php new file mode 100644 index 0000000..0518e4f --- /dev/null +++ b/website/community/editor.php @@ -0,0 +1,156 @@ + 0; +$postData = null; + +if ($isEdit) { + $db = Database::getInstance(); + $post = $db->fetchOne("SELECT * FROM posts WHERE id = ? AND user_id = ?", [$postId, $_SESSION['user_id']]); + if (!$post) { http_response_code(404); die('帖子不存在或无权编辑'); } + $postData = $post; +} + +$db = Database::getInstance(); +$categories = $db->fetchAll("SELECT * FROM categories ORDER BY sort_order ASC"); +$allTags = $db->fetchAll("SELECT * FROM tags ORDER BY name ASC"); +$postTags = []; +if ($postData) { + $postTags = $db->fetchAll("SELECT t.* FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = ?", [$postId]); +} +?> + + + + + + <?php echo $isEdit ? '编辑帖子' : '发布新帖'; ?> - OSS Community + + + + + + + + + + + +
+
+ +
+
+ + + 返回 + +

+
+
+ +
+
+ +
+ + + +
+ +
+ + +
+ +
+
+ Markdown 编辑器 +
+ + + + + + + +
+
+ +
+ + +
+
+
分类
+ +
+ +
+
标签
+ +
+ + + + + + +
+
+ + + +
+
+ +
+
Markdown 语法
+
+
# 标题 一级标题
+
## 标题 二级标题
+
**粗体** 粗体文字
+
*斜体* 斜体文字
+
`代码` 行内代码
+
> 引用 引用块
+
- 列表 无序列表
+
[链接](url) 超链接
+
+
+
+
+
+
+
+ +
+ + +
+
+ + +
+ + + + + diff --git a/website/community/includes/Database.php b/website/community/includes/Database.php new file mode 100644 index 0000000..2c79c24 --- /dev/null +++ b/website/community/includes/Database.php @@ -0,0 +1,47 @@ +pdo = new PDO($dsn, $config['username'], $config['password'], [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + } + + public static function getInstance() { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function query($sql, $params = []) { + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $stmt; + } + + public function fetchAll($sql, $params = []) { + return $this->query($sql, $params)->fetchAll(); + } + + public function fetchOne($sql, $params = []) { + return $this->query($sql, $params)->fetch(); + } + + public function lastInsertId() { + return $this->pdo->lastInsertId(); + } +} diff --git a/website/community/includes/dock.php b/website/community/includes/dock.php new file mode 100644 index 0000000..16acbd3 --- /dev/null +++ b/website/community/includes/dock.php @@ -0,0 +1,75 @@ +fetchOne("SELECT role, title FROM users WHERE id = ?", [$_SESSION['user_id']]); + if ($userData) { + $role = isset($userData['role']) ? ucfirst($userData['role']) : 'Member'; + $userTitle = $userData['title'] ?? ''; + // 更新 session 中的 role + $_SESSION['role'] = $userData['role'] ?? 'member'; + } + } catch (Exception $e) { + // 忽略错误,继续执行 + } +} + +// 构建 tooltip 文本 +$tooltipText = $userTitle ? "{$username} - {$userTitle}" : $username; +?> +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
diff --git a/website/community/includes/post-modal.php b/website/community/includes/post-modal.php new file mode 100644 index 0000000..6ebfca1 --- /dev/null +++ b/website/community/includes/post-modal.php @@ -0,0 +1,478 @@ + +
+
+
+
+

发布新帖

+ +
+ +
+ + +
+ + + 0/200 +
+ +
+ + +
+ +
+ + + 0 个字符 +
+ +
+ + + 例如:Go, 插件, 安装 +
+ +
+
+ +
+ + +
+
+
+
+ + + + diff --git a/website/community/index.php b/website/community/index.php new file mode 100644 index 0000000..18eed3f --- /dev/null +++ b/website/community/index.php @@ -0,0 +1,91 @@ + + + + + + OSS Community - 开发者社区 + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+

社区

+
+
+

分类

+
    +
  • + + 全部 + +
  • +
+
+ +
+

统计

+
+
+ 0 + 帖子 +
+
+ 0 + 回复 +
+
+ 0 + 用户 +
+
+
+
+ + +
+
+

全部帖子

+
+ + + +
+
+ +
+ +
+ +
+
+
+
+ + + + + + + + + diff --git a/website/community/install.sh b/website/community/install.sh new file mode 100644 index 0000000..93af858 --- /dev/null +++ b/website/community/install.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# OSS Community 安装脚本 + +set -e + +echo "========================================" +echo " OSS Community 安装向导" +echo "========================================" +echo "" + +# 1. 检测 PHP +echo -ne "[1/4] 检测 PHP 环境..." +if command -v php &> /dev/null; then + echo -e " \033[0;32m已安装 ($(php -v | head -n1 | awk '{print $2}'))\033[0m" +else + echo -e " \033[1;33m未安装 PHP\033[0m" + echo "请运行: sudo apt install php php-mysql php-pdo php-json" + exit 1 +fi + +# 2. 检测 MySQL +echo -ne "[2/4] 检测 MySQL 环境..." +if command -v mysql &> /dev/null; then + echo -e " \033[0;32m已安装\033[0m" +else + echo -e " \033[1;33m未安装 MySQL\033[0m" + echo "请运行: sudo apt install mysql-server" + exit 1 +fi + +# 3. 数据库配置 +echo "" +echo "请输入 MySQL 配置:" +read -p " 数据库主机 [127.0.0.1]: " DB_HOST +DB_HOST=${DB_HOST:-127.0.0.1} +read -p " 数据库端口 [3306]: " DB_PORT +DB_PORT=${DB_PORT:-3306} +read -p " 数据库用户名 [root]: " DB_USER +DB_USER=${DB_USER:-root} +read -sp " 数据库密码: " DB_PASS +echo "" +read -p " 数据库名 [oss_community]: " DB_NAME +DB_NAME=${DB_NAME:-oss_community} + +# 写入配置 +cat > config.php << EOF + '$DB_HOST', + 'port' => '$DB_PORT', + 'dbname' => '$DB_NAME', + 'username' => '$DB_USER', + 'password' => '$DB_PASS', + 'charset' => 'utf8mb4', +]; +EOF + +# 4. 导入数据库 +echo -ne "[3/4] 导入数据库结构..." +if mysql -u "$DB_USER" -p"$DB_PASS" -h "$DB_HOST" -P "$DB_PORT" < schema.sql 2>/dev/null; then + echo -e " \033[0;32m导入成功\033[0m" +else + echo -e " \033[0;31m导入失败,请检查 MySQL 连接信息\033[0m" + exit 1 +fi + +# 5. 启动 PHP 内置服务器 +echo -ne "[4/4] 启动社区服务器..." +echo -e " \033[0;32m完成\033[0m" + +echo "" +echo "========================================" +echo " 安装完成!" +echo "========================================" +echo "" +echo "访问 http://localhost:8081/community/ 查看社区" +echo "" +echo "启动命令: php -S localhost:8081 -t ../" +echo "" diff --git a/website/community/login.php b/website/community/login.php new file mode 100644 index 0000000..ca10518 --- /dev/null +++ b/website/community/login.php @@ -0,0 +1,110 @@ + + + + + + 登录 - OSS Community + + + + + + + + + + + +
+
+
+
+
+ + + +
+

欢迎回来

+

登录到你的 OSS Community 账户

+
+ +
+
+ +
+ + + + +
+
+ +
+ +
+ + + + + +
+
+ +
+ + 忘记密码? +
+ +
+
+ + +
+ +
+

还没有账户?立即注册

+
+
+ +
+
+
+
+
+
+
+ + + + + diff --git a/website/community/migrate-add-bio.php b/website/community/migrate-add-bio.php new file mode 100644 index 0000000..aad0f7d --- /dev/null +++ b/website/community/migrate-add-bio.php @@ -0,0 +1,29 @@ +fetchAll("SHOW COLUMNS FROM users LIKE 'bio'"); + + if (empty($columns)) { + // 字段不存在,添加 + $db->query("ALTER TABLE users ADD COLUMN bio TEXT AFTER avatar"); + echo "✓ 成功添加 bio 字段\n"; + } else { + echo "✓ bio 字段已存在,无需迁移\n"; + } + + echo "迁移完成!\n"; +} catch (Exception $e) { + echo "✗ 迁移失败:" . $e->getMessage() . "\n"; + exit(1); +} diff --git a/website/community/my-posts.php b/website/community/my-posts.php new file mode 100644 index 0000000..4d79188 --- /dev/null +++ b/website/community/my-posts.php @@ -0,0 +1,498 @@ +fetchOne("SELECT id, username FROM users WHERE id = ?", [$viewUserId]); + +if (!$user) { + header('HTTP/1.0 404 Not Found'); + exit('用户不存在'); +} + +$isCurrentUser = ($currentUserId == $viewUserId); + +// 获取用户文章 +$page = max(1, (int)($_GET['page'] ?? 1)); +$limit = 20; +$offset = ($page - 1) * $limit; + +$posts = $db->fetchAll( + "SELECT p.*, c.name as category_name, c.slug as category_slug, + (SELECT COUNT(*) FROM replies r WHERE r.post_id = p.id) as reply_count + FROM posts p + JOIN categories c ON p.category_id = c.id + WHERE p.user_id = ? + ORDER BY p.is_pinned DESC, p.created_at DESC + LIMIT ? OFFSET ?", + [$viewUserId, $limit, $offset] +); + +$total = $db->fetchOne("SELECT COUNT(*) as count FROM posts WHERE user_id = ?", [$viewUserId])['count']; +$pages = ceil($total / $limit); +?> + + + + + + <?= htmlspecialchars($user['username']) ?> 的文章 - OSS Community + + + + + + + + + + + + + +
+
+

+

+
+ +
+
+
+
文章总数
+
+
+
+
总浏览量
+
+
+
+
总点赞数
+
+
+ +
+ +
+ + + +

还没有发表文章

+

开始创作您的第一篇文章吧!

+ + + + + 发表文章 + +
+ + $post): ?> +
+
+ +
+ + 📌 置顶 + + + ✓ 已解决 + + +
+
+
...
+
+ 📅 + 👁️ 浏览 + ❤️ 点赞 + 💬 回复 +
+
+ + + + + + 编辑 + + + +
+
+ + + 1): ?> +
+ + + + + +
+ + +
+
+ + + + + + diff --git a/website/community/post.php b/website/community/post.php new file mode 100644 index 0000000..e75090d --- /dev/null +++ b/website/community/post.php @@ -0,0 +1,117 @@ +fetchOne( + "SELECT p.*, u.username, u.avatar, u.role, c.name as category_name + FROM posts p JOIN users u ON p.user_id = u.id + JOIN categories c ON p.category_id = c.id WHERE p.id = ?", + [$id] +); + +if (!$post) { header('HTTP/1.0 404 Not Found'); exit('帖子不存在'); } + +$db->query("UPDATE posts SET views = views + 1 WHERE id = ?", [$id]); +$replies = $db->fetchAll( + "SELECT r.*, u.username, u.avatar FROM replies r JOIN users u ON r.user_id = u.id WHERE r.post_id = ? ORDER BY r.created_at ASC", + [$id] +); +?> + + + + + + <?= htmlspecialchars($post['title']) ?> - OSS Community + + + + + + + + + + + + + + + +
+ + + 返回列表 + +
+ +
+
+
+

+
+
+
+
+
·
+
+
+
+
+
+ 👁️ 浏览 + ❤️ 点赞 + 💬 回复 +
+
+ +
+

回复 ()

+ +
暂无回复,抢沙发吧!
+ + +
+
+
+
+
+
+
+
+
+
+ + +
+
+ + + diff --git a/website/community/profile.php b/website/community/profile.php new file mode 100644 index 0000000..65be39d --- /dev/null +++ b/website/community/profile.php @@ -0,0 +1,717 @@ +fetchOne( + "SELECT id, username, email, avatar, role, bio, created_at FROM users WHERE id = ?", + [$viewUserId] +); + +if (!$user) { + header('HTTP/1.0 404 Not Found'); + exit('用户不存在'); +} + +// 获取用户统计数据 +$stats = $db->fetchOne( + "SELECT + (SELECT COUNT(*) FROM posts WHERE user_id = ?) as post_count, + (SELECT COUNT(*) FROM replies WHERE user_id = ?) as reply_count, + (SELECT SUM(views) FROM posts WHERE user_id = ?) as total_views, + (SELECT SUM(likes) FROM posts WHERE user_id = ?) as total_likes", + [$viewUserId, $viewUserId, $viewUserId, $viewUserId] +); + +// 获取用户的文章(最近 10 篇) +$posts = $db->fetchAll( + "SELECT p.*, c.name as category_name, c.slug as category_slug, + (SELECT COUNT(*) FROM replies r WHERE r.post_id = p.id) as reply_count + FROM posts p + JOIN categories c ON p.category_id = c.id + WHERE p.user_id = ? + ORDER BY p.created_at DESC + LIMIT 10", + [$viewUserId] +); + +// 获取用户的最近回复 +$replies = $db->fetchAll( + "SELECT r.*, p.title as post_title, p.id as post_id + FROM replies r + JOIN posts p ON r.post_id = p.id + WHERE r.user_id = ? + ORDER BY r.created_at DESC + LIMIT 5", + [$viewUserId] +); + +$isCurrentUser = ($currentUserId == $viewUserId); +$isAdminOrMod = in_array($_SESSION['role'] ?? '', ['admin', 'moderator']); +?> + + + + + + <?= htmlspecialchars($user['username']) ?> - OSS Community + + + + + + + + + + + + +
+ +
+
+
+
+
+

+ + + + + + +
+ +

+ +
+ + + 注册于 + + + + + + + +
+ +
+ + + 编辑资料 + +
+ +
+
+
+ + +
+
+
+
文章
+
+
+
+
回复
+
+
+
+
浏览
+
+
+
+
点赞
+
+
+ + +
+ +
+
+

+ + 文章 +

+ = 10): ?> + + 查看全部 + + + +
+ +
+ +

还没有发表文章

+
+ +
+ +
+
+ + +
+
...
+
+ 📅 + 👁️ + ❤️ + 💬 +
+
+ +
+ +
+ + +
+
+

+ + 回复 +

+
+ +
+ +

还没有回复

+
+ +
+ +
+ Re: +
...
+
+
+ +
+ +
+
+
+ + + + + + diff --git a/website/community/register.php b/website/community/register.php new file mode 100644 index 0000000..daac221 --- /dev/null +++ b/website/community/register.php @@ -0,0 +1,142 @@ + + + + + + 注册 - OSS Community + + + + + + + + + + + +
+
+
+
+
+ + + +
+

创建账户

+

加入 OSS Community 开发者社区

+
+ +
+
+ +
+ + + + +
+ 只能包含字母、数字和下划线 +
+ +
+ +
+ + + + +
+
+ +
+ +
+ + + + + +
+ 至少 6 个字符 +
+ +
+ +
+ + + + +
+
+ +
+
+ + +
+ +
+

已有账户?立即登录

+
+
+ +
+
+
+
+
+
+
+ + + + + diff --git a/website/community/schema.sql b/website/community/schema.sql new file mode 100644 index 0000000..c2e5fab --- /dev/null +++ b/website/community/schema.sql @@ -0,0 +1,104 @@ +-- OSS Community 数据库结构 +CREATE DATABASE IF NOT EXISTS oss_community CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE oss_community; + +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + avatar VARCHAR(255) DEFAULT '', + bio TEXT, + role ENUM('admin', 'moderator', 'member') DEFAULT 'member', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 分类表 +CREATE TABLE IF NOT EXISTS categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + slug VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + icon VARCHAR(50) DEFAULT 'folder', + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 帖子表 +CREATE TABLE IF NOT EXISTS posts ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + category_id INT NOT NULL, + title VARCHAR(200) NOT NULL, + slug VARCHAR(200) NOT NULL UNIQUE, + content TEXT NOT NULL, + views INT DEFAULT 0, + likes INT DEFAULT 0, + is_pinned TINYINT(1) DEFAULT 0, + is_locked TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE, + FULLTEXT(title, content) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 回复表 +CREATE TABLE IF NOT EXISTS replies ( + id INT AUTO_INCREMENT PRIMARY KEY, + post_id INT NOT NULL, + user_id INT NOT NULL, + content TEXT NOT NULL, + likes INT DEFAULT 0, + is_solution TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 点赞表 +CREATE TABLE IF NOT EXISTS likes ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + post_id INT DEFAULT NULL, + reply_id INT DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_like (user_id, post_id, reply_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE SET NULL, + FOREIGN KEY (reply_id) REFERENCES replies(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 标签表 +CREATE TABLE IF NOT EXISTS tags ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE, + slug VARCHAR(50) NOT NULL UNIQUE, + color VARCHAR(7) DEFAULT '#06b6d4' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 帖子标签关联表 +CREATE TABLE IF NOT EXISTS post_tags ( + post_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (post_id, tag_id), + FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 初始数据 +INSERT IGNORE INTO categories (name, slug, description, icon, sort_order) VALUES +('公告', 'announcements', '官方公告和重要通知', 'megaphone', 1), +('问答', 'q-a', '提问与解答,互助交流', 'question', 2), +('讨论', 'discussions', '技术讨论与想法分享', 'chat', 3), +('插件市场', 'plugins', '插件展示与推荐', 'puzzle', 4), +('反馈', 'feedback', 'Bug 反馈与功能建议', 'bug', 5); + +INSERT IGNORE INTO tags (name, slug, color) VALUES +('Go', 'go', '#00add8'), +('插件', 'plugin', '#8b5cf6'), +('安装', 'install', '#22c55e'), +('配置', 'config', '#3b82f6'), +('求助', 'help', '#f59e0b'); diff --git a/website/community/seed-announcements.sql b/website/community/seed-announcements.sql new file mode 100644 index 0000000..605f895 --- /dev/null +++ b/website/community/seed-announcements.sql @@ -0,0 +1,15 @@ +-- 插入公告数据 +INSERT IGNORE INTO posts (user_id, category_id, title, slug, content, is_pinned) VALUES +(1, + (SELECT id FROM categories WHERE slug = 'announcements' LIMIT 1), + '欢迎使用 OSS Community', + 'welcome-to-oss-community', + '# 欢迎使用 OSS 开发者社区!\n\n这是我们社区的第一篇公告。\n\n## 社区规则\n\n- 尊重他人,文明交流\n- 禁止发布违法不良信息\n- 鼓励分享技术经验\n- 提问前请先搜索已有帖子\n\n## 功能介绍\n\n- 📝 **发帖** - 分享你的技术经验\n- 💬 **回复** - 参与讨论,帮助他人\n- ❤️ **点赞** - 为优质内容点赞\n- 🏷️ **标签** - 使用标签分类帖子\n- 🔍 **搜索** - 快速找到感兴趣的内容\n\n## 快速开始\n\n1. 注册并登录你的账户\n2. 完善个人资料和简介\n3. 发表第一篇帖子\n4. 参与社区讨论\n\n> 如果你有任何建议或反馈,欢迎在反馈区发帖!\n\n祝你在社区玩得愉快! 🎉', + 1), + +(1, + (SELECT id FROM categories WHERE slug = 'announcements' LIMIT 1), + '社区功能更新日志', + 'community-changelog-v1', + '# 社区功能更新\n\n## v1.1.0 - 2026-04-04\n\n### 新增\n- ✨ 用户个人主页\n- ✨ 编辑个人资料(支持 Bio)\n- ✨ 我的文章页面\n- ✨ 文章统计(浏览、点赞、回复)\n\n### 优化\n- 🚀 响应式布局优化\n- 📱 移动端适配\n- 🎨 UI 样式改进\n\n### 修复\n- 🐛 数据库连接问题\n- 🐛 用户菜单交互问题\n\n---\n\n感谢大家的支持!', + 1); diff --git a/website/css/architecture.css b/website/css/architecture.css new file mode 100644 index 0000000..ca13478 --- /dev/null +++ b/website/css/architecture.css @@ -0,0 +1,110 @@ +/* ===== 架构图 ===== */ +#architecture { + padding: 120px 0; + position: relative; + z-index: 1; +} + +.arch-diagram { + max-width: 900px; + margin: 0 auto; +} + +.arch-layer { + border-radius: 16px; + padding: 24px; + border: 1px solid var(--border); +} + +.arch-core { + background: rgba(6, 182, 212, 0.05); + border-color: rgba(6, 182, 212, 0.2); +} + +.arch-label { + text-align: center; + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + color: var(--cyan-light); +} + +.arch-modules { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; +} + +.arch-module { + padding: 10px 20px; + background: rgba(6, 182, 212, 0.1); + border: 1px solid rgba(6, 182, 212, 0.2); + border-radius: 8px; + font-size: 14px; + font-weight: 500; +} + +.arch-connector { + height: 40px; + width: 2px; + background: linear-gradient(to bottom, var(--cyan), var(--blue)); + margin: 0 auto; + position: relative; +} + +.arch-connector::before, +.arch-connector::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--cyan); +} + +.arch-connector::before { top: -6px; } +.arch-connector::after { bottom: -6px; } + +.arch-plugins { + background: var(--bg-glass); +} + +.arch-plugin-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} + +.arch-plugin { + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + text-align: center; + transition: all 0.3s; +} + +.arch-plugin:hover { + border-color: var(--border-hover); + transform: translateY(-4px); +} + +.arch-plugin-title { + font-weight: 700; + font-size: 15px; + margin-bottom: 12px; + color: var(--cyan-light); +} + +.arch-plugin-items { + font-size: 13px; + color: var(--text-muted); + margin-bottom: 4px; +} + +@media (max-width: 768px) { + .arch-plugin-grid { grid-template-columns: repeat(2, 1fr); } +} diff --git a/website/css/code-examples.css b/website/css/code-examples.css new file mode 100644 index 0000000..1d2b4a7 --- /dev/null +++ b/website/css/code-examples.css @@ -0,0 +1,63 @@ +/* ===== 代码示例 ===== */ +#examples { + padding: 120px 0; + background: var(--bg-secondary); + position: relative; + z-index: 1; +} + +.examples-grid { + max-width: 1000px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; +} + +.example-card:first-child { + grid-column: span 2; +} + +.example-card { + background: rgba(10, 15, 30, 0.9); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + overflow: hidden; +} + +.example-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.example-title { + font-size: 14px; + font-weight: 600; + color: var(--text-secondary); +} + +.example-lang { + padding: 4px 10px; + border-radius: 4px; + background: rgba(6, 182, 212, 0.1); + color: var(--cyan); + font-size: 12px; + font-weight: 600; +} + +.example-code { + padding: 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + line-height: 1.8; + color: var(--text-secondary); + overflow-x: auto; +} + +@media (max-width: 768px) { + .examples-grid { grid-template-columns: 1fr; } + .example-card:first-child { grid-column: span 1; } +} diff --git a/website/css/community.css b/website/css/community.css new file mode 100644 index 0000000..a943d75 --- /dev/null +++ b/website/css/community.css @@ -0,0 +1,46 @@ +/* ===== 社区区域 ===== */ +#community { + padding: 120px 0; + background: var(--bg-secondary); + position: relative; + z-index: 1; +} + +.community-content { + max-width: 900px; + margin: 0 auto; + text-align: center; +} + +.community-links { + display: flex; + justify-content: center; + gap: 24px; + flex-wrap: wrap; +} + +.community-card { + display: flex; + align-items: center; + gap: 12px; + padding: 20px 32px; + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 12px; + color: var(--text); + text-decoration: none; + font-weight: 600; + transition: all 0.3s; +} + +.community-card:hover { + border-color: var(--border-hover); + transform: translateY(-4px); + box-shadow: 0 12px 30px rgba(6, 182, 212, 0.1); +} + +.community-card svg { + width: 24px; + height: 24px; + color: var(--cyan); +} diff --git a/website/css/dock.css b/website/css/dock.css new file mode 100644 index 0000000..d35161f --- /dev/null +++ b/website/css/dock.css @@ -0,0 +1,156 @@ +/* ===== Dock 侧边栏 ===== */ +#dock { + position: fixed; + right: 20px; + top: 50%; + transform: translateY(-50%); + z-index: 1000; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px 8px; + background: rgba(3, 7, 18, 0.7); + backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: 16px; +} + +.dock-item { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; + color: var(--text-secondary); + text-decoration: none; + transition: all 0.25s ease; + position: relative; +} + +.dock-item:hover, .dock-item.active { + color: var(--cyan-light); + background: rgba(6, 182, 212, 0.15); + transform: scale(1.15); +} + +.dock-item svg { + width: 18px; + height: 18px; +} + +.dock-separator { + width: 20px; + height: 1px; + background: var(--border); + margin: 4px 0; +} + +/* 用户头像样式 */ +.dock-user-avatar { + position: relative; + cursor: pointer; + flex-direction: column; + gap: 4px; +} + +.dock-user-avatar svg { + color: var(--cyan); +} + +.dock-user-avatar:hover { + color: var(--cyan-light); + background: rgba(6, 182, 212, 0.15); + transform: scale(1.15); +} + +/* 称号徽章 */ +.user-title-badge { + position: absolute; + bottom: -18px; + left: 50%; + transform: translateX(-50%); + font-size: 9px; + font-weight: 600; + padding: 1px 4px; + border-radius: 4px; + background: linear-gradient(135deg, #f59e0b, #f97316); + color: #fff; + white-space: nowrap; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + box-shadow: 0 2px 4px rgba(245, 158, 11, 0.3); + pointer-events: none; +} + +/* 用户菜单按钮样式 */ +.dock-user-menu-btn { + position: relative; + cursor: pointer; +} + +.dock-user-menu-btn svg { + color: var(--text-secondary); +} + +.dock-user-menu-btn:hover { + color: var(--cyan-light); + background: rgba(6, 182, 212, 0.15); + transform: scale(1.15); +} + +/* Tooltip */ +.dock-item::before { + content: attr(data-tooltip); + position: absolute; + right: calc(100% + 10px); + top: 50%; + transform: translateY(-50%) scale(0.9); + padding: 4px 8px; + border-radius: 4px; + background: #1f2937; + border: 1px solid var(--border); + color: #fff; + font-size: 11px; + font-weight: 500; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: all 0.2s ease; +} + +.dock-item:hover::before { + opacity: 1; + transform: translateY(-50%) scale(1); +} + +/* 响应式 */ +@media (max-width: 768px) { + #dock { + right: auto; + left: 50%; + top: auto; + bottom: 12px; + transform: translateX(-50%); + flex-direction: row; + gap: 4px; + padding: 8px 12px; + } + .dock-separator { + width: 1px; + height: 20px; + margin: 0 4px; + } + .dock-item::before { + right: auto; + top: auto; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%) scale(0.9); + } + .dock-item:hover::before { + transform: translateX(-50%) scale(1); + } +} diff --git a/website/css/features.css b/website/css/features.css new file mode 100644 index 0000000..d39f41a --- /dev/null +++ b/website/css/features.css @@ -0,0 +1,98 @@ +/* ===== 特性区域 ===== */ +#features { + padding: 120px 0; + position: relative; + z-index: 1; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.feature-card { + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 20px; + padding: 32px; + transition: all 0.4s ease; + position: relative; + overflow: hidden; +} + +.feature-card::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(6, 182, 212, 0.05), transparent); + opacity: 0; + transition: opacity 0.4s; +} + +.feature-card:hover { + border-color: var(--border-hover); + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(6, 182, 212, 0.1); +} + +.feature-card:hover::before { opacity: 1; } + +.feature-icon { + width: 56px; + height: 56px; + border-radius: 14px; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + transition: transform 0.3s; +} + +.feature-card:hover .feature-icon { + transform: scale(1.1); +} + +.feature-icon svg { + width: 28px; + height: 28px; + color: #fff; +} + +.feature-card h3 { + font-size: 20px; + font-weight: 700; + margin-bottom: 12px; +} + +.feature-card p { + font-size: 15px; + color: var(--text-secondary); + line-height: 1.7; + margin-bottom: 20px; +} + +.feature-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.feature-tags span { + padding: 4px 12px; + border-radius: 6px; + background: rgba(6, 182, 212, 0.08); + border: 1px solid rgba(6, 182, 212, 0.15); + color: var(--cyan-light); + font-size: 12px; + font-weight: 500; +} + +@media (max-width: 1024px) { + .features-grid { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 640px) { + .features-grid { grid-template-columns: 1fr; } +} diff --git a/website/css/footer.css b/website/css/footer.css new file mode 100644 index 0000000..973e25d --- /dev/null +++ b/website/css/footer.css @@ -0,0 +1,79 @@ +/* ===== Footer ===== */ +#footer { + padding: 80px 0 40px; + border-top: 1px solid var(--border); + position: relative; + z-index: 1; +} + +.footer-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + display: grid; + grid-template-columns: 1fr 2fr; + gap: 64px; + margin-bottom: 64px; +} + +.footer-brand p { + color: var(--text-muted); + font-size: 14px; + margin-top: 12px; +} + +.footer-logo { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + font-size: 18px; +} + +.footer-links { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 32px; +} + +.footer-col h4 { + font-size: 14px; + font-weight: 600; + color: var(--text); + margin-bottom: 16px; +} + +.footer-col a { + display: block; + color: var(--text-muted); + text-decoration: none; + font-size: 14px; + margin-bottom: 10px; + transition: color 0.2s; +} + +.footer-col a:hover { color: var(--cyan); } + +.footer-bottom { + text-align: center; + padding-top: 32px; + border-top: 1px solid var(--border); + max-width: 1200px; + margin: 0 auto; + padding-left: 24px; + padding-right: 24px; +} + +.footer-bottom p { + color: var(--text-muted); + font-size: 13px; +} + +@media (max-width: 768px) { + .footer-container { grid-template-columns: 1fr; gap: 40px; } + .footer-links { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 480px) { + .footer-links { grid-template-columns: 1fr; } +} diff --git a/website/css/hero.css b/website/css/hero.css new file mode 100644 index 0000000..dea99a8 --- /dev/null +++ b/website/css/hero.css @@ -0,0 +1,259 @@ +/* ===== Hero 区域 ===== */ +#hero { + min-height: 100vh; + display: flex; + align-items: center; + position: relative; + z-index: 1; + padding-top: 64px; +} + +.hero-container { + max-width: 1400px; + margin: 0 auto; + padding: 60px 24px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 80px; + align-items: center; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 20px; + border-radius: 999px; + background: rgba(6, 182, 212, 0.1); + border: 1px solid rgba(6, 182, 212, 0.2); + margin-bottom: 32px; + font-size: 14px; + color: rgba(34, 211, 238, 0.9); +} + +.badge-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--cyan); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.hero-title { + font-size: clamp(48px, 8vw, 80px); + font-weight: 900; + line-height: 0.95; + margin-bottom: 24px; +} + +.title-line { display: block; } + +.hero-subtitle { + font-size: clamp(20px, 3vw, 28px); + color: var(--text-secondary); + font-weight: 300; + margin-bottom: 12px; +} + +.hero-desc { + font-size: 16px; + color: var(--text-muted); + max-width: 520px; + margin-bottom: 48px; + line-height: 1.8; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 64px; +} + +.hero-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.stat { text-align: left; } + +.stat-num { + display: block; + font-size: 28px; + font-weight: 800; + color: #fff; + margin-bottom: 4px; +} + +.stat-num .stat-suffix { + font-size: 20px; + color: var(--text-secondary); +} + +.stat-label { + font-size: 13px; + color: var(--text-muted); +} + +/* 右侧代码区域 */ +.hero-visual { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.orbit { + position: absolute; + border-radius: 50%; + border: 1px solid rgba(6, 182, 212, 0.1); +} + +.orbit-1 { + inset: 15%; + animation: spin 40s linear infinite; +} + +.orbit-2 { + inset: 25%; + animation: spin 30s linear infinite reverse; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.orbit-dot { + position: absolute; + top: -6px; + left: 50%; + transform: translateX(-50%); + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--cyan); + box-shadow: 0 0 20px rgba(6, 182, 212, 0.5); +} + +.orbit-dot-lg { + width: 16px; + height: 16px; + background: var(--blue); + box-shadow: 0 0 24px rgba(59, 130, 246, 0.5); +} + +.code-window { + background: rgba(10, 15, 30, 0.9); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + padding: 0; + width: 100%; + max-width: 480px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +.code-header { + display: flex; + align-items: center; + gap: 8px; + padding: 16px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.code-dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.code-dot-red { background: #ef4444; } +.code-dot-yellow { background: #eab308; } +.code-dot-green { background: #22c55e; } + +.code-title { + margin-left: 12px; + font-size: 13px; + color: var(--text-muted); + font-family: 'JetBrains Mono', monospace; +} + +.code-body { + padding: 20px 24px; + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + line-height: 1.8; + color: var(--text-secondary); + overflow-x: auto; +} + +/* 滚动指示器 */ +.scroll-indicator { + position: absolute; + bottom: 40px; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: var(--text-muted); + font-size: 13px; + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateX(-50%) translateY(0); } + 50% { transform: translateX(-50%) translateY(8px); } +} + +.scroll-mouse { + width: 24px; + height: 36px; + border: 2px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + position: relative; +} + +.scroll-mouse::after { + content: ''; + position: absolute; + top: 6px; + left: 50%; + transform: translateX(-50%); + width: 4px; + height: 8px; + background: var(--cyan); + border-radius: 2px; + animation: scroll-dot 2s ease-in-out infinite; +} + +@keyframes scroll-dot { + 0%, 100% { opacity: 1; top: 6px; } + 50% { opacity: 0.3; top: 16px; } +} + +/* 响应式 */ +@media (max-width: 1024px) { + .hero-container { + grid-template-columns: 1fr; + gap: 48px; + text-align: center; + } + .hero-desc, .hero-actions, .hero-stats { + margin-left: auto; + margin-right: auto; + } + .hero-stats { max-width: 400px; } + .stat { text-align: center; } +} + +@media (max-width: 640px) { + .hero-stats { grid-template-columns: 1fr; gap: 16px; } +} diff --git a/website/css/main.css b/website/css/main.css new file mode 100644 index 0000000..d33eb5d --- /dev/null +++ b/website/css/main.css @@ -0,0 +1,160 @@ +/* ===== 全局样式 ===== */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg: #030712; + --bg-secondary: #020510; + --bg-glass: rgba(255, 255, 255, 0.02); + --border: rgba(255, 255, 255, 0.05); + --border-hover: rgba(6, 182, 212, 0.3); + --cyan: #06b6d4; + --cyan-light: #22d3ee; + --blue: #3b82f6; + --purple: #8b5cf6; + --text: #fff; + --text-secondary: #9ca3af; + --text-muted: #6b7280; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 80px; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.6; + overflow-x: hidden; +} + +/* 粒子画布 */ +#particles { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; +} + +/* 通用 */ +.section-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; +} + +.section-header { + text-align: center; + margin-bottom: 64px; +} + +.section-badge { + display: inline-block; + padding: 6px 16px; + border-radius: 999px; + background: rgba(6, 182, 212, 0.1); + border: 1px solid rgba(6, 182, 212, 0.2); + color: var(--cyan-light); + font-size: 14px; + font-weight: 500; + margin-bottom: 20px; +} + +.section-title { + font-size: clamp(32px, 5vw, 48px); + font-weight: 800; + margin-bottom: 16px; + line-height: 1.2; +} + +.section-desc { + font-size: 18px; + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto; +} + +.gradient-text { + background: linear-gradient(135deg, var(--cyan), var(--blue)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.glass { + background: var(--bg-glass); + backdrop-filter: blur(12px); + border: 1px solid var(--border); + border-radius: 16px; +} + +/* 按钮 */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 28px; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + cursor: pointer; + border: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--cyan), var(--blue)); + color: #fff; +} + +.btn-primary:hover { + box-shadow: 0 8px 30px rgba(6, 182, 212, 0.3); + transform: translateY(-2px); +} + +.btn-outline { + background: transparent; + color: var(--text); + border: 1px solid rgba(6, 182, 212, 0.3); +} + +.btn-outline:hover { + background: rgba(6, 182, 212, 0.1); + transform: translateY(-2px); +} + +.btn-lg { + padding: 18px 36px; + font-size: 18px; +} + +.btn-arrow { + width: 20px; + height: 20px; + transition: transform 0.3s ease; +} + +.btn:hover .btn-arrow { + transform: translateX(4px); +} + +/* 代码样式 */ +.code-comment { color: #6b7280; font-style: italic; } +.code-str { color: #34d399; } +.code-keyword { color: #c084fc; } +.code-func { color: #60a5fa; } +.code-bool { color: #f472b6; } +.code-type { color: #fbbf24; } +.code-cmd { color: #9ca3af; } + +/* 响应式 */ +@media (max-width: 768px) { + .section-container { padding: 0 16px; } + .section-header { margin-bottom: 40px; } +} diff --git a/website/css/page.css b/website/css/page.css new file mode 100644 index 0000000..aae5af5 --- /dev/null +++ b/website/css/page.css @@ -0,0 +1,396 @@ +/* ===== 通用页面样式 ===== */ +.page-content { + max-width: 1100px; + margin: 0 auto; + padding: 100px 80px 80px 24px; + min-height: 100vh; +} + +.page-header { + text-align: center; + margin-bottom: 64px; +} + +.page-title { + font-size: clamp(32px, 5vw, 48px); + font-weight: 800; + margin-bottom: 16px; + line-height: 1.2; +} + +.page-desc { + font-size: 18px; + color: var(--text-secondary); + max-width: 600px; + margin: 0 auto; +} + +/* 特性网格 */ +.features-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.feature-card { + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 20px; + padding: 32px; + transition: all 0.4s ease; +} + +.feature-card:hover { + border-color: var(--border-hover); + transform: translateY(-8px); + box-shadow: 0 20px 40px rgba(6, 182, 212, 0.1); +} + +.feature-icon { + width: 56px; + height: 56px; + border-radius: 14px; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + transition: transform 0.3s; +} + +.feature-card:hover .feature-icon { transform: scale(1.1); } + +.feature-icon svg { width: 28px; height: 28px; color: #fff; } + +.feature-card h3 { font-size: 20px; font-weight: 700; margin-bottom: 12px; } + +.feature-card p { + font-size: 15px; + color: var(--text-secondary); + line-height: 1.7; + margin-bottom: 20px; +} + +.feature-tags { display: flex; flex-wrap: wrap; gap: 8px; } + +.feature-tags span { + padding: 4px 12px; + border-radius: 6px; + background: rgba(6, 182, 212, 0.08); + border: 1px solid rgba(6, 182, 212, 0.15); + color: var(--cyan-light); + font-size: 12px; + font-weight: 500; +} + +/* 插件页面 */ +.pkg-format { text-align: center; margin-bottom: 48px; } + +.pkg-card { + font-family: 'JetBrains Mono', monospace; + font-size: clamp(20px, 4vw, 36px); + font-weight: 700; + padding: 32px; + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 16px; + display: inline-block; + margin-bottom: 32px; +} + +.pkg-symbol { color: var(--text-muted); } +.pkg-highlight { color: var(--cyan); } +.pkg-version { color: var(--blue); } + +.pkg-explain { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + text-align: center; +} + +.explain-symbol { + font-family: 'JetBrains Mono', monospace; + font-size: 18px; + font-weight: 600; + color: var(--cyan-light); + margin-bottom: 8px; +} + +.explain-text { font-size: 14px; color: var(--text-muted); } + +/* 终端演示 */ +.install-demo { + max-width: 700px; + margin: 0 auto 48px; + background: rgba(10, 15, 30, 0.9); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + overflow: hidden; +} + +.demo-header { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.demo-dot { width: 12px; height: 12px; border-radius: 50%; } +.demo-dot-red { background: #ef4444; } +.demo-dot-yellow { background: #eab308; } +.demo-dot-green { background: #22c55e; } + +.demo-title { margin-left: 12px; font-size: 13px; color: var(--text-muted); } + +.demo-body { + padding: 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 14px; +} + +.cmd-line { display: flex; gap: 12px; margin-bottom: 8px; color: var(--text-secondary); } +.prompt { color: var(--cyan); font-weight: 600; } +.cmd-text { color: #e5e7eb; } +.cmd-output { color: #9ca3af; font-size: 13px; margin-bottom: 4px; } +.cmd-output.success { color: #34d399; } +.cmd-output.indent { padding-left: 24px; } + +/* 命令表格 */ +.pkg-table { max-width: 800px; margin: 0 auto; } +.pkg-table-title { font-size: 24px; font-weight: 700; margin-bottom: 20px; text-align: center; } + +.pkg-table table { + width: 100%; + border-collapse: collapse; +} + +.pkg-table th, .pkg-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border); + font-size: 14px; +} + +.pkg-table th { + color: var(--text-muted); + font-weight: 600; + font-size: 13px; +} + +.pkg-table td code { + font-family: 'JetBrains Mono', monospace; + background: rgba(6, 182, 212, 0.1); + padding: 2px 8px; + border-radius: 4px; + font-size: 13px; + color: var(--cyan-light); +} + +/* 架构图 */ +.arch-diagram { max-width: 900px; margin: 0 auto 64px; } + +.arch-layer { + border-radius: 16px; + padding: 24px; + border: 1px solid var(--border); +} + +.arch-core { + background: rgba(6, 182, 212, 0.05); + border-color: rgba(6, 182, 212, 0.2); +} + +.arch-label { + text-align: center; + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + color: var(--cyan-light); +} + +.arch-modules { + display: flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; +} + +.arch-module { + padding: 10px 20px; + background: rgba(6, 182, 212, 0.1); + border: 1px solid rgba(6, 182, 212, 0.2); + border-radius: 8px; + font-size: 14px; + font-weight: 500; +} + +.arch-connector { + height: 40px; + width: 2px; + background: linear-gradient(to bottom, var(--cyan), var(--blue)); + margin: 0 auto; + position: relative; +} + +.arch-connector::before, +.arch-connector::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--cyan); +} + +.arch-connector::before { top: -6px; } +.arch-connector::after { bottom: -6px; } + +.arch-plugins { background: var(--bg-glass); } + +.arch-plugin-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; +} + +.arch-plugin { + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + text-align: center; + transition: all 0.3s; +} + +.arch-plugin:hover { + border-color: var(--border-hover); + transform: translateY(-4px); +} + +.arch-plugin-title { + font-weight: 700; + font-size: 15px; + margin-bottom: 12px; + color: var(--cyan-light); +} + +.arch-plugin-items { + font-size: 13px; + color: var(--text-muted); + margin-bottom: 4px; +} + +/* 数据流 */ +.arch-flow { max-width: 900px; margin: 0 auto; } +.arch-flow-title { text-align: center; font-size: 28px; font-weight: 800; margin-bottom: 32px; } + +.arch-flow-steps { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 12px; +} + +.flow-step { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 12px; + font-size: 14px; + font-weight: 500; +} + +.flow-step-num { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + font-size: 13px; + font-weight: 700; + flex-shrink: 0; +} + +.flow-arrow { color: var(--cyan); font-size: 20px; font-weight: 700; } + +/* 快速开始 */ +.steps-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 64px; +} + +.step-card { + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 16px; + padding: 32px; + text-align: center; + transition: all 0.3s; +} + +.step-card:hover { + border-color: var(--border-hover); + transform: translateY(-4px); +} + +.step-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + font-size: 20px; + font-weight: 800; + margin-bottom: 20px; +} + +.step-title { font-size: 20px; font-weight: 700; margin-bottom: 16px; } + +.step-code { + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + line-height: 1.8; + color: var(--text-secondary); + text-align: left; + background: rgba(10, 15, 30, 0.6); + padding: 16px; + border-radius: 8px; +} + +.quickstart-links { + display: flex; + justify-content: center; + gap: 16px; + flex-wrap: wrap; +} + +/* 响应式 */ +@media (max-width: 1024px) { + .features-grid { grid-template-columns: repeat(2, 1fr); } + .arch-plugin-grid { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 768px) { + .page-content { padding-left: 24px; } + .features-grid { grid-template-columns: 1fr; } + .arch-plugin-grid { grid-template-columns: 1fr; } + .steps-grid { grid-template-columns: 1fr; } + .arch-flow-steps { flex-direction: column; } + .flow-arrow { transform: rotate(90deg); } + .pkg-explain { grid-template-columns: 1fr; } + .pkg-table { overflow-x: auto; } +} diff --git a/website/css/plugins.css b/website/css/plugins.css new file mode 100644 index 0000000..dd7f06f --- /dev/null +++ b/website/css/plugins.css @@ -0,0 +1,110 @@ +/* ===== 插件生态 ===== */ +#plugins { + padding: 120px 0; + background: var(--bg-secondary); + position: relative; + z-index: 1; +} + +.plugins-content { + max-width: 900px; + margin: 0 auto; +} + +.pkg-format { text-align: center; margin-bottom: 48px; } + +.pkg-card { + font-family: 'JetBrains Mono', monospace; + font-size: clamp(20px, 4vw, 36px); + font-weight: 700; + padding: 32px; + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 16px; + display: inline-block; + margin-bottom: 32px; +} + +.pkg-symbol { color: var(--text-muted); } +.pkg-highlight { color: var(--cyan); } +.pkg-version { color: var(--blue); } + +.pkg-explain { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + text-align: center; +} + +.explain-symbol { + font-family: 'JetBrains Mono', monospace; + font-size: 18px; + font-weight: 600; + color: var(--cyan-light); + margin-bottom: 8px; +} + +.explain-text { + font-size: 14px; + color: var(--text-muted); +} + +.install-demo { + background: rgba(10, 15, 30, 0.9); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + overflow: hidden; +} + +.demo-header { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.demo-dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.demo-dot-red { background: #ef4444; } +.demo-dot-yellow { background: #eab308; } +.demo-dot-green { background: #22c55e; } + +.demo-title { + margin-left: 12px; + font-size: 13px; + color: var(--text-muted); +} + +.demo-body { + padding: 20px; + font-family: 'JetBrains Mono', monospace; + font-size: 14px; +} + +.cmd-line { + display: flex; + gap: 12px; + margin-bottom: 8px; + color: var(--text-secondary); +} + +.prompt { color: var(--cyan); font-weight: 600; } +.cmd-text { color: #e5e7eb; } + +.cmd-output { + color: #9ca3af; + font-size: 13px; + margin-bottom: 4px; +} + +.cmd-output.success { color: #34d399; } +.cmd-output.indent { padding-left: 24px; } + +@media (max-width: 640px) { + .pkg-explain { grid-template-columns: 1fr; } +} diff --git a/website/css/quickstart.css b/website/css/quickstart.css new file mode 100644 index 0000000..f0eed4e --- /dev/null +++ b/website/css/quickstart.css @@ -0,0 +1,65 @@ +/* ===== 快速开始 ===== */ +#quickstart { + padding: 120px 0; + position: relative; + z-index: 1; +} + +.steps-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; + margin-bottom: 64px; +} + +.step-card { + background: var(--bg-glass); + border: 1px solid var(--border); + border-radius: 16px; + padding: 32px; + text-align: center; + transition: all 0.3s; +} + +.step-card:hover { + border-color: var(--border-hover); + transform: translateY(-4px); +} + +.step-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + font-size: 20px; + font-weight: 800; + margin-bottom: 20px; +} + +.step-title { + font-size: 20px; + font-weight: 700; + margin-bottom: 16px; +} + +.step-code { + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + line-height: 1.8; + color: var(--text-secondary); + text-align: left; + background: rgba(10, 15, 30, 0.6); + padding: 16px; + border-radius: 8px; +} + +.quickstart-cta { + text-align: center; +} + +@media (max-width: 768px) { + .steps-grid { grid-template-columns: 1fr; } +} diff --git a/website/css/stats.css b/website/css/stats.css new file mode 100644 index 0000000..7ee4b8e --- /dev/null +++ b/website/css/stats.css @@ -0,0 +1,52 @@ +/* ===== 统计区域 ===== */ +#stats { + padding: 80px 0; + position: relative; + z-index: 1; + background: linear-gradient(135deg, rgba(6, 182, 212, 0.05), rgba(59, 130, 246, 0.05)); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 32px; + max-width: 1000px; + margin: 0 auto; + text-align: center; +} + +.stat-card { + padding: 24px; +} + +.stat-number { + font-size: 48px; + font-weight: 900; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-suffix { + font-size: 32px; + font-weight: 800; + background: linear-gradient(135deg, var(--cyan), var(--blue)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-label { + display: block; + margin-top: 8px; + font-size: 16px; + color: var(--text-muted); + font-weight: 500; +} + +@media (max-width: 768px) { + .stats-grid { grid-template-columns: repeat(2, 1fr); gap: 24px; } +} diff --git a/website/features.html b/website/features.html new file mode 100644 index 0000000..b090d70 --- /dev/null +++ b/website/features.html @@ -0,0 +1,91 @@ + + + + + + 核心特性 - Future OSS + + + + + + + + + + + + + + + + + +
+ +
+
+ 核心能力 +

为何选择 Future OSS

+

每个模块都为生产环境而设计,为企业提供开箱即用的稳定性保障

+
+ +
+
+
+ +
+

一切皆插件

+

协议适配、中间件、通知渠道、数据库驱动……所有功能均以插件形式加载。框架本身是空壳,只提供插件管理、事件总线和配置系统。

+
热插拔按需加载隔离运行
+
+
+
+ +
+

依赖自动解析

+

自动识别多级上游依赖,拓扑排序决定加载顺序,循环依赖检测。插件只需声明依赖关系,运行时自动注入。

+
拓扑排序循环检测自动注入
+
+
+
+ +
+

熔断与降级

+

内置熔断器,失败 N 次后自动熔断。支持 6 种降级策略:静态值、缓存、默认值、上游、空、自定义函数。

+
自动熔断6 种降级自动恢复
+
+
+
+ +
+

包管理系统

+

类似 npm 的包管理体验。@{作者/插件名}<版本> 格式,一键安装、卸载、更新、同步。从 Gitee 仓库自动拉取。

+
一键安装版本管理自动同步
+
+
+
+ +
+

事件驱动架构

+

发布/订阅事件总线,支持通配符匹配。12 种系统事件,插件间松耦合通信。消息总线支持点对点通信。

+
发布订阅通配符点对点
+
+
+
+ +
+

丰富配置系统

+

100+ 配置参数,覆盖服务器、日志、认证、数据库、缓存、监控、安全、消息队列、任务调度、插件系统等模块。

+
100+ 参数热重载文件监听
+
+
+
+ + + + + + + + diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..dd5867b --- /dev/null +++ b/website/index.html @@ -0,0 +1,83 @@ + + + + + + Future OSS - 一切皆为插件的开发者工具运行时框架 + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + 2026 · 插件驱动 · 一切皆可扩展 +
+

+ OSS + Runtime +

+

一切皆为插件的开发者工具运行时框架

+

协议、中间件、通知渠道……所有功能均以插件形式加载。内置熔断降级、依赖自动解析、事件驱动等企业级稳定性机制。

+
+ + 快速开始 + + + 了解更多 +
+
+
100+配置参数
+
10插件类型
+
自动依赖解析
+
+
+
+
+
+
+
+ + + + main.go +
+
// 1. 加载配置
+cfg, _ := config.Load("config.yaml")
+
+// 2. 安装插件
+$ oss pkg install \
+  @{Falck/http-server}<1.0.0>
+
+// 3. 启动服务
+$ oss serve
+
+
+
+
+ + + + + + + + diff --git a/website/js/animations.js b/website/js/animations.js new file mode 100644 index 0000000..83acbfa --- /dev/null +++ b/website/js/animations.js @@ -0,0 +1,35 @@ +// 动画入口 +gsap.registerPlugin(ScrollTrigger); + +// 页面元素入场 +gsap.utils.toArray('.feature-card, .step-card, .arch-layer, .arch-plugin').forEach((el, i) => { + gsap.fromTo(el, + { y: 40, opacity: 0 }, + { + y: 0, opacity: 1, duration: 0.6, + ease: 'power3.out', + scrollTrigger: { trigger: el, start: 'top 85%' }, + delay: (i % 4) * 0.1 + } + ); +}); + +// Hero 动画 +if (document.querySelector('.hero-content')) { + gsap.fromTo('.hero-content > *', + { y: 50, opacity: 0 }, + { y: 0, opacity: 1, duration: 0.8, stagger: 0.1, ease: 'power3.out', delay: 0.2 } + ); + gsap.fromTo('.hero-visual', + { scale: 0.85, opacity: 0 }, + { scale: 1, opacity: 1, duration: 1, ease: 'back.out(1.4)', delay: 0.4 } + ); +} + +// 平滑滚动 +document.querySelectorAll('a[href^="#"]').forEach(a => { + a.addEventListener('click', e => { + const target = document.querySelector(a.getAttribute('href')); + if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth' }); } + }); +}); diff --git a/website/js/app.js b/website/js/app.js new file mode 100644 index 0000000..84eedba --- /dev/null +++ b/website/js/app.js @@ -0,0 +1,16 @@ +// 主应用入口 +document.addEventListener('DOMContentLoaded', () => { + // 平滑滚动到锚点 + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + + console.log('%c Future OSS ', 'background: linear-gradient(135deg, #06b6d4, #3b82f6); color: #fff; padding: 8px 16px; border-radius: 4px; font-weight: bold;'); + console.log('%c一切皆为插件的开发者工具运行时框架', 'color: #9ca3af; font-size: 14px;'); +}); diff --git a/website/js/dock.js b/website/js/dock.js new file mode 100644 index 0000000..8aca052 --- /dev/null +++ b/website/js/dock.js @@ -0,0 +1,75 @@ +/** + * Dock 侧边栏组件 + * 在所有 HTML 页面中统一引用 + */ + +(function () { + 'use strict'; + + // Dock HTML 模板 + // current: 当前页面文件名,用于设置 active 状态 + function renderDock(current) { + const items = [ + { href: 'index.html', tooltip: '首页', svg: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' }, + { href: 'features.html', tooltip: '特性', svg: 'M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z' }, + { href: 'plugins.html', tooltip: '插件', svg: 'M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z' }, + { href: 'architecture.html', tooltip: '架构', svg: 'M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z' }, + { separator: true }, + { href: 'quickstart.html', tooltip: '快速开始', svg: 'M13 10V3L4 14h7v7l9-11h-7z' }, + { separator: true }, + { href: 'community/', tooltip: '社区', svg: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z' }, + { href: 'https://gitee.com/starlight-apk/feature-oss', tooltip: '源码', svg: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4', target: '_blank' } + ]; + + let html = '
\n'; + + items.forEach(item => { + if (item.separator) { + html += '
\n'; + } else { + const isActive = item.href === current ? ' active' : ''; + const targetAttr = item.target ? ` target="${item.target}"` : ''; + html += ` \n`; + html += ` \n`; + html += ` \n`; + } + }); + + html += '
'; + return html; + } + + // 获取当前页面文件名 + function getCurrentPage() { + const path = window.location.pathname; + const filename = path.substring(path.lastIndexOf('/') + 1); + return filename || 'index.html'; + } + + // 初始化:将 dock 插入到 body 的第一个子元素之前 + function initDock() { + const current = getCurrentPage(); + const dockHTML = renderDock(current); + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = dockHTML; + const dockElement = tempDiv.firstElementChild; + + const body = document.body; + const firstChild = body.firstChild; + if (firstChild) { + body.insertBefore(dockElement, firstChild); + } else { + body.appendChild(dockElement); + } + } + + // DOM 加载完成后初始化 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initDock); + } else { + initDock(); + } + + // 对外暴露(如果需要手动调用) + window.OSSDock = { init: initDock, render: renderDock }; +})(); diff --git a/website/js/particles.js b/website/js/particles.js new file mode 100644 index 0000000..2337d98 --- /dev/null +++ b/website/js/particles.js @@ -0,0 +1,83 @@ +// 粒子背景动画 +const canvas = document.getElementById('particles'); +const ctx = canvas.getContext('2d'); + +let width, height, particles = []; + +function resize() { + width = canvas.width = window.innerWidth; + height = canvas.height = window.innerHeight; +} + +class Particle { + constructor() { + this.reset(); + } + + reset() { + this.x = Math.random() * width; + this.y = Math.random() * height; + this.size = 1 + Math.random() * 2; + this.speedX = -0.2 + Math.random() * 0.4; + this.speedY = -0.2 + Math.random() * 0.4; + this.opacity = 0.1 + Math.random() * 0.3; + } + + update() { + this.x += this.speedX; + this.y += this.speedY; + + if (this.x < 0 || this.x > width || this.y < 0 || this.y > height) { + this.reset(); + } + } + + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = `rgba(6, 182, 212, ${this.opacity})`; + ctx.fill(); + } +} + +function init() { + resize(); + const count = Math.min(80, Math.floor((width * height) / 15000)); + particles = Array.from({ length: count }, () => new Particle()); +} + +function animate() { + ctx.clearRect(0, 0, width, height); + + // 绘制连线 + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 120) { + ctx.beginPath(); + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.strokeStyle = `rgba(6, 182, 212, ${0.05 * (1 - dist / 120)})`; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + } + } + + particles.forEach(p => { + p.update(); + p.draw(); + }); + + requestAnimationFrame(animate); +} + +window.addEventListener('resize', () => { + resize(); +}); + +init(); +animate(); diff --git a/website/js/spa.js b/website/js/spa.js new file mode 100644 index 0000000..2900bac --- /dev/null +++ b/website/js/spa.js @@ -0,0 +1,100 @@ +/** + * 极简软重定向 (SPA Router) + * 拦截所有站内链接,使用 fetch + DOM 替换实现无刷新跳转 + */ +(function() { + document.addEventListener('click', function(e) { + const link = e.target.closest('a'); + if (!link || link.target === '_blank' || link.href.startsWith('javascript:')) return; + + const url = new URL(link.href); + if (url.origin !== window.location.origin) return; // 仅拦截站内 + + e.preventDefault(); + navigate(url.pathname + url.search, true); + }); + + window.addEventListener('popstate', function(e) { + if (e.state && e.state.html) { + document.title = e.state.title; + document.querySelector('main').innerHTML = e.state.html; + initPage(); + } else { + navigate(window.location.pathname + window.location.search, false); + } + }); + + async function navigate(url, push) { + try { + const res = await fetch(url); + if (!res.ok) { window.location.href = url; return; } + + const html = await res.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + + // 1. 更新标题 + document.title = doc.title; + + // 2. 替换 Main 内容 + const newMain = doc.querySelector('main'); + const oldMain = document.querySelector('main'); + if (newMain && oldMain) { + oldMain.innerHTML = newMain.innerHTML; + } + + // 3. 更新 URL + if (push) { + history.pushState({ + url: url, + html: newMain ? newMain.innerHTML : '', + title: doc.title + }, '', url); + window.scrollTo(0, 0); + } + + // 4. 更新 Dock 高亮 + document.querySelectorAll('#dock .dock-item').forEach(el => { + const href = el.getAttribute('href'); + el.classList.toggle('active', href && url.startsWith(href)); + }); + + // 5. 重新注入脚本/逻辑 + injectScripts(doc); + initPage(); + + } catch (err) { + console.error(err); + window.location.href = url; + } + } + + function injectScripts(doc) { + doc.scripts.forEach(oldScript => { + const newScript = document.createElement('script'); + Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value)); + if (oldScript.src) { + newScript.src = oldScript.src; + document.body.appendChild(newScript); + } else { + newScript.textContent = oldScript.textContent; + document.body.appendChild(newScript); + } + }); + } + + // 通用页面初始化入口 + function initPage() { + // 尝试调用社区模块初始化 + if (typeof initCommunity === 'function') { + // 如果脚本已加载,直接调用 + initCommunity(); + } else { + // 否则动态加载 + if (location.pathname.includes('/community/') || location.pathname.endsWith('/community/')) { + const s = document.createElement('script'); + s.src = '/community/assets/js/community.js'; + document.body.appendChild(s); + } + } + } +})(); \ No newline at end of file diff --git a/website/logo.svg b/website/logo.svg new file mode 100644 index 0000000..5ec660b --- /dev/null +++ b/website/logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/plugins.html b/website/plugins.html new file mode 100644 index 0000000..a627047 --- /dev/null +++ b/website/plugins.html @@ -0,0 +1,113 @@ + + + + + + 插件生态 - Future OSS + + + + + + + + + + + + + + + + + +
+ +
+
+ 插件生态 +

包名格式与安装流程

+

简洁且富有表现力的包名格式,让插件管理像呼吸一样自然

+
+ +
+
+
+ @{ + 作者 + / + 插件名 + }< + 版本 + > +
+
+
+
@{ }
+
大括号包裹作者和插件名
+
+
+
/
+
分隔作者与插件名
+
+
+
< >
+
尖括号包裹语义化版本号
+
+
+
+ +
+
+ + + + 终端 +
+
+
+ $ + oss pkg install @{Falck/http-server}<1.0.0> +
+
✅ http-server@1.0.0 安装完成 (1.2s)
+
+ $ + oss pkg list +
+
已安装 1 个包:
+
http-server@1.0.0 - HTTP 协议适配器
+
+ $ + oss pkg update +
+
✅ 所有包已是最新版本
+
+
+ +
+

常用命令

+ + + + + + + + + + + + + +
命令别名说明
oss pkg install @{x/y}<v>i / add安装指定版本的插件包
oss pkg remove @{x/y}rm / uninstall卸载已安装的插件
oss pkg listls列出所有已安装的包
oss pkg update [包名]—更新单个或所有包
oss pkg sync—从远程仓库同步所有包
oss pkg init—初始化 package.json
oss pkg clean—清理下载缓存
+
+
+
+ + + + + + + + diff --git a/website/quickstart.html b/website/quickstart.html new file mode 100644 index 0000000..180c991 --- /dev/null +++ b/website/quickstart.html @@ -0,0 +1,72 @@ + + + + + + 快速开始 - Future OSS + + + + + + + + + + + + + + + + + +
+ +
+
+ 快速开始 +

三步即可运行

+

从克隆代码到启动服务,只需几分钟

+
+ +
+
+
1
+

克隆代码

+
git clone https://gitee.com/starlight-apk/feature-oss.git
+cd feature-oss
+
+
+
2
+

编译构建

+
go mod download
+go build -o bin/oss .
+./bin/oss init
+
+
+
3
+

启动服务

+
./bin/oss serve
+# 访问 localhost:8080
+
+
+ +
+ + 查看完整文档 + + + + 前往 Gitee 仓库 + +
+
+ + + + + + + + diff --git a/website/sitemap.xml b/website/sitemap.xml new file mode 100644 index 0000000..2e200e9 --- /dev/null +++ b/website/sitemap.xml @@ -0,0 +1,97 @@ + + + + + + https://oss-runtime.dev/ + 2026-04-05 + weekly + 1.0 + + + + https://oss-runtime.dev/features + 2026-04-05 + monthly + 0.8 + + + + https://oss-runtime.dev/architecture + 2026-04-05 + monthly + 0.7 + + + + https://oss-runtime.dev/quickstart + 2026-04-05 + monthly + 0.9 + + + + https://oss-runtime.dev/plugins + 2026-04-05 + weekly + 0.8 + + + + + https://oss-runtime.dev/community/ + 2026-04-05 + daily + 0.7 + + + + https://oss-runtime.dev/community/post.php + 2026-04-05 + daily + 0.6 + + + + https://oss-runtime.dev/community/profile.php + 2026-04-05 + weekly + 0.5 + + + + https://oss-runtime.dev/community/my-posts.php + 2026-04-05 + weekly + 0.4 + + + + https://oss-runtime.dev/community/editor.php + 2026-04-05 + monthly + 0.4 + + + + https://oss-runtime.dev/community/edit-profile.php + 2026-04-05 + monthly + 0.3 + + + + https://oss-runtime.dev/community/login.php + 2026-04-05 + monthly + 0.3 + + + + https://oss-runtime.dev/community/register.php + 2026-04-05 + monthly + 0.3 + + + diff --git a/website/update.php b/website/update.php new file mode 100644 index 0000000..b8efdf9 --- /dev/null +++ b/website/update.php @@ -0,0 +1,126 @@ + $path, 'sha' => $item['sha'], 'size' => $item['size'] ?? 0]; + } + } + + $log .= "获取到 " . count($websiteFiles) . " 个远程文件\n"; + + $updated = 0; $created = 0; $errors = 0; + $shaCacheFile = CACHE_DIR . '/file_shas.json'; + $localShas = file_exists($shaCacheFile) ? json_decode(@file_get_contents($shaCacheFile), true) : []; + if (!is_array($localShas)) $localShas = []; + + foreach ($websiteFiles as $file) { + $relativePath = substr($file['path'], strlen('website/')); + $localPath = __DIR__ . '/' . $relativePath; + $remoteSha = $file['sha']; + + $dir = dirname($localPath); + if (!is_dir($dir)) @mkdir($dir, 0755, true); + + $needsUpdate = false; + $reason = ''; + + if (!file_exists($localPath)) { + $needsUpdate = true; $reason = '文件缺失'; + } else { + $localSha = $localShas[$relativePath] ?? ''; + if ($localSha !== $remoteSha) { $needsUpdate = true; $reason = '内容已变更'; } + } + + if ($needsUpdate) { + $contentUrl = sprintf('%s/%s/%s/contents/%s?ref=%s', GITEE_API_BASE, GITEE_OWNER, GITEE_REPO, $file['path'], GITEE_BRANCH); + $contentData = httpGetJson($contentUrl); + if ($contentData && isset($contentData['content'])) { + $content = base64_decode(str_replace(["\n", "\r"], '', $contentData['content'])); + if (@file_put_contents($localPath, $content) !== false) { + $localShas[$relativePath] = $remoteSha; + if ($reason === '文件缺失') { $created++; $log .= "✅ 创建: $relativePath\n"; } + else { $updated++; $log .= "🔄 更新: $relativePath ($reason)\n"; } + } else { $errors++; $log .= "❌ 写入失败: $relativePath\n"; } + } else { $errors++; $log .= "❌ 获取内容失败: $relativePath\n"; } + } + } + + @file_put_contents($shaCacheFile, json_encode($localShas)); + @file_put_contents(CACHE_DIR . '/update_check.json', json_encode([ + 'timestamp' => time(), 'files' => count($websiteFiles), + 'updated' => $updated, 'created' => $created, 'errors' => $errors + ], JSON_PRETTY_PRINT)); + + $log .= "完成: 更新 $updated 个,创建 $created 个,错误 $errors 个\n"; + $log .= "下次检查: " . date('Y-m-d H:i:s', time() + UPDATE_INTERVAL) . "\n\n"; + @file_put_contents($logFile, $log, FILE_APPEND); + + } catch (Exception $e) { + $log .= "异常: " . $e->getMessage() . "\n"; + @file_put_contents($logFile, $log, FILE_APPEND); + } +} + +function httpGetJson($url) { + $context = stream_context_create(['http' => ['method' => 'GET', 'header' => ['User-Agent: OSS-AutoUpdate/1.0', 'Accept: application/json'], 'timeout' => 30], 'ssl' => ['verify_peer' => true, 'verify_peer_name' => true]]); + $response = @file_get_contents($url, false, $context); + if ($response === false) return null; + return json_decode($response, true); +} + +register_shutdown_function('autoUpdateWebsite');