| 档案 | |
|---|---|
module |
studio |
summary |
工作室:HoopStudio 在代码上就是一屏(developer_center_screen.dart),格的名单只有一份(游戏 / 音乐 / 视频 / 钱包 / 小程序,底栏只画四格但下标不挪);创作者身份只有一张表 developers,status 由 ADMIN_UIDS 名单里的人批,一次放行游戏 / 音乐 / 小程序三条线(想只给一条做不到),批与驳都不发通知;能不能管一个游戏 / 小程序的判据是「本人 / 平台管理员 / 这个作品自己那间工作房里的分身(小程序)或成员分身、房管理员(游戏)」,「任何分身都能管」那笔债 08-16 已还,只剩「任何分身都能建小程序」一条;小程序是独立四张主表不寄生 games,代码上传 → 安检(和 AI 造游戏同一把扫描器)→ staging 发号(单句 UPDATE 先提交,对象后写,写失败时 staging 已指向不完整的那版)→ promote 一个事务翻指针 + 打 build + 首次上架进 review(指针在事务外读、不查对象完整、不查 build 标记影响行数);数据按 live / staging 分环境、影子 id 服务端算;权限只有三种(camera / photos / location),生效 = 申报 ∩ 授权,撤权只写 revoked_at、彻底删 app 才级联删授权行,加权限才重审;三个「素材」不是一件事;工坊页面归 rooms、内容归 studio,App 里 Skills / Assets 两段 08-21 已撤而后端端点还在;视频那格是占位没有实现;AI 造游戏配额有 Redis 才计数、Redis 报错 fail-closed、安检后才烘焙、已上线的只到 staging;分身能建能 Promote,只有删要本人或管理员。 |
updated |
2026-09-09 |
verified_ref |
a22730cf9659628190eccd1c391f40e0a57fc77c |
source_ref |
[backend/internal/domain/developer/{developer,studio,assets,asset_file,kit_manifest,cover}.go, backend/internal/domain/miniapp/{miniapp,capabilities,lifecycle,staging_door}.go, backend/internal/domain/workroom/{resources,resources_assets,console}.go(只引), backend/migrations/000131 000175 000193 000218-000242 000280 000318 000319, app/lib/screens/{developer_center,hosting_workspace,miniapp_webview,app_workspace,apps_hub,app_permissions,app_review_queue,ai_game_builder}_screen.dart, app/lib/widgets/studio/*.dart, app/lib/widgets/hoop_space_drawer.dart, app/lib/util/app_capabilities.dart] |
status |
current |
review_after |
2026-12-09 |
owner |
nova |
supersedes |
[DEVELOPER_PLATFORM.md(07-08 立项图纸,七处结构断言已被推翻,只留三条判据), hoopstudio/rename-audit-developer-center.md(四层判据吸收,数字全腐), hoopstudio/DOC_SYNC_TASKS_2026-09-07.md(一次性工单,七件完成六件,只留两句规矩), hoopstudio/hoopstudio-video-prd.md(压成 §3.6 一节), hoopstudio/hoopstudio-apps-prd.md(整份并进 §3.3-§3.5,Apps 不另开 L3)] |
partially_supersedes |
[hoopstudio/hoopstudio-prd.md(外壳 / 身份 / 角色 / 命名空间 / 环境变量 / 援兵入口登记归本文;钱 → wallet.md,托管 → games.md,音乐 → music.md), hoopstudio/hoopstudio-game-prd.md(partner_kind / Tools 上传门 / AI 造游戏三道防线归本文,其余已归 games.md;它继续留作产品规格与改动史), MINIAPP_PLATFORM.md(定位 / 判据 / 七件套 / 不做四条 / 待拍板归本文;§四 SDK 表大半未实现,§五·五第 7 条 canManage 债已作废), app_studio.html(五个概念 / 中心 vs 工作室分工表 / 数据三层 / 回滚红线 / 审核规则归本文;⑩ 两处「有洞」已由 ⑪ 销掉,上线权按 v1.927 改), GAME_KIT.md(§10 工坊三段命名判据 + §9.4 素材库约定), game_asset_pipeline.html(全份是未实现蓝图,只登记)] |
这份怎么读:每一节先讲「它解决什么、用户看到什么」,再讲「为什么这样做、数据在哪、哪一步会断、断了留下什么、有什么保障和缺口」;代码位置、参数、行号收在每段末尾的「出处」里。 出处标记:
[code@a22730cf 文件:行]= 那棵树上的实现;[prod@日期,谁核]= 生产机当天核过;[decision@日期,谁]= 拍板过的决定。代码证明「那一版怎么做的」,证明不了「线上是这样」和「应该是这样」。 边界:游戏托管的版本 / 发号 / promote / 探活归 games.md;音乐创作归 music.md;钱(dev_ledger / 提现 / eKYC / 广告分账)归 wallet.md;工作房的页面、材料柜、密钥保险箱归 rooms;援兵计划「申请开通」那三屏长在这一屏上但归 sentinel(docs/援兵计划-MCP.md);外部 AI bot 进群是另一刀(docs/AI_BOTS.md)。三个「素材」不是一件事:①app/assets/进包的美术件(ASSETS_R2.md+ 机器生成的assets.html);②game-assets/{pack}/manifest.json素材包库(工坊 Assets 段);③ 每个小程序自己的素材库(POST /v1/dev/apps/{id}/media)。本文只写 ②③。
0. 给接手的人:三分钟读懂工作室
先认六个词:HoopStudio = 创作者侧统一入口,代码上就是一屏 DeveloperCenterScreen,五个格(游戏 / 音乐 / 视频 / 钱包 / 小程序)一份名单 studioTabs,底栏画四格(钱包 08-29 撤下)但下标一个不挪;开发者身份 = developers 表一行,user_id 就是 HOOP 账号,不是另一套账号,status 只有 pending / approved / rejected 三个真会被写的值;canManage = 「能不能动这个作品」的判据,本人 / 平台管理员 / 这个作品自己那间工作房里的分身,不是「是分身就行」;小程序(App) = 独立四张主表(miniapps / miniapp_versions / miniapp_kv / miniapp_data)+ 权限两张 + 素材 + 日志,和游戏同构但故意三处不同(权限模型 / 数据分环境 / 能彻底删);工坊(Workshop) = 工作房 Setup 页里的三段 Skills / Assets / Services,页面归 rooms,内容(hoop-kit 模块表 / R2 素材包 / 密钥保险箱)归这里;四层改名 = 「Developer Center」改「HoopStudio」只改了界面文案那一层,/v1/dev/* 和 developers 表一个字没动。
它保证到什么程度:
1. 一次审批,三条线全通:developers.status='approved' 是游戏、音乐、小程序、聊天工具上传共用的门;想只给一条线做不到。批的人是 ADMIN_UIDS 环境变量名单,不是角色表;批与驳都不发通知,申请人自己回来看。status 列没有 CHECK,banned 只在注释里,没有代码写它。
2. 谁能动作品,已经从「任何分身」收窄过:小程序 canManageBy = 本人 ∨ 管理员 ∨ (分身 ∧ 这个 app 绑的工作房现任成员);游戏 releaseVerdict = 本人 ∨ 管理员 ∨ 房 owner / admin ∨ (分身 ∧ 绑房现任成员),查询失败一律拒。残留一条:createApp 对任何 is_agent 账号放行,分身能在自己名下建 draft 小程序(碰不到别人的)。删的门 canDeleteBy = 本人 ∨ 管理员,不看分身身份:分身凭绑房成员身份不能删别人的,但它自己建在名下的那个它就是 dev_id,能删。「AI 永远不能自己 Promote Live」是 08-16 的决定,不是今天的代码:codePromote 过的是 canManage,绑房分身能按。
3. 小程序上传的代码过和 AI 造游戏同一把安检(ScanForbidden,注释写 18 条、实测 21 条),命中 422 forbidden_code;安检过了才由一句 UPDATE … GREATEST(live,staging)+1 RETURNING 发号(先提交,对象后写,无顾问锁)—— 所以 PutObject 半途失败时 staging_version 已指向那个不完整的 v{N},能看 staging 的人下次 launch 就拿到它;promote 一个事务里翻 live、首次上架进 review、写 promotions、打 is_build,但 live / staging 两个数在事务外读、没有条件更新,codePromote 只看 staging ≠ 0 且 ≠ live,不查对象齐不齐,不完整那版也能被上线;is_build 那句不查影响行数,miniapp_versions 缺行时切了 live 没打标;回滚只能退到「上一次 promote 的来源版本」否则 409。
4. 数据按环境隔离、身份是影子:miniapp_kv / miniapp_data 主键带 env;运行端(launch / KV / 共享数据读)谁看 staging 由 envAndVersion 定 —— 只在有待上线版本(staging > 0 且 ≠ live)时才分流,本人 / 管理员 / 绑房现任成员进 staging,其余人和「没待上线版本」时全在 live;管理端(dataSet / dataList)是开发者自己挑 ?env=,不带 = live,两套入口两种规矩;app_user_id 是服务端两级 HMAC 算的 a_ 前缀 id,永远不收客户端传的;租户隔离运行时靠鉴权 + 每条 SQL 自带的 app_id / env / app_user_id 条件,另有一条读源码的回归守卫数每条 SQL 有没有 app_id=$n(它不是运行时的墙)—— 08-22 在生产库试过 RLS 不生效(hoop 角色 SUPERUSER + BYPASSRLS),当前生产角色未再核。
5. 权限只有三种、生效 = 申报 ∩ 授权:camera / photos / location,白名单在 Go 和 DB CHECK 各写一份(有意);撤权只写 revoked_at 不删行、撤申报也不动授权行,但重新授权会覆盖同一行(granted_at=now(), revoked_at=NULL),所以留的是最近一次状态,不是完整的授权 / 撤回历史(故意不给 (app_id, cap) 指向申报表的复合外键);app_id 对 miniapps 有 ON DELETE CASCADE,所以彻底删 app 会级联删掉授权行;撤回申报 = 用户授权还在也用不了;只有 active 的 app 加权限才重回审核,减权限或改理由不罚。
6. 视频那格是占位:后端没有 video 域、没有 video 迁移;卡片画「还没开门」不摆一排 0,守卫钉着不许画假数。
7. AI 造游戏(在 developer 包):配额每日 10 轮 —— 有注入 Redis 才计数,Redis 报错 503 fail-closed 不放行,没注入 Redis 时整段跳过;两遍各 200 秒;安检之后才烘焙 hoop-kit;从没发布过的直接上 live、已上线的只到 staging 等人按 Promote。CHANGELOG v1.919 记着 09-03 到 09-08 从 App 那条路一次都没成功过(由 App 20 秒全局超时 + 一轮 70-120 秒推导,配两条 context canceled 日志;不是覆盖五天的成功率统计),旧 PRD 全把它写成可用。
当前已核实没做的(§4 全表,先说五条):批准 / 驳回开发者、批准 / 驳回小程序都不发通知(驳回理由只进日志);小程序没有计费 / 分成路径;window.hoopApp 图纸里的 pay / social / notify / deeplink 都没做;外部 AI bot 表建了、执行器一行 Go 都没有;game-assets/ 素材包库没有任何后端写入路径(工坊 Assets 段没有「+」,工人拿不到原图;每个小程序自己的素材库有上传口,别混)。
出了问题先查哪:§5 —— 「申请了没动静」先看 ADMIN_UIDS 有没有人去点;「按不了上线 / 改不了 app」先算 canManage 三条;「staging 看到的不对」先看有没有待上线版本、再看 envAndVersion 把你判成谁、再看你走的是运行端还是管理端;「上传 500」看安检、发号、对象写三步,500 之后 staging 已经指着那版;「三个素材」先分清是哪一个。
1. 两段流程的一生
1.1 从申请开发者到一个小程序上线
你看到的:「我的」页或侧栏进 HoopStudio → 没申请过看到申请表(勾同意开发者条款才能交)→ 交了显示「审核中」→ 某天再进来是五格的工作室 → 小程序格 ➕ 起名、emoji 图标 → 进 app 工作页传一个 zip → 「测试版」按钮亮了 → 满意点 Promote → 第一次是「已提交审核」,过审才在 Apps 目录里出现;之后再 Promote 直接上。
背后发生的:
1. 申请:POST /v1/dev/apply(留言 ≤ 500 字节),UPSERT:没行 → pending;rejected → 回 pending;approved 不动。幂等,没有限流、没有通知。App 侧只有勾了条款才能按,交完顺手记一条 consent_records。
2. 审批:GET /v1/dev/admin/pending 对非管理员回 200 + is_admin:false(不是 403);POST /v1/dev/admin/dev 只认 ADMIN_UIDS,approve → approved + approved_at(这列写了没人读),否则 rejected。不发通知。
3. 门(按类别,不是一句「都过」):developer 包里游戏 / 素材 / AI 造游戏 / 创作者档案这些端点过 requireApproved(403 not_developer);apply / me 不过(没批的人得能申请、能看状态);admin/* 过 ADMIN_UIDS;小程序 /v1/dev/apps* 不走 requireApproved —— 建 app 是 approved ∨ is_agent,其余每条各自过 canManage;音乐是自己两份手抄的同一句 SQL;聊天工具上传靠注入的 StatusOf,没注入 = 503 关门不是开门。App 侧网络失败时不能当「没申请过」,否则断网把工作室变成申请表。
4. 建 app POST /v1/dev/apps:名 1–60 字节(UTF-8,和 Go len 一致)、标语 ≤ 120;id app-{hex6};status='draft';entry_url 写成 '/miniapp-code/'||id||'/'(没有版本段,只是 launch 失败时 App 的兜底,不是能开的地址)。
5. 传代码 POST /v1/dev/apps/{id}/code:body ≤ 20 MB;zip → ≤ 200 文件、解压 ≤ 20 MB、../ 或绝对路径 400 bad_path、根目录必须有 index.html;单文件 ≤ 2 MB 当 index.html。每个文本类文件过 ScanForbidden → 422 forbidden_code。发号:UPDATE miniapps SET staging_version = GREATEST(live_version, staging_version)+1 RETURNING(单句原子,无顾问锁)→ 逐个 PutObject 到 miniapps/{app}/v{N}/{name} → 写 miniapp_versions(失败只记日志,上传照报成功)→ 房里发 app_uploaded。
6. 试玩:POST /v1/apps/{id}/launch 先过一道门:app 不是 active 时只有 canManage 的人能开(普通房成员对 draft / review / disabled 拿 404);过了门 envAndVersion 再分流 —— 有待上线版本(staging > 0 且 ≠ live)且你是本人 / 管理员 / 绑房现任成员 → staging,发 1 小时票(cookie Path 钉在 /miniapp-code/{app}/v{N}/);没有待上线版本时所有人都在 live;查版本那句出错被忽略,当 live / 0 处理,不是查询失败一律拒。App 侧那个 staging 参数没人读,「正式 / 测试」两颗键今天开的是同一个版本。
7. Promote POST /v1/dev/apps/{id}/promote(过 canManage,绑房分身也能按):先在事务外读 live / staging,staging = 0 或 = live → 400 no_staging;然后一个事务:live_version = staging(无条件更新,不核对刚才读到的值);status = CASE WHEN 'active' THEN 'active' ELSE 'review' END(首次上架进审核,之后迭代不用);entry_url 只在空时填;写 miniapp_promotions;miniapp_versions.is_build = true 同一事务但不查影响行数;Commit 后再查一次 status(错误被忽略),按它发 app_promoted_pending 或 app_promoted 房事件。
8. 审核 GET /v1/admin/apps/review 列 review 的并给每个签 1 小时试玩票;POST /v1/admin/apps/{id}/review:approve → active,否则回 draft;WHERE status='review' + 影响行数 0 → 409 not_in_review;驳回理由只进 slog,不落库不通知。
9. 上架:GET /v1/apps 只列 active;GET /miniapp-code/{app}/{v}/{path} 对 live + active 公开,其余要票,没票只 404。
哪一步会断、留下什么:第 1 步重复交 = 幂等 200;第 2 步没人点 = 永远 pending,没人提醒;第 4 步分身建 = 成功(残留的宽口);第 5 步安检命中 = 422、号没发、什么都没写;发号成功后某个 PutObject 失败 = 500,已写的对象留在 v{N} 下,而 staging_version 已经指向这个 v{N}(发号那句先提交了;游戏那条路有 abort 清理,这条没有)—— live 没动,但能看 staging 的人下次 launch 拿到的就是这个不完整的版本,且 promote 不查对象齐不齐,它也能被上线;再传一次拿 v{N+1} 才盖过去;miniapp_versions 写失败 = 上传成功但版本历史缺一行,之后 promote 这版时 is_build 更新影响 0 行也不报错;第 7 步 Commit 之前任一步失败 = 回滚、明写「已回退」;Commit 本身出错 = 结果未知;Commit 后回包失败 = 已生效;上传 / promote / 回滚交错时用的是各自事务外读到的旧指针,没有串行化;第 8 步驳回 = 回 draft,开发者看不到理由;第 9 步 disable / enable = 只从 active 能 disable、enable 永远不能把没审过的变 active。
出处:申请
[code@a22730cf backend/internal/domain/developer/developer.go:2569-2612],状态:2388-2413,门:2398-2410,管理员队列:2854-2859,2898,批驳:2907-2935;名单[code@a22730cf backend/internal/config/config.go:45-59,326-338];三线共用门[code@a22730cf backend/internal/domain/music/handler.go:311-325]、[code@a22730cf backend/internal/domain/music/collab_eligible.go:45-57]、[code@a22730cf backend/internal/server/router.go:286-288];表[code@a22730cf backend/migrations/000131_developers.up.sql:4-10];建 app[code@a22730cf backend/internal/domain/miniapp/miniapp.go:853-925](is_agent放行:861-864,名长:800,826);传代码miniapp.go:1288-1404(安检:1350-1360,发号:1362-1366,对象:1381-1384,账本失败只记:1398-1400);试玩miniapp.go:165-203,221-248,票:1473;promote:1513-1586(is_build 同事务:1550-1559);回滚:1588-1633;审核:1784-1838;公开 serve:1635;App 侧申请表[code@a22730cf app/lib/screens/developer_center_screen.dart:1752-1804,1953-1963],断网不当没申请:568-583,小程序容器[code@a22730cf app/lib/screens/miniapp_webview_screen.dart:35-44,93-101,205-212];守卫miniapp/build_marked_on_promote_test.go、lifecycle_test.go:126、staging_ticket_test.go、serve_gate_test.go、app/test/dev_center_new_app_test.dart[decision@2026-07-08,Jeff:开发者体验平台是核心产品不是内部工具;不做 Web 控制台][decision@2026-08-12,Jeff:「四分天下,不要把 Apps 埋在 Games 之下」][decision@2026-08-13,Jeff:开发者旅程七件套原话定案][decision@2026-08-16,Jeff:AI 最多改 → Upload → Staging → Test,永远不能自己 Promote Live][decision@2026-08-30,Jeff:build 才等于我们的 version]。
1.2 一个小程序的权限:从申报到撤回
你看到的:开发者在 app 工作页勾「相机」并写一句理由;用户打开这个 app,它想拍照时弹出 HOOP 的授权框(不是系统框),同意后才轮到系统相机;用户在「设置 → 小程序权限」能看到谁拿了什么、一键撤回;开发者事后多勾一个「位置」,app 立刻回到审核队列。
背后发生的:
1. 申报 PUT /v1/dev/apps/{id}/capabilities:全量覆盖;cap 只能是 camera / photos / location(Go 白名单 + DB CHECK 各一份,有意);理由 ≤ 200 字,给用户原样看;未知 cap 400 bad_cap。App 侧 PUT 是全量,所以未知的会被抹掉(注释写明代价)。
2. 生效 = effectiveCaps = 申报 ∩ 授权。launch 握手里带的 capabilities 是那一刻的快照;桥上 hoopApp.permissions() / requestPermission() 每次重新问壳,不读快照。
3. 授权 POST /v1/apps/{id}/permissions/{cap} {granted:true}:没申报 → 409 not_declared;写 miniapp_grants(app_id, app_user_id, cap)。App 侧 _ask 三道顺序:已授权 → true;没申报 → false 且不弹框;否则 HOOP 自己的授权框 → 非 200 一律当没授权;单飞不并发。权限表拉失败 = 全部当未申报未授权(fail-closed)。
4. 原生那一层:getUserMedia 只放相机、任何麦克风请求直接拒 —— 这个回调在插件里标 iOS 15+,iOS 13 / 14 上不会被调用(默认行为是拒,不是放);iOS 上 navigator.geolocation 我们拦不住(插件没有 onGeolocationPermissionsShowPrompt 的 iOS 实现),唯一的闸是 app 自愿走 hoopApp.requestPermission('location') —— 这整段是读插件源码得出的,没在真机验过。
5. 撤回:用户侧 GET /v1/me/app-permissions 列「谁拿了我什么」,POST …/permissions/{cap} {granted:false} → revoked_at,行不删;开发者撤回申报 → 用户授权行还在但 effectiveCaps 里没有 → 用不了;只有彻底删 app 时授权行才跟着 ON DELETE CASCADE 没掉。
6. 改申报触发复审:shouldTriggerReview(old, new, status) —— 只对 active 的 app、只在加了 cap 时 → 回 review;减 cap、改理由不罚;App 侧收到 review_triggered:true 弹阻断对话框不是 toast。
哪一步会断、留下什么:第 1 步全量 PUT 带错 = 整份拒;第 3 步用户拒 = 不写行;第 4 步 iOS 定位 = 按源码推断系统框会绕过我们(未真机验);第 5 步撤回只改 revoked_at,再点头会把同一行的 granted_at 覆盖、revoked_at 清空 —— 一行只留最近一次状态;彻底删 app 时这行级联没掉;第 6 步复审期间 app 从目录消失(GET /v1/apps 只列 active),已装的用户 launch 走 canManage 那条 —— 普通用户 404。
出处:白名单与申报
[code@a22730cf backend/internal/domain/miniapp/capabilities.go:35-43,61-82,134-175],复审判据:88-110,交集effectiveCaps(同文件);授权与撤回[code@a22730cf backend/internal/domain/miniapp/capabilities.go:326-388](重新授权覆盖:371-374,撤回:378-381),「我给了谁什么」总览:398起;表[code@a22730cf backend/migrations/000238_miniapp_capabilities.up.sql:17-42](不给指向申报表的复合外键:29-34,对miniapps的级联:36);App 侧[code@a22730cf app/lib/screens/miniapp_webview_screen.dart:81,103-166,170-206,286-307,436-453]、[code@a22730cf app/lib/util/app_capabilities.dart:32-50]、[code@a22730cf app/lib/screens/app_permissions_screen.dart:44-47,60-73,87-117]、[code@a22730cf app/lib/screens/app_workspace_screen.dart:189-278,949-1041];守卫miniapp/capabilities_test.go:54-159、app/test/app_permissions_golden_test.dart[decision@2026-08-16,Jeff:数据三层,Hoop 用户私有资料永不放开、App 自产数据按用户授权、公共数据][decision@2026-08-16,Jeff:审核规则改掉「永远不用审」—— 新增敏感能力要推回审核队列]。
2. 用户能看到的能力(怎么用 / 限制 / 入口)
| 能力 | 怎么用 / 限制 | 后端入口 | App 落点 |
|---|---|---|---|
| 进 HoopStudio | 五格一份名单,底栏四格;侧栏 / 「我的」/ 设置 / 音乐页 / 工作房都能进 | — | developer_center_screen(studioTabs) |
| 申请开发者 | 勾条款才能交;pending / banned 只看到一句话;网络失败不当没申请 | POST /v1/dev/apply、GET /v1/dev/me |
_applyView |
| 四线一页的数 | GET /v1/dev/studio:查不到写「还没有」不写 0,拿不到用 -1,一段挂了不拖黑整页 |
GET /v1/dev/studio |
数据页 |
| 我的游戏 | 建 / 传 / AI 造 / 托管台;上线权三种人(主人 / 房管理员 / 绑房分身) | /v1/dev/games*(托管细节 → games.md) |
游戏格、hosting_workspace_screen |
| 我的音乐 | 内嵌 MusicStudioScreen(embedded:true),不是整屏 push(08-27 改) |
→ music.md | 音乐格 |
| 我的视频 | 占位卡,没有实现 | 无 | _videoTab |
| 钱包 | 底栏没有这格,页还在;侧栏 Wallet 09-07 起改落到「我的」页 Creator 钱包 | → wallet.md | walletTab = 3 |
| 我的小程序 | 建 / 改名 / 图标(emoji 或本 app 素材库里的图)/ 传代码 / 试玩 / Promote / 回滚 / 停用 / 恢复 / 彻底删 | /v1/dev/apps*(35 条) |
小程序格、app_workspace_screen |
| 小程序素材库 | 图 ≤ 2 MB(按文件头魔数判类型,SVG 拒;不是完整的安全扫描)、音视频 ≤ 20 MB、≤ 200 件、≤ 400 MB(先数再写,非原子);先 PutObject 再 INSERT,登记失败尝试删对象且不看结果 | POST/GET /v1/dev/apps/{id}/media |
_mediaCard |
| 小程序数据 | 每用户 KV 16 KB × 128 键 / 环境,开发者只看计数不看值;共享数据 64 KB × 256 键 / 环境,开发者能看能改(dataList 回 k / v);两个上限都是先 count 再写,不是并发硬上限 |
/v1/apps/{id}/kv/*、/v1/dev/apps/{id}/data*、kv/stats |
桥 hoopApp.storage / data |
| 小程序日志 | 任何登录用户可报,永不非 200;每 app 留 500 条;不带任何用户身份列 | POST /v1/apps/{id}/logs、GET /v1/dev/apps/{id}/logs |
AppLogReporter、日志卡 |
| 小程序权限 | 三种;申报 / 授权 / 撤回;加权限重审 | 见 §1.2 | 见 §1.2 |
| Apps 目录 | 只列 active;3 列网格;缓存先画 | GET /v1/apps |
apps_hub_screen |
| 审核台(管理员) | 开发者 / 游戏 / 小程序 / 歌 / KYC / 提现 / 援兵 —— 同一份 ADMIN_UIDS 名单扛七条队列 |
/v1/dev/admin/*、/v1/admin/apps/* |
⋮ 菜单里的 Admin |
| 工坊 | 工作房 Setup 页:材料柜(七类)+ Services(密钥保险箱);Skills / Assets 段 08-21 撤了 | /v1/workrooms/resources*(rooms);/v1/games/kit、/v1/games/assets(还在,App 零调用) |
workroom_deskboard_screen |
| 素材包库 | game-assets/{pack}/manifest.json 现算目录,≤ 2000 包,60 秒缓存;单文件签名 302 或代理;没有上传入口 |
GET /v1/games/assets、/v1/games/assets/file |
无 |
| 援兵「申请开通」 | 长在这一屏上;归 sentinel | /v1/reinforce/*(7 条) |
三屏 |
出处:格名单
[code@a22730cf app/lib/screens/developer_center_screen.dart:197-235,1969-1985];侧栏 Wallet 新落点[code@a22730cf app/lib/widgets/hoop_space_drawer.dart:283-306];/v1/dev/studio[code@a22730cf backend/internal/domain/developer/studio.go:1];小程序路由miniapp.go:89-128,上限:261-262,328-329,458-463,929-931;素材包库[code@a22730cf backend/internal/domain/developer/assets.go:20-38,176-208]、[code@a22730cf backend/internal/domain/developer/asset_file.go:35-39,82-122,156-223];工坊 Skills / Assets 撤[code@a22730cf app/lib/screens/workroom_deskboard_screen.dart:701-716];七条队列hoopstudio-prd.md§13。
3. 内部怎么运作
3.1 外壳:一屏、一份名单、四层改名
怎么做的:studioTabs 五格,navTabs = 去掉 walletTab 只画四格,下标不挪(李敏 08-29「Remove the wallet from studio navigation bar」);底栏 StudioNavBar 不复用 HoopNavBar(那个钉死四格 + 固定 ➕),selectedIndex:-1 合法 = 从侧栏进来不高亮;中间那颗 ✦ 在没有建路的格上是命名的空操作 _centerNoop(李敏 08-31 反转:五页要长一样),注释明写别改回 null;五页共用一颗 StudioBackButton(形状写死)、kStudioLeadingWidth 44 / kStudioTitleSpacing 4;⋮ 菜单必须是 showHoopGlassMenu(有读源码的守卫);extendBody:true,每个列表自己用 _navInset 留底(Mike 09-03「不可以留白,要 floating」)。钱包格标题还是普通 Text,没上 HoopPageHeader。四层改名:① 文案 dev_center = "HoopStudio" 改了;② 键名不改;③ /v1/dev/* 55 条不改(老包 404 在 App 里长得像网络不好);④ developers 表不改(三线共用 + 连着账本)。条款页 developer-terms.html 还写 4 处「Developer Center」,改名后旧同意算不算同意新条款,是人的决定,没人拍。
出处:
developer_center_screen.dart:197-235,1969-1985,2143-2155,4020-4027;[code@a22730cf app/lib/widgets/studio_nav_bar.dart:5-23,100-124]、[code@a22730cf app/lib/widgets/studio/studio_top_bar.dart:50-83,107-126];守卫app/test/studio_top_bar_0830_test.dart、studio_nav_placement_0903_test.dart、space_wallet_to_creator_0908_test.dart[decision@2026-08-21,李敏:「UI 改去 HoopStudio 就好了,不用到那么复杂」][decision@2026-08-29,李敏:底栏撤钱包格][decision@2026-09-07,Mike Ng:侧栏 Wallet 落到 Creator 钱包页]。
3.2 身份与角色:一张表、一份名单、三处手抄
见 §1.1 第 1-3 步。补:partner_kind(creator / external)是苹果 4.7.3 分档,只被游戏读(外部合作方要用户逐个同意才拿真名 / 好友,后台只看影子 id),没有任何写路径,只能手写 SQL;三种角色(创作者 / 平台管理员 / 外部合作方)之外还有「AI 分身」这个技术身份,它能做什么按每道门各自写,不从「它是分身」推绝对禁止:建 app 放行(is_agent)、管与 promote 看 canManage(绑房成员即可)、删看 canDeleteBy(本人或管理员,不否决分身:它自己建的它能删)、审核看 ADMIN_UIDS(分身不在名单里就不能审,在就能);developers.bio 只有创作者公开页一处读,游戏社交条不用它;StatusOf 导出了,音乐那边仍两份手抄并要求「手动保持同步」。
出处:
developer.go:2386-2410,2495-2522;[code@a22730cf backend/migrations/000193_dev_partner_kind.up.sql:2-15],读方[code@a22730cf backend/internal/domain/games/repository.go:566-594];三份手抄见 §1.1 出处[decision@2026-08-05,Jeff:partner_kind 三档,默认 creator]。
3.3 小程序:四张主表、同构三异、两级删
怎么做的:miniapps(id, name, icon, icon_url, tagline, entry_url, status ∈ active|disabled|draft|review, dev_id, live_version, staging_version, workroom_id),miniapp_versions(app_id, version, files, bytes, by_user, manifest[], is_build, note),miniapp_promotions(from, to, by_user)(promote 和回滚都记,回滚反着写),miniapp_kv / miniapp_data(主键带 env),miniapp_media,miniapp_capabilities / miniapp_grants,miniapp_logs(零身份列)。不寄生 games:games 表有 36 处读方(Jeff 一问推翻 games.kind='app')。与游戏三处故意不同:权限模型、数据分环境、能彻底删。图标只有 icon 一列在写(emoji 或本 app 素材 URL),icon_url 是死列每次都发空串;PUT …/icon 留着是装出去的包还在调。素材库:先 PutObject 再 INSERT miniapp_media,登记失败时尝试 DeleteObject 且不看结果,可能留孤儿对象 —— 和代码上传那条(不清理)是两种不同的失败。两级删:disable / enable 只改状态(enable 永远不能把没审过的变 active);DELETE 要 confirm = app id、canDeleteBy = 本人 ∨ 管理员(不看分身身份:绑房分身删不了别人的,自己名下建的它是 dev_id 能删;「造错了可以重造,删错了没有回收站」),顺序 purge 对象(每轮 1000、≤ 50 轮)→ 删行(miniapp_grants 等子表跟着级联)→ 解散房,房解散失败吞掉但 roomDissolved:false 老实回;purge 失败时回「什么都没动到数据库」—— 行没动是真,对象可能已删了一部分。manifest[] 老版本是 '{}' = 「当时没记路径」不是「没文件」,所以接口另回 files_recorded。绑房:新房或已绑别房 409,一房一 app 唯一索引。
出处:表
[code@a22730cf backend/migrations/000218_miniapps.up.sql:2-13]、000219 / 000225 / 000230 / 000231 / 000232 / 000237 / 000238 / 000242 / 000280 / 000318 / 000319;miniapp.go:1-3(产品判断)、图标:576-616、manifest:1264-1267,1379-1394、绑房:1720;两级删[code@a22730cf backend/internal/domain/miniapp/lifecycle.go:15-24,64-75,98-99,172-176,274-298];守卫lifecycle_test.go:22-403、can_manage_test.go、set_icon_guard_test.go、app_icon_test.go[decision@2026-08-19,Jeff:「我们组建了 apps 能彻底删除吗」→ 两级删][decision@2026-08-30,李敏:App 图标可以是真图片]。
3.4 小程序:数据、环境、影子、租户
怎么做的:app_user_id = "a_" + hex(HMAC(HMAC(key, "hoop:miniapp:identity:"+appID), appID+"|"+uid))[:24],服务端算,桥上只给这个;KV 每用户每环境 128 键 × 16 KB,超了 429 quota 但已有键可覆盖;共享数据 256 键 × 64 KB;两个配额都是先 count 再 INSERT、无事务无锁,count 出错被忽略,是顺序检查的阈值不是并发硬上限;KV 读不到或读出错都回 200 {v:null},KV / 共享数据的删除忽略 Exec 错误照回 200;dataGet staging 读不到回落 live(代价:staging 里测不了「删键」,dataList 回 inherits_live);运行端 envAndVersion 一个函数同时答代码版本和数据环境(000237 的全部目的),管理端 dataSet / dataList 走 devEnv(?env=,默认 live)—— 两套入口,别让「一个函数」遮掉管理端那份是人挑的;dataGet 另有一道 launch 同款的可见性门;租户隔离运行时靠鉴权和每条 SQL 自带的 app_id / env / app_user_id 条件,tenant_filter_test 是读源码的回归检查(数每条 miniapp_kv / miniapp_data SQL 的 app_id=$n),不是运行时的墙;它抬头记着 08-22 在生产库试过 RLS 不生效(当时角色 SUPERUSER + BYPASSRLS;当前生产角色未再核);kvStats / appDetail 对用户私有 KV 只给计数不给键值,共享数据开发者看全部;日志端点任何登录用户可报、内存限流 30 / 10 分钟、每 app 留 500 条、INSERT 里没有任何用户身份列(有守卫)。
出处:
miniapp.go:81-87,221-248,261-329,351-358,428-453,929-969,1000-1046;[code@a22730cf backend/internal/domain/miniapp/tenant_filter_test.go:18-27,82-110];[code@a22730cf backend/internal/domain/miniapp/logs_test.go:49];[code@a22730cf backend/migrations/000237_miniapp_env_and_versions.up.sql:10-22][decision@2026-08-16,Jeff:「Staging Build 和 Staging Data 必须一起隔离」]。
3.5 小程序容器与桥
怎么做的:launch 失败 = 裸 WebView,window.hoopApp 不存在,app 要能裸跑;桥在 AT_DOCUMENT_START 注入:init / user.profile(只有影子 id) / storage.get,set / data.get / permissions / requestPermission / setBackButton / saveImage / exit;另外两件非 hoopApp 的注入:劫持 <a download> + data:image/(WKWebView 静默丢下载)、AppLogReporter 用 capture 阶段的 error 监听(不抢 app 自己的 window.onerror),握手失败也注;顶栏没有,只有一颗浮动返回键(用 viewPadding 不用 padding,否则钻到刘海下);错误界面:握手失败不画;桥上 KV 调用拿不到响应时回 {err:'unavailable'} 让 app 自己处理 —— 后端吞掉的错(读错回 null、删错回 ok)到不了这个分支。图纸里的 pay / social.friendsInApp / social.degree / notify / deeplink 都没做。
出处:
[code@a22730cf app/lib/screens/miniapp_webview_screen.dart:23-29,223-336,365-391,401-479],[code@a22730cf app/lib/util/app_log_reporter.dart:40-60,137-159];守卫app/test/miniapp_no_header_test.dart、miniapp_save_image_test.dart、app_log_reporter_test.dart;图纸MINIAPP_PLATFORM.md§四。
3.6 视频那格:占位,不是功能
怎么做的:后端 44 个域没有 video、317 份迁移没有 video;App 只有 Key('studio_video_soon') 占位卡,不画 CTA、不画统计、不画目标卡(守卫钉着);创作者看板视频栏画「还没开门」不摆一排 0。真缺的是作品实体 + 转码 + 审核,身份 / 账本 / 提现 / 分成 / 存储 / 看板 / 入口七样已在。摆出来的入口就是承诺(video-prd 的话),这格位被一个不存在的产品占着。
出处:
developer_center_screen.dart:2225-2246;守卫[code@a22730cf app/test/studio_video_apps_0829_test.dart:163][decision@2026-08-29,李敏:「adapt it into the Hoop Video & Hoop App page」只统一外壳]。
3.7 工坊与三个「素材」
怎么做的:工坊三段是 Jeff 08-16「可以,照你说的做」定的(Skills = 能做什么 / Assets = 有什么材料 / Services = 能连谁),命名判据「一个词盖三样,以后每加一样都要重新解释一次」。今天 App 里 Skills / Assets 两段没有了(Jeff 08-21「你 workshop 上面的技能、素材都不要了」),换成材料柜七类(sdk / library / assets / guide / simulator / templates / tools,类别由后端 kinds 给,App 加第八类不用发版)+ Services(密钥保险箱,rooms);后端 GET /v1/games/kit(从 hoop-kit.js 源码解析模块表,拒列 pvp-live.js)和 GET /v1/games/assets 端点还在、App 零调用,后者仍被工作房打包时在进程内调用。素材包库:game-assets/{pack}/manifest.json 是唯一真相,Files 数出来不自报,坏 manifest 跳过并计入 skipped 不编;≤ 2000 包、60 秒只缓存成功;单文件 GET /v1/games/assets/file?key=:key 字符白名单 + path.Clean + 前缀最后判(防 game-assets/../hosted/),四种结局分开(403 / 400 / 404 / 502 store_unreachable 不是 404),有 CDN 就 302 签名,没有就代理且 .svg / .html 强制下载;TTL 借 MEDIA_CDN_TTL。没有任何上传路径:素材管线蓝图 game_asset_pipeline.html 那五张表在迁移和 Go 里都不存在;imagegen(fal)只接在音乐封面上,游戏工人调不动;工人拿不到原图三条路全堵,有守卫钉「目录里再出现 http 地址就红」。
出处:
[code@a22730cf backend/internal/domain/developer/kit_manifest.go:23-27,63-92]、assets.go:20-38,48-57,95,123-166,176-208、asset_file.go:23-39,67-122,124-180,204-207;工作房调用[code@a22730cf backend/internal/domain/workroom/resources.go:83-87,185-194,588-596]、[code@a22730cf backend/internal/domain/workroom/resources_assets.go:50,115];imagegen 只接音乐[code@a22730cf backend/internal/server/router.go:219];App 撤段workroom_deskboard_screen.dart:143-146,701-716,1971-1983;守卫developer/assets_test.go、asset_file_test.go、kit_manifest_test.go[decision@2026-08-16,Jeff:工坊三段][decision@2026-08-21,Jeff:撤 Skills / Assets 段;素材总目录「当然是 B」给人看]。
3.8 AI 造游戏(在 developer 包)
见 §0 第 7 条。补:模型 claude-sonnet-5,没配 503 ai_off;body 10 MB、prompt ≤ 150000 字节(len(string),不是字数)、参考图 ≤ 6 MB 且非法就忽略不挡;历史留 6 轮 × 4000 字;第二遍「苛刻试玩者」失败回落草稿不挡;迭代先读 staging 再读 live(否则每轮改的是陈旧的 live),读失败 502 fail-closed;安检 sanitizeAIGame = ≤ 400 KB + 21 条正则(含 08-18 加的裸 location = / window["location"]、ESM import … from "https://"),命中 422 blocked;烘焙顺序 kit → compat 垫片 → 游戏,幂等;第一版 <title> 当游戏名;id ai-{hex6} 撞三次。ANTHROPIC_API_KEY 一把钥匙供音乐润色和造游戏两条线,上游没按用途分账;造游戏自己有 aib:* 配额(要 Redis),音乐润色那边有没有自己的闸未核。
出处:
developer.go:1182-1207,1262-1330,1341-1355,1367-1411,1436-1493,1489-1562;router.go:184;守卫developer/scan_bypass_test.go:16-58、deploy_race_test.go:54、hoopkit_test.go、hoopcompat_test.go;v1.919 那条「从 App 一次没成功」的来源与推导边界见 §0 第 7 条。
4. 已知缺口与待核事项(逐行看状态)
| 事项 | 状态 | 出处 / 怎么核 |
|---|---|---|
| 批 / 驳开发者不发通知;批 / 驳小程序不发通知、理由只进日志 | 已核实没做 | developer.go:2907-2935、miniapp.go:1822,1827-1837 |
| 小程序计费 / 分成 | 已核实没做:shop / wallet / revshare 里 miniapp 零命中 |
apps-prd A1 |
hoopApp.pay / social / notify / deeplink |
已核实没做(图纸有) | MINIAPP_PLATFORM.md §四 vs miniapp_webview_screen.dart:436-462 |
| 外部 AI bot 执行器 | 已核实没做:room_bots 表在(000217 / 000222),Go 零引用 |
docs/AI_BOTS.md |
game-assets/ 上传 / 素材管线五张表 / imagegen 接游戏 |
已核实没做 | game_asset_pipeline.html、resources.go:185-194 |
| 视频 | 已核实没做 | §3.6 |
createApp 对任何 is_agent 放行 |
现状 / 残留宽口:只能建自己名下的 draft;建了它就是 dev_id,能删自己那个 |
miniapp.go:861-864、lifecycle.go:64-75 |
| 「AI 永远不能自己 Promote Live」 | 决定 ≠ 代码:codePromote 过 canManage,绑房分身能按;游戏侧 releaseVerdict 同样放绑房分身(v1.927) |
miniapp.go:1516、app_studio.html ⑥ 第 4 刀 |
| 上传失败后 staging 指针已指向不完整版本;promote 不查对象完整 | 已核缺口:发号先提交、对象后写;codePromote 只看 staging ≠ 0 且 ≠ live |
miniapp.go:1362-1366,1526 |
promote 指针在事务外读、无条件更新;is_build 不查影响行数;Commit 后查 status 忽略错误 |
现状 / 缺口:并发交错用旧指针(源码推导,未实测);缺 versions 行时切了 live 没 build 标记;房事件按可能失败的查询分支 | miniapp.go:1521-1526,1555-1559,1567 |
envAndVersion 首句查询出错回 live / 0;KV 读错回 200 null、删除忽略 Exec 错误 |
现状 | miniapp.go:221-248,273,292,358,375 |
| KV / 共享数据 / 素材库配额先 count 再写、无锁、count 错忽略 | 现状:阈值是顺序检查,不是并发硬上限 | miniapp.go:302-304,388-390,667-670 |
| 素材库登记失败删对象不看结果 | 现状:可能留孤儿 | miniapp.go:684-689 |
| 代码上传发号无顾问锁、对象写在锁外、失败留残留、版本账本失败只记日志 | 现状 / 缺口(游戏那条有 abort 清理和 pg_advisory_xact_lock);残留不只是对象,见上一行「指针已动」 |
miniapp.go:1362-1400 |
miniapps.entry_url 写成无版本段的路径 |
现状:只是 launch 失败时的兜底,不是能开的地址(是否真 404 未实测) | miniapp.go:1542,:117 |
miniapps.icon_url 死列 |
现状:无写入方,每次发空串;删列要单独一刀先问 | 000319:19,miniapp.go:576-578 |
App 侧 staging 参数没人读,「正式 / 测试」两键开同一版本 |
现状 / 缺口(?env= 那半欠着) |
miniapp_webview_screen.dart:35-44 |
iOS 上 navigator.geolocation 拦不住 |
现状(读插件源码,未真机验) | miniapp_webview_screen.dart:170-187 |
developers.status 无 CHECK、banned 无写路径、approved_at 无读方、partner_kind 无写路径 |
现状 | 000131:6-9、developer.go:2386,2928、000193:11 |
三份手抄的 developers.status='approved' SQL |
现状 / 债 | music/handler.go:319、collab_eligible.go:53,92 |
一份 ADMIN_UIDS 扛七条队列、无细粒度 |
现状 | config.go:326 |
developer-terms.html 4 处旧名;改名后旧同意算不算 |
待拍板(人的决定) | hoopstudio-prd.md M6 |
hosted_bundles 托管底座 |
待拍板:四处记录互不相引;不拍的代价已见(live_version=0 三个含义、Apps 没有 build manifest / 探活) |
MINIAPP_PLATFORM.md:104-106、000230:2 |
| 「新域名 / 安检异常推回审核」 | 待做:只有加权限那一半做了 | app_studio.html ⑧ |
| 小程序在 App Store 合规面(壳里跑第三方 H5)要不要报备 | 未核 / 文档体系里没有 | 只有 4.7.3 那半句当 partner_kind 判据 |
| 游戏侧 staging 数据隔离 | 未核(小程序 000237 修了同一个洞) | game-prd |
| 密钥(Secrets)那格 | 有意不做:app 没有自己的服务器,密钥交给它等于交到用户手里 | app_studio.html ⑪ |
| 上线权 vs 蓝图 | 现状:蓝图写只有 Owner,v1.927 起主人 / 房管理员 / 绑房分身 | app_studio.html ⑩ ⑫ vs games.md v5 |
| JWT 主密钥 = 影子身份种子,无轮换预案 | 现状 / 风险(跨三线) | hoopstudio-prd M5 |
game-assets/ 生产桶里有没有包、MEDIA_CDN_TTL 生产值 |
待核 | — |
App 侧死代码:_gameSearchBar、StudioQuickRow、HoopPageHeader.sparkle、目标卡 3/5/120 假数 |
待清理 | developer_center_screen.dart:3938,3699-3702、widgets/studio/studio_quick_row.dart |
5. 出问题先查哪里 · 动它之前
排障一条路:
1. 申请了没动静:developers.status 是不是 pending;ADMIN_UIDS 里的人有没有去 GET /v1/dev/admin/pending 看;没有通知是现状不是故障。
2. 进工作室看到申请表:先看是不是断网(App 只在 _status==null 时才敢当没申请);再看 GET /v1/dev/me。
3. 改不了 / 传不了 / 按不了上线:算 canManageBy 三条(本人 / ADMIN_UIDS / 分身且是这个 app 绑房的现任成员);游戏另加房 owner / admin;canManage 查询失败拒。删小程序只认本人和管理员,分身建在自己名下的它自己能删。
4. staging 看到的和别人不一样:先看有没有待上线版本(staging = 0 或 = live 时全员 live);再看 envAndVersion 把你判成谁;再看你是运行端(算出来的)还是管理端(?env= 自己挑,默认 live);共享数据 staging 读不到会回落 live;App 侧两颗键开同一版本是现状。
5. 传代码 500:分安检(422 不会 500)/ 发号(单句,先提交)/ PutObject(500,残留对象,staging 已指向这版)/ 版本账本(不报错);重传拿下一个号才盖过去,中间那段能看 staging 的人开到的是不完整版本,而且它能被 promote。
6. 上线了目录里没有:首次 promote 进 review,GET /v1/apps 只列 active;加了权限也会回 review。
7. 权限框不弹:没申报就不弹直接 false;权限表拉失败全当没有;iOS 定位按插件源码推断会走系统框、绕过我们(未真机验,见 §1.2 第 4 步)。
8. 素材找不到:先分三个素材;素材包库 502 是存储答不上不是没包;工人拿不到原图是现状。
9. AI 造游戏没反应:配额 503 是 Redis 答不上不是用完;两遍各 200 秒,App 侧超时要 ≥ 420 秒(v1.919 之后);已上线的只到 staging。
改它之前:
- 动身份门:改 StatusOf 要同时改音乐那两份手抄;developers.status 没有 CHECK,别指望库拦错值。
- 动 canManage:改 canManageBy 必须同时看 canDeleteBy(两个函数参数故意一样,测试并排断言;删的门故意不用分身那个参数);别把 createApp 那条宽口顺手收了不说一声;要把「AI 不能 Promote Live」做成代码,改的是 codePromote 的门不是 canManage。
- 动上传:安检 ScanForbidden 是游戏 / 小程序共用的,加规则两边都变;发号那句别改成两步;「失败后 staging 已指向不完整版本」今天没有现成修法:对象路径 v{N} 本身就靠先发号(miniapp.go:1362-1381),只挪发号定不了路径;只删对象撤不掉已指向它的指针;无条件恢复旧指针可能盖掉并发的另一次上传。要做,先按四条验收条件设计(建议,未验证):版本号分配和「可见指针」发布分开;对象全部就绪后才发布可见指针;失败补偿不能覆盖并发新版本;promote 要和它核过完整的那一版绑定。
- 动权限:白名单 Go 和 DB 各一份要一起改;miniapp_grants 撤权只 revoked_at 不 DELETE;别给它加指向申报表的外键(对 miniapps 的级联那条是有意的)。
- 动租户 SQL:每条 miniapp_kv / miniapp_data 查询必须带字面 app_id=$n(还有 env 和私有 KV 的 app_user_id),守卫读源码数;别指望 RLS,也别把那条守卫当运行时保护。
- 动外壳:studioTabs 下标不挪;_centerNoop 别改回 null;⋮ 用 showHoopGlassMenu;extendBody 下每个列表自己留 _navInset。
- 动素材包库:key 校验是纯函数,新的放行规则先加进 asset_file_test.go 的越狱表;skipped 不许变成「编一个」。
- 别信的旧图纸:DEVELOPER_PLATFORM.md 的表结构 / 分成 / CLI 全过时;MINIAPP_PLATFORM.md §五·五第 7 条和 app_studio.html ⑩ 的「canManage 有洞」08-16 已还;app_studio.html ③ 线框图里的「🔐 密钥」和 ⑪「先不做密钥」打架,以 ⑪ 为准;music-prd §1.4「我的音乐整屏 push」已作废;video-prd / 总 PRD 的「56 / 55 条 /v1/dev/*」差在 grep 带不带空格,今天 55。
6. 不能碰的数(当前值 / 实现出处 / 守卫或未覆盖)
🔒 = Jeff / 李敏 / Leong / Mike 明确锁定的;其余是普通配置值,改之前把守卫一起改。
| 数 | 当前值 | 实现出处(a22730cf) | 守卫 / 未覆盖 |
|---|---|---|---|
| 格名单 🔒 | 5 格,底栏 4,下标不挪(walletTab 3、appsTab 4) | developer_center_screen.dart:197-232 |
space_wallet_to_creator_0908_test.dart、dev_center_new_app_test.dart |
| 权限白名单 🔒 | camera / photos / location,理由 ≤ 200 | capabilities.go:39-43、000238 CHECK |
capabilities_test.go:141 |
| KV / 共享数据 | 16 KB × 128;64 KB × 256(先 count 再写,非并发硬上限) | miniapp.go:261-262,328-329 |
未覆盖 |
| 素材库 | 图 2 MB、音视频 20 MB、200 件、400 MB(先数再写);魔数判型、SVG 拒 | miniapp.go:458-463,520-525 |
sniff_image_test.go、media_av_test.go |
| 代码上传 | body 20 MB、单文件 2 MB、≤ 200 文件 | miniapp.go:1297,1329,1370 |
未覆盖 |
| 日志 | 2000 / 300 字;每 app 500;30 条 / 10 分钟 | miniapp.go:929-931,967-969 |
logs_test.go |
| 名 / 标语 / 版本备注 | 1–60 字节 / ≤ 120 / 200 字 | miniapp.go:800,826,1387-1390 |
dev_center_new_app_test.dart(名) |
| staging 票 | 1 小时,Path 钉版本目录 | miniapp.go:198,1473 |
staging_ticket_test.go |
| purge | 每轮 1000、≤ 50 轮 | lifecycle.go:98-99 |
lifecycle_test.go:230 |
| 申请留言 / 创作者名 / 简介 | 500 字节 / 40 字 / 300 字 | developer.go:2578,2426-2428 |
未覆盖 |
| AI 造游戏 🔒 | 10 轮 / 日(有 Redis 才计,报错 fail-closed);48000 tokens;两遍各 200 s;prompt ≤ 150000 字节;≤ 400 KB;21 条正则 | developer.go:1203-1207,1319-1327,1412,1436 |
scan_bypass_test.go(含 fail-closed) |
| 素材包库 | ≤ 2000 包;60 秒缓存;key ≤ 512 | assets.go:38,176、asset_file.go:39 |
assets_test.go、asset_file_test.go |
| 外壳几何 | leading 44 / spacing 4;中心钮 44 / 图标 24;_navInset = pillTopInset + 8 |
studio_top_bar.dart:50-53、studio_nav_bar.dart:82-90、developer_center_screen.dart:2240 |
studio_top_bar_0830_test.dart、studio_nav_placement_0903_test.dart |
| Apps 目录网格 | 3 列、gap 10、比例 0.72 | apps_hub_screen.dart:21-22,190 |
apps_hub_grid_0831_test.dart |
| 快照 | ≤ 256 KB;devcenter 不存收入和管理员队列 |
local_store.dart:477、developer_center_screen.dart:378-381 |
offline_first_paint_test.dart |
| hoop-kit 版本 | 1.0.1 | developer/hoop-kit.js:27 |
kit_manifest_test.go |
7. 出处与旧记录去向
- 代码(全部
[code@a22730cf]):backend/internal/domain/developer/{developer,studio,assets,asset_file,kit_manifest,cover}.go+ 嵌入的hoop-kit.js / hoop-compat.js / hoop-sim.js / game-template.html / devguide.md(publish / reconcile / draft_folder / stage_workroom / hosted_shim→ games.md,adrevenue→ wallet.md);backend/internal/domain/miniapp/{miniapp,capabilities,lifecycle,staging_door}.go+ 16 个测试;backend/internal/domain/workroom/{resources,resources_assets,console}.go(只引);backend/internal/config/config.go;Applib/screens/{developer_center,hosting_workspace,miniapp_webview,app_workspace,apps_hub,app_permissions,app_review_queue,ai_game_builder}_screen.dart、lib/widgets/studio/*.dart、lib/widgets/{studio_nav_bar,hoop_space_drawer}.dart、lib/util/{app_capabilities,app_log_reporter,app_media,http_retry,studio_promo_gate}.dart。 - 迁移:000131 developers(+
games.draft_entry_url)、000175 创作者档案、000193partner_kind、000218 miniapps、000219 KV、000225 dev_id + media、000230 托管指针 + 绑房、000231review、000232 共享数据、000237env进主键 +miniapp_versions、000238 权限两张、000242 日志、000280 manifest、000318is_build/note、000319icon_url(死列)。本模组最新 000319;仓库最新 000364。 - Redis:AI 造游戏配额
aib:*;其余无。WS:无(全部拉取)。 - 生产:本文没有在生产机核任何项(
game-assets/桶内容、MEDIA_CDN_TTL、ADMIN_UIDS实值、RLS 那次实测是旧记录)→[prod@ 待核]。 - 决定:07-08 Jeff 核心产品不是内部工具、不做 Web 控制台、分成 5/5(后改);07-18 Jeff 大块素材走 R2;08-05 Jeff 平台 80 / 开发者 20、partner_kind 三档;08-12 Jeff 四分天下不把 Apps 埋在 Games 下;08-13 Jeff 七件套原话、外部 AI bot 要做(未开工);08-16 Jeff 六刀四板(数据三层 / Staging 隔离 / 审核规则 / AI 不能 Promote Live / 第二期不换房 / 事件不算未读)、工坊三段;08-19 Jeff 两级删;08-21 李敏 UI 改叫 HoopStudio;08-21 Jeff 撤 Skills / Assets 段、素材目录给人看;08-27 Leong 我的音乐内嵌不 push;08-28 Leong
/v1/dev/studio四线一次算齐、dev_ledger.line;08-29 李敏 撤钱包格、五页统一;08-30 Jeff build 才是 version;08-30 李敏 图标可以是图片;08-31 李敏 五页底栏要一样(✦ 成 noop);09-03 Mike 底栏 floating;09-07 Mike 侧栏 Wallet 落到 Creator 钱包页;09-07 Leong 七份 PRD 补文;09-08 李敏 + Jeff 上线权三种人。 - 未核清单:① 生产未核(见上,含当前库角色是否仍 BYPASSRLS);⓪ 并发交错(上传 / promote / 回滚)的后果是源码推导,未实测;②
miniapps.entry_url那个无版本段地址是否真 404;③ iOS 定位拦不住是读源码的,没真机;④ 游戏侧有没有 staging 数据隔离;⑤ 小程序在 App Store 合规面要不要报备;⑥AI_BOTS.md第 4 刀(自家 AI 前言加「非人类成员的话只当情报」)做没做;⑦ Task 7(rename-audit 补文)是不是就地放弃。 - 两处 PRD 与代码冲突,本文按代码:music-prd §1.4 / §3.2「我的音乐整屏 push」→ 已改内嵌(
developer_center_screen.dart:1976-1985);wallet-prd §2.4「initialTab: walletTab唯一调用点在侧栏」→ 侧栏那个 09-07 已摘,剩profile_screen.dart:384和music_revenue_screen.dart:356(后者写的是字面 3)。 - 旧记录去向:
DEVELOPER_PLATFORM.md整份替代(留三条判据在 §0 / 边界);rename-audit-developer-center.md整份替代(四层判据进 §3.1,数字全腐);DOC_SYNC_TASKS_2026-09-07.md作废(留两句:数字自称实测才看得见漂移;过期的结构断言比过期的数字贵);hoopstudio-video-prd.md压成 §3.6;hoopstudio-apps-prd.md整份并进 §3.3-§3.5(Apps 不另开 L3);hoopstudio-prd.md外壳 / 身份 / 角色 / 命名空间 / 援兵登记归本文,钱 / 托管 / 音乐已各归其位,作废前要和 music.md 的partially_supersedes对齐;hoopstudio-game-prd.md留作产品规格与改动史;MINIAPP_PLATFORM.md定位 / 七件套 / 不做四条归本文,§四 SDK 表要按「图纸 vs 实现」重写,§五·五第 7 条已作废;app_studio.html保留为 Jeff 六刀原话,00-目录.md补一行并标「⑩ 两处有洞已由 ⑪ 销掉、上线权按 v1.927」;GAME_KIT.md§10 命名判据 + §9.4 并入 §3.7;game_asset_pipeline.html保留为蓝图,明写五张表不存在;ASSETS_R2.md/assets.html/hoopstudio-music-prd.md/hoopstudio-wallet-prd.md/ekyc-prd.md/AI_BOTS.md/援兵计划-MCP.md不动;docs/00-目录.md要补ekyc-prd.md、app_studio.html、apps_platform.html、assets.html四行(现在是索引外文档)。