1panel-toolkit 0.3.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/CHANGELOG.md +80 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/PKG-INFO +31 -2
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/README.md +30 -1
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/pyproject.toml +1 -1
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/skill/SKILL.md +3 -1
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/__init__.py +1 -1
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/compose.py +16 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/cli.py +92 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/gen.py +15 -2
- 1panel_toolkit-0.5.0/src/p1toolkit/recipes.py +253 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/resolve/__init__.py +143 -64
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/resolve/analyze.py +61 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/__init__.py +19 -0
- 1panel_toolkit-0.5.0/src/p1toolkit/rules/data/recipes.yaml +59 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/selftest.py +34 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_checks.py +28 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_gen.py +45 -0
- 1panel_toolkit-0.5.0/tests/test_recipes.py +182 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_resolve.py +96 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_selftest.py +1 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/.gitignore +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/LICENSE +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/docs/generation-depth.md +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/docs/patterns.md +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/examples/demo-db-spec.json +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/examples/demo-spec.json +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/__init__.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/forms.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/hygiene.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/i18n.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/lessons.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/structure.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/checks/substance.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/config.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/dependencies.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/deploy.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/depth.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/errors.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/evidence.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/fix.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/lifecycle.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/loader.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/logo.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/model.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/net.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/panel.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/patterns/__init__.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/patterns/catalog.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/patterns/data/patterns.json +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/patterns/extract.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/remote.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/report.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/resolve/sources.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/decisions.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/dependencies.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/envkeys.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/form-design.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/i18n.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/lessons.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/src/p1toolkit/rules/data/store.yaml +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/conftest.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_config.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_corpus.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_dependencies.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_deploy.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_depth.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_evidence.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_fix.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_forms.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_github_source.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_lifecycle.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_net.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_panel.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_patterns.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_regressions.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_remote.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_remote_cli.py +0 -0
- {1panel_toolkit-0.3.2 → 1panel_toolkit-0.5.0}/tests/test_rules_data.py +0 -0
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-09-20
|
|
4
|
+
|
|
5
|
+
配方(recipes):把"一类应用的成套写法"变成一条命令。
|
|
6
|
+
|
|
7
|
+
### 配方是什么,不是什么
|
|
8
|
+
|
|
9
|
+
配方只填空的那部分是**结构**——面板变量到应用变量怎么映射、选择器什么形状、
|
|
10
|
+
哪些变量必须由包自己声明。应用侧的变量名永远来自 `p1 resolve` 的
|
|
11
|
+
`dependency_hints` 或显式的 `--bind`,配方不发明变量名,也不替人决定要不要接依赖。
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
p1 resolve <仓库> --out ./out/app # 1. 上游证据 + D10 线索
|
|
15
|
+
p1 recipe apply panel-postgres ./out/app/draft-spec.json --write # 2. 一键接线
|
|
16
|
+
p1 gen ./out/app/draft-spec.json --check # 3. 生成 + 校验
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `p1 recipe list` / `p1 recipe show <id>`:三条配方——`panel-postgres`(PostgreSQL /
|
|
20
|
+
MySQL / MariaDB,两步式选择器)、`panel-redis`、`panel-db-and-cache`(复用前两条,
|
|
21
|
+
角色表只有一份)。数据在 `rules/data/recipes.yaml`。
|
|
22
|
+
- `p1 recipe apply <id> <spec>` 默认只预览,`--write` 写回 spec;`--bind <角色>=<变量>`
|
|
23
|
+
补上 hints 没覆盖的角色,`--service <名字>` 指定接到哪个服务上。
|
|
24
|
+
- `require: [host|url]` 表示"二选一即可":一个连接串本身就带了主机、端口、库名和凭据,
|
|
25
|
+
这时不该再要求单独的 HOST。
|
|
26
|
+
- 重复 apply 不会产生重复依赖(同 kind 的旧接线被替换)。
|
|
27
|
+
|
|
28
|
+
### 顺带修掉的 hints 陷阱
|
|
29
|
+
|
|
30
|
+
原来 `dependency_hints.keys` 把所有 URL 混在一个 `url` 角色里,于是
|
|
31
|
+
`REDIS_URL` 会被 `panel-postgres` 绑到 Postgres 的 URL 模板上——包能生成,
|
|
32
|
+
装出来连的是错的库。现在 hints 增加了 `subjects`(按依赖主体分组:
|
|
33
|
+
`postgresql` / `redis` / `generic-db`…),配方只绑同一 family 的变量;
|
|
34
|
+
`DB_HOST` 这类泛型变量留在 `generic-db` 里,同时可供任何 DB 引擎复用。
|
|
35
|
+
线索的 note 也会直接给出该跑哪条配方。
|
|
36
|
+
|
|
37
|
+
### 验证
|
|
38
|
+
|
|
39
|
+
- 实测链路:对一个带 `DATABASE_URL` / `DB_HOST` / `REDIS_URL` 的检出跑
|
|
40
|
+
`resolve` → `recipe apply panel-postgres --write` → `gen --check` → `0 fail`,
|
|
41
|
+
表单里出现两步式选择器与 `PANEL_DB_*` 字段,compose 里是字面接线。
|
|
42
|
+
- 新增 12 条测试(绑定、跨主体不串味、缺失必需角色、`--bind`、幂等、
|
|
43
|
+
组合配方、CLI 预览/写回/未知配方、配方生成的包含法校验),`pytest` 277 passed,
|
|
44
|
+
`p1 selftest` 9/9。
|
|
45
|
+
|
|
46
|
+
## 0.4.0 — 2026-09-20
|
|
47
|
+
|
|
48
|
+
多服务包:draft spec 从"只有主服务"变成"compose 里每个服务各一条"。
|
|
49
|
+
|
|
50
|
+
### 为什么
|
|
51
|
+
|
|
52
|
+
0.3.1 修了挂载点跨服务错配,但 `image` / `container_port` / `env` 仍然是跨服务聚合的:
|
|
53
|
+
`draft_spec` 取 `images[0]`、`ports[0]`,并把所有服务的环境变量合并成一份。一个
|
|
54
|
+
"应用 + 自带数据库"的 compose 会因此生成**一个** spec service,里面塞着 sidecar 的
|
|
55
|
+
镜像、端口、甚至数据库密码。openviking(openviking + caddy)实测就是这种形态:
|
|
56
|
+
caddy 的 `OV_ACME_EMAIL` 会出现在主服务上,反过来 caddy 只拿到自己的变量。
|
|
57
|
+
|
|
58
|
+
### 改了什么
|
|
59
|
+
|
|
60
|
+
- 新增 `analyze.service_facts()`:按服务取出镜像、端口、bind 挂载、环境变量、
|
|
61
|
+
`depends_on`、运行时字段(security_opt / user / devices …)与 healthcheck。
|
|
62
|
+
- `draft_spec` 为每个服务生成一条 spec(compose 顺序,第一条仍是主服务、拿
|
|
63
|
+
`PANEL_APP_PORT_HTTP`,其余服务的容器名带后缀)。Dockerfile 里的变量只归主服务,
|
|
64
|
+
且不会重复落到已经声明过它的服务上;sidecar 的凭据不会被并进应用服务。
|
|
65
|
+
- 多服务时加一条 note:这些服务已按归属写进 draft,如果面板上已有对应应用
|
|
66
|
+
(postgresql / redis / mysql),考虑改用 `dependencies` 走面板托管(D10)。
|
|
67
|
+
- **healthcheck**:上游的 healthcheck 块(`test` / `interval` / …)会原样带进 spec 和
|
|
68
|
+
生成的 compose;上游只有"存在 healthcheck"这一事实时,draft 只写一条提示,
|
|
69
|
+
不再输出 `healthcheck: true`——那不是定义,Docker 会直接解析失败。新增规则
|
|
70
|
+
**C005** 拦住手写的非映射 healthcheck。
|
|
71
|
+
- 表单字段顺序:端口字段统一排在前面,多服务时不会被另一个服务的变量隔开。
|
|
72
|
+
|
|
73
|
+
### 实测
|
|
74
|
+
|
|
75
|
+
- openviking(openviking + caddy):draft 从 1 条(镜像/端口/挂载/变量全混)变成
|
|
76
|
+
2 条各自正确——openviking 拿到自己的镜像、1933、`/app/.openviking`、
|
|
77
|
+
Dockerfile 变量与真实 healthcheck 块;caddy 拿到 `caddy:2`、1934、
|
|
78
|
+
`./Caddyfile:/etc/caddy/Caddyfile`、`depends_on: [openviking]` 与自己的变量。
|
|
79
|
+
`p1 gen --check` → `0 fail`。
|
|
80
|
+
- 新增 9 条测试(多服务归属、sidecar 凭据不串味、healthcheck 块 vs 标志、
|
|
81
|
+
C005、多服务生成),`pytest` 265 passed。
|
|
82
|
+
|
|
3
83
|
## 0.3.2 — 2026-09-20
|
|
4
84
|
|
|
5
85
|
`p1 depth` 的判定阈值校准。用 `p1 fix` 收敛用户自己的四个包时发现的误判。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: 1panel-toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Generate, check, fix and runtime-verify 1Panel app packages from a single spec.
|
|
5
5
|
Author: idkan
|
|
6
6
|
License: MIT License
|
|
@@ -87,6 +87,8 @@ p1 remote smoke <包目录> --ports 10000 # 上传→起容器→探针→
|
|
|
87
87
|
p1 gen examples/demo-spec.json --out ./out --check # 从 spec 生成包并立即校验
|
|
88
88
|
p1 gen examples/demo-db-spec.json --out ./out --check # 含面板数据库依赖与 init.sh 的示例
|
|
89
89
|
p1 gen ./out/app/draft-spec.json --allow-unresolved # 明知缺证据也要生成(会记账)
|
|
90
|
+
p1 recipe list # 三条"成套写法":面板 DB / Redis / 两者
|
|
91
|
+
p1 recipe apply panel-postgres ./out/app/draft-spec.json --write # 一键接线
|
|
90
92
|
p1 deploy ./out/p1-demo --param PANEL_APP_PORT_HTTP=15000 # 走面板 API 正式安装
|
|
91
93
|
p1 fix <包目录> --check # 把 check 的结果幂等修掉
|
|
92
94
|
p1 selftest # 验收:这个安装能不能用
|
|
@@ -105,7 +107,8 @@ p1 selftest # 验收:这个安装能不能用
|
|
|
105
107
|
`values[].label` 必须是纯字符串、bool label 必须加引号。
|
|
106
108
|
|
|
107
109
|
规则组 `C0xx` 管 compose:顶层 `version:`、image 引号、`${VAR}` 与表单 envKey
|
|
108
|
-
闭合、`createdBy` 标签、端口 envKey 前缀、外部网络、同名服务 DNS
|
|
110
|
+
闭合、`createdBy` 标签、端口 envKey 前缀、外部网络、同名服务 DNS 冲突、
|
|
111
|
+
`healthcheck` 必须是映射(`C005`,`healthcheck: true` 会让 Docker 直接解析失败)。
|
|
109
112
|
|
|
110
113
|
规则组 `H0xx` 管文件卫生:LF/BOM、README store 风格、`.env.sample` 闭合。
|
|
111
114
|
|
|
@@ -175,6 +178,11 @@ README,抽取出端口、挂载点、环境变量、运行用户、额外运
|
|
|
175
178
|
落进 draft spec——人工确认过的值必须能到达生成器,而不是停在报告里。产出三份文件:
|
|
176
179
|
`decision-report.md`、`draft-spec.json`、`resolve.json`。
|
|
177
180
|
|
|
181
|
+
**多服务 compose 按服务归属**:compose 里每个服务在 draft spec 里各占一条,镜像、容器端口、
|
|
182
|
+
bind 挂载、环境变量、`depends_on`、运行时字段都只跟着自己的服务走。Dockerfile 里的变量归
|
|
183
|
+
主服务;sidecar(自带的数据库/缓存)的密码不会被并到应用服务上,反过来也一样。多服务时还会给
|
|
184
|
+
一条提示:如果面板上已经有对应的应用,考虑改用 `dependencies` 走面板托管(D10)。
|
|
185
|
+
|
|
178
186
|
实测:`halo-dev/halo` 的挂载点在 Dockerfile 里没有 `VOLUME`,但 `HALO_WORK_DIR`
|
|
179
187
|
默认值是 `/root/.halo2`;镜像只在 README 里出现——它把两条都标成 `inferred`
|
|
180
188
|
并给出出处,而不是停下来或者瞎猜。
|
|
@@ -250,6 +258,9 @@ p1 fix <包目录> --check # 改完立刻重新校验
|
|
|
250
258
|
| openviking | 1 fail + 6 warn | **0 fail + 1 warn** | 8 处 |
|
|
251
259
|
| xiaozhi-esp32-server | 1 fail + 13 warn | **0 fail + 1 warn** | 14 处 |
|
|
252
260
|
|
|
261
|
+
(2026-09-20 在真实包上的实测记录;这些包随后已就地修复,
|
|
262
|
+
`xiaozhi-esp32-server-full` 现在是 `0 fail / 0 warn`。)
|
|
263
|
+
|
|
253
264
|
静态校验和生成的 YAML 都不能告诉你"容器起不起得来、端口通不通、应用找不找得到配置文件"。
|
|
254
265
|
|
|
255
266
|
```bash
|
|
@@ -382,6 +393,24 @@ p1 gen ./out/app/draft-spec.json --allow-unresolved
|
|
|
382
393
|
这两件事同样受证据约束:声明 `user` / `data_owner` 却拿不出 D07 证据、带了依赖而
|
|
383
394
|
D10 说"未检测到"、带了脚本而 D09 无证据——都会在证据审计里被点名(lesson L004)。
|
|
384
395
|
|
|
396
|
+
### 配方(recipes):把成套写法变成一条命令
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
p1 resolve <仓库> --out ./out/app # 上游证据 + D10 线索
|
|
400
|
+
p1 recipe apply panel-postgres ./out/app/draft-spec.json --write # 一键接线
|
|
401
|
+
p1 gen ./out/app/draft-spec.json --check # 生成 + 校验
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
配方只填空的那部分是**结构**:面板变量怎么映射到应用变量、选择器什么形状、哪些变量得由
|
|
405
|
+
包自己声明。应用侧的变量名一律来自 `resolve` 的 `dependency_hints`(或 `--bind` 显式给出),
|
|
406
|
+
配方不发明变量名,也不替人决定要不要接依赖。
|
|
407
|
+
|
|
408
|
+
三条内置配方:`panel-postgres`(PostgreSQL / MySQL / MariaDB,两步式选择器)、
|
|
409
|
+
`panel-redis`、`panel-db-and-cache`(复用前两条)。`p1 recipe show <id>` 展开角色表;
|
|
410
|
+
`p1 recipe apply` 默认只预览,加 `--write` 写回 spec;`--bind host=DB_HOST` 补 hints 没覆盖的
|
|
411
|
+
角色。hints 的 `subjects` 会区分 `postgresql` 与 `redis` 这类**依赖主体**,所以
|
|
412
|
+
`REDIS_URL` 不会被绑到 Postgres 的连接串模板上。
|
|
413
|
+
|
|
385
414
|
## 当前状态
|
|
386
415
|
|
|
387
416
|
完整流程已经闭环:
|
|
@@ -53,6 +53,8 @@ p1 remote smoke <包目录> --ports 10000 # 上传→起容器→探针→
|
|
|
53
53
|
p1 gen examples/demo-spec.json --out ./out --check # 从 spec 生成包并立即校验
|
|
54
54
|
p1 gen examples/demo-db-spec.json --out ./out --check # 含面板数据库依赖与 init.sh 的示例
|
|
55
55
|
p1 gen ./out/app/draft-spec.json --allow-unresolved # 明知缺证据也要生成(会记账)
|
|
56
|
+
p1 recipe list # 三条"成套写法":面板 DB / Redis / 两者
|
|
57
|
+
p1 recipe apply panel-postgres ./out/app/draft-spec.json --write # 一键接线
|
|
56
58
|
p1 deploy ./out/p1-demo --param PANEL_APP_PORT_HTTP=15000 # 走面板 API 正式安装
|
|
57
59
|
p1 fix <包目录> --check # 把 check 的结果幂等修掉
|
|
58
60
|
p1 selftest # 验收:这个安装能不能用
|
|
@@ -71,7 +73,8 @@ p1 selftest # 验收:这个安装能不能用
|
|
|
71
73
|
`values[].label` 必须是纯字符串、bool label 必须加引号。
|
|
72
74
|
|
|
73
75
|
规则组 `C0xx` 管 compose:顶层 `version:`、image 引号、`${VAR}` 与表单 envKey
|
|
74
|
-
闭合、`createdBy` 标签、端口 envKey 前缀、外部网络、同名服务 DNS
|
|
76
|
+
闭合、`createdBy` 标签、端口 envKey 前缀、外部网络、同名服务 DNS 冲突、
|
|
77
|
+
`healthcheck` 必须是映射(`C005`,`healthcheck: true` 会让 Docker 直接解析失败)。
|
|
75
78
|
|
|
76
79
|
规则组 `H0xx` 管文件卫生:LF/BOM、README store 风格、`.env.sample` 闭合。
|
|
77
80
|
|
|
@@ -141,6 +144,11 @@ README,抽取出端口、挂载点、环境变量、运行用户、额外运
|
|
|
141
144
|
落进 draft spec——人工确认过的值必须能到达生成器,而不是停在报告里。产出三份文件:
|
|
142
145
|
`decision-report.md`、`draft-spec.json`、`resolve.json`。
|
|
143
146
|
|
|
147
|
+
**多服务 compose 按服务归属**:compose 里每个服务在 draft spec 里各占一条,镜像、容器端口、
|
|
148
|
+
bind 挂载、环境变量、`depends_on`、运行时字段都只跟着自己的服务走。Dockerfile 里的变量归
|
|
149
|
+
主服务;sidecar(自带的数据库/缓存)的密码不会被并到应用服务上,反过来也一样。多服务时还会给
|
|
150
|
+
一条提示:如果面板上已经有对应的应用,考虑改用 `dependencies` 走面板托管(D10)。
|
|
151
|
+
|
|
144
152
|
实测:`halo-dev/halo` 的挂载点在 Dockerfile 里没有 `VOLUME`,但 `HALO_WORK_DIR`
|
|
145
153
|
默认值是 `/root/.halo2`;镜像只在 README 里出现——它把两条都标成 `inferred`
|
|
146
154
|
并给出出处,而不是停下来或者瞎猜。
|
|
@@ -216,6 +224,9 @@ p1 fix <包目录> --check # 改完立刻重新校验
|
|
|
216
224
|
| openviking | 1 fail + 6 warn | **0 fail + 1 warn** | 8 处 |
|
|
217
225
|
| xiaozhi-esp32-server | 1 fail + 13 warn | **0 fail + 1 warn** | 14 处 |
|
|
218
226
|
|
|
227
|
+
(2026-09-20 在真实包上的实测记录;这些包随后已就地修复,
|
|
228
|
+
`xiaozhi-esp32-server-full` 现在是 `0 fail / 0 warn`。)
|
|
229
|
+
|
|
219
230
|
静态校验和生成的 YAML 都不能告诉你"容器起不起得来、端口通不通、应用找不找得到配置文件"。
|
|
220
231
|
|
|
221
232
|
```bash
|
|
@@ -348,6 +359,24 @@ p1 gen ./out/app/draft-spec.json --allow-unresolved
|
|
|
348
359
|
这两件事同样受证据约束:声明 `user` / `data_owner` 却拿不出 D07 证据、带了依赖而
|
|
349
360
|
D10 说"未检测到"、带了脚本而 D09 无证据——都会在证据审计里被点名(lesson L004)。
|
|
350
361
|
|
|
362
|
+
### 配方(recipes):把成套写法变成一条命令
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
p1 resolve <仓库> --out ./out/app # 上游证据 + D10 线索
|
|
366
|
+
p1 recipe apply panel-postgres ./out/app/draft-spec.json --write # 一键接线
|
|
367
|
+
p1 gen ./out/app/draft-spec.json --check # 生成 + 校验
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
配方只填空的那部分是**结构**:面板变量怎么映射到应用变量、选择器什么形状、哪些变量得由
|
|
371
|
+
包自己声明。应用侧的变量名一律来自 `resolve` 的 `dependency_hints`(或 `--bind` 显式给出),
|
|
372
|
+
配方不发明变量名,也不替人决定要不要接依赖。
|
|
373
|
+
|
|
374
|
+
三条内置配方:`panel-postgres`(PostgreSQL / MySQL / MariaDB,两步式选择器)、
|
|
375
|
+
`panel-redis`、`panel-db-and-cache`(复用前两条)。`p1 recipe show <id>` 展开角色表;
|
|
376
|
+
`p1 recipe apply` 默认只预览,加 `--write` 写回 spec;`--bind host=DB_HOST` 补 hints 没覆盖的
|
|
377
|
+
角色。hints 的 `subjects` 会区分 `postgresql` 与 `redis` 这类**依赖主体**,所以
|
|
378
|
+
`REDIS_URL` 不会被绑到 Postgres 的连接串模板上。
|
|
379
|
+
|
|
351
380
|
## 当前状态
|
|
352
381
|
|
|
353
382
|
完整流程已经闭环:
|
|
@@ -85,7 +85,9 @@ Install the CLI first: `uv tool install 1panel-toolkit` (or `pip install
|
|
|
85
85
|
[{kind, kinds, style, env}]`. The generator renders the selector
|
|
86
86
|
(`type: apps` + `child.type: service` by default) and maps `${PANEL_DB_*}`
|
|
87
87
|
onto the application's own variable names, which *you* supply - it never
|
|
88
|
-
invents them. `p1 resolve` lists candidates under `dependency_hints
|
|
88
|
+
invents them. `p1 resolve` lists candidates under `dependency_hints`, and
|
|
89
|
+
`p1 recipe apply panel-postgres <spec> --write` turns those hints into the
|
|
90
|
+
wiring in one step (`p1 recipe list` for the other recipes).
|
|
89
91
|
* **D09, first-run config and permissions** - `config_files` ships the
|
|
90
92
|
template the container reads, `data_owner` generates `init.sh` /
|
|
91
93
|
`upgrade.sh` that create the mounted directories and hand them to the user
|
|
@@ -59,6 +59,7 @@ def run(package: Package, report: Report, strict: bool = False) -> None:
|
|
|
59
59
|
_check_services(version, report, name, generic)
|
|
60
60
|
_check_ports(version, report, name)
|
|
61
61
|
_check_networks(version, report, name)
|
|
62
|
+
_check_healthcheck(version, report, name)
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def _check_images(version: Version, report: Report, name: str, strict: bool) -> None:
|
|
@@ -186,3 +187,18 @@ def _check_networks(version: Version, report: Report, name: str) -> None:
|
|
|
186
187
|
"can be reached through 1Panel's entry",
|
|
187
188
|
name,
|
|
188
189
|
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _check_healthcheck(version: Version, report: Report, name: str) -> None:
|
|
193
|
+
"""`healthcheck:` must be a mapping; `healthcheck: true` breaks the parse."""
|
|
194
|
+
for service_name, service in version.services.items():
|
|
195
|
+
if not isinstance(service, dict) or "healthcheck" not in service:
|
|
196
|
+
continue
|
|
197
|
+
healthcheck = service["healthcheck"]
|
|
198
|
+
if not isinstance(healthcheck, dict):
|
|
199
|
+
report.fail(
|
|
200
|
+
"C005",
|
|
201
|
+
f"service {service_name}: healthcheck 必须是映射(test/interval/…),"
|
|
202
|
+
f"现在是 {type(healthcheck).__name__};Docker 解析不过",
|
|
203
|
+
name,
|
|
204
|
+
)
|
|
@@ -31,6 +31,7 @@ from .resolve import resolve as resolve_target
|
|
|
31
31
|
from .resolve import write_outputs as write_resolve_outputs
|
|
32
32
|
from .remote import RemoteError, SshTarget, check_host, smoke as run_smoke
|
|
33
33
|
from .gen import GenResult, SpecError, generate as generate_package, load_spec
|
|
34
|
+
from . import recipes as recipes_module
|
|
34
35
|
from .deploy import DeployError, deploy as run_deploy, remove_local, uninstall as run_uninstall
|
|
35
36
|
from .selftest import run as run_selftest
|
|
36
37
|
from .fix import fix as run_fix
|
|
@@ -494,6 +495,71 @@ def cmd_selftest(args: argparse.Namespace) -> int:
|
|
|
494
495
|
return 0 if result.ok else 1
|
|
495
496
|
|
|
496
497
|
|
|
498
|
+
def cmd_recipe(args: argparse.Namespace) -> int:
|
|
499
|
+
import json as _json
|
|
500
|
+
|
|
501
|
+
if args.action == "list":
|
|
502
|
+
items = recipes_module.rules.recipes()
|
|
503
|
+
if args.json:
|
|
504
|
+
print(_json.dumps(items, ensure_ascii=False, indent=2))
|
|
505
|
+
return 0
|
|
506
|
+
width = max((len(str(item["id"])) for item in items), default=10)
|
|
507
|
+
for item in items:
|
|
508
|
+
matches = ",".join(item.get("matches") or [])
|
|
509
|
+
print(f"{item['id']:<{width}} {item.get('title')} [{matches}]")
|
|
510
|
+
return 0
|
|
511
|
+
|
|
512
|
+
if args.action == "show":
|
|
513
|
+
try:
|
|
514
|
+
print(recipes_module.describe(args.recipe_id))
|
|
515
|
+
except SpecError as exc:
|
|
516
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
517
|
+
return 2
|
|
518
|
+
return 0
|
|
519
|
+
|
|
520
|
+
# apply
|
|
521
|
+
spec_path = Path(args.spec).resolve()
|
|
522
|
+
if not spec_path.exists():
|
|
523
|
+
print(f"error: 找不到 spec: {spec_path}", file=sys.stderr)
|
|
524
|
+
return 2
|
|
525
|
+
try:
|
|
526
|
+
spec = load_spec(spec_path)
|
|
527
|
+
updated, result = recipes_module.apply(
|
|
528
|
+
args.recipe_id, spec, binds=args.bind, service=args.service or ""
|
|
529
|
+
)
|
|
530
|
+
except SpecError as exc:
|
|
531
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
532
|
+
return 2
|
|
533
|
+
|
|
534
|
+
if args.json:
|
|
535
|
+
print(
|
|
536
|
+
_json.dumps(
|
|
537
|
+
{
|
|
538
|
+
"recipe": result.recipe.get("id"),
|
|
539
|
+
"dependencies": result.dependencies,
|
|
540
|
+
"bound": result.bound,
|
|
541
|
+
"missing": result.missing,
|
|
542
|
+
"warnings": result.warnings,
|
|
543
|
+
},
|
|
544
|
+
ensure_ascii=False,
|
|
545
|
+
indent=2,
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
print(result.render())
|
|
550
|
+
if args.write:
|
|
551
|
+
spec_path.write_text(
|
|
552
|
+
_json.dumps(updated, ensure_ascii=False, indent=2) + "\n",
|
|
553
|
+
encoding="utf-8",
|
|
554
|
+
newline="\n",
|
|
555
|
+
)
|
|
556
|
+
print(f"\n已写入 {spec_path}")
|
|
557
|
+
print("下一步:`p1 gen <spec> --check`")
|
|
558
|
+
elif not args.json:
|
|
559
|
+
print("\n(预览;加 --write 写回 spec)")
|
|
560
|
+
return 0
|
|
561
|
+
|
|
562
|
+
|
|
497
563
|
def cmd_fix(args: argparse.Namespace) -> int:
|
|
498
564
|
target = Path(args.path).resolve()
|
|
499
565
|
if not target.is_dir():
|
|
@@ -733,6 +799,32 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
733
799
|
pat_export.set_defaults(action="export", func=cmd_patterns)
|
|
734
800
|
pat.set_defaults(action="list", func=cmd_patterns)
|
|
735
801
|
|
|
802
|
+
recipe = sub.add_parser(
|
|
803
|
+
"recipe", help="apply a packaged way of wiring panel-managed dependencies"
|
|
804
|
+
)
|
|
805
|
+
recipe_sub = recipe.add_subparsers(dest="action")
|
|
806
|
+
recipe_list = recipe_sub.add_parser("list", help="list recipes")
|
|
807
|
+
recipe_list.add_argument("--json", action="store_true")
|
|
808
|
+
recipe_list.set_defaults(action="list", func=cmd_recipe)
|
|
809
|
+
recipe_show = recipe_sub.add_parser("show", help="explain one recipe")
|
|
810
|
+
recipe_show.add_argument("recipe_id")
|
|
811
|
+
recipe_show.set_defaults(action="show", func=cmd_recipe)
|
|
812
|
+
recipe_apply = recipe_sub.add_parser(
|
|
813
|
+
"apply", help="merge a recipe's dependency wiring into a spec"
|
|
814
|
+
)
|
|
815
|
+
recipe_apply.add_argument("recipe_id")
|
|
816
|
+
recipe_apply.add_argument("spec", help="spec JSON (a resolve draft works)")
|
|
817
|
+
recipe_apply.add_argument(
|
|
818
|
+
"--bind",
|
|
819
|
+
action="append",
|
|
820
|
+
help="role=ENV_NAME for a role the hints do not cover, e.g. --bind host=DB_HOST",
|
|
821
|
+
)
|
|
822
|
+
recipe_apply.add_argument("--service", help="which spec service to wire (default: the first)")
|
|
823
|
+
recipe_apply.add_argument("--write", action="store_true", help="write the spec back")
|
|
824
|
+
recipe_apply.add_argument("--json", action="store_true")
|
|
825
|
+
recipe_apply.set_defaults(action="apply", func=cmd_recipe)
|
|
826
|
+
recipe.set_defaults(action="list", func=cmd_recipe)
|
|
827
|
+
|
|
736
828
|
depth = sub.add_parser(
|
|
737
829
|
"depth", help="measure how app-specific a package is (template vs adapted)"
|
|
738
830
|
)
|
|
@@ -208,6 +208,9 @@ def build_fields(
|
|
|
208
208
|
dependency wiring (which is rendered literally, not as a default).
|
|
209
209
|
"""
|
|
210
210
|
fields: list[dict] = []
|
|
211
|
+
# Ports first: with more than one service the port fields would otherwise be
|
|
212
|
+
# scattered between another service's variables.
|
|
213
|
+
port_fields: list[dict] = []
|
|
211
214
|
hidden: dict[str, list[str]] = {}
|
|
212
215
|
# Keys the generator adds on its own; they must not be double counted as
|
|
213
216
|
# "hidden upstream variables" when the upstream compose also sets them.
|
|
@@ -219,7 +222,7 @@ def build_fields(
|
|
|
219
222
|
port = service.get("container_port")
|
|
220
223
|
if port:
|
|
221
224
|
env_key = "PANEL_APP_PORT_HTTP" if port_index == 0 else f"PANEL_APP_PORT_{port_index}"
|
|
222
|
-
|
|
225
|
+
port_fields.append(
|
|
223
226
|
{
|
|
224
227
|
"envKey": env_key,
|
|
225
228
|
"type": "number",
|
|
@@ -252,6 +255,7 @@ def build_fields(
|
|
|
252
255
|
entry["description"] = item["description"]
|
|
253
256
|
fields.append(entry)
|
|
254
257
|
|
|
258
|
+
fields = port_fields + fields
|
|
255
259
|
hidden = {
|
|
256
260
|
service: [key for key in keys if key not in reserved]
|
|
257
261
|
for service, keys in hidden.items()
|
|
@@ -370,7 +374,6 @@ def build_compose(
|
|
|
370
374
|
if env_lines:
|
|
371
375
|
entry["environment"] = env_lines
|
|
372
376
|
for extra in (
|
|
373
|
-
"healthcheck",
|
|
374
377
|
"security_opt",
|
|
375
378
|
"cap_add",
|
|
376
379
|
"cap_drop",
|
|
@@ -392,6 +395,16 @@ def build_compose(
|
|
|
392
395
|
):
|
|
393
396
|
if service.get(extra):
|
|
394
397
|
entry[extra] = service[extra]
|
|
398
|
+
# `healthcheck` is a mapping, never a flag: a boolean from a spec (or an
|
|
399
|
+
# evidence flag) must not reach the compose file, Docker rejects it.
|
|
400
|
+
healthcheck = service.get("healthcheck")
|
|
401
|
+
if isinstance(healthcheck, dict):
|
|
402
|
+
entry["healthcheck"] = healthcheck
|
|
403
|
+
elif healthcheck:
|
|
404
|
+
warnings.append(
|
|
405
|
+
f"{name}: healthcheck 不是映射({healthcheck!r}),已忽略;"
|
|
406
|
+
"探针要写成 {test: [...], interval: 10s, ...}"
|
|
407
|
+
)
|
|
395
408
|
entry["labels"] = {"createdBy": "Apps"}
|
|
396
409
|
services[name] = entry
|
|
397
410
|
|