feat: 打通 HBuilderX CLI 构建 + 修复文章闭环静态盲区

构建:
- 新增 tools/build.js:junction HBuilderX 工具链,CLI 构建 h5/mp-weixin
- vue 指向补丁版 @dcloudio/uni-h5-vue(官方 npm vue 不导出 isInSSRComponentSetup)
- 设 HX_APP_ROOT 避免退化成 H5 空壳产物;产物完整性校验

校验工具:
- 新增 check-cloud-methods.js:acorn 解析云对象方法,比对 94 处调用点
- 新增 check-android-contract.js:Kotlin 侧云对象契约校验
- audit-project.js 修 downloadFile 误报(注释未剥离);tools/ 排除出扫描
- package.json 声明此前隐式依赖的 acorn

功能:
- 补 uni-cms-articles.getPublishedArticles(安卓端依赖但此前不存在)
- 修 u-parse <audio> 引用已移除组件导致 H5 构建失败
This commit is contained in:
2026-09-12 01:02:45 +08:00
parent 4f5893f87a
commit 3117146281
26 changed files with 2043 additions and 337 deletions
+412
View File
@@ -0,0 +1,412 @@
# 下一步计划 · 文章闭环收尾 + 工具链打通
> 交接对象:下一个接手的模型 / 工程师。
> 本文档分两部分:**A. 本轮实际做完的事**(含验证证据),**B. 还没做的清单**(含具体做法)。
> 前置计划:`.zcode/plans/plan-sess_cd5437b2-dd23-478b-bfec-f4cce1ee4b10.md`(架构决策与 7 步实施步骤,已全部落地)。
---
## A. 本轮完成清单(本次会话实测,非推断)
### A1. 文章编辑→存储→渲染闭环(上一轮遗留,本轮核对确认已落地)
| # | 内容 | 落点 | 核对方式 |
|---|---|---|---|
| 1 | 数据适配层 `normalizeCmsList` / `cmsLstToDelta` / `createVoteId` / `getArticleText` / `getArticleImages` / `hasRenderableContent` | `uniCloud-alipay/cloudfunctions/uni-cms-articles/content-adapter.js`(170 行,含空块过滤、媒体字符串归一、投票最少 2 项) | 已读全文 + `check-article-flow.js` 通过 |
| 2 | 云对象 add/update 契约:mobile 缺 content 时服务端生成兼容 Delta;update 用 `valueOrExisting` 合并缺省字段;发布强制 `hasRenderableContent`;vote_id 自动生成 | `uni-cms-articles/index.obj.js` | 已读全文 |
| 3 | schema:`content` 从 `required` 移除(现为 `['user_id','title']`);read 权限收敛为 `doc.article_status == 1 \|\| (auth.uid != null && ('admin' in auth.role \|\| doc.user_id == auth.uid))` | `uni-cms-articles.schema.json` | 程序化解析确认 |
| 4 | 审核覆盖:`content \|\| cmsLst` 双通道;mobile 不依赖 content 返回正文;`content.ops` 缺失时降级不抛异常 | `uni-cms-articles.schema.ext.js` | 已读 `afterRead` 全文 |
| 5 | 移动编辑器:首块文字可加载(`editText.vue:97` 无 `if (index && ...)` 假值陷阱,`:137` 用 `Number.isInteger`);`{...oldItem}` 保留媒体字段;小标题写回;封面统一字符串数组 | `pages2/editCms/*.vue` | 逐处 grep 确认 |
| 6 | 详情渲染:mobile 分支输出 text/title;`render-article-detail/*` 兼容 `data.value` 与 `data.attributes.src`;列表递归 children;移除 Vue2 `this.$set`;`wx.` 已用 `#ifdef MP-WEIXIN` 包裹;`100wv/60hv` 全部清零 | `uni_modules/uni-cms-article/**` | grep 确认 0 命中 |
| 7 | 媒体删除鉴权:admin 放行;普通作者必须带 `article_id` 且文件确实被该文章引用 | `ext-storage-co/index.obj.js:65-82` | 已读实现 |
### A2. 本轮新修的问题
**① `ext-storage-co.downloadFile()` 审计告警 = 误报,已从根上修掉**
真因:`tools/audit-project.js` 的 `WRITE_METHOD_RE` 不剥离 `//` 注释,
把文件里**被注释掉的** `async downloadFile()` 当成活方法,报「未校验登录态」。
修法(已落地):新增 `blankLineComments()`,用**等长空格**替换行注释——
保持字节偏移不变,所以 `lineAt()` 行号和 `src.slice()` 切片仍然正确。
`sectionCloudAuth` 内所有正则改跑 `scanned`,`lineAt` 仍传原始 `src`。
> ⚠️ 不要改成直接 `src.replace(...)` 后再切片——偏移会错位,行号全乱。
**② 新增 `tools/check-cloud-methods.js`——云对象方法存在性校验**
补的是一层**原本完全没人管的静态盲区**:`audit-project.js` 只校验
`importObject` 引用的「云函数文件」是否存在,**不校验方法名**。
方法名写错在静态审计里 0 报错,只在运行时炸 `xxx is not a function`。
- 用 `acorn` 解析 `module.exports` 对象字面量取顶层方法名
- 支持两种导出形态:对象字面量 / `module.exports = require('./functions')`(递归解析 require 链,深度上限 3)
- 同时发现随插件分发的云对象(`uni-id-co` / `uni-pay-co` / `uni-captcha-co` / `uni-media-library-co`)
- 4 组内联样本自测:对象字面量 / 默认对象参数 / 注释里的假方法 / 字符串里的花括号
- **实测结果:18 个云对象、94 处调用点,0 错误**
被它拦下的真实问题(均已确认无碍):
- `article_info` 缺 `getInfo` / `isSelectVote` / `getCmsArticleLog` → 实际都有,是我第一版正则提取器漏了
- `uni-media-library-co` 缺 6 个方法 → 该云对象是 `module.exports = require('./functions')` 转出,加了 require 链解析后正常
**③ 新增 `tools/check-android-contract.js`——安卓端 × 云对象契约校验**
`android/` 是独立 Kotlin 工程,通过 URL 化 HTTP 网关调云对象,同样有方法名漂移风险。
- 从 Kotlin 源码抽 `callChecked/callRaw("云对象","方法")` 调用对(70 个文件、44 处调用点、去重 43 个)
- **做了反例测试**:故意把 `getPublishedArticles` 改成 `getPublishedArticlesXX` → 脚本正确报错;还原后恢复 0 错误
**④ `package.json` 声明了此前隐式依赖的 `acorn`**
`check-cloud-methods.js` 用了 `acorn`,但它是**未声明的传递依赖**——
一次 `npm install` 就会把它剪掉,脚本随即 `Cannot find module 'acorn'`。
现已写入 `devDependencies`(`^8.11.3`)并补了 npm scripts。
**⑤ 补上安卓端依赖的 `getPublishedArticles`(真实功能缺口)**
`android/README.md` 要求部署该方法,`ArticleRepository.kt:34` 优先调用它,
**但云函数里根本不存在**——安卓首页/搜索永远只能回退到「仅本人文章」,游客拿到空列表。
已实现(`uni-cms-articles/index.obj.js`):
- 公开、无需登录;`article_status = 1` 过滤
- 支持 `offset` / `limit`(上限 100)、`category_id`、`keyword`
- **keyword 做了正则转义**(`escapeRegExp`),避免用户输入被当正则执行
- **投影只取列表字段,刻意不取 `content`**:因为 `schema.ext` 的 `afterRead`
在读取 `content` 字段时会顺带给 `view_count` 加 1,列表页不该把「出现在首页」
算成一次阅读
- 排序 `publish_date desc`,附带 `total`
**⑥ 修掉 H5 构建的硬阻塞:`u-parse` 引用已被移除的 `<audio>` 组件**
`uni_modules/uview-ui/components/u-parse/node/node.vue` 用了 `<audio>`,
但新版 uni-h5 已不再导出 `Audio` 组件,H5 端编译直接失败:
```
"Audio" is not exported by "@dcloudio/uni-h5/dist/uni-h5.es.js"
```
改为普通 `view` 承载(保留 id/class/style 以维持排版),并写明了原因。
这是 vendor 文件,但**不修就构建不出来**,属于必要最小改动。
**注**:`components/cu-editor/cu-editor.vue:182` 也有 `<audio>`,但该组件全项目无引用,
且它不在构建路径上(H5 构建已通过),所以没动——**留着记录,别顺手改**。
### A3. 打通 HBuilderX CLI 构建(本轮最大产出)
这是原来完全没解决的事:**只能靠 HBuilderX GUI 点按钮,无法命令行/CI 构建**。
现在 `node tools/build.js -p h5` 直接可跑。
#### 踩过的 5 个坑(按发现顺序)
| # | 现象 | 真因 | 解法 |
|---|---|---|---|
| 1 | `ENOENT: ...\t\src\manifest.json` | uni CLI 默认按标准 CLI 工程布局找 `src/`,HBuilderX 工程根目录才是 `UNI_INPUT_DIR` | 设 `UNI_INPUT_DIR` / `UNI_OUTPUT_DIR` |
| 2 | 找不到 `@dcloudio/*` | 项目 `node_modules` 是 **Vue2 时代的残缺安装**(只有 4 个 @dcloudio 包),Vue3 工具链只随 HBuilderX 分发 | 把 HBuilderX 工具链目录 **junction** 进项目 `node_modules` |
| 3 | `Preprocessor dependency "sass" not found` | sass 在另一个插件目录 `plugins/compile-dart-sass/` | 单独 junction |
| 4 | `"isInSSRComponentSetup" is not exported by vue` | **关键**:官方 npm `vue` 3.x 不导出这个符号,但 `@dcloudio/uni-app` 从 `'vue'` 导入它。uni-app 用的是**打过补丁的运行时** `@dcloudio/uni-h5-vue`(`dist/vue.runtime.esm.js`,确实导出该符号)。uni 的 resolveId 插件本应把 `vue` 别名过去,但没拦住 | 把 `node_modules/vue` **直接 junction 到 `@dcloudio/uni-h5-vue`** |
| 5 | 构建"成功"但产物是 H5 空壳(无 `app.json`、无业务分包) | 缺 `HX_APP_ROOT` 时 `uni-cli-shared` 的 `isInHBuilderX()` 返回 false,退化成非 HBuilderX 模块解析路径 | 设 `HX_APP_ROOT` + `UNI_HBUILDERX_PLUGINS` |
附带:系统 node v24 跑 vite 会报 `Invalid or incompatible cached data (cachedDataRejected)`,
改用 **HBuilderX 自带的 node**(`plugins/node/node.exe`,v22.22.2)即稳定。
#### 交付物:`tools/build.js`
- 幂等:每次跑自动建联接,已存在的跳过(重跑 `新建 0 个`)
- `--doctor` 环境体检(含验证补丁版 vue 是否真的导出 `isInSSRComponentSetup`)
- `--link-only` 只建联接
- `--report` 打印产物体积明细
- **产物校验**:构建退出码为 0 不代表产物可用——脚本会检查
mp 必须有 `app.json`/`app.js`/`app.wxss`,H5 必须有 `index.html`,且 JS 文件数必须 > 0
npm scripts:`npm run build:h5` / `npm run build:mp-weixin` / `npm run verify` / `npm run verify:quick`
#### 实测证据
```
$ node tools/build.js -p h5 --report
✓ 构建完成(90 个 JS 文件)
产物目录:unpackage/dist/build/web
assets/ 2335.9 KB
static/ 312.1 KB
合计 2791.1 KB
$ node tools/build.js -p mp-weixin
✓ 构建完成(335 个 JS 文件)
→ app.json / app.js / app.wxss 齐全,pages2 等业务分包完整
```
H5 产物里 `editCms` / `editText` / `smallTitle` / `vote` / `editSetting` / `preview`
各自的 chunk 都在,7 个 chunk 含 `cmsLst`,说明文章链路真的编译进去了:
```
pages2-editCms-editCms.Cc0kHD0h.js
pages2-editCms-editText.CLBQkN2p.js
pages2-editCms-smallTitle.D2I257SK.js
pages2-editCms-vote.EUryHJH3.js
...
```
用本地静态服务器验证过 `HTTP 200`(`index.html` 1095 B、主 bundle 749754 B、
`editCms` chunk 12164 B)。
### A4. 验证门禁现状
```
$ node tools/verify.js
【检查器自测】
✓ 静态审计自测
✓ Vue 校验自测
✓ 文章内容适配自测
✓ 云对象方法校验自测
【项目检查】
✓ 静态审计 0 错误 / 11 警告 ← 原 12,误报已修
✓ Vue SFC 校验 0 错误 / 49 警告
✓ 云对象方法校验 0 错误 / 1 警告 ← 新增
✓ 安卓端契约校验 0 错误 / 0 警告 ← 新增
【数据与资源】13 项全 ✓(schema 63 文件有效、db_init 9 表)
═══════════════════════════════════════════
✓ 全部通过
```
---
## B. 未完成清单
### P0-1 · 提交工作区(最高优先)
**为什么**:19 个改动文件 + 5 个新源文件全裸奔在工作区,一次误操作就全丢。
```bash
cd C:/Users/12914/Desktop/t
git add -A
git status --short
git diff --cached --stat
git commit -m "feat: 打通 HBuilderX CLI 构建 + 修复文章闭环静态盲区
构建:
- 新增 tools/build.js:junction HBuilderX 工具链,CLI 构建 h5/mp-weixin
- vue 指向补丁版 @dcloudio/uni-h5-vue(官方 npm vue 不导出 isInSSRComponentSetup)
- 设 HX_APP_ROOT 避免退化成 H5 空壳产物;产物完整性校验
校验工具:
- 新增 check-cloud-methods.js:acorn 解析云对象方法,比对 94 处调用点
- 新增 check-android-contract.js:Kotlin 侧云对象契约校验
- audit-project.js 修 downloadFile 误报(注释未剥离);tools/ 排除出扫描
- package.json 声明此前隐式依赖的 acorn
功能:
- 补 uni-cms-articles.getPublishedArticles(安卓端依赖但此前不存在)
- 修 u-parse <audio> 引用已移除组件导致 H5 构建失败"
git log --oneline -1
```
**注意**:
- `.zcode/` 已加入 `.gitignore`(本次已做),不要提交。
- CRLF 警告是仓库既有配置,不影响提交,别改 `core.autocrlf`。
- `unpackage/` 与 `package-lock.json` 本来就在 `.gitignore`,构建产物不会入库。
- `private.wxb4c2c6c51ecba864.key` 已在 `.gitignore`,确认没被 `git add -A` 带进去。
### P0-2 · 补厚内容适配契约测试(最大的测试空心区)
`tools/check-article-flow.js` 只有 44 行、4 组断言,原计划第 8 步列的 7 条要求中
**a/b/c/d/e/f/g 六条测不到——因为逻辑还埋在 .vue 组件里,Node 侧无法引用**。
| 缺失断言 | 逻辑现状位置 |
|---|---|
| a. 编辑首个文字块(index=0)不被清空 | `editText.vue:97-108` |
| b. 视频改说明后 `src/poster/duration` 仍在 | `editCms.vue` 的 video 文字编辑分支 |
| c. 小标题能写入 `cmsLst`(`type:'title'`) | `smallTitle.vue:120-140` |
| d. 封面始终是字符串数组 `['','','']` | `editSetting.vue` |
| e. mobile 发布 payload 同时含 `cmsLst` + `content` | `preview.vue:404-420` |
| f. 5 种块在预览/详情分流后都有渲染目标 | 预览与详情模板分支 |
| g. `hasRenderableContent` 对空/纯空块的判定 | 已在 adapter,但只测了正例 |
**关键:先抽纯函数,再写断言。** 在 `content-adapter.js` 追加
`mergeCmsItem` / `normalizeThumbnail` / `buildPublishPayload`,
把组件里的内联逻辑改为调用它们(行为不变),然后补齐 a–g 共 ≥10 组断言。
**抽取时别弄丢已修好的地方**:
- `editText.vue:97` 是 `if (cms && cms.cmsLst && cms.cmsLst[index])`(没有 `if (index && ...)` 的假值陷阱)
- `editText.vue:137` 用 `Number.isInteger(Number(index))`
- `editText.vue:102` 已在用 `{ ...oldItem, text, html, delta }` 合并
- `smallTitle.vue:133` 用 `editingIndex.value >= 0`
### P0-3 · 端到端 QA(唯一能真证明「闭环」的一步)
构建能过 ≠ 功能正常。静态脚本只能证明数据层;保存/预览/发布/详情是跨端运行时行为。
**本轮已排除的**:浏览器自动化(`mcp__node_repl__js` 未在本会话提供),
所以只能做到「H5 产物可服务 + chunk 齐全」,**真正的页面渲染与交互没人验证过**。
环境:HBuilderX(或用 `node tools/build.js -p h5` 出产物后起静态服务器)+
关联 `uniCloud-alipay` 服务空间,上传 `cloudfunctions` 与 `database`。
| # | 操作 | 期望 |
|---|---|---|
| 1 | 新建 mobile 文章:标题 + 首块文字 + 小标题 + 图片 + 视频(填说明) + 投票 | 存草稿成功 |
| 2 | 退出编辑页重新进入 | **首块文字仍在**(回归 a);小标题在;视频说明在;封面是字符串 |
| 3 | **打开已有文字的文章,不点任何东西直接保存** | 原文不变(见 P1-2) |
| 4 | 点预览 | 5 种块各渲染一次,**不重复渲染**(历史上 u-parse + 单独渲染会重复) |
| 5 | 发布 | 请求 payload 同时含 `cmsLst` 与 `content`;`vote_id` 非空 |
| 6 | 打开详情页 | 全部块可见;视频可播;投票可提交并计数 |
| 7 | 匿名/未登录,用草稿 `_id` 直连读 | **被拒**(read 权限表达式生效) |
| 8 | 换封面:先让上传失败(断网) | 旧封面仍在,文章引用未被改坏 |
| 9 | 普通作者删**自己**文章的图 | 成功 |
| 10 | 普通作者拿 A 文章 `article_id` 删 B 文章的文件 | **失败**「文件不是该文章引用资源」 |
| 11 | 老 PC 文章(`edit_type != 'mobile'`,content-only)打开详情 | 走官方 renderer,**不被 mobile 分支污染、不被兜底清空** |
| 12 | H5/小程序启动控制台 | 无 `wx is not defined`、无 `$set` 报错、无非法单位 |
| 13 | **安卓端首页(未部署 getPublishedArticles 时 vs 部署后)** | 部署前回退「仅本人」;部署后游客可见已发布文章 |
**注意第 11 条**:`afterRead` 对 PC 文章有兜底
`if (!article.content || !Array.isArray(article.content.ops)) { article.content = { ops: [] } }`,
要验证老文章**不会因此静默变空白正文**。若发现被清空,改成「保留原值 + 记日志」。
### P1-1 · 内容审核链路未真跑(本地脚本覆盖不到)
`schema.ext.js` 的 `beforeAdd` / `beforeUpdate` / `afterRead` 只在 uniCloud 运行时执行。
做法:控制台/真机发一条含敏感词 + 一张违规图的 payload,确认
文本命中抛错、图片命中抛错、**mobile 文章只填 `cmsLst` 不填 `content` 也要被审到**。
记录请求 id 与返回。
### P1-2 · 编辑器空内容覆盖保护(残留边界风险)
`pages2/editCms/editText.vue:100-108`(已核对):`fail` 分支已经不覆盖原内容,
但 `success` 分支在「editor ready 但内容未 hydrate」时会用空串把原文字抹掉。
```js
const nextText = text || ''
const hadContent = Boolean(oldItem.text || oldItem.html)
const gotContent = Boolean(nextText || html || (delta && delta.ops && delta.ops.length))
if (hadContent && !gotContent) {
console.warn('编辑器内容为空,跳过覆盖以防丢稿')
await back()
return
}
```
同一守卫在 `smallTitle.vue` 的 `getContents` 成功回调里也要加。
**验收**:P0-3 用例 3。
### P1-3 · 安卓端真实可跑性未验证
`tools/check-android-contract.js` 只证明**方法名对得上**,不证明能跑。
现状盘点(本轮查明):
- 工具链齐:JDK 17(`android-tools/jdk17/jdk-17.0.20+8`)、Gradle 8.7(`android-tools/gradle-8.7`)、Android SDK(`android-tools/sdk`,build-tools 34.0.0)
- AVD 存在:`dsh_api34` / `test_avd`
- `gradle projects` 已跑通(`Root project 'junge-liaoliang'` → `Project ':app'`)
- **但 `android/` 没有 `gradlew` wrapper**,只能用外部 gradle;`adb devices` 当前无设备
做法:
```bash
# 1) 起模拟器
cd C:/Users/12914/Desktop/t/android
export JAVA_HOME="C:/Users/12914/Desktop/android-tools/jdk17/jdk-17.0.20+8"
"/c/Users/12914/Desktop/android-tools/sdk/emulator/emulator.exe" -avd dsh_api34 &
"/c/Users/12914/Desktop/android-tools/sdk/platform-tools/adb.exe" devices
# 2) 构建 debug
"/c/Users/12914/Desktop/android-tools/gradle-8.7/bin/gradle.bat" :app:assembleDebug
# 3) 装并跑,对着 uniCloud 空间验证 getPublishedArticles 真能返回数据
```
**先确认 `UNICLOUD_BASE_URL`**:`android/app/build.gradle.kts:21` 当前是
`https://env-00jxt5y29hrd.dev-hz.cloudbasefunction.cn`——需确认这是控制台里
真实开通 URL 化的域名,否则所有接口都会失败。
**另一个要留意的集成点**:`uni-cms-articles.schema.ext.js` 的 `afterRead` 开头有
```js
if (!config.clientAppIds || !config.clientAppIds.length) throw new Error(...)
if (!config.clientAppIds.includes(clientInfo.appId)) return // ← 静默跳过
```
当前 `uni-cms-config/uni-cms/config.json` 里 `clientAppIds = ["__UNI__EF95AB3"]`,
与 `manifest.json` 的 appid 一致,所以小程序/H5 正常。
但从安卓经 URL 化网关调用时 `clientInfo.appId` 很可能是空值 →
`includes(undefined)` 为 false → **`afterRead` 直接 return,整段内容后处理被跳过**
(解锁内容、`content_images`、`view_count` 自增都不会执行)。
对 `getPublishedArticles` 无影响(它本来就不取 `content`)。
但要确认 `article_info.getDetail` 从安卓调用时返回的 `content` 是否仍是**未处理的原始 Delta**——
如果是,安卓端要么自己处理,要么得把安卓的 appId 加进 `clientAppIds`。
这是 P1-3 真跑时要专门看一眼的点。
### P2-1 · 编码类警告(独立问题,别混进文章链路提交)
| 文件 | 问题 | 处理 |
|---|---|---|
| `hx_page.html` | UTF-8 BOM | 已在 `.gitignore`(构建产物)→ 给 audit 加 ignore 白名单 |
| `uni_modules/uni-pagination/components/uni-pagination/i18n/es.json` | 非法 UTF-8(GBK/二次编码损坏) | 独立修:从 `git show HEAD:<path>` 取原文重存 UTF-8 |
### P2-2 · `check-vue.js` 49 条警告 = 全部不在范围内
分布:`uni_modules` 36 / `components` 12 / `pages3` 1。
全是第三方或 Vue2 遗留写法(`beforeDestroy`、`filters`、`this.$set`、`<script lang="uts">`)。
**结论:不改**。顺手改会引入回归,且会让文章闭环的提交 diff 失焦。登记为「既有基线」。
同理 `components/thorui/**` 的 `this.$set`(12 处)也不要动。
---
## C. 建议执行顺序
```
P0-1 提交工作区 ← 先做,锁住本轮成果
↓
P0-2 抽纯函数 + 补断言 ← 纯本地,可离线
↓
P1-2 编辑器空覆盖守卫 ← 小改动,配合 P0-2 一起测
↓
P0-3 端到端 QA ← 需要 HBuilderX + 服务空间,真正验收
↓
P1-3 安卓端真跑
↓
P1-1 审核链路真跑
↓
P2 编码/警告清理(可选)
```
---
## D. 验收标准
1. ✅ 新建 mobile 文章:标题+文字+小标题+图片+视频+投票,能存草稿、重开、预览、发布
2. ✅ 发布后详情所有块可见,文字/小标题不消失,视频可播,投票可提交
3. ✅ 编辑旧文章的首个文字块、视频说明、封面、模板、音乐后,其他字段不丢
4. ✅ 上传失败旧媒体保留;普通作者只能处理自己文章的媒体
5. ✅ 匿名用户只能看已发布文章,不能按已知 ID 读草稿
6. ✅ H5/App/微信小程序不因 `wx`、Vue2 `$set`、非法单位、空图片地址崩溃
7. ✅ 现有 PC/标准 `content` 文章继续走官方 renderer,不被 mobile 逻辑破坏
8. ✅ `node tools/build.js -p h5` 与 `-p mp-weixin` 均产出完整产物
9. ✅ `node tools/verify.js` 全绿(4 自测 + 4 检查 + 13 数据项)
10. ⬜ `check-article-flow.js` 断言组数 ≥10,覆盖 a–g(**待做**)
11. ⬜ 工作区已提交(**待做**)
12. ⬜ 安卓端在模拟器上真实跑通首页文章流(**待做**)
---
## E. 环境备忘(本轮实测)
| 项 | 值 |
|---|---|
| Node | v24.19.0(跑 `tools/*.js` 可以;**跑 vite 构建不行**,要用 HBuilderX 的 node) |
| HBuilderX 构建 node | `C:/Program Files/HBuilderX/HBuilderX/plugins/node/node.exe` (v22.22.2) |
| HBuilderX | `C:/Program Files/HBuilderX/HBuilderX`(编译器 5.15,Vue3) |
| JDK | `C:/Users/12914/Desktop/android-tools/jdk17/jdk-17.0.20+8` |
| Gradle | `C:/Users/12914/Desktop/android-tools/gradle-8.7/bin/gradle.bat` |
| Android SDK | `C:/Users/12914/Desktop/android-tools/sdk`(build-tools 34.0.0,AVD `dsh_api34`) |
| 当前分支 | `test`(主分支 `main`) |
| `.zcode/` | 工具链运行产物,已 ignore |
| 端口 | 5173 被**别的项目**(english-drill)占用;adb 用 5037 |
**不要做的事**:
- 不要在项目根跑 `npm install`——会按 Vue2 时代的 `package.json` 剪掉联接,
需要重跑 `node tools/build.js --link-only` 恢复(本项目是 HBuilderX 工程,不是标准 CLI 工程)
- 不要改 `vite.config.js`(HBuilderX 需要它指向 HBX 工具链)
- 不要动 `unpackage/`(构建产物,已 ignore)