diff --git a/.cursor/agent/开发助理/evolution/2026-03-21-MBTI头像C端全链路兜底.md b/.cursor/agent/开发助理/evolution/2026-03-21-MBTI头像C端全链路兜底.md
new file mode 100644
index 00000000..173f3477
--- /dev/null
+++ b/.cursor/agent/开发助理/evolution/2026-03-21-MBTI头像C端全链路兜底.md
@@ -0,0 +1,13 @@
+# 2026-03-21 MBTI 头像 C 端全链路兜底
+
+## 问题
+系统设置瘦身与 MBTI 映射迁到用户管理后,需在小程序多页面与匹配接口统一「无微信头像 → MBTI 映射」行为,避免仅海报单点生效。
+
+## 做法
+- 新增 `miniprogram/utils/mbtiAvatar.js`(`resolveAvatarWithMbti`);`app.resolveAvatarWithMbti` 封装全局 map。
+- 我的页 `profileAvatarDisplay`;资料编辑 `avatarPreviewUrl`;profile-show、member-detail、referral 海报复用同一逻辑。
+- 后端 `match.go`:`avatar` 为空时用 `getMbtiAvatar`;响应增加 `mbti` 字段;找伙伴卡片 wxml 增加无图占位。
+- 管理端 `MbtiAvatarsManager` 补充 downloadFile 域名说明。
+
+## 可复用规则
+配置驱动展示:公开 `GET /api/miniprogram/config/mbti-avatars` + 本地短时缓存;业务侧只调 `resolveAvatarWithMbti`,避免重复拼接 baseUrl。
diff --git a/.cursor/agent/开发助理/evolution/索引.md b/.cursor/agent/开发助理/evolution/索引.md
index 39773d58..7dfccffb 100644
--- a/.cursor/agent/开发助理/evolution/索引.md
+++ b/.cursor/agent/开发助理/evolution/索引.md
@@ -4,4 +4,5 @@
| 日期 | 摘要 | 文件 |
|------|------|------|
+| 2026-03-21 | MBTI 头像小程序全链路兜底 + 匹配接口回填 | 2026-03-21-MBTI头像C端全链路兜底.md |
| 2026-03-16 | 用户交互习惯分析(基于 agent-transcripts) | 2026-03-16-交互习惯分析.md |
diff --git a/.cursor/skills/karuo-party/SKILL.md b/.cursor/skills/karuo-party/SKILL.md
index 83681e0b..98f2fd8d 100644
--- a/.cursor/skills/karuo-party/SKILL.md
+++ b/.cursor/skills/karuo-party/SKILL.md
@@ -7,8 +7,8 @@ description: >
triggers: 运营报表、视频切片、多平台分发、飞书视频下载、派对运营、卡若创业派对、派对填表、视频剪辑、一键分发、妙记下载
owner: 水岸
group: 运营
-version: "1.1"
-updated: "2026-03-21"
+version: "1.2"
+updated: "2026-03-23"
---
# 卡若创业派对运营 Skill 包
@@ -123,6 +123,14 @@ python3 "$DIST_SCRIPT/distribute_all.py" --now
**详细流程**:见 `skills/多平台分发_SKILL.md`
+#### 视频号发布前置(强制)
+
+在执行视频号发布前,固定做以下 3 步:
+
+1. **账号信息校验**:调用 `auth_data` 校验 `nickname` 与 `headImgUrl`,不一致先改到目标值再发。
+2. **线上失败/重复清理**:先查 `post_list`,删除失败条目;同标题仅保留最新一条(去重后再补发)。
+3. **仅定时发布**:禁止立即发布;若页面定时控件失效,使用 `post_create` 注入定时参数并拦截立即发布。
+
---
## 四、完整流程(派对结束后)
@@ -279,5 +287,6 @@ curl -sS -X POST -H "Content-Type: application/json" -d "$TEXT" "$FEISHU_PARTY_C
| 版本 | 日期 | 说明 |
|:---|:---|:---|
+| 1.2 | 2026-03-23 | 新增视频号发布前置三步:头像昵称校验、失败/重复清理、强制定时发布(含请求注入兜底) |
| 1.1 | 2026-03-21 | 新增 §九 闭环复盘发群:卡若五块复盘 + 飞书 Webhook v2(msg_type 必填) |
| 1.0 | 2026-03-20 | 初版:整合运营报表、视频切片、多平台分发、飞书视频文字下载 4 大技能,统一凭证管理 |
diff --git a/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md b/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
index 7717b056..0f61a7b2 100644
--- a/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
+++ b/.cursor/skills/karuo-party/skills/多平台分发_SKILL.md
@@ -7,14 +7,14 @@ description: >
triggers: 多平台分发、一键分发、全平台发布、批量分发、视频分发
owner: 木叶
group: 木
-version: "4.3"
+version: "4.4"
updated: "2026-03-23"
---
-# 多平台分发 Skill(v4.3)
+# 多平台分发 Skill(v4.4)
> **核心原则**:API 发布为主,Playwright 为辅。确保确定性地分发到各平台。
-> **v4.3**:默认**静默**(不自动 `channels_login`);需弹窗时 `--auto-channels-login` 或 `CHANNELS_AUTO_LOGIN=1`(独立脚本)。**v4.2**:智能排期与去重下标对齐。
+> **v4.4**:视频号新增发前强制检查(头像昵称校验、失败清理、同标题去重)与“仅定时发布(请求注入兜底)”。**v4.3**:默认静默登录。
## 〇、执行原则(第一性原理)
@@ -89,7 +89,7 @@ python3 distribute_all.py --platforms 视频号 --auto-channels-login --video-di
| 平台 | 定时方式 | 参数 |
|------|----------|------|
| B站 | API `meta.dtime` | Unix 时间戳(秒) |
-| 视频号 | API `postTimingInfo.postTime`(秒级 Unix);首条若时间过近则立即发 | `channels_api_publish._scheduled_ts_for_channels` |
+| 视频号 | API `postTimingInfo.postTime`(秒级 Unix);过近时间自动顺延,不允许立即发 | `channels_api_publish._scheduled_ts_for_channels` |
| 抖音 | API `timing_ts` | Unix 时间戳 |
| 快手 | Playwright UI | `schedule_helper.py` |
| 小红书 | Playwright UI | `schedule_helper.py` |
@@ -148,6 +148,17 @@ meta.hashtags("视频号") # … + #小程序卡若创业派对 #公众号卡
---
+## 六点五、视频号发布前置检查(强制)
+
+每次发布视频号前,必须先跑:
+
+1. `auth/auth_data`:校验 `nickname` 与 `headImgUrl`(不一致先改号资料,再执行发布)。
+2. `post/post_list`:筛查失败条目并删除。
+3. 同标题去重:若存在多条,仅保留最新 `objectId`,其余调用 `post/post_delete` 删除。
+4. 发布阶段若页面定时控件失败,改为 `post_create` 请求注入 `postTimingInfo`,继续定时发布;注入也失败则中止该条(防止误发立即)。
+
+---
+
## 七、去重机制
- 日志:`publish_log.json`(JSON Lines)
diff --git a/miniprogram/app.js b/miniprogram/app.js
index 6bc0d36a..2c1f4a66 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -12,7 +12,7 @@ const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
const PRODUCTION_BASE_URL = 'https://soulapi.quwanzhi.com'
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version(正式版),否则用本字段
-const APP_DISPLAY_VERSION = '1.7.1'
+const APP_DISPLAY_VERSION = '1.7.2'
App({
globalData: {
@@ -98,6 +98,9 @@ App({
lastVipContactCheck: 0,
// 头像昵称检测:上次检测时间戳(与 VIP 检测同周期刷新)
lastAvatarNicknameCheck: 0,
+ /** MBTI → 默认头像 URL(/api/miniprogram/config/mbti-avatars),供推广海报等 */
+ mbtiAvatarsMap: {},
+ mbtiAvatarsExpires: 0,
},
@@ -824,27 +827,58 @@ App({
async loadMpConfig() {
try {
const res = await this.getConfig()
- if (!res) return
- const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
- if (mp && typeof mp === 'object') {
- if (mp.appId) this.globalData.appId = mp.appId
- if (mp.mchId) this.globalData.mchId = mp.mchId
- if (mp.withdrawSubscribeTmplId) this.globalData.withdrawSubscribeTmplId = mp.withdrawSubscribeTmplId
- this.globalData.auditMode = !!mp.auditMode
- this.globalData.supportWechat = mp.supportWechat || mp.customerWechat || mp.serviceWechat || ''
- // 通知当前已加载的页面刷新 auditMode(从后台切回时配置更新后立即生效)
- try {
- const pages = getCurrentPages()
- pages.forEach(p => {
- if (p && p.data && 'auditMode' in p.data) {
- p.setData({ auditMode: this.globalData.auditMode || false })
- }
- })
- } catch (_) {}
+ if (res) {
+ const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
+ if (mp && typeof mp === 'object') {
+ if (mp.appId) this.globalData.appId = mp.appId
+ if (mp.mchId) this.globalData.mchId = mp.mchId
+ if (mp.withdrawSubscribeTmplId) this.globalData.withdrawSubscribeTmplId = mp.withdrawSubscribeTmplId
+ this.globalData.auditMode = !!mp.auditMode
+ this.globalData.supportWechat = mp.supportWechat || mp.customerWechat || mp.serviceWechat || ''
+ }
}
} catch (e) {
console.warn('[App] loadMpConfig 失败,使用默认值:', e?.message || e)
}
+ // 审核模式不走 5min 本地 config 缓存:始终以独立接口为准,避免后台已开审核端仍显示支付入口
+ try {
+ await this.getAuditMode()
+ } catch (_) {}
+ this.loadMbtiAvatarsMap()
+ },
+
+ /** 拉取后台配置的 16 型 MBTI 默认头像(公开接口,约 5 分钟本地缓存) */
+ async loadMbtiAvatarsMap() {
+ try {
+ const now = Date.now()
+ if (this.globalData.mbtiAvatarsExpires && this.globalData.mbtiAvatarsExpires > now) return
+ const res = await this.request({
+ url: '/api/miniprogram/config/mbti-avatars',
+ silent: true,
+ timeout: 8000,
+ })
+ if (res && res.success && res.avatars && typeof res.avatars === 'object') {
+ this.globalData.mbtiAvatarsMap = res.avatars
+ this.globalData.mbtiAvatarsExpires = now + 5 * 60 * 1000
+ }
+ } catch (e) {
+ console.warn('[App] loadMbtiAvatarsMap:', e?.message || e)
+ }
+ },
+
+ /** 展示用头像:优先用户头像,否则 MBTI 映射(需已 loadMbtiAvatarsMap) */
+ resolveAvatarWithMbti(avatar, mbti) {
+ try {
+ const { resolveAvatarWithMbti } = require('./utils/mbtiAvatar.js')
+ return resolveAvatarWithMbti(
+ avatar,
+ mbti,
+ this.globalData.mbtiAvatarsMap || {},
+ this.globalData.baseUrl || ''
+ )
+ } catch (_) {
+ return (avatar && String(avatar).trim()) || ''
+ }
},
/**
diff --git a/miniprogram/assets/images/karuo-link-avatar.png b/miniprogram/assets/images/karuo-link-avatar.png
new file mode 100644
index 00000000..e059c5b6
Binary files /dev/null and b/miniprogram/assets/images/karuo-link-avatar.png differ
diff --git a/miniprogram/assets/images/part-books/0.png b/miniprogram/assets/images/part-books/0.png
new file mode 100644
index 00000000..ed3dc4a0
Binary files /dev/null and b/miniprogram/assets/images/part-books/0.png differ
diff --git a/miniprogram/assets/images/part-books/1.png b/miniprogram/assets/images/part-books/1.png
new file mode 100644
index 00000000..29cad917
Binary files /dev/null and b/miniprogram/assets/images/part-books/1.png differ
diff --git a/miniprogram/assets/images/part-books/2.png b/miniprogram/assets/images/part-books/2.png
new file mode 100644
index 00000000..0bc8f47e
Binary files /dev/null and b/miniprogram/assets/images/part-books/2.png differ
diff --git a/miniprogram/assets/images/part-books/3.png b/miniprogram/assets/images/part-books/3.png
new file mode 100644
index 00000000..ed3dc4a0
Binary files /dev/null and b/miniprogram/assets/images/part-books/3.png differ
diff --git a/miniprogram/assets/images/part-books/4.png b/miniprogram/assets/images/part-books/4.png
new file mode 100644
index 00000000..29cad917
Binary files /dev/null and b/miniprogram/assets/images/part-books/4.png differ
diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js
index a48bd084..0a4e0fdd 100644
--- a/miniprogram/pages/chapters/chapters.js
+++ b/miniprogram/pages/chapters/chapters.js
@@ -7,6 +7,8 @@
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
+const { partEmojiForBodyIndex } = require('../../utils/partIcons.js')
+const { isSafeImageSrc } = require('../../utils/imageUrl.js')
Page({
data: {
@@ -116,15 +118,21 @@ Page({
{ id: 'appendix-2', title: '附录2|创业者自检清单', mid: fixedMap['appendix-2'] },
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源', mid: fixedMap['appendix-3'] }
]
- const bookData = parts.map((p) => ({
- id: p.id,
- icon: p.icon || '',
- title: p.title,
- subtitle: p.subtitle || '',
- chapterCount: p.chapterCount || 0,
- chapters: [],
- alwaysShow: (p.title || '').indexOf('每日派对干货') > -1
- }))
+ const bookData = parts.map((p, idx) => {
+ let icon = String(p.icon || '').trim()
+ if (icon && !isSafeImageSrc(icon)) icon = ''
+ const iconEmoji = icon ? '' : partEmojiForBodyIndex(idx)
+ return {
+ id: p.id,
+ icon,
+ iconEmoji,
+ title: p.title,
+ subtitle: p.subtitle || '',
+ chapterCount: p.chapterCount || 0,
+ chapters: [],
+ alwaysShow: (p.title || '').indexOf('每日派对干货') > -1
+ }
+ })
app.globalData.totalSections = totalSections
this.setData({
bookData,
diff --git a/miniprogram/pages/chapters/chapters.wxml b/miniprogram/pages/chapters/chapters.wxml
index 1cc3ff76..b3a972fe 100644
--- a/miniprogram/pages/chapters/chapters.wxml
+++ b/miniprogram/pages/chapters/chapters.wxml
@@ -72,7 +72,8 @@
@@ -52,7 +57,6 @@
@@ -160,4 +164,30 @@
+
+
+
+ 温馨提示
+ 使用手机号能力前,请先同意《用户隐私保护指引》
+
+ 拒绝
+
+
+
+
+
+
+ 留下联系方式
+ 方便卡若与您联系
+
+ 或手动输入
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss
index f8c22e4c..7fc9b66f 100644
--- a/miniprogram/pages/index/index.wxss
+++ b/miniprogram/pages/index/index.wxss
@@ -85,6 +85,10 @@
font-size: 20rpx;
color: rgba(255, 255, 255, 0.7);
white-space: nowrap;
+ max-width: 140rpx;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: center;
}
.logo-title {
@@ -963,6 +967,61 @@
height: 40rpx;
}
+/* ===== 隐私授权(与 avatar-nickname 对齐) ===== */
+.privacy-mask {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ z-index: 2000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 48rpx;
+ box-sizing: border-box;
+}
+.privacy-modal {
+ width: 100%;
+ max-width: 560rpx;
+ background: #17212F;
+ border-radius: 24rpx;
+ padding: 48rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ box-sizing: border-box;
+}
+.privacy-title {
+ font-size: 36rpx;
+ font-weight: 700;
+ color: #fff;
+ margin-bottom: 24rpx;
+}
+.privacy-desc {
+ font-size: 28rpx;
+ color: #94A3B8;
+ text-align: center;
+ line-height: 1.5;
+ margin-bottom: 40rpx;
+}
+.privacy-btn {
+ width: 100%;
+ height: 88rpx;
+ line-height: 88rpx;
+ text-align: center;
+ background: #5EEAD4;
+ color: #000;
+ font-size: 30rpx;
+ font-weight: 600;
+ border-radius: 16rpx;
+ border: none;
+}
+.privacy-btn::after { border: none; }
+.privacy-cancel {
+ margin-top: 24rpx;
+ font-size: 28rpx;
+ color: #64748B;
+}
+
/* ===== 链接卡若 - 留资弹窗 ===== */
.lead-mask {
position: fixed;
@@ -971,7 +1030,7 @@
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
- z-index: 1000;
+ z-index: 2100;
display: flex;
align-items: center;
justify-content: center;
diff --git a/miniprogram/pages/match/match.wxml b/miniprogram/pages/match/match.wxml
index eb9d8fb5..24d3146b 100644
--- a/miniprogram/pages/match/match.wxml
+++ b/miniprogram/pages/match/match.wxml
@@ -121,7 +121,8 @@