diff --git a/.cursor/agent/开发助理/项目索引/小程序.md b/.cursor/agent/开发助理/项目索引/小程序.md index 2fc93a88..2a4a6011 100644 --- a/.cursor/agent/开发助理/项目索引/小程序.md +++ b/.cursor/agent/开发助理/项目索引/小程序.md @@ -52,9 +52,10 @@ | 2026-04-13 | 会议:按功能同步开发文档;页面级记 miniprogram 接口与分享行为;无接口变更亦标注 | 进行中 | | 2026-04-14 | 吸收沉淀:逆推闭环自检写入 evolution;LOOP 需求行 | 进行中 | | 2026-04-15 | 业务补齐:`app.js` 挂 `err.response`;导师详情微信支付;找伙伴 join 失败展示后端文案 | 已完成 | +| 2026-05-09 | 我的页快捷入口:渐变面性 SVG 图标、`stat-icon-wrap` 圆形底托与卡片光效;开发文档运营与变更 / 当前小程序开发细则 §2.2.1 已同步 | 已完成 | > **格式说明**:每次开发后在此追加一行,日期格式 YYYY-MM-DD,状态用:已完成 / 进行中 / 待续 / 搁置 --- -**最后更新**:2026-04-15 +**最后更新**:2026-05-09 diff --git a/.cursor/skills/assistant-doc-sync/sync-log.md b/.cursor/skills/assistant-doc-sync/sync-log.md index 6454e908..75a49ad9 100644 --- a/.cursor/skills/assistant-doc-sync/sync-log.md +++ b/.cursor/skills/assistant-doc-sync/sync-log.md @@ -6,6 +6,10 @@ --- +## 2026-05-09 + +- 文档同步:小程序「我的」快捷入口 UI 变更写入 `开发文档/10、项目管理/运营与变更.md`、`开发文档/4、前端/当前小程序开发细则.md` §2.2.1;`项目索引/小程序.md` 开发进度追加 + ## 2026-04-15 - 吸收经验(联网):编程思维沉淀至 `agent/团队/evolution/2026-04-15.md`;升级 `role-flow-control` §1.3;`经验清单`、总索引、团队/助理橙子项目索引、sync-log 已更新 diff --git a/.cursor/skills/change-checklist/SKILL.md b/.cursor/skills/change-checklist/SKILL.md index b8b1de7a..70579d9f 100644 --- a/.cursor/skills/change-checklist/SKILL.md +++ b/.cursor/skills/change-checklist/SKILL.md @@ -77,7 +77,7 @@ description: Soul 创业派对变更关联检查。miniprogram/soul-admin/soul-a | 找伙伴/匹配 | 匹配、展示 | match、ckb 等 miniprogram 接口 | 匹配配置、开关 | | 配置项 | 仅读取 | miniprogram/config 或 db/config | 配置编辑(admin/db) | | VIP/超级个体 | 展示列表、详情 | miniprogram/vip/members;db/users PUT、db/vip-roles | 用户列表「设置 VIP」弹窗、VIP 角色管理页 | -| 超级个体发文(UGC+AI) | `super-article-editor`、`super-article-detail` | 表 `super_articles`;`GET/POST` `/api/miniprogram/super/articles*`、`/super/articles/generate` | 当前无独立超管文章列表为常态时勿只挂 admin;**勿**与 book/章节阅读接口混改或合并列表 | +| 超级个体发文(UGC+AI) | `super-article-editor`、阅读页 **`read`(`superArticleId`)**、`super-article-detail`(兼容重定向)、`super-article-mine` 等 | 表 `super_articles`;`GET/POST/DELETE` `/api/miniprogram/super/articles*`、`/super/articles/generate` | 当前无独立超管文章列表为常态时勿只挂 admin;**勿**与 book/章节阅读接口混改或合并列表;详情 UI 与章节共用 `read` 但 **数据模型仍隔离** | **专项检查**:若本次变更涉及 `super_articles`、**`super-article-*` 页面** 或 `super/articles` 接口,再读 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md),确认未与书籍/章节、MBTI 的 `soul_articles` 混表或混接口。 diff --git a/.cursor/skills/miniprogram-dev/SKILL.md b/.cursor/skills/miniprogram-dev/SKILL.md index e7ac2ac6..4e027cca 100644 --- a/.cursor/skills/miniprogram-dev/SKILL.md +++ b/.cursor/skills/miniprogram-dev/SKILL.md @@ -47,7 +47,7 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑 - **支付**:下单/查单走 `POST/GET /api/miniprogram/pay`、回调由 soul-api 处理;支付前必须已有 openId(通过登录或 getOpenId 获得)。 - **提现**:申请/记录/确认等走 `/api/miniprogram/withdraw/*`;订阅消息模板 ID 在 `app.globalData.withdrawSubscribeTmplId`。 - **scene 编解码**:海报与扫码统一用 `utils/scene.js` 的 `buildScene`/`parseScene`,支持 mid、id、ref,分隔符与后端生成一致(如 `_`)。 -- **超级个体 AI 写文章**:数据落在 soul-api 表 `super_articles`,接口为 `/api/miniprogram/super/articles`(生成/发布/列表/详情),页面 `super-article-editor`、`super-article-detail`;**勿与书籍/章节阅读流混为同一数据模型或接口**。详见 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md)。 +- **超级个体 AI 写文章**:数据落在 soul-api 表 `super_articles`,接口为 `/api/miniprogram/super/articles`(生成/发布/列表/详情/删除);编辑器 `super-article-editor`;**正文详情展示与书籍章节共用 `pages/read/read`,通过 query `superArticleId`(或 `saId`)进入动态模式**;`super-article-detail` 仅保留对 `read` 的兼容重定向。**数据模型与书籍章节接口仍分离**,勿混表混列表。详见 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md)。 --- @@ -115,6 +115,6 @@ description: Soul 创业派对小程序开发规范。在 miniprogram/ 下编辑 - 做个人中心、设置页布局时(遵循 §7,卡片区边距 16rpx)。 - 做阅读、文章等需长按复制的文本时(遵循 §9,text 加 user-select)。 - 做编辑资料页分享名片时(遵循 §10)。 -- 做超级个体 **AI 写文章**、`super-article-editor` / `super-article-detail` 时(遵循 §4 最后一条与 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md))。 +- 做超级个体 **AI 写文章**、`super-article-editor`、**`read`(含 `superArticleId` 动态详情)** / `super-article-detail`(兼容跳转)时(遵循 §4 超级个体一条与 [super-individual-article/SKILL.md](../super-individual-article/SKILL.md))。 遵循本 Skill 可保证小程序只与 soul-api 的 miniprogram 路由组对接,避免与管理端或 next-project 接口混用。 diff --git a/.cursor/skills/super-individual-article/SKILL.md b/.cursor/skills/super-individual-article/SKILL.md index 1289c1e7..15533077 100644 --- a/.cursor/skills/super-individual-article/SKILL.md +++ b/.cursor/skills/super-individual-article/SKILL.md @@ -44,7 +44,7 @@ description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/s | 方法 | 路径 | 说明 | |------|------|------| -| POST | `/api/miniprogram/super/articles/generate` | 根据描述 + 可选图片 URL 调用大模型,返回 `title`/`content` 草稿;需配置 OpenAI 兼容的 Key/模型 | +| POST | `/api/miniprogram/super/articles/generate` | 提示词 `description` + 可选 `referenceText`(端上文本文件拼接)+ `materialUrls`(附件上传 URL,服务端安全域内抓取)+ `imageUrls`;四者至少一项;返回 `title`/`content` 草稿;需 OpenAI 兼容 Key/模型;契约见 `开发文档/5、接口/API接口完整文档.md` §1.3 | | POST | `/api/miniprogram/super/articles` | 发布;body:`userId`, `title`, `content`;失败时可能带 `forbidden: true` | | GET | `/api/miniprogram/super/articles?authorUserId=&page=&pageSize=` | 作者为超级个体时返回 ta 的文章列表(含 preview) | | GET | `/api/miniprogram/super/articles/:id` | 详情(作者须为超级个体,否则不可见) | @@ -57,11 +57,14 @@ description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/s | 页面/逻辑 | 路径 | |-----------|------| -| AI 写文 + 发布 | [miniprogram/pages/super-article-editor/](miniprogram/pages/super-article-editor/):生成 `POST .../super/articles/generate`,发布 `POST .../super/articles`;图片走 `wx.uploadFile` 至 `/api/upload`,`formData.folder = article-images` | -| 详情 | [miniprogram/pages/super-article-detail/](miniprogram/pages/super-article-detail/) | -| 入口 | [miniprogram/pages/my/my.js](miniprogram/pages/my/my.js) 中 `superArticle`、`showSuperArticleEntry`;进编辑器前会调 `GET /api/miniprogram/my/super-stats` 校验 `isSuperIndividual` | +| AI 写文 + 发布 | [miniprogram/pages/super-article-editor/](miniprogram/pages/super-article-editor/):多文件 `chooseMessageFile`;图片上传 `folder=article-images`,附件 `folder=book-attachments`,文本可读类型可本地 read 后并入 `referenceText`;生成 `POST .../super/articles/generate`(timeout 建议 ≥120s),发布 `POST .../super/articles`;统一 `utils/miniprogramUpload.js` → `POST /api/miniprogram/upload` | +| **动态详情(正文展示)** | **`pages/read/read`**:`onLoad` 含 **`superArticleId`**(或 `saId`)时为超级个体详情模式(顶栏「文章详情」);正文/HTML、配图解析见 [miniprogram/utils/superArticleDetail.js](miniprogram/utils/superArticleDetail.js) | +| 兼容旧链 | [miniprogram/pages/super-article-detail/](miniprogram/pages/super-article-detail/):仅 **`redirectTo`** → `read?superArticleId=` | +| 我的动态列表 | [miniprogram/pages/super-article-mine/](miniprogram/pages/super-article-mine/) | +| 入口 | [miniprogram/pages/my/my.js](miniprogram/pages/my/my.js) 中 **`superArticleMine`**、`showSuperArticleEntry`;进编辑器前会调 `GET /api/miniprogram/my/super-stats` 校验 `isSuperIndividual` | - 所有请求**必须**走 `app.request` 与 **`/api/miniprogram/...` 前缀**(与 miniprogram-dev 一致)。 +- **数据仍独立**:`super_articles` 与书籍章节接口不合并;仅小程序 **UI** 共用 `read` 页面容器。 --- @@ -81,6 +84,6 @@ description: 超级个体 AI 写文章。super_articles 表、/api/miniprogram/s ## 7. 何时激活本 Skill -- 改「超级个体」「AI 写文章」「super-article-editor」「super-article-detail」或 `super_articles`。 +- 改「超级个体」「AI 写文章」「super-article-editor」、**阅读页动态模式(`read` + `superArticleId`)**、`super-article-detail`(兼容重定向)或 `super_articles`。 - 需区分 **本书章节**、**MBTI 引流文** 与 **超级个体发文** 时。 - 设计草稿/审核/定时发等新能力前,先读本 Skill §2 与 §5,避免与现有表职责混淆。 diff --git a/miniprogram/pages/super-article-editor/super-article-editor.js b/miniprogram/pages/super-article-editor/super-article-editor.js index 2c918379..2dfb28c2 100644 --- a/miniprogram/pages/super-article-editor/super-article-editor.js +++ b/miniprogram/pages/super-article-editor/super-article-editor.js @@ -15,8 +15,8 @@ function newAttachId() { Page({ data: { - /** 临时隐藏「AI 写文章」卡片;需上线时再设为 true */ - showAiWriteSection: false, + /** 「AI 帮你写」卡片:参考图 + 提示词 + 生成 */ + showAiWriteSection: true, statusBarHeight: 44, auditMode: false, title: '', diff --git a/miniprogram/pages/super-article-editor/super-article-editor.wxml b/miniprogram/pages/super-article-editor/super-article-editor.wxml index 2ac65f5d..ad20622e 100644 --- a/miniprogram/pages/super-article-editor/super-article-editor.wxml +++ b/miniprogram/pages/super-article-editor/super-article-editor.wxml @@ -11,7 +11,7 @@ AI 帮你写 - 参考图片(可多选) + 参考图片(可选) + 添加图片 @@ -25,7 +25,7 @@ 移除 - 从相册或相机添加参考图,可多选(单次最多 9 张),上传后参与 AI 生成。单张建议不超 30MB。 + 从相册或相机添加参考图,可选(单次最多 9 张),上传后与提示词一并交给 AI 分析。单张建议不超 30MB。 提示词 diff --git a/miniprogram/pages/super-moments/super-moments.js b/miniprogram/pages/super-moments/super-moments.js index dcb02fa7..18480cc8 100644 --- a/miniprogram/pages/super-moments/super-moments.js +++ b/miniprogram/pages/super-moments/super-moments.js @@ -1,5 +1,5 @@ /** - * 动态广场:超级个体图文列表(触底分页) + * 动态广场:超级个体图文列表(下拉刷新 + 触底分页加载) */ const app = getApp() const { isSafeImageSrc } = require('../../utils/imageUrl.js') @@ -85,6 +85,9 @@ function mapFeedRows(rows) { } Page({ + /** 递增序列:下拉刷新时废弃进行中的上拉请求结果,避免列表错乱 */ + loadSeq: 0, + data: { statusBarHeight: 44, feedList: [], @@ -126,38 +129,63 @@ Page({ this.loadMore() }, + async onPullDownRefresh() { + try { + await app.getAuditMode() + } catch (_) {} + if (app.globalData.auditMode) { + wx.stopPullDownRefresh() + return + } + try { + await this.reloadFeed() + } finally { + wx.stopPullDownRefresh() + } + }, + async reloadFeed() { + this.loadSeq += 1 + const seq = this.loadSeq this.setData({ loading: true, + loadingMore: false, page: 1, feedList: [], hasMore: true, total: 0, }) try { - await this.fetchPage(1, true) + await this.fetchPage(1, true, seq) } finally { - this.setData({ loading: false }) + if (seq === this.loadSeq) { + this.setData({ loading: false }) + } } }, async loadMore() { if (this.data.loading || this.data.loadingMore || !this.data.hasMore) return + const seq = this.loadSeq const next = this.data.page + 1 this.setData({ loadingMore: true }) try { - await this.fetchPage(next, false) + await this.fetchPage(next, false, seq) } finally { this.setData({ loadingMore: false }) } }, - async fetchPage(page, replace) { + async fetchPage(page, replace, seq) { const res = await app.request({ url: `/api/miniprogram/super/articles/feed?page=${encodeURIComponent(String(page))}&pageSize=${PAGE_SIZE}`, silent: true, }) + if (seq !== this.loadSeq) { + return + } if (!res?.success) { + if (seq !== this.loadSeq) return wx.showToast({ title: String(res?.error || '加载失败'), icon: 'none' }) if (replace) this.setData({ feedList: [], hasMore: false, total: 0 }) return diff --git a/miniprogram/pages/super-moments/super-moments.json b/miniprogram/pages/super-moments/super-moments.json index 89389a5d..980d848a 100644 --- a/miniprogram/pages/super-moments/super-moments.json +++ b/miniprogram/pages/super-moments/super-moments.json @@ -2,7 +2,7 @@ "usingComponents": { "icon": "/components/icon/icon" }, - "enablePullDownRefresh": false, + "enablePullDownRefresh": true, "onReachBottomDistance": 160, "backgroundTextStyle": "light", "backgroundColor": "#0b1220" diff --git a/soul-api/.env.super-article-ai.example b/soul-api/.env.super-article-ai.example new file mode 100644 index 00000000..c72b570c --- /dev/null +++ b/soul-api/.env.super-article-ai.example @@ -0,0 +1,12 @@ +# 超级个体「发动态」→ AI 生成(OpenAI 兼容协议) +# 智增增文档:https://doc.zhizengzeng.com/doc-3979947 +# 接入端点基地址(勿省略 /v1) +SUPER_ARTICLE_AI_BASE_URL=https://api.zhizengzeng.com/v1 +# 控制台创建的 API Key(勿提交到 Git;复制到本机 .env / .env.development) +SUPER_ARTICLE_AI_API_KEY= +# 模型名以智增增控制台/文档为准;gpt-4o-mini 支持文本与参考图(vision) +SUPER_ARTICLE_AI_MODEL=gpt-4o-mini +# 用户上传参考图时:自动改用视觉模型多模态分析图片(与 SUPER_ARTICLE_AI_MODEL 可为不同模型) +SUPER_ARTICLE_AI_VISION_MODEL=gpt-4o-mini + +# 若未配置以上项,则回退使用 OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL;视觉模型缺省为 gpt-4o-mini diff --git a/soul-api/internal/config/config.go b/soul-api/internal/config/config.go index 0cd96faf..770e3b7b 100644 --- a/soul-api/internal/config/config.go +++ b/soul-api/internal/config/config.go @@ -72,6 +72,12 @@ type Config struct { OpenAIAPIKey string // OPENAI_API_KEY OpenAIBaseURL string // OPENAI_BASE_URL,默认 https://api.openai.com/v1 OpenAIModel string // OPENAI_MODEL,默认 gpt-4o-mini + + // 超级个体「发动态」AI 生成(默认同上;可单独指向智增增等网关,避免与其它 OPENAI 用途混 Key) + SuperArticleAIAPIKey string // SUPER_ARTICLE_AI_API_KEY,空则沿用 OPENAI_API_KEY + SuperArticleAIBaseURL string // SUPER_ARTICLE_AI_BASE_URL,空则沿用 OPENAI_BASE_URL + SuperArticleAIModel string // SUPER_ARTICLE_AI_MODEL,空则沿用 OPENAI_MODEL(纯文本生成) + SuperArticleAIVisionModel string // SUPER_ARTICLE_AI_VISION_MODEL,有参考图时用;默认 gpt-4o-mini } // BaseURLJoin 将路径拼接到 BaseURL,path 应以 / 开头 @@ -326,6 +332,23 @@ func Load() (*Config, error) { aiModel = "gpt-4o-mini" } + superArticleAIKey := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_API_KEY")) + if superArticleAIKey == "" { + superArticleAIKey = aiAPIKey + } + superArticleAIBase := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_BASE_URL")) + if superArticleAIBase == "" { + superArticleAIBase = aiBaseURL + } + superArticleAIModel := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_MODEL")) + if superArticleAIModel == "" { + superArticleAIModel = aiModel + } + superArticleAIVisionModel := strings.TrimSpace(os.Getenv("SUPER_ARTICLE_AI_VISION_MODEL")) + if superArticleAIVisionModel == "" { + superArticleAIVisionModel = "gpt-4o-mini" + } + cfg := &Config{ Port: port, Mode: mode, @@ -362,6 +385,10 @@ func Load() (*Config, error) { OpenAIAPIKey: aiAPIKey, OpenAIBaseURL: aiBaseURL, OpenAIModel: aiModel, + SuperArticleAIAPIKey: superArticleAIKey, + SuperArticleAIBaseURL: superArticleAIBase, + SuperArticleAIModel: superArticleAIModel, + SuperArticleAIVisionModel: superArticleAIVisionModel, } // 生产环境(GIN_MODE=release)强制校验敏感配置,禁止使用默认值 diff --git a/soul-api/internal/handler/admin_super_articles.go b/soul-api/internal/handler/admin_super_articles.go index f934aa11..b26f2f33 100644 --- a/soul-api/internal/handler/admin_super_articles.go +++ b/soul-api/internal/handler/admin_super_articles.go @@ -58,6 +58,7 @@ func AdminSuperArticlesList(c *gin.Context) { list := make([]gin.H, 0, len(rows)) for _, r := range rows { a := authorMap[strings.TrimSpace(r.UserID)] + an, aa := resolveSuperArticleAuthorDisplay(a) list = append(list, gin.H{ "id": r.ID, "userId": r.UserID, @@ -66,8 +67,8 @@ func AdminSuperArticlesList(c *gin.Context) { "images": parseSuperArticleImagesJSON(r.Images), "auditStatus": strings.TrimSpace(r.AuditStatus), "rejectReason": strings.TrimSpace(r.RejectReason), - "authorNickname": strings.TrimSpace(a.Nickname), - "authorAvatar": strings.TrimSpace(a.Avatar), + "authorNickname": an, + "authorAvatar": aa, "createdAt": r.CreatedAt, "updatedAt": r.UpdatedAt, }) diff --git a/soul-api/internal/handler/h5_super_article.go b/soul-api/internal/handler/h5_super_article.go index 20877e86..9f55babb 100644 --- a/soul-api/internal/handler/h5_super_article.go +++ b/soul-api/internal/handler/h5_super_article.go @@ -98,8 +98,9 @@ func H5SuperArticlePage(c *gin.Context) { } authorMap := loadSuperArticleAuthorMap([]string{row.UserID}) - author := authorMap[row.UserID] - nick := strings.TrimSpace(author.Nickname) + author := authorMap[strings.TrimSpace(row.UserID)] + an, aa := resolveSuperArticleAuthorDisplay(author) + nick := an if nick == "" { nick = "超级个体" } @@ -141,7 +142,7 @@ func H5SuperArticlePage(c *gin.Context) { ogImage = h5AbsolutizeImage(strings.TrimSpace(articleImages[0]), cfg.BaseURL) } if ogImage == "" { - ogImage = strings.TrimSpace(author.Avatar) + ogImage = aa if ogImage != "" { ogImage = h5AbsolutizeImage(ogImage, cfg.BaseURL) } diff --git a/soul-api/internal/handler/miniprogram_ai_article.go b/soul-api/internal/handler/miniprogram_ai_article.go index e43dcf88..d9584c1e 100644 --- a/soul-api/internal/handler/miniprogram_ai_article.go +++ b/soul-api/internal/handler/miniprogram_ai_article.go @@ -22,15 +22,47 @@ import ( const aiArticleSystemPrompt = `你是「卡若创业派对」的内容创作助手,帮助超级个体写出真实、有深度的商业与创业内容文章。 -写作要求: -- 标题:精炼有力,≤30字,有吸引力,点明核心价值 -- 正文:600-1000字,有真实故事或案例,有观点洞察,语气自然真诚 -- 风格:像朋友分享经验,而非说教;有画面感,接地气 -- 不要使用"首先、其次、最后"等模板化结构词 +若用户提供的素材末尾附有「写文章规则提示词」或以「---」分隔的写作规范段落,你必须优先、严格按该段落成稿(体裁、字数、人称、版式、Markdown 交付与自检等)。 -严格输出 JSON,不要有任何额外文字: +输出封装(始终遵守):只输出一段合法 JSON,不要有任何 JSON 外的文字。title 为主标题纯文本(可与正文首行 Markdown 标题语义一致,但不要带「#」前缀);content 为正文全文,可为 Markdown(含 **加粗**、分段、---、HTML 注释等),字符串内需合法 JSON 转义。 + +若用户未附带上述写作规范段落,则默认:标题精炼有力≤30字;正文约600~1000字;语气自然真诚,像朋友分享经验;不要使用"首先、其次、最后"等模板化结构词。 + +严格输出 JSON: {"title":"文章标题","content":"文章正文"}` +// superArticleWritingRulesSuffix 在服务端拼接到用户素材末尾(与 开发文档/ai开发文档.txt 写作规则段落保持一致)。 +const superArticleWritingRulesSuffix = `写文章规则提示词: + +写文章。严格按以下规则成稿。 + +【材料】用户提供:本场聊天记录/纪要/妙记要点、场次号、主题短句;写作前先按规范阅读飞书运营报表,把报表上有的时长、场观、进房等具体数字写进开篇(只写一次)。 + +【人称与禁用】叙述全程用「我」,禁止「房主」。禁止「这边」「那边」。禁止「回答说,……」——问句后直接接我的陈述。语气偏强势:干脆、笃定,少「可能/也许」。「卡若」每篇最多 1 次,可完全不出现。 + +【刺点三对齐】正文第一行标题「# 第X场|短句」、磁盘文件名、开篇刺激性金句三者同一刺点;不写空泛抒情。 + +【结构与比例】 +- 默认单场 采用「2026 每日派对干货 / 派对体」:三块约 40% / 20%~30% / 余下约 30%;块与块之间必须有过渡口语;每一块结束必须有一段「段末小结」单独成段,30~50 个汉字一句,禁止出现「干货」等标签字。 + +【开篇】先有冲突或世俗金句,再切入场景。固定数据句式(报表有则写具体数,没有的项不写):「第 X 场,X 分钟;场观 XXXXX,进房 XXX 人。」不写「约」、不写「见报表」。时长/场观/进房仅在开篇出现一次。 + +【中段】推进用时间线或事件线;多写对话(「X 号问」「有人问」);我的话直接陈述。约在全文 50% 处插入一段浓缩要点 3~6 条(20~40 字/条),前后用 --- 分隔,条目中禁止「干货」二字。 + +【分享句】约在 20% 处一句、结尾一句,各 ≤50 字,紧扣本节,禁止「干货:」字样。 + +【强调·派对体成稿必选】Markdown **加粗**:至少 2 句完整立场/结论句(分布在不同段落);至少 3 个不同的重点关键词加粗。禁止堆砌无意义加粗。 + +【结尾】升华主题;派对体可有文末小结(3~6 条短金句)+ 总结段落 + 一句收尾金句。 + +【字数与真实性】小节 3000~5000 字;忠于来源不改原意;所有数字必须有依据、写具体算法/金额/人次等,Soul 侧常用口径可参考规范中的曝光/进房/成本写法。 + +【隐晦植入】文中自然散布 2~4 处:联系管理、做切片、做副业相关一句,不单段喊话,随机10%加率不用每篇文章都加 + +【版式强制】一句一行,句间空一行;分号也拆成行;段落之间空行;大段堆砌禁止。 + +【交付】输出 Markdown;标题一行「# 第X场|…」;成稿末尾按模板追加 HTML 注释自检:「我」字数/次数、本场关键字频次、关键字加粗处数、重点句/词加粗是否达标。` + const ( maxArticleMaterialFetchBody = 512 * 1024 // 单素材最大读取 maxCombinedReferenceRunes = 60000 // 合并后截断上限(含提示与素材摘要) @@ -40,11 +72,11 @@ const ( // 根据提示词 + 可选参考文本 / 素材文件 URL(本站上传链接) / 图片 URL,调用 AI 生成草稿(仅超级个体)。 func MiniprogramSuperArticleGenerate(c *gin.Context) { var req struct { - UserID string `json:"userId"` - Description string `json:"description"` // 提示词(与原字段兼容) - ReferenceText string `json:"referenceText"` // 小程序端可读出的纯文本(多文件拼接) - MaterialURLs []string `json:"materialUrls"` // 上传后的文件 URL(服务端在安全域名内抓取文本) - ImageURLs []string `json:"imageUrls"` + UserID string `json:"userId"` + Description string `json:"description"` // 提示词(与原字段兼容) + ReferenceText string `json:"referenceText"` // 小程序端可读出的纯文本(多文件拼接) + MaterialURLs []string `json:"materialUrls"` // 上传后的文件 URL(服务端在安全域名内抓取文本) + ImageURLs []string `json:"imageUrls"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"}) @@ -86,8 +118,8 @@ func MiniprogramSuperArticleGenerate(c *gin.Context) { } cfg := config.Get() - if cfg == nil || strings.TrimSpace(cfg.OpenAIAPIKey) == "" { - c.JSON(http.StatusOK, gin.H{"success": false, "error": "AI 功能暂未开放,请联系管理员配置"}) + if cfg == nil || strings.TrimSpace(cfg.SuperArticleAIAPIKey) == "" { + c.JSON(http.StatusOK, gin.H{"success": false, "error": "AI 功能暂未开放,请在服务端配置 SUPER_ARTICLE_AI_API_KEY 或 OPENAI_API_KEY"}) return } @@ -102,6 +134,7 @@ func MiniprogramSuperArticleGenerate(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": false, "error": errMsg}) return } + combined = appendSuperArticleWritingRules(combined) title, content, err := aiGenerateArticle(cfg, combined, validImageURLs) if err != nil { @@ -156,6 +189,32 @@ func assembleSuperArticlePrompt(cfg *config.Config, prompt, refText string, mate return combined, "" } +// appendSuperArticleWritingRules 在用户提示词与参考素材组装结果末尾追加固定写作规则(保证规则段不被 maxCombinedReferenceRunes 截断)。 +func appendSuperArticleWritingRules(combined string) string { + suffix := strings.TrimSpace(superArticleWritingRulesSuffix) + if suffix == "" { + return strings.TrimSpace(combined) + } + suffixRunes := len([]rune(suffix)) + sep := "\n\n---\n\n" + sepLen := len([]rune(sep)) + maxBody := maxCombinedReferenceRunes - suffixRunes - sepLen + if maxBody < 0 { + maxBody = 0 + } + base := strings.TrimSpace(combined) + if base != "" { + rs := []rune(base) + if len(rs) > maxBody { + base = string(rs[:maxBody]) + "\n\n(上文参考已截断)" + } + } + if base == "" { + return suffix + } + return base + sep + suffix +} + func snippetNameFromURL(raw string) string { u, err := url.Parse(strings.TrimSpace(raw)) if err != nil { @@ -281,34 +340,66 @@ func looksLikeUTF8ArticleRef(b []byte) bool { return float64(printable)/float64(total) > 0.90 } +// superArticleModelLikelySupportsVision 粗略判断 chat/completions 模型是否适合传 image_url(智增增/OpenAI 等多网关)。 +func superArticleModelLikelySupportsVision(model string) bool { + m := strings.ToLower(strings.TrimSpace(model)) + if m == "" { + return false + } + if strings.Contains(m, "vision") || strings.Contains(m, "-vl") || strings.Contains(m, "vqa") { + return true + } + if strings.Contains(m, "gpt-4") || strings.Contains(m, "gpt-5") || strings.Contains(m, "gpt-4o") { + return true + } + if strings.Contains(m, "claude-3") || strings.Contains(m, "claude-4") { + return true + } + if strings.Contains(m, "glm-4v") || strings.Contains(m, "qwen-vl") || strings.Contains(m, "gemini") { + return true + } + return false +} + // aiGenerateArticle 调用 OpenAI 兼容 API 生成文章标题和正文。 -// 若有图片 URL 且模型支持视觉,以多模态格式传入;否则退化为纯文本。 +// 若有参考图片 URL:使用多模态 user content(text + image_url);当前主模型不支持 vision 时自动改用 SUPER_ARTICLE_AI_VISION_MODEL(默认同为 gpt-4o-mini)。 func aiGenerateArticle(cfg *config.Config, description string, imageURLs []string) (title, content string, err error) { - apiKey := strings.TrimSpace(cfg.OpenAIAPIKey) - baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.OpenAIBaseURL), "/") + apiKey := strings.TrimSpace(cfg.SuperArticleAIAPIKey) + baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.SuperArticleAIBaseURL), "/") if baseURL == "" { baseURL = "https://api.openai.com/v1" } - model := strings.TrimSpace(cfg.OpenAIModel) - if model == "" { - model = "gpt-4o-mini" + baseModel := strings.TrimSpace(cfg.SuperArticleAIModel) + if baseModel == "" { + baseModel = "gpt-4o-mini" } - // 判断模型是否支持视觉(OpenAI gpt-4 系列 / claude-3 系列) - supportsVision := len(imageURLs) > 0 && ( - strings.Contains(model, "gpt-4") || - strings.Contains(model, "claude-3") || - strings.Contains(model, "vision")) + requestModel := baseModel + useVision := len(imageURLs) > 0 + if useVision { + if superArticleModelLikelySupportsVision(baseModel) { + requestModel = baseModel + } else { + vm := strings.TrimSpace(cfg.SuperArticleAIVisionModel) + if vm == "" { + vm = "gpt-4o-mini" + } + requestModel = vm + } + } var userContent interface{} - if supportsVision { + if useVision { parts := []map[string]interface{}{ {"type": "text", "text": buildUserPrompt(description, imageURLs, true)}, } for _, u := range imageURLs { parts = append(parts, map[string]interface{}{ - "type": "image_url", - "image_url": map[string]string{"url": u, "detail": "low"}, + "type": "image_url", + "image_url": map[string]string{ + "url": u, + "detail": "low", + }, }) } userContent = parts @@ -317,16 +408,16 @@ func aiGenerateArticle(cfg *config.Config, description string, imageURLs []strin } reqBody, _ := json.Marshal(map[string]interface{}{ - "model": model, + "model": requestModel, "messages": []map[string]interface{}{ {"role": "system", "content": aiArticleSystemPrompt}, {"role": "user", "content": userContent}, }, "temperature": 0.8, - "max_tokens": 2000, + "max_tokens": 8192, }) - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() httpReq, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/chat/completions", bytes.NewReader(reqBody)) @@ -341,7 +432,10 @@ func aiGenerateArticle(cfg *config.Config, description string, imageURLs []strin return "", "", fmt.Errorf("请求 AI 服务失败: %v", err) } defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, errRead := io.ReadAll(resp.Body) + if errRead != nil { + return "", "", fmt.Errorf("读取 AI 响应失败: %v", errRead) + } var apiResp struct { Choices []struct { @@ -354,10 +448,23 @@ func aiGenerateArticle(cfg *config.Config, description string, imageURLs []strin } `json:"error"` } if err := json.Unmarshal(body, &apiResp); err != nil { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "", "", fmt.Errorf("AI 服务 HTTP %d", resp.StatusCode) + } return "", "", fmt.Errorf("解析 AI 响应失败") } - if apiResp.Error != nil { - return "", "", fmt.Errorf("%s", apiResp.Error.Message) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + if apiResp.Error != nil && strings.TrimSpace(apiResp.Error.Message) != "" { + return "", "", fmt.Errorf("%s", strings.TrimSpace(apiResp.Error.Message)) + } + snippet := strings.TrimSpace(string(body)) + if len(snippet) > 400 { + snippet = snippet[:400] + "…" + } + return "", "", fmt.Errorf("AI 服务 HTTP %d %s", resp.StatusCode, snippet) + } + if apiResp.Error != nil && strings.TrimSpace(apiResp.Error.Message) != "" { + return "", "", fmt.Errorf("%s", strings.TrimSpace(apiResp.Error.Message)) } if len(apiResp.Choices) == 0 || apiResp.Choices[0].Message.Content == "" { return "", "", fmt.Errorf("AI 无返回内容") @@ -410,12 +517,12 @@ func parseArticleJSON(raw string) (title, content string, err error) { return "", "", fmt.Errorf("AI 生成内容不完整,请重试") } - // 截断超限内容 - if tr := []rune(title); len(tr) > 40 { - title = string(tr[:40]) + // 截断超限内容(派对体长文可到数千字,预留上限避免极端响应撑爆存储) + if tr := []rune(title); len(tr) > 120 { + title = string(tr[:120]) } - if cr := []rune(content); len(cr) > 5000 { - content = string(cr[:5000]) + if cr := []rune(content); len(cr) > 20000 { + content = string(cr[:20000]) } return title, content, nil diff --git a/soul-api/internal/handler/miniprogram_super_articles.go b/soul-api/internal/handler/miniprogram_super_articles.go index a660c5b3..7a2d55f1 100644 --- a/soul-api/internal/handler/miniprogram_super_articles.go +++ b/soul-api/internal/handler/miniprogram_super_articles.go @@ -14,9 +14,24 @@ import ( ) type superArticleAuthorRow struct { - ID string `gorm:"column:id"` - Nickname string `gorm:"column:nickname"` - Avatar string `gorm:"column:avatar"` + ID string `gorm:"column:id"` + Nickname string `gorm:"column:nickname"` + Avatar string `gorm:"column:avatar"` + VipName string `gorm:"column:vip_name"` + VipAvatar string `gorm:"column:vip_avatar"` +} + +// resolveSuperArticleAuthorDisplay 展示用昵称/头像:普通资料优先,超级个体常用 vip_* 兜底。 +func resolveSuperArticleAuthorDisplay(a superArticleAuthorRow) (nickname, avatar string) { + nickname = strings.TrimSpace(a.Nickname) + if nickname == "" { + nickname = strings.TrimSpace(a.VipName) + } + avatar = strings.TrimSpace(a.Avatar) + if avatar == "" { + avatar = strings.TrimSpace(a.VipAvatar) + } + return nickname, avatar } func loadSuperArticleAuthorMap(ids []string) map[string]superArticleAuthorRow { @@ -26,7 +41,7 @@ func loadSuperArticleAuthorMap(ids []string) map[string]superArticleAuthorRow { } var rows []superArticleAuthorRow db := database.DB() - _ = db.Table("users").Select("id", "nickname", "avatar").Where("id IN ?", ids).Find(&rows).Error + _ = db.Table("users").Select("id", "nickname", "avatar", "vip_name", "vip_avatar").Where("id IN ?", ids).Find(&rows).Error for _, r := range rows { key := strings.TrimSpace(r.ID) if key == "" { @@ -142,6 +157,7 @@ func MiniprogramSuperArticleFeed(c *gin.Context) { preview = string([]rune(preview)[:120]) + "..." } a := authorMap[strings.TrimSpace(r.UserID)] + an, aa := resolveSuperArticleAuthorDisplay(a) list = append(list, gin.H{ "id": r.ID, "userId": r.UserID, @@ -149,8 +165,8 @@ func MiniprogramSuperArticleFeed(c *gin.Context) { "content": r.Content, "preview": preview, "images": parseSuperArticleImagesJSON(r.Images), - "authorNickname": strings.TrimSpace(a.Nickname), - "authorAvatar": strings.TrimSpace(a.Avatar), + "authorNickname": an, + "authorAvatar": aa, "createdAt": r.CreatedAt, }) } @@ -343,6 +359,7 @@ func MiniprogramSuperArticleList(c *gin.Context) { authorMap := loadSuperArticleAuthorMap([]string{authorUserID}) author := authorMap[authorUserID] + an, aa := resolveSuperArticleAuthorDisplay(author) list := make([]gin.H, 0, len(rows)) for _, r := range rows { content := strings.TrimSpace(r.Content) @@ -357,8 +374,8 @@ func MiniprogramSuperArticleList(c *gin.Context) { "content": r.Content, "preview": preview, "images": parseSuperArticleImagesJSON(r.Images), - "authorNickname": strings.TrimSpace(author.Nickname), - "authorAvatar": strings.TrimSpace(author.Avatar), + "authorNickname": an, + "authorAvatar": aa, "createdAt": r.CreatedAt, }) } @@ -409,6 +426,7 @@ func MiniprogramSuperArticleMine(c *gin.Context) { authorMap := loadSuperArticleAuthorMap([]string{userID}) author := authorMap[userID] + an, aa := resolveSuperArticleAuthorDisplay(author) list := make([]gin.H, 0, len(rows)) for _, r := range rows { content := strings.TrimSpace(r.Content) @@ -427,8 +445,8 @@ func MiniprogramSuperArticleMine(c *gin.Context) { "content": r.Content, "preview": preview, "images": parseSuperArticleImagesJSON(r.Images), - "authorNickname": strings.TrimSpace(author.Nickname), - "authorAvatar": strings.TrimSpace(author.Avatar), + "authorNickname": an, + "authorAvatar": aa, "createdAt": r.CreatedAt, "auditStatus": ast, "rejectReason": strings.TrimSpace(r.RejectReason), @@ -474,7 +492,8 @@ func MiniprogramSuperArticleDetail(c *gin.Context) { ast = model.SuperArticleAuditApproved } authorMap := loadSuperArticleAuthorMap([]string{row.UserID}) - author := authorMap[row.UserID] + author := authorMap[strings.TrimSpace(row.UserID)] + an, aa := resolveSuperArticleAuthorDisplay(author) c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ @@ -483,8 +502,8 @@ func MiniprogramSuperArticleDetail(c *gin.Context) { "title": row.Title, "content": row.Content, "images": parseSuperArticleImagesJSON(row.Images), - "authorNickname": strings.TrimSpace(author.Nickname), - "authorAvatar": strings.TrimSpace(author.Avatar), + "authorNickname": an, + "authorAvatar": aa, "createdAt": row.CreatedAt, "auditStatus": ast, "rejectReason": strings.TrimSpace(row.RejectReason), diff --git a/开发文档/10、项目管理/运营与变更.md b/开发文档/10、项目管理/运营与变更.md index b7436e0e..57278203 100644 --- a/开发文档/10、项目管理/运营与变更.md +++ b/开发文档/10、项目管理/运营与变更.md @@ -6,6 +6,27 @@ ## 一、近期同步(摘要) +### 2026-05-09 超级个体「动态详情」并入小程序阅读页(文章详情) + +- **动机**:产品与章节阅读统一为同一套「文章详情」承载页;动态分享与 H5 拉起小程序路径对齐。 +- **小程序**:`pages/read/read?superArticleId={数字}`(兼容 `saId`);`data.pageMode` 为 `super_article` 时渲染动态 UI(顶栏文案「文章详情」)。解析逻辑见 `miniprogram/utils/superArticleDetail.js`。 +- **兼容**:`pages/super-article-detail` 保留注册,`onLoad` 内 `redirectTo` 至 `read?superArticleId=`。 +- **入口更新**:`super-article-mine`、`super-moments`、`member-detail` 等跳转已改为阅读页 query。 +- **后端**:`h5_super_article.go` 中拉起小程序 path 已改为 `pages/read/read?superArticleId=...`。 +- **文档**:`开发文档/4、前端/当前小程序开发细则.md` §2.2.2。 + +### 2026-05-09 小程序「我的」页快捷入口 UI 优化 + +- **范围**:`pages/my/my.wxml`、`pages/my/my.wxss`,以及 `miniprogram/assets/icons/list-teal.svg`、`share-teal.svg`、`wallet-teal.svg`、`users-teal.svg`、`book-arrow-teal.svg`。 +- **说明**:快捷入口图标改为渐变面性 SVG;每项增加圆形发光底托(`stat-icon-wrap`)、卡片渐变边框与弱外发光;余额主文案使用 `stat-num-balance`(渐变字,低版本可降级纯色)。**`book-arrow-teal.svg`** 与「最近阅读」卡片标题共用,改图标时需注意两处观感一致。 +- **交叉引用**:实现约定见 `开发文档/4、前端/当前小程序开发细则.md` §2.2.1。 + +### 2026-05-09 超级个体「AI 写文章」:多文件 + 提示词 + 生成草稿 + +- **小程序**:`pages/super-article-editor/super-article-editor`,`wx.chooseMessageFile` 多选;图片上传 `folder=article-images`,附件 `folder=book-attachments`,文本类可本地读入后随 `referenceText` 提交;`POST /api/miniprogram/super/articles/generate` 建议超时 120s;上传走 `utils/miniprogramUpload.js`(Bearer)。 +- **后端**:`/super/articles/generate` 支持 `description`、`referenceText`、`materialUrls`、`imageUrls`,至少其一;`materialUrls` 仅允许白名单域拉取(防 SSRF)。 +- **文档**:`开发文档/5、接口/API接口完整文档.md` §1.3、`开发文档/三端需求业务对齐-小程序与API.md` §四、`开发文档/4、前端/当前小程序开发细则.md` §2.2.3、`.cursor/skills/super-individual-article/SKILL.md`。 + ### 2026-04-28 超级个体发文章接口对接与排障 - **目标**:小程序超级个体发布文章,统一走 `super_articles` 表对应接口,避免与管理端文章体系混用。 @@ -34,4 +55,4 @@ --- -**最后更新**:2026-04-15 +**最后更新**:2026-05-09 diff --git a/开发文档/4、前端/当前小程序开发细则.md b/开发文档/4、前端/当前小程序开发细则.md index 1aad3fe1..1218fbdb 100644 --- a/开发文档/4、前端/当前小程序开发细则.md +++ b/开发文档/4、前端/当前小程序开发细则.md @@ -1,7 +1,7 @@ # 当前小程序开发细则 > 汇总当前 Soul 创业派对小程序的架构、经验与规划,便于新人上手与后续迭代。 -> 最后整理:2026-02 +> 最后整理:2026-05-09(§2.2.2 阅读页承载超级个体动态;§2.2.3 AI 发文章) --- @@ -52,6 +52,33 @@ miniprogram/ 非 Tab 页:阅读页 read、推广中心 referral、订单 purchases、设置 settings、搜索 search、关于 about。 +### 2.2.1 我的页「快捷入口」(UI 约定,2026-05) + +- **位置**:`pages/my/my` 中「快捷入口」卡片:`stats-card` → `stats-grid` → 若干 `stat-box`。 +- **结构**:每个入口为 `stat-box`;图标外包一层 `stat-icon-wrap`(圆形渐变底、描边与弱外发光);列表项设 `hover-class="stat-box-hover"` 提供按压反馈。 +- **图标资源**(`miniprogram/assets/icons/`):`list-teal`(订单)、`share-teal`(代付)、`wallet-teal`(余额)、`users-teal`(客资,条件展示)、`book-arrow-teal`(发文,条件展示)。均为青绿渐变面性 SVG。 +- **共用资源**:`book-arrow-teal.svg` 同时用于同页「最近阅读」卡片标题左侧图标,修改该文件需兼顾两处视觉。 +- **余额文案**:主数字 class 为 `stat-num-balance`(渐变字 + 等宽数字倾向);若某端 `background-clip: text` 表现不佳,可降级为纯色 `#4FD1C5`。 + +### 2.2.2 阅读页 `read` 双模式:书籍章节 + 超级个体动态(文章详情) + +- **同一页面**:`pages/read/read` 既承载**书籍章节阅读**(付费墙、上下篇、阅读进度等),也承载 **超级个体 UGC 动态的正文详情展示**(与原先独立「动态详情」页合并)。 +- **区分参数(必选其一语义)** + - **章节**:`id`(章节 id)、可选 `mid`(章节 mid)、及原有 `ref`/`gift`/scene 等;逻辑不变。 + - **动态**:**`superArticleId`**(数字字符串,表 `super_articles.id`);兼容简写 **`saId`**。**不要**用章节用的 `id` 传动态 id,避免与章节 id 混淆。 +- **落地路径示例**:`/pages/read/read?superArticleId=123`;分享卡片 / 朋友圈已为该格式。 +- **兼容**:`pages/super-article-detail/super-article-detail?id=` 仍注册在 `app.json`,进入后 **`redirectTo`** 到上述 `read?superArticleId=`(旧分享链接可用)。 +- **相关页面**:`super-article-editor`(编辑/发布)、`super-article-mine`(我的动态列表)、`super-moments`(广场)、`member-detail`(嘉宾动态入口)等跳转详情时统一 **`navigateTo` 阅读页 + `superArticleId`**。 +- **工具**:动态详情用到的正文/HTML、配图解析见 `miniprogram/utils/superArticleDetail.js`(由 `read.js` 引用)。 +- **后端 H5**:拉起小程序路径已对齐为 `pages/read/read?superArticleId=...`(见 `soul-api/internal/handler/h5_super_article.go`)。 + +### 2.2.3 超级个体「发文章」页:多文件参考 + AI 生成 + 发布(2026-05) + +- **页面**:`pages/super-article-editor/super-article-editor`。 +- **流程**:选择多份参考文件 → 填写提示词 → **生成文章**(`POST /api/miniprogram/super/articles/generate`,建议 `timeout` ≥ 120s)自动回填标题与正文 → 可微调后 **发布文章**(`POST /api/miniprogram/super/articles`)。 +- **选文件**:`wx.chooseMessageFile` 多选。**图片**走 `POST /api/miniprogram/upload` + `folder=article-images`;**PDF/Office 等附件**走 `folder=book-attachments`;**`.txt` / `.md` / `.json` / `.csv` 等**可在端上 `readFile` 后并入请求体 `referenceText`(详见 `开发文档/5、接口/API接口完整文档.md` §1.3)。 +- **上传封装**:`utils/miniprogramUpload.js`(Bearer 与 baseUrl 一致)。 + ### 2.3 全局状态(app.js globalData) - `userInfo`:登录后用户信息(id、openId、nickname、purchasedSections、hasFullBook、referralCode、referralCount 等)。 @@ -95,6 +122,7 @@ miniprogram/ ### 3.4 分享与落地 - 阅读页分享:`onShareAppMessage` / `onShareTimeline` 带 `id=章节ID&ref=当前用户邀请码`,落地后 ref 写入 storage,绑定与订单归属同上。 +- **超级个体动态**(阅读页 `pageMode=super_article`):分享路径为 `/pages/read/read?superArticleId=…`,与章节链路分离。 - 文章/章节分销与全局同一套:不按“哪篇文章带来”单独分成或统计,仅按“谁发的链接(ref=谁)”归属。 --- diff --git a/开发文档/5、接口/API接口完整文档.md b/开发文档/5、接口/API接口完整文档.md index 2fa96790..1d036136 100644 --- a/开发文档/5、接口/API接口完整文档.md +++ b/开发文档/5、接口/API接口完整文档.md @@ -73,7 +73,59 @@ Authorization: Bearer admin-token-secret **接口**: `GET /api/miniprogram/super/articles/{id}` **说明**: -- 返回文章详情(标题、正文、作者昵称头像、创建时间) +- 返回文章详情(标题、正文、配图、作者昵称头像、创建时间、审核状态等,以后端实现为准)。 +- 审核中非公开稿件:作者查看须在请求中带 `viewerUserId`(当前用户 openId),与 soul-api `MiniprogramSuperArticleDetail` 一致。 + +### 删除文章(作者本人) + +**接口**: `DELETE /api/miniprogram/super/articles/{id}?userId={openId}` + +**兼容**: `POST /api/miniprogram/super/articles/{id}/delete?userId=`(语义同 DELETE,便于调试)。 + +### 小程序落地页(详情 UI) + +- **统一使用阅读页**:`pages/read/read?superArticleId={id}`(兼容 query **`saId`**)。**勿**使用章节参数 `id` 传递动态主键,以免与章节 id 混淆。 +- **旧版路径**:`pages/super-article-detail/super-article-detail?id=` 仍可用,进入后重定向至上述路径。 +- **H5 拉起小程序**:服务端生成的 path 为 `pages/read/read?superArticleId=...`(见 `h5_super_article.go`)。 + +### AI 生成文章草稿(多文件 + 提示词) + +**接口**: `POST /api/miniprogram/super/articles/generate` + +**请求体**: +```json +{ + "userId": "小程序当前用户 id,必填", + "description": "提示词,可选;可与参考素材组合", + "referenceText": "可选,端上读取的纯文本(如多份 .txt/.md 拼接)", + "materialUrls": ["可选,附件上传接口返回的完整 URL"], + "imageUrls": ["可选,图片上传接口返回的公网 URL"] +} +``` + +**校验**: +- 仅**超级个体**可调(与发布接口同一身份判定)。 +- `description`、`referenceText` 非空、`materialUrls`、`imageUrls` **四者至少一项有内容**;若仅有图片且无文字素材,服务端会补充默认写作说明再调模型。 + +**响应**: +```json +{ "success": true, "data": { "title": "...", "content": "..." } } +``` + +**运行依赖**: 服务端需配置 OpenAI 兼容通道(如 `OPENAI_API_KEY`,及可选 `OPENAI_BASE_URL`、`OPENAI_MODEL`)。 + +**小程序对接要点**(页面:`pages/super-article-editor/super-article-editor`): +- 会话内选文件:`wx.chooseMessageFile`(可多选)。 +- **图片**:`POST /api/miniprogram/upload`,`formData.folder = article-images`,将返回 URL 填入 `imageUrls`。 +- **附件**(PDF、Office、zip 等):`folder = book-attachments`(走附件校验与大小上限,与章节附件同源策略),URL 填入 `materialUrls`。 +- **可读文本**(`.txt`、`.md`、`.json`、`.csv` 等):可在端上用 `FileSystemManager.readFile` 读入 UTF-8,拼进 `referenceText`(过长时端上可先截断,如约 2.8 万字符再提交)。 +- 上传建议使用 `utils/miniprogramUpload.js`,请求头携带 `Authorization: Bearer `。 +- 生成接口耗时长,建议 `wx.request` / `app.request` **timeout ≥ 120000**(毫秒)。 + +**服务端素材拉取(materialUrls)**: +- 仅允许拉取 URL 路径含 **`/uploads/`**,且主机为 **配置项 `API_BASE_URL` 对应主机**或 **阿里云 OSS 域名(含 `*.aliyuncs.com`)**,用于降低 SSRF 风险。 +- 单文件最多读取约 **512KB**;合并后的参考正文在服务端约 **6 万字符**处截断。 +- PDF/Office 等二进制文件通常无法在服务端抽出正文,接口会在提示中说明「未解析」;产品侧可引导用户改用 `.txt/.md` 或把要点写在 `description`。 --- diff --git a/开发文档/5、接口/README.md b/开发文档/5、接口/README.md index c7e72faf..bb54651d 100644 --- a/开发文档/5、接口/README.md +++ b/开发文档/5、接口/README.md @@ -7,6 +7,7 @@ | 文档 | 说明 | |------|------| | [**API接口完整文档.md**](./API接口完整文档.md) | Soul `/api` **主真源**(REST 模块划分、鉴权说明) | +| [**文件上传与OSS.md**](./文件上传与OSS.md) | **路由摘要**;OSS/本地策略与运维见 [6、后端/阿里云OSS与文件上传.md](../6、后端/阿里云OSS与文件上传.md) | | [配置清单-完整版.md](./配置清单-完整版.md) | 环境变量与配置项 | | [在线支付对接文档.md](./在线支付对接文档.md) | 支付对接 | | [接口与提现.md](./接口与提现.md) | 提现与相关接口 | diff --git a/开发文档/5、接口/文件上传与OSS.md b/开发文档/5、接口/文件上传与OSS.md new file mode 100644 index 00000000..e42a00db --- /dev/null +++ b/开发文档/5、接口/文件上传与OSS.md @@ -0,0 +1,23 @@ +# 文件上传与 OSS(接口侧摘要) + +> **完整约定与运维排障**:见 [**6、后端/阿里云OSS与文件上传.md**](../6、后端/阿里云OSS与文件上传.md)(以 `internal/oss`、`upload.go` 为准)。本节仅列路由与语义,避免与后端长文重复。最后更新:2026-05-09。 + +## 路由摘要 + +| 方法与路径 | 鉴权 | 说明 | +|-----------|------|------| +| `POST /api/upload` | 无硬性要求 | multipart:`file`、`folder` | +| `POST /api/admin/upload` | **Admin JWT** | **soul-admin**:`ADMIN_UPLOAD_PATH` | +| `DELETE /api/upload`、`DELETE /api/admin/upload` | 后者须 Admin | query:`path=` | +| `POST /api/miniprogram/upload` | - | 与 `UploadPost` 同源 | +| `POST /api/miniprogram/upload/image`、`/video` | - | `upload_content`,图可压缩 | + +## 行为摘要 + +**先 OSS(`oss_config` 有效),失败或未配置则落本地**;接口多返回 `success: true`。成功时 **`data.storage`**:`oss` | `local`(`/api/upload` 系列)。 + +## 脚本 + +- 缺少 `super_articles` 表:`soul-api/scripts/create_super_articles.sql` + +返回 [本目录 README](./README.md) · [后端完整说明](../6、后端/阿里云OSS与文件上传.md) · [开发文档索引](../索引.md) diff --git a/开发文档/5、接口/配置清单-完整版.md b/开发文档/5、接口/配置清单-完整版.md index 1ca859a7..688eb2d9 100644 --- a/开发文档/5、接口/配置清单-完整版.md +++ b/开发文档/5、接口/配置清单-完整版.md @@ -1,6 +1,6 @@ # Soul创业实验 - API密钥与配置清单 -> 最后更新: 2026-01-25 +> 最后更新: 2026-05-09(补充 OSS 业务文档链接) > 维护人: 卡若 > ⚠️ 本文件包含敏感信息,请勿公开 @@ -87,6 +87,8 @@ | **AccessKey ID** | `LTAI5t9zkiWmFtHG8qmtdysW` | | **AccessKey Secret** | `xxjXnZGLNvA2zDkj0aEBSQm3XZAaro` | +**Soul 业务 OSS(素材/头像等)**:以库表 `system_config.oss_config` 为主;可选环境变量 `ALIYUN_OSS_ACCESS_KEY_ID` / `ALIYUN_OSS_ACCESS_KEY_SECRET` 在库内 Secret 无效时兜底。详见 [**开发文档/6、后端/阿里云OSS与文件上传.md**](../6、后端/阿里云OSS与文件上传.md)。 + --- ## 五、数据库 diff --git a/开发文档/6、后端/README.md b/开发文档/6、后端/README.md index a361e469..7f6853db 100644 --- a/开发文档/6、后端/README.md +++ b/开发文档/6、后端/README.md @@ -7,6 +7,7 @@ | 文件 | 说明 | |------|------| | [后端架构.md](./后端架构.md) | 服务架构与模块 | +| [**阿里云OSS与文件上传.md**](./阿里云OSS与文件上传.md) | **OSS 配置、环境变量兜底、上传策略、路由与排障**(路由短表互链 [5、接口/文件上传与OSS.md](../5、接口/文件上传与OSS.md)) | | [后端开发规范.md](./后端开发规范.md) | 编码、路由、响应约定 | | [内容创建问题修复说明.md](./内容创建问题修复说明.md) | 内容创建专项 | | [soul-admin与Mycontent-temp内容页对比.md](./soul-admin与Mycontent-temp内容页对比.md) | 管理端内容页对比 | diff --git a/开发文档/6、后端/阿里云OSS与文件上传.md b/开发文档/6、后端/阿里云OSS与文件上传.md new file mode 100644 index 00000000..a7dc99ac --- /dev/null +++ b/开发文档/6、后端/阿里云OSS与文件上传.md @@ -0,0 +1,86 @@ +# 阿里云 OSS 与文件上传(soul-api) + +> **以代码为准**:`soul-api/internal/oss`、`internal/handler/upload.go`、`internal/handler/upload_content.go`、`internal/handler/db.go`(系统设置)。 +> 最后更新:2026-05-09 + +--- + +## 1. 配置存哪里 + +| 来源 | 说明 | +|------|------| +| **主配置** | MySQL 表 `system_config`,`config_key = 'oss_config'`,`config_value` 为 JSON | +| **环境变量兜底** | 当库里的 `accessKeySecret` 为空或为占位符(如 `****`、全星号)时,可用 `ALIYUN_OSS_ACCESS_KEY_ID` / `ALIYUN_OSS_ACCESS_KEY_SECRET` 补齐后再校验(见 `soul-api/.env.development` 注释) | + +JSON 字段与管理端「系统设置 → OSS」一致,常见键名: + +- `endpoint`:如 `oss-cn-beijing.aliyuncs.com`(可不含 `https://`) +- `bucket`、`region` +- `accessKeyId`、`accessKeySecret` +- `publicBaseUrl`(可选):CDN 或自定义域名,`https://` 开头或裸域名皆可 + +**说明**:RAM 控制台下发的 **AccessKey Secret 为长随机串**。若库里误存字面量 `****`,阿里云会返回 **SignatureDoesNotMatch**(403),与「界面脱敏」不是一回事。 + +--- + +## 2. 运行时如何读配置 + +- 业务统一走 **`internal/oss.LoadConfig()`**:解析 `oss_config`,再应用环境变量兜底,最后校验 bucket / endpoint / 非占位 Secret。 +- **`oss.IsEnabled()`**、通用上传、`upload/image`、`upload/video` 均依赖同一套逻辑。 + +管理端 **`GET /api/admin/settings`** 中的 `ossConfig`: + +- 在 **能成功 `LoadConfig()`** 时,回显 **与实际上传一致的生效字段**(含合并环境变量后的密钥),并设 `secretConfigured: true`。 +- 若 **不能加载有效配置**,则仍以库中原始 JSON 为主,`secretConfigured: false`(常见于库里 Secret 为占位符且未配置环境变量兜底)。 + +保存:**`POST /api/admin/settings`** 的 `ossConfig` 会合并历史密钥(避免仅因前端未重填 Secret 而覆盖);合并后须能通过 **`oss.ConfigJSONIsReady`** 校验才写入。 + +--- + +## 3. 上传策略与路由 + +**策略(统一)**:若 `LoadConfig()` 成功则 **先上传 OSS**;若 PutObject 失败、返回空 URL,或 **未配置有效 OSS**,则 **自动回退本地磁盘**(`uploads/` 等,由 `UPLOAD_DIR` / 默认目录与 Gin `Static /uploads` 提供访问)。处理函数 **不再因 OSS 失败向客户端返回 5xx**(仍 `success: true` 时),便于线上不因 OSS 瞬时故障中断业务;服务端打 **log** 便于排障。响应 JSON 中常见 **`data.storage`**:`oss` | `local`。 + +> 说明:配置里曾出现历史环境变量 `UPLOAD_ALLOW_LOCAL_FALLBACK`,**handler 已默认本地兜底**,该开关不再控制是否允许回退(详见 `internal/config/config.go` 注释)。 + +| 方法 | 路径 | 鉴权 | 说明 | +|------|------|------|------| +| POST | `/api/admin/upload` | 管理端 JWT | 通用 multipart:`file` + `folder` | +| DELETE | `/api/admin/upload` | 管理端 JWT | query `path=`,本地路径或 OSS URL | +| POST | `/api/upload` | 无硬性要求 | 与 `UploadPost` 同源 | +| DELETE | `/api/upload` | 视部署 | 同上 | +| POST | `/api/miniprogram/upload` | 小程序侧约定 | 同源 `UploadPost` | +| POST | `/api/miniprogram/upload/image` | 同上 | 图片,可压缩;内部 `ossUploadBytes` 再本地 | +| POST | `/api/miniprogram/upload/video` | 同上 | 视频;内部 `ossUploadFile` 再本地 | + +小程序封装见 `miniprogram/utils/miniprogramUpload.js`。**接口侧短表**见 [5、接口/文件上传与OSS.md](../5、接口/文件上传与OSS.md)。 + +--- + +## 4. 本地与排障 + +### 4.1 自检脚本 + +仓库 **`soul-api/scripts/test_oss_connect.py`**(依赖 `pip install oss2`): + +- 默认读取脚本内示例 JSON;**Secret 占位时必须设置环境变量** `OSS_ACCESS_KEY_SECRET` 或 `ALIYUN_OSS_ACCESS_KEY_SECRET`。 +- 成功则对目标 Bucket 执行 `list_objects(max_keys=1)`。 + +### 4.2 常见现象 + +| 现象 | 可能原因 | +|------|----------| +| `secretConfigured: false` | 库内 Secret 无效/占位;或未设环境变量兜底 | +| 上传提示未配置 OSS | `LoadConfig()` 失败,查日志 `oss: oss_config rejected` / `parse error` | +| 管理端 Network 里 Secret 仍是 `****` | 多为 **库里就是字面量 `****`**;修好库或配置 env 并重启后,若 `LoadConfig` 成功,GET settings 会回显生效密钥 | + +--- + +## 5. 安全与运维 + +- **不要将含真实 OSS Secret 的 `.env` 提交到公开仓库。** +- 管理端返回完整密钥依赖 **Admin 鉴权**;若接口暴露在公网,须配合 HTTPS、IP 限制与强口令。 + +--- + +返回 [6、后端 README](./README.md) · [开发文档索引](../索引.md) diff --git a/开发文档/ai开发文档.txt b/开发文档/ai开发文档.txt new file mode 100644 index 00000000..a050aed8 --- /dev/null +++ b/开发文档/ai开发文档.txt @@ -0,0 +1,50 @@ +智增增 OpenAI 兼容 API(超级个体 AI 写文章) + +基地址:https://api.zhizengzeng.com/v1 +接口:POST /chat/completions(与 OpenAI 一致) + +开发文档(模型与价格): +https://doc.zhizengzeng.com/doc-3979947 + +密钥:请在智增增控制台创建,写入 soul-api 环境变量(勿明文存入仓库): +- SUPER_ARTICLE_AI_API_KEY(推荐,与其他 OPENAI 用途隔离) +- 或 OPENAI_API_KEY +- SUPER_ARTICLE_AI_MODEL:默认文本与配图均可优先使用(如 gpt-4o-mini,支持 vision) +- SUPER_ARTICLE_AI_VISION_MODEL:有参考图时自动改用、与图片一并分析的多模态模型(默认 gpt-4o-mini,需在控制台支持 vision) + +完整示例见仓库:soul-api/.env.super-article-ai.example + +--- + +以下「写文章规则提示词」由服务端在调用大模型前自动追加在用户提示词与参考素材之后(无须小程序重复传);若需修改规则,请同步改 soul-api/internal/handler/miniprogram_ai_article.go 常量 superArticleWritingRulesSuffix。 + +写文章规则提示词: + +写文章。严格按以下规则成稿。 + +【材料】用户提供:本场聊天记录/纪要/妙记要点、场次号、主题短句;写作前先按规范阅读飞书运营报表,把报表上有的时长、场观、进房等具体数字写进开篇(只写一次)。 + +【人称与禁用】叙述全程用「我」,禁止「房主」。禁止「这边」「那边」。禁止「回答说,……」——问句后直接接我的陈述。语气偏强势:干脆、笃定,少「可能/也许」。「卡若」每篇最多 1 次,可完全不出现。 + +【刺点三对齐】正文第一行标题「# 第X场|短句」、磁盘文件名、开篇刺激性金句三者同一刺点;不写空泛抒情。 + +【结构与比例】 +- 默认单场 采用「2026 每日派对干货 / 派对体」:三块约 40% / 20%~30% / 余下约 30%;块与块之间必须有过渡口语;每一块结束必须有一段「段末小结」单独成段,30~50 个汉字一句,禁止出现「干货」等标签字。 + +【开篇】先有冲突或世俗金句,再切入场景。固定数据句式(报表有则写具体数,没有的项不写):「第 X 场,X 分钟;场观 XXXXX,进房 XXX 人。」不写「约」、不写「见报表」。时长/场观/进房仅在开篇出现一次。 + +【中段】推进用时间线或事件线;多写对话(「X 号问」「有人问」);我的话直接陈述。约在全文 50% 处插入一段浓缩要点 3~6 条(20~40 字/条),前后用 --- 分隔,条目中禁止「干货」二字。 + +【分享句】约在 20% 处一句、结尾一句,各 ≤50 字,紧扣本节,禁止「干货:」字样。 + +【强调·派对体成稿必选】Markdown **加粗**:至少 2 句完整立场/结论句(分布在不同段落);至少 3 个不同的重点关键词加粗。禁止堆砌无意义加粗。 + +【结尾】升华主题;派对体可有文末小结(3~6 条短金句)+ 总结段落 + 一句收尾金句。 + +【字数与真实性】小节 3000~5000 字;忠于来源不改原意;所有数字必须有依据、写具体算法/金额/人次等,Soul 侧常用口径可参考规范中的曝光/进房/成本写法。 + +【隐晦植入】文中自然散布 2~4 处:联系管理、做切片、做副业相关一句,不单段喊话,随机10%加率不用每篇文章都加 + +【版式强制】一句一行,句间空一行;分号也拆成行;段落之间空行;大段堆砌禁止。 + +【交付】输出 Markdown;标题一行「# 第X场|…」;成稿末尾按模板追加 HTML 注释自检:「我」字数/次数、本场关键字频次、关键字加粗处数、重点句/词加粗是否达标。 diff --git a/开发文档/三端需求业务对齐-小程序与API.md b/开发文档/三端需求业务对齐-小程序与API.md index 0ac9eb43..7722bb96 100644 --- a/开发文档/三端需求业务对齐-小程序与API.md +++ b/开发文档/三端需求业务对齐-小程序与API.md @@ -133,6 +133,10 @@ /api/miniprogram/ckb/join /api/miniprogram/ckb/match ← 已挂载 /api/miniprogram/upload +/api/miniprogram/my/super-stats +/api/miniprogram/super/articles ← GET 列表、POST 发布 +/api/miniprogram/super/articles/:id ← GET 详情 +/api/miniprogram/super/articles/generate ← POST AI 生成草稿(参考 开发文档/5、接口/API接口完整文档.md §1.3) /api/miniprogram/user/addresses, addresses/:id /api/miniprogram/user/check-purchased, profile, purchase-status, reading-progress, update /api/miniprogram/withdraw, withdraw/records, pending-confirm, confirm-received, confirm-info diff --git a/开发文档/索引.md b/开发文档/索引.md index 0849dcf2..e0bcb4e6 100644 --- a/开发文档/索引.md +++ b/开发文档/索引.md @@ -1,6 +1,6 @@ # Soul 创业派对 - 开发文档索引 -> **以代码为准**:需求或实现变更时同步更新本文档与对应章节 README。最后更新:2026-04-07 +> **以代码为准**:需求或实现变更时同步更新本文档与对应章节 README。最后更新:2026-05-09 --- @@ -99,7 +99,7 @@ | [API接口完整文档.md](./5、接口/API接口完整文档.md) | **Soul 项目 API 主真源** | | [配置清单-完整版.md](./5、接口/配置清单-完整版.md) | 配置项清单 | | [在线支付对接文档.md](./5、接口/在线支付对接文档.md) | 支付 | -| [接口与提现.md](./5、接口/接口与提现.md) | 提现相关接口说明 | +| [文件上传与OSS.md](./5、接口/文件上传与OSS.md) | 路由摘要,`data.storage`,互链后端详版 | --- @@ -108,6 +108,7 @@ | 文件/路径 | 用途 | |-----------|------| | [README.md](./6、后端/README.md) | 本目录索引 | +| [**阿里云OSS与文件上传.md**](./6、后端/阿里云OSS与文件上传.md) | **OSS、上传策略、路由、env 兜底、`storage`、`UPLOAD_*`、自检脚本** | | [后端架构.md](./6、后端/后端架构.md) | soul-api 架构 | | [后端开发规范.md](./6、后端/后端开发规范.md) | 编码与路由约定 | | [内容创建问题修复说明.md](./6、后端/内容创建问题修复说明.md) | 专项修复说明 |