chore: initial commit of ai site platform

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 10:19:22 +08:00
commit 6366859bb3
222 changed files with 47313 additions and 0 deletions

79
blueprint/README.md Normal file
View 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 内字段;多余字段校验失败

View 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 渲染
```

View 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
}
}

View 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
```

View 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 }

View 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)`

View 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=共享实例独立 schemadatabase_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": {}
}
}
}
}
}
}
}
}
}

View 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())