From dec66db5aa37f4db080a017b67eaba7e97da1db0 Mon Sep 17 00:00:00 2001 From: logan Date: Fri, 18 Sep 2026 00:16:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OpsxAPI=20v1.0=20=E5=85=A8=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E6=8E=A5=E5=8F=A3=E5=B7=A5=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT - 集合树/多标签/多环境变量/历史记录,localStorage 持久化 - 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、 Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖) - 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、 全量备份;单请求生成 cURL/fetch/Python/Go 代码 - Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、 WebSocket 隧道(浏览器自定义握手 Header) - 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付) - lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题 - 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全 --- .gitignore | 28 + Makefile | 32 + README.md | 109 + api/openapi.yaml | 69 + apps/desktop/package-lock.json | 220 + apps/desktop/package.json | 11 + apps/desktop/src-tauri/Cargo.lock | 4808 +++++++++++++++++ apps/desktop/src-tauri/Cargo.toml | 26 + apps/desktop/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 11 + apps/desktop/src-tauri/icons/128x128.png | Bin 0 -> 8315 bytes apps/desktop/src-tauri/icons/128x128@2x.png | Bin 0 -> 17915 bytes apps/desktop/src-tauri/icons/32x32.png | Bin 0 -> 1671 bytes apps/desktop/src-tauri/icons/64x64.png | Bin 0 -> 3807 bytes .../src-tauri/icons/Square107x107Logo.png | Bin 0 -> 6893 bytes .../src-tauri/icons/Square142x142Logo.png | Bin 0 -> 9466 bytes .../src-tauri/icons/Square150x150Logo.png | Bin 0 -> 10066 bytes .../src-tauri/icons/Square284x284Logo.png | Bin 0 -> 20295 bytes .../src-tauri/icons/Square30x30Logo.png | Bin 0 -> 1513 bytes .../src-tauri/icons/Square310x310Logo.png | Bin 0 -> 22190 bytes .../src-tauri/icons/Square44x44Logo.png | Bin 0 -> 2497 bytes .../src-tauri/icons/Square71x71Logo.png | Bin 0 -> 4235 bytes .../src-tauri/icons/Square89x89Logo.png | Bin 0 -> 5455 bytes apps/desktop/src-tauri/icons/StoreLogo.png | Bin 0 -> 2888 bytes .../android/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1248 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1816 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 1022 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1196 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1276 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 950 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2208 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2360 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 1404 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3107 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 3678 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 1987 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 4089 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 4917 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 2424 bytes .../android/values/ic_launcher_background.xml | 4 + apps/desktop/src-tauri/icons/icon.icns | Bin 0 -> 91812 bytes apps/desktop/src-tauri/icons/icon.ico | Bin 0 -> 742 bytes apps/desktop/src-tauri/icons/icon.png | Bin 0 -> 46113 bytes apps/desktop/src-tauri/icons/icon.svg | 14 + .../src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 0 -> 349 bytes .../icons/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 520 bytes .../src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 0 -> 520 bytes .../src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 0 -> 801 bytes .../src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 0 -> 445 bytes .../icons/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 774 bytes .../src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 0 -> 774 bytes .../src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 0 -> 1095 bytes .../src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 0 -> 520 bytes .../icons/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 987 bytes .../src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 0 -> 987 bytes .../src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 0 -> 1478 bytes .../src-tauri/icons/ios/AppIcon-512@2x.png | Bin 0 -> 13734 bytes .../src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 0 -> 1478 bytes .../src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 0 -> 2089 bytes .../src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 0 -> 957 bytes .../src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 0 -> 1754 bytes .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 1947 bytes apps/desktop/src-tauri/src/lib.rs | 136 + apps/desktop/src-tauri/src/main.rs | 5 + apps/desktop/src-tauri/tauri.conf.json | 43 + apps/server/cmd/opsxapi/main.go | 19 + apps/server/config.example.yaml | 7 + apps/server/go.mod | 38 + apps/server/go.sum | 93 + apps/server/internal/docs/assets/docs.css | 246 + apps/server/internal/docs/assets/docs.js | 219 + apps/server/internal/docs/content/01-intro.md | 36 + .../internal/docs/content/02-quickstart.md | 48 + .../internal/docs/content/03-interface.md | 41 + apps/server/internal/docs/content/04-http.md | 47 + apps/server/internal/docs/content/05-auth.md | 29 + apps/server/internal/docs/content/06-env.md | 50 + .../internal/docs/content/07-graphql.md | 32 + .../internal/docs/content/08-websocket.md | 34 + apps/server/internal/docs/content/09-sse.md | 29 + .../internal/docs/content/10-socketio.md | 45 + apps/server/internal/docs/content/11-mqtt.md | 41 + .../server/internal/docs/content/12-import.md | 40 + .../server/internal/docs/content/13-export.md | 30 + .../server/internal/docs/content/14-manage.md | 39 + .../internal/docs/content/15-server-api.md | 75 + apps/server/internal/docs/content/16-faq.md | 45 + .../internal/docs/content/manifest.json | 41 + apps/server/internal/docs/docs.go | 264 + apps/server/internal/docs/docs_test.go | 122 + apps/server/internal/docs/render.go | 25 + apps/server/internal/docs/template.html | 110 + apps/server/internal/proxy/guard.go | 36 + apps/server/internal/proxy/guard_test.go | 23 + apps/server/internal/proxy/handler.go | 116 + apps/server/internal/proxy/stream.go | 145 + apps/server/internal/proxy/types.go | 23 + apps/server/internal/proxy/ws_tunnel.go | 236 + apps/server/internal/server/router.go | 59 + apps/server/internal/site/assets/site.css | 282 + apps/server/internal/site/site.go | 67 + apps/server/internal/site/site_test.go | 63 + apps/server/internal/site/template.html | 357 ++ apps/web/index.html | 14 + apps/web/package.json | 30 + apps/web/postcss.config.js | 6 + apps/web/public/favicon.svg | 15 + apps/web/src/main.tsx | 10 + apps/web/src/styles.css | 417 ++ apps/web/tailwind.config.ts | 15 + apps/web/tsconfig.app.json | 17 + apps/web/tsconfig.json | 11 + apps/web/tsconfig.node.json | 14 + apps/web/vite.config.ts | 41 + docs/refs/.keep | 0 package-lock.json | 3714 +++++++++++++ package.json | 27 + packages/core/package.json | 23 + packages/core/src/exporters/codegen.ts | 184 + packages/core/src/exporters/curl.ts | 76 + packages/core/src/exporters/index.ts | 61 + packages/core/src/exporters/markdown.ts | 95 + packages/core/src/exporters/openapi.ts | 265 + packages/core/src/exporters/postman.ts | 146 + packages/core/src/factory.ts | 267 + packages/core/src/importers.test.ts | 451 ++ packages/core/src/importers/apipost.ts | 209 + packages/core/src/importers/curl.ts | 386 ++ packages/core/src/importers/har.ts | 151 + packages/core/src/importers/hoppscotch.ts | 135 + packages/core/src/importers/index.ts | 138 + packages/core/src/importers/insomnia.ts | 203 + packages/core/src/importers/markdown.ts | 86 + packages/core/src/importers/openapi.ts | 400 ++ packages/core/src/importers/postman.ts | 228 + packages/core/src/index.ts | 118 + packages/core/src/model.ts | 250 + packages/core/src/replaceVariables.test.ts | 29 + packages/core/src/replaceVariables.ts | 35 + packages/core/src/tree.ts | 227 + packages/core/src/types.ts | 57 + packages/core/tsconfig.json | 10 + packages/core/vitest.config.ts | 7 + packages/transport/package.json | 27 + packages/transport/src/browser.ts | 81 + packages/transport/src/detect.ts | 41 + packages/transport/src/ginProxy.ts | 35 + packages/transport/src/index.ts | 29 + packages/transport/src/mqtt.ts | 120 + packages/transport/src/socketio.ts | 92 + packages/transport/src/streaming.ts | 351 ++ packages/transport/src/tauriNative.ts | 68 + packages/transport/src/wsSmart.ts | 190 + packages/transport/tsconfig.json | 10 + packages/ui/package.json | 25 + packages/ui/src/App.tsx | 118 + packages/ui/src/components/EnvBadge.tsx | 61 + packages/ui/src/components/Logo.tsx | 22 + packages/ui/src/components/Sidebar.tsx | 306 ++ packages/ui/src/components/TabBar.tsx | 105 + packages/ui/src/components/TopBar.tsx | 215 + packages/ui/src/components/VarHintInput.tsx | 179 + packages/ui/src/components/common.tsx | 357 ++ .../src/components/dialogs/ExportDialog.tsx | 134 + .../src/components/dialogs/ImportDialog.tsx | 288 + .../components/dialogs/WorkspaceDialogs.tsx | 432 ++ .../src/components/editors/GraphqlEditor.tsx | 104 + .../ui/src/components/editors/HttpEditor.tsx | 317 ++ .../ui/src/components/editors/MqttEditor.tsx | 134 + .../src/components/editors/ResponsePanel.tsx | 126 + .../src/components/editors/SocketIoEditor.tsx | 127 + .../ui/src/components/editors/SseEditor.tsx | 121 + .../components/editors/WebsocketEditor.tsx | 100 + .../ui/src/components/editors/liveCommon.tsx | 141 + packages/ui/src/components/icons.tsx | 119 + packages/ui/src/index.ts | 3 + packages/ui/src/send.ts | 452 ++ packages/ui/src/workspaceStore.ts | 695 +++ packages/ui/tsconfig.json | 10 + run.sh | 275 + tsconfig.base.json | 23 + 182 files changed, 23377 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 api/openapi.yaml create mode 100644 apps/desktop/package-lock.json create mode 100644 apps/desktop/package.json create mode 100644 apps/desktop/src-tauri/Cargo.lock create mode 100644 apps/desktop/src-tauri/Cargo.toml create mode 100644 apps/desktop/src-tauri/build.rs create mode 100644 apps/desktop/src-tauri/capabilities/default.json create mode 100644 apps/desktop/src-tauri/icons/128x128.png create mode 100644 apps/desktop/src-tauri/icons/128x128@2x.png create mode 100644 apps/desktop/src-tauri/icons/32x32.png create mode 100644 apps/desktop/src-tauri/icons/64x64.png create mode 100644 apps/desktop/src-tauri/icons/Square107x107Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square142x142Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square150x150Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square284x284Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square30x30Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square310x310Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square44x44Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square71x71Logo.png create mode 100644 apps/desktop/src-tauri/icons/Square89x89Logo.png create mode 100644 apps/desktop/src-tauri/icons/StoreLogo.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 apps/desktop/src-tauri/icons/android/values/ic_launcher_background.xml create mode 100644 apps/desktop/src-tauri/icons/icon.icns create mode 100644 apps/desktop/src-tauri/icons/icon.ico create mode 100644 apps/desktop/src-tauri/icons/icon.png create mode 100644 apps/desktop/src-tauri/icons/icon.svg create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png create mode 100644 apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png create mode 100644 apps/desktop/src-tauri/src/lib.rs create mode 100644 apps/desktop/src-tauri/src/main.rs create mode 100644 apps/desktop/src-tauri/tauri.conf.json create mode 100644 apps/server/cmd/opsxapi/main.go create mode 100644 apps/server/config.example.yaml create mode 100644 apps/server/go.mod create mode 100644 apps/server/go.sum create mode 100644 apps/server/internal/docs/assets/docs.css create mode 100644 apps/server/internal/docs/assets/docs.js create mode 100644 apps/server/internal/docs/content/01-intro.md create mode 100644 apps/server/internal/docs/content/02-quickstart.md create mode 100644 apps/server/internal/docs/content/03-interface.md create mode 100644 apps/server/internal/docs/content/04-http.md create mode 100644 apps/server/internal/docs/content/05-auth.md create mode 100644 apps/server/internal/docs/content/06-env.md create mode 100644 apps/server/internal/docs/content/07-graphql.md create mode 100644 apps/server/internal/docs/content/08-websocket.md create mode 100644 apps/server/internal/docs/content/09-sse.md create mode 100644 apps/server/internal/docs/content/10-socketio.md create mode 100644 apps/server/internal/docs/content/11-mqtt.md create mode 100644 apps/server/internal/docs/content/12-import.md create mode 100644 apps/server/internal/docs/content/13-export.md create mode 100644 apps/server/internal/docs/content/14-manage.md create mode 100644 apps/server/internal/docs/content/15-server-api.md create mode 100644 apps/server/internal/docs/content/16-faq.md create mode 100644 apps/server/internal/docs/content/manifest.json create mode 100644 apps/server/internal/docs/docs.go create mode 100644 apps/server/internal/docs/docs_test.go create mode 100644 apps/server/internal/docs/render.go create mode 100644 apps/server/internal/docs/template.html create mode 100644 apps/server/internal/proxy/guard.go create mode 100644 apps/server/internal/proxy/guard_test.go create mode 100644 apps/server/internal/proxy/handler.go create mode 100644 apps/server/internal/proxy/stream.go create mode 100644 apps/server/internal/proxy/types.go create mode 100644 apps/server/internal/proxy/ws_tunnel.go create mode 100644 apps/server/internal/server/router.go create mode 100644 apps/server/internal/site/assets/site.css create mode 100644 apps/server/internal/site/site.go create mode 100644 apps/server/internal/site/site_test.go create mode 100644 apps/server/internal/site/template.html create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/postcss.config.js create mode 100644 apps/web/public/favicon.svg create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/styles.css create mode 100644 apps/web/tailwind.config.ts create mode 100644 apps/web/tsconfig.app.json create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts create mode 100644 docs/refs/.keep create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 packages/core/package.json create mode 100644 packages/core/src/exporters/codegen.ts create mode 100644 packages/core/src/exporters/curl.ts create mode 100644 packages/core/src/exporters/index.ts create mode 100644 packages/core/src/exporters/markdown.ts create mode 100644 packages/core/src/exporters/openapi.ts create mode 100644 packages/core/src/exporters/postman.ts create mode 100644 packages/core/src/factory.ts create mode 100644 packages/core/src/importers.test.ts create mode 100644 packages/core/src/importers/apipost.ts create mode 100644 packages/core/src/importers/curl.ts create mode 100644 packages/core/src/importers/har.ts create mode 100644 packages/core/src/importers/hoppscotch.ts create mode 100644 packages/core/src/importers/index.ts create mode 100644 packages/core/src/importers/insomnia.ts create mode 100644 packages/core/src/importers/markdown.ts create mode 100644 packages/core/src/importers/openapi.ts create mode 100644 packages/core/src/importers/postman.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/model.ts create mode 100644 packages/core/src/replaceVariables.test.ts create mode 100644 packages/core/src/replaceVariables.ts create mode 100644 packages/core/src/tree.ts create mode 100644 packages/core/src/types.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/vitest.config.ts create mode 100644 packages/transport/package.json create mode 100644 packages/transport/src/browser.ts create mode 100644 packages/transport/src/detect.ts create mode 100644 packages/transport/src/ginProxy.ts create mode 100644 packages/transport/src/index.ts create mode 100644 packages/transport/src/mqtt.ts create mode 100644 packages/transport/src/socketio.ts create mode 100644 packages/transport/src/streaming.ts create mode 100644 packages/transport/src/tauriNative.ts create mode 100644 packages/transport/src/wsSmart.ts create mode 100644 packages/transport/tsconfig.json create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/App.tsx create mode 100644 packages/ui/src/components/EnvBadge.tsx create mode 100644 packages/ui/src/components/Logo.tsx create mode 100644 packages/ui/src/components/Sidebar.tsx create mode 100644 packages/ui/src/components/TabBar.tsx create mode 100644 packages/ui/src/components/TopBar.tsx create mode 100644 packages/ui/src/components/VarHintInput.tsx create mode 100644 packages/ui/src/components/common.tsx create mode 100644 packages/ui/src/components/dialogs/ExportDialog.tsx create mode 100644 packages/ui/src/components/dialogs/ImportDialog.tsx create mode 100644 packages/ui/src/components/dialogs/WorkspaceDialogs.tsx create mode 100644 packages/ui/src/components/editors/GraphqlEditor.tsx create mode 100644 packages/ui/src/components/editors/HttpEditor.tsx create mode 100644 packages/ui/src/components/editors/MqttEditor.tsx create mode 100644 packages/ui/src/components/editors/ResponsePanel.tsx create mode 100644 packages/ui/src/components/editors/SocketIoEditor.tsx create mode 100644 packages/ui/src/components/editors/SseEditor.tsx create mode 100644 packages/ui/src/components/editors/WebsocketEditor.tsx create mode 100644 packages/ui/src/components/editors/liveCommon.tsx create mode 100644 packages/ui/src/components/icons.tsx create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/src/send.ts create mode 100644 packages/ui/src/workspaceStore.ts create mode 100644 packages/ui/tsconfig.json create mode 100755 run.sh create mode 100644 tsconfig.base.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0128e54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +node_modules/ +dist/ +build/ +target/ +.DS_Store +*.log +.env +.env.* +!.env.example +release/ +*.local +.vite/ +coverage/ +.turbo/ +.idea/ +.vscode/ +apps/server/opsxapi +apps/server/server +apps/desktop/src-tauri/gen/ +*.exe +*.dylib +*.so +Cargo.lock +!apps/desktop/src-tauri/Cargo.lock + +logs/ +.*.pid +apps/server/bin/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4679e29 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: install web gui all server stop status build-web test desktop + +install: + npm install + npm --prefix apps/desktop install + +web: + ./run.sh web + +gui: + ./run.sh gui + +all: + ./run.sh all + +server: + ./run.sh server + +stop: + ./run.sh stop + +status: + ./run.sh status + +desktop: + ./run.sh desktop + +build-web: + npm run build:web + +test: + npm run test:core diff --git a/README.md b/README.md new file mode 100644 index 0000000..998cdc4 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# OpsxAPI + +给运维和开发用的专业接口工作台 —— 对标 Apifox / Apipost 的自研替代。 + +**HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT,一个工作台全搞定;数据全在本地。** + +``` +packages/core 数据模型 / 树操作 / 导入导出器(纯 TS,21 个单测) +packages/transport browser | gin-proxy | tauri-native + 流式/SSE/WS隧道/Socket.IO/MQTT 客户端 +packages/ui 共享工作台 UI(集合树 / 多标签 / 编辑器 / 对话框) +apps/web Vite + React 入口(:5173) +apps/desktop Tauri 2 壳(frontendDist → web dist) +apps/server Gin:HTTP 代理 + 流式代理 + WebSocket 隧道(:8787) +``` + +核心原则:**一套 UI、一份请求契约,Web / Desktop 只换 Transport。** + +## 功能总览 + +### 协议 +- **HTTP/REST**:GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS;Params / Body(JSON·XML·Text·HTML·x-www-form-urlencoded·form-data)/ Headers / Cookies / Auth(Bearer·Basic·APIKey) +- **GraphQL**:Query + Variables 编辑、独立响应面板 +- **WebSocket**:消息日志(收/发/系统分色)、快捷发送、握手 Header(经 Gin 隧道,绕开浏览器限制) +- **SSE**:断点续传(Last-Event-ID)、直连失败自动切 Gin 代理、事件按 `event:` 分发 +- **Socket.IO**:事件监听(可 onAny)、emit 参数 JSON、path/transports/auth 连接选项 +- **MQTT**:wss broker、主题订阅(通配符)、发布、QoS 0/1/2、自动重连 + +### 导入(粘贴 / 文件 / URL 三种来源,格式自动识别) +| 格式 | 说明 | +| --- | --- | +| OpenAPI 3.0 / 3.1 / Swagger 2.0 | JSON 或 YAML;Apifox 导出同格式直接可用 | +| Swagger URL | 经本地 Gin 代理拉取,避开 CORS | +| Postman Collection v2.0 / v2.1 | 含鉴权、变量、GraphQL body | +| Apipost 项目 | `api_collection` 宽容解析 | +| cURL | 引号 / 续行 / -d / -F / -u / --data-urlencode 全支持 | +| HAR 抓包 | 按域名分组、去重 | +| Insomnia 导出 | v3 / v4(workspace/group 层级) | +| Hoppscotch 集合 | 数组或单对象 | +| Markdown 接口文档 | 提取 ```curl 代码块 | +| OpsxAPI 备份 | 全量集合 + 环境 | +| YApi / Apidoc / Apizza / Eolink | 请先导出为 OpenAPI 或 Postman 格式再导入 | + +导入后自动:tag→目录、`{path}` 参数→`{{变量}}` 并写入环境、schema→示例 body、security→鉴权配置。 + +### 导出 +- OpenAPI 3.0(JSON / YAML,带 tag 分组)· Postman v2.1 · Markdown 接口文档 · 全量备份 +- 单请求代码生成:cURL / JavaScript fetch / Python requests / Go net/http + +### 工作台 +集合树(搜索 / 右键菜单 / 拖拽移动 / 复制 / 重命名)· 多标签页 · 历史记录(点击回跳) +· 环境变量(多环境切换、`{{var}}` 全局替换)· 明暗双主题 · localStorage 自动持久化 + +## 怎么跑 + +```bash +make install # npm install(含 desktop 依赖) +./run.sh web # 浏览器:Gin(:8787) + Vite(:5173) ← 推荐 +./run.sh gui # 桌面 Tauri 窗口(共用 Vite) +./run.sh all # 全开 +./run.sh stop | status +``` + +打开 http://127.0.0.1:5173 —— 内置「示例集合」覆盖全部协议,开箱即用。 + +## 用户文档站(/docs) + +Gin 内置产品文档,Markdown 嵌入二进制、SSR 渲染,随服务一键部署: + +- 地址:`http://127.0.0.1:8787/docs`(开发态 `http://127.0.0.1:5173/docs`,Vite 已反代;工作台顶栏「📖 文档」直达) +- 布局:顶栏搜索 + 左侧分组导航(16 篇:开始使用 / 请求调试 / 数据管理 / 参考)+ 右侧本页目录 +- 能力:全文搜索、代码块复制 + 高亮、提示块、上一页/下一页、反馈组件、打印导出 PDF、移动端响应式、404 页 +- 内容在 `apps/server/internal/docs/content/*.md`,改完重新 `go build` 即生效 + +## 产品官网(/home) + +Gin 内置营销单页,与 /docs 同为 SSR 嵌入交付(`internal/site`): + +- 地址:`/` 与 `/home` 双入口(开发态 `http://127.0.0.1:5173/home`);文档站顶栏「官网」互链 +- 区块:Hero(纯 CSS 产品界面 mock)/ 卖点 / 功能 / 六协议 / 导入导出生态 / 与 Apifox·Apipost 对比表 / 定价 / 让利计划 / 购买 FAQ / 联系 CTA / Footer +- 定价三档(标价/成交价分离):个人版 ¥99/年、团队版 ¥99/人/年(入门锚点);企业私有化版标价 **¥8,000/年 · ¥30,000 买断**,经早鸟/教育/多年预付/案例合作落在成交区间 ¥5,000–8,000/年、¥15,000–30,000 +- 让利机制四项:早鸟创始价(前 20 席 5–7 折永久)、教育/开源 6–8 折、多年预付 8–9 折、案例换授权;销售原则:不直接降价,用「永久授权 + 1 年升级 + 5 人团队 1–2 年回本」回应预算顾虑 +- 交付前替换:联系邮箱 `hello@opsxapi.dev`(template.html 内两处)与对比表脚注中的价格核对 + +## 服务端端点(apps/server) + +| 端点 | 用途 | +| --- | --- | +| `GET /health` | 健康检查 | +| `POST /v1/proxy/http` | HTTP 代理(绕 CORS,全方法) | +| `POST /v1/proxy/http/stream` | NDJSON 流式代理(SSE / chunked) | +| `POST /v1/proxy/ws/connect` | WS 隧道:拨号目标(带自定义 Header),返回 sessionId | +| `GET /v1/proxy/ws/attach` | WS 隧道:浏览器 attach,双向桥接 | +| `POST /v1/proxy/ws/close` | 关闭隧道会话 | + +浏览器直连 WS 无法带自定义 Header 时,transport 层自动走隧道;SSE 直连 CORS 失败自动切代理。 + +## 架构决策 + +- **一套 UI**:集合 / 编辑器 / 对话框全部在 `packages/ui`,Web 与桌面零分叉 +- **判别联合数据模型**:`node.kind === "http"` 自动收窄 `node.data` 类型 +- **实时会话不落盘**:WS/SSE/Socket.IO/MQTT 会话存模块级注册表,消息(500 条上限)与状态进 store +- **zustand 选择器必须返回稳定引用**:`?? []` 会触发 useSyncExternalStore 无限重渲染(踩过的坑) + +## 测试 + +```bash +make test # core 21 个单测:导入导出全格式往返、树操作、cURL 解析 +cd apps/server && go test ./... +``` diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..c4515ec --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.3 +info: + title: OpsxAPI Server + version: 0.1.0 + description: Minimal health + HTTP proxy API +servers: + - url: http://127.0.0.1:8787 +paths: + /health: + get: + summary: Health check + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + ok: { type: boolean } + service: { type: string } + /v1/proxy/http: + post: + summary: Proxy an HTTP request + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/HttpSpec" + responses: + "200": + description: Proxied result + content: + application/json: + schema: + $ref: "#/components/schemas/HttpResult" +components: + schemas: + HttpSpec: + type: object + required: [method, url] + properties: + method: + type: string + enum: [GET, POST, PUT, PATCH, DELETE] + url: { type: string } + headers: + type: object + additionalProperties: { type: string } + query: + type: object + additionalProperties: { type: string } + body: + type: object + properties: + mode: { type: string, enum: [none, json, raw, form, urlencoded] } + raw: { type: string } + timeoutMs: { type: integer } + HttpResult: + type: object + properties: + status: { type: integer } + headers: + type: object + additionalProperties: { type: string } + body: { type: string } + timeMs: { type: number } + size: { type: integer } diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json new file mode 100644 index 0000000..48f0420 --- /dev/null +++ b/apps/desktop/package-lock.json @@ -0,0 +1,220 @@ +{ + "name": "@opsxapi/desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@opsxapi/desktop", + "version": "0.1.0", + "devDependencies": { + "@tauri-apps/cli": "^2.4.0" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000..9867536 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,11 @@ +{ + "name": "@opsxapi/desktop", + "version": "0.1.0", + "private": true, + "scripts": { + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.4.0" + } +} diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..14e7335 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4808 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.5", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.2", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opsxapi" +version = "0.1.0" +dependencies = [ + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tokio", + "url", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.5", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.6+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..cc3576c --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "opsxapi" +version = "0.1.0" +description = "OpsxAPI — API workbench for ops & developers" +authors = ["OpsxAPI"] +edition = "2021" +default-run = "OpsxAPI" + +[lib] +name = "opsxapi_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "OpsxAPI" +path = "src/main.rs" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +tokio = { version = "1", features = ["time"] } +url = "2" diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..93feea7 --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capabilities for the OpsxAPI main window", + "windows": [ + "main" + ], + "permissions": [ + "core:default" + ] +} diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..be05088b5f6358e684f150b3ebcc64e226831238 GIT binary patch literal 8315 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 FV1nax3I_lH literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1cef1b7d86b63318d9139afbe9e7e62021f9bb33 GIT binary patch literal 17915 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%0D>rt7CVVW?QKHOD$$(h`B^4k9FYH<(X_9_A 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 Ef+^JNr2qf` literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..b3ebe1c6702351d3ff0f9cda5e22ec9a766eb76e GIT binary patch literal 1671 zcmV;226*|2P)2FJ`H>@~ zhRqCW3qT|*11K4DYuc~mj`tuDy9`hb0M3*R0pO@o_=gCxF9(^S&j5r|e${aRCUR0$ zuk5*p`86BWf$uwsSqw_*KAe>i0mJ|i0Sh{IsbiyCvk!DUkIjnkBuUE*+Onk-y5~Q_ z8tN8F?-B{l1*Bv+7si;>!H{QnDj7WjU@N}p4FE_od7-DIn{4c}SFHMw^{#gih1@_t z{j66uL!wf!9U;GbM}&WS>(60v;$5g)w73RI_wi)yL$venNgCW9gK^V#PK}5lC6NeS zUe&M>lj9!)yEOv<{KlrFU;mxu&1od=D5x_~{Omv|B_fK8>2y5tQ&$+~elQxaZM&Hm z)tf<5SI9YzzHF+LR3cVGUQIK?+W*)@^Bm*j%^!9aU7!vRBg zUwRP^4UgdP$e4++zAlCH+UFTiaU`&3{}2uxJdE~*ZD?+&$9IE2o;?807&5s$E?;pm zK7aoeeDujzShL|_NTsm$=Bx1P3s2+V@Cd4_d_+-%-ka{k%HA$KyLl5@I#wEX7&~!Q zvr_>eMu>wD_uT#`92q@|E4r59qRu6d`AOK8W$0eIVl`3=yAcb4dk1bsI-5fr>uuoF z#!l*-2*qNEwgs(Nd(%qXI`CI~|HB>(Y*+^r3TBz-oa2_&*W$jr`q9)-k3D;bz`5R9 zaP|N=XUOOBSaV|^JTHMIe`q%}uU~T=?7EZ@pdRzsrn|A}{@bwiwRiC1OK%`Gry4@c zjsfdrol+`TwuKGrS7G0QgBZAN9du#UrRJcwuN#Fz(NNue-3A;>XTZ6IC=5-x3aQSH z0mc||T6|s0ap|&7+_CWqtmx@SS5H6w`OQu|`p{jlEDqbYkVv@j{UqvYYxIXEEndQ% z6#&vdcFlt2j#s^%yVXR*G<4GW*L2TsYr*{1Cj9H$U54%`j4-dY32m)S*n8lRDZ_7f z3_?humsrG6jMmmh%x`VRzjy5ZjYFj*M36&VIC_1M8UFNy01P@Jgkgjr3{mALONbdE z3?kF67am;&zV986XzH6Vir^=`Qv$F~#*Kn9i%i0GAfz;t>j+I-!ZkJSy0+2EaR;Su zG`LwT#i$HG#~rqOEG}9HTJhz5LW9&ADDM}u1V9?9J^*r4 zz>xrIT!0fWUIaN7f?^RDc3yz<&tHgaHiuoi2T|qw7#STyZEXpZ#Vr$cI+K~PNmUM# z#B>!zVRlqWIblN5(Rvo>iEhv{R|0hjP%O>TyJX2CY}@v=iSVkv9xPqD7%TgFaKrW2 zpryGP{rzjvHh-ScjAo>QSEbK}N{Os292+&o2J~2%93NIt+m#dwP{JGu@=@s#S_!2^ zX~%zd;_9pWkjZ3C-P^W}ef#(0@Zlryt93AnCnsMlK1 zpei~iQZf#Nj6*9fjNcukhu#F1$5V+~ZUf&SId{=>%<&&2ZVATpfdveVrnj=vA)~ar zX+bj1XGE*}f9qUBS5k@)O2k}*+1Dq>{{I)CX%aY6C@Y14KRUqw63MxXpC!w^pIHuw z>C^elT$T@QMr8T~^vi2bCDS>VF{FqfqvC6mV?RC))TV)noW42zQmWdMUO!o)wtW>@ z-T*PajEHJaaD|(VDWzmu=Uj%l*Ms!X2LSYiq@cB54(QhbY7n|CecP^SXSUO*cqt#J zYb0bW!eA_pGeZD$#M&mO?u^q8E13^h literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..c86173262e284a2b487b36e2b3b662fb8930c5ab GIT binary patch literal 3807 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 V)_TX8lBECu002ovPDHLkV1fciCU^h< literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7885a517c4be6b8c483771dbfe830e10b984c5a7 GIT binary patch literal 6893 zcmV*%E_X_k566hNwk9Q`CJ`T{KfOk|t6-W7=oqax9&py^9yZFDe&xcIfk+?}GpolI$ z7rL&mku308cJjH;;)1F-r!V6y$AB7r80ew4GxF17;K7f*-7 zsC=n$fuX$$^w)q!I&J{S05Co0yXPILcR5y+&v2{#_d>U~3IHvJ^Lt(J3W1n^zivq! zVUTp7p9(ak0MHijmk$^Mwfs~?(?%;oImrSB3W$*X{z_i}D3AEPrG%9d67^DAyCKbw z%v5|!I<{{&fCK=)vzbmq&6hmd<6uD(hY6J8Y?`yR(LufPq;a}lJ{zhs8&ZrHil`RC z(SXMxNhu+v*$jzw(lTC=$>>YT=$;Q8u)@whod=p%+W0m{60CMfnscWHEu|vbq@cN8 zLnV@k#Y}&_IYYJNVgWHv2qjFK1%iqHm6n+XC;v0->+c925Kc)(t)$|V9nQrFSJO| z%Mvb9n$A^)?9=f`k{M5bg-{7UZmj9#ax<~-+bQ4;k6I)OV)$^0?c0q`RAUD!hKbHO z(^cSAZUx~b>t-r}$OI`(4F~pCVeMb;5K@*&%ajnpf|Mr;K%J4p2MD1kij*=V$4q=! z3?8wI@IxZHE_Tm(%ULzn*6P@`D^m<$qG*Y#sgdQUEv~@mvmX(G@I0tGwIKV>(xlHO zEJ zteGpD>PU3kNm}$KC7=ou@kL4q3qm*mNQjD>slJmZKb4ww`%J7~lK}!Mu+?<r%NP6#W6m$brZ>`g z@mUUNUHQ|tYpxAri4no_^PuWInsq+DNRTHc)kx)eEPbk_dqN>^4BI7GxxsGZJ_1X? z7is_`J>W!k9F8oJudn+k%@aVf&5c!w-vu4Ct|ZGwdHs8=@~nsM>yJbXP&L4EZD5HG zi8$nhfRTpv)c5*v^wYe{R{*RW>Bs zvPts+71fIM@*@&&y}a00b;@)!HYR}(rKr@-q^ppl-~p^c16tC$s{fnfiY!6N%ThwCSZe2!S^o)SOFa-Qr|(T66xB?& zd_K{%{SQFU4Gz#oBxD_v&3bmdEyD^+Cdq+T&(v@g*A4G55tqGYTPiVoFJiR2O; zItiHRIHZwwvp|x*%ft>`m5%Lw)4_qHi?SNogs;|#tj<*yt zj(q!)R2@1=D5@W(>E?8$B@|WF0%dd4vAwIP8xTmUj&v!mbh+MOp<0Q6BJ|mI_l9#& zXb_540_q45K?)+Z1Ixa0}WjhZrnX z2&Klkp|EXW38P4Ki4Ygvbe|xEQ45TO0+-7XlgdbmZnp zR(_o2{ai9mJwn`@Tqu<4kjU-O;Z>p@p+eQlof@JNU_DRW_Ggu-cThoLDS_38s03CY zq7qnrh)Q7fAu55@ho}TrAEI7a2CZ;*KYy^AEb3ZHi`nC(x+A)$Gjy!0ci}XfIHY$u zA;5~q+p@JlwAVEaKA+w#lL(}AI)jX1bbgL~A{0eHRa9u23QbjET2{gPCz2^X=DrJ^ z@Avt71h8HO7BPSrH}R~~nele@8?gUC6ZY2aho)(rGKtPN31SA8`I}8r;b249CY4Aps}eHyY}qs@?DO5u!vF3&8=8C ze-@s6_<@{bZ@;?^(=Wf38Pxgp3YweZxceV(z#X?N$}+bQ5sx#O3}W#%Y~Q&TPd&FB zOMdwTQmGXDem{dms<^E!fggVRK1@GALy6 zcNt+>66a1h15?f!&+0MXp5>F}w06_9Jd$Ma{oKv6Oz66f{{1R&_L*aG|JU!t3r{XV zWkosC=`^dz?EM@`bMHs*(^J$dz#=2wmPlgkn31^n!pY2Fk`&^*%d8u(na9%LWn?i# z(nS)MHn{pXJ}|_hOT#SFI)*DHTRn*o`p}R;ZI`NZO3CvU*L0GX5Mt&

0*lC}8pVI)bx0sS$cx*t0QyjAr>Boe~? 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..885f198e86c68e650515742aa371843e7d7585e8 GIT binary patch literal 9466 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 xa)2$JV-^lLDDqr{{U{nVXy!I literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a13ec44535e89a25ee40bf0a45bcbcd0090f8615 GIT binary patch literal 20295 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..07ce2e4ff2dd76693d3eeae3165e8fdaa2c09619 GIT binary patch literal 2497 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a13ec3af62b9afd6ee48c2091c008b8fbd472b6f GIT binary patch literal 5455 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 + + + + \ No newline at end of file diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..61773151feeda32a49cbd89c0679306003048094 GIT binary patch literal 1248 zcmV<61Rwi}P)6U7h@L2}3%g7`tqNmno)h1~Qe{tpIEUX1^Nha4n? zV1ne30l~0QLKej+xGsU1>|#jvV`h5hy|Svi$INDXwx@f$rw7>wZ|Uyo?pMF6Uw!ne zb>M$6oNW->AVvs6=~C6LJ#ie!o&tiSeCav4oTE%lo(l-rAcg0=mHQ2AI%}IffSdhT zt3PwdDm-JY_h!tkISm4(+{?<+XDiEp-{1?Hrx_YVb@It+Kz+d)JBf&$&}uj`>4RyA zsHC)d2xLw+AO6#b0um`yrT)~bN~z1%&=lX{@g=uF%6kmQMUBha>gmuT>nXp%G}_sT z=zPr<5CENm0f4_gE0X1@3$@utCt^XAYg6wLnO6X@%crF=2|%t;T6~j(%ycXWAoWpr zq{l3hYGFT;eLtQ~L`Q~+Tm=kY9wL~I%@dSw4%jjX9(%IM*sKnJW+52{nxozc#uA{K z4G4!H!>t=kp(KJp#d2(| zJjWrDl}$D;5WI4?3v}Qh(Ca7pxRaXPBKYAQW%zh^9TqzVzFcg9w+hUY6t=o1K?W1x4Mw%3AG%paQ+W~ zo3ml)#aJLxklo?=Dc>)5I}E_NHOGZcDUnlW2!a>>^#?@R{W>B1K`R9z}X)HWfTJ9KY?r@RMKZ8VQ!&( z8Iye{N}Qqd10fk#>Ev{BIAdt1Xb|KJk4tv}ZvT}`tDLE!Tn7B}RuB*hD?5BaSerlp z2zWMGvU^lis=$(k*G?it$?XF{Bl}{^4um8-DbH<-Y2hXD4!oIc7*_4@sJz?(k>v>YjjbyRS+20}t@Mn2KvS&4N( zabQ0~XsWWg_T(~RW(*)UW zQW_g5CVKD>N zrFS3i;o+cOol&MihU#O~DO%!A(J}56{Sx1w;NiEF$h;#hCeZ+25^7n$~mde8ru-$nCGa+&S78+Idtxi*PMcLLqJO)avr$Y~t=|?*Z`< zX)&R-GpwH1An0RPasMQnp8di)Es_R}qkB`j(H0000< KMNUMnLSTa6Cr?)Z literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..34a8a0713fc36873558aa74989860f167ec68796 GIT binary patch literal 1816 zcmaJ?c{tmN7L8I(+A<`f)KC(PEzQIlM%D5YF(FDjRBCv!t9@$(Rqa?}szvQBk z*nOL&e-~KzTW4-d%78$^Z!H0nD-l!cg@b_-9#S0-DW2XHoEr_UcXb}r2AQ5ee@367 z9Qha{hc~QkT`^yMBdC-H5Q{OZB71^XB=iICg`K{BC0i>)6okceEr=NoAb9oc%I;fJ z2-CTRA9i*LQyK>&9HWnm)Y+v%%B;aC5YikSU@0m5TL+r;;P!Y= zPrW-g*8!!6LOyRjG)K)1?ejp1Ap0NBI|UhNretUH*hyZj8o#gMsCkv(MBJd2|6_v( zS8-tthkD@_d`Mb?tO_?WFYhMecl_dWZ$RHxv`)- zC^s*jNm&3%C+~+(hno~v@Rs)If@4-Beb%r{DLl(JxBoixM6h_m=Qc2+?!ga z$5h<`T4rce>Oe;D*i5RpKp$p&aJ9v>w+)u-NFa#$VZe zh6qLGcw*7Z{Z91@lLf0<6?`#q*QFz7=$E~XN+Wv9%qI0MUkmACR3J&*GAY#*J;Ep< zdm~HZzYC-ngzARmjruG4#S0%7pBBA8iJyoYk(CDRnF@v5|KMx~fA9=={IhfJV>RbloTg)oB9F}oVJ5<62) zS2XSlMwC?Ku(e#E&^TXmV+iYA&A+>S+9&raDSy~LOD#SoVx=RRG5z-UflooH<%heI zqSZnC_>`yN2Gb5{mxw{-#Yt~AP5gO2N9OJI&QE*4T|6}!+ct}(Kf)gfm)z!My$fHO z0IH~$;TO7Y=5HFO|NHUG{eo$=(oRNP6?m|-zE^k1C(!Rn%XLQz8U{e}az$LAn96B} zR%M1242AD8S)a)Allo<`nxZkuGpuNs9@k3*%P0=3xjePLD1Gb82ZONBJK3K}-4xpQ zEUG~unL&CT!!hI3l}o6OhS;Y$#r?kd@dLfB9 zi#|V?Evpx5_f|s;|Jotj@S3deCyA(vhjlI3i}#aj(Gai7?!qF^_JJW&9Nv!UASo`b zU{ivY#k-dtz%Gxo7ogQ6VB*ct#gud;M&3992zZO(|s;wb!+j*GwzK;@kHKPSVPN2 zq5upYjN~-Cb& literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..77a3311a7cc023deb848d784c5a27fbca9f1e791 GIT binary patch literal 1022 zcmVgUnfU3197yxD1pKuGnz#pAm;*AISjweL%(Ag1?!EA2V0Vr~%P;=sF&(XZlpKT#5#_xvAAwKr zfOI}ku{i3a%uI2o5cR#_ixfK|+vPoW1IS>GuLJDfoK_A|cDozB=qCLh6bb@2x}p3! zeEFXK^CD8aB-IWhWl~C_3@!{oUb+s9j%j7u4JK0#XpX~i_HD=_Q5Tt@FpDFvKwJwy zOMPWSn#JJpIB0e{851gvC(0A1Ybganv6)~kW^Yquxq*cgOAv1D{-zC5A`shv)Ehgu-8j*W5@%CLbvk4r?ehAD3hh?goZN8_AgLb@9(7Mp sMK2D|ZNKCHJ&fN|Ka0TrPlfEYe{XoO9imiMd;kCd07*qoM6N<$f}Z%+>;M1& literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8466930ce92c7d25280fd4b7e88845ad5ae0d5bf GIT binary patch literal 1196 zcmV;d1XKHoP)oWNn8sh5siDkxw`aw zfWeI<;NQIX(-3(2+ongQKB>5kh?Z2cMUt3ktt|e%Ma9E(-cy_YgXd)t2Ai;yrO-bT zw!ugOY_Oir`)X6#1&+pIETl%K2-{bY1jyT*4gOWT$XlvS*m}58#1nBm)J{93P1t(I zf$`|$3gU(s^~QJ{0@3K&?KnRcxuoc*ZEz`C$CE^+RDyvIkCIlZOeDy=0Qp96>5Qt9 zfG)ZB<#&mtn~Z=Yn}5FsICT@?qmKbv&D84DChlXFf)BKS8ApPce*x<1w*dRWFl0Gm84Aj!6L#J^RM8bmM}wPyzgab!dWl@HaIdo zdF?X5{BuHAiKR~T7QCd_X6ao`&Otfknk%(L2Aq=wkP~-+g*O1N=Y0@pV&@0CV?Hc7 zDf?JbG6`=BNMeeftdvaV2=XNm=q*Q4GDL5qN%lQ^vy?$GI{_B^`g<_ff9~i)0{bDO zuMdL!?rVS-Pe}c$iHYQ=LfT56zp~e|%9)bG{0OkxV2W>83V^$SfM>ud5<{7-SCAh8 z$4+Mhb}Lg>n@=FC4+MtzG%&;zUMOY&EGCdK2u|+WcDg!22Jxdm1iFpb_#hHVfYs0R zGZOnX0&tDcSWl-444!kL#kU1lWZQ|KiV^euRIF??)DsbDG4~9_l}nTu4is6DMbyb@ z0uopB)hgiH3J8O_RAfC)!41Hj-C4Gso;*vD9e{wSpoVb>%x&Kr3V5-n;p9!}>^&kT zz}1}6ak@Hl(FAC?K!=WpOTy3n1?T?NTBfeVW*hM836AFB67aDwwx~u?ZD@3oZ0Hs{ zw{2RF07C~d_l352q27HV4-0A9h4tly-+B_@_IkNKe2*GU+@TI{uRBhD;JZnLcjj#R zElViDWH_z$S#4FH(`4i{ zxj7xo3Pc}9OeGpIwT*GlwN{tz1+^+h67ZAD#*8l4riE0;=%6F8v+Z`Tux7O}=uUQl zH%U6t-UnS{*0aHw^+>^E*12HJdZ%m58VY{54Kem6^?L;RvHk;8Wz*1AMl~e>0000< KMNUMnLSTY+;XgwF literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..85be2200ef61fa68f9591beb8c871e5d7424df53 GIT binary patch literal 1276 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..03028f928941c17110415e1684bbb695fcea0a36 GIT binary patch literal 1404 zcmV-?1%vvDP)*-ksvYY*wc~h^u>%r{IECc=O_p7Q`Rqrc?HqjO< z1WJGspiQosq6^SmI%kXAf2s<&z0L+H0Rd@qD1=be-H0?1q`q603{v1_i68}DmI$NZ z%l4ZTcv)*O3cjp7M!}brixhZSmt+)tS$U0uFDtiE@MYyU3cf4}Iw6CCPROYC&jt^p z;LDO=6nt3{bPOoc7@*oedz@%4;M)T8M&z|z;ok*0@B=5nFiiF!Jt6;dR^(0lo#qS% zz_ZT-PdpVI2{!Y;ly!429rdljOpJ)&zY*(ZXB$}i0;o6npQ#rs@Mi&rhnGPg_QGY@ ziWZcww2a$6fnzC&g!*U99(qi2FUZfc8{lz!<-WSwZaI z&dtc2DG4U*>#?@4)?>W?TH!2f5Mpv~^3lUUptBzR?I0k5BJg^>r8E$(-IV89Zs#0JkTH85m4-2Y;BGZBBN z^ZUJFiWip`vhQ{dc|qkYz=fQBHi6ASipWJl4-C%8#s_f=C}Y-Z^Fgo6?&fxPFZ2Co zVvu#88vOmM?H>3iGtB@F!?TiOK#k4d`n_FzyuOVyhW5d)G5DWu?%?yS?%X1_xJ;@4 z@e?p)8vfyW8#g<9xN>eWb31GId$`f=q0hNNM)hB!`A7KR15sDlNi zwX0lvxjIVTgtjiwBo(e7t;IZxFS84xT=f<>0a{)WCEx*Pl_V7z11PtIWXN(y(0#(% zlmI0_Th$22r!U=DKthA?oSz#9RU;sdeoQoakyNcZmEEQ~vn0%cxYL1s=L6XDud!Aq z;ZERZbHH31z4IsNy|;mlyIh;Dd+E!OiJ1F=SS8d5)dq6{GNt)HAFkjSV6EJ9QyJ^i zC`h9qB|w`Jpaf`B0+axheLnce^(q3-j}Ob%M3OOA1Cq*wv*#yX50N=&jc6cKk}P;@ z3Ff1Bfg9K5Ye8;4*j8;>3gyB(GMw;Q4HaU+0bq=7(hK#VQ;PfJZVPLl^To8IeD z@w6s)xD|{}ouZ$9$iJq=@g>71dXt{4BAF`P-5X-R z^)}V2+@`Ac3G0p8dZV`9DIYiiJ_!jm5YUMg=pHd8K${X!+4diFV>|tzjy`Gt0000< KMNUMnLSTZ&I;lzk literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..51fc3f049468e9eec99d3b582d514f9ccae78167 GIT binary patch literal 3107 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 VEF0mXu)qKS002ovPDHLkV1m6XmM{PS literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..eb05e0c84555a0ef7e518038f5634f8ee3c8d6ef GIT binary patch literal 4089 zcma)sL@OG7KRCqqeY7v1W^)GtQ|obx6;Z z#ZYOdAQZ!|C47vJT8#%wE%<<;XuksW6iZlOd<=nTzDzAJlD(+h$XppCy$Y7p-+-CT6oFG{dy_p=VIL*J{lhvq8yES| z-Cla*HVca0yTHS)b|j03?^;tcQyRu&cCm4pk(8=?QwX8{_h8+7K2wvS^sa-8MkHy| z1$}s^>J9=%9c$se2YQ!z*RyBry;HGK6mIYbUO@SCZ6hn}h6=_E!-u{b1QK4l0{H8# zhv~yy`NzXc64nr*rZu_KIUuTMccF2$1T`7 zr4*uZ#vU%VE?B@()dx34l;pN6ER0pJc-Ow)jgH~^FkX2coeIWj<&L)=vg&0#_@ozc zrRAEqTt^i(JhgCiWwMUO6}LlMWgB6jG-kr)hY|a!iI85+O2i8x@n30_(aLUmbqdRw z^)2*3gl#f~yaiwadGRIX=IGJ;#`ycVG~p||N@im@mL0xuEWV`zF(%ZI5^nPc_icR% z6bxcvHE;MWlH`y7=k+xx!ZFt;&gSak(RJQqN*vc!tXJl6V{OgCd_PPibM~K+G@$Qe z?N2nZcH_ZK{{1J44#@-GQUDz88w61|G}xsaiqG+J`R%nAP@HdZAKXDs$9PjztNko` z*s}>{mW#Up^;1ILG$&3k>SBMt7yEH}?&t25MNHKqUwafAaj(}#LSW(R0>L9Q=9MVzD7AS!qL~Ql!t&=rH?-{xeC$fV`Ly^>yY02`Dd3jDzzMdS3uHCjR z7qd958@my|38PJ5%fruBm=T?EY6mn@SO|S1xBrudKauTUppD%B27S@egqsM8z28rX`l%2s-`&Iw^UniM zDd!LrMI%|Q{EptU9EVMCc(M7l=xU|4yr*XTPh&!}q-(_cIWeOj|MU4YaP9 zd!yl)iM$IxD@n=0*0ua{-ay33Mb`ixZ4zW%_z3@2bmkC!oIi`pkYpg#pYWs8;xRQI zrHOf%u>&YI%O$);@V9GPb{2R%MmzoEA>>BTN4%>$%n;nWs@BQN{Uvfy@bBXAyJsX# zjR${u8n}mMw<#Iq$4nh+F>-y+6eb+sGcVke>VbRgxebN(u#>Zog?DBlUQd;PLLvGXJKx$pM<{%U zeyCd9hYdLOaLV$4)^$`8vnRT)+#m#}3HUH0-qfR3IUK;-?_gh@>38Udojzl>lvwoR3MKf`&-SSwe~&75EZcQO9Jy1Y=SVr4*VMsFOXQdn{V@& zNSKb&#S_wxCcpC8C%G@A9?TI|=bh$!q2&{}K&vPy{pw`?`G9_p7;3dTGtBE}{20w1 zCU^chDFHYSAly$CfuROo&3}!hom!m(2&I%W8&Q!I{MAC+EC>5ZTma*#)31`IPP!$m z4>2Kw8TBB-#n<;A!{<90X$Vy%nfRysgOz5jU@A{*>X;Xf;lwVOcjm^qxO}zx$S890 zH=Ba+zQgA!3(s3ypOyL-D_E^DyVfzB!S4K-d75@_l-T&l@6$=83LV*6?|L>ltJSl! ztsY11tr2^Aiy_72`dZ&M37Oe@+Y#x*E&1!rldQ>VE@Cktiq{><6sF5?rI#}O}i#z4sj1*OvX&@^A_Ze=KZY{@QSL+-(L zL}!?YRag1bScI^sXE08Ixs#!b;er|x*M8H@6YqkZWI_^KisXPJ+t29(e_N+zyEacaEu|d z?r21Y15~LDz+=v`RMkzAu*gGs2qGTRTh?f^krLtmk?wH}E2IY=)0vSYgDp>aBFZvo zAk-r!>OA-8*VTQqJMb0OmD%WQkYESK*&xyC!=X5*>~S`AKH;Lw0?dNa-CX z1?lYDOR+3ugp3NE>8M>2I|@ei)Ql0z$PDREa9x&4ah7a7-<&Dh(oLt#6imJ#Q`3#% zlIi24?sC9J_`5>U+`iIFU<6Q3_Ca6MpZ&oc>$SsyQ^V_0)pKhpt@{~Z6EmvS6YYR4 z)EwkG#)!S|JTn{qmYnFAo6wwc@L;S)fEy{~vzW|1FoZhTpM&jr#TcA@`SSFhL=U@H zV-57K%d^zzJ=7nTjzI2kqasy$RwSds=9yo30ECd2+mOP9acIGvJ-wKzD5&9naq}Y! z)xaHiBT9#dfymopB~6sCI?^P!An{kC2_D@2@i=BGPGN#iQocsW^t79*yMo9DclK0| zrlTpk++b{-EJ$teT9|~8*O-Eq!V-xGooC^_nk}cN@cyx2;)ug6Zv*Az5$iUW<^W>i&@8#)~b^{T7mn zB?YaW#Hz49V~NZ%6&1hN zg?1*f2B=>IYqA^Hp|=mauv9(6YjZxSCUa{B1p;J5N{=cCKTm7yh2>7!E9xX^@M{=^ zwHRjY2;WvZ0a1MkFfuwqc)v2*I>!?xNz-U<2eZwsWO0)L!2b=rM->Yh&t}Ue623#n z&j@uM5VESoB9eN7TC~#2)1oCji@VmjEtE^tP^JmxyXq0-t6KDzc{f zjWpKdY0B&Xl6JAb<}$9jc9ohWNrPKsVNDW^0!{M6QlOIWB|d*B)K_vY;zxu7tw696 zyCH0*M6}HAhq6<^N6*BNOCBJk%B97`Yf048!O0OYA+@HHG{=KT?bcRD zC@kE++FD(b$YyPtsOry)jLn6SdL_OVUXx`~;t=eg^mNfA4-dEDa17%nAs^$BiLARq zZ}H79K|th+YK+`(;d-T!6f?{0?lv~?L7!fKW5^q7!INDxw=AJ~!uL@piaLJ8=h2yxj;HsbFuWvcB|*UwErtjB&S5k zr{7}qu@7vo3gd&l`0J_~swlF6)$MYH>!Xpds$4@`0RScOn-3$M+t)PN1pE8Cv$!Vx zR!?s=Jl1!Gq04b1hBdF_NcMY5!S*xzW3;gI-nQCSedgw!!DyEj{UqXR-gS`eL%>Pw zSiL%V*3j;%71KliL!jWX^YoHHhMWBrG0d+MJWad|CYj_VMeJ?HwQN7p`#l^G!VND^ z`$UfU?twmegwuY?&QFBW0Cvw0y>fF;$HGUU5ufdip7?H1GItEvq6;nBZ1{pV4s2&3 z95(WRuF%pzOJHu|*tD$MFN4v70VAeTv9EnN#~7DeZM3Vzml`a3h9haazCC1=%E#Da z-JxF1Wmj#S*v-fW)BdI8C&z1FPe(`b@-d@({ppK(9(Fdi;K}073^`K93+aWECN42j zJafy|krG<$X+e6`^Q!D#9;ncQ|J&H&L1x{Kgf*M zlXe<_&=shFzwwGDNyeq;zmvL|pX%9^eXSOr-Y7;4aRdx#PVptnBVl>QK*~I zB@xFN`1n>`-Mw9^V2w;hfz3{3FZ5cJUt2N1badeFcw))ihHL6>WSbba#JW4D$}9$O z>c7#6T^h(#uA*B%@ztvLUy)6ng%x0Y{bFYvZU0W+GuSNOM;XHFDCIhlEL0K+mv^$dsU^v|sh!_4wsXFt1P zUMmLJNn*)q)&bS1w)EFO*fT?5GN;apt06~^4f}aBEMJLePt6Qas3d0)h~*XMKG=6| zj@!YV%F$K^4lj0lA$uT96d_}<3rEv=fnm>0K~zL(t7N%D3Nv_W{hr149ZE0_3Av#> zyS78`E2L=a{AXk2GJ52f8J&PsQA28%a)zS6W9fU|^1@gnjfikh1E^KbM}w;0JLv(@ z1_Llpi;NviagnHb#fm?QBF~zDJ&&5?KOTdDZsDVdC@rjERM8>%?MM%3s_On#tzr}P EUtU;v1^@s6 literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..dc0df3cc724f0259b719ca9b06c31d5867e5bf23 GIT binary patch literal 4917 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 uC86GSLF1O8%J}1t literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..002ab1934e78c66cf1d279c308db9fce6d74b516 GIT binary patch literal 2424 zcmZ8j2UL^E7M`DELm)vSC?tpy7XgtLii;~q!V366TCh-5jFixefb!~p;j z=KnIW+nU?AI}))q+D-b30w6{-H!(gPKF%(*Nxm?=m-%e2B~CZ9?}*sr)RGRTLmJ2* zO%HL@LxxPr4GW!C`oH8R)(>Pis%KGWtUV{iQkyV!1E#VviTFmfx?2O}5~(XxR(?br zXRTn9^8%Zh*Vht}8MXL5#we(F~bt(yeW$UIbNwwxJ#G2*bx({)HX}EGGQ_%@0T7Pw*&X z@O?{H-K7JK5y>c>WC2AF72YkGV>P{#=d8SKuVZFg-29K2PFI0<;(LgY>2-d$~<$^-L6QVTl|>nK|3huuntEgSBp1I zeO*1IYy4QJU4t$l^Gfd4g_I0ML>2r2*$G!5NIBBPlfP9nqc60545`Veglppe-glxOX zrQ8qG4_o~`_noI=bk#tReJh#wQSZeFeq!HY6hV?w_aTKW9Lz2fG}b?4sE5`~E<1_u zBrZg&@TMxy%C1cozKFcU_i4qY4~V(gXgAd7tvxYCeq5jj^FOZME+Ph7gk3o&cgT&`#xzFvN%wc!tnFq2O8Sy) zsF^oe#Q0Q-v|}sgdw*=r-M9chpTR}qql_{~aTYPoz4%#l3z-+IRy|ygZ6CLbeQp}~ zYEcm8xVROomQ=v@dpZRzug-biTsddJ<)6q+`yt40F6*2#%7o7_Q{81M93u<`2saap zxHIO*qBd)+pTsOh9H>CDlRAYNT9xww4BzW<24^dcS;W3^{MPkf!nOYhNK$ZasdF@x zWXyjO5X@!N3Q(5{zRqOd{i*ZFUFjNf}? zT39{Wr7Fr1Dws1WR0^SWovyZvPv1zN_fMt!ogPfVtjLMpgevwZU4KxNxch0CP_;Fb zGoB?mDn&0%5tT}3-h*3wc|$jpUzy?Z%|p^u`jr}SZvLCqIM{*lr>AC^t|WQHo7w}N zASLw6a0yXo{P=bH2@er;);xK^4G<&{|Kv+705GskqWEB+_?r9rUGv3hBHA=#mc_u? z7wfS!!;Gl*8*U&NHN$$DbZ&+_tWrc4Yz*l%t9o8G=Un~v;mWWJA9gf%z0A|1*hD{4p55$`DXopJgAlYB&0W(PnE&zh-}(P0aof}r ziu+dElxRmg_yGKc)A9mRJ@D#L_NGckKpED<@j`CKg%QW3yo)6W?atUSsm8gojc{{H z3t!tNDhi9(FeHGZp%(9-{e5fs3KVWAr+hTjvTNhRwb(K$pnfQL<5G(fLaxtS2nm|z96 zNLj{|D&BtBJk=+DlEV#O88toBn;$a15_s{pnu#mZ`Wrm_y}j~qc zMCD19T$LylIT?O0h$>yfj4d}WW^=lL{-ioFV?$P;sQ)AEe(n!0NjpMQEy3n%L3Kt! zfE(Keab&=KP-Fa3Wqt3z4Q7LyS3n<%lvzT=fo{vdj483eN$!s@fTO7cnG#_&4oH(u zt(u6zvq)}G6_!t*0(Zbdg#x297j*YyL2<>l=au0!o28QwtFja#K(DP7(jig->U8{z zx%vodB>l`%aL#I?|HGF#ggCz~>OSgepSSSQC81GO(fQNQyNyP1dxc)O%d&U!7l+Ab zvVUhk{M?#nMQCl)eYvZ_hg_?BRD$co=Xn+C>7O+pC7!417(b1;;Nx-MU<5xZ zMu>KF@;;b@9z1~w%FyxC+BDoLd?*^=IilMd + + #fff + \ No newline at end of file diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..331cf0398d86ec7843ca7cd770bdce1dda449335 GIT binary patch literal 91812 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 YzZv5W4EH`tKmY&$07*qoM6N<$g5&T>B>(^b literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5710de45693f392f5b0a5ed9d7b8057ac1f4fa1c GIT binary patch literal 46113 zcmeFZ`9GBH|2KZjSV9XCQbL8YL=;7qX;EY?`#K6qwz4+1nYOD!$i8!>>`TeMjTU>B zh_RC;TlU>Bb06n;f8L+_hwp#zz2|y7TshCzah$L9`C5*1yw=gWv7hA#3jkohn(DRN z0MNsK(gP+&_&@wHv1b5$o>aSbS?_WD-+?{R$5gx~ch&>1KMEfeczR9i#6+NF!{xgu zW_l*sc`uUvkUhDV;xoUKn>X9~+rrkn@~!5p(`<4Rr(M`z1wIc6dH#IlwEBeiR`u}j zhh^oN$bU1`(U^&qjNG{n*L&yV*K>+9Rp68VzdnS$4{Wdigg+qJJut;4zu;|SwJxV916PUSUn$^2b z2QcZGTo}VyGAeN7s)A*3E49j}$^!t)pjK32E9rpImzO>QKk(X0=3)RmjMBbbq)lQm zy37Xj)`x;;NVX>Tha^F@^1lqpxp-nOmfw<>KN>})HkUlsRKlJDplV4d*?m#oyh6D8 zqEZN*-_FtBUQl>WI*Z&rrvd*Us+ejXjyoe!$qIl#wm|m>(Eq_6vcnH0Bq~!4bQyY2W~I?K6X4^f5qKb8Q+sh*+GH7Szc|nWb*RPc5bCah(Bv}dcJ6L zR-XRC5WXjm6%gj9IemOhhjd@fa02YtJrDIX^`BB=U(Pe*wV@hJ2fmROMH+^C?a}b2 zhu&q*Kb0hOiEN)wsTRPL_%Nf*50sf#$r@r=H=sU#pG%^nnlR+my&yp$t*g!y80HPH zmTY?g{`%iS{LcRxX4SiQD`AZ@mwW+a?Pi-n-Xs%IjP!fj{`f3U_oO zRu2J?j>b;pMDetdBlig$1Tz~S_megct&kKhMfNeFfsSMD;Jv);G0gw=N$Fv!u1)3JBGNE zayfyHL=rkT|N1A#H)DOOOow8iE(q<5|8gJSL$TSjqa<`tgU_@UhXM-;^PIqjVO~NQ zijjQYr~CT-Cq&lE#3@T6i!PlR-Vp#Z))q7I)eAkXb=KfDI26SC<73T4HE}X|$XZ~2 z=@*Ei4wm>py5bUEBTn-~i>~{=1HTc{i*Hyf&o+N(U&!a05X)q*j<7==ACv)r@NTmp zlW6~>;x*~#R8%3p%c@a9QsLW6$LD+-+?*@rq31t#wyu0qs(^aV-E&7pkG{m9*4dlr zq&!fC3ldqbSfJ;Rerp&(N3HP9pL3P5OYS56k<)Npj0M2SMT#KmRfsoyx>=xPydESAXEpIhGV$+D*-rkN*4(ZvMu|iLpIdq#ZCt;|bKjUY*I*1`BDij}1?Kx1W^!$mKMZ`mBTVjA7!-`k zWdOr3yyw<9Qb}YD`u$AOjCH%|z?NokW88z*&#m7n*}j^* zw$Ic_Xz(yj9Thsuj_1BX)+~(k^VKE0lWk_PE1SbiN-JMGUGV&L_=L3+p?h$_9kXeD zq}s)*N(l%?t1Q7QGix%(1mjpn0#w;cqM0mBz-{OoRY68 zIDYz_LjS!Cym88o%_%y6GrhC+)OW2f*%$8|RXn&6=SZsL;)p;OsZ#^889%zyl+8N2}X{g`RYBdM3~fV7`aw&yt5f_S=|iDX|&1ZQPf?CgppqEHCU= zZW*>vJCS0OzyZKfGoy^kz#2*;OZ8{r{r+0?V`}L1wT2F2y6YEdrSmUq#;cWQDAIH@ z1p-6XGK4o%^rB>qzZcv!-cHbgYA){UmW~n|wq*`OW9h`%>LHI0gx5PbGL+cp>&rh6 z!#TIh&yHZn>ivG|)60sT&|Ay+-?c*=K=wvO0B_}b;cWrOB&{TI-v9+}@!_kks|Pot zIL4x4PS>{^t}7UCMPE#P<%$B9x;z>AYWY7a_l;b2Ei65< z&3$a655~d4mAJCC64|g>W5k($l>+cF{np@-Qk$<>Qu5 zIm;2>)iVb%?A20&ATurSR;Pl{h|QagKA*(3EgM5d!8M`uJs>&10ew^#XarLx_Gfo& zmB%uDGeE1n-%@MK&q;Cp`ju-3CeY+qc6t?I z;LleoxnySClGhPg6#M`bTtg?z`yZfaMv>!q7d?eSiem&+XRjv1_iHs==ydO(_lb0dShGeixL0}XGL_ZEgetX8oqfV zc@8H7)ClZv!2&ktE~K4U=9aqaJW|pm$I`0r8d1D;v^T#XZMLbsbxn@#89kxTjCniY@M>!;Q`>A> zyRx=Q^+aFz8XW*ec?Zr$fVLc#S2ND%L_)?(wM=i9PeX$gwLTFR1pWG1^=5d(KdZI} z#@vgUw^0E6m@-kYt@z^N*U^V#M@i&zhj?Nm5pq6NP|~nEJXD)blzgIm8S09FZ6IW$ zDra63q_a?}=*Rx_1aF_c6!YkbB$4~A-n=v#?gm1|Wn#{9rqj${U0iPOsSHI&+J-20 z@P&~o5wsO`Y(s4J6e48$HRq zoljI75aw^5sD_xJRXu+NahZ6?-j}pttmiLceKm2S=&dXo5ABl2ff9bU`Hr0dSM0tP zb6>Y7>O)e_=(Fo+fxzJ*g9bcL3Z;t<|I`mkGhJ`YspEE{y6^!oOrsB>uYX04Da|~x z-N(0jY5{Ubc%4a5P($%&(z~q{dhwAWe)=`k$xRFUBYAWg2l`ANL*jL2-wTRn!UFJuvx!B{%WN2x96lX8}# zA|b_^Hc49iN9!10&||uncN`=L~(V7e9VODJyezO z?LLY1){0;nS&$&w;nZFYr!0kF`=<8XzPdk)5!cbxO8n2LCvNPR&bF5C(y3%~gq%8P zDSbTov$EZ#SvfV(3Io==3V&Pqs1VgAXyw!@_3Ozm&_%$$Qz!>L^jHFyj-Cyn(KVsy z{aW^p2EVDXAo#oZ=o2w^G==iIjtx;(GT|B~aac+FDaJjih4LOMs|fj%!p(P?8PG;bjL-m3b;Z z)+OJKDccc0i2|BW=m{RQJkecjyg&Jp5TWSTRB8$9vEckpbMq}t9Z!VO zZtV2Ny?Vu;6fTt+9yej(1*P#jujoSjno5$!4oqDC4wKQk`N>jM={)_V4p z84YFuW=kGsaL(rd#W7+#t$9>?x+xt|h>te?A;qn{quX(@>=3$7!XSi5D!FE8&_JAi z=SOpd1&b#gXtzwd{DKT8TO1!LHDB9?m}6B5Z85+=bzr0lH&QjDC>+~})}}kq?R<{< zd&YXA3UWT@buO34M!@{7$v>gU4mO{&ZO57X$n%b)N%TcWgZ_~De#?B(|F{?epK(ep z_JSZk>%l^vDaz8ifJlBjjEJS*QTVteR}Q^%A`9J)_6r4xCsAzo7M#7FD0fzU>I5`a z)8(7(P{!AHmg#ql#X!Cua{-JDgkOAoawveB?wZGh#f>7&!{RI+ya}w)l@XIRAj6qiqlE7W{DL zZ-q*oJfpq;)l66yqU56YFfZhU046ier&NQK9hx>U)n0;Vx2Jv_Gw2Zd)6C3-5p}Bi zUdo;7Lf zUzTqA=`!jGZ_uy^BwMWtS^Z;b!5eau5UPXHar);K&1eK&##W=H*Z;B*-ie_?e@XL( zO1IV7%CkIY(SWVRb2$g zB!?D#R?xO*En*i~p$A`Rk3D^YdDs*AogncaQBN`t=Y?>9RD6A^Uk?)nrXR2oLZ2=d zLVuPsYHCT^j_m=u+AZc~i)75SQ1Dj^@ z1+HyY$dlvORfGeK9kf8cxD|WypZn~|ksn*fq+YNbM-AUy$a8Vpm3#2?X=b*Uf`q4R z?4V3zJLIz-T4xOjj${_MKjax<)KcI!=^FOK-Tcf`z(1PwZ#m(biEzL$O_ID3QJ|rR zK0j|?h`pe+_kkio6cU}@A?=K-CCm&}1TWzcHmVResuMx^B?=Ozj{w=xi+N!{Ukgl1 zFVYzvn{5s4K_g`Rf(%8p%b|cGOyTB z@L5Kuo8&=JC@AEDsYhewLarz?FnmfaSu4P9l_()}!&!5dN+Kn#MfqN+pL6biLdG`71>qzS`nhQPb*lc2ow(MB@mrocq`H#A+cc3)R~xpjrqVnU zqdyfynK1y9W;eQw@}zZ9J~u&$(N zP`HiY2oN~be3|%O!7u~VOEXlDXMnEFP>_z(+2Z%)6GaHXb5W-;?1v-C_ma_=aU;W2 zv{5jFuNo+^*vi{d4`18c*KOh35dD*Rf<`@&Q)T@lSL7sBG=-Y8w(8*QWS`ng$nzab^(s9<)O4IjjYp_+rY+|`ILu~pBDwU;6t3N)E4eCl;Rr5* zZ>BUJoL&4gc5KH`E?7(L^>jMk5$2IvQ8OAZ7j5A$YkIt!QQ@);I;b{R(?WRxdxz*% zg|a-|DSE=|rB~u#@FuA@R-;s5ojoC|Ph-o6#QWD)jsE-Ut$`Ny;*9VA;6j1$-GLvy znn`U|#y8dUTGC=O8gJ9$0p$7z*iB23ibqZ!zUd14|SW@|3w(Bhm1Z^H?^M>Vp}aS-VHhSOyp|;Uok=Wx3%Gr>avs zddrH#5|KPWXw`eV^Iilr&#JY?6)$Trx4q^Z-O5~%jJHQr%QoQfCZnBNfGv2R-N~JJ zBZWGkolf(*;efeBi~&Z|&y3VUuDb7)ku#rimzjaSywIUkcw?(>=k5oaOr;_I3tO${X?o5L z2r%{1L#D3dKD8l17|eR82!X-wO+}qSygs@ z_}$JZb8uXqz4*ZpRT2VDkHj@Ef150ooQ=9sj86X9^;7Qm)fY1<$SDdSxay6D zLlfn#KaP!Ly5E_m@9k>ZI+3&1DJ@9fzBaYekoCtKjfv1Iy0*F}ZRwk~mxdUF8u>$D z!aDy;L1YJDI9Yp2Y6kV{o^-Olcf0GYSK|*vf}8Vt9$d;u;(!_a6makf%kewjzLjC^ zGqP1vC}K!khrzhrpy-F9GG7x+ZkqP5)*d@bx{!nAlfiO;Obr_~J+f`{*8Ls!%t*?G zU1l*o`L#kxnhdYv(8;%twLM5`TsG15&LY{ZM%U_@J@6%z{GcafrBEMD=!mS(mnLkO z6|WF^c9m+=a5VUQ&K`jy%uh;ujZ@LlfqP7kg>^{=o9K%Hhak$SgsVa)!wZhB_Z$6> z9aPR8emNPqm8HcH5ZEu4x7E$~V#UmtgS>77O#i>~ z`;+yD3}%}hjk8exL25brsBqYvIj&&WD_-}U4ojgbzNIBZ%YdB6DH$vnaSaBYH@{=` z^I59LSqRxZY!Md(&XlJ}GbI0Nry8izymjGBOItjP0Am_DT?=(TRridJYX(d5Bt7Ac zi~sv7JL7~uzM?Tn27Q^Fplf&c;YU7i&+ivFGYs)JT}H9>2uJPe&nd&l+h(ddvA6NyY}!OeZ5tw$Yk&g3YSJR&}MG74c(2W7KQ+~Z8*=ipbrtf~a>U`>>N zK9&#(9_Y(it1GNoechMWo|uu;qzm+->$B4h7z=ux+16}>%C)FUGhFf3%=(JH8%mXI=+CN@Vo{zQuD_O&DM-Hh()s(@K6$GlYgpe_Uw-Z(uOw)MlF6*tjeH$$o7z2yB|o1d2(tKb|P zfY_wbVh1DhHJd{jsd7TegTGRxk0s;q{Z?63##Kq#x^(Qp?&A`m%yq?7u~>0k*)bRY z;X3u`Zka8q|9ja7!Yj!{-?VtcpM#Va+19m;TlVNGk(u+kJ6vJna0>Vw*$KASRg>1s zr#Tz~Se_Y#;@(_`}Nr`amsotLqS> zk7I3&jUZ#TFwn3Sf53HvYI-cmfSsN6r%-hJEh2MCAzw6k~oKR%U6ov+o82IQEqNp(P4{i1@AGMQWX0^Sg%tcPb*!gB4DCtVVOkztg*i>2Yp{? zrb_VoNQ*Q(5U#qDkgeWMDG1G@+Q`e49Otxx{Z{a!iu zUyT~z7q7fw1oHQhR1FOXp~Kt8+fO+5i(T8+D=y=MqdI|Cn%~ z)vo764|A{ZGD&1XRJ&+6vuYNNPF~)h`QwvnCb~@xE!c8=v2>e8>C<(7KSriI&yJVl zLJh{DVn-W7(=K8UQ+nU+3m++(Ek!H%7S%eel1z%E*d~#tfFymvwXl`Av^SdF)qnG!8Pn<$8yO zz+7g-DZfk+OL^KutIXx}#kEvf7VKju?sxL7o7t9?oynMR&U&~>Y&imMo}?-TQw;y| z%OHC`^LfL~?!=s)c`un=z-~W?Ahouq{rTny9K)#-JUfOlL$u`a5F2?hH}_$c*A>$) zQ?&5vn@fgH!m!n3Xp(*E)LFxG`v%2Vi)ACW8HqI=-r!M2jztsAPwWCyI1}%|nWmgq zBEVSUyuI9u2b`Gx~ezLr=yw;%@HCI7>zpQ@( z9x=lY=RMJTS*PN$o{FHebX#=CC8?>V4@^r?(%KC_;O{06!3u?LF|e-Z5<)GsC>1m* zwZFQ+lTp3m;pQh4! zQ}*CA7`P+3b_UUPv*&nPQ_WwVgFwr`8&egru7MOPLNT0IV3eDS zbLc;TSDUzL!YVK)HTU>r1WIL)@#8B?`HXGs>H%P}ETxX{RjtVxB>8|Dn^`AZIQ8Rq z>lOEeODCBL1qatEChA=!25P8>XAwHW^v7GB=IX{*CFXFE*9e7EXcYrk1DjHkJd`4) zP;VspC#Z;1VCvyvRYkT`FIIP3T8`cMfmgY#A%Ynq32&)4gEkCH6p-yOY6@@8In;T} z)Rc6Lt-eo4xpxv3GX8bhTE$Rg9=Lz>xQ-NEO*c9&8lPYd_*d_ivuZxZc?Ol>|t zL|EL`f3lGhX(f-zd2R>px*Q?QvVfym$1|;s+}71OLusK$kZgNB|A3L5zKq;4Vv+J1 z-3fawVCs_lPxRWo4++{+FJYm5$IpZx*^od+v}|p;sLY{@(i`b4@y(o2)n7XTjK>?Q z&Qseek1gh5Huoc>t$Kn7GHf(hRsv7Dr=qQAt-_n>As<#InsPch63PDw&l{&o@@G|^)RMb9R(C$x`lI(HHc zA~vadq|i&&be!QNVyG_}KwwjU#;)$C$1pd>Ruk7@q7NzE+2Uca=} zvYgS6OiVB``T`OeJX%bXweMu;X236>vi-?97vyrWe_6ZJHfg~?=$`>&YE0G%w=Jyq zIr!`+NKa5_B+!RFl~S1z^lc?O-c9GA;58RTW$GR;1H$t6lb(8qMPIsqKA@x?GvQe7&xGFK|*3hmLRXk zy>xE%Nb_tz8onL`z*oJQPmZIJ1e<|B+M6;sR41A}@y!}!+t?)A0dB^b65V@Ymgg&W`KTxARhVt=)&5V$PCGZMcP+tVvWXp+83_`(t4a6{-IB}^nPX7M z02Gr9Y2ttNd2J|PRl;WZ`9v4$BQCx~57p-X`sddBKHA_42r%TGvOTBO%zlalOS=bh zf1n9K^vWOMoniQD8vI}$KZtz|BBsz~i)qtW2*#tHGf-v`vpjhaC zDlDkAj4An+BSSM)@-C`cd}8gX5HF<+MG?$BeEzi-9(b1cO;3 zYa$P-sv1VwTW{S%SAReHR(AazyC<>?fasqvKfH*y2kAQIdz0%?e`EJ@g0j6wC+7k# z@kh|gPqreHe>ToeMHjeE&7CV_OU_UM`iAGSU$~^q7>@0(T+U->Whck`7`jX65-~6h`n?CkStASdmvzj)ap1-$JqlE=n zirgmM?bTP`)ZeOw$3~hAIx(UPFO+ii!6f6HF&EJ2Ll6`&sh=6WLbyGAgL~g{J&r>>m|$pZE`aeHh#6>D71M%2i3@7m;KhJ4$8BrldK$NTQm$)Qj`e)^Jsay$k_m4Ew zLp0U&8`^s%77ZufR35b<3zrr#C1+Lykl(^rO5n%(%_(P);X=7G?BueG3sE;0V8^6A}v+E5Ur3f9D|?we>=ylxB&l6~~EH1UrLHP2qMPwRZFta>jtR%E>FvFKKrOht8cfzgf{2 z`CFx_L?_CbNV}RI8T;E$>!Uf!^3ucg>1Ntw{BerGFmA6g(-3^=q+TGNz>iSs2o=6- z>G<>e0_nlO^z6G%6V-X1i*jzKOly!qfOQvC0e7d_ji- zgG%K9xeNo%%qUVW%_zgUu~)#5Xp6Y?a9&20#Vt;R7`|AQ+GS-bRPT9LP+2U_9-Qr^ zH8Be+n#Y6OE6WrX%X)cB4;v1_SFqsLMh`|(`#srClO;fI)hFZ<;bG^#)ldIMu0|KZ zI-HbCGuv=t?7vDj8Kt*TI?F5jX%?&A{OQM?2HqdvtxO0{x_ef45doY%dw=dWn(%YC z;;et*KN~Y*%gqi7!;1=bkY|96)W|#dT%+z6|5@=d`v*N{@Rm;|?JiKt6e;2vH(>ym zX`KV$8nGs^1NoWVyE-Z|M^CxSq1pm#0Ie9Q%KA`X9N_AE6I|OJ{s13+U5C%_%rlq& zS>2{NO0q*?>clD+9O7m#!Xut{a>p?)wB6dWZ3B&3*{X}D!@7v)t2AjFg6e-_zb1bo z(KQL~K5|1_6jc?4oXXNZ@KpM{C-;rR`V#kOxrbUfij(tkL{bl;H z&Vwi7WSt}7wfa;|Q|r-G&3wC#KeeN^Z{n;6w)W@W`6g^!eCJuVX|YAw5$sWGN5iOW zivc0~E{}%HPJ5g2Oqu+w z_cO$eN{s$U8VSy~V!Kr#&R8CWx$bnst(V6H%IEXLHr_iY4Z18c*ftV({>J7@GAvv% zH8nkH$K^X)+PCrj;K+r+m{YEQs?JDOE&Zdgvz?W{kn-V$g&0WQf>DG~lORs=F8<`? z);@s(hl|W7(rZcItyswwvje!jW4UC$s7rI+8i}~y`aNY48T5eQV?@seZl1kw`BFpP z>fOfu;snodt6+unzfC~YnY4JRk+sVGQ@{I8@K}zma}=K3ryOzeKu_M^CNy{)h-hK> zt6G{Wm9q2Q5N0C6$TeN2cQRM!Vm7UC(}_ck#c4^B&-UyWC+EY-+i?h&LM0BFN#oS! z02KQ4;Gp;46ZrKBs{UZKullIOiEo?5OhD%vgl?9id5iLf%j8)hr5THTY#1ElzDZI^ z#AH_R&}r%@EAiyjm-|G?p#TJ(gqt#yZ;!ZsV38oE9~s&+Tponl8E36hHn;F|OZ6FT zUq0dqXEn*36jl^q2#(i|am&=Z?6&tR7ae`Sl=T8;lT6&WTDo4FRR6rvQ;h5azyYW{ zz#zXK_&P$g!$t~It@x6!Bv`?xFJg&E+3;>JPq?H+2Phnf<=Sj?TOvj8KqAfAO0R}_ z{QB+EgKl#;};{WnyL-~ z0v%$!%Mz6f4jG*fZp$DYw z%_!n#SnbeBfQjCKb>^YraE_qRS@|1k`kxL948NSxZGCI+x8^Bri6KIN0y=1RSeFo* zQVr}EiTB`>Hb#HFzR{39v>oQQZxXxq0c!s`G=cb0MVFFzk0q(wHVD!9tE+2a_IQeC zdmeU(wvPbZ$FiqpBI##|kYOkCtf2vC=Ut8~|F zAVV00p|b6Fu7ZL;?-ITlLy8te)iKhWzaUNkHQ5CH1PCJf}c!Z_ZNp+t63=t$e{gT|*CUW6IJ5-sx; zj0>ZOn>BZWD4teuwKH`cPkjKAvg5qa(QL5)=yn`O)ZLe~ z^QHJbc1Xzv%LuBG`ovN-&%#mq&5QXe0<5>FZ+K*`NNkBxgHXvVi1^3T;va@PkmNG| zHa%<^=a$+J76#U=gd3cyZn`pFVQ+;qx(nbBjIO@MYU9XFE-k|$cI7#eH!&dfstK}8R z+EKSZnQyI3o$>Wrj~w*vJsP}2UX7saY^hNxKdrDl=RaO$+&>-i<(a&7{qSXJmkk{r zDW6w?N2D}rJ%S?dT-~FdUPXNqFgExyQ55DYQg$6R1v}mKXp^N;gpy` zgs(_?x?^9FZ|S4ud!un@Gjk#$&Fvg^8@$u$~c1L^gQ^AD=U=o z(2QtM4`Ip5?&&Wnif2O&Q7^F|Uo7u_``S%-eX3;iy6@ef-_w@qVl$ZCvd{nd55GPAlnCxR?su^HaI$oEwDzn)^+~A%vDYnQQ zdFrYqLWYh3Uyla_UWs_On`{C27_)s3H$EPRUZWFz#!#2Q!;tDrco^NuH5U9;ch zrw$PVT7#6QVHiUtq7Wq}L${W;Y0rS(UR)*Xc)KSGgimAf9);Vh$`msn_rGtU2C8o$ zYIx?VrTkXzquj^C0s>3L)Ft)OskJ@dN$!AgpCw8UEbK@2v)uaS+E_(D&rMe`nUxmh z%Y&Knn+T@d9DMAZxRGj*)nHLRYlaAO9Xx9 zHm*}A^Q>pO97p0R*ZV*GRR`cc65p{BaV}a`D^2?6oJ+O+TxT6e5Fk)m-EPWj(#HEf z+qjpch(w_?^5Y+i%HFZiy)B2L%zZ}_Tu3{?P5u#CHSQ3#eWt0~eR(#`y%LKJ7T#wW z;9I>{A(Ebtclmm&UGCodlIg+=gKyzzApv&aJTU z$tTSrD!KrFV7{#c^jmDtPamD%YMqVoe8n1ovhbnsKo1yxj%|q+GA(lA#0K&xUg_=K zIL!={e&@-8(?^h0;?@3hB=i2kPxmSK;wTGWT?_oO`J-bH0h3HB+)Dd#^Hw7PYZjH( z^u^2cAP)&kl@5uEpBJBy^jVeG0>w8U;~Z27SrAM%w2KFKK5htRLRmJR(ZeiU=GNFV zl5g)+v8?V$YAve^2)&yL2-*Yd3%VBDakE`=y#q400Kg)N?a;N8zshGTQc51I#umCv z_+llFS(N?dD5d`H87Wh<%|9%_W?bl%Q=u@SxyS&!>tWc#0-Yx{)O!Pq$qwBm8RJzv zP`8iAUqns#OB%tZe)|gC_Vzx$^E$#wQn$=JG{2fr%jfY|BJ+%da;=Vlg^VbLOfWf= z=L~%hv?0mtw%Nt0LsJtAFZ*%);-NjQmF(sC<;XKG-_ET2&mbcH+GYs&&h1MsBNrb8 z3pR*%*4XIwOHhexsVfnbyu{6M2DJG#P9U)K`<>88fS)hSaL_&XBa~lYB@;%s-luEk zF`HekORiYzHI=rVyR_>@<`o^>@bxi3Fof=dc{;_NASU5G-4g}BeWPbKFe3XT7nU;^ z>u>*b?e!{q1rM1P_QH)-H!Lr~C>og=!8hk7xocY~0+RXM<93FR5Fs7&A9dVYyqss@ z6HX7377UMqyYb$&uch7U1s>_U^%3LdeN;=S`Pv4JzO0HuxqYt1i{gy0h6@Wjzwe2= z;Cu!FmLrHWrv*?49?hrZdo`QnSlxqhI9yRwBs(=QD52C{&=Y)<0aT|UT<^UU+-DJ_ zxFoRXyGp&cuC!sD!e0GydM`x1bVk-`Hmrj%j3Wyq+nq7<`rUIotJL&q`Pk_A(y&$Q zeSYp7)WdJh>#ta6B;hlz6b?k+8rh5O5`_Wc5B;gY=+`tYq6Qu=SLVCV6lv61xIg3X z0MIi3_C6v@jRX|hlOp~{-^mRkr+9^6jo{yuTRJuOqpoVL_R`YyjB@~t4nFs86oQW> zzH9yZK8p>F`UoUq5PZKcy>p_Nn;P(R{q(C=tJ?iAnjk>AKqoomMD<-~!)6TN@+I#GrS@-(nbs+$C;#wW=?^egN%Ep(WYxyAF==F*#(SE*D z-K@}N#vlgQr?oJPWXQM8^-xxjwXL2O-&Qu<1ss9 z7!xNaVx>BQYd~H}G=$=y23yTTNw9$_`7cjNa$5^@Hn3@c;A5Y>P3(i_wGof8^5XiT#TwXI9mT>gt0OV3%|#7Rx33j{>eaRk?oHa zt0UF#&j~}Y6195y$!d(Gyrpgj?d@}pTxxivK@EQA35E%iNkQ3m+wo`LZ46C~w)1*+ zf`3E`@k?@(V6~ok!oyTGoIO(1g zMj-CK=GzojWu4renxTd;PwMqlEhWuf@6}Mz&DFx}rbu3#BAoeuME-nelfpEQa*~FU~BO>Iw{IIH(p9z z$rz4rkj~`$2?BU3^|%ca<5PR>k#PYFb%0etLZZYgl|KM={X_Q+zA`*~fS!Td3=vP> zl=*MN^z_m1_qy+~#Hx|}I46Aa;+;N*3xgg+STLyhEEK8>!$S>>&CXxaIh)1y@&8>V z^o1c^aS^(<6QTY;N3Uf-7OjV|?WzpJPCI~*4yZ;M5g9*i$ zMBJUAlCkL+Pk&ZuetOF;EVV=ONCO5QARrM!FL|BnTw!Xq+&0?Ucq2fW3)vfeb^;NYqMno@$VFv^xE2N8Q`@^yeGY$jB&u&BJ zz!?hZ@9etLLnp_l?`wd$_8Q0y=sLL>K&09smiveR!p%Nh|C-B>gM0j&y7Gnuiu*T1 zN`6F%0$2qv^;Fmd_$*wW8V+;xVM)~pYDeql>s6H}w3#fdV^jOPhxamBhp0fkKmhD!t*`l!7 zHBZHj1_{EqQD7;bobOX2xxHH4-&Zma$;yXY!WnP;yuJ8k?40&pSY-W#mAXxDG3EPa zhAJRzLkvxSFO_n=UW6lqd>n01YRZZMz&U(!$3rz%W1>9?>O;zi*OMe6MU>mzTiqTPv(vnj*oA?GxxOca{ z!m9Ix5<)we+8j4-ue_1uLOuBzu`lKc!b!YU&_z{`W5kpar6t+rF7o#q7WydY4GTgr z5r?W1;asKsl;;{eoQ6#WX5376)#1^=4r^n<)<0)2QU4_Q9iPyoN)BpWLhnjp^S8Kl&ZYXfVvRfLu^DA3 z$0DyuhHsz-4nYpS+uW-Z>YBQvL-~`G=O73R2SfUIc`NG?Kh>OXgsTZhRRKsT%R4YzJjr?DT8{N^?u)=hJt}l<`%U zVP@U2A-rFXLr+=ZUv)INh>W1-XBC{h<^H^)t8q6pOy4G&>fQC38DM=a3H?x?WgG9_ ze8y!`jk_sndmbODZIDeH#_~aZRC_~>-*^DCx%d$w;%e^85B@%}jMaAu3j>Y%_G3uj z&HHfs#S)p?I@GXR?4f`{yTuvSv)>brGjAi z#f8DdQ^f&g?rFbe|7j@RVxx z85{1+?{gatqTob_9;6?J-d7=W?-!Gt&B!f?-wKD$Fv4?38w6o~m@d4n*qQUe%{F_E zuU;66ApNQ8W7+vO~eK5-q0tu^10E*I(MAfyrEK|KQy`7Y8bVfZ= zA}lykJWM4(IZ`Dcs_|=Zq-vDXat6*)!Z^c6LvYR1{Xo-!Y|9C>|V@Ns{EahG#8F0q{X(DGIV+jgV`e~Ph=SK z8*z|Jy244KJ6o%;iqk=bwaOd+&+rEvdWv|Sne6yqIKAOC-Nyza4E|XH@Ysgv>k&mK z{MUHRCiyM|2lyfl%n~5a%b?e+vo&?_3a0kU|0n z4i}T33Mb*%Y4nY74qJURN>efIuR2CW#WMl|6$A`-$b?enGMNM@Egf6;SrX#5*`=01@S)2BXes_DKo)BPH0CDCBvcDTFW{#$O#2D{Nj|{%FVwED6@Zqo>L^wyZ9lx{I zW4<=z84MZQ)t(6Qpoyh`7TD1f{<(joQDq{*0-1$@!vY59`hykQYX?DzeK zf29pWag1~h8H57Dk8~*0bKO&XXA)9wykbR$96eDw%%79IRR(eRCmT-@$qDl?5FdVB z$?y}Wa5H4@mN@^1z4vfxs_WW^4+tW1D+&Sv3Mx{hN|%lWP*8f28g2pUARxUYb~@6e z7eR`&&^rVasR9DhOOO_Nhd@G--;U4ozBAw7@V%LFbjEXXcJ^Lpm1|vVt^Dvp+nMW% z05$>NAP&~5&JuBwA-2P}Ly=8-4q!QR03t~3P?MJiWsvPs{r^~Q`V!PTD#W9(1i@Gu zPJh*a)@KNC#tR>^!)_8%nSF#Mu1LQUM~A`C>?9 zH~%DKm_%1a4s4fl8`^mPzMug>NR>Z(vDUn~5_t4#?hP3kt#fTMI<%qcsIiAY>gfZg z{AaRD3q1tysPbpw6u)ZVbdryG(Fa4I79$&SURa7$9T_6|&qe=|L%6VhCA|QElu6b7 z8S(FF4!7)0Trpz$?*@~9d7>TplY=nfAP^u z&%d$^aRC7+b?9CgVzU`g^zojd^xe!E1#hlI-TaIiIeRk4_*WW*!Ov?c=#e)$*?OO{ z1r*V*Z|_0&f2kN7;tIklj<&x-DMVgP1*zFzqB+2Ax)%#TjQmasME>ex*4|&8S4(Co zIg*{bfF0mO`+D~ue{4KjW#7f6Y=J#WY$w+`&N71+tSACuac4z;=f^=fQErRBzW_QM z_b~dq7EK^-EumeukDaC{T4+*goT5`)8c@I_hOXI84;>jF|Fr`Maz?RquKX)ekce%` z*6P`1Dg-v>%hSUy^*U@T?+;!~n&)4u1=#{wv78c~_xeai3If=S-*X1}gDgrck%0fJ zzF(XM0%E92##4?g0q|e9N9+Kb#8(gC?Q(#B{&$l~$eK4F!0iijd;OPSQ+}i00XL!@ z97t%j(=`96uVLALG!YZnr^}w*b0p-aCE6{J1b8`aHP@2CGT^EJ+ay-|ACasCqGtKClCICG zh+>uwy%aOAH0?7LjDrKS_k#tAIxcAh5G(is_o=2oBMqDv(afjh z*E0kED)I}r!J&lT0nV#Uf}32h$YlZ+Z6JLldG6Y7m3?dvK)Im*T#~3bobiK{f=Ll<=P24hpEiE+KD4ko0Q# zNKMBgolAN2AD6(`AtR7`L(8!+$El-gSa%}1#_{Y1Kg{`R8siM}hbk<)85tb~P^XKj zR1tH6r%<|wk3_}oKaQs4E^2PDtY17r)v+8_llkPUAm|$CZSwbX2J|iY`=JCr5&46F zfGm+e|98pXZTP=^`9ER!|JO^hLGIAoLjV2!`S@rqwa(Ii@7s)({wo#$I1^&l!TSJ> z^~3%pT13w@`D<*=Y?Ryd7>+mk#yU+j8cVrO2(C#xeT)|{>I!8NuvzSmkLY+8rWoV7 zHWE*82JSr-7pM&(^ZTIDVF$@^mj+E%X%{tCB^O@sH!rKii_F~}9foT-wqM!Kc~`xU zbWw{vO40Pp0ep1ek82Y_lS>qc)d% zl>vKeLpFV>ql=MpThYUgBXQ(RZwve&cUIOO<&aTH%mGYjZP$x!(x@`3W9XMPvgz<6 zU4O;j$X1PrLz=sN-QR*mcmkTda?UW*B)>Tjlg^bQaZSgf zIL<84_vnc~;MxJe2FSr|s+1OsvL)!yWO8T!b%Gwbh z!71U*;O6MiVx4o)5AtcG29B`4Ya9XW(}g$Gbk@WZdh7A>#qyI_rt;^#Y~z*o6;3ma zUqva=4c?q9Xk^Ae>{9yg={b2OU6raYrbb;Box&YlebExM`{$d=?t-3{DV$ue@MaZo z>lrAj;xZd@e*sa`bZ|UKtW5izi(@iS_)QizAs}oW_Y9%m2P;FSL6`=#LtM)w0D_+T z5)PynauQ(i-ZOzl)y_Si5FAo3^RK*Up_17~5QJb`%*R;L(bs^J9Wh5F*pZW_bC#oX zGCJed3X6^@pMirBGdSYVFl;q-$TDH31?@YL>1?!n#3)7F839bB>On@(lCkglPTrTo z!k#f-N6F~G!x>Yabe~kG8uPrd(ieYj9Pel$m?%NQvQqBp zX1_k5NZj^n(3IJID7QyM2dp{Q9gN5U2j8H*-y9j%c!)Fe4cugRUx@zD^%W-b2#~?7 z_9e6IX*xK)!{tvptyKGg%F+H<-(?m0lU&&V&gKMY zb?aAQM!ZgeC_i@$4 zlvQ4nei~3XN0!)~fgWCrz-h_v_+(?&S50`8P`lE*t*_~G{u%H)HEJfUxAfC@dfAs= zY5fALqEq642Z)Ed3&K*|(X^RDEz;*8{(ngXp3cr_uJDAzdxXJT+3#R1j~LMAnsakOjbU^ z!7dS212i&SI;83r&Ih^&JbCH!hb8o((^|U`pu{Ix-HLeb*15)R1TsnK-cZ=W)XxltHPxdP~#BoDg z9=|&;ial#P1K54pWgq7e6sNv;Z+Wc@7n)B@`b#o@3}-G1Z_YaV$#ZSA2j{tFJ-iBN z+&@B9DQ}rP6)SM~WB3uIPWMhc3n;rEUgASp=+saM@{9gDiC|nmmH*}Yd4;jDx-0|Y z7JU1<+dx(nbI*ESMs#+R?6=P2$U7&$rf3lYKwEjCokt&clvp&W5)Vly*@M$p4`&?v z+iyfQY&Dx&UH^V(pqMcmdG{1eccN&vk}JC2Gv#NB5-i=Hr~+QQI1 z;m3dIn|a@yJ}2STXX@IZmmahvBPygW7&yO{pwas)FUMFKJ#rLy3X+IRy1=JEp-nYf zKp#FOo81IkqZi-S+u+T+g)N3-I0{E8;VN7i`1Pi|vb||vOjE=UteGe26ul(nzl=4z zVFf(Z=%6A&umZE+K!0Cx-@fah4QVPEFZBiL86u6XmfmI_}O&&Biqg--F@NFOTtCN}t^nKt}V{;z720NJZbB zCp_sdQsr@DjJs{ITI^{zwh!gDy4T11C1zY1v*25pRFC&RYjJ*}i~B>GqF%P)l8z(Y zMJcY*?!pkCI?$WXyXg)p3r_Rh5Y?8hJ7XE0^pCANFXBs!g6lS{z3xv`)8W+ju{sXl zb=sY~`qiA=l#j=NDf>|+L)4ii^}3q5a35{iZ)e_hABfvq%=*egX4>c2fr!*4AVW{& z#K9hDu7PAqoogqAtw0S~xsJ;U*lFyH>wY8~INOBvhW;7VzN#K!xt>2Ct~xkT4CK%K zJ6-Ro3iQIO0v6A7sVK@rL(Glq5bsNxB^7Fn3W{T@e62)|T}EsFRnvO3r2)H?(?=sr zV%_s+lCkC_%tOP915JU!!k(!Dlwb@@~5bKI^T8(bBcgzrRB7DGr@Ch z4?X_?rG_RPN?RwSb(q47?e=lIJ-L#qQ|3%UH}4#L+heCwNoJUSHQS8mlrU*^oA?Ur z@{APMRLUt^&iZ=m0d@+Hi1wgByjgvZmudB)R!*00f1cG^$jLCZVDf8Y;-Dq+s`u2) zSe?a=^*+kTlA`QqxbbeKL?pmM4^l6-Rb&Rj&!V@2Idy zFe6`@$6b-driw4lzBzx}tFI%&r(mbSvHRZ-9D}vz?3f!i>%+<%VwX!8M*CBT^Iw9^ zC}7`ASRAVdt@TFB1ubq;JiR-lARJZO5a4{<3ct}yRWcBOqgOmq4q%wxBQ#-uQt3`c zO>1_W5s1KJa!2mEx-aTsMAlU9GlS60%kjh%_pLzs&bc2N;c4{Bo<1C?-;%kf$ATZNRa+UMh*AH<{Y;ph zNLYK@TxKN~k!nkJWhSLWb~9m?4mYbRD)c5My6ZgFjWpBkxX1eZHEQ{8U5rmmVVHjZ zqMI7S+F+#;%{ChJ$9E8rsJSqvRen`d0c%&EiItK4Li?*8ORRZpran8<-so7~jAZ8p zGiWxrelEE2YYXb3C#aDba=mw*?iyrpz96u{BA-=q@Mo6uR)30!VzM;)@$fwmoQOI6 z=DTI0)&a>7YqNi-3{<`i6KO7WGol3KY}`Xu(`PyFLgbiJJ(pJ6`&fi(j|Y2@Y;tJY z4o(!R_vPy}tiIC+z>NH4ceU4-f|M;^!CAvmE=f#~6uQmt+bE`{ke<-q3jwxO$7XOQhr^B6`nxs#eD( zQKrysO^q=mGtFy4=e*Lowl6LT^9)apZz3E{Gp57;aQkjl)v5MTLlj@xEkrUmpHXK1 zd~lDOWzcS`gJxLlh~vbYi&py_XeO6U4H*^hdcR8N^ZoZGg(V+f`QmqamhLI(nYiOU zmd^6U9vcGgT*SKz*#igBT&fbxUnjlMO*ZRZ&qv-ZGYbiO_ZjE)#G~r@_hfcIrKoZJ zj8Z&&E03L`i?CGcs`p%Dhh+qm-<$E-6Y@QZ?qH`AOb4M$`Ie%;hQ)w0L-yNmucolBp3!Yh6?4jJI6j1QPi{q3C5Gg)xPT$xY zHh&C#q2BGqd#VuxQuq+Y2DHS6XsYduuhbLZjBhELHg!KfE-Pe$Tv2-^Q~u~T<#q#V zLRh5vSdkY1=>ll-k;&1+j>%7wMrC_Q&A4S`9yrZ1 zkzEkn72D6O7cV^^M3QozuwDp&w995uCCBvz0wh}*_-9AxQrYd-Uq>xh*r=FJj0pI% zKU!X0Rqd;gQnOrqU1jbVk=jvp7Losj(4OKF4I}ueI&Gu78-4c{JCu&!$#2hcsj@tm zv!B~ZQv11AyZJuFotmW3bh2CM-s>suff8XnoX!(Xn2EEy8n)(IyHa8Q&+$eRHa)`O zV|t(B+DofP-#>5!{kEf|g5^7 zb}X47OsLhNupFGKbM3Z@QcA*?T=d_p`;chyYqz9fOWdp3qfd5z@8%B+|BdT3S8Gwx zdqJhM-&J#HA*w8kUmK7{*!@ZM`a8CU_xBeyq4eIo9YelWH$JrOa3Ux{Zq4n_s@vw- z-<&ozyAW?cT9p=G`2H@;J!5UrXsJ`EW=1&o%F9`biH?6bKLwU`pJV@d;$#|O>FrtE z;~Ck6ozQz6#=Paf*IN^j>i8@3l{?NpPhuoEF<^7NeT1&=&wJzc7+7Z+kRA2xLm z0JNh-Fd5&k{nj_ZD&1Fa5VYlu61aNxIPd6zH!PU{c9u1Qwg&B%5K#W?9odR@te#Jk z#->4`NBdT*he5`D1FI3&?Gc~uu`}7~D3kVAZJv*R5^Cm!bPeAb4bNBvQMWiE`<=rl zQoK6Suo8xdet%4`-TCA5a_iSP14@;T6D>Bzl}z?GCe`|6_au@$_ne=Zymb6-6{%Q$ z@fUKPbpg&Xi?j>(VG%Ofn$0z?>9R?%h{Kx=q)~f|y`*cC@v!KZ=ca}z^+)r0ZWHQF zCKQ^AVH3?RRCsL$lWlawO$2OW;^$4MnvLzQNDpAR3FuHTM=CYm?$L+9{7$CTtGO~1$OkaqMz((-4K3CHgHZa zKv!NWKlYaTx!zX7rwPW+^7Fo%=bDw#{f-KoJu-J8croem{MMjz3^?s^2T| zXQw;$NvPiH8?aiU)gz}7w|$uNeoHdr^b|5e6s4kS+}V{)i_q{g$DJz`8u)JlpFwFF z9RW}8w(0L5$>7YgFZGfzpnoQ|rg!LIeNV(hfkcjveZhzVk_ap5p^-eN=OO&oMf1D-b-xcbC|BT<&7t%tSQ5D0G^2 zx^E{A`B-rAfE+Oa*?yDLC21#QTX;JYc4jqmhIaiB(R#1+6|KqjoMD{^#vJpAhr_2T3cWl=jrQDVL(hr)bW!| z{)wM`bI*eJY$Qv-(dTvGrquO``|a};Q!g+J;U2a}PHToWoGgca(1lvRZjX2? zF;RtozA!FJD^0XGLlpY$xli6RK<_s%Zu(PNes7P^{lu*iN>{0fvwO5j)iT@a7jLVj zw3nJHeSGDXMx|y81z*;`(3*}jbjVqHED<>O+3}UZSwR5 zI`Ht51z<-OS_G^x`Fi%Py(?04n=djgLY;@=iowJSE~UxeNo&X(w>E$J(J99tJ`M~_ zJCiKIefLt~g~sADmRF%`k9F*;XhBHmEVyoX=rC^_aYc0MD^^^?xH)8U@2i+wVQcG~ zeFn(1+9_UcvoY_YT|=%`#!Q4Mohoc?6q%3!OXAY%4QdZD-Ii=v9GSGqOY{18J(=mU ztV{jz$UQhGg}z2h4XMy}nY~6U&%CBT0ayPNG$!hmxu_Cn53+m%jn5fDHF;a`#koQ| z6+cG9Vt(7twS3D{)y1_78VRqMcJa@jK6ugRi0mmuHuDItp;2(F>#b9J3f9JyY)C4sG8z=i-oXqd^RgYeLp_@9xfwQXtgDSW6=Z zp8oEw+9gq(!pX&J4^-Lj<=%t266z1?`Ue`CymRMWQauX=!7ElPZ1V6$l!906RuFzo zj4&8x)AmB#fQP3MJA)^O%&bbT`a3T$=2$i*5lCNq+OqMh2c5DW#wiK9-2}ZZ@oPo$ zHX~2X8rPkJ?t6GjyGSs~Z~)}fT6Y4s9pyQHZ}VJ}S}=(w=3 z@ge2QNss>N#SbXFg3O;IE4B~{7P3>-UGuO0!D{Mg__Q&Va$-QJn%_@Sjp$Wnqx3s9 zoA*Fuhfva(yNf*tHH*HOZrR!};scw)4m=Zo^_vW!D^A#)_;6uD)VH6|=y#tz&!2sK z|&Z81_j8*1U( zTr31{zrK}BO^&(IW}co_Hzyz5%i1=M^Q7t3BC65g!8d(R{Vquv%R3Y-6@^UC|0;)#1X+s3qUp zT~{+%qNl02@<0G{THBc^^!{pStKUei4n~tgcB24~`rR>1h3LNV+HmQ?QRR6Eu`Ot3 zolAMmeU<_rqIHOd!>sO(eS7zu;g<0wbKhuBVvrV3AvYOwH1hyVmJU!`P7WPF-C4w3 zeG_&@4+ImxzQLq%%#2)j;sFR;gj}iju;w<218@b|hCh{`kaKxBV?lgdDV}6LGGjyDrU(UcPG^-l?VnnXc$& z_n!l5mk#}OUT@}eh50u9`ToAk*tYzPAVR%^v!Kj!y5$A}jB)C>e@CYWC2yU{OkwRR zzhKE7O96e`ZclM#l^X@_$lQ--hykJT$G&?{#A)N?K}4Z5=6H@>_9j+lq)1fHGt0dL zVVmK9H;9l@xQHa;wyfJFM|(W_QG5bM#SAQ0+&k|^v?VzM&SpGav+UPjo;2qb7Osu^ zBNf8@??QZ$8=0tlYRF)Iv281REIzoIufEizZ+@yaMOLxUir+TJok3<{Q9i%0@<5M?Z;@@U4-?8!gX~gsoDs@9E02Ys0q?mR( zp5s&$Brmqih~k3nP;T!oZp~GqxjvSsVWU25y}qYUEa>D8>+Q^=w&B^kZ>)im1p%W{ zDNYuX8ucm;mvWq z`$E$(iHY|)grZL^YywGoX}a_nit-6m3xfk((?fm! zOG%h;P?(|8(ky#?<+!WAU`A<75OjvhMjkADKM*~1#rI=4m)ejT1yyezQ z12;WD_Yr+FhUvR6cILPvka^FQhU|cnv9prI+r3pRU}IJAtBhyBm09%`sSI>yzB*+y zSZDhvDGN~EULx#PsUiK~UP(xQIc~$$oVUfAT6w(Hdt$A|@69v3_&EsCGm{|=`y@;l z8$0`FpsBrI0V7yvKT$bb;zhxSjmhYr^mVYml z(s{J_X<7Ns0f1wq(!F2)^ARB928Su7QR{Ubn-LBSj36H-T%GDYBa`#k*^XK0%+ZYh z-a;(XqEQg^;zQmdN$L%;K8G4AF0VV2xq0JkF~@vgo?m}kC8ug3XELMG~XGb{;`hf^NdDNOK; zI49;U2+?8_U_n(neMBs28@bzEh-8&<{qU-RzfGuXTQ1U=W$(UQ<=@&->|+V4D$1gNy?ymEB7S%&KnU|MsVa1&ZT zaER7Fv{+cL&?Kk9yYAB~O`J&$t0Wv+Ee>krN*S6;uKfX#}p8_5f@$%@`<-0SmK&&HEB=;5g7-<|yG=N|~nP8KPK zjGPH(_{VqivLz1veW&B9fE;VRKwA@5;aa~u-fTXAhq*tTxggcO)pvJ`v?D%VZW9*m zNQHQ9O?$K^W1;c4Mdkh{gw!bXD_#P57jsk_sDM7I^}RBKJf&T>dj&d6H#zOGb;_{s zDzy~~+klQ$I2?Q86sr1OB;D^W*RR`GPrT8ht?wqssu%)Mdbt5r7wwW>{(6Nca{yBD z^5jsY&@hh#=RSM>iJ+YiNZYWQ7KyB&A9EBM7K?ca>AY)y#He-Ky<~ahEpWzjqqpnb zMvB`vy)Ft#_<4T#K6@f5;}VP~O<)boDL|&l-zLC~hkqQGrxw=r9yXo> z6YCy!y!M8*;r}52vM!byT^6SXQa5@;Qh0_$8lHNM1r-f=U^ga>K>D}wWE4$mehd;s zV;3Td(Eg}2s*qc9o25-4elWADDGE~!>X(`PS^1F#7GPYrwT47R;}5b9psXL;P9gl4 zcEgEkpMn~Hc2+$wU}Uj)ksiIE6^E!>S(;SLD6arXW#753@n_vSYFGcg`I!phE_7`3 zR%weapo7?$u<*1Sr(p3hA(cTRGo^M{p`FLn6)z#(Dfh{%h8Rt?!dUoL13Jz%aDKOa zni6mlV%M7Y2kNQGN3YfFz8eAF%2s)0e@_K;$r%{soAEStAg+Jq%^n`Qlzqhn6aGO~89W%**z|%u3 zEI23%*|=L%{%lcQl-mzR_MxU8<u5jYhtWdz=1)!&SngGFdwNd;J;K3${$XSk$e5o8fVpVL?%kpA-f& zucqSLQ%vHoLnn*J%Wwftx;>IXpYa3Frd`LehtLA;dP9_RP`Dg?k625L*+~YCBoIcM z@Y(Ai>u)#z#xEZj)TbrQOMde;jsog0>Dnv$}vjeBP;|mw+FtAQ@>I)&S4_t%h&-(7< zB(Qe{FJlS>3KV64d$SPyonBmQl(>?Y(GwV1(J!0rX8M4UBWR&am0B~VVXF@I*CKXB zxd6e^Sn7veD;!-W1wqQRyHqE}ZC6t&4J~xx!_?4^2b3+p&op)W4&tn3)Y4lxDQE-P zpheO!V=<)Yvz@707igC?cnl*QI*gZoPBdki_>F>-+B=JTRd!55Mq+BWUVRYbsFIJWx zK;sd0)hG@jkP6nYiVxJw8imjIN_(VP0Vu$w2y1!o(vi~cvXy#`P$S@!ES77Ch#mSL z4X_dTz9i{!q>oEL&*9OLt_4ghZ7^d5nrS+bRA^3 zM z>e|G0{b|{=oRfW9)MKF!s^!4(=;{>W&i+c*uRzMu|DmU)z_qv3+wU2<$CJSYLK0+X zFnBRzBTNY^wHuKQHq-S1Yb+Az;_D`+6ws9f?-@9Q-lJE7Vi{aQ6qfwk!!lY^0W`z^ zgp?g$WbV_s!&%{NLo0Oo{09D18!YKCYXU zpI_4gQ_UG`>54(YLdIOD%c1#BuggGPCtwM|3tN&qvRP z3x?enkwKGBFNqdbS0|hK5Et5SyyN+qPAK|Gvb}Zk#bJ1>j6-= zL2K=GR~2OiwW55+3}#hs3QPR!@^mG8b!D>^m6^6TqajfKSHBl#2V3+z%z{VS#0@lj zA~EG^#7&$7NcJwMDG`(8@l$kj71HPc)1bXc$MDlu^;!0efWS1m+jZ7|h<5}D6G0s< zx-M3YE`^F@iQ(-`x^@>c7G(Q@pTs4{Y7#7Nf|;AabKZ{G4&i_OI3yP`^J(=RZ18rq z!cZZ?cT0{9rMD-FC+5@19od;=+i9w4>v!Vei_vMU zjO4-Rc)N>j%jk}7gX2w}vT3h0_rW&C2y>(TEiVvVs`eeMd-V6>ja++<8hw7rDp~J$Wd!+hw78M`%C5NDdn47P$?X4l}T%Kl+uzSe z0~fJ(>=?RKnB{iwlTS&>WtQ9@iMeXTh@T|VVACe!Z?R1zx4hP*p|AdQ$*oGqKu-{j z-=`x{zw@Rp^%D zxVA-TRdW;Pd1x~^G5@Iv6Mc%KiYy&4?6-VJ zlva5=O8)fu^^Xc(=;E^AaZP~fW~)VsrC$i{#%KSaReHMpxe>qV->0QkYE1FVi5%y( z{rS-Sie^*f>eW+~Fe{+ucMm7Gx-0rozkFVDZ$~i}$D!~bMUn2mEpgw!=ldVJ|MJj(T^F-#L9{p9F>Pz1CfHk@FCkne8tZmy z-M+WCI}c}FB({%+#+Uo-#I$F-hN?p^!`y|fIwIrEy~m9~Qf|e)}TFQm+jYie(XmK2 z-(wK!#)7a`q8ZT0YxmI)SDHV2@F6KOZ#Y6o_rBVWFQcvpX1XJ6+QWC$$XFU)_8g)L z+NBuQ;ous^(F*`}KmwGsYIY)s=U% zVV@psADCE8YOshr$#yqx)mDgrG%fOzHoor4P4SVtbM#!BrvEe{rz!QjxDsM!={CdAd^ z*Pdx#pjJh2$KwL94_EZ=HlL4OB*`L^Y*FI0(6Bw^7A1?(Fx9xmh%SU4QeJwK2^zve zHronEB&U5P$4sQ@CT!H_9OEMCQ8rPPk+t5Ng3oW~wID{lyS%OGvx;>vxEzAK#tW$O zS+t4BNiDpYF(Xt5t3{03&A1%g?@AFWxP-2FO}>gBFQc^eFt9=%qa7jwrohzHzyE>} zVoHJ@#vYzd;*WiZ`@BlrM*WgbKK*f!{4*g-?;WMCB))us!_hR3kGv(4F%L@PA-<-{ z>$wVR^um|_XI|G5#(xM*EFNT_D6XEz#WB0Gm{Vwr%wA4AfMS+jSmO%rF)34%Kq(<; zQ1$g22FDymTIswp8e&p3HkDpo?AJT8@CX`9@35>ed#z5s3fk6-Hh?@MbO>2<IiRB3Et9b7r>I9xBG!5L5~uGu>q(+L52|nReZy@vAzbe(4fosk+vqppvOQrvb7S zO3_{^97NlFeaKV*QJ%~L8HRj{`7_XrswC&QW8M(|;-W>8uHi8%gbMLBtW0P10ycNW z+R5*5lvtn+>ifVabP&w_7GH~E|fj;?K2$7vl(w*&uH?8;)*d zimGr!tEcfR=^6(gV&fJ+Gh`-@tAOiT9GUO|>9+eQ7vBZcOqP)Sq}M;GEBIm?8NPX6 z2iA!}FgnHItg~DH{KQ=IAWk*plL>ij-Ip6+7~bDrSD+1&!7Z|&qg-YkQ|`rNmPBhm zbc|)51T(u%7p>(2L&p#8TAS))ZI4eIM0V-XW+vMgm@O*D6UWF4ZT>ooabs*nX*wB7 z-`+*9`kJNmiIS&%iMXGGCb=&dLGb}V4?3c`7Uky(@R9GHNVU+J&zRi z0OObIrrv=UTLfKob#rfS=y*2ZeFy0cun8-BK?drS`nT84fSc|h)ib`I`$Tm7VG|Sd zcw!koQLP>@v3ev?$KUgW2Xcz9o$M_6LQQ&B%5bfx$}d|T{&#xW=+3d6PXAF)TPMo; zG36yzj6;*f028Fz!@>}$*IRP)3B^M^w{|bbaG@FXg+EcCXc@AE4J&$^vJs>&qK28C z{V<&Db@rus8`04({Fvgx58a3_&cpLP1(ez{sL_JM4)WSwJW^3|#e{^-%@v8Mk=D*4 zbKKJ_=f%a-pnSQP!eHj7o3p~C0lcfZ6y;z5F%h$=z)f)lp&1qaf+q>Km8fGMQ3;&L zX*E(>XmL+IbX$k`aWSG#7tGnz(XE?lL-Law76MIKu@6)Gbn{hronHa_6+*w*?c)1u zcZ_nOG2D9tEH~i=x0ULxhXy}2$wiztvO&6)AyNCEu7(m@X3nX1*()*!gNM4{0zv32(uNrIuYYFe>NkPZroC$d% z--D^_4Sq;({FbZ1P82h^($e&Yk{XL74#}36#6jp!zVguToSOngrg>FCeDWzI=^uNPFMWf9@nK^e1lM8dM7G#b>W=NIoJT1N5qJ7cQ`l_)y*eo)7iYaD$fxS!VERnTW+*TVuOHJpS*=Ri zOEuB}JpwWcpz@O!F9o1DP$68Aq5pK4v`qeG*MrXY>EJR9($6CNzInvDz|B<|)kz;OZetlb70p}{#>?uW|jnrmB@EKB6%6jL* zRtt!lyx)e?>)BVigapz4@PtY_T_SE+M}-6GY>UR=Ra@y&p;3ky4y#t z&&67dWL(WMieAAvR3eFT2~ek=)E_YVuirTyR&9viJsC98Jp;c9R<9lSDyT$~31c0R z^pYPckia2-BYA!lEUp`A*gMX^G?s^^My6&~%Y022ea%dT98u(fkLuU{SJOg5*QtrL zIV6QQR*bNi>C`N{XMaUY3^3#W?Z*D6UfW^Pix69CDS|Zre?t9Xx&EqN^wvHQ% z?uc%VHCFZ+@83WeTNruXZTW(+Cr_1~q}R@fv7Bm>vmhvRfs=>Dr^` zIpjP8>LbBhc=;6#NmevmJrl`}Z#%GeiSoB_Eprw#6qM~_;>?AXo10f9AeLOp{KqsG z-V8y_zfk01kqp;z$$Gw}`obv^2begnUBbSevMR#XKUD3>VE_j4MmcIY^iT9RoP{RI z6G8RG?-d8V(ofe&*C81a?iW#=IgTRNxQ1R_#q@%j&q%zJ_$_8p$$I1jYFIHIbC7#z zh#XoKjbOp|ruNar+)2o2=v-C`qYF+=`Fn z)IQbnl6PRxgWb*23|$%I`#Ug=GSmN@F&K^fn66Wn`&z#~;!6yv)+E3J@Xi_Cza}Y4&08>~pnLDhKg{3R{ zF8ZAVZIxwrtuNcJzb zD+$~j4la%d^0$sZ^3G^XR$uIA`}6G*qI~m=r2{`=vl2>cplqvhoEFUlhttDw=kMWM zd$Rlr^(~a-woxCbkNawIe{jzbI6uW4LlgQL9aY>_wi`xCz}`teS^SXcN30~ep|07F z=YMfBGHWnYCDX)C-F$H(N{(U}6h(MWlT_yzegwJUO{+A%1=$az^Ue&dSaBU8&8QI} z{0KvQXg=kyucseQ+IsxkGAlrFA#z z6QTYK8T(H#S-vJmQ*J>#Gte|cM5Xg|_~R)V6K7rpBWKqtk;A9Cr%rf@%fMi~4e5Us z(h8T(Il6J5v+E-Mrh+TeR7&L9;A^;>%#n8)Ws5!&f?kip&t>f~wcT*8Ig>JX3k#@Z zB`ZcMqIxT;FSv1)Sg9CO_CyzWo&US^JS6C3BZWSb#;K~y84qKdE{fh1X|nFj4g5?W z55d|ZNDwrvI=0u!^R6L=7xHvA7JQ`n}h7q;@tBG}idYIvX%rqz$ zo}9LRLM5S86LaPo?Ju?va32J}cqBkgDmhKG?}E~t)70W0AiDQztG^sJ1~zFy%VscL zrD>q;n3O*OTQ}>fa8a4z8pnU`yl|AjaKgfLv_^Hp^M>g6)2k5U=FE>T-=`C!(0%|+ z$VP3)rZeL(^p2PEA>~c3i{Npy!jz;b8`3jAgowbl*4h2o*95;ItHJ93eWnG~uYL9} zyQcodgcFSArb)OS0;HI@a3D6wkIA)1ZRzJ{8U+=lU&*S%x zKk$B?^E$6{p0DRQulM_PNcCLkd72op$3hObUt}$z-G!u9TV&|f8@Om#X5&fB zVP!e!AW}aM0*b?#DrtfG(YX5w6b=uM^f)aF3Ik+J!R?sruyiJ9VJ&jWP-=0RM0S2sLGx;>?7h-M-g5FGfaW6nj8Sd6?T9 zFV@TmVo^_$(YR>%f=Ebm^@zlVbB;G&x^Rj6QUULyq0Z;fple$EWgP@PvL90nfkmL< zA$blSc_PjOenje2BgnTB?n3D-=iKFhpjxngU0(WpJKn>BpuhuYSVM4ej16)Wjj}{K zY=NzR&J(rnMZkjpvGucxu=a0F1JEGSFI9??R}4Mkg)j+J1V#?&e0$xIY>9jj56db< z#;uz=3LZX=y)`gI{I>@-V<<1e_<8M^19G$mw#h|PJ7`BX>RhSxIVpZaoee=e-zaFn z`&B7`*v&RG+qZ~X!9|(22N}(B6fg4%B`p10ub1LMh=b9EqNAvy&5K`5||rkh8vKk zydcSQLIw9xhj&ZCL`?!2tu`W5n`))Xyt^LAod~D}U%$`ou5%5Fa0yLAz(H!gp!Zx_ z3hJNv+Y7kQ2u1@LLWRPq$d^J*Rr)QFY#Y>?ZIYlkfClocPZo>y05xk>jmB%zfzFp^ zmHm+HFgaZ4!l1+KAn*jp66ArD%aZ);qboDL(>G?zd0pejOON;(6n1Q=t3pvW$U=-5 zAdYu!k!WKzsiAFhzynmI5RLPy8|pj^@+R#~Oy6?q=dStlp~O#N*WSeZj-m`0)9{oZ z#`Ro;GjlT@dB)BX^&%l3EX~z}JvZ$^v9DPJ)m9JMIRihRdSQ`HA9S2mK7bgi5G|^M z98$jpx|jx_Gc?6FfQp*klmP6*h{c(_TUmD&KC;~H(ccE#r)xa?wQsI1?Y*)5T4~|` zTaEdWorp3FA|xk;IxW22-TKr~pD%&v?YSM%B2 z#oZc=Oh^-)Lf7S@ajcON@RdG9ar4BkJd86JRxjBA*R>C~_P6TOm0c~KVRVtT?cE#|PC zWr=jcgE}M-gX8Wwx^m2Y!DJ%r({k2AVf`^*0EpP98_sc{nI;0*Kpts^K}0aK7}7oi z1?eFwFdS0uZdzx`Lm!*J;}r_`J?tX>D-2vLtS=8YAQ3Cx#7Ig}u!HWGV*1Z-)Qr?7 zk?LC@u$KAczVcI|{FHG2#%JQqhWL(k?3+fv_S_w-1+|{!GP#oOVwqJ7OK4PCzK)ZQ zrW7^d&P&C$&+tHD;$n?f-iXw@pd?$z3ksLNhvHC$L(3eVs zCSY#o($B{n-~E7Q3+2P8J2|nE+4a1>bk6YIkVy>+mocMP8sm(IxE_?iJa+Bx?1xM| zY)CfMMVPfk;3~xr6%%4lHEwT46*x6&+2${NU9eVc)xGZZi%Ax;ZO*Zpb*^wh|9Jvov=-NI!^cqc>BK7<8O_l$9 z2t7L!{kgwDF|~N1t}=U3=m*y9m@inf%P#G2V&1_fffkK*8Bhpd`Wvra10bt58UW4@ zvCW6dtzYopz3Z%#8S-KpWHT>!U$4hh z?kJ&cSpZ-}*b3Xj&`gdUJw&{Pq&Fn{!2>%#yJ;H@r~z>(^wr?Tl9YO(HEJeYezSUa&WH#YVLj=$VLj*4AaxRx3O%>j zaH-9*-tzrq%%rhM;Hkkj&IqW)%zLlolc*4&>H)axo@DxW9h6$ZE8b0)Nq6DxT~dJA^>NvA@&%kd#av=MI{Z{2m&S18yjarnt;O;v#r2 zJ#=ircQNzXk^fP~K9-8ef|_X%yl-1@jde97L%{bJmM|ruU!MmAerRwTAj;US!015tu`yl+TXt4F68Y-;6dspb>RhNQB*m2MK^N5goKY8kEIWP6ELDUz9a?@=1pmR zgyl{0FkJjjMYV5TCnc@#+S%@61&*F4K_~77ZywP0n#L$hCwL|^p+$z{)Bb^WegBCs zt2g;i{Iu}9DcDVk0}M(P73nOTSU4XZ5I}{Q`8D;?V_JyR+LV@ceR7qdm+$;>*g+`O zu(H^6M7TOsUq@0E517;r{#ZJ2o1=`E0FOOTS`el}6)(ZfDGq|-X{$b4UO{KZXCM(E zh$#|hcNktf=VGytG;uTZwz&mvgt8?JA>X&H&P%N6$|If&9vRzZ%s+p<-Is*t`3gh+ zo9teAvM+K`KDFf~z&&;4Q=SX;??XO;6Qhqc8!QaK5FhR>2p>Tep)72Qb6*;>bLrWl z-9&MN$*9QGgWxHqVQgc|(BK&zift~IT&+Z{=_Vns(;Uy8BET|e>2)=R%kxB#*MLkun5gRjqa`?*Wktl{RWQhtF z%m`cfB#E`sC7R?t5-5phcQ&!|N!jF55rehw4HBG~xepy)Ho|I4wXYgl$>C~0dg)N6 z-M{F3KUqG_s$u4fG+{4t1@ov?##+@q4^Yo(+V674RAuV?h_L0;^ZIO1-U3V^eT{i< zQBew7rb2kK-yWXdpLL zJ^|uRB=EeiH*jxcGxNjxjdkxWDuI}evPJ%C!r5EsRU{ohLDR!6|AL@@x)`pEhYJVr z;b!yO$xmCCI&+FAwFx_iP9s&O`PdOlUO{yuD7(G1b-BS(BIa z@vLNzB`R~8)@5QhSTo{$J2D%M8yGxKZT|kx;3V&GV2D{b>nj2j(`WA4RcM0i)WD z)AZgQ0r*yzGHvW4g4C*-iKWW$sAoT)bmuPX2es5D|AJsXM}GHhZQ#6pnb>0+8CdJ-$?S$gy0IN?JS+py7DI`Au6ZPAG|r5p*MD`dPLdhf>S!ar7Tyt=p6t zt@dX$9W+izrRx2Xs&_3+wSqH8W&}=5+J~2k5WF!SyokUrxpIM+%|2>nN{{!zIpbp; zN%g`xR^U9d4HPDWNzQ%MOWNdsRQ|cCWufu~9RPYfF4C72p3*TztIhSx#lDH|LY?Yj zndJbYPqUW50*2~r9%zF~3BTQw7w1dmLgD4At<3)rG_9`_o^yCqD!=_Qi-AFW%Asvp zFaGtfpelV_flYz53Mto58SrX~3^+@FxA%9?J!72fm(TN#5+ptP%j-$s{{JLXwks$< z`WuhTUu?BkyaM1)0}-7S8=WB=olPO?ouqd*Hyy{^6Nl)MW)Jnfw>E{uv9S0!!$a%O znXZ;HJ$+puuLt#qjA-V)?TX5o{a@|n*y(9=J z0a)VYT07thEbLl9u2|G)Xhsm=_`34I?^rb|MrE(#8FAS1qH4S49=JkgD0Eoj>4USp z+#WFc)*n?E=zWRA=*p{jdViBY?MwGK49Nc*F8Ax{+J?(^FeL5|Kg!T&haAK$>r`KE zfE~tJSU7FpTh2S?9|KyHJ=;5(Dcuxu+erTD2hiX?Sa_GA^ltu<0fo; zPOK7eE?tn_nq3sjH{!@U6)#*o8IyyGO^9~RDQ51}c^@=4vpuF2lxGSbh8(3zQA!TA zda6PW%z3n&gCLV1U1iwqYvp&bznsTeI@$jF_K(rmRAuQdT7mkqZW_OwTVTQ?P1LB? z7EA%<`Yd*e20N^eaEtif$dH0uco=~vOyr&c-X~INca+c49SxA?tzj%4yIP_aq5?JE zxcL!_!3^`kFrXIvZMS?gh{)d~-ZcNPkpb^qe7BLzwB0tUl)J2By#UJB{<*-oVcfNP zcoC%d?0r8ttzJ5~FUIn3il+Qx^N^7)Qc${aI8_gZmdq2FW9!>McbRnA{1<7^xvdB1 zC928@n^rfbvYpd-np?!RmrZJJAY7VQ=lWwXN;uWw3$iMf7DAs|xtNcc-CiApe_}T| z@uxlf_1NXXLH?2ErmnSA<;9?mBr~*751jsiJMJjt5Y=DO#i5|{D&OKMI?mJ6``Y^s z8gD38Cs|Ehat+%e^H<)@W3&bYE7^OOgnA-sCo?El`)$tKXJJL=PZR&Vc>vrzegT@D z7MS!J8MMjYLmBomMwn3-87AY zpci*IL;UkCye1^h><;@u(;x$!gjM1vVZoa0yyb1`iI8hEl9f!-*FX33r2C}rWxww6 zJTf2+x~C8-BlEvE88d1di~EViACnN~P7Z^A=O03-HR!E8z92cv_d*Lh#4|cyd@kdf zfwllpz_%_WyYA=vG72N`c08x?ZA~B5=C|%~#y5(r0{7;5S5&Ic5xl3P1skvZ?3&6- z0QZcqPFU&DzFpu{TV8Jml;1aAMtSd)!GBaqYFNJ?%tj=R3*%R5=9sXUgkjm1>t1s) zRo*-GqLa;hjwmK}4TDlwg{)QDj_ePFoe(QITz-NY{a0YLmrLWwj6avqjNF7}%y`=C z;T-e&TqWN{Ncdm2`e45=(YAOh9%QDeW+4Y3R8nc&=p4e1!fAATghcX;@2Us2r?l_x z2^m!4kA|^Pi9#%DRgrU+uC_z$mHYi+$7}*|C%|46YMU!J2}uciRjjQtT)lWQI@$TH zoo+%uqclnURD};Kx5$ig&lKl*;mXTm)|IeKgqe-IR@|z8p5Xx@=thkI7R_I@fv8^*HbXt#_7b zj2YyuDo@vthvDndrg6lV!$BPv|B3Se5+6?QcjA=IeLf`iD10Du)teP`3v`-dD`-)l zB{646W|X^mabY5fo~bo^A_VcXYl()&~z${zd$SftdU z&MQ)<>kPOfDfaMGC%*+#X{Dn?CSZLiW;y}-3Mw5T{mQc>L+v-+Z!6zc`(XPliQq9> z;ke}JVbqyWnKMMg-A2(>_{j^&7R3GA72WP{9i+<)Ew_v!Zz*!X?;S~hZZN|UkehPu zvw3p`F|D?0?@(z6^iTA9g@nRuO>mT0Xf;~0gtw5!4yPw<49+cc9*h7PMo}3i7ER(5 z`Fj%9HiAQwm$Dssd*l_be%-7WB4(|D@);(CYB;O}9o7VP4$(Y*sx@$}PT&t1!(eVE z-(`XO(@headDZHGo9S`-c*(j=!x~{`_kbtx_&*p;A&%%%PoEXuMb zYh*A8yv;#I2!;2Q#{LiOu=mWrmI-m7{8oR)X~+q^}7$lE`@R`~pRIPfUSG9-J~g4tSR z^+vhDH=A5opWwe`A^%tLPa!!{v)Szn4UPreh$#KxzvP@2On-IRj8G(p{dJ|kjM%ZR z{B>}@GB_^!aR^e}@N=zmEv_CfHO>ycx(66@_@@8aixh|A*DEFmqY1Ap2k*UrRnN?X z!fO7COq*-#TEpXG!UCKm6aC(K-8ewkdfC?V$IJ=o%EdM9F=t>)U8)@89#LY{_VJNj zke6`^A!sp6X0O8EdAkNKNmlR)IklX#@2U+Tt#8)wU7v0d7Zh}eA=gVgB1dvE79v;c z-TxM^aQo8_qkzP+xVvUNr&hgqIZv-fX&IZ7q1s%C7u+5KM5F z{bq#1#$odEPt?Q>kLE0WFh5EL%>b&ZwG&LDS>xCL@vZ!7>ZEyEkGdI=u{(QeZKrWv z7?>Gk?R|C=#nI(!XZV=|2Idw4FLo>NH2)Y&@C^CQHT{d#+WOI3b+g9c6REMNqnX-= zdYAZS`^9JZX9PE!@3dZhv!yfhie$xG{@&~NpZrrdXY^)27*jLbWd^GFzztSf*Iv+# zmWy#hBwo2SfAfalakgUJ-<*lPEpPxpnK?~1)?bXG>o9AC8|*|kFj|q|o}+@8I&b61 zy35^TdM3a^A3QIa>K>7r@_AA|>J*=J+*j$=9&E%_dD>k>!3o#?O;^k)*@FXiF^HDXJm^KxVjUSP&)Ixs z+h(tx4_&Bg!QAVlT&)jrHv!dUzd~0It1bK*tczLOKmW#rzg_X(timU9dM literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/icon.svg b/apps/desktop/src-tauri/icons/icon.svg new file mode 100644 index 0000000..e7835b5 --- /dev/null +++ b/apps/desktop/src-tauri/icons/icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + API + Opsx + + diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..ba91d70129da7c01ac9485e231d410a6c8603d33 GIT binary patch literal 349 zcmV-j0iyniP)bP* z<~Q%Z)bHN2kCA~w@bcv=5cQw=*{e4VKYsjVpa%H#=`$lE2d~I~1}Xv;3=wSuhX0xd zL}_Nceu07E>IM7;{?`H;7?Ob3pc)x|1~VRZ2LF~W21Y^#5(9RB_`&e>*Iy#Nj0Bj% z9k7a@hGIri4Wv$_Ac21?58#fFH47LRS1%Y~7lK@YQ|{UYG7Mb3fVwUulpcvn*eC{u z;x~{16ftN9GU@5)P%STm%wn;#wPLt*op5D8q*jpBQKfES1(NhUgJP!9a&XjvRB+(Gx(Q@k$`tM}8iJz^9R3a@r=T7U!BGbe zmV%m;7WHTpQwo^mGEZU(8#y}i*8V~ zfop5D8q*jpBQKfES1(NhUgJP!9a&XjvRB+(Gx(Q@k$`tM}8iJz^9R3a@r=T7U!BGbe zmV%m;7WHTpQwo^mGEZU(8#y}i*8V~ zfDST@E7F|gENFmWLeg-BwGNZA;JiH{Aw zg0HwJqAXRw#0^2pD~1K7BpQi{R2~si#yM?Nifv5`h3TF9ui8m7nXmu3_ntfV97$G$ zDDsSZ(cbY2U2nVL@lN1FzZ+_jtsoy;OeWY03t%rULY8@V)Zep6zy#`OY{r9!ZAd1S zDA3NPhKS%{fgO!bG$^pitg~lM!+F&a9e8TWhohxs=<0q49z<^6zS#nkDK$Ls@zQd* zl<~@w2>Nl`y+7Y|i^5pu4VFYq#1`UkzuS%d)&t;KBLlsGi7epo`I%{z~ytmU|-Dl(WyNI zkE9#Xo3|6MEj!@K%?N-mN2&%+)iX3@ZAGdSe9XN@1L`u<_?@)&Y%u(DZ5PiD56{ix zyMH0%+WYAr`~v&@GERP+#PDno{Q1`zT>s?bdmSE{`w9EV7j-8dy1OmGbuy6}8R z1RKX1DN;)TEPw^@ngAziW5J%af<0{&4hvubEPw^D0FG(-c+&|?eqQBTcMR{uJlkx6{hw>2e}`|#7L}x@?q+4;N9hr z@?rW=S#gS6muoLn$-kRELRtWhohXyrUv@IyLix#~`z&k)z@1w+7*FGwScoZN$pfv) z^lwlI@DRqtvNX?#+Nf1keKF}moLp^oUSTmcYApqKz35O{@p@#cH&&7qrnYpEGAK$a fN)8wQw-xUfpxPhJ1TMZ%00000NkvXXu0mjf2HSLq literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..b04d76f0b2857b040332b29ae09aebd4aaa4e700 GIT binary patch literal 445 zcmV;u0Yd(XP)Knpoq<}w&)btBJ3F1h&c+%p#t5?X~hV1JX1T#5a0Mw5S*jZaMFdjU7Ij&oi@cc4lmf zD(L9tNE8DDLx{-d<$smSDMqx&^G&iC@9D>VLrMYh@Y{-cOw)yK#3rV8BA80+A zV_DKsa&lMm9@9j1In}CT_vzq+yt8>nDXzRglH%&*NlcpPhWf&$j;mgb9}PbK9o?<| zom{zDJR46YUcpAhyh4(QjV30m!?~ebP9zyPmR*t*>pHvR?95D>XHH0x1ZH|7ULj_z~W(Yeuy%;I-kpUz<7%Qya_+fXUt z#^!2Gd-wP>vj6yd7Q=!%B~81^X2JIL75q7xRpYvnd!~2vc6tG0*{}KbophGZF-mHG zGYfZxH8&Y$78PSXN7;)fxvNzASRB323kGp?_7gB36P1ww6`%sNyr9al&fHaMS@uOsb6J*KNfxdbBmm>UP+}sFkZ*! zWb!r3Eoq!NeTv(iW4)IzG0lG6{RfXI!_iT$fLodxm1jc_nP!oyKN4aApO2eI4Y3f@ z9}B^>Q~B1-Yh1t*1F;nDt6%!?KwHqXF{ZeZle&ioemP6Iz8}5|GgVF0SL!qMy%U|1Ec?=O4RXkJ-1UuT;atC~V0AIBvQ_$P;^8f$<07*qoM6N<$ Ef>wcNH2?qr literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3d4ca88f799c4efd469f44728d1cc592faf7d3cd GIT binary patch literal 774 zcmV+h1Nr=kP)Ij&oi@cc4lmf zD(L9tNE8DDLx{-d<$smSDMqx&^G&iC@9D>VLrMYh@Y{-cOw)yK#3rV8BA80+A zV_DKsa&lMm9@9j1In}CT_vzq+yt8>nDXzRglH%&*NlcpPhWf&$j;mgb9}PbK9o?<| zom{zDJR46YUcpAhyh4(QjV30m!?~ebP9zyPmR*t*>pHvR?95D>XHH0x1ZH|7ULj_z~W(Yeuy%;I-kpUz<7%Qya_+fXUt z#^!2Gd-wP>vj6yd7Q=!%B~81^X2JIL75q7xRpYvnd!~2vc6tG0*{}KbophGZF-mHG zGYfZxH8&Y$78PSXN7;)fxvNzASRB323kGp?_7gB36P1ww6`%sNyr9al&fHaMS@uOsb6J*KNfxdbBmm>UP+}sFkZ*! zWb!r3Eoq!NeTv(iW4)IzG0lG6{RfXI!_iT$fLodxm1jc_nP!oyKN4aApO2eI4Y3f@ z9}B^>Q~B1-Yh1t*1F;nDt6%!?KwHqXF{ZeZle&ioemP6Iz8}5|GgVF0SL!qMy%U|1Ec?=O4RXkJ-1UuT;atC~V0AIBvQ_$P;^8f$<07*qoM6N<$ Ef>wcNH2?qr literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..ddcdddf83ac5f4e47cf919402902a000a971930f GIT binary patch literal 1095 zcmV-N1i1T&P)P^D*~)vf_m_ z^3nN;8lN>&ZEYQWbGgXv_E4MMK}KOkl8wyfC|VF5O*uJPl(8m_;;lC zN`83%E=5P1RlmoicA>GUnU>j-Ayx&BM)qTOIJC*Fymo^&W^JK?0XHlLf&HYUt@HVs zR4njcf?oi%(bC*NNj96hTrROzr)OfthJeLlrZ0XMnv`Rj`XF1h&5hgB=44(@Wi99y|ptFA+=Ah^*QUA?}Qn_ydo?n`ik@uI1L zq&B-tR)%!q@HCgZeC4#47&kY9dWL>al#5sh z4ReGT{=lF>o&#^cQR&F@NdNWovf9J`N8xHQ@>f0wA3wkjtVxeX5C$)Hf2E10$Hwd}m zvj4-^34*y=xYCDeK}a;@clY~h+}OD?b$%ZT8W)fo0^B?VY9I_J*R(AYdkX+^As2E1 zjBww(gqJH!n3lAnNNisNB3MmfP8CT)Bt^0J)G0xd3t@7jgmQLaw$B=}yJe zzWe@O1IxW#ft3p&7jhvNKrZA$?i4ZI9!}|K6)T zg-3PO5F3J6_V#bLJ7JgwAAwR{RtjSp#Fm^sCH;GJOwc2{_>7(nS(_0NlUT8V#0DM1 z!Y|+q!LFJ5&CBOUMeKiP+;E0)r@=A7^&6Cq)UoV?tAhXSC~7-{(tC2pg|hNlS+4Pa zFn3-BJ9p@Fz~}gqjn6^2S6ip>RC)f|NWxhpuS(vIop5D8q*jpBQKfES1(NhUgJP!9a&XjvRB+(Gx(Q@k$`tM}8iJz^9R3a@r=T7U!BGbe zmV%m;7WHTpQwo^mGEZU(8#y}i*8V~ zfVoGdz)uug{|3jW_E2I_y31s=L43VAOC0G=Y98`=XD5G zWJ3NxfL^?MO`Toc+HpWpC=?2l9hIH!BA?f*?U(E>rh^A6sLc>g%il91f09RrLuP@CPWJ zwYAVkZ`z*4bfYdne$MUNe<M2mFcv4r@{eRYZnkMb_ou$cO|S7NlCw(be4pcnL_# zbmkz06G&$OFhMzh0003H00A%wDk~>ZT5gZj<6{TWhmNHMP^`}-v88B9fpTR?w0ZSP z>Ye@-ePOVlB!qB$HRikNWsy zvVK?po1PfA>4-GXr7lbDTe-(tPnT-f3*O2zUm?tRh7iqbgW# z-?>Nl6$K#U=6+t$+w)%1Edh#9HXgdVFB|fOx9!g*bno=x&9QOCe_NAF{Q{ysJVRxW zBM{lD9PfDMd9|>ORbyX{P2ENOuxz=0mD16Te9wIE%phJ(eKU&@2({KKE%zP^_b@m~ zRu?0XlCl^9=`V*NIy>i literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..59a535fde52f2e17b2ff5cba974ebf8afc02f00f GIT binary patch literal 987 zcmV<110?*3P)VoGdz)uug{|3jW_E2I_y31s=L43VAOC0G=Y98`=XD5G zWJ3NxfL^?MO`Toc+HpWpC=?2l9hIH!BA?f*?U(E>rh^A6sLc>g%il91f09RrLuP@CPWJ zwYAVkZ`z*4bfYdne$MUNe<M2mFcv4r@{eRYZnkMb_ou$cO|S7NlCw(be4pcnL_# zbmkz06G&$OFhMzh0003H00A%wDk~>ZT5gZj<6{TWhmNHMP^`}-v88B9fpTR?w0ZSP z>Ye@-ePOVlB!qB$HRikNWsy zvVK?po1PfA>4-GXr7lbDTe-(tPnT-f3*O2zUm?tRh7iqbgW# z-?>Nl6$K#U=6+t$+w)%1Edh#9HXgdVFB|fOx9!g*bno=x&9QOCe_NAF{Q{ysJVRxW zBM{lD9PfDMd9|>ORbyX{P2ENOuxz=0mD16Te9wIE%phJ(eKU&@2({KKE%zP^_b@m~ zRu?0XlCl^9=`V*NIy>i literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5bb0874243719cb7207d083c2c3ec790acf30cbe GIT binary patch literal 1478 zcmV;%1v&bOP)s?uLxEAW<}SHBp!*Ydc*0fa7`2GH7YLp2=C;^X~sk7FKAQ z<@xb{-uJxkdv+{}3K>LvJVDXuDH@AJC^Q_VR4Pp;BT)i{xvkMjHk*|^ce$v(u8vwa ztS7tOX4vOT*D9E$4h9dCzb^op21@sN+o+{^ohrSyJc2_bVe0McSH*q~BMXSnyP4X( zo5Ecz!ye*p7 zmeHEUXw~d8M{w7UZd6AQdi?<1I-%T-@irq|bVY zem*Ox;N>ewx%KwNEm*AqTC_=NT2XcR`_ZYtFb0dl1NLg_TxUC!UzEHb@tqm67!BOHM3PgwlV^RK=Cw++OzORU{Y*1`rG&7(g(9U;x1Yf*}|{Fo0kH!2p5*1OpLn zT{%5Q(i_`h3<(Ah48Z_`0R#gG1`rG&7(g%t0|+iF%hYEbi154z^|u9SzfcB(vr+ho zEw8DfM5W@0D=Jo0|;JB$c+sR3d$fR?ABtp+fWHXVYjl!<3c3_ zh22`}YilKxLQL4L#p7|K5`xm&(yS)9u2vh2Be9rNisvry9SVz@5^WW-Q*{iMw96!) zZtRvStGZ;T zl=*PVc3&H2(}M8)wyjdm(?>mS7qzT^7%4V6?6A@4y!=`lgJ)mxSlr~gTS+FmDi1;s-EfQRC?0gHpbYBe~En#J)YdB<|@bU*(UxGp_RQ3d#llSEw_wqeBNFxx(eL zqs&>eZo!$7aVVfd+j0@P1;^lgWAc`#w_)N{K@+Nddp?x(9M+Kao($j_TKBte5n6V6 zTQWOf7kW?P=t^)VycqIg$E!$l8B+UR_l}(FT*C#=5*i6puRoyp_Xi-F7Ix!+*OrTo zwT27MO!UB^Ao>0MxNbIZ0>%5ets9zYzKJzl@YR#Cfx#ff6A2|03Tv++UDwGA?SLGg gq2gmUMxJ#251Mo&mzS<%_y7O^07*qoM6N<$g1Q8}{Qv*} literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3f96515901bbf32411e7186cd5d49b65b0481c84 GIT binary patch literal 13734 zcmdse2UJs8yXa0rQv{--*nkYq2#z!b1Suvef{KcY(h&p&kt!k}2szk5QLq7u3Me9@ zG?6ACBo>MyT~KwrGVc9-`|D-zv(NGBf&GSJ z3s)@!0Eq40rGE$j0XP%@B0}&Vy7Y#N0BE@F*57v6uW7i|&Gk{=xo_h}rPmKLLXVF$ z?OT3p(egz{tYy6R2bZa?uUvNEb;huwXN`7v_d1?ksh2~akzC@PIp;ksCwZF~rTl8w z+X=^a-1zK&`XV9C@Rp^d-M)`&b!4tA`u_NW_jt?AyAH;CejRNw%sHxX=!yQs*tytA zE+K7Ldt31X#rPrmXmr#-4Cp=K<@LM4?~!$=uAkiuyWPw{qO?k0zHgo-vqJkMfdCvc ziqNPnXlr_I_RhQ{E}JTm!qVYQ7k>B3%Fec=-Ah_V1P?cvN~iOV+{nl>1(#GS-R8&TUXxD*0$>V=xEdaV3X4whgXmV$sqo* zP{DQ?YU#Ru69)0e4%)@1t29WkzlMv?`z&HlTS!95JKvn8w#K5<2?

  • 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 BZu$TK literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5bb0874243719cb7207d083c2c3ec790acf30cbe GIT binary patch literal 1478 zcmV;%1v&bOP)s?uLxEAW<}SHBp!*Ydc*0fa7`2GH7YLp2=C;^X~sk7FKAQ z<@xb{-uJxkdv+{}3K>LvJVDXuDH@AJC^Q_VR4Pp;BT)i{xvkMjHk*|^ce$v(u8vwa ztS7tOX4vOT*D9E$4h9dCzb^op21@sN+o+{^ohrSyJc2_bVe0McSH*q~BMXSnyP4X( zo5Ecz!ye*p7 zmeHEUXw~d8M{w7UZd6AQdi?<1I-%T-@irq|bVY zem*Ox;N>ewx%KwNEm*AqTC_=NT2XcR`_ZYtFb0dl1NLg_TxUC!UzEHb@tqm67!BOHM3PgwlV^RK=Cw++OzORU{Y*1`rG&7(g(9U;x1Yf*}|{Fo0kH!2p5*1OpLn zT{%5Q(i_`h3<(Ah48Z_`0R#gG1`rG&7(g%t0|+iF%hYEbi154z^|u9SzfcB(vr+ho zEw8DfM5W@0D=Jo0|;JB$c+sR3d$fR?ABtp+fWHXVYjl!<3c3_ zh22`}YilKxLQL4L#p7|K5`xm&(yS)9u2vh2Be9rNisvry9SVz@5^WW-Q*{iMw96!) zZtRvStGZ;T zl=*PVc3&H2(}M8)wyjdm(?>mS7qzT^7%4V6?6A@4y!=`lgJ)mxSlr~gTS+FmDi1;s-EfQRC?0gHpbYBe~En#J)YdB<|@bU*(UxGp_RQ3d#llSEw_wqeBNFxx(eL zqs&>eZo!$7aVVfd+j0@P1;^lgWAc`#w_)N{K@+Nddp?x(9M+Kao($j_TKBte5n6V6 zTQWOf7kW?P=t^)VycqIg$E!$l8B+UR_l}(FT*C#=5*i6puRoyp_Xi-F7Ix!+*OrTo zwT27MO!UB^Ao>0MxNbIZ0>%5ets9zYzKJzl@YR#Cfx#ff6A2|03Tv++UDwGA?SLGg gq2gmUMxJ#251Mo&mzS<%_y7O^07*qoM6N<$g1Q8}{Qv*} literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2735556131a5074efe09a2e9dc41f2d3dd7baef3 GIT binary patch literal 2089 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eb659c747c2e8e6ca4bf59b95155e8fabfe68bdd GIT binary patch literal 1754 zcmZWqX*3&%7LE#)NLx%1YFt_BE71-bwT@AQD$$`emZBNXJMXR za0uh-C^@OL}U7>8IBtS_t|%&%2_X< zx7Re4yrA!K?W3`emc<_-F=Ox9n(h}pBofi?6A8+(=bA#zhoz;`^>2`KzNZYaeiZ)_ zZR~YQ!o|P?UzztbgW8!)1Qc?X_?w2Fp>FX`oZ{%MCW~Gf#e+jKq-TXdCWkXO{f75IN$Wns?Bya`Fl?KB33a*PJ{%*z zE)rfSa)w?o4P4XKE@LVOFG*SSL@eZyt7i~Hw;xu?@jn$uLv4ueE(?jjz8(!GN4lpp zCV(>D<`adI-K^P=Qdi{17!f)2OIso>&XwGzEmbfhwMD72gN$*Y1$gxLQ;~30L^g-2 zjEW^kCQa6CXi|aI^wHPA=!m5L@Wh_%qv(MIVj_9c=J&rP8LuQet6^G8s4cxR)eK!~ z#QtZw{)olFC3|1vD6`;M9hta&P*SvTvGvj^SsU?6**)z*mfD9VbRU5K zh%z^E>D1*j);)<~@?@T};UfifmqHFu`YfVe##yMt|Hh0isLVP^Z<36YK0qJCG9+E1 zWC9$Da9IaBF3$HC)jh!coEkf}rq>?3Zhn7jj$GNf;~5}9pQD@F%tduw$6jLBy(&MN znXGeN5GW^`)fRiLMC4I*ij^tl6ZO9TJCv(e>qX(Eo7lpbi@$^dr8nHWb$npf?_kRj zpR}~~6d}p8Bh3;R^zrP@Tr)`WW8PE7!0lV;gwJa$cik9vcJE@kQpawnaeHh!QAJBsShBLY%h+dRhASz&e%A z4*D3@Th2lkeZDS!)P`&2U@H77R^+~Di-C>YSvn0U*DTvwR0&t*CX2*+dj-G+C_whH zNB% z{l26SSH@;0ok;m=;207&d=8grP)od!wZE`QeBj)gdD@ zXyr@SG~-6xEj;lAT*?+?>32XL{0WkNUVchG2BW2heWK=q*F=@T*mF!;ku=H!|AiaYV8BC1JE z%N0b5SXrHY4TST0me$k@@PkJuo)8S;?7bV&ZKcR`gnv=pj5dpDoF0)sAZVKvjr?3` zGMlnF+1;5huXGu4Wd=;Y0Yy(G=bo4LYcv>4`N@B#q*vFo*l*0JrRIll6s+0LHtOlr zuc$gW6-;jyxL~b4>}28Vfk7JE&!NlM4y)l}zuiNp&X7Yz>OQTFq2oPTYLF7QD&lAB zmyOy5aUi}<=@Ky8sC0bD2^021csKz7y^Dof*_q}>nF+Ex{4@fsx#-C1AG@d8#qk(8>zDic%-bIS?BGE!#jjh}wX zmxiWxYno{y4w&uE66O&J%UTod99}yIHs#ob+s>(bV&HK6{RoorN9`zXb*4fBXkQQ@ UGql>D@BJ(Q#@WM(;eaRq39mLRPXGV_ literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/apps/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4c43722c6e4e9f5c71b10ad82b135c672a11dbf2 GIT binary patch literal 1947 zcmZ{leKZq_8^>KYxyoybi<0YjU*3~>8MekSM$M%xL~1Kr%)G1Ji}fNRbi8CH*+sH> z$y*rG$h=HO$h(Po4Vh?Yw|ma*oZs*K&hL-sdH(vI=bX>?d!8rJ9RZU%qO`LBSqTqpK6H*rQi}ZQVd;irHQ_j*p{WmG4&@QAkeA zCY}m8M?LaT%emlyp;~sE#B0aYcAX84hqCo{-k4c0D25gyTRWkr4M%|=KEa}>a)}d1 zzyKrm%L^l!`i*0&J%~o3Ux3CtPx3x5CweO9CZovy_)+O?I062mx~2|Z@&X1ac*NiK zOCMN0AsVTpYmmG*#@kE`j!4;oKL`X9n*-5k3&FM#`Ti#6Y{qe$QJoVf@}R&JeVtCl zmSgl5>II6S&l%z@c5UI2&&f|O+pv$Cn~A<3B(9BCpSt+6)TPU-MNe$VhKtO7v+{an+d8C|!Sb-O(iJYIm*~NLwq?04Z7IMdGa|SA#4b zRJ@0Ui#v>$q$(8~e9brTZCgB^a_SLrVaH&83*9pyXplNal7L~*Cz??cClVDbZ9!Ycb>+Mw08M1gS7Iq^g1MoOdtAq#VPf3O+inc2 zXjAr+ge@{>Mmf&Th*M}D`kOM=45$&(+orSQN`(cN##e1fZ}%5m9|wh77Khg(oX!<2&Kb4RvdJO(TH5#J)ey4Uc4Rfg zpTM8E2k@dy3GRXWrYV};%u7wlUY*xESXycFuFM=sLWhen&ycFxn0H-1fh8e}?&x|F z*x3|YbV2BnNr`*$d92mUf-vXDg~k+)Q%qfbiR;Z{-^zITFRBWQ-BRueRU?Ids=lKe zxMnj=H)|WfwQN0}LHP>B)Ms)NWJeB(2pF=lJX_VY;pgx$arZ?7)aY_574ZG=rotIhS9|w}M66L=Ppwjbw_5cN`jIh-P?BRV~ zY}*@S6m8lDICU~Ifc|Zb_l34$Nr}(7<@1-Ip-VWlle0rC;uyW(K1JB7o?Wd{$ zW+|m&-TEJlY{?l5cO2uxPG8efzXAV9F#wZ2RVSyB@h@i)eEffK{sEtz$mXMLMf`o| z8GTK16-2eyYYgV%5-=-p-i>F(u9f>Ahkhvk%Qkstj$CqvE1j4?WVN1jwj=lBKmUCA zhsFPAwd8axGv-Q^FaNi13*oD~?Pw43@UW-stWk&dZu?G@?Z1(bXG;9Y379N`7p=UC zb~6KM+TXE0^wY3zXE!>ujbUvwg-sE?1Z(6AZoN7#QE$Y)wDJ82NlJ^2J2dhQI{SEz zt>M*hw}^<$8k4Ro?b3?0!A+M{4&e$(Arw0O3>h;+bB$_g=`5LK4FCQIcs6-!a&>6C zt~G9Ur53nvJ_Npdz2C2==k|!?J;O?1Jaw1t9=u(iCJjnxd3Eh~CD_61iW4(5V7Hfv zS=u*Xp=Fa2I<6*+XJf^2+6CMn9pyuX{&QbwE^gG`^>25Xi-raPG3q!Za#zI|QKUU? zVs@qD`BrARH;YfYqIZVgg1h0&w?S8j)ceiP>jzHBbD7Qasd>1&lk{XOi{M+3%#|^u zYj#CmgaV*PZq@_foCWL9zb6wxVb{D&V_MT-D3AI*5a1* zl5kp%@niO*@GAe58=($XoOe3QXFMw0AwA3RiOVemHuKqT2&LL{<#m*L#nhRB^No(0 zJ&(1~)pl_A(ukGQxd}9vbFA^Vll;YJ7i92MGlaTkSLFjvEl|8BGR)Bx$GAu+LDXyI zWe-0V^kM`cTd^~Ypky3g+7Bou3O0Q=u7?G)>L-tI%&p(80o=A?IA-{+irbulArGn3 z;=S%AC`bXlbB&-hO&_S<6P_UnZUzgAyaki?g&(Ay+y9smd@U>O_kH#K>54enBcOE1 H<@mn=3Esa| literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..33da137 --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,136 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BodySpec { + mode: String, + raw: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct HttpSpec { + method: String, + url: String, + #[serde(default)] + headers: HashMap, + query: Option>, + body: Option, + timeout_ms: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct HttpResult { + status: u16, + headers: HashMap, + body: String, + time_ms: u128, + size: usize, +} + +#[derive(Debug, Deserialize)] +struct WsSpec { + url: String, + protocols: Option>, + headers: Option>, +} + +fn validate_http_url(raw: &str) -> Result { + let parsed = url::Url::parse(raw).map_err(|e| format!("invalid URL: {e}"))?; + match parsed.scheme() { + "http" | "https" => {} + _ => return Err("only http/https schemes are allowed".into()), + } + match parsed.host_str().unwrap_or("").to_ascii_lowercase().as_str() { + "169.254.169.254" | "metadata.google.internal" => { + return Err("blocked metadata host".into()) + } + _ => {} + } + Ok(parsed) +} + +#[tauri::command] +async fn http_send(spec: HttpSpec) -> Result { + let mut url = validate_http_url(&spec.url)?; + if let Some(query) = &spec.query { + let mut pairs = url.query_pairs_mut(); + for (key, value) in query { + pairs.append_pair(key, value); + } + } + + let method = reqwest::Method::from_bytes(spec.method.as_bytes()) + .map_err(|e| format!("invalid method: {e}"))?; + let timeout = Duration::from_millis(spec.timeout_ms.unwrap_or(30_000)); + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .map_err(|e| e.to_string())?; + let mut request = client.request(method, url); + for (key, value) in spec.headers { + request = request.header(&key, &value); + } + if let Some(body) = spec.body { + if body.mode != "none" { + request = request.body(body.raw.unwrap_or_default()); + } + } + + let started = Instant::now(); + let response = request.send().await.map_err(|e| e.to_string())?; + let status = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + let bytes = response.bytes().await.map_err(|e| e.to_string())?; + let size = bytes.len(); + let body = String::from_utf8_lossy(&bytes).into_owned(); + Ok(HttpResult { + status, + headers, + body, + time_ms: started.elapsed().as_millis(), + size, + }) +} + +#[tauri::command] +async fn ws_connect(spec: WsSpec) -> Result { + let _ = (&spec.protocols, &spec.headers); + // TODO(week2): hold WebSocket sessions in managed state and emit messages. + Err(format!( + "WebSocket native tunnel is not implemented yet for {} (use browser WS in MVP)", + spec.url + )) +} + +#[tauri::command] +async fn ws_send(session_id: String, data: String) -> Result<(), String> { + let _ = (session_id, data); + Err("WebSocket native send is a Week 2 stub".into()) +} + +#[tauri::command] +async fn ws_close(session_id: String) -> Result<(), String> { + let _ = session_id; + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![ + http_send, + ws_connect, + ws_send, + ws_close + ]) + .run(tauri::generate_context!()) + .expect("error while running OpsxAPI desktop"); +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..4a72382 --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + opsxapi_lib::run(); +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..9a3bedd --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "OpsxAPI", + "version": "0.1.0", + "identifier": "com.opsx.api", + "build": { + "beforeDevCommand": "bash -lc 'curl -fsS http://127.0.0.1:5173 >/dev/null 2>&1 || npm --prefix ../.. run dev:web'", + "devUrl": "http://127.0.0.1:5173", + "beforeBuildCommand": "npm --prefix ../.. run build:web", + "frontendDist": "../../web/dist" + }, + "app": { + "windows": [ + { + "title": "OpsxAPI", + "width": 1280, + "height": 800, + "minWidth": 960, + "minHeight": 640, + "resizable": true, + "fullscreen": false, + "label": "main" + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "category": "DeveloperTool", + "shortDescription": "OpsxAPI — API workbench", + "longDescription": "OpsxAPI is an API workbench for ops and developers: HTTP / WebSocket, one React UI, Web and Desktop share Transport." + } +} diff --git a/apps/server/cmd/opsxapi/main.go b/apps/server/cmd/opsxapi/main.go new file mode 100644 index 0000000..88f3fea --- /dev/null +++ b/apps/server/cmd/opsxapi/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "log" + "os" + + "opsxapi/server/internal/server" +) + +func main() { + addr := os.Getenv("OPSXAPI_ADDR") + if addr == "" { + addr = ":8787" + } + log.Printf("OpsxAPI server listening on %s", addr) + if err := server.NewRouter().Run(addr); err != nil { + log.Fatal(err) + } +} diff --git a/apps/server/config.example.yaml b/apps/server/config.example.yaml new file mode 100644 index 0000000..f4d3a4a --- /dev/null +++ b/apps/server/config.example.yaml @@ -0,0 +1,7 @@ +server: + addr: ":8787" +proxy: + timeout_ms: 30000 +# Environment overrides: +# OPSXAPI_ADDR=:8787 +# OPSXAPI_PROXY_TIMEOUT_MS=30000 diff --git a/apps/server/go.mod b/apps/server/go.mod new file mode 100644 index 0000000..3dc0e7e --- /dev/null +++ b/apps/server/go.mod @@ -0,0 +1,38 @@ +module opsxapi/server + +go 1.23.0 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/gorilla/websocket v1.5.3 + github.com/yuin/goldmark v1.8.6 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/apps/server/go.sum b/apps/server/go.sum new file mode 100644 index 0000000..71fb00b --- /dev/null +++ b/apps/server/go.sum @@ -0,0 +1,93 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/apps/server/internal/docs/assets/docs.css b/apps/server/internal/docs/assets/docs.css new file mode 100644 index 0000000..4d56f63 --- /dev/null +++ b/apps/server/internal/docs/assets/docs.css @@ -0,0 +1,246 @@ +/* OpsxAPI 文档站 —— 清爽专业风格(浅色) */ +:root { + --blue: #2563eb; + --blue-dark: #1d4ed8; + --blue-soft: rgba(37, 99, 235, 0.08); + --bg: #f6f8fb; + --panel: #ffffff; + --border: #e4e9f1; + --text: #1c2434; + --text-2: #5b6880; + --text-3: #98a2b3; + --code-bg: #f3f6fb; + --code-border: #e1e8f3; + --green: #059669; + --amber: #b45309; + --red: #dc2626; + --sidebar-w: 262px; + --toc-w: 220px; + --topbar-h: 56px; + --radius: 10px; + --shadow: 0 1px 3px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.05); + --mono: "JetBrains Mono", SFMono-Regular, Menlo, Consolas, monospace; +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; scroll-padding-top: 72px; } +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14.5px; + line-height: 1.75; +} +a { color: var(--blue); text-decoration: none; } +.i { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -2px; } +a:hover { color: var(--blue-dark); text-decoration: underline; } + +/* ---------- 顶栏 ---------- */ +.topbar { + position: sticky; top: 0; z-index: 50; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: saturate(1.4) blur(8px); + border-bottom: 1px solid var(--border); +} +.topbar-inner { + max-width: 1440px; margin: 0 auto; height: var(--topbar-h); + display: flex; align-items: center; gap: 14px; padding: 0 20px; +} +.menu-btn { + display: none; border: 1px solid var(--border); background: #fff; + border-radius: 8px; width: 34px; height: 34px; cursor: pointer; color: var(--text-2); + place-items: center; +} +.menu-btn .i { width: 17px; height: 17px; } +.brand { display: flex; align-items: center; gap: 9px; text-decoration: none !important; } +.brand-logo { display: inline-flex; } +.brand-logo svg { display: block; border-radius: 7px; } +.brand-name { font-size: 16px; font-weight: 700; color: var(--text); } +.brand-name em { font-style: normal; font-weight: 500; color: var(--text-2); margin-left: 2px; } +.ver-badge { + font-size: 11px; font-weight: 600; color: var(--blue); + background: var(--blue-soft); border: 1px solid rgba(37, 99, 235, 0.22); + padding: 1px 9px; border-radius: 999px; white-space: nowrap; +} +.search { position: relative; flex: 1; max-width: 420px; margin-left: 8px; } +.search input { + width: 100%; height: 36px; border: 1px solid var(--border); border-radius: 9px; + background: var(--bg); padding: 0 14px 0 34px; font-size: 13.5px; color: var(--text); outline: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%2398a2b3' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; background-position: 12px center; +} +.search input:focus { border-color: var(--blue); background: #fff; box-shadow: 0 0 0 3px var(--blue-soft); } +.search-panel { + position: absolute; top: 44px; left: 0; right: 0; background: #fff; + border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); + max-height: 420px; overflow: auto; padding: 6px; +} +.search-item { display: block; padding: 8px 12px; border-radius: 8px; text-decoration: none !important; } +.search-item:hover, .search-item.sel { background: var(--blue-soft); } +.search-item .si-title { font-weight: 600; color: var(--text); font-size: 13.5px; } +.search-item .si-group { color: var(--text-3); font-size: 11px; margin-left: 6px; } +.search-item .si-snippet { color: var(--text-2); font-size: 12px; margin-top: 2px; display: block; } +.search-empty { padding: 14px; text-align: center; color: var(--text-3); font-size: 13px; } +.top-links { margin-left: auto; display: flex; gap: 16px; align-items: center; } +.top-links a { font-size: 13.5px; font-weight: 500; } + +/* ---------- 三栏布局 ---------- */ +.layout { + max-width: 1440px; margin: 0 auto; display: grid; + grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--toc-w); + gap: 0 28px; padding: 24px 20px 60px; +} + +/* 侧栏 */ +.sidebar { position: sticky; top: calc(var(--topbar-h) + 24px); align-self: start; max-height: calc(100vh - var(--topbar-h) - 48px); overflow-y: auto; padding-right: 8px; } +.sidebar::-webkit-scrollbar { width: 4px; } +.sidebar::-webkit-scrollbar-thumb { background: #d6dde8; border-radius: 4px; } +.nav-group { margin-bottom: 18px; } +.nav-group-title { + font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; + color: var(--text-3); padding: 0 10px 6px; +} +.nav-link { + display: block; padding: 6px 10px; border-radius: 8px; font-size: 13.5px; + color: var(--text-2); text-decoration: none !important; border-left: 2px solid transparent; +} +.nav-link:hover { background: rgba(37, 99, 235, 0.06); color: var(--text); } +.nav-link.active { + background: var(--blue-soft); color: var(--blue-dark); font-weight: 600; + border-left-color: var(--blue); +} +.sidebar-foot { margin-top: 24px; padding: 12px 10px 0; border-top: 1px solid var(--border); font-size: 11.5px; color: var(--text-3); } + +/* 内容 */ +.content { min-width: 0; max-width: 860px; } +.breadcrumb { font-size: 12.5px; color: var(--text-3); margin-bottom: 10px; } +.breadcrumb span { margin: 0 6px; } +.page-title { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; margin: 0 0 6px; line-height: 1.3; } +.page-meta { font-size: 12.5px; color: var(--text-3); margin-bottom: 22px; } + +/* Markdown 排版 */ +.md h2 { font-size: 21px; font-weight: 700; margin: 38px 0 12px; padding-top: 8px; letter-spacing: -0.01em; } +.md h3 { font-size: 16.5px; font-weight: 700; margin: 26px 0 8px; } +.md h2 + h3, .md h1 + h2 { margin-top: 18px; } +.md p { margin: 10px 0; color: #2a3348; } +.md ul, .md ol { padding-left: 22px; margin: 10px 0; } +.md li { margin: 5px 0; color: #2a3348; } +.md li::marker { color: var(--blue); } +.md strong { color: var(--text); } +.md hr { border: none; border-top: 1px solid var(--border); margin: 30px 0; } +.md img { max-width: 100%; border-radius: var(--radius); border: 1px solid var(--border); } +.md blockquote { + margin: 14px 0; padding: 10px 16px; border-left: 3px solid var(--blue); + background: var(--blue-soft); border-radius: 0 8px 8px 0; color: #33415e; +} + +/* 表格 */ +.md table { border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 13.5px; background: #fff; border-radius: var(--radius); overflow: hidden; box-shadow: 0 0 0 1px var(--border); } +.md th, .md td { padding: 9px 14px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; } +.md th { background: #f9fbfe; font-weight: 600; color: var(--text); white-space: nowrap; } +.md tr:last-child td { border-bottom: none; } + +/* 代码块 */ +.md code { font-family: var(--mono); font-size: 12.8px; background: var(--code-bg); border: 1px solid var(--code-border); border-radius: 5px; padding: 1px 6px; color: #334155; } +.md pre { + position: relative; background: #0f172a; color: #dbe4f3; border-radius: var(--radius); + padding: 16px 16px 14px; overflow-x: auto; margin: 16px 0; font-size: 13px; line-height: 1.65; +} +.md pre code { background: none; border: none; color: inherit; padding: 0; font-size: inherit; } +.md pre .lang-tag { + position: absolute; top: 8px; right: 12px; font-size: 10.5px; color: #7c8db0; + font-family: var(--mono); text-transform: lowercase; letter-spacing: 0.04em; +} +.md pre .copy-btn { + position: absolute; top: 6px; right: 60px; font-size: 11px; color: #9fb0d0; + background: rgba(255, 255, 255, 0.07); border: none; border-radius: 6px; padding: 2px 9px; cursor: pointer; + opacity: 0; transition: opacity 0.15s; +} +.md pre:hover .copy-btn { opacity: 1; } +.md pre .copy-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.14); } +/* 行内高亮 token */ +.md pre .tok-key { color: #7dd3fc; } +.md pre .tok-str { color: #86efac; } +.md pre .tok-num { color: #fcd34d; } +.md pre .tok-com { color: #64748b; font-style: italic; } + +/* 提示块(通过 blockquote 首字符约定) */ +.md blockquote.tip { border-left-color: var(--green); background: rgba(5, 150, 105, 0.07); } +.md blockquote.warn { border-left-color: var(--amber); background: rgba(180, 83, 9, 0.07); } + +/* 上下页 */ +.pn-nav { display: flex; gap: 14px; margin-top: 48px; } +.pn { + flex: 1; border: 1px solid var(--border); background: #fff; border-radius: var(--radius); + padding: 13px 16px; text-decoration: none !important; transition: border-color 0.15s, box-shadow 0.15s; +} +.pn:hover { border-color: var(--blue); box-shadow: var(--shadow); } +.pn.next { text-align: right; } +.pn-label { display: block; font-size: 11.5px; color: var(--text-3); } +.pn-title { display: block; font-weight: 600; color: var(--text); font-size: 14px; margin-top: 2px; } + +/* 反馈 */ +.feedback { + margin-top: 28px; padding: 16px 18px; border-top: 1px solid var(--border); + display: flex; align-items: center; gap: 10px; color: var(--text-2); font-size: 13px; flex-wrap: wrap; +} +.fb-btn { + border: 1px solid var(--border); background: #fff; border-radius: 8px; padding: 5px 12px; + font-size: 12.5px; cursor: pointer; color: var(--text-2); transition: all 0.15s; +} +.fb-btn:hover { border-color: var(--blue); color: var(--blue); } +.fb-btn.done { border-color: var(--green); color: var(--green); background: rgba(5, 150, 105, 0.06); } +.fb-thanks { color: var(--green); font-size: 12.5px; } + +/* 右侧目录 */ +.toc { position: sticky; top: calc(var(--topbar-h) + 24px); align-self: start; max-height: calc(100vh - var(--topbar-h) - 48px); overflow-y: auto; padding-top: 34px; } +.toc-title { font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; color: var(--text-3); margin-bottom: 8px; } +.toc-nav { display: flex; flex-direction: column; gap: 1px; border-left: 2px solid var(--border); padding-left: 0; } +.toc-nav a { + font-size: 12.5px; color: var(--text-2); padding: 4px 0 4px 12px; text-decoration: none !important; + border-left: 2px solid transparent; margin-left: -2px; line-height: 1.5; +} +.toc-nav a:hover { color: var(--blue); } +.toc-nav a.toc-l3 { padding-left: 26px; font-size: 12px; color: var(--text-3); } +.toc-nav a.active { color: var(--blue); border-left-color: var(--blue); font-weight: 600; } +.print-btn { + margin-top: 18px; width: 100%; border: 1px solid var(--border); background: #fff; + border-radius: 8px; padding: 7px; font-size: 12.5px; color: var(--text-2); cursor: pointer; +} +.print-btn:hover { border-color: var(--blue); color: var(--blue); } + +/* 404 */ +.doc-404 { display: grid; place-items: center; min-height: 100vh; text-align: center; } +.doc-404 h1 { font-size: 56px; margin: 0; color: var(--blue); } +.doc-404 .btn { + display: inline-block; margin-top: 12px; background: var(--blue); color: #fff; + padding: 8px 18px; border-radius: 8px; text-decoration: none !important; font-weight: 600; +} + +/* ---------- 响应式 ---------- */ +@media (max-width: 1200px) { + .layout { grid-template-columns: var(--sidebar-w) minmax(0, 1fr); } + .toc { display: none; } +} +@media (max-width: 880px) { + .menu-btn { display: inline-grid; place-items: center; } + .layout { grid-template-columns: minmax(0, 1fr); padding-top: 12px; } + .sidebar { + position: fixed; z-index: 60; left: 0; top: var(--topbar-h); bottom: 0; + width: 280px; background: #fff; border-right: 1px solid var(--border); + padding: 16px; transform: translateX(-100%); transition: transform 0.2s; max-height: none; + } + .sidebar.open { transform: none; box-shadow: var(--shadow); } + .ver-badge { display: none; } + .top-links a { font-size: 12.5px; } + .page-title { font-size: 24px; } +} + +/* ---------- 打印 ---------- */ +@media print { + .topbar, .sidebar, .toc, .pn-nav, .feedback, .menu-btn { display: none !important; } + .layout { display: block; max-width: none; padding: 0; } + body { background: #fff; } + .md pre { background: #f1f5f9 !important; color: #1e293b !important; } + .md pre .copy-btn, .md pre .lang-tag { display: none; } +} diff --git a/apps/server/internal/docs/assets/docs.js b/apps/server/internal/docs/assets/docs.js new file mode 100644 index 0000000..005b49e --- /dev/null +++ b/apps/server/internal/docs/assets/docs.js @@ -0,0 +1,219 @@ +/* OpsxAPI 文档站交互:搜索 / 代码复制 / 目录高亮 / 反馈 / 移动端导航 */ +(function () { + "use strict"; + + /* ---------- 代码块:语言标签 + 复制按钮 + 轻量高亮 ---------- */ + document.querySelectorAll("article.md pre").forEach(function (pre) { + var code = pre.querySelector("code"); + if (!code) return; + var lang = (code.className.match(/language-([\w-]+)/) || [, ""])[1]; + if (lang) { + var tag = document.createElement("span"); + tag.className = "lang-tag"; + tag.textContent = lang; + pre.appendChild(tag); + } + // 复制按钮 + var btn = document.createElement("button"); + btn.className = "copy-btn"; + btn.type = "button"; + btn.textContent = "复制"; + btn.addEventListener("click", function () { + navigator.clipboard.writeText(code.textContent).then( + function () { + btn.textContent = "已复制 ✓"; + setTimeout(function () { btn.textContent = "复制"; }, 1400); + }, + function () { btn.textContent = "复制失败"; } + ); + }); + pre.appendChild(btn); + // JSON / bash 轻量高亮 + if (lang === "json") highlightJSON(code); + else if (lang === "bash" || lang === "sh" || lang === "shell") highlightBash(code); + }); + + function esc(s) { + return s.replace(/&/g, "&").replace(//g, ">"); + } + function highlightJSON(code) { + var src = code.textContent; + code.innerHTML = esc(src).replace( + /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, + function (m, str, colon, kw, num) { + if (str && colon) return '' + str + "" + colon; + if (str) return '' + str + ""; + if (kw) return '' + kw + ""; + if (num) return '' + num + ""; + return m; + } + ); + } + function highlightBash(code) { + var lines = code.textContent.split("\n"); + code.innerHTML = lines + .map(function (line) { + if (/^\s*#/.test(line)) return '' + esc(line) + ""; + return esc(line).replace(/(^|\s)(-{1,2}[\w-]+)/g, '$1$2'); + }) + .join("\n"); + } + + /* ---------- blockquote 提示样式约定:> **提示** / > **注意** ---------- */ + document.querySelectorAll("article.md blockquote").forEach(function (bq) { + var t = (bq.textContent || "").trim(); + if (/^(注意|警告|⚠)/.test(t)) bq.classList.add("warn"); + else if (/^(提示|说明|💡|Tip)/i.test(t)) bq.classList.add("tip"); + }); + + /* ---------- 搜索 ---------- */ + var input = document.getElementById("search-input"); + var panel = document.getElementById("search-panel"); + var entries = null; + var selIndex = -1; + + function ensureIndex(cb) { + if (entries) return cb(); + fetch("/docs/search-data.json") + .then(function (r) { return r.json(); }) + .then(function (data) { entries = data; cb(); }) + .catch(function () { entries = []; cb(); }); + } + + function snippet(text, q) { + var i = text.toLowerCase().indexOf(q); + if (i < 0) return text.slice(0, 60) + "…"; + var start = Math.max(0, i - 24); + return (start > 0 ? "…" : "") + text.slice(start, i + q.length + 40) + "…"; + } + + function doSearch() { + var q = input.value.trim().toLowerCase(); + selIndex = -1; + if (!q) { panel.hidden = true; return; } + ensureIndex(function () { + var hits = []; + entries.forEach(function (e) { + var titleHit = e.title.toLowerCase().indexOf(q) >= 0; + var idx = e.text.toLowerCase().indexOf(q); + if (titleHit || idx >= 0) { + hits.push({ + slug: e.slug, + title: e.title, + group: e.group, + score: (titleHit ? 10 : 0) + (idx === 0 ? 3 : 0), + snippet: titleHit ? snippet(e.text, q) : snippet(e.text, q), + }); + } + }); + hits.sort(function (a, b) { return b.score - a.score; }); + hits = hits.slice(0, 8); + if (!hits.length) { + panel.innerHTML = '

    没有找到相关内容,换个关键词试试
    '; + } else { + panel.innerHTML = hits + .map(function (h) { + return ( + '' + + '' + h.title + '' + h.group + "" + + '' + h.snippet + "" + ); + }) + .join(""); + } + panel.hidden = false; + }); + } + + var debounce; + if (input) { + input.addEventListener("input", function () { + clearTimeout(debounce); + debounce = setTimeout(doSearch, 160); + }); + input.addEventListener("focus", function () { + if (input.value.trim()) doSearch(); + }); + input.addEventListener("keydown", function (e) { + var items = panel.querySelectorAll(".search-item"); + if (e.key === "ArrowDown" && items.length) { + e.preventDefault(); + selIndex = Math.min(selIndex + 1, items.length - 1); + } else if (e.key === "ArrowUp" && items.length) { + e.preventDefault(); + selIndex = Math.max(selIndex - 1, 0); + } else if (e.key === "Enter") { + if (selIndex >= 0 && items[selIndex]) { location.href = items[selIndex].href; return; } + var first = items[0]; + if (first) location.href = first.href; + return; + } else if (e.key === "Escape") { + panel.hidden = true; + return; + } else { + return; + } + items.forEach(function (el, i) { el.classList.toggle("sel", i === selIndex); }); + }); + document.addEventListener("click", function (e) { + if (!e.target.closest(".search")) panel.hidden = true; + }); + } + + /* ---------- 目录滚动高亮 ---------- */ + var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".toc-nav a")); + if (tocLinks.length && "IntersectionObserver" in window) { + var byId = {}; + tocLinks.forEach(function (a) { byId[a.getAttribute("href").slice(1)] = a; }); + var io = new IntersectionObserver( + function (entries) { + entries.forEach(function (en) { + if (en.isIntersecting) { + tocLinks.forEach(function (a) { a.classList.remove("active"); }); + var a = byId[en.target.id]; + if (a) a.classList.add("active"); + } + }); + }, + { rootMargin: "-72px 0px -70% 0px", threshold: 0 } + ); + document.querySelectorAll("article.md h2[id], article.md h3[id]").forEach(function (h) { io.observe(h); }); + } + + /* ---------- 反馈组件 ---------- */ + var fb = document.getElementById("feedback"); + if (fb) { + var pageKey = "opsx-docs-fb:" + location.pathname; + var voted = null; + try { voted = localStorage.getItem(pageKey); } catch (e) {} + var btns = fb.querySelectorAll(".fb-btn"); + var thanks = document.getElementById("fb-thanks"); + function mark(v) { + btns.forEach(function (b) { + if (b.dataset.v === v) b.classList.add("done"); + }); + thanks.hidden = false; + try { localStorage.setItem(pageKey, v); } catch (e) {} + } + if (voted) mark(voted); + btns.forEach(function (b) { + b.addEventListener("click", function () { mark(b.dataset.v); }); + }); + } + + /* ---------- 打印 ---------- */ + var printBtn = document.getElementById("print-btn"); + if (printBtn) printBtn.addEventListener("click", function () { window.print(); }); + + /* ---------- 移动端侧栏 ---------- */ + var menuBtn = document.getElementById("menu-btn"); + var sidebar = document.getElementById("sidebar"); + if (menuBtn && sidebar) { + menuBtn.addEventListener("click", function () { sidebar.classList.toggle("open"); }); + document.addEventListener("click", function (e) { + if (sidebar.classList.contains("open") && !e.target.closest(".sidebar") && !e.target.closest(".menu-btn")) { + sidebar.classList.remove("open"); + } + }); + } +})(); diff --git a/apps/server/internal/docs/content/01-intro.md b/apps/server/internal/docs/content/01-intro.md new file mode 100644 index 0000000..890eb40 --- /dev/null +++ b/apps/server/internal/docs/content/01-intro.md @@ -0,0 +1,36 @@ +OpsxAPI 是一款**本地优先**的专业接口工作台,面向运维与开发,把 HTTP、GraphQL、WebSocket、SSE、Socket.IO、MQTT 六种协议的调试放进同一个窗口。 + +无需注册、无需联网同步,数据全部保存在本地浏览器(localStorage),导出备份随时带走。 + +## 功能总览 + +| 能力 | 说明 | +| --- | --- | +| 六种协议 | HTTP / GraphQL / WebSocket / SSE / Socket.IO / MQTT | +| 集合管理 | 目录树、搜索、右键菜单、拖拽移动、多标签页 | +| 多环境变量 | `{{baseUrl}}`、`{{token}}` 全局替换,一键切换环境 | +| 数据导入 | OpenAPI/Swagger、Postman、Apifox、Apipost、cURL、HAR、Insomnia、Hoppscotch、Markdown 等 | +| 数据导出 | OpenAPI 3.0、Postman v2.1、Markdown 文档、全量备份 | +| 代码生成 | cURL / JavaScript fetch / Python requests / Go net/http | +| 代理能力 | 本地 Gin 代理绕开 CORS;WebSocket 隧道支持自定义握手 Header | +| 双端形态 | 浏览器(Web)与桌面端(Tauri)共用同一套界面 | + +## 支持的协议与场景 + +| 协议 | 典型场景 | 特色 | +| --- | --- | --- | +| HTTP / REST | 调试 REST 接口、运维平台 API | 7 种方法、6 种请求体、3 种鉴权 | +| GraphQL | 查询网关、中台接口 | Query + Variables 独立编辑 | +| WebSocket | 实时推送、行情、IM | 自定义握手 Header(经本地隧道) | +| SSE | AI 流式输出、监控事件流 | 直连失败自动切代理、断点续传 | +| Socket.IO | Node 实时应用 | 事件监听、emit 参数、path/auth 配置 | +| MQTT | IoT 设备、消息总线 | 通配符订阅、QoS、自动重连 | + +## 为什么选 OpsxAPI + +- **数据主权**:所有集合、环境、历史保存在本机,不经过任何第三方服务器。 +- **开箱即用**:`./run.sh web` 一条命令启动;首次打开自带覆盖全部协议的示例集合。 +- **工程友好**:导入自动建目录、路径参数自动转变量;导出即标准 OpenAPI,可回流 Apifox / Swagger 生态。 +- **轻量克制**:一个 Go 二进制 + 一份前端,资源占用小,内网可部署。 + +> **提示**:想快速感受?直接看下一章《快速开始》,30 秒发出第一个请求。 diff --git a/apps/server/internal/docs/content/02-quickstart.md b/apps/server/internal/docs/content/02-quickstart.md new file mode 100644 index 0000000..f9d36a3 --- /dev/null +++ b/apps/server/internal/docs/content/02-quickstart.md @@ -0,0 +1,48 @@ +## 环境要求 + +| 组件 | 版本 | 用途 | +| --- | --- | --- | +| Node.js | ≥ 20 | Web 前端(Vite) | +| Go | ≥ 1.23 | 本地代理服务(Gin) | +| Rust + Cargo | 可选 | 仅编译桌面端(Tauri)时需要 | + +## 启动 + +```bash +git clone <你的仓库地址> opsxapi +cd opsxapi +make install # 安装依赖(npm install,含桌面端) + +./run.sh web # 启动:Gin(:8787) + Web UI(:5173) +``` + +看到下面的输出即可打开浏览器: + +```bash +[OK] 后端 UP http://127.0.0.1:8787/health +[OK] Web UP http://127.0.0.1:5173 +``` + +| 命令 | 说明 | +| --- | --- | +| `./run.sh web` | 浏览器版(推荐) | +| `./run.sh gui` | 桌面窗口(Tauri,共用同一 UI) | +| `./run.sh all` | Web + 桌面一起开 | +| `./run.sh status` / `stop` | 查看状态 / 停止 | + +## 30 秒发出第一个请求 + +1. 打开 ,左侧是内置的「示例集合」。 +2. 点击 **HTTP · httpbin → 获取请求回显**。 +3. 点击右上角 **发送**。 +4. 下方响应区显示 `200`、耗时与 JSON 高亮响应体 —— 完成! + +## 桌面端(可选) + +```bash +./run.sh gui +``` + +桌面端复用同一份界面与数据通道,HTTP 请求走 Tauri 原生网络栈,不依赖浏览器 CORS。 + +> **提示**:首次打开会自动创建两个环境(正式 / 本地)和一个覆盖全部协议的示例集合,可随时在「设置」中重置。 diff --git a/apps/server/internal/docs/content/03-interface.md b/apps/server/internal/docs/content/03-interface.md new file mode 100644 index 0000000..bbf9e94 --- /dev/null +++ b/apps/server/internal/docs/content/03-interface.md @@ -0,0 +1,41 @@ +OpsxAPI 主界面分为五个区域: + +## 顶栏 + +| 元素 | 作用 | +| --- | --- | +| + 新建 | 快捷创建各协议请求 / 目录(默认建在第一个集合) | +| 导入 / 导出 | 打开数据导入、导出对话框 | +| 环境选择器 | 切换当前环境(变量随之生效),「⚙ 环境」管理环境与变量 | +| 🌙 / ☀️ | 明暗主题切换 | +| ⋯ | 设置(主题、超时、重置工作区) | + +## 左侧集合树 + +- 点击请求 → 在标签页中打开;点击目录 → 展开 / 折叠。 +- **右键**任意节点:新建、重命名、复制、导出、删除。 +- **拖拽**请求或目录到另一个目录,即可移动(支持跨集合)。 +- 顶部搜索框按名称 / 方法 / URL 实时过滤。 +- 底部为**历史记录**:显示最近请求的状态码与耗时,点击可回跳到对应接口。 + +## 标签页栏 + +同时打开多个接口,中键或 × 关闭;右键支持「关闭其他 / 关闭全部」。正在连接中的实时协议标签会显示呼吸圆点。 + +## 请求编辑区(上半部分) + +按协议呈现不同编辑器: + +| 协议 | 标签页 | +| --- | --- | +| HTTP | Params · Body · Headers · Cookies · Auth · 说明 | +| GraphQL | Query · Variables · Headers · Auth | +| WebSocket | 消息 · 握手配置 · 说明 | +| SSE | 事件流 · 连接配置 · 说明 | +| Socket.IO | 事件 · 连接配置 · 说明 | +| MQTT | 消息 · Broker / 订阅 · 发布 · 说明 | + +## 响应区(下半部分) + +- **HTTP / GraphQL**:响应体(Pretty JSON 高亮)、Raw、响应头,右上角状态 / 耗时 / 大小 / 复制。 +- **实时协议**:分色消息日志(接收 · 绿、发送 · 蓝、系统 · 灰、错误 · 红),顶部实时统计收发条数与连接时长。 diff --git a/apps/server/internal/docs/content/04-http.md b/apps/server/internal/docs/content/04-http.md new file mode 100644 index 0000000..dfafae7 --- /dev/null +++ b/apps/server/internal/docs/content/04-http.md @@ -0,0 +1,47 @@ +## 发送请求 + +1. 选择方法(GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS)。 +2. 输入 URL,支持 `{{baseUrl}}/path` 形式的变量(见《环境与变量》)。 +3. 点击 **发送**,或直接在 URL 输入框按 **Enter**。 + +请求默认经本地 Gin 代理(`/v1/proxy/http`)发出,天然绕开浏览器 CORS 限制。 + +## Params(查询参数) + +键值对编辑器,每行可勾选启用 / 停用、填写说明。URL 中已有的查询串会被拆分到表格里,两边双向同步。 + +## Body(请求体) + +| 模式 | 适用场景 | +| --- | --- | +| none | GET 等无体请求 | +| JSON | REST 接口(默认自动补 `Content-Type: application/json`,带格式化按钮) | +| XML / Text / HTML | 原始报文 | +| x-www-form-urlencoded | 表单提交 | +| form-data | multipart 文本字段(文件上传请用代码生成的 cURL) | + +## Headers / Cookies + +- Headers:键值对,支持 `{{变量}}`;`Authorization`、`Cookie` 由 Auth / Cookies 标签统一管理时避免重复。 +- Cookies:键值对,发送时自动拼接为 `Cookie` 请求头。 + +## 代码生成 + +点击 URL 栏右侧 **`` 代码**,可生成: + +- cURL(可直接在终端执行) +- JavaScript fetch +- Python requests +- Go net/http + +生成内容保留 `{{变量}}` 原样,便于粘贴到脚本后再替换。 + +## 响应面板 + +| 标签 | 内容 | +| --- | --- | +| 响应体 | JSON 自动格式化 + 语法高亮;其他类型原样展示 | +| Raw | 未格式化的原始文本 | +| 响应头 | 全部响应头表格 | + +右上角常驻 **状态码 / 耗时 / 大小 / Content-Type**,并提供「复制体」按钮。 diff --git a/apps/server/internal/docs/content/05-auth.md b/apps/server/internal/docs/content/05-auth.md new file mode 100644 index 0000000..5cdb509 --- /dev/null +++ b/apps/server/internal/docs/content/05-auth.md @@ -0,0 +1,29 @@ +在 HTTP / GraphQL / SSE 请求的 **Auth** 标签中选择鉴权方式: + +## Bearer Token + +```text +Authorization: Bearer +``` + +Token 支持 `{{token}}` 变量引用 —— 推荐把 token 放进环境变量,一次配置全局生效。 + +## Basic Auth + +填写用户名 / 密码,自动进行 Base64 编码后放入 `Authorization` 头。 + +## API Key + +| 字段 | 说明 | +| --- | --- | +| Key 名 | 如 `X-API-Key` | +| 值 | 支持 `{{变量}}` | +| 位置 | Header(默认)或 Query 参数 | + +## 组合规则 + +- 鉴权产生的 Header 会与 Headers 标签中的同名项合并(鉴权优先)。 +- API Key 放 Query 时,会追加到 Params,不会重复出现在 Headers。 +- 导入 OpenAPI / Postman 时,文档中声明的 `securitySchemes` 会自动映射到对应鉴权方式。 + +> **注意**:鉴权信息保存在本地集合数据中。如需分享集合,请先通过「导出」审查是否包含敏感 token。 diff --git a/apps/server/internal/docs/content/06-env.md b/apps/server/internal/docs/content/06-env.md new file mode 100644 index 0000000..235175a --- /dev/null +++ b/apps/server/internal/docs/content/06-env.md @@ -0,0 +1,50 @@ +## 变量语法 + +在 URL、Params、Headers、Cookies、Body、鉴权字段中,使用双花括号引用变量: + +```text +{{baseUrl}}/v1/users?page={{page}} +Authorization: Bearer {{token}} +``` + +发送时统一替换为**当前启用环境**中同名变量的值;未定义的变量保留原样,便于发现遗漏。 + +## 切换环境 + +顶栏环境切换器显示**彩色单字徽标 + 环境名 + 变量数**,点击弹出环境列表(当前环境带「当前」标记),一眼防误发;底部可进入「管理环境」或「不使用环境」。 + +## 管理环境 + +顶栏 **环境** 按钮打开管理弹窗(左侧环境列表 + 右侧编辑区): + +- **左栏**:环境列表(徽标 + 名称 + 当前标记),底部虚线按钮新建环境。 +- **右栏**:提示条(变量引用语法 + 文档直达)、环境重命名、设为当前 / 取消使用、**复制环境**、删除;变量表支持**按名称 / 描述搜索**、逐行启用停用、内联编辑。 +- 所有修改即时生效并自动保存。 + +## 输入 {{ 自动补全 + +在 URL 输入框或任意「值」输入框键入 `{{`,会弹出当前环境的变量浮层: + +- ↑↓ 选择、Enter / Tab 插入完整的 `{{变量名}}`;Esc 关闭。 +- 浮层同时显示变量值,选之前即可确认。 + +内置示例环境: + +| 环境 | baseUrl | token | +| --- | --- | --- | +| 正式 | `https://httpbin.org` | demo-token | +| 本地 | `http://127.0.0.1:8787` | dev-token | + +## 变量从哪来自动生成 + +| 来源 | 行为 | +| --- | --- | +| 导入 OpenAPI / Swagger | `servers` 地址写入 `baseUrl`;路径参数 `{id}` 转为 `{{id}}` 并写入示例值 | +| 导入 Postman | `collection.variable` 写入当前环境 | +| 手动输入 | 在环境面板随时增改 | + +## 实际地址预览 + +URL 含 `{{ }}` 时,编辑器会在 URL 栏下方实时显示替换后的实际地址,切换环境立即更新。 + +> **提示**:路径参数用变量而不是写死,是接口可复用的关键 —— 复制接口后只改变量值即可调试另一个资源。 diff --git a/apps/server/internal/docs/content/07-graphql.md b/apps/server/internal/docs/content/07-graphql.md new file mode 100644 index 0000000..bce7335 --- /dev/null +++ b/apps/server/internal/docs/content/07-graphql.md @@ -0,0 +1,32 @@ +## 编辑查询 + +GraphQL 请求由三部分组成: + +| 部分 | 说明 | +| --- | --- | +| Query | GraphQL 查询语句,支持变量定义 `$code: ID!` | +| Variables | JSON 对象,随查询一起发送;非法 JSON 会红框提示 | +| Headers / Auth | 与 HTTP 请求一致 | + +## 发送 + +点击 **执行**(或 URL 栏按 Enter)。实际发送的是标准 GraphQL over HTTP: + +```json +{ + "query": "query ($code: ID!) { country(code: $code) { name capital currency } }", + "variables": { "code": "CN" } +} +``` + +响应面板会自动格式化 `data` / `errors`,字段名高亮。 + +## 内置示例 + +示例集合中的「国家查询 · countries」连接公共 GraphQL 服务 ,无需鉴权,可直接体验。 + +## 最佳实践 + +- 把 endpoint 存为 `{{baseUrl}}/graphql`,环境切换即切换网关。 +- 需要鉴权时在 Auth 标签配置 Bearer,不必手写 Header。 +- 想看实际报文?点 `` 代码 生成 cURL,即为此查询的完整 HTTP 请求。 diff --git a/apps/server/internal/docs/content/08-websocket.md b/apps/server/internal/docs/content/08-websocket.md new file mode 100644 index 0000000..f2ff524 --- /dev/null +++ b/apps/server/internal/docs/content/08-websocket.md @@ -0,0 +1,34 @@ +## 连接 + +1. 填写 `ws://` 或 `wss://` 地址。 +2. 点击 **连接**;状态点变绿(已连接)后即可收发消息。 +3. 再次点击按钮即断开。 + +## 消息收发 + +- **消息日志**:接收(绿)/ 发送(蓝)/ 系统(灰)/ 错误(红)分色展示,顶部统计 ↑↓ 条数与连接时长。 +- **发送框**:支持 JSON / 纯文本;**⌘/Ctrl + Enter** 快速发送;「格式化 JSON」一键美化。 + +## 子协议 + +「握手配置」标签中的**子协议**填逗号分隔列表,例如: + +```text +graphql-transport-ws, v1.json +``` + +## 自定义握手 Header(隧道模式) + +浏览器原生 WebSocket **不能**携带自定义 Header(如 `Authorization`、`X-API-Key`)。OpsxAPI 的处理方式: + +1. 「握手配置」中填写 Header 后,工具自动改走**本地 Gin 隧道**: + - 浏览器 → Gin `/v1/proxy/ws/attach`(普通 WS) + - Gin → 目标服务器(携带你填的全部 Header) +2. 不填 Header 时直连目标,零额外开销。 + +> **注意**:隧道要求本地 Gin 服务(:8787)在运行,即用 `./run.sh web` / `gui` 启动的默认形态。 + +## 调试技巧 + +- 消息过多时点「清空」;日志上限 500 条,超出自动滚动淘汰。 +- 把常用报文保存在「说明」里,或为不同报文建多个 WS 请求节点。 diff --git a/apps/server/internal/docs/content/09-sse.md b/apps/server/internal/docs/content/09-sse.md new file mode 100644 index 0000000..050c0d6 --- /dev/null +++ b/apps/server/internal/docs/content/09-sse.md @@ -0,0 +1,29 @@ +## 连接事件流 + +1. 填写 SSE 地址(如 `https://stream.wikimedia.org/v2/stream/recentchange`)。 +2. 点击 **连接**,事件按到达顺序流入消息日志。 +3. 每条消息按 `event:` 名称归类显示;`data` 为 JSON 时自动格式化。 + +## 直连与代理自动切换 + +- 默认**直连**目标站(fetch 流式读取,延迟最低)。 +- 直连因 CORS / 网络失败时,自动降级走**本地 Gin 流式代理**,日志中会提示切换过程,无需手工干预。 + +## 断点续传(Last-Event-ID) + +「连接配置」中填写 Last-Event-ID,连接时以 `Last-Event-ID` 请求头携带,支持断点续传的流会在该事件之后继续推送。 + +## 连接配置项 + +| 配置 | 说明 | +| --- | --- | +| Query 参数 / Headers | 以键值对附加 | +| Last-Event-ID | 断点续传游标 | +| 仅显示 data 内容 | 开启后只显示 `data:` 载荷;关闭则连同 `event:` 名称一起显示 | +| 鉴权 | 与 HTTP 相同(Bearer / Basic / API Key) | + +## 典型场景 + +- LLM 流式输出接口(`text/event-stream`) +- 监控 / 审计事件流 +- 需要长期挂机观察的推送通道 diff --git a/apps/server/internal/docs/content/10-socketio.md b/apps/server/internal/docs/content/10-socketio.md new file mode 100644 index 0000000..cb9e0b6 --- /dev/null +++ b/apps/server/internal/docs/content/10-socketio.md @@ -0,0 +1,45 @@ +## 连接 + +1. 填写服务地址(**不含** `/socket.io` 路径),如 `http://127.0.0.1:3000`。 +2. 点击 **连接**,状态变为「已连接」后即可收发事件。 + +## 监听事件 + +「连接配置」→ **监听事件** 填逗号分隔的事件名: + +```text +message, notification +``` + +留空则接收全部服务端事件(onAny 模式)。事件名在每条日志中标注。 + +## 发送(emit) + +「事件」标签底部的发送区: + +- **事件名**:如 `message` +- **参数**:JSON 数组,作为 `socket.emit(event, ...args)` 的参数 + +```json +["hello", 42] +``` + +## 连接选项 + +以 JSON 配置传给 socket.io-client: + +```json +{ + "path": "/socket.io", + "transports": ["websocket"], + "auth": { "token": "{{token}}" } +} +``` + +| 选项 | 说明 | +| --- | --- | +| path | 服务端挂载路径,默认 `/socket.io` | +| transports | `websocket` / `polling`,默认两者皆可 | +| auth | 握手鉴权载荷,支持 `{{变量}}` | + +> **提示**:浏览器直连外部站点同样受 CORS 限制,Socket.IO 服务端需配置对应 origin,或把服务与本工具部署在同源下。 diff --git a/apps/server/internal/docs/content/11-mqtt.md b/apps/server/internal/docs/content/11-mqtt.md new file mode 100644 index 0000000..fdb1995 --- /dev/null +++ b/apps/server/internal/docs/content/11-mqtt.md @@ -0,0 +1,41 @@ +## 连接 Broker + +浏览器环境要求 **WebSocket 形式**的 Broker 地址: + +```text +wss://broker.emqx.io:8084/mqtt +``` + +| 配置 | 说明 | +| --- | --- | +| 用户名 / 密码 | 可选,支持 `{{变量}}` | +| ClientId | 默认随机生成;同一 Broker 上必须唯一 | +| QoS | 0 至多一次 · 1 至少一次 · 2 恰好一次 | + +## 订阅 + +「Broker / 订阅」中填写逗号分隔的主题,支持通配符: + +```text +opsxapi/demo, sensor/+, home/# +``` + +> **注意**:订阅在**建立连接时**生效,修改主题后需断开重连。 + +## 发布 + +「发布」标签配置主题与 Payload(JSON / 纯文本),点击发布按钮即送达;已发布消息以蓝色出现在日志中并标注主题。 + +## 公共测试 Broker + +| Broker | 地址 | +| --- | --- | +| EMQX | `wss://broker.emqx.io:8084/mqtt` | +| HiveMQ | `wss://broker.hivemq.com:8884/mqtt` | + +两个浏览器窗口连接同一主题,即可互相收发验证。 + +## 行为说明 + +- 断线自动重连(3 秒间隔),状态点显示「重连中」。 +- 消息日志上限 500 条;QoS 与主题逐条标注。 diff --git a/apps/server/internal/docs/content/12-import.md b/apps/server/internal/docs/content/12-import.md new file mode 100644 index 0000000..13d5384 --- /dev/null +++ b/apps/server/internal/docs/content/12-import.md @@ -0,0 +1,40 @@ +顶栏 **导入** 打开导入对话框,支持三种来源: + +| 来源 | 说明 | +| --- | --- | +| 粘贴文本 / cURL | 直接粘贴任意受支持格式的内容 | +| 文件导入 | 选择或拖入 `.json` / `.yaml` / `.har` / `.md` / `.txt` | +| URL 导入(Swagger) | 填写 OpenAPI 地址,经本地 Gin 代理拉取,避开 CORS | + +格式**自动识别**,也可手动指定。 + +## 支持格式 + +| 格式 | 细节 | +| --- | --- | +| OpenAPI 3.0 / 3.1、Swagger 2.0 | JSON 或 YAML;Apifox 导出的 OpenAPI 同样适用 | +| Postman Collection | v2.0 / v2.1,含鉴权、变量、GraphQL body | +| Apipost 项目 | `api_collection` 格式(宽容解析) | +| cURL | 引号、续行、`-d` / `-F` / `-u` / `--data-urlencode`、cookie、代理等参数 | +| HAR | 按域名分组、相同请求去重 | +| Insomnia | 导出文件 v3 / v4,含 workspace / group 层级 | +| Hoppscotch | 集合 JSON(数组或单对象) | +| Markdown | 提取文中的 ```curl 代码块 | +| OpsxAPI 备份 | 全量集合 + 环境,用于迁移恢复 | + +YApi、Apidoc、Apizza、Eolink 请先在原平台导出为 **OpenAPI 或 Postman** 格式,再导入本工具。 + +## 导入时自动完成 + +| 原始内容 | 转换结果 | +| --- | --- | +| OpenAPI `tags` | 目录分组 | +| 路径参数 `/pets/{petId}` | `{{petId}}` 变量 + 环境示例值 | +| `servers` 地址 | 当前环境 `baseUrl` 变量 | +| requestBody schema | JSON 示例请求体 | +| securitySchemes | Bearer / Basic / API Key 鉴权配置 | +| Postman `variable` | 当前环境变量 | + +## 结果确认 + +导入完成显示汇总:新建集合、接口数量、写入的环境变量与告警(如跳过的文件上传字段)。集合名重复时自动追加序号。 diff --git a/apps/server/internal/docs/content/13-export.md b/apps/server/internal/docs/content/13-export.md new file mode 100644 index 0000000..0bdcbc6 --- /dev/null +++ b/apps/server/internal/docs/content/13-export.md @@ -0,0 +1,30 @@ +顶栏 **导出**(多集合时可选择目标集合): + +| 格式 | 用途 | +| --- | --- | +| OpenAPI 3.0 · JSON / YAML | 回流 Apifox、Swagger UI、代码生成器 | +| Postman v2.1 | 直接导入 Postman | +| Markdown 接口文档 | 带目录、参数表格、请求体代码块,可放 Wiki | +| 全量备份 | 全部集合 + 环境,用于迁移与灾备 | + +每种格式均支持**复制到剪贴板**与**下载文件**;对话框内实时预览导出内容。 + +## 全量备份与恢复 + +- 备份文件是包含 `__opsxapi__: "backup"` 标记的 JSON。 +- 恢复:导入对话框 → 粘贴备份内容或选择备份文件 → 格式自动识别为「OpsxAPI 备份」。 + +## 代码生成(单请求) + +在 HTTP / GraphQL 编辑器点 **`` 代码**: + +| 目标 | 说明 | +| --- | --- | +| cURL | 含方法、头、体、cookie,可直接执行 | +| JavaScript fetch | `async/await` 风格 | +| Python requests | 含 `import requests` 完整脚本 | +| Go net/http | 完整 `main` 函数 | + +`{{变量}}` 保留原样输出,方便接入你自己的配置体系。 + +> **提示**:导出的 OpenAPI 中,目录名会成为 `tags`,请求说明写入 `description`,鉴权自动生成 `securitySchemes`。 diff --git a/apps/server/internal/docs/content/14-manage.md b/apps/server/internal/docs/content/14-manage.md new file mode 100644 index 0000000..2c7f71b --- /dev/null +++ b/apps/server/internal/docs/content/14-manage.md @@ -0,0 +1,39 @@ +## 新建 + +| 入口 | 行为 | +| --- | --- | +| 顶栏「+ 新建」 | 快捷菜单,建在第一个集合 | +| 树右键「新建请求 / 目录」 | 精确指定位置 | +| 新建对话框 | 选择类型(六协议 + 目录)、命名、目标集合与目录 | + +接口名自动避免重复(追加序号)。 + +## 集合树操作 + +| 操作 | 方式 | +| --- | --- | +| 重命名 | 右键 → 重命名 | +| 复制 | 右键 → 复制(深拷贝子树,名字加「副本」) | +| 移动 | 拖拽到目标目录(支持跨集合) | +| 删除 | 右键 → 删除(直接生效,请先备份重要数据) | +| 搜索 | 侧栏搜索框,匹配名称 / 方法 / URL | + +## 标签页 + +- 点击树节点在标签页打开;同时打开数量不限。 +- 中键 / × 关闭;右键「关闭其他 / 关闭全部」。 +- 关闭实时协议(WS / SSE / MQTT…)标签时自动断开连接。 + +## 历史记录 + +- 每次 HTTP / GraphQL 发送记录:时间、方法、接口名、状态码,失败标红。 +- 点击记录跳回对应接口;最多保留 100 条;可一键清空。 + +## 数据存储与备份 + +- 位置:浏览器 localStorage(键 `opsxapi.workspace.v2`),自动保存(防抖 400ms)。 +- 内容:集合、环境、历史、设置。标签页会话与实时消息不落盘。 +- 备份:导出 → 全量备份;恢复:导入备份文件。 +- 重置:设置 → 重置工作区(恢复初始示例,请先备份)。 + +> **注意**:桌面端与浏览器是两份独立的 localStorage。跨端迁移请使用全量备份 / 导入。 diff --git a/apps/server/internal/docs/content/15-server-api.md b/apps/server/internal/docs/content/15-server-api.md new file mode 100644 index 0000000..d0767c7 --- /dev/null +++ b/apps/server/internal/docs/content/15-server-api.md @@ -0,0 +1,75 @@ +本地服务(Gin)默认监听 `:8787`,可用环境变量 `OPSXAPI_ADDR` 修改。所有端点仅监听本机回环,供 Web UI 与桌面端调用。 + +## 端点总览 + +| 方法 | 路径 | 用途 | +| --- | --- | --- | +| GET | `/health` | 健康检查 | +| POST | `/v1/proxy/http` | HTTP 代理(全方法、自定义头、超时) | +| POST | `/v1/proxy/http/stream` | NDJSON 流式代理(SSE / chunked 透传) | +| POST | `/v1/proxy/ws/connect` | 建立到目标 WS 的隧道会话 | +| GET | `/v1/proxy/ws/attach` | 浏览器 attach 到隧道会话(双向桥接) | +| POST | `/v1/proxy/ws/close` | 关闭隧道会话 | +| GET | `/docs` | 本文档站 | + +## HTTP 代理 + +```bash +curl -s -X POST http://127.0.0.1:8787/v1/proxy/http \ + -H 'Content-Type: application/json' \ + -d '{"method":"GET","url":"https://httpbin.org/get","headers":{}}' +``` + +响应: + +```json +{ + "status": 200, + "headers": { "content-type": "application/json" }, + "body": "{ ... }", + "timeMs": 320, + "size": 274 +} +``` + +请求体 `body.mode` 支持 `json` / `xml` / `text` / `html` / `urlencoded` / `form-data`,`raw` 为原始报文;`timeoutMs` 可选。 + +## 流式代理 + +请求体与 HTTP 代理一致,响应为 NDJSON 事件流: + +```text +{"type":"meta","status":200,"headers":{...}} +{"type":"chunk","data":"","size":1024} +{"type":"end","timeMs":1200,"size":8192} +``` + +## WebSocket 隧道 + +```bash +# 1. 建立会话(可带自定义 Header) +curl -s -X POST http://127.0.0.1:8787/v1/proxy/ws/connect \ + -H 'Content-Type: application/json' \ + -d '{"url":"wss://echo.example.com","headers":{"Authorization":"Bearer t"}}' +# → {"sessionId":"..."} + +# 2. 浏览器 / 客户端 attach(普通 WebSocket) +# ws://127.0.0.1:8787/v1/proxy/ws/attach?sessionId=... + +# 3. 关闭 +curl -s -X POST http://127.0.0.1:8787/v1/proxy/ws/close \ + -H 'Content-Type: application/json' -d '{"sessionId":"..."}' +``` + +attach 后第一条消息为控制帧 `{"type":"open"}`;此后浏览器与目标服务器双向透传。 + +## 安全边界 + +- 仅允许 `http/https`(代理)与 `ws/wss`(隧道)目标。 +- 拒绝云元数据地址(169.254.169.254 等)防 SSRF。 +- 单响应上限 10 MiB;隧道会话 10 分钟无活动自动回收。 + +## 部署建议 + +- 开发:`./run.sh web`(Vite :5173 反代 `/v1`、`/docs` 到 :8787)。 +- 生产:`npm run build:web` 产物交给任意静态服务器,`opsxapi-server` 独立部署;生产环境建议置于内网并按需增加鉴权反向代理。 diff --git a/apps/server/internal/docs/content/16-faq.md b/apps/server/internal/docs/content/16-faq.md new file mode 100644 index 0000000..fd9125c --- /dev/null +++ b/apps/server/internal/docs/content/16-faq.md @@ -0,0 +1,45 @@ +## 请求报 CORS 错误? + +Web 端请求默认经本地 Gin 代理(gin-proxy)发出,不会遇到 CORS。若仍报错: + +- 确认 Gin 在运行:`curl http://127.0.0.1:8787/health`。 +- 用 `./run.sh web` / `gui` 场景启动(Vite 已配置 `/v1` 反代)。 +- 桌面端优先走 Tauri 原生网络栈,同样不受 CORS 限制。 + +## 浏览器 WebSocket 不能加自定义 Header? + +规范限制。在 WS「握手配置」里填写 Header 后,OpsxAPI 自动改走本地 Gin 隧道(见《WebSocket》章节),Header 由 Gin 在握手时代为携带。不填 Header 则直连。 + +## 外部 wss:// 地址连不上(DNS 失败)? + +本机网络环境可能限制部分域名解析。可先用 `curl -v wss://…` 或换用公共 echo 服务验证;自建服务请确认防火墙与证书。 + +## MQTT 一定要 wss:// 吗? + +浏览器只能通过 WebSocket 连 MQTT。常见 Broker 的 WS 端口:EMQX `8084/mqtt`(TLS)、`8083/mqtt`(明文,仅 http 页面)。`mqtt://` TCP 直连仅在桌面原生环境可行。 + +## 数据存在哪里?换电脑怎么办? + +集合 / 环境 / 历史保存在浏览器 localStorage。迁移:**导出 → 全量备份** 生成 JSON,在新设备 **导入** 即可。 + +## 误删了接口 / 想回到初始状态? + +设置(顶栏 ⋯)→ 重置工作区,恢复内置示例集合。日常请养成导出备份的习惯。 + +## SSE 事件流看着卡住了? + +- 事件频率低(如告警流)属正常,等待即可。 +- 部分网关会缓冲 SSE,工具会在直连失败时自动切换 Gin 流式代理;若仍无事件,检查中间层(Nginx 需关闭 `proxy_buffering`)。 + +## 发送按钮点击后没反应? + +查看响应区是否有红色错误提示(如代理不可达)。`` 代码 生成 cURL 在终端执行可快速分辨是工具问题还是网络 / 接口问题。 + +## 支持文件上传吗? + +form-data 当前支持文本字段;文件上传请用 `` 代码 生成的 cURL / 脚本执行。 + +## 更多问题? + +- 服务端能力与边界:见《服务端 API 参考》 +- 各协议用法:见左侧「请求调试」分组 diff --git a/apps/server/internal/docs/content/manifest.json b/apps/server/internal/docs/content/manifest.json new file mode 100644 index 0000000..b04288a --- /dev/null +++ b/apps/server/internal/docs/content/manifest.json @@ -0,0 +1,41 @@ +{ + "productVersion": "1.0", + "groups": [ + { + "name": "开始使用", + "pages": [ + { "file": "01-intro.md", "slug": "intro", "title": "产品介绍", "updated": "2026-09-17" }, + { "file": "02-quickstart.md", "slug": "quickstart", "title": "快速开始", "updated": "2026-09-17" }, + { "file": "03-interface.md", "slug": "interface", "title": "界面导览", "updated": "2026-09-17" } + ] + }, + { + "name": "请求调试", + "pages": [ + { "file": "04-http.md", "slug": "http", "title": "发送 HTTP 请求", "updated": "2026-09-17" }, + { "file": "05-auth.md", "slug": "auth", "title": "接口鉴权", "updated": "2026-09-17" }, + { "file": "06-env.md", "slug": "env", "title": "环境与变量", "updated": "2026-09-17" }, + { "file": "07-graphql.md", "slug": "graphql", "title": "GraphQL", "updated": "2026-09-17" }, + { "file": "08-websocket.md", "slug": "websocket", "title": "WebSocket", "updated": "2026-09-17" }, + { "file": "09-sse.md", "slug": "sse", "title": "SSE 事件流", "updated": "2026-09-17" }, + { "file": "10-socketio.md", "slug": "socketio", "title": "Socket.IO", "updated": "2026-09-17" }, + { "file": "11-mqtt.md", "slug": "mqtt", "title": "MQTT", "updated": "2026-09-17" } + ] + }, + { + "name": "数据管理", + "pages": [ + { "file": "12-import.md", "slug": "import", "title": "数据导入", "updated": "2026-09-17" }, + { "file": "13-export.md", "slug": "export", "title": "导出与代码生成", "updated": "2026-09-17" }, + { "file": "14-manage.md", "slug": "manage", "title": "集合与历史", "updated": "2026-09-17" } + ] + }, + { + "name": "参考", + "pages": [ + { "file": "15-server-api.md", "slug": "server-api", "title": "服务端 API 参考", "updated": "2026-09-17" }, + { "file": "16-faq.md", "slug": "faq", "title": "常见问题", "updated": "2026-09-17" } + ] + } + ] +} diff --git a/apps/server/internal/docs/docs.go b/apps/server/internal/docs/docs.go new file mode 100644 index 0000000..e1611d8 --- /dev/null +++ b/apps/server/internal/docs/docs.go @@ -0,0 +1,264 @@ +// Package docs 在 /docs 路径提供产品用户文档(Markdown 嵌入二进制,SSR 渲染)。 +package docs + +import ( + "embed" + "encoding/json" + "fmt" + "net/http" + "path" + "regexp" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + gmhtml "github.com/yuin/goldmark/renderer/html" +) + +//go:embed content assets template.html +var files embed.FS + +type PageDef struct { + File string `json:"file"` + Slug string `json:"slug"` + Title string `json:"title"` + Updated string `json:"updated,omitempty"` +} + +type GroupDef struct { + Name string `json:"name"` + Pages []PageDef `json:"pages"` +} + +type Manifest struct { + ProductVersion string `json:"productVersion"` + Groups []GroupDef `json:"groups"` +} + +// TocItem 本页目录项 +type TocItem struct { + ID string `json:"id"` + Level int `json:"level"` + Text string `json:"text"` +} + +type SearchEntry struct { + Slug string `json:"slug"` + Title string `json:"title"` + Group string `json:"group"` + Text string `json:"text"` +} + +type NavLink struct { + Slug string + Title string +} + +type Page struct { + Slug string + Title string + Group string + Updated string + HTML string + TOC []TocItem + Prev *NavLink + Next *NavLink +} + +// Handler 文档服务 +type Handler struct { + manifest Manifest + pages map[string]*Page + order []string + searchData []byte + tpl string +} + +func New() *Handler { + h := &Handler{pages: map[string]*Page{}} + raw, err := files.ReadFile("content/manifest.json") + if err != nil { + panic("docs: manifest.json 缺失: " + err.Error()) + } + if err := json.Unmarshal(raw, &h.manifest); err != nil { + panic("docs: manifest.json 解析失败: " + err.Error()) + } + tplRaw, err := files.ReadFile("template.html") + if err != nil { + panic("docs: template.html 缺失: " + err.Error()) + } + h.tpl = string(tplRaw) + + md := goldmark.New( + goldmark.WithExtensions(extension.Table, extension.Strikethrough, extension.TaskList), + goldmark.WithRendererOptions(gmhtml.WithHardWraps()), + ) + + search := []SearchEntry{} + var prev *NavLink + for _, group := range h.manifest.Groups { + for _, def := range group.Pages { + src, err := files.ReadFile("content/" + def.File) + if err != nil { + panic("docs: 缺少内容文件 " + def.File) + } + var buf strings.Builder + if err := md.Convert(src, &buf); err != nil { + panic("docs: 渲染失败 " + def.File + ": " + err.Error()) + } + body, toc := injectHeadingIDs(buf.String()) + page := &Page{ + Slug: def.Slug, + Title: def.Title, + Group: group.Name, + Updated: def.Updated, + HTML: body, + TOC: toc, + Prev: prev, + } + h.pages[def.Slug] = page + h.order = append(h.order, def.Slug) + prev = &NavLink{Slug: def.Slug, Title: def.Title} + search = append(search, SearchEntry{ + Slug: def.Slug, + Title: def.Title, + Group: group.Name, + Text: plainText(string(src)), + }) + } + } + // 下一页链接 + for i, slug := range h.order { + if i+1 < len(h.order) { + next := h.pages[h.order[i+1]] + h.pages[slug].Next = &NavLink{Slug: next.Slug, Title: next.Title} + } + } + searchJSON, _ := json.Marshal(search) + h.searchData = searchJSON + return h +} + +// Register 注册路由(单一 catch-all,避免静态/参数段冲突) +func (h *Handler) Register(r *gin.Engine) { + r.GET("/docs", func(c *gin.Context) { + c.Redirect(http.StatusFound, "/docs/"+h.order[0]) + }) + r.GET("/docs/*path", h.dispatch) +} + +func (h *Handler) dispatch(c *gin.Context) { + p := strings.Trim(c.Param("path"), "/") + switch { + case p == "": + c.Redirect(http.StatusFound, "/docs/"+h.order[0]) + case p == "search-data.json": + h.SearchData(c) + case strings.HasPrefix(p, "assets/"): + c.Params = []gin.Param{{Key: "file", Value: "/" + strings.TrimPrefix(p, "assets")}} + h.Asset(c) + default: + // 取第一段作为 slug(忽略子路径) + slug := p + if i := strings.IndexByte(p, '/'); i >= 0 { + slug = p[:i] + } + c.Params = []gin.Param{{Key: "slug", Value: slug}} + h.Page(c) + } +} + +// Page 渲染文档页 +func (h *Handler) Page(c *gin.Context) { + slug := c.Param("slug") + page, ok := h.pages[slug] + if !ok { + c.Status(http.StatusNotFound) + _, _ = c.Writer.WriteString(notFoundHTML(h.order[0])) //nolint:errcheck + return + } + c.Header("Content-Type", "text/html; charset=utf-8") + c.Header("Cache-Control", "no-cache") + _, _ = c.Writer.WriteString(renderPage(h, page)) +} + +// SearchData 搜索索引 +func (h *Handler) SearchData(c *gin.Context) { + c.Data(http.StatusOK, "application/json; charset=utf-8", h.searchData) +} + +// Asset 静态资源 +func (h *Handler) Asset(c *gin.Context) { + name := path.Clean(c.Param("file")) + if strings.Contains(name, "..") { + c.Status(http.StatusBadRequest) + return + } + data, err := files.ReadFile("assets" + name) + if err != nil { + c.Status(http.StatusNotFound) + return + } + switch { + case strings.HasSuffix(name, ".css"): + c.Header("Content-Type", "text/css; charset=utf-8") + case strings.HasSuffix(name, ".js"): + c.Header("Content-Type", "application/javascript; charset=utf-8") + case strings.HasSuffix(name, ".svg"): + c.Header("Content-Type", "image/svg+xml") + default: + c.Header("Content-Type", "application/octet-stream") + } + c.Header("Cache-Control", "public, max-age=3600") + c.Data(http.StatusOK, "", data) +} + +// ManifestExport 供测试使用 +func (h *Handler) ManifestExport() Manifest { return h.manifest } + +func (h *Handler) FirstSlug() string { return h.order[0] } + +var ( + hRe = regexp.MustCompile(`(?s)<(h[23])>(.*?)`) + tagRe = regexp.MustCompile(`<[^>]+>`) +) + +// injectHeadingIDs 给 h2/h3 加锚点 id,并产出目录 +func injectHeadingIDs(html string) (string, []TocItem) { + var toc []TocItem + counter := 0 + out := hRe.ReplaceAllStringFunc(html, func(m string) string { + sub := hRe.FindStringSubmatch(m) + tag, inner := sub[1], sub[2] + counter++ + id := fmt.Sprintf("sec-%d", counter) + text := strings.TrimSpace(tagRe.ReplaceAllString(inner, "")) + level := 2 + if tag == "h3" { + level = 3 + } + toc = append(toc, TocItem{ID: id, Level: level, Text: text}) + return fmt.Sprintf(`<%s id="%s">%s`, tag, id, inner, tag) + }) + return out, toc +} + +var mdNoise = regexp.MustCompile("(?s)```.*?```|`[^`]*`|!\\[[^\\]]*\\]\\([^)]*\\)|\\[[^\\]]*\\]\\([^)]*\\)|[#>*_\\-|]+") + +// plainText 供搜索索引的纯文本 +func plainText(src string) string { + s := mdNoise.ReplaceAllString(src, " ") + s = strings.Join(strings.Fields(s), " ") + if len(s) > 6000 { + s = s[:6000] + } + return s +} + +func notFoundHTML(home string) string { + return fmt.Sprintf(`404 · OpsxAPI 文档 + +

    404

    文档页面不存在或已被移动。

    返回文档首页
    +`, home) +} diff --git a/apps/server/internal/docs/docs_test.go b/apps/server/internal/docs/docs_test.go new file mode 100644 index 0000000..1bd41ac --- /dev/null +++ b/apps/server/internal/docs/docs_test.go @@ -0,0 +1,122 @@ +package docs + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" +) + +func newTestRouter(t *testing.T) *gin.Engine { + t.Helper() + gin.SetMode(gin.TestMode) + r := gin.New() + New().Register(r) + return r +} + +func TestDocsIndexRedirect(t *testing.T) { + r := newTestRouter(t) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs", nil)) + if w.Code != http.StatusFound { + t.Fatalf("expect 302, got %d", w.Code) + } + if !strings.HasPrefix(w.Header().Get("Location"), "/docs/") { + t.Fatalf("bad redirect: %s", w.Header().Get("Location")) + } +} + +func TestDocsAllPagesRender(t *testing.T) { + h := New() + r := newTestRouter(t) + for _, slug := range h.order { + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/docs/"+slug, nil) + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("page %s: expect 200, got %d", slug, w.Code) + } + body := w.Body.String() + if !strings.Contains(body, "") || !strings.Contains(body, `class="md"`) { + t.Fatalf("page %s: html structure incomplete", slug) + } + // 正文有渲染出的标题与段落 + if !strings.Contains(body, "<h1") { + t.Fatalf("page %s: missing h1", slug) + } + // 侧栏包含全部分组 + for _, g := range h.manifest.Groups { + if !strings.Contains(body, g.Name) { + t.Fatalf("page %s: sidebar missing group %s", slug, g.Name) + } + } + } +} + +func TestDocsPageContainsTOCAndNav(t *testing.T) { + r := newTestRouter(t) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/http", nil)) + body := w.Body.String() + if !strings.Contains(body, "sec-1") { + t.Fatal("expect heading anchors") + } + if !strings.Contains(body, "上一页") { + t.Fatal("expect prev link") + } +} + +func TestDocsSearchData(t *testing.T) { + r := newTestRouter(t) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/search-data.json", nil)) + if w.Code != http.StatusOK { + t.Fatalf("expect 200, got %d", w.Code) + } + var entries []SearchEntry + if err := json.Unmarshal(w.Body.Bytes(), &entries); err != nil { + t.Fatalf("invalid json: %v", err) + } + if len(entries) < 10 { + t.Fatalf("expect >= 10 search entries, got %d", len(entries)) + } + // MQTT 页应可被检索(标题或正文命中) + found := false + for _, e := range entries { + if e.Slug == "mqtt" && (e.Title == "MQTT" || strings.Contains(e.Text, "MQTT")) { + found = true + } + } + if !found { + t.Fatal("mqtt entry missing searchable text") + } +} + +func TestDocsAssetsAnd404(t *testing.T) { + r := newTestRouter(t) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/assets/docs.css", nil)) + if w.Code != http.StatusOK || !strings.Contains(w.Header().Get("Content-Type"), "text/css") { + t.Fatalf("css asset: %d %s", w.Code, w.Header().Get("Content-Type")) + } + + w = httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/assets/docs.js", nil)) + if w.Code != http.StatusOK { + t.Fatalf("js asset: %d", w.Code) + } + + w = httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/docs/not-exist-page", nil)) + if w.Code != http.StatusNotFound { + t.Fatalf("404 page: %d", w.Code) + } + if !strings.Contains(w.Body.String(), "返回文档首页") { + t.Fatal("404 page missing home link") + } +} diff --git a/apps/server/internal/docs/render.go b/apps/server/internal/docs/render.go new file mode 100644 index 0000000..9f464ab --- /dev/null +++ b/apps/server/internal/docs/render.go @@ -0,0 +1,25 @@ +package docs + +import ( + "html/template" + "strings" +) + +func renderPage(h *Handler, p *Page) string { + tpl, err := template.New("doc").Parse(h.tpl) + if err != nil { + return "<!doctype html><meta charset=utf-8><title>errortemplate error: " + template.HTMLEscapeString(err.Error()) + } + + var sb strings.Builder + data := map[string]any{ + "Manifest": h.manifest, + "Page": p, + "TOC": p.TOC, + "Content": template.HTML(p.HTML), //nolint:gosec —— 内容为内置受信 Markdown + } + if err := tpl.Execute(&sb, data); err != nil { + return "errorrender error: " + template.HTMLEscapeString(err.Error()) + } + return sb.String() +} diff --git a/apps/server/internal/docs/template.html b/apps/server/internal/docs/template.html new file mode 100644 index 0000000..b17595d --- /dev/null +++ b/apps/server/internal/docs/template.html @@ -0,0 +1,110 @@ + + + + + +{{.Page.Title}} · OpsxAPI 文档 + + + + + + + +
    +
    + + + + OpsxAPI 文档 + + v{{.Manifest.ProductVersion}} + + +
    +
    + +
    + + +
    + +

    {{.Page.Title}}

    + {{if .Page.Updated}}
    更新于 {{.Page.Updated}}
    {{end}} +
    +{{.Content}} +
    + +
    + {{with .Page.Prev}} + + {{else}}{{end}} + {{with .Page.Next}} + + {{else}}{{end}} +
    + + +
    + + +
    + + + + diff --git a/apps/server/internal/proxy/guard.go b/apps/server/internal/proxy/guard.go new file mode 100644 index 0000000..fc2184e --- /dev/null +++ b/apps/server/internal/proxy/guard.go @@ -0,0 +1,36 @@ +package proxy + +import ( + "fmt" + "net" + "net/url" + "strings" +) + +var blockedHosts = map[string]struct{}{ + "169.254.169.254": {}, + "metadata.google.internal": {}, +} + +// ValidateURL applies MVP SSRF basics. Week 2 should add configurable CIDR +// deny-lists and DNS-rebinding protection before exposing this beyond localhost. +func ValidateURL(raw string) (*url.URL, error) { + u, err := url.Parse(raw) + if err != nil { + return nil, fmt.Errorf("invalid url: %w", err) + } + if u.Scheme != "http" && u.Scheme != "https" { + return nil, fmt.Errorf("only http/https schemes are allowed") + } + if u.Hostname() == "" { + return nil, fmt.Errorf("url host is required") + } + host := strings.ToLower(strings.TrimSuffix(u.Hostname(), ".")) + if _, blocked := blockedHosts[host]; blocked { + return nil, fmt.Errorf("blocked metadata host") + } + if ip := net.ParseIP(host); ip != nil && ip.Equal(net.ParseIP("169.254.169.254")) { + return nil, fmt.Errorf("blocked metadata address") + } + return u, nil +} diff --git a/apps/server/internal/proxy/guard_test.go b/apps/server/internal/proxy/guard_test.go new file mode 100644 index 0000000..2750500 --- /dev/null +++ b/apps/server/internal/proxy/guard_test.go @@ -0,0 +1,23 @@ +package proxy + +import "testing" + +func TestValidateURL(t *testing.T) { + for _, tc := range []struct { + raw string + ok bool + }{ + {"https://example.com/a", true}, + {"file:///etc/passwd", false}, + {"http://169.254.169.254/latest/meta-data", false}, + {"http://metadata.google.internal/computeMetadata/v1", false}, + } { + _, err := ValidateURL(tc.raw) + if tc.ok && err != nil { + t.Fatalf("%s: %v", tc.raw, err) + } + if !tc.ok && err == nil { + t.Fatalf("expected rejection: %s", tc.raw) + } + } +} diff --git a/apps/server/internal/proxy/handler.go b/apps/server/internal/proxy/handler.go new file mode 100644 index 0000000..c364244 --- /dev/null +++ b/apps/server/internal/proxy/handler.go @@ -0,0 +1,116 @@ +package proxy + +import ( + "bytes" + "io" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +const maxResponseBytes = 10 << 20 // 10 MiB MVP safety cap + +type Handler struct { + BaseClient *http.Client +} + +func NewHandler() *Handler { + return &Handler{BaseClient: &http.Client{Timeout: 30 * time.Second}} +} + +func normalizeMethod(raw string) string { + m := strings.ToUpper(strings.TrimSpace(raw)) + switch m { + case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, + http.MethodDelete, http.MethodHead, http.MethodOptions: + return m + default: + return http.MethodGet + } +} + +func (h *Handler) ProxyHTTP(c *gin.Context) { + var spec HttpSpec + if err := c.ShouldBindJSON(&spec); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid HttpSpec", "detail": err.Error()}) + return + } + + method := normalizeMethod(spec.Method) + + u, err := ValidateURL(spec.URL) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + query := u.Query() + for key, value := range spec.Query { + query.Set(key, value) + } + u.RawQuery = query.Encode() + + var body io.Reader + if spec.Body != nil && spec.Body.Mode != "none" && method != http.MethodGet && method != http.MethodHead { + body = bytes.NewBufferString(spec.Body.Raw) + } + + req, err := http.NewRequestWithContext(c.Request.Context(), method, u.String(), body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "cannot create request", "detail": err.Error()}) + return + } + for key, value := range spec.Headers { + // Hop-by-hop headers are managed by net/http. + if !isHopByHop(key) { + req.Header.Set(key, value) + } + } + if spec.Body != nil && spec.Body.Mode == "json" && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } + + client := h.BaseClient + if spec.TimeoutMS > 0 { + client = &http.Client{Timeout: time.Duration(spec.TimeoutMS) * time.Millisecond} + } + started := time.Now() + res, err := client.Do(req) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed", "detail": err.Error()}) + return + } + defer res.Body.Close() + + payload, err := io.ReadAll(io.LimitReader(res.Body, maxResponseBytes+1)) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "cannot read upstream response", "detail": err.Error()}) + return + } + if len(payload) > maxResponseBytes { + c.JSON(http.StatusBadGateway, gin.H{"error": "upstream response exceeds 10 MiB limit"}) + return + } + + headers := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + headers[strings.ToLower(key)] = strings.Join(values, ", ") + } + c.JSON(http.StatusOK, HttpResult{ + Status: res.StatusCode, + Headers: headers, + Body: string(payload), + TimeMS: time.Since(started).Milliseconds(), + Size: len(payload), + }) +} + +func isHopByHop(name string) bool { + switch strings.ToLower(name) { + case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host", "content-length": + return true + default: + return false + } +} diff --git a/apps/server/internal/proxy/stream.go b/apps/server/internal/proxy/stream.go new file mode 100644 index 0000000..052d922 --- /dev/null +++ b/apps/server/internal/proxy/stream.go @@ -0,0 +1,145 @@ +package proxy + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// ProxyHTTPStream 以 NDJSON 流式转发响应(SSE / chunked / 大响应体)。 +// 事件序列:meta → chunk* → end | error +type streamEvent struct { + Type string `json:"type"` + Status int `json:"status,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Data string `json:"data,omitempty"` + Message string `json:"message,omitempty"` + TimeMS int64 `json:"timeMs,omitempty"` + Size int `json:"size,omitempty"` +} + +func (h *Handler) ProxyHTTPStream(c *gin.Context) { + var spec HttpSpec + if err := c.ShouldBindJSON(&spec); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid HttpSpec", "detail": err.Error()}) + return + } + + method := normalizeMethod(spec.Method) + u, err := ValidateURL(spec.URL) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + query := u.Query() + for key, value := range spec.Query { + query.Set(key, value) + } + u.RawQuery = query.Encode() + + var body io.Reader + if spec.Body != nil && spec.Body.Mode != "none" && method != http.MethodGet && method != http.MethodHead { + body = bytes.NewBufferString(spec.Body.Raw) + } + + req, err := http.NewRequestWithContext(c.Request.Context(), method, u.String(), body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "cannot create request", "detail": err.Error()}) + return + } + for key, value := range spec.Headers { + if !isHopByHop(key) { + req.Header.Set(key, value) + } + } + if spec.Body != nil && spec.Body.Mode == "json" && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } + + client := h.BaseClient + if spec.TimeoutMS > 0 { + client = &http.Client{Timeout: time.Duration(spec.TimeoutMS) * time.Millisecond} + } else { + // 流式场景默认不设整体超时(SSE 长连接),交给客户端断开 + client = &http.Client{} + } + + started := time.Now() + res, err := client.Do(req) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed", "detail": err.Error()}) + return + } + defer res.Body.Close() + + headers := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + headers[key] = joinNonEmpty(values) + } + + flusher, ok := c.Writer.(http.Flusher) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "streaming unsupported"}) + return + } + c.Header("Content-Type", "application/x-ndjson") + c.Header("Cache-Control", "no-cache") + c.Header("X-Accel-Buffering", "no") + c.Status(http.StatusOK) + + enc := json.NewEncoder(c.Writer) + writeEvent := func(ev streamEvent) bool { + if err := enc.Encode(ev); err != nil { + return false + } + flusher.Flush() + return true + } + + headers["content-type"] = res.Header.Get("Content-Type") + if !writeEvent(streamEvent{Type: "meta", Status: res.StatusCode, Headers: headers}) { + return + } + + reader := bufio.NewReaderSize(res.Body, 32*1024) + buf := make([]byte, 16*1024) + total := 0 + for { + n, readErr := reader.Read(buf) + if n > 0 { + total += n + ok := writeEvent(streamEvent{ + Type: "chunk", + Data: base64.StdEncoding.EncodeToString(buf[:n]), + Size: n, + }) + if !ok { + return + } + } + if readErr != nil { + break + } + if c.Request.Context().Err() != nil { + return + } + } + writeEvent(streamEvent{Type: "end", TimeMS: time.Since(started).Milliseconds(), Size: total}) +} + +func joinNonEmpty(values []string) string { + out := "" + for i, v := range values { + if i > 0 { + out += ", " + } + out += v + } + return out +} diff --git a/apps/server/internal/proxy/types.go b/apps/server/internal/proxy/types.go new file mode 100644 index 0000000..e945925 --- /dev/null +++ b/apps/server/internal/proxy/types.go @@ -0,0 +1,23 @@ +package proxy + +type BodySpec struct { + Mode string `json:"mode"` + Raw string `json:"raw,omitempty"` +} + +type HttpSpec struct { + Method string `json:"method" binding:"required"` + URL string `json:"url" binding:"required"` + Headers map[string]string `json:"headers"` + Query map[string]string `json:"query,omitempty"` + Body *BodySpec `json:"body,omitempty"` + TimeoutMS int `json:"timeoutMs,omitempty"` +} + +type HttpResult struct { + Status int `json:"status"` + Headers map[string]string `json:"headers"` + Body string `json:"body"` + TimeMS int64 `json:"timeMs"` + Size int `json:"size"` +} diff --git a/apps/server/internal/proxy/ws_tunnel.go b/apps/server/internal/proxy/ws_tunnel.go new file mode 100644 index 0000000..877a9b1 --- /dev/null +++ b/apps/server/internal/proxy/ws_tunnel.go @@ -0,0 +1,236 @@ +package proxy + +import ( + "crypto/rand" + "encoding/hex" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +// WSTunnel 让浏览器借道 Gin 连接带自定义 Header 的 WebSocket: +// +// POST /v1/proxy/ws/connect {url, headers, protocols} → {sessionId} +// GET /v1/proxy/ws/attach?sessionId=… (浏览器 WebSocket 双向桥) +// POST /v1/proxy/ws/close {sessionId} +type wsTunnelSpec struct { + URL string `json:"url" binding:"required"` + Headers map[string]string `json:"headers,omitempty"` + Protocols []string `json:"protocols,omitempty"` +} + +type wsTunnelSession struct { + id string + target *websocket.Conn + attach chan *websocket.Conn + createdAt time.Time +} + +type WSTunnel struct { + mu sync.Mutex + sessions map[string]*wsTunnelSession + upgrader websocket.Upgrader +} + +func NewWSTunnel() *WSTunnel { + t := &WSTunnel{ + sessions: map[string]*wsTunnelSession{}, + upgrader: websocket.Upgrader{ + ReadBufferSize: 32 * 1024, + WriteBufferSize: 32 * 1024, + CheckOrigin: func(r *http.Request) bool { + // 仅本地 UI 使用;Origin 校验交给 CORS 中间件 + return true + }, + }, + } + go t.gc() + return t +} + +func (t *WSTunnel) gc() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for range ticker.C { + t.mu.Lock() + for id, s := range t.sessions { + // 未 attach 或空闲超过 10 分钟的会话回收 + if time.Since(s.createdAt) > 10*time.Minute { + _ = s.target.Close() + delete(t.sessions, id) + } + } + t.mu.Unlock() + } +} + +// ValidateWSURL 校验 ws/wss 目标(http/https 自动换算) +func ValidateWSURL(raw string) (*url.URL, error) { + u, err := url.Parse(raw) + if err != nil { + return nil, err + } + switch u.Scheme { + case "ws", "http": + u.Scheme = "ws" + case "wss", "https": + u.Scheme = "wss" + default: + return nil, errOnlyWS + } + if u.Hostname() == "" { + return nil, errEmptyHost + } + if _, blocked := blockedHosts[strings.ToLower(u.Hostname())]; blocked { + return nil, errBlockedHost + } + return u, nil +} + +var ( + errOnlyWS = &wsTunnelError{"only ws/wss schemes are allowed"} + errEmptyHost = &wsTunnelError{"url host is required"} + errBlockedHost = &wsTunnelError{"blocked metadata host"} +) + +type wsTunnelError struct{ msg string } + +func (e *wsTunnelError) Error() string { return e.msg } + +func (t *WSTunnel) Connect(c *gin.Context) { + var spec wsTunnelSpec + if err := c.ShouldBindJSON(&spec); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid spec", "detail": err.Error()}) + return + } + u, err := ValidateWSURL(spec.URL) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + dialer := websocket.Dialer{HandshakeTimeout: 15 * time.Second} + dialHeader := http.Header{} + for key, value := range spec.Headers { + if !isHopByHop(key) { + dialHeader.Set(key, value) + } + } + target, _, err := dialer.Dial(u.String(), dialHeader) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "dial failed", "detail": err.Error()}) + return + } + + id := randomSessionID() + session := &wsTunnelSession{id: id, target: target, attach: make(chan *websocket.Conn, 1), createdAt: time.Now()} + t.mu.Lock() + t.sessions[id] = session + t.mu.Unlock() + + c.JSON(http.StatusOK, gin.H{"sessionId": id}) + + // 30 秒内没有浏览器 attach 就回收 + go func() { + select { + case client := <-session.attach: + t.bridge(client, session) + case <-time.After(30 * time.Second): + t.mu.Lock() + if s, ok := t.sessions[id]; ok { + _ = s.target.Close() + delete(t.sessions, id) + } + t.mu.Unlock() + } + }() +} + +func (t *WSTunnel) Attach(c *gin.Context) { + id := c.Query("sessionId") + t.mu.Lock() + session, ok := t.sessions[id] + t.mu.Unlock() + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": "session not found"}) + return + } + client, err := t.upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + return + } + select { + case session.attach <- client: + // bridge goroutine 已在 Connect 中启动 + default: + _ = client.Close() + } +} + +func (t *WSTunnel) Close(c *gin.Context) { + var body struct { + SessionID string `json:"sessionId"` + } + _ = c.ShouldBindJSON(&body) + t.mu.Lock() + session, ok := t.sessions[body.SessionID] + if ok { + delete(t.sessions, body.SessionID) + } + t.mu.Unlock() + if ok { + _ = session.target.Close() + } + c.JSON(http.StatusOK, gin.H{"ok": true}) +} + +// bridge 双向转发:浏览器 → 目标;目标 → 浏览器 +func (t *WSTunnel) bridge(client *websocket.Conn, session *wsTunnelSession) { + defer func() { + _ = client.Close() + _ = session.target.Close() + t.mu.Lock() + delete(t.sessions, session.id) + t.mu.Unlock() + }() + + done := make(chan struct{}) + // 浏览器 → 目标 + go func() { + defer close(done) + for { + msgType, data, err := client.ReadMessage() + if err != nil { + return + } + if err := session.target.WriteMessage(msgType, data); err != nil { + return + } + } + }() + // 目标 → 浏览器(bridge 协议:控制帧 + 数据帧) + _ = client.WriteMessage(websocket.TextMessage, []byte(`{"type":"open"}`)) + for { + msgType, data, err := session.target.ReadMessage() + if err != nil { + _ = client.WriteMessage(websocket.TextMessage, []byte(`{"type":"closed","reason":"target closed"}`)) + return + } + if err := client.WriteMessage(msgType, data); err != nil { + return + } + } +} + +func randomSessionID() string { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return time.Now().Format("20060102150405.000000000") + } + return hex.EncodeToString(b) +} diff --git a/apps/server/internal/server/router.go b/apps/server/internal/server/router.go new file mode 100644 index 0000000..d81c821 --- /dev/null +++ b/apps/server/internal/server/router.go @@ -0,0 +1,59 @@ +package server + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "opsxapi/server/internal/docs" + "opsxapi/server/internal/proxy" + "opsxapi/server/internal/site" +) + +func NewRouter() *gin.Engine { + r := gin.New() + _ = r.SetTrustedProxies(nil) + r.Use(gin.Logger(), gin.Recovery(), localhostCORS()) + + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true, "service": "opsxapi"}) + }) + p := proxy.NewHandler() + r.POST("/v1/proxy/http", p.ProxyHTTP) + r.POST("/v1/proxy/http/stream", p.ProxyHTTPStream) + + tunnel := proxy.NewWSTunnel() + r.POST("/v1/proxy/ws/connect", tunnel.Connect) + r.GET("/v1/proxy/ws/attach", tunnel.Attach) + r.POST("/v1/proxy/ws/close", tunnel.Close) + + docs.New().Register(r) + site.New().Register(r) + + return r +} + +func localhostCORS() gin.HandlerFunc { + allowed := map[string]struct{}{ + "http://localhost:5173": {}, + "http://127.0.0.1:5173": {}, + "http://localhost:1420": {}, + "http://127.0.0.1:1420": {}, + "tauri://localhost": {}, + "http://tauri.localhost": {}, + } + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if _, ok := allowed[strings.TrimSuffix(origin, "/")]; ok { + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Vary", "Origin") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + } + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } + c.Next() + } +} diff --git a/apps/server/internal/site/assets/site.css b/apps/server/internal/site/assets/site.css new file mode 100644 index 0000000..6035df1 --- /dev/null +++ b/apps/server/internal/site/assets/site.css @@ -0,0 +1,282 @@ +/* OpsxAPI 官网 —— 营销单页样式 */ +:root { + --blue: #2563eb; + --blue-dark: #1d4ed8; + --blue-soft: rgba(37, 99, 235, 0.08); + --bg: #ffffff; + --bg-alt: #f6f8fc; + --border: #e4e9f1; + --text: #111827; + --text-2: #5b6880; + --text-3: #98a2b3; + --green: #059669; + --amber: #d97706; + --radius: 14px; + --shadow: 0 1px 3px rgba(16,24,40,.06), 0 12px 32px rgba(16,24,40,.07); + --mono: "JetBrains Mono", SFMono-Regular, Menlo, monospace; +} +* { box-sizing: border-box; scroll-behavior: smooth; } +body { + margin: 0; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif; + color: var(--text); background: var(--bg); font-size: 15px; line-height: 1.75; +} +a { color: var(--blue); text-decoration: none; } +h1,h2,h3 { letter-spacing: -0.015em; } +section[id] { scroll-margin-top: 76px; } +.wrap { max-width: 1120px; margin: 0 auto; padding: 0 24px; } +.wrap.narrow { max-width: 820px; } +/* ---------- SVG 图标(lucide sprite) ---------- */ +.i { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -2px; } +.i-lg { width: 28px; height: 28px; vertical-align: middle; } +code { font-family: var(--mono); font-size: 13px; background: var(--blue-soft); color: var(--blue-dark); border-radius: 5px; padding: 1px 6px; } + +/* ---------- 按钮 ---------- */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + border-radius: 10px; padding: 9px 20px; font-size: 14.5px; font-weight: 600; cursor: pointer; + transition: all .18s; border: 1px solid transparent; white-space: nowrap; +} +.btn-lg { padding: 12px 26px; font-size: 15.5px; } +.btn-primary { background: var(--blue); color: #fff !important; box-shadow: 0 6px 18px rgba(37,99,235,.28); } +.btn-primary:hover { background: var(--blue-dark); transform: translateY(-1px); } +.btn-ghost { border-color: var(--border); color: var(--text) !important; background: #fff; } +.btn-ghost:hover { border-color: var(--blue); color: var(--blue) !important; } +.btn-inverse { background: #fff; color: var(--blue) !important; } +.btn-inverse:hover { transform: translateY(-1px); } +.btn-outline { border-color: rgba(255,255,255,.5); color: #fff !important; } +.btn-outline:hover { border-color: #fff; background: rgba(255,255,255,.1); } + +/* ---------- 导航 ---------- */ +.nav { + position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,.9); + backdrop-filter: blur(10px); border-bottom: 1px solid var(--border); +} +.nav-inner { max-width: 1120px; margin: 0 auto; padding: 0 24px; height: 60px; display: flex; align-items: center; gap: 28px; } +.brand { display: flex; align-items: center; gap: 9px; } +.brand-logo { display: inline-flex; } +.brand-logo svg { display: block; border-radius: 8px; } +.brand-name { font-size: 17px; font-weight: 800; color: var(--text) !important; } +.nav-links { display: flex; gap: 22px; } +.nav-links a { font-size: 14px; font-weight: 500; color: var(--text-2) !important; } +.nav-links a:hover { color: var(--blue) !important; } +.nav-cta { margin-left: auto; } + +/* ---------- Hero ---------- */ +.hero { + position: relative; overflow: hidden; + background: + radial-gradient(1200px 500px at 85% -10%, rgba(37,99,235,.14), transparent 60%), + radial-gradient(800px 400px at -10% 10%, rgba(5,150,105,.08), transparent 55%), + linear-gradient(180deg, #f8faff 0%, #fff 100%); + border-bottom: 1px solid var(--border); +} +.hero-inner { + max-width: 1120px; margin: 0 auto; padding: 72px 24px 64px; + display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 48px; align-items: center; +} +.hero-badge { + display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; color: var(--blue-dark); + background: #fff; border: 1px solid rgba(37,99,235,.25); border-radius: 999px; padding: 4px 14px; + box-shadow: 0 2px 8px rgba(37,99,235,.1); margin-bottom: 18px; +} +.hero h1 { font-size: 44px; line-height: 1.22; font-weight: 800; margin: 0 0 16px; } +.hero h1 em { font-style: normal; color: var(--blue); } +.hero-sub { color: var(--text-2); font-size: 16px; margin: 0 0 26px; } +.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; } +.hero-actions.center { justify-content: center; } +.hero-pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; } +.hero-pills span { + font-size: 12px; color: var(--text-2); background: #fff; border: 1px solid var(--border); + padding: 3px 12px; border-radius: 999px; +} + +/* ---------- Mock 界面 ---------- */ +.mock { + border-radius: 14px; overflow: hidden; box-shadow: var(--shadow); border: 1px solid #1e2b45; + background: #0d1526; transform: perspective(1400px) rotateY(-4deg) rotateX(1.5deg); +} +.mock-titlebar { + display: flex; align-items: center; gap: 7px; height: 34px; padding: 0 12px; + background: #101a30; border-bottom: 1px solid #1e2b45; +} +.mock-titlebar i { width: 11px; height: 11px; border-radius: 50%; background: #3a4a6b; } +.mock-titlebar i:nth-child(1) { background: #f87171; } .mock-titlebar i:nth-child(2) { background: #fbbf24; } .mock-titlebar i:nth-child(3) { background: #34d399; } +.mock-titlebar span { margin-left: 8px; color: #8b99b0; font-size: 11px; } +.mock-body { display: grid; grid-template-columns: 158px minmax(0,1fr); min-height: 320px; } +.mock-side { border-right: 1px solid #1e2b45; padding: 10px 8px; background: #0c1322; } +.mock-search { height: 22px; border-radius: 6px; background: #16213a; margin-bottom: 10px; } +.mock-group { color: #8b99b0; font-size: 11px; padding: 6px 6px 3px; } +.mock-item { display: flex; align-items: center; gap: 6px; color: #aab8d0; font-size: 11.5px; padding: 4px 6px; border-radius: 6px; } +.mock-item.active { background: rgba(37,99,235,.18); color: #93c5fd; } +.mi { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; opacity: .8; } +.m-get { color: #34d399; font-style: normal; font-size: 9.5px; min-width: 30px; } +.m-post { color: #fbbf24; font-style: normal; font-size: 9.5px; min-width: 30px; } +.mock-main { padding: 10px 12px; display: flex; flex-direction: column; gap: 9px; } +.mock-urlbar { display: flex; align-items: center; gap: 8px; } +.mock-url { + flex: 1; background: #0a1120; border: 1px solid #22304f; border-radius: 7px; color: #cbd5e1; + font-family: var(--mono); font-size: 11px; padding: 6px 10px; +} +.mock-send { + background: var(--blue); color: #fff; font-size: 11px; font-weight: 600; + border-radius: 7px; padding: 6px 14px; +} +.mock-req, .mock-resp { border: 1px solid #1e2b45; border-radius: 9px; padding: 8px 10px; background: #0c1322; } +.mock-req .tab, .mock-resp .tab { color: #5c6a83; font-size: 10.5px; margin-right: 12px; } +.mock-req .tab.on { color: #93c5fd; border-bottom: 2px solid var(--blue); padding-bottom: 2px; } +.mock-resp .tab.on { color: #6ee7b7; border-bottom: 2px solid var(--green); padding-bottom: 2px; } +.mock-status { float: right; color: #34d399; font-size: 10px; font-family: var(--mono); } +.mock pre { + margin: 8px 0 2px; font-family: var(--mono); font-size: 11px; line-height: 1.6; color: #cbd5e1; + white-space: pre-wrap; +} +.mock pre i { font-style: normal; color: #7dd3fc; } +.mock pre u { text-decoration: none; color: #86efac; } + +/* ---------- 通用 Section ---------- */ +.section { padding: 72px 0; } +.section.alt { background: var(--bg-alt); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); } +.sec-title { font-size: 30px; font-weight: 800; text-align: center; margin: 0 0 8px; } +.sec-sub { text-align: center; color: var(--text-2); margin: 0 0 40px; } +.grid { display: grid; gap: 18px; } +.g3 { grid-template-columns: repeat(3, minmax(0,1fr)); } +.g4 { grid-template-columns: repeat(4, minmax(0,1fr)); } + +/* ---------- 卡片 ---------- */ +.card { + background: #fff; border: 1px solid var(--border); border-radius: var(--radius); + padding: 22px; box-shadow: 0 1px 2px rgba(16,24,40,.04); transition: all .18s; +} +.card:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: rgba(37,99,235,.35); } +.card h3 { font-size: 16px; margin: 0 0 6px; } +.card p { color: var(--text-2); font-size: 13.5px; margin: 0; } +.card-icon { color: var(--blue); margin-bottom: 10px; } +.card-icon .i-lg { width: 30px; height: 30px; } +.card.row { display: flex; gap: 14px; align-items: flex-start; } +.card.row .card-icon { margin: 2px 0 0; } +.card.mini h3 { font-size: 15px; display: flex; align-items: center; gap: 8px; } +.card.mini h3 .i-lg { width: 20px; height: 20px; color: var(--blue); } +.card.mini p { font-size: 13px; } + +/* ---------- 协议 ---------- */ +.proto { + background: #fff; border: 1px solid var(--border); border-left: 3px solid var(--blue); + border-radius: 12px; padding: 18px 20px; +} +.proto h3 { margin: 0 0 6px; font-size: 15.5px; display: flex; align-items: center; gap: 8px; } +.proto h3 .i { color: var(--blue); width: 17px; height: 17px; } +.proto p { margin: 0; color: var(--text-2); font-size: 13.5px; } + +/* ---------- 导入导出 ---------- */ +.badges { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 18px; } +.badges span { + background: #fff; border: 1px solid var(--border); border-radius: 999px; + padding: 6px 16px; font-size: 13px; font-weight: 500; color: var(--text); +} +.badges span.more { border-style: dashed; color: var(--text-2); } +.footnote { text-align: center; color: var(--text-3); font-size: 12.5px; margin: 10px 0 0; } +.exchange-actions { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; margin-top: 26px; } +.chip { + display: inline-flex; align-items: center; gap: 6px; + background: #fff; border: 1px solid var(--border); border-radius: 10px; padding: 8px 16px; + font-size: 13px; font-weight: 600; color: var(--text-2); +} +.chip.big { background: var(--blue); border-color: var(--blue); color: #fff; padding: 10px 22px; } +.exchange-arrow { color: var(--text-3); font-size: 15px; } + +/* ---------- 对比表 ---------- */ +.table-wrap { overflow-x: auto; } +.cmp { border-collapse: collapse; width: 100%; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 0 0 1px var(--border); font-size: 14px; } +.cmp th, .cmp td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); } +.cmp th { background: #f9fbfe; font-size: 13.5px; } +.cmp tr:last-child td { border-bottom: none; } +.cmp td.hl { background: rgba(37,99,235,.05); color: var(--blue-dark); } +.cmp th.hl { background: var(--blue); color: #fff; } + +/* ---------- 定价 ---------- */ +.pricing { display: grid; grid-template-columns: repeat(2, minmax(0, 380px)); gap: 22px; justify-content: center; } +.pricing.three { grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 1080px; margin: 0 auto; } +.price-card { + position: relative; background: #fff; border: 1px solid var(--border); border-radius: 16px; + padding: 30px 28px 26px; box-shadow: 0 1px 2px rgba(16,24,40,.04); display: flex; flex-direction: column; +} +.price-card.featured { border: 2px solid var(--blue); box-shadow: 0 12px 36px rgba(37,99,235,.16); } +.price-card.enterprise { + background: linear-gradient(160deg, #101a30 0%, #0d1526 70%); + border: 1px solid #2a3b5e; box-shadow: 0 14px 40px rgba(13,21,38,.4); +} +.price-card.enterprise .plan { color: #8b99b0; } +.price-card.enterprise .price b { color: #fff; font-size: 36px; } +.price-card.enterprise .price span { color: #8b99b0; } +.price-card.enterprise .plan-sub { color: #8b99b0; } +.price-card.enterprise li { color: #aab8d0; border-bottom-color: #22304f; } +.price-card.enterprise .deal-note { + margin-top: 14px; font-size: 11.5px; line-height: 1.7; color: #8b99b0; + background: rgba(255,255,255,.05); border: 1px dashed #2a3b5e; border-radius: 9px; padding: 9px 12px; +} +.price-card.enterprise .deal-note b { color: #93c5fd; } +.btn-dark { background: #fff; color: #0d1526 !important; } +.btn-dark:hover { transform: translateY(-1px); background: #e8eefc; } +.plan-tag { + position: absolute; top: -13px; left: 50%; transform: translateX(-50%); + background: var(--blue); color: #fff; font-size: 12px; font-weight: 700; + padding: 3px 14px; border-radius: 999px; white-space: nowrap; +} +.plan-tag.dark { background: #0d1526; border: 1px solid #2a3b5e; } +.plan { font-size: 16px; font-weight: 700; color: var(--text-2); } +.price { margin: 8px 0 2px; } +.price b { font-size: 44px; font-weight: 800; letter-spacing: -.02em; color: var(--text); } +.price span { color: var(--text-3); font-size: 14px; margin-left: 4px; } +.plan-sub { color: var(--text-3); font-size: 13px; margin-bottom: 18px; } +.price-card ul { list-style: none; padding: 0; margin: 0 0 22px; flex: 1; } +.price-card li { font-size: 13.5px; color: var(--text-2); padding: 6px 0; border-bottom: 1px dashed var(--border); } +.price-card li:last-child { border-bottom: none; } +.price-card .btn { width: 100%; } +.growth-title { text-align: center; font-size: 20px; font-weight: 700; margin: 52px 0 22px; } + +/* ---------- FAQ ---------- */ +.faq details { + background: #fff; border: 1px solid var(--border); border-radius: 12px; + padding: 16px 20px; margin-bottom: 12px; +} +.faq summary { font-weight: 600; cursor: pointer; font-size: 15px; list-style: none; display: flex; justify-content: space-between; } +.faq summary::after { content: "+"; color: var(--text-3); font-weight: 400; } +.faq details[open] summary::after { content: "-"; } +.faq details p { margin: 10px 0 0; color: var(--text-2); font-size: 14px; } + +/* ---------- CTA ---------- */ +.cta { + background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 55%, #3b82f6 100%); + color: #fff; text-align: center; padding: 72px 24px; +} +.cta h2 { font-size: 32px; font-weight: 800; margin: 0 0 10px; } +.cta p { color: rgba(255,255,255,.85); margin: 0 0 28px; } + +/* ---------- Footer ---------- */ +.footer { background: #0d1526; color: #8b99b0; padding: 52px 0 0; font-size: 13.5px; } +.footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 32px; padding-bottom: 36px; } +.footer .brand-name { color: #fff !important; } +.footer .f-logo { display: block; border-radius: 8px; } +.footer .brand { display: flex; align-items: center; gap: 9px; } +.footer-desc { margin: 12px 0 0; font-size: 13px; line-height: 1.8; } +.footer h4 { color: #fff; font-size: 13.5px; margin: 4px 0 12px; } +.footer a { display: block; color: #8b99b0 !important; padding: 4px 0; font-size: 13px; } +.footer a:hover { color: #fff !important; } +.footer-bar { border-top: 1px solid #1e2b45; text-align: center; padding: 18px; font-size: 12px; color: #5c6a83; } + +/* ---------- 响应式 ---------- */ +@media (max-width: 1020px) { + .hero-inner { grid-template-columns: 1fr; padding-top: 52px; } + .mock { transform: none; } + .g4 { grid-template-columns: repeat(2, minmax(0,1fr)); } + .g3 { grid-template-columns: repeat(2, minmax(0,1fr)); } + .pricing, .pricing.three { grid-template-columns: minmax(0, 420px); } + .footer-grid { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 680px) { + .nav-links { display: none; } + .hero h1 { font-size: 32px; } + .sec-title { font-size: 24px; } + .g3, .g4 { grid-template-columns: 1fr; } + .footer-grid { grid-template-columns: 1fr; } +} diff --git a/apps/server/internal/site/site.go b/apps/server/internal/site/site.go new file mode 100644 index 0000000..4d7e427 --- /dev/null +++ b/apps/server/internal/site/site.go @@ -0,0 +1,67 @@ +// Package site 在 /home(及 /)提供产品官网单页(SSR,资源嵌入二进制)。 +package site + +import ( + "embed" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +//go:embed template.html assets +var files embed.FS + +// Handler 官网服务 +type Handler struct { + html string +} + +func New() *Handler { + raw, err := files.ReadFile("template.html") + if err != nil { + panic("site: template.html 缺失: " + err.Error()) + } + return &Handler{html: string(raw)} +} + +// Register 注册路由:/ 与 /home 均渲染官网 +func (h *Handler) Register(r *gin.Engine) { + r.GET("/", h.Page) + r.GET("/home", h.Page) + r.GET("/index.html", h.Page) + r.GET("/site/assets/*file", h.Asset) +} + +// Page 渲染官网 +func (h *Handler) Page(c *gin.Context) { + c.Header("Content-Type", "text/html; charset=utf-8") + c.Header("Cache-Control", "no-cache") + _, _ = c.Writer.WriteString(h.html) +} + +// Asset 静态资源 +func (h *Handler) Asset(c *gin.Context) { + name := strings.TrimPrefix(c.Param("file"), "/") + if strings.Contains(name, "..") { + c.Status(http.StatusBadRequest) + return + } + data, err := files.ReadFile("assets/" + name) + if err != nil { + c.Status(http.StatusNotFound) + return + } + switch { + case strings.HasSuffix(name, ".css"): + c.Header("Content-Type", "text/css; charset=utf-8") + case strings.HasSuffix(name, ".js"): + c.Header("Content-Type", "application/javascript; charset=utf-8") + case strings.HasSuffix(name, ".svg"): + c.Header("Content-Type", "image/svg+xml") + default: + c.Header("Content-Type", "application/octet-stream") + } + c.Header("Cache-Control", "public, max-age=3600") + c.Data(http.StatusOK, "", data) +} diff --git a/apps/server/internal/site/site_test.go b/apps/server/internal/site/site_test.go new file mode 100644 index 0000000..f9e0272 --- /dev/null +++ b/apps/server/internal/site/site_test.go @@ -0,0 +1,63 @@ +package site + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" +) + +func newTestRouter(t *testing.T) *gin.Engine { + t.Helper() + gin.SetMode(gin.TestMode) + r := gin.New() + New().Register(r) + return r +} + +func TestSitePageRender(t *testing.T) { + r := newTestRouter(t) + for _, path := range []string{"/", "/home", "/index.html"} { + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil)) + if w.Code != http.StatusOK { + t.Fatalf("%s: expect 200, got %d", path, w.Code) + } + body := w.Body.String() + for _, want := range []string{ + "OpsxAPI", // 标题 + `id="features"`, // 功能区 + `id="pricing"`, // 定价区 + "¥99", // 入门价 + "¥8,000", // 企业档标价 + "¥30,000", // 买断标价 + "¥5,000–8,000", // 成交区间 + "永久买断", // 买断 FAQ + "早鸟创始价", // 让利策略 + "Socket.IO", // 协议覆盖 + "hello@opsxapi.dev", // 联系方式 + `href="/docs"`, // 文档互链 + `href="/site/assets/site.css"`, // 样式资源 + } { + if !strings.Contains(body, want) { + t.Fatalf("%s: missing %q", path, want) + } + } + } +} + +func TestSiteAsset(t *testing.T) { + r := newTestRouter(t) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/site/assets/site.css", nil)) + if w.Code != http.StatusOK || !strings.Contains(w.Header().Get("Content-Type"), "text/css") { + t.Fatalf("css: %d %s", w.Code, w.Header().Get("Content-Type")) + } + w = httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/site/assets/../template.html", nil)) + if w.Code != http.StatusBadRequest && w.Code != http.StatusNotFound { + t.Fatalf("path traversal should be rejected, got %d", w.Code) + } +} diff --git a/apps/server/internal/site/template.html b/apps/server/internal/site/template.html new file mode 100644 index 0000000..9d78d8b --- /dev/null +++ b/apps/server/internal/site/template.html @@ -0,0 +1,357 @@ +<!doctype html> +<html lang="zh-CN"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>OpsxAPI — 本地优先的全协议接口工作台 + + + + + + + + + + + + +
    +
    +
    +
    v1.0 正式发布 · 数据 100% 本地
    +

    一个工作台,
    调完六种协议的接口

    +

    + HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT。
    + 集合管理、多环境变量、全格式导入导出、代码生成、内置文档站 —— 运维与开发的自托管接口利器。 +

    + +
    + 本地优先自定义 WS 握手头SSE 断点续传OpenAPI 双向同步 +
    +
    + + + +
    +
    + + +
    +
    +

    为什么团队选择 OpsxAPI

    +

    不追赶云端协作的花活,专注把「调试接口」这件事做到极致。

    +
    +
    +
    +

    本地优先,数据主权

    +

    集合、环境、历史全部保存在本机,不经过任何第三方服务器。内网、涉密项目放心用。

    +
    +
    +
    +

    六协议一个窗口

    +

    HTTP / GraphQL / WebSocket / SSE / Socket.IO / MQTT 统一工作台,实时消息分色日志。

    +
    +
    +
    +

    浏览器也能带握手头

    +

    内置 WebSocket 隧道与流式代理,突破浏览器 CORS 与 Header 限制,调试网关级接口。

    +
    +
    +
    +

    生态互通

    +

    Apifox / Postman / Swagger / cURL / HAR 一键导入导出,迁移零成本,文档即代码。

    +
    +
    +
    +
    + + +
    +
    +

    开箱即用的专业能力

    +

    从单人调试到团队交付,该有的都有。

    +
    +

    集合与多标签

    目录树、搜索、右键菜单、拖拽移动;多标签并行调试。

    +

    多环境变量

    {{baseUrl}} 全局替换,正式 / 测试 / 本地一键切换。

    +

    鉴权管理

    Bearer / Basic / API Key,token 走变量,一处配置全局生效。

    +

    实时协议调试

    WS 握手头、SSE 断点续传、Socket.IO 事件收发、MQTT 通配符订阅。

    +

    代码生成

    任意请求一键生成 cURL / fetch / Python / Go,直接进脚本与 CI。

    +

    内置文档站

    随服务自带 /docs 用户文档与 Markdown 接口文档导出,交付即完整。

    +
    +
    +
    + + +
    +
    +

    六种协议,全覆盖

    +

    每个协议都有为它定制的编辑器,而不是塞进同一个表单。

    +
    +

    HTTP / REST

    7 种方法 · 6 种请求体 · 响应 Pretty 高亮 · 历史回跳

    +

    GraphQL

    Query + Variables 编辑 · errors 高亮 · 生成 cURL 报文

    +

    WebSocket

    自定义握手 Header(本地隧道)· 子协议 · 分色消息日志

    +

    SSE

    直连失败自动切代理 · Last-Event-ID 续传 · 事件按名称归类

    +

    Socket.IO

    事件监听(onAny)· emit 参数 JSON · path / auth 配置

    +

    MQTT

    wss Broker · 通配符订阅 · QoS 0/1/2 · 断线自动重连

    +
    +
    +
    + + +
    +
    +

    导入导出,迁移零成本

    +

    粘贴、文件、URL 三种来源,格式自动识别。

    +
    + OpenAPI 3.0 / 3.1Swagger 2.0ApifoxPostman v2.1 + ApipostcURLHARInsomniaHoppscotchMarkdown + YApi / Apidoc / Apizza / Eolink ① +
    +

    ① 经 OpenAPI / Postman 格式中转导入。导入自动:tag 建目录、路径参数转变量、schema 生成示例体、security 映射鉴权。

    +
    +
    导入
    ──→
    OpsxAPI
    ──→
    +
    OpenAPI / Postman
    Markdown 文档
    代码生成
    全量备份
    +
    +
    +
    + + +
    +
    +

    与主流工具的对比

    +

    价格为各官网公开标价(2026-09 整理),以官方为准。

    +
    + + + + + + + + + + + + +
    能力OpsxAPIApifoxApipost
    入门付费价格¥99/年(个人)¥99/人/年②免费 + 商业版
    专业档价格¥99/人/年(团队)¥199/人/年按席位订阅
    数据存储位置100% 本地云端 / 私有化云端 / 私有化
    调试协议HTTP · GraphQL · WS · SSE · Socket.IO · MQTTHTTP · WS · GraphQLHTTP · WS · gRPC 等
    WS 自定义握手 Header(Web)✓ 内置隧道桌面端支持桌面端支持
    自托管部署单二进制 + 静态页私有化付费私有化付费
    私有化 / 买断参考价¥8,000/年起 · ¥30,000 买断询价询价
    内置用户文档站✓ /docs——
    +
    +

    ② Apifox 免费版即含大部分调试能力,付费档面向团队协作;对比仅帮助按需选型,各有所长。

    +
    +
    + + +
    +
    +

    简单透明的定价

    +

    先在工作台免费用起来,需要授权与服务时再付费。

    +
    +
    +
    个人版
    +
    ¥99/ 年
    +
    单设备授权 · 全功能
    +
      +
    • ✓ 六种协议全部调试能力
    • +
    • ✓ 集合 / 环境 / 历史无限量
    • +
    • ✓ 全格式导入导出 + 代码生成
    • +
    • ✓ 桌面端 + Web 双形态
    • +
    • ✓ 1 年免费升级
    • +
    + 购买咨询 +
    + +
    +
    私有化 · 单法人不限席位
    +
    企业私有化版
    +
    ¥8,000/ 年 · 标价
    +
    或 ¥30,000 一次性永久买断(标价)
    +
      +
    • ✓ 整机 / 内网部署,数据不出企业
    • +
    • ✓ 不限席位,团队版全部能力
    • +
    • ✓ 远程部署协助 + 专属支持
    • +
    • ✓ 定制开发通道(另行报价)
    • +
    • ✓ 买断可选:永久使用 + 1 年升级
    • +
    + 获取报价 +
    + 标价透明,成交灵活:经早鸟 / 教育 / 多年预付 / 案例合作,常见成交区间 ¥5,000–8,000/年、买断 ¥15,000–30,000。 +
    +
    +
    + + +

    早鸟与让利计划 —— 把优惠变成共同成长

    +
    +
    +

    早鸟创始价

    +

    前 20 个付费席位享 5-7 折永久优惠,并进入创始客户名单,参与需求优先排期。

    +
    +
    +

    教育 / 开源

    +

    高校师生、开源维护者凭证享 6-8 折。教育用途可申请批量授权。

    +
    +
    +

    多年预付

    +

    一次预付 2 年 9 折、3 年 8 折,锁定当期价格,改善双方现金流。

    +
    +
    +

    案例换授权

    +

    愿意公开使用案例、撰写评测或作为推荐客户的,可获 额外折扣或免费席位。

    +
    +
    +

    以上为官网标价机制;最终成交价支持按规模与场景商务沟通,多年预付 / 服务范围(部署协助、定制开发)可组合。

    +
    +
    + + +
    +
    +

    购买常见问题

    +
    +
    授权如何交付?包含哪些内容?

    邮件交付授权码与部署包(单二进制 + Web 静态资源),含使用文档。团队版附内网部署指南。

    +
    一年后不续费还能用吗?

    可以。授权期内下载的版本可 永久使用,续费获得后续升级与支持。

    +
    团队版怎么计费?人员变动怎么办?

    按启用席位每年计费,5 席起。人员变动可在授权期内置换席位,无需额外费用。

    +
    可以开发票吗?

    支持增值税普通发票与专票,对公转账或线上支付均可。

    +
    我们能自己部署吗?安全吗?

    完全可以,这正是 OpsxAPI 的设计初衷:数据 100% 本地,服务仅监听本机回环,内网部署无外联。

    +
    可以永久买断吗?

    可以。企业私有化版支持一次性买断(标价 ¥30,000,含 1 年免费升级与支持),买断版本永久使用,续费仅用于继续获得升级。订阅档同理:授权期内下载的版本永久可用。

    +
    预算有限,有优惠空间吗?

    我们不直接降价,但提供四种「让利换增长」机制:早鸟创始价(前 20 席 5–7 折永久)、教育 / 开源 6–8 折、多年预付 8–9 折、以案例 / 评测换折扣。相比按年订阅的 SaaS,5 人以上团队通常 1–2 年即可回本;可自行部署(我们提供文档)或以案例分享换取创始价。

    +
    先试用再决定可以吗?

    欢迎。工作台全部调试能力可直接体验;授权版差异主要在商业支持与升级服务。

    +
    +
    +
    + + +
    +
    +

    把接口调试收回自己手里

    +

    现在打开工作台,30 秒发出第一个请求;采购事宜邮件联系,当天回复。

    + +
    +
    + + + + + + diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..0da3773 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + OpsxAPI + + +
    + + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..a6e506e --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,30 @@ +{ + "name": "@opsxapi/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "tsc -b && vite build", + "preview": "vite preview --host 127.0.0.1 --port 5173" + }, + "dependencies": { + "@opsxapi/core": "*", + "@opsxapi/transport": "*", + "@opsxapi/ui": "*", + "@tauri-apps/api": "^2.5.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.20.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", + "vite": "^6.2.2" + } +} diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg new file mode 100644 index 0000000..7e6d4c5 --- /dev/null +++ b/apps/web/public/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..efb08cb --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { OpsxWorkspace } from "@opsxapi/ui"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..e307f96 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,417 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ============ 主题变量 ============ */ +:root, +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #0b1220; + --panel: #0f1726; + --panel-2: #131e33; + --panel-3: #182540; + --border: #1e2b45; + --border-strong: #2b3b5c; + --text: #dbe4f3; + --text-dim: #8b99b0; + --text-faint: #5c6a83; + --accent: #3b82f6; + --accent-strong: #60a5fa; + --accent-soft: rgba(59, 130, 246, 0.14); + --success: #10b981; + --warn: #f59e0b; + --danger: #f43f5e; + --input-bg: #0a1120; + --code-bg: #070c16; + --hover: rgba(148, 163, 184, 0.08); + --active: rgba(59, 130, 246, 0.12); + --scrollbar: #334155; + + --m-GET: #34d399; + --m-POST: #fbbf24; + --m-PUT: #60a5fa; + --m-PATCH: #c084fc; + --m-DELETE: #fb7185; + --m-HEAD: #94a3b8; + --m-OPTIONS: #38bdf8; +} + +:root[data-theme="light"] { + color-scheme: light; + --bg: #eef1f6; + --panel: #ffffff; + --panel-2: #f6f8fb; + --panel-3: #eef2f8; + --border: #e3e8f0; + --border-strong: #cbd5e1; + --text: #1c2434; + --text-dim: #5b6880; + --text-faint: #8a95a9; + --accent: #2563eb; + --accent-strong: #1d4ed8; + --accent-soft: rgba(37, 99, 235, 0.1); + --success: #059669; + --warn: #d97706; + --danger: #e11d48; + --input-bg: #ffffff; + --code-bg: #f5f7fa; + --hover: rgba(100, 116, 139, 0.08); + --active: rgba(37, 99, 235, 0.1); + --scrollbar: #c4cdda; + + --m-GET: #059669; + --m-POST: #d97706; + --m-PUT: #2563eb; + --m-PATCH: #9333ea; + --m-DELETE: #e11d48; + --m-HEAD: #64748b; + --m-OPTIONS: #0284c7; +} + +@layer base { + * { + scrollbar-color: var(--scrollbar) transparent; + scrollbar-width: thin; + } + body { + @apply m-0 font-sans antialiased; + background: var(--bg); + color: var(--text); + } + button, + input, + select, + textarea { + font: inherit; + color: inherit; + } + ::selection { + background: var(--accent-soft); + } +} + +@layer components { + /* ---------- 文本工具 ---------- */ + .ops-dim { color: var(--text-dim); } + .ops-faint { color: var(--text-faint); } + .ops-accent { color: var(--accent-strong); } + .ops-success { color: var(--success); } + .ops-warn { color: var(--warn); } + .ops-danger { color: var(--danger); } + + /* ---------- 面板 ---------- */ + .ops-card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 0.75rem; + } + .ops-panel { background: var(--panel); } + .ops-panel-2 { background: var(--panel-2); } + .ops-border { border-color: var(--border); } + .ops-divider { border-bottom: 1px solid var(--border); } + + /* ---------- 输入 ---------- */ + .ops-input { + width: 100%; + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 0.4rem 0.7rem; + font-size: 0.8125rem; + color: var(--text); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; + } + .ops-input::placeholder { color: var(--text-faint); } + .ops-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); + } + .ops-input-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + } + .ops-label { + display: block; + margin-bottom: 0.375rem; + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-faint); + } + + /* ---------- 按钮 ---------- */ + .ops-btn { + @apply inline-flex items-center justify-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition; + border: 1px solid transparent; + cursor: pointer; + } + .ops-btn:disabled { opacity: 0.45; cursor: not-allowed; } + .ops-btn-primary { background: var(--accent); color: #fff; } + .ops-btn-primary:hover:not(:disabled) { background: var(--accent-strong); } + .ops-btn-success { background: var(--success); color: #fff; } + .ops-btn-success:hover:not(:disabled) { filter: brightness(1.1); } + .ops-btn-danger { background: var(--danger); color: #fff; } + .ops-btn-danger:hover:not(:disabled) { filter: brightness(1.1); } + .ops-btn-secondary { + background: var(--panel-2); + border-color: var(--border); + color: var(--text); + } + .ops-btn-secondary:hover:not(:disabled) { border-color: var(--border-strong); background: var(--hover); } + .ops-btn-ghost { + background: transparent; + color: var(--text-dim); + } + .ops-btn-ghost:hover:not(:disabled) { background: var(--hover); color: var(--text); } + + /* ---------- chip / badge ---------- */ + .ops-chip { + @apply rounded-full px-2.5 py-1 text-[11px] font-medium transition; + border: 1px solid var(--border); + background: var(--panel-2); + color: var(--text-dim); + } + .ops-chip:hover { border-color: var(--accent); color: var(--accent-strong); } + .ops-chip-active { + background: var(--accent-soft); + border-color: var(--accent); + color: var(--accent-strong); + } + + /* ---------- Tab ---------- */ + .ops-tab { + @apply px-3.5 py-2 text-xs font-medium transition; + color: var(--text-faint); + border-bottom: 2px solid transparent; + white-space: nowrap; + } + .ops-tab:hover { color: var(--text); } + .ops-tab-active { + color: var(--accent-strong); + border-bottom-color: var(--accent); + } + .ops-tab .ops-count { + @apply ml-1 rounded-full px-1.5 text-[10px]; + background: var(--hover); + color: var(--text-dim); + } + + /* ---------- 代码 ---------- */ + .ops-code { + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 0.75rem; + font-family: "JetBrains Mono", SFMono-Regular, Menlo, monospace; + font-size: 0.75rem; + line-height: 1.55; + color: var(--text); + outline: none; + } + .ops-code:focus { border-color: var(--accent); } + .ops-code-no-bg { + background: transparent; + border: none; + padding: 0; + } + + /* ---------- 方法色 ---------- */ + .ops-method-GET { color: var(--m-GET); } + .ops-method-POST { color: var(--m-POST); } + .ops-method-PUT { color: var(--m-PUT); } + .ops-method-PATCH { color: var(--m-PATCH); } + .ops-method-DELETE { color: var(--m-DELETE); } + .ops-method-HEAD { color: var(--m-HEAD); } + .ops-method-OPTIONS { color: var(--m-OPTIONS); } + .ops-method-badge { + @apply inline-block rounded px-1.5 py-0.5 text-[10px] font-bold tracking-wide; + background: var(--hover); + } + + /* ---------- 树 ---------- */ + .ops-tree-row { + @apply flex items-center gap-1.5 rounded-md px-2 py-1.5 text-[13px] cursor-pointer transition; + color: var(--text-dim); + } + .ops-tree-row:hover { background: var(--hover); color: var(--text); } + .ops-tree-row-active { + background: var(--active); + color: var(--accent-strong); + } + + /* ---------- 弹窗 ---------- */ + .ops-modal-mask { + @apply fixed inset-0 z-50 grid place-items-center p-6; + background: rgba(2, 6, 18, 0.55); + backdrop-filter: blur(2px); + } + .ops-modal { + @apply w-full max-w-lg rounded-2xl shadow-2xl flex flex-col overflow-hidden; + background: var(--panel); + border: 1px solid var(--border); + max-height: 85vh; + } + .ops-modal-header { + @apply flex items-center justify-between px-5 py-3.5; + border-bottom: 1px solid var(--border); + } + .ops-modal-title { @apply text-sm font-semibold; } + .ops-modal-body { @apply px-5 py-4 overflow-auto; } + .ops-modal-footer { + @apply flex items-center justify-end gap-2 px-5 py-3; + border-top: 1px solid var(--border); + background: var(--panel-2); + } + + /* ---------- 右键菜单 ---------- */ + .ops-menu { + @apply fixed z-[60] min-w-[180px] rounded-xl py-1.5 shadow-2xl; + background: var(--panel); + border: 1px solid var(--border); + } + .ops-menu-item { + @apply flex w-full items-center gap-2.5 px-3.5 py-1.5 text-left text-[13px] transition; + color: var(--text-dim); + } + .ops-menu-item:hover { background: var(--hover); color: var(--text); } + .ops-menu-item-danger:hover { color: var(--danger); } + .ops-menu-sep { border-top: 1px solid var(--border); margin: 0.375rem 0; } + + /* ---------- 实时消息 ---------- */ + .ops-msg { + @apply rounded-lg border px-2.5 py-1.5 font-mono text-[12px] leading-5 whitespace-pre-wrap break-words; + } + .ops-msg-in { border-color: color-mix(in srgb, var(--success) 30%, transparent); background: color-mix(in srgb, var(--success) 8%, transparent); } + .ops-msg-out { border-color: color-mix(in srgb, var(--accent) 30%, transparent); background: color-mix(in srgb, var(--accent) 8%, transparent); } + .ops-msg-sys { background: var(--hover); border-color: var(--border); color: var(--text-dim); } + .ops-msg-err { border-color: color-mix(in srgb, var(--danger) 40%, transparent); background: color-mix(in srgb, var(--danger) 8%, transparent); } + + /* ---------- JSON 高亮 ---------- */ + .ops-json-key { color: var(--accent-strong); } + .ops-json-str { color: var(--success); } + .ops-json-num { color: var(--m-POST); } + .ops-json-bool { color: var(--m-PATCH); } + .ops-json-null { color: var(--text-faint); } + + /* ---------- 状态点 ---------- */ + .ops-dot { @apply inline-block h-2 w-2 rounded-full; } + .ops-dot-open { background: var(--success); box-shadow: 0 0 6px var(--success); } + .ops-dot-connecting { background: var(--warn); animation: ops-pulse 1s infinite; } + .ops-dot-closed { background: var(--text-faint); } + .ops-dot-error { background: var(--danger); } + + /* ---------- 滚动区 ---------- */ + .ops-scroll { overflow: auto; } + + /* ---------- 环境徽标 ---------- */ + .env-badge { + display: inline-grid; + place-items: center; + border-radius: 6px; + font-weight: 700; + line-height: 1; + border: 1px solid transparent; + flex-shrink: 0; + user-select: none; + } + + /* ---------- 顶栏环境切换器 ---------- */ + .ops-envswitch { + @apply inline-flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs font-medium transition; + border: 1px solid var(--border); + background: var(--panel-2); + color: var(--text); + max-width: 240px; + } + .ops-envswitch:hover { border-color: var(--border-strong); } + + /* ---------- 菜单项 flex(支持徽标行) ---------- */ + .ops-menu-item-flex { display: flex; align-items: center; width: 100%; } + + /* ---------- 环境管理弹窗 ---------- */ + .env-dialog { display: grid; grid-template-columns: 196px minmax(0, 1fr); gap: 0 18px; min-height: 420px; margin: 0 -1.25rem -0.25rem; } + .env-dialog-side { + border-right: 1px solid var(--border); + padding-right: 14px; + display: flex; flex-direction: column; gap: 2px; + max-height: 60vh; overflow-y: auto; + } + .env-dialog-group { @apply mb-1.5 px-2 text-[11px] font-semibold uppercase tracking-[0.08em]; color: var(--text-faint); } + .env-dialog-item { + @apply flex items-center gap-2 rounded-lg px-2 py-1.5 text-[13px] transition; + color: var(--text-dim); + } + .env-dialog-item:hover { background: var(--hover); color: var(--text); } + .env-dialog-item.active { background: var(--active); color: var(--accent-strong); } + .env-dialog-current { + @apply rounded-full px-1.5 py-0.5 text-[10px] font-semibold; + background: color-mix(in srgb, var(--success) 14%, transparent); + color: var(--success); + } + .env-dialog-new { + @apply mt-2 flex items-center justify-center gap-1 rounded-lg border border-dashed px-2 py-2 text-xs transition; + border-color: var(--border); + color: var(--text-dim); + } + .env-dialog-new:hover { border-color: var(--accent); color: var(--accent-strong); } + .env-dialog-main { min-width: 0; display: flex; flex-direction: column; } + .env-dialog-vars { margin-top: 10px; } + + /* ---------- 信息提示条 ---------- */ + .ops-hint { + @apply mb-3 flex items-start gap-2 rounded-lg px-3 py-2 text-xs leading-5; + background: color-mix(in srgb, var(--accent) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); + color: var(--text-dim); + } + .ops-hint svg { color: var(--accent-strong); margin-top: 2px; } + .ops-hint b { color: var(--accent-strong); font-family: inherit; } + + /* ---------- {{变量}} 补全浮层 ---------- */ + .var-hint-wrap { position: relative; min-width: 0; flex: 1; display: inline-flex; } + .var-hint-panel { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 40; + min-width: 280px; + max-width: 420px; + @apply overflow-hidden rounded-xl py-1 shadow-2xl; + background: var(--panel); + border: 1px solid var(--border); + } + .var-hint-head { + @apply flex items-center gap-1.5 px-2.5 py-1.5 text-[11px]; + color: var(--text-dim); + border-bottom: 1px solid var(--border); + } + .var-hint-head .ops-faint { margin-left: auto; } + .var-hint-item { + @apply flex w-full items-center gap-3 px-2.5 py-1.5 text-left font-mono text-[12px] transition; + color: var(--text-dim); + } + .var-hint-item:hover, .var-hint-item.sel { background: var(--active); color: var(--text); } + .var-hint-name { color: var(--accent-strong); flex-shrink: 0; } + .var-hint-value { @apply truncate; color: var(--text-dim); } + + /* ---------- misc ---------- */ + .ops-link { color: var(--accent-strong); text-decoration: none; } + .ops-link:hover { text-decoration: underline; } + .ops-selected-input { + outline: 1.5px solid var(--accent); + border-radius: 0.25rem; + } +} + +@keyframes ops-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +/* 拖拽落点提示 */ +.ops-drop-target { + outline: 1.5px dashed var(--accent); + outline-offset: -2px; +} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts new file mode 100644 index 0000000..6250766 --- /dev/null +++ b/apps/web/tailwind.config.ts @@ -0,0 +1,15 @@ +import type { Config } from "tailwindcss"; + +export default { + content: [ + "./index.html", + "./src/**/*.{ts,tsx}", + "../../packages/ui/src/**/*.{ts,tsx}", + ], + theme: { + extend: { + fontFamily: { mono: ["JetBrains Mono", "SFMono-Regular", "Menlo", "monospace"] }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json new file mode 100644 index 0000000..7a3d8ce --- /dev/null +++ b/apps/web/tsconfig.app.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "noEmit": true, + "allowImportingTsExtensions": true, + "types": [ + "vite/client" + ] + }, + "include": [ + "src", + "../../packages/ui/src", + "../../packages/core/src", + "../../packages/transport/src" + ] +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..ea9d0cd --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json new file mode 100644 index 0000000..d8ff2ca --- /dev/null +++ b/apps/web/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "noEmit": true, + "composite": true, + "types": [ + "node" + ] + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..ab70e35 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,41 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "node:path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@opsxapi/core": path.resolve(__dirname, "../../packages/core/src/index.ts"), + "@opsxapi/transport": path.resolve(__dirname, "../../packages/transport/src/index.ts"), + "@opsxapi/ui": path.resolve(__dirname, "../../packages/ui/src/index.ts"), + }, + }, + server: { + host: "127.0.0.1", + port: 5173, + strictPort: true, + proxy: { + "/v1": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + }, + "/health": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + }, + "/docs": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + }, + "/home": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + }, + "/site": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + }, + }, + }, +}); diff --git a/docs/refs/.keep b/docs/refs/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..419fb2e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3714 @@ +{ + "name": "opsxapi", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opsxapi", + "workspaces": [ + "apps/web", + "packages/*" + ], + "dependencies": { + "js-yaml": "^5.4.2", + "mqtt": "^5.16.0", + "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9" + }, + "engines": { + "node": ">=20" + } + }, + "apps/web": { + "name": "@opsxapi/web", + "version": "0.1.0", + "dependencies": { + "@opsxapi/core": "*", + "@opsxapi/transport": "*", + "@opsxapi/ui": "*", + "@tauri-apps/api": "^2.5.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.20.3", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", + "vite": "^6.2.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.3.0.tgz", + "integrity": "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opsxapi/core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@opsxapi/transport": { + "resolved": "packages/transport", + "link": true + }, + "node_modules/@opsxapi/ui": { + "resolved": "packages/ui", + "link": true + }, + "node_modules/@opsxapi/web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz", + "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz", + "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz", + "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz", + "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz", + "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz", + "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz", + "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz", + "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz", + "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz", + "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz", + "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz", + "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz", + "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz", + "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz", + "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz", + "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz", + "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz", + "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz", + "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz", + "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz", + "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz", + "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz", + "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz", + "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz", + "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz", + "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", + "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "node_modules/browserslist": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", + "dev": true + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqtt": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.16.0.tgz", + "integrity": "sha512-G0VsEtuJz5sJUgZJaaADMVfW7SIFPzkIuHR+d6rk+tBeSwdCfd9BaIRycRfd2AbyFDEmLlFM+SDQeti/+IbOnw==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz", + "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.3", + "@rollup/rollup-android-arm64": "4.63.3", + "@rollup/rollup-darwin-arm64": "4.63.3", + "@rollup/rollup-darwin-x64": "4.63.3", + "@rollup/rollup-freebsd-arm64": "4.63.3", + "@rollup/rollup-freebsd-x64": "4.63.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", + "@rollup/rollup-linux-arm-musleabihf": "4.63.3", + "@rollup/rollup-linux-arm64-gnu": "4.63.3", + "@rollup/rollup-linux-arm64-musl": "4.63.3", + "@rollup/rollup-linux-loong64-gnu": "4.63.3", + "@rollup/rollup-linux-loong64-musl": "4.63.3", + "@rollup/rollup-linux-ppc64-gnu": "4.63.3", + "@rollup/rollup-linux-ppc64-musl": "4.63.3", + "@rollup/rollup-linux-riscv64-gnu": "4.63.3", + "@rollup/rollup-linux-riscv64-musl": "4.63.3", + "@rollup/rollup-linux-s390x-gnu": "4.63.3", + "@rollup/rollup-linux-x64-gnu": "4.63.3", + "@rollup/rollup-linux-x64-musl": "4.63.3", + "@rollup/rollup-openbsd-x64": "4.63.3", + "@rollup/rollup-openharmony-arm64": "4.63.3", + "@rollup/rollup-win32-arm64-msvc": "4.63.3", + "@rollup/rollup-win32-ia32-msvc": "4.63.3", + "@rollup/rollup-win32-x64-gnu": "4.63.3", + "@rollup/rollup-win32-x64-msvc": "4.63.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.34", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.34.tgz", + "integrity": "sha512-WXL+Dqsm0G6dnC66rQsvM3tPT2adhbqSirWUCZGglkALOr8ocS0KlpU0iuKbjflJOlu3ydZikMEVrYnHOM9suw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/zustand": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "packages/core": { + "name": "@opsxapi/core", + "version": "0.1.0", + "dependencies": { + "js-yaml": "^4.1.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "typescript": "^5.8.2", + "vitest": "^3.0.9" + } + }, + "packages/core/node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "packages/transport": { + "name": "@opsxapi/transport", + "version": "0.1.0", + "dependencies": { + "@opsxapi/core": "*", + "mqtt": "^5.10.1", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "typescript": "^5.8.2" + }, + "peerDependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "peerDependenciesMeta": { + "@tauri-apps/api": { + "optional": true + } + } + }, + "packages/ui": { + "name": "@opsxapi/ui", + "version": "0.1.0", + "dependencies": { + "@opsxapi/core": "*", + "@opsxapi/transport": "*", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "typescript": "^5.8.2" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e236b78 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "opsxapi", + "private": true, + "workspaces": [ + "apps/web", + "packages/*" + ], + "scripts": { + "dev:web": "npm run dev -w @opsxapi/web", + "build:web": "npm run build -w @opsxapi/web", + "test": "npm run test:core", + "test:core": "npm run test -w @opsxapi/core", + "dev:desktop": "npm --prefix apps/desktop run tauri -- dev", + "build:desktop": "npm --prefix apps/desktop run tauri -- build" + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "js-yaml": "^5.4.2", + "mqtt": "^5.16.0", + "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9" + } +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..daa55e2 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,23 @@ +{ + "name": "@opsxapi/core", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "js-yaml": "^4.1.1" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "typescript": "^5.8.2", + "vitest": "^3.0.9" + } +} diff --git a/packages/core/src/exporters/codegen.ts b/packages/core/src/exporters/codegen.ts new file mode 100644 index 0000000..9447434 --- /dev/null +++ b/packages/core/src/exporters/codegen.ts @@ -0,0 +1,184 @@ +/** + * 代码生成:JavaScript fetch / Python requests / Go net/http。 + * 变量保留 {{var}} 原样(用户可自行替换)。 + */ +import type { GraphqlRequestData, HttpRequestData } from "../model"; +import { authHeader, buildUrlWithQuery, exportCurl } from "./curl"; + +export type CodeTarget = "curl" | "fetch" | "python" | "go"; + +export function generateCode( + target: CodeTarget, + data: HttpRequestData | GraphqlRequestData, +): string { + if (data.kind === "graphql") return generateGraphql(target, data); + switch (target) { + case "curl": + return generateCurlHttp(data); + case "fetch": + return generateFetch(data); + case "python": + return generatePython(data); + case "go": + return generateGo(data); + } +} + +/* ---------------- HTTP ---------------- */ + +function commonHeaders(data: HttpRequestData): [string, string][] { + const out: [string, string][] = []; + const auth = authHeader(data); + if (auth) out.push([auth.key, auth.value]); + if (data.auth.mode === "apikey" && data.auth.apiKeyName && data.auth.apiKeyValue && data.auth.apiKeyIn !== "query") { + out.push([data.auth.apiKeyName, data.auth.apiKeyValue]); + } + for (const h of data.headers) { + if (!h.enabled || !h.key) continue; + if (["authorization", "cookie"].includes(h.key.toLowerCase())) continue; + out.push([h.key, h.value]); + } + const cookie = data.cookies.filter((c) => c.enabled && c.key).map((c) => `${c.key}=${c.value}`).join("; "); + if (cookie) out.push(["Cookie", cookie]); + if (data.bodyMode === "json" && !out.some(([k]) => k.toLowerCase() === "content-type")) { + out.push(["Content-Type", "application/json"]); + } + if (data.bodyMode === "urlencoded" && !out.some(([k]) => k.toLowerCase() === "content-type")) { + out.push(["Content-Type", "application/x-www-form-urlencoded"]); + } + return out; +} + +function bodyPayload(data: HttpRequestData): { mode: "raw"; raw: string } | { mode: "urlencoded"; pairs: [string, string][] } | { mode: "form"; pairs: [string, string][] } | null { + if (data.bodyMode === "urlencoded") { + return { mode: "urlencoded", pairs: data.bodyForm.filter((f) => f.enabled && f.key).map((f) => [f.key, f.value]) }; + } + if (data.bodyMode === "form-data") { + return { mode: "form", pairs: data.bodyForm.filter((f) => f.enabled && f.key).map((f) => [f.key, f.value]) }; + } + if (data.bodyMode !== "none" && data.bodyRaw) return { mode: "raw", raw: data.bodyRaw }; + return null; +} + +function generateCurlHttp(data: HttpRequestData): string { + return exportCurl(data); +} + +function generateFetch(data: HttpRequestData): string { + const headers = commonHeaders(data); + const body = bodyPayload(data); + let bodyExpr = ""; + if (body?.mode === "raw") bodyExpr = ` body: ${JSON.stringify(body.raw)},\n`; + else if (body?.mode === "urlencoded") bodyExpr = ` body: new URLSearchParams(${JSON.stringify(Object.fromEntries(body.pairs))}).toString(),\n`; + else if (body?.mode === "form") bodyExpr = ` body: new FormData(${JSON.stringify(Object.fromEntries(body.pairs))} as any),\n`; + + return `const response = await fetch(${JSON.stringify(buildUrlWithQuery(data))}, { + method: ${JSON.stringify(data.method)}, + headers: ${JSON.stringify(Object.fromEntries(headers), null, 2).replace(/\n/g, "\n ")}, +${bodyExpr}}); +if (!response.ok) throw new Error(\`HTTP \${response.status}\`); +const data = await response.json(); +console.log(data); +`; +} + +function generatePython(data: HttpRequestData): string { + const headers = commonHeaders(data); + const body = bodyPayload(data); + let bodyLine = ""; + if (body?.mode === "raw") { + const asJson = data.bodyMode === "json"; + bodyLine = asJson ? `data = json.dumps(${body.raw})` : `data = ${JSON.stringify(body.raw)}`; + } else if (body?.mode === "urlencoded") { + bodyLine = `data = ${JSON.stringify(Object.fromEntries(body.pairs))}`; + } else if (body?.mode === "form") { + bodyLine = `data = ${JSON.stringify(Object.fromEntries(body.pairs))}`; + } + + const needsJson = data.bodyMode === "json"; + return `import requests${needsJson ? "\nimport json" : ""} + +url = ${JSON.stringify(buildUrlWithQuery(data))} +headers = ${JSON.stringify(Object.fromEntries(headers), null, 4).replace(/\n/g, "\n")} +${bodyLine ? bodyLine + "\n" : ""}response = requests.${pyMethod(data.method)}(url, headers=headers${bodyLine ? ", data=data" : ""}) +print(response.status_code) +print(response.text) +`; +} + +function pyMethod(method: string): string { + return method.toLowerCase(); +} + +function generateGo(data: HttpRequestData): string { + const headers = commonHeaders(data); + const body = bodyPayload(data); + const bodyLiteral = body?.mode === "raw" ? body.raw : body?.mode === "urlencoded" || body?.mode === "form" ? new URLSearchParams(body.pairs).toString() : ""; + const payload = bodyLiteral ? JSON.stringify(bodyLiteral) : `""`; + + const headerLines = headers.map(([k, v]) => `\treq.Header.Set(${JSON.stringify(k)}, ${JSON.stringify(v)})`).join("\n"); + + return `package main + +import ( + "fmt" + "io" + "net/http" + "strings" +) + +func main() { + payload := strings.NewReader(${payload}) + req, _ := http.NewRequest(${JSON.stringify(data.method)}, ${JSON.stringify(buildUrlWithQuery(data))}, payload) +${headerLines} + res, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + fmt.Println(res.Status) + fmt.Println(string(body)) +} +`; +} + +/* ---------------- GraphQL ---------------- */ + +function generateGraphql(target: CodeTarget, data: GraphqlRequestData): string { + const gqlBody = JSON.stringify({ query: data.query, variables: safeParse(data.variables) }, null, 2); + const httpLike: HttpRequestData = { + kind: "http", + method: "POST", + url: data.url, + query: [], + headers: [...data.headers.filter((h) => h.enabled && h.key).map((h) => ({ ...h })), { id: "ct", key: "Content-Type", value: "application/json", enabled: true }], + cookies: [], + auth: data.auth, + bodyMode: "json", + bodyRaw: gqlBody, + bodyForm: [], + }; + switch (target) { + case "curl": + return generateCurlHttp(httpLike); + case "fetch": { + const code = generateFetch(httpLike); + return `// GraphQL → POST { query, variables }\n${code}`; + } + case "python": { + const code = generatePython(httpLike); + return `# GraphQL → POST { query, variables }\n${code}`; + } + case "go": + return generateGo(httpLike); + } +} + +function safeParse(s: string): unknown { + try { + return JSON.parse(s); + } catch { + return {}; + } +} diff --git a/packages/core/src/exporters/curl.ts b/packages/core/src/exporters/curl.ts new file mode 100644 index 0000000..1fb7bec --- /dev/null +++ b/packages/core/src/exporters/curl.ts @@ -0,0 +1,76 @@ +/** + * cURL 命令导出(保留 {{var}} 变量原样)。 + */ +import type { HttpRequestData } from "../model"; + +export function shellQuote(s: string): string { + if (/^[A-Za-z0-9_\-:/.@%^=+,?&]+$/.test(s) && !s.includes(" ")) return s; + return `'${s.replaceAll("'", "'\\''")}'`; +} + +export function buildUrlWithQuery(data: HttpRequestData): string { + const enabled = data.query.filter((q) => q.enabled && q.key); + if (!enabled.length) return data.url; + const qs = enabled.map((q) => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join("&"); + return data.url.includes("?") ? `${data.url}&${qs}` : `${data.url}?${qs}`; +} + +export function authHeader(data: HttpRequestData): { key: string; value: string } | null { + if (data.auth.mode === "bearer" && data.auth.bearer) { + return { key: "Authorization", value: `Bearer ${data.auth.bearer}` }; + } + if (data.auth.mode === "basic") { + const raw = `${data.auth.basicUser ?? ""}:${data.auth.basicPass ?? ""}`; + try { + return { key: "Authorization", value: `Basic ${btoa(raw)}` }; + } catch { + return null; + } + } + return null; +} + +export function exportCurl(data: HttpRequestData, name?: string): string { + const lines: string[] = []; + const url = buildUrlWithQuery(data); + const parts = [`curl -X ${data.method} ${shellQuote(url)}`]; + + if (data.auth.mode === "apikey" && data.auth.apiKeyName && data.auth.apiKeyValue) { + if (data.auth.apiKeyIn === "query") { + parts[0] = parts[0].replace(shellQuote(url), shellQuote(`${url}${url.includes("?") ? "&" : "?"}${encodeURIComponent(data.auth.apiKeyName)}=${encodeURIComponent(data.auth.apiKeyValue)}`)); + } else { + parts.push(` -H ${shellQuote(`${data.auth.apiKeyName}: ${data.auth.apiKeyValue}`)}`); + } + } + const auth = authHeader(data); + if (auth) parts.push(` -H ${shellQuote(`${auth.key}: ${auth.value}`)}`); + + for (const h of data.headers) { + if (!h.enabled || !h.key) continue; + if (["authorization", "cookie"].includes(h.key.toLowerCase())) continue; + parts.push(` -H ${shellQuote(`${h.key}: ${h.value}`)}`); + } + + const cookie = data.cookies.filter((c) => c.enabled && c.key).map((c) => `${c.key}=${c.value}`).join("; "); + if (cookie) parts.push(` -b ${shellQuote(cookie)}`); + + const hasJsonHeader = data.headers.some((h) => h.enabled && h.key.toLowerCase() === "content-type" && h.value.includes("json")); + + if (data.bodyMode === "urlencoded") { + if (!hasJsonHeader) parts.push(` -H ${shellQuote("Content-Type: application/x-www-form-urlencoded")}`); + const body = data.bodyForm.filter((b) => b.enabled && b.key).map((b) => `${encodeURIComponent(b.key)}=${encodeURIComponent(b.value)}`).join("&"); + if (body) parts.push(` --data-raw ${shellQuote(body)}`); + } else if (data.bodyMode === "form-data") { + if (!hasJsonHeader) parts.push(` -H ${shellQuote("Content-Type: multipart/form-data")}`); + for (const f of data.bodyForm) { + if (f.enabled && f.key) parts.push(` -F ${shellQuote(`${f.key}=${f.value}`)}`); + } + } else if (data.bodyMode !== "none" && data.bodyRaw) { + if (data.bodyMode === "json" && !hasJsonHeader) parts.push(` -H ${shellQuote("Content-Type: application/json")}`); + if (data.bodyMode === "xml" && !hasJsonHeader) parts.push(` -H ${shellQuote("Content-Type: application/xml")}`); + parts.push(` --data-raw ${shellQuote(data.bodyRaw)}`); + } + + lines.push(parts.join(" \\\n")); + return lines.join("\n"); +} diff --git a/packages/core/src/exporters/index.ts b/packages/core/src/exporters/index.ts new file mode 100644 index 0000000..156b647 --- /dev/null +++ b/packages/core/src/exporters/index.ts @@ -0,0 +1,61 @@ +/** + * 导出入口 + 备份序列化。 + */ +import { dump as yamlDump } from "js-yaml"; +import type { Collection, Environment, WorkspaceData } from "../model"; +import { exportOpenApiWithFolders, type OpenApiExportOptions } from "./openapi"; +import { exportPostman } from "./postman"; +import { exportMarkdown } from "./markdown"; + +export type ExportFormat = "openapi-json" | "openapi-yaml" | "postman" | "markdown" | "single-curl" | "backup"; + +export function exportCollectionAs( + format: ExportFormat, + collection: Collection, + options: OpenApiExportOptions = {}, +): string { + switch (format) { + case "openapi-json": + return JSON.stringify(exportOpenApiWithFolders(collection, options), null, 2); + case "openapi-yaml": + return yamlDump(exportOpenApiWithFolders(collection, options), { lineWidth: 120 }); + case "postman": + return JSON.stringify(exportPostman(collection), null, 2); + case "markdown": + return exportMarkdown(collection); + default: + throw new Error(`格式 ${format} 不适用于集合导出`); + } +} + +export function downloadText(filename: string, text: string, mime = "application/octet-stream"): void { + const blob = new Blob([text], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +export function exportBackup( + collections: Collection[], + environments: Environment[], +): string { + return JSON.stringify( + { + __opsxapi__: "backup", + version: 2, + exportedAt: new Date().toISOString(), + collections, + environments, + }, + null, + 2, + ); +} + +export { exportOpenApiWithFolders, exportPostman, exportMarkdown }; +export type { OpenApiExportOptions } from "./openapi"; diff --git a/packages/core/src/exporters/markdown.ts b/packages/core/src/exporters/markdown.ts new file mode 100644 index 0000000..cb50d6d --- /dev/null +++ b/packages/core/src/exporters/markdown.ts @@ -0,0 +1,95 @@ +/** + * 集合 → Markdown 接口文档。 + */ +import type { Collection, CollectionNode, HttpRequestData } from "../model"; +import { buildUrlWithQuery } from "./curl"; + +export function exportMarkdown(collection: Collection): string { + const lines: string[] = []; + lines.push(`# ${collection.name}`, ""); + if (collection.description) lines.push(collection.description, ""); + lines.push("> 由 OpsxAPI 导出 · " + new Date().toLocaleString("zh-CN"), ""); + + const toc: string[] = []; + const visitToc = (nodes: CollectionNode[], prefix: string) => { + for (const node of nodes) { + if (node.type === "folder") { + toc.push(`${prefix}- 📁 ${node.name}`); + visitToc(node.children, `${prefix} `); + } else { + toc.push(`${prefix}- [${node.name}](#${anchor(node.name)})`); + } + } + }; + visitToc(collection.children, ""); + if (toc.length) lines.push("## 目录", "", ...toc, ""); + + const visit = (nodes: CollectionNode[], depth: number) => { + for (const node of nodes) { + const hashes = "#".repeat(Math.min(depth + 1, 6)); + if (node.type === "folder") { + lines.push(`${hashes} ${node.name}`, ""); + if (node.description) lines.push(node.description, ""); + visit(node.children, depth + 1); + continue; + } + lines.push(`${hashes} ${node.name}`, ""); + if (node.description) lines.push(node.description, ""); + + if (node.kind === "http") { + const d: HttpRequestData = node.data; + lines.push( + "", + `- **Method**:\`${d.method}\``, + `- **URL**:\`${buildUrlWithQuery(d)}\``, + ); + if (d.auth.mode !== "none") { + lines.push(`- **鉴权**:\`${d.auth.mode}\``); + } + if (d.query.filter((q) => q.enabled && q.key).length) { + lines.push("", "**Query 参数**", "", "| 参数 | 示例值 | 说明 |", "| --- | --- | --- |"); + for (const q of d.query) { + if (!q.enabled || !q.key) continue; + lines.push(`| \`${q.key}\` | \`${escapeTable(q.value)}\` | ${escapeTable(q.description ?? "")} |`); + } + } + if (d.headers.filter((h) => h.enabled && h.key).length) { + lines.push("", "**Headers**", "", "| Header | 示例值 | 说明 |", "| --- | --- | --- |"); + for (const h of d.headers) { + if (!h.enabled || !h.key) continue; + lines.push(`| \`${h.key}\` | \`${escapeTable(h.value)}\` | ${escapeTable(h.description ?? "")} |`); + } + } + if (d.bodyMode !== "none") { + const lang = d.bodyMode === "json" ? "json" : d.bodyMode === "xml" ? "xml" : ""; + if (d.bodyMode === "urlencoded" || d.bodyMode === "form-data") { + lines.push("", `**请求体(${d.bodyMode})**`, "", "| 字段 | 示例值 |", "| --- | --- |"); + for (const f of d.bodyForm) { + if (!f.enabled || !f.key) continue; + lines.push(`| \`${f.key}\` | \`${escapeTable(f.value)}\` |`); + } + } else if (d.bodyRaw) { + lines.push("", `**请求体(${d.bodyMode})**`, "", "```" + lang, d.bodyRaw, "```"); + } + } + lines.push(""); + } else { + const d = node.data as { url?: string; brokerUrl?: string }; + const url = "url" in d ? d.url : d.brokerUrl ?? ""; + lines.push("", `- **协议**:\`${node.kind}\``, `- **地址**:\`${url}\``); + lines.push(""); + } + } + }; + visit(collection.children, 1); + + return lines.join("\n"); +} + +function escapeTable(s: string): string { + return s.replaceAll("|", "\\|").replaceAll("\n", " "); +} + +function anchor(name: string): string { + return name.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-"); +} diff --git a/packages/core/src/exporters/openapi.ts b/packages/core/src/exporters/openapi.ts new file mode 100644 index 0000000..ceef46e --- /dev/null +++ b/packages/core/src/exporters/openapi.ts @@ -0,0 +1,265 @@ +/** + * 集合 → OpenAPI 3.0.3 导出。 + */ +import type { Collection, CollectionNode, FolderNode, HttpApiNode, HttpRequestData, RequestNode } from "../model"; +import { walk } from "../tree"; + +type Spec = Record; +type Op = Record; + +export type OpenApiExportOptions = { + title?: string; + version?: string; + yaml?: boolean; +}; + +export function exportOpenApi( + collection: Collection, + options: OpenApiExportOptions = {}, +): Spec { + const requests: HttpApiNode[] = []; + const skipped: RequestNode[] = []; + walk(collection.children, (node) => { + if (node.type !== "request") return; + if (node.kind === "http") requests.push(node); + else skipped.push(node); + }); + + // server 推断:{{baseUrl}} 前缀或首个绝对 URL 的 origin + const serverUrl = inferServerUrl(requests.map((r) => r.data.url)); + + const paths: Spec = {}; + const usedPathParams = new Map>(); + const securities = new Set(); + + for (const req of requests) { + const data = req.data; + const path = toOpenApiPath(stripServer(data.url, serverUrl)); + const paramsInPath = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]); + const method = data.method.toLowerCase(); + if (method === "head" || method === "options") continue; // OpenAPI 常规导出不含这两个 + + const op: Op = { + summary: req.name, + operationId: slugify(req.name), + }; + if (req.description) op.description = req.description; + + const parameters: Spec[] = []; + for (const p of paramsInPath) { + if (!usedPathParams.has(path)) usedPathParams.set(path, new Set()); + usedPathParams.get(path)!.add(p); + parameters.push({ name: p, in: "path", required: true, schema: { type: "string" } }); + } + for (const q of data.query) { + if (!q.enabled || !q.key) continue; + parameters.push({ + name: q.key, + in: "query", + required: false, + schema: { type: "string", example: q.value }, + ...(q.description ? { description: q.description } : {}), + }); + } + for (const h of data.headers) { + if (!h.enabled || !h.key) continue; + if (["authorization", "content-type", "cookie"].includes(h.key.toLowerCase())) continue; + parameters.push({ + name: h.key, + in: "header", + required: false, + schema: { type: "string", example: h.value }, + }); + } + if (parameters.length) op.parameters = parameters; + + // body + if (data.bodyMode !== "none" && method !== "get" && method !== "head") { + op.requestBody = buildRequestBody(data); + } + + // security + if (data.auth.mode === "bearer" && data.auth.bearer) { + securities.add("bearerAuth"); + op.security = [{ bearerAuth: [] }]; + } else if (data.auth.mode === "basic") { + securities.add("basicAuth"); + op.security = [{ basicAuth: [] }]; + } else if (data.auth.mode === "apikey" && data.auth.apiKeyName) { + securities.add("apiKeyAuth"); + op.security = [{ apiKeyAuth: [] }]; + } + + if (!paths[path]) paths[path] = {}; + (paths[path] as Spec)[method] = op; + } + + const components: Spec = {}; + const schemes: Spec = {}; + if (securities.has("bearerAuth")) schemes.bearerAuth = { type: "http", scheme: "bearer", bearerFormat: "JWT" }; + if (securities.has("basicAuth")) schemes.basicAuth = { type: "http", scheme: "basic" }; + if (securities.has("apiKeyAuth")) + schemes.apiKeyAuth = { + type: "apiKey", + name: findApiKeyHeader(requests) ?? "X-API-Key", + in: "header", + }; + if (Object.keys(schemes).length) components.securitySchemes = schemes; + + const spec: Spec = { + openapi: "3.0.3", + info: { + title: options.title ?? collection.name, + version: options.version ?? "1.0.0", + ...(collection.description ? { description: collection.description } : {}), + }, + servers: [{ url: serverUrl }], + paths, + ...(Object.keys(components).length ? { components } : {}), + }; + + if (skipped.length) { + (spec.info as Spec).description = `${str((spec.info as Spec).description)}\n\n> 注:${skipped.length} 个非 HTTP 节点(GraphQL/WS/SSE/Socket.IO/MQTT)不参与 OpenAPI 导出。`.trim(); + } + + return spec; +} + +function str(v: unknown): string { + return typeof v === "string" ? v : ""; +} + +function inferServerUrl(urls: string[]): string { + for (const u of urls) { + if (u.includes("{{baseUrl}}")) return "http://localhost"; + } + for (const u of urls) { + try { + return new URL(u).origin; + } catch { + /* next */ + } + } + return "http://localhost"; +} + +function stripServer(url: string, serverUrl: string): string { + let u = url; + if (u.includes("{{baseUrl}}")) { + u = u.split("{{baseUrl}}")[1] || "/"; + } else if (serverUrl && u.startsWith(serverUrl)) { + u = u.slice(serverUrl.length) || "/"; + } + if (!u.startsWith("/")) u = `/${u}`; + return u.split("?")[0]; +} + +function toOpenApiPath(path: string): string { + // {{var}} → {var} + return path.replace(/\{\{([^}]+)\}\}/g, "{$1}"); +} + +function buildRequestBody(data: HttpRequestData): Spec { + if (data.bodyMode === "urlencoded") { + const props: Spec = {}; + for (const f of data.bodyForm) { + if (f.key) props[f.key] = { type: "string", example: f.value }; + } + return { + required: true, + content: { + "application/x-www-form-urlencoded": { + schema: { type: "object", properties: props }, + }, + }, + }; + } + if (data.bodyMode === "form-data") { + const props: Spec = {}; + for (const f of data.bodyForm) { + if (f.key) props[f.key] = { type: "string", example: f.value }; + } + return { + required: true, + content: { + "multipart/form-data": { + schema: { type: "object", properties: props }, + }, + }, + }; + } + const mime = + data.bodyMode === "json" + ? "application/json" + : data.bodyMode === "xml" + ? "application/xml" + : data.bodyMode === "html" + ? "text/html" + : "text/plain"; + let example: unknown = data.bodyRaw; + if (data.bodyMode === "json") { + try { + example = JSON.parse(data.bodyRaw); + } catch { + /* keep raw */ + } + } + return { + required: true, + content: { [mime]: { example } }, + }; +} + +function findApiKeyHeader(requests: HttpApiNode[]): string | null { + for (const r of requests) { + if (r.data.auth.mode === "apikey" && r.data.auth.apiKeyName) return r.data.auth.apiKeyName; + } + return null; +} + +function slugify(name: string): string { + const cleaned = name + .replace(/[^\p{L}\p{N}\s-]/gu, "") + .trim() + .replace(/\s+/g, "_"); + return cleaned || "operation"; +} + +/** 导出包含 tag 分组(目录名 = tag)的 spec */ +export function exportOpenApiWithFolders( + collection: Collection, + options: OpenApiExportOptions = {}, +): Spec { + const spec = exportOpenApi(collection, options); + // 目录名 → tag:重新走一遍树,为每个请求打 tag + const paths = spec.paths as Spec; + const tags: { name: string }[] = []; + const tagByPath = new Map(); + + const visit = (nodes: CollectionNode[], tag: string | null) => { + for (const node of nodes) { + if (node.type === "folder") { + visit(node.children, node.name); + } else if (node.kind === "http" && tag) { + const path = toOpenApiPath(stripServer(node.data.url, "http://localhost")); + tagByPath.set(`${path}:${node.data.method.toLowerCase()}`, tag); + } + } + }; + visit(collection.children, null); + for (const t of tagByPath.values()) { + if (!tags.some((x) => x.name === t)) tags.push({ name: t }); + } + if (tags.length) { + spec.tags = tags; + for (const [path, methods] of Object.entries(paths)) { + for (const [method, opRaw] of Object.entries(methods as Spec)) { + const tag = tagByPath.get(`${path}:${method}`); + if (tag) (opRaw as Op).tags = [tag]; + } + } + } + return spec; +} + +export type { FolderNode }; diff --git a/packages/core/src/exporters/postman.ts b/packages/core/src/exporters/postman.ts new file mode 100644 index 0000000..eab4b45 --- /dev/null +++ b/packages/core/src/exporters/postman.ts @@ -0,0 +1,146 @@ +/** + * 集合 → Postman Collection v2.1 导出。 + */ +import type { Collection, CollectionNode, GraphqlRequestData, HttpRequestData, RequestNode } from "../model"; + +type Obj = Record; + +export function exportPostman(collection: Collection): Obj { + return { + info: { + name: collection.name, + _postman_id: `opsxapi-${collection.id}`, + description: collection.description || undefined, + schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + }, + item: convertNodes(collection.children), + }; +} + +function convertNodes(nodes: CollectionNode[]): Obj[] { + const out: Obj[] = []; + for (const node of nodes) { + if (node.type === "folder") { + out.push({ + name: node.name, + description: node.description || undefined, + item: convertNodes(node.children), + }); + } else if (node.kind === "http" || node.kind === "graphql") { + out.push(convertRequest(node)); + } + } + return out; +} + +function convertRequest(node: RequestNode): Obj { + const data = node.data as HttpRequestData | GraphqlRequestData; + + const header = data.headers + .filter((h) => h.enabled && h.key) + .map((h) => ({ key: h.key, value: h.value, type: "text" })); + + const authObj = postmanAuth(data.auth); + + if (data.kind === "graphql") { + return { + name: node.name, + request: { + method: "POST", + header: [{ key: "Content-Type", value: "application/json", type: "text" }, ...header], + body: { + mode: "graphql", + graphql: { query: data.query, variables: data.variables }, + }, + url: { raw: data.url, host: splitHost(data.url) }, + ...(authObj ? { auth: authObj } : {}), + ...(node.description ? { description: node.description } : {}), + }, + }; + } + + const query = data.query + .filter((q) => q.enabled && q.key) + .map((q) => ({ key: q.key, value: q.value, disabled: false })); + const raw = buildRaw(data); + + let body: Obj | undefined; + if (data.bodyMode === "json" || data.bodyMode === "xml" || data.bodyMode === "text" || data.bodyMode === "html") { + body = { + mode: "raw", + raw: data.bodyRaw, + options: { raw: { language: data.bodyMode === "json" ? "json" : data.bodyMode === "xml" ? "xml" : "text" } }, + }; + } else if (data.bodyMode === "urlencoded") { + body = { + mode: "urlencoded", + urlencoded: data.bodyForm.filter((f) => f.key).map((f) => ({ key: f.key, value: f.value, disabled: !f.enabled })), + }; + } else if (data.bodyMode === "form-data") { + body = { + mode: "formdata", + formdata: data.bodyForm.filter((f) => f.key).map((f) => ({ key: f.key, value: f.value, type: "text", disabled: !f.enabled })), + }; + } + + const cookie = data.cookies.filter((c) => c.enabled && c.key).map((c) => `${c.key}=${c.value}`).join("; "); + + return { + name: node.name, + request: { + method: data.method, + header: header.length ? header : undefined, + body, + ...(cookie ? { header: [...(header ?? []), { key: "Cookie", value: cookie, type: "text" }] } : {}), + url: { + raw, + host: splitHost(raw), + query: query.length ? query : undefined, + }, + ...(authObj ? { auth: authObj } : {}), + ...(node.description ? { description: node.description } : {}), + }, + }; +} + +function buildRaw(data: HttpRequestData): string { + const enabled = data.query.filter((q) => q.enabled && q.key); + if (!enabled.length) return data.url; + const qs = enabled.map((q) => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join("&"); + return data.url.includes("?") ? `${data.url}&${qs}` : `${data.url}?${qs}`; +} + +function splitHost(url: string): string[] { + try { + const u = new URL(url.includes("://") ? url : `http://${url}`); + return [u.host]; + } catch { + return [url]; + } +} + +function postmanAuth(auth: HttpRequestData["auth"]): Obj | undefined { + if (auth.mode === "bearer" && auth.bearer) { + return { type: "bearer", bearer: [{ key: "token", value: auth.bearer, type: "string" }] }; + } + if (auth.mode === "basic") { + return { + type: "basic", + basic: [ + { key: "username", value: auth.basicUser ?? "", type: "string" }, + { key: "password", value: auth.basicPass ?? "", type: "string" }, + ], + }; + } + if (auth.mode === "apikey" && auth.apiKeyName) { + return { + type: "apikey", + apikey: [ + { key: "key", value: auth.apiKeyName, type: "string" }, + { key: "value", value: auth.apiKeyValue ?? "", type: "string" }, + { key: "in", value: auth.apiKeyIn ?? "header", type: "string" }, + ], + }; + } + return undefined; +} diff --git a/packages/core/src/factory.ts b/packages/core/src/factory.ts new file mode 100644 index 0000000..fdf81b8 --- /dev/null +++ b/packages/core/src/factory.ts @@ -0,0 +1,267 @@ +/** + * 节点 / 集合 / 环境的默认工厂。 + */ +import type { + AuthConfig, + Collection, + CollectionNode, + Environment, + FolderNode, + GraphqlRequestData, + HttpRequestData, + KVRow, + MqttRequestData, + RequestKind, + RequestNode, + RequestDataOf, + SocketIoRequestData, + SseRequestData, + WebsocketRequestData, + WorkspaceData, +} from "./model"; +import { newId } from "./tree"; + +export function kv( + key = "", + value = "", + enabled = true, + description?: string, +): KVRow { + return { id: newId(), key, value, enabled, description }; +} + +export const noAuth: AuthConfig = { mode: "none" }; + +export function defaultRequestData(kind: RequestKind) { + switch (kind) { + case "http": + return { + kind, + method: "GET", + url: "{{baseUrl}}/", + query: [], + headers: [], + cookies: [], + auth: { ...noAuth }, + bodyMode: "none", + bodyRaw: "", + bodyForm: [], + } satisfies HttpRequestData; + case "graphql": + return { + kind, + url: "{{baseUrl}}/graphql", + headers: [kv("Content-Type", "application/json")], + auth: { ...noAuth }, + query: "query {\n \n}", + variables: "{}", + } satisfies GraphqlRequestData; + case "websocket": + return { + kind, + url: "wss://echo.websocket.events", + headers: [], + protocols: "", + } satisfies WebsocketRequestData; + case "sse": + return { + kind, + url: "{{baseUrl}}/events", + headers: [kv("Accept", "text/event-stream")], + query: [], + auth: { ...noAuth }, + lastEventId: "", + dataOnly: true, + } satisfies SseRequestData; + case "socketio": + return { + kind, + url: "http://127.0.0.1:3000", + options: '{\n "path": "/socket.io",\n "transports": ["websocket"]\n}', + listenEvents: ["message"], + sendEvent: "message", + sendArgs: '[\n "hello"\n]', + } satisfies SocketIoRequestData; + case "mqtt": + return { + kind, + brokerUrl: "wss://broker.emqx.io:8084/mqtt", + username: "", + password: "", + clientId: `opsxapi-${Math.random().toString(16).slice(2, 10)}`, + subscribeTopics: "opsxapi/demo", + publishTopic: "opsxapi/demo", + publishPayload: '{\n "hello": "opsxapi"\n}', + qos: 0, + } satisfies MqttRequestData; + } +} + +export function createRequest( + kind: K, + name?: string, + partial?: { data?: Partial>; description?: string }, +): Extract { + const data = { + ...defaultRequestData(kind), + ...(partial?.data ?? {}), + kind, + } as RequestDataOf; + const base = { + id: newId(), + type: "request" as const, + name: name ?? defaultName(kind), + description: partial?.description, + createdAt: Date.now(), + updatedAt: Date.now(), + }; + return { ...base, kind, data } as unknown as Extract; +} + +export function defaultName(kind: RequestKind): string { + switch (kind) { + case "http": + return "新建 HTTP 接口"; + case "graphql": + return "新建 GraphQL 查询"; + case "websocket": + return "新建 WebSocket"; + case "sse": + return "新建 SSE"; + case "socketio": + return "新建 Socket.IO"; + case "mqtt": + return "新建 MQTT"; + } +} + +export function createFolder(name = "新建目录", children: CollectionNode[] = []): FolderNode { + return { + id: newId(), + type: "folder", + name, + children, + createdAt: Date.now(), + updatedAt: Date.now(), + }; +} + +export function createCollection( + name: string, + children: CollectionNode[] = [], +): Collection { + return { + id: newId(), + type: "collection", + name, + children, + createdAt: Date.now(), + updatedAt: Date.now(), + }; +} + +export function createEnvironment( + name: string, + vars: { key: string; value: string }[] = [], +): Environment { + return { id: newId(), name, variables: vars.map((v) => kv(v.key, v.value)) }; +} + +/** 首次启动的演示集合:覆盖全部协议,开箱即用 */ +export function createSeedWorkspace(): WorkspaceData { + const http = ( + name: string, + method: HttpRequestData["method"], + url: string, + extra?: Partial, + description?: string, + ): RequestNode => + createRequest("http", name, { + data: { method, url, ...extra }, + description, + }); + + const demo = createCollection("示例集合", [ + createFolder("HTTP · httpbin", [ + http("获取请求回显", "GET", "https://httpbin.org/get", undefined, "经典回显接口,返回请求头 / IP / 参数。"), + http( + "提交 JSON", + "POST", + "https://httpbin.org/post", + { + bodyMode: "json", + bodyRaw: '{\n "name": "opsxapi",\n "lang": "zh-CN"\n}', + headers: [kv("Content-Type", "application/json")], + }, + "POST 一个 JSON 体,观察 Pretty 视图。", + ), + http( + "表单提交", + "POST", + "https://httpbin.org/post", + { + bodyMode: "urlencoded", + bodyForm: [kv("username", "logan"), kv("role", "ops")], + }, + ), + http( + "Bearer 鉴权示例", + "GET", + "https://httpbin.org/bearer", + { + auth: { mode: "bearer", bearer: "{{token}}" }, + }, + "在 Auth 标签填 Token;环境变量 token 默认 demo-token。", + ), + http("状态码 · 404", "GET", "https://httpbin.org/status/404"), + ]), + createFolder("GraphQL", [ + createRequest("graphql", "国家查询 · countries", { + data: { + url: "https://countries.trevorblades.com/graphql", + query: + "query ($code: ID!) {\n country(code: $code) {\n name\n capital\n currency\n }\n}", + variables: '{\n "code": "CN"\n}', + }, + description: "公共 GraphQL 服务,无需鉴权。", + }), + ]), + createFolder("实时协议", [ + createRequest("websocket", "WebSocket · echo", { + data: { url: "wss://echo.websocket.events" }, + description: "发送什么就回什么。", + }), + createRequest("sse", "SSE · 维基媒体变更流", { + data: { + url: "https://stream.wikimedia.org/v2/stream/recentchange", + headers: [kv("Accept", "text/event-stream")], + }, + description: "维基百科实时编辑事件流。", + }), + createRequest("mqtt", "MQTT · EMQX 公共 Broker", { + data: { + subscribeTopics: "opsxapi/demo", + publishTopic: "opsxapi/demo", + }, + description: "wss 公共 broker;两个窗口同时打开可互相收发。", + }), + createRequest("socketio", "Socket.IO · 本地服务", { + description: "连你的本地 socket.io 服务,或改 URL 到任意 demo 站点。", + }), + ]), + ]); + + const envs = [ + createEnvironment("正式", [{ key: "baseUrl", value: "https://httpbin.org" }, { key: "token", value: "demo-token" }]), + createEnvironment("本地", [{ key: "baseUrl", value: "http://127.0.0.1:8787" }, { key: "token", value: "dev-token" }]), + ]; + + return { + version: 2, + collections: [demo], + environments: envs, + activeEnvironmentId: envs[0]?.id ?? null, + history: [], + settings: { theme: "dark", timeoutMs: 30000 }, + }; +} diff --git a/packages/core/src/importers.test.ts b/packages/core/src/importers.test.ts new file mode 100644 index 0000000..8a3b601 --- /dev/null +++ b/packages/core/src/importers.test.ts @@ -0,0 +1,451 @@ +import { describe, expect, it } from "vitest"; +import { + detectImportFormat, + importAuto, + importCurl, + importOpenApi, + parseCurl, +} from "./importers"; +import { importPostman } from "./importers/postman"; +import { importHar } from "./importers/har"; +import { importInsomnia } from "./importers/insomnia"; +import { importHoppscotch } from "./importers/hoppscotch"; +import { importApipost } from "./importers/apipost"; +import { importMarkdown } from "./importers/markdown"; +import { findNode, insertIntoCollection, moveNode, removeNode } from "./tree"; +import { createSeedWorkspace } from "./factory"; +import { exportCollectionAs, exportBackup } from "./exporters"; +import { exportCurl } from "./exporters/curl"; + +const openapi3 = JSON.stringify({ + openapi: "3.0.3", + info: { title: "Petstore", version: "1.0.0" }, + servers: [{ url: "https://petstore.example.com/v1" }], + tags: [{ name: "pets" }], + paths: { + "/pets": { + get: { + tags: ["pets"], + summary: "列出宠物", + operationId: "listPets", + parameters: [ + { name: "limit", in: "query", schema: { type: "integer" }, example: 10 }, + { name: "X-Trace", in: "header", schema: { type: "string" }, example: "abc" }, + ], + responses: {}, + }, + post: { + tags: ["pets"], + summary: "新增宠物", + operationId: "createPet", + security: [{ petstoreAuth: [] }], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { name: { type: "string" }, age: { type: "integer" } }, + }, + }, + }, + }, + responses: {}, + }, + }, + "/pets/{petId}": { + get: { + summary: "查询单个宠物", + parameters: [{ name: "petId", in: "path", required: true, schema: { type: "string" }, example: "42" }], + responses: {}, + }, + }, + }, + components: { + securitySchemes: { petstoreAuth: { type: "http", scheme: "bearer" } }, + }, +}); + +const openapi2 = ` +swagger: "2.0" +info: + title: Old API + version: "1.0" +host: old.example.com +basePath: /api +schemes: [https] +paths: + /users: + get: + summary: 用户列表 + parameters: + - name: page + in: query + type: integer + example: 1 + responses: + 200: + description: ok + post: + summary: 创建用户 + consumes: [application/json] + parameters: + - name: body + in: body + schema: + type: object + properties: + username: + type: string + responses: + 201: + description: created +`; + +const postmanV21 = JSON.stringify({ + info: { name: "Demo", schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, + item: [ + { + name: "目录A", + item: [ + { + name: "获取用户", + request: { + method: "GET", + url: { raw: "https://api.example.com/users?page=2", query: [{ key: "page", value: "2" }] }, + header: [{ key: "Accept", value: "application/json" }], + }, + }, + { + name: "登录", + request: { + method: "POST", + url: { raw: "{{baseUrl}}/login" }, + header: [{ key: "Content-Type", value: "application/json" }], + body: { mode: "raw", raw: "{\n \"user\": \"a\",\n \"pass\": \"b\"\n}", options: { raw: { language: "json" } } }, + auth: { type: "bearer", bearer: [{ key: "token", value: "xyz" }] }, + }, + }, + ], + }, + ], + variable: [{ key: "baseUrl", value: "https://api.example.com" }], +}); + +const harDoc = JSON.stringify({ + log: { + entries: [ + { + request: { + method: "GET", + url: "https://api.example.com/items?size=5", + headers: [{ name: "Accept", value: "application/json" }], + queryString: [{ name: "size", value: "5" }], + }, + }, + { + request: { + method: "POST", + url: "https://api.example.com/items", + headers: [{ name: "Content-Type", value: "application/json" }], + postData: { mimeType: "application/json", text: '{"a":1}' }, + }, + }, + ], + }, +}); + +const insomniaDoc = JSON.stringify({ + _type: "export", + __export_format: 4, + resources: [ + { _id: "wrk_1", _type: "workspace", name: "我的服务" }, + { _id: "fld_1", _type: "request_group", parentId: "wrk_1", name: "用户" }, + { + _id: "req_1", + _type: "request", + parentId: "fld_1", + name: "资料", + method: "GET", + url: "https://api.example.com/me", + headers: [{ name: "Authorization", value: "Bearer t" }], + parameters: [], + authentication: { type: "bearer", token: "t" }, + body: {}, + }, + ], +}); + +const hoppscotchDoc = JSON.stringify([ + { + name: "SC", + folders: [{ name: "子", requests: [] }], + requests: [ + { + name: "列表", + method: "GET", + url: "https://api.example.com/list", + headers: [{ key: "Accept", value: "application/json", active: true }], + params: [], + bodyMode: "none", + }, + ], + }, +]); + +const apipostDoc = JSON.stringify({ + name: "Apipost 项目", + api_collection: [ + { target_id: "f1", parent_id: "0", name: "订单", children: [] }, + { + target_id: "r1", + parent_id: "f1", + name: "下单", + method: "POST", + url: "https://api.example.com/orders", + request: { + headers: { "Content-Type": "application/json" }, + body: { mode: "json", raw: '{"sku":1}' }, + }, + }, + ], +}); + +const curlCmd = `curl -X POST 'https://api.example.com/v1/users?dry=1' \\ + -H 'Content-Type: application/json' \\ + -H 'Authorization: Bearer tok' \\ + -d '{"name":"logan","tags":["a","b"]}'`; + +describe("格式识别", () => { + it("识别各格式", () => { + expect(detectImportFormat(openapi3)).toBe("openapi"); + expect(detectImportFormat(openapi2)).toBe("openapi"); + expect(detectImportFormat(postmanV21)).toBe("postman"); + expect(detectImportFormat(harDoc)).toBe("har"); + expect(detectImportFormat(insomniaDoc)).toBe("insomnia"); + expect(detectImportFormat(hoppscotchDoc)).toBe("hoppscotch"); + expect(detectImportFormat(apipostDoc)).toBe("apipost"); + expect(detectImportFormat(curlCmd)).toBe("curl"); + expect(detectImportFormat("# 文档\n\n```curl\ncurl http://a.com\n```")).toBe("markdown"); + }); +}); + +describe("cURL", () => { + it("解析方法/URL/头/体", () => { + const parsed = parseCurl(curlCmd); + expect(parsed.method).toBe("POST"); + expect(parsed.url).toBe("https://api.example.com/v1/users"); + expect(parsed.query).toEqual([{ key: "dry", value: "1", enabled: true }]); + expect(parsed.bodyMode).toBe("json"); + expect(JSON.parse(parsed.bodyRaw)).toEqual({ name: "logan", tags: ["a", "b"] }); + }); + + it("表单与 basic auth", () => { + const parsed = parseCurl("curl -u tom:pw -F file=@x.txt -F name=x https://a.io/up"); + expect(parsed.auth.mode).toBe("basic"); + expect(parsed.auth.basicUser).toBe("tom"); + expect(parsed.bodyMode).toBe("form-data"); + expect(parsed.bodyForm.some((f) => f.key === "name" && f.value === "x")).toBe(true); + }); + + it("urlencoded 推断", () => { + const parsed = parseCurl(`curl -d 'a=1&b=2' http://x.io/f`); + expect(parsed.method).toBe("POST"); + expect(parsed.bodyMode).toBe("urlencoded"); + expect(parsed.bodyForm.length).toBe(2); + }); + + it("导入为集合", () => { + const c = importCurl(curlCmd); + expect(c.children.length).toBe(1); + expect(c.children[0].type).toBe("request"); + }); +}); + +describe("OpenAPI", () => { + it("导入 v3(tag 目录、query/header、bearer、path 参数)", () => { + const result = importOpenApi(openapi3); + const c = result.collections[0]; + expect(c.name).toBe("Petstore"); + expect(c.children.length).toBe(2); // pets 目录 + 根级 /pets/{petId} + const folder = c.children[0]; + expect(folder.type).toBe("folder"); + if (folder.type !== "folder") return; + const list = folder.children[0]; + if (list.type !== "request" || list.kind !== "http") return; + expect(list.data.method).toBe("GET"); + expect(list.data.url).toBe("{{baseUrl}}/pets"); + expect(result.environmentVariables.find((v) => v.key === "baseUrl")?.value).toBe( + "https://petstore.example.com/v1", + ); + expect(list.data.query[0].key).toBe("limit"); + expect(list.data.query[0].value).toBe("10"); + expect(list.data.headers.some((h) => h.key === "X-Trace")).toBe(true); + + const single = c.children[1]; + if (single.type !== "request" || single.kind !== "http") return; + expect(single.data.url).toContain("{{petId}}"); + expect(result.environmentVariables.find((v) => v.key === "petId")?.value).toBe("42"); + }); + + it("导入 v2 YAML(basePath、body schema)", () => { + const result = importOpenApi(openapi2); + const c = result.collections[0]; + expect(result.environmentVariables.find((v) => v.key === "baseUrl")?.value).toBe("https://old.example.com/api"); + const post = c.children.find((n) => n.type === "request" && n.kind === "http" && n.data.method === "POST"); + expect(post).toBeTruthy(); + if (post && post.type === "request" && post.kind === "http") { + expect(post.data.bodyMode).toBe("json"); + expect(post.data.bodyRaw).toContain("username"); + } + }); +}); + +describe("Postman", () => { + it("导入 v2.1(目录/鉴权/变量)", () => { + const result = importPostman(postmanV21); + const c = result.collections[0]; + expect(c.name).toBe("Demo"); + const folder = c.children[0]; + if (folder.type !== "folder") throw new Error("应当是目录"); + const login = folder.children[1]; + if (login.type !== "request" || login.kind !== "http") throw new Error("应当是请求"); + expect(login.data.auth.mode).toBe("bearer"); + expect(login.data.bodyMode).toBe("json"); + expect(result.environmentVariables.find((v) => v.key === "baseUrl")?.value).toBe("https://api.example.com"); + const getUser = folder.children[0]; + if (getUser.type !== "request" || getUser.kind !== "http") throw new Error("应当是请求"); + expect(getUser.data.query[0].key).toBe("page"); + }); +}); + +describe("HAR", () => { + it("按域名分组导入", () => { + const result = importHar(harDoc); + const c = result.collections[0]; + expect(c.children.length).toBe(1); + const hostFolder = c.children[0]; + if (hostFolder.type !== "folder") throw new Error("应当是目录"); + expect(hostFolder.name).toBe("api.example.com"); + expect(hostFolder.children.length).toBe(2); + }); +}); + +describe("Insomnia", () => { + it("导入 workspace/group/request 层级", () => { + const result = importInsomnia(insomniaDoc); + const c = result.collections[0]; + expect(c.name).toBe("我的服务"); + const folder = c.children[0]; + if (folder.type !== "folder") throw new Error("应当是目录"); + expect(folder.name).toBe("用户"); + const req = folder.children[0]; + if (req.type !== "request" || req.kind !== "http") throw new Error("应当是请求"); + expect(req.data.url).toBe("https://api.example.com/me"); + expect(req.data.auth.mode).toBe("bearer"); + }); +}); + +describe("Hoppscotch", () => { + it("导入集合数组", () => { + const result = importHoppscotch(hoppscotchDoc); + const c = result.collections[0]; + expect(c.name).toBe("SC"); + expect(c.children.length).toBe(2); // 子目录 + 列表 + }); +}); + +describe("Apipost", () => { + it("导入 api_collection(目录 + 请求)", () => { + const result = importApipost(apipostDoc); + const c = result.collections[0]; + expect(c.name).toBe("Apipost 项目"); + const folder = c.children[0]; + if (folder.type !== "folder") throw new Error("应当是目录"); + expect(folder.name).toBe("订单"); + const req = folder.children[0]; + if (req.type !== "request" || req.kind !== "http") throw new Error("应当是请求"); + expect(req.data.method).toBe("POST"); + expect(req.data.bodyMode).toBe("json"); + }); +}); + +describe("Markdown", () => { + it("提取 curl 代码块", () => { + const md = `# 用户文档 + +## 登录 + +\`\`\`curl +curl -X POST https://a.io/login -d '{"u":1}' +\`\`\` +`; + const result = importMarkdown(md); + const c = result.collections[0]; + expect(c.name).toBe("用户文档"); + expect(c.children.length).toBe(1); + }); +}); + +describe("树操作", () => { + it("插入/移动/删除/查找", () => { + const ws = createSeedWorkspace(); + const collections = ws.collections; + const demo = collections[0]; + const folder = demo.children[0]; + if (folder.type !== "folder") throw new Error("seed 第一个子节点应为目录"); + const req = folder.children[0]; + const found = findNode(collections, req.id); + expect(found?.node.id).toBe(req.id); + + // 移动到顶层 + const moved = moveNode(collections, req.id, demo.id, null); + const movedRef = findNode(moved, req.id); + expect(movedRef?.parent).toBeNull(); + + // 删除 + const removed = removeNode(moved, req.id); + expect(findNode(removed, req.id)).toBeNull(); + + // 插入顶层 + const newNode = { ...req, id: "new-1" }; + const inserted = insertIntoCollection(removed, demo.id, null, newNode); + expect(findNode(inserted, "new-1")).toBeTruthy(); + }); +}); + +describe("导出", () => { + it("OpenAPI / Postman / Markdown 往返", () => { + const imported = importAuto(openapi3); + const c = imported.collections[0]; + const openapiJson = exportCollectionAs("openapi-json", c); + const reparsed = JSON.parse(openapiJson); + expect(reparsed.openapi).toBe("3.0.3"); + expect(reparsed.paths["/pets"].get).toBeTruthy(); + + const postmanJson = exportCollectionAs("postman", c); + const reparsedPostman = JSON.parse(postmanJson); + expect(reparsedPostman.info.schema).toContain("v2.1.0"); + + const md = exportCollectionAs("markdown", c); + expect(md).toContain("# Petstore"); + expect(md).toContain("GET"); + }); + + it("cURL 导出", () => { + const c = importCurl(curlCmd); + const req = c.children[0]; + if (req.type !== "request" || req.kind !== "http") throw new Error("应当是请求"); + const exported = exportCurl(req.data); + expect(exported).toContain("-X POST"); + expect(exported).toContain("--data-raw"); + expect(exported).toContain("api.example.com/v1/users"); + }); + + it("备份导出", () => { + const ws = createSeedWorkspace(); + const backup = exportBackup(ws.collections, ws.environments); + const parsed = JSON.parse(backup); + expect(parsed.collections.length).toBeGreaterThan(0); + expect(detectImportFormat(backup)).toBe("backup"); + }); +}); diff --git a/packages/core/src/importers/apipost.ts b/packages/core/src/importers/apipost.ts new file mode 100644 index 0000000..70adf2b --- /dev/null +++ b/packages/core/src/importers/apipost.ts @@ -0,0 +1,209 @@ +/** + * Apipost 项目导出文件导入器(宽容解析:api_collection / parent_id / children 均支持)。 + * Apidoc / Apizza / Eolink / YApi 建议先导出为 OpenAPI 或 Postman 格式再导入。 + */ +import type { Collection, CollectionNode, FolderNode, HttpRequestData } from "../model"; +import { createCollection, createFolder, createRequest, kv } from "../factory"; +import type { ImportResult } from "../model"; +import { normalizeMethod } from "./curl"; + +type Obj = Record; +const asObj = (v: unknown): Obj => (v && typeof v === "object" ? (v as Obj) : {}); +const asArr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); +const str = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : String(v)); + +export function isApipostExport(doc: Obj): boolean { + if (Array.isArray(doc.api_collection)) return true; + if (Array.isArray(asObj(doc.data).api_collection)) return true; + // apipost v7 导出:{"name":..., "api_collection":[...]} 或数组包裹 + if (Array.isArray(doc.item_list)) return true; + return false; +} + +export function importApipost(text: string): ImportResult { + const doc = asObj(JSON.parse(text)); + if (!isApipostExport(doc)) throw new Error("不是 Apipost 导出文件(缺少 api_collection)"); + + const warnings: string[] = []; + const rawItems = asArr(doc.api_collection ?? asObj(doc.data).api_collection ?? doc.item_list); + const items = rawItems.map(asObj); + + // 取出文件夹(target_type/name+children 或 parent_id 结构) + const requestItems = items.filter((it) => !isFolderItem(it)); + const folderItems = items.filter(isFolderItem); + + const byParent = new Map(); + const parentKeyOf = (it: Obj): string => { + const parent = str(it.parent_id) || str(it.parentId); + if (!parent || parent === "0" || parent === "null" || parent === "root") return "__root__"; + return parent; + }; + for (const it of items) { + const parent = parentKeyOf(it); + if (!byParent.has(parent)) byParent.set(parent, []); + byParent.get(parent)!.push(it); + } + + const envVars: { key: string; value: string }[] = []; + + const build = (parentKey: string): CollectionNode[] => { + const out: CollectionNode[] = []; + for (const it of byParent.get(parentKey) ?? []) { + if (isFolderItem(it)) { + const name = str(it.name) || str(it.title) || "目录"; + const folder: FolderNode = createFolder(name, build(str(it.target_id) || str(it.id) || str(it._id) || name)); + out.push(folder); + } else { + const node = convertRequest(it, envVars, warnings); + if (node) out.push(node); + } + } + return out; + }; + + const children = build("__root__"); + // parent_id 断链的请求兜底平铺 + const placed = new Set(); + for (const it of folderItems) placed.add(str(it.target_id) || str(it.id)); + for (const it of requestItems) { + const parent = parentKeyOf(it); + if (parent !== "__root__" && !placed.has(parent)) { + const node = convertRequest(it, envVars, warnings); + if (node) children.push(node); + } + } + + const collection = createCollection(str(doc.name) || str(asObj(doc.data).name) || "Apipost 导入", children); + let count = countNodes(children); + if (count === 0) warnings.push("没有解析到请求;若是新版导出格式,请改用 OpenAPI/Postman 格式导出后导入"); + + // apipost 环境 + for (const env of asArr(doc.env ?? asObj(doc.data).env)) { + const envObj = asObj(env); + for (const v of asArr(envObj.param ?? envObj.variable)) { + const vObj = asObj(v); + const key = str(vObj.key); + if (key) envVars.push({ key, value: str(vObj.value) }); + } + } + + return { collections: [collection], environmentVariables: envVars, warnings }; +} + +function isFolderItem(it: Obj): boolean { + const hasChildren = Array.isArray(it.children) || Array.isArray(it.item); + const typeMark = str(it.target_type ?? it.type ?? it.item_type); + return hasChildren || typeMark === "folder" || typeMark === "group" || typeMark === "目录"; +} + +function convertRequest( + it: Obj, + _envVars: { key: string; value: string }[], + warnings: string[], +): CollectionNode | null { + const name = str(it.name) || str(it.title) || "未命名请求"; + const method = normalizeMethod(str(it.method) || "GET"); + const request = asObj(it.request); + + // url 可能是字符串或对象 + let url = ""; + if (typeof request.url === "string") url = request.url; + else { + url = str(asObj(request.url).raw ?? asObj(it.url).raw ?? it.url ?? ""); + } + + const headers: HttpRequestData["headers"] = []; + const query: HttpRequestData["query"] = []; + + // params/headers 可能是数组或对象 + const paramContainer = request.parameter ?? request.params ?? it.parameter; + collectKV(paramContainer, query); + const headerContainer = request.headers ?? request.header ?? it.headers; + collectKV(headerContainer, headers); + + const body = asObj(request.body ?? it.request_body); + const mode = str(body.mode ?? body.type ?? "none"); + let bodyMode: HttpRequestData["bodyMode"] = "none"; + let bodyRaw = ""; + let bodyForm: HttpRequestData["bodyForm"] = []; + if (mode === "json" || mode === "raw") { + bodyRaw = str(body.raw ?? body.text ?? ""); + bodyMode = looksLikeJson(bodyRaw) || mode === "json" ? "json" : "text"; + if (bodyMode === "json") bodyRaw = formatJson(bodyRaw); + } else if (mode === "urlencoded" || mode === "urlencoded[]") { + bodyMode = "urlencoded"; + collectKV(body.parameter ?? body.params, bodyForm); + } else if (mode === "form-data" || mode === "formdata") { + bodyMode = "form-data"; + collectKV(body.parameter ?? body.params, bodyForm); + } else if (mode === "xml") { + bodyMode = "xml"; + bodyRaw = str(body.raw ?? ""); + } else if (mode === "binary" || mode === "file") { + warnings.push(`${name}:binary body 不支持导入`); + } + + const auth = asObj(request.auth ?? it.auth); + let authConfig: HttpRequestData["auth"] = { mode: "none" }; + const authType = str(auth.type ?? auth.mode); + if (authType === "bearer") authConfig = { mode: "bearer", bearer: str(auth.bearer ?? auth.token) }; + if (authType === "basic") authConfig = { mode: "basic", basicUser: str(auth.user), basicPass: str(auth.password) }; + + return createRequest("http", name, { + data: { + kind: "http", + method, + url, + query, + headers, + cookies: [], + auth: authConfig, + bodyMode, + bodyRaw, + bodyForm, + }, + description: str(it.description) || str(request.description) || undefined, + }); +} + +/** apipost 的 KV 容器:[{key,value}] / [{name,value}] / {k:v} 都收 */ +function collectKV(container: unknown, out: HttpRequestData["headers"] | HttpRequestData["query"] | HttpRequestData["bodyForm"]): void { + if (Array.isArray(container)) { + for (const item of container) { + const itemObj = asObj(item); + const key = str(itemObj.key ?? itemObj.name); + const value = str(itemObj.value ?? ""); + const fieldTypeId = itemObj.field_type ?? itemObj.fieldType; + const disabled = itemObj.is_checked === 0 || itemObj.disabled === true || itemObj.enable === false; + if (key && str(fieldTypeId) !== "2") out.push(kv(key, value, !disabled)); + } + } else if (container && typeof container === "object") { + for (const [k, v] of Object.entries(container as Obj)) { + const vObj = asObj(v); + if (typeof v === "string") out.push(kv(k, v)); + else if (vObj.value !== undefined || vObj.key !== undefined) out.push(kv(str(vObj.key ?? k), str(vObj.value))); + } + } +} + +function looksLikeJson(s: string): boolean { + const t = s.trimStart(); + return t.startsWith("{") || t.startsWith("["); +} + +function formatJson(s: string): string { + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch { + return s; + } +} + +function countNodes(nodes: CollectionNode[]): number { + let n = 0; + for (const node of nodes) { + if (node.type === "request") n++; + else n += countNodes(node.children); + } + return n; +} diff --git a/packages/core/src/importers/curl.ts b/packages/core/src/importers/curl.ts new file mode 100644 index 0000000..93c4b6b --- /dev/null +++ b/packages/core/src/importers/curl.ts @@ -0,0 +1,386 @@ +/** + * cURL 命令解析器(覆盖 curl 常用 flag,容忍换行续行 / 引号 / 嵌套引号)。 + */ +import type { Collection } from "../model"; +import { kv } from "../factory"; +import { createCollection, createRequest } from "../factory"; + +/** 分词:支持 '…"…'、"…'…"、\" 转义、\ 续行 */ +export function tokenizeCurl(input: string): string[] { + const text = input.replace(/\\\r?\n/g, " ").replace(/\r?\n/g, " "); + const tokens: string[] = []; + let i = 0; + while (i < text.length) { + while (i < text.length && /\s/.test(text[i])) i++; + if (i >= text.length) break; + let token = ""; + let quote: '"' | "'" | null = null; + while (i < text.length) { + const ch = text[i]; + if (quote) { + if (ch === "\\" && quote === '"') { + const next = text[i + 1]; + if (next === '"' || next === "\\") { + token += next; + i += 2; + continue; + } + token += ch; + i++; + continue; + } + if (ch === quote) { + quote = null; + i++; + continue; + } + token += ch; + i++; + continue; + } + if (ch === '"' || ch === "'") { + quote = ch; + i++; + continue; + } + if (/\s/.test(ch)) break; + token += ch; + i++; + } + if (token) tokens.push(token); + } + return tokens; +} + +export type ParsedCurl = { + method: string; + url: string; + headers: { key: string; value: string; enabled: boolean }[]; + query: { key: string; value: string; enabled: boolean }[]; + cookies: { key: string; value: string; enabled: boolean }[]; + bodyMode: "none" | "json" | "xml" | "text" | "html" | "urlencoded" | "form-data"; + bodyRaw: string; + bodyForm: { key: string; value: string; enabled: boolean }[]; + auth: { mode: "none" | "bearer" | "basic"; bearer?: string; basicUser?: string; basicPass?: string }; + warnings: string[]; +}; + +export function parseCurl(input: string): ParsedCurl { + const warnings: string[] = []; + const tokens = tokenizeCurl(input); + let url = ""; + let method = ""; + const headers: ParsedCurl["headers"] = []; + const formRows: ParsedCurl["bodyForm"] = []; + const dataParts: string[] = []; + let auth: ParsedCurl["auth"] = { mode: "none" }; + let cookieRaw = ""; + let forceJson = false; + + const flagAliases: Record = { + "-X": "--request", + "-H": "--header", + "-d": "--data", + "--data-raw": "--data", + "--data-binary": "--data", + "--data-ascii": "--data", + "--data-urlencode": "--data", + "-F": "--form", + "--form-string": "--form", + "-u": "--user", + "--user": "--user", + "-b": "--cookie", + "-A": "--user-agent", + "-e": "--referer", + "-o": "--output", + "-x": "--proxy", + }; + + for (let i = 0; i < tokens.length; i++) { + let t = tokens[i]; + if (t === "curl") continue; + if (t === "npx" || t === "wget") warnings.push(`检测到 ${t},仅按 curl 子集解析`); + + const next = (): string | undefined => { + const v = tokens[++i]; + return v === "$" || v === undefined ? undefined : v; + }; + + if (!t.startsWith("-")) { + // 位置参数 → URL + const value = t.startsWith("$'") ? t.slice(2, -1) : t; + if (/^https?:\/\//i.test(value) || !url) url = url || value; + continue; + } + + // --flag=value 形式 + let inlineValue: string | undefined; + const eq = t.indexOf("="); + if (t.startsWith("--") && eq > 0) { + inlineValue = t.slice(eq + 1); + t = t.slice(0, eq); + } + const flag = flagAliases[t] ?? t; + + const take = (): string => { + if (inlineValue !== undefined) return inlineValue; + const v = next(); + return v ?? ""; + }; + + switch (flag) { + case "--request": + method = take().toUpperCase(); + break; + case "--header": { + const raw = take(); + const idx = raw.indexOf(":"); + if (idx > 0) { + const key = raw.slice(0, idx).trim(); + const value = raw.slice(idx + 1).trim(); + if (key.endsWith(";")) { + headers.push({ key: key.slice(0, -1), value: "", enabled: false }); + } else { + headers.push({ key, value, enabled: true }); + } + } + break; + } + case "--data": + case "--json": { + if (t === "--json") forceJson = true; + let v = take(); + if (v.startsWith("$'") && v.endsWith("'")) v = v.slice(2, -1); + if (v === "-" || v.startsWith("@")) { + warnings.push("忽略 @file / stdin 形式的数据体"); + break; + } + if (t === "--data-urlencode") { + v = encodeURIComponent(v); + } + dataParts.push(v); + break; + } + case "--form": { + const v = take(); + const [k, ...rest] = v.split("="); + if (k && !k.startsWith("@")) { + formRows.push({ key: k, value: rest.join("="), enabled: true }); + } else { + warnings.push(`忽略文件表单项 ${k}`); + } + break; + } + case "--user": { + const v = take(); + const idx = v.indexOf(":"); + auth = { + mode: "basic", + basicUser: idx >= 0 ? v.slice(0, idx) : v, + basicPass: idx >= 0 ? v.slice(idx + 1) : "", + }; + break; + } + case "--cookie": + cookieRaw = take(); + break; + case "--user-agent": + headers.push({ key: "User-Agent", value: take(), enabled: true }); + break; + case "--referer": + headers.push({ key: "Referer", value: take(), enabled: true }); + break; + case "--output": + case "--proxy": + take(); + break; + default: + // -k -s -L -i --compressed 等 → 忽略;未知带值 flag 尝试吞掉一个值 + if (!flag.startsWith("--") && flag.length > 2 && !inlineValue) { + // 组合短 flag(-sS)忽略 + } else if (flag.startsWith("--") && inlineValue === undefined && !isNoArgFlag(flag)) { + next(); + } + break; + } + } + + const data = dataParts.join("&"); + const inferred = + method || + (data || formRows.length + ? "POST" + : url.includes(";type=") + ? "PUT" + : "GET"); + + const headerMap = new Map(headers.map((h) => [h.key.toLowerCase(), h])); + const contentType = headerMap.get("content-type")?.value ?? ""; + + let bodyMode: ParsedCurl["bodyMode"] = "none"; + let bodyRaw = ""; + const bodyForm: ParsedCurl["bodyForm"] = []; + + if (formRows.length) { + bodyMode = "form-data"; + bodyForm.push(...formRows); + } else if (data) { + if (forceJson || contentType.includes("json") || looksLikeJson(data)) { + bodyMode = "json"; + bodyRaw = tryFormatJson(data); + } else if ( + contentType.includes("x-www-form-urlencoded") || + (!contentType && /^[^={\[][\s\S]*=[^=]*(&|$)/.test(data) && !data.trim().startsWith("<")) + ) { + bodyMode = "urlencoded"; + for (const pair of data.split("&")) { + const idx = pair.indexOf("="); + if (idx > 0) { + bodyForm.push({ + key: decodeMaybe(pair.slice(0, idx)), + value: decodeMaybe(pair.slice(idx + 1)), + enabled: true, + }); + } + } + } else if (contentType.includes("xml") || data.trimStart().startsWith("<")) { + bodyMode = "xml"; + bodyRaw = data; + } else { + bodyMode = "text"; + bodyRaw = data; + } + } + + // Cookie 头 → cookies 行 + const cookies: ParsedCurl["cookies"] = []; + if (cookieRaw) { + for (const pair of cookieRaw.split(";")) { + const idx = pair.indexOf("="); + if (idx > 0) { + cookies.push({ key: pair.slice(0, idx).trim(), value: pair.slice(idx + 1).trim(), enabled: true }); + } + } + } + + // URL 查询参数拆行 + const query: ParsedCurl["query"] = []; + let normalizedUrl = url; + const qIdx = url.indexOf("?"); + if (qIdx >= 0) { + normalizedUrl = url.slice(0, qIdx); + for (const pair of url.slice(qIdx + 1).split("&")) { + if (!pair) continue; + const idx = pair.indexOf("="); + query.push({ + key: decodeMaybe(idx > 0 ? pair.slice(0, idx) : pair), + value: idx > 0 ? decodeMaybe(pair.slice(idx + 1)) : "", + enabled: true, + }); + } + } + + return { + method: inferred, + url: normalizedUrl, + headers, + query, + cookies, + bodyMode, + bodyRaw, + bodyForm, + auth, + warnings, + }; +} + +function isNoArgFlag(flag: string): boolean { + return [ + "--compressed", + "--insecure", + "--silent", + "--verbose", + "--location", + "--include", + "--head", + "-k", + "-s", + "-S", + "-L", + "-i", + "-I", + "-v", + "-f", + "-G", + "--get", + "--no-buffer", + ].includes(flag); +} + +function looksLikeJson(s: string): boolean { + const t = s.trimStart(); + return t.startsWith("{") || t.startsWith("["); +} + +function tryFormatJson(s: string): string { + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch { + return s; + } +} + +function decodeMaybe(s: string): string { + try { + return decodeURIComponent(s.replace(/\+/g, " ")); + } catch { + return s; + } +} + +export function importCurl(input: string, collectionName = "cURL 导入"): Collection { + const parsed = parseCurl(input); + const name = deriveCurlName(parsed); + const node = createRequest("http", name, { + data: { + method: normalizeMethod(parsed.method), + url: parsed.url, + query: parsed.query.map((q) => kv(q.key, q.value, q.enabled)), + headers: parsed.headers + .filter((h) => h.key.toLowerCase() !== "cookie") + .map((h) => kv(h.key, h.value, h.enabled)), + cookies: parsed.cookies.map((c) => kv(c.key, c.value, c.enabled)), + auth: + parsed.auth.mode === "basic" + ? { mode: "basic", basicUser: parsed.auth.basicUser, basicPass: parsed.auth.basicPass } + : parsed.auth.mode === "bearer" + ? { mode: "bearer", bearer: parsed.auth.bearer } + : { mode: "none" }, + bodyMode: parsed.bodyMode, + bodyRaw: parsed.bodyRaw, + bodyForm: parsed.bodyForm.map((f) => kv(f.key, f.value, f.enabled)), + }, + description: parsed.warnings.join("\n") || undefined, + }); + const collection = createCollection(collectionName, [node]); + collection.description = "由 cURL 命令导入"; + return collection; +} + +export function normalizeMethod(m: string) { + const upper = m.toUpperCase(); + const allowed = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const; + return (allowed as readonly string[]).includes(upper) + ? (upper as (typeof allowed)[number]) + : "GET"; +} + +function deriveCurlName(parsed: ParsedCurl): string { + try { + const u = new URL(parsed.url); + const seg = u.pathname.split("/").filter(Boolean).pop(); + return `${parsed.method} /${seg ?? ""}`.trim(); + } catch { + return `${parsed.method} ${parsed.url}`.slice(0, 60); + } +} diff --git a/packages/core/src/importers/har.ts b/packages/core/src/importers/har.ts new file mode 100644 index 0000000..8105e08 --- /dev/null +++ b/packages/core/src/importers/har.ts @@ -0,0 +1,151 @@ +/** + * HAR(HTTP Archive)导入器:按域名分组。 + */ +import type { Collection, CollectionNode, FolderNode, HttpRequestData } from "../model"; +import { createCollection, createFolder, createRequest, kv } from "../factory"; +import type { ImportResult } from "../model"; +import { normalizeMethod } from "./curl"; + +type Obj = Record; +const asObj = (v: unknown): Obj => (v && typeof v === "object" ? (v as Obj) : {}); +const asArr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); +const str = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : String(v)); + +export function isHarDocument(doc: Obj): boolean { + const log = asObj(doc.log); + return Array.isArray(log.entries); +} + +export function importHar(text: string): ImportResult { + const doc = asObj(JSON.parse(text)); + if (!isHarDocument(doc)) throw new Error("不是 HAR 文件(缺少 log.entries)"); + + const warnings: string[] = []; + const entries = asArr(asObj(doc.log).entries); + const byHost = new Map(); + const seen = new Set(); + let count = 0; + + for (const entryRaw of entries) { + const entry = asObj(entryRaw); + const request = asObj(entry.request); + const urlStr = str(request.url); + if (!/^https?:\/\//i.test(urlStr)) continue; + const method = normalizeMethod(str(request.method) || "GET"); + const dedupeKey = `${method} ${urlStr}`; + if (seen.has(dedupeKey)) continue; + seen.add(dedupeKey); + + let u: URL; + try { + u = new URL(urlStr); + } catch { + warnings.push(`跳过非法 URL:${urlStr.slice(0, 60)}`); + continue; + } + + const host = u.host; + let folder = byHost.get(host); + if (!folder) { + folder = createFolder(host, []); + byHost.set(host, folder); + } + + // query + const query: HttpRequestData["query"] = []; + for (const q of asArr(request.queryString)) { + const qObj = asObj(q); + if (qObj.name) query.push(kv(str(qObj.name), str(qObj.value))); + } + // headers(剔除 cookie) + const headers: HttpRequestData["headers"] = []; + const cookies: HttpRequestData["cookies"] = []; + for (const h of asArr(request.headers)) { + const hObj = asObj(h); + const name = str(hObj.name); + if (!name) continue; + if (name.toLowerCase() === "cookie") { + for (const pair of str(hObj.value).split(";")) { + const idx = pair.indexOf("="); + if (idx > 0) cookies.push(kv(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim())); + } + continue; + } + headers.push(kv(name, str(hObj.value))); + } + // body + const postData = asObj(request.postData); + let bodyMode: HttpRequestData["bodyMode"] = "none"; + let bodyRaw = ""; + let bodyForm: HttpRequestData["bodyForm"] = []; + const mime = str(postData.mimeType); + const text = str(postData.text); + if (mime.includes("json") || (!mime && looksLikeJson(text))) { + bodyMode = "json"; + bodyRaw = formatJson(text); + } else if (mime.includes("x-www-form-urlencoded")) { + bodyMode = "urlencoded"; + const params = asArr(postData.params); + if (params.length) { + for (const p of params) { + const pObj = asObj(p); + if (pObj.name) bodyForm.push(kv(str(pObj.name), str(pObj.value))); + } + } else { + for (const pair of text.split("&")) { + const idx = pair.indexOf("="); + if (idx > 0) bodyForm.push(kv(pair.slice(0, idx), pair.slice(idx + 1))); + } + } + } else if (mime.includes("xml")) { + bodyMode = "xml"; + bodyRaw = text; + } else if (mime.includes("multipart")) { + bodyMode = "form-data"; + for (const p of asArr(postData.params)) { + const pObj = asObj(p); + if (pObj.name && str(pObj.fileName) === "") bodyForm.push(kv(str(pObj.name), str(pObj.value))); + } + } else if (mime) { + bodyMode = "text"; + bodyRaw = text; + } + + const pathname = decodeURIComponent(u.pathname) || "/"; + const node = createRequest("http", `${method} ${pathname}`, { + data: { + kind: "http", + method, + url: `${u.origin}${u.pathname}`, + query, + headers, + cookies, + auth: { mode: "none" }, + bodyMode, + bodyRaw, + bodyForm, + }, + }); + folder.children.push(node); + count++; + } + + const children: CollectionNode[] = Array.from(byHost.values()); + const collection = createCollection("HAR 导入", children); + collection.description = `从 HAR 捕获导入 ${count} 个请求`; + if (count === 0) warnings.push("HAR 里没有可导入的 http(s) 请求"); + return { collections: [collection], environmentVariables: [], warnings }; +} + +function looksLikeJson(s: string): boolean { + const t = s.trimStart(); + return t.startsWith("{") || t.startsWith("["); +} + +function formatJson(s: string): string { + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch { + return s; + } +} diff --git a/packages/core/src/importers/hoppscotch.ts b/packages/core/src/importers/hoppscotch.ts new file mode 100644 index 0000000..cc38d87 --- /dev/null +++ b/packages/core/src/importers/hoppscotch.ts @@ -0,0 +1,135 @@ +/** + * Hoppscotch 集合导入器(数组或单对象,folders/requests 递归)。 + */ +import type { Collection, CollectionNode, FolderNode, HttpRequestData } from "../model"; +import { createCollection, createFolder, createRequest, kv } from "../factory"; +import type { ImportResult } from "../model"; +import { normalizeMethod } from "./curl"; + +type Obj = Record; +const asObj = (v: unknown): Obj => (v && typeof v === "object" ? (v as Obj) : {}); +const asArr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); +const str = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : String(v)); + +export function isHoppscotchCollection(doc: unknown): boolean { + const list = Array.isArray(doc) ? doc.map(asObj) : [asObj(doc)]; + return list.some((c) => Array.isArray(c.requests) || Array.isArray(c.folders)); +} + +export function importHoppscotch(text: string): ImportResult { + const parsed: unknown = JSON.parse(text); + const roots = Array.isArray(parsed) ? parsed.map(asObj) : [asObj(parsed)]; + if (!isHoppscotchCollection(parsed)) throw new Error("不是 Hoppscotch 集合(缺少 requests/folders)"); + + const warnings: string[] = []; + const results: Collection[] = []; + + for (const root of roots) { + if (!Array.isArray(root.requests) && !Array.isArray(root.folders)) continue; + const children = convertFolder(root, warnings); + const collection = createCollection(str(root.name) || "Hoppscotch 导入", children); + results.push(collection); + } + + if (results.length === 0) warnings.push("没有可导入的集合"); + return { collections: results, environmentVariables: [], warnings }; +} + +function convertFolder(folder: Obj, warnings: string[]): CollectionNode[] { + const out: CollectionNode[] = []; + for (const f of asArr(folder.folders)) { + const fObj = asObj(f); + out.push(createFolder(str(fObj.name) || "目录", convertFolder(fObj, warnings))); + } + for (const r of asArr(folder.requests)) { + out.push(convertRequest(asObj(r), warnings)); + } + return out; +} + +function convertRequest(r: Obj, warnings: string[]): CollectionNode { + const method = normalizeMethod(str(r.method) || "GET"); + const url = str(r.url) + str(r.path === "/" || r.path === "" ? "" : str(r.path)); + + const headers: HttpRequestData["headers"] = []; + for (const h of asArr(r.headers)) { + const hObj = asObj(h); + if (hObj.key) headers.push(kv(str(hObj.key), str(hObj.value), hObj.active !== false)); + } + const query: HttpRequestData["query"] = []; + for (const p of asArr(r.params)) { + const pObj = asObj(p); + if (pObj.key) query.push(kv(str(pObj.key), str(pObj.value), pObj.active !== false)); + } + + const bodyModeRaw = str(r.bodyMode ?? r.requestBodyMode ?? "none"); + let bodyMode: HttpRequestData["bodyMode"] = "none"; + let bodyRaw = ""; + let bodyForm: HttpRequestData["bodyForm"] = []; + + if (bodyModeRaw === "raw" || bodyModeRaw === "application/json" || bodyModeRaw === "application/xml" || bodyModeRaw === "text/plain") { + bodyRaw = str(r.rawRequestBody ?? r.rawParams ?? ""); + bodyMode = bodyModeRaw.includes("xml") ? "xml" : looksLikeJson(bodyRaw) ? "json" : "text"; + } else if (bodyModeRaw === "urlencoded" || bodyModeRaw === "application/x-www-form-urlencoded") { + bodyMode = "urlencoded"; + for (const p of asArr(r.bodyParams ?? r.params)) { + const pObj = asObj(p); + if (pObj.key) bodyForm.push(kv(str(pObj.key), str(pObj.value), pObj.active !== false)); + } + } else if (bodyModeRaw === "formdata" || bodyModeRaw === "multipart/form-data") { + bodyMode = "form-data"; + for (const p of asArr(r.bodyParams)) { + const pObj = asObj(p); + if (pObj.key && !str(pObj.value).startsWith("@file")) bodyForm.push(kv(str(pObj.key), str(pObj.value), pObj.active !== false)); + } + } + + let auth: HttpRequestData["auth"] = { mode: "none" }; + const authType = str(r.auth); + if (authType === "bearer") auth = { mode: "bearer", bearer: str(r.bearerToken ?? r.httpUser) }; + if (authType === "basic") + auth = { mode: "basic", basicUser: str(r.httpUser), basicPass: str(r.httpPassword) }; + if (authType === "oauth2") auth = { mode: "bearer", bearer: str(r.oauth2Token ?? "") }; + + if (r.graphqlQuery !== undefined || str(r.requestType).includes("graphql")) { + return createRequest("graphql", str(r.name) || "GraphQL", { + data: { + kind: "graphql", + url, + headers, + auth, + query: str(r.graphqlQuery) || "query {\n \n}", + variables: safeStringify(r.graphqlVariables) || "{}", + }, + }); + } + + return createRequest("http", str(r.name) || `${method} 请求`, { + data: { + kind: "http", + method, + url, + query, + headers, + cookies: [], + auth, + bodyMode, + bodyRaw, + bodyForm, + }, + }); +} + +function looksLikeJson(s: string): boolean { + const t = s.trimStart(); + return t.startsWith("{") || t.startsWith("["); +} + +function safeStringify(v: unknown): string { + if (typeof v === "string") return v; + try { + return JSON.stringify(v, null, 2) ?? ""; + } catch { + return ""; + } +} diff --git a/packages/core/src/importers/index.ts b/packages/core/src/importers/index.ts new file mode 100644 index 0000000..c3df4a4 --- /dev/null +++ b/packages/core/src/importers/index.ts @@ -0,0 +1,138 @@ +/** + * 导入入口:格式自动识别 + 统一 importWorkspaceData()。 + */ +import type { Collection, ImportResult } from "../model"; +import { importCurl, tokenizeCurl, parseCurl } from "./curl"; +import { importOpenApi, isOpenApiDocument, parseSpecDocument } from "./openapi"; +import { importPostman, isPostmanCollection } from "./postman"; +import { importHar, isHarDocument } from "./har"; +import { importInsomnia, isInsomniaExport } from "./insomnia"; +import { importHoppscotch, isHoppscotchCollection } from "./hoppscotch"; +import { importApipost, isApipostExport } from "./apipost"; +import { importMarkdown, looksLikeMarkdown } from "./markdown"; + +export type ImportFormat = + | "openapi" + | "postman" + | "curl" + | "har" + | "insomnia" + | "hoppscotch" + | "apipost" + | "markdown" + | "backup" + | "unknown"; + +export const FORMAT_LABELS: Record = { + openapi: "OpenAPI / Swagger / Apifox", + postman: "Postman Collection", + curl: "cURL 命令", + har: "HAR 抓包文件", + insomnia: "Insomnia 导出", + hoppscotch: "Hoppscotch 集合", + apipost: "Apipost 项目", + markdown: "Markdown 文档", + backup: "OpsxAPI 备份", + unknown: "未知格式", +}; + +export function detectImportFormat(text: string): ImportFormat { + const trimmed = text.trim(); + + if (/^(curl|wget)\b/.test(trimmed) || (trimmed.startsWith("npx ") && trimmed.includes(" curl"))) { + return "curl"; + } + + let json: unknown = null; + let isJson = false; + try { + json = JSON.parse(trimmed); + isJson = true; + } catch { + isJson = false; + } + + if (isJson && json && typeof json === "object") { + const doc = json as Record; + if (isOpenApiDocument(doc)) return "openapi"; + if (isPostmanCollection(doc)) return "postman"; + if (isHarDocument(doc)) return "har"; + if (isInsomniaExport(doc)) return "insomnia"; + if (isApipostExport(doc)) return "apipost"; + if (isHoppscotchCollection(json)) return "hoppscotch"; + if (isOpsxBackup(doc)) return "backup"; + // 数组也可能是 hoppscotch + if (Array.isArray(json) && isHoppscotchCollection(json)) return "hoppscotch"; + } + + // YAML(OpenAPI 常见) + if (!isJson && /^(openapi|swagger)\s*:/m.test(trimmed)) { + try { + const doc = parseSpecDocument(trimmed); + if (isOpenApiDocument(doc)) return "openapi"; + } catch { + /* fallthrough */ + } + } + + if (looksLikeMarkdown(trimmed)) return "markdown"; + + return "unknown"; +} + +export function isOpsxBackup(doc: Record): boolean { + return doc.__opsxapi__ === "backup" || (doc.version === 2 && Array.isArray(doc.collections)); +} + +export function importByFormat(text: string, format: ImportFormat): ImportResult { + switch (format) { + case "openapi": + return importOpenApi(text); + case "postman": + return importPostman(text); + case "curl": + return { collections: [importCurl(text)], environmentVariables: [], warnings: [] }; + case "har": + return importHar(text); + case "insomnia": + return importInsomnia(text); + case "hoppscotch": + return importHoppscotch(text); + case "apipost": + return importApipost(text); + case "markdown": + return importMarkdown(text); + case "backup": + return importBackup(text); + case "unknown": { + const detected = detectImportFormat(text); + if (detected !== "unknown") return importByFormat(text, detected); + throw new Error("无法识别格式。支持:OpenAPI/Swagger、Postman、Apifox、Apipost、cURL、HAR、Insomnia、Hoppscotch、Markdown、OpsxAPI 备份。"); + } + } +} + +/** 自动识别并导入 */ +export function importAuto(text: string): ImportResult { + const format = detectImportFormat(text); + return importByFormat(text, format); +} + +export function importBackup(text: string): ImportResult { + const doc = JSON.parse(text) as { collections?: Collection[] }; + if (!Array.isArray(doc.collections)) throw new Error("备份文件缺少 collections"); + return { + collections: doc.collections, + environmentVariables: [], + warnings: [], + }; +} + +export { tokenizeCurl, parseCurl, importCurl }; +export { importOpenApi, isOpenApiDocument, parseSpecDocument }; +export { importPostman, isPostmanCollection }; +export { importHar, isHarDocument }; +export { importInsomnia, isInsomniaExport }; +export { importHoppscotch, isHoppscotchCollection }; +export { importApipost, isApipostExport }; +export { importMarkdown, looksLikeMarkdown }; diff --git a/packages/core/src/importers/insomnia.ts b/packages/core/src/importers/insomnia.ts new file mode 100644 index 0000000..e7b0194 --- /dev/null +++ b/packages/core/src/importers/insomnia.ts @@ -0,0 +1,203 @@ +/** + * Insomnia 导出文件(export_format v3/v4)导入器。 + */ +import type { Collection, CollectionNode, FolderNode, HttpRequestData } from "../model"; +import { createCollection, createFolder, createRequest, kv } from "../factory"; +import type { ImportResult } from "../model"; +import { normalizeMethod } from "./curl"; + +type Obj = Record; +const asObj = (v: unknown): Obj => (v && typeof v === "object" ? (v as Obj) : {}); +const asArr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); +const str = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : String(v)); + +export function isInsomniaExport(doc: Obj): boolean { + if (str(doc._type) === "export" && Array.isArray(doc.resources)) return true; + // v4 嵌套格式:resources 里是 workspace + return Array.isArray(doc.resources) && asArr(doc.resources).some((r) => asObj(r)._type === "workspace"); +} + +export function importInsomnia(text: string): ImportResult { + const doc = asObj(JSON.parse(text)); + if (!isInsomniaExport(doc)) throw new Error("不是 Insomnia 导出文件(缺少 resources)"); + + const warnings: string[] = []; + const resources = asArr(doc.resources).map(asObj); + const workspaces = resources.filter((r) => str(r._type) === "workspace"); + const results: Collection[] = []; + + for (const ws of workspaces) { + const scopeIds = new Set([str(ws._id)]); + // 找出属于该 workspace 的所有 resource(递归 parentId) + const byParent = new Map(); + for (const r of resources) { + if (str(r._type) === "workspace") continue; + const parent = str(r.parentId); + if (!byParent.has(parent)) byParent.set(parent, []); + byParent.get(parent)!.push(r); + } + const children = collectChildren(scopeIds, byParent, warnings); + const collection = createCollection(str(ws.name) || "Insomnia 导入", children); + results.push(collection); + } + + if (results.length === 0) { + // 无 workspace(v3):全部平铺 + const byParent = new Map(); + for (const r of resources) { + if (str(r._type) === "workspace") continue; + const parent = str(r.parentId) || "__root__"; + if (!byParent.has(parent)) byParent.set(parent, []); + byParent.get(parent)!.push(r); + } + const children = collectChildren(new Set(["__root__"]), byParent, warnings); + results.push(createCollection("Insomnia 导入", children)); + } + + let total = 0; + for (const c of results) total += countNodes(c.children); + if (total === 0) warnings.push("没有可导入的请求"); + return { collections: results, environmentVariables: [], warnings }; +} + +function collectChildren( + parentIds: Set, + byParent: Map, + warnings: string[], +): CollectionNode[] { + const out: CollectionNode[] = []; + for (const id of parentIds) { + for (const r of byParent.get(id) ?? []) { + const type = str(r._type); + if (type === "request_group") { + const folder = createFolder(str(r.name) || "目录", collectChildren(new Set([str(r._id)]), byParent, warnings)); + if (r.description) folder.description = str(r.description); + out.push(folder); + } else if (type === "request") { + out.push(convertRequest(r, warnings)); + } + } + } + return out; +} + +function convertRequest(r: Obj, warnings: string[]): CollectionNode { + const url = str(r.url) || str(asObj(r.url).raw) || ""; + const method = normalizeMethod(str(r.method) || "GET"); + + const headers: HttpRequestData["headers"] = []; + for (const h of asArr(r.headers)) { + const hObj = asObj(h); + if (hObj.name && str(hObj.name).toLowerCase() !== "cookie") { + headers.push(kv(str(hObj.name), str(hObj.value), hObj.disabled !== true)); + } + } + const query: HttpRequestData["query"] = []; + let baseUrl = url; + for (const p of asArr(r.parameters)) { + const pObj = asObj(p); + if (pObj.name) query.push(kv(str(pObj.name), str(pObj.value), pObj.disabled !== true)); + } + + const body = asObj(r.body); + const mime = str(body.mimeType); + let bodyMode: HttpRequestData["bodyMode"] = "none"; + let bodyRaw = ""; + let bodyForm: HttpRequestData["bodyForm"] = []; + if (mime.includes("graphql")) { + const gqlText = typeof body.text === "string" ? body.text : ""; + const gql = asObj(gqlText ? safeJsonParse(gqlText) : {}); + return createRequest("graphql", str(r.name) || "GraphQL", { + data: { + kind: "graphql", + url: baseUrl, + headers, + auth: authFromInsomnia(asObj(r.authentication)), + query: str(gql.query) || "query {\n \n}", + variables: safeStringify(gql.variables) || "{}", + }, + }); + } + if (mime.includes("urlencoded")) { + bodyMode = "urlencoded"; + for (const p of asArr(body.params)) { + const pObj = asObj(p); + if (pObj.name) bodyForm.push(kv(str(pObj.name), str(pObj.value), pObj.disabled !== true)); + } + } else if (mime.includes("form-data") || mime.includes("multipart")) { + bodyMode = "form-data"; + for (const p of asArr(body.params)) { + const pObj = asObj(p); + if (pObj.name && !pObj.file) bodyForm.push(kv(str(pObj.name), str(pObj.value), pObj.disabled !== true)); + } + } else if (mime.includes("json")) { + bodyMode = "json"; + bodyRaw = formatJson(str(body.text)); + } else if (mime) { + bodyMode = mime.includes("xml") ? "xml" : mime.includes("html") ? "html" : "text"; + bodyRaw = str(body.text); + } + + return createRequest("http", str(r.name) || `${method} 请求`, { + data: { + kind: "http", + method, + url: baseUrl, + query, + headers, + cookies: [], + auth: authFromInsomnia(asObj(r.authentication)), + bodyMode, + bodyRaw, + bodyForm, + }, + description: str(r.description) || undefined, + }); +} + +function authFromInsomnia(auth: Obj): HttpRequestData["auth"] { + const type = str(auth.type); + if (type === "bearer") return { mode: "bearer", bearer: str(auth.token) }; + if (type === "basic") return { mode: "basic", basicUser: str(auth.username), basicPass: str(auth.password) }; + if (type === "apikey" || type === "api_key") + return { + mode: "apikey", + apiKeyName: str(auth.key), + apiKeyValue: str(auth.value), + apiKeyIn: str(auth.in) === "query" ? "query" : "header", + }; + return { mode: "none" }; +} + +function safeJsonParse(s: string): unknown { + try { + return JSON.parse(s); + } catch { + return {}; + } +} + +function safeStringify(v: unknown): string { + try { + return JSON.stringify(v, null, 2) ?? ""; + } catch { + return ""; + } +} + +function formatJson(s: string): string { + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch { + return s; + } +} + +function countNodes(nodes: CollectionNode[]): number { + let n = 0; + for (const node of nodes) { + if (node.type === "request") n++; + else n += countNodes(node.children); + } + return n; +} diff --git a/packages/core/src/importers/markdown.ts b/packages/core/src/importers/markdown.ts new file mode 100644 index 0000000..a0ac4fc --- /dev/null +++ b/packages/core/src/importers/markdown.ts @@ -0,0 +1,86 @@ +/** + * Markdown API 文档导入器:提取 curl 代码块(及可选 JSON 请求体示例)。 + */ +import type { Collection } from "../model"; +import { createCollection, createFolder } from "../factory"; +import type { ImportResult } from "../model"; +import { importCurl } from "./curl"; + +export function looksLikeMarkdown(text: string): boolean { + const t = text.trimStart(); + if (!t.startsWith("#") && !t.includes("```")) return false; + return /^#{1,6}\s/m.test(text) || /```/.test(text); +} + +export function importMarkdown(text: string): ImportResult { + const warnings: string[] = []; + const blocks: { heading: string; code: string }[] = []; + + const lines = text.split(/\r?\n/); + let heading = ""; + let inBlock = false; + let lang = ""; + let buf: string[] = []; + + for (const line of lines) { + const fence = line.match(/^\s*```(\w*)\s*$/); + if (fence) { + if (!inBlock) { + inBlock = true; + lang = fence[1].toLowerCase(); + buf = []; + } else { + inBlock = false; + const code = buf.join("\n"); + if (["curl", "bash", "sh", "shell", ""].includes(lang) && code.trim().startsWith("curl")) { + blocks.push({ heading: heading || `curl ${blocks.length + 1}`, code }); + } + } + continue; + } + if (inBlock) { + buf.push(line); + continue; + } + const h = line.match(/^#{1,6}\s+(.*)$/); + if (h) heading = h[1].trim(); + } + + if (blocks.length === 0) { + warnings.push("未在 Markdown 中找到 curl 代码块(也支持直接粘贴 cURL 导入)"); + } + + const folders = new Map>(); + const rootChildren = []; + for (const block of blocks) { + try { + const temp = importCurl(block.code, "temp"); + const node = temp.children[0]; + if (!node) continue; + // 一级标题做目录,二级及以下做请求名 + const parts = block.heading.split(/[::]/); + const folderName = parts[0]?.trim(); + if (folderName && folders.has(folderName)) { + folders.get(folderName)!.children.push(node); + } else if (folderName) { + const f = createFolder(folderName, [node]); + folders.set(folderName, f); + } else { + rootChildren.push(node); + } + } catch (e) { + warnings.push(`解析代码块失败:${e instanceof Error ? e.message : String(e)}`); + } + } + + const collection = createCollection( + firstHeading(text) || "Markdown 导入", + [...folders.values(), ...rootChildren], + ); + return { collections: [collection], environmentVariables: [], warnings }; +} + +function firstHeading(text: string): string { + const m = text.match(/^#\s+(.+)$/m); + return m ? m[1].trim().slice(0, 60) : ""; +} diff --git a/packages/core/src/importers/openapi.ts b/packages/core/src/importers/openapi.ts new file mode 100644 index 0000000..933e65d --- /dev/null +++ b/packages/core/src/importers/openapi.ts @@ -0,0 +1,400 @@ +/** + * OpenAPI 3.1 / 3.0 / Swagger 2.0 导入器(JSON 或 YAML;Apifox 导出的 OpenAPI 格式同样适用)。 + */ +import { load as yamlLoad } from "js-yaml"; +import type { + Collection, + CollectionNode, + FolderNode, + HttpRequestData, + RequestMethod, +} from "../model"; +import { createCollection, createFolder, createRequest, kv } from "../factory"; +import type { ImportResult } from "../model"; + +type Obj = Record; +const asObj = (v: unknown): Obj => (v && typeof v === "object" ? (v as Obj) : {}); +const asArr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); +const str = (v: unknown): string => (typeof v === "string" ? v : v === undefined || v === null ? "" : String(v)); + +const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"]; + +export function parseSpecDocument(text: string): Obj { + const trimmed = text.trim(); + if (trimmed.startsWith("{")) { + return asObj(JSON.parse(trimmed)); + } + return asObj(yamlLoad(trimmed)); +} + +export function isOpenApiDocument(doc: Obj): boolean { + return "openapi" in doc || "swagger" in doc; +} + +export function importOpenApi(text: string): ImportResult { + const doc = parseSpecDocument(text); + if (!isOpenApiDocument(doc)) throw new Error("不是 OpenAPI/Swagger 文档(缺少 openapi/swagger 字段)"); + const version = str(doc.openapi || doc.swagger); + const info = asObj(doc.info); + const warnings: string[] = []; + + const baseUrl = resolveBaseUrl(doc); + const envVars: { key: string; value: string }[] = []; + if (baseUrl) envVars.push({ key: "baseUrl", value: baseUrl }); + + const securitySchemes = asObj( + version.startsWith("2") + ? doc.securityDefinitions + : asObj(doc.components).securitySchemes, + ); + + // tag → folder + const tagOrder: string[] = []; + const tagDesc: Record = {}; + for (const t of asArr(doc.tags)) { + const tag = asObj(t); + if (tag.name) { + tagOrder.push(str(tag.name)); + if (tag.description) tagDesc[str(tag.name)] = str(tag.description); + } + } + const folderMap = new Map(); + const rootChildren: CollectionNode[] = []; + const ensureFolder = (tag: string): FolderNode | null => { + if (!tag) return null; + let f = folderMap.get(tag); + if (!f) { + f = createFolder(tag, []); + if (tagDesc[tag]) f.description = tagDesc[tag]; + folderMap.set(tag, f); + } + return f; + }; + + const paths = asObj(doc.paths); + let count = 0; + for (const [path, pathItemRaw] of Object.entries(paths)) { + const pathItem = asObj(pathItemRaw); + for (const method of HTTP_METHODS) { + const opRaw = pathItem[method]; + if (!opRaw) continue; + const op = asObj(opRaw); + count++; + const node = buildOperationNode({ + method: method.toUpperCase() as RequestMethod, + path, + op, + doc, + version, + securitySchemes, + baseUrl, + envVars, + warnings, + name: str(op.summary) || str(op.operationId) || `${method.toUpperCase()} ${path}`, + }); + const tags = asArr(op.tags).map(str).filter(Boolean); + const folder = tags.length ? ensureFolder(tags[0]) : null; + if (folder) folder.children.push(node); + else rootChildren.push(node); + } + } + + if (count === 0) warnings.push("文档中没有可导入的路径操作"); + + const children: CollectionNode[] = [ + ...tagOrder.filter((t) => folderMap.has(t)).map((t) => folderMap.get(t)!), + ...Array.from(folderMap.values()).filter((f) => !tagOrder.includes(f.name)), + ...rootChildren, + ]; + + const collection = createCollection(str(info.title) || "OpenAPI 导入", children); + collection.description = str(info.description) || undefined; + return { collections: [collection], environmentVariables: envVars, warnings }; +} + +function resolveBaseUrl(doc: Obj): string { + // v3: servers[]; v2: host + basePath + schemes + const servers = asArr(doc.servers); + if (servers.length) { + const first = asObj(servers[0]); + let url = str(first.url); + const vars = asObj(first.variables); + for (const [k, v] of Object.entries(vars)) { + const def = asObj(v).default; + if (def !== undefined) url = url.replaceAll(`{${k}}`, str(def)); + } + if (url) return url.replace(/\/$/, ""); + } + const host = str(doc.host); + if (host) { + const schemes = asArr(doc.schemes).map(str); + const scheme = schemes.includes("https") ? "https" : schemes[0] || "https"; + const basePath = str(doc.basePath) || ""; + return `${scheme}://${host}${basePath}`.replace(/\/$/, ""); + } + return ""; +} + +function buildOperationNode(ctx: { + method: RequestMethod; + path: string; + op: Obj; + doc: Obj; + version: string; + securitySchemes: Obj; + baseUrl: string; + envVars: { key: string; value: string }[]; + warnings: string[]; + name: string; +}): CollectionNode { + const { method, path, op, doc, version, securitySchemes, baseUrl, envVars, warnings } = ctx; + + // path 参数 {id} → {{id}} + let urlPath = path; + const pathParams: { name: string; example: string }[] = []; + for (const p of asArr(asObj(op).parameters ?? asObj(asObj(doc.paths)[path]).parameters)) { + const param = asObj(p); + if (str(param.in) === "path" && param.name) { + pathParams.push({ name: str(param.name), example: exampleOf(param) || "1" }); + } + } + for (const { name, example } of pathParams) { + urlPath = urlPath.replaceAll(`{${name}}`, `{{${name}}}`); + if (!envVars.some((v) => v.key === name)) envVars.push({ key: name, value: example }); + } + + // 统一使用 {{baseUrl}} 前缀(服务器地址写入环境变量),保持环境切换能力 + const url = `{{baseUrl}}${urlPath.startsWith("/") ? urlPath : `/${urlPath.replace(/^\{\{baseUrl\}\}?\/?/, "/")}`}`; + + const query: HttpRequestData["query"] = []; + const headers: HttpRequestData["headers"] = []; + let auth: HttpRequestData["auth"] = { mode: "none" }; + + for (const p of asArr(op.parameters)) { + const param = asObj(p); + if (!param.name) continue; + const in_ = str(param.in); + const row = kv(str(param.name), exampleOf(param), true, str(param.description)); + if (in_ === "query") query.push(row); + else if (in_ === "header") headers.push(row); + } + // v2 顶层 parameters(path 级) + const pathLevel = asArr(asObj(asObj(doc.paths)[path]).parameters); + for (const p of pathLevel) { + const param = asObj(p); + if (!param.name) continue; + const in_ = str(param.in); + if (in_ === "query") query.push(kv(str(param.name), exampleOf(param), true, str(param.description))); + else if (in_ === "header") headers.push(kv(str(param.name), exampleOf(param), true, str(param.description))); + } + + // 鉴权 + const securities = asArr(op.security ?? doc.security); + if (securities.length) { + const first = asObj(securities[0]); + const schemeName = Object.keys(first)[0]; + if (schemeName && securitySchemes[schemeName] !== undefined) { + const scheme = asObj(securitySchemes[schemeName]); + const type = str(scheme.type); + if (version.startsWith("2")) { + if (type === "basic") auth = { mode: "basic", basicUser: "", basicPass: "" }; + else if (type === "apiKey") + auth = { + mode: "apikey", + apiKeyName: str(scheme.name), + apiKeyIn: str(scheme.in) === "query" ? "query" : "header", + apiKeyValue: "", + }; + } else { + if (type === "http" && str(scheme.scheme) === "bearer") auth = { mode: "bearer", bearer: "" }; + else if (type === "http" && str(scheme.scheme) === "basic") auth = { mode: "basic", basicUser: "", basicPass: "" }; + else if (type === "apiKey") + auth = { + mode: "apikey", + apiKeyName: str(scheme.name), + apiKeyIn: str(scheme.in) === "query" ? "query" : "header", + apiKeyValue: "", + }; + else if (type === "oauth2") auth = { mode: "bearer", bearer: "" }; + } + } + } + + // 请求体 + let bodyMode: HttpRequestData["bodyMode"] = "none"; + let bodyRaw = ""; + let bodyForm: HttpRequestData["bodyForm"] = []; + + if (version.startsWith("2")) { + for (const p of [...asArr(op.parameters), ...pathLevel]) { + const param = asObj(p); + if (str(param.in) === "body") { + const consumes = asArr(op.consumes).map(str); + const mime = consumes[0] ?? "application/json"; + const example = schemaToExample(asObj(param.schema), doc, 0); + if (mime.includes("json")) { + bodyMode = "json"; + bodyRaw = safeStringify(example); + } else if (mime.includes("xml")) { + bodyMode = "xml"; + bodyRaw = safeStringify(example); + } else { + bodyMode = "text"; + bodyRaw = safeStringify(example); + } + } + if (str(param.in) === "formData") { + bodyMode = asArr(op.consumes).map(str)[0]?.includes("urlencoded") ? "urlencoded" : "form-data"; + bodyForm.push(kv(str(param.name), exampleOf(param), true, str(param.description))); + } + } + } else { + const requestBody = asObj(op.requestBody); + const content = asObj(requestBody.content); + const mimes = Object.keys(content); + if (mimes.length) { + const mime = + mimes.find((m) => m.includes("json")) ?? + mimes.find((m) => m.includes("xml")) ?? + mimes.find((m) => m.includes("urlencoded")) ?? + mimes.find((m) => m.includes("form-data")) ?? + mimes.find((m) => m.includes("text")) ?? + mimes[0]; + const media = asObj(content[mime]); + if (mime.includes("urlencoded")) { + bodyMode = "urlencoded"; + const schema = asObj(media.schema); + for (const [k, v] of Object.entries(asObj(schema.properties))) { + bodyForm.push(kv(k, schemaFieldExample(asObj(v)), true, str(asObj(v).description))); + } + if (media.example) bodyRaw = safeStringify(media.example); + } else if (mime.includes("form-data") || mime.includes("multipart")) { + bodyMode = "form-data"; + const schema = asObj(media.schema); + for (const [k, v] of Object.entries(asObj(schema.properties))) { + bodyForm.push(kv(k, schemaFieldExample(asObj(v)), true, str(asObj(v).description))); + } + } else { + const example = + media.example !== undefined + ? media.example + : firstExampleObject(media.examples) ?? schemaToExample(asObj(media.schema), doc, 0); + if (mime.includes("xml")) { + bodyMode = "xml"; + bodyRaw = str(example); + } else if (mime.includes("text") || mime.includes("html")) { + bodyMode = mime.includes("html") ? "html" : "text"; + bodyRaw = str(example); + } else { + bodyMode = "json"; + bodyRaw = safeStringify(example); + } + } + } + } + + if (bodyMode !== "none" && !headers.some((h) => h.key.toLowerCase() === "content-type")) { + if (bodyMode === "json") headers.unshift(kv("Content-Type", "application/json")); + if (bodyMode === "xml") headers.unshift(kv("Content-Type", "application/xml")); + if (bodyMode === "urlencoded") + headers.unshift(kv("Content-Type", "application/x-www-form-urlencoded")); + } + + const descriptionParts: string[] = []; + if (op.description) descriptionParts.push(str(op.description)); + if (op.deprecated === true) descriptionParts.push("> ⚠️ 已废弃(deprecated)"); + + return createRequest("http", ctx.name.slice(0, 120), { + data: { + kind: "http", + method, + url, + query, + headers, + cookies: [], + auth, + bodyMode, + bodyRaw, + bodyForm, + }, + description: descriptionParts.join("\n\n") || undefined, + }); +} + +function exampleOf(param: Obj): string { + if (param.example !== undefined) return str(param.example); + const ex = asObj(param.examples); + const first = Object.values(ex)[0]; + if (first !== undefined) return str(asObj(first).value); + const schema = asObj(param.schema); + if (schema.default !== undefined) return str(schema.default); + if (schema.example !== undefined) return str(schema.example); + const t = str(schema.type); + if (t === "integer" || t === "number") return "1"; + if (t === "boolean") return "true"; + if (Array.isArray(schema.enum) && schema.enum.length) return str(schema.enum[0]); + return ""; +} + +function firstExampleObject(examples: unknown): unknown { + const first = Object.values(asObj(examples))[0]; + return asObj(first).value; +} + +function schemaFieldExample(field: Obj): string { + if (field.example !== undefined) return str(field.example); + if (field.default !== undefined) return str(field.default); + if (Array.isArray(field.enum) && field.enum.length) return str(field.enum[0]); + const t = str(field.type); + if (t === "integer" || t === "number") return "0"; + if (t === "boolean") return "true"; + return ""; +} + +/** 从 JSON Schema 生成示例值(解析 $ref,深度封顶) */ +export function schemaToExample(schema: Obj, doc: Obj, depth: number): unknown { + if (!schema || depth > 6) return null; + if (schema.example !== undefined) return schema.example; + if (schema.default !== undefined) return schema.default; + if (Array.isArray(schema.enum) && schema.enum.length) return schema.enum[0]; + + const resolved = resolveRef(schema, doc); + if (resolved !== schema) return schemaToExample(resolved, doc, depth + 1); + + const t = str(schema.type); + if (t === "object" || schema.properties) { + const out: Record = {}; + for (const [k, v] of Object.entries(asObj(schema.properties))) { + out[k] = schemaToExample(asObj(v), doc, depth + 1); + } + return out; + } + if (t === "array" || schema.items) { + return [schemaToExample(asObj(schema.items), doc, depth + 1)]; + } + if (t === "integer" || t === "number") return 0; + if (t === "boolean") return true; + return ""; +} + +function resolveRef(schema: Obj, doc: Obj): Obj { + const ref = str(schema.$ref); + if (!ref) return schema; + // #/components/schemas/X 或 #/definitions/X + const parts = ref.replace(/^#\//, "").split("/"); + let cur: unknown = doc; + for (const part of parts) { + cur = asObj(cur)[part]; + if (cur === undefined) return schema; + } + return asObj(cur); +} + +function safeStringify(v: unknown): string { + if (typeof v === "string") return v; + try { + return JSON.stringify(v, null, 2) ?? ""; + } catch { + return ""; + } +} diff --git a/packages/core/src/importers/postman.ts b/packages/core/src/importers/postman.ts new file mode 100644 index 0000000..17db55d --- /dev/null +++ b/packages/core/src/importers/postman.ts @@ -0,0 +1,228 @@ +/** + * Postman Collection v2.0/v2.1 导入器。 + */ +import type { + Collection, + CollectionNode, + FolderNode, + HttpRequestData, + RequestMethod, +} from "../model"; +import { createCollection, createFolder, createRequest, kv } from "../factory"; +import type { ImportResult } from "../model"; +import { normalizeMethod } from "./curl"; + +type Obj = Record; +const asObj = (v: unknown): Obj => (v && typeof v === "object" ? (v as Obj) : {}); +const asArr = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); +const str = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : String(v)); + +export function isPostmanCollection(doc: Obj): boolean { + const info = asObj(doc.info); + const schema = str(info.schema); + return schema.includes("postman") || Array.isArray(doc.item) || info._postman_id !== undefined; +} + +export function importPostman(text: string): ImportResult { + const doc = asObj(JSON.parse(text)); + if (!isPostmanCollection(doc)) throw new Error("不是 Postman Collection(缺少 item/info.schema)"); + + const warnings: string[] = []; + const envVars: { key: string; value: string }[] = []; + + const root = createFolder("root", []); + convertItems(asArr(doc.item), root, envVars, warnings); + + // collection.variable → 环境变量 + for (const v of asArr(doc.variable)) { + const varObj = asObj(v); + const key = str(varObj.key); + if (key) envVars.push({ key, value: str(varObj.value) }); + } + + const info = asObj(doc.info); + const collection = createCollection(str(info.name) || "Postman 导入", root.children); + collection.description = str(info.description) || undefined; + + let count = 0; + const countReqs = (nodes: CollectionNode[]) => { + for (const n of nodes) { + if (n.type === "request") count++; + else countReqs(n.children); + } + }; + countReqs(collection.children); + if (count === 0) warnings.push("集合里没有请求"); + + return { collections: [collection], environmentVariables: dedupeVars(envVars), warnings }; +} + +function convertItems( + items: unknown[], + folder: FolderNode, + envVars: { key: string; value: string }[], + warnings: string[], +): void { + for (const itemRaw of items) { + const item = asObj(itemRaw); + if (Array.isArray(item.item)) { + const sub = createFolder(str(item.name) || "目录", []); + if (item.description) sub.description = str(item.description); + folder.children.push(sub); + convertItems(item.item, sub, envVars, warnings); + continue; + } + const request = item.request; + if (request === undefined) continue; + if (typeof request === "string") { + // url-only 形式,跳过 + warnings.push(`跳过仅有 URL 的 item:${str(item.name)}`); + continue; + } + folder.children.push(convertRequest(asObj(request), str(item.name) || "未命名请求", envVars)); + } +} + +function convertRequest( + request: Obj, + name: string, + envVars: { key: string; value: string }[], +): CollectionNode { + const urlObj = typeof request.url === "string" ? { raw: request.url } : asObj(request.url); + + // URL:raw 优先;query 行会拼进 raw,因此从 raw 里剥掉 query 后再拼 query 行 + let raw = str(urlObj.raw); + let hostPath = raw; + if (!raw) { + const host = asArr(urlObj.host).map((s) => str(s)).join("."); + const path = asArr(urlObj.path).map((s) => str(s)).join("/"); + const protocol = str(urlObj.protocol) || "https"; + const port = str(urlObj.port) ? `:${str(urlObj.port)}` : ""; + hostPath = `${protocol}://${host}${port}${path ? `/${path}` : ""}`; + raw = hostPath; + } + + const query: HttpRequestData["query"] = []; + for (const q of asArr(urlObj.query)) { + const qObj = asObj(q); + query.push(kv(str(qObj.key), str(qObj.value), qObj.disabled !== true, str(qObj.description))); + } + // raw 自带 query 时拆出来 + const qMark = hostPath.indexOf("?"); + let baseUrl = hostPath; + if (qMark >= 0) { + baseUrl = hostPath.slice(0, qMark); + if (query.length === 0) { + for (const pair of hostPath.slice(qMark + 1).split("&")) { + if (!pair) continue; + const idx = pair.indexOf("="); + query.push(kv(idx > 0 ? pair.slice(0, idx) : pair, idx > 0 ? pair.slice(idx + 1) : "")); + } + } + } + + // path 变量 {{var}} + for (const v of asArr(urlObj.variable)) { + const varObj = asObj(v); + if (varObj.key) envVars.push({ key: str(varObj.key), value: str(varObj.value) }); + } + + const headers: HttpRequestData["headers"] = []; + for (const h of asArr(request.header)) { + const hObj = asObj(h); + headers.push(kv(str(hObj.key), str(hObj.value), hObj.disabled !== true)); + } + + const cookies: HttpRequestData["cookies"] = []; + + // body + let bodyMode: HttpRequestData["bodyMode"] = "none"; + let bodyRaw = ""; + let bodyForm: HttpRequestData["bodyForm"] = []; + const body = asObj(request.body); + if (body.mode === "raw") { + bodyRaw = str(body.raw); + const lang = str(asObj(asObj(body.options).raw).language); + bodyMode = + lang === "json" || looksLikeJson(bodyRaw) + ? "json" + : lang === "xml" + ? "xml" + : lang === "html" + ? "html" + : "text"; + } else if (body.mode === "urlencoded") { + bodyMode = "urlencoded"; + for (const p of asArr(body.urlencoded)) { + const pObj = asObj(p); + bodyForm.push(kv(str(pObj.key), str(pObj.value), pObj.disabled !== true)); + } + } else if (body.mode === "formdata") { + bodyMode = "form-data"; + for (const p of asArr(body.formdata)) { + const pObj = asObj(p); + if (str(pObj.type) === "file") continue; + bodyForm.push(kv(str(pObj.key), str(pObj.value), pObj.disabled !== true)); + } + } else if (body.mode === "graphql") { + const g = asObj(body.graphql); + return createRequest("graphql", name, { + data: { + kind: "graphql", + url: baseUrl, + headers, + auth: authFromPostman(asObj(request.auth)), + query: str(g.query) || "query {\n \n}", + variables: str(g.variables) || "{}", + }, + }); + } + + return createRequest("http", name, { + data: { + kind: "http", + method: normalizeMethod(str(request.method) || "GET") as RequestMethod, + url: baseUrl, + query, + headers, + cookies, + auth: authFromPostman(asObj(request.auth)), + bodyMode, + bodyRaw, + bodyForm, + }, + description: str(request.description) || undefined, + }); +} + +function authFromPostman(auth: Obj): HttpRequestData["auth"] { + const type = str(auth.type); + const read = (key: string): string => { + for (const kvRow of asArr(auth[type])) { + const kvObj = asObj(kvRow); + if (str(kvObj.key) === key) return str(kvObj.value); + } + return ""; + }; + if (type === "bearer") return { mode: "bearer", bearer: read("token") }; + if (type === "basic") return { mode: "basic", basicUser: read("username"), basicPass: read("password") }; + if (type === "apikey") + return { + mode: "apikey", + apiKeyName: read("key"), + apiKeyValue: read("value"), + apiKeyIn: read("in") === "query" ? "query" : "header", + }; + return { mode: "none" }; +} + +function looksLikeJson(s: string): boolean { + const t = s.trimStart(); + return t.startsWith("{") || t.startsWith("["); +} + +function dedupeVars(vars: { key: string; value: string }[]) { + const map = new Map(); + for (const v of vars) if (!map.has(v.key)) map.set(v.key, v.value); + return Array.from(map, ([key, value]) => ({ key, value })); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..fcb0c02 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,118 @@ +// 传输层契约(HttpSpec / WsSpec / Transport) +export type { + HttpMethod, + HttpBodyMode, + HttpSpec, + WsSpec, + HttpResult, + WsSession, + Transport, +} from "./types"; + +// 集合数据模型 +export type { + RequestMethod, + RequestKind, + KVRow, + BodyMode, + AuthMode, + AuthConfig, + HttpRequestData, + GraphqlRequestData, + WebsocketRequestData, + SseRequestData, + SocketIoRequestData, + MqttRequestData, + RequestData, + RequestDataOf, + RequestNodeBase, + HttpApiNode, + GraphqlApiNode, + WebsocketApiNode, + SseApiNode, + SocketIoApiNode, + MqttApiNode, + RequestNode, + FolderNode, + CollectionNode, + Collection, + Environment, + HistoryEntry, + ThemeMode, + WorkspaceSettings, + WorkspaceData, + ImportResult, +} from "./model"; +export { + REQUEST_METHODS, + REQUEST_KINDS, + KIND_LABELS, + BODY_MODE_LABELS, + RAW_BODY_MODES, +} from "./model"; + +// 树操作 +export { + newId, + walk, + findNode, + mapTree, + replaceNode, + removeNode, + insertNode, + insertIntoCollection, + moveNode, + containsId, + deepCopyNode, + uniqueName, + countRequests, + searchTree, +} from "./tree"; +export type { NodeRef } from "./tree"; + +// 工厂 / 种子 +export { + kv, + defaultRequestData, + createRequest, + defaultName, + createFolder, + createCollection, + createEnvironment, + createSeedWorkspace, +} from "./factory"; + +// 变量替换 +export { replaceVariables, replaceVariablesInSpec } from "./replaceVariables"; + +// 导入 +export { + detectImportFormat, + importAuto, + importByFormat, + importBackup, + FORMAT_LABELS, +} from "./importers"; +export type { ImportFormat } from "./importers"; +export { parseCurl, importCurl, tokenizeCurl, normalizeMethod } from "./importers/curl"; +export { importOpenApi } from "./importers/openapi"; +export { importPostman } from "./importers/postman"; +export { importHar } from "./importers/har"; +export { importInsomnia } from "./importers/insomnia"; +export { importHoppscotch } from "./importers/hoppscotch"; +export { importApipost } from "./importers/apipost"; +export { importMarkdown } from "./importers/markdown"; + +// 导出 +export { + exportCollectionAs, + exportBackup, + exportOpenApiWithFolders, + exportPostman, + exportMarkdown, + downloadText, +} from "./exporters"; +export type { ExportFormat, OpenApiExportOptions } from "./exporters"; +export { exportCurl, shellQuote, buildUrlWithQuery, authHeader } from "./exporters/curl"; +export { generateCode } from "./exporters/codegen"; +export type { CodeTarget } from "./exporters/codegen"; diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts new file mode 100644 index 0000000..9911ca9 --- /dev/null +++ b/packages/core/src/model.ts @@ -0,0 +1,250 @@ +/** + * OpsxAPI 集合数据模型 —— UI / 导入导出 / 持久化共用的唯一事实源。 + */ + +export type RequestMethod = + | "GET" + | "POST" + | "PUT" + | "PATCH" + | "DELETE" + | "HEAD" + | "OPTIONS"; + +export const REQUEST_METHODS: RequestMethod[] = [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS", +]; + +export type RequestKind = "http" | "graphql" | "websocket" | "sse" | "socketio" | "mqtt"; + +export const REQUEST_KINDS: RequestKind[] = [ + "http", + "graphql", + "websocket", + "sse", + "socketio", + "mqtt", +]; + +export const KIND_LABELS: Record = { + http: "HTTP", + graphql: "GraphQL", + websocket: "WebSocket", + sse: "SSE", + socketio: "Socket.IO", + mqtt: "MQTT", +}; + +/** 键值行:Params / Headers / Cookies / 表单 / 环境变量共用 */ +export type KVRow = { + id: string; + key: string; + value: string; + enabled: boolean; + description?: string; +}; + +export type BodyMode = + | "none" + | "json" + | "xml" + | "text" + | "html" + | "urlencoded" + | "form-data"; + +export const BODY_MODE_LABELS: Record = { + none: "none", + json: "JSON", + xml: "XML", + text: "Text", + html: "HTML", + urlencoded: "x-www-form-urlencoded", + "form-data": "form-data", +}; + +export const RAW_BODY_MODES: BodyMode[] = ["json", "xml", "text", "html"]; + +export type AuthMode = "none" | "bearer" | "basic" | "apikey"; + +export type AuthConfig = { + mode: AuthMode; + bearer?: string; + basicUser?: string; + basicPass?: string; + apiKeyName?: string; + apiKeyValue?: string; + apiKeyIn?: "header" | "query"; +}; + +export type HttpRequestData = { + kind: "http"; + method: RequestMethod; + url: string; + query: KVRow[]; + headers: KVRow[]; + cookies: KVRow[]; + auth: AuthConfig; + bodyMode: BodyMode; + bodyRaw: string; + bodyForm: KVRow[]; +}; + +export type GraphqlRequestData = { + kind: "graphql"; + url: string; + headers: KVRow[]; + auth: AuthConfig; + query: string; + variables: string; +}; + +export type WebsocketRequestData = { + kind: "websocket"; + url: string; + headers: KVRow[]; + protocols: string; +}; + +export type SseRequestData = { + kind: "sse"; + url: string; + headers: KVRow[]; + query: KVRow[]; + auth: AuthConfig; + lastEventId: string; + /** 仅解析 data: 行为 JSON */ + dataOnly: boolean; +}; + +export type SocketIoRequestData = { + kind: "socketio"; + url: string; + /** 额外连接参数(JSON:auth / transports / path …) */ + options: string; + /** 监听的服务端事件名 */ + listenEvents: string[]; + sendEvent: string; + sendArgs: string; +}; + +export type MqttRequestData = { + kind: "mqtt"; + brokerUrl: string; + username: string; + password: string; + clientId: string; + /** 订阅主题,逗号分隔 */ + subscribeTopics: string; + publishTopic: string; + publishPayload: string; + qos: 0 | 1 | 2; +}; + +export type RequestData = + | HttpRequestData + | GraphqlRequestData + | WebsocketRequestData + | SseRequestData + | SocketIoRequestData + | MqttRequestData; + +export type RequestDataOf = Extract; + +export type RequestNodeBase = { + id: string; + type: "request"; + name: string; + description?: string; + createdAt: number; + updatedAt: number; +}; + +export type HttpApiNode = RequestNodeBase & { kind: "http"; data: HttpRequestData }; +export type GraphqlApiNode = RequestNodeBase & { kind: "graphql"; data: GraphqlRequestData }; +export type WebsocketApiNode = RequestNodeBase & { kind: "websocket"; data: WebsocketRequestData }; +export type SseApiNode = RequestNodeBase & { kind: "sse"; data: SseRequestData }; +export type SocketIoApiNode = RequestNodeBase & { kind: "socketio"; data: SocketIoRequestData }; +export type MqttApiNode = RequestNodeBase & { kind: "mqtt"; data: MqttRequestData }; + +/** 判别联合:`node.kind === "http"` 时自动收窄 data 类型 */ +export type RequestNode = + | HttpApiNode + | GraphqlApiNode + | WebsocketApiNode + | SseApiNode + | SocketIoApiNode + | MqttApiNode; + +export type FolderNode = { + id: string; + type: "folder"; + name: string; + description?: string; + children: CollectionNode[]; + createdAt: number; + updatedAt: number; +}; + +export type CollectionNode = RequestNode | FolderNode; + +/** 一个集合(项目),本身是一个顶层目录 */ +export type Collection = { + id: string; + type: "collection"; + name: string; + description?: string; + children: CollectionNode[]; + createdAt: number; + updatedAt: number; +}; + +export type Environment = { + id: string; + name: string; + variables: KVRow[]; +}; + +export type HistoryEntry = { + id: string; + at: number; + kind: RequestKind; + label: string; + method?: RequestMethod; + url: string; + status?: number; + durationMs?: number; + ok: boolean; + /** 关联的请求节点 id(可回跳) */ + nodeId?: string; +}; + +export type ThemeMode = "dark" | "light"; + +export type WorkspaceSettings = { + theme: ThemeMode; + /** 请求超时(毫秒) */ + timeoutMs: number; +}; + +export type WorkspaceData = { + version: 2; + collections: Collection[]; + environments: Environment[]; + activeEnvironmentId: string | null; + history: HistoryEntry[]; + settings: WorkspaceSettings; +}; + +/** 导入结果:集合 + 建议写入环境的前缀变量(如 baseUrl) */ +export type ImportResult = { + collections: Collection[]; + environmentVariables: { key: string; value: string }[]; + warnings: string[]; +}; diff --git a/packages/core/src/replaceVariables.test.ts b/packages/core/src/replaceVariables.test.ts new file mode 100644 index 0000000..b27a932 --- /dev/null +++ b/packages/core/src/replaceVariables.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { replaceVariables, replaceVariablesInSpec } from "./replaceVariables"; + +describe("replaceVariables", () => { + it("replaces Postman-style {{var}}", () => { + expect(replaceVariables("{{baseUrl}}/api", { baseUrl: "https://x.test" })).toBe( + "https://x.test/api", + ); + }); + + it("leaves unknown vars intact", () => { + expect(replaceVariables("hello {{missing}}", {})).toBe("hello {{missing}}"); + }); + + it("supports multiple and spaced keys", () => { + expect( + replaceVariables("{{ host }}/{{path}}", { host: "h", path: "p" }), + ).toBe("h/p"); + }); + + it("replaceVariablesInSpec walks nested strings", () => { + const out = replaceVariablesInSpec( + { url: "{{baseUrl}}/v1", headers: { Authorization: "Bearer {{token}}" } }, + { baseUrl: "http://a", token: "t" }, + ); + expect(out.url).toBe("http://a/v1"); + expect(out.headers.Authorization).toBe("Bearer t"); + }); +}); diff --git a/packages/core/src/replaceVariables.ts b/packages/core/src/replaceVariables.ts new file mode 100644 index 0000000..4ce99f5 --- /dev/null +++ b/packages/core/src/replaceVariables.ts @@ -0,0 +1,35 @@ +/** + * Postman-style {{var}} replacement. + * Unknown variables are left as-is. + */ +export function replaceVariables( + input: string, + env: Record, +): string { + if (!input) return input; + return input.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key: string) => { + if (Object.prototype.hasOwnProperty.call(env, key)) { + return env[key] ?? ""; + } + return match; + }); +} + +export function replaceVariablesInSpec>( + spec: T, + env: Record, +): T { + const walk = (v: unknown): unknown => { + if (typeof v === "string") return replaceVariables(v, env); + if (Array.isArray(v)) return v.map(walk); + if (v && typeof v === "object") { + const out: Record = {}; + for (const [k, val] of Object.entries(v as Record)) { + out[k] = walk(val); + } + return out; + } + return v; + }; + return walk(spec) as T; +} diff --git a/packages/core/src/tree.ts b/packages/core/src/tree.ts new file mode 100644 index 0000000..82f8f29 --- /dev/null +++ b/packages/core/src/tree.ts @@ -0,0 +1,227 @@ +/** + * 集合树纯函数操作(不可变更新)。 + */ +import type { + Collection, + CollectionNode, + FolderNode, + RequestNode, +} from "./model"; + +export type NodeRef = { + collection: Collection; + /** 父目录;集合顶层时为 null */ + parent: FolderNode | null; + node: CollectionNode; +}; + +export function newId(): string { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return crypto.randomUUID(); + } + return `id-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; +} + +export function walk( + nodes: CollectionNode[], + fn: (node: CollectionNode, parent: FolderNode | null) => void, + parent: FolderNode | null = null, +): void { + for (const node of nodes) { + fn(node, parent); + if (node.type === "folder") walk(node.children, fn, node); + } +} + +export function findNode( + collections: Collection[], + id: string, +): NodeRef | null { + for (const collection of collections) { + let found: NodeRef | null = null; + walk(collection.children, (node, parent) => { + if (!found && node.id === id) found = { collection, parent, node }; + }); + if (found) return found; + } + return null; +} + +export function mapTree( + nodes: T[], + fn: (node: T) => T, +): T[] { + return nodes.map((node) => { + const mapped = fn(node); + if (mapped.type === "folder") { + return { ...mapped, children: mapTree(mapped.children as T[], fn) }; + } + return mapped; + }); +} + +/** 返回新的 collections 数组;未命中时原样返回 */ +export function replaceNode( + collections: Collection[], + id: string, + updater: (node: CollectionNode) => CollectionNode, +): Collection[] { + const visit = (nodes: CollectionNode[]): CollectionNode[] => + nodes.map((node) => { + if (node.id === id) return updater(node); + if (node.type === "folder") return { ...node, children: visit(node.children) }; + return node; + }); + let changed = false; + const result = collections.map((c) => { + const children = visit(c.children); + if (children !== c.children) changed = true; + return changed || children !== c.children ? { ...c, children } : c; + }); + return result; +} + +export function removeNode( + collections: Collection[], + id: string, +): Collection[] { + const visit = (nodes: CollectionNode[]): CollectionNode[] => + nodes + .filter((node) => node.id !== id) + .map((node) => + node.type === "folder" ? { ...node, children: visit(node.children) } : node, + ); + return collections.map((c) => ({ ...c, children: visit(c.children) })); +} + +export function insertNode( + collections: Collection[], + target: { parentId: string | null }, + node: CollectionNode, +): Collection[] { + const visit = (nodes: CollectionNode[]): CollectionNode[] => { + if (target.parentId === null) return [...nodes, node]; + return nodes.map((n) => { + if (n.type === "folder") { + if (n.id === target.parentId) return { ...n, children: [...n.children, node] }; + return { ...n, children: visit(n.children) }; + } + return n; + }); + }; + // parentId 为 null 时插入第一个集合顶层(调用方一般会用 appendToCollection) + return collections.map((c) => ({ ...c, children: visit(c.children) })); +} + +/** 插入到指定集合(collectionId)的指定目录(parentId 可为 null = 顶层) */ +export function insertIntoCollection( + collections: Collection[], + collectionId: string, + parentId: string | null, + node: CollectionNode, +): Collection[] { + return collections.map((c) => { + if (c.id !== collectionId) return c; + if (parentId === null) return { ...c, children: [...c.children, node] }; + const visit = (nodes: CollectionNode[]): CollectionNode[] => + nodes.map((n) => { + if (n.type !== "folder") return n; + if (n.id === parentId) return { ...n, children: [...n.children, node] }; + return { ...n, children: visit(n.children) }; + }); + return { ...c, children: visit(c.children) }; + }); +} + +/** 把节点移动到目标目录(跨集合移动也支持;null = 集合顶层) */ +export function moveNode( + collections: Collection[], + id: string, + targetCollectionId: string, + targetParentId: string | null, +): Collection[] { + const ref = findNode(collections, id); + if (!ref) return collections; + if (id === targetParentId) return collections; // 不能移动到自己下面 + // 目标是自己后代时拒绝 + if (ref.node.type === "folder" && containsId(ref.node.children, targetParentId)) { + return collections; + } + const node = ref.node; + const removed = removeNode(collections, id); + return insertIntoCollection(removed, targetCollectionId, targetParentId, node); +} + +export function containsId(nodes: CollectionNode[], id: string | null): boolean { + if (id === null) return false; + let hit = false; + walk(nodes, (n) => { + if (n.id === id) hit = true; + }); + return hit; +} + +export function deepCopyNode(node: CollectionNode, rename?: (name: string) => string): CollectionNode { + if (node.type === "request") { + return { ...node, id: newId(), name: rename ? rename(node.name) : node.name, createdAt: Date.now(), updatedAt: Date.now() }; + } + return { + ...node, + id: newId(), + name: rename ? rename(node.name) : node.name, + children: node.children.map((c) => deepCopyNode(c, rename)), + createdAt: Date.now(), + updatedAt: Date.now(), + }; +} + +export function uniqueName(siblings: CollectionNode[], base: string): string { + const names = new Set(siblings.map((n) => n.name)); + if (!names.has(base)) return base; + for (let i = 2; i < 999; i++) { + const candidate = `${base} ${i}`; + if (!names.has(candidate)) return candidate; + } + return `${base} ${Date.now()}`; +} + +export function countRequests(collection: Collection): number { + let n = 0; + walk(collection.children, (node) => { + if (node.type === "request") n++; + }); + return n; +} + +/** 名称匹配搜索(大小写不敏感),返回命中的请求/目录 id 集合(目录命中自身或含命中后代) */ +export function searchTree( + collections: Collection[], + term: string, +): Set { + const q = term.trim().toLowerCase(); + const hits = new Set(); + if (!q) return hits; + const visitFolder = (nodes: CollectionNode[], ancestors: string[]): boolean => { + let any = false; + for (const node of nodes) { + if (node.type === "request") { + const text = `${node.name} ${node.kind} ${ + node.kind === "http" ? node.data.method + " " + node.data.url : "" + }`.toLowerCase(); + if (text.includes(q)) { + hits.add(node.id); + any = true; + } + } else { + const childHit = visitFolder(node.children, [...ancestors, node.id]); + if (childHit || node.name.toLowerCase().includes(q)) { + hits.add(node.id); + any = true; + } + } + } + return any; + }; + for (const c of collections) visitFolder(c.children, [c.id]); + return hits; +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..2e5ee7f --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,57 @@ +export type HttpMethod = + | "GET" + | "POST" + | "PUT" + | "PATCH" + | "DELETE" + | "HEAD" + | "OPTIONS"; + +export type HttpBodyMode = + | "none" + | "json" + | "raw" + | "xml" + | "text" + | "html" + | "urlencoded" + | "form" + | "form-data"; + +export type HttpSpec = { + method: HttpMethod; + url: string; + headers: Record; + query?: Record; + body?: { + mode: HttpBodyMode; + raw?: string; + }; + timeoutMs?: number; +}; + +export type WsSpec = { + url: string; + protocols?: string[]; + headers?: Record; +}; + +export type HttpResult = { + status: number; + headers: Record; + body: string; + timeMs: number; + size: number; +}; + +export interface WsSession { + onMessage(cb: (data: string) => void): void; + onStatus(cb: (s: string) => void): void; + send(data: string): void; + close(): void; +} + +export interface Transport { + http(spec: HttpSpec): Promise; + ws(spec: WsSpec): WsSession; +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..a47cede --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src" + ] +} diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts new file mode 100644 index 0000000..f624398 --- /dev/null +++ b/packages/core/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + }, +}); diff --git a/packages/transport/package.json b/packages/transport/package.json new file mode 100644 index 0000000..1c6eee0 --- /dev/null +++ b/packages/transport/package.json @@ -0,0 +1,27 @@ +{ + "name": "@opsxapi/transport", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@opsxapi/core": "*", + "mqtt": "^5.10.1", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "typescript": "^5.8.2" + }, + "peerDependencies": { + "@tauri-apps/api": "^2.0.0" + }, + "peerDependenciesMeta": { + "@tauri-apps/api": { + "optional": true + } + } +} \ No newline at end of file diff --git a/packages/transport/src/browser.ts b/packages/transport/src/browser.ts new file mode 100644 index 0000000..299ebda --- /dev/null +++ b/packages/transport/src/browser.ts @@ -0,0 +1,81 @@ +import type { HttpResult, HttpSpec, Transport, WsSession, WsSpec } from "@opsxapi/core"; + +function buildUrl(spec: HttpSpec): string { + const u = new URL(spec.url); + if (spec.query) { + for (const [k, v] of Object.entries(spec.query)) { + if (v !== undefined && v !== "") u.searchParams.set(k, v); + } + } + return u.toString(); +} + +function headersToRecord(h: Headers): Record { + const out: Record = {}; + h.forEach((v, k) => { + out[k] = v; + }); + return out; +} + +export const browserTransport: Transport = { + async http(spec: HttpSpec): Promise { + const started = performance.now(); + const init: RequestInit = { + method: spec.method, + headers: { ...spec.headers }, + signal: spec.timeoutMs + ? AbortSignal.timeout(spec.timeoutMs) + : undefined, + }; + if (spec.body && spec.body.mode !== "none" && spec.method !== "GET") { + init.body = spec.body.raw ?? ""; + if (spec.body.mode === "json" && !("content-type" in Object.fromEntries( + Object.entries(spec.headers).map(([k, v]) => [k.toLowerCase(), v]), + ))) { + (init.headers as Record)["Content-Type"] = "application/json"; + } + } + const res = await fetch(buildUrl(spec), init); + const body = await res.text(); + const timeMs = Math.round(performance.now() - started); + return { + status: res.status, + headers: headersToRecord(res.headers), + body, + timeMs, + size: new TextEncoder().encode(body).length, + }; + }, + + ws(spec: WsSpec): WsSession { + const socket = new WebSocket(spec.url, spec.protocols); + const messageCbs: Array<(data: string) => void> = []; + const statusCbs: Array<(s: string) => void> = []; + const emitStatus = (s: string) => statusCbs.forEach((cb) => cb(s)); + + emitStatus("connecting"); + socket.onopen = () => emitStatus("open"); + socket.onclose = () => emitStatus("closed"); + socket.onerror = () => emitStatus("error"); + socket.onmessage = (ev) => { + const data = typeof ev.data === "string" ? ev.data : String(ev.data); + messageCbs.forEach((cb) => cb(data)); + }; + + return { + onMessage(cb) { + messageCbs.push(cb); + }, + onStatus(cb) { + statusCbs.push(cb); + }, + send(data) { + socket.send(data); + }, + close() { + socket.close(); + }, + }; + }, +}; diff --git a/packages/transport/src/detect.ts b/packages/transport/src/detect.ts new file mode 100644 index 0000000..90f4076 --- /dev/null +++ b/packages/transport/src/detect.ts @@ -0,0 +1,41 @@ +import type { Transport } from "@opsxapi/core"; +import { browserTransport } from "./browser"; +import { ginProxyTransport } from "./ginProxy"; +import { tauriNativeTransport } from "./tauriNative"; + +export type TransportKind = "tauri-native" | "gin-proxy" | "browser"; + +export type DetectOptions = { + ginBaseUrl?: string; + /** Force a transport regardless of environment */ + force?: TransportKind; +}; + +function isTauri(): boolean { + if (typeof window === "undefined") return false; + const w = window as unknown as { __TAURI_INTERNALS__?: unknown }; + return Boolean(w.__TAURI_INTERNALS__); +} + +export function detectTransport(opts: DetectOptions = {}): { + kind: TransportKind; + transport: Transport; +} { + if (opts.force === "browser") { + return { kind: "browser", transport: browserTransport }; + } + if (opts.force === "tauri-native") { + return { kind: "tauri-native", transport: tauriNativeTransport }; + } + if (opts.force === "gin-proxy") { + const base = opts.ginBaseUrl ?? ""; + return { kind: "gin-proxy", transport: ginProxyTransport(base) }; + } + + if (isTauri()) { + return { kind: "tauri-native", transport: tauriNativeTransport }; + } + + const base = opts.ginBaseUrl ?? ""; + return { kind: "gin-proxy", transport: ginProxyTransport(base) }; +} diff --git a/packages/transport/src/ginProxy.ts b/packages/transport/src/ginProxy.ts new file mode 100644 index 0000000..c419fc6 --- /dev/null +++ b/packages/transport/src/ginProxy.ts @@ -0,0 +1,35 @@ +import type { HttpResult, HttpSpec, Transport, WsSession, WsSpec } from "@opsxapi/core"; +import { browserTransport } from "./browser"; + +/** + * HTTP goes through Gin POST /v1/proxy/http. + * WS (MVP): falls back to browser WebSocket — custom headers require + * desktop/Gin tunnel later (Week 2). + */ +export function ginProxyTransport(baseUrl: string): Transport { + const root = baseUrl.replace(/\/$/, ""); + + return { + async http(spec: HttpSpec): Promise { + const res = await fetch(`${root}/v1/proxy/http`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(spec), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`gin-proxy HTTP ${res.status}: ${text}`); + } + return (await res.json()) as HttpResult; + }, + + ws(spec: WsSpec): WsSession { + // TODO(week2): custom headers need desktop/Gin WS tunnel. + // For MVP, fall back to browser WebSocket (no custom headers). + console.warn( + "[opsxapi/transport] gin-proxy WS falls back to browser WebSocket; custom headers need desktop/Gin tunnel later", + ); + return browserTransport.ws(spec); + }, + }; +} diff --git a/packages/transport/src/index.ts b/packages/transport/src/index.ts new file mode 100644 index 0000000..28efa36 --- /dev/null +++ b/packages/transport/src/index.ts @@ -0,0 +1,29 @@ +export { browserTransport } from "./browser"; +export { ginProxyTransport } from "./ginProxy"; +export { tauriNativeTransport } from "./tauriNative"; +export { detectTransport } from "./detect"; +export type { TransportKind, DetectOptions } from "./detect"; + +export { + SseParser, + httpStreamViaGin, + httpStreamDirect, + openSse, +} from "./streaming"; +export type { + StreamStatus, + StreamMeta, + StreamSession, + StreamCallbacks, + SseEvent, + SseOptions, +} from "./streaming"; + +export { wsSmart, probeGin } from "./wsSmart"; +export type { WsSmartOptions, WsTunnelInfo } from "./wsSmart"; + +export { connectSocketIo } from "./socketio"; +export type { SocketIoSession, SocketIoStatus, SocketIoOptions } from "./socketio"; + +export { connectMqtt } from "./mqtt"; +export type { MqttSession, MqttStatus, MqttMessage, MqttOptions } from "./mqtt"; diff --git a/packages/transport/src/mqtt.ts b/packages/transport/src/mqtt.ts new file mode 100644 index 0000000..27b4d11 --- /dev/null +++ b/packages/transport/src/mqtt.ts @@ -0,0 +1,120 @@ +/** + * MQTT 客户端封装(动态加载 mqtt,浏览器走 ws/wss)。 + */ +export type MqttStatus = "connecting" | "open" | "reconnecting" | "closed" | "error"; + +export type MqttMessage = { + topic: string; + payload: string; + at: number; + qos: number; +}; + +export type MqttSession = { + publish(topic: string, payload: string, qos?: 0 | 1 | 2): void; + subscribe(topics: string[]): void; + unsubscribe(topics: string[]): void; + close(): void; +}; + +export type MqttOptions = { + brokerUrl: string; + username?: string; + password?: string; + clientId?: string; + /** 初始订阅 */ + topics: string[]; + qos?: 0 | 1 | 2; + onMessage(msg: MqttMessage): void; + onStatus?(s: MqttStatus, detail?: string): void; + onError?(msg: string): void; +}; + +export function connectMqtt(opts: MqttOptions): MqttSession { + type MqttClient = { + on(ev: string, cb: (...args: unknown[]) => void): void; + subscribe(topic: string | string[], opts?: { qos?: number }, cb?: (err: unknown) => void): void; + unsubscribe(topic: string | string[], cb?: () => void): void; + publish(topic: string, message: string | Uint8Array, opts?: { qos?: number }, cb?: (err: unknown) => void): void; + end(force?: boolean): void; + }; + let client: MqttClient | null = null; + let disposed = false; + + const topics = opts.topics.map((t) => t.trim()).filter(Boolean); + + void (async () => { + try { + opts.onStatus?.("connecting"); + const mqttMod = await import("mqtt"); + const connect = (mqttMod as unknown as { connect: typeof mqttMod.default.connect }).connect ?? mqttMod.default.connect; + const url = opts.brokerUrl.trim(); + if (!/^(wss?|mqtts?):\/\//i.test(url)) { + throw new Error("浏览器端 MQTT 需 ws:// 或 wss:// 地址(例如 wss://broker.emqx.io:8084/mqtt)"); + } + client = connect(url, { + clientId: opts.clientId || `opsxapi-${Math.random().toString(16).slice(2, 10)}`, + username: opts.username || undefined, + password: opts.password || undefined, + clean: true, + reconnectPeriod: 3000, + connectTimeout: 10_000, + }) as MqttClient; + + client.on("connect", () => { + opts.onStatus?.("open"); + if (topics.length) client?.subscribe(topics, { qos: opts.qos ?? 0 }); + }); + client.on("reconnect", () => opts.onStatus?.("reconnecting")); + client.on("close", () => opts.onStatus?.("closed")); + client.on("error", (...args: unknown[]) => { + const err = args[0]; + opts.onError?.(err instanceof Error ? err.message : String(err)); + opts.onStatus?.("error"); + }); + client.on("message", (...args: unknown[]) => { + const topic = String(args[0]); + const payload = args[1] as Uint8Array; + const packet = args[2] as { qos?: number } | undefined; + const qos = packet?.qos ?? 0; + let text: string; + try { + text = new TextDecoder().decode(payload); + } catch { + text = Array.from(payload ?? []).join(","); + } + opts.onMessage({ topic, payload: text, at: Date.now(), qos }); + }); + + if (disposed) client.end(true); + } catch (e) { + opts.onError?.(`mqtt.js 加载/连接失败:${e instanceof Error ? e.message : String(e)}`); + opts.onStatus?.("error"); + } + })(); + + return { + publish(topic, payload, qos) { + if (!client) { + opts.onError?.("MQTT 尚未连接"); + return; + } + client.publish(topic.trim(), payload, { qos: qos ?? opts.qos ?? 0 }, (err) => { + if (err) opts.onError?.(`发布失败:${String(err)}`); + }); + }, + subscribe(list) { + const t = list.map((x) => x.trim()).filter(Boolean); + if (t.length) client?.subscribe(t, { qos: opts.qos ?? 0 }); + }, + unsubscribe(list) { + const t = list.map((x) => x.trim()).filter(Boolean); + if (t.length) client?.unsubscribe(t); + }, + close() { + disposed = true; + client?.end(true); + opts.onStatus?.("closed"); + }, + }; +} diff --git a/packages/transport/src/socketio.ts b/packages/transport/src/socketio.ts new file mode 100644 index 0000000..c70352a --- /dev/null +++ b/packages/transport/src/socketio.ts @@ -0,0 +1,92 @@ +/** + * Socket.IO 客户端封装(动态加载 socket.io-client)。 + */ +export type SocketIoStatus = "connecting" | "open" | "closed" | "error"; + +export type SocketIoSession = { + emit(event: string, args: unknown[]): void; + close(): void; +}; + +export type SocketIoOptions = { + url: string; + /** 附加选项 JSON:{ path?, transports?, auth?, ... } */ + optionsJson?: string; + /** 监听事件列表;空数组 = onAny 全收 */ + listenEvents: string[]; + onEvent(event: string, args: unknown[]): void; + onStatus?(s: SocketIoStatus, detail?: string): void; + onError?(msg: string): void; +}; + +type IoSocket = { + emit(ev: string, ...args: unknown[]): void; + on(ev: string, cb: (...args: unknown[]) => void): void; + onAny(cb: (ev: string, ...args: unknown[]) => void): void; + disconnect(): void; +}; + +export function connectSocketIo(opts: SocketIoOptions): SocketIoSession { + let socket: IoSocket | null = null; + let disposed = false; + + void (async () => { + try { + opts.onStatus?.("connecting"); + const { io } = await import("socket.io-client"); + const extra: Record = {}; + if (opts.optionsJson?.trim()) { + try { + Object.assign(extra, JSON.parse(opts.optionsJson)); + } catch { + opts.onError?.("连接选项不是合法 JSON,已忽略"); + } + } + const s: IoSocket = io(opts.url, { + transports: ["websocket", "polling"], + reconnection: false, + ...extra, + }) as unknown as IoSocket; + socket = s; + + s.on("connect", () => opts.onStatus?.("open")); + s.on("disconnect", (...args: unknown[]) => + opts.onStatus?.("closed", String(args[0] ?? "")), + ); + s.on("connect_error", (...args: unknown[]) => { + const err = args[0]; + const msg = err instanceof Error ? err.message : String(err); + opts.onError?.(msg); + opts.onStatus?.("error", msg); + }); + + const events = opts.listenEvents.map((e) => e.trim()).filter(Boolean); + if (events.length === 0 || events.includes("*")) { + s.onAny((ev, ...args) => opts.onEvent(ev, args)); + } else { + for (const ev of events) { + s.on(ev, (...args: unknown[]) => opts.onEvent(ev, args)); + } + } + if (disposed) s.disconnect(); + } catch (e) { + opts.onError?.(`socket.io-client 加载/连接失败:${e instanceof Error ? e.message : String(e)}`); + opts.onStatus?.("error"); + } + })(); + + return { + emit(event, args) { + if (!socket) { + opts.onError?.("Socket.IO 尚未连接"); + return; + } + socket.emit(event, ...args); + }, + close() { + disposed = true; + socket?.disconnect(); + opts.onStatus?.("closed"); + }, + }; +} diff --git a/packages/transport/src/streaming.ts b/packages/transport/src/streaming.ts new file mode 100644 index 0000000..6d9669f --- /dev/null +++ b/packages/transport/src/streaming.ts @@ -0,0 +1,351 @@ +/** + * 流式 HTTP / SSE 客户端。 + * - 直连模式:fetch + ReadableStream(目标站需允许 CORS) + * - 代理模式:Gin POST /v1/proxy/http/stream(NDJSON,穿透 SSE/chunked) + */ +import type { HttpSpec } from "@opsxapi/core"; + +export type StreamStatus = "connecting" | "open" | "closed" | "error"; + +export type StreamMeta = { + status: number; + headers: Record; +}; + +export type StreamSession = { + abort(): void; +}; + +export type StreamCallbacks = { + onMeta?(meta: StreamMeta): void; + onChunk?(text: string): void; + onEnd?(info: { timeMs: number; size: number }): void; + onStatus?(status: StreamStatus): void; + onError?(message: string): void; +}; + +/** SSE 帧 */ +export type SseEvent = { + event: string; + data: string; + id?: string; +}; + +/** 增量 SSE 解析器:喂任意分片,吐完整事件 */ +export class SseParser { + private buffer = ""; + private event = "message"; + private dataLines: string[] = []; + private lastId: string | undefined; + + constructor( + private readonly emit: (ev: SseEvent) => void, + private readonly onId?: (id: string) => void, + ) {} + + feed(text: string): void { + this.buffer += text; + let idx: number; + while ((idx = this.buffer.indexOf("\n")) >= 0) { + const line = this.buffer.slice(0, idx).replace(/\r$/, ""); + this.buffer = this.buffer.slice(idx + 1); + this.handleLine(line); + } + // 兼容无结尾换行的流结束 + } + + flush(): void { + if (this.buffer) { + this.handleLine(this.buffer.replace(/\r$/, "")); + this.buffer = ""; + } + this.dispatch(); + } + + private handleLine(line: string): void { + if (line === "") { + this.dispatch(); + return; + } + if (line.startsWith(":")) return; // 注释/心跳 + const colon = line.indexOf(":"); + const field = colon < 0 ? line : line.slice(0, colon); + let value = colon < 0 ? "" : line.slice(colon + 1); + if (value.startsWith(" ")) value = value.slice(1); + switch (field) { + case "event": + this.event = value || "message"; + break; + case "data": + this.dataLines.push(value); + break; + case "id": + this.lastId = value; + this.onId?.(value); + break; + default: + break; // retry 等忽略 + } + } + + private dispatch(): void { + if (this.dataLines.length === 0) return; + const ev: SseEvent = { + event: this.event, + data: this.dataLines.join("\n"), + id: this.lastId, + }; + this.event = "message"; + this.dataLines = []; + this.emit(ev); + } +} + +/** 通过 Gin 流式代理发送(NDJSON 协议) */ +export async function httpStreamViaGin( + ginBase: string, + spec: HttpSpec, + cb: StreamCallbacks, +): Promise { + const controller = new AbortController(); + cb.onStatus?.("connecting"); + const started = performance.now(); + try { + const res = await fetch(`${ginBase}/v1/proxy/http/stream`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(spec), + signal: controller.signal, + }); + if (!res.ok || !res.body) { + const text = await res.text().catch(() => ""); + throw new Error(`代理流式请求失败 ${res.status}: ${text.slice(0, 200)}`); + } + await pumpNdjson(res.body, cb, started); + } catch (e) { + if (controller.signal.aborted) { + cb.onStatus?.("closed"); + return { abort: () => controller.abort() }; + } + cb.onError?.(e instanceof Error ? e.message : String(e)); + cb.onStatus?.("error"); + } + return { abort: () => controller.abort() }; +} + +async function pumpNdjson( + body: ReadableStream, + cb: StreamCallbacks, + started: number, +): Promise { + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let size = 0; + let metaSeen = false; + + const handleLine = (line: string) => { + if (!line.trim()) return; + let ev: { + type: string; + status?: number; + headers?: Record; + data?: string; + message?: string; + timeMs?: number; + size?: number; + }; + try { + ev = JSON.parse(line); + } catch { + return; + } + switch (ev.type) { + case "meta": + metaSeen = true; + cb.onMeta?.({ status: ev.status ?? 0, headers: ev.headers ?? {} }); + cb.onStatus?.("open"); + break; + case "chunk": { + size += ev.size ?? 0; + if (ev.data) { + const binary = atob(ev.data); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + cb.onChunk?.(decoder.decode(bytes, { stream: true })); + } + break; + } + case "end": + cb.onEnd?.({ timeMs: ev.timeMs ?? Math.round(performance.now() - started), size: ev.size ?? size }); + cb.onStatus?.("closed"); + break; + case "error": + cb.onError?.(ev.message ?? "代理流错误"); + cb.onStatus?.("error"); + break; + default: + break; + } + }; + + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + let idx: number; + while ((idx = buffer.indexOf("\n")) >= 0) { + handleLine(buffer.slice(0, idx)); + buffer = buffer.slice(idx + 1); + } + } + if (buffer) handleLine(buffer); + if (!metaSeen) { + cb.onEnd?.({ timeMs: Math.round(performance.now() - started), size }); + cb.onStatus?.("closed"); + } + } finally { + reader.releaseLock?.(); + } +} + +/** 直连流式 fetch(CORS 允许时) */ +export async function httpStreamDirect(spec: HttpSpec, cb: StreamCallbacks): Promise { + const controller = new AbortController(); + cb.onStatus?.("connecting"); + const started = performance.now(); + const url = new URL(spec.url); + if (spec.query) { + for (const [k, v] of Object.entries(spec.query)) { + if (v !== "") url.searchParams.set(k, v); + } + } + try { + const init: RequestInit = { + method: spec.method, + headers: { ...spec.headers }, + signal: controller.signal, + }; + if (spec.body && spec.body.mode !== "none" && spec.method !== "GET") { + init.body = spec.body.raw ?? ""; + } + const res = await fetch(url.toString(), init); + const headers: Record = {}; + res.headers.forEach((v, k) => { + headers[k] = v; + }); + cb.onMeta?.({ status: res.status, headers }); + cb.onStatus?.("open"); + + if (!res.body) { + const text = await res.text(); + cb.onChunk?.(text); + cb.onEnd?.({ timeMs: Math.round(performance.now() - started), size: text.length }); + cb.onStatus?.("closed"); + return { abort: () => controller.abort() }; + } + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let size = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + size += value?.length ?? 0; + cb.onChunk?.(decoder.decode(value, { stream: true })); + } + cb.onEnd?.({ timeMs: Math.round(performance.now() - started), size }); + cb.onStatus?.("closed"); + } catch (e) { + if (controller.signal.aborted) { + cb.onStatus?.("closed"); + } else { + cb.onError?.(e instanceof Error ? e.message : String(e)); + cb.onStatus?.("error"); + } + } + return { abort: () => controller.abort() }; +} + +export type SseOptions = { + url: string; + headers: Record; + query?: Record; + lastEventId?: string; + /** 强制走 Gin 代理(目标站 CORS 不允许时) */ + forceProxy?: boolean; + ginBase?: string; + onEvent(ev: SseEvent): void; + onStatus?(s: StreamStatus, meta?: StreamMeta): void; + onError?(msg: string, willFallback: boolean): void; +}; + +/** SSE 会话:先试直连,失败自动切代理 */ +export function openSse(opts: SseOptions): StreamSession { + const controller = { aborted: false, abortFns: [] as Array<() => void> }; + const run = async () => { + const parser = new SseParser((ev) => opts.onEvent(ev)); + + const callbacks: StreamCallbacks = { + onMeta: (meta) => opts.onStatus?.("open", meta), + onChunk: (text) => parser.feed(text), + onEnd: () => { + parser.flush(); + opts.onStatus?.("closed"); + }, + onError: (msg) => { + opts.onError?.(msg, false); + opts.onStatus?.("error"); + }, + }; + + const specHeaders = { ...opts.headers }; + if (opts.lastEventId) specHeaders["Last-Event-ID"] = opts.lastEventId; + + if (!opts.forceProxy) { + let direct: StreamSession | null = null; + let directFailed = false; + const directCb: StreamCallbacks = { + ...callbacks, + onError: (msg) => { + directFailed = true; + opts.onError?.(msg, true); + }, + }; + direct = await httpStreamDirect( + { + method: "GET", + url: opts.url, + headers: specHeaders, + query: opts.query, + body: { mode: "none" }, + }, + directCb, + ); + controller.abortFns.push(() => direct?.abort()); + // 直连迅速失败(连接层错误)→ 代理兜底 + if (!directFailed) return; + } + if (controller.aborted) return; + const proxy = await httpStreamViaGin( + opts.ginBase ?? "", + { + method: "GET", + url: opts.url, + headers: specHeaders, + query: opts.query, + body: { mode: "none" }, + }, + callbacks, + ); + controller.abortFns.push(() => proxy.abort()); + }; + void run(); + return { + abort() { + controller.aborted = true; + for (const fn of controller.abortFns) fn(); + opts.onStatus?.("closed"); + }, + }; +} diff --git a/packages/transport/src/tauriNative.ts b/packages/transport/src/tauriNative.ts new file mode 100644 index 0000000..518514f --- /dev/null +++ b/packages/transport/src/tauriNative.ts @@ -0,0 +1,68 @@ +import type { HttpResult, HttpSpec, Transport, WsSession, WsSpec } from "@opsxapi/core"; + +async function invoke(cmd: string, args?: Record): Promise { + try { + const { invoke: tauriInvoke } = await import("@tauri-apps/api/core"); + return await tauriInvoke(cmd, args); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + throw new Error( + `tauri-native invoke("${cmd}") failed (are you running inside OpsxAPI desktop?): ${msg}`, + ); + } +} + +export const tauriNativeTransport: Transport = { + async http(spec: HttpSpec): Promise { + return invoke("http_send", { spec }); + }, + + ws(spec: WsSpec): WsSession { + const messageCbs: Array<(data: string) => void> = []; + const statusCbs: Array<(s: string) => void> = []; + let sessionId: string | null = null; + + const emitStatus = (s: string) => statusCbs.forEach((cb) => cb(s)); + + void (async () => { + try { + emitStatus("connecting"); + sessionId = await invoke("ws_connect", { spec }); + emitStatus("open"); + // Full event listen pipeline is Week 2; stub keeps UI usable. + } catch (e) { + emitStatus("error"); + messageCbs.forEach((cb) => + cb(`[ws_connect error] ${e instanceof Error ? e.message : String(e)}`), + ); + } + })(); + + return { + onMessage(cb) { + messageCbs.push(cb); + }, + onStatus(cb) { + statusCbs.push(cb); + }, + send(data) { + if (!sessionId) { + messageCbs.forEach((cb) => cb("[ws] not connected")); + return; + } + void invoke("ws_send", { sessionId, data }).catch((e) => { + messageCbs.forEach((cb) => + cb(`[ws_send error] ${e instanceof Error ? e.message : String(e)}`), + ); + }); + }, + close() { + if (!sessionId) return; + void invoke("ws_close", { sessionId }).finally(() => { + sessionId = null; + emitStatus("closed"); + }); + }, + }; + }, +}; diff --git a/packages/transport/src/wsSmart.ts b/packages/transport/src/wsSmart.ts new file mode 100644 index 0000000..7757740 --- /dev/null +++ b/packages/transport/src/wsSmart.ts @@ -0,0 +1,190 @@ +/** + * 智能 WebSocket:无自定义 Header → 浏览器直连;有 Header → 借道 Gin WS 隧道。 + */ +import type { WsSession, WsSpec } from "@opsxapi/core"; + +export type WsTunnelInfo = { + mode: "direct" | "tunnel"; + /** tunnel 模式的会话 id */ + sessionId?: string; +}; + +export type WsSmartOptions = { + /** Gin 基地址(默认同源 /v1 反代) */ + ginBase?: string; + /** 有自定义 header 时强制走隧道(默认自动) */ + forceTunnel?: boolean; + onInfo?(info: WsTunnelInfo): void; +}; + +type Spec = WsSpec & { headers?: Record }; + +export function wsSmart(spec: Spec, opts: WsSmartOptions = {}): WsSession { + const headers = spec.headers ?? {}; + const hasCustomHeaders = Object.keys(headers).length > 0; + if (hasCustomHeaders || opts.forceTunnel) { + return tunnelSession(spec, headers, opts); + } + return directSession(spec); +} + +function directSession(spec: Spec): WsSession { + const protocols = spec.protocols?.filter(Boolean); + const socket = new WebSocket(spec.url, protocols?.length ? protocols : undefined); + return wrapSocket(socket); +} + +function wrapSocket(socket: WebSocket): WsSession { + const messageCbs: Array<(data: string) => void> = []; + const statusCbs: Array<(s: string) => void> = []; + const emitStatus = (s: string) => statusCbs.forEach((cb) => cb(s)); + + socket.onopen = () => emitStatus("open"); + socket.onclose = () => emitStatus("closed"); + socket.onerror = () => emitStatus("error"); + socket.onmessage = (ev) => { + let data: string; + if (typeof ev.data === "string") { + data = ev.data; + } else if (ev.data instanceof ArrayBuffer) { + data = new TextDecoder().decode(ev.data); + } else if (ev.data instanceof Blob) { + // Blob 异步读,转同步回调 + void ev.data.text().then((t) => messageCbs.forEach((cb) => cb(t))); + return; + } else { + data = String(ev.data); + } + messageCbs.forEach((cb) => cb(data)); + }; + + return { + onMessage(cb) { + messageCbs.push(cb); + }, + onStatus(cb) { + statusCbs.push(cb); + }, + send(data) { + socket.send(data); + }, + close() { + socket.close(); + }, + }; +} + +function tunnelSession(spec: Spec, headers: Record, opts: WsSmartOptions): WsSession { + const ginBase = opts.ginBase ?? ""; + const messageCbs: Array<(data: string) => void> = []; + const statusCbs: Array<(s: string) => void> = []; + const emitStatus = (s: string) => statusCbs.forEach((cb) => cb(s)); + + let socket: WebSocket | null = null; + let sessionId: string | null = null; + let disposed = false; + + const wsBase = ginBase + .replace(/^http:\/\//i, "ws://") + .replace(/^https:\/\//i, "wss://"); + + emitStatus("connecting"); + + void (async () => { + try { + const res = await fetch(`${ginBase}/v1/proxy/ws/connect`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: spec.url, + headers, + protocols: spec.protocols ?? [], + }), + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`WS 隧道连接失败 ${res.status}: ${text.slice(0, 200)}`); + } + const data = (await res.json()) as { sessionId: string }; + sessionId = data.sessionId; + if (disposed) { + void closeTunnel(ginBase, sessionId); + return; + } + opts.onInfo?.({ mode: "tunnel", sessionId }); + + socket = new WebSocket(`${wsBase}/v1/proxy/ws/attach?sessionId=${encodeURIComponent(sessionId)}`); + socket.onmessage = (ev) => { + if (typeof ev.data !== "string") return; + // 隧道控制帧(json)与透传数据区分 + if (ev.data.startsWith('{"type":"open"}')) { + emitStatus("open"); + return; + } + if (ev.data.startsWith('{"type":"closed"')) { + emitStatus("closed"); + return; + } + messageCbs.forEach((cb) => cb(ev.data as string)); + }; + socket.onclose = () => emitStatus("closed"); + socket.onerror = () => emitStatus("error"); + } catch (e) { + messageCbs.forEach((cb) => cb(`[ws tunnel] ${e instanceof Error ? e.message : String(e)}`)); + emitStatus("error"); + } + })(); + + return { + onMessage(cb) { + messageCbs.push(cb); + }, + onStatus(cb) { + statusCbs.push(cb); + }, + send(data) { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send(data); + } else { + messageCbs.forEach((cb) => cb("[ws tunnel] 未连接,无法发送")); + } + }, + close() { + disposed = true; + socket?.close(); + if (sessionId) void closeTunnel(ginBase, sessionId); + emitStatus("closed"); + }, + }; +} + +async function closeTunnel(ginBase: string, sessionId: string): Promise { + try { + await fetch(`${ginBase}/v1/proxy/ws/close`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId }), + }); + } catch { + /* 后台会话回收兜底 */ + } +} + +/** Gin 是否可达(用于 UI 提示) */ +let ginProbeCache: { base: string; ok: boolean; at: number } | null = null; + +export async function probeGin(base = "", maxAgeMs = 15_000): Promise { + const now = Date.now(); + if (ginProbeCache && now - ginProbeCache.at < maxAgeMs && ginProbeCache.base === base) { + return ginProbeCache.ok; + } + try { + const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(1500) }); + const ok = res.ok; + ginProbeCache = { base, ok, at: now }; + return ok; + } catch { + ginProbeCache = { base, ok: false, at: now }; + return false; + } +} diff --git a/packages/transport/tsconfig.json b/packages/transport/tsconfig.json new file mode 100644 index 0000000..a47cede --- /dev/null +++ b/packages/transport/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src" + ] +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..629f267 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@opsxapi/ui", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@opsxapi/core": "*", + "@opsxapi/transport": "*", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "typescript": "^5.8.2" + } +} diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx new file mode 100644 index 0000000..d15261c --- /dev/null +++ b/packages/ui/src/App.tsx @@ -0,0 +1,118 @@ +/** + * OpsxAPI 工作台入口:组装顶栏 / 侧栏 / 标签页 / 编辑器 / 对话框。 + */ +import { findNode } from "@opsxapi/core"; +import { useEffect } from "react"; +import { closeSession } from "./send"; +import { Icon } from "./components/icons"; +import { Logo } from "./components/Logo"; +import { useWorkspace } from "./workspaceStore"; +import { Sidebar } from "./components/Sidebar"; +import { TabBar } from "./components/TabBar"; +import { TopBar } from "./components/TopBar"; +import { GraphqlEditor } from "./components/editors/GraphqlEditor"; +import { HttpEditor } from "./components/editors/HttpEditor"; +import { MqttEditor } from "./components/editors/MqttEditor"; +import { SocketIoEditor } from "./components/editors/SocketIoEditor"; +import { SseEditor } from "./components/editors/SseEditor"; +import { WebsocketEditor } from "./components/editors/WebsocketEditor"; +import { ExportDialog } from "./components/dialogs/ExportDialog"; +import { ImportDialog } from "./components/dialogs/ImportDialog"; +import { + EnvironmentsDialog, + NewItemDialog, + RenameDialog, + SettingsDialog, +} from "./components/dialogs/WorkspaceDialogs"; + +export function OpsxWorkspace() { + const init = useWorkspace((s) => s.init); + const inited = useWorkspace((s) => s.collections.length > 0 || s.environments.length > 0); + const collections = useWorkspace((s) => s.collections); + const activeTabId = useWorkspace((s) => s.activeTabId); + const dialog = useWorkspace((s) => s.dialog); + const openTab = useWorkspace((s) => s.openTab); + + useEffect(() => { + init(); + }, [init]); + + // 关闭标签页时断开实时会话 + useEffect(() => { + useWorkspace.subscribe((state, prev) => { + const closed = prev.openTabIds.filter((id) => !state.openTabIds.includes(id)); + for (const id of closed) closeSession(id); + }); + }, []); + + const activeRef = activeTabId ? findNode(collections, activeTabId) : null; + const activeNode = activeRef?.node; + + return ( +
    + +
    + +
    + + {inited && activeNode === undefined && ( + collections[0] && openTab(collections[0].id)} /> + )} + {activeNode && activeNode.type === "request" && ( + <> + {activeNode.kind === "http" && } + {activeNode.kind === "graphql" && } + {activeNode.kind === "websocket" && } + {activeNode.kind === "sse" && } + {activeNode.kind === "socketio" && } + {activeNode.kind === "mqtt" && } + + )} + {activeNode && activeNode.type === "folder" && ( + undefined} /> + )} +
    +
    + + {dialog?.type === "new" && } + {dialog?.type === "import" && } + {dialog?.type === "export" && } + {dialog?.type === "environments" && } + {dialog?.type === "settings" && } + {dialog?.type === "rename" && } +
    + ); +} + +function EmptyState({ onQuick }: { onQuick: (kind: string) => void }) { + const setDialog = useWorkspace((s) => s.setDialog); + void onQuick; + return ( +
    +
    +
    +
    OpsxAPI · 接口工作台
    +
    + 从左侧选择接口,或 + + / + +
    +
    + HTTP · GraphQL · WebSocket · SSE · Socket.IO · MQTT +
    + 支持 OpenAPI / Postman / Apifox / cURL / HAR 等格式导入导出 +
    +
    +
    + ); +} diff --git a/packages/ui/src/components/EnvBadge.tsx b/packages/ui/src/components/EnvBadge.tsx new file mode 100644 index 0000000..aa4be97 --- /dev/null +++ b/packages/ui/src/components/EnvBadge.tsx @@ -0,0 +1,61 @@ +/** + * 环境徽标:取环境名首字的彩色单字标(颜色按 id 稳定分配)。 + * 左栏列表、顶栏切换器、环境弹窗共用,帮助一眼识别当前环境。 + */ +import type { CSSProperties } from "react"; + +const PALETTE = [ + "#3b82f6", // blue + "#8b5cf6", // violet + "#10b981", // emerald + "#f59e0b", // amber + "#f43f5e", // rose + "#06b6d4", // cyan + "#ec4899", // pink + "#84cc16", // lime +]; + +function hashColor(id: string): string { + let h = 0; + for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0; + return PALETTE[h % PALETTE.length]; +} + +function firstChar(name: string): string { + const trimmed = (name || "").trim(); + if (!trimmed) return "环"; + // 取第一个字符(含中文);英文取大写 + const ch = Array.from(trimmed)[0]; + return /[a-z]/.test(ch) ? ch.toUpperCase() : ch; +} + +export function EnvBadge({ + envId, + name, + size = 24, + style, +}: { + envId: string; + name: string; + size?: number; + style?: CSSProperties; +}) { + const color = hashColor(envId); + return ( + + ); +} diff --git a/packages/ui/src/components/Logo.tsx b/packages/ui/src/components/Logo.tsx new file mode 100644 index 0000000..ccee1eb --- /dev/null +++ b/packages/ui/src/components/Logo.tsx @@ -0,0 +1,22 @@ +/** + * OpsxAPI 产品标识 —— 蓝→青渐变圆角方块 + 双向请求/响应箭头。 + */ +export function Logo({ size = 26, className }: { size?: number; className?: string }) { + return ( + + + + + + + + + + + + + + + + ); +} diff --git a/packages/ui/src/components/Sidebar.tsx b/packages/ui/src/components/Sidebar.tsx new file mode 100644 index 0000000..f5abc23 --- /dev/null +++ b/packages/ui/src/components/Sidebar.tsx @@ -0,0 +1,306 @@ +/** + * 左侧集合树:搜索 / 目录 / 请求 / 右键菜单 / 拖拽移动 / 历史记录。 + */ +import type { Collection, CollectionNode, RequestKind } from "@opsxapi/core"; +import { KIND_LABELS, findNode, searchTree } from "@opsxapi/core"; +import { useMemo, useState } from "react"; +import { useWorkspace } from "../workspaceStore"; +import { ContextMenu, cx, formatTime, type MenuItem } from "./common"; +import { Icon, type IconName } from "./icons"; +import { Logo } from "./Logo"; + +const KIND_ICONS: Record = { + http: "globe", + graphql: "hexagon", + websocket: "zap", + sse: "radio", + socketio: "plug-zap", + mqtt: "mailbox", +}; + +export function Sidebar() { + const collections = useWorkspace((s) => s.collections); + const search = useWorkspace((s) => s.search); + const setSearch = useWorkspace((s) => s.setSearch); + const history = useWorkspace((s) => s.history); + const clearHistory = useWorkspace((s) => s.clearHistory); + const openTab = useWorkspace((s) => s.openTab); + const [showHistory, setShowHistory] = useState(true); + const [menu, setMenu] = useState<{ x: number; y: number; items: MenuItem[] } | null>(null); + + const hitIds = useMemo(() => searchTree(collections, search), [collections, search]); + + return ( + + ); +} + +function TreeChildren({ + nodes, + depth, + hitIds, + collectionId, + onMenu, +}: { + nodes: CollectionNode[]; + depth: number; + hitIds: Set; + collectionId: string; + onMenu: (m: { x: number; y: number; items: MenuItem[] } | null) => void; +}) { + const expanded = useWorkspace((s) => s.expanded); + const toggleExpanded = useWorkspace((s) => s.toggleExpanded); + const search = useWorkspace((s) => s.search); + + return ( + <> + {nodes + .filter((n) => search === "" || hitIds.has(n.id)) + .map((node) => ( +
    + + {node.type === "folder" && expanded[node.id] && ( + + )} +
    + ))} + + ); +} + +function TreeRow({ + node, + depth, + hitIds, + collectionId, + isCollection = false, + onMenu, +}: { + node: CollectionNode | Collection; + depth: number; + hitIds: Set; + collectionId: string; + isCollection?: boolean; + onMenu: (m: { x: number; y: number; items: MenuItem[] } | null) => void; +}) { + const activeTabId = useWorkspace((s) => s.activeTabId); + const expanded = useWorkspace((s) => s.expanded); + const toggleExpanded = useWorkspace((s) => s.toggleExpanded); + const openTab = useWorkspace((s) => s.openTab); + const removeNodeById = useWorkspace((s) => s.removeNodeById); + const duplicateNodeById = useWorkspace((s) => s.duplicateNodeById); + const addRequest = useWorkspace((s) => s.addRequest); + const addFolder = useWorkspace((s) => s.addFolder); + const addCollection = useWorkspace((s) => s.addCollection); + const setDialog = useWorkspace((s) => s.setDialog); + const moveNodeTo = useWorkspace((s) => s.moveNodeTo); + + const [dragOver, setDragOver] = useState(false); + const isFolder = node.type === "folder" || node.type === "collection"; + const requestNode = node.type === "request" ? node : null; + const isOpen = isFolder && expanded[node.id]; + const isActive = requestNode !== null && node.id === activeTabId; + + const buildMenu = (): MenuItem[] => { + const items: MenuItem[] = []; + if (isCollection) { + items.push( + { label: "新建请求", icon: , onClick: () => setDialog({ type: "new", collectionId, parentId: null }) }, + { label: "新建目录", icon: , onClick: () => addFolder(collectionId, null) }, + { type: "sep" }, + { label: "重命名", icon: , onClick: () => setDialog({ type: "rename", id: node.id }) }, + { label: "导出该集合…", icon: , onClick: () => setDialog({ type: "export", collectionId }) }, + { type: "sep" }, + { label: "新建集合", icon: , onClick: () => addCollection() }, + ); + return items; + } + if (isFolder) { + items.push( + { label: "新建请求", icon: , onClick: () => setDialog({ type: "new", collectionId, parentId: node.id }) }, + { label: "新建子目录", icon: , onClick: () => addFolder(collectionId, node.id) }, + { type: "sep" }, + { label: "重命名", icon: "✎", onClick: () => setDialog({ type: "rename", id: node.id }) }, + { + label: "复制目录", + icon: , + onClick: () => duplicateNodeById(node.id), + }, + { type: "sep" }, + { label: "删除目录", icon: , danger: true, onClick: () => removeNodeById(node.id) }, + ); + return items; + } + // request + if (node.kind === "http") { + items.unshift({ label: "打开", icon: , onClick: () => openTab(node.id) }); + items.push( + { label: "重命名", icon: , onClick: () => setDialog({ type: "rename", id: node.id }) }, + { label: "复制接口", icon: , onClick: () => duplicateNodeById(node.id) }, + { type: "sep" }, + { label: "删除", icon: , danger: true, onClick: () => removeNodeById(node.id) }, + ); + } else { + items.push( + { label: "打开", icon: , onClick: () => openTab(node.id) }, + { label: "重命名", icon: , onClick: () => setDialog({ type: "rename", id: node.id }) }, + { label: "复制", icon: , onClick: () => duplicateNodeById(node.id) }, + { type: "sep" }, + { label: "删除", icon: , danger: true, onClick: () => removeNodeById(node.id) }, + ); + } + return items; + }; + + const label = {node.name}; + + return ( +
    { + e.dataTransfer.setData("text/opsxapi-node", node.id); + e.dataTransfer.effectAllowed = "move"; + }} + onDragOver={(e) => { + if (!isFolder) return; + e.preventDefault(); + setDragOver(true); + }} + onDragLeave={() => setDragOver(false)} + onDrop={(e) => { + e.preventDefault(); + setDragOver(false); + if (!isFolder) return; + const dragId = e.dataTransfer.getData("text/opsxapi-node"); + if (dragId && dragId !== node.id) { + const parentId = isCollection ? null : node.id; + moveNodeTo(dragId, collectionId, parentId); + if (parentId) toggleExpanded(parentId, true); + } + }} + onClick={() => { + if (isFolder) toggleExpanded(node.id); + else openTab(node.id); + }} + onContextMenu={(e) => { + e.preventDefault(); + onMenu({ x: e.clientX, y: e.clientY, items: buildMenu() }); + }} + > + {isFolder ? ( + + ) : ( + + )} + {node.type === "collection" ? ( + + ) : node.type === "folder" ? ( + + ) : requestNode && requestNode.kind === "http" ? ( + + {requestNode.data.method} + + ) : requestNode ? ( + + ) : null} + {label} +
    + ); +} diff --git a/packages/ui/src/components/TabBar.tsx b/packages/ui/src/components/TabBar.tsx new file mode 100644 index 0000000..e357960 --- /dev/null +++ b/packages/ui/src/components/TabBar.tsx @@ -0,0 +1,105 @@ +/** + * 请求标签页栏。 + */ +import { KIND_LABELS, findNode } from "@opsxapi/core"; +import { useState } from "react"; +import { useWorkspace } from "../workspaceStore"; +import { ContextMenu, cx, type MenuItem } from "./common"; + +const KIND_TAB_ICON: Record = { + http: "", + graphql: "◈", + websocket: "⚡", + sse: "📡", + socketio: "🔌", + mqtt: "📮", +}; + +export function TabBar() { + const collections = useWorkspace((s) => s.collections); + const openTabIds = useWorkspace((s) => s.openTabIds); + const activeTabId = useWorkspace((s) => s.activeTabId); + const setActiveTab = useWorkspace((s) => s.setActiveTab); + const closeTab = useWorkspace((s) => s.closeTab); + const closeOtherTabs = useWorkspace((s) => s.closeOtherTabs); + const [menu, setMenu] = useState<{ x: number; y: number; id: string } | null>(null); + + const nodes = openTabIds + .map((id) => findNode(collections, id)?.node) + .filter((n): n is NonNullable => Boolean(n)); + + const menuItems = (id: string): MenuItem[] => [ + { label: "关闭", onClick: () => closeTab(id) }, + { label: "关闭其他", onClick: () => closeOtherTabs(id) }, + { + label: "关闭全部", + onClick: () => { + for (const n of nodes) closeTab(n.id); + }, + }, + ]; + + return ( +
    { + if (e.target === e.currentTarget) e.preventDefault(); + }} + > + {nodes.length === 0 && ( +
    没有打开的请求
    + )} + {nodes.map((node) => { + const active = node.id === activeTabId; + const runtime = useWorkspace.getState().runtime[node.id]; + const busy = runtime?.loading || runtime?.status === "connecting" || runtime?.status === "open"; + return ( +
    setActiveTab(node.id)} + onAuxClick={(e) => e.button === 1 && closeTab(node.id)} + onContextMenu={(e) => { + e.preventDefault(); + setMenu({ x: e.clientX, y: e.clientY, id: node.id }); + }} + title={node.name} + > + {node.type === "request" && node.kind === "http" ? ( + {node.data.method} + ) : node.type === "request" ? ( + {KIND_TAB_ICON[node.kind]} + ) : null} + {node.name} + {busy && } + +
    + ); + })} + setMenu(null)} + /> +
    + ); +} diff --git a/packages/ui/src/components/TopBar.tsx b/packages/ui/src/components/TopBar.tsx new file mode 100644 index 0000000..a081eb7 --- /dev/null +++ b/packages/ui/src/components/TopBar.tsx @@ -0,0 +1,215 @@ +/** + * 顶栏:logo / 新建 / 导入导出 / 环境 / 主题 / 设置。 + */ +import { detectTransport } from "@opsxapi/transport"; +import { useEffect, useMemo, useState } from "react"; +import { useWorkspace } from "../workspaceStore"; +import { ContextMenu, cx, type MenuItem } from "./common"; +import { EnvBadge } from "./EnvBadge"; +import { Icon, type IconName } from "./icons"; +import { Logo } from "./Logo"; + +const NEW_ITEMS: { kind: string; label: string; icon: IconName }[] = [ + { kind: "http", label: "HTTP 请求", icon: "globe" }, + { kind: "graphql", label: "GraphQL", icon: "hexagon" }, + { kind: "websocket", label: "WebSocket", icon: "zap" }, + { kind: "sse", label: "SSE", icon: "radio" }, + { kind: "socketio", label: "Socket.IO", icon: "plug-zap" }, + { kind: "mqtt", label: "MQTT", icon: "mailbox" }, +]; + +export function TopBar() { + const collections = useWorkspace((s) => s.collections); + const environments = useWorkspace((s) => s.environments); + const activeEnvironmentId = useWorkspace((s) => s.activeEnvironmentId); + const settings = useWorkspace((s) => s.settings); + const setActiveEnvironment = useWorkspace((s) => s.setActiveEnvironment); + const setTheme = useWorkspace((s) => s.setTheme); + const setDialog = useWorkspace((s) => s.setDialog); + const addRequest = useWorkspace((s) => s.addRequest); + const addFolder = useWorkspace((s) => s.addFolder); + + const [newMenu, setNewMenu] = useState<{ x: number; y: number } | null>(null); + const [exportMenu, setExportMenu] = useState<{ x: number; y: number } | null>(null); + const transportKind = useMemo(() => detectTransport({ ginBaseUrl: "" }).kind, []); + + useEffect(() => { + document.title = "OpsxAPI · 接口工作台"; + }, []); + + const firstCollection = collections[0]; + + const quickCreate = (kind: string) => { + if (!firstCollection) return; + if (kind === "folder") addFolder(firstCollection.id, null); + else addRequest(firstCollection.id, null, kind as never); + }; + + const newItems: MenuItem[] = [ + ...NEW_ITEMS.map((i) => ({ + label: i.label, + icon: , + onClick: () => quickCreate(i.kind), + })), + { type: "sep" as const }, + { label: "新建目录", icon: , onClick: () => quickCreate("folder") }, + { type: "sep" as const }, + { label: "选择位置新建…", icon: , onClick: () => setDialog({ type: "new", collectionId: firstCollection?.id ?? "", parentId: null }) }, + ]; + + const activeEnv = environments.find((e) => e.id === activeEnvironmentId); + + const [envMenu, setEnvMenu] = useState<{ x: number; y: number } | null>(null); + const envItems: MenuItem[] = [ + ...environments.map((env) => ({ + label: ( + + + {env.name} + {env.id === activeEnvironmentId ? ( + ● 当前 + ) : ( + + {env.variables.filter((v) => v.enabled && v.key).length} 变量 + + )} + + ), + onClick: () => setActiveEnvironment(env.id), + })), + { type: "sep" as const }, + { + label: "不使用环境", + icon: , + onClick: () => setActiveEnvironment(null), + }, + { + label: "管理环境…", + icon: , + onClick: () => setDialog({ type: "environments" }), + }, + ]; + + return ( +
    +
    +
    + + OpsxAPI +
    + + HTTP · GraphQL · WS · SSE · Socket.IO · MQTT + +
    + +
    + + + + +
    + + + + +
    + + + + 文档 + + + + {transportKind} + +
    + + setNewMenu(null)} + /> + setEnvMenu(null)} + /> + ({ + label: c.name, + onClick: () => setDialog({ type: "export", collectionId: c.id }), + })), + } + : null + } + onClose={() => setExportMenu(null)} + /> +
    + ); +} diff --git a/packages/ui/src/components/VarHintInput.tsx b/packages/ui/src/components/VarHintInput.tsx new file mode 100644 index 0000000..1c640e2 --- /dev/null +++ b/packages/ui/src/components/VarHintInput.tsx @@ -0,0 +1,179 @@ +/** + * 带环境变量自动补全的输入框: + * 输入 "{{" 后弹出当前环境变量浮层,↑↓ 选择、Enter/Tab 插入 {{变量名}}。 + */ +import { useWorkspace } from "../workspaceStore"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"; +import { EnvBadge } from "./EnvBadge"; + +type Candidate = { name: string; value: string; description?: string }; + +export function VarHintInput({ + value, + onChange, + placeholder, + className, + onKeyDown, + autoFocus, + spellCheck = false, + ariaLabel, +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; + className?: string; + onKeyDown?: (e: KeyboardEvent) => void; + autoFocus?: boolean; + spellCheck?: boolean; + ariaLabel?: string; +}) { + const inputRef = useRef(null); + const [query, setQuery] = useState<{ prefix: string; caret: number } | null>(null); + const [active, setActive] = useState(0); + + const candidates = useMemo(() => { + const state = useWorkspace.getState(); + const env = state.environments.find((e) => e.id === state.activeEnvironmentId); + return (env?.variables ?? []) + .filter((v) => v.enabled && v.key.trim()) + .map((v) => ({ name: v.key.trim(), value: v.value, description: v.description })); + }, [query]); // query 变化时重查(环境可能被改) + + const filtered = useMemo(() => { + if (!query) return []; + const q = query.prefix.toLowerCase(); + const list = candidates.filter((c) => c.name.toLowerCase().includes(q)); + return list.slice(0, 8); + }, [candidates, query]); + + // 光标前未闭合的 {{ 即触发 + const detect = (v: string, caret: number) => { + const before = v.slice(0, caret); + const idx = before.lastIndexOf("{{"); + if (idx < 0) return null; + const frag = before.slice(idx + 2); + if (frag.includes("}") || frag.includes("{") || frag.includes(" ") || frag.length > 40) return null; + return { prefix: frag, caret }; + }; + + const sync = () => { + const el = inputRef.current; + if (!el) return; + const q = detect(el.value, el.selectionStart ?? 0); + setQuery(q); + setActive(0); + }; + + const insert = (name: string) => { + const el = inputRef.current; + if (!el || !query) return; + const idx = el.value.slice(0, query.caret).lastIndexOf("{{"); + if (idx < 0) return; + const next = `${el.value.slice(0, idx)}{{${name}}}` + el.value.slice(query.caret); + onChange(next); + setQuery(null); + const caretTarget = idx + name.length + 4; + requestAnimationFrame(() => { + el.focus(); + el.setSelectionRange(caretTarget, caretTarget); + }); + }; + + // 过滤结果为空时收起 + useEffect(() => { + if (query && filtered.length === 0) setQuery(null); + }, [filtered.length, query]); + + // 外部清空时重置 + useLayoutEffect(() => { + if (!value) setQuery(null); + }, [value]); + + const handleKeyDown = (e: KeyboardEvent) => { + if (query && filtered.length > 0) { + if (e.key === "ArrowDown") { + e.preventDefault(); + e.stopPropagation(); + setActive((a) => Math.min(a + 1, filtered.length - 1)); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + e.stopPropagation(); + setActive((a) => Math.max(a - 1, 0)); + return; + } + if (e.key === "Enter" || e.key === "Tab") { + e.preventDefault(); + e.stopPropagation(); + insert(filtered[active].name); + return; + } + if (e.key === "Escape") { + e.stopPropagation(); + setQuery(null); + return; + } + } + onKeyDown?.(e); + }; + + const envName = useWorkspace( + (s) => s.environments.find((e) => e.id === s.activeEnvironmentId)?.name ?? "", + ); + const envId = useWorkspace((s) => s.activeEnvironmentId); + + return ( +
    + { + const el = e.currentTarget; + onChange(el.value); + // 同步阶段探测(input 事件时 DOM 已更新,避免 React 并发提交的竞态) + setQuery(detect(el.value, el.selectionStart ?? el.value.length)); + setActive(0); + }} + onKeyUp={sync} + onClick={sync} + onBlur={() => setTimeout(() => setQuery(null), 120)} + onKeyDown={handleKeyDown} + /> + {query && filtered.length > 0 && ( +
    e.preventDefault()}> +
    + {envId ? ( + <> + + {envName || "当前环境"} + + ) : ( + 未选择环境 + )} + {"输入 {{变量名}} 引用 · Enter 插入"} +
    + {filtered.map((c, i) => ( + + ))} +
    + )} +
    + ); +} diff --git a/packages/ui/src/components/common.tsx b/packages/ui/src/components/common.tsx new file mode 100644 index 0000000..476128c --- /dev/null +++ b/packages/ui/src/components/common.tsx @@ -0,0 +1,357 @@ +/** + * 基础组件:Modal / 右键菜单 / JSON 高亮 / KV 编辑器 / 复制按钮。 + */ +import type { KVRow } from "@opsxapi/core"; +import { kv } from "@opsxapi/core"; +import { Icon } from "./icons"; +import { VarHintInput } from "./VarHintInput"; +import { useEffect, useRef, useState, type ReactNode } from "react"; + +export function cx(...xs: Array): string { + return xs.filter(Boolean).join(" "); +} + +/* ---------------- Modal ---------------- */ + +export function Modal({ + title, + onClose, + children, + footer, + width, +}: { + title: ReactNode; + onClose: () => void; + children: ReactNode; + footer?: ReactNode; + width?: string; +}) { + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + return ( +
    e.target === e.currentTarget && onClose()}> +
    +
    +
    {title}
    + +
    +
    {children}
    + {footer &&
    {footer}
    } +
    +
    + ); +} + +/* ---------------- 右键菜单 ---------------- */ + +export type MenuItem = + | { type: "sep" } + | { + type?: "item"; + label: ReactNode; + icon?: ReactNode; + danger?: boolean; + onClick: () => void; + }; + +export function ContextMenu({ + menu, + onClose, +}: { + menu: { x: number; y: number; items: MenuItem[] } | null; + onClose: () => void; +}) { + const ref = useRef(null); + + useEffect(() => { + if (!menu) return; + const close = () => onClose(); + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); + window.addEventListener("click", close); + window.addEventListener("resize", close); + window.addEventListener("keydown", onKey); + return () => { + window.removeEventListener("click", close); + window.removeEventListener("resize", close); + window.removeEventListener("keydown", onKey); + }; + }, [menu, onClose]); + + if (!menu) return null; + + const style: React.CSSProperties = { + left: Math.min(menu.x, window.innerWidth - 210), + top: Math.min(menu.y, window.innerHeight - menu.items.length * 30 - 20), + }; + + return ( +
    e.stopPropagation()}> + {menu.items.map((item, i) => + "type" in item && item.type === "sep" ? ( +
    + ) : ( + + ), + )} +
    + ); +} + +/* ---------------- JSON 高亮 ---------------- */ + +export function JsonView({ text, className }: { text: string; className?: string }) { + let value: unknown; + try { + value = JSON.parse(text); + } catch { + return {text}; + } + return
    {renderJson(value, 0)}
    ; +} + +function renderJson(value: unknown, indent: number, key?: string): ReactNode { + const pad = " ".repeat(indent); + const padEnd = " ".repeat(Math.max(0, indent - 1)); + const keyPart = key !== undefined ? {JSON.stringify(key)}: : null; + + if (value === null) { + return ( +
    + {pad} + {keyPart} + null +
    + ); + } + if (typeof value === "string") { + return ( +
    + {pad} + {keyPart} + {JSON.stringify(value)} +
    + ); + } + if (typeof value === "number") { + return ( +
    + {pad} + {keyPart} + {String(value)} +
    + ); + } + if (typeof value === "boolean") { + return ( +
    + {pad} + {keyPart} + {String(value)} +
    + ); + } + if (Array.isArray(value)) { + if (value.length === 0) { + return ( +
    + {pad} + {keyPart}[] +
    + ); + } + return ( +
    + {pad} + {keyPart}{"["} + {value.map((v, i) => ( +
    {renderJson(v, indent + 1)}
    + ))} + {padEnd}] +
    + ); + } + // object + const entries = Object.entries(value as Record); + if (entries.length === 0) { + return ( +
    + {pad} + {keyPart}{"{}"} +
    + ); + } + return ( +
    + {pad} + {keyPart}{"{"} + {entries.map(([k, v]) => ( +
    {renderJson(v, indent + 1, k)}
    + ))} + {padEnd}{"}"} +
    + ); +} + +/* ---------------- 复制按钮 ---------------- */ + +export function CopyButton({ text, label = "复制" }: { text: string; label?: string }) { + const [done, setDone] = useState(false); + return ( + + ); +} + +/* ---------------- KV 编辑器 ---------------- */ + +export function KVEditor({ + rows, + onChange, + onAdd, + onUpdate, + onRemove, + keyPlaceholder = "Key", + valuePlaceholder = "Value", + withDescription = false, + compact = false, +}: { + rows: KVRow[]; + onChange?: (rows: KVRow[]) => void; + onAdd?: () => void; + onUpdate?: (rowId: string, patch: Partial) => void; + onRemove?: (rowId: string) => void; + keyPlaceholder?: string; + valuePlaceholder?: string; + withDescription?: boolean; + compact?: boolean; +}) { + const update = (id: string, patch: Partial) => { + if (onUpdate) onUpdate(id, patch); + else onChange?.(rows.map((r) => (r.id === id ? { ...r, ...patch } : r))); + }; + const remove = (id: string) => { + if (onRemove) onRemove(id); + else onChange?.(rows.filter((r) => r.id !== id)); + }; + const add = () => { + if (onAdd) onAdd(); + else onChange?.([...rows, kv()]); + }; + const enabledCount = rows.filter((r) => r.enabled && r.key.trim()).length; + + return ( +
    + {!compact && rows.length > 0 && ( +
    + 共 {rows.length} 行 · 启用 {enabledCount} 行 +
    + )} + {rows.map((r) => ( +
    + update(r.id, { enabled: e.target.checked })} + className="mx-auto accent-[var(--accent)]" + aria-label="启用" + /> + update(r.id, { key: e.target.value })} + /> + update(r.id, { value: v })} + /> + {withDescription && ( + update(r.id, { description: e.target.value })} + /> + )} + +
    + ))} + +
    + ); +} + +/* ---------------- 状态点 ---------------- */ + +export function StatusDot({ status }: { status: string }) { + const cls = + status === "open" + ? "ops-dot-open" + : status === "connecting" || status === "reconnecting" + ? "ops-dot-connecting" + : status === "error" + ? "ops-dot-error" + : "ops-dot-closed"; + return ; +} + +export function formatBytes(n: number): string { + if (n < 1024) return `${n} B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; + return `${(n / 1024 / 1024).toFixed(2)} MB`; +} + +export function formatTime(at: number): string { + const d = new Date(at); + return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String( + d.getSeconds(), + ).padStart(2, "0")}`; +} diff --git a/packages/ui/src/components/dialogs/ExportDialog.tsx b/packages/ui/src/components/dialogs/ExportDialog.tsx new file mode 100644 index 0000000..c025523 --- /dev/null +++ b/packages/ui/src/components/dialogs/ExportDialog.tsx @@ -0,0 +1,134 @@ +/** + * 导出对话框:OpenAPI JSON/YAML、Postman、Markdown、全量备份。 + */ +import { countRequests, exportBackup, exportCollectionAs, type ExportFormat } from "@opsxapi/core"; +import { useMemo, useState } from "react"; +import { useWorkspace } from "../../workspaceStore"; +import { CopyButton, Modal, cx } from "../common"; +import { Icon } from "../icons"; + +const FORMATS: { key: ExportFormat; label: string; ext: string; hint: string }[] = [ + { key: "openapi-json", label: "OpenAPI 3.0 · JSON", ext: "json", hint: "标准 OpenAPI 规范,可回导 Apifox/Swagger" }, + { key: "openapi-yaml", label: "OpenAPI 3.0 · YAML", ext: "yaml", hint: "标准 OpenAPI 规范(YAML)" }, + { key: "postman", label: "Postman v2.1", ext: "json", hint: "Postman Collection,可直接导入 Postman" }, + { key: "markdown", label: "Markdown 接口文档", ext: "md", hint: "带目录的接口文档,可放 Wiki" }, +]; + +export function ExportDialog({ collectionId }: { collectionId: string }) { + const collections = useWorkspace((s) => s.collections); + const environments = useWorkspace((s) => s.environments); + const setDialog = useWorkspace((s) => s.setDialog); + const [selectedCollection, setSelectedCollection] = useState(collectionId); + const [backupMode, setBackupMode] = useState(collections.length === 0); + const [format, setFormat] = useState("openapi-json"); + + const collection = collections.find((c) => c.id === selectedCollection) ?? collections[0]; + + const { content, filename } = useMemo(() => { + if (!collection) return { content: "", filename: "" }; + const meta = FORMATS.find((f) => f.key === format) ?? FORMATS[0]; + const safeName = collection.name.replace(/[\\/:*?"<>|\s]+/g, "-"); + try { + return { + content: exportCollectionAs(format, collection), + filename: `${safeName}.${meta.ext}`, + }; + } catch (e) { + return { content: `导出失败:${e instanceof Error ? e.message : String(e)}`, filename: "error.txt" }; + } + }, [collection, format]); + + const backupContent = useMemo( + () => exportBackup(collections, environments), + [collections, environments], + ); + + const finalContent = backupMode ? backupContent : content; + const finalFilename = backupMode + ? `opsxapi-backup-${new Date().toISOString().slice(0, 10)}.json` + : filename; + + const download = () => { + const blob = new Blob([finalContent], { type: "text/plain;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = finalFilename; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( + setDialog(null)} + width="720px" + footer={ + <> + + + + + } + > +
    + + + {!backupMode && ( + <> + {collections.length > 1 && ( +
    + 集合 + +
    + )} + +
    + {FORMATS.map((f) => ( + + ))} +
    + + )} + +
    +          {finalContent.slice(0, 4000)}
    +          {finalContent.length > 4000 ? "\n…(预览截断,导出为完整内容)" : ""}
    +        
    +
    +
    + ); +} diff --git a/packages/ui/src/components/dialogs/ImportDialog.tsx b/packages/ui/src/components/dialogs/ImportDialog.tsx new file mode 100644 index 0000000..2d1073b --- /dev/null +++ b/packages/ui/src/components/dialogs/ImportDialog.tsx @@ -0,0 +1,288 @@ +/** + * 导入对话框:粘贴 / 文件 / URL(Swagger URL)三种来源 + 格式自动识别。 + */ +import type { Collection } from "@opsxapi/core"; +import { + FORMAT_LABELS, + detectImportFormat, + importByFormat, + countRequests, + type ImportFormat, +} from "@opsxapi/core"; +import { useMemo, useRef, useState } from "react"; +import { useWorkspace } from "../../workspaceStore"; +import { Modal, cx } from "../common"; +import { Icon } from "../icons"; + +type Source = "paste" | "file" | "url"; + +const ALL_FORMATS: (ImportFormat | "auto")[] = [ + "auto", + "openapi", + "postman", + "apipost", + "curl", + "har", + "insomnia", + "hoppscotch", + "markdown", + "backup", +]; + +type ImportSummary = { + collections: Collection[]; + requestCount: number; + envVarCount: number; + warnings: string[]; +}; + +export function ImportDialog() { + const setDialog = useWorkspace((s) => s.setDialog); + const importCollections = useWorkspace((s) => s.importCollections); + + const [source, setSource] = useState("paste"); + const [text, setText] = useState(""); + const [url, setUrl] = useState(""); + const [format, setFormat] = useState("auto"); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [summary, setSummary] = useState(null); + const fileRef = useRef(null); + + const detected = useMemo(() => (text.trim() ? detectImportFormat(text) : null), [text]); + + const effectiveFormat = (): ImportFormat => { + if (format !== "auto") return format; + if (!text.trim()) throw new Error("请先粘贴内容 / 选择文件 / 填写 URL"); + const d = detectImportFormat(text); + if (d === "unknown") throw new Error("无法识别格式,请手动选择格式"); + return d; + }; + + const doImport = (raw?: string) => { + setError(null); + setBusy(true); + try { + const content = raw ?? text; + if (!content.trim()) throw new Error("内容为空"); + const fmt = effectiveFormat(); + const result = importByFormat(content, fmt); + const requestCount = result.collections.reduce((acc, c) => acc + countRequests(c), 0); + importCollections(result.collections, result.environmentVariables); + setSummary({ + collections: result.collections, + requestCount, + envVarCount: result.environmentVariables.length, + warnings: result.warnings, + }); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + }; + + const loadFile = (file: File) => { + const reader = new FileReader(); + reader.onload = () => { + const content = String(reader.result ?? ""); + setText(content); + setSource("paste"); + setTimeout(() => doImport(content), 30); + }; + reader.readAsText(file); + }; + + const loadUrl = async () => { + setError(null); + setBusy(true); + try { + if (!url.trim()) throw new Error("请填写 URL"); + // 经 Gin 代理拉取,避开 CORS + const res = await fetch("/v1/proxy/http", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + method: "GET", + url: url.trim(), + headers: { Accept: "application/json, text/yaml, text/plain, */*" }, + body: { mode: "none" }, + }), + }); + if (!res.ok) { + const t = await res.text().catch(() => ""); + throw new Error(`拉取失败 ${res.status}:${t.slice(0, 160)}`); + } + const payload = (await res.json()) as { body?: string; status?: number }; + if (payload.status && payload.status >= 400) { + throw new Error(`目标返回 HTTP ${payload.status}`); + } + const content = payload.body ?? ""; + if (!content.trim()) throw new Error("URL 返回内容为空"); + setText(content); + setSource("paste"); + doImport(content); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + }; + + return ( + setDialog(null)} + width="680px" + footer={ + summary ? ( + + ) : ( + <> + + + + ) + } + > + {summary ? ( +
    +
    导入成功 ✓
    +
    + {summary.collections.map((c) => ( +
    + 📚 + {c.name} + · {countRequests(c)} 个接口 +
    + ))} +
    + {summary.envVarCount > 0 && ( +
    + 已向当前环境追加 {summary.envVarCount} 个变量(如 baseUrl / 路径参数) +
    + )} + {summary.warnings.length > 0 && ( +
    + {summary.warnings.map((w, i) => ( +
    ⚠ {w}
    + ))} +
    + )} +
    + ) : ( +
    + {/* 来源切换 */} +
    + {( + [ + ["paste", "粘贴文本 / cURL"], + ["file", "文件导入"], + ["url", "URL 导入(Swagger)"], + ] as [Source, string][] + ).map(([key, label]) => ( + + ))} +
    + + {source === "paste" && ( +