Compare commits

..

205 Commits

Author SHA1 Message Date
kdletters a5906ac26e 修复图片交付尺寸与提示重触发
Project CI / Repository checks (push) Successful in 47s
Project CI / Frontend tests (push) Successful in 1m39s
Project CI / Native shell tests (push) Successful in 2m15s
Project CI / Backend tests (push) Successful in 2m56s
统一图片模型的业务交付像素并保留 GPT provider 合法请求尺寸
禁止欠交付图片被放大并返回尺寸降级提示
重置相同 warning 的三秒自动关闭计时
补充前后端回归测试与编辑器契约文档
2026-07-27 21:16:01 +08:00
kdletters 25ee537e39 修复画布提示与图片尺寸一致性
Project CI / Repository checks (push) Successful in 1m3s
Project CI / Frontend tests (push) Successful in 2m5s
Project CI / Native shell tests (push) Successful in 2m33s
Project CI / Backend tests (push) Successful in 3m9s
提示弹窗三秒后自动消失并保留手动关闭
统一 nanobanana2 与 gpt-image-2 同规格图片的交付像素
补充尺寸归一和提示生命周期回归测试
同步图片画布生成契约文档
2026-07-27 20:24:07 +08:00
kdletters 38c5a1a11a 兼容压缩旧版备份结果
Project CI / Repository checks (push) Successful in 1m4s
Project CI / Frontend tests (push) Successful in 1m36s
Project CI / Native shell tests (push) Successful in 2m20s
Project CI / Backend tests (push) Successful in 3m6s
兼容旧 result 缺少 symlinks 清单的历史格式
补充缺字段元数据压缩回归测试
2026-07-27 12:05:33 +08:00
kdletters d38e8c913e 压缩并清理逐文件备份元数据
Project CI / Repository checks (push) Successful in 1m11s
Project CI / Native shell tests (push) Successful in 2m41s
Project CI / Frontend tests (push) Successful in 2m51s
Project CI / Backend tests (push) Successful in 3m15s
将 files state 升级为去重 gzip v2 并兼容旧格式迁移
压缩保留最新 full catalog,清理历史 catalog 与冗余结果
补充损坏拒绝、恢复兼容和清理顺序回归测试
更新生产运维与项目记忆文档
2026-07-27 11:56:23 +08:00
k88936 60a4548524 fix: 输入框内滚动会触发画布滚动 (#110)
Project CI / Frontend tests (push) Successful in 2m28s
Project CI / Repository checks (push) Successful in 1m30s
Project CI / Native shell tests (push) Successful in 2m49s
Project CI / Backend tests (push) Successful in 3m40s
fix:  ignore wheel event from text area to prevent canva move
before:
![shotmd-1784861686-compressed.webp](/attachments/916a0d1c-2ab0-4a16-ae35-0ee599ee9df2)
after:
![shotmd-1784861713-compressed.webp](/attachments/f0558ff5-55d6-471f-b048-18b623dfb911)

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/110
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-24 20:47:05 +08:00
lhk229 a2ee879fc8 添加gfilter专用worker (#103)
Project CI / Repository checks (push) Successful in 1m3s
Project CI / Native shell tests (push) Successful in 2m26s
Project CI / Frontend tests (push) Successful in 2m0s
Project CI / Backend tests (push) Successful in 3m1s
Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/103
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-23 18:06:29 +08:00
kdletters 00da0dd1c8 刷新Gitea CI依赖缓存镜像
Project CI / Repository checks (push) Successful in 1m0s
Project CI / Frontend tests (push) Successful in 1m29s
Project CI / Backend tests (push) Successful in 3m0s
Project CI / Native shell tests (push) Successful in 2m32s
预热当前server-rs的SpacetimeDB 2.7.0依赖
为Cargo锁定依赖下载增加整命令级有界重试
更新Runner镜像事实和运维文档
2026-07-23 16:06:56 +08:00
kdletters 24b10f0660 修复LLM原文日志并行测试污染
Project CI / Repository checks (push) Successful in 1m34s
Project CI / Native shell tests (push) Successful in 2m12s
Project CI / Backend tests (push) Failing after 1m54s
Project CI / Frontend tests (push) Successful in 43s
将原文日志目录收敛为LlmConfig实例配置
迁移platform-llm与api-server测试到独立日志目录
补充Rust异步测试环境变量隔离规范
2026-07-23 15:38:45 +08:00
k88936 44748b7846 fix:前后端校验手机号区域码 (#104)
Project CI / Frontend tests (push) Successful in 34s
Project CI / Backend tests (push) Failing after 39s
Project CI / Repository checks (push) Successful in 52s
Project CI / Native shell tests (push) Successful in 2m4s
来自今早群友反映
before:
![shotmd-1784692399.jpg](/attachments/3ab36252-78f4-44e2-bb51-fca84badc79e)
and failed
after:
![shotmd-1784702966.jpg](/attachments/3eea7ec9-8aca-43f6-a675-1316162619d4)
![shotmd-1784702843.jpg](/attachments/29182a41-26cf-4954-be16-de060d844d9c)
![shotmd-1784787134.jpg](/attachments/c04a1981-c6bf-4a26-b5ad-d2af51d74a29)

* https://developers.weixin.qq.com/miniprogram/dev/server/API/user-info/phone-number/api_getphonenumber.html#Res-phone-info-Object-Payload 参考微信这个文档修改了微信返回的struct 手机号 区域码字段不应是Optional

* 登录注册改密码改绑定等 api 把单一 phone字段改成 country_code(可选,缺省为86) + pure_phone_number 分别进行了前后端校验

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/104
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-23 14:52:57 +08:00
kdletters b72e2163d7 恢复后台灰度发布控制面
Project CI / Frontend tests (push) Successful in 29s
Project CI / Repository checks (push) Successful in 39s
Project CI / Native shell tests (push) Successful in 2m10s
Project CI / Backend tests (push) Failing after 2m50s
恢复灰度发布导航、权限、前端契约与页面编译链
解除灰度页面对旧创作入口配置的依赖
补充路由、API、页面回归测试及当前架构文档
2026-07-23 14:23:18 +08:00
kdletters a7d492eea6 升级 SpacetimeDB 至 2.7.0
Project CI / Frontend tests (push) Successful in 33s
Project CI / Repository checks (push) Successful in 48s
Project CI / Native shell tests (push) Successful in 2m21s
Project CI / Backend tests (push) Successful in 3m43s
统一 Rust 依赖、CLI、容器与部署产物到 2.7.0 hotfix3
重新生成 Rust bindings 并接入 2.7 capability traits
补齐本地 dev 与生产 provision 的 hotfix3 commit 门禁
更新 SpacetimeDB skills、运维文档与 MCP 验证口径
2026-07-23 13:47:32 +08:00
kdletters 080dca8c4e 实现精选顺序分列瀑布流
Project CI / Repository checks (push) Successful in 46s
Project CI / Frontend tests (push) Successful in 49s
Project CI / Native shell tests (push) Successful in 2m35s
Project CI / Backend tests (push) Successful in 3m7s
按容器宽度循环分配精选卡片到三列、两列或单列
使用 ResizeObserver 动态重排并保留 Grid 安全回退
补充布局临界值、分页追加和样式回归测试
同步创作主页文档与项目共享决策
2026-07-22 20:30:53 +08:00
kdletters 5d6a426013 统一后台精选审核素材预览
抽取后台素材媒体共享组件并统一懒换签与限流重试
为精选审核补充列表和详情中的素材预览交互
透传精选素材缩略图字段并覆盖历史 OSS 地址回归
同步后台预览数据契约与排障文档
2026-07-22 18:49:09 +08:00
kdletters 2c134d87da 修复精选活动卡图片读取权限
补齐后台活动卡图片上传确认与私有资产登记链路
为启用活动卡当前对象键派生受限匿名读取授权
覆盖禁用换图越目录及历史未登记对象回归场景
同步后端架构创作主页与项目排障文档
2026-07-22 18:25:52 +08:00
kdletters 7c9edca923 合并稳定画布素材删除异步测试 (#109)
Project CI / Repository checks (push) Successful in 55s
Project CI / Frontend tests (push) Successful in 29s
Project CI / Native shell tests (push) Successful in 2m13s
Project CI / Backend tests (push) Successful in 2m48s
2026-07-22 18:21:39 +08:00
kdletters 9023d6df1e 稳定画布素材删除异步测试
Project CI / Backend tests (pull_request) Successful in 3m15s
Project CI / Repository checks (pull_request) Successful in 1m5s
Project CI / Frontend tests (pull_request) Successful in 43s
Project CI / Native shell tests (pull_request) Successful in 3m18s
逐次等待带 objectKey 的素材完成换签并渲染画布图片
记录画布签名图片测试的异步等待约定
2026-07-22 18:06:10 +08:00
k88936 9bb4942ae7 Editor agent 右键菜单支持复制, 引用,下载 (#95)
Project CI / Repository checks (push) Successful in 1m21s
Project CI / Frontend tests (push) Successful in 31s
Project CI / Native shell tests (push) Successful in 2m25s
Project CI / Backend tests (push) Successful in 3m7s
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/95
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-22 18:00:27 +08:00
kdletters aba65d49f8 合并预构建Gitea CI工具链镜像 (#106)
Project CI / Repository checks (push) Successful in 41s
Project CI / Frontend tests (push) Failing after 1m27s
Project CI / Backend tests (push) Successful in 2m45s
Project CI / Native shell tests (push) Successful in 2m58s
预装并固定Gitea CI工具链镜像
预热npm与Cargo依赖下载缓存
在push与PR中执行完整项目检查
2026-07-22 17:52:40 +08:00
kdletters f4f7d8ac20 预热Gitea CI项目依赖缓存
Project CI / Repository checks (pull_request) Successful in 42s
Project CI / Frontend tests (pull_request) Successful in 1m6s
Project CI / Native shell tests (pull_request) Successful in 3m37s
Project CI / Backend tests (pull_request) Successful in 3m27s
将npm与Rust依赖下载缓存预热到固定CI镜像
改为直接从Gitea checkout并增加网络重试
收窄镜像构建上下文并补齐离线与运行时校验
同步Runner切换边界和运维文档
2026-07-22 15:44:40 +08:00
kdletters 2e02c28d0a 预构建Gitea CI工具链镜像
Project CI / Frontend tests (pull_request) Failing after 1m38s
Project CI / Repository checks (pull_request) Failing after 1m38s
Project CI / Native shell tests (pull_request) Failing after 13s
Project CI / Backend tests (pull_request) Failing after 3m23s
固定Node、Rust、Chrome与系统依赖并生成内容寻址镜像
四个CI任务改用genarrative-ci并保留独立npm ci
补充镜像构建装载导出和运行时沙箱校验脚本
同步开发运维文档与项目共享记忆
2026-07-22 14:58:05 +08:00
kdletters 52ade75007 稳定Gitea Runner镜像启动
Runner固定镜像改为缓存优先并保留digest验真规则
开发运维和共享流程同步force_pull配置与升级步骤
踩坑记录补充registry TLS超时的根因与验证方法
2026-07-22 11:52:15 +08:00
kdletters ae9502d9ed 稳定Gitea CI依赖安装
Project CI / Frontend tests (pull_request) Successful in 1m59s
Project CI / Repository checks (pull_request) Successful in 2m55s
Project CI / Native shell tests (pull_request) Successful in 6m38s
Project CI / Backend tests (pull_request) Successful in 7m37s
Project CI / Frontend tests (push) Successful in 1m44s
Project CI / Repository checks (push) Successful in 2m37s
Project CI / Native shell tests (push) Successful in 7m3s
Project CI / Backend tests (push) Successful in 8m5s
保留隔离 runner 的受控代理环境
为 Cargo 与 rustup 下载增加有界重试
记录 Runner systempaths 修补和真实 job 验收口径
2026-07-21 22:23:49 +08:00
k88936 29369e6376 fix: 画布鼠标/触摸板可以上下滚动,无法左右滚动 (#100)
Project CI / Frontend tests (push) Successful in 1m39s
Project CI / Repository checks (push) Successful in 2m38s
Project CI / Native shell tests (push) Successful in 5m25s
Project CI / Backend tests (push) Failing after 5m27s
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/100
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-21 20:50:03 +08:00
kdletters 31a93c6842 补齐Gitea测试门禁
Project CI / Repository checks (push) Successful in 2m9s
Project CI / Frontend tests (push) Successful in 1m22s
Project CI / Backend tests (push) Successful in 7m25s
Project CI / Native shell tests (push) Successful in 5m56s
拆分前端后端与原生壳测试任务
接入server-rs正式workspace全量测试
修正过期Rust断言和Vitest遗漏路径
将AI游戏创作分支纳入CI触发范围
2026-07-21 18:12:43 +08:00
kdletters 7e58fd2195 接入Gitea项目检查门禁
Project CI / Repository checks (push) Successful in 3m56s
Project CI / Backend checks (push) Successful in 3m14s
Project CI / Native shell checks (push) Successful in 4m13s
新增 master 推送、PR 和手工触发的三路项目检查工作流
收紧 PR 基线、只读权限、action 固定版本与依赖锁漂移检查
补充 Gitea 版本、隔离 runner 和 required context 运维说明
2026-07-21 17:15:57 +08:00
kdletters 6bed37542c 调整 VectorEngine 图片模型兜底顺序
统一 gpt-image-2 首发并在符合条件的失败后切换 gpt-image-2-c
保留总尝试预算、worker deadline 与首选失败审计
补齐响应图片校验、脚本兜底和回归测试
同步后端架构、开发运维和项目共享记忆文档
2026-07-21 17:06:39 +08:00
kdletters 7072e52a25 收口美术 Agent 整体超时预算
为消息规划链路增加 18 分钟总 deadline。
修正 LLM 重试响应体错误的 attempt 计数。
统一配置、定价失败中文文案并补齐测试文档。
2026-07-21 16:44:46 +08:00
kdletters 48c9ee2fae 优化美术 Agent 长等待提示与超时处理
将 120 秒硬超时改为请求存活时的耐心等待提示。
收口 provider 安全上限、有限重试和明确失败错误。
补齐等待互斥、计时清理、前后端测试及契约文档。
2026-07-21 16:12:17 +08:00
lhk229 8bdc728fd3 Fix/padding (#96)
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/96
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-21 16:11:22 +08:00
kdletters 8ed0e6ffa8 修复 SpacetimeDB schema guard 基线扫描误报
统一当前工作树与基线提交按 Cargo lib.path 扫描可达模块
补充保留旧源码误报与真实 accessor 重复回归测试
更新旧创作模板退役方案和项目排障记忆
2026-07-21 13:57:10 +08:00
kdletters 2f9ad4b11c 合并图片画布结构化持久化
引入画布图层与生成器结构化存储、revision CAS 保存及迁移回滚流程
补齐历史画布资源修复脚本、SpacetimeDB schema、迁移和生成绑定
前端接入串行保存队列并保留 2MiB 旧布局兼容回退
优化角色动画单进程批量抽帧并同步 OpenAPI、运维与项目文档
同步 active mapper 的 V2 画布保存契约,保证 master 后端可编译
2026-07-20 21:34:05 +08:00
kdletters f748dc72c3 收口图片生成任务超时预算
- 将四类 VectorEngine 图片任务纳入长任务预算并预留终态写回窗口
- 通过请求上下文传递绝对截止时间并约束发送重试与图片下载
- 允许显式下调单次请求超时并补齐预算回归测试
- 同步外部生成 Worker 运维文档与项目记忆
2026-07-20 20:57:34 +08:00
kdletters 1d17698619 优化角色动画批量抽帧
使用单个FFmpeg filter graph批量输出32、40或48帧
保留精确采样时刻和尾帧安全步长
补充整批缺帧诊断与真实短视频回归测试
同步角色动画设计、决策记录和排障说明
2026-07-20 20:57:11 +08:00
kdletters 06c12f40b6 修复画板 Portal 菜单主题继承
新增画板 Portal 主题桥接并同步亮暗主题
让编辑器根节点与 Portal 菜单共享完整品牌 Token
补充静音开关、角色动画与样式契约回归测试
同步图片画布技术方案和 Portal 排障记忆
2026-07-20 20:56:36 +08:00
kdletters b49bf9c984 结构化图片画布持久化并补充历史资源修复
将旧布局上限统一提升至2MiB并接入revision CAS保存
新增画布图层、生成对话框和迁移状态结构化存储
新增migration operator保护的回填、激活和回滚脚本
新增历史图片映射及音频资源恢复的定向dry-run/apply流程
补齐前端保存队列、OpenAPI、Bindings、发布归档与回归测试
2026-07-20 20:45:15 +08:00
kdletters f9e6ae40db 合并 master 最新 OSS 上传优化
合入 origin/master 的动画生成 OSS 上传优化
保留现役编辑器 Agent LLM client 与退役隔离边界
保留角色动画 OSS 客户端复用与并发限制
2026-07-20 20:08:55 +08:00
kdletters ee3948b491 合并旧创作模板退役分支
合并 codex/retire-legacy-creation,退役旧创作模板业务并保留历史数据壳
保留 master 最新编辑器 Agent、画布和个人页能力
补齐现役编辑器 Agent 的 LLM 与生成结果读取链路
同步 Vite、ESLint、Rust workspace、SpacetimeDB 与文档边界
2026-07-20 20:06:26 +08:00
lhk229 a4b3898ff4 动画生成过程中的OSS上传行为优化 (#93)
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/93
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-20 19:50:37 +08:00
kdletters fc8813a8ed 修复画布美术Agent规划与附件弹窗
显式标记待确认工具并在规划完成后结束当前回合
保留普通工具和Hook跳过场景的最大轮次保护并补充回归测试
修复附件选择弹窗Portal主题丢失导致的透明背景
同步画布Agent专题文档与共享排障经验
2026-07-20 19:29:34 +08:00
kdletters 8965a53fb8 修复维护退出阶段隐式拉取源码
将 Exit Maintenance 改为 agent none 并显式分配部署节点,只执行 current release 维护脚本。
增加生产运维静态门禁,禁止维护退出节点 checkout Git 或挂载 Git 凭据。
同步生产运维说明与 Jenkins 重启恢复场景的排障记录。
2026-07-20 18:55:35 +08:00
zhaojingyan eb608ac6f2 完善图片画布撤销保护与操作提示 (#89)
为画布历史记录补充操作类型和内容消失保护

增加撤销成功、恢复成功及阻止提示并在三秒后隐藏

补充拖动、上传、生成和替换等操作的历史语义

保留恢复按钮、Ctrl+Shift+Z 快捷键和双向历史栈

补充撤销与恢复规则的定向测试和说明文档

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/89
Co-authored-by: oj-afraid-student <1373241747@qq.com>
Co-committed-by: oj-afraid-student <1373241747@qq.com>
2026-07-20 15:19:48 +08:00
Yunine a5669602c0 调整画布拖拽交互仅保留鼠标中键 (#88)
移除鼠标右键拖拽画布的交互处理

保留鼠标中键拖拽画布功能

同步更新相关交互测试

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/88
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: Yunine <rxy24@mails.tsinghua.edu.cn>
Co-committed-by: Yunine <rxy24@mails.tsinghua.edu.cn>
2026-07-20 14:49:00 +08:00
kdletters f1f2332e61 修复泥点余额明细刷新
钱包明细每次展开并在实时余额变化时读取最新拆分。

图片画板生成扣费或退款后同步刷新总额与充值中心。

充值中心读取增加 latest-wins 门禁并阻止旧响应覆盖权威结果。

补充钱包刷新、画板入口和充值乱序回归测试。

同步更新泥点统一资产入口决策记录。
2026-07-20 12:48:28 +08:00
kdletters b7c36074b9 热修图片画布布局上限至2MiB
将 API 与 SpacetimeDB 的画布布局校验上限统一提高到 2 MiB
为站内与外部画布保存接口增加 4 MiB 请求体上限
补充 2 MiB 边界通过和超限拒绝测试
2026-07-19 17:58:00 +08:00
kdletters 7ea463ed08 退役旧创作模板业务并保留数据壳
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义
移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖
恢复现役创作、项目、我的入口及桌面移动导航
收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界
补齐开发栈、网关、原生壳和文档退役约束
2026-07-18 22:02:22 +08:00
lhk229 0c04bbbea3 内存优化 (#91)
BGfilter服务调用改为传url,减少内存占用

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/91
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-18 21:17:35 +08:00
kdletters d465e9b66c 补充图集拆分中的前端表现
维护逐图层拆分状态并在请求结束后可靠清理
源图集显示扫描蒙层和拆图中状态胶囊
工具栏显示旋转图标并禁用重复提交
补充工作流、画布、工具栏测试及入口验收文档
2026-07-18 19:52:45 +08:00
kdletters 4ceb91cd48 完整恢复多产物原图回填画布
图标素材成功时同时回填透明图集和provider原图
UI素材提取成功时保留原图并将切片排列到其右侧
保持透明主结果锚点和后处理失败原图降级
同步多产物画布决策、踩坑与技术方案
2026-07-18 19:31:44 +08:00
kdletters 6926e5ecb1 恢复角色生成原图回填画布
角色生成成功时同时回填透明主图和右侧原图
保持透明图为生成器主锚点并保留失败降级边界
维持图标和UI生成不回填中间原图的现有规则
同步图片画布多产物文档和项目共享记忆
2026-07-18 18:58:56 +08:00
kdletters bc76b1327d 修复项目重命名弹窗背景丢失
项目重命名弹窗改用平台工具弹窗壳继承主题
新增暗色主题下的重命名弹窗回归测试
补充 portal 弹窗主题丢失的排障记录
2026-07-18 17:44:43 +08:00
kdletters 9c9c8f468a 恢复平台个人页并阻断旧业务回流
恢复创作、项目、我的平台侧栏与现役搜索
恢复全宽个人页及资料编辑、充值兑换、社区反馈和 API Key 入口
迁移平台资料、钱包和设置客户端并移除旧创作依赖
新增 Vite 与 ESLint 退役模块门禁并补齐界面和搜索测试
同步旧创作退役方案、平台入口决策与踩坑记录
2026-07-18 17:29:06 +08:00
menghao 47f3297e84 修复图片改造后二维清晰度降级问题 (#90)
前端按图层真实尺寸恢复图片改造比例与清晰度

后端在角色去背景后恢复生成原图像素尺寸

补充前后端二千像素尺寸回归测试

同步更新图片画布技术文档与项目决策记录

---------

Co-authored-by: MengHao <menghao@MengHaodeMacBook-Air.local>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/90
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 孟豪 <mh18530625731@163.com>
Co-committed-by: 孟豪 <mh18530625731@163.com>
2026-07-18 16:50:17 +08:00
kdletters f3ab2f2ec0 恢复平台账号顶栏与我的页签
按新版创作页原布局恢复泥点入口与账号身份胶囊。

在桌面侧栏补回我的页签及游客、登录态个人页面。

个人页面只复用账号、钱包、统计、设置和法律信息等平台能力。

补充壳层与个人页面回归测试并更新退役边界文档。
2026-07-18 16:15:40 +08:00
kdletters c56da8dc26 恢复新版创作主页与平台侧栏
恢复 /creation 新版创作工具主页及创作、项目公共侧栏。

将 creation-home 重新纳入 Vite、TypeScript、ESLint 与 Vitest 活动编译链。

恢复 /creation 的 Nginx 与 Pingora SPA 白名单并保留旧子路径下线边界。

补充路由、标题、CSS 过滤和网关 smoke 回归验证。
2026-07-18 15:41:55 +08:00
kdletters f625fe8e8b 修复退役样式开发态加载
将退役 CSS 源码过滤调整到 Tailwind 转换前执行
新增 Vite 插件顺序回归测试
补充退役方案与开发态白屏排障记录
2026-07-18 15:12:09 +08:00
kdletters a5ae283561 修复画布Agent工具调用轮询状态同步
补齐工具调用轮询依赖并同步服务端终态
清理任务切换后的旧轮询,避免迟到响应覆盖
新增失败、取消及任务切换竞态测试
2026-07-18 12:44:40 +08:00
kdletters 5e512a6b23 修复 Rust 代码格式 2026-07-18 12:22:58 +08:00
kdletters e9c3dc1120 退役旧创作模板业务
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码
切换前端 active 入口并解除旧创作页面和路由编译链
移除旧后端路由、worker 与纯业务 crate 依赖
收敛 SpacetimeDB 模块为历史数据壳
同步 Nginx、Pingora、验证门禁与架构文档
2026-07-17 22:07:52 +08:00
kdletters 5468bb3212 修复画布生成器恢复串台
区分生成结果归属与派生输入关系,避免角色动作和快速编辑面板串台
补全角色动作、视频、音效、背景音乐与宣发素材的生成器恢复类型
统一清理剪切和素材删除后的关联生成状态,避免孤立占位
补充生成器恢复、宣发类型和删除清理回归测试并同步编辑器文档
2026-07-17 21:23:12 +08:00
k88936 f5368c825f Editor agent refactored (#76)
重构了editor agent.
使得它能进行多轮工具调用, 把工具调用的结果嵌入到上下文里。
对于上下文的图片, 使用哈希后的id用来引用,不暴露细节信息to llm。
当前实现, 状态直接维护在json doc里, 需要对整个doc加锁,任务目前只能串行。
把SSE改成一个简单请求( 因为生图工具耗时需要二次确认, 不需要再实时展示给用户进度)。
增加确认/取消生图操作。
把tool的参数和错误情况做了反馈。

TODO:  工具调用的规范/prompt还可以改进。

改用external job来做素材生成, 针对来自editor agent 的生成会把生成资产的引用加入到结果json里,
客户端轮询 external job 确定生成状态, 发现结束了或者失败了就 重新get 会话,后端重新提供会话的时候把 结果插入回会话历史里,用来让 ai 引用 以及显示

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/76
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-07-17 21:03:15 +08:00
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00
kdletters 18af49c27f 收紧编辑器媒体引用契约
禁止当前画布与外部编辑器接口接收 Data URL 和 Blob URL
生成请求统一先上传 OSS 并使用稳定资源引用
角色动画和视频签名前校验资源归属并支持资源与素材 ID
同步前后端 256KB 引用限制及相关文档测试
2026-07-17 16:44:01 +08:00
kdletters 93b27a2307 修复图集拆分运行时身份传递
在 procedure 外层捕获调用 identity 并显式传入事务
补齐素材归组持久表的迁移导入导出清单
修正 DDD 门禁注释解析并补齐编辑器 Agent 事件落位
同步后端架构约束与项目排障记忆
2026-07-17 14:45:11 +08:00
kdletters 8febb799f3 升级SpacetimeDB至2.6.1
统一Rust crates、CLI、容器镜像和生产provision版本
使用2.6.1 CLI重新生成Rust bindings
更新运行检查、架构运维文档和项目共享记忆
2026-07-17 13:44:12 +08:00
kdletters ed1a972c9e 禁止快速编辑任务中继续框选
快速编辑进入生成中后移除框选工具和覆盖层
新增挂起请求回归测试覆盖任务开始后的锁定状态
更新图片画布快速编辑项目记忆与验证口径
2026-07-16 22:23:29 +08:00
kdletters a0242e3547 修复图集快速编辑模型路由
快速编辑请求补齐模型比例和清晰度参数
后端按模型分流nanobanana与GPT图片编辑协议
兼容旧队列参数并收紧图片编辑计费边界
补充图集回归测试并同步OpenAPI与编辑器文档
2026-07-16 22:08:05 +08:00
kdletters 77e931ed96 修复外部编辑器接口测试格式门禁
调整图片生成警告断言的 rustfmt 换行格式。
2026-07-16 21:07:10 +08:00
kdletters 0253a35e72 补齐归档清单上传限速
让已有全量归档的 manifest 继承同一上传带宽限制器
收紧并发上传共享累计带宽预算测试
2026-07-16 19:02:42 +08:00
kdletters 947f59c894 限制数据库备份上传带宽
新增共享上传带宽限制器并覆盖数据对象、分片、catalog 与 latest pointer
补齐并发聚合限速、源流错误传播和大型 manifest 分块测试
更新备份环境变量示例与生产运维文档
2026-07-16 19:00:35 +08:00
lhk229 aecabdacfd 新抠图算法 (#85)
Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/85
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-16 18:33:06 +08:00
kdletters 94014392ed 优化逐文件小对象上传
小于等于16MiB的CAS对象改用单次PUT并执行HEAD验真
大对象继续保留multipart上传与失败清理
补齐单次PUT请求路径测试和运维说明
2026-07-16 18:28:04 +08:00
kdletters 40684aafcd 优化逐文件备份并发上传
增加files对象PUT和HEAD受控并发
限制并发范围并降低大目录进度日志量
补齐并发上限测试、环境示例与运维文档
2026-07-16 18:18:26 +08:00
kdletters 56d70bf720 补齐逐文件备份符号链接恢复
记录data-dir内部相对符号链接并拒绝绝对或越界目标
恢复full catalog时安全重建符号链接
补齐备份测试与生产运维文档
2026-07-16 18:16:38 +08:00
kdletters 9ae3f53989 完善SpacetimeDB逐文件增量备份
新增data-dir逐文件CAS基线、history归档与安全清理
发布并校验OSS latest pointer,支持异机自动恢复
接入dev和release可选的files-history定时备份profile
补齐备份测试、生产门禁、环境示例与运维文档
2026-07-16 18:00:06 +08:00
kdletters b303605bf2 新增后台新增用户付费率
扩展 Dashboard 聚合与契约,统计筛选期新增用户的真实付费转化
生成 SpacetimeDB typed bindings 并补充后端聚合测试
后台新增付费率卡片与响应式布局,覆盖零分母和时段文案
同步 Dashboard 运营口径与项目决策记录
2026-07-15 22:14:52 +08:00
kdletters ea8d0ef064 修复泥点余额自动刷新
顶部泥点总余额优先展示实时钱包余额,避免缓存明细覆盖刷新结果。

补充缓存明细场景的余额变更回归测试。

同步主站与图片画布的泥点余额数据口径文档。
2026-07-15 22:01:25 +08:00
kdletters cfdba640ee 修复图集快速编辑后端校验与错误提示
后端允许完整图标图集进入图片快速编辑,仅继续禁止单个图标。

快速编辑失败时向红色错误框传递具体错误文案。

补充前后端回归测试并同步画布编辑器技术文档。
2026-07-15 21:11:46 +08:00
kdletters 271b153308 统一Rust格式化工具链与构建门禁
固定Rust 1.96.0与rustfmt格式配置

接入本地lint、Codex提交前检查和Jenkins构建门禁

提交server-rs全workspace一次性格式化基线

同步开发运维文档与production-ops防回退检查
2026-07-15 21:06:05 +08:00
kdletters 25f6027c4a 补齐外部素材创建归组字段
为 External Editor 素材创建补充归组 Task ID 默认值

为 External Editor 素材创建补充拆分预期数量默认值
2026-07-15 20:34:07 +08:00
kdletters 793ec1b6b0 修复后台素材查询分页与任务归组
支持陶泥号解析、稳定游标和后台素材多页查询

补齐查询防抖、请求取消、刷新保留与加载更多状态

新增可信来源索引和拆分批次完成事实,修复跨项目与删除切片归组

同步生成绑定、数据契约、项目记忆和回归测试
2026-07-15 20:02:50 +08:00
kdletters 8d1c640eb5 修复精选图片读取与审核返还
为已通过、已展示且返还完成的精选资产补充精确匿名读取授权

将精选批准、泥点返还和返还完成标记收进同一SpacetimeDB事务

保留确定性返还流水的幂等语义并支持修复历史半完成记录

补充精选公开门禁测试并同步后端与创作主页文档
2026-07-15 16:58:18 +08:00
kdletters 7ed99d7353 拆分SpacetimeDB调用池与缓存读连接
将HTTP调用池改为无订阅连接并增加单一共享缓存读连接

统一缓存重建总超时并拒绝发布断线连接

补充连接拓扑回归测试与生产运维说明
2026-07-15 15:11:11 +08:00
kdletters 2f092c3566 统一素材任务层级与成本归因
后台素材查询按任务分页并将中间产物逐行收纳为子项
生成原始产物沿用真实素材类型并承载任务生成成本
抠图、拆分和逐帧后处理记录实际生成器且阶段成本归零
补齐历史类型映射、角色动作链路、测试与文档
2026-07-15 03:04:41 +08:00
kdletters 3f3a6c4ab2 修复画布生图交付与素材成本展示
修复 nanobanana 标量尺寸被误判为宽高的问题。

普通生图和快速编辑在内存完成尺寸变换后仅上传一次,变换失败保留 provider 原图。

移除快速编辑重复的原始输出素材,保留角色、图集和 UI 的独立中间产物。

后台将不重复计费的中间产物显示为不单独计费。

补充单次上传、降级交付和成本展示回归测试及相关文档。
2026-07-15 01:36:16 +08:00
kdletters 5c1434511b 修复后台看板留存统计与素材详情布局
修正周月日期范围、每日访问人数、零值柱和趋势图滚动同步
新增次日与七日留存及数据库侧精确聚合,移除五万行访问明细截断
聚合失败时拒绝返回伪零值并补齐契约、客户端绑定和定向测试
修复素材详情陶泥号竖排并完善移动端单列布局
同步运营口径与本地数据库版本漂移排障文档
2026-07-15 01:25:29 +08:00
kdletters 9807295a7d 调整画布生成输入与图集快速编辑
隐藏生成面板资源名称字段并恢复提示词输入边框

恢复图标图集快速编辑并仅对单图标禁用

补充工具栏右键菜单工作流回归测试与编辑器文档
2026-07-14 23:26:35 +08:00
kdletters c8dd6cbd33 统一 Jenkins 本机 Git 源
将生产流水线 Git checkout 地址统一为 127.0.0.1:2222
数据库导入导出改由 Built-In Node 准备并传递脚本
新增 Git 源与目标节点边界的生产运维门禁
同步生产运维文档和项目共享记忆
2026-07-14 22:03:32 +08:00
k88936 34c6ed8756 自动扩缩edit图片的大小适应gptimage2的生成图片大小参数要求 (#82)
尺寸严格限制规则 (Strict Size Constraints):
图片最大边长 ≤ 3840px <- by hardcoded length
宽高两边像素均为 16px 的倍数 <- by backend do crop
长边 / 短边 比值 ≤ 3:1 <- by hardcoded aspect ratio
总像素范围:最小 655360 ~ 最大 8294400 <- by auto scale in this pr

logic impl in the backend

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/82
Co-authored-by: kvtodev <kvtodev@outlook.com>
Co-committed-by: kvtodev <kvtodev@outlook.com>
2026-07-14 21:04:54 +08:00
kdletters 630fce1438 Merge pull request '抽取钱包/充值入口组件, 用来在客户端里复用' (#77) from extract-wallet-entry-to-shared into master
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/77
Reviewed-by: 段舒康 <kdletters@qq.com>
2026-07-14 21:03:16 +08:00
kdletters 75dd05d2e6 Merge branch 'master' into extract-wallet-entry-to-shared 2026-07-14 21:02:06 +08:00
kdletters f3936f60a3 修复后台多账号安全与事务边界
将Argon2密码操作移出异步执行器并限制并发
统一未知停用和非法账号的登录校验与错误语义
隔离登录凭据快照和普通账号公开契约
调整管理员显示名查询顺序避免写入伪失败
新增后台账号procedure身份与事务集成烟测
同步后台账号技术方案与开发验证文档
2026-07-14 20:30:44 +08:00
k88936 08e17fa640 Merge branch 'master' into extract-wallet-entry-to-shared 2026-07-14 20:28:45 +08:00
kdletters bddc25b4e0 后台支持多账号与页面访问权限
保留环境变量管理员作为 owner,并新增可持久化管理的 member 账号
建立一级页面权限、实时会话失效和后端逐请求鉴权
统一操作记录与配置记录展示管理员显示名称,隐藏内部主体标识
补充账号管理页面、生成绑定、定向测试和技术文档
2026-07-14 19:21:33 +08:00
kdletters a597fcd076 收紧退款人工复核与重试契约
补齐退款事实对比字段与人工复核审计展示
人工复核重放严格校验管理员、原因和错误码
退款占用重试完整匹配管理员与归一化原因
统一退款原因持久化上限并修复刷新恢复
补充前后端回归测试、生成绑定与数据契约文档
2026-07-14 19:20:25 +08:00
kdletters c70a33d682 修复退款复核与渠道校验
退款 observation ID 按来源与事实指纹稳定区分
人工复核只豁免本次批准的订单冲突并持久化错误码
普通微信渠道在查单前拒绝虚拟支付、会员与非正式渠道
网络结果未知时当前弹窗直接复用原请求继续核对
按 release 首次上线基线移除未发布退款结构兼容并同步文档与绑定
2026-07-14 18:09:50 +08:00
k88936 d66b17b855 抽取配色css (#79)
Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/79
Reviewed-by: 段舒康 <kdletters@qq.com>
Co-authored-by: kvtodev <kvtodev@outlook.com>
Co-committed-by: kvtodev <kvtodev@outlook.com>
2026-07-14 17:01:39 +08:00
k88936 99d76b25f9 Merge branch 'master' into extract-wallet-entry-to-shared 2026-07-14 16:52:47 +08:00
kdletters 0baa8cf8cc 修复退款复核与发布安全边界
补齐退款人工复核、刷新恢复、会话隔离和旧冷备导入兼容
强化真实微信退款对账开关、重复环境变量和生产发布门禁
优化公开作品资产授权查询并保持实时撤销语义
统一后台微信渠道、退款追回流水和画布正式资源测试契约
补齐生成与素材侧栏回归测试
2026-07-14 16:31:22 +08:00
kdletters aa56c117ec 修复角色动作素材入库
将最终角色动作帧序列保存为单条素材并保留完整元数据
登记全部动作帧的私有资产对象并以首帧作为预览
补充动作素材持久化测试和后端数据契约
2026-07-14 15:48:30 +08:00
kdletters 2653adeb34 合并微信支付退款闭环
新增后台充值退款管理与微信 V3 退款闭环
统一退款观察、对账、权益追回和钱包冻结
修复成功退款冲突时点数钱包未冻结问题
保留后台素材预览连续加载修复
2026-07-14 13:58:16 +08:00
k88936 708b9cafda Merge remote-tracking branch 'origin/master' into extract-wallet-entry-to-shared 2026-07-14 13:45:19 +08:00
kdletters 3861c9c99a 修复后台素材预览连续加载
按视口懒加载并错峰执行素材换签请求
增加限流退避与卸载清理,避免后续素材永久占位
补充滚动、分页、大批量排队和缩略图路径回归测试
记录后台素材换签限流排障与验证口径
2026-07-14 13:39:20 +08:00
kdletters 8862887baa 新增后台充值退款管理与微信V3退款闭环
新增充值订单查询、通用用户详情、钱包冻结与部分退款操作
接入微信V3退款申请、回调、主动查单、交易账单对账与安全诊断
新增退款占用、权益追回、异常欠账和消费限制事务
补齐Native支付SSE自动收口及退款编号与部分退款校验
同步SpacetimeDB schema、生成绑定、测试与项目文档
2026-07-14 12:16:01 +08:00
k88936 11abb43438 if it run , dont del it 2026-07-14 11:12:07 +08:00
k88936 b13d73c5cf sync doc 2026-07-14 11:11:11 +08:00
k88936 8d7394eda1 fix simple import 2026-07-14 11:05:09 +08:00
k88936 d307abf7a3 removed component export from index 2026-07-14 11:04:01 +08:00
k88936 5244dbfcd8 rollback: all in relative path now 2026-07-14 10:56:11 +08:00
kdletters ed8be38287 统一画布生成资源命名
新增所有生成面板的资源名称输入并统一80字符边界
贯通画布标题、项目资源、素材库与生成请求的名称传播
为图标图集和角色动作补齐命名契约及中间产物后缀
补充前后端回归测试、OpenAPI与编辑器文档
2026-07-14 10:27:25 +08:00
kdletters f7f799df81 修复画布音频拖放与拖拽热区
素材库显式拖放不再叠加级联偏移,音频卡中心落在鼠标松手位置
将音频播放按钮热区收紧到中央,释放卡片主体用于选择与拖拽
补充模型、桥接和画布交互回归测试并同步音频设计文档
2026-07-14 10:23:42 +08:00
kdletters 838d819c3d 修复Suno背景音乐下载失败
识别audiopipe中转地址并通过item_id获取稳定WAV文件

避免封面地址混入音频候选并跳过失败任务的额外探测

补充Suno两跳解析回归测试并更新设计与排障文档
2026-07-13 22:06:36 +08:00
kdletters 4e7bcb8f24 补齐生成中间产物持久化与拆图提示
图标和UI图集拆分失败时保留主图集并在内联、队列及刷新路径提示
角色、图片修改、图标UI和角色动作链路把可恢复中间产物写入项目与素材库
外部生成摘要新增独立告警字段并清理成功任务的历史错误
同步共享契约、SpacetimeDB迁移与绑定、OpenAPI、设计文档和回归测试
2026-07-13 21:41:46 +08:00
kdletters f1639de5fe 修复图标图集自动拆分降级
图集带背景原图和透明整图先写入项目资源与账号素材库并回填画布
自动拆分识别或持久化失败时保留图集并返回结构化警告
手动拆分限制图片尺寸、总像素和单次切片数量
同步共享契约、外部 OpenAPI、前端类型、设计文档和回归测试
2026-07-13 20:29:06 +08:00
kdletters 052d656a52 修复公开资产授权一致性与签名期限
公开资产 metadata 和作品授权改为在同一 SpacetimeDB 事务快照中判断
移除连接级公开授权订阅缓存对 ACL 的参与
公开读取签名有效期上限固定为 600 秒并保留 owner 和 admin 行为
同步生成 bindings 并更新后端契约与项目记忆
2026-07-13 20:04:29 +08:00
kdletters 4fc8beea72 合并画板图集自动拆分 2026-07-13 19:22:33 +08:00
kdletters eae00292a1 新增画板图集自动拆分
生成图标图集后自动拆分并持久化独立素材
新增图集工具栏手动拆分入口与权威画布回填
修复连通域切片阅读顺序并补齐前后端测试
同步外部接口与编辑器设计文档
2026-07-13 19:10:30 +08:00
k88936 e6d0ccc243 use import { formatMudPointCount } from '@genarrative/shared' style 2026-07-13 18:48:58 +08:00
kdletters 3cbc33bd69 优化画布快速编辑参数与替换规则
快速编辑继承并支持模型、比例和尺寸参数,旧模型回落当前默认模型
仅禁止图标与图标图集快速编辑,图标规范继续按普通图片处理
按用户选定分辨率替换当前图层并保持图层中心位置
补齐后端真实图层校验、移动端避让、定向测试与项目文档
2026-07-13 18:48:38 +08:00
kdletters 2ac57463c0 修复公开作品私有资产读取
新增公开可见作品的精确资产授权视图并覆盖十类玩法

改用受限 procedure 权威点查资产对象并在失败时关闭访问

移除资产对象全表订阅并统一 ACL 与编辑器引用查询

生成 SpacetimeDB 绑定并补齐回归测试、架构文档和项目记忆
2026-07-13 18:35:26 +08:00
kdletters 5536fba95e 补齐画布多产物生成回填
将角色、图标图集和UI素材提取的原图登记为项目资源并随处理结果写入画布
保持后处理结果为生成器主锚点并将原图错开放置
新增原图标签、后端多产物布局测试和前端快照回归
同步更新图片画布方案与共享决策记录
2026-07-13 18:28:54 +08:00
k88936 a3034b1d51 fix import 2026-07-13 18:08:46 +08:00
kdletters 55471e15de 补齐后台兑换码有效期与独立滚动
为兑换码增加可选生效和截止时间,并按左闭右开区间校验
同步 SpacetimeDB 迁移、生成绑定、共享契约和后台管理界面
让后台侧边栏与主内容区域独立滚动
修正兑换业务拒绝的 HTTP 状态映射并补充回归测试和文档
2026-07-13 17:26:47 +08:00
kdletters f4153ebfa7 统一维护与错误页面并修复移动端顶部文案
新增品牌维护页和404页面并补齐网关路由
将临时维护公告迁到release外运行态覆盖并增加构建门禁
同步Nginx、Pingora、部署脚本与生产运维文档
隐藏移动端顶部SEO介绍并保留桌面端与H1语义
增加维护页、404页和移动端回归测试
2026-07-13 16:26:27 +08:00
k88936 c038275e11 add back moved code to git 2026-07-13 15:49:34 +08:00
k88936 071dac67c1 fix import extension for formatMudPointCount in PlatformMudPointWalletEntry 2026-07-13 14:46:24 +08:00
k88936 9c982992a7 update import paths for PlatformMudPointWalletEntry 2026-07-13 14:18:21 +08:00
k88936 9831571dda fix resolve res in Vitest 2026-07-13 14:03:02 +08:00
k88936 b7b4dd0066 extract mud point wallet entry component for reuse in tauri app 2026-07-13 13:52:40 +08:00
kdletters 36a8719d0f 补齐微信虚拟支付发货确认
在虚拟支付商品入账后调用微信发货确认接口
允许已入账商品订单只重试发货而不重复发放权益
遇到失效 access token 时强制刷新并最多重放一次
要求使用微信 paid_time 并完善历史补单文件清理和回归测试
2026-07-13 13:39:59 +08:00
kdletters 6143189dd9 恢复停服升级公告页面
恢复今天晚上 21:00 至 22:00 的维护时间公告
列出画布与 PC 网页充值修复内容
补充抠图与背景色升级说明
适配桌面端和移动端维护页面布局
2026-07-13 13:17:22 +08:00
kdletters e815ece133 补齐完整发布备份模式参数
在 Full 流水线显式增加 DATABASE_BACKUP_MODE 参数
将备份模式原样透传到 SpacetimeDB 发布作业
增加生产运维门禁防止 Full 静默回退到异步备份
补充 release 禁止再次上传时必须选择 skip 的运维说明
2026-07-13 13:13:11 +08:00
kdletters 2f69f2fda2 补齐微信虚拟支付查单与补偿
接入微信官方虚拟支付查单并缓存稳定接口令牌
让用户确认与订单过期补偿统一核对支付类型、状态、金额和单号
增加单笔历史订单的 dry-run 与确认指纹补单工具
隔离运行时身份与敏感 openid 文件并补充配置契约和回归测试
2026-07-13 13:12:44 +08:00
kdletters 06aa99e3eb 修复后台表查询筛选与分页
使用单次哨兵查询统一计算扫描范围、匹配总数和截断状态
增加完整候选集服务端排序与越界页码钳制
增加固定分页栏、扫描上限提示和后端排序交互
补充大表响应边界文档与前后端回归测试
2026-07-13 13:09:53 +08:00
kdletters 7becd38117 修复泥点悬浮窗与登录弹窗交互
桌面端泥点详情支持从按钮移动到浮层并取消点击保持
统一弹窗仅在完整遮罩指针序列后关闭
补充泥点悬浮与登录遮罩拖出回归测试
记录遮罩关闭交互约束
2026-07-13 12:47:40 +08:00
kdletters 184d7e24c9 恢复默认维护页面
移除临时停服升级公告和维护时段文案。

恢复简洁的服务维护提示页面。
2026-07-13 11:36:07 +08:00
kdletters aeca412cdb 修复数据库冷备分片上传
冷备归档改用 OSS Multipart 顺序分片上传并加入可重试请求。

Complete 后通过签名 HEAD 严格核对远端对象长度,再更新清单或清理本地归档。

补齐签名查询、分片重试、缺失 ETag、Complete 歧义和 Abort 回归测试。

记录数据库冷备上传与清理边界的长期运维决策。
2026-07-13 11:13:57 +08:00
kdletters cde9b597bc 修复微信支付通知公钥验签
解析微信平台 SPKI 公钥并提取 PKCS#1 RSA 公钥。
复用生产 RSA-SHA256 验签路径处理支付通知。
补充标准 SPKI 公钥与真实签名向量回归测试。
同步微信支付验签排障文档和解析依赖。
2026-07-13 00:45:20 +08:00
kdletters 35568eeef4 修复泥点账单结算顺序
按当前余额反向校验流水结算链,纠正延迟入账的历史错序。

钱包流水与钱包状态统一记录事务实际结算时间。

补充延迟支付场景回归测试和账本时间契约文档。
2026-07-12 23:45:01 +08:00
kdletters 8e1f441d6b 补齐发布包维护脚本
把 API 部署与维护开关脚本复制进 current release
补充发布夹具对三个随包脚本的存在性断言
记录 Full 退出维护依赖 current release 自包含脚本
2026-07-12 23:02:58 +08:00
kdletters 1a90b4f644 收窄充值过期监听范围
改为订阅活跃充值过期定时器删除事件并按订单编号回读状态
移除不再使用的充值订单表行映射
同步后端契约、生产运维与项目共享记忆
2026-07-12 22:42:30 +08:00
kdletters 425699b2e2 修复发布就绪等待与充值过期监听
为 API readiness 探测增加单次超时,避免端口已建立时无限挂起
修复 SpacetimeDB 枚举状态订阅导致的充值过期监听失败
在 pause-after-stdb 审批提示中强调运行时与迁移身份互斥
补齐生产发布门禁、回归测试与运维排障文档
2026-07-12 22:24:47 +08:00
kdletters 51740c021b 补齐流水线维护退出选项与停服公告
Full 发布全程保持维护并在最后按选项退出
API Deploy 支持成功后保留维护并补齐回归门禁
新增停服公告维护页并同步运维文档
2026-07-12 21:24:17 +08:00
kdletters 71811f04c3 维护模式仅拦截公网流量
Nginx 按真实连接地址让内网绕过全站维护闸
Pingora 统一内网维护放行并拒绝 XFF 伪造
补齐公网与内网维护回归测试和运维文档
2026-07-12 20:27:04 +08:00
kdletters 8bfedd8ad7 调整首页品牌文案字号
将品牌说明字号固定为 17px
将首页主描述字号调整为 20px
将泥点赠送文案字号调整为 16px
2026-07-12 20:22:18 +08:00
kdletters 66178edc7c 统一泥点账户与每日免费额度
- 新增每日免费泥点独立余额桶,按北京时间零点固定重置20泥点
- 按每日免费、会员限时、永久顺序扣费并支持跨日退款按原分桶回补
- 统一主站与图片画板泥点入口,隐藏限时泥点、任务入口和会员购买入口
- 收敛四档充值商品与泥点购买弹窗,并接入后台动态商品配置
- 同步余额read model、SpacetimeDB迁移绑定、回归测试和工程文档
- 将泥点明细浮层圆角匹配首页卡片视觉规范
2026-07-12 19:35:22 +08:00
kdletters 2b13067492 调整首页品牌文案层级
更新首页唯一 H1、小眉题、产品说明与权益文案
统一游戏美术 AI 创作工具 H2 和功能弹窗 H2 语义
补充首页标题层级测试与 SEO、创作主页文档约束
2026-07-12 19:32:11 +08:00
kdletters 300e69c97d 修复图片改造尺寸保持
保持图片改造提交与画布回填的原图尺寸
在 VectorEngine 边界对齐非 16 倍数参考图并恢复目标尺寸
补充前后端回归测试与编辑器尺寸契约文档
2026-07-11 18:10:00 +08:00
kdletters d756e8e04b 补齐Jenkins凭据与维护后台通道
固化三个Stdb流水线的Secret File凭据默认值
将04:00开发服定时发布默认切换为normal
显式关闭三个下游Build Job的自动发布
允许内网来源在维护模式下访问后台完整链路
保持公网后台与普通业务路由的维护阻断
补齐Nginx、Pingora回归门禁与运维文档
2026-07-11 17:40:37 +08:00
kdletters ad2e59292c 合并 SEO 基础设施与精确路由
合并 codex/seo 的首页 SEO 元数据、robots 与 sitemap
统一 Nginx 和 Pingora SPA 精确路由及未知路径 404
保留 master 与 SEO 分支的项目记忆条目
补齐路由门禁、Pingora smoke 与前端回归测试
2026-07-11 16:12:05 +08:00
kdletters 2dac4a69fd 统一升级 SpacetimeDB 至 2.6.0
精确锁定 SpacetimeDB Rust crate 并更新依赖锁文件
使用 2.6.0 CLI 重新生成 Rust bindings
对齐容器镜像、server provision 与本地版本校验
更新运维文档、项目记忆与 SpacetimeDB skills 版本口径
2026-07-11 15:54:16 +08:00
kdletters 324f99efdf 修复外部生成任务大载荷导致的内存膨胀
阻止图片编辑器将 Data URL 和超限 JSON 写入生成任务,复用 objectKey 等轻量引用
新增外部生成任务摘要投影与分批 payload 压缩回填流程,避免正式列表读取大字段
收紧 SpacetimeDB 发布为永不删数据并移除 Jenkins 普通清库入口,补齐维护窗口门禁
更新生成绑定、运维文档和回归测试,并完成生产数据副本迁移与压缩验证
2026-07-11 13:55:05 +08:00
kdletters f848cbf78b 修复模型定价运行时身份边界
仅允许 HTTP 角色初始化定价,非 HTTP 角色改用队列只读鉴权
移除定价 procedure 快照中的 writer identity 并同步 bindings
后台保存复用受保护 bootstrap secret 支持缺失配置自愈
补充回归测试与架构运维文档
2026-07-10 19:50:55 +08:00
kdletters 187d7735b9 修复编辑器定价持久化与资源预览
将模型定价改为 SpacetimeDB 强类型持久化并兼容旧配置种子迁移
修复快速编辑支付弹窗期间框选显示和交互冻结
补齐后台图片放大以及视频音频资源预览
收紧主站和 External 资源换签授权并记录管理员跨用户审计
固化外部生成入队价格、attempt 钱包结算和最终 lease 失败收口
加固运行时身份轮换、bootstrap secret 与生产构建发布门禁
同步生成绑定、定向测试、运维脚本和项目文档
2026-07-10 18:28:06 +08:00
lhk229 5c9b5ef69e 动作视频/生图背景色链路升级 + 阿里云抠图接入 (#75)
主要改动
1. 阿里云通用抠图接入(新增 platform-matting crate)

手搓阿里云 VIAPI 通用抠图接口(ACS3-HMAC-SHA256 签名),跨区输入走临时桶通道。
图片抠图升级为三档降级:BgFilter → 阿里云 → 本地键色兜底。
动作视频逐帧抠图改用阿里云,remove_editor_generated_green_screen_background 降为最后兜底。
2. 背景色不再写死绿色,改多背景色自动决策

动作视频背景色和生图一样从候选调色板自动选,选项继承。
3. 视觉背景色决策(修"蓝撞蓝")

让 LLM 看参考图选背景色(gpt-4o-mini 视觉模型,路由到 VectorEngine 视觉客户端),修复原纯文本盲选。
4. 背景色候选硬过滤器(新增 editor_screen_background_filter)

按参考图前景配色算 Lab 危险质量 + 皮肤专属三判据(ΔE 距离 / 色调投影 / RGB 分离)剔除撞色候选,LLM 只在安全集合里审美选。露肤角色自动收敛到冷区。新增"灰竹绿"候选补齐安全弧绿色段。
5. 源图合成到背景色(修视频背景变白)

透明源图先填成选定实色再发给 Ark,视频背景确定性等于抠图键色,不再赌模型服从提示词。
6. screenColorHex 结构化记录 + 死代码清理

四条链路统一把实际背景色写进 generation_inputs,修复生图链路"抠图背景色"字段从未生效的旧逻辑。
清理前端抠绿 + qwenSprite 死代码,移除一次性探针示例。

Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/75
Reviewed-by: kdletters <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-10 18:22:17 +08:00
kdletters c483980c9c 修复 Pingora 部署检查误报
显式传播 systemd 与 EnvironmentFile 提取失败,首错后立即终止部署检查
补充 direct-entry 与 systemctl cat 失败回归,阻止空环境连带误报
同步记录旧 release 残留 capability 的安全清理与验收口径
2026-07-10 16:23:37 +08:00
kdletters 3462c1efe1 修复 Pingora 部署检查误报
显式传播 systemd 与 EnvironmentFile 提取失败,首错后立即终止部署检查
补充 direct-entry 与 systemctl cat 失败回归,阻止空环境连带误报
同步记录旧 release 残留 capability 的安全清理与验收口径
2026-07-10 16:20:21 +08:00
kdletters 09fec601b1 完善官网 SEO 地基与精确路由
补充 robots、sitemap、SEO 元信息、结构化数据与首页语义内容
统一三套 Nginx 与 Pingora 的 62 条 SPA 路由和真实 404 行为
新增路由一致性检查、网关测试并同步技术文档与项目记忆
2026-07-10 12:14:44 +08:00
kdletters e464e62697 修复创作页滚动与画布生成状态同步
为创作页恢复可见滚动条并补充样式测试
普通点击画布对象时收敛多选状态,避免批量拖动
裁扩结果先资源化,并在去背完成时保留源图层
队列完成后重读未决项目快照并补充回归测试
同步图片画布持久化文档与长期排障记录
2026-07-10 08:46:17 +08:00
kdletters 2de9b79e69 修复充值过期补偿确认竞态
调整微信充值确认终态判断,未完成补偿查单的 expired 不再提前结束。

让 SSE 客户端和前端确认流程等待补偿后的 paid 或已检查 expired。

补充 unchecked expired 到 paid 的回归测试。
2026-07-10 02:09:59 +08:00
kdletters 00ff425e03 改造充值订单过期补偿流程
新增 SpacetimeDB 原生充值订单过期 timer 与 expired 状态。

移除 api-server 轮询 worker,改为 HTTP 角色订阅 Pending 到 Expired 事件并查单补偿。

统一微信下单 5 分钟 time_expire,并恢复前端支付通道选择与 expired 展示。

补齐生成绑定、前后端契约、测试和项目文档。
2026-07-10 01:37:53 +08:00
kdletters 6ddde830ef 修复钱包流水枚举追加顺序
将会员周期流水枚举追加到已发布枚举末尾

重新生成 SpacetimeDB Rust 绑定保持枚举判别序号兼容

补充 SpacetimeDB 持久化枚举追加约束文档
2026-07-09 20:12:16 +08:00
kdletters 12ba0e95f3 修复会员旧档迁移与周期权益
修正旧月季年会员迁移到新同级档位的购买判定

修正 active 升级后续周期天数切换为新商品配置

修正 mock 立即结算订单先按支付前会员状态定价

修正会员商品周期泥点文案按周期天数展示

补充会员迁移升级与前端周期文案测试
2026-07-09 18:16:34 +08:00
kdletters 104c376d6e 修复后台Dashboard月视图日期刻度
放宽 Dashboard 柱状图日期列宽并保持柱子居中

补充 Dashboard 长时段横轴滚动展示口径
2026-07-09 17:41:56 +08:00
kdletters c022bbb8a0 修复小程序内充值设备识别
为 API 请求和刷新请求补充宿主运行时请求头。

在小程序充值设备身份异常时清理旧 token 并重新拉起小程序登录。

补充小程序充值回归测试并隔离测试 spy。
2026-07-09 16:28:32 +08:00
kdletters 67fee3718d 修复画布背景色与任务计时显示
恢复生成任务列表的进行中计时显示并兼容微秒时间戳

替换画布背景色原生取色器为可拖拽色盘和色带

持久化画布背景色并在项目恢复时应用

补充背景色交互持久化和任务计时显示测试
2026-07-09 16:05:25 +08:00
kdletters 5f6f4d0ba6 接入会员周期泥点制度
新增 Starter、Basic、Pro、Ultimate 会员商品配置与权益字段。

拆分会员有效期、周期重置时间、周期限时泥点和永久泥点余额。

实现升级补价补点但不延长有效期或移动重置时间,同级购买只延长有效期。

补齐钱包扣费与退款中的会员周期泥点消耗、重置和流水记录。

同步后台商品配置、前端充值展示、共享契约、SpacetimeDB 迁移与生成绑定。
2026-07-09 14:48:26 +08:00
kdletters 2cf3953bad 收紧外部生成任务租约和超时保护
缩短 worker lease 并新增普通和长任务执行预算配置

让 worker 超过执行预算后写失败并释放槽位

降低 BgFilter 默认超时并加入连续失败熔断兜底

补充资产计费 future 取消时的退款补偿保护

同步生产 env 示例、部署脚本和运维文档
2026-07-09 01:32:23 +08:00
kdletters f1ca3e79c1 轻量化外部生成队列进程
worker 和 controller 改为订阅 external_generation_job 队列变更来唤醒 claim 与扩缩容评估
非 HTTP 角色关闭 API 读模型订阅并将 SpacetimeDB 连接池收敛为 1
更新 worker/controller 环境模板和运维记忆,明确 poll interval 只作兜底
2026-07-08 22:16:51 +08:00
kdletters 542fb09365 接入微信 Native 充值与订单过期处理
调整网页端充值渠道,微信内浏览器在开放平台接入前走 Native 支付。

拆分 Native 支付二维码弹窗,展示金额和充值内容并取消无效 code_url 跳转。

修复微信 Native 下单 time_expire 秒级 RFC3339 格式,避免小数秒参数错误。

新增充值订单 5 分钟过期调度,过期前查询微信真实订单状态。

补充微信授权绑定、路由加载恢复和后台表时间显示等配套逻辑。

更新 SpacetimeDB schema、绑定和相关文档测试。
2026-07-08 21:35:02 +08:00
kdletters 2721fec51c Merge remote-tracking branch 'origin/codex/gray-release' 2026-07-08 20:44:29 +08:00
kdletters 1462ec2920 优化灰度规则判定与后台表单
按需读取灰度用户标签

新建 Gate Target 时重置表单规则

补充灰度语义和踩坑文档
2026-07-08 12:22:48 +08:00
kdletters 25cac367c6 接入功能灰度与画布 Agent 灰度
新增 SpacetimeDB 灰度配置表与后端判定链路

新增后台灰度发布页面和两段式 Gate Key 选择器

将画布 Agent 入口接入灰度配置与运行时能力下发

修复灰度配置刷新读取空本地缓存的问题

补充灰度规则和后台下拉维护约定文档
2026-07-08 11:26:41 +08:00
kdletters 4e3c26d5d8 修复 release 发布与冷备份恢复
冷备份增加工作目录空间预检并确保失败后恢复依赖服务

API deploy 增加 Pingora 产物硬校验并区分 current 切换前后维护模式处理

Jenkins release 默认构建和部署 Pingora 影子网关并在全量流水线透传参数

补充备份与 API deploy 回归检查和生产运维护栏

更新生产运维文档与项目记忆的事故处理口径
2026-07-08 11:04:48 +08:00
kdletters 754d364227 修复画布生成刷新后完成同步
任务列表识别当前画布项目的未确认完成任务并发出完成通知。

编辑器收到任务完成通知后重新加载项目快照并刷新素材库。

补充刷新后任务已完成和轮询完成的回归测试。
2026-07-07 19:45:16 +08:00
kdletters 00db7d39ea 统一生成器对话框范围行为
移除生成器对话框打开后的安全范围自动拉动视口。

取消快速编辑、裁扩和角色动画面板的画布范围夹取。

保留创建生成器时聚焦新占位的行为并更新回归测试。
2026-07-07 19:36:20 +08:00
kdletters 862653c987 修复精选提示词展示
精选卡片优先展示用户可见的生成输入

过滤系统内置和素材提取提示词

补充精选提示词外露回归测试
2026-07-07 18:47:23 +08:00
kdletters 5888219c95 移除精选预览弹窗点赞操作
精选素材预览弹窗只保留点赞数展示

补充弹窗不展示点赞按钮的回归测试
2026-07-07 18:37:58 +08:00
kdletters 446c6945ad 恢复精选瀑布流布局
将创作主页精选列表从网格恢复为多栏瀑布流

新增样式契约测试防止瀑布流再次退化为网格
2026-07-07 18:34:07 +08:00
kdletters 9b49085869 修复画布生成器偶发无法打开
同步生成器 active/inactive ref 与 state

激活已有生成器时清理选参考图和菜单临时状态

窗口失焦时释放 Shift 与空格按住态

补充生成器状态与快捷键回归测试

记录图片画布生成器排障经验
2026-07-07 16:22:09 +08:00
kdletters b743152c6a 补齐zustand依赖锁文件
在 package-lock.json 中补齐 zustand 依赖声明

记录 zustand 5.0.14 的 npm lock 条目
2026-07-07 15:10:45 +08:00
lhk229 308d440816 新抠图算法(轻量版)上线 (#73)
新抠图算法(轻量版)上线,生图背景色同步改为多颜色+自动决策

---------

Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/73
Reviewed-by: kdletters <kdletters@qq.com>
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-07 14:57:10 +08:00
k88936 0c14e51419 Editor agent paste image (#72)
![shotmd-1783397773.webp](/attachments/659f35ce-87c1-449d-8342-8afc09934da8)

---------

Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/72
Reviewed-by: kdletters <kdletters@qq.com>
Co-authored-by: kvtodev <kvtodev@outlook.com>
Co-committed-by: kvtodev <kvtodev@outlook.com>
2026-07-07 14:56:28 +08:00
kdletters 5477abd7ba 修正精选活动图居中显示
恢复精选预览容器 23rem 高度上限

为活动卡预览添加独立样式并使用 contain 居中显示图片

补充活动卡预览 class 与比例测试
2026-07-07 14:19:46 +08:00
kdletters 2a98b60830 改为运行时控制画板 Agent 入口
新增前端运行时配置接口下发画板 Agent 开关

将画板 Agent 入口改为读取后端运行时配置

把开关变量改为 GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR

补充前后端测试和生产环境变量文档
2026-07-07 14:04:08 +08:00
kdletters 1d43783685 Merge pull request '隐藏画板素材信息中Object ID' (#71) from hide-obj-id into master
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/71
Reviewed-by: kdletters <kdletters@qq.com>
2026-07-07 13:54:09 +08:00
kdletters 9e92b3e673 Merge branch 'master' into hide-obj-id 2026-07-07 13:53:44 +08:00
kdletters 4622b35a32 修复精选活动图裁切
移除精选预览容器最大高度限制

让活动图按记录宽高自然拉伸卡片高度

通过浏览器验证 900x1200 活动图比例不再被压到 23rem
2026-07-06 22:59:25 +08:00
kdletters 2d631becc7 显示精选审核提交失败原因
统一素材精选审核提交资格判断

在素材菜单显示不可提交和后端拒绝原因

将编辑器业务拒绝映射为 400 并保留中文原因

补充精选审核原因展示测试和文档口径
2026-07-06 21:35:10 +08:00
kdletters be7f0375e5 记录精选活动卡图片宽高
后台上传活动卡图片时读取并保存原始宽高

隐藏活动卡 Object Key 输入,重复上传替换图片引用

公开精选按活动图宽高设置卡片比例

同步 SpacetimeDB schema、bindings 和后端数据契约文档
2026-07-06 21:00:21 +08:00
k88936 29db6365e1 隐藏画板素材信息中Object ID 2026-07-06 19:23:02 +08:00
kdletters ab61390121 修复画布 Agent 默认被任务列表隐藏
打开画布时 Agent 开关启用则保持 Agent 默认展开。

恢复任务自动打开任务列表时不再抢占 Agent 默认态。

用户手动打开任务列表时仍按右侧面板互斥关闭 Agent。

补充恢复任务场景下 Agent 默认显示的回归测试。
2026-07-06 19:15:03 +08:00
kdletters fcd353f12f 同步精选分类与活动卡文档
更新精选审核公开口径,说明未设置分类进入全部。

更新创作主页文档,移除素材包分类并说明全部 Tab。

更新后端表目录,记录活动卡 imageObjectKey 和 OSS 上传方式。
2026-07-06 18:59:40 +08:00
kdletters 51c47c952b 调整前台精选分类展示
前台精选分类新增全部并作为默认 Tab。

移除素材包 Tab 和素材包组合逻辑,未设置分类素材保留在全部中。

活动卡支持从 imageObjectKey 兜底图片地址。

精选卡片改为网格布局并让图片素材铺满预览区域。

更新前台精选模型和创作主页测试。
2026-07-06 18:58:58 +08:00
kdletters 68e9087552 完善后台精选活动卡上传
后台精选活动卡支持选择图片并通过 OSS 直传凭证上传。

保存活动卡时同步 imageSrc 和 imageObjectKey。

移除后台素材包分类选项,清空分类时不再自动隐藏精选素材。

补充后台活动卡上传和分类交互测试。
2026-07-06 18:57:20 +08:00
kdletters b5aa68fc6a 调整精选后端公开契约
新增精选活动卡图片 objectKey 存储字段和迁移兼容。

新增后台活动卡图片 OSS 直传凭证接口。

移除素材包精选分类约束,允许未设置分类素材进入全部精选。

同步 admin 和前台读取活动卡 objectKey 的类型契约。
2026-07-06 18:56:49 +08:00
kdletters b8e6e9f3ef 控制画布 Agent 入口默认关闭
新增 VITE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR 控制画布 Agent 入口显示。

默认 .env 关闭、本地 .env.local 开启,并补充示例和类型声明。

禁用时隐藏 dock 按钮和收起态 Agent 面板,开启时保留右侧任务列表互斥逻辑。

修正右侧 Agent 和任务列表切换不影响左侧资源栏。

补充画布 Agent 开关、面板独立性和 dock 隐藏测试。
2026-07-06 18:51:11 +08:00
kdletters 7c449f0596 修正音频素材封面
使用用户提供的透明 PNG 替换自绘 SVG 音频封面
统一后台素材查询、精选审核、创作主页和画板素材库的音频封面引用
补齐画板图层列表音频图层封面展示
更新相关测试覆盖
2026-07-06 14:44:43 +08:00
kdletters 0b766c8bf9 统一音频素材封面缩略图
新增音频素材统一封面图。
素材库、创作主页精选和后台缩略图改为使用音频封面。
补充音频封面展示回归测试。
2026-07-06 14:10:16 +08:00
kdletters b288b091b2 完善精选审核与素材查询展示
移除素材查询分类筛选和分类列

精选审核通过后改为手动设置展示分类

公开精选按分类展示并补充底部点赞按钮

使用文档大拇指透明图标并同步点赞测试

同步精选审核数据契约、绑定和文档
2026-07-05 18:01:02 +08:00
kdletters a24bbeeeb8 补齐API启动controller弱依赖
API systemd unit 增加 controller 弱依赖
生产 ops 护栏校验 API unit 依赖
同步运维文档和项目记忆说明
2026-07-05 18:01:02 +08:00
kdletters 1fb5a0773e 修复画板提示与失效项目跳转
画板参考图选择提示改为持续显示并支持手动关闭。

显式项目访问失效时同步切回项目页状态。

补充提示关闭和项目失效回退测试。
2026-07-05 18:01:02 +08:00
kdletters d7d98acfd4 接入VectorEngine画布Agent规划
切换画布 Agent 与通用 LLM 代理到 VectorEngine gpt-5.4-mini。

为画布 Agent 注入上一轮生成结果并默认承接上一张图编辑。

补齐规范图工具分流、规范展板 prompt 要求和 JSON 残片回复兜底。

同步前后端契约、测试脚本、环境示例和项目文档。
2026-07-05 17:42:27 +08:00
kdletters a4a7aed024 合并画布 Agent 侧边栏聊天
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	server-rs/crates/spacetime-client/src/module_bindings.rs
2026-07-04 18:39:40 +08:00
kdletters 1671d35995 修复画布Agent聊天生成体验
让画布 Agent 消息统一走后端规划与 LLM 回复
修复生成图片在聊天、画布和素材库中的展示与恢复
补齐右键删除、画布焦点、聊天滚轮和面板避让交互
同步 Agent 事件契约、文档和定向回归测试
2026-07-04 16:41:33 +08:00
kdletters 8dad52f040 实现画布Agent侧边栏聊天
新增画布Agent会话契约、领域规则、SpacetimeDB元数据表和生成绑定。
新增api-server画布Agent会话CRUD、OSS消息文档读写、SSE消息流和生成工具编排。
新增前端右侧Agent对话面板、会话状态hook、SSE客户端、BFF客户端和相关测试。
调整画布侧栏、任务栏、小地图默认状态和面板互斥交互。
补齐画布Agent、OSS消息存储、SSE收口、后端契约和项目记忆文档。
2026-07-04 12:03:58 +08:00
1927 changed files with 275382 additions and 43263 deletions
-15
View File
@@ -1,15 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(netstat -ano)",
"Bash(npm run:*)",
"Bash(findstr :8081)",
"Bash(taskkill:*)",
"Bash(findstr LISTENING)",
"Bash(npx tsc:*)",
"Bash(lsof -ti:8081)",
"Bash(curl -s http://localhost:8081/health)"
]
}
}
+31 -6
View File
@@ -14,20 +14,40 @@ if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) {
}
const validationSteps = [
{
label: 'Rust format check',
command: npmCommand,
args:
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run check:rustfmt']
: ['run', 'check:rustfmt'],
},
{
label: 'TypeScript typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'],
args:
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run typecheck']
: ['run', 'typecheck'],
},
{
label: 'Admin web typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'],
args:
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run admin-web:typecheck']
: ['run', 'admin-web:typecheck'],
},
{
label: 'Rust api-server compile check',
command: 'cargo',
args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
args: [
'check',
'-p',
'api-server',
'--manifest-path',
'server-rs/Cargo.toml',
],
},
];
@@ -66,7 +86,9 @@ function runStep(step) {
}
if (result.error) {
console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`);
console.error(
`[codex-hook] ${step.label} 启动失败:${result.error.message}`,
);
return { ok: false, status: 1 };
}
@@ -104,12 +126,15 @@ function extractShellCommand(input) {
input?.command,
];
const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0);
const command = candidates.find(
(value) => typeof value === 'string' && value.trim().length > 0,
);
if (command) {
return command;
}
const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
const shellCommand =
input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
if (Array.isArray(shellCommand)) {
return shellCommand.join(' ');
}
@@ -318,6 +318,14 @@ For image edit/redraw that should replace an existing canvas layer, pass `projec
For sound effects and BGM, `assetFolderId` and `assetLabel` can write the generated audio to the account asset library, same as image/video generation.
## Successful Responses with Warnings
Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction can return HTTP 2xx with an optional structured `warning`. A 2xx response means the task completed, but it does not guarantee that every requested post-processed derivative exists.
- Apply the returned `project` and media snapshots before interpreting optional derivatives: character responses use `resource` / `asset`, while icon spritesheet and UI extraction responses use `spritesheetResource` / `spritesheetAsset`. When `warning.code` is `postprocess-failed-source-preserved`, the saved provider source image is the authoritative main result. Character output has no transparent derivative; icon spritesheet and UI extraction output have neither a transparent spritesheet nor slices. Display `warning.reason` directly, and do not synthesize missing derivatives or restart generation.
- `sliceWarning` is a separate condition used only when transparent spritesheet post-processing succeeded but automatic slicing failed. Keep `sliceWarning.reason` as the original diagnostic and continue using the complete transparent spritesheet; a UI may add context when displaying it, but must not rewrite the stored reason.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. As defensive handling for a malformed response containing both, treat the general `warning` as authoritative and do not misclassify the source-preserved result as a slicing-only warning.
## Guardrails
- Do not invent endpoints outside the OpenAPI, especially internal worker or runtime task-list routes.
@@ -78,6 +78,14 @@ Ask a follow-up only when two routes could both be correct and produce different
All generation requests should be placed into both the current canvas and its same-name asset-library folder. For endpoints that support `assetLabel`, pass it. For UI extraction, use `spritesheetLabel`. For icon spritesheet, the folder is enough. For character animation, the endpoint does not return `asset`; after success call `POST /api/external/v1/editor/assets` using the first returned frame as `imageSrc`, the session `assetFolderId`, and `assetKind: "character-animation"`.
## HTTP 2xx Warning Handling
Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction may return HTTP 2xx while carrying a structured `warning`; completion does not imply that all post-processed derivatives exist.
- Consume the returned `project` and media snapshots as authoritative: character responses use `resource` / `asset`, while icon spritesheet and UI extraction responses use `spritesheetResource` / `spritesheetAsset`. `warning.code: "postprocess-failed-source-preserved"` means the saved provider source is the main result. Character output has no transparent derivative, while icon spritesheet and UI extraction have no transparent spritesheet and no slices. Display `warning.reason` directly; do not construct missing assets or retry the provider generation from scratch.
- `sliceWarning` is only for a transparent spritesheet that was created successfully but could not be split automatically. Use the complete transparent spritesheet and preserve `sliceWarning.reason` as the original diagnostic; it is not a post-processing/source-preserved warning.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. If a malformed response contains both, prioritize the general `warning` over `sliceWarning` defensively.
## Reference Image Upload
If the user provides a local file as a reference image, run upload before the generation request:
+4 -4
View File
@@ -1,11 +1,11 @@
---
name: gpt-image-2-apimart
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
---
# gpt-image-2 VectorEngine
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow
@@ -65,9 +65,9 @@ size=1024x1024
image=@reference.png
```
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2 currently returns synchronously; do not poll APIMart task endpoints.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints.
## Environment
@@ -9,6 +9,8 @@ const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
const defaultTimeoutMs = 1000000;
const preferredImageModel = 'gpt-image-2';
const fallbackImageModel = 'gpt-image-2-c';
const prompts = [
{
@@ -165,6 +167,25 @@ function extractBase64Images(payload) {
return values;
}
function decodeStrictBase64Image(raw) {
const normalized = String(raw || '').trim();
if (
!normalized ||
normalized.length % 4 !== 0 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
normalized,
)
) {
return null;
}
const bytes = Buffer.from(normalized, 'base64');
return bytes.length > 0 &&
bytes.toString('base64') === normalized &&
inferExtensionFromBytes(bytes)
? bytes
: null;
}
function inferExtensionFromContentType(contentType) {
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
if (normalized === 'image/png') {
@@ -192,7 +213,13 @@ function inferExtensionFromBytes(bytes) {
) {
return 'webp';
}
return 'png';
if (
bytes.subarray(0, 6).toString('ascii') === 'GIF87a' ||
bytes.subarray(0, 6).toString('ascii') === 'GIF89a'
) {
return 'gif';
}
return null;
}
async function fetchJson(url, options, timeoutMs) {
@@ -205,9 +232,20 @@ async function fetchJson(url, options, timeoutMs) {
});
const text = await response.text();
if (!response.ok) {
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
const error = new Error(
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
);
error.vectorEngineStatus = response.status;
error.vectorEngineBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
error.vectorEngineResponseParse = true;
error.vectorEngineBody = text;
throw error;
}
return JSON.parse(text);
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
@@ -218,6 +256,83 @@ async function fetchJson(url, options, timeoutMs) {
}
}
function shouldFallbackImageModel(error) {
const raw = `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
if (error?.vectorEngineResponseParse) {
return !containsContentRejection(raw);
}
const status = Number(error?.vectorEngineStatus || 0);
if (status === 408 || status >= 500) {
return true;
}
if (status === 429) {
return !containsContentRejection(raw);
}
const mentionsImageModel =
raw.includes('model') ||
raw.includes('模型') ||
raw.includes(preferredImageModel) ||
raw.includes(fallbackImageModel);
return (
[400, 404, 422].includes(status) &&
mentionsImageModel &&
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
raw,
)
);
}
function containsContentRejection(raw) {
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
raw,
);
}
async function requestImagePayload(env, entry) {
for (const model of [preferredImageModel, fallbackImageModel]) {
const requestBody = {
model,
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
};
try {
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
if (
extractImageUrls(payload)[0] ||
base64Image
) {
return payload;
}
const error = new Error(`VectorEngine returned no image for ${entry.id}`);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
throw error;
} catch (error) {
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
throw error;
}
console.warn(
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
);
}
}
throw new Error(`VectorEngine returned no image for ${entry.id}`);
}
async function downloadUrl(url, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
@@ -244,25 +359,7 @@ async function downloadUrl(url, timeoutMs) {
}
async function generateOne(env, entry, outDir) {
const requestBody = {
model: 'gpt-image-2',
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const payload = await requestImagePayload(env, entry);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
@@ -271,7 +368,10 @@ async function generateOne(env, entry, outDir) {
if (urls[0]) {
image = await downloadUrl(urls[0], env.timeoutMs);
} else if (b64Images[0]) {
const bytes = Buffer.from(b64Images[0], 'base64');
const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) {
throw new Error(`VectorEngine returned invalid base64 image for ${entry.id}`);
}
image = {
bytes,
extension: inferExtensionFromBytes(bytes),
@@ -304,8 +404,9 @@ if (dryRun) {
requests: selectedPrompts.map((entry) => ({
id: entry.id,
title: entry.title,
fallbackModel: fallbackImageModel,
body: {
model: 'gpt-image-2',
model: preferredImageModel,
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
@@ -14,6 +14,8 @@ const promptsPath = path.join(
);
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
const defaultTimeoutMs = 1000000;
const preferredImageModel = 'gpt-image-2';
const fallbackImageModel = 'gpt-image-2-c';
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
@@ -131,6 +133,25 @@ function extractBase64Images(payload) {
return values;
}
function decodeStrictBase64Image(raw) {
const normalized = String(raw || '').trim();
if (
!normalized ||
normalized.length % 4 !== 0 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
normalized,
)
) {
return null;
}
const bytes = Buffer.from(normalized, 'base64');
return bytes.length > 0 &&
bytes.toString('base64') === normalized &&
inferExtensionFromBytes(bytes)
? bytes
: null;
}
function inferExtensionFromContentType(contentType) {
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
if (normalized === 'image/png') {
@@ -158,7 +179,13 @@ function inferExtensionFromBytes(bytes) {
) {
return 'webp';
}
return 'png';
if (
bytes.subarray(0, 6).toString('ascii') === 'GIF87a' ||
bytes.subarray(0, 6).toString('ascii') === 'GIF89a'
) {
return 'gif';
}
return null;
}
async function fetchJson(url, options, timeoutMs) {
@@ -171,9 +198,20 @@ async function fetchJson(url, options, timeoutMs) {
});
const text = await response.text();
if (!response.ok) {
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
const error = new Error(
`VectorEngine ${response.status}: ${text.slice(0, 600)}`,
);
error.vectorEngineStatus = response.status;
error.vectorEngineBody = text;
throw error;
}
try {
return JSON.parse(text);
} catch (error) {
error.vectorEngineResponseParse = true;
error.vectorEngineBody = text;
throw error;
}
return JSON.parse(text);
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
@@ -184,6 +222,83 @@ async function fetchJson(url, options, timeoutMs) {
}
}
function shouldFallbackImageModel(error) {
const raw = `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase();
if (error?.vectorEngineResponseParse) {
return !containsContentRejection(raw);
}
const status = Number(error?.vectorEngineStatus || 0);
if (status === 408 || status >= 500) {
return true;
}
if (status === 429) {
return !containsContentRejection(raw);
}
const mentionsImageModel =
raw.includes('model') ||
raw.includes('模型') ||
raw.includes(preferredImageModel) ||
raw.includes(fallbackImageModel);
return (
[400, 404, 422].includes(status) &&
mentionsImageModel &&
/(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test(
raw,
)
);
}
function containsContentRejection(raw) {
return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test(
raw,
);
}
async function requestImagePayload(env, template) {
for (const model of [preferredImageModel, fallbackImageModel]) {
const requestBody = {
model,
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
};
try {
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const base64Image = decodeStrictBase64Image(extractBase64Images(payload)[0]);
if (
extractImageUrls(payload)[0] ||
base64Image
) {
return payload;
}
const error = new Error(`VectorEngine returned no image for ${template.id}`);
error.vectorEngineResponseParse = true;
error.vectorEngineBody = JSON.stringify(payload).slice(0, 600);
throw error;
} catch (error) {
if (model !== preferredImageModel || !shouldFallbackImageModel(error)) {
throw error;
}
console.warn(
`VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`,
);
}
}
throw new Error(`VectorEngine returned no image for ${template.id}`);
}
async function downloadUrl(url, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
@@ -210,25 +325,7 @@ async function downloadUrl(url, timeoutMs) {
}
async function generateOne(env, template, outDir) {
const requestBody = {
model: 'gpt-image-2',
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const payload = await requestImagePayload(env, template);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
@@ -237,7 +334,10 @@ async function generateOne(env, template, outDir) {
if (urls[0]) {
image = await downloadUrl(urls[0], env.timeoutMs);
} else if (b64Images[0]) {
const bytes = Buffer.from(b64Images[0], 'base64');
const bytes = decodeStrictBase64Image(b64Images[0]);
if (!bytes) {
throw new Error(`VectorEngine returned invalid base64 image for ${template.id}`);
}
image = {
bytes,
extension: inferExtensionFromBytes(bytes),
@@ -274,8 +374,9 @@ if (dryRun) {
requests: selectedTemplates.map((template) => ({
id: template.id,
title: template.title,
fallbackModel: fallbackImageModel,
body: {
model: 'gpt-image-2',
model: preferredImageModel,
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
+32 -5
View File
@@ -1,6 +1,6 @@
---
name: spacetimedb-cli
description: SpacetimeDB 2.5 CLI reference for Genarrative. Use for spacetime build, publish, generate, call, sql, logs, server management, local dev, explicit server targeting, version checks, and remote runtime verification.
description: SpacetimeDB 2.7 CLI reference for Genarrative. Use for spacetime build, publish, generate, call, sql, logs, server management, local dev, explicit server targeting, version checks, and remote runtime verification.
---
# SpacetimeDB CLI
@@ -61,13 +61,38 @@ spacetime describe my-db table users --server http://127.0.0.1:3101 --json
# Reducer/procedure calls. Arguments are positional JSON values.
spacetime call --server http://127.0.0.1:3101 my-db my_reducer '"value"' '123'
# 2.5 accepts hex strings for Identity arguments without full JSON tuple syntax.
# 2.5+ accepts hex strings for Identity arguments without full JSON tuple syntax.
spacetime call --server http://127.0.0.1:3101 my-db reducer_needing_identity 0xabc123...
# Subscribe from CLI
spacetime subscribe my-db "SELECT * FROM users" --num-updates 10 --server http://127.0.0.1:3101
```
## Standalone MCP Endpoint (2.7)
SpacetimeDB 2.7 standalone exposes an authenticated JSON-RPC MCP endpoint at
`POST /v1/database/{name_or_identity}/mcp`. It advertises `ping`, `get_schema`,
`sql`, and `call`. The SQL and reducer tools execute with the bearer token's
identity, so keep routine smoke checks read-only.
```bash
curl -fsS \
-H "Authorization: Bearer ${SPACETIME_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"genarrative-smoke","version":"1.0.0"}}}' \
http://127.0.0.1:3101/v1/database/my-db/mcp
curl -fsS \
-H "Authorization: Bearer ${SPACETIME_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{"message":"genarrative"}}}' \
http://127.0.0.1:3101/v1/database/my-db/mcp
```
For repository upgrade validation, also call `tools/list` and the read-only
`get_schema` tool against an isolated local database. Do not use `sql` or `call`
for writes unless that mutation is explicitly in scope.
## Server & Auth
```bash
@@ -102,7 +127,7 @@ curl -fsS http://127.0.0.1:3101/v1/ping
| Flag | Description |
|------|-------------|
| `--server`, `-s` | Target server nickname, host, or URL |
| `--yes`, `-y` | Non-interactive prompt skipping; in 2.5 prefer scoped values |
| `--yes`, `-y` | Non-interactive prompt skipping; in 2.6+ use scoped values |
| `--delete-data`, `-c` | Publish data policy: `always`, `on-conflict`, or `never` |
| `--module-path`, `-p` | Module project path |
| `--bin-path`, `-b` | Publish/generate from compiled wasm |
@@ -146,6 +171,8 @@ pid="$(systemctl show spacetimedb.service -p MainPID --value)"
## Notes
- Procedure calls are stable in 2.5; module HTTP handlers/webhooks, unstable view features, and RLS remain behind unstable gates per release notes.
- 2.5 fixes `publish --delete-data` config fallback so `spacetime.json` can provide the database name.
- Procedure calls remain stable in 2.7; module HTTP handlers/webhooks and RLS capabilities still require their documented gates.
- 2.5 fixed `publish --delete-data` config fallback; 2.6 kept that behavior and improved CLI binary distribution; 2.7 adds `spacetime sql --format json` and database `lock` / `unlock`.
- The official 2.7.0 Linux release archives and container image currently use the `v2.7.0-hotfix3` asset tag while binaries report `2.7.0`; keep the asset tag distinct from the runtime version check.
- Do not assume `spacetime version install 2.7.0` selected hotfix3: stale updater metadata can install bare-tag commit `a08663c7...`. For the current release, verify CLI commit `d220349a...` and use the official hotfix3 archive or repository provision flow when it differs.
- Genarrative scripts should pass `--server` or `--server-url` explicitly instead of relying on CLI defaults.
+22 -8
View File
@@ -1,6 +1,6 @@
---
name: spacetimedb-concepts
description: Understand SpacetimeDB 2.5 architecture, reducer/procedure/table/view semantics, schema evolution, subscriptions, identity, and Genarrative-specific backend boundaries. Use when designing or reviewing SpacetimeDB-backed features.
description: Understand SpacetimeDB 2.7 architecture, reducer/procedure/table/view semantics, schema evolution, subscriptions, identity, and Genarrative-specific backend boundaries. Use when designing or reviewing SpacetimeDB-backed features.
---
# SpacetimeDB Core Concepts
@@ -20,7 +20,7 @@ SpacetimeDB is a relational database that also executes application logic in upl
1. **Reducers are transactional**: they do not return data to callers. Read through subscriptions, read models, views, or BFF endpoints.
2. **Reducers are deterministic**: no filesystem, network, wall-clock, or external RNG. Use `ctx.timestamp`, `ctx.rng()` / `ctx.random()`, and tables.
3. **Procedures are stable in 2.5**: they can use explicit transactions and outgoing HTTP via `ctx.http`.
3. **Procedures are stable in 2.7**: they can use explicit transactions and outgoing HTTP via `ctx.http`.
4. **Identity comes from context**: use `ctx.sender()` or language equivalent for authorization. Never trust identity passed as an argument.
5. **Auto-increment IDs are not ordering guarantees**: gaps are normal. Use timestamps or explicit sequence columns for ordering.
6. **Schema changes need migration discipline**: existing Genarrative table fields must be appended with defaults; update migration code, table catalog, generated bindings, and run `npm run check:spacetime-schema`.
@@ -44,25 +44,25 @@ Reducers are deterministic transactional functions. They are the primary client-
## Procedures
Procedures are stable in 2.5. They can be scheduled, can open explicit transactions with `with_tx` / `try_with_tx`, and can use outgoing HTTP (`ctx.http`).
Procedures are stable in 2.7. They can be scheduled, can open explicit transactions with `with_tx` / `try_with_tx`, and can use outgoing HTTP (`ctx.http`).
Genarrative default: keep external provider protocols in `platform-*` and orchestration in `api-server` unless a task explicitly moves a workflow into a module procedure.
Module HTTP handlers/webhooks, unstable view features, and RLS `client_visibility_filter` remain gated behind unstable according to the 2.5 release notes.
Module HTTP handlers/webhooks and RLS `client_visibility_filter` remain subject to their documented gates in 2.7.
## Views
Views expose computed read-only data. In 2.4.1 Rust and TypeScript gained primary key support for procedural views; in 2.5 C# gained the same. Clients can receive `OnUpdate` events when subscribed to such views with primary keys. Ensure the view never returns duplicate primary keys, because that can fail view refresh and roll back the triggering transaction.
Views expose computed read-only data. SpacetimeDB 2.7 supports primary keys on procedural views in Rust, TypeScript, C#, and C++. Clients can receive update events when subscribed to such views with primary keys. Ensure the view never returns duplicate primary keys, because that can fail view refresh and roll back the triggering transaction.
## Event Tables
Event tables broadcast reducer/procedure-specific facts to subscribers and must be subscribed explicitly. They are excluded from `subscribe_to_all_tables()`.
2.5 adds broader layout-altering automigrations for event tables, including column removal, reordering, and type changes that regular tables reject. This relaxed migration behavior is for event-only tables, not persistent tables.
Since 2.6, event tables support broader layout-altering automigrations, including column removal, reordering, and type changes that regular tables reject. This relaxed migration behavior is for event-only tables, not persistent tables.
Event-table primary keys and constraints are transaction-scoped. They can reject duplicate event rows within one transaction, but event rows are not retained in client cache, so clients observe event tables through insert callbacks only. Do not design Genarrative event tables around `OnUpdate` / `on_update` / `onUpdate`; use a persistent table or a primary-keyed procedural view when update callbacks are required.
Official 2.4.1/2.5 release notes document primary-key-backed update callbacks for procedural views, not event tables.
Official 2.4.1 through 2.7 release notes document primary-key-backed update callbacks for procedural views, not event tables.
## Subscriptions
@@ -78,7 +78,18 @@ Best practices:
- Avoid overlapping queries that duplicate row delivery.
- Use indexes for subscribed filters.
## 2.2.0 to 2.5.0 Delta
## Standalone MCP
SpacetimeDB 2.7 standalone exposes `POST /v1/database/{name_or_identity}/mcp`
using MCP JSON-RPC protocol `2025-06-18`. Its tools are `ping`, `get_schema`,
`sql`, and `call`; SQL and reducer calls run with the authenticated caller's
identity. In Genarrative this is an operator/developer integration surface, not
a replacement for `api-server` BFF routes, `spacetime-client` facades, or public
read models. Upgrade smoke should use an isolated local database and restrict
itself to `initialize`, `tools/list`, `ping`, and `get_schema` unless writes are
explicitly intended.
## 2.2.0 to 2.7.0 Delta
Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
@@ -87,6 +98,9 @@ Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
- **2.4.0**: unstable module HTTP handlers/webhooks, faster synchronous WASM reducer runtime, commitlog resume truncation fix for silent data loss risk, better commitlog decode context, V8 heap metrics for procedure workers, JS execution-time billing regression reverted.
- **2.4.1**: Rust and TypeScript procedural views can declare primary keys, enabling `OnUpdate` events for subscribed views; fixed index schema from ST tables.
- **2.5.0**: procedures are stable, C# procedural views gain primary keys, event tables allow broader layout-altering automigrations, BTreeSet storage makes row insertion deterministic and avoids accidentally quadratic bulk insert behavior, `wasm_memory_bytes` billing metric semantics changed, template version constraints unified, `publish --delete-data` config fallback fixed, CLI `call` accepts hex Identity arguments.
- **2.6.0**: procedural-view primary keys are available across Rust, TypeScript, and C#, commitlog gains `max_segment_size` / `write_buffer_size` / `preallocate_segments`, the default write buffer increases for throughput, event-table automigrations improve, and CLI binary distribution expands.
- **2.6.1**: procedure contexts again receive the caller `Identity` and `ConnectionId`; generated TypeScript `Option<T>` fields use optional keys; `spacetime init --template` lists available templates when no template argument is supplied.
- **2.7.0**: existing tables can add unique or primary-key constraints when current data satisfies them; standalone exposes an authenticated database MCP endpoint; Rust adds context-capability and table-accessor traits; `spacetime sql --format json` and database locking are available; view cleanup, backing-table migration, connection metrics, and memory metrics improve. Official current release assets use the `v2.7.0-hotfix3` tag while binaries report `2.7.0`.
## Debugging Checklist
+5 -5
View File
@@ -1,6 +1,6 @@
---
name: spacetimedb-rust
description: Develop SpacetimeDB 2.5 server modules in Rust for Genarrative. Use when writing or reviewing tables, reducers, procedures, views, migrations, row mappers, schema changes, and module logic.
description: Develop SpacetimeDB 2.7 server modules in Rust for Genarrative. Use when writing or reviewing tables, reducers, procedures, views, migrations, row mappers, schema changes, and module logic.
---
# SpacetimeDB Rust Module Development
@@ -28,7 +28,7 @@ ctx.db.player.find(id) // Use ctx.db.player().id().find(&id)
ctx.sender // Use ctx.sender()
ctx.db.user().name().update(..) // Update by primary key only
spacetimedb = { version = "...", features = ["unstable"] } // Not needed for procedures in 2.5
spacetimedb = { version = "...", features = ["unstable"] } // Not needed for procedures since 2.5
```
## Required Patterns
@@ -181,11 +181,11 @@ fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
Event tables must be subscribed explicitly and are excluded from `subscribe_to_all_tables()`.
In 2.5, event tables support broader layout-altering automigrations than regular tables, including column removal, reordering, and type changes. This relaxed migration policy does not apply to persistent tables.
Since 2.6, event tables support broader layout-altering automigrations than regular tables, including column removal, reordering, and type changes. This relaxed migration policy does not apply to persistent tables.
Event-table primary keys and constraints are enforced only within the current transaction. They do not make event rows persistent, and client SDKs expose event tables as insert-only event streams. Do not rely on `OnUpdate` / `on_update` / `onUpdate` for event tables; use a persistent table or a primary-keyed procedural view when update callbacks are required.
Official 2.4.1/2.5 release notes tie primary-key-backed update callbacks to procedural views, not event tables.
Official 2.4.1 through 2.7 release notes tie primary-key-backed update callbacks to procedural views, not event tables.
## Views
@@ -228,7 +228,7 @@ For scheduled reducers, check `ctx.sender_auth().is_internal()` when the reducer
## Procedures
Procedures are stable in 2.5 and no longer require the `unstable` feature.
Procedures remain stable in 2.7 and no longer require the `unstable` feature.
```rust
use spacetimedb::{procedure, ProcedureContext};
+2
View File
@@ -1,5 +1,7 @@
# 微信小程序 web-view 登录配置。
# 留空时不覆盖已有微信网页 OAuth 配置;正式联调时再填小程序 AppID / AppSecret。
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=false
WECHAT_MINI_PROGRAM_APP_ID=""
WECHAT_MINI_PROGRAM_APP_SECRET=""
WECHAT_JS_CODE_SESSION_ENDPOINT=""
+30 -18
View File
@@ -1,11 +1,12 @@
# Server-side OpenAI-compatible LLM endpoint base URL.
LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
LLM_BASE_URL="https://api.vectorengine.cn/v1"
# Server-side API key used by the local Vite proxy.
# Recommended: set `LLM_API_KEY` or `ARK_API_KEY`.
# Recommended: set `LLM_API_KEY` locally, or use `VECTOR_ENGINE_API_KEY`
# through the Rust api-server proxy.
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
# but it should not be relied on by browser code.
LLM_API_KEY="YOUR_API_KEY"
LLM_API_KEY=""
# Optional frontend override for the local proxy path.
VITE_LLM_PROXY_BASE_URL="/api/llm"
@@ -103,6 +104,8 @@ WECHAT_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/sns/oauth2/access_token"
WECHAT_USER_INFO_ENDPOINT="https://api.weixin.qq.com/sns/userinfo"
WECHAT_JS_CODE_SESSION_ENDPOINT="https://api.weixin.qq.com/sns/jscode2session"
WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/cgi-bin/stable_token"
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_QUERY_ORDER_ENDPOINT="https://api.weixin.qq.com/xpay/query_order"
WECHAT_MINI_PROGRAM_VIRTUAL_PAYMENT_NOTIFY_PROVIDE_GOODS_ENDPOINT="https://api.weixin.qq.com/xpay/notify_provide_goods"
WECHAT_PHONE_NUMBER_ENDPOINT="https://api.weixin.qq.com/wxa/business/getuserphonenumber"
WECHAT_STATE_TTL_MINUTES="15"
WECHAT_MOCK_USER_ID="wx-mock-user"
@@ -111,27 +114,19 @@ WECHAT_MOCK_DISPLAY_NAME="微信旅人"
WECHAT_MOCK_AVATAR_URL=""
WECHAT_MINIPROGRAM_MESSAGE_TOKEN=""
WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=""
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_ENABLED="true"
WECHAT_MINIPROGRAM_GENERATION_RESULT_TEMPLATE_ID="m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU"
WECHAT_MINIPROGRAM_SUBSCRIBE_MESSAGE_STATE="formal"
# Model name for chat completions.
VITE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
# Optional: enable upstream web search for RPG story text generation.
RPG_LLM_WEB_SEARCH_ENABLED="true"
VITE_LLM_MODEL="gpt-5.4-mini"
GENARRATIVE_LLM_PROVIDER="openai-compatible"
GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
GENARRATIVE_LLM_API_KEY=""
GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
# Server-side DashScope endpoint and API key used by the local scene-image proxy.
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
# APIMart Responses config for creative-agent text/multimodal understanding.
APIMART_BASE_URL="https://api.apimart.ai/v1"
APIMART_API_KEY="YOUR_APIMART_API_KEY"
APIMART_IMAGE_REQUEST_TIMEOUT_MS="180000"
# VectorEngine GPT-image-2 / Gemini image generation config.
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.ai"
# VectorEngine LLM and GPT-image-2 / Gemini image generation config.
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
VECTOR_ENGINE_API_KEY=""
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
@@ -148,6 +143,19 @@ ALIYUN_OSS_POST_EXPIRE_SECONDS="600"
ALIYUN_OSS_POST_MAX_SIZE_BYTES="20971520"
ALIYUN_OSS_SUCCESS_ACTION_STATUS="200"
# BgFilter 受限资源 worker。父 api-server / external-generation-worker 与唯一的
# `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 进程必须使用同一个内部 Token。
# `npm run dev` 与 `npm run dev:api-server` 都会自动带起并验活唯一 worker,不要再开第二个终端重复启动。
# 只有需要脱离父 API 单独验证 worker 时才运行 `npm run dev:bgfilter-worker`;不要让 `all` 角色兼任它。
GENARRATIVE_BGFILTER_WORKER_HOST="127.0.0.1"
GENARRATIVE_BGFILTER_WORKER_PORT="8083"
GENARRATIVE_BGFILTER_WORKER_BASE_URL="http://127.0.0.1:8083"
GENARRATIVE_BGFILTER_INTERNAL_TOKEN="CHANGE_ME_FOR_LOCAL"
GENARRATIVE_BGFILTER_WORKER_CONCURRENCY="16"
GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS="5000"
GENARRATIVE_BGFILTER_WORKER_MAX_REQUESTS="2048"
GENARRATIVE_BGFILTER_WORKER_CONNECT_TIMEOUT_MS="2000"
# SpacetimeDB 数据目录备份到 OSS。备份 bucket 可与资源 bucket 分离;未设置时脚本回退使用 ALIYUN_OSS_BUCKET。
GENARRATIVE_DATABASE_BACKUP_DATA_DIR=""
GENARRATIVE_DATABASE_BACKUP_WORK_DIR=""
@@ -199,6 +207,10 @@ VITE_LLM_DEBUG_LOG="false"
# Set to "true" to expose local diagnostic panels, or "false" to hide them.
VITE_DEBUG_MODE=""
# Optional: show the image editor right-side Agent entry at runtime.
# This is read by api-server and exposed through /api/runtime/frontend-config.
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false"
# Optional: official VikingDB credentials for regenerating build-tag similarities
# with the Python embedding script. The script auto-loads `.env.local` and uses
# the fixed `bge-large-zh` embedding model.
+4
View File
@@ -29,6 +29,7 @@ GENARRATIVE_LLM_PROVIDER="ark"
GENARRATIVE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
GENARRATIVE_LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
GENARRATIVE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
GENARRATIVE_EDITOR_BGFILTER_BASE_URL="https://u1082648-b442-cd409e05.westx.seetacloud.com:8443"
APIMART_BASE_URL="https://api.apimart.ai/v1"
APIMART_API_KEY=""
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
@@ -36,12 +37,15 @@ DASHSCOPE_SCENE_IMAGE_MODEL="wan2.2-t2i-flash"
DASHSCOPE_REFERENCE_IMAGE_MODEL="qwen-image-2.0"
DASHSCOPE_COVER_IMAGE_MODEL="wan2.2-t2i-flash"
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
# 启用服务端大模型调试日志(记录所有输入输出)
LLM_DEBUG_LOG="true"
# 注意:不要在客户端启用调试日志,避免敏感数据泄露
# VITE_LLM_DEBUG_LOG="false"
GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR=true
ALIYUN_OSS_BUCKET="xushi-dev"
ALIYUN_OSS_REGION="oss-cn-beijing"
ALIYUN_OSS_ENDPOINT="oss-cn-beijing.aliyuncs.com"
+338 -8
View File
@@ -24,6 +24,104 @@ module.exports = {
'no-console': 'off',
},
},
{
files: ['**/scripts/**/*.{ts,js,mjs,cjs}'],
rules: {
'no-console': 'off',
},
},
{
files: ['**/*.test.{ts,tsx,js,mjs,cjs}'],
rules: {
'no-console': 'off',
'unused-imports/no-unused-vars': 'off',
},
},
{
files: ['miniprogram/**/*.js'],
globals: {
App: 'readonly',
},
},
{
files: [
'apps/admin-web/src/pages/*.tsx',
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/components/platform-entry/PlatformEntryFlowShellImpl.tsx'],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: [
'src/active-main.tsx',
'src/ActiveApp.tsx',
'src/AuthenticatedApp.tsx',
'src/hooks/useGameSettings.ts',
'src/routing/activeApp*.ts',
'src/routing/activeApp*.tsx',
'src/components/auth/**/*.{ts,tsx}',
'src/components/common/**/*.{ts,tsx}',
'src/components/creation-home/**/*.{ts,tsx}',
'src/components/image-editor/**/*.{ts,tsx}',
'src/components/project/**/*.{ts,tsx}',
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
'src/components/platform-entry/PlatformProfileModalShell.tsx',
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
'src/components/platform-entry/PlatformProfileRechargeModal.tsx',
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
'src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx',
'src/components/platform-entry/PlatformRechargePaymentStatusDialogs.tsx',
'src/components/platform-entry/platformActiveProfileModel.ts',
'src/components/platform-entry/platformEntryActiveTypes.ts',
'src/components/platform-entry/platformProfileFundsModel.ts',
'src/components/platform-entry/platformProfileHostClipboard.ts',
'src/components/platform-entry/usePlatformProfileCenterController.ts',
'src/services/image-editor/**/*.{ts,tsx}',
'src/services/platform-entry/**/*.{ts,tsx}',
],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'**/components/rpg-entry/**',
'**/components/*-creation/**',
'**/components/*-result/**',
'**/components/*-runtime/**',
'**/components/creation-agent/**',
'**/components/creative-agent/**',
'**/components/custom-world-*/**',
'**/components/unified-creation/**',
'**/services/rpg-entry/**',
'**/services/rpg-runtime/**',
'**/services/*-creation/**',
'**/services/*-runtime/**',
'**/services/*-works/**',
'**/services/creation-agent/**',
'**/services/creative-agent/**',
'**/services/puzzle-*/**',
'**/services/storyEngine/**',
],
message: '现役前端不得重新导入已退役的创作模板模块。',
},
],
},
],
},
},
{
files: ['src/components/game-canvas/**/*.tsx'],
rules: {
@@ -37,12 +135,6 @@ module.exports = {
'simple-import-sort/exports': 'off',
},
},
{
files: ['src/components/match3d-runtime/Match3DPhysicsBoard.tsx'],
rules: {
'react-refresh/only-export-components': 'off',
},
},
],
plugins: [
'@typescript-eslint',
@@ -61,7 +153,242 @@ module.exports = {
'dist',
'dist_check',
'dist_check_monster_position',
'coverage',
'build',
'output',
'retired/**',
'node_modules',
'server-rs/target',
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'target',
'src/main.tsx',
'src/App.tsx',
'src/routing/appPageRoutes.ts',
'src/routing/appPageRoutes.test.ts',
'src/routing/appRoutes.tsx',
'src/routing/appRoutes.test.ts',
'src/services/appTitle.ts',
'src/services/appTitle.test.ts',
'src/components/CharacterAnimator.tsx',
'src/components/*.ts',
'src/components/*.tsx',
'!src/components/ResolvedAssetAudio.tsx',
'!src/components/ResolvedAssetImage.tsx',
'!src/components/ResolvedAssetVideo.tsx',
'src/hooks/*.ts',
'src/hooks/*.tsx',
'!src/hooks/useGameSettings.ts',
'!src/hooks/useHostNavigationCanGoBack.ts',
'!src/hooks/useHostNavigationCanGoBack.test.tsx',
'!src/hooks/useResolvedAssetReadUrl.ts',
'!src/hooks/useResolvedAssetReadUrl.test.tsx',
'src/persistence/*.ts',
'src/persistence/*.tsx',
'!src/persistence/gameSettingsStorage.ts',
'!src/persistence/gameSettingsStorage.test.ts',
'!src/persistence/storage.ts',
'!src/persistence/storage.test.ts',
'src/routing/*.ts',
'src/routing/*.tsx',
'!src/routing/activeAppPageRoutes.ts',
'!src/routing/activeAppPageRoutes.test.ts',
'!src/routing/activeAppRoutes.tsx',
'!src/routing/activeAppRoutes.test.ts',
'src/services/*.ts',
'src/services/*.tsx',
'!src/services/activeAppTitle.ts',
'!src/services/activeAppTitle.test.ts',
'!src/services/apiClient.ts',
'!src/services/apiClient.test.ts',
'!src/services/assetReadUrlService.ts',
'!src/services/assetReadUrlService.test.ts',
'!src/services/authService.ts',
'!src/services/authService.test.ts',
'!src/services/clipboard.ts',
'!src/services/clipboard.test.ts',
'!src/services/frontendRuntimeConfigService.ts',
'!src/services/frontendRuntimeConfigService.test.ts',
'!src/services/sseStream.ts',
'!src/services/sseStream.test.ts',
'src/AdventurePanel.tsx',
'src/AdventureEntityModal.tsx',
'src/App.tsx',
'src/App.test.tsx',
'src/CharacterCreation.tsx',
'src/RpgRuntimeApp.tsx',
'src/routing/appPageRoutes.ts',
'src/routing/appPageRoutes.test.ts',
'src/routing/appRoutes.tsx',
'src/routing/appRoutes.test.ts',
'src/*PlaygroundApp.tsx',
'src/ChildMotionDemoApp.tsx',
'src/Match3DPlaygroundApp.tsx',
'src/components/child-motion-demo/**',
'src/components/asset-studio/**',
'src/components/bark-battle-creation/**',
'src/components/big-fish-creation/**',
'src/components/big-fish-result/**',
'src/components/big-fish-runtime/**',
'src/components/creation-agent/**',
'src/components/creative-agent/**',
'src/components/custom-world-agent/**',
'src/components/custom-world-home/**',
'src/components/edutainment-creation/**',
'src/components/edutainment-result/**',
'src/components/edutainment-runtime/**',
'src/components/game-canvas/**',
'src/components/jump-hop-result/**',
'src/components/jump-hop-runtime/**',
'src/components/match3d-result/**',
'src/components/match3d-runtime/**',
'src/components/puzzle-clear-creation/**',
'src/components/puzzle-clear-result/**',
'src/components/puzzle-clear-runtime/**',
'src/components/puzzle-gallery/**',
'src/components/puzzle-result/**',
'src/components/puzzle-runtime/**',
'src/components/rpg-creation-asset-studio/**',
'src/components/rpg-creation-result/**',
'src/components/rpg-runtime-panels/**',
'src/components/square-hole-creation/**',
'src/components/square-hole-result/**',
'src/components/square-hole-runtime/**',
'src/components/unified-creation/**',
'src/components/visual-novel-creation/**',
'src/components/visual-novel-result/**',
'src/components/visual-novel-runtime/**',
'src/components/wooden-fish-result/**',
'src/components/wooden-fish-runtime/**',
'src/components/common/PublishShare*',
'src/components/common/publishShare*',
'src/components/platform-entry/PlatformEntryCreation*',
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
'src/components/platform-entry/platformEntryTypes.ts',
'src/components/platform-entry/PlatformEntryHome*',
'src/components/platform-entry/PlatformEntryWorld*',
'src/components/platform-entry/PlatformMobileHome*',
'src/components/platform-entry/PlatformWork*',
'src/components/platform-entry/PlatformDraft*',
'src/components/platform-entry/PlatformTask*',
'src/components/platform-entry/PlatformError*',
'src/components/platform-entry/barkBattle*',
'src/components/platform-entry/platformCreation*',
'src/components/platform-entry/platformDialog*',
'src/components/platform-entry/platformDraft*',
'src/components/platform-entry/platformEdutainment*',
'src/components/platform-entry/platformEntryCreation*',
'src/components/platform-entry/platformExternal*',
'src/components/platform-entry/platformGeneration*',
'src/components/platform-entry/platformHost*',
'src/components/platform-entry/platformMiniGame*',
'src/components/platform-entry/platformPlayed*',
'src/components/platform-entry/platformPublic*',
'src/components/platform-entry/platformPuzzle*',
'src/components/platform-entry/platformRecommend*',
'src/components/platform-entry/platformRpg*',
'src/components/platform-entry/platformSelection*',
'src/components/platform-entry/puzzleDraft*',
'src/components/platform-entry/usePlatformCreation*',
'src/components/platform-entry/usePlatformEntry*',
'src/components/platform-entry/PlatformEntryFlowShellImpl/**',
'src/components/platform-entry/platformMatch3DRuntimeProfile*',
'src/components/unified-creation/workspaces/JumpHopCreationWorkspace*',
'src/components/unified-creation/workspaces/Match3DCreationWorkspace*',
'src/components/rpg-creation-editor/**',
'src/components/rpg-entry/**',
'src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx',
'src/components/custom-world-home/CustomWorldCreationHub.test.tsx',
'src/components/custom-world-home/CustomWorldCreationHub.testAdapter.tsx',
'src/components/custom-world-home/creationWorkShelf.test.ts',
'src/components/rpg-runtime-shell/**',
'src/hooks/rpg-runtime-story/**',
'src/hooks/rpg-session/**',
'src/hooks/combat/**',
'src/hooks/useCombatFlow.ts',
'src/hooks/useStoryOptions.ts',
'src/prompts/customWorldPrompts.ts',
'src/services/ai.ts',
'src/services/appTitle.ts',
'src/services/appTitle.test.ts',
'src/services/miniGameDraftGenerationProgress.ts',
'src/services/creationEntryConfigService.ts',
'src/services/creationUrlState*',
'src/services/customWorld*',
'src/services/input-devices/**',
'src/services/publicWorkCode.ts',
'src/services/runtimeGuestAuth.ts',
'src/services/runtimeRequest*',
'src/services/runtimeAudioFeedback.ts',
'src/services/useMocapInput*',
'src/services/wechatMiniProgramSubscribe*',
'src/services/bark-battle-creation/**',
'src/services/bark-battle-runtime/**',
'src/services/big-fish-creation/**',
'src/services/big-fish-gallery/**',
'src/services/big-fish-runtime/**',
'src/services/big-fish-works/**',
'src/services/creation-agent/**',
'src/services/creation-audio/**',
'src/services/creative-agent/**',
'src/services/edutainment-baby-drawing/**',
'src/services/edutainment-baby-object/**',
'src/services/child-motion-demo/**',
'src/services/jump-hop/**',
'src/services/match3d-creation/**',
'src/services/match3d-runtime/**',
'src/services/match3d-works/**',
'src/services/match3dGeneratedModelCache*',
'src/services/match3dSpritesheetParser*',
'src/services/puzzle-agent/**',
'src/services/puzzle-gallery/**',
'src/services/puzzle-onboarding/**',
'src/services/puzzle-runtime/**',
'src/services/puzzle-works/**',
'src/services/rpg-creation/**',
'src/services/rpg-entry/**',
'src/services/rpg-runtime/**',
'src/services/square-hole-creation/**',
'src/services/square-hole-runtime/**',
'src/services/square-hole-works/**',
'src/services/storyEngine/**',
'src/services/visual-novel-creation/**',
'src/services/visual-novel-runtime/**',
'src/services/visual-novel-works/**',
'src/services/wooden-fish/**',
'src/types.ts',
'src/types/**',
'src/uiAssets.ts',
'scripts/loadtest/**',
'packages/shared/src/contracts/jumpHop.ts',
'packages/shared/src/contracts/match3dAgent.ts',
'packages/shared/src/contracts/match3dRuntime.ts',
'packages/shared/src/contracts/match3dWorks.ts',
'packages/shared/src/contracts/barkBattle*',
'packages/shared/src/contracts/bigFish*',
'packages/shared/src/contracts/creationAgent*',
'packages/shared/src/contracts/creationAudio*',
'packages/shared/src/contracts/creativeAgent*',
'packages/shared/src/contracts/customWorld*',
'packages/shared/src/contracts/edutainment*',
'packages/shared/src/contracts/playTypes*',
'packages/shared/src/contracts/publicWork*',
'packages/shared/src/contracts/puzzle*',
'packages/shared/src/contracts/rpg*',
'packages/shared/src/contracts/squareHole*',
'packages/shared/src/contracts/story*',
'packages/shared/src/contracts/visualNovel*',
'packages/shared/src/contracts/woodenFish*',
'scripts/export-match3d-resource-pipeline*',
'scripts/generate-child-motion-demo-assets.mjs',
'src/services/puzzle-clear/**',
'src/games/**',
'src/data/**',
'src/prompts/**',
'apps/admin-web/src/pages/AdminCreationEntrySwitchPage*',
'apps/admin-web/src/pages/AdminWorkVisibilityPage*',
'src/services/recommendedRuntimeGuestLaunch.test.ts',
'src/data/sceneEncounterPreviews.ts',
'public/Icons',
'media',
'.codex-logs',
@@ -78,7 +405,10 @@ module.exports = {
rules: {
'@typescript-eslint/no-var-requires': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-refresh/only-export-components': ['error', {allowConstantExport: true}],
'react-refresh/only-export-components': [
'error',
{ allowConstantExport: true },
],
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'@typescript-eslint/no-unused-vars': 'off',
@@ -91,7 +421,7 @@ module.exports = {
},
],
'no-constant-condition': 'error',
'no-console': ['error', {allow: ['warn', 'error']}],
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-useless-escape': 'error',
'prefer-const': 'error',
},
+193
View File
@@ -0,0 +1,193 @@
name: Project CI
on:
push:
branches:
- master
- codex/ai-game-creator-app
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
CI: 'true'
CARGO_INCREMENTAL: '0'
CARGO_HTTP_MULTIPLEXING: 'false'
CARGO_NET_RETRY: '10'
CARGO_TERM_COLOR: always
NPM_CONFIG_AUDIT: 'false'
NPM_CONFIG_FETCH_RETRIES: '10'
NPM_CONFIG_FETCH_RETRY_FACTOR: '2'
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: '60000'
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: '2000'
NPM_CONFIG_FUND: 'false'
NPM_CONFIG_PREFER_OFFLINE: 'true'
RUSTUP_AUTO_INSTALL: '0'
RUSTC_WRAPPER: ''
CARGO_BUILD_RUSTC_WRAPPER: ''
jobs:
repository-checks:
name: Repository checks
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
- name: Resolve comparison base
shell: bash
run: |
set -euo pipefail
base_ref="$(node -e '
const fs = require("node:fs");
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
')"
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
echo "comparison base commit is unavailable: ${base_ref}" >&2
exit 1
}
else
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
fi
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
exit 1
fi
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
- name: Install npm dependencies
run: npm ci
- name: Run repository lint gates
run: npm run lint
- name: Build web applications
run: npm run build
- name: Validate content data
run: npm run check:content
- name: Check committed whitespace
shell: bash
run: |
set -euo pipefail
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
test -n "${base_ref}"
git cat-file -e "${base_ref}^{commit}"
git diff --check "${base_ref}"...HEAD
frontend-tests:
name: Frontend tests
runs-on: genarrative-ci
steps:
- name: Checkout source from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
- name: Install npm dependencies
run: npm ci
- name: Run frontend and script tests
run: npm run test
- name: Run BgFilter worker smoke harness tests
run: npm run bgfilter-worker:smoke-test
- name: Validate production health patrol behavior
run: npm run check:production-health-patrol
- name: Validate production API release behavior
run: npm run check:production-api-release
- name: Validate production API deploy behavior
run: npm run check:production-api-deploy
backend-tests:
name: Backend tests
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
- name: Resolve comparison base
shell: bash
run: |
set -euo pipefail
base_ref="$(node -e '
const fs = require("node:fs");
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
')"
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
echo "comparison base commit is unavailable: ${base_ref}" >&2
exit 1
}
else
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
fi
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
exit 1
fi
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
- name: Install npm dependencies
run: npm ci
- name: Check server-rs boundaries
run: npm run check:server-rs-ddd
- name: Run server-rs workspace tests
run: cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml
- name: Check api-server targets
run: cargo check --locked -p api-server --all-targets --manifest-path server-rs/Cargo.toml
- name: Check SpacetimeDB module
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
native-shell-tests:
name: Native shell tests
runs-on: genarrative-ci
steps:
- name: Checkout full history from Gitea
env:
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
run: genarrative-gitea-checkout
- name: Validate preinstalled CI job image and sandbox
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
- name: Install npm dependencies
run: npm ci
- name: Run native shell gates
run: npm run check:native-shells
- name: Ensure native lockfile is unchanged
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock
+2
View File
@@ -42,6 +42,7 @@ temp*build*/
/.app/
/target/
/logs
/.claude/settings.local.json
/.codegraph/
/.playwright-cli/
**/.playwright-cli/
@@ -50,6 +51,7 @@ temp*build*/
.worktrees/
.rag/
.env.secrets.local
nohup.out
spacetime.local.json
deploy/container/api-server.env
deploy/container/worker-smoke/
@@ -1,8 +1,8 @@
---
name: genarrative-dev-stack-port-routing
short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。
description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
version: 1.0.0
description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:bgfilter-worker / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、Rust 进程环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
version: 1.1.0
author: Hermes Agent
license: MIT
metadata:
@@ -13,7 +13,7 @@ metadata:
# Genarrative 本地 dev 启动端口与代理目标串联流程
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev``dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递。
用于维护 Genarrative 本地开发栈启动脚本,重点覆盖 `npm run dev``dev:*` 单模块命令的端口检查、端口漂移和后续流程目标传递。
## 适用场景
@@ -31,40 +31,44 @@ metadata:
2. Rust `api-server``8082`,健康检查为 `http://127.0.0.1:<api-port>/healthz`
3. SpacetimeDB standalone`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`
4. 后台 Vite`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`
5. 独立 BgFilter worker`8083`,就绪检查为 `http://127.0.0.1:<bgfilter-worker-port>/readyz`
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE``--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start``api = start + 1``spacetime = start + 2``adminWeb = start + 3`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199``10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有端口探测漂移和复用逻辑,不读系统级注册表。
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE``--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start``api = start + 1``spacetime = start + 2``adminWeb = start + 3``bgfilterWorker = start + 4`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199``10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有统一端口探测漂移逻辑,不读系统级注册表。
## 实现入口
- `package.json`
- `dev`:执行 `node scripts/dev.mjs`,启动完整四模块
- `dev:spacetime` / `dev:api-server` / `dev:web` / `dev:admin-web`:执行 `node scripts/dev.mjs <module>`
- `dev`:执行 `node scripts/dev.mjs`,启动完整五服务
- `dev:spacetime` / `dev:api-server` / `dev:bgfilter-worker` / `dev:web` / `dev:admin-web`:执行 `node scripts/dev.mjs <module>``dev:api-server` 会安全带起其依赖的 BgFilter worker
- `scripts/dev-stack-port-utils.mjs`
- `isPortAvailable(...)`:探测端口是否可监听。
- `findAvailablePort(...)`:从优先端口向后寻找可用端口,`0` 表示申请临时端口。
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite 端口,并避免本次解析结果互相冲突。
- `resolveDevStackPorts(...)`:一次性解析 SpacetimeDB、api-server、主站 Vite、后台 Vite、BgFilter worker 端口,并避免本次解析结果互相冲突。
- Linux 注册表分配:`reserveLinuxDevPortRange(...)` / `releaseLinuxDevPortRange(...)`,仅在 Linux 上启用系统级端口段登记与用户段复用,自动分配从 `10000-10099` 起。
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102`
- CLI 模式:`node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:3101 api:127.0.0.1:8082 web:0.0.0.0:3000 adminWeb:127.0.0.1:3102 bgfilterWorker:127.0.0.1:8083`
- `scripts/dev.mjs`
- 解析 CLI 参数后统一计算 client host、端口、`SPACETIME_SERVER``RUST_SERVER_TARGET`
- 完整栈按 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 顺序启动。
- Linux 下会先申请系统级端口段并把它映射成个 dev 端口;自动分配从 `10000-10099` 起,Windows 则直接沿用原有参数解析与端口漂移逻辑。
- 完整栈按 SpacetimeDB、publish、BgFilter worker readiness、api-server readiness、主站 Vite、后台 Vite 顺序启动。
- Linux 下会先申请系统级端口段并把它映射成个 dev 端口;自动分配从 `10000-10099` 起,Windows 则把第五个服务纳入原有统一参数解析与端口漂移逻辑。
- 完整栈和 `dev:api-server` 把两个 Rust 进程作为同一重启单元,先全部停止,再先启动 BgFilter worker、后启动 api-server;不要为同一份 Rust 源码创建两个并发 `cargo` watcher。
- 单模块命令复用同一套参数和 env 解析。
## 必须保持的传递链路
`npm run dev``dev:*` 单模块命令中端口解析后,必须同步到以下位置:
`npm run dev``dev:*` 单模块命令中端口解析后,必须同步到以下位置:
1. SpacetimeDB 启动:`spacetime start --listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}"`
2. SpacetimeDB 发布:`spacetime publish ... --server "${SPACETIME_SERVER}"`
3. Rust api-server`GENARRATIVE_API_HOST``GENARRATIVE_API_PORT``GENARRATIVE_SPACETIME_SERVER_URL``GENARRATIVE_SPACETIME_DATABASE`
4. api-server 健康检查:`wait_for_api_server "${RUST_SERVER_TARGET}/healthz" ...`
5. 主站 Vite`RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET``ADMIN_WEB_TARGET``ADMIN_WEB_PORT``--port=${WEB_PORT}``--host=${WEB_HOST}`
6. 后台 Vite`ADMIN_API_TARGET``GENARRATIVE_API_TARGET``GENARRATIVE_API_PORT``--port=${ADMIN_WEB_PORT}`
7. 控制台日志:`[dev:ports]``[dev] web/admin web/api-server/spacetime` 必须显示最终实际地址
8. Linux 端口段注册:`[dev] port-range:``[dev] port-range-registry:` 只在 Linux 输出,Windows 不应依赖系统级注册表
5. BgFilter worker`GENARRATIVE_PROCESS_ROLE=bgfilter-worker`、解析后的 `HOST / PORT`、与父 API 相同的 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` / `GENARRATIVE_BGFILTER_INTERNAL_TOKEN`,以及显式有效的 `N / Q`
6. BgFilter worker readiness:父 API 启动前检查解析后地址的 `/readyz`
7. 主站 Vite`RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET``ADMIN_WEB_TARGET``ADMIN_WEB_PORT``--port=${WEB_PORT}``--host=${WEB_HOST}`
8. 后台 Vite`ADMIN_API_TARGET``GENARRATIVE_API_TARGET``GENARRATIVE_API_PORT``--port=${ADMIN_WEB_PORT}`
9. 控制台日志:`[dev:ports]``[dev] web/admin web/api-server/bgfilter-worker/spacetime` 必须显示最终实际地址。
10. Linux 端口段注册:`[dev] port-range:``[dev] port-range-registry:` 只在 Linux 输出,Windows 不应依赖系统级注册表。
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
@@ -74,7 +78,7 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
- `scripts/dev-stack-port-utils.mjs`
- `scripts/dev.mjs`
- `scripts/dev-utils.mjs`
- `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`
- `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
- `docs/project-memory/shared-memory/pitfalls.md`
2. 优先改公共端口工具,不要把端口探测逻辑复制到多个脚本。
3. 修改 `scripts/dev.mjs` 时确认变量顺序:先解析参数和端口,再构造 `SPACETIME_SERVER` / `RUST_SERVER_TARGET`,最后启动对应 service。
@@ -91,21 +95,21 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
node --check scripts/dev.mjs
npm run test -- scripts/dev-stack-port-utils.test.ts
npm run check:encoding
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0
node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 api:127.0.0.1:0 web:0.0.0.0:0 adminWeb:127.0.0.1:0 bgfilterWorker:127.0.0.1:0
```
端口冲突回归测试建议:
1. 用测试或临时 Node server 占用某个优先端口。
2. 调用 `findAvailablePort`,断言结果大于被占用端口。
3. 调用 `resolveDevStackPorts`,断言个结果互不相同。
3. 调用 `resolveDevStackPorts`,断言个结果互不相同。
4. 如果实际启动完整栈,观察控制台:
- `[dev:ports] ... 不可用,改用 ...`
- `[dev] api-server: http://...:<actual-api-port>`
- `[dev] spacetime: http://...:<actual-spacetime-port>`
- 主站和后台 Vite 启动端口与日志一致。
完整启动属于长驻进程。需要 smoke 时用 background 方式启动,并另开命令检查 `/healthz``/v1/ping` 和页面端口;不要等待 `npm run dev` 自然退出。
完整启动属于长驻进程。需要 smoke 时用 background 方式启动,并另开命令检查 api-server `/healthz`BgFilter worker `/readyz`、SpacetimeDB `/v1/ping`两个页面端口;不要等待 `npm run dev` 自然退出。检查地址必须取 `.app/dev-stack.json` 或启动日志中的实际端口,不能假定 worker 一定停在 `8083`
## 常见坑
@@ -122,7 +126,8 @@ node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 ap
- [ ] Linux 注册表分配、同用户复用固定段并继续漂移、自动分配从 `10000-10099` 起、Windows bypass 都有测试覆盖。
- [ ] `scripts/dev.mjs` 通过 `node --check`
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
- [ ] BgFilter worker 在 api-server 前 ready,父子共享实际 base URL / TokenRust watch 只触发一次组合重启。
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
- [ ] 文档同步更新 `docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`
- [ ] 文档同步更新 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
- [ ] 长期踩坑同步更新 `docs/project-memory/shared-memory/pitfalls.md`
- [ ] 修改中文文件后运行 `npm run check:encoding`
+12
View File
@@ -16,6 +16,18 @@ _Avoid_: 在玩法页面内手写上传、参考图、重绘、预览、删除
独立 `/editor` 中可保存、恢复和继续编辑的图片画布工作状态,包含画布视图、图层布局和资源引用;用于多图对比、生成结果衍生和画布级编辑,不替代玩法页面内的单图资产编辑。
_Avoid_: 玩法结果页单图槽位、发布态作品、只存在前端内存里的临时画布
**画布Agent对话**:
图片画布工程右侧的对话式编辑器工具,用户通过自然语言调度画布已有的图片类生成与编辑能力(生成图片、生成角色形象、生成图标素材、生成 UI 设计图、基于附件的图片修改),并可附加画布素材或素材库图片作为参考;对话归属单个图片画布工程,可保存历史、新开会话和软删会话。属于画布域工具,不承接玩法创作、不产出玩法作品或模板,与「表单/图片输入创作工作台」的 Avoid 边界不冲突。
_Avoid_: 对话式玩法创作工作台、绕过模型定价收口的生成入口、把对话消息当作画布布局真相、复用拼图专用 creative-agent 内存会话
**画布Agent会话记录**:
画布Agent对话的持久化形态:SpacetimeDB 表只存会话元数据(会话 ID、所属工程、属主、标题、软删标记、聊天记录 OSS 对象引用、时间戳),完整消息内容以会话粒度 JSON 对象存 OSS,追加消息即整体重写对象。
_Avoid_: api-server 内存会话、消息全文入 SpacetimeDB 表、对话混入工程布局快照、每条消息一个 OSS 对象
**画布Agent对话附件**:
画布Agent对话消息携带的图片参考,统一为画布资源 / 素材库对象引用(resourceId / assetId + 可选 objectKey),单条消息上限 9 张;上传图片若从对话入口进入,必须复用素材库 / 画布资源登记链路,在上传格未落地前只从已有画布资源和账号素材库选择,不存在只属于对话的第三种图。
_Avoid_: 对话私有图片副本、内嵌 base64 附件、音视频附件
**画布资源**:
图片画布工程中可被一个或多个图层引用的图片资源记录,保存 OSS 对象引用、上传 / 生成来源、提示词、模型、任务和尺寸等资源元数据;同一资源可以在工程布局中出现多次。
_Avoid_: 图层位置、前端 hover / selected 状态、直接内嵌图片二进制
+8 -9
View File
@@ -44,10 +44,10 @@ npm run dev
补充说明:
- `npm run dev` 会启动 SpacetimeDB standalone、Rust `api-server`、主站 Vite 与后台 Vite,适合完整联调。
- `npm run dev` 会启动 SpacetimeDB standalone、独立 `bgfilter-worker`Rust `api-server`、主站 Vite 与后台 Vite,适合完整联调;内部 worker ready 后才启动 API
- 主站默认地址是 `http://127.0.0.1:3000`,后台可从 `http://127.0.0.1:3000/admin/` 进入,也可直连 `http://127.0.0.1:3102`
- 个模块可独立启动:`npm run dev:spacetime``npm run dev:api-server``npm run dev:web``npm run dev:admin-web`
- 如需自动刷新后端模块,使用 `npm run dev -- --watch`;其中 `spacetime-module` 改动后只会重新发布模块,不会重启 standalone,`api-server` 改动后会重启 Rust 进程。主站和后台前端源码变化交给 Vite 自身 HMR,不由外层 watcher 重启。非 watch 模式下可在 `npm run dev` 终端输入 `rs api-server``rs web``rs admin-web``rs spacetime``rs all`,其中 `rs spacetime` 也是只重新发布模块。
- 个模块可独立启动:`npm run dev:spacetime``npm run dev:api-server``npm run dev:bgfilter-worker``npm run dev:web``npm run dev:admin-web`;其中 `dev:api-server` 会安全带起同 runner 的 BgFilter worker 依赖
- 如需自动刷新后端模块,使用 `npm run dev -- --watch`;其中 `spacetime-module` 改动后只会重新发布模块,不会重启 standalone,Rust 源码改动会把 `api-server` `bgfilter-worker` 作为一个组合单元重启。主站和后台前端源码变化交给 Vite 自身 HMR,不由外层 watcher 重启。非 watch 模式下可在 `npm run dev` 终端输入 `rs api-server``rs bgfilter-worker``rs web``rs admin-web``rs spacetime``rs all`,其中 `rs spacetime` 也是只重新发布模块。
构建生产包:
@@ -98,21 +98,20 @@ npm run check:content
主运行时:
- [src/App.tsx](./src/App.tsx)
- [src/components/GameShell.tsx](./src/components/GameShell.tsx)
- [src/AuthenticatedApp.tsx](./src/AuthenticatedApp.tsx)
- [src/routing/appRoutes.tsx](./src/routing/appRoutes.tsx)
- [src/hooks/useCombatFlow.ts](./src/hooks/useCombatFlow.ts)
- [src/hooks/useStoryGeneration.ts](./src/hooks/useStoryGeneration.ts)
主流程内嵌编辑能力:
- [src/components/CustomWorldEntityEditorModal.tsx](./src/components/CustomWorldEntityEditorModal.tsx)
- [src/components/CustomWorldNpcVisualEditor.tsx](./src/components/CustomWorldNpcVisualEditor.tsx)
- [src/components/CustomWorldRoleAssetStudioModal.tsx](./src/components/CustomWorldRoleAssetStudioModal.tsx)
- [src/components/image-editor/ImageCanvasEditorView.tsx](./src/components/image-editor/ImageCanvasEditorView.tsx)
- [src/components/rpg-creation-editor/RpgCreationEntityEditorModal.tsx](./src/components/rpg-creation-editor/RpgCreationEntityEditorModal.tsx)
- [src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.tsx](./src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.tsx)
核心数据:
- [src/data/scenePresets.ts](./src/data/scenePresets.ts)
- [src/data/characterPresets.ts](./src/data/characterPresets.ts)
- [src/data/monsterPresets.ts](./src/data/monsterPresets.ts)
- [src/data/npcInteractions.ts](./src/data/npcInteractions.ts)
- [src/data/treasureInteractions.ts](./src/data/treasureInteractions.ts)
@@ -0,0 +1,299 @@
import { afterEach, expect, test, vi } from 'vitest';
import {
createAdminAccount,
executeAdminRechargeRefund,
getAdminFeatureGateConfig,
getAdminUserDetail,
listAdminRechargeOrders,
resolveAdminRechargeRefundManualReview,
updateAdminAccount,
uploadAdminEditorShowcaseCampaignImage,
upsertAdminFeatureGateConfig,
} from './adminApiClient';
afterEach(() => {
vi.unstubAllGlobals();
});
test('后台账号创建和更新携带 owner 会话与 Tab 权限', async () => {
const fetchMock = vi.fn().mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify({ account: { accountId: 'member-1' } }), {
status: 200,
}),
),
);
vi.stubGlobal('fetch', fetchMock);
await createAdminAccount('owner-token', {
username: 'operator',
displayName: '运营',
password: 'secret123',
tabPermissions: ['dashboard', 'tracking'],
enabled: true,
});
await updateAdminAccount('owner-token', 'member/1', {
displayName: '运营二组',
tabPermissions: ['tracking'],
enabled: false,
});
expect(fetchMock.mock.calls[0]?.[0]).toBe('/admin/api/accounts');
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer owner-token' }),
}),
);
expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/accounts/member%2F1');
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({
displayName: '运营二组',
tabPermissions: ['tracking'],
enabled: false,
}),
}),
);
});
test('灰度配置读写只使用通用 feature-gates 管理接口', async () => {
const fetchMock = vi.fn().mockImplementation(() =>
Promise.resolve(
new Response(JSON.stringify({ gates: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
),
);
vi.stubGlobal('fetch', fetchMock);
await getAdminFeatureGateConfig('gray-token');
await upsertAdminFeatureGateConfig('gray-token', {
gateKey: 'image-editor:agent-sidebar',
enabled: true,
rolloutPercent: 25,
allowUserIds: ['user-1'],
allowUserTags: ['beta'],
denyUserIds: ['blocked-1'],
description: '画布 Agent 入口灰度',
});
expect(fetchMock.mock.calls[0]?.[0]).toBe('/admin/api/feature-gates');
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer gray-token' }),
}),
);
expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/feature-gates');
expect(fetchMock.mock.calls[1]?.[1]).toEqual(
expect.objectContaining({
method: 'PUT',
headers: expect.objectContaining({ Authorization: 'Bearer gray-token' }),
body: JSON.stringify({
gateKey: 'image-editor:agent-sidebar',
enabled: true,
rolloutPercent: 25,
allowUserIds: ['user-1'],
allowUserTags: ['beta'],
denyUserIds: ['blocked-1'],
description: '画布 Agent 入口灰度',
}),
}),
);
});
test('活动卡图片上传成功后先确认正式私有对象再返回图片引用', async () => {
const closeBitmap = vi.fn();
vi.stubGlobal(
'createImageBitmap',
vi
.fn()
.mockResolvedValue({ width: 1024, height: 1536, close: closeBitmap }),
);
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
upload: {
bucket: 'genarrative-release',
host: 'https://genarrative-release.oss.example.com',
objectKey:
'generated-character-drafts/editor/showcase-campaign/current/card.png',
legacyPublicPath:
'/generated-character-drafts/editor/showcase-campaign/current/card.png',
contentType: 'image/png',
formFields: { key: 'campaign-key', policy: 'signed-policy' },
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
),
)
.mockResolvedValueOnce(new Response('', { status: 200 }))
.mockResolvedValueOnce(
new Response(
JSON.stringify({ assetObject: { assetObjectId: 'assetobj-1' } }),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
),
);
vi.stubGlobal('fetch', fetchMock);
const file = new File(['image-bytes'], 'card.png', { type: 'image/png' });
const uploaded = await uploadAdminEditorShowcaseCampaignImage(
'admin-token',
file,
);
expect(fetchMock.mock.calls[0]?.[0]).toBe(
'/admin/api/editor-showcase/campaign/image-upload-ticket',
);
expect(fetchMock.mock.calls[1]?.[0]).toBe(
'https://genarrative-release.oss.example.com',
);
expect(fetchMock.mock.calls[2]?.[0]).toBe(
'/admin/api/editor-showcase/campaign/image-upload-confirm',
);
expect(fetchMock.mock.calls[2]?.[1]).toEqual(
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }),
body: JSON.stringify({
bucket: 'genarrative-release',
objectKey:
'generated-character-drafts/editor/showcase-campaign/current/card.png',
contentType: 'image/png',
contentLength: file.size,
}),
}),
);
expect(uploaded).toEqual({
imageSrc:
'/generated-character-drafts/editor/showcase-campaign/current/card.png',
imageObjectKey:
'generated-character-drafts/editor/showcase-campaign/current/card.png',
imageWidth: 1024,
imageHeight: 1536,
legacyPublicPath:
'/generated-character-drafts/editor/showcase-campaign/current/card.png',
});
expect(closeBitmap).toHaveBeenCalledOnce();
});
test('充值订单查询按后台契约序列化筛选参数', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ entries: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
vi.stubGlobal('fetch', fetchMock);
await listAdminRechargeOrders('token-1', {
orderId: 'order 1',
userId: 'user-1',
providerTransactionId: 'wx-1',
paymentChannel: 'wechat_native',
status: 'paid',
createdAfter: '2026-07-01T00:00:00.000Z',
createdBefore: '2026-07-13T23:59:00.000Z',
limit: 50,
});
const requestUrl = String(fetchMock.mock.calls[0]?.[0]);
const parsed = new URL(requestUrl, 'http://admin.local');
expect(parsed.pathname).toBe('/admin/api/profile/recharge-orders');
expect(Object.fromEntries(parsed.searchParams)).toEqual({
orderId: 'order 1',
providerTransactionId: 'wx-1',
userId: 'user-1',
paymentChannel: 'wechat_native',
status: 'paid',
createdAfter: '2026-07-01T00:00:00.000Z',
createdBefore: '2026-07-13T23:59:00.000Z',
limit: '50',
});
});
test('用户详情只发送实际提供的用户定位字段', async () => {
const fetchMock = vi
.fn()
.mockResolvedValue(
new Response(JSON.stringify({ userId: 'user-1' }), { status: 200 }),
);
vi.stubGlobal('fetch', fetchMock);
await getAdminUserDetail('token-1', { publicUserCode: 'TN1001' });
const requestUrl = String(fetchMock.mock.calls[0]?.[0]);
const parsed = new URL(requestUrl, 'http://admin.local');
expect(parsed.pathname).toBe('/admin/api/profile/users/detail');
expect(Object.fromEntries(parsed.searchParams)).toEqual({
publicUserCode: 'TN1001',
});
});
test('退款执行使用独立 execute 管理员路由', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ outRefundNo: 'refund-1' }), {
status: 200,
}),
);
vi.stubGlobal('fetch', fetchMock);
await executeAdminRechargeRefund('token-1', {
orderId: 'order-1',
refundAmountCents: 300,
requestId: 'request-1',
reason: '用户申请',
});
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
'/admin/api/profile/recharge-refunds/execute',
);
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
orderId: 'order-1',
refundAmountCents: 300,
requestId: 'request-1',
reason: '用户申请',
}),
}),
);
});
test('退款人工复核使用独立 resolve 管理员路由', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ outRefundNo: 'refund-1' }), {
status: 200,
}),
);
vi.stubGlobal('fetch', fetchMock);
await resolveAdminRechargeRefundManualReview('token-1', {
outRefundNo: 'refund-1',
reason: '已核对微信商户平台原始账单',
expectedErrorCode: 'provider_transaction_id_mismatch',
});
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
'/admin/api/profile/recharge-refunds/manual-review/resolve',
);
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
outRefundNo: 'refund-1',
reason: '已核对微信商户平台原始账单',
expectedErrorCode: 'provider_transaction_id_mismatch',
}),
}),
);
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+102 -64
View File
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useState} from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
formatAdminApiError,
@@ -17,25 +17,31 @@ import {
getStoredAdminToken,
setStoredAdminToken,
} from '../auth/adminAuthStore';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminDashboardPage} from '../pages/AdminDashboardPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminEditorAssetQueryPage} from '../pages/AdminEditorAssetQueryPage';
import {AdminEditorShowcaseReviewPage} from '../pages/AdminEditorShowcaseReviewPage';
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
import {AdminWorkVisibilityPage} from '../pages/AdminWorkVisibilityPage';
import {AdminShell} from './AdminShell';
import type {AdminRouteId} from './adminRoutes';
import {resolveAdminRoute, routeHash} from './adminRoutes';
import { AdminAccountsPage } from '../pages/AdminAccountsPage';
import { AdminDashboardPage } from '../pages/AdminDashboardPage';
import { AdminDatabaseTablesPage } from '../pages/AdminDatabaseTablesPage';
import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage';
import { AdminEditorAssetQueryPage } from '../pages/AdminEditorAssetQueryPage';
import { AdminEditorGenerationPricingPage } from '../pages/AdminEditorGenerationPricingPage';
import { AdminEditorShowcaseReviewPage } from '../pages/AdminEditorShowcaseReviewPage';
import { AdminGrayReleaseConfigPage } from '../pages/AdminGrayReleaseConfigPage';
import { AdminInviteCodePage } from '../pages/AdminInviteCodePage';
import { AdminLoginPage } from '../pages/AdminLoginPage';
import { AdminOverviewPage } from '../pages/AdminOverviewPage';
import { AdminProfileWalletConfigPage } from '../pages/AdminProfileWalletConfigPage';
import { AdminRechargeOrderPage } from '../pages/AdminRechargeOrderPage';
import { AdminRechargeProductPage } from '../pages/AdminRechargeProductPage';
import { AdminRedeemCodePage } from '../pages/AdminRedeemCodePage';
import { AdminTaskConfigPage } from '../pages/AdminTaskConfigPage';
import { AdminTrackingEventsPage } from '../pages/AdminTrackingEventsPage';
import type { AdminRouteId } from './adminRoutes';
import {
getAccessibleAdminRoutes,
resolveAccessibleAdminRoute,
resolveAdminRoute,
routeHash,
} from './adminRoutes';
import { AdminShell } from './AdminShell';
type SessionStatus = 'checking' | 'guest' | 'authenticated';
@@ -53,6 +59,13 @@ export function AdminApp() {
useState<ProfileWalletConfigAdminResponse | null>(null);
const [rechargeProductResult, setRechargeProductResult] =
useState<ProfileRechargeProductConfigAdminResponse | null>(null);
const accessibleRoutes = useMemo(
() => (admin ? getAccessibleAdminRoutes(admin) : []),
[admin],
);
const activeRouteId = accessibleRoutes.some((route) => route.id === routeId)
? routeId
: null;
const clearSession = useCallback((message = '') => {
clearStoredAdminToken();
@@ -105,6 +118,26 @@ export function AdminApp() {
};
}, []);
useEffect(() => {
if (status !== 'authenticated' || !admin) {
return;
}
const nextRouteId = resolveAccessibleAdminRoute(
window.location.hash,
accessibleRoutes,
);
if (!nextRouteId) {
return;
}
setRouteId(nextRouteId);
const nextHash = routeHash(nextRouteId);
if (window.location.hash !== nextHash) {
window.history.replaceState(null, '', nextHash);
}
}, [accessibleRoutes, admin, routeId, status]);
useEffect(() => {
const handleHashChange = () => {
setRouteId(resolveAdminRoute(window.location.hash));
@@ -122,17 +155,20 @@ export function AdminApp() {
}
}, []);
const handleLogin = useCallback(async (username: string, password: string) => {
const response = await loginAdmin(username, password);
setStoredAdminToken(response.token);
setToken(response.token);
setAdmin(response.admin);
setTaskConfigResult(null);
setProfileWalletConfigResult(null);
setRechargeProductResult(null);
setLoginNotice('');
setStatus('authenticated');
}, []);
const handleLogin = useCallback(
async (username: string, password: string) => {
const response = await loginAdmin(username, password);
setStoredAdminToken(response.token);
setToken(response.token);
setAdmin(response.admin);
setTaskConfigResult(null);
setProfileWalletConfigResult(null);
setRechargeProductResult(null);
setLoginNotice('');
setStatus('authenticated');
},
[],
);
const handleUnauthorized = useCallback(
(message = '登录状态已失效') => {
@@ -161,63 +197,56 @@ export function AdminApp() {
return (
<AdminShell
admin={admin}
routeId={routeId}
routeId={activeRouteId}
routes={accessibleRoutes}
onLogout={handleLogout}
onRouteChange={handleRouteChange}
>
{routeId === 'dashboard' ? (
{activeRouteId === null ? (
<section className="admin-panel admin-zero-permission-state">
<h2>访</h2>
</section>
) : null}
{activeRouteId === 'dashboard' ? (
<AdminDashboardPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{routeId === 'overview' ? (
{activeRouteId === 'overview' ? (
<AdminOverviewPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{routeId === 'tables' ? (
{activeRouteId === 'tables' ? (
<AdminDatabaseTablesPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'debug' ? (
{activeRouteId === 'debug' ? (
<AdminDebugHttpPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
{routeId === 'tracking' ? (
{activeRouteId === 'tracking' ? (
<AdminTrackingEventsPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'redeem' ? (
{activeRouteId === 'gray-release' ? (
<AdminGrayReleaseConfigPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'redeem' ? (
<AdminRedeemCodePage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'invite' ? (
{activeRouteId === 'invite' ? (
<AdminInviteCodePage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'creation-announcement' ? (
<AdminCreationEntrySwitchPage
mode="announcements"
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'creation-entry' ? (
<AdminCreationEntrySwitchPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'work-visibility' ? (
<AdminWorkVisibilityPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'tasks' ? (
{activeRouteId === 'tasks' ? (
<AdminTaskConfigPage
result={taskConfigResult}
token={token}
@@ -225,7 +254,7 @@ export function AdminApp() {
onResultChange={setTaskConfigResult}
/>
) : null}
{routeId === 'profile-wallet' ? (
{activeRouteId === 'profile-wallet' ? (
<AdminProfileWalletConfigPage
result={profileWalletConfigResult}
token={token}
@@ -233,7 +262,7 @@ export function AdminApp() {
onResultChange={setProfileWalletConfigResult}
/>
) : null}
{routeId === 'recharge-products' ? (
{activeRouteId === 'recharge-products' ? (
<AdminRechargeProductPage
result={rechargeProductResult}
token={token}
@@ -241,24 +270,33 @@ export function AdminApp() {
onResultChange={setRechargeProductResult}
/>
) : null}
{routeId === 'editor-generation-pricing' ? (
{activeRouteId === 'recharge-orders' ? (
<AdminRechargeOrderPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'editor-generation-pricing' ? (
<AdminEditorGenerationPricingPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'editor-showcase' ? (
{activeRouteId === 'editor-showcase' ? (
<AdminEditorShowcaseReviewPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{routeId === 'editor-assets' ? (
{activeRouteId === 'editor-assets' ? (
<AdminEditorAssetQueryPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
{activeRouteId === 'accounts' ? (
<AdminAccountsPage token={token} onUnauthorized={handleUnauthorized} />
) : null}
</AdminShell>
);
}
+23 -22
View File
@@ -1,32 +1,32 @@
import {
Activity,
Bug,
BadgeDollarSign,
Bug,
Coins,
LayoutDashboard,
LogOut,
Megaphone,
Eye,
Images,
Star,
WalletCards,
ShieldCheck,
ListChecks,
SlidersHorizontal,
Database,
GitBranch,
Images,
LayoutDashboard,
ListChecks,
LogOut,
ReceiptText,
ShieldCheck,
Star,
Table2,
TicketCheck,
TicketPercent,
Users,
WalletCards,
} from 'lucide-react';
import type {ReactNode} from 'react';
import type { ReactNode } from 'react';
import type {AdminSessionPayload} from '../api/adminApiTypes';
import type {AdminRouteId} from './adminRoutes';
import {adminRoutes} from './adminRoutes';
import type { AdminSessionPayload } from '../api/adminApiTypes';
import type { AdminRouteDefinition, AdminRouteId } from './adminRoutes';
interface AdminShellProps {
admin: AdminSessionPayload;
routeId: AdminRouteId;
routeId: AdminRouteId | null;
routes: AdminRouteDefinition[];
children: ReactNode;
onRouteChange: (routeId: AdminRouteId) => void;
onLogout: () => void;
@@ -38,22 +38,23 @@ const routeIcons = {
tables: Database,
debug: Bug,
tracking: Table2,
'gray-release': GitBranch,
redeem: TicketPercent,
invite: TicketCheck,
'profile-wallet': WalletCards,
tasks: ListChecks,
'recharge-products': BadgeDollarSign,
'recharge-orders': ReceiptText,
'editor-generation-pricing': Coins,
'editor-showcase': Star,
'editor-assets': Images,
'creation-announcement': Megaphone,
'creation-entry': SlidersHorizontal,
'work-visibility': Eye,
accounts: Users,
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
export function AdminShell({
admin,
routeId,
routes,
children,
onRouteChange,
onLogout,
@@ -72,7 +73,7 @@ export function AdminShell({
</div>
<nav className="admin-nav" aria-label="后台导航">
{adminRoutes.map((route) => {
{routes.map((route) => {
const Icon = routeIcons[route.id];
return (
<button
@@ -95,7 +96,7 @@ export function AdminShell({
<header className="admin-topbar">
<div className="admin-user">
<span>{admin.displayName || admin.username}</span>
<small>{admin.roles.join(' / ')}</small>
<small>{admin.accountRole === 'owner' ? 'owner' : 'member'}</small>
</div>
<button
className="admin-icon-button"
@@ -112,7 +113,7 @@ export function AdminShell({
</div>
<nav className="admin-bottom-nav" aria-label="后台导航">
{adminRoutes.map((route) => {
{routes.map((route) => {
const Icon = routeIcons[route.id];
return (
<button
+78 -15
View File
@@ -1,6 +1,12 @@
import {expect, test} from 'vitest';
import { expect, test } from 'vitest';
import {adminRoutes, resolveAdminRoute, routeHash} from './adminRoutes';
import {
adminRoutes,
getAccessibleAdminRoutes,
resolveAccessibleAdminRoute,
resolveAdminRoute,
routeHash,
} from './adminRoutes';
test('后台默认进入 Dashboard', () => {
expect(adminRoutes[0]).toEqual({
@@ -13,19 +19,6 @@ test('后台默认进入 Dashboard', () => {
expect(routeHash('dashboard')).toBe('#dashboard');
});
// 中文注释:后台入口公告必须作为独立导航存在,避免公告表单被误藏在入口开关页。
test('后台入口公告路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'creation-announcement',
label: '入口公告',
hash: '#creation-announcement',
});
expect(resolveAdminRoute('#creation-announcement')).toBe(
'creation-announcement',
);
expect(routeHash('creation-announcement')).toBe('#creation-announcement');
});
test('后台模型定价路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'editor-generation-pricing',
@@ -40,6 +33,22 @@ test('后台模型定价路由可通过导航和 hash 访问', () => {
);
});
test('后台灰度发布路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'gray-release',
label: '灰度发布',
hash: '#gray-release',
});
expect(resolveAdminRoute('#gray-release')).toBe('gray-release');
expect(routeHash('gray-release')).toBe('#gray-release');
});
test('后台不再暴露旧创作模板管理路由', () => {
expect(resolveAdminRoute('#creation-entry')).toBe('dashboard');
expect(resolveAdminRoute('#creation-announcement')).toBe('dashboard');
expect(resolveAdminRoute('#work-visibility')).toBe('dashboard');
});
test('后台素材查询路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'editor-assets',
@@ -59,3 +68,57 @@ test('后台精选审核路由可通过导航和 hash 访问', () => {
expect(resolveAdminRoute('#editor-showcase')).toBe('editor-showcase');
expect(routeHash('editor-showcase')).toBe('#editor-showcase');
});
test('后台充值管理路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'recharge-orders',
label: '充值管理',
hash: '#recharge-orders',
});
expect(resolveAdminRoute('#recharge-orders')).toBe('recharge-orders');
expect(routeHash('recharge-orders')).toBe('#recharge-orders');
});
test('owner 可访问全部业务 Tab 和账号管理', () => {
const routes = getAccessibleAdminRoutes({
accountRole: 'owner',
tabPermissions: [],
});
expect(routes).toEqual(adminRoutes);
expect(routes.at(-1)).toMatchObject({ id: 'accounts', ownerOnly: true });
});
test('member 只访问已分配 Tab 且无权 hash 回落到第一项', () => {
const routes = getAccessibleAdminRoutes({
accountRole: 'member',
tabPermissions: ['tracking', 'recharge-orders'],
});
expect(routes.map((route) => route.id)).toEqual([
'tracking',
'recharge-orders',
]);
expect(resolveAccessibleAdminRoute('#accounts', routes)).toBe('tracking');
expect(resolveAccessibleAdminRoute('#recharge-orders', routes)).toBe(
'recharge-orders',
);
});
test('member 可单独获得灰度发布 Tab 权限', () => {
const routes = getAccessibleAdminRoutes({
accountRole: 'member',
tabPermissions: ['gray-release'],
});
expect(routes.map((route) => route.id)).toEqual(['gray-release']);
expect(resolveAccessibleAdminRoute('#gray-release', routes)).toBe(
'gray-release',
);
});
test('零权限 member 不回落到 Dashboard', () => {
const routes = getAccessibleAdminRoutes({
accountRole: 'member',
tabPermissions: [],
});
expect(routes).toEqual([]);
expect(resolveAccessibleAdminRoute('#dashboard', routes)).toBeNull();
});

Some files were not shown because too many files have changed in this diff Show More