master #43

Merged
kdletters merged 101 commits from master into release 2026-05-27 03:07:12 +08:00
705 changed files with 77275 additions and 17271 deletions
+1
View File
@@ -0,0 +1 @@
{"containers":[],"config":{}}
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+143
View File
@@ -0,0 +1,143 @@
{
"version": 1,
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.py",
"**/*.go",
"**/*.rs",
"**/*.java",
"**/*.c",
"**/*.h",
"**/*.cpp",
"**/*.hpp",
"**/*.cc",
"**/*.cxx",
"**/*.cs",
"**/*.php",
"**/*.rb",
"**/*.swift",
"**/*.kt",
"**/*.kts",
"**/*.dart",
"**/*.svelte",
"**/*.vue",
"**/*.liquid",
"**/*.pas",
"**/*.dpr",
"**/*.dpk",
"**/*.lpr",
"**/*.dfm",
"**/*.fmx",
"**/*.scala",
"**/*.sc"
],
"exclude": [
"**/.git/**",
"**/node_modules/**",
"**/vendor/**",
"**/Pods/**",
"**/dist/**",
"**/build/**",
"**/out/**",
"**/bin/**",
"**/obj/**",
"**/target/**",
"**/*.min.js",
"**/*.bundle.js",
"**/.next/**",
"**/.nuxt/**",
"**/.svelte-kit/**",
"**/.output/**",
"**/.turbo/**",
"**/.cache/**",
"**/.parcel-cache/**",
"**/.vite/**",
"**/.astro/**",
"**/.docusaurus/**",
"**/.gatsby/**",
"**/.webpack/**",
"**/.nx/**",
"**/.yarn/cache/**",
"**/.pnpm-store/**",
"**/storybook-static/**",
"**/.expo/**",
"**/web-build/**",
"**/ios/Pods/**",
"**/ios/build/**",
"**/android/build/**",
"**/android/.gradle/**",
"**/__pycache__/**",
"**/.venv/**",
"**/venv/**",
"**/site-packages/**",
"**/dist-packages/**",
"**/.pytest_cache/**",
"**/.mypy_cache/**",
"**/.ruff_cache/**",
"**/.tox/**",
"**/.nox/**",
"**/*.egg-info/**",
"**/.eggs/**",
"**/go/pkg/mod/**",
"**/target/debug/**",
"**/target/release/**",
"**/.gradle/**",
"**/.m2/**",
"**/generated-sources/**",
"**/.kotlin/**",
"**/.dart_tool/**",
"**/.vs/**",
"**/.nuget/**",
"**/artifacts/**",
"**/publish/**",
"**/cmake-build-*/**",
"**/CMakeFiles/**",
"**/bazel-*/**",
"**/vcpkg_installed/**",
"**/.conan/**",
"**/Debug/**",
"**/Release/**",
"**/x64/**",
"**/.pio/**",
"**/release/**",
"**/*.app/**",
"**/*.asar",
"**/DerivedData/**",
"**/.build/**",
"**/.swiftpm/**",
"**/xcuserdata/**",
"**/Carthage/Build/**",
"**/SourcePackages/**",
"**/__history/**",
"**/__recovery/**",
"**/*.dcu",
"**/.composer/**",
"**/storage/framework/**",
"**/bootstrap/cache/**",
"**/.bundle/**",
"**/tmp/cache/**",
"**/public/assets/**",
"**/public/packs/**",
"**/.yardoc/**",
"**/coverage/**",
"**/htmlcov/**",
"**/.nyc_output/**",
"**/test-results/**",
"**/.coverage/**",
"**/.idea/**",
"**/logs/**",
"**/tmp/**",
"**/temp/**",
"**/_build/**",
"**/docs/_build/**",
"**/site/**"
],
"languages": [],
"frameworks": [],
"maxFileSize": 1048576,
"extractDocstrings": true,
"trackCallSites": true
}
+23
View File
@@ -0,0 +1,23 @@
# Genarrative 项目级 Codex 配置。
# 这里仅保存可进入仓库的 hook 配置与脚本;个人 token、MCP server、模型路由仍放在个人 ~/.codex/config.toml。
[features]
hooks = true
# Codex 准备执行 git commit 前检查 TypeScript / admin-web / api-server 编译错误。
# 脚本也可手动运行:
# node .codex/hooks/pre-submit-compile-check.mjs
[[hooks.PreToolUse]]
matcher = "Bash|shell_command|functions.shell_command"
command = "node .codex/hooks/pre-submit-compile-check.mjs"
timeout = 180
statusMessage = "提交前检查编译错误"
# Codex 每次工具修改文件后执行:同步 CodeGraph 索引。
# 脚本也可手动运行:
# node .codex/hooks/post-edit-codegraph-sync.mjs
[[hooks.PostToolUse]]
matcher = "Bash|Edit|MultiEdit|Write|apply_patch|shell_command|functions.shell_command|functions.apply_patch"
command = "node .codex/hooks/post-edit-codegraph-sync.mjs"
timeout = 60
statusMessage = "更新 CodeGraph 索引"
+11 -1
View File
@@ -3,4 +3,14 @@ version = 1
name = "Genarrative"
[setup]
script = ""
script = '''
cp "$env:CODEX_SOURCE_TREE_PATH\.env.secrets.local" "$env:CODEX_WORKTREE_PATH\.env.secrets.local"
npm install
npm run codegraph:init
npm run codegraph:index
'''
[[actions]]
name = "运行"
icon = "run"
command = "npm run dev"
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..', '..');
const logDir = resolve(repoRoot, '.codex', 'logs');
const hasCodegraphConfig = existsSync(resolve(repoRoot, '.codegraph', 'config.json'));
const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm';
if (!hasCodegraphConfig) {
console.log('[codex-hook] 未发现 .codegraph/config.json,跳过 CodeGraph 同步。');
process.exit(0);
}
const result = spawnSync(npmCommand, process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run codegraph:sync'] : ['run', 'codegraph:sync'], {
cwd: repoRoot,
shell: false,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: process.env.NO_COLOR ?? '1',
},
});
mkdirSync(logDir, { recursive: true });
if (result.stdout) {
process.stdout.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.error) {
console.error(`[codex-hook] CodeGraph 同步启动失败:${result.error.message}`);
process.exit(1);
}
if (result.signal) {
console.error(`[codex-hook] CodeGraph 同步被信号终止:${result.signal}`);
process.exit(1);
}
if ((result.status ?? 0) !== 0) {
console.error('[codex-hook] CodeGraph 同步失败,请手动运行 npm run codegraph:sync 查看详情。');
process.exit(result.status ?? 1);
}
console.log('[codex-hook] CodeGraph 已同步。');
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..', '..');
const npmCommand = process.platform === 'win32' ? 'cmd' : 'npm';
const hookInput = readHookInput();
if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) {
process.exit(0);
}
const validationSteps = [
{
label: 'TypeScript typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'],
},
{
label: 'Admin web typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'],
},
{
label: 'Rust api-server compile check',
command: 'cargo',
args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
},
];
for (const step of validationSteps) {
const result = runStep(step);
if (!result.ok) {
const reason = `[codex-hook] 提交前编译检查失败:${step.label}。请修复编译错误后再提交。`;
console.error(reason);
if (hookInput) {
console.log(JSON.stringify({ decision: 'block', reason }));
process.exit(0);
}
process.exit(result.status ?? 1);
}
}
console.error('[codex-hook] 提交前编译检查通过。');
function runStep(step) {
console.error(`[codex-hook] ${step.label}`);
const result = spawnSync(step.command, step.args, {
cwd: repoRoot,
shell: false,
encoding: 'utf8',
env: {
...process.env,
NO_COLOR: process.env.NO_COLOR ?? '1',
},
});
if (result.stdout) {
process.stderr.write(result.stdout);
}
if (result.stderr) {
process.stderr.write(result.stderr);
}
if (result.error) {
console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`);
return { ok: false, status: 1 };
}
if (result.signal) {
console.error(`[codex-hook] ${step.label} 被信号终止:${result.signal}`);
return { ok: false, status: 1 };
}
return {
ok: (result.status ?? 0) === 0,
status: result.status ?? 1,
};
}
function readHookInput() {
try {
const rawInput = readFileSync(0, 'utf8').trim();
if (!rawInput) {
return null;
}
return JSON.parse(rawInput);
} catch {
return null;
}
}
function extractShellCommand(input) {
const candidates = [
input?.tool_input?.command,
input?.toolInput?.command,
input?.tool_args?.command,
input?.toolArgs?.command,
input?.arguments?.command,
input?.params?.command,
input?.command,
];
const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0);
if (command) {
return command;
}
const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
if (Array.isArray(shellCommand)) {
return shellCommand.join(' ');
}
return '';
}
function isGitCommitCommand(command) {
return /(^|[;&|]\s*)git(?:\.exe)?\b[\s\S]{0,200}\bcommit\b/iu.test(command);
}
File diff suppressed because it is too large Load Diff
+5 -13
View File
@@ -5,7 +5,7 @@ description: Generate or inspect project image assets through this repository's
# gpt-image-2 VectorEngine
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2-all` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow
@@ -40,22 +40,14 @@ Default body:
```json
{
"model": "gpt-image-2-all",
"model": "gpt-image-2",
"prompt": "<prompt>",
"n": 1,
"size": "1024x1024"
}
```
For weak visual references in text-to-image generation, add:
```json
{
"image": ["data:image/png;base64,..."]
}
```
For image-to-image work that must follow a reference image closely, use the VectorEngine edits endpoint instead of the generations `image` array:
For visual references, use the edit endpoint instead of the create endpoint:
```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits
@@ -73,9 +65,9 @@ size=1024x1024
image=@reference.png
```
Prefer edits for workflows where the reference image controls composition, pose, container shape, or layout. In this repository, Match3D container UI generation uses edits with `public/match3d-background-references/pot-fused-reference.png` as the `image` part.
In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2-all currently returns synchronously; do not poll APIMart task endpoints.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2 currently returns synchronously; do not poll APIMart task endpoints.
## Environment
@@ -245,7 +245,7 @@ async function downloadUrl(url, timeoutMs) {
async function generateOne(env, entry, outDir) {
const requestBody = {
model: 'gpt-image-2-all',
model: 'gpt-image-2',
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
@@ -305,7 +305,7 @@ if (dryRun) {
id: entry.id,
title: entry.title,
body: {
model: 'gpt-image-2-all',
model: 'gpt-image-2',
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
@@ -211,7 +211,7 @@ async function downloadUrl(url, timeoutMs) {
async function generateOne(env, template, outDir) {
const requestBody = {
model: 'gpt-image-2-all',
model: 'gpt-image-2',
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
@@ -275,7 +275,7 @@ if (dryRun) {
id: template.id,
title: template.title,
body: {
model: 'gpt-image-2-all',
model: 'gpt-image-2',
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
+1
View File
@@ -36,6 +36,7 @@ temp*build*/
/.codex-temp
/target/
/logs
/server-rs/crates/*/logs/
.worktrees/
.env.secrets.local
spacetime.local.json
+399
View File
@@ -0,0 +1,399 @@
# Genarrative(陶泥儿)项目架构优化计划书
**文档版本**v1.0
**编制日期**2026-05-26
**项目名称**Genarrative(陶泥儿)
**文档密级**:内部
---
## 一、项目概况
| 维度 | 详情 |
|---|---|
| **项目名** | Genarrative(陶泥儿) |
| **项目定位** | AI Native 互动视觉 RPG 平台——支持多种玩法模板的 AI 创作、运行与分享("玩法类型平台" |
| **核心玩法** | 拼图、视觉小说、Match3D、Bark Battle、Big Fish、Jump-Hop、Square Hole、木鱼、教娱等 10+ 种玩法模板 |
| **代码规模** | 前端 ~823 个 TS/TSX 文件,后端 ~1532 个 Rust 文件,属于大型项目 |
### 1.1 计划目的
本计划书基于对 Genarrative 项目当前架构的全面分析,识别架构层面的关键问题,并提出分阶段、可落地的优化方案。旨在:
- 统一前端架构模式,降低团队认知成本和新人上手门槛
- 提升模块内聚性,减少不必要的耦合与依赖
- 建立自动化契约保障机制,降低跨语言同步出错风险
- 优化工程基础设施,提高开发效率和运维可观测性
---
## 二、技术栈总览
| 层级 | 技术选型 | 版本 |
|---|---|---|
| **前端框架** | React + TypeScript + Vite | React 19 / TS 5.8 / Vite 6 |
| **样式** | TailwindCSS | v4 |
| **3D / 动画** | Three.js、Motion、cannon-es | - |
| **后端 HTTP** | Rust + AxumBFF 门面) | Axum 0.8 |
| **游戏状态 DB** | SpacetimeDB(实时反应式数据库) | v2.2 |
| **AI / LLM** | LangChain-Rust + LLM Proxy | - |
| **小程序** | 微信小程序(含微信支付) | - |
| **容器化** | Docker ComposeNginx + API Server + OTel Collector | - |
| **运维** | systemd、Nginx、Jenkins CI/CD、k6 压测 | - |
| **可观测性** | OpenTelemetryOTLP → Grafana | - |
---
## 三、当前架构分层图
```
┌──────────────────────────────────────────────────────────────┐
│ 入口层 │
│ index.html → main.tsx → resolveAppRoute() → RouteComponent │
│ (多入口路由:平台主页 / 拼图 / BigFish / Match3D / ...) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 前端应用层 (src/) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐│
│ │ components/ │ │ services/ │ │ games/ ││
│ │ *-creation │ │ *-creation │ │ bark-battle/ ││
│ │ *-result │ │ *-runtime │ │ domain/ ││
│ │ *-runtime │ │ *-works │ │ application/ ││
│ │ common/ │ │ storyEngine │ │ infrastructure/ ││
│ │ auth/ │ │ payment │ │ ui/ ││
│ └─────────────┘ └──────────────┘ └──────────────────────┘│
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐│
│ │ hooks/ │ │ data/ │ │ routing/ ││
│ │ persistence/│ │ functionCat. │ │ config/ editor/ ││
│ └─────────────┘ └──────────────┘ └──────────────────────┘│
└──────────────────────────────────────────────────────────────┘
│ Vite Proxy (/api/*)
┌──────────────────────────────────────────────────────────────┐
│ Rust 后端 (server-rs/) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ api-server (Axum HTTP / SSE / BFF 门面) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ platform │ │ module-* │ │ shared │ │
│ │ -auth │ │ -puzzle │ │ -contracts │ │
│ │ -llm │ │ -visual-novel │ │ -kernel │ │
│ │ -image │ │ -match3d │ │ -logging │ │
│ │ -oss │ │ -bark-battle │ └──────────────┘ │
│ │ -speech │ │ -big-fish ... │ │
│ │ -agent │ │ -runtime │ │
│ └──────────┘ │ -combat/npc │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ spacetime-module + spacetime-client → SpacetimeDB │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 辅助子系统 │
│ apps/admin-web (React 后台管理) │
│ miniprogram/ (微信小程序) │
│ packages/shared (前后端共享契约/LLM工具) │
│ deploy/ (Docker/Nginx/systemd/OTel) │
│ scripts/ (40+ 构建/部署/检查脚本) │
│ jenkins/ (CI/CD Pipeline) │
└──────────────────────────────────────────────────────────────┘
```
---
## 四、架构亮点
1. **清晰的"玩法模板"平台模式**
每种玩法遵循统一的 `creation → result → runtime` 三段式生命周期,前端 `components/``services/` 均按此模式组织。新增玩法可快速套用模板,极大降低了横向扩展成本。
2. **后端严格的分层约束**
`api-server`(门面)→ `module-*`(领域)→ `spacetime-module`(持久化),`module-*` 不直接依赖 Axum、HTTP、SpacetimeDB table、LLM、文件系统,保证了领域纯净性和可测试性。
3. **SpacetimeDB 作为游戏状态核心**
采用反应式实时数据库替代传统 Redis + PostgreSQL 组合,天然适合多人实时游戏状态同步,减少了中间层复杂度和延迟。
4. **前端 DDD 探索**
`src/games/bark-battle/` 采用 `domain / application / infrastructure / ui` 四层 DDD 结构,为复杂玩法的前端架构提供了良好范本。
5. **完善的多入口路由体系**
通过 `resolveAppRoute()` 按 URL path 分发到不同的 lazy-loaded 组件,实现了按需加载和良好的首屏性能。
6. **运维体系完备**
Docker Compose + systemd + Nginx + OpenTelemetry + k6 压测 + Jenkins CI/CD,覆盖了构建、部署、监控、压测全链路。
---
## 五、问题诊断与改进方案
### 问题 1:前端 services/ 与 components/ 的耦合不统一
**现状**
大部分玩法遵循 `services/` + `components/` 分离模式,但部分玩法运行时直接放在 `components/` 下,services 层职责模糊——有的承担了业务逻辑编排,有的仅作简单 API 调用。
**影响**
- 新人阅读代码时无法预判某个逻辑应位于哪个目录
- 单元测试困难:services 与组件耦合的业务逻辑无法独立测试
- 跨玩法复用时需要额外的迁移成本
**改进方案**
1. 制定规范:`services/` 仅负责纯 API 调用和数据转换,不包含业务判断逻辑
2. 业务逻辑统一归到 `hooks/``games/<play-type>/application/`
3. 以 puzzle 玩法为样板,先行重构并形成迁移指南,再推广至其余玩法
4. 在 CI 中增加 ESLint 规则,禁止 `components/` 直接 import `services/` 之外的外部模块
**预期收益**
- 职责边界清晰,降低认知成本约 30%
- services 层可独立单测覆盖率达到 80%+
- 新玩法开发上手时间从 2 天缩短至 0.5 天
---
### 问题 2:前端 DDD 与模板模式并存,架构不统一
**现状**
`bark-battle` 采用 DDD 四层结构(`domain/application/infrastructure/ui`),其余玩法散落在 `components/` + `services/` 下,存在两种截然不同的组织范式。
**影响**
- 团队内部对"正确"的代码组织方式缺乏共识
- 代码审查时需要切换判断标准
- DDD 玩法的优势无法在全局范围内发挥
**改进方案**
1. 所有玩法统一迁移到 `src/games/<play-type>/` 下,采用 `domain / application / ui` 三层结构(infrastructure 按需保留)
2. 以 puzzle 为样板完成首例迁移,产出迁移 Checklist 和模板生成脚本
3. 新增玩法脚手架直接生成 DDD 结构目录
4. 旧玩法分批次迁移,每批次 2-3 个玩法,在 4 个迭代内完成
**预期收益**
- 架构一致性提升至 100%
- 跨玩法逻辑复用变得可能(domain 层可共享)
- 为后续 monorepo 改造打下基础
---
### 问题 3:后端 module-* 粒度偏细,存在潜在循环依赖风险
**现状**
后端共 35 个 crate。`module-runtime` 被拆分为 3 个独立 crate`module-combat / npc / inventory` 各自独立。部分紧密协作的模块之间可能存在隐式耦合。
**影响**
- 编译时间增长(35 个 crate 独立编译)
- 跨 crate 重构时需要同时修改多处
- 循环依赖风险增加,可能在特定组合下触发编译失败
**改进方案**
1. 评估合并方案:
- `module-runtime-*` 系列合并为单 crate `module-runtime`,内部用 `mod` 做逻辑隔离
- `module-combat / npc / inventory` 评估合并为 `module-combat`npc 和 inventory 作为子模块
2. 保留 trait/interface 抽象层,确保 module 之间不直接依赖具体实现
3. 在 CI 中引入 `cargo-deny` 或自定义脚本,自动检测 module 间的依赖方向是否违反分层约束
4. 目标:将 35 个 crate 精简至 25 个以内
**预期收益**
- 全量编译时间预计缩短 15%-20%
- 循环依赖风险归零
- module 内部重构成本降低
---
### 问题 4:根目录 env 文件过多且混乱
**现状**
根目录存在 4 个 env 文件(`.env``.env.example``.env.production` 等),且 `deploy/` 下另有多个 env 文件。配置分散在多处,部分文件之间字段不一致。
**影响**
- 排查配置问题时需要翻阅多个文件
- 新人无法快速确定本地开发需要哪些环境变量
- 部署时可能遗漏或错误覆盖某项配置
**改进方案**
1. 收敛为三层配置体系:
- `.env.example`:包含所有可配置项的说明和默认值(唯一提交到仓库的 env 文件)
- `.env.local`:本地开发覆盖(加入 .gitignore
- `deploy/env/<env-name>.env`:各部署环境专用配置
2. 引入 config crate,支持层次覆盖(default → local → env-specific),启动时自动校验必填字段
3. 在 CI 中加入 env 校验步骤:对比 `.env.example` 与部署环境的 env 文件,标记缺失或多余字段
**预期收益**
- 配置查找时间从分钟级降至秒级
- 部署配置遗漏导致的线上事故减少 90%+
- 新成员本地环境搭建时间从 30 分钟缩短至 10 分钟
---
### 问题 5scripts/ 目录膨胀为"万能工具箱"
**现状**
`scripts/` 目录共 42 个文件,涵盖构建、部署、检查、迁移、生成、压测等,全部平铺在同一层级,缺乏分类。
**影响**
- 难以快速定位所需脚本
- 同类脚本缺乏命名规范
- 新增脚本时不知道放在何处
**改进方案**
1. 按功能域分类重组:
```
scripts/
├── build/ # 构建相关(vite、cargo、wasm 等)
├── deploy/ # 部署相关(docker、systemd、rsync
├── check/ # 检查/校验(lint、format、type-check
├── spacetime/ # SpacetimeDB 相关(migration、seed
├── generate/ # 代码生成(scaffold、proto、types
└── loadtest/ # 压测脚本(k6 配置及辅助)
```
2. 为每个子目录添加 README.md,说明各脚本用途和调用方式
3. 将重复逻辑抽取为共享函数库
**预期收益**
- 脚本查找效率提升 60%+
- 降低脚本重复概率
- 便于 CI Pipeline 直接引用标准化路径
---
### 问题 6:前端路由系统缺乏统一的"玩法注册"机制
**现状**
`appRoutes.tsx` 中硬编码 `switch-case` 逻辑,每新增一种玩法需手动修改路由文件、入口组件、资源加载等多个位置。
**影响**
- 新增玩法的接入点分散,容易遗漏
- 路由文件随玩法增多持续膨胀
- 无法实现"按需注册"——即使某环境不包含某玩法,路由代码仍然存在
**改进方案**
1. 建立 `PlayTypeRegistry` 模式:每个玩法导出一个注册项对象,包含 `path``lazyComponent``preload` 等字段
2. `resolveAppRoute()` 改为动态聚合所有注册项,替代硬编码 switch-case
3. 支持环境级玩法开关:通过配置控制某环境启用哪些玩法,路由系统自动忽略未启用的
**预期收益**
- 新增玩法零侵入路由系统,只需在玩法目录内添加注册文件
- 路由文件体积与玩法数量解耦
- 灰度发布和 A/B 测试变得可能
---
### 问题 7:前端缺少统一的状态管理层
**现状**
前端状态管理依赖 React hooks + props drilling + services 层手动管理。未使用任何状态管理库(如 Zustand、Jotai、Redux)。
**影响**
- 全局状态(用户认证、会话、通知)通过多层 props 传递,组件耦合度高
- 跨页面状态无法优雅共享
- 状态变更难以追踪和调试
**改进方案**
1. 引入 Zustand(轻量、无 boilerplate、TS 友好)管理全局状态:
- `useAuthStore`:认证状态
- `useSessionStore`:当前会话/游戏状态
- `useNotificationStore`:全局通知
2. 玩法内状态继续使用 React hooks + `useReducer`,保持局部自治
3. 全局 store 与玩法内 state 通过事件总线松耦合通信
**预期收益**
- props drilling 层级从 5+ 层降至 1-2 层
- 全局状态可追溯,支持 Redux DevTools 调试
- 跨玩法状态共享(如用户余额、道具)变得自然
---
### 问题 8shared-contracts 的实际复用程度待验证
**现状**
前后端通过 `packages/shared` 共享 DTO 类型定义,但依赖手动同步 TypeScript 类型。可能存在前后端契约不一致但编译期无法检出的情况。
**影响**
- 后端修改 DTO 字段后,前端可能遗漏更新导致运行时错误
- 手动同步耗时且易出错
- Code Review 时难以判断契约一致性
**改进方案**
1. 引入 `ts-rs`:从 Rust 结构体自动生成 TypeScript 类型定义
2. 将生成步骤集成到 CI Pipeline
- 每次 Rust PR 触发 `ts-rs` 重新生成 TS 类型
- 对比生成的类型与仓库中的类型是否一致,不一致则 CI 失败
3. 长期考虑引入 Protobuf / OpenAPI 作为跨语言契约的单一事实来源
**预期收益**
- 前后端契约不一致导致的线上 bug 减少 95%+
- 手动同步工作量归零
- PR Review 时契约一致性问题自动拦截
---
## 六、改进优先级路线图
| 优先级 | 改进项 | 涉及层 | 建议时间 | 预期收益 |
|---|---|---|---|---|
| **P0** | 统一前端 services/hooks/components 职责边界 | 前端 | 第 1-2 周 | 降低认知成本,提升可测试性 |
| **P1** | 建立 PlayType 注册机制 | 前端 | 第 2-3 周 | 新增玩法零侵入路由 |
| **P1** | 评估 module-* 合并方案并执行 | 后端 | 第 3-4 周 | 减少编译时间 15%-20% |
| **P1** | 引入 Zustand 全局状态管理 | 前端 | 第 4-5 周 | 改善状态追踪与跨组件共享 |
| **P2** | 清理 scripts/ 目录结构 | 工程 | 第 5-6 周 | 提高可发现性 |
| **P2** | 前端玩法统一迁移至 DDD 结构 | 前端 | 第 5-8 周 | 架构一致性 100% |
| **P2** | env 配置收敛 | 工程 | 第 6-8 周 | 减少部署配置事故 |
| **P3** | 前后端共享 DTO 自动化(ts-rs) | 全栈 | 第 6-8 周 | 消除契约不一致风险 |
| **P3** | CI 分层约束检查(cargo-deny | 后端 | 第 8-10 周 | 循环依赖归零 |
> **说明**:P0 为阻塞项,必须最先完成。P1 项可部分并行推进(PlayType 注册与 module 合并互不依赖)。P2/P3 为优化项,可在日常迭代中穿插推进。
---
## 七、架构健康度评分卡
| 维度 | 评分 | 当前状态 | 目标状态 |
|---|---|---|---|
| **分层清晰度** | ★★★★☆ | 后端分层严格,前端分层存在不一致 | ★★★★★ 前后端均严格分层 |
| **模块化程度** | ★★★★☆ | 后端 35 crate 粒度偏细,前端结构化较好 | ★★★★☆ 后端精简至 25 crate |
| **可扩展性** | ★★★★★ | 玩法模板模式使新增玩法成本低 | ★★★★★ 维持 |
| **代码复用** | ★★★☆☆ | shared 层作用有限,services 层有重复 | ★★★★☆ DDD 统一后 domain 可复用 |
| **DevOps 成熟度** | ★★★★★ | Docker + k6 + OTel + Jenkins 覆盖完整 | ★★★★★ 维持 |
| **文档完备性** | ★★★★★ | docs/ 分类清晰,基线文档齐全 | ★★★★★ 维持 |
| **技术债务管控** | ★★★★☆ | 有明确的"历史残留"标记和废弃策略 | ★★★★★ 增加自动化检测 |
**综合评级:A-(优秀,存在可优化空间)**
---
## 八、附录:代码规模统计
| 维度 | 数量 |
|---|---|
| **前端 TypeScript/TSX 文件** | ~823 个 |
| **后端 Rust 源文件** | ~1532 个 |
| **后端 Crate 数量** | 35 个 |
| **核心玩法类型** | 10+ 种 |
| **scripts/ 脚本数量** | 42 个 |
| **根目录 env 文件** | 4 个 + deploy 下多个 |
### 模块规模明细(后端)
| Crate | 职责 | 建议 |
|---|---|---|
| `api-server` | Axum HTTP 门面,路由聚合 | 保持 |
| `platform-*` (auth/llm/image/oss/speech/agent) | 平台级跨玩法能力 | 保持 |
| `module-puzzle` | 拼图玩法 | 作为 DDD 迁移样板 |
| `module-visual-novel` | 视觉小说 | 后续迁移 |
| `module-match3d` | Match3D 三消 | 后续迁移 |
| `module-bark-battle` | 犬吠对战 | 已对接前端 DDD |
| `module-big-fish` | Big Fish | 后续迁移 |
| `module-runtime*` (3 crates) | 通用运行时 | **建议合并为单 crate** |
| `module-combat / npc / inventory` | 战斗系统 | **建议合并** |
| `spacetime-module` + `spacetime-client` | SpacetimeDB 接入 | 保持 |
| `shared-contracts / kernel / logging` | 共享基础设施 | 保持 |
---
> **文档结束**
> 本计划书由 Genarrative 架构分析报告衍生,所有改进项均基于对当前项目代码库的实际分析。执行过程中如遇阻力或新发现,应及时更新本计划书并同步相关方。
File diff suppressed because it is too large Load Diff
@@ -93,6 +93,8 @@ npm run dev:admin-web
`npm run dev:api-server` 会保留终端实时输出,并把同一份输出持久化到 `logs/api-server/api-server-<timestamp>.log`。完整联调入口 `npm run dev` 启动的 Rust `api-server` 使用同一套日志规则。如需改写路径,可设置 `GENARRATIVE_API_SERVER_LOG_FILE`;如只改目录,可设置 `GENARRATIVE_API_SERVER_LOG_DIR`
开发态 `npm run dev` / `npm run dev:api-server` 默认打开 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true`,密码入口可以直接注册未知手机号账号;生产默认仍关闭该开关。
查看本地 Rust/SpacetimeDB 日志:
```bash
@@ -112,6 +114,24 @@ SpacetimeDB bindings 生成:
npm run spacetime:generate
```
CodeGraph 本地语义索引:
```bash
npm run codegraph:init
npm run codegraph:status
npm run codegraph:sync
npm run codegraph:index
```
`.codegraph/config.json` 可随仓库共享;`.codegraph/codegraph.db`、缓存和日志为本机生成物,不提交。
Codex 项目级 hook 保存在 `.codex/config.toml``.codex/hooks/`
- `PreToolUse` hook`node .codex/hooks/pre-submit-compile-check.mjs`Codex 准备执行 `git commit` 前检查 `npm run typecheck``npm run admin-web:typecheck``cargo check -p api-server --manifest-path server-rs/Cargo.toml`
- `PostToolUse` hook`node .codex/hooks/post-edit-codegraph-sync.mjs`,工具修改文件后执行 `npm run codegraph:sync`
个人 token、模型路由、MCP server 仍属于个人环境;需要时由成员本机执行 `codegraph install` 或查看 `codegraph install --print-config codex`,不要提交个人全局配置。
## 常用检查命令
- 后端通用用户行为埋点统一通过 `record_tracking_event_and_return` procedure、`SpacetimeRuntimeClient::record_tracking_event(...)` 与 api-server `tracking` 中间件写入 `tracking_event` / `tracking_daily_stat`;后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 默认排除;作品级游玩埋点统一使用 `work_play_start`,详细事件清单见 `docs/technical/BACKEND_TRACKING_EVENT_COVERAGE_2026-05-09.md`
File diff suppressed because it is too large Load Diff
@@ -10,6 +10,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A
- RPG / 自定义世界创作与运行时。
- 拼图玩法创作、草稿、发布、运行态和排行榜。
- 敲木鱼玩法创作、草稿、发布、运行态、公开详情和分享码。
- 抓大鹅 Match3D 创作、2D 多视角素材生成、发布和运行态。
- 大鱼吃小鱼、方洞挑战、视觉小说、汪汪声浪和儿童向寓教于乐玩法。
- 账号、短信 / 密码 / 微信登录、个人资料、任务、钱包、邀请码、充值、反馈、法律信息和后台管理。
File diff suppressed because it is too large Load Diff
+4
View File
@@ -26,6 +26,10 @@ Use the default canonical triage labels: `needs-triage`, `needs-info`, `ready-fo
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 的,不允许进入编码。
## 项目约束
- 代码需要有完善的中文注释
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
+40
View File
@@ -2,8 +2,48 @@
Genarrative 是一个 AI 原生互动内容与小游戏平台,当前上下文记录团队在玩法、作品、运行态和平台闭环中使用的领域语言。
## 平台创作工具
**表单/图片输入创作工作台**:
新增玩法默认采用的创作工具模式,用户通过结构化表单、图片槽位和配置控件提交创作输入,链路覆盖入口、工作台、生成页、结果页、试玩、发布和运行态闭环。
_Avoid_: 默认对话式 Agent 工作台、默认轻输入 Agent 工作台、复制既有玩法工作台
**单图资产编辑**:
角色形象、UI 背景、容器、封面、分享图等单张图资产的统一输入与重生成方式,统一通过 `CreativeImageInputPanel` 表达上传、AI 重绘、参考图、历史图和删除确认。
_Avoid_: 在玩法页面内手写上传、参考图、重绘、预览、删除确认
**系列素材图集生成**:
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
## Language
### Wooden Fish
**敲木鱼**:
轻量点击型互动玩法,玩家在单次运行中点击非功能区敲击中央物品,触发敲击音效、敲击动画、随机飘字和本次运行内的词条计数。
_Avoid_: 长期功德账本、排行榜玩法、全局账户累计
**敲击物图案**:
敲木鱼作品中被玩家点击敲击的单张物品图案;默认模板使用内置透明 PNG `/wooden-fish/default-hit-object.png`,用户自定义关键词或上传图时再使用 image2 生成最终资产,上传图只作为 image2 参考。
_Avoid_: 直接把上传图作为运行态素材、系列素材图集
**敲木鱼背景环境图**:
敲木鱼作品中的竖屏 9:16 背景资产;由后端在敲击物图案生成后,以新敲击物图案作为主题和画风参考,再结合用户原始题材关键词或参考图主题调用 image2 生成。背景只适配敲击物主题和画风,不包含敲击物本体或木槌互动物品。
_Avoid_: 把背景当封面图、在背景里重复绘制敲击物、让前端临时拼背景
**敲击音效**:
敲木鱼作品中每次有效敲击播放的短音频资产,可由描述生成、文件上传或麦克风录制产生,最终统一写回作品的敲击音效资产槽位。
_Avoid_: 背景音乐、长音频轨道、运行态实时录音
**飘字**:
每次有效敲击后从作品配置中等概率抽取词条,并在敲击物上方以“词条+1”短暂漂浮显示的文本;配置里只保存幸运、健康、财富、姻缘、幸福、事业、成功、功德等词条名本身。
_Avoid_: 带权重奖励、账户属性、可结算货币
**单次 run 计数**:
敲木鱼运行态只在当前 run 内累计总敲击次数和已出现飘字词条计数,run 结束后作为摘要保存,不形成账号级长期账本。
_Avoid_: 用户永久功德值、跨作品累计值、排行榜积分
### Bark Battle
**汪汪声浪大作战**:
+6
View File
@@ -157,6 +157,9 @@ export interface AdminCreationEntryTypeConfigPayload {
visible: boolean;
open: boolean;
sortOrder: number;
categoryId: string;
categoryLabel: string;
categorySortOrder: number;
updatedAtMicros: number;
}
@@ -169,6 +172,9 @@ export interface AdminUpsertCreationEntryTypeConfigRequest {
visible: boolean;
open: boolean;
sortOrder: number;
categoryId: string;
categoryLabel: string;
categorySortOrder: number;
}
export interface AdminUpsertProfileRedeemCodeRequest {
@@ -27,6 +27,9 @@ export function AdminCreationEntrySwitchPage({
const [visible, setVisible] = useState(true);
const [open, setOpen] = useState(true);
const [sortOrder, setSortOrder] = useState('30');
const [categoryId, setCategoryId] = useState('recent');
const [categoryLabel, setCategoryLabel] = useState('最近创作');
const [categorySortOrder, setCategorySortOrder] = useState('10');
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [listErrorMessage, setListErrorMessage] = useState('');
@@ -82,6 +85,9 @@ export function AdminCreationEntrySwitchPage({
visible,
open,
sortOrder: parseInteger(sortOrder),
categoryId: categoryId.trim(),
categoryLabel: categoryLabel.trim(),
categorySortOrder: parseInteger(categorySortOrder),
});
const nextEntries = sortEntries(response.entries);
setEntries(nextEntries);
@@ -105,6 +111,9 @@ export function AdminCreationEntrySwitchPage({
setVisible(entry.visible);
setOpen(entry.open);
setSortOrder(String(entry.sortOrder));
setCategoryId(entry.categoryId);
setCategoryLabel(entry.categoryLabel);
setCategorySortOrder(String(entry.categorySortOrder));
}
return (
@@ -189,6 +198,32 @@ export function AdminCreationEntrySwitchPage({
/>
</label>
<div className="admin-form-row">
<label className="admin-field">
<span> ID</span>
<input
value={categoryId}
onChange={(event) => setCategoryId(event.target.value)}
/>
</label>
<label className="admin-field">
<span></span>
<input
value={categoryLabel}
onChange={(event) => setCategoryLabel(event.target.value)}
/>
</label>
</div>
<label className="admin-field">
<span></span>
<input
inputMode="numeric"
value={categorySortOrder}
onChange={(event) => setCategorySortOrder(event.target.value)}
/>
</label>
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
@@ -211,6 +246,7 @@ export function AdminCreationEntrySwitchPage({
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
@@ -228,6 +264,7 @@ export function AdminCreationEntrySwitchPage({
</td>
<td>{entry.visible ? '是' : '否'}</td>
<td>{entry.open ? '是' : '否'}</td>
<td>{entry.categoryLabel || entry.categoryId}</td>
<td>{entry.sortOrder}</td>
</tr>
))}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -170,6 +170,8 @@ http {
location ~ ^/api(?:/|$) {
default_type application/json;
# 中文注释:创作接口会携带参考图 Data URLNginx 只放行到 api-server;真实大小限制仍由路由 DefaultBodyLimit 和业务字节校验负责。
client_max_body_size 64m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
+6
View File
@@ -2,6 +2,12 @@
本配置片段由 `scripts/jenkins-server-provision.sh` 在安装 Nginx 站点配置时展开。
## 请求体大小
- 生产、开发服和容器模板都在通用 `location ~ ^/api(?:/|$)` 内设置 `client_max_body_size 64m`
- 该值只用于让携带参考图 Data URL 的创作接口抵达 `api-server`;不要把它当作业务上传上限。Rust 路由仍通过 `DefaultBodyLimit` 和解码后字节校验限制具体接口,例如拼图参考图路由只放宽到 12 MiB 请求体,图片字节继续按业务规则拒绝。
- 若线上看到 `413 Request Entity Too Large`,并且 access log 里 `request_time=0.000 upstream_status=-`,通常是 Nginx 没有加载该模板或未 reload;先执行 `nginx -T | grep client_max_body_size``nginx -t` 再检查 `api-server`
## gzip
- `deploy/nginx/genarrative.conf``deploy/nginx/genarrative-dev-http.conf` 默认开启 gzip。

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