✨ feat(prototype): 代码阅读原型加 Dockerfile 一页(阶段套指令、RUN 按命令拆、连着的 COPY / ENV 并表、健康检查、启动);Shell 和 Dockerfile 共用一套画法,插件菜单加 Dockerfile

This commit is contained in:
cruldra committed 2026-09-28 11:09:06 +08:00
1 parent 14582c5c43
commit 834673391e
4 files changed
+206 -36

No files matched your search

@@ -47,11 +47,18 @@
| --- | --- | --- |
| `Prototype / Shell · fetch-runtime.sh` | luffy 桌面端 `apps/desktop/scripts/fetch-runtime.sh` | bash 语言插件拆块,命令的意思由「命令规则」按命令名注册(rm、mkdir、curl、tar、find……),像库规则一样把选项读成中文。块:解释器(#! 用哪个解释器、怎么找);选项(set -euo pipefail 每个开关一行);变量(`${1:-}`、`$(…)`、`${x:-默认}` 读成「第 1 个参数(没给就是空)」「执行 … 的输出」「x(没设就用 …)」,大写的当常量上色);分情况(每个情况都只有几句时画成「情况 / 做什么」表,有长的就每个情况一块子块,`*.zip` 读成「以 .zip 结尾」);函数(紧贴的注释当说明;按 `local x="$1"` 认出第几个参数叫什么;输出写它打印的东西);命令(「删除 dest(连同里面的东西,不提示)」「下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)」,`>&2` 写「打印到错误输出」);管道(a → b,后面的 while read 照常是子块);如果 / 遍历(`[ -f x ]`、`[[ x == *-y ]]`、`x && y \|\| continue` 读成中文) |
## Dockerfile(插件菜单「Dockerfile」,单独一页 `代码阅读器 · Dockerfile`)
| 画板 | 对应源码 | 设计点 |
| --- | --- | --- |
| `Prototype / Dockerfile · Dockerfile` | luffy 的 `frontend/client-nextjs/Dockerfile` | 每个 FROM 开一个「阶段」(名称、基础镜像 = 镜像名 + 标签),后面的指令是它的子块;`# syntax=` 是「解析指令」;WORKDIR / USER / EXPOSE 各一行「设置」;ARG「构建参数」、ENV「环境变量」,`$X` 读成变量;连着写的 ENV 并成「名称 / 值」表;`COPY --from=deps` 写「从阶段 deps 复制 … → …」,阶段名 Cmd+单击跳过去;连着写的 COPY 并成「从 / 源 / 到」表,属主相同就写在头一行;CMD「启动」写命令和写法(数组不经过 shell) |
| `Prototype / Dockerfile · Dockerfile.backend` | luffy 的 `backend/docker/Dockerfile.backend` 几段 | LABEL「标签」表;`&&` 连起来的 RUN 每条命令一个子块,命令照 Shell 的命令规则读(apt-get、curl、gpg、uv……),管道写「a → b」;`COPY --from=node:22-slim` 写「从镜像」;12 条连着的 COPY 一张表;`uv sync` 一长串选项画成「选项 / 意思」表,重复的 `--no-install-package` 并成一行;HEALTHCHECK「健康检查」:间隔 / 超时 / 启动后宽限 / 失败几次算不健康 / 检查命令 |
## 导入与运行
1. 打开 Figma Desktop,打开任意 Design 文件。
2. `Plugins > Development > Import plugin from manifest…`,选本目录的 `manifest.json`(已经导入过就跳过)。
3. `Plugins > Development > 代码阅读器 Design Lab`,选「主画板」或「Shell 脚本」,各自新建一页。改过 `code.js` 后要重启 Figma 才会用新代码。
3. `Plugins > Development > 代码阅读器 Design Lab`,选「主画板」「Shell 脚本」或「Dockerfile」,各自新建一页。改过 `code.js` 后要重启 Figma 才会用新代码。
## 运行条件
+165 -31
View File
@@ -917,6 +917,8 @@ const LUCIDE = {
"signpost": [["path", { d: "M12 13v8" }], ["path", { d: "M12 3v3" }], ["path", { d: "M2.354 10.354a1.207 1.207 0 0 1 0-1.708l2.06-2.06A2 2 0 0 1 5.828 6h12.344a2 2 0 0 1 1.414.586l2.06 2.06a1.207 1.207 0 0 1 0 1.708l-2.06 2.06a2 2 0 0 1-1.414.586H5.828a2 2 0 0 1-1.414-.586z" }]],
"sliders-horizontal": [["path", { d: "M10 5H3" }], ["path", { d: "M12 19H3" }], ["path", { d: "M14 3v4" }], ["path", { d: "M16 17v4" }], ["path", { d: "M21 12h-9" }], ["path", { d: "M21 19h-5" }], ["path", { d: "M21 5h-7" }], ["path", { d: "M8 10v4" }], ["path", { d: "M8 12H3" }]],
"terminal": [["path", { d: "M12 19h8" }], ["path", { d: "m4 17 6-6-6-6" }]],
"layers": [["path", { d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z" }], ["path", { d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" }], ["path", { d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" }]],
"activity": [["path", { d: "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" }]],
"package": [["path", { d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" }], ["path", { d: "M12 22V12" }], ["polyline", { points: "3.29 7 12 12 20.71 7" }], ["path", { d: "m7.5 4.27 9 5.15" }]],
"git-pull-request": [["circle", { cx: "18", cy: "18", r: "3" }], ["circle", { cx: "6", cy: "6", r: "3" }], ["path", { d: "M13 6h3a2 2 0 0 1 2 2v7" }], ["path", { d: "M6 9v12" }]],
"files": [["path", { d: "M20 7h-3a2 2 0 0 1-2-2V2" }], ["path", { d: "M9 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h7l4 4v10a2 2 0 0 1-2 2Z" }], ["path", { d: "M3 7.6v12.8A1.6 1.6 0 0 0 4.6 22h9.8" }]],
@@ -2635,10 +2637,9 @@ const SHELL_BLOCKS = [
{ line: 130, kind: "command", text: `[c|统计占用空间] [v|dest][q|/git]、[v|dest][q|/python][d|(按人读的单位,只看总数)]${STDERR}` },
];
/** Shell / 块 — Line=<行号>:一块,按 spec 画一行译文、属性行、表格。 */
function shellBlock(ctx, spec) {
const kind = SHELL_KINDS[spec.kind];
const { node, content } = cardShell(ctx, `Line=${spec.line}`, kind, { kids: Boolean(spec.kids) });
/** <页> / <文件> — Line=<行号>:一块,按 spec 画一行译文、属性行、表格(列数由 columns 定)。 */
function scriptBlock(ctx, kinds, spec) {
const { node, content } = cardShell(ctx, `Line=${spec.line}`, kinds[spec.kind], { kids: Boolean(spec.kids) });
if (spec.text) richText(ctx, content, "Text", rich(spec.text));
const rows = spec.rows || [];
rows.forEach(([label, value], index) => {
@@ -2646,78 +2647,211 @@ function shellBlock(ctx, spec) {
propRow(ctx, content, label, rich(value), { twisty });
});
if (spec.table) {
// 分情况的表跟在头一行下面;函数的参数表跟在「输入参数」下面,「输出 / 返回」挪到表后面
propTable(ctx, content, "Table", spec.table.columns, spec.table.rows.map(([a, b], index) => [`Row / ${index + 1}`, [rich(a), rich(b)]]));
// 表跟在头一行 / 最后一行属性下面;函数的参数表跟在「输入参数」下面,「输出 / 返回」挪到表后面
propTable(ctx, content, "Table", spec.table.columns, spec.table.rows.map((cells, index) => [`Row / ${index + 1}`, cells.map(rich)]));
if (spec.kind === "function") content.appendChild(named(content, `Prop / ${rows[rows.length - 1][0]}`));
}
return { node };
}
function shellSpecs(specs, visit) {
function scriptSpecs(specs, visit) {
for (const spec of specs) {
visit(spec);
if (spec.kids) shellSpecs(spec.kids, visit);
if (spec.kids) scriptSpecs(spec.kids, visit);
}
}
/** 一层 shell 块,和代码块树同样的竖线和缩进。 */
function shellLevel(ctx, parent, lib, specs, name) {
/** 一层块,和代码块树同样的竖线和缩进。 */
function scriptLevel(ctx, parent, blocks, specs, name) {
const { level, list } = levelList(ctx, parent, name);
for (const spec of specs) {
const instance = lib.shell.get(spec.line).node.createInstance();
const instance = blocks.get(spec.line).node.createInstance();
instance.name = `Block / L${spec.line}`;
append(list, instance, true);
if (spec.kids) shellLevel(ctx, list, lib, spec.kids, `Children / L${spec.line}`);
if (spec.kids) scriptLevel(ctx, list, blocks, spec.kids, `Children / L${spec.line}`);
}
return level;
}
/** 「Shell 脚本」这一页:只建它用得到的组件和一张画板。 */
function buildShell(ctx, api, page, stage) {
/** 插件菜单里 main 以外的几项:各自一页,只建用得到的组件,每个示例文件一张画板。 */
function buildScriptPage(ctx, api, page, stage, key) {
const spec = SCRIPT_PAGES[key];
stage("生成组件");
const sets = [];
const lib = {};
ctx.lib = lib;
const make = setMaker(ctx, page, sets);
lib.tools = make("Reader / BlockTools", MODES.slice().reverse(), 2, (state) => blockToolsComponent(ctx, state));
const lines = [];
shellSpecs(SHELL_BLOCKS, (spec) => lines.push(spec.line));
const byLine = new Map();
shellSpecs(SHELL_BLOCKS, (spec) => byLine.set(spec.line, spec));
lib.shell = make(`Shell / ${SHELL_FILE.split("/").pop()}`, lines, 2, (line) => shellBlock(ctx, byLine.get(line)), (item) => item.node);
const built = spec.boards.map((board) => {
const byLine = new Map();
scriptSpecs(board.blocks, (block) => byLine.set(block.line, block));
const name = board.file.split("/").pop();
return { board, name, blocks: make(`${spec.prefix} / ${name}`, [...byLine.keys()], 2, (line) => scriptBlock(ctx, spec.kinds, byLine.get(line)), (item) => item.node) };
});
lib.blocks = built.map((entry) => entry.blocks);
stage("生成演示画板");
const board = frame(ctx, `Prototype / Shell · ${SHELL_FILE.split("/").pop()}`, { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, fill: COLORS.editor });
page.appendChild(board);
timed(board.name, () => shellLevel(ctx, board, lib, SHELL_BLOCKS, "Level 0"));
const screens = built.map(({ board, name, blocks }) => {
const screen = frame(ctx, `Prototype / ${spec.prefix} · ${name}`, { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, fill: COLORS.editor });
page.appendChild(screen);
timed(screen.name, () => scriptLevel(ctx, screen, blocks, board.blocks, "Level 0"));
return screen;
});
stage("排布画板");
board.x = 0;
board.y = 0;
let x = 0;
for (const screen of screens) {
screen.x = x;
screen.y = 0;
x += screen.width + 120;
}
let setY = 0;
for (const set of sets) {
set.x = board.width + 160;
set.x = x + 40;
set.y = setY;
setY += set.height + 64;
}
for (const node of page.children) node.expanded = false;
api.viewport.scrollAndZoomIntoView([board]);
return { clock, page, components: { lib, sets }, board, screens: [board], start: board.name };
api.viewport.scrollAndZoomIntoView([screens[0]]);
return { clock, page, components: { lib, sets }, board: screens[0], screens, start: screens[0].name };
}
// ─── Dockerfile ──────────────────────────────────────────────────────────────
//
// Dockerfile 语言插件按指令拆块(语法另外引入,插件现用的分析组件里没有 Dockerfile):
// - 解析指令:第一行 # syntax=…,写 BuildKit 按哪个语法版本读这份文件;
// - 阶段:每个 FROM 开一个阶段,后面的指令都是它的子块;卡片写阶段名、基础镜像(镜像名 + 标签)、平台(写了才有);
// - 执行:RUN 里的 shell 交给 Shell 语言插件和命令规则读(apt-get、pnpm、uv、corepack、useradd……);
// 只有一条命令写一行,&& 连起来的每条命令一个子块,管道照 Shell 那样写「a → b」;
// - 复制:COPY / ADD 写「从阶段 X / 从镜像 Y 复制 源 → 目标(属主 u:g)」,阶段名 Cmd+单击跳到那个阶段;
// - 变量:ARG 是构建参数(默认值写了才有),ENV 是环境变量,值里的 $X 读成变量;
// - 连着写的同种指令(几行 ENV、几行 COPY)并成一张表,中间隔了注释就断开;
// - 设置:WORKDIR「工作目录」、USER「之后以 … 身份运行」、EXPOSE「开放端口」、VOLUME、STOPSIGNAL、SHELL 各一行;
// - 标签:LABEL 画成「名称 / 值」表;
// - 健康检查:间隔、超时、启动后宽限、失败几次算不健康、检查命令(命令照 Shell 读);
// - 启动:CMD / ENTRYPOINT 写命令和写法(数组写法不经过 shell),命令照命令规则读。
// 原型的译文按这份设计手写。示例:luffy 的 frontend/client-nextjs/Dockerfile(三个阶段)和 backend/docker/Dockerfile.backend 的几段。
const DOCKER_PAGE_NAME = "代码阅读器 · Dockerfile";
const DOCKER_KINDS = {
comment: BLOCK_KINDS.comment,
variable: BLOCK_KINDS.variable,
directive: { label: "解析指令", color: "#9d9d9d", icon: "terminal" },
stage: { label: "阶段", color: "#56b6c2", icon: "layers" },
run: { label: "执行", color: "#d7ba7d", icon: "square-terminal" },
copy: { label: "复制", color: "#c586c0", icon: "copy" },
setting: { label: "设置", color: "#e5c07b", icon: "sliders-horizontal" },
label: { label: "标签", color: "#9cdcfe", icon: "tag" },
health: { label: "健康检查", color: "#f48771", icon: "activity" },
start: { label: "启动", color: "#89d185", icon: "play" },
};
const PNPM_ACTIVATE = [
{ line: 0, kind: "run", text: "[c|开启 corepack]" },
{ line: 0, kind: "run", text: "[c|准备 pnpm] [q|latest][d|(并设为当前用的版本)]" },
];
const withLines = (blocks, first) => blocks.map((block, index) => ({ ...block, line: first + index / 10 }));
const NEXT_DOCKERFILE = [
{ line: 1, kind: "directive", rows: [["语法", "[q|docker/dockerfile:1.7][d|(BuildKit 按这个版本读这份文件)]"]] },
{ line: 3, kind: "stage", rows: [["名称", "[v|deps]"], ["基础镜像", "[t|node][d|,标签 ][q|22-alpine]"]], kids: [
{ line: 4, kind: "setting", text: "[k|工作目录] [q|/app]" },
{ line: 5, kind: "run", text: "[k|执行]", kids: withLines(PNPM_ACTIVATE, 5.1) },
{ line: 6, kind: "copy", text: "[c|复制] [q|package.json]、[q|pnpm-lock.yaml] [k|→] [q|./]" },
{ line: 7, kind: "run", text: "[c|pnpm 装依赖][d|(严格按 lock 文件、不跑安装脚本)]" },
] },
{ line: 9, kind: "stage", rows: [["名称", "[v|builder]"], ["基础镜像", "[t|node][d|,标签 ][q|22-alpine]"]], kids: [
{ line: 10, kind: "setting", text: "[k|工作目录] [q|/app]" },
{ line: 11, kind: "run", text: "[k|执行]", kids: withLines(PNPM_ACTIVATE, 11.1) },
{ line: 12, kind: "copy", text: "[k|从阶段] [v|deps] [c|复制] [q|/app/node_modules] [k|→] [q|./node_modules]" },
{ line: 13, kind: "copy", text: "[c|复制] [d|整个构建目录] [k|→] [q|./]" },
{ line: 14, kind: "variable", rows: [["环境变量", "[u|NEXT_TELEMETRY_DISABLED]"], ["值", "[n|1]"]] },
{ line: 15, kind: "comment", text: "[d|部署 ID 烙进本次构建: Next 据此给静态资源加 ?dpl=、页内跳转时比对版本;\n浏览器包与服务端包都能读 process.env.NEXT_DEPLOYMENT_ID (见 src/lib/version-skew).]" },
{ line: 17, kind: "variable", rows: [["构建参数", "[u|DEPLOYMENT_ID]"], ["默认值", "[d|没写,构建时传]"]] },
{ line: 18, kind: "variable", rows: [["环境变量", "[u|NEXT_DEPLOYMENT_ID]"], ["值", "[u|DEPLOYMENT_ID][d|(构建参数)]"]] },
{ line: 19, kind: "run", text: "[c|pnpm 构建]" },
] },
{ line: 21, kind: "stage", rows: [["名称", "[v|runner]"], ["基础镜像", "[t|node][d|,标签 ][q|22-alpine]"]], kids: [
{ line: 22, kind: "setting", text: "[k|工作目录] [q|/app]" },
{ line: 23, kind: "variable", text: "[k|环境变量][d|(4 条连着写)]", table: { columns: [["名称", 260], ["值", 0]], rows: [
["[u|NODE_ENV]", "[q|production]"], ["[u|NEXT_TELEMETRY_DISABLED]", "[n|1]"], ["[u|PORT]", "[n|3000]"], ["[u|HOSTNAME]", "[q|0.0.0.0]"],
] } },
{ line: 27, kind: "variable", rows: [["构建参数", "[u|DEPLOYMENT_ID]"], ["默认值", "[d|没写,构建时传]"]] },
{ line: 28, kind: "variable", rows: [["环境变量", "[u|NEXT_DEPLOYMENT_ID]"], ["值", "[u|DEPLOYMENT_ID][d|(构建参数)]"]] },
{ line: 30, kind: "run", text: "[k|执行]", kids: [
{ line: 30.1, kind: "run", text: "[c|新建系统用户组] [v|nodejs][d|(gid ][n|1001][d|)]" },
{ line: 30.2, kind: "run", text: "[c|新建系统用户] [v|nextjs][d|(uid ][n|1001][d|)]" },
] },
{ line: 33, kind: "copy", text: "[c|复制][d|(2 条连着写,属主都是 ][v|nextjs:nodejs][d|)]", table: { columns: [["从", 120], ["源", 280], ["到", 0]], rows: [
["[k|阶段] [v|builder]", "[q|/app/.next/standalone]", "[q|./]"],
["[k|阶段] [v|builder]", "[q|/app/.next/static]", "[q|./.next/static]"],
] } },
{ line: 35, kind: "comment", text: "[d|standalone 输出不含 public/, 必须显式拷贝 (字体等静态资源)]" },
{ line: 36, kind: "copy", text: "[k|从阶段] [v|builder] [c|复制] [q|/app/public] [k|→] [q|./public][d|(属主 ][v|nextjs:nodejs][d|)]" },
{ line: 38, kind: "setting", text: "[k|之后以] [v|nextjs] [k|身份运行]" },
{ line: 39, kind: "setting", text: "[k|开放端口] [n|3000]" },
{ line: 41, kind: "start", rows: [["命令", "[c|node] [q|server.js]"], ["写法", "[d|数组,不经过 shell]"]] },
] },
];
const BACKEND_DOCKERFILE = [
{ line: 4, kind: "stage", rows: [["名称", "[v|base]"], ["基础镜像", "[t|python][d|,标签 ][q|3.12-slim]"]], kids: [
{ line: 6, kind: "label", text: "[k|标签]", table: { columns: [["名称", 160], ["值", 0]], rows: [
["[v|maintainer]", "[q|Cruldra]"], ["[v|description]", "[q|luffy-agent-platform - 统一 Backend 镜像 (API/Worker/Beat/Flower)]"],
] } },
{ line: 13, kind: "run", text: "[k|执行][d|(10 条命令,一条失败就停)]", kids: [
{ line: 13.1, kind: "run", text: "[c|更新软件包列表]" },
{ line: 13.2, kind: "run", text: "[c|安装] [q|ca-certificates]、[q|curl]、[q|gnupg]、[q|postgresql-client]、[q|git][d|(不装推荐的包、不用确认)]" },
{ line: 13.3, kind: "run", text: "[c|建目录] [q|/usr/share/keyrings]" },
{ line: 13.4, kind: "run", text: "[c|下载] [q|https://www.postgresql.org/media/keys/ACCC4CF8.asc][d|(不打印进度、失败就报错、跟着跳转走)] [k|→] [c|转成二进制密钥] [k|写到] [q|/usr/share/keyrings/postgresql.gpg]" },
{ line: 13.5, kind: "run", text: "[c|读入] [q|/etc/os-release] [d|里的变量]" },
{ line: 13.6, kind: "run", text: "[c|写入] [q|\"deb [signed-by=…] http://apt.postgresql.org/pub/repos/apt ][v|VERSION_CODENAME][q|-pgdg main\"] [k|到] [q|/etc/apt/sources.list.d/pgdg.list][d|(覆盖)]" },
{ line: 13.7, kind: "run", text: "[c|更新软件包列表]" },
{ line: 13.8, kind: "run", text: "[c|安装] [q|postgresql-16-repack][d|(不装推荐的包、不用确认)]" },
{ line: 13.9, kind: "run", text: "[c|打印 pg_repack 的版本]" },
{ line: 13.95, kind: "run", text: "[c|删除] [q|/var/lib/apt/lists/*][d|(连同里面的东西、不提示)]" },
] },
{ line: 32, kind: "copy", text: "[c|复制][d|(2 条连着写)]", table: { columns: [["从", 160], ["源", 300], ["到", 0]], rows: [
["[k|镜像] [t|node][d|:][q|22-slim]", "[q|/usr/local/bin/node]", "[q|/usr/local/bin/node]"],
["[k|镜像] [t|node][d|:][q|22-slim]", "[q|/usr/local/lib/node_modules]", "[q|/usr/local/lib/node_modules]"],
] } },
{ line: 54, kind: "copy", text: "[c|复制][d|(12 条连着写,属主都是 ][v|appuser:appuser][d|)]", table: { columns: [["源", 420], ["到", 0]], rows: [
["[q|backend/pyproject.toml]、[q|backend/uv.lock]", "[q|./]"],
...["agent-dsl", "agent-compiler-deepagents", "vendor-tools", "code-sandbox-core", "code-sandbox-daytona", "code-sandbox-e2b", "code-sandbox-local",
"code-publish-core", "code-publish-compose-runner", "luffy-billing-sdk", "luffy-observability"].map((name) => [`[q|lib/${name}/pyproject.toml]`, `[q|/lib/${name}/]`]),
] } },
{ line: 67, kind: "comment", text: "[d|三方依赖先装;本项目与 lib editable 包此时无源码,跳过安装留给第二次 sync]" },
{ line: 68, kind: "run", rows: [["命令", "[c|uv 按 lock 同步依赖]"], ["选项", "[d|4 个]"]], table: { columns: [["选项", 320], ["意思", 0]], rows: [
["[q|--frozen]", "[d|严格按 lock 文件,不改它]"],
["[q|--no-dev]", "[d|不装开发依赖]"],
["[q|--no-install-project]", "[d|不装本项目]"],
["[q|--no-install-package] [d|× 11]", "[d|不装这些包:]agent-dsl、agent-compiler-deepagents、vendor-tools、code-sandbox-core、code-sandbox-daytona、code-sandbox-e2b、code-sandbox-local、code-publish-core、code-publish-compose-runner、luffy-billing-sdk、luffy-observability"],
] } },
{ line: 104, kind: "setting", text: "[k|开放端口] [n|8000]" },
{ line: 107, kind: "health", rows: [["间隔", "[n|30] [d|秒]"], ["超时", "[n|5] [d|秒]"], ["启动后宽限", "[n|10] [d|秒]"], ["失败几次算不健康", "[n|3] [d|次]"],
["检查命令", "[c|请求] [q|http://localhost:8000/health][d|(失败就报错)] [k|或者] [c|退出][d|,退出码 ][n|1]"]] },
{ line: 110, kind: "start", rows: [["命令", "[c|用 uv 运行] [c|uvicorn] [q|luffy_agent.main:app][d|,监听 ][q|0.0.0.0][d|:][n|8000]"], ["写法", "[d|数组,不经过 shell]"]] },
] },
];
// 插件菜单里 main 以外的几项
const SCRIPT_PAGES = {
shell: { page: SHELL_PAGE_NAME, prefix: "Shell", kinds: SHELL_KINDS, boards: [{ file: SHELL_FILE, blocks: SHELL_BLOCKS }] },
docker: { page: DOCKER_PAGE_NAME, prefix: "Dockerfile", kinds: DOCKER_KINDS, boards: [
{ file: "frontend/client-nextjs/Dockerfile", blocks: NEXT_DOCKERFILE },
{ file: "backend/docker/Dockerfile.backend", blocks: BACKEND_DOCKERFILE },
] },
};
// ─── Entry ───────────────────────────────────────────────────────────────────
/** command:插件菜单里点的哪一项,main = 主画板,shell = Shell 脚本那一页。 */
/** command:插件菜单里点的哪一项,main = 主画板,其余见 SCRIPT_PAGES(shell、docker)。 */
async function build(api, stage = () => {}, command = "main") {
clock = new Map();
stage("加载中文字体");
const ctx = { api, fonts: await chooseFonts(api) };
stage("新建独立页面");
const name = nextPageName(api.root.children, command === "shell" ? SHELL_PAGE_NAME : PAGE_NAME);
const name = nextPageName(api.root.children, SCRIPT_PAGES[command] ? SCRIPT_PAGES[command].page : PAGE_NAME);
const page = api.createPage();
page.name = name;
await api.setCurrentPageAsync(page);
page.backgrounds = paint("#111214");
page.setSharedPluginData(NAMESPACE, "generator", GENERATOR);
if (command === "shell") return buildShell(ctx, api, page, stage);
if (SCRIPT_PAGES[command]) return buildScriptPage(ctx, api, page, stage, command);
stage("生成组件");
const components = createComponents(ctx, page);
@@ -2911,6 +3045,6 @@ async function run(api) {
}
if (typeof module !== "undefined") {
module.exports = { SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
module.exports = { NEXT_DOCKERFILE, BACKEND_DOCKERFILE, DOCKER_PAGE_NAME, SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, blockAttributes, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, parseImport, parseVariable, translateCode, build, run, nextPageName, PAGE_NAME, NAMESPACE, TREE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, typeSegments };
}
if (typeof figma !== "undefined") void run(figma);
@@ -19,6 +19,10 @@
{
"name": "Shell 脚本",
"command": "shell"
},
{
"name": "Dockerfile",
"command": "docker"
}
]
}
+29 -4
View File
@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const path = require("node:path");
const { SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { NEXT_DOCKERFILE, BACKEND_DOCKERFILE, DOCKER_PAGE_NAME, SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { fakeFigma } = require("../../figma-lab/fake-figma");
function action(node) { return node.reactions[0].actions[0]; }
@@ -100,8 +100,8 @@ test("manifest stays local, without a fabricated plugin ID", () => {
assert.deepEqual(manifest.editorType, ["figma"]);
assert.deepEqual(manifest.networkAccess.allowedDomains, ["none"]);
assert.equal("id" in manifest, false);
// 插件菜单分两项:主画板、Shell 脚本,各自只生成自己那一页
assert.deepEqual(manifest.menu.map((item) => item.command), ["main", "shell"]);
// 插件菜单:主画板、Shell 脚本、Dockerfile,各自只生成自己那一页
assert.deepEqual(manifest.menu.map((item) => item.command), ["main", "shell", "docker"]);
});
test("plugin source passes Figma's load-time scan", () => {
@@ -531,7 +531,7 @@ test("the Shell menu item builds its own page: blocks, case tables, function par
assert.equal(new Set(lines).size, lines.length, "one block per source line");
const board = result.board;
assert.deepEqual(board.findAll((node) => node.type === "INSTANCE" && node.name.startsWith("Block / L")).map((node) => Number(node.name.slice(9))), lines);
const block = (line) => result.components.lib.shell.get(line).node;
const block = (line) => result.components.lib.blocks[0].get(line).node;
// 情况都短的 case 画成表,情况长的每个情况一块子块
assert.equal(named(named(block(24), "Table"), "Table").children.length - 1, 3);
assert.ok(!named(block(78), "Table"));
@@ -548,6 +548,31 @@ test("the Shell menu item builds its own page: blocks, case tables, function par
assert.ok(owned < 3000, `shell page owns ${owned} layers`);
});
test("the Dockerfile menu item: stages hold their instructions, RUN chains split into commands, runs of COPY / ENV become tables", async () => {
const api = fakeFigma();
const result = await build(api, () => {}, "docker");
assert.equal(result.page.name, DOCKER_PAGE_NAME);
assert.equal(result.screens.length, 2);
const [next, backend] = result.components.lib.blocks;
// 每个 FROM 一个阶段,指令是阶段的子块
const stages = NEXT_DOCKERFILE.filter((spec) => spec.kind === "stage");
assert.deepEqual(stages.map((spec) => spec.rows[0][1]), ["[v|deps]", "[v|builder]", "[v|runner]"]);
const board = result.screens[0];
for (const stage of stages) assert.ok(named(board, `Children / L${stage.line}`), `stage L${stage.line} holds its instructions`);
assert.ok(named(next.get(3).node, "Kind / 阶段"));
// && 连起来的 RUN 每条命令一个子块
assert.equal(BACKEND_DOCKERFILE[0].kids.find((spec) => spec.line === 13).kids.length, 10);
// 连着写的 ENV、COPY 并成一张表
assert.equal(named(named(next.get(23).node, "Table"), "Table").children.length - 1, 4);
assert.equal(named(named(backend.get(54).node, "Table"), "Table").children.length - 1, 12);
assert.ok(named(backend.get(107).node, "Kind / 健康检查"));
assert.ok(named(backend.get(110).node, "Kind / 启动"));
let owned = 0;
const walk = (node) => { owned += 1; if (node.type !== "INSTANCE") node.children.forEach(walk); };
result.page.children.forEach(walk);
assert.ok(owned < 3000, `Dockerfile page owns ${owned} layers`);
});
test("block kinds show as icons, the legend lists every kind with its name", async () => {
const api = fakeFigma();
const { kindBoard } = await build(api);