# 官网资讯 PC 接口对接实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 `news.html` 与 `article-detail.html` 接入公开 PC 站点文章列表接口,交付可筛选列表与精确详情闭环。 **Architecture:** `utils/ApiClient.js` 独占 PC path、公开鉴权标记和 Query 校验;`public/js/site-news-pages.js` 独占 `SiteArticleVo` 规范化、渲染和页面初始化。后端没有单条详情接口,详情页使用 URL 中由列表响应生成的字符串 ID 重读列表并精确匹配。 **Tech Stack:** 原生 HTML/CSS/JavaScript、现有 Axios 请求层、Node `node:test`。 ## Global Constraints - 后端 `D:\WorkSpace\Java\Genealogy` 只读,不得修改。 - 只使用 `/genealogy/pc/site/articles`,不得调用 APP、后台管理或家谱谱文 CRUD 接口。 - 本批只修改 `news.html` 与 `article-detail.html`,不混改其他静态内容页。 - 业务 ID 始终保持字符串,不提供手填 ID 或 OSS ID。 - `articleType` 只允许 `news/notice/download`;`limit` 固定为 100 且客户端只允许 1–100 的整数。 - 公开请求设置 `auth: false`;其 401/403 不清理既有 token、不跳转登录。 - 响应只接受直接 `SiteArticleVo[]`;旧字段、分页对象和任一非法元素均使整批失败。 - 所有响应文本 HTML 转义;正文仅保留换行;外链只允许绝对 HTTP/HTTPS。 - 当前工作区由用户明确授权直接施工;不得 stage、commit、push 或创建 PR。 - 子代理仅用于探索和只读复核;业务代码修改、取舍与最终验证由主代理完成。 --- ### Task 1: 修正公开请求的登录态隔离 **Files:** - Modify: `tests/request-auth-state.test.js` - Modify: `utils/AxiosRequestUtil.js` **Interfaces:** - Consumes: `createRequester({ getToken, onUnauthorized, axiosInstance })` - Produces: `request(method, path, { auth: false })` 不发送 Authorization,且 HTTP/业务 401 均不调用 `onUnauthorized` - [ ] **Step 1: 写失败测试** 在 `tests/request-auth-state.test.js` 引入 `../utils/AxiosRequestUtil.js`,新增两个真实请求层用例: ```js test('public HTTP 401 neither sends nor clears the stored login token', async () => { let storedToken = 'access-token'; let seenRequest; const requester = AxiosRequestUtil.createRequester({ baseUrl: 'https://api.example.test', clientId: 'web-pc', getToken() { return storedToken; }, onUnauthorized() { storedToken = ''; }, axiosInstance: { request(config) { seenRequest = config; return Promise.reject({ message: 'Request failed', response: { status: 401, data: { code: 401, msg: '认证失败' } } }); } } }); await assert.rejects(requester('GET', '/public', { auth: false })); assert.equal(seenRequest.headers.Authorization, undefined); assert.equal(storedToken, 'access-token'); }); test('public business 401 preserves the stored login token', async () => { let storedToken = 'access-token'; const requester = AxiosRequestUtil.createRequester({ baseUrl: 'https://api.example.test', clientId: 'web-pc', getToken() { return storedToken; }, onUnauthorized() { storedToken = ''; }, axiosInstance: { request() { return Promise.resolve({ data: { code: 401, msg: '认证失败' } }); } } }); await assert.rejects(requester('GET', '/public', { auth: false })); assert.equal(storedToken, 'access-token'); }); ``` - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/request-auth-state.test.js ``` Expected: 新增用例因 `handleUnauthorized` 未区分 `auth: false` 而失败。 - [ ] **Step 3: 写最小实现** 在 `utils/AxiosRequestUtil.js` 的单次 `request` 闭包内,使 HTTP catch 与业务解包 catch 只在 `req.auth !== false` 时调用 `handleUnauthorized`: ```js if (req.auth !== false) handleUnauthorized(status); ``` 不得改变默认鉴权请求的现有行为。 - [ ] **Step 4: 运行 GREEN** Run: ```powershell node --test tests/request-auth-state.test.js ``` Expected: 现有默认 401 清 token、403 保留 token,以及新增公开请求用例全部通过。 --- ### Task 2: 增加 PC 站点文章 ApiClient 契约 **Files:** - Modify: `tests/api-client-contract.test.js` - Modify: `utils/ApiClient.js` **Interfaces:** - Produces: `siteArticles(query?: { articleType?: 'news'|'notice'|'download', limit?: number }): Promise` - Request: `GET /genealogy/pc/site/articles`, `{ auth: false, query }` - [ ] **Step 1: 写失败测试** 在公开方法清单中增加 `siteArticles`,并新增契约用例: ```js await client.siteArticles({ articleType: 'notice', limit: 100, keyword: 'must-drop' }); assert.deepEqual(seen, { method: 'get', url: '/genealogy/pc/site/articles', params: { articleType: 'notice', limit: 100 }, authorization: undefined }); assert.throws( () => client.siteArticles({ articleType: 'culture', limit: 100 }), /不支持的资讯类型/ ); assert.throws( () => client.siteArticles({ limit: 101 }), /资讯数量限制/ ); ``` 同时覆盖 `limit` 的 `0`、小数、字符串和负数,确保只接受 1–100 的 Number 整数;省略 `articleType` 时只发送 `{ limit: 100 }`。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/api-client-contract.test.js ``` Expected: `siteArticles` 不存在。 - [ ] **Step 3: 写最小实现** 在 `utils/ApiClient.js` 增加 `siteArticles`: ```js siteArticles: function (query) { var source = query || {}; var articleType = source.articleType === undefined || source.articleType === null ? '' : String(source.articleType).trim(); var limit = source.limit; var params = {}; if (articleType && ['news', 'notice', 'download'].indexOf(articleType) === -1) { throw new Error('不支持的资讯类型:' + articleType); } if (limit !== undefined && (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 100)) { throw new Error('资讯数量限制必须是 1 到 100 的整数'); } if (articleType) params.articleType = articleType; if (limit !== undefined) params.limit = limit; return request('GET', '/genealogy/pc/site/articles', { auth: false, query: params }); }, ``` - [ ] **Step 4: 运行 GREEN** Run: ```powershell node --test tests/api-client-contract.test.js tests/request-auth-state.test.js ``` Expected: 全部通过,且公开请求没有 Authorization Header。 --- ### Task 3: 实现站点资讯页面 owner **Files:** - Create: `tests/site-news-pages.test.js` - Create: `public/js/site-news-pages.js` **Interfaces:** - Consumes: `GenealogyApi.defaultClient.siteArticles({ articleType?, limit: 100 })` - Produces: - `normalizeArticle(item)` - `normalizeArticleList(data)` - `normalizeArticleType(value)` - `normalizeExternalUrl(value)` - `readArticleId(search)` - `renderArticleList(data)` - `renderArticleDetail(item)` - `loadArticles(api, articleType?)` - `loadArticleDetail(api, articleId)` - `init()` - [ ] **Step 1: 写完整 RED 测试** 创建 `tests/site-news-pages.test.js`,使用完整 `SiteArticleVo` fixture: ```js { articleId: '2062179707935264769', articleType: 'notice', articleTitle: '平台公告', articleSummary: '公告摘要', articleContent: '第一行\n第二行', coverOssId: '2062179707935264701', externalUrl: 'https://example.com/notice', publishTime: '2026-07-30 09:00:00', sortOrder: 1, status: '0', remark: 'internal-only' } ``` 必须覆盖: - 安全长 ID 字符串保留;不安全 Number、零、空 ID 拒绝。 - `articleType` 只接受 `news/notice/download`。 - 标题必填、状态必须为 `0`。 - 直接数组全有或全无;`{ rows: [...] }` 拒绝。 - 规范化结果不包含 `coverOssId/sortOrder/status/remark`。 - 列表 HTML 指向 `article-detail.html?articleId=2062179707935264769`。 - 标题、摘要、正文转义;正文换行变为 `
`。 - 危险、相对和 `data:` 外链不渲染;合法 HTTP/HTTPS 使用 `noopener noreferrer`。 - `loadArticles(api, 'notice')` 调用 `{ articleType: 'notice', limit: 100 }`。 - `loadArticles(api, '')` 调用 `{ limit: 100 }`。 - `loadArticleDetail` 只返回同 ID;未匹配抛出“资讯不存在或已下线”。 - 非法详情 ID 在调用 API 前抛出“资讯编号无效”。 - `readArticleId('?articleId=...')` 只返回安全字符串。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/site-news-pages.test.js ``` Expected: 模块不存在。 - [ ] **Step 3: 写 UMD 模块最小实现** 创建 UMD 模块:Node 环境导出 `module.exports = factory(root)`;浏览器环境挂到 `root.SiteNewsPages`,并在 `DOMContentLoaded` 调用 `root.SiteNewsPages.init()`。关键行为: ```js async function loadArticles(api, articleType) { var type = normalizeArticleType(articleType); var query = { limit: 100 }; var data; var items; if (articleType && !type) throw new Error('资讯分类无效'); if (type) query.articleType = type; data = await api.siteArticles(query); items = normalizeArticleList(data); if (!Array.isArray(data) || items.length !== data.length) { throw new Error('资讯列表响应无效'); } return items; } ``` `init()`: - `[data-site-news-page]`:从 `location.search` 读取 `articleType`,加载并渲染 `[data-site-news-list]`。 - `[data-site-article-page]`:读取 `articleId`;非法时不发请求;合法时精确重读并渲染 `[data-site-article-detail]`。 - 所有失败只更新对应区域和 `[data-site-*-status]`,不跳转、不清 token。 - [ ] **Step 4: 运行 GREEN** Run: ```powershell node --test tests/site-news-pages.test.js tests/api-client-contract.test.js tests/request-auth-state.test.js ``` Expected: 全部通过。 --- ### Task 4: 接入 HTML、规划并完成验收 **Files:** - Modify: `news.html` - Modify: `article-detail.html` - Modify: `tests/site-news-pages.test.js` - Modify: `tests/public-static-pages.test.js` - Modify: `tests/pc-scope.test.js` - Modify: `docs/PC接口对接规划.md` **Interfaces:** - `news.html`: `[data-site-news-page]`, `[data-site-news-list]`, `[data-site-news-status]` - `article-detail.html`: `[data-site-article-page]`, `[data-site-article-detail]`, `[data-site-article-status]` - [ ] **Step 1: 写 HTML 接入 RED 测试** 断言: ```js assert.match(newsPage, /data-site-news-page/); assert.match(newsPage, /data-site-news-list/); assert.match(newsPage, /news\.html\?articleType=news/); assert.match(newsPage, /news\.html\?articleType=notice/); assert.match(newsPage, /news\.html\?articleType=download/); assert.match(newsPage, /src="public\/js\/site-news-pages\.js"/); assert.match(detailPage, /data-site-article-page/); assert.match(detailPage, /data-site-article-detail/); assert.match(detailPage, /src="public\/js\/site-news-pages\.js"/); assert.doesNotMatch(detailPage, /src="public\/js\/article-pages\.js"/); assert.doesNotMatch(newsPage + detailPage, /name="(?:articleId|coverOssId)"/); ``` 更新 `public-static-pages.test.js`,移除旧的固定 `#platform/#culture` 链接断言,改为真实分类入口和详情目标文件断言。 - [ ] **Step 2: 运行 RED** Run: ```powershell node --test tests/site-news-pages.test.js tests/public-static-pages.test.js tests/pc-scope.test.js ``` Expected: HTML 尚未加载新模块、旧固定链接断言需迁移。 - [ ] **Step 3: 修改 HTML** `news.html`: - `` - 删除“当前为静态展示”文案和两条硬编码文章。 - 列表容器初始化为“资讯内容加载中”。 - 分类链接使用规格中的四个固定 URL。 - 按顺序加载 `config.js`、`StorageUtil.js`、`axios.js`、`AxiosRequestUtil.js`、`ApiClient.js`、`page-effects.js`、`site-news-pages.js`。 `article-detail.html`: - body 标记改为 `data-site-article-page`,避免与家谱谱文语义混用。 - 将 hero 和正文合并到 `data-site-article-detail` 可替换容器。 - 增加 `data-site-article-status`。 - 加载 `site-news-pages.js`,不加载 `article-pages.js`。 - [ ] **Step 4: 更新规划** 在 `docs/PC接口对接规划.md` 第 7 节新增“官网资讯契约”,记录: - method/path、公开鉴权、Query 枚举和 limit 范围。 - `SiteArticleVo` 11 个字段的 R/I/S/A 分类、SQL 长度和默认值。 - 列表直接数组、服务端排序、详情重读匹配。 - YAML 只给通用响应且没有导出 `SiteArticleVo` Schema 的差异。 - `coverOssId` 文件 URL 和站点文章单条详情接口仍阻断。 在阶段 7 增加第八批完成说明。 - [ ] **Step 5: 运行自动化验收** Run: ```powershell node --check public/js/site-news-pages.js node --check utils/ApiClient.js node --check utils/AxiosRequestUtil.js node --test tests/site-news-pages.test.js tests/api-client-contract.test.js tests/request-auth-state.test.js tests/public-static-pages.test.js tests/pc-scope.test.js node --test tests/*.test.js git -c safe.directory=D:/WorkSpace/Web/jiapu diff --check ``` Expected: 所有命令退出码 0。 - [ ] **Step 6: 浏览器验收** 启动临时本地静态服务器,验证: 1. 未登录打开 `news.html`,页面不跳登录。 2. 真实列表、分类或空状态正确。 3. 有数据时点击一个由响应生成的详情链接,详情 ID 精确匹配;无数据时记录真实空状态。 4. 控制台没有业务脚本错误。 5. 不点击外部链接。 6. 停止服务器并删除临时辅助文件。 - [ ] **Step 7: 阶段报告** 按以下格式报告并停下: ```text Changed: 官网资讯列表、详情、公开请求语义和规划补充。 Verified: 专项/全量测试数量及真实浏览器覆盖。 Conflicts: YAML 通用响应与后端完整 VO/SQL 的差异。 Blocked: 单条详情接口、封面文件 URL、仍无 PC Controller 的 pending 页面。 ```