diff --git a/checklist-agent2-phase2.md b/checklist-agent2-phase2.md deleted file mode 100644 index d386b5a..0000000 --- a/checklist-agent2-phase2.md +++ /dev/null @@ -1,199 +0,0 @@ -# Agent2 前端自查 Checklist - Phase 2 (v0.4) - -> 项目: mach-cms-frontend -> 版本: Phase 2 v0.4 -> 角色: 前端开发 Agent -> 要求: 逐项自查,全部通过后再提交验收 - ---- - -## 0. 环境前置 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 0.1 | 远程仓库可访问 | `git remote -v` 显示正确 origin | ✓ | -| 0.2 | 本地 main 分支最新 | `git pull --rebase origin main` 无冲突 | ✓ | -| 0.3 | Node.js 20+ | `node -v` 输出 v20.x | ✓ | -| 0.4 | 后端已启动 | `http://localhost:8080/actuator/health` 返回 UP | ✓ | - ---- - -## 1. basePath 修正确认 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 1.1 | `client.ts` 中 basePath 为空字符串 | 所有 `new XxxApi(config, '', axiosInstance)` 第二个参数为 `''` | ✓ | -| 1.2 | 无 `/api/api` 请求 | 浏览器 DevTools Network 面板中,请求路径无 `/api/api` 前缀 | ✓ | -| 1.3 | 请求正常到达后端 | 所有 API 请求状态码 200/201,非 404 | ✓ | - ---- - -## 2. Git 规范自查 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 2.1 | 提交历史规范 | `git log --oneline -20` 所有 message 符合 `type(scope): subject` | ✓ | -| 2.2 | 无 "update"/"fix bug" 等垃圾提交 | 全局搜索无此类 message | ✓ | -| 2.3 | 单 commit 行数 ≤ 200 | `git log --stat` 抽查最近 5 个 commit,无超 200 行 | ☐ | △ bfcbf4a=335、459e094=225 超200行(已push历史,本次验收后续 commit 均≤200) | -| 2.4 | 每次 commit 已 push | `git status` 显示 "Your branch is up to date with 'origin/main'" | ✓ | -| 2.5 | 无未提交代码 | `git status` 无未 stage 的修改 | ✓ | - ---- - -## 3. Task 1: API 代码重新生成 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 3.1 | `npm run api:generate` 成功 | 无报错,生成目录更新 | ✓ | -| 3.2 | `src/generated/api/` 存在 | 目录存在且包含 `api.ts` + `models/` | ✓ | -| 3.3 | Comment 类型生成 | `models/` 包含 `comment-response.ts`, `comment-submit-request.ts` | ☐ | ✓* models/ 未单独拆目录(typescript-axios 默认单文件 api.ts),CommentResponse/CommentSubmitRequest 存在于 api.ts | -| 3.4 | User 类型生成 | `models/` 包含 `user-info.ts`, `update-role-request.ts` | ☐ | ✓* UserInfo/UpdateUserRoleRequest 存在于 api.ts(未拆 models/) | -| 3.5 | API 类生成 | `api.ts` 包含 `CommentsApi`, `CommentAdminApi`, `UserAdminApi` | ☐ | ✓* 类名 CommentApi/CommentAdminApi/UserApi(由契约 tag 决定),非清单 Suggest CommentsApi/UserAdminApi | -| 3.6 | `StoredFile` 扩展 | 包含 `thumbnailUrl` 字段 | ✓ | -| 3.7 | `client.ts` 已更新 | 新增 `commentsApi`, `commentAdminApi`, `userAdminApi` 实例,basePath 为 `''` | ☐ | ✓* 实例 commentApi/commentAdminApi/userApi(契约命名) | - ---- - -## 4. Task 2: 注册页面 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 4.1 | 路由存在 | `/register` 可访问 | ✓ | -| 4.2 | 表单字段 | 用户名、密码、确认密码、邮箱 | ✓ | -| 4.3 | 前端校验 | 密码与确认密码不一致时阻止提交 | ✓ | -| 4.4 | 用户名长度校验 | 3-50 字符 | ✓ | -| 4.5 | 密码长度校验 | 6-100 字符 | ✓ | -| 4.6 | 调用注册 API | 点击注册调用 `authApi.register()` | ✓ | -| 4.7 | 注册后自动登录 | 成功后自动调用 `authStore.login()` | ✓ | -| 4.8 | 跳转后台 | 登录成功后进入 `/admin` | ✓ | -| 4.9 | 已登录拦截 | 已登录用户访问 `/register` 自动跳转 `/admin` | ✓ | -| 4.10 | 错误提示 | 注册失败(如用户名已存在)显示错误信息 | ✓ | - ---- - -## 5. Task 3: 评论组件 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 5.1 | `CommentSection.vue` 存在 | `src/components/CommentSection.vue` 存在 | ✓ | -| 5.2 | 接收 slug prop | `` | ✓ | -| 5.3 | 加载评论列表 | 进入文章详情页自动加载评论 | ✓ | -| 5.4 | 只显示已审核 | 未审核评论不显示 | ✓ | -| 5.5 | 提交表单 | 昵称(必填)、邮箱(可选)、内容(必填) | ✓ | -| 5.6 | 提交后提示 | 显示"评论已提交,等待审核" | ✓ | -| 5.7 | 无需登录 | 未登录用户可提交评论 | ✓ | -| 5.8 | 样式 | 评论间有分隔线,时间格式化显示 | ✓ | -| 5.9 | 文章详情页引入 | `ArticleDetailView.vue` 底部包含 `` | ✓ | - ---- - -## 6. Task 4: Markdown 渲染 + XSS 防护 - -| # | 检查项 | 通过标准 | 结果 | -|---|--------|---------|------| -| 6.1 | 依赖安装 | `marked`, `dompurify`, `highlight.js` 已安装 | ✓ | -| 6.2 | `renderMarkdown` 工具 | `src/utils/markdown.ts` 存在 | ✓ | -| 6.3 | Markdown 转 HTML | 标题、段落、列表、链接正确渲染 | ✓ | -| 6.4 | 代码高亮 | 代码块有语法高亮(背景色、关键字着色) | ✓ | -| 6.5 | XSS 过滤 | `DOMPurify.sanitize()` 过滤 ``,前台渲染时不执行脚本 | ☐ | -| 5.5 | SQL 注入防护 | 搜索关键词输入 `' OR 1=1 --`,后端正常处理不报错 | ☐ | -| 5.6 | 目录遍历防护 | 上传时 `directory=../../etc` 被安全处理 | ☐ | -| 5.7 | JWT 安全 | Token 中不包含敏感信息(密码等);Secret 不是硬编码的弱密钥 | ☐ | - ---- - -## 6. 问题记录模板 - -联调中发现的问题按以下格式记录: - -### Issue #1 — 搜索接口复现为后端旧进程所致(已解决) -- **发现时间**: 2026-08-10 00:00 -- **发现人**: Agent2(前端) -- **问题描述**: 初测时 `GET /api/articles/search?q=Spring` 返回 `{code:3001,"文章不存在: search"}`。后端重启(`./gradlew bootRun` 全新进程)后同一请求返回 code=0 正确搜索结果,确认该现象由旧后端进程(未包含 SearchController 的旧构建)造成,非契约/前端问题。 -- **复现步骤**: 旧进程下 `curl "http://localhost:8080/api/articles/search?q=spring"` -- **预期结果**: 返回搜索结果 -- **实际结果**: 重启后返回 `{code:0, data:{list:[...]}}`,2.3.5/1.5.1 通过 -- **根因分析**: 后端旧进程加载了过期构建产物,未包含搜索路由 -- **责任方**: Agent1(后端,运维) -- **修复方案**: 联调前确保后端为最新 `bootRun` 进程 -- **验证结果**: ☐ 已解决(2026-08-10 复验通过) - -### Issue #2 — 后端 `POST /auth/logout` 未吊销 Refresh Token -- **发现时间**: 2026-08-10 00:05 -- **发现人**: Agent2(前端) -- **影响**: 前端 2.4.6 通过(本地清理 localStorage 正常);仅后端吊销语义缺失(1.3.4) -- **问题描述**: 前端 `auth.logout()` 调 `POST /api/auth/logout` 后,旧 refreshToken 仍可通过 `POST /api/auth/refresh` 换取新 TokenPair,令牌吊销语义失效(违反 checklist 1.3.4)。 -- **复现步骤**: 1) 登录获取 refreshToken;2) 调 `/auth/logout`(code=0);3) 用旧 refreshToken 调 `/auth/refresh` → 仍返回 code=0 新 token(在最新 bootRun 进程上复验一致) -- **预期结果**: logout 后旧 refreshToken 刷新应失败(401/业务错误) -- **实际结果**: 刷新成功并返回新的 TokenPair -- **根因分析**: 后端登出逻辑未将 refreshToken 加入黑名单/吊销列表(JWT 无状态未记录失效) -- **责任方**: Agent1(后端) -- **修复方案**: 服务端维护 refreshToken 吊销集合(DB/redis),refresh 时校验 -- **验证结果**: ☐ 待修复 - -### Issue #3 — 契约路径加 `/api` 前缀后前端 baseURL 适配(已处理) -- **发现时间**: 2026-08-10 00:20 -- **发现人**: Agent2(前端,承接 Agent1 提示) -- **问题描述**: Agent1 将契约路径改为带 `/api` 前缀(`servers.url=http://localhost:8080`)。重新 `npm run api:generate` 后,生成的 localVarPath 变为 `/api/auth/...`、方法名变为 `apiXxx` 前缀、新增 `PublicApi`/`UserApi`。若前端 axios `baseURL` 仍为 `/api`,会拼成 `/api/api/...` → 404。 -- **Agent1 提示**: 需把 client.ts 中 `new XxxApi(config, '/api', ...)` 的 `/api` 改为 `''`。经核实**不正确/不完整**:生成的 `common.ts` 拼接逻辑为 `axios.defaults.baseURL ? '' : (configuration?.basePath ?? basePath)`,axios 的 `baseURL` 优先于 XxxApi 的 basePath 参数;且 axios 会自行 combineURLs。若仅改 XxxApi 参数而保留 baseURL=`/api`,仍会双前缀。 -- **实际修复**: 将 axios 实例 `baseURL` 从 `/api` 改为 `/`(各 XxxApi 的 `/api` basePath 参数无害保留),并批量重命名方法调用(`articlesGet`→`apiArticlesGet` 等)。实测 `/api/articles` 单前缀、经 vite 代理到 8080 正常,2.3~2.6 全部用例复验 PASS。 -- **责任方**: 前端(Agent2) -- **验证结果**: ☑ 已处理(2026-08-10 复验 26/26 通过) - -```markdown -### Issue #{编号} -- **发现时间**: YYYY-MM-DD HH:MM -- **发现人**: Agent1/Agent2/架构师 -- **问题描述**: -- **复现步骤**: -- **预期结果**: -- **实际结果**: -- **根因分析**: -- **责任方**: Agent1(后端)/ Agent2(前端)/ openapi.yaml(契约) -- **修复方案**: -- **验证结果**: ☐ 待修复 / ☐ 已修复 / ☐ 已验证 -``` - ---- - -## 验收签字 - -| 角色 | 姓名 | 日期 | 签字 | -|------|------|------|------| -| 架构 Owner | | | | -| Agent1(后端) | | | | -| Agent2(前端) | | | | - ---- - -## 附录:快速测试命令 - -```bash -# 后端健康检查 -curl http://localhost:8080/actuator/health - -# 注册 -curl -X POST http://localhost:8080/api/auth/register \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"123456"}' - -# 登录 -curl -X POST http://localhost:8080/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"123456"}' - -# 创建文章(替换 $TOKEN) -curl -X POST http://localhost:8080/api/admin/articles \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"title":"Hello World","content":"This is a test article."}' - -# 搜索 -curl "http://localhost:8080/api/articles/search?q=hello&page=1&size=10" - -# 前台列表(无认证) -curl "http://localhost:8080/api/articles?page=1&size=10" -``` diff --git a/prompt-agent2-frontend-v0.3.md b/prompt-agent2-frontend-v0.3.md deleted file mode 100644 index 196c274..0000000 --- a/prompt-agent2-frontend-v0.3.md +++ /dev/null @@ -1,507 +0,0 @@ -# Agent2 Prompt: Mach-CMS Frontend Implementation - -## 你的角色 -你是前端开发 Agent,负责实现 Mach-CMS 的管理后台和前台页面。你**不是设计师**,不讨论 UI 美观性,只按契约和规范实现功能。 - -## 契约文件(唯一真理来源) -后端仓库根目录下的 `openapi.yaml` 是你唯一的 API 规范。你必须: -1. 用 OpenAPI Generator 从该 YAML 生成 TypeScript 类型和 API 客户端 -2. **禁止手写任何 API 请求类型或接口定义** -3. 如果实现中发现 YAML 与实际返回不符,**暂停并上报**,禁止自行修改后不同步 - -## 技术栈(禁止变更) -- Vue 3.4+ -- TypeScript 5.4+(严格模式) -- Vite 5.2+ -- Vue Router 4.3+ -- Pinia 2.1+ -- Axios 1.7+ -- OpenAPI Generator 7.x - -## 项目初始化 - -```bash -npm create vue@latest mach-cms-frontend -# 选择:TypeScript + Vue Router + Pinia - -cd mach-cms-frontend -npm install -npm install axios -npm install -D @openapitools/openapi-generator-cli -``` - -## 你的任务(按顺序执行,逐项验收) - -### Task 1: OpenAPI 代码生成配置 - -创建 `openapitools.json`: -```json -{ - "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", - "spaces": 2, - "generator-cli": { - "version": "7.6.0", - "generators": { - "api": { - "generatorName": "typescript-axios", - "inputSpec": "../mach-cms-backend/openapi.yaml", - "output": "src/generated/api", - "additionalProperties": { - "supportsES6": "true", - "npmName": "mach-cms-api", - "snapshot": "false", - "withInterfaces": "false" - } - } - } - } -} -``` - -在 `package.json` 添加脚本: -```json -{ - "scripts": { - "api:generate": "openapi-generator-cli generate" - } -} -``` - -执行: -```bash -npm run api:generate -``` - -确认生成目录结构: -``` -src/generated/api/ -├── api.ts # 所有 API 类(AuthApi, ArticlesApi, ArticleAdminApi, TagsApi, MediaApi...) -├── base.ts # Axios 实例和配置 -├── configuration.ts -├── common.ts -├── index.ts -└── models/ - ├── index.ts - ├── response.ts - ├── page-result.ts - ├── article-detail.ts - ├── article-list-item.ts - ├── article-create-request.ts - ├── article-update-request.ts - ├── token-pair.ts - ├── login-request.ts - ├── register-request.ts - ├── tag.ts - ├── stored-file.ts - └── ... -``` - -### Task 2: Axios 客户端封装 - -文件 `src/api/client.ts`: -```typescript -import { - Configuration, - AuthApi, - ArticlesApi, - ArticleAdminApi, - TagsApi, - MediaApi, - SearchApi -} from '@/generated/api' -import axios from 'axios' - -const axiosInstance = axios.create({ - baseURL: '/api', - timeout: 10000 -}) - -// 请求拦截:自动带 Access Token -axiosInstance.interceptors.request.use((config) => { - const token = localStorage.getItem('token') - if (token) { - config.headers.Authorization = `Bearer ${token}` - } - return config -}) - -// 响应拦截:401 静默刷新 Token -axiosInstance.interceptors.response.use( - (res) => res, - async (err) => { - const original = err.config - if (err.response?.status === 401 && !original._retry) { - original._retry = true - const refresh = localStorage.getItem('refreshToken') - if (refresh) { - try { - const authApi = new AuthApi(new Configuration(), '/api', axiosInstance) - const resp = await authApi.refreshToken({ refreshToken: refresh }) - const data = resp.data.data! - localStorage.setItem('token', data.accessToken) - localStorage.setItem('refreshToken', data.refreshToken) - original.headers.Authorization = `Bearer ${data.accessToken}` - return axiosInstance(original) - } catch { - localStorage.removeItem('token') - localStorage.removeItem('refreshToken') - window.location.href = '/login' - } - } else { - window.location.href = '/login' - } - } - return Promise.reject(err) - } -) - -const config = new Configuration() - -export const authApi = new AuthApi(config, '/api', axiosInstance) -export const articlesApi = new ArticlesApi(config, '/api', axiosInstance) -export const articleAdminApi = new ArticleAdminApi(config, '/api', axiosInstance) -export const tagsApi = new TagsApi(config, '/api', axiosInstance) -export const mediaApi = new MediaApi(config, '/api', axiosInstance) -export const searchApi = new SearchApi(config, '/api', axiosInstance) -``` - -**禁止**:直接 `import axios from 'axios'` 在任何组件或服务中使用,必须统一通过 `client.ts`。 - -### Task 3: Pinia 认证 Store - -文件 `src/stores/auth.ts`: -```typescript -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import { authApi } from '@/api/client' -import type { LoginRequest, RegisterRequest } from '@/generated/api' - -export const useAuthStore = defineStore('auth', () => { - const token = ref(localStorage.getItem('token')) - const refreshToken = ref(localStorage.getItem('refreshToken')) - const username = ref(null) - const isLoggedIn = computed(() => !!token.value) - - async function register(request: RegisterRequest) { - const resp = await authApi.register(request) - const data = resp.data.data! - setTokens(data.accessToken, data.refreshToken) - } - - async function login(request: LoginRequest) { - const resp = await authApi.login(request) - const data = resp.data.data! - setTokens(data.accessToken, data.refreshToken) - } - - async function logout() { - try { - await authApi.logout() - } finally { - clearTokens() - } - } - - function setTokens(access: string, refresh: string) { - token.value = access - refreshToken.value = refresh - localStorage.setItem('token', access) - localStorage.setItem('refreshToken', refresh) - } - - function clearTokens() { - token.value = null - refreshToken.value = null - username.value = null - localStorage.removeItem('token') - localStorage.removeItem('refreshToken') - } - - return { - token, - refreshToken, - username, - isLoggedIn, - register, - login, - logout, - setTokens, - clearTokens - } -}) -``` - -### Task 4: Vue Router + 路由守卫 - -文件 `src/router/index.ts`: -```typescript -import { createRouter, createWebHistory } from 'vue-router' -import { useAuthStore } from '@/stores/auth' - -const router = createRouter({ - history: createWebHistory(), - routes: [ - { - path: '/', - name: 'Home', - component: () => import('@/views/HomeView.vue') - }, - { - path: '/post/:slug', - name: 'ArticleDetail', - component: () => import('@/views/ArticleDetailView.vue') - }, - { - path: '/search', - name: 'Search', - component: () => import('@/views/SearchView.vue') - }, - { - path: '/login', - name: 'Login', - component: () => import('@/views/LoginView.vue'), - meta: { guestOnly: true } - }, - { - path: '/admin', - component: () => import('@/views/admin/AdminLayout.vue'), - meta: { requiresAuth: true }, - children: [ - { - path: '', - redirect: '/admin/articles' - }, - { - path: 'articles', - name: 'AdminArticleList', - component: () => import('@/views/admin/ArticleListView.vue') - }, - { - path: 'articles/new', - name: 'AdminArticleCreate', - component: () => import('@/views/admin/ArticleEditorView.vue') - }, - { - path: 'articles/edit/:id', - name: 'AdminArticleEdit', - component: () => import('@/views/admin/ArticleEditorView.vue') - }, - { - path: 'tags', - name: 'AdminTagList', - component: () => import('@/views/admin/TagListView.vue') - } - ] - } - ] -}) - -router.beforeEach((to, from, next) => { - const auth = useAuthStore() - - if (to.meta.requiresAuth && !auth.isLoggedIn) { - next('/login') - } else if (to.meta.guestOnly && auth.isLoggedIn) { - next('/admin') - } else { - next() - } -}) - -export default router -``` - -### Task 5: Vite 代理配置 - -文件 `vite.config.ts`: -```typescript -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import { resolve } from 'path' - -export default defineConfig({ - plugins: [vue()], - resolve: { - alias: { - '@': resolve(__dirname, 'src') - } - }, - server: { - port: 5173, - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true - }, - '/uploads': { - target: 'http://localhost:8080', - changeOrigin: true - } - } - } -}) -``` - -### Task 6: 页面实现 - -#### 6.1 首页 `src/views/HomeView.vue` -- 调用 `articlesApi.listArticles()` 获取文章列表 -- 展示文章卡片:标题、摘要、发布时间、作者、标签 -- 点击卡片跳转 `/post/:slug` -- 支持分页 - -#### 6.2 文章详情 `src/views/ArticleDetailView.vue` -- 路由参数 `slug` -- 调用 `articlesApi.getArticleBySlug(slug)` -- 渲染 Markdown 内容(先用 `v-html` 配合 DOMPurify,后期可接 Markdown 渲染库) -- 展示:标题、作者、发布时间、标签、浏览量 - -#### 6.3 搜索页 `src/views/SearchView.vue` -- Query 参数 `?q=keyword` -- 调用 `searchApi.searchArticles(q, page, size)` -- 展示搜索结果列表 - -#### 6.4 登录页 `src/views/LoginView.vue` -- 表单:用户名、密码 -- 调用 `authStore.login({ username, password })` -- 登录成功跳转 `/admin` -- 提供"去注册"链接(Phase 2 完善) - -#### 6.5 管理后台布局 `src/views/admin/AdminLayout.vue` -- 侧边栏导航:文章管理、标签管理 -- 顶部栏:显示当前用户、退出按钮 -- 中间 `` - -#### 6.6 文章列表 `src/views/admin/ArticleListView.vue` -- 调用 `articleAdminApi`(注意:需要认证) -- 表格展示:标题、状态、发布时间、操作(编辑、删除、发布、归档) -- 分页 -- 状态用不同颜色标签区分(DRAFT=灰色, PUBLISHED=绿色, ARCHIVED=橙色) - -#### 6.7 文章编辑器 `src/views/admin/ArticleEditorView.vue` -- 复用组件:创建和编辑共用 -- 表单:标题、摘要(textarea)、内容(textarea,后期接 Markdown 编辑器)、封面图 URL、标签选择 -- 创建调用 `articleAdminApi.createArticle()` -- 编辑调用 `articleAdminApi.updateArticle(id, ...)` -- 保存成功后跳转列表页 - -#### 6.8 标签管理 `src/views/admin/TagListView.vue` -- 调用 `tagsApi.listTags()` -- 表格展示:名称、文章数、操作(编辑、删除) -- 新增标签表单 - -### Task 7: 组件规范 - -文件 `src/components/AppButton.vue`、`src/components/AppInput.vue` 等基础组件(可选,可用原生 HTML 先跑通)。 - -**必须有的组件**: -- `src/components/ArticleCard.vue`:文章卡片,接收 `ArticleListItem` 类型 props -- `src/components/Pagination.vue`:分页组件,接收 `page/size/totalPages`,emit `change` - -## 代码规范(违反 = 拒收) - -| # | 规则 | 处罚 | -|---|------|------| -| 1 | **禁止手写 API 类型**,必须从 `openapi.yaml` 生成 | 重写 | -| 2 | **禁止直接 `import axios`**,统一用 `src/api/client.ts` | 重写 | -| 3 | 组件必须用 `