#!/usr/bin/env node /** * Project Intake & PRD Agent — SF-01 * * 将用户的一句话需求自动转换为结构化 PRD。 * * 核心功能: * - 需求解析:从自然语言提取关键信息 * - 领域识别:自动匹配项目领域(宠物/电商/教育/健身/企业/笔记等) * - PRD 生成:输出产品简介、用户画像、用户故事、功能清单、页面清单、API 需求 * - 任务拆解:自动拆分为可执行的开发任务列表 * - MVP 范围:自动确定最小可行产品范围 * * Usage: * node scripts/project-intake-agent.mjs --input [options] * node scripts/project-intake-agent.mjs --input-file [options] * * Options: * --input 一句话需求(直接输入) * --input-file 从 JSON 文件读取需求 * --output 输出 PRD JSON 文件路径 * --pretty 格式化 JSON 输出 * --verbose 详细输出 * --help 显示帮助 * * @module project-intake-agent */ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(resolve(fileURLToPath(import.meta.url))); const WORKSPACE = resolve(__dirname, ".."); // ═══════════════════════════════════════════════════════ // 1. Domain Knowledge Base // ═══════════════════════════════════════════════════════ /** * Domain definitions: keyword → domain metadata. * Each domain provides templates for PRD generation. */ const DOMAINS = { pet: { keywords: ["宠物", "猫", "狗", "动物", "领养", "遛狗", "萌宠"], projectName: "PetCare", chineseName: "宠物管理", summary: "一款帮助宠物主人管理宠物健康、日常活动和成长记录的移动应用。", personas: [ { name: "宠物主人", description: "养有1-3只宠物的都市青年,希望科学管理宠物健康", painPoints: ["忘记打疫苗时间", "找不到附近靠谱宠物医院", "宠物异常行为无法判断"] }, { name: "宠物医生", description: "社区宠物医院的执业兽医", painPoints: ["病历管理混乱", "与宠物主人沟通低效"] }, ], features: [ { name: "宠物档案", description: "记录宠物的基本信息、品种、年龄、体重", priority: "P0" }, { name: "健康日程", description: "疫苗提醒、驱虫提醒、体检预约", priority: "P0" }, { name: "日常记录", description: "饮食、运动、排泄、体重变化记录", priority: "P1" }, { name: "成长相册", description: "按时间线记录宠物成长照片", priority: "P1" }, { name: "附近医院", description: "地图显示附近宠物医院,支持预约挂号", priority: "P1" }, { name: "健康百科", description: "常见宠物疾病、护理知识库", priority: "P2" }, { name: "社区分享", description: "宠物主人社区,分享萌宠日常", priority: "P2" }, ], pages: [ { name: "首页", route: "/home", description: "宠物卡片、今日提醒、快捷入口" }, { name: "宠物档案", route: "/pet/:id", description: "宠物详细信息、健康记录" }, { name: "健康日程", route: "/schedule", description: "疫苗、驱虫、体检日历" }, { name: "日常记录", route: "/daily-log", description: "饮食/运动/排泄记录表单" }, { name: "成长相册", route: "/album", description: "时间线照片浏览" }, { name: "附近医院", route: "/hospitals", description: "地图+列表视图" }, { name: "我的", route: "/profile", description: "个人中心、设置" }, ], apiRequirements: [ { method: "POST", path: "/api/pets", description: "添加宠物档案" }, { method: "GET", path: "/api/pets/:id", description: "获取宠物详情" }, { method: "PUT", path: "/api/pets/:id", description: "更新宠物信息" }, { method: "GET", path: "/api/schedules", description: "获取健康日程列表" }, { method: "POST", path: "/api/schedules", description: "创建健康日程" }, { method: "POST", path: "/api/daily-logs", description: "记录日常活动" }, { method: "GET", path: "/api/daily-logs", description: "查询日常记录" }, { method: "GET", path: "/api/hospitals", description: "查询附近医院" }, { method: "POST", path: "/api/albums", description: "上传宠物照片" }, ], }, ecommerce: { keywords: ["电商", "商城", "购物", "店铺", "商品", "订单", "支付", "微信支付", "物流", "快递", "小程序"], projectName: "ShopApp", chineseName: "电商平台", summary: "一款支持商品浏览、购物车、在线支付和物流追踪的电商应用。", personas: [ { name: "消费者", description: "在线购物的普通用户", painPoints: ["商品选择困难", "物流进度不透明", "售后流程复杂"] }, { name: "商家", description: "入驻平台的店铺经营者", painPoints: ["订单管理繁琐", "库存同步不及时"] }, ], features: [ { name: "商品浏览", description: "分类浏览、搜索、筛选、商品详情", priority: "P0" }, { name: "购物车", description: "加入购物车、修改数量、批量结算", priority: "P0" }, { name: "订单管理", description: "创建订单、支付、取消、退款", priority: "P0" }, { name: "收货地址", description: "管理多个收货地址", priority: "P0" }, { name: "商品评价", description: "评分、文字评价、晒图", priority: "P1" }, { name: "优惠券", description: "领取、使用、过期提醒", priority: "P1" }, ], pages: [ { name: "首页", route: "/home", description: "轮播图、分类导航、推荐商品" }, { name: "商品详情", route: "/product/:id", description: "商品图片、价格、规格、评价" }, { name: "购物车", route: "/cart", description: "商品列表、价格汇总、去结算" }, { name: "订单列表", route: "/orders", description: "全部订单、状态筛选" }, { name: "订单详情", route: "/order/:id", description: "订单信息、物流追踪" }, { name: "我的", route: "/profile", description: "个人中心、地址管理、优惠券" }, ], apiRequirements: [ { method: "GET", path: "/api/products", description: "商品列表(分页、筛选)" }, { method: "GET", path: "/api/products/:id", description: "商品详情" }, { method: "POST", path: "/api/orders", description: "创建订单" }, { method: "GET", path: "/api/orders/:id", description: "订单详情" }, { method: "POST", path: "/api/payments", description: "发起支付" }, { method: "GET", path: "/api/logistics/:orderId", description: "物流追踪" }, { method: "GET", path: "/api/coupons", description: "优惠券列表" }, ], }, education: { keywords: ["教育", "学习", "课程", "在线", "培训", "考试", "题库", "教学", "直播课", "录播"], projectName: "EduPlatform", chineseName: "在线教育平台", summary: "一个支持在线课程学习、直播授课、题库练习和学情追踪的教育平台。", personas: [ { name: "学生", description: "希望通过线上学习提升技能的在校生或职场人", painPoints: ["课程质量参差不齐", "缺乏学习动力", "疑惑无人解答"] }, { name: "讲师", description: "发布课程内容的专业讲师", painPoints: ["课件管理不便", "学生互动效率低"] }, ], features: [ { name: "课程中心", description: "课程分类、搜索、推荐、试看", priority: "P0" }, { name: "视频播放", description: "在线播放、倍速、清晰度切换、断点续播", priority: "P0" }, { name: "题库练习", description: "章节练习、模拟考试、错题本", priority: "P0" }, { name: "直播课堂", description: "实时直播、弹幕互动、回放", priority: "P1" }, { name: "学习进度", description: "课程进度追踪、学习时长统计", priority: "P1" }, { name: "问答社区", description: "课程问答、讲师答疑", priority: "P2" }, ], pages: [ { name: "首页", route: "/home", description: "推荐课程、热门讲师、学习统计" }, { name: "课程详情", route: "/course/:id", description: "课程介绍、目录、评价" }, { name: "学习页", route: "/learn/:courseId/:lessonId", description: "视频播放器+笔记区" }, { name: "题库", route: "/exercises", description: "章节练习、模拟考试" }, { name: "直播", route: "/live", description: "直播列表、直播间" }, { name: "我的", route: "/profile", description: "学习报告、已购课程、设置" }, ], apiRequirements: [ { method: "GET", path: "/api/courses", description: "课程列表(分页、分类)" }, { method: "GET", path: "/api/courses/:id", description: "课程详情(含章节)" }, { method: "GET", path: "/api/lessons/:id", description: "获取课时内容(视频URL)" }, { method: "GET", path: "/api/exercises", description: "题库列表" }, { method: "POST", path: "/api/exercises/submit", description: "提交练习答案" }, { method: "GET", path: "/api/progress", description: "学习进度查询" }, ], }, fitness: { keywords: ["健身", "运动", "打卡", "跑步", "瑜伽", "训练", "减肥", "跳绳", "游泳"], projectName: "FitTracker", chineseName: "健身打卡", summary: "一款帮助用户记录运动数据、制定训练计划和坚持打卡的健身应用。", personas: [ { name: "健身爱好者", description: "规律运动的都市白领,希望数据化追踪训练效果", painPoints: ["训练计划不科学", "缺少坚持的动力", "运动数据分散"] }, ], features: [ { name: "打卡记录", description: "每日运动打卡、连续天数统计", priority: "P0" }, { name: "训练计划", description: "自定义训练日程、模板推荐", priority: "P0" }, { name: "数据统计", description: "周/月运动时长、卡路里消耗趋势", priority: "P0" }, { name: "运动教程", description: "视频教程、动作指导", priority: "P1" }, { name: "社交分享", description: "分享打卡、好友PK", priority: "P2" }, ], pages: [ { name: "首页", route: "/home", description: "今日打卡、本周统计、训练计划" }, { name: "打卡", route: "/checkin", description: "选择运动类型、记录时长/数据" }, { name: "统计", route: "/stats", description: "运动趋势图表、成就徽章" }, { name: "教程", route: "/tutorials", description: "分类视频教程" }, { name: "我的", route: "/profile", description: "个人资料、目标设置" }, ], apiRequirements: [ { method: "POST", path: "/api/checkins", description: "提交运动打卡" }, { method: "GET", path: "/api/checkins", description: "打卡记录查询" }, { method: "GET", path: "/api/stats", description: "运动统计" }, { method: "GET", path: "/api/plans", description: "训练计划列表" }, ], }, enterprise: { keywords: ["企业", "办公", "OA", "审批", "考勤", "打卡", "部门", "请假", "报销", "人事", "行政", "自动化", "流程"], projectName: "OAFlow", chineseName: "企业办公自动化", summary: "一款支持审批流、考勤管理、部门协作的企业办公自动化系统。", personas: [ { name: "普通员工", description: "需要日常考勤打卡、发起审批的企业员工", painPoints: ["审批流程繁琐", "考勤异常处理麻烦"] }, { name: "部门主管", description: "负责审批、团队管理的管理者", painPoints: ["审批堆积", "团队出勤无法一目了然"] }, { name: "HR/行政", description: "负责考勤统计、人事管理的HR", painPoints: ["考勤数据手工统计", "跨部门流程混乱"] }, ], features: [ { name: "考勤打卡", description: "GPS定位打卡、外勤打卡、补卡申请", priority: "P0" }, { name: "审批流程", description: "自定义审批模板、多级审批、催办", priority: "P0" }, { name: "部门管理", description: "组织架构、人员管理、角色权限", priority: "P0" }, { name: "请假管理", description: "请假申请、余额查询、审批记录", priority: "P0" }, { name: "考勤统计", description: "日报/月报、异常汇总、导出", priority: "P1" }, { name: "公告通知", description: "公司公告、部门通知、已读确认", priority: "P1" }, { name: "报销管理", description: "报销申请、发票上传、审批", priority: "P2" }, ], pages: [ { name: "首页", route: "/home", description: "待办审批、考勤状态、公告" }, { name: "考勤", route: "/attendance", description: "打卡、考勤记录、统计" }, { name: "审批", route: "/approvals", description: "我发起的、待我审批、已处理" }, { name: "通讯录", route: "/contacts", description: "组织架构、人员搜索" }, { name: "公告", route: "/notices", description: "公告列表、详情" }, { name: "我的", route: "/profile", description: "个人资料、请假余额、设置" }, ], apiRequirements: [ { method: "POST", path: "/api/attendance/checkin", description: "考勤打卡" }, { method: "GET", path: "/api/attendance/records", description: "考勤记录查询" }, { method: "POST", path: "/api/approvals", description: "发起审批" }, { method: "GET", path: "/api/approvals", description: "审批列表" }, { method: "PUT", path: "/api/approvals/:id", description: "审批处理(通过/驳回)" }, { method: "GET", path: "/api/departments", description: "部门列表" }, { method: "GET", path: "/api/users", description: "人员查询" }, { method: "POST", path: "/api/notices", description: "发布公告" }, ], }, note: { keywords: ["笔记", "备忘录", "记事本", "便签", "日记", "记录", "写作", "文档", "Markdown", "todo"], projectName: "NoteApp", chineseName: "笔记应用", summary: "一款支持多端同步、Markdown 编辑和标签管理的笔记应用。", personas: [ { name: "知识工作者", description: "需要记录灵感和整理知识的职场人", painPoints: ["笔记分散多处", "搜索困难", "格式化繁琐"] }, ], features: [ { name: "笔记编辑", description: "富文本/Markdown编辑器、实时保存", priority: "P0" }, { name: "文件夹/标签", description: "分类管理、多级文件夹", priority: "P0" }, { name: "全文搜索", description: "标题+内容全文检索", priority: "P0" }, { name: "多端同步", description: "手机/PC/Web 实时同步", priority: "P1" }, { name: "协作分享", description: "分享链接、协作编辑", priority: "P2" }, ], pages: [ { name: "笔记列表", route: "/notes", description: "文件夹导航、笔记列表、搜索" }, { name: "笔记编辑", route: "/note/:id", description: "编辑器页面" }, { name: "标签管理", route: "/tags", description: "标签列表、关联笔记" }, { name: "设置", route: "/settings", description: "主题、同步、账户" }, ], apiRequirements: [ { method: "GET", path: "/api/notes", description: "笔记列表" }, { method: "POST", path: "/api/notes", description: "创建笔记" }, { method: "PUT", path: "/api/notes/:id", description: "更新笔记" }, { method: "DELETE", path: "/api/notes/:id", description: "删除笔记" }, { method: "GET", path: "/api/tags", description: "标签列表" }, ], }, social: { keywords: ["社交", "社区", "朋友圈", "动态", "关注", "粉丝", "私信", "评论", "点赞", "分享"], projectName: "SocialApp", chineseName: "社交平台", summary: "一款以内容分享和用户互动为核心的社交应用。", personas: [ { name: "内容创作者", description: "发布图文/视频内容的活跃用户", painPoints: ["内容曝光不足", "互动管理复杂"] }, { name: "普通用户", description: "浏览内容和互动的用户", painPoints: ["信息过载", "隐私顾虑"] }, ], features: [ { name: "动态发布", description: "图文/视频发布、滤镜编辑", priority: "P0" }, { name: "信息流", description: "关注动态推荐、时间线", priority: "P0" }, { name: "互动系统", description: "点赞、评论、转发、收藏", priority: "P0" }, { name: "个人主页", description: "用户资料、动态列表", priority: "P0" }, { name: "私信", description: "一对一聊天、群聊", priority: "P1" }, ], pages: [ { name: "首页", route: "/home", description: "信息流" }, { name: "发布", route: "/publish", description: "编辑器、相册选择" }, { name: "消息", route: "/messages", description: "私信列表、聊天" }, { name: "个人", route: "/profile/:userId", description: "个人主页" }, ], apiRequirements: [ { method: "GET", path: "/api/feed", description: "信息流" }, { method: "POST", path: "/api/posts", description: "发布动态" }, { method: "POST", path: "/api/posts/:id/like", description: "点赞" }, { method: "POST", path: "/api/comments", description: "发表评论" }, { method: "GET", path: "/api/users/:id", description: "用户信息" }, ], }, food: { keywords: ["外卖", "点餐", "美食", "餐厅", "食谱", "烹饪", "菜谱", "食材", "厨房"], projectName: "FoodApp", chineseName: "美食应用", summary: "一款支持餐厅浏览、在线点餐或食谱管理的餐饮应用。", personas: [ { name: "食客", description: "喜欢尝试不同美食的用户", painPoints: ["不知道吃什么", "排队等待时间长"] }, ], features: [ { name: "餐厅浏览", description: "附近餐厅、评分、评价", priority: "P0" }, { name: "在线点餐", description: "菜单浏览、下单、支付", priority: "P0" }, { name: "订单追踪", description: "订单状态、配送进度", priority: "P0" }, { name: "评价系统", description: "评分、评价、晒图", priority: "P1" }, ], pages: [ { name: "首页", route: "/home", description: "附近推荐、搜索" }, { name: "餐厅详情", route: "/restaurant/:id", description: "菜单、评价" }, { name: "购物车", route: "/cart", description: "已选菜品、下单" }, { name: "订单", route: "/orders", description: "订单列表、状态" }, { name: "我的", route: "/profile", description: "个人中心" }, ], apiRequirements: [ { method: "GET", path: "/api/restaurants", description: "餐厅列表" }, { method: "GET", path: "/api/menus/:restaurantId", description: "菜单" }, { method: "POST", path: "/api/orders", description: "创建订单" }, { method: "GET", path: "/api/orders/:id", description: "订单详情" }, ], }, travel: { keywords: ["旅游", "旅行", "酒店", "机票", "攻略", "行程", "景点", "民宿", "签证"], projectName: "TravelApp", chineseName: "旅游应用", summary: "一款支持行程规划、酒店预订和旅游攻略浏览的旅行应用。", personas: [ { name: "旅行者", description: "喜欢自由行的年轻旅行者", painPoints: ["行程规划费时", "当地信息获取困难"] }, ], features: [ { name: "行程规划", description: "多日行程编排、景点添加", priority: "P0" }, { name: "攻略浏览", description: "目的地攻略、游记分享", priority: "P0" }, { name: "预订服务", description: "酒店/门票预订", priority: "P1" }, { name: "地图导航", description: "景点地图、路线规划", priority: "P1" }, ], pages: [ { name: "首页", route: "/home", description: "热门目的地、推荐攻略" }, { name: "攻略详情", route: "/guide/:id", description: "图文攻略" }, { name: "行程", route: "/itinerary", description: "行程编辑器" }, { name: "预订", route: "/booking", description: "酒店/门票搜索" }, { name: "我的", route: "/profile", description: "我的行程、收藏" }, ], apiRequirements: [ { method: "GET", path: "/api/destinations", description: "目的地列表" }, { method: "GET", path: "/api/guides", description: "攻略列表" }, { method: "POST", path: "/api/itineraries", description: "创建行程" }, { method: "GET", path: "/api/hotels", description: "酒店搜索" }, ], }, }; /** * Default / generic domain for unrecognized inputs. * Only provides personas — features/pages/APIs must come from user input. */ const DEFAULT_DOMAIN = { keywords: [], projectName: "MyProject", chineseName: "通用应用", summary: "一款根据用户需求定制的应用。", personas: [ { name: "用户", description: "目标用户群体", painPoints: ["待明确"] }, ], features: [], pages: [], apiRequirements: [], }; // ═══════════════════════════════════════════════════════ // 2. Input Parser // ═══════════════════════════════════════════════════════ /** * Detect platform preference from input text. * * @param {string} input * @returns {string[]} */ export function detectPlatforms(input) { const platforms = []; const patterns = [ { keyword: "小程序", platform: "miniapp" }, { keyword: "微信小程序", platform: "wechat-miniapp" }, { keyword: "iOS", platform: "ios" }, { keyword: "Android", platform: "android" }, { keyword: "安卓", platform: "android" }, { keyword: "Web", platform: "web" }, { keyword: "网页", platform: "web" }, { keyword: "H5", platform: "h5" }, { keyword: "桌面", platform: "desktop" }, { keyword: "PC", platform: "desktop" }, { keyword: "移动端", platform: "mobile" }, { keyword: "手机", platform: "mobile" }, ]; for (const { keyword, platform } of patterns) { if (input.includes(keyword) && !platforms.includes(platform)) { platforms.push(platform); } } return platforms.length > 0 ? platforms : ["mobile", "web"]; } /** * Detect special feature requirements from input text. * * @param {string} input * @returns {string[]} */ export function detectExtraFeatures(input) { const features = []; const patterns = [ { keyword: "微信支付", feature: "wechat-pay" }, { keyword: "支付宝", feature: "alipay" }, { keyword: "支付", feature: "online-payment" }, { keyword: "物流", feature: "logistics-tracking" }, { keyword: "推送", feature: "push-notification" }, { keyword: "通知", feature: "push-notification" }, { keyword: "AI", feature: "ai-powered" }, { keyword: "智能推荐", feature: "smart-recommendation" }, { keyword: "离线", feature: "offline-mode" }, { keyword: "多语言", feature: "i18n" }, { keyword: "国际化", feature: "i18n" }, { keyword: "暗黑模式", feature: "dark-mode" }, { keyword: "深色模式", feature: "dark-mode" }, { keyword: "实时", feature: "real-time" }, { keyword: "语音", feature: "voice-input" }, ]; for (const { keyword, feature } of patterns) { if (input.includes(keyword) && !features.includes(feature)) { features.push(feature); } } return features; } // ═══════════════════════════════════════════════════════ // 3. Domain Matcher // ═══════════════════════════════════════════════════════ /** * Match input to the best-fitting domain. * Returns matchCount so caller can decide confidence. * * @param {string} input — User input text * @returns {{ domain: object, domainKey: string, matchCount: number }} */ export function matchDomain(input) { let bestDomain = { domain: DEFAULT_DOMAIN, domainKey: "generic", matchCount: 0 }; for (const [key, domain] of Object.entries(DOMAINS)) { let count = 0; for (const kw of domain.keywords) { if (input.includes(kw)) count++; } if (count > bestDomain.matchCount) { bestDomain = { domain, domainKey: key, matchCount: count }; } } return bestDomain; } // ═══════════════════════════════════════════════════════ // 3b. User Input Extractor (P0 fix: user input > domain template) // ═══════════════════════════════════════════════════════ /** * Extract project name from user input. * Patterns: "叫 XXX", "命名为 XXX", "名为 XXX", "叫做 XXX" */ function extractProjectName(input) { const patterns = [ /叫[\s]*["「]?([A-Za-z\u4e00-\u9fff]{2,20})["」]?/, /命名[为]?[\s]*["「]?([A-Za-z\u4e00-\u9fff]{2,20})["」]?/, /名为[\s]*["「]?([A-Za-z\u4e00-\u9fff]{2,20})["」]?/, /叫做[\s]*["「]?([A-Za-z\u4e00-\u9fff]{2,20})["」]?/, ]; for (const p of patterns) { const m = input.match(p); if (m && m[1]) return m[1]; } return null; } /** * Extract features from user input. * Looks for numbered items (1. 2. 3.) and Chinese feature keywords. */ function extractFeaturesFromInput(input) { const features = []; // Pattern 1: numbered items "1. 功能名:描述" or "1、功能名" const numbered = input.matchAll(/\d+[.、))][\s]*([^\d\.、))\n]{2,60})/g); for (const m of numbered) { const text = m[1].trim(); // Split on colon/description separator const parts = text.split(/[::]/); const name = parts[0].trim().replace(/[。,、;:!?\.\,\;\!\?]+$/g, "").trim(); const desc = parts[1]?.trim() || name; if (name.length >= 2 && name.length <= 30) { features.push({ name, description: desc, priority: "P0" }); } } // Pattern 2: if no numbered items, try comma-separated features after "功能" or "支持" if (features.length === 0) { const funcMatch = input.match(/(?:功能|支持)[::]*([^。]+)/); if (funcMatch) { const items = funcMatch[1].split(/[,,、和]/); for (const item of items) { const name = item.trim().replace(/^[\s]+/, ""); if (name.length >= 2 && name.length <= 20) { features.push({ name, description: name, priority: "P0" }); } } } } return features; } /** * Extract pages/screens from user input. * Looks for page-related keywords. */ function extractPagesFromInput(input, features) { const pages = []; const pageKeywords = ["首页", "列表", "详情", "编辑", "设置", "管理", "统计", "仪表盘", "搜索", "个人", "我的", "登录", "注册"]; // Extract from numbered items that contain page-like words const numbered = input.matchAll(/\d+[.、))][\s]*([^\d\.、))\n]{2,60})/g); for (const m of numbered) { const text = m[1].trim(); const parts = text.split(/[::]/); const name = parts[0].trim(); const desc = parts[1]?.trim() || ""; if (pageKeywords.some(kw => name.includes(kw) || desc.includes(kw))) { const route = "/" + _toSlug(name); pages.push({ name, route, description: desc || name }); } } // Derive pages from features — each feature becomes a page if (features.length > 0) { const existingNames = new Set(pages.map(p => p.name)); for (const f of features) { if (!existingNames.has(f.name)) { const route = "/" + _toSlug(f.name); pages.push({ name: f.name, route, description: f.description || f.name }); } } } // Always add home page if not present if (!pages.some(p => p.name === "首页")) { pages.unshift({ name: "首页", route: "/home", description: "应用首页" }); } return pages; } /** * Extract API requirements from user input. * Derives CRUD APIs from features. */ /** * Chinese-to-English slug for API paths and page routes. */ const _ZH_SLUG = { "书籍库": "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", "客户管理": "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 _toSlug(name) { const c = name.replace(/[。,、;:!?\.\,\;\!\?]+$/g, "").trim(); if (/^[a-z][a-z0-9_]*$/.test(c)) return c; if (_ZH_SLUG[c]) return _ZH_SLUG[c]; // Longest-match-first: sort keys by length descending for (const zh of Object.keys(_ZH_SLUG).sort((a,b) => b.length - a.length)) { if (c.includes(zh)) return _ZH_SLUG[zh]; } return c.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase().replace(/_+/g, "_").replace(/^_|_$/g, "") || "items"; } function extractAPIsFromInput(input, features) { const apis = []; // Check for explicit API mentions const apiPattern = /(?:API|接口|api)[::]*([^。]+)/g; const apiMatches = input.matchAll(apiPattern); for (const m of apiMatches) { const desc = m[1].trim(); apis.push({ method: "GET", path: "/api/data", description: desc }); } // If no explicit APIs, derive CRUD from features if (apis.length === 0 && features.length > 0) { for (const f of features.slice(0, 4)) { const slug = _toSlug(f.name); apis.push({ method: "GET", path: `/api/${slug}`, description: `获取${f.name}列表` }); apis.push({ method: "POST", path: `/api/${slug}`, description: `创建${f.name}` }); } // Always add auth apis.push({ method: "POST", path: "/api/auth/login", description: "用户登录" }); apis.push({ method: "GET", path: "/api/auth/me", description: "获取当前用户" }); } return apis; } // ═══════════════════════════════════════════════════════ // 4. PRD Generator // ═══════════════════════════════════════════════════════ /** * Generate user stories from personas. * * @param {object[]} personas * @param {object[]} features * @returns {object[]} */ export function generateUserStories(personas, features) { const stories = []; let id = 1; for (const persona of personas) { for (const feature of features.slice(0, 3)) { stories.push({ id: `US-${String(id).padStart(3, "0")}`, as: persona.name, want: feature.description, soThat: `解决痛点:${persona.painPoints[0] || "提升使用体验"}`, priority: feature.priority, }); id++; } } return stories; } /** * Determine tech constraints based on domain and extra features. * * @param {object} domain * @param {string[]} platforms * @param {string[]} extraFeatures * @returns {object} */ export function determineTechConstraints(domain, platforms, extraFeatures) { const constraints = { platforms, recommendedStack: "", considerations: [], }; if (platforms.includes("miniapp") || platforms.includes("wechat-miniapp")) { constraints.recommendedStack = "微信小程序原生 / Taro / uni-app"; constraints.considerations.push("需通过微信审核"); constraints.considerations.push("包大小限制 2MB(分包后可至 20MB)"); } else if (platforms.includes("ios") && platforms.length === 1) { constraints.recommendedStack = "SwiftUI / UIKit"; constraints.considerations.push("需通过 App Store 审核"); } else if (platforms.includes("android") && platforms.length === 1) { constraints.recommendedStack = "Jetpack Compose / Kotlin"; constraints.considerations.push("需适配 Android 各版本"); } else { constraints.recommendedStack = "React Native / Flutter(跨平台)"; constraints.considerations.push("建议跨平台框架减少开发成本"); } if (extraFeatures.includes("wechat-pay")) { constraints.considerations.push("需接入微信支付商户平台"); } if (extraFeatures.includes("alipay")) { constraints.considerations.push("需接入支付宝开放平台"); } if (extraFeatures.includes("logistics-tracking")) { constraints.considerations.push("需对接物流公司 API(如快递鸟、菜鸟)"); } if (extraFeatures.includes("ai-powered")) { constraints.considerations.push("需对接 AI 服务(如 OpenAI / 文心 / 通义)"); } if (extraFeatures.includes("real-time")) { constraints.recommendedStack += " + WebSocket"; constraints.considerations.push("需 WebSocket 或 SSE 支持实时通信"); } if (extraFeatures.includes("offline-mode")) { constraints.considerations.push("需实现本地缓存和离线数据同步策略"); } if (extraFeatures.includes("i18n")) { constraints.considerations.push("需集成 i18n 国际化方案"); } if (extraFeatures.includes("push-notification")) { constraints.considerations.push("需对接推送服务(APNs / FCM / 个推)"); } // Domain-specific constraints if (domain.chineseName?.includes("企业") || domain.chineseName?.includes("OA")) { constraints.considerations.push("需支持 RBAC 权限模型"); constraints.considerations.push("需考虑企业 SSO 集成"); } if (domain.chineseName?.includes("电商")) { constraints.considerations.push("需考虑高并发秒杀场景"); constraints.considerations.push("需 PCI-DSS 或等保合规"); } return constraints; } /** * Generate development tasks from features. * * @param {string} projectName * @param {object[]} features * @param {object[]} pages * @param {object[]} apiRequirements * @returns {object[]} */ export function generateDevTasks(projectName, features, pages, apiRequirements) { const tasks = []; let id = 1; // Foundation tasks tasks.push({ id: `T-${String(id).padStart(3, "0")}`, title: "项目脚手架搭建", description: `初始化 ${projectName} 项目结构,配置构建工具和 CI/CD`, phase: "Foundation", estimatedHours: 8, priority: "P0", }); id++; tasks.push({ id: `T-${String(id).padStart(3, "0")}`, title: "数据库设计与初始化", description: "设计数据模型,创建数据库迁移脚本", phase: "Foundation", estimatedHours: 16, priority: "P0", }); id++; // Page tasks for (const page of pages.slice(0, 4)) { tasks.push({ id: `T-${String(id).padStart(3, "0")}`, title: `页面开发:${page.name}`, description: `开发 ${page.name} 页面(${page.route}):${page.description}`, phase: "UI Development", estimatedHours: 8, priority: "P0", }); id++; } // API tasks for (const api of apiRequirements.slice(0, 4)) { tasks.push({ id: `T-${String(id).padStart(3, "0")}`, title: `API 开发:${api.method} ${api.path}`, description: api.description, phase: "Backend Development", estimatedHours: 4, priority: "P0", }); id++; } // Integration & polish tasks.push({ id: `T-${String(id).padStart(3, "0")}`, title: "前后端联调", description: "前端页面与后端 API 集成联调", phase: "Integration", estimatedHours: 16, priority: "P0", }); id++; tasks.push({ id: `T-${String(id).padStart(3, "0")}`, title: "测试与修复", description: "功能测试、Bug 修复、性能优化", phase: "Testing", estimatedHours: 24, priority: "P1", }); id++; return tasks; } // ═══════════════════════════════════════════════════════ // 5. Main PRD Pipeline // ═══════════════════════════════════════════════════════ /** * Generate a full PRD from user input. * * P0 FIX: User input is parsed FIRST. Domain template is supplementary only. * If user input provides features/pages/APIs, those take priority. * If extraction yields empty results, fail with error. * * @param {string} input — User's requirement description * @returns {object} — Structured PRD */ export function generatePRD(input) { const trimmed = (input || "").trim(); if (!trimmed) { return { error: "EMPTY_INPUT", message: "请输入需求描述。例如:做一个宠物管理 App", }; } // ── Step 1: Extract from user input (HIGHEST PRIORITY) ── const extractedName = extractProjectName(trimmed); const extractedFeatures = extractFeaturesFromInput(trimmed); const extractedAPIs = extractAPIsFromInput(trimmed, extractedFeatures); // ── Step 2: Match domain (supplementary only) ── const { domain, domainKey, matchCount } = matchDomain(trimmed); // ── Step 3: Merge — user input wins, domain fills gaps ── const projectName = extractedName || domain.projectName; const features = extractedFeatures.length > 0 ? extractedFeatures : domain.features; // Merge APIs: prefer domain template when user didn't mention explicit APIs // (extractedAPIs always includes CRUD per feature + auth; explicit API mentions use API/接口 pattern) const userMentionedAPIs = /(?:API|接口|api)/g.test(trimmed); const apiRequirements = userMentionedAPIs ? (extractedAPIs.length > 0 ? extractedAPIs : domain.apiRequirements) : (domain.apiRequirements.length > 0 ? domain.apiRequirements : extractedAPIs); // Extract pages AFTER features are resolved (so derived pages use final features) const extractedPages = extractPagesFromInput(trimmed, features); // Merge pages: prefer domain template when user didn't mention specific pages // (extractPagesFromInput always adds 首页 + feature-derived pages; explicit mentions are numbered lists) const userMentionedPages = /\d+[.、))]/g.test(trimmed); const pages = userMentionedPages ? extractedPages : domain.pages; // ── Step 4: Fail if still empty (P0: prevent empty generation) ── if (features.length === 0 || pages.length === 0 || apiRequirements.length === 0) { return { error: "EXTRACTION_FAILED", message: `Requirement extraction failed: no ${features.length === 0 ? "features" : ""}${pages.length === 0 ? "/pages" : ""}${apiRequirements.length === 0 ? "/apis" : ""} detected. Please provide more detailed requirements.`, projectName, domain: domainKey, features, pages, apiRequirements, }; } // ── Step 5: Build PRD ── const platforms = detectPlatforms(trimmed); const extraFeatures = detectExtraFeatures(trimmed); const userStories = generateUserStories(domain.personas, features); const techConstraints = determineTechConstraints(domain, platforms, extraFeatures); const devTasks = generateDevTasks(projectName, features, pages, apiRequirements); const mvpFeatures = features.filter(f => f.priority === "P0").map(f => f.name); const mvpPages = pages.slice(0, Math.ceil(pages.length * 0.6)).map(p => p.name); const totalEstimatedHours = devTasks.reduce((sum, t) => sum + t.estimatedHours, 0); return { projectName, chineseName: domain.chineseName || projectName, domain: domainKey, matchConfidence: matchCount > 0 ? (extractedFeatures.length > 0 ? "high" : "medium") : "low", summary: domain.summary || `${projectName} — 根据用户需求定制的应用。`, personas: domain.personas, userStories, mvpScope: { description: `MVP 聚焦 ${mvpFeatures.join("、")},包含页面:${mvpPages.join("、")}`, features: mvpFeatures, pages: mvpPages, estimatedWeeks: Math.ceil(totalEstimatedHours / 40) || 2, }, features, pages, apiRequirements, techConstraints, extraFeatures, devTasks, meta: { generatedAt: new Date().toISOString(), inputLength: trimmed.length, domainMatchCount: matchCount, extractedFeatureCount: extractedFeatures.length, extractedPageCount: extractedPages.length, extractedAPICount: extractedAPIs.length, }, }; } // ═══════════════════════════════════════════════════════ // 6. File I/O // ═══════════════════════════════════════════════════════ export function loadInput(path) { try { if (!existsSync(path)) { return { data: null, error: `Input 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 input: ${e.message}` }; } } export function writeOutput(prd, outputPath, pretty = false) { const dir = dirname(outputPath); mkdirSync(dir, { recursive: true }); const content = pretty ? JSON.stringify(prd, null, 2) : JSON.stringify(prd); writeFileSync(outputPath, content); } // ═══════════════════════════════════════════════════════ // 7. CLI Entry // ═══════════════════════════════════════════════════════ function parseArgs() { const args = process.argv.slice(2); const opts = { input: null, inputFile: 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-file" && args[i + 1]) opts.inputFile = 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(` Project Intake & PRD Agent — SF-01 Usage: node scripts/project-intake-agent.mjs --input [options] node scripts/project-intake-agent.mjs --input-file [options] Options: --input 一句话需求 --input-file 从 JSON 文件读取需求(字段名:input) --output 输出 PRD JSON 文件路径 --pretty Format JSON with indentation --verbose 详细输出 --help 显示帮助 Examples: node scripts/project-intake-agent.mjs --input "做一个宠物管理 App" --pretty node scripts/project-intake-agent.mjs --input-file test/fixtures/project-intake/pet-management.json --output prd-output.json `); return; } let inputText = null; if (opts.input) { inputText = opts.input; } else if (opts.inputFile) { const { data, error } = loadInput(opts.inputFile); if (error) { console.error(error); process.exit(1); } inputText = data.input || ""; } if (!inputText) { console.error("Error: --input or --input-file is required. Use --help for usage."); process.exit(1); } const prd = generatePRD(inputText); if (opts.verbose) { console.error(`Input: "${inputText}"`); console.error(`Domain: ${prd.domain} (confidence: ${prd.matchConfidence})`); console.error(`Features: ${prd.features.length}, Pages: ${prd.pages.length}, APIs: ${prd.apiRequirements.length}`); console.error(`Dev Tasks: ${prd.devTasks.length}, Est. Hours: ${prd.devTasks.reduce((s,t) => s + t.estimatedHours, 0)}`); } if (opts.output) { writeOutput(prd, resolve(opts.output), opts.pretty); if (opts.verbose) console.error(`PRD written to: ${opts.output}`); } // Check for extraction failure if (prd.error === "EXTRACTION_FAILED") { console.error(prd.message); if (opts.output) writeOutput(prd, resolve(opts.output), opts.pretty); process.exit(1); } // Output project name + summary as minified summary printJSON({ projectName: prd.projectName, chineseName: prd.chineseName, domain: prd.domain, summary: prd.summary, featureCount: prd.features.length, pageCount: prd.pages.length, apiCount: prd.apiRequirements.length, taskCount: prd.devTasks.length, totalEstimatedHours: prd.devTasks.reduce((s, t) => s + t.estimatedHours, 0), 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(); }