Merge remote-tracking branch 'origin/master'
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-01 22:14:49 +08:00
151 changed files with 3952 additions and 1299 deletions

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>陶泥后台</title> <title>百梦后台</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -5,8 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1", "dev": "vite --host 127.0.0.1",
"build": "tsc --noEmit && vite build", "build": "node ../../scripts/admin-web-build.mjs build",
"typecheck": "tsc --noEmit", "typecheck": "node ../../scripts/admin-web-build.mjs typecheck",
"preview": "vite preview --host 127.0.0.1" "preview": "vite preview --host 127.0.0.1"
}, },
"dependencies": { "dependencies": {

View File

@@ -42,7 +42,7 @@ export function AdminShell({
<ShieldCheck size={20} aria-hidden="true" /> <ShieldCheck size={20} aria-hidden="true" />
</div> </div>
<div> <div>
<strong></strong> <strong></strong>
<span>Admin</span> <span>Admin</span>
</div> </div>
</div> </div>

View File

@@ -39,7 +39,7 @@ export function AdminLoginPage({notice, onLogin}: AdminLoginPageProps) {
<ShieldCheck size={26} aria-hidden="true" /> <ShieldCheck size={26} aria-hidden="true" />
</div> </div>
<div> <div>
<h1></h1> <h1></h1>
<span>Admin Console</span> <span>Admin Console</span>
</div> </div>
</div> </div>

View File

@@ -166,7 +166,7 @@ export function AdminRedeemCodePage({
/> />
</label> </label>
<label className="admin-field"> <label className="admin-field">
<span></span> <span></span>
<textarea <textarea
rows={6} rows={6}
value={allowedPublicUserCodes} value={allowedPublicUserCodes}

View File

@@ -6,7 +6,7 @@
当前自定义世界创作工具已经有了比较强的生成骨架、锚点结构和结果编辑能力,但整体仍处在一个很明显的“半收口状态”: 当前自定义世界创作工具已经有了比较强的生成骨架、锚点结构和结果编辑能力,但整体仍处在一个很明显的“半收口状态”:
**设计目标已经走到“陶泥主工作台”,数据结构已经支持“锚点化输入”,但实际体验仍然更像“大文本生成器 + 大型结果总表编辑器”。** **设计目标已经走到“百梦主工作台”,数据结构已经支持“锚点化输入”,但实际体验仍然更像“大文本生成器 + 大型结果总表编辑器”。**
如果用一句话概括当前问题,就是: 如果用一句话概括当前问题,就是:
@@ -61,7 +61,7 @@
- 标志性要素 - 标志性要素
- 禁止事项 - 禁止事项
但实际入口 `src/components/SelectionCustomizationModals.tsx` 里,陶泥主弹窗仍然基本只有: 但实际入口 `src/components/SelectionCustomizationModals.tsx` 里,百梦主弹窗仍然基本只有:
- 生成模式 - 生成模式
- 一块大 textarea - 一块大 textarea
@@ -82,7 +82,7 @@
--- ---
## 2.2 澄清机制已经存在,但没有真正服务陶泥 ## 2.2 澄清机制已经存在,但没有真正服务百梦
`server-node/src/services/customWorldSessionStore.ts` 已经支持: `server-node/src/services/customWorldSessionStore.ts` 已经支持:
@@ -101,7 +101,7 @@
这意味着: 这意味着:
**系统表面上已经有“先澄清再生成”的能力,但实际体验里,陶泥主并没有真正参与这一步。** **系统表面上已经有“先澄清再生成”的能力,但实际体验里,百梦主并没有真正参与这一步。**
结果就是: 结果就是:
@@ -113,7 +113,7 @@
- 把 session question 真正接到前端,作为生成前的二次确认步骤。 - 把 session question 真正接到前端,作为生成前的二次确认步骤。
- 每次只问 `1~3` 个最关键问题,不要把它做成问卷。 - 每次只问 `1~3` 个最关键问题,不要把它做成问卷。
- 支持“一键使用系统建议”,但必须让陶泥主可见,而不是静默自动填充。 - 支持“一键使用系统建议”,但必须让百梦主可见,而不是静默自动填充。
- 把回答结果回写到 `creatorIntent`,而不是只作为一次性会话答案。 - 把回答结果回写到 `creatorIntent`,而不是只作为一次性会话答案。
--- ---
@@ -175,7 +175,7 @@
这会带来三层问题: 这会带来三层问题:
1. 陶泥主负担过重 1. 百梦主负担过重
- 很多字段属于“系统编译层”,不属于“创作决策层”。 - 很多字段属于“系统编译层”,不属于“创作决策层”。
2. 移动端负担过重 2. 移动端负担过重
@@ -240,7 +240,7 @@
--- ---
## 2.6 快速模式还不够“快”,生成页也还不够“陶泥主视角” ## 2.6 快速模式还不够“快”,生成页也还不够“百梦主视角”
当前快速模式的主要区别,是把数量降成: 当前快速模式的主要区别,是把数量降成:
@@ -269,7 +269,7 @@
- 计时 - 计时
- 模型阶段 - 模型阶段
而不是陶泥主真正关心的: 而不是百梦主真正关心的:
- 关键角色有没有成型 - 关键角色有没有成型
- 核心冲突有没有稳定 - 核心冲突有没有稳定
@@ -281,7 +281,7 @@
- 快速模式改成真正的“关键锚点预览模式”: - 快速模式改成真正的“关键锚点预览模式”:
- 先只生成关键角色、关键地点、核心冲突摘要 - 先只生成关键角色、关键地点、核心冲突摘要
- 暂不补全所有长尾档案 - 暂不补全所有长尾档案
- 生成页改成“陶泥主视角进度”: - 生成页改成“百梦主视角进度”:
- 世界灵魂已确定 - 世界灵魂已确定
- 关键角色已成型 - 关键角色已成型
- 关键地点已落地 - 关键地点已落地
@@ -391,16 +391,16 @@
### P0先修主链路闭环 ### P0先修主链路闭环
- 补卡片化输入入口,至少把关键锚点输入真正开放出来。 - 补卡片化输入入口,至少把关键锚点输入真正开放出来。
- 把澄清问题正式接入陶泥主流程,不再静默自动兜底。 - 把澄清问题正式接入百梦主流程,不再静默自动兜底。
- 修正“新建完成后直接回世界列表”的流程,生成后默认进入结果工作台。 - 修正“新建完成后直接回世界列表”的流程,生成后默认进入结果工作台。
- 统一锁定与局部重生成规则,先让“陶泥主不怕重生成”成立。 - 统一锁定与局部重生成规则,先让“百梦主不怕重生成”成立。
### P1再降低工作台负担 ### P1再降低工作台负担
- 结果页默认只展示高杠杆编辑。 - 结果页默认只展示高杠杆编辑。
- 低杠杆字段进入高级模式。 - 低杠杆字段进入高级模式。
- 快速模式改成真正的关键对象预览模式。 - 快速模式改成真正的关键对象预览模式。
- 生成页改成陶泥主视角进度,而不是模型批次视角。 - 生成页改成百梦主视角进度,而不是模型批次视角。
### P2最后做架构收口与去模板化 ### P2最后做架构收口与去模板化
@@ -441,4 +441,4 @@
当前自定义世界创作工具最需要的,不是再继续补更多字段或更多生成步骤,而是: 当前自定义世界创作工具最需要的,不是再继续补更多字段或更多生成步骤,而是:
**把“陶泥主先决定灵魂锚点,系统再稳定展开世界”这条主逻辑真正落到 UI、流程和后端边界上。** **把“百梦主先决定灵魂锚点,系统再稳定展开世界”这条主逻辑真正落到 UI、流程和后端边界上。**

View File

@@ -1,4 +1,4 @@
# 自定义世界陶泥主输入与 AI 分工边界设计 # 自定义世界百梦主输入与 AI 分工边界设计
更新时间:`2026-04-06` 更新时间:`2026-04-06`
@@ -6,9 +6,9 @@
这份文档回答一个非常关键的问题: 这份文档回答一个非常关键的问题:
**在“低创作门槛、高创作自由度”的前提下,自定义世界里哪些内容应该交给陶泥主直接定义,哪些内容应该交给 AI 和系统完成。** **在“低创作门槛、高创作自由度”的前提下,自定义世界里哪些内容应该交给百梦主直接定义,哪些内容应该交给 AI 和系统完成。**
这里默认我们的陶泥主: 这里默认我们的百梦主:
- 不需要有专业作家背景 - 不需要有专业作家背景
- 不需要有专业游戏设计背景 - 不需要有专业游戏设计背景
@@ -16,33 +16,33 @@
一句话目标: 一句话目标:
**让陶泥主把精力放在“决定这个世界为什么值得被创作”,把 AI 用在“把这个世界展开、编译、铺开、校验、补足”。** **让百梦主把精力放在“决定这个世界为什么值得被创作”,把 AI 用在“把这个世界展开、编译、铺开、校验、补足”。**
## 1. 总体结论 ## 1. 总体结论
自定义世界的分工边界应该遵守 3 条硬原则: 自定义世界的分工边界应该遵守 3 条硬原则:
1. 灵魂归陶泥主,杂活归 AI。 1. 灵魂归百梦主,杂活归 AI。
- 凡是决定作品气质、主题、冲突、人物关系、审美方向的内容,都应由陶泥主掌握。 - 凡是决定作品气质、主题、冲突、人物关系、审美方向的内容,都应由百梦主掌握。
2. 重点对象归陶泥主,长尾铺量归 AI。 2. 重点对象归百梦主,长尾铺量归 AI。
- 陶泥主应重点塑造少量关键角色、关键地点、关键冲突、关键意象,而不是被迫手填几十个 NPC、几十个场景、几百条描述。 - 百梦主应重点塑造少量关键角色、关键地点、关键冲突、关键意象,而不是被迫手填几十个 NPC、几十个场景、几百条描述。
3. 决策归陶泥主,编译归 AI / 系统。 3. 决策归百梦主,编译归 AI / 系统。
- 陶泥主负责说“这个世界要成为什么样”AI / 系统负责把它编译成可运行的数据、规则、文本、关系钩子和运行时结构。 - 百梦主负责说“这个世界要成为什么样”AI / 系统负责把它编译成可运行的数据、规则、文本、关系钩子和运行时结构。
这意味着: 这意味着:
- 陶泥主应该主要编辑“高杠杆创作锚点” - 百梦主应该主要编辑“高杠杆创作锚点”
- AI 应该主要承担“批量展开 + 结构编译 + 一致性维护 + 专业执行” - AI 应该主要承担“批量展开 + 结构编译 + 一致性维护 + 专业执行”
## 2. 什么内容应该交给陶泥 ## 2. 什么内容应该交给百梦
真正应该交给陶泥主的,不是大量表格字段,而是下面这些会显著决定作品质量、且 AI 不擅长替代的内容。 真正应该交给百梦主的,不是大量表格字段,而是下面这些会显著决定作品质量、且 AI 不擅长替代的内容。
## 2.1 世界核心命题 ## 2.1 世界核心命题
陶泥主应该直接定义: 百梦主应该直接定义:
- 这个世界的一句话设定 - 这个世界的一句话设定
- 这个世界最吸引人的核心幻想 - 这个世界最吸引人的核心幻想
@@ -56,7 +56,7 @@
## 2.2 主题、气质与边界 ## 2.2 主题、气质与边界
陶泥主应该直接定义: 百梦主应该直接定义:
- 主题关键词 - 主题关键词
- 情绪基调 - 情绪基调
@@ -71,7 +71,7 @@
## 2.3 玩家身份与开局处境 ## 2.3 玩家身份与开局处境
陶泥主应该直接定义: 百梦主应该直接定义:
- 玩家扮演的是什么人 - 玩家扮演的是什么人
- 玩家一开始最缺什么、最想要什么 - 玩家一开始最缺什么、最想要什么
@@ -85,7 +85,7 @@
## 2.4 核心冲突与关键势力 ## 2.4 核心冲突与关键势力
陶泥主应该直接定义少量高价值内容: 百梦主应该直接定义少量高价值内容:
- 世界当前最重要的 `2~4` 条明面冲突 - 世界当前最重要的 `2~4` 条明面冲突
- 世界背后最关键的 `1~3` 条暗面问题 - 世界背后最关键的 `1~3` 条暗面问题
@@ -96,13 +96,13 @@
- 冲突结构决定世界是否“有戏” - 冲突结构决定世界是否“有戏”
- 势力关系是 AI 最容易写散、写平、写成百科介绍的部分 - 势力关系是 AI 最容易写散、写平、写成百科介绍的部分
- 这一层由陶泥主把握,才能真正提高作品的辨识度 - 这一层由百梦主把握,才能真正提高作品的辨识度
## 2.5 关键角色与关系张力 ## 2.5 关键角色与关系张力
陶泥主应该直接定义少量关键角色,而不是所有 NPC。 百梦主应该直接定义少量关键角色,而不是所有 NPC。
建议重点交给陶泥主的,是: 建议重点交给百梦主的,是:
- `3~8` 个关键角色 - `3~8` 个关键角色
- 玩家与这些人的潜在关系 - 玩家与这些人的潜在关系
@@ -113,11 +113,11 @@
- 角色关系是最能显著提升作品质量的部分之一 - 角色关系是最能显著提升作品质量的部分之一
- 这也是 AI 最容易写得“完整但无味”的部分 - 这也是 AI 最容易写得“完整但无味”的部分
- 陶泥主不需要写长篇背景,但应掌握这些角色真正的关系骨架 - 百梦主不需要写长篇背景,但应掌握这些角色真正的关系骨架
## 2.6 关键地点与空间记忆点 ## 2.6 关键地点与空间记忆点
陶泥主应该直接定义: 百梦主应该直接定义:
- `4~12` 个关键地点 / 区域 / 地标 - `4~12` 个关键地点 / 区域 / 地标
- 这些地方为什么重要 - 这些地方为什么重要
@@ -131,7 +131,7 @@
## 2.7 标志性意象、物件、怪物、制度与规则 ## 2.7 标志性意象、物件、怪物、制度与规则
陶泥主应该优先控制世界里最能代表它的东西: 百梦主应该优先控制世界里最能代表它的东西:
- 标志性物件 - 标志性物件
- 标志性怪物 / 生物 - 标志性怪物 / 生物
@@ -144,9 +144,9 @@
- 这些内容决定世界的“手感” - 这些内容决定世界的“手感”
- 它们不是普通细节,而是会反复影响命名、剧情、视觉、对话与玩法解释的母题 - 它们不是普通细节,而是会反复影响命名、剧情、视觉、对话与玩法解释的母题
## 2.8 陶泥主应直接控制的“禁止事项” ## 2.8 百梦主应直接控制的“禁止事项”
陶泥主必须能明确锁定: 百梦主必须能明确锁定:
- 什么绝对不能改 - 什么绝对不能改
- 什么不能被 AI 自动扩写到别的方向 - 什么不能被 AI 自动扩写到别的方向
@@ -156,7 +156,7 @@
原因: 原因:
- 高自由度不等于所有内容都开放漂移 - 高自由度不等于所有内容都开放漂移
- 如果没有“锁定机制”AI 会把陶泥主真正关心的内容稀释掉 - 如果没有“锁定机制”AI 会把百梦主真正关心的内容稀释掉
## 3. 什么内容应该交给 AI 和系统 ## 3. 什么内容应该交给 AI 和系统
@@ -176,7 +176,7 @@
原因: 原因:
- 这些内容数量大、重复度高 - 这些内容数量大、重复度高
- 它们需要“贴合世界”,但不需要都由陶泥主逐个手写 - 它们需要“贴合世界”,但不需要都由百梦主逐个手写
- AI 很适合做“围绕锚点的批量铺量” - AI 很适合做“围绕锚点的批量铺量”
## 3.2 从创作锚点到系统结构的编译 ## 3.2 从创作锚点到系统结构的编译
@@ -186,7 +186,7 @@
- 从自然语言世界设定中提取题材词汇 - 从自然语言世界设定中提取题材词汇
- 从关键冲突中编译出世界叙事图谱 - 从关键冲突中编译出世界叙事图谱
- 从关键角色卡编译出角色叙事档案 - 从关键角色卡编译出角色叙事档案
-陶泥主输入里自动生成标签、钩子、隐藏线索、章节摘要 -百梦主输入里自动生成标签、钩子、隐藏线索、章节摘要
- 从地点和关系中编译出场景连接、事件触发和叙事回响 - 从地点和关系中编译出场景连接、事件触发和叙事回响
对应当前仓库,下面这些结构更适合由 AI / 系统生成,而不是让玩家直接编辑: 对应当前仓库,下面这些结构更适合由 AI / 系统生成,而不是让玩家直接编辑:
@@ -203,7 +203,7 @@
原因: 原因:
- 这些是运行时结构,不是陶泥主真正想表达的作品内容 - 这些是运行时结构,不是百梦主真正想表达的作品内容
- 直接暴露给玩家,会把创作过程变成专业数据填表 - 直接暴露给玩家,会把创作过程变成专业数据填表
## 3.3 专业化、规则化的任务 ## 3.3 专业化、规则化的任务
@@ -223,7 +223,7 @@
原因: 原因:
- 这些工作要么重复、要么专业、要么容易做脏活累活 - 这些工作要么重复、要么专业、要么容易做脏活累活
- 让非专业陶泥主处理,会显著提高门槛,却不一定显著提高质量 - 让非专业百梦主处理,会显著提高门槛,却不一定显著提高质量
## 3.4 一致性、纠错与查漏补缺 ## 3.4 一致性、纠错与查漏补缺
@@ -240,15 +240,15 @@
原因: 原因:
- 这是 AI 比人更适合做的“维护型工作” - 这是 AI 比人更适合做的“维护型工作”
- 它属于创作支持,不属于陶泥主必须亲手完成的创作 - 它属于创作支持,不属于百梦主必须亲手完成的创作
## 4. 最合理的边界不是二分法,而是三层分工 ## 4. 最合理的边界不是二分法,而是三层分工
自定义世界最合理的结构不是“玩家写”与“AI 写”的简单二选一,而是三层。 自定义世界最合理的结构不是“玩家写”与“AI 写”的简单二选一,而是三层。
## 4.1 第一层:陶泥主必控层 ## 4.1 第一层:百梦主必控层
这一层必须给陶泥主高自由度,且能被锁定: 这一层必须给百梦主高自由度,且能被锁定:
- 世界核心命题 - 世界核心命题
- 主题与气质 - 主题与气质
@@ -264,9 +264,9 @@
**少而重。** **少而重。**
## 4.2 第二层:陶泥主可选强化层 ## 4.2 第二层:百梦主可选强化层
这一层不应强制填写,但应该允许陶泥主继续深挖: 这一层不应强制填写,但应该允许百梦主继续深挖:
- 明线 / 暗线种子 - 明线 / 暗线种子
- 角色之间的旧事 - 角色之间的旧事
@@ -301,17 +301,17 @@
## 5. 具体模块的建议归属 ## 5. 具体模块的建议归属
| 模块 | 建议归属 | 陶泥主应控制什么 | AI / 系统应负责什么 | | 模块 | 建议归属 | 百梦主应控制什么 | AI / 系统应负责什么 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 世界一句话设定、核心幻想、核心卖点 | 陶泥主直接控制 | 直接写、直接改、可锁定 | 给出备选表述和扩展方向 | | 世界一句话设定、核心幻想、核心卖点 | 百梦主直接控制 | 直接写、直接改、可锁定 | 给出备选表述和扩展方向 |
| 主题、基调、审美、禁忌 | 陶泥主直接控制 | 选择 / 改写 / 锁定 | 生成风格词、避雷词、提示词约束 | | 主题、基调、审美、禁忌 | 百梦主直接控制 | 选择 / 改写 / 锁定 | 生成风格词、避雷词、提示词约束 |
| 玩家身份、开局处境、玩家目标 | 陶泥主直接控制 | 直接定义 | 补足开局钩子和初始叙事包装 | | 玩家身份、开局处境、玩家目标 | 百梦主直接控制 | 直接定义 | 补足开局钩子和初始叙事包装 |
| 关键势力与核心冲突 | 陶泥主控AI 辅助 | 定义核心关系和立场 | 扩展冲突支路、生成世界线程 | | 关键势力与核心冲突 | 百梦主控AI 辅助 | 定义核心关系和立场 | 扩展冲突支路、生成世界线程 |
| 关键角色 | 陶泥主控AI 辅助 | 定义角色骨架、关系张力、秘密方向 | 生成长背景、章节拆分、技能、物品、叙事档案 | | 关键角色 | 百梦主控AI 辅助 | 定义角色骨架、关系张力、秘密方向 | 生成长背景、章节拆分、技能、物品、叙事档案 |
| 关键地点 | 陶泥主控AI 辅助 | 定义地点意义、气氛、秘密 | 扩展场景细节、连接关系、遭遇分布 | | 关键地点 | 百梦主控AI 辅助 | 定义地点意义、气氛、秘密 | 扩展场景细节、连接关系、遭遇分布 |
| 标志性物件 / 怪物 / 制度 / 规则 | 陶泥主控AI 辅助 | 定义代表性要素与硬边界 | 扩展变体、命名、说明、运行时挂钩 | | 标志性物件 / 怪物 / 制度 / 规则 | 百梦主控AI 辅助 | 定义代表性要素与硬边界 | 扩展变体、命名、说明、运行时挂钩 |
| 普通 NPC / 路人 / 杂兵 / 次级地点 | 主要交给 AI | 仅在需要时抽查或替换 | 批量生成与风格保持 | | 普通 NPC / 路人 / 杂兵 / 次级地点 | 主要交给 AI | 仅在需要时抽查或替换 | 批量生成与风格保持 |
| 角色长背景、章节 teaser、context snippet | 主要交给 AI | 陶泥主只改关键角色即可 | 自动拆章、压缩、解锁节奏整理 | | 角色长背景、章节 teaser、context snippet | 主要交给 AI | 百梦主只改关键角色即可 | 自动拆章、压缩、解锁节奏整理 |
| 技能、初始物品、标签、构筑倾向 | 主要交给 AI / 系统 | 提供偏好或少量 override | 按角色和世界规则自动编译 | | 技能、初始物品、标签、构筑倾向 | 主要交给 AI / 系统 | 提供偏好或少量 override | 按角色和世界规则自动编译 |
| 世界图谱、知识事实、可见性、导演指令 | AI / 系统内部层 | 不应默认暴露给玩家 | 运行时编译与维护 | | 世界图谱、知识事实、可见性、导演指令 | AI / 系统内部层 | 不应默认暴露给玩家 | 运行时编译与维护 |
| 一致性检查、冲突检查、越权检查 | AI / 系统内部层 | 查看报告、决定是否采纳修改 | 自动扫描并提出修正建议 | | 一致性检查、冲突检查、越权检查 | AI / 系统内部层 | 查看报告、决定是否采纳修改 | 自动扫描并提出修正建议 |
@@ -328,7 +328,7 @@
- 精确数值型 build 倾向 - 精确数值型 build 倾向
- 复杂掉落预算 - 复杂掉落预算
更合理的做法是让陶泥主填写直觉表达,例如: 更合理的做法是让百梦主填写直觉表达,例如:
- `初见就戒备` - `初见就戒备`
- `容易合作` - `容易合作`
@@ -351,7 +351,7 @@
原因: 原因:
- 这些字段属于系统运行结构,不属于陶泥主自然的创作语言 - 这些字段属于系统运行结构,不属于百梦主自然的创作语言
- 直接让玩家填,会把工具变成只有懂系统的人才能用 - 直接让玩家填,会把工具变成只有懂系统的人才能用
## 6.3 不应该要求玩家逐个补完所有人物设定字段 ## 6.3 不应该要求玩家逐个补完所有人物设定字段
@@ -378,7 +378,7 @@
## 7. 推荐的创作输入形态 ## 7. 推荐的创作输入形态
要让非专业陶泥主也能高自由度创作,输入形态必须改成“自然语言创作卡”,而不是“系统字段表单”。 要让非专业百梦主也能高自由度创作,输入形态必须改成“自然语言创作卡”,而不是“系统字段表单”。
## 7.1 世界层卡片 ## 7.1 世界层卡片
@@ -397,10 +397,10 @@
## 7.2 每张卡片都允许 3 种输入方式 ## 7.2 每张卡片都允许 3 种输入方式
1. 一句话自由输入 1. 一句话自由输入
- 适合低门槛陶泥 - 适合低门槛百梦
2. 标签 / 选项 / 语气滑条 2. 标签 / 选项 / 语气滑条
- 适合不想写太多字的陶泥 - 适合不想写太多字的百梦
3. 高级补充 3. 高级补充
- 适合愿意继续深挖的人 - 适合愿意继续深挖的人
@@ -414,7 +414,7 @@
这是高创作自由度里非常关键的一点。 这是高创作自由度里非常关键的一点。
陶泥主应当能: 百梦主应当能:
- 锁定一个角色 - 锁定一个角色
- 锁定一个地点 - 锁定一个地点
@@ -422,13 +422,13 @@
- 只重生成未锁定部分 - 只重生成未锁定部分
- 围绕锁定内容重写其余世界 - 围绕锁定内容重写其余世界
否则陶泥主每次调用 AI都会有“好不容易想好的东西被洗掉”的感受。 否则百梦主每次调用 AI都会有“好不容易想好的东西被洗掉”的感受。
## 8. 面向当前仓库的结构映射建议 ## 8. 面向当前仓库的结构映射建议
为了便于后续落实现有系统,这份边界建议可以直接映射到当前结构: 为了便于后续落实现有系统,这份边界建议可以直接映射到当前结构:
## 8.1 陶泥主输入层 ## 8.1 百梦主输入层
建议主要映射到: 建议主要映射到:
@@ -445,7 +445,7 @@
## 8.2 AI 编译层 ## 8.2 AI 编译层
由 AI / 系统从陶泥主输入自动补出: 由 AI / 系统从百梦主输入自动补出:
- `themePack` - `themePack`
- `storyGraph` - `storyGraph`
@@ -465,7 +465,7 @@
- `CarrierStoryFingerprint` - `CarrierStoryFingerprint`
- `StorySignal` - `StorySignal`
这些内容应该是“系统如何把世界跑起来”,不是“陶泥主必须亲手写完的创作内容”。 这些内容应该是“系统如何把世界跑起来”,不是“百梦主必须亲手写完的创作内容”。
## 9. 产品层面的最终结论 ## 9. 产品层面的最终结论
@@ -480,12 +480,12 @@
它应该做成这样: 它应该做成这样:
1. 陶泥主决定世界的灵魂锚点。 1. 百梦主决定世界的灵魂锚点。
2. 陶泥主重点塑造少量关键人、关键地、关键冲突、关键物。 2. 百梦主重点塑造少量关键人、关键地、关键冲突、关键物。
3. AI 围绕这些锚点批量展开长尾内容。 3. AI 围绕这些锚点批量展开长尾内容。
4. 系统把这些内容编译成可运行的图谱、可见性、任务、物件和关系结构。 4. 系统把这些内容编译成可运行的图谱、可见性、任务、物件和关系结构。
5. 陶泥主随时可以锁定核心创意,并局部重生成其余部分。 5. 百梦主随时可以锁定核心创意,并局部重生成其余部分。
一句话收束: 一句话收束:
**陶泥主应该写“这个世界为什么动人”AI 应该负责“让这个世界长出来并跑起来”。** **百梦主应该写“这个世界为什么动人”AI 应该负责“让这个世界长出来并跑起来”。**

View File

@@ -6,17 +6,17 @@
这份文档用于回答一个更具体的问题: 这份文档用于回答一个更具体的问题:
**参考 RPG 专业剧情策划全流程后,在自定义世界创作工具里,哪些设定必须要求陶泥主手动填写,哪些设定应该由 AI 先生成但允许陶泥主修改,哪些设定应完全交给系统托管,才能在“尽可能降低门槛”和“尽可能提高作品质量”之间取一个平衡。** **参考 RPG 专业剧情策划全流程后,在自定义世界创作工具里,哪些设定必须要求百梦主手动填写,哪些设定应该由 AI 先生成但允许百梦主修改,哪些设定应完全交给系统托管,才能在“尽可能降低门槛”和“尽可能提高作品质量”之间取一个平衡。**
这份文档不再只回答“陶泥主与 AI 怎么分工”,而是进一步把创作工作台收束成一个更可执行的三层输入结构: 这份文档不再只回答“百梦主与 AI 怎么分工”,而是进一步把创作工作台收束成一个更可执行的三层输入结构:
1. 陶泥主必须手填的高杠杆锚点 1. 百梦主必须手填的高杠杆锚点
2. AI 先生成、陶泥主可修改的内容草稿层 2. AI 先生成、百梦主可修改的内容草稿层
3. 系统自动编译和运行的托管层 3. 系统自动编译和运行的托管层
一句话结论: 一句话结论:
**让陶泥主只负责决定作品的灵魂、视角、冲突和关系钩子,让 AI 负责把这些锚点展开成可编辑的剧情草稿,让系统负责把草稿编译成可运行的结构。** **让百梦主只负责决定作品的灵魂、视角、冲突和关系钩子,让 AI 负责把这些锚点展开成可编辑的剧情草稿,让系统负责把草稿编译成可运行的结构。**
--- ---
@@ -25,27 +25,27 @@
这套平衡设计要同时满足 5 个目标: 这套平衡设计要同时满足 5 个目标:
1. 低门槛 1. 低门槛
-陶泥主不需要写长篇设定,也不需要理解底层系统结构。 -百梦主不需要写长篇设定,也不需要理解底层系统结构。
2. 高辨识度 2. 高辨识度
- 陶泥主写出来的世界,不应该只是“像一个世界”,而应该保留明显的个人方向。 - 百梦主写出来的世界,不应该只是“像一个世界”,而应该保留明显的个人方向。
3. 高可编辑性 3. 高可编辑性
- AI 不能一次生成后就不可控,陶泥主必须能改关键对象、关键关系和关键章节。 - AI 不能一次生成后就不可控,百梦主必须能改关键对象、关键关系和关键章节。
4. 高稳定性 4. 高稳定性
- 任务、章节、关系、物件和可见性等运行层结构不能依赖陶泥主手填专业字段。 - 任务、章节、关系、物件和可见性等运行层结构不能依赖百梦主手填专业字段。
5. 可扩展 5. 可扩展
- 愿意深挖的陶泥主可以继续补充世界上限,不愿深挖的人也能快速产出质量不错的作品。 - 愿意深挖的百梦主可以继续补充世界上限,不愿深挖的人也能快速产出质量不错的作品。
--- ---
## 2. 核心原则 ## 2. 核心原则
## 2.1 陶泥主手填的必须是“高杠杆决策”,不是“高工作量字段” ## 2.1 百梦主手填的必须是“高杠杆决策”,不是“高工作量字段”
应该要求陶泥主手填的内容,必须同时满足下面两个条件: 应该要求百梦主手填的内容,必须同时满足下面两个条件:
1. 会显著决定作品气质和辨识度 1. 会显著决定作品气质和辨识度
2. AI 很难替代判断 2. AI 很难替代判断
@@ -67,9 +67,9 @@
- 章节拆分 - 章节拆分
- 运行时信号结构 - 运行时信号结构
## 2.2 陶泥主可改层应该承接“专业策划初稿”,而不是“原始底层字段” ## 2.2 百梦主可改层应该承接“专业策划初稿”,而不是“原始底层字段”
AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而应该是一批已经成形的内容卡片,例如: AI 生成后允许百梦主修改的,不应该是一堆技术型字段,而应该是一批已经成形的内容卡片,例如:
- 关键角色卡 - 关键角色卡
- 势力卡 - 势力卡
@@ -81,11 +81,11 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
也就是说: 也就是说:
**AI 先给陶泥主一个像策划初稿的东西,而不是给一堆系统字段让陶泥主自己拼。** **AI 先给百梦主一个像策划初稿的东西,而不是给一堆系统字段让百梦主自己拼。**
## 2.3 系统托管层必须彻底隐藏专业运行结构 ## 2.3 系统托管层必须彻底隐藏专业运行结构
以下这类结构不应该默认要求陶泥主理解或编辑: 以下这类结构不应该默认要求百梦主理解或编辑:
- `ThemePack` - `ThemePack`
- `WorldStoryGraph` - `WorldStoryGraph`
@@ -98,7 +98,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
- 稀有度映射 - 稀有度映射
- 掉落和 build 权重 - 掉落和 build 权重
陶泥主应该编辑的是自然语言与内容卡,而不是运行时图结构。 百梦主应该编辑的是自然语言与内容卡,而不是运行时图结构。
## 2.4 先少量必填,再逐层展开 ## 2.4 先少量必填,再逐层展开
@@ -107,9 +107,9 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
```text ```text
先填最小必填卡 先填最小必填卡
-> AI 生成世界初稿 -> AI 生成世界初稿
-> 陶泥主修改关键对象 -> 百梦主修改关键对象
-> 系统继续展开长尾 -> 系统继续展开长尾
-> 陶泥主决定是否进入高级补充 -> 百梦主决定是否进入高级补充
``` ```
## 2.5 默认清爽,深度能力后置 ## 2.5 默认清爽,深度能力后置
@@ -127,27 +127,27 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 3. 最终建议:三层分工 ## 3. 最终建议:三层分工
## 3.1 第一层:必须要求陶泥主手动填写 ## 3.1 第一层:必须要求百梦主手动填写
这一层只保留最影响作品质量的高杠杆锚点,建议默认强制填写 6 张卡。 这一层只保留最影响作品质量的高杠杆锚点,建议默认强制填写 6 张卡。
## 3.2 第二层AI 生成后支持陶泥主修改 ## 3.2 第二层AI 生成后支持百梦主修改
这一层由 AI 根据第一层锚点自动展开成专业剧情策划初稿,陶泥主可以逐项修改、锁定、局部重生成。 这一层由 AI 根据第一层锚点自动展开成专业剧情策划初稿,百梦主可以逐项修改、锁定、局部重生成。
## 3.3 第三层:其余都交给系统 ## 3.3 第三层:其余都交给系统
这一层是把前两层编译成可运行游戏结构所需的系统字段、数值和运行时指令,默认不要求陶泥主处理。 这一层是把前两层编译成可运行游戏结构所需的系统字段、数值和运行时指令,默认不要求百梦主处理。
--- ---
## 4. 最低门槛方案:只强制手填 6 张卡 ## 4. 最低门槛方案:只强制手填 6 张卡
如果目标是尽可能降低门槛,同时又保留作品辨识度,建议只强制陶泥主填写以下 6 张卡。 如果目标是尽可能降低门槛,同时又保留作品辨识度,建议只强制百梦主填写以下 6 张卡。
## 4.1 卡 1世界一句话与核心幻想 ## 4.1 卡 1世界一句话与核心幻想
陶泥主必须手填: 百梦主必须手填:
- 世界一句话设定 - 世界一句话设定
- 玩家来到这个世界最想体验的感觉 - 玩家来到这个世界最想体验的感觉
@@ -165,7 +165,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 4.2 卡 2玩家身份与开局困境 ## 4.2 卡 2玩家身份与开局困境
陶泥主必须手填: 百梦主必须手填:
- 玩家是谁 - 玩家是谁
- 玩家开局最缺什么 - 玩家开局最缺什么
@@ -179,7 +179,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 4.3 卡 3主题气质与禁忌边界 ## 4.3 卡 3主题气质与禁忌边界
陶泥主必须手填: 百梦主必须手填:
- 主题关键词 - 主题关键词
- 情绪基调 - 情绪基调
@@ -199,7 +199,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 4.4 卡 4核心冲突 ## 4.4 卡 4核心冲突
陶泥主必须手填: 百梦主必须手填:
- 当前世界最重要的 `1~3` 个明面冲突 - 当前世界最重要的 `1~3` 个明面冲突
- 至少 `1` 个隐藏问题或暗面危机 - 至少 `1` 个隐藏问题或暗面危机
@@ -212,9 +212,9 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 4.5 卡 5关键关系钩子 ## 4.5 卡 5关键关系钩子
这里不强制陶泥主一开始填写完整角色档案,只要求填写更高杠杆的“关系骨架”。 这里不强制百梦主一开始填写完整角色档案,只要求填写更高杠杆的“关系骨架”。
陶泥主必须手填: 百梦主必须手填:
- `2~4` 条关键关系钩子 - `2~4` 条关键关系钩子
- 每条钩子至少说明: - 每条钩子至少说明:
@@ -229,7 +229,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 4.6 卡 6标志性要素与硬规则 ## 4.6 卡 6标志性要素与硬规则
陶泥主必须手填: 百梦主必须手填:
- `2~5` 个标志性要素 - `2~5` 个标志性要素
- 物件 - 物件
@@ -247,11 +247,11 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
--- ---
## 5. 不建议强制手填,但应该让 AI 生成后支持陶泥主修改的设定 ## 5. 不建议强制手填,但应该让 AI 生成后支持百梦主修改的设定
这一层是平衡“低门槛”和“高质量”的关键。 这一层是平衡“低门槛”和“高质量”的关键。
陶泥主不需要从零填写这些内容,但 AI 生成后必须能看、能改、能锁定、能局部重生成。 百梦主不需要从零填写这些内容,但 AI 生成后必须能看、能改、能锁定、能局部重生成。
## 5.1 世界外观层 ## 5.1 世界外观层
@@ -282,7 +282,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 势力很重要,但让新手一开始手写完整势力表太重 - 势力很重要,但让新手一开始手写完整势力表太重
- 更合理的做法是让 AI 基于核心冲突先出草稿,再由陶泥主修正 - 更合理的做法是让 AI 基于核心冲突先出草稿,再由百梦主修正
## 5.3 关键角色层 ## 5.3 关键角色层
@@ -302,8 +302,8 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 陶泥主已经通过“关系钩子”给出最关键的人物骨架 - 百梦主已经通过“关系钩子”给出最关键的人物骨架
- AI 负责把钩子展开成可编辑角色卡,陶泥主再做精修 - AI 负责把钩子展开成可编辑角色卡,百梦主再做精修
## 5.4 关键地点层 ## 5.4 关键地点层
@@ -319,7 +319,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 地点是世界感的重要来源 - 地点是世界感的重要来源
- 但新陶泥主未必能一开始就写出完整地点网络 - 但新百梦主未必能一开始就写出完整地点网络
## 5.5 世界线程层 ## 5.5 世界线程层
@@ -335,7 +335,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 线程是专业剧情结构,适合 AI 先搭骨架 - 线程是专业剧情结构,适合 AI 先搭骨架
-陶泥主必须有权修正哪条线更重要、哪条线该隐藏 -百梦主必须有权修正哪条线更重要、哪条线该隐藏
## 5.6 主线章节层 ## 5.6 主线章节层
@@ -350,9 +350,9 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 陶泥主已经给出了世界目标、冲突和关系 - 百梦主已经给出了世界目标、冲突和关系
- AI 可以先把它们编成主线章节初稿 - AI 可以先把它们编成主线章节初稿
- 陶泥主再选择保留、删减或重排 - 百梦主再选择保留、删减或重排
## 5.7 支线、角色线、阵营线层 ## 5.7 支线、角色线、阵营线层
@@ -367,7 +367,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 这是最适合 AI 拉开内容宽度的部分 - 这是最适合 AI 拉开内容宽度的部分
- 也是最需要陶泥主局部精修的部分 - 也是最需要百梦主局部精修的部分
## 5.8 场景章节层 ## 5.8 场景章节层
@@ -384,7 +384,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 当前项目已经在走“场景 = 章节单元”的方向 - 当前项目已经在走“场景 = 章节单元”的方向
- 这层非常适合 AI 编排出第一版,再由陶泥主补强记忆点 - 这层非常适合 AI 编排出第一版,再由百梦主补强记忆点
## 5.9 叙事载体层 ## 5.9 叙事载体层
@@ -397,7 +397,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
- 场景遗物 - 场景遗物
- 怪物命名及其故事指向 - 怪物命名及其故事指向
陶泥主主要修改: 百梦主主要修改:
- 哪些载体最重要 - 哪些载体最重要
- 哪些载体和哪条线程绑定 - 哪些载体和哪条线程绑定
@@ -417,13 +417,13 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 这些内容适合 AI 批量铺量 - 这些内容适合 AI 批量铺量
- 陶泥主只需要挑、改、锁定,不必从零起草 - 百梦主只需要挑、改、锁定,不必从零起草
--- ---
## 6. 其余设定应交给系统托管 ## 6. 其余设定应交给系统托管
以下内容不建议默认暴露给陶泥主编辑,应由系统根据前两层自动编译和维护。 以下内容不建议默认暴露给百梦主编辑,应由系统根据前两层自动编译和维护。
## 6.1 题材与术语编译层 ## 6.1 题材与术语编译层
@@ -450,7 +450,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 陶泥主要的是“故事线能对”,不是维护图数据库 - 百梦主要的是“故事线能对”,不是维护图数据库
## 6.3 可见性和 prompt 裁剪层 ## 6.3 可见性和 prompt 裁剪层
@@ -465,7 +465,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
原因: 原因:
- 这层必须稳定、严格、自动化 - 这层必须稳定、严格、自动化
- 不适合依赖陶泥主手动维护 - 不适合依赖百梦主手动维护
## 6.4 运行时导演层 ## 6.4 运行时导演层
@@ -494,7 +494,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
说明: 说明:
- 陶泥主可以编辑“任务卡”和“章节卡” - 百梦主可以编辑“任务卡”和“章节卡”
- 但不应默认编辑底层 contract 结构 - 但不应默认编辑底层 contract 结构
## 6.6 数值与配置层 ## 6.6 数值与配置层
@@ -511,7 +511,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
说明: 说明:
- 陶泥主可以给“偏向” - 百梦主可以给“偏向”
- 系统负责编译成具体数值 - 系统负责编译成具体数值
## 6.7 QA 与一致性层 ## 6.7 QA 与一致性层
@@ -547,7 +547,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
| 主线 | 不强制首轮手写完整主线 | 幕结构、章节卡、高潮与 handoff | 章节状态编译 | | 主线 | 不强制首轮手写完整主线 | 幕结构、章节卡、高潮与 handoff | 章节状态编译 |
| 支线/角色线 | 不强制首轮手写完整矩阵 | 支线种子、角色线事件、阵营线分歧 | 任务 contract 编译 | | 支线/角色线 | 不强制首轮手写完整矩阵 | 支线种子、角色线事件、阵营线分歧 | 任务 contract 编译 |
| 场景章节 | 不强制首轮手写全量章节 | 场景章节卡、阶段内容、章节载体 | signal 与导演层 | | 场景章节 | 不强制首轮手写全量章节 | 场景章节卡、阶段内容、章节载体 | signal 与导演层 |
| 运行时结构 | 不建议陶泥主接触 | 不建议默认编辑 | 可见性、导演、信号、编译、QA | | 运行时结构 | 不建议百梦主接触 | 不建议默认编辑 | 可见性、导演、信号、编译、QA |
--- ---
@@ -555,7 +555,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 8.1 第一步:只填写最小必填集 ## 8.1 第一步:只填写最小必填集
陶泥主只需要完成: 百梦主只需要完成:
1. 世界一句话与核心幻想 1. 世界一句话与核心幻想
2. 玩家身份与开局困境 2. 玩家身份与开局困境
@@ -584,9 +584,9 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
这里的重点不是一次补满全世界,而是先形成一个像样的内容骨架。 这里的重点不是一次补满全世界,而是先形成一个像样的内容骨架。
## 8.3 第三步:陶泥主只精修高价值卡片 ## 8.3 第三步:百梦主只精修高价值卡片
建议默认优先让陶泥主编辑这 4 类卡片: 建议默认优先让百梦主编辑这 4 类卡片:
1. 关键角色 1. 关键角色
2. 核心冲突与线程 2. 核心冲突与线程
@@ -606,7 +606,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
- 任务包装 - 任务包装
- 文案变体 - 文案变体
## 8.5 第五步:陶泥主按需进入高级模式 ## 8.5 第五步:百梦主按需进入高级模式
高级模式只对愿意深挖的人开放: 高级模式只对愿意深挖的人开放:
@@ -665,7 +665,7 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
## 10.2 每张卡只保留自然语言输入 ## 10.2 每张卡只保留自然语言输入
不要强迫陶泥主在首轮填写: 不要强迫百梦主在首轮填写:
- tags - tags
- ids - ids
@@ -676,20 +676,20 @@ AI 生成后允许陶泥主修改的,不应该是一堆技术型字段,而
更合理的做法是: 更合理的做法是:
-陶泥主输入自然语言或选择直觉标签 -百梦主输入自然语言或选择直觉标签
- 再由系统编译成结构化字段 - 再由系统编译成结构化字段
## 10.3 首轮生成后默认先看“精修建议” ## 10.3 首轮生成后默认先看“精修建议”
AI 初稿生成后,不应该把陶泥主直接扔进一个大编辑器。 AI 初稿生成后,不应该把百梦主直接扔进一个大编辑器。
更好的做法是先给出: 更好的做法是先给出:
1. 哪些卡片最值得改 1. 哪些卡片最值得改
2. 哪些内容已经比较稳定 2. 哪些内容已经比较稳定
3. 哪些内容仍然偏泛,需要陶泥主补个性 3. 哪些内容仍然偏泛,需要百梦主补个性
这样能明显提高陶泥主的修改效率。 这样能明显提高百梦主的修改效率。
## 10.4 移动端优先只保留高杠杆操作 ## 10.4 移动端优先只保留高杠杆操作
@@ -707,15 +707,15 @@ AI 初稿生成后,不应该把陶泥主直接扔进一个大编辑器。
## 11. 最后结论 ## 11. 最后结论
如果目标是在自定义世界创作中真正平衡“降低门槛”和“提高作品质量”,最好的做法不是让陶泥主填更多字段,也不是把一切都交给 AI。 如果目标是在自定义世界创作中真正平衡“降低门槛”和“提高作品质量”,最好的做法不是让百梦主填更多字段,也不是把一切都交给 AI。
更合理的平衡是: 更合理的平衡是:
1. 陶泥主必须手填最小但高杠杆的 6 张卡,掌握世界灵魂。 1. 百梦主必须手填最小但高杠杆的 6 张卡,掌握世界灵魂。
2. AI 根据这 6 张卡生成一套可编辑的专业剧情初稿,负责把骨架展开成角色、地点、线程、章节和载体。 2. AI 根据这 6 张卡生成一套可编辑的专业剧情初稿,负责把骨架展开成角色、地点、线程、章节和载体。
3. 陶泥主只精修最有价值的关键对象,锁定真正重要的内容。 3. 百梦主只精修最有价值的关键对象,锁定真正重要的内容。
4. 其余运行结构、数值、可见性、任务编译和 QA 检查都交给系统托管。 4. 其余运行结构、数值、可见性、任务编译和 QA 检查都交给系统托管。
一句话收束: 一句话收束:
**陶泥主负责决定“这个世界为什么值得被创作”AI 负责把它整理成可修改的策划初稿,系统负责把它稳定地跑成一个游戏世界。** **百梦主负责决定“这个世界为什么值得被创作”AI 负责把它整理成可修改的策划初稿,系统负责把它稳定地跑成一个游戏世界。**

View File

@@ -10,7 +10,7 @@
- 基于“最小必填锚点 + AI 初稿卡片 + 系统托管层”的结构化创作方案 - 基于“最小必填锚点 + AI 初稿卡片 + 系统托管层”的结构化创作方案
2. 纯 Agent 式方向 2. 纯 Agent 式方向
- 以前台对话为唯一主交互,陶泥主主要通过和 Agent 聊天来完成世界构建、角色塑造、剧情扩展和修改 - 以前台对话为唯一主交互,百梦主主要通过和 Agent 聊天来完成世界构建、角色塑造、剧情扩展和修改
文档需要回答 3 个问题: 文档需要回答 3 个问题:
@@ -34,7 +34,7 @@
当前方案的核心是: 当前方案的核心是:
1. 陶泥主手填最小高杠杆锚点 1. 百梦主手填最小高杠杆锚点
2. AI 生成一批可编辑的剧情策划初稿卡片 2. AI 生成一批可编辑的剧情策划初稿卡片
3. 系统把内容编译成运行时结构 3. 系统把内容编译成运行时结构
@@ -42,7 +42,7 @@
**结构化工作台 + AI 协作生成。** **结构化工作台 + AI 协作生成。**
陶泥主的主要行为是: 百梦主的主要行为是:
1. 填写关键卡片 1. 填写关键卡片
2. 修改关键角色、地点、势力、章节等内容卡 2. 修改关键角色、地点、势力、章节等内容卡
@@ -53,9 +53,9 @@
纯 Agent 式不是指“系统内部没有结构”,而是指: 纯 Agent 式不是指“系统内部没有结构”,而是指:
**陶泥主前台几乎不需要面对表单和卡片编辑器,主要通过自然语言对话来完成创作。** **百梦主前台几乎不需要面对表单和卡片编辑器,主要通过自然语言对话来完成创作。**
陶泥主的主要行为变成: 百梦主的主要行为变成:
1. 用自然语言描述世界想法 1. 用自然语言描述世界想法
2. 回答 Agent 的追问 2. 回答 Agent 的追问
@@ -77,7 +77,7 @@
1. 前台用户主要通过什么方式思考和输入? 1. 前台用户主要通过什么方式思考和输入?
2. 后台系统是否仍然有稳定的世界模型和编译层? 2. 后台系统是否仍然有稳定的世界模型和编译层?
3. 陶泥主是否还能看见摘要、锁定内容和修改范围? 3. 百梦主是否还能看见摘要、锁定内容和修改范围?
对当前项目来说,真正危险的不是“转成聊天”,而是: 对当前项目来说,真正危险的不是“转成聊天”,而是:
@@ -93,11 +93,11 @@
它更擅长: 它更擅长:
1. 帮不擅长表单和结构思考的陶泥主起步 1. 帮不擅长表单和结构思考的百梦主起步
2.陶泥主思路模糊时做追问和陪创作 2.百梦主思路模糊时做追问和陪创作
3. 把“我要做一个世界”变成一次自然聊天 3. 把“我要做一个世界”变成一次自然聊天
4. 动态决定追问深度,而不是一上来摆很多字段 4. 动态决定追问深度,而不是一上来摆很多字段
5.陶泥主感觉自己是在和一个懂 RPG 的剧情搭档共创 5.百梦主感觉自己是在和一个懂 RPG 的剧情搭档共创
## 2.2 纯 Agent 式的主要问题 ## 2.2 纯 Agent 式的主要问题
@@ -110,7 +110,7 @@
1. 聊天很多,但世界状态越来越难总览 1. 聊天很多,但世界状态越来越难总览
2. 角色、地点、势力和章节信息散落在多轮消息里 2. 角色、地点、势力和章节信息散落在多轮消息里
3. 锁定范围不清,重生成容易误伤已有内容 3. 锁定范围不清,重生成容易误伤已有内容
4. Agent 很容易“替陶泥主决定太多” 4. Agent 很容易“替百梦主决定太多”
5. 长会话越来越贵,越来越慢,也越来越容易漂移 5. 长会话越来越贵,越来越慢,也越来越容易漂移
## 2.3 对当前项目的判断 ## 2.3 对当前项目的判断
@@ -197,7 +197,7 @@
纯 Agent 式更弱的地方在于: 纯 Agent 式更弱的地方在于:
1. 世界模型隐藏得太深时,陶泥主会失去整体掌控感 1. 世界模型隐藏得太深时,百梦主会失去整体掌控感
2. 多轮对话后,已确定内容不容易被清晰回看 2. 多轮对话后,已确定内容不容易被清晰回看
3. 局部重做和精确编辑边界会变模糊 3. 局部重做和精确编辑边界会变模糊
4. Agent 容易过度代写、过度主导 4. Agent 容易过度代写、过度主导
@@ -223,7 +223,7 @@
因为这些环节的关键问题不是“字段如何摆放”,而是: 因为这些环节的关键问题不是“字段如何摆放”,而是:
**陶泥主有没有被真正引导出自己想做的世界。** **百梦主有没有被真正引导出自己想做的世界。**
## 4.2 不值得直接转成纯聊天黑箱的部分 ## 4.2 不值得直接转成纯聊天黑箱的部分
@@ -261,8 +261,8 @@
即使转成纯 Agent 式,也仍然要保留这三层: 即使转成纯 Agent 式,也仍然要保留这三层:
1. 陶泥主必须确认的高杠杆锚点 1. 百梦主必须确认的高杠杆锚点
2. AI 生成但允许陶泥主修改的策划初稿层 2. AI 生成但允许百梦主修改的策划初稿层
3. 系统托管的运行时编译层 3. 系统托管的运行时编译层
变化的只是: 变化的只是:
@@ -339,7 +339,7 @@
2. 会阶段性总结 2. 会阶段性总结
3. 会把聊天结果沉淀成结构化世界状态 3. 会把聊天结果沉淀成结构化世界状态
4. 会提醒风险和冲突 4. 会提醒风险和冲突
5. 会在陶泥主要求时进行局部重写和定向扩展 5. 会在百梦主要求时进行局部重写和定向扩展
## 6.2 正确理解 ## 6.2 正确理解
@@ -349,7 +349,7 @@
也就是说: 也就是说:
1. 陶泥主看到的是对话 1. 百梦主看到的是对话
2. 系统内部维护的是世界模型、锁定状态、摘要和编译结果 2. 系统内部维护的是世界模型、锁定状态、摘要和编译结果
--- ---
@@ -389,7 +389,7 @@ Agent 首轮不应该直接铺满全世界,而应该给出一份简明底稿
2. 建议内容 2. 建议内容
3. 待确认内容 3. 待确认内容
## 7.3 阶段 C陶泥主锁定锚点 ## 7.3 阶段 C百梦主锁定锚点
在纯 Agent 模式里,锁定行为必须被显式支持。 在纯 Agent 模式里,锁定行为必须被显式支持。
@@ -455,7 +455,7 @@ Agent 不应该每轮都继续扩全局,而应该支持“单对象工作模
| 结构 | 作用 | | 结构 | 作用 |
| --- | --- | | --- | --- |
| `creatorIntentProfile` | 当前陶泥主最初和最新的创作意图 | | `creatorIntentProfile` | 当前百梦主最初和最新的创作意图 |
| `lockedAnchors` | 已确认不可自动改写的内容 | | `lockedAnchors` | 已确认不可自动改写的内容 |
| `worldDraftSnapshot` | 当前世界底稿快照 | | `worldDraftSnapshot` | 当前世界底稿快照 |
| `editableDraftCards` | 角色、地点、势力、章节等可编辑初稿 | | `editableDraftCards` | 角色、地点、势力、章节等可编辑初稿 |
@@ -530,7 +530,7 @@ Agent 不能像问卷系统,也不能一次追问太多。
1. 一次最多追问 `1~3` 个问题 1. 一次最多追问 `1~3` 个问题
2. 问题必须是当前最缺的高杠杆信息 2. 问题必须是当前最缺的高杠杆信息
3. 每次追问都给默认建议方向 3. 每次追问都给默认建议方向
4. 如果陶泥主不想细答,允许 Agent 先代补一个版本再确认 4. 如果百梦主不想细答,允许 Agent 先代补一个版本再确认
这样才能保持“像聊天”,而不是“像客服表单”。 这样才能保持“像聊天”,而不是“像客服表单”。
@@ -614,14 +614,14 @@ Agent 应能识别这些常见修改类型:
3. 锁定内容固定展示 3. 锁定内容固定展示
4. 提供“当前世界圣经”入口 4. 提供“当前世界圣经”入口
## 11.2 风险 2Agent 过度代写,陶泥主失去作品归属感 ## 11.2 风险 2Agent 过度代写,百梦主失去作品归属感
防护方式: 防护方式:
1. 高杠杆锚点必须要求确认 1. 高杠杆锚点必须要求确认
2. 重要改动前先说“我准备改什么” 2. 重要改动前先说“我准备改什么”
3. 默认优先给多个候选,而不是直接盖写 3. 默认优先给多个候选,而不是直接盖写
4. 允许陶泥主随时回退到旧版本 4. 允许百梦主随时回退到旧版本
## 11.3 风险 3局部修改带出全局漂移 ## 11.3 风险 3局部修改带出全局漂移

View File

@@ -37,8 +37,8 @@
- 不能先删旧字段,再补新结构。 - 不能先删旧字段,再补新结构。
- 必须先补新设定层,再逐步迁读,最后再让旧模板字段退化成兼容层。 - 必须先补新设定层,再逐步迁读,最后再让旧模板字段退化成兼容层。
4. 不能增加陶泥主负担 4. 不能增加百梦主负担
- 这次不是让陶泥主多填一堆底层 schema。 - 这次不是让百梦主多填一堆底层 schema。
- 这些设定仍然应由 AI / 系统编译出来,只是所有权从模板世界转移到自定义世界自己。 - 这些设定仍然应由 AI / 系统编译出来,只是所有权从模板世界转移到自定义世界自己。
--- ---

View File

@@ -102,9 +102,9 @@
这不是真正跨题材,只是换了名字。 这不是真正跨题材,只是换了名字。
## 3.3 不能让陶泥主承担更多底层配置工作 ## 3.3 不能让百梦主承担更多底层配置工作
这次优化不是让陶泥主额外填写: 这次优化不是让百梦主额外填写:
- 怪物模板表 - 怪物模板表
- 场景参考池 - 场景参考池

View File

@@ -349,7 +349,7 @@ export interface ChapterProgressionPlan {
} }
``` ```
建议作为后端运行时编译结果缓存,不作为陶泥主直接编辑字段。 建议作为后端运行时编译结果缓存,不作为百梦主直接编辑字段。
## 3.7 章节经验记账 ## 3.7 章节经验记账
@@ -636,7 +636,7 @@ chapterXpBudget =
3. 非主角色友方 NPC 3. 非主角色友方 NPC
- `support``ambient` - `support``ambient`
如需修正,再允许章节蓝图加可选 override但不要求陶泥主每次手填。 如需修正,再允许章节蓝图加可选 override但不要求百梦主每次手填。
## 7.2 等级锚点 ## 7.2 等级锚点

View File

@@ -46,6 +46,13 @@ RpgEntryHomeView
3. 大鱼公开广场走 `list_big_fish_works(published_only=true)`;由于部分已部署模块会在公开列表分支前仍校验 `owner_user_id` 非空,客户端与模块内部公共列表输入都使用 `public-big-fish-gallery` 占位 owner。该字段在 `published_only` 分支不参与筛选,只用于兼容旧校验。 3. 大鱼公开广场走 `list_big_fish_works(published_only=true)`;由于部分已部署模块会在公开列表分支前仍校验 `owner_user_id` 非空,客户端与模块内部公共列表输入都使用 `public-big-fish-gallery` 占位 owner。该字段在 `published_only` 分支不参与筛选,只用于兼容旧校验。
4. 自定义世界公开广场走 `list_custom_world_gallery_entries`,当前主云数据为空时应返回成功空列表,而不是错误态。 4. 自定义世界公开广场走 `list_custom_world_gallery_entries`,当前主云数据为空时应返回成功空列表,而不是错误态。
### 2.4 作者头像读取策略
1. 首页“推荐”和“今日游戏”作品卡不把 `avatarUrl` 固化到作品读模型里,避免作者修改头像后旧作品卡继续展示过期头像。
2. 前端首页在聚合公开作品后,按作品摘要中的 `authorPublicUserCode` 优先读取公开用户摘要;没有公开用户码的玩法摘要,使用 `ownerUserId` 读取公开用户摘要。
3. 作者头像查询必须走匿名公开接口,并在首页组件内按作者维度缓存;单个作者在推荐与今日游戏中重复出现时不能重复请求。
4. 公开用户摘要返回 `avatarUrl` 时,作品卡展示真实头像;头像缺失、读取失败或作者身份字段缺失时,继续使用作者昵称首字占位。
## 3. 移动端布局 ## 3. 移动端布局
1. 移动端首页只在 `RpgEntryHomeView` 的 mobile content 内重排。 1. 移动端首页只在 `RpgEntryHomeView` 的 mobile content 内重排。

View File

@@ -96,7 +96,7 @@
- 同时开放短信与密码登录时,面板顶部展示两个居中的文字页签,当前页签使用深色字重和短下划线强调。 - 同时开放短信与密码登录时,面板顶部展示两个居中的文字页签,当前页签使用深色字重和短下划线强调。
- 只渲染当前页签对应的输入区;切换页签不弹出新面板,不展示二维码入口。 - 只渲染当前页签对应的输入区;切换页签不弹出新面板,不展示二维码入口。
- `短信登录` 页签包含手机号、验证码、获取验证码和主按钮。 - `短信登录` 页签包含手机号、验证码、获取验证码和主按钮。
- `密码登录` 页签只包含手机号、密码、主按钮和忘记密码入口;不支持邮箱、用户名或陶泥号。 - `密码登录` 页签只包含手机号、密码、主按钮和忘记密码入口;不支持邮箱、用户名或百梦号。
- 密码登录只是手机号验证码登录的补充方式:只有已登录并设置过密码的手机号账号才能使用,不能在密码页签创建账号。 - 密码登录只是手机号验证码登录的补充方式:只有已登录并设置过密码的手机号账号才能使用,不能在密码页签创建账号。
- `密码登录` 主按钮固定为 `登录`,不得使用 `注册/登录` - `密码登录` 主按钮固定为 `登录`,不得使用 `注册/登录`
- 未开放某个登录方式时不展示对应页签,避免用户进入不可用表单。 - 未开放某个登录方式时不展示对应页签,避免用户进入不可用表单。

View File

@@ -59,8 +59,8 @@
### 3.2 排版 ### 3.2 排版
- 平台层正文、按钮、说明、功能标签统一使用非像素字体 - 平台层正文、按钮、说明、功能标签统一使用非像素字体
- 左上角 `陶泥 / GENARRATIVE` 品牌字标允许单独做成像素化 logo - 左上角 `百梦 / GENARRATIVE` 品牌字标允许单独做成像素化 logo
- `GENARRATIVE``陶泥` 都优先直接使用游戏内同款 `Fusion Pixel` - `GENARRATIVE``百梦` 都优先直接使用游戏内同款 `Fusion Pixel`
- 品牌字标默认保持正常像素字观感,禁止再叠双层粗阴影或手动加粗到影响识别 - 品牌字标默认保持正常像素字观感,禁止再叠双层粗阴影或手动加粗到影响识别
- 品牌字标直接使用字体文件内原字形,不额外做运行时描字、轮廓拼字或伪粗体处理 - 品牌字标直接使用字体文件内原字形,不额外做运行时描字、轮廓拼字或伪粗体处理
- 主标题保留明显层级,但不再做像素描边效果 - 主标题保留明显层级,但不再做像素描边效果

View File

@@ -15,10 +15,10 @@
统一详情页只做作品展示与动作入口,不承担规则说明。 统一详情页只做作品展示与动作入口,不承担规则说明。
1. 顶部导航:返回按钮、标题“详情”、更多按钮占位;不展示“统计 / 详情 / 评价 / 论坛”Tab。 1. 顶部导航:返回按钮、标题“详情”、更多按钮占位;不展示“统计 / 详情 / 评价 / 论坛”Tab。
2. 封面区:固定 `16:9` 比例,默认使用作品封面图 `cover` 填满整块主视觉;背景可用同图弱化铺底;缺图时只显示平台主题底,不新增说明文字。拼图作品详情页若详情数据包含多个关卡图,则顶部封面区优先按关卡正式图轮播展示,每张图对应一个关卡;无可用关卡图时再回退作品封面图。 2. 封面区:固定 `16:9` 比例,默认使用作品封面图 `cover` 填满整块主视觉;背景可用同图弱化铺底;缺图时只显示平台主题底,不新增说明文字。拼图作品详情页若详情数据包含多个关卡图,则顶部封面区优先按关卡正式图轮播展示,每张图对应一个关卡;无可用关卡图时再回退作品封面图。拼图多关封面只默认展示第一张真实图,轮播节奏与左右切换保持不变;未解锁的后续封面必须使用同图毛玻璃模糊底和大问号图标遮罩,不能展示真实清晰图,也不能追加规则说明文字。玩家完成对应前置关卡后,当前详情页可按本次 `PuzzleRunSnapshot.clearedLevelCount + 1` 即时解锁可见封面数;刷新后持久化解锁应由后端从当前用户的拼图运行记录汇总到详情读模型,前端只消费读模型或当前 run 状态。
3. 移动端首页“推荐”和“今日游戏”列表中,只有最接近屏幕垂直中心的作品卡片进入封面轮播态;若该拼图作品有多张关卡封面,则按详情页同源封面序列自动轮换。用户滚动后,离开中心的旧卡片必须立即恢复首张封面,新中心卡片再开始轮播;“游戏分类”、排行、桌面端列表不启用该自动轮播。 3. 移动端首页“推荐”和“今日游戏”列表中,只有最接近屏幕垂直中心的作品卡片进入封面轮播态;若该拼图作品有多张关卡封面,则按详情页同源封面序列自动轮换。用户滚动后,离开中心的旧卡片必须立即恢复首张封面,新中心卡片再开始轮播;“游戏分类”、排行、桌面端列表不启用该自动轮播。
4. 基础信息区: 4. 基础信息区:
- 左侧作品图标使用作品封面或首图 - 左侧作品图标使用作品封面序列首图;顶部封面轮播切换时,该正方形图标保持首图不变,避免作品名称旁的身份标识跟随大图闪动
- 中间展示作品名、作者头像、作者名、玩法类型;作者头像读取公开用户资料 `avatarUrl`,缺失时使用作者昵称首字占位。 - 中间展示作品名、作者头像、作者名、玩法类型;作者头像读取公开用户资料 `avatarUrl`,缺失时使用作者昵称首字占位。
- 右侧原 TapTap 评分位置替换为 `点赞` 按钮;点击后调用后端点赞接口,由后端记录当前登录用户对该公开作品的点赞关系并返回更新后的真实 `likeCount` 读模型,前端不伪造点赞增长。 - 右侧原 TapTap 评分位置替换为 `点赞` 按钮;点击后调用后端点赞接口,由后端记录当前登录用户对该公开作品的点赞关系并返回更新后的真实 `likeCount` 读模型,前端不伪造点赞增长。
5. 统计区固定四项: 5. 统计区固定四项:
@@ -29,6 +29,7 @@
- 四项统计需要使用浅色图标底强化识别,但不得追加规则说明类文案。 - 四项统计需要使用浅色图标底强化识别,但不得追加规则说明类文案。
6. 简介区:展示玩法标签和作品简介;不追加说明类文案。 6. 简介区:展示玩法标签和作品简介;不追加说明类文案。
7. 底部动作:左侧按钮为“作品改造”,右侧主按钮为“启动”;两个按钮必须位于同一行,点击“启动”后进入对应玩法运行态并记录游玩次数。 7. 底部动作:左侧按钮为“作品改造”,右侧主按钮为“启动”;两个按钮必须位于同一行,点击“启动”后进入对应玩法运行态并记录游玩次数。
- 未登录用户可进入并浏览作品详情页,但点击“作品改造”和“启动”都必须先弹出登录入口面板;登录成功后自动继续刚才点击的动作,不直接发起 Remix、启动 run 或本地运行态。
8. 页面配色必须跟随平台明暗主题变量;亮色主题使用平台浅色底、深色文字和主按钮渐变,暗色主题使用平台暗色底、亮色文字和对应主按钮渐变,不在详情页写死独立黑色皮肤。 8. 页面配色必须跟随平台明暗主题变量;亮色主题使用平台浅色底、深色文字和主按钮渐变,暗色主题使用平台暗色底、亮色文字和对应主按钮渐变,不在详情页写死独立黑色皮肤。
9. 字号规范跟随平台页面既有节奏:标题/主按钮使用 `1rem` 级别,作品名使用卡片标题同级 `1rem`,辅助信息与简介使用 `0.8125rem` / `0.875rem`,标签与统计标签使用 `0.75rem`,避免在详情页使用随视口放大的独立大字号。 9. 字号规范跟随平台页面既有节奏:标题/主按钮使用 `1rem` 级别,作品名使用卡片标题同级 `1rem`,辅助信息与简介使用 `0.8125rem` / `0.875rem`,标签与统计标签使用 `0.75rem`,避免在详情页使用随视口放大的独立大字号。
@@ -100,5 +101,6 @@
4. Remix 后原作品改造次数增加,新草稿归当前用户所有,且不会继承源作品统计。 4. Remix 后原作品改造次数增加,新草稿归当前用户所有,且不会继承源作品统计。
5. 点赞公开作品会走对应后端记录入口,首次点赞后刷新仍能看到递增后的点赞次数,重复点赞不会继续增加。 5. 点赞公开作品会走对应后端记录入口,首次点赞后刷新仍能看到递增后的点赞次数,重复点赞不会继续增加。
6. 启动公开作品会走对应后端记录入口,刷新后仍能看到递增后的游玩次数。 6. 启动公开作品会走对应后端记录入口,刷新后仍能看到递增后的游玩次数。
7. 移动端首页“推荐”和“今日游戏”列表滚动时,仅中心卡片自动轮播多封面;旧中心卡离开后回到首张封面,新的中心卡接续轮播 7. 未登录进入作品详情页后,点击“作品改造”和“启动”只打开登录入口面板;登录成功后恢复对应动作,未登录期间不会创建 Remix 草稿、开始拼图 run、记录 RPG 游玩或启动大鱼本地运行态
8. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。 8. 移动端首页“推荐”和“今日游戏”列表滚动时,仅中心卡片自动轮播多封面;旧中心卡离开后回到首张封面,新的中心卡接续轮播。
9. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。

View File

@@ -4,7 +4,7 @@
## 文档列表 ## 文档列表
- [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](./CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md):自定义世界里陶泥主输入与 AI 分工边界设计。 - [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](./CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md):自定义世界里百梦主输入与 AI 分工边界设计。
- [CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md):自定义世界创作里“手填锚点 / AI 可改初稿 / 系统托管层”的平衡设计。 - [CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md):自定义世界创作里“手填锚点 / AI 可改初稿 / 系统托管层”的平衡设计。
- [CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md):纯 Agent 式创作工具与结构化工作台方案的优缺点对比,以及转型设计。 - [CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md):纯 Agent 式创作工具与结构化工作台方案的优缺点对比,以及转型设计。
- [CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md](./CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md):把自定义世界从武侠/仙侠模板依赖迁到跨题材通用设定层的优化设计。 - [CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md](./CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md):把自定义世界从武侠/仙侠模板依赖迁到跨题材通用设定层的优化设计。
@@ -29,8 +29,8 @@
- 做物品、Build、锻造相关需求时先看前两份。 - 做物品、Build、锻造相关需求时先看前两份。
- 做 RPG 全剧情规划、主支线矩阵、角色线、场景章节与剧情交付模板时,先看新增的全剧情策划流程。 - 做 RPG 全剧情规划、主支线矩阵、角色线、场景章节与剧情交付模板时,先看新增的全剧情策划流程。
- 做自定义世界创作工作台、陶泥主输入边界、AI 分工设计时,先看第一份。 - 做自定义世界创作工作台、百梦主输入边界、AI 分工设计时,先看第一份。
- 做“哪些内容必须让陶泥主手填、哪些适合 AI 先生成再改、哪些必须系统托管”这类分层设计时,优先看新增的输入平衡设计稿。 - 做“哪些内容必须让百梦主手填、哪些适合 AI 先生成再改、哪些必须系统托管”这类分层设计时,优先看新增的输入平衡设计稿。
- 做“是否应该转成纯 Agent 式创作工具、转了之后前后台各该怎么收口”这类产品方向评估时,优先看新增的纯 Agent 对比与转型设计稿。 - 做“是否应该转成纯 Agent 式创作工具、转了之后前后台各该怎么收口”这类产品方向评估时,优先看新增的纯 Agent 对比与转型设计稿。
- 做自定义世界去模板依赖、跨题材泛化、兼容迁移设计时,优先看新增的去模板化优化设计稿。 - 做自定义世界去模板依赖、跨题材泛化、兼容迁移设计时,优先看新增的去模板化优化设计稿。
- 做“模板依赖如何真正变成自定义世界自有设定层”的具体迁移方案时,优先看新增的自有设定层优化方案。 - 做“模板依赖如何真正变成自定义世界自有设定层”的具体迁移方案时,优先看新增的自有设定层优化方案。

View File

@@ -29,7 +29,7 @@
结论: 结论:
- 独立编辑器入口如果没有继续接入主流程,应及时物理删除,不要长期保留兼容壳 - 独立编辑器入口如果没有继续接入主流程,应及时物理删除,不要长期保留兼容壳
- 页签命名要贴近陶泥主语言,而不是内部实现命名 - 页签命名要贴近百梦主语言,而不是内部实现命名
### 2.2 NPC 视觉模块并入 NPC 编辑 ### 2.2 NPC 视觉模块并入 NPC 编辑
@@ -144,7 +144,7 @@
经验: 经验:
- 陶泥主并不关心 “function” 这个技术词,更关心“这个选项会发生什么” - 百梦主并不关心 “function” 这个技术词,更关心“这个选项会发生什么”
- 同类编辑器如果只给字段表单而没有模板起稿能力,复用效率会很低 - 同类编辑器如果只给字段表单而没有模板起稿能力,复用效率会很低
### 2.8 选项行为预览升级到实机回放 ### 2.8 选项行为预览升级到实机回放
@@ -217,7 +217,7 @@
- 预览面板要么都显示“实时状态” - 预览面板要么都显示“实时状态”
- 要么都显示“同一个阶段的快照” - 要么都显示“同一个阶段的快照”
- 混用实时值和预测值会让陶泥主误判 - 混用实时值和预测值会让百梦主误判
## 4. 这类项目里沉淀下来的方法论 ## 4. 这类项目里沉淀下来的方法论
@@ -245,7 +245,7 @@
- 不是所有字段都应该在所有行为类型下开放 - 不是所有字段都应该在所有行为类型下开放
- 如果某类行为最终不会直接读取某个字段,就应该禁用或弱化它 - 如果某类行为最终不会直接读取某个字段,就应该禁用或弱化它
- 否则陶泥主会错误地以为改动无效是 bug - 否则百梦主会错误地以为改动无效是 bug
### 4.4 模板比空白表单更重要 ### 4.4 模板比空白表单更重要

View File

@@ -25,7 +25,7 @@
3. 历史已发布作品必须能自动补齐 gallery 投影。 3. 历史已发布作品必须能自动补齐 gallery 投影。
- 公开列表读取 `list_custom_world_gallery_entries` 前,会扫描 `custom_world_profile` 中已发布且未删除的 profile。 - 公开列表读取 `list_custom_world_gallery_entries` 前,会扫描 `custom_world_profile` 中已发布且未删除的 profile。
- 若已发布 profile 缺少 `custom_world_gallery_entry`,或缺少公开作品码 / 作者陶泥号,会先补齐公开字段并同步 gallery 投影。 - 若已发布 profile 缺少 `custom_world_gallery_entry`,或缺少公开作品码 / 作者百梦号,会先补齐公开字段并同步 gallery 投影。
- 这样旧版本发布成功但未落入广场读模型的作品,在下一次首页 / 分类页读取公开列表时会自动出现。 - 这样旧版本发布成功但未落入广场读模型的作品,在下一次首页 / 分类页读取公开列表时会自动出现。
## 经验 ## 经验

View File

@@ -394,7 +394,7 @@ MVP 阶段不需要单独设置密码。
落地规则: 落地规则:
- 入参只允许 `phone``password`,不支持邮箱、用户名或陶泥号。 - 入参只允许 `phone``password`,不支持邮箱、用户名或百梦号。
- 手机号不存在时,不创建账号,返回统一的登录失败。 - 手机号不存在时,不创建账号,返回统一的登录失败。
- 手机号存在但账号未设置过密码时,不允许密码登录。 - 手机号存在但账号未设置过密码时,不允许密码登录。
- 首次设置密码只能在已登录账号中心内完成;用户必须先通过手机号验证码或已绑定手机号的微信账号进入已登录态。 - 首次设置密码只能在已登录账号中心内完成;用户必须先通过手机号验证码或已绑定手机号的微信账号进入已登录态。
@@ -734,7 +734,7 @@ MVP 阶段建议至少提供一个轻量账号中心,包含:
约束: 约束:
- 不支持邮箱、用户名或陶泥号。 - 不支持邮箱、用户名或百梦号。
- 不承担注册能力。 - 不承担注册能力。
- 只有已存在、已验证手机号、且 `passwordLoginEnabled=true` 的账号可以登录。 - 只有已存在、已验证手机号、且 `passwordLoginEnabled=true` 的账号可以登录。

View File

@@ -12,6 +12,7 @@
2. 管理数据、业务规则、权限校验和写操作继续统一走 `server-rs/crates/api-server` 2. 管理数据、业务规则、权限校验和写操作继续统一走 `server-rs/crates/api-server`
3. v1 只接管已有管理能力:管理员登录、当前管理员信息、服务/数据库概览、受控 API 调试、兑换码管理、注册邀请码管理。 3. v1 只接管已有管理能力:管理员登录、当前管理员信息、服务/数据库概览、受控 API 调试、兑换码管理、注册邀请码管理。
4. 保持管理端清爽、可扫读、移动端可用,不在界面堆大段规则说明。 4. 保持管理端清爽、可扫读、移动端可用,不在界面堆大段规则说明。
5. 发布包内由 Web 网关把独立后台前端挂到同域 `/admin/`Rust `api-server` 自身仍不恢复旧的 `GET /admin` 内嵌页面。
## 2. 用户与使用场景 ## 2. 用户与使用场景
@@ -54,7 +55,7 @@
- 调用 `POST /admin/api/profile/redeem-codes` 创建/更新兑换码。 - 调用 `POST /admin/api/profile/redeem-codes` 创建/更新兑换码。
- 调用 `POST /admin/api/profile/redeem-codes/disable` 停用兑换码。 - 调用 `POST /admin/api/profile/redeem-codes/disable` 停用兑换码。
- 支持 `public``unique``private` 三种模式。 - 支持 `public``unique``private` 三种模式。
- 私有码支持输入内部 `userId` 和公开陶泥号,提交给后端统一解析。 - 私有码支持输入内部 `userId` 和公开百梦号,提交给后端统一解析。
7. 邀请码管理页: 7. 邀请码管理页:
- 调用 `POST /admin/api/profile/invite-codes` 创建/更新注册邀请码。 - 调用 `POST /admin/api/profile/invite-codes` 创建/更新注册邀请码。
- 支持输入 JSON 对象 metadata提交前做基础 JSON 对象校验,最终校验以服务端为准。 - 支持输入 JSON 对象 metadata提交前做基础 JSON 对象校验,最终校验以服务端为准。
@@ -66,7 +67,7 @@
3. 不新增 SpacetimeDB 表结构。 3. 不新增 SpacetimeDB 表结构。
4. 不实现完整用户管理、作品审核、资产审核、充值订单后台。 4. 不实现完整用户管理、作品审核、资产审核、充值订单后台。
5. 不实现多角色权限体系、管理员 refresh session、多端会话管理。 5. 不实现多角色权限体系、管理员 refresh session、多端会话管理。
6. 不保留 `GET /admin` 同源内嵌页面作为正式后台入口。 6. 不保留 Rust `api-server` `GET /admin` 同源内嵌页面作为正式后台入口;部署态 `/admin/` 只允许由独立后台前端静态产物承接
## 4. 页面与交互要求 ## 4. 页面与交互要求
@@ -113,7 +114,7 @@ API 调试页是受控接口调试台,不是通用代理工具:
4. maxUses必须为正整数最终校验以服务端为准。 4. maxUses必须为正整数最终校验以服务端为准。
5. enabled创建/更新时可切换。 5. enabled创建/更新时可切换。
6. allowedUserIds私有码允许的内部用户 ID 列表。 6. allowedUserIds私有码允许的内部用户 ID 列表。
7. allowedPublicUserCodes私有码允许的公开陶泥号列表。 7. allowedPublicUserCodes私有码允许的公开百梦号列表。
提交成功后展示后端返回的兑换码记录;失败时展示后端错误消息。 提交成功后展示后端返回的兑换码记录;失败时展示后端错误消息。
@@ -144,7 +145,7 @@ API 调试页是受控接口调试台,不是通用代理工具:
5. API 调试页可通过后端调试接口访问 `/healthz` 5. API 调试页可通过后端调试接口访问 `/healthz`
6. 兑换码管理页可创建/更新、停用兑换码,并展示返回记录。 6. 兑换码管理页可创建/更新、停用兑换码,并展示返回记录。
7. 邀请码管理页可创建/更新注册邀请码,并展示返回记录。 7. 邀请码管理页可创建/更新注册邀请码,并展示返回记录。
8. `GET /admin` 保持 404不恢复旧内嵌页面。 8. 直连 Rust `api-server` `GET /admin` 保持 404不恢复旧内嵌页面;通过发布包 Web 网关访问 `/admin/` 时返回独立后台前端
9. `npm run check:encoding` 通过。 9. `npm run check:encoding` 通过。
## 7. 首版任务拆解 ## 7. 首版任务拆解

View File

@@ -31,7 +31,7 @@
大鱼吃小鱼玩法是一个 `Agent-First` 的轻量实时成长玩法创作链: 大鱼吃小鱼玩法是一个 `Agent-First` 的轻量实时成长玩法创作链:
**陶泥主先与 Agent 共创“进化母题、等级阶梯、生态风格与场地气质”,系统再自动编译出一个竖屏全屏、摇杆移动、吞噬收编、三合一进化、持续刷怪、升到最高级即通关的可运行玩法作品。** **百梦主先与 Agent 共创“进化母题、等级阶梯、生态风格与场地气质”,系统再自动编译出一个竖屏全屏、摇杆移动、吞噬收编、三合一进化、持续刷怪、升到最高级即通关的可运行玩法作品。**
--- ---
@@ -115,26 +115,26 @@
`Agent-First 大鱼吃小鱼玩法创作工具` `Agent-First 大鱼吃小鱼玩法创作工具`
玩法运行态对外展示名可由陶泥主自定义,不强绑平台内部域名。 玩法运行态对外展示名可由百梦主自定义,不强绑平台内部域名。
## 5.2 目标用户 ## 5.2 目标用户
目标用户主要是 3 类: 目标用户主要是 3 类:
1.陶泥 1.百梦
- 想快速做一个可玩的成长吞噬小游戏,但不懂完整关卡编辑器 - 想快速做一个可玩的成长吞噬小游戏,但不懂完整关卡编辑器
2. 视觉驱动型陶泥 2. 视觉驱动型百梦
- 更关心“每级长什么样、动作怎么样、背景氛围如何” - 更关心“每级长什么样、动作怎么样、背景氛围如何”
3. 玩法原型陶泥 3. 玩法原型百梦
- 想快速验证一套吞噬成长节奏、等级曲线和场地压迫感 - 想快速验证一套吞噬成长节奏、等级曲线和场地压迫感
## 5.3 成功标准 ## 5.3 成功标准
本期上线后,至少要满足下面这些结果: 本期上线后,至少要满足下面这些结果:
1. 陶泥主可在 `5~12` 分钟内通过 Agent 聊天形成一版可用锚点草稿。 1. 百梦主可在 `5~12` 分钟内通过 Agent 聊天形成一版可用锚点草稿。
2. 系统默认能编译出 `8` 级实体阶梯的初版玩法草稿。 2. 系统默认能编译出 `8` 级实体阶梯的初版玩法草稿。
3. 每一级实体都能在结果页单独生成和重生成主图。 3. 每一级实体都能在结果页单独生成和重生成主图。
4. 每一级实体都能在结果页单独生成和重生成动作。 4. 每一级实体都能在结果页单独生成和重生成动作。
@@ -179,9 +179,9 @@
Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事: Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事:
1.陶泥主明确高杠杆锚点 1.百梦主明确高杠杆锚点
2.陶泥主把模糊灵感总结成可编译结构 2.百梦主把模糊灵感总结成可编译结构
3.陶泥主收束出第一版等级阶梯与视觉方向 3.百梦主收束出第一版等级阶梯与视觉方向
## 7.2 前台交互原则 ## 7.2 前台交互原则
@@ -222,7 +222,7 @@ Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事:
3. `成长阶梯` 3. `成长阶梯`
- 这一玩法一共大致有几级,以及每一级如何逐步升级、变大、变强、变异 - 这一玩法一共大致有几级,以及每一级如何逐步升级、变大、变强、变异
- 最高级终局形态也并入这一锚点统一确定 - 最高级终局形态也并入这一锚点统一确定
-陶泥主没有明确总层数,本期默认按 `8` 级编译,可配置范围固定为 `6~12` -百梦主没有明确总层数,本期默认按 `8` 级编译,可配置范围固定为 `6~12`
4. `风险节奏` 4. `风险节奏`
- 玩家周围应该更偏压迫、平衡还是偏爽快 - 玩家周围应该更偏压迫、平衡还是偏爽快
@@ -235,7 +235,7 @@ Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事:
2. `等级总层数` 并入 `成长阶梯` 2. `等级总层数` 并入 `成长阶梯`
3. `升级轮廓` 并入 `成长阶梯` 3. `升级轮廓` 并入 `成长阶梯`
4. `终局形态` 并入 `成长阶梯` 4. `终局形态` 并入 `成长阶梯`
5. `开局成长方式` 改为系统固定规则,不再作为陶泥主锚点 5. `开局成长方式` 改为系统固定规则,不再作为百梦主锚点
后续 Agent 追问时,不再把这些内容拆成独立必答题。 后续 Agent 追问时,不再把这些内容拆成独立必答题。
@@ -302,7 +302,7 @@ Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事:
## 9.1 默认草稿规模 ## 9.1 默认草稿规模
陶泥主没有特别指定时,第一版玩法草稿必须默认编译为: 百梦主没有特别指定时,第一版玩法草稿必须默认编译为:
1. `8` 级实体阶梯 1. `8` 级实体阶梯
2. `1` 张活动区域背景图 2. `1` 张活动区域背景图

View File

@@ -680,7 +680,7 @@ assistant 回复应包含:
1. 对 seedText / 用户消息的简要复述 1. 对 seedText / 用户消息的简要复述
2. 当前仍缺哪些世界锚点 2. 当前仍缺哪些世界锚点
3. 建议陶泥主下一步回答什么 3. 建议百梦主下一步回答什么
#### 用户后续消息 #### 用户后续消息

View File

@@ -24,7 +24,7 @@
那么第二阶段的目标就是: 那么第二阶段的目标就是:
**让 Agent 会话真正开始理解陶泥主输入,并把自然语言聊天沉淀成结构化创作锚点。** **让 Agent 会话真正开始理解百梦主输入,并把自然语言聊天沉淀成结构化创作锚点。**
一句话定义: 一句话定义:

View File

@@ -26,7 +26,7 @@
那么第四阶段的目标就是: 那么第四阶段的目标就是:
**让陶泥主直接修改这版草稿设定,并且继续用 AI 为这版草稿扩出新的角色和场景。** **让百梦主直接修改这版草稿设定,并且继续用 AI 为这版草稿扩出新的角色和场景。**
一句话定义: 一句话定义:
@@ -100,7 +100,7 @@
一句话目标: 一句话目标:
**让第四阶段结束时,陶泥主第一次能像在真正做作品一样修改草稿、继续长出新对象。** **让第四阶段结束时,百梦主第一次能像在真正做作品一样修改草稿、继续长出新对象。**
--- ---

View File

@@ -200,7 +200,7 @@
1. 主线关键角色 1. 主线关键角色
2. 可扮演角色 2. 可扮演角色
3. 陶泥主重点想看的角色 3. 百梦主重点想看的角色
## 7.2 入口位置 ## 7.2 入口位置

View File

@@ -42,21 +42,21 @@
## 1.2 一句话定义 ## 1.2 一句话定义
陶泥主通过与一个懂 RPG 剧情策划方法的 Agent 对话,逐步完成世界锚点收集、关键对象塑造、剧情骨架搭建和长尾内容展开;同时由 Express 后端持续维护结构化世界状态、锁定边界、局部重生成和质量检查。 百梦主通过与一个懂 RPG 剧情策划方法的 Agent 对话,逐步完成世界锚点收集、关键对象塑造、剧情骨架搭建和长尾内容展开;同时由 Express 后端持续维护结构化世界状态、锁定边界、局部重生成和质量检查。
## 1.3 目标用户 ## 1.3 目标用户
目标用户分三类: 目标用户分三类:
1.陶泥 1.百梦
- 有世界灵感,但不擅长结构化填表 - 有世界灵感,但不擅长结构化填表
2. 中度陶泥 2. 中度百梦
- 愿意精修角色、地点、主线第一幕,但不想维护大量底层字段 - 愿意精修角色、地点、主线第一幕,但不想维护大量底层字段
3. 重度陶泥 3. 重度百梦
- 需要局部重生成、锁定、版本化和导出世界圣经 - 需要局部重生成、锁定、版本化和导出世界圣经
## 1.4 产品成功标准 ## 1.4 产品成功标准
@@ -76,7 +76,7 @@
1. 不把整套系统做成纯聊天黑箱。 1. 不把整套系统做成纯聊天黑箱。
2. 不让前端继续承担锁定合并、重生成裁决、结构编译等核心逻辑。 2. 不让前端继续承担锁定合并、重生成裁决、结构编译等核心逻辑。
3. 不要求陶泥主直接编辑 `ThemePack / WorldStoryGraph / VisibilitySlice / ThreadContract` 等运行时结构。 3. 不要求百梦主直接编辑 `ThemePack / WorldStoryGraph / VisibilitySlice / ThreadContract` 等运行时结构。
4. 不把长项目世界管理完全交给一条无限增长的聊天记录。 4. 不把长项目世界管理完全交给一条无限增长的聊天记录。
5. 不再保留“生成完直接回世界列表并自动保存”的旧流程。 5. 不再保留“生成完直接回世界列表并自动保存”的旧流程。
6. 不允许角色主图、角色动作、场景背景图继续停留在临时候选状态后直接发布世界。 6. 不允许角色主图、角色动作、场景背景图继续停留在临时候选状态后直接发布世界。
@@ -151,7 +151,7 @@
1. `src/services/customWorldCreatorIntent.ts` 1. `src/services/customWorldCreatorIntent.ts`
- 已有陶泥主意图、锚点包、锁定状态的基础结构 - 已有百梦主意图、锚点包、锁定状态的基础结构
2. `src/types/customWorld.ts` 2. `src/types/customWorld.ts`
@@ -228,7 +228,7 @@
后台必须持续维护: 后台必须持续维护:
1. 陶泥主意图 1. 百梦主意图
2. 锁定状态 2. 锁定状态
3. 世界底稿快照 3. 世界底稿快照
4. 可编辑草稿对象列表 4. 可编辑草稿对象列表
@@ -271,11 +271,11 @@
-> 打开 Agent 创作入口 -> 打开 Agent 创作入口
-> Agent 收集最小锚点 -> Agent 收集最小锚点
-> Agent 输出首轮世界底稿 -> Agent 输出首轮世界底稿
-> 陶泥主锁定/修改关键内容 -> 百梦主锁定/修改关键内容
-> Agent 局部生成关键角色/地点/主线第一幕 -> Agent 局部生成关键角色/地点/主线第一幕
-> 进入角色与场景资产工坊,生成主形象 / 动作 / 背景图 -> 进入角色与场景资产工坊,生成主形象 / 动作 / 背景图
-> Agent 扩展长尾内容 -> Agent 扩展长尾内容
-> 陶泥主发布世界 -> 百梦主发布世界
-> 保存到世界库并进入世界 -> 保存到世界库并进入世界
``` ```
@@ -2077,4 +2077,4 @@ Agent 会话每次 operation 完成后自动保存 session snapshot。
这次新创作工具的正确方向,不是把现有工作台换成一个更大的聊天框,而是: 这次新创作工具的正确方向,不是把现有工作台换成一个更大的聊天框,而是:
**让 Agent 成为陶泥主的主交互入口,让 Express 后端成为真正的世界状态管理者,让锁定、局部重生成、摘要、质量护栏和发布链把整个创作过程牢牢收住。** **让 Agent 成为百梦主的主交互入口,让 Express 后端成为真正的世界状态管理者,让锁定、局部重生成、摘要、质量护栏和发布链把整个创作过程牢牢收住。**

View File

@@ -37,15 +37,15 @@
## 1.3 目标用户 ## 1.3 目标用户
目标用户仍然是当前自定义世界创作工具的三类陶泥主,但本流程更偏向解决其中两类人的起步问题: 目标用户仍然是当前自定义世界创作工具的三类百梦主,但本流程更偏向解决其中两类人的起步问题:
1.陶泥 1.百梦
- 有模糊灵感,但不知道先想什么 - 有模糊灵感,但不知道先想什么
2. 中度陶泥 2. 中度百梦
- 有一些设定点子,但缺少把设定收束成可运行剧情骨架的方法 - 有一些设定点子,但缺少把设定收束成可运行剧情骨架的方法
重度陶泥主也可使用本流程,但他们更关心的是: 重度百梦主也可使用本流程,但他们更关心的是:
- Agent 是否会少问废话 - Agent 是否会少问废话
- 摘要是否准确 - 摘要是否准确
@@ -1190,7 +1190,7 @@ Agent 不应回复成八问表:
## 13.2 后续可编辑范围 ## 13.2 后续可编辑范围
进入世界底稿阶段后,陶泥主默认优先精修: 进入世界底稿阶段后,百梦主默认优先精修:
1. 关键角色 1. 关键角色
2. 核心冲突与线程 2. 核心冲突与线程

View File

@@ -8,11 +8,11 @@
目标不是推翻当前已经存在的多阶段生成链,而是解决下面这个核心错位: 目标不是推翻当前已经存在的多阶段生成链,而是解决下面这个核心错位:
**当前仓库已经开始把世界生成拆成 `framework -> themePack -> storyGraph -> role outline -> dossier -> narrativeProfile` 的分阶段 AI 编译流程,但陶泥主入口仍然是“一段大文本”,结果页又把大量低杠杆字段重新扔回给陶泥主人工兜底。** **当前仓库已经开始把世界生成拆成 `framework -> themePack -> storyGraph -> role outline -> dossier -> narrativeProfile` 的分阶段 AI 编译流程,但百梦主入口仍然是“一段大文本”,结果页又把大量低杠杆字段重新扔回给百梦主人工兜底。**
一句话定义本次优化: 一句话定义本次优化:
**让陶泥主先定义世界灵魂锚点,再让 AI / 系统围绕锚点分层生成、分层展开、分层可控地完成长尾内容。** **让百梦主先定义世界灵魂锚点,再让 AI / 系统围绕锚点分层生成、分层展开、分层可控地完成长尾内容。**
## 1. 当前流程现状 ## 1. 当前流程现状
@@ -64,7 +64,7 @@
## 1.3 当前流程的核心问题 ## 1.3 当前流程的核心问题
## 1.3.1 陶泥主入口过于粗糙 ## 1.3.1 百梦主入口过于粗糙
当前创建入口只有一块大文本输入框。 当前创建入口只有一块大文本输入框。
@@ -72,23 +72,23 @@
1. 不会写长描述的用户很难开局。 1. 不会写长描述的用户很难开局。
2. 愿意精细创作的用户没有结构化落点。 2. 愿意精细创作的用户没有结构化落点。
3. 系统无法明确分辨“哪些是陶泥主真正想锁定的锚点,哪些只是随口补充的描述”。 3. 系统无法明确分辨“哪些是百梦主真正想锁定的锚点,哪些只是随口补充的描述”。
结果就是: 结果就是:
**输入端自由但信息信号不稳定AI 虽然能生成很多内容,却不一定生成的是陶泥主真正关心的内容。** **输入端自由但信息信号不稳定AI 虽然能生成很多内容,却不一定生成的是百梦主真正关心的内容。**
## 1.3.2 陶泥主与 AI 的职责发生倒置 ## 1.3.2 百梦主与 AI 的职责发生倒置
当前流程实际上是: 当前流程实际上是:
- 陶泥主先写一段泛化设定 - 百梦主先写一段泛化设定
- AI 再把整个世界铺满 - AI 再把整个世界铺满
- 陶泥主最后回到结果页,人工修改大量角色、章节、技能、初始物品、场景连接等细节 - 百梦主最后回到结果页,人工修改大量角色、章节、技能、初始物品、场景连接等细节
这与“低创作门槛、高创作自由度”的目标相反。 这与“低创作门槛、高创作自由度”的目标相反。
因为真正应该由陶泥主控制的,是: 因为真正应该由百梦主控制的,是:
- 世界核心命题 - 世界核心命题
- 主题与气质 - 主题与气质
@@ -98,7 +98,7 @@
- 关键地点 - 关键地点
- 标志性物件 / 怪物 / 禁忌 - 标志性物件 / 怪物 / 禁忌
而不是让陶泥主在结果页里逐个补: 而不是让百梦主在结果页里逐个补:
- `backstoryReveal.chapters` - `backstoryReveal.chapters`
- `skills` - `skills`
@@ -117,13 +117,13 @@
问题不在数量本身,而在于系统并没有明确区分: 问题不在数量本身,而在于系统并没有明确区分:
1. 哪些是陶泥主应重点塑造的关键对象 1. 哪些是百梦主应重点塑造的关键对象
2. 哪些只是 AI 应自动展开的长尾铺量 2. 哪些只是 AI 应自动展开的长尾铺量
这会导致两个问题: 这会导致两个问题:
1. AI 在早期就花大量成本生成长尾内容,等待时间长。 1. AI 在早期就花大量成本生成长尾内容,等待时间长。
2. 陶泥主在结果页里面对的是一整套“全部都生成了”的世界,而不是“先抓关键锚点,再决定是否继续铺开”。 2. 百梦主在结果页里面对的是一整套“全部都生成了”的世界,而不是“先抓关键锚点,再决定是否继续铺开”。
## 1.3.4 当前结果页暴露了过多低杠杆字段 ## 1.3.4 当前结果页暴露了过多低杠杆字段
@@ -134,7 +134,7 @@
- 场景 NPC 分配 - 场景 NPC 分配
- 场景连接网络 - 场景连接网络
这对“专业陶泥主”当然有帮助,但对目标用户来说,容易把工具变成: 这对“专业百梦主”当然有帮助,但对目标用户来说,容易把工具变成:
**看起来自由度很高,实际上需要承担很多系统编辑工作。** **看起来自由度很高,实际上需要承担很多系统编辑工作。**
@@ -144,11 +144,11 @@
这意味着: 这意味着:
1. 陶泥主一旦修改过内容,就会担心被覆盖。 1. 百梦主一旦修改过内容,就会担心被覆盖。
2. 没有“锁定关键内容,只重生成长尾部分”的机制。 2. 没有“锁定关键内容,只重生成长尾部分”的机制。
3. AI 无法真正成为创作搭档,只像一次性大批量生成器。 3. AI 无法真正成为创作搭档,只像一次性大批量生成器。
## 1.3.6 当前生成阶段是“模型视角”,不是“陶泥主视角” ## 1.3.6 当前生成阶段是“模型视角”,不是“百梦主视角”
当前生成页展示的是系统批次和阶段进度,这很好,但它主要回答的是: 当前生成页展示的是系统批次和阶段进度,这很好,但它主要回答的是:
@@ -156,7 +156,7 @@
没有回答的是: 没有回答的是:
- 陶泥主最关心的关键角色是否已经成型 - 百梦主最关心的关键角色是否已经成型
- 世界冲突是否已经稳定 - 世界冲突是否已经稳定
- 当前这轮已经锁定了哪些核心创意 - 当前这轮已经锁定了哪些核心创意
- 接下来生成的是关键锚点,还是长尾内容 - 接下来生成的是关键锚点,还是长尾内容
@@ -170,19 +170,19 @@
这次优化要同时满足 6 个目标: 这次优化要同时满足 6 个目标:
1. 降低输入门槛 1. 降低输入门槛
- 不要求陶泥主一上来写长文,不要求理解系统字段。 - 不要求百梦主一上来写长文,不要求理解系统字段。
2. 提高高杠杆创作自由度 2. 提高高杠杆创作自由度
-陶泥主直接控制世界灵魂锚点,而不是低价值细节。 -百梦主直接控制世界灵魂锚点,而不是低价值细节。
3. 明确陶泥主与 AI 的职责边界 3. 明确百梦主与 AI 的职责边界
- 陶泥主负责“决定什么值得创作”AI 负责“把它展开并跑起来”。 - 百梦主负责“决定什么值得创作”AI 负责“把它展开并跑起来”。
4. 保留现有分阶段生成骨架 4. 保留现有分阶段生成骨架
- 不推翻 `framework -> themePack -> storyGraph -> role/landmark` 的已有结构。 - 不推翻 `framework -> themePack -> storyGraph -> role/landmark` 的已有结构。
5. 引入锁定与局部重生成 5. 引入锁定与局部重生成
-陶泥主能保住自己在乎的内容,只重做其余部分。 -百梦主能保住自己在乎的内容,只重做其余部分。
6. 把结果页从“数据总表”升级成“创作工作台” 6. 把结果页从“数据总表”升级成“创作工作台”
- 让编辑界面按创作价值组织,而不是按底层对象堆字段。 - 让编辑界面按创作价值组织,而不是按底层对象堆字段。
@@ -192,11 +192,11 @@
优化后的自定义世界流程应该改为: 优化后的自定义世界流程应该改为:
```text ```text
陶泥主输入世界锚点 百梦主输入世界锚点
-> AI 编译陶泥主意图摘要 -> AI 编译百梦主意图摘要
-> 陶泥主确认 / 锁定关键锚点 -> 百梦主确认 / 锁定关键锚点
-> AI 先生成关键角色与关键地点 -> AI 先生成关键角色与关键地点
-> 陶泥主可局部修改 / 局部重生成 -> 百梦主可局部修改 / 局部重生成
-> AI 再展开长尾 NPC、长尾场景与运行时编译结构 -> AI 再展开长尾 NPC、长尾场景与运行时编译结构
-> 结果页以“锚点 / 关键对象 / 扩展内容 / 运行时摘要”方式组织 -> 结果页以“锚点 / 关键对象 / 扩展内容 / 运行时摘要”方式组织
-> 保存并进入世界 -> 保存并进入世界
@@ -204,7 +204,7 @@
一句话: 一句话:
**先做创作决策,再做内容展开;先做关键对象,再做长尾铺量;先让陶泥主锁定灵魂,再让 AI 扩散世界。** **先做创作决策,再做内容展开;先做关键对象,再做长尾铺量;先让百梦主锁定灵魂,再让 AI 扩散世界。**
## 4. 输入层优化方案 ## 4. 输入层优化方案
@@ -251,7 +251,7 @@
2. 卡片模式 2. 卡片模式
- 用户直接按结构化方式输入世界锚点 - 用户直接按结构化方式输入世界锚点
两种模式最终都编译成统一的陶泥主意图对象。 两种模式最终都编译成统一的百梦主意图对象。
## 4.3 必填与选填要分开 ## 4.3 必填与选填要分开
@@ -272,7 +272,7 @@
- 标志性要素 - 标志性要素
- 禁止事项 - 禁止事项
这样既能保证世界最小成型,又不会把陶泥主门槛抬高。 这样既能保证世界最小成型,又不会把百梦主门槛抬高。
## 4.3.1 抽象统一“聊天补充设定”能力 ## 4.3.1 抽象统一“聊天补充设定”能力
@@ -307,11 +307,11 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
1. AI 不得在重生成时覆盖该内容 1. AI 不得在重生成时覆盖该内容
2. 长尾内容只能围绕它展开 2. 长尾内容只能围绕它展开
3. 结果页里应明确显示其为“陶泥主锚点” 3. 结果页里应明确显示其为“百梦主锚点”
## 5. 生成链路优化方案 ## 5. 生成链路优化方案
## 5.1 新增“陶泥主意图编译层” ## 5.1 新增“百梦主意图编译层”
在真正开始世界生成前,先新增一个轻量阶段: 在真正开始世界生成前,先新增一个轻量阶段:
@@ -324,19 +324,19 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
输出: 输出:
- 陶泥主意图摘要 - 百梦主意图摘要
- 世界锚点摘要 - 世界锚点摘要
- 系统识别出的关键角色 / 冲突 / 地点 / 禁忌 - 系统识别出的关键角色 / 冲突 / 地点 / 禁忌
这一步的作用不是生成世界,而是先回答: 这一步的作用不是生成世界,而是先回答:
1. 系统理解到的世界核心是什么 1. 系统理解到的世界核心是什么
2. 哪些内容将被视为陶泥主强锚点 2. 哪些内容将被视为百梦主强锚点
3. 哪些内容将交给 AI 扩展 3. 哪些内容将交给 AI 扩展
## 5.2 把当前生成链改成“关键先行、长尾后补” ## 5.2 把当前生成链改成“关键先行、长尾后补”
当前 `generateCustomWorldProfile(...)` 的分阶段结构可以保留,但生成顺序需要更陶泥主化。 当前 `generateCustomWorldProfile(...)` 的分阶段结构可以保留,但生成顺序需要更百梦主化。
建议改成 5 层: 建议改成 5 层:
@@ -347,9 +347,9 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
- 世界框架 - 世界框架
- ThemePack - ThemePack
- StoryGraph 的基础版 - StoryGraph 的基础版
- 陶泥主锚点摘要 - 百梦主锚点摘要
这一层完成后,系统应能让陶泥主看到: 这一层完成后,系统应能让百梦主看到:
- 世界现在到底被理解成了什么 - 世界现在到底被理解成了什么
- 哪些冲突 / 势力 / 意象被识别出来了 - 哪些冲突 / 势力 / 意象被识别出来了
@@ -362,11 +362,11 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
- 关键场景角色 - 关键场景角色
- 关键地点 - 关键地点
这一层优先围绕陶泥主明确输入的角色和地点,而不是先铺满全部数量。 这一层优先围绕百梦主明确输入的角色和地点,而不是先铺满全部数量。
### 第三层:陶泥主校对层 ### 第三层:百梦主校对层
在继续展开长尾内容前,应允许陶泥主做一次轻量校对: 在继续展开长尾内容前,应允许百梦主做一次轻量校对:
- 确认关键角色是否对 - 确认关键角色是否对
- 确认关键地点是否对 - 确认关键地点是否对
@@ -408,7 +408,7 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
这样做的价值很高: 这样做的价值很高:
1. 降低首次等待焦虑 1. 降低首次等待焦虑
2.陶泥主更早介入关键对象校正 2.百梦主更早介入关键对象校正
3. 避免系统在创作方向还没稳定前,先铺满大量长尾内容 3. 避免系统在创作方向还没稳定前,先铺满大量长尾内容
## 5.4 角色与场景生成要改成“锚点优先 + 长尾补位” ## 5.4 角色与场景生成要改成“锚点优先 + 长尾补位”
@@ -417,11 +417,11 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
优化后应改为: 优化后应改为:
1. 先生成陶泥主明确指定的关键角色 / 地点 1. 先生成百梦主明确指定的关键角色 / 地点
2. 再根据世界冲突自动补位缺失的角色原型和场景功能位 2. 再根据世界冲突自动补位缺失的角色原型和场景功能位
3. 最后再铺长尾 3. 最后再铺长尾
这样生成出来的世界会更像“围绕陶泥主意图长出来”,而不是“先生成了一个完整世界,再让陶泥主去认领” 这样生成出来的世界会更像“围绕百梦主意图长出来”,而不是“先生成了一个完整世界,再让百梦主去认领”
## 6. 结果页与编辑工作台优化方案 ## 6. 结果页与编辑工作台优化方案
@@ -439,7 +439,7 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
优化后建议改成 4 层工作台: 优化后建议改成 4 层工作台:
1. 创作锚点 1. 创作锚点
- 展示陶泥主输入和锁定内容 - 展示百梦主输入和锁定内容
2. 关键对象 2. 关键对象
- 关键角色、关键地点、关键冲突对象 - 关键角色、关键地点、关键冲突对象
@@ -448,11 +448,11 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
- AI 自动展开的长尾角色、长尾地点、补位内容 - AI 自动展开的长尾角色、长尾地点、补位内容
4. 世界编译摘要 4. 世界编译摘要
- 展示世界线程、题材包、运行时摘要,但默认不要求陶泥主编辑 - 展示世界线程、题材包、运行时摘要,但默认不要求百梦主编辑
## 6.2 编辑界面应遵守“高价值字段前置,低价值字段折叠” ## 6.2 编辑界面应遵守“高价值字段前置,低价值字段折叠”
陶泥主默认暴露的应是: 百梦主默认暴露的应是:
- 角色一句话定位 - 角色一句话定位
- 角色表面面貌 - 角色表面面貌
@@ -507,7 +507,7 @@ RPG 创作工作台、拼图创作工作台、大鱼吃小鱼创作工作台都
## 7.1 新增 `CustomWorldCreatorIntent` ## 7.1 新增 `CustomWorldCreatorIntent`
建议新增陶泥主输入的统一结构: 建议新增百梦主输入的统一结构:
```ts ```ts
interface CustomWorldCreatorIntent { interface CustomWorldCreatorIntent {
@@ -529,7 +529,7 @@ interface CustomWorldCreatorIntent {
作用: 作用:
- 把“陶泥主真正输入了什么”从最终 `CustomWorldProfile` 中分离出来 - 把“百梦主真正输入了什么”从最终 `CustomWorldProfile` 中分离出来
## 7.2 新增 `CustomWorldAnchorPack` ## 7.2 新增 `CustomWorldAnchorPack`
@@ -583,7 +583,7 @@ interface CustomWorldGenerationDraft {
作用: 作用:
- 让“陶泥主输入、AI 编译、结果编辑”成为连续工作流,而不是只有最终成品对象 - 让“百梦主输入、AI 编译、结果编辑”成为连续工作流,而不是只有最终成品对象
## 8. 与当前仓库的接入建议 ## 8. 与当前仓库的接入建议
@@ -597,7 +597,7 @@ interface CustomWorldGenerationDraft {
目标: 目标:
- 把单 textarea 升级为“快速模式 + 卡片模式” - 把单 textarea 升级为“快速模式 + 卡片模式”
- 新增陶泥主意图状态 - 新增百梦主意图状态
- 新增锁定和局部重生成入口 - 新增锁定和局部重生成入口
## 8.2 prompt 与生成服务层 ## 8.2 prompt 与生成服务层
@@ -623,7 +623,7 @@ interface CustomWorldGenerationDraft {
目标: 目标:
-`CustomWorldProfile` 增加陶泥主意图与锚点相关扩展字段 -`CustomWorldProfile` 增加百梦主意图与锚点相关扩展字段
- 保持旧档兼容 - 保持旧档兼容
- 让现有 builder 能同时消费 `creatorIntent + anchorPack + profile seed` - 让现有 builder 能同时消费 `creatorIntent + anchorPack + profile seed`
@@ -647,28 +647,28 @@ interface CustomWorldGenerationDraft {
本次优化不做以下事情: 本次优化不做以下事情:
1. 不推翻当前自定义世界最终输出仍是 `CustomWorldProfile` 的兼容目标 1. 不推翻当前自定义世界最终输出仍是 `CustomWorldProfile` 的兼容目标
2. 不把所有运行时结构都暴露给陶泥主直接编辑 2. 不把所有运行时结构都暴露给百梦主直接编辑
3. 不要求陶泥主理解 `themePack / storyGraph / knowledgeFacts / threadContracts` 等系统结构 3. 不要求百梦主理解 `themePack / storyGraph / knowledgeFacts / threadContracts` 等系统结构
4. 不把复杂数值平衡、掉落预算、build 预算转移给陶泥 4. 不把复杂数值平衡、掉落预算、build 预算转移给百梦
5. 不把“高自由度”理解成“所有字段都手工可改” 5. 不把“高自由度”理解成“所有字段都手工可改”
## 10. 验收标准 ## 10. 验收标准
做到以下几点,才算这次优化真正成立: 做到以下几点,才算这次优化真正成立:
1. 陶泥主可以不用写长文,只靠卡片输入也能完成自定义世界创建。 1. 百梦主可以不用写长文,只靠卡片输入也能完成自定义世界创建。
2. 系统会明确区分“陶泥主锚点”和“AI 自动展开内容”。 2. 系统会明确区分“百梦主锚点”和“AI 自动展开内容”。
3. 陶泥主不再需要默认手改大量 `skills / initialItems / backstoryReveal / scene connections` 才能得到可用世界。 3. 百梦主不再需要默认手改大量 `skills / initialItems / backstoryReveal / scene connections` 才能得到可用世界。
4. 结果页支持锁定关键角色、关键地点、关键冲突,并支持局部重生成。 4. 结果页支持锁定关键角色、关键地点、关键冲突,并支持局部重生成。
5. 重新生成不再默认覆盖整个世界。 5. 重新生成不再默认覆盖整个世界。
6. 当前 `framework -> themePack -> storyGraph -> role/landmark` 生成主链可以继续复用,而不是被废弃。 6. 当前 `framework -> themePack -> storyGraph -> role/landmark` 生成主链可以继续复用,而不是被废弃。
7. 结果页默认展示的是高创作价值对象,而不是系统级低层字段。 7. 结果页默认展示的是高创作价值对象,而不是系统级低层字段。
8. 长尾内容生成明显后置于关键对象生成,陶泥主能更早看到并修正关键对象。 8. 长尾内容生成明显后置于关键对象生成,百梦主能更早看到并修正关键对象。
9. 旧的自由文本输入模式仍然可用,但不再是唯一入口。 9. 旧的自由文本输入模式仍然可用,但不再是唯一入口。
## 11. 推荐落地顺序 ## 11. 推荐落地顺序
## 阶段 A先加陶泥主意图层 ## 阶段 A先加百梦主意图层
先做: 先做:
@@ -678,7 +678,7 @@ interface CustomWorldGenerationDraft {
目标: 目标:
- 先把陶泥主输入从“单一大文本”升级成“可识别的创作锚点” - 先把百梦主输入从“单一大文本”升级成“可识别的创作锚点”
## 阶段 B再加锚点包与锁定能力 ## 阶段 B再加锚点包与锁定能力
@@ -721,4 +721,4 @@ interface CustomWorldGenerationDraft {
当前自定义世界流程最需要优化的,不是“让 AI 再多生成一点内容”,而是: 当前自定义世界流程最需要优化的,不是“让 AI 再多生成一点内容”,而是:
**把陶泥主从低价值字段编辑里解放出来,让陶泥主负责世界灵魂锚点,让 AI 负责围绕这些锚点分层生成、分层展开、分层可控地把世界长出来。** **把百梦主从低价值字段编辑里解放出来,让百梦主负责世界灵魂锚点,让 AI 负责围绕这些锚点分层生成、分层展开、分层可控地把世界长出来。**

View File

@@ -16,7 +16,7 @@
## 1. 一句话定义 ## 1. 一句话定义
陶泥主通过 Agent 对话确认题材、需要消除次数和难度,系统编译出一个可试玩、可发布的单局抓大鹅玩法作品;玩家在 `10` 分钟倒计时内点击圆形空间中可见物品,把物品放入下方 `7` 格备选栏,每凑齐 `3` 个同物品 id 自动消除,最终清空圆形空间内全部物品即胜利。 百梦主通过 Agent 对话确认题材、需要消除次数和难度,系统编译出一个可试玩、可发布的单局抓大鹅玩法作品;玩家在 `10` 分钟倒计时内点击圆形空间中可见物品,把物品放入下方 `7` 格备选栏,每凑齐 `3` 个同物品 id 自动消除,最终清空圆形空间内全部物品即胜利。
--- ---

View File

@@ -13,7 +13,7 @@
-> 选择“拼图玩法” -> 选择“拼图玩法”
-> Agent 聊天收束高杠杆锚点 -> Agent 聊天收束高杠杆锚点
-> 生成拼图结果页 -> 生成拼图结果页
-> 陶泥主生成并确认拼图图片 -> 百梦主生成并确认拼图图片
-> 发布到拼图广场 -> 发布到拼图广场
-> 玩家从广场进入第 1 关 -> 玩家从广场进入第 1 关
-> 全屏拼图运行时 -> 全屏拼图运行时
@@ -26,7 +26,7 @@
## 1. 一句话定义 ## 1. 一句话定义
陶泥主通过 Agent 对话确定拼图作品的高杠杆视觉锚点再由系统生成结果页、AI 生成拼图图片并发布到广场;玩家进入游戏后,在全屏拼图画布中通过交换、合并、拖动和拆分完成关卡,并沿着“相似题材优先、同作者次优先”的关卡链持续游玩。 百梦主通过 Agent 对话确定拼图作品的高杠杆视觉锚点再由系统生成结果页、AI 生成拼图图片并发布到广场;玩家进入游戏后,在全屏拼图画布中通过交换、合并、拖动和拆分完成关卡,并沿着“相似题材优先、同作者次优先”的关卡链持续游玩。
--- ---
@@ -78,7 +78,7 @@
- 拼图关卡名 - 拼图关卡名
- AI 生成拼图图片的功能 - AI 生成拼图图片的功能
- 图片题材标签 - 图片题材标签
4. 陶泥主发布后的拼图作品必须进入平台广场。 4. 百梦主发布后的拼图作品必须进入平台广场。
5. 玩家从广场进入某个作品时,第 1 关必须先显示当前作品本身。 5. 玩家从广场进入某个作品时,第 1 关必须先显示当前作品本身。
6. 第 2 关及以后必须按照“标签相似度权重 `70%` + 同作者权重 `30%`”选择下一关。 6. 第 2 关及以后必须按照“标签相似度权重 `70%` + 同作者权重 `30%`”选择下一关。
7. 游戏运行时必须全屏展示拼图画布。 7. 游戏运行时必须全屏展示拼图画布。
@@ -129,7 +129,7 @@
创建拼图作品 创建拼图作品
-> Agent 聊天收束 5 个视觉锚点 -> Agent 聊天收束 5 个视觉锚点
-> 生成结果页 -> 生成结果页
-> 陶泥主确认关卡名、标签、图片 -> 百梦主确认关卡名、标签、图片
-> 发布到拼图广场 -> 发布到拼图广场
``` ```
@@ -137,12 +137,12 @@
### 5.1.1 已发布作品二次编辑 ### 5.1.1 已发布作品二次编辑
陶泥主在“我的创作”中点击自己已发布的拼图作品时,不进入只读详情页,而是回到该作品绑定的拼图结果页继续编辑。独立的“体验”按钮仍然直接进入第 1 关,不与编辑入口混用。 百梦主在“我的创作”中点击自己已发布的拼图作品时,不进入只读详情页,而是回到该作品绑定的拼图结果页继续编辑。独立的“体验”按钮仍然直接进入第 1 关,不与编辑入口混用。
落地规则: 落地规则:
1. 已发布拼图作品必须优先通过 `sourceSessionId` 恢复原 Agent session。 1. 已发布拼图作品必须优先通过 `sourceSessionId` 恢复原 Agent session。
2. 恢复后的结果页沿用原草稿、当前正式图、标题、摘要和标签;陶泥主可以继续改标题、摘要、标签,并重新生成图片。 2. 恢复后的结果页沿用原草稿、当前正式图、标题、摘要和标签;百梦主可以继续改标题、摘要、标签,并重新生成图片。
3. 再次点击发布时不得创建新作品,必须覆盖同一个 `profileId / workId` 3. 再次点击发布时不得创建新作品,必须覆盖同一个 `profileId / workId`
4. 覆盖发布只更新作品内容、更新时间、发布时间与广场投影;不得清零 `playCount`,不得改变作品归属。 4. 覆盖发布只更新作品内容、更新时间、发布时间与广场投影;不得清零 `playCount`,不得改变作品归属。
5. 如果历史作品缺少 `sourceSessionId`,前端只能退回作品详情,不伪造编辑 session。 5. 如果历史作品缺少 `sourceSessionId`,前端只能退回作品详情,不伪造编辑 session。
@@ -210,9 +210,9 @@
拼图 Agent 必须做到: 拼图 Agent 必须做到:
1. 优先接住陶泥主的画面灵感,而不是立刻列问卷。 1. 优先接住百梦主的画面灵感,而不是立刻列问卷。
2. 每轮只追问当前最影响图片生成质量的 `1` 个问题。 2. 每轮只追问当前最影响图片生成质量的 `1` 个问题。
3.陶泥主已经说出足够信息时,优先总结,不重复追问。 3.百梦主已经说出足够信息时,优先总结,不重复追问。
4. 当会话至少完成 `2` 轮后,工作区必须提供 `补充剩余关键字` 快捷动作。 4. 当会话至少完成 `2` 轮后,工作区必须提供 `补充剩余关键字` 快捷动作。
- 该动作沿用 RPG 聊天链路,仍走发送消息接口,但请求体必须携带 `quickFillRequested: true` - 该动作沿用 RPG 聊天链路,仍走发送消息接口,但请求体必须携带 `quickFillRequested: true`
- 前端不补数据、不伪造锚点状态,只发送“请补充剩余关键字。”作为本轮用户消息。 - 前端不补数据、不伪造锚点状态,只发送“请补充剩余关键字。”作为本轮用户消息。
@@ -255,7 +255,7 @@ interface PuzzleAnchorPack {
## 7.1 结果页定位 ## 7.1 结果页定位
拼图结果页是陶泥主从 Agent 共创转入正式发布前的最小工作台。 拼图结果页是百梦主从 Agent 共创转入正式发布前的最小工作台。
它至少承担 5 件事: 它至少承担 5 件事:
@@ -303,7 +303,7 @@ interface PuzzleAnchorPack {
关卡名生成规则建议如下: 关卡名生成规则建议如下:
1. 默认由 Agent 根据锚点自动生成 `1` 个正式候选名。 1. 默认由 Agent 根据锚点自动生成 `1` 个正式候选名。
2. 陶泥主可直接手改。 2. 百梦主可直接手改。
3. 关卡名长度建议控制在 `4~12` 个中文字符。 3. 关卡名长度建议控制在 `4~12` 个中文字符。
4. 不允许空标题发布。 4. 不允许空标题发布。
@@ -678,7 +678,7 @@ V1 规则如下:
1. 点击道具必须弹出独立确认窗口。 1. 点击道具必须弹出独立确认窗口。
2. 确认窗口期间暂停游戏时间。 2. 确认窗口期间暂停游戏时间。
3. 正式后端运行态每次确认消耗 `1` 陶泥币 3. 正式后端运行态每次确认消耗 `1` 光点
4. 本地调试 run 不伪造钱包扣费,只保持确认、暂停和表现一致。 4. 本地调试 run 不伪造钱包扣费,只保持确认、暂停和表现一致。
--- ---
@@ -1160,7 +1160,7 @@ interface PuzzleRunSnapshot {
完成标准: 完成标准:
1. 陶泥主能从平台进入拼图 Agent 工作区 1. 百梦主能从平台进入拼图 Agent 工作区
2. 能通过聊天生成结果页草稿 2. 能通过聊天生成结果页草稿
## 阶段 B再做结果页与图片资产 ## 阶段 B再做结果页与图片资产
@@ -1174,7 +1174,7 @@ interface PuzzleRunSnapshot {
完成标准: 完成标准:
1. 陶泥主能生成正式拼图图片并发布 1. 百梦主能生成正式拼图图片并发布
2. 作品能进入拼图广场 2. 作品能进入拼图广场
## 阶段 C再做拼图运行时核心循环 ## 阶段 C再做拼图运行时核心循环
@@ -1232,4 +1232,4 @@ interface PuzzleRunSnapshot {
这次平台新增拼图玩法,正确的做法不是只补一个拼图画布,而是: 这次平台新增拼图玩法,正确的做法不是只补一个拼图画布,而是:
**把拼图作为平台内独立玩法类型接进现有 Agent-first 创作中心、结果页、发布链、广场分发链和运行时链,让陶泥主先收束高杠杆视觉锚点,让玩家在全屏交换-合并-拆分的拼图循环中持续游玩。** **把拼图作为平台内独立玩法类型接进现有 Agent-first 创作中心、结果页、发布链、广场分发链和运行时链,让百梦主先收束高杠杆视觉锚点,让玩家在全屏交换-合并-拆分的拼图循环中持续游玩。**

View File

@@ -630,7 +630,7 @@ SSE 事件:
1. 增加背景音乐和环境音,但不改变四帧三段主链。 1. 增加背景音乐和环境音,但不改变四帧三段主链。
2. 为移动端生成 `9:16` 竖版裁切版本。 2. 为移动端生成 `9:16` 竖版裁切版本。
3. 支持陶泥主手动上传某张关键帧,再生成相邻视频。 3. 支持百梦主手动上传某张关键帧,再生成相邻视频。
4. 支持发布后版本化替换开场动画。 4. 支持发布后版本化替换开场动画。
5. 支持用第四幕直接生成开局场景动态背景。 5. 支持用第四幕直接生成开局场景动态背景。
6. 支持把开场动画拆出的关键帧回流为作品详情页轮播素材。 6. 支持把开场动画拆出的关键帧回流为作品详情页轮播素材。

View File

@@ -22,7 +22,7 @@
接成一条新的稳定流程: 接成一条新的稳定流程:
**每个场景由陶泥主在工具中配置为 `2~5` 幕;每一幕都绑定独立背景图和相遇 NPC 顺序;每一幕的第一个 NPC 视为主角色;运行时按幕切换背景和可遇对象,并根据主角色当前好感度裁决聊天轮数与第 5 轮收束方式。** **每个场景由百梦主在工具中配置为 `2~5` 幕;每一幕都绑定独立背景图和相遇 NPC 顺序;每一幕的第一个 NPC 视为主角色;运行时按幕切换背景和可遇对象,并根据主角色当前好感度裁决聊天轮数与第 5 轮收束方式。**
本次还追加一条必须和草稿生成阶段一起落地的约束: 本次还追加一条必须和草稿生成阶段一起落地的约束:
@@ -31,13 +31,13 @@
补充口径修正: 补充口径修正:
1. `scene_chapter` 在本期继续保留为数据层 / 编译层 / 运行时层概念。 1. `scene_chapter` 在本期继续保留为数据层 / 编译层 / 运行时层概念。
2. `scene_chapter` 不作为陶泥主可见的独立 Tab、独立卡片或独立导航入口。 2. `scene_chapter` 不作为百梦主可见的独立 Tab、独立卡片或独立导航入口。
3. 陶泥主配置多幕的唯一入口,是现有“场景”列表里的场景编辑弹层。 3. 百梦主配置多幕的唯一入口,是现有“场景”列表里的场景编辑弹层。
4. 每一幕的 NPC 配置区必须直接叠在当前幕背景预览上,以“对面角色站位”的方式呈现;三个站位既是预览,也是编辑入口。 4. 每一幕的 NPC 配置区必须直接叠在当前幕背景预览上,以“对面角色站位”的方式呈现;三个站位既是预览,也是编辑入口。
5. 幕编辑站位中每个角色只显示角色形象与名称,不展示额外信息块、规则说明或说明性标签。 5. 幕编辑站位中每个角色只显示角色形象与名称,不展示额外信息块、规则说明或说明性标签。
6. 幕内小预览的构图固定为左侧玩家、右侧当前幕角色;右侧三个站位采用一前两后。 6. 幕内小预览的构图固定为左侧玩家、右侧当前幕角色;右侧三个站位采用一前两后。
前排主角色的 y 轴必须与玩家角色对齐后排两个角色必须同一列、x 轴对齐,上下分布,且后排整体的 y 轴中点与前排主角色保持一致。 前排主角色的 y 轴必须与玩家角色对齐后排两个角色必须同一列、x 轴对齐,上下分布,且后排整体的 y 轴中点与前排主角色保持一致。
7. 新建幕默认仅预置 1 个主角色槽位内容,其余槽位留空,等待陶泥主补充。 7. 新建幕默认仅预置 1 个主角色槽位内容,其余槽位留空,等待百梦主补充。
8. 角色名称显示在角色形象上方,角色渲染不附带方形 UI 底板。 8. 角色名称显示在角色形象上方,角色渲染不附带方形 UI 底板。
9. 世界档案的场景详情页不再单独展示“场景图片”和“场景内 NPC”字段相关兼容数据统一由多幕配置自动同步回场景对象。 9. 世界档案的场景详情页不再单独展示“场景图片”和“场景内 NPC”字段相关兼容数据统一由多幕配置自动同步回场景对象。
@@ -55,7 +55,7 @@
本次迭代必须同时满足以下目标: 本次迭代必须同时满足以下目标:
1. 陶泥主可以在现有创作页面中为每个场景章节配置多幕内容。 1. 百梦主可以在现有创作页面中为每个场景章节配置多幕内容。
2. 每一幕都必须绑定一张正式背景图。 2. 每一幕都必须绑定一张正式背景图。
3. 每一幕都可以配置玩家会遇到哪些 NPC并且保留顺序。 3. 每一幕都可以配置玩家会遇到哪些 NPC并且保留顺序。
4. 每一幕配置的第一个 NPC 必须被系统认定为该幕主角色。 4. 每一幕配置的第一个 NPC 必须被系统认定为该幕主角色。
@@ -89,7 +89,7 @@
1. 不新建独立的“场景编辑器”页面。 1. 不新建独立的“场景编辑器”页面。
2. 不把幕推进逻辑放到前端本地计算。 2. 不把幕推进逻辑放到前端本地计算。
3. 不让陶泥主直接编辑底层运行时 `ChapterState` 或聊天状态对象。 3. 不让百梦主直接编辑底层运行时 `ChapterState` 或聊天状态对象。
4. 不做多 NPC 并行聊天。 4. 不做多 NPC 并行聊天。
5. 不做每一幕的复杂分支树可视化编辑器。 5. 不做每一幕的复杂分支树可视化编辑器。
6. 不把“规则说明文案”默认堆到创作页或游戏 UI 面板里。 6. 不把“规则说明文案”默认堆到创作页或游戏 UI 面板里。
@@ -122,7 +122,7 @@
1. 场景章节没有“幕”这一层结构化对象。 1. 场景章节没有“幕”这一层结构化对象。
2. 背景图是场景级资产,不是幕级资产。 2. 背景图是场景级资产,不是幕级资产。
3. NPC 与场景的关系主要还是地点级归属,不是幕级相遇编排。 3. NPC 与场景的关系主要还是地点级归属,不是幕级相遇编排。
4. 陶泥主无法在创作页里明确控制“这一幕谁先出场、谁是主角色”。 4. 百梦主无法在创作页里明确控制“这一幕谁先出场、谁是主角色”。
## 4.2 游戏运行侧现状 ## 4.2 游戏运行侧现状
@@ -185,7 +185,7 @@
这意味着: 这意味着:
1. 陶泥主在工具里编辑的是“第几幕”。 1. 百梦主在工具里编辑的是“第几幕”。
2. 运行时仍然只认现有章节阶段枚举。 2. 运行时仍然只认现有章节阶段枚举。
3. `chapterDirector` 可以继续复用,只是数据来源从“纯 quest 推导”升级成“quest + 幕蓝图联合推导”。 3. `chapterDirector` 可以继续复用,只是数据来源从“纯 quest 推导”升级成“quest + 幕蓝图联合推导”。
@@ -214,7 +214,7 @@
- `name` - `name`
- `description` - `description`
- `imageSrc` - `imageSrc`
- `sceneNpcIds`(仅作为兼容字段,由多幕配置自动派生,不再作为陶泥主可编辑字段) - `sceneNpcIds`(仅作为兼容字段,由多幕配置自动派生,不再作为百梦主可编辑字段)
- `connections` - `connections`
- `sceneChapterBlueprints` 对应的多幕配置 - `sceneChapterBlueprints` 对应的多幕配置
2. 场景配置面板中,开局场景必须复用普通场景同级的配置 UI而不是继续保留一套缩水版表单。 2. 场景配置面板中,开局场景必须复用普通场景同级的配置 UI而不是继续保留一套缩水版表单。
@@ -251,7 +251,7 @@
原因: 原因:
1. 当前创作工作区已经进入“先收关键锚点、再逐步扩写”的阶段。 1. 当前创作工作区已经进入“先收关键锚点、再逐步扩写”的阶段。
2. 一次铺太多 playable、场景和长尾对象会稀释陶泥主对第一版底稿的掌控感。 2. 一次铺太多 playable、场景和长尾对象会稀释百梦主对第一版底稿的掌控感。
3. 本期还要把幕级背景图和角色主形象自动挂回草稿,如果对象规模不收束,等待时间和生成成本都会直接失控。 3. 本期还要把幕级背景图和角色主形象自动挂回草稿,如果对象规模不收束,等待时间和生成成本都会直接失控。
### 5.5.2 幕级出演角色与背景必须由剧情引擎判定 ### 5.5.2 幕级出演角色与背景必须由剧情引擎判定
@@ -309,7 +309,7 @@
- 角色主形象是否就绪 - 角色主形象是否就绪
- 场景幕背景是否就绪 - 场景幕背景是否就绪
这样陶泥主一进入草稿精修工作区,就能直接看到: 这样百梦主一进入草稿精修工作区,就能直接看到:
1. 角色已经带主形象 1. 角色已经带主形象
2. 每个场景章节的每一幕已经带背景图 2. 每个场景章节的每一幕已经带背景图
@@ -369,7 +369,7 @@ interface CustomWorldFoundationDraftSceneChapter {
1. `primaryNpcId` 必须等于 `encounterNpcIds[0]`,不允许单独填写成别的角色。 1. `primaryNpcId` 必须等于 `encounterNpcIds[0]`,不允许单独填写成别的角色。
2. 每幕必须至少有 `1` 个 NPC。 2. 每幕必须至少有 `1` 个 NPC。
3. 每幕必须有 `backgroundImageSrc``backgroundAssetId` 3. 每幕必须有 `backgroundImageSrc``backgroundAssetId`
4. `advanceRule` 由系统按幕位置默认编译,第一版不要求陶泥主手改。 4. `advanceRule` 由系统按幕位置默认编译,第一版不要求百梦主手改。
## 6.2 发布到运行时的蓝图结构 ## 6.2 发布到运行时的蓝图结构
@@ -416,7 +416,7 @@ sceneChapterBlueprints?: SceneChapterBlueprint[] | null;
原因: 原因:
1. 现有 `landmarks` 只足够表达地点,不足够表达幕顺序。 1. 现有 `landmarks` 只足够表达地点,不足够表达幕顺序。
2. 现有 `ChapterState` 是运行时状态,不适合直接兼做陶泥主蓝图。 2. 现有 `ChapterState` 是运行时状态,不适合直接兼做百梦主蓝图。
3. 独立蓝图层更适合后端编译和发布校验。 3. 独立蓝图层更适合后端编译和发布校验。
## 6.3 聊天状态扩展 ## 6.3 聊天状态扩展
@@ -483,9 +483,9 @@ type NpcChatTurnResult = {
新增规则: 新增规则:
1. 陶泥主从现有“场景”列表点击任一场景卡,进入对应场景编辑弹层。 1. 百梦主从现有“场景”列表点击任一场景卡,进入对应场景编辑弹层。
2. 多幕配置必须作为场景编辑弹层内的一个区块出现,归属于该场景。 2. 多幕配置必须作为场景编辑弹层内的一个区块出现,归属于该场景。
3. `scene_chapter` 仅作为保存层和运行时蓝图存在,不单独暴露在陶泥主导航里。 3. `scene_chapter` 仅作为保存层和运行时蓝图存在,不单独暴露在百梦主导航里。
4. 场景卡片可增加“幕数量”轻量摘要,但第一版不是阻塞项。 4. 场景卡片可增加“幕数量”轻量摘要,但第一版不是阻塞项。
## 7.2 场景编辑弹层展示要求 ## 7.2 场景编辑弹层展示要求
@@ -498,8 +498,8 @@ type NpcChatTurnResult = {
补充约束: 补充约束:
1. “场景图片”不再作为场景详情页里的独立字段展示,陶泥主只能通过每一幕的“配置背景”入口管理视觉。 1. “场景图片”不再作为场景详情页里的独立字段展示,百梦主只能通过每一幕的“配置背景”入口管理视觉。
2. “场景内 NPC”不再作为场景详情页里的独立字段展示陶泥主只能通过每一幕角色槽位配置相遇 NPC。 2. “场景内 NPC”不再作为场景详情页里的独立字段展示百梦主只能通过每一幕角色槽位配置相遇 NPC。
3. 为兼容现有运行时与旧数据结构,场景对象上的 `imageSrc / sceneNpcIds` 仍然保留,但必须由多幕配置自动回填,前台不再暴露单独编辑控件,且不能再用 `sceneNpcIds` 限制每幕可选角色。 3. 为兼容现有运行时与旧数据结构,场景对象上的 `imageSrc / sceneNpcIds` 仍然保留,但必须由多幕配置自动回填,前台不再暴露单独编辑控件,且不能再用 `sceneNpcIds` 限制每幕可选角色。
多幕区块至少展示: 多幕区块至少展示:
@@ -566,11 +566,11 @@ NPC 配置面板必须支持:
3. 不允许把不存在于当前世界角色池中的 id 写入幕配置。 3. 不允许把不存在于当前世界角色池中的 id 写入幕配置。
4. 若主角色未与当前场景或线程建立任何关联,给出发布警告。 4. 若主角色未与当前场景或线程建立任何关联,给出发布警告。
5. 存储时继续落到 `encounterNpcIds` 有序数组,槽位从左到右按顺序压缩写入。 5. 存储时继续落到 `encounterNpcIds` 有序数组,槽位从左到右按顺序压缩写入。
6. `sceneNpcIds` 不再作为陶泥主字段,也不再作为幕角色选择范围;保存时只从所有幕的 `encounterNpcIds` 自动派生兼容值。 6. `sceneNpcIds` 不再作为百梦主字段,也不再作为幕角色选择范围;保存时只从所有幕的 `encounterNpcIds` 自动派生兼容值。
## 7.6 幕预览 ## 7.6 幕预览
陶泥主在场景编辑弹层里点击“幕预览”后,必须直接进入当前幕的运行时预览。 百梦主在场景编辑弹层里点击“幕预览”后,必须直接进入当前幕的运行时预览。
要求如下: 要求如下:
@@ -633,7 +633,7 @@ interface SceneActRuntimeState {
## 8.3 幕推进规则 ## 8.3 幕推进规则
第一版不要求陶泥主手填推进条件,而是由系统按幕位置默认编译: 第一版不要求百梦主手填推进条件,而是由系统按幕位置默认编译:
1.`1` 幕默认 `after_primary_contact` 1.`1` 幕默认 `after_primary_contact`
- 玩家与主角色发生首次有效接触后可进入下一幕判定 - 玩家与主角色发生首次有效接触后可进入下一幕判定
@@ -871,7 +871,7 @@ Adventure 主面板在本次迭代中至少增加下面这些表现:
当下面这些结果都成立时,视为本次 PRD 已被正确落地: 当下面这些结果都成立时,视为本次 PRD 已被正确落地:
1. 陶泥主可以在现有场景编辑弹层中配置每个场景的多幕。 1. 百梦主可以在现有场景编辑弹层中配置每个场景的多幕。
2. 每个场景章节都可以配置 `2~5` 幕。 2. 每个场景章节都可以配置 `2~5` 幕。
3. 每一幕都可以绑定独立背景图。 3. 每一幕都可以绑定独立背景图。
4. 每一幕都可以配置有序 NPC 列表,第一位自动成为主角色。 4. 每一幕都可以配置有序 NPC 列表,第一位自动成为主角色。

View File

@@ -4,7 +4,7 @@
## 0. 目标 ## 0. 目标
把“陶泥币 / 游戏时长 / 玩过”这一排信息卡,从静态数字展示升级成稳定的个人数据看板,让玩家在进入“我的”页时一眼知道自己的账号资产和游玩投入。 把“光点 / 游戏时长 / 玩过”这一排信息卡,从静态数字展示升级成稳定的个人数据看板,让玩家在进入“我的”页时一眼知道自己的账号资产和游玩投入。
--- ---
@@ -12,7 +12,7 @@
当前三个数字来源并不统一: 当前三个数字来源并不统一:
1. 陶泥币来自当前存档上下文,不等于账号总资产 1. 光点来自当前存档上下文,不等于账号总资产
2. 游戏时长依赖当前快照,不代表全账号累计 2. 游戏时长依赖当前快照,不代表全账号累计
3. 玩过当前几乎是硬编码推导,不是真实统计 3. 玩过当前几乎是硬编码推导,不是真实统计
@@ -39,11 +39,11 @@
## 3. 指标定义 ## 3. 指标定义
## 3.1 陶泥币 ## 3.1 光点
定义: 定义:
- 当前账号可立即消费的陶泥币余额 - 当前账号可立即消费的光点余额
不使用: 不使用:
@@ -80,7 +80,7 @@
点击行为: 点击行为:
1. 陶泥币 1. 光点
- 打开资产流水抽屉 - 打开资产流水抽屉
2. 游戏时长卡 2. 游戏时长卡
- 打开游玩统计抽屉 - 打开游玩统计抽屉
@@ -125,7 +125,7 @@
返回: 返回:
- 陶泥币流水列表 - 光点流水列表
### `GET /api/profile/play-stats` ### `GET /api/profile/play-stats`

View File

@@ -73,7 +73,7 @@
首期奖励建议采用可控方案: 首期奖励建议采用可控方案:
1. 邀请人获得陶泥币 1. 邀请人获得光点
2. 被邀请人获得新手奖励 2. 被邀请人获得新手奖励
所有奖励必须走台账,不允许前端本地加值。 所有奖励必须走台账,不允许前端本地加值。
@@ -164,4 +164,4 @@
1. 用户能看到自己的邀请码与邀请链接 1. 用户能看到自己的邀请码与邀请链接
2. 可以一键复制或分享 2. 可以一键复制或分享
3. 邀请成功后能看到正确统计 3. 邀请成功后能看到正确统计
4. 奖励到账后陶泥币余额同步变化 4. 奖励到账后光点余额同步变化

View File

@@ -51,11 +51,11 @@
首期只保留两种状态: 首期只保留两种状态:
1. `普通用户` 1. `普通用户`
2. `陶泥会员` 2. `百梦会员`
会员权益首期建议控制在直接可编码的范围: 会员权益首期建议控制在直接可编码的范围:
1. 每日额外陶泥币领取额度 1. 每日额外光点领取额度
2. 高级世界模板或创作槽位 2. 高级世界模板或创作槽位
3. 更高的云存档上限 3. 更高的云存档上限
4. 会员专属标识 4. 会员专属标识
@@ -119,7 +119,7 @@
支付成功后: 支付成功后:
1. 刷新会员状态 1. 刷新会员状态
2. 刷新陶泥币余额 2. 刷新光点余额
3. 刷新权益标签 3. 刷新权益标签
--- ---

View File

@@ -8,7 +8,7 @@
1. 头像编辑 1. 头像编辑
2. 昵称编辑 2. 昵称编辑
3. 陶泥号展示与复制 3. 百梦号展示与复制
4. 登录方式与绑定状态展示 4. 登录方式与绑定状态展示
5. 进入资料编辑抽屉 5. 进入资料编辑抽屉
@@ -22,7 +22,7 @@
- 头像占位 - 头像占位
- 昵称 - 昵称
- 陶泥 - 百梦
- 登录方式 - 登录方式
- 绑定状态 - 绑定状态
@@ -31,7 +31,7 @@
1. 头像按钮和昵称编辑按钮都直接打开账号弹窗,信息架构混在一起 1. 头像按钮和昵称编辑按钮都直接打开账号弹窗,信息架构混在一起
2. 头像当前只是视觉壳,没有真正的上传与裁剪能力 2. 头像当前只是视觉壳,没有真正的上传与裁剪能力
3. 昵称缺少明确的编辑规则与唯一性策略 3. 昵称缺少明确的编辑规则与唯一性策略
4. 陶泥号只是前端拼接值,不适合长期作为正式公开识别码 4. 百梦号只是前端拼接值,不适合长期作为正式公开识别码
--- ---
@@ -43,7 +43,7 @@
2. 资料编辑抽屉 2. 资料编辑抽屉
3. 头像上传、裁切、保存 3. 头像上传、裁切、保存
4. 昵称编辑、校验、保存 4. 昵称编辑、校验、保存
5. 陶泥号固定生成与复制 5. 百梦号固定生成与复制
6. 登录方式与账号状态标签展示 6. 登录方式与账号状态标签展示
## 2.2 本期不做 ## 2.2 本期不做
@@ -63,7 +63,7 @@
- 用户头像 - 用户头像
- 用户昵称 - 用户昵称
- `陶泥号` - `百梦号`
- 登录方式标签 - 登录方式标签
- 账号状态标签 - 账号状态标签
- 资料编辑入口 - 资料编辑入口
@@ -85,7 +85,7 @@
- 打开“编辑资料”抽屉,并默认聚焦头像编辑区域 - 打开“编辑资料”抽屉,并默认聚焦头像编辑区域
2. 点击昵称右侧编辑按钮 2. 点击昵称右侧编辑按钮
- 打开“编辑资料”抽屉,并默认聚焦昵称输入框 - 打开“编辑资料”抽屉,并默认聚焦昵称输入框
3. 点击陶泥号复制按钮 3. 点击百梦号复制按钮
- 直接复制,并给出轻提示 - 直接复制,并给出轻提示
4. 点击登录方式/状态标签 4. 点击登录方式/状态标签
- 不跳页,不弹复杂说明 - 不跳页,不弹复杂说明
@@ -125,9 +125,9 @@
4. 不要求全站唯一,但要允许后端做敏感词审核 4. 不要求全站唯一,但要允许后端做敏感词审核
5. 审核失败时返回明确错误 5. 审核失败时返回明确错误
## 4.3 陶泥 ## 4.3 百梦
陶泥号规则: 百梦号规则:
1. 作为公开可复制识别码 1. 作为公开可复制识别码
2. 用户创建后固定生成,不允许用户修改 2. 用户创建后固定生成,不允许用户修改
@@ -207,6 +207,6 @@
1. 用户可以上传并保存头像 1. 用户可以上传并保存头像
2. 用户可以修改昵称并实时看到更新 2. 用户可以修改昵称并实时看到更新
3. 陶泥号由后端返回,复制后可正常使用 3. 百梦号由后端返回,复制后可正常使用
4. 未登录或待绑定状态下,不出现无效编辑入口 4. 未登录或待绑定状态下,不出现无效编辑入口
5. 页面不出现冗长规则说明文案 5. 页面不出现冗长规则说明文案

View File

@@ -108,6 +108,16 @@
- 最后游玩时间 - 最后游玩时间
- 游戏信息 - 游戏信息
### 3.3.0 拼图存档字段与视觉层级
拼图玩法的存档列表项必须按作品维度展示,不把关卡名当作作品名:
- 主标题展示作品名,来源为服务端存档投影的 `worldName`,视觉层级必须是卡片内最醒目的文本。
- 副标题展示当前可继续入口,格式为 `第 N 关 · 关卡名`;无关卡名时只展示 `第 N 关`
- 卡片中不展示英文 `Archive` / `ARCHIVE` 标签。
- 封面缩略图固定为 `1:1` 正方形比例,使用当前可继续关卡图片。
- 封面图上不再覆盖“最近存档”标签;保存时间独立展示在信息区内。
### 3.3.1 移动端卡片布局约束 ### 3.3.1 移动端卡片布局约束
- 移动端列表卡片中的封面只能作为独立缩略图或弱化背景层使用,不能直接占满整张卡片并压在正文信息下方。 - 移动端列表卡片中的封面只能作为独立缩略图或弱化背景层使用,不能直接占满整张卡片并压在正文信息下方。

View File

@@ -35,7 +35,7 @@ TXT 模式核心玩法是一个包含“创作编辑器 -> 测试体验 -> 正
1. 支持创建 TXT 模式作品。 1. 支持创建 TXT 模式作品。
2. 支持 TXT 模式作品的完整创作流程。 2. 支持 TXT 模式作品的完整创作流程。
3. 支持陶泥主测试体验。 3. 支持百梦主测试体验。
4. 支持玩家正式游玩。 4. 支持玩家正式游玩。
5. 支持文本模式运行。 5. 支持文本模式运行。
6. 支持双会话机制。 6. 支持双会话机制。
@@ -174,9 +174,9 @@ TXT 模式核心玩法必须完整保留双会话机制。
2. 正式继续体验 2. 正式继续体验
3. 正式游玩推进 3. 正式游玩推进
## 7.2 陶泥主测试/读档会话 ## 7.2 百梦主测试/读档会话
陶泥主测试/读档会话用于: 百梦主测试/读档会话用于:
1. 编辑器内测试体验 1. 编辑器内测试体验
2. 指定存档加载 2. 指定存档加载

View File

@@ -154,47 +154,11 @@ claims 设计:
2. 当前 `SpacetimeDB server/database` 配置。 2. 当前 `SpacetimeDB server/database` 配置。
3. `SpacetimeDB` 数据库基础信息。 3. `SpacetimeDB` 数据库基础信息。
4. 当前 schema 表清单。 4. 当前 schema 表清单。
5. 首批关键表的行数统计。 5. schema 表清单对应的逐表行数统计。
首批关键表固定覆盖: 表统计必须以 SpacetimeDB schema 返回的表名为唯一来源,`schemaTableNames` 的数量必须与 `tableStats` 的行数一致。后台服务只对 schema 中符合安全 SQL 标识符格式的表名发起 `SELECT COUNT(*)`,不提供任意 SQL 输入能力。
1. `runtime_setting` 返回中的计数失败项必须带错误信息不能静默吞掉。SpacetimeDB private 表或当前身份不可见的表可能在 `/sql` 下返回 `no such table` / `marked private`这类项统一展示为“不可统计private 或当前身份不可见)”,不作为整页读取失败处理。
2. `runtime_snapshot`
3. `user_browse_history`
4. `profile_dashboard_state`
5. `profile_wallet_ledger`
6. `profile_played_world`
7. `profile_save_archive`
8. `story_session`
9. `story_event`
10. `battle_state`
11. `inventory_slot`
12. `quest_record`
13. `quest_log`
14. `treasure_record`
15. `npc_state`
16. `custom_world_profile`
17. `custom_world_gallery_entry`
18. `custom_world_agent_session`
19. `custom_world_agent_message`
20. `custom_world_agent_operation`
21. `custom_world_draft_card`
22. `big_fish_creation_session`
23. `big_fish_agent_message`
24. `big_fish_asset_slot`
25. `big_fish_runtime_run`
26. `puzzle_work_profile`
27. `puzzle_agent_session`
28. `puzzle_agent_message`
29. `puzzle_runtime_run`
30. `ai_task`
31. `ai_task_stage`
32. `ai_text_chunk`
33. `ai_result_reference`
34. `asset_object`
35. `asset_entity_binding`
返回中的计数失败项必须带错误信息,不能静默吞掉。
## 8. API 调试设计 ## 8. API 调试设计

View File

@@ -4,13 +4,13 @@
对应 PRD[后台管理独立前端工程 PRD](../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md) 对应 PRD[后台管理独立前端工程 PRD](../prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md)
落地状态:`2026-04-30` 已创建 `apps/admin-web` 独立前端工程包含登录、总览、API 调试、兑换码管理和注册邀请码管理首版页面;根工程已补 `admin-web:*` 转发脚本。 落地状态:`2026-04-30` 已创建 `apps/admin-web` 独立前端工程包含登录、总览、API 调试、兑换码管理和注册邀请码管理首版页面;根工程已补 `admin-web:*` 转发脚本。`2026-05-01` 起,根构建与 Ubuntu 发布包会同步构建后台前端,并在发布包 Web 网关中以同域 `/admin/` 暴露。
## 1. 结论 ## 1. 结论
后台管理端采用独立前端工程,路径固定为 `apps/admin-web`。它只负责 UI 表现、输入采集、请求发起和结果渲染所有鉴权、聚合、写操作、SpacetimeDB 访问和业务校验继续收口在 `server-rs/crates/api-server` 后台管理端采用独立前端工程,路径固定为 `apps/admin-web`。它只负责 UI 表现、输入采集、请求发起和结果渲染所有鉴权、聚合、写操作、SpacetimeDB 访问和业务校验继续收口在 `server-rs/crates/api-server`
本方案接管旧 `api-server` 内嵌 HTML/CSS/JS 页面,旧 `GET /admin` 不再挂载。后续后台入口由独立前端工程部署产物承接 本方案接管旧 `api-server` 内嵌 HTML/CSS/JS 页面,Rust `api-server` 直连时`GET /admin` 不再挂载。部署态后台入口由发布包内 `web-server.mjs` 承接:`/admin/` 返回独立前端静态产物,`/admin/api/*` 继续反代到 `api-server`
## 2. 工程结构 ## 2. 工程结构
@@ -253,9 +253,13 @@ export interface ProfileInviteCodeAdminResponse {
后端读取 SpacetimeDB schema 时必须请求 `/v1/database/{database}/schema?version=9`。SpacetimeDB 2.x schema HTTP API 缺少 `version` query 会返回 `400 missing field version`,后台页面只能展示读取异常,不能拿到真实表名。 后端读取 SpacetimeDB schema 时必须请求 `/v1/database/{database}/schema?version=9`。SpacetimeDB 2.x schema HTTP API 缺少 `version` query 会返回 `400 missing field version`,后台页面只能展示读取异常,不能拿到真实表名。
`schemaTableNames``tableStats` 必须采用同一份 schema 表清单生成不能再用硬编码关键表白名单补齐统计项。后台右上角显示的表数量必须等于统计表格实际行数schema 读取失败时两者均为空,并通过 `fetchErrors` 暴露读取失败原因。
后端读取表行数时必须按 SpacetimeDB 2.x `/sql` 响应解析:接口返回 statement result 数组,单条结果内的 `schema.elements` 描述列名,`rows` 是按列顺序排列的数组行,例如 `rows: [[0]]`。后台服务不能再假设响应是 `{ rows: [{ row_count: 0 }] }` 的对象行形状;为了兼容小版本差异,可保留对象行兜底解析。 后端读取表行数时必须按 SpacetimeDB 2.x `/sql` 响应解析:接口返回 statement result 数组,单条结果内的 `schema.elements` 描述列名,`rows` 是按列顺序排列的数组行,例如 `rows: [[0]]`。后台服务不能再假设响应是 `{ rows: [{ row_count: 0 }] }` 的对象行形状;为了兼容小版本差异,可保留对象行兜底解析。
`tableStats` 中单表失败必须展示 `errorMessage`,不能让整页变成空白。 `tableStats` 中单表失败必须展示 `errorMessage`,不能让整页变成空白。SpacetimeDB private 表或当前身份不可见的表在 `/sql` 下可能返回 `no such table` / `marked private`后台服务必须将这类错误归一为“不可统计private 或当前身份不可见)”,避免把预期的访问边界展示成原始 HTTP 400 故障。
线上如果大量表都显示“不可统计private 或当前身份不可见)”,优先检查 `api-server` 启动环境中的 `GENARRATIVE_SPACETIME_TOKEN` / `GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN` 是否存在且属于目标库 owner。Jenkins 覆盖发布包时必须保留部署目录已有运行 token只带迁移 token 不能让后台概览读取 private 表。
### 4.6 API 调试 contract ### 4.6 API 调试 contract
@@ -384,12 +388,14 @@ export interface ProfileInviteCodeAdminResponse {
### 7.2 构建部署 ### 7.2 构建部署
首版构建产物由独立后台工程输出到 `apps/admin-web/dist`。部署可以选择 当前发布形态固定为同域 `/admin/`
1. 独立静态站点域名,例如 `https://admin.example.com` 1. 本地单独执行 `npm run admin-web:build` 时,后台构建产物默认输出到 `apps/admin-web/dist`
2. 与主站同域不同路径,由网关把后台静态资源和 `/admin/api/*` 分别路由到正确目标 2. 根工程执行 `npm run build` 时,会先构建主前端,再构建后台前端;任一构建失败或输出 warning 都会让构建门禁失败
3. Ubuntu 发布包执行 `npm run deploy:rust:remote` 时,后台前端以 Vite `--base /admin/` 构建到发布包 `web/admin/`
4. 发布包 `web-server.mjs``/admin` 返回 301 到 `/admin/`,对 `/admin/``/admin/*` 提供后台 SPA fallback`/admin/api/*` 优先反代到 `api-server`
无论哪种方式,`server-rs` 仍然是唯一管理 API 后端。 该形态不新增后台静态端口和后台专用后端。`server-rs` 仍然是唯一管理 API 后端,后台前端不直连 SpacetimeDB
### 7.3 后台工程脚本 ### 7.3 后台工程脚本
@@ -399,8 +405,8 @@ export interface ProfileInviteCodeAdminResponse {
{ {
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1", "dev": "vite --host 127.0.0.1",
"build": "tsc --noEmit && vite build", "build": "node ../../scripts/admin-web-build.mjs build",
"typecheck": "tsc --noEmit", "typecheck": "node ../../scripts/admin-web-build.mjs typecheck",
"preview": "vite preview --host 127.0.0.1" "preview": "vite preview --host 127.0.0.1"
} }
} }
@@ -408,6 +414,8 @@ export interface ProfileInviteCodeAdminResponse {
如果后续接入根 npm workspace再在根 `package.json` 增加转发脚本;本轮不要为了后台工程强行重排现有前端脚本。 如果后续接入根 npm workspace再在根 `package.json` 增加转发脚本;本轮不要为了后台工程强行重排现有前端脚本。
当前工程没有启用 npm workspace因此后台构建脚本必须从仓库根目录调用 root toolchain。`scripts/admin-web-build.mjs` 统一执行 `tsc --noEmit -p apps/admin-web/tsconfig.json` 与 Vite 构建,避免 `npm --prefix apps/admin-web` 在子目录找不到 `tsc`
当前根工程同步提供以下转发脚本: 当前根工程同步提供以下转发脚本:
1. `npm run admin-web:dev` 1. `npm run admin-web:dev`

View File

@@ -45,7 +45,7 @@
修复: 修复:
1.`map_password_entry_error(...)` 中补充 `InvalidPublicUserCode` 1.`map_password_entry_error(...)` 中补充 `InvalidPublicUserCode`
2. 返回中文错误文案 `陶泥号格式不正确` 2. 返回中文错误文案 `百梦号格式不正确`
### 3.3 `module-custom-world` 的 `Display` 分支未覆盖新字段错误 ### 3.3 `module-custom-world` 的 `Display` 分支未覆盖新字段错误

View File

@@ -1,8 +1,8 @@
# 资产操作陶泥币消耗接入方案 # 资产操作光点消耗接入方案
## 背景 ## 背景
当前陶泥币钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成和作品发布由 Axum API 调用外部模型或写入业务状态SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此计费需要拆成两层: 当前光点钱包余额、充值流水与邀请奖励已经收口到 `server-rs/crates/spacetime-module/src/runtime/profile.rs`。资产图片生成和作品发布由 Axum API 调用外部模型或写入业务状态SpacetimeDB reducer/procedure 不能直接执行外部网络生成,因此计费需要拆成两层:
- SpacetimeDB 负责钱包余额和流水的原子变更。 - SpacetimeDB 负责钱包余额和流水的原子变更。
- Axum 资产操作服务负责在执行业务资产操作前扣费,并在生成、持久化或发布失败时补偿退款。 - Axum 资产操作服务负责在执行业务资产操作前扣费,并在生成、持久化或发布失败时补偿退款。
@@ -24,13 +24,13 @@
暂不接入以下入口: 暂不接入以下入口:
- 旧资产工坊角色主形象/动作生成接口:当前仍使用 `asset-tool` 作为兼容归属,无法确认真实用户。 - 旧资产工坊角色主形象/动作生成接口:当前仍使用 `asset-tool` 作为兼容归属,无法确认真实用户。
- 手动上传封面:不调用外部生成模型,不消耗陶泥币 - 手动上传封面:不调用外部生成模型,不消耗光点
- 自定义世界草稿自动补图链路:属于后台补全流程,避免一次用户操作触发多笔不可预期扣费。 - 自定义世界草稿自动补图链路:属于后台补全流程,避免一次用户操作触发多笔不可预期扣费。
- 文本实体、NPC 生成:本次需求聚焦图片资产和发布资产操作,首期只覆盖可明确归属的入口。 - 文本实体、NPC 生成:本次需求聚焦图片资产和发布资产操作,首期只覆盖可明确归属的入口。
## 计费规则 ## 计费规则
- 每次可计费资产操作消耗 `1`陶泥币 - 每次可计费资产操作消耗 `1`光点
- 图片生成和作品发布都按资产操作计费;余额不足时禁止继续执行。 - 图片生成和作品发布都按资产操作计费;余额不足时禁止继续执行。
- 在调用外部图片生成或发布 mutation 前预扣,余额不足时直接返回业务错误,不继续调用后续资产操作。 - 在调用外部图片生成或发布 mutation 前预扣,余额不足时直接返回业务错误,不继续调用后续资产操作。
- 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,资产操作服务自动发起同额退款。 - 如果图片生成、远程下载、OSS 写入、资产记录确认或发布 mutation 失败,资产操作服务自动发起同额退款。

View File

@@ -0,0 +1,63 @@
# 新账号短信登录后置邀请码弹窗设计
日期:`2026-05-01`
## 1. 目标
账号入口不再展示独立注册入口。用户统一从短信登录进入,后端通过 `POST /api/auth/phone/login` 返回的 `created` 字段判断本次是否创建了新账号。
`created=true` 时,前端在登录成功后额外弹出独立邀请码面板:
1. 标题固定为 `请填写邀请码`
2. 标题下方展示邀请码输入框。
3. 输入为空时主按钮显示 `跳过`,点击后关闭面板。
4. 输入非空时主按钮显示 `提交`,点击后提交邀请码。
5. 面板右上角提供取消按钮,点击后关闭面板。
## 2. 入口调整
登录弹窗只保留可用登录方式:
1. 短信登录。
2. 密码登录。
3. 微信登录。
不得再展示 `注册` 页签、注册按钮或注册表单。邀请码不再出现在短信验证码表单中,避免用户把登录和注册理解成两套入口。
## 3. 邀请码提交
后置弹窗提交邀请码时调用已登录接口:
```text
POST /api/profile/referrals/redeem-code
```
请求体:
```json
{
"inviteCode": "SPRING2026"
}
```
后端继续使用 SpacetimeDB 的 `redeem_profile_referral_invite_code` procedure 作为唯一真相源。该 procedure 已负责校验:
1. 每个用户最多只能填写一个邀请码。
2. 邀请码必须存在。
3. 用户不能填写自己的邀请码。
4. 双方奖励与钱包流水在同一事务内落地。
## 4. URL 邀请码
若地址中存在 `inviteCode``invite_code`,前端只将其作为新账号后置弹窗的默认输入值。它不会触发注册页签,也不会在短信登录请求中提前提交。
若用户登录的是已有账号,则不会弹出新账号邀请码面板。
## 5. 完成定义
1. 登录弹窗内不可见注册入口。
2. 短信登录创建新账号后弹出邀请码面板。
3. 邀请码为空时按钮为 `跳过`,非空时按钮为 `提交`
4. 取消按钮可关闭面板。
5. 已登录邀请码接口允许提交,并继续由 SpacetimeDB procedure 兜底业务校验。
6. 前端测试覆盖注册入口删除、新账号弹窗、URL 邀请码预填与提交。

View File

@@ -34,7 +34,7 @@ Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot`
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `user_id` | `String` | 主键。 | | `user_id` | `String` | 主键。 |
| `public_user_code` | `String` | 公开陶泥号。 | | `public_user_code` | `String` | 公开百梦号。 |
| `username` | `String` | 当前账号用户名。 | | `username` | `String` | 当前账号用户名。 |
| `display_name` | `String` | 展示名。 | | `display_name` | `String` | 展示名。 |
| `phone_number_masked` | `Option<String>` | 脱敏手机号。 | | `phone_number_masked` | `Option<String>` | 脱敏手机号。 |

View File

@@ -37,7 +37,7 @@
- 当前场景的核心任务描述。 - 当前场景的核心任务描述。
- 文本会作为游戏中首次进入某个场景生成章节任务的关键上下文。 - 文本会作为游戏中首次进入某个场景生成章节任务的关键上下文。
- 必须结合场景描述、场景入口钩子、出场角色与 3 幕事件,说明玩家首次进入该场景时要完成什么。 - 必须结合场景描述、场景入口钩子、出场角色与 3 幕事件,说明玩家首次进入该场景时要完成什么。
- 世界档案的场景详情页必须直接展示该字段,便于陶泥主确认每个场景的默认章节任务。 - 世界档案的场景详情页必须直接展示该字段,便于百梦主确认每个场景的默认章节任务。
### Landmark 生成源字段 ### Landmark 生成源字段

View File

@@ -406,6 +406,12 @@ Node 侧入口位于:
5. `profile_dashboard_state.total_play_time_ms` 通过同一用户同一世界的 `runtimeStats.playTimeMs - last_observed_play_time_ms` 增量累积,后端使用 `saturating_sub` 防止旧快照回退导致负增量。 5. `profile_dashboard_state.total_play_time_ms` 通过同一用户同一世界的 `runtimeStats.playTimeMs - last_observed_play_time_ms` 增量累积,后端使用 `saturating_sub` 防止旧快照回退导致负增量。
6. 作品卡上的公开热度计数如果需要覆盖 RPG 作品,应另立公开作品统计方案;不能把个人 `profile_played_world` 误当成全站作品 `playCount` 6. 作品卡上的公开热度计数如果需要覆盖 RPG 作品,应另立公开作品统计方案;不能把个人 `profile_played_world` 误当成全站作品 `playCount`
## 10.2 2026-05-01 新用户注册赠送修正
新注册用户默认获得 `10` 个光点,注册链路通过 SpacetimeDB procedure 写入 `profile_dashboard_state.wallet_balance``profile_wallet_ledger`。流水来源为 `new_user_registration_reward`,流水 ID 固定为 `new-user-registration:{user_id}`,重复调用不重复发放。
注册赠送、邀请码奖励、充值、兑换码、资产扣费等都属于真实平台钱包流水。用户只要已经存在非 `snapshot_sync` 钱包流水,后续 `runtime_snapshot.game_state.playerCurrency` 不再覆盖 `wallet_balance`,只继续刷新游玩时长和玩过世界,避免首次保存旧运行态货币字段时把注册赠送覆盖成 `0`
## 11. 测试策略 ## 11. 测试策略
### 11.1 必跑 ### 11.1 必跑

View File

@@ -6,33 +6,33 @@
本轮在“我的”页面的“会员充值”入口落地账户充值弹窗,包含两个页签: 本轮在“我的”页面的“会员充值”入口落地账户充值弹窗,包含两个页签:
1. `陶泥币充值` 1. `光点充值`
2. `会员卡充值` 2. `会员卡充值`
前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 `server-rs` 后端返回。当前没有真实支付网关,本轮采用服务端模拟支付成功:创建订单后立即写入余额或会员状态,并返回最新账户中心快照。后续接入真实支付时,只替换订单支付状态推进,不改前端套餐与账户快照 contract。 前端只负责展示与发起购买,套餐、价格、赠送规则、会员权益、生效时间、钱包余额与交易流水统一由 `server-rs` 后端返回。当前没有真实支付网关,本轮采用服务端模拟支付成功:创建订单后立即写入余额或会员状态,并返回最新账户中心快照。后续接入真实支付时,只替换订单支付状态推进,不改前端套餐与账户快照 contract。
## 2. 产品规则 ## 2. 产品规则
### 2.1 陶泥币充值套餐 ### 2.1 光点充值套餐
| productId | 陶泥币 | 金额分 | 徽标 | 说明 | | productId | 光点 | 金额分 | 徽标 | 说明 |
| --- | ---: | ---: | --- | --- | | --- | ---: | ---: | --- | --- |
| `points_60` | 60 | 600 | 首充双倍 | 首充送60陶泥币 | | `points_60` | 60 | 600 | 首充双倍 | 首充送60光点 |
| `points_180` | 180 | 1800 | 首充双倍 | 首充送180陶泥币 | | `points_180` | 180 | 1800 | 首充双倍 | 首充送180光点 |
| `points_300` | 300 | 3000 | 首充双倍 | 首充送300陶泥币 | | `points_300` | 300 | 3000 | 首充双倍 | 首充送300光点 |
| `points_680` | 680 | 6800 | 首充双倍 | 首充送680陶泥币 | | `points_680` | 680 | 6800 | 首充双倍 | 首充送680光点 |
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280陶泥币 | | `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280光点 |
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280陶泥币 | | `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280光点 |
陶泥币充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账陶泥币为基础陶泥币与等额赠送陶泥币之和;已有充值流水后只到账基础陶泥币。实际到账陶泥币写入交易流水,余额以 SpacetimeDB projection 为准。 光点充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账光点为基础光点与等额赠送光点之和;已有充值流水后只到账基础光点。实际到账光点写入交易流水,余额以 SpacetimeDB projection 为准。
### 2.2 会员卡套餐 ### 2.2 会员卡套餐
| productId | 类型 | 天数 | 金额分 | 权益 | | productId | 类型 | 天数 | 金额分 | 权益 |
| --- | --- | ---: | ---: | --- | | --- | --- | ---: | ---: | --- |
| `member_month` | 月卡 | 30 | 2800 | 免陶泥币回合数100每日签到加成0% | | `member_month` | 月卡 | 30 | 2800 | 免光点回合数100每日签到加成0% |
| `member_season` | 季卡 | 90 | 7800 | 免陶泥币回合数100每日签到加成100% | | `member_season` | 季卡 | 90 | 7800 | 免光点回合数100每日签到加成100% |
| `member_year` | 年卡 | 365 | 24800 | 免陶泥币回合数100每日签到加成210% | | `member_year` | 年卡 | 365 | 24800 | 免光点回合数100每日签到加成210% |
购买会员时,如果当前会员仍有效,则从当前到期时间顺延;如果已过期或从未购买,则从当前服务端时间开始计算。状态只区分 `普通` 与已生效会员,前端不自行推断。 购买会员时,如果当前会员仍有效,则从当前到期时间顺延;如果已过期或从未购买,则从当前服务端时间开始计算。状态只区分 `普通` 与已生效会员,前端不自行推断。
@@ -42,8 +42,8 @@
需要 Bearer JWT。返回 需要 Bearer JWT。返回
1. 当前陶泥币余额、会员状态、到期时间 1. 当前光点余额、会员状态、到期时间
2. 陶泥币套餐与会员套餐 2. 光点套餐与会员套餐
3. 会员权益表 3. 会员权益表
4. 最近订单摘要 4. 最近订单摘要
@@ -64,7 +64,7 @@
1. 校验 `productId` 1. 校验 `productId`
2. 后端创建已支付订单 2. 后端创建已支付订单
3. 陶泥币套餐写入钱包余额与流水 3. 光点套餐写入钱包余额与流水
4. 会员套餐写入会员状态 4. 会员套餐写入会员状态
5. 返回最新账户中心快照与订单摘要 5. 返回最新账户中心快照与订单摘要
@@ -74,15 +74,15 @@
1. “我的”页会员充值按钮打开独立弹窗,不在当前面板下方展开。 1. “我的”页会员充值按钮打开独立弹窗,不在当前面板下方展开。
2. 弹窗顶部标题为 `账户充值`,右上角关闭。 2. 弹窗顶部标题为 `账户充值`,右上角关闭。
3. 默认打开 `陶泥币充值`,可切换到 `会员卡充值` 3. 默认打开 `光点充值`,可切换到 `会员卡充值`
4. 点击套餐后调用下单接口,按钮进入处理中状态,成功后刷新 `profileDashboard` 4. 点击套餐后调用下单接口,按钮进入处理中状态,成功后刷新 `profileDashboard`
5. 弹窗内不写大段说明文案,只保留必要金额、陶泥币、会员权益和状态反馈。 5. 弹窗内不写大段说明文案,只保留必要金额、光点、会员权益和状态反馈。
6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。 6. 会员卡充值区以套餐卡片优先展示周期、价格和处理状态;移动端单列,桌面端三列,权益表允许横向滚动,避免小屏挤压。
## 5. 验收 ## 5. 验收
1. 普通用户打开弹窗能看到陶泥币与会员套餐。 1. 普通用户打开弹窗能看到光点与会员套餐。
2. 陶泥币购买后余额增加,流水来源为 `points_recharge` 2. 光点购买后余额增加,流水来源为 `points_recharge`
3. 首充赠送只在首次陶泥币充值时生效。 3. 首充赠送只在首次光点充值时生效。
4. 会员购买后会员状态与到期时间立即更新。 4. 会员购买后会员状态与到期时间立即更新。
5. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。 5. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。

View File

@@ -13,9 +13,9 @@
## 2. 前端交互 ## 2. 前端交互
### 2.1 陶泥号复制 ### 2.1 百梦号复制
1. 点击“我的”页陶泥号后的复制按钮后,按钮文案临时切换为 `已复制` 1. 点击“我的”页百梦号后的复制按钮后,按钮文案临时切换为 `已复制`
2. 复制失败时临时切换为 `复制失败` 2. 复制失败时临时切换为 `复制失败`
3. 状态自动恢复为 `复制` 3. 状态自动恢复为 `复制`
@@ -84,7 +84,7 @@ SpacetimeDB 正式表 `user_account` 需要增加 `avatar_url: Option<String>`
## 5. 验收 ## 5. 验收
1. 创作页已发布作品分享按钮点击后显示 `已复制` 1. 创作页已发布作品分享按钮点击后显示 `已复制`
2. “我的”页陶泥号复制按钮点击后显示 `已复制` 2. “我的”页百梦号复制按钮点击后显示 `已复制`
3. “我的”页不展示 `手机号``正常` 标签。 3. “我的”页不展示 `手机号``正常` 标签。
4. 昵称编辑成功后,资料卡与顶部账号入口同步新昵称。 4. 昵称编辑成功后,资料卡与顶部账号入口同步新昵称。
5. 昵称与头像裁剪弹窗面板不透明,不能露出底层页面内容。 5. 昵称与头像裁剪弹窗面板不透明,不能露出底层页面内容。

View File

@@ -1,20 +1,20 @@
# 我的 Tab 邀请与玩家社区首期落地方案 # 我的 Tab 邀请与玩家社区首期落地方案
更新时间:`2026-04-29` 更新时间:`2026-05-01`
## 目标 ## 目标
在现有“我的”Tab 常用功能落地三个轻量入口: 在现有“我的”Tab 功能入口区(常用功能落地三个轻量入口,入口顺序固定为 `邀请好友``填邀请码``玩家社区`
1. `邀请好友`:弹出面板展示当前账号绑定的邀请码。 1. `邀请好友`:弹出面板展示当前账号绑定的邀请码、邀请奖励规则和成功邀请用户列表
2. `填邀请码`:弹出面板填写邀请码,成功后邀请者与被邀请者各获得 `30` 陶泥币 2. `填邀请码`:弹出面板填写邀请码,成功后邀请者与被邀请者各获得 `30` 光点
3. `玩家社区`:弹出面板展示微信群与 QQ 群正式二维码图片。 3. `玩家社区`:弹出面板展示微信群与 QQ 群正式二维码图片。
## 后端边界 ## 后端边界
- 邀请码、邀请关系与奖励发放全部存入 `server-rs/crates/spacetime-module` - 邀请码、邀请关系与奖励发放全部存入 `server-rs/crates/spacetime-module`
- Axum 只做鉴权、参数转发与响应映射,不在 API 层自行计算奖励。 - Axum 只做鉴权、参数转发与响应映射,不在 API 层自行计算奖励。
- 前端只读取后端状态与调用提交接口,不做本地加陶泥币 - 前端只读取后端状态与调用提交接口,不做本地加光点
- 钱包余额继续复用 `profile_dashboard_state.wallet_balance` - 钱包余额继续复用 `profile_dashboard_state.wallet_balance`
- 奖励流水继续复用 `profile_wallet_ledger`,新增来源类型: - 奖励流水继续复用 `profile_wallet_ledger`,新增来源类型:
- `invite_inviter_reward` - `invite_inviter_reward`
@@ -43,7 +43,7 @@
- 每个用户拥有一个稳定邀请码,首次进入邀请中心时自动生成。 - 每个用户拥有一个稳定邀请码,首次进入邀请中心时自动生成。
- 用户不能填写自己的邀请码。 - 用户不能填写自己的邀请码。
- 用户最多填写一个邀请码,成功后不可修改。 - 用户最多填写一个邀请码,成功后不可修改。
- 被邀请者绑定成功后获得 `30` 陶泥币 - 被邀请者绑定成功后获得 `30` 光点
- 邀请者每天最多获得 `10` 次邀请奖励,超过后关系仍可绑定,被邀请者仍获得奖励,邀请者当次不再加分。 - 邀请者每天最多获得 `10` 次邀请奖励,超过后关系仍可绑定,被邀请者仍获得奖励,邀请者当次不再加分。
- 每次奖励都写入钱包流水,钱包余额以后端返回为准。 - 每次奖励都写入钱包流水,钱包余额以后端返回为准。
@@ -51,7 +51,26 @@
### `GET /api/runtime/profile/referrals/invite-center` ### `GET /api/runtime/profile/referrals/invite-center`
返回当前用户的邀请码、邀请链接、今日奖励次数、剩余奖励次数、已绑定状态奖励参数。 返回当前用户的邀请码、邀请链接、今日奖励次数、剩余奖励次数、已绑定状态奖励参数与成功邀请用户列表
成功邀请用户列表字段:
```json
{
"invitedUsers": [
{
"userId": "user_001",
"displayName": "百梦玩家",
"avatarUrl": null,
"boundAt": "2026-05-01T08:00:00Z"
}
]
}
```
- `invitedUsers` 只包含当前账号作为邀请人的关系。
- 列表按 `boundAt` 倒序返回,最多展示最近 `20` 位成功邀请用户。
- 昵称与头像从 `user_account` 读取;缺失昵称时前端回退展示 `玩家`
### `POST /api/runtime/profile/referrals/redeem-code` ### `POST /api/runtime/profile/referrals/redeem-code`
@@ -69,13 +88,18 @@
- `server-rs/crates/spacetime-module` 已新增邀请码与邀请关系表,邀请中心读取和填码绑定均通过 SpacetimeDB procedure 执行。 - `server-rs/crates/spacetime-module` 已新增邀请码与邀请关系表,邀请中心读取和填码绑定均通过 SpacetimeDB procedure 执行。
- `server-rs/crates/api-server` 已挂接 `/api/runtime/profile/referrals/*``/api/profile/referrals/*` 两组路由。 - `server-rs/crates/api-server` 已挂接 `/api/runtime/profile/referrals/*``/api/profile/referrals/*` 两组路由。
- 前端“我的”Tab 三个快捷入口均打开独立弹窗,玩家社区使用 `media/social-media-group/wechat.png``media/social-media-group/qq.png` 两张正式二维码图片。 - 前端“我的”Tab 三个功能入口均打开独立弹窗,玩家社区使用 `media/social-media-group/wechat.png``media/social-media-group/qq.png` 两张正式二维码图片。
- 复制邀请会复制邀请码和邀请链接;填码成功后刷新个人看板陶泥币 - 复制邀请会复制邀请码和邀请链接;填码成功后刷新个人看板光点
- 邀请好友弹窗展示 `邀请一个用户注册,双方都可获得 30 光点。每日最多获得十次邀请奖励。`,不再展示“邀请 / 已奖 / 今日”三项统计。
- 邀请好友弹窗底部展示成功邀请用户头像和昵称列表;没有成功邀请时展示短空状态。
- “我的”页 `邀请好友` 按钮副标题展示 `双方得30光点icon``玩家社区` 按钮副标题展示 `每日领福利`
- “我的”页功能入口区不展示 `常用功能` 标题和 `快捷入口` 副标题,避免首屏重复说明类文案。
## 前端交互 ## 前端交互
- 三个入口继续放在“我的”Tab 常用功能,不新增页面。 - 三个入口继续放在“我的”Tab 功能入口区(常用功能,不新增页面。
- `邀请好友` 弹窗展示邀请码、复制按钮、邀请链接 - `邀请好友` 弹窗展示邀请码、复制按钮、邀请奖励规则和成功邀请用户头像昵称列表
- `填邀请码` 弹窗在未绑定时展示输入框;已绑定时展示短状态 - `填邀请码` 入口只在账号注册后 `24` 小时内且尚未填写过邀请码时展示;若 `auth.user.createdAt` 缺失或解析失败,前端按已超时处理并隐藏入口
- `填邀请码` 弹窗在未绑定时展示输入框;成功绑定后刷新邀请中心与个人看板,并隐藏常用功能里的入口。
- `玩家社区` 弹窗展示两个紧凑二维码图片区,保留微信群与 QQ 群短标签。 - `玩家社区` 弹窗展示两个紧凑二维码图片区,保留微信群与 QQ 群短标签。
- 弹窗文案只保留必要标签和短提示,不放长规则说明 - 弹窗文案只保留必要标签和短提示;本次邀请奖励规则属于必要交易说明,固定展示在邀请码下方

View File

@@ -1,6 +1,6 @@
# 密码登录入口历史落地设计 # 密码登录入口历史落地设计
> 2026-04-25 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经登录后设置过密码的手机号账号。`POST /api/auth/entry` 只接受 `phone + password`,不支持邮箱、用户名或陶泥号登录,也不承担自动建号能力。本文原有“密码自动建号”内容仅作为历史背景保留,当前落地以本更新和 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准。 > 2026-04-25 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经登录后设置过密码的手机号账号。`POST /api/auth/entry` 只接受 `phone + password`,不支持邮箱、用户名或百梦号登录,也不承担自动建号能力。本文原有“密码自动建号”内容仅作为历史背景保留,当前落地以本更新和 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准。
> >
> 2026-04-28 更新:为开发期本地/测试服联调新增服务端环境变量 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED`,默认 `false`。仅当该变量显式为 `true` 时,`POST /api/auth/entry` 可对未知手机号用本次密码直接创建账号并登录;默认关闭时仍严格保持未知手机号返回 `401` 的生产语义。该开关不得用于生产环境,也不新增任何前端规则说明文案。 > 2026-04-28 更新:为开发期本地/测试服联调新增服务端环境变量 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED`,默认 `false`。仅当该变量显式为 `true` 时,`POST /api/auth/entry` 可对未知手机号用本次密码直接创建账号并登录;默认关闭时仍严格保持未知手机号返回 `401` 的生产语义。该开关不得用于生产环境,也不新增任何前端规则说明文案。
@@ -17,7 +17,7 @@
1. `api-server` 对外只暴露 `phone + password` 的最小接口。 1. `api-server` 对外只暴露 `phone + password` 的最小接口。
2. `module-auth` 只负责已存在手机号账号的密码校验。 2. `module-auth` 只负责已存在手机号账号的密码校验。
3. 密码入口不创建账号,不接收邮箱、用户名或陶泥号。 3. 密码入口不创建账号,不接收邮箱、用户名或百梦号。
4. 登录成功后与 JWT、refresh cookie 的衔接方式。 4. 登录成功后与 JWT、refresh cookie 的衔接方式。
## 1.1 当前冻结结论 ## 1.1 当前冻结结论
@@ -239,7 +239,7 @@
1. 未知手机号密码登录返回 `401`,且不创建账号。 1. 未知手机号密码登录返回 `401`,且不创建账号。
2. 已登录手机号账号设置密码后可用 `phone + password` 登录。 2. 已登录手机号账号设置密码后可用 `phone + password` 登录。
3. 同手机号错误密码返回 `401` 3. 同手机号错误密码返回 `401`
4. 邮箱、用户名或陶泥号作为密码登录标识返回 `400` 4. 邮箱、用户名或百梦号作为密码登录标识返回 `400`
5. 登录成功时返回 access token。 5. 登录成功时返回 access token。
6. 登录成功时写回 refresh cookie。 6. 登录成功时写回 refresh cookie。
7. `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED` 默认关闭时行为不变。 7. `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED` 默认关闭时行为不变。

View File

@@ -19,7 +19,7 @@
沿用现有 `POST /api/auth/entry` 沿用现有 `POST /api/auth/entry`
1. 请求字段固定为 `phone``password`,前端只提交手机号。 1. 请求字段固定为 `phone``password`,前端只提交手机号。
2. 后端只按标准手机号归一化后查找账号,不兼容邮箱、用户名、陶泥号或历史开发游客标识。 2. 后端只按标准手机号归一化后查找账号,不兼容邮箱、用户名、百梦号或历史开发游客标识。
3. 手机号不存在时返回 `401`,不创建账号。 3. 手机号不存在时返回 `401`,不创建账号。
4. 手机号存在但未设置密码时返回 `401` 4. 手机号存在但未设置密码时返回 `401`
5. 校验成功后签发 access token并写入 refresh cookie。 5. 校验成功后签发 access token并写入 refresh cookie。

View File

@@ -0,0 +1,26 @@
# 平台首页作品模糊搜索 2026-05-01
## 背景
首页顶部搜索框原本主要承担公开编号直达能力,适合输入 `SY / CW / BF / M3 / PZ` 编号后打开用户或作品详情。用户在浏览首页时也会按作品名称、作者昵称或作品描述回忆作品,现有搜索口径没有覆盖这些常见路径。
## 落地规则
1. 首页搜索框先在当前公开作品聚合列表中做本地模糊匹配。
2. 匹配字段包含:
- 作品 ID`publicWorkCode``profileId``workId`
- 作品名称:`worldName`
- 作者昵称:`authorDisplayName`
- 作品描述:`summaryText``subtitle`
3. 匹配忽略大小写,允许用户输入去掉空格、连字符或下划线后的连续片段,例如 `PZEPUBLIC1` 命中 `PZ-EPUBLIC1`
4. 输入完整公开作品号并本地命中时,保留既有作品号直达行为。
5. 输入模糊片段命中公开作品时,在首页直接展示搜索结果列表,点击结果打开对应作品详情。
6. 当前公开作品列表无命中时,保留既有公开编号直达兜底,继续支持远端按作品号或百梦号查找。
## 验收标准
1. 输入作品号片段可命中对应公开作品,输入完整公开作品号仍可直达。
2. 输入作品名称片段可命中对应公开作品。
3. 输入作者昵称片段可命中对应公开作品。
4. 输入作品描述片段可命中对应公开作品。
5. 未命中本地公开作品时,原有编号搜索行为不退化。

View File

@@ -1,23 +1,24 @@
# 陶泥产品命名替换落地说明 # 百梦产品命名规范落地说明
## 背景 ## 背景
本轮将产品中文展示名从“叙世”调整为“陶泥”,并同步调整平台内三类对外称谓: 平台对外中文命名统一使用以下称谓:
- `叙世币` 对外展示为 `陶泥币` - 产品中文展示名:`百梦`
- `叙世号` 对外展示为 `陶泥号` - 平台内消费单位:`光点`
- `创作者` 对外展示为 `陶泥主` - 公开账号标识:`百梦号`
- 创作侧面向创作者称谓:`百梦主`
## 落地边界 ## 落地边界
1. 前端页面、弹窗、测试断言和后端返回给用户的中文错误文案统一使用新称谓。 1. 前端页面、弹窗、测试断言和后端返回给用户的中文错误文案统一使用新称谓。
2. SpacetimeDB 表字段、Rust/TypeScript contract 字段、流水来源枚举、`points_*` 商品 ID、`public_user_code` 字段名继续保持不变,避免引入数据库迁移和历史数据兼容风险。 2. SpacetimeDB 表字段、Rust/TypeScript contract 字段、流水来源枚举、`points_*` 商品 ID、`public_user_code` 字段名继续保持不变,避免引入数据库迁移和历史数据兼容风险。
3. 公开编号现有 `SY-XXXXXXXX` 格式本轮不迁移,只调整用户可见标签为“陶泥号”;编号格式如需改为新前缀,应另起迁移方案并同步老用户兼容策略。 3. 公开编号现有 `SY-XXXXXXXX` 格式本轮不迁移,只调整用户可见标签为“百梦号”;编号格式如需改为新前缀,应另起迁移方案并同步老用户兼容策略。
4. 历史日志、构建产物、第三方依赖和生成绑定不参与本轮文本替换。 4. 历史日志、构建产物、第三方依赖和生成绑定不参与本轮文本替换。
## 验收点 ## 验收点
1. 首页、登录绑定页、我的页和搜索结果不再展示旧产品名。 1. 首页、登录绑定页、我的页和搜索结果不再展示旧产品名。
2. 钱包、充值、邀请、兑换码、资产计费和拼图道具确认文案统一展示“陶泥币”。 2. 钱包、充值、邀请、兑换码、资产计费和拼图道具确认文案统一展示“光点”。
3. 账号公开标识相关错误和搜索空状态统一展示“陶泥号”。 3. 账号公开标识相关错误和搜索空状态统一展示“百梦号”。
4. 创作相关可见默认称谓使用“陶泥主”。 4. 创作相关可见默认称谓使用“百梦主”。

View File

@@ -0,0 +1,24 @@
# 百梦产品命名替换落地说明
## 1. 本轮目标
本轮统一平台对外中文命名,当前生效称谓如下:
- 产品中文展示名:`百梦`
- 平台内消费单位:`光点`
- 公开账号标识:`百梦号`
- 创作侧面向创作者称谓:`百梦主`
## 2. 落地范围
1. 前端网页、管理后台、HTML 标题、metadata 与品牌标识统一展示“百梦”。
2. 钱包、充值、邀请、兑换码、资产计费、拼图道具与作者激励统一展示“光点”。
3. 公开账号标识、搜索兜底、登录限制与错误信息统一展示“百梦号”。
4. 创作侧面向创作者的称谓统一展示“百梦主”。
5. 后端错误信息、默认商品文案、测试断言与文档说明同步更新。
## 3. 非目标
1. 本轮只调整对外文本不修改数据库字段名、API 字段名、流水 source、商品 productId 或现有 `SY-XXXXXXXX` 公开编号格式。
2. 不修改 SpacetimeDB 表结构,因此不需要新增 migration。
3. 不引入新的前端页面或后端系统。

View File

@@ -49,7 +49,7 @@ admin:{管理员用户名}:{邀请码}
## 验收标准 ## 验收标准
1. 未登录用户访问 `/?inviteCode=ABC123` 自动打开注册弹窗并预填 `ABC123` 1. 未登录用户访问 `/?inviteCode=ABC123` 自动打开注册弹窗并预填 `ABC123`
2. 有效邀请码注册成功后,被邀请人获得陶泥币奖励,邀请关系落库。 2. 有效邀请码注册成功后,被邀请人获得光点奖励,邀请关系落库。
3. 无效邀请码注册成功但不绑定,并返回短提示。 3. 无效邀请码注册成功但不绑定,并返回短提示。
4. 管理员可添加邀请码并写入 metadata重复提交同管理员同码更新 metadata。 4. 管理员可添加邀请码并写入 metadata重复提交同管理员同码更新 metadata。
5. 管理员邀请码被使用时不产生 `admin:*` 虚拟主体的钱包流水。 5. 管理员邀请码被使用时不产生 `admin:*` 虚拟主体的钱包流水。

View File

@@ -0,0 +1,34 @@
# 新用户注册默认光点落地说明
## 目标
每个新注册用户默认获得 `10` 个光点。赠送必须由后端统一落账,前端只展示余额和流水,不在本地补发或推算。
## 落账规则
1. 默认赠送数量固定为 `10`,由 `module-runtime` 暴露常量,避免不同 crate 散落数字。
2. 钱包余额继续使用 `profile_dashboard_state.wallet_balance`,流水继续使用 `profile_wallet_ledger`
3. 新增流水来源 `new_user_registration_reward`,用于区分注册赠送、邀请奖励、充值和兑换码。
4. 注册赠送的流水 ID 固定为 `new-user-registration:{user_id}`SpacetimeDB procedure 内做幂等保护,重复调用不重复加钱。
5. 手机号注册、开发密码自动注册、微信新用户绑定手机号后激活,都必须调用同一个注册赠送入口。
6. 微信 callback 阶段只会生成待绑定手机号的临时账号,不在该阶段赠送;只有绑定新手机号并激活该微信账号时才赠送。若绑定到已有手机号账号,则不补发注册赠送。
7. 注册赠送和邀请码奖励可以叠加。先发注册赠送,再处理邀请码奖励时,新用户最终余额为 `10 + 邀请奖励`
## 存档同步约束
`runtime_snapshot.game_state.playerCurrency` 是运行态内的旧货币字段,不允许覆盖已经存在真实钱包业务流水的账户余额。只要用户已有非 `snapshot_sync` 的钱包流水,后续存档同步只能累计游玩时长和玩过世界,不再用 `playerCurrency` 回写 `wallet_balance`
这样可以避免新用户注册赠送的 `10` 个光点,在首次保存 `playerCurrency = 0` 的运行态快照时被覆盖成 `0`
## 影响文件
1. `server-rs/crates/module-runtime/src/lib.rs`
2. `server-rs/crates/spacetime-module/src/runtime/profile.rs`
3. `server-rs/crates/spacetime-client/src/runtime.rs`
4. `server-rs/crates/api-server/src/phone_auth.rs`
5. `server-rs/crates/api-server/src/password_entry.rs`
6. `server-rs/crates/api-server/src/wechat_auth.rs`
7. `server-rs/crates/shared-contracts/src/runtime.rs`
8. `packages/shared/src/contracts/runtime.ts`
9. `src/components/rpg-entry/RpgEntryHomeView.tsx`

View File

@@ -2,9 +2,9 @@
## 1. 目标 ## 1. 目标
本轮在现有“我的”资料与钱包 projection 上新增兑换码能力。用户兑换成功后直接增加陶泥币余额,写入 `profile_wallet_ledger`,并同步刷新 `profile_dashboard_state.wallet_balance` 本轮在现有“我的”资料与钱包 projection 上新增兑换码能力。用户兑换成功后直接增加光点余额,写入 `profile_wallet_ledger`,并同步刷新 `profile_dashboard_state.wallet_balance`
管理侧本轮只提供后端 API不新增管理后台页面。私有兑换码创建时支持内部 `userId` 与公开陶泥号两类输入,后端创建阶段统一解析成内部 `userId` 存储。 管理侧本轮只提供后端 API不新增管理后台页面。私有兑换码创建时支持内部 `userId` 与公开百梦号两类输入,后端创建阶段统一解析成内部 `userId` 存储。
## 2. 兑换码类型 ## 2. 兑换码类型
@@ -26,7 +26,7 @@
| --- | --- | --- | | --- | --- | --- |
| `code` | `String` | 主键,标准化后的兑换码。 | | `code` | `String` | 主键,标准化后的兑换码。 |
| `mode` | `RuntimeProfileRedeemCodeMode` | 兑换码模式。 | | `mode` | `RuntimeProfileRedeemCodeMode` | 兑换码模式。 |
| `reward_points` | `u64` | 单次到账陶泥币。 | | `reward_points` | `u64` | 单次到账光点。 |
| `max_uses` | `u32` | 公共码为单用户上限,唯一码/私有码为全局上限。 | | `max_uses` | `u32` | 公共码为单用户上限,唯一码/私有码为全局上限。 |
| `global_used_count` | `u32` | 全局已使用次数。公共码也记录总使用次数,但不参与公共码上限判断。 | | `global_used_count` | `u32` | 全局已使用次数。公共码也记录总使用次数,但不参与公共码上限判断。 |
| `enabled` | `bool` | 是否启用。 | | `enabled` | `bool` | 是否启用。 |
@@ -42,7 +42,7 @@
| `usage_id` | `String` | 主键,格式 `redeem:{code}:{user_id}:{micros}:{sequence}`。 | | `usage_id` | `String` | 主键,格式 `redeem:{code}:{user_id}:{micros}:{sequence}`。 |
| `code` | `String` | 兑换码。 | | `code` | `String` | 兑换码。 |
| `user_id` | `String` | 兑换用户。 | | `user_id` | `String` | 兑换用户。 |
| `amount_granted` | `u64` | 到账陶泥币。 | | `amount_granted` | `u64` | 到账光点。 |
| `created_at` | `Timestamp` | 兑换时间。 | | `created_at` | `Timestamp` | 兑换时间。 |
索引:`code``user_id``(code, user_id)` 索引:`code``user_id``(code, user_id)`
@@ -121,7 +121,7 @@
“我的”页头像右侧入口由 `会员充值` 改为 `兑换码`。点击打开独立模态窗口,窗口内只保留输入框、兑换按钮和后端返回提示,不展示兑换规则说明。 “我的”页头像右侧入口由 `会员充值` 改为 `兑换码`。点击打开独立模态窗口,窗口内只保留输入框、兑换按钮和后端返回提示,不展示兑换规则说明。
成功后展示 `已到账 X 陶泥币`,并刷新 profile dashboard。失败后直接展示后端 `message` 成功后展示 `已到账 X 光点`,并刷新 profile dashboard。失败后直接展示后端 `message`
## 8. 测试矩阵 ## 8. 测试矩阵

View File

@@ -2,7 +2,7 @@
## 1. 背景 ## 1. 背景
当前前端展示的“陶泥号”由前端基于 `AuthUser.id` 临时拼装: 当前前端展示的“百梦号”由前端基于 `AuthUser.id` 临时拼装:
- 前缀固定为 `SY-` - 前缀固定为 `SY-`
-`user.id``username` 去除非字母数字字符后的末 8 位 -`user.id``username` 去除非字母数字字符后的末 8 位
@@ -174,7 +174,7 @@
1. `id` 返回内部 ID 仅供当前工程内部跳转与资源读取使用,不在 UI 上直接暴露为文案 1. `id` 返回内部 ID 仅供当前工程内部跳转与资源读取使用,不在 UI 上直接暴露为文案
2. 不返回手机号、登录方式、绑定状态、tokenVersion 等敏感字段 2. 不返回手机号、登录方式、绑定状态、tokenVersion 等敏感字段
3. 未命中返回 `404` 3. 未命中返回 `404`
4. `by-id` 仅接受内部 `user_XXXXXXXX` 这类用户 ID用于工程内跳转、运营排查或已有资源引用不替代公开陶泥号主搜索语义 4. `by-id` 仅接受内部 `user_XXXXXXXX` 这类用户 ID用于工程内跳转、运营排查或已有资源引用不替代公开百梦号主搜索语义
## 5.2 广场作品公开编号搜索 ## 5.2 广场作品公开编号搜索
@@ -251,7 +251,7 @@
## 7.1 账号展示 ## 7.1 账号展示
当前首页资料卡和桌面顶部都展示前端拼装陶泥号,改为: 当前首页资料卡和桌面顶部都展示前端拼装百梦号,改为:
1. 直接展示 `authUi.user.publicUserCode` 1. 直接展示 `authUi.user.publicUserCode`
2. 复制按钮复制后端返回值 2. 复制按钮复制后端返回值
@@ -262,7 +262,7 @@
广场作品卡和详情页增加: 广场作品卡和详情页增加:
1. 作品号 `CW-XXXXXXXX` 1. 作品号 `CW-XXXXXXXX`
2. 作者陶泥`SY-XXXXXXXX` 2. 作者百梦`SY-XXXXXXXX`
展示要求: 展示要求:
@@ -284,7 +284,7 @@
用户搜索命中后的最小行为: 用户搜索命中后的最小行为:
1. 打开独立用户搜索结果面板或对话框 1. 打开独立用户搜索结果面板或对话框
2. 展示头像字母、显示名、陶泥 2. 展示头像字母、显示名、百梦
3. 提供“查看该作者作品”入口 3. 提供“查看该作者作品”入口
作品搜索命中后的行为: 作品搜索命中后的行为:
@@ -325,7 +325,7 @@
## 11. 当前落地说明 ## 11. 当前落地说明
1. 首页陶泥号展示已优先读取后端 `publicUserCode`,原本基于 `AuthUser.id/username` 的前端拼装仅保留为兼容兜底,避免老会话未刷新时界面直接空白。 1. 首页百梦号展示已优先读取后端 `publicUserCode`,原本基于 `AuthUser.id/username` 的前端拼装仅保留为兼容兜底,避免老会话未刷新时界面直接空白。
2. 用户公开搜索与广场作品公开搜索均已改为调用后端匿名接口,前端只负责输入、展示与跳转,不再自行决定最终编号格式。 2. 用户公开搜索与广场作品公开搜索均已改为调用后端匿名接口,前端只负责输入、展示与跳转,不再自行决定最终编号格式。
3. 自定义世界发布链路已改为从认证服务读取真实 `public_user_code` 写入作品真相与广场读模型,不再从内部 `user_id` 临时反推 `SY-XXXXXXXX` 3. 自定义世界发布链路已改为从认证服务读取真实 `public_user_code` 写入作品真相与广场读模型,不再从内部 `user_id` 临时反推 `SY-XXXXXXXX`
4. 当前作品号 `public_work_code` 仍采用基于 `profile_id` 的稳定 fallback 方案生成 `CW-XXXXXXXX`;若后续补独立计数表,需要在不改变读写接口的前提下替换生成来源。 4. 当前作品号 `public_work_code` 仍采用基于 `profile_id` 的稳定 fallback 方案生成 `CW-XXXXXXXX`;若后续补独立计数表,需要在不改变读写接口的前提下替换生成来源。

View File

@@ -201,7 +201,7 @@ Rust DTO 只承载对前端公开的 HTTP contract不直接泄露 `module-puz
1. 每次生成 2 张候选图。 1. 每次生成 2 张候选图。
2. 候选图通过 `api-server` 写入 OSS兼容展示路径统一为 `/generated-puzzle-assets/...`,禁止再落到仓库 `public/` 目录。 2. 候选图通过 `api-server` 写入 OSS兼容展示路径统一为 `/generated-puzzle-assets/...`,禁止再落到仓库 `public/` 目录。
3. Axum 把候选图 URL、assetId、prompt snapshot 回写到 Spacetime session draft。 3. Axum 把候选图 URL、assetId、prompt snapshot 回写到 Spacetime session draft。
4. 陶泥主在结果页选择其中 1 张作为正式图。 4. 百梦主在结果页选择其中 1 张作为正式图。
这样可以保证: 这样可以保证:
@@ -211,7 +211,7 @@ Rust DTO 只承载对前端公开的 HTTP contract不直接泄露 `module-puz
### 6.1 发布前编辑真相补充 ### 6.1 发布前编辑真相补充
结果页允许陶泥主在发布前直接编辑: 结果页允许百梦主在发布前直接编辑:
1. `关卡名` 1. `关卡名`
2. `摘要` 2. `摘要`
@@ -449,6 +449,8 @@ finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3;
后续反馈要求合并块边界也要圆角后,外轮廓描边补充按四个角判断:只有相邻两条外露边同时存在的真实外轮廓角才应用圆角,内部拼接角保持直角且不显示分界线。 后续反馈要求合并块边界也要圆角后,外轮廓描边补充按四个角判断:只有相邻两条外露边同时存在的真实外轮廓角才应用圆角,内部拼接角保持直角且不显示分界线。
2026-05-01 追加修正:合并块的圆角不能继续依赖逐格 `border-radius` 叠加。运行时应根据合并组真实占据格提取一条整体 SVG 轮廓路径,外凸角和内凹角都通过同一条路径生成二次贝塞尔圆角;合并块图片层也必须用这条整体路径裁剪,避免 L 形、阶梯形凹口处仍露出直角图片像素。
### 12.4 第二关后打乱规则旁路修正 ### 12.4 第二关后打乱规则旁路修正
用户反馈“从第二关开始打乱规则像是完全相同”后,检查发现 `api-server` 的本地下一关 fallback 仍使用旧版 `build_local_puzzle_board` 固定左移一格,没有复用 `module-puzzle` 的种子化初始化规则。该路径会在图库/正式推荐不可用、由 API 临时构造下一关时触发。 用户反馈“从第二关开始打乱规则像是完全相同”后,检查发现 `api-server` 的本地下一关 fallback 仍使用旧版 `build_local_puzzle_board` 固定左移一格,没有复用 `module-puzzle` 的种子化初始化规则。该路径会在图库/正式推荐不可用、由 API 临时构造下一关时触发。

View File

@@ -14,8 +14,8 @@
1. 限定时间内未完成时弹出失败面板。 1. 限定时间内未完成时弹出失败面板。
2. 失败面板提供两个选择: 2. 失败面板提供两个选择:
- `重新开始`:重新开启当前拼图关卡,不扣陶泥币 - `重新开始`:重新开启当前拼图关卡,不扣光点
- `继续1分钟`:先弹出确认窗口,确认后消耗 `1` 陶泥币,并把当前失败关卡恢复为 `playing`,剩余时间固定为 `60000ms` - `继续1分钟`:先弹出确认窗口,确认后消耗 `1` 光点,并把当前失败关卡恢复为 `playing`,剩余时间固定为 `60000ms`
3. 进入拼图作品后立即写入 `profile_save_archive`,存档页显示拼图存档。 3. 进入拼图作品后立即写入 `profile_save_archive`,存档页显示拼图存档。
4. 每次进入下一关后更新同一条拼图存档,使存档恢复时指向最新可继续的关卡。 4. 每次进入下一关后更新同一条拼图存档,使存档恢复时指向最新可继续的关卡。
@@ -26,7 +26,7 @@
`PuzzleRuntimePropKind` 增加 `extendTime`,沿用现有道具确认与扣费接口: `PuzzleRuntimePropKind` 增加 `extendTime`,沿用现有道具确认与扣费接口:
1. 前端只在 `runtimeStatus = failed` 时开放 `继续1分钟` 1. 前端只在 `runtimeStatus = failed` 时开放 `继续1分钟`
2. 点击后打开独立确认弹窗,文案只显示短标题和 `消耗 1 陶泥币` 2. 点击后打开独立确认弹窗,文案只显示短标题和 `消耗 1 光点`
3. 正式 run 继续走 `POST /api/runtime/puzzle/runs/:runId/props` 3. 正式 run 继续走 `POST /api/runtime/puzzle/runs/:runId/props`
4. `api-server``extendTime` 映射为账单 `asset_kind = puzzle_prop_extend_time` 4. `api-server``extendTime` 映射为账单 `asset_kind = puzzle_prop_extend_time`
5. SpacetimeDB 侧只允许失败关卡续时;续时成功后: 5. SpacetimeDB 侧只允许失败关卡续时;续时成功后:
@@ -37,7 +37,7 @@
- 清空暂停与冻结生效点 - 清空暂停与冻结生效点
- 调整 `paused_accumulated_ms`,保证从确认成功那一刻开始完整倒计时 `60` - 调整 `paused_accumulated_ms`,保证从确认成功那一刻开始完整倒计时 `60`
本地调试 run 没有真实钱包,沿用本地道具兜底:仍弹确认窗,但不扣真实陶泥币 本地调试 run 没有真实钱包,沿用本地道具兜底:仍弹确认窗,但不扣真实光点
### 重新开始 ### 重新开始
@@ -91,7 +91,7 @@ SpacetimeDB 拼图运行态每次持久化 run 时同步刷新存档:
1. 倒计时归零后失败弹窗有 `重新开始``继续1分钟` 1. 倒计时归零后失败弹窗有 `重新开始``继续1分钟`
2. 点击 `继续1分钟` 后先出现扣费确认,确认成功后失败弹窗关闭并恢复 `60` 秒倒计时。 2. 点击 `继续1分钟` 后先出现扣费确认,确认成功后失败弹窗关闭并恢复 `60` 秒倒计时。
3. 陶泥币余额不足时确认弹窗保留,并展示错误。 3. 光点余额不足时确认弹窗保留,并展示错误。
4. 点击 `重新开始` 后当前关卡重新打乱并重置倒计时。 4. 点击 `重新开始` 后当前关卡重新打乱并重置倒计时。
5. 进入拼图作品后,存档页出现 `worldType = PUZZLE` 的拼图存档。 5. 进入拼图作品后,存档页出现 `worldType = PUZZLE` 的拼图存档。
6. 通过一关后,只要后端确认同作品下一关存在,同一条存档立即更新到新关卡;没有同作品下一关时保留已完成关卡,等待玩家选择相似作品。 6. 通过一关后,只要后端确认同作品下一关存在,同一条存档立即更新到新关卡;没有同作品下一关时保留已完成关卡,等待玩家选择相似作品。

View File

@@ -12,7 +12,7 @@
2. 新 session 的 `seedText` 允许为空SpacetimeDB 侧用空锚点和空表单草稿初始化,不得把默认题材文案写入玩家草稿字段。 2. 新 session 的 `seedText` 允许为空SpacetimeDB 侧用空锚点和空表单草稿初始化,不得把默认题材文案写入玩家草稿字段。
3. 初始表单输入自动保存到 session 的 `draft_json``puzzle_work_profile` 投影。保存字段只包含 `workTitle``workDescription``pictureDescription`、可推断标签和一个 `generationStatus = idle` 的默认关卡草稿设置阶段默认关卡名称必须为空不得写入“第一关”“第1关”或作品名称作为默认值。参考图只保存在当前前端会话内不落入 SpacetimeDB。 3. 初始表单输入自动保存到 session 的 `draft_json``puzzle_work_profile` 投影。保存字段只包含 `workTitle``workDescription``pictureDescription`、可推断标签和一个 `generationStatus = idle` 的默认关卡草稿设置阶段默认关卡名称必须为空不得写入“第一关”“第1关”或作品名称作为默认值。参考图只保存在当前前端会话内不落入 SpacetimeDB。
4. 玩家在生成草稿前退出,再次从创作中心点击这条拼图草稿时,必须恢复到填表页,并回填之前自动保存的作品名称、作品描述和画面描述;只有执行 `compile_puzzle_draft` 且生成结果页草稿后,草稿入口才进入结果页。 4. 玩家在生成草稿前退出,再次从创作中心点击这条拼图草稿时,必须恢复到填表页,并回填之前自动保存的作品名称、作品描述和画面描述;只有执行 `compile_puzzle_draft` 且生成结果页草稿后,草稿入口才进入结果页。
5. 表单自动保存走 `save_puzzle_form_draft` action不消耗陶泥币,不生成图片,不改变 `stage = collecting_anchors`;生成草稿按钮仍单独触发 `compile_puzzle_draft` 并进入进度页。 5. 表单自动保存走 `save_puzzle_form_draft` action不消耗光点,不生成图片,不改变 `stage = collecting_anchors`;生成草稿按钮仍单独触发 `compile_puzzle_draft` 并进入进度页。
6. 点击拼图入口始终创建新草稿,不复用上一次未完成 session恢复旧草稿只通过“我的创作”中的草稿卡进入。 6. 点击拼图入口始终创建新草稿,不复用上一次未完成 session恢复旧草稿只通过“我的创作”中的草稿卡进入。
7. 若 Maincloud 仍运行旧 wasm缺少 `save_puzzle_form_draft` procedure前端提交生成或生成失败页重试时不得继续复用空 `seedText` 的表单 session必须用当前表单 payload 新建带真实 seed 的 session 再执行 `compile_puzzle_draft` 7. 若 Maincloud 仍运行旧 wasm缺少 `save_puzzle_form_draft` procedure前端提交生成或生成失败页重试时不得继续复用空 `seedText` 的表单 session必须用当前表单 payload 新建带真实 seed 的 session 再执行 `compile_puzzle_draft`
8. api-server 也要兼容旧 wasm`save_puzzle_form_draft` 缺失时,自动保存 action 降级返回当前 session`compile_puzzle_draft` 前置保存缺失且当前 session 为空 seed 时,创建一条带表单 seed 的替代 session 后继续编译,避免再次暴露 `No such procedure` 8. api-server 也要兼容旧 wasm`save_puzzle_form_draft` 缺失时,自动保存 action 降级返回当前 session`compile_puzzle_draft` 前置保存缺失且当前 session 为空 seed 时,创建一条带表单 seed 的替代 session 后继续编译,避免再次暴露 `No such procedure`
@@ -71,7 +71,7 @@
9. `StartPuzzleRunRequest` 新增可选 `levelId`。详情页或草稿结果页单独体验某关时传入目标关卡,后端从作品/草稿的 `levels` 中选取该关卡生成运行态。 9. `StartPuzzleRunRequest` 新增可选 `levelId`。详情页或草稿结果页单独体验某关时传入目标关卡,后端从作品/草稿的 `levels` 中选取该关卡生成运行态。
10. `ExecutePuzzleAgentActionRequest` 必须保留 `pictureDescription` 字段。表单直达生成时,`compile_puzzle_draft` 优先用 `pictureDescription` 作为首图 prompt再回退到旧 `promptText`;避免生成页展示的是玩家画面描述,但后端实际用作品名称或旧摘要出图。 10. `ExecutePuzzleAgentActionRequest` 必须保留 `pictureDescription` 字段。表单直达生成时,`compile_puzzle_draft` 优先用 `pictureDescription` 作为首图 prompt再回退到旧 `promptText`;避免生成页展示的是玩家画面描述,但后端实际用作品名称或旧摘要出图。
11. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。 11. `compile_puzzle_draft` 中的图片上游失败不得映射成 `400 BAD_REQUEST`。DashScope 返回 `InvalidParameter` 或任务失败时api-server 统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留“拼图图片生成失败:...”的业务原因,避免生成页只显示“请求参数不合法”。
12. `compile_puzzle_draft` 前置陶泥币预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。 12. `compile_puzzle_draft` 前置光点预扣失败不得映射成 `400 BAD_REQUEST`。余额不足返回 `409 CONFLICT`SpacetimeDB procedure 不可用、绑定不匹配、钱包服务异常等统一按 `502 UPSTREAM_ERROR` 暴露,并在 `details.message` 中保留真实钱包错误。
13. 生成拼图作品草稿动作涉及的表单 seed prompt 与首图 prompt 来源选择统一收口在 `server-rs/crates/api-server/src/prompt/puzzle/draft.rs``puzzle.rs` 只负责调用 SpacetimeDB、计费、图片服务和持久化不再直接拼草稿 prompt 文本。 13. 生成拼图作品草稿动作涉及的表单 seed prompt 与首图 prompt 来源选择统一收口在 `server-rs/crates/api-server/src/prompt/puzzle/draft.rs``puzzle.rs` 只负责调用 SpacetimeDB、计费、图片服务和持久化不再直接拼草稿 prompt 文本。
## 结果页 ## 结果页

View File

@@ -21,7 +21,7 @@
5. DashScope 上游失败时api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。 5. DashScope 上游失败时api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
6. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。 6. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。
7. 拼图文生图请求体按 DashScope Wan text2image 协议收口:`input``prompt` 与非空 `negative_prompt``parameters``n``size``prompt_extend``watermark`。不要在 `input``parameters` 里重复写入反向提示词,否则上游容易返回参数非法。 7. 拼图文生图请求体按 DashScope Wan text2image 协议收口:`input``prompt` 与非空 `negative_prompt``parameters``n``size``prompt_extend``watermark`。不要在 `input``parameters` 里重复写入反向提示词,否则上游容易返回参数非法。
8. 陶泥币预扣失败属于钱包或 SpacetimeDB 服务链路错误,不得映射成 `400 BAD_REQUEST`。除余额不足返回 `409 CONFLICT` 外,其余预扣异常统一按上游/服务错误暴露,避免生成页误提示“请求参数不合法”。 8. 光点预扣失败属于钱包或 SpacetimeDB 服务链路错误,不得映射成 `400 BAD_REQUEST`。除余额不足返回 `409 CONFLICT` 外,其余预扣异常统一按上游/服务错误暴露,避免生成页误提示“请求参数不合法”。
### 2. 前端规则裁决 ### 2. 前端规则裁决
@@ -42,6 +42,8 @@
1. 基础单块和合并块都必须使用圆角,不能只让合并后的外轮廓有圆角。 1. 基础单块和合并块都必须使用圆角,不能只让合并后的外轮廓有圆角。
2. 基础单块的图片层必须跟随单块容器裁剪,避免图片直角从圆角边框里露出。 2. 基础单块的图片层必须跟随单块容器裁剪,避免图片直角从圆角边框里露出。
3. 合并块继续按实际拼块外轮廓描边,内部相邻边不额外显示边框。 3. 合并块继续按实际拼块外轮廓描边,内部相邻边不额外显示边框。
4. 合并块轮廓必须按真实占据格生成整体路径,再统一处理外凸角与内凹角圆角;不能只依赖单个格子的 `border-radius` 拼接,否则 L 形、阶梯形合并块的凹入角会露出直角。
5. 合并块图片层必须使用同一条整体轮廓做裁剪,确保凹入角的图片像素也跟随圆角被裁掉。
## 验收 ## 验收
@@ -51,5 +53,5 @@
4. DashScope 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message后端日志能看到 `upstreamStatus``rawExcerpt` 4. DashScope 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message后端日志能看到 `upstreamStatus``rawExcerpt`
5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag` 5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`
6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。 6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。
7. 基础单块和合并块都能看到圆角,且基础单块图片不会溢出圆角裁剪。 7. 基础单块和合并块都能看到圆角,合并块的外凸角与内凹角都不是直角,且图片不会溢出圆角裁剪。
8. 下一关、道具、排行榜仍走现有后端链路,不把外部 I/O 或扣费逻辑塞回前端。 8. 下一关、道具、排行榜仍走现有后端链路,不把外部 I/O 或扣费逻辑塞回前端。

View File

@@ -4,7 +4,7 @@
拼图结果页此前存在两个串联问题: 拼图结果页此前存在两个串联问题:
1. 陶泥主在结果页修改 `关卡名`、新增标签、删除标签,只会改前端本地 `editState`,不会立即写回拼图作品 profile。 1. 百梦主在结果页修改 `关卡名`、新增标签、删除标签,只会改前端本地 `editState`,不会立即写回拼图作品 profile。
2. 发布弹窗同时混用了旧 session 内的 `publishReady` 与前端本地编辑态,导致标签已经在界面里补够,但发布校验仍然盯着旧草稿里的标签数量,用户无法通过发布检验。 2. 发布弹窗同时混用了旧 session 内的 `publishReady` 与前端本地编辑态,导致标签已经在界面里补够,但发布校验仍然盯着旧草稿里的标签数量,用户无法通过发布检验。
这会直接破坏拼图创作主链的可用性:用户明明已经在结果页补齐正式标签,却因为没有自动保存、也没有按当前编辑态重算门槛而卡在发布前。 这会直接破坏拼图创作主链的可用性:用户明明已经在结果页补齐正式标签,却因为没有自动保存、也没有按当前编辑态重算门槛而卡在发布前。

View File

@@ -6,7 +6,7 @@
1. 不同难度有明确倒计时,超时即失败。 1. 不同难度有明确倒计时,超时即失败。
2. 底部固定 3 个轻量道具:提示、查看原图、冻结时间。 2. 底部固定 3 个轻量道具:提示、查看原图、冻结时间。
3. 道具使用必须经过确认弹窗并消耗 `1` 陶泥币,确认弹窗期间暂停关卡计时。 3. 道具使用必须经过确认弹窗并消耗 `1` 光点,确认弹窗期间暂停关卡计时。
本设计只处理拼图运行时,不改拼图创作链、发布链和广场推荐链。 本设计只处理拼图运行时,不改拼图创作链、发布链和广场推荐链。
@@ -91,10 +91,11 @@ remainingMs = max(0, timeLimitMs - effectiveElapsedMs)
1. 播放冻结视觉特效。 1. 播放冻结视觉特效。
2. 显示冻结剩余时长。 2. 显示冻结剩余时长。
3. 第一版冻结 `10000ms` 3. 第一版冻结 `10000ms`
4. 若玩家打开冻结确认弹窗前视觉上仍是 `playing`,但确认扣费期间正式 run 已被服务端计时结算为 `failed`,服务端不得返回“操作不合法”。本次调用视为一次边界同步,已预扣费用必须退款,只返回失败态快照并刷新存档;前端关闭道具确认窗,展示失败面板,不播放冻结特效。
## 计费规则 ## 计费规则
每次确认使用道具消耗 `1` 陶泥币 每次确认使用道具消耗 `1` 光点
正式后端运行态复用现有资产操作钱包预扣链路,新增道具 `asset_kind` 正式后端运行态复用现有资产操作钱包预扣链路,新增道具 `asset_kind`
@@ -106,6 +107,8 @@ remainingMs = max(0, timeLimitMs - effectiveElapsedMs)
若扣费或道具过程失败,确认弹窗保持打开并继续暂停倒计时,在弹窗内展示失败原因;只有成功确认后才关闭弹窗并播放对应反馈。 若扣费或道具过程失败,确认弹窗保持打开并继续暂停倒计时,在弹窗内展示失败原因;只有成功确认后才关闭弹窗并播放对应反馈。
补充规则:冻结时间的边界同步不属于道具使用成功。服务端若发现 run 已超时失败,应退回本次预扣、把失败态落库并返回最新快照,避免玩家在确认窗内看到“操作不合法”。
## UI 规则 ## UI 规则
1. 底部只放 3 个道具按钮,不写规则说明文案。 1. 底部只放 3 个道具按钮,不写规则说明文案。

View File

@@ -6,18 +6,18 @@
1. 拼图草稿页“新增关卡”按钮下方显示一行小字:“获得更多积分激励”。 1. 拼图草稿页“新增关卡”按钮下方显示一行小字:“获得更多积分激励”。
2. 创作页的已发布拼图作品卡展示当前作品的积分激励总数、待领取积分数和领取按钮。 2. 创作页的已发布拼图作品卡展示当前作品的积分激励总数、待领取积分数和领取按钮。
3. 用户在他人已发布拼图作品中消耗陶泥币时,作品作者获得消耗陶泥币数量的一半作为积分激励。 3. 用户在他人已发布拼图作品中消耗光点时,作品作者获得消耗光点数量的一半作为积分激励。
4. 作者领取时只能领取整数个陶泥币,待领取值向下取整;未满 1 个陶泥币的半数余额继续保留。 4. 作者领取时只能领取整数个光点,待领取值向下取整;未满 1 个光点的半数余额继续保留。
## 2. 数据模型 ## 2. 数据模型
拼图作品激励归属到 `puzzle_work_profile` 拼图作品激励归属到 `puzzle_work_profile`
1. `point_incentive_total_half_points: u64` 1. `point_incentive_total_half_points: u64`
- 记录该作品累计获得的激励,单位为“半个陶泥币”。 - 记录该作品累计获得的激励,单位为“半个光点”。
- 每消耗 `N`陶泥币,增加 `N` 个 half points当前拼图道具每次消耗 1 个陶泥币,因此每次为作者增加 0.5。 - 每消耗 `N`光点,增加 `N` 个 half points当前拼图道具每次消耗 1 个光点,因此每次为作者增加 0.5。
2. `point_incentive_claimed_points: u64` 2. `point_incentive_claimed_points: u64`
- 记录作者已领取的整数陶泥币数量。 - 记录作者已领取的整数光点数量。
3. 前端展示: 3. 前端展示:
- 激励总数 = `pointIncentiveTotalHalfPoints / 2`,允许展示一位小数。 - 激励总数 = `pointIncentiveTotalHalfPoints / 2`,允许展示一位小数。
- 待领取积分 = `floor(pointIncentiveTotalHalfPoints / 2) - pointIncentiveClaimedPoints` - 待领取积分 = `floor(pointIncentiveTotalHalfPoints / 2) - pointIncentiveClaimedPoints`
@@ -33,7 +33,7 @@
- 只允许作品作者领取。 - 只允许作品作者领取。
- 计算可领取整数 `claimable = total_half_points / 2 - claimed_points` - 计算可领取整数 `claimable = total_half_points / 2 - claimed_points`
- `claimable <= 0` 时拒绝领取。 - `claimable <= 0` 时拒绝领取。
- 同一事务内更新作品 `claimed_points += claimable`,并向作者钱包增加 `claimable` 陶泥币,钱包流水来源使用 `puzzle_author_incentive_claim` - 同一事务内更新作品 `claimed_points += claimable`,并向作者钱包增加 `claimable` 光点,钱包流水来源使用 `puzzle_author_incentive_claim`
## 4. API 与前端 ## 4. API 与前端
@@ -55,6 +55,6 @@
2. 已发布拼图作品卡展示“积分激励总数”和“待领取”两个数值。 2. 已发布拼图作品卡展示“积分激励总数”和“待领取”两个数值。
3. 待领取积分为 0 时领取按钮禁用。 3. 待领取积分为 0 时领取按钮禁用。
4. 非作者游玩他人拼图并使用付费道具后,该作品累计 half points 增加。 4. 非作者游玩他人拼图并使用付费道具后,该作品累计 half points 增加。
5. 作者领取后钱包增加向下取整后的整数陶泥币,作品待领取数归零或保留不足 1 的小数余额。 5. 作者领取后钱包增加向下取整后的整数光点,作品待领取数归零或保留不足 1 的小数余额。
6. 领取成功后顶部/我的页钱包余额随个人看板刷新。 6. 领取成功后顶部/我的页钱包余额随个人看板刷新。
7. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。 7. 修改后运行编码检查、SpacetimeDB 绑定生成、Rust 检查和必要前端测试。

View File

@@ -4,6 +4,7 @@
## 文档列表 ## 文档列表
- [PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md](./PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md):冻结当前对外中文命名,产品展示名统一为“百梦”,消费单位为“光点”,公开账号标识为“百梦号”,创作侧称谓为“百梦主”。
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。 - [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。 - [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。
- [AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md](./AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md):记录 Maincloud `xushi-p4wfr` 挂起导致认证快照同步和抓大鹅创作失败的根因、认证同步非阻断修复、`/api/creation` Vite 代理补齐和本地 SpacetimeDB 可跑链路。 - [AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md](./AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md):记录 Maincloud `xushi-p4wfr` 挂起导致认证快照同步和抓大鹅创作失败的根因、认证同步非阻断修复、`/api/creation` Vite 代理补齐和本地 SpacetimeDB 可跑链路。

View File

@@ -2,7 +2,7 @@
## 背景 ## 背景
世界创作结果页已经提供“作品测试”入口,但测试运行时此前缺少与“幕预览”一致的显式退出按钮。陶泥主进入测试后只能依赖浏览器返回、刷新或其他间接链路离开,不符合独立运行时面板的交互语义。 世界创作结果页已经提供“作品测试”入口,但测试运行时此前缺少与“幕预览”一致的显式退出按钮。百梦主进入测试后只能依赖浏览器返回、刷新或其他间接链路离开,不符合独立运行时面板的交互语义。
## 本次约束 ## 本次约束

View File

@@ -2,7 +2,7 @@
## 背景 ## 背景
幕预览和测试作品用于陶泥主检查玩法表现,不能被当作玩家正式游玩记录。若这类运行时复用正式 RPG 壳、story action 或 snapshot 接口,必须在进入个人存档页、游玩统计、作品游玩历史前被过滤。 幕预览和测试作品用于百梦主检查玩法表现,不能被当作玩家正式游玩记录。若这类运行时复用正式 RPG 壳、story action 或 snapshot 接口,必须在进入个人存档页、游玩统计、作品游玩历史前被过滤。
## 落地约束 ## 落地约束

View File

@@ -1,6 +1,6 @@
# Rust API Server 路由索引2026-04-23 # Rust API Server 路由索引2026-04-23
更新时间:`2026-04-30` 更新时间:`2026-05-01`
## 1. 文档目标 ## 1. 文档目标
@@ -10,7 +10,7 @@
## 2. 当前统计 ## 2. 当前统计
当前 Rust `api-server``app.rs` 可抽取到 `102` 条路由: 当前 Rust `api-server``app.rs` 可抽取到 `116` 条路由:
1. 管理后台 API`6` 条。 1. 管理后台 API`6` 条。
2. 内部鉴权调试接口:`2` 条。 2. 内部鉴权调试接口:`2` 条。
@@ -18,11 +18,12 @@
4. assets / OSS 接口:`15` 条。 4. assets / OSS 接口:`15` 条。
5. auth 接口:`12` 条。 5. auth 接口:`12` 条。
6. custom world / agent 接口:`23` 条。 6. custom world / agent 接口:`23` 条。
7. llm proxy 接口:`1` 条。 7. match3d creation / runtime 接口:`14` 条。
8. profile / runtime profile 接口:`12` 条。 8. llm proxy 接口:`1` 条。
9. runtime story / story gameplay 接口:`15` 条。 9. profile / runtime profile 接口:`12` 条。
10. legacy generated 静态路径兼容`6` 条。 10. runtime story / story gameplay 接口`15` 条。
11. health check`1` 条。 11. legacy generated 静态路径兼容`6` 条。
12. health check`1` 条。
## 3. 路由清单 ## 3. 路由清单
@@ -117,7 +118,24 @@
1. `POST /api/llm/chat/completions` 1. `POST /api/llm/chat/completions`
### 3.8 Profile / Runtime Profile ### 3.8 Match3D Creation / Runtime
1. `POST /api/creation/match3d/sessions`
2. `GET /api/creation/match3d/sessions/{session_id}`
3. `POST /api/creation/match3d/sessions/{session_id}/messages`
4. `POST /api/creation/match3d/sessions/{session_id}/messages/stream`
5. `POST /api/creation/match3d/sessions/{session_id}/actions`
6. `POST /api/creation/match3d/sessions/{session_id}/compile`
7. `GET /api/creation/match3d/works`
8. `GET / PATCH / PUT / DELETE /api/creation/match3d/works/{profile_id}`
9. `POST /api/creation/match3d/works/{profile_id}/publish`
10. `GET /api/runtime/match3d/gallery`
11. `POST /api/runtime/match3d/works/{profile_id}/runs`
12. `GET /api/runtime/match3d/runs/{run_id}`
13. `POST /api/runtime/match3d/runs/{run_id}/click`
14. `POST /api/runtime/match3d/runs/{run_id}/stop / restart / time-up`
### 3.9 Profile / Runtime Profile
1. `GET /api/profile/dashboard` 1. `GET /api/profile/dashboard`
2. `GET /api/runtime/profile/dashboard` 2. `GET /api/runtime/profile/dashboard`
@@ -132,7 +150,7 @@
11. `POST /api/profile/save-archives/{world_key}` 11. `POST /api/profile/save-archives/{world_key}`
12. `POST /api/runtime/profile/save-archives/{world_key}` 12. `POST /api/runtime/profile/save-archives/{world_key}`
### 3.9 Runtime Story / Gameplay ### 3.10 Runtime Story / Gameplay
1. `POST /api/runtime/save/snapshot` 1. `POST /api/runtime/save/snapshot`
2. `GET /api/runtime/settings` 2. `GET /api/runtime/settings`
@@ -150,7 +168,7 @@
14. `POST /api/story/npc/battle` 14. `POST /api/story/npc/battle`
15. `GET /api/runtime/sessions/{runtime_session_id}/inventory` 15. `GET /api/runtime/sessions/{runtime_session_id}/inventory`
### 3.10 Legacy Generated 路径 ### 3.11 Legacy Generated 路径
1. `GET /generated-character-drafts/{*path}` 1. `GET /generated-character-drafts/{*path}`
2. `GET /generated-characters/{*path}` 2. `GET /generated-characters/{*path}`
@@ -159,7 +177,7 @@
5. `GET /generated-custom-world-covers/{*path}` 5. `GET /generated-custom-world-covers/{*path}`
6. `GET /generated-qwen-sprites/{*path}` 6. `GET /generated-qwen-sprites/{*path}`
### 3.11 Health ### 3.12 Health
1. `GET /healthz` 1. `GET /healthz`

View File

@@ -45,7 +45,8 @@ Vite 代理覆盖范围:
1. `/api/runtime/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖旧 runtime story 兼容接口。 1. `/api/runtime/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖旧 runtime story 兼容接口。
2. `/api/story/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖新 story session、battle 查询与 NPC battle 切片接口。 2. `/api/story/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖新 story session、battle 查询与 NPC battle 切片接口。
3. 其他 `/api/auth``/api/assets``/api/custom-world``/api/llm` 等路径仍由同一个 `GENARRATIVE_RUNTIME_SERVER_TARGET` 控制,便于 M7 按服务能力逐项做对比 smoke 3. `/api/creation/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖 Match3D 等创作域接口,避免开发态请求落回 Vite SPA HTML
4. 其他 `/api/auth``/api/assets``/api/custom-world``/api/llm` 等路径仍由同一个 `GENARRATIVE_RUNTIME_SERVER_TARGET` 控制,便于 M7 按服务能力逐项做对比 smoke。
安全边界: 安全边界:
@@ -137,11 +138,11 @@ npm run deploy:rust:remote
1. 在仓库根目录创建 `build/` 1. 在仓库根目录创建 `build/`
2.`build/` 下创建当前时间命名的目标目录,例如 `build/20260422-153000/` 2.`build/` 下创建当前时间命名的目标目录,例如 `build/20260422-153000/`
3. 使用 Vite 构建前端 release 到目标目录的 `web/` 3. 使用 Vite 构建前端 release 到目标目录的 `web/`,并构建后台管理前端 release 到 `web/admin/`;后台构建固定使用 `/admin/` 作为 Vite base
4. 执行 `cargo build -p api-server --release --target x86_64-unknown-linux-gnu --manifest-path server-rs/Cargo.toml`,并把 `api-server` 复制到目标目录。 4. 执行 `cargo build -p api-server --release --target x86_64-unknown-linux-gnu --manifest-path server-rs/Cargo.toml`,并把 `api-server` 复制到目标目录。
5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。 5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。
6. 把仓库根目录的 `.env``.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库。 6. 把仓库根目录的 `.env``.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库。
7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*``/generated-*``/healthz` 反代到本包内的 `api-server` 7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` `web/admin/`;其中 `/admin` 跳转到 `/admin/``/admin/` 提供后台 SPA`/admin/api/*``/api/*``/generated-*``/healthz` 反代到本包内的 `api-server`
8. 在目标目录写入 `start.sh``stop.sh``start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env``.env.local`,兼容 UTF-8 BOM 与 CRLF再回退到构建时通过 `--database``--api-port``--web-host``--web-port``--spacetime-host``--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`;并默认导出 `NO_COLOR=1``CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。 8. 在目标目录写入 `start.sh``stop.sh``start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env``.env.local`,兼容 UTF-8 BOM 与 CRLF再回退到构建时通过 `--database``--api-port``--web-host``--web-port``--spacetime-host``--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`;并默认导出 `NO_COLOR=1``CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli``$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr <host>:<port>`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。
9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。 9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/<timestamp> ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。
@@ -157,7 +158,9 @@ build/<timestamp>/
├─ .env.local ├─ .env.local
├─ web/ ├─ web/
│ ├─ .env │ ├─ .env
─ .env.local ─ .env.local
│ └─ admin/
│ └─ index.html
├─ api-server ├─ api-server
├─ spacetime_module.wasm ├─ spacetime_module.wasm
├─ migration-bootstrap-secret.txt ├─ migration-bootstrap-secret.txt
@@ -178,6 +181,8 @@ npm run build:rust:ubuntu -- --database genarrative-dev --web-host 127.0.0.1 --w
npm run build:rust:ubuntu -- --skip-upload npm run build:rust:ubuntu -- --skip-upload
``` ```
`--skip-web-build` 会同时跳过主前端和后台管理前端构建,仅用于调试已有发布包内容;正式发布不应使用该参数。
目标服务器启动: 目标服务器启动:
```bash ```bash
@@ -186,7 +191,7 @@ cd build/<timestamp>
./stop.sh ./stop.sh
``` ```
如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/``api-server``spacetime_module.wasm``migration-bootstrap-secret.txt``scripts/``.env*``start.sh``stop.sh``web-server.mjs``README.md` 等发布产物;文件产物使用普通复制,`web/``scripts/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/``logs/``run/``deploy-state/``database-migrations/` 这类运行态目录。 如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/``api-server``spacetime_module.wasm``migration-bootstrap-secret.txt``scripts/``.env*``start.sh``stop.sh``web-server.mjs``README.md` 等发布产物;后台管理前端位于 `web/admin/`,随 `web/` 一并覆盖。文件产物使用普通复制,`web/``scripts/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/``logs/``run/``deploy-state/``database-migrations/` 这类运行态目录。Jenkins 覆盖 `.env.local` 时会保留目标部署目录已有的 `GENARRATIVE_SPACETIME_TOKEN` / `GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN`,避免后台表统计在部署后失去读取 private 表所需的 owner 身份。
安全边界: 安全边界:
@@ -207,7 +212,7 @@ cd build/<timestamp>
1. Ubuntu x86_64。 1. Ubuntu x86_64。
2. 已安装 `node`,用于运行发布包内的 `web-server.mjs` 2. 已安装 `node`,用于运行发布包内的 `web-server.mjs`
3. 已安装 `spacetime` CLI`start.sh` 会启动本地 SpacetimeDB 并发布 wasm。 3. 已安装 `spacetime` CLI`start.sh` 会启动本地 SpacetimeDB 并发布 wasm。
4. 业务密钥通过目标服务器环境变量或发布包同目录 `.env.local` 提供。 4. 业务密钥通过目标服务器环境变量或发布包同目录 `.env.local` 提供;后台概览如果需要统计 private 表,`GENARRATIVE_SPACETIME_TOKEN` 必须是目标库 owner 或具备等效读取权限的 token
## 4. 与 M7 的关系 ## 4. 与 M7 的关系

View File

@@ -10,7 +10,7 @@
1. 草稿层可以承载 `scene chapter / scene act` 1. 草稿层可以承载 `scene chapter / scene act`
2. 后端可以把 `scene_chapter` 编译成正式蓝图 2. 后端可以把 `scene_chapter` 编译成正式蓝图
3. 陶泥主可以在现有场景编辑弹层里看到并编辑多幕配置 3. 百梦主可以在现有场景编辑弹层里看到并编辑多幕配置
4. 编辑后的幕信息可以正确写回 `sceneChapterBlueprints` 4. 编辑后的幕信息可以正确写回 `sceneChapterBlueprints`
5. 运行时共享层先具备读取幕背景、主角色、相遇 NPC 池的基础能力 5. 运行时共享层先具备读取幕背景、主角色、相遇 NPC 池的基础能力
6. 当前幕主角色的负好感 `5` 轮聊天限制先形成首个可运行闭环 6. 当前幕主角色的负好感 `5` 轮聊天限制先形成首个可运行闭环
@@ -60,7 +60,7 @@
前端已完成第一批接入: 前端已完成第一批接入:
1. `scene_chapter` 不再作为独立 Tab / 独立卡片暴露给陶泥 1. `scene_chapter` 不再作为独立 Tab / 独立卡片暴露给百梦
2. 多幕配置已内嵌到 `CustomWorldEntityEditorModal.tsx``LandmarkEditor` 2. 多幕配置已内嵌到 `CustomWorldEntityEditorModal.tsx``LandmarkEditor`
3. 单幕编辑已从文本表单切成“背景大图预览 + 3 个角色槽位”的轻量交互 3. 单幕编辑已从文本表单切成“背景大图预览 + 3 个角色槽位”的轻量交互
4. “幕标题 / 幕摘要 / 幕目标 / 过渡钩子”已从场景手工编辑区移除,继续留在草稿生成与编译层 4. “幕标题 / 幕摘要 / 幕目标 / 过渡钩子”已从场景手工编辑区移除,继续留在草稿生成与编译层
@@ -88,7 +88,7 @@
7. 幕预览运行时已补 custom world NPC 的视觉兜底链路,优先使用 `visual / imageSrc` 渲染,避免角色形象或动画空白 7. 幕预览运行时已补 custom world NPC 的视觉兜底链路,优先使用 `visual / imageSrc` 渲染,避免角色形象或动画空白
8. 当前幕小预览已调整为左侧玩家、右侧敌对/相遇角色的构图NPC 站位采用一前两后 8. 当前幕小预览已调整为左侧玩家、右侧敌对/相遇角色的构图NPC 站位采用一前两后
前排主角色与玩家角色保持同一 y 轴后排两个角色改为同一列、x 轴对齐并上下分布,且后排整体 y 轴中点与前排主角色一致 前排主角色与玩家角色保持同一 y 轴后排两个角色改为同一列、x 轴对齐并上下分布,且后排整体 y 轴中点与前排主角色一致
9. 新增幕默认只带 1 个主角色,后续槽位由陶泥主按需补充 9. 新增幕默认只带 1 个主角色,后续槽位由百梦主按需补充
10. 小预览里的名字已移动到角色头顶,角色渲染不再带方形底板,避免遮挡场景背景 10. 小预览里的名字已移动到角色头顶,角色渲染不再带方形底板,避免遮挡场景背景
11. 幕预览复用真实游戏壳时隐藏左上角角色等级徽标,退出入口固定在上方画面区域底部居中,并使用“结束预览”作为操作文案 11. 幕预览复用真实游戏壳时隐藏左上角角色等级徽标,退出入口固定在上方画面区域底部居中,并使用“结束预览”作为操作文案
12. 创作侧场景列表封面、多幕配置卡片、配置背景弹层统一读取同一张场景显示图;在任一幕保存背景时同步回全部幕背景字段和场景兼容图,避免同一场景在不同层级出现不同预览图 12. 创作侧场景列表封面、多幕配置卡片、配置背景弹层统一读取同一张场景显示图;在任一幕保存背景时同步回全部幕背景字段和场景兼容图,避免同一场景在不同层级出现不同预览图

View File

@@ -409,7 +409,7 @@ Access-Control-Allow-Credentials: true
职责: 职责:
- 面向陶泥主、运营、内部编辑器 - 面向百梦主、运营、内部编辑器
- 必须鉴权 - 必须鉴权
- 必须审计 - 必须审计
- 不建议对公网完全开放 - 不建议对公网完全开放
@@ -469,7 +469,7 @@ flowchart TD
当出现这些需求时,再进入下一阶段: 当出现这些需求时,再进入下一阶段:
- 多人同时在线 - 多人同时在线
-陶泥主协作 -百梦主协作
- 图片/视频生成任务变多 - 图片/视频生成任务变多
- 需要账号体系、存档、云同步 - 需要账号体系、存档、云同步
- 需要审计和版本回滚 - 需要审计和版本回滚

View File

@@ -206,7 +206,7 @@
1. 密码登录仍由 `user_account.password_hash` 承担 1. 密码登录仍由 `user_account.password_hash` 承担
2. 本轮不引入 `password` provider identity 2. 本轮不引入 `password` provider identity
3. 密码登录只接受已绑定手机号的账号,不支持邮箱、用户名或陶泥号作为登录身份 3. 密码登录只接受已绑定手机号的账号,不支持邮箱、用户名或百梦号作为登录身份
4. 密码登录不创建账号,新账号只由手机号验证码登录创建 4. 密码登录不创建账号,新账号只由手机号验证码登录创建
### 9.2 `POST /api/auth/phone/login` ### 9.2 `POST /api/auth/phone/login`

View File

@@ -63,7 +63,7 @@ SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default';
### `user_account` ### `user_account`
- 作用:用户账号主表,保存用户名、公开陶泥号、手机号掩码、登录方式、密码登录开关和 token 版本。 - 作用:用户账号主表,保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关和 token 版本。
- 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: Option<String>`, `password_login_enabled: bool`, `token_version: u64` - 结构:`user_id PK: String`, `public_user_code: String`, `username: String`, `display_name: String`, `avatar_url: Option<String>`, `phone_number_masked: Option<String>`, `phone_number_e164: Option<String>`, `login_method: String`, `binding_status: String`, `wechat_bound: bool`, `password_hash: Option<String>`, `password_login_enabled: bool`, `token_version: u64`
- 索引:`username`, `public_user_code` - 索引:`username`, `public_user_code`
@@ -134,6 +134,7 @@ SELECT * FROM user_browse_history WHERE user_id = '<user_id>' AND owner_user_id
- 作用:个人主页聚合状态,保存钱包余额和总游玩时长。 - 作用:个人主页聚合状态,保存钱包余额和总游玩时长。
- 结构:`user_id PK: String`, `wallet_balance: u64`, `total_play_time_ms: u64`, `created_at: Timestamp`, `updated_at: Timestamp` - 结构:`user_id PK: String`, `wallet_balance: u64`, `total_play_time_ms: u64`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:主键 `user_id` - 索引:主键 `user_id`
- 新用户注册成功后默认写入 `10` 个光点,余额仍以后端钱包 projection 为准。
```sql ```sql
SELECT * FROM profile_dashboard_state WHERE user_id = '<user_id>'; SELECT * FROM profile_dashboard_state WHERE user_id = '<user_id>';
@@ -144,6 +145,7 @@ SELECT * FROM profile_dashboard_state WHERE user_id = '<user_id>';
- 作用:钱包流水账,记录金币/货币变更来源与变更后的余额。 - 作用:钱包流水账,记录金币/货币变更来源与变更后的余额。
- 结构:`wallet_ledger_id PK: String`, `user_id: String`, `amount_delta: i64`, `balance_after: u64`, `source_type: RuntimeProfileWalletLedgerSourceType`, `created_at: Timestamp` - 结构:`wallet_ledger_id PK: String`, `user_id: String`, `amount_delta: i64`, `balance_after: u64`, `source_type: RuntimeProfileWalletLedgerSourceType`, `created_at: Timestamp`
- 索引:`user_id`, `(user_id, created_at)` - 索引:`user_id`, `(user_id, created_at)`
- 注册赠送流水来源为 `new_user_registration_reward`,流水 ID 固定为 `new-user-registration:{user_id}`,用于保证重复调用不重复发放。
```sql ```sql
SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>'; SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>';
@@ -152,7 +154,7 @@ SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>' ORDER BY created
### `profile_redeem_code` ### `profile_redeem_code`
- 作用:运营发放的陶泥币兑换码,支持公共码、唯一码和私有码。 - 作用:运营发放的光点兑换码,支持公共码、唯一码和私有码。
- 结构:`code PK: String`, `mode: RuntimeProfileRedeemCodeMode`, `reward_points: u64`, `max_uses: u32`, `global_used_count: u32`, `enabled: bool`, `allowed_user_ids: Vec<String>`, `created_by: String`, `created_at: Timestamp`, `updated_at: Timestamp` - 结构:`code PK: String`, `mode: RuntimeProfileRedeemCodeMode`, `reward_points: u64`, `max_uses: u32`, `global_used_count: u32`, `enabled: bool`, `allowed_user_ids: Vec<String>`, `created_by: String`, `created_at: Timestamp`, `updated_at: Timestamp`
- 索引:主键 `code` - 索引:主键 `code`

View File

@@ -39,7 +39,7 @@
- 发布 - 发布
3. 完整复制外部 TXT 模式的运行机制: 3. 完整复制外部 TXT 模式的运行机制:
- 玩家游玩会话 - 玩家游玩会话
- 陶泥主测试/读档会话 - 百梦主测试/读档会话
- 流式动作执行 - 流式动作执行
- 文本模式显示 - 文本模式显示
- 历史记录 - 历史记录
@@ -99,7 +99,7 @@
- 属性面板 - 属性面板
5. 双会话机制: 5. 双会话机制:
- 玩家游玩会话 - 玩家游玩会话
- 陶泥主测试/读档会话 - 百梦主测试/读档会话
6. 流式动作接口与事件协议: 6. 流式动作接口与事件协议:
- `start` - `start`
- `raw_text` - `raw_text`
@@ -551,7 +551,7 @@ TXT 模式后续必须完整落地双会话机制:
1. 玩家游玩会话 1. 玩家游玩会话
- 对应外部 `POST /api/optical/games/session/create` - 对应外部 `POST /api/optical/games/session/create`
- 用于正式游玩 - 用于正式游玩
2. 陶泥主测试/读档会话 2. 百梦主测试/读档会话
- 对应外部 `POST /api/visual/session/create` - 对应外部 `POST /api/visual/session/create`
- 用于测试体验与加载指定存档 - 用于测试体验与加载指定存档

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>陶泥</title> <title>百梦</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,5 +1,5 @@
{ {
"name": "陶泥", "name": "百梦",
"description": "一个 AI 原生的武侠/仙侠 RPG 游戏,具有动态剧情生成和横版卷轴视觉效果。", "description": "一个 AI 原生的武侠/仙侠 RPG 游戏,具有动态剧情生成和横版卷轴视觉效果。",
"requestFramePermissions": [] "requestFramePermissions": []
} }

View File

@@ -9,8 +9,8 @@
"dev:rust:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh", "dev:rust:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh",
"dev:web": "node scripts/dev-web-rust.mjs", "dev:web": "node scripts/dev-web-rust.mjs",
"admin-web:dev": "npm --prefix apps/admin-web run dev --", "admin-web:dev": "npm --prefix apps/admin-web run dev --",
"admin-web:build": "npm --prefix apps/admin-web run build --", "admin-web:build": "node scripts/admin-web-build.mjs build",
"admin-web:typecheck": "npm --prefix apps/admin-web run typecheck --", "admin-web:typecheck": "node scripts/admin-web-build.mjs typecheck",
"admin-web:preview": "npm --prefix apps/admin-web run preview --", "admin-web:preview": "npm --prefix apps/admin-web run preview --",
"spacetime:publish:maincloud": "node scripts/run-bash-script.mjs scripts/spacetime-publish-maincloud.sh", "spacetime:publish:maincloud": "node scripts/run-bash-script.mjs scripts/spacetime-publish-maincloud.sh",
"spacetime:generate": "node scripts/generate-spacetime-bindings.mjs", "spacetime:generate": "node scripts/generate-spacetime-bindings.mjs",

View File

@@ -11,6 +11,7 @@ export type AuthUser = {
loginMethod: AuthLoginMethod; loginMethod: AuthLoginMethod;
bindingStatus: AuthBindingStatus; bindingStatus: AuthBindingStatus;
wechatBound: boolean; wechatBound: boolean;
createdAt: string;
}; };
export type PublicUserSummary = { export type PublicUserSummary = {

View File

@@ -59,6 +59,7 @@ export type ProfileWalletLedgerEntry = {
balanceAfter: number; balanceAfter: number;
sourceType: sourceType:
| 'snapshot_sync' | 'snapshot_sync'
| 'new_user_registration_reward'
| 'invite_inviter_reward' | 'invite_inviter_reward'
| 'invite_invitee_reward' | 'invite_invitee_reward'
| 'points_recharge' | 'points_recharge'
@@ -149,12 +150,20 @@ export type ProfileReferralInviteCenterResponse = {
todayInviterRewardCount: number; todayInviterRewardCount: number;
todayInviterRewardRemaining: number; todayInviterRewardRemaining: number;
rewardPoints: number; rewardPoints: number;
invitedUsers: ProfileReferralInvitedUser[];
hasRedeemedCode: boolean; hasRedeemedCode: boolean;
boundInviterUserId: string | null; boundInviterUserId: string | null;
boundAt: string | null; boundAt: string | null;
updatedAt: string; updatedAt: string;
}; };
export type ProfileReferralInvitedUser = {
userId: string;
displayName: string;
avatarUrl: string | null;
boundAt: string;
};
export type RedeemProfileReferralInviteCodeRequest = { export type RedeemProfileReferralInviteCodeRequest = {
inviteCode: string; inviteCode: string;
}; };

View File

@@ -0,0 +1,73 @@
import {spawnSync} from 'node:child_process';
import {dirname, resolve} from 'node:path';
import {fileURLToPath} from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..');
const adminWebDir = resolve(repoRoot, 'apps/admin-web');
const adminTsconfigPath = resolve(adminWebDir, 'tsconfig.json');
const adminViteConfigPath = resolve(adminWebDir, 'vite.config.ts');
const tscBinPath = resolve(repoRoot, 'node_modules/typescript/bin/tsc');
const viteCliPath = resolve(scriptDir, 'vite-cli.mjs');
const command = process.argv[2] ?? 'build';
const extraArgs = process.argv.slice(3);
function usage() {
console.error('用法: node scripts/admin-web-build.mjs <typecheck|build> [vite-build-args...]');
}
function runNodeScript(label, args) {
console.log(`[admin-web] ${label}`);
const result = spawnSync(process.execPath, args, {
cwd: repoRoot,
encoding: 'utf8',
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.error) {
console.error(`[admin-web] ${label} failed to start: ${result.error.message}`);
process.exit(1);
}
if (result.signal) {
console.error(`[admin-web] ${label} was terminated by signal ${result.signal}`);
process.exit(1);
}
if ((result.status ?? 0) !== 0) {
process.exit(result.status ?? 1);
}
}
function runTypecheck() {
runNodeScript('typecheck', [
tscBinPath,
'--noEmit',
'-p',
adminTsconfigPath,
]);
}
if (command === 'typecheck') {
runTypecheck();
} else if (command === 'build') {
runTypecheck();
runNodeScript('vite build', [
viteCliPath,
'build',
'--config',
adminViteConfigPath,
...extraArgs,
]);
} else {
usage();
process.exit(1);
}

View File

@@ -2,39 +2,60 @@ import {spawnSync} from 'node:child_process';
import {fileURLToPath} from 'node:url'; import {fileURLToPath} from 'node:url';
const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url)); const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url));
const args = [viteCliPath, 'build', ...process.argv.slice(2)]; const adminWebBuildPath = fileURLToPath(new URL('./admin-web-build.mjs', import.meta.url));
const forwardedArgs = process.argv.slice(2);
const result = spawnSync(process.execPath, args, { const results = [
cwd: process.cwd(), runBuildStep('web', [viteCliPath, 'build', ...forwardedArgs]),
encoding: 'utf8', runBuildStep('admin-web', [adminWebBuildPath, 'build']),
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if ((result.status ?? 0) !== 0) {
process.exit(result.status ?? 1);
}
const warningPattern = /\bwarn(?:ing)?\b/i;
const ignoredWarningPatterns = [
/ExperimentalWarning/u,
]; ];
const warningLines = `${result.stdout ?? ''}\n${result.stderr ?? ''}` const failedResult = results.find(result => result.error || result.signal || (result.status ?? 0) !== 0);
.split(/\r?\n/u) if (failedResult) {
.map(line => line.trim()) if (failedResult.error) {
.filter(line => line.length > 0) console.error(`Build gate failed to start a build step: ${failedResult.error.message}`);
.filter(line => warningPattern.test(line)) } else if (failedResult.signal) {
.filter(line => !ignoredWarningPatterns.some(pattern => pattern.test(line))); console.error(`Build gate step was terminated by signal ${failedResult.signal}`);
}
process.exit(failedResult.status ?? 1);
}
const warningLines = results.flatMap((result) => collectWarningLines(result));
if (warningLines.length > 0) { if (warningLines.length > 0) {
console.error('Build gate failed because warnings were emitted:'); console.error('Build gate failed because warnings were emitted:');
[...new Set(warningLines)].forEach(line => console.error(`- ${line}`)); [...new Set(warningLines)].forEach(line => console.error(`- ${line}`));
process.exit(1); process.exit(1);
} }
function runBuildStep(label, args) {
console.log(`[build-gate] ${label}`);
const result = spawnSync(process.execPath, args, {
cwd: process.cwd(),
encoding: 'utf8',
});
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
return result;
}
function collectWarningLines(result) {
const warningPattern = /\bwarn(?:ing)?\b/i;
const ignoredWarningPatterns = [
/ExperimentalWarning/u,
];
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`
.split(/\r?\n/u)
.map(line => line.trim())
.filter(line => line.length > 0)
.filter(line => warningPattern.test(line))
.filter(line => !ignoredWarningPatterns.some(pattern => pattern.test(line)));
}

View File

@@ -310,6 +310,7 @@ fi
TARGET_DIR="${BUILD_ROOT}/${BUILD_NAME}" TARGET_DIR="${BUILD_ROOT}/${BUILD_NAME}"
WEB_DIR="${TARGET_DIR}/web" WEB_DIR="${TARGET_DIR}/web"
ADMIN_WEB_DIR="${WEB_DIR}/admin"
API_BINARY_SOURCE="${SERVER_RS_DIR}/target/x86_64-unknown-linux-gnu/release/api-server" API_BINARY_SOURCE="${SERVER_RS_DIR}/target/x86_64-unknown-linux-gnu/release/api-server"
WASM_SOURCE="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm" WASM_SOURCE="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm"
@@ -364,6 +365,12 @@ if [[ "${SKIP_WEB_BUILD}" -ne 1 ]]; then
cd "${REPO_ROOT}" cd "${REPO_ROOT}"
node scripts/vite-cli.mjs build --outDir "${WEB_DIR}" --emptyOutDir node scripts/vite-cli.mjs build --outDir "${WEB_DIR}" --emptyOutDir
) )
echo "[deploy:rust] 构建后台 Vite release -> ${ADMIN_WEB_DIR}"
(
cd "${REPO_ROOT}"
MSYS2_ARG_CONV_EXCL="--base=" node scripts/admin-web-build.mjs build --base=/admin/ --outDir "${ADMIN_WEB_DIR}" --emptyOutDir
)
fi fi
if [[ "${SKIP_API_BUILD}" -ne 1 ]]; then if [[ "${SKIP_API_BUILD}" -ne 1 ]]; then
@@ -421,11 +428,14 @@ import {fileURLToPath} from 'node:url';
const releaseDir = path.dirname(fileURLToPath(import.meta.url)); const releaseDir = path.dirname(fileURLToPath(import.meta.url));
const webRoot = path.join(releaseDir, 'web'); const webRoot = path.join(releaseDir, 'web');
const adminWebRoot = path.join(webRoot, 'admin');
const webHost = process.env.GENARRATIVE_WEB_HOST || '127.0.0.1'; const webHost = process.env.GENARRATIVE_WEB_HOST || '127.0.0.1';
const webPort = Number(process.env.GENARRATIVE_WEB_PORT || '3000'); const webPort = Number(process.env.GENARRATIVE_WEB_PORT || '3000');
const apiTarget = new URL(process.env.GENARRATIVE_API_TARGET || 'http://127.0.0.1:8082'); const apiTarget = new URL(process.env.GENARRATIVE_API_TARGET || 'http://127.0.0.1:8082');
const indexPath = path.join(webRoot, 'index.html'); const indexPath = path.join(webRoot, 'index.html');
const adminIndexPath = path.join(adminWebRoot, 'index.html');
const proxyPrefixes = [ const proxyPrefixes = [
'/admin/api',
'/api/', '/api/',
'/api', '/api',
'/generated-character-drafts', '/generated-character-drafts',
@@ -466,11 +476,11 @@ function sendFile(response, filePath) {
.pipe(response); .pipe(response);
} }
function serveStatic(request, response, pathname) { function serveStaticFromRoot(response, pathname, rootDir, fallbackIndexPath) {
const decodedPath = decodeURIComponent(pathname); const decodedPath = decodeURIComponent(pathname);
const relativePath = decodedPath === '/' ? '/index.html' : decodedPath; const relativePath = decodedPath === '/' ? '/index.html' : decodedPath;
const filePath = path.normalize(path.join(webRoot, relativePath)); const filePath = path.normalize(path.join(rootDir, relativePath));
const safeRelativePath = path.relative(webRoot, filePath); const safeRelativePath = path.relative(rootDir, filePath);
if (safeRelativePath.startsWith('..') || path.isAbsolute(safeRelativePath)) { if (safeRelativePath.startsWith('..') || path.isAbsolute(safeRelativePath)) {
response.writeHead(403, {'content-type': 'text/plain; charset=utf-8'}); response.writeHead(403, {'content-type': 'text/plain; charset=utf-8'});
@@ -480,12 +490,21 @@ function serveStatic(request, response, pathname) {
const resolvedFilePath = fs.existsSync(filePath) && fs.statSync(filePath).isFile() const resolvedFilePath = fs.existsSync(filePath) && fs.statSync(filePath).isFile()
? filePath ? filePath
: indexPath; : fallbackIndexPath;
response.writeHead(200, {'content-type': contentTypeFor(resolvedFilePath)}); response.writeHead(200, {'content-type': contentTypeFor(resolvedFilePath)});
sendFile(response, resolvedFilePath); sendFile(response, resolvedFilePath);
} }
function serveStatic(request, response, pathname) {
serveStaticFromRoot(response, pathname, webRoot, indexPath);
}
function serveAdminStatic(response, pathname) {
const adminPath = pathname === '/admin/' ? '/' : pathname.replace(/^\/admin/u, '');
serveStaticFromRoot(response, adminPath, adminWebRoot, adminIndexPath);
}
function proxyToApi(request, response) { function proxyToApi(request, response) {
const targetUrl = new URL(request.url || '/', apiTarget); const targetUrl = new URL(request.url || '/', apiTarget);
const proxyRequest = http.request( const proxyRequest = http.request(
@@ -522,6 +541,17 @@ const server = http.createServer((request, response) => {
return; return;
} }
if (url.pathname === '/admin') {
response.writeHead(301, {location: '/admin/'});
response.end();
return;
}
if (url.pathname === '/admin/' || url.pathname.startsWith('/admin/')) {
serveAdminStatic(response, url.pathname);
return;
}
serveStatic(request, response, url.pathname); serveStatic(request, response, url.pathname);
}); });
@@ -1189,7 +1219,7 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
## 内容 ## 内容
- \`.env\` / \`.env.local\`:从仓库根目录复制的环境文件,同时各保留一份到 \`web/\` - \`.env\` / \`.env.local\`:从仓库根目录复制的环境文件,同时各保留一份到 \`web/\`
- \`web/\`Vite release 静态资源 - \`web/\`主前端 Vite release 静态资源\`web/admin/\` 为后台管理前端静态资源
- \`api-server\`x86_64-unknown-linux-gnu release 可执行文件 - \`api-server\`x86_64-unknown-linux-gnu release 可执行文件
- \`spacetime_module.wasm\`wasm32-unknown-unknown release 模块 - \`spacetime_module.wasm\`wasm32-unknown-unknown release 模块
- \`migration-bootstrap-secret.txt\`:本发布包 wasm 编译时注入的迁移引导密钥;服务器 \`start.sh\` 发布时会显示,迁移授权完成后可删除 - \`migration-bootstrap-secret.txt\`:本发布包 wasm 编译时注入的迁移引导密钥;服务器 \`start.sh\` 发布时会显示,迁移授权完成后可删除
@@ -1211,6 +1241,11 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
默认启动会先尝试无清库发布;如果 SpacetimeDB 返回 schema 冲突,\`start.sh\` 会把旧库导出到 \`database-migrations/<database>/\`,随后清库发布新 wasm并用 \`--replace-existing\` 导入回灌。 默认启动会先尝试无清库发布;如果 SpacetimeDB 返回 schema 冲突,\`start.sh\` 会把旧库导出到 \`database-migrations/<database>/\`,随后清库发布新 wasm并用 \`--replace-existing\` 导入回灌。
## 入口
- 主站:\`http://<web-host>:<web-port>/\`
- 后台:\`http://<web-host>:<web-port>/admin/\`
## 环境变量 ## 环境变量
- 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。 - 启动时会先加载发布目录根部的 \`.env\` 与 \`.env.local\`,再回退到脚本内默认值。
@@ -1220,12 +1255,12 @@ cat >"${TARGET_DIR}/README.md" <<'EOF'
- \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\` - \`GENARRATIVE_WEB_HOST\` / \`GENARRATIVE_WEB_PORT\`
- \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\` - \`GENARRATIVE_API_HOST\` / \`GENARRATIVE_API_PORT\` / \`GENARRATIVE_API_LOG\`
- \`GENARRATIVE_SPACETIME_HOST\` / \`GENARRATIVE_SPACETIME_PORT\` - \`GENARRATIVE_SPACETIME_HOST\` / \`GENARRATIVE_SPACETIME_PORT\`
- \`GENARRATIVE_SPACETIME_SERVER_URL\` / \`GENARRATIVE_SPACETIME_DATABASE\` - \`GENARRATIVE_SPACETIME_SERVER_URL\` / \`GENARRATIVE_SPACETIME_DATABASE\` / \`GENARRATIVE_SPACETIME_TOKEN\`
- \`GENARRATIVE_SPACETIME_ROOT_DIR\`:默认使用发布目录下的 \`.spacetimedb/\`,同时承载本地 SpacetimeDB 运行数据与 CLI 身份。 - \`GENARRATIVE_SPACETIME_ROOT_DIR\`:默认使用发布目录下的 \`.spacetimedb/\`,同时承载本地 SpacetimeDB 运行数据与 CLI 身份。
- \`GENARRATIVE_SPACETIME_TIMEOUT_SECONDS\`:等待 SpacetimeDB 就绪的秒数,默认 \`60\`。 - \`GENARRATIVE_SPACETIME_TIMEOUT_SECONDS\`:等待 SpacetimeDB 就绪的秒数,默认 \`60\`。
- \`GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT\`:默认 \`true\`,普通发布遇到 schema 冲突时自动导出、清库发布、导入回灌;设为 \`false\` 时保留原始发布失败。 - \`GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT\`:默认 \`true\`,普通发布遇到 schema 冲突时自动导出、清库发布、导入回灌;设为 \`false\` 时保留原始发布失败。
- \`GENARRATIVE_SPACETIME_MIGRATION_DIR\`:自动迁移 JSON 输出目录,默认 \`database-migrations/<database>/\`。 - \`GENARRATIVE_SPACETIME_MIGRATION_DIR\`:自动迁移 JSON 输出目录,默认 \`database-migrations/<database>/\`。
- OSS、LLM、短信、微信等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理。 - OSS、LLM、短信、微信、SpacetimeDB owner token 等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理;后台表统计读取 private 表时需要 \`GENARRATIVE_SPACETIME_TOKEN\` 对目标库有 owner 权限
- 迁移引导密钥由构建发布包时随机生成,构建日志和服务器 \`start.sh\` 发布日志都会显示同一份密钥。 - 迁移引导密钥由构建发布包时随机生成,构建日志和服务器 \`start.sh\` 发布日志都会显示同一份密钥。
EOF EOF
replace_placeholder_in_file "${TARGET_DIR}/README.md" "__GENARRATIVE_BUILD_NAME__" "${BUILD_NAME}" replace_placeholder_in_file "${TARGET_DIR}/README.md" "__GENARRATIVE_BUILD_NAME__" "${BUILD_NAME}"

View File

@@ -10,10 +10,11 @@ usage() {
说明: 说明:
1. 如果部署目录已有旧版本且存在 stop.sh则先执行旧版本 stop.sh。 1. 如果部署目录已有旧版本且存在 stop.sh则先执行旧版本 stop.sh。
2. 仅删除并替换发布产物文件或目录,保留部署目录中的运行数据目录。 2. 仅删除并替换发布产物文件或目录,保留部署目录中的运行数据目录。
3. 把指定发布目录中的白名单产物复制覆盖到部署目录。 3. 把指定发布目录中的白名单产物复制覆盖到部署目录,后台前端随 web/admin/ 一并覆盖
4. 如指定 --clear-database则以清库模式执行新版本 start.sh。 4. 如指定 --clear-database则以清库模式执行新版本 start.sh。
5. 默认允许新版本 start.sh 在 schema 冲突时自动导出、清库发布、导入回灌。 5. 默认允许新版本 start.sh 在 schema 冲突时自动导出、清库发布、导入回灌。
6. 最后执行新版本 start.sh 6. 覆盖 .env.local 时保留目标机已有 SpacetimeDB 运行 token供 api-server 后台概览读取 private 表统计
7. 最后执行新版本 start.sh。
参数: 参数:
--source-dir <path> 必填,待部署的发布目录,例如 build/123 --source-dir <path> 必填,待部署的发布目录,例如 build/123
@@ -179,6 +180,8 @@ MIGRATION_EXPORT_TOKEN=""
MIGRATION_IMPORT_TOKEN="" MIGRATION_IMPORT_TOKEN=""
PRESERVED_MIGRATION_EXPORT_TOKEN="" PRESERVED_MIGRATION_EXPORT_TOKEN=""
PRESERVED_MIGRATION_IMPORT_TOKEN="" PRESERVED_MIGRATION_IMPORT_TOKEN=""
PRESERVED_SPACETIME_TOKEN=""
PRESERVED_SPACETIME_MAINCLOUD_TOKEN=""
DEPLOY_ITEMS=( DEPLOY_ITEMS=(
".env" ".env"
".env.local" ".env.local"
@@ -364,6 +367,8 @@ fi
normalize_release_env_files "${SOURCE_DIR}" normalize_release_env_files "${SOURCE_DIR}"
PRESERVED_MIGRATION_EXPORT_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" PRESERVED_MIGRATION_EXPORT_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_EXPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
PRESERVED_MIGRATION_IMPORT_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" PRESERVED_MIGRATION_IMPORT_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
PRESERVED_SPACETIME_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
PRESERVED_SPACETIME_MAINCLOUD_TOKEN="$(read_env_value "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
if [[ -x "${DEPLOY_DIR}/stop.sh" ]]; then if [[ -x "${DEPLOY_DIR}/stop.sh" ]]; then
echo "[jenkins-deploy] 先停止旧版本: ${DEPLOY_DIR}" echo "[jenkins-deploy] 先停止旧版本: ${DEPLOY_DIR}"
@@ -424,6 +429,16 @@ elif [[ -n "${PRESERVED_MIGRATION_IMPORT_TOKEN}" ]] \
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]]; then && [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]]; then
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${PRESERVED_MIGRATION_IMPORT_TOKEN}" write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MIGRATION_IMPORT_TOKEN" "${PRESERVED_MIGRATION_IMPORT_TOKEN}"
fi fi
if [[ -n "${PRESERVED_SPACETIME_TOKEN}" ]] \
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]] \
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]]; then
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_TOKEN" "${PRESERVED_SPACETIME_TOKEN}"
fi
if [[ -n "${PRESERVED_SPACETIME_MAINCLOUD_TOKEN}" ]] \
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]] \
&& [[ -z "$(read_env_value "GENARRATIVE_SPACETIME_TOKEN" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" ]]; then
write_env_override "${DEPLOY_DIR}/.env.local" "GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN" "${PRESERVED_SPACETIME_MAINCLOUD_TOKEN}"
fi
DEPLOY_DATABASE="$(read_env_value "GENARRATIVE_SPACETIME_DATABASE" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")" DEPLOY_DATABASE="$(read_env_value "GENARRATIVE_SPACETIME_DATABASE" "${DEPLOY_DIR}/.env" "${DEPLOY_DIR}/.env.local")"
if [[ -z "${DEPLOY_DATABASE}" ]]; then if [[ -z "${DEPLOY_DATABASE}" ]]; then

View File

@@ -39,43 +39,6 @@ const BLOCKED_DEBUG_HEADERS: &[&str] = &[
"transfer-encoding", "transfer-encoding",
"expect", "expect",
]; ];
// 数据库概览首版只统计受控白名单表,禁止后台页面直接输入任意 SQL。
const DATABASE_OVERVIEW_TABLES: &[&str] = &[
"runtime_setting",
"runtime_snapshot",
"user_browse_history",
"profile_dashboard_state",
"profile_wallet_ledger",
"profile_played_world",
"profile_save_archive",
"story_session",
"story_event",
"battle_state",
"inventory_slot",
"quest_record",
"quest_log",
"treasure_record",
"npc_state",
"custom_world_profile",
"custom_world_gallery_entry",
"custom_world_agent_session",
"custom_world_agent_message",
"custom_world_agent_operation",
"custom_world_draft_card",
"big_fish_creation_session",
"big_fish_agent_message",
"big_fish_asset_slot",
"puzzle_work_profile",
"puzzle_agent_session",
"puzzle_agent_message",
"puzzle_runtime_run",
"ai_task",
"ai_task_stage",
"ai_text_chunk",
"ai_result_reference",
"asset_object",
"asset_entity_binding",
];
// SpacetimeDB 2.x 的 schema HTTP API 要求显式传入 BSATN JSON 版本。 // SpacetimeDB 2.x 的 schema HTTP API 要求显式传入 BSATN JSON 版本。
// 后台总览只读取表名,固定使用当前 CLI 2.1.0 兼容的版本参数即可。 // 后台总览只读取表名,固定使用当前 CLI 2.1.0 兼容的版本参数即可。
const SPACETIME_SCHEMA_VERSION_QUERY: &str = "version=9"; const SPACETIME_SCHEMA_VERSION_QUERY: &str = "version=9";
@@ -283,7 +246,7 @@ async fn fetch_database_overview(state: &AppState) -> AdminDatabaseOverviewPaylo
.ok() .ok()
.flatten(); .flatten();
let mut schema_table_names = schema let schema_table_names = schema
.as_ref() .as_ref()
.and_then(|value| value.tables.as_ref()) .and_then(|value| value.tables.as_ref())
.map(|tables| { .map(|tables| {
@@ -300,31 +263,33 @@ async fn fetch_database_overview(state: &AppState) -> AdminDatabaseOverviewPaylo
.unwrap_or_default(); .unwrap_or_default();
let mut table_stats = Vec::new(); let mut table_stats = Vec::new();
for table_name in DATABASE_OVERVIEW_TABLES { for table_name in &schema_table_names {
if !is_safe_spacetime_table_name(table_name) {
table_stats.push(AdminDatabaseTableStatPayload {
table_name: table_name.clone(),
row_count: None,
error_message: Some("表名不适合 SQL 统计".to_string()),
});
continue;
}
let sql = format!("SELECT COUNT(*) AS row_count FROM {table_name}"); let sql = format!("SELECT COUNT(*) AS row_count FROM {table_name}");
match fetch_spacetime_sql_count(&client, server_root, database, token, &sql).await { match fetch_spacetime_sql_count(&client, server_root, database, token, &sql).await {
Ok(row_count) => table_stats.push(AdminDatabaseTableStatPayload { Ok(row_count) => table_stats.push(AdminDatabaseTableStatPayload {
table_name: (*table_name).to_string(), table_name: table_name.clone(),
row_count: Some(row_count), row_count: Some(row_count),
error_message: None, error_message: None,
}), }),
Err(error) => { Err(error) => {
table_stats.push(AdminDatabaseTableStatPayload { table_stats.push(AdminDatabaseTableStatPayload {
table_name: (*table_name).to_string(), table_name: table_name.clone(),
row_count: None, row_count: None,
error_message: Some(error), error_message: Some(normalize_table_count_error(&error)),
}); });
} }
} }
} }
for table_name in DATABASE_OVERVIEW_TABLES {
if !schema_table_names.iter().any(|name| name == table_name) {
schema_table_names.push((*table_name).to_string());
}
}
schema_table_names.sort();
AdminDatabaseOverviewPayload { AdminDatabaseOverviewPayload {
database_identity: database_info database_identity: database_info
.as_ref() .as_ref()
@@ -345,6 +310,27 @@ fn build_spacetime_schema_url(server_root: &str, database: &str) -> String {
format!("{server_root}/v1/database/{database}/schema?{SPACETIME_SCHEMA_VERSION_QUERY}") format!("{server_root}/v1/database/{database}/schema?{SPACETIME_SCHEMA_VERSION_QUERY}")
} }
// 表名来自 schema但进入 SQL 前仍做最小标识符校验,避免未来 schema 来源变化时扩大风险面。
fn is_safe_spacetime_table_name(table_name: &str) -> bool {
let mut chars = table_name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first == '_' || first.is_ascii_alphabetic()) {
return false;
}
chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
// private 表在 SpacetimeDB SQL 下会表现为不可见,后台只展示可理解状态,不暴露整段 HTTP 噪音。
fn normalize_table_count_error(error: &str) -> String {
let normalized = error.to_ascii_lowercase();
if normalized.contains("marked private") || normalized.contains("no such table") {
return "不可统计private 或当前身份不可见)".to_string();
}
error.to_string()
}
async fn fetch_spacetime_json<T>( async fn fetch_spacetime_json<T>(
client: &Client, client: &Client,
url: &str, url: &str,
@@ -662,7 +648,8 @@ fn build_admin_session_payload(session: crate::state::AdminSession) -> AdminSess
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
build_body_preview, build_debug_base_url, build_spacetime_schema_url, normalize_debug_path, build_body_preview, build_debug_base_url, build_spacetime_schema_url,
is_safe_spacetime_table_name, normalize_debug_path, normalize_table_count_error,
parse_spacetime_sql_count_response, trim_preview, parse_spacetime_sql_count_response, trim_preview,
}; };
use axum::{http::StatusCode, response::IntoResponse}; use axum::{http::StatusCode, response::IntoResponse};
@@ -722,6 +709,38 @@ mod tests {
); );
} }
#[test]
fn is_safe_spacetime_table_name_accepts_schema_identifiers() {
assert!(is_safe_spacetime_table_name("runtime_setting"));
assert!(is_safe_spacetime_table_name("_private_table"));
assert!(is_safe_spacetime_table_name("AiTaskStage2"));
}
#[test]
fn is_safe_spacetime_table_name_rejects_sql_fragments() {
assert!(!is_safe_spacetime_table_name(""));
assert!(!is_safe_spacetime_table_name("bad-name"));
assert!(!is_safe_spacetime_table_name("1bad"));
assert!(!is_safe_spacetime_table_name("runtime_setting;DROP"));
}
#[test]
fn normalize_table_count_error_hides_private_table_http_noise() {
let error = "HTTP 400no such table: `runtime_setting`. If the table exists, it may be marked private.";
assert_eq!(
normalize_table_count_error(error),
"不可统计private 或当前身份不可见)"
);
}
#[test]
fn normalize_table_count_error_keeps_other_errors() {
let error = "SQL 请求失败connection refused";
assert_eq!(normalize_table_count_error(error), error);
}
#[test] #[test]
fn parse_spacetime_sql_count_response_accepts_statement_array_rows() { fn parse_spacetime_sql_count_response_accepts_statement_array_rows() {
let payload = json!([ let payload = json!([

View File

@@ -1835,6 +1835,10 @@ mod tests {
payload["user"]["loginMethod"], payload["user"]["loginMethod"],
Value::String("password".to_string()) Value::String("password".to_string())
); );
assert_eq!(
payload["user"]["createdAt"],
Value::String(seed_user.created_at)
);
assert!(payload["token"].as_str().is_some()); assert!(payload["token"].as_str().is_some());
} }
@@ -2114,6 +2118,7 @@ mod tests {
payload["user"]["phoneNumberMasked"], payload["user"]["phoneNumberMasked"],
Value::String("138****8000".to_string()) Value::String("138****8000".to_string())
); );
assert!(payload["user"]["createdAt"].as_str().is_some());
assert_eq!(payload["created"], Value::Bool(true)); assert_eq!(payload["created"], Value::Bool(true));
assert!(payload["referral"].is_null()); assert!(payload["referral"].is_null());
} }

View File

@@ -29,7 +29,7 @@ where
} }
} }
/// 资产操作统一预扣陶泥币;扣费流水 ID 由业务资源 ID 参与构造,保证重试幂等。 /// 资产操作统一预扣光点;扣费流水 ID 由业务资源 ID 参与构造,保证重试幂等。
async fn consume_asset_operation_points( async fn consume_asset_operation_points(
state: &AppState, state: &AppState,
owner_user_id: &str, owner_user_id: &str,
@@ -79,7 +79,7 @@ async fn refund_asset_operation_points(
asset_kind, asset_kind,
asset_id, asset_id,
error = %error, error = %error,
"资产操作失败后的陶泥币退款失败" "资产操作失败后的光点退款失败"
); );
} }
} }
@@ -89,10 +89,10 @@ pub(crate) fn map_asset_operation_wallet_error(error: SpacetimeClientError) -> A
tracing::warn!( tracing::warn!(
provider = "profile-wallet", provider = "profile-wallet",
error = %message, error = %message,
"资产操作陶泥币预扣失败" "资产操作光点预扣失败"
); );
let status = match &error { let status = match &error {
SpacetimeClientError::Procedure(message) if message.contains("陶泥币余额不足") => { SpacetimeClientError::Procedure(message) if message.contains("光点余额不足") => {
StatusCode::CONFLICT StatusCode::CONFLICT
} }
_ => StatusCode::BAD_GATEWAY, _ => StatusCode::BAD_GATEWAY,

View File

@@ -12,6 +12,7 @@ pub fn map_auth_user_payload(user: AuthUser) -> AuthUserPayload {
login_method: user.login_method.as_str().to_string(), login_method: user.login_method.as_str().to_string(),
binding_status: user.binding_status.as_str().to_string(), binding_status: user.binding_status.as_str().to_string(),
wechat_bound: user.wechat_bound, wechat_bound: user.wechat_bound,
created_at: user.created_at,
} }
} }

View File

@@ -20,7 +20,7 @@ pub async fn get_public_user_by_code(
.get_user_by_public_user_code(&code) .get_user_by_public_user_code(&code)
.map_err(map_public_user_search_error)? .map_err(map_public_user_search_error)?
.ok_or_else(|| { .ok_or_else(|| {
AppError::from_status(StatusCode::NOT_FOUND).with_message("未找到对应陶泥号用户") AppError::from_status(StatusCode::NOT_FOUND).with_message("未找到对应百梦号用户")
})?; })?;
Ok(json_success_body( Ok(json_success_body(
@@ -60,7 +60,7 @@ pub async fn get_public_user_by_id(
fn map_public_user_search_error(error: module_auth::PasswordEntryError) -> AppError { fn map_public_user_search_error(error: module_auth::PasswordEntryError) -> AppError {
match error { match error {
module_auth::PasswordEntryError::InvalidPublicUserCode => { module_auth::PasswordEntryError::InvalidPublicUserCode => {
AppError::from_status(StatusCode::BAD_REQUEST).with_message("陶泥号格式不正确") AppError::from_status(StatusCode::BAD_REQUEST).with_message("百梦号格式不正确")
} }
module_auth::PasswordEntryError::Store(_) module_auth::PasswordEntryError::Store(_)
| module_auth::PasswordEntryError::PasswordHash(_) | module_auth::PasswordEntryError::PasswordHash(_)

View File

@@ -3462,7 +3462,7 @@ fn resolve_author_public_user_code(
request_context, request_context,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "custom-world-library", "provider": "custom-world-library",
"message": format!("作者陶泥号读取失败:{error}"), "message": format!("作者百梦号读取失败:{error}"),
})), })),
) )
})? })?
@@ -3473,7 +3473,7 @@ fn resolve_author_public_user_code(
request_context, request_context,
AppError::from_status(StatusCode::UNAUTHORIZED).with_details(json!({ AppError::from_status(StatusCode::UNAUTHORIZED).with_details(json!({
"provider": "custom-world-library", "provider": "custom-world-library",
"message": "当前登录用户缺少陶泥", "message": "当前登录用户缺少百梦",
})), })),
) )
}) })

View File

@@ -41,7 +41,7 @@ pub async fn generate_custom_world_foundation_draft(
emit_foundation_draft_progress( emit_foundation_draft_progress(
&mut on_progress, &mut on_progress,
"整理世界骨架", "整理世界骨架",
"正在根据陶泥主锚点生成第一版世界框架。", "正在根据百梦主锚点生成第一版世界框架。",
12, 12,
); );
let mut framework = request_foundation_json_stage( let mut framework = request_foundation_json_stage(

View File

@@ -49,6 +49,7 @@ mod prompt;
mod puzzle; mod puzzle;
mod puzzle_agent_turn; mod puzzle_agent_turn;
mod refresh_session; mod refresh_session;
mod registration_reward;
mod request_context; mod request_context;
mod response_headers; mod response_headers;
mod runtime_browse_history; mod runtime_browse_history;

View File

@@ -39,6 +39,14 @@ pub async fn password_entry(
state.password_entry_service().execute(input).await state.password_entry_service().execute(input).await
} }
.map_err(map_password_entry_error)?; .map_err(map_password_entry_error)?;
if result.created {
crate::registration_reward::grant_new_user_registration_wallet_reward(
&state,
&request_context,
&result.user.id,
)
.await;
}
let session_client = resolve_session_client_context(&headers); let session_client = resolve_session_client_context(&headers);
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?; let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
state state
@@ -80,7 +88,7 @@ fn map_password_entry_error(error: PasswordEntryError) -> AppError {
"field": "password", "field": "password",
})), })),
PasswordEntryError::InvalidPublicUserCode => AppError::from_status(StatusCode::BAD_REQUEST) PasswordEntryError::InvalidPublicUserCode => AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("陶泥号格式不正确") .with_message("百梦号格式不正确")
.with_details(json!({ .with_details(json!({
"field": "phone", "field": "phone",
})), })),

View File

@@ -149,6 +149,14 @@ pub async fn phone_login(
} }
}; };
let created = result.created; let created = result.created;
if created {
crate::registration_reward::grant_new_user_registration_wallet_reward(
&state,
&request_context,
&result.user.id,
)
.await;
}
let referral = if created { let referral = if created {
bind_referral_invite_code_on_registration( bind_referral_invite_code_on_registration(
&state, &state,

View File

@@ -10,7 +10,7 @@ use crate::creation_agent_anchor_templates::{
}; };
use crate::creation_agent_chat::render_quick_fill_extra_rules; use crate::creation_agent_chat::render_quick_fill_extra_rules;
pub(crate) const BIG_FISH_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和陶泥主共创“大鱼吃小鱼”竖屏玩法的中文创意策划。 pub(crate) const BIG_FISH_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和百梦主共创“大鱼吃小鱼”竖屏玩法的中文创意策划。
你必须把用户灵感收束成可以编译为可玩草稿的玩法、生态视觉、成长阶梯和风险节奏。 你必须把用户灵感收束成可以编译为可玩草稿的玩法、生态视觉、成长阶梯和风险节奏。

View File

@@ -12,7 +12,7 @@ use crate::creation_agent_chat::render_quick_fill_extra_rules;
/// 拼图共创 Agent 的系统提示词。 /// 拼图共创 Agent 的系统提示词。
/// ///
/// 这里作为拼图聊天提示词主源,业务文件只负责调用 LLM、解析结果和写回状态。 /// 这里作为拼图聊天提示词主源,业务文件只负责调用 LLM、解析结果和写回状态。
pub(crate) const PUZZLE_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和陶泥主共创拼图画面的中文创意策划。 pub(crate) const PUZZLE_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和百梦主共创拼图画面的中文创意策划。
你要帮助用户把一句灵感逐步收束成可以发布成拼图关卡的视觉方案。 你要帮助用户把一句灵感逐步收束成可以发布成拼图关卡的视觉方案。

View File

@@ -14,7 +14,7 @@ pub(crate) const PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS: usize = 500;
const PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS: usize = 40; const PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS: usize = 40;
const PUZZLE_IMAGE_PROMPT_FALLBACK: &str = "清晰、有辨识度的拼图画面"; const PUZZLE_IMAGE_PROMPT_FALLBACK: &str = "清晰、有辨识度的拼图画面";
/// 根据拼图关卡名和陶泥主输入构造最终发给图片模型的提示词。 /// 根据拼图关卡名和百梦主输入构造最终发给图片模型的提示词。
pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> String { pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> String {
let level_name = let level_name =
truncate_puzzle_prompt_segment(level_name.trim(), PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS); truncate_puzzle_prompt_segment(level_name.trim(), PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS);

View File

@@ -1396,9 +1396,13 @@ pub async fn use_puzzle_runtime_prop(
)); ));
} }
}; };
let should_sync_freeze_boundary = matches!(prop_kind.as_str(), "freezeTime" | "freeze_time");
let billing_asset_id = format!("{}:{}:{}", run_id, prop_kind, current_utc_micros()); let billing_asset_id = format!("{}:{}:{}", run_id, prop_kind, current_utc_micros());
let reducer_owner_user_id = owner_user_id.clone(); let reducer_owner_user_id = owner_user_id.clone();
let run = execute_billable_asset_operation( let reducer_run_id = run_id.clone();
let fallback_run_id = run_id.clone();
let fallback_owner_user_id = owner_user_id.clone();
let run_result = execute_billable_asset_operation(
&state, &state,
&owner_user_id, &owner_user_id,
billing_asset_kind, billing_asset_kind,
@@ -1407,7 +1411,7 @@ pub async fn use_puzzle_runtime_prop(
state state
.spacetime_client() .spacetime_client()
.use_puzzle_runtime_prop(PuzzleRunPropRecordInput { .use_puzzle_runtime_prop(PuzzleRunPropRecordInput {
run_id, run_id: reducer_run_id,
owner_user_id: reducer_owner_user_id, owner_user_id: reducer_owner_user_id,
prop_kind, prop_kind,
used_at_micros: current_utc_micros(), used_at_micros: current_utc_micros(),
@@ -1417,8 +1421,30 @@ pub async fn use_puzzle_runtime_prop(
.map_err(map_puzzle_client_error) .map_err(map_puzzle_client_error)
}, },
) )
.await .await;
.map_err(|error| puzzle_error_response(&request_context, PUZZLE_RUNTIME_PROVIDER, error))?;
let run = match run_result {
Ok(run) => run,
Err(error) if should_sync_puzzle_freeze_boundary(&error, should_sync_freeze_boundary) => {
// 中文注释:冻结确认窗打开时前端会暂停视觉计时,但正式 run 仍可能在服务端边界帧先结算失败。
// 这类情况已由扣费包装器退款,此处只同步失败态快照,避免玩家看到“操作不合法”。
state
.spacetime_client()
.get_puzzle_run(fallback_run_id, fallback_owner_user_id)
.await
.map_err(map_puzzle_client_error)
.map_err(|error| {
puzzle_error_response(&request_context, PUZZLE_RUNTIME_PROVIDER, error)
})?
}
Err(error) => {
return Err(puzzle_error_response(
&request_context,
PUZZLE_RUNTIME_PROVIDER,
error,
));
}
};
Ok(json_success_body( Ok(json_success_body(
Some(&request_context), Some(&request_context),
@@ -2503,6 +2529,10 @@ fn map_puzzle_client_error(error: SpacetimeClientError) -> AppError {
})) }))
} }
fn should_sync_puzzle_freeze_boundary(error: &AppError, is_freeze_time: bool) -> bool {
is_freeze_time && error.body_text().contains("操作不合法")
}
fn is_missing_puzzle_form_draft_procedure_error(error: &SpacetimeClientError) -> bool { fn is_missing_puzzle_form_draft_procedure_error(error: &SpacetimeClientError) -> bool {
matches!(error, SpacetimeClientError::Procedure(message) if matches!(error, SpacetimeClientError::Procedure(message) if
message.contains("save_puzzle_form_draft") message.contains("save_puzzle_form_draft")
@@ -3580,6 +3610,26 @@ mod tests {
let response = error.into_response(); let response = error.into_response();
assert_eq!(response.status(), StatusCode::BAD_GATEWAY); assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
} }
#[test]
fn freeze_boundary_sync_only_matches_freeze_invalid_operation() {
let invalid_operation =
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "spacetimedb",
"message": "操作不合法",
}));
let other_error = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "spacetimedb",
"message": "光点余额不足",
}));
assert!(should_sync_puzzle_freeze_boundary(&invalid_operation, true));
assert!(!should_sync_puzzle_freeze_boundary(
&invalid_operation,
false
));
assert!(!should_sync_puzzle_freeze_boundary(&other_error, true));
}
} }
struct PuzzleDashScopeSettings { struct PuzzleDashScopeSettings {

View File

@@ -0,0 +1,30 @@
#[cfg(not(test))]
use tracing::warn;
use crate::{request_context::RequestContext, state::AppState};
pub async fn grant_new_user_registration_wallet_reward(
state: &AppState,
request_context: &RequestContext,
user_id: &str,
) {
#[cfg(test)]
{
let _ = (state, request_context, user_id);
}
#[cfg(not(test))]
if let Err(error) = state
.spacetime_client()
.grant_new_user_registration_wallet_reward(user_id.to_string())
.await
{
warn!(
request_id = request_context.request_id(),
operation = request_context.operation(),
user_id = user_id,
error = %error,
"新用户注册光点赠送失败,注册流程继续"
);
}
}

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