prd-workflow-cli 1.2.6 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/prd-cli.js CHANGED
@@ -102,12 +102,12 @@ program
102
102
 
103
103
  // A2UI 预览服务
104
104
  program
105
- .command('ui')
106
- .description('启动 A2UI 界面预览服务')
105
+ .command('ui [file]')
106
+ .description('启动 A2UI 界面预览服务 [可指定 .json 文件]')
107
107
  .option('-p, --port <number>', '指定端口号', '3333')
108
- .action((options) => {
108
+ .action((file, options) => {
109
109
  const A2UIServer = require('../commands/a2ui-server');
110
- const server = new A2UIServer(options.port);
110
+ const server = new A2UIServer(options.port, file);
111
111
  server.start();
112
112
  });
113
113
 
@@ -5,8 +5,9 @@ const chalk = require('chalk');
5
5
  const { exec } = require('child_process');
6
6
 
7
7
  class A2UIServer {
8
- constructor(port = 3333) {
8
+ constructor(port = 3333, targetFile = null) {
9
9
  this.port = port;
10
+ this.targetFile = targetFile;
10
11
  this.viewerPath = path.join(__dirname, '../a2ui-viewer');
11
12
  this.projectPath = process.cwd();
12
13
  }
@@ -18,11 +19,32 @@ class A2UIServer {
18
19
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
19
20
 
20
21
  // 路由处理
21
- if (req.url === '/') {
22
+ const parsedUrl = new URL(req.url, `http://localhost:${this.port}`);
23
+ const pathname = parsedUrl.pathname;
24
+
25
+ if (pathname === '/') {
22
26
  this.serveFile(res, path.join(this.viewerPath, 'index.html'), 'text/html');
23
- } else if (req.url === '/ui.json') {
24
- // 读取项目根目录下的 a2ui-data.json
25
- this.serveFile(res, path.join(this.projectPath, '.a2ui/current.json'), 'application/json');
27
+ } else if (pathname === '/ui.json') {
28
+ // Priority: 1. URL Query Param (?file=...) 2. CLI Argument 3. Default
29
+ const queryFile = parsedUrl.searchParams.get('file');
30
+ let jsonPath = path.join(this.projectPath, '.a2ui/current.json');
31
+
32
+ if (queryFile) {
33
+ // Prevent directory traversal attacks (basic check)
34
+ // Allow relative paths from project root
35
+ jsonPath = path.resolve(this.projectPath, queryFile);
36
+ if (!jsonPath.startsWith(this.projectPath)) {
37
+ res.writeHead(403);
38
+ res.end('Access denied: File outside project directory');
39
+ return;
40
+ }
41
+ } else if (this.targetFile) {
42
+ jsonPath = path.isAbsolute(this.targetFile)
43
+ ? this.targetFile
44
+ : path.join(this.projectPath, this.targetFile);
45
+ }
46
+
47
+ this.serveFile(res, jsonPath, 'application/json');
26
48
  } else {
27
49
  res.writeHead(404);
28
50
  res.end('Not found');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prd-workflow-cli",
3
- "version": "1.2.6",
3
+ "version": "1.3.4",
4
4
  "description": "产品需求管理规范 CLI 工具 - 基于 A→R→B→C 流程,集成 PM 确认机制和对话归档的需求管理命令行工具",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,127 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{TITLE}} - PRD UI 原型</title>
8
+ <!-- React -->
9
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
10
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
11
+ <!-- Ant Design -->
12
+ <link rel="stylesheet" href="https://unpkg.com/antd@5/dist/reset.css">
13
+ <script src="https://unpkg.com/dayjs@1/dayjs.min.js"></script>
14
+ <script src="https://unpkg.com/antd@5/dist/antd.min.js"></script>
15
+ <!-- Icons -->
16
+ <script src="https://unpkg.com/@ant-design/icons@5/dist/index.umd.min.js"></script>
17
+ <style>
18
+ body {
19
+ margin: 0;
20
+ padding: 24px;
21
+ background: #f5f5f5;
22
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
23
+ }
24
+
25
+ #root {
26
+ max-width: 1400px;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .page-header {
31
+ margin-bottom: 16px;
32
+ }
33
+
34
+ .page-title {
35
+ font-size: 20px;
36
+ font-weight: 600;
37
+ color: rgba(0, 0, 0, 0.88);
38
+ margin: 0;
39
+ }
40
+
41
+ .meta-info {
42
+ background: #fff;
43
+ border: 1px solid #d9d9d9;
44
+ border-radius: 6px;
45
+ padding: 12px 16px;
46
+ margin-bottom: 16px;
47
+ font-size: 13px;
48
+ color: rgba(0, 0, 0, 0.65);
49
+ }
50
+
51
+ .meta-info span {
52
+ margin-right: 24px;
53
+ }
54
+
55
+ .meta-info strong {
56
+ color: rgba(0, 0, 0, 0.88);
57
+ }
58
+ </style>
59
+ </head>
60
+
61
+ <body>
62
+ <div class="meta-info">
63
+ <span><strong>需求编号:</strong>{{REQ_ID}}</span>
64
+ <span><strong>界面名称:</strong>{{NAME}}</span>
65
+ <span><strong>确认时间:</strong>{{DATE}}</span>
66
+ </div>
67
+ <div id="root"></div>
68
+
69
+ <script>
70
+ // UI 数据(内嵌)
71
+ const UI_DATA = {{JSON_DATA}};
72
+
73
+ // A2UI 渲染器
74
+ const { ConfigProvider, Card, Button, Input, Select, Table, Tabs, Tag, Badge, Space, Row, Col, Typography, Divider, Alert, Upload, Form } = antd;
75
+ const { Title, Text } = Typography;
76
+ const { TextArea } = Input;
77
+ const { PlusOutlined, InboxOutlined } = icons;
78
+ const { Dragger } = Upload;
79
+
80
+ const A2UIRenderer = ({ data }) => {
81
+ const renderNode = (node) => {
82
+ if (!node) return null;
83
+ const { type, children, ...props } = node;
84
+
85
+ switch (type) {
86
+ case 'Page': return React.createElement('div', { key: props.id }, props.title && React.createElement('div', { className: 'page-header' }, React.createElement('h1', { className: 'page-title' }, props.title)), children && children.map((child, i) => renderNode({ ...child, key: i })));
87
+ case 'Panel': return React.createElement(Card, { key: props.key, title: props.title, extra: props.extra && React.createElement(Space, null, props.extra.map((btn, i) => React.createElement(Button, { key: i, type: btn.variant === 'primary' ? 'primary' : 'default' }, btn.text || btn))), style: { marginBottom: 16 } }, children && children.map((child, i) => renderNode({ ...child, key: i })));
88
+ case 'Row': return React.createElement(Row, { key: props.key, gutter: 16 }, children && children.map((child, i) => renderNode({ ...child, key: i })));
89
+ case 'Col': return React.createElement(Col, { key: props.key, flex: 1 }, children && children.map((child, i) => renderNode({ ...child, key: i })));
90
+ case 'Input': return React.createElement(Form.Item, { key: props.key, label: props.label, required: props.required, style: { marginBottom: 16 } }, React.createElement(Input, { placeholder: props.placeholder }));
91
+ case 'Textarea': return React.createElement(Form.Item, { key: props.key, label: props.label, style: { marginBottom: 16 } }, React.createElement(TextArea, { placeholder: props.placeholder, rows: props.rows || 4 }));
92
+ case 'Select': return React.createElement(Form.Item, { key: props.key, label: props.label, style: { marginBottom: 16 } }, React.createElement(Select, { placeholder: '请选择', options: (props.options || []).map(opt => ({ value: typeof opt === 'string' ? opt : opt.value, label: typeof opt === 'string' ? opt : opt.label })), style: { width: '100%' } }));
93
+ case 'Button': return React.createElement(Button, { key: props.key, type: props.variant === 'secondary' ? 'default' : props.variant === 'danger' ? 'primary' : 'primary', danger: props.variant === 'danger', style: { marginRight: 8 } }, props.text);
94
+ case 'Text': return React.createElement(Text, { key: props.key, style: { display: 'block', marginBottom: 8 } }, props.content);
95
+ case 'Tabs': return React.createElement(Tabs, { key: props.key, items: (props.items || []).map((item, i) => ({ key: String(i), label: item })), style: { marginBottom: 16 } });
96
+ case 'Table':
97
+ const columns = (props.columns || []).map(col => {
98
+ const column = { key: col.key || col, dataIndex: col.key || col, title: col.title || col };
99
+ if (col.type === 'link') column.render = (text) => React.createElement('a', null, text);
100
+ else if (col.type === 'badge') column.render = (text) => React.createElement(Tag, { color: col.variantMap?.[text] === 'success' ? 'green' : col.variantMap?.[text] === 'warning' ? 'orange' : col.variantMap?.[text] === 'danger' ? 'red' : 'blue' }, text);
101
+ else if (col.type === 'status') column.render = (text) => React.createElement(Badge, { status: text === '已发布' ? 'success' : 'default', text });
102
+ else if (col.type === 'actions') column.render = () => React.createElement(Space, null, React.createElement('a', null, '编辑'), React.createElement('a', null, '复制'), React.createElement('a', { style: { color: '#ff4d4f' } }, '删除'));
103
+ return column;
104
+ });
105
+ return React.createElement(Table, { key: props.key, columns, dataSource: (props.data || []).map((row, i) => ({ ...row, key: i })), pagination: false, size: 'middle' });
106
+ case 'Badge': return React.createElement(Tag, { key: props.key, color: props.variant === 'success' ? 'green' : props.variant === 'warning' ? 'orange' : props.variant === 'danger' ? 'red' : 'blue' }, props.text);
107
+ case 'Card': return React.createElement(Card, { key: props.key, size: 'small', style: { marginBottom: 12 } }, React.createElement(Row, { justify: 'space-between', align: 'middle' }, React.createElement(Col, null, React.createElement(Space, { direction: 'vertical', size: 0 }, React.createElement(Text, { strong: true }, props.title), props.status && React.createElement(Badge, { status: props.status === '已发布' ? 'success' : 'default', text: props.status }))), props.actions && React.createElement(Col, null, React.createElement(Space, null, props.actions.map((action, i) => React.createElement(Button, { key: i, size: 'small' }, action.text || action))))));
108
+ case 'Upload': return React.createElement(Dragger, { key: props.key }, React.createElement('p', { className: 'ant-upload-drag-icon' }, React.createElement(InboxOutlined)), React.createElement('p', { className: 'ant-upload-text' }, props.text || '点击或拖拽文件上传'));
109
+ case 'Divider': return React.createElement(Divider, { key: props.key });
110
+ case 'Alert': return React.createElement(Alert, { key: props.key, type: props.variant === 'danger' ? 'error' : props.variant || 'info', message: props.content || props.text, showIcon: true, style: { marginBottom: 16 } });
111
+ case 'Diagram': return React.createElement('div', { key: props.key, style: { background: 'linear-gradient(135deg, #1677ff 0%, #722ed1 100%)', borderRadius: 8, padding: 32, minHeight: 300 } }, props.title && React.createElement('div', { style: { color: 'white', fontSize: 18, fontWeight: 600, textAlign: 'center', marginBottom: 24 } }, props.title), React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 } }, children && children.map((child, i) => renderNode({ ...child, key: i }))));
112
+ case 'Box': return React.createElement(Card, { key: props.key, size: 'small', style: { minWidth: 120, textAlign: 'center', borderLeft: props.color ? `3px solid ${props.color}` : undefined } }, React.createElement(Text, { strong: true }, props.title), props.desc && React.createElement('div', null, React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, props.desc)));
113
+ case 'Arrow': return React.createElement('div', { key: props.key, style: { color: 'white', fontSize: 24, textAlign: 'center' } }, (props.direction === 'up' ? '↑' : props.direction === 'left' ? '←' : props.direction === 'right' ? '→' : '↓'), props.label && React.createElement('span', { style: { fontSize: 12, marginLeft: 8 } }, props.label));
114
+ case 'Layer': return React.createElement('div', { key: props.key, style: { display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center', width: '100%' } }, props.title && React.createElement('div', { style: { width: '100%', textAlign: 'center', color: 'rgba(255,255,255,0.8)', fontSize: 12, marginBottom: 8 } }, props.title), children && children.map((child, i) => renderNode({ ...child, key: i })));
115
+ case 'DiagramGroup': return React.createElement('div', { key: props.key, style: { background: 'rgba(255,255,255,0.1)', border: '1px dashed rgba(255,255,255,0.3)', borderRadius: 8, padding: 16, width: '100%' } }, props.title && React.createElement('div', { style: { color: 'rgba(255,255,255,0.9)', fontSize: 14, marginBottom: 12 } }, props.title), React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center' } }, children && children.map((child, i) => renderNode({ ...child, key: i })));
116
+ default: return React.createElement(Alert, { key: props.key, type: 'warning', message: `未知组件: ${type}` });
117
+ }
118
+ };
119
+ return React.createElement(ConfigProvider, { theme: { token: { colorPrimary: '#1677ff', borderRadius: 6 } } }, renderNode(data));
120
+ };
121
+
122
+ const root = ReactDOM.createRoot(document.getElementById('root'));
123
+ root.render(React.createElement(A2UIRenderer, { data: UI_DATA }));
124
+ </script>
125
+ </body>
126
+
127
+ </html>
@@ -1,245 +0,0 @@
1
- ---
2
- description: C1 A2UI 界面示意专项指南 (Web Preview 版)
3
- ---
4
-
5
- # C1 A2UI 界面示意专项指南
6
-
7
- **本文档是 prd-c1-requirement-list.md 的补充,专注于界面示意环节的详细指导。**
8
-
9
- ---
10
-
11
- ## 🚀 新一代 A2UI 工作流 (Web Preview)
12
-
13
- 我们已经升级了 A2UI 能力!现在支持通过 `prd ui` 命令在浏览器中实时预览可交互的界面原型。
14
-
15
- ### 核心机制
16
-
17
- 1. **AI 生成数据**:AI 根据需求生成标准 JSON 数据。
18
- 2. **自动写入**:AI 将数据写入 `.a2ui/current.json` 文件。
19
- 3. **实时预览**:预览服务 (`prd ui`) 应在 C1 开始前启动,之后 PM 只需刷新浏览器即可看到更新。
20
-
21
- ---
22
-
23
- ## 🛠️ A2UI 标准组件库 (Schema)
24
-
25
- 为了确保 Web 预览器能正确渲染,AI **必须** 严格遵守以下 Schema。
26
-
27
- ### 1. 基础结构
28
- ```json
29
- {
30
- "type": "Page",
31
- "title": "页面标题",
32
- "children": [ ... ]
33
- }
34
- ```
35
-
36
- ### 2. 布局组件
37
- - **Page**: 根节点
38
- - `title`: 页面标题 (string)
39
- - `children`: 子组件数组
40
- - **Row**: 水平布局
41
- - `children`: 子组件数组
42
- - **Col**: 垂直/列布局
43
- - `children`: 子组件数组
44
- - **Panel**: 带边框的面板
45
- - `title`: 面板标题 (可选)
46
- - `children`: 子组件数组
47
-
48
- ### 3. 表单与交互组件
49
- - **Input**: 输入框
50
- - `label`: 标签 (string)
51
- - `placeholder`: 占位符 (string)
52
- - **Button**: 按钮
53
- - `text`: 按钮文字 (string)
54
- - **Text**: 纯文本
55
- - `content`: 文本内容 (string)
56
-
57
- ### 4. 架构图组件 (用于 B1/P0 阶段)
58
-
59
- **适用于规划阶段生成系统框架图、模块关系图等**
60
-
61
- - **Diagram**: 架构图容器(紫色渐变背景)
62
- - `title`: 图表标题 (string)
63
- - `children`: 子组件数组
64
- - **Layer**: 层级分区(水平排列子元素)
65
- - `title`: 层级标题 (string, 可选)
66
- - `children`: 子组件数组
67
- - **DiagramGroup**: 虚线分组框
68
- - `title`: 分组标题 (string)
69
- - `children`: 子组件数组
70
- - **Box**: 模块方框
71
- - `title`: 模块名称 (string)
72
- - `desc`: 模块描述 (string, 可选)
73
- - `color`: 左边框颜色 (string, 可选, 如 "#3b82f6")
74
- - **Arrow**: 连接箭头
75
- - `direction`: 方向 ("up" | "down" | "left" | "right")
76
- - `label`: 箭头说明 (string, 可选)
77
-
78
- ---
79
-
80
- ## 📝 交互流程规范
81
-
82
- ### 第一步:生成并写入预览数据
83
-
84
- **当 PM 描述完界面需求后,AI 必须执行:**
85
-
86
- 1. **构思 JSON 结构**:在思维链中构建符合 Schema 的 JSON。
87
- 2. **写入文件**:使用 `write_to_file` 工具将 JSON 写入 `.a2ui/current.json`。
88
- 3. **提示预览**:告知 PM 刷新浏览器查看(假设预览服务已启动)。
89
-
90
- **AI 对话示例:**
91
-
92
- ```
93
- AI: "明白了,这个用户详情页需要包含基本信息表单和操作按钮。
94
-
95
- 我已生成 A2UI 预览数据。
96
-
97
- 👉 请刷新浏览器 (http://localhost:3333) 查看实时预览。
98
-
99
- (如果预览服务未启动,请先运行 prd ui)"
100
- ```
101
-
102
- ### 第二步:根据反馈迭代
103
-
104
- **当 PM 提出修改意见(如"把按钮移到左边")时:**
105
-
106
- 1. **修改 JSON**:调整 `.a2ui/current.json` 中的数据结构。
107
- 2. **覆盖写入**:再次使用 `write_to_file` 覆盖原文件。
108
- 3. **提示刷新**:告知 PM 刷新浏览器即可看到变化。
109
-
110
- ### 第三步:正式保存(PM 确认后必须执行!)
111
-
112
- **当 PM 确认界面原型后,AI 必须将其保存到正式目录,作为交付物给开发参考。**
113
-
114
- #### 保存位置
115
-
116
- ```
117
- 02_迭代记录/第XX轮迭代/C1_UI原型/
118
- ├── REQ-001-登录页.html ← 👁️ 开发双击直接看界面
119
- ├── REQ-001-登录页.json ← 📄 数据结构
120
- ├── REQ-002-首页看板.html
121
- ├── REQ-002-首页看板.json
122
- └── index.md ← 📋 索引目录
123
- ```
124
-
125
- #### AI 保存流程
126
-
127
- ```
128
- 1. PM 确认:"这个界面可以了"
129
-
130
- 2. AI 询问需求编号:
131
- "好的,界面已确认。请告诉我这个界面对应的需求编号,如 REQ-001。"
132
-
133
- 3. PM 回答:"REQ-001"
134
-
135
- 4. AI 正式保存(必须生成 3 个文件):
136
- a) REQ-001-界面名称.json - JSON 数据文件
137
- b) REQ-001-界面名称.html - 独立 HTML 预览文件(内嵌渲染器和数据)
138
- c) 更新 index.md 索引文件
139
-
140
- 5. AI 确认:
141
- "✅ 界面原型已正式保存!
142
-
143
- 📁 文件位置:
144
- - 👁️ 预览: 02_迭代记录/第01轮迭代/C1_UI原型/REQ-001-登录页.html
145
- - 📄 数据: 02_迭代记录/第01轮迭代/C1_UI原型/REQ-001-登录页.json
146
-
147
- 开发可以双击 .html 文件直接在浏览器查看界面效果。"
148
- ```
149
-
150
- #### HTML 预览文件要求
151
-
152
- **AI 生成的 HTML 文件必须:**
153
- 1. 包含完整的 CSS 样式(复制 a2ui-viewer 的样式)
154
- 2. 包含完整的渲染引擎 JS 代码
155
- 3. 内嵌 JSON 数据(不依赖外部文件)
156
- 4. 顶部显示需求编号、界面名称、确认时间
157
- 5. 可以脱离服务器,双击直接在浏览器打开
158
-
159
- #### index.md 索引文件格式
160
-
161
- ```markdown
162
- # C1 UI 原型索引
163
-
164
- | 需求编号 | 界面名称 | 👁️ 预览 | 📄 数据 | 确认时间 |
165
- |---------|---------|--------|--------|---------|
166
- | REQ-001 | 登录页 | [.html](./REQ-001-登录页.html) | [.json](./REQ-001-登录页.json) | 2025-12-27 |
167
- ```
168
-
169
- #### 在 Markdown 文档中嵌入预览
170
-
171
- **AI 可以在 B1/B2/C1 文档中直接嵌入 HTML 预览,让用户无需切换文件夹:**
172
-
173
- ```markdown
174
- ## 系统架构图
175
-
176
- <iframe src="./架构图/系统架构.html" width="100%" height="400" frameborder="0"></iframe>
177
-
178
- > 💡 如果 iframe 无法显示,请直接打开 [系统架构.html](./架构图/系统架构.html)
179
- ```
180
-
181
- **注意**:不是所有 Markdown 编辑器都支持 iframe。如果不支持,AI 应该:
182
- 1. 提供 HTML 文件的链接
183
- 2. 或者将关键内容以 ASCII 图的形式展示在 Markdown 中
184
-
185
- ---
186
-
187
- ## 💻 完整示例:用户注册页
188
-
189
- ### 场景描述
190
- PM: "做一个简单的注册页,要有用户名、密码,下面是注册按钮,右边放个帮助说明。"
191
-
192
- ### AI 生成的 JSON (写入 .a2ui/current.json)
193
-
194
- ```json
195
- {
196
- "type": "Page",
197
- "title": "用户注册",
198
- "children": [
199
- {
200
- "type": "Row",
201
- "children": [
202
- {
203
- "type": "Col",
204
- "children": [
205
- {
206
- "type": "Panel",
207
- "title": "填写账户信息",
208
- "children": [
209
- { "type": "Input", "label": "用户名", "placeholder": "请输入用户名" },
210
- { "type": "Input", "label": "密码", "placeholder": "请输入密码" },
211
- { "type": "Button", "text": "立即注册" }
212
- ]
213
- }
214
- ]
215
- },
216
- {
217
- "type": "Col",
218
- "children": [
219
- {
220
- "type": "Panel",
221
- "title": "帮助中心",
222
- "children": [
223
- { "type": "Text", "content": "密码必须包含字母和数字。" },
224
- { "type": "Text", "content": "遇到问题请联系客服。" }
225
- ]
226
- }
227
- ]
228
- }
229
- ]
230
- }
231
- ]
232
- }
233
- ```
234
-
235
- ---
236
-
237
- ## 🚫 AI 避坑指南
238
-
239
- 1. **不要编造组件**:只能使用 `Page`, `Panel`, `Row`, `Col`, `Input`, `Button`, `Text`。不要用 `Table` (暂不支持), `Chart` 等。
240
- 2. **不要忘记写入**:必须调用 `write_to_file`,否则 `prd ui` 读不到数据。
241
- 3. **不要只给 JSON**:生成 JSON 的同时,最好也简单描述一下布局,照顾无法运行命令的场景。
242
-
243
- ---
244
-
245
- **参考**:C1 主流程请查看 `prd-c1-requirement-list.md`