构建: - 新增 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 构建失败
22 KiB
下一步计划 · 文章闭环收尾 + 工具链打通
交接对象:下一个接手的模型 / 工程师。 本文档分两部分: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/没有gradlewwrapper,只能用外部 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. 验收标准
- ✅ 新建 mobile 文章:标题+文字+小标题+图片+视频+投票,能存草稿、重开、预览、发布
- ✅ 发布后详情所有块可见,文字/小标题不消失,视频可播,投票可提交
- ✅ 编辑旧文章的首个文字块、视频说明、封面、模板、音乐后,其他字段不丢
- ✅ 上传失败旧媒体保留;普通作者只能处理自己文章的媒体
- ✅ 匿名用户只能看已发布文章,不能按已知 ID 读草稿
- ✅ H5/App/微信小程序不因
wx、Vue2$set、非法单位、空图片地址崩溃 - ✅ 现有 PC/标准
content文章继续走官方 renderer,不被 mobile 逻辑破坏 - ✅
node tools/build.js -p h5与-p mp-weixin均产出完整产物 - ✅
node tools/verify.js全绿(4 自测 + 4 检查 + 13 数据项) - ⬜
check-article-flow.js断言组数 ≥10,覆盖 a–g(待做) - ⬜ 工作区已提交(待做)
- ⬜ 安卓端在模拟器上真实跑通首页文章流(待做)
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)