diff --git a/AGENTS.md b/AGENTS.md index 5699de822..65504fb28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` facade,HTTP/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 diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index 0a3a17921..dcd5340d4 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -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( + '/admin/api/editor-generation-pricing', + { token }, + ); +} + +export function upsertAdminEditorGenerationPricing( + token: string, + payload: EditorGenerationPricingConfigPayload, +) { + return request( + '/admin/api/editor-generation-pricing', + { + method: 'POST', + token, + body: payload, + }, + ); +} + export function listAdminWorkVisibility(token: string) { return request( '/admin/api/works/visibility', diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index d3cb8ebe6..c876dcc40 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -210,6 +210,19 @@ export interface AdminUpsertPublicWorkInteractionConfigRequest { publicWorkInteractions: PublicWorkInteractionConfigPayload[]; } +/** 图片画布生成模型泥点定价配置。 */ +export type EditorGenerationPricingUnitPayload = 'perGeneration' | 'perSecond'; + +export interface EditorGenerationModelPricingPayload { + unit: EditorGenerationPricingUnitPayload; + price?: number; + prices?: Record; +} + +export interface EditorGenerationPricingConfigPayload { + models: Record; +} + /** 后台统一创作工作台契约表单的传输结构。 */ export interface UnifiedCreationSpecPayload { playId: string; diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index 9d50f380f..8a30a7afd 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -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' ? ( + + ) : null} ); } diff --git a/apps/admin-web/src/app/AdminShell.tsx b/apps/admin-web/src/app/AdminShell.tsx index e5908331d..cd975a644 100644 --- a/apps/admin-web/src/app/AdminShell.tsx +++ b/apps/admin-web/src/app/AdminShell.tsx @@ -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, diff --git a/apps/admin-web/src/app/adminRoutes.test.ts b/apps/admin-web/src/app/adminRoutes.test.ts index bae46be87..6428d7892 100644 --- a/apps/admin-web/src/app/adminRoutes.test.ts +++ b/apps/admin-web/src/app/adminRoutes.test.ts @@ -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', + ); +}); diff --git a/apps/admin-web/src/app/adminRoutes.ts b/apps/admin-web/src/app/adminRoutes.ts index 3b6ed6c36..d6a0769af 100644 --- a/apps/admin-web/src/app/adminRoutes.ts +++ b/apps/admin-web/src/app/adminRoutes.ts @@ -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'}, diff --git a/apps/admin-web/src/pages/AdminEditorGenerationPricingPage.test.tsx b/apps/admin-web/src/pages/AdminEditorGenerationPricingPage.test.tsx new file mode 100644 index 000000000..d487cab38 --- /dev/null +++ b/apps/admin-web/src/pages/AdminEditorGenerationPricingPage.test.tsx @@ -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( + , + ); + + 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, + }), + }), + }), + }), + ); + }); +}); diff --git a/apps/admin-web/src/pages/AdminEditorGenerationPricingPage.tsx b/apps/admin-web/src/pages/AdminEditorGenerationPricingPage.tsx new file mode 100644 index 000000000..bb2fa9587 --- /dev/null +++ b/apps/admin-web/src/pages/AdminEditorGenerationPricingPage.tsx @@ -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 = { + perGeneration: '按次', + perSecond: '按秒', +}; + +export function AdminEditorGenerationPricingPage({ + token, + onUnauthorized, +}: AdminEditorGenerationPricingPageProps) { + const [pricing, setPricing] = + useState(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) { + 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 ( +
+
+
+

模型定价

+

生成泥点消耗

+
+ +
+ + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} + +
+
+ {Object.entries(pricing.models).map(([model, modelPricing]) => + renderModelPricingCard({ + model, + modelPricing, + onFlatChange: updateFlatPrice, + onTierChange: updateTierPrice, + }), + )} +
+ + +
+ + {confirmDialog} +
+ ); +} + +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 ( +
+
+ {model} + {unitLabel} +
+ {modelPricing.prices ? ( +
+ {Object.entries(modelPricing.prices).map(([tier, price]) => ( + + ))} +
+ ) : ( + + )} +
+ ); +} + +function parsePositiveInteger(value: string) { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; +} diff --git a/apps/admin-web/src/styles/admin.css b/apps/admin-web/src/styles/admin.css index 4f8b1faaf..6aa13364f 100644 --- a/apps/admin-web/src/styles/admin.css +++ b/apps/admin-web/src/styles/admin.css @@ -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, diff --git a/docs/README.md b/docs/README.md index 7d2cc35b8..26f01f613 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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)。 diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index bf7087e02..414f34fbc 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -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", diff --git a/docs/project-memory/README.md b/docs/project-memory/README.md index f95953f80..8245289d0 100644 --- a/docs/project-memory/README.md +++ b/docs/project-memory/README.md @@ -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/` 为准,并同步修正过期记忆。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 6de082cbe..a0b9fce0b 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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 / 40,Veo 旧布局兼容价仍为 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`。 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 0c59fc5be..cdff5605e 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.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. 建议的测试命令和提交信息。 diff --git a/docs/project-memory/shared-memory/document-map.md b/docs/project-memory/shared-memory/document-map.md index 311d04979..ea8afce2b 100644 --- a/docs/project-memory/shared-memory/document-map.md +++ b/docs/project-memory/shared-memory/document-map.md @@ -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`。 - 阶段性流水账、一次性修复记录和已关闭实验不要再新增为长期文档。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index e160acdb6..6c0fa7832 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -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//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`,用于 `