合并 master 最新改动
同步 origin/master 至 b303605bf
保留项目总控 Runtime 决策并合入后台账号与画布更新
沿用当前 platform-llm LlmRunRequest 接口解决背景决策冲突
This commit is contained in:
+1
-1
@@ -46,7 +46,7 @@ React 组件测试的用户行为、稳定契约、hook / model 分层断言口
|
||||
|
||||
本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。
|
||||
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。Pingora 只作为独立二进制影子网关试点时,边界、路由口径与替换前验收见 [【开发运维】Pingora独立网关试点-2026-06-11.md](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md),Dashboard 默认入口、运营指标和统计口径见 [【后台管理】Dashboard运营看板方案-2026-06-23.md](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)。
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。Pingora 只作为独立二进制影子网关试点时,边界、路由口径与替换前验收见 [【开发运维】Pingora独立网关试点-2026-06-11.md](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md),Dashboard 默认入口、运营指标和统计口径见 [【后台管理】Dashboard运营看板方案-2026-06-23.md](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md),owner/member 多账号、18 个一级 Tab 权限和逐请求鉴权方案见 [【后台管理】多账号与Tab访问权限方案-2026-07-14.md](./technical/【后台管理】多账号与Tab访问权限方案-2026-07-14.md)。
|
||||
|
||||
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
|
||||
|
||||
|
||||
@@ -66,19 +66,27 @@
|
||||
- 验证方式:确定性测试覆盖发现根、优先级、YAML/路径/符号链接/预算、metadata 清洗、正文按需可见和 fingerprint 漂移;正式 `openai_chat / gpt-5.5` 的 `project-skill` suite 必须证明 hash-only fixture 先失败、匹配 Skill 在首个变更前读取、无关 Skill 不读取、唯一目标文件修改、Agent 与宿主验证通过,以及 Provider lifecycle、唯一回复、配置隔离和零泄漏全部闭合。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-07-13 图片画布生成资源支持提交前统一命名
|
||||
## 2026-07-14 后台账号采用 owner 引导账号与一级 Tab 实时授权
|
||||
|
||||
- 背景:后台此前只支持一组环境变量管理员,所有 `/admin/api/*` 共用统一 admin 门禁,无法给运营、审核等人员分配独立账号和页面范围。
|
||||
- 决策:现有 `GENARRATIVE_ADMIN_USERNAME/PASSWORD` 账号固定作为不可编辑 owner;新增 member 独立保存到私有 `admin_account` 表,密码使用 Argon2id 摘要。登录凭据快照与普通账号快照在类型层分离,普通列表、按 ID 查询和写入响应不包含 `password_hash`。Argon2id 在 blocking 任务中运行并由 api-server 有界限流;未知、停用和 owner 错密账号使用 dummy hash 抹平耗时。member 权限粒度固定为后台 18 个一级 Tab,“账号管理”只允许 owner 且不可授予 member。member 每次请求重新读取当前账号并校验启停、`token_version` 和 Tab 权限;权限、密码或启停变化递增版本并立即淘汰旧 JWT。账号不存在返回 `401`,SpacetimeDB 故障保留 `502/503` 而不清理有效 token。前端导航过滤和页面挂载门禁只负责体验,正式授权由 api-server 的 API-to-Tab 矩阵执行,未登记的新后台路由对 member 默认拒绝。后台面向运营展示管理员身份时统一使用 `displayName`;持久审计仍保存稳定 subject,由 api-server 解析显示名称,前端不得暴露账号 ID 或用登录用户名代替。写接口必须在主事务前加载显示名目录,或在主事务后降级解析,不能把已提交写入伪装为失败。
|
||||
- 影响范围:`admin_account`、SpacetimeDB typed procedures / client facade、后台 JWT 与 session DTO、`/admin/api/accounts*`、后台路由权限中间件、admin-web 导航和账号管理页。
|
||||
- 验证方式:SpacetimeDB schema / client / API 定向测试、`npm run check:admin-account-procedures` 隔离 procedure smoke、完整路由矩阵测试、admin-web 权限路由与账号 API 测试、owner/member 浏览器 smoke、`npm run check:spacetime-schema`、编码与 diff 门禁。
|
||||
- 关联文档:`docs/technical/【后台管理】多账号与Tab访问权限方案-2026-07-14.md`。
|
||||
|
||||
## 2026-07-13 图片画布生成资源统一命名
|
||||
|
||||
- 背景:图片画布的普通图片、规范、角色、图标图集、UI 设计、宣发素材、视频和音频默认使用“类型 + 数字”命名,用户只能在生成后单独重命名素材,画布图层、项目资源和素材库名称容易不一致。
|
||||
- 决策:主生成状态统一使用可选 `assetLabel`,所有生成面板提供同一个资源名称输入。名称最多 80 个字符,提交时去除首尾空格;留空时继续使用现有自动编号名称。一次提交解析出的名称必须同时用于 `assetLabel`、`canvasCompletion.title`、本地结果图层标题、项目资源和账号素材库,不允许各链路自行生成不同名称。
|
||||
- 派生产物:图标和角色动作后端契约补齐 `assetLabel`。带背景原图、图片修改 provider 原始输出等中间产物基于主名称追加“(原图)”或“(原始输出)”;图标切片继续按用户填写的图标描述命名,不继承图集名称覆盖独立素材语义。
|
||||
- 决策:主生成状态继续使用可选 `assetLabel`,名称最多 80 个字符并在提交时去除首尾空格;当前生成面板不展示“资源名称”标签和输入框,默认沿用现有自动编号名称,历史状态或内部调用若携带非空名称,仍必须让同一个名称贯穿 `assetLabel`、`canvasCompletion.title`、本地结果图层标题、项目资源和账号素材库,不允许各链路自行生成不同名称。移除名称输入后,角色、图标图集、UI 设计和角色动作等提示词输入恢复统一可见边框。
|
||||
- 派生产物:图标和角色动作后端契约补齐 `assetLabel`。带背景原图、角色动作绿幕预览等具有独立复用价值的中间产物基于主名称追加“(原图)”等后缀;普通图片和图片修改的纯尺寸变换在内存完成后只上传一次,不生成“原始输出”副本。图标切片继续按用户填写的图标描述命名,不继承图集名称覆盖独立素材语义。
|
||||
- 影响范围:图片画布生成状态与面板、提交模型、图标和角色动作请求契约、项目资源 / 素材库持久化和相关编辑器文档。
|
||||
- 验证方式:覆盖自定义名、空白回退、长度限制,以及图片 / 图标 / 视频 / 音频 / 角色动作的请求名称、完成快照标题和素材名称一致性;运行前端定向测试、Rust 契约与 API 定向测试、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
- 验证方式:覆盖生成面板不渲染资源名称输入、提示词边框、空白回退、内部自定义名与长度限制,以及图片 / 图标 / 视频 / 音频 / 角色动作的请求名称、完成快照标题和素材名称一致性;运行前端定向测试、Rust 契约与 API 定向测试、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-07-13 图片画布多产物生成任务必须保存全部可恢复产物
|
||||
|
||||
- 背景:角色形象、图标 spritesheet 和 UI 素材提取会先得到带纯色背景的原图,再执行抠图或拆分;图片修改会先得到模型对齐尺寸的原始输出,角色动作会先得到绿幕预览视频,再抽帧和抠图。此前部分原始产物只登记到 OSS,或者要等后处理成功后才进入项目资源,用户无法在失败后找回已经生成成功的内容。
|
||||
- 决策:凡一次资产生成任务产生多个可恢复产物,后端必须先把上游已返回的中间产物写入 OSS、`asset_object`、项目资源和账号素材库,再执行抠图、尺寸恢复、抽帧或拆分;未指定素材文件夹时进入默认“项目”文件夹。角色形象、图标 spritesheet 和 UI 素材提取同时保留纯色背景原图与透明后处理结果;图片修改保留模型原始输出与尺寸恢复结果;角色动作把绿幕预览视频作为一个可复用素材保存,逐帧源图继续留在同一任务 OSS 路径,不把 32 至 48 帧逐张灌入素材库。普通图片、去背景、音频等没有独立上游中间产物的任务不制造重复副本。
|
||||
- 画布与降级:有项目上下文的图片多产物继续由同一次 `canvasCompletion` 写入权威画布快照,生成器 `generatedLayerId` 锚定主后处理结果。图标和 UI 图集自动拆分是 best-effort;识别或切片持久化失败仍完成整张透明图集,并在 inline、队列轮询和刷新后任务列表中提示非阻断 warning,不得借用失败错误字段。
|
||||
- 决策:凡一次资产生成任务产生多个具有独立复用价值的产物,后端必须把上游已返回的中间产物写入 OSS、`asset_object`、项目资源和账号素材库,再执行抠图、抽帧或拆分;未指定素材文件夹时进入默认“项目”文件夹。角色形象、图标 spritesheet 和 UI 素材提取同时保留纯色背景原图与透明后处理结果。普通图片和图片修改的纯尺寸变换不属于独立产物:provider 回图保留在内存,变换成功只上传变换结果,变换失败只上传 provider 原图,整个流程只写一次 OSS 并只创建一个素材,不能制造重复“原始输出”。`nanobanana2` 使用标量清晰度档位和独立比例,保留 provider 输出尺寸,不按 `WIDTHxHEIGHT` 解析。角色动作把绿幕预览视频作为一个可复用素材保存,逐帧源图继续留在同一任务 OSS 路径,不把 32 至 48 帧逐张灌入素材库。去背景、音频等没有独立上游中间产物的任务不制造重复副本。
|
||||
- 画布、成本与降级:有项目上下文的图片多产物继续由同一次 `canvasCompletion` 写入权威画布快照,生成器 `generatedLayerId` 锚定主后处理结果。图标和 UI 图集自动拆分是 best-effort;识别或切片持久化失败仍完成整张透明图集,并在 inline、队列轮询和刷新后任务列表中提示非阻断 warning,不得借用失败错误字段。provider 原图或角色动作预览视频承载该任务的模型生成成本,抠图、逐帧处理、透明图集和切片等后处理派生产物的 `generation_cost_mud_points = 0`,避免把生图成本误显示成抠图成本;所有中间产物沿用所属任务的真实 `asset_kind`,角色原图仍为 `character`、图标和 UI 图集原图仍为 `icon-spritesheet`、角色动作预览仍为 `character-animation`,不得再写新的“原图类型”。后台素材查询按任务分页,最终产物作为父行并显示任务总成本,每个中间产物作为可展开的独立子行显示阶段生成器和阶段成本。扣费确认边界保持为 provider 成功,OSS、尺寸恢复和画布回填不延长退款保护。
|
||||
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs`、`character_animation_assets.rs`、外部生成任务摘要、图片画布完成快照、账号素材库和前端生成提示。
|
||||
- 验证方式:覆盖中间产物登记先于后处理、默认素材文件夹、图集拆分降级、inline / queue warning 和主结果锚定的定向测试,并运行 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`、前端定向测试、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
@@ -294,6 +302,7 @@
|
||||
- 决策:`陶泥儿精选` 的公开事实改为独立 `editor_showcase_asset` 审核表。生成素材默认不公开;用户在账号级素材库对 `sourceType="generated"` 且有媒体内容的素材提交审核,后端快照素材信息并写入 `pending`。后台审核通过后写入 `approved`,但默认 `display_enabled=false` 且 `showcase_category=null`,运营可按前台具体 Tab 手动设置分类并开启展示;未设置分类的素材展示开启后进入前台“全部”,但不进入角色 / UI / 音乐 / 美宣具体分类。审核通过时按 `generation_cost_mud_points` 返还 50% 泥点;拒绝后写入 `rejected`。公开接口 `GET /api/editor/showcase/resources` 返回已通过、展示开启且媒体非空的快照,按通过时间和 `showcaseId` 倒序分页,并可携带后台配置的固定活动卡。旧 `editor_project_resource.public_showcase_enabled` 和旧 PATCH 接口只保留兼容,不再驱动精选公开。
|
||||
- 影响范围:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`spacetime-client` 绑定与 mapper、`api-server` 编辑器和后台路由、admin-web 精选审核页、素材库右键菜单、`/creation` 精选瀑布流、图片画布文档和后端表目录。
|
||||
- 验证方式:运行 `npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo check --manifest-path server-rs/Cargo.toml -p spacetime-module -p spacetime-client -p api-server`、前端 / 后台 typecheck 与精选相关组件测试,确认默认不公开、提交后 pending、审核通过后展示和返还、展示开关与点赞生效。
|
||||
- 后续修正:精选批准、确定性返还流水和返还完成标记必须由同一个 SpacetimeDB procedure 在单事务内落地,失败时不得先留下 `approved`;已公开精选私有对象通过同 owner 的精确 `assetObjectId` / `objectKey` 派生匿名读取授权,不把 `generated-*` 前缀整体公开。
|
||||
- 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 2026-07-03 外部编辑器 API 生成默认写入画布与素材库
|
||||
@@ -503,7 +512,7 @@
|
||||
## 2026-06-18 图片画布 UI 设计图提取素材保留图集
|
||||
|
||||
- 背景:UI 设计图需要从成图中继续抽取可复用独立素材;原图标素材生成只把拆分后的图标放入画布,spritesheet 原图没有保留,后续追溯和二次切图不方便。
|
||||
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`,点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和纯色背景素材提取提示词,返回结构复用图标 spritesheet 响应。UI 提取把 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分后的 `assetKind="icon"` 素材。2026-07-03 起,UI 提取的纯色背景由 `screenColor` 选择并经 BgFilter 透明化。2026-07-13 起,图标素材生成先把带背景原图和透明 spritesheet 同时写入项目资源、账号素材库并回填画布,未指定文件夹时落默认“项目”文件夹,再 best-effort 按 alpha 连通域拆分独立图标;拆分失败不改变生成成功状态,响应以空 `iconImageSrcs` 和结构化 `sliceWarning` 返回原因,用户可从图集工具栏手动重试。手动拆分不计费,限制单边 `4096`、总像素 `2048×2048`、最多 `64` 个切片,所有切片用 `sourceResourceId` 指向透明图集。本条新决策取代“图标素材生成只保留图集”的旧口径。
|
||||
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`,点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和纯色背景素材提取提示词,返回结构复用图标 spritesheet 响应。UI 提取把 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分后的 `assetKind="icon"` 素材。2026-07-03 起,UI 提取的纯色背景由 `screenColor` 选择并经 BgFilter 透明化。2026-07-13 起,图标素材生成先把带背景原图和透明 spritesheet 同时写入项目资源、账号素材库并回填画布,未指定文件夹时落默认“项目”文件夹,再 best-effort 按 alpha 连通域拆分独立图标;拆分失败不改变生成成功状态,响应以空 `iconImageSrcs` 和结构化 `sliceWarning` 返回原因,用户可从图集工具栏手动重试。手动拆分不计费,限制单边 `4096`、总像素 `2048×2048`、最多 `64` 个切片,所有切片用 `sourceResourceId` 指向透明图集。`icon-spritesheet` 图集继续显示并允许快速编辑,只有拆分后的 `assetKind="icon"` 单图标隐藏并拒绝快速编辑;工具栏、右键菜单、打开流程和提交兜底必须共用同一判定。本条新决策取代“图标素材生成只保留图集”的旧口径。
|
||||
- 影响范围:图片画布浮动工具栏、编辑器图片生成 BFF、`platform-image` 图集连通域拆分、画布图层类型和编辑器文档。
|
||||
- 验证方式:运行图片画布工具栏 / 图集落层 / 生成提交相关前端测试,`cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml`,以及 `cargo test -p api-server editor_ui_design_asset_extraction_prompt_is_fixed --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联文档:`docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`。
|
||||
@@ -4730,9 +4739,44 @@
|
||||
## 2026-07-13 后台充值退款使用钱包占用与统一用户详情
|
||||
|
||||
- 背景:普通 V3 退款已经能从回调、查单和账单收口现金事实,但后台主动退款若先调微信再扣泥点,会在用户余额不足时产生本可避免的欠账;后台各页面也没有统一查询用户余额、绑定状态和充值订单的入口。
|
||||
- 决策:新增 `profile_recharge_refund_hold`,后台执行退款前按累计部分退款公式原子占用本次应追回的永久泥点,再使用客户端稳定 `requestId` 派生 `out_refund_no` 调微信。部分退款额外占用 1 泥点并发舍入缓冲,防止占用创建后到达的外部退款跨越累计 `floor` 边界;全额退款不加缓冲。`SUCCESS` 扣款并结算匹配占用,`CLOSED` 释放,`PROCESSING / ABNORMAL` 保持;结果未知时不擅自释放,至少等待 10 分钟并连续 3 次退款查单收到官方 `RESOURCE_NOT_EXISTS` 才由 worker 释放。普通消费必须排除全部活动占用。
|
||||
- 决策:新增 `profile_recharge_refund_hold`,后台执行退款前按累计部分退款公式原子占用本次应追回的永久泥点,再使用客户端稳定 `requestId` 派生 `out_refund_no` 调微信。重试必须同时匹配原订单、退款号、退款金额、管理员和归一化原因,不能绕过底层完整幂等校验。部分退款额外占用 1 泥点并发舍入缓冲,防止占用创建后到达的外部退款跨越累计 `floor` 边界;全额退款不加缓冲。`SUCCESS` 扣款并结算匹配占用,`CLOSED` 释放,`PROCESSING / ABNORMAL` 保持;结果未知时不擅自释放,至少等待 10 分钟并连续 3 次退款查单收到官方 `RESOURCE_NOT_EXISTS` 才由 worker 释放。普通消费必须排除全部活动占用。
|
||||
- 欠账与冻结:支付侧已经成功退款时不能回滚现金事实;永久泥点不足部分继续只写订单 settlement 的 `unrecovered_points`,限制普通消费,后续永久泥点优先自动偿还。每日免费和会员周期泥点不参与。人工冻结单独使用 `profile_wallet_manual_restriction`,解除人工冻结不解除退款欠账限制。
|
||||
- 人工复核:交易号或订单总额冲突只允许管理员确认退款归属,退款行追加不可变的管理员、原因、时间后重新运行标准结算;该操作不是“直接解冻”,余额不足仍形成欠账。会员退款、未知错误和非法结算计划不显示该入口,也不能复用泥点钱包冻结语义。
|
||||
- 人工复核:交易号或订单总额冲突只允许管理员确认退款归属;管理员 DTO 与确认面板必须并排展示本地订单和微信退款事实的交易号、订单总额与冲突类型。提交请求携带界面所见 `expectedErrorCode`,SpacetimeDB 事务核对当前错误码一致后,退款行才追加不可变的管理员、原因、时间和获批错误码并重新运行标准结算;已处理请求只有管理员、归一化原因和错误码全部一致时才视为幂等重放,状态变化或任一审计内容不一致继续 fail-closed。获批错误码通过正式读取契约展示,一次确认只豁免对应冲突,其他不变量继续 fail-closed。该操作不是“直接解冻”,余额不足仍形成欠账。会员退款、未知错误和非法结算计划不显示该入口,也不能复用泥点钱包冻结语义。
|
||||
- 发布基线:退款功能和退款事实表尚未进入 release,首次上线只以 release 已有 schema 和数据为迁移基线;不为 master 上未发布的退款中间结构保留旧行 normalization 或历史人工复核兼容。
|
||||
- 后台边界:充值订单、预检、执行、应急退款号登记、用户详情和钱包冻结均只挂在管理员鉴权路由。用户详情由 `user_id` 或陶泥号经认证服务解析,返回头像、昵称、脱敏手机号、绑定状态、钱包分桶、占用、欠账和最近订单;后台语义明确的用户字段复用同一个图标按钮和弹窗,管理员主体及 `admin:*` 合成 ID 不打开用户详情。
|
||||
- 部分退款预检:微信支付查单 `trade_state=REFUND` 只表示已发生退款,不代表全额退款。刷新已登记退款后,本地累计成功退款大于 0 且小于订单总额、且不存在非终态退款、活动 hold、欠账或人工冻结时,可以继续退本地剩余额度;没有本地成功退款事实能解释 `REFUND` 时继续失败关闭并要求登记或账单对账。
|
||||
- 影响范围:`module-runtime`、`spacetime-module`、`spacetime-client`、`api-server` 管理员 BFF / refund worker、`shared-contracts` 与 `apps/admin-web`。
|
||||
|
||||
## 2026-07-14 Jenkins Git 源收口到本机 loopback
|
||||
|
||||
- 背景:Jenkins controller 与 Gitea SSH 当前同机运行,live Job 的 `Pipeline script from SCM` 已使用 `127.0.0.1:2222`,但仓库 Jenkinsfile 内部 checkout 仍固定到局域网 IP,导致入口 SCM 与执行阶段来源不一致。
|
||||
- 决策:所有生产 Job 的 SCM URL 和 Jenkinsfile 内部源码准备统一使用 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`,继续使用 `genarrative-local-gitea-ssh`,不保留局域网 IP、HTTP 内网或公网 fallback。该决策覆盖 2026-06-19 的局域网 SSH 地址口径。
|
||||
- 目标机边界:`127.0.0.1` 只允许在 Jenkins controller / Built-In Node 用于 Git。数据库导入导出与 Server-Provision 都必须在带 `linux && genarrative-build` 标签的 Built-In Node 完成 checkout 和 commit 校验,再通过 stash 把必要脚本交给 dev / release 目标 agent;目标 agent 不得自行 checkout Git 或挂载 Git SSH 凭据。
|
||||
- 影响范围:生产构建、Full、数据库导入导出和 Server-Provision Jenkinsfile,生产运维文档、共享踩坑记录与生产运维静态门禁。
|
||||
- 验证方式:`npm run check:production-ops`、`npm run check:encoding`、`bash -n scripts/jenkins-checkout-source.sh`、`git diff --check`;只读核对 live Job `config.xml` 的 SCM URL,并在 Jenkins 凭据环境对 loopback SSH 地址执行 `git ls-remote ... HEAD`。
|
||||
|
||||
## 2026-07-14 后台 Dashboard 修正访问趋势并增加新增用户留存
|
||||
|
||||
- 背景:Dashboard 本月范围包含未来日期,四张图可停在不同横向窗口;“访问人数”又把整个时段 UV 塞到终止日,0 值仍显示短柱。访问模块分布从 `tracking_event LIMIT 50000` 的任意截断样本计算,页面却继续展示精确值并产生置顶告警。
|
||||
- 决策:本周、本月快捷范围和手动日期均不晚于北京时间今天;访问人数趋势按日去重登录用户绘制,图头与时段卡保留跨日去重 UV,0 值不绘柱,四图同步横向滚动。“当前使用人数(五分钟统计一次)”纠正为滚动口径“近 5 分钟活跃用户”。
|
||||
- 聚合边界:不新增持久化表或字段;新增仅 runtime service identity 可调用的 `get_admin_dashboard_stats_and_return` procedure,在 SpacetimeDB 事务快照内聚合现有 profile、tracking 私有事实,经 `spacetime-client` facade 返回紧凑投影;素材与钱包仍走原查询。api-server 不再依赖固定 50,000 行原始明细读取来生成访问模块分布或精确统计;该权威聚合失败时 Dashboard 请求失败,不把未知统计降级成 0。
|
||||
- 扩展性边界:现有表缺少覆盖跨 scope、跨日期统计的现成索引,本阶段为保持精确性仍在 procedure 内遍历相关事实并监控耗时;数据规模继续增长时改为日期前缀索引或持久化日聚合事实,不恢复固定 `LIMIT` 截断。
|
||||
- 留存口径:筛选范围内 `profile_dashboard_state.created_at` 的北京时间注册日构成 cohort;在精确 `D+1` / `D+7` 存在有效登录 user scope 日聚合即留存。观察日必须早于今天;D1、D7 分别返回留存人数、可观察人数和四舍五入后的基点率,按人数加权汇总,零分母前端显示 `-`。
|
||||
- 影响范围:SpacetimeDB Dashboard 聚合 procedure、`spacetime-client` facade、`/admin/api/dashboard` 与 shared contracts、`apps/admin-web` Dashboard 页面和运营文档。
|
||||
- 验证方式:SpacetimeDB 聚合与 api-server 定向 Rust 测试、Dashboard Vitest、`npm run admin-web:typecheck`、`npm run check:spacetime-schema`、`npm run check:spacetime-runtime-access`、`npm run check:encoding`、`git diff --check`,并用桌面 / 移动浏览器核对留存、日期、每日 UV、零值与滚动同步。
|
||||
|
||||
## 2026-07-15 后台 Dashboard 增加新增用户付费率
|
||||
|
||||
- 背景:Dashboard 已能展示筛选时段的新增用户数和 D1 / D7 留存,但缺少同一新增 cohort 的真实付费转化指标。
|
||||
- 决策:新增用户付费率的分母为筛选期内 `profile_dashboard_state.created_at` 归属的新增用户,分子为其中截至查询时至少有一笔 `profile_recharge_order.paid_at` 的去重用户;已退款仍代表曾发生付费转化,未支付订单不计。
|
||||
- 聚合边界:继续扩展仅 runtime service identity 可调用的 `get_admin_dashboard_stats_and_return` procedure,不新增持久化表或字段,前端只展示 BFF 返回的付费人数、新增用户数和基点率。
|
||||
- 验证方式:SpacetimeDB Dashboard 聚合测试、api-server admin 测试、Dashboard Vitest、`npm run admin-web:typecheck`、`npm run check:spacetime-schema`、`npm run check:spacetime-runtime-access`、`npm run check:encoding`、`git diff --check`,并用桌面 / 移动浏览器核对付费率卡片。
|
||||
|
||||
## 2026-07-14 SpacetimeDB 调用池与缓存读连接分离
|
||||
|
||||
- 背景:HTTP 角色原先在每个 `GENARRATIVE_SPACETIME_POOL_SIZE` 槽位首次建连时订阅同一批 read model;release 配置为 8 时会保存 8 份相同行缓存和 subscription handles,放大 api-server 内存。
|
||||
- 决策:`pool_size` 条连接只承接 procedure / reducer 调用并保持无订阅;HTTP 角色额外创建且只创建 1 条共享缓存读连接,全部 `read_after_connect` 读取统一路由到该连接。缓存连接只在应用 facade 中作为只读用途,不宣称 SDK 或 identity 具备连接级只读权限。
|
||||
- 并发与恢复:缓存读连接通过 `Arc` 共享,读取不占用调用池 permit,也不使用单槽租约串行化;只有首次初始化和 broken 后重建使用单飞锁。首次建连与全部订阅共用一次总超时预算;required subscriptions 全部 applied、optional 阶段连接仍未 broken 后才发布新连接,旧连接由在途读取自然释放。
|
||||
- 就绪边界:HTTP `/readyz` 同时验证调用池握手和缓存读连接;required subscription 失败必须不就绪。非 HTTP worker / controller 不创建缓存读连接,继续使用 1 条调用连接和各自的队列窄订阅。
|
||||
- 运维口径:`GENARRATIVE_SPACETIME_POOL_SIZE=8` 表示 8 条调用连接,HTTP 基础拓扑另加 1 条缓存读连接;外部生成和充值过期监听的独立窄订阅不计入该值。读模型行缓存从 8 份降为 1 份,但 SDK 空 table metadata、8 条调用 socket 和 runner 仍存在,不承诺总 RSS 等比例降为八分之一。
|
||||
- 验证方式:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`;发布后在 8 个调用槽暖机后对比 api-server cgroup memory / PSS,并确认 `/readyz` 与代表性 gallery、公开详情、创作入口和用户标签读取正常。
|
||||
|
||||
@@ -246,6 +246,19 @@ RAG 主要供 Agent 检索项目上下文,开发者仍按 `AGENTS.md`、`docs/
|
||||
|
||||
## 常用检查命令
|
||||
|
||||
Rust 格式检查:
|
||||
|
||||
```bash
|
||||
npm run check:rustfmt
|
||||
```
|
||||
|
||||
仓库通过根目录 `rust-toolchain.toml` 固定 Rust `1.96.0` 和 `rustfmt` 组件,
|
||||
并通过 `rustfmt.toml` 固定 Edition 2024 格式口径。需要修复格式时运行:
|
||||
|
||||
```bash
|
||||
cargo fmt --all --manifest-path server-rs/Cargo.toml
|
||||
```
|
||||
|
||||
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`。
|
||||
|
||||
编码检查:
|
||||
|
||||
@@ -55,6 +55,14 @@
|
||||
- 验证:`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`server-rs/crates/spacetime-client/src/editor_project.rs`、`server-rs/crates/api-server/src/admin.rs`。
|
||||
|
||||
## 后台素材查询要在分页前归一用户、游标和派生任务
|
||||
|
||||
- 现象:输入 `SY-*` 陶泥号查不到已有素材;无筛选时只显示 80 个任务且没有“读取更多”;手动重拆图集后,`素材 N` 被单独当成零成本父任务,原图集又显示成另一组。
|
||||
- 原因:`editor_asset.owner_user_id` 保存内部 `user_id`,不保存公开陶泥号;作者陶泥号在 procedure 返回后才映射,不能直接参与素材表过滤。`spacetime-client` 的统一时间文本是 `seconds.microsZ`,若 cursor 只按 RFC3339 解析,第 80 个任务无法生成 `nextCursor`。手动图集拆分使用独立 `editor-atlas-split-*` 任务号,但切片项目资源通过 `source_resource_id` 指回原图集资源,只按当前 `task_id` 分组会割裂同一条素材生产链。
|
||||
- 处理:`api-server` 在调用素材 procedure 前把“用户 ID / 陶泥号”字段或精确 `SY-*` keyword 解析成内部 `user_id`;游标解析同时接受整数微秒、统一 `seconds.microsZ` 和 RFC3339,编码失败必须返回服务错误,不能伪装成末页,客户端传入的非法 cursor 必须返回 `400`,不能静默回到第一页。手动拆分素材保留真实 `task_id`,通过 `editor_asset_group_source_provenance` 按原 resource、asset object 或 Object Key 查找服务端生成账号素材的可信来源任务,再把来源任务写入 `editor_asset.group_task_id`;跨项目复用时同时携带当前和原始 source resource,首次历史回溯命中后补写 provenance,后续不再扫描账号全量素材。没有可信来源的新批次显式以自己的拆分任务作为 `group_task_id`,不得落回可读取用户资源元数据的 legacy 分支。每片保存 `group_task_expected_asset_count`,全部切片落库后再写不可逆的 `editor_asset_group_cohort` 完成事实;read model 依据完成事实判断批次资格,不用当前剩余行数猜测初始是否完整,因此用户后来删除切片不会让批次脱组。部分失败批次没有完成事实,始终保留为独立拆分任务。历史行兼容沿资源链回溯,删除项目时只固化直接引用待删资源且尚未固化的历史切片;固化始终保存真实来源任务,有界展示分组不得反写覆盖来源。read model 只让同根任务的一个完整拆分批次并入原任务,重复批次按真实拆分任务独立分页。带 owner 条件时先走 `by_editor_asset_owner_user_id`,不要为单用户查询扫描全站素材。后台筛选输入使用防抖并取消旧 transport;手动刷新同一查询失败时保留已有结果和游标,筛选已变化时不展示旧查询结果。
|
||||
- 验证:API 测试覆盖陶泥号组合条件、未知陶泥号、可信来源归组、`seconds.microsZ` / 极值游标和真实 / 归组 Task ID;SpacetimeDB 测试覆盖资源删除后稳定归组、部分失败批次不抢占根任务、重复拆分有界无丢失和 owner 索引分支;后台页面测试覆盖逐字输入防抖、请求取消、刷新失败保留结果、筛选请求乱序、旧分页响应失效和“用户 ID / 陶泥号”请求。再运行 `cargo test -p api-server admin_editor_asset --manifest-path server-rs/Cargo.toml`、`cargo test -p spacetime-module admin_editor_asset --manifest-path server-rs/Cargo.toml` 和 `npm run test -- apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx`。
|
||||
- 关联:`apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx`、`server-rs/crates/api-server/src/admin.rs`、`server-rs/crates/spacetime-module/src/editor_project_storage.rs`。
|
||||
|
||||
## 后台素材缩略图不要在首次挂载时全量换签
|
||||
|
||||
- 现象:后台“素材查询”首批缩略图正常,继续向下滚动或读取更多后长期显示占位图;api-server journald 中已到达的 `/admin/api/assets/read-url` 可能全部是 `200`。
|
||||
@@ -392,6 +400,14 @@
|
||||
- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx` 覆盖来源参数继承、模型参数切换、目标尺寸提交和图层回填;`cargo test -p api-server editor_image_edit --manifest-path server-rs/Cargo.toml` 覆盖图标类拒绝、provider 尺寸对齐和回图恢复。
|
||||
- 关联:`src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/ImageCanvasGenerationLayerModel.ts`、`server-rs/crates/api-server/src/editor_project.rs`。
|
||||
|
||||
## 图片画布纯尺寸变换必须先在内存决策再单次上传
|
||||
|
||||
- 现象:`nanobanana2` 已返回并成功解码图片,任务随后报“尺寸无效”;普通生图若把尺寸恢复作为硬失败,provider 已成功回图后仍可能没有素材进入素材库和画布。
|
||||
- 原因:通用尺寸恢复把 `nanobanana2` 的标量清晰度档位 `512 / 1024 / 2K` 当成 `WIDTHxHEIGHT` 解析;普通图片与快速编辑还把 OSS 持久化放在尺寸恢复之后。同步 `generateContent` 返回的是内联 base64,本地兜底 task id 不能用于向 provider 回查原图。
|
||||
- 处理:`nanobanana2` 保留 provider 输出几何尺寸;其它模型仍按显式像素目标尝试恢复。普通生图和快速编辑先把 provider 回图留在内存,尺寸恢复成功后只上传变换结果,恢复失败则降级为只上传 provider 原图;每个主结果只执行一次 OSS 持久化并只创建一个素材。角色、图标图集、UI 提取和角色动作的 provider 原始输出按多产物语义单独保留并承载任务模型成本,后续抠图、逐帧处理和切片阶段成本为 0;中间产物沿用 `character`、`icon-spritesheet`、`character-animation` 等真实类型,不新增“原图类型”。扣费确认仍以 provider 成功为界,不延长到 OSS、后处理或画布回填;后台按任务显示最终产物父行,并把每个中间产物作为独立子行展开,分别展示阶段生成器和阶段成本。
|
||||
- 验证:`cargo test -p api-server editor_project --manifest-path server-rs/Cargo.toml` 覆盖 nanobanana 标量尺寸、变换失败回落 provider 原图、变换先于单次持久化;`cargo test -p api-server character_animation_assets --manifest-path server-rs/Cargo.toml` 覆盖角色动作原始预览的真实类型和成本归因;`npm run test -- apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx` 覆盖中间产物逐行展开和成本文案。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx`。
|
||||
|
||||
## 图片画布快速编辑元数据必须记录原图引用
|
||||
|
||||
- 现象:快速编辑生成的新图可以替换画布,但打开图片信息时“生成输入”里看不到被修改的原图。
|
||||
@@ -954,12 +970,12 @@
|
||||
- 验证:`npm run test -- src/services/miniGameDraftGenerationProgress.test.ts -t "wooden fish"`,并观察木鱼生成页在 5 分钟以上等待时仍停留在合理阶段。
|
||||
- 关联:`src/services/miniGameDraftGenerationProgress.ts`、`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||
|
||||
## 敲木鱼点击生成出现 SpacetimeDB procedure 超时先查版本错配
|
||||
## 本地 SpacetimeDB procedure 超时或缺失先查版本错配
|
||||
|
||||
- 现象:敲木鱼创作时点击“生成”,前端提示 `SpacetimeDB procedure 调用超时`,但服务端日志更早出现 `Failed to BSATN deserialize procedure return value` 或类似反序列化错误。
|
||||
- 原因:本机 `spacetime` CLI / standalone 版本与 `server-rs/Cargo.toml` 锁定的 `spacetimedb` 版本不一致时,procedure 返回值会在宿主侧反序列化失败,api-server 继续等待就表现成调用超时。若旧 standalone 进程还在复用,也会把这个错配继续带进新一轮创作。
|
||||
- 处理:先用 `spacetime --version` 确认 `spacetimedb tool version`,再和 `server-rs/Cargo.toml` 的 `spacetimedb = "..."` 对齐;遇到版本不匹配时先直接执行 `spacetime version install <version> && spacetime version use <version>`,或在目标就是最新版本时执行 `spacetime version upgrade`,升级后重启 `npm run dev:spacetime` 再重试。当前 dev 脚本会在启动和复用本地 SpacetimeDB 前写入并校验 `dev-spacetime-tool-version`,避免继续复用旧宿主。
|
||||
- 验证:`spacetime --version` 输出与 `server-rs/Cargo.toml` 一致,`http://127.0.0.1:3101/v1/ping` 正常,`npm run test -- scripts/dev.test.ts` 通过,敲木鱼创作点击生成不再卡在 procedure timeout。
|
||||
- 现象:敲木鱼创作时点击“生成”提示 `SpacetimeDB procedure 调用超时`,或后台 Dashboard 的指标与柱状图同时消失;服务端日志更早出现 `Failed to BSATN deserialize procedure return value`、`No such procedure`,Dashboard 请求返回 `502`。
|
||||
- 原因:本机 `spacetime` CLI / standalone 版本与 `server-rs/Cargo.toml` 锁定的 `spacetimedb` 版本不一致时,procedure 返回值会在宿主侧反序列化失败,api-server 继续等待就表现成调用超时。若旧 worktree 已删除但其 orphan standalone 仍监听原端口,API 还可能连到旧 wasm:健康检查正常,新 bindings 对应的 procedure 却尚未发布。
|
||||
- 处理:先用 `spacetime --version` 和监听端口对应的 `/proc/<pid>/exe --version` 分别核对 CLI 与真实宿主,再和 `server-rs/Cargo.toml` 的锁定版本对齐;不能把新版本模块硬发布到旧宿主。旧实例仍有需要保留的本地数据时,先用迁移 procedure 导出,在独立端口启动匹配版本、发布当前模块并增量导入,逐表对账后再把本次 API 切到新实例;旧实例在对账前不停止。当前 dev 脚本会对带版本记录的本地实例校验 `dev-spacetime-tool-version`,但显式连接历史端口时仍要核对真实进程和 module schema。
|
||||
- 验证:CLI、standalone 与 Cargo 锁定版本一致,`/v1/ping` 正常,`spacetime describe` 可找到调用中的 procedure;Dashboard 接口返回 `200` 且包含 4 张图,敲木鱼生成不再卡在 procedure timeout。另执行 `npm run test -- scripts/dev.test.ts` 验证本地调度门禁。
|
||||
- 关联:`scripts/dev.mjs`、`scripts/dev.test.ts`、`server-rs/Cargo.toml`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
## 拼图 UI spritesheet 运行态不要二次包圆底或拉伸比例
|
||||
@@ -1904,13 +1920,13 @@
|
||||
- 验证:deploy 工作区应直接出现 `build/<version>/web.tar.gz` 与 `web.tar.gz.sha256`;后续仍由 `scripts/deploy/production-web-deploy.sh` 执行 checksum 校验和解压 smoke。
|
||||
- 关联:`jenkins/Jenkinsfile.production-web-deploy`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
## Jenkins 生产流水线拉 Git 统一走内网 SSH
|
||||
## Jenkins 生产流水线拉 Git 统一走本机 SSH
|
||||
|
||||
- 后续更新:2026-06-19 起常规构建 / 导入导出 / Full Build 流水线的 Jenkinsfile 内部 checkout 统一使用内网 SSH 地址 `ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git` 和凭据 `genarrative-local-gitea-ssh`,不再把 `https://git.genarrative.world/git/GenarrativeAI/Genarrative.git` 作为默认主源或 fallback,也不再配置公网 Git fallback;`Genarrative-Server-Provision` 仍是服务器初始化专用口径,Job 的 `Pipeline script from SCM` 和 Jenkinsfile 内部 checkout 都必须使用本机路径或目标 agent 可访问的内网 Git 源。
|
||||
- 后续更新:2026-07-14 起所有生产 Job 的 `Pipeline script from SCM` 和 Jenkinsfile 内部 checkout 统一使用本机 SSH 地址 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git` 与凭据 `genarrative-local-gitea-ssh`,不再保留局域网 IP、HTTP 内网地址或公网 fallback。
|
||||
- 现象:生产发布、数据库导入导出、服务器配置、构建或 `Genarrative-Full-Build-And-Deploy` 流水线执行 `GitSCM checkout` 时,如果 Jenkins 生成的 fetch 是 `+refs/heads/*:refs/remotes/origin/*`,公网 Git 链路可能在收包阶段以 `git-remote-https died of signal 15`、`curl 56 GnuTLS recv error (-9)`、`early EOF`、`invalid index-pack output` 失败;写死 `127.0.0.1:3000` 也会在当前执行 agent 不是 Gitea 所在机器时失败。
|
||||
- 原因:`127.0.0.1` 只代表当前执行阶段的 agent 自身;公网域名会绕外部链路并受公网代理、TLS、带宽和凭据影响。HTTP 私有仓库入口如果没有配置 Jenkins 凭据,会在 Git 插件日志中显示 `No credentials specified` 并以 `Failed to authenticate user` 失败。即使只使用内网 Git,如果 `GitSCM` 没有显式 refspec 并开启 `CloneOption honorRefspec=true`,Jenkins Git 插件也会拉取所有分支。
|
||||
- 处理:运行于 `linux && genarrative-build` 的 `Genarrative-Full-Build-And-Deploy` 源码解析阶段、`Genarrative-Web-Build` / `Genarrative-Api-Build` / `Genarrative-Stdb-Module-Build` checkout 阶段,以及数据库导入导出流水线的首次 `checkout([$class: 'GitSCM', ...])` 层统一使用 `GIT_REMOTE_URL=ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git` 和 `GIT_REMOTE_CREDENTIAL_ID=genarrative-local-gitea-ssh`,`GIT_REMOTE_FALLBACK_URL` 留空。这些首次 checkout 都必须使用目标分支 refspec、`CloneOption shallow=true depth=1 noTags=true honorRefspec=true`。后续统一走 `scripts/jenkins-checkout-source.sh`,构建类流水线以 `GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT=true` 复用首次 `GitSCM` 带凭据浅克隆,只有指定 commit 不在浅克隆里时才通过同一 SSH 凭据继续 fetch 和加深;`COMMIT_HASH` 为空时继续 `--depth=1 --no-tags`,指定 commit 时也先保持 `depth=1` 校验,浅历史无法证明归属时才按 `GENARRATIVE_JENKINS_CHECKOUT_DEEPEN_STEPS` 逐步加深,最后才展开完整历史。发布流水线不得为了缩短 checkout 时间清空上游构建传入的 `COMMIT_HASH`。
|
||||
- 验证:扫描本地 Jenkins live job `config.xml`,确认 SCM `<url>` 不再指向 `https://git.genarrative.world/GenarrativeAI/Genarrative.git`;扫描所有生产 Jenkinsfile 的首次 `GitSCM checkout`,确认 `GIT_REMOTE_URL` 是 `ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git`、`GIT_REMOTE_CREDENTIAL_ID` 是 `genarrative-local-gitea-ssh`、`GIT_REMOTE_FALLBACK_URL` 为空,`userRemoteConfigs` 带 `+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}`,`CloneOption` 带 `honorRefspec: true`;重放 Jenkins 时 checkout 日志不应再出现 `No credentials specified`;扫描发布流水线确认传给 `scripts/jenkins-checkout-source.sh` 的 `COMMIT_HASH` 未被硬编码为空;运行 `bash -n scripts/jenkins-checkout-source.sh`。
|
||||
- 原因:`127.0.0.1` 只代表当前执行阶段的 agent 自身,因此 Git checkout 必须收口到同机运行 Gitea SSH、带 `linux && genarrative-build` 标签的 Jenkins Built-In Node;公网域名和局域网 IP 会引入额外网络、代理、TLS 与地址漂移。即使使用本机 Git,如果 `GitSCM` 没有显式 refspec 并开启 `CloneOption honorRefspec=true`,Jenkins Git 插件仍会拉取所有分支。
|
||||
- 处理:Full、Web、API、Stdb、Server-Provision 与数据库导入导出的源码准备统一在 Jenkins Built-In Node 使用 `GIT_REMOTE_URL=ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git` 和 `GIT_REMOTE_CREDENTIAL_ID=genarrative-local-gitea-ssh`,`GIT_REMOTE_FALLBACK_URL` 留空。数据库导入导出把经过 commit 校验的必要脚本 stash 给目标 agent,release / dev 目标阶段只 unstash,不再 checkout Git 或挂载 Git SSH 凭据。首次 checkout 保留目标分支 refspec、`CloneOption shallow=true depth=1 noTags=true honorRefspec=true`,随后由 `scripts/jenkins-checkout-source.sh` 复用并在必要时逐步加深。
|
||||
- 验证:扫描本地 Jenkins live Job `config.xml` 和所有生产 Jenkinsfile,确认 Git URL 均为 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`,凭据仍为 `genarrative-local-gitea-ssh` 且 fallback 为空;确认数据库导入导出在 Prepare 阶段 checkout / stash,目标阶段只 unstash;在 Jenkins 凭据环境运行 `git ls-remote ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git HEAD`,并运行 `npm run check:production-ops`、`bash -n scripts/jenkins-checkout-source.sh`。
|
||||
- 关联:`jenkins/Jenkinsfile.production-full-build-and-deploy`、`jenkins/Jenkinsfile.production-web-build`、`jenkins/Jenkinsfile.production-api-build`、`jenkins/Jenkinsfile.production-stdb-module-build`、`jenkins/Jenkinsfile.production-web-deploy`、`jenkins/Jenkinsfile.production-api-deploy`、`jenkins/Jenkinsfile.production-stdb-module-publish`、`jenkins/Jenkinsfile.production-server-provision`、`jenkins/Jenkinsfile.production-database-export`、`jenkins/Jenkinsfile.production-database-import`、`scripts/jenkins-checkout-source.sh`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## Jenkins 可选参数在 set -u 下不能裸读
|
||||
@@ -3258,3 +3274,24 @@
|
||||
- 处理:网络结果未知时保留活动 hold,页面把原 `requestId`、订单、金额、原因和已知 `out_refund_no` 保存到当前后台标签页、当前管理员会话隔离的 `sessionStorage`,有效期 2 小时;刷新后必须与后端 active hold 的金额、原因和退款号对账一致才允许直接复用原请求,服务端明确拒绝时清理上下文。没有原请求上下文、上下文过期或管理员会话已切换时只开放预填 `out_refund_no` 的安全查单登记,不生成新 ID 硬撞活动 hold。worker 在占用创建至少 10 分钟后查同一退款号,查到退款就将验签事实写入统一 observation,只有连续 3 次查单收到官方 `RESOURCE_NOT_EXISTS` 才释放。进程重启清空连续次数并重新观察;超时、签名、配置、解析等错误一律重置次数并继续占用。
|
||||
- 补充:退款查单适配器必须保留微信 `RESOURCE_NOT_EXISTS` 业务码,并兼容同类 `ORDER_NOT_EXIST`,不能把所有非 2xx 都抹平成通用上游错误;签名有效的退款申请/查询响应仍须与本次 `out_refund_no`、订单号、交易号和金额做关联校验。已释放 hold 复用旧 `requestId` 时必须在调用微信前拒绝,并要求重新预检生成新的请求 ID。
|
||||
- 关联:`server-rs/crates/api-server/src/admin_recharge.rs`、`server-rs/crates/api-server/src/profile_recharge_refund_reconciliation.rs`、`profile_recharge_refund_hold`。
|
||||
|
||||
## 时段 UV 不能伪装成单日趋势
|
||||
|
||||
- 现象:访问人数卡显示整段时间有数百人,但趋势图只有终止日一根满柱,其余日期是同样高度的小短柱;横向滚动后还容易误以为后半月突然出现访问。
|
||||
- 原因:把时段跨日去重 UV 通过单值 series 塞进 `anchor_date_key`,再对 0 值强制设置最小可见高度。时段 UV、每日 UV 和每日 UV 之和是三个不同指标,不能互相替代。
|
||||
- 处理:趋势 bucket 按 `day_key + user_id` 每日去重,图头单独使用整个筛选范围跨日去重人数;0 值高度必须为 0。多张同轴图使用同一日期范围并同步横向滚动,快捷范围不生成未来日 bucket。
|
||||
- 验证:构造同一用户跨两日访问与某日零访问的 fixture,断言每日 bucket、时段去重总数和零值柱分别正确;浏览器核对四图首尾日期窗口一致。
|
||||
|
||||
## 运营聚合不能用固定 LIMIT 的原始事实冒充精确结果
|
||||
|
||||
- 现象:Dashboard 出现“达到单次读取上限 50000 行”告警,但模块分布和累计值仍以无“不完整”标识的精确数字展示;数据增长后结果会随任意截断样本漂移。
|
||||
- 原因:api-server 拉取 `SELECT ... LIMIT 50000` 原始事实再聚合,没有完整分页、稳定排序或数据库侧聚合。提高上限只会推迟错误,并增加响应体与内存压力。
|
||||
- 处理:精确运营指标通过受 runtime service identity 限制的 SpacetimeDB procedure 在事务内聚合,只返回紧凑统计投影,再由 `spacetime-client` facade 交给 BFF。权威聚合失败时请求必须失败,不能用默认 0 代替未知值;现有索引无法覆盖跨 scope、跨日期统计时先监控事务扫描耗时,数据增长后补日期前缀索引或持久化日聚合事实,不能退回固定 `LIMIT`。若某查询只能采样,契约和 UI 必须明确标为采样,不能展示成精确值。
|
||||
- 验证:聚合结果不随 HTTP SQL 行上限变化;超过 50,000 条事实时仍无截断告警,并用数据库事实抽样对账每日、时段、累计与分布结果。
|
||||
|
||||
## 后台详情列表的 grid 规则不要命中嵌套身份组件
|
||||
|
||||
- 现象:素材查询或精选素材详情弹窗中的作者陶泥号被逐字符竖排,用户详情按钮也被挤到编号旁边;窄屏下图片与详情列继续互相挤压。
|
||||
- 原因:`.admin-info-list div` 会命中列表内所有后代 `div`,把字段值内部的 `.admin-inline-identity` 和昵称容器也覆盖成双列 grid;陶泥号又允许任意位置换行,最终只剩单字符宽度。素材详情布局若始终固定为 `220px + 信息列`,移动端也没有足够空间。
|
||||
- 处理:信息列表的行布局只使用直接子选择器 `.admin-info-list > div`;作者昵称与陶泥号在身份组件内分行,陶泥号保持单行并在真正不足时省略。`560px` 以下的素材详情改为单列,缩略图居中;素材查询与精选审核共用该规则。
|
||||
- 验证:在桌面、560px、390px 和 320px 浏览器宽度打开素材详情,确认 `.admin-inline-identity` 的 computed `display` 为 `flex`、陶泥号横向显示、详情字段不溢出页面。
|
||||
|
||||
@@ -45,7 +45,7 @@ server-rs + Axum + SpacetimeDB
|
||||
- `spacetime-client`:后端访问 SpacetimeDB 的 typed facade。
|
||||
- `module-*`:纯领域模型、命令、应用规则、领域事件和领域错误。
|
||||
- `platform-*`:OSS、LLM、认证、语音等外部平台能力。
|
||||
- `shared-contracts` / `packages/shared`:前后端 DTO 和公开契约。
|
||||
- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码;`packages/shared` 可承载共享 UI 组件与纯工具,但不承载领域规则、后端副作用或正式状态。
|
||||
- 前端:表现、交互、临时 UI 状态和后端结果渲染。
|
||||
|
||||
明确废弃:旧 `server-node`、Express、PostgreSQL、Go 服务端、`maincloud`、人工 `spacetime --root-dir` 口径,以及前端承接正式业务真相的路线。
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
- 新增 Markdown 文档时,文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;只在任务需要时重命名历史文档,避免无关大 diff。
|
||||
- 涉及中文文本时注意 UTF-8 编码和乱码排查。
|
||||
- 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。
|
||||
- `packages/shared` 用于前后端 DTO、公开契约及跨页面复用的无业务真相 UI 组件和纯工具;不得把领域规则、后端副作用或正式状态放入其中。
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求均视为历史残留,禁止新增、运行或引用;API smoke 统一使用 `npm run dev:api-server` 与 `/healthz`。
|
||||
- 涉及 SpacetimeDB 表结构、发布或迁移时,先看 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 和 `SPACETIMEDB_TABLE_CATALOG.md`。
|
||||
- 涉及生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,8 @@
|
||||
|
||||
- 后台管理默认入口为 `#dashboard`,原服务 / 数据库状态页保留为 `#overview`,导航展示名为“服务总览”。
|
||||
- Dashboard 由 `GET /admin/api/dashboard` 提供统一 BFF 投影,前端只展示后端返回的 `range`、`metrics`、`charts`、`operations` 和 `warnings`。
|
||||
- 本次不修改 SpacetimeDB schema,不新增统计表;读取现有 private 表后在 api-server 聚合。
|
||||
- 不新增持久化统计表或字段;profile / tracking 私有事实由仅 runtime service identity 可调用的 `get_admin_dashboard_stats_and_return` procedure 在同一事务快照内聚合,再经 `spacetime-client` facade 返回 api-server;素材与钱包继续复用原有查询。api-server 不再通过固定 `LIMIT` 拉取原始访问明细后自行拼装精确指标;该权威聚合失败时整个 Dashboard 请求失败,不用 0 伪装未知值。
|
||||
- 当前 profile / tracking 表没有覆盖这些跨 scope、跨日期统计的现成索引,因此 procedure 为保证精确性仍需遍历相关事实;上线后需监控调用耗时,数据规模继续增长时再以日期前缀索引或持久化日聚合事实替换,不能重新引入固定行数截断。
|
||||
|
||||
## 查询参数
|
||||
|
||||
@@ -13,10 +14,10 @@
|
||||
- `granularity` 默认为 `day`。
|
||||
- `anchor` 使用北京时间日历日期,保留给 `day` / `week` / `month` 兼容旧查询口径。`week` 和 `month` 选择包含该日期的自然周 / 自然月。
|
||||
- `period` 使用 `startDate` / `endDate` 作为闭区间自定义时段,最多 366 天。
|
||||
- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”按当天真实日期自动填充对应自然日 / 自然周 / 自然月范围并刷新,手动修改起止日期后按当前日期范围查询。
|
||||
- 前端统一展示起始日期和终止日期两个日期选择器;点击“本日 / 本周 / 本月”分别填充当天、当周周一至当天、当月月初至当天并刷新,不生成北京时间今天之后的未来日期;手动修改起止日期也不能超过北京时间今天。
|
||||
- 前端默认日期和“本日 / 本周 / 本月”快捷入口都按 `Asia/Shanghai` 计算,不使用浏览器本地时区。
|
||||
- 前端每 5 分钟自动刷新一次,同时保留手动刷新按钮。
|
||||
- 图表横轴按后端返回的 bucket label 展示;本月和较长自定义时段使用固定最小日期列宽并允许横向滚动,避免日期刻度互相挤压。
|
||||
- 图表横轴按后端返回的 bucket label 展示;本月和较长自定义时段使用固定最小日期列宽并允许横向滚动,四张趋势图同步滚动位置,保证同屏日期可横向比较。0 值 bucket 不绘制伪柱。
|
||||
|
||||
## 指标口径
|
||||
|
||||
@@ -24,15 +25,21 @@
|
||||
- 消耗泥点数:`profile_wallet_ledger` 中 `source_type = asset_operation_consume` 且 `amount_delta < 0` 的流水绝对值,按 `created_at` 映射到北京时间业务日。
|
||||
- 总注册用户:`profile_dashboard_state` 行数。
|
||||
- 新增用户数:`profile_dashboard_state` 中 `created_at` 落在当前筛选时间窗内的账号数,按北京时间业务日归属,支持本日 / 本周 / 本月快捷日期范围。
|
||||
- 新增用户付费率:分母为当前筛选时间窗内的新增用户,分子为这些用户中截至本次查询时已至少完成一次真实支付的去重人数。真实支付以 `profile_recharge_order.paid_at` 存在且不晚于本次查询时刻为准;只创建订单或未支付订单不计,已退款订单仍表示曾经发生过付费转化,因此保留在分子。返回付费人数、新增用户数和四舍五入后的基点率;分母为 0 时 DTO 返回 0,前端百分比显示 `-`。
|
||||
- 访问次数:`tracking_daily_stat` 中 `scope_kind = site` 的日聚合次数。它表示站点级成功路由 / 站点级事件,不把用户级钱包、任务、生成等业务操作混入访问次数。
|
||||
- 访问人数:当前数据只具备登录用户维度,按 `tracking_daily_stat` 中 `scope_kind = user` 的 `scope_id` 去重;匿名访问人数需要未来补充稳定 visitor id 后才能统计。
|
||||
- 当前使用人数:最近 5 分钟内 `tracking_event` 中有 `user_id` 的登录用户去重;不跟随页面选择的历史日 / 周 / 月。
|
||||
- 访问人数:当前数据只具备登录用户维度,按 `tracking_daily_stat` 中 `scope_kind = user`、`count > 0` 且 `scope_id` 非空、非 `anonymous` 的用户去重;匿名访问人数需要未来补充稳定 visitor id 后才能统计。时段指标按整个筛选范围跨日去重,趋势图按 `day_key + scope_id` 每日去重,不能把时段 UV 塞到终止日,也不能把每日 UV 之和当成时段 UV。
|
||||
- 近 5 分钟活跃用户:以请求时刻为终点,最近 5 分钟内 `tracking_event` 中有 `user_id` 的登录用户去重;不跟随页面选择的历史日 / 周 / 月。这是滚动窗口,不是每 5 分钟才采样一次。
|
||||
- 次日 / 七日留存 cohort:`profile_dashboard_state.created_at` 按北京时间归日,注册日落在当前筛选闭区间内。该投影是现有运营注册口径,不等同认证账号表;未来若切换正式注册事实,必须单独变更契约,不能静默替换。
|
||||
- 留存活跃:用户在注册日恰好 `D+1` / `D+7` 的 `tracking_daily_stat` 中存在上述有效 user scope 行;同一用户同日多个事件只计一次,不按“1 / 7 天内累计回访”计算。筛选范围约束注册 cohort,观察日允许晚于筛选结束日。
|
||||
- 留存成熟条件:目标观察日必须早于当前北京时间业务日;观察日为今天时因当天尚未完整结束而排除。D1、D7 的可观察人数通常不同,分别返回 `eligibleUsers`、`retainedUsers` 与 `rateBasisPoints = round(retainedUsers * 10000 / eligibleUsers)`;分母为 0 时 DTO 返回 0,前端百分比显示 `-`。汇总率按总人数加权,不平均每日百分比。
|
||||
- 运营汇总页签:复用同一时间窗,展示运营指标卡、素材类型分布和访问模块分布。
|
||||
|
||||
## 前后端文件
|
||||
|
||||
- 后端路由:`server-rs/crates/api-server/src/modules/admin.rs`
|
||||
- 后端聚合:`server-rs/crates/api-server/src/admin.rs`
|
||||
- SpacetimeDB 聚合 procedure:`server-rs/crates/spacetime-module/src/admin_dashboard.rs`
|
||||
- 后端访问 facade:`server-rs/crates/spacetime-client/src/admin_dashboard.rs`
|
||||
- 契约:`server-rs/crates/shared-contracts/src/admin.rs`、`apps/admin-web/src/api/adminApiTypes.ts`
|
||||
- 前端页面:`apps/admin-web/src/pages/AdminDashboardPage.tsx`
|
||||
- 后台路由:`apps/admin-web/src/app/adminRoutes.ts`
|
||||
@@ -47,5 +54,8 @@
|
||||
## 验证
|
||||
|
||||
- `cargo test -p api-server --manifest-path server-rs/Cargo.toml admin`
|
||||
- `cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml admin_dashboard`
|
||||
- `npm run check:spacetime-schema`
|
||||
- `npm run check:spacetime-runtime-access`
|
||||
- `npm run admin-web:typecheck`
|
||||
- `npx vitest run apps/admin-web/src/pages/AdminDashboardPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose`
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
# 后台管理多账号与 Tab 访问权限方案
|
||||
|
||||
更新时间:`2026-07-14`
|
||||
|
||||
## 1. 文档定位
|
||||
|
||||
本文定义陶泥儿后台从单一环境变量管理员扩展为“1 个 owner 引导账号 + 多个 member 持久账号”的编码契约,并为每个一级 Tab 建立前后端一致的访问权限。
|
||||
|
||||
本次只增加后台管理员账号与整页访问权限,不引入页面内按钮级、字段级或只读权限。正式实现必须同时完成前端导航过滤和后端 API 鉴权;前端过滤只改善体验,不能作为安全边界。
|
||||
|
||||
## 2. 当前基线与目标
|
||||
|
||||
当前后台由 `GENARRATIVE_ADMIN_USERNAME`、`GENARRATIVE_ADMIN_PASSWORD` 提供唯一管理员账号,`apps/admin-web/src/app/adminRoutes.ts` 定义 18 个一级 Tab,`server-rs/crates/api-server/src/modules/admin.rs` 中的后台路由只校验统一的管理员 JWT。
|
||||
|
||||
改造后的目标如下:
|
||||
|
||||
1. 现有环境变量账号升级为 `owner`,仍由部署环境提供,不迁移、不复制到 SpacetimeDB。
|
||||
2. owner 始终拥有全部 18 个业务 Tab 权限,并独占“账号管理”Tab 和账号管理 API。
|
||||
3. owner 可以创建、修改、启停 member;member 保存在 SpacetimeDB 私有表 `admin_account`。
|
||||
4. member 按一级 Tab 分配权限;获得一个 Tab 权限即获得该页面内全部读写能力,页面内部二级 Tab、弹窗和操作区继承一级权限。
|
||||
5. member JWT 每次请求都重新读取当前账号并校验 `enabled`、`token_version` 和实时权限,权限、密码或启停变更应立即让旧 JWT 失效。
|
||||
|
||||
## 3. 角色与不可变规则
|
||||
|
||||
### 3.1 owner
|
||||
|
||||
- owner 用户名和密码继续读取 `GENARRATIVE_ADMIN_USERNAME`、`GENARRATIVE_ADMIN_PASSWORD`。
|
||||
- owner 是环境变量构造的虚拟账号,不写入 `admin_account`,不允许通过后台改名、改密、禁用或删除。
|
||||
- owner 始终拥有本文列出的全部 18 个可分配权限,不能在前端取消,也不从数据库加载权限。
|
||||
- “账号管理”是 owner-only 能力。它可以作为新增一级路由 `accounts` / `#accounts` 展示,但 `accounts` 不进入 `ADMIN_TAB_PERMISSIONS`,不能写入 member 的 `permissions_json`。
|
||||
- owner 会话返回 `accountRole = "owner"`、`roles = ["admin", "owner"]`;账号管理权限必须根据服务端确认的 `accountRole` 判断,不能只相信前端角色字符串。
|
||||
- owner 配置缺失时,后台整体保持未启用状态;不能依赖数据库中的 member 绕过 owner 引导配置启动后台。
|
||||
|
||||
### 3.2 member
|
||||
|
||||
- member 只来自 `admin_account`,不新增第二套环境变量账号。
|
||||
- member 会话返回 `accountRole = "member"`、`roles = ["admin", "member"]` 和当前实时 `tabPermissions`。
|
||||
- member 永远不能访问账号管理页面或账号管理 API,也不能给自己或他人分配 `accounts`。
|
||||
- member 的一个一级 Tab 权限覆盖该页面的查询、创建、修改、启停、退款等全部现有操作,不拆成 `read` / `write`。
|
||||
- 页面内二级 Tab、筛选视图、抽屉、弹窗和共享详情弹窗继承触发它的一级 Tab 权限,不另设 permission id。
|
||||
|
||||
## 4. 权限标识
|
||||
|
||||
`ADMIN_TAB_PERMISSIONS` 必须是 shared-contracts 与 admin-web 共用的闭合集合,值与现有 `AdminRouteId` 一致。18 个可分配权限如下,顺序同时作为前端寻找“第一可访问项”的稳定顺序:
|
||||
|
||||
| permission id | 一级 Tab | hash |
|
||||
| --- | --- | --- |
|
||||
| `dashboard` | Dashboard | `#dashboard` |
|
||||
| `overview` | 服务总览 | `#overview` |
|
||||
| `tables` | 表查询 | `#tables` |
|
||||
| `debug` | API 调试 | `#debug` |
|
||||
| `tracking` | 埋点数据 | `#tracking` |
|
||||
| `gray-release` | 灰度发布 | `#gray-release` |
|
||||
| `redeem` | 兑换码 | `#redeem` |
|
||||
| `invite` | 邀请码 | `#invite` |
|
||||
| `profile-wallet` | 账号配置 | `#profile-wallet` |
|
||||
| `tasks` | 任务配置 | `#tasks` |
|
||||
| `recharge-products` | 充值商品 | `#recharge-products` |
|
||||
| `recharge-orders` | 充值管理 | `#recharge-orders` |
|
||||
| `editor-generation-pricing` | 模型定价 | `#editor-generation-pricing` |
|
||||
| `editor-showcase` | 精选审核 | `#editor-showcase` |
|
||||
| `editor-assets` | 素材查询 | `#editor-assets` |
|
||||
| `creation-announcement` | 入口公告 | `#creation-announcement` |
|
||||
| `creation-entry` | 入口开关 | `#creation-entry` |
|
||||
| `work-visibility` | 作品可见性 | `#work-visibility` |
|
||||
|
||||
权限数组必须去重并按上表顺序规范化后保存。保存时拒绝未知值和 `accounts`;读取旧数据时遇到未知值应忽略并记录告警,绝不能将未知值解释为全权限。空数组合法,表示 member 可以登录但没有业务页面权限。
|
||||
|
||||
后续新增一级 Tab 时,必须在同一次改动中更新:
|
||||
|
||||
- shared-contracts 的 `ADMIN_TAB_PERMISSIONS`。
|
||||
- admin-web 的路由定义、权限标签和第一可访问项顺序。
|
||||
- 本文 API-to-Tab 权限矩阵。
|
||||
- 后端路由权限测试;没有明确权限映射的新 `/admin/api/*` 路由必须默认拒绝 member,而不是默认放行。
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
新增 SpacetimeDB 私有表 `admin_account`。表不能标记 `public`,浏览器不能订阅或直查;所有读写都由 `api-server -> spacetime-client facade -> 受限 procedure` 完成。
|
||||
|
||||
| 字段 | Rust / SpacetimeDB 类型 | 约束与语义 |
|
||||
| --- | --- | --- |
|
||||
| `account_id` | `String` | 主键;服务端生成不可变 opaque id,建议 `admin-account-<uuid>`,请求体不得指定 |
|
||||
| `username` | `String` | `unique`;登录名,创建后不可修改;按 `trim + ASCII lowercase` 规范化 |
|
||||
| `display_name` | `String` | 展示名,去除首尾空白后 1 至 64 字符 |
|
||||
| `password_hash` | `String` | Argon2id PHC 字符串;只在内部登录查询中返回给 api-server,永不进入 HTTP DTO、日志或前端状态 |
|
||||
| `permissions_json` | `String` | 规范化后的 Tab permission JSON;只允许第 4 节 18 个值,空数组为 `[]` |
|
||||
| `enabled` | `bool` | 是否允许登录和继续使用现有 JWT |
|
||||
| `token_version` | `u64` | 初始为 `1`;权限、密码或启停状态发生有效变化时加 `1` |
|
||||
| `created_by` | `String` | 创建者后台 subject;当前只能是 owner subject |
|
||||
| `updated_by` | `String` | 最近更新者后台 subject;当前只能是 owner subject |
|
||||
| `created_at` | `Timestamp` | 创建时间,使用 `ctx.timestamp` |
|
||||
| `updated_at` | `Timestamp` | 最近更新时间,使用 `ctx.timestamp` |
|
||||
|
||||
账号规则:
|
||||
|
||||
- `username` 建议限制为 3 至 64 个字符,只允许 ASCII 字母、数字、`.`、`_`、`-`;规范化后做唯一性校验。
|
||||
- owner 用户名属于保留名称。创建 member 时必须同时与当前规范化后的 owner 用户名比较并拒绝冲突,不能只依赖 `admin_account.username` 唯一索引。
|
||||
- 密码明文只存在于登录、创建和改密请求生命周期内;限制为 6 至 128 个字符,并复用 `platform-auth` 的 Argon2id 哈希与校验能力。Argon2id 必须在 blocking 任务中执行,api-server 通过有界信号量限制同时 hash / verify 数量,不得占用 Tokio worker 或无界堆积高成本任务。
|
||||
- 不提供物理删除 API。离职或停用通过 `enabled = false` 完成,以保留 `created_by`、`updated_by` 和账号标识。
|
||||
- `display_name` 单独变化只更新 `updated_by`、`updated_at`,不要求递增 `token_version`;权限、密码、`enabled` 任一有效变化必须在同一事务中递增版本。
|
||||
- `u64` 版本到达上限时更新失败关闭,不能回绕。
|
||||
|
||||
## 6. SpacetimeDB 与 facade 边界
|
||||
|
||||
建议新增 `server-rs/crates/spacetime-module/src/admin_account.rs`,并在 `spacetime-client` 增加对应 admin facade。至少提供以下 typed procedures:
|
||||
|
||||
| procedure | 用途 | 是否可返回 `password_hash` |
|
||||
| --- | --- | --- |
|
||||
| `get_admin_account_by_username_and_return` | member 登录查询 | 是,仅返回给 api-server 内部认证路径 |
|
||||
| `get_admin_account_by_id_and_return` | member JWT 逐请求校验 | 否 |
|
||||
| `list_admin_accounts_and_return` | owner 账号列表 | 否 |
|
||||
| `create_admin_account_and_return` | owner 创建 member | 否 |
|
||||
| `update_admin_account_and_return` | owner 更新展示名、密码 hash、权限、启停 | 否 |
|
||||
|
||||
所有 `admin_account` procedures 都必须在事务入口调用现有 `require_editor_generation_runtime_service_identity(...)` 等价的统一 runtime service identity 守卫,只允许 api-server 当前 runtime service identity 调用。不能因为它们位于后台命名空间就接受任意 SpacetimeDB client identity,也不能新增 public table/view 暴露账号或 hash。
|
||||
|
||||
写 procedure 接收由 api-server 从已认证 owner 会话生成的 `actor_subject`,校验非空后写入 `created_by` / `updated_by`。SpacetimeDB 仍以 `ctx.sender()` 校验调用方是 runtime service identity;不能把请求体中的 actor 当成调用身份。
|
||||
|
||||
procedure result 使用 typed snapshot,不使用不透明 `row_json`。账号列表 snapshot 明确排除 `password_hash`;登录专用 snapshot 与普通账号 DTO 分离,避免序列化时误回传 hash。
|
||||
|
||||
## 7. 登录与 JWT 契约
|
||||
|
||||
### 7.1 登录优先级
|
||||
|
||||
`POST /admin/api/login` 按下列固定顺序处理:
|
||||
|
||||
1. 规范化提交的用户名。
|
||||
2. 若用户名等于 owner 用户名,直接校验 `GENARRATIVE_ADMIN_PASSWORD`。
|
||||
3. owner 密码不匹配时返回统一的“管理员用户名或密码错误”,不得继续查询同名 member。
|
||||
4. 用户名不等于 owner 时,按规范化用户名查询 `admin_account`。
|
||||
5. member 不存在、`enabled = false` 或 Argon2id 校验失败时返回同一登录错误,不泄露账号是否存在或被停用。
|
||||
6. member 登录成功后,用当前 `account_id`、`token_version` 签发后台 JWT。
|
||||
|
||||
owner 优先既保持原账号行为,也防止数据库同名记录遮蔽或降级 owner。密码比较不得写日志;member 必须复用 Argon2id 校验,不能存明文或可逆密文。未知 member、已停用 member 和 owner 错误密码路径仍执行同成本 dummy Argon2id 校验,避免从响应耗时枚举启用账号。
|
||||
|
||||
### 7.2 JWT claims 与逐请求校验
|
||||
|
||||
后台 JWT 继续使用后台独立 TTL 与签名配置。claims 至少能区分:
|
||||
|
||||
- `account_type`: `owner | member`。
|
||||
- owner 的稳定 subject,或 member 的 `account_id` subject。
|
||||
- `token_version`:member 使用表中当前值;owner 使用固定虚拟值,不能由数据库覆盖。
|
||||
- `roles`:保留 `admin`,并追加 `owner` 或 `member`。
|
||||
|
||||
权限不作为 JWT 中的授权真相。即使为调试在 claims 中携带 permissions,后端也必须忽略它并读取实时账号。
|
||||
|
||||
`require_admin_auth` 每次请求都要构造“当前账号”:
|
||||
|
||||
1. 验签并校验后台 issuer、过期时间和 `admin` role。
|
||||
2. owner JWT:与当前环境变量构造的 owner subject 匹配,得到始终启用、全权限的虚拟当前账号;owner 不查 `admin_account`。
|
||||
3. member JWT:按 claim 中 `account_id` 调用 `get_admin_account_by_id_and_return`,账号不存在或 `enabled = false` 时拒绝。只有明确的账号不存在才视为凭据失效;SpacetimeDB 超时、断连或 procedure 故障必须失败关闭但保留 `502/503` 依赖错误语义,不能伪装成 `401` 导致前端清除 token。
|
||||
4. member claim 的 `token_version` 必须与表中当前值完全一致,否则返回 `401 Unauthorized` 并要求重新登录。
|
||||
5. 将服务端实时构造的 `AuthenticatedAdmin` 放入 request extensions,后续权限 middleware 只读取该对象,不再相信原始 claims。
|
||||
|
||||
权限、密码、启停更新递增 `token_version` 后,目标 member 的所有旧 JWT 从下一次请求开始失效。owner 修改自己的环境变量密码仍通过部署配置和服务重启完成;owner 不入表,因此不使用 member 的 `token_version` 机制。
|
||||
|
||||
### 7.3 会话响应
|
||||
|
||||
`AdminSessionPayload` 在现有字段基础上增加:
|
||||
|
||||
```text
|
||||
accountRole: "owner" | "member"
|
||||
tabPermissions: string[]
|
||||
```
|
||||
|
||||
owner 返回全部 18 个 permission id;member 返回数据库中的实时规范化数组。`GET /admin/api/me` 同样执行逐请求校验并返回实时权限,供刷新页面后恢复导航。
|
||||
|
||||
后台所有面向运营展示的管理员身份统一使用 `displayName`。审计表继续保存稳定 subject,例如 owner subject 或 `admin-account-<uuid>`;api-server 在返回兑换码、邀请码等操作记录时,按 owner 运行态和 `admin_account` 批量解析显示名称,同时兼容历史用户名记录。已无法解析的历史主体统一展示“已停用管理员”,前端不得直接渲染 `operatorUserId`、账号 ID 或登录用户名代替显示名称。对写接口,显示名目录必须在主事务前加载,或在主事务成功后降级为占位文案;不得因二次读取失败把已提交写入伪装成失败。
|
||||
|
||||
## 8. 权限中间件与错误语义
|
||||
|
||||
在统一 `require_admin_auth` 之后增加可复用的权限守卫,支持:
|
||||
|
||||
- `require_admin_permission(permission)`:owner 自动通过;member 必须包含该 permission。
|
||||
- `require_any_admin_permission([permission...])`:owner 自动通过;member 至少包含一个,用于共享 API。
|
||||
- `require_admin_owner`:只接受服务端确认的 owner。
|
||||
|
||||
返回语义统一如下:
|
||||
|
||||
- `401 Unauthorized`:token 缺失、无效、过期,member 不存在、被停用或 `token_version` 过期。
|
||||
- `403 Forbidden`:会话有效但缺少目标 Tab 权限,或 member 请求 owner-only API。
|
||||
- 前端收到 `401` 清除本地 token 并回到登录页;收到 `403` 不应伪装成掉线,应刷新 `/me` 权限并跳转到第一可访问项或零权限空态。
|
||||
|
||||
## 9. API-to-Tab 权限矩阵
|
||||
|
||||
下表覆盖 `server-rs/crates/api-server/src/modules/admin.rs` 当前全部路由,并追加账号管理 API。`A OR B` 表示 member 拥有任一权限即可;owner 对全部行自动通过。
|
||||
|
||||
| Method | 路径 | 权限 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/admin/api/login` | 公开登录入口,不要求 JWT |
|
||||
| `GET` | `/admin/api/me` | 任意有效后台会话 |
|
||||
| `GET` | `/admin/api/overview` | `overview` |
|
||||
| `GET` | `/admin/api/dashboard` | `dashboard` |
|
||||
| `POST` | `/admin/api/debug/http` | `debug` |
|
||||
| `GET` | `/admin/api/tracking/events` | `tracking` |
|
||||
| `GET` | `/admin/api/tracking/event-keys` | `tracking OR tasks` |
|
||||
| `GET` | `/admin/api/database/tables` | `tables` |
|
||||
| `GET` | `/admin/api/database/tables/{table_name}/rows` | `tables` |
|
||||
| `GET` | `/admin/api/creation-entry/config` | `gray-release OR creation-announcement OR creation-entry` |
|
||||
| `POST` | `/admin/api/creation-entry/config` | `creation-entry` |
|
||||
| `POST` | `/admin/api/creation-entry/config/banners` | `creation-announcement` |
|
||||
| `POST` | `/admin/api/creation-entry/config/interactions` | `creation-entry` |
|
||||
| `GET` | `/admin/api/feature-gates` | `gray-release` |
|
||||
| `PUT` | `/admin/api/feature-gates` | `gray-release` |
|
||||
| `GET` | `/admin/api/editor-generation-pricing` | `editor-generation-pricing` |
|
||||
| `POST` | `/admin/api/editor-generation-pricing` | `editor-generation-pricing` |
|
||||
| `GET` | `/admin/api/editor-assets` | `editor-assets` |
|
||||
| `GET` | `/admin/api/assets/read-url` | `editor-assets OR editor-showcase` |
|
||||
| `GET` | `/admin/api/editor-showcase/assets` | `editor-showcase` |
|
||||
| `POST` | `/admin/api/editor-showcase/assets/review` | `editor-showcase` |
|
||||
| `POST` | `/admin/api/editor-showcase/assets/display` | `editor-showcase` |
|
||||
| `GET` | `/admin/api/editor-showcase/campaign` | `editor-showcase` |
|
||||
| `POST` | `/admin/api/editor-showcase/campaign` | `editor-showcase` |
|
||||
| `POST` | `/admin/api/editor-showcase/campaign/image-upload-ticket` | `editor-showcase` |
|
||||
| `GET` | `/admin/api/works/visibility` | `work-visibility` |
|
||||
| `POST` | `/admin/api/works/visibility` | `work-visibility` |
|
||||
| `GET` | `/admin/api/profile/redeem-codes` | `redeem` |
|
||||
| `POST` | `/admin/api/profile/redeem-codes` | `redeem` |
|
||||
| `POST` | `/admin/api/profile/redeem-codes/disable` | `redeem` |
|
||||
| `GET` | `/admin/api/profile/invite-codes` | `invite` |
|
||||
| `POST` | `/admin/api/profile/invite-codes` | `invite` |
|
||||
| `GET` | `/admin/api/profile/tasks` | `tasks` |
|
||||
| `POST` | `/admin/api/profile/tasks` | `tasks` |
|
||||
| `POST` | `/admin/api/profile/tasks/disable` | `tasks` |
|
||||
| `GET` | `/admin/api/profile/wallet-config` | `profile-wallet` |
|
||||
| `POST` | `/admin/api/profile/wallet-config` | `profile-wallet` |
|
||||
| `GET` | `/admin/api/profile/recharge-products` | `recharge-products` |
|
||||
| `POST` | `/admin/api/profile/recharge-products` | `recharge-products` |
|
||||
| `GET` | `/admin/api/profile/recharge-orders` | `recharge-orders` |
|
||||
| `POST` | `/admin/api/profile/recharge-refunds/preview` | `recharge-orders` |
|
||||
| `POST` | `/admin/api/profile/recharge-refunds/execute` | `recharge-orders` |
|
||||
| `POST` | `/admin/api/profile/recharge-refunds/register` | `recharge-orders` |
|
||||
| `POST` | `/admin/api/profile/recharge-refunds/manual-review/resolve` | `recharge-orders` |
|
||||
| `GET` | `/admin/api/profile/users/detail` | `tables OR tracking OR recharge-orders OR editor-showcase OR editor-assets OR work-visibility` |
|
||||
| `POST` | `/admin/api/profile/wallet-restriction` | `recharge-orders` |
|
||||
| `GET` | `/admin/api/accounts` | owner-only |
|
||||
| `POST` | `/admin/api/accounts` | owner-only |
|
||||
| `PUT` | `/admin/api/accounts/{account_id}` | owner-only |
|
||||
|
||||
三个共享读取接口必须按 OR 规则实现,不能为了复用简单中间件扩大成“任意 member 可访问”:
|
||||
|
||||
- `/admin/api/assets/read-url` 只服务素材查询和精选审核。
|
||||
- `/admin/api/profile/users/detail` 只服务当前实际包含用户详情入口的表查询、埋点数据、充值管理、精选审核、素材查询和作品可见性页面。
|
||||
- `GET /admin/api/creation-entry/config` 同时为灰度发布、入口公告和入口开关提供页面初始化数据;写操作仍按具体页面单独收紧。
|
||||
|
||||
## 10. 账号管理 HTTP 契约
|
||||
|
||||
账号管理 API 放在现有 `/admin/api` 命名空间,统一使用现有 success/error envelope。
|
||||
|
||||
### 10.1 `GET /admin/api/accounts`
|
||||
|
||||
返回:
|
||||
|
||||
```text
|
||||
accounts: Array<{
|
||||
accountId,
|
||||
username,
|
||||
displayName,
|
||||
tabPermissions,
|
||||
enabled,
|
||||
tokenVersion,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}>
|
||||
```
|
||||
|
||||
首项由 api-server 根据环境变量合成只读 owner 记录,后续 member 按用户名和账号 ID 稳定排序。不得返回 `password` 或 `passwordHash`,owner 记录必须带 `accountRole = "owner"` 且前后端都拒绝编辑。
|
||||
|
||||
### 10.2 `POST /admin/api/accounts`
|
||||
|
||||
请求:
|
||||
|
||||
```text
|
||||
{
|
||||
username: string,
|
||||
displayName: string,
|
||||
password: string,
|
||||
tabPermissions: string[],
|
||||
enabled?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`password` 创建时必填;`enabled` 默认 `true`。api-server 完成用户名、密码和权限校验,使用 Argon2id 生成 hash 后调用创建 procedure。用户名冲突返回 `409 Conflict`;参数非法返回 `400 Bad Request`。响应为不含任何密码字段的 `account`。
|
||||
|
||||
### 10.3 `PUT /admin/api/accounts/{account_id}`
|
||||
|
||||
请求:
|
||||
|
||||
```text
|
||||
{
|
||||
displayName: string,
|
||||
password?: string,
|
||||
tabPermissions: string[],
|
||||
enabled: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`username` 和 `account_id` 不可修改。更新请求完整提交显示名称、Tab 权限和启停状态;密码省略表示不修改,空字符串密码作为非法参数拒绝。api-server 只在提供新密码时生成新 hash。procedure 比较有效变化,在权限、密码或启停任一变化时只递增一次 `token_version`,并在同一事务写入账号字段、`updated_by`、`updated_at`。响应仍不返回密码或 hash。
|
||||
|
||||
## 11. admin-web 行为
|
||||
|
||||
### 11.1 路由与导航
|
||||
|
||||
- `adminRoutes` 增加权限元数据;18 个业务路由使用同名 permission id。
|
||||
- `accounts` 路由只在 `admin.accountRole === "owner"` 时加入侧栏和移动底栏,不属于 member 可分配列表。
|
||||
- member 导航只渲染 `admin.tabPermissions` 包含的业务路由。页面组件也必须只在当前路由已授权时挂载,避免隐藏导航后仍发起无权限 API。
|
||||
- owner 渲染全部业务路由和账号管理路由。
|
||||
|
||||
### 11.2 hash 回落
|
||||
|
||||
登录成功、`GET /me` 恢复会话、权限刷新和 `hashchange` 时都执行同一解析:
|
||||
|
||||
1. 当前 hash 对应可访问路由时保持不变。
|
||||
2. hash 未知、属于无权限业务 Tab,或 member 访问 `#accounts` 时,使用 `replaceState` 回落到按第 4 节顺序找到的第一可访问业务 Tab。
|
||||
3. member 权限为空时,不回落 Dashboard;渲染独立的零权限空态,只保留账号信息和退出登录,不挂载任何业务页,也不发起业务 API。
|
||||
4. owner 的默认项仍可保持 Dashboard;账号管理不改变 18 个业务路由的排序。
|
||||
|
||||
后端返回 `403` 时,前端重新请求 `/me` 获取实时权限并执行上述回落。即使前端状态陈旧或被篡改,后端权限 middleware 仍必须拒绝越权请求。
|
||||
|
||||
### 11.3 账号管理页
|
||||
|
||||
- 权限编辑器展示 18 个明确的 checkbox,每项使用现有 Tab 中文名称;不能展示或提交 `accounts`。
|
||||
- 创建和编辑使用独立弹窗或抽屉,不在列表下方追加表单。
|
||||
- 编辑时密码字段默认空,空表示请求中省略 `password`;页面永不展示现有密码或 hash。
|
||||
- 停用使用开关并二次确认。保存成功后以 API 返回 account snapshot 更新列表。
|
||||
- owner 以只读项显示在账号列表首位,页面顶部同时标明当前 owner;owner 行不可进入编辑表单,后端也拒绝以 owner subject 调用更新接口。
|
||||
|
||||
## 12. 实现落点
|
||||
|
||||
建议按以下边界落地,避免在前端或 `api-server` 重新发明持久化规则:
|
||||
|
||||
- `shared-contracts`:`ADMIN_TAB_PERMISSIONS`、扩展后的 `AdminSessionPayload`、账号管理 request/response DTO。
|
||||
- `spacetime-module`:私有表、输入类型、typed procedures、唯一性与版本递增事务。
|
||||
- `spacetime-client`:生成绑定、row mapper、登录查询与账号管理 facade。
|
||||
- `api-server`:owner/member 登录编排、Argon2id、逐请求账号解析、权限 middleware、账号管理 handlers。
|
||||
- `apps/admin-web`:权限感知路由、hash 回落、零权限空态、owner-only 账号管理页。
|
||||
|
||||
不能将 `permissions_json` 的解析与授权只放在前端;不能让 admin-web 直连 SpacetimeDB;不能用进程内 member 列表替代 `admin_account`。
|
||||
|
||||
## 13. 迁移、绑定与发布顺序
|
||||
|
||||
`admin_account` 是新增私有表,没有旧数据回填。原环境变量 owner 不入表,因此迁移不创建 owner 行。
|
||||
|
||||
实现 schema 后必须:
|
||||
|
||||
1. 将 `admin_account` 加入 `server-rs/crates/spacetime-module/src/migration.rs` 的导入导出/迁移表目录,保证备份迁移保留 member。
|
||||
2. 将表和 procedures 加入 `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的机器可读表目录与后台契约。
|
||||
3. 生成并提交 `spacetime-client` bindings,不手改生成文件。
|
||||
4. 先发布 SpacetimeDB module,再发布依赖新 procedures 的 api-server,最后发布 admin-web。
|
||||
|
||||
本地生成与门禁命令:
|
||||
|
||||
```bash
|
||||
npm run spacetime:generate
|
||||
npm run check:admin-account-procedures
|
||||
npm run check:spacetime-runtime-access
|
||||
npm run check:spacetime-schema
|
||||
npm run check:server-rs-ddd
|
||||
```
|
||||
|
||||
本地迁移联调优先使用项目脚本:
|
||||
|
||||
```bash
|
||||
npm run dev:spacetime
|
||||
npm run dev:api-server
|
||||
```
|
||||
|
||||
需要人工发布到指定 SpacetimeDB 时必须显式目标,不使用 `spacetime --root-dir`:
|
||||
|
||||
```bash
|
||||
spacetime publish <database> \
|
||||
--server <server-url> \
|
||||
--module-path server-rs/crates/spacetime-module \
|
||||
--yes=migrate
|
||||
```
|
||||
|
||||
回滚旧 api-server 时保留 `admin_account` 表和数据;旧版本只认识 owner,不会读取 member。不得为了回滚删除表或清空 member 数据。
|
||||
|
||||
## 14. 测试与验收
|
||||
|
||||
### 14.1 后端与数据
|
||||
|
||||
- owner 使用原环境变量账号密码登录成功,且不生成 `admin_account` 行。
|
||||
- owner 用户名匹配但密码错误时,不回退到 member 查询。
|
||||
- member 密码使用 Argon2id 校验;账号不存在、密码错误、停用账号返回相同登录错误。
|
||||
- member 私表不能被普通 SpacetimeDB identity 查询或调用 procedure;只有 runtime service identity 可读写。
|
||||
- 创建重复规范化用户名、owner 保留用户名、未知权限或 `accounts` 权限均失败。
|
||||
- GET/POST/PUT 账号 API 任何响应和日志都不包含明文密码或 `password_hash`。
|
||||
- 权限、密码、启停更新各自会递增 `token_version`;同一次请求修改多项只递增一次;仅改展示名不递增。
|
||||
- member 被停用、改密或改权限后,旧 JWT 下一次请求返回 401;重新登录后获得实时权限。
|
||||
- API-to-Tab 矩阵逐路由覆盖 `modules/admin.rs`,每条路由至少测试 owner 成功、具备权限的 member 成功、缺权限 member 返回 403。
|
||||
- 三个共享读取接口分别覆盖每个允许 permission 的成功用例,以及无关 permission 的 403 用例。
|
||||
- owner-only 账号 API 对任意 member 都返回 403,即使其 `permissions_json` 被污染为包含 `accounts`。
|
||||
|
||||
### 14.2 前端
|
||||
|
||||
- owner 看到 18 个业务 Tab 和账号管理;member 只看到被分配的业务 Tab。
|
||||
- 每个一级 Tab 内的二级 Tab、弹窗和写操作继承一级权限并正常使用,不出现“页面可见但内部 API 403”的错误映射。
|
||||
- 直接输入无权限 hash 自动替换为第一可访问项,不短暂挂载无权限页面。
|
||||
- 当前 Tab 权限被 owner 收回后,下一请求触发重新登录;新会话恢复后落到第一可访问项。
|
||||
- 零权限 member 登录后显示空态,不回落 Dashboard、不发送 Dashboard 或其它业务请求,并可正常退出。
|
||||
- 桌面侧栏和移动底栏应用同一过滤结果;账号创建/编辑弹层在移动端和桌面端都可操作。
|
||||
- 编辑账号时页面不读取、不显示、不回填密码;不改密码时 PUT 请求不包含 `password`。
|
||||
|
||||
### 14.3 建议验证命令
|
||||
|
||||
```bash
|
||||
cargo test -p spacetime-module --manifest-path server-rs/Cargo.toml admin_account
|
||||
cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml admin_account
|
||||
cargo test -p api-server --manifest-path server-rs/Cargo.toml admin
|
||||
npm run check:admin-account-procedures
|
||||
npx vitest run apps/admin-web/src/app/adminRoutes.test.ts \
|
||||
apps/admin-web/src/app/AdminApp.test.tsx \
|
||||
apps/admin-web/src/pages/AdminAccountManagementPage.test.tsx
|
||||
npm run admin-web:typecheck
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
API smoke 使用 `npm run dev:api-server` 启动后端,先检查 `/healthz`,再用 owner 和至少两个不同权限集合的 member 验证登录、`/admin/api/me`、共享 OR 路由、越权 403 和 `token_version` 即时失效。
|
||||
|
||||
## 15. 非目标
|
||||
|
||||
- 不提供 member 自助改密、忘记密码、MFA、SSO 或外部身份源。
|
||||
- 不提供按钮级、字段级、只读/读写分离权限。
|
||||
- 不提供 owner 数据库化、多个 owner 或 member 删除。
|
||||
- 不改变普通用户认证、普通用户 `token_version` 或主站权限体系。
|
||||
- 不把后台账号暴露为公开 SpacetimeDB 表、浏览器 subscription 或普通用户账号。
|
||||
@@ -66,7 +66,7 @@ RAG 默认不安装运行时依赖,也不把 LanceDB、Transformers.js 或本
|
||||
- 领域规则沉到 `module-*`;SpacetimeDB 表、reducer、procedure、事务 adapter 和 row mapper 留在 `spacetime-module`。
|
||||
- 后端访问 SpacetimeDB 统一经 `spacetime-client` facade。
|
||||
- HTTP / SSE / BFF 和外部副作用编排留在 `api-server`;OSS、LLM、认证、语音等外部平台能力留在 `platform-*`。
|
||||
- 前后端 DTO 和公开契约留在 `shared-contracts` / `packages/shared`。
|
||||
- 前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码留在 `shared-contracts` / `packages/shared`;共享 UI 组件与纯工具可留在 `packages/shared`,但领域规则、后端副作用和正式状态不得下沉。
|
||||
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`api-server/src/app.rs`、`shared-contracts` 和 `packages/shared` 为准。
|
||||
|
||||
后端修改后按当前 DDD 文档执行验收。涉及 API smoke 时,使用 `npm run dev:api-server` 重新拉起后端并检查 `/healthz`;不要使用旧 `maincloud` 启动口径。
|
||||
|
||||
@@ -30,11 +30,24 @@
|
||||
|
||||
## 验收
|
||||
|
||||
- `cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`(35 通过,含上述新测试)
|
||||
- `cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`(44 通过,含上述连接池与缓存连接测试)
|
||||
- `cargo test -p api-server --manifest-path server-rs/Cargo.toml readyz`(2 通过)
|
||||
- `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
|
||||
## 2026-07-14 缓存读连接拆分
|
||||
|
||||
HTTP 角色的连接拓扑调整为“`pool_size` 条调用连接 + 1 条缓存读连接”:
|
||||
|
||||
1. 调用连接池只承接 procedure / reducer 回调,不订阅 read model,也不装载订阅行;`GENARRATIVE_SPACETIME_POOL_SIZE=8` 表示 8 条调用连接,不包含额外的缓存读连接。
|
||||
2. 缓存读连接只由 `read_after_connect` 使用,并持有一份 required / optional read-model subscriptions。多个本地读取通过 `Arc` 共享同一 SDK `ClientCache`,初始化和断线重建使用单飞锁,读取本身不经过单槽 semaphore 串行化。
|
||||
3. 缓存读连接断线后,下一次读取只重建一条连接并等待 required subscriptions 全部 applied;首次建连与 required / optional 订阅共用一次总超时预算,optional 阶段发生断线时禁止发布 broken 连接;旧连接由在途读取持有到结束后再断开。
|
||||
4. `/readyz` 对 HTTP 角色同时检查调用池连接和缓存读连接;required subscription 失败时不得报告 ready。worker / controller 关闭 read-model cache 时不创建额外连接。
|
||||
5. 这里的“只读”是 facade 用途边界。SpacetimeDB Rust SDK 2.6 没有连接级 read-only builder;缓存连接仍使用相同 runtime identity,但代码不向 procedure / reducer 调用路径暴露它。
|
||||
|
||||
该拆分把相同 read-model 行缓存从 `pool_size` 份降为 1 份;SDK 每条连接仍会注册空 table metadata,总 WebSocket 数在 HTTP 角色中会从 `pool_size` 增加到 `pool_size + 1`,因此不能把总 RSS 简单承诺为原来的 `1 / pool_size`。
|
||||
|
||||
## 运维提示
|
||||
|
||||
- 此修复解决的是"取消导致的永久泄漏"。StDB 真慢时仍会出现成批 45 秒超时(连接被在途请求合法占用),那是容量/上游问题,应结合 `GENARRATIVE_SPACETIME_POOL_SIZE` 与 StDB 负载排查,不要再怀疑池泄漏。
|
||||
- 健康检查 `/readyz` 在池被在途请求占满时仍可能短暂 503(stage=pool_acquire),恢复后自动转好,无需重启。
|
||||
- HTTP 角色排查连接数时按“调用池 + 1 条缓存读连接”计算;外部生成唤醒和充值过期监听还有各自的窄订阅连接,不属于调用池或缓存读连接。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -57,7 +57,9 @@ Windows 本地如果已在 `%LOCALAPPDATA%\Genarrative\ffmpeg\bin` 安装 FFmpeg
|
||||
|
||||
本地排查外部内容生成 worker 队列时,默认同一 Rust 进程同时监听 HTTP 并消费 `external_generation_job` 队列;更接近生产的验证应分别启动 `api`、`external-generation-worker` 和 `external-generation-controller`。生产默认 `GENARRATIVE_PROCESS_ROLE=api`,外部生成任务由独立 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` 进程消费;生产与容器扩缩容验证保持 `queue`。当前进入持久队列的外部生成动作包括:拼图 `compile_puzzle_draft` / `generate_puzzle_images` / `generate_puzzle_ui_background`,跳一跳 `compile-draft` / `regenerate-tiles`,拼消消 `compile-draft` / `regenerate-atlas`,敲木鱼 `compile-draft` / `regenerate-hit-object`,以及图片画布 `editor_image_generation` / `editor_image_edit` / `editor_background_removal` / `editor_icon_spritesheet_generation` / `editor_ui_design_asset_extraction` / `editor_character_animation_generation` / `editor_video_generation` / `editor_sound_effect_generation` / `editor_background_music_generation`。非外部 provider 生成动作继续 inline,不进入队列。显式把本地进程角色设为 `api` 且没有 worker 时,HTTP 只返回 queued/running,不会兜底执行外部 provider。
|
||||
|
||||
生产拆分角色时,`external-generation-worker` 和 `external-generation-controller` 的专属 env 示例会把 `GENARRATIVE_SPACETIME_POOL_SIZE` 覆盖为 `1`;非 HTTP 角色只保留 `external_generation_job` 队列窄订阅作为响应式唤醒信号,实际抢占和扩缩容判断仍走 SpacetimeDB procedure,且不再订阅 API 读模型连接池。worker / controller 不执行模型定价 seed,启动时先调用受 runtime writer 鉴权的 queue-stats procedure 做只读预检,身份不匹配时 fail-fast;当前正式 systemd unit 通过共同加载 `/etc/genarrative/api-server.env` 继承同一 `GENARRATIVE_SPACETIME_TOKEN`,专属角色 env 示例不重复配置该 token。`GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS` 与 controller poll interval 只作为订阅失效、漏事件和 lease 过期这类时间条件的兜底,不作为正常领取任务的主路径。
|
||||
HTTP 角色的 `GENARRATIVE_SPACETIME_POOL_SIZE` 只表示 procedure / reducer 调用池大小;池连接不订阅 read model。HTTP 角色会额外创建 1 条共享缓存读连接,统一保存公开读模型、近期游玩统计、创作入口配置和用户标签等订阅行,因此配置为 `8` 时基础连接拓扑是 8 条调用连接加 1 条缓存读连接。`/readyz` 同时检查调用池与缓存读连接的 required subscriptions,缓存未准备好时不能放量。
|
||||
|
||||
生产拆分角色时,`external-generation-worker` 和 `external-generation-controller` 的专属 env 示例会把 `GENARRATIVE_SPACETIME_POOL_SIZE` 覆盖为 `1`;非 HTTP 角色不创建 API 缓存读连接,只保留 `external_generation_job` 队列窄订阅作为响应式唤醒信号,实际抢占和扩缩容判断仍走 SpacetimeDB procedure。worker / controller 不执行模型定价 seed,启动时先调用受 runtime writer 鉴权的 queue-stats procedure 做只读预检,身份不匹配时 fail-fast;当前正式 systemd unit 通过共同加载 `/etc/genarrative/api-server.env` 继承同一 `GENARRATIVE_SPACETIME_TOKEN`,专属角色 env 示例不重复配置该 token。`GENARRATIVE_EXTERNAL_GENERATION_WORKER_POLL_INTERVAL_MS` 与 controller poll interval 只作为订阅失效、漏事件和 lease 过期这类时间条件的兜底,不作为正常领取任务的主路径。
|
||||
|
||||
生产 worker 默认 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS=600`,只覆盖 worker 心跳抖动和短暂断连窗口,不再把 lease 当成完整任务时长;默认 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS=900`,角色动画 / 视频类长任务使用 `GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS=1800`。worker 在单次尝试超过执行预算后会停止当前尝试、写入失败 / 重试状态并释放 worker 槽位;如果 SpacetimeDB 当时不可写,当前租约最多再保留到 lease 过期,之后任务重新变为可领取。生产部署和 provision 脚本会给 `/etc/genarrative/api-server.env` 与 `/etc/genarrative/external-generation-worker.env` 补齐这些变量;已有自定义值不覆盖,只会把历史旧默认 `3600` 迁移为 `600`。
|
||||
|
||||
@@ -144,7 +146,7 @@ spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --serv
|
||||
|
||||
本地 `spacetime` CLI / standalone 版本必须和 `server-rs/Cargo.toml` 里锁定的 `spacetimedb` 版本一致;当前统一版本为 `2.6.0`。若版本错配,procedure 返回值可能在宿主侧触发 `Failed to BSATN deserialize procedure return value`,api-server 最终表现为敲木鱼等创作动作的 `SpacetimeDB procedure 调用超时`。排障时先运行 `spacetime --version`,再对照 `server-rs/Cargo.toml` 的 `spacetimedb = "..."`;遇到版本不匹配时不要继续深挖业务超时,直接执行 `spacetime version install <version> && spacetime version use <version>`,或在目标就是最新版本时执行 `spacetime version upgrade`,升级后重启 `npm run dev:spacetime` 再重试。当前 `scripts/dev.mjs` 会在启动和复用本地 SpacetimeDB 前写入并校验 `dev-spacetime-tool-version`,避免把旧 standalone 继续带进新一轮创作。
|
||||
|
||||
本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine `gpt-image-2` 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`;`api-server` 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若 VectorEngine 在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 `request_id` 的 provider 日志字段 `source`、`source_chain`、`source_chain_depth`,再查 `external_api_call_failure.metadata_json.errorSource`;当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。拼图关卡资产按 `level_scene -> ui_spritesheet -> level_background` 顺序生成,日志会带 `slot`、`asset_kind` 和 `elapsed_ms`。
|
||||
本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine `gpt-image-2` 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`;`api-server` 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前,按原比例尽量收敛 `gpt-image-2` 的显式像素尺寸:最大边长不超过 `3840`、宽高均为 `16` 的倍数、长短边比不超过 `3:1`,总像素范围为 `655360` 至 `8294400`。无法满足全部条件时回退为 `1024x1024`;其他模型保留其传入尺寸。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若 VectorEngine 在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 `request_id` 的 provider 日志字段 `source`、`source_chain`、`source_chain_depth`,再查 `external_api_call_failure.metadata_json.errorSource`;当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。拼图关卡资产按 `level_scene -> ui_spritesheet -> level_background` 顺序生成,日志会带 `slot`、`asset_kind` 和 `elapsed_ms`。
|
||||
|
||||
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout`、`connect`、libcurl 35 SSL connect reset、libcurl 56 receive error / `unexpected eof while reading`、recv failure 等临时传输错误,或在 `upstream_status` 阶段收到 408 / 429 / 5xx(例如 Nginx HTML `502 Bad Gateway`)时,`platform-image` 会对同一请求最多发送 5 次;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。日志中 `VectorEngine 图片请求发送失败,准备重试` 或 `VectorEngine 图片上游状态可重试,准备重试` 表示本次失败已进入下一次尝试;最终仍失败时才会写入 `external_api_call_failure` 并返回 504 / 502。排查生产失败时应同时统计 retry 前的尝试日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。
|
||||
|
||||
@@ -202,6 +204,12 @@ SpacetimeDB bindings:
|
||||
npm run spacetime:generate
|
||||
```
|
||||
|
||||
后台账号 procedure 的 identity、唯一索引和版本事务使用隔离 smoke 验证;脚本会在随机本机端口启动临时 SpacetimeDB 2.6、发布当前 module,结束后自动关闭并清理临时数据:
|
||||
|
||||
```bash
|
||||
npm run check:admin-account-procedures
|
||||
```
|
||||
|
||||
## CodeGraph 本地代码索引
|
||||
|
||||
项目已安装 `@colbymchenry/codegraph` 作为开发期依赖,用于在本地生成语义代码索引,辅助 AI / IDE 做符号搜索、调用关系和影响范围分析。索引目录为 `.codegraph/`,其中 `config.json` 可提交,数据库、缓存和日志由 `.codegraph/.gitignore` 保持本机私有。
|
||||
@@ -225,7 +233,7 @@ npm run codegraph:index
|
||||
|
||||
Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`:
|
||||
|
||||
- `PreToolUse` hook 会在 Codex 准备执行 `git commit` 前运行 `node .codex/hooks/pre-submit-compile-check.mjs`,依次执行 `npm run typecheck`、`npm run admin-web:typecheck`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`,发现编译错误会阻止本次提交。
|
||||
- `PreToolUse` hook 会在 Codex 准备执行 `git commit` 前运行 `node .codex/hooks/pre-submit-compile-check.mjs`,依次执行 `npm run check:rustfmt`、`npm run typecheck`、`npm run admin-web:typecheck`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`,发现格式或编译错误会阻止本次提交。
|
||||
- `PostToolUse` hook 会在 Codex 工具修改文件后运行 `node .codex/hooks/post-edit-codegraph-sync.mjs`,执行 `npm run codegraph:sync` 刷新本地语义索引。
|
||||
- 如果某个 Codex 客户端版本尚未自动加载项目级 hook,可先手动运行 `node .codex/hooks/pre-submit-compile-check.mjs` 与 `node .codex/hooks/post-edit-codegraph-sync.mjs`;个人模型、token、MCP server 仍放在个人 `~/.codex/config.toml`,不要提交。
|
||||
|
||||
@@ -235,6 +243,7 @@ Codex 项目级 hook 已放在 `.codex/config.toml` 与 `.codex/hooks/`:
|
||||
|
||||
后端代码修改后,按变更范围选择:
|
||||
|
||||
- `npm run check:rustfmt`
|
||||
- `cargo test -p <crate> --manifest-path server-rs/Cargo.toml`
|
||||
- `cargo test -p platform-image --manifest-path server-rs/Cargo.toml`
|
||||
- `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
|
||||
@@ -257,6 +266,12 @@ npm run spacetime:generate
|
||||
npm run check:spacetime-schema
|
||||
```
|
||||
|
||||
仓库根目录的 `rust-toolchain.toml` 固定 Rust `1.96.0` 并要求 `rustfmt` 组件,
|
||||
`rustfmt.toml` 固定 Edition 2024 的格式化口径。Rust 源码统一使用
|
||||
`cargo fmt --all --manifest-path server-rs/Cargo.toml` 格式化,并用
|
||||
`npm run check:rustfmt` 做只读校验;Codex 提交前门禁、API 生产构建和
|
||||
SpacetimeDB module 生产构建都会执行同一检查,避免不同开发机或构建节点反复产生格式差异。
|
||||
|
||||
## 前端改动验收
|
||||
|
||||
前端修改后,根据范围选择:
|
||||
@@ -460,9 +475,9 @@ Nginx 与 Pingora 在维护 marker 存在时对内网来源绕过整站维护闸
|
||||
|
||||
版本化默认维护页固定为 `public/maintenance.html`,使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉,只允许保存无日期、无具体时段的通用文案;正常 Web 构建由 Vite 复制到发布包根目录的 `web/maintenance.html`,并由 `check-maintenance-page.mjs` 在打包前拒绝“今天 / 今晚”、具体日期或 `HH:MM` 等临时公告。计划内停服的临时公告必须放在 release 外文件中,通过 `/opt/genarrative/current/scripts/deploy/maintenance-on.sh --page-file <公告HTML> <维护原因>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 `maintenance-on.sh` 调用保留已安装公告,`maintenance-off.sh` 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 `genarrative.world` 与 `www.genarrative.world` 的真实 HTTPS 响应校验 `503` 和公告正文。
|
||||
|
||||
生产 Jenkins 的 `Pipeline script from SCM` 由 Jenkins controller 读取 Jenkinsfile。`Genarrative-Server-Provision` 是服务器初始化流水线,Job 配置里的 SCM URL 必须使用 controller 本机可访问的仓库路径或内网 Gitea 地址,不能使用 `https://git.genarrative.world/...`;否则日志一开始的 `Checking out git ... to read jenkins/Jenkinsfile.production-server-provision` 就会先从公网拉 Jenkinsfile。构建类流水线和 `Genarrative-Server-Provision` 的 Jenkinsfile 内部源码准备阶段统一使用 `ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git`,并显式传入 Jenkins SSH 凭据 `genarrative-local-gitea-ssh`;不再配置 `https://git.genarrative.world/...` 公网 fallback,也不再默认使用 `https://git.genarrative.world/git/GenarrativeAI/Genarrative.git`。所有 `GitSCM checkout` 都必须保留单分支 refspec、`shallow=true`、`depth=1`、`noTags=true` 与 `honorRefspec=true`。API / Web / Stdb 发布类流水线不在目标机器 checkout Git,统一执行上游构建归档里的部署脚本,避免产物 commit 与部署脚本 commit 漂移;Server-Provision 也不在目标 dev / release agent checkout Git,而是由 Jenkins 构建节点先准备 provision 脚本与配置并上传给目标 agent。
|
||||
生产 Jenkins 的 `Pipeline script from SCM` 由 Jenkins controller 读取 Jenkinsfile。所有生产 Job 的 SCM URL,以及 Jenkinsfile 内部在 Jenkins Built-In Node 执行的源码准备,统一使用 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`,并显式传入 Jenkins SSH 凭据 `genarrative-local-gitea-ssh`;不再配置局域网 IP、`https://git.genarrative.world/...` 公网 fallback 或 `https://git.genarrative.world/git/GenarrativeAI/Genarrative.git`。所有 `GitSCM checkout` 都必须保留单分支 refspec、`shallow=true`、`depth=1`、`noTags=true` 与 `honorRefspec=true`。API / Web / Stdb 发布类流水线不在目标机器 checkout Git,统一执行上游构建归档里的部署脚本;Server-Provision 和数据库导入导出也由带 `linux && genarrative-build` 标签的 Jenkins Built-In Node 先 checkout 并 stash 所需脚本,再交给目标 dev / release agent,避免目标机把 `127.0.0.1` 误解为远端 Gitea 或让产物 commit 与执行脚本漂移。
|
||||
|
||||
当前 Jenkins / 本机内网 Git 入口固定为 `ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git`,用于 controller、构建节点和本机 Agent 直接拉取仓库,避免绕公网 `git.genarrative.world`。验证时在具备对应 SSH key 和 known_hosts 的环境执行 `git ls-remote ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git HEAD`,应能返回 HEAD。若机器仍保留旧的 `https://git.genarrative.world/git/GenarrativeAI/Genarrative.git` 或 `http://10.2.0.10/GenarrativeAI/Genarrative.git` 内网入口,只作为历史兼容和排障参考,新流水线不再默认使用。
|
||||
当前 Jenkins / 本机 Git 入口固定为 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`。验证时在具备 Jenkins SSH key 和 `[127.0.0.1]:2222` known_hosts 的环境执行 `git ls-remote ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git HEAD`,应能返回 HEAD;同时扫描 live Job `config.xml` 和仓库 Jenkinsfile,确认没有残留局域网 IP 或公网 Git 地址。旧的局域网、公网和 HTTP 内网入口只作为历史兼容与排障参考,新流水线不再默认使用。
|
||||
|
||||
`scripts/jenkins-checkout-source.sh` 是生产 Jenkinsfile 内部二次确认源码的统一入口。构建流水线由 Jenkins `GitSCM checkout` 先用凭据完成浅克隆,再以 `GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT=true` 调用脚本复用当前 checkout;只有显式 `COMMIT_HASH` 不在这次浅克隆里时,脚本才通过传入的 SSH 远端继续 fetch 并逐步加深。构建流水线和服务器初始化流水线传入 `COMMIT_HASH` 时,脚本必须先保持 `depth=1` 浅拉,若上游 commit 已在浅历史内则直接校验并 checkout;只有浅历史无法证明 commit 属于目标分支时,才按 `GENARRATIVE_JENKINS_CHECKOUT_DEEPEN_STEPS`(默认 `50 200 1000 5000`)逐步加深,最后才尝试展开完整历史。`Genarrative-Api-Deploy`、`Genarrative-Web-Deploy` 和 `Genarrative-Stdb-Module-Publish` 仍保留上游构建传入的 `COMMIT_HASH` 作为通知和追溯字段,但不再用它在目标机器重新 checkout 部署脚本。
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
`陶泥儿精选` 是页面底部的全站公开画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/<play>` 工作台使用,但不作为本页精选区内容。
|
||||
|
||||
精选内容只使用用户从账号级素材库主动提交、后台审核通过且展示状态开启的 `editor_showcase_asset` 快照。新生成素材不会默认公开,审核通过后也不会自动展示;运营可在后台按前台具体 Tab 设置分类(角色、UI、音乐、美宣)并开启展示,未设置分类的素材不会隐藏,会进入前台“全部”。旧 `editor_project_resource.public_showcase_enabled` 只保留历史兼容,不再作为 `/creation` 精选事实源。账号级 `editor_asset` 仍是素材库私有事实;只有 `sourceType="generated"`、有媒体内容、提交审核并通过的素材才可进入精选,上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。审核通过时按生成成本返还 50% 泥点,返还流水使用确定性 `editor-showcase-refund:{showcaseId}` 保证幂等。公开 BFF 必须返回作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
|
||||
精选内容只使用用户从账号级素材库主动提交、后台审核通过且展示状态开启的 `editor_showcase_asset` 快照。新生成素材不会默认公开,审核通过后也不会自动展示;运营可在后台按前台具体 Tab 设置分类(角色、UI、音乐、美宣)并开启展示,未设置分类的素材不会隐藏,会进入前台“全部”。旧 `editor_project_resource.public_showcase_enabled` 只保留历史兼容,不再作为 `/creation` 精选事实源。账号级 `editor_asset` 仍是素材库私有事实;只有 `sourceType="generated"`、有媒体内容、提交审核并通过的素材才可进入精选,上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。审核通过时按生成成本返还 50% 泥点,返还流水使用确定性 `editor-showcase-refund:{showcaseId}` 保证幂等;批准状态、钱包返还流水和 `refund_completed_at` 必须在同一个 SpacetimeDB 事务中完成,任一步失败都保持待审核,历史已通过但未返还记录重复批准时按同一流水补齐且不得重复入账。已通过、已开启展示且返还完成的精选快照必须按同 owner 的精确 `assetObjectId` 或 `objectKey` 派生匿名读取授权,不能放开整个 `generated-*` 前缀。公开 BFF 必须返回作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
|
||||
|
||||
瀑布流通过 `GET /api/editor/showcase/resources` 按通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条;响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有瀑布流,而不是固定只展示首屏数量。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选。
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
旧库或旧迁移包没有 `event_banners_json` 时,后端读取层必须把 `eventBanners` 归一到 `module-runtime` 默认公告数组,不能把旧结构化 `eventBanner` 当成前端优先数组下发。默认公告引用的背景图必须指向 `public/` 下真实存在的站内静态资源,当前默认使用 `/creation-type-references/puzzle.webp`,避免创作入口顶部 banner 出现失效图片。
|
||||
|
||||
创作页和草稿页顶栏右上角统一复用公共泥点资产入口,不再把余额区本身作为直接充值按钮。余额区展开后只展示不限时泥点、每日免费泥点及重置口径;会员周期限时泥点仅由后端保留用于存量兼容和结算,当前版本不在前台展示。独立“充值”按钮进入“购买更多泥点”弹窗,“使用详情”进入泥点账单。主站各位置必须保持同一组件、数据口径和交互语义,头像 / 设置等账号入口继续保留各自语义。
|
||||
创作页和草稿页顶栏右上角统一复用公共泥点资产入口,不再把余额区本身作为直接充值按钮。顶部总余额优先展示会随扣费、退回和到账刷新的 `profileDashboard.walletBalance`,充值中心的 `mudPointBalance` 只承接展开面板中的不限时 / 每日免费泥点明细,不得用已缓存的明细总数覆盖已刷新的顶部余额。余额区展开后只展示不限时泥点、每日免费泥点及重置口径;会员周期限时泥点仅由后端保留用于存量兼容和结算,当前版本不在前台展示。独立“充值”按钮进入“购买更多泥点”弹窗,“使用详情”进入泥点账单。主站各位置必须保持同一组件、数据口径和交互语义,头像 / 设置等账号入口继续保留各自语义。
|
||||
|
||||
创作恢复参数只保留 `sessionId`、`profileId`、`draftId`、`workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留;切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。平台入口刷新直达时,路径到玩法恢复目标、四个 query 归一化、生成页标记、大鱼吃小鱼 workId 兜底、作品 / 草稿身份匹配和跳一跳 / 敲木鱼恢复阶段落点统一由 `platformCreationUrlStateModel.ts` 解析,壳层只执行读取作品、恢复草稿和切换阶段等副作用。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ Prompt 输入摘要与 Prompt 约束只作为内部生成契约维护,不在 U
|
||||
- 前端生成请求统一通过 `/api/editor/images/generations`。
|
||||
- 宣发素材请求携带 `kind: publication-material`,前端提交和后端 handler 都固定归一为 `gpt-image-2`;即使旧前端或外部请求传入 `nanobanana2`,后端也按 `gpt-image-2` 生成和计费,但价格仍来自运行时模型定价配置,不写死数值。
|
||||
- `publication-detail-gallery` 不再携带 `candidateCount: 5`;后端仍兼容多候选请求,但当前宣发素材入口不主动批量生成。
|
||||
- 尺寸请求必须使用明确像素值:游戏首图 `720x540`、详情图 `720x1280`、运营海报 `1280x720`。VectorEngine 适配层对明确像素值保持原样透传;只有 `16:9`、`9:16`、`2k` 等比例 / 档位别名才走 provider 预设映射。
|
||||
- 尺寸请求和成品交付规格必须使用明确像素值:游戏首图 `720x540`、详情图 `720x1280`、运营海报 `1280x720`。这些值是画布图层与成品的业务规格;由于三种规格并非都满足 `gpt-image-2` 的上游尺寸约束,VectorEngine 适配层会在发送前等比归一到合法请求尺寸(最大边 `3840`、两边为 `16` 的倍数、长短边比不超过 `3:1`、总像素 `655360..8294400`),不能将上游归一后的尺寸当作宣发成品规格。只有 `16:9`、`9:16`、`2k` 等比例 / 档位别名才走 provider 预设映射。
|
||||
- 参考图在提交 `/api/editor/images/generations` 前由前端压缩成适合生成理解的图片 Data URL,避免原图 Data URL 撑爆 JSON 请求体;后端该路由保留 `12MB` body limit 作为兼容兜底。
|
||||
- 扣费通过现有钱包资产操作封装执行;上游生成失败或未返回图片时按现有补偿逻辑退款。
|
||||
- 生成成功后,成品作为图片画布生成图层加入画布,并保留游戏输入和参考图摘要供图层信息使用。
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
- 一次生成任务产生多个可复用产物时,全部产物都必须由后端登记为项目资源并随同一次完成快照加入画布,不能只保留最终产物或由前端临时追加。角色形象、图标 spritesheet 和 UI 素材提取至少同时回填纯色背景原图与透明后处理结果;UI 素材提取继续一并回填拆分素材。`generatedLayerId` 仍锚定透明后处理主结果,附属产物从主结果右侧开始错开放置。
|
||||
- 多产物任务的可恢复中间产物还必须进入账号素材库,未传 `assetFolderId` 时落默认“项目”文件夹,并在抠图、尺寸恢复、抽帧或拆分前完成登记。图片修改保存模型对齐尺寸的原始输出;角色动作把绿幕预览视频保存为一个素材,逐帧绿幕源图只保留在同一任务 OSS 路径,避免素材库一次新增 32 至 48 张帧图。普通图片、去背景和音频等没有独立上游中间产物的任务不重复复制最终结果。
|
||||
- 图标和 UI 图集自动拆分属于非阻断附加动作;识别或切片持久化失败时整张透明图集仍完成并回填,前端通过 warning toast 提示用户可手动重试。inline 响应、worker 队列终态和刷新后的任务列表必须使用同一 warning 语义,不能把已完成图集标记为失败。
|
||||
- 普通图片、图片修改、规范、角色、图标、UI 设计、宣发素材、视频、音效、背景音乐和角色动作生成面板统一提供“资源名称”输入,状态字段沿用请求契约 `assetLabel`。输入最多 80 个字符,提交时 trim;留空继续使用现有“类型 + 编号”名称。最终解析出的同一个名称必须同时写入画布图层、`editor_project_resource`、`editor_asset` 和 `canvasCompletion.title`。中间原图在主名称后追加“(原图)/(原始输出)”,拆分图标仍使用各自素材描述。
|
||||
- 普通图片、图片修改、规范、角色、图标、UI 设计、宣发素材、视频、音效、背景音乐和角色动作生成面板不展示“资源名称”输入,默认继续使用现有“类型 + 编号”名称;提示词输入保持统一可见边框。状态与请求契约仍兼容可选 `assetLabel`,内部调用或历史状态携带名称时最多 80 个字符并在提交时 trim,最终解析出的同一个名称必须同时写入画布图层、`editor_project_resource`、`editor_asset` 和 `canvasCompletion.title`。中间原图在主名称后追加“(原图)/(原始输出)”,拆分图标仍使用各自素材描述。
|
||||
- 图片、视频和音频生成结果都要写入账号级素材库;视频 / 音频结果由后端持久化到 OSS 并回传 `objectKey` / `assetObjectId`,前端保存素材库时一并记录,后续预览和再次加入画布走统一换签链路。
|
||||
- 刷新项目后,画布需要同时恢复图层、生成器快照和生成输入框跟随关系。
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ server-rs + Axum + SpacetimeDB
|
||||
- `spacetime-client`:后端访问 SpacetimeDB 的 typed facade。
|
||||
- `module-*`:纯领域模型、命令、应用规则、领域事件和领域错误。
|
||||
- `platform-*`:OSS、LLM、认证、语音等外部平台能力。
|
||||
- `shared-contracts` / `packages/shared`:前后端 DTO 和公开契约。
|
||||
- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码;`packages/shared` 可承载共享 UI 组件与纯工具,但不承载领域规则、后端副作用或正式状态。
|
||||
- 前端:表现、交互、临时 UI 状态和后端结果渲染。
|
||||
|
||||
明确废弃:
|
||||
|
||||
Reference in New Issue
Block a user