hookehuyr

feat: 添加 Speckit 相关配置与实现文件

新增 Speckit 工具链相关文件,包括:
- 添加 specs/.gitkeep 文件
- 在 package.json 中添加 mcp:speckit 脚本
- 添加 .github/prompts 下的多个 prompt 文件
- 添加 .specify/memory/constitution.md 项目原则文件
- 添加 src/docs 下的 Speckit 测试文档和硬核模板
- 实现 mcp/speckit_stdio_server.js 服务端逻辑
1 +---
2 +description: "生成/更新项目原则(constitution)"
3 +agent: agent
4 +---
5 +
6 +请根据当前仓库的技术栈与业务场景,生成或更新 .specify/memory/constitution.md:
7 +
8 +- 内容使用中文
9 +- 原则需要覆盖:工程规范、UI/样式规范、网络请求返回结构、资源链接规则、开发与发布约束
10 +- 原则要具体可执行,避免空泛口号
11 +- 如果已存在同名文件,保留现有有效条款,只补齐缺失部分并做结构化整理
1 +---
2 +description: "按任务实现并自检(implement)"
3 +agent: agent
4 +argument-hint: "feature_key(例如 001-xxx)"
5 +---
6 +
7 +你需要按 specs/<feature_key>/tasks.md 逐条完成实现,并遵循:
8 +
9 +- 严格对照 specs/<feature_key>/spec.md 的验收标准
10 +- 修改尽量小,避免无关重构
11 +- 接口返回结构固定为 { code, data, msg },错误提示使用 msg
12 +- 图片域名为 cdn.ipadbiz.cn 时追加 ?imageMogr2/thumbnail/200x/strip/quality/70
13 +- 完成后运行现有的测试/检查脚本(如果仓库提供)
14 +- 不要启动或重启任何服务;不要引导打开预览
1 +---
2 +description: "生成技术方案(plan)"
3 +agent: agent
4 +argument-hint: "feature_key(例如 001-xxx)"
5 +---
6 +
7 +基于 specs/<feature_key>/spec.md,生成 specs/<feature_key>/plan.md。
8 +
9 +约束:
10 +- 给出最小改动方案,优先复用现有组件、工具与目录结构
11 +- 明确需要修改/新增的文件清单(按路径列出)
12 +- 明确数据流、状态管理方式(如需要 pinia/route query/组件本地状态)
13 +- 明确接口调用点与返回结构 { code, data, msg } 的处理策略
14 +- 样式:优先 TailwindCSS 类名(项目已引入),Less 用于层级补充
15 +- 不要启动或重启任何服务;不要引导打开预览
16 +
17 +plan.md 输出结构:
18 +1) 总体方案
19 +2) 页面与路由变更
20 +3) 组件与复用点
21 +4) 状态管理与数据流
22 +5) 接口与错误处理
23 +6) 样式与响应式策略
24 +7) 影响范围与回归清单
1 +---
2 +description: "生成需求规格说明(spec)"
3 +agent: agent
4 +argument-hint: "feature_key 与需求描述(例如 001-xxx ...)"
5 +---
6 +
7 +你需要把用户的需求整理成可评审的规格说明,输出到 specs/<feature_key>/spec.md。
8 +
9 +约束:
10 +- 只写“做什么/为什么”,不要写实现细节
11 +- 不确定的信息必须写成 [NEEDS CLARIFICATION],并给出具体要问的问题
12 +- 规格说明使用中文,结构清晰,便于团队协作评审
13 +- feature_key 使用数字前缀与短横线命名,例如 001-course-share-poster
14 +
15 +spec.md 模板:
16 +
17 +1) 背景与目标
18 +- 背景:
19 +- 目标:
20 +- 非目标:
21 +
22 +2) 用户画像与使用场景
23 +
24 +3) 需求范围
25 +- 页面/入口:
26 +- 核心流程:
27 +- 状态与异常分支:
28 +
29 +4) 用户故事(User Stories)
30 +- 作为……我想……以便……
31 +
32 +5) 验收标准(Acceptance Criteria)
33 +- 使用可验证的条件描述
34 +
35 +6) 交互与视觉要点
36 +- 移动端优先(如涉及 PC/移动差异需要说明)
37 +
38 +7) 数据与接口
39 +- 需要的字段(仅描述数据含义,不给实现)
40 +- 返回结构固定为 { code, data, msg }
41 +
42 +8) 边界条件与风险
43 +
44 +9) 待确认事项
45 +- [NEEDS CLARIFICATION] ...
1 +---
2 +description: "从方案拆分可执行任务(tasks)"
3 +agent: agent
4 +argument-hint: "feature_key(例如 001-xxx)"
5 +---
6 +
7 +基于 specs/<feature_key>/plan.md,生成 specs/<feature_key>/tasks.md。
8 +
9 +约束:
10 +- 任务要可执行、可验收,按顺序列出
11 +- 每条任务不超过 14 个字
12 +- 标注必要的回归点
13 +- 不要包含“启动服务/打开预览”相关任务
14 +
15 +tasks.md 格式:
16 +- [ ] 任务1
17 +- [ ] 任务2
18 +
19 +回归清单:
20 +- [ ] 回归项1
21 +- [ ] 回归项2
1 +## 项目原则
2 +
3 +- 以用户需求为中心,优先实现最小可用闭环,避免过度设计
4 +- 修改应尽量小且可回滚,避免大范围重构
5 +- 保持现有代码风格与目录结构一致,优先复用已有工具与组件
6 +- 生产代码完毕后不要重启服务
7 +- 操作完成后不要自动打开预览
8 +
9 +## 技术与工程约束
10 +
11 +- 依赖管理使用 pnpm
12 +- 构建工具使用 Vite
13 +- 前端使用 Vue3(优先 setup 语法糖)
14 +- 移动端 UI 优先使用 Vant
15 +- 样式使用 Less;如果项目已引入 TailwindCSS,布局优先用 Tailwind 类名,细节用 Less 补充
16 +- 路由使用 vue-router
17 +- 状态管理使用 pinia(如项目已接入)
18 +- 不引入不必要的新库;如必须新增依赖,需要在方案中说明原因与替代方案
19 +
20 +## 接口与数据约束
21 +
22 +- 所有网络请求返回对象结构必须为 { code, data, msg }
23 +- code=1 表示成功,其他值表示失败;失败时保持 msg 可读
24 +
25 +## 资源与链接约束
26 +
27 +- 若图片域名为 cdn.ipadbiz.cn,需要自动追加 ?imageMogr2/thumbnail/200x/strip/quality/70
28 +
1 +import { spawn } from 'node:child_process'
2 +import path from 'node:path'
3 +import { fileURLToPath } from 'node:url'
4 +
5 +const PROTOCOL_VERSIONS = ['2024-11-05', '2025-06-18']
6 +
7 +const server_info = {
8 + name: 'speckit-cli-mcp',
9 + title: 'Speckit CLI MCP Server',
10 + version: '0.1.0',
11 +}
12 +
13 +const module_dirname = path.dirname(fileURLToPath(import.meta.url))
14 +const workspace_root = path.resolve(module_dirname, '..')
15 +
16 +process.stderr.write(`[speckit-cli-mcp] cwd=${process.cwd()} workspace_root=${workspace_root}\n`)
17 +
18 +const send_json = (payload) => {
19 + process.stdout.write(`${JSON.stringify(payload)}\n`)
20 +}
21 +
22 +const send_result = (id, result) => {
23 + send_json({ jsonrpc: '2.0', id, result })
24 +}
25 +
26 +const send_error = (id, code, message, data) => {
27 + const error = { code, message }
28 + if (data !== undefined) {
29 + error.data = data
30 + }
31 + send_json({ jsonrpc: '2.0', id, error })
32 +}
33 +
34 +const as_text_content = (obj) => {
35 + return {
36 + content: [
37 + {
38 + type: 'text',
39 + text: JSON.stringify(obj, null, 4),
40 + },
41 + ],
42 + isError: obj.code !== 1,
43 + }
44 +}
45 +
46 +const normalize_subcommand = (subcommand) => {
47 + if (typeof subcommand !== 'string') {
48 + return null
49 + }
50 + const trimmed = subcommand.trim()
51 + if (!trimmed) {
52 + return null
53 + }
54 + return trimmed
55 +}
56 +
57 +const validate_args = (args) => {
58 + if (args === undefined) {
59 + return { ok: true, value: [] }
60 + }
61 + if (!Array.isArray(args)) {
62 + return { ok: false, message: 'args 必须为字符串数组' }
63 + }
64 + for (const item of args) {
65 + if (typeof item !== 'string') {
66 + return { ok: false, message: 'args 必须为字符串数组' }
67 + }
68 + if (item.length > 200) {
69 + return { ok: false, message: 'args 单项长度不能超过 200' }
70 + }
71 + }
72 + return { ok: true, value: args }
73 +}
74 +
75 +const resolve_cwd = (cwd_input) => {
76 + if (!cwd_input) {
77 + return workspace_root
78 + }
79 + if (typeof cwd_input !== 'string') {
80 + return null
81 + }
82 + const resolved = path.resolve(workspace_root, cwd_input)
83 + if (!resolved.startsWith(workspace_root)) {
84 + return null
85 + }
86 + return resolved
87 +}
88 +
89 +const run_specify = async ({ cli, subcommand, args, cwd, timeout_ms }) => {
90 + const cli_bin = cli === 'speckit' ? 'speckit' : 'specify'
91 + const allowed_subcommands = new Set(['check', 'init'])
92 + const normalized_subcommand = normalize_subcommand(subcommand)
93 + if (!normalized_subcommand || !allowed_subcommands.has(normalized_subcommand)) {
94 + return { code: 0, data: null, msg: `不支持的子命令:${subcommand}` }
95 + }
96 +
97 + const validated_args = validate_args(args)
98 + if (!validated_args.ok) {
99 + return { code: 0, data: null, msg: validated_args.message }
100 + }
101 +
102 + const resolved_cwd = resolve_cwd(cwd)
103 + if (!resolved_cwd) {
104 + return { code: 0, data: null, msg: 'cwd 非法,必须为工作区内的相对路径' }
105 + }
106 +
107 + const final_timeout_ms = Number.isFinite(timeout_ms) ? Math.max(1000, timeout_ms) : 10 * 60 * 1000
108 + const final_args = [normalized_subcommand, ...validated_args.value]
109 +
110 + return await new Promise((resolve) => {
111 + const child = spawn(cli_bin, final_args, {
112 + cwd: resolved_cwd,
113 + env: process.env,
114 + })
115 +
116 + let stdout = ''
117 + let stderr = ''
118 + let timed_out = false
119 +
120 + const timer = setTimeout(() => {
121 + timed_out = true
122 + child.kill('SIGTERM')
123 + }, final_timeout_ms)
124 +
125 + child.stdout.on('data', (chunk) => {
126 + stdout += chunk.toString()
127 + if (stdout.length > 2_000_000) {
128 + stdout = `${stdout.slice(0, 2_000_000)}\n...[stdout 截断]`
129 + }
130 + })
131 +
132 + child.stderr.on('data', (chunk) => {
133 + stderr += chunk.toString()
134 + if (stderr.length > 2_000_000) {
135 + stderr = `${stderr.slice(0, 2_000_000)}\n...[stderr 截断]`
136 + }
137 + })
138 +
139 + child.on('error', (err) => {
140 + clearTimeout(timer)
141 + resolve({
142 + code: 0,
143 + data: {
144 + cwd: resolved_cwd,
145 + cli: cli_bin,
146 + args: final_args,
147 + stdout,
148 + stderr,
149 + error: String(err?.message || err),
150 + },
151 + msg: '命令启动失败(请确认已安装 specify/speckit)',
152 + })
153 + })
154 +
155 + child.on('close', (exit_code, signal) => {
156 + clearTimeout(timer)
157 + resolve({
158 + code: exit_code === 0 && !timed_out ? 1 : 0,
159 + data: {
160 + cwd: resolved_cwd,
161 + cli: cli_bin,
162 + args: final_args,
163 + exit_code,
164 + signal,
165 + timed_out,
166 + stdout,
167 + stderr,
168 + },
169 + msg: exit_code === 0 && !timed_out ? 'ok' : '命令执行失败',
170 + })
171 + })
172 + })
173 +}
174 +
175 +const list_tools = () => {
176 + return [
177 + {
178 + name: 'speckit_check',
179 + description: '运行 specify check(检查工具链是否齐全)',
180 + inputSchema: {
181 + type: 'object',
182 + properties: {
183 + cli: { type: 'string', enum: ['specify', 'speckit'], default: 'specify' },
184 + cwd: { type: 'string', description: '相对工作区路径(可选)' },
185 + timeout_ms: { type: 'number', description: '超时时间(毫秒,可选)' },
186 + },
187 + required: [],
188 + },
189 + },
190 + {
191 + name: 'speckit_init_here',
192 + description: '运行 specify init --here(初始化/更新 Spec Kit 文件)',
193 + inputSchema: {
194 + type: 'object',
195 + properties: {
196 + cli: { type: 'string', enum: ['specify', 'speckit'], default: 'specify' },
197 + ai: { type: 'string', default: 'copilot' },
198 + force: { type: 'boolean', default: false },
199 + cwd: { type: 'string', description: '相对工作区路径(可选)' },
200 + timeout_ms: { type: 'number', description: '超时时间(毫秒,可选)' },
201 + },
202 + required: [],
203 + },
204 + },
205 + {
206 + name: 'speckit_run',
207 + description: '运行 allowlist 内的 specify 子命令(check/init)',
208 + inputSchema: {
209 + type: 'object',
210 + properties: {
211 + cli: { type: 'string', enum: ['specify', 'speckit'], default: 'specify' },
212 + subcommand: { type: 'string', enum: ['check', 'init'] },
213 + args: { type: 'array', items: { type: 'string' } },
214 + cwd: { type: 'string', description: '相对工作区路径(可选)' },
215 + timeout_ms: { type: 'number', description: '超时时间(毫秒,可选)' },
216 + },
217 + required: ['subcommand'],
218 + },
219 + },
220 + ]
221 +}
222 +
223 +const handle_request = async (msg) => {
224 + const { id, method, params } = msg || {}
225 +
226 + if (method === 'initialize') {
227 + const requested = params?.protocolVersion
228 + const negotiated = PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0]
229 + send_result(id, {
230 + protocolVersion: negotiated,
231 + capabilities: {
232 + tools: { listChanged: false },
233 + },
234 + serverInfo: server_info,
235 + instructions: '仅提供 Speckit/Spec Kit CLI 的最小封装工具(check/init)。',
236 + })
237 + return
238 + }
239 +
240 + if (method === 'ping') {
241 + send_result(id, {})
242 + return
243 + }
244 +
245 + if (method === 'tools/list') {
246 + send_result(id, { tools: list_tools() })
247 + return
248 + }
249 +
250 + if (method === 'tools/call') {
251 + const name = params?.name
252 + const args = params?.arguments || {}
253 +
254 + if (name === 'speckit_check') {
255 + const res = await run_specify({
256 + cli: args.cli,
257 + subcommand: 'check',
258 + args: [],
259 + cwd: args.cwd,
260 + timeout_ms: args.timeout_ms,
261 + })
262 + send_result(id, as_text_content(res))
263 + return
264 + }
265 +
266 + if (name === 'speckit_init_here') {
267 + const force_flag = args.force ? ['--force'] : []
268 + const ai_value = typeof args.ai === 'string' && args.ai.trim() ? args.ai.trim() : 'copilot'
269 + const res = await run_specify({
270 + cli: args.cli,
271 + subcommand: 'init',
272 + args: ['--here', '--ai', ai_value, ...force_flag],
273 + cwd: args.cwd,
274 + timeout_ms: args.timeout_ms,
275 + })
276 + send_result(id, as_text_content(res))
277 + return
278 + }
279 +
280 + if (name === 'speckit_run') {
281 + const res = await run_specify({
282 + cli: args.cli,
283 + subcommand: args.subcommand,
284 + args: args.args,
285 + cwd: args.cwd,
286 + timeout_ms: args.timeout_ms,
287 + })
288 + send_result(id, as_text_content(res))
289 + return
290 + }
291 +
292 + send_error(id, -32602, `Unknown tool: ${name}`)
293 + return
294 + }
295 +
296 + if (id !== undefined) {
297 + send_error(id, -32601, `Method not found: ${method}`)
298 + }
299 +}
300 +
301 +let buffer = ''
302 +process.stdin.setEncoding('utf8')
303 +process.stdin.on('data', async (chunk) => {
304 + buffer += chunk
305 + while (buffer.indexOf('\n') !== -1) {
306 + const idx = buffer.indexOf('\n')
307 + const line = buffer.slice(0, idx).trim()
308 + buffer = buffer.slice(idx + 1)
309 + if (!line) {
310 + continue
311 + }
312 + let msg
313 + try {
314 + msg = JSON.parse(line)
315 + } catch (e) {
316 + continue
317 + }
318 + await handle_request(msg)
319 + }
320 +})
321 +
322 +process.stdin.on('end', () => {
323 + process.exit(0)
324 +})
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
8 "build": ". ~/.nvm/nvm.sh && nvm use 18.19.1 && vite build", 8 "build": ". ~/.nvm/nvm.sh && nvm use 18.19.1 && vite build",
9 "preview": "vite preview", 9 "preview": "vite preview",
10 "test": "vitest run", 10 "test": "vitest run",
11 + "mcp:speckit": "node mcp/speckit_stdio_server.js",
11 "tar": "tar -czvpf dist.tar.gz mlaj", 12 "tar": "tar -czvpf dist.tar.gz mlaj",
12 "build_tar": "npm run build && npm run tar", 13 "build_tar": "npm run build && npm run tar",
13 "scp-dev": "scp dist.tar.gz ipadbiz-inner:/opt/space-dev/f", 14 "scp-dev": "scp dist.tar.gz ipadbiz-inner:/opt/space-dev/f",
......
1 +# 001-recall-activity-history-state - 活动历史页空状态与加载失败提示
2 +
3 +## 1) 背景与目标
4 +
5 +- 背景:
6 + - 当前“活动历史”页面会在进入时请求活动列表数据并渲染列表。
7 + - 当接口返回无数据或请求失败时,页面缺少清晰的页面内反馈,用户难以判断是“没有活动记录”还是“加载失败”。
8 +- 目标:
9 + - 在不改接口、不新增接口的前提下,为活动历史页补齐“空状态提示”和“加载失败提示 + 重试入口”,提升可理解性与可恢复性。
10 +- 非目标:
11 + - 不调整接口入参、返回结构或后端逻辑。
12 + - 不改动页面既有的顶部 Banner、底部按钮与“没找到我的星球活动”弹窗等核心结构与入口。
13 + - 不引入新的页面路由与新的业务流程。
14 +
15 +## 2) 用户画像与使用场景
16 +
17 +- 已登录/已完成必要信息的用户,通过召回链路进入活动历史页,查看历史参与记录。
18 +- 用户可能出现以下情况:
19 + - 确实没有参与过活动(应看到“暂无活动记录”的空状态)。
20 + - 网络不稳定、服务异常、参数缺失导致请求失败(应看到“加载失败”的错误提示,并可重试)。
21 +
22 +## 3) 需求范围
23 +
24 +- 页面/入口:
25 + - 活动历史页(现有页面入口保持不变)。
26 +- 核心流程:
27 + - 页面进入后发起列表加载。
28 + - 加载成功:
29 + - 有数据:展示列表。
30 + - 无数据:展示空状态。
31 + - 加载失败:展示失败提示与重试按钮。
32 +- 状态与异常分支:
33 + - 请求失败(返回 code 不为 1 或发生异常)时进入失败状态。
34 + - 重试成功后根据数据展示列表或空状态。
35 +
36 +## 4) 用户故事(User Stories)
37 +
38 +- 作为用户,我想在没有活动记录时看到明确提示,以便确认自己没有缺失记录。
39 +- 作为用户,我想在列表加载失败时看到错误提示并能重试,以便在网络恢复后继续查看记录。
40 +
41 +## 5) 验收标准(Acceptance Criteria)
42 +
43 +- 有数据时:
44 + - 页面展示活动列表。
45 + - 不展示空状态与失败状态提示。
46 +- 无数据时:
47 + - 当列表加载已完成且列表为空时,页面在列表区域展示空状态提示文案(例如“暂无活动记录”)。
48 + - 空状态为页面内可见提示,不以 Toast 作为唯一反馈。
49 +- 加载失败时:
50 + - 当列表请求失败(code!=1 或发生异常)时,页面在列表区域展示失败状态提示文案(例如“加载失败,请稍后重试”)。
51 + - 失败提示区域提供“重试”按钮;点击后会重新发起列表请求。
52 + - 重试成功后:
53 + - 有数据则展示列表;
54 + - 无数据则展示空状态;
55 + - 不再展示失败提示。
56 +- 结构与入口:
57 + - 顶部 Banner、底部按钮、“没找到我的星球活动”入口与弹窗行为不受影响,保持可用。
58 +
59 +## 6) 交互与视觉要点
60 +
61 +- 空状态与失败状态展示位置:
62 + - 在列表区域内展示,避免遮挡顶部 Banner 和底部固定按钮。
63 +- 可读性:
64 + - 文案简洁、明确区分“无数据”与“加载失败”。
65 +- 一致性:
66 + - 视觉风格与页面现有样式保持一致(背景、圆角卡片、间距等)。
67 +
68 +## 7) 数据与接口
69 +
70 +- 不新增接口、不修改接口。
71 +- 数据来源仍为现有活动列表查询接口。
72 +- 返回结构固定为 { code, data, msg }:
73 + - code=1 视为成功;
74 + - code!=1 视为失败;
75 + - msg 可作为失败提示文案来源之一(若为空则使用默认失败文案)。
76 +
77 +## 8) 边界条件与风险
78 +
79 +- 缓存参数缺失或不完整可能导致请求失败,应进入失败状态并可重试。
80 +- 网络波动可能导致首次失败、重试成功,状态切换需要正确更新页面展示。
81 +
82 +## 9) 待确认事项
83 +
84 +- [NEEDS CLARIFICATION] 失败状态文案是否需要优先展示接口返回 msg?若 msg 为空是否统一使用默认文案?
85 +- [NEEDS CLARIFICATION] 空状态文案是否固定为“暂无活动记录”,还是需要根据场景区分(例如“未查询到您的历史活动”)?
1 +## 步骤 0:选一个 feature_key
2 +建议用:
3 +
4 +- 001-recall-activity-history-state
5 +## 步骤 1:/speckit.specify(生成 spec)
6 +在 Trae 的 Copilot Chat 输入:
7 +
8 +```
9 +/speckit.specify 
10 +001-recall-activity-history-state 
11 +ActivityHistoryPage 增加一个空状态提示 + 列表加
12 +载失败提示(不改接口)
13 +
14 +背景:
15 +- 页面文件:src/views/recall/
16 +ActivityHistoryPage.vue
17 +- 当前页面仅在 onMounted 调用 
18 +searchOldActivityAPI 获取 activities,没有 
19 +loading/empty/error 视图
20 +- 接口返回结构为 { code, data, msg },不修改接口
21 +与入参
22 +
23 +需求:
24 +1) 空状态:当活动列表加载完成且无数据
25 +(activities 为空)时,展示页面内空状态提示(不要 
26 +toast 代替)。
27 +2) 加载失败:当 searchOldActivityAPI 请求失败
28 +(code!=1 或抛异常)时,展示页面内错误提示,并提供
29 +“重试”按钮;点击重试会重新发起列表请求。
30 +3) 不影响页面现有结构:顶部 Banner、底部按钮、"没
31 +找到我的星球活动" 弹窗等保持不变。
32 +4) 样式:项目已引入 TailwindCSS,布局优先用 
33 +Tailwind 类;必要补充用 Less(保持现有文件风
34 +格)。
35 +5) 不新增接口、不改接口返回结构。
36 +验收:
37 +- 正常有数据:列表正常展示,不显示空/错。
38 +- 无数据:显示空状态文案(建议:暂无活动记录)。
39 +- 请求失败:显示失败状态文案(建议:加载失败,请稍后
40 +重试),点击重试后如果成功则显示列表/空状态。
41 +```
42 +预期产物:
43 +
44 +- specs/001-recall-activity-history-state/spec.md
45 +## 步骤 2:处理 [NEEDS CLARIFICATION](如果 Speckit 问你)
46 +它可能会问这些,你直接用下面“推荐回答”贴回去:
47 +
48 +- Q:失败判断是什么? A: searchOldActivityAPI 返回 code!==1 或请求抛异常都视为失败。
49 +- Q:失败状态是否要展示 msg? A:优先展示接口 msg (若为空则用“加载失败,请稍后重试”)。
50 +- Q:空状态是整页替换还是列表区域内? A:在“列表区域”显示空状态,不遮挡顶部 Banner 与底部按钮。
51 +- Q:是否需要 loading 骨架/加载中? A:本次只做空状态与失败提示,不额外做骨架;但允许加一个轻量 loading 状态避免闪烁(可选,不作为验收点)。
52 +## 步骤 3:/speckit.plan(生成 plan)
53 +在 Copilot Chat 输入:
54 +
55 +```
56 +/speckit.plan 
57 +001-recall-activity-history-state
58 +
59 +技术约束:
60 +- Vue3 <script setup>,不引入新库
61 +- 仅修改 ActivityHistoryPage.vue(如确有必要允
62 +许新增一个小的复用组件,但优先不新增文件)
63 +- 不启动/不重启服务,不打开预览
64 +```
65 +预期产物:
66 +
67 +- specs/001-recall-activity-history-state/plan.md
68 +## 步骤 4:/speckit.tasks(生成 tasks)
69 +在 Copilot Chat 输入:
70 +
71 +```
72 +/speckit.tasks 
73 +001-recall-activity-history-state
74 +```
75 +预期产物:
76 +
77 +- specs/001-recall-activity-history-state/tasks.md
78 +## 步骤 5:/speckit.implement(按任务落地改代码)
79 +在 Copilot Chat 输入:
80 +
81 +```
82 +/speckit.implement 
83 +001-recall-activity-history-state
84 +```
85 +你可以在实现阶段额外强调一句(避免它越界):
86 +
87 +- “只改 ActivityHistoryPage.vue,不要改接口,不要新增服务端逻辑,不要打开预览,不要重启任何服务。”
88 +## 步骤 6:自检(不启动服务)
89 +实现完成后只跑已有测试即可:
90 +
91 +```
92 +pnpm test
93 +```
94 +## 你改动点会落在哪里(便于你 review)
95 +Speckit 最终大概率会在这个文件里做这些事(都不改接口):
96 +
97 +- ActivityHistoryPage.vue
98 + - 增加 loading / load_error / error_msg 之类的状态
99 + - 把 onMounted 里的拉取逻辑抽成 fetch_activities (带 try/catch)
100 + - 模板里在 List 区域根据状态展示:
101 + - 列表
102 + - 空状态
103 + - 失败状态 + 重试按钮
104 +如果你愿意,我也可以直接给你一份“更硬核的 /speckit.specify 文案”(把每条验收标准写成更可验证的条件),这样后面 plan/tasks/implement 的输出会更稳定。
1 +# 硬核 /speckit.specify 提示模板(可验证验收标准版)
2 +
3 +> 用途:帮助你在调用 `/speckit.specify` 时,把需求描述成**更可验证、更稳定**的规格说明,让后续 `/speckit.plan`、`/speckit.tasks`、`/speckit.implement` 的输出更可控。
4 +
5 +---
6 +
7 +## 一、使用方式总览
8 +
9 +- 在聊天里调用命令时,将本模板中的「完整提示文案」作为补充说明一起发给智能体,例如:
10 + - `/speckit.specify 001-recall-activity-history-state ActivityHistoryPage 增加一个空状态提示 + 列表加载失败提示(不改接口)`
11 + - 然后追加一段话:「请严格按照《硬核 /speckit.specify 提示模板(可验证验收标准版)》来生成 specs/001-recall-activity-history-state/spec.md」
12 +- 规格说明的目标:
13 + - 明确 **做什么 / 为什么做**,不写具体实现细节。
14 + - 每条验收标准都能被前端 / 后端 / 测试同事用「条件 + 操作 + 预期结果」直接验证。
15 + - 所有分支(成功 / 无数据 / 失败 / 边界情况)都有清晰的预期。
16 +
17 +---
18 +
19 +## 二、规格结构模板(spec.md 结构)
20 +
21 +输出文件固定为:`specs/<feature_key>/spec.md`,推荐结构如下:
22 +
23 +1. 背景与目标
24 + - 背景:说明业务场景、已有行为中的问题或机会。
25 + - 目标:本次改动要解决什么问题、带来什么价值。
26 + - 非目标:本次明确**不做**的范围(例如「不改接口、不动路由、不调整整体布局」)。
27 +
28 +2. 用户画像与使用场景
29 + - 谁在用(角色、平台、入口)。
30 + - 在什么场景下触发(来源页面、入口按钮、链路前后步骤)。
31 +
32 +3. 需求范围
33 + - 页面 / 入口:涉及到哪些页面、弹窗或组件。
34 + - 核心流程:从用户视角按步骤描述主流程。
35 + - 状态与异常分支:列出所有状态(正常、有数据、无数据、加载中、失败等)和切换条件。
36 +
37 +4. 用户故事(User Stories)
38 + - 使用「作为……我想……以便……」的句式,每个故事只描述一个明确目的。
39 +
40 +5. 验收标准(Acceptance Criteria)——**重点加强部分**
41 + - 使用「可验证条件」描述,每条至少包含:
42 + - 前置条件(环境、数据状态、接口返回等)。
43 + - 用户操作(点击、进入页面、刷新、重试等)。
44 + - 预期结果(UI 展示、状态变化、接口调用、副作用)。
45 + - 必要时补充「不允许出现的结果 / 行为」以减少歧义。
46 +
47 +6. 交互与视觉要点
48 + - 交互:点击反馈、加载状态、重试行为、动画等。
49 + - 视觉:布局位置、层级关系、是否与现有组件对齐。
50 + - 终端:默认移动端优先,如有 PC 差异需单独说明。
51 +
52 +7. 数据与接口
53 + - 数据字段:只描述字段含义,不写具体数据结构实现细节。
54 + - 接口约定:本项目约定所有接口返回结构均为 `{ code, data, msg }`
55 + - `code = 1` 表示成功;
56 + - `code ≠ 1` 表示失败;
57 + - `data` 为业务数据;
58 + - `msg` 为提示文案(前端可用作错误提示文本来源)。
59 +
60 +8. 边界条件与风险
61 + - 写出「极端但可能真实发生」的情况以及期望行为。
62 + - 对于不确定的行为,用自然语言描述当前的默认假设。
63 +
64 +9. 待确认事项
65 + - 所有不确定点都必须以 `[NEEDS CLARIFICATION]` 开头。
66 + - 每条都要写成一个具体的问题,便于后续和产品 / 业务沟通。
67 +
68 +---
69 +
70 +## 三、硬核验收标准书写规范
71 +
72 +编写验收标准时,建议将每条标准写成「**编号 + 条件 + 操作 + 预期结果**」的形式,示例如下:
73 +
74 +- AC1:正常有数据时展示列表
75 + - 条件:接口 `searchOldActivityAPI` 请求成功,返回 `code = 1``data.list` 为长度 ≥ 1 的数组。
76 + - 操作:用户进入 `ActivityHistoryPage`,页面完成首次数据加载。
77 + - 预期结果:
78 + - 列表区域展示所有活动记录卡片,条数等于 `data.list.length`
79 + - 不展示空状态文案、不展示失败状态或重试按钮。
80 + - 顶部 Banner、底部固定按钮等既有内容保持当前实现,不被遮挡或改变行为。
81 +
82 +- AC2:无数据时展示空状态
83 + - 条件:接口 `searchOldActivityAPI` 请求成功,返回 `code = 1`,但 `data.list` 为空数组或不存在可用记录。
84 + - 操作:用户进入 `ActivityHistoryPage`,页面完成首次数据加载。
85 + - 预期结果:
86 + - 列表原本展示卡片的位置,显示空状态区域(如「暂无活动记录」文案)。
87 + - 空状态展示在页面内容区域内,不通过 Toast 作为唯一反馈。
88 + - 不展示失败状态或重试按钮。
89 + - 顶部 Banner、底部按钮与其他入口可正常点击、滚动不受影响。
90 +
91 +- AC3:请求失败时展示失败状态 + 重试按钮
92 + - 条件:
93 + - 接口 `searchOldActivityAPI` 返回 `code ≠ 1`;或
94 + - 请求发生网络异常 / JS 异常导致无法获取数据。
95 + - 操作:用户进入 `ActivityHistoryPage`,触发列表加载。
96 + - 预期结果:
97 + - 列表区域显示失败状态区域,包含错误提示文案和「重试」按钮。
98 + - 提示文案优先使用接口返回的 `msg` 字段;若 `msg` 为空或不可用,则使用默认文案(例如「加载失败,请稍后重试」)。
99 + - 不展示有效的活动卡片,不展示空状态提示。
100 + - 顶部 Banner、底部按钮与其他入口仍可正常使用。
101 +
102 +- AC4:点击重试后的行为
103 + - 条件:页面当前处于失败状态,且显示重试按钮。
104 + - 操作:用户点击失败状态区域中的「重试」按钮。
105 + - 预期结果:
106 + - 重新发起一次 `searchOldActivityAPI` 请求,使用与首次请求相同的参数。
107 + - 在请求结果返回前,页面可选择短暂展示「加载中」状态或保持失败状态(需在规格中明确);无论哪种方式,都不得多次并发重复请求。
108 + - 重试成功且有数据:页面展示列表卡片,隐藏失败状态与重试按钮。
109 + - 重试成功但无数据:页面展示空状态,隐藏失败状态与重试按钮。
110 + - 重试仍然失败:保持或更新失败状态提示文案(可使用新的 `msg`),重试按钮仍可再次点击。
111 +
112 +- AC5:缓存或参数异常时的行为
113 + - 条件:本地缓存的查询参数缺失、格式异常或读取缓存时抛出异常。
114 + - 操作:用户正常进入 `ActivityHistoryPage`
115 + - 预期结果:
116 + - 页面不会出现崩溃或白屏。
117 + - 若无法构造有效请求参数,则进入失败状态,并展示失败提示与重试按钮。
118 + - 若重试按钮依赖重新读取缓存,则需要在规格中说明默认兜底行为(例如「使用默认时间范围重试」或「继续展示失败状态并提示用户稍后重试」)。
119 +
120 +编写其他页面的验收标准时,可参考上述形式,将核心场景拆分为 AC1 / AC2 / AC3...,每条都保证:
121 +
122 +- 可以通过**明确的前置条件**在测试环境中复现;
123 +- 可以通过**清晰的操作步骤**验证结果;
124 +- 可以通过**观察 UI 或接口行为**判断是否通过。
125 +
126 +---
127 +
128 +## 四、可直接复用的 /speckit.specify 提示文案
129 +
130 +当你在聊天中描述新需求时,可以在自然语言需求后追加如下提示,引导智能体按「硬核」方式输出规格说明:
131 +
132 +> 请将上述需求整理为 Spec Kit 规格说明,输出到 `specs/&lt;feature_key&gt;/spec.md`,并严格遵守以下要求:
133 +> 1. 使用本项目既有的 spec 结构:背景与目标、用户画像与使用场景、需求范围、用户故事、验收标准、交互与视觉要点、数据与接口、边界条件与风险、待确认事项。
134 +> 2. 所有说明使用中文,专注描述「做什么 / 为什么」,不要写实现细节(例如具体组件名、函数名、接口实现代码)。
135 +> 3. 在「验收标准」一节中,以 AC1 / AC2 / AC3... 的形式列出所有可验证的标准。每条必须包含:
136 +> - 条件:清晰的前置条件(数据状态、接口返回、环境等);
137 +> - 操作:用户在前端界面上的具体操作;
138 +> - 预期结果:界面展示、状态变化、接口调用或其他可观察行为;
139 +> - 如有必要,补充「不允许出现的结果」。
140 +> 4. 明确区分以下几类场景,并分别给出验收标准:
141 +> - 正常有数据时的行为;
142 +> - 无数据时的行为;
143 +> - 请求失败 / 异常时的行为;
144 +> - 点击重试或重新加载时的行为;
145 +> - 缓存缺失、参数异常等边界条件。
146 +> 5. 在「数据与接口」部分中,明确约定接口返回结构统一为 `{ code, data, msg }`:
147 +> - `code = 1` 视为成功,`code ≠ 1` 视为失败;
148 +> - `msg` 用作错误提示文案的主要来源;
149 +> - 若本次需求不允许改动接口,请在此处显式说明「不新增接口、不修改现有接口」。
150 +> 6. 所有不确定点必须写入「待确认事项」并使用 `[NEEDS CLARIFICATION]` 前缀,形式为可直接询问产品 / 业务同学的具体问题。
151 +> 7. 请避免在验收标准中使用模糊描述(例如「尽量」「可能」「大概」),改用可观测、可验证的具体条件(例如「当 X 时必须显示 Y 文案,不展示 Z」)。
152 +
153 +---
154 +
155 +## 五、示例:ActivityHistoryPage 空状态 + 失败提示(摘要示例)
156 +
157 +以下是基于 `ActivityHistoryPage` 需求的验收标准摘要示例,可作为编写类似功能规格时的参考:
158 +
159 +- AC1:有历史活动时
160 + - 条件:`searchOldActivityAPI` 返回 `code = 1``data.list` 长度 ≥ 1。
161 + - 操作:进入 `ActivityHistoryPage`,等待列表加载完成。
162 + - 预期结果:
163 + - 页面展示等同于 `data.list` 数量的活动卡片;
164 + - 不展示空状态和失败状态;
165 + - 头部 Banner、底部按钮和「没找到我的星球活动」入口可正常使用。
166 +
167 +- AC2:没有历史活动时
168 + - 条件:`searchOldActivityAPI` 返回 `code = 1``data.list` 为空数组或无可展示记录。
169 + - 操作:进入 `ActivityHistoryPage`,等待列表加载完成。
170 + - 预期结果:
171 + - 列表区域展示空状态文案(如「暂无活动记录」);
172 + - 不展示失败状态和重试按钮;
173 + - 其他区域交互行为与有数据时保持一致,不出现遮挡。
174 +
175 +- AC3:接口失败或异常时
176 + - 条件:`searchOldActivityAPI` 返回 `code ≠ 1`,或请求抛出异常。
177 + - 操作:进入 `ActivityHistoryPage`,触发列表加载。
178 + - 预期结果:
179 + - 列表区域展示失败状态文案,优先使用接口返回的 `msg`,否则使用默认文案(如「加载失败,请稍后重试」);
180 + - 同时展示「重试」按钮;
181 + - 不展示列表卡片和空状态;
182 + - 用户仍可操作顶部和底部区域。
183 +
184 +- AC4:点击重试
185 + - 条件:页面处于失败状态。
186 + - 操作:点击失败状态中的「重试」按钮。
187 + - 预期结果:
188 + - 重新调用 `searchOldActivityAPI`
189 + - 根据新结果切换到 AC1 或 AC2 描述的状态;
190 + - 若依旧失败,保持或更新失败状态提示文案,仍可再次重试。
191 +
192 +在实际书写 spec.md 时,可在此基础上补全其它章节(背景、用户故事、边界条件等),并针对具体业务做适当调整。
193 +