MagicPanel 开发文档
给要改这份代码的人(人或 Agent)。看完能回答三个问题:要改的东西在哪个文件、改完怎么验证、怎么发到线上。
配置数据本身的结构与改法见 配置文档;给使用者的说明见 使用文档。
目录
一句话认识这个项目
一个自托管的代理订阅配置管理服务。用户维护「节点来源 / 规则来源 / 策略组 / 分流策略 / 引擎平台 / 拓展模块 / 基础配置」,面板把它们编译成 Stash 和 Mihomo 能直接用的 YAML,构建成不可变快照,发布出去,设备按固定订阅地址拉取。
它不碰流量:不开发代理内核,不做中转,只负责”把配置编对并分发”。
跑起来
本地开发
python3 -m venv .venv && .venv/bin/pip install -r server/requirements.txt
npm install
npm run dev
- 前端 http://127.0.0.1:7100,API http://127.0.0.1:8000,Vite 已把
/api和/feed代理过去。 - 第一次访问
/admin会引导创建第一位维护者。 - 后端单独跑:
cd server && ../.venv/bin/python -m uvicorn app.main:app --reload --port 8000。 - 前端产物输出到
server/app/static(已在.gitignore里,不要提交)。
环境变量
| 变量 | 作用 |
|---|---|
MAGICPANEL_DATA_DIR |
数据目录,默认 server/../data |
MAGICPANEL_SCHEDULER=0 |
关闭内置定时任务(测试用) |
MAGICPANEL_SCHEDULER_TICK |
调度器心跳间隔秒数,默认 30 |
数据目录里有什么
data/
magicpanel.db SQLite(主库,WAL 模式)
secret.key Fernet 主密钥,丢了备份就解不开
builds/ 构建产物
bin/mihomo 测速核心
geoip.dat / geosite.dat / country.mmdb
avatars/ 头像
环境与内网地址
仓库和脚本里出现的内网地址都出自 环境与地址速查。一句话版:
- 面板跑在 NAS 上(
10.0.1.2,容器magicpanel),不在开发机上。 - 代理跑在 Mac mini 上(
10.0.1.8,Stash 混合端口7890)——开发机就是这台 Mac mini。 - 因此:面板服务端做的事(抓取订阅、抓规则集)必须用
http://10.0.1.8:7890。 写127.0.0.1会连到 NAS 自己,实测 connection refused,订阅与规则集全部抓不到。 - 在本机执行的脚本(
release.sh/fetch_geo_data.sh)用127.0.0.1:7890或10.0.1.8:7890都行;但deploy.sh的DEPLOY_PROXY是注入 NAS 端构建过程的, 必须用10.0.1.8。
改网络环境(换 IP、换机器)时,先改那份速查文档,再改引用它的地方。
代码地图
server/app/
main.py 应用装配:路由挂载、静态托管、调度器启动
config.py 路径、常量、白名单(引擎标签、规则类型、策略组类型…)
db.py 表结构(SCHEMA)、迁移(_migrate_*)、事务与查询助手
deps.py 依赖注入:当前用户、CSRF 校验
security.py 口令散列、令牌、Fernet 加解密、审计写入
routers/ HTTP 接口,一个业务一个文件
auth.py 登录、登出、改密、头像
policy.py 策略组、分流规则
targets_devices.py 引擎平台、设备
sources.py 节点来源
rule_providers.py 规则来源
local_nodes.py 本地单节点
snippets.py 拓展模块
base_config.py 基础配置(三层)
builds.py 构建、发布、回滚
feed.py 订阅分发
automation.py 自动构建发布与运行记录
portal.py 门户页
deps.py 自主维护(依赖文件库的浏览、编辑、提交、同步)
misc.py 概览、审计、设置、备份、测速核心
services/
base_config.py 三层合并、差异计算、默认配置、参数 schema
fetcher.py 订阅抓取
repo_manager.py 依赖文件库的 Git 操作层(只做 Git,不做业务判断)
repo_sync.py 仓库内容同步:rule/*.yaml → 规则来源
tester.py Mihomo 核心拨测
scheduler.py 定时任务
releases.py 发布与回滚
updater.py 核心安装
webdav.py 远程备份
local_nodes.py 本地节点参数与渲染
direct.py 直连域名、Fake-IP 过滤等伪键
refs.py 引用检查(谁引用了这个)
retention.py 保留策略
compiler/
render.py 产物渲染(合并、渲染目标、设备叠加)
validate.py 产物校验
src/
views/ 一个页面对应一个 .vue
components/ 通用组件(StatusDot、EmptyState、ErrorState…)
api.js 后端接口封装
router.js 路由表
store.js 登录态与全局状态
core.js 通用逻辑
format.js 时间、数字、枚举文案
confirm.js 二次确认
theme.js 主题变量
mobile.js useMobile() 断点判断
useRowSortable.js 拖动排序
scopeOptions.js 「包含所有」哨兵(与后端 config.ALL_SCOPE 对齐)
snippetPresets.js 拓展模块预设
scripts/
deploy.sh 一键发布到 NAS
package.sh 打出可部署的 tar.gz
release.sh 打标签推送、发 GitHub Release
check_secrets.py 凭据扫描(CI + 部署前)
check_engine_defaults.py 引擎默认配置与官方文档对账
check_data_consistency.py 线上数据自检(部署后)
数据怎么流动
节点来源(订阅) ──┐
本地节点 ──┤
规则来源(规则集) ──┤
策略组 ──┼──► collect_snapshot() ──► 编译 ──► 校验 ──► 构建快照
分流规则 ──┤ (builds.py) (compiler/) (compiler/validate)
拓展模块 ──┤
基础配置三层 ──┤
平台扩展 ──┘
│
├──► 平台产物 builds/<id>/<平台>.yaml
└──► 设备产物 builds/<id>/<设备名>.yaml
│
发布 ───────┤
▼
/feed/<令牌>/config.yaml (分发时叠加设备独立配置)
关键点:三层配置的作用域不同。
- 引擎默认配置、引擎平台配置 → 固化在构建快照里,改了必须重新构建发布。
- 设备独立配置 → 分发时实时叠加,改完客户端下次拉取就生效。
改动落点速查
| 想改什么 | 改哪里 |
|---|---|
| 页面上的一个字段、一个按钮 | src/views/<页面>.vue |
| 新增一个页面 | src/views/Xxx.vue + src/router.js 加路由 + src/App.vue 加菜单 |
| 接口地址与参数 | src/api.js |
| 增删改接口 | server/app/routers/<业务>.py |
| 策略组 / 规则的校验规则 | server/app/routers/policy.py + server/app/config.py 的白名单 |
| 产物顶层键(端口、模式、DNS…) | server/app/services/base_config.py 的 SCHEMA 与 PRESETS |
| 引擎默认配置 | server/app/services/base_config.py 的 _COMMON_DEFAULTS / STASH_* / MIHOMO_* |
| 基础配置页的本层改动标记 | src/views/ArtifactConfig.vue(蓝色 = 本层新增,橙色 = 本层覆盖) |
| 产物合并、渲染 | server/app/compiler/render.py |
| 产物校验规则 | server/app/compiler/validate.py |
| 定时任务 | server/app/services/scheduler.py |
| 依赖仓库的 Git 操作 | server/app/services/repo_manager.py(命令封装、排除清单、冲突三方) |
| 仓库内容 → 规则来源 | server/app/services/repo_sync.py |
| 发布与回滚 | server/app/services/releases.py、server/app/routers/builds.py |
| 订阅分发 | server/app/routers/feed.py |
| 订阅内容解析(Clash YAML / base64 URI 列表) | server/app/services/source_nodes.py |
| 删除前的引用检查(含当前生产产物是否还引用它) | server/app/services/refs.py |
| 数据库表结构 | server/app/db.py 的 SCHEMA + init_db() 里加迁移 |
| 适用范围(引擎平台 / 设备 / 节点来源) | 哨兵 config.ALL_SCOPE("*")= 多选里的「包含所有」;空数组 = 不适用任何 |
| 引擎名、平台名、规则类型白名单 | server/app/config.py(RULE_TYPES 管创建,ENGINE_RULE_TYPES 管构建校验) |
| 基础配置预设 | server/app/services/base_config.py 的 PRESETS(经 /api/v1/base-config/schema 下发) |
| 拓展模块预设 | src/snippetPresets.js 的 SNIPPET_PRESETS(纯前端常量,服务端无预设接口) |
| 部署流程 | scripts/deploy.sh、docker-compose.yml、Dockerfile |
| 文档 | docs/,本文件与配置文档优先更新 |
| 文档站 | scripts/build_docs_site.py 生成站点源码,.github/workflows/pages.yml 自动发布 |
数据库
SQLite,WAL 模式,单文件 data/magicpanel.db。
| 表 | 存什么 | 关键点 |
|---|---|---|
users |
维护者账号 | 口令散列、门户 PIN 散列、头像 MIME |
sessions |
登录会话 | 默认 7 天过期 |
sources |
节点来源 | URL 加密存储;抓取快照、测速结果、自动抓取/测速开关 |
rule_providers |
规则来源 | URL 加密存储;远程/内联;行为类型 |
local_nodes |
本地单节点 | 参数加密;协议不同字段不同 |
proxy_groups |
策略组 | targets / devices 是 JSON 数组,["*"] = 全部(哨兵 config.ALL_SCOPE),[] = 不适用任何平台 / 设备 |
rules |
分流规则 | 同上;no_resolve 标记;sort 决定顺序 |
targets |
引擎平台 | (engine, platform) 唯一 |
devices |
设备 | 令牌哈希 + 加密副本;target_id 指向引擎平台 |
base_configs |
基础配置三层 | 主键 (engine, platform, device_id);device_id=0 是引擎平台配置层,platform='' 是引擎默认配置层 |
snippets |
拓展模块 | YAML 片段,标引擎 |
builds |
构建快照 | 产物、校验结果、策略摘要 |
releases |
发布记录 | testing / production 各至多一条生效 |
audit |
审计日志 | 谁在何时改了什么 |
automation_runs |
自动任务运行记录 | 最近 20 条 |
settings |
键值设置 | 见 config.SETTINGS_DEFAULTS |
加字段的固定做法
不要手改线上库,也不要改历史数据。加列 + 写迁移:
# server/app/db.py
def _ensure_column(conn, table, column, decl) -> bool:
... # 已有
# 在 init_db() 里
_ensure_column(conn, "sources", "new_flag", "new_flag INTEGER NOT NULL DEFAULT 0")
# 需要搬历史数据时,再写一个 _migrate_xxx(conn),在 init_db() 末尾调用
def _migrate_xxx(conn) -> None:
conn.execute("UPDATE ... WHERE ...")
迁移必须幂等(重复执行不报错、不重复改数据),因为每次启动都会跑。
后端约定
- 数据库访问只用
server/app/db.py的助手:with db.tx() as conn:写事务db.one(conn, sql, args)/db.all_(conn, sql, args)查db.get_setting(conn, key, default)/db.set_setting(conn, key, value)db.paged(conn, table, where, args, order)分页db.reorder_subset(conn, table, ids)排序(只改 subset 内的顺序)db.insert_unique(conn, sql, args, message)唯一冲突转成友好报错- 注意
one/all_的参数是元组,不是散参。
- 接口:
@router.get/post/put/delete,依赖用CurrentUser,写操作靠deps.py的 CSRF 校验。返回 JSON,错误用HTTPException。 - 列表接口:一律分页(
db.paged)。要加搜索/筛选就在同一接口上加查询参数, 把条件拼进where与args;返回值额外带上未筛选时的总数(total_all), 界面据此写”筛选出 N 条,共 M 条”。 - 抓取失败要留旧数据:抓取失败只更新
last_status/last_error, 绝不覆盖content(上一次抓到的内容)。构建与分发照旧用旧内容, 客户端不会突然少规则。定时抓取的失败重试在services/scheduler.py的_fetch_with_retry()(默认 3 次)。 - 加密:敏感值(订阅 URL、节点参数、令牌、备份密码)用
security.encrypt_payload存密文,需要展示时再解密;口令用security.hash_password。 - 审计:写操作后调
security.audit(conn, actor, action, object_type, object_id, detail)。 - 命名:层级的名字只有三个——引擎默认配置 / 引擎平台配置 / 设备独立配置,代码、注释、界面、文档都要一致。
基础配置的比较基线
三层各自只存”相对基线的差异”,基线怎么算决定了差异怎么算:
- 引擎默认配置层没有上层可比,基线 = 程序内置的默认值(
base_config_service.default_doc(engine))。 - 引擎平台配置 / 设备独立配置的基线 = 上层合并后的生效值。
- 基线由
routers/base_config.py的_baseline()给出;GET /api/v1/base-config、PUT /api/v1/base-config、POST /api/v1/base-config/reset三个接口都返回inherited字段(= 本层的比较基线)。 PUT /api/v1/base-config与POST /api/v1/base-config/apply都按这个基线求差异,所以「保存并构建发布」只会把改动过的键写进覆盖记录,没改的键不落库。
前端约定
- Vue 3
<script setup>+ Element Plus,Vite 构建,产物进server/app/static。 - 一个页面对应
src/views/下的一个文件;共用组件放src/components/。 - 按钮配色有统一约定,新增按钮照着来:
| 语义 | 写法 |
|---|---|
| 新建 / 保存 | type="primary"(实心蓝) |
| 触发一次动作 | type="primary" plain |
| 检测 / 校验 | type="success" plain |
| 发布 | type="success" |
| 查看 | type="info" plain |
| 撤销 / 警告 | type="warning" plain |
| 删除 | type="danger" plain |
| 中性(编辑、排序、取消、批量修改) | 不写 type |
- 窄屏适配用
useMobile()判断;表格在窄屏要固定操作列、隐藏次要列,别让按钮挤成竖排。 - 抽屉里的标题行不要把名字和按钮塞进同一个不换行的 flex——名字加
white-space: nowrap,按钮窄屏独占一行。 - 基础配置页以颜色区分本层改动:蓝色 = 本层新增(基线里没有这个键),橙色 = 本层覆盖(基线里有该键但值不同)。基线 = 上层生效值;引擎默认配置层的基线是程序内置默认值(
GET /api/v1/base-config返回的inherited字段)。改动直接标在表里,页面没有单独的「当前层级改动」小节。 - 改完前端记得
npm run build,产物在server/app/static。
改完怎么验证
cd server && ../.venv/bin/python -m pytest tests -q # 后端测试,应全绿
npm run build # 前端能构建
.venv/bin/python scripts/check_secrets.py # 凭据扫描
.venv/bin/python scripts/check_engine_defaults.py # 引擎默认配置与官方文档对账
线上数据的自检在部署脚本里自动跑(scripts/check_data_consistency.py),也可以手动对线上库跑:
scp scripts/check_data_consistency.py archon@10.0.1.2:/tmp/check_data.py
ssh archon@10.0.1.2 "docker cp /tmp/check_data.py magicpanel:/tmp/check_data.py && \
docker exec magicpanel python3 /tmp/check_data.py /app/data/magicpanel.db"
改了配置层或引擎默认配置,要重新构建发布才生效;只改代码(页面、接口、逻辑)则部署后立刻可见。
怎么发到线上
DEPLOY_PROXY=http://10.0.1.8:7890 bash scripts/deploy.sh # 带测试
DEPLOY_PROXY=http://10.0.1.8:7890 bash scripts/deploy.sh --fast # 只改前端时跳过测试
DEPLOY_PROXY 指向 Mac mini 上的 Stash 混合端口(见环境与地址速查)。
它是通过 ssh 注入到 NAS 端 docker compose build 的(构建时要拉 npm/pip 包),
所以必须写 Mac mini 的内网地址 10.0.1.8——写 127.0.0.1 会让 NAS 连自己、拉包失败。
脚本依次做:跑后端测试 → 同步工作区到 NAS → 重建容器 → 等健康检查 → 跑数据自检 → 验域名。任何一步不过就停。
发完还要发一版订阅:
# 后台「构建发布 → 创建构建 → 发布至测试 → 发布至生产」
# 或用接口
curl -X POST https://magichub.top:8443/api/v1/builds -H 'Content-Type: application/json' \
-H "Cookie: mp_session=..." -H "X-CSRF-Token: ..." \
-d '{"note":"说明这次改了什么"}'
验证线上:
curl -s https://magichub.top:8443/api/v1/setup/status # 面板活着
ssh archon@10.0.1.2 "docker logs magicpanel --tail 50" # 看日志
回滚:面板「构建发布」里「回滚至生产」;代码问题用 git revert + 重新部署。
打包与发布版本
./scripts/package.sh # dist/magicpanel-<时间戳>.tar.gz
./scripts/release.sh 1.2.0 # 打标签推送 + GitHub Release
硬性约束
这几条是设计决定,破坏它们会直接让客户端加载失败。完整清单见仓库根目录的 AGENTS.md,最关键的四条:
- 结构化内容不能被配置层覆盖。
proxies/proxy-providers/proxy-groups/rule-providers/rules这五个键,引擎默认配置 / 引擎平台配置 / 设备独立配置、平台扩展、拓展模块都不许写。服务端硬校验,不是提示。 - 兜底规则(
MATCH/FINAL)永远在最后。 规则按sort排序,兜底之后不能有规则。 - 列表的合并语义分两种:引擎默认配置 / 引擎平台配置 = 整体替换;设备独立配置 = 追加。差异计算、层级合并、分发叠加三处必须一致。
- 设备独立配置的键跟着设备的引擎平台走。 设备换平台、平台改引擎时要自动换键,否则配置静默失效。
常见改动配方
加一个新的产物顶层键
server/app/services/base_config.py的SCHEMA里加字段(类型、说明、分组、默认值)。- 需要默认开启的,加进
_COMMON_DEFAULTS或引擎专属的STASH_*/MIHOMO_*。 - 跑
scripts/check_engine_defaults.py——每个默认键都要能在official-example/里找到出处。 - 界面自动出现该参数(表单是 schema 驱动的)。
加一个拓展模块预设
src/snippetPresets.js的SNIPPET_PRESETS里加一条(key / name / category / engine / note / content)。- 预设内容不许出现结构化内容键。
基础配置的预设是另一套,在
server/app/services/base_config.py的PRESETS,不要混。
加一条数据自检规则
scripts/check_data_consistency.py 里加一段:查出问题写 problems(阻断部署),只是提示写 notes(不阻断)。规则集合、策略组、规则、设备的引用完整性、命名、冗余覆盖都从这里管。
加一个页面
src/views/Xxx.vue。src/router.js加路由,src/App.vue加菜单项。- 后端接口放
server/app/routers/下合适的文件。 - 窄屏用
useMobile()处理表格与操作列。
改产物结构(合并顺序、渲染逻辑)
server/app/compiler/render.py。改完必须跑构建并逐个目标比对产物,Mihomo 目标还会跑 mihomo -t 校验。
遇到问题先看哪里
| 现象 | 先看 |
|---|---|
| 客户端加载失败 | 构建详情里的校验错误;server/app/compiler/validate.py |
| 产物内容不对 | 构建详情的「预览产物」;render.py 的合并顺序 |
| 订阅 404 | 令牌被轮换或设备被删;server/app/routers/feed.py |
| 测速结果不准 | 系统状态页的「测速链路」;容器代理变量 |
| 端口占用 / 启动失败 | docker logs magicpanel --tail 50 |
| 定时任务没跑 | 自动构建发布页的开关与运行记录;scheduler.py |