1044 lines
47 KiB
JavaScript
1044 lines
47 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Project Intake & PRD Agent — SF-01
|
||
*
|
||
* 将用户的一句话需求自动转换为结构化 PRD。
|
||
*
|
||
* 核心功能:
|
||
* - 需求解析:从自然语言提取关键信息
|
||
* - 领域识别:自动匹配项目领域(宠物/电商/教育/健身/企业/笔记等)
|
||
* - PRD 生成:输出产品简介、用户画像、用户故事、功能清单、页面清单、API 需求
|
||
* - 任务拆解:自动拆分为可执行的开发任务列表
|
||
* - MVP 范围:自动确定最小可行产品范围
|
||
*
|
||
* Usage:
|
||
* node scripts/project-intake-agent.mjs --input <text> [options]
|
||
* node scripts/project-intake-agent.mjs --input-file <path> [options]
|
||
*
|
||
* Options:
|
||
* --input <text> 一句话需求(直接输入)
|
||
* --input-file <path> 从 JSON 文件读取需求
|
||
* --output <path> 输出 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 <text> [options]
|
||
node scripts/project-intake-agent.mjs --input-file <path> [options]
|
||
|
||
Options:
|
||
--input <text> 一句话需求
|
||
--input-file <path> 从 JSON 文件读取需求(字段名:input)
|
||
--output <path> 输出 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 <text> or --input-file <path> 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();
|
||
}
|