From 736390e694002cfd0760f67f99670118db8b75a4 Mon Sep 17 00:00:00 2001 From: zyh Date: Thu, 7 May 2026 14:32:34 +0800 Subject: [PATCH] =?UTF-8?q?sync:=20=E6=96=B0=E5=A2=9E=20d8d-weapp-publish?= =?UTF-8?q?=20=E6=8A=80=E8=83=BD=E5=92=8C=20uni-app-compatibility=20?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../d8d-dev/rules/uni-app-compatibility.md | 207 +++++++++++ .claude/skills/d8d-weapp-publish/SKILL.md | 89 +++++ .claude/skills/d8d-weapp-publish/package.json | 9 + .../d8d-weapp-publish/references/config.md | 185 ++++++++++ .../d8d-weapp-publish/references/faq.md | 100 ++++++ .../d8d-weapp-publish/references/usage.md | 69 ++++ .../scripts/publish-weapp.js | 330 ++++++++++++++++++ 7 files changed, 989 insertions(+) create mode 100644 .claude/skills/d8d-dev/rules/uni-app-compatibility.md create mode 100644 .claude/skills/d8d-weapp-publish/SKILL.md create mode 100644 .claude/skills/d8d-weapp-publish/package.json create mode 100644 .claude/skills/d8d-weapp-publish/references/config.md create mode 100644 .claude/skills/d8d-weapp-publish/references/faq.md create mode 100644 .claude/skills/d8d-weapp-publish/references/usage.md create mode 100644 .claude/skills/d8d-weapp-publish/scripts/publish-weapp.js diff --git a/.claude/skills/d8d-dev/rules/uni-app-compatibility.md b/.claude/skills/d8d-dev/rules/uni-app-compatibility.md new file mode 100644 index 0000000..73ecc12 --- /dev/null +++ b/.claude/skills/d8d-dev/rules/uni-app-compatibility.md @@ -0,0 +1,207 @@ +# uni-app 兼容性检查铁律 + +## 规则 1: 自定义导航栏胶囊检测 + +**问题模式**: 扫描所有 `Navigation*.vue` / `NavBar*.vue` / `CustomHeader*.vue` 组件 + +**检查项**: +1. ✅ **生命周期正确性**: 必须使用 `onReady` (uni-app),禁止使用 `onMounted` +2. ✅ **跨平台检测**: 必须包含 `systemInfo.platform === 'h5'` 环境判断 +3. ✅ **胶囊 API 调用**: 必须调用 `uni.getMenuButtonBoundingClientRect()` +4. ✅ **动态 padding**: 必须根据胶囊 `left` 位置计算 `padding-right` +5. ✅ **动态高度**: 必须根据胶囊 `bottom` 位置计算导航栏高度 + +**自动修复模板(检测到缺失时应用)**: +```typescript +import { ref } from "vue" +import { onReady } from "@dcloudio/uni-app" + +const navBarHeight = ref(64) +const statusBarHeight = ref(0) +const rightPadding = ref(24) + +onReady(() => { + const systemInfo = uni.getSystemInfoSync() + statusBarHeight.value = systemInfo.statusBarHeight || 0 + + // H5 环境下跳过胶囊适配 + if (systemInfo.platform === "h5") return + + // 微信小程序环境执行胶囊适配 + const menuButton = uni.getMenuButtonBoundingClientRect() + + // 计算导航栏总高度 = 胶囊底部 + 胶囊与状态栏间距 + navBarHeight.value = menuButton.bottom + (menuButton.top - statusBarHeight.value) + + // ⚠️ 重要:胶囊只在右侧,只需要给右侧加 padding,左侧固定 24px + // 左侧不要动态计算,否则整个内容会被推到右边! + rightPadding.value = systemInfo.windowWidth - menuButton.left + 12 +}) +``` + +**模板内联样式绑定(含 Flex 防挤压)**: +```vue + + + {{ logoText }} + + + + + +``` + +**配套 CSS 规范(必须同时应用)**: +```css +.nav-bar { + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; +} +.nav-left { + flex: 1; /* ⚠️ 关键:左侧占据剩余空间,防止被右侧挤压 */ +} +.nav-right { + flex-shrink: 0; /* ⚠️ 关键:右侧图标不被挤压 */ + display: flex; + align-items: center; + gap: 20px; +} +.logo-text { + white-space: nowrap; /* ⚠️ 关键:标题不换行竖排 */ +} +``` + +**违规后果**: +- 微信小程序环境下右侧图标被胶囊按钮遮挡 +- 用户无法点击搜索、通知等核心功能按钮 +- P0 级 UI 缺陷 + +--- + +## 规则 2: uni-app 生命周期误用检测 + +**检查范围**: 所有 Vue 组件文件 + +**禁止模式**: +- `onMounted` 中调用 `uni.*` API 进行布局计算 +- `onMounted` 中调用 `uni.createSelectorQuery()` +- `onMounted` 中获取元素位置尺寸 + +**推荐模式**: 使用 `onReady` 替代(uni-app 页面生命周期) + +**自动修复**: +1. 将 `onMounted` → `onReady` +2. 添加导入: `import { onReady } from '@dcloudio/uni-app'` +3. 移除错误的 `onMounted` 导入 + +**检测正则**: +``` +/onMounted\s*\(\s*\(\s*\)\s*=>\s*{[\s\S]*?uni\.(getSystemInfoSync|getMenuButtonBoundingClientRect|createSelectorQuery)/ +``` + +--- + +## 规则 3: pages.json 自定义导航栏自动检查 + +**触发条件**: 检测到 `"navigationStyle": "custom"` 配置 + +**自动执行**: +1. 扫描所有使用自定义导航栏的页面 +2. 检查每个页面引用的导航栏组件是否通过胶囊适配检测 +3. 为未通过检测的页面创建修复任务 + +**报告格式**: +``` +⚠️ 自定义导航栏胶囊适配检测报告 +-------------------------------- +页面 pages/home/index: ✅ 已适配 +页面 pages/square/index: ✅ 已适配 +页面 pages/profile/index: ✅ 已适配 +页面 pages/event-detail: ❌ 未适配 + - 缺失: uni.getMenuButtonBoundingClientRect() 调用 + - 建议: 应用规则 1 自动修复模板 +``` + +--- + +## 规则 4: H5/MP 条件编译检查 + +**场景**: 小程序专属 API 不应该在 H5 环境下直接调用 + +**检测项**: +- ❌ 直接调用 `uni.getMenuButtonBoundingClientRect()` 而没有平台检测 +- ❌ 直接调用 `wx.*` API 而没有条件编译包裹 +- ✅ 推荐: 先检测 platform,再执行平台专属代码 + +**安全模式**: +```typescript +// ✅ 正确 - 先检测平台 +const systemInfo = uni.getSystemInfoSync() +if (systemInfo.platform !== "h5") { + const menuButton = uni.getMenuButtonBoundingClientRect() + // ... +} +``` + +**违规自动修复**: 为缺失平台检测的 API 调用添加 `if` 包裹 + +--- + +## 规则 5: 导航栏 Flex 布局防挤压(新增) + +**问题描述**:左右 padding 很大时,如果没有 flex 布局控制,标题会被挤压得竖排显示,或者整体内容右移。 + +**检查项**: +1. ✅ `.nav-left` 必须有 `flex: 1` +2. ✅ `.nav-right` 必须有 `flex-shrink: 0` +3. ✅ 标题文字必须有 `white-space: nowrap` + +**修复优先级**:P0(用户直接感知的视觉缺陷) + +--- + +## 规则 6: wxss 标签名选择器禁用(新增) + +**问题描述**:微信小程序组件 wxss 不允许使用标签名选择器(如 `svg`, `view`, `text`),编译后样式不生效,控制台警告。 + +**禁止的选择器模式**: +```css +/* ❌ 禁止:标签名选择器 */ +.icon-item > view svg { ... } +.nav-left text { ... } +view { ... } +``` + +**✅ 正确写法**: +```css +/* 只使用类名选择器 */ +.icon-item > .icon-wrapper { ... } +.nav-left .title { ... } +``` + +**检测命令**: +```bash +grep -rn ">\s*svg\|>\s*view\|>\s*text" .claude/skills/d8d-dev/ app/mini-program/src/ --include="*.vue" --include="*.md" +``` + +--- + +## 规则 7: uni-app vs Vue 生命周期正确选择(新增) + +| 使用场景 | 正确生命周期 | 错误选择 | 原因 | +|---------|-------------|---------|------| +| 获取胶囊位置、元素尺寸 | `onReady` | `onMounted` | `onMounted` 时组件可能还没渲染到位 | +| 获取 API 数据(普通场景) | `onMounted` 或 `onReady` | - | 都可以,通常 onMounted 足够 | +| navigateBack 返回后需要刷新数据 | `onShow` | `onMounted` | `onMounted` 只在首次创建时触发 | + +**记忆口诀**:布局计算用 `onReady`,数据请求随便,返回刷新用 `onShow` diff --git a/.claude/skills/d8d-weapp-publish/SKILL.md b/.claude/skills/d8d-weapp-publish/SKILL.md new file mode 100644 index 0000000..72a1c5a --- /dev/null +++ b/.claude/skills/d8d-weapp-publish/SKILL.md @@ -0,0 +1,89 @@ +--- +name: d8d-weapp-publish +description: 微信小程序自动化发布工具。支持企业小程序和人才小程序的开发版和体验版发布。当用户需要:发布小程序到微信平台、上传体验版、生成开发版预览二维码、配置小程序发布环境时触发。使用 scripts/publish-weapp.js 脚本执行发布流程。 +--- + +# 微信小程序发布 + +支持两种框架:**Taro**(企业小程序、人才小程序)和 **uni-app**(酒吧组局小程序)。 + +- **企业小程序**(用人方)- Taro +- **人才小程序**(求职者)- Taro +- **酒吧组局小程序** - uni-app + +--- + +## 快速开始 + +### 发布命令 + +```bash +# 企业小程序 - 体验版上传 +pnpm run publish:enterprise + +# 企业小程序 - 开发版生成二维码 +pnpm run publish:enterprise:dev + +# 人才小程序 - 体验版上传 +pnpm run publish:talent + +# 人才小程序 - 开发版生成二维码 +pnpm run publish:talent:dev + +# 酒吧组局小程序 (uni-app) - 体验版上传 +pnpm run publish:hazy + +# 酒吧组局小程序 (uni-app) - 开发版生成二维码 +pnpm run publish:hazy:dev +``` + +### 自定义版本和描述 + +```bash +VERSION=1.2.0 DESC="新增功能" pnpm run publish:enterprise +``` + +--- + +## 发布流程 + +### 自动构建说明 + +**重要**: 开发版预览会**自动重新构建** `development` 环境,确保使用最新代码。 + +**为什么开发版需要重新构建?** + +微信小程序开发版预览有 **2MB 大小限制**,而使用 `pnpm run dev:weapp`(watch 模式)生成的开发版输出体积较大(通常超过 2MB),会导致上传失败: + +``` +errcode: 80051, errmsg: source size 2368KB exceed max limit 2MB +``` + +因此开发版预览必须使用 `NODE_ENV=development` 进行完整构建,这样可以: +- 控制输出大小在限制内(约 1.3MB) +- 确保使用最新代码 +- 避免因 watch 模式输出过大导致上传失败 + +**构建环境说明**: +- **开发版预览**: 使用 `NODE_ENV=development` 构建,输出到 `dist/weapp/development/` +- **体验版**: 使用 `NODE_ENV=production` 构建,输出到 `dist/weapp/production/` + +如需跳过构建(使用已有构建输出),可添加 `--no-build` 参数: + +```bash +node .claude/skills/d8d-weapp-publish/scripts/publish-weapp.js enterprise dev --no-build +``` + +### 发布步骤 + +1. 构建项目(Taro 编译) +2. 上传到微信服务器 +3. 生成体验版或预览二维码 + +--- + +## 参考文档 + +- **配置说明**: [config.md](references/config.md) - 小程序信息、package.json 配置、环境变量 +- **常见问题**: [faq.md](references/faq.md) - 故障排查、注意事项 +- **使用示例**: [usage.md](references/usage.md) - 完整使用示例、快捷命令 diff --git a/.claude/skills/d8d-weapp-publish/package.json b/.claude/skills/d8d-weapp-publish/package.json new file mode 100644 index 0000000..89ee0eb --- /dev/null +++ b/.claude/skills/d8d-weapp-publish/package.json @@ -0,0 +1,9 @@ +{ + "name": "weapp-publish-skill", + "version": "1.0.0", + "description": "WeChat mini-program publishing skill", + "type": "commonjs", + "dependencies": { + "miniprogram-ci": "^2.1.26" + } +} diff --git a/.claude/skills/d8d-weapp-publish/references/config.md b/.claude/skills/d8d-weapp-publish/references/config.md new file mode 100644 index 0000000..a0b9e66 --- /dev/null +++ b/.claude/skills/d8d-weapp-publish/references/config.md @@ -0,0 +1,185 @@ +# 配置参考 + +## 支持的小程序 + +| 小程序 | 目录 | AppID | 说明 | +|--------|------|-------|------| +| **企业小程序** | `mini/` | `wx1e791ed2e0229eb8` | 用人方小程序(企业管理端) | +| **人才小程序** | `mini-talent/` | `wx3c47dbce1ea7d43c` | 人才小程序(求职者端) | + +--- + +## 技能架构 + +``` +.claude/skills/weapp-publish/ +├── package.json # 技能依赖 +├── node_modules/ # 技能依赖安装目录 +├── SKILL.md # 技能说明 +├── scripts/ +│ └── publish-weapp.js # 发布脚本 +└── references/ # 参考文档 + ├── config.md + ├── faq.md + └── usage.md +``` + +**技能是自包含的**,有自己的依赖管理。 + +--- + +## 安装技能依赖 + +首次使用或更新依赖时,在技能目录安装: + +```bash +cd .claude/skills/weapp-publish +pnpm install +``` + +--- + +## package.json 配置 + +项目根目录 `package.json` 中的发布命令: + +```json +{ + "scripts": { + "publish:enterprise": "node .claude/skills/weapp-publish/scripts/publish-weapp.js enterprise experience", + "publish:enterprise:dev": "node .claude/skills/weapp-publish/scripts/publish-weapp.js enterprise dev", + "publish:talent": "node .claude/skills/weapp-publish/scripts/publish-weapp.js talent experience", + "publish:talent:dev": "node .claude/skills/weapp-publish/scripts/publish-weapp.js talent dev" + } +} +``` + +--- + +## 命令行参数 + +### 发布类型 (action) + +| 参数 | 说明 | 构建行为 | 输出路径 | +|------|------|----------|----------| +| `experience` | 体验版(默认) | 默认执行构建 | `dist/weapp/production/` | +| `dev` | 开发版 | 默认跳过构建 | `dist/weapp/development/` | + +### 构建控制参数 + +| 参数 | 说明 | +|------|------| +| `--build` | 强制执行构建 | +| `--no-build` | 跳过构建 | + +### 使用示例 + +```bash +# 开发版使用 watch 模式输出(快速) +node scripts/publish-weapp.js enterprise dev + +# 开发版强制重新构建 +node scripts/publish-weapp.js enterprise dev --build + +# 体验版跳过构建(不推荐) +node scripts/publish-weapp.js enterprise experience --no-build +``` + +--- + +## 环境变量配置 + +发布时可通过环境变量自定义: + +```bash +# 发布版本号 +VERSION=1.2.0 + +# 发布描述 +DESC=新增订单管理功能 + +# CI 机器人编号 (1-30) +ROBOT=1 + +# 公网访问域名(可选,用于预览二维码 URL) +WEB_HOST=example.com +``` + +使用方式: +```bash +VERSION=1.2.0 DESC="新增功能" pnpm run publish:enterprise +``` + +--- + +## 输出路径说明 + +发布脚本根据类型自动选择输出路径: + +| 类型 | 环境变量 | 输出目录 | 用途 | +|------|----------|----------|------| +| **体验版** | `NODE_ENV=production` | `dist/weapp/production/` | 提交审核/体验版 | +| **开发版** | `NODE_ENV=development` | `dist/weapp/development/` | 开发调试 | + +开发版使用 `.env.development` 中的 API 配置,体验版使用 `.env.production`。 + +--- + +## 发布前准备 + +### 1. 安装技能依赖 + +```bash +cd .claude/skills/weapp-publish +pnpm install +``` + +### 2. 获取上传密钥 + +使用小程序管理员身份访问"微信公众平台 > 开发 > 开发设置",下载代码上传密钥。 + +**企业小程序密钥文件**: `mini/certs/private.upload.key` +**人才小程序密钥文件**: `mini-talent/certs/private.upload.key` + +> 密钥文件已存在于各自小程序目录的 `certs/` 目录下。 + +### 3. 配置 IP 白名单 + +在微信公众平台配置服务器的 IP 白名单。 + +### 4. 确保 ES5 转译配置 + +小程序的 `config/dev.ts` 中应包含以下配置,确保所有依赖被转译为 ES5: + +```ts +mini: { + compile: { + include: [ + // 确保产物为 es5,转译所有 node_modules(除了 Babel 核心) + filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename) + ] + } +} +``` + +**原因**:微信小程序的预览编译器不完全支持 ES6+ 语法(如可选链 `?.`),需要确保所有代码被转译为 ES5。 + +### 5. 开发版预览 - 关闭 Prebundle(重要) + +微信小程序开发版预览有 **2MB 主包大小限制**,Taro 在开发模式默认开启 prebundle(依赖预编译),会导致主包超过限制。 + +在 `config/dev.ts` 中添加以下配置关闭 prebundle: + +```ts +compiler: { + // 关闭开发模式的依赖预编译,减少主包体积以支持微信小程序开发版预览(2MB限制) + prebundle: { + enable: false + } +} +``` + +**注意**: +- 此配置仅影响开发版预览,不影响体验版和生产版 +- 关闭 prebundle 后首次编译会稍慢,但主包体积可从 ~3MB 降至 ~650KB +- 人才小程序 (`mini-talent`) 也需要同样配置 diff --git a/.claude/skills/d8d-weapp-publish/references/faq.md b/.claude/skills/d8d-weapp-publish/references/faq.md new file mode 100644 index 0000000..8e6a2db --- /dev/null +++ b/.claude/skills/d8d-weapp-publish/references/faq.md @@ -0,0 +1,100 @@ +# 常见问题 + +### Q: 发布失败提示 IP 不在白名单 + +**A**: 请在微信公众平台"开发 > 开发设置"中添加服务器 IP 到白名单。 + +### Q: 构建成功但发布失败 + +**A**: 检查密钥文件路径是否正确,密钥是否匹配对应的 AppID。 + +### Q: 构建时提示找不到模块(如 `tailwindcss` 或 `@/` 路径别名) + +**A**: **此问题已在脚本中修复**(2025-03-18)。如果仍然遇到,请使用 `--no-build` 参数作为备用方案。 + +**备用方案**: +```bash +# 1. 进入小程序目录手动构建 +cd mini +NODE_ENV=production pnpm taro build --type weapp + +# 2. 返回根目录,使用 --no-build 发布 +cd .. +pnpm run publish:enterprise --no-build +``` + +### Q: 如何设置体验版 + +**A**: 发布成功后,登录小程序后台"版本管理",将指定版本设置为体验版。 + +### Q: 开发版预览二维码生成失败,提示语法错误 + +**A**: 这通常是因为代码中包含了微信小程序不支持的 ES6+ 语法(如可选链 `?.`)。 + +**解决方案**: +1. 确保小程序的 `config/dev.ts` 中配置了 ES5 转译: + ```ts + mini: { + compile: { + include: [ + filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename) + ] + } + } + ``` +2. 使用 `--build` 参数强制重新构建 + +### Q: 开发版为什么默认跳过构建? + +**A**: 开发版默认使用 `dev:weapp` (watch 模式) 的构建输出,因为: +1. 开发时通常会运行 watch 模式,代码实时编译 +2. 跳过构建可以快速生成预览二维码,提高开发效率 +3. 如需重新构建,添加 `--build` 参数 + +### Q: 体验版和开发版有什么区别? + +**A**: +| 类型 | 用途 | 构建行为 | 输出路径 | +|------|------|----------|----------| +| **体验版** (experience) | 提交给测试人员体验 | 默认执行构建 | `dist/weapp/production/` | +| **开发版** (dev) | 开发者自测 | 默认跳过构建 | `dist/weapp/development/` | + +### Q: 开发版预览上传失败,提示超过 2MB 限制 + +**A**: 微信小程序开发版预览有严格的 **2MB 主包大小限制**,这是微信服务器端的硬性限制,无法通过配置参数绕过。 + +**根本原因**: +1. `maxUploadSourceSize` **不是** miniprogram-ci 的有效参数(只在 `project.config.json` 中对本地开发者工具有效) +2. Taro 在开发模式默认开启 **prebundle(依赖预编译)**,预编译的依赖包会占用约 2MB 空间 + +**解决方案**:关闭开发模式的 prebundle 功能 + +在 `mini/config/dev.ts` 中添加: +```typescript +compiler: { + // 关闭开发模式的依赖预编译,减少主包体积以支持微信小程序开发版预览(2MB限制) + prebundle: { + enable: false + } +} +``` + +**优化效果**: +| 优化前 | 优化后 | 减少 | +|--------|--------|------| +| ~3MB | ~650KB | ~78% | + +**注意**: +- 关闭 prebundle 后首次编译会稍慢(需要完整编译依赖) +- 不影响体验版和生产版(这些版本本身就需要完整的依赖编译) + +--- + +## 注意事项 + +1. **密钥安全**: 上传密钥拥有预览、上传权限,请勿提交到代码仓库 +2. **构建依赖**: 发布前确保项目依赖已安装 (`pnpm install`) +3. **版本管理**: 建议使用语义化版本号 (Semantic Versioning) +4. **IP 白名单**: 确保服务器 IP 已在微信公众平台配置白名单 +5. **机器人编号**: 多个 CI 并发时使用不同的 robot 编号 (1-30) +6. **开发模式**: 开发版预览前请确保 `dev:weapp` 正在运行 diff --git a/.claude/skills/d8d-weapp-publish/references/usage.md b/.claude/skills/d8d-weapp-publish/references/usage.md new file mode 100644 index 0000000..e463765 --- /dev/null +++ b/.claude/skills/d8d-weapp-publish/references/usage.md @@ -0,0 +1,69 @@ +# 使用示例 + +## 发布企业小程序 + +```bash +# 使用默认版本号和描述(体验版) +pnpm run publish:enterprise + +# 自定义版本号和描述 +VERSION=1.2.0 DESC="新增订单管理功能" pnpm run publish:enterprise + +# 生成预览二维码(默认跳过构建,使用 watch 模式输出) +pnpm run publish:enterprise:dev + +# 强制重新构建后生成预览二维码 +pnpm run publish:enterprise:dev --build +``` + +## 发布人才小程序 + +```bash +# 使用默认版本号和描述(体验版) +pnpm run publish:talent + +# 自定义版本号和描述 +VERSION=2.0.0 DESC="全新改版,优化求职流程" pnpm run publish:talent + +# 生成预览二维码(默认跳过构建,使用 watch 模式输出) +pnpm run publish:talent:dev + +# 强制重新构建后生成预览二维码 +pnpm run publish:talent:dev --build +``` + +--- + +## 构建行为说明 + +### 开发版 (dev) +- **默认跳过构建**:直接使用 `dev:weapp` (watch 模式) 的输出 +- 适用场景:本地开发时快速生成预览二维码 +- 强制构建:添加 `--build` 参数 + +### 体验版 (experience) +- **默认执行构建**:确保发布的是最新代码 +- 跳过构建:添加 `--no-build` 参数(适用于已手动构建的情况) + +### 输出路径 +- 开发版:`dist/weapp/development/` +- 体验版:`dist/weapp/production/` + +--- + +## 快捷命令参考 + +| 用户指令 | Claude 行为 | +|----------|-------------| +| "发布企业小程序" | 运行 `pnpm run publish:enterprise` | +| "发布人才小程序" | 运行 `pnpm run publish:talent` | +| "生成企业小程序预览二维码" | 运行 `pnpm run publish:enterprise:dev` | +| "发布两个小程序" | 依次运行企业小程序和人才小程序的发布命令 | + +--- + +## 相关资源 + +- [miniprogram-ci 官方文档](https://www.npmjs.com/package/miniprogram-ci) +- [微信公众平台](https://mp.weixin.qq.com/) +- [miniprogram-ci Skill](.claude/skills/miniprogram-ci/SKILL.md) diff --git a/.claude/skills/d8d-weapp-publish/scripts/publish-weapp.js b/.claude/skills/d8d-weapp-publish/scripts/publish-weapp.js new file mode 100644 index 0000000..63af3c8 --- /dev/null +++ b/.claude/skills/d8d-weapp-publish/scripts/publish-weapp.js @@ -0,0 +1,330 @@ +const ci = require('miniprogram-ci') +const path = require('path') +const { execSync } = require('child_process') +const fs = require('fs') + +// 公网访问域名(从环境变量读取,默认本地) +const WEB_HOST = process.env.WEB_HOST || 'localhost:8080' +const PUBLIC_HOST = WEB_HOST.startsWith('http') ? WEB_HOST : `https://${WEB_HOST}` + +// 项目根目录(从 scripts/ 目录向上 4 级) +const PROJECT_ROOT = path.resolve(__dirname, '../../../../') + +// 小程序配置基础信息 +const MINI_CONFIGS = { + enterprise: { + name: '企业小程序', + framework: 'taro', + appid: 'wx1e791ed2e0229eb8', + dir: 'mini', + privateKeyPath: path.resolve(PROJECT_ROOT, 'mini/certs/private.upload.key'), + distPath: 'dist/weapp', + }, + talent: { + name: '人才小程序', + framework: 'taro', + appid: 'wx3c47dbce1ea7d43c', + dir: 'mini-talent', + privateKeyPath: path.resolve(PROJECT_ROOT, 'mini-talent/certs/private.upload.key'), + distPath: 'dist/weapp', + }, + hazy: { + name: '酒吧组局', + framework: 'uniapp', + appid: process.env.WECHAT_APPID || '', + dir: 'app/mini-program', + privateKeyPath: path.resolve(PROJECT_ROOT, 'app/mini-program/certs/private.upload.key'), + distPath: 'dist/build/mp-weixin', + }, +} + +/** + * 获取构建配置(根据 action 类型动态选择) + * + * 说明: + * - dev (开发版预览): 使用 development 模式,自动构建 + * - experience (体验版): 使用 production 模式,自动构建(体验版=生产环境) + * - production (正式版): 使用 production 模式,自动构建 + * + * 目录结构:dist/weapp/{mode}/ + */ +function getBuildConfig(config, action) { + // 只有开发版使用 development 模式 + // 体验版和正式版都使用 production 模式 + const mode = action === 'dev' ? 'development' : 'production' + + // 根据模式设置对应的 NODE_ENV + // 这样构建输出目录和上传目录就能匹配了 + const nodeEnv = mode === 'development' ? 'development' : 'production' + // 小程序目录的绝对路径,用于 execSync 的 cwd 参数 + const miniDir = path.resolve(PROJECT_ROOT, config.dir) + + // 根据框架选择不同的构建命令和输出路径 + let buildCmd, projectPath + + if (config.framework === 'uniapp') { + // uni-app: 固定输出路径,不区分环境目录 + buildCmd = `NODE_ENV=${nodeEnv} pnpm build:mp-weixin` + projectPath = path.resolve(PROJECT_ROOT, `${config.dir}/${config.distPath}`) + } else { + // Taro: 按环境分目录输出 + buildCmd = `NODE_ENV=${nodeEnv} pnpm taro build --type weapp` + projectPath = path.resolve(PROJECT_ROOT, `${config.dir}/${config.distPath}/${mode}`) + } + + return { + ...config, + mode, + nodeEnv, + miniDir, + projectPath, + buildCmd, + } +} + +/** + * 构建小程序 + */ +function buildMiniProject(config) { + console.log(`\n🔨 正在构建 ${config.name}...`) + console.log(` 工作目录: ${config.miniDir}`) + console.log(` 环境变量: NODE_ENV=${config.nodeEnv}`) + try { + execSync(config.buildCmd, { stdio: 'inherit', cwd: config.miniDir, shell: true }) + console.log(`✅ ${config.name} 构建完成`) + } catch (error) { + console.error(`❌ ${config.name} 构建失败`) + throw error + } +} + +/** + * 发布小程序体验版到微信服务器 + */ +async function uploadExperienceProject(config, options) { + const { version, desc, robot = 1 } = options + + console.log(`\n📤 正在发布体验版 ${config.name}...`) + console.log(` 版本: ${version}`) + console.log(` 描述: ${desc}`) + + // 更新 project.config.json 中的版本号(确保微信后台正确显示) + const projectConfigPath = path.join(config.projectPath, 'project.config.json') + if (fs.existsSync(projectConfigPath)) { + const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf-8')) + projectConfig.version = version + fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2)) + console.log(` 已更新 project.config.json 版本号为: ${version}`) + } + + const project = new ci.Project({ + appid: config.appid, + type: 'miniProgram', + projectPath: config.projectPath, + privateKeyPath: config.privateKeyPath, + ignores: ['node_modules/**/*'], + }) + + try { + const result = await ci.upload({ + project, + version, + desc, + setting: { + useProjectConfig: true, + es7: true, + minify: true, + minifyJS: true, + minifyWXML: true, + minifyWXSS: true, + autoPrefixWXSS: true, + }, + onProgressUpdate: (progress) => { + process.stdout.write(`\r 上传进度: ${progress}%`) + }, + robot, + }) + + console.log(`\n✅ ${config.name}体验版上传成功!`) + console.log(` 时间: ${result.time}`) + console.log(` 版本: ${result.version}`) + console.log(` 请前往小程序后台查看: https://mp.weixin.qq.com/`) + } catch (error) { + console.error(`\n❌ ${config.name}体验版上传失败`) + console.error(error.message) + throw error + } +} + +/** + * 预览小程序(生成二维码) + */ +async function previewDevProject(config, options) { + const { desc, robot = 1 } = options + + console.log(`\n👀 正在生成 ${config.name} 预览二维码...`) + + const project = new ci.Project({ + appid: config.appid, + type: 'miniProgram', + projectPath: config.projectPath, + privateKeyPath: config.privateKeyPath, + ignores: ['node_modules/**/*'], + }) + + // 二维码保存目录 + const qrcodeDir = path.resolve(PROJECT_ROOT, 'web/public/qrcode') + const qrcodeFileName = `qrcode-${config.appid}.jpg` + const qrcodePath = path.join(qrcodeDir, qrcodeFileName) + + // 确保 qrcode 目录存在 + if (!fs.existsSync(qrcodeDir)) { + fs.mkdirSync(qrcodeDir, { recursive: true }) + } + + try { + await ci.preview({ + project, + desc, + setting: { + useProjectConfig: false, // 不使用项目配置,完全使用下方自定义设置 + es6: false, + es7: false, // 增强编译 + minify: true, // 压缩所有代码 + minifyWXSS: true, // 压缩样式 + minifyWXML: true, // 压缩模板 + minifyJS: true, // 压缩JS + codeProtect: false, + autoPrefixWXSS: false, + }, + qrcodeFormat: 'image', + qrcodeOutputDest: qrcodePath, + onProgressUpdate: (progress) => { + process.stdout.write(`\r 生成进度: ${progress}%`) + }, + robot, + uploadType: 'preview', + }) + + // 公网访问 URL + const publicUrl = `${PUBLIC_HOST}/qrcode/${qrcodeFileName}` + + console.log(`\n✅ 开发版预览二维码已生成!`) + console.log(`📱 本地路径: ${qrcodePath}`) + console.log(`🌐 公网访问: ${publicUrl}`) + console.log(`\n>>>QRCode_IMAGE_START<<<`) + console.log(qrcodePath) + console.log(`>>>QRCode_URL_START<<<`) + console.log(publicUrl) + console.log(`>>>QRCode_URL_END<<<`) + } catch (error) { + console.error(`\n❌ 开发版预览二维码生成失败`) + console.error(error.message) + throw error + } +} + +/** + * 递增版本号 (例如: 0.0.39 -> 0.0.40) + */ +function bumpVersion(version) { + const parts = version.split('.') + const patch = parseInt(parts[2] || '0') + parts[2] = (patch + 1).toString() + return parts.join('.') +} + +/** + * 从 package.json 读取当前版本号 + */ +function getCurrentVersion(config) { + const packageJsonPath = path.join(PROJECT_ROOT, config.dir, 'package.json') + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + return packageJson.version || '1.0.0' + } + return '1.0.0' +} + +/** + * 更新 package.json 中的版本号 + */ +function updatePackageVersion(config, version) { + const packageJsonPath = path.join(PROJECT_ROOT, config.dir, 'package.json') + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) + packageJson.version = version + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)) + console.log(` 已更新 package.json 版本号为: ${version}`) + } +} + +/** + * 主函数 + */ +async function main() { + const args = process.argv.slice(2) + + // --no-build 参数可以跳过构建(默认会自动构建) + const noBuildIndex = args.indexOf('--no-build') + const noBuild = noBuildIndex !== -1 + if (noBuild) { + args.splice(noBuildIndex, 1) + } + + const [miniType, action = 'experience'] = args + + // 只有指定 --no-build 时才跳过构建 + const shouldSkipBuild = noBuild + + // 获取构建配置(需要提前获取以便读取版本号) + const config = getBuildConfig(MINI_CONFIGS[miniType], action) + + // 解析参数 - 体验版如果没有指定 VERSION,则自动递增版本号 + let version = process.env.VERSION + if (action === 'experience' && !version) { + const currentVersion = getCurrentVersion(config) + version = bumpVersion(currentVersion) + console.log(`\n🔢 自动递增版本号: ${currentVersion} -> ${version}`) + updatePackageVersion(config, version) + } + + const options = { + version: version || '1.0.0', + desc: process.env.DESC || '自动化发布', + robot: parseInt(process.env.ROBOT || '1'), + } + + if (!miniType || !MINI_CONFIGS[miniType]) { + console.error('❌ 请指定小程序类型: enterprise 或 talent') + console.error('示例: node scripts/publish-weapp.js enterprise') + process.exit(1) + } + + try { + // 构建项目(跳过或显式指定构建时) + if (!shouldSkipBuild && !noBuild) { + buildMiniProject(config) + } else { + console.log(`\n⏭️ 跳过构建,使用现有构建输出`) + console.log(` 使用路径: ${config.projectPath}`) + } + + // 执行操作 + if (action === 'experience') { + await uploadExperienceProject(config, options) + } else if (action === 'dev') { + await previewDevProject(config, { + desc: options.desc, + robot: options.robot, + }) + } else { + console.error(`❌ 操作无效: ${action}`) + process.exit(1) + } + } catch (_error) { + process.exit(1) + } +} + +main() -- 2.49.1