Files
16gagent/scripts/project-intake-agent.mjs
2026-06-06 10:40:48 +08:00

1044 lines
47 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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();
}