搭起项目骨架,验证配置文件往返
核心是确认「改配置而不弄乱文件」这件事成立:新增一路入站之后,其余 52 行逐字节不变。 搬进来的改写引擎用默认输出参数时会重排整个文件——行尾注释的对齐空格消失、流式数组被加上内边距—— 连根本没改动的段落也一起波及。不修的话「改一处只动一处」这个承诺直接落空,所以关掉了它的自动排版, 并在写回之后补了一步逐行还原。
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
# 复制成 .env 再填。.env 已在 .gitignore 里,不会被提交。
|
||||||
|
|
||||||
|
# mihomo 管理接口的地址。开发时通过 SSH 隧道映射到本机:
|
||||||
|
# ssh -N -f -L 9090:127.0.0.1:9090 用户名@服务器地址
|
||||||
|
MIHOMO_API=http://127.0.0.1:9090
|
||||||
|
|
||||||
|
# 管理接口的访问密钥,即配置文件里 secret: 那一行的值
|
||||||
|
MIHOMO_SECRET=
|
||||||
|
|
||||||
|
# 要编辑的配置文件路径。开发时指向本地副本,部署时指向 /etc/mihomo/config.yaml
|
||||||
|
PATCHBAY_CONFIG=../pkgs/sample-config.yaml
|
||||||
|
|
||||||
|
# Patchbay 自己监听的地址和端口。默认只监听本机,远程访问走 SSH 隧道。
|
||||||
|
PATCHBAY_HOST=127.0.0.1
|
||||||
|
PATCHBAY_PORT=7580
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.output/
|
||||||
|
.nuxt/
|
||||||
|
.vite/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 含真实密钥,永远不要提交
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# 从目标机器拷回来的配置副本
|
||||||
|
*.local.yaml
|
||||||
|
config.local.*
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
# Patchbay
|
||||||
|
|
||||||
|
一个编辑 mihomo 配置文件的网页界面。核心动作是把某一路入站接到某一个出站上——名字取自录音棚的插线板。
|
||||||
|
|
||||||
|
这份文件是术语表。界面文案、代码命名、文档措辞都从这里取词,同一个概念不要有第二种叫法。
|
||||||
|
|
||||||
|
## 入站与出站
|
||||||
|
|
||||||
|
**入站**:
|
||||||
|
mihomo 上的一个监听端口,客户端连上它把流量交进来。
|
||||||
|
_Avoid_: inbound、监听器、listener、进站
|
||||||
|
|
||||||
|
**默认入站**:
|
||||||
|
由配置文件顶层端口字段声明的入站(`mixed-port`、`port`、`socks-port`、`redir-port`、`tproxy-port`)。每种类型只能有一个,且无法单独指定出站。界面上不可删除。
|
||||||
|
_Avoid_: 主端口、老式端口、默认端口
|
||||||
|
|
||||||
|
**命名入站**:
|
||||||
|
由 `listeners:` 声明的入站。数量不限,每个可以有自己的出站和附加规则。
|
||||||
|
_Avoid_: listener、新式端口、自定义端口
|
||||||
|
|
||||||
|
**出站**:
|
||||||
|
流量离开 mihomo 的去处。可以是内置出站、线路或策略组。
|
||||||
|
_Avoid_: outbound、出口、上游、落地
|
||||||
|
|
||||||
|
**内置出站**:
|
||||||
|
内核自带、不需要配置就存在的出站,包括直连和拒绝。
|
||||||
|
_Avoid_: 系统出站、特殊出站
|
||||||
|
|
||||||
|
**出站模式**:
|
||||||
|
一路入站决定其出站的四种方式之一:跟着规则走、直连、指向策略组、固定线路。
|
||||||
|
_Avoid_: 代理模式、mode、路由模式
|
||||||
|
|
||||||
|
## 线路与分组
|
||||||
|
|
||||||
|
**线路**:
|
||||||
|
一个具体的代理服务器条目。
|
||||||
|
_Avoid_: 节点、proxy、代理、服务器
|
||||||
|
|
||||||
|
**自有线路**:
|
||||||
|
写在配置文件顶层 `proxies:` 里、由用户手写的线路。可以被按名字引用。
|
||||||
|
_Avoid_: 本地节点、手写节点
|
||||||
|
|
||||||
|
**订阅线路**:
|
||||||
|
由订阅拉取而来的线路。**不能被按名字引用**,只能通过策略组整批引入。
|
||||||
|
_Avoid_: 远程节点、provider 节点、机场节点
|
||||||
|
|
||||||
|
**订阅**:
|
||||||
|
一个远程地址,mihomo 定时从它拉取一批线路。
|
||||||
|
_Avoid_: provider、机场、订阅链接、节点池
|
||||||
|
|
||||||
|
**策略组**:
|
||||||
|
一组线路加一个挑选方式(手动选、自动测速选最快等)。策略组本身也可以作为出站被引用。
|
||||||
|
_Avoid_: 分组、group、代理组、策略
|
||||||
|
|
||||||
|
**筛选组**:
|
||||||
|
Patchbay 自动创建的策略组,用筛选条件从订阅里只挑出用户指定的那几条线路。它的存在是为了让入站能固定到具体线路——因为订阅线路无法被直接引用。
|
||||||
|
_Avoid_: 隐藏组、临时组、影子组
|
||||||
|
|
||||||
|
## 名字与引用
|
||||||
|
|
||||||
|
**名字表**:
|
||||||
|
内核启动时构建的、所有可被按名字引用的出站的集合。由内置出站、自有线路和策略组构成。**订阅线路不在其中**,这是许多约束的共同来源。
|
||||||
|
_Avoid_: 代理表、proxies map、出站列表
|
||||||
|
|
||||||
|
**断引用**:
|
||||||
|
配置里出现了一个名字表中不存在的名字。内核自带的校验命令不检查这种情况,只在有流量经过时才报错,因此 Patchbay 必须自行检查。
|
||||||
|
_Avoid_: 无效引用、悬空引用、坏引用
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
|
||||||
|
**分流规则**:
|
||||||
|
配置文件 `rules:` 里的有序列表,自上而下匹配,决定一条连接走哪个出站。顺序本身是语义的一部分。
|
||||||
|
_Avoid_: 路由规则、规则表
|
||||||
|
|
||||||
|
**附加规则**:
|
||||||
|
挂在某一路入站上的一组分流规则,只对从该入站进来的流量生效。
|
||||||
|
_Avoid_: sub-rules、子规则、私有规则
|
||||||
|
|
||||||
|
## 配置文件
|
||||||
|
|
||||||
|
**接管段落**:
|
||||||
|
Patchbay 负责读写的配置段落。其余段落原样保留、界面上也不呈现。
|
||||||
|
_Avoid_: 托管段落、受控段落
|
||||||
|
|
||||||
|
**默认路由**:
|
||||||
|
默认入站在界面上的呈现。它对应配置文件顶层的端口字段而非 `listeners:`,因此不可删除。
|
||||||
|
_Avoid_: 主路由、根路由
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "patchbay",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vite": "^7.0.0",
|
||||||
|
"vue-tsc": "^3.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
||||||
|
|
||||||
|
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.8", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew=="],
|
||||||
|
|
||||||
|
"@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
|
||||||
|
|
||||||
|
"@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
|
||||||
|
|
||||||
|
"@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
|
||||||
|
|
||||||
|
"@vue/compiler-core": ["@vue/compiler-core@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.41", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg=="],
|
||||||
|
|
||||||
|
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.41", "", { "dependencies": { "@vue/compiler-core": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw=="],
|
||||||
|
|
||||||
|
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.41", "@vue/compiler-dom": "3.5.41", "@vue/compiler-ssr": "3.5.41", "@vue/shared": "3.5.41", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ=="],
|
||||||
|
|
||||||
|
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A=="],
|
||||||
|
|
||||||
|
"@vue/language-core": ["@vue/language-core@3.3.9", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.2.1", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" } }, "sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ=="],
|
||||||
|
|
||||||
|
"@vue/reactivity": ["@vue/reactivity@3.5.41", "", { "dependencies": { "@vue/shared": "3.5.41" } }, "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA=="],
|
||||||
|
|
||||||
|
"@vue/runtime-core": ["@vue/runtime-core@3.5.41", "", { "dependencies": { "@vue/reactivity": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg=="],
|
||||||
|
|
||||||
|
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.41", "", { "dependencies": { "@vue/reactivity": "3.5.41", "@vue/runtime-core": "3.5.41", "@vue/shared": "3.5.41", "csstype": "^3.2.3" } }, "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw=="],
|
||||||
|
|
||||||
|
"@vue/server-renderer": ["@vue/server-renderer@3.5.41", "", { "dependencies": { "@vue/compiler-ssr": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ=="],
|
||||||
|
|
||||||
|
"@vue/shared": ["@vue/shared@3.5.41", "", {}, "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA=="],
|
||||||
|
|
||||||
|
"alien-signals": ["alien-signals@3.2.1", "", {}, "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||||
|
|
||||||
|
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||||
|
|
||||||
|
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||||
|
|
||||||
|
"vue": ["vue@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/compiler-sfc": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/server-renderer": "3.5.41", "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg=="],
|
||||||
|
|
||||||
|
"vue-tsc": ["vue-tsc@3.3.9", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.9" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "patchbay",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "mihomo 配置的网页界面:把每一路入站接到它自己的出站上",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --hot server/index.ts",
|
||||||
|
"dev:web": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"test": "bun test",
|
||||||
|
"typecheck": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"yaml": "^2.9.0",
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vite": "^7.0.0",
|
||||||
|
"vue-tsc": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* 往返验证:读一份真实配置 → 改一处 → 写回,看未改动的部分保住了多少。
|
||||||
|
*
|
||||||
|
* 这是整个项目最先要回答的问题。如果写回之后文件面目全非,界面做得再好也没人敢用。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* bun run scripts/roundtrip.ts <源配置> <输出目录>
|
||||||
|
*
|
||||||
|
* 产出三个文件,各对应一种改动,再用 git diff --no-index 跟源文件比对。
|
||||||
|
*/
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { replaceDocumentData, openDocument } from '../vendor/config-editor/index.ts'
|
||||||
|
import type { ConfigObject } from '../vendor/config-editor/index.ts'
|
||||||
|
import { restoreInlineCommentAlignment } from '../shared/yaml-cosmetics.ts'
|
||||||
|
|
||||||
|
const [sourcePath, outDir] = process.argv.slice(2)
|
||||||
|
if (!sourcePath || !outDir) {
|
||||||
|
console.error('用法:bun run scripts/roundtrip.ts <源配置> <输出目录>')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = readFileSync(sourcePath, 'utf8')
|
||||||
|
const doc = openDocument(source)
|
||||||
|
mkdirSync(outDir, { recursive: true })
|
||||||
|
|
||||||
|
const clone = (v: unknown) => JSON.parse(JSON.stringify(v)) as ConfigObject
|
||||||
|
|
||||||
|
/** 每个场景改一处,模拟界面上一次真实操作。 */
|
||||||
|
const scenarios: Array<{ file: string; what: string; mutate: (d: ConfigObject) => void }> = [
|
||||||
|
{
|
||||||
|
file: 'A-加一路入站.yaml',
|
||||||
|
what: '新增命名入站 7778,出站固定走 AUTO(本项目的核心操作)',
|
||||||
|
mutate: (d) => {
|
||||||
|
d.listeners = [
|
||||||
|
{ name: '容器A', type: 'mixed', port: 7778, listen: '0.0.0.0', proxy: 'AUTO' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'B-改一条规则.yaml',
|
||||||
|
what: '把最后一条兜底规则的出站从 PROXY 换成 AUTO',
|
||||||
|
mutate: (d) => {
|
||||||
|
const rules = d.rules as string[]
|
||||||
|
rules[rules.length - 1] = 'MATCH,AUTO'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'C-改一个策略组.yaml',
|
||||||
|
what: '给 PROXY 组的成员列表加一个 REJECT',
|
||||||
|
mutate: (d) => {
|
||||||
|
const groups = d['proxy-groups'] as Array<Record<string, unknown>>
|
||||||
|
const proxy = groups.find((g) => g.name === 'PROXY')!
|
||||||
|
proxy.proxies = [...(proxy.proxies as string[]), 'REJECT']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const countComments = (yaml: string) =>
|
||||||
|
yaml.split('\n').filter((line) => line.trim().startsWith('#') || / #\s/.test(line)).length
|
||||||
|
|
||||||
|
console.log(`源文件 ${sourcePath}`)
|
||||||
|
console.log(` ${source.split('\n').length} 行,其中带注释 ${countComments(source)} 行\n`)
|
||||||
|
|
||||||
|
for (const scenario of scenarios) {
|
||||||
|
const draft = clone(doc.data)
|
||||||
|
scenario.mutate(draft)
|
||||||
|
const result = replaceDocumentData(doc, draft)
|
||||||
|
const written = restoreInlineCommentAlignment(source, result.yaml)
|
||||||
|
const outPath = join(outDir, scenario.file)
|
||||||
|
writeFileSync(outPath, written, 'utf8')
|
||||||
|
|
||||||
|
const before = countComments(source)
|
||||||
|
const after = countComments(written)
|
||||||
|
console.log(`${scenario.file}`)
|
||||||
|
console.log(` 改动:${scenario.what}`)
|
||||||
|
console.log(` 注释行 ${before} → ${after}${after < before ? `(丢了 ${before - after} 行)` : '(没丢)'}`)
|
||||||
|
console.log(` 写到 ${outPath}\n`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 测试样本 — 仿照真实配置的形状,不含任何真实密钥或订阅地址
|
||||||
|
# 关键特征:整行注释、带对齐空格的行尾注释、流式写法的数组
|
||||||
|
|
||||||
|
mixed-port: 7777 # HTTP 和 SOCKS5 共用这一个端口
|
||||||
|
allow-lan: true # true = 同局域网的其它设备也能连这个代理
|
||||||
|
mode: rule
|
||||||
|
|
||||||
|
profile:
|
||||||
|
store-selected: true # 记住你在面板里选的节点,重启不丢
|
||||||
|
|
||||||
|
external-controller: 127.0.0.1:9090
|
||||||
|
secret: "占位密钥-不是真的"
|
||||||
|
|
||||||
|
proxy-providers:
|
||||||
|
subs:
|
||||||
|
type: http
|
||||||
|
url: "http://127.0.0.1:3001/占位路径/download/示例" # 来源标记
|
||||||
|
path: ./providers/subs.yaml
|
||||||
|
interval: 86400 # 每 24 小时自动拉取一次订阅
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
- name: PROXY # 总开关,手动选;默认跟着 AUTO 走
|
||||||
|
type: select
|
||||||
|
proxies: [AUTO, DIRECT]
|
||||||
|
use: [subs]
|
||||||
|
- name: AUTO # 全部节点里自动挑延迟最低的
|
||||||
|
type: url-test
|
||||||
|
use: [subs]
|
||||||
|
interval: 300
|
||||||
|
|
||||||
|
rules:
|
||||||
|
# 本机和内网地址一律直连,不然会绕出去再回来
|
||||||
|
- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
|
||||||
|
- IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
|
||||||
|
- MATCH,PROXY
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { openDocument, replaceDocumentData } from '../vendor/config-editor/index.ts'
|
||||||
|
import type { ConfigObject } from '../vendor/config-editor/index.ts'
|
||||||
|
import { restoreInlineCommentAlignment } from './yaml-cosmetics.ts'
|
||||||
|
|
||||||
|
const fixture = readFileSync(
|
||||||
|
join(import.meta.dir, '__fixtures__', 'config-with-comments.yaml'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
const clone = (v: unknown) => JSON.parse(JSON.stringify(v)) as ConfigObject
|
||||||
|
|
||||||
|
/** 走一遍真实的写回链路:解析 → 改 → 写回 → 修复格式。 */
|
||||||
|
function roundtrip(source: string, mutate: (draft: ConfigObject) => void): string {
|
||||||
|
const doc = openDocument(source)
|
||||||
|
const draft = clone(doc.data)
|
||||||
|
mutate(draft)
|
||||||
|
return restoreInlineCommentAlignment(source, replaceDocumentData(doc, draft).yaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('写回一份带注释的配置', () => {
|
||||||
|
it('原样写回时逐字节不变', () => {
|
||||||
|
expect(roundtrip(fixture, () => {})).toBe(fixture)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('新增一个此前不存在的段落时,其余每一行都不变', () => {
|
||||||
|
const out = roundtrip(fixture, (draft) => {
|
||||||
|
draft.listeners = [{ name: '容器A', type: 'mixed', port: 7778, proxy: 'AUTO' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 这是这个项目最核心的操作,也是取信于用户的底线:只应多出新段落,别的一行都不许动。
|
||||||
|
const before = fixture.split('\n')
|
||||||
|
const after = out.split('\n')
|
||||||
|
expect(after.slice(0, before.length - 1)).toEqual(before.slice(0, before.length - 1))
|
||||||
|
expect(out).toContain('listeners:')
|
||||||
|
expect(out).toContain('port: 7778')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('改动某个段落时,不碰其它段落的注释和写法', () => {
|
||||||
|
const out = roundtrip(fixture, (draft) => {
|
||||||
|
const rules = draft.rules as string[]
|
||||||
|
rules[rules.length - 1] = 'MATCH,AUTO'
|
||||||
|
})
|
||||||
|
|
||||||
|
// rules 之外的段落必须原封不动,含对齐空格和流式数组。
|
||||||
|
expect(out).toContain('mixed-port: 7777 # HTTP 和 SOCKS5 共用这一个端口')
|
||||||
|
expect(out).toContain(' - name: PROXY # 总开关,手动选;默认跟着 AUTO 走')
|
||||||
|
expect(out).toContain(' proxies: [AUTO, DIRECT]')
|
||||||
|
expect(out).toContain(' interval: 86400 # 每 24 小时自动拉取一次订阅')
|
||||||
|
expect(out).toContain('MATCH,AUTO')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('被改动的段落内部注释会丢失,这是已知缺陷(见 ADR 0002)', () => {
|
||||||
|
const out = roundtrip(fixture, (draft) => {
|
||||||
|
const rules = draft.rules as string[]
|
||||||
|
rules[rules.length - 1] = 'MATCH,AUTO'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 记录现状而非期望。将来若实现了段落内的就地修改,这条断言应当反转。
|
||||||
|
expect(out).not.toContain('# 本机和内网地址一律直连')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('restoreInlineCommentAlignment', () => {
|
||||||
|
it('内容与注释都相同时还原原本的对齐', () => {
|
||||||
|
const source = 'a: 1 # 说明\n'
|
||||||
|
const output = 'a: 1 # 说明\n'
|
||||||
|
expect(restoreInlineCommentAlignment(source, output)).toBe(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('注释文字变了就不还原,避免掩盖真实改动', () => {
|
||||||
|
const source = 'a: 1 # 旧说明\n'
|
||||||
|
const output = 'a: 1 # 新说明\n'
|
||||||
|
expect(restoreInlineCommentAlignment(source, output)).toBe(output)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('值变了就不还原', () => {
|
||||||
|
const source = 'a: 1 # 说明\n'
|
||||||
|
const output = 'a: 2 # 说明\n'
|
||||||
|
expect(restoreInlineCommentAlignment(source, output)).toBe(output)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('相同的行出现多次时按顺序依次配对', () => {
|
||||||
|
const source = 'a: 1 # 同\nb: 2\na: 1 # 同\n'
|
||||||
|
const output = 'a: 1 # 同\nb: 2\na: 1 # 同\n'
|
||||||
|
expect(restoreInlineCommentAlignment(source, output)).toBe(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('不把字符串里的井号当成注释', () => {
|
||||||
|
const line = 'url: "http://example.com/a#b"\n'
|
||||||
|
expect(restoreInlineCommentAlignment(line, line)).toBe(line)
|
||||||
|
})
|
||||||
|
})
|
||||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["bun", "vite/client"],
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
|
"jsx": "preserve",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./web/*"],
|
||||||
|
"@config-editor": ["./vendor/config-editor/index.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["server/**/*.ts", "web/**/*.ts", "web/**/*.vue", "vendor/**/*.ts", "*.ts"]
|
||||||
|
}
|
||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# config-editor 的来源
|
||||||
|
|
||||||
|
复制自 **metacubexd**(GitHub `MetaCubeX/metacubexd`,MIT 许可),路径 `packages/config-editor/src/`,取自版本 **1.271.0**。
|
||||||
|
|
||||||
|
采用它的理由和已知缺陷记在决策记录 0002。决策记录**不在本仓库里**,它们放在仓库外面的工作区目录 `docs/adr/`(与 `patchbay/` 平级)。
|
||||||
|
|
||||||
|
## 本地改动
|
||||||
|
|
||||||
|
对照上游逐条记录,方便将来同步上游修复时区分「我们改的」和「上游变的」。
|
||||||
|
|
||||||
|
| 文件 | 改动 | 原因 |
|
||||||
|
|---|---|---|
|
||||||
|
| `index.test.ts` | 测试框架的导入从 `vitest` 换成 `bun:test` | 本项目用 Bun 自带的测试运行器,不引入 vitest。两者的 `describe` / `it` / `expect` 接口一致,用例本身一行没动 |
|
||||||
|
| `index.ts` | `stringifyTouched` 结尾的 `String(doc)` 换成 `doc.toString({ flowCollectionPadding: false, lineWidth: 0 })` | 上游用默认输出参数,会把 `[a, b]` 改写成 `[ a, b ]`,并且在超过 80 列时折行——目标机器的订阅地址就够长。这两样都会让没被改动的段落出现在 diff 里 |
|
||||||
|
|
||||||
|
## 配套的本项目代码
|
||||||
|
|
||||||
|
`shared/yaml-cosmetics.ts` 里的 `restoreInlineCommentAlignment` 不属于上游,是本项目补的一步。`yaml` 库把行尾注释的文字存在节点上,但不存注释前的对齐空格,所以重新输出时 `interval: 86400 # 说明` 会变成 `interval: 86400 # 说明`。那个函数在写回之后逐行比对,把只差对齐空格的行还原成原文。
|
||||||
|
|
||||||
|
加上这两处之后的实测结果记在决策记录 0002 的「实测」一节。要自己重跑一遍:`bun run scripts/roundtrip.ts <配置文件> <输出目录>`,再拿输出跟源文件比对。
|
||||||
|
|
||||||
|
## 同步上游
|
||||||
|
|
||||||
|
上游修了 bug 想同步时:拉取 metacubexd 对应版本的 `packages/config-editor/src/index.ts`,与本目录的同名文件比对,把上表里的本地改动重新应用一遍。
|
||||||
Vendored
+197
@@ -0,0 +1,197 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test'
|
||||||
|
import {
|
||||||
|
applyPatch,
|
||||||
|
ConfigDocumentError,
|
||||||
|
diffDocument,
|
||||||
|
findResourceReferences,
|
||||||
|
openDocument,
|
||||||
|
splitRuleFields,
|
||||||
|
validateDocument,
|
||||||
|
} from './index'
|
||||||
|
|
||||||
|
describe('config editor document', () => {
|
||||||
|
it('rejects invalid YAML and non-mapping roots', () => {
|
||||||
|
expect(() => openDocument('dns: [')).toThrow(ConfigDocumentError)
|
||||||
|
expect(() => openDocument('- one\n- two\n')).toThrow(ConfigDocumentError)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves untouched section comments while replacing touched sections', () => {
|
||||||
|
const base = `# header\nmode: rule # keep\ndns:\n enable: true\n`
|
||||||
|
const draft = `mode: rule\ndns:\n enable: false\n nameserver: [1.1.1.1]\n`
|
||||||
|
const result = applyPatch(base, diffDocument(base, draft))
|
||||||
|
expect(result.conflicts).toEqual([])
|
||||||
|
expect(result.document.yaml).toContain('# header')
|
||||||
|
expect(result.document.yaml).toContain('mode: rule # keep')
|
||||||
|
expect(result.document.data.dns).toEqual({
|
||||||
|
enable: false,
|
||||||
|
nameserver: ['1.1.1.1'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('upserts, deletes, and reorders named resources', () => {
|
||||||
|
const base = `proxies:\n - { name: a, type: ss, server: old }\n - { name: b, type: direct }\n`
|
||||||
|
const draft = `proxies:\n - { name: c, type: direct }\n - { name: a, type: ss, server: new }\n`
|
||||||
|
const patch = diffDocument(base, draft)
|
||||||
|
expect(patch.operations.map((op) => op.op)).toEqual([
|
||||||
|
'upsert',
|
||||||
|
'delete',
|
||||||
|
'upsert',
|
||||||
|
'order',
|
||||||
|
])
|
||||||
|
const result = applyPatch(base, patch)
|
||||||
|
expect(result.conflicts).toEqual([])
|
||||||
|
expect(result.document.data.proxies).toEqual([
|
||||||
|
{ name: 'c', type: 'direct' },
|
||||||
|
{ name: 'a', type: 'ss', server: 'new' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rebases an independent change and reports an overlapping change', () => {
|
||||||
|
const base = `proxies:\n - { name: a, type: ss, server: one }\n - { name: b, type: direct }\n`
|
||||||
|
const draft = `proxies:\n - { name: a, type: ss, server: two }\n - { name: b, type: direct }\n`
|
||||||
|
const patch = diffDocument(base, draft)
|
||||||
|
const independent = applyPatch(`${base}dns:\n enable: true\n`, patch)
|
||||||
|
expect(independent.conflicts).toEqual([])
|
||||||
|
|
||||||
|
const changed = applyPatch(
|
||||||
|
`proxies:\n - { name: a, type: ss, server: upstream }\n - { name: b, type: direct }\n`,
|
||||||
|
patch,
|
||||||
|
)
|
||||||
|
expect(changed.conflicts).toHaveLength(1)
|
||||||
|
expect(changed.conflicts[0]?.reason).toBe('changed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates duplicate resources and broken references', () => {
|
||||||
|
const diagnostics = validateDocument({
|
||||||
|
proxies: [
|
||||||
|
{ name: 'same', type: 'direct' },
|
||||||
|
{ name: 'same', type: 'direct' },
|
||||||
|
],
|
||||||
|
'proxy-groups': [{ name: 'group', type: 'select', proxies: ['missing'] }],
|
||||||
|
rules: ['MATCH,missing'],
|
||||||
|
})
|
||||||
|
expect(diagnostics.map((item) => item.code)).toEqual([
|
||||||
|
'duplicate-name',
|
||||||
|
'missing-reference',
|
||||||
|
'missing-reference',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('addresses rules by content identity instead of array index', () => {
|
||||||
|
const base = `rules:\n - DOMAIN,one.test,DIRECT\n - MATCH,DIRECT\n`
|
||||||
|
const draft = `rules:\n - DOMAIN,one.test,REJECT\n - MATCH,DIRECT\n`
|
||||||
|
const patch = diffDocument(base, draft)
|
||||||
|
expect(patch.operations[0]?.op).toBe('sequence-replace')
|
||||||
|
|
||||||
|
const refreshed = `rules:\n - DOMAIN,new.test,DIRECT\n - DOMAIN,one.test,DIRECT\n - MATCH,DIRECT\n`
|
||||||
|
const result = applyPatch(refreshed, patch)
|
||||||
|
expect(result.conflicts).toEqual([])
|
||||||
|
expect(result.document.data.rules).toEqual([
|
||||||
|
'DOMAIN,new.test,DIRECT',
|
||||||
|
'DOMAIN,one.test,REJECT',
|
||||||
|
'MATCH,DIRECT',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports a conflict when a reorder anchor disappeared upstream', () => {
|
||||||
|
const base = `proxies:\n - { name: a, type: direct }\n - { name: b, type: direct }\n`
|
||||||
|
const draft = `proxies:\n - { name: b, type: direct }\n - { name: a, type: direct }\n`
|
||||||
|
const patch = diffDocument(base, draft)
|
||||||
|
const result = applyPatch(
|
||||||
|
`proxies:\n - { name: b, type: direct }\n`,
|
||||||
|
patch,
|
||||||
|
)
|
||||||
|
expect(result.conflicts).toEqual([
|
||||||
|
expect.objectContaining({ reason: 'missing' }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('splits logical and quoted rules only at top-level commas', () => {
|
||||||
|
expect(
|
||||||
|
splitRuleFields(
|
||||||
|
'AND,((DOMAIN,one.test),(NETWORK,UDP)),Proxy Group,no-resolve',
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
'AND',
|
||||||
|
'((DOMAIN,one.test),(NETWORK,UDP))',
|
||||||
|
'Proxy Group',
|
||||||
|
'no-resolve',
|
||||||
|
])
|
||||||
|
expect(splitRuleFields('DOMAIN,"one,two.test",DIRECT')).toEqual([
|
||||||
|
'DOMAIN',
|
||||||
|
'"one,two.test"',
|
||||||
|
'DIRECT',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds named proxy references across routing and network sections', () => {
|
||||||
|
const data = openDocument(`
|
||||||
|
proxies:
|
||||||
|
- { name: chained, type: direct, dialer-proxy: target }
|
||||||
|
proxy-groups:
|
||||||
|
- { name: group, type: select, proxies: [target] }
|
||||||
|
proxy-providers:
|
||||||
|
remote: { type: http, url: https://example.test, proxy: target }
|
||||||
|
rule-providers:
|
||||||
|
rules: { type: http, url: https://example.test, proxy: target }
|
||||||
|
listeners:
|
||||||
|
- { name: inbound, type: mixed, port: 7890, proxy: target }
|
||||||
|
tunnels:
|
||||||
|
- { network: tcp, address: 127.0.0.1:1, target: example.test:2, proxy: target }
|
||||||
|
ntp: { enable: true, dialer-proxy: target }
|
||||||
|
sub-rules:
|
||||||
|
child:
|
||||||
|
- MATCH,target
|
||||||
|
rules:
|
||||||
|
- AND,((DOMAIN,one.test),(NETWORK,UDP)),target
|
||||||
|
- SUB-RULE,(NETWORK,TCP),target
|
||||||
|
`).data
|
||||||
|
expect(
|
||||||
|
findResourceReferences(data, 'target').map((item) => item.kind),
|
||||||
|
).toEqual([
|
||||||
|
'proxy-group-member',
|
||||||
|
'rule-policy',
|
||||||
|
'rule-policy',
|
||||||
|
'dialer-proxy',
|
||||||
|
'dialer-proxy',
|
||||||
|
'provider-proxy',
|
||||||
|
'provider-proxy',
|
||||||
|
'listener-proxy',
|
||||||
|
'tunnel-proxy',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not treat unrelated same-name strings as proxy references', () => {
|
||||||
|
const data = openDocument(`
|
||||||
|
proxies:
|
||||||
|
- { name: target, type: direct, server: target }
|
||||||
|
hosts: { target: 127.0.0.1 }
|
||||||
|
rules:
|
||||||
|
- DOMAIN,target,DIRECT
|
||||||
|
`).data
|
||||||
|
expect(findResourceReferences(data, 'target')).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates missing references in sub-rules and dialer fields', () => {
|
||||||
|
const diagnostics = validateDocument({
|
||||||
|
proxies: [{ name: 'node', type: 'direct', 'dialer-proxy': 'missing' }],
|
||||||
|
'sub-rules': { child: ['MATCH,missing'] },
|
||||||
|
rules: ['SUB-RULE,(NETWORK,TCP),absent', 'MATCH,GLOBAL'],
|
||||||
|
})
|
||||||
|
expect(diagnostics).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
path: ['sub-rules', 'child', 0],
|
||||||
|
severity: 'warning',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
path: ['proxies', 0, 'dialer-proxy'],
|
||||||
|
severity: 'error',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
path: ['rules', 0],
|
||||||
|
message: 'Unknown sub-rule: absent',
|
||||||
|
severity: 'error',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
Vendored
+1084
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user