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 的输出会更稳定。
This diff is collapsed. Click to expand it.