1221 lines
52 KiB
JavaScript
1221 lines
52 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Architecture Agent — SF-02
|
||
*
|
||
* 根据 SF-01 生成的 PRD 自动生成技术架构。
|
||
*
|
||
* 核心功能:
|
||
* - 技术选型:前端/后端/数据库/部署
|
||
* - 系统架构图:ASCII art 三层架构
|
||
* - 数据流:请求链路文档
|
||
* - 模块划分:按功能域拆解
|
||
* - 数据库设计:表结构 + 字段 + 关系
|
||
* - API 设计:按资源分组
|
||
* - 目录结构:完整 /src 树
|
||
*
|
||
* Usage:
|
||
* node scripts/architecture-agent.mjs --input <prd.json> [options]
|
||
* node scripts/architecture-agent.mjs --input-text "做一个宠物管理 App" [options]
|
||
*
|
||
* Options:
|
||
* --input <path> PRD JSON 文件路径
|
||
* --input-text <text> 直接传入需求(先调 SF-01 生成 PRD)
|
||
* --output <path> 输出架构 JSON 文件路径
|
||
* --pretty 格式化 JSON 输出
|
||
* --verbose 详细输出
|
||
* --help 显示帮助
|
||
*
|
||
* @module architecture-agent
|
||
*/
|
||
|
||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
||
import { resolve, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { createContract } from "./model-contract.mjs";
|
||
|
||
const __dirname = dirname(resolve(fileURLToPath(import.meta.url)));
|
||
const WORKSPACE = resolve(__dirname, "..");
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 1. Tech Stack Templates
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Tech stack options keyed by platform combination.
|
||
*/
|
||
const TECH_STACKS = {
|
||
"wechat-miniapp": {
|
||
frontend: "微信小程序原生 / Taro 3.x + React",
|
||
backend: "Node.js + Express / 云函数(微信云开发)",
|
||
database: "MySQL 8.0(用户数据)+ 微信云数据库(小程序数据)",
|
||
deployment: "微信云托管 / 腾讯云 Lighthouse",
|
||
},
|
||
miniapp: {
|
||
frontend: "uni-app 3.x(多端小程序)",
|
||
backend: "Node.js + Koa2 / NestJS",
|
||
database: "PostgreSQL 15 + Redis 7",
|
||
deployment: "阿里云 ECS / Docker + Nginx",
|
||
},
|
||
"ios-only": {
|
||
frontend: "SwiftUI + Combine(iOS 17+)",
|
||
backend: "Vapor 4(Swift 服务端)/ Node.js + Express",
|
||
database: "PostgreSQL 15 + Redis 7",
|
||
deployment: "AWS EC2 / Railway + App Store Connect",
|
||
},
|
||
"android-only": {
|
||
frontend: "Jetpack Compose + Kotlin Coroutines",
|
||
backend: "Spring Boot 3 / Ktor",
|
||
database: "PostgreSQL 15 + Redis 7",
|
||
deployment: "GCP Cloud Run / AWS ECS",
|
||
},
|
||
"web-only": {
|
||
frontend: "React 19 + TypeScript + Vite",
|
||
backend: "NestJS / Fastify",
|
||
database: "PostgreSQL 15 + Redis 7",
|
||
deployment: "Docker + Nginx / Vercel + Railway",
|
||
},
|
||
"mobile-web": {
|
||
frontend: "React Native 0.76 + Expo / Flutter 3.x",
|
||
backend: "NestJS + Prisma",
|
||
database: "PostgreSQL 15 + Redis 7 + MinIO(文件存储)",
|
||
deployment: "Docker Compose + Nginx / K8s(规模化)",
|
||
},
|
||
mobile: {
|
||
frontend: "React Native 0.76 + Expo",
|
||
backend: "NestJS + Prisma",
|
||
database: "PostgreSQL 15 + Redis 7",
|
||
deployment: "Docker Compose + Nginx",
|
||
},
|
||
default: {
|
||
frontend: "React 19 + TypeScript + Vite",
|
||
backend: "NestJS + Prisma",
|
||
database: "PostgreSQL 15 + Redis 7",
|
||
deployment: "Docker + Nginx",
|
||
},
|
||
};
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 2. Architecture Diagram Generator (ASCII)
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Generate an ASCII architecture diagram.
|
||
*
|
||
* @param {string} projectName
|
||
* @param {object} stack — Tech stack
|
||
* @param {string[]} modules — Module names
|
||
* @returns {string}
|
||
*/
|
||
export function generateArchDiagram(projectName, stack, modules) {
|
||
const modList = modules.slice(0, 6).map((m, i) => `│ ${String(i + 1).padEnd(3)} ${m.padEnd(24)} │`).join("\n");
|
||
|
||
return [
|
||
`┌─────────────────────────────────────────────────────────┐`,
|
||
`│ ${projectName} — System Architecture │`,
|
||
`├─────────────────────────────────────────────────────────┤`,
|
||
`│ │`,
|
||
`│ ┌──────────────────────┐ ┌──────────────────────┐ │`,
|
||
`│ │ Client Layer │ │ Admin / Web │ │`,
|
||
`│ │ ${(stack.frontend || "Mobile App").padEnd(20)} │ │ React + Vite (Web) │ │`,
|
||
`│ └──────────┬───────────┘ └──────────┬───────────┘ │`,
|
||
`│ │ │ │`,
|
||
`│ └──────────┬────────────────┘ │`,
|
||
`│ │ │`,
|
||
`│ ┌─────────▼──────────┐ │`,
|
||
`│ │ API Gateway │ │`,
|
||
`│ │ Nginx / Kong │ │`,
|
||
`│ └─────────┬──────────┘ │`,
|
||
`│ │ │`,
|
||
`│ ┌─────────▼──────────┐ │`,
|
||
`│ │ Backend Services │ │`,
|
||
`│ │ ${(stack.backend || "NestJS").padEnd(20)} │ │`,
|
||
`│ └─────────┬──────────┘ │`,
|
||
`│ │ │`,
|
||
`│ ┌───────────────┼───────────────┐ │`,
|
||
`│ │ │ │ │`,
|
||
`│ ┌─────▼─────┐ ┌──────▼──────┐ ┌────▼─────┐ │`,
|
||
`│ │ ${(stack.database || "PostgreSQL").split("+")[0].trim().padEnd(12)}│ │ Redis Cache │ │ MinIO │ │`,
|
||
`│ │ Primary │ │ Session │ │ Files │ │`,
|
||
`│ └───────────┘ └─────────────┘ └──────────┘ │`,
|
||
`│ │`,
|
||
`├─────────────────────────────────────────────────────────┤`,
|
||
`│ Modules: │`,
|
||
`${modList}`,
|
||
`│ │`,
|
||
`└─────────────────────────────────────────────────────────┘`,
|
||
].join("\n");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 3. Data Flow Generator
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Generate data flow documentation from pages and API requirements.
|
||
*
|
||
* @param {object[]} pages
|
||
* @param {object[]} apis
|
||
* @returns {object[]}
|
||
*/
|
||
export function generateDataFlows(pages, apis) {
|
||
const flows = [];
|
||
|
||
for (const page of pages.slice(0, 5)) {
|
||
const pageName = page.name;
|
||
const route = page.route;
|
||
|
||
// Find APIs related to this page (simple heuristic: route name in API path)
|
||
const relatedApis = apis.filter(a =>
|
||
route.replace(/[:/]/g, "").split("/").some(seg =>
|
||
a.path.includes(seg.replace(":id", ""))
|
||
)
|
||
).slice(0, 2);
|
||
|
||
if (relatedApis.length > 0) {
|
||
flows.push({
|
||
page: pageName,
|
||
route,
|
||
description: page.description,
|
||
dataFlow: relatedApis.map(a => `${a.method} ${a.path} → ${a.description}`),
|
||
direction: "Client → API Gateway → Backend → Database → Response → Client",
|
||
});
|
||
}
|
||
}
|
||
|
||
// If no specific flows found, generate generic ones from APIs
|
||
if (flows.length === 0 && apis.length > 0) {
|
||
flows.push({
|
||
page: "通用页面",
|
||
route: "/*",
|
||
description: "通用数据流",
|
||
dataFlow: apis.slice(0, 4).map(a => `${a.method} ${a.path} → ${a.description}`),
|
||
direction: "Client → API Gateway → Backend → Database → Response → Client",
|
||
});
|
||
}
|
||
|
||
return flows;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 4. Module Decomposition Engine
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Decompose features into backend modules.
|
||
*
|
||
* @param {object[]} features
|
||
* @param {string} domain
|
||
* @returns {object[]}
|
||
*/
|
||
export function decomposeModules(features, domain) {
|
||
// Group features into logical modules by keyword
|
||
const moduleMap = new Map();
|
||
const modulePatterns = {
|
||
auth: ["用户", "登录", "注册", "权限", "角色", "auth", "profile"],
|
||
settings: ["设置", "偏好", "配置"],
|
||
notification: ["通知", "提醒", "推送", "消息"],
|
||
storage: ["上传", "文件", "图片", "相册", "照片"],
|
||
analytics: ["统计", "报告", "分析", "趋势"],
|
||
content: ["社区", "分享", "动态", "评论", "评价"],
|
||
payment: ["支付", "订单", "购物车", "优惠券"],
|
||
schedule: ["日程", "日历", "提醒", "预约"],
|
||
workflow: ["审批", "流程", "考勤", "请假", "报销"],
|
||
};
|
||
|
||
// Default grouping: 1 feature = 1 module
|
||
for (const feature of features) {
|
||
let assigned = false;
|
||
for (const [modKey, keywords] of Object.entries(modulePatterns)) {
|
||
if (keywords.some(kw => feature.name.includes(kw))) {
|
||
if (!moduleMap.has(modKey)) {
|
||
moduleMap.set(modKey, { name: modKey, label: modKey.charAt(0).toUpperCase() + modKey.slice(1), features: [] });
|
||
}
|
||
moduleMap.get(modKey).features.push(feature.name);
|
||
assigned = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!assigned) {
|
||
const key = feature.name;
|
||
if (!moduleMap.has(key)) {
|
||
moduleMap.set(key, { name: key, label: feature.name, features: [feature.name] });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Domain-specific module additions
|
||
const domainModules = {
|
||
pet: [{ name: "pet", label: "宠物管理", features: ["宠物档案", "健康日程"] }],
|
||
ecommerce: [
|
||
{ name: "product", label: "商品管理", features: ["商品浏览"] },
|
||
{ name: "order", label: "订单管理", features: ["订单管理"] },
|
||
],
|
||
education: [
|
||
{ name: "course", label: "课程管理", features: ["课程中心", "视频播放"] },
|
||
{ name: "exercise", label: "题库管理", features: ["题库练习"] },
|
||
],
|
||
enterprise: [
|
||
{ name: "approval", label: "审批引擎", features: ["审批流程"] },
|
||
{ name: "attendance", label: "考勤管理", features: ["考勤打卡"] },
|
||
{ name: "department", label: "组织架构", features: ["部门管理"] },
|
||
],
|
||
fitness: [
|
||
{ name: "checkin", label: "打卡引擎", features: ["打卡记录"] },
|
||
{ name: "plan", label: "训练计划", features: ["训练计划"] },
|
||
],
|
||
note: [
|
||
{ name: "editor", label: "编辑器", features: ["笔记编辑"] },
|
||
{ name: "search", label: "搜索引擎", features: ["全文搜索"] },
|
||
],
|
||
};
|
||
|
||
const extraModules = domainModules[domain] || [];
|
||
for (const em of extraModules) {
|
||
if (!moduleMap.has(em.name)) {
|
||
moduleMap.set(em.name, em);
|
||
}
|
||
}
|
||
|
||
return Array.from(moduleMap.values());
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 5. Database Design Engine
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Generate database schema from PRD.
|
||
*
|
||
* @param {object[]} features
|
||
* @param {object[]} apis
|
||
* @param {string} domain
|
||
* @returns {object[]} — Array of table definitions
|
||
*/
|
||
|
||
/**
|
||
* Chinese-to-English slug mapping for common entity names.
|
||
*/
|
||
const ZH_SLUG_MAP = {
|
||
"书籍库": "books", "书籍": "books", "书评": "reviews", "阅读状态": "reading_status", "阅读进度": "reading_progress",
|
||
"笔记": "notes", "标注": "annotations", "标签": "tags", "统计": "stats", "仪表盘": "dashboard",
|
||
"看板": "boards", "任务": "tasks", "成员": "members", "活动日志": "activity_logs", "日志": "logs",
|
||
"食物库": "foods", "食物": "foods", "客户": "clients", "餐计划": "meal_plans", "营养": "nutrition",
|
||
"购物清单": "shopping_lists", "模板": "templates", "商品": "products", "订单": "orders",
|
||
"库存": "inventory", "供应商": "suppliers", "预警": "alerts", "操作日志": "operation_logs",
|
||
"工单": "tickets", "审批": "approvals", "考勤": "attendance", "员工": "employees",
|
||
"课程": "courses", "章节": "chapters", "学员": "students", "作业": "assignments",
|
||
"预约": "appointments", "服务": "services", "宠物": "pets", "日程": "schedules",
|
||
"记录": "records", "相册": "albums", "医院": "hospitals", "文章": "articles",
|
||
"评论": "comments", "媒体": "media", "项目": "projects", "资产": "assets",
|
||
"公告": "notices", "通知": "notifications", "报销": "expenses", "请假": "leaves",
|
||
"打卡": "checkins", "部门": "departments", "薪资": "salaries", "绩效": "performance",
|
||
"客户管理": "customers", "销售漏斗": "sales_pipeline", "跟进记录": "follow_ups",
|
||
// v1.1: Contract / Inspection / Callback / Warehouse / Schedule
|
||
"合同": "contracts", "合同创建": "contracts", "合同归档": "contracts", "到期提醒": "reminders",
|
||
"巡检": "inspections", "巡检计划": "inspection_plans", "巡检记录": "inspection_records",
|
||
"故障": "faults", "故障上报": "faults", "设备": "equipment", "设备台账": "equipment",
|
||
"回访": "callbacks", "回访计划": "callback_plans", "回访记录": "callback_records",
|
||
"满意度": "satisfaction", "满意度评价": "satisfaction",
|
||
"入库": "stock_in", "入库登记": "stock_in", "出库": "stock_out", "出库审批": "stock_out",
|
||
"盘点": "stocktaking", "库存盘点": "stocktaking", "库存预警": "stock_alerts",
|
||
"教室": "classrooms", "教室管理": "classrooms", "排课": "schedules", "排课冲突检测": "schedules",
|
||
"课表": "schedules", "课表查看": "schedules",
|
||
"合同管理": "contracts", "巡检系统": "inspections", "回访系统": "callbacks",
|
||
"出入库": "warehouse", "排课系统": "schedules",
|
||
};
|
||
|
||
function toSafeSlug(name) {
|
||
// Strip trailing Chinese/English punctuation
|
||
const cleaned = name.replace(/[。,、;:!?\.\,\;\!\?]+$/g, "").trim();
|
||
if (/^[a-z][a-z0-9_]*$/.test(cleaned)) return cleaned;
|
||
if (ZH_SLUG_MAP[cleaned]) return ZH_SLUG_MAP[cleaned];
|
||
// Longest-match-first
|
||
for (const zh of Object.keys(ZH_SLUG_MAP).sort((a,b) => b.length - a.length)) {
|
||
if (cleaned.includes(zh)) return ZH_SLUG_MAP[zh];
|
||
}
|
||
const slug = cleaned.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase().replace(/_+/g, "_").replace(/^_|_$/g, "");
|
||
return slug || "items";
|
||
}
|
||
|
||
/**
|
||
* Domain-specific field templates keyed by keyword match.
|
||
* Each entry: { match: string[], fields: [...extra fields beyond id/user_id/timestamps] }
|
||
* These are inserted BETWEEN user_id and created_at.
|
||
*/
|
||
const FIELD_TEMPLATES = [
|
||
// ── Books / Reading ──
|
||
{ match: ["book", "书籍"], fields: [
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "author", type: "VARCHAR(128)" },
|
||
{ name: "isbn", type: "VARCHAR(20)" },
|
||
{ name: "cover_url", type: "TEXT" },
|
||
{ name: "publisher", type: "VARCHAR(128)" },
|
||
{ name: "publish_date", type: "DATE" },
|
||
{ name: "genre", type: "VARCHAR(64)" },
|
||
]},
|
||
{ match: ["reading_status", "阅读状态"], fields: [
|
||
{ name: "book_id", type: "UUID", constraints: "FK → books.id" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'unread', CHECK (status IN ('unread','reading','finished','abandoned'))" },
|
||
{ name: "started_at", type: "TIMESTAMPTZ" },
|
||
{ name: "finished_at", type: "TIMESTAMPTZ" },
|
||
]},
|
||
{ match: ["reading_progress", "阅读进度"], fields: [
|
||
{ name: "book_id", type: "UUID", constraints: "FK → books.id" },
|
||
{ name: "current_page", type: "INTEGER", constraints: "DEFAULT 0" },
|
||
{ name: "total_pages", type: "INTEGER" },
|
||
{ name: "percentage", type: "DECIMAL(5,2)", constraints: "DEFAULT 0" },
|
||
]},
|
||
{ match: ["review", "书评"], fields: [
|
||
{ name: "book_id", type: "UUID", constraints: "FK → books.id" },
|
||
{ name: "rating", type: "INTEGER", constraints: "CHECK (rating >= 1 AND rating <= 5)" },
|
||
{ name: "content", type: "TEXT" },
|
||
]},
|
||
// ── Tasks / Boards ──
|
||
{ match: ["task"], fields: [
|
||
{ name: "board_id", type: "UUID", constraints: "FK → boards.id" },
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'todo'" },
|
||
{ name: "priority", type: "VARCHAR(16)", constraints: "DEFAULT 'medium'" },
|
||
{ name: "due_date", type: "DATE" },
|
||
{ name: "assignee_id", type: "UUID", constraints: "FK → users.id" },
|
||
]},
|
||
{ match: ["board", "看板"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "color", type: "VARCHAR(7)" },
|
||
]},
|
||
{ match: ["member", "成员"], fields: [
|
||
{ name: "board_id", type: "UUID", constraints: "FK → boards.id" },
|
||
{ name: "user_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "role", type: "VARCHAR(32)", constraints: "DEFAULT 'member'" },
|
||
]},
|
||
{ match: ["activity_log", "活动日志"], fields: [
|
||
{ name: "board_id", type: "UUID", constraints: "FK → boards.id" },
|
||
{ name: "action", type: "VARCHAR(64)", constraints: "NOT NULL" },
|
||
{ name: "entity_type", type: "VARCHAR(32)" },
|
||
{ name: "entity_id", type: "UUID" },
|
||
{ name: "detail", type: "JSONB" },
|
||
]},
|
||
// ── Food / Meal ──
|
||
{ match: ["food", "食物"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "category", type: "VARCHAR(64)" },
|
||
{ name: "calories_per_100g", type: "INTEGER" },
|
||
{ name: "protein_g", type: "DECIMAL(5,2)" },
|
||
{ name: "fat_g", type: "DECIMAL(5,2)" },
|
||
{ name: "carb_g", type: "DECIMAL(5,2)" },
|
||
]},
|
||
{ match: ["meal_plan", "餐计划"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "start_date", type: "DATE" },
|
||
{ name: "end_date", type: "DATE" },
|
||
{ name: "target_calories", type: "INTEGER" },
|
||
]},
|
||
{ match: ["nutrition", "营养"], fields: [
|
||
{ name: "food_id", type: "UUID", constraints: "FK → foods.id" },
|
||
{ name: "meal_plan_id", type: "UUID", constraints: "FK → meal_plans.id" },
|
||
{ name: "serving_g", type: "DECIMAL(6,2)" },
|
||
{ name: "meal_type", type: "VARCHAR(32)" },
|
||
]},
|
||
{ match: ["shopping_list", "购物清单"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "items", type: "JSONB", constraints: "DEFAULT '[]'" },
|
||
{ name: "completed", type: "BOOLEAN", constraints: "DEFAULT false" },
|
||
]},
|
||
// ── CRM ──
|
||
{ match: ["customer", "客户管理"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "email", type: "VARCHAR(256)" },
|
||
{ name: "phone", type: "VARCHAR(20)" },
|
||
{ name: "company", type: "VARCHAR(128)" },
|
||
{ name: "source", type: "VARCHAR(64)" },
|
||
{ name: "tags", type: "JSONB", constraints: "DEFAULT '[]'" },
|
||
]},
|
||
{ match: ["sales_pipeline", "销售漏斗"], fields: [
|
||
{ name: "customer_id", type: "UUID", constraints: "FK → customers.id" },
|
||
{ name: "stage", type: "VARCHAR(32)", constraints: "NOT NULL" },
|
||
{ name: "amount", type: "DECIMAL(12,2)" },
|
||
{ name: "probability", type: "INTEGER" },
|
||
{ name: "expected_close", type: "DATE" },
|
||
]},
|
||
{ match: ["follow_up", "跟进记录"], fields: [
|
||
{ name: "customer_id", type: "UUID", constraints: "FK → customers.id" },
|
||
{ name: "type", type: "VARCHAR(32)" },
|
||
{ name: "content", type: "TEXT" },
|
||
{ name: "next_action", type: "VARCHAR(256)" },
|
||
{ name: "next_action_date", type: "DATE" },
|
||
]},
|
||
// ── Inventory ──
|
||
{ match: ["inventory", "库存"], exclude: ["stocktaking", "stock_alert", "库存盘点", "库存预警"], fields: [
|
||
{ name: "product_id", type: "UUID", constraints: "FK → products.id" },
|
||
{ name: "quantity", type: "INTEGER", constraints: "DEFAULT 0" },
|
||
{ name: "warehouse", type: "VARCHAR(64)" },
|
||
{ name: "min_stock", type: "INTEGER", constraints: "DEFAULT 0" },
|
||
{ name: "max_stock", type: "INTEGER" },
|
||
]},
|
||
{ match: ["supplier", "供应商"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "contact", type: "VARCHAR(64)" },
|
||
{ name: "phone", type: "VARCHAR(20)" },
|
||
{ name: "email", type: "VARCHAR(256)" },
|
||
{ name: "address", type: "TEXT" },
|
||
]},
|
||
// ── Ticket / OA ──
|
||
{ match: ["ticket", "工单"], fields: [
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "priority", type: "VARCHAR(16)", constraints: "DEFAULT 'medium'" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'open'" },
|
||
{ name: "assignee_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "category", type: "VARCHAR(64)" },
|
||
]},
|
||
{ match: ["employee", "员工"], fields: [
|
||
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL" },
|
||
{ name: "email", type: "VARCHAR(256)" },
|
||
{ name: "phone", type: "VARCHAR(20)" },
|
||
{ name: "department", type: "VARCHAR(64)" },
|
||
{ name: "position", type: "VARCHAR(64)" },
|
||
{ name: "hire_date", type: "DATE" },
|
||
{ name: "salary", type: "DECIMAL(12,2)" },
|
||
]},
|
||
// ── Education ──
|
||
{ match: ["course", "课程"], fields: [
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "instructor", type: "VARCHAR(64)" },
|
||
{ name: "price", type: "DECIMAL(10,2)" },
|
||
{ name: "cover_url", type: "TEXT" },
|
||
{ name: "category", type: "VARCHAR(64)" },
|
||
]},
|
||
{ match: ["chapter", "章节"], fields: [
|
||
{ name: "course_id", type: "UUID", constraints: "FK → courses.id" },
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "sort_order", type: "INTEGER", constraints: "DEFAULT 0" },
|
||
{ name: "duration_min", type: "INTEGER" },
|
||
]},
|
||
{ match: ["student", "学员"], fields: [
|
||
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL" },
|
||
{ name: "email", type: "VARCHAR(256)" },
|
||
{ name: "enrolled_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
{ name: "progress", type: "DECIMAL(5,2)", constraints: "DEFAULT 0" },
|
||
]},
|
||
{ match: ["assignment", "作业"], fields: [
|
||
{ name: "course_id", type: "UUID", constraints: "FK → courses.id" },
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "due_date", type: "DATE" },
|
||
{ name: "max_score", type: "INTEGER", constraints: "DEFAULT 100" },
|
||
]},
|
||
// ── Appointment / Service ──
|
||
{ match: ["appointment", "预约"], fields: [
|
||
{ name: "client_id", type: "UUID", constraints: "FK → clients.id" },
|
||
{ name: "service_id", type: "UUID", constraints: "FK → services.id" },
|
||
{ name: "start_time", type: "TIMESTAMPTZ", constraints: "NOT NULL" },
|
||
{ name: "end_time", type: "TIMESTAMPTZ" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'pending'" },
|
||
{ name: "notes", type: "TEXT" },
|
||
]},
|
||
{ match: ["service", "服务"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "duration_min", type: "INTEGER" },
|
||
{ name: "price", type: "DECIMAL(10,2)" },
|
||
]},
|
||
// ── Asset ──
|
||
{ match: ["asset", "资产"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "category", type: "VARCHAR(64)" },
|
||
{ name: "purchase_date", type: "DATE" },
|
||
{ name: "purchase_price", type: "DECIMAL(12,2)" },
|
||
{ name: "current_value", type: "DECIMAL(12,2)" },
|
||
{ name: "location", type: "VARCHAR(128)" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'available'" },
|
||
]},
|
||
// ── Pet ──
|
||
{ match: ["pet", "宠物"], fields: [
|
||
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL" },
|
||
{ name: "species", type: "VARCHAR(32)" },
|
||
{ name: "breed", type: "VARCHAR(64)" },
|
||
{ name: "birth_date", type: "DATE" },
|
||
{ name: "weight_kg", type: "DECIMAL(5,2)" },
|
||
{ name: "avatar_url", type: "TEXT" },
|
||
]},
|
||
{ match: ["album", "相册"], fields: [
|
||
{ name: "title", type: "VARCHAR(128)" },
|
||
{ name: "image_url", type: "TEXT", constraints: "NOT NULL" },
|
||
{ name: "caption", type: "TEXT" },
|
||
{ name: "taken_at", type: "TIMESTAMPTZ" },
|
||
]},
|
||
{ match: ["hospital", "医院"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "address", type: "TEXT" },
|
||
{ name: "phone", type: "VARCHAR(20)" },
|
||
{ name: "rating", type: "DECIMAL(2,1)" },
|
||
{ name: "lat", type: "DECIMAL(9,6)" },
|
||
{ name: "lng", type: "DECIMAL(9,6)" },
|
||
]},
|
||
// ── Blog / CMS ──
|
||
{ match: ["article", "文章"], fields: [
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "content", type: "TEXT" },
|
||
{ name: "excerpt", type: "TEXT" },
|
||
{ name: "cover_url", type: "TEXT" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'draft'" },
|
||
{ name: "author_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "category", type: "VARCHAR(64)" },
|
||
{ name: "published_at", type: "TIMESTAMPTZ" },
|
||
]},
|
||
{ match: ["comment", "评论"], fields: [
|
||
{ name: "entity_type", type: "VARCHAR(32)", constraints: "NOT NULL" },
|
||
{ name: "entity_id", type: "UUID", constraints: "NOT NULL" },
|
||
{ name: "content", type: "TEXT", constraints: "NOT NULL" },
|
||
{ name: "parent_id", type: "UUID", constraints: "FK → comments.id (自引用)" },
|
||
]},
|
||
{ match: ["media", "媒体"], fields: [
|
||
{ name: "filename", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "url", type: "TEXT", constraints: "NOT NULL" },
|
||
{ name: "mime_type", type: "VARCHAR(64)" },
|
||
{ name: "size_bytes", type: "INTEGER" },
|
||
]},
|
||
// ── Tags (通用) ──
|
||
{ match: ["tag", "标签"], fields: [
|
||
{ name: "name", type: "VARCHAR(64)", constraints: "NOT NULL, UNIQUE" },
|
||
{ name: "color", type: "VARCHAR(7)" },
|
||
]},
|
||
// ── Stats / Dashboard ──
|
||
{ match: ["stat", "统计", "dashboard", "仪表盘"], fields: [
|
||
{ name: "metric", type: "VARCHAR(64)", constraints: "NOT NULL" },
|
||
{ name: "value", type: "DECIMAL(12,2)" },
|
||
{ name: "period", type: "VARCHAR(32)" },
|
||
{ name: "recorded_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
]},
|
||
// ── Contract ──
|
||
{ match: ["contract", "合同"], fields: [
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "party_a", type: "VARCHAR(128)" },
|
||
{ name: "party_b", type: "VARCHAR(128)" },
|
||
{ name: "amount", type: "DECIMAL(14,2)" },
|
||
{ name: "signed_at", type: "DATE" },
|
||
{ name: "expires_at", type: "DATE" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'draft'" },
|
||
{ name: "file_url", type: "TEXT" },
|
||
]},
|
||
// ── Approval / Reminder ──
|
||
{ match: ["approval", "审批"], exclude: ["stock_out", "出库审批"], fields: [
|
||
{ name: "entity_type", type: "VARCHAR(32)", constraints: "NOT NULL" },
|
||
{ name: "entity_id", type: "UUID" },
|
||
{ name: "applicant_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'pending'" },
|
||
{ name: "form_data", type: "JSONB" },
|
||
]},
|
||
{ match: ["reminder", "提醒", "到期提醒"], fields: [
|
||
{ name: "entity_type", type: "VARCHAR(32)", constraints: "NOT NULL" },
|
||
{ name: "entity_id", type: "UUID" },
|
||
{ name: "remind_at", type: "TIMESTAMPTZ", constraints: "NOT NULL" },
|
||
{ name: "message", type: "TEXT" },
|
||
{ name: "sent", type: "BOOLEAN", constraints: "DEFAULT false" },
|
||
]},
|
||
// ── Inspection ──
|
||
{ match: ["inspection_plan", "巡检计划"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "frequency", type: "VARCHAR(32)" },
|
||
{ name: "equipment_id", type: "UUID", constraints: "FK → equipment.id" },
|
||
{ name: "next_run", type: "TIMESTAMPTZ" },
|
||
{ name: "active", type: "BOOLEAN", constraints: "DEFAULT true" },
|
||
]},
|
||
{ match: ["inspection_record", "巡检记录"], fields: [
|
||
{ name: "plan_id", type: "UUID", constraints: "FK → inspection_plans.id" },
|
||
{ name: "equipment_id", type: "UUID", constraints: "FK → equipment.id" },
|
||
{ name: "inspector_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "result", type: "VARCHAR(32)" },
|
||
{ name: "notes", type: "TEXT" },
|
||
{ name: "inspected_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
]},
|
||
{ match: ["fault", "故障"], fields: [
|
||
{ name: "equipment_id", type: "UUID", constraints: "FK → equipment.id" },
|
||
{ name: "reported_by", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "description", type: "TEXT", constraints: "NOT NULL" },
|
||
{ name: "severity", type: "VARCHAR(16)", constraints: "DEFAULT 'medium'" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'open'" },
|
||
{ name: "resolved_at", type: "TIMESTAMPTZ" },
|
||
]},
|
||
{ match: ["equipment", "设备"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "code", type: "VARCHAR(64)", constraints: "UNIQUE" },
|
||
{ name: "location", type: "VARCHAR(128)" },
|
||
{ name: "model", type: "VARCHAR(64)" },
|
||
{ name: "installed_at", type: "DATE" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'active'" },
|
||
]},
|
||
// ── Callback / Satisfaction ──
|
||
{ match: ["callback_plan", "回访计划"], fields: [
|
||
{ name: "customer_id", type: "UUID", constraints: "FK → clients.id" },
|
||
{ name: "scheduled_at", type: "TIMESTAMPTZ", constraints: "NOT NULL" },
|
||
{ name: "assigned_to", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'pending'" },
|
||
]},
|
||
{ match: ["callback_record", "回访记录"], fields: [
|
||
{ name: "plan_id", type: "UUID", constraints: "FK → callback_plans.id" },
|
||
{ name: "customer_id", type: "UUID", constraints: "FK → clients.id" },
|
||
{ name: "content", type: "TEXT" },
|
||
{ name: "result", type: "VARCHAR(32)" },
|
||
{ name: "visited_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
]},
|
||
{ match: ["satisfaction", "满意度"], fields: [
|
||
{ name: "customer_id", type: "UUID", constraints: "FK → clients.id" },
|
||
{ name: "callback_record_id", type: "UUID", constraints: "FK → callback_records.id" },
|
||
{ name: "rating", type: "INTEGER", constraints: "CHECK (rating >= 1 AND rating <= 5)" },
|
||
{ name: "feedback", type: "TEXT" },
|
||
]},
|
||
// ── Warehouse / Stock ──
|
||
{ match: ["stock_in", "入库"], fields: [
|
||
{ name: "product_name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "quantity", type: "INTEGER", constraints: "NOT NULL" },
|
||
{ name: "supplier", type: "VARCHAR(128)" },
|
||
{ name: "received_by", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "received_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
]},
|
||
{ match: ["stock_out", "出库"], fields: [
|
||
{ name: "product_name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "quantity", type: "INTEGER", constraints: "NOT NULL" },
|
||
{ name: "recipient", type: "VARCHAR(128)" },
|
||
{ name: "approved_by", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "shipped_at", type: "TIMESTAMPTZ" },
|
||
]},
|
||
{ match: ["stocktaking", "盘点"], fields: [
|
||
{ name: "warehouse", type: "VARCHAR(64)" },
|
||
{ name: "product_name", type: "VARCHAR(128)" },
|
||
{ name: "expected_qty", type: "INTEGER" },
|
||
{ name: "actual_qty", type: "INTEGER" },
|
||
{ name: "diff", type: "INTEGER" },
|
||
{ name: "stocktaken_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
]},
|
||
{ match: ["stock_alert", "库存预警"], fields: [
|
||
{ name: "product_name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "current_qty", type: "INTEGER" },
|
||
{ name: "threshold", type: "INTEGER" },
|
||
{ name: "alerted_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
]},
|
||
// ── Classroom / Schedule ──
|
||
{ match: ["classroom", "教室"], fields: [
|
||
{ name: "name", type: "VARCHAR(128)", constraints: "NOT NULL" },
|
||
{ name: "building", type: "VARCHAR(64)" },
|
||
{ name: "capacity", type: "INTEGER" },
|
||
{ name: "equipment", type: "JSONB" },
|
||
]},
|
||
{ match: ["schedule", "排课", "课表"], fields: [
|
||
{ name: "course_id", type: "UUID", constraints: "FK → courses.id" },
|
||
{ name: "classroom_id", type: "UUID", constraints: "FK → classrooms.id" },
|
||
{ name: "instructor_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "day_of_week", type: "INTEGER", constraints: "CHECK (day_of_week >= 0 AND day_of_week <= 6)" },
|
||
{ name: "start_time", type: "TIME" },
|
||
{ name: "end_time", type: "TIME" },
|
||
]},
|
||
];
|
||
|
||
/**
|
||
* Match a table name against field templates to get domain-specific fields.
|
||
* Returns the best-matching template's fields, or generic fields as fallback.
|
||
*/
|
||
function getDomainFields(tableName, featureName) {
|
||
const lower = tableName.toLowerCase();
|
||
const featureLower = (featureName || "").toLowerCase();
|
||
for (const template of FIELD_TEMPLATES) {
|
||
// Check exclusions first
|
||
if (template.exclude && template.exclude.some(ex => lower === ex || lower.includes(ex) || featureLower.includes(ex))) {
|
||
continue;
|
||
}
|
||
for (const keyword of template.match) {
|
||
if (lower === keyword || lower.includes(keyword) || featureLower.includes(keyword)) {
|
||
return template.fields;
|
||
}
|
||
}
|
||
}
|
||
// Generic fallback
|
||
return [
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "description", type: "TEXT" },
|
||
{ name: "status", type: "VARCHAR(32)", constraints: "DEFAULT 'active'" },
|
||
{ name: "data", type: "JSONB" },
|
||
];
|
||
}
|
||
|
||
function deriveTableFromFeature(feature) {
|
||
const tableName = toSafeSlug(feature.name);
|
||
if (tableName === "users" || tableName === "auth") return null;
|
||
const domainFields = getDomainFields(tableName, feature.name);
|
||
// Deduplicate: domain fields override base fields with same name
|
||
const baseNames = new Set(["id", "user_id", "created_at", "updated_at"]);
|
||
const merged = [
|
||
{ name: "id", type: "UUID", constraints: "PK, DEFAULT gen_random_uuid()" },
|
||
{ name: "user_id", type: "UUID", constraints: "FK → users.id" },
|
||
...domainFields.filter(f => !baseNames.has(f.name)),
|
||
{ name: "created_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
{ name: "updated_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
];
|
||
return {
|
||
table: tableName,
|
||
description: (feature.description || feature.name) + "表",
|
||
fields: merged,
|
||
indexes: ["idx_" + tableName + "_user ON " + tableName + "(user_id)"],
|
||
};
|
||
}
|
||
|
||
export function generateDatabaseSchema(features, apis, domain) {
|
||
const tables = [];
|
||
|
||
// Always: users table
|
||
tables.push({
|
||
table: "users",
|
||
description: "用户表",
|
||
fields: [
|
||
{ name: "id", type: "UUID", constraints: "PK, DEFAULT gen_random_uuid()" },
|
||
{ name: "phone", type: "VARCHAR(20)", constraints: "UNIQUE" },
|
||
{ name: "nickname", type: "VARCHAR(64)" },
|
||
{ name: "avatar_url", type: "TEXT" },
|
||
{ name: "created_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
{ name: "updated_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
],
|
||
indexes: ["idx_users_phone ON users(phone)"],
|
||
});
|
||
|
||
// Resource tables derived from API paths
|
||
const resourceMap = new Map();
|
||
for (const api of apis) {
|
||
// Extract resource name from path: /api/pets → pets, /api/orders → orders
|
||
const match = api.path.match(/^\/api\/([^/:]+)/);
|
||
if (match) {
|
||
const resource = match[1];
|
||
if (resource === "auth" || resource === "health") continue;
|
||
if (!resourceMap.has(resource)) resourceMap.set(resource, []);
|
||
resourceMap.get(resource).push(api);
|
||
}
|
||
}
|
||
|
||
|
||
// Derive tables from features (P0 fix: user input > domain template)
|
||
const derivedTableNames = new Set();
|
||
for (const feature of features) {
|
||
const table = deriveTableFromFeature(feature);
|
||
if (table && !derivedTableNames.has(table.table)) {
|
||
derivedTableNames.add(table.table);
|
||
tables.push(table);
|
||
}
|
||
}
|
||
|
||
// Also add tables from API paths not covered by features
|
||
for (const [resource] of resourceMap) {
|
||
if (!derivedTableNames.has(resource) && resource !== "users") {
|
||
derivedTableNames.add(resource);
|
||
tables.push({
|
||
table: resource,
|
||
description: resource + "表",
|
||
fields: [
|
||
{ name: "id", type: "UUID", constraints: "PK, DEFAULT gen_random_uuid()" },
|
||
{ name: "user_id", type: "UUID", constraints: "FK → users.id" },
|
||
{ name: "title", type: "VARCHAR(256)", constraints: "NOT NULL" },
|
||
{ name: "data", type: "JSONB" },
|
||
{ name: "created_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
{ name: "updated_at", type: "TIMESTAMPTZ", constraints: "DEFAULT NOW()" },
|
||
],
|
||
});
|
||
}
|
||
}
|
||
|
||
return tables;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 6. API Design Generator
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Group and enrich API requirements.
|
||
*
|
||
* @param {object[]} apiRequirements
|
||
* @returns {object[]}
|
||
*/
|
||
export function designAPI(apiRequirements) {
|
||
// Group APIs by resource
|
||
const resourceMap = new Map();
|
||
|
||
for (const api of apiRequirements) {
|
||
const match = api.path.match(/^\/api\/([^/:]+)/);
|
||
const resource = match ? match[1] : "misc";
|
||
|
||
if (!resourceMap.has(resource)) {
|
||
resourceMap.set(resource, {
|
||
resource,
|
||
basePath: `/api/${resource}`,
|
||
endpoints: [],
|
||
});
|
||
}
|
||
resourceMap.get(resource).endpoints.push({
|
||
method: api.method,
|
||
path: api.path,
|
||
description: api.description,
|
||
});
|
||
}
|
||
|
||
// Sort resources alphabetically
|
||
return Array.from(resourceMap.values()).sort((a, b) => a.resource.localeCompare(b.resource));
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 7. Directory Structure Generator
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Generate a project directory structure from the architecture.
|
||
*
|
||
* @param {string} projectName
|
||
* @param {string} domain
|
||
* @param {object[]} modules
|
||
* @returns {string[]} — Lines of directory tree text
|
||
*/
|
||
export function generateDirectoryStructure(projectName, domain, modules) {
|
||
const dirName = projectName.toLowerCase();
|
||
const lines = [];
|
||
|
||
lines.push(`${dirName}/`);
|
||
lines.push(`├── apps/`);
|
||
lines.push(`│ ├── mobile/ # React Native / Flutter 移动端`);
|
||
lines.push(`│ │ ├── src/`);
|
||
lines.push(`│ │ │ ├── screens/ # 页面组件`);
|
||
|
||
// Screen directories from modules
|
||
for (const mod of modules.slice(0, 4)) {
|
||
lines.push(`│ │ │ │ ├── ${mod.name}/`);
|
||
}
|
||
lines.push(`│ │ │ ├── components/ # 通用组件`);
|
||
lines.push(`│ │ │ ├── hooks/ # 自定义 Hooks`);
|
||
lines.push(`│ │ │ ├── services/ # API 调用层`);
|
||
lines.push(`│ │ │ ├── store/ # 状态管理`);
|
||
lines.push(`│ │ │ └── utils/ # 工具函数`);
|
||
lines.push(`│ │ ├── app.json`);
|
||
lines.push(`│ │ └── package.json`);
|
||
lines.push(`│ └── web/ # Web 管理后台`);
|
||
lines.push(`│ ├── src/`);
|
||
lines.push(`│ │ ├── pages/`);
|
||
lines.push(`│ │ ├── components/`);
|
||
lines.push(`│ │ └── layouts/`);
|
||
lines.push(`│ └── package.json`);
|
||
lines.push(`├── packages/`);
|
||
lines.push(`│ └── shared/ # 共享类型/常量`);
|
||
|
||
// Backend
|
||
lines.push(`├── server/ # NestJS 后端服务`);
|
||
lines.push(`│ ├── src/`);
|
||
lines.push(`│ │ ├── modules/ # 业务模块`);
|
||
|
||
for (const mod of modules.slice(0, 5)) {
|
||
const modName = mod.name.replace(/\s+/g, "-").toLowerCase();
|
||
lines.push(`│ │ │ ├── ${modName}/`);
|
||
lines.push(`│ │ │ │ ├── ${modName}.controller.ts`);
|
||
lines.push(`│ │ │ │ ├── ${modName}.service.ts`);
|
||
lines.push(`│ │ │ │ ├── ${modName}.module.ts`);
|
||
lines.push(`│ │ │ │ └── dto/`);
|
||
}
|
||
|
||
lines.push(`│ │ ├── common/ # 通用(guards, filters, interceptors)`);
|
||
lines.push(`│ │ ├── prisma/ # Prisma ORM`);
|
||
lines.push(`│ │ │ └── schema.prisma`);
|
||
lines.push(`│ │ ├── config/ # 环境配置`);
|
||
lines.push(`│ │ └── main.ts`);
|
||
lines.push(`│ ├── test/`);
|
||
lines.push(`│ ├── Dockerfile`);
|
||
lines.push(`│ └── package.json`);
|
||
|
||
// Root-level files
|
||
lines.push(`├── docker-compose.yml`);
|
||
lines.push(`├── .github/workflows/ # CI/CD`);
|
||
lines.push(`│ └── ci.yml`);
|
||
lines.push(`├── .env.example`);
|
||
lines.push(`├── README.md`);
|
||
lines.push(`└── turbo.json # Monorepo 配置`);
|
||
|
||
return lines;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 8. Main Architecture Pipeline
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
/**
|
||
* Resolve tech stack from PRD.
|
||
*
|
||
* @param {object} prd
|
||
* @returns {object}
|
||
*/
|
||
export function resolveTechStack(prd) {
|
||
const platforms = prd.techConstraints?.platforms || ["web"];
|
||
const constraints = prd.techConstraints?.considerations || [];
|
||
const extraFeatures = prd.extraFeatures || [];
|
||
const recommended = prd.techConstraints?.recommendedStack || "";
|
||
|
||
// Determine platform key
|
||
let key = "default";
|
||
if (platforms.includes("wechat-miniapp")) key = "wechat-miniapp";
|
||
else if (platforms.includes("miniapp")) key = "miniapp";
|
||
else if (platforms.length === 1 && platforms[0] === "ios") key = "ios-only";
|
||
else if (platforms.length === 1 && platforms[0] === "android") key = "android-only";
|
||
else if (platforms.length === 1 && platforms[0] === "web") key = "web-only";
|
||
else if (platforms.includes("mobile") && platforms.includes("web")) key = "mobile-web";
|
||
else if (platforms.includes("mobile")) key = "mobile";
|
||
|
||
const stack = { ...TECH_STACKS[key] };
|
||
|
||
// Extra feature adjustments
|
||
if (extraFeatures.includes("wechat-pay") || extraFeatures.includes("alipay")) {
|
||
stack.considerations = stack.considerations || [];
|
||
stack.considerations.push("需集成第三方支付 SDK");
|
||
}
|
||
if (extraFeatures.includes("logistics-tracking")) {
|
||
stack.considerations = stack.considerations || [];
|
||
stack.considerations.push("需集成物流追踪 API(如快递鸟)");
|
||
}
|
||
if (extraFeatures.includes("real-time")) {
|
||
stack.backend += " + WebSocket (Socket.io)";
|
||
}
|
||
if (extraFeatures.includes("ai-powered")) {
|
||
stack.backend += " + AI 服务(OpenAI / 文心)";
|
||
}
|
||
|
||
return stack;
|
||
}
|
||
|
||
/**
|
||
* Generate full architecture from PRD.
|
||
*
|
||
* @param {object} prd — PRD from SF-01
|
||
* @returns {object}
|
||
*/
|
||
export function generateArchitecture(prd) {
|
||
// Validate input
|
||
if (!prd || prd.error) {
|
||
return {
|
||
error: "INVALID_PRD",
|
||
message: prd?.message || "Invalid PRD input",
|
||
};
|
||
}
|
||
if (!prd.projectName || !prd.features) {
|
||
return {
|
||
error: "INCOMPLETE_PRD",
|
||
message: "PRD missing required fields: projectName, features",
|
||
};
|
||
}
|
||
|
||
const {
|
||
projectName, domain = "generic", features = [], pages = [],
|
||
apiRequirements = [], techConstraints = {}, extraFeatures = [], meta = {},
|
||
} = prd;
|
||
|
||
// 1. Resolve tech stack
|
||
const stack = resolveTechStack(prd);
|
||
|
||
// 2. Decompose modules
|
||
const modules = decomposeModules(features, domain);
|
||
|
||
// 3. Generate architecture diagram
|
||
const moduleNames = modules.map(m => m.label);
|
||
const archDiagram = generateArchDiagram(projectName, stack, moduleNames);
|
||
|
||
// 4. Generate data flows
|
||
const dataFlows = generateDataFlows(pages, apiRequirements);
|
||
|
||
// 5. Generate database schema
|
||
const databaseSchema = generateDatabaseSchema(features, apiRequirements, domain);
|
||
|
||
// 6. Design API
|
||
const apiDesign = designAPI(apiRequirements);
|
||
|
||
// 7. Generate directory structure
|
||
const directoryStructure = generateDirectoryStructure(projectName, domain, modules);
|
||
|
||
// 8. Deployment strategy
|
||
const deployment = {
|
||
environments: ["development", "staging", "production"],
|
||
strategy: stack.deployment || "Docker + Nginx",
|
||
services: [
|
||
"API Server (NestJS)",
|
||
"PostgreSQL 15",
|
||
"Redis 7",
|
||
...(extraFeatures.includes("real-time") ? ["WebSocket Server"] : []),
|
||
],
|
||
ci: "GitHub Actions → Build → Test → Deploy",
|
||
};
|
||
|
||
// Generate contract AFTER schema is built
|
||
const contractArch = { databaseSchema };
|
||
const contract = createContract(prd, contractArch);
|
||
|
||
return {
|
||
projectName,
|
||
domain,
|
||
contract,
|
||
techStack: {
|
||
frontend: stack.frontend,
|
||
backend: stack.backend,
|
||
database: stack.database,
|
||
deployment: stack.deployment,
|
||
considerations: stack.considerations || [],
|
||
},
|
||
architectureDiagram: archDiagram,
|
||
dataFlows,
|
||
modules: modules.map(m => ({
|
||
name: m.name,
|
||
label: m.label,
|
||
features: m.features,
|
||
responsibilities: m.features.map(f => `提供 ${f} 相关功能`),
|
||
})),
|
||
databaseSchema,
|
||
apiDesign,
|
||
directoryStructure,
|
||
deployment,
|
||
meta: {
|
||
generatedAt: new Date().toISOString(),
|
||
sourceDomain: domain,
|
||
moduleCount: modules.length,
|
||
tableCount: databaseSchema.length,
|
||
apiEndpointCount: apiRequirements.length,
|
||
},
|
||
};
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 9. File I/O
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
export function loadPRD(path) {
|
||
try {
|
||
if (!existsSync(path)) {
|
||
return { data: null, error: `PRD file not found: ${path}` };
|
||
}
|
||
const raw = readFileSync(path, "utf-8");
|
||
return { data: JSON.parse(raw), error: null };
|
||
} catch (e) {
|
||
return { data: null, error: `Failed to load PRD: ${e.message}` };
|
||
}
|
||
}
|
||
|
||
export function writeArchitecture(architecture, outputPath, pretty = false) {
|
||
const dir = dirname(outputPath);
|
||
mkdirSync(dir, { recursive: true });
|
||
const content = pretty ? JSON.stringify(architecture, null, 2) : JSON.stringify(architecture);
|
||
writeFileSync(outputPath, content);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// 10. CLI Entry
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
function parseArgs() {
|
||
const args = process.argv.slice(2);
|
||
const opts = {
|
||
input: null, inputText: null, output: null,
|
||
pretty: false, verbose: false, help: false,
|
||
};
|
||
for (let i = 0; i < args.length; i++) {
|
||
if (args[i] === "--input" && args[i + 1]) opts.input = args[++i];
|
||
else if (args[i] === "--input-text" && args[i + 1]) opts.inputText = args[++i];
|
||
else if (args[i] === "--output" && args[i + 1]) opts.output = args[++i];
|
||
else if (args[i] === "--pretty") opts.pretty = true;
|
||
else if (args[i] === "--verbose") opts.verbose = true;
|
||
else if (args[i] === "--help" || args[i] === "-h") opts.help = true;
|
||
}
|
||
return opts;
|
||
}
|
||
|
||
function printJSON(data, pretty) {
|
||
console.log(pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
||
}
|
||
|
||
async function main() {
|
||
const opts = parseArgs();
|
||
|
||
if (opts.help) {
|
||
console.log(`
|
||
Architecture Agent — SF-02
|
||
|
||
Usage:
|
||
node scripts/architecture-agent.mjs --input <prd.json> [options]
|
||
node scripts/architecture-agent.mjs --input-text "<需求>" [options]
|
||
|
||
Options:
|
||
--input <path> PRD JSON 文件路径(SF-01 输出)
|
||
--input-text <text> 直接传入需求(自动调 SF-01 生成 PRD)
|
||
--output <path> 输出架构 JSON 文件路径
|
||
--pretty Format JSON with indentation
|
||
--verbose 详细输出
|
||
--help 显示帮助
|
||
|
||
Examples:
|
||
node scripts/architecture-agent.mjs --input prd-example.json --pretty
|
||
node scripts/architecture-agent.mjs --input-text "做一个电商小程序" --output architecture.json
|
||
`);
|
||
return;
|
||
}
|
||
|
||
let prd = null;
|
||
|
||
if (opts.input) {
|
||
const { data, error } = loadPRD(opts.input);
|
||
if (error) { console.error(error); process.exit(1); }
|
||
prd = data;
|
||
} else if (opts.inputText) {
|
||
// Auto-generate PRD from SF-01
|
||
try {
|
||
const mod = await import("./project-intake-agent.mjs");
|
||
prd = mod.generatePRD(opts.inputText);
|
||
if (prd.error) {
|
||
console.error(`SF-01 PRD generation failed: ${prd.message}`);
|
||
process.exit(1);
|
||
}
|
||
} catch (e) {
|
||
console.error(`Failed to load SF-01 agent: ${e.message}`);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
if (!prd) {
|
||
console.error("Error: --input <path> or --input-text <text> is required. Use --help for usage.");
|
||
process.exit(1);
|
||
}
|
||
|
||
const architecture = generateArchitecture(prd);
|
||
|
||
if (architecture.error) {
|
||
console.error(`Architecture generation failed: ${architecture.message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
if (opts.verbose) {
|
||
console.error(`Project: ${architecture.projectName}`);
|
||
console.error(`Domain: ${architecture.domain}`);
|
||
console.error(`Modules: ${architecture.meta.moduleCount}`);
|
||
console.error(`Tables: ${architecture.meta.tableCount}`);
|
||
console.error(`API Endpoints: ${architecture.meta.apiEndpointCount}`);
|
||
}
|
||
|
||
if (opts.output) {
|
||
writeArchitecture(architecture, resolve(opts.output), opts.pretty);
|
||
if (opts.verbose) console.error(`Architecture written to: ${opts.output}`);
|
||
}
|
||
|
||
printJSON({
|
||
projectName: architecture.projectName,
|
||
domain: architecture.domain,
|
||
techStack: architecture.techStack,
|
||
moduleCount: architecture.meta.moduleCount,
|
||
tableCount: architecture.meta.tableCount,
|
||
apiEndpointCount: architecture.meta.apiEndpointCount,
|
||
outputPath: opts.output || null,
|
||
}, opts.pretty);
|
||
}
|
||
|
||
if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]))) {
|
||
main();
|
||
}
|