Set up bilingual Felis documentation site

This commit is contained in:
Lemon-miaow committed 2026-10-02 19:58:16 +08:00
commit 67d82529a6
47 files changed
+20023

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
node_modules/
docs/.vitepress/cache/
docs/.vitepress/dist/
.DS_Store
.env*.local
+81
View File
@@ -0,0 +1,81 @@
# Felis 文档站
基于 VitePress 默认主题,使用 Felis 黄绿色配色。参考 [Vdrias World Manual](https://github.com/vdriasworld/manual) 的插件选型,接入 Nólëbase 阅读增强与标题定位;使用 Mermaid 渲染主仓库的时序图。导航、搜索、代码复制和主题切换由 VitePress 提供。
## 本地开发
需要 Node.js 22+ 与 Bun 1.3.14。
```sh
bun install --frozen-lockfile
bun run docs:dev
```
```sh
bun run docs:build
bun run docs:preview
```
文档放在 `docs/`,导航与侧栏在 `docs/.vitepress/config.mts`,品牌色与样式在 `docs/.vitepress/theme/style.css`。入口直接显示带侧栏的文档正文,沿用 VitePress 默认主题。
## 中英文内容
仅提供简体中文与英文。中文页面沿用 `docs/` 下的现有路径,英文对应页面放在 `docs/en/`,例如 `docs/guide/deployment.md` 对应 `docs/en/guide/deployment.md`。VitePress 的语言菜单切换到同一篇文章,导航、侧栏、搜索与阅读增强菜单随语言切换。搜索索引按语言划分。
修改技术内容时同时更新两种语言,命令、配置键、错误码和验证标记保持一致。标题末尾的 `{#章节标识}` 在对应页面中使用同一值;翻译标题时保留该标识,避免旧链接和语言切换后的章节定位失效。OpenAPI、许可证和图片由两种语言共用 `docs/public/` 下的原件。
## 发布
使用 Bun 安装依赖和执行 VitePress 构建,产物目录为 `docs/.vitepress/dist`。
### Cloudflare Pages
连接本仓库后,填写以下构建设置:
| 设置 | 值 |
| --- | --- |
| 框架预设 | `VitePress`,随后覆盖下面的命令和输出目录 |
| 根目录 | 留空(仓库根目录) |
| 构建命令 | `bun install --frozen-lockfile && bun run docs:build` |
| 构建输出目录 | `docs/.vitepress/dist` |
生产环境和预览环境都添加以下环境变量:
| 变量 | 值 |
| --- | --- |
| `BUN_VERSION` | `1.3.14` |
| `NODE_VERSION` | `22` |
| `SKIP_DEPENDENCY_INSTALL` | `1` |
构建命令按 `bun.lock` 显式安装依赖,因此关闭 Pages 的自动依赖安装。`pages.dev` 或独立文档域名使用默认的 `/`,无需设置 `DOCS_BASE`。版本变量与跳过自动安装的设置参见 [Cloudflare 构建环境文档](https://developers.cloudflare.com/pages/configuration/build-image/)。
### 部署到子路径
如果使用 `https://felismc.github.io/docs/`,构建时设置 `DOCS_BASE=/docs/`:
```sh
DOCS_BASE=/docs/ bun run docs:build
```
## 内容来源
内容以 [Felis 主仓库](https://github.com/FelisMC/Felis) 为准,本次迁移基于本地提交 `2e4f118939aec47852c181f8043f8ab98c62014c`。两种语言保留原文的命令、限制与验证标记。入门内容分别使用 `README.md`、`README_EN.md`;技术文章的译文与对应原文保持相同结构和章节标识。
| 主仓库来源 | 文档站位置 |
| --- | --- |
| `README.md` | 认识 Felis、安装与部署、管理服务器、备份与恢复、从源码构建、开源协议 |
| `docs/operations.md` | `docs/operations/index.md` |
| `docs/troubleshooting.md` | `docs/operations/troubleshooting.md` |
| `docs/distributed.md` | `docs/guide/distributed.md` |
| `docs/sequence-diagrams.md` | `docs/reference/sequence-diagrams.md` |
| `docs/deferred-seams.md` | `docs/reference/deferred-seams.md` |
| `CONTRIBUTING.md` | `docs/reference/contributing.md` |
| `plugins/README.md` | `docs/reference/plugins.md` |
| `deploy/lobby/README.md`、`deploy/limbo/README.md` | `docs/reference/lobby.md`、`docs/reference/limbo.md` |
| `README.md`、`README_EN.md` | 中英文的 `reference/readme-en.md`(项目说明,保留原有地址) |
| `docs/openapi.yaml` | `docs/public/openapi.yaml`,由 API 定义页面提供下载 |
| `LICENSE` | `docs/public/LICENSE.txt` |
部署架构及入门页面摘取上述原文,并链接到完整手册。更新功能说明时先核对主仓库对应文章,保留其发布通道、实验性状态与验证边界,避免独立改写出另一套口径。每页末尾均链接到主仓库原文。
渲染适配:故障排查里跨行的行内代码已合并一处换行;时序图的文本分号使用 [Mermaid 要求的 `#59;` 转义](https://mermaid.js.org/syntax/sequenceDiagram.html#entity-codes-to-escape-characters);数字开头的章节锚点按 VitePress 的格式添加 `_`,两种语言通过显式章节标识共享锚点。README 的居中品牌页眉改为普通 Markdown。
+650
View File
@@ -0,0 +1,650 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "felis-docs",
"devDependencies": {
"@nolebase/vitepress-plugin-enhanced-readabilities": "2.18.2",
"@nolebase/vitepress-plugin-highlight-targeted-heading": "2.18.2",
"mermaid": "11.17.2",
"vitepress": "1.6.4",
"vitepress-plugin-mermaid": "2.0.17",
"vue": "^3.5.43",
},
},
},
"packages": {
"@algolia/abtesting": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-rSTin9Uta23uaewYVQEp8XI9T3iA/zrg0/1G2vhf8oFFDxFL5vybnZ5IQwsVAg4JpKxPX4/WYNKdcfWrZymk7w=="],
"@algolia/autocomplete-core": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
"@algolia/autocomplete-plugin-algolia-insights": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A=="],
"@algolia/autocomplete-preset-algolia": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA=="],
"@algolia/autocomplete-shared": ["@algolia/[email protected]", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg=="],
"@algolia/client-abtesting": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-bm2XN0hCSMYwStSsCBT0/PUB2BDxoyR1Lnub3c392HMEy9bi8PUSW8vR6zltVEKWG2t4PQFWMD5C07bmJxGgPg=="],
"@algolia/client-analytics": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-XOFPOTa69WuqHR6c5tMgnUUwwqQgNSzMpxmhrgA9KmxRf8WIqEa0cokHJvohk5CYb7CZx0xeSL6Bk2IUJ7Lv7Q=="],
"@algolia/client-common": ["@algolia/[email protected]", "", {}, "sha512-PC8ipLOYFKRTfIUY1J3FJxS6ryzWziaXmIX9/sNMoUR8L+XhF7hX2QAeUI87YVl5zujvlYTSOb+HAIqKXDjyHQ=="],
"@algolia/client-insights": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-yFNcCMM5fHiyoR0HuxMrzy+VjDcmhFUZTm2IJ2DHwGsVH3B5SEob4zTmeEZ3j/AZqNnmrJOCKl/tJnBg7+84bA=="],
"@algolia/client-personalization": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-GYja6HkDt2VrQhWmB2cLx3Z5fDwI9no7q+xwCWcFrTPm5CLH9QZvK0XLO9co1FcNIDxxkMaP3AXM39/XcecEOg=="],
"@algolia/client-query-suggestions": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-Wofg7bMpWh8N5qDDZs0wy6whc+KMmdNsxgrIGp9Ug2s1Bka0uq7AjyckdxReKPHLA0Q1qH8X/SNh0wn2t2X2Lw=="],
"@algolia/client-search": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-fHnALZfbEnODczGk14Y/1YBRApp6UEpZUTexGcMUPzY7RDc7q4HN2Y6jh0KUG8Jo9B+q+wOoQEc3ziR0ErbuFg=="],
"@algolia/ingestion": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-Fa38s1mHgoaLCT117sfJ6P78rtxUt93CYxBYpq1VOIcslaF+cH+1h5uikAPsKfxLnsuEDZHiBiaI6eNPTsQMRA=="],
"@algolia/monitoring": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-NyNsRSqM2tF1MX7ZGw/j4rduoEJiQ5wfvbSo/CFVdEzYCjyxbGFRPOZyS/GETJG9rk1TdHOq8NZqKqk/u1fm4g=="],
"@algolia/recommend": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-nXBK2uygWtvbCOffMWqqfjjcEtp9enDKY5/2Pw/Hhw8VVaOyK2ThbGK04bNX/Le+qoK1VHYkodWGVTKfw0Otkw=="],
"@algolia/requester-browser-xhr": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0" } }, "sha512-yb+4afX/zja8QwX0KmV4/ae2kyYkRknsE79CRusYS2U5D8qwn2wq0cn1x12f3tm3F3+5FgmDdbuGdQTuCLSKMQ=="],
"@algolia/requester-fetch": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0" } }, "sha512-Lp52TmpA1QtNmdHzs505Xwf4tgII7RAapkDSF9AAtWIBETcGaTIaXci4Rd9nS6SdaWK1BWTSqAPp5wzh6UN3Ag=="],
"@algolia/requester-node-http": ["@algolia/[email protected]", "", { "dependencies": { "@algolia/client-common": "5.59.0" } }, "sha512-YYHLEs5rC6oRTFwT7bJaKBR9NSFzikeVHGAT1ffATmUQzR8XqyhHXjoieAUoR7fBU6I5cfy1FVeh5GoRgriuzA=="],
"@antfu/install-pkg": ["@antfu/[email protected]", "", { "dependencies": { "package-manager-detector": "^1.8.0", "tinyexec": "^1.3.1" } }, "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw=="],
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="],
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@braintree/sanitize-url": ["@braintree/[email protected]", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
"@chevrotain/types": ["@chevrotain/[email protected]", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@docsearch/css": ["@docsearch/[email protected]", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="],
"@docsearch/js": ["@docsearch/[email protected]", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="],
"@docsearch/react": ["@docsearch/[email protected]", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="],
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
"@iconify-json/carbon": ["@iconify-json/[email protected]", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-tufpkYXBDlXqczQEZo6QgvfwBd5aIK36gt+pHOI66eK4KOO8oh7CIHDZ4JsiSnOyjqRbApVHzXv4Nz8NO0GPFQ=="],
"@iconify-json/icon-park-outline": ["@iconify-json/[email protected]", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-NyZxXe2gD2TbTOyoRRMdtEJhr6i2KQCdDlYYoOn5oZLndQjwpIhw79hzeFhXvP38/o40D3gQ+l+IaSJgbB+0TQ=="],
"@iconify-json/octicon": ["@iconify-json/[email protected]", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-YHlJokjC+uBj1kSd7+YTRMNIN85TvAGr4ZJVOHCTzj0nyH64ImXRIZmh4P6Nbv0RGDd7hS7+UwEDGchFgGxxVw=="],
"@iconify-json/simple-icons": ["@iconify-json/[email protected]", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-Z6PmM6jvWVU7rmLzqbxCLJA7SeqCnK6lMHXtjyzHPLZebwuoojBEcHHycppahDzEeN9xujRkrV5J8QpOhMyNrg=="],
"@iconify/types": ["@iconify/[email protected]", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/[email protected]", "", { "dependencies": { "@antfu/install-pkg": "^2.0.1", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@mermaid-js/mermaid-mindmap": ["@mermaid-js/[email protected]", "", { "dependencies": { "@braintree/sanitize-url": "^6.0.0", "cytoscape": "^3.23.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.1.0", "d3": "^7.0.0", "khroma": "^2.0.0", "non-layered-tidy-tree-layout": "^2.0.2" } }, "sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw=="],
"@mermaid-js/parser": ["@mermaid-js/[email protected]", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="],
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
"@nolebase/ui": ["@nolebase/[email protected]", "", { "dependencies": { "@iconify-json/octicon": "^1.2.10", "less": "^4.4.0" }, "peerDependencies": { "vitepress": "^1.5.0 || ^2.0.0-alpha.1", "vue": ">= 3.5.18" } }, "sha512-xxfRacF9cqQ5/umMhvhr0y2W4SkhzTmrrAHJ0UAAu/pIWfV/JPE9Hj0buH06bK7ZEUur+036gxkKlStI6UtDBw=="],
"@nolebase/vitepress-plugin-enhanced-readabilities": ["@nolebase/[email protected]", "", { "dependencies": { "@iconify-json/carbon": "^1.2.11", "@iconify-json/icon-park-outline": "^1.2.2", "@nolebase/ui": "^2.18.2", "less": "^4.4.0" }, "peerDependencies": { "vitepress": "^1.5.0 || ^2.0.0-alpha.1" } }, "sha512-fDhdZBSJL2qs/1xac0PtJfU5UI7b36ffVPYgzM8Ig2NjqMJ7cBvrTTPGq03lUeMjK7GD7fItAKudaP+ZrtMg8w=="],
"@nolebase/vitepress-plugin-highlight-targeted-heading": ["@nolebase/[email protected]", "", { "dependencies": { "less": "^4.4.0" }, "peerDependencies": { "vitepress": "^1.5.0 || ^2.0.0-alpha.1" } }, "sha512-RVrT7FgyjxrnFFR9twc1OJI/5B09+biVbSiO36Yiu2YC+kRMz1Mb553l2DyioLtWJgVNTJ7sj66ZJi40JpDh7g=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg=="],
"@rollup/rollup-android-arm64": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg=="],
"@rollup/rollup-darwin-x64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw=="],
"@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="],
"@shikijs/engine-javascript": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^3.1.0" } }, "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w=="],
"@shikijs/engine-oniguruma": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw=="],
"@shikijs/langs": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w=="],
"@shikijs/themes": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw=="],
"@shikijs/transformers": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/types": "2.5.0" } }, "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg=="],
"@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw=="],
"@shikijs/vscode-textmate": ["@shikijs/[email protected]", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@types/d3": ["@types/[email protected]", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
"@types/d3-array": ["@types/[email protected]", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
"@types/d3-axis": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
"@types/d3-brush": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
"@types/d3-chord": ["@types/[email protected]", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
"@types/d3-color": ["@types/[email protected]", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-contour": ["@types/[email protected]", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
"@types/d3-delaunay": ["@types/[email protected]", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
"@types/d3-dispatch": ["@types/[email protected]", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
"@types/d3-drag": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
"@types/d3-dsv": ["@types/[email protected]", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
"@types/d3-ease": ["@types/[email protected]", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-fetch": ["@types/[email protected]", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
"@types/d3-force": ["@types/[email protected]", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
"@types/d3-format": ["@types/[email protected]", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
"@types/d3-geo": ["@types/[email protected]", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="],
"@types/d3-hierarchy": ["@types/[email protected]", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
"@types/d3-interpolate": ["@types/[email protected]", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/[email protected]", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
"@types/d3-polygon": ["@types/[email protected]", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
"@types/d3-quadtree": ["@types/[email protected]", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
"@types/d3-random": ["@types/[email protected]", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="],
"@types/d3-scale": ["@types/[email protected]", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-scale-chromatic": ["@types/[email protected]", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
"@types/d3-selection": ["@types/[email protected]", "", {}, "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw=="],
"@types/d3-shape": ["@types/[email protected]", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="],
"@types/d3-time": ["@types/[email protected]", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-time-format": ["@types/[email protected]", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
"@types/d3-timer": ["@types/[email protected]", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/d3-transition": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
"@types/d3-zoom": ["@types/[email protected]", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
"@types/estree": ["@types/[email protected]", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/geojson": ["@types/[email protected]", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
"@types/linkify-it": ["@types/[email protected]", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/markdown-it": ["@types/[email protected]", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ=="],
"@types/mdast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdurl": ["@types/[email protected]", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/web-bluetooth": ["@types/[email protected]", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="],
"@upsetjs/venn.js": ["@upsetjs/[email protected]", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
"@vitejs/plugin-vue": ["@vitejs/[email protected]", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
"@vue/compiler-core": ["@vue/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.43", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg=="],
"@vue/compiler-dom": ["@vue/[email protected]", "", { "dependencies": { "@vue/compiler-core": "3.5.43", "@vue/shared": "3.5.43" } }, "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ=="],
"@vue/compiler-sfc": ["@vue/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.43", "@vue/compiler-dom": "3.5.43", "@vue/compiler-ssr": "3.5.43", "@vue/shared": "3.5.43", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.28", "source-map-js": "^1.2.1" } }, "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ=="],
"@vue/compiler-ssr": ["@vue/[email protected]", "", { "dependencies": { "@vue/compiler-dom": "3.5.43", "@vue/shared": "3.5.43" } }, "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg=="],
"@vue/devtools-api": ["@vue/[email protected]", "", { "dependencies": { "@vue/devtools-kit": "^7.7.10" } }, "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA=="],
"@vue/devtools-kit": ["@vue/[email protected]", "", { "dependencies": { "@vue/devtools-shared": "^7.7.10", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw=="],
"@vue/devtools-shared": ["@vue/[email protected]", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ=="],
"@vue/reactivity": ["@vue/[email protected]", "", { "dependencies": { "@vue/shared": "3.5.43" } }, "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA=="],
"@vue/runtime-core": ["@vue/[email protected]", "", { "dependencies": { "@vue/reactivity": "3.5.43", "@vue/shared": "3.5.43" } }, "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw=="],
"@vue/runtime-dom": ["@vue/[email protected]", "", { "dependencies": { "@vue/reactivity": "3.5.43", "@vue/runtime-core": "3.5.43", "@vue/shared": "3.5.43", "csstype": "^3.2.3" } }, "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw=="],
"@vue/server-renderer": ["@vue/[email protected]", "", { "dependencies": { "@vue/compiler-ssr": "3.5.43", "@vue/runtime-dom": "3.5.43", "@vue/shared": "3.5.43" } }, "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ=="],
"@vue/shared": ["@vue/[email protected]", "", {}, "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q=="],
"@vueuse/core": ["@vueuse/[email protected]", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
"@vueuse/integrations": ["@vueuse/[email protected]", "", { "dependencies": { "@vueuse/core": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g=="],
"@vueuse/metadata": ["@vueuse/[email protected]", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
"@vueuse/shared": ["@vueuse/[email protected]", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
"algoliasearch": ["[email protected]", "", { "dependencies": { "@algolia/abtesting": "1.25.0", "@algolia/client-abtesting": "5.59.0", "@algolia/client-analytics": "5.59.0", "@algolia/client-common": "5.59.0", "@algolia/client-insights": "5.59.0", "@algolia/client-personalization": "5.59.0", "@algolia/client-query-suggestions": "5.59.0", "@algolia/client-search": "5.59.0", "@algolia/ingestion": "1.59.0", "@algolia/monitoring": "1.59.0", "@algolia/recommend": "5.59.0", "@algolia/requester-browser-xhr": "5.59.0", "@algolia/requester-fetch": "5.59.0", "@algolia/requester-node-http": "5.59.0" } }, "sha512-wUXzaeI7B526W4y1gFg3lcxgDZ67XSgRJIiellYWOas/pLpO7rOtxm9Gr2E/C8aiSDXIgx1q5TdSsvK67Uakqw=="],
"birpc": ["[email protected]", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"ccount": ["[email protected]", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities-html4": ["[email protected]", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["[email protected]", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"comma-separated-tokens": ["[email protected]", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["[email protected]", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"copy-anything": ["[email protected]", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
"cose-base": ["[email protected]", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"cytoscape": ["[email protected]", "", {}, "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ=="],
"cytoscape-cose-bilkent": ["[email protected]", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
"cytoscape-fcose": ["[email protected]", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
"d3": ["[email protected]", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
"d3-array": ["[email protected]", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-axis": ["[email protected]", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
"d3-brush": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
"d3-chord": ["[email protected]", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
"d3-color": ["[email protected]", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-contour": ["[email protected]", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
"d3-delaunay": ["[email protected]", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
"d3-dispatch": ["[email protected]", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
"d3-drag": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
"d3-dsv": ["[email protected]", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
"d3-ease": ["[email protected]", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-fetch": ["[email protected]", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
"d3-force": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
"d3-format": ["[email protected]", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
"d3-geo": ["[email protected]", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-hierarchy": ["[email protected]", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
"d3-interpolate": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["[email protected]", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-polygon": ["[email protected]", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
"d3-quadtree": ["[email protected]", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
"d3-random": ["[email protected]", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
"d3-sankey": ["[email protected]", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
"d3-scale": ["[email protected]", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-scale-chromatic": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
"d3-selection": ["[email protected]", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
"d3-shape": ["[email protected]", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["[email protected]", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["[email protected]", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"d3-transition": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
"d3-zoom": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
"dagre-d3-es": ["[email protected]", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
"dayjs": ["[email protected]", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="],
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"delaunator": ["[email protected]", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
"dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"devlop": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"dompurify": ["[email protected]", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ=="],
"emoji-regex-xs": ["[email protected]", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"errno": ["[email protected]", "", { "dependencies": { "prr": "~1.0.1" }, "bin": { "errno": "cli.js" } }, "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A=="],
"es-toolkit": ["[email protected]", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="],
"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"estree-walker": ["[email protected]", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"fastdom": ["[email protected]", "", { "dependencies": { "strictdom": "^1.0.1" } }, "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg=="],
"focus-trap": ["[email protected]", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="],
"fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"graceful-fs": ["[email protected]", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"hachure-fill": ["[email protected]", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
"hast-util-to-html": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hookable": ["[email protected]", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"html-void-elements": ["[email protected]", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"import-meta-resolve": ["[email protected]", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
"internmap": ["[email protected]", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-what": ["[email protected]", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
"katex": ["[email protected]", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
"khroma": ["[email protected]", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
"layout-base": ["[email protected]", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
"less": ["[email protected]", "", { "dependencies": { "copy-anything": "^3.0.5", "parse-node-version": "^1.0.1" }, "optionalDependencies": { "errno": "^0.1.1", "graceful-fs": "^4.1.2", "make-dir": "^5.1.0", "mime": "^1.4.1", "needle": "^3.1.0", "probe-image-size": "^7.2.3", "source-map": "~0.6.0" }, "bin": { "lessc": "bin/lessc" } }, "sha512-orp15PfJvvNDIqJdVWzMI9Sjpjp3VTiw3sfvbB+67LlISTEn8uVT2EdYSuyl02BLvaftv6sdk9Umnxmm5rckmg=="],
"lodash-es": ["[email protected]", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.merge": ["[email protected]", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"make-dir": ["[email protected]", "", {}, "sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw=="],
"mark.js": ["[email protected]", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="],
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"mdast-util-to-hast": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mermaid": ["[email protected]", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="],
"micromark-util-character": ["[email protected]", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["[email protected]", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["[email protected]", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["[email protected]", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["[email protected]", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"mime": ["[email protected]", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"minisearch": ["[email protected]", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="],
"mitt": ["[email protected]", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
"needle": ["[email protected]", "", { "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, "bin": { "needle": "bin/needle" } }, "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w=="],
"non-layered-tidy-tree-layout": ["[email protected]", "", {}, "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="],
"oniguruma-to-es": ["[email protected]", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ=="],
"package-manager-detector": ["[email protected]", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="],
"parse-node-version": ["[email protected]", "", {}, "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="],
"path-data-parser": ["[email protected]", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
"perfect-debounce": ["[email protected]", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"points-on-curve": ["[email protected]", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
"points-on-path": ["[email protected]", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
"preact": ["[email protected]", "", { "peerDependencies": { "preact-render-to-string": ">=5" }, "optionalPeers": ["preact-render-to-string"] }, "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q=="],
"probe-image-size": ["[email protected]", "", { "dependencies": { "lodash.merge": "^4.6.2", "needle": "^2.5.2", "stream-parser": "~0.3.1" } }, "sha512-cdEprVtZxV+awMde9X+4jILBFYh4CARxVrQaMl4wY4YcPWbul9jntXrIW95NInBDyJwcVUP3U0T6yukN8rMBaQ=="],
"property-information": ["[email protected]", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
"prr": ["[email protected]", "", {}, "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="],
"regex": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["[email protected]", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"rfdc": ["[email protected]", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"robust-predicates": ["[email protected]", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.5", "@rollup/rollup-android-arm64": "4.63.5", "@rollup/rollup-darwin-arm64": "4.63.5", "@rollup/rollup-darwin-x64": "4.63.5", "@rollup/rollup-freebsd-arm64": "4.63.5", "@rollup/rollup-freebsd-x64": "4.63.5", "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", "@rollup/rollup-linux-arm-musleabihf": "4.63.5", "@rollup/rollup-linux-arm64-gnu": "4.63.5", "@rollup/rollup-linux-arm64-musl": "4.63.5", "@rollup/rollup-linux-loong64-gnu": "4.63.5", "@rollup/rollup-linux-loong64-musl": "4.63.5", "@rollup/rollup-linux-ppc64-gnu": "4.63.5", "@rollup/rollup-linux-ppc64-musl": "4.63.5", "@rollup/rollup-linux-riscv64-gnu": "4.63.5", "@rollup/rollup-linux-riscv64-musl": "4.63.5", "@rollup/rollup-linux-s390x-gnu": "4.63.5", "@rollup/rollup-linux-x64-gnu": "4.63.5", "@rollup/rollup-linux-x64-musl": "4.63.5", "@rollup/rollup-openbsd-x64": "4.63.5", "@rollup/rollup-openharmony-arm64": "4.63.5", "@rollup/rollup-win32-arm64-msvc": "4.63.5", "@rollup/rollup-win32-ia32-msvc": "4.63.5", "@rollup/rollup-win32-x64-gnu": "4.63.5", "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw=="],
"roughjs": ["[email protected]", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
"rw": ["[email protected]", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"safer-buffer": ["[email protected]", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sax": ["[email protected]", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
"search-insights": ["[email protected]", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
"shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="],
"source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["[email protected]", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"speakingurl": ["[email protected]", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"stream-parser": ["[email protected]", "", { "dependencies": { "debug": "2" } }, "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ=="],
"strictdom": ["[email protected]", "", {}, "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg=="],
"stringify-entities": ["[email protected]", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"stylis": ["[email protected]", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
"superjson": ["[email protected]", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
"tabbable": ["[email protected]", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
"tinyexec": ["[email protected]", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="],
"trim-lines": ["[email protected]", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"ts-dedent": ["[email protected]", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
"unist-util-is": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"uuid": ["[email protected]", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ=="],
"vfile": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"vitepress": ["[email protected]", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="],
"vitepress-plugin-mermaid": ["[email protected]", "", { "optionalDependencies": { "@mermaid-js/mermaid-mindmap": "^9.3.0" }, "peerDependencies": { "mermaid": "10 || 11", "vitepress": "^1.0.0 || ^1.0.0-alpha" } }, "sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg=="],
"vue": ["[email protected]", "", { "dependencies": { "@vue/compiler-dom": "3.5.43", "@vue/compiler-sfc": "3.5.43", "@vue/runtime-dom": "3.5.43", "@vue/server-renderer": "3.5.43", "@vue/shared": "3.5.43" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng=="],
"zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@mermaid-js/mermaid-mindmap/@braintree/sanitize-url": ["@braintree/[email protected]", "", {}, "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="],
"cytoscape-fcose/cose-base": ["[email protected]", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
"d3-dsv/commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"d3-sankey/d3-array": ["[email protected]", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
"d3-sankey/d3-shape": ["[email protected]", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
"probe-image-size/needle": ["[email protected]", "", { "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" }, "bin": { "needle": "./bin/needle" } }, "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ=="],
"stream-parser/debug": ["[email protected]", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"superjson/copy-anything": ["[email protected]", "", {}, "sha512-AoT6Imdr98feSpFfmFwTFN73ccdr7uFPf27cBCgYvyyRyn1BzLRxMvrHNmwXO5LJMddRy4Rdhw2b1h7vSMKsEw=="],
"cytoscape-fcose/cose-base/layout-base": ["[email protected]", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
"d3-sankey/d3-array/internmap": ["[email protected]", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
"d3-sankey/d3-shape/d3-path": ["[email protected]", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
"probe-image-size/needle/iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"stream-parser/debug/ms": ["[email protected]", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
}
}
+267
View File
@@ -0,0 +1,267 @@
import { withMermaid } from 'vitepress-plugin-mermaid'
import type { DefaultTheme } from 'vitepress/theme'
const gettingStartedSidebar: DefaultTheme.SidebarItem[] = [
{
text: '开始使用',
items: [
{ text: '认识 Felis', link: '/' },
{ text: '安装与部署', link: '/guide/deployment' },
{ text: '管理服务器', link: '/guide/servers' },
],
},
{
text: '项目资料',
collapsed: true,
items: [
{ text: '开源协议', link: '/reference/license' },
{ text: '项目说明', link: '/reference/readme-en' },
],
},
]
const operationsSidebar: DefaultTheme.SidebarItem[] = [
{
text: '部署与运维',
items: [
{ text: '部署架构', link: '/reference/architecture' },
{ text: '运维手册', link: '/operations/' },
{ text: '备份与恢复', link: '/operations/backup' },
{ text: '故障排查', link: '/operations/troubleshooting' },
{ text: '多机部署(实验性)', link: '/guide/distributed' },
],
},
]
const developmentSidebar: DefaultTheme.SidebarItem[] = [
{
text: '开发参考',
items: [
{ text: '从源码构建', link: '/guide/building' },
{ text: '贡献指南', link: '/reference/contributing' },
{ text: 'API 定义', link: '/reference/api' },
{ text: '时序图', link: '/reference/sequence-diagrams' },
{ text: '服务端插件', link: '/reference/plugins' },
{ text: '集成边界', link: '/reference/deferred-seams' },
],
},
{
text: '镜像参考',
collapsed: true,
items: [
{ text: '大厅镜像', link: '/reference/lobby' },
{ text: '登录服镜像', link: '/reference/limbo' },
],
},
]
const sidebar: Record<string, DefaultTheme.SidebarItem[]> = {
'/guide/distributed': operationsSidebar,
'/guide/building': developmentSidebar,
'/reference/architecture': operationsSidebar,
'/reference/license': gettingStartedSidebar,
'/reference/readme-en': gettingStartedSidebar,
'/operations/': operationsSidebar,
'/reference/': developmentSidebar,
'/': gettingStartedSidebar,
}
const englishLabels: Record<string, string> = {
'开始使用': 'Getting started',
'认识 Felis': 'Introduction to Felis',
'安装与部署': 'Installation and deployment',
'管理服务器': 'Managing servers',
'项目资料': 'Project information',
'开源协议': 'License',
'项目说明': 'Project README',
'部署与运维': 'Deployment and operations',
'部署架构': 'Deployment architecture',
'运维手册': 'Operations guide',
'备份与恢复': 'Backup and restore',
'故障排查': 'Troubleshooting',
'多机部署(实验性)': 'Multi-node deployment (experimental)',
'开发参考': 'Developer reference',
'从源码构建': 'Build from source',
'贡献指南': 'Contributing',
'API 定义': 'API definition',
'时序图': 'Sequence diagrams',
'服务端插件': 'Server-side plugins',
'集成边界': 'Integration boundaries',
'镜像参考': 'Image reference',
'大厅镜像': 'Lobby image',
'登录服镜像': 'Login server image',
}
function englishSidebar(items: DefaultTheme.SidebarItem[]): DefaultTheme.SidebarItem[] {
return items.map(item => ({
...item,
text: item.text && englishLabels[item.text],
items: item.items && englishSidebar(item.items),
}))
}
export default withMermaid({
title: 'Felis 文档',
titleTemplate: ':title · Felis 文档',
description: 'Felis 文档:Kubernetes 驱动的 Minecraft 服务器托管,从部署到唤醒、备份与恢复。',
lang: 'zh-CN',
locales: {
root: { label: '简体中文', lang: 'zh-CN' },
en: {
label: 'English',
lang: 'en',
title: 'Felis Docs',
titleTemplate: ':title · Felis Docs',
description: 'Documentation for Felis, a Kubernetes-driven Minecraft server hosting platform.',
themeConfig: {
nav: [
{
text: 'Getting started',
link: '/en/',
activeMatch: '^/en/(?:$|guide/(?:deployment|servers)$|reference/(?:license|readme-en)$)',
},
{
text: 'Deployment and operations',
link: '/en/operations/',
activeMatch: '^/en/(?:operations/|guide/distributed$|reference/architecture$)',
},
{
text: 'Developer reference',
link: '/en/reference/contributing',
activeMatch: '^/en/(?:guide/building$|reference/(?!(?:architecture|license|readme-en)$))',
},
],
sidebar: Object.fromEntries(Object.entries(sidebar).map(([path, items]) => [
'/en' + path,
{ base: '/en', items: englishSidebar(items) },
])),
outline: { level: [2, 3], label: 'On this page' },
docFooter: { prev: 'Previous page', next: 'Next page' },
editLink: {
pattern: 'https://github.com/FelisMC/docs/edit/main/docs/:path',
text: 'Edit this page on GitHub',
},
lastUpdated: { text: 'Last updated', formatOptions: { dateStyle: 'medium' } },
sidebarMenuLabel: 'Menu',
returnToTopLabel: 'Back to top',
langMenuLabel: 'Change language',
skipToContentLabel: 'Skip to content',
darkModeSwitchLabel: 'Theme',
lightModeSwitchTitle: 'Switch to light theme',
darkModeSwitchTitle: 'Switch to dark theme',
notFound: {
title: 'Page not found',
quote: 'The page you are looking for does not exist.',
linkLabel: 'Go to the home page',
linkText: 'Back to documentation',
},
footer: { copyright: 'FelisMC · Built with VitePress' },
},
},
},
base: process.env.DOCS_BASE || '/',
appearance: true,
lastUpdated: true,
markdown: {
config(md) {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => fence(...args).replace(
'title="Copy Code"',
`title="${args[3].localeIndex === 'en' ? 'Copy code' : '复制代码'}"`,
)
},
},
mermaid: {
sequence: { useMaxWidth: false, wrap: true },
},
head: [
['meta', { name: 'theme-color', content: '#ffffff', media: '(prefers-color-scheme: light)' }],
['meta', { name: 'theme-color', content: '#131610', media: '(prefers-color-scheme: dark)' }],
],
vite: {
optimizeDeps: {
exclude: ['@nolebase/vitepress-plugin-enhanced-readabilities/client', 'vitepress', '@nolebase/ui'],
},
ssr: {
noExternal: [
'@nolebase/vitepress-plugin-enhanced-readabilities',
'@nolebase/vitepress-plugin-highlight-targeted-heading',
'@nolebase/ui',
],
},
},
themeConfig: {
logo: { src: '/felis-logo.png', alt: 'Felis' },
nav: [
{
text: '开始使用',
link: '/',
activeMatch: '^/(?:$|guide/(?:deployment|servers)$|reference/(?:license|readme-en)$)',
},
{
text: '部署与运维',
link: '/operations/',
activeMatch: '^/(?:operations/|guide/distributed$|reference/architecture$)',
},
{
text: '开发参考',
link: '/reference/contributing',
activeMatch: '^/(?:guide/building$|reference/(?!(?:architecture|license|readme-en)$))',
},
],
sidebar,
socialLinks: [{ icon: 'github', link: 'https://github.com/FelisMC/docs' }],
search: {
provider: 'local',
options: {
locales: {
en: {
translations: {
button: { buttonText: 'Search docs', buttonAriaLabel: 'Search docs' },
modal: {
displayDetails: 'Display detailed list',
resetButtonTitle: 'Reset search',
backButtonTitle: 'Close search',
noResultsText: 'No results found',
footer: { selectText: 'Select', navigateText: 'Navigate', closeText: 'Close' },
},
},
},
},
translations: {
button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
modal: {
displayDetails: '显示详细列表',
resetButtonTitle: '清除搜索',
backButtonTitle: '关闭搜索',
noResultsText: '没有找到相关内容',
footer: { selectText: '选择', navigateText: '切换', closeText: '关闭' },
},
},
},
},
outline: { level: [2, 3], label: '本页内容' },
docFooter: { prev: '上一页', next: '下一页' },
editLink: {
pattern: 'https://github.com/FelisMC/docs/edit/main/docs/:path',
text: '在 GitHub 上编辑此页',
},
lastUpdated: { text: '最后更新', formatOptions: { dateStyle: 'medium' } },
sidebarMenuLabel: '目录',
returnToTopLabel: '回到顶部',
langMenuLabel: '切换语言',
skipToContentLabel: '跳至正文',
darkModeSwitchLabel: '主题',
lightModeSwitchTitle: '切换到浅色模式',
darkModeSwitchTitle: '切换到深色模式',
notFound: {
title: '页面不存在',
quote: '你访问的页面不存在或已移动。',
linkLabel: '返回文档首页',
linkText: '返回文档',
},
footer: {
copyright: 'FelisMC · 使用 VitePress 构建',
},
},
})
+24
View File
@@ -0,0 +1,24 @@
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import type { Theme } from 'vitepress'
import {
InjectionKey,
NolebaseEnhancedReadabilitiesMenu,
NolebaseEnhancedReadabilitiesScreenMenu,
} from '@nolebase/vitepress-plugin-enhanced-readabilities/client'
import { NolebaseHighlightTargetedHeading } from '@nolebase/vitepress-plugin-highlight-targeted-heading/client'
import '@nolebase/vitepress-plugin-enhanced-readabilities/client/style.css'
import '@nolebase/vitepress-plugin-highlight-targeted-heading/client/style.css'
import './style.css'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.provide(InjectionKey, { spotlight: { defaultToggle: true } })
},
Layout: () => h(DefaultTheme.Layout, null, {
'nav-bar-content-after': () => h(NolebaseEnhancedReadabilitiesMenu),
'nav-screen-content-after': () => h(NolebaseEnhancedReadabilitiesScreenMenu),
'layout-top': () => h(NolebaseHighlightTargetedHeading),
}),
} satisfies Theme
+68
View File
@@ -0,0 +1,68 @@
:root {
--felis-lime: #c7e86b;
--felis-lime-hover: #b7da54;
--vp-c-brand-1: #587f00;
--vp-c-brand-2: #7fa91c;
--vp-c-brand-3: #a6ce39;
--vp-c-brand-soft: rgb(199 232 107 / 26%);
--vp-c-bg: #ffffff;
--vp-c-bg-alt: #f5fbe8;
--vp-c-bg-soft: #f4fae8;
--vp-c-bg-elv: #ffffff;
--vp-sidebar-bg-color: var(--vp-c-bg);
--vp-c-text-1: #19220d;
--vp-c-text-2: #4f5d3c;
--vp-c-text-3: #6e7c5b;
--vp-c-divider: #e3eccf;
--vp-custom-block-warning-bg: #fff8d0;
--vp-button-brand-bg: var(--felis-lime);
--vp-button-brand-text: #263015;
--vp-button-brand-hover-bg: var(--felis-lime-hover);
--vp-button-brand-hover-text: #263015;
--vp-button-brand-active-bg: #acca48;
--vp-button-brand-active-text: #263015;
--vp-button-alt-bg: transparent;
--vp-button-alt-border: var(--vp-c-divider);
--vp-button-alt-hover-border: var(--vp-c-text-3);
--vp-font-family-base: 'Inter', 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--vp-font-family-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
--vp-nav-height: 76px;
}
.dark {
--vp-c-brand-1: var(--felis-lime);
--vp-c-brand-2: #d5ee93;
--vp-c-brand-3: #afd04f;
--vp-c-brand-soft: rgb(199 232 107 / 6%);
--vp-c-bg: #131610;
--vp-c-bg-alt: #181c14;
--vp-c-bg-soft: #1c2018;
--vp-c-bg-elv: #20251b;
--vp-c-text-1: #edf0e6;
--vp-c-text-2: #a6ad9b;
--vp-c-text-3: #808875;
--vp-c-divider: #2a3024;
}
body { -webkit-font-smoothing: antialiased; }
html[lang='zh-CN'] { --vp-code-copy-copied-text-content: '已复制'; }
::selection { color: var(--vp-c-text-1); background: var(--vp-c-brand-soft); }
:focus-visible { outline: 2px solid var(--vp-c-brand-1); outline-offset: 4px; }
.VPNavBarTitle .title { font-size: 21px; font-weight: 650; letter-spacing: -0.6px; }
.VPNavBarTitle .logo { width: 36px; height: 36px; }
.VPNavBarMenuLink { font-size: 13px; }
.VPNavBarSearch .DocSearch-Button { border: 1px solid var(--vp-c-divider); border-radius: 5px; background: transparent; }
.VPSocialLinks.VPNavBarSocialLinks.social-links { margin-right: 0; }
.VPSidebarItem.is-active > .item { padding: 0 10px; border-radius: 5px; background: var(--felis-lime); }
.VPSidebarItem.is-active > .item > .link .text { color: #263b08 !important; }
.vp-doc h1 { font-size: 34px; line-height: 1.35; font-weight: 600; letter-spacing: -0.04em; }
.vp-doc h2 { font-weight: 550; letter-spacing: -0.02em; }
.vp-doc p, .vp-doc li { line-height: 1.9; }
.vp-doc div[class*='language-'] { border: 1px solid var(--vp-c-divider); border-radius: 5px; }
.vp-doc .custom-block { border-radius: 5px; }
.vp-doc .mermaid { overflow-x: auto; }
.VPDocAsideOutline .outline-title { font-weight: 500; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation: none !important; transition: none !important; }
}
+25
View File
@@ -0,0 +1,25 @@
---
title: Build from source
---
# Build from source {#从源码构建}
Felis is built with Go and Node.js:
```bash
# Backend (Go 1.26+)
go build -o felis ./cmd/felis
# Frontend (Node.js 22+)
cd panel
npm ci
npm run build
# Docker image
docker build -t felis:custom .
```
See [Contributing](/en/reference/contributing) for the development environment, tests and plugin builds.
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md), [CONTRIBUTING.md](https://github.com/FelisMC/Felis/blob/main/CONTRIBUTING.md).
+40
View File
@@ -0,0 +1,40 @@
---
title: Installation and deployment
---
# Installation and deployment {#安装与部署}
> [!CAUTION]
> **This project is still in early development. Do not use it in production. The FelisMC team accepts no civil or criminal liability for problems arising from its use.**
On a prepared Linux host, run:
```bash
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash
```
The script installs K3s, deploys PostgreSQL and the control plane inside it, and launches a setup wizard. When setup completes, open the configured domain in a browser to reach the control panel.
* **Setup wizard**: The wizard first binds the platform Owner: join the address it shows in Minecraft Java Edition, then enter the 8-character link code that the login server displays (valid for 10 minutes). The step can be skipped and completed later by running `sudo felis setup` again; until an Owner is bound, nobody can sign in to the control panel, and the sign-in page states this together with the binding steps and the address to join. The installer launches the wizard automatically only on an interactive terminal; when output is redirected to a log or the install runs under cloud-init, run `sudo felis setup` after it finishes. Setting `FELIS_NO_SETUP=1` makes the installer end at its summary.
* **Supported hosts**: CentOS Stream 9 (aarch64) is verified on physical hardware; Ubuntu 24.04 (x86_64) is tested in CI on every push with a fresh install, a rerun, an upgrade and the install command above (see [operations §1](/en/operations/#_1-supported-hosts)).
* **Preflight checks**: Before modifying the host, the installer checks memory, disk, ports, network range conflicts, existing Kubernetes installations and outbound connectivity. If any check fails, it lists all problems and exits, leaving the host unchanged (see [operations §1](/en/operations/#_1-supported-hosts) for the checks).
* **Upgrading**: Rerun the install command to upgrade felis-api to a newer version; `felis setup` only uses the binary already installed on the host and cannot upgrade it. A rerun keeps the installed root domain, and the release channel must be specified again: hosts that follow the main branch must also set `export FELIS_VERSION_BOOTSTRAP=dev`. A PostgreSQL instance installed on the host by an earlier release is migrated into K3s during the rerun; the original instance on the host is stopped and retained for rollback (see [operations §4](/en/operations/#_4-upgrading-the-pieces-around-felis)).
<details>
<summary>Installation sources and restricted networks</summary>
<br>
A release installation takes the binary, all images and the Velocity plugin from the release assets prebuilt in CI, verifying each against `SHA256SUMS` before import. The host requires no Docker, Gradle or Go, and no access to Docker Hub. If an asset is missing or fails verification, only that image falls back to a local build, and the installer prints a notice (see [troubleshooting §15c](/en/operations/troubleshooting#_15c-the-installer-builds-on-the-host-although-it-installs-a-release)).
The assets can also be copied to the host in advance and installed with `FELIS_ARTIFACT_DIR=<absolute path>`; the Felis binary, images and plugin are then read from that directory. k3s and its images, the JRE, cloudflared, Velocity and the Via plugins are still downloaded from GitHub and PaperMC; hosts with SELinux enabled, such as RHEL, Fedora and openSUSE Leap, additionally install k3s-selinux from rpm.rancher.io; system packages come from the distribution's repositories.
A host with restricted outbound access must therefore allow HTTPS to these addresses or set `https_proxy`. Preflight probes each address before changing the host. Fully offline installation is not yet supported (see [operations §1](/en/operations/#_1-supported-hosts) for the address list).
</details>
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md).
+126
View File
@@ -0,0 +1,126 @@
---
title: Multi-node deployment
---
# A controller with multiple workers {#a-主控与多机-worker}
Distributed mode is disabled by default. A runs the sole Felis API/operator, k3s server, PostgreSQL, registry, archive service and system servers; Velocity remains a systemd service on A. Nodes B, C and others run only k3s-agent/containerd, game Pods and maintenance Jobs created by A. Every node must use the same architecture and k3s version as A; administrators must trust and maintain the hosts.
## Prepare A first {#先准备-a}
Stop the servers during a maintenance window, back up PostgreSQL using the existing database backup procedure, and keep an offline copy of the k3s state, server token and current installation configuration. For SQLite-backed k3s, the state directory is `/var/lib/rancher/k3s/server/db`; for another datastore, follow its own backup procedure. Do not copy these files to workers.
Record A's existing node name and keep it in subsequent installations. Pin the current control workloads to A before enabling WireGuard; existing game PVCs are not migrated.
```bash
# Run as root on A; replace the node name and every static node address.
A_NODE=existing-node-name
PEERS=192.0.2.10/32,192.0.2.11/32,192.0.2.12/32
k3s kubectl label node "$A_NODE" \
felis.node-restriction.kubernetes.io/identity="$A_NODE" \
felis.node-restriction.kubernetes.io/role=controller --overwrite
# Use the actual Deployment names and protected identity labels throughout.
for d in felis-api felis-operator felis-postgres registry; do
k3s kubectl -n felis patch deployment "$d" --type merge \
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"felis.node-restriction.kubernetes.io/identity\":\"$A_NODE\"}}}}}"
done
```
Rerun an installer that includes this feature on A with `FELIS_DISTRIBUTED=1`, `FELIS_NODE_EXTERNAL_IP=<A-static-public-IP>` and `FELIS_PEER_CIDRS="$PEERS"`, retaining the existing installation parameters. This enables `wireguard-native`, `flannel-external-ip`, NodeRestriction and a separate agent token, and installs the archive service, minimal RBAC and host isolation rules. Changing WireGuard requires a maintenance window with the servers stopped. Update existing workers' peer lists in advance as well.
Pushing to main alone does not publish a release. Until release assets contain these changes, run the following as root in the updated source checkout to build explicitly from main, retaining all other existing installation parameters. Updating only the installer while keeping the default release channel can download an older binary without distributed commands.
```bash
FELIS_REF=main FELIS_DISTRIBUTED=1 \
FELIS_NODE_EXTERNAL_IP=<A-static-public-IP> \
FELIS_PEER_CIDRS="$PEERS" bash deploy/bootstrap.sh
```
When generating manifests directly, add:
```bash
felis manifests --felis-image <existing-logical-image-reference> \
--distributed --controller-node "$A_NODE" \
--egress-probe felis-api.felis.svc:443 \
--velocity-cidr <A-exact-source-IP/32> \
--archive-local-path <original-archive.local_path> --backup-pvc felis-backups
```
Retain the other existing parameters. The installer also pins CoreDNS and local-path-provisioner to A; for a manual deployment, give these Deployments A's protected node selector too. Manual deployments must store the same random key in the `felis-archive-key` Secret in both the `felis` and `minecraft` namespaces (field `key`, at least 32 characters). Only the API, Reaper and archive service receive the key; the operator does not. The archive PVC, registry and database stay on A.
## Join B, then C {#接入-b-再接入-c}
First run `felis node firewall --peers "$PEERS" --controller-ip <A-address>` on every existing node to update the complete peer list; add `--controller` on A. Only exact `/32` or `/128` addresses are allowed. Do not use an entire node or Pod range as the Velocity or registry source. Allow WireGuard UDP 51820–51821 between static public node addresses, and allow k3s 6443 only from known peers.
```bash
# A: create a separate token for each worker, valid for 10 minutes by default.
# The output file is root-only; the token is not printed.
felis node token --name b --ttl 10m --out /root/b.bootstrap
k3s kubectl -n felis get svc registry
# Transfer this file and the same-version felis binary to B over trusted SSH/SCP.
# B needs no server token, admin kubeconfig, database or registry write credentials.
felis node join --name b --server https://<A-public-IP>:6443 \
--external-ip <B-public-IP> --token-file /root/b.bootstrap \
--registry-ip <Registry ClusterIP> --peers "$PEERS"
# A: SSH uses existing host-key verification; the target account needs sudo -n.
felis node approve --name b --ssh-target <B-SSH-alias> \
--image <Felis-logical-image-reference>
felis node list
```
Installing a worker does not install the database, Velocity, API or operator, and does not delete local volumes or node-password. A repeated installation rejects renaming the node or switching clusters. The mirror uses the registry ClusterIP, disables fallback to the default image source, and preserves the original image references.
Before approval, a worker has a `NoSchedule` quarantine taint and no protected approved label. Approval checks the architecture, version, node availability, WireGuard and host isolation, verifies that kubelet cannot modify protected labels, deletes the specified image and performs a real `crictl pull`. It then creates temporary probes to test cross-node Services, positive reachability of control services, and denial paths for game-labelled Pods. A connects to each test Service from the host and reads the source address actually observed by the backend; only exact addresses belonging to A are written into game policies. Any failed check keeps the node quarantined. Approval deletes the specified cached image, so run it before the node has game workloads.
Once approved, administrators can select the node when creating a server in the panel, or pass `nodeName` in the creation request. Ordinary server owners cannot select a node or specify a PVC. An unreachable node rejects new work; the operator removes the server Service's backends and Ready status, and Velocity follows its existing fallback flow without moving to another node.
## Migrate a stopped server {#停服迁移}
Stop the server in the panel and wait for `Stopped` and for the game Pod to exit. Open **Stopped migration** and select an online, approved target worker. The panel reads the latest operation from the CR's persistent record, so progress remains visible after a page refresh or an A restart.
```bash
# Root operations on A; separate from the database's felis migrate command:
felis server-migrate start --name survival --target-node c
felis server-migrate status --name survival
felis server-migrate retry --name survival --id <operation-ID>
```
Administrator API:
| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/api/v1/nodes` | Execution node list |
| POST | `/api/v1/servers/{name}/migrations` | `{ "targetNode": "c" }`; returns the operation and ID |
| GET | `/api/v1/servers/{name}/migrations` | Latest migration record |
| GET | `/api/v1/servers/{name}/migrations/{id}` | Current operation progress |
| POST | `/api/v1/servers/{name}/migrations/{id}/retry` | Retry the failed stage |
Stages are `backing_up` → `restoring` → `switching` → `succeeded`. The lock is `migration@<start-time>` and does not expire under the temporary maintenance lock's two-minute rule. Failure records the stage and reason while keeping the lock and stopped state. A repeated request for the same target returns the existing operation; other migrations are rejected. The restore Job checks the download's SHA-256 and reads back each restored file for verification. Only after success does a single optimistic-lock CR update switch the node, active PVC and progress. The stopped StatefulSet is rebuilt to use the new PVC; the CR, Service, ClusterIP, domain and ownership remain unchanged.
A successful migration still does not start the server automatically. Inspect the target world and start it manually. The recorded `sourcePVC` is retained and is not removed by the reaper; an administrator must explicitly clean it up once it is no longer needed. On failure, do not manually delete migration annotations or locks. Resolve source-node loss, insufficient target disk space or transfer/verification errors, then retry. A committed switch does not automatically roll back to the source world.
## Isolation and acceptance {#隔离与验收}
Game processes keep the existing restrictions: non-root, no privilege escalation, drop ALL, no service-account token and no host namespaces. A maintenance Job mounts only one world PVC. Transfer Jobs can access only the archive service and DNS; file/export Jobs additionally access only the existing API transfer endpoint. The archive service has no database configuration or Kubernetes identity. A records success only after the complete archive has been written atomically; existing tarLocal paths, retention and off-site workflows remain in use.
Host INPUT/FORWARD rules close the resident-node path, and raw PREROUTING closes worker NodePorts before DNAT; trusted control Pods on A can reach the apiserver from their exact addresses. Raw rules also deny new game-Pod connections to the host, preventing kube-router's early ACCEPT from bypassing filter rules; replies on established Velocity/RCON connections remain allowed. Rules are installed through systemd, with control-Pod addresses refreshed periodically. After changing node addresses, firewalls or CNI configuration, stop the servers and rerun approval checks before running untrusted code. The game egress gate checks both the allowed DNS TCP path and denial paths; in distributed mode, a timeout prevents startup.
Acceptance must be completed on three Linux machines, A/B/C; single-node unit tests do not replace it:
- Cross-node Velocity `ClusterIP:25565` connections and actual source addresses, RCON, idle shutdown and wake, and image pulls after cache removal.
- Backup and restore on B, B→C migration, unchanged Service IP/ownership, retained source PVC, and a target that remains stopped.
- Mutual exclusion of wake, file writes, export, reaping and duplicate requests during migration; reconciliation continues after A restarts.
- Source-node loss, interrupted transfers, full target/archive disks and read-back verification failures; confirm that the source world remains recoverable.
- Game-Pod requests to other servers, the controller, registry, host ports, kubelet and metadata are all denied; every denial target is reachable by a trusted positive probe.
- Expired, replayed, cross-server and wrong-operation archive tokens are rejected; kubelet cannot forge protected labels.
Current local verification uses one existing ARM64 CentOS Stream 9 Felis VM and a temporary test program for the archive and migration logic. `deploy/test-node-firewall.sh` tests resident-node, NodePort DNAT and early-ACCEPT protection in isolated network namespaces without changing the VM's existing cluster network. Three-machine network acceptance remains mandatory before deployment. A remains a single point of failure for the control plane and public entry; the first version has no controller HA or automatic failover.
Related upstream documentation: [k3s networking across public networks](https://docs.k3s.io/networking/distributed-multicloud), [time-limited bootstrap tokens](https://docs.k3s.io/cli/token), [NodeRestriction labels](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-isolationrestriction), [NetworkPolicy node boundaries](https://kubernetes.io/docs/concepts/services-networking/network-policies/).
---
Source: [docs/distributed.md](https://github.com/FelisMC/Felis/blob/main/docs/distributed.md).
+39
View File
@@ -0,0 +1,39 @@
---
title: Managing servers
---
# Managing servers {#管理服务器}
* **On-demand Start and Stop**: A server starts when a player connects to the proxy. The player waits in a queue during start-up and is transferred once the server is ready. Idle servers stop automatically to free memory.
* **Web Dashboard**: Monitor server status, online players, and resource usage from your browser.
* Console (RCON), whitelist, bans, OPs and LuckPerms permissions
* File manager: create, delete, rename, chunked upload, download, and unzip while the server is stopped; also used to import worlds
* Schedules: run commands, restart, stop, start or back up by weekday and time zone, with an in-game warning to players beforehand
* **Multi-core Support**: Compatible with Paper, Fabric, Forge, and NeoForge, accessed through a single Velocity proxy.
* **Modpack Submission**: Players can upload modpacks. After admin approval, each modpack is built automatically and scanned with Trivy; the result is added to the image whitelist and can be selected as a server image.
## 12. A configuration field seems to be ignored {#_12-a-configuration-field-seems-to-be-ignored}
Every field below is read by a controller. What varies is the condition that
decides whether setting it does anything.
| Field | What you might expect | Reality |
|---|---|---|
| `spec.startup.timeoutSeconds` | Start budget before `Failed` | Read by `startupTimedOut` (`reconciler.go:479`), called at `:126`. `0` or unset falls back to **300s**, then `markFailed("StartupTimeout")` |
| `spec.startup.readinessTimeoutSeconds` | First-probe budget | Read by `readinessTimedOut` (`reconciler.go:490`), called at `:157`. `0` or unset falls back to **300s**, then `markFailed("ReadinessTimeout")`. Not to be confused with the prober's own 5s dial timeout (`prober.go:45`) |
| `spec.idle.autoStopEnabled` | Auto-stop empty servers | Read at `reconciler.go:175` — but gated on `spec.rcon.enabled`, since the player tally comes from the RCON probe (§11) |
| `spec.idle.emptySecondsBeforeStop` | Empty grace period | Same branch. Must be `> 0`; the guard treats `0` as "off", not "stop immediately" |
Both startup budgets are measured from the same `status.startRequestedAt`, so
`readinessTimeoutSeconds` is not a budget *after* pod readiness — it is a
deadline for the whole start, applied on the RCON-probe branch.
---
See [Troubleshooting](/en/operations/troubleshooting) for startup, connection, idle shutdown and file operations.
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md), [docs/troubleshooting.md](https://github.com/FelisMC/Felis/blob/main/docs/troubleshooting.md).
+70
View File
@@ -0,0 +1,70 @@
---
title: Introduction to Felis
---
# Introduction to Felis {#认识-felis}
A Kubernetes-driven Minecraft server hosting platform.
One command to deploy, with automatic lifecycle, backup, and security.
> [!CAUTION]
> **This project is still in early development. Do not use it in production. The FelisMC team accepts no civil or criminal liability for problems arising from its use.**
## Features {#特性}
* **On-demand Start and Stop**: A server starts when a player connects to the proxy. The player waits in a queue during start-up and is transferred once the server is ready. Idle servers stop automatically to free memory.
* **Web Dashboard**: Monitor server status, online players, and resource usage from your browser.
* Console (RCON), whitelist, bans, OPs and LuckPerms permissions
* File manager: create, delete, rename, chunked upload, download, and unzip while the server is stopped; also used to import worlds
* Schedules: run commands, restart, stop, start or back up by weekday and time zone, with an in-game warning to players beforehand
* **Backup & Restore**: Enabled by default; the installer renders the archive PVC and its path.
* Manual backups: archive a server's entire data volume (`/data`, including worlds, configuration, plugins and mods) to the cluster's archive store, with rollback to any backup point
* Daily restore points: a server played that day gets a restore point once it stops; by default 7 are kept for up to 90 days, rotated separately from manual backups
* Download and export: download a single backup (with sha256 verification), delete a single backup, or export a whole world
* Off-site copy (optional): backups are encrypted on the host and synced to S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, MinIO and others)
* Control-plane database: the database holding accounts, server ownership, quotas and the archive index is backed up daily and snapshotted before every upgrade migration; `felis db restore` rolls it back atomically, and the panel's Maintenance & Backups page shows the age of the latest backup (see [troubleshooting §16](/en/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery))
* **Diagnostics**
* `sudo felis status`: a summary of the node, control plane, game proxy, each server, backups and open alerts
* `sudo felis doctor`: runs all health checks and lists problems by area with troubleshooting pointers; sends no email
* `sudo felis support-bundle`: generates a redacted diagnostics archive to attach to support requests (see [troubleshooting §0](/en/operations/troubleshooting#_0-first-look-felis-status-felis-doctor-felis-support-bundle))
* Watchdog: runs a check every 2 minutes and emails the platform owners when a problem persists; supports an external heartbeat monitor
* **World Reaper** (optional): Worlds idle for more than 15 days are backed up and then removed to free disk space. Enable it by setting `FELIS_WORLDS_HOST_PATH` at install time (on k3s: `/var/lib/rancher/k3s/storage`); without it, no world is deleted. Expired backups are cleaned up daily regardless of this setting.
* **Multi-core Support**: Compatible with Paper, Fabric, Forge, and NeoForge, accessed through a single Velocity proxy.
* **Modpack Submission**: Players can upload modpacks. After admin approval, each modpack is built automatically and scanned with Trivy; the result is added to the image whitelist and can be selected as a server image.
* **Security**
* Passkey login: passwordless authentication via fingerprint, face recognition, or hardware security keys
* Zero-trust access: panel traffic is protected by Cloudflare Access, and the internal API is not exposed to the internet
* **Multi-node Deployment** (experimental, off by default): a single controller node issues all commands, the other nodes run game servers only, and a stopped server can be migrated to another node. Currently available only on the main branch; three-node acceptance testing is not yet complete (see [distributed mode](/en/guide/distributed)).
## Read the documentation {#阅读文档}
- [Installation and deployment](/en/guide/deployment)
- [Operations guide](/en/operations/)
- [Troubleshooting](/en/operations/troubleshooting)
- [Multi-node deployment](/en/guide/distributed)
- [Contributing](/en/reference/contributing)
- [Project README](/en/reference/readme-en)
## Acknowledgements {#致谢}
* [Kubernetes](https://kubernetes.io/): Container orchestration engine
* [K3s](https://k3s.io/): Lightweight Kubernetes distribution
* [Cloudflare Zero Trust](https://www.cloudflare.com/zero-trust/): Zero trust security infrastructure
* [PostgreSQL](https://www.postgresql.org/): Data persistence
* [React](https://react.dev/): User interface framework
* [Vite](https://vitejs.dev/): Frontend build tool
* [TailwindCSS](https://tailwindcss.com/): CSS framework
* [Bubble Tea](https://github.com/charmbracelet/bubbletea): TUI framework
* [Minecraft](https://www.minecraft.net/): The game this project serves
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md).
+32
View File
@@ -0,0 +1,32 @@
---
title: Backup and restore
---
# Backup and restore {#备份与恢复}
* **Backup & Restore**: Enabled by default; the installer renders the archive PVC and its path.
* Manual backups: archive a server's entire data volume (`/data`, including worlds, configuration, plugins and mods) to the cluster's archive store, with rollback to any backup point
* Daily restore points: a server played that day gets a restore point once it stops; by default 7 are kept for up to 90 days, rotated separately from manual backups
* Download and export: download a single backup (with sha256 verification), delete a single backup, or export a whole world
* Off-site copy (optional): backups are encrypted on the host and synced to S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, MinIO and others)
* Control-plane database: the database holding accounts, server ownership, quotas and the archive index is backed up daily and snapshotted before every upgrade migration; `felis db restore` rolls it back atomically, and the panel's Maintenance & Backups page shows the age of the latest backup (see [troubleshooting §16](/en/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery))
## What a backup contains {#what-a-backup-contains}
A backup tars the server's ENTIRE data volume — the same volume the server mounts
at `/data`: world folders, `server.properties`, plugins/mods, configs, jars,
libraries, logs and cache, not just the `world/` directory. A restore replaces the
volume's contents with the archive (files added since the backup are pruned), so a
restore also rolls config/plugin changes back. Sizes are dominated by
libraries/cache on stock Paper servers (~170MB for a fresh instance before any
world growth) — do not size the archive PVC as if only world data were stored.
## Detailed procedures {#详细操作}
- [World backups, downloads, exports and daily restore points](/en/operations/troubleshooting#_10-world-reaper-false-deletes-and-skipped-backups-spec-§18)
- [Control-plane database backups and disaster recovery](/en/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery)
- [Disaster recovery and planned migration](/en/operations/#_5-disaster-recovery)
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md), [docs/troubleshooting.md](https://github.com/FelisMC/Felis/blob/main/docs/troubleshooting.md), [docs/operations.md](https://github.com/FelisMC/Felis/blob/main/docs/operations.md).
+853
View File
@@ -0,0 +1,853 @@
---
title: Operations guide
---
# Felis Operations Guide {#felis-operations-guide}
What a Felis host needs, how big it should be, how to take Felis off it again, and where
the disaster-recovery procedures live. Fault-finding is in
[troubleshooting.md](/en/operations/troubleshooting); this document refers to its sections as §N.
Evidence tags follow troubleshooting.md: **[VM-VERIFIED]** was run on a real host,
**[CI]** runs end to end on every push to main (`.github/workflows/e2e.yml`),
**[GO-TESTED]** / **[SH-TESTED]** is covered by `go test` or the shell tests under
`deploy/`, **[CODE-ONLY]** is what the code does and has not been run end to end.
## 1. Supported hosts {#_1-supported-hosts}
`deploy/bootstrap.sh` defaults to a single node. For the opt-in A controller / worker deployment, see [distributed.md](/en/guide/distributed). It needs systemd, root, and one of the
package managers below; everything else (k3s, the JRE, cloudflared, and Docker when an image
has to be built on the host; see "Where the binary and the images come from" below) it
installs.
PostgreSQL runs inside k3s as the `felis-postgres` Deployment, from the official image the
release pins by digest, with its data on the host in `/var/lib/felis/postgres`.
| OS family | Package manager | Architectures | Status |
|---|---|---|---|
| CentOS Stream 9 (firewalld active, SELinux enforcing) | dnf | aarch64 | **[VM-VERIFIED]** fresh install from release assets and its rerun, upgrade from v0.1.0 (moving the database off the host PostgreSQL 13 into felis-postgres), uninstall and reinstall |
| Ubuntu 24.04 LTS | apt | x86_64 | **[CI]** fresh install and same-commit rerun from the pushed commit's release assets; the README's one-line install as a new host runs it (the newest release's own assets); upgrade from the newest release, installed from its assets by its own installer and seeded with rows in twelve tables, onto them, every seeded row read back unchanged; the on-host build weekly |
| RHEL / Rocky / Alma 9, Fedora | dnf | x86_64, aarch64 | [CODE-ONLY] same code path as CentOS Stream |
| Debian 12, other Ubuntu releases | apt | x86_64, aarch64 | [CODE-ONLY] |
| openSUSE Leap / Tumbleweed | zypper | x86_64, aarch64 | [CODE-ONLY] |
| Arch Linux | pacman | x86_64, aarch64 | [CODE-ONLY] |
Pinned component versions (a fresh install gets exactly these; an installed k3s or
cloudflared is left as it is, see §4):
| Component | Version | Where it is pinned |
|---|---|---|
| k3s | v1.36.4+k3s1 | `FELIS_K3S_VERSION` in `bootstrap.sh` |
| cloudflared | 2026.9.1 | `FELIS_CLOUDFLARED_VERSION`, sha256 per architecture |
| Temurin JRE (Velocity) | 25, patch build pinned | `FELIS_JRE_VERSION`, sha256 per architecture |
| Go (nano builds) | 1.26.8 | `GO_PINNED_VERSION`, sha256 per architecture |
| Minecraft / Limbo / Paper / Velocity / LuckPerms | `deploy/game-stack.lock` | §15b |
| PostgreSQL | 18.6, the official `postgres` image by digest | `POSTGRES_IMAGE` in `bootstrap.sh`, `defaultPostgresImage` in `internal/platform` |
32-bit hosts are not supported: there is no k3s, JRE or Go build the installer will fetch
for them.
Before it changes anything the installer checks the host and reports every problem at
once, then stops with nothing touched **[SH-TESTED]**:
- the architecture, systemd as init, and the memory cgroup controller k3s needs;
- RAM: under 1.75 GiB is refused (a "2 GB" VPS passes), under 3.5 GiB is a warning;
- free disk on each filesystem it writes to, summed when they share one: on a bare host
about 17 GiB installing a release, 15 GiB from `FELIS_ARTIFACT_DIR` and 23 GiB when it
builds the images itself; 7 GiB for a rerun; a directory that already holds data
(Docker's cache, a reused k3s) counting at the rerun size; a filesystem that would end
over 85%, where k3s starts deleting cached images, is a warning;
- the ports it will listen on: the game port, the panel NodePort, k3s's 6443/6444 and
10248–10259 and the registry's loopback 5000. A port held by the installer's own
proxy or k3s is a rerun and passes;
- another Kubernetes (kubelet, RKE2, k0s, MicroK8s) or a k3s agent on the host;
- the node address or a routed network inside k3s's `10.42.0.0/16` and `10.43.0.0/16`
(a Docker network there is the usual case); a wider route such as a `10.0.0.0/8` VPN
is a warning;
- HTTPS to the hosts it downloads from: GitHub and PaperMC's download API always, Docker
Hub when it builds images on the host, Rancher's RPM repository where k3s's installer
adds it (the list is under "Where the binary and the images come from"). A host counts
as reachable once a TLS handshake with it completes, and each gets three tries two
seconds apart. Installing a release, an
unreachable Docker Hub is a warning (it is needed only if an asset turns out unusable);
from `FELIS_ARTIFACT_DIR` it is not checked.
`FELIS_PREFLIGHT=warn` reports the same problems as warnings and installs anyway, for a
host the checks misjudge.
A host firewall is opened, never turned off **[SH-TESTED]**. With firewalld active the
installer adds the panel NodePort, the game port and 6443, and puts k3s's pod and service
ranges in the trusted zone. With ufw enabled (common on Ubuntu and Debian, and enabled in
the CI install **[CI]**) it admits `10.42.0.0/16` and `10.43.0.0/16`, the panel NodePort and
the game port, each rule commented `felis-…`; 6443 stays closed to the network, since pods
reach the API server from their own range. Felis-nano opens its port to
`FELIS_NANO_PROXY_CIDR` alone in either. `uninstall.sh` removes these again, the k3s ranges
only when k3s goes too. Any other firewall in front of the host must admit the same:
dropped pod traffic shows up as the first rollout timing out ("control-plane rollout did
not complete").
Two things the host must keep for as long as the install lives:
- **Its address.** The install is bound to the IPv4 address it was made on (the k3s
node, the network policies, the panel certificate and the default nip.io domain all
carry it). Give the host a static address or a DHCP reservation before installing;
the installer warns when the address is a lease, and the watchdog reports
`host-address` when the host loses it (troubleshooting §13c). The k3s node name is
pinned at install time, so a hostname change is harmless.
- **A synchronized clock.** The installer turns NTP on (chrony where nothing else can)
and the watchdog reports a clock that stays unsynchronized. Allow outbound UDP 123,
or set `FELIS_MANAGE_TIME_SYNC=0` on a host whose clock is kept another way.
The installer also makes the system journal persistent (capped at
`FELIS_JOURNAL_MAX_USE`, default 1G; `FELIS_MANAGE_JOURNAL=0` skips it) and writes the
admin kubeconfig `/etc/rancher/k3s/k3s.yaml` root-only: run `sudo k3s kubectl`.
Single-node deployment remains the default. The opt-in [distributed mode](/en/guide/distributed)
keeps the sole API and operator on A and runs games on approved k3s agents. A world is
a ReadWriteOnce claim on its node's local-path storage; moving it requires an explicit
stopped migration through A's archive service. There is no automatic failover or
standby controller. An A restart pauses control operations until its workloads return;
a lost worker leaves its worlds on that node. Cross-node networking still requires the
three-machine acceptance described in the distributed runbook.
### Where the binary and the images come from {#where-the-binary-and-the-images-come-from}
A release install (the default channel, and the setup console) takes everything Felis
builds from that release's assets, each checked against the release's `SHA256SUMS` before
it is used: the `felis` binary, the control-plane image, the limbo, lobby and paper images,
the registry and PostgreSQL images (at the digests `bootstrap.sh` pins), and
`felis-velocity.jar`. The images go into k3s's containerd with `k3s ctr images import` and
from there into the in-cluster registry, so the host needs no Docker, Gradle, Go or Docker
Hub for them. k3s's own images come from k3s's GitHub release
(`k3s-airgap-images-<arch>.tar.zst`, checked against k3s's sha256 list) before k3s first
starts. An upgrade downloads only the image tars holding an image the host lacks; they wait
in `/var/lib/felis/artifacts` until the registry has the images, and are deleted then.
`deploy/build-release-artifacts.sh` documents every asset. The decisions are **[SH-TESTED]**.
Installing from the assets is **[VM-VERIFIED]** on CentOS Stream 9 aarch64 through
`FELIS_ARTIFACT_DIR`: a fresh install and an upgrade over a release that built on the host
pulled no image and built nothing, and a rerun imported and uploaded nothing. Downloading them from a
release is [SH-TESTED] until a release publishes assets.
The release is the newest one unless `FELIS_RELEASE=<tag>` names an earlier one, which
installs from that release's assets the same way: the way back after a bad upgrade
(troubleshooting §16, "Roll back an upgrade that broke the database"), with the installer
read at that tag.
The installer builds on the host instead, installing Docker for it and stopping Docker once
the images are in the registry, when:
- the source is not a release: `FELIS_VERSION_BOOTSTRAP=dev`, a pinned `FELIS_REF`, or
`FELIS_SKIP_FETCH`;
- `FELIS_GAME_STACK=latest`, for the login, lobby and paper images (the rest still come
from the release);
- the release publishes no `SHA256SUMS` (one cut before release assets existed, or still
uploading), or an asset is missing, fails its checksum or is malformed. Only that image is
built (the registry and PostgreSQL images are pulled from Docker Hub instead), and a
warning names it; troubleshooting §15c lists the messages. Each download is tried
three times first, and a host without the room for the build stops before installing
Docker (troubleshooting §15c).
`FELIS_ARTIFACT_DIR=<absolute path>` installs from a directory instead of the release: a
release's assets downloaded there (every `felis-*` file and `SHA256SUMS`), or the directory
`deploy/build-release-artifacts.sh <version> <dir>` wrote. Nothing of Felis's own is
downloaded or built (except the game images under `FELIS_GAME_STACK=latest`, which no release
ships), so an asset the directory lacks, or one failing its checksum, stops the install. It
cannot be combined with `FELIS_REF` or `FELIS_SKIP_FETCH`, which name a source too.
The rest of the host's software still downloads, so the host needs outbound HTTPS to these,
directly or through `https_proxy`. A host with no outbound access cannot be installed yet
**[SH-TESTED]**:
| Host | What comes from it |
|---|---|
| `github.com`, and the githubusercontent.com hosts its release downloads redirect to | k3s and its images (`k3s-airgap-images-<arch>.tar.zst`), cloudflared, the Temurin JRE, ViaVersion, ViaBackwards and ViaRewind |
| `raw.githubusercontent.com` | k3s's install script, until k3s is installed |
| `rpm.rancher.io` | k3s-selinux, which k3s's install script adds on an SELinux host of the Red Hat or SUSE family (CentOS Stream, RHEL, Rocky, Alma, Fedora, openSUSE Leap), until k3s is installed |
| `fill-data.papermc.io` | the Velocity jar, unless `FELIS_VELOCITY_FORK_JAR` supplies one |
| the distribution's package mirrors | the base packages (CA certificates, OpenSSL, curl and tar where missing), and container-selinux beside k3s-selinux |
Preflight probes each named host above before it changes anything and lists every one it
cannot reach in one refusal (`cannot reach … over HTTPS`); the package manager reports its
own mirrors. An override adds a host the download itself tries: `FELIS_JRE_VERSION` reads
`api.adoptium.net`, a `FELIS_VELOCITY_VERSION` other than the pinned one reads
`fill.papermc.io`, and `FELIS_GAME_STACK=latest` builds its images on the host from Docker
Hub (which preflight probes), PaperMC, Limbo's CI and LuckPerms.
```
# on a machine with access: the release's assets for the host's architecture
gh release download v1.4.0 --repo FelisMC/Felis --dir felis-v1.4.0 \
--pattern 'felis-*linux-amd64*' --pattern felis-velocity.jar --pattern SHA256SUMS
# on the host, after copying the directory over
sudo FELIS_ARTIFACT_DIR=/root/felis-v1.4.0 bash bootstrap.sh
```
`SHA256SUMS` lists both architectures; the files of the other one may be left out.
### While felis-api restarts {#while-felis-api-restarts}
An installer rerun that changes felis-api, a node restart or a crashed pod takes the API
away until its new pod is ready: about 12 s on the reference VM (`kubectl rollout
restart` to Available). Its Deployment keeps one replica with the Recreate strategy, so
the old pod is gone before the new one starts. Two pods at once would be wrong for
felis-api: the uploads volume is ReadWriteOnce, a chunked upload is serialized inside the
process, and the build reconciler, restore settler, registry pruner, upload reapers and
audit retention run in-process without leader election, so each would run twice. During
the window:
- Players already on a server stay there; game servers keep running.
- A player leaving the login gate or joining a server by its address is admitted when
felis-api confirmed their link within the last 10 minutes; anyone else is told login
verification is temporarily unavailable.
- The login gate retries a new login for up to 60 s and tells the player it is retrying,
so a restart shorter than that only delays the login.
- Wakes, stops, `/link` and the panel wait for the API.
- A Velocity restart in the window routes on
`/opt/felis/velocity/plugins/felis-link/last-servers.json`, the last server list the
API answered with, until a refresh succeeds (every 15 s).
A longer outage reads like this in the logs [VM-VERIFIED]. The drill scaled felis-api
to 0 for about 8 minutes on the reference VM.
- The proxy logged `server list refresh failed ... keeping current registrations` 11 s
in, then `still failing: 22 failed attempts over 304 s` at the 5-minute mark.
- The watchdog found `deployment/felis-api` critical on its first run after the scale.
It raised the alert on the first run past 5 minutes, at about 7 minutes; with no
`[smtp]` that is logged only (`journalctl -u felis-watchdog`).
- The proxy logged `server list refresh recovered after 32 failed attempts over 469 s`
as soon as the new pod was Available.
## 2. Sizing {#_2-sizing}
### What the platform itself uses {#what-the-platform-itself-uses}
Measured on the verification host (4 vCPU, 5.5 GB RAM, 6 GB swap, CentOS Stream 9
aarch64) on an idle network, as each process's proportional set size (PSS: a page shared
by several processes is split among them; `/proc/<pid>/smaps_rollup`) **[VM-VERIFIED]**:
| Process | Memory (PSS) |
|---|---|
| k3s (API server, controllers, scheduler, kubelet) | ~370 MiB |
| k3s's containerd and the pods' shims | ~170 MiB |
| CoreDNS and the local-path volume provisioner | ~105 MiB |
| Velocity (`-Xms16M -Xmx1G`, idle; it grows with players) | ~175 MiB |
| felis-api, felis-operator, registry gate | ~85 MiB together |
| Image registry | ~25 MiB |
| PostgreSQL (the felis-postgres pod) | ~40 MiB plus page cache |
| **Infrastructure total** | **~1 GB** |
The installer runs k3s, and the containerd it starts, with Go's collector at half its
default heap growth (`GOGC=50`, in `/etc/systemd/system/k3s.service.d/50-felis.conf`): an
idle k3s holds about 150 MiB live and would otherwise let its heap reach twice that before
collecting. It saves about 70 MiB for about 2% of one core. An install from before this
picks it up on its next installer run, which restarts k3s; the pods keep running.
The login (Limbo, pod limit 512 MiB, ~0.16 GB) and lobby (Paper, pod limit 1 GiB,
~0.7–0.85 GB) system servers come on top, and every game server adds the memory its owner
gave it: the pod's limit equals its request, and the JVM heap is derived from it (§1a).
Quotas cap it per user (panel → Admin → Quotas).
A release install builds nothing (§1). When the installer builds on the host its peak is
the image builds (Docker plus a Gradle container). Afterwards it stops Docker, and Docker's
containerd when nothing else uses it, so that memory goes back to the servers; a Docker the
installer put there does not start at boot. On a host under 2 GB of RAM without swap it
adds a 2 GiB `/swapfile`.
### Recommendations {#recommendations}
| Concurrent players | Game servers running | CPU | RAM | `FELIS_VELOCITY_XMX` |
|---|---|---|---|---|
| up to 20 | 1–2 small | 2 vCPU | 4 GB + 2 GB swap | 1G (default) |
| up to 100 | 3–5 | 4 vCPU | 8–16 GB | 1G |
| up to 300 | 5–10 | 8 vCPU | 16–32 GB | 2G |
| 300+ | more | 8+ vCPU | 32 GB+ | 3G–4G |
The player-count rows are planning figures, not measurements: a Minecraft server's cost
depends mostly on what its players do (view distance, redstone, mods). Size RAM as the
infrastructure's ~1 GB and the login and lobby servers' ~1 GB, plus the sum of the servers
you expect to run at once, then add a quarter for the page cache and PostgreSQL. Velocity
itself needs little per player; raise its heap when `journalctl -u felis-velocity` shows
long GC pauses or `OutOfMemoryError`.
`FELIS_VELOCITY_XMX` (default `1G`, at least `256M`, written `<n>M` or `<n>G`) is read on
every installer run. Up to 1G the heap starts at 16M and the proxy runs the serial collector
and only the C1 compiler: its plugins hold about 50M live, so a collection takes
milliseconds, and compression and encryption run in Velocity's native library. Above 1G it
runs G1 from a 64M start, since a serial full collection over a large heap would stall
every player at once, and a periodic collection hands the growth back once players have
left. Changing it rewrites the unit, and the rerun restarts the proxy, which disconnects
everyone online; do it in a quiet hour **[VM-VERIFIED]**:
```
curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo FELIS_VELOCITY_XMX=2G bash
```
### Disk {#disk}
| What | Where | Size |
|---|---|---|
| Worlds | one volume per server under `/var/lib/rancher/k3s/storage` | what the world grows to |
| World archives | the `felis-backups` volume (`FELIS_BACKUP_STORAGE`, default 10Gi requested) | about one compressed world per backup kept |
| In-cluster registry | the `registry` volume (default 10Gi requested) | 2–3 GB for the stock images; grows with custom builds, pruned daily (§9) |
| k3s's containerd images | `/var/lib/rancher/k3s/agent/containerd` | 6–9 GB |
| Docker's images and build cache | `/var/lib/containerd` (Docker's containerd store), on a host that built its images (§1) | 5–10 GB after repeated upgrades |
| Release assets during an install | `/var/lib/felis/artifacts` | up to ~2 GB, deleted once the images are in the registry |
| Toolchains and sources | `/opt/felis` | ~2.5 GB |
| Database | `/var/lib/felis/postgres` (felis-postgres's cluster) | tens of MB; the audit log is most of it |
| Database bundles | `/var/lib/felis/db-backups` | a few MB each, 14 daily kept |
k3s's local-path volumes do not enforce the requested sizes (§9), so every volume shares
the root filesystem. Give the host at least **40 GB**, and 60 GB or more once worlds and
custom images accumulate. The watchdog mails the owners when a watched filesystem passes
its threshold, and §13b covers a full disk. On a host that built its images,
`docker builder prune -af` (with Docker started) reclaims the build cache when space is
short; the next upgrade rebuilds it.
### Growing the disk {#growing-the-disk}
Everything above shares the root filesystem, so more room means a bigger root
filesystem. It grows in place, with everything running: enlarge the virtual disk at the
provider, then the partition and the filesystem on it.
```bash
sudo felis backup-now -yes # a mistyped partition number is how a resize loses a disk
lsblk -f # which disk and partition hold /, and whether LVM sits on it
sudo growpart /dev/vda 3 # cloud-utils-growpart (RHEL) / cloud-guest-utils (Debian, Ubuntu)
# LVM (the RHEL-family default):
sudo pvresize /dev/vda3
sudo lvextend -r -l +100%FREE /dev/<vg>/root # -r grows the filesystem with it
# no LVM:
sudo xfs_growfs / # xfs
sudo resize2fs /dev/vda3 # ext4
df -h /
```
`felis backup-now` (troubleshooting.md §10) archives every stopped world; add `-stop` to
include the running ones.
### Moving the data to its own disk [VM-VERIFIED] {#moving-the-data-to-its-own-disk-vm-verified}
The bulk lives under `/var/lib/rancher/k3s`: the worlds, the world archives, the registry
and the images. On a disk of its own it grows without touching the system, and a full
world store leaves the root filesystem alone. The database and its bundles
(`/var/lib/felis`) are small and stay on the root disk. The move takes the platform down
for the copy plus a minute or two: the drill copied 4.2 GB in 18 s, and felis-api answered
`/readyz` 14 s after k3s started on the new disk.
1. Attach the disk and put a filesystem on it (the whole disk; `lsblk` shows it empty):
```bash
sudo mkfs.xfs /dev/vdb
U=$(sudo blkid -s UUID -o value /dev/vdb)
```
2. Archive every world, stopping the servers so each one saves, and keep the watchdog
quiet for the next hour (the marker the installer writes: no mail, no failure pings
to the heartbeat, until the time in it):
```bash
sudo felis backup-now -yes -stop
sudo install -d -m 0755 /run/felis
echo $(( $(date +%s) + 3600 )) | sudo tee /run/felis/watchdog-quiet-until
```
3. Stop k3s and copy:
```bash
sudo systemctl stop k3s
sudo /usr/local/bin/k3s-killall.sh # the containers k3s leaves running, and their mounts
sudo mkdir -p /mnt/felis-data
sudo mount UUID=$U /mnt/felis-data
sudo rsync -aHAX --numeric-ids /var/lib/rancher/k3s/ /mnt/felis-data/
sudo umount /mnt/felis-data
```
`-X` carries the SELinux labels k3s set itself. Leave `restorecon` out: it would reset
runc and the CNI binaries from `container_runtime_exec_t` to the policy default.
4. Mount it in place, and tie k3s to the mount:
```bash
sudo mv /var/lib/rancher/k3s /var/lib/rancher/k3s.old
sudo mkdir /var/lib/rancher/k3s
echo "UUID=$U /var/lib/rancher/k3s xfs defaults,nofail 0 0" | sudo tee -a /etc/fstab
sudo mkdir -p /etc/systemd/system/k3s.service.d
printf '[Unit]\nRequiresMountsFor=/var/lib/rancher/k3s\n' | sudo tee /etc/systemd/system/k3s.service.d/data-disk.conf
sudo systemctl daemon-reload
sudo mount /var/lib/rancher/k3s
sudo systemctl start k3s
```
The drop-in is what keeps the data safe: k3s started on the empty mount point creates
a new, empty cluster there. With it, a disk that does not come up fails the start with
`A dependency job for k3s.service failed`, and `nofail` keeps the host booting so you
can reach it. In the drill a detached disk left k3s inactive and the mount point empty;
reattached, `systemctl start k3s` mounted it and started.
5. Check that `sudo k3s kubectl -n felis get pods` shows every pod ready and
`findmnt /var/lib/rancher/k3s` names the new disk, then start the servers from the
panel and `sudo rm /run/felis/watchdog-quiet-until`. Once the host has run a day,
`sudo rm -rf /var/lib/rancher/k3s.old` frees the root disk.
The watchdog already watches `/var/lib/rancher/k3s` as a filesystem of its own (its
`-disk-paths`), so the new disk's fill level is mailed like the root's.
## 3. Uninstall {#_3-uninstall}
`deploy/uninstall.sh` takes off what the installer put on. It prints what it will remove
and asks before it starts (`--yes` skips the question) **[SH-TESTED]
[VM-VERIFIED]**:
```
curl -fsSL <raw-url>/deploy/uninstall.sh | sudo bash -s -- --yes # keep the data
curl -fsSL <raw-url>/deploy/uninstall.sh | sudo bash -s -- --purge # remove the data too
```
Both modes remove the `felis-*` systemd units and `cloudflared-felis.service`, the
Velocity user, `/opt/felis`, `/usr/local/bin/felis`, the release assets an interrupted
install left in `/var/lib/felis/artifacts`, the installer's cloudflared binary (unless
another unit runs it), the `felis_edge` nftables table (and `felis_postgres`, which
releases before the database moved into k3s loaded), the firewalld ports the installer
opened and its `felis-`-commented ufw rules. k3s goes with k3s's own `k3s-uninstall.sh` when the cluster holds nothing but
Felis's namespaces; when it runs anything else only `felis`, `minecraft`, `felis-build`
and the MinecraftServer CRD are deleted.
`--keep-k3s` and `--remove-k3s` override that choice.
| | keep data (default) | `--purge` |
|---|---|---|
| Final database bundle | taken first (`felis db backup -label manual`); a failure stops the uninstall before anything is removed. `--no-backup` skips it | none |
| The database (`/var/lib/felis/postgres`) | kept; felis-postgres is stopped cleanly before k3s goes | deleted with `/var/lib/felis` |
| A host PostgreSQL an earlier release ran the database on | kept as it is: stopped after the move into k3s (below, §4), with its old copy of `felis` | its `felis` database and role are dropped (the server is started for that and stopped again), and `listen_addresses` and `pg_hba.conf` go back to how they were. Checked before anything is removed: a role that still owns another database (the `felis_pgint` the PG contract tests use, CONTRIBUTING.md) or holds grants elsewhere stops the purge up front with the list and the `ALTER DATABASE … OWNER TO postgres` to run |
| `/etc/felis` (secrets, `felis.toml`, `offsite.env`, the mail relay password and uploads bucket keys `felis setup` took, tunnel config) | kept; `bootstrap.done` and the per-run records go | deleted, with the tunnel's credentials file |
| `/var/lib/felis` (the database, its bundles) | kept | deleted |
| Worlds, archives, registry, uploads | moved to `/var/lib/felis/retained/k3s-storage-<stamp>/` (with `--keep-k3s`: their volumes switch to `Retain` and stay in place) | deleted |
| Felis images, Docker build cache | kept | deleted |
The two database rows are [SH-TESTED] (`deploy/uninstall_test.sh`); the VM runs above
predate felis-postgres.
Neither mode removes packages (Docker, git, nftables, and the PostgreSQL server an earlier
release installed) or the swap file: other software may use them. On a host that should
end up bare:
```
sudo swapoff /swapfile && sudo rm /swapfile && sudo sed -i '\|^/swapfile |d' /etc/fstab
sudo dnf remove docker-ce docker-ce-cli containerd.io postgresql-server # or apt/zypper/pacman
```
The Cloudflare side outlives the host. After an uninstall that is final, delete the
tunnel (Zero Trust → Networks → Tunnels, or `cloudflared tunnel delete <name>`), its
DNS records for the panel hostnames, and the Access application.
### Reinstall on top of kept data {#reinstall-on-top-of-kept-data}
A keep-data uninstall leaves everything a reinstall needs. The installer reuses
`/etc/felis/secrets.env`, so the database password and the forwarding and session
secrets are unchanged, and the installer migrates the kept database instead of creating
one **[VM-VERIFIED]** (with the host database of the releases before felis-postgres).
felis-postgres starts again on the cluster kept in `/var/lib/felis/postgres` [SH-TESTED].
Each step below was run on the reference VM after a keep-data uninstall, and the
restored worlds matched their kept `level.dat` checksums **[VM-VERIFIED]**. `kept` names
the directory the uninstall moved the volumes to:
```
kept="$(ls -d /var/lib/felis/retained/k3s-storage-* | tail -n 1)"
store=/var/lib/rancher/k3s/storage
```
1. Install as usual (`curl ... | sudo bash`). Name the same root domain if it was not
the `<ip>.nip.io` default: `felis.host.toml` is kept, and the installer reads the
domain from it.
2. Run `sudo felis setup`. It recreates the login and lobby servers; the Owner already
exists, so it opens on the status screen and you can quit there.
3. Put the image registry and the uploads back. They hold every custom server image
and uploaded file; without the registry, a restored server fails to pull its image.
```
sudo k3s kubectl -n felis scale deploy/registry deploy/felis-api --replicas=0
sudo k3s kubectl -n felis wait --for=delete pod -l app.kubernetes.io/component=registry --timeout=120s
sudo k3s kubectl -n felis wait --for=delete pod -l app.kubernetes.io/component=api --timeout=120s
sudo rsync -a --delete "$kept"/pvc-*_felis_registry/ "$(ls -d $store/pvc-*_felis_registry)"/
sudo rsync -a --delete "$kept"/pvc-*_felis_felis-uploads/ "$(ls -d $store/pvc-*_felis_felis-uploads)"/
sudo k3s kubectl -n felis scale deploy/registry deploy/felis-api --replicas=1
```
Then run the installer once more. It pushes this release's images over the older
copies the kept registry carried.
4. Bring the game servers back. The final bundle holds every MinecraftServer as it was;
the selector skips login and lobby, which step 2 created for this release:
```
b="$(ls -t /var/lib/felis/db-backups/felis-db-*-manual.tar | head -n 1)"
tar -xOf "$b" k8s/minecraftservers.json \
| sudo k3s kubectl apply -l '!felis.lolicon.best/system-role' -f -
```
5. Put each world back. A server's volume exists once it has started once, so start it
from the panel, stop it again, and copy the kept world over the new one:
```
s=<server>
sudo rsync -a --delete "$kept"/pvc-*_minecraft_world-$s-0/ "$(ls -d $store/pvc-*_minecraft_world-$s-0)"/
```
Then start it. The lobby works the same way: stop it with
`sudo k3s kubectl -n minecraft patch minecraftserver lobby --type=merge -p '{"spec":{"desiredState":"Stopped"}}'`,
copy `world-lobby-0`, and patch it back to `Running`.
6. Bring the archives back so the panel's restore points work again. The archive volume
appears with the first backup, so back up any server from the panel first, then:
```
sudo rsync -a "$kept"/pvc-*_minecraft_felis-backups/ "$(ls -d $store/pvc-*_minecraft_felis-backups)"/
```
With an off-site bucket configured, `sudo felis offsite fetch-worlds` fetches them
instead (troubleshooting §16).
7. Delete `/var/lib/felis/retained/` once every server is back.
## 4. Upgrading the pieces around Felis {#_4-upgrading-the-pieces-around-felis}
A rerun of the installer upgrades Felis itself (§15). The components it installs keep
the version they were installed with unless noted:
| Component | How a rerun treats it | Upgrade |
|---|---|---|
| Velocity, Limbo, Paper, LuckPerms | follow `deploy/game-stack.lock` | rerun after a release that moves the lock (§15b) |
| Temurin JRE | moves to the pinned patch build | rerun |
| k3s | left alone | rerun with `FELIS_UPGRADE_DEPS=1`: moves to the pinned release through that tag's install script, one minor version at a time (a bigger jump stops before anything changes and names the release to go through), never backwards |
| cloudflared | left alone | rerun with `FELIS_UPGRADE_DEPS=1`: swaps `/usr/local/bin/cloudflared` for the pinned, sha256-checked release and restarts `cloudflared-felis`; a cloudflared the distribution installed stays with its package manager |
| PostgreSQL | follows the image the release pins | a minor release comes with a Felis release, and the rerun restarts felis-postgres on it (a few seconds without the API); a major version is a dump and restore (below) |
| Docker, git, nftables | distribution packages | the package manager |
```sh
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh \
| sudo FELIS_UPGRADE_DEPS=1 bash
```
`sudo felis update` reports Felis, Velocity, k3s, cloudflared, the JRE and PostgreSQL
against their newest releases; `--k3s`, `--cloudflared`, `--jre` and `--postgres` narrow
it to one. PostgreSQL is read from the felis-postgres container and compared within its
major, since a minor release arrives with a Felis release, and a major past its end of life
gets a note naming the current one.
The installer also sets up `felis-update-check.timer`, which runs `felis update --record`
once a day around 05:30 (and at boot after a missed run). `--record` stores the result
in `platform_settings`, and the panel's **Admin → Updates → Component versions** card
shows it: each component's installed and newest version, and for the ones with a newer
release the `sudo felis update --<component>` line that prints how to apply it. Felis
applies nothing on its own; the installer re-run above is the apply path. The card turns
red when the newest record is older than 26 hours, meaning the timer stopped:
```sh
systemctl list-timers felis-update-check.timer
journalctl -u felis-update-check -n 50 --no-pager
sudo felis update --record # record a fresh check now
```
### Bringing an older install up to date [VM-VERIFIED] {#bringing-an-older-install-up-to-date-vm-verified}
Three pieces of an install keep the shape they had on the day they were created, and
neither `felis setup` nor `kubectl rollout restart` reaches them: the felis-api
Deployment (an env var added later, such as `FELIS_SMTP_PASSWORD`, is absent until the
Deployment is rendered again), the lobby image (built with whatever plugins the recipe
had then; LuckPerms came later, and without it every permission change from the panel
answers `luckperms_missing`), and the `MinecraftServer` specs (a field added later stays
unset). Bring all three forward in this order, images first:
```sh
# 1. Rerun the installer: renders and applies the control-plane bundle, rebuilds and
# re-imports the login and lobby images, and recreates those two pods so they run
# the new images. The [smtp] relay the setup wizard wrote is carried forward.
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash
# 2. Fill the spec fields the system servers gained since (troubleshooting §12b), then
# RCON for user servers created before it was the default. -user-rcon waits on
# each server's image opening RCON; see §12b before running it.
sudo felis converge
sudo felis converge -user-rcon
```
Check each piece:
```sh
kubectl -n felis get deploy felis-api \
-o jsonpath='{.spec.template.spec.containers[0].env[*].name}' | tr ' ' '\n' | grep SMTP
kubectl -n minecraft exec lobby-0 -- ls /data/plugins | grep -i luckperms
kubectl -n minecraft get minecraftserver \
-o custom-columns=NAME:.metadata.name,RCON:.spec.rcon.enabled,IDLE:.spec.idle.autoStopEnabled
```
The env var only carries the password; mail still needs the relay itself, set in
`felis setup` → email. A user server picks up its new RCON block at its next start.
### PostgreSQL major versions [CODE-ONLY] {#postgresql-major-versions-code-only}
felis-postgres keeps its cluster in `/var/lib/felis/postgres/<major>/docker`. A release that
moves the image to a new major finds the old major's cluster there and stops before it
changes anything: the new server would start an empty cluster beside it. The way across is
a bundle, taken on the release you run now, restored into the new major's empty cluster:
```sh
# On the release you run now:
b="$(sudo felis db backup -label pre-upgrade | sed -n 's/^felis db backup: wrote //p')"
sudo k3s kubectl -n felis scale deploy/felis-postgres --replicas=0
sudo mv /var/lib/felis/postgres/18 /var/lib/felis/postgres-18.old # the old major's cluster, for a way back
# Install the new release: it starts an empty cluster on the new major and creates the schema.
curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo bash
# Put the data back and bring its schema up to the new release.
sudo k3s kubectl -n felis scale deploy/felis-api deploy/felis-operator --replicas=0
sudo felis db restore -yes -no-safety-backup "$b"
sudo felis migrate up -config /etc/felis/felis.host.toml
sudo k3s kubectl -n felis scale deploy/felis-api deploy/felis-operator --replicas=1
```
Delete `/var/lib/felis/postgres-18.old` once the new release has run for a while. To go back
instead, scale felis-postgres to 0, move the new major's directory out of
`/var/lib/felis/postgres`, move `postgres-18.old` back as `/var/lib/felis/postgres/18`, and
rerun the older release's installer.
### The database's move into k3s [VM-VERIFIED] [CI] {#the-database-s-move-into-k3s-vm-verified-ci}
Releases before the move ran the database on a PostgreSQL the installer installed on the
host. The first rerun of a release with felis-postgres moves it, once:
1. It stops felis-api, felis-operator and the host timers, and heads the host's
`pg_hba.conf` with a block that refuses every connection to `felis` but its own copy
(the original is kept beside it as `pg_hba.conf.pre-pg-move`).
2. It takes a `pre-pg-move` bundle of the host database (`felis db backup`), restores it
into felis-postgres (`felis db restore`) and compares the row count of every table on
both servers. Any failure up to here puts `pg_hba.conf` and the control plane back and
the platform keeps running on the host database, untouched.
3. It stops and disables the host `postgresql` service, which stays installed with its
copy of the data, and writes `/var/lib/felis/postgres-moved`. A host server that also
holds other databases keeps running; its `felis` copy is then reachable over loopback
only.
The e2e upgrade job seeds the newest release's database with users, links, sessions, audit
rows, backups, builds and the rest (`deploy/e2e_seed.sh`), upgrades, and checks that
felis-postgres holds every seeded row with the same values after the pending migrations.
While that release is v0.1.0, the upgrade is this move.
From then on the host config points at felis-postgres (`127.0.0.1:15432`, and
`deployment = "felis/felis-postgres"`, through which `felis db` runs `pg_dump`, `psql`
and `pg_restore` inside the pod) and the pods at `felis-postgres.felis.svc:5432`.
To go back to the host database, for instance to reinstall the release before the move:
```sh
sudo k3s kubectl -n felis scale deploy/felis-api deploy/felis-operator deploy/felis-postgres --replicas=0
hba="$(sudo -u postgres psql -XtAc 'SHOW hba_file' 2>/dev/null || echo /var/lib/pgsql/data/pg_hba.conf)"
sudo cp -p "${hba}.pre-pg-move" "$hba"
sudo systemctl enable --now postgresql
sudo rm /var/lib/felis/postgres-moved
curl -fsSL <raw-url-of-that-release>/deploy/bootstrap.sh | sudo bash
```
`SHOW hba_file` needs the server running; with it stopped, the fallback path is EL's
(Debian and Ubuntu keep it in `/etc/postgresql/<major>/main/`). Whatever the platform wrote
after the move lives only in felis-postgres; take a bundle there first
(`sudo felis db backup`) and restore it onto the host database afterwards if that matters.
Once the move has run for a while, drop the host copy:
`sudo systemctl start postgresql; sudo -u postgres dropdb felis; sudo -u postgres dropuser felis`,
or remove the server package altogether.
### The MinecraftServer CRD [VM-VERIFIED] {#the-minecraftserver-crd-vm-verified}
Every rerun applies the CRD embedded in the `felis` binary (`felis bootstrap-assets crd`).
It serves and stores the single version `v1alpha1`, and the apiserver refuses values the
operator cannot act on:
| Field | Accepted |
|---|---|
| `spec.rcon.port` | unset, `0` or `25575`: the allow-rcon NetworkPolicy opens only 25575, so any other port leaves the server unprobeable |
| `spec.startup.timeoutSeconds`, `readinessTimeoutSeconds` | 0 – 86400 |
| `spec.startup.healthHTTPPort` | 0 – 65535 |
| `spec.lifecycle.terminationGracePeriodSeconds` | 0 – 3600 |
| `spec.idle.emptySecondsBeforeStop` | 0 – 604800 (the panel caps it at 86400) |
`0` means the operator's default throughout. An object stored before these rules keeps an
out-of-range value until someone edits that field (CRD validation ratcheting). The operator
reads a negative value as its default and an oversized one as written, so fix such a
value by hand: `kubectl -n minecraft edit minecraftserver <name>`.
**Moving to `v1beta1` (planned, not built).** The first breaking change to the spec ships as a new
version, in this order, each step one release:
1. The CRD serves `v1alpha1` and `v1beta1`, storage stays `v1alpha1`. While the two
schemas carry the same fields, `conversion.strategy: None` suffices; a renamed or
reshaped field needs a conversion webhook, which felis-operator would serve.
2. Storage moves to `v1beta1`. The installer rewrites every object so etcd holds the new
version (`kubectl get minecraftservers -A -o json | kubectl replace -f -`), then sets
`status.storedVersions` of the CRD to `["v1beta1"]`.
3. A later release stops serving `v1alpha1`. Felis itself reads through one Go type at a
time, so the operator and felis-api switch in the release that moves storage.
### Legacy-forwarded backends [VM-VERIFIED] {#legacy-forwarded-backends-vm-verified}
A 1.8-era backend sits behind ViaVersion, which drops modern forwarding's login plugin
message on the way down to protocol 47, so the proxy has to hand that server the
player's identity BungeeCord-style, in the handshake address. Only the Felis-Legacy
Velocity fork can do that per server. Mark the server's CR and the proxy picks it up at
its next server-list refresh (every 15 s):
```sh
kubectl -n minecraft label minecraftserver <name> felis.lolicon.best/forwarding=legacy
kubectl -n minecraft label minecraftserver <name> felis.lolicon.best/forwarding- # back to modern
journalctl -u felis-velocity | grep 'legacy forwarding list'
```
The installer's `FELIS_LEGACY_FORWARDING_SERVERS` (default `legacy18`) stays in the list
whatever the labels say. What a label does depends on the proxy the host runs:
| Proxy | A label applies |
|---|---|
| Fork with patch 0004 (`build-velocity.sh` default arm) | from the next connection to that server |
| Fork with 0003 alone (`--deployed`) | at the next `systemctl restart felis-velocity` |
| Stock Velocity | never; the log line is a warning naming the server |
On the test VM (fork with 0004) labelling a server logged `legacy forwarding list is now
[legacy18,resolvecheck]` 12 s later, and removing the label logged the list back to
`[legacy18]`. The fork's own test (`FelisLegacyForwardingTest`) covers the next
connection following the rewritten list.
Legacy forwarding carries no secret. A marked server believes any identity that reaches
its game port, which `felis-allow-game-from-velocity` limits to the proxy and the node
itself; anything else running on the node can reach it too.
## 5. Disaster recovery {#_5-disaster-recovery}
The procedures are in §16: what a database bundle holds, restoring one on the same host,
rolling back an upgrade, and rebuilding on a new host from the off-site copy. For a
production install:
- **Configure the off-site copy** (`FELIS_OFFSITE_*`, §16 "Keep a copy somewhere
else"). Without it the world archives sit on the same disk as the worlds, and the
database bundles on the same disk as the database; losing the disk loses both. The
installer ends with `NO OFF-SITE COPY` until it is set.
- **Keep the off-site encryption key off the host**, in a password manager. The bucket
holds only sealed objects.
- **Keep one database bundle off the host** as well when there is no bucket. It contains
`secrets.env`, which a rebuild needs to read the rest.
- **Rehearse the rebuild** once on a spare VM: troubleshooting.md §16 "Rebuild on a new
host", every step but 8 (take-over) and 11 (the tunnel), then its checks: sign in with
an email code, restore one world and join it. `felis offsite status` and `felis db check` exit
non-zero when the copy or the newest daily bundle is stale; wire them into your monitoring,
or rely on the watchdog's mail.
### Moving to another host (planned) {#moving-to-another-host-planned}
A planned move is the rebuild of troubleshooting.md §16, with the old host still there to
hand over a copy that misses nothing. It needs the off-site bucket: that is how the world
archives reach the new host (§16 step 7). The platform is down from step 1 until the new
host serves.
1. **On the old host**, stop everything that changes a world, then send the last copy:
```bash
sudo install -d -m 0755 /run/felis
echo $(( $(date +%s) + 4 * 3600 )) | sudo tee /run/felis/watchdog-quiet-until
sudo systemctl stop felis-velocity # no joins, so no server wakes
sudo felis backup-now -yes -stop # every world archived; the servers stay stopped
sudo k3s kubectl -n felis scale deploy/felis-operator --replicas=0 # nothing starts a server from here on
sudo felis db backup # a bundle that lists those archives
sudo systemctl start felis-offsite.service
sudo felis offsite status # again until nothing waits
```
The order matters. The new host fetches the archives its restored database lists, so
the bundle comes after the last archive. The operator goes after `backup-now`, which
needs it to stop the servers. The quiet marker keeps the watchdog from mailing the
owners about the stopped proxy and operator for the next 4 hours.
2. **On the new host**, follow troubleshooting.md §16 "Rebuild on a new host" from step 1;
`fetch-db latest` picks the bundle the old host just sent. Step 8 (`felis offsite
take-over`) makes the new host the one that writes the bucket, and from then on the
old host copies nothing more. Steps 10 and 11 move the names and the tunnel.
3. **Check the new host** before announcing it: sign in with an email code, restore one
world and join it, and see `sudo felis offsite status` show a recent `last success` and
no stand-by notice.
4. **Retire the old host.** It holds the last copy of every world outside the bucket, so
keep it powered off with its disk for a few days first, disabled so a boot brings
nothing up:
```bash
sudo systemctl disable k3s felis-velocity felis-watchdog.timer felis-offsite.timer \
felis-db-backup.timer felis-update-check.timer felis-build-tools.timer
sudo poweroff
```
Then uninstall it (§3) or wipe it.
Each step is covered where it is documented (backup-now in troubleshooting.md §10, the
rebuild in §16); the sequence as a whole has not been rehearsed as one move.
## 6. Changing the root domain [VM-VERIFIED] [GO-TESTED] [SH-TESTED] {#_6-changing-the-root-domain-vm-verified-go-tested-sh-tested}
The root domain is written into more places than the installer's config: the panel
certificate (`/etc/felis/panel-tls.crt`), the `felis-config` Secret in both namespaces,
the `felis-api-tls` Secret, the proxy's `felis-link.properties`, the login gate's
`MinecraftServer` env (`FELIS_ROOT_DOMAIN`, `FELIS_PANEL_HOSTNAME`), the Cloudflare tunnel
and DNS. `felis domain set` moves every one of them that lives on the host, in that
order, then restarts what reads them; `felis domain check` reports each surface on its
own line. The installer keeps the installed domain: a rerun with a different
`FELIS_ROOT_DOMAIN` stops and names this command.
```sh
sudo felis domain set new.example.net # the plan: every surface, what it moves to, what it costs
sudo felis domain set -yes new.example.net # do it
sudo felis domain check # one line per surface; exits 1 while any is behind
```
What it keeps:
- A panel or admin-console hostname set by hand in `[auth]` (anything other than
`console.<root>` / `op.console.<root>`) stays as it is; change it in
`/etc/felis/felis.host.toml` yourself if it should move, then run `set` again.
- The other `[auth]` keys (`access_jwt_aud`, `client_ip_header`) and every other line of
both config files. The edit refuses a file it cannot change line for line (a multi-line
value, a quoted or dotted key) and names what to fix.
- An operator's own certificate. The installer's self-signed certificate is reissued for
the new names (same shape, the old pair saved beside it as `*.pre-domain-<time>`); a
certificate from another issuer that does not cover the new names stops the command
before anything changes. Replace it with one that does, then run `set` again.
What it costs, which the plan prints before `-yes`:
- **DNS.** `<root>`, `console.<root>`, `op.console.<root>` and `*.<root>` must reach the
host. The wildcard does not cover `op.console.<root>`, a third-level name: give it its
own record. `check` resolves each name and warns on the ones that do not resolve yet.
- **Players.** Servers are reached as `<name>.<new root>`; the old addresses stop routing,
and the proxy restart disconnects everyone online. The first installer re-run after a
move restarts the proxy once more: the fingerprint it keeps of the proxy's files
predates the move.
- **Sign-in.** Session cookies belong to the old hostnames, so everyone signs in again.
Passkeys are bound to the panel hostname: when it changes, the plan counts the passkeys
that stop working, and their users sign in with an email code and register a new one.
Without an `[smtp]` relay no code is delivered; an Owner locked out that way recovers
with `sudo felis breakGlass`.
- **Cloudflare.** The tunnel's ingress and the Access application still carry the old
names. Re-run the Cloudflare step of `sudo felis setup` after the move; `check` lists
the tunnel's hostnames against the new ones.
- **A proxy on another host** (a remote `felis-link.properties`) is outside this host's
reach: `set` prints the three keys to put there.
`set` is safe to repeat: a second run changes only what is still behind, and on an
install that is already on the domain it converges whatever `check` reports. The same
holds after an interruption.
On the reference VM the move from `10.211.55.6.nip.io` to `10-211-55-6.nip.io` took 34
seconds. The certificate served on 30443, `/config.json` on both hostnames, the proxy's
`Felis routing ready: rootDomain=` log line and the login pod's env all carried the new
names afterwards. A second `set -yes` changed and restarted nothing; the installer run
with the old `FELIS_ROOT_DOMAIN` stopped at its first check; a full installer re-run kept
the moved domain and left `check` clean; moving back restored every surface
**[VM-VERIFIED]**.
`check` reads the proxy as behind when `felis-velocity` started before
`felis-link.properties` last changed. Installers before this command rewrote that file
on every run, so a host upgraded from one can show that line once with the file already
on the names; `sudo systemctl restart felis-velocity` clears it. The installer now leaves
the file alone when its content is the same.
---
Source: [docs/operations.md](https://github.com/FelisMC/Felis/blob/main/docs/operations.md).
File diff suppressed because it is too large. Load diff
+72
View File
@@ -0,0 +1,72 @@
---
title: API definition
---
# API definition {#api-定义}
[Download the complete OpenAPI 3.1 definition](/openapi.yaml).
Control plane for the Felis Minecraft orchestration platform. The same binary
exposes an internal face (per-caller service tokens, for velocity / backend
callbacks, never Zero Trust) and an external face (the felis_session cookie, for
people and the panel; Cloudflare Access, when present, is enforced at the edge).
Admin-tier external operations additionally require a staff session on the
operator console host. See `x-felis-face` / `x-felis-tier` on each operation.
Behaviour every operation shares, and so not repeated under each:
* Every response carries `X-Request-Id` (a well-formed inbound one is kept),
`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`,
`Referrer-Policy: no-referrer` and `Content-Security-Policy: default-src 'none'`.
`Strict-Transport-Security` is added when the request came through the TLS
edge (`X-Forwarded-Proto: https`).
* A path no operation serves is `404 not_found`; a path served under other
methods is `405 method_not_allowed` with an `Allow` header.
* A POST/PUT/PATCH/DELETE a browser sends from another site (`Sec-Fetch-Site`
`same-site` or `cross-site`, or an `Origin` whose host is not the request's)
is `403 cross_site`, before authentication. Callers that send neither header
(the plugins, scripts) are unaffected.
* A JSON body over 1 MiB is `413 too_large`. A request body must keep arriving:
after 30 s it has to average 16 KiB/s or the connection is closed.
* Event streams (the console and build logs) tag each line with `id:` (unix
seconds); an EventSource that reconnects with `Last-Event-ID` within the hour
resumes from that second instead of the tailed backlog. The server re-checks
the caller every minute and ends the stream with `event: revoked` once the
session or the access is gone; a stream also closes after 30 minutes and on
server shutdown, and the client simply reconnects.
## Definition and verification scope {#定义与验证范围}
```text
felis-api — OpenAPI 3.1 description of both faces (spec §7, §14, §28 #7).
ONE binary serves TWO http.Handlers (internal / external). This document
describes both, distinguished per-operation by the `x-felis-face` extension
(an array, because `/healthz` is served by both faces) and `x-felis-tier`
(the Zero-Trust grade: public | service | app | admin).
VERIFIED vs. HAND-MAINTAINED — read before trusting a field:
* The {method, path} -> {x-felis-face set, x-felis-tier} mapping is
machine-checked. internal/api/openapi_test.go parses this file and asserts
EXACT bidirectional parity against the route tables the handlers are built
from (internalAPIRoutes / externalAPIRoutes in internal/api/api.go). A route
added, removed, re-faced, or re-tiered without updating this file fails
`go test ./...`. So path, method, face and tier are as trustworthy as the code.
* Which operations a setup-lockdown session may still use (x-felis-setup-allowed)
is checked the same way against the SetupAllowed flag in those tables.
* The named response schemas are compared field by field with the Go structs
the handlers encode (internal/api/openapi_parity_test.go).
* Every request the handler tests send is held to this file once the package
has run (internal/api/openapi_contract_test.go): the operation (or
x-felis-common-responses) must list the status that came back, a JSON response
must fit the schema for that status and carry no property it does not name,
and the JSON request behind a 2xx must fit the requestBody. Statuses and
bodies no test reaches are still hand-maintained.
The deployment zone (RootDomain, spec §2) never appears here — `example.test`
is a placeholder, per the no-hardcoded-domain red line.
```
---
Source: [docs/openapi.yaml](https://github.com/FelisMC/Felis/blob/main/docs/openapi.yaml).
+34
View File
@@ -0,0 +1,34 @@
---
title: Deployment architecture
---
# Deployment architecture {#部署架构}
## Single-node deployment {#单机部署}
`deploy/bootstrap.sh` defaults to a single node. For the opt-in A controller / worker deployment, see [distributed.md](/en/guide/distributed). It needs systemd, root, and one of the
package managers below; everything else (k3s, the JRE, cloudflared, and Docker when an image
has to be built on the host; see "Where the binary and the images come from" below) it
installs.
PostgreSQL runs inside k3s as the `felis-postgres` Deployment, from the official image the
release pins by digest, with its data on the host in `/var/lib/felis/postgres`.
Single-node deployment remains the default. The opt-in [distributed mode](/en/guide/distributed)
keeps the sole API and operator on A and runs games on approved k3s agents. A world is
a ReadWriteOnce claim on its node's local-path storage; moving it requires an explicit
stopped migration through A's archive service. There is no automatic failover or
standby controller. An A restart pauses control operations until its workloads return;
a lost worker leaves its worlds on that node. Cross-node networking still requires the
three-machine acceptance described in the distributed runbook.
## Controller and workers {#主控与工作节点}
Distributed mode is disabled by default. A runs the sole Felis API/operator, k3s server, PostgreSQL, registry, archive service and system servers; Velocity remains a systemd service on A. Nodes B, C and others run only k3s-agent/containerd, game Pods and maintenance Jobs created by A. Every node must use the same architecture and k3s version as A; administrators must trust and maintain the hosts.
## Runtime flows {#运行流程}
The [sequence diagrams](/en/reference/sequence-diagrams) cover join-and-wake, claim transactions and account linking from the main repository. See [server-side plugins](/en/reference/plugins) for the implementation and routing constraints.
---
Source: [docs/operations.md](https://github.com/FelisMC/Felis/blob/main/docs/operations.md), [docs/distributed.md](https://github.com/FelisMC/Felis/blob/main/docs/distributed.md), [docs/sequence-diagrams.md](https://github.com/FelisMC/Felis/blob/main/docs/sequence-diagrams.md), [plugins/README.md](https://github.com/FelisMC/Felis/blob/main/plugins/README.md).
+357
View File
@@ -0,0 +1,357 @@
---
title: Contributing
---
# Felis Contributor Guide {#felis-contributor-guide}
This document is the practical entry point for contributors. It focuses on how to
run, test, and reason about the project while keeping changes small and aligned
with the current codebase.
## Project Shape {#project-shape}
Felis is a Kubernetes-native Minecraft server control plane. The repository has
four main areas:
- `cmd/felis/`: the single Go CLI binary. It dispatches subcommands such as
`api`, `operator`, `migrate`, `reaper`, `restore`, `manifests`, and
`breakGlass`.
- `internal/`: backend packages for API handlers, store migrations, Kubernetes
rendering, operator reconciliation, build, backup, restore, and related domain
logic.
- `panel/`: the React/Vite web control panel.
- `plugins/`: Minecraft-side plugins and mods for Velocity, Paper, Fabric,
Forge, and NeoForge.
The codebase is intentionally split by responsibility. Prefer changing the
smallest owning module instead of adding broad abstractions or rebuilding nearby
code.
## Local Development {#local-development}
You can do most day-to-day development on macOS or Linux without a full cluster.
The full product needs Postgres and Kubernetes, but unit tests and frontend work
run locally.
Recommended local tools:
- Go matching `go.mod`
- Node.js and npm for `panel/`
- Optional: JDK/Gradle for plugin work
- Optional integration environment: a clean Linux VM or server with Docker,
k3s, and Postgres
Check tool versions:
```bash
go version
node --version
npm --version
java -version
```
## Backend Commands {#backend-commands}
Run these from the repository root:
```bash
cd /path/to/Felis
```
Run all Go tests:
```bash
go test ./...
```
Run a focused package:
```bash
go test ./internal/api
go test ./cmd/felis
```
The hermetic suites run against in-memory fakes; the business stores' SQL is
verified separately against a real Postgres, on a throwaway database whose name
must contain `pgint` (the harness drops and recreates its schema and replays the
embedded migrations):
```bash
FELIS_TEST_PG_URL='postgres://felis:***@127.0.0.1:5432/felis_pgint?sslmode=disable' \
go test -tags pgint ./internal/pgint/ -v
```
Run it after touching anything under `internal/api/pgrepo.go`, `internal/submit`,
`internal/build` or `internal/dbbackup` that speaks SQL: the fakes encode the
contract, and this suite exists to catch the drift between the fakes and the real
queries. The `felis db backup` and `restore` tests also run `pg_dump`, `pg_restore`
and `psql`, which must be the server's major version. For a server in a container,
run them in it, as production does in felis-postgres:
```bash
FELIS_TEST_PG_EXEC='docker exec -i <container>' FELIS_TEST_PG_URL=... go test -tags pgint ./internal/pgint/
```
Build the CLI:
```bash
go build -o /tmp/felis-dev ./cmd/felis
/tmp/felis-dev help
```
Render Kubernetes manifests without contacting a cluster:
```bash
/tmp/felis-dev manifests \
--felis-image registry.felis.svc:5000/felis:dev \
--velocity-cidr 10.0.0.5/32
```
`felis api`, `felis operator`, `felis migrate up`, and `felis reaper` are real
runtime commands. They need external services such as Postgres and/or a
Kubernetes config, so they are not the first choice for quick local iteration.
## Frontend Commands {#frontend-commands}
Run these from the frontend workspace:
```bash
cd /path/to/Felis/panel
```
Install dependencies:
```bash
npm ci
```
Run against a real backend at `http://localhost:8080`:
```bash
npm run dev
```
Run with the local mock API:
```bash
npm run dev:mock
```
The mock dev server prints its accounts, link code, and reset command when it
starts. Use it for frontend work when you do not have the Go API and cluster
running.
Common frontend checks:
```bash
npm run typecheck
npm test
npm run build
```
## Mock API {#mock-api}
The frontend mock API lives under `panel/dev/` and is loaded only by
`npm run dev:mock`. It must not leak into production code or business
components.
Run mock commands from the frontend workspace:
```bash
cd /path/to/Felis/panel
npm run dev:mock
```
Current mock accounts:
| Username | Password | Scenario |
| --- | --- | --- |
| `owner` | `devpassword` | admin, linked |
| `user` | `devpassword` | normal user, not linked |
| `linked` | `devpassword` | normal user, linked |
| `setup` | `devpassword` | admin, first-login password change |
Mock Minecraft link code:
```text
LINK1234
```
Reset mock state:
```bash
curl -X POST http://127.0.0.1:5173/api/v1/__mock/reset
```
Mock rules:
- Keep mock-only logic in `panel/dev/`.
- Do not import mock code from `panel/src/`.
- Keep response shapes aligned with `panel/src/lib/types.ts` and the Go API
handlers.
- Prefer realistic error codes over happy-path-only mocks.
- Do not present mock data as live production data.
## Full Integration Environment {#full-integration-environment}
Run integration/deploy commands from the repository root on the Linux host:
```bash
cd /path/to/Felis
```
The setup TUI is intended for a clean Linux host, not a typical macOS
development machine:
```bash
sudo felis setup
```
Useful overrides:
```bash
export FELIS_REPO_URL=<your fork url>
export FELIS_REF=<your branch> # pins the build; overrides the channel below
export FELIS_IMAGE=felis:dev
export FELIS_ROOT_DOMAIN=<node-ip>.nip.io
```
By default the installer builds the newest **published GitHub release**. Building the
development tip needs an opt-in, and installing from a private fork additionally needs a
token for the release lookup and the clone:
```bash
export FELIS_VERSION_BOOTSTRAP=dev # build main instead of the newest release
export FELIS_GITHUB_TOKEN=<token> # private forks only: read access to the fork
```
`dev` is also the escape hatch before the first `vX.Y.Z` tag exists: with no published
release the default channel has nothing to resolve and stops with that instruction.
A release install downloads that tag's CI-built binary, images and Velocity plugin, verifies
`SHA256SUMS`, and imports them; the panel is embedded in the same binary (`internal/panel`).
`dev` clones and compiles from source. Missing or unusable release assets fall back to a
host build of **the same tag**, with a warning for the affected component. The installer
never silently changes commits. `FELIS_REF` forces the source path. See
[Installation and deployment](/en/guide/deployment) and
[Where the binary and images come from](/en/operations/#where-the-binary-and-the-images-come-from)
for the current installation behavior.
The channel decides the version stamp linked into the binary (`felis version`), which is
what `felis update` compares against upstream — release builds stamp the tag, dev builds
stamp `<latest-tag>+g<short-sha>`, and a pinned `FELIS_REF` stamps `v0.0.0+g<short-sha>`
because skipping channel resolution also skips the tag lookup. An unstamped build reports
`dev` and update reporting
is disabled for it, so build through `bootstrap.sh` (or the Dockerfile's `FELIS_VERSION`
build arg) rather than a bare `go build` when testing that path.
The setup flow wraps the host bootstrap, then continues to Owner account setup
and optional Cloudflare edge setup in the same command. The raw
`deploy/bootstrap.sh` script remains available for low-level host provisioning
when debugging the installer itself.
Use a VM or disposable Linux server for this. Treat it as an integration and
acceptance environment, while keeping normal coding and quick tests local.
## Plugin Development {#plugin-development}
Plugin docs live in `plugins/README.md`.
The plugin modules intentionally use separate Gradle builds:
```bash
# cwd: repository root
cd /path/to/Felis
bash plugins/velocity/gradlew -p plugins/velocity build
bash plugins/paper/gradlew -p plugins/paper build
bash plugins/fabric/gradlew -p plugins/fabric build
bash plugins/forge/gradlew -p plugins/forge build
bash plugins/neoforge/gradlew -p plugins/neoforge build
```
Notes:
- Fabric/Forge/NeoForge use module wrappers.
- Velocity, Paper and Limbo also use their module wrappers.
- Java and Minecraft version requirements are listed in the [plugin version table](/en/reference/plugins#minecraft-versions).
- Paper currently needs a Java 25 toolchain; use the requirements of each module.
- First builds may be slow because Minecraft dependencies are downloaded and
remapped.
## Frontend Status {#frontend-status}
The panel is still in early development. It currently offers server lists and status,
wake/stop/claim, console and RCON, file management, account linking,
whitelist/ban/OP/LuckPerms management, scheduled tasks, and backup and restore.
The current feature scope follows the [Project README](/en/reference/readme-en#features)
and the relevant backend endpoints. The mock API is for local frontend development.
Keep UI state accurate when adding features. An endpoint that does not exist should have
an explicit placeholder, never fake live data. Interaction depth and component/browser
test coverage still need work.
## i18n Notes {#i18n-notes}
This section concerns the **Felis control panel**; the documentation site has its own language switch.
Panel i18n is not yet established as a full system. User-facing strings are currently
mostly inline in TSX and helper functions.
Good first targets:
- Error messages mapped from stable API error codes
- Navigation labels
- Page titles and primary actions
- Phase/status labels
- Empty/loading/error states
Recommended initial approach:
```text
panel/src/i18n/
index.ts
en.ts
zh-CN.ts
```
Use a small typed dictionary first. Add a larger library such as
`i18next/react-i18next` only when the project needs runtime language switching,
pluralization rules, external translation workflows, or more complex
localization behavior.
Do not translate code comments, internal logs, or mock-only terminal messages
unless there is a clear contributor need.
## Contribution Style {#contribution-style}
Follow the existing code. Keep changes narrow and easy to review.
Guidelines:
- Prefer minimal changes over rewrites.
- Do not add a new abstraction unless it removes real duplication or names a
strong local concept.
- Keep frontend mock code out of business components.
- Keep backend tests close to the package that owns the behavior.
- Preserve public API and persistence shapes unless the change explicitly needs a
contract update.
- Do not commit generated build output such as `panel/dist/`, `node_modules/`,
Gradle build directories, or local binaries.
AI-assisted work is welcome, but the contributor is responsible for the result.
Do not submit a PR that is entirely AI-generated and not personally reviewed.
Low-quality AI dumps, broad rewrites that ignore the current design, unverified
changes, or code the author cannot explain will not be accepted.
Before handing off a change, run the smallest meaningful checks:
```bash
go test ./...
cd panel && npm run typecheck && npm test && npm run build
```
If you cannot run a relevant check, say so explicitly in the handoff.
---
Source: [CONTRIBUTING.md](https://github.com/FelisMC/Felis/blob/main/CONTRIBUTING.md). Installation and feature status are also synchronized with the main README and plugin documentation.
+150
View File
@@ -0,0 +1,150 @@
---
title: Integration boundaries
---
# Deferred integration seams {#deferred-integration-seams}
`INTEGRATION-ONLY` and `KNOWN-LIMITATION` are grep-able markers in the Go source.
This file is the index of what each one currently means, so that reading the
unfinished face of the system does not require re-deriving it from 34 comment
sites.
It exists for two reasons. The markers do not all mean the same thing — four
distinct states share them, and "declared, nothing implements it" reads exactly
like "implemented, but its I/O cannot be exercised from this repo". And a marker
outlives the condition it describes: two of them were stale when this index was
first assembled, both claiming as future work something that had already shipped.
The pattern here is the one `internal/updater/doc.go` already uses for its own
package — state the verification boundary in buckets, so a green test suite is not
mistaken for a finished integration. This file is the same idea across the whole
tree.
## The marker collides with a different vocabulary in troubleshooting.md {#the-marker-collides-with-a-different-vocabulary-in-troubleshooting-md}
`docs/troubleshooting.md` uses `[INTEGRATION-ONLY]` for something else, defined in
its own opening at `:19`: the symptom is produced by the kubelet, kaniko or a live
handshake, so it cannot be reproduced from the repository. Those twelve marks say
where a failure comes from. They are not unfinished work and are not indexed below.
A grep across `*.md` and `*.go` returns both sets; only the Go ones are seams.
## Declared, nothing implements it {#declared-nothing-implements-it}
- `internal/updates/seams.go:32` — `Notifier`. `internal/mail` sends OTP over SMTP,
but nothing adapts it to this interface and no in-game channel exists. `felis
update` passes nil deliberately. The notification is the panel instead:
`felis-update-check.timer` runs `felis update --record` daily on the host, which
stores the report under `platform_settings.update_report`, and **Admin → Updates →
Component versions** shows it with the command that applies each update.
- `internal/updates/seams.go:43` — `Applier`. Nothing applies an update anywhere. A
nil applier is not silent — `Run` records `errNoApplier` against every planned
apply, so a mis-scheduled apply is loud rather than lost.
- `internal/updater/gatherer_integration.go:22` — the two current-version seams
`NewSysGatherer` leaves nil, for the in-cluster path: the k8s read of the
control-plane Deployment image, and the Velocity jar inspection. Both are answered
on the host path (see "Built" below), so this gap is specific to a caller that has
a cluster client instead of the node.
- `internal/api/handlers_updates.go` — the maintenance window is advisory: no
in-cluster runner applies updates. `felis update` reads the stored window, prints
where now sits against it and warns before an apply outside it; the runner itself
still runs with a zero window, so no path can claim an apply is under way.
- `internal/submit/blobstore.go` — CLOSED 2026-09-22. The uploads PVC still cannot
cross namespaces, so the transport went through the API instead of a mount: the
derived context ref is now the internal-face URL
(`/api/v1/internal/submissions/{id}/context`, service-token gated), the build
Job's `context-fetch` initContainer streams it with `felis fetch-context` and
extracts under a zip-slip guard into a size-limited emptyDir, and Kaniko builds
`--context=/context`. The token reaches the build namespace through the same
Secret-replica mechanism the login gate uses (bootstrap + `felis setup`), and the
build egress lock allows exactly the control namespace on the internal port.
Uniform for local and s3:// stores — neither hands the sandboxed build Pod a
filesystem view or object-store credentials. Kaniko, Trivy and Trivy's two DBs
come from the registry's `mirror/` copies, which the installer and
felis-build-tools.timer keep current (`felis mirror-build-tools`,
docs/troubleshooting.md §8e); the `[registry]` keys override them.
## Built; only its I/O is unverifiable from this repo {#built-only-its-i-o-is-unverifiable-from-this-repo}
Code exists and is unit-tested against fakes. What is missing is a host, a cluster
or a real upstream account to run it against — not an implementation.
- `cmd/felis/tui_edge_apply.go:246,274,295` — the `nft` edge fence, its idempotent
teardown, and the cloudflared invocation.
- `internal/cfsetup/runner.go:18` and `internal/cfsetup/cfsetup.go:329` — the real
Cloudflare Tunnel and Access API calls; `internal/cfsetup/cfsetup_test.go:11`
drives the whole flow through a fake.
- `internal/api/console.go:39`, `internal/api/logstream.go:236`,
`internal/api/logstream.go:306`, `internal/fileedit/k8sjobs.go:45` — each needs a
live cluster (RCON, `pods/log` follow, a Job). **Verified live 2026-09-22/23
(auditfix7–25):** the RCON command spine (wake → probe → `command`/access
mutations/stop), the log SSE stream, and the fileedit Job have each run
end-to-end on the drill cluster.
- `internal/api/handlers_access.go:170,490` — parsing real vanilla and LuckPerms
command output. **Verified live 2026-09-23:** players / whitelist / banlist
parses matched a live Paper server's replies (LuckPerms not installed → the raw
reply falls through as documented; the input guards held on four negative cases).
## Deliberately accepted, not scheduled to close {#deliberately-accepted-not-scheduled-to-close}
These are decisions, not backlog. Each names the condition under which it would be
worth revisiting.
- ~~`internal/api/pgrepo.go:281` — the quota check and `ClaimServer` are two statements
(audit #4 TOCTOU). Closeable only against a real Postgres.~~ **Closed** — the gate
moved inside `ClaimServer` (advisory lock + re-check + UPDATE in one transaction),
red-then-green in the pgint suite, which is exactly the real-Postgres harness this
line was waiting for.
- `internal/api/api.go:773` — `cooldownLimiter` is process-local, so across N api
replicas a caller could draw up to N OTP codes per window. The intra-replica burst
is closed; cross-replica bounding needs a shared store, out of scope for a
single-replica install. Revisit before the api Deployment runs more than one
replica.
- `internal/submit/submit.go:524` — the per-user upload storage budget reads the
stored bytes, then writes. On one replica the API's per-user upload reservation
serializes it; across replicas a burst can overshoot by one blob per interleaved
upload, each still under the single-blob cap. The pending-submission cap no
longer has this shape: `CreateSubmission` counts and inserts under a
per-submitter advisory lock (pgint `TestSubmitPendingCapHoldsUnderConcurrency`).
Revisit with the cooldown above, before scaling api replicas: a reservation row
per upload in the same kind of transaction closes it.
- `internal/submit/submit.go:436` and `internal/submit/submit_test.go:351` — a
post-CAS `Approve`
failure leaves a row indistinguishable from the benign case, so `Approve` returns a
distinct error naming the running build rather than allowing a blind re-drive that
would double-push. The alternative ordering is worse.
- `cmd/felis/tui_edge_apply.go:246`, second marker on the same site — the fence
targets nftables. On a firewalld host a reload can flush the standalone table;
firewalld-native coordination is not handled. A missing `nft` binary fails loud
rather than leaving the port open.
- `internal/api/handlers_account.go:169` — the reclaim "start fresh vs inherit"
choice is CODE-ONLY on the Java/Velocity side; the link-status endpoint reports
link completion only and does not surface it.
## Wired since the marker was written {#wired-since-the-marker-was-written}
- `internal/api/handlers_email_otp.go` and `internal/api/api.go` — SMTP shipped on
2026-07-20 (`internal/mail`, wired in `cmd/felis/api.go`). An install with no
`[smtp]` section leaves the `Mailer` nil, and every door that mails a code answers
503 `mail_unavailable`; codes are never logged. The reading "Felis cannot send
mail" is stale.
- `internal/config/config.go:117` — was stale. It described the upload transport as a
deferred integration after both backends had shipped (`LocalContextStore`,
`S3ContextStore`, selected in `cmd/felis/api.go` by the shape of the configured
base). Corrected in the change that added this file; what remains deferred is only
Kaniko's read, indexed above.
- `internal/updater/doc.go:44` — was stale. Its REMAINING INTEGRATION bullet listed
the `felis update` CLI and the off-cluster Velocity jar read, both of which exist
(`cmd/felis/update.go`, `internal/updater/gatherer_host.go`). Corrected in the same
change; the two nil seams it also names are real and remain above.
## Recorded outside the code {#recorded-outside-the-code}
- The minecraft-namespace egress is locked (`felis-server-egress`, DNS plus the
public internet with every private range and the node's own global addresses
excluded) and `felis-login-to-internal-api` opens the one platform path a game pod
needs — login → felis-api:8081. Any new in-cluster service a game server must call
needs its own allow policy next to that one (`internal/platform/netpol.go`).
---
Source: [docs/deferred-seams.md](https://github.com/FelisMC/Felis/blob/main/docs/deferred-seams.md).
+16
View File
@@ -0,0 +1,16 @@
---
title: License
---
# License {#开源协议}
This project is licensed under [AGPL-3.0-only](/LICENSE.txt).
### License Notes {#协议注意事项}
1. **Derivative works must use AGPL**: Any distribution of this project or of software derived from it must be released under AGPL-3.0 and must include the original copyright notice and license statement.
2. **Network services must also provide source** (AGPL section 13): anyone who offers a modified Felis to others over a network must provide those users with the complete source of the modified version, even without distributing any binary. This is the only substantive difference between AGPL and GPL; as Felis is a hosting platform accessed over a network, this clause applies to virtually every deployment.
3. **Disclaimer**: This project is provided "as is", without warranty of any kind.
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md), [LICENSE](https://github.com/FelisMC/Felis/blob/main/LICENSE).
+174
View File
@@ -0,0 +1,174 @@
---
title: Login server image
---
# Login-limbo image (LOOHP/Limbo + felis-limbo) {#login-limbo-image-loohp-limbo-felis-limbo}
The always-on **login** auth gate. `felis setup` provisions it as a system
service (`DesiredState=Running`, reaper-exempt) when `[velocity] login_image` is
set in `felis.toml`. Every fresh connection lands here first; it is the only safe
fallback (a stopped/starting backend routes here, never past authentication).
> **Code-only.** This image is not built by the Go CI. It compiles the
> `plugins/limbo` plugin (plus the shared `plugins/shared` link core it
> srcDir-includes) and bundles it with a LOOHP/Limbo release.
## What the plugin does {#what-the-plugin-does}
`felis-limbo` does two jobs — readiness and the in-game login flow.
### Readiness {#readiness}
LOOHP/Limbo has no RCON, so Felis cannot use its usual RCON readiness probe
(spec §5). A bare TCP check would report "ready" the instant the socket binds.
The plugin instead serves an HTTP readiness endpoint that flips to `200` only
after the first server tick — i.e. once the limbo has genuinely started. The
`login` MinecraftServer sets `spec.startup.healthHTTPPort: 8080`, so the pod's
HTTP readinessProbe (and, with RCON disabled, the operator's Ready gate) follows
that true signal.
- Endpoint: `GET /healthz` on `:8080` (override with `FELIS_HEALTH_PORT`).
- `503 starting` before the first tick, `200 ok` after.
- Fail-closed: if the endpoint cannot bind, readiness never turns green and the
operator keeps the gate in `Starting` — it never advertises an unstarted gate.
### In-game login (spec §B3) {#in-game-login-spec-§b3}
A player reaching the limbo has been UUID-verified upstream (Velocity online-mode)
but is not yet linked to a web account. On join, off the tick thread, the plugin:
1. checks the username-collision **blacklist** and disconnects a barred squatter
UUID (the genuine Mojang player — different UUID — passes);
2. mints a one-time **Bind Code** for the verified UUID via the felis-api internal
face;
3. opens a **book** with a clickable link to `console.<root_domain>` plus a chat
line carrying the code, and tells the player to finish in their **system**
browser — never the WeChat/QQ in-app browser, where passkey/WebAuthn does not
work (the web entry additionally guards this; see `internal/panel`);
4. **polls** `link/status/{uuid}` until the player redeems the code on the web
console, then **transfers** them to the lobby via a BungeeCord `Connect` plugin
message on `bungeecord:main`;
5. **disconnects (fail-closed)** on blacklist, on a mint/transport failure, or when
the login window elapses — "rather refuse than admit unauthenticated".
Configuration (deployment inputs, never compiled in; env wins over a
`felis-link.properties` template written in the plugin data dir on first run):
| Env | Meaning | Default |
| --- | ------- | ------- |
| `FELIS_API_BASE_URL` | felis-api **internal** face base URL | *(required for login)* |
| `FELIS_SERVICE_TOKEN` | internal service token (secret) | *(required for login)* |
| `FELIS_ROOT_DOMAIN` | deployment zone, builds `https://console.<zone>` | *(required for login)* |
| `FELIS_LOBBY_SERVER` | Velocity server name to transfer to | `lobby` |
| `FELIS_LOGIN_TIMEOUT_SECONDS` | login window (clamped 30–3600) | `600` |
| `FELIS_HEALTH_PORT` | readiness port | `8080` |
| `FELIS_API_CONNECT_TIMEOUT_SECONDS` | felis-api connect timeout (1–120) | `10` |
| `FELIS_API_REQUEST_TIMEOUT_SECONDS` | felis-api call timeout (1–120) | `10` |
If the API config **or** the root domain is absent the login flow stays **OFF** and
the plugin runs readiness-only (the same "load un-crippled" fail-safe the other
Felis plugins use), so a bare image still boots — production must supply the config
for the gate to authenticate. Transfer requires Velocity to accept the BungeeCord
plugin-message channel (`bungee-plugin-message-channel` on the proxy).
## Build {#build}
LOOHP/Limbo has no official image and no release zip. Its CI
(`ci.loohpjames.com/job/Limbo`) publishes two **loose** artifacts per build —
`target/Limbo-<ver>.jar` and `spawn.schem` — so the image is assembled from those
two URLs (there is no bundled `server.properties`; Limbo writes a default on first
run):
```
docker build -f deploy/limbo/Dockerfile \
--build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
--build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
--build-arg LIMBO_VERSION=<maven-api-version> \
-t felis-limbo:demo .
```
- `LIMBO_JAR_URL` (required) — the server jar; it is saved as `Limbo.jar`.
- `LIMBO_SCHEM_URL` (optional) — the default spawn schematic, saved as
`spawn.schem` and loaded as the spawn world.
- `LIMBO_VERSION` — the Limbo **maven** API version the plugin compiles against
(Gradle `-PlimboVersion`). This differs from the jar's CI build-qualified
filename: e.g. the jar `Limbo-2026.0.2-ALPHA-26.2.jar` corresponds to maven
version `2026.0.2-ALPHA` (the `-26.2` CI qualifier is not published to the
maven repo).
Publish it into the cluster's registry and point config at it. On the node
itself (docker treats `127.0.0.1` as insecure by default):
```
docker tag felis-limbo:demo 127.0.0.1:5000/felis/limbo:demo
docker push 127.0.0.1:5000/felis/limbo:demo
# felis.toml → [velocity] login_image = "registry.felis.svc:5000/felis/limbo:demo"
sudo felis setup
```
The registry keys a repository by the path after the host, so pushing through a
`kubectl -n felis port-forward svc/registry 5000:5000` from another machine is
equivalent. Hosting the image in the registry (rather than only importing it
into containerd) is what lets kubelet re-pull it after an image GC.
## Ports (handled for you) {#ports-handled-for-you}
The entrypoint (`deploy/limbo/entrypoint.sh`) pins Limbo's `server-port` to
`FELIS_GAME_PORT` (default **25565**, the operator's `GamePort`) on every start —
LOOHP/Limbo would otherwise default to `30000`, unreachable through the Velocity
`NetworkPolicy` / Service / probe the operator drives off that one const. It is
idempotent, so a persisted world volume keeps all its other `server.properties`
settings. Do **not** override `FELIS_GAME_PORT` except in lockstep with the operator.
It also pins `max-players=-1` (no cap, Limbo's own default): unbound players wait at
the gate for up to ten minutes and a stopped server's players all fall back here at
once, so a cap left on the volume would turn players away at the door.
## Configure (deployer's responsibility) {#configure-deployer-s-responsibility}
One setting this image does **not** guess (it keeps the release's own default):
- **Player forwarding** — align Limbo's forwarding with the off-cluster Velocity
proxy so authenticated players hand off cleanly, and enable the BungeeCord
plugin-message channel on the proxy so the login gate's `Connect` transfer to the
lobby lands.
The login flow's own inputs (`FELIS_API_BASE_URL`, `FELIS_ROOT_DOMAIN`,
`FELIS_LOBBY_SERVER`, `FELIS_SERVICE_TOKEN`; see
**[In-game login](#in-game-login-spec-§b3)** above) are wired in for you — you do not
set them by hand:
- The three **non-secret** vars are baked into the `login` MinecraftServer's
`spec.env` by `felis setup` (`cmd/felis` derives the internal API URL from the
control namespace — the platform default `felis`; a renamed control namespace must
be reflected by hand — and the root domain from `felis.toml`).
- `FELIS_SERVICE_TOKEN` is a **secret**, so it is never written into the CRD. The
login gate has its own internal-API token, `felis-limbo-token`, which may only mint
link codes, poll link status and check the blacklist. The installer applies it into
the minecraft namespace (and `felis setup` refreshes that replica from the control
namespace), and the operator injects it into the `login` pod (only) as
`FELIS_SERVICE_TOKEN` via a `secretKeyRef`, keyed off the reserved `login` name.
`sudo felis rotate-token -yes limbo` replaces it and restarts the pod. Until the token is
present the plugin fail-safes to readiness-only, so the gate is never broken — it
simply does not authenticate yet.
- **Service:** the login pod dials `FELIS_API_BASE_URL`, which resolves to the
ClusterIP Service `felis-api-internal` (control namespace) that fronts the api
pod's internal port 8081. That Service is deliberately separate from the external
NodePort `felis-api` (443) so the no-Zero-Trust internal face is never published on
a node's external IP.
- **NetworkPolicy:** the minecraft namespace is egress-locked
(`felis-server-egress`: DNS plus the public internet, every private range
excluded), so the internal API is unreachable from a game server by default.
`felis-login-to-internal-api` opens exactly the login pod → felis-api (8081) path,
selecting on the reserved `login` name AND the setup-owned
`felis.lolicon.best/system-role=login` label the operator copies onto the pod — the
same pair that decides who receives `FELIS_SERVICE_TOKEN`, so a user server cannot
match it by picking a name.
The Velocity gate/lobby wiring is printed by `felis setup` and enforces the
invariant: fresh connections hit `login` first, and only an authenticated release
from that gate can enter the post-auth lobby or a remembered user backend.
---
Source: [deploy/limbo/README.md](https://github.com/FelisMC/Felis/blob/main/deploy/limbo/README.md).
+78
View File
@@ -0,0 +1,78 @@
---
title: Lobby image
---
# Lobby image (Paper + felis-paper) {#lobby-image-paper-felis-paper}
The always-on **lobby** hub. `felis setup` provisions it as a system service
(`DesiredState=Running`, reaper-exempt) when `[velocity] lobby_image` is set in
`felis.toml`.
> **Code-only.** Not built by the Go CI. It compiles the `plugins/paper`
> `/menu` face and bundles it onto a Paper server.
## Topology & the one invariant {#topology-the-one-invariant}
```
connect → login (limbo auth gate) → lobby (/menu hub) → target backend
```
The lobby is reached **only** when the login gate transfers an authenticated
player onward. It is never a fallback or waiting-park target — routing a fresh
connection to the lobby would drop the player past authentication. Felis enforces
this at every layer:
- the login system service has **no** fallback (refuse if down);
- the lobby and every user server fall back to **login**, never to the lobby;
- `buildSystemServer` refuses to construct any service whose fallback is `lobby`;
- `felis setup` prints the off-cluster Velocity wiring: default landing and
waiting-park both point at `login`.
## Build {#build}
```
docker build -f deploy/lobby/Dockerfile \
--build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
--build-arg PAPER_JAR_SHA256=<sha256 of that jar> \
-t felis-lobby:demo .
# Publish into the cluster's registry (on the node; docker treats 127.0.0.1 as
# insecure by default — or through a `kubectl -n felis port-forward svc/registry
# 5000:5000`, which is equivalent: only the path after the host matters).
docker tag felis-lobby:demo 127.0.0.1:5000/felis/lobby:demo
docker push 127.0.0.1:5000/felis/lobby:demo
# felis.toml → [velocity] lobby_image = "registry.felis.svc:5000/felis/lobby:demo"
sudo felis setup
```
## Configure (deployer's responsibility) {#configure-deployer-s-responsibility}
- Game port must be `25565` (the CRD `GamePort`).
- Align `online-mode` / player forwarding with the off-cluster Velocity proxy.
- The lobby speaks only the `felis:control` plugin-message channel; it holds no
felis-api token by design (spec §12).
## What the lobby allows {#what-the-lobby-allows}
felis-paper's `LobbyGuard` keeps the lobby a hub that nobody can hurt, get hurt in,
or leave a mark on:
- every world is peaceful, with natural spawning, PvP, mob griefing and TNT off,
time frozen at noon, clear weather and inventories kept;
- players take no damage and never go hungry; a fall into the void lands at spawn;
- a player without `felis.lobby.build` joins at spawn in adventure mode and cannot
break or place blocks, use buckets, trample farmland, light fires, or harm mobs,
item frames, paintings, armor stands or vehicles. Buttons, doors, pressure plates
and containers keep working;
- every join gets a chat line with a click that runs `/menu`.
`felis.lobby.build` defaults to ops. To let an admin build the lobby, grant it with
LuckPerms (`lp user <name> permission set felis.lobby.build true` on the lobby console)
or op them.
The entrypoint pins `max-players=200` on every boot, over Paper's default of 20: every
authenticated player passes through here, and a stopped server's players arrive all at
once.
---
Source: [deploy/lobby/README.md](https://github.com/FelisMC/Felis/blob/main/deploy/lobby/README.md).
+411
View File
@@ -0,0 +1,411 @@
---
title: Server-side plugins
---
# Felis server-side plugins {#felis-server-side-plugins}
These are the in-cluster and edge plugins for Felis. The Velocity proxy and the three loader
mods (these on a standalone online-mode server only, see the warning under the module table)
ship the in-game first leg of the §10 account-link flow: a player who is already online
(so Mojang has verified their UUID) runs `/link`; the plugin asks felis-api to
mint a one-time code for that UUID and shows it in chat. The player then enters
the code on the web console → **Account** page (the second leg), which binds the
code to their logged-in account. The web side is already built.
The **limbo** module reaches the same felis-api endpoint without a command: it is the login
gate, so it mints the code on join for anyone not yet linked and holds them until they redeem
it. The **paper** lobby ships neither — see below.
The **Velocity** module additionally carries the §11 domain-autostart routing
loop — recognizing each server's subdomain, registering backends dynamically,
waking a sleeping target and holding the player until it is ready. It is a full
proxy plugin, not just `/link`; see **[Velocity routing](#velocity-routing-§11)**
below. The Fabric / Forge / NeoForge mods are `/link`-only.
The **Paper** module is different in kind: it is the §12 lobby UI face. It ships
**no** `/link` and holds **no** felis-api token — it only paints the `/menu`
(and `/server`) chest GUI and speaks the `felis:control` plugin-message channel
to Velocity, which is the only side that ever talks to felis-api. See
**[Lobby menu](#lobby-menu-§12)** below.
| Module | Platform | Compiles against | Jar | Built by the installer |
| ------------------ | --------------------------- | ---------------------------------------- | ---------------------------- | ---------------------- |
| `velocity/` | Velocity proxy plugin | velocity-api 3.5.1 (Java 21 bytecode) | `felis-velocity-0.1.0.jar` | yes |
| `limbo/` | LOOHP/Limbo plugin (login) | Limbo API 2026.0.3-ALPHA (Java 17 bytecode) | `felis-limbo-0.1.0.jar` | yes |
| `paper/` | Paper server plugin (lobby) | paper-api 26.3.build.40-alpha | `felis-paper-0.1.0.jar` | yes |
| `fabric/` | Fabric server mod | MC 1.20.1 / fabric-loader 0.16.5 / fabric-api 0.92.2+1.20.1 | `felis-fabric-0.1.0.jar` | no |
| `forge/` | Forge server mod | MC 1.20.1 / Forge 47.3.0 | `felis-forge-0.1.0.jar` | no |
| `neoforge/` | NeoForge server mod | MC 1.20.4 / NeoForge 20.4.251 | `felis-neoforge-0.1.0.jar` | no |
| `shared/` | *(not built on its own)* | — | source compiled into each | source only |
The installer's three versions are the builds `deploy/game-stack.lock` installs (Velocity
`VELOCITY_VERSION`, Limbo `LIMBO_VERSION`, the Paper jar in `PAPER_JAR_URL`), and
`go test .` fails when a pin and the lock drift apart; see
[Dependency verification](#dependency-verification).
### Minecraft versions {#minecraft-versions}
| Module | Runs on | Minecraft |
| ---------- | ----------------------------------------- | ------------------------------------------------ |
| `velocity` | the Felis proxy (Velocity 3.5.1) | whatever clients the proxy accepts: 26.3 natively, older clients through the ViaVersion stack bootstrap installs |
| `limbo` | the Felis login gate (Limbo) | 26.3 only — Limbo speaks exactly one protocol, the lock's `MC_VERSION` |
| `paper` | the Felis lobby (Paper 26.3) | 26.3, the lock's `MC_VERSION` |
| `fabric` | a standalone Fabric server | 1.20.1 (`fabric.mod.json` declares `~1.20.1`) |
| `forge` | a standalone Forge server | 1.20.1 (`mods.toml` declares `[1.20.1,1.20.2)`) |
| `neoforge` | a standalone NeoForge server | 1.20.4 (`mods.toml` declares `[1.20.4,1.20.5)`) |
The Felis network itself runs Minecraft 26.3. The loader mods target the older 1.20.x
modding lines and belong on a standalone server outside the network; they load on no
26.x server, and nothing in a Felis install loads them.
"Built by the installer" is what `deploy/bootstrap.sh` produces, and it is the same set
`bootstrap_asset.go` embeds into the felis binary for the TUI install path, which has no source
checkout to build from. **The three loader mods are not in that set** — a finished install has
no `felis-fabric`/`felis-forge`/`felis-neoforge` jar anywhere. They build from this checkout with
the commands under [Building](#building) and are deployed by hand; the account-link flow they
carry works, but nothing installs them for you.
> **The loader mods are for a standalone server only.** Inside a Felis network the proxy's
> `/link` already serves every backend and shadows a backend's own, so user game servers need
> no mod. A mod answers `/link` only when its server runs `online-mode=true`: an offline-mode
> server is either behind a proxy (whose `/link` applies) or cracked, where the UUID is
> whatever the client claims.
>
> **The token a mod holds is a real credential.** It mints a link code for any UUID the mod
> asks about, so whoever can read that server's files — its operator, any plugin or mod on
> it, a copied backup — can bind a not-yet-linked player's Minecraft account to their own web
> account. Put a mod only on a server whose operator you would trust with that, and give it
> the `limbo` token: it opens the link-code, link-status and blacklist routes and nothing
> else. The `velocity` token also approves op-logins and wakes or claims servers for any
> player; it stays on the proxy host. `sudo felis rotate-token limbo` replaces a leaked
> token (the login gate restarts onto the new value; copy it into the mod's
> `felis-link.properties` by hand; the mod takes it on its next call, without a restart).
## Whose identity each path trusts {#whose-identity-each-path-trusts}
Only the **Velocity path** is protected against a forged identity. The proxy runs
`online-mode=true` (bootstrap writes it), so Velocity checks every login with Mojang, and
everything downstream takes its identity from that login:
- the proxy's own `/link`, `/felis` and `/invite` use the UUID of the verified connection;
- the lobby's `felis:control` frames are attributed to the backend connection they arrived
on, and the `player` field a lobby writes is ignored (see
[Lobby menu](#lobby-menu-§12));
- the login gate and the lobby run offline-mode behind the proxy and accept only logins
carrying Velocity's modern-forwarding signature (the shared forwarding secret), so a
client that bypasses the proxy cannot claim a UUID.
A proxy started with `online-mode=false` refuses to route (it logs an error and turns
routing off); its `/link` then sees only name-derived offline UUIDs, which never equal a
Mojang account's.
The **loader mods** are outside that protection. A mod trusts the UUID its own server
reports (`getUUID()`), so it is exactly as trustworthy as that server: the mod refuses
`/link` unless the server runs `online-mode=true`, and whoever operates the server holds
a token that can mint a code for any UUID (see the warning above).
## Architecture {#architecture}
Each platform is an **independent** Gradle build with its own `settings.gradle`,
not one root project mixing loader plugins (the loader Gradle plugins have
conflicting Gradle-version requirements — see below). The platform-neutral link
core lives in `shared/src/main/java` and is pulled into every module via:
```groovy
sourceSets { main { java { srcDir '../shared/src/main/java' } } }
```
The core (`best.lolicon.felis.link`) has **zero third-party dependencies** — it
uses the JDK's `java.net.http.HttpClient` and a small hand-written JSON parser —
so there is nothing to shade and each jar is self-contained.
- `LinkClient` — `POST {apiBaseUrl}/api/v1/internal/account/link/code` with
`Authorization: Bearer <service-token>` and body `{"mc_uuid":"<uuid>"}`;
`201 → {code, expires_at}`, otherwise the `{error:{code,message}}` envelope.
- `LinkConfigLoader` — reads `FELIS_API_BASE_URL` / `FELIS_SERVICE_TOKEN` (env
wins) or a `felis-link.properties` file written as a commented template on
first run. **The API URL and service token are deployment inputs and are never
compiled in.** Two optional keys bound each call, in whole seconds from 1 to 120
(default 10): `connect-timeout-seconds` / `FELIS_API_CONNECT_TIMEOUT_SECONDS`
for opening the connection, `request-timeout-seconds` /
`FELIS_API_REQUEST_TIMEOUT_SECONDS` for the whole call. Anything else fails the
load with the key named.
- `FelisApiClient` — the routing client. A GET that failed on a dropped
connection or a 502/503/504 is tried once more after a 100–400 ms jittered
pause; a GET that timed out, and every POST (wake, claim, join-event,
approvals), is never repeated.
Threading: the command runs on the server thread; the HTTP call is dispatched to
a daemon single-thread executor and the reply is hopped back onto the server
thread, so a slow felis-api never stalls the tick loop. If config is missing the
plugin loads but never registers `/link`, so the server runs un-crippled.
All three mods use **official Mojang mappings**, so the MC class/method names are
identical across Fabric/Forge/NeoForge and the command handler is uniform; only
the `@Mod`/event-bus/config-dir glue differs per loader.
## Velocity routing (§11) {#velocity-routing-§11}
Velocity sits on the player-facing edge, off-cluster, so it is where
domain-autostart routing lives. Beyond `/link`, the Velocity plugin recognizes
each felis server by its subdomain, registers backends into Velocity's dynamic
server registry, and decides — per join — whether to send the player straight in,
wake a sleeping server and park them, or ask them to reconnect. It drives §9 wake
and §11 routing over the felis-api **internal** face (service-token auth), and
additionally terminates the `felis:control` plugin-message channel that backs the
§12 lobby menu — translating each lobby frame into the same wake/claim/status
calls, against the player's connection-derived identity rather than anything the
lobby claims. See **[Lobby menu](#lobby-menu-§12)** below.
Two preconditions gate routing, **each fails safe** (routing turns off, `/link`
keeps working):
- **online mode** — `online-mode=true` in `velocity.toml`. The autostartPolicy
and allowlist gates trust Mojang-verified UUIDs; under offline mode the plugin
refuses to route on spoofable identities and logs an error.
- **root-domain** — the deployment zone (e.g. `mc.example.net`). This is the only
place the zone enters the proxy and is **never compiled in**; without it,
host-based routing has nothing to match and stays off.
What it does when routing is active:
| Surface | Behavior |
| ------- | -------- |
| Backend registry | Polls `GET /api/v1/servers` every 15 s and reconciles Velocity's dynamic registry. A failed poll **keeps existing registrations** — a control-plane blip never deregisters live backends. The API advertises each backend Service's host-routable ClusterIP, avoiding cluster-DNS names on the host-run proxy. |
| Join (`PlayerChooseInitialServerEvent`) | Resolves `subdomain.<root-domain>` and remembers the target, but every fresh connection still enters `login`. When the login gate requests its post-auth lobby transfer, Velocity re-checks link status: a ready remembered target is selected immediately; an asleep target is woken and queued from the lobby. |
| Waiting queue | One scheduled drain every 2 s polls status once per distinct waited-on server. A waiter stays while its server is on the way up (starting, or Failed inside the operator's restart backoff), hears a progress line each minute, and drops out on transfer, on the player leaving, when the start is given up (`startGaveUp`) or the server is stopped, after 120 s without an answer from felis-api, or at a one-hour backstop. |
| Wake gate | The wake is `POST /api/v1/internal/servers/{name}/wake` keyed on the player's online-mode UUID. **403** (policy refused) tells the player and stops; **429** (wake already in flight) keeps waiting. |
| Server-list ping (`ProxyPingEvent`) | Answers from the cached lifecycle view with a phase-aware MOTD (online / starting / sleeping) — **read-only, never wakes** anything. Mirroring each backend's own MOTD by background-pinging ready servers is a later slice. |
| Join report (`ServerConnectedEvent`) | Reports real joins to a felis backend via `POST …/join-event`, so the reaper sees activity and the player is auto-added to the server allowlist. |
| `/felis`, `/felis list` | Operator status: online-mode, root-domain, lobby, and the known server set with phase/ready. |
| `/felis lobby`, `/felis go <lobby>` | Moves the player back to the lobby from any backend. Nothing is woken, and a wait already queued still moves them when its server is ready. Kept under `/felis` so a user server's own `/lobby` or `/hub` is not shadowed by the proxy. |
| `/server` | Velocity's own `/server` is removed so the command reaches the backend: the lobby's server menu there, a user server's own `/server` anywhere else. It listed the login gate, the lobby and every running server to every player. A `/server` another proxy plugin registered is kept. With routing off the built-in stays, since it is then the one way to switch servers. |
Velocity-only config keys (read from the same `felis-link.properties` / env as
`/link`; env wins):
| Key | Env | Meaning |
| --- | --- | ------- |
| `root-domain` | `FELIS_ROOT_DOMAIN` | Routing zone, e.g. `mc.example.net`. Unset → routing off. |
| `login-server` | `FELIS_LOGIN_SERVER` | The system auth gate every fresh connection must pass. Defaults to `login`. |
| `lobby-server` | `FELIS_LOBBY_SERVER` | The distinct post-auth holding server used while a backend wakes. Defaults to `lobby`; it must not equal `login-server`. |
Load bounds on the proxy: felis-api calls run on a pool of 8 threads with 64
waiting slots. Past that a call is refused at once — the player reads "busy", a
lobby frame gets a `busy` error, and a dropped join-event is logged at warn —
rather than piling up threads while felis-api is slow. The registration refresh
(15 s) and the waiting-queue poll (2 s) skip a run that falls due while the last
one is still going. Acting commands (`/link`, `/felis claim`, migrate, op
approve) share a per-player budget of 5 then one per 5 s; felis:control frames
from the lobby are metered per player and menu status answers are cached.
Health on the proxy: every 10 minutes that saw any activity the proxy logs one
info line, `Felis: last 10 min: felis-api calls=… (no answer=…, 4xx=…, 5xx=…,
retried=…), avg=… ms, max=… ms, busy refusals=…, join-events failed=…,
join-events dropped=…, transfers failed=…, server-list refreshes failed=…,
waiting now=…`, with only that window's counts. `/felis` run from the console
adds the same felis-api counts since start, the waiting count and the failure
totals. A server-list refresh that keeps failing warns once when it starts, then
every 5 minutes with the running count, and logs at info when it recovers; the
login gate treats its link-status polls the same way.
## Lobby menu (§12) {#lobby-menu-§12}
The `paper/` module is the lobby's player-facing face for §27 scenario 10
(`/menu → plugin msg → velocity → api → shared waiting queue → Connect when ready`). It runs
on the Paper lobby server and gives players a chest GUI instead of a command
line: `/menu` (alias `/server`, which the proxy passes through with routing active) opens a grid of one tile per server the proxy routes,
and clicking a tile wakes, claims, or joins that backend.
**Pure UI face.** The lobby holds no felis-api token, opens no HTTP connection,
and keeps no waiting queue. Every action it takes is a single frame on the
`felis:control` plugin-message channel; every piece of state it shows arrives as
a frame on the same channel. Velocity (the `ControlChannel`, above) is the only
side that talks to felis-api. This is enforced **physically** by the build, not
just by convention: the module's `sourceSets` include-filter compiles in only the
paper package plus the three codec classes, so the lobby jar contains exactly
these classes —
```
best/lolicon/felis/link/Control.class (channel framing)
best/lolicon/felis/link/ControlFrame.class (the frame model)
best/lolicon/felis/link/Json.class (codec)
best/lolicon/felis/paper/FelisPaperPlugin.class (+ $1)
best/lolicon/felis/paper/LobbyGuard.class
best/lolicon/felis/paper/MenuHolder.class
best/lolicon/felis/paper/MenuTiles.class (+ $Kind, $Tile)
```
— and **no** `FelisApiClient`, `LinkClient`, or token-config class. If a codec
class ever grew a dependency on the API client, compilation would fail here
rather than silently widen the lobby's reach.
**Frames.** Upstream (lobby → velocity) carries `ListRequest`, `WakeRequest`,
`ClaimRequest` and `StatusQuery`; downstream (velocity → lobby) carries `ListUpdate`,
`StatusUpdate`, `TransferReady` and `Error`. `/menu` sends a `ListRequest`, and the
proxy answers with a `ListUpdate` naming every user server it routes, built from the
registry it routes by, so a server created in the panel appears without anyone editing
the lobby. The list leads with the player's own servers and carries, per server,
felis-api's verdict for this player (`GET /api/v1/internal/player/menu-access/{uuid}`,
one call per menu open): `owner`, `wake`, `owner_only`, `allowlist`, `retiring` or
`start_failed`. When felis-api cannot answer, the names go out alone and the tiles
fall back to the wake's own judgement. The menu then paints a grey "loading" tile per server (45 per page, arrows
in the bottom row) and fires a `StatusQuery` for each; the proxy answers with
`StatusUpdate` frames that repaint each tile by phase + ownership.
**Anti-spoof (§14).** The `player` field a lobby puts in a frame is **not**
trusted. Velocity derives the acting player and UUID from the `ServerConnection`
the plugin message arrived on, and the server-side autostartPolicy / ownership
gates authorize against that verified identity. The frame's `server` field is the
trusted payload — it only names *which* tile was clicked. A fully compromised
lobby therefore cannot act as another player or reach the API directly.
**Button rules** (`MenuTiles`: the last `StatusUpdate` plus the player's verdict, first match wins):
| Tile state | Label | Frame sent |
| ---------- | ----- | ---------- |
| up (`ready`), any verdict | **Join** (green) | `WakeRequest{server}` |
| ownerless (`claimable`) | **Claim & Start** (gold) | `ClaimRequest{server}` |
| `retiring` / `start_failed` / `owner_only` / `allowlist` | **Can't start** (grey, reason in the lore) | nothing; the reason goes to chat and the menu stays open |
| anything else (`owner`, `wake`, no verdict) | **Start** (red) | `WakeRequest{server}` |
The player's own servers are marked ★ and "Your server". The status line shows the
phase in the player's language (Running, Starting, Stopping,
Stopped, Failed to start, Unknown). "Join" and "Start" are the
**same** upstream frame (`WakeRequest`): the proxy joins a server that is already up,
and every linked player may join one. A refusal comes back as an `Error` frame (`not_linked` / `quota_exceeded` /
`already_claimed` → a friendly message), which is the only place a claim/quota/
policy failure surfaces to the player; readiness arrives as `TransferReady` just
before the proxy Connects them.
> **Status.** This slice is **code-complete and compile-verified** (paper jar
> builds green on a Java-25 toolchain; the velocity end compiles the full shared
> tree; the wire codec round-trips; the fabric/forge/neoforge mods compile through
> their vendored wrappers and boot real dedicated servers with `/link` registered —
> all of it gated by CI). It is **not** client-verified:
> no real game client has joined through the stack, so §27 scenario 10 stays
> **FAIL (live-unverified)** until such a join is exercised. The client-independent
> faces (proxy edge, subdomain MOTD, login boundary, backend registration) are
> exercised on a live deployment.
## Building {#building}
Every module builds through its own vendored Gradle wrapper, and each wrapper pins its
distribution's sha256 (`distributionSha256Sum`), so a tampered or swapped Gradle download
fails before it runs. The platforms need different Gradle versions (a real, measured
constraint):
| Module | Gradle | JDK | Why |
| ----------- | ------ | --- | ------------------------------------------------------------ |
| `velocity` | 9.8.0 | ≥ 21 runs it, emits Java 21 | plain `java` plugin; velocity-api 3.5.1 declares `jvm.version = 21` |
| `paper` | 9.8.0 | **25 toolchain** | paper-api 26.3 is published as a Java-25 artifact, so the module declares a `JavaLanguageVersion.of(25)` toolchain |
| `limbo` | 9.8.0 | ≥ 21 runs it, emits Java 17 | current LOOHP/Limbo releases ship class-file major 65, so the compiler JDK must be ≥ 21 to read them; `release 17` bytecode loads on any Limbo running Java 17+ |
| `fabric` | 8.8 | 17 | loom 1.7.4 uses `Problems.forNamespace`, removed in Gradle 9 |
| `forge` | 8.8 | 17 | ForgeGradle 6 is Gradle-8-only |
| `neoforge` | 8.14 | 17 | NeoGradle 7.1.38 requires Gradle API ≥ 8.14 |
The three plugins the installer bakes in (`velocity`, `paper`, `limbo`) are built with
Gradle 9.8.0 everywhere: through the wrapper locally and in CI, and inside the image
`gradle:9.8.0-jdk25@sha256:…` in the lobby and limbo Dockerfiles and bootstrap's
Velocity build. `bootstrap_asset_test.go` fails when the image, its digest or the
wrapper version drift apart.
```bash
# Velocity and Paper
plugins/velocity/gradlew -p plugins/velocity build
plugins/paper/gradlew -p plugins/paper build
# limbo compiles against the LOOHP/Limbo API release the login gate bundles, which has
# to be named: pass deploy/game-stack.lock's LIMBO_VERSION, exactly as bootstrap does.
plugins/limbo/gradlew -p plugins/limbo build -PlimboVersion="$(sed -n 's/^LIMBO_VERSION=//p' deploy/game-stack.lock)"
# Fabric / Forge / NeoForge. Nothing installs these; the jar you want is the one this
# produces.
plugins/fabric/gradlew -p plugins/fabric build
plugins/forge/gradlew -p plugins/forge build
plugins/neoforge/gradlew -p plugins/neoforge build
```
The first build of each mod downloads and remaps/decompiles Minecraft, so it takes a few
minutes; subsequent builds are fast. Jars land in each module's `build/libs`. CI runs
both gates: `bash plugins/test.sh` (JDK 25 — the install-time plugins, the
codec/invite/server-list tests, and the proxy routing tests that drive ServerRegistry,
WaitingRouter and ControlChannel against the real velocity-api; run those alone with
`plugins/velocity/gradlew -p plugins/velocity routingTest`. It also runs the lobby menu
test that drives LobbyMenu against the real paper-api (`plugins/paper/gradlew -p
plugins/paper lobbyTest`), the login gate test that drives LoginFlow against a stub
felis-api on virtual ticks (`plugins/limbo/gradlew -p plugins/limbo
-PlimboVersion=<lock's LIMBO_VERSION> loginTest`), and ModLinkTest for the `/link` the
loader mods share) and `bash plugins/test-mods.sh` (JDK 17 — the three loader mods,
via the wrappers above).
### Dependency verification {#dependency-verification}
Every dependency version is exact: paper-api is the API of the Paper build
`deploy/game-stack.lock` installs (`paper-26.3-40.jar` → `26.3.build.40-alpha`), limbo
compiles against the lock's `LIMBO_VERSION`, velocity-api is the lock's
`VELOCITY_VERSION`, and ForgeGradle is `6.0.54`. The installer's three modules also
carry `gradle/verification-metadata.xml`, the sha256 of every artifact their build
resolves, and Gradle refuses any artifact whose bytes differ. The Limbo API entry is
the very jar the login gate runs (its sha256 equals the lock's `LIMBO_JAR_SHA256`).
After `deploy/update-game-stack-lock.sh` moves Paper, Limbo or Velocity, bring the pins
along and regenerate the checksums, then review the diff:
```bash
# 1. set paper-api in plugins/paper/build.gradle to the new build (paper-<mc>-<n>.jar → <mc>.build.<n>-<channel>)
# 2. regenerate the three verification files (JDK 25) from an EMPTY Gradle home: with a
# warm cache Gradle skips the BOMs and parent POMs it already holds, and the image
# builds, which start empty, then refuse them
rm -f plugins/{velocity,paper,limbo}/gradle/verification-metadata.xml
export GRADLE_USER_HOME="$(mktemp -d)"
plugins/velocity/gradlew -p plugins/velocity --write-verification-metadata sha256 build
plugins/paper/gradlew -p plugins/paper --write-verification-metadata sha256 build
plugins/limbo/gradlew -p plugins/limbo --write-verification-metadata sha256 build \
-PlimboVersion="$(sed -n 's/^LIMBO_VERSION=//p' deploy/game-stack.lock)"
unset GRADLE_USER_HOME
# 3. go test . fails until the pins, the checksums and the lock agree
```
The loader mods pin exact plugin and dependency versions and their wrappers' sha256,
but carry no verification file: loom, ForgeGradle and NeoGradle fetch and remap
Minecraft through their own downloaders (checked against Mojang's manifest hashes), and
nothing installs these jars.
## Deploying {#deploying}
Drop the matching jar into the server/proxy mods or plugins directory, start
once to generate `config/felis-link.properties` (or `plugins/felis-link/…` on
Velocity), then set `api-base-url` and `service-token` — or provide
`FELIS_API_BASE_URL` and `FELIS_SERVICE_TOKEN` in the environment, which take
precedence. The token is one of felis-api's per-caller internal tokens: the
Velocity proxy uses the `velocity` token (Secret `felis/felis-service-token`), the
login gate the `limbo` token (`felis-limbo-token`), and each serves only its own
routes (a token on another caller's route gets `403 wrong_caller`). A loader mod
takes the `limbo` token, on a standalone online-mode server only (see the warning
under the module table). Treat it as a secret; `sudo felis rotate-token <caller>`
replaces it. A token read from this file follows the file: the proxy or mod
presents a new `service-token` from its next call to felis-api (it re-reads the
file at most once a second), logs `service-token reloaded from … (fingerprint …)`, and keeps its
players. A token from `FELIS_SERVICE_TOKEN` is fixed until the process restarts.
On **Velocity**, also set `root-domain` (and optionally `lobby-server`) in the
same file to turn on §11 routing, and make sure `online-mode=true` in
`velocity.toml` — without either, the proxy still serves `/link` but routing
stays off (see **[Velocity routing](#velocity-routing-§11)**). The config dir is
`plugins/felis-link/` because the plugin id is `felis-link` (kept stable across
the 0.1 → 0.2 jar so existing config carries over).
On the **Paper lobby** there is no token to set, because the lobby never talks to
felis-api, and no server list to keep: the menu shows the servers the proxy routes,
which the proxy sends over `felis:control` (a `servers:` list left in
`plugins/FelisPaper/config.yml` by an older version is ignored, and the plugin says so
at startup). The lobby must sit behind the same Velocity proxy as the backends — it
reaches the control plane only through the proxy's `felis:control` terminus — so it
needs no `api-base-url` and no `service-token` of its own. The installer builds and
bakes this jar into the lobby image; installing it by hand is for a lobby you run
yourself.
---
Source: [plugins/README.md](https://github.com/FelisMC/Felis/blob/main/plugins/README.md).
+131
View File
@@ -0,0 +1,131 @@
---
title: Project README
---
# Felis {#felis}
A Kubernetes-driven Minecraft server hosting platform
One command to deploy, with automatic lifecycle, backup, and security
> [!CAUTION]
> **This project is still in early development. Do not use it in production. The FelisMC team accepts no civil or criminal liability for problems arising from its use.**
<details>
<summary>Table of Contents</summary>
- [Features](#features)
- [Getting Started](#getting-started)
- [Build from Source](#build-from-source)
- [License](#license)
- [Acknowledgements](#acknowledgements)
</details>
## Features {#features}
* **On-demand Start and Stop**: A server starts when a player connects to the proxy. The player waits in a queue during start-up and is transferred once the server is ready. Idle servers stop automatically to free memory.
* **Web Dashboard**: Monitor server status, online players, and resource usage from your browser.
* Console (RCON), whitelist, bans, OPs and LuckPerms permissions
* File manager: create, delete, rename, chunked upload, download, and unzip while the server is stopped; also used to import worlds
* Schedules: run commands, restart, stop, start or back up by weekday and time zone, with an in-game warning to players beforehand
* **Backup & Restore**: Enabled by default; the installer renders the archive PVC and its path.
* Manual backups: archive a server's entire data volume (`/data`, including worlds, configuration, plugins and mods) to the cluster's archive store, with rollback to any backup point
* Daily restore points: a server played that day gets a restore point once it stops; by default 7 are kept for up to 90 days, rotated separately from manual backups
* Download and export: download a single backup (with sha256 verification), delete a single backup, or export a whole world
* Off-site copy (optional): backups are encrypted on the host and synced to S3-compatible storage (AWS S3, Cloudflare R2, Backblaze B2, MinIO and others)
* Control-plane database: the database holding accounts, server ownership, quotas and the archive index is backed up daily and snapshotted before every upgrade migration; `felis db restore` rolls it back atomically, and the panel's Maintenance & Backups page shows the age of the latest backup (see [troubleshooting §16](/en/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery))
* **Diagnostics**
* `sudo felis status`: a summary of the node, control plane, game proxy, each server, backups and open alerts
* `sudo felis doctor`: runs all health checks and lists problems by area with troubleshooting pointers; sends no email
* `sudo felis support-bundle`: generates a redacted diagnostics archive to attach to support requests (see [troubleshooting §0](/en/operations/troubleshooting#_0-first-look-felis-status-felis-doctor-felis-support-bundle))
* Watchdog: runs a check every 2 minutes and emails the platform owners when a problem persists; supports an external heartbeat monitor
* **World Reaper** (optional): Worlds idle for more than 15 days are backed up and then removed to free disk space. Enable it by setting `FELIS_WORLDS_HOST_PATH` at install time (on k3s: `/var/lib/rancher/k3s/storage`); without it, no world is deleted. Expired backups are cleaned up daily regardless of this setting.
* **Multi-core Support**: Compatible with Paper, Fabric, Forge, and NeoForge, accessed through a single Velocity proxy.
* **Modpack Submission**: Players can upload modpacks. After admin approval, each modpack is built automatically and scanned with Trivy; the result is added to the image whitelist and can be selected as a server image.
* **Security**
* Passkey login: passwordless authentication via fingerprint, face recognition, or hardware security keys
* Zero-trust access: panel traffic is protected by Cloudflare Access, and the internal API is not exposed to the internet
* **Multi-node Deployment** (experimental, off by default): a single controller node issues all commands, the other nodes run game servers only, and a stopped server can be migrated to another node. Currently available only on the main branch; three-node acceptance testing is not yet complete (see [distributed mode](/en/guide/distributed)).
## Getting Started {#getting-started}
On a prepared Linux host, run:
```bash
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash
```
The script installs K3s, deploys PostgreSQL and the control plane inside it, and launches a setup wizard. When setup completes, open the configured domain in a browser to reach the control panel.
* **Setup wizard**: The wizard first binds the platform Owner: join the address it shows in Minecraft Java Edition, then enter the 8-character link code that the login server displays (valid for 10 minutes). The step can be skipped and completed later by running `sudo felis setup` again; until an Owner is bound, nobody can sign in to the control panel, and the sign-in page states this together with the binding steps and the address to join. The installer launches the wizard automatically only on an interactive terminal; when output is redirected to a log or the install runs under cloud-init, run `sudo felis setup` after it finishes. Setting `FELIS_NO_SETUP=1` makes the installer end at its summary.
* **Supported hosts**: CentOS Stream 9 (aarch64) is verified on physical hardware; Ubuntu 24.04 (x86_64) is tested in CI on every push with a fresh install, a rerun, an upgrade and the install command above (see [operations §1](/en/operations/#_1-supported-hosts)).
* **Preflight checks**: Before modifying the host, the installer checks memory, disk, ports, network range conflicts, existing Kubernetes installations and outbound connectivity. If any check fails, it lists all problems and exits, leaving the host unchanged (see [operations §1](/en/operations/#_1-supported-hosts) for the checks).
* **Upgrading**: Rerun the install command to upgrade felis-api to a newer version; `felis setup` only uses the binary already installed on the host and cannot upgrade it. A rerun keeps the installed root domain, and the release channel must be specified again: hosts that follow the main branch must also set `export FELIS_VERSION_BOOTSTRAP=dev`. A PostgreSQL instance installed on the host by an earlier release is migrated into K3s during the rerun; the original instance on the host is stopped and retained for rollback (see [operations §4](/en/operations/#_4-upgrading-the-pieces-around-felis)).
<details>
<summary>Installation sources and restricted networks</summary>
<br>
A release installation takes the binary, all images and the Velocity plugin from the release assets prebuilt in CI, verifying each against `SHA256SUMS` before import. The host requires no Docker, Gradle or Go, and no access to Docker Hub. If an asset is missing or fails verification, only that image falls back to a local build, and the installer prints a notice (see [troubleshooting §15c](/en/operations/troubleshooting#_15c-the-installer-builds-on-the-host-although-it-installs-a-release)).
The assets can also be copied to the host in advance and installed with `FELIS_ARTIFACT_DIR=<absolute path>`; the Felis binary, images and plugin are then read from that directory. k3s and its images, the JRE, cloudflared, Velocity and the Via plugins are still downloaded from GitHub and PaperMC; hosts with SELinux enabled, such as RHEL, Fedora and openSUSE Leap, additionally install k3s-selinux from rpm.rancher.io; system packages come from the distribution's repositories.
A host with restricted outbound access must therefore allow HTTPS to these addresses or set `https_proxy`. Preflight probes each address before changing the host. Fully offline installation is not yet supported (see [operations §1](/en/operations/#_1-supported-hosts) for the address list).
</details>
## Build from Source {#build-from-source}
Felis is built with Go and Node.js:
```bash
# Backend (Go 1.26+)
go build -o felis ./cmd/felis
# Frontend (Node.js 22+)
cd panel
npm ci
npm run build
# Docker image
docker build -t felis:custom .
```
## License {#license}
This project is licensed under [AGPL-3.0-only](/LICENSE.txt).
### License Notes {#license-notes}
1. **Derivative works must use AGPL**: Any distribution of this project or of software derived from it must be released under AGPL-3.0 and must include the original copyright notice and license statement.
2. **Network services must also provide source** (AGPL section 13): anyone who offers a modified Felis to others over a network must provide those users with the complete source of the modified version, even without distributing any binary. This is the only substantive difference between AGPL and GPL; as Felis is a hosting platform accessed over a network, this clause applies to virtually every deployment.
3. **Disclaimer**: This project is provided "as is", without warranty of any kind.
## Acknowledgements {#acknowledgements}
* [Kubernetes](https://kubernetes.io/): Container orchestration engine
* [K3s](https://k3s.io/): Lightweight Kubernetes distribution
* [Cloudflare Zero Trust](https://www.cloudflare.com/zero-trust/): Zero trust security infrastructure
* [PostgreSQL](https://www.postgresql.org/): Data persistence
* [React](https://react.dev/): User interface framework
* [Vite](https://vitejs.dev/): Frontend build tool
* [TailwindCSS](https://tailwindcss.com/): CSS framework
* [Bubble Tea](https://github.com/charmbracelet/bubbletea): TUI framework
* [Minecraft](https://www.minecraft.net/): The game this project serves
---
Source: [README_EN.md](https://github.com/FelisMC/Felis/blob/main/README_EN.md).
+163
View File
@@ -0,0 +1,163 @@
---
title: Sequence diagrams
---
# Felis Sequence Diagrams {#felis-sequence-diagrams}
This file carries the spec section 28 sequence-diagram deliverables that are not
covered by the OpenAPI artifact.
## Section 28 #9: Ping To Join To Wake To Ready To Teleport {#section-28-9-ping-to-join-to-wake-to-ready-to-teleport}
```mermaid
sequenceDiagram
autonumber
actor Player
participant Velocity as Velocity proxy
participant Registry as Velocity server registry
participant API as felis-api internal face
participant Cluster as MinecraftServer CRD/status
participant Operator as felis operator
participant Backend as Minecraft backend
Player->>Velocity: server-list ping for subdomain.root-domain
Velocity->>Registry: read cached lifecycle view
Registry-->>Velocity: phase-aware MOTD
Velocity-->>Player: ping response (read-only, no wake)
Player->>Velocity: join subdomain.root-domain
Velocity->>Registry: resolve host to server
Registry-->>Velocity: ServerView(name, ready=false)
alt backend already ready and registered
Velocity-->>Player: initial server = backend
Player->>Backend: connect
else backend not ready and lobby configured
Velocity-->>Player: initial server = lobby
Velocity->>API: POST /api/v1/internal/servers/{name}/wake {mc_uuid}
API->>Cluster: GetServer(name)
API->>API: authorize autostartPolicy, cooldown, running cap
API->>Cluster: SetDesiredState(name, Running)
API-->>Velocity: 202 phase/ready
Velocity->>Velocity: enqueue waiter
Operator->>Cluster: reconcile DesiredState=Running
Operator->>Backend: start pod/service
Backend-->>Operator: RCON-ready / lifecycle ready
Operator-->>Cluster: status.ready=true
loop every waiting tick
Velocity->>API: GET /api/v1/internal/servers/{name}/status
API->>Cluster: GetServer(name)
API-->>Velocity: ready flag
end
Velocity->>Registry: lookup registered backend
Velocity-->>Player: "ready - moving you in"
Velocity->>Player: Connect request to backend
Player->>Backend: connect
Velocity->>API: POST /api/v1/internal/servers/{name}/join-event {mc_uuid}
API->>API: RecordJoin#59; refresh activity and allowlist UUID
API-->>Velocity: 204
else backend not ready and no lobby configured
Velocity-->>Player: disconnect with reconnect-later message
Velocity->>API: POST /api/v1/internal/servers/{name}/wake {mc_uuid}
API->>Cluster: SetDesiredState(name, Running) if authorized
API-->>Velocity: 202 or branchable error
end
```
## Section 28 #11: Claim Transaction {#section-28-11-claim-transaction}
```mermaid
sequenceDiagram
autonumber
actor Player
participant Panel as Web panel
participant API as felis-api external face
participant Repo as Repo / Postgres
participant Audit as Audit log
Player->>Panel: click Claim on ownerless server
Panel->>API: POST /api/v1/servers/{name}/claim
API->>API: validate server name and principal
API->>Repo: IsLinked(user_id)
alt user has no verified account link
Repo-->>API: false
API-->>Panel: 412 not_linked
else linked
Repo-->>API: true
API->>Repo: QuotaCheck(user_id, the server's real size)
Note over API,Repo: all four caps: servers, CPU, memory, storage
alt quota exhausted
Repo-->>API: false
API-->>Panel: 403 quota_exceeded
else quota available
Repo-->>API: true
API->>Repo: ClaimServer(name, user_id)
Note over Repo: ONE transaction: pg_advisory_xact_lock(user_id) serializes this user's claim lane#59; SELECT FROM servers WHERE name=$1 AND deleted_at IS NULL FOR UPDATE#59; re-run the four-dimension quota gate (authoritative — the pre-check above is a fast path)#59; then UPDATE servers SET owner_id=$2, claimed_at=now() WHERE name=$1 AND owner_id IS NULL AND deleted_at IS NULL
alt server missing
Repo-->>API: ErrNotFound
API-->>Panel: 404 not_found
else zero rows affected
Repo-->>API: claimed=false
API-->>Panel: 409 already_claimed
else one row affected
Repo-->>API: claimed=true
API->>Audit: external claim audit
API-->>Panel: 200 {"claimed":true}
end
end
end
```
## Section 28 #12: Account Binding /link Flow {#section-28-12-account-binding-link-flow}
```mermaid
sequenceDiagram
autonumber
actor Player
participant Game as Minecraft server or Velocity
participant LinkClient as Felis LinkClient
participant APIInternal as felis-api internal face
participant Repo as Repo / Postgres
participant Panel as Web panel
participant APIExternal as felis-api external face
Player->>Game: /link
Game->>Game: read verified online-mode UUID
Game->>LinkClient: requestCode(mc_uuid)
LinkClient->>APIInternal: POST /api/v1/internal/account/link/code {mc_uuid}
APIInternal->>APIInternal: validate UUID#59; derive auth_source from the UUID version nibble if absent (v3 → thirdparty, else mojang)#59; generate 8-symbol code
APIInternal->>Repo: CreateLinkCode(code, mc_uuid, auth_source, expires_at)
Repo-->>APIInternal: inserted account_link_codes row
APIInternal-->>LinkClient: 201 {code, expires_at, panel_url?}
LinkClient-->>Game: LinkCode
Game-->>Player: show one-time code in chat
Player->>Panel: open Account link flow
Panel->>APIExternal: POST /api/v1/account/link/start
APIExternal->>Repo: IsLinked(user_id)
Repo-->>APIExternal: linked status
APIExternal-->>Panel: status and "run /link" instructions
Player->>Panel: submit code
Panel->>APIExternal: POST /api/v1/account/link/verify {code}
APIExternal->>APIExternal: trim and uppercase code
APIExternal->>Repo: VerifyLinkCode(user_id, code, now)
Repo->>Repo: SELECT mc_uuid, auth_source FROM account_link_codes WHERE code=$1 AND expires_at>$2 FOR UPDATE
alt missing or expired code
Repo-->>APIExternal: ErrLinkCodeInvalid
APIExternal-->>Panel: 400 invalid_code
else UUID linked to a different, live user
Repo-->>APIExternal: ErrConflict
APIExternal-->>Panel: 409 already_linked
else valid code (re-verify by the same user is idempotent#59; a retired/soft-deleted owner's link is taken over)
Repo->>Repo: INSERT account_links(user_id, mc_uuid, auth_source) ON CONFLICT (user_id, mc_uuid) DO UPDATE auth_source
Repo->>Repo: DELETE account_link_codes WHERE code=$1
Repo-->>APIExternal: mc_uuid, auth_source
APIExternal->>Repo: Audit account.link
APIExternal-->>Panel: 200 {linked:true, mc_uuid, auth_source}
end
```
---
Source: [docs/sequence-diagrams.md](https://github.com/FelisMC/Felis/blob/main/docs/sequence-diagrams.md).
+26
View File
@@ -0,0 +1,26 @@
---
title: 从源码构建
---
# 从源码构建 {#从源码构建}
本项目基于 Go 与 Node.js 开发:
```bash
# 后端(Go 1.26+)
go build -o felis ./cmd/felis
# 前端(Node.js 22+)
cd panel
npm ci
npm run build
# Docker 镜像
docker build -t felis:custom .
```
开发环境、测试和插件构建步骤见[贡献指南](/reference/contributing)。
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)、[CONTRIBUTING.md](https://github.com/FelisMC/Felis/blob/main/CONTRIBUTING.md)。
+40
View File
@@ -0,0 +1,40 @@
---
title: 安装与部署
---
# 安装与部署 {#安装与部署}
> [!CAUTION] 注意
> **此项目仍处于早期开发阶段,您不该在任何生产环境使用该项目。若产生任何问题,贵用户的使用行为与 FelisMC 团队无任何民事刑事法律关系。**<br>
在已准备好的 Linux 主机上执行:
```bash
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash
```
脚本将安装 K3s,在 K3s 中部署 PostgreSQL 与控制平面,随后启动设置向导。设置完成后,通过浏览器访问所配置的域名即可进入控制面板。
* **设置向导**:向导首先绑定平台所有者:以 Minecraft Java 版加入向导所示的地址,登录服务器会给出 8 位绑定码(10 分钟内有效),将其输入向导即可。该步骤可以跳过,之后再次执行 `sudo felis setup` 补做;绑定所有者之前,任何人均无法登录控制面板,登录页届时会说明原因并列出绑定步骤及连接地址。安装器仅在交互式终端中自动启动向导;输出重定向至日志或经由 cloud-init 安装时,请在安装结束后执行 `sudo felis setup`。设置 `FELIS_NO_SETUP=1` 时,安装器在输出摘要后直接结束。
* **支持的系统**:CentOS Stream 9(aarch64)已在实机上验证;Ubuntu 24.04(x86_64)在每次推送时由 CI 执行全新安装、重复安装、升级及上述安装命令(参见 [运维手册 §1](/operations/#_1-supported-hosts))。
* **安装前检查**:安装器在修改主机之前检查内存、磁盘、端口、网段冲突、已有的 Kubernetes 及外网连通性。发现问题时一次性列出全部问题并退出,主机保持原状(检查项参见 [运维手册 §1](/operations/#_1-supported-hosts))。
* **升级**:重新执行安装命令即可将 felis-api 升级至新版本;`felis setup` 仅使用本机已安装的二进制,无法用于升级。重新执行时沿用已安装的根域名,发布通道需重新指定:跟随 main 分支的主机须同时设置 `export FELIS_VERSION_BOOTSTRAP=dev`。早期版本安装在宿主机上的 PostgreSQL 会在重新执行时整库迁入 K3s,宿主机上的原实例停用并保留,以便回退(参见 [运维手册 §4](/operations/#_4-upgrading-the-pieces-around-felis))。
<details>
<summary>安装来源与受限网络环境下的安装</summary>
<br>
安装发布版时,二进制文件、全部镜像及 Velocity 插件均取自该版本由 CI 预构建的 release 附件,逐一校验 `SHA256SUMS` 后导入。主机无需安装 Docker、Gradle 或 Go,也无需访问 Docker Hub。若某个附件缺失或校验失败,仅该镜像回退为本机构建,并输出提示(参见 [故障排查 §15c](/operations/troubleshooting#_15c-the-installer-builds-on-the-host-although-it-installs-a-release))。
也可将附件预先复制到主机,再通过 `FELIS_ARTIFACT_DIR=<绝对路径>` 安装,此时 Felis 自身的二进制、镜像与插件均从该目录读取。k3s 及其镜像、JRE、cloudflared、Velocity 与 Via 插件仍从 GitHub 和 PaperMC 下载;RHEL、Fedora、openSUSE Leap 等启用 SELinux 的主机还需从 rpm.rancher.io 安装 k3s-selinux;系统软件包来自发行版软件源。
因此,出站网络受限的主机须放行上述地址的 HTTPS 访问,或设置 `https_proxy`。preflight 会在修改主机之前逐一探测这些地址。目前暂不支持完全离线安装(地址清单参见 [运维手册 §1](/operations/#_1-supported-hosts))。
</details>
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)。
+125
View File
@@ -0,0 +1,125 @@
---
title: 多机部署
---
# 多机部署 {#a-主控与多机-worker}
分布式模式默认关闭。A 运行唯一 Felis API/operator、k3s server、PostgreSQL、Registry、归档服务和系统服;Velocity 继续使用 A 的 systemd 服务。B、C 等节点只运行 k3s-agent/containerd、游戏 Pod 和 A 创建的维护 Job。节点必须与 A 同架构、同 k3s 版本,宿主机由管理员信任并维护。
## 先准备 A {#先准备-a}
在维护窗口停服,使用现有数据库备份流程备份 PostgreSQL,并离线保存 k3s 状态、server token 和当前安装配置。SQLite k3s 的状态目录是 `/var/lib/rancher/k3s/server/db`;使用其他 datastore 时按对应备份流程操作。不要把这些文件复制到 worker。
记录 A 的现有节点名称,后续安装必须沿用。先把当前控制工作负载固定到 A,再启用 WireGuard;已有游戏 PVC 不迁移。
```bash
# 在 A,以 root 执行;替换节点名和所有固定节点地址。
A_NODE=existing-node-name
PEERS=192.0.2.10/32,192.0.2.11/32,192.0.2.12/32
k3s kubectl label node "$A_NODE" \
felis.node-restriction.kubernetes.io/identity="$A_NODE" \
felis.node-restriction.kubernetes.io/role=controller --overwrite
# 按实际部署名称执行,均使用受保护身份标签。
for d in felis-api felis-operator felis-postgres registry; do
k3s kubectl -n felis patch deployment "$d" --type merge \
-p "{\"spec\":{\"template\":{\"spec\":{\"nodeSelector\":{\"felis.node-restriction.kubernetes.io/identity\":\"$A_NODE\"}}}}}"
done
```
用包含此功能的 Felis 安装器在 A 重跑安装:`FELIS_DISTRIBUTED=1`、`FELIS_NODE_EXTERNAL_IP=<A 固定公网 IP>`、`FELIS_PEER_CIDRS="$PEERS"`,保留现有安装参数。该步骤会启用 `wireguard-native`、`flannel-external-ip`、NodeRestriction 和独立 agent token,并安装归档服务、最小 RBAC 和宿主机隔离规则。WireGuard 更换需要停服维护窗口。已有 worker 的对等地址列表也必须提前更新。
仅推送 main 不会自动发布 release。还未使用包含这些变更的发布资产时,在新版源码目录中以 root 执行下面的命令,显式从 main 构建;其他原安装参数继续保留。只更新安装脚本、仍使用默认 release 通道,可能下载到不支持分布式命令的旧二进制。
```bash
FELIS_REF=main FELIS_DISTRIBUTED=1 \
FELIS_NODE_EXTERNAL_IP=<A固定公网IP> \
FELIS_PEER_CIDRS="$PEERS" bash deploy/bootstrap.sh
```
直接生成部署清单时,增加:
```bash
felis manifests --felis-image <现有逻辑镜像引用> \
--distributed --controller-node "$A_NODE" \
--egress-probe felis-api.felis.svc:443 \
--velocity-cidr <A精确来源IP/32> \
--archive-local-path <原archive.local_path> --backup-pvc felis-backups
```
其他已有参数继续保留。安装器也把 CoreDNS、local-path-provisioner 固定在 A;手动部署时同样给这些 Deployment 设置 A 的受保护节点选择器。手动部署须把同一随机密钥保存到 `felis` 和 `minecraft` 命名空间的 `felis-archive-key` Secret(字段 `key`,至少 32 字符);只有 API、Reaper 和归档服务获得密钥,operator 不获得。归档 PVC、Registry 和数据库均留在 A。
## 接入 B,再接入 C {#接入-b-再接入-c}
先在所有现有节点执行 `felis node firewall --peers "$PEERS" --controller-ip <A地址>` 更新完整对等地址列表;A 增加 `--controller`。只允许精确 `/32` 或 `/128` 地址,不能用整个节点/Pod 网段充当 Velocity 或 Registry 来源。固定公网节点之间允许 WireGuard UDP 51820–51821,k3s 6443 只允许已知对等节点。
```bash
# A:每台 worker 独立创建,默认 10 分钟;输出文件 root-only,令牌不打印。
felis node token --name b --ttl 10m --out /root/b.bootstrap
k3s kubectl -n felis get svc registry
# 通过可信 SSH/SCP 把该文件和同版本 felis 二进制交给 B。
# B:不需要 server token、管理员 kubeconfig、数据库或 Registry 写入凭据。
felis node join --name b --server https://<A公网IP>:6443 \
--external-ip <B公网IP> --token-file /root/b.bootstrap \
--registry-ip <Registry ClusterIP> --peers "$PEERS"
# A:SSH 使用既有主机密钥校验;目标账号须能 sudo -n。
felis node approve --name b --ssh-target <B的SSH别名> \
--image <Felis逻辑镜像引用>
felis node list
```
安装 worker 不安装数据库、Velocity、API 或 operator,不删除本地卷或 node-password;重复安装会拒绝改名或更换集群。mirror 使用 Registry ClusterIP,关闭默认镜像源回退,保留原镜像引用。
批准前 worker 带 `NoSchedule` 隔离 taint,且没有受保护的 approved 标签。批准命令检查架构、版本、节点在线状态、WireGuard、宿主机隔离、kubelet 修改受保护标签被拒绝,删除指定镜像后执行真实 `crictl pull`。随后创建临时探针,检查跨节点 Service、控制服务的正向可达性和游戏标签 Pod 的拒绝路径。A 从宿主机连接每个测试 Service,读取后端实际观察到的源地址,只将属于 A 的精确地址写入游戏策略。任何检查失败都保留隔离状态。批准过程中会删除指定缓存镜像,须在该节点尚无游戏任务时执行。
节点批准后,管理员可在面板创建服务器时选择它,或在创建请求中传 `nodeName`。普通服主不能选节点或指定 PVC。失联节点拒绝新任务;operator 撤销该服务器 Service 的后端和 Ready 状态,Velocity 进入原有 fallback 流程,不换机。
## 停服迁移 {#停服迁移}
先在面板停服并等待 `Stopped` 和游戏 Pod 退出。打开“停服迁移”,选择在线且已批准的目标 worker。面板从 CR 的持久化记录读取最新操作,刷新页面或重启 A 后仍可查进度。
```bash
# A 的 root 运维入口,区别于数据库 felis migrate:
felis server-migrate start --name survival --target-node c
felis server-migrate status --name survival
felis server-migrate retry --name survival --id <操作ID>
```
管理员 API:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/api/v1/nodes` | 执行节点列表 |
| POST | `/api/v1/servers/{name}/migrations` | `{ "targetNode": "c" }`,返回操作及 ID |
| GET | `/api/v1/servers/{name}/migrations` | 最新迁移记录 |
| GET | `/api/v1/servers/{name}/migrations/{id}` | 当前操作进度 |
| POST | `/api/v1/servers/{name}/migrations/{id}/retry` | 重试失败阶段 |
阶段为 `backing_up` → `restoring` → `switching` → `succeeded`。锁是 `migration@<开始时间>`,不按临时维护锁的两分钟规则过期。失败保存阶段与原因,保持锁和停服状态;相同目标的重复请求返回已有操作,其他迁移被拒绝。恢复 Job 校验下载 SHA-256,恢复后逐文件读回校验;成功后才在一次乐观锁 CR 更新中切换节点、活动 PVC 和进度。停服 StatefulSet 会重建以使用新的 PVC,CR、Service、ClusterIP、域名及归属保持不变。
迁移成功仍不自动启动。检查目标世界后手动启动。记录中的 `sourcePVC` 保留,不被回收流程顺带删除;确认不再需要后由管理员显式清理。失败不要手动删除迁移注解或锁;排除源节点失联、目标磁盘不足、传输/校验错误后使用重试。已提交的切换不会自动回退到源世界。
## 隔离与验收 {#隔离与验收}
游戏进程沿用非 root、禁止提权、drop ALL、无 SA token 和宿主机命名空间的限制。维护 Job 只挂一个世界 PVC:传输 Job 仅访问归档服务和 DNS,文件/导出 Job 仅额外访问既有 API 传输入口。归档服务无数据库配置和 Kubernetes 身份;只有完整归档原子落盘后 A 才记录成功,现有 tarLocal 路径、保留与 offsite 流程继续使用。
宿主机 INPUT/FORWARD 规则封闭 resident-node 路径,raw PREROUTING 在 DNAT 前封闭 worker NodePort;A 的受信控制 Pod 精确地址可以访问 apiserver。raw 规则也封闭游戏 Pod 向宿主机发起的新连接,避免 kube-router 的提前 ACCEPT 绕过 filter 规则;Velocity/RCON 的已建立连接回复保留。规则由 systemd 安装,控制 Pod 地址定期更新。节点地址、防火墙或 CNI 配置变动后,先停服并重新执行批准检查,再运行不可信代码。游戏 egress gate 同时检查允许的 DNS TCP 路径和拒绝路径,分布式模式超时拒绝启动。
必须在 A/B/C 三台 Linux 机器完成上线验收,不能用单机单元测试代替:
- Velocity `ClusterIP:25565` 跨节点连接及实际源地址、RCON、休眠唤醒、缓存清除后镜像拉取。
- B 上备份恢复,B→C 迁移,确认 Service IP/归属不变、源 PVC 保留、目标仍停服。
- 迁移期间唤醒、文件写入、导出、回收及重复请求互斥;A 重启继续协调。
- 源节点失联、传输中断、目标/归档磁盘满和读回校验失败,确认源世界可恢复。
- 游戏 Pod 对各服、主控、Registry、宿主机端口、kubelet、元数据的请求均拒绝;各拒绝目标在受信正向探针中确实可达。
- 过期、重放、跨服和错误操作的归档令牌拒绝;kubelet 伪造受保护标签拒绝。
当前本地验证使用一台已有 Felis 的 ARM64 CentOS Stream 9 VM,通过临时测试程序验证归档和迁移逻辑;`deploy/test-node-firewall.sh` 在独立网络命名空间中实测 resident-node、NodePort DNAT 和提前 ACCEPT 的防护,不改变其现有集群网络。三机网络验收仍是上线前必要步骤。A 继续是控制面和公网入口单点,首版没有主控 HA 或自动故障迁移。
相关上游说明:[k3s 跨公网组网](https://docs.k3s.io/networking/distributed-multicloud)、[限时 bootstrap token](https://docs.k3s.io/cli/token)、[NodeRestriction 标签](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-isolationrestriction)、[NetworkPolicy 的节点边界](https://kubernetes.io/docs/concepts/services-networking/network-policies/)。
---
原文:[docs/distributed.md](https://github.com/FelisMC/Felis/blob/main/docs/distributed.md)。
+37
View File
@@ -0,0 +1,37 @@
---
title: 管理服务器
---
# 管理服务器 {#管理服务器}
* **按需启停**:玩家连接代理时自动启动目标服务器,启动期间玩家进入等待队列,服务器就绪后自动传送;服务器空闲后自动停止,释放内存。
* **Web 控制面板**:在浏览器中查看服务器状态、在线玩家与资源用量。
* 控制台(RCON)、白名单、封禁、OP 与 LuckPerms 权限管理
* 文件管理:新建、删除、重命名、分片上传、下载,以及停服状态下解压 zip,可用于导入世界
* 计划任务:按星期与时区定时执行命令、重启、停止、启动或备份,执行前在游戏内向玩家发送提醒
* **多核心支持**:兼容 Paper、Fabric、Forge 与 NeoForge,统一经由 Velocity 代理接入。
* **模组包投稿**:玩家可上传模组包,经服主审批后自动构建并通过 Trivy 安全扫描;构建产物加入镜像白名单后,可直接选作服务器镜像。
## 12. 配置字段似乎没有生效 {#_12-a-configuration-field-seems-to-be-ignored}
以下字段都会被控制器读取。是否生效,取决于各自的触发条件。
| 字段 | 预期用途 | 实际行为 |
|---|---|---|
| `spec.startup.timeoutSeconds` | 启动失败前的时间限制 | 由 `startupTimedOut`(`reconciler.go:479`)读取,在 `:126` 调用。未设置或为 `0` 时默认 **300s**,超时后执行 `markFailed("StartupTimeout")` |
| `spec.startup.readinessTimeoutSeconds` | 首次探测的时间限制 | 由 `readinessTimedOut`(`reconciler.go:490`)读取,在 `:157` 调用。未设置或为 `0` 时默认 **300s**,超时后执行 `markFailed("ReadinessTimeout")`。它不同于探测器自身的 5 秒连接超时(`prober.go:45`) |
| `spec.idle.autoStopEnabled` | 自动停止无人服务器 | 在 `reconciler.go:175` 读取,但必须启用 `spec.rcon.enabled`,因为玩家数来自 RCON 探测(§11) |
| `spec.idle.emptySecondsBeforeStop` | 无人状态的宽限期 | 属于同一分支,必须 `> 0`;`0` 表示关闭,而非立即停服 |
两个启动时间限制都从同一个 `status.startRequestedAt` 开始计时。因此,`readinessTimeoutSeconds` 限制的是整个启动过程,并在 RCON 探测分支检查;它不是 Pod 就绪之后额外分配的时间。
---
完整的启动、连接、休眠和文件操作排查步骤见[故障排查](/operations/troubleshooting)。
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)、[docs/troubleshooting.md](https://github.com/FelisMC/Felis/blob/main/docs/troubleshooting.md)。
+71
View File
@@ -0,0 +1,71 @@
---
title: 认识 Felis
---
# 认识 Felis {#认识-felis}
基于 Kubernetes 的 Minecraft 服务器托管平台。
单条命令完成部署,自动管理服务器生命周期、备份与安全。
> [!CAUTION] 注意
> **此项目仍处于早期开发阶段,您不该在任何生产环境使用该项目。若产生任何问题,贵用户的使用行为与 FelisMC 团队无任何民事刑事法律关系。**<br>
## 特性 {#特性}
* **按需启停**:玩家连接代理时自动启动目标服务器,启动期间玩家进入等待队列,服务器就绪后自动传送;服务器空闲后自动停止,释放内存。
* **Web 控制面板**:在浏览器中查看服务器状态、在线玩家与资源用量。
* 控制台(RCON)、白名单、封禁、OP 与 LuckPerms 权限管理
* 文件管理:新建、删除、重命名、分片上传、下载,以及停服状态下解压 zip,可用于导入世界
* 计划任务:按星期与时区定时执行命令、重启、停止、启动或备份,执行前在游戏内向玩家发送提醒
* **备份与恢复**:默认启用,归档 PVC 及其路径由安装器生成。
* 手动备份:将服务器的完整数据卷(`/data`,含世界、配置、插件与模组)归档至集群内的归档存储,可回滚至任一备份点
* 每日恢复点:当天有玩家进入过的服务器在停止后自动生成恢复点,默认保留 7 个,保存期限 90 天;恢复点单独轮换,不影响手动备份
* 下载与导出:支持下载单个备份(附 sha256 校验)、删除单个备份及导出完整世界
* 异地副本(可选):备份在主机上加密后同步至 S3 兼容存储(AWS S3、Cloudflare R2、Backblaze B2、MinIO 等)
* 控制面数据库:存放账号、服务器归属、配额与存档索引的数据库每日自动备份,每次升级迁移前额外创建快照,故障时可通过 `felis db restore` 整库原子回滚;面板「维护与备份」页显示最近一次备份的时效(参见 [故障排查 §16](/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery))
* **运维诊断**
* `sudo felis status`:汇总显示节点、控制面、游戏代理、各服务器、备份及未解决的告警
* `sudo felis doctor`:执行全部健康检查,按模块列出问题及排查方向,执行过程中不发送邮件
* `sudo felis support-bundle`:生成已脱敏的诊断包,供提交问题时附带(参见 [故障排查 §0](/operations/troubleshooting#_0-first-look-felis-status-felis-doctor-felis-support-bundle))
* 看门狗:每 2 分钟执行一次巡检,异常持续时向平台所有者发送邮件告警,支持外部心跳监测
* **世界回收(可选)**:超过 15 天无人游玩的世界在备份后删除,以释放磁盘空间。安装时设置 `FELIS_WORLDS_HOST_PATH`(k3s 默认为 `/var/lib/rancher/k3s/storage`)即启用每日回收;未设置时不删除任何世界。过期备份的每日清理与此设置无关,始终执行。
* **多核心支持**:兼容 Paper、Fabric、Forge 与 NeoForge,统一经由 Velocity 代理接入。
* **模组包投稿**:玩家可上传模组包,经服主审批后自动构建并通过 Trivy 安全扫描;构建产物加入镜像白名单后,可直接选作服务器镜像。
* **安全**
* Passkey 登录:支持指纹、面容识别及硬件密钥等无密码认证方式
* 零信任访问:面板流量经 Cloudflare Access 保护,集群内部 API 不对公网开放
* **多机部署(实验性,默认关闭)**:由一台主控节点统一下发指令,其余节点仅运行游戏服务器,已停止的服务器可迁移至其他节点。该功能目前仅位于 main 分支,尚未完成三机验收(参见 [多机部署](/guide/distributed))。
## 阅读文档 {#阅读文档}
- [安装与部署](/guide/deployment)
- [运维手册](/operations/)
- [故障排查](/operations/troubleshooting)
- [多机部署](/guide/distributed)
- [贡献指南](/reference/contributing)
- [项目说明](/reference/readme-en)
## 致谢 {#致谢}
* [Kubernetes](https://kubernetes.io/):容器编排引擎
* [K3s](https://k3s.io/):轻量级 Kubernetes 发行版
* [Cloudflare Zero Trust](https://www.cloudflare.com/zero-trust/):零信任安全基础设施
* [PostgreSQL](https://www.postgresql.org/):数据持久化
* [React](https://react.dev/):前端用户界面框架
* [Vite](https://vitejs.dev/):前端构建工具
* [TailwindCSS](https://tailwindcss.com/):CSS 框架
* [Bubble Tea](https://github.com/charmbracelet/bubbletea):TUI 框架
* [Minecraft](https://www.minecraft.net/):本项目服务的游戏
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)。
+28
View File
@@ -0,0 +1,28 @@
---
title: 备份与恢复
---
# 备份与恢复 {#备份与恢复}
* **备份与恢复**:默认启用,归档 PVC 及其路径由安装器生成。
* 手动备份:将服务器的完整数据卷(`/data`,含世界、配置、插件与模组)归档至集群内的归档存储,可回滚至任一备份点
* 每日恢复点:当天有玩家进入过的服务器在停止后自动生成恢复点,默认保留 7 个,保存期限 90 天;恢复点单独轮换,不影响手动备份
* 下载与导出:支持下载单个备份(附 sha256 校验)、删除单个备份及导出完整世界
* 异地副本(可选):备份在主机上加密后同步至 S3 兼容存储(AWS S3、Cloudflare R2、Backblaze B2、MinIO 等)
* 控制面数据库:存放账号、服务器归属、配额与存档索引的数据库每日自动备份,每次升级迁移前额外创建快照,故障时可通过 `felis db restore` 整库原子回滚;面板「维护与备份」页显示最近一次备份的时效(参见 [故障排查 §16](/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery))
## 备份包含哪些数据 {#what-a-backup-contains}
备份会将服务器挂载到 `/data` 的整个数据卷打包,包括世界目录、`server.properties`、插件与模组、配置、jar、依赖库、日志和缓存,而不只是 `world/` 目录。
恢复时,数据卷内容会被归档替换,备份之后新增的文件会被清理,因此配置和插件的改动也会回滚。标准 Paper 服务器的体积主要来自依赖库和缓存:一个尚未积累世界数据的新实例约为 170MB。规划归档 PVC 容量时,需要计入这些数据。
## 详细操作 {#详细操作}
- [世界备份、下载、导出与每日恢复点](/operations/troubleshooting#_10-world-reaper-false-deletes-and-skipped-backups-spec-§18)
- [控制面数据库备份与灾难恢复](/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery)
- [灾难恢复与计划迁移](/operations/#_5-disaster-recovery)
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)、[docs/troubleshooting.md](https://github.com/FelisMC/Felis/blob/main/docs/troubleshooting.md)、[docs/operations.md](https://github.com/FelisMC/Felis/blob/main/docs/operations.md)。
+547
View File
@@ -0,0 +1,547 @@
---
title: 运维手册
---
# 运维手册 {#felis-operations-guide}
本文说明主机要求、资源规划、卸载及灾难恢复入口。故障诊断见[故障排查](/operations/troubleshooting),下文以 §N 引用其中章节。
验证标记与故障排查一致:**[VM-VERIFIED]** 表示在真实主机执行过;**[CI]** 表示每次推送 main 都运行端到端验证(`.github/workflows/e2e.yml`);**[GO-TESTED]** / **[SH-TESTED]** 表示由 `go test` 或 `deploy/` 下的 shell 测试覆盖;**[CODE-ONLY]** 表示仅描述代码行为,尚未完成端到端验证。
## 1. 支持的主机 {#_1-supported-hosts}
`deploy/bootstrap.sh` 默认单节点部署。可选的 A 主控与工作节点部署见[多机部署](/guide/distributed)。主机须有 systemd、root 权限和下列包管理器之一;安装器负责其余依赖:k3s、JRE、cloudflared,以及本机构建镜像时所需的 Docker,见下文「二进制与镜像的来源」。PostgreSQL 以 `felis-postgres` Deployment 在 k3s 内运行,使用发布版按摘要固定的官方镜像,数据位于主机 `/var/lib/felis/postgres`。
| 系统 | 包管理器 | 架构 | 状态 |
| --- | --- | --- | --- |
| CentOS Stream 9(启用 firewalld,SELinux enforcing) | dnf | aarch64 | **[VM-VERIFIED]** 从发布版附件全新安装、重复安装、从 v0.1.0 升级(将宿主机 PostgreSQL 13 数据迁入 felis-postgres)、卸载和重装 |
| Ubuntu 24.04 LTS | apt | x86_64 | **[CI]** 从推送提交的发布版附件全新安装及同提交重装;按 README 的单行命令安装最新发布版;从由其自身安装器及附件安装、向十二个表写入数据的最新发布版升级,并逐行确认数据未变;每周验证本机构建 |
| RHEL / Rocky / Alma 9、Fedora | dnf | x86_64、aarch64 | [CODE-ONLY] 与 CentOS Stream 使用同一路径 |
| Debian 12、其他 Ubuntu 版本 | apt | x86_64、aarch64 | [CODE-ONLY] |
| openSUSE Leap / Tumbleweed | zypper | x86_64、aarch64 | [CODE-ONLY] |
| Arch Linux | pacman | x86_64、aarch64 | [CODE-ONLY] |
全新安装使用以下固定版本。已安装的 k3s、cloudflared 默认保留原版本,见 §4。
| 组件 | 版本 | 固定位置 |
| --- | --- | --- |
| k3s | v1.36.4+k3s1 | `bootstrap.sh` 的 `FELIS_K3S_VERSION` |
| cloudflared | 2026.9.1 | `FELIS_CLOUDFLARED_VERSION`,各架构独立 sha256 |
| Temurin JRE(Velocity) | 25,固定补丁构建 | `FELIS_JRE_VERSION`,各架构独立 sha256 |
| Go(nano 构建) | 1.26.8 | `GO_PINNED_VERSION`,各架构独立 sha256 |
| Minecraft / Limbo / Paper / Velocity / LuckPerms | `deploy/game-stack.lock` | §15b |
| PostgreSQL | 18.6,官方 `postgres` 镜像按摘要固定 | `bootstrap.sh` 的 `POSTGRES_IMAGE`、`internal/platform` 的 `defaultPostgresImage` |
不支持 32 位主机;安装器没有可供其下载的 k3s、JRE 或 Go 构建。
安装器在修改主机前一次性检查并列出全部问题,若不满足要求则停止,不做修改 **[SH-TESTED]**:
- 架构、systemd 是否为 init,以及 k3s 所需的内存 cgroup 控制器。
- 内存低于 1.75 GiB 时拒绝安装(标称「2 GB」的 VPS 可通过);低于 3.5 GiB 时警告。
- 各写入文件系统的可用空间,共用文件系统时累加:裸机发布版安装约需 17 GiB,`FELIS_ARTIFACT_DIR` 安装约需 15 GiB,本机构建约需 23 GiB,重装约需 7 GiB。已有 Docker 缓存或复用 k3s 等数据目录按重装计算。预计占用超过 85% 时警告,因为 k3s 会开始删除镜像缓存。
- 游戏端口、面板 NodePort、k3s 的 6443/6444 与 10248–10259、镜像仓库回环端口 5000。被安装器自身代理或 k3s 占用的端口按重装处理,允许通过。
- 主机上的其他 Kubernetes(kubelet、RKE2、k0s、MicroK8s)或 k3s agent。
- 节点地址或路由网段是否落入 k3s 的 `10.42.0.0/16`、`10.43.0.0/16`,常见冲突是 Docker 网络。覆盖范围更大的路由,例如 `10.0.0.0/8` VPN,仅警告。
- 下载主机的 HTTPS:始终检查 GitHub 和 PaperMC 下载 API;本机构建时检查 Docker Hub;k3s 安装器需要添加 Rancher RPM 源时也检查它,完整清单见下文。完成 TLS 握手即视为可达,每个地址尝试三次、间隔两秒。发布版安装时 Docker Hub 不可达仅警告,因为只在附件不可用时需要;`FELIS_ARTIFACT_DIR` 模式不检查 Docker Hub。
检查误判时,可设置 `FELIS_PREFLIGHT=warn`,将相同问题降为警告并继续安装。
安装器会增加防火墙放行规则,**不会关闭防火墙** [SH-TESTED]。启用 firewalld 时放行面板 NodePort、游戏端口、6443,并将 k3s 的 Pod 和 Service 网段置于 trusted zone。启用 ufw 时(Ubuntu、Debian 常见,CI 安装也启用 **[CI]**),允许 `10.42.0.0/16`、`10.43.0.0/16`、面板 NodePort 和游戏端口,规则注释为 `felis-…`;6443 不对外开放,Pod 从自身网段访问 API server。两种防火墙中的 Felis-nano 端口仅向 `FELIS_NANO_PROXY_CIDR` 开放。卸载器移除这些规则,只有卸载 k3s 时才移除其网段规则。主机前方的其他防火墙也须放行相同流量;Pod 流量被丢弃时,首次部署会超时并报告 `control-plane rollout did not complete`。
主机在安装存续期间必须保持:
- **固定地址**:安装与最初的 IPv4 绑定,k3s 节点、网络策略、面板证书、默认 nip.io 域名均使用它。安装前设置静态地址或 DHCP 保留。租约地址会触发安装警告;地址丢失时看门狗报告 `host-address`,见 §13c。k3s 节点名在安装时固定,修改主机名不影响它。
- **时钟同步**:安装器开启 NTP,无其他实现时使用 chrony;看门狗报告持续未同步的时钟。允许出站 UDP 123;由其他机制校时的主机可设置 `FELIS_MANAGE_TIME_SYNC=0`。
安装器还启用持久化系统日志,默认限额 1G,可用 `FELIS_JOURNAL_MAX_USE` 修改,或 `FELIS_MANAGE_JOURNAL=0` 跳过。管理员 kubeconfig `/etc/rancher/k3s/k3s.yaml` 仅 root 可读,请运行 `sudo k3s kubectl`。
单节点仍是默认方式。可选[分布式模式](/guide/distributed)将唯一 API/operator 留在 A,在获批的 k3s agent 上运行游戏。世界使用所在节点 local-path 存储的 ReadWriteOnce PVC;移动必须停服并经 A 的归档服务显式迁移。目前没有自动故障切换或备用主控。A 重启会暂停控制操作,工作节点丢失时世界仍留在该节点。跨节点网络还需完成多机手册规定的三机验收。
### 二进制与镜像的来源 {#where-the-binary-and-the-images-come-from}
默认通道和设置控制台安装发布版时,从该版本附件获取 Felis 构建的全部产物,使用前逐一校验 `SHA256SUMS`:`felis` 二进制、控制平面、limbo、lobby、paper 镜像、按 `bootstrap.sh` 摘要固定的 registry 和 PostgreSQL 镜像,以及 `felis-velocity.jar`。镜像通过 `k3s ctr images import` 导入 containerd,再上传集群内仓库;主机无须 Docker、Gradle、Go,也不必为这些产物访问 Docker Hub。k3s 自身镜像在首次启动前从其 GitHub release 获取 `k3s-airgap-images-<arch>.tar.zst` 并校验其 sha256 清单。升级只下载含本机缺失镜像的 tar,暂存 `/var/lib/felis/artifacts`,仓库保存成功后删除。附件清单见 `deploy/build-release-artifacts.sh`。
来源选择已由 **[SH-TESTED]** 覆盖。CentOS Stream 9 aarch64 经 `FELIS_ARTIFACT_DIR` 的附件安装已 **[VM-VERIFIED]**:全新安装及从本机构建版升级都未拉取或构建镜像,重装也未重复导入或上传。发布版下载路径在附件发布前仅为 [SH-TESTED]。
默认选择最新发布版;`FELIS_RELEASE=<tag>` 可指定旧版,从该版本附件安装,并读取该标签的安装器。这也是升级失败后回退的方法,见 §16「回滚损坏数据库的升级」。
以下情况改为本机构建,安装 Docker,并在镜像入库后停止 Docker:
- 不是发布版来源:`FELIS_VERSION_BOOTSTRAP=dev`、固定 `FELIS_REF` 或 `FELIS_SKIP_FETCH`。
- `FELIS_GAME_STACK=latest`:login、lobby、paper 镜像本机构建,其余仍取自发布版。
- 没有 `SHA256SUMS`(附件机制之前的旧版,或尚未上传完成),或附件缺失、校验失败、格式错误。仅对应镜像回退构建;registry、PostgreSQL 镜像改从 Docker Hub 拉取,并输出对应警告。每个下载先重试三次;磁盘不足以构建时,在安装 Docker 前停止。相关消息见 §15c。
`FELIS_ARTIFACT_DIR=<绝对路径>` 从目录读取发布版的全部 `felis-*` 文件和 `SHA256SUMS`,也可使用 `deploy/build-release-artifacts.sh <version> <dir>` 生成的目录。不会下载或构建 Felis 自身产物,除非使用发布版不包含的 `FELIS_GAME_STACK=latest` 游戏镜像;附件缺失或校验失败会停止安装。不能同时使用另行指定源码来源的 `FELIS_REF`、`FELIS_SKIP_FETCH`。
其余主机软件仍需下载,须直接或经 `https_proxy` 提供出站 HTTPS。目前不支持完全离线安装 **[SH-TESTED]**:
| 地址 | 下载内容 |
| --- | --- |
| `github.com` 及附件重定向的 githubusercontent.com 主机 | k3s 及其镜像、cloudflared、Temurin JRE、ViaVersion、ViaBackwards、ViaRewind |
| `raw.githubusercontent.com` | k3s 尚未安装时所需的安装脚本 |
| `rpm.rancher.io` | Red Hat 或 SUSE 系启用 SELinux 的主机上,k3s 尚未安装时所需的 k3s-selinux;包括 CentOS Stream、RHEL、Rocky、Alma、Fedora、openSUSE Leap |
| `fill-data.papermc.io` | Velocity jar,除非通过 `FELIS_VELOCITY_FORK_JAR` 提供 |
| 发行版软件源 | 缺少的 CA 证书、OpenSSL、curl、tar 等基础包,以及与 k3s-selinux 配套的 container-selinux |
preflight 在任何修改前逐一探测上述地址,一次列出所有 `cannot reach … over HTTPS`;包管理器自行报告软件源问题。覆盖配置可能增加下载来源:`FELIS_JRE_VERSION` 使用 `api.adoptium.net`;非固定 `FELIS_VELOCITY_VERSION` 使用 `fill.papermc.io`;`FELIS_GAME_STACK=latest` 会从 Docker Hub、PaperMC、Limbo CI、LuckPerms 本机构建,preflight 会探测 Docker Hub。
```
# on a machine with access: the release's assets for the host's architecture
gh release download v1.4.0 --repo FelisMC/Felis --dir felis-v1.4.0 \
--pattern 'felis-*linux-amd64*' --pattern felis-velocity.jar --pattern SHA256SUMS
# on the host, after copying the directory over
sudo FELIS_ARTIFACT_DIR=/root/felis-v1.4.0 bash bootstrap.sh
```
`SHA256SUMS` 同时列出两种架构,可不复制另一架构的文件。
### felis-api 重启期间的行为 {#while-felis-api-restarts}
安装器更新 API、节点重启或 Pod 崩溃时,API 在新 Pod 就绪前不可用。参考虚拟机从 `kubectl rollout restart` 到 Available 约 12 秒。Deployment 为单副本、Recreate 策略,旧 Pod 先删除再启动新 Pod。不能同时运行两个 API:上传卷是 ReadWriteOnce,分片上传在进程内串行化;构建协调、恢复收尾、仓库清理、上传回收和审计保留均在进程内运行,没有选主,双副本会重复执行。
- 已在服内的玩家不受影响,游戏服务器继续运行。
- 离开登录网关或通过服务器地址进服的玩家,若最近十分钟内 API 确认过绑定,则允许加入;其他玩家收到登录验证暂不可用的提示。
- 登录网关为新登录最多重试 60 秒并提示正在重试,较短的重启只会延迟登录。
- 唤醒、停服、`/link`、面板需等待 API。
- 此期间 Velocity 重启时,使用 `/opt/felis/velocity/plugins/felis-link/last-servers.json` 缓存的服务器列表路由,直到每 15 秒一次的刷新成功。
参考虚拟机将 API 缩至 0 副本约八分钟的演练 [VM-VERIFIED]:
- 11 秒后代理记录 `server list refresh failed ... keeping current registrations`;五分钟时记录 `still failing: 22 failed attempts over 304 s`。
- 看门狗首次运行即发现 `deployment/felis-api` 严重异常;超过五分钟后的首次巡检(约第七分钟)触发告警。无 `[smtp]` 时仅写日志,见 `journalctl -u felis-watchdog`。
- 新 Pod Available 后代理记录 `server list refresh recovered after 32 failed attempts over 469 s`。
## 2. 资源规划 {#_2-sizing}
### 平台本身的资源用量 {#what-the-platform-itself-uses}
以下数据在空闲网络的验证主机测得:4 vCPU、5.5 GB 内存、6 GB swap、CentOS Stream 9 aarch64。使用 PSS,即共享页面按进程分摊,数据来自 `/proc/<pid>/smaps_rollup` **[VM-VERIFIED]**。
| 进程 | 内存(PSS) |
| --- | --- |
| k3s(API server、控制器、调度器、kubelet) | 约 370 MiB |
| k3s containerd 和 Pod shim | 约 170 MiB |
| CoreDNS、local-path 存储供应器 | 约 105 MiB |
| 空闲 Velocity(`-Xms16M -Xmx1G`,随玩家增加) | 约 175 MiB |
| felis-api、felis-operator、registry gate | 合计约 85 MiB |
| 镜像仓库 | 约 25 MiB |
| PostgreSQL(felis-postgres Pod) | 约 40 MiB,另有页面缓存 |
| **基础设施合计** | **约 1 GB** |
安装器在 `/etc/systemd/system/k3s.service.d/50-felis.conf` 为 k3s 及其 containerd 设置 `GOGC=50`,将 Go 的默认堆增长阈值减半。空闲 k3s 活跃堆约 150 MiB,原本会增长至两倍后回收;此设置节省约 70 MiB,代价约为一个核心的 2%。旧安装重跑安装器即可获得设置,k3s 会重启,Pod 继续运行。
登录服 Limbo(Pod 限制 512 MiB,用量约 0.16 GB)和大厅 Paper(限制 1 GiB,用量约 0.7–0.85 GB)另计。每个游戏服务器增加其所有者分配的内存;Pod 的 limit 等于 request,JVM 堆由它推导,见 §1a。每用户配额在面板「管理 → 配额」限制。
发布版安装不需要构建(§1)。本机构建的峰值来自 Docker 和 Gradle 容器;完成后停止 Docker,以及未被其他程序使用的 Docker containerd,释放内存。安装器安装的 Docker 不随开机启动。内存不足 2 GB 且无 swap 的主机会新增 2 GiB `/swapfile`。
### 配置建议 {#recommendations}
| 同时在线玩家 | 运行中的游戏服 | CPU | 内存 | `FELIS_VELOCITY_XMX` |
| --- | --- | --- | --- | --- |
| 不超过 20 | 1–2 个小服 | 2 vCPU | 4 GB + 2 GB swap | 1G(默认) |
| 不超过 100 | 3–5 个 | 4 vCPU | 8–16 GB | 1G |
| 不超过 300 | 5–10 个 | 8 vCPU | 16–32 GB | 2G |
| 300 以上 | 更多 | 8+ vCPU | 32 GB 以上 | 3G–4G |
玩家数是规划估算,并非实测容量。游戏服开销主要取决于视距、红石、模组及玩家行为。内存按基础设施约 1 GB、登录和大厅约 1 GB、预计同时运行的各服务器内存总和计算,再增加四分之一供页面缓存和 PostgreSQL。Velocity 每玩家开销较小;`journalctl -u felis-velocity` 出现长 GC 停顿或 `OutOfMemoryError` 时再增加堆。
`FELIS_VELOCITY_XMX` 默认 `1G`,至少 `256M`,格式为 `<n>M` 或 `<n>G`,每次重跑安装器读取。堆不超过 1G 时从 16M 起步,使用串行 GC 和仅 C1 编译器;插件活跃内存约 50M,回收只需毫秒,压缩与加密由 Velocity 原生库处理。超过 1G 时从 64M 起步并使用 G1,避免大堆全量串行回收阻塞全部玩家;周期回收在玩家离开后归还增长的内存。修改会重写 unit 并重启代理,断开所有在线玩家,应在空闲时操作 **[VM-VERIFIED]**:
```
curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo FELIS_VELOCITY_XMX=2G bash
```
### 磁盘 {#disk}
| 内容 | 位置 | 大小 |
| --- | --- | --- |
| 世界 | `/var/lib/rancher/k3s/storage` 下每服一个卷 | 随世界增长 |
| 世界归档 | `felis-backups` 卷,`FELIS_BACKUP_STORAGE` 默认请求 10Gi | 每个保留备份约为一个压缩世界 |
| 集群内镜像仓库 | `registry` 卷,默认请求 10Gi | 默认镜像 2–3 GB,自定义构建会增长,每日清理(§9) |
| k3s containerd 镜像 | `/var/lib/rancher/k3s/agent/containerd` | 6–9 GB |
| Docker 镜像与构建缓存 | 本机构建时的 `/var/lib/containerd` | 多次升级后 5–10 GB |
| 安装时的发布版附件 | `/var/lib/felis/artifacts` | 最多约 2 GB,镜像入库后删除 |
| 工具链与源码 | `/opt/felis` | 约 2.5 GB |
| 数据库 | `/var/lib/felis/postgres` | 几十 MB,主要是审计日志 |
| 数据库备份包 | `/var/lib/felis/db-backups` | 每份几 MB,保留 14 份每日备份 |
local-path 卷不强制请求容量(§9),所有卷共用根文件系统。至少准备 **40 GB**,世界和自定义镜像增多后建议 60 GB 以上。文件系统超过阈值时看门狗邮件告警,磁盘耗尽见 §13b。本机构建的主机可先启动 Docker,再用 `docker builder prune -af` 回收构建缓存;下次升级会重新构建。
### 扩容磁盘 {#growing-the-disk}
上述内容共用根文件系统。可保持服务运行,先在云服务商处扩大虚拟磁盘,再扩分区和文件系统:
```bash
sudo felis backup-now -yes # a mistyped partition number is how a resize loses a disk
lsblk -f # which disk and partition hold /, and whether LVM sits on it
sudo growpart /dev/vda 3 # cloud-utils-growpart (RHEL) / cloud-guest-utils (Debian, Ubuntu)
# LVM (the RHEL-family default):
sudo pvresize /dev/vda3
sudo lvextend -r -l +100%FREE /dev/<vg>/root # -r grows the filesystem with it
# no LVM:
sudo xfs_growfs / # xfs
sudo resize2fs /dev/vda3 # ext4
df -h /
```
`felis backup-now`(故障排查 §10)归档所有已停止世界;加 `-stop` 可包含运行中的世界。
### 将数据迁移到独立磁盘 [VM-VERIFIED] {#moving-the-data-to-its-own-disk-vm-verified}
主要数据都在 `/var/lib/rancher/k3s`:世界、世界归档、仓库和镜像。使用独立磁盘后,数据增长或世界存储耗尽不会占满根文件系统。较小的数据库及备份包(`/var/lib/felis`)仍放在根磁盘。迁移停机时间为复制时间加一至两分钟;演练复制 4.2 GB 用时 18 秒,k3s 在新磁盘启动后 14 秒 API 即响应 `/readyz`。
1. 接入磁盘并创建文件系统。以下使用整盘,须先用 `lsblk` 确认其为空:
```bash
sudo mkfs.xfs /dev/vdb
U=$(sudo blkid -s UUID -o value /dev/vdb)
```
2. 停服使世界保存,再归档全部世界。使用安装器同样的标记,让看门狗在一小时内不发送邮件或失败心跳:
```bash
sudo felis backup-now -yes -stop
sudo install -d -m 0755 /run/felis
echo $(( $(date +%s) + 3600 )) | sudo tee /run/felis/watchdog-quiet-until
```
3. 停止 k3s 并复制:
```bash
sudo systemctl stop k3s
sudo /usr/local/bin/k3s-killall.sh # the containers k3s leaves running, and their mounts
sudo mkdir -p /mnt/felis-data
sudo mount UUID=$U /mnt/felis-data
sudo rsync -aHAX --numeric-ids /var/lib/rancher/k3s/ /mnt/felis-data/
sudo umount /mnt/felis-data
```
`-X` 保留 k3s 设置的 SELinux 标签。不要执行 `restorecon`,否则会将 runc、CNI 二进制的 `container_runtime_exec_t` 重置为策略默认值。
4. 在原路径挂载,并让 k3s 依赖该挂载:
```bash
sudo mv /var/lib/rancher/k3s /var/lib/rancher/k3s.old
sudo mkdir /var/lib/rancher/k3s
echo "UUID=$U /var/lib/rancher/k3s xfs defaults,nofail 0 0" | sudo tee -a /etc/fstab
sudo mkdir -p /etc/systemd/system/k3s.service.d
printf '[Unit]\nRequiresMountsFor=/var/lib/rancher/k3s\n' | sudo tee /etc/systemd/system/k3s.service.d/data-disk.conf
sudo systemctl daemon-reload
sudo mount /var/lib/rancher/k3s
sudo systemctl start k3s
```
drop-in 保护数据:若 k3s 在空挂载点启动,会创建全新空集群。依赖设置会在磁盘无法挂载时以 `A dependency job for k3s.service failed` 拒绝启动,`nofail` 则允许主机正常启动,便于远程修复。演练移除磁盘后 k3s 保持 inactive、挂载点为空;重新接入后 `systemctl start k3s` 完成挂载并启动。
5. 确认 `sudo k3s kubectl -n felis get pods` 全部就绪,`findmnt /var/lib/rancher/k3s` 指向新磁盘。然后在面板启动服务器,执行 `sudo rm /run/felis/watchdog-quiet-until`。正常运行一天后,可用 `sudo rm -rf /var/lib/rancher/k3s.old` 释放根磁盘空间。
看门狗已通过 `-disk-paths` 将 `/var/lib/rancher/k3s` 作为独立文件系统监控,新磁盘的占用也会触发邮件通知。
## 3. 卸载 {#_3-uninstall}
`deploy/uninstall.sh` 移除安装器创建的内容。执行前打印移除清单并询问,`--yes` 跳过询问 **[SH-TESTED] [VM-VERIFIED]**:
```
curl -fsSL <raw-url>/deploy/uninstall.sh | sudo bash -s -- --yes # keep the data
curl -fsSL <raw-url>/deploy/uninstall.sh | sudo bash -s -- --purge # remove the data too
```
两种模式均移除 `felis-*` systemd unit、`cloudflared-felis.service`、Velocity 用户、`/opt/felis`、`/usr/local/bin/felis`、中断安装残留的 `/var/lib/felis/artifacts`、安装器放置的 cloudflared(二进制仍被其他 unit 使用时保留)、`felis_edge` nftables 表和旧宿主机数据库版本的 `felis_postgres` 表,以及安装器开放的 firewalld 端口和带 `felis-` 注释的 ufw 规则。
集群只有 Felis 命名空间时,通过 k3s 的 `k3s-uninstall.sh` 卸载 k3s;存在其他工作负载时,仅删除 `felis`、`minecraft`、`felis-build` 和 MinecraftServer CRD。`--keep-k3s`、`--remove-k3s` 可覆盖该选择。
| 内容 | 默认保留数据 | `--purge` |
| --- | --- | --- |
| 最终数据库备份包 | 先执行 `felis db backup -label manual`,失败则在删除前停止;`--no-backup` 可跳过 | 不创建 |
| 数据库 `/var/lib/felis/postgres` | 保留,卸载 k3s 前先正常停止 felis-postgres | 随 `/var/lib/felis` 删除 |
| 早期版本的宿主机 PostgreSQL | 保持原状;已迁入 k3s 时停用,保留旧 `felis` 副本 | 删除其 `felis` 数据库和角色,为此临时启动再停止服务器;恢复原 `listen_addresses`、`pg_hba.conf`。若该角色仍拥有其他数据库(如 PG 合约测试的 `felis_pgint`)或其他授权,则在删除前拒绝,并列出资源及需执行的 `ALTER DATABASE … OWNER TO postgres` |
| `/etc/felis`:机密、`felis.toml`、`offsite.env`、设置向导记录的邮件密码和上传存储密钥、隧道配置 | 保留,删除 `bootstrap.done` 及每次运行记录 | 连同隧道凭证删除 |
| `/var/lib/felis`:数据库和备份包 | 保留 | 删除 |
| 世界、归档、仓库、上传文件 | 移到 `/var/lib/felis/retained/k3s-storage-<stamp>/`;`--keep-k3s` 时将卷设为 `Retain` 并留在原处 | 删除 |
| Felis 镜像、Docker 构建缓存 | 保留 | 删除 |
数据库相关两行由 [SH-TESTED](`deploy/uninstall_test.sh`)覆盖;上面的虚拟机验证早于 felis-postgres。
两种模式都不会卸载 Docker、git、nftables、旧 PostgreSQL 服务端软件包或 swap 文件,因为其他程序可能使用它们。需要恢复裸机时可自行处理:
```
sudo swapoff /swapfile && sudo rm /swapfile && sudo sed -i '\|^/swapfile |d' /etc/fstab
sudo dnf remove docker-ce docker-ce-cli containerd.io postgresql-server # or apt/zypper/pacman
```
Cloudflare 配置独立于主机。最终卸载后,还需删除隧道(Zero Trust → Networks → Tunnels,或 `cloudflared tunnel delete <name>`)、面板域名的 DNS 记录及 Access 应用。
### 保留数据后重新安装 {#reinstall-on-top-of-kept-data}
保留数据模式留下重装所需内容。安装器复用 `/etc/felis/secrets.env`,数据库密码、转发和会话密钥不变;对已有数据库执行迁移,不会新建。旧宿主机数据库版本已 **[VM-VERIFIED]**;felis-postgres 从 `/var/lib/felis/postgres` 保留的集群启动为 [SH-TESTED]。
以下步骤在参考虚拟机的保留数据卸载后逐项执行过,恢复世界的 `level.dat` 校验和与保留副本一致 **[VM-VERIFIED]**。`kept` 指向卸载器移出的卷目录:
```
kept="$(ls -d /var/lib/felis/retained/k3s-storage-* | tail -n 1)"
store=/var/lib/rancher/k3s/storage
```
1. 正常安装(`curl ... | sudo bash`)。若原域名不是默认 `<ip>.nip.io`,使用相同根域名;保留的 `felis.host.toml` 会供安装器读取。
2. 执行 `sudo felis setup`,重建登录服和大厅。已有所有者时直接进入状态页,可退出。
3. 恢复镜像仓库和上传文件,否则自定义服务器镜像无法拉取:
```
sudo k3s kubectl -n felis scale deploy/registry deploy/felis-api --replicas=0
sudo k3s kubectl -n felis wait --for=delete pod -l app.kubernetes.io/component=registry --timeout=120s
sudo k3s kubectl -n felis wait --for=delete pod -l app.kubernetes.io/component=api --timeout=120s
sudo rsync -a --delete "$kept"/pvc-*_felis_registry/ "$(ls -d $store/pvc-*_felis_registry)"/
sudo rsync -a --delete "$kept"/pvc-*_felis_felis-uploads/ "$(ls -d $store/pvc-*_felis_felis-uploads)"/
sudo k3s kubectl -n felis scale deploy/registry deploy/felis-api --replicas=1
```
再运行一次安装器,将当前发布版镜像推送到保留仓库,覆盖旧版本。
4. 恢复游戏服务器。最终备份包保存全部 MinecraftServer;筛选器跳过步骤 2 已重建的 login 和 lobby:
```
b="$(ls -t /var/lib/felis/db-backups/felis-db-*-manual.tar | head -n 1)"
tar -xOf "$b" k8s/minecraftservers.json \
| sudo k3s kubectl apply -l '!felis.lolicon.best/system-role' -f -
```
5. 恢复各世界。服务器至少启动一次后才有卷,因此先在面板启动,再停服,将保留数据复制到新卷:
```
s=<server>
sudo rsync -a --delete "$kept"/pvc-*_minecraft_world-$s-0/ "$(ls -d $store/pvc-*_minecraft_world-$s-0)"/
```
然后启动服务器。大厅同理:用 `sudo k3s kubectl -n minecraft patch minecraftserver lobby --type=merge -p '{"spec":{"desiredState":"Stopped"}}'` 停止,复制 `world-lobby-0` 后修改回 `Running`。
6. 恢复归档,使面板恢复点可用。归档卷在首次备份时出现,先在面板备份任意服务器,再执行:
```
sudo rsync -a "$kept"/pvc-*_minecraft_felis-backups/ "$(ls -d $store/pvc-*_minecraft_felis-backups)"/
```
已配置异地存储桶时,可改用 `sudo felis offsite fetch-worlds` 获取归档,见 §16。
7. 所有服务器恢复后,删除 `/var/lib/felis/retained/`。
## 4. 升级 Felis 的配套组件 {#_4-upgrading-the-pieces-around-felis}
重跑安装器升级 Felis 本身(§15)。其他组件默认保留首次安装版本,例外如下:
| 组件 | 重跑行为 | 升级方式 |
| --- | --- | --- |
| Velocity、Limbo、Paper、LuckPerms | 跟随 `deploy/game-stack.lock` | 固定版本更新的发布版出现后重跑(§15b) |
| Temurin JRE | 更新至固定补丁构建 | 重跑 |
| k3s | 默认保留 | `FELIS_UPGRADE_DEPS=1` 使用固定发布版的安装脚本逐个 minor 升级;更大跨度会在修改前停止并提示中间版本,绝不降级 |
| cloudflared | 默认保留 | `FELIS_UPGRADE_DEPS=1` 用已校验 sha256 的固定版替换 `/usr/local/bin/cloudflared` 并重启 `cloudflared-felis`;发行版安装的仍由包管理器维护 |
| PostgreSQL | 跟随发布版固定镜像 | minor 随 Felis 发布版更新,重跑会重启 felis-postgres,API 暂停数秒;major 需导出恢复,见下文 |
| Docker、git、nftables | 发行版软件包 | 包管理器 |
```sh
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh \
| sudo FELIS_UPGRADE_DEPS=1 bash
```
`sudo felis update` 比较 Felis、Velocity、k3s、cloudflared、JRE、PostgreSQL 的已安装与最新版本;`--k3s`、`--cloudflared`、`--jre`、`--postgres` 可只查看一项。PostgreSQL 从 felis-postgres 容器读取,仅比较同 major 的 minor;minor 更新随 Felis 发布,major 过期时提示当前支持版本。
安装器创建 `felis-update-check.timer`,每天约 05:30 执行 `felis update --record`,错过时在开机补执行。结果写入 `platform_settings`,面板「管理 → 更新 → 组件版本」显示已安装和最新版本;可更新时给出 `sudo felis update --<component>`,该命令打印应用方法。Felis 不自动应用,实际更新通过重跑安装器完成。记录超过 26 小时未更新时卡片变红,说明定时器已停止:
```sh
systemctl list-timers felis-update-check.timer
journalctl -u felis-update-check -n 50 --no-pager
sudo felis update --record # record a fresh check now
```
### 升级旧版本安装 [VM-VERIFIED] {#bringing-an-older-install-up-to-date-vm-verified}
三部分保留创建时的状态,`felis setup` 或 `kubectl rollout restart` 无法更新:API Deployment(新环境变量如 `FELIS_SMTP_PASSWORD` 需重新渲染)、大厅镜像(早期未包含 LuckPerms,会使权限操作返回 `luckperms_missing`)、MinecraftServer spec(新增字段仍为空)。先更新镜像,再依序处理:
```sh
# 1. Rerun the installer: renders and applies the control-plane bundle, rebuilds and
# re-imports the login and lobby images, and recreates those two pods so they run
# the new images. The [smtp] relay the setup wizard wrote is carried forward.
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash
# 2. Fill the spec fields the system servers gained since (troubleshooting §12b), then
# RCON for user servers created before it was the default. -user-rcon waits on
# each server's image opening RCON; see §12b before running it.
sudo felis converge
sudo felis converge -user-rcon
```
检查各部分:
```sh
kubectl -n felis get deploy felis-api \
-o jsonpath='{.spec.template.spec.containers[0].env[*].name}' | tr ' ' '\n' | grep SMTP
kubectl -n minecraft exec lobby-0 -- ls /data/plugins | grep -i luckperms
kubectl -n minecraft get minecraftserver \
-o custom-columns=NAME:.metadata.name,RCON:.spec.rcon.enabled,IDLE:.spec.idle.autoStopEnabled
```
环境变量仅携带密码,仍需在 `felis setup` 的邮件设置中配置中继。用户服务器的新 RCON 配置在下次启动生效。
### PostgreSQL 大版本升级 [CODE-ONLY] {#postgresql-major-versions-code-only}
数据库集群位于 `/var/lib/felis/postgres/<major>/docker`。发布版改用新 major 时,安装器发现旧集群后会在修改前停止,以免新服务器在旁边创建空集群。应先在当前发布版创建备份包,再恢复到新 major 的空集群:
```sh
# On the release you run now:
b="$(sudo felis db backup -label pre-upgrade | sed -n 's/^felis db backup: wrote //p')"
sudo k3s kubectl -n felis scale deploy/felis-postgres --replicas=0
sudo mv /var/lib/felis/postgres/18 /var/lib/felis/postgres-18.old # the old major's cluster, for a way back
# Install the new release: it starts an empty cluster on the new major and creates the schema.
curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo bash
# Put the data back and bring its schema up to the new release.
sudo k3s kubectl -n felis scale deploy/felis-api deploy/felis-operator --replicas=0
sudo felis db restore -yes -no-safety-backup "$b"
sudo felis migrate up -config /etc/felis/felis.host.toml
sudo k3s kubectl -n felis scale deploy/felis-api deploy/felis-operator --replicas=1
```
新版本稳定运行一段时间后删除 `/var/lib/felis/postgres-18.old`。需要回退时,将 felis-postgres 缩至 0,把新 major 目录移出 `/var/lib/felis/postgres`,将 `postgres-18.old` 放回 `/var/lib/felis/postgres/18`,再重跑旧发布版安装器。
### 将数据库迁入 k3s [VM-VERIFIED] [CI] {#the-database-s-move-into-k3s-vm-verified-ci}
早期版本使用安装在宿主机上的 PostgreSQL。首次重跑包含 felis-postgres 的发布版会进行一次迁移:
1. 停止 API、operator、宿主机定时器;在宿主机 `pg_hba.conf` 顶部加入规则,拒绝除迁移连接以外对 `felis` 的访问,原文件保存在 `pg_hba.conf.pre-pg-move`。
2. 用 `felis db backup` 创建 `pre-pg-move` 备份包,`felis db restore` 恢复到 felis-postgres,并比较两个服务器每个表的行数。至此任何失败都会恢复 `pg_hba.conf` 和控制平面,继续使用未改变的宿主机数据库。
3. 停止并禁用宿主机 `postgresql` 服务,保留软件和旧数据,写入 `/var/lib/felis/postgres-moved`。若宿主机还运行其他数据库,则服务保持运行,但 `felis` 仅能经回环地址访问。
e2e 升级任务通过 `deploy/e2e_seed.sh` 写入用户、绑定、会话、审计、备份、构建等数据,升级后检查迁移后的 felis-postgres 是否保持全部值不变。最新发布版仍为 v0.1.0 时,该测试验证的就是此迁移。
之后主机配置指向 `127.0.0.1:15432`,并以 `deployment = "felis/felis-postgres"` 使 `felis db` 在 Pod 内运行 `pg_dump`、`psql`、`pg_restore`;Pod 使用 `felis-postgres.felis.svc:5432`。
回退到宿主机数据库,例如重装迁移前的发布版:
```sh
sudo k3s kubectl -n felis scale deploy/felis-api deploy/felis-operator deploy/felis-postgres --replicas=0
hba="$(sudo -u postgres psql -XtAc 'SHOW hba_file' 2>/dev/null || echo /var/lib/pgsql/data/pg_hba.conf)"
sudo cp -p "${hba}.pre-pg-move" "$hba"
sudo systemctl enable --now postgresql
sudo rm /var/lib/felis/postgres-moved
curl -fsSL <raw-url-of-that-release>/deploy/bootstrap.sh | sudo bash
```
`SHOW hba_file` 需要数据库运行。停止时的回退路径是 EL 默认路径,Debian/Ubuntu 使用 `/etc/postgresql/<major>/main/`。迁移后的写入只在 felis-postgres;需要保留时先执行 `sudo felis db backup`,再恢复到宿主机数据库。迁移稳定后,可用 `sudo systemctl start postgresql; sudo -u postgres dropdb felis; sudo -u postgres dropuser felis` 删除旧副本,或卸载服务端软件包。
### MinecraftServer CRD [VM-VERIFIED] {#the-minecraftserver-crd-vm-verified}
每次重跑均应用二进制内嵌 CRD(`felis bootstrap-assets crd`)。目前只提供并存储 `v1alpha1`,API server 拒绝 operator 无法处理的值:
| 字段 | 允许值 |
| --- | --- |
| `spec.rcon.port` | 未设置、`0` 或 `25575`;allow-rcon 策略只开放 25575,其他端口无法探测 |
| `spec.startup.timeoutSeconds`、`readinessTimeoutSeconds` | 0–86400 |
| `spec.startup.healthHTTPPort` | 0–65535 |
| `spec.lifecycle.terminationGracePeriodSeconds` | 0–3600 |
| `spec.idle.emptySecondsBeforeStop` | 0–604800,面板上限 86400 |
`0` 均表示 operator 默认值。旧对象的越界值可通过 CRD validation ratcheting 保留,直到有人编辑该字段。operator 将负值解释为默认,过大正值按原值执行;请用 `kubectl -n minecraft edit minecraftserver <name>` 手动修正。
**迁入 `v1beta1` 是计划,尚未实现。** 首次破坏性 spec 变更将通过新版本交付,每一步间隔一个发布版:
1. CRD 同时提供 `v1alpha1`、`v1beta1`,仍存储 `v1alpha1`。字段一致时 `conversion.strategy: None` 足够;改名或结构变化需要由 operator 提供转换 webhook。
2. 存储改为 `v1beta1`。安装器用 `kubectl get minecraftservers -A -o json | kubectl replace -f -` 重写对象,再将 CRD 的 `status.storedVersions` 设为 `["v1beta1"]`。
3. 后续发布版停止提供 `v1alpha1`。Felis 一次使用一个 Go 类型,operator 和 API 在存储切换的发布版同步切换。
### 使用旧式转发的后端 [VM-VERIFIED] {#legacy-forwarded-backends-vm-verified}
1.8 时代的后端经过 ViaVersion 降至协议 47 时,现代转发的登录插件消息会被丢弃。因此代理须在握手地址中按 BungeeCord 方式传递身份。只有 Felis-Legacy Velocity fork 支持按服务器设置。标记 CR 后,下次每 15 秒的服务器列表刷新会读取:
```sh
kubectl -n minecraft label minecraftserver <name> felis.lolicon.best/forwarding=legacy
kubectl -n minecraft label minecraftserver <name> felis.lolicon.best/forwarding- # back to modern
journalctl -u felis-velocity | grep 'legacy forwarding list'
```
安装器的 `FELIS_LEGACY_FORWARDING_SERVERS`(默认 `legacy18`)不受标签影响,始终保留在列表中。
| 代理 | 标签生效时间 |
| --- | --- |
| 含补丁 0004 的 fork(`build-velocity.sh` 默认分支) | 下次连接该服务器 |
| 只有 0003 的 fork(`--deployed`) | 下次 `systemctl restart felis-velocity` |
| 原版 Velocity | 不生效,日志警告并注明服务器 |
测试虚拟机的 0004 fork 在添加标签 12 秒后记录 `legacy forwarding list is now [legacy18,resolvecheck]`;移除后恢复 `[legacy18]`。fork 的 `FelisLegacyForwardingTest` 验证列表变化影响后续连接。
旧式转发没有密钥,后端信任所有到达游戏端口的身份。`felis-allow-game-from-velocity` 只允许代理和节点本身,但节点上的其他程序也能访问。
## 5. 灾难恢复 {#_5-disaster-recovery}
数据库备份包内容、原主机恢复、升级回退、从异地副本重建新主机均见 §16。部署时应:
- **配置异地副本**:`FELIS_OFFSITE_*`,见 §16「保存异地副本」。否则世界与归档、数据库与备份包都在同一磁盘,磁盘丢失即全部丢失。未配置时安装结束会提示 `NO OFF-SITE COPY`。
- **将异地加密密钥保存在主机之外**,例如密码管理器;存储桶仅持有加密对象。
- 没有存储桶时,也应在主机之外保存一份数据库备份包,内含重建所需的 `secrets.env`。
- 在备用虚拟机**演练重建**:执行 §16 新主机重建步骤,跳过第 8 步接管和第 11 步隧道;随后用邮件验证码登录、恢复一个世界并加入。`felis offsite status`、`felis db check` 在副本或最新每日备份过期时非零退出,可接入监控,也可依赖看门狗邮件。
### 计划迁移到另一台主机 {#moving-to-another-host-planned}
计划迁移采用 §16 的重建流程,但旧主机仍能提供完整最终副本。需要异地桶传送世界归档(§16 第 7 步)。从第 1 步开始停机,直到新主机提供服务。
1. **旧主机**停止全部可能改变世界的操作,发送最终副本:
```bash
sudo install -d -m 0755 /run/felis
echo $(( $(date +%s) + 4 * 3600 )) | sudo tee /run/felis/watchdog-quiet-until
sudo systemctl stop felis-velocity # no joins, so no server wakes
sudo felis backup-now -yes -stop # every world archived; the servers stay stopped
sudo k3s kubectl -n felis scale deploy/felis-operator --replicas=0 # nothing starts a server from here on
sudo felis db backup # a bundle that lists those archives
sudo systemctl start felis-offsite.service
sudo felis offsite status # again until nothing waits
```
顺序不能颠倒:新主机按恢复数据库的索引获取归档,因此数据库备份必须晚于最终世界归档。`backup-now` 需要 operator 停服,所以之后才能停止 operator。静默标记使看门狗在四小时内不因代理/operator 停止发邮件。
2. **新主机**从 §16「在新主机重建」第 1 步开始。`fetch-db latest` 取得旧主机刚发送的备份包;第 8 步 `felis offsite take-over` 将新主机设为桶写入者,旧主机此后不再复制。第 10、11 步迁移域名和隧道。
3. **检查新主机**:用邮件验证码登录、恢复世界并进服,确认 `sudo felis offsite status` 的 `last success` 较新且没有待机提示,再宣布迁移完成。
4. **退役旧主机**:它保存桶之外各世界的最终副本,先连同磁盘关机保留数日,并禁用服务,避免开机自动启动:
```bash
sudo systemctl disable k3s felis-velocity felis-watchdog.timer felis-offsite.timer \
felis-db-backup.timer felis-update-check.timer felis-build-tools.timer
sudo poweroff
```
之后卸载(§3)或清空。
各步骤分别有对应验证:`backup-now` 见 §10,重建见 §16;完整迁移顺序尚未作为一次整体迁移演练。
## 6. 更换根域名 [VM-VERIFIED] [GO-TESTED] [SH-TESTED] {#_6-changing-the-root-domain-vm-verified-go-tested-sh-tested}
根域名不仅写入安装器配置,还涉及面板证书 `/etc/felis/panel-tls.crt`、两个命名空间的 `felis-config` Secret、`felis-api-tls` Secret、代理 `felis-link.properties`、登录 MinecraftServer 的 `FELIS_ROOT_DOMAIN` / `FELIS_PANEL_HOSTNAME`、Cloudflare 隧道和 DNS。`felis domain set` 按此顺序更新主机内可处理的各处,再重启读取它们的服务;`felis domain check` 逐项报告。安装器保留已安装域名,带不同 `FELIS_ROOT_DOMAIN` 重跑时会停止并提示此命令。
```sh
sudo felis domain set new.example.net # the plan: every surface, what it moves to, what it costs
sudo felis domain set -yes new.example.net # do it
sudo felis domain check # one line per surface; exits 1 while any is behind
```
以下内容保留:
- `[auth]` 中手动设置的面板、管理控制台域名,即不是 `console.<root>`、`op.console.<root>` 的值。需要迁移时手动改 `/etc/felis/felis.host.toml`,再运行 `set`。
- 其他 `[auth]` 键(`access_jwt_aud`、`client_ip_header`)及两个配置文件的其他行。多行值、带引号或点号的键无法逐行修改时,命令拒绝并指出需修复内容。
- 管理员自备证书。安装器自签证书会按相同结构为新域名重签,旧证书/密钥保存为 `*.pre-domain-<time>`;其他颁发者的证书若不覆盖新域名,则在修改前停止。换成覆盖新域名的证书后重跑。
计划在 `-yes` 前打印以下影响:
- **DNS**:`<root>`、`console.<root>`、`op.console.<root>`、`*.<root>` 必须指向主机。通配符不覆盖三级域名 `op.console.<root>`,须单独建记录。`check` 会解析各域名并警告未生效项。
- **玩家**:服务器地址改为 `<name>.<新根域名>`,旧地址不再路由;代理重启断开全部在线玩家。迁移后的首次安装器重跑还会重启一次代理,因为其文件指纹早于迁移。
- **登录**:Cookie 属于旧主机名,所有人须重新登录。Passkey 绑定面板域名;计划显示失效数量,用户需用邮件验证码登录并注册新 Passkey。无 `[smtp]` 中继时无法收码,被锁在外的所有者可用 `sudo felis breakGlass` 恢复。
- **Cloudflare**:隧道 ingress 和 Access 应用仍使用旧域名,迁移后重跑 `sudo felis setup` 的 Cloudflare 步骤。`check` 会对比隧道与新域名。
- **远程代理**:另一主机上的 `felis-link.properties` 不在本机处理范围,`set` 会打印需更新的三个键。
`set` 可安全重复执行,只修改落后的部分;已处于目标域名时,会收敛 `check` 报告的差异。中断后也可重跑。
参考虚拟机从 `10.211.55.6.nip.io` 迁入 `10-211-55-6.nip.io` 用时 34 秒。30443 的证书、两个域名的 `/config.json`、代理日志 `Felis routing ready: rootDomain=`、登录 Pod 环境变量均更新。第二次 `set -yes` 无修改或重启;旧 `FELIS_ROOT_DOMAIN` 在首项检查即拒绝;完整重跑保留新域名且 `check` 全部通过;迁回也恢复所有位置 **[VM-VERIFIED]**。
若代理启动时间早于 `felis-link.properties` 最近修改时间,`check` 会报告落后。旧安装器每次重写该文件,因此从旧版升级后,文件域名已正确时也可能报告一次;`sudo systemctl restart felis-velocity` 可清除。现在内容相同时安装器不再重写文件。
---
来源:[docs/operations.md](https://github.com/FelisMC/Felis/blob/main/docs/operations.md)。
File diff suppressed because it is too large. Load diff
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because it is too large. Load diff
+34
View File
@@ -0,0 +1,34 @@
---
title: API 定义
---
# API 定义 {#api-定义}
[下载完整 OpenAPI 3.1 定义](/openapi.yaml)。
这是 Felis Minecraft 编排平台控制平面的 API 定义。同一个二进制提供内部和外部两套接口。内部接口使用按调用方分配的服务令牌,处理 Velocity 与后端回调,不经过零信任访问;外部接口使用 `felis_session` Cookie,面向用户和控制面板,配置了 Cloudflare Access 时由边缘层执行访问控制。管理员级外部操作还需要运维控制台主机上的工作人员会话。各操作的接口侧和权限等级见 `x-felis-face` / `x-felis-tier`。
以下行为适用于所有操作,定义中不再逐项重复:
* 每个响应都包含 `X-Request-Id`(请求传入的值格式合法时沿用)、`X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`、`Referrer-Policy: no-referrer` 和 `Content-Security-Policy: default-src 'none'`。请求经过 TLS 边缘层(`X-Forwarded-Proto: https`)时,还会添加 `Strict-Transport-Security`。
* 不存在的路径返回 `404 not_found`;路径存在但请求方法不匹配时,返回 `405 method_not_allowed`,并附带 `Allow` 响应头。
* 浏览器从其他站点发起的 POST/PUT/PATCH/DELETE 请求,会在认证之前返回 `403 cross_site`。判定依据是 `Sec-Fetch-Site` 为 `same-site` 或 `cross-site`,或 `Origin` 的主机与请求主机不同。不发送这两个头的插件、脚本等调用方不受影响。
* JSON 请求体超过 1 MiB 时返回 `413 too_large`。请求体必须持续到达:30 秒之后,平均传输速度不足 16 KiB/s 时会关闭连接。
* 控制台和构建日志的事件流为每行附加 `id:`(Unix 秒)。EventSource 在一小时内携带 `Last-Event-ID` 重连时,会从该秒继续,而非重新读取末尾历史日志。服务端每分钟重新检查调用方;会话或权限失效时,用 `event: revoked` 结束事件流。事件流也会在 30 分钟后或服务端关闭时断开,客户端随后重连。
## 定义与验证范围 {#定义与验证范围}
`felis-api` 用 OpenAPI 3.1 描述两套接口,对应规范 §7、§14、§28 #7。同一个二进制提供内部、外部两个 `http.Handler`。每个操作通过 `x-felis-face` 区分接口侧(它是数组,因为 `/healthz` 同时属于两侧),通过 `x-felis-tier` 区分零信任等级:`public` / `service` / `app` / `admin`。
使用字段之前,需要区分自动验证和人工维护的范围:
* `{method, path}` 到 `{x-felis-face 集合, x-felis-tier}` 的映射由机器检查。`internal/api/openapi_test.go` 解析定义,与 `internal/api/api.go` 中构造处理器的 `internalAPIRoutes` / `externalAPIRoutes` 路由表执行严格双向一致性校验。新增、删除路由,或修改接口侧、权限等级而未同步定义时,`go test ./...` 会失败。
* 设置锁定会话仍可使用哪些操作(`x-felis-setup-allowed`),也会对照路由表中的 `SetupAllowed` 标记检查。
* 命名响应模型会与处理器实际编码的 Go 结构逐字段比较,见 `internal/api/openapi_parity_test.go`。
* 处理器测试发出的每个请求,在测试包运行后都会与定义核对,见 `internal/api/openapi_contract_test.go`:操作或 `x-felis-common-responses` 必须列出实际返回的状态码;JSON 响应必须满足对应模型,且不能携带模型未声明的属性;返回 2xx 的 JSON 请求必须满足 `requestBody`。测试未覆盖的状态码和请求、响应体仍由人工维护。
部署域(`RootDomain`,规范 §2)不会写入定义。`example.test` 是占位符,遵循禁止硬编码域名的约束。
---
原文:[docs/openapi.yaml](https://github.com/FelisMC/Felis/blob/main/docs/openapi.yaml)。
+27
View File
@@ -0,0 +1,27 @@
---
title: 部署架构
---
# 部署架构 {#部署架构}
## 单机部署 {#单机部署}
`deploy/bootstrap.sh` 默认部署在单个节点。可选的 A 主控与工作节点部署见[多机部署](/guide/distributed)。主机需要 systemd、root 权限和[受支持的包管理器](/operations/#_1-supported-hosts);安装器负责安装 k3s、JRE、cloudflared,以及需要在主机上构建镜像时使用的 Docker。二进制与镜像的来源见[运维手册](/operations/#where-the-binary-and-the-images-come-from)。
PostgreSQL 作为 `felis-postgres` Deployment 运行在 k3s 内,使用发布版按摘要固定的官方镜像,数据存放在宿主机的 `/var/lib/felis/postgres`。
单机部署仍是默认方式。可选的[分布式模式](/guide/distributed)将唯一的 API 和 operator 留在 A,在获批的 k3s 工作节点上运行游戏。世界使用所在节点 local-path 存储上的 ReadWriteOnce PVC;迁移需要先停服,再通过 A 的归档服务显式执行。
目前没有自动故障切换或备用主控。A 重启期间,控制操作会暂停,直到其工作负载恢复;工作节点失联时,世界仍留在该节点。跨节点网络仍须完成多机部署手册规定的三机验收。
## 主控与工作节点 {#主控与工作节点}
分布式模式默认关闭。A 运行唯一 Felis API/operator、k3s server、PostgreSQL、Registry、归档服务和系统服;Velocity 继续使用 A 的 systemd 服务。B、C 等节点只运行 k3s-agent/containerd、游戏 Pod 和 A 创建的维护 Job。节点必须与 A 同架构、同 k3s 版本,宿主机由管理员信任并维护。
## 运行流程 {#运行流程}
[时序图](/reference/sequence-diagrams)保留主仓库的进服唤醒、认领事务与账号绑定流程。服务端实现与路由限制见[服务端插件](/reference/plugins)。
---
原文:[docs/operations.md](https://github.com/FelisMC/Felis/blob/main/docs/operations.md)、[docs/distributed.md](https://github.com/FelisMC/Felis/blob/main/docs/distributed.md)、[docs/sequence-diagrams.md](https://github.com/FelisMC/Felis/blob/main/docs/sequence-diagrams.md)、[plugins/README.md](https://github.com/FelisMC/Felis/blob/main/plugins/README.md)。
+280
View File
@@ -0,0 +1,280 @@
---
title: 贡献指南
---
# 贡献指南 {#felis-contributor-guide}
本文说明如何运行、测试和理解项目。修改应尽量小,并与当前代码保持一致。
## 项目结构 {#project-shape}
Felis 是基于 Kubernetes 的 Minecraft 服务器控制平面。仓库分为四个主要部分:
- `cmd/felis/`:单一 Go CLI 二进制,分发 `api`、`operator`、`migrate`、`reaper`、`restore`、`manifests`、`breakGlass` 等子命令。
- `internal/`:API 处理器、存储迁移、Kubernetes 清单渲染、operator 协调、构建、备份、恢复及相关业务逻辑。
- `panel/`:React/Vite Web 控制面板。
- `plugins/`:Velocity、Paper、Fabric、Forge、NeoForge 的 Minecraft 侧插件和模组。
代码按职责拆分。优先修改实际负责该行为的最小模块,避免增加宽泛抽象或重写周边代码。
## 本地开发 {#local-development}
大部分日常开发可在 macOS 或 Linux 上完成,不需要完整集群。完整产品依赖 Postgres 和 Kubernetes,单元测试与前端开发可以本地运行。
建议准备:
- 与 `go.mod` 一致的 Go 版本。
- `panel/` 所需的 Node.js 和 npm。
- 插件开发所需的 JDK/Gradle(可选)。
- 安装 Docker、k3s 和 Postgres 的干净 Linux 虚拟机或服务器,供集成测试使用(可选)。
检查工具版本:
```bash
go version
node --version
npm --version
java -version
```
## 后端命令 {#backend-commands}
在仓库根目录运行:
```bash
cd /path/to/Felis
```
运行全部 Go 测试:
```bash
go test ./...
```
运行指定包的测试:
```bash
go test ./internal/api
go test ./cmd/felis
```
隔离测试使用内存替身。业务存储 SQL 则单独针对真实 Postgres 验证,必须使用名称包含 `pgint` 的临时数据库。测试工具会删除并重建 schema,再重放内嵌迁移:
```bash
FELIS_TEST_PG_URL='postgres://felis:***@127.0.0.1:5432/felis_pgint?sslmode=disable' \
go test -tags pgint ./internal/pgint/ -v
```
修改 `internal/api/pgrepo.go`、`internal/submit`、`internal/build`、`internal/dbbackup` 中涉及 SQL 的代码后,应运行此测试。替身描述接口约定,这套测试负责发现替身与真实查询之间的偏差。`felis db backup` 和 `restore` 测试还需要与服务器大版本一致的 `pg_dump`、`pg_restore`、`psql`。服务器在容器中时,可像生产的 felis-postgres 一样在容器内执行:
```bash
FELIS_TEST_PG_EXEC='docker exec -i <container>' FELIS_TEST_PG_URL=... go test -tags pgint ./internal/pgint/
```
构建 CLI:
```bash
go build -o /tmp/felis-dev ./cmd/felis
/tmp/felis-dev help
```
在不连接集群的情况下渲染 Kubernetes 清单:
```bash
/tmp/felis-dev manifests \
--felis-image registry.felis.svc:5000/felis:dev \
--velocity-cidr 10.0.0.5/32
```
`felis api`、`felis operator`、`felis migrate up`、`felis reaper` 是实际运行命令,需要 Postgres 和/或 Kubernetes 配置,不适合作为快速本地迭代的起点。
## 前端命令 {#frontend-commands}
进入前端工作目录:
```bash
cd /path/to/Felis/panel
```
安装依赖:
```bash
npm ci
```
连接 `http://localhost:8080` 上的真实后端:
```bash
npm run dev
```
使用本地模拟 API:
```bash
npm run dev:mock
```
模拟开发服务器启动时会输出账号、绑定码和重置命令。没有运行 Go API 和集群时,可用它开发前端。
常用前端检查:
```bash
npm run typecheck
npm test
npm run build
```
## 模拟 API {#mock-api}
前端模拟 API 位于 `panel/dev/`,仅由 `npm run dev:mock` 加载。模拟逻辑不得进入生产代码或业务组件。
在前端目录运行:
```bash
cd /path/to/Felis/panel
npm run dev:mock
```
当前模拟账号:
| 用户名 | 密码 | 场景 |
| --- | --- | --- |
| `owner` | `devpassword` | 已绑定的管理员 |
| `user` | `devpassword` | 未绑定的普通用户 |
| `linked` | `devpassword` | 已绑定的普通用户 |
| `setup` | `devpassword` | 首次登录需要修改密码的管理员 |
模拟 Minecraft 绑定码:
```text
LINK1234
```
重置模拟状态:
```bash
curl -X POST http://127.0.0.1:5173/api/v1/__mock/reset
```
模拟规则:
- 模拟专用逻辑保留在 `panel/dev/`。
- `panel/src/` 不得导入模拟代码。
- 响应结构与 `panel/src/lib/types.ts` 和 Go API 处理器保持一致。
- 使用真实错误码,不能只覆盖成功情况。
- 不得将模拟数据呈现为生产实时数据。
## 完整集成环境 {#full-integration-environment}
在 Linux 主机的仓库根目录运行集成和部署命令:
```bash
cd /path/to/Felis
```
设置 TUI 面向干净的 Linux 主机,不适合普通 macOS 开发机:
```bash
sudo felis setup
```
常用覆盖配置:
```bash
export FELIS_REPO_URL=<your fork url>
export FELIS_REF=<your branch> # pins the build; overrides the channel below
export FELIS_IMAGE=felis:dev
export FELIS_ROOT_DOMAIN=<node-ip>.nip.io
```
安装器默认使用最新的**已发布 GitHub release**。开发分支需要显式选择;私有 fork 还需要用于查询发布版和克隆的令牌:
```bash
export FELIS_VERSION_BOOTSTRAP=dev # build main instead of the newest release
export FELIS_GITHUB_TOKEN=<token> # private forks only: read access to the fork
```
首次 `vX.Y.Z` 标签发布前,也需使用 `dev`;没有发布版时默认通道无法解析,安装器会停止并提示此选项。
发布版默认下载该标签由 CI 生成的二进制、镜像和 Velocity 插件,校验 `SHA256SUMS` 后导入;面板内嵌于同一二进制(`internal/panel`)。`dev` 克隆并编译源码。发布版附件缺失或校验失败时,仅对应组件回退到**同一标签**的本机构建,并输出警告;不会悄悄切换提交。设置 `FELIS_REF` 会强制源码路径。具体安装行为以[安装与部署](/guide/deployment)和[二进制与镜像来源](/operations/#where-the-binary-and-the-images-come-from)为准。
通道决定二进制里的版本戳(`felis version`),供 `felis update` 与上游比较:发布版使用标签,开发版使用 `<latest-tag>+g<short-sha>`;固定 `FELIS_REF` 跳过通道及标签查询,使用 `v0.0.0+g<short-sha>`。未设置版本戳的构建显示 `dev`,禁用更新报告;测试该路径时,应通过 `bootstrap.sh` 或 Dockerfile 的 `FELIS_VERSION` 构建参数设置版本,不要仅执行普通 `go build`。
设置流程包装主机初始化,随后在同一命令中完成所有者账号及可选 Cloudflare 边缘设置。调试安装器时,仍可直接运行 `deploy/bootstrap.sh` 做底层主机配置。
请使用虚拟机或可丢弃的 Linux 服务器作为集成、验收环境,普通编码和快速测试仍在本地完成。
## 插件开发 {#plugin-development}
插件说明见[服务端插件](/reference/plugins),各模块保持独立 Gradle 构建:
```bash
# cwd: repository root
cd /path/to/Felis
bash plugins/velocity/gradlew -p plugins/velocity build
bash plugins/paper/gradlew -p plugins/paper build
bash plugins/fabric/gradlew -p plugins/fabric build
bash plugins/forge/gradlew -p plugins/forge build
bash plugins/neoforge/gradlew -p plugins/neoforge build
```
- 所有模块都使用各自的 Gradle wrapper,版本和校验和由模块固定。
- Java 和 Minecraft 版本要求以[插件版本表](/reference/plugins#minecraft-versions)为准;不要把各模块的工具链要求混为一谈。
- 首次构建会下载和重映射 Minecraft 依赖,可能较慢。
## 前端状态 {#frontend-status}
面板仍处于早期开发阶段。目前已提供服务器列表与状态、启停和认领、控制台/RCON、文件管理、账号绑定、白名单/封禁/OP/LuckPerms、计划任务及备份恢复等功能。当前功能范围以[项目说明](/reference/readme-en#features)与具体后端接口为准;模拟 API 仅用于本地前端开发。
新增功能时如实反映后端状态。没有实际端点时使用明确的占位提示,不能用假数据冒充实时数据;更完整的交互深度及组件、浏览器测试仍需持续完善。
## 国际化说明 {#i18n-notes}
这里讨论的是 **Felis 控制面板**的国际化,文档站的中英文切换是独立功能。面板尚未建立完整 i18n 系统,用户可见字符串多数仍内联于 TSX 或辅助函数。
优先覆盖:
- 根据稳定 API 错误码映射的错误消息。
- 导航标签、页面标题与主要操作。
- 阶段、状态标签。
- 空白、加载、错误状态。
建议从以下结构开始:
```text
panel/src/i18n/
index.ts
en.ts
zh-CN.ts
```
先使用小型类型化词典。需要运行时切换语言、复数规则、外部翻译流程或更复杂本地化时,再引入 `i18next/react-i18next` 等库。
除非贡献者确有需要,否则不翻译代码注释、内部日志或模拟环境专用终端消息。
## 贡献规范 {#contribution-style}
沿用现有代码,修改范围小且便于审阅。
- 优先最小修改,避免重写。
- 新抽象应消除实际重复,或表达清晰的局部概念。
- 前端模拟代码不得进入业务组件。
- 后端测试靠近负责该行为的包。
- 保持公共 API 和持久化结构,除非修改明确要求更新约定。
- 不提交 `panel/dist/`、`node_modules/`、Gradle 构建目录或本地二进制等生成产物。
欢迎使用 AI 辅助,但贡献者负责最终结果。不能提交全部由 AI 生成、本人未审阅的 PR。低质量内容、大范围无视现有设计的重写、未验证修改,或作者无法解释的代码,不会被接受。
交付前执行最小且有意义的检查:
```bash
go test ./...
cd panel && npm run typecheck && npm test && npm run build
```
无法运行相关检查时,交付说明中须明确说明。
---
来源:[CONTRIBUTING.md](https://github.com/FelisMC/Felis/blob/main/CONTRIBUTING.md)。安装及功能状态另据主仓库 README 和插件文档同步。
+57
View File
@@ -0,0 +1,57 @@
---
title: 集成边界
---
# 集成边界 {#deferred-integration-seams}
Go 源码中的 `INTEGRATION-ONLY` 和 `KNOWN-LIMITATION` 是可搜索的标记。本文汇总各标记当前的含义,免去从 34 处注释重新推导系统尚未完成的部分。
这些标记代表四种不同状态:尚无实现、已经实现但无法仅凭本仓库验证实际 I/O 等,共用相同标记容易混淆。此外,标记可能在问题解决后仍未更新;首次整理本文时,就有两处将已交付的功能描述成未来工作。
本文沿用 `internal/updater/doc.go` 的做法,按验证边界归类。测试全部通过,并不等于所有真实集成都已完成。
## 与故障排查文档中同名标记的区别 {#the-marker-collides-with-a-different-vocabulary-in-troubleshooting-md}
`docs/troubleshooting.md:19` 对 `[INTEGRATION-ONLY]` 的定义不同:症状来自 kubelet、Kaniko 或真实握手,无法在仓库内复现。那里的十二处标记说明故障来源,不代表未完成工作,本文不收录。搜索 `*.md` 和 `*.go` 会同时找到两种标记,只有 Go 中的标记表示本文所述集成边界。
## 已声明但尚未实现 {#declared-nothing-implements-it}
- `internal/updates/seams.go:32`:`Notifier`。`internal/mail` 可通过 SMTP 发送 OTP,但没有适配此接口,也没有游戏内通知通道。`felis update` 有意传入 nil。通知由面板承担:主机上的 `felis-update-check.timer` 每日执行 `felis update --record`,把报告存入 `platform_settings.update_report`;面板「管理 → 更新 → 组件版本」显示报告及每个更新对应的命令。
- `internal/updates/seams.go:43`:`Applier`。尚无执行更新的实现。nil 不会静默跳过:`Run` 为每个计划应用的更新记录 `errNoApplier`,使误排程显式失败。
- `internal/updater/gatherer_integration.go:22`:`NewSysGatherer` 在集群内路径将两个当前版本查询接口留为 nil:读取控制平面 Deployment 镜像,以及检查 Velocity jar。主机路径已实现这两项,见下文「已实现」;此缺口仅影响使用集群客户端的调用方。
- `internal/api/handlers_updates.go`:维护窗口仅供参考,没有集群内执行器应用更新。`felis update` 读取窗口,显示当前时间的位置,并在窗口外应用前警告;执行器本身仍使用零窗口,任何路径都不能据此宣称更新正在应用。
- `internal/submit/blobstore.go`:**2026-09-22 已关闭**。上传 PVC 不能跨命名空间挂载,改为通过 API 传输:派生的上下文引用现在是由服务令牌保护的内部 URL `/api/v1/internal/submissions/{id}/context`。构建 Job 的 `context-fetch` initContainer 使用 `felis fetch-context` 流式下载,经过 zip-slip 检查解压至有大小限制的 emptyDir,Kaniko 使用 `--context=/context`。令牌通过与登录网关相同的 Secret 副本机制进入构建命名空间(bootstrap + `felis setup`),构建出站策略仅允许访问控制命名空间的内部端口。此流程同时适用于本地和 `s3://` 存储,构建沙箱 Pod 都不会获得文件系统视图或对象存储凭证。Kaniko、Trivy 及 Trivy 的两个数据库来自仓库的 `mirror/` 副本,由安装器和 `felis-build-tools.timer` 保持更新(`felis mirror-build-tools`,故障排查 §8e);可用 `[registry]` 配置覆盖。
## 已实现,实际 I/O 无法仅凭本仓库验证 {#built-only-its-i-o-is-unverifiable-from-this-repo}
代码已实现,并使用替身完成单元测试。欠缺的是可运行的主机、集群或真实上游账号,而非实现本身。
- `cmd/felis/tui_edge_apply.go:246,274,295`:`nft` 边缘防护、幂等清理及 cloudflared 调用。
- `internal/cfsetup/runner.go:18`、`internal/cfsetup/cfsetup.go:329`:真实 Cloudflare Tunnel 和 Access API 调用;`internal/cfsetup/cfsetup_test.go:11` 通过替身测试完整流程。
- `internal/api/console.go:39`、`internal/api/logstream.go:236,306`、`internal/fileedit/k8sjobs.go:45`:分别需要真实集群的 RCON、`pods/log` 持续读取或 Job。**2026-09-22/23 已实测(auditfix7–25)**:RCON 命令链(唤醒 → 探测 → `command` / 访问权限变更 / 停服)、日志 SSE 流和文件编辑 Job 均在演练集群完成端到端验证。
- `internal/api/handlers_access.go:170,490`:解析原版服务器和 LuckPerms 的真实命令输出。**2026-09-23 已实测**:玩家、白名单、封禁列表解析与真实 Paper 回复一致;未安装 LuckPerms 时按文档返回原始回复;四个反例的输入保护均有效。
## 已接受且暂不计划修复的限制 {#deliberately-accepted-not-scheduled-to-close}
这些是当前设计决策,不是待办事项。每项都说明何时需要重新评估。
- ~~`internal/api/pgrepo.go:281`:配额检查和 `ClaimServer` 是两个语句(审计 #4 的 TOCTOU),需真实 Postgres 才能关闭。~~ **已关闭**:检查移入 `ClaimServer`,在一个事务中执行咨询锁、复查和 UPDATE。pgint 的先失败后通过测试提供了真实 Postgres 验证。
- `internal/api/api.go:773`:`cooldownLimiter` 仅在单进程内生效。API 有 N 个副本时,调用方每窗口最多能取得 N 份 OTP 额度。单副本内的突发问题已解决,跨副本限制需要共享存储,超出单副本安装范围;扩容 API Deployment 前须重新评估。
- `internal/submit/submit.go:524`:每用户上传存储预算先读取已存字节,再写入。单副本通过每用户上传预留串行化;跨副本交错上传时,每个并行上传可能多出一个仍符合单文件限制的对象。待处理投稿上限已无此问题:`CreateSubmission` 在每投稿者咨询锁内计数和插入(pgint `TestSubmitPendingCapHoldsUnderConcurrency`)。扩容 API 前,与上面的冷却限制一起评估;可用同类事务中的上传预留行解决。
- `internal/submit/submit.go:436`、`internal/submit/submit_test.go:351`:CAS 之后 `Approve` 失败,记录状态与正常情况无法区分,因此返回注明正在运行构建的独立错误,避免盲目重试导致重复推送。其他操作顺序风险更大。
- `cmd/felis/tui_edge_apply.go:246` 的第二处标记:防护使用 nftables。在 firewalld 主机上,重载可能清空独立表,目前未处理 firewalld 原生协调。缺少 `nft` 可执行文件时显式失败,不会留下开放端口。
- `internal/api/handlers_account.go:169`:重新认领时「从头开始或继承」的选择,在 Java/Velocity 侧仅有代码验证;绑定状态端点只报告绑定完成,不暴露该选择。
## 标记之后已接入的功能 {#wired-since-the-marker-was-written}
- `internal/api/handlers_email_otp.go`、`internal/api/api.go`:SMTP 已于 2026-07-20 交付(`internal/mail`,在 `cmd/felis/api.go` 接入)。未配置 `[smtp]` 时 `Mailer` 为 nil,发送验证码的端点均返回 503 `mail_unavailable`,验证码绝不写入日志。因此「Felis 无法发送邮件」已过时。
- `internal/config/config.go:117`:原注释已过时,在两个存储后端交付后仍把上传传输描述成待集成。`LocalContextStore`、`S3ContextStore` 已由 `cmd/felis/api.go` 按配置基础地址的形式选择。新增本文时已修正;当时剩余的是上文所列 Kaniko 的读取路径。
- `internal/updater/doc.go:44`:原「剩余集成」列表将已存在的 `felis update` CLI 和集群外 Velocity jar 读取列为未完成(`cmd/felis/update.go`、`internal/updater/gatherer_host.go`),已在同次修改中纠正;另两个 nil 接口缺口仍存在,见上文。
## 代码之外记录的边界 {#recorded-outside-the-code}
- minecraft 命名空间通过 `felis-server-egress` 限制出站流量:允许 DNS 和公网,排除全部私有网段及节点自身公网地址。`felis-login-to-internal-api` 只开放游戏 Pod 必需的平台路径:login → felis-api:8081。游戏服务器以后需要访问其他集群内服务时,必须在相邻位置添加单独的允许策略(`internal/platform/netpol.go`)。
---
来源:[docs/deferred-seams.md](https://github.com/FelisMC/Felis/blob/main/docs/deferred-seams.md)。
+17
View File
@@ -0,0 +1,17 @@
---
title: 开源协议
---
# 开源协议 {#开源协议}
本项目采用 [AGPL-3.0-only](/LICENSE.txt) 许可证。
### 协议注意事项 {#协议注意事项}
1. **衍生作品须采用 AGPL**:分发本项目副本或基于本项目的衍生软件时,须以 AGPL-3.0 开源,并保留原作者的版权声明与许可声明。
2. **网络服务同样须提供源码**(AGPL 第 13 条):通过网络向他人提供经修改的 Felis 服务时,即使未分发任何二进制文件,也须向这些用户提供修改后的完整源码。这是 AGPL 与 GPL 唯一的实质区别;Felis 作为通过网络访问的托管平台,几乎所有部署场景都适用此条款。
3. **免责声明**:本项目按"原样"提供,作者不承担因使用本项目而产生的任何法律责任。
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)、[LICENSE](https://github.com/FelisMC/Felis/blob/main/LICENSE)。
+96
View File
@@ -0,0 +1,96 @@
---
title: 登录服镜像
---
# 登录服镜像 {#login-limbo-image-loohp-limbo-felis-limbo}
**登录服**是常驻的认证网关。在 `felis.toml` 的 `[velocity] login_image` 中指定镜像后,`felis setup` 会将它配置为系统服务(`DesiredState=Running`,不参与世界回收)。每个新连接首先到达这里;它也是唯一安全的回退目的地,后端停服或启动时均回退到这里,不能绕过认证。
> **仅代码验证。** Go CI 不构建此镜像。镜像会编译 `plugins/limbo` 插件,以及通过源码目录引用的共享绑定核心 `plugins/shared`,再与 LOOHP/Limbo 发布版一起打包。
## 插件的功能 {#what-the-plugin-does}
`felis-limbo` 负责就绪检查和游戏内登录流程。
### 就绪检查 {#readiness}
LOOHP/Limbo 没有 RCON,不能使用 Felis 通常的 RCON 就绪探测(规范 §5)。仅检查 TCP 会在套接字刚绑定时就误报就绪。插件提供 HTTP 就绪端点,只有服务器完成第一次 tick 后才返回 `200`。`login` MinecraftServer 设置 `spec.startup.healthHTTPPort: 8080`,Pod 的 HTTP readinessProbe,以及禁用 RCON 时 operator 的就绪判定,均使用这个信号。
- 端点:`:8080` 上的 `GET /healthz`,可用 `FELIS_HEALTH_PORT` 覆盖。
- 首次 tick 前返回 `503 starting`,之后返回 `200 ok`。
- 失败时拒绝放行:端点无法绑定时不会进入就绪状态,operator 将网关保持在 `Starting`,不会宣告未启动的网关可用。
### 游戏内登录(规范 §B3) {#in-game-login-spec-§b3}
到达登录服的玩家已由上游 Velocity 的 online-mode 验证 UUID,但尚未绑定 Web 账号。玩家加入后,插件在 tick 线程之外执行以下流程:
1. 检查用户名冲突**黑名单**,断开被禁止的抢占者 UUID;真正的 Mojang 玩家使用另一 UUID,不受影响。
2. 通过 felis-api 内部接口,为已验证的 UUID 生成一次性**绑定码**。
3. 打开一本包含 `console.<root_domain>` 可点击链接的**书**,并在聊天中发送绑定码;提示玩家在**系统浏览器**中完成操作,避免使用不支持 Passkey/WebAuthn 的微信、QQ 内置浏览器。Web 入口也会检查此限制,见 `internal/panel`。
4. **轮询** `link/status/{uuid}`,玩家在 Web 控制台兑换绑定码后,通过 `bungeecord:main` 上的 BungeeCord `Connect` 插件消息将其**传送**到大厅。
5. 命中黑名单、生成绑定码或网络传输失败、登录窗口超时,均**断开连接**;认证失败时不予放行。
配置通过部署时的输入提供,不编译进插件。首次运行会在插件数据目录生成 `felis-link.properties` 模板,环境变量优先于该文件:
| 环境变量 | 含义 | 默认值 |
| --- | --- | --- |
| `FELIS_API_BASE_URL` | felis-api **内部**接口基础 URL | 登录必需 |
| `FELIS_SERVICE_TOKEN` | 内部服务令牌(机密) | 登录必需 |
| `FELIS_ROOT_DOMAIN` | 部署域名,用于构造 `https://console.<zone>` | 登录必需 |
| `FELIS_LOBBY_SERVER` | 传送目标的 Velocity 服务器名 | `lobby` |
| `FELIS_LOGIN_TIMEOUT_SECONDS` | 登录窗口,限制为 30–3600 秒 | `600` |
| `FELIS_HEALTH_PORT` | 就绪端口 | `8080` |
| `FELIS_API_CONNECT_TIMEOUT_SECONDS` | felis-api 连接超时,1–120 秒 | `10` |
| `FELIS_API_REQUEST_TIMEOUT_SECONDS` | felis-api 请求超时,1–120 秒 | `10` |
缺少 API 配置**或**根域名时,登录流程保持**关闭**,插件仅执行就绪检查,与其他 Felis 插件的安全降级行为一致。因此,未配置的镜像仍可启动,但生产部署必须提供配置才能完成认证。传送还要求 Velocity 启用 BungeeCord 插件消息通道(代理的 `bungee-plugin-message-channel`)。
## 构建 {#build}
LOOHP/Limbo 没有官方镜像,也没有发布版 zip。它的 CI(`ci.loohpjames.com/job/Limbo`)每次构建分别发布 `target/Limbo-<ver>.jar` 和 `spawn.schem` 两个文件,镜像通过这两个 URL 组装。不包含 `server.properties`,Limbo 会在首次运行时生成默认配置:
```
docker build -f deploy/limbo/Dockerfile \
--build-arg LIMBO_JAR_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/target/Limbo-<ver>.jar \
--build-arg LIMBO_SCHEM_URL=https://ci.loohpjames.com/job/Limbo/<n>/artifact/spawn.schem \
--build-arg LIMBO_VERSION=<maven-api-version> \
-t felis-limbo:demo .
```
- `LIMBO_JAR_URL`(必需):服务器 jar,保存为 `Limbo.jar`。
- `LIMBO_SCHEM_URL`(可选):默认出生点结构,保存为 `spawn.schem`,并作为出生点世界加载。
- `LIMBO_VERSION`:插件编译时使用的 Limbo **Maven** API 版本(Gradle `-PlimboVersion`)。它与带 CI 构建后缀的 jar 文件名不同。例如,`Limbo-2026.0.2-ALPHA-26.2.jar` 对应 Maven 版本 `2026.0.2-ALPHA`;`-26.2` 这个 CI 后缀未发布到 Maven 仓库。
将镜像推送到集群内镜像仓库,并在配置中引用。在节点本机执行时,Docker 默认将 `127.0.0.1` 视为非安全仓库:
```
docker tag felis-limbo:demo 127.0.0.1:5000/felis/limbo:demo
docker push 127.0.0.1:5000/felis/limbo:demo
# felis.toml → [velocity] login_image = "registry.felis.svc:5000/felis/limbo:demo"
sudo felis setup
```
镜像仓库按主机名之后的路径区分仓库,因此从另一台机器经 `kubectl -n felis port-forward svc/registry 5000:5000` 推送也是等价的。镜像须存放在仓库中,不能仅导入 containerd,这样 kubelet 在镜像垃圾回收后才能重新拉取。
## 端口(自动处理) {#ports-handled-for-you}
入口脚本 `deploy/limbo/entrypoint.sh` 每次启动都将 Limbo 的 `server-port` 固定为 `FELIS_GAME_PORT`,默认 **25565**,与 operator 的 `GamePort` 一致。LOOHP/Limbo 原本默认使用 `30000`,但 Velocity 的 NetworkPolicy、Service 和探测均使用 operator 的同一端口常量,导致无法访问。此操作是幂等的,持久化世界卷里的其他 `server.properties` 设置保持不变。除非同时调整 operator,否则**不要**覆盖 `FELIS_GAME_PORT`。
脚本还将 `max-players` 固定为 `-1`(不设上限,也是 Limbo 默认值)。未绑定玩家最多在网关等待十分钟,停服时所有玩家会同时回退到此处;若卷中保留人数限制,玩家会被拒之门外。
## 配置(由部署者负责) {#configure-deployer-s-responsibility}
镜像不会猜测**玩家身份转发**设置,沿用发布版默认值。部署者须使 Limbo 的转发设置与集群外 Velocity 一致,并在代理上启用 BungeeCord 插件消息通道,确保登录网关的 `Connect` 消息能将玩家送到大厅。
登录所需的 `FELIS_API_BASE_URL`、`FELIS_ROOT_DOMAIN`、`FELIS_LOBBY_SERVER`、`FELIS_SERVICE_TOKEN`(见上文[游戏内登录](#in-game-login-spec-§b3))会自动接入,无须手动设置:
- 三个**非机密**变量由 `felis setup` 写入 `login` MinecraftServer 的 `spec.env`。`cmd/felis` 根据控制命名空间推导内部 API URL,根据 `felis.toml` 获取根域名。控制命名空间默认为 `felis`;改名后须手动同步内部 API URL。
- `FELIS_SERVICE_TOKEN` 是**机密**,不会写进 CRD。登录网关有独立的内部 API 令牌 `felis-limbo-token`,仅允许生成绑定码、轮询绑定状态和检查黑名单。安装器将它复制到 minecraft 命名空间,`felis setup` 会从控制命名空间刷新此副本。operator 根据保留名称 `login`,仅向登录 Pod 通过 `secretKeyRef` 注入 `FELIS_SERVICE_TOKEN`。`sudo felis rotate-token -yes limbo` 替换令牌并重启 Pod。令牌缺失时插件仅执行就绪检查,网关仍可启动,但暂不认证玩家。
- **Service**:登录 Pod 通过 `FELIS_API_BASE_URL` 访问控制命名空间里的 ClusterIP Service `felis-api-internal`,转发至 API Pod 的内部端口 `8081`。它与外部 NodePort `felis-api`(443)独立,避免将不经过零信任验证的内部接口发布到节点外部 IP。
- **NetworkPolicy**:minecraft 命名空间默认通过 `felis-server-egress` 限制出站访问,只允许 DNS 和公网,排除全部私有网段;普通游戏服务器无法访问内部 API。`felis-login-to-internal-api` 仅开放登录 Pod → felis-api:8081,要求同时匹配保留名称 `login` 和 setup 管理、operator 复制到 Pod 上的 `felis.lolicon.best/system-role=login` 标签。令牌注入也使用同一组条件,用户服务器不能仅通过取名来匹配。
`felis setup` 输出 Velocity 的网关和大厅配置,保证新连接首先进入 `login`。只有认证成功并由网关放行后,才能进入大厅或之前记住的用户后端。
---
来源:[deploy/limbo/README.md](https://github.com/FelisMC/Felis/blob/main/deploy/limbo/README.md)。
+61
View File
@@ -0,0 +1,61 @@
---
title: 大厅镜像
---
# 大厅镜像 {#lobby-image-paper-felis-paper}
**大厅**是常驻的中转服务器。在 `felis.toml` 的 `[velocity] lobby_image` 中指定镜像后,`felis setup` 会将它配置为系统服务(`DesiredState=Running`,不参与世界回收)。
> **仅代码验证。** Go CI 不构建此镜像。镜像会编译 `plugins/paper` 中的 `/menu` 功能,并将插件安装到 Paper 服务器。
## 拓扑与必须遵守的约束 {#topology-the-one-invariant}
```
connect → login (limbo auth gate) → lobby (/menu hub) → target backend
```
只有登录网关完成认证并放行的玩家才能进入大厅。大厅不能用作回退目的地或等待区,否则新连接会绕过认证。Felis 在每一层都遵守此约束:
- 登录系统服务**没有**回退目标;网关不可用时拒绝连接。
- 大厅和所有用户服务器均回退到 **login**,不会回退到大厅。
- `buildSystemServer` 拒绝创建回退目标为 `lobby` 的服务。
- `felis setup` 输出集群外 Velocity 的接线配置:默认落点和等待区均指向 `login`。
## 构建 {#build}
```
docker build -f deploy/lobby/Dockerfile \
--build-arg PAPER_JAR_URL=https://<mirror>/paper-1.21.x-<build>.jar \
--build-arg PAPER_JAR_SHA256=<sha256 of that jar> \
-t felis-lobby:demo .
# Publish into the cluster's registry (on the node; docker treats 127.0.0.1 as
# insecure by default — or through a `kubectl -n felis port-forward svc/registry
# 5000:5000`, which is equivalent: only the path after the host matters).
docker tag felis-lobby:demo 127.0.0.1:5000/felis/lobby:demo
docker push 127.0.0.1:5000/felis/lobby:demo
# felis.toml → [velocity] lobby_image = "registry.felis.svc:5000/felis/lobby:demo"
sudo felis setup
```
## 配置(由部署者负责) {#configure-deployer-s-responsibility}
- 游戏端口必须为 `25565`,与 CRD 的 `GamePort` 一致。
- `online-mode` 和玩家身份转发设置须与集群外 Velocity 代理一致。
- 大厅仅使用 `felis:control` 插件消息通道,按设计不持有 felis-api 令牌(规范 §12)。
## 大厅允许哪些操作 {#what-the-lobby-allows}
felis-paper 的 `LobbyGuard` 使大厅成为不会受到破坏、玩家也不会受伤的中转区:
- 所有世界设为和平模式,关闭自然生物生成、PvP、生物破坏和 TNT;时间固定为正午,天气晴朗,死亡保留物品。
- 玩家不受伤害,也不会饥饿;落入虚空时传送回出生点。
- 没有 `felis.lobby.build` 权限的玩家以冒险模式在出生点加入,不能破坏或放置方块、使用桶、踩坏耕地、点火,也不能伤害生物、物品展示框、画、盔甲架或载具。按钮、门、压力板和容器仍可使用。
- 每次加入都会收到一条可点击执行 `/menu` 的聊天消息。
`felis.lobby.build` 默认授予 OP。允许管理员建造大厅时,可以在大厅控制台用 LuckPerms 授权(`lp user <name> permission set felis.lobby.build true`),或将其设为 OP。
入口脚本每次启动都将 `max-players` 固定为 `200`,覆盖 Paper 的默认值 `20`:所有已认证玩家都经过大厅,服务器停服时其玩家也会同时来到这里。
---
来源:[deploy/lobby/README.md](https://github.com/FelisMC/Felis/blob/main/deploy/lobby/README.md)。
+219
View File
@@ -0,0 +1,219 @@
---
title: 服务端插件
---
# 服务端插件 {#felis-server-side-plugins}
这里收录 Felis 的集群内插件与边缘插件。Velocity 代理和三种加载器模组实现 §10 账号绑定流程的游戏内第一步:已在线的玩家(UUID 已由 Mojang 验证)执行 `/link`,插件请求 felis-api 为该 UUID 生成一次性绑定码,并显示在聊天中。玩家随后在 Web 控制台的**账号**页面输入绑定码,完成第二步,将 Minecraft 身份绑定到当前登录的账号。Web 端已实现。三种加载器模组仅用于开启 online-mode 的独立服务器,限制见下方警告。
**limbo** 模块无需玩家执行命令,就会调用同一 felis-api 接口。它作为登录网关,在未绑定的玩家进服时生成绑定码,并让玩家等待,直到完成兑换。**paper** 大厅模块不提供这两种绑定方式,详见下文。
**Velocity** 模块还实现了 §11 域名自动启动路由:识别各服务器子域名、动态注册后端、唤醒休眠目标,并让玩家等待至服务器就绪。它是完整的代理插件;功能与限制见下方 [Velocity 路由](#velocity-routing-§11)。Fabric / Forge / NeoForge 模组仅提供 `/link`。
**Paper** 模块提供 §12 的大厅界面。它**不提供** `/link`,也**不持有** felis-api 令牌,只绘制 `/menu`(以及 `/server`)箱子界面,并通过 `felis:control` 插件消息通道与 Velocity 通信。只有 Velocity 直接访问 felis-api。详见[大厅菜单](#lobby-menu-§12)。
| 模块 | 平台 | 编译依赖 | Jar | 由安装器构建 |
| --- | --- | --- | --- | --- |
| `velocity/` | Velocity 代理插件 | velocity-api 3.5.1(Java 21 字节码) | `felis-velocity-0.1.0.jar` | 是 |
| `limbo/` | LOOHP/Limbo 插件(登录服) | Limbo API 2026.0.3-ALPHA(Java 17 字节码) | `felis-limbo-0.1.0.jar` | 是 |
| `paper/` | Paper 服务器插件(大厅) | paper-api 26.3.build.40-alpha | `felis-paper-0.1.0.jar` | 是 |
| `fabric/` | Fabric 服务器模组 | MC 1.20.1 / fabric-loader 0.16.5 / fabric-api 0.92.2+1.20.1 | `felis-fabric-0.1.0.jar` | 否 |
| `forge/` | Forge 服务器模组 | MC 1.20.1 / Forge 47.3.0 | `felis-forge-0.1.0.jar` | 否 |
| `neoforge/` | NeoForge 服务器模组 | MC 1.20.4 / NeoForge 20.4.251 | `felis-neoforge-0.1.0.jar` | 否 |
| `shared/` | 不独立构建 | — | 源码编入各模块 | 仅源码 |
安装器使用的三种版本,与 `deploy/game-stack.lock` 中的固定构建一致:Velocity 的 `VELOCITY_VERSION`、Limbo 的 `LIMBO_VERSION` 和 Paper 的 `PAPER_JAR_URL`。编译依赖与锁文件不一致时,`go test .` 会失败;见[依赖校验](#dependency-verification)。
### Minecraft 版本 {#minecraft-versions}
| 模块 | 运行位置 | Minecraft |
| --- | --- | --- |
| `velocity` | Felis 代理(Velocity 3.5.1) | 代理接受的客户端版本:原生支持 26.3,旧版通过安装器部署的 ViaVersion 组件接入 |
| `limbo` | Felis 登录网关(Limbo) | 仅 26.3;Limbo 只支持锁文件 `MC_VERSION` 指定的单一协议 |
| `paper` | Felis 大厅(Paper 26.3) | 26.3,即锁文件的 `MC_VERSION` |
| `fabric` | 独立 Fabric 服务器 | 1.20.1(`fabric.mod.json` 声明 `~1.20.1`) |
| `forge` | 独立 Forge 服务器 | 1.20.1(`mods.toml` 声明 `[1.20.1,1.20.2)`) |
| `neoforge` | 独立 NeoForge 服务器 | 1.20.4(`mods.toml` 声明 `[1.20.4,1.20.5)`) |
Felis 网络本身运行 Minecraft 26.3。加载器模组面向较早的 1.20.x 模组生态,只用于网络之外的独立服务器;它们无法加载到 26.x 服务器,Felis 安装也不会加载它们。
“由安装器构建”指 `deploy/bootstrap.sh` 生成的产物,同一组源码也由 `bootstrap_asset.go` 嵌入 felis 二进制,供没有源码检出的 TUI 安装流程使用。**三种加载器模组不在其中**:安装完成后,不会出现 `felis-fabric` / `felis-forge` / `felis-neoforge` jar。它们需要从当前源码按[构建](#building)中的命令编译,并手动部署。账号绑定流程已实现,但安装器不负责部署这些模组。
> **加载器模组仅用于独立服务器。** Felis 网络中,代理的 `/link` 已覆盖所有后端,并优先于后端同名命令,因此用户游戏服务器无需安装模组。模组只在服务器设置 `online-mode=true` 时响应 `/link`;offline-mode 服务器可能位于代理之后(使用代理的 `/link`),也可能是未认证服务器,其 UUID 由客户端自行声明。
>
> **模组持有的令牌是真实凭据。** 它可以为任意指定 UUID 生成绑定码。因此,能读取服务器文件的人,包括运维人员、服务器上的插件或模组、备份副本的持有人,都能将尚未绑定的玩家 Minecraft 账号绑定到自己的 Web 账号。只在你信任运维人员的服务器上安装模组,并使用 `limbo` 令牌;它只开放 link-code、link-status 和 blacklist 路由。`velocity` 令牌还可以批准管理员登录,为任意玩家唤醒或认领服务器,应保留在代理主机上。`sudo felis rotate-token limbo` 用于替换泄露令牌:登录网关会重启并使用新值;模组的 `felis-link.properties` 需要手动更新,模组会在下一次调用时读取,无需重启。
## 各调用路径信任的身份 {#whose-identity-each-path-trusts}
只有 **Velocity 路径**能防止身份伪造。代理运行 `online-mode=true`(由安装器写入),Velocity 会向 Mojang 验证每次登录,下游均使用该登录身份:
- 代理自身的 `/link`、`/felis` 和 `/invite` 使用已验证连接的 UUID。
- 大厅的 `felis:control` 消息按其实际后端连接确定玩家身份,大厅填写的 `player` 字段会被忽略;见[大厅菜单](#lobby-menu-§12)。
- 登录网关和大厅在代理后运行 offline-mode,只接受携带 Velocity modern-forwarding 签名(共享转发密钥)的登录,因此绕过代理的客户端不能自行声明 UUID。
代理以 `online-mode=false` 启动时会拒绝路由,记录错误并关闭路由;其 `/link` 只能看到从名称生成的离线 UUID,与 Mojang 账号 UUID 不一致。
**加载器模组**不在这一保护范围内。模组信任自身服务器返回的 UUID(`getUUID()`),可信程度与服务器相同。服务器未开启 `online-mode=true` 时,模组拒绝 `/link`;服务器运维人员持有可为任意 UUID 生成绑定码的令牌,见上方警告。
## 架构 {#architecture}
各平台都是**独立的** Gradle 构建,分别有自己的 `settings.gradle`。加载器 Gradle 插件对 Gradle 版本的要求存在冲突,因此没有混用这些插件的根项目。平台无关的绑定核心位于 `shared/src/main/java`,由各模块通过以下配置引入:
```groovy
sourceSets { main { java { srcDir '../shared/src/main/java' } } }
```
核心包(`best.lolicon.felis.link`)**没有第三方依赖**,使用 JDK 的 `java.net.http.HttpClient` 和小型手写 JSON 解析器。因此无需合并依赖,每个 jar 都可独立使用。
- `LinkClient`:以 `Authorization: Bearer <service-token>` 和请求体 `{"mc_uuid":"<uuid>"}` 调用 `POST {apiBaseUrl}/api/v1/internal/account/link/code`;成功返回 `201 → {code, expires_at}`,否则使用 `{error:{code,message}}` 错误封装。
- `LinkConfigLoader`:读取 `FELIS_API_BASE_URL` / `FELIS_SERVICE_TOKEN`(环境变量优先),或首次运行时生成的带注释模板 `felis-link.properties`。**API 地址和服务令牌属于部署输入,不会编译进代码。** 两个可选配置限制每次调用,接受 1–120 的整数秒数,默认 10 秒:`connect-timeout-seconds` / `FELIS_API_CONNECT_TIMEOUT_SECONDS` 限制连接建立时间;`request-timeout-seconds` / `FELIS_API_REQUEST_TIMEOUT_SECONDS` 限制整个请求。其他值会导致配置加载失败,并指出对应键名。
- `FelisApiClient`:路由客户端。GET 因连接中断或 502/503/504 失败时,会在 100–400 毫秒的随机延迟后重试一次。GET 超时以及所有 POST(唤醒、认领、join-event、批准)都不会重试。
线程处理:命令在服务器线程执行,HTTP 请求交给单线程守护执行器,响应再切回服务器线程,缓慢的 felis-api 不会阻塞 tick 循环。配置缺失时,插件会加载,但不注册 `/link`,服务器仍正常运行。
三种模组均使用 **Mojang 官方映射**,因此 Fabric / Forge / NeoForge 的 MC 类名、方法名相同,命令处理器一致;只有 `@Mod`、事件总线和配置目录的适配代码随加载器不同。
## Velocity 路由(§11) {#velocity-routing-§11}
Velocity 位于面向玩家的集群外边缘,域名自动启动路由也在这里实现。除 `/link` 外,Velocity 插件还按子域名识别 Felis 服务器,将后端注册到 Velocity 的动态服务器注册表,并在每次进服时决定直接进入、唤醒休眠服务器后等待,或提示稍后重连。它通过 felis-api 的**内部**接口(服务令牌认证)执行 §9 唤醒和 §11 路由,并接收 §12 大厅菜单的 `felis:control` 插件消息,将消息转换为相同的唤醒、认领和状态调用。玩家身份取自连接,而不信任大厅自行声明的身份。见[大厅菜单](#lobby-menu-§12)。
路由受两个前置条件限制,**任何一个不满足都会安全关闭路由**,但 `/link` 仍可使用:
- **在线模式**:`velocity.toml` 中设置 `online-mode=true`。autostartPolicy 和白名单检查信任 Mojang 验证过的 UUID;离线模式下,插件拒绝根据可伪造的身份路由,并记录错误。
- **根域名**:部署域,例如 `mc.example.net`。这是部署域进入代理的唯一位置,**不会编译进代码**。未配置时,基于主机名的路由没有匹配依据,因此保持关闭。
启用路由后的行为:
| 功能 | 行为 |
| --- | --- |
| 后端注册表 | 每 15 秒轮询 `GET /api/v1/servers`,并同步 Velocity 动态注册表。轮询失败时**保留已有注册**,控制平面短暂不可用不会注销正在运行的后端。API 返回后端 Service 可由宿主机路由的 ClusterIP,避免宿主机上的代理依赖集群 DNS 名称。 |
| 进服(`PlayerChooseInitialServerEvent`) | 解析 `subdomain.<root-domain>` 并记住目标,但所有新连接仍首先进入 `login`。登录网关在认证后请求传送到大厅时,Velocity 重新检查绑定状态:记住的目标已就绪就直接进入;目标休眠时,从大厅发起唤醒并加入队列。 |
| 等待队列 | 每 2 秒执行一次定时处理,按不同目标服务器各查询一次状态。服务器正在启动,或处于 operator 重启退避期间的 Failed 时,玩家继续等待,每分钟收到进度提示。完成传送、玩家离线、放弃启动(`startGaveUp`)、服务器停止、连续 120 秒未收到 felis-api 响应,或达到一小时上限时,退出等待。 |
| 唤醒检查 | 按玩家 online-mode UUID 调用 `POST /api/v1/internal/servers/{name}/wake`。**403** 表示策略拒绝,告知玩家并结束;**429** 表示唤醒已在进行,继续等待。 |
| 服务器列表查询(`ProxyPingEvent`) | 从缓存的生命周期视图返回对应阶段的 MOTD(在线 / 启动中 / 休眠),**只读,不唤醒任何服务器**。通过后台查询已就绪后端来镜像其原生 MOTD,属于后续功能。 |
| 进服上报(`ServerConnectedEvent`) | 通过 `POST …/join-event` 上报实际进入 Felis 后端的事件,供回收器更新活跃时间,并自动将玩家加入服务器白名单。 |
| `/felis`、`/felis list` | 显示运维状态:online-mode、root-domain、大厅,以及已知服务器集合的 phase/ready 状态。 |
| `/felis lobby`、`/felis go <lobby>` | 将玩家从任意后端送回大厅,不唤醒服务器;已排队的等待仍会在目标就绪后传送玩家。命令放在 `/felis` 下,避免代理覆盖用户服务器自身的 `/lobby` 或 `/hub`。 |
| `/server` | 移除 Velocity 内置 `/server`,使命令传给后端:在大厅打开服务器菜单,在其他后端使用其自己的 `/server`。内置命令会向所有玩家列出登录网关、大厅和所有运行中的服务器。其他代理插件注册的 `/server` 保留。关闭路由时保留内置命令,因为此时它是切服入口。 |
Velocity 专用配置键也从 `felis-link.properties` 或环境变量读取,与 `/link` 相同,环境变量优先:
| 配置键 | 环境变量 | 含义 |
| --- | --- | --- |
| `root-domain` | `FELIS_ROOT_DOMAIN` | 路由域,例如 `mc.example.net`。未设置时关闭路由。 |
| `login-server` | `FELIS_LOGIN_SERVER` | 所有新连接必须通过的系统认证网关,默认 `login`。 |
| `lobby-server` | `FELIS_LOBBY_SERVER` | 后端唤醒期间使用的独立认证后等待服务器,默认 `lobby`,不得与 `login-server` 相同。 |
代理负载限制:felis-api 调用使用 8 个线程和 64 个等待槽。超过容量时立即拒绝请求:玩家收到“忙碌”提示,大厅消息得到 `busy` 错误,被丢弃的 join-event 记录为警告。这避免了 felis-api 缓慢时线程无限堆积。注册刷新(15 秒)和队列轮询(2 秒)在上一轮未结束时跳过本轮。操作命令(`/link`、`/felis claim`、迁移、管理员批准)共用每玩家限额:初始 5 次,此后每 5 秒补充 1 次。大厅的 felis:control 消息也按玩家限流,菜单状态响应会缓存。
代理健康记录:每个有活动的 10 分钟窗口输出一条 info 日志,内容为 `Felis: last 10 min: felis-api calls=… (no answer=…, 4xx=…, 5xx=…, retried=…), avg=… ms, max=… ms, busy refusals=…, join-events failed=…, join-events dropped=…, transfers failed=…, server-list refreshes failed=…, waiting now=…`,只统计当前窗口。从控制台执行 `/felis` 还会显示启动以来的 felis-api 调用次数、当前等待人数和累计失败次数。服务器列表刷新持续失败时,首次记录警告,之后每 5 分钟记录累计失败次数,恢复时输出 info 日志。登录网关对 link-status 轮询采用相同方式。
## 大厅菜单(§12) {#lobby-menu-§12}
`paper/` 模块实现 §27 场景 10 的大厅玩家界面:`/menu → 插件消息 → Velocity → API → 共享等待队列 → 就绪后连接`。它运行在 Paper 大厅服务器,用箱子界面代替命令行。`/menu`(别名 `/server`,路由启用时由代理透传)为代理路由的每台服务器显示一个格子,点击后可以唤醒、认领或进入后端。
**大厅只负责界面。** 它不持有 felis-api 令牌,不建立 HTTP 连接,也不维护等待队列。每个操作都是 `felis:control` 插件消息通道中的一帧,显示的所有状态也从该通道接收。只有 Velocity 的 `ControlChannel` 访问 felis-api。这一约束由构建**实际强制执行**:模块 `sourceSets` 的包含过滤器只编译 paper 包和三个编解码类,因此大厅 jar 恰好包含以下类:
```
best/lolicon/felis/link/Control.class (channel framing)
best/lolicon/felis/link/ControlFrame.class (the frame model)
best/lolicon/felis/link/Json.class (codec)
best/lolicon/felis/paper/FelisPaperPlugin.class (+ $1)
best/lolicon/felis/paper/LobbyGuard.class
best/lolicon/felis/paper/MenuHolder.class
best/lolicon/felis/paper/MenuTiles.class (+ $Kind, $Tile)
```
其中**不包含** `FelisApiClient`、`LinkClient` 或令牌配置类。编解码类若新增对 API 客户端的依赖,这里的编译就会失败,不会静默扩大大厅权限。
**消息帧。** 上行(大厅 → Velocity)包括 `ListRequest`、`WakeRequest`、`ClaimRequest` 和 `StatusQuery`;下行(Velocity → 大厅)包括 `ListUpdate`、`StatusUpdate`、`TransferReady` 和 `Error`。`/menu` 发送 `ListRequest`,代理根据实际路由的注册表返回 `ListUpdate`,列出所有用户服务器,因此在面板创建服务器后无需手动编辑大厅。列表优先显示玩家自己的服务器,并附带 felis-api 对当前玩家的判断:`owner`、`wake`、`owner_only`、`allowlist`、`retiring` 或 `start_failed`。该判断来自 `GET /api/v1/internal/player/menu-access/{uuid}`,每次打开菜单调用一次。felis-api 无法响应时,只返回名称,格子的操作交由实际唤醒请求判断。菜单先为每台服务器绘制灰色“加载中”格子,每页 45 个,底行是翻页箭头,再逐一发送 `StatusQuery`;代理通过 `StatusUpdate` 按阶段和归属更新格子。
**防伪造(§14)。** 大厅消息中的 `player` 字段**不可信**。Velocity 从插件消息实际到达的 `ServerConnection` 确定操作玩家及 UUID,服务端 autostartPolicy 和归属检查均使用该已验证身份。消息的 `server` 字段才是操作载荷,只指定点击了哪个格子。因此,即使大厅被完全控制,也无法冒充其他玩家或直接访问 API。
**按钮规则**:`MenuTiles` 根据最近的 `StatusUpdate` 和玩家访问判断,采用第一个匹配项。
| 格子状态 | 按钮 | 发出的消息 |
| --- | --- | --- |
| 已就绪(`ready`),任意访问判断 | **进入**(绿色) | `WakeRequest{server}` |
| 无主(`claimable`) | **认领并启动**(金色) | `ClaimRequest{server}` |
| `retiring` / `start_failed` / `owner_only` / `allowlist` | **无法启动**(灰色,说明中显示原因) | 不发送消息;在聊天中说明原因,菜单保持打开 |
| 其他情况(`owner`、`wake` 或没有访问判断) | **启动**(红色) | `WakeRequest{server}` |
玩家自己的服务器以 ★ 和“你的服务器”标记。状态行使用玩家语言显示运行中、启动中、停止中、已停止、启动失败或未知。“进入”和“启动”发送**相同**的 `WakeRequest` 消息:服务器已运行时,代理直接让玩家进入,任何已绑定的玩家都可加入。拒绝通过 `Error` 消息返回,例如 `not_linked` / `quota_exceeded` / `already_claimed`,并显示易懂的提示。这是认领、配额和策略失败向玩家反馈的位置。就绪状态通过 `TransferReady` 发送,随后代理连接玩家。
> **验证状态。** 该部分**代码完整,且已验证编译**:Paper jar 在 Java 25 工具链下构建成功;Velocity 端编译完整 shared 目录;消息编解码完成往返测试;Fabric / Forge / NeoForge 模组通过自带 wrapper 编译,并在真实独立服务器启动后注册 `/link`。这些检查均由 CI 执行。**尚未通过游戏客户端验证**:没有真实客户端通过整套服务进服,因此 §27 场景 10 在完成实际进服测试之前仍为 **FAIL(实际客户端未验证)**。不依赖客户端的代理边缘、子域名 MOTD、登录边界和后端注册已在真实部署上验证。
## 构建 {#building}
每个模块通过各自随源码提供的 Gradle wrapper 构建。所有 wrapper 都固定发行包 sha256(`distributionSha256Sum`),Gradle 下载被篡改或替换时,会在运行之前失败。各平台所需的 Gradle 版本不同,以下是实际验证过的要求:
| 模块 | Gradle | JDK | 原因 |
| --- | --- | --- | --- |
| `velocity` | 9.8.0 | 运行需要 ≥ 21,生成 Java 21 字节码 | 普通 `java` 插件;velocity-api 3.5.1 声明 `jvm.version = 21` |
| `paper` | 9.8.0 | **Java 25 工具链** | paper-api 26.3 是 Java 25 产物,模块声明 `JavaLanguageVersion.of(25)` 工具链 |
| `limbo` | 9.8.0 | 运行需要 ≥ 21,生成 Java 17 字节码 | 当前 LOOHP/Limbo 发布版使用 class-file major 65,编译 JDK 必须 ≥ 21 才能读取;`release 17` 字节码可运行于使用 Java 17+ 的 Limbo |
| `fabric` | 8.8 | 17 | loom 1.7.4 使用的 `Problems.forNamespace` 在 Gradle 9 被移除 |
| `forge` | 8.8 | 17 | ForgeGradle 6 仅支持 Gradle 8 |
| `neoforge` | 8.14 | 17 | NeoGradle 7.1.38 要求 Gradle API ≥ 8.14 |
安装器内置的 `velocity`、`paper`、`limbo` 插件在所有构建路径中均使用 Gradle 9.8.0:本地和 CI 使用 wrapper,大厅与登录服 Dockerfile、安装器的 Velocity 构建使用 `gradle:9.8.0-jdk25@sha256:…` 镜像。镜像、摘要或 wrapper 版本不一致时,`bootstrap_asset_test.go` 会失败。
```bash
# Velocity and Paper
plugins/velocity/gradlew -p plugins/velocity build
plugins/paper/gradlew -p plugins/paper build
# limbo compiles against the LOOHP/Limbo API release the login gate bundles, which has
# to be named: pass deploy/game-stack.lock's LIMBO_VERSION, exactly as bootstrap does.
plugins/limbo/gradlew -p plugins/limbo build -PlimboVersion="$(sed -n 's/^LIMBO_VERSION=//p' deploy/game-stack.lock)"
# Fabric / Forge / NeoForge. Nothing installs these; the jar you want is the one this
# produces.
plugins/fabric/gradlew -p plugins/fabric build
plugins/forge/gradlew -p plugins/forge build
plugins/neoforge/gradlew -p plugins/neoforge build
```
每个模组首次构建需要下载 Minecraft 并完成映射或反编译,因此可能耗时几分钟;后续构建较快。Jar 位于各模块的 `build/libs`。CI 执行两组检查:
- `bash plugins/test.sh` 使用 JDK 25,检查安装时部署的插件、编解码、邀请、服务器列表,以及基于真实 velocity-api 驱动 ServerRegistry、WaitingRouter、ControlChannel 的代理路由测试。仅运行路由测试可执行 `plugins/velocity/gradlew -p plugins/velocity routingTest`。它还运行基于真实 paper-api 驱动 LobbyMenu 的大厅菜单测试(`plugins/paper/gradlew -p plugins/paper lobbyTest`)、使用虚拟 tick 和 felis-api 桩驱动 LoginFlow 的登录网关测试(`plugins/limbo/gradlew -p plugins/limbo -PlimboVersion=<lock's LIMBO_VERSION> loginTest`),以及检查加载器模组共用 `/link` 的 ModLinkTest。
- `bash plugins/test-mods.sh` 使用 JDK 17,通过上述 wrapper 检查三种加载器模组。
### 依赖校验 {#dependency-verification}
所有依赖均使用精确版本。paper-api 对应 `deploy/game-stack.lock` 安装的 Paper 构建(`paper-26.3-40.jar` → `26.3.build.40-alpha`),Limbo 编译依赖锁文件的 `LIMBO_VERSION`,velocity-api 对应 `VELOCITY_VERSION`,ForgeGradle 固定为 `6.0.54`。安装器使用的三个模块还包含 `gradle/verification-metadata.xml`,记录构建解析的每个产物的 sha256,字节不一致时 Gradle 拒绝使用。Limbo API 校验项就是登录网关实际运行的 jar,其 sha256 与锁文件的 `LIMBO_JAR_SHA256` 一致。
`deploy/update-game-stack-lock.sh` 更新 Paper、Limbo 或 Velocity 后,需要同步固定版本,重新生成校验和,并检查差异:
```bash
# 1. set paper-api in plugins/paper/build.gradle to the new build (paper-<mc>-<n>.jar → <mc>.build.<n>-<channel>)
# 2. regenerate the three verification files (JDK 25) from an EMPTY Gradle home: with a
# warm cache Gradle skips the BOMs and parent POMs it already holds, and the image
# builds, which start empty, then refuse them
rm -f plugins/{velocity,paper,limbo}/gradle/verification-metadata.xml
export GRADLE_USER_HOME="$(mktemp -d)"
plugins/velocity/gradlew -p plugins/velocity --write-verification-metadata sha256 build
plugins/paper/gradlew -p plugins/paper --write-verification-metadata sha256 build
plugins/limbo/gradlew -p plugins/limbo --write-verification-metadata sha256 build \
-PlimboVersion="$(sed -n 's/^LIMBO_VERSION=//p' deploy/game-stack.lock)"
unset GRADLE_USER_HOME
# 3. go test . fails until the pins, the checksums and the lock agree
```
加载器模组也固定插件、依赖版本和 wrapper 的 sha256,但不包含校验文件。loom、ForgeGradle 和 NeoGradle 使用自己的下载器获取和映射 Minecraft,并对照 Mojang 清单的哈希检查;这些 jar 不由安装器部署。
## 部署 {#deploying}
将对应 jar 放入服务器或代理的 mods / plugins 目录,启动一次生成 `config/felis-link.properties`(Velocity 为 `plugins/felis-link/…`),随后设置 `api-base-url` 和 `service-token`,或提供优先级更高的环境变量 `FELIS_API_BASE_URL` 和 `FELIS_SERVICE_TOKEN`。
令牌属于 felis-api 按调用方分配的内部令牌。Velocity 代理使用 `velocity` 令牌(Secret `felis/felis-service-token`),登录网关使用 `limbo` 令牌(`felis-limbo-token`),各自只可访问自己的路由,跨调用方路由返回 `403 wrong_caller`。加载器模组只在开启 online-mode 的独立服务器上使用 `limbo` 令牌,详见模块表后的警告。令牌应作为密钥保存;`sudo felis rotate-token <caller>` 可替换它。
从文件读取的令牌会跟随文件更新:代理或模组最多每秒重新读取一次,下一次调用 felis-api 时使用新 `service-token`,记录 `service-token reloaded from … (fingerprint …)`,保留已有玩家连接。环境变量 `FELIS_SERVICE_TOKEN` 中的令牌需重启进程才会改变。
**Velocity** 还需要在同一文件设置 `root-domain`(以及可选的 `lobby-server`),并确保 `velocity.toml` 中为 `online-mode=true`,才能启用 §11 路由。任一条件不满足时,代理仍提供 `/link`,但路由保持关闭,见 [Velocity 路由](#velocity-routing-§11)。插件 ID 固定为 `felis-link`,因此配置目录为 `plugins/felis-link/`;ID 在 0.1 → 0.2 jar 之间保持不变,旧配置可继续使用。
**Paper 大厅**无需配置令牌,也无需维护服务器列表:菜单显示代理实际路由的服务器,由代理通过 `felis:control` 发送。旧版本在 `plugins/FelisPaper/config.yml` 中留下的 `servers:` 列表会被忽略,插件会在启动时提示。大厅必须与后端位于同一 Velocity 代理之后,只通过代理的 `felis:control` 入口访问控制平面,因此不需要自己的 `api-base-url` 或 `service-token`。安装器将该 jar 构建并打包进大厅镜像;自行运行大厅时才需要手动安装。
---
原文:[plugins/README.md](https://github.com/FelisMC/Felis/blob/main/plugins/README.md)。
+131
View File
@@ -0,0 +1,131 @@
---
title: 项目说明
---
# 项目说明 {#felis}
基于 Kubernetes 的 Minecraft 服务器托管平台。
单条命令完成部署,自动管理服务器生命周期、备份与安全。
> [!CAUTION] 注意
> **此项目仍处于早期开发阶段,您不该在任何生产环境使用该项目。若产生任何问题,贵用户的使用行为与 FelisMC 团队无任何民事刑事法律关系。**<br>
<details>
<summary>目录</summary>
- [特性](#features)
- [使用方式](#getting-started)
- [从源码构建](#build-from-source)
- [开源协议](#license)
- [致谢](#acknowledgements)
</details>
## 特性 {#features}
* **按需启停**:玩家连接代理时自动启动目标服务器,启动期间玩家进入等待队列,服务器就绪后自动传送;服务器空闲后自动停止,释放内存。
* **Web 控制面板**:在浏览器中查看服务器状态、在线玩家与资源用量。
* 控制台(RCON)、白名单、封禁、OP 与 LuckPerms 权限管理
* 文件管理:新建、删除、重命名、分片上传、下载,以及停服状态下解压 zip,可用于导入世界
* 计划任务:按星期与时区定时执行命令、重启、停止、启动或备份,执行前在游戏内向玩家发送提醒
* **备份与恢复**:默认启用,归档 PVC 及其路径由安装器生成。
* 手动备份:将服务器的完整数据卷(`/data`,含世界、配置、插件与模组)归档至集群内的归档存储,可回滚至任一备份点
* 每日恢复点:当天有玩家进入过的服务器在停止后自动生成恢复点,默认保留 7 个,保存期限 90 天;恢复点单独轮换,不影响手动备份
* 下载与导出:支持下载单个备份(附 sha256 校验)、删除单个备份及导出完整世界
* 异地副本(可选):备份在主机上加密后同步至 S3 兼容存储(AWS S3、Cloudflare R2、Backblaze B2、MinIO 等)
* 控制面数据库:存放账号、服务器归属、配额与存档索引的数据库每日自动备份,每次升级迁移前额外创建快照,故障时可通过 `felis db restore` 整库原子回滚;面板「维护与备份」页显示最近一次备份的时效(参见 [故障排查 §16](/operations/troubleshooting#_16-control-plane-database-backups-and-disaster-recovery))
* **运维诊断**
* `sudo felis status`:汇总显示节点、控制面、游戏代理、各服务器、备份及未解决的告警
* `sudo felis doctor`:执行全部健康检查,按模块列出问题及排查方向,执行过程中不发送邮件
* `sudo felis support-bundle`:生成已脱敏的诊断包,供提交问题时附带(参见 [故障排查 §0](/operations/troubleshooting#_0-first-look-felis-status-felis-doctor-felis-support-bundle))
* 看门狗:每 2 分钟执行一次巡检,异常持续时向平台所有者发送邮件告警,支持外部心跳监测
* **世界回收(可选)**:超过 15 天无人游玩的世界在备份后删除,以释放磁盘空间。安装时设置 `FELIS_WORLDS_HOST_PATH`(k3s 默认为 `/var/lib/rancher/k3s/storage`)即启用每日回收;未设置时不删除任何世界。过期备份的每日清理与此设置无关,始终执行。
* **多核心支持**:兼容 Paper、Fabric、Forge 与 NeoForge,统一经由 Velocity 代理接入。
* **模组包投稿**:玩家可上传模组包,经服主审批后自动构建并通过 Trivy 安全扫描;构建产物加入镜像白名单后,可直接选作服务器镜像。
* **安全**
* Passkey 登录:支持指纹、面容识别及硬件密钥等无密码认证方式
* 零信任访问:面板流量经 Cloudflare Access 保护,集群内部 API 不对公网开放
* **多机部署(实验性,默认关闭)**:由一台主控节点统一下发指令,其余节点仅运行游戏服务器,已停止的服务器可迁移至其他节点。该功能目前仅位于 main 分支,尚未完成三机验收(参见 [多机部署](/guide/distributed))。
## 使用方式 {#getting-started}
在已准备好的 Linux 主机上执行:
```bash
curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap.sh | sudo bash
```
脚本将安装 K3s,在 K3s 中部署 PostgreSQL 与控制平面,随后启动设置向导。设置完成后,通过浏览器访问所配置的域名即可进入控制面板。
* **设置向导**:向导首先绑定平台所有者:以 Minecraft Java 版加入向导所示的地址,登录服务器会给出 8 位绑定码(10 分钟内有效),将其输入向导即可。该步骤可以跳过,之后再次执行 `sudo felis setup` 补做;绑定所有者之前,任何人均无法登录控制面板,登录页届时会说明原因并列出绑定步骤及连接地址。安装器仅在交互式终端中自动启动向导;输出重定向至日志或经由 cloud-init 安装时,请在安装结束后执行 `sudo felis setup`。设置 `FELIS_NO_SETUP=1` 时,安装器在输出摘要后直接结束。
* **支持的系统**:CentOS Stream 9(aarch64)已在实机上验证;Ubuntu 24.04(x86_64)在每次推送时由 CI 执行全新安装、重复安装、升级及上述安装命令(参见 [运维手册 §1](/operations/#_1-supported-hosts))。
* **安装前检查**:安装器在修改主机之前检查内存、磁盘、端口、网段冲突、已有的 Kubernetes 及外网连通性。发现问题时一次性列出全部问题并退出,主机保持原状(检查项参见 [运维手册 §1](/operations/#_1-supported-hosts))。
* **升级**:重新执行安装命令即可将 felis-api 升级至新版本;`felis setup` 仅使用本机已安装的二进制,无法用于升级。重新执行时沿用已安装的根域名,发布通道需重新指定:跟随 main 分支的主机须同时设置 `export FELIS_VERSION_BOOTSTRAP=dev`。早期版本安装在宿主机上的 PostgreSQL 会在重新执行时整库迁入 K3s,宿主机上的原实例停用并保留,以便回退(参见 [运维手册 §4](/operations/#_4-upgrading-the-pieces-around-felis))。
<details>
<summary>安装来源与受限网络环境下的安装</summary>
<br>
安装发布版时,二进制文件、全部镜像及 Velocity 插件均取自该版本由 CI 预构建的 release 附件,逐一校验 `SHA256SUMS` 后导入。主机无需安装 Docker、Gradle 或 Go,也无需访问 Docker Hub。若某个附件缺失或校验失败,仅该镜像回退为本机构建,并输出提示(参见 [故障排查 §15c](/operations/troubleshooting#_15c-the-installer-builds-on-the-host-although-it-installs-a-release))。
也可将附件预先复制到主机,再通过 `FELIS_ARTIFACT_DIR=<绝对路径>` 安装,此时 Felis 自身的二进制、镜像与插件均从该目录读取。k3s 及其镜像、JRE、cloudflared、Velocity 与 Via 插件仍从 GitHub 和 PaperMC 下载;RHEL、Fedora、openSUSE Leap 等启用 SELinux 的主机还需从 rpm.rancher.io 安装 k3s-selinux;系统软件包来自发行版软件源。
因此,出站网络受限的主机须放行上述地址的 HTTPS 访问,或设置 `https_proxy`。preflight 会在修改主机之前逐一探测这些地址。目前暂不支持完全离线安装(地址清单参见 [运维手册 §1](/operations/#_1-supported-hosts))。
</details>
## 从源码构建 {#build-from-source}
本项目基于 Go 与 Node.js 开发:
```bash
# 后端(Go 1.26+)
go build -o felis ./cmd/felis
# 前端(Node.js 22+)
cd panel
npm ci
npm run build
# Docker 镜像
docker build -t felis:custom .
```
## 开源协议 {#license}
本项目采用 [AGPL-3.0-only](/LICENSE.txt) 许可证。
### 协议注意事项 {#license-notes}
1. **衍生作品须采用 AGPL**:分发本项目副本或基于本项目的衍生软件时,须以 AGPL-3.0 开源,并保留原作者的版权声明与许可声明。
2. **网络服务同样须提供源码**(AGPL 第 13 条):通过网络向他人提供经修改的 Felis 服务时,即使未分发任何二进制文件,也须向这些用户提供修改后的完整源码。这是 AGPL 与 GPL 唯一的实质区别;Felis 作为通过网络访问的托管平台,几乎所有部署场景都适用此条款。
3. **免责声明**:本项目按"原样"提供,作者不承担因使用本项目而产生的任何法律责任。
## 致谢 {#acknowledgements}
* [Kubernetes](https://kubernetes.io/):容器编排引擎
* [K3s](https://k3s.io/):轻量级 Kubernetes 发行版
* [Cloudflare Zero Trust](https://www.cloudflare.com/zero-trust/):零信任安全基础设施
* [PostgreSQL](https://www.postgresql.org/):数据持久化
* [React](https://react.dev/):前端用户界面框架
* [Vite](https://vitejs.dev/):前端构建工具
* [TailwindCSS](https://tailwindcss.com/):CSS 框架
* [Bubble Tea](https://github.com/charmbracelet/bubbletea):TUI 框架
* [Minecraft](https://www.minecraft.net/):本项目服务的游戏
---
原文:[README.md](https://github.com/FelisMC/Felis/blob/main/README.md)。
+162
View File
@@ -0,0 +1,162 @@
---
title: 时序图
---
# 时序图 {#felis-sequence-diagrams}
本文收录规范第 28 节要求、且不由 OpenAPI 定义覆盖的时序图。
## 第 28 节 #9:查询 → 进服 → 唤醒 → 就绪 → 传送 {#section-28-9-ping-to-join-to-wake-to-ready-to-teleport}
```mermaid
sequenceDiagram
autonumber
actor Player as 玩家
participant Velocity as Velocity 代理
participant Registry as Velocity 服务器注册表
participant API as felis-api 内部接口
participant Cluster as MinecraftServer CRD/status
participant Operator as Felis 控制器
participant Backend as Minecraft 后端
Player->>Velocity: 查询 subdomain.root-domain 的服务器列表
Velocity->>Registry: 读取缓存的生命周期视图
Registry-->>Velocity: 根据阶段生成 MOTD
Velocity-->>Player: 返回查询结果(只读,不唤醒)
Player->>Velocity: 加入 subdomain.root-domain
Velocity->>Registry: 由主机名解析目标服务器
Registry-->>Velocity: ServerView(name, ready=false)
alt 后端已就绪并注册
Velocity-->>Player: 初始服务器 = 后端
Player->>Backend: 连接
else 后端未就绪且已配置大厅
Velocity-->>Player: 初始服务器 = 大厅
Velocity->>API: POST /api/v1/internal/servers/{name}/wake {mc_uuid}
API->>Cluster: GetServer(name)
API->>API: 检查 autostartPolicy、冷却时间和运行数量上限
API->>Cluster: SetDesiredState(name, Running)
API-->>Velocity: 202 阶段/就绪状态
Velocity->>Velocity: 加入等待队列
Operator->>Cluster: 协调 DesiredState=Running
Operator->>Backend: 启动 Pod/Service
Backend-->>Operator: RCON 就绪 / 生命周期就绪
Operator-->>Cluster: status.ready=true
loop 每次等待轮询
Velocity->>API: GET /api/v1/internal/servers/{name}/status
API->>Cluster: GetServer(name)
API-->>Velocity: 就绪标记
end
Velocity->>Registry: 查找已注册的后端
Velocity-->>Player: "已就绪,正在传送"
Velocity->>Player: 请求连接后端
Player->>Backend: 连接
Velocity->>API: POST /api/v1/internal/servers/{name}/join-event {mc_uuid}
API->>API: RecordJoin#59; 更新活跃时间和 UUID 白名单
API-->>Velocity: 204
else 后端未就绪且未配置大厅
Velocity-->>Player: 断开连接并提示稍后重连
Velocity->>API: POST /api/v1/internal/servers/{name}/wake {mc_uuid}
API->>Cluster: 授权通过后 SetDesiredState(name, Running)
API-->>Velocity: 202 或可区分的错误
end
```
## 第 28 节 #11:认领事务 {#section-28-11-claim-transaction}
```mermaid
sequenceDiagram
autonumber
actor Player as 玩家
participant Panel as Web 控制面板
participant API as felis-api 外部接口
participant Repo as 存储层 / Postgres
participant Audit as 审计日志
Player->>Panel: 点击无主服务器的认领按钮
Panel->>API: POST /api/v1/servers/{name}/claim
API->>API: 校验服务器名称和调用者身份
API->>Repo: IsLinked(user_id)
alt 用户尚无已验证的账号绑定
Repo-->>API: false
API-->>Panel: 412 not_linked
else 已绑定
Repo-->>API: true
API->>Repo: QuotaCheck(user_id, 服务器实际规格)
Note over API,Repo: 四项上限:服务器数、CPU、内存、存储
alt 配额耗尽
Repo-->>API: false
API-->>Panel: 403 quota_exceeded
else 配额可用
Repo-->>API: true
API->>Repo: ClaimServer(name, user_id)
Note over Repo: 同一事务:pg_advisory_xact_lock(user_id) 串行化该用户的认领#59; SELECT FROM servers WHERE name=$1 AND deleted_at IS NULL FOR UPDATE#59; 重新执行四维配额检查(以此处为准,上方预检只是快速路径)#59; 随后 UPDATE servers SET owner_id=$2, claimed_at=now() WHERE name=$1 AND owner_id IS NULL AND deleted_at IS NULL
alt 服务器不存在
Repo-->>API: ErrNotFound
API-->>Panel: 404 not_found
else 影响 0 行
Repo-->>API: claimed=false
API-->>Panel: 409 already_claimed
else 影响 1 行
Repo-->>API: claimed=true
API->>Audit: 记录外部认领审计
API-->>Panel: 200 {"claimed":true}
end
end
end
```
## 第 28 节 #12:账号绑定与 /link 流程 {#section-28-12-account-binding-link-flow}
```mermaid
sequenceDiagram
autonumber
actor Player as 玩家
participant Game as Minecraft 服务器或 Velocity
participant LinkClient as Felis LinkClient
participant APIInternal as felis-api 内部接口
participant Repo as 存储层 / Postgres
participant Panel as Web 控制面板
participant APIExternal as felis-api 外部接口
Player->>Game: /link
Game->>Game: 读取已验证的 online-mode UUID
Game->>LinkClient: requestCode(mc_uuid)
LinkClient->>APIInternal: POST /api/v1/internal/account/link/code {mc_uuid}
APIInternal->>APIInternal: 校验 UUID#59; auth_source 缺失时根据 UUID 版本位推导(v3 → thirdparty,其余 → mojang)#59; 生成 8 位绑定码
APIInternal->>Repo: CreateLinkCode(code, mc_uuid, auth_source, expires_at)
Repo-->>APIInternal: 已插入 account_link_codes 记录
APIInternal-->>LinkClient: 201 {code, expires_at, panel_url?}
LinkClient-->>Game: LinkCode
Game-->>Player: 在聊天中显示一次性绑定码
Player->>Panel: open Account link flow
Panel->>APIExternal: POST /api/v1/account/link/start
APIExternal->>Repo: IsLinked(user_id)
Repo-->>APIExternal: linked status
APIExternal-->>Panel: 返回状态和“运行 /link”的提示
Player->>Panel: 提交绑定码
Panel->>APIExternal: POST /api/v1/account/link/verify {code}
APIExternal->>APIExternal: 去掉首尾空白并转为大写
APIExternal->>Repo: VerifyLinkCode(user_id, code, now)
Repo->>Repo: SELECT mc_uuid, auth_source FROM account_link_codes WHERE code=$1 AND expires_at>$2 FOR UPDATE
alt 绑定码不存在或已过期
Repo-->>APIExternal: ErrLinkCodeInvalid
APIExternal-->>Panel: 400 invalid_code
else UUID 已绑定到另一个有效用户
Repo-->>APIExternal: ErrConflict
APIExternal-->>Panel: 409 already_linked
else 绑定码有效(同一用户重复验证幂等#59; 接管已停用或软删除用户的绑定)
Repo->>Repo: INSERT account_links(user_id, mc_uuid, auth_source) ON CONFLICT (user_id, mc_uuid) DO UPDATE auth_source
Repo->>Repo: DELETE account_link_codes WHERE code=$1
Repo-->>APIExternal: mc_uuid, auth_source
APIExternal->>Repo: 记录 account.link 审计
APIExternal-->>Panel: 200 {linked:true, mc_uuid, auth_source}
end
```
---
原文:[docs/sequence-diagrams.md](https://github.com/FelisMC/Felis/blob/main/docs/sequence-diagrams.md)。
+19
View File
@@ -0,0 +1,19 @@
{
"name": "felis-docs",
"private": true,
"type": "module",
"packageManager": "[email protected]",
"scripts": {
"docs:dev": "vitepress dev docs --host 127.0.0.1",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs --host 127.0.0.1"
},
"devDependencies": {
"@nolebase/vitepress-plugin-enhanced-readabilities": "2.18.2",
"@nolebase/vitepress-plugin-highlight-targeted-heading": "2.18.2",
"mermaid": "11.17.2",
"vitepress": "1.6.4",
"vitepress-plugin-mermaid": "2.0.17",
"vue": "^3.5.43"
}
}