Files
t/NEXT-STEPS.md
root 3117146281 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 构建失败
2026-09-12 01:02:45 +08:00

22 KiB
Raw Permalink Blame History

下一步计划 · 文章闭环收尾 + 工具链打通

交接对象:下一个接手的模型 / 工程师。 本文档分两部分: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 个新源文件全裸奔在工作区,一次误操作就全丢。

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」时会用空串把原文字抹掉。

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 当前无设备

做法:

# 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 开头有

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)