#!/usr/bin/env node /** * Architecture Agent — SF-02 * * 根据 SF-01 生成的 PRD 自动生成技术架构。 * * 核心功能: * - 技术选型:前端/后端/数据库/部署 * - 系统架构图:ASCII art 三层架构 * - 数据流:请求链路文档 * - 模块划分:按功能域拆解 * - 数据库设计:表结构 + 字段 + 关系 * - API 设计:按资源分组 * - 目录结构:完整 /src 树 * * Usage: * node scripts/architecture-agent.mjs --input [options] * node scripts/architecture-agent.mjs --input-text "做一个宠物管理 App" [options] * * Options: * --input PRD JSON 文件路径 * --input-text 直接传入需求(先调 SF-01 生成 PRD) * --output 输出架构 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 [options] node scripts/architecture-agent.mjs --input-text "<需求>" [options] Options: --input PRD JSON 文件路径(SF-01 输出) --input-text 直接传入需求(自动调 SF-01 生成 PRD) --output 输出架构 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 or --input-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(); }