From 25569fa506716f3c27a2c6c0c7634337b6c9637a Mon Sep 17 00:00:00 2001 From: cruldra Date: Sun, 27 Sep 2026 21:11:48 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(prototype):=20=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=98=85=E8=AF=BB=E5=8E=9F=E5=9E=8B=E5=8A=A0=20FastAP?= =?UTF-8?q?I=20=E8=B7=AF=E7=94=B1=E5=99=A8=E5=8D=A1=E7=89=87=EF=BC=88?= =?UTF-8?q?=E5=89=8D=E7=BC=80=E3=80=81=E6=A0=87=E7=AD=BE=E3=80=81=E6=8C=82?= =?UTF-8?q?=E5=9C=A8=E4=B8=8A=E9=9D=A2=E7=9A=84=E6=8E=A5=E5=8F=A3=E8=A1=A8?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=8E=A5=E5=8F=A3=E5=8D=A1=E7=89=87=E5=86=99?= =?UTF-8?q?=E6=89=80=E5=B1=9E=E8=B7=AF=E7=94=B1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-reader/figma-plugin/README.md | 4 +- .../code-reader/figma-plugin/code.js | 41 ++++++++++++++++++- .../code-reader/figma-plugin/test.js | 10 ++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/docs/prototype/code-reader/figma-plugin/README.md b/docs/prototype/code-reader/figma-plugin/README.md index 0d3bc54..6bec7d7 100644 --- a/docs/prototype/code-reader/figma-plugin/README.md +++ b/docs/prototype/code-reader/figma-plugin/README.md @@ -26,7 +26,7 @@ | `Prototype / 库规则 · SQLAlchemy 数据表` | luffy 的 `backend/src/luffy_agent/referral/models.py` 里的 `ReferralRelation` | 库规则(文件导入了 sqlalchemy 才启用)把模型类整块换成「数据表」卡片:名称 / 表名 / 说明 / 继承,字段表(字段 / 类型 / 约束 / 说明)和索引表;`__tablename__`、`__table_args__`、各字段块并进卡片 | | `Prototype / 枚举` | luffy 的 `referral/models.py` 里的 `CommissionStatus` | 枚举卡片:名称 / 访问级别 / 继承,成员收成一张表(成员 / 值 / 说明),说明取行尾注释;Python 用库规则(导入了 enum 才启用),TypeScript 的 enum 由语言插件报结构,画成同一张卡片 | | `Prototype / 库规则 · Celery` | luffy 的 `backend/src/luffy_agent/celery_app.py` | 库规则(导入了 celery 才启用)两张卡片:「Celery 应用」名称 / 应用名 / 消息队列 / 结果存储;「定时任务」应用 + 任务表(任务 / 频率 / 说明),频率换算成「每 5 分钟」「每天」,说明取行尾注释,`**` 展开的另起一行 | -| `Prototype / 库规则 · FastAPI 接口` | luffy 的 `user/routes.py` 里的 `update_user` | 库规则(导入了 fastapi 才启用)把挂在路由对象上的函数整块换成「接口」卡片,函数体照常是子块:名称 / 说明 / 标签 / 异步 / 依赖(`Depends(...)` 注入的参数),然后两张表。请求表一项一行:URL(路由对象的 prefix + 装饰器里的路径)/ 方法 / 路径参数 / 查询参数 / 请求体,没有的项不列。响应表每个状态码一行(状态码 / 内容 / 说明):成功那行来自 `status_code` 和 `response_model`,`responses={…}` 里写的各占一行,函数体里直接抛的异常按异常类上的 `status_code` 归行、说明列出抛出时的消息,有参数时 FastAPI 自动带的 422 淡色。挂到应用上时再叠的前缀(`/api/v1`)在别的文件里,不算 | +| `Prototype / 库规则 · FastAPI 接口` | luffy 的 `user/routes.py` 里的 `router` 和 `update_user` | 路由器(`APIRouter(...)`)和接口是两张卡片。「路由器」卡片:名称 / 前缀 / 标签(写了依赖、默认响应再加行),接口表列这个文件里挂在它上面的全部接口(方法 / 完整路径 / 函数 / 说明),函数名 Cmd+单击跳到接口卡片。库规则(导入了 fastapi 才启用)把挂在路由对象上的函数整块换成「接口」卡片,函数体照常是子块:名称 / 说明 / 路由器 / 标签 / 异步 / 依赖(`Depends(...)` 注入的参数),然后两张表。请求表一项一行:URL(路由对象的 prefix + 装饰器里的路径)/ 方法 / 路径参数 / 查询参数 / 请求体,没有的项不列。响应表每个状态码一行(状态码 / 内容 / 说明):成功那行来自 `status_code` 和 `response_model`,`responses={…}` 里写的各占一行,函数体里直接抛的异常按异常类上的 `status_code` 归行、说明列出抛出时的消息,有参数时 FastAPI 自动带的 422 淡色。挂到应用上时再叠的前缀(`/api/v1`)在别的文件里,不算 | | `Prototype / 块种类图标` | — | 每种块一个图标、颜色、名称的图例,库规则带的种类后面淡色写规则名;最上面一块「返回」停着鼠标、图标下面弹出名称提示 | | `Prototype / 遍历 · 推导式` | luffy 邀请码工具的 `"".join(secrets.choice(ALPHABET) for _ in range(length))`,外加带条件的列表、字典推导 | 写在表达式里的遍历原地套一个带循环图标和结果类型(生成器 / 列表 / 字典)的小框,框里按读的顺序重排:「遍历 range(length) 的每个 _,仅当 …,取 …」 | | `Prototype / 侧栏 · 复制菜单` | 同上 | 悬停行行尾第一个按钮「复制」(带小箭头),点开一组复制方式:文件名 / 文件名(不带后缀)/ 相对路径 / 绝对路径,右边淡色预览会复制出什么;文件夹没有「不带后缀」 | @@ -69,7 +69,7 @@ - `Reader / TableCard`:SQLAlchemy 规则认出的数据表卡片。 - `Reader / EnumCard`:枚举卡片。 - `Reader / CeleryAppCard`、`Reader / BeatCard`:Celery 规则的两张卡片。 -- `Reader / RouteCard`:FastAPI 规则的接口卡片。 +- `Reader / RouterCard`、`Reader / RouteCard`:FastAPI 规则的路由器卡片和接口卡片。 - `Reader / IterationBlock`:`Sample=generator | list | dict`,三种推导式的返回块。 - `Reader / FilterChip`:`State=default | active`,筛选栏里一个属性的下拉框,各筛选栏共用。 - `Reader / FilterBar`:`Active=none | access | kind`,预览顶部的筛选栏,打开的下拉菜单是勾选列表,每个值后面是块数。 diff --git a/docs/prototype/code-reader/figma-plugin/code.js b/docs/prototype/code-reader/figma-plugin/code.js index 11c9367..dda2a1c 100644 --- a/docs/prototype/code-reader/figma-plugin/code.js +++ b/docs/prototype/code-reader/figma-plugin/code.js @@ -907,6 +907,7 @@ const LUCIDE = { "server-cog": [["path", { d: "m10.852 14.772-.383.923" }], ["path", { d: "M13.148 14.772a3 3 0 1 0-2.296-5.544l-.383-.923" }], ["path", { d: "m13.148 9.228.383-.923" }], ["path", { d: "m13.53 15.696-.382-.924a3 3 0 1 1-2.296-5.544" }], ["path", { d: "m14.772 10.852.923-.383" }], ["path", { d: "m14.772 13.148.923.383" }], ["path", { d: "M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5" }], ["path", { d: "M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5" }], ["path", { d: "M6 18h.01" }], ["path", { d: "M6 6h.01" }], ["path", { d: "m9.228 10.852-.923-.383" }], ["path", { d: "m9.228 13.148-.923.383" }]], "calendar-clock": [["path", { d: "M16 14v2.2l1.6 1" }], ["path", { d: "M16 2v4" }], ["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5" }], ["path", { d: "M3 10h5" }], ["path", { d: "M8 2v4" }], ["circle", { cx: "16", cy: "16", r: "6" }]], "globe": [["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }], ["path", { d: "M2 12h20" }]], + "network": [["rect", { x: "16", y: "16", width: "6", height: "6", rx: "1" }], ["rect", { x: "2", y: "16", width: "6", height: "6", rx: "1" }], ["rect", { x: "9", y: "2", width: "6", height: "6", rx: "1" }], ["path", { d: "M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3" }], ["path", { d: "M12 12V8" }]], "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" }]], @@ -1130,7 +1131,38 @@ function beatCard(ctx) { // 有参数或请求体时 FastAPI 自动带 422。 // 挂到应用上时再叠的前缀(include_router、settings.API_V1_PREFIX)在别的文件里,URL 不算。 // 示例:luffy 的 user/routes.py 的 update_user;Forbidden 在 core/exceptions/schemas.py,status_code = 403。 +// +// 路由器(`X = APIRouter(...)` / `FastAPI(...)`)和挂在它上面的接口是两回事,单独一张「路由器」卡片: +// 名称、前缀、标签、依赖(dependencies=)、默认响应(responses=,写了才有),接口表列这个文件里挂在它上面的全部接口(方法 / 路径 / 函数 / 说明), +// 路径是完整的(前缀 + 装饰器里的路径),函数名 Cmd+单击跳到那张接口卡片。接口卡片多一行「路由器」写挂在哪个上面。 +// 接口多一个筛选属性「请求方法」(GET / POST / …),由 FastAPI 规则报。 const ROUTE_KIND = { label: "接口", color: "#61afef", icon: "globe", rule: "FastAPI" }; +const ROUTER_KIND = { label: "路由器", color: "#7aa2f7", icon: "network", rule: "FastAPI" }; +const ROUTER_SAMPLE = { + name: "router", prefix: "/users", tags: ["用户管理"], + routes: [ + ["GET", "/users/{user_id}", "get_user", "获取用户详情。"], + ["PUT", "/users/{user_id}", "update_user", "更新用户信息。"], + ["POST", "/users/me/avatar", "upload_current_user_avatar", "上传当前登录用户头像。"], + ], +}; + +/** Reader / RouterCard — State=preview:FastAPI 规则认出的路由器卡片,接口表列挂在它上面的接口。 */ +function routerCard(ctx) { + const router = ROUTER_SAMPLE; + const { node, content } = cardShell(ctx, "State=preview", ROUTER_KIND, { kids: false }); + propRow(ctx, content, "名称", [{ text: router.name, color: COLORS.tokenVariable }]); + propRow(ctx, content, "前缀", pathSegments(router.prefix)); + propRow(ctx, content, "标签", [{ text: router.tags.join("、"), color: COLORS.tokenString }]); + propRow(ctx, content, "接口", countSegments(router.routes), { twisty: true }); + propTable(ctx, content, "Routes", [["方法", 80], ["路径", 240], ["函数", 260], ["说明", 0]], router.routes.map(([method, path, fn, doc]) => [`Route / ${fn}`, [ + [{ text: method, color: HTTP_METHOD_COLORS[method] }], + pathSegments(path), + [{ text: fn, color: COLORS.tokenFunction }], + [{ text: doc, color: COLORS.comment }], + ]])); + return node; +} const DATACLASS_KIND = { label: "数据类", color: "#ef7fb0", icon: "package", rule: "dataclasses" }; // Swagger UI 的方法配色,看接口文档的人最熟 const HTTP_METHOD_COLORS = { GET: "#61affe", POST: "#49cc90", PUT: "#fca130", PATCH: "#50e3c2", DELETE: "#f93e3e" }; @@ -1165,6 +1197,7 @@ function routeCard(ctx) { const { node, content } = cardShell(ctx, "State=preview", ROUTE_KIND, { kids: true }); propRow(ctx, content, "名称", [{ text: route.name, color: COLORS.tokenFunction }]); propRow(ctx, content, "说明", [{ text: route.doc, color: COLORS.comment }]); + propRow(ctx, content, "路由器", [{ text: ROUTER_SAMPLE.name, color: COLORS.tokenVariable }]); propRow(ctx, content, "标签", [{ text: route.tags.join("、"), color: COLORS.tokenString }]); propRow(ctx, content, "异步", yesNo(route.async)); propRow(ctx, content, "依赖", route.deps.flatMap(([param, fn], index) => [ @@ -1955,6 +1988,7 @@ function createComponents(ctx, page) { lib.enumCard = make("Reader / EnumCard", ["preview"], 1, () => enumCard(ctx)); lib.celeryApp = make("Reader / CeleryAppCard", ["preview"], 1, () => celeryAppCard(ctx)); lib.beat = make("Reader / BeatCard", ["preview"], 1, () => beatCard(ctx)); + lib.router = make("Reader / RouterCard", ["preview"], 1, () => routerCard(ctx)); lib.route = make("Reader / RouteCard", ["preview"], 1, () => routeCard(ctx)); lib.iteration = make("Reader / IterationBlock", ITERATION_SAMPLES.map((sample) => sample.key), 3, (key) => iterationBlock(ctx, ITERATION_SAMPLES.find((sample) => sample.key === key))); @@ -2264,7 +2298,7 @@ function kindLegend(ctx, page, lib) { tip.layoutPositioning = "ABSOLUTE"; tip.x = 26; tip.y = 34; - const rule = [TABLE_KIND, DATACLASS_KIND, CELERY_APP_KIND, BEAT_KIND, ROUTE_KIND]; + const rule = [TABLE_KIND, DATACLASS_KIND, CELERY_APP_KIND, BEAT_KIND, ROUTER_KIND, ROUTE_KIND]; for (const spec of [...Object.values(BLOCK_KINDS), ...EXTRA_KINDS, ...rule]) { const row = filled(ctx, board, `Kind row / ${spec.label}`, { dir: "HORIZONTAL", gap: 10, align: "CENTER" }); kindBadge(ctx, row, spec); @@ -2345,6 +2379,9 @@ async function build(api, stage = () => {}) { } const routeBoard = frame(ctx, "Prototype / 库规则 · FastAPI 接口", { width: TREE_W + 2 * VIEW_PAD, pad: VIEW_PAD, gap: 8, fill: COLORS.editor }); page.appendChild(routeBoard); + const routerInstance = lib.router.get("preview").createInstance(); + routerInstance.name = `Block / ${ROUTER_SAMPLE.name}`; + append(routeBoard, routerInstance, true); const routeInstance = lib.route.get("preview").createInstance(); routeInstance.name = `Block / ${ROUTE_SAMPLE.name}`; append(routeBoard, routeInstance, true); @@ -2469,6 +2506,6 @@ async function run(api) { } if (typeof module !== "undefined") { - module.exports = { 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 = { 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); diff --git a/docs/prototype/code-reader/figma-plugin/test.js b/docs/prototype/code-reader/figma-plugin/test.js index bbd115a..828eafa 100644 --- a/docs/prototype/code-reader/figma-plugin/test.js +++ b/docs/prototype/code-reader/figma-plugin/test.js @@ -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 { 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 { 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]; } @@ -35,6 +35,7 @@ const EXPECTED_SETS = [ ["Reader / EnumCard", 1], ["Reader / CeleryAppCard", 1], ["Reader / BeatCard", 1], + ["Reader / RouterCard", 1], ["Reader / RouteCard", 1], ["Reader / IterationBlock", 3], ["Reader / FilterChip", 2], @@ -432,8 +433,13 @@ test("the Celery rule draws the app and the beat schedule", async () => { test("the FastAPI rule turns a route into a card with a request table and a status-code table", async () => { const api = fakeFigma(); const { routeBoard, components } = await build(api); - assert.equal(routeBoard.children.length, 1); + assert.equal(routeBoard.children.length, 2); + // 路由器和接口是两张卡片:路由器的接口表列挂在它上面的全部接口,接口卡片写挂在哪个路由器上 + const router = components.lib.router.get("preview"); + assert.ok(named(router, "Kind / 路由器")); + assert.deepEqual(router.findAll((node) => node.name.startsWith("Route / ")).map((node) => node.name.slice(8)), ROUTER_SAMPLE.routes.map(([, , fn]) => fn)); const card = components.lib.route.get("preview"); + assert.equal(named(named(card, "Prop / 路由器"), "Value").characters, ROUTER_SAMPLE.name); assert.equal(named(named(card, "Prop / 依赖"), "Value").characters, "get_db(db)、get_current_user(current_user)"); // 请求:一项一行 const cell = (row, column) => named(named(named(card, row), column), "Text").characters;