chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
79
blueprint/README.md
Normal file
79
blueprint/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 应用蓝图落地说明(v1)
|
||||
|
||||
用户上传「描述/图片 + Excel」后,系统只围绕一份 **AppBlueprint JSON** 工作:AI 生成 → 校验 → 用户确认 → 中台执行建库/挂 API/渲染页面。
|
||||
|
||||
## 目录
|
||||
|
||||
| 路径 | 作用 |
|
||||
|------|------|
|
||||
| `schema/app-blueprint.schema.json` | 蓝图 JSON Schema(契约) |
|
||||
| `examples/inventory-ledger.blueprint.json` | 完整示例(库存台账) |
|
||||
| `openapi/dynamic-crud.openapi.yaml` | 动态 API 形状(按蓝图挂载) |
|
||||
| `platform/execution-steps.md` | 中台执行步骤与安全闸门 |
|
||||
| `ai/prompt-contract.md` | AI 服务输入输出约定 |
|
||||
|
||||
## 主链路
|
||||
|
||||
```text
|
||||
1) 上传 excel + prompt/image
|
||||
2) FastAPI:解析 Excel + 多模态理解 → 产出 blueprint draft
|
||||
3) 用 JSON Schema 校验;置信度 < 0.75 强制人工确认
|
||||
4) 用户确认/改字段
|
||||
5) go-zero:
|
||||
- 分配 schema_name / database
|
||||
- CREATE TABLE(白名单 DDL)
|
||||
- 注册元数据
|
||||
- 按 apis 挂动态 CRUD
|
||||
- 可选导入 Excel 行
|
||||
6) 前端:按 pages[] 渲染,不落不可控源码
|
||||
```
|
||||
|
||||
## 服务边界
|
||||
|
||||
### AI 服务(FastAPI)
|
||||
|
||||
- **能做**:推断实体、字段类型、页面布局、图表建议
|
||||
- **不能做**:直接连业务库执行 SQL、注册网关路由、写连接串
|
||||
|
||||
输出:`AppBlueprint` draft + `warnings[]`
|
||||
|
||||
### 中台(go-zero)
|
||||
|
||||
- **能做**:租户鉴权、DDL 执行、元数据落库、动态 API、审计
|
||||
- **不能做**:信任模型原文;必须 Schema 校验 + 标识符白名单
|
||||
|
||||
### 前端渲染器
|
||||
|
||||
输入:`GET /api/v1/apps/{slug}/blueprint`
|
||||
按 `pages[].type` 渲染 list/form/dashboard。
|
||||
|
||||
## 动态 API(示例应用)
|
||||
|
||||
确认并发布后,库存示例会得到:
|
||||
|
||||
```http
|
||||
GET /api/v1/apps/inventory_ledger/inventory_items
|
||||
GET /api/v1/apps/inventory_ledger/inventory_items/{id}
|
||||
POST /api/v1/apps/inventory_ledger/inventory_items
|
||||
PATCH /api/v1/apps/inventory_ledger/inventory_items/{id}
|
||||
DELETE /api/v1/apps/inventory_ledger/inventory_items/{id}
|
||||
POST /api/v1/apps/inventory_ledger/inventory_items:import
|
||||
GET /api/v1/apps/inventory_ledger/inventory_items:export
|
||||
```
|
||||
|
||||
所有请求强制:`Authorization` + 租户上下文;`row_policies=tenant_isolated` 由服务端注入,客户端不可关。
|
||||
|
||||
## 本地快速校验示例
|
||||
|
||||
用任意 JSON Schema 校验器验证示例:
|
||||
|
||||
```bash
|
||||
# 需本机有 ajv-cli 或等价工具
|
||||
npx --yes ajv-cli validate -s blueprint/schema/app-blueprint.schema.json -d blueprint/examples/inventory-ledger.blueprint.json
|
||||
```
|
||||
|
||||
## 版本策略
|
||||
|
||||
- `version: "1.0"` 固定本契约
|
||||
- 不兼容变更升 `1.1` / `2.0`,旧应用蓝图可继续读取
|
||||
- AI 与中台只认 Schema 内字段;多余字段校验失败
|
||||
71
blueprint/ai/prompt-contract.md
Normal file
71
blueprint/ai/prompt-contract.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# AI 服务输入输出约定
|
||||
|
||||
## 输入
|
||||
|
||||
`POST /api/v1/apps:generate`(可由网关转到 FastAPI)
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| prompt | 是 | 用户自然语言需求 |
|
||||
| excel | 否 | xlsx/xls/csv |
|
||||
| images[] | 否 | 页面截图/手绘线框,最多 10 |
|
||||
| storage_mode | 否 | 默认 `schema_per_app` |
|
||||
|
||||
Excel 解析侧建议先产出中间结构再喂给 LLM:
|
||||
|
||||
```json
|
||||
{
|
||||
"sheets": [
|
||||
{
|
||||
"name": "Sheet1",
|
||||
"headers": ["SKU", "商品名称", "仓库", "数量", "单价", "状态", "更新时间"],
|
||||
"sample_rows": [
|
||||
["A-1001", "无线鼠标", "华东仓", 120, 59.9, "在售", "2026-07-01 10:00:00"]
|
||||
],
|
||||
"inferred_types": {
|
||||
"SKU": "string",
|
||||
"数量": "int",
|
||||
"单价": "decimal"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 输出
|
||||
|
||||
```json
|
||||
{
|
||||
"draft": { "...": "AppBlueprint" },
|
||||
"warnings": [
|
||||
"列「备注」样本过少,已标为可空 text",
|
||||
"截图中有图表区域,已生成 dashboard 草案"
|
||||
],
|
||||
"confidence": 0.86,
|
||||
"require_confirm": true
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `draft` 必须能通过 `app-blueprint.schema.json`
|
||||
- `confidence < 0.75` ⇒ `require_confirm=true`(前端禁止一键跳过)
|
||||
- 字段名、表名由 AI 生成后,**发布前中台会再次规范化/重写**
|
||||
- 不得在输出中包含数据库连接串、密码、任意 SQL
|
||||
|
||||
## 系统提示词要点(实现时使用)
|
||||
|
||||
1. 只输出符合 Schema 的 JSON(可包在 draft 字段)
|
||||
2. 一表对应 Excel 主 sheet;多 sheet 才多 entity(≤ 20)
|
||||
3. 优先 enum:当某列 distinct 值 ≤ 20 且稳定
|
||||
4. 页面至少包含一个 `list`;若用户提到统计再加 `dashboard`
|
||||
5. API operations 默认 `list/get/create/update/delete`;提到导入导出再加
|
||||
6. `security.visibility` 默认 `private`,`row_policies` 默认 `tenant_isolated`
|
||||
|
||||
## 与中台的分工
|
||||
|
||||
```text
|
||||
AI:理解意图,填蓝图
|
||||
中台:校验、改写标识符、建表、挂路由、强制租户隔离
|
||||
前端:确认页编辑蓝图 → publish → 用 blueprint 渲染
|
||||
```
|
||||
336
blueprint/examples/inventory-ledger.blueprint.json
Normal file
336
blueprint/examples/inventory-ledger.blueprint.json
Normal file
@@ -0,0 +1,336 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"meta": {
|
||||
"name": "库存台账",
|
||||
"slug": "inventory_ledger",
|
||||
"description": "根据上传的库存 Excel 与「要一个可筛选的库存列表页」描述生成",
|
||||
"locale": "zh-CN",
|
||||
"source": {
|
||||
"prompt": "根据这张库存表生成可筛选、可编辑的数据展示页,支持按仓库和状态过滤",
|
||||
"image_refs": ["oss://uploads/u_1001/mockups/inventory-list.png"],
|
||||
"excel_ref": "oss://uploads/u_1001/excel/inventory.xlsx"
|
||||
},
|
||||
"confidence": 0.86
|
||||
},
|
||||
"storage": {
|
||||
"mode": "schema_per_app",
|
||||
"engine": "postgres",
|
||||
"schema_name": "app_inventory_ledger"
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"name": "inventory_item",
|
||||
"table": "inventory_item",
|
||||
"label": "库存明细",
|
||||
"primary_key": "id",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"label": "ID",
|
||||
"type": "bigint",
|
||||
"nullable": false,
|
||||
"ui": { "widget": "hidden", "listable": false }
|
||||
},
|
||||
{
|
||||
"name": "sku",
|
||||
"label": "SKU",
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"unique": true,
|
||||
"max_length": 64,
|
||||
"ui": {
|
||||
"widget": "input",
|
||||
"listable": true,
|
||||
"filterable": true,
|
||||
"sortable": true,
|
||||
"width": "md"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "SKU",
|
||||
"sample_values": ["A-1001", "A-1002", "B-2001"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "product_name",
|
||||
"label": "商品名称",
|
||||
"type": "string",
|
||||
"nullable": false,
|
||||
"max_length": 128,
|
||||
"ui": {
|
||||
"widget": "input",
|
||||
"listable": true,
|
||||
"filterable": true,
|
||||
"sortable": true,
|
||||
"width": "lg"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "商品名称",
|
||||
"sample_values": ["无线鼠标", "机械键盘", "显示器支架"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "warehouse",
|
||||
"label": "仓库",
|
||||
"type": "enum",
|
||||
"nullable": false,
|
||||
"enum_values": ["华东仓", "华南仓", "华北仓"],
|
||||
"ui": {
|
||||
"widget": "select",
|
||||
"listable": true,
|
||||
"filterable": true,
|
||||
"sortable": false,
|
||||
"width": "sm"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "仓库",
|
||||
"sample_values": ["华东仓", "华南仓"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "qty",
|
||||
"label": "库存数量",
|
||||
"type": "int",
|
||||
"nullable": false,
|
||||
"default": 0,
|
||||
"ui": {
|
||||
"widget": "number",
|
||||
"listable": true,
|
||||
"filterable": false,
|
||||
"sortable": true,
|
||||
"width": "sm"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "数量",
|
||||
"sample_values": ["120", "35", "8"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unit_price",
|
||||
"label": "单价",
|
||||
"type": "decimal",
|
||||
"nullable": true,
|
||||
"precision": 12,
|
||||
"scale": 2,
|
||||
"ui": {
|
||||
"widget": "number",
|
||||
"listable": true,
|
||||
"sortable": true,
|
||||
"width": "sm"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "单价",
|
||||
"sample_values": ["59.90", "299.00"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"label": "状态",
|
||||
"type": "enum",
|
||||
"nullable": false,
|
||||
"enum_values": ["在售", "停售", "盘点中"],
|
||||
"ui": {
|
||||
"widget": "select",
|
||||
"listable": true,
|
||||
"filterable": true,
|
||||
"width": "sm"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "状态",
|
||||
"sample_values": ["在售", "停售"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"label": "更新时间",
|
||||
"type": "datetime",
|
||||
"nullable": false,
|
||||
"ui": {
|
||||
"widget": "datepicker",
|
||||
"listable": true,
|
||||
"sortable": true,
|
||||
"width": "md"
|
||||
},
|
||||
"from_excel": {
|
||||
"column": "更新时间",
|
||||
"sample_values": ["2026-07-01 10:00:00"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_inventory_item_warehouse_status",
|
||||
"columns": ["warehouse", "status"],
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"name": "idx_inventory_item_sku",
|
||||
"columns": ["sku"],
|
||||
"unique": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"apis": {
|
||||
"base_path": "/api/v1/apps/inventory_ledger",
|
||||
"resources": [
|
||||
{
|
||||
"entity": "inventory_item",
|
||||
"path": "/inventory_items",
|
||||
"operations": [
|
||||
"list",
|
||||
"get",
|
||||
"create",
|
||||
"update",
|
||||
"delete",
|
||||
"import",
|
||||
"export"
|
||||
],
|
||||
"list": {
|
||||
"default_page_size": 20,
|
||||
"max_page_size": 100,
|
||||
"allowed_filters": ["sku", "product_name", "warehouse", "status"],
|
||||
"allowed_sorts": ["sku", "qty", "unit_price", "updated_at"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"id": "inventory_list",
|
||||
"title": "库存列表",
|
||||
"route": "/inventory",
|
||||
"type": "list",
|
||||
"entity": "inventory_item",
|
||||
"layout": {
|
||||
"columns": [
|
||||
"sku",
|
||||
"product_name",
|
||||
"warehouse",
|
||||
"qty",
|
||||
"unit_price",
|
||||
"status",
|
||||
"updated_at"
|
||||
],
|
||||
"filters": ["warehouse", "status", "sku"],
|
||||
"actions": ["create", "edit", "delete", "export", "import", "refresh"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inventory_create",
|
||||
"title": "新增库存",
|
||||
"route": "/inventory/create",
|
||||
"type": "form_create",
|
||||
"entity": "inventory_item",
|
||||
"layout": {
|
||||
"form_fields": [
|
||||
"sku",
|
||||
"product_name",
|
||||
"warehouse",
|
||||
"qty",
|
||||
"unit_price",
|
||||
"status"
|
||||
],
|
||||
"actions": ["create"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inventory_edit",
|
||||
"title": "编辑库存",
|
||||
"route": "/inventory/:id/edit",
|
||||
"type": "form_edit",
|
||||
"entity": "inventory_item",
|
||||
"layout": {
|
||||
"form_fields": [
|
||||
"sku",
|
||||
"product_name",
|
||||
"warehouse",
|
||||
"qty",
|
||||
"unit_price",
|
||||
"status"
|
||||
],
|
||||
"actions": ["edit"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "inventory_dashboard",
|
||||
"title": "库存概览",
|
||||
"route": "/inventory/dashboard",
|
||||
"type": "dashboard",
|
||||
"entity": "inventory_item",
|
||||
"layout": {
|
||||
"widgets": [
|
||||
{
|
||||
"type": "kpi",
|
||||
"title": "SKU 总数",
|
||||
"metric": "count",
|
||||
"entity": "inventory_item"
|
||||
},
|
||||
{
|
||||
"type": "kpi",
|
||||
"title": "总库存量",
|
||||
"metric": "sum:qty",
|
||||
"entity": "inventory_item"
|
||||
},
|
||||
{
|
||||
"type": "bar_chart",
|
||||
"title": "各仓库库存量",
|
||||
"metric": "sum:qty",
|
||||
"group_by": "warehouse",
|
||||
"entity": "inventory_item"
|
||||
},
|
||||
{
|
||||
"type": "pie_chart",
|
||||
"title": "状态分布",
|
||||
"metric": "count",
|
||||
"group_by": "status",
|
||||
"entity": "inventory_item"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"visibility": "private",
|
||||
"roles": [
|
||||
{
|
||||
"name": "owner",
|
||||
"permissions": [
|
||||
"app.read",
|
||||
"app.write",
|
||||
"app.admin",
|
||||
"row.create",
|
||||
"row.read",
|
||||
"row.update",
|
||||
"row.delete",
|
||||
"row.export",
|
||||
"row.import"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "editor",
|
||||
"permissions": [
|
||||
"app.read",
|
||||
"row.create",
|
||||
"row.read",
|
||||
"row.update",
|
||||
"row.export",
|
||||
"row.import"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "viewer",
|
||||
"permissions": ["app.read", "row.read", "row.export"]
|
||||
}
|
||||
],
|
||||
"row_policies": [
|
||||
{
|
||||
"entity": "inventory_item",
|
||||
"rule": "tenant_isolated"
|
||||
}
|
||||
]
|
||||
},
|
||||
"seed": {
|
||||
"import_excel": true,
|
||||
"max_rows": 5000
|
||||
}
|
||||
}
|
||||
39
blueprint/openapi/README.md
Normal file
39
blueprint/openapi/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# API 契约(唯一)
|
||||
|
||||
- **运行时契约**:`GET /api/v1/meta/openapi.yaml`(与代码同嵌入)
|
||||
- **目录清单**:`GET /api/v1/meta/apis`
|
||||
- **源文件**:`platform/internal/handler/openapi.yaml` + `platform/internal/apidef/catalog.go`
|
||||
|
||||
## 动词
|
||||
|
||||
| 方法 | 用途 |
|
||||
|------|------|
|
||||
| GET | 读取 |
|
||||
| POST | 创建,或无幂等动作(login / import / publish / generate) |
|
||||
| PUT | 更新 |
|
||||
| DELETE | 删除 |
|
||||
|
||||
禁止再用 PATCH 作为更新主路径;禁止为行业场景新增平行 CRUD。
|
||||
|
||||
## 动态业务数据
|
||||
|
||||
仅:
|
||||
|
||||
```http
|
||||
GET|POST /api/v1/apps/{slug}/{resource}
|
||||
GET|PUT|DELETE /api/v1/apps/{slug}/{resource}/{id}
|
||||
POST /api/v1/apps/{slug}/{resource}/import
|
||||
GET /api/v1/apps/{slug}/{resource}/export
|
||||
GET /api/v1/apps/{slug}/{resource}/aggregate
|
||||
```
|
||||
|
||||
字段由蓝图定义,平台不做行业写死接口。
|
||||
|
||||
## AI(不重复实现)
|
||||
|
||||
经网关 `/ai` 前缀:
|
||||
|
||||
```http
|
||||
POST /ai/api/v1/apps/generate
|
||||
GET /ai/api/v1/llm/providers
|
||||
```
|
||||
326
blueprint/openapi/dynamic-crud.openapi.yaml
Normal file
326
blueprint/openapi/dynamic-crud.openapi.yaml
Normal file
@@ -0,0 +1,326 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: AI建站 Platform API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
通用中台契约。业务行数据仅通过 apps/{slug}/{resource} CRUD 传输;
|
||||
行业字段由蓝图定义,不在此增加行业专用路由。
|
||||
动词约定:GET 读 / POST 创建或动作 / PUT 更新 / DELETE 删除。
|
||||
servers:
|
||||
- url: http://127.0.0.1:8180
|
||||
paths:
|
||||
/api/v1/meta/apis:
|
||||
get:
|
||||
operationId: listApis
|
||||
summary: API 目录
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
/api/v1/meta/openapi.yaml:
|
||||
get:
|
||||
operationId: getOpenAPI
|
||||
summary: OpenAPI 原文
|
||||
responses:
|
||||
"200":
|
||||
description: YAML
|
||||
/api/v1/auth/register:
|
||||
post:
|
||||
operationId: authRegister
|
||||
summary: 注册
|
||||
responses:
|
||||
"201": { description: Created }
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
operationId: authLogin
|
||||
summary: 登录
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/api/v1/auth/token:
|
||||
post:
|
||||
operationId: authToken
|
||||
summary: 服务签发 JWT
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/apps/{slug}/publish:
|
||||
post:
|
||||
operationId: publishApp
|
||||
summary: 发布蓝图
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/api/v1/apps/{slug}/blueprint:
|
||||
get:
|
||||
operationId: getBlueprint
|
||||
summary: 读取蓝图
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
/api/v1/apps/{slug}/agent-capsule:
|
||||
get:
|
||||
operationId: getAgentCapsule
|
||||
summary: 智能体胶囊
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
get:
|
||||
operationId: listRows
|
||||
summary: 列表
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, default: 1 }
|
||||
- name: page_size
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
||||
- name: sort
|
||||
in: query
|
||||
schema: { type: string }
|
||||
- name: filter.*
|
||||
in: query
|
||||
description: 如 filter.status=在售,键须在蓝图 allowed_filters
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PageResult"
|
||||
post:
|
||||
operationId: createRow
|
||||
summary: 创建
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Row"
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/{id}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Id"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
get:
|
||||
operationId: getRow
|
||||
summary: 详情
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Row"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
put:
|
||||
operationId: updateRow
|
||||
summary: 更新
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Row"
|
||||
delete:
|
||||
operationId: deleteRow
|
||||
summary: 删除
|
||||
responses:
|
||||
"204": { description: No Content }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/import:
|
||||
post:
|
||||
operationId: importRows
|
||||
summary: 导入
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file: { type: string, format: binary }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/export:
|
||||
get:
|
||||
operationId: exportRows
|
||||
summary: 导出
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- name: format
|
||||
in: query
|
||||
schema: { type: string, enum: [xlsx, csv], default: xlsx }
|
||||
responses:
|
||||
"200": { description: 文件流 }
|
||||
|
||||
/api/v1/apps/{slug}/{resource}/aggregate:
|
||||
get:
|
||||
operationId: aggregateRows
|
||||
summary: 聚合
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Slug"
|
||||
- $ref: "#/components/parameters/Resource"
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- name: group_by
|
||||
in: query
|
||||
schema: { type: string }
|
||||
- name: sum
|
||||
in: query
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/audit/logs:
|
||||
get:
|
||||
operationId: listAuditLogs
|
||||
summary: 审计日志
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/storage:
|
||||
post:
|
||||
operationId: uploadObject
|
||||
summary: 上传
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file: { type: string, format: binary }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/storage/{tenant}/{day}/{name}:
|
||||
get:
|
||||
operationId: downloadObject
|
||||
summary: 下载
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Authorization"
|
||||
- name: tenant
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: day
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200": { description: 文件流 }
|
||||
|
||||
/api/v1/apps/generate:
|
||||
post:
|
||||
operationId: generateBlueprint
|
||||
summary: 生成蓝图(AI 服务,经网关 /ai 前缀)
|
||||
description: 实际请求 /ai/api/v1/apps/generate;勿在 platform 重复实现。
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
prompt: { type: string }
|
||||
excel: { type: string, format: binary }
|
||||
images: { type: string, format: binary }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api/v1/llm/providers:
|
||||
get:
|
||||
operationId: listLlmProviders
|
||||
summary: LLM 厂商(AI 服务)
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
components:
|
||||
parameters:
|
||||
Authorization:
|
||||
name: Authorization
|
||||
in: header
|
||||
required: true
|
||||
schema: { type: string }
|
||||
description: Bearer JWT
|
||||
Slug:
|
||||
name: slug
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string, pattern: "^[a-z][a-z0-9_]{1,47}$" }
|
||||
Resource:
|
||||
name: resource
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
description: 蓝图 apis.resources.path(无前导 /);不可为保留名
|
||||
Id:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
schemas:
|
||||
PageResult:
|
||||
type: object
|
||||
properties:
|
||||
items: { type: array, items: { $ref: "#/components/schemas/Row" } }
|
||||
total: { type: integer }
|
||||
Row:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
NotFound:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer }
|
||||
message: { type: string }
|
||||
128
blueprint/platform/execution-steps.md
Normal file
128
blueprint/platform/execution-steps.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# 中台执行步骤与安全闸门
|
||||
|
||||
面向 go-zero(或等价中台)实现 `POST /api/v1/apps/{slug}:publish`。
|
||||
|
||||
## 1. 入口校验
|
||||
|
||||
1. 鉴权:用户已登录,具备 `app.admin` 或「创建应用」权限
|
||||
2. 限流:每租户每小时发布次数上限(建议 ≤ 20)
|
||||
3. Body 必须是完整 `AppBlueprint`
|
||||
4. 用 `app-blueprint.schema.json` 校验;失败直接 400
|
||||
5. `meta.slug` 与 path 中 slug 一致;租户内唯一
|
||||
|
||||
## 2. 标识符白名单(防 SQL 注入)
|
||||
|
||||
仅允许匹配:
|
||||
|
||||
```text
|
||||
^[a-z][a-z0-9_]{1,47}$
|
||||
```
|
||||
|
||||
校验对象:`slug`、`entity.name/table`、所有 `field.name`、`index.name`
|
||||
拒绝:大小写混用、连字符、空格、引号、注释符、SQL 关键字作表名(建议黑名单:`select/drop/user/...`)
|
||||
|
||||
## 3. 分配存储(用户不可指定 DSN)
|
||||
|
||||
| storage.mode | 行为 |
|
||||
|--------------|------|
|
||||
| `schema_per_app` | 平台在共享实例创建 `schema_name`(可忽略 AI 填的名字,按 `app_{tenant}_{slug}` 重写) |
|
||||
| `database_per_app` | 平台开通独立库,凭证写入 KMS/密钥服务 |
|
||||
|
||||
落库元数据示例:
|
||||
|
||||
```text
|
||||
tenant_apps(app_id, tenant_id, slug, schema_name, engine, blueprint_json, status, created_at)
|
||||
tenant_app_entities(...)
|
||||
tenant_app_fields(...)
|
||||
tenant_app_apis(...)
|
||||
```
|
||||
|
||||
## 4. 生成并执行 DDL(参数化/白名单拼接)
|
||||
|
||||
伪代码原则:
|
||||
|
||||
- 只拼已经过白名单的标识符
|
||||
- 字段类型映射用固定字典,禁止把 AI 的 type 字符串直接塞进 SQL
|
||||
|
||||
类型映射建议(Postgres):
|
||||
|
||||
| blueprint type | SQL |
|
||||
|----------------|-----|
|
||||
| string | VARCHAR(n) |
|
||||
| text | TEXT |
|
||||
| int | INTEGER |
|
||||
| bigint | BIGINT |
|
||||
| decimal | NUMERIC(p,s) |
|
||||
| boolean | BOOLEAN |
|
||||
| date | DATE |
|
||||
| datetime | TIMESTAMPTZ |
|
||||
| enum | VARCHAR(n) + CHECK |
|
||||
| json | JSONB |
|
||||
| file_ref | VARCHAR(512) |
|
||||
|
||||
每个表强制附加系统列(即使蓝图未写):
|
||||
|
||||
```sql
|
||||
tenant_id BIGINT NOT NULL,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
```
|
||||
|
||||
`row_policies=tenant_isolated` → 所有查询自动 `AND tenant_id = :current_tenant`。
|
||||
|
||||
## 5. 注册动态 API
|
||||
|
||||
读取 `apis.resources[]`:
|
||||
|
||||
- 在网关/服务路由表注册 path
|
||||
- `operations` 决定开放方法
|
||||
- `allowed_filters/sorts` 写入配置;运行时拒绝未声明字段
|
||||
|
||||
禁止:按请求参数动态选物理表名。
|
||||
允许:`app_slug + resource` → 元数据查出 `schema.table`。
|
||||
|
||||
## 6. 种子数据
|
||||
|
||||
若 `seed.import_excel=true`:
|
||||
|
||||
1. 从 `meta.source.excel_ref` 拉文件
|
||||
2. 按 `field.from_excel.column` 映射
|
||||
3. 行数 ≤ `seed.max_rows`
|
||||
4. 批量插入,单行失败记入 errors,不中断整单(或按策略 fail-fast)
|
||||
|
||||
## 7. 发布事务与回滚
|
||||
|
||||
建议状态机:
|
||||
|
||||
```text
|
||||
draft → validating → provisioning → importing → published
|
||||
↘ failed(保留草稿,DDL 尽量事务/可回滚)
|
||||
```
|
||||
|
||||
失败时:
|
||||
|
||||
- schema 已建:标记 `failed`,提供 `DELETE app` 清理
|
||||
- 写审计:谁、何时、哪份 blueprint hash、结果
|
||||
|
||||
## 8. 运行时读路径(列表页)
|
||||
|
||||
```text
|
||||
Auth → 解析 tenant → 查 app 元数据 → 校验 resource
|
||||
→ 校验 filter/sort 白名单 → 组装 SELECT
|
||||
→ 强制 tenant_id 条件 → 分页返回
|
||||
```
|
||||
|
||||
前端只读 blueprint 渲染,不信任客户端传来的「表名/SQL」。
|
||||
|
||||
## 9. 最小落地服务拆分
|
||||
|
||||
| 服务 | 职责 |
|
||||
|------|------|
|
||||
| `ai-generate` | 素材 → draft blueprint |
|
||||
| `app-meta` | 蓝图存储、校验、发布状态 |
|
||||
| `schema-runner` | DDL / 迁移 / 清理 |
|
||||
| `dynamic-crud` | 统一 CRUD / import / export |
|
||||
| `gateway` | 鉴权、限流、WAF、路由 |
|
||||
|
||||
首版可先 2 个进程:`ai-generate(FastAPI)` + `platform(go-zero 含 meta/runner/crud)`。
|
||||
483
blueprint/schema/app-blueprint.schema.json
Normal file
483
blueprint/schema/app-blueprint.schema.json
Normal file
@@ -0,0 +1,483 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://local.dev/schemas/app-blueprint/v1.json",
|
||||
"title": "AppBlueprint",
|
||||
"description": "AI 生成 → 用户确认 → 中台执行的唯一契约(v1)",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"version",
|
||||
"meta",
|
||||
"storage",
|
||||
"entities",
|
||||
"apis",
|
||||
"pages",
|
||||
"security"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"const": "1.0"
|
||||
},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "slug", "locale"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64,
|
||||
"description": "应用显示名"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{1,47}$",
|
||||
"description": "应用标识,用于路径与资源前缀"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"default": "zh-CN"
|
||||
},
|
||||
"source": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"prompt": { "type": "string" },
|
||||
"image_refs": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"maxItems": 10
|
||||
},
|
||||
"excel_ref": { "type": "string" },
|
||||
"layout_refs": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"screenshot_faithful": { "type": "boolean" },
|
||||
"vision_summary": { "type": "string" },
|
||||
"layout_summary": { "type": "string" },
|
||||
"llm_provider": { "type": "string" },
|
||||
"llm_model": { "type": "string" },
|
||||
"data_format": { "type": "string" },
|
||||
"row_count": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"ui_preset": {
|
||||
"type": "string",
|
||||
"enum": ["default", "screenshot_faithful", "ops_monitor"]
|
||||
},
|
||||
"platform_title": { "type": "string", "maxLength": 64 },
|
||||
"project_context": { "type": "string", "maxLength": 120 },
|
||||
"ui": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "AI 整体置信度,低于阈值应强制人工确认"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode", "engine"],
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["schema_per_app", "database_per_app"],
|
||||
"description": "schema_per_app=共享实例独立 schema;database_per_app=独立库(更高隔离)"
|
||||
},
|
||||
"engine": {
|
||||
"type": "string",
|
||||
"enum": ["postgres", "mysql"]
|
||||
},
|
||||
"schema_name": {
|
||||
"type": "string",
|
||||
"pattern": "^app_[a-z0-9_]{1,48}$",
|
||||
"description": "由平台生成或校验;禁止用户/AI 指定任意库名"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 20,
|
||||
"items": { "$ref": "#/$defs/entity" }
|
||||
},
|
||||
"apis": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["base_path", "resources"],
|
||||
"properties": {
|
||||
"base_path": {
|
||||
"type": "string",
|
||||
"pattern": "^/api/v1/apps/[a-z][a-z0-9_]{1,47}$"
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/apiResource" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/page" }
|
||||
},
|
||||
"security": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["visibility", "roles", "row_policies"],
|
||||
"properties": {
|
||||
"visibility": {
|
||||
"type": "string",
|
||||
"enum": ["private", "org", "public_readonly"]
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "permissions"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["owner", "editor", "viewer"]
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"app.read",
|
||||
"app.write",
|
||||
"app.admin",
|
||||
"row.create",
|
||||
"row.read",
|
||||
"row.update",
|
||||
"row.delete",
|
||||
"row.export",
|
||||
"row.import"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"row_policies": {
|
||||
"type": "array",
|
||||
"description": "行级规则,中台强制注入,不可被前端绕过",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["entity", "rule"],
|
||||
"properties": {
|
||||
"entity": { "type": "string" },
|
||||
"rule": {
|
||||
"type": "string",
|
||||
"enum": ["tenant_isolated", "owner_only", "org_shared"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"seed": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"import_excel": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100000,
|
||||
"default": 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"entity": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "table", "label", "fields", "primary_key"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{1,47}$",
|
||||
"description": "逻辑实体名"
|
||||
},
|
||||
"table": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{1,47}$",
|
||||
"description": "物理表名(不含 schema 前缀)"
|
||||
},
|
||||
"label": { "type": "string", "maxLength": 64 },
|
||||
"primary_key": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{1,47}$"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 80,
|
||||
"items": { "$ref": "#/$defs/field" }
|
||||
},
|
||||
"indexes": {
|
||||
"type": "array",
|
||||
"maxItems": 20,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "columns"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^idx_[a-z0-9_]{1,48}$"
|
||||
},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 5,
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"unique": { "type": "boolean", "default": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"field": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "type", "label"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{1,47}$"
|
||||
},
|
||||
"label": { "type": "string", "maxLength": 64 },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"text",
|
||||
"int",
|
||||
"bigint",
|
||||
"decimal",
|
||||
"boolean",
|
||||
"date",
|
||||
"datetime",
|
||||
"enum",
|
||||
"json",
|
||||
"file_ref"
|
||||
]
|
||||
},
|
||||
"nullable": { "type": "boolean", "default": true },
|
||||
"unique": { "type": "boolean", "default": false },
|
||||
"default": {},
|
||||
"max_length": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 4000
|
||||
},
|
||||
"precision": { "type": "integer", "minimum": 1, "maximum": 38 },
|
||||
"scale": { "type": "integer", "minimum": 0, "maximum": 10 },
|
||||
"enum_values": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"maxItems": 50
|
||||
},
|
||||
"ui": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"widget": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"input",
|
||||
"textarea",
|
||||
"number",
|
||||
"select",
|
||||
"switch",
|
||||
"datepicker",
|
||||
"upload",
|
||||
"hidden"
|
||||
]
|
||||
},
|
||||
"listable": { "type": "boolean", "default": true },
|
||||
"filterable": { "type": "boolean", "default": false },
|
||||
"sortable": { "type": "boolean", "default": false },
|
||||
"width": {
|
||||
"type": "string",
|
||||
"enum": ["sm", "md", "lg"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"from_excel": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"column": { "type": "string" },
|
||||
"sample_values": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"maxItems": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiResource": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["entity", "path", "operations"],
|
||||
"properties": {
|
||||
"entity": { "type": "string" },
|
||||
"path": {
|
||||
"type": "string",
|
||||
"pattern": "^/[a-z][a-z0-9_]{1,47}$"
|
||||
},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["list", "get", "create", "update", "delete", "import", "export"]
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default_page_size": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 20
|
||||
},
|
||||
"max_page_size": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 200,
|
||||
"default": 100
|
||||
},
|
||||
"allowed_filters": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"allowed_sorts": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["id", "title", "route", "type", "entity"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]{1,47}$"
|
||||
},
|
||||
"title": { "type": "string", "maxLength": 64 },
|
||||
"route": {
|
||||
"type": "string",
|
||||
"pattern": "^/[a-z0-9\\-_/:]{1,64}$",
|
||||
"description": "支持 :id 这类路径参数"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["list", "detail", "form_create", "form_edit", "dashboard"]
|
||||
},
|
||||
"entity": { "type": "string" },
|
||||
"layout": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"preset": {
|
||||
"type": "string",
|
||||
"enum": ["default", "screenshot_faithful", "ops_monitor"]
|
||||
},
|
||||
"action_labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"filter_style": { "type": "string" },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"description": "list 页展示列(字段名)",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"filters": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["create", "edit", "delete", "export", "import", "refresh"]
|
||||
}
|
||||
},
|
||||
"form_fields": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"widgets": {
|
||||
"type": "array",
|
||||
"description": "dashboard 组件",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"required": ["type", "title"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["kpi", "bar_chart", "line_chart", "pie_chart", "table", "status_strip"]
|
||||
},
|
||||
"title": { "type": "string" },
|
||||
"metric": { "type": "string" },
|
||||
"metrics": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"group_by": { "type": "string" },
|
||||
"x_field": { "type": "string" },
|
||||
"y_unit": { "type": "string" },
|
||||
"entity": { "type": "string" },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"label_field": { "type": "string" },
|
||||
"value_field": { "type": "string" },
|
||||
"warn_field": { "type": "string" },
|
||||
"filter_field": { "type": "string" },
|
||||
"filter_op": { "type": "string" },
|
||||
"filter_value": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
blueprint/tools/validate_blueprint.py
Normal file
56
blueprint/tools/validate_blueprint.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate AppBlueprint JSON against the local JSON Schema (Draft 2020-12)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import jsonschema
|
||||
from jsonschema import Draft202012Validator
|
||||
except ImportError:
|
||||
print("请先安装: pip install jsonschema", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description="Validate AppBlueprint")
|
||||
parser.add_argument(
|
||||
"blueprint",
|
||||
nargs="?",
|
||||
default=str(root / "examples" / "inventory-ledger.blueprint.json"),
|
||||
help="blueprint json path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--schema",
|
||||
default=str(root / "schema" / "app-blueprint.schema.json"),
|
||||
help="json schema path",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
schema = json.loads(Path(args.schema).read_text(encoding="utf-8"))
|
||||
data = json.loads(Path(args.blueprint).read_text(encoding="utf-8"))
|
||||
|
||||
validator = Draft202012Validator(schema)
|
||||
errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path))
|
||||
if errors:
|
||||
print(f"FAIL: {len(errors)} error(s)")
|
||||
for err in errors:
|
||||
path = ".".join(str(p) for p in err.path) or "$"
|
||||
print(f" - {path}: {err.message}")
|
||||
return 1
|
||||
|
||||
print("OK: blueprint is valid")
|
||||
print(f" app: {data['meta']['name']} ({data['meta']['slug']})")
|
||||
print(f" entities: {len(data['entities'])}")
|
||||
print(f" pages: {len(data['pages'])}")
|
||||
print(f" apis: {len(data['apis']['resources'])}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user