合并编辑器素材库分支

合并 codex/editor-asset-library 到 master
保留原生壳桥接统一分支已合入内容
合并图片编辑器素材库、模型定价和项目记忆更新
解决项目记忆踩坑记录冲突
This commit is contained in:
2026-06-22 22:37:14 +08:00
102 changed files with 8494 additions and 1648 deletions
+42 -70
View File
@@ -1,93 +1,65 @@
# AGENTS.md
## 项目共享记忆
- 本仓库的团队级项目记忆位于 [`docs/project-memory/`](docs/project-memory/),用于在 3 名开发人员和各自本地 Agent 之间同步长期项目知识。
- [`.hermes/`](.hermes/) 只保存 Hermes 专用的仓库级工具资源,例如 skills、plugins 和启用说明,不作为项目知识库。
- 开始复杂开发任务前,除阅读本文件外,还应优先读取:
- [`docs/project-memory/README.md`](docs/project-memory/README.md)
- [`docs/project-memory/shared-memory/project-overview.md`](docs/project-memory/shared-memory/project-overview.md)
- [`docs/project-memory/shared-memory/team-conventions.md`](docs/project-memory/shared-memory/team-conventions.md)
- [`docs/project-memory/shared-memory/development-workflow.md`](docs/project-memory/shared-memory/development-workflow.md)
- 与任务相关的 [`docs/project-memory/shared-memory/decision-log.md`](docs/project-memory/shared-memory/decision-log.md) 和 [`docs/project-memory/shared-memory/pitfalls.md`](docs/project-memory/shared-memory/pitfalls.md)
- 仅在需要使用仓库级 Hermes skills/plugins 时,再读取 [`.hermes/README.md`](.hermes/README.md);长期项目记忆不要从 `.hermes/` 读取。
- 如果本次任务产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则,应同步更新 `docs/project-memory/shared-memory/` 中对应文件。
- 禁止提交个人 `~/.hermes` 配置、`.env`、API Key、Token、会话记录、认证文件和本地私密路径。
-`docs/project-memory/shared-memory/` 与当前代码或 `docs/` 最新文档冲突,以代码和最新 `docs/` 为准,并同步修正过期共享记忆。
## 入口定位
## Agent 本地 RAG
- 本仓库提供面向 Agent 的本地文档 RAG,入口位于 [`scripts/rag/`](scripts/rag/);RAG 主要用于 Agent 检索项目上下文,不替代人工阅读 `AGENTS.md``docs/README.md``docs/project-memory/`
- 开始复杂任务、跨模块任务或不确定文档入口时,Agent 可先用 `npm run rag:search -- --query "问题或关键词" --limit 8 --max-chars 12000` 取候选上下文;需要刷新索引时运行 `npm run rag:index`
- RAG 输出只作为候选上下文。涉及精确代码或文档修改时,仍需打开对应源文件核对;来源冲突,以当前代码和最新 `docs/` 为准。
- 默认不安装 RAG 运行时依赖,也不把 LanceDB、Transformers.js 或本地 embedding 模型写入根 `package.json`。需要启用时,Agent 必须先询问用户是否安装,并在确认后只安装到 gitignored 的 `.rag/runtime/`;详细命令见 [`scripts/rag/README.md`](scripts/rag/README.md)。
- 本文件只保留 Agent 进入仓库后必须立即遵守的最高优先级规则;完整执行细则见 [`docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`](docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md)。
- 团队级长期项目记忆位于 [`docs/project-memory/`](docs/project-memory/),供 3 名开发人员和各自本地 Agent 通过 Git 同步
- [`.hermes/`](.hermes/) 只保存 Hermes 专用仓库级工具资源,例如 skills、plugins 和启用说明;长期项目知识不要写入 `.hermes/`
- `docs/project-memory/shared-memory/` 与当前代码或最新 `docs/` 冲突,以代码和最新 `docs/` 为准,并同步修正过期共享记忆
## Agent skills
## 开始任务前
### Issue tracker
- 简单自包含任务可以直接执行;复杂开发、跨模块修改、后端 / UI / 文档体系调整前,按顺序读取:
1. 本文件。
2. [`docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`](docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md)。
3. [`docs/project-memory/README.md`](docs/project-memory/README.md)、[`project-overview.md`](docs/project-memory/shared-memory/project-overview.md)、[`team-conventions.md`](docs/project-memory/shared-memory/team-conventions.md)、[`development-workflow.md`](docs/project-memory/shared-memory/development-workflow.md)。
4. 与任务相关的 [`decision-log.md`](docs/project-memory/shared-memory/decision-log.md)、[`pitfalls.md`](docs/project-memory/shared-memory/pitfalls.md)、[`docs/README.md`](docs/README.md) 和当前专题文档。
- 落地工程修改前,先确认是否已有足够具体的 PRD、技术方案或当前融合文档;文档仍存在编码级歧义时,先补文档再编码。
- 本仓库的本地 RAG 位于 [`scripts/rag/`](scripts/rag/);RAG 只作为候选上下文,不替代打开源文件核对。默认不安装 RAG 运行时依赖,需要启用时必须先询问用户,并只安装到 gitignored 的 `.rag/runtime/`
Issues are tracked in the self-hosted Gitea remote for this repo. Use Gitea Issues via the configured Gitea UI/API or `tea` CLI when available; do not use GitHub `gh` or GitLab `glab` unless the repo is migrated. Current issue workflow is summarized in `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`.
## 绝对约束
### Triage labels
- 禁止提交个人 `~/.hermes` 配置、`.env`、API Key、Token、Cookie、会话记录、认证文件、本地私密路径、构建产物、日志、缓存和数据库 dump。
- 不要在 `.gitignore` 中新增 `.env.local`
- 不要擅自把现有中文文案、注释、剧情或文档改写成英文;看到中文乱码时先确认真实编码,不要沿用乱码或用英文替换。
- 修改包含中文的文件时优先局部补丁,避免整文件重写;修改后优先运行仓库编码检查。
- 后续新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;历史文档不要求批量重命名,除非本次任务明确涉及。
- 工程修改要同步更新对应 `docs/` 文档;产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则时,同步更新 `docs/project-memory/shared-memory/`
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件,不新建平行系统或平行页面。
- UI 面板中不要默认写功能说明、规则描述或开发解释文案;移动端优先,同时保证网页端可正常显示和操作。
- 点击按钮弹出独立面板的设计,不要实现成在当前面板下面追加内容。
Use the default canonical triage labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`.
## 任务路由
### Domain docs
- Issue 使用自托管 Gitea;优先用 Gitea UI/API 或 `tea` CLI,不使用 GitHub `gh` 或 GitLab `glab`,除非仓库已迁移。默认 triage 标签:`needs-triage``needs-info``ready-for-agent``ready-for-human``wontfix`
- 需要仓库级 Hermes skills/plugins 时,再读取 [`.hermes/README.md`](.hermes/README.md)。
- 新增、补齐、迁移或重构玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model 前,必须读取并按 [`genarrative-play-type-integration`](.codex/skills/genarrative-play-type-integration/SKILL.md) 执行。
- 涉及 `npm run dev` / `npm run dev:spacetime` / `npm run dev:api-server` / `npm run dev:web` / `npm run dev:admin-web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。
- 涉及 SpacetimeDB 的设计、实现、脚本、调试、发布、绑定生成、schema、reducer、procedure、view 或 Rust API 时,必须读取并按 [`spacetimedb-cli`](.codex/skills/spacetimedb-cli/SKILL.md)、[`spacetimedb-rust`](.codex/skills/spacetimedb-rust/SKILL.md)、[`spacetimedb-concepts`](.codex/skills/spacetimedb-concepts/SKILL.md) 中相关 skill 执行。
Single-context layout: read root `CONTEXT.md` when present. Current architecture and product constraints are consolidated under `docs/`.
## 后端红线
### 新增玩法接入
- 凡是新增、补齐、迁移或重构任何玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model 的任务,开始前必须显式读取并按 [$genarrative-play-type-integration](.codex\skills\genarrative-play-type-integration\SKILL.md) 执行;未先使用该 skill 的,不允许进入编码。
## 项目约束
- 代码需要有完善的中文注释
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
- 对工程的修改不仅要落地到代码更面,还要更改对应文档,若没有生成新的文档,文档统一存在doc目录中
- 后续新增的 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;例如 `【后端架构】api-server能力模块化与图片资产Adapter收口计划-2026-05-14.md`。不要求批量重命名历史文档,除非本次任务明确涉及该文档。
- 不要擅自把现有中文文案、注释、剧情、文档改写成英文,除非用户明确要求翻译。
- 看到中文乱码时,不要直接沿用乱码文本,也不要用英文替换;先确认文件真实编码,再决定是否修改。
- 在 PowerShell 5.1 中读取或写入文本时,必须显式使用 UTF-8;如果终端输出疑似乱码,要用 `Get-Content -Encoding UTF8`、Python 或 Node 再次核对原文。
- 非必要不要整文件重写,尤其是包含中文的文件;优先做局部补丁,避免把未改动的中文内容重新编码。
- 修改包含中文的文件后,优先运行仓库里的编码检查,确保没有把文本写坏。
- UI面板中不要默认写一些规则描述文案,清爽一些,按照游戏UI设计规范设计即可。
- UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。
- 不要在gitignore中添加.env.local文件。
- 提交代码时,提交标题必须使用中文;标题后必须逐行写明本次提交修改了什么,每条变更单独一行。
- 严格遵循简洁的代码风格
- 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。
- 禁止将功能说明描述类的文本默认写入UI界面中。
- prd文档中每个模块的描述要落地设计到可以精准编码到位,不能出现需求落地漂移。
- 点击按钮弹出独立的面板的设计不要实现成在当前面板下面显示内容。
- 每个阶段任务完成后自动压缩上下文,确保后续阶段在清晰、低噪音的上下文基础上继续推进。
## 后端技术约束
- 后端最新技术约束以 [`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md) 为准。
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`server-rs/crates/api-server/src/app.rs``shared-contracts` `packages/shared` 为准;不得在前端、`api-server` 或临时兼容层中重新发明旧接口
- SpacetimeDB 表结构、自动迁移限制和冲突处理以当前后端架构文档的 schema 变更规则和表目录为准;涉及 table、reducer、procedure、row shape 或绑定变化时,必须同步 `migration.rs`、表目录和生成绑定。
- SpacetimeDB 已有表新增字段时,字段必须放在 Rust 表结构体最后,并设置明确默认值(例如 `#[default(...)]`);需要修改字段名时,必须先询问用户并确认迁移计划,再改代码,同时更新 `server-rs/crates/spacetime-module/src/migration.rs`、表目录和生成绑定。
- 修改 SpacetimeDB schema 后必须运行 `npm run check:spacetime-schema`;该检查会拦截新增字段缺 default、字段不在末尾、字段删除/改名/重排/改类型,以及漏改 `migration.rs`、表目录或生成绑定。
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`。旧 `server-node`、Express、PostgreSQL 不再作为兼容目标;历史实现只能作为迁移参考,若旧文档与 DDD 约束冲突,先修正文档和方案再编码。
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`;旧 `server-node`、Express、PostgreSQL、Go 服务端、`maincloud` / `Maincloud` / `MAINCLOUD` 只作为历史残留,不作为兼容目标
- DDD 分层边界按总纲执行:领域规则沉到 `module-*`SpacetimeDB 表和事务编排留在 `spacetime-module`,后端访问 SpacetimeDB 统一经 `spacetime-client` facadeHTTP/SSE/BFF 留在 `api-server`,外部副作用留在 `platform-*`,前后端 DTO 留在 `shared-contracts`
- 前端只做表现、交互和临时 UI 状态,不承接正式业务真相,不绕过后端投影或后端 API 直接实现业务规则。
- 修改后端代码后,按对应 DDD 文档中的验收命令执行测试;涉及 API smoke 时使用 `npm run api-server` 重新拉起后端并执行相应自动测试,同时确认 `/healthz`
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关脚本、环境变量、测试、文档要求和命名全部视为历史残留,禁止新增、运行或引用;若旧材料仍要求 `api-server:maincloud``GENARRATIVE_SPACETIME_MAINCLOUD_*`,以当前后端架构文档和本文件为准
- 除 CI/CD 脚本内部受控用法外,人工命令、本地联调、排障步骤和文档示例禁止继续使用 `spacetime --root-dir`。本地数据隔离使用项目脚本或 `--data-dir`,发布目标必须显式传 `--server` / `--server-url`,身份问题通过同一 CLI 登录态、专用运行用户或显式 token 处理;若旧文档仍推荐 `--root-dir`,先修正文档口径再执行
- 凡是涉及 SpacetimeDB 的设计、实现、脚本、调试、前端绑定接入,统一显式使用以下 skill 作为执行依据:
- [$spacetimedb-cli](.codex\\skills\\spacetimedb-cli\\SKILL.md)
- [$spacetimedb-rust](.codex\\skills\\spacetimedb-rust\\SKILL.md)
- [$spacetimedb-concepts](.codex\\skills\\spacetimedb-concepts\\SKILL.md)
- 涉及 `spacetime` CLI、发布、绑定生成、本地联调时,按 `spacetimedb-cli` 执行
- 涉及 `npm run dev` / `npm run dev:rust` / `npm run dev:web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。
- 涉及 `crates/spacetime-module` 的表、reducer、view、Rust API 使用时,按 `spacetimedb-rust``spacetimedb-concepts`行。
- 涉及前端或 Node 侧的 SpacetimeDB 订阅、绑定使用时,按当前生成绑定、项目代码和官方文档核对;本仓库不再维护单独 TypeScript / C# / Unity SpacetimeDB skill。
- 若仓库内旧实现或旧文档与这些 skill 冲突,先修正文档和方案,再继续编码。
- 修改后端代码后,必须使用 `npm run api-server` 自动重新运行后端,并执行相应自动测试;不要再使用旧的后端重启命令。
- 数据库表结构更改后,需要对齐migration.rs
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`server-rs/crates/api-server/src/app.rs``shared-contracts``packages/shared` 为准;不得在前端、`api-server` 或临时兼容层中重新发明旧接口
- SpacetimeDB 已有表新增字段时,字段必须放在 Rust 表结构体最后,并设置明确默认值;需要删除、改名、重排或改类型时,必须先询问用户并确认迁移计划
- 修改 SpacetimeDB schema 后必须同步 `migration.rs`、表目录和生成绑定,并运行 `npm run check:spacetime-schema`
- 除 CI/CD 脚本内部受控用法外,人工命令、本地联调、排障步骤和文档示例禁止继续使用 `spacetime --root-dir`
## 验证与提交
- 修改后按范围运行定向测试、类型检查、`npm run check:encoding``git diff --check`;后端 API smoke 使用 `npm run dev:api-server` 拉起后端并检查 `/healthz`
- 修改 SpacetimeDB schema 后追加 `npm run check:spacetime-schema`;涉及发布或生产运维时按当前开发运维文档和脚本门禁执行。
- 提交代码时,提交标题必须使用中文;标题后逐行写明本次提交修改了什么,每条变更单独一行。
## 文档图谱
```text
docs/
├─ README.md
├─ 【协作规范】Agent工作入口与执行准则-2026-06-22.md
├─ 【项目基线】当前产品与工程约束-2026-05-15.md
├─ 【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md
├─ 【玩法创作】平台入口与玩法链路-2026-05-15.md
+22
View File
@@ -25,6 +25,7 @@ import type {
ApiErrorEnvelope,
ApiMeta,
ApiSuccessEnvelope,
EditorGenerationPricingConfigPayload,
ProfileInviteCodeAdminListResponse,
ProfileInviteCodeAdminResponse,
ProfileRechargeProductConfigAdminListResponse,
@@ -228,6 +229,27 @@ export function upsertAdminPublicWorkInteractions(
);
}
export function getAdminEditorGenerationPricing(token: string) {
return request<EditorGenerationPricingConfigPayload>(
'/admin/api/editor-generation-pricing',
{ token },
);
}
export function upsertAdminEditorGenerationPricing(
token: string,
payload: EditorGenerationPricingConfigPayload,
) {
return request<EditorGenerationPricingConfigPayload>(
'/admin/api/editor-generation-pricing',
{
method: 'POST',
token,
body: payload,
},
);
}
export function listAdminWorkVisibility(token: string) {
return request<AdminWorkVisibilityListResponse>(
'/admin/api/works/visibility',
+13
View File
@@ -210,6 +210,19 @@ export interface AdminUpsertPublicWorkInteractionConfigRequest {
publicWorkInteractions: PublicWorkInteractionConfigPayload[];
}
/** 图片画布生成模型泥点定价配置。 */
export type EditorGenerationPricingUnitPayload = 'perGeneration' | 'perSecond';
export interface EditorGenerationModelPricingPayload {
unit: EditorGenerationPricingUnitPayload;
price?: number;
prices?: Record<string, number>;
}
export interface EditorGenerationPricingConfigPayload {
models: Record<string, EditorGenerationModelPricingPayload>;
}
/** 后台统一创作工作台契约表单的传输结构。 */
export interface UnifiedCreationSpecPayload {
playId: string;
+7
View File
@@ -23,6 +23,7 @@ import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
@@ -235,6 +236,12 @@ export function AdminApp() {
onResultChange={setRechargeProductResult}
/>
) : null}
{routeId === 'editor-generation-pricing' ? (
<AdminEditorGenerationPricingPage
token={token}
onUnauthorized={handleUnauthorized}
/>
) : null}
</AdminShell>
);
}
+2
View File
@@ -1,6 +1,7 @@
import {
Bug,
BadgeDollarSign,
Coins,
LayoutDashboard,
LogOut,
Megaphone,
@@ -36,6 +37,7 @@ const routeIcons = {
invite: TicketCheck,
tasks: ListChecks,
'recharge-products': BadgeDollarSign,
'editor-generation-pricing': Coins,
'creation-announcement': Megaphone,
'creation-entry': SlidersHorizontal,
'work-visibility': Eye,
@@ -14,3 +14,17 @@ test('后台入口公告路由可通过导航和 hash 访问', () => {
);
expect(routeHash('creation-announcement')).toBe('#creation-announcement');
});
test('后台模型定价路由可通过导航和 hash 访问', () => {
expect(adminRoutes).toContainEqual({
id: 'editor-generation-pricing',
label: '模型定价',
hash: '#editor-generation-pricing',
});
expect(resolveAdminRoute('#editor-generation-pricing')).toBe(
'editor-generation-pricing',
);
expect(routeHash('editor-generation-pricing')).toBe(
'#editor-generation-pricing',
);
});
+2
View File
@@ -8,6 +8,7 @@ export type AdminRouteId =
| 'invite'
| 'tasks'
| 'recharge-products'
| 'editor-generation-pricing'
| 'creation-announcement'
| 'creation-entry'
| 'work-visibility';
@@ -28,6 +29,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
{id: 'invite', label: '邀请码', hash: '#invite'},
{id: 'tasks', label: '任务配置', hash: '#tasks'},
{id: 'recharge-products', label: '充值商品', hash: '#recharge-products'},
{id: 'editor-generation-pricing', label: '模型定价', hash: '#editor-generation-pricing'},
{id: 'creation-announcement', label: '入口公告', hash: '#creation-announcement'},
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
{id: 'work-visibility', label: '作品可见性', hash: '#work-visibility'},
@@ -0,0 +1,92 @@
/* @vitest-environment jsdom */
import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {beforeEach, expect, test, vi} from 'vitest';
import {
getAdminEditorGenerationPricing,
upsertAdminEditorGenerationPricing,
} from '../api/adminApiClient';
import type {EditorGenerationPricingConfigPayload} from '../api/adminApiTypes';
import {AdminEditorGenerationPricingPage} from './AdminEditorGenerationPricingPage';
vi.mock('../api/adminApiClient', () => ({
formatAdminApiError: vi.fn((error: unknown) =>
error instanceof Error ? error.message : '请求失败',
),
getAdminEditorGenerationPricing: vi.fn(),
isAdminApiError: vi.fn(() => false),
upsertAdminEditorGenerationPricing: vi.fn(),
}));
const pricing: EditorGenerationPricingConfigPayload = {
models: {
'gemini-3.1-flash-image-preview': {
unit: 'perGeneration',
prices: {'0.5K': 8, '1K': 12, '2K': 24},
},
'gpt-image-2': {
unit: 'perGeneration',
prices: {'1K': 3, '2K': 5},
},
'seedance2.0-fast': {
unit: 'perSecond',
prices: {'480p': 10, '720p': 20, '1080p': 40},
},
'seedance2.0': {
unit: 'perSecond',
prices: {'480p': 12, '720p': 24, '1080p': 48},
},
'audio1.0': {unit: 'perGeneration', price: 5},
'chirp-v5': {unit: 'perGeneration', price: 12},
},
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getAdminEditorGenerationPricing).mockResolvedValue(pricing);
vi.mocked(upsertAdminEditorGenerationPricing).mockResolvedValue({
...pricing,
models: {
...pricing.models,
'gpt-image-2': {
unit: 'perGeneration',
prices: {'1K': 20, '2K': 58},
},
},
});
});
test('模型定价后台按模型展示单位并保存尺寸定价', async () => {
const user = userEvent.setup();
render(
<AdminEditorGenerationPricingPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
expect((await screen.findAllByText('按次')).length).toBeGreaterThan(0);
expect(screen.getAllByText('按秒').length).toBeGreaterThan(0);
const gptImage2kInput = screen.getByLabelText('gpt-image-2 2K');
fireEvent.change(gptImage2kInput, {target: {value: '58'}});
await user.click(screen.getByRole('button', {name: '保存定价'}));
await user.click(screen.getByRole('button', {name: '确认'}));
await waitFor(() => {
expect(upsertAdminEditorGenerationPricing).toHaveBeenCalledWith(
'admin-token',
expect.objectContaining({
models: expect.objectContaining({
'gpt-image-2': expect.objectContaining({
unit: 'perGeneration',
prices: expect.objectContaining({
'2K': 58,
}),
}),
}),
}),
);
});
});
@@ -0,0 +1,224 @@
import {RefreshCcw, Save} from 'lucide-react';
import {FormEvent, useEffect, useState} from 'react';
import {
getAdminEditorGenerationPricing,
upsertAdminEditorGenerationPricing,
} from '../api/adminApiClient';
import type {
EditorGenerationModelPricingPayload,
EditorGenerationPricingConfigPayload,
EditorGenerationPricingUnitPayload,
} from '../api/adminApiTypes';
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
import {handlePageError} from './pageUtils';
interface AdminEditorGenerationPricingPageProps {
token: string;
onUnauthorized: (message?: string) => void;
}
const emptyPricing: EditorGenerationPricingConfigPayload = {
models: {},
};
const unitLabels: Record<EditorGenerationPricingUnitPayload, string> = {
perGeneration: '按次',
perSecond: '按秒',
};
export function AdminEditorGenerationPricingPage({
token,
onUnauthorized,
}: AdminEditorGenerationPricingPageProps) {
const [pricing, setPricing] =
useState<EditorGenerationPricingConfigPayload>(emptyPricing);
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
useEffect(() => {
void refreshPricing();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
async function refreshPricing() {
setIsLoading(true);
setErrorMessage('');
try {
setPricing(await getAdminEditorGenerationPricing(token));
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
}
async function handleSave(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isSaving) {
return;
}
setErrorMessage('');
const confirmed = await confirmWrite({
action: '保存模型定价',
target: '图片画布生成',
});
if (!confirmed) {
return;
}
setIsSaving(true);
try {
setPricing(await upsertAdminEditorGenerationPricing(token, pricing));
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsSaving(false);
}
}
function updateFlatPrice(model: string, value: string) {
setPricing((current) => {
const modelPricing = current.models[model];
if (!modelPricing) {
return current;
}
return {
...current,
models: {
...current.models,
[model]: {
...modelPricing,
price: parsePositiveInteger(value),
},
},
};
});
}
function updateTierPrice(model: string, tier: string, value: string) {
setPricing((current) => {
const modelPricing = current.models[model];
if (!modelPricing) {
return current;
}
return {
...current,
models: {
...current.models,
[model]: {
...modelPricing,
prices: {
...(modelPricing.prices ?? {}),
[tier]: parsePositiveInteger(value),
},
},
},
};
});
}
return (
<section className="admin-page admin-page-wide">
<div className="admin-page-heading">
<div>
<h2></h2>
<p></p>
</div>
<button
className="admin-secondary-button"
disabled={isLoading}
type="button"
onClick={refreshPricing}
>
<RefreshCcw size={17} aria-hidden="true" />
<span>{isLoading ? '刷新中' : '刷新'}</span>
</button>
</div>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
</div>
) : null}
<form className="admin-stack" onSubmit={handleSave}>
<div className="admin-pricing-model-list admin-pricing-model-list--single">
{Object.entries(pricing.models).map(([model, modelPricing]) =>
renderModelPricingCard({
model,
modelPricing,
onFlatChange: updateFlatPrice,
onTierChange: updateTierPrice,
}),
)}
</div>
<button className="admin-primary-button" disabled={isSaving} type="submit">
<Save size={17} aria-hidden="true" />
<span>{isSaving ? '保存中' : '保存定价'}</span>
</button>
</form>
{confirmDialog}
</section>
);
}
function renderModelPricingCard({
model,
modelPricing,
onFlatChange,
onTierChange,
}: {
model: string;
modelPricing: EditorGenerationModelPricingPayload;
onFlatChange: (model: string, value: string) => void;
onTierChange: (model: string, tier: string, value: string) => void;
}) {
const unitLabel = unitLabels[modelPricing.unit] ?? modelPricing.unit;
return (
<section className="admin-panel admin-pricing-model-card" key={model}>
<div className="admin-pricing-model-heading">
<strong>{model}</strong>
<span className="admin-pricing-unit">{unitLabel}</span>
</div>
{modelPricing.prices ? (
<div className="admin-form-row">
{Object.entries(modelPricing.prices).map(([tier, price]) => (
<label className="admin-field" key={`${model}-${tier}`}>
<span>{tier}</span>
<input
aria-label={`${model} ${tier}`}
min={1}
step={1}
type="number"
value={price}
onChange={(event) => onTierChange(model, tier, event.target.value)}
/>
</label>
))}
</div>
) : (
<label className="admin-field">
<span></span>
<input
aria-label={`${model} 泥点`}
min={1}
step={1}
type="number"
value={modelPricing.price ?? 0}
onChange={(event) => onFlatChange(model, event.target.value)}
/>
</label>
)}
</section>
);
}
function parsePositiveInteger(value: string) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
+44
View File
@@ -972,6 +972,49 @@ button:disabled {
box-shadow: 0 2px 8px rgba(112, 57, 30, 0.08);
}
.admin-pricing-grid,
.admin-pricing-field-list,
.admin-pricing-model-list,
.admin-pricing-model-card {
display: grid;
gap: 12px;
}
.admin-pricing-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.admin-pricing-model-card {
border: 1px solid #eaded2;
border-radius: 8px;
background: #fffdf9;
padding: 12px;
}
.admin-pricing-model-card strong {
color: #3d1f10;
overflow-wrap: anywhere;
}
.admin-pricing-model-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.admin-pricing-unit {
flex: 0 0 auto;
border-radius: 999px;
background: #f2e3d7;
color: #7b4a2f;
font-size: 12px;
font-weight: 800;
line-height: 1;
padding: 5px 8px;
}
.admin-bottom-nav {
display: none;
}
@@ -1007,6 +1050,7 @@ button:disabled {
.admin-overview-grid,
.admin-two-column,
.admin-two-column-wide,
.admin-pricing-grid,
.admin-form-row,
.admin-filter-grid,
.admin-table-query-grid,
+4 -1
View File
@@ -1,9 +1,10 @@
# 文档总览
`docs/` 现在按主题拆成了 6 类;旧后端路线文档开始聚合和删除,后续实现以 Rust / SpacetimeDB 当前基线为准。
`docs/` 现在按主题维护项目当前口径;旧后端路线文档开始聚合和删除,后续实现以 Rust / SpacetimeDB 当前基线为准。
## 快速入口
- [Agent 工作入口与执行准则](./%E3%80%90%E5%8D%8F%E4%BD%9C%E8%A7%84%E8%8C%83%E3%80%91Agent%E5%B7%A5%E4%BD%9C%E5%85%A5%E5%8F%A3%E4%B8%8E%E6%89%A7%E8%A1%8C%E5%87%86%E5%88%99-2026-06-22.md):复杂任务前的 Agent 阅读顺序、执行边界、技能路由、文档规则和验证口径。
- [经验沉淀](./experience/README.md):项目开发经验、UI 交接、历史实现经验。
- [审计与复盘](./audits/README.md):工程审查、文本/乱码审计、专项落地审计。
- [系统设计](./design/README.md):玩法、关系、物品与对话设计。
@@ -31,6 +32,8 @@ Expo React Native 移动壳和 Tauri 桌面壳的工程结构、同源 WebView
`/editor/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,第一版采用前端 JSZip 打包画布中有效图层引用的上传图、生成图和修改结果,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。
图片画布生成类面板的模型泥点默认 JSON、运行时 override、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
桌面端 `/creation` 创作工具主页、顶级“草稿”入口替换为“项目”、最近项目、新建项目和陶泥儿精选素材瀑布流的落地计划见 [【玩法创作】创作主页与项目入口改版计划-2026-06-18.md](./%E3%80%90%E7%8E%A9%E6%B3%95%E5%88%9B%E4%BD%9C%E3%80%91%E5%88%9B%E4%BD%9C%E4%B8%BB%E9%A1%B5%E4%B8%8E%E9%A1%B9%E7%9B%AE%E5%85%A5%E5%8F%A3%E6%94%B9%E7%89%88%E8%AE%A1%E5%88%92-2026-06-18.md)。
本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。
@@ -2838,13 +2838,33 @@
"EditorUiDesignAssetExtractionRequest": {
"type": "object",
"required": [
"sourceImageSrc"
"sourceImageSrc",
"aspectRatio",
"imageSize",
"priceMudPoints"
],
"properties": {
"sourceImageSrc": {
"type": "string",
"description": "UI 设计图 Data URL。"
},
"aspectRatio": {
"type": "string",
"const": "1:1",
"description": "UI 素材提取固定使用 1:1。"
},
"imageSize": {
"type": "string",
"enum": [
"1K",
"2K"
],
"description": "框选数量少时使用 1K,超过阈值时使用 2K。"
},
"priceMudPoints": {
"type": "integer",
"minimum": 0
},
"projectId": {
"type": [
"string",
+1 -1
View File
@@ -21,7 +21,7 @@ docs/project-memory/
## 使用原则
- 开发前先读 `AGENTS.md`,再按任务读取 `docs/project-memory/shared-memory/` 和当前 `docs/` 文档。
- 开发前先读 `AGENTS.md`;复杂任务继续读取 `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`,再按任务读取 `docs/project-memory/shared-memory/` 和当前 `docs/` 文档。
- 长期有效的架构约定、接口变化、排障经验、开发流程和协作规则写入 `shared-memory/`
- 阶段性计划写入 `plans/`,已确定但暂未实施的共享 TODO 写入 `todos/`
- 如果本目录内容与代码或最新 `docs/` 冲突,以代码和最新 `docs/` 为准,并同步修正过期记忆。
@@ -16,6 +16,30 @@
---
## 2026-06-22 编辑器生成模型默认定价调整
- 背景:图片画布生成按钮、后端 `priceMudPoints` 校验和后台定价页需要统一使用新的模型默认泥点。
- 决策:`audio1.0` 默认按次 `5` 泥点,`chirp-v5` 默认按次 `12` 泥点,`gpt-image-2` 默认 `1K=3``2K=5` 泥点;运行态仍允许后台 override 覆盖,前端兜底必须与后端默认 JSON 保持一致。
- 影响范围:`editor-generation-pricing.default.json``ImageCanvasGenerationModel.ts`、后台定价页 fixture、后端价格校验和编辑器定价文档。
- 验证方式:运行 `editor_generation_config`、公开定价路由、图标素材价格校验、图片画布定价模型和后台定价页相关测试。
- 关联文档:`docs/【编辑器】模型定价配置管理方案-2026-06-22.md``docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`
## 2026-06-22 AGENTS.md 收敛为入口导航
- 背景:`AGENTS.md` 同时承载项目记忆、RAG、Issue、UI、Git、后端、SpacetimeDB 和文档图谱等细则,入口过重,复杂任务启动成本高。
- 决策:`AGENTS.md` 只保留最高优先级规则、任务路由、后端红线、验证提交要求和文档图谱;新增 `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md` 承接完整执行细则。复杂任务阅读顺序固定为 `AGENTS.md` -> Agent 执行准则 -> `docs/project-memory/` -> `docs/README.md` 和专题文档。
- 影响范围:`AGENTS.md``docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md``docs/README.md``docs/project-memory/README.md` 和共享记忆索引。
- 验证方式:执行 `npm run check:encoding``git diff --check`,并检查入口文档不再重复承载专题细则。
- 关联文档:`AGENTS.md``docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`
## 2026-06-22 图片画布角色动作主媒体改为透明序列帧
- 背景:角色动作生成后端已经在视频生成后抽取透明 PNG 帧并完成绿幕去背;画板继续把 `previewVideoPath` 当主媒体会让用户看到未扣绿幕视频,下载也拿不到可直接用于游戏素材的帧序列。
- 决策:`/api/editor/character-animations/generations` 的上游预览视频继续保留为来源信息,但画板落层主类型固定为 `mediaType="image-sequence"``assetKind="character-animation"`;图层 `src` / `thumbnailSrc` 使用首帧,完整 `frames` 保存到 `imageSequenceFrames`,画布展示使用序列帧播放器循环播放。单图层下载生成序列帧 ZIP,画布素材 ZIP 中角色动作写入 `sequences/<编号-标题>/frames/`,不再把预览视频作为角色动作下载产物。
- 影响范围:图片画布角色动作生成、画布图层快照、序列帧播放器、素材导出、角色动作设计文档和排障记忆。
- 验证方式:运行角色动作图层工厂、画布展示、画布持久化、生成提交和素材导出相关前端测试,执行 `npm run typecheck``npm run check:encoding``git diff --check`
- 关联文档:`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`
## 2026-06-21 图片画布生成完成态由后端写入画布布局
- 背景:图片画布角色形象等长耗时生成在服务端完成后,如果浏览器已刷新或原 HTTP 回调丢失,前端无法再把生成结果图层和 `generation-dialog` 完成态写回 `editor_canvas.layers_json`,用户会继续看到“生成中”卡片。
@@ -51,15 +75,15 @@
## 2026-06-19 图片画布生成按钮价格统一绑定模型定价配置
- 背景:图片画布的生成图片、生成视频、生成规范、生成角色、生成素材、生成 UI、宣发素材、快速编辑、重绘和音频生成入口都在按钮内显示泥点;如果按钮文案、前端提交和后端校验各自写固定数值,后续调整模型价格会出现展示价、提交价和扣费价不一致。
- 决策:所有画板生成按钮价格必须从 `src/components/image-editor/ImageCanvasGenerationModel.ts` 的模型定价配置函数计算;需要提交 `priceMudPoints` 的视频、角色动、图标素材、音效和背景音乐也使用同一函数。后端 `server-rs/crates/api-server/src/editor_generation_config.rs` 的同名语义配置重新计算并校验 / 扣费。当前正式模型均已覆盖定价:图片类 `nanobanana2`(真实模型 `gemini-3.1-flash-image-preview`)为 12`gpt-image-2` 为 20;生成规范固定 `gpt-image-2` 为 5;视频按模型和清晰度分档,`seedance2.0-fast` 为 480p 每秒 10 / 720p 每秒 20`seedance2.0` 为 12 / 24`kling3.0` 为 15 / 30`kling3.0-omni` 为 20 / 40Veo 旧布局兼容价仍为 10 / 20。画板 UI 统一显示 `nanobanana2`,历史输入或旧布局中的 `nano-banana` 必须归一到真实模型 ID 后再提交和计费。
- 影响范围:图片画布生成类面板、生成提交模型、编辑器图片 / 视频 / 音频 BFF、`editor_generation_config` 和 Lovart 生成类面板文档。
- 验证方式:运行 `npm run test -- src/components/image-editor/ImageCanvasGenerationModel.test.ts src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`,并执行 `npm run typecheck``npm run check:encoding``git diff --check`
- 关联文档:`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`
- 决策:所有画板生成按钮价格必须从 `src/components/image-editor/ImageCanvasGenerationModel.ts` 的模型定价配置函数计算;需要提交 `priceMudPoints` 的视频、角色动、图标素材、音效和背景音乐也使用同一函数。后端默认配置独立放在 `server-rs/crates/api-server/config/editor-generation-pricing.default.json`,运行时 override 默认写入 `.app/editor-generation-pricing.override.json`,可用 `GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH` 指定可写路径;后台“模型定价”通过 `/admin/api/editor-generation-pricing` 读取和保存完整 `models` 配置,主站通过 `/api/editor/generation-pricing` 动态下发。后端提交校验 / 扣费仍以 `AppState` 当前运行时配置为准,前端内置定价只作为接口失败兜底。模型定价不再按图片 / 规范、视频 / 动作用途拆分,只按模型区分:图片模型按尺寸单次计价,`gemini-3.1-flash-image-preview` 必须配置 `0.5K / 1K / 2K``gpt-image-2` 必须配置 `1K / 2K`规范固定读取 `gpt-image-2` `2K`;视频和角色动作共用视频模型分辨率每秒价格,角色动作仍固定 `seedance2.0-fast`。后台管理页必须显示定价单位“按次 / 按秒”。画板 UI 统一显示 `nanobanana2`,历史输入或旧布局中的 `nano-banana` 必须归一到真实模型 ID 后再提交和计费。
- 影响范围:图片画布生成类面板、生成提交模型、编辑器图片 / 视频 / 音频 BFF、`editor_generation_config`、后台管理端和 Lovart 生成类面板文档。
- 验证方式:运行 `cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_generation_config::tests editor_generation_pricing_route -- --nocapture``npx vitest run src/components/image-editor/ImageCanvasGenerationModel.test.ts src/services/image-editor/editorProjectClient.test.ts apps/admin-web/src/pages/AdminEditorGenerationPricingPage.test.tsx apps/admin-web/src/app/adminRoutes.test.ts --reporter verbose``npm run admin-web:typecheck``npm run check:encoding``git diff --check`
- 关联文档:`docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md``docs/【编辑器】模型定价配置管理方案-2026-06-22.md`
## 2026-06-18 图片画布 UI 设计图提取素材保留图集
- 背景:UI 设计图需要从成图中继续抽取可复用独立素材;原图标素材生成只把拆分后的图标放入画布,spritesheet 原图没有保留,后续追溯和二次切图不方便。
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`固定调用 `/api/editor/ui-designs/assets/extractions`后端固定 `gpt-image-2` 和提示词 `提取画面中的所有独立并整理成spritesheet`,返回结构复用图标 spritesheet 响应。图标生成与 UI 提取都必须把 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分后的 `assetKind="icon"` 素材。
- 决策:`assetKind="ui-design"` 图层浮动工具栏新增 `提取素材`点击后先进入红框素材框选编辑态,默认矩形框选,并支持椭圆框选和画笔自由框选。至少存在一个框选区域后才能提交;前端把红色轮廓绘入原 UI 设计图并将合成图作为 `/api/editor/ui-designs/assets/extractions` 的参考图。后端固定 `gpt-image-2` 和提示词 `提取被红色框框选的素材并整理成spritesheet`,返回结构复用图标 spritesheet 响应。图标生成与 UI 提取都必须把 spritesheet 图集作为 `assetKind="icon-spritesheet"` 图层放到画布,再放拆分后的 `assetKind="icon"` 素材。
- 影响范围:图片画布浮动工具栏、编辑器图片生成 BFF、`platform-image` 图集连通域拆分、画布图层类型和编辑器文档。
- 验证方式:运行图片画布工具栏 / 图集落层 / 生成提交相关前端测试,`cargo test -p platform-image generated_asset_sheets --manifest-path server-rs/Cargo.toml`,以及 `cargo test -p api-server editor_ui_design_asset_extraction_prompt_is_fixed --manifest-path server-rs/Cargo.toml`
- 关联文档:`docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md``docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`
@@ -5,7 +5,7 @@
## 标准任务流程
```text
同步代码 → 读取 AGENTS.md → 读取 docs/project-memory/shared-memory → 查找/完善 docs → 制定计划 → 小步实现 → 本地验证 → 更新文档/记忆 → 提交
同步代码 → 读取 AGENTS.md → 复杂任务读取 Agent 执行准则 → 读取 docs/project-memory/shared-memory → 查找/完善 docs → 制定计划 → 小步实现 → 本地验证 → 更新文档/记忆 → 提交
```
## 建议启动方式
@@ -30,6 +30,7 @@ hermes
- [ ] 当前分支是否正确
- [ ] 是否已拉取最新代码
- [ ] 是否阅读 `AGENTS.md`
- [ ] 复杂任务是否阅读 `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`
- [ ] 是否阅读 `docs/project-memory/shared-memory/` 相关文件
- [ ] 是否阅读 `README.md` 中的运行和检查命令
- [ ] 是否阅读 `docs/README.md` 及任务相关分类 README
@@ -319,7 +320,7 @@ npm run check:server-rs-ddd
```text
请检查当前 git diff,指出:
1. 是否违反 AGENTS.md 或 docs/project-memory/shared-memory 约定;
1. 是否违反 AGENTS.md、Agent 执行准则或 docs/project-memory/shared-memory 约定;
2. 是否需要补充 docs
3. 是否有长期知识需要写入 docs/project-memory/shared-memory
4. 建议的测试命令和提交信息。
@@ -1,12 +1,13 @@
# 文档地图与阅读索引
更新时间:`2026-05-15`
更新时间:`2026-06-22`
## 当前文档入口
| 场景 | 优先阅读 |
| --- | --- |
| 建立项目背景 | `README.md``AGENTS.md``docs/project-memory/shared-memory/project-overview.md` |
| Agent 复杂任务执行规则 | `AGENTS.md``docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md` |
| 找当前文档 | `docs/README.md` |
| 产品、命名、UI、协作和废弃路线 | `docs/【项目基线】当前产品与工程约束-2026-05-15.md` |
| 后端、DDD、API、SpacetimeDB schema 和表目录 | `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` |
@@ -16,15 +17,17 @@
| 本地启动、验证、部署、埋点和运营查询 | `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` |
| 微信小程序虚拟支付 | `docs/【技术方案】微信虚拟支付接入-2026-05-26.md` |
| UI 像素资产与 9-slice 规范 | `UI_CODING_STANDARD.md` |
| 图片画布生成面板与模型定价 | `docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md``docs/【编辑器】模型定价配置管理方案-2026-06-22.md` |
## 阅读顺序
通用复杂任务:
1. `AGENTS.md`
2. `docs/project-memory/shared-memory/`
3. `docs/README.md`
4. 与任务匹配的当前融合文档
2. `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`
3. `docs/project-memory/shared-memory/`
4. `docs/README.md`
5. 与任务匹配的当前融合文档
后端 / 数据真相 / SpacetimeDB
@@ -48,6 +51,7 @@
## 维护规则
- 当前 `docs/` 只保留少量融合文档。
- `AGENTS.md` 只保留最高优先级入口;Agent 执行细则优先沉到 `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md` 或对应专题文档。
- 新增工程实现时,如果已有对应当前文档,必须同步更新。
- 如果没有合适位置,新文档文件名必须使用 `【标签名】中文标题-YYYY-MM-DD.md`
- 阶段性流水账、一次性修复记录和已关闭实验不要再新增为长期文档。
+21 -6
View File
@@ -15,6 +15,14 @@
- 关联:相关文件、文档、提交或 Issue
```
## 本地 dev 启动日志先看成功锚点,不要把非阻断 warning 当失败
- 现象:`npm run dev` 启动 SpacetimeDB 时可能先打印 `static max level is off``Skipping tokio metrics`,或 SpacetimeDB CLI 提示存在新版本 / 当前版本较旧,看起来像启动异常。
- 原因:这些是 tracing、metrics 或 CLI 更新提示,不代表本地 dev 栈失败;同一段日志后续仍可能已经完成 `SpacetimeDB listening on 127.0.0.1:3101`、模块 publish、`api-server` `/healthz` 200、主站 Vite `3000` 和后台 Vite `3102` ready。
- 处理:排查本地 dev 栈时先确认成功锚点:`[dev:spacetime] actual``Updated database``api-server 已完成 tracing 初始化并开始监听``/healthz` 200、两个 Vite `ready`。只有缺少这些锚点或进程退出时,再继续查 CLI 权限、端口占用、publish 或 API 编译问题。
- 验证:`http://127.0.0.1:3101/v1/ping` 可访问、`http://127.0.0.1:8082/healthz` 返回 200、`http://127.0.0.1:3000/``http://127.0.0.1:3102/admin/` 可打开。
- 关联:`scripts/dev.mjs``.app/dev-stack.json``docs/project-memory/shared-memory/development-workflow.md`
## API Build / Deploy 归档清单不能漏掉随包 Pingora 脚本
- 现象:`Genarrative-Api-Deploy` 在发布阶段报 `发布产物缺少 Pingora TLS 证书同步脚本: build/<version>/scripts/deploy/pingora-tls-cert-sync.mjs`
@@ -23,12 +31,12 @@
- 验证:运行 `npm run check:production-ops``npm run check:production-api-release``npm run check:production-api-deploy`,确认构建包、Jenkins 归档链路和 deploy fail-fast 检查口径一致。
- 关联:`jenkins/Jenkinsfile.production-api-build``jenkins/Jenkinsfile.production-api-deploy``scripts/deploy/production-api-deploy.sh``scripts/check-production-ops-guardrails.mjs`
## 图片画布角色动作结果不能把首帧当主媒体
## 图片画布角色动作结果主类型是序列帧
- 现象:画板 `生成角色动作` 返回后显示成一张不可播放图片,点击下载或素材 ZIP 导出时拿到的也是 PNG,而不是动作视频
- 原因:后端已经生成 `previewVideoPath`,并继续执行抽帧、绿幕去背和帧素材 OSS 落盘;前端落层时却把 `frames[0].imageSrc` 当作图层主 `src`,且没有设置 `mediaType: "video"`,导致预览和导出都按图片处理
- 处理:角色动作结果图层主 `src` 必须使用 `previewVideoPath``mediaType` 固定为 `video``assetKind` 固定为 `character-animation`;首帧透明 PNG 只写入 `thumbnailSrc`,用于 `<video poster>` 和后续动作素材二次生成源。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。
- 验证:`ImageCanvasGenerationLayerModel` 应断言动作结果 `src`预览视频且 `thumbnailSrc` 为首帧;画布集成测试应出现 `<video controls poster=...>`;导出模型应按 `mediaType="video"` 输出 mp4/webm/mov 等真实视频扩展名
- 现象:产品要求画板 `生成角色动作` 返回后按透明序列帧播放和下载,但旧实现或旧测试可能继续把结果当作预览视频处理
- 原因:后端仍需要先生成 `previewVideoPath`抽帧、绿幕去背和 OSS;如果前端把预览视频当主媒体,就会绕过已经扣绿幕的 PNG 帧,也无法按序列帧打包下载
- 处理:角色动作结果图层主 `src` 使用 `frames[0].imageSrc``mediaType` 固定为 `image-sequence``assetKind` 固定为 `character-animation`,完整帧列表写入 `imageSequenceFrames``previewVideoPath` 只作为来源信息保留。单图层下载必须生成序列帧 ZIP;画布素材 ZIP 中角色动作写入 `sequences/<编号-标题>/frames/`。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。
- 验证:`ImageCanvasGenerationLayerModel` 应断言动作结果 `src`首帧且 `mediaType="image-sequence"`;画布集成测试应出现 `画布序列帧:角色动作` 图片播放器,不应出现角色动作 `<video>`;导出测试应断言角色动作下载和画布素材导出都包含序列帧 ZIP / frames 目录
- 关联:`src/components/image-editor/ImageCanvasGenerationLayerModel.ts``src/components/image-editor/ImageCanvasWorldView.tsx``src/components/image-editor/ImageCanvasExportModel.ts``server-rs/crates/api-server/src/character_animation_assets.rs``docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`
## Vidu 文生音频线上网关可能要求 sound 字段
@@ -103,6 +111,14 @@
- 验证:`cargo test -p api-server editor_character_animation --manifest-path server-rs/Cargo.toml`,其中 `editor_character_animation_extracts_final_sample_from_short_video` 应覆盖本机 FFmpeg 8 的 0 帧回归。
- 关联:`server-rs/crates/api-server/src/character_animation_assets.rs``docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`
## Windows 本地角色动画抽帧找不到 ffmpeg 先查 dev 子进程环境
- 现象:画板角色动画抽帧报 `抽取动作视频帧失败:无法启动进程 ffmpegprogram not found(requestId:...)`,但新开的 PowerShell 里 `ffmpeg -version` 正常。
- 原因:长期运行的 `api-server` 可能是在安装 FFmpeg 或更新用户 Path 之前启动的,子进程不会自动继承后续写入的用户环境变量。
- 处理:Windows 本地默认把 FFmpeg 安装到 `%LOCALAPPDATA%\Genarrative\ffmpeg\bin`,并确保用户 Path 包含该目录;`npm run dev` / `npm run dev:api-server` 会在启动 `api-server` 时自动注入该目录和 `CHARACTER_ANIMATION_FFMPEG_PATH` / `CHARACTER_ANIMATION_FFPROBE_PATH` 绝对路径。修复后需要重启 `api-server`,不能只刷新浏览器。
- 验证:`where ffmpeg``where ffprobe` 能找到本地安装;`npm run test -- scripts/dev.test.ts -t "FFmpeg"`;重启 `npm run dev:api-server` 后访问 `/healthz`
- 关联:`scripts/dev.mjs``server-rs/crates/api-server/src/config.rs``server-rs/crates/api-server/src/character_animation_assets.rs`
## 图片编辑器生成长请求完成态必须由后端写入画布
- 现象:画板角色形象等生成请求已经在服务端返回 `200`OSS 中也已有 `generated-character-drafts/.../image.png`,但用户刷新或页面重载后仍看到旧生成卡片停在“生成中”。
@@ -158,7 +174,6 @@
- 处理:Windows 本地 dev 脚本应把 `RUSTC_WRAPPER``CARGO_BUILD_RUSTC_WRAPPER` 显式设为空字符串,让 Cargo 覆盖项目配置并直连真实 rustc;Linux 保持 `/usr/bin/env` 绕过 sccache。
- 验证:`npm run test -- scripts/dev.test.ts -t "Windows 下本地 dev Rust env 用空 wrapper 覆盖项目 sccache"`,并用 `npm run dev:api-server` 拉起后访问实际 api 端口的 `/healthz` 返回 200。
- 关联:`scripts/dev.mjs``scripts/dev.test.ts``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## Pingora 直连 80/443 不能只改 env
- 现象:`/etc/genarrative/pingora-gateway.env` 已把 `GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN` / `HTTP_REDIRECT_LISTEN` 改到 `0.0.0.0:443` / `0.0.0.0:80`,但 `genarrative-pingora-gateway.service` 启动失败,日志出现低端口绑定权限错误。
@@ -51,6 +51,7 @@ server-rs + Axum + SpacetimeDB
## 当前文档入口
- `docs/README.md`
- `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`
- `docs/【项目基线】当前产品与工程约束-2026-05-15.md`
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
@@ -19,6 +19,7 @@
- `.hermes/skills/` Hermes 专用仓库级 skills
- `docs/` 中 PRD、设计、技术、经验、审计、查询手册
- `AGENTS.md` 项目级 Agent 约束
- `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md` Agent 执行细则
禁止提交:
@@ -33,10 +34,11 @@
1. 拉取最新代码。
2. 阅读 `AGENTS.md`
3. 阅读 `docs/project-memory/shared-memory/` 中与任务相关的文件
4. 阅读 `docs/README.md` 任务相关分类 README
5. 阅读对应 PRD、设计、技术、经验或审计文档
6. 如果文档不足以指导编码,先补充或修正文档。
3. 复杂任务阅读 `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`
4. 阅读 `docs/project-memory/shared-memory/` 中与任务相关的文件
5. 阅读 `docs/README.md` 和任务相关分类 README
6. 阅读对应 PRD、设计、技术、经验或审计文档。
7. 如果文档不足以指导编码,先补充或修正文档。
## 开发中
@@ -64,21 +66,16 @@
1. `README.md`
2. `AGENTS.md`
3. `docs/project-memory/shared-memory/`
4. `docs/README.md`
5. `docs/experience/README.md`
6. `docs/audits/README.md`
7. 任务所属分类:`docs/design/``docs/technical/``docs/planning/``docs/prd/``docs/reference/``docs/tracking/``docs/operations/`
3. `docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`
4. `docs/project-memory/shared-memory/`
5. `docs/README.md`
6. 任务所属分类:`docs/design/``docs/technical/``docs/planning/``docs/prd/``docs/reference/``docs/tracking/``docs/operations/`
后端任务建议:
1. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
2. `docs/technical/SERVER_RS_DDD_FULL_REFACTOR_2026-04-28.md`
3. `docs/technical/SERVER_RS_DDD_G1_CONTRACT_AND_ROUTE_MATRIX_2026-04-29.md`
4. `docs/technical/SERVER_RS_DDD_PARALLEL_TASKLIST_2026-04-29.md`
5. `docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`
6. `docs/technical/SPACETIMEDB_TABLE_CATALOG.md`
7. `docs/technical/MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md`
1. `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
2. `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
3. 任务相关 crate README、源码和当前专题文档
## 共享记忆更新准则
@@ -22,6 +22,7 @@
- 导出当前画布中有效图层引用的图片。
- 包含上传图、生成图、修改生成结果。
- 角色动作 `image-sequence` 图层导出完整透明 PNG 序列帧。
- 默认包含隐藏图层。
- 跳过已被素材库删除且已判定无效的上传图层。
- 锁定、分组、翻转等状态不影响图片文件导出,但写入元数据。
@@ -50,6 +51,13 @@
│ ├─ 001-拼图素材.png
│ ├─ 002-生成图片.png
│ └─ 003-修改结果.png
├─ sequences/
│ └─ 004-角色动作/
│ ├─ frames/
│ │ ├─ frame-01.png
│ │ └─ frame-02.png
│ ├─ metadata.json
│ └─ manifest.txt
├─ metadata.json
└─ manifest.txt
```
@@ -120,10 +128,11 @@ assetObjectId > objectKey > sourceAssetId > src
1. 从当前 `layers` 取目标图层。
2. 过滤无效图层,保留隐藏图层。
3. 按去重 key 合并图片源。
4. 对每个图片源读取 Blob
3. 按去重 key 合并图片源或序列帧源
4. 对每个素材源读取 Blob
- `data:image/...` 直接转换为 Blob。
- 同源或可访问 URL 使用 `fetch` 拉取 Blob。
- `mediaType="image-sequence"` 逐帧读取 `imageSequenceFrames`,写入 `sequences/<编号-标题>/frames/`
- 拉取失败时记录失败项,不中断整个导出。
5. 使用 `JSZip` 写入 `images/``metadata.json``manifest.txt`
6. `zip.generateAsync({ type: 'blob' })` 生成文件。
File diff suppressed because one or more lines are too long
@@ -0,0 +1,116 @@
# Agent 工作入口与执行准则
更新时间:`2026-06-22`
## 文档定位
本文件承接 `AGENTS.md` 中不适合塞在入口页里的执行细则。`AGENTS.md` 是最高优先级入口,本文件是复杂任务的默认操作手册;如果本文件与代码、最新 `docs/` 或项目共享记忆冲突,以当前代码和最新 `docs/` 为准,并同步修正文档。
## 阅读顺序
简单自包含任务可以直接执行。复杂开发、跨模块修改、后端 / UI / 文档体系调整前,按这个顺序读:
1. `AGENTS.md`
2. 本文件
3. `docs/project-memory/README.md`
4. `docs/project-memory/shared-memory/project-overview.md`
5. `docs/project-memory/shared-memory/team-conventions.md`
6. `docs/project-memory/shared-memory/development-workflow.md`
7. 与任务相关的 `decision-log.md``pitfalls.md``docs/README.md` 和专题文档
如果已有文档不能精确指导字段、契约、页面状态、资产链路、迁移或验收命令,先补文档再编码。
## 信息来源边界
- `docs/`:当前 PRD、架构、开发运维、设计和测试口径。
- `docs/project-memory/shared-memory/`:长期团队记忆、决策、流程和踩坑摘要。
- `.hermes/`:Hermes 工具资源,不作为项目知识库。
- `.codex/skills/`:Codex 可复用技能;只在任务命中时读取。
- `scripts/rag/`:Agent 本地检索入口,只提供候选上下文。
RAG 默认不安装运行时依赖,也不把 LanceDB、Transformers.js 或本地 embedding 模型写入根 `package.json`。需要启用时,先询问用户;用户确认后只安装到 gitignored 的 `.rag/runtime/`,模型缓存和向量库留在 `.rag/`
## 执行风格
- 修改范围保持聚焦,不做无关重构。
- 优先复用现有系统、页面、组件、脚本、DTO 和文档位置。
- 不新增平行入口、平行作品架、平行公开列表、平行业务真相或临时兼容层。
- 不把前端临时状态当正式业务事实;正式状态以后端投影、后端 API 或当前架构文档为准。
- 涉及中文内容时保持中文,不擅自翻译成英文。
- 发现中文乱码时先确认真实编码,不直接沿用乱码,也不用英文替换。
- 含中文文件优先局部补丁;非必要不要整文件重写。
- 阶段性大任务完成后,整理当前上下文、剩余风险和下一步入口,降低后续接手噪音。
## 文档规则
- 工程修改必须同步更新对应 `docs/` 文档。
- 没有合适文档时,新文档放入 `docs/` 下合适位置,文件名使用 `【标签名】中文标题-日期.md`
- PRD 或技术方案要具体到能指导编码,不写会导致落地漂移的泛泛描述。
- 长期有效的架构约定、接口变化、排障经验、开发流程或协作规则写入 `docs/project-memory/shared-memory/`
- 阶段性计划放入 `docs/project-memory/plans/`;确定但未实施的共享 TODO 放入 `docs/project-memory/todos/`
- 不提交个人配置、密钥、Token、Cookie、会话记录、认证文件、本地私密路径、构建产物、日志、缓存和数据库 dump。
## UI 与前端规则
- UI 面板保持清爽,不默认写功能说明、规则说明、键盘快捷键说明或开发解释文本。
- 移动端优先,同时保证桌面端体验完整。
- 弹出独立面板的交互使用弹窗、抽屉、popover 或页面级 portal,不在当前面板下面追加内容。
- 页面展示以后端返回状态为准,不在前端自行计算结论型业务状态。
- 创作入口事实源来自 SpacetimeDB,经 `/api/creation-entry/config` 下发;前端只做展示派生。
- 优先扩展现有公共组件,例如平台弹窗、图片输入、媒体预览、状态提示和动作按钮,不在业务页复制通用逻辑。
## 后端与数据真相
- 后端路线固定为 `server-rs + Axum + SpacetimeDB`
-`server-node`、Express、PostgreSQL、Go 服务端、`maincloud` 相关脚本、环境变量、测试和文档要求均为历史残留。
- 领域规则沉到 `module-*`SpacetimeDB 表、reducer、procedure、事务 adapter 和 row mapper 留在 `spacetime-module`
- 后端访问 SpacetimeDB 统一经 `spacetime-client` facade。
- HTTP / SSE / BFF 和外部副作用编排留在 `api-server`;OSS、LLM、认证、语音等外部平台能力留在 `platform-*`
- 前后端 DTO 和公开契约留在 `shared-contracts` / `packages/shared`
- 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`api-server/src/app.rs``shared-contracts``packages/shared` 为准。
后端修改后按当前 DDD 文档执行验收。涉及 API smoke 时,使用 `npm run dev:api-server` 重新拉起后端并检查 `/healthz`;不要使用旧 `maincloud` 启动口径。
## SpacetimeDB 规则
涉及 SpacetimeDB 设计、实现、脚本、调试、发布、绑定生成、schema、reducer、procedure、view 或 Rust API 时,先读取对应 skill
- `.codex/skills/spacetimedb-cli/SKILL.md`
- `.codex/skills/spacetimedb-rust/SKILL.md`
- `.codex/skills/spacetimedb-concepts/SKILL.md`
已有表新增字段时,字段必须放在 Rust 表结构体最后,并设置明确默认值。删除、改名、重排或改类型前必须先询问用户并确认迁移计划。
修改 schema 后必须同步:
- `server-rs/crates/spacetime-module/src/migration.rs`
- 表目录 / 数据契约文档
- 生成绑定
- `npm run check:spacetime-schema`
人工命令、本地联调、排障步骤和文档示例禁止继续使用 `spacetime --root-dir`;本地数据隔离使用项目脚本或 `--data-dir`,发布目标显式传 `--server` / `--server-url`
## 技能路由
- 新增、补齐、迁移或重构玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model:读取 `.codex/skills/genarrative-play-type-integration/SKILL.md`
- 本地 dev 端口、代理目标、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理和后台 dev 串联:读取 `.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`
- 仓库级 Hermes skills/plugins:先读 `.hermes/README.md`,只把 `.hermes/` 当工具目录。
## Issue 与提交
- Issue 使用自托管 Gitea;优先使用 Gitea UI/API 或 `tea` CLI。
- 默认 triage 标签:`needs-triage``needs-info``ready-for-agent``ready-for-human``wontfix`
- 提交标题必须使用中文;标题后逐行写明本次提交修改了什么,每条变更单独一行。
- 提交前检查 staged diff,避免把无关文件、密钥、本地配置或用户未要求的改动带进去。
## 默认验证
按修改范围选择验证,不追求无意义全量扫:
- 文档 / 中文文本:`npm run check:encoding``git diff --check`
- 前端:定向测试、`npm run typecheck`、必要的页面交互 smoke 和移动端视口检查
- 后端:对应 crate 的 `cargo test` / `cargo check`、API smoke、`/healthz`
- SpacetimeDB schema`npm run check:spacetime-schema`
- 发布 / 运维:当前开发运维文档中的脚本门禁、host 侧进程和公开端点验证
如果无法运行某项验证,最终说明要写清原因、风险和已经完成的替代检查。
@@ -51,6 +51,8 @@ Linux 本机多用户并发开发时,`npm run dev` 和 `npm run dev:*` 单模
Windows 本地 `npm run dev` / `npm run dev:api-server` 会用空的 `RUSTC_WRAPPER` / `CARGO_BUILD_RUSTC_WRAPPER` 覆盖 `server-rs/.cargo/config.toml` 里的 `sccache`,从而直连真实 `rustc`。不要把 wrapper 绕过值写成 `rustc`Cargo 会按 wrapper 协议调用 `rustc <真实rustc路径> - ...`,最终报 `multiple input filenames provided` 并导致 api-server 无法启动。排查本地启动失败时,先看 dev 日志是否出现该错误,再确认脚本注入的 wrapper 为空。
Windows 本地如果已在 `%LOCALAPPDATA%\Genarrative\ffmpeg\bin` 安装 FFmpeg`npm run dev` / `npm run dev:api-server` 会自动把该目录加入本次 `api-server` 子进程 `Path`,并注入 `CHARACTER_ANIMATION_FFMPEG_PATH` / `CHARACTER_ANIMATION_FFPROBE_PATH` 的绝对路径。这样即使外层终端或长期运行的 dev 进程是在安装 FFmpeg 之前启动,角色动画抽帧也不会继续因为 `ffmpeg: program not found` 失败;若手动配置了上述环境变量或 `GENARRATIVE_CHARACTER_ANIMATION_*` 前缀变量,显式配置优先。
开发态 `npm run dev``npm run dev:api-server` 会默认注入 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true`,因此密码登录在本地开发环境可直接注册未知手机号账号;生产环境仍按 `api-server` 配置默认关闭该开关。
本地 `npm run dev``npm run dev:api-server` 默认保留 inline 开发体验:未显式设置 `GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 时,外部生成 handler 会同步复用 worker executor,完成后返回 `completed`,便于快速确认 provider、OSS 和 SpacetimeDB 写回链路。inline 不创建 `external_generation_job`,也不能验证 worker lease、队列等待展示或动态扩缩容。
@@ -0,0 +1,78 @@
# 编辑器模型定价配置管理方案
## 背景
图片画布的生成图片、生成规范、生成角色、生成素材、生成 UI、生成视频、角色动作、音效和背景音乐都需要在生成按钮旁展示泥点消耗,并在后端提交时校验 `priceMudPoints`。定价不能散落在前端组件和具体 handler 中,也不能按“图片 / 规范”“视频 / 动作”等用途拆出不同价格事实源。
## 配置来源
- 默认配置文件:`server-rs/crates/api-server/config/editor-generation-pricing.default.json`
- 运行时覆盖文件:默认 `.app/editor-generation-pricing.override.json`
- 生产或特殊环境可通过 `GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH` 指定可写覆盖文件路径。
默认文件进入 Git,作为空 override 或 override 丢失时的兜底。覆盖文件属于运行态配置,不提交 Git。
## 配置结构
定价只按模型区分,不按用途区分。同一个模型用于多个入口时必须读取同一条模型配置:
- `gpt-image-2` 同时用于普通图片、规范、UI 设计等图片类入口。
- `seedance2.0-fast` 同时用于生成视频和角色动作;角色动作入口仍只允许该模型。
```json
{
"models": {
"gemini-3.1-flash-image-preview": {
"unit": "perGeneration",
"prices": { "0.5K": 8, "1K": 12, "2K": 24 }
},
"gpt-image-2": {
"unit": "perGeneration",
"prices": { "1K": 3, "2K": 5 }
},
"seedance2.0-fast": {
"unit": "perSecond",
"prices": { "480p": 10, "720p": 20, "1080p": 40 }
},
"audio1.0": { "unit": "perGeneration", "price": 5 },
"chirp-v5": { "unit": "perGeneration", "price": 12 }
}
}
```
字段规则:
- `unit`:定价单位,`perGeneration` 表示按次,`perSecond` 表示按秒。
- `price`:单一价格,适合音效、背景音乐等单次生成模型。
- `prices`:档位价格,图片模型按尺寸档位配置,视频模型按分辨率配置。
- 生图模型必须补齐支持尺寸:`gemini-3.1-flash-image-preview``0.5K / 1K / 2K``gpt-image-2``1K / 2K`
后端保存前校验当前正式模型、必要尺寸和必要分辨率都存在且大于 0。
## 后端契约
- `GET /api/editor/generation-pricing`:主站读取当前模型定价。
- `GET /admin/api/editor-generation-pricing`:后台读取当前模型定价。
- `POST /admin/api/editor-generation-pricing`:后台保存完整模型定价,并写入 override 文件。
后端 `AppState` 启动时加载默认配置和 override。提交视频、角色动作、图标素材、音效和背景音乐时继续以运行时配置重新计算价格,前端传入的 `priceMudPoints` 只作为一致性校验值。普通图片生成和宣发素材扣费也读取同一运行时模型配置。
## 管理端
后台“模型定价”页面按模型统一展示和编辑,不再拆“图片模型 / 规范模型 / 视频模型 / 角色动作”等用途分组。每个模型卡片显示:
- 模型名。
- 定价单位:按次 / 按秒。
- 单价或档位价格;生图模型展示 `0.5K / 1K / 2K` 等尺寸档位,视频模型展示 `480p / 720p / 1080p` 等分辨率档位。
## 前端展示
主站画板启动后调用 `GET /api/editor/generation-pricing`,成功后覆盖前端内置兜底价格并触发重渲染。接口失败时保留内置兜底,避免画板不可用。图片类价格计算必须传入当前模型和 `imageSize`;规范生成固定读取 `gpt-image-2``2K` 定价。
后续新增模型时必须先补默认 JSON、后端校验、前端兜底和测试,再暴露到模型选择框。
## 验证
- 后端配置解析、override、路由保存与公开读取测试。
- 前端价格读取、运行时覆盖、图片尺寸档位计算测试。
- 管理端模型定价页面单位展示和档位保存测试。
@@ -32,7 +32,7 @@
3. 角色规范参考图组件只展示一个方形参考图图标;图标内上方是规范 logo,下方固定短标 `角色规范`,不再展示绑定状态、来源说明或长标题。已有绑定图片的原始标题只作为可访问名称、悬浮 title 和图片信息回看使用。
4. 参考图图标尽量少文字;必要文字写在图标块内或短标签内,不写规则说明。
5. 已有参考图在鼠标悬停或键盘聚焦到对应参考图槽位时,右上角显示一个 `×` 删除按钮;鼠标不在槽位上时不显示。点击 `×` 只移除该参考图绑定,不触发来源菜单、不删除画布图片。
6. 普通参考图支持连续追加:已有图缩略图后始终保留一个 `+` 入口。点击入口只弹出“从画布中选择 / 上传图片”来源选项,不再直接打开系统文件选择器;生成图片、生成视频、角色常规参考图等同类参考图入口都遵循同一交互。
6. 普通参考图支持连续追加:已有图缩略图后始终保留一个 `+` 入口。点击入口只弹出“从画布中选择 / 上传图片”来源选项,不再直接打开系统文件选择器;生成图片、生成视频、角色常规参考图等同类参考图入口都遵循同一交互。角色规范和图标规范引用在浏览器本地分别缓存最近一次成功绑定的对象;再次新建角色形象、图标素材或 UI 设计图时自动带入对应缓存,刷新编辑器后仍可恢复。新建图标规范对象不依赖该图标规范缓存,未选择参考图也可以直接生成。
7. 单文本输入面板不显示文本框标题,用问题式 placeholder
- 生成图片:`今天想生成什么画面?`
- 生成角色:`你希望角色如何设计?`
@@ -41,6 +41,7 @@
8. 多输入框面板必须保留每个字段标题和输入框边界,例如生成规范。图标素材生成不再使用多描述列表,改为复用角色形象生成面板同款单文本输入框。
9. 生成规范下的角色规范、图标规范和自定义规范都使用同一生成类 shell:首行参考图区域、中央字段区、底部生成按钮区,不再出现缺首行参考区或单独 footer 样式。
10. 快速编辑最多允许额外绑定 8 张参考图;原图始终自动作为最后一张隐式参考图提交,不占用额外参考图额度,也不在参考图条里固定展示 `图x`
11. 快速编辑打开后,画布视口应调整到原图完整展示,且面板位于原图下方并不遮挡原图;原图右侧显示竖向框选工具,支持矩形、椭圆和画笔自由框选。快速编辑进入时不默认启用框选工具,点击工具后出现选中态并保持高亮,再点同一工具取消启用;红色圈选框使用细描边。每完成一次框选,红色圈选框按完成顺序标注 `1 / 2 / 3...`,并在快速编辑提示词中追加一行 `对N号红色圈选框里的内容做以下修改:`
## 参数交互
@@ -74,11 +75,11 @@
- 本次消耗泥点必须显示在生成按钮内部。
- 生成按钮内明确显示 `N泥点`,例如 `生成12泥点``生成40泥点`;不使用泥点图标替代文字。
- 画板内所有会提交外部生成任务的生成按钮,价格都必须从模型定价配置函数推导,不允许在按钮文案或提交 payload 中散落固定泥点数字;修改 `ImageCanvasGenerationModel.ts``api-server/src/editor_generation_config.rs` 的同名定价配置后,按钮展示、前端提交的 `priceMudPoints` 和后端校验 / 扣费应同步变化。
- 当前前端展示价统一收口在 `ImageCanvasGenerationModel.ts`:生成图片、生成角色、快速编辑、重绘、宣发素材走 `calculateEditorImageModelPrice` / `calculateEditorImageGenerationPrice`;生成图标素材走 `calculateEditorIconSpritesheetPrice`;生成 UI 设计图走 `calculateEditorUiDesignPrice`;生成规范走 `calculateEditorSpecGenerationPrice`;生成视频走 `calculateEditorVideoPrice`;角色动`calculateCharacterAnimationPrice`;音效 / 背景音乐分别走 `calculateEditorSoundEffectPrice` / `calculateEditorBackgroundMusicPrice`
- 泥点配置统一收口到 `api-server` 的编辑器生成配置模块;前端只保留与后端配置同名的展示兜底,后续可接接口动态下发。当前不是运行时动态配置接口,若要后台改价实时影响前端,需要新增配置下发能力
- 当前前端展示价统一收口在 `ImageCanvasGenerationModel.ts`:生成图片、生成角色、快速编辑、重绘、宣发素材走 `calculateEditorImageModelPrice` / `calculateEditorImageGenerationPrice`;生成图标素材走 `calculateEditorIconSpritesheetPrice`;生成 UI 设计图走 `calculateEditorUiDesignPrice`;生成规范走 `calculateEditorSpecGenerationPrice`;生成视频走 `calculateEditorVideoPrice`;角色动`calculateCharacterAnimationPrice`;音效 / 背景音乐分别走 `calculateEditorSoundEffectPrice` / `calculateEditorBackgroundMusicPrice`这些函数启动时会被后端下发配置覆盖,接口失败时才使用内置兜底。定价配置只按模型区分,不按图片 / 规范、视频 / 动作用途拆分;图片类价格必须同时传入模型和 `imageSize`,规范固定读取 `gpt-image-2``2K` 定价。
- 泥点配置默认值独立收口到 `server-rs/crates/api-server/config/editor-generation-pricing.default.json`JSON 结构为 `models[model] = { unit, price | prices }`;后台“模型定价”页面通过 `POST /admin/api/editor-generation-pricing` 保存完整 override 到 `.app/editor-generation-pricing.override.json`(可由 `GENARRATIVE_EDITOR_GENERATION_PRICING_OVERRIDE_PATH` 覆盖路径),主站通过 `GET /api/editor/generation-pricing` 动态读取当前配置。后台必须展示定价单位:`perGeneration` 显示“按次”,`perSecond` 显示“按秒”
- 生成图标素材面板提交 `POST /api/editor/icon-spritesheets/generations` 时必须携带 `priceMudPoints`,取同一份 `icon` 计费配置;后端用 `editor_generation_config` 校验,不允许绕过配置继续生成。
- 生成视频、角色动画、音效和背景音乐提交时携带的 `priceMudPoints` 也必须由同一模型配置函数计算,后端按归一后的模型、清晰度、时长或音频模型重新计算并校验。
- `提取素材` 是 UI 设计图工具栏动作,不是面板生成按钮;它固定使用 `gpt-image-2` 和图标素材拆分链路,后端响应里的 `priceMudPoints` 仍必须来自 `editor_generation_config`,不能写死。
- `提取素材` 点击后进入 UI 素材提取态:右侧框选工具对齐底部工具栏按钮风格,素材下方显示与生成新素材一致宽度的提取面板。提取面板显示短提示语、框选截图预览、固定模型 `gpt-image-2`、计划规格 `1:1·1K/2K``提取 · N泥点` 按钮;前后端提交 `aspectRatio / imageSize / priceMudPoints`,后端按 `editor_generation_config` 校验,不能写死。
- 画板 UI 统一显示 `nanobanana2`,它代表上游真实模型 `gemini-3.1-flash-image-preview`;历史输入或旧布局中的 `nano-banana` 必须先归一为 `nanobanana2` 对应的真实模型 ID 后再提交和计费。定价表仍以真实模型 ID `gemini-3.1-flash-image-preview``gpt-image-2` 为准,别名不能作为新增正式模型绕过定价表。
## 画布占位落点
@@ -96,7 +97,7 @@
- 图片类待生成占位尺寸必须与面板当前比例和尺寸同步:普通图片、角色形象、图标素材、UI 设计图按当前 `aspectRatio + imageSize` 计算像素尺寸;生成规范固定为 `16:9·2K`,占位为 `2048 x 1152`;宣发素材按 workflow 输出尺寸创建占位。
- 视频待生成占位必须与面板当前比例和清晰度同步:默认 `16:9 · 480p``854 x 480`,切换比例、`720p``1080p` 后按比例和清晰度重算偶数宽度;调整参数时保持占位中心点不变。
- 面板中用户修改比例、尺寸或清晰度后,已有空白待生成占位立即同步更新 `width / height / originalWidth / originalHeight`,且保持中心点不跳动。
- 快速编辑点击生成后不在原图上播放生成中遮罩,而是立即创建独立 `Quick Edit Generator` 画布生成占位并播放生成中动画;生成成功后结果落在该占位框位置,失败时占位标记失败并恢复快速编辑面板。
- 快速编辑点击生成后不在原图上播放生成中遮罩,而是立即创建独立 `Quick Edit Generator` 画布生成占位并播放生成中动画;该占位必须复用新建图片的 placement 避让逻辑,和已有素材 / 生成占位至少保留 32px 画布间距,不允许固定放到原图右侧后压住其它素材;生成成功后结果落在该占位框位置,失败时占位标记失败并恢复快速编辑面板。
## 画布悬浮信息
@@ -117,22 +118,19 @@
## 第一版计费配置
```text
生成图片 / 生成角色形象 / 生成图标素材 / 生成UI设计图 / 宣发素材 / 快速编辑 / 重绘:nanobanana2 为 12 泥点,gpt-image-2 为 20 泥点
生成规范:固定 gpt-image-25 泥点
生成视频:seedance2.0-fast 为 480p 每秒 10 / 720p 每秒 20,不支持 1080pseedance2.0 为 480p 每秒 12 / 720p 每秒 24 / 1080p 每秒 48kling3.0 为 480p 每秒 15 / 720p 每秒 30 / 1080p 每秒 60kling3.0-omni 为 480p 每秒 20 / 720p 每秒 40 / 1080p 每秒 80
角色动画:480p 每秒 10 泥点,720p 每秒 20 泥点
生成音效:10 泥点
生成背景音乐:5 泥点
图片模型按尺寸单次计价:nanobanana2 / gemini-3.1-flash-image-preview 为 0.5K 8 泥点、1K 12 泥点、2K 24 泥点;gpt-image-2 为 1K 3 泥点、2K 5 泥点
生成规范不再单独定价:固定读取 gpt-image-2 的 2K 定价
视频 / 角色动作按同一视频模型分辨率每秒计价seedance2.0-fast 为 480p 每秒 10 / 720p 每秒 20 / 1080p 每秒 40(生成视频入口仍禁止 fast 选 1080pseedance2.0 为 480p 每秒 12 / 720p 每秒 24 / 1080p 每秒 48kling3.0 为 480p 每秒 15 / 720p 每秒 30 / 1080p 每秒 60kling3.0-omni 为 480p 每秒 20 / 720p 每秒 40 / 1080p 每秒 80
生成音效:audio1.0 按次 5 泥点
生成背景音乐:chirp-v5 按次 12 泥点
```
当前必须显式覆盖的正式模型定价配置:
当前必须显式覆盖的正式模型定价配置(默认值来自 `editor-generation-pricing.default.json`,运行时可由后台 override 修改)
- 图片`gemini-3.1-flash-image-preview`UI 显示与历史别名统一为 `nanobanana2`为 12 泥点`gpt-image-2` 为 20 泥点
- 规范:`gpt-image-2` 为 5 泥点
- 视频`seedance2.0-fast` 为 10 / 20 泥点每秒;`seedance2.0` 为 12 / 24 泥点每秒;`kling3.0` 为 15 / 30 泥点每秒;`kling3.0-omni` 为 20 / 40 泥点每秒。兼容旧布局回放的 `veo3.1``veo3.1-fast` 也要保留 10 / 20 泥点每秒定价配置,但前端模型菜单不展示
- 角色动画:`seedance2.0-fast`
- 音效:`audio1.0`
- 背景音乐:`chirp-v5`
- 图片模型`gemini-3.1-flash-image-preview`UI 显示与历史别名统一为 `nanobanana2`必须配置 `0.5K / 1K / 2K``gpt-image-2` 必须配置 `1K / 2K`
- 视频模型:`seedance2.0-fast``seedance2.0``kling3.0``kling3.0-omni``480p / 720p / 1080p` 配每秒泥点。兼容旧布局回放的 `veo3.1``veo3.1-fast` 也要保留同样分辨率定价配置,但前端模型菜单不展示
- 音效`audio1.0` 按次定价
- 背景音乐:`chirp-v5` 按次定价
本次审计未发现当前正式入口缺少定价配置的模型;若后续新增模型,必须先补前端展示配置、前端提交计算、后端校验配置和对应测试,再出现在任一模型选择框中。
@@ -176,14 +174,15 @@
- 规范面板比图片生成面板更紧凑,字段间距和输入高度更小,但外层 shell、首行参考图和底部按钮区必须继续对齐生成图片 / 生成角色 / 生成视频。
- 生成规范类图片底部展示禁用态参数按钮 `16:9·2K``gpt-image-2`,视觉对齐可编辑面板的比例 / 尺寸 / 模型按钮;提交参数也固定为这三项,不出现可展开选项。
- 快速编辑底部展示当前选择的比例 / 尺寸和模型,视觉对齐可编辑面板的比例 / 尺寸 / 模型按钮,并允许展开修改;额外参考图最多 8 张,原图作为隐式最后参考图参与提交但不在参考图条里固定显示 `图x`
- 快速编辑打开后画布自动缩放平移到原图完整展示,并让面板位于原图下方且不遮挡原图;原图右侧出现竖向矩形 / 椭圆 / 画笔自由框选按钮。进入快速编辑不默认启用框选,点击工具启用并保持高亮,再点同一工具取消;完成框选后画布红色细框显示连续序号,输入框同步追加 `对N号红色圈选框里的内容做以下修改:`
- 快速编辑提交前必须把提示词里对原图的 `原图``当前图片``当前图``图1` 引用改写成最后一张原图编号,例如额外 1 张参考图时改为 `图2`
- 快速编辑提交给后端的参考图顺序必须是额外参考图在前、原图在最后。
- 快速编辑提交给后端的参考图顺序必须是额外参考图在前、原图在最后;若用户已完成红色圈选,最后一张隐式原图必须替换为绘制了红框和序号的标注图
- 生成中的占位图聚焦后可用 `Delete` / `Backspace` 删除;删除后异步结果不再落回画布,也不显示额外删除 UI。
- 快速编辑生成中的占位图同样只支持键盘 `Delete` / `Backspace` 删除,不新增 UI 删除按钮;删除后异步结果不得再落回画布。
- 生成视频 / 角色形象 / 角色动作 / 音效 / 背景音乐新建后,画布占位空白样式和右上角标签均与对应生成类型一致,不再统一使用图片占位 icon。
- 新建空白待生成占位的尺寸必须和面板参数一致;图片类修改比例 / 尺寸、视频修改清晰度后,画布空白占位同步变更且保持中心点。
- 点击角色图只选中图层并显示工具栏,不自动弹出重绘、快速编辑或角色动画面板;点击工具栏或右键菜单中的 `生成动画` 才创建角色动作占位和面板。
- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后固定用 `gpt-image-2`提示词 `提取画面中的所有独立并整理成spritesheet` 生成 spritesheet,再复用图标素材拆分流程,把 spritesheet 图集和拆分素材都放到画布。
- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后画布自动缩放平移到素材完整展示,并在素材下方显示 UI 素材提取面板。UI 素材提取默认启用矩形框选,右侧工具栏与快速编辑统一,当前启用工具按钮保持高亮,点击同一工具可取消启用态;面板提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选截图预览、固定模型 `gpt-image-2`、计划规格和提取按钮泥点,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。用户至少框选一个区域后才能点击 `提取`,前端把红色轮廓绘入原图作为参考图,再固定用提示词 `提取被红色框框选的素材并整理成spritesheet` 生成 spritesheet。框选数量不超过阈值时按 `1:1·1K` 和 gpt-image-2 1K 泥点提交,超过阈值时改用 `1:1·2K` 和 2K 泥点;后续复用图标素材拆分流程,把 spritesheet 图集和拆分素材都放到画布。
- 生成游戏音效面板底部不显示字段标题,左下角只有一个时长参数按钮,选项为 Vidu duration `2-10` 秒;右下角固定模型胶囊显示 `Vidu` 并紧贴生成按钮。
- 生成游戏背景音乐面板右下角固定模型胶囊显示 `Suno` 并紧贴生成按钮;`make_instrumental` 不在 UI 中展示。
- 生成视频结果以视频图层加入画布,画布媒体元素标记为 `画布视频:生成视频 N`

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