⓪ 浅白版:整个平台,用开商场打比方(Jeff 版,5 分钟读完)
🏬 平台是什么:我们开了一座商场
HOOP Apps 平台就是一座商场。店(app)是别人开的,商场是我们的。 商场管四件事:给店面(代码放哪)、给仓库(数据放哪)、管装修换新(版本)、管开业资格(审核)。 —— 这正好就是你问的那四个问题,所以你问的框架是对的。
📸 「快照」是什么:每一版装修,拍一张永远不改的照片
店主每次改装修(改代码),我们不是在原来的墙上涂改,而是把整套新装修完整存一份, 编上号:第 1 版、第 2 版、第 3 版……存进去之后就封死,永远不再动 —— 这就是「快照」。 就像拍立得照片:拍下来就是那个样子,谁也改不了。
为什么要这样做?因为只要旧照片都在,就永远回得去。 第 3 版上线发现是坏的?把牌子指回第 2 版就完了(这就是「回滚」)—— 一秒钟,不用重做任何东西。 如果当初是在原墙上涂改,旧的样子就没了,坏了就只能哭。
🪧 live 和 staging:门口挂两块牌子
每家店门口有两块牌子: 「live」牌子指着哪一版,顾客看到的就是哪一版; 「staging」牌子指着店主正在试的新版 —— 只有店主自己、他的 AI 帮手、还有他拉进"试衣间群"的人看得到。 按一下「上线」按钮 = 把 live 牌子挪到新照片上。牌子随便挪,照片永远不动。
试的新版怎么防外人偷看?看新版要一张一小时就过期的门票(进店时我们发的), 没票的人来了,我们连"这里有个新版"都不承认(直接说"没有这东西")。
🗄️ 数据放哪:每家店有两种储物
① 顾客的带锁抽屉:顾客在店里存的东西(游戏进度、收藏),每人一个抽屉,钥匙只在顾客手上。 店主打不开 —— 他只能知道"一共有几个抽屉、总共多重",看不到里面是什么。 这一条是我们和微信不一样的地方:微信的店主翻得到顾客的数据,我们故意不给。这是 HOOP 的隐私招牌。
② 店里的公告板:店主要给所有顾客看的内容(餐厅名单、新闻栏目),写在公告板上,人人看到同一份。 店主随时能改,顾客只能看。
另外顾客在每家店里用的是假名(每家店一个不同的代号):店主永远不知道顾客真名是谁, 两家店也对不上"这是同一个人"。真名以后要经顾客自己点头才给。
🛃 上传的代码要过安检机
店主上传的每一份装修材料,进门先过安检:不许私联外网、不许夹带执行外来代码的暗门, 查出一条就整包退回(会告诉他退货原因)。太大的包(炸弹包)、想溜出自己店面偷别家东西的路径,一律拒收。
🚪 开业要审,翻新不审
新店第一次开门,要我们(管理员,只有你和你指定的人,AI 无权批)点头才对顾客可见; 开了业之后店主翻新装修,不用再排队 —— 只审"能不能开业"这一次,不卡人家做生意。
📏 每家店的额度(白纸黑字)
| 东西 | 额度 | 打比方 |
|---|---|---|
| 一版装修(代码) | 单文件 2MB / 整包解开 20MB、最多 200 个文件 | 一次进货的体积上限 |
| 图片库 | 200 张、每张 2MB、总共不超过 400MB | 店里相册的厚度 |
| 顾客抽屉 | 每人每店 128 格、每格 16KB | 抽屉格数 |
| 公告板 | 256 条、每条 64KB | 公告板面积 |
⚠️ 还没做完的三件(不瞒你)
① 现在任何一个 AI 帮手都能进任何一家店当店员 —— 今天店全是我们自己的,没事; 以后外面的人来开店,这条必须收紧(只有店主授权的帮手才能进)。 ② 商场最值钱的服务 —— 帮店主接上顾客的朋友关系、支付 —— 门还没开(故意的:先立规矩再开门)。 ③ 店主还没有一个好看的柜台界面,现在全靠 AI 帮手代办(界面要等下一班 TestFlight)。
① 我们对标谁,各学哪半
| 微信小程序(9 亿月活生态) | Telegram Mini Apps | HOOP 取哪边 | |
|---|---|---|---|
| 技术 | 专有 WXML/WXSS,学习曲线陡 | 纯 HTML/CSS/JS,零专有语法 | Telegram 边:H5 游戏管线原样复用,AI 也最会写 |
| 平台独有能力 | 支付 + 社交关系(开发者离不开它的根本原因) | TON 支付 + 频道分发 | 微信边:我们的杀手锏是关系链 / 六度 / 温度 —— 别人抄不走 |
| 数据库 | 云开发(云数据库/云函数/云存储,2018 起,小开发者主流+平台收入) | 不提供,开发者自带服务器 | 微信边:hoopApp.storage = 我们的"云开发"第一块 |
| 审核 | 强制过审才上架 | 基本不审 | 微信边(还没做,复盘 🔴#1) |
| 隐私 | openid 化名,但生态内数据打通 | 拿真 username | 比两家都狠:影子身份数学隔离,见 ⑪ |
② 总架构(一张图)
┌─ 用户端(App)──────────────────────────────────────────────┐
│ 底栏第二格长按 → 「Apps」页(列表,本地优先缓存) │
│ └→ 全屏壳 MiniappWebViewScreen │
│ ├ 先握手 POST /v1/apps/{id}/launch(影子身份+版本分流) │
│ └ 注入 window.hoopApp 桥(init/user/storage,文档流之前)│
└──────────────────────────┬──────────────────────────────┘
│ 用户 JWT(app 永远拿不到)
┌─ 后端 miniapp 域(Go,独立于 games)────────────────────────┐
│ 目录 miniapps · KV miniapp_kv · 图像 miniapp_media │
│ 版本双指针 live/staging · 留痕 miniapp_promotions · 一房一app │
└───────┬───────────────────────────┬─────────────────────┘
Postgres(云库) R2 对象存储
目录/KV/图像账/留痕 miniapps/{app}/v{N}/index.html
miniapps/{app}/media/{uuid}
hosted_bundles 底座表两边共用(记录在案,见 ⑫ 架构债)。③ 开发者旅程(Jeff 2026-08-13 原话 = 产品流程定案)
| 件 | 状态 | 落点 |
|---|---|---|
| 创建项目 = 开辟空间 | 生产 | POST /v1/dev/apps(第 6 步) |
| 数据库 | 生产 | miniapp_kv + hoopApp.storage(第 4 步) |
| 图像库 | 生产 | miniapp_media + R2(第 5 步) |
| 代码上传 | 生产 | POST /v1/dev/apps/{id}/code(第 7 步) |
| live / staging 按钮 | 生产 | 双指针 + promote + 留痕(第 7 步) |
| App 工作房 | 生产 | miniapps.workroom_id 一房一 app(第 7 步) |
| 开发者中心界面(把按钮画出来) | 待做 | 目前全靠 API;App UI 是下一批(⑬) |
④ 目录与身份
功能
平台的「账本」:有哪些 app、归谁、什么状态。用户打开 app 的那一刻,拿到的是影子身份 —— app 永远不知道你是现实中的谁。
操作
用户侧:Apps 页(GET /v1/apps,只列 active)→ 点开 → POST /v1/apps/{id}/launch
launch 返回:{ app_id, app_user_id: "a_…", sdk_version, capabilities:[], entry_url(按身份分流好版本) }
draft 状态的 app:owner / AI 分身可打开(真机测试),其他人 404 当不存在
代码
| 东西 | 位置 | 关键判据 |
|---|---|---|
表 miniapps | 迁移 000218 / 225 / 230 | id·name·icon·tagline·entry_url·status(active/disabled/draft)·dev_id·live_version·staging_version·workroom_id |
域 backend/internal/domain/miniapp/ | 单文件 miniapp.go | 与 games 零共享代码(等底座抽取) |
影子身份 appUserID() | HMAC 双层派生 | 域前缀 hoop:miniapp:identity: → 同一人在游戏(g_)和小程序(a_)里影子不同,两平台数据拼不上是数学事实 |
| launch 版本分流 | miniapp.go | owner/AI 分身/所挂工作房成员 → staging;路人 → live;canSeeStaging 查不出来按「看不到」处理(错放比错关贵) |
⑤ 数据库(HOOP 云开发第一块 —— 微信云数据库的位置)
功能
每个 app × 每个影子用户,一个 KV 命名空间。小开发者/AI 造的 app 零服务器就能有"记忆"(收藏、进度、用户内容)。
操作(app 里的代码就这么写)
await hoopApp.storage.set('fav', 'Penang'); // 存
const v = await hoopApp.storage.get('fav'); // 取 → "Penang"
await hoopApp.storage.set('fav', null); // 删
代码
| 东西 | 位置 | 关键判据 |
|---|---|---|
表 miniapp_kv | 迁移 000219;PK=(app_id, app_user_id, k) | 隔离是主键结构给的,不是代码自觉 |
端点 GET/PUT /v1/apps/{id}/kv/{key} | 用户 JWT 鉴权 | 影子 id 服务器现算,桥上不传 —— app 连"报自己是谁"的机会都没有 |
| 配额 | 单值 16KB · 每人每 app 128 键 | 58MB 分身会话那课:不设限的存储都会涨到爆;已有键仍可覆盖,只挡开新键 |
桥 hoopKv handler | miniapp_webview_screen.dart | app JS → 壳 → 带用户 JWT 调 API;断网回 {err},app 自己兜底(裸跑军规) |
生产实测:写→读回 / 换人读同键=null(隔离)/ 删→null,四路全过。
⑥ 图像库
功能
每个 app 独立媒体库:传图 → 拿永久地址 → app 里 <img> 直接用。
操作
上传:POST /v1/dev/apps/{id}/media (body=图片字节, Content-Type: image/*)
→ { id, url:"/miniapp-media/{app}/{id}", mime, size }
列表:GET /v1/dev/apps/{id}/media
直出:GET /miniapp-media/{app}/{mid} (公开,无鉴权头 —— WebView 里的 img 用)
代码
| 东西 | 关键判据 |
|---|---|
表 miniapp_media(000225)+ R2 miniapps/{app}/media/{uuid} | 登记失败即删 R2 对象 —— 不留没账的孤儿文件 |
权限 canManage = owner / AI 分身 | 和发布按钮同族判据 |
| 配额:单张 2MB · 每 app 200 张 | 同 ⑤ 的病,同一个药 |
缓存头 immutable | id 即内容,永不变 → 可以放心让 CDN 缓一年 |
⑦ 代码上传与版本管理(live / staging 双指针)
功能
「修改的时候不影响线上」:每次上传产出一个不可变版本快照,库里两个整数决定谁看哪版。 上线/回退 = 改一个整数,零文件搬运、天然原子。这套双指针与游戏平台同构(games 000205 先行验证过)。
操作
传代码:POST /v1/dev/apps/{id}/code (body=index.html)
→ staging_version = max(live, staging)+1 ← 版本号服务器发,客户端不数
自己试:launch 自动把 owner 指到 staging 版
上线: POST /v1/dev/apps/{id}/promote
→ live = staging;留痕(谁/何时/从几到几);首次上线自动 draft→active(=上架)
直出: GET /miniapp-code/{app}/v{N}/ (immutable:版本内容永不变)
代码与判据
| 判据 | 为什么(都是真踩过的) |
|---|---|
| 上传只动 staging,live 一字不碰 | 「上传即上线」正好推翻双轨的意义(games 踩过) |
| 发布按钮绝不改 entry_url | g2048 那颗雷:promote 顺手改写入口 → Pages 游戏一按全球 404。同构照搬防御 |
| 没有测试版就报错,不静默成功 | 假成功比报错贵:让人以为推上去了而线上没动 |
留痕进 miniapp_promotions,留痕失败整笔回退 | 出事那一刻唯一想知道的是「谁推的、何时推的」 |
| 单文件 index.html(≤2MB)或 zip 多文件包(≤20MB 解压 / ≤200 文件 / 根必须 index.html) | 同一端点按 Content-Type 分流;zip 内文本文件逐个过 18+1 条安检(第 9 步已落地) |
⑧ App 工作房(「在群里测」)
功能
一个 app 挂一间工作房;房里所有成员自动看到测试版 —— 名单不是名单,是房间成员本身,进人出人可见范围自动跟(不另建内测名单表,那种表必然和房间漂)。
操作
绑房:POST /v1/dev/apps/{id}/workroom {conversation_id} (空串=解绑)
规则:必须是工作房 + 本人建的;一房一 app(唯一索引 uniq_miniapps_workroom 兜底)⚠️ 复盘 🔴#3:games 的绑房还查「房里除系统消息一条正文都没有」(必须新房),Apps 这边我漏抄了 —— 见 ⑫。
⑨ 用户端(App 里长什么样)
| 东西 | 文件 | 说明 |
|---|---|---|
| 入口 | home_screen.dart 发现槽 mode 2 | 底栏第二格长按菜单「Work/办公」→「Apps」(图标 squaresFour;办公占位页已删) |
| Apps 页 | apps_hub_screen.dart | 列表读 /v1/apps;本地优先(上次目录秒显,断网不白屏);失败态 ErrorRetry 不糊异常串 |
| 全屏壳 | miniapp_webview_screen.dart | 先握手后加载;launch 返回的 entry 优先(版本分流);hoopApp 桥在文档第一行 JS 前注入 → 无 init 竞态;握手失败裸跑降级 |
| 示范 app | 🍜 Malaysia Food Guide | 已认领给 Jeff;页脚显示影子身份 = SDK 桥的看得见的自证 |
⚠️ App 侧改动等下一班 TestFlight;后端全部即时生效。
⑩ SDK 能力面(window.hoopApp)—— 「扶持」的落点
| 能力 | 状态 | 说明 |
|---|---|---|
init() → {app_id, app_user_id, sdk_version, capabilities} | 生产 | 影子身份,先握手后注入 |
user.profile() | 生产 | 只有影子;真名/头像走 consent(000193 地基现成) |
storage.get/set | 生产 | 见 ⑤ |
social.friendsInApp() 你的好友里谁也在用 | 规划 | 只回影子 id 集合;必须过 is_shadowed 闸 |
social.degree() 六度距离 | 规划 | 交友类 app 杀手锏;只给数字不给路径 |
social.invite() 发 app 卡进聊天 | 规划 | 必经用户确认弹层,app 不能静默发 |
pay.charge() 绿能量支付 | 规划 | 钱包/流水表现成;先只开这一种 |
notify.push() | 规划 | 平台节流+审核,绝不给裸推送权 |
⑪ 隐私闸(和微信最大的不同,品牌线)
- 默认影子身份:app 天生不知道你是谁;游戏/小程序影子分域,拼不上是数学事实
- 关系链 API 全过
is_shadowed闸:暗影好友绝不出现在任何 app 的任何返回里 - 六度只给数字不给路径:路径就是社交图,给了就回不了头
- 真名/头像 = consent 记录在案(谁/何时/同意了什么,000189/000193 现成)
⑫ 复盘:不完善清单(按危险排序,诚实的)
GET /v1/admin/apps/review 带 try_url(批前先玩到)。?t= 短时签名票(HMAC,1 小时,launch 时只发给 owner/分身/房成员);
没票或票过期 = 404 不解释(不暴露"这版存在")。live 版照旧公开 + immutable 缓存。
生产实测:owner 入口带票、撕票裸访问 404、带票 index/子文件都通。games 同病仍在(那边风险低一档,记在 GAME_STAGING)。POST /v1/dev/apps/{id}/rollback:
live 指针拨回上一笔 promote 的 from_version;留痕 promotions 表、不碰 entry_url(games 两课都带上);
首版之前是空 = 409。快照不可变,回滚只是拨指针。生产实测:v1→v2→回 v1 全链 + 首版 409。developer.ScanForbidden(和 AI 造游戏同一张 18+1 条规则表,单一出处铁律 23)。
接 zip 时把规则表修准了一处:<script src= 只禁外链(相对引用是多文件 app 的正当需求),
顺手补了 ESM import "https://…" 外链这条漏网。生产实测:恶意 fetch 422 / 路径穿越 400 / 外链 script 422 / 正当 3 文件包 200。GET /v1/dev/apps/{id}/kv/stats 只给 users/keys/bytes 三个数字 ——
故意不给键名和值:用户 KV 是用户在这个 app 里的私产,这是和微信云开发的分水岭(隐私品牌线贯穿到底)。
图像库字节总账:除 200 张件数闸外,SUM(size_bytes)≤400MB。
共享数据(000232 miniapp_data,微信"共享集合"的位置):owner/分身写、全员读同一份,
64KB/值、256 键/app;桥出 hoopApp.data.get(只读)—— Food Guide 的餐厅表、新闻的栏目表住这儿,不用烧进代码。
生产 13 路实测含真路人(非分身账号):读 404 / 写 403 / stats 403 / rollback 403。🔍 2026-08-19 全包审计(Jeff 派题:「审计我们的小程序 Apps 平台的代码」)
codeServe 判「给不给这个文件」只看 v == live_version,整个函数不查 status ——
于是 disabled(刚做的下架)、review(没过审)、驳回回 draft 的 app,直链
/miniapp-code/{app}/v{N}/ 一律 200,还挂着 immutable 永久缓存。
list 和 launch 都挡住了它们,唯独真正装着页面的那道门没挡。「下架」如果只是从列表里消失,那不叫下架。
修:新纯函数 publiclyServable(status,v,live) 当唯一出处,其余一律要签名票;
连它的两个调用方一起改(launch 给 owner 发票 · 审核员 try_url 自带票)——
闸修好了先把自己人挡在外面,才是真事故。
生产实测:上架 200 → 按下停用 404 → 恢复 200 → 待审 404 而 owner 带票 200。inherits_live,
免得开发者看到空列表以为 app 那边也读不到。生产实测:staging 环境下读到了 live 那份。logPush 故意不验身份(app 跑在谁手机上就是谁在打,这是对的),
但配合「每 app 只留最近 500 条」的自动修剪,任何登录用户都能朝别人的 app 灌满 500 条,把开发者真实的报错全挤掉 ——
修剪本身就是那把刀。原注释写着「刷掉的也只是他自己 app 的旧日志」,那句话是错的:它假设了刷的人是主人。
修:内存限流每人每 app 30 条 / 10 分钟(一个字节身份都不落库 —— 那张表的第一条规矩不为配额破例);
超限回 200 不写(回 429 只会让 app 重试得更凶)。生产实测:路人灌 35 条,落库 30 条。coming in step 7(那步早做完了)=
假路标,换成六条真端点;mediaServe 顺同一句 SQL 取 status,停用的 app 图也跟着下架
(只挡 disabled 不挡 draft/review —— WebView 里的 <img> 不带鉴权头,一起挡了 owner 连自己的图都看不见)。entry_url 是个死字段。
codePromote 往库里写的是 /miniapp-code/{id}/ —— 不带版本号,
而直出路由要 /{app}/{v}/{path},所以这个地址永远解析不出版本、必然 404。
今天没人受影响:launch 会返回带版本的真地址,壳优先用它;只有「launch 失败退回目录地址」那条路会撞上,
而那时候 app 本来也进不去。为什么不顺手改:正确的修法不是补个版本号(版本会变,一写就腐),
而是让 launch 成为地址的唯一出处、目录不再存这一列(铁律 23)—— 那要动客户端,不该塞进一次审计里。hosted_bundles 托管底座 —— 现在 miniapps 和 games 各自一套同构的三列+留痕表,是有意的过渡:先各自干净,等两边都稳定后一次迁移共用底座,别在功能高速期动地基。判据写在 MINIAPP_PLATFORM.md ⑦。⑬ 路线图(Jeff 2026-08-15 定框架重排:托盘 = 定规矩 + 定空间)
| 步 | 类 | 内容 | 依赖 |
|---|---|---|---|
| 8 | 规矩 | 生产上架审核流:draft→review→active,守门只归管理员 | done 2026-08-15 |
| 9 | 空间 | 生产staging 签名(✅#2)+ zip 多文件 + 上传静态扫描(✅#5) | done 2026-08-15 |
| 10 | 规矩 | 能力三道门:manifest 声明 → 审核看过 → 运行时授权(capabilities 从恒空开始逐个亮) | 8 |
| 11 | 空间 | 生产KV 总账(只数字不泄值)+ 图像库字节配额 + app 级共享数据(000232)+ 一键回滚 | done 2026-08-15 |
| 12 | 门面 | 开发者中心界面:把七件套画成按钮(🟠#6) | 要出 TF 包 |
| 13 | 生态 | AI 造 app 管线 + 首批示范(新闻头条/交友);SDK social/pay 随三道门开闸 | 8/10 |
| 边角 | — | 绑房新房检查(🔴#3 降级:工作房不在主线)· rollback 端点(🟠#4)—— 顺手清,不占主位 | 无 |
相关:MINIAPP_PLATFORM.md(判据与拍板过程)· GAME_STAGING.md(双轨先行者,含 8 条评审)· GAME_PLATFORM_SPEC.md(影子身份)· GAME_KIT.md(烘焙库模式)· DEVELOPER_PLATFORM.md(审核分成)。 本页由架构线维护;改了实现记得回来改这页 —— 图纸最坏的状态不是没有,是看起来像真相。