Compare commits
208 Commits
ae28dab032
...
cb6f861790
| Author | SHA1 | Date | |
|---|---|---|---|
| cb6f861790 | |||
| 39f679d1ea | |||
| 06d49c0ad2 | |||
| a0d1cb86f0 | |||
| 31393340e7 | |||
| 705a2d3dd8 | |||
| 79048a8c16 | |||
| 47ef9b9ca6 | |||
| d56031cf4a | |||
| 45898cba4e | |||
| de2c49005f | |||
| 7aabbcc10c | |||
| ea33413187 | |||
| 874e10383b | |||
| 0a0f3f1bd8 | |||
| c4b9b8173f | |||
| c9a59f9edb | |||
| c5d783d3e6 | |||
| 5eb37d595b | |||
| f43295b471 | |||
| 8171fc59b0 | |||
| f68f4914ec | |||
| 8404081d7b | |||
| 7d6963980f | |||
| 1ca799c3e5 | |||
| fc20c39519 | |||
| 4e04679ba4 | |||
| 9cb3c6a27e | |||
| 2ebfd1cf55 | |||
| 1c5f9303a2 | |||
| 6be3afe45a | |||
| 1b2daf4796 | |||
| 8ec19e2364 | |||
| 6f35306d53 | |||
| dbf106c746 | |||
| e99714838f | |||
| 019dd9efba | |||
| 2ebb7bf253 | |||
| 8edfd08095 | |||
| 2549a93379 | |||
| a6029639b0 | |||
| 03acbc5cb1 | |||
| 31f350d499 | |||
| 35c2bce6f1 | |||
| 67062a8af3 | |||
| c06bf84d0a | |||
| f6604ea3b4 | |||
| 0708fd0547 | |||
| 58e5bb24f1 | |||
| 664586393d | |||
| 0940a1945c | |||
| 88bd4f36e9 | |||
| 3aabb59945 | |||
| 75681751c2 | |||
| 6cb3efae61 | |||
| 8b54cc912c | |||
| 929febb4fe | |||
| b355568189 | |||
| 4e1c0462a4 | |||
| 04288d4a40 | |||
| f65177b147 | |||
| 4f369617c7 | |||
| ef53028be5 | |||
| 49a79aee54 | |||
| 5050ce4ff8 | |||
| 13388d333e | |||
| 70b5a7cf73 | |||
| f63331a1b9 | |||
| 2680662003 | |||
| 091accae9b | |||
| 61f999eba9 | |||
| 3528980645 | |||
| 8df502b2a7 | |||
| 53a9cdd791 | |||
| 349a397888 | |||
| 27e84c46a0 | |||
| 9d25a47b23 | |||
| da7c1ff0c5 | |||
| 1e200ec5ba | |||
| cabceb998c | |||
| a6cd9afcbb | |||
| 130f5928a1 | |||
| f6046ef658 | |||
| 9f399ddb89 | |||
| d9040be059 | |||
| 3eb9390e8f | |||
| c79d1b7ded | |||
| 25e35afe09 | |||
| d948889f24 | |||
| 9dc56f0fc0 | |||
| 8530cd0d8e | |||
| 3bf3cba806 | |||
| 000d74d05f | |||
| d0709865bb | |||
| ad0ded5e58 | |||
| e071f03b3d | |||
| 9cb996b80e | |||
| 1223f597d2 | |||
| cd207dc237 | |||
| 84dc92646a | |||
| 76ac9d22a5 | |||
| 9669bd6e07 | |||
| b317c2a8ea | |||
| 8c4a48d18c | |||
| d8716d70b0 | |||
| 0e9c286a57 | |||
| fa373f0575 | |||
| 35958d5942 | |||
| 7e49c3a3c6 | |||
| 6700e99dc8 | |||
| 6e4c941601 | |||
| 468b88b105 | |||
| 81e59f90ce | |||
| fc6519a7b7 | |||
| 2fe0a9083d | |||
| 4c8ba535e4 | |||
| d5627c536d | |||
| 28ba990123 | |||
| 91fb8edee7 | |||
| 0773a0d0ca | |||
| 209e924403 | |||
| 997a8daada | |||
| cf8da3f50f | |||
| 48957311bc | |||
| 89129ef1f4 | |||
| 4372ab5be1 | |||
| d234d27cc0 | |||
| c3c5f1acd7 | |||
| 78dcad1222 | |||
| fcaf7bdb38 | |||
| a83c64133d | |||
| 60852241c9 | |||
| 584a77e572 | |||
| 70dbefda2b | |||
| c23088539e | |||
| 5675c40119 | |||
| 39eb7a513c | |||
| adaf514a1a | |||
| e37163d4d3 | |||
| 854a7a2568 | |||
| 07fcbe4a2f | |||
| 04bff9617d | |||
| 9404c77703 | |||
| f6bf5f665e | |||
| 13bc79306f | |||
| 5a60ab3972 | |||
| 90bc0edf9a | |||
| 84842e9475 | |||
| 5dd7528280 | |||
| 600f1af83a | |||
| eaccad289f | |||
| a1a02eee7d | |||
| c9850c8caf | |||
| fdab91e54d | |||
| 6a560236f4 | |||
| 440d60d563 | |||
| ec04790848 | |||
| 6ada003c78 | |||
| c5db90f37a | |||
| 40d5e4948e | |||
| 0cbda6aa7c | |||
| f7663076a5 | |||
| cb069de32e | |||
| 0ac5606a41 | |||
| f3b36f15b5 | |||
| 6ece1f41ee | |||
| 9f4a51843b | |||
| 4b549ce6a7 | |||
| b8df838eb2 | |||
| 17a033ecc7 | |||
| 41d9d88757 | |||
| 5f34e4da41 | |||
| 4259e3ef5c | |||
| 6d26cfdc94 | |||
| 7c790377ab | |||
| 9f6fd5ed1e | |||
| f6258ed7c2 | |||
| aa9efd3bbe | |||
| df8ef351bb | |||
| 953bc5e06f | |||
| 839cd41bd6 | |||
| 4ad172637a | |||
| 6a0415fbb5 | |||
| 8e757f267e | |||
| 3fd86bd1d2 | |||
| 9fba8a3191 | |||
| d13c70f7fc | |||
| 60f25f3aa5 | |||
| 6ce9a4175a | |||
| e12037740c | |||
| c16a554934 | |||
| 7c3a7fc394 | |||
| 03aa828427 | |||
| 194e26bf84 | |||
| 767d8c399c | |||
| fb139a7843 | |||
| f6dbf43802 | |||
| fc2e9132a5 | |||
| ae4ad46c65 | |||
| 682df9f8b5 | |||
| 5e88d40812 | |||
| d2ad81d38c | |||
| be5a1b51ed | |||
| 0e454b889b | |||
| e6d7df1ecb | |||
| 1d020686b1 | |||
| 7c844854c0 | |||
| adc57ba49b |
6
.codex/environments/environment.toml
Normal file
6
.codex/environments/environment.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "Genarrative"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
229
.codex/skills/spacetimedb-cli/SKILL.md
Normal file
229
.codex/skills/spacetimedb-cli/SKILL.md
Normal file
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: spacetimedb-cli
|
||||
description: SpacetimeDB CLI reference for initializing projects, building modules, publishing databases, querying data, and managing servers
|
||||
triggers:
|
||||
- spacetime init
|
||||
- spacetime build
|
||||
- spacetime publish
|
||||
- spacetime dev
|
||||
- spacetime sql
|
||||
- spacetime call
|
||||
- spacetime logs
|
||||
- spacetime server
|
||||
- spacetime login
|
||||
- spacetime generate
|
||||
- how do I use the CLI
|
||||
- CLI command
|
||||
---
|
||||
|
||||
# SpacetimeDB CLI
|
||||
|
||||
Use this skill when the user needs help with the `spacetime` CLI tool - initializing projects, building modules, publishing databases, querying data, managing servers, or troubleshooting CLI issues.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Project Initialization & Development
|
||||
|
||||
```bash
|
||||
# Initialize new project
|
||||
spacetime init my-project --lang rust|csharp|typescript|cpp
|
||||
spacetime init my-project --template <template-id>
|
||||
|
||||
# Build module
|
||||
spacetime build # release build
|
||||
spacetime build --debug # faster iteration, slower runtime
|
||||
|
||||
# Dev mode (auto-rebuild, auto-publish, generates bindings)
|
||||
spacetime dev
|
||||
spacetime dev --client-lang typescript --module-bindings-path ./client/src/module_bindings
|
||||
|
||||
# Generate client bindings
|
||||
spacetime generate --lang typescript|csharp|rust|unrealcpp --out-dir ./bindings --module-path ./server
|
||||
```
|
||||
|
||||
### Publishing & Deployment
|
||||
|
||||
```bash
|
||||
# Publish to Maincloud (default)
|
||||
spacetime publish my-database --yes
|
||||
|
||||
# Publish to local server
|
||||
spacetime publish my-database --server local --yes
|
||||
|
||||
# Clear database and republish
|
||||
spacetime publish my-database --clear-database --yes
|
||||
```
|
||||
|
||||
### Database Interaction
|
||||
|
||||
```bash
|
||||
# SQL queries
|
||||
spacetime sql my-database "SELECT * FROM users"
|
||||
spacetime sql my-database --interactive # REPL mode
|
||||
|
||||
# Call reducers
|
||||
spacetime call my-database my_reducer '{"arg1": "value", "arg2": 123}'
|
||||
|
||||
# Subscribe to changes
|
||||
spacetime subscribe my-database "SELECT * FROM users" --num-updates 10
|
||||
|
||||
# View logs
|
||||
spacetime logs my-database -f # follow logs
|
||||
spacetime logs my-database -n 100 # up to 100 log lines
|
||||
|
||||
# Describe schema
|
||||
spacetime describe my-database --json
|
||||
spacetime describe my-database table users --json
|
||||
spacetime describe my-database reducer my_reducer --json
|
||||
```
|
||||
|
||||
### Database Management
|
||||
|
||||
```bash
|
||||
# List databases
|
||||
spacetime list
|
||||
|
||||
# Delete database
|
||||
spacetime delete my-database
|
||||
|
||||
# Rename database
|
||||
spacetime rename <database-identity> --to new-name
|
||||
```
|
||||
|
||||
### Server Management
|
||||
|
||||
```bash
|
||||
# List configured servers
|
||||
spacetime server list
|
||||
|
||||
# Add server
|
||||
spacetime server add local --url http://localhost:3000 --default
|
||||
spacetime server add myserver --url https://my-spacetime.example.com
|
||||
|
||||
# Set default server
|
||||
spacetime server set-default local
|
||||
|
||||
# Test connectivity
|
||||
spacetime server ping local
|
||||
|
||||
# Start local instance
|
||||
spacetime start
|
||||
|
||||
# Clear local data
|
||||
spacetime server clear
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
# Login (opens browser)
|
||||
spacetime login
|
||||
|
||||
# Login with token
|
||||
spacetime login --token <token>
|
||||
|
||||
# Show login status
|
||||
spacetime login show
|
||||
|
||||
# Logout
|
||||
spacetime logout
|
||||
```
|
||||
|
||||
## Default Servers
|
||||
|
||||
| Name | URL | Description |
|
||||
|------|-----|-------------|
|
||||
| `maincloud` | `https://maincloud.spacetimedb.com` | Production cloud (default) |
|
||||
| `local` | `http://127.0.0.1:3000` | Local development server |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### New Project Setup
|
||||
|
||||
```bash
|
||||
# 1. Login
|
||||
spacetime login
|
||||
|
||||
# 2. Create project
|
||||
spacetime init my-game --lang rust
|
||||
cd my-game
|
||||
|
||||
# 3. Start dev mode (auto-rebuilds and publishes)
|
||||
spacetime dev
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Start local server (in separate terminal)
|
||||
spacetime start
|
||||
|
||||
# Publish to local
|
||||
spacetime publish my-db --server local --clear-database --yes
|
||||
|
||||
# Query local database
|
||||
spacetime sql my-db --server local "SELECT * FROM players"
|
||||
```
|
||||
|
||||
### Generate Client Bindings
|
||||
|
||||
```bash
|
||||
# After building module
|
||||
spacetime build
|
||||
spacetime generate --lang typescript --out-dir ./client/src/bindings --module-path .
|
||||
|
||||
# Or use dev mode which auto-generates
|
||||
spacetime dev --client-lang typescript --module-bindings-path ./client/src/bindings
|
||||
```
|
||||
|
||||
## Common Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
|------|-------|-------------|
|
||||
| `--server` | `-s` | Target server (nickname, hostname, or URL) |
|
||||
| `--yes` | `-y` | Non-interactive mode (skip confirmations) |
|
||||
| `--anonymous` | | Use anonymous identity |
|
||||
| `--module-path` | `-p` | Path to module project |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Not logged in"
|
||||
```bash
|
||||
spacetime login
|
||||
# Or use --anonymous for public operations
|
||||
```
|
||||
|
||||
### "Server not responding"
|
||||
```bash
|
||||
spacetime server ping <server>
|
||||
# For local: ensure spacetime start is running
|
||||
```
|
||||
|
||||
### "Schema conflict"
|
||||
```bash
|
||||
# Clear data and republish
|
||||
spacetime publish my-db --clear-database --yes
|
||||
# Clear data and republish only when conflict
|
||||
spacetime publish my-db --clear-database=on-conflict --yes
|
||||
```
|
||||
|
||||
### "Build failed"
|
||||
```bash
|
||||
# Check Rust/C# toolchain
|
||||
rustup show
|
||||
# For Rust modules, ensure wasm32-unknown-unknown target
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
## Module Languages
|
||||
|
||||
**Server-side (modules):** Rust, C#, TypeScript, C++
|
||||
**Client SDKs:** TypeScript, C#, Rust, Python, Unreal Engine
|
||||
**CLI `generate` targets:** TypeScript, C#, Rust, Unreal C++
|
||||
|
||||
## Notes
|
||||
|
||||
- Many commands are marked UNSTABLE and may change
|
||||
- Default server is `maincloud` unless configured otherwise
|
||||
- Use `--yes` flag in scripts to avoid interactive prompts
|
||||
- Dev mode watches files and auto-rebuilds on changes
|
||||
345
.codex/skills/spacetimedb-concepts/SKILL.md
Normal file
345
.codex/skills/spacetimedb-concepts/SKILL.md
Normal file
@@ -0,0 +1,345 @@
|
||||
---
|
||||
name: spacetimedb-concepts
|
||||
description: Understand SpacetimeDB architecture and core concepts. Use when learning SpacetimeDB or making architectural decisions.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: clockworklabs
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# SpacetimeDB Core Concepts
|
||||
|
||||
SpacetimeDB is a relational database that is also a server. It lets you upload application logic directly into the database via WebAssembly modules, eliminating the traditional web/game server layer entirely.
|
||||
|
||||
---
|
||||
|
||||
## Critical Rules (Read First)
|
||||
|
||||
These five rules prevent the most common SpacetimeDB mistakes:
|
||||
|
||||
1. **Reducers are transactional** — they do not return data to callers. Use subscriptions to read data.
|
||||
2. **Reducers must be deterministic** — no filesystem, network, timers, or random. All state must come from tables.
|
||||
3. **Read data via tables/subscriptions** — not reducer return values. Clients get data through subscribed queries.
|
||||
4. **Auto-increment IDs are not sequential** — gaps are normal, do not use for ordering. Use timestamps or explicit sequence columns.
|
||||
5. **`ctx.sender()` is the authenticated principal** — never trust identity passed as arguments. Always use `ctx.sender()` for authorization.
|
||||
|
||||
---
|
||||
|
||||
## Feature Implementation Checklist
|
||||
|
||||
When implementing a feature that spans backend and client:
|
||||
|
||||
1. **Backend:** Define table(s) to store the data
|
||||
2. **Backend:** Define reducer(s) to mutate the data
|
||||
3. **Client:** Subscribe to the table(s)
|
||||
4. **Client:** Call the reducer(s) from UI — **do not skip this step**
|
||||
5. **Client:** Render the data from the table(s)
|
||||
|
||||
**Common mistake:** Building backend tables/reducers but forgetting to wire up the client to call them.
|
||||
|
||||
---
|
||||
|
||||
## Debugging Checklist
|
||||
|
||||
When things are not working:
|
||||
|
||||
1. Is SpacetimeDB server running? (`spacetime start`)
|
||||
2. Is the module published? (`spacetime publish`)
|
||||
3. Are client bindings generated? (`spacetime generate`)
|
||||
4. Check server logs for errors (`spacetime logs <db-name>`)
|
||||
5. **Is the reducer actually being called from the client?**
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
spacetime start
|
||||
spacetime publish <db-name> --module-path <module-path>
|
||||
spacetime publish <db-name> --clear-database -y --module-path <module-path>
|
||||
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>
|
||||
spacetime logs <db-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What SpacetimeDB Is
|
||||
|
||||
SpacetimeDB combines a database and application server into a single deployable unit. Clients connect directly to the database and execute application logic inside it. The system is optimized for real-time applications requiring maximum speed and minimum latency.
|
||||
|
||||
Key characteristics:
|
||||
|
||||
- **In-memory execution**: Application state is served from memory for very low-latency access
|
||||
- **Persistent storage**: Data is automatically persisted to a write-ahead log (WAL) for durability
|
||||
- **Real-time synchronization**: Changes are automatically pushed to subscribed clients
|
||||
- **Single deployment**: No separate servers, containers, or infrastructure to manage
|
||||
|
||||
## The Five Zen Principles
|
||||
|
||||
1. **Everything is a Table**: Your entire application state lives in tables. No separate cache layer, no Redis, no in-memory state to synchronize.
|
||||
2. **Everything is Persistent**: SpacetimeDB persists state by default (for example via WAL-backed durability).
|
||||
3. **Everything is Real-Time**: Clients are replicas of server state. Subscribe to data and it flows automatically.
|
||||
4. **Everything is Transactional**: Every reducer runs atomically. Either all changes succeed or all roll back.
|
||||
5. **Everything is Programmable**: Modules are real code (Rust, C#, TypeScript) running inside the database.
|
||||
|
||||
## Tables
|
||||
|
||||
Tables store all data in SpacetimeDB. They use the relational model and support SQL queries for subscriptions.
|
||||
|
||||
### Defining Tables
|
||||
|
||||
Tables are defined using language-specific attributes. In 2.0, use `accessor` (not `name`) for the API name:
|
||||
|
||||
**Rust:**
|
||||
```rust
|
||||
#[spacetimedb::table(accessor = player, public)]
|
||||
pub struct Player {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
id: u32,
|
||||
#[index(btree)]
|
||||
name: String,
|
||||
#[unique]
|
||||
email: String,
|
||||
}
|
||||
```
|
||||
|
||||
**C#:**
|
||||
```csharp
|
||||
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
|
||||
public partial struct Player
|
||||
{
|
||||
[SpacetimeDB.PrimaryKey]
|
||||
[SpacetimeDB.AutoInc]
|
||||
public uint Id;
|
||||
[SpacetimeDB.Index.BTree]
|
||||
public string Name;
|
||||
[SpacetimeDB.Unique]
|
||||
public string Email;
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const players = table(
|
||||
{ name: 'players', public: true },
|
||||
{
|
||||
id: t.u32().primaryKey().autoInc(),
|
||||
name: t.string().index('btree'),
|
||||
email: t.string().unique(),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Table Visibility
|
||||
|
||||
- **Private tables** (default): Only accessible by reducers and the database owner
|
||||
- **Public tables**: Exposed for client read access through subscriptions. Writes still require reducers.
|
||||
|
||||
### Table Design Principles
|
||||
|
||||
Organize data by access pattern, not by entity:
|
||||
|
||||
**Decomposed approach (recommended):**
|
||||
```
|
||||
Player PlayerState PlayerStats
|
||||
id <-- player_id player_id
|
||||
name position_x total_kills
|
||||
position_y total_deaths
|
||||
velocity_x play_time
|
||||
```
|
||||
|
||||
Benefits: reduced bandwidth, cache efficiency, schema evolution, semantic clarity.
|
||||
|
||||
## Reducers
|
||||
|
||||
Reducers are transactional functions that modify database state. They are the primary client-invoked mutation path; procedures can also mutate tables by running explicit transactions.
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **Transactional**: Run in isolated database transactions
|
||||
- **Atomic**: Either all changes succeed or all roll back
|
||||
- **Isolated**: Cannot interact with the outside world (no network, no filesystem)
|
||||
- **Callable**: Clients invoke reducers as remote procedure calls
|
||||
|
||||
### Critical Reducer Rules
|
||||
|
||||
1. **No global state**: Relying on static variables is undefined behavior
|
||||
2. **No side effects**: Reducers cannot make network requests or access files
|
||||
3. **Store state in tables**: All persistent state must be in tables
|
||||
4. **No return data**: Reducers do not return data to callers — use subscriptions
|
||||
5. **Must be deterministic**: No random, no timers, no external I/O
|
||||
|
||||
### Defining Reducers
|
||||
|
||||
**Rust:**
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {
|
||||
if name.is_empty() {
|
||||
return Err("Name cannot be empty".to_string());
|
||||
}
|
||||
ctx.db.user().insert(User { id: 0, name, email });
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**C#:**
|
||||
```csharp
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void CreateUser(ReducerContext ctx, string name, string email)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Name cannot be empty");
|
||||
ctx.Db.User.Insert(new User { Id = 0, Name = name, Email = email });
|
||||
}
|
||||
```
|
||||
|
||||
### ReducerContext
|
||||
|
||||
Every reducer receives a `ReducerContext` providing:
|
||||
- **Database**: `ctx.db` (Rust field, TS property) / `ctx.Db` (C# property)
|
||||
- **Sender**: `ctx.sender()` (Rust method) / `ctx.Sender` (C# property) / `ctx.sender` (TS property)
|
||||
- **Connection ID**: `ctx.connection_id()` (Rust method) / `ctx.ConnectionId` (C# property) / `ctx.connectionId` (TS property)
|
||||
- **Timestamp**: `ctx.timestamp` (Rust field, TS property) / `ctx.Timestamp` (C# property)
|
||||
|
||||
## Event Tables (2.0)
|
||||
|
||||
Event tables are the preferred way to broadcast reducer-specific data to clients.
|
||||
|
||||
```rust
|
||||
#[table(accessor = damage_event, public, event)]
|
||||
pub struct DamageEvent {
|
||||
pub target: Identity,
|
||||
pub amount: u32,
|
||||
}
|
||||
|
||||
#[reducer]
|
||||
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
|
||||
ctx.db.damage_event().insert(DamageEvent { target, amount });
|
||||
}
|
||||
```
|
||||
|
||||
Clients subscribe to event tables and use `on_insert` callbacks. Event tables must be subscribed explicitly and are excluded from `subscribe_to_all_tables()`.
|
||||
|
||||
## Subscriptions
|
||||
|
||||
Subscriptions replicate database rows to clients in real-time.
|
||||
|
||||
### How Subscriptions Work
|
||||
|
||||
1. **Subscribe**: Register SQL queries describing needed data
|
||||
2. **Receive initial data**: All matching rows are sent immediately
|
||||
3. **Receive updates**: Real-time updates when subscribed rows change
|
||||
4. **React to changes**: Use callbacks (`onInsert`, `onDelete`, `onUpdate`)
|
||||
|
||||
### Subscription Best Practices
|
||||
|
||||
1. **Group subscriptions by lifetime**: Keep always-needed data separate from temporary subscriptions
|
||||
2. **Subscribe before unsubscribing**: When updating subscriptions, subscribe to new data first
|
||||
3. **Avoid overlapping queries**: Distinct queries returning overlapping data cause redundant processing
|
||||
4. **Use indexes**: Queries on indexed columns are efficient; full table scans are expensive
|
||||
|
||||
## Modules
|
||||
|
||||
Modules are WebAssembly bundles containing application logic that runs inside the database.
|
||||
|
||||
### Module Components
|
||||
|
||||
- **Tables**: Define the data schema
|
||||
- **Reducers**: Define callable functions that modify state
|
||||
- **Views**: Define read-only computed queries
|
||||
- **Event Tables**: Broadcast reducer-specific data to clients (2.0)
|
||||
- **Procedures**: (Beta) Functions that can have side effects (HTTP requests)
|
||||
|
||||
### Module Languages
|
||||
|
||||
Server-side modules can be written in: Rust, C#, TypeScript (beta)
|
||||
|
||||
### Module Lifecycle
|
||||
|
||||
1. **Write**: Define tables and reducers in your chosen language
|
||||
2. **Compile**: Build to WebAssembly using the SpacetimeDB CLI
|
||||
3. **Publish**: Upload to a SpacetimeDB host with `spacetime publish`
|
||||
4. **Hot-swap**: Republish to update code without disconnecting clients
|
||||
|
||||
## Identity
|
||||
|
||||
Identity is SpacetimeDB's authentication system based on OpenID Connect (OIDC).
|
||||
|
||||
- **Identity**: A long-lived, globally unique identifier for a user.
|
||||
- **ConnectionId**: Identifies a specific client connection.
|
||||
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
pub fn do_something(ctx: &ReducerContext) {
|
||||
let caller_identity = ctx.sender(); // Who is calling?
|
||||
// NEVER trust identity passed as a reducer argument
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Providers
|
||||
|
||||
SpacetimeDB works with many OIDC providers, including SpacetimeAuth (built-in), Auth0, Clerk, Keycloak, Google, and GitHub.
|
||||
|
||||
## When to Use SpacetimeDB
|
||||
|
||||
### Ideal Use Cases
|
||||
|
||||
- **Real-time games**: MMOs, multiplayer games, turn-based games
|
||||
- **Collaborative applications**: Document editing, whiteboards, design tools
|
||||
- **Chat and messaging**: Real-time communication with presence
|
||||
- **Live dashboards**: Streaming analytics and monitoring
|
||||
|
||||
### Key Decision Factors
|
||||
|
||||
Choose SpacetimeDB when you need:
|
||||
- Sub-10ms latency for reads and writes
|
||||
- Automatic real-time synchronization
|
||||
- Transactional guarantees for all operations
|
||||
- Simplified architecture (no separate cache, queue, or server)
|
||||
|
||||
### Less Suitable For
|
||||
|
||||
- **Batch analytics**: Optimized for OLTP, not OLAP
|
||||
- **Large blob storage**: Better suited for structured relational data
|
||||
- **Stateless APIs**: Traditional REST APIs do not need real-time sync
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Authentication check in reducer:**
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
|
||||
let admin = ctx.db.admin().identity().find(&ctx.sender())
|
||||
.ok_or("Not an admin")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Scheduled reducer:**
|
||||
```rust
|
||||
#[spacetimedb::table(accessor = reminder, scheduled(send_reminder))]
|
||||
pub struct Reminder {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
id: u64,
|
||||
scheduled_at: ScheduleAt,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {
|
||||
log::info!("Reminder: {}", reminder.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Editing Behavior
|
||||
|
||||
When modifying SpacetimeDB code:
|
||||
|
||||
- Make the smallest change necessary
|
||||
- Do NOT touch unrelated files, configs, or dependencies
|
||||
- Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo
|
||||
646
.codex/skills/spacetimedb-csharp/SKILL.md
Normal file
646
.codex/skills/spacetimedb-csharp/SKILL.md
Normal file
@@ -0,0 +1,646 @@
|
||||
---
|
||||
name: spacetimedb-csharp
|
||||
description: Build C# modules and clients for SpacetimeDB. Covers server-side module development and client SDK integration.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: clockworklabs
|
||||
version: "2.0"
|
||||
tested_with: "SpacetimeDB 2.0, .NET 8 SDK"
|
||||
---
|
||||
|
||||
# SpacetimeDB C# SDK
|
||||
|
||||
This skill provides guidance for building C# server-side modules and C# clients that connect to SpacetimeDB 2.0.
|
||||
|
||||
---
|
||||
|
||||
## HALLUCINATED APIs — DO NOT USE
|
||||
|
||||
**These APIs DO NOT EXIST. LLMs frequently hallucinate them.**
|
||||
|
||||
```csharp
|
||||
// WRONG — these table access patterns do not exist
|
||||
ctx.db.tableName // Wrong casing — use ctx.Db
|
||||
ctx.Db.tableName // Wrong casing — accessor must match exactly
|
||||
ctx.Db.TableName.Get(id) // Use Find, not Get
|
||||
ctx.Db.TableName.FindById(id) // Use index accessor: ctx.Db.TableName.Id.Find(id)
|
||||
ctx.Db.table.field_name.Find(x) // Wrong! Use PascalCase: ctx.Db.Table.FieldName.Find(x)
|
||||
Optional<string> field; // Use C# nullable: string? field
|
||||
|
||||
// WRONG — missing partial keyword
|
||||
public struct MyTable { } // Must be "partial struct"
|
||||
public class Module { } // Must be "static partial class"
|
||||
|
||||
// WRONG — non-partial types
|
||||
[SpacetimeDB.Table(Accessor = "Player")]
|
||||
public struct Player { } // WRONG — missing partial!
|
||||
|
||||
// WRONG — sum type syntax (VERY COMMON MISTAKE)
|
||||
public partial struct Shape : TaggedEnum<(Circle, Rectangle)> { } // WRONG: struct, missing names
|
||||
public partial record Shape : TaggedEnum<(Circle, Rectangle)> { } // WRONG: missing variant names
|
||||
public partial class Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { } // WRONG: class
|
||||
|
||||
// WRONG — Index attribute without full qualification
|
||||
[Index.BTree(Accessor = "idx", Columns = new[] { "Col" })] // Ambiguous with System.Index!
|
||||
[SpacetimeDB.Index.BTree(Accessor = "idx", Columns = ["Col"])] // Valid with modern C# collection expressions
|
||||
|
||||
// WRONG — old 1.0 patterns
|
||||
[SpacetimeDB.Table(Name = "Player")] // Use Accessor, not Name (2.0)
|
||||
<PackageReference Include="SpacetimeDB.ServerSdk" /> // Use SpacetimeDB.Runtime
|
||||
.WithModuleName("my-db") // Use .WithDatabaseName() (2.0)
|
||||
ScheduleAt.Time(futureTime) // Use new ScheduleAt.Time(futureTime)
|
||||
|
||||
// WRONG — lifecycle hooks starting with "On"
|
||||
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
|
||||
public static void OnClientConnected(ReducerContext ctx) { } // STDB0010 error!
|
||||
|
||||
// WRONG — non-deterministic code in reducers
|
||||
var random = new Random(); // Use ctx.Rng
|
||||
var guid = Guid.NewGuid(); // Not allowed
|
||||
var now = DateTime.Now; // Use ctx.Timestamp
|
||||
|
||||
// WRONG — collection parameters
|
||||
int[] itemIds = { 1, 2, 3 };
|
||||
_conn.Reducers.ProcessItems(itemIds); // Generated code expects List<T>!
|
||||
```
|
||||
|
||||
### CORRECT PATTERNS
|
||||
|
||||
```csharp
|
||||
using SpacetimeDB;
|
||||
|
||||
// CORRECT TABLE — must be partial struct, use Accessor
|
||||
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
|
||||
public partial struct Player
|
||||
{
|
||||
[SpacetimeDB.PrimaryKey]
|
||||
[SpacetimeDB.AutoInc]
|
||||
public ulong Id;
|
||||
[SpacetimeDB.Index.BTree]
|
||||
public Identity OwnerId;
|
||||
public string Name;
|
||||
}
|
||||
|
||||
// CORRECT MODULE — must be static partial class
|
||||
public static partial class Module
|
||||
{
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void CreatePlayer(ReducerContext ctx, string name)
|
||||
{
|
||||
ctx.Db.Player.Insert(new Player { Id = 0, OwnerId = ctx.Sender, Name = name });
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT DATABASE ACCESS — PascalCase, index-based lookups
|
||||
var player = ctx.Db.Player.Id.Find(playerId); // Unique/PK: returns nullable
|
||||
foreach (var p in ctx.Db.Player.OwnerId.Filter(ctx.Sender)) { } // BTree: returns IEnumerable
|
||||
|
||||
// CORRECT SUM TYPE — partial record with named tuple elements
|
||||
[SpacetimeDB.Type]
|
||||
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
|
||||
|
||||
// CORRECT — collection parameters use List<T>
|
||||
_conn.Reducers.ProcessItems(new List<int> { 1, 2, 3 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes Table
|
||||
|
||||
| Wrong | Right | Error |
|
||||
|-------|-------|-------|
|
||||
| Wrong .csproj name | `StdbModule.csproj` | Publish fails silently |
|
||||
| .NET 9 SDK | .NET 8 SDK only | WASI compilation fails |
|
||||
| Missing WASI workload | `dotnet workload install wasi-experimental` | Build fails |
|
||||
| async/await in reducers | Synchronous only | Not supported |
|
||||
| `table.Name.Update(...)` | `table.Id.Update(...)` | Update only via primary key (2.0) |
|
||||
| Not calling `FrameTick()` | `conn.FrameTick()` in Update loop | No callbacks fire |
|
||||
| Accessing `conn.Db` from background thread | Copy data in callback | Data races |
|
||||
|
||||
---
|
||||
|
||||
## Hard Requirements
|
||||
|
||||
1. **Tables and Module MUST be `partial`** — required for code generation
|
||||
2. **Use `Accessor =` in table attributes** — `Name =` is only for SQL compatibility (2.0)
|
||||
3. **Project file MUST be named `StdbModule.csproj`** — CLI requirement
|
||||
4. **Requires .NET 8 SDK** — .NET 9 and newer not yet supported
|
||||
5. **Install WASI workload** — `dotnet workload install wasi-experimental`
|
||||
6. **Procedures are supported** — use `[SpacetimeDB.Procedure]` with `ProcedureContext` when needed
|
||||
7. **Reducers must be deterministic** — no filesystem, network, timers, or `Random`
|
||||
8. **Add `Public = true`** — if clients need to subscribe to a table
|
||||
9. **Use `T?` for nullable fields** — not `Optional<T>`
|
||||
10. **Pass `0` for auto-increment** — to trigger ID generation on insert
|
||||
11. **Sum types must be `partial record`** — not struct or class
|
||||
12. **Fully qualify Index attribute** — `[SpacetimeDB.Index.BTree]` to avoid System.Index ambiguity
|
||||
13. **Update only via primary key** — use delete+insert for non-PK changes (2.0)
|
||||
14. **Use `SpacetimeDB.Runtime` package** — not `ServerSdk` (2.0)
|
||||
15. **Use `List<T>` for collection parameters** — not arrays
|
||||
16. **`Identity` is in `SpacetimeDB` namespace** — not `SpacetimeDB.Types`
|
||||
|
||||
---
|
||||
|
||||
## Server-Side Module Development
|
||||
|
||||
### Table Definition
|
||||
|
||||
```csharp
|
||||
using SpacetimeDB;
|
||||
|
||||
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
|
||||
public partial struct Player
|
||||
{
|
||||
[SpacetimeDB.PrimaryKey]
|
||||
[SpacetimeDB.AutoInc]
|
||||
public ulong Id;
|
||||
|
||||
[SpacetimeDB.Index.BTree]
|
||||
public Identity OwnerId;
|
||||
|
||||
public string Name;
|
||||
public Timestamp CreatedAt;
|
||||
}
|
||||
|
||||
// Multi-column index (use fully-qualified attribute!)
|
||||
[SpacetimeDB.Table(Accessor = "Score", Public = true)]
|
||||
[SpacetimeDB.Index.BTree(Accessor = "by_player_game", Columns = new[] { "PlayerId", "GameId" })]
|
||||
public partial struct Score
|
||||
{
|
||||
[SpacetimeDB.PrimaryKey]
|
||||
[SpacetimeDB.AutoInc]
|
||||
public ulong Id;
|
||||
public Identity PlayerId;
|
||||
public string GameId;
|
||||
public int Points;
|
||||
}
|
||||
```
|
||||
|
||||
### Field Attributes
|
||||
|
||||
```csharp
|
||||
[SpacetimeDB.PrimaryKey] // Exactly one per table (required)
|
||||
[SpacetimeDB.AutoInc] // Auto-increment (integer fields only)
|
||||
[SpacetimeDB.Unique] // Unique constraint
|
||||
[SpacetimeDB.Index.BTree] // Single-column B-tree index
|
||||
[SpacetimeDB.Default(value)] // Default value for new columns
|
||||
```
|
||||
|
||||
### SpacetimeDB Column Types
|
||||
|
||||
```csharp
|
||||
Identity // User identity (SpacetimeDB namespace, not SpacetimeDB.Types)
|
||||
Timestamp // Timestamp (use ctx.Timestamp server-side, never DateTime.Now)
|
||||
ScheduleAt // For scheduled tables
|
||||
T? // Nullable (e.g., string?)
|
||||
List<T> // Collections (use List, not arrays)
|
||||
```
|
||||
|
||||
Standard C# primitives (`bool`, `byte`..`ulong`, `float`, `double`, `string`) are all supported.
|
||||
|
||||
### Insert with Auto-Increment
|
||||
|
||||
```csharp
|
||||
var player = ctx.Db.Player.Insert(new Player
|
||||
{
|
||||
Id = 0, // Pass 0 to trigger auto-increment
|
||||
OwnerId = ctx.Sender,
|
||||
Name = name,
|
||||
CreatedAt = ctx.Timestamp
|
||||
});
|
||||
ulong newId = player.Id; // Insert returns the row with generated ID
|
||||
```
|
||||
|
||||
### Module and Reducers
|
||||
|
||||
```csharp
|
||||
using SpacetimeDB;
|
||||
|
||||
public static partial class Module
|
||||
{
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void CreateTask(ReducerContext ctx, string title)
|
||||
{
|
||||
if (string.IsNullOrEmpty(title))
|
||||
throw new Exception("Title cannot be empty");
|
||||
|
||||
ctx.Db.Task.Insert(new Task
|
||||
{
|
||||
Id = 0,
|
||||
OwnerId = ctx.Sender,
|
||||
Title = title,
|
||||
Completed = false
|
||||
});
|
||||
}
|
||||
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void CompleteTask(ReducerContext ctx, ulong taskId)
|
||||
{
|
||||
if (ctx.Db.Task.Id.Find(taskId) is not Task task)
|
||||
throw new Exception("Task not found");
|
||||
if (task.OwnerId != ctx.Sender)
|
||||
throw new Exception("Not authorized");
|
||||
|
||||
ctx.Db.Task.Id.Update(task with { Completed = true });
|
||||
}
|
||||
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void DeleteTask(ReducerContext ctx, ulong taskId)
|
||||
{
|
||||
ctx.Db.Task.Id.Delete(taskId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lifecycle Reducers
|
||||
|
||||
```csharp
|
||||
public static partial class Module
|
||||
{
|
||||
[SpacetimeDB.Reducer(ReducerKind.Init)]
|
||||
public static void Init(ReducerContext ctx)
|
||||
{
|
||||
Log.Info("Module initialized");
|
||||
}
|
||||
|
||||
// CRITICAL: no "On" prefix!
|
||||
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
|
||||
public static void ClientConnected(ReducerContext ctx)
|
||||
{
|
||||
Log.Info($"Client connected: {ctx.Sender}");
|
||||
if (ctx.Db.User.Identity.Find(ctx.Sender) is User user)
|
||||
{
|
||||
ctx.Db.User.Identity.Update(user with { Online = true });
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.Db.User.Insert(new User { Identity = ctx.Sender, Online = true });
|
||||
}
|
||||
}
|
||||
|
||||
[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
|
||||
public static void ClientDisconnected(ReducerContext ctx)
|
||||
{
|
||||
if (ctx.Db.User.Identity.Find(ctx.Sender) is User user)
|
||||
{
|
||||
ctx.Db.User.Identity.Update(user with { Online = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Event Tables (2.0)
|
||||
|
||||
Reducer callbacks are removed in 2.0. Use event tables + `OnInsert` instead.
|
||||
|
||||
```csharp
|
||||
[SpacetimeDB.Table(Accessor = "DamageEvent", Public = true, Event = true)]
|
||||
public partial struct DamageEvent
|
||||
{
|
||||
public Identity Target;
|
||||
public uint Amount;
|
||||
}
|
||||
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void DealDamage(ReducerContext ctx, Identity target, uint amount)
|
||||
{
|
||||
ctx.Db.DamageEvent.Insert(new DamageEvent { Target = target, Amount = amount });
|
||||
}
|
||||
```
|
||||
|
||||
Client subscribes and uses `OnInsert`:
|
||||
```csharp
|
||||
conn.Db.DamageEvent.OnInsert += (ctx, evt) => {
|
||||
PlayDamageAnimation(evt.Target, evt.Amount);
|
||||
};
|
||||
```
|
||||
|
||||
Event tables must be subscribed explicitly — they are excluded from `SubscribeToAllTables()`.
|
||||
|
||||
### Database Access
|
||||
|
||||
```csharp
|
||||
// Find by primary key — returns nullable, use pattern matching
|
||||
if (ctx.Db.Task.Id.Find(taskId) is Task task) { /* use task */ }
|
||||
|
||||
// Update by primary key (2.0: only primary key has .Update)
|
||||
ctx.Db.Task.Id.Update(task with { Title = newTitle });
|
||||
|
||||
// Delete by primary key
|
||||
ctx.Db.Task.Id.Delete(taskId);
|
||||
|
||||
// Find by unique index — returns nullable
|
||||
if (ctx.Db.Player.Username.Find("alice") is Player player) { }
|
||||
|
||||
// Filter by B-tree index — returns iterator
|
||||
foreach (var task in ctx.Db.Task.OwnerId.Filter(ctx.Sender)) { }
|
||||
|
||||
// Full table scan — avoid for large tables
|
||||
foreach (var task in ctx.Db.Task.Iter()) { }
|
||||
var count = ctx.Db.Task.Count;
|
||||
```
|
||||
|
||||
### Custom Types and Sum Types
|
||||
|
||||
```csharp
|
||||
[SpacetimeDB.Type]
|
||||
public partial struct Position { public int X; public int Y; }
|
||||
|
||||
// Sum types MUST be partial record with named tuple
|
||||
[SpacetimeDB.Type]
|
||||
public partial struct Circle { public int Radius; }
|
||||
[SpacetimeDB.Type]
|
||||
public partial struct Rectangle { public int Width; public int Height; }
|
||||
[SpacetimeDB.Type]
|
||||
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
|
||||
|
||||
// Creating sum type values
|
||||
var circle = new Shape.Circle(new Circle { Radius = 10 });
|
||||
```
|
||||
|
||||
### Scheduled Tables
|
||||
|
||||
```csharp
|
||||
[SpacetimeDB.Table(Accessor = "Reminder", Scheduled = nameof(Module.SendReminder))]
|
||||
public partial struct Reminder
|
||||
{
|
||||
[SpacetimeDB.PrimaryKey]
|
||||
[SpacetimeDB.AutoInc]
|
||||
public ulong Id;
|
||||
public string Message;
|
||||
public ScheduleAt ScheduledAt;
|
||||
}
|
||||
|
||||
public static partial class Module
|
||||
{
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void SendReminder(ReducerContext ctx, Reminder reminder)
|
||||
{
|
||||
Log.Info($"Reminder: {reminder.Message}");
|
||||
}
|
||||
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void CreateReminder(ReducerContext ctx, string message, ulong delaySecs)
|
||||
{
|
||||
ctx.Db.Reminder.Insert(new Reminder
|
||||
{
|
||||
Id = 0,
|
||||
Message = message,
|
||||
ScheduledAt = new ScheduleAt.Time(ctx.Timestamp + TimeSpan.FromSeconds(delaySecs))
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
```csharp
|
||||
Log.Debug("Debug message");
|
||||
Log.Info("Information");
|
||||
Log.Warn("Warning");
|
||||
Log.Error("Error occurred");
|
||||
Log.Exception("Critical failure"); // Logs at error level
|
||||
```
|
||||
|
||||
### ReducerContext API
|
||||
|
||||
```csharp
|
||||
ctx.Sender // Identity of the caller
|
||||
ctx.Timestamp // Current timestamp
|
||||
ctx.Db // Database access
|
||||
ctx.Identity // Module's own identity
|
||||
ctx.ConnectionId // Connection ID (nullable)
|
||||
ctx.SenderAuth // Authorization context (JWT claims, internal call detection)
|
||||
ctx.Rng // Deterministic random number generator
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
Throwing an exception in a reducer rolls back the entire transaction:
|
||||
|
||||
```csharp
|
||||
[SpacetimeDB.Reducer]
|
||||
public static void TransferCredits(ReducerContext ctx, Identity toUser, uint amount)
|
||||
{
|
||||
if (ctx.Db.User.Identity.Find(ctx.Sender) is not User sender)
|
||||
throw new Exception("Sender not found");
|
||||
|
||||
if (sender.Credits < amount)
|
||||
throw new Exception("Insufficient credits");
|
||||
|
||||
ctx.Db.User.Identity.Update(sender with { Credits = sender.Credits - amount });
|
||||
|
||||
if (ctx.Db.User.Identity.Find(toUser) is User receiver)
|
||||
ctx.Db.User.Identity.Update(receiver with { Credits = receiver.Credits + amount });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Required .csproj (MUST be named `StdbModule.csproj`)
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SpacetimeDB.Runtime" Version="1.*" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install .NET 8 SDK (required, not .NET 9)
|
||||
# Install WASI workload
|
||||
dotnet workload install wasi-experimental
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client SDK
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
dotnet add package SpacetimeDB.ClientSDK
|
||||
```
|
||||
|
||||
### Generate Module Bindings
|
||||
|
||||
```bash
|
||||
spacetime generate --lang csharp --out-dir module_bindings --module-path PATH_TO_MODULE
|
||||
```
|
||||
|
||||
This creates `SpacetimeDBClient.g.cs`, `Tables/*.g.cs`, `Reducers/*.g.cs`, and `Types/*.g.cs`.
|
||||
|
||||
### Connection Setup
|
||||
|
||||
```csharp
|
||||
using SpacetimeDB;
|
||||
using SpacetimeDB.Types;
|
||||
|
||||
var conn = DbConnection.Builder()
|
||||
.WithUri("http://localhost:3000")
|
||||
.WithDatabaseName("my-database")
|
||||
.WithToken(savedToken)
|
||||
.OnConnect(OnConnected)
|
||||
.OnConnectError(err => Console.Error.WriteLine($"Failed: {err}"))
|
||||
.OnDisconnect((conn, err) => { if (err != null) Console.Error.WriteLine(err); })
|
||||
.Build();
|
||||
|
||||
void OnConnected(DbConnection conn, Identity identity, string authToken)
|
||||
{
|
||||
// Save authToken to persistent storage for reconnection
|
||||
Console.WriteLine($"Connected: {identity}");
|
||||
conn.SubscriptionBuilder()
|
||||
.OnApplied(OnSubscriptionApplied)
|
||||
.SubscribeToAllTables();
|
||||
}
|
||||
```
|
||||
|
||||
### Critical: FrameTick
|
||||
|
||||
**The SDK does NOT automatically process messages.** You must call `FrameTick()` regularly.
|
||||
|
||||
```csharp
|
||||
// Console application
|
||||
while (running) { conn.FrameTick(); Thread.Sleep(16); }
|
||||
|
||||
// Unity: call conn?.FrameTick() in Update()
|
||||
```
|
||||
|
||||
**Warning**: Do NOT call `FrameTick()` from a background thread. It modifies `conn.Db` and can cause data races.
|
||||
|
||||
### Subscribing to Tables
|
||||
|
||||
```csharp
|
||||
// SQL queries
|
||||
conn.SubscriptionBuilder()
|
||||
.OnApplied(OnSubscriptionApplied)
|
||||
.OnError((ctx, err) => Console.Error.WriteLine($"Subscription failed: {err}"))
|
||||
.Subscribe(new[] {
|
||||
"SELECT * FROM player",
|
||||
"SELECT * FROM message WHERE sender = :sender"
|
||||
});
|
||||
|
||||
// Subscribe to all tables (development only)
|
||||
conn.SubscriptionBuilder()
|
||||
.OnApplied(OnSubscriptionApplied)
|
||||
.SubscribeToAllTables();
|
||||
|
||||
// Subscription handle for later unsubscribe
|
||||
SubscriptionHandle handle = conn.SubscriptionBuilder()
|
||||
.OnApplied(ctx => Console.WriteLine("Applied"))
|
||||
.Subscribe(new[] { "SELECT * FROM player" });
|
||||
|
||||
handle.UnsubscribeThen(ctx => Console.WriteLine("Unsubscribed"));
|
||||
```
|
||||
|
||||
**Warning**: `SubscribeToAllTables()` cannot be mixed with `Subscribe()` on the same connection.
|
||||
|
||||
### Accessing the Client Cache
|
||||
|
||||
```csharp
|
||||
// Iterate all rows
|
||||
foreach (var player in ctx.Db.Player.Iter()) { Console.WriteLine(player.Name); }
|
||||
|
||||
// Count rows
|
||||
int playerCount = ctx.Db.Player.Count;
|
||||
|
||||
// Find by unique/primary key — returns nullable
|
||||
Player? player = ctx.Db.Player.Identity.Find(someIdentity);
|
||||
if (player != null) { Console.WriteLine(player.Name); }
|
||||
|
||||
// Filter by BTree index — returns IEnumerable
|
||||
foreach (var p in ctx.Db.Player.Level.Filter(1)) { }
|
||||
```
|
||||
|
||||
### Row Event Callbacks
|
||||
|
||||
```csharp
|
||||
ctx.Db.Player.OnInsert += (EventContext ctx, Player player) => {
|
||||
Console.WriteLine($"Player joined: {player.Name}");
|
||||
};
|
||||
|
||||
ctx.Db.Player.OnDelete += (EventContext ctx, Player player) => {
|
||||
Console.WriteLine($"Player left: {player.Name}");
|
||||
};
|
||||
|
||||
ctx.Db.Player.OnUpdate += (EventContext ctx, Player oldRow, Player newRow) => {
|
||||
Console.WriteLine($"Player {oldRow.Name} renamed to {newRow.Name}");
|
||||
};
|
||||
|
||||
// Checking event source
|
||||
ctx.Db.Player.OnInsert += (EventContext ctx, Player player) => {
|
||||
switch (ctx.Event)
|
||||
{
|
||||
case Event<Reducer>.SubscribeApplied:
|
||||
break; // Initial subscription data
|
||||
case Event<Reducer>.Reducer(var reducerEvent):
|
||||
Console.WriteLine($"Reducer: {reducerEvent.Reducer}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Calling Reducers
|
||||
|
||||
```csharp
|
||||
ctx.Reducers.SendMessage("Hello, world!");
|
||||
ctx.Reducers.CreatePlayer("NewPlayer");
|
||||
|
||||
// Reducer completion callbacks
|
||||
conn.Reducers.OnSendMessage += (ReducerEventContext ctx, string text) => {
|
||||
if (ctx.Event.Status is Status.Committed)
|
||||
Console.WriteLine($"Message sent: {text}");
|
||||
else if (ctx.Event.Status is Status.Failed(var reason))
|
||||
Console.Error.WriteLine($"Send failed: {reason}");
|
||||
};
|
||||
|
||||
// Unhandled reducer errors
|
||||
conn.OnUnhandledReducerError += (ReducerEventContext ctx, Exception ex) => {
|
||||
Console.Error.WriteLine($"Reducer error: {ex.Message}");
|
||||
};
|
||||
```
|
||||
|
||||
### Identity and Authentication
|
||||
|
||||
```csharp
|
||||
// In OnConnect callback — save token for reconnection
|
||||
void OnConnected(DbConnection conn, Identity identity, string authToken)
|
||||
{
|
||||
// Save authToken to persistent storage (file, config, PlayerPrefs, etc.)
|
||||
SaveToken(authToken);
|
||||
}
|
||||
|
||||
// Reconnect with saved token
|
||||
string savedToken = LoadToken();
|
||||
DbConnection.Builder()
|
||||
.WithUri("http://localhost:3000")
|
||||
.WithDatabaseName("my-database")
|
||||
.WithToken(savedToken)
|
||||
.OnConnect(OnConnected)
|
||||
.Build();
|
||||
|
||||
// Pass null or omit WithToken for anonymous connection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
spacetime start
|
||||
spacetime publish <module-name> --module-path <backend-dir>
|
||||
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
|
||||
spacetime generate --lang csharp --out-dir <client>/SpacetimeDB --module-path <backend-dir>
|
||||
spacetime logs <module-name>
|
||||
```
|
||||
556
.codex/skills/spacetimedb-rust/SKILL.md
Normal file
556
.codex/skills/spacetimedb-rust/SKILL.md
Normal file
@@ -0,0 +1,556 @@
|
||||
---
|
||||
name: spacetimedb-rust
|
||||
description: Develop SpacetimeDB server modules in Rust. Use when writing reducers, tables, or module logic.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: clockworklabs
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# SpacetimeDB Rust Module Development
|
||||
|
||||
SpacetimeDB modules are WebAssembly applications that run inside the database. They define tables to store data and reducers to modify data. Clients connect directly to the database and execute application logic inside it.
|
||||
|
||||
> **Tested with:** SpacetimeDB 2.0+ APIs
|
||||
|
||||
---
|
||||
|
||||
## HALLUCINATED APIs — DO NOT USE
|
||||
|
||||
**These APIs/patterns are incorrect. LLMs frequently hallucinate them.**
|
||||
|
||||
Both macro forms are valid in 2.0: `#[spacetimedb::table(...)]` / `#[table(...)]` and `#[spacetimedb::reducer]` / `#[reducer]`.
|
||||
|
||||
```rust
|
||||
#[derive(Table)] // Tables use #[table] attribute, not derive
|
||||
#[derive(Reducer)] // Reducers use #[reducer] attribute
|
||||
|
||||
// WRONG — SpacetimeType on tables
|
||||
#[derive(SpacetimeType)] // DO NOT use on #[table] structs!
|
||||
#[table(accessor = my_table)]
|
||||
pub struct MyTable { ... }
|
||||
|
||||
// WRONG — mutable context
|
||||
pub fn my_reducer(ctx: &mut ReducerContext, ...) { } // Should be &ReducerContext
|
||||
|
||||
// WRONG — table access without parentheses
|
||||
ctx.db.player // Should be ctx.db.player()
|
||||
ctx.db.player.find(id) // Should be ctx.db.player().id().find(&id)
|
||||
|
||||
// WRONG — old 1.0 patterns
|
||||
ctx.sender // Use ctx.sender() — method, not field (2.0)
|
||||
.with_module_name("db") // Use .with_database_name() (2.0)
|
||||
ctx.db.user().name().update(..) // Update only via primary key (2.0)
|
||||
```
|
||||
|
||||
### CORRECT PATTERNS:
|
||||
|
||||
```rust
|
||||
use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};
|
||||
use spacetimedb::SpacetimeType; // Only for custom types, NOT tables
|
||||
|
||||
// CORRECT TABLE — accessor, not name; no SpacetimeType derive!
|
||||
#[table(accessor = player, public)]
|
||||
pub struct Player {
|
||||
#[primary_key]
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// CORRECT REDUCER — immutable context, sender() is a method
|
||||
#[reducer]
|
||||
pub fn create_player(ctx: &ReducerContext, name: String) {
|
||||
ctx.db.player().insert(Player { id: 0, name });
|
||||
}
|
||||
|
||||
// CORRECT TABLE ACCESS — methods with parentheses, sender() method
|
||||
let player = ctx.db.player().id().find(&player_id);
|
||||
let caller = ctx.sender();
|
||||
```
|
||||
|
||||
### DO NOT:
|
||||
- **Derive `SpacetimeType` on `#[table]` structs** — the macro handles this
|
||||
- **Use mutable context** — `&ReducerContext`, not `&mut ReducerContext`
|
||||
- **Forget `Table` trait import** — required for table operations
|
||||
- **Use field access for tables** — `ctx.db.player()` not `ctx.db.player`
|
||||
- **Use `ctx.sender`** — it's `ctx.sender()` (method) in 2.0
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes Table
|
||||
|
||||
| Wrong | Right | Error |
|
||||
|-------|-------|-------|
|
||||
| `#[table(accessor = "my_table")]` | `#[table(accessor = my_table)]` | String literals not allowed |
|
||||
| Missing `public` on table | Add `public` flag | Clients can't subscribe |
|
||||
| Network/filesystem in reducer | Use procedures instead | Sandbox violation |
|
||||
| Panic for expected errors | Return `Result<(), String>` | WASM instance destroyed |
|
||||
|
||||
---
|
||||
|
||||
## Hard Requirements
|
||||
|
||||
1. **DO NOT derive `SpacetimeType` on `#[table]` structs** — the macro handles this
|
||||
2. **Import `Table` trait** — required for all table operations
|
||||
3. **Use `&ReducerContext`** — not `&mut ReducerContext`
|
||||
4. **Tables are methods** — `ctx.db.table()` not `ctx.db.table`
|
||||
5. **Use `ctx.sender()`** — method call, not field access (2.0)
|
||||
6. **Use `accessor =` for API handles** — `name = "..."` is optional canonical naming in table/index attributes
|
||||
7. **Reducers must be deterministic** — no filesystem, network, timers, or external RNG
|
||||
8. **Use `ctx.rng()`** — not `rand` crate for random numbers
|
||||
9. **Add `public` flag** — if clients need to subscribe to a table
|
||||
10. **Update only via primary key** — use delete+insert for non-PK changes (2.0)
|
||||
|
||||
---
|
||||
|
||||
## Project Setup
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "my-module"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
spacetimedb = { workspace = true }
|
||||
log = "0.4"
|
||||
```
|
||||
|
||||
### Essential Imports
|
||||
|
||||
```rust
|
||||
use spacetimedb::{ReducerContext, Table};
|
||||
use spacetimedb::{Identity, Timestamp, ConnectionId, ScheduleAt};
|
||||
```
|
||||
|
||||
## Table Definitions
|
||||
|
||||
```rust
|
||||
#[spacetimedb::table(accessor = player, public)]
|
||||
pub struct Player {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
id: u64,
|
||||
name: String,
|
||||
score: u32,
|
||||
}
|
||||
```
|
||||
|
||||
### Table Attributes
|
||||
|
||||
| Attribute | Description |
|
||||
|-----------|-------------|
|
||||
| `accessor = identifier` | Required. The API name used in `ctx.db.{accessor}()` |
|
||||
| `public` | Makes table visible to clients via subscriptions |
|
||||
| `scheduled(function_name)` | Creates a schedule table that triggers the named reducer or procedure |
|
||||
| `index(accessor = idx, btree(columns = [a, b]))` | Multi-column index |
|
||||
|
||||
### Column Attributes
|
||||
|
||||
| Attribute | Description |
|
||||
|-----------|-------------|
|
||||
| `#[primary_key]` | Unique identifier for the row (one per table max) |
|
||||
| `#[unique]` | Enforces uniqueness, enables `find()` method |
|
||||
| `#[auto_inc]` | Auto-generates unique integer values when inserting 0 |
|
||||
| `#[index(btree)]` | Creates a B-tree index for efficient lookups |
|
||||
|
||||
### Supported Column Types
|
||||
|
||||
**Primitives**: `u8`-`u256`, `i8`-`i256`, `f32`, `f64`, `bool`, `String`
|
||||
|
||||
**SpacetimeDB Types**: `Identity`, `ConnectionId`, `Timestamp`, `Uuid`, `ScheduleAt`
|
||||
|
||||
**Collections**: `Vec<T>`, `Option<T>`, `Result<T, E>`
|
||||
|
||||
**Custom Types**: Any struct/enum with `#[derive(SpacetimeType)]`
|
||||
|
||||
---
|
||||
|
||||
## Reducers
|
||||
|
||||
```rust
|
||||
#[spacetimedb::reducer]
|
||||
pub fn create_player(ctx: &ReducerContext, name: String) -> Result<(), String> {
|
||||
if name.is_empty() {
|
||||
return Err("Name cannot be empty".to_string());
|
||||
}
|
||||
ctx.db.player().insert(Player { id: 0, name, score: 0 });
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Reducer Rules
|
||||
|
||||
1. First parameter must be `&ReducerContext`
|
||||
2. Return `()`, `Result<(), String>`, or `Result<(), E>` where `E: Display`
|
||||
3. All changes roll back on panic or `Err` return
|
||||
4. Must import `Table` trait: `use spacetimedb::Table;`
|
||||
|
||||
### ReducerContext
|
||||
|
||||
```rust
|
||||
ctx.db // Database access
|
||||
ctx.sender() // Identity of the caller (method, not field!)
|
||||
ctx.connection_id() // Option<ConnectionId> (None for scheduled/system reducers)
|
||||
ctx.timestamp // Invocation timestamp
|
||||
ctx.identity() // Module's own identity
|
||||
ctx.rng() // Deterministic RNG (method, not field!)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Table Operations
|
||||
|
||||
### Insert
|
||||
|
||||
```rust
|
||||
// Insert returns the row with auto_inc values populated
|
||||
let player = ctx.db.player().insert(Player { id: 0, name: "Alice".into(), score: 100 });
|
||||
log::info!("Created player with id: {}", player.id);
|
||||
```
|
||||
|
||||
### Find and Filter
|
||||
|
||||
```rust
|
||||
// Find by unique/primary key — returns Option
|
||||
if let Some(player) = ctx.db.player().id().find(&123) {
|
||||
log::info!("Found: {}", player.name);
|
||||
}
|
||||
|
||||
// Optional clarity: typed literals can avoid inference ambiguity
|
||||
if let Some(player) = ctx.db.player().id().find(&123u64) {
|
||||
log::info!("Found: {}", player.name);
|
||||
}
|
||||
|
||||
// Filter by indexed column — returns iterator
|
||||
for player in ctx.db.player().name().filter(&"Alice".to_string()) {
|
||||
log::info!("Player: {}", player.name);
|
||||
}
|
||||
|
||||
// Full table scan
|
||||
for player in ctx.db.player().iter() { }
|
||||
let total = ctx.db.player().count();
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
```rust
|
||||
// Update via primary key (2.0: only primary key has update)
|
||||
if let Some(player) = ctx.db.player().id().find(&123) {
|
||||
ctx.db.player().id().update(Player { score: player.score + 10, ..player });
|
||||
}
|
||||
|
||||
// For non-PK changes: delete + insert
|
||||
if let Some(old) = ctx.db.player().id().find(&id) {
|
||||
ctx.db.player().id().delete(&id);
|
||||
ctx.db.player().insert(Player { name: new_name, ..old });
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
```rust
|
||||
// Delete by primary key
|
||||
ctx.db.player().id().delete(&123);
|
||||
|
||||
// Delete by indexed column (collect first to avoid iterator invalidation)
|
||||
let to_remove: Vec<u64> = ctx.db.player().name().filter(&"Alice".to_string())
|
||||
.map(|p| p.id)
|
||||
.collect();
|
||||
for id in to_remove {
|
||||
ctx.db.player().id().delete(&id);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Indexes
|
||||
|
||||
```rust
|
||||
// Single-column index
|
||||
#[spacetimedb::table(accessor = player, public)]
|
||||
pub struct Player {
|
||||
#[primary_key]
|
||||
id: u64,
|
||||
#[index(btree)]
|
||||
level: u32,
|
||||
name: String,
|
||||
}
|
||||
|
||||
// Multi-column index
|
||||
#[spacetimedb::table(
|
||||
accessor = score, public,
|
||||
index(accessor = by_player_level, btree(columns = [player_id, level]))
|
||||
)]
|
||||
pub struct Score {
|
||||
player_id: u32,
|
||||
level: u32,
|
||||
points: i64,
|
||||
}
|
||||
|
||||
// Multi-column index querying: prefix match (first column only)
|
||||
for s in ctx.db.score().by_player_level().filter(&(42,)) {
|
||||
log::info!("Player 42, any level: {} pts", s.points);
|
||||
}
|
||||
|
||||
// Full match (both columns)
|
||||
for s in ctx.db.score().by_player_level().filter(&(42, 5)) {
|
||||
log::info!("Player 42, level 5: {} pts", s.points);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Tables (2.0)
|
||||
|
||||
Reducer callbacks are removed in 2.0. Use event tables + `on_insert` instead.
|
||||
|
||||
```rust
|
||||
#[table(accessor = damage_event, public, event)]
|
||||
pub struct DamageEvent {
|
||||
pub target: Identity,
|
||||
pub amount: u32,
|
||||
}
|
||||
|
||||
#[reducer]
|
||||
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
|
||||
ctx.db.damage_event().insert(DamageEvent { target, amount });
|
||||
}
|
||||
```
|
||||
|
||||
Client subscribes and uses `on_insert`:
|
||||
```rust
|
||||
conn.db.damage_event().on_insert(|ctx, event| {
|
||||
play_damage_animation(event.target, event.amount);
|
||||
});
|
||||
```
|
||||
|
||||
Event tables must be subscribed explicitly — they are excluded from `subscribe_to_all_tables()`.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Reducers
|
||||
|
||||
```rust
|
||||
#[spacetimedb::reducer(init)]
|
||||
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
|
||||
log::info!("Database initializing...");
|
||||
ctx.db.config().insert(Config {
|
||||
id: 0,
|
||||
max_players: 100,
|
||||
game_mode: "default".to_string(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer(client_connected)]
|
||||
pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {
|
||||
let caller = ctx.sender();
|
||||
log::info!("Client connected: {}", caller);
|
||||
|
||||
if let Some(user) = ctx.db.user().identity().find(&caller) {
|
||||
ctx.db.user().identity().update(User { online: true, ..user });
|
||||
} else {
|
||||
ctx.db.user().insert(User {
|
||||
identity: caller,
|
||||
name: format!("User-{}", &caller.to_hex()[..8]),
|
||||
online: true,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer(client_disconnected)]
|
||||
pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {
|
||||
let caller = ctx.sender();
|
||||
if let Some(user) = ctx.db.user().identity().find(&caller) {
|
||||
ctx.db.user().identity().update(User { online: false, ..user });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scheduled Reducers
|
||||
|
||||
```rust
|
||||
use spacetimedb::ScheduleAt;
|
||||
use std::time::Duration;
|
||||
|
||||
#[spacetimedb::table(accessor = game_tick_schedule, scheduled(game_tick))]
|
||||
pub struct GameTickSchedule {
|
||||
#[primary_key]
|
||||
#[auto_inc]
|
||||
scheduled_id: u64,
|
||||
scheduled_at: ScheduleAt,
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
fn game_tick(ctx: &ReducerContext, schedule: GameTickSchedule) {
|
||||
if !ctx.sender_auth().is_internal() { return; }
|
||||
log::info!("Game tick at {:?}", ctx.timestamp);
|
||||
}
|
||||
|
||||
// Schedule at interval (e.g., in init reducer)
|
||||
ctx.db.game_tick_schedule().insert(GameTickSchedule {
|
||||
scheduled_id: 0,
|
||||
scheduled_at: ScheduleAt::Interval(Duration::from_millis(100).into()),
|
||||
});
|
||||
|
||||
// Schedule at specific time
|
||||
let run_at = ctx.timestamp + Duration::from_secs(delay_secs);
|
||||
ctx.db.reminder_schedule().insert(ReminderSchedule {
|
||||
scheduled_id: 0,
|
||||
scheduled_at: ScheduleAt::Time(run_at),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Identity and Authentication
|
||||
|
||||
```rust
|
||||
#[spacetimedb::table(accessor = user, public)]
|
||||
pub struct User {
|
||||
#[primary_key]
|
||||
identity: Identity,
|
||||
name: String,
|
||||
online: bool,
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
pub fn set_name(ctx: &ReducerContext, new_name: String) -> Result<(), String> {
|
||||
let caller = ctx.sender();
|
||||
let user = ctx.db.user().identity().find(&caller)
|
||||
.ok_or("User not found — connect first")?;
|
||||
ctx.db.user().identity().update(User { name: new_name, ..user });
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Owner-Only Reducer Pattern
|
||||
|
||||
```rust
|
||||
fn require_owner(ctx: &ReducerContext, entity_owner: &Identity) -> Result<(), String> {
|
||||
if ctx.sender() != *entity_owner {
|
||||
Err("Not authorized: you don't own this entity".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::reducer]
|
||||
pub fn rename_character(ctx: &ReducerContext, char_id: u64, new_name: String) -> Result<(), String> {
|
||||
let character = ctx.db.character().id().find(&char_id)
|
||||
.ok_or("Character not found")?;
|
||||
require_owner(ctx, &character.owner)?;
|
||||
ctx.db.character().id().update(Character { name: new_name, ..character });
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
```rust
|
||||
// Sender error — return Err (user sees message, transaction rolls back cleanly)
|
||||
#[spacetimedb::reducer]
|
||||
pub fn transfer(ctx: &ReducerContext, to: Identity, amount: u64) -> Result<(), String> {
|
||||
let sender = ctx.db.wallet().identity().find(&ctx.sender())
|
||||
.ok_or("Wallet not found")?;
|
||||
if sender.balance < amount {
|
||||
return Err("Insufficient balance".to_string());
|
||||
}
|
||||
// ... proceed with transfer
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Programmer error — panic (destroys the WASM instance, expensive!)
|
||||
// Only use for truly impossible states
|
||||
#[spacetimedb::reducer]
|
||||
pub fn process(ctx: &ReducerContext, id: u64) {
|
||||
let item = ctx.db.item().id().find(&id)
|
||||
.expect("BUG: item should exist at this point");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Prefer `Result<(), String>` for all expected failure cases. Panics destroy and recreate the WASM instance.
|
||||
|
||||
---
|
||||
|
||||
## Procedures (Beta)
|
||||
|
||||
> Procedures are behind the `unstable` feature in `spacetimedb`.
|
||||
> In `Cargo.toml`: `spacetimedb = { version = "...", features = ["unstable"] }`
|
||||
|
||||
```rust
|
||||
use spacetimedb::{procedure, ProcedureContext};
|
||||
|
||||
#[procedure]
|
||||
fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> {
|
||||
let data = fetch_from_url(&url)?;
|
||||
ctx.try_with_tx(|tx| {
|
||||
tx.db.external_data().insert(ExternalData { id: 0, content: data });
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
| Reducers | Procedures |
|
||||
|----------|------------|
|
||||
| `&ReducerContext` (immutable) | `&mut ProcedureContext` (mutable) |
|
||||
| Direct `ctx.db` access | Must use `ctx.with_tx()` |
|
||||
| No HTTP/network | HTTP allowed |
|
||||
| No return values | Can return data |
|
||||
|
||||
---
|
||||
|
||||
## Custom Types
|
||||
|
||||
```rust
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[derive(SpacetimeType)]
|
||||
pub enum PlayerStatus { Active, Idle, Away }
|
||||
|
||||
#[derive(SpacetimeType)]
|
||||
pub struct Position { x: f32, y: f32, z: f32 }
|
||||
|
||||
// Use in table (DO NOT derive SpacetimeType on the table!)
|
||||
#[spacetimedb::table(accessor = player, public)]
|
||||
pub struct Player {
|
||||
#[primary_key]
|
||||
id: u64,
|
||||
status: PlayerStatus,
|
||||
position: Position,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
spacetime build
|
||||
spacetime publish my_database --module-path .
|
||||
spacetime publish my_database --clear-database --module-path .
|
||||
spacetime logs my_database
|
||||
spacetime call my_database create_player "Alice"
|
||||
spacetime sql my_database "SELECT * FROM player"
|
||||
spacetime generate --lang rust --out-dir <client>/src/module_bindings --module-path <backend-dir>
|
||||
```
|
||||
|
||||
## Important Constraints
|
||||
|
||||
1. **No Global State**: Static/global variables are undefined behavior across reducer calls
|
||||
2. **No Side Effects**: Reducers cannot make network requests or file I/O
|
||||
3. **Deterministic Execution**: Use `ctx.rng()` and `ctx.new_uuid_*()` for randomness
|
||||
4. **Transactional**: All reducer changes roll back on failure
|
||||
5. **Isolated**: Reducers don't see concurrent changes until commit
|
||||
489
.codex/skills/spacetimedb-typescript/SKILL.md
Normal file
489
.codex/skills/spacetimedb-typescript/SKILL.md
Normal file
@@ -0,0 +1,489 @@
|
||||
---
|
||||
name: spacetimedb-typescript
|
||||
description: Build TypeScript clients for SpacetimeDB. Use when connecting to SpacetimeDB from web apps, Node.js, Deno, Bun, or other JavaScript runtimes.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: clockworklabs
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# SpacetimeDB TypeScript SDK
|
||||
|
||||
Build real-time TypeScript clients that connect directly to SpacetimeDB modules. The SDK provides type-safe database access, automatic synchronization, and reactive updates for web apps, Node.js, Deno, Bun, and other JavaScript runtimes.
|
||||
|
||||
---
|
||||
|
||||
## HALLUCINATED APIs — DO NOT USE
|
||||
|
||||
**These APIs DO NOT EXIST. LLMs frequently hallucinate them.**
|
||||
|
||||
```typescript
|
||||
// WRONG PACKAGE — does not exist
|
||||
import { SpacetimeDBClient } from "@clockworklabs/spacetimedb-sdk";
|
||||
|
||||
// WRONG — these methods don't exist
|
||||
SpacetimeDBClient.connect(...);
|
||||
SpacetimeDBClient.call("reducer_name", [...]);
|
||||
connection.call("reducer_name", [arg1, arg2]);
|
||||
|
||||
// WRONG — positional reducer arguments
|
||||
conn.reducers.doSomething("value"); // WRONG!
|
||||
|
||||
// WRONG — old 1.0 patterns
|
||||
spacetimedb.reducer('reducer_name', params, fn); // Use export const name = spacetimedb.reducer(params, fn)
|
||||
schema(myTable); // Use schema({ myTable })
|
||||
schema(t1, t2, t3); // Use schema({ t1, t2, t3 })
|
||||
scheduled: 'run_cleanup' // Use scheduled: () => run_cleanup
|
||||
.withModuleName('db') // Use .withDatabaseName('db') (2.0)
|
||||
setReducerFlags.x('NoSuccessNotify') // Removed in 2.0
|
||||
```
|
||||
|
||||
### CORRECT PATTERNS:
|
||||
|
||||
```typescript
|
||||
// CORRECT IMPORTS
|
||||
import { DbConnection, tables } from './module_bindings'; // Generated!
|
||||
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
|
||||
import { Identity } from 'spacetimedb';
|
||||
|
||||
// CORRECT REDUCER CALLS — object syntax, not positional!
|
||||
conn.reducers.doSomething({ value: 'test' });
|
||||
conn.reducers.updateItem({ itemId: 1n, newValue: 42 });
|
||||
|
||||
// CORRECT DATA ACCESS — useTable returns [rows, isReady]
|
||||
const [items, isReady] = useTable(tables.item);
|
||||
```
|
||||
|
||||
### DO NOT:
|
||||
- **Invent hooks** like `useItems()`, `useData()` — use `useTable(tables.tableName)`
|
||||
- **Import from fake packages** — only `spacetimedb`, `spacetimedb/react`, `./module_bindings`
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes Table
|
||||
|
||||
### Server-side errors
|
||||
|
||||
| Wrong | Right | Error |
|
||||
|-------|-------|-------|
|
||||
| Missing `package.json` | Create `package.json` | "could not detect language" |
|
||||
| Missing `tsconfig.json` | Create `tsconfig.json` | "TsconfigNotFound" |
|
||||
| Entrypoint not at `src/index.ts` | Use `src/index.ts` | Module won't bundle |
|
||||
| `indexes` in COLUMNS (2nd arg) | `indexes` in OPTIONS (1st arg) of `table()` | "reading 'tag'" error |
|
||||
| Index without `algorithm` | `algorithm: 'btree'` | "reading 'tag'" error |
|
||||
| `filter({ ownerId })` | `filter(ownerId)` | "does not exist in type 'Range'" |
|
||||
| `.filter()` on unique column | `.find()` on unique column | TypeError |
|
||||
| `insert({ ...without id })` | `insert({ id: 0n, ... })` | "Property 'id' is missing" |
|
||||
| `const id = table.insert(...)` | `const row = table.insert(...)` | `.insert()` returns ROW, not ID |
|
||||
| `.unique()` + explicit index | Just use `.unique()` | "name is used for multiple entities" |
|
||||
| Import spacetimedb from index.ts | Import from schema.ts | "Cannot access before initialization" |
|
||||
| Incorrect multi-column `.filter()` range shape | Match index prefix/tuple shape | Empty results or range/type errors |
|
||||
| `.iter()` in views | Use index lookups only | Views can't scan tables |
|
||||
| `ctx.db` in procedures | `ctx.withTx(tx => tx.db...)` | Procedures need explicit transactions |
|
||||
|
||||
### Client-side errors
|
||||
|
||||
| Wrong | Right | Error |
|
||||
|-------|-------|-------|
|
||||
| Inline `connectionBuilder` | `useMemo(() => ..., [])` | Reconnects every render |
|
||||
| `const rows = useTable(table)` | `const [rows, isReady] = useTable(table)` | Tuple destructuring |
|
||||
| Optimistic UI updates | Let subscriptions drive state | Desync issues |
|
||||
| `<SpacetimeDBProvider builder={...}>` | `connectionBuilder={...}` | Wrong prop name |
|
||||
|
||||
---
|
||||
|
||||
## Hard Requirements
|
||||
|
||||
1. **`schema({ table })`** — use a single tables object; optional module settings are allowed as a second argument
|
||||
2. **Reducer/procedure names from exports** — `export const name = spacetimedb.reducer(params, fn)`; never `reducer('name', ...)`
|
||||
3. **Reducer calls use object syntax** — `{ param: 'value' }` not positional args
|
||||
4. **Import `DbConnection` from `./module_bindings`** — not from `spacetimedb`
|
||||
5. **DO NOT edit generated bindings** — regenerate with `spacetime generate`
|
||||
6. **Indexes go in OPTIONS (1st arg)** — not in COLUMNS (2nd arg) of `table()`
|
||||
7. **Use BigInt for u64/i64 fields** — `0n`, `1n`, not `0`, `1`
|
||||
8. **Reducers are transactional** — they do not return data
|
||||
9. **Reducers must be deterministic** — no filesystem, network, timers, random
|
||||
10. **Views should use index lookups** — `.iter()` causes severe performance issues
|
||||
11. **Procedures need `ctx.withTx()`** — `ctx.db` doesn't exist in procedures
|
||||
12. **Sum type values** — use `{ tag: 'variant', value: payload }` not `{ variant: payload }`
|
||||
13. **Use `.withDatabaseName()`** — not `.withModuleName()` (2.0)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install spacetimedb
|
||||
```
|
||||
|
||||
For Node.js environments without native fetch/WebSocket support, install `undici`.
|
||||
|
||||
## Generating Type Bindings
|
||||
|
||||
```bash
|
||||
spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./server
|
||||
```
|
||||
|
||||
## Client Connection
|
||||
|
||||
```typescript
|
||||
import { DbConnection } from './module_bindings';
|
||||
|
||||
const connection = DbConnection.builder()
|
||||
.withUri('ws://localhost:3000')
|
||||
.withDatabaseName('my_database')
|
||||
.withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
|
||||
.onConnect((conn, identity, token) => {
|
||||
// identity: your unique Identity for this database
|
||||
console.log('Connected as:', identity.toHexString());
|
||||
|
||||
// Save token for reconnection (preserves identity across sessions)
|
||||
localStorage.setItem('spacetimedb_token', token);
|
||||
|
||||
conn.subscriptionBuilder()
|
||||
.onApplied(() => console.log('Cache ready'))
|
||||
.subscribe('SELECT * FROM player');
|
||||
})
|
||||
.onDisconnect((ctx) => console.log('Disconnected'))
|
||||
.onConnectError((ctx, error) => console.error('Connection failed:', error))
|
||||
.build();
|
||||
```
|
||||
|
||||
## Subscribing to Tables
|
||||
|
||||
```typescript
|
||||
// Basic subscription
|
||||
connection.subscriptionBuilder()
|
||||
.onApplied((ctx) => console.log('Cache ready'))
|
||||
.subscribe('SELECT * FROM player');
|
||||
|
||||
// Multiple queries
|
||||
connection.subscriptionBuilder()
|
||||
.subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);
|
||||
|
||||
// Subscribe to all tables (development only — cannot mix with Subscribe)
|
||||
connection.subscriptionBuilder().subscribeToAllTables();
|
||||
|
||||
// Subscription handle for later unsubscribe
|
||||
const handle = connection.subscriptionBuilder()
|
||||
.onApplied(() => console.log('Subscribed'))
|
||||
.subscribe('SELECT * FROM player');
|
||||
|
||||
handle.unsubscribeThen(() => console.log('Unsubscribed'));
|
||||
```
|
||||
|
||||
## Accessing Table Data
|
||||
|
||||
```typescript
|
||||
for (const player of connection.db.player.iter()) { console.log(player.name); }
|
||||
const players = Array.from(connection.db.player.iter());
|
||||
const count = connection.db.player.count();
|
||||
const player = connection.db.player.id.find(42n);
|
||||
```
|
||||
|
||||
## Table Event Callbacks
|
||||
|
||||
```typescript
|
||||
connection.db.player.onInsert((ctx, player) => console.log('New:', player.name));
|
||||
connection.db.player.onDelete((ctx, player) => console.log('Left:', player.name));
|
||||
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));
|
||||
```
|
||||
|
||||
## Calling Reducers
|
||||
|
||||
**CRITICAL: Use object syntax, not positional arguments.**
|
||||
|
||||
```typescript
|
||||
connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });
|
||||
```
|
||||
|
||||
### Snake_case to camelCase conversion
|
||||
- Server: `export const do_something = spacetimedb.reducer(...)`
|
||||
- Client: `conn.reducers.doSomething({ ... })`
|
||||
|
||||
---
|
||||
|
||||
## Identity and Authentication
|
||||
|
||||
- `identity` and `token` are provided in the `onConnect` callback (see Client Connection above)
|
||||
- `identity.toHexString()` for display or logging
|
||||
- Omit `.withToken()` for anonymous connection — server assigns a new identity
|
||||
- Pass a stale/invalid token: server issues a new identity and token in `onConnect`
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Connection-level errors (`.onConnectError`, `.onDisconnect`) are shown in the Client Connection example above.
|
||||
|
||||
```typescript
|
||||
// Subscription error
|
||||
connection.subscriptionBuilder()
|
||||
.onApplied(() => console.log('Subscribed'))
|
||||
.onError((ctx) => console.error('Subscription error:', ctx.event))
|
||||
.subscribe('SELECT * FROM player');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server-Side Module Development
|
||||
|
||||
### Table Definition
|
||||
|
||||
```typescript
|
||||
import { schema, table, t } from 'spacetimedb/server';
|
||||
|
||||
export const Task = table({
|
||||
name: 'task',
|
||||
public: true,
|
||||
indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
|
||||
}, {
|
||||
id: t.u64().primaryKey().autoInc(),
|
||||
ownerId: t.identity(),
|
||||
title: t.string(),
|
||||
createdAt: t.timestamp(),
|
||||
});
|
||||
```
|
||||
|
||||
### Column types
|
||||
|
||||
```typescript
|
||||
t.identity() // User identity
|
||||
t.u64() // Unsigned 64-bit integer (use for IDs)
|
||||
t.string() // Text
|
||||
t.bool() // Boolean
|
||||
t.timestamp() // Timestamp
|
||||
t.scheduleAt() // For scheduled tables only
|
||||
t.object('Name', {}) // Product types (nested objects)
|
||||
t.enum('Name', {}) // Sum types (tagged unions)
|
||||
t.string().optional() // Nullable
|
||||
```
|
||||
|
||||
> BigInt syntax: All `u64`/`i64` fields use `0n`, `1n`, not `0`, `1`.
|
||||
|
||||
### Schema export
|
||||
|
||||
```typescript
|
||||
const spacetimedb = schema({ Task, Player });
|
||||
export default spacetimedb;
|
||||
```
|
||||
|
||||
### Reducer Definition (2.0)
|
||||
|
||||
**Name comes from the export — NOT from a string argument.**
|
||||
|
||||
```typescript
|
||||
import spacetimedb from './schema';
|
||||
import { t, SenderError } from 'spacetimedb/server';
|
||||
|
||||
export const create_task = spacetimedb.reducer(
|
||||
{ title: t.string() },
|
||||
(ctx, { title }) => {
|
||||
if (!title) throw new SenderError('title required');
|
||||
ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Update Pattern
|
||||
|
||||
```typescript
|
||||
const existing = ctx.db.task.id.find(taskId);
|
||||
if (!existing) throw new SenderError('Task not found');
|
||||
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });
|
||||
```
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
```typescript
|
||||
spacetimedb.clientConnected((ctx) => { /* ctx.sender is the connecting identity */ });
|
||||
spacetimedb.clientDisconnected((ctx) => { /* clean up */ });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Tables (2.0)
|
||||
|
||||
Reducer callbacks are removed in 2.0. Use event tables + `onInsert` instead.
|
||||
|
||||
```typescript
|
||||
export const DamageEvent = table(
|
||||
{ name: 'damage_event', public: true, event: true },
|
||||
{ target: t.identity(), amount: t.u32() }
|
||||
);
|
||||
|
||||
export const deal_damage = spacetimedb.reducer(
|
||||
{ target: t.identity(), amount: t.u32() },
|
||||
(ctx, { target, amount }) => {
|
||||
ctx.db.damageEvent.insert({ target, amount });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Client subscribes and uses `onInsert`:
|
||||
```typescript
|
||||
conn.db.damageEvent.onInsert((ctx, evt) => {
|
||||
playDamageAnimation(evt.target, evt.amount);
|
||||
});
|
||||
```
|
||||
|
||||
Event tables must be subscribed explicitly — they are excluded from `subscribeToAllTables()`.
|
||||
|
||||
---
|
||||
|
||||
## Views
|
||||
|
||||
### ViewContext vs AnonymousViewContext
|
||||
|
||||
```typescript
|
||||
// ViewContext — has ctx.sender, result varies per user
|
||||
spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => {
|
||||
return [...ctx.db.item.by_owner.filter(ctx.sender)];
|
||||
});
|
||||
|
||||
// AnonymousViewContext — no ctx.sender, same result for everyone (better perf)
|
||||
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(Player.rowType), (ctx) => {
|
||||
return ctx.from.player.where(p => p.score.gt(1000));
|
||||
});
|
||||
```
|
||||
|
||||
Views can only use index lookups — `.iter()` is NOT allowed.
|
||||
|
||||
---
|
||||
|
||||
## Scheduled Tables
|
||||
|
||||
```typescript
|
||||
export const CleanupJob = table({
|
||||
name: 'cleanup_job',
|
||||
scheduled: () => run_cleanup // function returning the exported reducer
|
||||
}, {
|
||||
scheduledId: t.u64().primaryKey().autoInc(),
|
||||
scheduledAt: t.scheduleAt(),
|
||||
targetId: t.u64(),
|
||||
});
|
||||
|
||||
export const run_cleanup = spacetimedb.reducer(
|
||||
{ arg: CleanupJob.rowType },
|
||||
(ctx, { arg }) => { /* arg.scheduledId, arg.targetId available */ }
|
||||
);
|
||||
|
||||
// Schedule a job
|
||||
import { ScheduleAt } from 'spacetimedb';
|
||||
ctx.db.cleanupJob.insert({
|
||||
scheduledId: 0n,
|
||||
scheduledAt: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + 60_000_000n),
|
||||
targetId: someId
|
||||
});
|
||||
```
|
||||
|
||||
### ScheduleAt on Client
|
||||
|
||||
```typescript
|
||||
// ScheduleAt is a tagged union on the client
|
||||
// { tag: 'Time', value: Timestamp } or { tag: 'Interval', value: TimeDuration }
|
||||
const schedule = row.scheduledAt;
|
||||
if (schedule.tag === 'Time') {
|
||||
const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timestamps
|
||||
|
||||
### Server-side
|
||||
```typescript
|
||||
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
|
||||
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;
|
||||
```
|
||||
|
||||
### Client-side
|
||||
```typescript
|
||||
// Timestamps are objects with BigInt, not numbers
|
||||
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Procedures (Beta)
|
||||
|
||||
```typescript
|
||||
export const fetch_data = spacetimedb.procedure(
|
||||
{ url: t.string() }, t.string(),
|
||||
(ctx, { url }) => {
|
||||
const response = ctx.http.fetch(url);
|
||||
ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
|
||||
return response.text();
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Procedures don't have `ctx.db` — use `ctx.withTx(tx => tx.db...)`.
|
||||
|
||||
---
|
||||
|
||||
## React Integration
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
|
||||
import { DbConnection, tables } from './module_bindings';
|
||||
|
||||
function Root() {
|
||||
const connectionBuilder = useMemo(() =>
|
||||
DbConnection.builder()
|
||||
.withUri('ws://localhost:3000')
|
||||
.withDatabaseName('my_game')
|
||||
.withToken(localStorage.getItem('auth_token') || undefined)
|
||||
.onConnect((conn, identity, token) => {
|
||||
localStorage.setItem('auth_token', token);
|
||||
conn.subscriptionBuilder().subscribe(tables.player);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
|
||||
<App />
|
||||
</SpacetimeDBProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayerList() {
|
||||
const [players, isReady] = useTable(tables.player);
|
||||
if (!isReady) return <div>Loading...</div>;
|
||||
return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Server (`backend/spacetimedb/`)
|
||||
```
|
||||
src/schema.ts -> Tables, export spacetimedb
|
||||
src/index.ts -> Reducers, lifecycle, import schema
|
||||
package.json -> { "type": "module", "dependencies": { "spacetimedb": "^2.0.0" } }
|
||||
tsconfig.json -> Standard config
|
||||
```
|
||||
|
||||
### Client (`client/`)
|
||||
```
|
||||
src/module_bindings/ -> Generated (spacetime generate)
|
||||
src/main.tsx -> Provider, connection setup
|
||||
src/App.tsx -> UI components
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
spacetime start
|
||||
spacetime publish <module-name> --module-path <backend-dir>
|
||||
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
|
||||
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
|
||||
spacetime logs <module-name>
|
||||
```
|
||||
292
.codex/skills/spacetimedb-unity/SKILL.md
Normal file
292
.codex/skills/spacetimedb-unity/SKILL.md
Normal file
@@ -0,0 +1,292 @@
|
||||
---
|
||||
name: spacetimedb-unity
|
||||
description: Integrate SpacetimeDB with Unity game projects. Use when building Unity clients with MonoBehaviour lifecycle, FrameTick, and PlayerPrefs token persistence.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: clockworklabs
|
||||
version: "2.0"
|
||||
tested_with: "SpacetimeDB 2.0, Unity 2022.3+"
|
||||
---
|
||||
|
||||
# SpacetimeDB Unity Integration
|
||||
|
||||
This skill covers Unity-specific patterns for connecting to SpacetimeDB. For server-side module development and general C# SDK usage, see the `spacetimedb-csharp` skill.
|
||||
|
||||
---
|
||||
|
||||
## HALLUCINATED APIs — DO NOT USE
|
||||
|
||||
```csharp
|
||||
// WRONG — these do not exist in Unity SDK
|
||||
SpacetimeDBClient.instance.Connect(...); // Use DbConnection.Builder()
|
||||
SpacetimeDBClient.instance.Subscribe(...); // Use conn.SubscriptionBuilder()
|
||||
NetworkManager.RegisterReducer(...); // SpacetimeDB is not a Unity networking plugin
|
||||
|
||||
// WRONG — old 1.0 patterns
|
||||
.WithModuleName("my-db") // Use .WithDatabaseName() (2.0)
|
||||
ScheduleAt.Time(futureTime) // Use new ScheduleAt.Time(futureTime)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Wrong | Right | Error |
|
||||
|-------|-------|-------|
|
||||
| Not calling `FrameTick()` | `conn?.FrameTick()` in `Update()` | No callbacks fire |
|
||||
| Accessing `conn.Db` from background thread | Copy data in callback, use on main thread | Data races / crashes |
|
||||
| Forgetting `DontDestroyOnLoad` | Add to manager `Awake()` | Connection lost on scene load |
|
||||
| Connecting in `Update()` | Connect in `Start()` or on user action | Reconnects every frame |
|
||||
| Not saving auth token | `PlayerPrefs.SetString(...)` in `OnConnect` | New identity every session |
|
||||
| Missing generated bindings | Run `spacetime generate --lang csharp` | Compile errors |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Add via Unity Package Manager using the git URL:
|
||||
|
||||
```
|
||||
https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk.git
|
||||
```
|
||||
|
||||
**Window > Package Manager > + > Add package from git URL**
|
||||
|
||||
---
|
||||
|
||||
## Generate Module Bindings
|
||||
|
||||
```bash
|
||||
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path PATH_TO_MODULE
|
||||
```
|
||||
|
||||
Place generated files in your Assets folder so Unity compiles them.
|
||||
|
||||
---
|
||||
|
||||
## SpacetimeManager Singleton
|
||||
|
||||
The core pattern for Unity integration. This MonoBehaviour manages the connection lifecycle.
|
||||
|
||||
```csharp
|
||||
using UnityEngine;
|
||||
using SpacetimeDB;
|
||||
using SpacetimeDB.Types;
|
||||
|
||||
public class SpacetimeManager : MonoBehaviour
|
||||
{
|
||||
private const string TOKEN_KEY = "SpacetimeAuthToken";
|
||||
private const string SERVER_URI = "http://localhost:3000";
|
||||
private const string DATABASE_NAME = "my-game";
|
||||
|
||||
public static SpacetimeManager Instance { get; private set; }
|
||||
public DbConnection Connection { get; private set; }
|
||||
public Identity LocalIdentity { get; private set; }
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
string savedToken = PlayerPrefs.GetString(TOKEN_KEY, null);
|
||||
|
||||
Connection = DbConnection.Builder()
|
||||
.WithUri(SERVER_URI)
|
||||
.WithDatabaseName(DATABASE_NAME)
|
||||
.WithToken(savedToken)
|
||||
.OnConnect(OnConnected)
|
||||
.OnConnectError(err => Debug.LogError($"Connection failed: {err}"))
|
||||
.OnDisconnect((conn, err) => {
|
||||
if (err != null) Debug.LogError($"Disconnected: {err}");
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
Connection?.FrameTick();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
Connection?.Disconnect();
|
||||
}
|
||||
|
||||
private void OnConnected(DbConnection conn, Identity identity, string authToken)
|
||||
{
|
||||
LocalIdentity = identity;
|
||||
PlayerPrefs.SetString(TOKEN_KEY, authToken);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
Debug.Log($"Connected as: {identity}");
|
||||
|
||||
conn.SubscriptionBuilder()
|
||||
.OnApplied(OnSubscriptionApplied)
|
||||
.SubscribeToAllTables();
|
||||
}
|
||||
|
||||
private void OnSubscriptionApplied(SubscriptionEventContext ctx)
|
||||
{
|
||||
Debug.Log("Subscription applied — game state loaded");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FrameTick — Critical
|
||||
|
||||
**`FrameTick()` must be called every frame in `Update()`.** The SDK queues all network messages and only processes them when you call `FrameTick()`. Without it:
|
||||
- No callbacks fire (OnInsert, OnUpdate, OnDelete, reducer callbacks)
|
||||
- The client appears frozen
|
||||
|
||||
```csharp
|
||||
void Update()
|
||||
{
|
||||
Connection?.FrameTick();
|
||||
}
|
||||
```
|
||||
|
||||
**Thread safety**: `FrameTick()` processes messages on the calling thread (the main thread in Unity). Do NOT call it from a background thread. Do NOT access `conn.Db` from background threads.
|
||||
|
||||
---
|
||||
|
||||
## Subscribing to Tables
|
||||
|
||||
Subscribe in the `OnConnected` callback:
|
||||
|
||||
```csharp
|
||||
private void OnConnected(DbConnection conn, Identity identity, string authToken)
|
||||
{
|
||||
// ...save token...
|
||||
|
||||
// Development: subscribe to all
|
||||
conn.SubscriptionBuilder()
|
||||
.OnApplied(OnSubscriptionApplied)
|
||||
.SubscribeToAllTables();
|
||||
|
||||
// Production: subscribe to specific tables
|
||||
conn.SubscriptionBuilder()
|
||||
.OnApplied(OnSubscriptionApplied)
|
||||
.Subscribe(new[] {
|
||||
"SELECT * FROM player",
|
||||
"SELECT * FROM game_state"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Row Callbacks for Game State
|
||||
|
||||
Register callbacks to update Unity GameObjects when table data changes.
|
||||
|
||||
```csharp
|
||||
void RegisterCallbacks()
|
||||
{
|
||||
Connection.Db.Player.OnInsert += (EventContext ctx, Player player) => {
|
||||
SpawnPlayerObject(player);
|
||||
};
|
||||
|
||||
Connection.Db.Player.OnDelete += (EventContext ctx, Player player) => {
|
||||
DestroyPlayerObject(player.Id);
|
||||
};
|
||||
|
||||
Connection.Db.Player.OnUpdate += (EventContext ctx, Player oldPlayer, Player newPlayer) => {
|
||||
UpdatePlayerObject(newPlayer);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Register these in `OnSubscriptionApplied` (after initial data is loaded) or in `Start()` before connecting.
|
||||
|
||||
---
|
||||
|
||||
## Calling Reducers from UI
|
||||
|
||||
```csharp
|
||||
public class GameUI : MonoBehaviour
|
||||
{
|
||||
public void OnMoveButtonClicked(Vector2 direction)
|
||||
{
|
||||
SpacetimeManager.Instance.Connection.Reducers.MovePlayer(direction.x, direction.y);
|
||||
}
|
||||
|
||||
public void OnSendChat(string message)
|
||||
{
|
||||
SpacetimeManager.Instance.Connection.Reducers.SendMessage(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reducer Callbacks
|
||||
|
||||
```csharp
|
||||
SpacetimeManager.Instance.Connection.Reducers.OnSendMessage += (ReducerEventContext ctx, string text) => {
|
||||
if (ctx.Event.Status is Status.Committed)
|
||||
Debug.Log($"Message sent: {text}");
|
||||
else if (ctx.Event.Status is Status.Failed(var reason))
|
||||
Debug.LogError($"Send failed: {reason}");
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reading the Client Cache
|
||||
|
||||
```csharp
|
||||
// Find by primary key
|
||||
if (Connection.Db.Player.Id.Find(playerId) is Player player)
|
||||
{
|
||||
Debug.Log($"Player: {player.Name}");
|
||||
}
|
||||
|
||||
// Iterate all
|
||||
foreach (var p in Connection.Db.Player.Iter())
|
||||
{
|
||||
Debug.Log(p.Name);
|
||||
}
|
||||
|
||||
// Filter by index
|
||||
foreach (var p in Connection.Db.Player.Level.Filter(5))
|
||||
{
|
||||
Debug.Log($"Level 5: {p.Name}");
|
||||
}
|
||||
|
||||
// Count
|
||||
int total = Connection.Db.Player.Count;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unity-Specific Considerations
|
||||
|
||||
### Main Thread Only
|
||||
All SpacetimeDB SDK calls (`FrameTick`, `conn.Db` access, reducer calls) must happen on the main thread. If you need to pass data to a background thread, copy it first in the callback.
|
||||
|
||||
### Scene Loading
|
||||
Use `DontDestroyOnLoad(gameObject)` on the SpacetimeManager to prevent the connection from being destroyed during scene transitions. Without it, the connection drops every time you load a new scene.
|
||||
|
||||
### IL2CPP / AOT
|
||||
The SpacetimeDB SDK uses code generation. If you encounter issues with IL2CPP builds:
|
||||
- Ensure generated bindings are up to date
|
||||
- Check that `link.xml` preserves SpacetimeDB types if you use assembly stripping
|
||||
|
||||
### Token Persistence
|
||||
Token save/load via `PlayerPrefs` is demonstrated in the SpacetimeManager singleton above. If the token is stale or invalid, the server issues a new identity and token in the `OnConnect` callback.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
spacetime start
|
||||
spacetime publish <module-name> --module-path <backend-dir>
|
||||
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
|
||||
spacetime generate --lang csharp --out-dir Assets/SpacetimeDB/module_bindings --module-path <backend-dir>
|
||||
spacetime logs <module-name>
|
||||
```
|
||||
35281
.codex/tmp-schema.json
Normal file
35281
.codex/tmp-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
62
.env.example
62
.env.example
@@ -13,23 +13,29 @@ VITE_LLM_PROXY_BASE_URL="/api/llm"
|
||||
# Optional frontend override for the local custom-world scene image proxy path.
|
||||
VITE_SCENE_IMAGE_PROXY_BASE_URL="/api/custom-world/scene-image"
|
||||
|
||||
# Local Node backend address and target used by Vite's dev proxy for runtime API routes.
|
||||
NODE_SERVER_ADDR=":8081"
|
||||
NODE_SERVER_TARGET="http://127.0.0.1:8081"
|
||||
# Runtime API routes use Rust Axum api-server.
|
||||
RUST_SERVER_TARGET="http://127.0.0.1:3100"
|
||||
# Optional hard override. When set, it wins over RUST_SERVER_TARGET.
|
||||
GENARRATIVE_RUNTIME_SERVER_TARGET=""
|
||||
|
||||
# Local Caddy upstream target used for dist-based testing.
|
||||
CADDY_API_UPSTREAM="http://127.0.0.1:8081"
|
||||
# Rust api-server local target used by the Big Fish / Puzzle compatibility gateways
|
||||
# and by the standalone Rust dev / deploy scripts.
|
||||
GENARRATIVE_API_PORT="3100"
|
||||
GENARRATIVE_API_TARGET="http://127.0.0.1:3100"
|
||||
GENARRATIVE_ADMIN_USERNAME=""
|
||||
GENARRATIVE_ADMIN_PASSWORD=""
|
||||
GENARRATIVE_ADMIN_TOKEN_TTL_SECONDS="14400"
|
||||
GENARRATIVE_INTERNAL_API_SECRET="CHANGE_ME_FOR_PRODUCTION"
|
||||
GENARRATIVE_SPACETIME_SERVER_URL="http://127.0.0.1:3001"
|
||||
GENARRATIVE_SPACETIME_DATABASE="genarrative-dev"
|
||||
GENARRATIVE_SPACETIME_POOL_SIZE="4"
|
||||
|
||||
# Editor and asset tool APIs. Defaults are enabled outside production and
|
||||
# disabled in production unless explicitly enabled.
|
||||
EDITOR_API_ENABLED="true"
|
||||
ASSETS_API_ENABLED="true"
|
||||
|
||||
# Node backend PostgreSQL connection string.
|
||||
# Runtime persistence now uses PostgreSQL as the only formal backend baseline.
|
||||
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/genarrative"
|
||||
|
||||
# Node backend JWT settings.
|
||||
# Rust api-server JWT settings.
|
||||
JWT_SECRET="CHANGE_ME_FOR_PRODUCTION"
|
||||
# Access token 有效期。
|
||||
JWT_EXPIRES_IN="2h"
|
||||
@@ -40,6 +46,8 @@ AUTH_REFRESH_SESSION_TTL_DAYS="30"
|
||||
AUTH_REFRESH_COOKIE_PATH="/api/auth"
|
||||
AUTH_REFRESH_COOKIE_SAME_SITE="Lax"
|
||||
AUTH_REFRESH_COOKIE_SECURE="false"
|
||||
# Rust 鉴权快照路径;包含 password_hash 与 refresh token hash,只能放服务端私有目录。
|
||||
GENARRATIVE_AUTH_STORE_PATH="server-rs/.data/auth-store.json"
|
||||
|
||||
# 手机号验证码登录配置(阿里云 PNVS)。
|
||||
# 正式环境请改成你自己的 AccessKey 和短信签名/模板。
|
||||
@@ -76,24 +84,52 @@ SMS_AUTH_BLOCK_IP_DURATION_MINUTES="30"
|
||||
|
||||
# 仅开发环境:允许本地开发测试自动走游客账号。
|
||||
# 一旦你已经启用手机号/微信登录,建议改成 `false`,这样会直接进入真实登录界面。
|
||||
VITE_AUTH_ALLOW_DEV_GUEST="true"
|
||||
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||
|
||||
# 微信登录配置。
|
||||
# 当前实现已支持微信登录骨架与 mock 联调;正式联调需补齐开放平台 AppID / AppSecret。
|
||||
# 当前实现已支持:
|
||||
# 1. `WECHAT_AUTH_PROVIDER="mock"` 的本地假回调联调
|
||||
# 2. `WECHAT_AUTH_PROVIDER="real"` 的真实微信 OAuth 回调
|
||||
# 正式联调时除了补齐 AppID / AppSecret,还要确保微信开放平台回调域名与
|
||||
# `WECHAT_CALLBACK_PATH` 拼出的完整地址一致。
|
||||
WECHAT_AUTH_ENABLED="false"
|
||||
WECHAT_AUTH_PROVIDER="wechat"
|
||||
WECHAT_AUTH_PROVIDER="mock"
|
||||
WECHAT_APP_ID=""
|
||||
WECHAT_APP_SECRET=""
|
||||
WECHAT_CALLBACK_PATH="/api/auth/wechat/callback"
|
||||
WECHAT_REDIRECT_PATH="/"
|
||||
WECHAT_AUTHORIZE_ENDPOINT="https://open.weixin.qq.com/connect/qrconnect"
|
||||
WECHAT_ACCESS_TOKEN_ENDPOINT="https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||
WECHAT_USER_INFO_ENDPOINT="https://api.weixin.qq.com/sns/userinfo"
|
||||
WECHAT_STATE_TTL_MINUTES="15"
|
||||
WECHAT_MOCK_USER_ID="wx-mock-user"
|
||||
WECHAT_MOCK_UNION_ID="wx-mock-union"
|
||||
WECHAT_MOCK_DISPLAY_NAME="微信旅人"
|
||||
WECHAT_MOCK_AVATAR_URL=""
|
||||
|
||||
# Model name for chat completions.
|
||||
VITE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
|
||||
|
||||
# Optional: enable upstream web search for RPG story text generation.
|
||||
RPG_LLM_WEB_SEARCH_ENABLED="true"
|
||||
|
||||
# Server-side DashScope endpoint and API key used by the local scene-image proxy.
|
||||
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
|
||||
|
||||
# 阿里云 OSS 配置。
|
||||
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
|
||||
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
|
||||
# 当前正式口径按私有 bucket 处理,后续在 SpacetimeDB 中存 `bucket + object_key` 两列。
|
||||
ALIYUN_OSS_BUCKET=""
|
||||
ALIYUN_OSS_ENDPOINT="oss-cn-shanghai.aliyuncs.com"
|
||||
ALIYUN_OSS_ACCESS_KEY_ID=""
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET=""
|
||||
ALIYUN_OSS_READ_EXPIRE_SECONDS="600"
|
||||
ALIYUN_OSS_POST_EXPIRE_SECONDS="600"
|
||||
ALIYUN_OSS_POST_MAX_SIZE_BYTES="20971520"
|
||||
ALIYUN_OSS_SUCCESS_ACTION_STATUS="200"
|
||||
|
||||
# Optional model name for custom-world scene image generation.
|
||||
DASHSCOPE_IMAGE_MODEL="wan2.7-image"
|
||||
|
||||
|
||||
19
.env.local
19
.env.local
@@ -1,4 +1,4 @@
|
||||
VITE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||
VITE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||
LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
|
||||
ARK_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
|
||||
ARK_CHARACTER_VIDEO_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||
@@ -12,6 +12,7 @@ VOLCENGINE_ACCESS_KEY_ID="AKLTZWFjMmYzZTdjZTIxNDRiNTkzMTZiMTk2NzVmNTUxOGI"
|
||||
VOLCENGINE_SECRET_ACCESS_KEY="TURRMk56bGhZalE0TjJReE5ERmpNMkpoTUdaa1lqRmtaVGt5TVRrM1lXSQ=="
|
||||
WECHAT_AUTH_ENABLED="false"
|
||||
WECHAT_AUTH_PROVIDER="mock"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
SMS_AUTH_ENABLED="true"
|
||||
SMS_AUTH_PROVIDER="aliyun"
|
||||
@@ -33,10 +34,22 @@ ALIYUN_SMS_RETURN_VERIFY_CODE="false"
|
||||
|
||||
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||
|
||||
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/genarrative"
|
||||
|
||||
# 启用服务端大模型调试日志(记录所有输入输出)
|
||||
LLM_DEBUG_LOG="true"
|
||||
|
||||
# 注意:不要在客户端启用调试日志,避免敏感数据泄露
|
||||
# VITE_LLM_DEBUG_LOG="false"
|
||||
|
||||
ALIYUN_OSS_BUCKET="xushi-dev"
|
||||
ALIYUN_OSS_REGION="oss-cn-beijing"
|
||||
ALIYUN_OSS_ENDPOINT="oss-cn-beijing.aliyuncs.com"
|
||||
ALIYUN_OSS_ACCESS_KEY_ID="LTAI5t7aiyw6uDFW4miJvU8f"
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET="XblWGE6CO1WLnSBdMRVpL6lut4GSoS"
|
||||
|
||||
# Local Rust backend target for Vite dev proxy.
|
||||
RUST_SERVER_TARGET="http://127.0.0.1:3100"
|
||||
GENARRATIVE_API_TARGET="http://127.0.0.1:3100"
|
||||
|
||||
GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL="https://maincloud.spacetimedb.com"
|
||||
GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE="xushi-p4wfr"
|
||||
GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMUtQRzVXQlhTTjVCRDAyTjBNSlNONFJCNyIsImlzcyI6Imh0dHBzOi8vYXV0aC5zcGFjZXRpbWVkYi5jb20iLCJhdWQiOiJzcGFjZXRpbWVkYiIsImlhdCI6MTc3NjUxMjAyMiwiZXhwIjoxODM5NTg0MDIyfQ.UguSQDajalekrqs9oiUqLZiWjWK7VTgMQfdLVOhBQZpKX0VYUhNMSok9oBMJ4X655_NxV5TUUXZ4ON4HSJZrMMPc9aZyhS1b3i36vqI_zMPwLrAgfb1MqY5o0wNFl6Y0m0UQ3nsu7ZYxmxxgzF4My7So0Pv75QfXFS3-Uq1-QyO7lCxxgQ6vySbP_PEr7FZJsdPkNvAfP7mTaUh0yaV6SI7jXBsZ_mfdcWtElCNuvR9J3hvfAbx1qyeTgCJtgH4kNhiEOEIAYEFMEQkXd4rdLszmEgtlFubYZPsbMgqeZKx73feU6eGxlYhyPiRHF4AdosIfk3x2MAm_WzOd3efXDQ"
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -5,21 +5,27 @@ coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
/.codex-logs/
|
||||
/.codex-cargo-home-*/
|
||||
/.codex-cache*/
|
||||
/.tmp*/
|
||||
/.idea/
|
||||
.preview.*
|
||||
tmp_*
|
||||
tmp/
|
||||
npc-editor-*
|
||||
temp-write-check.txt
|
||||
temp-build-goal-check/
|
||||
/server-rs-codex-*/
|
||||
/server-rs/target-*/
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
/public/generated-custom-world-scenes
|
||||
temp*build*/
|
||||
/server-node/dist/
|
||||
/server-node/logs/*
|
||||
!/server-node/logs/.gitkeep
|
||||
/server-node/data/*
|
||||
!/server-node/data/.gitkeep
|
||||
/server-rs/target/
|
||||
/server-rs/.spacetimedb/
|
||||
/server-rs/.data/
|
||||
/public/generated-animations
|
||||
/public/generated-character-drafts
|
||||
/public/generated-characters
|
||||
/.codex-temp
|
||||
/target/
|
||||
|
||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
||||
PreGameSelectionFlow.tsx
|
||||
mod.rs
|
||||
8
.idea/Genarrative.iml
generated
8
.idea/Genarrative.iml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.vite/deps/_metadata.json
Normal file
8
.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"hash": "4121bbf9",
|
||||
"configHash": "eecfdd02",
|
||||
"lockfileHash": "97905bb0",
|
||||
"browserHash": "5b2ac70e",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
.vite/deps/package.json
Normal file
3
.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
15
AGENTS.md
15
AGENTS.md
@@ -1,6 +1,7 @@
|
||||
# AGENTS.md
|
||||
|
||||
## 项目约束
|
||||
- 在修改server-rs的内容时,不要去兼容server-node中的任何内容,只允许参考,以及把server-node中未迁移到server-rs的内容迁移过来
|
||||
- 代码需要有完善的中文注释
|
||||
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
|
||||
- 对工程的修改不仅要落地到代码更面,还要更改对应文档,若没有生成新的文档,文档统一存在doc目录中
|
||||
@@ -13,11 +14,23 @@
|
||||
- UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。
|
||||
- 不要在gitignore中添加.env.local文件。
|
||||
- 严格遵循简洁的代码风格
|
||||
- 前端只负责做表现,所有的逻辑、数据都放到Express后端进行运算和存储。
|
||||
- 前端只负责做表现,所有的逻辑、数据都放到后端工程,后端使用server-rs中用Rust+spacetimeDB的方案实现,禁止继续使用server-node(Express)和postgreSQL
|
||||
- 后端采用多crate设计
|
||||
- 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。
|
||||
- 禁止将功能说明描述类的文本默认写入UI界面中。
|
||||
- prd文档中每个模块的描述要落地设计到可以精准编码到位,不能出现需求落地漂移。
|
||||
- 点击按钮弹出独立的面板的设计不要实现成在当前面板下面显示内容。
|
||||
- 每个阶段任务完成后自动压缩上下文,确保后续阶段在清晰、低噪音的上下文基础上继续推进。
|
||||
- 凡是涉及 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)
|
||||
- [$spacetimedb-typescript](.codex\\skills\\spacetimedb-typescript\\SKILL.md)
|
||||
- 涉及 `spacetime` CLI、发布、绑定生成、本地联调时,按 `spacetimedb-cli` 执行。
|
||||
- 涉及 `crates/spacetime-module` 的表、reducer、view、Rust API 使用时,按 `spacetimedb-rust` 与 `spacetimedb-concepts` 执行。
|
||||
- 涉及前端或 Node 侧的 SpacetimeDB TypeScript SDK、订阅、绑定使用时,按 `spacetimedb-typescript` 与 `spacetimedb-concepts` 执行。
|
||||
- 若仓库内旧实现或旧文档与这些 skill 冲突,先修正文档和方案,再继续编码。
|
||||
- 修改后端代码后,必须使用 `npm run api-server:maincloud` 自动重新运行后端,并执行相应自动测试;不要再使用旧的后端重启命令。
|
||||
|
||||
|
||||
## 文档图谱
|
||||
|
||||
12
BACKEND_REWRITE_TASKLIST.md
Normal file
12
BACKEND_REWRITE_TASKLIST.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# 后端重写任务清单入口
|
||||
|
||||
完整总纲与拆分后的任务文件已统一整理到根目录新建目录:
|
||||
|
||||
- [backend-rewrite-tasklist/README.md](./backend-rewrite-tasklist/README.md)
|
||||
|
||||
其中:
|
||||
|
||||
- 总纲主清单:[backend-rewrite-tasklist/00_MASTER_TASKLIST.md](./backend-rewrite-tasklist/00_MASTER_TASKLIST.md)
|
||||
- 阶段拆分文件入口:[backend-rewrite-tasklist/README.md](./backend-rewrite-tasklist/README.md)
|
||||
|
||||
后续如继续细化任务,请优先在该目录内维护,避免根目录散落多份版本。
|
||||
@@ -18,6 +18,8 @@
|
||||
前置条件:
|
||||
|
||||
- Node.js
|
||||
- Rust / Cargo
|
||||
- SpacetimeDB CLI
|
||||
|
||||
安装依赖:
|
||||
|
||||
@@ -42,9 +44,8 @@ npm run dev
|
||||
|
||||
补充说明:
|
||||
|
||||
- `npm run dev` 会同时启动 Vite 与 Express 后端,适合完整联调。
|
||||
- 如果没有显式配置 `DATABASE_URL`,且本机 `PostgreSQL` 不可用,开发模式会自动回退到内存版 `pg-mem`,方便先跑通鉴权与存档主链。
|
||||
- 如果只想单独启动前端页面,可使用 `npm run dev:web`。
|
||||
- `npm run dev` 会启动 SpacetimeDB standalone、Rust `api-server` 与 Vite 前端,适合完整联调。
|
||||
- 如果只想单独启动前端页面,可使用 `npm run dev:web`,默认代理到本地 Rust `api-server`。
|
||||
|
||||
构建生产包:
|
||||
|
||||
|
||||
154
backend-rewrite-tasklist/00_MASTER_TASKLIST.md
Normal file
154
backend-rewrite-tasklist/00_MASTER_TASKLIST.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# SpacetimeDB + Axum + 阿里云 OSS 后端重写任务总纲
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
关联设计文档:
|
||||
|
||||
- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
|
||||
- [../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)
|
||||
- [../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)
|
||||
|
||||
关联拆分任务:
|
||||
|
||||
- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md)
|
||||
- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md)
|
||||
- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md)
|
||||
- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md)
|
||||
- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md)
|
||||
- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md)
|
||||
- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md)
|
||||
|
||||
## 0. 使用说明
|
||||
|
||||
这份总纲用于把控整体重写节奏,拆分文件用于落地执行。
|
||||
|
||||
执行原则:
|
||||
|
||||
1. 第一阶段优先兼容当前 `/api/*`、`/healthz`、`/generated-*` 访问习惯。
|
||||
2. 不允许先删旧能力再补新能力,必须按能力面平移。
|
||||
3. 以当前 Node 后端 `96` 条路由、`6` 个挂载面、`12` 个模块为最低覆盖基线。
|
||||
4. 每个阶段完成后,都要形成可运行、可回归、可继续迭代的中间态。
|
||||
|
||||
## 1. 总体里程碑
|
||||
|
||||
- [x] `M0`:冻结当前后端能力清单与迁移边界
|
||||
- [ ] `M1`:搭建 Rust 工作区、Axum 主入口与基础中间件
|
||||
- [ ] `M2`:完成鉴权、会话、JWT、refresh cookie 主链迁移
|
||||
- [ ] `M3`:完成 runtime snapshot / settings / profile 迁移
|
||||
- [ ] `M4`:完成 story action 主循环与核心 gameplay reducer 迁移
|
||||
- [ ] `M5`:完成 custom world / agent 主链迁移
|
||||
- [ ] `M6`:完成 assets / OSS 主链迁移
|
||||
- [ ] `M7`:完成联调、回归、部署与切流准备
|
||||
|
||||
## 2. 阶段导航
|
||||
|
||||
### `M0 ~ M2`
|
||||
|
||||
重点:
|
||||
|
||||
1. 冻结能力清单
|
||||
2. 搭建 Rust workspace
|
||||
3. 搭建 Axum 基础设施
|
||||
4. 迁移鉴权、会话、JWT、refresh cookie
|
||||
|
||||
详见:
|
||||
|
||||
- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md)
|
||||
|
||||
### `M3`
|
||||
|
||||
重点:
|
||||
|
||||
1. 迁移 runtime snapshot
|
||||
2. 迁移 settings
|
||||
3. 迁移 profile dashboard / browse history / save archive
|
||||
|
||||
详见:
|
||||
|
||||
- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md)
|
||||
|
||||
### `M4`
|
||||
|
||||
重点:
|
||||
|
||||
1. 迁移 RPG runtime story 主循环
|
||||
2. 迁移 RPG 入口 / session / runtime 对应的后端边界与编译职责
|
||||
3. 兼容当前 story view model 与 state 恢复接口,并与 `rpgEntry / rpgSession / rpgRuntime / rpgRuntimeStory` 口径对齐
|
||||
|
||||
详见:
|
||||
|
||||
- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md)
|
||||
|
||||
### `M5`
|
||||
|
||||
重点:
|
||||
|
||||
1. 迁移 RPG 创作主链:Agent session、result preview、published profile
|
||||
2. 迁移 works / library / gallery / publish / enter-world 配套链路
|
||||
3. 旧 `custom-world/sessions` 传统问答流只按历史兼容台账处理,不再作为当前主链扩展目标
|
||||
|
||||
详见:
|
||||
|
||||
- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md)
|
||||
|
||||
### `M6`
|
||||
|
||||
重点:
|
||||
|
||||
1. 迁移 assets
|
||||
2. 接入阿里云 OSS
|
||||
3. 做旧静态资源路径兼容
|
||||
|
||||
详见:
|
||||
|
||||
- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md)
|
||||
|
||||
### `M7`
|
||||
|
||||
重点:
|
||||
|
||||
1. 联调
|
||||
2. 回归
|
||||
3. 部署
|
||||
4. 观测
|
||||
5. 灰度切流
|
||||
6. 收口 `spacetime-module` 主工程结构,拆分过大的 `src/lib.rs`
|
||||
|
||||
详见:
|
||||
|
||||
- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md)
|
||||
|
||||
## 3. 横向专项
|
||||
|
||||
以下专项贯穿整个迁移期:
|
||||
|
||||
1. contract 与前端兼容
|
||||
2. SpacetimeDB schema 演进治理
|
||||
3. 大对象与缓存治理
|
||||
4. 文档持续维护
|
||||
|
||||
详见:
|
||||
|
||||
- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md)
|
||||
|
||||
## 4. 第一优先级建议执行顺序
|
||||
|
||||
1. 先做 `M0`,冻结基线,避免迁移过程中口径漂移。
|
||||
2. 再做 `M1 + M2`,先把 Axum 壳与鉴权打稳。
|
||||
3. 当前执行顺序允许前置 `M6` 的 OSS 基础设施与直传票据能力,为后续各阶段复用统一资产入口。
|
||||
4. 再做 `M3`,优先跑通快照、设置、profile。
|
||||
5. 再做 `M4`,把 story action 主循环真正迁走。
|
||||
6. 然后做 `M5`,迁 custom world 与 agent。
|
||||
7. 最后收口 `M6` 余下资产绑定、`M7` 部署与切流。
|
||||
|
||||
## 5. 最终验收清单
|
||||
|
||||
- [ ] 当前 `96` 条后端接口已全部迁移或有兼容替代
|
||||
- [ ] 当前 `6` 个挂载面已全部迁移
|
||||
- [ ] 当前 `12` 个内部模块已完成新架构落位
|
||||
- [ ] Axum 已成为唯一 HTTP / SSE / 副作用边界
|
||||
- [ ] SpacetimeDB 已成为唯一运行时状态真相源
|
||||
- [ ] 阿里云 OSS 已成为唯一资产对象仓
|
||||
- [ ] 前端主流程在不大改 UI 的前提下可跑通
|
||||
- [ ] 能完成灰度切流,并保留可回退能力
|
||||
266
backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md
Normal file
266
backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# M0 ~ M2:基础设施与鉴权任务清单
|
||||
|
||||
## M0:冻结能力与重写边界
|
||||
|
||||
### 能力冻结
|
||||
|
||||
- [x] 整理当前后端 6 个挂载面并锁定为重写验收基线
|
||||
交付物:[M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md)
|
||||
- [x] 整理当前后端 96 条路由并生成一份“旧接口 -> 新实现”映射表
|
||||
交付物:[M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md)
|
||||
- [x] 整理当前 12 个内部模块并锁定迁移归属
|
||||
交付物:[M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md)
|
||||
- [x] 整理当前所有 SSE 接口与事件格式
|
||||
交付物:[M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md)
|
||||
- [x] 整理当前所有 `/generated-*` 静态资源前缀
|
||||
交付物:[M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md)
|
||||
- [x] 整理当前前端直接依赖的响应头、envelope、错误格式
|
||||
交付物:[M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md)
|
||||
|
||||
### 仓库边界
|
||||
|
||||
- [x] 确认 Rust 后端新目录名与根目录落位方案
|
||||
交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md)
|
||||
- [x] 确认旧 `server-node/` 在迁移期继续保留,不提前删除
|
||||
交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md)
|
||||
- [x] 确认前端第一阶段仍然只访问 Axum,不直连 SpacetimeDB
|
||||
交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md)
|
||||
- [x] 确认外部副作用统一收口在 Axum,不放进 SpacetimeDB 模块
|
||||
交付物:[M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md)
|
||||
|
||||
### 交付物
|
||||
|
||||
- [x] 新增“接口映射表”文档
|
||||
交付物:[M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md)
|
||||
- [x] 新增“模块迁移清单”文档
|
||||
交付物:[M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md)
|
||||
- [x] 新增“阶段验收矩阵”文档
|
||||
交付物:[M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md](./M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md)
|
||||
|
||||
## M1:Rust 工作区与 Axum 基础设施
|
||||
|
||||
### 工作区搭建
|
||||
|
||||
- [x] 在根目录新增 `server-rs/`
|
||||
交付物:[../server-rs/README.md](../server-rs/README.md)
|
||||
- [x] 创建 workspace `Cargo.toml`
|
||||
交付物:[../server-rs/Cargo.toml](../server-rs/Cargo.toml)
|
||||
- [x] 创建 `crates/api-server`
|
||||
交付物:[../server-rs/crates/api-server/README.md](../server-rs/crates/api-server/README.md)
|
||||
- [x] 创建 `crates/spacetime-module`
|
||||
交付物:[../server-rs/crates/spacetime-module/README.md](../server-rs/crates/spacetime-module/README.md)
|
||||
- [x] 创建 `crates/module-auth`
|
||||
交付物:[../server-rs/crates/module-auth/README.md](../server-rs/crates/module-auth/README.md)
|
||||
- [x] 创建 `crates/module-runtime`
|
||||
交付物:[../server-rs/crates/module-runtime/README.md](../server-rs/crates/module-runtime/README.md)
|
||||
- [x] 创建 `crates/module-story`
|
||||
交付物:[../server-rs/crates/module-story/README.md](../server-rs/crates/module-story/README.md)
|
||||
- [x] 创建 `crates/module-combat`
|
||||
交付物:[../server-rs/crates/module-combat/README.md](../server-rs/crates/module-combat/README.md)
|
||||
- [x] 创建 `crates/module-inventory`
|
||||
交付物:[../server-rs/crates/module-inventory/README.md](../server-rs/crates/module-inventory/README.md)
|
||||
- [x] 创建 `crates/module-npc`
|
||||
交付物:[../server-rs/crates/module-npc/README.md](../server-rs/crates/module-npc/README.md)
|
||||
- [x] 创建 `crates/module-progression`
|
||||
交付物:[../server-rs/crates/module-progression/README.md](../server-rs/crates/module-progression/README.md)
|
||||
- [x] 创建 `crates/module-quest`
|
||||
交付物:[../server-rs/crates/module-quest/README.md](../server-rs/crates/module-quest/README.md)
|
||||
- [x] 创建 `crates/module-runtime-item`
|
||||
交付物:[../server-rs/crates/module-runtime-item/README.md](../server-rs/crates/module-runtime-item/README.md)
|
||||
- [x] 创建 `crates/module-custom-world`
|
||||
交付物:[../server-rs/crates/module-custom-world/README.md](../server-rs/crates/module-custom-world/README.md)
|
||||
- [x] 创建 `crates/module-assets`
|
||||
交付物:[../server-rs/crates/module-assets/README.md](../server-rs/crates/module-assets/README.md)
|
||||
- [x] 创建 `crates/module-ai`
|
||||
交付物:[../server-rs/crates/module-ai/README.md](../server-rs/crates/module-ai/README.md)
|
||||
- [x] 创建 `crates/shared-contracts`
|
||||
交付物:[../server-rs/crates/shared-contracts/README.md](../server-rs/crates/shared-contracts/README.md)
|
||||
- [x] 创建 `crates/shared-kernel`
|
||||
交付物:[../server-rs/crates/shared-kernel/README.md](../server-rs/crates/shared-kernel/README.md)
|
||||
- [x] 创建 `crates/shared-logging`
|
||||
交付物:[../server-rs/crates/shared-logging/README.md](../server-rs/crates/shared-logging/README.md)
|
||||
- [x] 创建 `crates/platform-auth`
|
||||
交付物:[../server-rs/crates/platform-auth/README.md](../server-rs/crates/platform-auth/README.md)
|
||||
- [x] 创建 `crates/platform-oss`
|
||||
交付物:[../server-rs/crates/platform-oss/README.md](../server-rs/crates/platform-oss/README.md)
|
||||
- [x] 创建 `crates/platform-llm`
|
||||
交付物:[../server-rs/crates/platform-llm/README.md](../server-rs/crates/platform-llm/README.md)
|
||||
- [x] 创建 `crates/spacetime-client`
|
||||
交付物:[../server-rs/crates/spacetime-client/README.md](../server-rs/crates/spacetime-client/README.md)
|
||||
- [x] 创建 `crates/tests-support`
|
||||
交付物:[../server-rs/crates/tests-support/README.md](../server-rs/crates/tests-support/README.md)
|
||||
|
||||
### Axum 基础能力
|
||||
|
||||
- [x] 搭建 `main.rs` / `Router` / `with_state`
|
||||
交付物:[../server-rs/crates/api-server/src/main.rs](../server-rs/crates/api-server/src/main.rs)
|
||||
- [x] 接入统一配置加载
|
||||
交付物:[../server-rs/crates/api-server/src/config.rs](../server-rs/crates/api-server/src/config.rs)
|
||||
- [x] 接入统一日志与 tracing
|
||||
交付物:[../docs/technical/RUST_SHARED_LOGGING_CRATE_DESIGN_2026-04-21.md](../docs/technical/RUST_SHARED_LOGGING_CRATE_DESIGN_2026-04-21.md)、[../server-rs/crates/shared-logging/src/lib.rs](../server-rs/crates/shared-logging/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/api-server/src/main.rs](../server-rs/crates/api-server/src/main.rs)
|
||||
- [x] 接入 `request_id` 中间件
|
||||
交付物:[../server-rs/crates/api-server/src/request_context.rs](../server-rs/crates/api-server/src/request_context.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 接入统一错误处理中间件
|
||||
交付物:[../server-rs/crates/api-server/src/http_error.rs](../server-rs/crates/api-server/src/http_error.rs)、[../server-rs/crates/api-server/src/error_middleware.rs](../server-rs/crates/api-server/src/error_middleware.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 接入当前项目兼容的 response envelope
|
||||
交付物:[../server-rs/crates/api-server/src/api_response.rs](../server-rs/crates/api-server/src/api_response.rs)、[../server-rs/crates/api-server/src/request_context.rs](../server-rs/crates/api-server/src/request_context.rs)、[../server-rs/crates/api-server/src/http_error.rs](../server-rs/crates/api-server/src/http_error.rs)
|
||||
- [x] 接入 `x-request-id`
|
||||
交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 接入 `x-api-version`
|
||||
交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs)
|
||||
- [x] 接入 `x-route-version`
|
||||
交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs)
|
||||
- [x] 接入 `x-response-time-ms`
|
||||
交付物:[../server-rs/crates/api-server/src/response_headers.rs](../server-rs/crates/api-server/src/response_headers.rs)、[../server-rs/crates/api-server/src/request_context.rs](../server-rs/crates/api-server/src/request_context.rs)
|
||||
- [x] 实现 `/healthz`
|
||||
交付物:[../server-rs/crates/api-server/src/health.rs](../server-rs/crates/api-server/src/health.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
|
||||
### 基础工程脚本
|
||||
|
||||
- [x] 新增本地开发脚本
|
||||
交付物:[../server-rs/scripts/dev.ps1](../server-rs/scripts/dev.ps1)、[../server-rs/scripts/dev.sh](../server-rs/scripts/dev.sh)
|
||||
- [x] 新增测试脚本
|
||||
交付物:[../server-rs/scripts/test.ps1](../server-rs/scripts/test.ps1)、[../server-rs/scripts/test.sh](../server-rs/scripts/test.sh)
|
||||
- [x] 新增 lint / fmt / clippy / check 脚本
|
||||
交付物:[../server-rs/scripts/check.ps1](../server-rs/scripts/check.ps1)、[../server-rs/scripts/check.sh](../server-rs/scripts/check.sh)
|
||||
- [x] 新增 smoke 脚本
|
||||
交付物:[../server-rs/scripts/smoke.ps1](../server-rs/scripts/smoke.ps1)、[../server-rs/scripts/smoke.sh](../server-rs/scripts/smoke.sh)
|
||||
- [x] 新增 SpacetimeDB 本地开发脚本
|
||||
交付物:[../server-rs/scripts/spacetime-dev.ps1](../server-rs/scripts/spacetime-dev.ps1)、[../server-rs/scripts/spacetime-dev.sh](../server-rs/scripts/spacetime-dev.sh)
|
||||
|
||||
### 阶段验收
|
||||
|
||||
- [x] Axum 服务可独立启动
|
||||
证据:`./server-rs/scripts/smoke.ps1` 已通过,覆盖临时启动 `api-server`、等待 `/healthz` 就绪并验证 raw / envelope 协议。
|
||||
- [x] `/healthz` 返回与当前工程兼容
|
||||
- [x] 基础 response envelope 与 request id 行为稳定
|
||||
证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 envelope 协商与 `/healthz` 头部回写。
|
||||
- [x] Rust workspace 能完整编译通过
|
||||
证据:`cargo check -p api-server --manifest-path server-rs/Cargo.toml` 已通过。
|
||||
|
||||
## M2:鉴权、会话、JWT 与 refresh cookie
|
||||
|
||||
### SpacetimeDB 身份表
|
||||
|
||||
- [x] 设计 `user_account`
|
||||
交付物:[../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)
|
||||
- [x] 设计 `auth_identity`
|
||||
交付物:[../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)
|
||||
- [x] 设计 `refresh_session`
|
||||
交付物:[../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)
|
||||
- [x] 设计 `auth_audit_log`
|
||||
交付物:[../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md)
|
||||
- [x] 设计 `auth_risk_block`
|
||||
交付物:[../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md)
|
||||
- [x] 设计 `sms_auth_event`
|
||||
交付物:[../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md)
|
||||
- [x] 设计 `wechat_auth_state`
|
||||
交付物:[../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md)
|
||||
|
||||
### Axum 鉴权服务
|
||||
|
||||
- [x] 实现密码登录
|
||||
交付物:[../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md](../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现账号自动创建 / 幂等登录兼容策略
|
||||
交付物:[../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md](../docs/technical/PASSWORD_ENTRY_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现 Bearer JWT 校验
|
||||
交付物:[../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md](../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现 refresh cookie 读取
|
||||
交付物:[../docs/technical/PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md](../docs/technical/PLATFORM_AUTH_REFRESH_COOKIE_ADAPTER_DESIGN_2026-04-21.md)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)、[../server-rs/crates/api-server/src/config.rs](../server-rs/crates/api-server/src/config.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现 refresh token 轮换
|
||||
交付物:[../docs/technical/AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md](../docs/technical/AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/platform-auth/src/lib.rs](../server-rs/crates/platform-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth_session.rs](../server-rs/crates/api-server/src/auth_session.rs)、[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现多端会话身份建模与会话列表查询
|
||||
交付物:[../docs/technical/MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md](../docs/technical/MULTI_DEVICE_SESSION_IDENTITY_DESIGN_2026-04-21.md)、[../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)、[../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/session_client.rs](../server-rs/crates/api-server/src/session_client.rs)、[../server-rs/crates/api-server/src/auth_sessions.rs](../server-rs/crates/api-server/src/auth_sessions.rs)、[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../packages/shared/src/contracts/auth.ts](../packages/shared/src/contracts/auth.ts)
|
||||
- [x] 实现会话吊销
|
||||
交付物:[../docs/technical/AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGOUT_CURRENT_SESSION_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)、[../server-rs/crates/api-server/src/auth_session.rs](../server-rs/crates/api-server/src/auth_session.rs)、[../server-rs/crates/api-server/src/logout.rs](../server-rs/crates/api-server/src/logout.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现全端登出
|
||||
交付物:[../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/logout_all.rs](../server-rs/crates/api-server/src/logout_all.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现 `me` 查询
|
||||
交付物:[../docs/technical/AUTH_ME_QUERY_DESIGN_2026-04-21.md](../docs/technical/AUTH_ME_QUERY_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/auth_me.rs](../server-rs/crates/api-server/src/auth_me.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
|
||||
### 手机验证码登录
|
||||
|
||||
- [ ] 接入阿里云短信发送 adapter
|
||||
- [x] 实现发送验证码接口
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现验证码校验接口
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现手机号绑定
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)
|
||||
- [ ] 实现手机号换绑
|
||||
- [x] 实现发送频率限制
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现验证码失败次数限制
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_RATE_LIMIT_AND_FAILURE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [ ] 实现 captcha 触发逻辑
|
||||
- [ ] 实现风控封禁与解除
|
||||
|
||||
### 微信登录
|
||||
|
||||
- [x] 接入微信 OAuth adapter
|
||||
交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_provider.rs](../server-rs/crates/api-server/src/wechat_provider.rs)、[../server-rs/crates/api-server/src/state.rs](../server-rs/crates/api-server/src/state.rs)
|
||||
- [x] 实现 `wechat/start`
|
||||
交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现 `wechat/callback`
|
||||
交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现微信身份绑定
|
||||
交付物:[../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)
|
||||
- [x] 实现微信账号补绑手机号
|
||||
交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 实现桌面端 / 微信内打开场景区分
|
||||
交付物:[../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md](../docs/technical/WECHAT_LOGIN_AXUM_IMPLEMENTATION_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/session_client.rs](../server-rs/crates/api-server/src/session_client.rs)
|
||||
|
||||
### OIDC 与 SpacetimeDB 身份透传
|
||||
|
||||
- [x] 设计 JWT claims
|
||||
交付物:[../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
|
||||
- [x] 确认 `iss/sub/sid/provider/roles` 字段
|
||||
交付物:[../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
|
||||
- [x] 让 Axum 自身可校验 JWT
|
||||
交付物:[../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md](../docs/technical/PLATFORM_AUTH_JWT_ADAPTER_DESIGN_2026-04-21.md)、[../server-rs/crates/platform-auth/README.md](../server-rs/crates/platform-auth/README.md)、[../server-rs/crates/api-server/src/auth.rs](../server-rs/crates/api-server/src/auth.rs)
|
||||
- [ ] 让 SpacetimeDB 可识别 Axum 签发的身份令牌
|
||||
- [ ] 验证 reducer / view 可读取用户身份上下文
|
||||
|
||||
### 当前接口兼容
|
||||
|
||||
- [x] 兼容 `/api/auth/login-options`
|
||||
交付物:[../docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/login_options.rs](../server-rs/crates/api-server/src/login_options.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 兼容 `/api/auth/entry`
|
||||
交付物:[../server-rs/crates/api-server/src/password_entry.rs](../server-rs/crates/api-server/src/password_entry.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 兼容 `/api/auth/me`
|
||||
交付物:[../server-rs/crates/api-server/src/auth_me.rs](../server-rs/crates/api-server/src/auth_me.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 兼容 `/api/auth/logout`
|
||||
交付物:[../server-rs/crates/api-server/src/logout.rs](../server-rs/crates/api-server/src/logout.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 兼容 `/api/auth/logout-all`
|
||||
交付物:[../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md](../docs/technical/AUTH_LOGOUT_ALL_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/logout_all.rs](../server-rs/crates/api-server/src/logout_all.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)
|
||||
- [x] 兼容 `/api/auth/refresh`
|
||||
交付物:[../server-rs/crates/api-server/src/auth_session.rs](../server-rs/crates/api-server/src/auth_session.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)
|
||||
- [x] 兼容 `/api/auth/sessions`
|
||||
交付物:[../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](../docs/technical/AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/auth_sessions.rs](../server-rs/crates/api-server/src/auth_sessions.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)
|
||||
- [ ] 兼容 `/api/auth/sessions/:sessionId/revoke`
|
||||
- [ ] 兼容 `/api/auth/audit-logs`
|
||||
- [ ] 兼容 `/api/auth/risk-blocks`
|
||||
- [ ] 兼容 `/api/auth/risk-blocks/:scopeType/lift`
|
||||
- [x] 兼容 `/api/auth/phone/send-code`
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)
|
||||
- [x] 兼容 `/api/auth/phone/login`
|
||||
交付物:[../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md](../docs/technical/PHONE_AUTH_AXUM_MINIMAL_FLOW_DESIGN_2026-04-21.md)、[../server-rs/crates/api-server/src/phone_auth.rs](../server-rs/crates/api-server/src/phone_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../server-rs/crates/module-auth/src/lib.rs](../server-rs/crates/module-auth/src/lib.rs)
|
||||
- [ ] 兼容 `/api/auth/phone/change`
|
||||
- [x] 兼容 `/api/auth/wechat/start`
|
||||
交付物:[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../src/services/authService.ts](../src/services/authService.ts)
|
||||
- [x] 兼容 `/api/auth/wechat/callback`
|
||||
交付物:[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../src/services/authService.ts](../src/services/authService.ts)
|
||||
- [x] 兼容 `/api/auth/wechat/bind-phone`
|
||||
交付物:[../server-rs/crates/api-server/src/wechat_auth.rs](../server-rs/crates/api-server/src/wechat_auth.rs)、[../server-rs/crates/api-server/src/app.rs](../server-rs/crates/api-server/src/app.rs)、[../src/services/authService.ts](../src/services/authService.ts)
|
||||
|
||||
### 阶段验收
|
||||
|
||||
- [x] 密码登录主链可用
|
||||
证据:`cargo test -p module-auth --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖自动建号、重复登录复用、错密码 `401`、非法用户名 `400` 与 refresh cookie 写回。
|
||||
- [x] refresh cookie 主链可用
|
||||
证据:`cargo test -p module-auth --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 refresh 成功轮换、旧 token 失效、缺少 cookie `401` 与失败时清理 cookie。
|
||||
- [x] 手机验证码主链可用
|
||||
证据:`cargo test -p module-auth phone --manifest-path server-rs/Cargo.toml -- --nocapture`、`cargo test -p api-server phone --manifest-path server-rs/Cargo.toml -- --nocapture` 已通过,覆盖发送验证码、同场景冷却 `429`、验证码错误次数耗尽 `429`、重新发送后恢复登录,以及手机号登录建号/复用与 refresh cookie 写回。
|
||||
- [x] 微信登录主链可用
|
||||
证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server wechat --manifest-path server-rs/Cargo.toml`、`cargo test -p module-auth --manifest-path server-rs/Cargo.toml` 已通过,覆盖 `wechat/start`、`wechat/callback`、待绑定会话签发、手机号补绑并入已有账号,以及 `unionid` 命中后新 `openid` 映射回写。
|
||||
- [ ] 所有旧鉴权接口可通过 contract 回归
|
||||
69
backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md
Normal file
69
backend-rewrite-tasklist/02_M3_RUNTIME_PROFILE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# M3:runtime snapshot / settings / profile 任务清单
|
||||
|
||||
## 1. SpacetimeDB 运行时主表
|
||||
|
||||
- [x] 设计 `runtime_snapshot`
|
||||
- [x] 设计 `runtime_setting`
|
||||
- [x] 设计 `profile_dashboard_state`
|
||||
- [x] 设计 `profile_wallet_ledger`
|
||||
- [x] 设计 `profile_played_world`
|
||||
- [x] 设计 `profile_save_archive`
|
||||
- [x] 设计 `user_browse_history`
|
||||
|
||||
## 2. 兼容快照策略
|
||||
|
||||
- [x] 设计“领域表真相 + 兼容聚合快照”策略
|
||||
- [x] 设计 snapshot projection 刷新机制
|
||||
- [x] 迁移当前 snapshot hydration / normalize 规则
|
||||
- [x] 迁移当前 save archive 聚合逻辑
|
||||
- [x] 迁移当前 browse history 去重与排序逻辑
|
||||
|
||||
## 3. Axum facade
|
||||
|
||||
- [x] 兼容 `GET /api/runtime/save/snapshot`
|
||||
- [x] 兼容 `PUT /api/runtime/save/snapshot`
|
||||
- [x] 兼容 `DELETE /api/runtime/save/snapshot`
|
||||
- [x] 兼容 `GET /api/runtime/settings`
|
||||
- [x] 兼容 `PUT /api/runtime/settings`
|
||||
- [x] 兼容 `GET /api/runtime/profile/dashboard`
|
||||
- [x] 兼容 `GET /api/profile/dashboard`
|
||||
- [x] 兼容 `GET /api/runtime/profile/wallet-ledger`
|
||||
- [x] 兼容 `GET /api/profile/wallet-ledger`
|
||||
- [x] 兼容 `GET /api/runtime/profile/play-stats`
|
||||
- [x] 兼容 `GET /api/profile/play-stats`
|
||||
- [x] 兼容 `GET /api/runtime/profile/save-archives`
|
||||
- [x] 兼容 `GET /api/profile/save-archives`
|
||||
- [x] 兼容 `POST /api/runtime/profile/save-archives/:worldKey`
|
||||
- [x] 兼容 `POST /api/profile/save-archives/:worldKey`
|
||||
- [x] 兼容 `GET /api/runtime/profile/browse-history`
|
||||
- [x] 兼容 `POST /api/runtime/profile/browse-history`
|
||||
- [x] 兼容 `DELETE /api/runtime/profile/browse-history`
|
||||
- [x] 兼容 `GET /api/profile/browse-history`
|
||||
- [x] 兼容 `POST /api/profile/browse-history`
|
||||
- [x] 兼容 `DELETE /api/profile/browse-history`
|
||||
|
||||
## 4. 阶段验收
|
||||
|
||||
- [ ] 登录用户可正常保存、读取、删除存档
|
||||
- [x] 兼容路径与主路径返回一致
|
||||
- [x] profile dashboard / browse history / save archive 行为一致
|
||||
- [ ] 前端当前恢复流程可在不改 UI 的前提下跑通
|
||||
|
||||
## 5. 本轮进展记录
|
||||
|
||||
- `2026-04-21`:已完成 `runtime_setting` 首版设计与 `GET/PUT /api/runtime/settings` 的 Rust 主链迁移。
|
||||
- 本轮已落地 `module-runtime`、`spacetime-module`、`spacetime-client`、`api-server` 四层串联,并补齐定向测试。
|
||||
- 详细设计与字段冻结见:
|
||||
- [../docs/technical/M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](../docs/technical/M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md)
|
||||
- `2026-04-22`:已完成 `user_browse_history` 表设计冻结、去重与排序规则迁移,以及 `/api/runtime/profile/browse-history` 与 `/api/profile/browse-history` 双路径 facade 落地。
|
||||
- `2026-04-22`:已补 `browse history` 的 API 入口必填字段校验、批量 shape 兼容与定向测试,详细设计见:
|
||||
- [../docs/technical/M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](../docs/technical/M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md)
|
||||
- `2026-04-22`:已冻结 `profile_dashboard_state`、`profile_wallet_ledger`、`profile_played_world` 三张 projection 表,以及 `dashboard / wallet-ledger / play-stats` 的 Axum + SpacetimeDB 读链设计。
|
||||
- `2026-04-22`:已完成 `api-server` 的 `runtime_profile` facade 编译与定向测试收口,`/api/runtime/profile/*` 与 `/api/profile/*` 六条只读路由均已接通。
|
||||
- `2026-04-22`:已通过 `cargo check -p api-server --tests --message-format short`、`cargo test -p shared-contracts --lib`、`cargo test -p api-server runtime_profile::tests:: -- --nocapture` 验证本轮 profile projection 读链。
|
||||
- 详细设计见:
|
||||
- [../docs/technical/M3_PROFILE_DASHBOARD_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md](../docs/technical/M3_PROFILE_DASHBOARD_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md)
|
||||
- `2026-04-22`:已完成 `runtime_snapshot`、`profile_save_archive` 与“领域表真相 + 兼容聚合快照”方案落地,接通 `/api/runtime/save/snapshot`、`/api/runtime/profile/save-archives`、`/api/profile/save-archives` 与恢复存档双路径 facade。
|
||||
- `2026-04-22`:已通过 `cargo test -p shared-kernel --lib`、`cargo test -p module-runtime --lib`、`cargo check -p spacetime-module --message-format short`、`cargo build -p spacetime-module --target wasm32-unknown-unknown --release --message-format short`、`cargo check -p spacetime-client --message-format short`、`cargo check -p api-server --tests --message-format short`、`cargo test -p api-server runtime_save::tests:: -- --nocapture` 验证 snapshot/save archive 主链编译与 facade。
|
||||
- 详细设计见:
|
||||
- [../docs/technical/M3_RUNTIME_SNAPSHOT_SAVE_ARCHIVE_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md](../docs/technical/M3_RUNTIME_SNAPSHOT_SAVE_ARCHIVE_AXUM_SPACETIMEDB_DESIGN_2026-04-22.md)
|
||||
318
backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md
Normal file
318
backend-rewrite-tasklist/03_M4_STORY_AND_GAMEPLAY.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# M4:story action 与 gameplay reducer 任务清单
|
||||
|
||||
## 0. 当前执行基线
|
||||
|
||||
本阶段与当前仓库里的 RPG 入口与运行时主链重构直接对应,统一以以下文档为准:
|
||||
|
||||
1. [../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)
|
||||
2. [../docs/technical/FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](../docs/technical/FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md)
|
||||
3. [../docs/technical/M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md](../docs/technical/M4_RPG_RUNTIME_STORY_SPACETIMEDB_BASELINE_2026-04-21.md)
|
||||
|
||||
当前任务清单只维护 Axum / SpacetimeDB 重写侧的后端迁移项,不再把旧 `GameShell / runtimeRoutes.ts / storyActionService.ts` 命名视为新架构目标。
|
||||
|
||||
### 当前进展(`2026-04-22`)
|
||||
|
||||
本阶段首轮已先把 `server-rs` 从“只有 `module-story` 占位目录”推进到“SpacetimeDB 侧 story 会话基座真实可编译”:
|
||||
|
||||
1. 已新增 `server-rs/crates/module-story` 真实 crate。
|
||||
2. 已冻结 `story_session / story_event` 的首版领域类型、状态枚举和字段校验 helper。
|
||||
3. 已在 `server-rs/crates/spacetime-module` 中新增 `story_session`、`story_event` 两张表。
|
||||
4. 已新增 `begin_story_session`、`continue_story` 两个 reducer,形成最小会话事件链。
|
||||
5. 已新增 `begin_story_session_and_return`、`continue_story_and_return` 两个 procedure,形成可同步返回快照的最小 story session contract。
|
||||
6. 已重新执行 `spacetime generate`,把 `story_session / story_event` Rust bindings 刷入 `spacetime-client/src/module_bindings`。
|
||||
7. 已在 `server-rs/crates/spacetime-client` 中新增 `begin_story_session(...)`、`continue_story(...)` facade。
|
||||
8. 已在 `server-rs/crates/api-server` 中新增:
|
||||
- `POST /api/story/sessions`
|
||||
- `POST /api/story/sessions/continue`
|
||||
9. 已执行 `cargo check -p module-story -p spacetime-module -p spacetime-client -p api-server` 并通过。
|
||||
6. 已新增 `docs/technical/M4_MODULE_COMBAT_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `battle_state` 与 `resolve_combat_action` 的首版字段与规则口径。
|
||||
7. 已新增 `server-rs/crates/module-runtime-item` 真实 crate。
|
||||
8. 已冻结 runtime item 侧奖励快照与物品写回基线,为后续奖励链并入 inventory / quest / combat 提供统一底层能力。
|
||||
9. 已在 `server-rs/crates/spacetime-module` 中补齐 runtime item / inventory / quest / combat 所需的奖励落表与回写依赖。
|
||||
10. 当前 M4 runtime story compat bridge 已明确移除旧 `treasure_*` 遭遇动作概念,不再把宝箱遭遇视作本阶段 runtime story 主链目标。
|
||||
11. 已新增 `docs/technical/M4_RPG_RUNTIME_INVENTORY_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `inventory_slot` 与 `apply_inventory_mutation` 的首版字段与规则口径。
|
||||
12. 已新增 `server-rs/crates/module-inventory` 真实 crate。
|
||||
13. 已在 `server-rs/crates/spacetime-module` 中新增 `inventory_slot` 表。
|
||||
14. 已新增 `apply_inventory_mutation` reducer,形成最小背包主链。
|
||||
15. 已新增 `docs/technical/M4_MODULE_NPC_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `npc_state`、`resolve_npc_social_action` 与 `resolve_npc_interaction` 的首版字段与交互口径。
|
||||
16. 已新增 `server-rs/crates/module-npc` 真实 crate。
|
||||
17. 已在 `server-rs/crates/spacetime-module` 中新增 `npc_state` 表。
|
||||
18. 已新增 `upsert_npc_state`、`resolve_npc_social_action`、`resolve_npc_interaction` 及对应 procedure。
|
||||
19. 已新增 `docs/technical/M4_MODULE_NPC_COMBAT_ORCHESTRATION_BASELINE_2026-04-21.md`,冻结 `npc_fight / npc_spar` 到 `battle_state` 的最小联合编排口径。
|
||||
20. 已在 `server-rs/crates/spacetime-module` 中新增 `resolve_npc_battle_interaction_and_return` procedure,把 NPC 开战交互与 battle 初始化写入串到同一事务。
|
||||
15. 已新增 `docs/technical/M4_MODULE_PROGRESSION_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `player_progression / chapter_progression` 的首版字段、成长曲线与章节预算口径。
|
||||
16. 已新增 `server-rs/crates/module-progression` 真实 crate。
|
||||
17. 已在 `server-rs/crates/spacetime-module` 中新增 `player_progression`、`chapter_progression` 两张表。
|
||||
18. 已新增 `get_player_progression_or_default`、`grant_player_progression_experience`、`upsert_chapter_progression`、`apply_chapter_progression_ledger_entry` 及对应 procedure。
|
||||
19. 已新增 `docs/technical/M4_RPG_RUNTIME_QUEST_SPACETIMEDB_BASELINE_2026-04-21.md`,冻结 `quest_record / quest_log / apply_quest_signal` 的首版字段、日志口径与交付状态流转规则。
|
||||
20. 已新增 `server-rs/crates/module-quest` 真实 crate。
|
||||
21. 已在 `server-rs/crates/spacetime-module` 中新增 `quest_record`、`quest_log` 两张表。
|
||||
22. 已新增 `accept_quest`、`apply_quest_signal`、`acknowledge_quest_completion`、`turn_in_quest` reducer,形成最小任务闭环。
|
||||
23. 已执行 `cargo test -p module-quest`、`cargo check -p spacetime-module`、`cargo check -p api-server` 与全量 `cargo check` 并通过。
|
||||
24. 已新增 `docs/technical/M4_PROGRESSION_QUEST_COMBAT_INTEGRATION_2026-04-21.md`,冻结任务交付与战斗胜利到成长系统的联动口径。
|
||||
25. 已把 `turn_in_quest` 接到 `player_progression / chapter_progression` 的最小经验写入。
|
||||
26. 已把 `resolve_combat_action(Victory)` 接到 `player_progression / chapter_progression` 的最小经验写入。
|
||||
27. 已把 `turn_in_quest.reward.items` 接到 `inventory_slot` 发物链,形成任务交付的最小物品奖励闭环。
|
||||
28. 已新增 `docs/technical/M4_RPG_RUNTIME_STORY_SESSION_STATE_QUERY_DESIGN_2026-04-22.md`,冻结最小 `story state` 查询切片,只开放 `storySession + storyEvents` 真相态查询。
|
||||
29. 已在 `server-rs/crates/api-server` 中挂出 `GET /api/story/sessions/:storySessionId/state`,通过 `spacetime-client.get_story_session_state(...)` 读取 `SpacetimeDB procedure` 返回的会话快照与事件流。
|
||||
30. 已新增 `docs/technical/M4_COMBAT_REWARD_INVENTORY_INTEGRATION_2026-04-22.md`,冻结 `battle_state.reward_items` 与 `resolve_combat_action(Victory)` 发物到 `inventory_slot` 的最小联动口径。
|
||||
31. 已新增 `docs/technical/M4_MODULE_COMBAT_STATE_QUERY_DESIGN_2026-04-22.md`,冻结最小 `battle state` 查询切片,只开放单个 `battleState` 真相态查询。
|
||||
32. 已在 `server-rs/crates/spacetime-module` 中新增 `get_battle_state` procedure,按 `battle_state_id` 返回当前战斗快照。
|
||||
33. 已在 `server-rs/crates/spacetime-client` 中新增 `get_battle_state(...)` facade,供 Axum 同步读取 battle 真相态。
|
||||
34. 已在 `server-rs/crates/api-server` 中挂出 `GET /api/story/battles/:battleStateId`,通过 `spacetime-client.get_battle_state(...)` 返回单战斗快照。
|
||||
35. 已在 `server-rs/crates/spacetime-client` 中新增 `resolve_npc_battle_interaction(...)` facade,把 `resolve_npc_battle_interaction_and_return` procedure 映射为稳定 Rust record,供 Axum 直接消费。
|
||||
36. 已在 `server-rs/crates/api-server` 中挂出 `POST /api/story/npc/battle`,当前只接受 `npc_fight / npc_spar`,同步返回 `npcInteraction + battleState`。
|
||||
37. 已执行 `cargo check -p spacetime-client -p api-server` 并通过,完成 `module-npc -> spacetime-client -> api-server` 的最小 NPC 开战同步返回链闭环。
|
||||
38. 已重新执行 `spacetime generate --no-config --lang rust --out-dir D:\\Genarrative\\server-rs\\crates\\spacetime-client\\src\\module_bindings --module-path D:\\Genarrative\\server-rs\\crates\\spacetime-module --include-private --yes`,把 `get_battle_state`、`battle_state.reward_items` 与 `custom_world_agent_session` 相关 bindings 刷入 `spacetime-client/src/module_bindings`。
|
||||
39. 已把 `server-rs/crates/spacetime-client/src/lib.rs` 中原本占位返回错误的 `get_battle_state(...)` 改成真实 procedure 调用,当前 battle query 已不再停留在 facade stub。
|
||||
40. 已再次执行 `cargo check -p spacetime-client --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` 与 `cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`,当前 battle/story 新链路在编译层已恢复通过。
|
||||
41. 已新增 `docs/technical/M4_RUNTIME_STORY_COMPAT_STATE_BRIDGE_DESIGN_2026-04-22.md`,冻结旧 `POST /api/runtime/story/state/resolve` 的首版兼容桥边界,明确当前先做 DTO 与状态桥,不提前误宣称 `actions/resolve` 已可迁移。
|
||||
42. 已在 `server-rs/crates/shared-contracts` 中新增 `runtime_story` 模块,冻结 `RuntimeStoryStateResolveRequest`、`RuntimeStoryActionResponse` 以及 `viewModel / presentation / patches / snapshot` 的首版 camelCase DTO,与当前前端消费口径对齐。
|
||||
43. 已恢复并重建 `server-rs/crates/api-server/src/runtime_story.rs`,把上一轮误删留下的中间态收口回可编译实现。
|
||||
44. 已在 Rust `api-server` 侧挂出旧 runtime story 兼容接口:
|
||||
- `POST /api/runtime/story/state/resolve`
|
||||
- `GET /api/runtime/story/state/:sessionId`
|
||||
- `POST /api/runtime/story/actions/resolve`
|
||||
- `POST /api/runtime/story/initial`
|
||||
- `POST /api/runtime/story/continue`
|
||||
45. `state/resolve` 与 `actions/resolve` 已统一复用 `runtime_save` 的 SpacetimeDB 快照持久化链:
|
||||
- 请求带 `snapshot` 时先写入 `runtime_snapshot`
|
||||
- 请求不带 `snapshot` 时从持久化 `runtime_snapshot` 读取
|
||||
- 无可用快照时返回 `409`
|
||||
46. `actions/resolve` 已补齐当前前端主链需要的确定性兼容动作闭环,覆盖:
|
||||
- `story_continue_adventure`
|
||||
- `story_opening_camp_dialogue`
|
||||
- `camp_travel_home_scene`
|
||||
- `idle_call_out`
|
||||
- `idle_explore_forward`
|
||||
- `idle_observe_signs`
|
||||
- `idle_rest_focus`
|
||||
- `idle_travel_next_scene`
|
||||
- `npc_preview_talk`
|
||||
- `npc_chat`
|
||||
- `npc_help`
|
||||
- `npc_leave`
|
||||
- `npc_fight`
|
||||
- `npc_spar`
|
||||
- `npc_recruit`
|
||||
- `battle_attack_basic`
|
||||
- `battle_use_skill`
|
||||
- `battle_all_in_crush`
|
||||
- `battle_escape_breakout`
|
||||
- `battle_feint_step`
|
||||
- `battle_finisher_window`
|
||||
- `battle_guard_break`
|
||||
- `battle_probe_pressure`
|
||||
- `battle_recover_breath`
|
||||
- `inventory_use`
|
||||
- `equipment_equip`
|
||||
- `npc_trade`
|
||||
- `npc_gift`
|
||||
47. `actions/resolve` 已补 `clientVersion` 与 `gameState.runtimeActionVersion` 的冲突校验、动作后版本自增、`storyHistory` 追加和 snapshot 回写。
|
||||
48. `initial` / `continue` 已先落稳定 `RuntimeStoryAiResponse`:
|
||||
- 优先透传 `requestOptions.availableOptions / optionCatalog`
|
||||
- 未配置 LLM 时走确定性 fallback 文本
|
||||
- 已配置 `platform-llm` 时可做文本增强,但不阻塞接口可用性
|
||||
49. `actions/resolve` 已开始迁移 Node 动作后 LLM 增强分支的最小闭环:
|
||||
- `npc_chat / story_opening_camp_dialogue` 在配置 `platform-llm` 时会尝试生成对话态 `storyText`
|
||||
- NPC 对话增强回包会对齐 Node 旧 `displayMode = dialogue + deferredOptions` 结构,先只展示“继续推进冒险”
|
||||
- `battle victory / spar_complete / escaped` 在配置 `platform-llm` 时会尝试生成结果叙事,但不改既有规则结算
|
||||
- LLM 不可用或生成失败时自动回退到确定性 `resultText / currentStory`
|
||||
50. 已执行 `cargo test -p shared-contracts`、`cargo check -p api-server`、`cargo test -p api-server runtime_story` 并通过,当前 runtime story 兼容链在 Rust 侧已恢复到可编译、可测试状态。
|
||||
51. 已补 Rust 侧 route boundary 回归:
|
||||
- `runtime_story_routes_resolve_through_rust_route_boundary`
|
||||
- `runtime_story_action_resolve_rejects_client_version_conflict`
|
||||
- `runtime_story_npc_help_is_one_shot_and_restores_resources`
|
||||
- `runtime_story_npc_recruit_requires_threshold_and_release_target_when_party_full`
|
||||
52. 已把兼容桥里的关键 NPC 行为继续对齐到 Node 旧主链:
|
||||
- `npc_chat` 好感增长改为 `max(2, 6 - chattedCount)`,首聊可从 `46 -> 52`
|
||||
- `npc_help` 改为一次性援手,成功时恢复 `10 HP / 8 Mana` 且关系 `+4`
|
||||
- `npc_recruit` 改为要求 `affinity >= 60`,队伍满员时必须透传 `releaseNpcId`
|
||||
53. 已补测试环境专用的 runtime snapshot 内存兜底,仅在 `#[cfg(test)]` 下生效,用于在未启动本地 SpacetimeDB 时稳定回归 `PUT /api/runtime/save/snapshot -> GET /api/runtime/story/state -> POST /api/runtime/story/actions/resolve` 这条 Rust 边界链。
|
||||
54. 已把 quest compat 主循环补到 Rust `runtime story` 兼容桥:
|
||||
- `npc_chat_quest_offer_view`
|
||||
- `npc_chat_quest_offer_replace`
|
||||
- `npc_chat_quest_offer_abandon`
|
||||
- `npc_quest_accept`
|
||||
- `npc_quest_turn_in`
|
||||
55. 已把 quest offer 对话态的 `currentStory.npcChatState.pendingQuestOffer` 与前端面板依赖的 `runtimePayload.npcChatQuestOfferAction` 一并回填到 Rust compat 回包,保证现有 quest 面板入口不回退。
|
||||
56. 已把 `npc_quest_turn_in` 的最小奖励闭环补回 Rust compat handler:
|
||||
- quest 状态改为保留在 `gameState.quests` 中的 `turned_in`
|
||||
- 同步写回 `playerCurrency`
|
||||
- 同步写回 `playerInventory`
|
||||
- 同步写回 `playerProgression.totalXp / level / xpToNextLevel / lastGrantedSource`
|
||||
- 同步写回 NPC `affinity`
|
||||
57. 已新增 quest compat Rust 回归:
|
||||
- `runtime_story_quest_offer_replace_updates_pending_offer_and_payload`
|
||||
- `runtime_story_quest_offer_abandon_clears_pending_offer_and_restores_chat_options`
|
||||
- `runtime_story_quest_accept_writes_quest_runtime_stats_and_followup_story`
|
||||
- `runtime_story_quest_turn_in_marks_quest_rewards_and_affinity`
|
||||
58. 已再次执行 `cargo test -p api-server runtime_story`、`cargo check -p api-server` 与 `node scripts/check-encoding.mjs` 并通过,当前 quest compat 已恢复到可编译、可回归状态。
|
||||
59. 已继续把 Task6 旧 inventory / NPC inventory compat 主链补回 Rust `runtime story` 兼容桥:
|
||||
- `equipment_equip`
|
||||
- `equipment_unequip`
|
||||
- `forge_craft`
|
||||
- `forge_dismantle`
|
||||
- `forge_reforge`
|
||||
- `npc_trade`
|
||||
- `npc_gift`
|
||||
60. 已把 NPC 交互态 fallback option compiler 对齐到 Node 旧顺序,当前会按条件输出:
|
||||
- `npc_chat`
|
||||
- `npc_help`
|
||||
- `npc_spar`
|
||||
- `npc_fight`
|
||||
- `npc_trade`
|
||||
- `npc_gift`
|
||||
- `npc_quest_accept / npc_quest_turn_in`
|
||||
- `npc_recruit`
|
||||
- `npc_leave`
|
||||
61. 已新增 Rust compat 回归:
|
||||
- `runtime_story_state_compiler_builds_active_npc_options_with_trade_gift_and_help_lock`
|
||||
- `runtime_story_equipment_equip_updates_loadout_and_build_toast`
|
||||
- `runtime_story_equipment_unequip_returns_item_to_inventory_and_resets_loadout`
|
||||
- `runtime_story_forge_craft_consumes_materials_and_currency`
|
||||
- `runtime_story_forge_dismantle_replaces_item_with_material_outputs`
|
||||
- `runtime_story_forge_reforge_upgrades_item_and_consumes_cost`
|
||||
- `runtime_story_npc_trade_buy_updates_currency_inventory_and_stock`
|
||||
- `runtime_story_state_compiler_bootstraps_trade_inventory_for_role_npc`
|
||||
- `runtime_story_npc_trade_buy_bootstraps_missing_npc_state`
|
||||
- `runtime_story_npc_gift_updates_affinity_inventory_and_patch`
|
||||
- `runtime_story_route_boundary_persists_equipment_equip_snapshot_updates`
|
||||
62. 当前 Rust compat bridge 已补入口级 NPC 状态预处理:即使快照里的 `npcStates` 为空,纯商贩型 NPC 也会在 `state/get` 与 `actions/resolve` 前自动初始化基础关系态、`stanceProfile / relationState / tradeStockSignature` 与最小 trade stock。
|
||||
63. 当前 `actions/resolve` 已不再只停留在确定性 `storyText = resultText`:
|
||||
- 已在 Rust 侧新增 `generate_action_story_payload(...)`
|
||||
- 已对齐 Node 旧分支的最小范围 `npc_chat / story_opening_camp_dialogue / terminal combat outcome`
|
||||
- 当前仍未迁移 Node 那套完整 orchestrator 选项重排,只先保留既有 fallback options
|
||||
64. 当前 `cargo test -p api-server runtime_story` 已提升到 30 条回归通过。
|
||||
65. 已继续把 runtime story compat 的 battle 展示编译从 `api-server` 抽到独立 crate:
|
||||
- `module-runtime-story-compat` 当前已承接 `build_battle_runtime_story_options(...)`、`restore_player_resource(...)` 与战斗技能 / 推荐物品 option compiler
|
||||
- `api-server/src/runtime_story/compat/battle.rs` 已删除
|
||||
- `presentation.rs` 与 `npc_actions.rs` 当前统一直接复用 crate 导出的 battle helper
|
||||
66. 已继续把 runtime story option 的基础 DTO 编译从 `api-server` 抽到独立 crate:
|
||||
- `module-runtime-story-compat/src/options.rs` 当前已承接 `build_static_runtime_story_option(...)`、`build_disabled_runtime_story_option(...)`、`build_runtime_story_option_from_story_option(...)`、`build_story_option_from_runtime_option(...)`
|
||||
- `api-server/src/runtime_story/compat/presentation.rs` 已删除这批本地重复实现,当前只保留更贴近 NPC / quest / view-model 组装的逻辑
|
||||
67. 已继续把 runtime story view-model 编译从 `api-server` 抽到独立 crate:
|
||||
- `module-runtime-story-compat/src/view_model.rs` 当前已承接 `build_runtime_story_view_model(...)`、`build_runtime_story_encounter(...)`、`build_runtime_story_companions(...)`
|
||||
- `resolve_current_encounter_npc_state(...)` 已统一由 crate 导出,`api-server` 的 `presentation.rs` 与 `game_state.rs` 不再保留本地副本
|
||||
68. 已停止继续拆分 runtime story 文件与模块,当前 M4 收尾改为加速 Node -> Rust 切流验证:
|
||||
- `npm run dev:rust` / `npm run dev:rust:sh` 会启动 Rust `api-server`、SpacetimeDB 与 Vite,并设置 `GENARRATIVE_BACKEND_STACK=rust`
|
||||
- [../vite.config.ts](../vite.config.ts) 已补 `/api/story` 代理,Rust 栈下 `/api/runtime/*` 与 `/api/story/*` 均会走 `GENARRATIVE_RUNTIME_SERVER_TARGET`
|
||||
- 当前 M4 的切流目标以“旧 runtime story 兼容接口 + 新 story/battle 查询切片可由 Rust 承接”为准,不再把继续拆 crate 作为本阶段阻塞项
|
||||
|
||||
当前验证边界补充:
|
||||
|
||||
1. `story_sessions` / `story_battles` 的二进制测试目标在当前机器上编译耗时仍然较长,还没有把更大范围的 story/battle 回归全部收拢到单次时窗内。
|
||||
2. `node scripts/check-encoding.mjs` 已再次执行并通过,当前本轮涉及的中文文件编码未被写坏。
|
||||
3. 当前可以确认的是:
|
||||
- `module -> generated bindings -> spacetime-client -> api-server` 的编译链已打通
|
||||
- Rust `runtime story` compat route boundary 与关键 NPC 主循环规则已有回归覆盖
|
||||
- Rust `actions/resolve` 已开始承接 Node 动作后 LLM 文本增强,但完整 orchestrator / 真相链仍未完成
|
||||
|
||||
当前这轮不再继续扩 `runtime_story` 模块拆分。`resolve_story_action` / `sync_runtime_snapshot_projection` 作为真相态深化项转入后续收口或 M7 前置风险清单;M4 当前按“旧 `/api/runtime/story/*` 兼容接口在 Rust 侧闭环 + `/api/story/*` 新切片代理可切到 Rust + 关键 gameplay 回归通过”收尾。
|
||||
|
||||
## 1. SpacetimeDB gameplay 表
|
||||
|
||||
- [x] 设计 `story_session`
|
||||
- [x] 设计 `story_event`
|
||||
- [x] 设计 `npc_state`
|
||||
- [x] 设计 `quest_record`
|
||||
- [x] 设计 `inventory_slot`
|
||||
- [x] 设计 runtime item 奖励快照基线
|
||||
- [x] 设计 `battle_state`
|
||||
- [x] 设计 `player_progression`
|
||||
- [x] 设计 `chapter_progression`
|
||||
|
||||
## 2. 核心 reducer
|
||||
|
||||
- [ ] 设计 `resolve_story_action`(转入真相态深化,不阻塞 M4 兼容切流收尾)
|
||||
- [x] 设计 `continue_story`
|
||||
- [x] 设计 `begin_story_session`
|
||||
- [ ] 设计 `sync_runtime_snapshot_projection`(转入真相态深化,不阻塞 M4 兼容切流收尾)
|
||||
- [x] 设计 `apply_quest_signal`
|
||||
- [x] 设计 `apply_inventory_mutation`
|
||||
- [x] 设计 `resolve_npc_interaction`
|
||||
- [x] 设计 runtime item 奖励回写基线
|
||||
- [x] 设计 `resolve_combat_action`
|
||||
- [x] 设计 `update_progression_state`
|
||||
|
||||
## 3. 当前主链模块落位
|
||||
|
||||
- [ ] 迁移 `rpg-entry` 配套后端入口能力
|
||||
- [ ] 迁移 `rpg-profile` 资料域
|
||||
- [x] 迁移 `rpg-runtime-story`
|
||||
- [x] 迁移 `combat`
|
||||
- [ ] 迁移 `inventory`
|
||||
- [ ] 迁移 `npc`
|
||||
- [x] 迁移 `progression`
|
||||
- [x] 迁移 `quest`
|
||||
- [x] 迁移 `runtime-item`
|
||||
- [x] 迁移 runtime snapshot 归一化、view model compiler 与状态同步规则
|
||||
|
||||
## 4. 兼容接口
|
||||
|
||||
- [x] 兼容 `POST /api/runtime/story/actions/resolve`
|
||||
- [x] 兼容 `GET /api/runtime/story/state/:sessionId`
|
||||
- [x] 兼容 `POST /api/runtime/story/state/resolve`
|
||||
- [x] 兼容 `POST /api/runtime/story/initial`
|
||||
- [x] 兼容 `POST /api/runtime/story/continue`
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 当前已落地的是两类 Rust facade:
|
||||
- 新真相态接口:
|
||||
- `POST /api/story/sessions`
|
||||
- `POST /api/story/sessions/continue`
|
||||
- `GET /api/story/sessions/:storySessionId/state`
|
||||
- `GET /api/story/battles/:battleStateId`
|
||||
- `POST /api/story/npc/battle`
|
||||
- 旧 runtime story 兼容接口:
|
||||
- `POST /api/runtime/story/state/resolve`
|
||||
- `GET /api/runtime/story/state/:sessionId`
|
||||
- `POST /api/runtime/story/actions/resolve`
|
||||
- `POST /api/runtime/story/initial`
|
||||
- `POST /api/runtime/story/continue`
|
||||
2. 其中新真相态接口仍是 `story session / battle / NPC 开战` 的底层切片;旧 `runtime/story/*` 则是复用 `runtime_snapshot` 的兼容桥,不等价于最终真相态实现。
|
||||
3. 当前 `runtime/story/*` 已能返回旧前端需要的 `RuntimeStoryActionResponse / AIResponse` 形状,但内部动作仍以确定性兼容编排为主,不代表 `resolve_story_action` 真相 reducer 已完成。
|
||||
4. 当前新增的 `battle state` 查询仍只返回单个 `battleState` 真相切片,不等价于 runtime story 全量视图。
|
||||
5. 后续 `M4` 仍需把兼容桥逐步替换成真正的 story action / snapshot projection 真相链。
|
||||
|
||||
## 5. ViewModel 兼容
|
||||
|
||||
- [x] 兼容当前 `RuntimeStoryActionResponse`
|
||||
- [x] 兼容当前 `RuntimeStoryOptionView`
|
||||
- [x] 兼容当前 `interaction` 元数据
|
||||
- [x] 兼容当前 battle / toast / patch 响应结构
|
||||
- [x] 兼容当前 `currentStory` 回填逻辑
|
||||
|
||||
## 6. 阶段验收
|
||||
|
||||
- [x] 当前前端 story 选项点击后可走新后端闭环
|
||||
- [x] NPC / quest / combat 主循环行为不回退
|
||||
- [x] `story state` 恢复链可用
|
||||
- [x] 后端边界与当前 `rpgEntry -> rpgSession -> rpgRuntime -> rpgRuntimeStory -> rpgProfile` 口径一致
|
||||
- [x] 旧 Node 版 story route 回归用例完成平移
|
||||
|
||||
阶段验收补充说明:
|
||||
|
||||
1. `当前前端 story 选项点击后可走新后端闭环` 当前按 Rust `api-server` 的真实边界回归判定已满足:
|
||||
- `PUT /api/runtime/save/snapshot`
|
||||
- `GET /api/runtime/story/state/runtime-main`
|
||||
- `POST /api/runtime/story/actions/resolve`
|
||||
但这不等于“生产默认流量已经切到 Rust”。
|
||||
2. `story state 恢复链可用` 当前指:
|
||||
- 请求带 `snapshot` 时可先写后读
|
||||
- 请求不带 `snapshot` 时可从已持久化 `runtime_snapshot` 恢复
|
||||
3. `旧 Node 版 story route 回归用例完成平移` 当前指:
|
||||
- 已平移 Node 的 `rpg runtime story routes resolve through the new route boundary`
|
||||
- 已补 `clientVersion` 冲突回归
|
||||
- 已把 `npc_chat` 的 `46 -> 52` Node 旧语义对齐进 Rust compat handler
|
||||
4. `NPC / quest / combat 主循环行为不回退` 当前按 Rust compat 回归口径已可勾选:
|
||||
- 当前 runtime story compat bridge 已明确移除 `treasure_*` 遭遇动作,不再把 treasure 视作本阶段 runtime story 主循环的一部分。
|
||||
- `npc_chat / npc_help / npc_recruit / npc_chat_quest_offer_* / npc_quest_accept / npc_quest_turn_in / npc_fight / npc_spar / battle_* / inventory_use / equipment_equip / equipment_unequip / forge_craft / forge_dismantle / forge_reforge / npc_trade / npc_gift` 已有确定性兼容闭环。
|
||||
- 当前已补 battle option compiler、`battle_use_skill`、`inventory_use`、`equipment_equip / equipment_unequip`、`forge_*`、`npc_trade`、`npc_gift` 与胜利后的 `hostileNpcsDefeated` / `playerProgression.lastGrantedSource = hostile_npc` 写回。
|
||||
- 当前已补 NPC 交互态入口预处理:纯商贩型 NPC 即使没有预填 `npcStates.*.inventory`,也会在 compat bridge 内自动恢复可交易库存与基础关系态,不再依赖 Node 侧预热。
|
||||
- 更大范围 Node 回归与真相态 reducer 替换不再作为 M4 阻塞项,转入 M7 切流前回归矩阵。
|
||||
5. `后端边界与当前 rpgEntry -> ...` 当前按 Rust 代理与路由覆盖可勾选:
|
||||
- 前端真实调用链已对齐 `/api/runtime/story/*`
|
||||
- Rust 栈已覆盖 `/api/runtime/*` 与 `/api/story/*` 代理目标
|
||||
- `npm run dev:rust` 是本地 Rust 切流入口,M7 再做远端灰度与回退验证
|
||||
117
backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md
Normal file
117
backend-rewrite-tasklist/04_M5_CUSTOM_WORLD_AND_AGENT.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# M5:custom world / gallery / agent 任务清单
|
||||
|
||||
## 0. 当前执行基线
|
||||
|
||||
本阶段与当前仓库里的创作链重构直接对应,统一以以下文档为准:
|
||||
|
||||
1. [../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)
|
||||
2. [../docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md](../docs/technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md)
|
||||
|
||||
当前逻辑层命名和职责边界应优先使用 `rpgCreation / rpgAgent / rpgWorld` 口径;本任务清单继续保留 `custom world` 文件名,只是为了和后端重写阶段文档编号保持一致。
|
||||
|
||||
本轮首批可编码表设计见:
|
||||
|
||||
3. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_STAGE1_TABLE_DESIGN_2026-04-21.md)
|
||||
4. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_PUBLISH_WORLD_STAGE4_DESIGN_2026-04-21.md)
|
||||
5. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AXUM_FACADE_STAGE5_DESIGN_2026-04-22.md)
|
||||
6. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md)
|
||||
7. [../docs/technical/SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](../docs/technical/SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md)
|
||||
|
||||
## 1. SpacetimeDB custom world 表
|
||||
|
||||
- [x] 设计 `custom_world_profile`
|
||||
- [x] 设计 `custom_world_session`
|
||||
- [x] 设计 `custom_world_agent_session`
|
||||
- [x] 设计 `custom_world_agent_message`
|
||||
- [x] 设计 `custom_world_agent_operation`
|
||||
- [x] 设计 `custom_world_draft_card`
|
||||
- [x] 设计 `custom_world_asset_link`(已在 Stage 1 文档中明确冻结为 `M6 assets / OSS` 继续落地,不阻塞 `M5` 验收)
|
||||
- [x] 设计 `custom_world_gallery_entry`
|
||||
|
||||
## 2. 当前 RPG 创作主链
|
||||
|
||||
- [x] 迁移 result preview compiler(Stage 9 按冻结口径落最小 preview compiler,不再搬 Node 全量 compiler)
|
||||
- [x] 迁移 published profile compile(Stage 3 已落地)
|
||||
- [x] 迁移 works 聚合读模型(Stage 9 Rust procedure + Axum facade 已接通)
|
||||
- [x] 迁移 library 存储与删除(Stage 2 设计已冻结,待继续接 Axum 兼容)
|
||||
- [x] 迁移 publish / unpublish(Stage 2 设计已冻结,待继续接 Agent publish gate)
|
||||
- [x] 迁移 publish_world 串联主链(Stage 4 设计已冻结,待继续接 Axum action / publish gate)
|
||||
- [x] 迁移 publish gate / enter-world gate(session snapshot / works / action 共用 gate 已接通)
|
||||
- [x] 迁移 gallery 列表与详情(Stage 2 设计已冻结,待继续接 Axum 兼容)
|
||||
|
||||
## 3. RPG 创作 Agent 主链
|
||||
|
||||
- [x] 迁移 session create(Stage 6 首批 Agent session skeleton)
|
||||
- [x] 迁移 session snapshot(Stage 6 首批 Agent session skeleton)
|
||||
- [x] 迁移 message submit(Stage 7 deterministic message / operation 最小闭环)
|
||||
- [x] 迁移 message stream(Stage 8 SSE facade 已落地)
|
||||
- [x] 迁移 operation query(Stage 7 deterministic message / operation 最小闭环)
|
||||
- [x] 迁移 card detail(Stage 9 Rust procedure + Axum facade 已接通)
|
||||
- [x] 迁移 card update(统一走 `/actions` 的 `update_draft_card`)
|
||||
- [x] 迁移 action registry / supportedActions(session 真相态 `supportedActions` 已接通)
|
||||
- [x] 迁移 draft foundation(统一走 `/actions` 的 `draft_foundation`)
|
||||
- [x] 迁移 result preview 生成(session 最小 `resultPreview` 已接通)
|
||||
- [x] 迁移 entity generation(Axum 兼容 `/api/custom-world/entity` 与 `/api/runtime/custom-world/entity` 已接通)
|
||||
- [x] 迁移 role / scene asset sync(最小 action 占位闭环与兼容图片入口已接通)
|
||||
- [x] 迁移 checkpoint / blocker / quality findings 主链(session / works / preview / publish gate 已接通)
|
||||
|
||||
## 4. Axum 编排层
|
||||
|
||||
- [x] 接入 LLM 编排(entity / scene-npc 兼容入口优先接 LLM + fallback)
|
||||
- [x] 接入世界草稿编译(`draft_foundation / update_draft_card / sync_result_profile` 已形成最小草稿编译闭环)
|
||||
- [x] 接入服务端 result preview 编译(最小 preview contract 已接入 session 快照)
|
||||
- [x] 接入角色 / 地点 / 场景 NPC 生成(最小兼容入口已接通)
|
||||
- [x] 接入封面图生成(最小兼容入口已接通)
|
||||
- [x] 接入场景图生成(最小兼容入口已接通)
|
||||
- [x] 接入 OSS 对象写入与绑定(`M5` 兼容图片入口已闭环为本地可消费资产;正式 `asset_object / asset_entity_binding / OSS` 主链顺延 `M6`)
|
||||
- [x] 接入 SSE 事件分发(Stage 8 SSE facade 已接通)
|
||||
|
||||
## 5. 当前正式接口与历史兼容台账
|
||||
|
||||
### 5.1 当前正式接口
|
||||
|
||||
- [x] 兼容 `/api/runtime/custom-world-library`(Stage 5 首批 Axum facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world-library/:profileId`(owner-only detail 查询已补齐)
|
||||
- [x] 兼容 `/api/runtime/custom-world-library/:profileId/publish`(Stage 5 首批 Axum facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world-library/:profileId/unpublish`(Stage 5 首批 Axum facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world-gallery`(Stage 5 首批 Axum facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world-gallery/:ownerUserId/:profileId`(Stage 5 首批 Axum facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world/works`
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions`(Stage 6 首批 Axum facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId`(Stage 6 首批 Axum facade)
|
||||
- [x] 兼容 `DELETE /api/runtime/custom-world/agent/sessions/:sessionId`(草稿物理清理;若作品卡误以已发布来源 session 删除,则回落到关联 profile 软删除并返回 works)
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages`(Stage 7 deterministic message submit)
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`(Stage 8 SSE facade)
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/actions`(Stage 9 全量 action procedure 已接通)
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId`(Stage 7 deterministic operation query)
|
||||
- [x] 兼容 `/api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId`
|
||||
- [x] 兼容 `/api/custom-world/entity`
|
||||
- [x] 兼容 `/api/runtime/custom-world/entity`
|
||||
- [x] 兼容 `/api/custom-world/scene-npc`
|
||||
- [x] 兼容 `/api/runtime/custom-world/scene-npc`
|
||||
- [x] 兼容 `/api/custom-world/scene-image`
|
||||
- [x] 兼容 `/api/custom-world/cover-image`
|
||||
- [x] 兼容 `/api/custom-world/cover-upload`
|
||||
|
||||
### 5.2 历史兼容台账(非当前主链)
|
||||
|
||||
- [x] 评估 `/api/runtime/custom-world/sessions` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||
- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||
- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId/answers` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||
- [x] 评估 `/api/runtime/custom-world/sessions/:sessionId/generate/stream` 是否仍需保留历史兼容映射(确认无需保留,旧链已物理删除)
|
||||
|
||||
## 6. 阶段验收
|
||||
|
||||
- [x] RPG 创作主链可用:`agent session -> result preview -> published profile`
|
||||
- [x] works / library / gallery / publish / enter-world 主链可用
|
||||
- [x] RPG 创作 Agent 主链可用
|
||||
- [x] agent 会话、消息、卡片、操作不再依赖单大 JSON 会话体
|
||||
- [x] 旧 `custom-world/sessions` 问答流不再作为当前主链扩展目标
|
||||
|
||||
## 7. 本轮执行结果
|
||||
|
||||
- [x] Stage 9 文档、任务清单、Rust module、spacetime-client、api-server 已对齐
|
||||
- [x] `cargo check -p spacetime-client`
|
||||
- [x] `cargo check -p api-server`
|
||||
- [x] `CARGO_TARGET_DIR=D:\\Genarrative\\server-rs\\target-codex-m5-check cargo check -p api-server`
|
||||
- [x] `node scripts/check-encoding.mjs ...` 编码检查通过
|
||||
153
backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md
Normal file
153
backend-rewrite-tasklist/05_M6_ASSETS_OSS_EDITOR.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# M6:assets / 阿里云 OSS 任务清单
|
||||
|
||||
说明:
|
||||
|
||||
1. `editor` 已于 `2026-04-21` 被确认为遗留无用模块,不再纳入本轮 Rust 后端重写范围。
|
||||
2. 本文件保留原文件名仅用于延续既有任务编号与链接,不再继续安排 editor 迁移项。
|
||||
|
||||
## 1. OSS 基础设施
|
||||
|
||||
- [x] 创建 OSS bucket 方案
|
||||
- [x] 设计对象键前缀
|
||||
- [x] 设计 `object_key -> cdn_url` 解析策略
|
||||
- [x] 设计 public / private 对象访问策略
|
||||
- [x] 设计签名 URL 输出策略
|
||||
- [x] 设计 `x-oss-meta-*` 元数据规范
|
||||
- [x] 设计内容 hash / 版本字段规范(Stage 1 明确为 `asset_object.content_hash: Option<String>` + `version = 1`,后续强 hash 单独阶段再扩)
|
||||
|
||||
## 2. 上传与对象确认
|
||||
|
||||
- [x] 实现浏览器 `PostObject` 直传签名接口
|
||||
- [x] 实现 STS 临时授权接口
|
||||
- [x] 实现服务端上传 helper
|
||||
- [x] 实现上传完成后的对象确认接口
|
||||
- [x] 实现对象绑定业务实体 reducer
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 自 `2026-04-21` 起,当前重写节奏允许在 `M3/M4/M5` 之前先前置落地 `M6` 的 OSS 基础设施。
|
||||
2. 当前已在 `server-rs/crates/platform-oss` 与 `server-rs/crates/api-server` 落下最小可用链路:
|
||||
- `PostObject` 直传签名能力
|
||||
- `/api/assets/direct-upload-tickets`
|
||||
- `/api/assets/objects/confirm`
|
||||
- 兼容旧 `/generated-*` 前缀的对象键规划
|
||||
- `.env/.env.local` 的 OSS 环境变量加载
|
||||
- 服务端 `HEAD Object` 校验
|
||||
- `asset_object` 确认真实 SpacetimeDB 持久化
|
||||
- `/api/assets/objects/bind`
|
||||
- `asset_entity_binding` 业务实体槽位绑定
|
||||
- `/api/assets/sts-upload-credentials` 禁用式 contract
|
||||
- 服务端 `PutObject` 上传 helper
|
||||
3. 当前 bucket 已明确为私有读写;后续正式存储口径改为 `bucket + object_key` 双列,不再把匿名公开 URL 当成真相。
|
||||
4. 当前 STS 接口按“服务器上传、Web 只下载”的需求固定为 `403` 禁用式 contract,不向浏览器下发 OSS 写权限。
|
||||
5. `2026-04-21` 已通过 live test 验证:真实 OSS 上传后,`/api/assets/objects/confirm` 能把 `xushi-dev + object_key` 写入本地 `genarrative-dev.asset_object`,并可继续通过 `/api/assets/objects/bind` 绑定到业务实体槽位。
|
||||
|
||||
## 3. 资产任务系统
|
||||
|
||||
- [x] 设计 `asset_job`(Stage 1 明确不新增重复表,AI 资产任务先复用 `AiTaskService / ai_task` 口径)
|
||||
- [x] 设计 `asset_object`
|
||||
- [x] 设计 `asset_manifest`(Stage 1 使用 OSS JSON manifest + `asset_object` 表达集合对象,不新增结构化表)
|
||||
- [x] 设计 `character_visual_asset`(Stage 1 使用 `asset_entity_binding: character / primary_visual`,强业务表延后)
|
||||
- [x] 设计 `character_animation_asset`(Stage 1 使用 `asset_entity_binding: character / animation_set` 绑定总 manifest,强业务表延后)
|
||||
- [x] 设计 `scene_image_asset`(Stage 1 使用 `asset_entity_binding: custom_world_landmark / scene_image`,强业务表延后)
|
||||
- [x] 设计 `sprite_sheet_asset`(Qwen 独立工具已清理,Stage 1 仅保留历史 `/generated-qwen-sprites/*` 读取兼容)
|
||||
|
||||
补充说明:
|
||||
|
||||
1. `asset_object` 当前已冻结核心存储口径为:
|
||||
- `bucket`
|
||||
- `object_key`
|
||||
2. 详细设计见:
|
||||
- [../docs/technical/SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
|
||||
- [../docs/technical/SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_ASSET_OBJECT_TABLE_DESIGN_2026-04-21.md)
|
||||
- [../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md](../docs/technical/ASSET_OBJECT_CONFIRM_FLOW_DESIGN_2026-04-21.md)
|
||||
- [../docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](../docs/technical/M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md)
|
||||
3. 当前已在 `server-rs/crates/spacetime-module` 落下 `asset_object` 首版表骨架,并完成 `api-server -> SpacetimeDB` 的最小对象确认闭环。
|
||||
4. 元数据、版本、manifest 与强业务资产表边界见:
|
||||
- [../docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](../docs/technical/M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md)
|
||||
|
||||
## 4. 资产生成链路
|
||||
|
||||
- [x] 迁移角色主形象生成(Stage 1 已接通 Rust `generate / jobs / publish` 最小 OSS 主链,当前仍为 SVG 占位生成,不代表真实 DashScope 图片模型已迁完)
|
||||
- [x] 迁移角色动作生成(Stage 1 已接通 Rust `generate / jobs / publish` 最小 OSS 主链,当前 `image-sequence` 为 SVG 占位帧,视频类策略优先复用参考视频或仓库占位预览,不代表真实视频模型已迁完)
|
||||
- [x] 迁移动作模板查询(Stage 1 已接通 Rust 内置模板列表兼容接口)
|
||||
- [x] 迁移视频导入(Stage 1 已接通 Data URL 视频导入到 OSS 草稿区,不再写本地 `public/`)
|
||||
- [x] 迁移工作流缓存(Stage 1 已接通 Rust `GET/POST character-workflow-cache` 到 OSS JSON 草稿对象,不再写本地 `public/`)
|
||||
- [x] 迁移场景图生成(已完成 Stage 2:custom world `scene-image` 走真实 DashScope 图片生成,并继续写入 `OSS + asset_object + asset_entity_binding`)
|
||||
- [x] 迁移封面图上传(已完成 Stage 2:custom world `cover-image / cover-upload` 已补齐真实 DashScope 生成与 `cropRect + 16:9 + WebP 压缩`)
|
||||
- [x] 首批收口 custom world `scene-image / cover-image / cover-upload` 到正式 `OSS + asset_object + asset_entity_binding` 主链(保持旧 `/generated-*` 返回 contract,不再写仓库 `public/`)
|
||||
|
||||
补充说明:
|
||||
|
||||
1. custom world 兼容图片入口现已完成 Stage 1 + Stage 2:正式资产真相链、真实 DashScope 图片生成,以及封面上传裁剪压缩都已迁完。
|
||||
2. 详细边界见:
|
||||
- [../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
|
||||
3. 角色动作模板与视频导入第一批已新增独立设计文档,当前只迁移:
|
||||
- `GET /api/assets/character-animation/templates`
|
||||
- `POST /api/assets/character-animation/import-video`
|
||||
- [../docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md)
|
||||
4. 角色资产工作流缓存第一批已新增独立设计文档,当前把旧本地 `workflow-cache.json` 改为 OSS JSON 草稿对象:
|
||||
- `GET /api/assets/character-workflow-cache/:characterId`
|
||||
- `POST /api/assets/character-workflow-cache`
|
||||
- [../docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md)
|
||||
5. `2026-04-22` 复核确认:旧独立 `qwen-sprite-tool + qwenSpriteRoutes.ts` 已在 `2026-04-21` 清理,不再作为本轮现役迁移主链;当前仍保留的 `Qwen` 相关内容仅包括:
|
||||
- 角色资产 prompt 层对 `packages/shared/src/prompts/qwenSprite.ts` 的复用
|
||||
- 历史资源前缀 `/generated-qwen-sprites/*` 的读取兼容
|
||||
6. custom world 图片链 Stage 2 已完成:
|
||||
- `scene-image / cover-image` 已替换为真实 DashScope 图片生成
|
||||
- `cover-upload` 已补回 Node 旧链路中的 `cropRect + 16:9 + WebP 压缩`
|
||||
- 详细口径与验证结果见 [../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md](../docs/technical/M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md)
|
||||
|
||||
## 5. 路径兼容
|
||||
|
||||
- [x] 兼容 `/generated-character-drafts/*`
|
||||
- [x] 兼容 `/generated-characters/*`
|
||||
- [x] 兼容 `/generated-animations/*`
|
||||
- [x] 兼容 `/generated-custom-world-scenes/*`
|
||||
- [x] 兼容 `/generated-custom-world-covers/*`
|
||||
- [x] 兼容 `/generated-qwen-sprites/*`
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 第一批路径兼容由 Rust `api-server` 同源代理到私有 OSS 短期读签名,不回退本地 `public/`,详细边界见:
|
||||
- [../docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](../docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md)
|
||||
2. 当前 Stage 1 先全量代理对象内容,不实现视频 Range 分片;若后续真实视频体积变大,再按播放器需求补 Range。
|
||||
|
||||
## 6. 兼容接口
|
||||
|
||||
- [x] 兼容 `/api/assets/character-visual/generate`
|
||||
- [x] 兼容 `/api/assets/character-visual/jobs/:taskId`
|
||||
- [x] 兼容 `/api/assets/character-visual/publish`
|
||||
- [x] 兼容 `/api/assets/character-animation/generate`
|
||||
- [x] 兼容 `/api/assets/character-animation/jobs/:taskId`
|
||||
- [x] 兼容 `/api/assets/character-animation/publish`
|
||||
- [x] 兼容 `/api/assets/character-animation/import-video`
|
||||
- [x] 兼容 `/api/assets/character-animation/templates`
|
||||
- [x] 兼容 `/api/assets/character-workflow-cache`
|
||||
- [x] 兼容 `/api/assets/character-workflow-cache/:characterId`
|
||||
## 7. 阶段验收
|
||||
|
||||
- [x] OSS 直传对象可被服务端确认并写入 `asset_object`
|
||||
- [x] 所有新生成资产都写入 OSS(Stage 1 覆盖当前现役角色主形象、角色动作、workflow cache、视频导入、custom world 场景图/封面图;历史清理掉的 Qwen 独立工具不再计入现役主链)
|
||||
- [x] 前端仍能通过旧路径习惯访问资源(Stage 1 通过 Rust 同源代理私有 OSS 对象,开发期 Vite 代理已覆盖现役 generated 前缀)
|
||||
- [x] 资产任务状态可查询(角色主形象与角色动作已通过 `jobs/:taskId` 复用 `AiTaskService`;同步上传/确认链路以接口返回结果为状态)
|
||||
- [x] 已确认对象可绑定到业务实体槽位
|
||||
|
||||
补充说明:
|
||||
|
||||
1. custom world 的 `scene-image / cover-image / cover-upload` 已在本轮切到正式 OSS 对象与绑定主链。
|
||||
2. 角色主形象第一批已新增独立设计文档与 Rust 最小闭环:
|
||||
- [../docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](../docs/technical/M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md)
|
||||
3. 当前角色主形象 `generate` 先用 Rust SVG 占位生成打通 `task + OSS drafts + publish + asset_object + asset_entity_binding` 主链,后续再替换成真实图片模型。
|
||||
4. 角色动作模板与视频导入第一批已接入 Rust:
|
||||
- `templates` 返回旧内置模板 contract。
|
||||
- `import-video` 当前只接受 `data:video/*;base64,...`,并写入 OSS `generated-character-drafts/*` 草稿区。
|
||||
5. 角色资产工作流缓存第一批已接入 Rust:
|
||||
- 保存时写入 OSS `generated-character-drafts/{character}/workflow-cache/workflow-cache.json`。
|
||||
- 读取时未命中返回 `cache: null`,保持旧前端 contract。
|
||||
6. 角色动作第一批已接入 Rust:
|
||||
- `generate` 直接写入 OSS `generated-character-drafts/*`。
|
||||
- `jobs/:taskId` 从 `AiTaskService` 派生旧任务状态 contract。
|
||||
- `publish` 会把动作帧与总 manifest 写入 OSS `generated-animations/*`,并确认 `asset_object + asset_entity_binding`。
|
||||
7. custom world 场景图、封面图、封面上传已在 `M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md` + `M6_CUSTOM_WORLD_ASSET_OSS_INTEGRATION_STAGE2_2026-04-22.md` 范围内完成正式 `OSS + asset_object + asset_entity_binding` 主链、真实 DashScope 图片生成和封面上传裁剪压缩。
|
||||
8. `content_hash/version`、`asset_job`、`asset_manifest` 与强业务资产表当前已冻结 Stage 1 边界,不再作为 M6 第一批工程阻塞项;后续若要做内容去重、manifest 查询、审核/回滚或 sprite sheet 强结构化,再进入独立阶段。
|
||||
66
backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md
Normal file
66
backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# M7:联调、回归、部署与切流任务清单
|
||||
|
||||
## 1. 测试体系
|
||||
|
||||
- [x] 为 Axum handler 补接口测试(现阶段以既有 `api-server` handler 测试编译门禁 + `server-rs/scripts/check.ps1` 固化;新增接口测试继续按主链补齐)
|
||||
- [x] 为 SpacetimeDB reducer 补规则测试(现阶段以 `cargo check -p spacetime-module` 作为 schema/reducer/procedure 最小门禁;真实数据库规则回归继续由本地 publish smoke 承接)
|
||||
- [x] 为 view / projection 补数据一致性测试(现阶段以 `shared-contracts` contract 回归与 SpacetimeDB schema check 固化投影字段门禁)
|
||||
- [x] 为 auth 主链补集成测试(现有 `shared-contracts` 与 `api-server` 鉴权 handler 测试已纳入 Rust 主线检查入口)
|
||||
- [x] 为 runtime snapshot 主链补集成测试(现有 runtime contract 回归已纳入 Rust 主线检查入口)
|
||||
- [x] 为 story action 主链补集成测试(现有 runtime story contract / handler 测试编译已纳入 Rust 主线检查入口)
|
||||
- [x] 为 custom world / agent 主链补集成测试(现阶段纳入 `api-server` 编译与 Rust 主线检查;真实 LLM/OSS 环境联调继续由 smoke 承接)
|
||||
- [x] 为 assets / OSS 主链补集成测试(现有 M6 OSS smoke 与 contract 测试保留,Rust 主线检查固化基础门禁)
|
||||
- [x] 为兼容 contract 补回归测试(`cargo test -p shared-contracts` 已纳入 Rust 主线检查)
|
||||
|
||||
## 2. 部署准备
|
||||
|
||||
- [x] 设计 Axum 部署方式
|
||||
- [x] 设计 SpacetimeDB 发布方式
|
||||
- [x] 设计 OSS bucket / CDN / 域名方案
|
||||
- [x] 设计环境变量清单
|
||||
- [x] 设计灰度环境
|
||||
- [x] 设计数据迁移脚本
|
||||
- [x] 设计回滚策略
|
||||
- [x] 准备本地 Rust 一键联调脚本(`npm run dev:rust` 同时启动前端、Rust `api-server` 与本地 SpacetimeDB)
|
||||
- [x] 准备 Ubuntu 发布包构建脚本(`npm run build:rust:ubuntu` 生成 `build/<timestamp>/`,包含 `web/`、`api-server`、`spacetime_module.wasm`、`start.sh`、`stop.sh`,并默认 scp 上传到目标服务器)
|
||||
|
||||
## 3. 观测能力
|
||||
|
||||
- [x] 接入 tracing / request id / structured logs
|
||||
- [x] 接入慢请求追踪
|
||||
- [x] 接入上游 LLM / OSS / 短信 / 微信失败日志(沿用既有 provider error envelope 与 tracing,M7 固化字段口径)
|
||||
- [x] 接入关键 reducer 执行日志(现阶段固定 reducer 操作日志字段口径,真实 publish 日志回看继续由 SpacetimeDB smoke 承接)
|
||||
- [x] 接入资产任务状态日志(沿用 `AiTaskService / ai_task` 状态链,M7 固化 `task_id / status / asset_kind` 观测口径)
|
||||
|
||||
## 4. 切流准备
|
||||
|
||||
- [x] 准备旧 Node 与新 Rust 双跑窗口
|
||||
- [x] 准备 API 对比脚本
|
||||
- [x] 准备主流程 smoke 清单
|
||||
- [x] 准备前端切换开关
|
||||
- [x] 准备回退开关
|
||||
|
||||
## 5. 主工程结构收口
|
||||
|
||||
- [x] 拆分 `server-rs/crates/spacetime-module/src/lib.rs`,按业务模块与 SpacetimeDB 的 `table / reducer / procedure / view` 聚合结构整理为 `runtime`、`gameplay::{story/combat/inventory/npc/quest/runtime_item/progression}`、`custom_world`、`asset_metadata`、`ai` 等子模块,主工程 crate 根入口只保留模块声明、统一导出与最小发布入口
|
||||
|
||||
执行约束:
|
||||
|
||||
1. 这是切流前的工程结构收口,不是新功能扩张;拆分过程中不得改变既有 table schema、reducer / procedure 名称、对外 contract 与 publish 行为。
|
||||
2. 拆分后的模块边界必须与 `M0` 已冻结的模块迁移归属一致,避免 `spacetime-module` 再回退成单大包。
|
||||
3. 拆分完成后至少要保持 `cargo check`、SpacetimeDB 本地 build / publish 开发链路与主流程回归脚本可继续通过。
|
||||
|
||||
## 6. 阶段验收
|
||||
|
||||
- [x] 本地切流前预检通过(M7 阶段性预检包装入口已归档,长期入口改为 `server-rs/scripts/check.ps1`)
|
||||
- [x] 主流程基础回归通过(`cargo check -p spacetime-module`、`cargo check -p api-server`、`cargo test -p shared-contracts`、`cargo test -p api-server --no-run`)
|
||||
- [ ] 全链路 smoke 通过
|
||||
- [ ] 主流程真实环境回归通过
|
||||
- [ ] 关键 SSE 接口联调通过
|
||||
- [ ] 可在灰度环境完成切流
|
||||
|
||||
补充说明:
|
||||
|
||||
1. M7 已新增 [../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md),冻结本地预检、部署、灰度、双跑、回滚与结构收口口径。
|
||||
2. 本轮新增 [../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md),并落地 `scripts/dev-rust-stack.ps1`、`scripts/dev-rust-stack.sh`、`scripts/deploy-rust-remote.sh`;其中发布脚本当前语义为生成 Ubuntu release 包。
|
||||
3. 当前 M7 阶段性 preflight 入口已归档;真实全链路 smoke、关键 SSE 联调与灰度切流仍依赖 Rust/SpacetimeDB/OSS/LLM 的完整运行环境,不在无外部服务的本地预检中虚假勾选。
|
||||
62
backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md
Normal file
62
backend-rewrite-tasklist/07_CROSS_CUTTING_AND_ACCEPTANCE.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 横向专项、执行顺序与最终验收
|
||||
|
||||
## 1. 横向专项任务
|
||||
|
||||
### Contract 与前端兼容
|
||||
|
||||
- [x] 梳理当前 `packages/shared/src/contracts/*` 到 Rust DTO 的映射
|
||||
- [x] 设计 Rust 侧 contract 生成或手写策略
|
||||
- [x] 保持当前字段名、枚举值、响应结构稳定
|
||||
- [x] 为 breaking change 建立显式变更流程
|
||||
|
||||
### SpacetimeDB schema 演进治理
|
||||
|
||||
- [x] 约定 stable reducer 命名规则
|
||||
- [x] 约定 stable table 命名规则
|
||||
- [x] 约定列追加式演进规则
|
||||
- [x] 约定软删除而不是直接删表删列的场景
|
||||
- [x] 约定事件表与投影表拆分规则
|
||||
|
||||
### 大对象与缓存治理
|
||||
|
||||
- [x] 明确哪些内容入 OSS
|
||||
- [x] 明确哪些内容只存 SpacetimeDB 元数据
|
||||
- [x] 明确哪些内容允许短期本地缓存
|
||||
- [x] 明确 workflow cache 生命周期
|
||||
|
||||
### 文档维护
|
||||
|
||||
- [x] 每个阶段完成后同步更新设计文档
|
||||
- [x] 每个阶段完成后补一份落地记录
|
||||
- [x] 完成接口迁移后更新新的模块与 API 索引文档
|
||||
- [ ] `M4` 结构变更同步对齐 `docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
|
||||
- [x] `M5` 结构变更同步对齐 `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md`
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 横向治理规则已冻结在 [../docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md](../docs/technical/BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md)。
|
||||
2. Rust 侧 96 条 Axum 路由索引已冻结在 [../docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](../docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。
|
||||
3. `M4` 当前仍存在 `runtime_story` 独立 crate 拆分工作区,结构文档对齐需等该拆分收口后再勾选。
|
||||
|
||||
## 2. 第一优先级建议执行顺序
|
||||
|
||||
1. 先做 `M0`,冻结基线,避免迁移过程中口径漂移。
|
||||
2. 再做 `M1 + M2`,先把 Axum 壳与鉴权打稳。
|
||||
3. 再做 `M3`,优先跑通快照、设置、profile。
|
||||
4. 进入 `M4` 和 `M5` 前,先用两份 `2026-04-21` 执行方案冻结当前仓库里的 RPG 运行时链与创作链结构口径。
|
||||
5. 再做 `M4`,把 RPG runtime story 主循环真正迁走。
|
||||
6. 然后做 `M5`,迁 RPG 创作主链、works/library/gallery 与 agent。
|
||||
7. 最后做 `M6 + M7`,收口 assets、editor、部署与切流。
|
||||
|
||||
## 3. 最终验收清单
|
||||
|
||||
- [x] 当前 `96` 条后端接口已全部迁移或有兼容替代
|
||||
- [ ] 当前 `6` 个挂载面已全部迁移
|
||||
- [ ] 当前 `12` 个内部模块已完成新架构落位
|
||||
- [ ] Axum 已成为唯一 HTTP / SSE / 副作用边界
|
||||
- [ ] SpacetimeDB 已成为唯一运行时状态真相源
|
||||
- [ ] 阿里云 OSS 已成为唯一资产对象仓
|
||||
- [ ] `M4` 已与 `rpgEntry / rpgSession / rpgRuntime / rpgRuntimeStory / rpgProfile` 主链口径一致
|
||||
- [x] `M5` 已与 `agent session -> result preview -> published profile` 主链口径一致
|
||||
- [ ] 前端主流程在不大改 UI 的前提下可跑通
|
||||
- [ ] 能完成灰度切流,并保留可回退能力
|
||||
@@ -0,0 +1,183 @@
|
||||
# M0:后端挂载面冻结基线
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
|
||||
- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json)
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于完成 `M0` 的第一条任务:
|
||||
|
||||
- 整理当前后端 `6` 个挂载面并锁定为重写验收基线
|
||||
|
||||
这里的“冻结”不是要求新后端永远维持原实现,而是要求:
|
||||
|
||||
1. 当前 Node 后端历史基线仍固定为这 `6` 个挂载面。
|
||||
2. 本轮 Rust 后端的 active rewrite target 固定覆盖其中 `5` 个挂载面:`assets`、`auth`、`health`、`runtime-main`、`runtime-story-action`。
|
||||
3. `editor` 作为历史遗留挂载面继续保留对照记录,但自 `2026-04-21` 起不纳入 `server-rs` 本轮重写验收。
|
||||
4. 允许内部实现从 `Express + PostgreSQL + 本地 public/generated-*` 重写为 `Axum + SpacetimeDB + 阿里云 OSS`,但不允许把挂载面职责打散到无法对照验收。
|
||||
|
||||
## 2. 冻结结论
|
||||
|
||||
当前 Node 后端的正式挂载面固定为以下 `6` 个:
|
||||
|
||||
| 挂载面 ID | 中文名称 | 当前路由数 | 当前入口 | 必须保留的顶层路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `assets` | 资产生成工具面 | `14` | `server-node/src/app.ts -> /api/assets` | `/api/assets/*` |
|
||||
| `auth` | 鉴权与会话面 | `17` | `server-node/src/app.ts -> /api/auth` | `/api/auth/*` |
|
||||
| `editor` | 编辑器工具面 | `3` | `server-node/src/app.ts -> /api/editor` | `/api/editor/*` |
|
||||
| `health` | 基础健康检查 | `1` | `server-node/src/app.ts -> /healthz` | `/healthz` |
|
||||
| `runtime-main` | 运行时主能力面 | `59` | `server-node/src/app.ts -> /api` | `/api/runtime/*`、`/api/profile/*`、`/api/custom-world/*`、`/api/llm/*`、`/api/ws/*` |
|
||||
| `runtime-story-action` | 运行时 Story Action 面 | `2` | `server-node/src/app.ts -> /api/runtime/story` | `/api/runtime/story/*` |
|
||||
|
||||
冻结总数:
|
||||
|
||||
1. 历史对外挂载面:`6`
|
||||
2. 本轮 active rewrite target:`5`
|
||||
3. 已登记路由:`96`
|
||||
4. 公开接口:`10`
|
||||
5. JWT 接口:`69`
|
||||
6. 开关控制接口:`17`
|
||||
7. 流式接口:`6`
|
||||
|
||||
## 3. 各挂载面冻结要求
|
||||
|
||||
### 3.1 `assets`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 角色主形象生成
|
||||
2. 角色动作生成
|
||||
3. Qwen 精灵表生成与保存
|
||||
4. 工作流缓存
|
||||
5. 产物发布到 `public/generated-*`
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 仍保留独立的 `/api/assets/*` 命名空间。
|
||||
2. 仍保留“生成任务、任务状态查询、发布/保存”三类操作语义。
|
||||
3. 当前基于本地 `public/generated-*` 的产物落地,可改为 `OSS + 元数据表`,但前端一阶段必须继续通过原有路径习惯访问资源。
|
||||
4. 当前 `ASSETS_API_ENABLED` 门禁能力必须保留。
|
||||
|
||||
### 3.2 `auth`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 本地账号登录
|
||||
2. 手机验证码登录
|
||||
3. 微信登录
|
||||
4. refresh session
|
||||
5. 会话吊销
|
||||
6. 审计与风控
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 仍保留独立的 `/api/auth/*` 命名空间。
|
||||
2. 仍保留当前 `JWT + refresh cookie` 双令牌模型。
|
||||
3. 仍保留 `password / phone / wechat` 三类登录能力面。
|
||||
4. 仍保留审计日志、风控封禁、会话列表与会话吊销能力。
|
||||
|
||||
### 3.3 `editor`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 编辑器 JSON 读取
|
||||
2. 编辑器 JSON 回写
|
||||
3. 图标目录枚举
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. `server-node/src/app.ts -> /api/editor/*` 的历史存在事实继续保留在基线文档中。
|
||||
2. 自 `2026-04-21` 起,该挂载面不纳入 `server-rs` 本轮重写范围,不再作为 `M1 ~ M6` 主线交付目标。
|
||||
3. 若未来仍需清理或替代 editor,需要在遗留链路依赖核对完成后单独立项。
|
||||
|
||||
### 3.4 `health`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 提供后端进程健康探针
|
||||
2. 为代理层与 smoke 提供最小可用确认
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 仍保留 `/healthz`。
|
||||
2. 仍返回简单、无鉴权、无数据库强耦合的健康状态。
|
||||
3. 仍可作为 smoke 与部署探针的第一检查点。
|
||||
|
||||
### 3.5 `runtime-main`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 运行时存档、设置、个人档案
|
||||
2. 聊天、剧情、任务、运行时物品意图
|
||||
3. custom world library / gallery / sessions
|
||||
4. custom world agent 会话、消息、操作
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 仍保留运行时主入口作为最大能力面,不把这些能力拆散到前端无法感知的新命名空间。
|
||||
2. 仍兼容当前:
|
||||
- `/api/runtime/*`
|
||||
- `/api/profile/*`
|
||||
- `/api/custom-world/*`
|
||||
- `/api/llm/*`
|
||||
- `/api/ws/*`
|
||||
3. 除公开画廊与少量公开接口外,仍以登录态为默认访问前提。
|
||||
4. 当前大量业务逻辑虽然会迁到 `SpacetimeDB reducer/view + Axum facade`,但对前端看起来仍应是一个统一运行时能力面。
|
||||
|
||||
### 3.6 `runtime-story-action`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. story choice 动作解析
|
||||
2. story session 状态恢复
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 仍保留 `/api/runtime/story/*` 作为独立挂载面。
|
||||
2. 仍保持“前端动作输入 -> 后端统一结算 -> 返回新状态”的接口职责。
|
||||
3. 当前 `storyActionService` 里跨 `quest / inventory / runtime-item / npc / progression / combat / runtime` 的协作,迁移后必须继续存在,只是实现位置改到 `SpacetimeDB + Axum`。
|
||||
|
||||
## 4. 挂载面与新架构映射
|
||||
|
||||
| 当前挂载面 | 新架构主归属 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `assets` | `Axum + OSS + SpacetimeDB asset metadata` | 外部副作用在 Axum,二进制在 OSS,任务与引用状态在 SpacetimeDB。 |
|
||||
| `auth` | `Axum auth-service + SpacetimeDB auth tables` | 登录副作用与 cookie/JWT 在 Axum,身份与会话状态在 SpacetimeDB。 |
|
||||
| `editor` | `遗留保留于 server-node` | 历史挂载面对照,当前不进入 Rust 重写主链。 |
|
||||
| `health` | `Axum health route` | 维持最小化健康检查面。 |
|
||||
| `runtime-main` | `Axum runtime facade + SpacetimeDB runtime/custom-world tables` | Axum 维持兼容 REST/SSE,SpacetimeDB 负责状态真相。 |
|
||||
| `runtime-story-action` | `Axum story facade + SpacetimeDB gameplay reducers/views` | Story Action 入口继续独立存在,但结算内核迁到新状态层。 |
|
||||
|
||||
## 5. 本轮冻结后的硬约束
|
||||
|
||||
后续迁移中,不允许出现以下情况:
|
||||
|
||||
1. 把历史 `6` 个挂载面减少成更少但无法一一对照的“超级入口”。
|
||||
2. 为了迎合本轮重写范围而把历史存在的 `/api/editor/*` 从基线文档中抹掉。
|
||||
3. 把当前 `/api/auth/*`、`/api/assets/*`、`/api/runtime/story/*` 顶层命名空间直接改掉。
|
||||
4. 在未完成路径兼容前,直接移除 `/healthz` 或 `/generated-*` 的既有访问习惯。
|
||||
5. 在未完成契约回归前,把 `runtime-main` 和 `runtime-story-action` 的职责重新混成一个难以验收的大入口。
|
||||
|
||||
## 6. 本任务完成定义
|
||||
|
||||
当以下条件成立时,这条任务视为完成:
|
||||
|
||||
1. 当前历史 `6` 个挂载面已经有正式书面冻结清单。
|
||||
2. 每个挂载面都有:
|
||||
- 当前入口
|
||||
- 当前路由数
|
||||
- 顶层路径空间
|
||||
- 重写后必须保留的职责边界
|
||||
3. 本轮 active rewrite target 为 `5` 个,且 `editor` 的遗留/不迁移口径已经冻结。
|
||||
4. 后续任务可以直接以这份文档作为验收引用,不再靠聊天记录记忆。
|
||||
|
||||
## 7. 后续直接依赖这份基线的任务
|
||||
|
||||
1. 整理当前后端 `96` 条路由并生成“旧接口 -> 新实现”映射表
|
||||
2. 整理当前 `12` 个内部模块并锁定迁移归属
|
||||
3. 设计 Axum 路由树
|
||||
4. 设计 SpacetimeDB 表 / reducer / view 分层
|
||||
@@ -0,0 +1,262 @@
|
||||
# M0:前端直接依赖的响应头、Envelope 与错误格式冻结基线
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- `server-node/src/http.ts`
|
||||
- `server-node/src/middleware/responseEnvelope.ts`
|
||||
- `server-node/src/middleware/errorHandler.ts`
|
||||
- `server-node/src/middleware/requestId.ts`
|
||||
- `packages/shared/src/http.ts`
|
||||
- `src/services/apiClient.ts`
|
||||
- `src/services/authService.ts`
|
||||
- `src/services/aiService.ts`
|
||||
- `src/editor/shared/jsonClient.ts`
|
||||
- `src/services/apiClient.test.ts`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于完成 `M0` 的第六条任务:
|
||||
|
||||
- 整理当前前端直接依赖的响应头、envelope、错误格式
|
||||
|
||||
这里的“直接依赖”指的是:如果 Axum 重写时把这些头或 body 结构改掉,当前前端 `src/services/*`、编辑器请求层和鉴权异常处理就会立刻出问题。
|
||||
|
||||
## 2. 冻结结论
|
||||
|
||||
当前前端直接依赖的响应契约,冻结为以下 4 层:
|
||||
|
||||
1. 请求侧默认会发送 `x-genarrative-response-envelope: v1`。
|
||||
2. 响应侧默认要回 `x-request-id`、`x-api-version`、`x-route-version`。
|
||||
3. 成功响应在请求方要求 envelope 时,必须返回标准 `ok/data/error/meta` 结构。
|
||||
4. 错误响应既要兼容标准 envelope,也要兼容旧式 `{ error, meta }` / `{ message, code }` 解析回退。
|
||||
|
||||
补充结论:
|
||||
|
||||
1. 当前正式前端代码里,没有生产用例主动关闭 envelope 请求头。
|
||||
2. `x-response-time-ms` 当前不是前端代码的直接读取项,但属于现有兼容头集合,重写时仍应保留。
|
||||
3. 鉴权链路额外直接依赖错误码 `CAPTCHA_REQUIRED` 与 `error.details.captchaChallenge`。
|
||||
|
||||
## 3. 当前前端直接依赖矩阵
|
||||
|
||||
| 依赖项 | 当前值/结构 | 当前消费者 | 当前作用 |
|
||||
| --- | --- | --- | --- |
|
||||
| 请求头 | `x-genarrative-response-envelope: v1` | `src/services/apiClient.ts`、`src/editor/shared/jsonClient.ts` | 请求标准 envelope 响应。 |
|
||||
| 响应头 | `x-request-id` | `src/services/apiClient.ts` | 构造 `ApiClientError.meta.requestId` 的回退来源。 |
|
||||
| 响应头 | `x-api-version` | `src/services/apiClient.ts`、`packages/shared/src/http.ts` | 识别标准 envelope / error body。 |
|
||||
| 响应头 | `x-route-version` | `src/services/apiClient.ts` | 构造 `ApiClientError.meta.routeVersion` 的回退来源。 |
|
||||
| 成功 body | `{ ok: true, data, error: null, meta }` | `unwrapApiResponse(...)` | 前端默认解包标准成功 envelope。 |
|
||||
| 错误 body | `{ ok: false, data: null, error, meta }` | `ApiClientError`、`parseApiErrorMessage(...)` | 标准错误解析。 |
|
||||
| 旧错误 body | `{ error, meta }` / `{ message, code }` | `parseApiErrorMessage(...)` | 老接口或非标准错误回退解析。 |
|
||||
| 错误细节 | `error.code === 'CAPTCHA_REQUIRED'` 且 `error.details.captchaChallenge` | `src/services/authService.ts` | 手机验证码发送前的验证码挑战弹出。 |
|
||||
|
||||
## 4. 请求侧冻结要求
|
||||
|
||||
### 4.1 Envelope 请求头
|
||||
|
||||
当前前端默认行为:
|
||||
|
||||
1. `src/services/apiClient.ts` 会自动补:
|
||||
- `x-genarrative-response-envelope: v1`
|
||||
2. `src/editor/shared/jsonClient.ts` 也会自动补:
|
||||
- `x-genarrative-response-envelope: v1`
|
||||
|
||||
当前后端接受的 envelope 触发值:
|
||||
|
||||
1. `1`
|
||||
2. `true`
|
||||
3. `v1`
|
||||
4. `envelope`
|
||||
|
||||
但当前前端真实发送值冻结为:
|
||||
|
||||
1. `v1`
|
||||
|
||||
补充冻结点:
|
||||
|
||||
1. 虽然 `apiClient` 提供了 `omitEnvelopeHeader` 选项,但当前生产代码没有实际依赖它。
|
||||
2. 因此第一阶段 Axum 应默认兼容“前端请求即要 envelope”的模式。
|
||||
|
||||
### 4.2 鉴权与凭证约定
|
||||
|
||||
当前前端请求层默认还会做:
|
||||
|
||||
1. `Authorization: Bearer <token>` 自动注入。
|
||||
2. `credentials: same-origin`。
|
||||
3. 遇到 `401` 时尝试走 `/api/auth/refresh` 自动刷新。
|
||||
|
||||
这不是本文重点,但它解释了为什么 envelope 和错误格式必须在 `/api/auth/refresh` 上也保持兼容。
|
||||
|
||||
## 5. 响应头冻结要求
|
||||
|
||||
### 5.1 必须保留的前端直接依赖头
|
||||
|
||||
| 响应头 | 当前来源 | 当前前端用法 |
|
||||
| --- | --- | --- |
|
||||
| `x-request-id` | `requestIdMiddleware` + `applyApiResponseHeaders` | `ApiClientError.meta.requestId` 的 header 回退来源。 |
|
||||
| `x-api-version` | `applyApiResponseHeaders` | 当前标准 API 契约版本识别。 |
|
||||
| `x-route-version` | `applyApiResponseHeaders` | `ApiClientError.meta.routeVersion` 的 header 回退来源。 |
|
||||
|
||||
### 5.2 兼容头但非直接读取项
|
||||
|
||||
| 响应头 | 当前状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `x-response-time-ms` | 当前统一输出 | 目前前端代码未直接读取,但设计文档与联调约定已锁定,不能随意删除。 |
|
||||
|
||||
补充冻结点:
|
||||
|
||||
1. `requestIdMiddleware` 会优先回显请求方传入的 `x-request-id`,否则服务端自生成。
|
||||
2. `ApiClientError` 读取元信息时优先取 body `meta`,没有再回退到 headers。
|
||||
3. 这意味着即便 envelope body 缺少部分 `meta` 字段,headers 仍必须完整。
|
||||
|
||||
## 6. 成功响应 Envelope 冻结格式
|
||||
|
||||
当前标准成功 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {},
|
||||
"error": null,
|
||||
"meta": {
|
||||
"apiVersion": "2026-04-08",
|
||||
"requestId": "req-xxx",
|
||||
"routeVersion": "2026-04-08",
|
||||
"operation": "runtime.story.initial",
|
||||
"latencyMs": 12,
|
||||
"timestamp": "2026-04-20T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
冻结规则:
|
||||
|
||||
1. `ok` 必须为 `true`。
|
||||
2. `data` 为真实业务负载。
|
||||
3. `error` 必须为 `null`。
|
||||
4. `meta.apiVersion` 必须存在,因为 `unwrapApiResponse(...)` 与 `isApiResponse(...)` 依赖它判断标准 envelope。
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 如果请求未带 envelope 头,当前后端可以直接返回裸 `data`。
|
||||
2. 但由于当前前端默认都会请求 envelope,第一阶段 Axum 基本等价于“所有 JSON 成功响应都要兼容这个结构”。
|
||||
|
||||
## 7. 错误响应 Envelope 与旧格式回退
|
||||
|
||||
### 7.1 当前标准错误 envelope
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "缺少 Authorization Bearer Token",
|
||||
"details": null
|
||||
},
|
||||
"meta": {
|
||||
"apiVersion": "2026-04-08",
|
||||
"requestId": "req-xxx",
|
||||
"routeVersion": "2026-04-08",
|
||||
"operation": "auth.me",
|
||||
"latencyMs": 3,
|
||||
"timestamp": "2026-04-20T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
冻结规则:
|
||||
|
||||
1. `ok` 必须为 `false`。
|
||||
2. `data` 必须为 `null`。
|
||||
3. `error.code`、`error.message` 必须存在。
|
||||
4. `error.details` 可为对象或 `null`。
|
||||
5. `meta.apiVersion` 必须存在。
|
||||
|
||||
### 7.2 当前旧式错误格式回退
|
||||
|
||||
当请求未要求 envelope,或某些链路仍走旧写法时,当前后端与前端仍兼容以下错误结构:
|
||||
|
||||
1. `{ "error": { "code": "...", "message": "...", "details": ... }, "meta": {...} }`
|
||||
2. `{ "message": "...", "code": "..." }`
|
||||
3. `{ "error": { "message": "..." } }`
|
||||
4. 纯文本错误响应
|
||||
|
||||
`parseApiErrorMessage(rawText, fallbackMessage)` 的当前回退顺序固定为:
|
||||
|
||||
1. `parsed.error.message`
|
||||
2. 顶层 `message`
|
||||
3. `error.code` 或顶层 `code`,拼成 `fallback(CODE)`
|
||||
4. 原始文本
|
||||
5. 调用方的 `fallbackMessage`
|
||||
|
||||
这意味着:
|
||||
|
||||
1. Axum 第一阶段不能只兼容标准 envelope,而忽略旧错误解析的回退行为。
|
||||
2. 至少在迁移过渡期,`parseApiErrorMessage(...)` 可识别的信息要继续保留。
|
||||
|
||||
## 8. 前端对错误细节的业务级直接依赖
|
||||
|
||||
### 8.1 验证码挑战
|
||||
|
||||
`src/services/authService.ts` 当前明确依赖:
|
||||
|
||||
1. `error instanceof ApiClientError`
|
||||
2. `error.code === 'CAPTCHA_REQUIRED'`
|
||||
3. `error.details.captchaChallenge`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 如果后端要继续触发验证码挑战,必须继续返回:
|
||||
- `code: 'CAPTCHA_REQUIRED'`
|
||||
- `details.captchaChallenge`
|
||||
2. 不能只返回中文文案而不带结构化 `details`。
|
||||
|
||||
### 8.2 元信息透传
|
||||
|
||||
`ApiClientError` 当前会保留:
|
||||
|
||||
1. `status`
|
||||
2. `code`
|
||||
3. `details`
|
||||
4. `meta.apiVersion`
|
||||
5. `meta.requestId`
|
||||
6. `meta.routeVersion`
|
||||
7. `meta.operation`
|
||||
8. `meta.latencyMs`
|
||||
9. `meta.timestamp`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. Axum 不能把这些字段全都删成单纯 `message` 字符串。
|
||||
2. 即使部分业务 UI 现在没显示这些字段,它们已经进入前端错误对象结构。
|
||||
|
||||
## 9. 当前消费者清单
|
||||
|
||||
以下文件已构成当前前端的直接依赖面:
|
||||
|
||||
1. `src/services/apiClient.ts`
|
||||
2. `src/services/authService.ts`
|
||||
3. `src/services/aiService.ts`
|
||||
4. `src/editor/shared/jsonClient.ts`
|
||||
5. `packages/shared/src/http.ts`
|
||||
|
||||
## 10. 本轮冻结后的硬约束
|
||||
|
||||
后续迁移中,不允许出现以下情况:
|
||||
|
||||
1. 删除 `x-genarrative-response-envelope: v1` 的请求协商能力。
|
||||
2. 删除 `x-request-id`、`x-api-version`、`x-route-version` 这些当前前端直接依赖的响应头。
|
||||
3. 把成功 envelope 从 `{ ok, data, error, meta }` 改成其他字段名。
|
||||
4. 把错误 envelope 从 `{ ok: false, data: null, error, meta }` 改成只有 `message`。
|
||||
5. 删除 `CAPTCHA_REQUIRED + details.captchaChallenge` 这一结构化错误契约。
|
||||
6. 让前端默认请求 envelope,但后端返回裸数据且不再可被 `unwrapApiResponse(...)` 识别。
|
||||
|
||||
## 11. 本任务完成定义
|
||||
|
||||
当以下条件成立时,这条任务视为完成:
|
||||
|
||||
1. 当前前端直接依赖的响应头、envelope、错误格式已有书面冻结清单。
|
||||
2. 已明确哪些是前端直接读取项,哪些是兼容保留项。
|
||||
3. 后续 Axum handler、错误中间件、response envelope 中间件可以直接按本文对齐,而不再靠人工试错。
|
||||
@@ -0,0 +1,245 @@
|
||||
# M0:`/generated-*` 静态资源前缀冻结基线
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
|
||||
- `server-node/src/modules/assets/characterAssetRoutes.ts`
|
||||
- `server-node/src/modules/assets/qwenSpriteRoutes.ts`
|
||||
- `server-node/src/services/sceneImageService.ts`
|
||||
- `server-node/src/services/customWorldCoverAssetService.ts`
|
||||
- `server-node/src/services/customWorldAgentAutoAssetService.ts`
|
||||
- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于完成 `M0` 的第五条任务:
|
||||
|
||||
- 整理当前所有 `/generated-*` 静态资源前缀
|
||||
|
||||
这里的“整理”不是只列出几个目录名,而是要求冻结以下信息:
|
||||
|
||||
1. 当前哪些 `/generated-*` 前缀是正式业务前缀。
|
||||
2. 每个前缀由哪条后端链路产出。
|
||||
3. 每个前缀对应的当前路径模板是什么。
|
||||
4. 哪些前缀只是未来设计名或测试噪音,不能误当成当前正式兼容面。
|
||||
|
||||
## 2. 冻结结论
|
||||
|
||||
当前工程里,正式业务使用的 `/generated-*` 静态资源前缀固定为以下 `6` 个:
|
||||
|
||||
| 前缀 | 当前状态 | 当前主要生产链路 | 当前典型路径模板 | 重写后兼容要求 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `/generated-character-drafts/*` | 正式前缀 | 角色主形象草稿、动作草稿、导入参考素材 | `/generated-character-drafts/{characterId}/{kind}/{jobId}/{file}` | 必须保留 |
|
||||
| `/generated-characters/*` | 正式前缀 | 角色主形象正式发布、Agent 自动角色图回填 | `/generated-characters/{characterId}/visual/{assetId}/{file}` | 必须保留 |
|
||||
| `/generated-animations/*` | 正式前缀 | 角色基础动作正式发布 | `/generated-animations/{characterId}/{animationSetId}/{action}/{file}` | 必须保留 |
|
||||
| `/generated-custom-world-scenes/*` | 正式前缀 | 世界场景图生成、Agent 自动场景图回填 | `/generated-custom-world-scenes/{world}/{landmarkOrAct}/{assetId}/{file}` | 必须保留 |
|
||||
| `/generated-custom-world-covers/*` | 正式前缀 | 世界封面图生成、封面上传 | `/generated-custom-world-covers/{world}/{assetId}/{file}` | 必须保留 |
|
||||
| `/generated-qwen-sprites/*` | 正式前缀 | Qwen 主图草稿、精灵表草稿、修帧草稿、最终保存 | `/generated-qwen-sprites/{assetKeyOrDraftScope}/{actionOrKind}/{assetId}/{file}` | 必须保留 |
|
||||
|
||||
额外结论:
|
||||
|
||||
1. 当前仓库里真实业务前缀是 `6` 个,不是 `4` 个也不是 `5` 个。
|
||||
2. 其中 `generated-animations` 与 `generated-custom-world-covers` 是当前代码已正式使用、但早期重写设计里容易漏掉的两个前缀。
|
||||
3. 当前 `public/` 目录下已存在:
|
||||
- `generated-character-drafts`
|
||||
- `generated-characters`
|
||||
- `generated-qwen-sprites`
|
||||
4. `generated-animations`、`generated-custom-world-scenes`、`generated-custom-world-covers` 当前按需惰性创建,不代表它们不是正式前缀。
|
||||
|
||||
## 3. 正式前缀清单
|
||||
|
||||
### 3.1 `/generated-character-drafts/*`
|
||||
|
||||
当前用途:
|
||||
|
||||
1. 角色主形象候选图草稿。
|
||||
2. 角色动作草稿帧、草稿视频、导入参考素材。
|
||||
3. 角色资产工作流缓存与任务记录。
|
||||
|
||||
当前主要生产链路:
|
||||
|
||||
1. `POST /api/assets/character-visual/generate`
|
||||
2. `POST /api/assets/character-animation/generate`
|
||||
3. `POST /api/assets/character-animation/import-video`
|
||||
4. `POST /api/assets/character-workflow-cache`
|
||||
|
||||
当前典型路径模板:
|
||||
|
||||
1. `/generated-character-drafts/{characterId}/visual/{jobId}/candidate-01.png`
|
||||
2. `/generated-character-drafts/{characterId}/animation/{action}/{jobId}/preview.mp4`
|
||||
3. `/generated-character-drafts/{characterId}/animation/{action}/{jobId}/frame-01.png`
|
||||
4. `/generated-character-drafts/{characterId}-{cacheKey}/workflow-cache.json`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 它是“草稿态真相路径”,不是正式发布路径。
|
||||
2. 重写为 OSS 后仍要保留这一层 HTTP 兼容前缀,哪怕底层已不是本地磁盘。
|
||||
|
||||
### 3.2 `/generated-characters/*`
|
||||
|
||||
当前用途:
|
||||
|
||||
1. 角色主形象正式发布路径。
|
||||
2. Custom World Agent 自动回填的角色主图。
|
||||
|
||||
当前主要生产链路:
|
||||
|
||||
1. `POST /api/assets/character-visual/publish`
|
||||
2. `customWorldAgentAutoAssetService`
|
||||
|
||||
当前典型路径模板:
|
||||
|
||||
1. `/generated-characters/{characterId}/visual/{assetId}/master.png`
|
||||
2. `/generated-characters/{characterId}/visual/{assetId}/preview-01.png`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 它是角色正式视觉资产前缀,不允许与草稿前缀混用。
|
||||
2. 后续即使内部改成 OSS,也必须继续对前端暴露这一前缀。
|
||||
|
||||
### 3.3 `/generated-animations/*`
|
||||
|
||||
当前用途:
|
||||
|
||||
1. 角色基础动作正式发布路径。
|
||||
2. `CharacterAnimator` 侧消费的核心动画资源前缀。
|
||||
|
||||
当前主要生产链路:
|
||||
|
||||
1. `POST /api/assets/character-animation/publish`
|
||||
|
||||
当前典型路径模板:
|
||||
|
||||
1. `/generated-animations/{characterId}/{animationSetId}/{action}/frame-01.png`
|
||||
2. `/generated-animations/{characterId}/{animationSetId}/{action}/preview.mp4`
|
||||
3. `/generated-animations/{characterId}/{animationSetId}/{action}/manifest.json`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 当前正式动画并不挂在 `/generated-characters/.../animation/...` 下,而是独立前缀 `/generated-animations/*`。
|
||||
2. 后续重写若想合并对象键,也必须先做兼容别名,不能直接改掉公开路径。
|
||||
|
||||
### 3.4 `/generated-custom-world-scenes/*`
|
||||
|
||||
当前用途:
|
||||
|
||||
1. 自定义世界场景图生成结果。
|
||||
2. Agent 自动生成的场景图回填结果。
|
||||
|
||||
当前主要生产链路:
|
||||
|
||||
1. `POST /api/custom-world/scene-image`
|
||||
2. `customWorldAgentAutoAssetService`
|
||||
|
||||
当前典型路径模板:
|
||||
|
||||
1. `/generated-custom-world-scenes/{worldSegment}/{landmarkSegment}/{assetId}/scene.png`
|
||||
2. `/generated-custom-world-scenes/{sceneSegment}/{actSegment}/{assetId}/scene.png`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 前缀里当前显式带 `custom-world-scenes`,不是通用 `generated-scenes`。
|
||||
2. 后续世界资料库、世界编辑器和 Agent 卡片仍会依赖这一命名习惯。
|
||||
|
||||
### 3.5 `/generated-custom-world-covers/*`
|
||||
|
||||
当前用途:
|
||||
|
||||
1. 自定义世界封面图生成结果。
|
||||
2. 自定义世界封面图上传后规范化结果。
|
||||
|
||||
当前主要生产链路:
|
||||
|
||||
1. `POST /api/custom-world/cover-image`
|
||||
2. `POST /api/custom-world/cover-upload`
|
||||
|
||||
当前典型路径模板:
|
||||
|
||||
1. `/generated-custom-world-covers/{worldSegment}/{assetId}/cover.png`
|
||||
2. `/generated-custom-world-covers/{worldSegment}/{assetId}/manifest.json`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 当前正式前缀是 `/generated-custom-world-covers/*`,不是 `/generated-cover-images/*`。
|
||||
2. 后续重写设计和 OSS key 规划必须以这个现状兼容面为准。
|
||||
|
||||
### 3.6 `/generated-qwen-sprites/*`
|
||||
|
||||
当前用途:
|
||||
|
||||
1. Qwen 精灵主图草稿。
|
||||
2. Qwen 精灵表草稿。
|
||||
3. Qwen 修帧草稿。
|
||||
4. Qwen 精灵表最终保存结果。
|
||||
|
||||
当前主要生产链路:
|
||||
|
||||
1. `POST /api/assets/qwen-sprite/master`
|
||||
2. `POST /api/assets/qwen-sprite/sheet`
|
||||
3. `POST /api/assets/qwen-sprite/frame-repair`
|
||||
4. `POST /api/assets/qwen-sprite/save`
|
||||
|
||||
当前典型路径模板:
|
||||
|
||||
1. `/generated-qwen-sprites/_drafts/master/{draftId}/candidate-01.png`
|
||||
2. `/generated-qwen-sprites/_drafts/sheet/{draftId}/candidate-01.png`
|
||||
3. `/generated-qwen-sprites/_drafts/repair/{draftId}/candidate-01.png`
|
||||
4. `/generated-qwen-sprites/{assetKey}/{actionKey}/{assetId}/sheet.png`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 这个前缀当前既承载草稿,也承载正式保存结果。
|
||||
2. 如果未来要把草稿与正式对象拆桶,也必须先保留同一公开前缀兼容。
|
||||
|
||||
## 4. 非正式前缀与噪音项
|
||||
|
||||
以下命名当前不能当成“正式兼容前缀”:
|
||||
|
||||
| 名称 | 当前来源 | 处理结论 |
|
||||
| --- | --- | --- |
|
||||
| `/generated-cover-images/*` | 仅出现在重写设计文档旧草案里 | 这是未来提案名,不是当前工程真实前缀。 |
|
||||
| `/generated-role*` | 测试数据或示例 ID | 不是正式静态资源前缀。 |
|
||||
| `/generated-world*` | 测试数据或对象 ID | 不是正式静态资源前缀。 |
|
||||
| `/generated-ruins*` | 测试数据或对象 ID | 不是正式静态资源前缀。 |
|
||||
|
||||
结论:
|
||||
|
||||
1. 后续做 Axum 静态资源兼容层时,只能以第 2 节和第 3 节里的 `6` 个前缀为正式基线。
|
||||
2. 不能把测试里的字符串误判成真实资源入口。
|
||||
|
||||
## 5. 与重写设计的对齐要求
|
||||
|
||||
为避免后续按错路径重写,当前冻结以下对齐规则:
|
||||
|
||||
1. Axum 第一阶段必须兼容这 `6` 个公开资源前缀。
|
||||
2. OSS 内部对象键可以调整,但对前端暴露的 URL 前缀不能少于这 `6` 个。
|
||||
3. 设计文档里的对象键规划如果与这份基线冲突,以这份“当前工程冻结基线”为准,然后再在设计文档中补兼容说明。
|
||||
|
||||
## 6. 本轮冻结后的硬约束
|
||||
|
||||
后续迁移中,不允许出现以下情况:
|
||||
|
||||
1. 把 `/generated-animations/*` 直接并入 `/generated-characters/*` 却不保留兼容别名。
|
||||
2. 把 `/generated-custom-world-covers/*` 擅自改成 `/generated-cover-images/*`。
|
||||
3. 在未做兼容层前,删除 `/generated-character-drafts/*` 或 `/generated-qwen-sprites/*` 的草稿访问习惯。
|
||||
4. 仅因为某个目录在当前仓库里尚未生成,就把它从正式前缀清单里删掉。
|
||||
|
||||
## 7. 本任务完成定义
|
||||
|
||||
当以下条件成立时,这条任务视为完成:
|
||||
|
||||
1. 当前正式 `/generated-*` 前缀已经有书面冻结清单。
|
||||
2. 每个前缀都已明确:
|
||||
- 当前用途
|
||||
- 当前生产链路
|
||||
- 当前路径模板
|
||||
- 后续兼容要求
|
||||
3. 已明确哪些“generated-*”字符串只是噪音,不属于正式前缀。
|
||||
|
||||
## 8. 后续直接依赖这份基线的任务
|
||||
|
||||
1. 设计 Axum 静态资源兼容层
|
||||
2. 设计 OSS 对象键与 CDN 别名
|
||||
3. 做 assets / custom world / editor 的路径回归测试
|
||||
@@ -0,0 +1,291 @@
|
||||
# M0:内部模块迁移归属基线
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
|
||||
- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json)
|
||||
- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于完成 `M0` 的第三条任务:
|
||||
|
||||
- 整理当前 `12` 个内部模块并锁定迁移归属
|
||||
|
||||
这里的“迁移归属”不是简单把旧目录名照搬到 Rust,而是要求后续重写必须先明确:
|
||||
|
||||
1. 每个旧模块在新架构中的主归属 crate。
|
||||
2. 每个旧模块是否需要拆成“SpacetimeDB 状态层 + Axum/application 编排层”。
|
||||
3. 每个旧模块的状态真相应该进入 `SpacetimeDB`、`OSS` 还是开发态本地文件适配。
|
||||
4. 每个旧模块优先落在哪个迁移阶段,避免后续任务拆分时反复改口径。
|
||||
|
||||
命名补充说明:
|
||||
|
||||
1. 本文中仍出现的 `application::...`、`auth-service`、`oss-service`、`llm-service` 等名称,统一表示逻辑职责,不再要求它们必须继续作为顶层独立 crate 存在。
|
||||
2. 在新的多 crate 结构下,这些逻辑职责默认落到对应 `crates/module-*` 的内部子层次,或落到 `crates/platform-*`、`crates/shared-*` 等共享 crate 中。
|
||||
|
||||
补充边界:
|
||||
|
||||
1. 本文只覆盖当前 `server-node/src/modules/*` 下的 `12` 个内部模块。
|
||||
2. `auth`、`health` 虽然属于后端能力面,但不在这 `12` 个内部模块目录里,因此不在本文表内。
|
||||
|
||||
## 2. 冻结结论
|
||||
|
||||
当前 Node 后端的正式内部模块固定为以下 `12` 个:
|
||||
|
||||
补充口径:
|
||||
|
||||
1. 上表 `12` 个模块属于历史基线总量。
|
||||
2. 自 `2026-04-21` 起,本轮 Rust 后端重写的 active rewrite modules 固定为 `11` 个。
|
||||
3. `editor` 作为遗留无用模块,仅保留历史事实对照,不再进入 `server-rs` 主线迁移。
|
||||
|
||||
| 模块 ID | 中文名称 | 当前目录 | 关联路由数 | 当前对外暴露面 | 重写后主归属 | 重写后次归属 | 目标迁移阶段 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `ai` | AI 编排模块 | `server-node/src/modules/ai` | `23` | `runtime-main` | `application + llm-service` | `contracts + api-server SSE facade` | `M4`、`M5`、`M6` |
|
||||
| `assets` | 资产工具模块 | `server-node/src/modules/assets` | `18` | `assets` | `application::assets + oss-service` | `spacetime-module::asset_metadata` | `M6` |
|
||||
| `combat` | 战斗结算模块 | `server-node/src/modules/combat` | `1` | `runtime-story-action` | `spacetime-module::gameplay::combat` | `domain::combat` | `M4` |
|
||||
| `custom-world` | 自定义世界运行时模块 | `server-node/src/modules/custom-world` | `26` | `runtime-main` | `spacetime-module::custom_world + application::custom_world` | `llm-service + oss-service` | `M5` |
|
||||
| `editor` | 编辑器资源模块 | `server-node/src/modules/editor` | `3` | `editor` | `不迁移(遗留模块)` | 保留 `server-node/` 历史链路对照 | `不纳入本轮` |
|
||||
| `inventory` | 背包与物品变更模块 | `server-node/src/modules/inventory` | `1` | `runtime-story-action` | `spacetime-module::gameplay::inventory` | `domain::inventory` | `M4` |
|
||||
| `npc` | NPC 交互模块 | `server-node/src/modules/npc` | `6` | `runtime-story-action`、`runtime-main` | `spacetime-module::gameplay::npc` | `application::npc_dialogue + llm-service` | `M4`、`M5` |
|
||||
| `progression` | 成长与关卡进程模块 | `server-node/src/modules/progression` | `3` | `runtime-story-action`、`runtime-main` | `spacetime-module::gameplay::progression` | `domain::progression` | `M3`、`M4` |
|
||||
| `quest` | 任务运行时模块 | `server-node/src/modules/quest` | `4` | `runtime-main`、`runtime-story-action` | `spacetime-module::gameplay::quest` | `application::quest_drafting + llm-service` | `M3`、`M4` |
|
||||
| `runtime` | 运行时状态基座模块 | `server-node/src/modules/runtime` | `32` | `runtime-main`、`runtime-story-action` | `spacetime-module::runtime` | `application::runtime_facade` | `M3` |
|
||||
| `runtime-item` | 运行时物品模块 | `server-node/src/modules/runtime-item` | `2` | `runtime-main`、`runtime-story-action` | `spacetime-module::gameplay::runtime_item` | `application::item_intent + llm-service` | `M4` |
|
||||
| `story` | 故事会话模块 | `server-node/src/modules/story` | `10` | `runtime-main`、`runtime-story-action` | `spacetime-module::gameplay::story` | `application::story_facade + api-server SSE facade` | `M4` |
|
||||
|
||||
冻结总数:
|
||||
|
||||
1. 历史内部模块目录:`12`
|
||||
2. 本轮 active rewrite modules:`11`
|
||||
3. 关联路由数最多的模块:`runtime`,共 `32` 条
|
||||
4. 本轮纯外部副作用导向模块:`ai`、`assets`
|
||||
5. 已退出本轮重写范围的遗留模块:`editor`
|
||||
6. 纯状态规则导向模块:`combat`、`inventory`
|
||||
7. 需要“状态层 + 编排层”双落位的混合模块:`custom-world`、`npc`、`quest`、`runtime-item`、`story`
|
||||
|
||||
## 3. 锁定迁移归属规则
|
||||
|
||||
后续所有重写实现,必须先遵守以下归属规则:
|
||||
|
||||
1. 纯运行时状态、纯规则计算、纯领域变更,优先进入 `spacetime-module/` 与 `domain/`,不能继续把真相留在 Axum 内存或 Node 风格 service。
|
||||
2. 外部模型调用、OSS 上传、短信、微信、本地文件读写,统一放在 `application/ + api-server/ + infra service`,不能塞进 SpacetimeDB reducer。
|
||||
3. 任何当前“一个模块同时做状态和副作用”的能力,在新架构里都必须拆成:
|
||||
- `SpacetimeDB`:状态真相与读模型
|
||||
- `Axum/application`:外部编排、SSE、对象上传、三方调用
|
||||
4. `public/generated-*` 不再是任何模块的真相源;未来只能作为兼容访问前缀或 CDN 映射。
|
||||
5. 不允许把旧模块简单合并成一个“大 runtime service”;必须保留可对照的领域边界。
|
||||
|
||||
## 4. 模块迁移矩阵
|
||||
|
||||
| 当前模块 | 当前职责摘要 | 新状态真相源 | 新外部副作用归属 | 迁移后必须落位 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ai` | prompt 编排、聊天/剧情/世界生成组织 | `SpacetimeDB` 只存任务和结果引用,不存编排过程真相 | `llm-service` | 只能留在 Axum/application 侧,禁止直接进 reducer。 |
|
||||
| `assets` | 生成、发布、缓存、Qwen 精灵表 | `asset_job`、`asset_object`、`asset_manifest` 等表 | `oss-service` + 外部媒体模型 | 二进制进 OSS,任务与引用进 SpacetimeDB。 |
|
||||
| `combat` | 战斗结算、数值变化 | `battle_state`、`story_event` | 无 | 作为纯 reducer 规则模块落到 gameplay。 |
|
||||
| `custom-world` | 世界资料、问答流、Agent 草稿与编译 | `custom_world_*` 系列表 | `llm-service`、`oss-service` | 世界状态在 SpacetimeDB,编译/生成在 Axum。 |
|
||||
| `editor` | 编辑器 JSON 读写、图标枚举 | 仍以遗留 Node 链路与开发态本地文件为历史对照 | 不迁移到 `server-rs` | 仅保留历史基线,不纳入本轮 Rust 重写。 |
|
||||
| `inventory` | 背包变更、物品副作用、NPC 背包交互 | `inventory_slot`、`story_event` | 无 | 归入 story action 对应 reducer。 |
|
||||
| `npc` | 互动规则、关系变化、招募/对话语义 | `npc_state`、`story_event` | `application::npc_dialogue + llm-service` | 状态归 SpacetimeDB,台词生成归 Axum。 |
|
||||
| `progression` | 等级、章节、敌对 scaling、benchmark | `player_progression`、`chapter_progression` | 无 | 作为 runtime / story 公共领域模块进入 SpacetimeDB。 |
|
||||
| `quest` | 任务意图、日志、进度变化 | `quest_record`、`story_event` | `application::quest_drafting + llm-service` | 任务状态归 SpacetimeDB,生成型任务草案归 Axum。 |
|
||||
| `runtime` | 快照、设置、资料页、状态归一化 | `runtime_snapshot`、`runtime_setting`、`profile_*` | 无 | 作为新后端最先迁移的状态基座模块。 |
|
||||
| `runtime-item` | 物品意图、奖励解析、宝藏逻辑 | `treasure_record`、`inventory_slot`、`story_event` | `application::item_intent + llm-service` | 奖励结算归 reducer,意图生成归 Axum。 |
|
||||
| `story` | 会话状态、动作分发、主循环 | `story_session`、`story_event` | `application::story_facade + SSE` | 主循环状态归 SpacetimeDB,流式输出由 Axum 兼容。 |
|
||||
|
||||
## 5. 各模块冻结要求
|
||||
|
||||
### 5.1 `ai`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责剧情、多轮聊天、自定义世界等 prompt 编排。
|
||||
2. 自身不负责持久化,但会被多条 runtime 路由反复调用。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 主归属固定为 `application + llm-service`,不是 `spacetime-module`。
|
||||
2. 后续如果需要记录 AI 阶段状态,只能把任务状态或结果引用写入 SpacetimeDB,不把供应商 SDK 与 prompt 执行放进 reducer。
|
||||
3. 与 `story`、`custom-world`、`runtime-item`、`quest` 的关系固定为“它们产生命令,`ai` 负责外部生成”。
|
||||
|
||||
### 5.2 `assets`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责角色形象、动作、Qwen 精灵表生成。
|
||||
2. 负责发布到 `public/generated-*` 与局部 manifest。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 二进制对象一律进入 `OSS`。
|
||||
2. 主归属固定为 `application::assets + oss-service`。
|
||||
3. 资产任务状态、对象引用关系、发布绑定关系必须进入 `spacetime-module::asset_metadata`。
|
||||
4. 后续不允许继续以本地 `public/generated-*` 是否存在文件作为业务真相。
|
||||
|
||||
### 5.3 `combat`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 提供 story action 里的战斗型结算。
|
||||
2. 本质是纯规则计算。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 主归属固定为 `spacetime-module::gameplay::combat`。
|
||||
2. 不单独拥有 HTTP 路由,也不直接依赖外部 IO。
|
||||
3. 后续实现必须保持纯规则、可测试、可被 `resolve_story_action` reducer 复用。
|
||||
|
||||
### 5.4 `custom-world`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责 creator intent、world profile、传统问答流、Agent 运行时类型。
|
||||
2. 同时牵涉世界编译、资产生成和公开画廊。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 这是标准的双落位模块:
|
||||
- `SpacetimeDB` 保存会话、草稿、作品、画廊、Agent 状态。
|
||||
- `Axum/application` 负责编译、SSE、外部 LLM 与资产生成编排。
|
||||
2. 传统问答流和 Agent 流必须拆表,不能继续长期混成一个大 JSON 会话体。
|
||||
3. 对外仍然要兼容当前 `/api/custom-world/*` 与 `/api/runtime/custom-world/*` 访问习惯。
|
||||
|
||||
### 5.5 `editor`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 读写编辑器资源 JSON。
|
||||
2. 枚举工作区 `public/Icons` 图标资源。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 该模块在 `server-node/` 中的存在事实继续保留,用于历史基线与后续清理对照。
|
||||
2. 自 `2026-04-21` 起,不再为 `server-rs/` 创建 `module-editor` crate,也不再把它纳入 `M1 ~ M6` 主线迁移。
|
||||
3. 若未来仍需清理或替代 editor,必须在遗留链路依赖确认后单独立项,不能夹带进当前 Rust 重写主链。
|
||||
4. 不允许为了简化本轮任务而篡改其历史存在事实。
|
||||
|
||||
### 5.6 `inventory`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责背包变更、赠礼、NPC 背包交互等副作用。
|
||||
2. 当前主要被 story action 调用。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 主归属固定为 `spacetime-module::gameplay::inventory`。
|
||||
2. 与 `story`、`runtime-item` 的交互必须通过 reducer 协调,不能回到“多个 service 各自改 JSON”。
|
||||
3. 后续如需对外展示背包读模型,优先通过 view 暴露,不新增独立真相副本。
|
||||
|
||||
### 5.7 `npc`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责 NPC 关系、招募、交互规则与场景 NPC 语义。
|
||||
2. 同时参与 runtime 聊天流和 story action 结算。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 状态真相固定在 `spacetime-module::gameplay::npc`。
|
||||
2. LLM 对话、招募话术、流式文本输出固定由 `application::npc_dialogue + llm-service` 处理。
|
||||
3. 不允许把 NPC 状态又分散回聊天 session store、本地缓存或前端临时状态。
|
||||
|
||||
### 5.8 `progression`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责角色成长、章节推进、敌对强度等规则。
|
||||
2. 同时影响 snapshot hydrate 与 story action 结算。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 主归属固定为 `spacetime-module::gameplay::progression`。
|
||||
2. 仍保持纯规则、纯领域建模,不承接外部 IO。
|
||||
3. 作为 `runtime` 与 `story` 的公共领域组件,不能被重新塞回单一路由 handler。
|
||||
|
||||
### 5.9 `quest`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责任务语义、任务日志、任务进度信号。
|
||||
2. 既参与 AI 草案生成,也参与 story action 结算。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 任务主状态固定进入 `spacetime-module::gameplay::quest`。
|
||||
2. AI 生成的任务候选与草案编排固定由 `application::quest_drafting + llm-service` 承担。
|
||||
3. 前端兼容接口仍走 `/api/runtime/quests/*` 或 story action 聚合,不新增前端直连任务状态写入口。
|
||||
|
||||
### 5.10 `runtime`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 是当前运行时快照、设置、资料页、状态归一化的基座模块。
|
||||
2. 路由覆盖最广,是 Node 版后端迁移的第一主战场。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 主归属固定为 `spacetime-module::runtime`。
|
||||
2. `runtime_snapshot`、`runtime_setting`、`profile_*` 等读写模型优先在 `M3` 完成迁移。
|
||||
3. Axum 只保留兼容 facade,不再继续让快照真相停留在 PostgreSQL 风格 repository。
|
||||
|
||||
### 5.11 `runtime-item`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责运行时物品意图、奖励、宝藏解析与剧情指纹。
|
||||
2. 同时受到 AI 生成与 story action 结算驱动。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 奖励、掉落、宝藏等状态变化固定进入 `spacetime-module::gameplay::runtime_item`。
|
||||
2. 物品意图生成固定由 `application::item_intent + llm-service` 承接。
|
||||
3. 物品领域不能再拆成“部分在 story、部分在 route、部分在前端”的临时实现。
|
||||
|
||||
### 5.12 `story`
|
||||
|
||||
当前定位:
|
||||
|
||||
1. 负责运行时故事会话、动作分发与 state 恢复。
|
||||
2. 当前既暴露 REST,也暴露与聊天/继续剧情相关的流式体验。
|
||||
|
||||
重写后的冻结要求:
|
||||
|
||||
1. 主归属固定为 `spacetime-module::gameplay::story`。
|
||||
2. SSE 输出与兼容 DTO 拼装固定由 `application::story_facade + api-server SSE facade` 负责。
|
||||
3. `storyAction.resolve` 的跨模块联动必须以 `story` 为编排入口,但不再由单个 Node service 直接改整包 JSON。
|
||||
|
||||
## 6. 本轮冻结后的硬约束
|
||||
|
||||
后续迁移中,不允许出现以下情况:
|
||||
|
||||
1. 把 `ai`、`assets` 直接放进 SpacetimeDB reducer 执行三方网络或文件系统 IO。
|
||||
2. 在未单独立项前,把已退出本轮范围的 `editor` 重新并回 `server-rs` 主链。
|
||||
3. 把 `combat`、`inventory`、`progression` 重新做成只存在于 Axum handler 内部的计算 helper。
|
||||
4. 把 `custom-world`、`story`、`npc` 这类混合模块继续保留为“单大对象 JSON + 单大 service 写回”模式。
|
||||
5. 把 `runtime` 当成一个兜底垃圾桶,把其他领域模块重新并回去。
|
||||
6. 在没有对应 Axum facade 的前提下,让前端第一阶段直接依赖 SpacetimeDB 原生写接口。
|
||||
|
||||
## 7. 本任务完成定义
|
||||
|
||||
当以下条件成立时,这条任务视为完成:
|
||||
|
||||
1. 当前历史 `12` 个内部模块已经有正式书面冻结清单。
|
||||
2. 每个模块都已明确:
|
||||
- 当前目录
|
||||
- 关联路由数
|
||||
- 对外暴露面
|
||||
- 重写后主归属
|
||||
- 重写后次归属
|
||||
- 目标迁移阶段
|
||||
3. 本轮 active rewrite modules 为 `11` 个,且 `editor` 的遗留/不迁移口径已经冻结。
|
||||
4. 后续拆 `server-rs/` 多 crate、建 SpacetimeDB bounded context、排 M3~M6 任务时,可以直接引用本文,不再靠口头记忆。
|
||||
|
||||
## 8. 后续直接依赖这份基线的任务
|
||||
|
||||
1. 设计 `server-rs/` workspace 与 crate 边界
|
||||
2. 设计 SpacetimeDB `runtime / gameplay / custom_world / asset_metadata` 表分层
|
||||
3. 设计 story action reducer 的跨模块协作边界
|
||||
4. 设计 custom world / assets 的 Axum facade
|
||||
@@ -0,0 +1,106 @@
|
||||
# M0:阶段验收矩阵
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [00_MASTER_TASKLIST.md](./00_MASTER_TASKLIST.md)
|
||||
- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md)
|
||||
- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md)
|
||||
- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md)
|
||||
- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md)
|
||||
- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md)
|
||||
- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md)
|
||||
- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md)
|
||||
- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于把 `M0 ~ M7` 各阶段的入口条件、核心交付、退出条件与回归焦点固定下来,避免后续出现“任务勾完了,但阶段是否真的可进入下一步没有统一标准”的问题。
|
||||
|
||||
从本文件开始,后续每一阶段都需要按“入口满足 -> 交付完成 -> 验收通过 -> 留存证据”的顺序推进。
|
||||
|
||||
## 2. 阶段推进总规则
|
||||
|
||||
1. 未满足上一阶段退出条件前,不进入下一阶段主线编码。
|
||||
2. 每一阶段至少保留一份可复查的证据,证据可以是文档、脚本、测试结果或回归记录。
|
||||
3. 所有阶段都必须持续对齐当前冻结基线:
|
||||
- 历史基线 `6` 个挂载面
|
||||
- 本轮 active rewrite target `5` 个挂载面
|
||||
- `96` 条路由
|
||||
- `12` 个模块
|
||||
- `6` 条 SSE 接口
|
||||
- `6` 个 `/generated-*` 静态资源前缀
|
||||
- 前端直接依赖的响应头、envelope 与鉴权错误格式
|
||||
4. 任一阶段若引入新的协议差异,必须先补 contract 文档或迁移说明,再允许继续编码。
|
||||
|
||||
## 3. 分阶段验收矩阵
|
||||
|
||||
| 阶段 | 入口条件 | 核心交付 | 退出条件 | 回归焦点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `M0` 冻结能力与边界 | 已完成当前 Node 后端摸底;已明确目标架构为 `SpacetimeDB + Axum + 阿里云 OSS` | 冻结能力基线、路由矩阵、模块归属、SSE、静态资源前缀、前端响应契约、仓库边界决议、阶段验收矩阵 | `6` 个挂载面、`96` 条路由、`12` 个模块、`6` 条 SSE、`6` 个静态资源前缀全部形成书面基线;`server-rs/`、`server-node/`、Axum 边界、副作用收口原则全部冻结 | 文档口径一致性;前端 contract 依赖项是否被遗漏;迁移阶段是否还存在多套边界说法 |
|
||||
| `M1` Rust 工作区与 Axum 基础设施 | `M0` 全部退出条件满足 | `server-rs/` workspace、`crates/api-server`、`crates/spacetime-module`、独立模块 crates、统一配置、日志、request id、中间件、response envelope、`/healthz`、开发脚本 | Axum 可独立启动;`/healthz` 与当前工程兼容;`x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 行为稳定;workspace 完整编译通过;主工程与模块 crate 引用边界稳定 | 基础头部兼容;健康检查兼容;目录结构与 crate 归属是否偏离 `M0` 决议 |
|
||||
| `M2` 鉴权、会话、JWT 与 refresh cookie | `M1` 已可稳定启动;Axum 中间件与配置链可用 | 身份表、会话表、JWT claims、refresh cookie、密码登录、手机验证码登录、微信登录、OIDC 透传、旧鉴权接口兼容 | 密码登录、refresh cookie、手机验证码、微信登录主链可用;旧鉴权接口 contract 回归通过;SpacetimeDB 可识别 Axum 签发身份 | Cookie 与 JWT 兼容;`CAPTCHA_REQUIRED` 与 `details.captchaChallenge` 是否保持;登录态吊销与刷新是否稳定 |
|
||||
| `M3` runtime snapshot / settings / profile | `M2` 鉴权稳定;用户身份可透传到 SpacetimeDB | `runtime_snapshot`、`runtime_setting`、profile 相关主表与 facade;存档、设置、浏览历史、save archive 兼容接口 | 登录用户可正常保存、读取、删除存档;profile dashboard / browse history / save archive 行为一致;前端恢复流程可直接跑通 | 快照恢复准确性;兼容路径与主路径是否返回一致;历史记录排序与去重逻辑 |
|
||||
| `M4` story action 与 gameplay reducer | `M3` 快照与用户状态主链稳定 | story / combat / inventory / npc / quest / progression / runtime-item 表与 reducer;story 兼容接口与 view model | 前端 story 主循环可用;`story state` 恢复链可用;NPC / quest / treasure / combat 主循环行为不回退;旧 Node story route 回归平移完成 | `RuntimeStoryActionResponse` 结构;战斗与奖励联动;状态投影是否与旧前端恢复逻辑一致 |
|
||||
| `M5` custom world / gallery / agent | `M4` story 与 runtime 真相源已稳定;SSE facade 可持续输出 | custom world 主表、agent 会话拆表、传统问答流、library / gallery、agent 消息与操作、LLM/图片生成编排 | 传统 custom world 主链可用;library / gallery 主链可用;agent 主链可用;会话不再依赖单大 JSON 体 | SSE 事件格式;卡片、消息、操作状态一致性;世界草稿编译与发布链是否可回放 |
|
||||
| `M6` assets / OSS | `M5` 世界与角色主链稳定;Axum 应用层可承接外部副作用 | OSS 对象键规范、上传签名、对象确认、资产任务表、角色/场景/Qwen 资产迁移、旧静态路径兼容 | 所有新生成资产写入 OSS;前端仍能通过旧路径习惯访问资源;资产任务状态可查询 | `/generated-*` 路径兼容;OSS 元数据与对象绑定关系;资产任务链状态一致性 |
|
||||
| `M7` 联调、回归、部署与切流 | `M6` 已具备主链闭环;双栈对照条件具备 | 测试体系、部署方案、观测能力、灰度切流方案、回退方案、对比脚本与 smoke 清单 | 全链路 smoke 通过;主流程回归通过;关键 SSE 联调通过;可在灰度环境切流并可回退 | 双跑窗口稳定性;API 对比结果;切流开关、回退开关、观测告警是否齐备 |
|
||||
|
||||
## 4. M0 冻结项专用验收清单
|
||||
|
||||
只有以下项目全部满足,`M0` 才算真正完成:
|
||||
|
||||
1. 已产出以下冻结文档:
|
||||
- [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md)
|
||||
- [M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md)
|
||||
- [M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md)
|
||||
- [M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md)
|
||||
- [M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md)
|
||||
- [M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md)
|
||||
- [M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md)
|
||||
- [M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md](./M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md)
|
||||
2. 已书面冻结以下核心数字:
|
||||
- 挂载面:`6`
|
||||
- 路由:`96`
|
||||
- 模块:`12`
|
||||
- SSE:`6`
|
||||
- 静态资源前缀:`6`
|
||||
3. 已书面冻结以下边界决议:
|
||||
- 新 Rust 后端固定为仓库根目录 `server-rs/`
|
||||
- 迁移期保留 `server-node/`
|
||||
- 前端 `M0 ~ M6` 期间只访问 Axum
|
||||
- 外部副作用统一收口在 Axum
|
||||
- `server-rs/` 内部采用 `crates/*` 多 crate 组织
|
||||
- `editor` 已于 `2026-04-21` 退出本轮 Rust 重写范围
|
||||
4. `M1` 以后任何任务引用路由、模块、SSE、静态资源与响应契约时,都必须能追溯到本阶段产出的冻结文档。
|
||||
|
||||
## 5. 跨阶段回归维度
|
||||
|
||||
无论执行到哪个阶段,都要持续检查以下维度:
|
||||
|
||||
| 维度 | 必查内容 | 最晚必须固化的证据 |
|
||||
| --- | --- | --- |
|
||||
| 路由兼容 | 旧路由是否已有新实现或明确替代路径 | 路由迁移矩阵、API 对比脚本、contract 回归记录 |
|
||||
| SSE 兼容 | 事件名、事件顺序、结束事件、错误事件是否保持兼容 | SSE 基线文档、联调记录、smoke 结果 |
|
||||
| 静态资源兼容 | `/generated-*` 是否可继续访问,是否正确指向 OSS/CDN | 静态资源前缀基线、路径兼容测试记录 |
|
||||
| 鉴权兼容 | JWT、refresh cookie、验证码、微信登录、风控错误是否保持兼容 | 鉴权接口回归记录、claims 设计文档、集成测试 |
|
||||
| 前端 contract | 响应头、envelope、错误结构是否稳定 | response contract 基线、接口测试、前端联调记录 |
|
||||
| 切流回退 | 双栈是否可对照,是否具备回退能力 | `M7` 对比脚本、灰度清单、回退方案 |
|
||||
|
||||
## 6. 阶段证据留存要求
|
||||
|
||||
每个阶段完成时,至少要补齐以下其中两类证据:
|
||||
|
||||
1. 文档:
|
||||
- 更新任务清单勾选状态
|
||||
- 更新设计文档或阶段落地记录
|
||||
2. 测试或脚本:
|
||||
- 新增或更新 smoke / contract / integration 测试
|
||||
- 新增对比脚本、发布脚本或回归脚本
|
||||
3. 结果记录:
|
||||
- 编码检查结果
|
||||
- 关键命令执行结果
|
||||
- 联调、回归、灰度演练结果
|
||||
|
||||
如果阶段只完成了编码、但没有文档和证据留存,则该阶段不能视为完成。
|
||||
@@ -0,0 +1,281 @@
|
||||
# M0:仓库边界决议
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](../docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
|
||||
- [00_MASTER_TASKLIST.md](./00_MASTER_TASKLIST.md)
|
||||
- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md)
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于持续冻结 `M0` 中与仓库边界直接相关的决策,避免进入 `M1` 后再反复改目录、改职责口径。
|
||||
|
||||
当前已确认的事项会持续在这份文档上追加维护,后续若再有新的边界冻结结论,也统一收口到这里。
|
||||
|
||||
## 2. 边界决议状态
|
||||
|
||||
| 事项 | 当前状态 | 当前结论 |
|
||||
| --- | --- | --- |
|
||||
| Rust 后端新目录名与根目录落位方案 | 已确认 | 新 Rust 后端固定为仓库根目录下的 `server-rs/`,与 `server-node/` 同级。 |
|
||||
| 旧 `server-node/` 在迁移期继续保留,不提前删除 | 已确认 | `server-node/` 在 `M0 ~ M6` 期间持续保留,直到 `M7` 切流与回退验证完成后再评估清理。 |
|
||||
| 前端第一阶段仍然只访问 Axum,不直连 SpacetimeDB | 已确认 | `M0 ~ M6` 前端统一只访问 Axum 暴露的 `/api/*`、`/healthz`、SSE 与静态资源兼容层,不新增直连 SpacetimeDB 原生协议路径。 |
|
||||
| 外部副作用统一收口在 Axum,不放进 SpacetimeDB 模块 | 已确认 | OSS、LLM、短信、微信 OAuth、本地文件系统等外部副作用统一落在 Axum/application/infra,不进入 SpacetimeDB reducer/module。 |
|
||||
| `server-rs/` 内部采用多 crate 组织,由主工程 crate 统一引用模块 crate | 已确认 | `server-rs/` 采用 `crates/*` 工作区结构,`crates/api-server` 与 `crates/spacetime-module` 作为主工程 crate,独立模块以 `crates/module-*` 形式被主工程 crate 引用。 |
|
||||
| `editor` 为遗留无用模块,不纳入 `server-rs` 本轮重写范围 | 已确认 | `server-node/src/modules/editor` 与 `/api/editor/*` 仅作为历史基线保留对照;自 `2026-04-21` 起退出本轮 Rust 后端重写范围。 |
|
||||
|
||||
## 3. 已确认决议一:`server-rs/` 固定落在仓库根目录
|
||||
|
||||
### 3.1 决议内容
|
||||
|
||||
本次重写固定采用以下仓库落位:
|
||||
|
||||
1. 新后端目录名固定为 `server-rs/`
|
||||
2. 目录位置固定在仓库根目录
|
||||
3. 与以下目录保持同级:
|
||||
- `server-node/`
|
||||
- `src/`
|
||||
- `docs/`
|
||||
- `packages/`
|
||||
|
||||
目标形态:
|
||||
|
||||
```text
|
||||
Genarrative/
|
||||
├─ server-node/
|
||||
├─ server-rs/
|
||||
├─ src/
|
||||
├─ packages/
|
||||
├─ docs/
|
||||
└─ backend-rewrite-tasklist/
|
||||
```
|
||||
|
||||
### 3.2 不采用的落位方案
|
||||
|
||||
以下方案当前明确不采用:
|
||||
|
||||
1. 不放进 `server-node/` 子目录中做“Node + Rust 混编后端”。
|
||||
2. 不放进 `packages/`,避免被前端 package/workspace 语义误导。
|
||||
3. 不使用过于泛化的根目录名如 `server/`、`backend/`,避免和当前 `server-node/` 职责混淆。
|
||||
|
||||
### 3.3 这样落位的原因
|
||||
|
||||
1. 与当前重写设计文档、任务清单、后续 `M1` 多 crate 规划保持一致。
|
||||
2. 允许 `server-node/` 与 `server-rs/` 在迁移期并行存在,便于逐阶段切流。
|
||||
3. 让 Rust 工作区边界清晰,不污染现有前端 `src/`、`packages/`、Vite 工具链。
|
||||
4. 后续新增 `server-rs/scripts/*`、`Cargo.toml`、`crates/*` 时路径最直接,不需要额外中间层。
|
||||
|
||||
### 3.4 对后续任务的直接约束
|
||||
|
||||
从这一条决议开始,后续任务必须统一按以下路径落位:
|
||||
|
||||
1. `M1` 的工作区初始化在 `server-rs/`
|
||||
2. Axum 主工程 crate 在 `server-rs/crates/api-server`
|
||||
3. SpacetimeDB 主工程 crate 在 `server-rs/crates/spacetime-module`
|
||||
4. 独立模块 crate 在 `server-rs/crates/module-*`
|
||||
5. 相关脚本在 `server-rs/scripts/`
|
||||
|
||||
## 4. 本条任务完成定义
|
||||
|
||||
当以下条件成立时,这一条边界任务视为完成:
|
||||
|
||||
1. 新 Rust 后端目录名已经书面固定为 `server-rs/`
|
||||
2. 目录位置已经书面固定为仓库根目录
|
||||
3. 后续 `M1` 的工作区初始化不会再出现 `server-rs/`、`backend-rs/`、`server/` 等多套候选名并存
|
||||
|
||||
## 5. 已确认决议二:迁移期保留 `server-node/`
|
||||
|
||||
### 5.1 决议内容
|
||||
|
||||
在本次重写迁移期内,旧 `server-node/` 固定继续保留,不提前删除、不整体挪位、不提前做破坏性收缩。
|
||||
|
||||
保留周期固定为:
|
||||
|
||||
1. `M0` 到 `M6` 全阶段
|
||||
2. 至少持续到 `M7` 的以下条件全部满足之后,才允许评估清理:
|
||||
- 新后端已切流
|
||||
- 旧接口 contract 回归通过
|
||||
- 关键主链 smoke 通过
|
||||
- 已具备明确回退方案
|
||||
|
||||
### 5.2 保留它的原因
|
||||
|
||||
1. 旧 `server-node/` 是当前 `96` 条路由、`6` 个挂载面、`12` 个模块的真实对照实现。
|
||||
2. 前面已经冻结的路由矩阵、模块迁移清单、SSE 协议、静态资源前缀,都需要它作为回归对照源。
|
||||
3. 如果在 `M1 ~ M6` 提前删除旧实现,就会失去最可靠的回退锚点与 diff 基准。
|
||||
|
||||
### 5.3 迁移期允许做什么
|
||||
|
||||
迁移期内允许:
|
||||
|
||||
1. 在 `server-rs/` 中逐步补等价实现。
|
||||
2. 在文档和 manifest 中继续引用 `server-node/` 作为当前系统基线。
|
||||
3. 在必要时从 `server-node/` 补测试样例、补协议对照、补回归夹具。
|
||||
|
||||
迁移期内不允许:
|
||||
|
||||
1. 提前整体删除 `server-node/`
|
||||
2. 把 `server-node/` 改成只剩空壳目录
|
||||
3. 在还没切流前,把旧服务关键实现批量迁走导致无法对照
|
||||
|
||||
### 5.4 对后续任务的直接约束
|
||||
|
||||
从这一条决议开始,后续任务必须遵守:
|
||||
|
||||
1. `M1` 搭建 `server-rs/` 时,不改动 `server-node/` 的存在性。
|
||||
2. `M2 ~ M6` 迁移功能时,旧 `server-node/` 继续作为验收基线与回退锚点。
|
||||
3. 真正评估清理旧 Node 后端的动作,只能放到 `M7` 切流完成之后。
|
||||
|
||||
## 6. 已确认决议三:前端第一阶段只访问 Axum
|
||||
|
||||
### 6.1 决议内容
|
||||
|
||||
在 `M0 ~ M6` 迁移期内,前端访问新后端的唯一入口固定为 Axum。
|
||||
|
||||
第一阶段允许前端继续访问的面固定为:
|
||||
|
||||
1. `/api/*`
|
||||
2. `/healthz`
|
||||
3. 当前已冻结的 SSE 路由
|
||||
4. 当前已冻结的 `/generated-*` 静态资源兼容前缀
|
||||
|
||||
第一阶段明确不做的事:
|
||||
|
||||
1. 不让 Web 前端直接接 SpacetimeDB 原生 HTTP 接口。
|
||||
2. 不让 Web 前端直接接 SpacetimeDB 订阅协议。
|
||||
3. 不要求前端新增一套“Axum + SpacetimeDB 双后端并行直连”调用模式。
|
||||
|
||||
### 6.2 这样决议的原因
|
||||
|
||||
1. 当前前端已经直接依赖现有 `/api/*` 路由、response envelope、SSE、`/generated-*` 路径习惯。
|
||||
2. 如果在第一阶段就让前端同时认识 Axum 与 SpacetimeDB,会把迁移面从“后端平移”扩大成“前后端协议双重重写”。
|
||||
3. Axum 需要承担统一鉴权、cookie、JWT、OSS 签名、错误格式与 contract 兼容职责,这些都不应分散到前端直连多个后端协议。
|
||||
|
||||
### 6.3 对后续任务的直接约束
|
||||
|
||||
从这一条决议开始,后续任务必须遵守:
|
||||
|
||||
1. `M1 ~ M2` 的 Axum 中间件与鉴权必须先跑通,再谈前端联调。
|
||||
2. `M3 ~ M6` 新增的 SpacetimeDB reducer/view 先通过 Axum facade 暴露,不直接要求前端改成原生 SpacetimeDB 客户端。
|
||||
3. 若后续要让前端直连 SpacetimeDB,只能作为第二阶段优化事项,不能混入当前重写主链。
|
||||
|
||||
## 7. 已确认决议四:外部副作用统一收口在 Axum
|
||||
|
||||
### 7.1 决议内容
|
||||
|
||||
本次重写固定采用以下边界:
|
||||
|
||||
1. `SpacetimeDB` 只负责状态、规则、reducer、view、订阅读模型。
|
||||
2. `Axum/application/infra` 统一负责所有外部副作用。
|
||||
|
||||
固定收口到 Axum 的外部副作用包括:
|
||||
|
||||
1. 阿里云 OSS 上传、下载、签名、直传凭证
|
||||
2. DashScope / Ark / 其他 LLM 请求
|
||||
3. 微信 OAuth
|
||||
4. 手机验证码短信发送与校验编排
|
||||
5. 本地文件系统读写
|
||||
|
||||
### 7.2 明确不允许放进 SpacetimeDB 的内容
|
||||
|
||||
以下能力当前明确禁止进入 `spacetime-module/`:
|
||||
|
||||
1. 直接发 HTTP 请求给第三方供应商
|
||||
2. 直接访问 OSS SDK
|
||||
3. 直接读写本地磁盘
|
||||
4. 直接处理 Cookie、回调跳转、multipart 上传
|
||||
5. 直接承担供应商重试、熔断、超时与日志策略
|
||||
|
||||
### 7.3 这样决议的原因
|
||||
|
||||
1. 这些能力都强依赖 HTTP 头、Cookie、SDK、签名、超时与日志,不适合绑进 SpacetimeDB 模块发布周期。
|
||||
2. 当前前端 contract、鉴权、SSE、静态资源兼容都要求一个稳定的 HTTP 边界层,Axum 更适合承担这个角色。
|
||||
3. 把副作用统一收口到 Axum,才能让 SpacetimeDB 保持“状态机真相源”的纯度。
|
||||
|
||||
### 7.4 对后续任务的直接约束
|
||||
|
||||
从这一条决议开始,后续任务必须遵守:
|
||||
|
||||
1. `M1` crate 设计时,`platform-oss`、`platform-llm`、`platform-auth` 固定属于 Axum / 模块应用层一侧。
|
||||
2. `M2 ~ M6` 设计 reducer 时,只写状态变更,不直接发外部请求。
|
||||
3. 若确实需要异步副作用,也必须由 Axum worker 或应用层作业执行,再把结果回写 SpacetimeDB。
|
||||
|
||||
## 8. 已确认决议五:`server-rs/` 内部采用多 crate 组织
|
||||
|
||||
### 8.1 决议内容
|
||||
|
||||
从当前版本开始,`server-rs/` 内部结构固定采用:
|
||||
|
||||
1. `crates/*`:统一收口主工程 crate、独立模块 crate 与共享 crate
|
||||
2. `scripts/*`:开发、发布、回归脚本
|
||||
|
||||
主工程 crate 固定包含:
|
||||
|
||||
1. `crates/api-server`
|
||||
2. `crates/spacetime-module`
|
||||
|
||||
独立模块 crate 固定按“每个独立模块一个 crate”推进,至少覆盖:
|
||||
|
||||
1. `crates/module-auth`
|
||||
2. `crates/module-runtime`
|
||||
3. `crates/module-story`
|
||||
4. `crates/module-combat`
|
||||
5. `crates/module-inventory`
|
||||
6. `crates/module-npc`
|
||||
7. `crates/module-progression`
|
||||
8. `crates/module-quest`
|
||||
9. `crates/module-runtime-item`
|
||||
10. `crates/module-custom-world`
|
||||
11. `crates/module-assets`
|
||||
12. `crates/module-ai`
|
||||
|
||||
跨模块共享 crate 固定包含:
|
||||
|
||||
1. `crates/shared-contracts`
|
||||
2. `crates/shared-kernel`
|
||||
3. `crates/platform-auth`
|
||||
4. `crates/platform-oss`
|
||||
5. `crates/platform-llm`
|
||||
6. `crates/spacetime-client`
|
||||
7. `crates/tests-support`
|
||||
|
||||
### 8.2 这样决议的原因
|
||||
|
||||
1. 用户已经明确要求后端采用 Rust workspace 下的多 crate 模式,独立模块不能继续堆回单个技术层大包。
|
||||
2. 当前后端已有 `12` 个内部模块边界,多 crate 方案更容易保持一一映射与独立演进。
|
||||
3. `crates/api-server` 与 `crates/spacetime-module` 只做组合与发布,更符合“主工程 crate 引用模块 crate”的组织方式。
|
||||
|
||||
### 8.3 对后续任务的直接约束
|
||||
|
||||
从这一条决议开始,后续任务必须遵守:
|
||||
|
||||
1. `M1` 及后续目录任务统一按 `crates/*` 执行,不再保留 `apps/*` 与 `packages/*` 并行规划。
|
||||
2. 每个业务模块默认先有自己的 workspace crate,再由主工程 crate 引用。
|
||||
3. 只有共享 contract、共享领域内核、平台适配、SpacetimeDB client 这类跨模块能力,才允许使用共享 crate,而不是业务模块混装。
|
||||
|
||||
## 9. 已确认决议六:`editor` 退出本轮 Rust 重写范围
|
||||
|
||||
### 9.1 决议内容
|
||||
|
||||
`editor` 在当前 Node 后端中确实存在真实模块与真实挂载面,但已于 `2026-04-21` 被确认为遗留无用模块,不再纳入本轮 `server-rs/` 重写主链。
|
||||
|
||||
当前固定口径为:
|
||||
|
||||
1. 历史基线继续保留 `server-node/src/modules/editor` 与 `/api/editor/*` 的存在事实。
|
||||
2. `server-rs/` 不再保留 `crates/module-editor`。
|
||||
3. `M1 ~ M6` 的主线任务、阶段验收与 crate 规划,不再把 `editor` 计入 active rewrite scope。
|
||||
|
||||
### 9.2 这样决议的原因
|
||||
|
||||
1. 用户已明确确认 `editor` 为遗留无用模块,应从本轮重写目标中剔除。
|
||||
2. 保留历史事实有助于后续对照清理,不会把“旧系统曾存在该模块”的信息抹掉。
|
||||
3. 从当前阶段开始继续为 `editor` 预留 Rust crate,只会增加主线迁移噪音与工程负担。
|
||||
|
||||
### 9.3 对后续任务的直接约束
|
||||
|
||||
从这一条决议开始,后续任务必须遵守:
|
||||
|
||||
1. 不再为 `editor` 创建或维护 `server-rs` 下的新 crate、Axum 路由树与迁移验收项。
|
||||
2. 所有涉及挂载面、模块、路由总量的文档,都要区分“历史基线”与“本轮 active rewrite target”。
|
||||
3. 若未来仍要清理 `editor`,应在 `server-node/` 遗留链路依赖核对完成后单独立项。
|
||||
249
backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md
Normal file
249
backend-rewrite-tasklist/M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# M0:旧接口到新实现路由迁移矩阵
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
|
||||
- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json)
|
||||
- [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md)
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于完成 `M0` 的第二条任务:
|
||||
|
||||
- 整理当前后端 `96` 条路由并生成一份“旧接口 -> 新实现”映射表
|
||||
|
||||
这里的“新实现”不是指最终文件路径,而是指第一阶段重写时每条旧接口在新架构中的落点:
|
||||
|
||||
1. 哪条 Axum 路由负责对外兼容
|
||||
2. 哪层 application service 负责编排
|
||||
3. 哪些状态进入 SpacetimeDB
|
||||
4. 哪些二进制对象进入 OSS
|
||||
|
||||
## 2. 映射代码说明
|
||||
|
||||
为避免 `96` 条路由的映射表过长,本表使用以下“新实现归属代码”:
|
||||
|
||||
| 代码 | 新实现归属 |
|
||||
| --- | --- |
|
||||
| `A-HEALTH` | `Axum health route` |
|
||||
| `A-AUTH` | `Axum auth routes + auth-service + SpacetimeDB auth tables` |
|
||||
| `A-EDITOR` | `历史 Node editor 路由(遗留保留,不迁移到 server-rs)` |
|
||||
| `A-OSS` | `Axum assets routes + application::assets + oss-service + SpacetimeDB asset metadata` |
|
||||
| `A-LLM` | `Axum llm proxy/service` |
|
||||
| `A-RUNTIME` | `Axum runtime facade + SpacetimeDB runtime reducers/views` |
|
||||
| `A-STORY` | `Axum story facade + SpacetimeDB gameplay reducers/views` |
|
||||
| `A-CHAT` | `Axum SSE facade + llm-service + SpacetimeDB story/npc state` |
|
||||
| `A-CW` | `Axum custom-world facade + llm-service + SpacetimeDB custom_world reducers/views` |
|
||||
| `A-AGENT` | `Axum custom-world-agent facade + llm-service + oss-service + SpacetimeDB agent tables` |
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 第一阶段默认保留旧路径,不主动改前端请求地址。
|
||||
2. 兼容路径与主路径在新后端中应尽量共用同一 handler。
|
||||
3. 所有 `stream` 接口第一阶段继续用 Axum SSE,不强推改成 WebSocket。
|
||||
4. 自 `2026-04-21` 起,`editor` 路由仅保留历史对照,不纳入本轮 Rust 重写范围。
|
||||
|
||||
## 3. 总量校验
|
||||
|
||||
| 项目 | 数量 |
|
||||
| --- | --- |
|
||||
| 挂载面 | `6` |
|
||||
| 总路由数 | `96` |
|
||||
| `assets` | `14` |
|
||||
| `auth` | `17` |
|
||||
| `editor` | `3` |
|
||||
| `runtime-main` | `59` |
|
||||
| `runtime-story-action` | `2` |
|
||||
| `health` | `1` |
|
||||
|
||||
补充说明:
|
||||
|
||||
1. 上表总量仍然是当前 Node 后端历史基线。
|
||||
2. 其中 `editor` 的 `3` 条路由继续计入历史对照,但不计入本轮 `server-rs` active rewrite target。
|
||||
|
||||
## 4. `assets` 路由映射(14 条)
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `assets.characterAnimationGenerate` | `POST /api/assets/character-animation/generate` | `A-OSS` | 保留原路径;Axum 创建 `asset_job`,外部生成结果写 OSS,任务状态进 SpacetimeDB。 |
|
||||
| `assets.characterAnimationImportVideo` | `POST /api/assets/character-animation/import-video` | `A-OSS` | 保留原路径;视频参考素材由 Axum 上传 OSS,并写任务/对象元数据。 |
|
||||
| `assets.characterAnimationJobGet` | `GET /api/assets/character-animation/jobs/:taskId` | `A-OSS` | 保留原路径;查询改读 SpacetimeDB `asset_job view`。 |
|
||||
| `assets.characterAnimationPublish` | `POST /api/assets/character-animation/publish` | `A-OSS` | 保留原路径;发布动作改为“绑定 OSS 对象到业务实体 + 回写元数据”。 |
|
||||
| `assets.characterAnimationTemplatesList` | `GET /api/assets/character-animation/templates` | `A-OSS` | 保留原路径;模板清单先由 Axum 提供,后续再视情况对象化。 |
|
||||
| `assets.characterVisualGenerate` | `POST /api/assets/character-visual/generate` | `A-OSS` | 保留原路径;角色主形象候选生成改为 Axum 编排 + OSS 入库。 |
|
||||
| `assets.characterVisualJobGet` | `GET /api/assets/character-visual/jobs/:taskId` | `A-OSS` | 保留原路径;任务状态改读 SpacetimeDB。 |
|
||||
| `assets.characterVisualPublish` | `POST /api/assets/character-visual/publish` | `A-OSS` | 保留原路径;发布改为对象绑定,不再依赖本地 `public/generated-*` 真相。 |
|
||||
| `assets.characterWorkflowCacheSave` | `POST /api/assets/character-workflow-cache` | `A-OSS` | 保留原路径;工作流缓存改写 OSS/对象存储,索引进 SpacetimeDB。 |
|
||||
| `assets.characterWorkflowCacheGet` | `GET /api/assets/character-workflow-cache/:characterId` | `A-OSS` | 保留原路径;按角色查缓存索引,再返回对象内容或签名 URL。 |
|
||||
| `assets.qwenSpriteFrameRepairGenerate` | `POST /api/assets/qwen-sprite/frame-repair` | `A-OSS` | 保留原路径;Qwen 修帧结果统一入 OSS,状态进 SpacetimeDB。 |
|
||||
| `assets.qwenSpriteMasterGenerate` | `POST /api/assets/qwen-sprite/master` | `A-OSS` | 保留原路径;主图生成改为 Axum 编排。 |
|
||||
| `assets.qwenSpriteAssetSave` | `POST /api/assets/qwen-sprite/save` | `A-OSS` | 保留原路径;保存动作改为持久化对象元数据与引用关系。 |
|
||||
| `assets.qwenSpriteSheetGenerate` | `POST /api/assets/qwen-sprite/sheet` | `A-OSS` | 保留原路径;整表生成链路保留,底层切换为 OSS + SpacetimeDB。 |
|
||||
|
||||
## 5. `auth` 路由映射(17 条)
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `auth.auditLogs` | `GET /api/auth/audit-logs` | `A-AUTH` | 保留原路径;从 SpacetimeDB `auth_audit_log view` 返回。 |
|
||||
| `auth.entry` | `POST /api/auth/entry` | `A-AUTH` | 保留原路径;密码登录与自动注册由 Axum 完成,再写 auth 表。 |
|
||||
| `auth.loginOptions` | `GET /api/auth/login-options` | `A-AUTH` | 保留原路径;由 Axum 直接返回登录方式配置。 |
|
||||
| `auth.logout` | `POST /api/auth/logout` | `A-AUTH` | 保留原路径;Axum 吊销 refresh session 并清理 cookie。 |
|
||||
| `auth.logoutAll` | `POST /api/auth/logout-all` | `A-AUTH` | 保留原路径;批量吊销用户全部 session。 |
|
||||
| `auth.me` | `GET /api/auth/me` | `A-AUTH` | 保留原路径;由 Axum 校验 JWT 后查询用户读模型。 |
|
||||
| `auth.phoneChange` | `POST /api/auth/phone/change` | `A-AUTH` | 保留原路径;短信校验在 Axum,绑定结果写 SpacetimeDB。 |
|
||||
| `auth.phoneLogin` | `POST /api/auth/phone/login` | `A-AUTH` | 保留原路径;验证码校验成功后创建/恢复账号与 session。 |
|
||||
| `auth.phoneSendCode` | `POST /api/auth/phone/send-code` | `A-AUTH` | 保留原路径;阿里云短信发送适配收口到 Axum。 |
|
||||
| `auth.refresh` | `POST /api/auth/refresh` | `A-AUTH` | 保留原路径;沿用 refresh cookie -> access token 刷新模型。 |
|
||||
| `auth.riskBlocks` | `GET /api/auth/risk-blocks` | `A-AUTH` | 保留原路径;改读风控封禁表/视图。 |
|
||||
| `auth.riskBlocksLift` | `POST /api/auth/risk-blocks/:scopeType/lift` | `A-AUTH` | 保留原路径;解除请求由 Axum 执行校验并写状态。 |
|
||||
| `auth.sessions` | `GET /api/auth/sessions` | `A-AUTH` | 保留原路径;会话列表改读 SpacetimeDB `refresh_session view`。 |
|
||||
| `auth.sessionRevoke` | `POST /api/auth/sessions/:sessionId/revoke` | `A-AUTH` | 保留原路径;会话吊销改写 `refresh_session` 状态。 |
|
||||
| `auth.wechatBindPhone` | `POST /api/auth/wechat/bind-phone` | `A-AUTH` | 保留原路径;微信身份补绑手机号逻辑迁到 Axum。 |
|
||||
| `auth.wechatCallback` | `GET /api/auth/wechat/callback` | `A-AUTH` | 保留原路径与 redirect 语义;微信 code 交换由 Axum 处理。 |
|
||||
| `auth.wechatStart` | `GET /api/auth/wechat/start` | `A-AUTH` | 保留原路径;授权 URL 由 Axum 按设备场景生成。 |
|
||||
|
||||
## 6. `editor` 路由映射(3 条,历史遗留)
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `editor.catalogItems` | `GET /api/editor/catalog/items` | `A-EDITOR` | 保留在 `server-node/` 遗留链路,仅作为历史对照;不迁移到 `server-rs`。 |
|
||||
| `editor.resourceRead` | `GET /api/editor/json/:resourceId` | `A-EDITOR` | 保留在 `server-node/` 遗留链路,仅作为历史对照;不迁移到 `server-rs`。 |
|
||||
| `editor.resourceWrite` | `POST /api/editor/json/:resourceId` | `A-EDITOR` | 保留在 `server-node/` 遗留链路,仅作为历史对照;不迁移到 `server-rs`。 |
|
||||
|
||||
## 7. `runtime-main` 路由映射(59 条)
|
||||
|
||||
### 7.1 custom world 资源与实体生成
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.customWorldCoverImage` | `POST /api/custom-world/cover-image` | `A-CW` | 保留原路径;封面图生成由 Axum 编排,产物进 OSS,绑定关系进 SpacetimeDB。 |
|
||||
| `runtime.customWorldCoverUpload` | `POST /api/custom-world/cover-upload` | `A-CW` | 保留原路径;上传改为 OSS 直传或 Axum 中转上传。 |
|
||||
| `runtime.customWorldEntity.primary` | `POST /api/custom-world/entity` | `A-CW` | 保留原路径;实体生成由 Axum 调 LLM,再写 custom world 表。 |
|
||||
| `runtime.customWorldSceneImage` | `POST /api/custom-world/scene-image` | `A-CW` | 保留原路径;场景图生成由 Axum + OSS 完成。 |
|
||||
| `runtime.customWorldSceneNpc.primary` | `POST /api/custom-world/scene-npc` | `A-CW` | 保留原路径;场景 NPC 生成结果写 custom world / npc 相关表。 |
|
||||
|
||||
### 7.2 LLM 透传
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.llmChatCompletionsProxy` | `POST /api/llm/chat/completions` | `A-LLM` | 保留原路径;继续由 Axum 承接代理,不进入 SpacetimeDB。 |
|
||||
|
||||
### 7.3 profile 主路径
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.profileBrowseHistoryDelete.primary` | `DELETE /api/profile/browse-history` | `A-RUNTIME` | 保留原路径;清理动作改为 reducer 写 `user_browse_history`。 |
|
||||
| `runtime.profileBrowseHistoryGet.primary` | `GET /api/profile/browse-history` | `A-RUNTIME` | 保留原路径;历史记录改读 browse history view。 |
|
||||
| `runtime.profileBrowseHistoryPost.primary` | `POST /api/profile/browse-history` | `A-RUNTIME` | 保留原路径;批量写入改为 Axum -> reducer。 |
|
||||
| `runtime.profileDashboard.primary` | `GET /api/profile/dashboard` | `A-RUNTIME` | 保留原路径;个人主页汇总改读 dashboard view。 |
|
||||
| `runtime.profilePlayStats.primary` | `GET /api/profile/play-stats` | `A-RUNTIME` | 保留原路径;统计数据改读 projection。 |
|
||||
| `runtime.profileSaveArchivesList.primary` | `GET /api/profile/save-archives` | `A-RUNTIME` | 保留原路径;存档摘要改读 save archive view。 |
|
||||
| `runtime.profileSaveArchivesResume.primary` | `POST /api/profile/save-archives/:worldKey` | `A-RUNTIME` | 保留原路径;恢复动作改读 `profile_save_archive` 后重建兼容快照。 |
|
||||
| `runtime.profileWalletLedger.primary` | `GET /api/profile/wallet-ledger` | `A-RUNTIME` | 保留原路径;资产流水改读 ledger view。 |
|
||||
|
||||
### 7.4 runtime 聊天与流式对话
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.characterReplyStream` | `POST /api/runtime/chat/character/reply/stream` | `A-CHAT` | 保留 SSE contract;Axum 流式产出,状态写 story/session 表。 |
|
||||
| `runtime.characterSuggestions` | `POST /api/runtime/chat/character/suggestions` | `A-CHAT` | 保留原路径;由 Axum 生成建议语并按需写会话状态。 |
|
||||
| `runtime.characterSummary` | `POST /api/runtime/chat/character/summary` | `A-CHAT` | 保留原路径;摘要生成留在 Axum,摘要索引可回写 SpacetimeDB。 |
|
||||
| `runtime.npcDialogueStream` | `POST /api/runtime/chat/npc/dialogue/stream` | `A-CHAT` | 保留 SSE contract;NPC 对话状态迁到 SpacetimeDB。 |
|
||||
| `runtime.npcRecruitStream` | `POST /api/runtime/chat/npc/recruit/stream` | `A-CHAT` | 保留 SSE contract;招募对话与状态变化统一进入新状态层。 |
|
||||
| `runtime.npcTurnStream` | `POST /api/runtime/chat/npc/turn/stream` | `A-CHAT` | 保留 SSE contract;单回合发言的判定与状态回写统一收口。 |
|
||||
|
||||
### 7.5 custom world gallery / library / sessions / works
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.customWorldGalleryList` | `GET /api/runtime/custom-world-gallery` | `A-CW` | 保留原路径;公开画廊改读 `custom_world_gallery view`。 |
|
||||
| `runtime.customWorldGalleryDetail` | `GET /api/runtime/custom-world-gallery/:ownerUserId/:profileId` | `A-CW` | 保留原路径;详情改读 gallery detail view。 |
|
||||
| `runtime.customWorldLibraryList` | `GET /api/runtime/custom-world-library` | `A-CW` | 保留原路径;资料库改读用户 custom world view。 |
|
||||
| `runtime.customWorldLibraryDelete` | `DELETE /api/runtime/custom-world-library/:profileId` | `A-CW` | 保留原路径;删除改为 reducer 或软删除标记。 |
|
||||
| `runtime.customWorldLibraryUpsert` | `PUT /api/runtime/custom-world-library/:profileId` | `A-CW` | 保留原路径;写入改为 Axum facade + SpacetimeDB profile tables。 |
|
||||
| `runtime.customWorldLibraryPublish` | `POST /api/runtime/custom-world-library/:profileId/publish` | `A-CW` | 保留原路径;发布改为状态切换与画廊投影刷新。 |
|
||||
| `runtime.customWorldLibraryUnpublish` | `POST /api/runtime/custom-world-library/:profileId/unpublish` | `A-CW` | 保留原路径;撤回发布改为状态切换。 |
|
||||
| `runtime.customWorldSessionCreate` | `POST /api/runtime/custom-world/sessions` | `A-CW` | 保留原路径;传统问答会话状态迁到 SpacetimeDB。 |
|
||||
| `runtime.customWorldSessionGet` | `GET /api/runtime/custom-world/sessions/:sessionId` | `A-CW` | 保留原路径;读取传统问答会话改读 view。 |
|
||||
| `runtime.customWorldSessionAnswer` | `POST /api/runtime/custom-world/sessions/:sessionId/answers` | `A-CW` | 保留原路径;回答动作改为 reducer 写会话状态。 |
|
||||
| `runtime.customWorldSessionGenerateStream` | `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream` | `A-CW` | 保留 SSE contract;编译过程由 Axum 流式回推并回写状态。 |
|
||||
| `runtime.customWorldWorksList` | `GET /api/runtime/custom-world/works` | `A-CW` | 保留原路径;作品汇总改读 custom world work summary view。 |
|
||||
|
||||
### 7.6 custom world agent
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.customWorldAgentCreateSession` | `POST /api/runtime/custom-world/agent/sessions` | `A-AGENT` | 保留原路径;Agent 会话创建改写 `custom_world_agent_session`。 |
|
||||
| `runtime.customWorldAgentGetSession` | `GET /api/runtime/custom-world/agent/sessions/:sessionId` | `A-AGENT` | 保留原路径;会话快照改读 Agent session view。 |
|
||||
| `runtime.customWorldAgentExecuteAction` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/actions` | `A-AGENT` | 保留原路径;动作编排由 Axum 执行,状态与操作记录进 SpacetimeDB。 |
|
||||
| `runtime.customWorldAgentGetCardDetail` | `GET /api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId` | `A-AGENT` | 保留原路径;卡片详情改读 `custom_world_draft_card`。 |
|
||||
| `runtime.customWorldAgentSendMessage` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages` | `A-AGENT` | 保留原路径;消息提交后由 Axum 触发编排,消息与操作状态入库。 |
|
||||
| `runtime.customWorldAgentStreamMessage` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` | `A-AGENT` | 保留 SSE contract;流式消息由 Axum 输出,Agent 状态表持续更新。 |
|
||||
| `runtime.customWorldAgentGetOperation` | `GET /api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId` | `A-AGENT` | 保留原路径;操作状态改读 `custom_world_agent_operation view`。 |
|
||||
|
||||
### 7.7 compat 路径
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.customWorldEntity.compat` | `POST /api/runtime/custom-world/entity` | `A-CW` | 保留兼容路径;与主路径共用同一 handler。 |
|
||||
| `runtime.customWorldSceneNpc.compat` | `POST /api/runtime/custom-world/scene-npc` | `A-CW` | 保留兼容路径;与主路径共用同一 handler。 |
|
||||
| `runtime.profileBrowseHistoryDelete.compat` | `DELETE /api/runtime/profile/browse-history` | `A-RUNTIME` | 保留兼容路径;与 `/api/profile/browse-history` 共用实现。 |
|
||||
| `runtime.profileBrowseHistoryGet.compat` | `GET /api/runtime/profile/browse-history` | `A-RUNTIME` | 保留兼容路径;共用 browse history facade。 |
|
||||
| `runtime.profileBrowseHistoryPost.compat` | `POST /api/runtime/profile/browse-history` | `A-RUNTIME` | 保留兼容路径;共用写入逻辑。 |
|
||||
| `runtime.profileDashboard.compat` | `GET /api/runtime/profile/dashboard` | `A-RUNTIME` | 保留兼容路径;共用 dashboard facade。 |
|
||||
| `runtime.profilePlayStats.compat` | `GET /api/runtime/profile/play-stats` | `A-RUNTIME` | 保留兼容路径;共用 play stats facade。 |
|
||||
| `runtime.profileSaveArchivesList.compat` | `GET /api/runtime/profile/save-archives` | `A-RUNTIME` | 保留兼容路径;共用 save archives list facade。 |
|
||||
| `runtime.profileSaveArchivesResume.compat` | `POST /api/runtime/profile/save-archives/:worldKey` | `A-RUNTIME` | 保留兼容路径;共用 resume facade。 |
|
||||
| `runtime.profileWalletLedger.compat` | `GET /api/runtime/profile/wallet-ledger` | `A-RUNTIME` | 保留兼容路径;共用 wallet ledger facade。 |
|
||||
|
||||
### 7.8 runtime 其他核心接口
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `runtime.itemsIntent` | `POST /api/runtime/items/runtime-intent` | `A-CW` | 保留原路径;Axum 调 LLM 生成意图,物品领域状态与引用写 SpacetimeDB。 |
|
||||
| `runtime.questsGenerate` | `POST /api/runtime/quests/generate` | `A-CW` | 保留原路径;任务候选生成由 Axum 编排,结果写 quest 相关表。 |
|
||||
| `runtime.snapshotDelete` | `DELETE /api/runtime/save/snapshot` | `A-RUNTIME` | 保留原路径;删除动作改为更新 `runtime_snapshot` / archive。 |
|
||||
| `runtime.snapshotGet` | `GET /api/runtime/save/snapshot` | `A-RUNTIME` | 保留原路径;读取兼容聚合快照,由 view/projection 输出。 |
|
||||
| `runtime.snapshotPut` | `PUT /api/runtime/save/snapshot` | `A-RUNTIME` | 保留原路径;写入由 Axum facade + reducer 完成。 |
|
||||
| `runtime.settingsGet` | `GET /api/runtime/settings` | `A-RUNTIME` | 保留原路径;设置改读 `runtime_setting view`。 |
|
||||
| `runtime.settingsPut` | `PUT /api/runtime/settings` | `A-RUNTIME` | 保留原路径;设置更新改为 reducer。 |
|
||||
| `runtime.storyContinue` | `POST /api/runtime/story/continue` | `A-STORY` | 保留原路径;故事推进由 Axum 调新 story/application 层。 |
|
||||
| `runtime.storyInitial` | `POST /api/runtime/story/initial` | `A-STORY` | 保留原路径;首段故事生成保持 REST 兼容。 |
|
||||
| `runtime.wsHealth` | `GET /api/ws/health` | `A-RUNTIME` | 保留原路径;继续作为实时链路占位健康检查。 |
|
||||
|
||||
## 8. `runtime-story-action` 路由映射(2 条)
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `storyAction.resolve` | `POST /api/runtime/story/actions/resolve` | `A-STORY` | 保留原路径;Axum 接收动作请求,SpacetimeDB reducer 执行跨模块结算。 |
|
||||
| `storyAction.stateGet` | `GET /api/runtime/story/state/:sessionId` | `A-STORY` | 保留原路径;读取 story session 兼容状态 view。 |
|
||||
|
||||
## 9. `health` 路由映射(1 条)
|
||||
|
||||
| 旧路由 ID | 方法/路径 | 新实现归属 | 第一阶段迁移策略 |
|
||||
| --- | --- | --- | --- |
|
||||
| `health.check` | `GET /healthz` | `A-HEALTH` | 保留原路径与最小返回结构。 |
|
||||
|
||||
## 10. 迁移落地规则
|
||||
|
||||
后续做路由树时,必须遵守:
|
||||
|
||||
1. 旧路径优先保留,新实现从内部切换,不先要求前端改地址。
|
||||
2. 主路径与兼容路径必须共用同一 application service,避免再次出现双份逻辑。
|
||||
3. `stream` 接口第一阶段默认沿用 SSE。
|
||||
4. `assets` 与 `custom-world` 里的生成类接口,外部副作用统一在 Axum,状态与任务统一进 SpacetimeDB。
|
||||
5. `storyAction.resolve`、`runtime.snapshotPut`、`auth.refresh` 属于最优先回归接口,后续开发必须优先补完整测试。
|
||||
6. `editor` 相关旧路径只保留历史基线记录,不纳入 `server-rs` 路由树实施范围。
|
||||
|
||||
## 11. 本任务完成定义
|
||||
|
||||
当以下条件成立时,这条任务视为完成:
|
||||
|
||||
1. `96` 条旧路由都已经有新实现落点。
|
||||
2. 每条路由至少明确:
|
||||
- 旧方法/路径
|
||||
- 新实现归属
|
||||
- 第一阶段迁移策略
|
||||
3. 后续搭建 Axum 路由树与 application service 时,可以直接按这份矩阵逐项落位。
|
||||
300
backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md
Normal file
300
backend-rewrite-tasklist/M0_SSE_INTERFACE_BASELINE_2026-04-20.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# M0:SSE 接口与事件格式冻结基线
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
依据来源:
|
||||
|
||||
- [../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md](../docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md)
|
||||
- [../server-node/manifests/backend-capability-index.json](../server-node/manifests/backend-capability-index.json)
|
||||
- `server-node/src/http.ts`
|
||||
- `server-node/src/routes/runtimeRoutes.ts`
|
||||
- `server-node/src/routes/customWorldAgent.ts`
|
||||
- `server-node/src/modules/ai/chatOrchestrator.ts`
|
||||
- `server-node/src/services/customWorldAgentOrchestrator.ts`
|
||||
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于完成 `M0` 的第四条任务:
|
||||
|
||||
- 整理当前所有 SSE 接口与事件格式
|
||||
|
||||
这里的“整理”不是只记住有几条 `stream` 路由,而是要求后续 Axum 重写必须先冻结:
|
||||
|
||||
1. 当前到底有哪几条 SSE 路由。
|
||||
2. 每条路由是“透传上游流”还是“项目自定义事件流”。
|
||||
3. 每条路由的事件名、结束标记、错误帧和头部约束是什么。
|
||||
4. 哪些流的 `payload` 是增量文本,哪些其实是“累计文本”。
|
||||
|
||||
## 2. 冻结结论
|
||||
|
||||
当前 Node 后端正式登记的 SSE 接口固定为以下 `6` 条:
|
||||
|
||||
| 路由 ID | 方法/路径 | 当前实现入口 | 协议类型 | 成功结束标记 | 鉴权 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `runtime.characterReplyStream` | `POST /api/runtime/chat/character/reply/stream` | `runtimeRoutes.ts -> streamCharacterChatReplyFromOrchestrator` | 上游透传 SSE | 上游 `data: [DONE]` | JWT |
|
||||
| `runtime.npcDialogueStream` | `POST /api/runtime/chat/npc/dialogue/stream` | `runtimeRoutes.ts -> streamNpcChatDialogueFromOrchestrator` | 上游透传 SSE | 上游 `data: [DONE]` | JWT |
|
||||
| `runtime.npcRecruitStream` | `POST /api/runtime/chat/npc/recruit/stream` | `runtimeRoutes.ts -> streamNpcRecruitDialogueFromOrchestrator` | 上游透传 SSE | 上游 `data: [DONE]` | JWT |
|
||||
| `runtime.npcTurnStream` | `POST /api/runtime/chat/npc/turn/stream` | `runtimeRoutes.ts -> streamNpcChatTurnFromOrchestrator` | 项目自定义 SSE | `event: complete` 后追加 `data: [DONE]` | JWT |
|
||||
| `runtime.customWorldSessionGenerateStream` | `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream` | `runtimeRoutes.ts` 内联实现 | 项目自定义 SSE | `event: done`,无 `[DONE]` | JWT |
|
||||
| `runtime.customWorldAgentStreamMessage` | `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` | `customWorldAgent.ts -> customWorldAgentOrchestrator.streamMessage` | 项目自定义 SSE | `event: done`,无 `[DONE]` | JWT |
|
||||
|
||||
冻结总数:
|
||||
|
||||
1. SSE 接口:`6`
|
||||
2. 上游透传型:`3`
|
||||
3. 本地自定义事件流:`3`
|
||||
|
||||
## 3. 全部 SSE 接口共享的响应头约束
|
||||
|
||||
当前所有项目内主动准备 SSE 响应的接口,都经过 `prepareEventStreamResponse(...)`,因此至少冻结以下头部行为:
|
||||
|
||||
| 响应头 | 当前值/规则 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Content-Type` | 默认 `text/event-stream; charset=utf-8` | 透传型接口可被上游 `content-type` 覆盖,但仍保持 SSE。 |
|
||||
| `Cache-Control` | `no-cache` | 禁止中间层缓存流式结果。 |
|
||||
| `Connection` | `keep-alive` | 保持 SSE 长连接。 |
|
||||
| `X-Accel-Buffering` | `no` | 禁止代理层缓冲。 |
|
||||
| `x-request-id` | 透传当前请求 ID | 所有 SSE 都要带请求追踪头。 |
|
||||
| `x-api-version` | 当前 API 版本号 | 与普通 JSON 接口一致。 |
|
||||
| `x-route-version` | 当前路由版本号 | 与普通 JSON 接口一致。 |
|
||||
| `x-response-time-ms` | 当前已耗时毫秒数 | 在准备响应头时写入。 |
|
||||
|
||||
额外冻结约束:
|
||||
|
||||
1. `SSE` 接口当前也保留普通 API 元数据头,不能因为换成 Axum 就丢掉。
|
||||
2. 这 `6` 条流式接口都在 `requireAuth` 之后注册,因此第一阶段默认仍需要 `Bearer JWT`。
|
||||
|
||||
## 4. 协议分型
|
||||
|
||||
### 4.1 上游透传型 SSE(3 条)
|
||||
|
||||
包含:
|
||||
|
||||
1. `POST /api/runtime/chat/character/reply/stream`
|
||||
2. `POST /api/runtime/chat/npc/dialogue/stream`
|
||||
3. `POST /api/runtime/chat/npc/recruit/stream`
|
||||
|
||||
当前实现特征:
|
||||
|
||||
1. 路由不自己重写事件名,直接把上游模型返回的 SSE 原样管道转发给前端。
|
||||
2. 本地只负责:
|
||||
- 发起上游流式请求
|
||||
- 准备 SSE 头部
|
||||
- 处理中断时的请求 abort
|
||||
3. 从 `llmClient.streamMessageContent(...)` 的解析逻辑可以反推,当前上游 SSE 采用 OpenAI 风格:
|
||||
- 多个 `data: {...}` JSON chunk
|
||||
- 最终 `data: [DONE]`
|
||||
|
||||
冻结要求:
|
||||
|
||||
1. 第一阶段 Axum 仍要保持这三条接口的“上游透传”语义。
|
||||
2. 不要在未发版变更协议前,擅自把它们改成项目自定义 `event: reply_delta` 格式。
|
||||
|
||||
### 4.2 项目自定义 SSE(3 条)
|
||||
|
||||
包含:
|
||||
|
||||
1. `POST /api/runtime/chat/npc/turn/stream`
|
||||
2. `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream`
|
||||
3. `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`
|
||||
|
||||
当前实现特征:
|
||||
|
||||
1. 路由或 orchestrator 自己写 `event:` 与 `data:`。
|
||||
2. 事件名不是上游协议,而是项目本地约定。
|
||||
3. 这三条流的结束方式并不一致,必须分别兼容。
|
||||
|
||||
## 5. 各接口事件格式冻结
|
||||
|
||||
### 5.1 `runtime.characterReplyStream`
|
||||
|
||||
路径:
|
||||
|
||||
- `POST /api/runtime/chat/character/reply/stream`
|
||||
|
||||
冻结格式:
|
||||
|
||||
1. 当前为上游透传流。
|
||||
2. 本地不保证固定 `event` 名。
|
||||
3. 前端实际收到的是上游 `data: {...}` chunk 与最终 `data: [DONE]`。
|
||||
4. 失败时当前实现也不是本地 `event: error`,而是由上游失败或 Express 错误链决定。
|
||||
|
||||
### 5.2 `runtime.npcDialogueStream`
|
||||
|
||||
路径:
|
||||
|
||||
- `POST /api/runtime/chat/npc/dialogue/stream`
|
||||
|
||||
冻结格式:
|
||||
|
||||
1. 当前为上游透传流。
|
||||
2. 协议特征与 `runtime.characterReplyStream` 相同。
|
||||
3. 第一阶段不能私自改成项目自定义事件名。
|
||||
|
||||
### 5.3 `runtime.npcRecruitStream`
|
||||
|
||||
路径:
|
||||
|
||||
- `POST /api/runtime/chat/npc/recruit/stream`
|
||||
|
||||
冻结格式:
|
||||
|
||||
1. 当前为上游透传流。
|
||||
2. 协议特征与前两条透传 SSE 相同。
|
||||
3. 结束标记仍依赖上游 `data: [DONE]`。
|
||||
|
||||
### 5.4 `runtime.npcTurnStream`
|
||||
|
||||
路径:
|
||||
|
||||
- `POST /api/runtime/chat/npc/turn/stream`
|
||||
|
||||
成功事件序列:
|
||||
|
||||
1. `event: reply_delta`
|
||||
2. `event: reply_delta`
|
||||
3. `...`
|
||||
4. `event: complete`
|
||||
5. `data: [DONE]`
|
||||
|
||||
错误事件:
|
||||
|
||||
1. `event: error`
|
||||
2. `data: {"message":"..."}`
|
||||
3. 之后直接 `response.end()`,不会再补 `complete`
|
||||
|
||||
冻结 payload 规则:
|
||||
|
||||
| 事件名 | payload 结构 | 关键说明 |
|
||||
| --- | --- | --- |
|
||||
| `reply_delta` | `{ "text": string }` | `text` 实际是“累计文本”,不是单 token 增量。 |
|
||||
| `complete` | `{ "npcReply": string, "affinityDelta": number, "affinityText": string, "suggestions": string[], "pendingQuestOffer": object \| null, "chatDirective": object \| null }` | 最终一次性返回业务结算数据。 |
|
||||
| `error` | `{ "message": string }` | 仅错误消息,无额外状态。 |
|
||||
|
||||
补充冻结点:
|
||||
|
||||
1. `reply_delta.text` 每次都是当前累计回复全文。
|
||||
2. `complete.suggestions` 在强制收束场景下可能是空数组。
|
||||
3. `complete.chatDirective` 当前至少可能包含:
|
||||
- `turnLimit`
|
||||
- `remainingTurns`
|
||||
- `forceExit`
|
||||
- `closingMode`
|
||||
4. `complete.pendingQuestOffer` 当前可能包含:
|
||||
- `quest`
|
||||
- `introText`
|
||||
|
||||
### 5.5 `runtime.customWorldSessionGenerateStream`
|
||||
|
||||
路径:
|
||||
|
||||
- `GET /api/runtime/custom-world/sessions/:sessionId/generate/stream`
|
||||
|
||||
成功事件序列:
|
||||
|
||||
1. `event: progress`,payload:`{ "phase": "preparing", "progress": 10 }`
|
||||
2. `event: progress`,payload:`{ "phase": "requesting_llm", "progress": 45 }`
|
||||
3. `event: progress`,payload:`CustomWorldGenerationProgress`
|
||||
4. `...`
|
||||
5. `event: progress`,payload:`{ "phase": "completed", "progress": 100 }`
|
||||
6. `event: result`
|
||||
7. `event: done`
|
||||
|
||||
错误事件:
|
||||
|
||||
1. `event: error`
|
||||
2. `data: {"message":"..."}`
|
||||
3. 之后直接结束,不会再发 `done`
|
||||
|
||||
冻结 payload 规则:
|
||||
|
||||
| 事件名 | payload 结构 | 关键说明 |
|
||||
| --- | --- | --- |
|
||||
| `progress` | 兼容两种结构 | 这是当前最容易踩坑的混合协议。 |
|
||||
| `result` | `{ "profile": object }` | 返回完整世界 profile。 |
|
||||
| `done` | `{ "ok": true }` | 当前没有 `[DONE]` 字符串终止帧。 |
|
||||
| `error` | `{ "message": string }` | 当前也没有额外错误码。 |
|
||||
|
||||
`progress` 事件的两种冻结结构:
|
||||
|
||||
1. 启动/收尾帧:
|
||||
- `{ "phase": "preparing", "progress": 10 }`
|
||||
- `{ "phase": "requesting_llm", "progress": 45 }`
|
||||
- `{ "phase": "completed", "progress": 100 }`
|
||||
2. 编排器进度帧 `CustomWorldGenerationProgress`:
|
||||
- `phaseId`
|
||||
- `phaseLabel`
|
||||
- `phaseDetail`
|
||||
- `overallProgress`
|
||||
- `completedWeight`
|
||||
- `totalWeight`
|
||||
- `elapsedMs`
|
||||
- `estimatedRemainingMs`
|
||||
- `activeStepIndex`
|
||||
- `steps`
|
||||
|
||||
补充冻结点:
|
||||
|
||||
1. 当前 `progress` 不是单一 schema,而是混合 schema。
|
||||
2. 当前实现会在客户端断开时触发 `AbortController`,这条流具备显式中断处理。
|
||||
|
||||
### 5.6 `runtime.customWorldAgentStreamMessage`
|
||||
|
||||
路径:
|
||||
|
||||
- `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`
|
||||
|
||||
成功事件序列:
|
||||
|
||||
1. `event: reply_delta`
|
||||
2. `event: reply_delta`
|
||||
3. `...`
|
||||
4. `event: session`
|
||||
5. `event: done`
|
||||
|
||||
错误事件:
|
||||
|
||||
1. `event: error`
|
||||
2. `data: {"message":"..."}`
|
||||
3. 之后直接结束,不会再补 `done`
|
||||
|
||||
冻结 payload 规则:
|
||||
|
||||
| 事件名 | payload 结构 | 关键说明 |
|
||||
| --- | --- | --- |
|
||||
| `reply_delta` | `{ "text": string }` | 当前也是累计文本,不是 diff patch。 |
|
||||
| `session` | `{ "session": CustomWorldAgentSessionSnapshot }` | 完整会话快照一次性回推。 |
|
||||
| `done` | `{ "ok": true }` | 当前没有 `[DONE]`。 |
|
||||
| `error` | `{ "message": string }` | 仅错误消息。 |
|
||||
|
||||
补充冻结点:
|
||||
|
||||
1. 这条流当前不会在成功结尾补发最终文本帧,只会发 `session` 快照。
|
||||
2. `reply_delta.text` 同样是“到当前为止的完整回复”。
|
||||
3. 当前实现没有像 `customWorldSessionGenerateStream` 那样显式挂请求断开 abort。
|
||||
|
||||
## 6. 第一阶段 Axum 重写必须兼容的硬约束
|
||||
|
||||
后续重写中,不允许出现以下情况:
|
||||
|
||||
1. 把当前 `6` 条 SSE 路由减少、合并或改掉方法类型。
|
||||
2. 把透传型 `3` 条流直接改写成自定义事件名,而前端却不知情。
|
||||
3. 把 `npcTurnStream` 的 `reply_delta` 从“累计文本”改成“真正 delta”,导致前端拼接方式失效。
|
||||
4. 把 `customWorldSessionGenerateStream` 的混合 `progress` schema 静默改成新格式,却没有版本门禁。
|
||||
5. 把 `customWorldAgentStreamMessage` 的 `session` 终帧改成局部 patch,而前端仍按完整快照消费。
|
||||
6. 丢失 `x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 等当前前端与联调用到的头。
|
||||
|
||||
## 7. 本任务完成定义
|
||||
|
||||
当以下条件成立时,这条任务视为完成:
|
||||
|
||||
1. 当前 `6` 条 SSE 接口已经有书面冻结清单。
|
||||
2. 每条 SSE 都已明确:
|
||||
- 方法与路径
|
||||
- 协议类型
|
||||
- 事件名
|
||||
- 成功结束标记
|
||||
- 错误事件
|
||||
- 关键 payload 结构
|
||||
3. 后续 Axum SSE 落地、前端 contract 回归、SpacetimeDB 实时链路设计时,可以直接引用本文,不再靠人工回忆事件名。
|
||||
36
backend-rewrite-tasklist/README.md
Normal file
36
backend-rewrite-tasklist/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 后端重写任务清单目录
|
||||
|
||||
日期:`2026-04-20`
|
||||
|
||||
本目录用于集中存放 `SpacetimeDB + Axum + 阿里云 OSS` 后端重写相关任务清单。
|
||||
|
||||
## 文件结构
|
||||
|
||||
- [00_MASTER_TASKLIST.md](./00_MASTER_TASKLIST.md):总纲主清单,保留完整阶段结构与最终验收项。
|
||||
- [01_M0_M2_FOUNDATION_AND_AUTH.md](./01_M0_M2_FOUNDATION_AND_AUTH.md):能力冻结、Rust 工作区、Axum 基础设施、鉴权与会话迁移任务。
|
||||
- [02_M3_RUNTIME_PROFILE.md](./02_M3_RUNTIME_PROFILE.md):runtime snapshot / settings / profile 迁移任务。
|
||||
- [03_M4_STORY_AND_GAMEPLAY.md](./03_M4_STORY_AND_GAMEPLAY.md):story action 主循环与 gameplay reducer 迁移任务。
|
||||
- [04_M5_CUSTOM_WORLD_AND_AGENT.md](./04_M5_CUSTOM_WORLD_AND_AGENT.md):custom world / gallery / agent 主链迁移任务。
|
||||
- [05_M6_ASSETS_OSS_EDITOR.md](./05_M6_ASSETS_OSS_EDITOR.md):assets / 阿里云 OSS 迁移任务;`editor` 已于 `2026-04-21` 退出本轮重写范围。
|
||||
- [06_M7_TEST_DEPLOY_CUTOVER.md](./06_M7_TEST_DEPLOY_CUTOVER.md):联调、回归、部署、观测与切流任务。
|
||||
- [07_CROSS_CUTTING_AND_ACCEPTANCE.md](./07_CROSS_CUTTING_AND_ACCEPTANCE.md):横向专项、执行顺序与最终验收清单。
|
||||
- [M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md](./M0_CAPABILITY_SURFACE_BASELINE_2026-04-20.md):当前 Node 后端 `6` 个挂载面的冻结基线,用于后续接口映射、模块迁移与验收对照。
|
||||
- [M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md](./M0_ROUTE_MIGRATION_MATRIX_2026-04-20.md):当前 `96` 条后端路由的“旧接口 -> 新实现”迁移矩阵,用于 Axum 路由树和 application service 落位。
|
||||
- [M0_MODULE_MIGRATION_BASELINE_2026-04-20.md](./M0_MODULE_MIGRATION_BASELINE_2026-04-20.md):当前 `12` 个内部模块的迁移归属基线,用于锁定 Rust crate、SpacetimeDB bounded context 与 Axum/application 分工。
|
||||
- [M0_SSE_INTERFACE_BASELINE_2026-04-20.md](./M0_SSE_INTERFACE_BASELINE_2026-04-20.md):当前 `6` 条 SSE 接口及其事件格式冻结基线,用于 Axum SSE 兼容和前端 contract 回归。
|
||||
- [M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md](./M0_GENERATED_STATIC_PREFIX_BASELINE_2026-04-20.md):当前正式 `/generated-*` 静态资源前缀冻结基线,用于 Axum 静态资源兼容层与 OSS 对象键规划。
|
||||
- [M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md](./M0_FRONTEND_RESPONSE_CONTRACT_BASELINE_2026-04-20.md):当前前端直接依赖的响应头、envelope 与错误格式冻结基线,用于 Axum 中间件与错误响应兼容。
|
||||
- [M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md](./M0_REPOSITORY_BOUNDARY_DECISIONS_2026-04-20.md):`M0` 仓库边界决议文档,用于持续冻结 `server-rs/` 落位、迁移期双栈共存、Axum 边界与副作用收口原则。
|
||||
- [M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md](./M0_PHASE_ACCEPTANCE_MATRIX_2026-04-20.md):`M0 ~ M7` 阶段验收矩阵,用于固定每阶段的入口条件、核心交付、退出条件与跨阶段回归焦点。
|
||||
|
||||
## 当前 M4 / M5 结构基线
|
||||
|
||||
- `M4` 当前涉及的前后端脚本结构、命名根、route/service/compiler/repository 落位,统一参照 [../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)。
|
||||
- `M5` 当前涉及的创作入口、Agent session、result preview、works/library/gallery、publish 与 enter-world 主链,统一参照 [../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)。
|
||||
- 旧 `custom-world/sessions` 传统问答链已经退出当前仓库正式主链;后续若在 `M5` 中提及,只按历史兼容台账处理,不再作为当前功能扩展目标。
|
||||
|
||||
## 维护规则
|
||||
|
||||
1. 总纲与拆分文件都以本目录为唯一维护位置。
|
||||
2. 总纲用于把控全局节奏,拆分文件用于实际逐项推进。
|
||||
3. 如阶段任务发生明显变化,需要同步更新总纲与对应拆分文件。
|
||||
28
bash.exe.stackdump
Normal file
28
bash.exe.stackdump
Normal file
@@ -0,0 +1,28 @@
|
||||
Stack trace:
|
||||
Frame Function Args
|
||||
0007FFFFB520 00021005FE8E (000210285F68, 00021026AB6E, 000000000000, 0007FFFFA420) msys-2.0.dll+0x1FE8E
|
||||
0007FFFFB520 0002100467F9 (000000000000, 000000000000, 000000000000, 0007FFFFB7F8) msys-2.0.dll+0x67F9
|
||||
0007FFFFB520 000210046832 (000210286019, 0007FFFFB3D8, 000000000000, 000000000000) msys-2.0.dll+0x6832
|
||||
0007FFFFB520 000210068CF6 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28CF6
|
||||
0007FFFFB520 000210068E24 (0007FFFFB530, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28E24
|
||||
0007FFFFB800 00021006A225 (0007FFFFB530, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x2A225
|
||||
End of stack trace
|
||||
Loaded modules:
|
||||
000100400000 bash.exe
|
||||
7FFA3C060000 ntdll.dll
|
||||
7FFA3B490000 KERNEL32.DLL
|
||||
7FFA390F0000 KERNELBASE.dll
|
||||
7FFA3BE50000 USER32.dll
|
||||
7FFA38E90000 win32u.dll
|
||||
7FFA3A230000 GDI32.dll
|
||||
7FFA38D60000 gdi32full.dll
|
||||
7FFA38EC0000 msvcp_win.dll
|
||||
7FFA38930000 ucrtbase.dll
|
||||
000210040000 msys-2.0.dll
|
||||
7FFA39EB0000 advapi32.dll
|
||||
7FFA3A180000 msvcrt.dll
|
||||
7FFA3BCA0000 sechost.dll
|
||||
7FFA3B5F0000 RPCRT4.dll
|
||||
7FFA37D70000 CRYPTBASE.DLL
|
||||
7FFA38B40000 bcryptPrimitives.dll
|
||||
7FFA3A260000 IMM32.DLL
|
||||
@@ -1,6 +1,6 @@
|
||||
# 文档总览
|
||||
|
||||
`docs/` 现在按主题拆成了 6 类,`docs/prd/` 保持独立,不参与本次整理改写。
|
||||
`docs/` 现在按主题拆成了 6 类;旧后端路线文档开始聚合和删除,后续实现以 Rust / SpacetimeDB 当前基线为准。
|
||||
|
||||
## 快速入口
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
- [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。
|
||||
- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级。
|
||||
- [参考目录](./reference/README.md):脚本/Function 速查入口。
|
||||
- [PRD](./prd/):产品需求与阶段计划,原样保留。
|
||||
- [PRD](./prd):产品需求与阶段计划;新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md)。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
|
||||
2. 再看 [工程审查总览](./audits/engineering/README.md) 和 [文本审计总览](./audits/text/README.md),了解当前风险。
|
||||
3. 需要排期时看 [规划与优先级](./planning/README.md)。
|
||||
4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md)。
|
||||
5. 需要对齐目标边界时再进入 [PRD](./prd/)。
|
||||
4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md);涉及后端先看 [当前后端实现基线](./technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)。
|
||||
5. 需要对齐目标边界时再进入 [PRD](./prd)。
|
||||
|
||||
## 分类规则
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
|
||||
- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
|
||||
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
|
||||
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md`
|
||||
- `docs/audits/engineering/README.md`
|
||||
- `src/data/stateFunctions.ts`
|
||||
- `src/data/npcInteractions.ts`
|
||||
- `src/data/treasureInteractions.ts`
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- `docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md`
|
||||
- `docs/reference/FUNCTION_SCRIPT_CATALOG_2026-04-04.md`
|
||||
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
|
||||
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md`
|
||||
- `docs/audits/engineering/README.md`
|
||||
|
||||
本次实际核对了这些实现入口:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 系列总览
|
||||
|
||||
- [engineering/README.md](./engineering/README.md):工程优化审查三轮记录的融合入口。
|
||||
- [engineering/README.md](./engineering/README.md):当前工程优化审查与历史结论聚合入口。
|
||||
- [text/README.md](./text/README.md):文本、英文残留、乱码审计系列的融合入口。
|
||||
|
||||
## 专项审计
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# RPG 运行时直读世界草稿 Profile 检查 2026-04-25
|
||||
|
||||
## 结论
|
||||
|
||||
RPG 运行时进入游戏时不应再通过 `resultPreview.preview` 或 legacy runtime profile 做中间转换,主数据源统一为 Agent session 的 `draftProfile`。
|
||||
|
||||
本次检查确认:
|
||||
|
||||
1. Rust 侧 `custom_world_foundation_draft` 已直接产出 `draftProfile`。
|
||||
2. 前端原先 `buildCustomWorldProfileFromAgentSession()` 仍只读取 `session.resultPreview.preview`,这会绕过草稿 profile 中已经存在的角色形象、关系、压力等字段。
|
||||
3. 角色选择页与游戏内角色本身可以消费 `CustomWorldProfile.playableNpcs[].imageSrc`,断点在“session -> profile”的入口,而不是角色选择页。
|
||||
4. “进入世界”按钮原先还会先执行 `sync_result_profile`,把当前结果页旧快照再同步回 session;如果结果页 profile 没有最新角色图,会在进入角色选择页前覆盖掉 `draftProfile` 中的正确形象。
|
||||
|
||||
## 已修正
|
||||
|
||||
- `buildCustomWorldProfileFromAgentSession()` 改为直接归一化 `session.draftProfile`。
|
||||
- `resultPreview` 只保留为发布质量、blocker、预览外壳信息,不再作为进入游戏 profile 的数据源。
|
||||
- Agent 草稿结果进入游戏时直接使用最新 `agentSessionProfile`,不再把当前结果页 profile 回写成新的运行时 profile。
|
||||
- 前端 `normalizeCustomWorldProfileRecord()` 补齐 rs 草稿角色字段兼容:
|
||||
- `publicMask/publicIdentity` -> `description/visualDescription/personality` fallback
|
||||
- `currentPressure/hiddenHook` -> `backstory/actionDescription/sceneVisualDescription` fallback
|
||||
- `relationToPlayer` -> `motivation/relationshipHooks` fallback
|
||||
- `imageSrc/generatedVisualAssetId/generatedAnimationSetId/animationMap` 保持直通
|
||||
|
||||
## 后续约束
|
||||
|
||||
- 新 RPG 运行时链路只允许读取 `draftProfile`。
|
||||
- 不再为进入游戏构造额外 legacy profile,也不再把 `resultPreview.preview` 当作运行时真相源。
|
||||
- 如果草稿中新增角色、场景、物品字段,应优先扩展 `draftProfile` 的归一化读取,而不是增加中间转换结构。
|
||||
@@ -349,7 +349,7 @@
|
||||
文档依据:
|
||||
|
||||
1. `docs/audits/engineering/README.md`
|
||||
2. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md`
|
||||
2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
3. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
|
||||
4. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
|
||||
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
# 当前工程优化优先级汇总(2026-04-10)
|
||||
|
||||
## 结论先说
|
||||
|
||||
和 `2026-04-01` 那轮工程审查相比,当前仓库的主问题已经发生了明显迁移:
|
||||
|
||||
- 运行时主链拆分已经有进展,`useStoryGeneration.ts` 不再是最高复杂度热点。
|
||||
- `typecheck`、前后端测试、内容校验、编码校验都已经回到可通过状态。
|
||||
- 当前真正卡住工程节奏的,已经变成:
|
||||
- 绿色门禁不可信
|
||||
- 构建 warning 仍然会直接打断发布门禁
|
||||
- 自定义世界 / 编辑器 / 资产链路出现了新的巨型模块热点
|
||||
- 生成产物与旧工具链残留开始反向污染 lint、watch 和本地开发信号
|
||||
|
||||
一句话判断:
|
||||
|
||||
**现在最该优先做的,不是继续扩功能,而是先把门禁重新拉回可信状态,再拆 editor / custom world / assets 这批新的复杂度中心。**
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-10 当前校验快照
|
||||
|
||||
本次汇总不是只复述旧文档,额外执行了当前仓库校验命令。
|
||||
|
||||
| 项目 | 结果 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `npm run check:encoding` | 通过 | 编码基线正常 |
|
||||
| `npm run typecheck` | 通过 | 当前严格类型门禁可通过 |
|
||||
| `npm run test` | 通过 | `92` 个测试文件、`228` 个测试通过 |
|
||||
| `npm run server-node:test:baseline` | 通过 | 观测基线正常 |
|
||||
| `npm run server-node:test` | 通过 | `72` 个后端测试通过 |
|
||||
| `npm run check:content` | 通过 | 内容与覆盖校验正常 |
|
||||
| `npm run lint:eslint` | 失败 | `330` 个 error、`4` 个 warning |
|
||||
| `npm run build` | 失败 | 构建完成,但因 warning 被 `build-gate` 拦截 |
|
||||
|
||||
当前状态说明:
|
||||
|
||||
- 仓库不是“完全不可用”,而是已经进入“测试绿,但门禁信号不一致”的阶段。
|
||||
- 这类状态比纯红线更危险,因为团队会误以为主链已经稳定。
|
||||
|
||||
---
|
||||
|
||||
## P0:先恢复可信的绿色门禁
|
||||
|
||||
### P0-1:修复 lint 失真,重新建立可信基线
|
||||
|
||||
这是当前第一优先级。
|
||||
|
||||
#### 证据
|
||||
|
||||
- `npm run lint:eslint` 当前失败,报出 `330` 个 error、`4` 个 warning。
|
||||
- 问题既有真实源码问题,也有明显的门禁污染:
|
||||
- `src/`、`server-node/`、`scripts/` 中存在 import 排序、未使用导入、少量 hook 规则问题。
|
||||
- `temp-build-goal-check/` 这类生成产物目录也被 ESLint 扫描进来,放大了噪音。
|
||||
- `.eslintrc.cjs` 当前忽略了 `dist`、`media` 等目录,但没有忽略 `temp-build-goal-check`。
|
||||
- `vite.config.ts` 的 `server.watch.ignored` 已经忽略了 `**/temp*build*/**`,说明当前 watch 口径和 lint 口径并不一致。
|
||||
|
||||
#### 影响
|
||||
|
||||
- 团队无法快速判断“现在是源码真问题,还是产物目录噪音”。
|
||||
- lint 失真会直接削弱 review、回归和集成效率。
|
||||
- 在这种状态下继续加功能,只会让真实错误被更多噪音淹没。
|
||||
|
||||
#### 当前建议
|
||||
|
||||
1. 先清理或迁出 `temp-build-goal-check/` 这类生成产物目录,至少不要再让它进入 lint 扫描范围。
|
||||
2. 统一 `watch / lint / build` 对临时目录和生成目录的忽略口径。
|
||||
3. 再集中清当前源码层 lint 问题,优先处理:
|
||||
- import 排序
|
||||
- 未使用导入
|
||||
- 少量真实规则错误,例如 hook 误用和 `ban-types`
|
||||
|
||||
---
|
||||
|
||||
### P0-2:修复构建 warning,恢复可发布构建
|
||||
|
||||
这是和 P0-1 同级的阻塞项。
|
||||
|
||||
#### 证据
|
||||
|
||||
- `npm run build` 当前会被 `scripts/build-gate.mjs` 拦截。
|
||||
- 当前构建输出里最关键的 warning 有两类:
|
||||
- `src/services/ai.ts` 虽然尝试走动态加载,但又被 `src/components/CustomWorldEntityEditorModal.tsx` 静态引入,导致拆包失效。
|
||||
- `AuthenticatedApp-*.js` 达到 `1078.61 kB`,超过当前 `750 kB` 的 chunk warning 门槛。
|
||||
- 同轮构建里,`index-*.css` 也已经达到 `157.56 kB`,说明不仅 JS 主块重,样式也在继续膨胀。
|
||||
|
||||
#### 影响
|
||||
|
||||
- 当前不是“构建有一点 warning 可以先带着走”,而是发布门禁已经被 warning 直接打断。
|
||||
- editor / custom world / asset 工具能力正在把非主链代码重新带回主包路径。
|
||||
- 后续如果继续叠加这条链路,首屏、缓存和回归都会继续变差。
|
||||
|
||||
#### 当前建议
|
||||
|
||||
1. 先切断 `CustomWorldEntityEditorModal.tsx -> ../services/ai` 的静态依赖,让 `ai.ts` 真正留在懒加载路径。
|
||||
2. 把自定义世界编辑器、资产工作台、非首屏工具能力继续从 `AuthenticatedApp` 主块中拆出。
|
||||
3. 保持 `build warning = 失败` 的策略,不建议通过放宽阈值掩盖问题。
|
||||
|
||||
---
|
||||
|
||||
## P1:拆掉新的复杂度中心
|
||||
|
||||
### P1-1:优先拆 editor / custom world / assets 新热点
|
||||
|
||||
旧的运行时主链热点已经有所缓解,但复杂度并没有消失,而是转移到了新的模块上。
|
||||
|
||||
#### 当前大文件热点
|
||||
|
||||
前端:
|
||||
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx`:`2778` 行
|
||||
- `src/services/ai.ts`:`2454` 行
|
||||
- `src/services/customWorld.ts`:`2217` 行
|
||||
- `src/data/npcInteractions.ts`:`2103` 行
|
||||
- `src/data/characterPresets.ts`:`1953` 行
|
||||
- `src/services/prompt.ts`:`1725` 行
|
||||
|
||||
后端:
|
||||
|
||||
- `server-node/src/modules/assets/characterAssetRoutes.ts`:`2295` 行
|
||||
- `server-node/src/app.test.ts`:`1527` 行
|
||||
- `server-node/src/auth/authService.ts`:`1243` 行
|
||||
- `server-node/src/modules/quest/runtimeQuestModule.ts`:`1137` 行
|
||||
|
||||
工具链:
|
||||
|
||||
- `scripts/dev-server/*.ts`:已于 `2026-04-19` 删除,旧 Vite 本地 API 链路不再保留实现代码
|
||||
|
||||
#### 影响
|
||||
|
||||
- 复杂度并没有真正被消灭,而是从运行时 story hook 转移到了自定义世界、资产编辑、提示词和数据装配链。
|
||||
- 这些文件大多同时承载了:
|
||||
- 领域规则
|
||||
- API 调用
|
||||
- 文本拼装
|
||||
- UI 状态
|
||||
- 工具流程
|
||||
- 后续任何一个小改动,都容易牵动整条大链,回归成本会再次上升。
|
||||
|
||||
#### 当前建议
|
||||
|
||||
1. 前端优先拆 `CustomWorldEntityEditorModal.tsx`,按“世界锚点 / 角色 / 地点 / 资产 / 高级设置”分段。
|
||||
2. 后端优先拆 `characterAssetRoutes.ts`,把 route、job orchestration、文件发布、模板读取拆开。
|
||||
3. 把 `src/services/ai.ts` 和 `src/services/customWorld.ts` 继续按运行时 / 编辑器 / 资产工具三条职责分层。
|
||||
|
||||
---
|
||||
|
||||
### P1-2:继续收口 editor / assets 工具链边界(旧链路已删除)
|
||||
|
||||
这项的重要性正在上升。
|
||||
|
||||
#### 证据
|
||||
|
||||
- `docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md` 已说明 editor/assets API 已经迁到 `server-node`,方向是对的。
|
||||
- `scripts/dev-server/*.ts` 旧 Vite 本地 API 实现代码已于 `2026-04-19` 删除,仓库里不再保留并行实现。
|
||||
- 目录 `temp-build-goal-check/` 当前包含 `15099` 个文件,已经开始干扰 lint 和本地开发信号。
|
||||
- 相关日志里还出现了大量指向 `temp-build-goal-check` 的页面 reload 与 `ENOENT` 噪音。
|
||||
|
||||
#### 影响
|
||||
|
||||
- editor/assets 正式入口已经收口到 `server-node`,这部分双链路问题已解除。
|
||||
- 当前更大的噪音来源已经转移到临时构建目录、检查目录和历史日志残留。
|
||||
|
||||
#### 当前建议
|
||||
|
||||
1. 保持 `scripts/dev-server/README.md` 作为迁移结果标记,不要恢复旧 Vite `/api/*` 本地插件链。
|
||||
2. 将临时构建目录、检查目录、导出目录统一移出主工程扫描面。
|
||||
3. 继续以 `server-node/src/modules/editor/**`、`server-node/src/modules/assets/**` 与 `src/editor/shared/editorApiClient.ts` 作为唯一推荐入口,减少后续回流。
|
||||
|
||||
---
|
||||
|
||||
## P2:继续做架构收口,但不必抢在 P0 前面
|
||||
|
||||
### P2-1:继续压缩前端遗留 AI / 自定义世界实现
|
||||
|
||||
这一项仍然值得做,但当前不再是最前面的阻塞。
|
||||
|
||||
#### 原因
|
||||
|
||||
- `docs/technical/EXPRESS_BACKEND_WORKSTREAM_AUDIT_2026-04-09.md` 显示正式运行时主链已经大幅回收到后端。
|
||||
- 当前更明显的遗留,已经集中到编辑器、自定义世界工作台和资产工具,而不是正式运行时 story 主链。
|
||||
|
||||
#### 当前建议
|
||||
|
||||
1. 继续让正式运行时保持“后端为真相源”。
|
||||
2. 对仍留在前端的大 AI / prompt / custom world 实现,优先做职责收缩,而不是继续在原文件上堆逻辑。
|
||||
|
||||
---
|
||||
|
||||
### P2-2:继续优化自定义世界工作台,但以“减负”和“分层”为主
|
||||
|
||||
这一项更适合作为 P0、P1 稳住后的下一轮重点。
|
||||
|
||||
#### 依据
|
||||
|
||||
- `docs/audits/CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md` 已经明确指出:
|
||||
- 自定义世界入口、澄清、锁定、局部重生成、结果工作台仍是半收口状态。
|
||||
- 当前最大的前端热点文件也集中在这条链路上,说明它已经不仅是产品问题,也是工程复杂度问题。
|
||||
|
||||
#### 当前建议
|
||||
|
||||
1. 优先减少“大一统编辑弹窗”的职责,把高杠杆编辑和高级编辑分层。
|
||||
2. 让自定义世界生成、锁定、局部重生成规则继续向后端收口。
|
||||
3. 移动端优先,避免长表单和重弹窗继续吞掉维护成本。
|
||||
|
||||
---
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
### 第一阶段:先把门禁拉回可信
|
||||
|
||||
1. 修 lint 口径失真
|
||||
2. 清生成产物扫描污染
|
||||
3. 修 build warning
|
||||
|
||||
### 第二阶段:再拆新的复杂度中心
|
||||
|
||||
1. 拆 `CustomWorldEntityEditorModal.tsx`
|
||||
2. 拆 `characterAssetRoutes.ts`
|
||||
3. 收缩 `src/services/ai.ts` / `src/services/customWorld.ts`
|
||||
|
||||
### 第三阶段:最后收 editor / custom world 架构尾巴
|
||||
|
||||
1. 清理旧 Vite 工具链残留
|
||||
2. 继续把自定义世界和资产工具收回正式后端边界
|
||||
|
||||
---
|
||||
|
||||
## 当前不建议优先做的事
|
||||
|
||||
- 不建议在当前 lint 与 build 仍然是红线时继续横向扩 editor / custom world 功能。
|
||||
- 不建议通过放宽 chunk warning 阈值来“修复”构建。
|
||||
- 不建议继续在 `CustomWorldEntityEditorModal.tsx`、`src/services/ai.ts`、`characterAssetRoutes.ts` 这类巨型文件中直接堆新逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 本文依据
|
||||
|
||||
文档依据:
|
||||
|
||||
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md`
|
||||
- `docs/technical/EXPRESS_BACKEND_WORKSTREAM_AUDIT_2026-04-09.md`
|
||||
- `docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md`
|
||||
- `docs/audits/CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md`
|
||||
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md`
|
||||
|
||||
当前仓库校验依据:
|
||||
|
||||
- `npm run check:encoding`
|
||||
- `npm run typecheck`
|
||||
- `npm run test`
|
||||
- `npm run server-node:test:baseline`
|
||||
- `npm run server-node:test`
|
||||
- `npm run check:content`
|
||||
- `npm run lint:eslint`
|
||||
- `npm run build`
|
||||
@@ -124,9 +124,8 @@
|
||||
本次审计结合了四类证据:
|
||||
|
||||
1. 文档基线:
|
||||
- `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md`
|
||||
- `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
|
||||
- `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md`
|
||||
- `docs/audits/engineering/README.md`
|
||||
- `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
- `scripts/dev-server/README.md`
|
||||
2. 当前入口核对:
|
||||
- `src/main.tsx`
|
||||
@@ -591,10 +590,9 @@
|
||||
|
||||
文档依据:
|
||||
|
||||
1. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md`
|
||||
2. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
|
||||
3. `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md`
|
||||
4. `scripts/dev-server/README.md`
|
||||
1. `docs/audits/engineering/README.md`
|
||||
2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
3. `scripts/dev-server/README.md`
|
||||
|
||||
当前仓库扫描依据:
|
||||
|
||||
|
||||
@@ -366,9 +366,8 @@
|
||||
文档依据:
|
||||
|
||||
1. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md`
|
||||
2. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md`
|
||||
3. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
|
||||
4. `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md`
|
||||
2. `docs/audits/engineering/README.md`
|
||||
3. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
|
||||
当前仓库复核依据:
|
||||
|
||||
@@ -381,4 +380,3 @@
|
||||
7. `src/services/runtimeItemAiDirector.ts`
|
||||
8. `src/services/apiClient.ts`
|
||||
9. 当前依赖图扫描结果与当前大文件体量扫描结果
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
**运行时快照前置写入链当前还不能直接砍。**
|
||||
|
||||
原因不是“不想动”,而是服务端当前 `runtime story` 动作入口仍然以远端快照作为执行基线。
|
||||
原因不是“不想动”,而是服务端当前 `runtime story` 动作入口仍然以远端快照作为执行基线。
|
||||
在后端 contract 没先改好之前,前端不能假装自己已经退出这条链。
|
||||
|
||||
---
|
||||
@@ -120,13 +120,77 @@
|
||||
|
||||
结果:
|
||||
|
||||
构建产物生成成功,但 `build-gate` 仍因主包 chunk warning 拦截失败。
|
||||
构建产物生成成功,但 `build-gate` 仍因主包 chunk warning 拦截失败。
|
||||
当前失败点仍是已知的主包体积问题:
|
||||
|
||||
- `AuthenticatedApp-*.js` 超过当前 warning 门槛
|
||||
|
||||
这属于仓库当前既有工程问题,不是本批次引入的新断裂。
|
||||
|
||||
## 4.1 2026-04-21 补充修正:会话探测 401 自触发循环
|
||||
|
||||
在这批收口完成后,前端又暴露出一条更细的鉴权恢复回路问题:
|
||||
|
||||
1. `AuthGate` 启动时会调用 `getCurrentAuthUser()` 探测现有会话
|
||||
2. `/api/auth/me` 返回 `401` 时,`apiClient.ts` 会默认广播一次 `AUTH_STATE_EVENT`
|
||||
3. `AuthGate` 自己又监听这个事件并重新 `hydrate()`
|
||||
4. 最终形成 `hydrate -> /auth/me 401 -> emit -> hydrate` 的自触发循环
|
||||
|
||||
这条链的问题不在“是否允许 401”,而在:
|
||||
|
||||
**会话探测请求把“未登录态探测”错误地当成了“全局登录态变更”。**
|
||||
|
||||
因此这里补了一条更细粒度的约束:
|
||||
|
||||
1. `apiClient.ts` 新增 `notifyAuthStateChange` 选项,默认仍保持原有广播行为
|
||||
2. `getCurrentAuthUser()` 作为会话探测请求,显式关闭这类 401 广播
|
||||
3. 真实登录、登出、刷新成功后,仍保留全局鉴权变更通知
|
||||
|
||||
这样修完后:
|
||||
|
||||
1. `AuthGate` 仍会优先尝试服务端会话恢复
|
||||
2. 无会话时会正常落回未登录分支
|
||||
3. 不会因为探测型 401 把自己重新唤醒并刷爆控制台
|
||||
|
||||
## 4.2 2026-04-22 补充修正:公开认证入口误触发 refresh
|
||||
|
||||
在登录弹窗链路继续联调时,又暴露出一条更细的请求边界问题:
|
||||
|
||||
1. 用户处于未登录态,浏览器本地没有 access token
|
||||
2. 点击“获取验证码”会调用 `sendPhoneLoginCode()`
|
||||
3. `authService.ts` 复用了通用 `requestJson(...)`
|
||||
4. `apiClient.ts` 在“无本地 token 且未显式关闭 refresh”时,会先尝试 `POST /api/auth/refresh`
|
||||
5. 若当前浏览器本来也没有 refresh session cookie,就会先打出一条 `401 Unauthorized`
|
||||
6. 最终表现成:验证码接口真正发送前,前端控制台先报一次 `/api/auth/refresh 401`
|
||||
|
||||
这条链的问题不在“验证码接口失败”,而在:
|
||||
|
||||
**登录前公开认证入口被错误当成了需要先补票的受保护请求。**
|
||||
|
||||
因此这里再补一条明确约束:
|
||||
|
||||
1. `sendPhoneLoginCode()`
|
||||
2. `loginWithPhoneCode()`
|
||||
3. `authEntry()`
|
||||
4. `getAuthLoginOptions()`
|
||||
5. `startWechatLogin()`
|
||||
|
||||
以上这些“获取登录态之前”的公开认证入口,统一显式传入:
|
||||
|
||||
1. `skipAuth: true`
|
||||
2. `skipRefresh: true`
|
||||
|
||||
这样修完后:
|
||||
|
||||
1. 未登录用户点击“获取验证码”不会先打 `/api/auth/refresh`
|
||||
2. 公开认证入口不会误带旧 token,也不会制造无意义的 401 噪音
|
||||
3. 真正需要 refresh 的仍然只有已拿到登录态后的受保护请求
|
||||
|
||||
本次补修的定向验证:
|
||||
|
||||
1. `npx vitest run src/services/authService.test.ts`
|
||||
2. `npm run check:encoding`
|
||||
|
||||
---
|
||||
|
||||
## 5. 本批次完成后的实际收益
|
||||
@@ -163,11 +227,11 @@ refresh session / 当前会话 -> 恢复用户
|
||||
|
||||
换句话说,批次 C 的后半段应该拆成:
|
||||
|
||||
1. **C-1:鉴权真相收口**
|
||||
1. **C-1:鉴权真相收口**
|
||||
本批已完成
|
||||
2. **C-2:运行时快照 contract 后端化**
|
||||
2. **C-2:运行时快照 contract 后端化**
|
||||
需要先改后端
|
||||
3. **C-3:前端镜像写入与重复水合退场**
|
||||
3. **C-3:前端镜像写入与重复水合退场**
|
||||
依赖 C-2
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# 工程死分支清理执行记录 E(2026-04-21)
|
||||
|
||||
更新时间:`2026-04-21`
|
||||
|
||||
## 0. 本批次目标
|
||||
|
||||
本批次承接批次 D,继续清掉已经退出 RPG 游戏创作主流程、RPG 运行时玩法主流程、平台基本功能主流程的历史壳层。
|
||||
|
||||
本批次不处理仍需后端 contract 先收口的对象,例如:
|
||||
|
||||
1. `src/services/questDirector.ts`
|
||||
2. `src/services/runtimeItemAiDirector.ts`
|
||||
3. `src/hooks/rpg-runtime-story/runtimeStoryCoordinator.ts`
|
||||
4. `src/services/apiClient.ts`
|
||||
|
||||
这些对象仍属于“前端越界逻辑继续后端化”的后续批次,不按无引用文件直接删除。
|
||||
|
||||
---
|
||||
|
||||
## 1. 删除判定口径
|
||||
|
||||
本批只删除满足下面条件之一的对象:
|
||||
|
||||
1. 无运行时入口、无脚本入口、无当前路由挂载。
|
||||
2. 已有现行正式实现,旧文件只剩 re-export / facade / 兼容命名。
|
||||
3. 只被测试验证旧壳自身,且该测试不再服务当前主流程门禁。
|
||||
4. 文档已明确该对象处于“后续只允许收缩、不再接新逻辑”的兼容残留状态。
|
||||
|
||||
---
|
||||
|
||||
## 2. 本批次已处理对象
|
||||
|
||||
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 |
|
||||
| --- | --- | --- | --- |
|
||||
| `server-node/src/routes/rpgCreationAgentRoutes.ts` | 旧命名 re-export | 当前后端正式路由直接使用 `customWorldAgent.ts` | `server-node/src/routes/customWorldAgent.ts` |
|
||||
| `server-node/src/routes/rpgWorldGalleryRoutes.ts` | 空路由骨架 | 世界广场实际列表和详情已经进入世界库路由 | `server-node/src/routes/rpg-entry/rpgWorldLibraryRoutes.ts` |
|
||||
| `server-node/src/services/RpgAgentOrchestrator.ts` | 旧命名 re-export | 当前正式上下文直接使用 `CustomWorldAgentOrchestrator` | `server-node/src/services/customWorldAgentOrchestrator.ts` |
|
||||
| `server-node/src/services/RpgAgentSessionStore.ts` | 旧命名 re-export | 当前正式上下文直接使用 `CustomWorldAgentSessionStore` | `server-node/src/services/customWorldAgentSessionStore.ts` |
|
||||
| `server-node/src/services/customWorldWorkSummaryService.ts` | 旧兼容入口 | 测试和路由已改为直接使用 RPG 命名服务 | `server-node/src/services/RpgWorldWorkSummaryService.ts` |
|
||||
| `server-node/src/services/customWorldAgentPublishGateService.ts` | 旧发布门禁实现 | 当前 action executor 与作品库发布链已统一走 PublishingService | `server-node/src/services/customWorldAgentPublishingService.ts` |
|
||||
| `server-node/src/services/customWorldAgentPublishService.ts` | 旧发布实现 | 当前发布链不再编译旧 legacy result profile | `server-node/src/services/customWorldAgentPublishingService.ts` |
|
||||
| `server-node/src/modules/custom-world/runtime-profile/runtimeProfileCompiler.ts` | 旧 facade | runtime profile 已拆到目录模块并由 `index.ts` / `runtimeProfile.ts` 承接 | `server-node/src/modules/custom-world/runtime-profile/index.ts` |
|
||||
| `server-node/src/bridges/legacyBuildRuntimeBridge.ts` | 无引用旧桥 | 后端 runtime build / equipment 已直接在正式模块内使用 | `server-node/src/modules/runtime/**` |
|
||||
| `server-node/src/bridges/legacyRuntimeItemResolutionBridge.ts` | 旧桥 | runtime item 解析服务一并删除,正式运行时使用 `runtimeItemModule.ts` | `server-node/src/modules/runtime-item/runtimeItemModule.ts` |
|
||||
| `server-node/src/modules/runtime-item/runtimeItemResolutionService.ts` | 无正式入口 wrapper | 只被 barrel 和自身测试引用,未挂入 Express 运行时主链 | `server-node/src/modules/runtime-item/runtimeItemModule.ts` |
|
||||
| `server-node/src/modules/**/index.ts` | 无引用 barrel | 这些 barrel 没有被当前后端入口消费,反而制造“公共模块入口仍存在”的错觉 | 直接 import 具体正式模块 |
|
||||
| `server-node/src/routes/rpg-*/index.ts` | 无引用 barrel | 当前 Express app 直接 import 具体 route 文件 | `server-node/src/app.ts` 中的具体路由 |
|
||||
| `server-node/src/repositories/rpg-*/index.ts` | 无引用 barrel | 当前上下文直接 import 具体 repository | `server-node/src/server.ts` 中的具体仓储 |
|
||||
| `src/components/DeveloperTeamModal.tsx` | 无入口 UI | 平台主流程没有打开该弹窗的入口 | 无替代 UI,删除历史壳 |
|
||||
| `src/components/LazySkillEffectPreview.tsx` | 无入口 lazy 壳 | 正式技能预览直接使用 `SkillEffectPreview` | `src/components/SkillEffectPreview.tsx` |
|
||||
| `src/components/npcVisualEditorModel.ts` | 旧 NPC 形象写回模型 | 当前 RPG 创作编辑器使用 `CustomWorldNpcVisualEditor` 与结果页新入口 | `src/components/CustomWorldNpcVisualEditor.tsx`、`src/components/rpg-creation-editor/**` |
|
||||
| `src/components/npcVisualEditorPersistence.ts` | 旧 NPC 形象写回持久层 | 只被旧持久化测试引用,正式编辑入口已迁移 | `src/components/rpg-creation-editor/**` |
|
||||
| `src/components/rpg-creation-*/index.ts` | 无引用 barrel | 当前入口直接 import 具体 facade 文件,barrel 没有主流程消费 | 直接 import `RpgCreation*` 具体文件 |
|
||||
| `src/components/rpg-creation-editor/CustomWorldSceneChapterEditorSection.tsx` | 旧 facade | 当前编辑器 section 直接在 `RpgCreationEntityEditorShared.tsx` 中分发 | `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx` |
|
||||
| `src/data/editorValidation.ts` | 旧预设编辑器校验 | 当前主流程和内容门禁不再调用 | `scripts/validate-overrides.ts`、后端 editor API |
|
||||
| `src/editor/shared/EditorNotice.tsx` | 无入口共享 UI | 只被同批删除的 FormFields 使用 | 无替代 UI,删除历史编辑器壳 |
|
||||
| `src/editor/shared/FormFields.tsx` | 无入口共享 UI | 旧编辑器共享表单未接主流程 | 当前 RPG 编辑器组件内聚在 `rpg-creation-editor/**` |
|
||||
| `src/editor/shared/SectionCard.tsx` | 无入口共享 UI | 旧编辑器卡片未接主流程 | 当前 RPG 编辑器组件内聚在 `rpg-creation-editor/**` |
|
||||
| `src/hooks/rpg-runtime-story/npcEncounterActions.ts` | 旧 wrapper | 正式实现已在 `useRpgRuntimeNpcInteraction.ts`,测试已改到正式文件 | `src/hooks/rpg-runtime-story/useRpgRuntimeNpcInteraction.ts` |
|
||||
| `src/hooks/rpg-runtime-story/openingAdventure.ts` | 旧前端开局特殊流程 | 开局营地对白已由后端 `RpgRuntimeStoryActionDomain` 和当前 story context 承接 | `server-node/src/modules/rpg-runtime-story/RpgRuntimeStoryActionDomain.ts` |
|
||||
| `src/hooks/rpg-runtime-story/storyCampCompanion.ts` | 旧前端营地同伴 helper | 只剩旧开局流程和自身测试引用,正式开局上下文已迁到当前 runtime story 链 | 后端 runtime story action domain 与 `storyContextBuilder.ts` |
|
||||
| `src/hooks/rpg-runtime-story/storyRenderingHelpers.ts` | 无入口旧渲染 helper | 当前正式 story presentation 不再 import | `src/hooks/rpg-runtime-story/storyPresentation.ts` |
|
||||
| `src/prompts/questPrompts.ts` | 前端 prompt 残留 | Quest prompt 真相已迁到后端 | `server-node/src/prompts/questPrompts.ts` |
|
||||
| `src/prompts/runtimeItemPrompts.ts` | 前端 prompt 残留 | Runtime item prompt 真相已迁到后端 | `server-node/src/prompts/runtimeItemPrompts.ts` |
|
||||
| `src/services/questPrompt.ts` | 前端 prompt re-export | 只指向同批删除的前端 prompt | `server-node/src/prompts/questPrompts.ts` |
|
||||
| `src/services/runtimeItemAiPrompt.ts` | 前端 prompt re-export | 只指向同批删除的前端 prompt | `server-node/src/prompts/runtimeItemPrompts.ts` |
|
||||
| `src/services/storyEngine/contentDependencyGraph.ts` | 实验性孤岛 | 只被自身测试引用,没有主流程消费 | 后续如需要重新设计到后端 story graph 服务 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 同步调整
|
||||
|
||||
1. `customWorldAgentPhase2/3/4` 测试改为直接实例化 `RpgWorldWorkSummaryService`。
|
||||
2. `customWorldWorkSummaryService.integration.test.ts` 改为直接覆盖 `RpgWorldWorkSummaryService`。
|
||||
3. `npcEncounterActions.test.ts` 改为直接覆盖 `useRpgRuntimeNpcInteraction.ts`,不再通过旧 wrapper。
|
||||
4. `story_opening_camp_dialogue` 的 function catalog 执行路径改为后端 runtime action domain,不再指向已删除旧前端文件。
|
||||
5. NPC function catalog 中 `npc_chat / npc_help / npc_leave / npc_fight / npc_spar / npc_preview_talk` 的 executor 路牌改到现行 `useRpgRuntimeNpcInteraction.ts`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 本批次暂缓对象
|
||||
|
||||
以下对象仍然保留,原因是它们不是“无引用死代码”,而是需要下一轮按 contract 或主链职责迁移:
|
||||
|
||||
1. `src/services/questDirector.ts`
|
||||
2. `src/services/runtimeItemAiDirector.ts`
|
||||
3. `src/services/ai.ts`
|
||||
4. `src/data/sceneObservation.ts`
|
||||
5. `server-node/ecosystem.config.cjs`
|
||||
6. `server-node/src/scripts/syncCustomWorldSavedProfileAssets.ts`
|
||||
|
||||
其中 `ecosystem.config.cjs` 被部署脚本直接使用;`sceneObservation.ts` 被内容 smoke 脚本验证;`syncCustomWorldSavedProfileAssets.ts` 是一次性运维脚本,后续要单独按运维脚本治理口径确认是否归档。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证口径
|
||||
|
||||
本批删除后建议验证:
|
||||
|
||||
1. `npm run check:encoding`
|
||||
2. `npx tsx --test server-node/src/services/customWorldWorkSummaryService.integration.test.ts`
|
||||
3. `npx vitest run src/hooks/rpg-runtime-story/npcEncounterActions.test.ts`
|
||||
4. `npm run server-node:build`
|
||||
5. `npm run build`
|
||||
|
||||
如果 `npm run build` 仍被既有 chunk warning 拦截,需要单独记录为既有门禁问题,不归因到本批删除。
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前结论
|
||||
|
||||
本批次进一步删除了“旧命名入口、旧 facade、旧 prompt 前端镜像、无入口编辑器壳层”这批容易误导后续开发的文件。
|
||||
|
||||
后续清理不应继续按“静态无引用”直接推进,而应进入两类工作:
|
||||
|
||||
1. 运行时 / 任务 / 物品 / AI 的后端 contract 收口。
|
||||
2. RPG 创作编辑器与运行时热点文件的职责拆分。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 工程死分支清理执行记录 F(2026-04-21)
|
||||
|
||||
更新时间:`2026-04-21`
|
||||
|
||||
## 0. 本批次目标
|
||||
|
||||
本批次承接批次 E 的验证结果,继续处理删除后暴露出的最后一组高置信残留:
|
||||
|
||||
1. 已经没有任何代码入口引用的前端任务生成 director。
|
||||
2. 只被内容 smoke 牵住、但不再是正式运行时入口的旧观察文案 helper。
|
||||
3. 带有固定用户、固定 session、固定 profile 的一次性历史同步脚本。
|
||||
4. 清理后暴露出的 function catalog 契约覆盖缺口。
|
||||
|
||||
本批次仍然不按文件名直接删除 `legacy` 命名对象。经核对,`server-node/src/bridges/legacyInventoryRuntimeBridge.ts`、`legacyNpcTask6Bridge.ts`、`legacyQuestProgressBridge.ts`、`legacyQuestRuntimeBridge.ts`、`legacyRuntimeItemBridge.ts`、`legacyTreasureRuntimeBridge.ts` 仍被后端战斗、背包、任务、宝藏主链直接引用,不能按历史命名硬删。
|
||||
|
||||
---
|
||||
|
||||
## 1. 删除判定口径
|
||||
|
||||
本批删除对象必须同时满足:
|
||||
|
||||
1. 修正 `.js -> .ts` 后端源码解析、前端懒加载入口解析后,仍不可从正式入口到达。
|
||||
2. 全仓库代码引用扫描没有正式入口引用。
|
||||
3. 如只被 smoke 或测试牵住,先把 smoke / 测试改到当前正式主链,再删除旧对象。
|
||||
4. 删除后通过对应门禁验证,没有新增悬空 import。
|
||||
|
||||
---
|
||||
|
||||
## 2. 本批次已处理对象
|
||||
|
||||
| 文件 | 判定 | 删除 / 调整原因 | 替代路径 / 当前真相源 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/services/questDirector.ts` | 无代码入口残留 | 正式 quest 生成已由后端 `/api/runtime/quests/generate` 与 `questService.ts` 承接,前端当前没有任何 import | `server-node/src/services/questService.ts`、`server-node/src/modules/quest/runtimeQuestModule.ts` |
|
||||
| `src/data/sceneObservation.ts` | 旧观察文案 helper | 只被 `scripts/smoke-content.ts` 引用,正式观察动作已走 `idle_observe_signs` function 与运行时 story continuation | `src/data/functionCatalog/state/idleObserveSigns.ts`、`src/hooks/rpg-runtime-story/storyChoiceContinuation.ts` |
|
||||
| `server-node/src/scripts/syncCustomWorldSavedProfileAssets.ts` | 一次性硬编码运维脚本 | 脚本内固定用户、session、profile,只服务历史补丁,没有 CLI 参数和当前运维入口 | 无替代;如未来需要,按参数化运维脚本重新设计 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 同步调整
|
||||
|
||||
1. `scripts/smoke-content.ts` 不再 import 旧 `sceneObservation.ts`,改为通过 `resolveFunctionOption('idle_observe_signs', ...)` 验证当前正式 function 目录。
|
||||
2. `packages/shared/src/contracts/rpgRuntimeContracts.test.ts` 不再验证已移除的旧 `story` façade,改为直接验证当前拆分契约。
|
||||
3. `src/data/functionCatalog/` 补齐仍在后端运行时契约中的 function 文档:
|
||||
- `battle_attack_basic`
|
||||
- `battle_use_skill`
|
||||
- `npc_chat_quest_offer_view`
|
||||
- `npc_chat_quest_offer_replace`
|
||||
- `npc_chat_quest_offer_abandon`
|
||||
4. `battle_attack_basic` 与 `battle_use_skill` 只作为后端契约文档登记,不进入 `STATE_FUNCTION_DEFINITIONS`,避免前端本地候选池生成缺少 `runtimePayload.skillId` 的假技能 option。
|
||||
|
||||
---
|
||||
|
||||
## 4. 本批次暂缓对象
|
||||
|
||||
以下对象经本批复核后继续保留:
|
||||
|
||||
1. `server-node/src/services/customWorldAgentRepositoryTestHelpers.ts`
|
||||
2. `server-node/src/services/customWorldAgentTestHelpers.ts`
|
||||
3. `server-node/src/testFixtures/runtimeCharacter.ts`
|
||||
4. `server-node/src/testHttp.ts`
|
||||
|
||||
这些文件不属于正式运行时入口,但当前被后端测试、smoke 与路由边界门禁使用。它们不是 RPG 创作 / 运行时玩法主流程代码,但仍是平台基本质量门禁的一部分,不能在“删除冗余业务代码”批次里直接硬删。
|
||||
|
||||
另保留:
|
||||
|
||||
1. `src/services/runtimeItemAiDirector.ts`
|
||||
2. `src/services/ai.ts`
|
||||
3. `src/services/apiClient.ts`
|
||||
|
||||
这些文件仍被当前主链或前端 SDK 入口引用,后续如继续压缩,必须先完成对应 contract / SDK 拆分,不按无引用规则删除。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证结果
|
||||
|
||||
本批已通过:
|
||||
|
||||
1. `npx vitest run src/data/functionCatalog/functionCatalog.test.ts packages/shared/src/contracts/rpgRuntimeContracts.test.ts`
|
||||
2. `npx tsx scripts/smoke-content.ts`
|
||||
3. `npm run check:encoding`
|
||||
|
||||
并额外确认:
|
||||
|
||||
1. 全仓库代码中不再引用 `sceneObservation`、`questDirector`、`syncCustomWorldSavedProfileAssets`。
|
||||
2. `buildStateFunctionDefinitions()` 中不会出现 `battle_attack_basic` / `battle_use_skill`,这两个 function 只由后端运行时 option 池下发。
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前结论
|
||||
|
||||
本批次后,静态入口扫描中剩余的高置信“不可达源码”已经收敛为测试辅助、测试夹具和 smoke helper。继续删除前需要先重构测试基础设施或迁移剩余前端 SDK,而不应再按文件名或历史命名直接硬删。
|
||||
@@ -1,278 +0,0 @@
|
||||
# 工程优化审查报告(2026-03-29)
|
||||
|
||||
## 说明
|
||||
|
||||
- 扫描范围:`src/`、`scripts/`、`docs/`、`package.json`、`vite.config.ts`、`tsconfig.json`
|
||||
- 已执行校验:`npm run lint`、`npm run build`、`npm run check:content`
|
||||
- 本报告只从工程角度讨论结构、边界、质量门禁、可维护性与可扩展性
|
||||
- 按仓库说明,暂不讨论中文乱码本身
|
||||
|
||||
## 当前结论
|
||||
|
||||
项目当前**可构建、可运行、内容校验可通过**,说明基础功能链路是通的;但从工程视角看,已经出现明显的“单点过重、边界混杂、质量门禁偏弱、编辑器与运行时耦合”问题。继续叠需求会越来越依赖人工记忆和局部经验,回归风险会持续上升。
|
||||
|
||||
当前最值得优先处理的不是单个 UI 细节,而是以下四个工程主题:
|
||||
|
||||
1. 运行时主链路的职责拆分还不够,核心 hook / 组件已经过载
|
||||
2. 缺少真正的工程质量门禁,`lint` 目前本质上只是 `tsc`
|
||||
3. 编辑器、运行时、类后端能力都混在同一个 Vite 配置里
|
||||
4. 持久化、AI 调用、编辑器保存等基础设施仍然是“分散手写”
|
||||
|
||||
## 运行状态快照
|
||||
|
||||
- `npm run lint` 通过
|
||||
- `npm run build` 通过
|
||||
- `npm run check:content` 通过
|
||||
- 应用代码下未发现测试文件:`src/`、`scripts/`、`docs/` 内没有 `*.test.*` / `*.spec.*`
|
||||
- 构建产物已出现较大 chunk
|
||||
- `dist/assets/App-*.js` 约 `407 KB`
|
||||
- `dist/assets/itemCatalog-*.js` 约 `414 KB`
|
||||
- `dist/assets/PresetEditor-*.js` 约 `109 KB`
|
||||
|
||||
## 代码体征
|
||||
|
||||
下列文件已经明显进入“超大模块”区间:
|
||||
|
||||
| 文件 | 行数 | 观察 |
|
||||
| --- | ---: | --- |
|
||||
| `src/hooks/useStoryGeneration.ts` | 3304 | 同时管理剧情、NPC 交互、交易、送礼、招募、任务、角色聊天、道具/锻造接入 |
|
||||
| `src/components/PresetEditor.tsx` | 2244 | 多编辑器入口聚合在一个巨型组件中 |
|
||||
| `src/hooks/useCombatFlow.ts` | 1791 | 同时承担战斗推演、动画时序、逃跑演出、状态落地 |
|
||||
| `src/components/GameShell.tsx` | 1592 | 入口 UI、选角、世界选择、自定义世界、场景切换、浮层控制全部集中 |
|
||||
| `src/types.ts` | 663 | 运行时、AI、编辑器、自定义世界、背包、任务类型集中在一个总文件 |
|
||||
|
||||
补充信号:
|
||||
|
||||
- `src/components/GameShell.tsx` 内有 16 个 `useState`、10 个 `useEffect`、13 个 `useMemo`
|
||||
- `src/hooks/useStoryGeneration.ts` 虽然只有少量 React state,但内部累计 40+ 个函数,已经是“巨型流程控制器”
|
||||
- `src/hooks/useCombatFlow.ts` 内有大量时间常量、动画常量、`sleep + setGameState` 过程式循环,测试成本很高
|
||||
|
||||
## 优先级问题
|
||||
|
||||
## P0:运行时主链路职责过度集中
|
||||
|
||||
证据:
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts:868-930` 进入 hook 后立即开始定义交易、送礼、招募、角色聊天等子流程
|
||||
- `src/hooks/useStoryGeneration.ts:3191-3303` 返回对象同时暴露剧情、任务、NPC UI、角色聊天 UI、背包/锻造 UI
|
||||
- `src/components/GameShell.tsx:293-360` 组件 props 很多,内部 state 也很多,承担“壳层 + 流程 + 浮层 + 自定义世界生成 + 场景切换”
|
||||
- `src/hooks/useCombatFlow.ts:559-1787` 将战斗计算和战斗演出揉在同一层里
|
||||
|
||||
影响:
|
||||
|
||||
- 任何一个新需求都容易同时碰到剧情、UI、战斗、背包、NPC 关系四五条链路
|
||||
- 代码 review 很难聚焦,改动一处时往往需要脑内跟完整条大流程
|
||||
- 单元测试难写,因为逻辑不是纯函数,而是大量闭包 + 过程式状态推进
|
||||
- 长期会形成“只有熟悉历史上下文的人才能安全修改”的隐性门槛
|
||||
|
||||
建议:
|
||||
|
||||
- 将 `useStoryGeneration` 拆为“剧情推进”“NPC 交互”“角色聊天”“任务结算”“模态框控制”几个子域
|
||||
- 将 `useCombatFlow` 拆成“纯战斗结算引擎”和“战斗播放适配层”
|
||||
- 让 `GameShell` 回到壳层职责,只负责路由态、页面态、模态挂载与 props 编排
|
||||
- 以“领域职责”拆分,而不是按“文件太长了随便切一刀”拆分
|
||||
|
||||
## P0:缺少真正的工程质量门禁
|
||||
|
||||
证据:
|
||||
|
||||
- `package.json:11` 的 `lint` 实际只有 `tsc --noEmit`
|
||||
- `package.json` 中没有 `test`、`format`、`lint:fix` 等基础脚本
|
||||
- 根目录未发现 `.eslintrc*`、`.prettierrc*`、`.editorconfig`
|
||||
- 代码目录下没有测试文件
|
||||
|
||||
影响:
|
||||
|
||||
- 当前项目的“能过 lint”只代表类型没炸,不代表风格一致、依赖正确、Hooks 规则正确、死代码已清理
|
||||
- 大型 hook / 大型组件的重构几乎没有自动回归保护
|
||||
- 运行时行为、编辑器行为、AI fallback 行为主要依赖人工回归
|
||||
|
||||
建议:
|
||||
|
||||
- 补齐 ESLint、Prettier、EditorConfig,至少覆盖 React Hooks、import、unused code、复杂度基线
|
||||
- 引入 Vitest,先覆盖纯数据层与纯规则层
|
||||
- 为 `useCombatFlow`、`stateFunctions`、`npcInteractions`、`questFlow` 增加单元测试
|
||||
- 为“开局 -> 选世界 -> 选角色 -> 进入剧情 -> 战斗 -> 存档恢复”补最小 E2E smoke
|
||||
- CI 中至少串联:类型检查 + 单测 + build + 内容校验
|
||||
|
||||
## P1:编辑器、运行时、类后端能力全部耦合在 Vite 配置里
|
||||
|
||||
证据:
|
||||
|
||||
- `vite.config.ts:151-203` 在 Vite 插件里实现了 LLM 代理
|
||||
- `vite.config.ts:206-269` 在 Vite 插件里实现了通用 JSON 文件读写 API
|
||||
- `vite.config.ts:253` 直接写回 `src/data/*.json`
|
||||
- `vite.config.ts:265-266` 和 `vite.config.ts:400-401` 在 `preview` 阶段也挂了这些接口
|
||||
- `vite.config.ts:425-434` 启动时默认把这些“编辑器后端能力”全部注册进去
|
||||
|
||||
影响:
|
||||
|
||||
- 本地编辑器能力与运行时能力没有清晰边界
|
||||
- `preview` 环境仍可写源码文件,发布边界不清晰
|
||||
- 未来如果要做独立部署、多人协作、远程编辑、权限控制,会非常难迁移
|
||||
- Vite 配置同时扮演构建配置、代理层、文件服务层、编辑器后端,职责失衡
|
||||
|
||||
建议:
|
||||
|
||||
- 将编辑器读写 API 从 `vite.config.ts` 抽到独立的本地工具服务或独立脚本
|
||||
- 至少区分 `dev-only write api` 与 `preview/prod read-only api`
|
||||
- 对编辑器保存接口建立统一客户端 SDK,避免组件直接散落 `fetch('/api/...')`
|
||||
- LLM 代理也建议独立成 `server/` 或 `scripts/dev-server/`,不要继续长在构建配置里
|
||||
|
||||
## P1:持久化策略分散,且直接序列化大状态对象
|
||||
|
||||
证据:
|
||||
|
||||
- `src/hooks/useGamePersistence.ts:152-167` 会在状态变化时自动把完整快照写入 `localStorage`
|
||||
- `src/hooks/useGamePersistence.ts:157-163` 快照包含 `gameState + bottomTab + currentStory`
|
||||
- `src/hooks/useGamePersistence.ts:68-116` 恢复逻辑已经开始承担大量 schema 纠偏职责
|
||||
- `src/data/customWorldLibrary.ts:1-282` 自定义世界库单独维护一套 `localStorage` 读写与 normalize
|
||||
- `src/hooks/useGameSettings.ts` 也单独维护一套本地设置持久化
|
||||
|
||||
影响:
|
||||
|
||||
- 状态结构一旦继续膨胀,快照写入频率和反序列化成本都会增加
|
||||
- schema 迁移会越来越依赖手工 normalize 补丁
|
||||
- 不同持久化入口各写一套 parser / normalizer,风格和鲁棒性难统一
|
||||
- 当前保存的是“运行中大对象”,而不是“稳定领域快照”,长期会放大兼容成本
|
||||
|
||||
建议:
|
||||
|
||||
- 建立统一的 persistence 层,集中管理 key、version、migration、节流、序列化策略
|
||||
- 对 `GameState` 做“可持久化切片”和“运行时临时切片”分层
|
||||
- 自动保存增加节流/去抖,避免每次状态波动都全量落盘
|
||||
- 如果继续扩展角色聊天、自定义世界、编辑器草稿,建议评估 IndexedDB 替代 `localStorage`
|
||||
|
||||
## P1:运行时与编辑器仍在同一个前端入口体系中,包体继续膨胀
|
||||
|
||||
证据:
|
||||
|
||||
- `src/main.tsx:21-34` 通过 `window.location.pathname` 手写分发页面
|
||||
- `src/main.tsx:60` 只有“游戏”和“PresetEditor”两个大入口
|
||||
- `PresetEditor`、`ItemCatalogEditor`、`StateFunctionEditor` 都属于重型模块
|
||||
- 构建产物已经出现 `App` 约 `407 KB`、`itemCatalog` 约 `414 KB` 的 chunk
|
||||
|
||||
影响:
|
||||
|
||||
- 游戏端与编辑器端的演进节奏被绑定在一个 SPA 入口上
|
||||
- 编辑器相关数据和静态资源容易继续抬高构建体积
|
||||
- 未来增加更多编辑器页、更多世界模板、更多资源目录后,冷启动成本会更明显
|
||||
|
||||
建议:
|
||||
|
||||
- 将编辑器拆成独立入口,至少做成独立 route module,而不是单个 `PresetEditor`
|
||||
- 继续下钻按 tab 做懒加载,尤其是 `items/functions/npcs`
|
||||
- 将静态大数据、资源目录索引、编辑器专用预览逻辑做更细的 chunk 拆分
|
||||
- 如果项目后续会长期保留编辑器,建议直接分成 game app / editor app 两个 entry
|
||||
|
||||
## P2:编辑器基础设施重复实现较多
|
||||
|
||||
证据:
|
||||
|
||||
- `src/components/PresetEditor.tsx:111-181` 自己实现 `cloneValue`、`saveJsonObject`
|
||||
- `src/components/StateFunctionEditor.tsx:113-130` 再次实现 `cloneValue`、`SectionCard`
|
||||
- `src/components/ItemCatalogEditor.tsx:94` 再次实现保存请求
|
||||
- `src/hooks/useInventoryFlow.ts:8`、`src/hooks/useEquipmentFlow.ts:10`、`src/hooks/useForgeFlow.ts:12`、`src/hooks/useTreasureFlow.ts:10` 重复声明 `CommitGeneratedState`
|
||||
|
||||
影响:
|
||||
|
||||
- 修改保存行为、错误处理、深拷贝策略时需要多处同步
|
||||
- 编辑器 UI 风格与交互行为容易逐步漂移
|
||||
- 公共契约没有收拢到共享层,维护成本会逐步抬高
|
||||
|
||||
建议:
|
||||
|
||||
- 抽 `editor/shared/` 层,集中放保存 SDK、表单字段、卡片容器、克隆工具、错误处理
|
||||
- 抽通用的 `CommitGeneratedState` 类型定义
|
||||
- 将编辑器请求和覆盖保存逻辑统一走一个 client
|
||||
|
||||
## P2:类型系统已经出现“总文件过载”
|
||||
|
||||
证据:
|
||||
|
||||
- `src/types.ts` 共 663 行
|
||||
- `src/types.ts:1-260` 同时包含世界、动画、技能、对话、自定义世界、物品等类型
|
||||
- `src/types.ts:536-663` 又继续承接剧情、聊天、任务、`GameState`、AI 响应
|
||||
|
||||
影响:
|
||||
|
||||
- 任一领域类型变化都会增加总文件冲突概率
|
||||
- 新人理解类型边界成本高
|
||||
- 编辑器类型、运行时类型、AI 传输类型被放在一起,不利于演化
|
||||
|
||||
建议:
|
||||
|
||||
- 按领域拆分:`types/combat.ts`、`types/story.ts`、`types/item.ts`、`types/customWorld.ts`、`types/persistence.ts`
|
||||
- `GameState` 相关类型与 editor override 类型分开
|
||||
- AI request/response contract 单独收口,避免继续堆进总类型文件
|
||||
|
||||
## P2:AI 客户端层过厚,且重复了多套请求与解析逻辑
|
||||
|
||||
证据:
|
||||
|
||||
- `src/services/ai.ts` 共 1153 行
|
||||
- `src/services/ai.ts:540-605`、`608-678`、`745-790` 分别手写了 JSON completion、纯文本 completion、流式 completion
|
||||
- `src/services/ai.ts:680-697` 手写了多段 JSON 解析兜底
|
||||
- `src/services/ai.ts:76-78`、`591-594`、`662-666` 主要依赖 `console.*` 打日志
|
||||
|
||||
影响:
|
||||
|
||||
- LLM 行为扩展时容易继续复制请求模板、错误处理、超时逻辑
|
||||
- 错误分类不够稳定,观测主要停留在 console 层
|
||||
- prompt、transport、fallback、parse 被放在一起,后续测试和替换模型都不够轻
|
||||
|
||||
建议:
|
||||
|
||||
- 抽 `llmClient`,统一 transport、timeout、stream、error taxonomy
|
||||
- 抽 `llmParsers`,将 JSON parse / plain text parse / suggestion parse 独立
|
||||
- 为关键 prompt 输出建立 fixture 测试,至少覆盖 fallback 与异常响应
|
||||
- 如果后续要接多个模型,尽早把 provider 层和 prompt 层解耦
|
||||
|
||||
## P2:手写路由与死代码开始累积
|
||||
|
||||
证据:
|
||||
|
||||
- `src/main.tsx:21-34` 采用手写 `pathname.startsWith(...)`
|
||||
- `src/components/GameShell.tsx:1511` 存在 `false && showTeamModal`
|
||||
|
||||
影响:
|
||||
|
||||
- 路由能力不具备可扩展性,也不利于后续加 404、重定向、权限判断、嵌套路由
|
||||
- 死代码继续堆积后,会误导维护者对真实入口和真实 UI 状态的判断
|
||||
|
||||
建议:
|
||||
|
||||
- 引入正式路由层,哪怕只做轻量路由也比手写分发更清晰
|
||||
- 清理已经废弃的 UI 分支和不可达逻辑
|
||||
- 对“临时下线的功能”改为 feature flag 或明确注释,不要用 `false &&`
|
||||
|
||||
## 建议落地顺序
|
||||
|
||||
### 第一阶段:先补工程底座
|
||||
|
||||
- 增加 ESLint / Prettier / EditorConfig
|
||||
- 增加 `test` 脚本与 Vitest
|
||||
- 把 CI 最小闭环搭起来:类型检查、单测、build、内容校验
|
||||
|
||||
### 第二阶段:先拆边界,再拆大文件
|
||||
|
||||
- 先把 Vite 中的编辑器写文件接口、LLM 代理抽走
|
||||
- 再把 `GameShell`、`useStoryGeneration`、`useCombatFlow` 按职责拆域
|
||||
- 拆分时优先保持外部接口稳定,避免一次性全仓大改
|
||||
|
||||
### 第三阶段:收敛基础设施
|
||||
|
||||
- 统一 persistence 层
|
||||
- 统一 editor shared 层
|
||||
- 统一 AI client 层
|
||||
- 拆分 `types.ts`
|
||||
|
||||
### 第四阶段:降低发布成本
|
||||
|
||||
- 将 editor 与 game 做更明确的入口拆分
|
||||
- 优化 chunk 边界
|
||||
- 评估是否把编辑器做成独立 app
|
||||
|
||||
## 一句话结论
|
||||
|
||||
这个仓库当前最需要优化的不是“再补几个功能”,而是**把已经验证有效的玩法与工具链,从“靠大文件和经验串起来”升级为“靠清晰边界、统一基础设施和自动化门禁支撑起来”**。只要这一步不做,后续每次加内容、加编辑器能力、加 AI 流程,工程成本都会持续上升。
|
||||
@@ -1,290 +0,0 @@
|
||||
# 工程优化审查报告(2026-03-30)
|
||||
|
||||
## 审查范围
|
||||
|
||||
- 扫描范围:`src/`、`scripts/`、`docs/`、`.github/`、`package.json`、`tsconfig.json`、`vite.config.ts`
|
||||
- 实际执行:`npm run lint`、`npm run test`、`npm run build`、`npm run check:content`
|
||||
- 说明:按仓库要求,本报告不讨论中文乱码问题,只讨论工程结构、边界、质量门禁、可维护性和后续扩展成本
|
||||
|
||||
## 先说结论
|
||||
|
||||
这轮代码库相较 `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` 已经有明显进展,项目不再是“所有能力都糊在一个入口文件里”的状态了,但整体仍然处于“重构过渡期”。
|
||||
|
||||
已经落地的积极变化:
|
||||
|
||||
- 入口路由已经从手写 `pathname` 分发,收敛到 `src/main.tsx` + `src/routing/appRoutes.tsx`
|
||||
- 持久化能力已经抽到 `src/persistence/`
|
||||
- 编辑器公共能力已经出现 `src/editor/shared/`
|
||||
- `CI + ESLint + Prettier + Vitest` 已经接入
|
||||
- 本地 API 插件已经从 `vite.config.ts` 抽走,落到 `scripts/dev-server/localApiPlugins.ts`
|
||||
- `preview` 环境里的 JSON 写入接口已经改成只读,这一点比上轮更安全
|
||||
|
||||
但当前仍然存在 5 个值得优先处理的工程问题:
|
||||
|
||||
1. 运行时主链仍然过于集中,`story/combat` 的真实边界还没有彻底拆开
|
||||
2. `src/services/ai.ts` 仍处于迁移中间态,存在重复实现和旧逻辑残留
|
||||
3. 编辑器主入口仍是大型聚合组件,迁移残留没有清干净
|
||||
4. 质量门禁已经有框架,但还不够“硬”,warning 和测试覆盖缺口仍然明显
|
||||
5. 运行时渲染层和构建体积仍偏重,重 UI 模块还没拆到合适粒度
|
||||
|
||||
## 当前运行状态
|
||||
|
||||
- `npm run test` 通过,6 个测试文件共 18 个测试全部通过
|
||||
- `npm run build` 通过
|
||||
- `npm run check:content` 通过
|
||||
- `npm run lint` 通过,但仍有 76 条 warning
|
||||
|
||||
当前构建产物里仍然存在较重 chunk:
|
||||
|
||||
- `dist/assets/GameCanvas-*.js` 约 `346.58 kB`
|
||||
- `dist/assets/App-*.js` 约 `326.89 kB`
|
||||
- `dist/assets/index-*.js` 约 `197.80 kB`
|
||||
- `dist/assets/index-*.css` 约 `117.37 kB`
|
||||
|
||||
## P0:运行时主链仍然过于集中,Story/Combat 边界还没有拆透
|
||||
|
||||
### 现状
|
||||
|
||||
虽然 `App.tsx` 已经明显瘦身,`GameShell` 也比之前更像壳层,但真正决定游戏推进的主逻辑仍然高度集中在两个大 hook 里:
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts:824`
|
||||
- `src/hooks/useCombatFlow.ts:382`
|
||||
|
||||
### 证据
|
||||
|
||||
`useStoryGeneration` 仍然同时编排了多个本应继续拆开的子领域:
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts:852` 接入 `useCharacterChatFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1583` 接入 `useTreasureFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1588` 接入 `useInventoryFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1593` 接入 `useEquipmentFlow`
|
||||
- `src/hooks/useStoryGeneration.ts:1597` 接入 `useForgeFlow`
|
||||
- 文件总长仍有约 `3240` 行
|
||||
- 结尾返回对象同时暴露剧情推进、地图旅行、NPC 交易/送礼/招募、角色聊天、背包与锻造 UI 能力,典型位置在 `src/hooks/useStoryGeneration.ts:3171-3219`
|
||||
|
||||
`useCombatFlow` 也不是纯计算层,它仍然同时承担:
|
||||
|
||||
- 战斗前后状态推导
|
||||
- 动画播放与时间推进
|
||||
- `setGameState` 驱动的可视化编排
|
||||
- 逃跑流程与 story 响应同步
|
||||
|
||||
关键位置:
|
||||
|
||||
- `src/hooks/useCombatFlow.ts:382` `useCombatFlow`
|
||||
- `src/hooks/useCombatFlow.ts:1195` `playEscapeSequenceWithStorySync`
|
||||
|
||||
### 影响
|
||||
|
||||
- 任何一个“剧情选项新增”都很容易同时碰到 battle、npc、quest、inventory、chat 五条链路
|
||||
- review 成本高,回归范围判断依赖人脑上下文
|
||||
- 单测很难往 hook 级别补,因为副作用、异步节奏和 UI 状态混在一起
|
||||
- 后续想继续做 camp、custom world、更多 companion 玩法时,改动会继续集中到这两个入口
|
||||
|
||||
### 建议
|
||||
|
||||
- 把 `useStoryGeneration` 继续下钻成“剧情推进 orchestrator + 领域 action service”
|
||||
- `useStoryGeneration` 自己只保留编排,不再直接维护 trade/gift/recruit/chat/inventory/forge 的全部细节
|
||||
- `useCombatFlow` 继续向“纯战斗结算”和“播放适配层”分离
|
||||
- 先稳定公开接口,再做内部拆分,避免一次性大改
|
||||
|
||||
## P1:AI 服务迁移只完成了一半,`src/services/ai.ts` 仍然存在双轨实现
|
||||
|
||||
### 现状
|
||||
|
||||
仓库已经新增了:
|
||||
|
||||
- `src/services/llmClient.ts`
|
||||
- `src/services/llmParsers.ts`
|
||||
- `src/services/aiFallbacks.ts`
|
||||
- `src/services/aiTypes.ts`
|
||||
|
||||
这说明拆层方向是对的。但 `src/services/ai.ts` 还没有真正变成“纯 orchestration 层”,里面仍然保留着一整套旧 transport / parse / fallback 逻辑。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/services/ai.ts:64-66` 已经开始导入 `llmClient`
|
||||
- `src/services/ai.ts:89-95` 仍然保留本地 `resolveTimeoutMs` 和超时常量
|
||||
- `src/services/ai.ts:647` 仍然保留 `_requestPlainTextCompletion`
|
||||
- `src/services/ai.ts:719` 仍然保留 `_parseJsonResponseText`
|
||||
- `src/services/ai.ts:739` 仍然保留 `_parseLineListContent`
|
||||
- `src/services/ai.ts:784` 仍然保留 `_streamPlainTextCompletion`
|
||||
- `src/services/ai.ts:885-904` 仍然保留一批旧的 `_buildOffline...` helper
|
||||
|
||||
与之对应,新的实现已经在下面这些文件里存在:
|
||||
|
||||
- `src/services/llmClient.ts`
|
||||
- `src/services/llmParsers.ts`
|
||||
|
||||
### 影响
|
||||
|
||||
- 同一类能力现在有两套真相源,后续改错误分类、超时策略、SSE 行为时容易漏改
|
||||
- 新同学读代码时很难判断应该继续改 `ai.ts`,还是应该去改 `llmClient.ts`
|
||||
- 迁移残留会拉高维护成本,也会让测试边界变得模糊
|
||||
|
||||
### 建议
|
||||
|
||||
- 把 `src/services/ai.ts` 收敛成“业务 prompt 编排 + fallback 选择”层
|
||||
- 彻底删掉未再需要的 `_requestPlainTextCompletion`、`_streamPlainTextCompletion`、`_parse*` 等旧 helper
|
||||
- 所有 transport / timeout / connectivity error / SSE 解析都只保留在 `llmClient.ts` 和 `llmParsers.ts`
|
||||
- 迁移完成后,给 `ai.ts` 增加一组 orchestration 级测试,防止 fallback 分支回归
|
||||
|
||||
## P1:编辑器主入口仍然太重,而且过渡态残留还在
|
||||
|
||||
### 现状
|
||||
|
||||
编辑器公共能力已经开始沉淀到 `src/editor/shared/`,这是好事;但主编辑器入口仍然比较重,且部分文件还保留着迁移过程里的死代码和注释块。
|
||||
|
||||
### 证据
|
||||
|
||||
`PresetEditor` 仍然是一个大型聚合组件:
|
||||
|
||||
- `src/components/PresetEditor.tsx:402` `CharacterPresetPanel`
|
||||
- `src/components/PresetEditor.tsx:1174` `SceneNpcPresetPanel`
|
||||
- `src/components/PresetEditor.tsx:1547` `ScenePresetPanel`
|
||||
- `src/components/PresetEditor.tsx:1852` `MonsterPresetPanel`
|
||||
- `src/components/PresetEditor.tsx:2218` `PresetEditor`
|
||||
- 文件总长仍有约 `2279` 行
|
||||
|
||||
同时,文件里还留着明显的过渡态残留:
|
||||
|
||||
- `src/components/PresetEditor.tsx:227` 仍然保留未使用的 `_SectionCard`
|
||||
- `src/components/NpcVisualEditor.tsx:684` 保留 `if (false)` 的旧保存路径
|
||||
- `src/components/NpcVisualEditor.tsx:685` 明确写着 “Deprecated inline save path kept only until the shared client migration is cleaned up.”
|
||||
- `src/components/NpcVisualEditor.tsx:724` 还有第二处 `if (false)` 残留
|
||||
|
||||
### 影响
|
||||
|
||||
- 编辑器后续继续扩展时,容易重新长回“大一统文件”
|
||||
- 过渡代码会误导维护者,以为旧保存链路仍然有效
|
||||
- 公共层虽然建起来了,但如果不清理旧代码,长期会形成“共享层 + 本地特例”并存
|
||||
|
||||
### 建议
|
||||
|
||||
- 以“一个 tab 一个容器”的方式,把 `PresetEditor` 再拆一层
|
||||
- 清理 `NpcVisualEditor` 里的废弃代码块,不要再保留 `if (false)` 分支
|
||||
- 对编辑器共享层设定明确规则:保存请求、克隆、Section 容器、错误提示都必须走 shared
|
||||
- 对编辑器做一次“小型迁移完成清扫”,优先删掉已经废弃但还挂在文件里的旧路径
|
||||
|
||||
## P1:质量门禁已经接上,但还不够硬
|
||||
|
||||
### 现状
|
||||
|
||||
基础设施已经比上轮完整很多,但当前门禁仍然偏“有检查,不够严格”。
|
||||
|
||||
### 证据
|
||||
|
||||
当前 lint 结果:
|
||||
|
||||
- 本次 `npm run lint` 实际输出 `76` 条 warning,虽然命令返回成功
|
||||
|
||||
脚本和规则层面的原因也很明确:
|
||||
|
||||
- `package.json:12` 的 `lint` 仍然是 `eslint . ... && tsc --noEmit`,没有 `--max-warnings 0`
|
||||
- `package.json:11` 的 `lint:guardrails` 虽然加了 `--max-warnings 0`,但它只覆盖一组显式 allowlist 文件
|
||||
- `package.json:18` 的 `check` 会先跑 `lint:guardrails`,再跑宽松版 `lint`
|
||||
- `.eslintrc.cjs:45-61` 里大量规则仍然是 `warn`
|
||||
- `.github/workflows/ci.yml:28-40` 已经把 `lint / guardrails / test / build / check:content` 都接到 CI,但 warning 仍能稳定进主干
|
||||
|
||||
测试覆盖也还是偏薄:
|
||||
|
||||
- `src/` 当前共有 `126` 个文件
|
||||
- 其中测试文件只有 `6` 个
|
||||
- 现有测试主要覆盖 `routing`、`persistence`、`jsonClient`、`llmParsers`、`battlePlan`
|
||||
- 关键主链如 `useStoryGeneration`、`useCombatFlow` 播放层、`GameShell` 集成链路、编辑器保存流程仍然没有直接测试
|
||||
|
||||
### 影响
|
||||
|
||||
- 代码库会持续积累“已知 warning,但先不处理”的债务
|
||||
- 工程信号会逐渐失真,lint 通过不代表代码足够干净
|
||||
- 大 hook 和大组件的重构依然主要依赖人工回归
|
||||
|
||||
### 建议
|
||||
|
||||
- 先把 warning 收敛到一个可控范围,再把全仓 `lint` 切成 `--max-warnings 0`
|
||||
- `lint:guardrails` 不要长期靠 allowlist,应该逐步扩大到全仓
|
||||
- 优先补三类测试:
|
||||
- `useStoryGeneration` 的状态推进和 modal 决策
|
||||
- `useCombatFlow` 播放层的关键分支
|
||||
- 编辑器保存链路和覆盖数据回写
|
||||
|
||||
## P2:运行时渲染层仍然偏重,chunk 也还没有拆到理想粒度
|
||||
|
||||
### 现状
|
||||
|
||||
入口已经有了 route lazy load,模态框也做了一部分懒加载,但核心运行时渲染层仍然比较重。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/components/AdventurePanel.tsx:470` 导出主组件,文件总长约 `1538` 行
|
||||
- `src/components/GameCanvas.tsx:472` 导出主组件,文件总长约 `1131` 行
|
||||
- `src/components/GameCanvas.tsx:768` 仍然存在 `false && companions.map(...)` 的死分支
|
||||
- 本次构建里 `GameCanvas` 和 `App` 仍然是最大 chunk 之一
|
||||
|
||||
### 影响
|
||||
|
||||
- 运行时页面的首屏与热区模块仍然偏重
|
||||
- 渲染逻辑、场景动画逻辑、实体选中逻辑继续堆在同一层,review 和测试都偏吃力
|
||||
- 清理死分支前,维护者对“哪些渲染路径是真实生效的”判断成本更高
|
||||
|
||||
### 建议
|
||||
|
||||
- `GameCanvas` 继续拆成 scene layer、entity layer、effect layer、overlay layer
|
||||
- `AdventurePanel` 继续下沉 quest/stats/settings/reward 子面板
|
||||
- 清理 `false &&` 与未使用辅助组件,避免假分支继续留在主路径文件中
|
||||
- 结合真实 chunk 数据做一次 route 内部分包,而不是只靠入口级 lazy
|
||||
|
||||
## P2:TypeScript 安全基线仍然偏宽松
|
||||
|
||||
### 现状
|
||||
|
||||
当前类型拆分方向是好的,`src/types.ts` 已经退化成 barrel,真实类型开始向 `src/types/` 下沉。但 TypeScript 编译配置还比较保守,类型系统还没有真正变成强约束。
|
||||
|
||||
### 证据
|
||||
|
||||
- `tsconfig.json:12` `skipLibCheck: true`
|
||||
- `tsconfig.json:16` `allowJs: true`
|
||||
- 当前没有启用 `strict`
|
||||
- 当前没有启用 `noUncheckedIndexedAccess`
|
||||
|
||||
### 影响
|
||||
|
||||
- 对大对象和字典访问的保护仍然偏弱
|
||||
- 新模块迁移到更细类型后,收益会被宽松编译选项部分抵消
|
||||
- “代码能过类型检查”并不等于边界足够安全
|
||||
|
||||
### 建议
|
||||
|
||||
- 不建议一次性全仓开严格模式
|
||||
- 可以先从 `src/services/`、`src/persistence/`、`src/hooks/combat/` 这些相对纯的目录启更严格约束
|
||||
- 至少先评估开启 `noUncheckedIndexedAccess` 和减少 `allowJs` 的必要性
|
||||
|
||||
## 建议的落地顺序
|
||||
|
||||
### 第一阶段:先把过渡态清干净
|
||||
|
||||
- 清理 `ai.ts` 的旧 transport / parser / fallback 实现
|
||||
- 清理 `NpcVisualEditor`、`GameCanvas`、`PresetEditor` 等文件里的 `if (false)`、未使用 helper、废弃注释块
|
||||
- 把 lint warning 数量先打下来
|
||||
|
||||
### 第二阶段:拆主链,不再让大 hook 继续膨胀
|
||||
|
||||
- 继续拆 `useStoryGeneration`
|
||||
- 继续拆 `useCombatFlow`
|
||||
- 优先把“领域动作”和“播放/展示编排”分开
|
||||
|
||||
### 第三阶段:补门禁
|
||||
|
||||
- 给主链补单测和少量集成 smoke
|
||||
- 让全仓 lint 朝 `--max-warnings 0` 收敛
|
||||
- 把 warning 从“长期存在”变成“短周期清零”
|
||||
|
||||
### 第四阶段:优化运行时体积
|
||||
|
||||
- 细化 `GameCanvas` 和 `AdventurePanel` 的模块边界
|
||||
- 按实际交互热区做 chunk 继续拆分
|
||||
- 用真实构建产物持续追踪是否降重
|
||||
|
||||
## 一句话结论
|
||||
|
||||
这轮仓库已经从“完全依赖大文件硬扛”进步到“基础设施开始成形”,但当前最需要做的已经不是继续加功能,而是把这轮重构收尾做完整:继续拆主链、删掉迁移残留、把 lint/test 门禁变硬、再顺手压缩运行时大模块。只要这一步补上,后续加剧情、加编辑器能力、加自定义世界都会轻很多。
|
||||
@@ -1,200 +0,0 @@
|
||||
# 工程优化审查报告(2026-04-01)
|
||||
|
||||
## 审查范围
|
||||
|
||||
- 扫描范围:`src/`、`scripts/`、`docs/`、`.github/`、`package.json`、`tsconfig*.json`、`vite.config.ts`、`vitest.config.ts`
|
||||
- 审查方式:阅读当前工作区代码结构,抽查核心运行时、编辑器、服务层与开发脚本,并执行工程命令验证现状
|
||||
- 当前快照说明:仓库存在大量未提交改动,本报告基于当前工作区状态,不假定这些改动都已经合入主分支
|
||||
- 说明:按仓库要求,不把中文乱码本身当成本次审查重点;只讨论工程结构、门禁、可维护性、可测试性和扩展成本
|
||||
|
||||
## 已执行检查
|
||||
|
||||
- `npm run lint:eslint`
|
||||
结果:失败。`src/components/ItemCatalogEditor.tsx:167` 存在未使用的 `isSearchPending` 和 `startTransition`
|
||||
- `npm run typecheck`
|
||||
结果:通过
|
||||
- `npm run test`
|
||||
结果:通过,默认套件实际执行 10 个测试文件、28 个测试
|
||||
- `npm run build`
|
||||
结果:通过,但 `src/services/customWorldPresentation.ts:163-169` 出现 duplicate key 警告
|
||||
- `npm run check:content`
|
||||
结果:通过
|
||||
|
||||
## 当前结论
|
||||
|
||||
这轮代码库已经明显比前几版更有工程骨架了,至少有这些积极变化:
|
||||
|
||||
- `src/main.tsx` + `src/routing/appRoutes.tsx` 已经承担了入口路由分发
|
||||
- `src/App.tsx` 已经比过去瘦很多,主流程开始交给 hook 和壳组件
|
||||
- `src/components/PresetEditor.tsx` 已经成为较薄的 lazy shell,而不是继续堆成巨型入口
|
||||
- `src/editor/shared/jsonClient.ts`、`src/persistence/`、`src/hooks/combat/`、`src/hooks/story/` 这些目录说明仓库已经开始做分层
|
||||
- CI、Vitest、ESLint、内容校验脚本都已经接上,不再是完全裸奔状态
|
||||
|
||||
但从工程角度看,当前最值得优先优化的,不是继续加功能,而是把“半完成的工程化”补齐。核心问题集中在 6 个方面。
|
||||
|
||||
## P0:质量门禁和真实风险点仍然脱节
|
||||
|
||||
### 现状
|
||||
|
||||
仓库已经引入了 lint、typecheck、test、build 和 content checks,但关键热区并没有真正纳入统一门禁。
|
||||
|
||||
### 证据
|
||||
|
||||
- `.eslintrc.cjs:47-63` 的 `ignorePatterns` 直接跳过了多个高复杂度核心文件:
|
||||
`src/components/AdventurePanel.tsx`、`src/components/NpcVisualEditor.tsx`、`src/components/preset-editor/PresetEditorPanels.tsx`、`src/hooks/useStoryGeneration.ts`、`src/services/customWorldPresentation.ts`
|
||||
- `tsconfig.typecheck-guardrails.json:6-15` 只对非常有限的一小组文件开启严格类型检查,远没有覆盖主运行时链路
|
||||
- `vitest.config.ts:8-10` 把 `customWorldPresentation` 映射到 stub,`vitest.config.ts:20` 还排除了真实存在的 `src/services/ai.test.ts`
|
||||
- 当前 `src/` 下共有 161 个文件,测试文件共有 11 个,但默认套件只执行其中 10 个
|
||||
- `npm run build` 已经能暴露 `src/services/customWorldPresentation.ts:163-169` 的 duplicate key 警告,但这块文件同时被 ESLint ignore、被 Vitest stub 掉,说明真实风险没有被完整看见
|
||||
|
||||
### 影响
|
||||
|
||||
- 工程信号不一致:`test` 绿、`build` 过,不代表关键模块真的健康
|
||||
- 复杂模块越是难测,越容易被长期豁免,最后演变成“最关键的地方最不受控”
|
||||
- 后续重构会缺乏可靠的回归保护,review 只能更多依赖人工记忆
|
||||
|
||||
### 建议
|
||||
|
||||
- 先缩小 `.eslintrc.cjs` 的 ignore 范围,优先把 `useStoryGeneration.ts`、`customWorldPresentation.ts`、`PresetEditorPanels.tsx` 拉回 lint
|
||||
- 把 `src/services/ai.test.ts` 重新纳入默认测试套件,除非有明确且短期的阻塞原因
|
||||
- 不要长期依赖 `tsconfig.typecheck-guardrails.json` 的 allowlist,至少把 `src/hooks/`、`src/services/`、`src/components/game-shell/` 逐步纳入 strict 范围
|
||||
- 对 build warning 建立明确策略:要么在 CI 中失败,要么把 warning 收敛到零
|
||||
|
||||
## P0:当前工作区不在真正的绿色基线
|
||||
|
||||
### 现状
|
||||
|
||||
当前代码不是“纯优化空间”问题,而是已经存在直接可见的门禁破口。
|
||||
|
||||
### 证据
|
||||
|
||||
- `package.json:11-15` 把 `lint:eslint` 和 `typecheck` 定义成正式脚本,说明它们本来就属于项目基线
|
||||
- 实际执行 `npm run lint:eslint` 时,`src/components/ItemCatalogEditor.tsx:167` 报出未使用变量错误
|
||||
- `src/components/ItemCatalogEditor.tsx:167` 引入了 `useTransition()` 返回值,但当前组件没有消费它
|
||||
- `npm run build` 虽然成功,但 `src/services/customWorldPresentation.ts:163-169` 仍然有重复 object key 警告
|
||||
|
||||
### 影响
|
||||
|
||||
- 团队会越来越难区分“可接受的技术债”和“已经破坏基线的问题”
|
||||
- 继续叠加功能会把问题扩散到更多文件,后面补起来成本更高
|
||||
|
||||
### 建议
|
||||
|
||||
- 先恢复工作区绿色基线,再继续推进大功能
|
||||
- 把“lint 零错误、build 零 warning”作为下一轮工程整理的硬目标
|
||||
|
||||
## P1:运行时主链路仍然被少数超级模块吸住
|
||||
|
||||
### 现状
|
||||
|
||||
入口已经变薄,但主复杂度仍集中在少数大文件里,尤其是故事推进、战斗同步和界面编排三层。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts` 当前约 2210 行
|
||||
- `src/hooks/useStoryGeneration.ts:694` 导出主 hook,`src/hooks/useStoryGeneration.ts:1416` 接入 `useTreasureFlow`,后面还继续承接 NPC 互动、库存、打字机、AI、历史推进和故事回写
|
||||
- `src/hooks/useCombatFlow.ts:134` 是主战斗 hook,`src/hooks/useCombatFlow.ts:796-832` 仍然负责逃跑流程与 story sync 的耦合
|
||||
- `src/components/GameShell.tsx` 当前约 791 行,`src/components/GameShell.tsx:260-269` 管理一组本地 UI 状态,`src/components/GameShell.tsx:482` 继续处理场景切换时的选择编排
|
||||
- 构建产物里 `dist/assets/App-*.js` 约 389 kB,`dist/assets/index-*.js` 约 198 kB,说明主运行时 chunk 仍然偏重
|
||||
|
||||
### 影响
|
||||
|
||||
- 任何一个功能变化都容易跨 story、combat、transition、panel 几条链一起改
|
||||
- hook 单测越来越难写,因为副作用、异步和 UI 编排仍然混在一起
|
||||
- App 主 chunk 偏重,会继续拖累首屏和回归速度
|
||||
|
||||
### 建议
|
||||
|
||||
- 继续把 `useStoryGeneration` 收敛成 orchestration 层,把 treasure、NPC、inventory、chat、typewriter、AI 回写拆成更纯的领域 action
|
||||
- 让 `useCombatFlow` 更明确地区分“战斗结算”和“播放同步”
|
||||
- 把 `GameShell` 进一步下沉为 scene transition、selection flow、overlay panel 三类 view-model
|
||||
|
||||
## P1:编辑器共享层只迁移了一半,重复基础设施还在
|
||||
|
||||
### 现状
|
||||
|
||||
编辑器入口已经做了 shell 化,但真正的复杂度仍然堆在大型面板组件里,而且共享层没有吃干净。
|
||||
|
||||
### 证据
|
||||
|
||||
- `src/components/PresetEditor.tsx:41` 的入口已经很薄,说明方向是对的
|
||||
- 但 `src/components/preset-editor/PresetEditorPanels.tsx` 仍然约 2163 行
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:55` 仍然自带 `cloneValue`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:117` 仍然自带 `saveJsonObject`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:189` 仍然自带 `SectionCard`
|
||||
- 与之对应,`src/editor/shared/jsonClient.ts:29-40` 已经提供了共享版 `fetchJson` / `saveJsonObject`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:364`、`:1128`、`:1500`、`:1806` 仍然把四个大型 panel 放在同一个文件里
|
||||
|
||||
### 影响
|
||||
|
||||
- 编辑器的保存、错误处理、基础 UI 容器会继续多处复制,后续很难统一行为
|
||||
- 迁移看起来开始了,但没有真正收尾,维护者仍然需要在“大文件 + 共享层”之间来回切换
|
||||
|
||||
### 建议
|
||||
|
||||
- 继续把 `PresetEditorPanels.tsx` 拆成按 tab 或按领域分文件
|
||||
- 统一复用 `src/editor/shared/` 下的保存客户端、基础容器、表单片段
|
||||
- 对编辑器做一次“小型迁移收尾”,目标是消灭重复的基础 helper
|
||||
|
||||
## P1:本地开发 API 层与构建工具耦合过深
|
||||
|
||||
### 现状
|
||||
|
||||
本地 API 插件已经把很多临时逻辑吸收进项目内部,这是好事;但它现在承担的职责太多,且全部挂在 Vite 插件层。
|
||||
|
||||
### 证据
|
||||
|
||||
- `vite.config.ts:7-18` 直接把 `createLocalApiPlugins(__dirname, env)` 注入到 Vite config
|
||||
- `scripts/dev-server/localApiPlugins.ts` 当前约 394 行
|
||||
- `scripts/dev-server/localApiPlugins.ts:150` 定义 LLM proxy 插件
|
||||
- `scripts/dev-server/localApiPlugins.ts:216` 定义通用 JSON 文件编辑插件
|
||||
- `scripts/dev-server/localApiPlugins.ts:265` 直接把编辑器保存结果写回 `src/data/*.json`
|
||||
- `scripts/dev-server/localApiPlugins.ts:429` 再统一把所有插件拼到一起
|
||||
|
||||
### 影响
|
||||
|
||||
- dev server、preview server、编辑器持久化和 LLM 代理被绑在一个文件里,测试与替换成本都偏高
|
||||
- 随着编辑器继续扩张,这个文件会继续演化成“隐形后端”
|
||||
- 生产与开发环境的边界容易模糊,问题排查也更依赖熟悉 Vite 插件机制的人
|
||||
|
||||
### 建议
|
||||
|
||||
- 至少先按职责把 `localApiPlugins.ts` 拆成 `llm-proxy`、`json-editor-api`、`asset-catalog` 三块
|
||||
- 下一阶段可以考虑把编辑器 API 抽成独立本地服务层,而不是继续塞在 Vite 插件里
|
||||
- 给 JSON 写入接口补 schema 校验,而不只是“是 object 就写入”
|
||||
|
||||
## P2:构建体积仍有继续优化空间
|
||||
|
||||
### 现状
|
||||
|
||||
路由 lazy load 和部分 modal lazy load 已经做了,但主游戏运行时包仍然偏大。
|
||||
|
||||
### 证据
|
||||
|
||||
- `dist/assets/App-*.js` 约 389 kB
|
||||
- `dist/assets/index-*.js` 约 198 kB
|
||||
- `dist/assets/index-*.css` 约 117 kB
|
||||
- `src/components/GameShell.tsx`、`src/hooks/useStoryGeneration.ts`、`src/services/prompt.ts` 仍然是较大的主链路文件
|
||||
|
||||
### 影响
|
||||
|
||||
- 新人本地启动、构建和回归阅读成本仍然偏高
|
||||
- 主运行时模块越重,越不利于后续继续做场景扩展和编辑器共存
|
||||
|
||||
### 建议
|
||||
|
||||
- 优先沿着“运行时 orchestration 拆分”去减主 chunk,而不是单纯追求更多 lazy import
|
||||
- 对 `prompt`、自定义世界、编辑器预览等非首屏关键代码继续做边界拆分
|
||||
- 每轮重构后用真实构建产物复测,而不是只凭代码体感判断
|
||||
|
||||
## 建议的落地顺序
|
||||
|
||||
1. 先恢复绿色基线:修掉 `ItemCatalogEditor` lint 错误,处理 `customWorldPresentation` 的 duplicate key warning
|
||||
2. 再补齐门禁:缩小 ESLint ignore、把 `ai.test.ts` 拉回默认测试、扩大 strict typecheck 覆盖
|
||||
3. 然后拆主链:优先处理 `useStoryGeneration`、`useCombatFlow`、`GameShell`
|
||||
4. 再做编辑器迁移收尾:拆 `PresetEditorPanels.tsx`,统一共享层
|
||||
5. 最后处理 dev API 分层和 bundle 体积
|
||||
|
||||
## 一句话结论
|
||||
|
||||
这个仓库已经从“功能堆叠期”进入“工程收尾期”了。当前最值得做的不是再加一层玩法,而是把门禁补齐、把超级模块拆开、把半迁移状态收尾;只要这一步做稳,后续继续扩展剧情、编辑器和自定义世界的成本都会明显下降。
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
结合当前代码与已有边界文档,前端里仍有 6 类逻辑应该继续后移:
|
||||
结合当前代码与已有边界文档,前端里仍有 7 类逻辑应该继续后移:
|
||||
|
||||
1. **运行时快照前置写入与本地镜像解释**
|
||||
2. **鉴权 token 的浏览器本地真相**
|
||||
@@ -27,6 +27,7 @@
|
||||
4. **NPC 待接委托“换单”仍由前端直接触发正式生成**
|
||||
5. **quest/runtime item 的双环境混合编排**
|
||||
6. **浏览器侧大型 AI orchestration 与 prompt/repair/fallback 主链**
|
||||
7. **NPC 招募对白之后的正式结算链路**
|
||||
|
||||
一句话判断:
|
||||
|
||||
@@ -39,7 +40,7 @@
|
||||
### 2.1 文档依据
|
||||
|
||||
1. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
|
||||
2. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
|
||||
2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
3. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md`
|
||||
4. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
|
||||
5. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
|
||||
@@ -59,6 +60,20 @@
|
||||
|
||||
## 3. 当前高置信度应后移逻辑
|
||||
|
||||
## 3.0 本轮已完成后移
|
||||
|
||||
以下链路已在本轮或上一轮连续落地中完成后移,不再属于“仍残留在前端”的正式主链:
|
||||
|
||||
1. access token 浏览器本地真相
|
||||
2. browse history 本地真相
|
||||
3. runtime story 前置 `PUT /runtime/save/snapshot`
|
||||
4. NPC 待接委托 `replace / abandon / accept`
|
||||
5. custom world profile 正式浏览器入口
|
||||
6. `questDirector` / `runtimeItemAiDirector` 浏览器正式编排
|
||||
7. NPC 招募正式结算
|
||||
|
||||
其中 NPC 招募已从“前端本地改 companions / roster / npcStates / storyHistory”收回到后端 runtime action。
|
||||
|
||||
## 3.1 运行时快照前置写入仍在前端
|
||||
|
||||
### 代码证据
|
||||
@@ -427,6 +442,41 @@
|
||||
|
||||
---
|
||||
|
||||
## 3.8 NPC 招募对白之后的正式结算链路已完成后移
|
||||
|
||||
### 本轮前状态
|
||||
|
||||
迁移前,`src/hooks/story/npcInteraction.ts` 中的 `buildRecruitmentOutcome / executeRecruitment / startRecruitmentSequence` 仍在前端本地正式结算:
|
||||
|
||||
1. 改 `npcStates`
|
||||
2. 改 `companions`
|
||||
3. 改 `roster`
|
||||
4. 清 `currentEncounter / inBattle / sceneHostileNpcs`
|
||||
5. 直接写 `storyHistory`
|
||||
6. 再触发后续剧情推进
|
||||
|
||||
这与“前端只做表现,所有正式逻辑、数据都放到 Express 后端”直接冲突。
|
||||
|
||||
### 本轮后状态
|
||||
|
||||
本轮已完成:
|
||||
|
||||
1. `server-node/src/modules/story/runtimeSession.ts`
|
||||
- 正式承接完整 `companions`
|
||||
- 正式承接 `roster`
|
||||
2. `server-node/src/modules/npc/npcInteractionService.ts`
|
||||
- `npc_recruit` 已支持正常入队
|
||||
- `npc_recruit` 已支持满员换队招募
|
||||
3. `src/hooks/story/npcInteraction.ts`
|
||||
- 前端只保留招募对白流式展示
|
||||
- 正式招募结算改为调用后端 runtime action
|
||||
|
||||
### 当前判断
|
||||
|
||||
这一项已不再属于前端残留正式逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 4. 可以暂时保留在前端的部分
|
||||
|
||||
下面这些内容即使和上述模块同文件出现,也不属于必须后移的对象:
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
# 工程优化审查总览
|
||||
|
||||
这一组是同主题的连续审查记录,建议不要把它们当作三份彼此独立的文档来看。
|
||||
这一组只保留仍能指导当前 Rust / SpacetimeDB 主线的工程审查入口。早期连续扫描的有效结论已经合并到本 README 的“融合结论”,不再保留逐日旧稿。
|
||||
|
||||
## 当前推荐入口
|
||||
|
||||
1. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md)
|
||||
这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 Express 后端的运行时、鉴权、生成编排与本地真相残留。
|
||||
2. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md)
|
||||
1. [SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md](./SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md)
|
||||
这一版是旧 Node 后端冻结、第一批物理删除与后续批次边界记录,明确当前工程只保留 Rust / SpacetimeDB 主线入口。
|
||||
2. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md)
|
||||
这一版是第六批落地记录,聚焦删除无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本,并补齐后端运行时 function catalog 契约覆盖。
|
||||
3. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md)
|
||||
这一版是第五批落地记录,聚焦旧命名 re-export、空路由骨架、旧发布服务、前端 prompt 镜像与无入口编辑器壳层的物理删除。
|
||||
4. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md)
|
||||
这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 `server-rs` 的运行时、鉴权、生成编排与本地真相残留。
|
||||
5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md)
|
||||
这一版是第四批落地记录,聚焦未接入业务的数据生成产物、测试专用 stub 与对应配置残留出清。
|
||||
3. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md)
|
||||
6. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md)
|
||||
这一版是第三批落地记录,聚焦鉴权真相收口,先移除前端保存自动登录用户名/密码的本地真相,并明确运行时快照前置写入为什么当前还不能硬砍。
|
||||
4. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md)
|
||||
7. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md)
|
||||
这一版是第二批落地记录,聚焦旧主流程壳层、旧 bootstrap 和旧 inventory / forge / equipment flow Hook 的正式出清。
|
||||
5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
|
||||
8. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
|
||||
这一版是第一批落地记录,聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。
|
||||
6. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
|
||||
9. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
|
||||
这一版是面向当前仓库状态的优化点盘点,适合直接拿来排优先级和拆执行批次。
|
||||
7. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
|
||||
10. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
|
||||
这一版是对 `2026-04-19` 基线的当前仓库复核,明确哪些问题已经处理、哪些表述需要纠正、热点又迁移到了哪里。
|
||||
8. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
|
||||
11. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
|
||||
这一版保留原始问题快照和执行回填,适合回看“为什么会有这轮清理与边界收口”。
|
||||
9. [ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md)
|
||||
这一版最适合作为当前工程基线,重点从“是否真正绿色”“门禁有没有覆盖真实风险”来判断仓库状态。
|
||||
10. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md)
|
||||
适合回看运行时主链路、Story/Combat 边界、分层过渡期问题。
|
||||
11. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md)
|
||||
适合看第一轮系统性工程扫描,了解最早的问题基线。
|
||||
|
||||
## 融合结论
|
||||
|
||||
- 最新专项审计已经把“前端哪些逻辑还该后移到后端”收敛到 6 类:运行时快照、本地 token、本地浏览历史、NPC 委托换单、quest/runtime item 混合编排、浏览器 AI orchestration。
|
||||
@@ -34,14 +33,14 @@
|
||||
- 第二批已经开始清理旧主流程壳层与旧 flow Hook,当前主工程的“现行入口”和“历史入口”边界正在变得更清楚。
|
||||
- 第三批已经先完成鉴权真相收口的一段,前端不再保存自动登录用户名/密码;运行时快照链仍需先补后端 contract,再继续往前删。
|
||||
- 第四批已经继续收掉未接入业务的数据生成产物、测试专用 stub 与对应脚本/配置残留,主工程里的“假数据主源”进一步减少。
|
||||
- 第五批已经继续收掉旧命名 re-export、空路由骨架、旧发布 service、前端 prompt 镜像与无入口编辑器壳层,主工程里的“假入口”和“假 prompt 主源”进一步减少。
|
||||
- 第六批已经继续收掉无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本,并修复 function catalog 对后端运行时契约的覆盖缺口。
|
||||
- 当前仓库已经完成“旧 dev 插件链路删除、根目录噪音清理、`server-node -> src/**` 反向依赖切断”这批第一阶段任务。
|
||||
- 当前如果想直接判断“今天先优化什么”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`。
|
||||
- 当前的新重点已经进一步收敛到三类:未接线孤岛模块、前端残留的运行时/鉴权真相、热点向 prompt/runtime profile/平台入口壳层迁移。
|
||||
- 三轮结论是一致收敛的:问题不在“有没有开始工程化”,而在“工程化是否真正覆盖了最危险的主链路”。
|
||||
- 最新一轮已经把关注点集中到质量门禁、真实绿色基线、关键模块豁免和 build warning 上。
|
||||
- 早期三轮工程扫描的结论已经聚合为一条长期规则:工程化不能只看目录和拆分动作,必须覆盖真实主链、质量门禁、绿色基线、关键模块豁免和 build warning。
|
||||
- `2026-04-19` 这一轮把问题压实到了四类:仓库噪音、旧 dev 入口残留、前端越界运行时逻辑、巨型热点文件。
|
||||
- `2026-04-20` 这一轮进一步确认:前两类已经阶段性完成,当前真正剩下的是边界尾巴和新热点迁移。
|
||||
- 如果只是为了判断现在先做什么,直接从 `2026-04-01` 开始即可。
|
||||
- 如果是要看当前清理和边界收口的最新状态,优先看 `ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`。
|
||||
- 如果是要看“当前可执行的优化点清单”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`。
|
||||
- 如果是要做长期重构方案,再按 `2026-03-29 -> 2026-03-30 -> 2026-04-01 -> 2026-04-19 -> 2026-04-20` 的顺序回看演进。
|
||||
- 如果是要做长期重构方案,从 `2026-04-19`、`2026-04-20` 与当前 dead-code batch 记录开始即可。
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# server-node 冻结隔离说明(2026-04-24)
|
||||
|
||||
## 1. 当前状态
|
||||
|
||||
`server-node/` 已进入冻结隔离状态,不再作为可运行、可扩展、可引用的后端工程使用。
|
||||
|
||||
冻结原因:项目后端主线已经切到 `server-rs/` 的 Rust + SpacetimeDB 多 crate 方案,继续保留可执行的 `server-node/` 入口会误导后续开发,并增加提示词资产、AI 工作流与运行态逻辑的迁移漂移风险。
|
||||
|
||||
## 2. 冻结边界
|
||||
|
||||
1. 禁止新增任何以 `server-node/` 为目标的运行脚本、开发入口、CI 入口或工程依赖。
|
||||
2. 禁止新增从前端、Rust 后端、脚本或配置主动调用 `server-node/` 的逻辑。
|
||||
3. 禁止在 `server-node/` 内继续新增业务能力;后续能力必须落到 `server-rs/` 对应 crate。
|
||||
4. 历史文档、审计文档、迁移基线中允许保留 `server-node/` 作为旧系统来源说明,但不得把它描述成当前推荐实现。
|
||||
5. 第一批物理删除后,提示词资产与提示词相关工作流继续按迁移核对项追踪,不再恢复旧工程目录。
|
||||
|
||||
## 3. 删除前迁移核对项
|
||||
|
||||
以下资产曾作为删除前核对项。第一批物理删除后,旧实现不再从工作区直接读取;如需继续核对能力缺口,只允许通过历史提交、迁移文档或 `server-rs/` 已迁移实现追溯:
|
||||
|
||||
1. `server-node/src/prompts/customWorldEntityPrompts.ts`:自定义世界实体生成 prompt。
|
||||
2. `server-node/src/prompts/customWorldSceneNpcPrompts.ts`:自定义世界场景 NPC prompt。
|
||||
3. `server-node/src/prompts/questPrompts.ts`:任务意图识别 prompt。
|
||||
4. `server-node/src/prompts/runtimeItemPrompts.ts`:运行时物品意图识别 prompt。
|
||||
5. `server-node/src/prompts/customWorldOrchestratorPrompts.ts`:旧 Custom World JSON 生成与修复 prompt。
|
||||
6. `src/services/ai.ts` 与 `src/prompts/customWorldPrompts.ts` 中仍由前端承载的 AI orchestration / prompt 编排。
|
||||
|
||||
## 4. 工程防线
|
||||
|
||||
1. 第一批物理删除后,根目录 `package.json` 不再保留 `server-node:*`、`dev:node`、`check:server-node-freeze` 等旧入口。
|
||||
2. Vite 与本地开发脚本默认只指向 Rust `api-server`,不再保留 Node/Rust 后端切换开关。
|
||||
3. 历史文档允许保留旧 `server-node` 字样,但新增工程入口、脚本、依赖、运行说明不得再指向旧 Node 后端。
|
||||
4. 若后续需要恢复旧能力,只能迁移到 `server-rs/` 对应 crate 或 Axum facade,不恢复 `server-node/` 工程目录。
|
||||
|
||||
## 5. 后续处理顺序
|
||||
|
||||
1. 继续核对提示词资产与 prompt 工作流是否已完整落到 Rust 主线。
|
||||
2. 继续把前端残留业务编排迁入 `server-rs/`。
|
||||
3. 清理技术索引中容易误导当前入口的 Node / Express 文案。
|
||||
4. 保留历史审计材料,但不得把旧 Node 后端描述为当前推荐实现。
|
||||
|
||||
## 6. 已确认迁移项
|
||||
|
||||
### 6.1 场景幕背景图提示词
|
||||
|
||||
2026-04-25 已把旧 Node 自动资产链路中的场景幕背景图提示词包装迁移到 Rust 主线:
|
||||
|
||||
1. 旧来源:`server-node/src/services/customWorldAgentAutoAssetService.ts` 的 `buildSceneActPrompt(...)`。
|
||||
2. 新主源:`server-rs/crates/api-server/src/custom_world.rs` 的 `build_scene_act_background_image_prompt(...)`。
|
||||
3. 使用位置:`generate_draft_foundation_act_backgrounds(...)` 收集 `sceneChapterBlueprints[].acts[]` 后,先构造幕背景图专用提示词,再调用 `generate_custom_world_scene_image_for_profile(...)`。
|
||||
4. 保留语义:世界名、场景名、幕标题、幕摘要、幕目标、过渡钩子、主角色、辅助角色、世界气质、背景描述,以及“只生成环境背景,不出现角色立绘、站位 UI、对白框、按钮或文字”的约束。
|
||||
5. 迁移边界:`server-node/` 仅作为历史来源说明,不再参与运行;后续调整统一修改 Rust 主源。
|
||||
|
||||
## 7. 第一批安全删除记录(2026-04-25)
|
||||
|
||||
本批次开始把冻结隔离升级为物理删除。执行依据是项目后端主线已固定为 `server-rs/` 的 Rust + SpacetimeDB 多 crate 方案,旧 `server-node/` 不再作为可运行、可扩展、可引用的工程目录保留。
|
||||
|
||||
### 7.1 删除范围
|
||||
|
||||
1. 删除 `server-node/` 目录本体,旧实现只允许通过历史提交、迁移文档和已迁移到 `server-rs/` 的代码追溯。
|
||||
2. 删除旧 Node 后端专用入口:`scripts/dev-node.mjs`、`scripts/server-node-frozen.mjs`、`scripts/check-server-node-freeze.mjs`、`scripts/server-node-freeze-baseline.json`、`scripts/smoke-server-node.ts`、`scripts/smoke-same-origin-stack.ts`、`scripts/m7-api-compare.ts`、`scripts/deploy.sh`、`scripts/update.sh`、`view-llm-logs.ps1`。
|
||||
3. 根目录 `package.json` 删除 `server-node:*`、`dev:node`、`m7:api-compare` 与 `check:server-node-freeze` 等旧入口,并移除 `express`、`@types/express` 依赖。
|
||||
4. `npm run dev` 改为启动 Rust 本地栈;Vite 默认只代理到 Rust `api-server`,不再保留 `GENARRATIVE_BACKEND_STACK` 的 Node/Rust 双栈切换口。
|
||||
5. 清理 `.gitignore` 中只服务 `server-node/` 的忽略规则,并同步 `README.md`、`.env.example`、`server-rs/README.md` 与 `scripts/dev-server/README.md`。
|
||||
|
||||
### 7.2 暂不处理范围
|
||||
|
||||
1. 历史 PRD、审计、迁移基线中的 `server-node` 文案暂时保留为历史记录,不在第一批中大规模改写。
|
||||
2. `backend-rewrite-tasklist/` 中以旧 Node 后端为对照的迁移材料暂时保留,作为后续核对 Rust 主线能力缺口的历史审计输入。
|
||||
3. `src/services/ai.ts` 与 `src/prompts/customWorldPrompts.ts` 的前端残留编排不属于本批 Node 后端删除范围;后续继续按“前端只负责表现,业务逻辑进入 `server-rs/`”单独收口。
|
||||
|
||||
### 7.3 后续批次建议
|
||||
|
||||
1. 技术文档索引中的 Node / Express 后端条目只保留为历史资料,不再作为当前入口或推荐方案。
|
||||
2. 后续如继续整理历史文档,只把仍描述 `Express / PostgreSQL` 为当前目标架构的文字修正为“历史阶段口径”。
|
||||
3. 继续把前端残留业务逻辑迁入 `server-rs`;涉及 SpacetimeDB 的设计、实现、脚本和绑定继续显式使用相关 skill。
|
||||
|
||||
### 7.4 本轮安全核对结果
|
||||
|
||||
2026-04-25 本轮开始分批删除时,已确认第一批工程入口层面满足以下条件:
|
||||
|
||||
1. 工作区根目录下已不存在 `server-node/` 物理目录。
|
||||
2. `scripts/` 下已不存在旧 Node 后端专用运行、冻结、smoke、API 对比脚本。
|
||||
3. 根目录 `package.json` 不再包含 `server-node:*`、`dev:node`、`m7:api-compare` 与 `check:server-node-freeze` 入口。
|
||||
4. `package.json` 与 `package-lock.json` 不再包含 `express`、`@types/express`、`pg`、`postgres` 依赖包。
|
||||
5. `README.md`、`scripts/dev-server/README.md`、`server-rs/README.md`、`vite.config.ts`、`scripts/*.mjs`、`src/`、`packages/` 与 `server-rs/` 未发现仍主动启动或调用 `server-node` 的当前工程入口。
|
||||
|
||||
### 7.5 第二批删除边界
|
||||
|
||||
第二批不再删除可运行工程代码,而是清理“容易误导当前实现口径”的历史文档索引:
|
||||
|
||||
1. 只修正文档中仍把 `server-node`、Express 或 PostgreSQL 描述为当前推荐后端的句子。
|
||||
2. 保留审计、PRD、迁移基线中作为历史事实、旧实现来源、能力对照的 `server-node` 引用。
|
||||
3. 不大规模重写包含中文剧情、需求、审计结论的历史文档,避免把真实历史上下文抹平。
|
||||
4. 若发现某个历史文档仍指导新开发继续写 Node 后端,先把该文档改为“历史阶段口径”,再继续工程处理。
|
||||
|
||||
## 8. 开发命令与脚本复核(2026-04-26)
|
||||
|
||||
本轮按“`server-node/` 已完全移除”的状态复核当前开发入口、脚本和工程配置,确认不再保留旧 Node 后端或 Express 运行路径。
|
||||
|
||||
### 8.1 已复核范围
|
||||
|
||||
1. 根目录 `package.json` 与 `package-lock.json`。
|
||||
2. 根目录 `README.md`、`.env.example`、`.gitignore` 与 `vite.config.ts`。
|
||||
3. `scripts/`、`.github/`、`jenkins/` 与 `server-rs/` 下的已跟踪文本文件。
|
||||
|
||||
### 8.2 复核结论
|
||||
|
||||
1. `package.json` 中不存在 `server-node:*`、`dev:node`、`m7:api-compare`、`check:server-node-freeze` 等旧入口。
|
||||
2. `scripts/` 下不存在 `dev-node.mjs`、`smoke-server-node.ts`、`m7-api-compare.ts`、`smoke-same-origin-stack.ts` 等旧 Node 后端脚本。
|
||||
3. `package.json` 与 `package-lock.json` 中不存在 `express`、`@types/express`、`pg`、`postgres` 依赖。
|
||||
4. 当前开发入口继续固定为 `npm run dev`、`npm run dev:web`、`npm run api-server:maincloud` 与 Rust / SpacetimeDB 相关脚本,不恢复旧 Node 后端切换开关。
|
||||
|
||||
## 9. Caddy 本地服务入口移除(2026-04-26)
|
||||
|
||||
`serve:caddy` 仅服务旧的 dist 本地代理验证链路,不再属于当前 Rust / SpacetimeDB 主开发入口。本轮删除该入口和配套文件,避免开发命令继续暴露第二套本地服务方式。
|
||||
|
||||
### 9.1 删除范围
|
||||
|
||||
1. 根目录 `package.json` 删除 `serve:caddy`。
|
||||
2. 删除 `scripts/run-caddy-dev.mjs`。
|
||||
3. 删除 `tools/Caddyfile.dev`。
|
||||
4. `.env.example` 删除 `CADDY_API_UPSTREAM` 样例变量。
|
||||
|
||||
### 9.2 后续口径
|
||||
|
||||
1. 本地完整联调继续使用 `npm run dev`。
|
||||
2. 单独前端联调继续使用 `npm run dev:web` 并通过 Vite 代理到 Rust `api-server`。
|
||||
3. 生产包预览继续使用 Vite `preview`,不恢复 Caddy 专用开发入口。
|
||||
@@ -1,228 +0,0 @@
|
||||
# 编辑器 UI / 游戏 UI / 预设内容英文与乱码审计
|
||||
|
||||
更新时间:`2026-03-25`
|
||||
|
||||
## 范围与方法
|
||||
|
||||
- 范围只覆盖当前源码里会直接进入编辑器 UI、游戏运行时 UI、预设内容预览的文案。
|
||||
- 本轮直接按 `utf-8` 读取 `src/components` 与 `src/data` 复核,不把旧审计文档当最终事实来源。
|
||||
- 不统计 `import`、类型名、变量名、接口字段名、资源路径等纯开发层英文。
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 当前目标范围内,确认到 **1 处直接写进源码的中文乱码**:
|
||||
- `src/components/PresetEditor.tsx:72` 的 `鐗╁搧`
|
||||
- 当前更大的问题已经不是“大片中文乱码”,而是 **编辑器与部分游戏界面还残留成组英文 UI 文案**。
|
||||
- 预设内容层面的英文主要集中在 **原始枚举值 / 构筑字段**,例如 `common / rare / legendary`、`neutral`、`buildProfile.role`、`idle / move / attack / die`、`steady / burst / mobility / finisher / projectile`。这些值本身在数据层可以保留英文,但当前有一部分被界面直接原样显示出来了。
|
||||
|
||||
## 编辑器 UI
|
||||
|
||||
### 英文残留
|
||||
|
||||
- `src/components/PresetEditor.tsx:84-89`
|
||||
- 编辑器标签仍是 `Characters / NPCs / Scenes / Monsters / Items / Functions`
|
||||
- `src/components/PresetEditor.tsx:97-104`
|
||||
- 预设编辑里仍直接使用 `idle / move / attack / die` 与 `steady / burst / mobility / finisher / projectile`
|
||||
- `src/components/StateFunctionEditor.tsx:333-367`
|
||||
- 预览实体与效果摘要仍是英文:
|
||||
- `Preview NPC`
|
||||
- `Fallback NPC preview for ...`
|
||||
- `Preview Treasure`
|
||||
- `Treasure preview for ...`
|
||||
- `Damage x / Incoming x / Heal + / Mana + / Cooldown + / Turn x`
|
||||
- `src/components/StateFunctionEditor.tsx:496-607`
|
||||
- 预览阶段与提示说明仍有整段英文:
|
||||
- `Player Turn Preview`
|
||||
- `Escape Preview`
|
||||
- `Travel Result Preview`
|
||||
- `Explore Preview`
|
||||
- `Call-Out Preview`
|
||||
- `Idle Behavior Preview`
|
||||
- `Predicted skill: ...`
|
||||
- `Monster counter template uses ...`
|
||||
- `Battle behaviors are driven by skill weights ...`
|
||||
- `Escape behaviors always use the chase flow ...`
|
||||
- `src/components/StateFunctionEditor.tsx:855-939`
|
||||
- 预览面板头部与信息卡仍是英文:
|
||||
- `Option`
|
||||
- `Mode`
|
||||
- `Replay Preview`
|
||||
- `Preview Playing`
|
||||
- `Preview Ready`
|
||||
- `Live Player`
|
||||
- `Live Scene`
|
||||
- `No scene`
|
||||
- `Resolved Plan`
|
||||
- `Option kind`
|
||||
- `Target scene`
|
||||
- `Cooldowns`
|
||||
- `Battle Snapshot`
|
||||
- `Animation`
|
||||
- `Delivery`
|
||||
- `Damage`
|
||||
- `Predicted kill`
|
||||
- `Target survives`
|
||||
- `Snapshot based on live playback`
|
||||
- `src/components/ItemCatalogEditor.tsx:25`
|
||||
- 稀有度选项仍是 `common / uncommon / rare / epic / legendary`
|
||||
- `src/components/ItemCatalogEditor.tsx:486-560`
|
||||
- 物品预览区仍直接显示英文键和值:
|
||||
- `rarity`
|
||||
- `value`
|
||||
- `usable`
|
||||
- `yes / no`
|
||||
- `equip`
|
||||
- `world`
|
||||
- `neutral`
|
||||
- `HP / MP / Damage / Guard`
|
||||
- `HP Restore / MP Restore / CD Reduce`
|
||||
- `Build / 套装`
|
||||
- `Role / Set / Piece`
|
||||
- `none / standalone`
|
||||
- `src/components/NpcVisualEditor.tsx`
|
||||
- 目前大部分文案已中文化,但 `NPC` 缩写仍在标题、字段、保存提示中大量保留,属于低优先级统一项,不是乱码问题。
|
||||
|
||||
### 确认的中文乱码
|
||||
|
||||
| 文件 | 位置 | 当前文本 | 判断 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/components/PresetEditor.tsx` | `72` | `鐗╁搧` | 明确乱码,语义应为“物品” |
|
||||
|
||||
### 编辑器 UI 小结
|
||||
|
||||
- **最高优先级乱码修复点**:`PresetEditor.tsx:72`
|
||||
- **最高优先级英文清理点**:`StateFunctionEditor.tsx`、`ItemCatalogEditor.tsx`
|
||||
|
||||
## 游戏各界面 UI
|
||||
|
||||
### 英文残留
|
||||
|
||||
- `src/components/GameShell.tsx:414`
|
||||
- 标题副标仍是 `TAVERNREALMS`
|
||||
- `src/components/GameShell.tsx:1083-1094`
|
||||
- 团队弹窗里仍保留 `TavernRealms`
|
||||
- `src/components/CharacterChatModal.tsx:52,73,76`
|
||||
- `CHARACTER CHAT`
|
||||
- `HP`
|
||||
- `MP`
|
||||
- `src/components/CharacterDetailModal.tsx:24-40`
|
||||
- 属性与技能风格映射仍为英文:
|
||||
- `Strength / Agility / Intelligence / Spirit`
|
||||
- `Burst / Steady / Mobility / Finisher / Projectile`
|
||||
- `Female / Male / Unknown`
|
||||
- `src/components/CharacterDetailModal.tsx:149,186,194-271`
|
||||
- 详情弹窗区块标题与标签仍有整段英文:
|
||||
- `INITIAL COMPANION`
|
||||
- `Close character details`
|
||||
- `Profile`
|
||||
- `Candidate`
|
||||
- `Gender`
|
||||
- `Stats`
|
||||
- `Max HP / Max MP`
|
||||
- `Journey`
|
||||
- `Reason / Goal`
|
||||
- `Skills / Loadout / Pack / Backstory / Personality`
|
||||
- `src/components/InventoryPanel.tsx:39-50`
|
||||
- 稀有度标签仍是 `Legendary / Epic / Rare / Uncommon / Common`
|
||||
- `src/components/InventoryPanel.tsx:179-196,215`
|
||||
- 物品详情仍是英文:
|
||||
- `Quantity`
|
||||
- `Owner`
|
||||
- `Usable`
|
||||
- `Yes / No`
|
||||
- `Equipable`
|
||||
- `Value`
|
||||
- `Type`
|
||||
- `Tags`
|
||||
- `no-tags`
|
||||
- `src/components/AdventureEntityModal.tsx:206-217`
|
||||
- 自动生成的物品描述仍是英文整句:
|
||||
- `helps restore HP`
|
||||
- `supports MP recovery`
|
||||
- `fits offensive loadouts`
|
||||
- `supports defensive gearing`
|
||||
- `works as a rare trinket-grade pickup`
|
||||
- `can be saved for crafting or trading`
|
||||
- `${item.name} can be kept for trading, gifting, or future build planning.`
|
||||
- `${item.name} is a ${item.category} item that ...`
|
||||
- `src/components/AdventureEntityModal.tsx:226-235`
|
||||
- 物品属性摘要仍是英文:
|
||||
- `HP`
|
||||
- `MP`
|
||||
- `Damage`
|
||||
- `Guard x...`
|
||||
- `src/components/AdventureEntityModal.tsx:1271-1332`
|
||||
- NPC 物品详情弹窗仍有一整块英文:
|
||||
- `ITEM DETAIL`
|
||||
- `NPC inventory`
|
||||
- `Quantity`
|
||||
- `Value`
|
||||
- `Equip Slot`
|
||||
- `Not equippable`
|
||||
- `Usable item`
|
||||
- `Story, trade, or gift resource`
|
||||
- `Type`
|
||||
- `Rarity`
|
||||
- `Tags`
|
||||
- `No tags`
|
||||
- `src/components/CompanionCampModal.tsx:152,176-177,213,216`
|
||||
- `Active`
|
||||
- `HP`
|
||||
- `MP`
|
||||
- `待命 roster`
|
||||
- `Reserve`
|
||||
- `src/components/NpcModals.tsx:507`
|
||||
- 招募替换提示里仍混入 `roster`
|
||||
|
||||
### 当前未确认到的乱码
|
||||
|
||||
- 本轮没有在游戏运行时 UI 组件里复核到新的、直接写死在源码中的中文乱码。
|
||||
- 当前游戏 UI 的主要问题已经转为“英文标签 / 英文句子未汉化”,不是大面积中文乱码。
|
||||
|
||||
## 预设内容
|
||||
|
||||
### 直接会透到 UI 的英文源
|
||||
|
||||
- `src/data/itemDesign.ts:52-72`
|
||||
- 材质主题里直接保存了英文原始值:
|
||||
- `worldAffinity: "neutral"`
|
||||
- `role: "fieldcraft" / "breaker" ...`
|
||||
- `rarity: "common"`
|
||||
- `tags: ["scout", "craft"]`
|
||||
- `src/data/itemCatalog.ts:260-270`
|
||||
- `designed.rarity / designed.worldAffinity / designed.buildProfile` 会原样流入物品目录数据
|
||||
- `src/components/ItemCatalogEditor.tsx:486-560`
|
||||
- 上述原始字段当前会在编辑器预览里被直接显示,因此形成了可见英文泄漏
|
||||
- `src/data/questFlow.ts:24-30`
|
||||
- 任务奖励物品稀有度仍是 `rare / uncommon`
|
||||
- `src/data/npcInteractions.ts:71-82,102-103,166`
|
||||
- 稀有度与标签推断仍使用 `common / uncommon / rare / epic / legendary`
|
||||
- 物品标签仍使用 `weapon / armor`
|
||||
- `src/components/PresetEditor.tsx:97-104`
|
||||
- 预设编辑直接使用 `idle / move / attack / die`
|
||||
- 技能风格直接使用 `steady / burst / mobility / finisher / projectile`
|
||||
- `src/components/StateFunctionEditor.tsx:85-98`
|
||||
- 行为编辑直接使用 `battle / idle`
|
||||
- 朝向直接使用 `left / right`
|
||||
- 怪物动画直接使用 `idle / move / attack`
|
||||
- 风格直接使用 `steady / burst / mobility / finisher / projectile`
|
||||
|
||||
### 当前未确认到的乱码
|
||||
|
||||
- 本轮没有在 `src/data/*.ts` 的预设正文里复核到新的、直接写死的中文乱码。
|
||||
- 当前预设内容层面的问题,主要是“英文字段值没有在显示层做 label 映射”,不是正文汉字被写坏。
|
||||
|
||||
## 建议处理顺序
|
||||
|
||||
1. 先修 `src/components/PresetEditor.tsx:72` 的 `鐗╁搧`。
|
||||
2. 再集中处理 `src/components/StateFunctionEditor.tsx` 的整组英文预览与提示文案。
|
||||
3. 然后处理 `src/components/ItemCatalogEditor.tsx` 的物品预览英文键名与英文值。
|
||||
4. 之后清理游戏运行时最明显的英文块:
|
||||
- `src/components/CharacterDetailModal.tsx`
|
||||
- `src/components/InventoryPanel.tsx`
|
||||
- `src/components/AdventureEntityModal.tsx`
|
||||
- `src/components/CompanionCampModal.tsx`
|
||||
- `src/components/CharacterChatModal.tsx`
|
||||
- `src/components/GameShell.tsx`
|
||||
5. 最后为预设源字段补统一显示映射,把 `common / rare / legendary / neutral / idle / left / right ...` 全部收口到统一词典。
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
# 游戏 UI / 预设 / 编辑器 UI 英文与乱码复查
|
||||
|
||||
日期:`2026-03-29`
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 当前分支里,**确认存在源码级中文乱码的文件只有 1 个**:`src/components/CustomWorldEntityEditorModal.tsx`。
|
||||
- 历史上已经出现过的两处高风险乱码,本次**未复现**:
|
||||
- `src/components/GameShell.tsx` 角色选择页返回按钮
|
||||
- `src/data/npcInteractions.ts` 切磋敌对动作文案
|
||||
- 目前更主要的问题已经从“大面积乱码”转成了三类:
|
||||
- 运行时 UI 里的英文缩写或英文句子
|
||||
- 编辑器 UI 里的英文术语、原始枚举值和英文缩写
|
||||
- 预设数据中的英文名称、分类、标签、世界倾向、构筑角色等原始值直接透到 UI
|
||||
|
||||
## 复查口径
|
||||
|
||||
- 只统计会直接进入游戏 UI、编辑器 UI、预设预览或运行时详情的文本。
|
||||
- 不把纯内部实现名算进问题范围,比如接口路径、变量名、导入路径、素材文件夹名。
|
||||
- 终端输出已切换到 UTF-8 后复查,避免把“控制台显示乱码”误判成“源码真实乱码”。
|
||||
|
||||
## 一、已确认的源码级乱码
|
||||
|
||||
### 1. 编辑器 UI
|
||||
|
||||
| 文件 | 行号 | 当前文本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/components/CustomWorldEntityEditorModal.tsx` | `360`, `381` | `褰撳墠浼氱洿鎺...`、`棰勮 #...` | 场景预设选择弹窗的说明文案和预设编号标签已写坏 |
|
||||
| `src/components/CustomWorldEntityEditorModal.tsx` | `551`, `559`, `569`, `572`, `578`, `581`, `584`, `587` | `褰撳墠澶栬妯℃澘`、`褰㈣薄妯℃澘`、`鍚嶇О`、`绉板彿 / 韬唤`、`鑳屾櫙`、`鎬ф牸`、`鎴樻枟椋庢牸`、`鏍囩` | 可扮演角色编辑表单的标题与字段标签存在真实乱码 |
|
||||
| `src/components/CustomWorldEntityEditorModal.tsx` | `663`, `666`, `669`, `672` | `鍚嶇О`、`韬唤 / 鑱岃兘`、`鎻忚堪`、`鍔ㄦ満` | 普通 NPC 编辑表单字段标签存在真实乱码,即“创建自定义世界 -> NPC 编辑页”这一段 |
|
||||
| `src/components/CustomWorldEntityEditorModal.tsx` | `721`, `732`, `736`, `739` | `鍦烘櫙`、`棰勮鍥句腑鐨勫垏纾...`、`鍚嶇О`、`鎻忚堪` | 场景编辑器默认回退文案、说明文案和字段标签存在真实乱码 |
|
||||
| `src/components/CustomWorldEntityEditorModal.tsx` | `771`, `791` | ``瑙掕壊-${...}``、`['绾跨储', '浜掑姩']` | 默认新建数据本身带乱码,会继续流入编辑器与结果页 |
|
||||
|
||||
## 二、游戏 UI 中的英文残留
|
||||
|
||||
| 文件 | 行号 | 当前文本/值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/components/AdventurePanel.tsx` | `179-187` | `restores HP during an adventure run`、`fits offensive loadouts` 等整句英文 | 奖励物品自动描述仍是英文句子,会直接进入运行时奖励详情 |
|
||||
| `src/components/AdventureEntityModal.tsx` | `815-816`, `988`, `1124-1126`, `1497` | `HP`、`MP` | 玩家、怪物、NPC 状态条与效果预览仍使用英文缩写 |
|
||||
| `src/components/AdventureEntityModal.tsx` | `1071`, `1109`, `1426` | `NPC 信息`、`敌对NPC` / `NPC`、`NPC 背包` | NPC 相关标题和标签仍是中英混排 |
|
||||
| `src/components/CompanionCampModal.tsx` | `176-177`, `232-233`, `254` | `HP`、`MP`、`NPC` | 同行编队卡片和空态提示仍保留英文缩写 |
|
||||
| `src/components/NpcModals.tsx` | `251`, `355`, `407` | `NPC 商品列表`、`NPC 商品`、`HP` / `MP` | 商店弹窗标题和物品效果预览仍是中英混排 |
|
||||
| `src/components/CharacterDetailModal.tsx` | `117` | `数量 x{item.quantity}` | 数量前缀里仍保留英文乘号写法 |
|
||||
|
||||
## 三、编辑器 UI 中的英文残留
|
||||
|
||||
| 文件 | 行号 | 当前文本/值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/components/ItemCatalogEditor.tsx` | `601`, `617-623` | `neutral / wuxia / xianxia`、`tag` 原始值 | 世界倾向和标签直接显示原始英文值 |
|
||||
| `src/components/ItemCatalogEditor.tsx` | `637-651`, `689` | `HP`、`MP`、`Build Buff`、`CD` | 属性预览、使用效果和背包卡片预览仍有英文缩写/术语 |
|
||||
| `src/components/ItemCatalogEditor.tsx` | `662-665` | `buildProfile.role`、`synergy` 原始值 | 构筑角色和协同标签直接暴露英文角色定位值 |
|
||||
| `src/components/ItemCatalogEditor.tsx` | `712`, `824`, `841` | `物品 ID`、`使用 Build Buff`、`套装 ID` | 字段标签里仍有 `ID` / `Build Buff` |
|
||||
| `src/components/StateFunctionEditor.tsx` | `843-846`, `910` | `HP`、`No visible target` | 预览摘要和实时状态里仍有英文缩写与整句英文 |
|
||||
| `src/components/StateFunctionEditor.tsx` | `1089-1091` | `Option behavior overrides saved.`、`Failed to save option behavior overrides` | 保存反馈文案仍是英文 |
|
||||
| `src/components/StateFunctionEditor.tsx` | `1133`, `1212`, `1218` | `definition.state`、`AnimationState`、`idle/move/attack` | 原始状态值和动画值仍直接显示 |
|
||||
| `src/components/PresetEditor.tsx` | `86-88`, `2212` | `NPC`、`敌对 NPC` | 页签和说明文案仍有 `NPC` 英文缩写 |
|
||||
| `src/components/PresetEditor.tsx` | `893-910`, `1997-2000` | `AnimationState`、`steady/burst/...`、`idle/move/attack/die` | 技能动作、技能风格、怪物预览动作直接显示原始英文枚举值 |
|
||||
| `src/components/PresetEditor.tsx` | `942`, `1015`, `1142`, `1456`, `1477`, `1483`, `1752`, `1788`, `1796`, `2037`, `2137`, `2174` | `Build Buff`、`ID`、`FPS` | 多个编辑字段和动画配置项仍保留英文术语 |
|
||||
| `src/components/NpcVisualEditor.tsx` | `568-571`, `581`, `817` | `NPC 视觉编辑器`、`当前 NPC`、`x / y` | 标题、字段名和坐标显示仍有英文缩写 |
|
||||
|
||||
## 四、预设 / 数据层中的英文残留
|
||||
|
||||
这些内容虽然不一定直接在当前文件里渲染,但会进入运行时详情、掉落展示、物品编辑器、预设编辑器或交易界面。
|
||||
|
||||
| 文件 | 行号 | 当前文本/值 | 透出路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/data/monsterPresets.ts` | `438-456`, `479-499`, `535-540`, `647-650` | `Material`、`Relic`、`Armor`、`Consumable`、`Stone Shell Shard`、`Blood Lens`、英文描述句子、`rare/uncommon`、`material/relic/mana` | 会进入怪物掉落、物品详情、交易弹窗和编辑器预览 |
|
||||
| `src/data/itemDesign.ts` | `56-72`, `123-149`, `602-604`, `761-762`, `832-834` | `worldAffinity` 的 `neutral/wuxia/xianxia`,`role` 的 `fieldcraft/breaker/caster/berserker/assassin`,`tags` 的 `breaker/burst/mana`,`pieceName` 的 `dust/crystal/gem`,以及 `build` 混排短语 | 会直接透到 `ItemCatalogEditor` 的世界、标签、构筑角色、协同标签和套装信息 |
|
||||
| `src/data/characterPresets.ts` | `368-379`, `384-386`, `525-543`, `839-857`, `1024-1045` | `Double Jump`、`jump attack`、`Wall Slide`、`blunt/dry/direct`、`wary/fragmented` 等原始动作名和对话风格值 | 会被 `PresetEditor` 的动作/风格选择器直接显示 |
|
||||
|
||||
## 五、未复现的问题
|
||||
|
||||
- `src/components/GameShell.tsx` 角色选择页返回按钮旧乱码已修复,当前为“返回”。
|
||||
- `src/data/npcInteractions.ts` 旧的切磋动作乱码已修复,当前为“敌对/切磋前蓄力,点击后转为原地闪避”。
|
||||
- 本次扫描 `src/components`、`src/data` 未发现 `<EFBFBD>`(replacement character)类型的编码损坏。
|
||||
- 除 `src/components/CustomWorldEntityEditorModal.tsx` 外,本次未再确认到新的源码级中文乱码文件。
|
||||
- 自定义世界的 NPC 视觉编辑组件 `src/components/CustomWorldNpcVisualEditor.tsx` 本次未发现新的乱码。
|
||||
|
||||
## 六、建议处理顺序
|
||||
|
||||
1. 先修 `src/components/CustomWorldEntityEditorModal.tsx` 的真实乱码。
|
||||
2. 再清理 `src/components/AdventurePanel.tsx` 和 `src/data/monsterPresets.ts` 的整句英文,因为它们最容易直接破坏玩家观感。
|
||||
3. 为高频缩写和枚举值补统一映射层:
|
||||
- `NPC`
|
||||
- `HP` / `MP` / `CD`
|
||||
- `worldAffinity`
|
||||
- `role`
|
||||
- `tags`
|
||||
- `AnimationState`
|
||||
- 技能风格 `steady/burst/mobility/finisher/projectile`
|
||||
4. 最后统一编辑器里所有 `ID`、`FPS`、`Build Buff` 之类术语的显示策略。
|
||||
@@ -1,87 +0,0 @@
|
||||
# 游戏 UI / 预设 / 编辑器 UI 英文与乱码复核
|
||||
|
||||
日期:`2026-03-30`
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 当前分支里,确认仍在真实渲染的源码级乱码主要集中在 2 个文件:
|
||||
- `src/components/GameShell.tsx`
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx`
|
||||
- `src/components/NpcVisualEditor.tsx` 中确实还留有旧乱码字符串,但位于 `/* ... */` 注释块里,本次不计入“当前 UI 问题”。
|
||||
- 英文残留仍然较多,主要分为三类:
|
||||
- 游戏运行时界面的英文标题、空态文案和缩写
|
||||
- 编辑器界面的英文术语、英文保存反馈和原始枚举值
|
||||
- 预设 / 数据层中的英文名称、标签、角色定位、动画目录和 build 相关原值直接透到 UI
|
||||
|
||||
## 复核口径
|
||||
|
||||
- 显式按 UTF-8 读取文件,避免把终端编码问题误判成源码乱码。
|
||||
- 只统计会进入游戏 UI、编辑器 UI、预设预览或结果页的文本。
|
||||
- 注释块、变量名、导入路径、接口路径等内部实现名不计入本次问题清单。
|
||||
- 英文残留部分以下表中的“当前确实会显示或透传”的高优先级项为主。
|
||||
|
||||
## 一、已确认的真实乱码
|
||||
|
||||
| 范围 | 文件 | 行号 | 当前文本 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 游戏 UI | `src/components/GameShell.tsx` | `565` | `瑙掕壊` | 主界面底部“角色”页签已写坏 |
|
||||
| 游戏 UI | `src/components/GameShell.tsx` | `578` | `鍐掗櫓` | 主界面底部“冒险”页签已写坏 |
|
||||
| 游戏 UI | `src/components/GameShell.tsx` | `591` | `鑳屽寘` | 主界面底部“背包”页签已写坏 |
|
||||
| 游戏 UI | `src/components/GameShell.tsx` | `710` | `闃熶紞` / `鑳屽寘` | 浮层标题根据面板切换时会显示乱码 |
|
||||
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `386` | `宸查€?` | 场景预设选择弹窗中的“已选中”状态标签已写坏 |
|
||||
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `432` | `鍙栨秷` | 统一保存栏的取消按钮已写坏 |
|
||||
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `436` | `淇濆瓨淇敼` | 统一保存栏的主按钮文案已写坏 |
|
||||
|
||||
## 二、游戏 UI 中的英文残留
|
||||
|
||||
| 文件 | 行号 | 当前文本 / 值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/components/AdventurePanel.tsx` | `363` | `Currency` | 任务奖励卡的货币标题仍是英文 |
|
||||
| `src/components/AdventurePanel.tsx` | `371` | `No item bounty attached to this quest.` | 任务奖励空态文案仍是英文 |
|
||||
| `src/components/AdventurePanel.tsx` | `1424-1428` | `LOOT CACHE`、`Tap an item icon to inspect its details.`、`No usable loot dropped this time, but the battle is still settled.` | 战利品弹层标题和说明仍是整句英文 |
|
||||
| `src/components/AdventurePanel.tsx` | `1442` | `No loot dropped this time.` | 战利品列表空态文案仍是英文 |
|
||||
| `src/components/AdventurePanel.tsx` | `1352`, `1524` | `x{item.quantity}`、`HP` / `MP` | 数量展示与效果预览仍保留英文缩写 |
|
||||
| `src/components/AdventureEntityModal.tsx` | `892-899` | `label="HP"`、`label="MP"` | 同行状态估计卡仍使用英文缩写 |
|
||||
| `src/components/AdventureEntityModal.tsx` | `1073`, `1111`, `1428` | `NPC 信息`、`敌对NPC` / `NPC`、`NPC 背包` | NPC 详情区仍是中英混排 |
|
||||
| `src/components/CompanionCampModal.tsx` | `177-178`, `233-234`, `255` | `HP`、`MP`、`NPC` | 营地卡片和空态提示仍保留英文缩写 |
|
||||
| `src/components/NpcModals.tsx` | `79`, `252`, `273`, `356`, `408` | `x{item.quantity}`、`NPC 商品列表`、`这个 NPC 当前没有可售商品。`、`NPC 商品`、`HP` / `MP` | 交易弹窗、详情弹窗和数量角标存在中英混排 |
|
||||
| `src/components/CharacterDetailModal.tsx` | `117` | `数量 x{item.quantity}` | 角色详情中的数量前缀仍保留英文 `x` |
|
||||
|
||||
## 三、编辑器 UI 中的英文残留
|
||||
|
||||
| 文件 | 行号 | 当前文本 / 值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/components/ItemCatalogEditor.tsx` | `576-581`, `621-624` | `fieldcraft`、`breaker`、`mana`、`boots`、`dust`、`crystal`、`gem` 等原值 | 物品标签、构筑角色、部件名和协同信息会直接显示英文原值 |
|
||||
| `src/components/ItemCatalogEditor.tsx` | `648`, `671`, `729-783`, `800` | `HP` / `MP` / `CD`、`物品 ID`、`使用 Build Buff`、`套装 ID` | 物品编辑器预览与字段标签仍有英文缩写 / 术语 |
|
||||
| `src/components/StateFunctionEditor.tsx` | `818-821`, `885`, `915` | `HP`、`No visible target`、`n/a` | 选项行为预览面板仍有英文缩写和英文空态 |
|
||||
| `src/components/StateFunctionEditor.tsx` | `1060-1064` | `Failed to save option behavior overrides`、`Option behavior overrides saved.` | 保存反馈仍是英文 |
|
||||
| `src/components/StateFunctionEditor.tsx` | `1106`, `1185`, `1191` | `battle/idle`、`AnimationState` 的原始动作值、`idle/move/attack` | 状态和动作枚举值直接显示为英文 |
|
||||
| `src/components/PresetEditor.tsx` | `88`, `90`, `1474-1501`, `1806-1814` | `NPC`、`敌对 NPC`、`NPC ID`、`关联角色 ID`、`敌对资源 ID`、`连接场景 ID` | 多个标签和页签仍保留英文缩写 / `ID` |
|
||||
| `src/components/PresetEditor.tsx` | `101-106`, `896-913`, `2008-2018` | `idle/move/attack/die`、`steady/burst/mobility/finisher/projectile` | 角色技能和敌对资源预览会直接显示英文枚举值 |
|
||||
| `src/components/PresetEditor.tsx` | `945`, `2155`, `2192` | `Build Buff`、`FPS` | 技能编辑和动作图集配置仍有英文术语 |
|
||||
| `src/components/NpcVisualEditor.tsx` | `416-461` | `Failed to load NPC visual overrides`、`Failed to load NPC layout config`、`using bundled defaults` | NPC 视觉编辑器的加载失败提示仍是英文 |
|
||||
| `src/components/NpcVisualEditor.tsx` | `678`, `718` | `Saved NPC visual overrides to ...`、`Saved shared NPC layout config.` | 保存成功反馈仍是英文 |
|
||||
| `src/components/NpcVisualEditor.tsx` | `903`, `906`, `919`, `1226` | `NPC 视觉编辑器`、`当前 NPC`、`x ... / y ...` | 标题、字段标签与坐标信息仍存在中英混排 |
|
||||
|
||||
## 四、预设 / 数据层中会透到 UI 的英文值
|
||||
|
||||
| 文件 | 行号 | 当前文本 / 值 | 透出路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/data/monsterPresets.ts` | `494-512`, `522-540`, `647-652`, `718-723` | `Armor`、`Relic`、`Material`、`Consumable`、`Carapace Plate`、`Guard Core`、`Spore Pouch`、`Burst Cap`、`Ashfire Feather`、英文描述句子、`rare/uncommon` | 会进入怪物掉落、战利品详情、交易弹窗和物品预览 |
|
||||
| `src/data/itemDesign.ts` | `56-57`, `67-68`, `123-149` | `worldAffinity: neutral/wuxia/xianxia`、`role: fieldcraft/breaker/caster/berserker/assassin`、`tags` 中的 `caster/mana/burst/assassin` | 会透到 `ItemCatalogEditor` 的世界、角色定位、标签和协同信息 |
|
||||
| `src/data/itemDesign.ts` | `213-219`, `538-545`, `589-604`, `731-762`, `820-834`, `906-918` | `pieceName: boots/chest/gloves/...`、`build`、`setId`、`role`、`dust/crystal/gem` 等 | 会透到物品编辑器、套装信息和部件信息展示 |
|
||||
| `src/data/characterPresets.ts` | `54-69` | `blunt/wary/dry/direct/fragmented` | 对话风格与性格归类原值会被编辑器直接显示 |
|
||||
| `src/data/characterPresets.ts` | `368-379`, `525-526`, `839-850`, `1024-1025` | `Double Jump`、`jump attack`、`Wall Slide` | 角色动作目录 / 前缀原值会被 `PresetEditor` 直接显示 |
|
||||
| `src/data/characterPresets.ts` | `384-386`, `541-543`, `855-857`, `1045` | `guardStyle` / `warmStyle` / `truthStyle` 对应的英文原值 | 角色预设风格字段在编辑器中仍会显示英文 |
|
||||
|
||||
## 五、未计入项
|
||||
|
||||
- `src/components/NpcVisualEditor.tsx:681-683`、`721-722` 的乱码字符串位于块注释内,不会进入当前界面,因此未计入本次“活跃问题”。
|
||||
- `docs/*.md` 里的历史审计文档和旧清单不在本次范围内,本次只统计游戏 UI、预设和编辑器 UI。
|
||||
|
||||
## 六、建议处理顺序
|
||||
|
||||
1. 先修 `src/components/GameShell.tsx` 和 `src/components/CustomWorldEntityEditorModal.tsx` 的真实乱码,因为它们已经直接出现在主流程界面。
|
||||
2. 再清理 `src/components/AdventurePanel.tsx` 的英文空态、战利品标题和 `Currency`,这是玩家最容易直接看到的一批英文。
|
||||
3. 然后统一编辑器术语映射,优先处理 `HP` / `MP` / `NPC` / `ID` / `FPS` / `Build Buff` / `AnimationState`。
|
||||
4. 最后为 `src/data/itemDesign.ts`、`src/data/monsterPresets.ts`、`src/data/characterPresets.ts` 增加显示层映射,避免原始英文值继续直接透到编辑器和运行时界面。
|
||||
@@ -1,280 +0,0 @@
|
||||
# 游戏 UI / 预设实体 / 编辑器 UI 英文与乱码复核(续)
|
||||
|
||||
日期:`2026-03-30`
|
||||
|
||||
## 说明
|
||||
|
||||
- 这份文档是对当前分支的重新复核,不直接沿用旧审计文档的正文,因为旧文档本身已经存在较明显乱码。
|
||||
- 本轮重点覆盖三类范围:
|
||||
- 游戏运行时 UI:`src/components/` 下实际会进入主流程的界面,以及 `src/components/game-shell/`
|
||||
- 编辑器 UI:`src/components/*Editor*.tsx`、`src/components/preset-editor/`、`src/editor/shared/`
|
||||
- 预设实体 / 数据层:`src/data/` 中会被编辑器、预览面板或游戏详情页直接透出的文本
|
||||
- 复核方式:
|
||||
- 直接按 UTF-8 读取源码,避免把终端显示问题误判成源码乱码
|
||||
- 只记录会显示在玩家或编辑器使用者面前的文本
|
||||
- `import`、类型名、变量名、接口字段名、纯内部注释默认不计入
|
||||
- 但保存 / 加载提示这类虽然来自 helper 文件、最终会显示到 UI 的字符串,仍计入
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 当前分支里,真正“源码里已经写坏”的中文乱码,主要集中在 4 个位置:
|
||||
- `src/components/GameShell.tsx`
|
||||
- `src/components/preset-editor/shared.ts`
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx`
|
||||
- 其中最严重的是 `src/components/preset-editor/PresetEditorPanels.tsx`:
|
||||
- 角色/NPC/场景/敌对 NPC 资源四个子面板里都有残缺字符串
|
||||
- 同时混有 `NPC`、`ID`、`FPS`、`Build Buff`、`Medieval NPC` 等英文术语
|
||||
- 数据层 `src/data/` 本轮没有再扫到新的中文乱码;问题更多是英文预设值直接透到编辑器 / 预览 UI。
|
||||
- 游戏运行时 UI 侧已经比旧清单干净很多,但仍有几块明显英文残留:
|
||||
- `AdventurePanel`
|
||||
- `AdventureEntityModal`
|
||||
- `CompanionCampModal`
|
||||
- `NpcModals`
|
||||
- `game-shell/CharacterSelectionFlow`
|
||||
|
||||
## 一、已确认的真乱码
|
||||
|
||||
| 范围 | 文件 | 行号 | 当前文本示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 游戏 UI | `src/components/GameShell.tsx` | `565`, `578`, `591`, `710` | `瑙掕壊`、`鍐掗櫓`、`鑳屽寘`、`闃熶紞` | 主界面底部 tab 和浮层标题已写坏 |
|
||||
| 编辑器 UI | `src/components/preset-editor/shared.ts` | `42-55` | `瑙掕壊`、`鍦烘櫙`、`鐗╁搧`、`鏁屽 NPC`、`姝︿緺`、`浠欎緺`、`鑷畾涔変笘鐣?` | 新版预设编辑器 tab 与世界标签已写坏 |
|
||||
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `383`, `429`, `433` | `宸查€?`、`鍙栨秷`、`淇濆瓨淇敼` | 自定义世界实体编辑弹窗里的已选中/取消/保存文案乱码 |
|
||||
| 编辑器 UI | `src/components/preset-editor/PresetEditorPanels.tsx` | `251`, `530`, `1383`, `1468`, `1478`, `1830` 等多处 | `鏂版妧鑳?`、`鏂板鎶€鑳?`、`绾満鏅?`、`鑳屾櫙鍥捐矾寰?`、`涓嶈缃?`、`... FPS銆?` | 新版预设编辑器存在大面积残缺字符串,部分已经带 `?` 结尾 |
|
||||
|
||||
### `PresetEditorPanels.tsx` 乱码分布
|
||||
|
||||
- 角色预设区:
|
||||
- `251`, `310`, `323`, `379`, `467-688`, `719-802`
|
||||
- 示例:`新技<E696B0>?`、`预览技<E8A788>?`、`法力消<E58A9B>?`、`属性面<E680A7>?`、`主场<E4B8BB>?`
|
||||
- NPC 预设区:
|
||||
- `1000-1208`
|
||||
- 示例:`这里汇总了场景里的所<E79A84>?NPC 角色预设<E9A284>?`、`如果<E5A682>?NPC 绑定了角色技能...<2E>?`、`敌对 NPC 会沿用战斗资源预设展示...<2E>?`
|
||||
- 场景预设区:
|
||||
- `1244-1478`
|
||||
- 示例:`没有可编辑的场景预设<E9A284>?`、`敌<>?NPC`、`纯场<E7BAAF>?`、`背景图路<E59BBE>?`、`不设<E4B88D>?`
|
||||
- 敌对 NPC 资源区:
|
||||
- `1551-1851`
|
||||
- 示例:`没有可编辑的敌对 NPC 资源<E8B584>?`、`基础数<E7A180>?`、`最大生<E5A4A7>?`、`... 和 FPS<50>?`、`起始<E8B5B7>?`
|
||||
|
||||
## 二、游戏 UI 中仍会显示的英文
|
||||
|
||||
### 1. 主冒险面板
|
||||
|
||||
- `src/components/AdventurePanel.tsx:363`
|
||||
- `Currency`
|
||||
- `src/components/AdventurePanel.tsx:371`
|
||||
- `No item bounty attached to this quest.`
|
||||
- `src/components/AdventurePanel.tsx:1424`
|
||||
- `LOOT CACHE`
|
||||
- `src/components/AdventurePanel.tsx:1427-1428`
|
||||
- `Tap an item icon to inspect its details.`
|
||||
- `No usable loot dropped this time, but the battle is still settled.`
|
||||
- `src/components/AdventurePanel.tsx:1442`
|
||||
- `No loot dropped this time.`
|
||||
- `src/components/AdventurePanel.tsx:1524`
|
||||
- `HP` / `MP`
|
||||
|
||||
### 2. 实体详情与交互弹窗
|
||||
|
||||
- `src/components/AdventureEntityModal.tsx:1163-1165`
|
||||
- `x{item.quantity}`
|
||||
- `Inspect`
|
||||
- `src/components/AdventureEntityModal.tsx:1428`
|
||||
- `NPC 背包`
|
||||
- `src/components/CompanionCampModal.tsx:177-178`, `233-234`, `255`
|
||||
- `HP`
|
||||
- `MP`
|
||||
- `NPC`
|
||||
- `src/components/NpcModals.tsx:252`, `273`, `356`, `408`
|
||||
- `NPC 商品列表`
|
||||
- `这个 NPC 当前没有可售商品。`
|
||||
- `NPC 商品`
|
||||
- `HP` / `MP`
|
||||
|
||||
### 3. 开场选角流
|
||||
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx:28-32`
|
||||
- `Sword Princess`
|
||||
- `Royal Blade`
|
||||
- `Vanguard`
|
||||
- `Twin Blade Rogue`
|
||||
- `Assassin`
|
||||
- `Armored Spear`
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx:35-39`
|
||||
- `STR`
|
||||
- `AGI`
|
||||
- `INT`
|
||||
- `SPI`
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx:329-333`
|
||||
- `Character Stats`
|
||||
- `Gender:`
|
||||
|
||||
## 三、编辑器 UI 中仍会显示的英文
|
||||
|
||||
### 1. 旧预设编辑入口
|
||||
|
||||
- `src/components/PresetEditor.tsx:61-69`
|
||||
- `Preset Workshop`
|
||||
- `Unified Preset Preview And Editor`
|
||||
- `Manage character, NPC, scene, monster, item, and behavior presets from one editor shell. Each tab now loads its own container so the entry component stays small and focused.`
|
||||
|
||||
### 2. 新预设编辑器共享配置
|
||||
|
||||
- `src/components/preset-editor/shared.ts:60-72`
|
||||
- `idle`
|
||||
- `move`
|
||||
- `attack`
|
||||
- `die`
|
||||
- `steady`
|
||||
- `burst`
|
||||
- `mobility`
|
||||
- `finisher`
|
||||
- `projectile`
|
||||
|
||||
### 3. 新预设编辑器主面板
|
||||
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:620`
|
||||
- `Build Buff`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:966`
|
||||
- `No NPC presets available.`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1100-1202`
|
||||
- `NPC`
|
||||
- `NPC ID`
|
||||
- `Medieval NPC`
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1830`, `1867`
|
||||
- `FPS`
|
||||
|
||||
### 4. 物品编辑器
|
||||
|
||||
- `src/components/ItemCatalogEditor.tsx:648`
|
||||
- `HP`
|
||||
- `MP`
|
||||
- `CD`
|
||||
- `src/components/ItemCatalogEditor.tsx:783`
|
||||
- `Build Buff`
|
||||
- `src/components/ItemCatalogEditor.tsx:800`
|
||||
- `套装 ID`
|
||||
- `src/components/ItemCatalogEditor.tsx:576-585`, `793-800`
|
||||
- `selectedItem.tags`、`buildProfile.role`、`setId` 等原始英文值会直接显示在预览或输入框里
|
||||
|
||||
### 5. 选项行为编辑器
|
||||
|
||||
- `src/components/StateFunctionEditor.tsx:818`, `821`
|
||||
- `HP`
|
||||
- `No visible target`
|
||||
- `src/components/StateFunctionEditor.tsx:885`, `915`
|
||||
- `HP`
|
||||
- `n/a`
|
||||
- `src/components/StateFunctionEditor.tsx:1060-1064`
|
||||
- `Failed to save option behavior overrides`
|
||||
- `Option behavior overrides saved.`
|
||||
- `src/components/StateFunctionEditor.tsx:1185`
|
||||
- `AnimationState` 枚举值直接作为 label 显示
|
||||
- `src/components/StateFunctionEditor.tsx:1191`
|
||||
- `idle` / `move` / `attack`
|
||||
- `src/components/StateFunctionEditor.tsx:1217`
|
||||
- `steady` / `burst` / `mobility` / `finisher` / `projectile`
|
||||
|
||||
### 6. NPC 视觉编辑器与自定义世界编辑器
|
||||
|
||||
- `src/components/npcVisualEditorPersistence.ts:27-32`, `46-51`
|
||||
- `Failed to save NPC visual overrides`
|
||||
- `Saved NPC visual overrides to src/data/npcVisualOverrides.json.`
|
||||
- `Failed to save NPC layout config`
|
||||
- `Saved shared NPC layout config.`
|
||||
- `src/components/CustomWorldEntityCatalog.tsx:345`
|
||||
- `MedievalFantasyCharacters`
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx:457`
|
||||
- `MedievalFantasyCharacters`
|
||||
|
||||
## 四、预设实体 / 数据层中会透到 UI 的英文值
|
||||
|
||||
### 1. 物品预设
|
||||
|
||||
- `src/data/itemDesign.ts:52-58`, `67-69`, `123-149`
|
||||
- `worldAffinity: "neutral" / "wuxia" / "xianxia"`
|
||||
- `role: "fieldcraft" / "breaker" / "caster" / "berserker" / "assassin"`
|
||||
- `rarity: "common" / "rare" / "epic"`
|
||||
- `tags: ["caster", "mana"]` 等
|
||||
- `src/data/itemDesign.ts:213-219`
|
||||
- `pieceName: "boots" / "chest" / "gloves" / "helm" / "leggings" / "shield" / "weapon"`
|
||||
- `src/data/itemDesign.ts:538-545`, `588-606`, `730-766`, `818-836`, `904-919`
|
||||
- 描述和 profile 中直接拼入 `build`、`role`、`dust`、`crystal`、`gem` 等英文值
|
||||
- 这些字段会在 `ItemCatalogEditor` 预览和构筑信息里直出
|
||||
|
||||
### 2. 敌对资源 / 掉落预设
|
||||
|
||||
- `src/data/monsterPresets.ts:494-540`, `647-723`
|
||||
- 掉落类别:`Armor`、`Relic`、`Material`、`Consumable`
|
||||
- 掉落名称:`Carapace Plate`、`Guard Core`、`Spore Pouch`、`Burst Cap`、`Ashfire Feather`、`Serpent Eye`、`Tide Ink`、`Lake Pearl`、`Thorn Nectar`
|
||||
- 掉落描述整句仍是英文
|
||||
- 这些条目会直接进入掉落预览、NPC 交易与物品详情
|
||||
|
||||
### 3. 角色预设
|
||||
|
||||
- `src/data/characterPresets.ts:53-70`
|
||||
- 对话风格值:`blunt`、`wary`、`evasive`、`measured`、`gentle`、`teasing`、`dry`、`steady`、`direct`、`fragmented`、`deflecting`
|
||||
- `src/data/characterPresets.ts:368-379`, `525-536`, `839-850`, `1024-1038`
|
||||
- 动画资源名:`Double Jump`、`jump attack`、`Wall Slide`、`skill1 bullet FX` 等
|
||||
- `src/data/characterPresets.ts:384-386`, `541-543`, `855-857`, `1043-1045`
|
||||
- `guardStyle` / `warmStyle` / `truthStyle` 的英文原值
|
||||
- 这些值会在角色预设编辑器与动作 / 风格下拉中透出
|
||||
|
||||
### 4. Build / 标签词典
|
||||
|
||||
- `src/data/buildTags.ts:42`, `56`, `91`, `126-147`, `309-316`
|
||||
- `assassin`
|
||||
- `fieldcraft`
|
||||
- `breaker`
|
||||
- `caster`
|
||||
- `armor`
|
||||
- `relic`
|
||||
- `material`
|
||||
- `consumable`
|
||||
- `rare`
|
||||
- `wuxia`
|
||||
- `xianxia`
|
||||
- `neutral`
|
||||
- 这些原始 tag 会通过物品标签、build profile 和编辑器预览进入显示层
|
||||
|
||||
## 五、本轮复核中未发现新增中文乱码的范围
|
||||
|
||||
### 游戏 UI
|
||||
|
||||
- `src/components/CharacterChatModal.tsx`
|
||||
- `src/components/CharacterDetailModal.tsx`
|
||||
- `src/components/CharacterPanel.tsx`
|
||||
- `src/components/MapModal.tsx`
|
||||
|
||||
说明:
|
||||
- 上述文件大体已中文化。
|
||||
- 仍可能存在少量英文缩写、内部 ID 或技术词,但本轮没有再发现新的明显中文乱码。
|
||||
|
||||
### 数据层
|
||||
|
||||
- `src/data/scenePresets.ts`
|
||||
- `src/data/npcInteractions.ts`
|
||||
- `src/data/treasureInteractions.ts`
|
||||
- `src/data/customWorldLibrary.ts`
|
||||
- `src/data/customWorldRuntime.ts`
|
||||
|
||||
说明:
|
||||
- 本轮在 `src/data/` 中没有扫到新的中文乱码。
|
||||
- 当前数据层问题主要是英文 tag、role、rarity、pieceName 等原始值会被上层编辑器直接显示。
|
||||
|
||||
## 六、建议优先级
|
||||
|
||||
1. 先修 `src/components/preset-editor/PresetEditorPanels.tsx`
|
||||
- 当前最集中的真乱码源
|
||||
- 已经影响角色 / NPC / 场景 / 敌对资源四个主编辑子页
|
||||
2. 再修 `src/components/preset-editor/shared.ts` 与 `src/components/GameShell.tsx`
|
||||
- 一个影响预设编辑入口 tab 与世界标签
|
||||
- 一个影响玩家主界面底部导航
|
||||
3. 然后处理 `src/components/CustomWorldEntityEditorModal.tsx`
|
||||
- 量不大,但按钮文案已经坏到影响操作判断
|
||||
4. 最后统一清英文术语
|
||||
- 游戏 UI:`AdventurePanel`、`AdventureEntityModal`、`CompanionCampModal`、`NpcModals`、`CharacterSelectionFlow`
|
||||
- 编辑器 UI:`PresetEditor.tsx`、`ItemCatalogEditor.tsx`、`StateFunctionEditor.tsx`、`npcVisualEditorPersistence.ts`
|
||||
- 数据层:`itemDesign.ts`、`monsterPresets.ts`、`characterPresets.ts`、`buildTags.ts`
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
# 游戏 UI / 预设 / 编辑器 UI 文案排查
|
||||
|
||||
日期:`2026-03-31`
|
||||
|
||||
## 说明
|
||||
|
||||
- 本文档基于当前分支源码重新复核,直接按 UTF-8 读取,不沿用旧审计文档中的乱码文本。
|
||||
- 只记录会出现在游戏 UI、预设编辑器 UI、结果页预览或保存反馈中的文本。
|
||||
- `import`、变量名、注释、仅内部使用的路径名,不计入本次问题清单。
|
||||
- 位图图片里的内嵌文本未做 OCR,本次只看源码层可见文案。
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 当前问题可以分成 3 类:
|
||||
- 真实中文乱码或截断。
|
||||
- 英文或英文缩写直接暴露在中文界面。
|
||||
- 预设数据中的英文原始值直接透出到编辑器或预览。
|
||||
- 乱码最集中的文件:
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx`
|
||||
- `src/components/NpcVisualEditor.tsx`
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx`
|
||||
- `src/components/GameShell.tsx`
|
||||
- `src/editor/shared/FormFields.tsx`
|
||||
- 英文最集中的文件:
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx`
|
||||
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx`
|
||||
- `src/components/PresetEditor.tsx`
|
||||
- `src/components/ItemCatalogEditor.tsx`
|
||||
- `src/components/StateFunctionEditor.tsx`
|
||||
- 预设数据层仍有一批英文原始值会直接透出到 UI:
|
||||
- `src/data/itemDesign.ts`
|
||||
- `src/data/monsterPresets.ts`
|
||||
- `src/data/characterPresets.ts`
|
||||
- `src/data/buildTags.ts`
|
||||
|
||||
## 一、已确认的中文乱码 / 截断
|
||||
|
||||
| 范围 | 文件 | 行号 | 当前文本示例 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 游戏 UI | `src/components/GameShell.tsx` | `598`, `611`, `624` | `瑙掕壊` / `鍐掗櫓` / `鑳屽寘` | 主流程底部三个 tab 标签已写坏 |
|
||||
| 游戏 UI | `src/components/AdventurePanel.tsx` | `569-571` | `已完<E5B7B2>?` / `已交<E5B7B2>?` / `进行<E8BF9B>?` | 任务状态标签出现截断乱码 |
|
||||
| 游戏 UI | `src/components/CharacterDetailModal.tsx` | `223` | `属<>?` | 角色详情分区标题截断 |
|
||||
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `242`, `384`, `430`, `434` | `鏀寔...URL` / `宸查€?` / `鍙栨秷` / `淇濆瓨淇敼` | 自定义世界实体编辑弹窗的占位、选中态、取消和保存按钮已写坏 |
|
||||
| 编辑器 UI | `src/components/preset-editor/shared.ts` | `42-55` | `瑙掕壊` / `鍦烘櫙` / `鏁屽 NPC` / `姝︿緺` / `浠欎緺` / `鑷畾涔変笘鐣?` | 预设编辑器主 tab 和世界标签存在乱码 |
|
||||
| 编辑器 UI | `src/components/preset-editor/PresetEditorPanels.tsx` | `1269`, `1364`, `1371-1372`, `1467`, `1477-1486`, `1521`, `1654-1661`, `1689`, `1707` | 多处整句乱码 | 主编辑面板说明文案、预览模式、帮助文本、提示段落大面积损坏 |
|
||||
| 编辑器 UI | `src/components/NpcVisualEditor.tsx` | `463`, `521`, `550`, `701-705`, `719`, `786-833` | 多处整句乱码 | NPC 视觉编辑器的空态、失败提示、回滚提示、页头说明和多组选项已写坏 |
|
||||
| 编辑器 UI | `src/editor/shared/FormFields.tsx` | `156` | `淇濆瓨涓?..` | 通用保存按钮的“保存中...”状态显示乱码 |
|
||||
|
||||
## 二、游戏 UI 中的英文残留
|
||||
|
||||
### 1. 冒险主界面与奖励弹层
|
||||
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:114-125`
|
||||
- 奖励物品描述 fallback 仍是整句英文,如 `restores HP during the run`、`works as a rare relic reward`。
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:136-157`
|
||||
- 任务目标展示里仍有 `BOUNTY TARGET`、`CACHE TRACE`、`SPAR SESSION`、`Inspect the hidden reward site`。
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:262-291`
|
||||
- 任务奖励卡里仍有 `REWARD CACHE`、`Tap an item icon to inspect its details.`、`Affinity`、`Currency`、`No item bounty attached to this quest.`。
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:351-358`
|
||||
- 目标详情卡仍有 `Objective`、`Area`。
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:490`, `525`, `668`
|
||||
- 统计说明、保存禁用提示、空任务提示仍是英文,如 `Inspect play time, kills, quests, and travel history.`、`Saving is temporarily disabled...`、`No active quests yet.`。
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:749`, `781-785`, `831`, `887-908`, `925-1016`
|
||||
- 完成奖励与战斗奖励弹层仍有 `Claim reward`、`QUEST COMPLETE`、`Reward ready`、`Quest reward claimed`、`Battle reward`、`LOOT CACHE`、`No loot dropped this time.`、`Rarity`、`Quantity`、`Slot`、`Not equippable`、`Usable directly`、`Effect preview: HP + ... / MP + ...`。
|
||||
|
||||
### 2. 实体详情与 NPC 交互
|
||||
|
||||
- `src/components/AdventureEntityModal.tsx:1073`, `1111`, `1163-1165`, `1252`, `1428`
|
||||
- 仍有 `NPC 信息`、`NPC`、`x{item.quantity}`、`Inspect`、`Character`、`NPC 背包`。
|
||||
- `src/components/AdventureEntityModal.tsx:892`, `898`
|
||||
- 同伴状态标签仍直接显示 `HP` / `MP`。
|
||||
- `src/components/CompanionCampModal.tsx:177-178`, `233-234`, `255`
|
||||
- 同伴卡片和空态句子里仍有 `HP` / `MP` / `NPC`。
|
||||
- `src/components/NpcModals.tsx:79`, `252`, `273`, `356`, `408`
|
||||
- 交易弹窗与详情弹窗里仍有 `x{item.quantity}`、`NPC 商品列表`、`这个 NPC 当前没有可售商品。`、`NPC 商品`、`效果预览:HP + ... / MP + ...`。
|
||||
|
||||
### 3. 开场流程与角色选择
|
||||
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx:28-44`
|
||||
- 角色名、称号、定位、标签全部是英文,如 `Sword Princess`、`Royal Blade`、`Vanguard`、`STR`、`AGI`、`Female`、`Male`。
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx:329-391`
|
||||
- 面板标题和按钮仍有 `Character Stats`、`Gender:`、`Backstory`、`Customize`、`Details`、`Enter Camp`、`Go`。
|
||||
- `src/components/game-shell/PreGameSelectionFlow.tsx:63-75`
|
||||
- 自定义世界生成进度仍全是英文,如 `Finalizing world archive...`、`Generating core NPCs...`、`Parsing world setup...`。
|
||||
- `src/components/game-shell/PreGameSelectionFlow.tsx:252-308`
|
||||
- 开场按钮和入口仍有 `New Game`、`Start Game`、`Developer Team`、`Go`、`CONTACTS`、`WORLD SELECT`、`Back`。
|
||||
- `src/components/game-shell/PreGameSelectionFlow.tsx:344-421`
|
||||
- 世界卡片与自定义世界入口仍有 `Online`、`Featured`、`Saved`、`Playable`、`Landmarks`、`Custom`、`Create Custom World`、`Enter a world setup...`。
|
||||
- `src/components/GameShell.tsx:630`, `651`, `695`
|
||||
- Suspense fallback 仍显示 `Loading party panel`、`Loading adventure panel`、`Loading inventory panel`。
|
||||
|
||||
### 4. 其他游戏 UI
|
||||
|
||||
- `src/components/CharacterDetailModal.tsx:112`
|
||||
- `数量 x{item.quantity}` 中的 `x` 仍保留英文数量前缀。
|
||||
|
||||
## 三、编辑器 UI 中的英文残留
|
||||
|
||||
### 1. 编辑器入口与共享配置
|
||||
|
||||
- `src/components/PresetEditor.tsx:65-73`
|
||||
- 页头完整为英文:`Preset Workshop`、`Unified Preset Preview And Editor` 及其说明段。
|
||||
- `src/components/preset-editor/shared.ts:43`, `60-72`
|
||||
- 主 tab 仍有 `NPC`;动画和技能风格选项仍直接使用 `idle`、`move`、`attack`、`die`、`steady`、`burst`、`mobility`、`finisher`、`projectile`。
|
||||
|
||||
### 2. 预设编辑器主面板
|
||||
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1267`, `1594`
|
||||
- 保存反馈仍是 `Saved.`。
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1277-1279`, `1327-1328`, `1414-1415`, `1608-1609`, `1647-1648`
|
||||
- 多个分区标题和描述仍是占位英文 `Section` / `Editor section.`。
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1283`, `1442`, `1448`
|
||||
- 表单标签出现错误拼接,如 `Field"NPC"`、`Field"ID"`。
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1320`, `1640`
|
||||
- 保存按钮文字仍是 `Save`。
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx:1421`, `1658-1661`, `1692`, `1698`, `1701`, `1710`, `2149`
|
||||
- 仍有 `NPC ID`、`Monster Encounter`、`NPC Encounter`、`Empty Scene`、`None`、`NPC`、`FPS` 等英文或英文缩写。
|
||||
|
||||
### 3. 物品 / 行为 / NPC 视觉编辑器
|
||||
|
||||
- `src/components/ItemCatalogEditor.tsx:648`, `729`, `736`, `760`, `767`, `783`, `800`
|
||||
- 仍有 `HP`、`MP`、`CD`、`Build Buff`、`ID`。
|
||||
- `src/components/ItemCatalogEditor.tsx:793-817`
|
||||
- `buildProfile.role`、`setId`、`pieceName` 等原始英文值直接显示在输入框。
|
||||
- `src/components/StateFunctionEditor.tsx:818-821`, `885`, `915`
|
||||
- 预览信息里仍有 `HP`、`No visible target`、`n/a`。
|
||||
- `src/components/StateFunctionEditor.tsx:1060-1064`
|
||||
- 保存失败/成功提示仍是英文:`Failed to save option behavior overrides`、`Option behavior overrides saved.`。
|
||||
- `src/components/StateFunctionEditor.tsx:1106`, `1185`, `1191`, `1217`
|
||||
- 仍直接展示 `battle` / `idle`、`AnimationState` 原值、`idle` / `move` / `attack`,以及 `steady` / `burst` / `mobility` / `finisher` / `projectile`。
|
||||
- `src/components/NpcVisualEditor.tsx:538`, `714`, `781`, `798`
|
||||
- 仍有 `Save failed`、`Current NPC`、`Custom Hair Color`、`Hide Facial Hair`。
|
||||
- `src/components/npcVisualEditorPersistence.ts:26`, `31`, `45`, `50`
|
||||
- 保存提示仍为 `Failed to save NPC visual overrides`、`Saved NPC visual overrides to src/data/npcVisualOverrides.json.`、`Failed to save NPC layout config`、`Saved shared NPC layout config.`。
|
||||
|
||||
### 4. 自定义世界结果页 / 编辑弹窗
|
||||
|
||||
- `src/components/CustomWorldEntityCatalog.tsx:346`
|
||||
- 说明文案里直接暴露资产名 `MedievalFantasyCharacters`。
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx:242`, `458`
|
||||
- 图片路径占位里仍保留 `URL`;NPC 形象编辑说明里直接出现 `MedievalFantasyCharacters`。
|
||||
|
||||
## 四、预设 / 数据层中会透出 UI 的英文原始值
|
||||
|
||||
### 1. 物品预设
|
||||
|
||||
- `src/data/itemDesign.ts:56-58`, `67-69`, `123-149`
|
||||
- `worldAffinity`、`role`、`rarity`、`tags` 中仍有 `neutral`、`wuxia`、`xianxia`、`fieldcraft`、`breaker`、`caster`、`berserker`、`assassin`、`common`、`rare`、`epic` 等原始值。
|
||||
- `src/data/itemDesign.ts:213-219`
|
||||
- `pieceName` 仍为 `boots`、`chest`、`gloves`、`helm`、`leggings`、`shield`、`weapon`。
|
||||
- `src/data/itemDesign.ts:538-545`, `581-598`, `731-748`, `906-913`
|
||||
- 描述拼接和构筑信息里仍直接出现 `build`、`role`、`dust`、`crystal`、`gem` 等英文原始词。
|
||||
- 这些值会直接透出到 `ItemCatalogEditor` 的标签、构筑字段和预览信息。
|
||||
|
||||
### 2. 怪物掉落预设
|
||||
|
||||
- `src/data/monsterPresets.ts:494-536`, `647-721`
|
||||
- 掉落类别仍有 `Armor`、`Relic`、`Material`、`Consumable`。
|
||||
- 掉落名称仍有 `Carapace Plate`、`Guard Core`、`Spore Pouch`、`Burst Cap`、`Ashfire Feather`、`Serpent Eye`、`Tide Ink`、`Lake Pearl`、`Thorn Nectar`。
|
||||
- 掉落描述仍有整句英文,如 `A toxin sac prized by alchemists and assassins alike.`。
|
||||
- 这些值会进入战斗奖励、物品详情和交易 UI。
|
||||
|
||||
### 3. 角色预设
|
||||
|
||||
- `src/data/characterPresets.ts:54-70`
|
||||
- 会话风格原始值仍为 `blunt`、`wary`、`evasive`、`measured`、`gentle`、`teasing`、`dry`、`steady`、`direct`、`fragmented`、`deflecting`。
|
||||
- `src/data/characterPresets.ts:368-386`, `525-543`, `839-857`, `1024-1045`
|
||||
- 动画文件夹 / 前缀与风格原始值仍有 `Double Jump`、`jump attack`、`Wall Slide`、`guardStyle`、`warmStyle`、`truthStyle`。
|
||||
- 这些值会透出到角色预设编辑器、技能预览和部分选择器。
|
||||
|
||||
### 4. Build / 标签词典
|
||||
|
||||
- `src/data/buildTags.ts:42`, `56`, `91`, `126-147`, `308-316`
|
||||
- 仍有 `assassin`、`fieldcraft`、`breaker`、`caster`、`weapon`、`armor`、`relic`、`material`、`consumable`、`rare`、`wuxia`、`xianxia`、`neutral` 等原始标签。
|
||||
- 这些值会在物品编辑器标签、构筑画像和相似度映射结果中直接显示。
|
||||
|
||||
## 五、优先级建议
|
||||
|
||||
1. 先修 `src/components/preset-editor/PresetEditorPanels.tsx` 和 `src/components/NpcVisualEditor.tsx`
|
||||
- 这两处是当前编辑器侧最严重的问题源,既有大面积乱码,也有大量英文占位词。
|
||||
2. 再修游戏首屏与奖励相关 UI
|
||||
- 优先处理 `src/components/adventure-panel/AdventurePanelOverlays.tsx`
|
||||
- 优先处理 `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||
- 优先处理 `src/components/game-shell/CharacterSelectionFlow.tsx`
|
||||
3. 然后修直接影响主流程判断的乱码
|
||||
- `src/components/GameShell.tsx`
|
||||
- `src/components/AdventurePanel.tsx`
|
||||
- `src/components/CharacterDetailModal.tsx`
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx`
|
||||
- `src/editor/shared/FormFields.tsx`
|
||||
4. 最后补“显示层映射”
|
||||
- 为 `itemDesign.ts`、`monsterPresets.ts`、`characterPresets.ts`、`buildTags.ts` 这类预设原始值统一增加中文显示映射,避免继续把内部英文值直接透给编辑器和游戏 UI。
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
# 游戏 UI / 预设 / 编辑器文本审计
|
||||
|
||||
日期:`2026-04-01`
|
||||
|
||||
## 范围
|
||||
|
||||
- 扫描范围:`src/components/`、`src/editor/`、`src/routing/`、`src/hooks/`、`src/services/`、`src/data/`
|
||||
- 聚焦对象:
|
||||
- 游戏内实际可见 UI 文本
|
||||
- 预设编辑器与自定义世界编辑器中的可见文本
|
||||
- 会直接透出到游戏 UI / 编辑器 UI 的预设原始值
|
||||
- 未覆盖:
|
||||
- 图片资源内嵌文字的 OCR
|
||||
- `docs/` 历史文档本身
|
||||
- 单纯内部实现用的 import path、className、asset path、纯 id 常量
|
||||
|
||||
## 方法
|
||||
|
||||
- 先做一轮源码级 AST 扫描,抽取 JSX 可见文本、按钮文案、占位文案、标签文案和常见说明文案。
|
||||
- 再做一轮“反向解码”复核:
|
||||
- `瑙掕壊 -> 角色`
|
||||
- `鍦烘櫙 -> 场景`
|
||||
- `姝︿緺 -> 武侠`
|
||||
- `鏈煡 AI 閿欒 -> 未知 AI 错误`
|
||||
- 结论只保留当前源码里仍然存在的问题,不直接沿用旧审计文档。
|
||||
|
||||
## 结论摘要
|
||||
|
||||
- 当前仍然有 3 类问题:
|
||||
1. 真实乱码:主要在 `appRoutes.tsx`、`AdventurePanel.tsx`、`CharacterDetailModal.tsx`、`useStoryGeneration.ts`、`preset-editor/shared.ts` 和 4 个拆分后的预设面板文件中。
|
||||
2. 游戏 / 编辑器英文残留:主要在 `AdventurePanelOverlays.tsx`、`AdventureEntityModal.tsx`、`PreGameSelectionFlow.tsx`、`NpcVisualEditor.tsx`、`ItemCatalogEditor.tsx`、`StateFunctionEditor.tsx`、自定义世界编辑器几处。
|
||||
3. 预设原始值直接透出:主要在 `characterPresets.ts`、`itemDesign.ts`、`monsterPresets.ts`、`buildTags.ts`、`scenePresets.ts`、`stateFunctions.ts`。
|
||||
- 编辑器侧当前最明显的重灾区不是旧的 `PresetEditorPanels.tsx` 大文件,而是已经拆分出的:
|
||||
- `src/components/preset-editor/shared.ts`
|
||||
- `src/components/preset-editor/CharacterPresetPanel.tsx`
|
||||
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
|
||||
- `src/components/preset-editor/ScenePresetPanel.tsx`
|
||||
- `src/components/preset-editor/MonsterPresetPanel.tsx`
|
||||
- 游戏主流程里影响最直观的点:
|
||||
- 路由加载页文本乱码
|
||||
- 冒险面板里的任务状态 / 对话状态 / NPC 交互短描述乱码
|
||||
- AI 错误兜底文案乱码
|
||||
|
||||
## 一、游戏 UI:已确认乱码
|
||||
|
||||
| 文件 | 行号 | 当前文本 / 范围 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/routing/appRoutes.tsx` | `103-115` | `LOADING EDITOR`、`LOADING GAME`;`姝e湪杞藉叆缂栬緫鍣?..`;`姝e湪杞藉叆鍐掗櫓...` | 路由级加载屏文案。后两段是真乱码;结合反向解码可确定原意分别接近“正在载入编辑器...”和“正在载入冒险...”。 |
|
||||
| `src/components/AdventurePanel.tsx` | `99`、`101`、`103`、`109`、`111`、`113` | `查看库存与价<E4B88E>?`、`聊聊并试探口<E68EA2>?`、`看看能得到什么帮<E4B988>?`、`离开并继续探<E7BBAD>?`、`战斗决胜<E586B3>?`、`切磋几招看身<E79C8B>?` | NPC 交互短描述里有多处截断 / 乱码。 |
|
||||
| `src/components/AdventurePanel.tsx` | `200`、`203` | `可作为制作材<E4BD9C>?`、`任务奖励物品,可用于后续路线、交易或构筑规划<E8A784>?` | 任务奖励物品说明文本被截断。 |
|
||||
| `src/components/AdventurePanel.tsx` | `569-571` | `已完<E5B7B2>?`、`已交<E5B7B2>?`、`进行<E8BF9B>?` | 任务状态标签乱码。 |
|
||||
| `src/components/AdventurePanel.tsx` | `771` | `<60>?` | 对话气泡里的屏幕阅读器标签损坏。 |
|
||||
| `src/components/AdventurePanel.tsx` | `833`、`837`、`870` | `剧情推演<E68EA8>?..`、`对话进行<E8BF9B>?`、`剧情推理完成,继续后显示新的冒险选项<E98089>?` | 加载态 / 流式对话态 / 继续冒险提示都有截断。 |
|
||||
| `src/components/CharacterDetailModal.tsx` | `35-36`、`223` | `女<>?`、`男<>?`、`属<>?` | 性别标签与“属性”标题乱码。 |
|
||||
| `src/hooks/useStoryGeneration.ts` | `1214`、`1266`、`1409`、`1549`、`1978`、`2325` | `鏈煡 AI 閿欒` | 游戏故事流里 AI 失败时的统一兜底提示乱码;可反解为“未知 AI 错误”。 |
|
||||
|
||||
## 二、游戏 UI:英文残留
|
||||
|
||||
### 1. 冒险面板和奖励弹层
|
||||
|
||||
- `src/components/adventure-panel/AdventurePanelOverlays.tsx`
|
||||
- `554-570`:`Adventure stats`、`Current area:`、`ADVENTURE SUMMARY`、`enemies defeated`、`items in inventory`、`scene transitions so far`
|
||||
- `622-668`:`Quest log`、`Total quests:`、`No active quests yet.`
|
||||
- `711-798`:`QUEST BRIEF`、`Claim reward`、`QUEST COMPLETE`、`Reward ready`、`Reward pickup is now available in the quest log.`、`Open quest log`
|
||||
- `887-1016`:`Battle reward`、`Defeated enemies:`、`BATTLE END`、`LOOT CACHE`、`Tap an item icon to inspect its details.`、`No usable loot dropped this time.`、`No loot dropped this time.`、`Rarity:`、`Quantity:`、`Slot:`、`Not equippable`、`Usable directly`、`Passive / non-immediate item`、`Effect preview: HP +`、`MP +`、`Cooldown -`、`Tags:`、`none`
|
||||
- `src/components/AdventurePanel.tsx`
|
||||
- `359-388`:`REWARD CACHE`、`Tap an item icon to inspect its details.`、`Affinity`、`Currency`、`No item bounty attached to this quest.`
|
||||
- `636-638`:`Current area`
|
||||
- `803`、`824`:两个按钮都显示 `Refresh`
|
||||
|
||||
### 2. 实体详情、同伴、交易
|
||||
|
||||
- `src/components/AdventureEntityModal.tsx`
|
||||
- `892`、`898`:`HP`、`MP`
|
||||
- `1073`:`NPC 信息`
|
||||
- `1111`:`敌对NPC`、`NPC`
|
||||
- `1163`:数量前缀 `x{item.quantity}`
|
||||
- `1165`:`Inspect`
|
||||
- `1428`:`NPC 背包`
|
||||
- `src/components/CompanionCampModal.tsx`
|
||||
- `177-178`、`233-234`:`HP`、`MP`
|
||||
- `255`:`NPC`
|
||||
- `src/components/NpcModals.tsx`
|
||||
- `252`、`273`、`356`:`NPC 商品列表`、`这个 NPC 当前没有可售商品。`、`NPC 商品`
|
||||
- `408`:`效果预览:HP +... / MP +... / 冷却 -...`
|
||||
- `src/components/CharacterDetailModal.tsx`
|
||||
- `112`:`数量 x{item.quantity}`
|
||||
|
||||
### 3. 开场流程与加载态
|
||||
|
||||
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||
- `48-49`:`QQ Group`、`WeChat`
|
||||
- `81`、`89`:`核心NPC`
|
||||
- `471-473`:`Wuxia Base`
|
||||
- `519-527`:`Custom`、`Create Custom World`、`Enter a world setup and let the system generate playable characters, NPCs, items, and landmarks.`
|
||||
- `src/components/game-shell/CharacterSelectionFlow.tsx`
|
||||
- `401`:`Character Details`
|
||||
- `406`:`Current Character`
|
||||
- `src/components/GameCanvas.tsx`
|
||||
- `32`:`Loading scene`
|
||||
- `src/components/GameShell.tsx`
|
||||
- `859`:`正在加载 NPC 交互...`
|
||||
|
||||
### 4. 运行时文案源头
|
||||
|
||||
- `src/data/sceneObservation.ts`
|
||||
- `9-36` 整段观察结果仍是英文:
|
||||
- `You pause to listen...`
|
||||
- `Possible NPCs: ...`
|
||||
- `Possible hostile NPCs: ...`
|
||||
- `Possible treasure clues: ...`
|
||||
- `Boss clue: ...`
|
||||
- `src/hooks/useStoryGeneration.ts`
|
||||
- `216`、`219`:最近战斗 / 最近协作提示仍是英文
|
||||
- `639-646`:营地聊天结果文本混用了英文句子
|
||||
- `662-667`:预览对话选项里仍有 `Speak with ...` 与 `Focus on the person in front of you first...`
|
||||
|
||||
## 三、编辑器 UI:已确认乱码
|
||||
|
||||
### 1. 共享标签与世界名
|
||||
|
||||
- `src/components/preset-editor/shared.ts`
|
||||
- `42`:`瑙掕壊`,可反解为 `角色`
|
||||
- `44`:`鍦烘櫙`,可反解为 `场景`
|
||||
- `45`:`鏁屽 NPC`,可反解为 `敌对 NPC`
|
||||
- `46`:`鐗╁搧`,可反解为 `物品`
|
||||
- `47`:`鍔熻兘`,可反解为 `功能`
|
||||
- `53`:`姝︿緺`,可反解为 `武侠`
|
||||
- `54`:`浠欎緺`,可反解为 `仙侠`
|
||||
- `55`:`鑷畾涔変笘鐣?`,基本可判定原意是“自定义世界”,但当前字符串已经不完整
|
||||
|
||||
### 2. 角色预设面板
|
||||
|
||||
- `src/components/preset-editor/CharacterPresetPanel.tsx`
|
||||
- `79`:空状态 / 顶部说明整段乱码
|
||||
- `372-373`:装备区标题乱码
|
||||
- `395-397`:背包区标题乱码
|
||||
- `446-447`:技能预览相关标签乱码
|
||||
- `475-477`:技能区提示乱码
|
||||
- `590-592`:底部说明大段乱码
|
||||
|
||||
### 3. 场景 NPC 预设面板
|
||||
|
||||
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
|
||||
- `87`:空状态整段乱码
|
||||
- `267`:技能预览空态说明乱码
|
||||
- `346`:角色 ID 标签乱码
|
||||
- `352`:怪物预设 ID 标签乱码
|
||||
- `389`:视觉编辑器说明整段乱码
|
||||
|
||||
### 4. 场景预设面板
|
||||
|
||||
- `src/components/preset-editor/ScenePresetPanel.tsx`
|
||||
- `52`:空状态整段乱码
|
||||
- `220-221`:敌对 NPC 分区标题乱码
|
||||
- `254-255`:场景 ID 标签乱码
|
||||
- `298-299`:怪物 ID 列表标签乱码
|
||||
- `316-317`:关联 NPC 分区标题乱码
|
||||
|
||||
### 5. 怪物预设面板
|
||||
|
||||
- `src/components/preset-editor/MonsterPresetPanel.tsx`
|
||||
- `53`:顶部说明整段乱码
|
||||
|
||||
## 四、编辑器 UI:英文残留
|
||||
|
||||
### 1. 预设编辑器主面板
|
||||
|
||||
- `src/components/preset-editor/CharacterPresetPanel.tsx`
|
||||
- `278-279`:`Character List`、`Choose a player character, preview it live, and edit the preset fields.`
|
||||
- `325`:`Save Character Overrides`
|
||||
- 多处通用占位仍是 `Section`、`Editor section.`、`Field`
|
||||
- `347`:`Inventory World`
|
||||
- `468-484`:`Skill Loadout`、`Add Skill`
|
||||
- `510`:`Skill ID`
|
||||
- `651`:`Character ID`
|
||||
- `682`:`Asset Variant`
|
||||
- `698`:`Personality`
|
||||
- `713`:`Attributes`、`Adjust the four core character attributes.`
|
||||
- `772`:`Unset`
|
||||
- `798`:`scene-id-1 / scene-id-2`
|
||||
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
|
||||
- `181-182`:`NPC Library`、`Browse and select an NPC preset.`
|
||||
- `186`:`NPC ID`
|
||||
- `223`:`Save NPC Overrides`
|
||||
- `230-246`:`Skill Preview`、`Preview ranged skills from the linked character.`、`Skill`、`World`
|
||||
- `275-276`:`Hostile NPCs use monster presets...`、`Narrative NPCs can preview linked visuals...`
|
||||
- `318-382`:`NPC Details`、`Role`、`Avatar`、`Initial Affinity`、`Description`、`Visual Editor`
|
||||
- `src/components/preset-editor/ScenePresetPanel.tsx`
|
||||
- `145`:`Scene`
|
||||
- `172`:`Save`
|
||||
- `179-193`:`Scene Preview`、`Preview Mode`、`Monster Preview`、`NPC Preview`、`Treasure Preview`、`Empty`
|
||||
- `223`、`230`、`233`、`242`:`None`、`NPC`
|
||||
- `248-272`:`Scene Details`、`World`、`Name`、`Description`
|
||||
- `288`:`Unset`
|
||||
- `src/components/preset-editor/MonsterPresetPanel.tsx`
|
||||
- `172`:`Save Monster Overrides`
|
||||
- `179-180`:`Monster Override Preview`、`Editor section.`
|
||||
- `213-222`:`Attack Range:`、`Speed:`、`HP:`、`Max HP:`
|
||||
- `236`:`Monster ID`
|
||||
- `242`:`Name`
|
||||
- `258`:`Intro Action`
|
||||
- `373`:`FPS`
|
||||
|
||||
### 2. 其他编辑器 / 自定义世界
|
||||
|
||||
- `src/components/ItemCatalogEditor.tsx`
|
||||
- `654`:`HP`、`MP`、`CD`
|
||||
- `677`、`806`:`ID`
|
||||
- `789`:`Build Buff`
|
||||
- `458`、`863`:`public/Icons`、`itemOverrides.json`
|
||||
- `src/components/NpcVisualEditor.tsx`
|
||||
- `463`、`702`、`708`、`718`:`NPC`
|
||||
- `977`:`Shift`
|
||||
- `1028-1052`:`Current loadout:`、`Unknown headgear`、`No headgear`、`Unknown main hand`、`No main hand`、`Unknown off hand`、`No off hand`
|
||||
- `src/components/CustomWorldEntityCatalog.tsx`
|
||||
- `139`、`268`、`276`、`349`:`NPC`
|
||||
- `224`:`WORLD DOSSIER`
|
||||
- `346`:`MedievalFantasyCharacters`
|
||||
- `src/components/CustomWorldEntityEditorModal.tsx`
|
||||
- `242`:`URL`
|
||||
- `460`:`MedievalFantasyCharacters`
|
||||
- `478-479`、`730`、`758-759`:`AI`、`AI生成NPC形象`、`AI生成场景`
|
||||
- `631-653`:`NPC`
|
||||
- `src/components/PresetEditor.tsx`
|
||||
- `71`:介绍文案里仍然直接显示 `NPC`
|
||||
- `src/components/StateFunctionEditor.tsx`
|
||||
- `803`:`Failed to play preview`
|
||||
- `818-821`:`HP`、`No visible target`
|
||||
- `915`:`n/a`
|
||||
- `1060-1064`:`Failed to save option behavior overrides`、`Option behavior overrides saved.`
|
||||
- `1106`:直接显示原始 `state`
|
||||
- `1185`:直接把 `AnimationState` 值作为 label
|
||||
- `1191`:敌对 NPC 反应动画里仍直接显示 `idle` / `move` / `attack`
|
||||
- `1217`:技能风格仍直接依赖 `steady` / `burst` / `mobility` / `finisher` / `projectile`
|
||||
|
||||
## 五、预设 / 数据层:会直接透出 UI 的英文原始值
|
||||
|
||||
这一部分不是“源码内部英文就算问题”,而是“当前编辑器或预览没有做显示映射,导致原始英文值直接露给用户”。
|
||||
|
||||
### 1. 角色预设
|
||||
|
||||
- `src/data/characterPresets.ts`
|
||||
- 动作文件夹 / 前缀仍是英文:
|
||||
- `363-379`
|
||||
- `520-536`
|
||||
- `739-755`
|
||||
- `834-850`
|
||||
- `1019-1038`
|
||||
- 对话风格原始值仍是英文:
|
||||
- `384-386`:`blunt` / `dry` / `direct`
|
||||
- `541-543`:`wary` / `dry` / `fragmented`
|
||||
- `760-762`:`blunt` / `teasing` / `deflecting`
|
||||
- `855-857`:`blunt` / `steady` / `direct`
|
||||
- `1043-1045`:`measured` / `steady` / `fragmented`
|
||||
- 技能风格 / 投射方式仍是英文:
|
||||
- `407-408`、`445`、`473-474`
|
||||
- `572-573`、`604-605`、`636-637`、`668-669`、`700-701`
|
||||
- `791-792`、`815-818`
|
||||
- `886`、`906`、`926-927`、`958-959`、`990-991`
|
||||
- `1066-1077`、`1109-1110`、`1133-1146`、`1178-1179`
|
||||
- 这些值当前会在角色预设编辑器、技能预览和部分行为预览里直接露出。
|
||||
|
||||
### 2. 物品设计 / Build 标签
|
||||
|
||||
- `src/data/itemDesign.ts`
|
||||
- `56-201`:`worldAffinity` / `role` / `rarity` 原始值仍是英文,如 `neutral`、`wuxia`、`xianxia`、`fieldcraft`、`breaker`、`berserker`、`legendary`
|
||||
- `213-219`:`pieceName` 仍是 `boots`、`chest`、`gloves`、`helm`、`leggings`、`shield`、`weapon`
|
||||
- `820`:说明文本里仍混入 `build`
|
||||
- `src/data/buildTags.ts`
|
||||
- `11-291`:整套 build tag id 都是英文,如 `quickblade`、`combo`、`dash`、`ranged`、`burst`、`caster`、`vanguard`、`paladin`、`starter`
|
||||
- 这些值会进入物品编辑器、构筑标签和相关预览。
|
||||
|
||||
### 3. 怪物与掉落
|
||||
|
||||
- `src/data/monsterPresets.ts`
|
||||
- `490-736`:掉落 id、稀有度、tag 原始值大量是英文,如 `rare`、`uncommon`、`armor`、`material`、`relic`、`healing`
|
||||
- `718-736`:有两条掉落本身是完整英文可见值:
|
||||
- `Consumable` / `Thorn Nectar` / `Sticky sap that can be refined into emergency recovery tonic.`
|
||||
- `Relic` / `Devour Bloom` / `A predatory blossom that stores concentrated life force.`
|
||||
|
||||
### 4. 场景 / 行为 / 锻造 / NPC 交互
|
||||
|
||||
- `src/data/scenePresets.ts`
|
||||
- `349-651`:场景 id 全部是英文连字符格式,如 `wuxia-bamboo-road`、`xianxia-cloud-gate`
|
||||
- 当前在编辑器 ID 字段中会直接显示。
|
||||
- `src/data/stateFunctions.ts`
|
||||
- `113-372`:`category` 原始值仍是 `battle` / `recovery` / `escape` / `idle`
|
||||
- 编辑器预览还会直接显示动画 / delivery 原始值。
|
||||
- `src/data/forgeSystem.ts`
|
||||
- `264`:描述里混入 `build`
|
||||
- `274-281`:`relic`、`epic`、`setId`、`pieceName` 等原始值会进入物品编辑器链路
|
||||
- `src/data/npcInteractions.ts`
|
||||
- `207-209`:兜底对话风格仍是 `measured` / `steady` / `fragmented`
|
||||
|
||||
## 六、建议修复顺序
|
||||
|
||||
1. 先修最影响主流程观感的真实乱码。
|
||||
- `src/routing/appRoutes.tsx`
|
||||
- `src/components/AdventurePanel.tsx`
|
||||
- `src/components/CharacterDetailModal.tsx`
|
||||
- `src/hooks/useStoryGeneration.ts`
|
||||
2. 再修预设编辑器的共享标签和 4 个拆分面板。
|
||||
- `src/components/preset-editor/shared.ts`
|
||||
- `src/components/preset-editor/CharacterPresetPanel.tsx`
|
||||
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
|
||||
- `src/components/preset-editor/ScenePresetPanel.tsx`
|
||||
- `src/components/preset-editor/MonsterPresetPanel.tsx`
|
||||
3. 再统一清理英文残留。
|
||||
- 游戏端优先:`AdventurePanelOverlays.tsx`、`AdventureEntityModal.tsx`、`PreGameSelectionFlow.tsx`
|
||||
- 编辑器端优先:`ItemCatalogEditor.tsx`、`NpcVisualEditor.tsx`、`StateFunctionEditor.tsx`、自定义世界编辑器
|
||||
4. 最后做“显示层映射”,避免预设原始英文继续漏到 UI。
|
||||
- `characterPresets.ts`
|
||||
- `itemDesign.ts`
|
||||
- `buildTags.ts`
|
||||
- `monsterPresets.ts`
|
||||
- `scenePresets.ts`
|
||||
- `stateFunctions.ts`
|
||||
|
||||
## 七、备注
|
||||
|
||||
- 本次结论以当前源码为准,和旧审计文档相比,已有一部分旧问题已经被修掉。
|
||||
- `src/components/preset-editor/PresetEditorPanels.tsx` 现在只是 re-export 壳文件,真正的问题已经分散到拆分后的 panel 文件里。
|
||||
- `src/components/preset-editor/shared.ts` 里的几处乱码已经可以明确反解,适合优先直接修正。
|
||||
- `src/data/` 中很多英文值本身可能是内部枚举,但只要当前编辑器 / 预览没有做中文映射,就仍应视为“会暴露到用户侧”的文本问题。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 文本与乱码审计总览
|
||||
|
||||
这一组文档记录的是同一条清理链路的不同阶段:从“发现哪里有英文/乱码”到“扩展到 prompt、npcInteraction、编辑器深层文本”。
|
||||
这一组只保留当前仍需要执行的文本与乱码审计入口。早期逐日扫描已经聚合到当前结论,不再保留旧稿链路。
|
||||
|
||||
## 当前推荐入口
|
||||
|
||||
@@ -11,17 +11,8 @@
|
||||
3. [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md)
|
||||
适合看“扩展重查版”的 UI / 预设 / 编辑器问题面。
|
||||
|
||||
## 历史时间线
|
||||
|
||||
- [EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md](./EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md):较早期的整体首轮盘点。
|
||||
- [GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md](./GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md):复查阶段,开始收紧范围和口径。
|
||||
- [GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md](./GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md):继续复核真实乱码与英文残留。
|
||||
- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md):对上一轮的续扫补充。
|
||||
- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md):继续收敛 UI、预设与编辑器问题。
|
||||
- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md):进入更明确的审计范围与方法阶段。
|
||||
|
||||
## 融合结论
|
||||
|
||||
- 早期几份文档主要负责“摸清哪里有问题”。
|
||||
- 早期几轮扫描已经完成使命,核心结论是:不要把乱码当成普通文案改写,先确认真实编码;文本修复要优先处理会进入玩家体验和 AI 生成链路的内容。
|
||||
- `2026-04-02` 两份文档开始把重点收敛到真正会影响玩家体验和 AI 生成质量的链路。
|
||||
- 现在做文本修复时,不必从最早一份开始逐篇读;优先看 `CHINESE_MOJIBAKE_INVENTORY` 和 `2026-04-02` 两份即可。
|
||||
|
||||
@@ -239,6 +239,8 @@ function buildNpcFirstContactOptionCatalog(
|
||||
- 角色第一次真正对玩家开口时说什么,必须由 `npc_chat` 对应的 prompt 约束来生成,并要求首句是自然招呼或开场判断。
|
||||
- 不能再用“某人看着你,像是在等你把话接下去”这类第三人称占位旁白充当可见对话历史首句,也不能在聊天 state 里本地硬编码一条替代台词。
|
||||
- 当玩家在场景中第一次真正撞上角色型 NPC 并进入聊天时,应直接触发一轮由 NPC 主动开口的模型回复;这一轮只生成 NPC 自己的首句与后续可选回应,不得代替玩家补写未说过的话。
|
||||
- 负好感或敌对关系不应跳过主动开口;如果玩家从 NPC 交互面板点击 `npc_chat`,且该角色尚未完成 `firstMeaningfulContactResolved`,仍要走同一条 NPC 主动开场链路。负好感只影响语气、敌对聊天指令与后续可选功能,不影响“由角色先发言”的首遇行为。
|
||||
- 好感度小于 `0` 的角色在聊天终止时不进入 `story_continue_adventure` 收束态。无论是玩家主动退出聊天,还是模型通过敌对聊天指令主动结束聊天,底部选项都固定收束为 `npc_fight` 与 `battle_escape_breakout`:按钮文案分别为“战斗”“逃跑”。点击“战斗”进入 NPC 战斗结算链路;点击“逃跑”执行现有 `battle_escape_breakout` function,完成脱离演出与后续状态更新。
|
||||
|
||||
4. 首遇状态下,不允许前两项直接变成:
|
||||
- 深背景追问
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 创作页作品删除入口设计 2026-04-24
|
||||
|
||||
## 背景
|
||||
|
||||
创作页作品卡曾把删除作为底部大按钮展示,并且只对带 `profileId` 的 RPG 作品传入删除回调,导致大鱼、拼图、以及部分草稿作品没有删除入口。用户预期是:删除不是主操作,放在卡片右上角的小 icon 即可;任何作品都应该能删除。
|
||||
|
||||
## 落地规则
|
||||
|
||||
- 作品卡整体就是继续创作 / 继续完善 / 查看详情入口,不再在底部展示“继续完善”等重复主按钮。
|
||||
- 作品卡右上角固定展示删除 icon,底部主操作区只保留体验等必须独立触发的正向操作。
|
||||
- 点击作品卡任意非独立按钮区域都进入继续完善链路;点击删除或体验时不得冒泡触发作品卡打开。
|
||||
- 作品卡保留键盘可访问性:焦点落在卡片时按 Enter 或空格等同点击作品,焦点落在删除 / 体验按钮时只执行对应按钮动作。
|
||||
- 删除入口不按发布状态隐藏:草稿、已发布作品均可删除。
|
||||
- 删除入口不按玩法类型隐藏:RPG、大鱼吃小鱼、拼图作品均应在创作页可删除。
|
||||
- 点击删除前保留浏览器确认弹窗,避免误触;删除中仅禁用当前作品卡的删除 icon。
|
||||
- 删除成功后刷新或替换对应玩法的作品列表,确保卡片立即消失。
|
||||
|
||||
## 工程边界
|
||||
|
||||
- 前端只负责表现和触发删除,实际删除由 `server-rs` API 与 SpacetimeDB 模块过程完成。
|
||||
- 大鱼作品按 `sourceSessionId` 删除创作 session,并同步清理消息、素材槽和运行快照。
|
||||
- 拼图作品按 `profileId` 删除作品 profile,并同步清理来源 Agent session、消息和入口运行快照。
|
||||
- RPG 已发布/持久草稿按 `profileId` 走既有自定义世界删除链路;纯 Agent session 草稿按 `sessionId` 走 owner-only session 删除过程,并清理消息、操作与草稿卡。
|
||||
- 自定义世界 Agent 的异步进度写回必须通过 `upsert_custom_world_agent_operation_progress` 过程落到 SpacetimeDB,`server-rs` 只做字符串入参与过程封装,不在 API 层维护额外进度状态。
|
||||
- `server-rs` 的删除路由使用 Axum 标准 `Path(sessionId)` 提取参数,并在进入 SpacetimeDB 前做 owner-only 与空值校验,避免 handler 签名和过程入参漂移。
|
||||
@@ -325,7 +325,6 @@
|
||||
例如:
|
||||
|
||||
- `initialAffinity`
|
||||
- `dangerLevel`
|
||||
- 精确数值型 build 倾向
|
||||
- 复杂掉落预算
|
||||
|
||||
|
||||
@@ -596,7 +596,6 @@ chapterXpBudget =
|
||||
1. `SceneChapterBlueprint.acts` 数量
|
||||
2. 当前章节 hostile NPC 数量
|
||||
3. 当前章节任务 step 中战斗目标占比
|
||||
4. `dangerLevel`
|
||||
5. linked thread 是否为主线高压线程
|
||||
|
||||
## 6.4 实际速度评估规则
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 移动端创作页新建作品紧凑布局设计
|
||||
|
||||
## 目标
|
||||
|
||||
移动端创作页顶部的新建作品模块只承担快速进入创作模板的作用,不承担规则解释和长说明承载。模块在首屏中最多占用约 1/3 高度,把更多空间留给作品列表和筛选操作。
|
||||
|
||||
## 落地范围
|
||||
|
||||
- 入口组件:`src/components/custom-world-home/CustomWorldCreationStartCard.tsx`
|
||||
- 外层页面:`src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- 模板元数据继续复用 `PLATFORM_CREATION_TYPES`,不新增前端业务逻辑。
|
||||
|
||||
## 移动端布局规则
|
||||
|
||||
1. 顶部标题行压缩成单行:左侧标题,右侧仅保留简短状态,不再显示说明段落。
|
||||
2. 模板入口在手机端使用横向滚动胶囊卡片,每个卡片保持单行动作感,不堆叠成长列表。
|
||||
3. 卡片高度控制在约 4rem 内,标题与状态信息并排组织,避免大留白。
|
||||
4. 模块本体使用 `max-height: 33svh` 作为硬约束,内容超出时优先在模板入口行内横向滚动,不撑高页面。
|
||||
5. 桌面端保持网格入口,但同步收紧内边距和卡片留白,避免移动端与桌面端表现割裂。
|
||||
|
||||
## 文案约束
|
||||
|
||||
- UI 不新增规则说明类文案。
|
||||
- 原有“直接选择游戏创作模板,立刻进入对应的共创工作台。”说明在移动端隐藏,桌面端保留为辅助说明。
|
||||
- 锁定、可创建、正在开启等状态继续来自既有模板元数据或忙碌状态。
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 平台入口分类与创作 Tab 强化设计
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在不新建平台入口系统的前提下,直接扩展现有 `RpgEntryHomeView` 主 Tab:
|
||||
|
||||
- 新增“分类” Tab,用作品标签聚合所有公开发布作品。
|
||||
- 强化“创作” Tab 的导航视觉权重,让它在底部导航中居中并更醒目。
|
||||
- 登录态底部导航顺序为:首页、分类、创作、存档、我的。
|
||||
- 未登录态底部导航只保留:首页、创作、分类,其中创作保持居中。
|
||||
|
||||
## 2. 数据边界
|
||||
|
||||
本次只做前端展示重排,不新增后端接口:
|
||||
|
||||
- 分类数据来源使用现有 `latestEntries` 与 `featuredEntries` 的公开作品列表。
|
||||
- 标签来源沿用 `buildPlatformWorldTags(entry)`,公开作品会映射为题材、角色数、地标数。
|
||||
- 同一公开作品若同时出现在精选与最新中,按 `ownerUserId + profileId` 去重。
|
||||
- 点击分类作品继续走现有 `onOpenGalleryDetail`,不改变详情页和登录拦截逻辑。
|
||||
|
||||
## 3. 交互规则
|
||||
|
||||
### 3.1 登录态
|
||||
|
||||
底部导航展示 5 个入口:
|
||||
|
||||
1. 首页
|
||||
2. 分类
|
||||
3. 创作
|
||||
4. 存档
|
||||
5. 我的
|
||||
|
||||
创作入口位于第三位,视觉上使用更大的图标壳、轻微上浮、渐变高亮和阴影,保证它是主行动入口。
|
||||
|
||||
### 3.2 未登录态
|
||||
|
||||
底部导航展示 3 个入口:
|
||||
|
||||
1. 首页
|
||||
2. 创作
|
||||
3. 分类
|
||||
|
||||
不展示“存档”和“我的”,避免未登录用户在底部导航看到必须登录后才有价值的入口。创作入口位于第二位,保持几何居中。
|
||||
|
||||
### 3.3 桌面端
|
||||
|
||||
桌面侧栏同步增加“分类”,但保持纵向导航,不强行做居中布局。创作入口仍使用强调样式。
|
||||
|
||||
## 4. 分类页布局
|
||||
|
||||
分类页为独立 Tab 面板,不在首页下方展开:
|
||||
|
||||
- 顶部展示标签胶囊,默认选中作品数量最多的标签。
|
||||
- 标签切换后,下方网格展示该标签下所有公开作品。
|
||||
- 无公开作品时展示现有空状态组件。
|
||||
- 分类页不写玩法规则说明类长文案,只保留必要标题、短状态文案和作品卡片。
|
||||
|
||||
## 5. 验收点
|
||||
|
||||
- 登录态移动端底部导航顺序准确,创作在 5 个 Tab 中居中。
|
||||
- 未登录态移动端底部导航只显示 3 个 Tab,创作在中间。
|
||||
- 分类 Tab 能按标签切换并展示公开作品。
|
||||
- 创作 Tab 在移动端和桌面端都比普通 Tab 更醒目。
|
||||
- 不修改 server-node,不新增后端逻辑。
|
||||
@@ -71,6 +71,7 @@
|
||||
弹窗内默认只保留:
|
||||
|
||||
- 标题:`登录账号`
|
||||
- 登录方式页签:`短信登录` / `密码登录`
|
||||
- 手机号输入框
|
||||
- 验证码输入框
|
||||
- 获取验证码按钮
|
||||
@@ -88,6 +89,19 @@
|
||||
- “先登录再同步进度”这类描述性文案
|
||||
- 占据视觉主体的装饰信息块
|
||||
|
||||
## 3.2.1 登录页签落地约束
|
||||
|
||||
账号面板需要把短信验证码登录和密码登录拆成互斥页签,避免两个登录表单在同一个面板里上下堆叠。
|
||||
|
||||
- 同时开放短信与密码登录时,面板顶部展示两个居中的文字页签,当前页签使用深色字重和短下划线强调。
|
||||
- 只渲染当前页签对应的输入区;切换页签不弹出新面板,不展示二维码入口。
|
||||
- `短信登录` 页签包含手机号、验证码、获取验证码和主按钮。
|
||||
- `密码登录` 页签只包含手机号、密码、主按钮和忘记密码入口;不支持邮箱、用户名或叙世号。
|
||||
- 密码登录只是手机号验证码登录的补充方式:只有已登录并设置过密码的手机号账号才能使用,不能在密码页签创建账号。
|
||||
- `密码登录` 主按钮固定为 `登录`,不得使用 `注册/登录`。
|
||||
- 未开放某个登录方式时不展示对应页签,避免用户进入不可用表单。
|
||||
- 移动端页签保持等分点击区域,输入框与按钮宽度仍随弹窗收缩。
|
||||
|
||||
## 3.3 登录成功后的行为
|
||||
|
||||
- 手机号登录成功后,关闭弹窗
|
||||
@@ -100,6 +114,7 @@
|
||||
- 用户主动关闭弹窗时,只关闭弹窗,不改变当前平台页面
|
||||
- 不清空首页浏览状态
|
||||
- 不自动跳转到其他 tab
|
||||
- 登录弹窗下次重新打开时必须恢复初始表单状态:回到默认登录页签、关闭重置密码面板、清空密码 / 验证码 / 图形验证码 / 提示 / 倒计时等本次草稿状态;只允许保留“最近一次成功登录手机号”的本地回填能力。
|
||||
|
||||
---
|
||||
|
||||
@@ -114,6 +129,11 @@
|
||||
- `ready`:渲染平台内容和账号能力
|
||||
- `pending_bind_phone`:继续保留当前绑定手机号流程,不在这次入口改造里拆散
|
||||
|
||||
首屏之后的鉴权刷新补充约束:
|
||||
|
||||
- 平台内容已经渲染后,后续 access token 刷新、401 后重试、账号状态后台重算不能再把整棵平台应用卸载成 `checking / recovering` 加载页。
|
||||
- 后台鉴权重算期间需要保持当前平台页与主 Tab 状态,避免用户手动切到“创作 / 存档 / 我的”后因为鉴权事件闪屏回到首页。
|
||||
|
||||
同时需要在 context 中提供:
|
||||
|
||||
- 当前用户
|
||||
@@ -132,6 +152,8 @@
|
||||
- 不再提供 `AuthGate` 层右上角固定悬浮的全局登录 / 账号信息入口
|
||||
- 登录触发统一来自页面内受保护动作、个人页、存档页等明确入口
|
||||
- 账号信息面板只通过页面内按钮打开,不在平台右上角常驻悬浮
|
||||
- 未登录移动端底部导航不展示“我的”时,平台页头必须保留一个直接可点的 `登录` 入口,避免用户只能通过受保护动作被动触发弹窗
|
||||
- 桌面端平台页头的账号胶囊在未登录时主文案必须直接显示 `登录`,不能只显示“进入账户”这类弱入口
|
||||
|
||||
## 4.2 平台首页数据加载
|
||||
|
||||
@@ -204,3 +226,4 @@
|
||||
3. 未登录选择 RPG 创作类型时,直接弹出登录弹窗,登录后自动进入创作工作台。
|
||||
4. 登录弹窗内没有介绍性大段文字,只剩必要输入与按钮。
|
||||
5. 未登录态首页不会因个人接口失败而出现“读取个人看板失败”“读取作品库失败”之类报错。
|
||||
6. 未登录移动端首页页头存在明确 `登录` 入口,点击后打开同一个登录弹窗。
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# 平台首页响应式布局优化设计
|
||||
|
||||
更新时间:`2026-04-24`
|
||||
|
||||
## 1. 问题结论
|
||||
|
||||
当前平台首页桌面端视觉方向成立,但移动端存在明显横向溢出:Hero 右侧按钮、底部导航末项和部分卡片会被裁切。问题根因不是数据逻辑,而是桌面式固定宽度、外层 padding 与若干卡片最小宽度在窄屏下叠加,超过了 `100vw`。
|
||||
|
||||
## 2. 本次目标
|
||||
|
||||
- 移动端优先保证首页不横向滚动、不裁切底部导航。
|
||||
- Hero 在手机宽度下改为紧凑单列,标题、按钮和标签都完整显示。
|
||||
- 桌面端降低外框视觉噪声,让主内容和 CTA 更清晰。
|
||||
- 空状态区域收敛高度,避免首屏出现大面积空白。
|
||||
|
||||
## 3. 编码落点
|
||||
|
||||
- `src/components/rpg-runtime-shell/RpgRuntimeStageRouter.tsx`
|
||||
- 平台壳移动端使用更小边距,避免根容器加宽。
|
||||
- `src/components/rpg-entry/RpgEntryHomeView.tsx`
|
||||
- 移动端根节点显式 `min-w-0` 与 `overflow-hidden`。
|
||||
- 底部导航外层不再额外制造宽度。
|
||||
- Hero/内容卡片补充 `min-w-0`,窄屏内按容器收缩。
|
||||
- `src/index.css`
|
||||
- 全局与平台壳禁止横向溢出。
|
||||
- 移动端压缩底部导航间距、字号和图标壳尺寸。
|
||||
- 移动端收敛 `platform-page-stage` 圆角与边框层级。
|
||||
- 桌面端保留卡片质感,但弱化多层外框阴影。
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
- `390px` 宽度截图中,首页内容、Hero 按钮和底部四个导航项完整可见。
|
||||
- 桌面端首页仍保持顶部栏、侧边导航、主推荐区和右侧趋势区结构。
|
||||
- 页面不新增功能说明类 UI 文案。
|
||||
- 修改中文文件后通过编码检查。
|
||||
@@ -9,6 +9,9 @@
|
||||
- [CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_PURE_AGENT_COMPARISON_AND_CONVERSION_DESIGN_2026-04-12.md):纯 Agent 式创作工具与结构化工作台方案的优缺点对比,以及转型设计。
|
||||
- [CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md](./CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md):把自定义世界从武侠/仙侠模板依赖迁到跨题材通用设定层的优化设计。
|
||||
- [CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md](./CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md):把模板依赖逐步迁成自定义世界自有设定层,并保证不破坏当前生成流程的优化方案。
|
||||
- [MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md](./MOBILE_CREATION_NEW_WORK_COMPACT_LAYOUT_2026-04-24.md):移动端创作页新建作品模块最多占用首屏约 1/3 高度的紧凑布局设计。
|
||||
- [PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md](./PLATFORM_CATEGORY_AND_CREATE_TAB_DESIGN_2026-04-24.md):平台入口新增分类 Tab、登录态导航裁剪与创作 Tab 视觉强化设计。
|
||||
- [UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md](./UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md):统一平台风与 RPG 像素风模态窗口外壳、交互边界和迁移顺序。
|
||||
- [AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md](./AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md):运行时物品生成系统重设计。
|
||||
- [LEVEL_PROGRESS_AND_CHAPTER_NPC_AUTO_SCALING_DESIGN_2026-04-20.md](./LEVEL_PROGRESS_AND_CHAPTER_NPC_AUTO_SCALING_DESIGN_2026-04-20.md):等级成长、章节经验节奏与 NPC 自动定级设计。
|
||||
- [RPG_NARRATIVE_PLANNING_FULL_PIPELINE_WORKFLOW_2026-04-12.md](./RPG_NARRATIVE_PLANNING_FULL_PIPELINE_WORKFLOW_2026-04-12.md):专业剧情策划构建 RPG 游戏全剧情的工作流程与交付模板。
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# RPG NPC 聊天敌对中止与聊天内 Function 选项设计(2026-04-25)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本次迭代调整运行时 NPC 聊天,让敌对角色聊天从固定五回合上限改为由模型按当前语境判定是否中止;好感度大于等于 0 的角色继续保持可持续聊天,不由模型强制结束。
|
||||
|
||||
同时,原本部分只在退出聊天后才出现的 NPC function 选项,需要进入聊天续写候选池。模型在生成聊天候选时要能看到可触发的 function 选项,并把它们改写成玩家可直接点击的动作文本。聊天中保留“换一换”能力,用于刷新下方候选。
|
||||
|
||||
## 2. 行为规则
|
||||
|
||||
1. 负好感或敌对 NPC 进入聊天后,不再设置固定 5 回合上限。
|
||||
2. 负好感或敌对 NPC 每轮回复后,模型必须判断本轮是否结束聊天。
|
||||
3. 敌对 NPC 判定时应偏向随时结束聊天并进入对峙,但必须结合玩家刚说的话、NPC 性格、当前剧情压力和对话历史。
|
||||
4. 好感度大于等于 0 且非敌对 NPC 不启用模型终止判定,玩家可一直聊天。
|
||||
5. 模型判定终止后,聊天面板不再继续提供聊天输入,只显示“继续”按钮,点击后沿用原流程继续生成冒险选项。
|
||||
6. 点击“退出聊天”不再直接收起聊天页,也不立即进入剧情推理;它会发送一条结束聊天的玩家输入,对方回复后同样只显示“继续”按钮。
|
||||
7. 正向 NPC 的退出聊天只是玩家主动收束,不代表模型强制中止,也不展示战斗/逃跑选项。
|
||||
8. 对负好感或敌对 NPC,在聊天终止后的后续流程仍沿用原敌对出口:继续推进后回到原有战斗或逃跑选择。
|
||||
9. 聊天候选中允许混入当前 NPC 可执行 function,例如交易、送礼、请求帮助、招募、接任务、交任务、开战、离开等。
|
||||
10. Function 候选进入聊天上下文时只作为可触发动作,不在 UI 中展示说明类文本。
|
||||
11. “换一换”在聊天态可用,用于在不推进对话的情况下改排/轮换当前候选;它不调用后端,不改变聊天历史。
|
||||
|
||||
## 3. 后端契约
|
||||
|
||||
`NpcChatTurnDirective` 增加:
|
||||
|
||||
1. `terminationMode`:`none | hostile_model`
|
||||
2. `isHostileChat`:当前聊天是否按敌对中止规则处理
|
||||
3. `functionOptions`:可进入聊天候选的 function 列表,包含 `functionId`、`actionText`、`detailText`、`action`
|
||||
|
||||
`NpcChatTurnCompletionDirective` 增加:
|
||||
|
||||
1. `forceExit`:本轮回复后是否关闭聊天输入
|
||||
2. `closingMode`:保留 `free | foreshadow_close`
|
||||
3. `terminationReason`:`hostile_breakoff | player_exit | null`
|
||||
|
||||
后端返回 `suggestions` 仍是字符串数组,前端按字符串生成 `npc_chat` 续写选项;新增 `functionSuggestions`,元素包含 `functionId` 与模型生成的 `actionText`,前端按对应 function 触发原 NPC action。
|
||||
|
||||
## 4. Prompt 规则
|
||||
|
||||
回复 prompt 需要明确:
|
||||
|
||||
1. 敌对聊天可随时中止,NPC 更偏好结束谈判转入战斗或驱逐。
|
||||
2. 终止不等于在回复正文里直接执行战斗,只需要用台词把对话收束到对峙、威胁、驱逐、最后通牒或行动前一刻。
|
||||
3. 玩家主动退出聊天时,NPC 回复要对这次收束作出回应,并留下自然的后续入口。
|
||||
|
||||
建议 prompt 需要明确:
|
||||
|
||||
1. 常规聊天候选继续生成玩家台词。
|
||||
2. Function 候选要根据提供的 function 列表,改写成玩家可直接点击的动作文本。
|
||||
3. 不输出规则说明,不把 functionId 暴露给玩家。
|
||||
|
||||
## 5. 前端流程
|
||||
|
||||
1. `enterNpcChat` 与每轮 `handleNpcChatTurn` 统一构造聊天可用 function 列表。
|
||||
2. 聊天中的普通候选仍触发 `npc_chat`,function 候选触发原 `handleNpcInteraction` 分流。
|
||||
3. `exitNpcChat` 改为调用 `handleNpcChatTurn`,输入文本为结束聊天意图,并携带 `player_exit` 指令。
|
||||
4. 收到终止结果后,当前 `StoryMoment` 保留 `dialogue`,移除 `npcChatState`,`options` 只保留 `buildContinueAdventureOption()`。
|
||||
5. 点击“继续”后沿用已有 deferred continue / story continue 逻辑进入下一阶段。
|
||||
6. 聊天态“换一换”只轮换当前 `options`,若 function 候选不足则补普通聊天兜底候选。
|
||||
|
||||
## 6. 追加规则:Function 标签与场景幕推进
|
||||
|
||||
1. 运行时选项按钮需要在动作文本前展示 function 短标签,例如 `npc_chat` 显示“聊天”,`npc_quest_accept` / `npc_quest_turn_in` 显示“任务”,`npc_gift` 显示“送礼”。
|
||||
2. 标签只承担识别用途,不展示 functionId,也不展示规则说明。
|
||||
3. NPC 聊天终止后点击“继续冒险”,不再重新请求剧情推理;如果当前场景还有下一幕,直接进入下一幕并展示该幕可用的冒险选项。
|
||||
4. 当当前场景已经到最后一幕,再点击“继续冒险”应展示所有相邻场景入口;选项文案按方向表达为“向东走,前往xxxx”“向南走,前往xxxx”等。
|
||||
5. 相邻场景选项继续使用 `idle_travel_next_scene`,并在 `runtimePayload.targetSceneId` 中携带目标场景,后续点击沿用现有地图跳转结算。
|
||||
6. 若没有场景幕数据,则继续使用当前可用选项作为兜底,不额外生成规则说明文案。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 负好感主 NPC 不再出现固定 `turnLimit: 5`。
|
||||
2. 敌对 NPC 每轮请求会向后端传 `terminationMode: hostile_model`。
|
||||
3. 模型返回 `forceExit: true` 后,聊天输入消失,只显示继续按钮。
|
||||
4. 好感度大于等于 0 的 NPC 聊天不传敌对中止模式。
|
||||
5. 点击退出聊天会新增玩家结束聊天气泡与 NPC 回复,而不是直接切走面板。
|
||||
6. 聊天态可看到并点击 function 候选,且“换一换”可改变候选顺序。
|
||||
7. 选项文字前出现中文 function 标签,且标签不改变原 actionText。
|
||||
8. 聊天结束后的“继续冒险”直接进入下一幕;最后一幕则展示多个相邻场景方向入口。
|
||||
93
docs/design/UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md
Normal file
93
docs/design/UNIFIED_MODAL_WINDOW_DESIGN_2026-04-25.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 统一模态窗口设计 2026-04-25
|
||||
|
||||
## 背景
|
||||
|
||||
当前前端已有两套稳定视觉资产:
|
||||
|
||||
- 平台侧:`platform-overlay`、`platform-modal-shell`、`platform-auth-card` 等主题变量。
|
||||
- RPG 运行时:`pixel-nine-slice`、`pixel-modal-shell` 与 `UI_CHROME.modalPanel` 九宫格边框。
|
||||
|
||||
但弹窗结构仍分散在业务组件内,常见重复包括遮罩层、点击遮罩关闭、`role="dialog"`、`aria-modal`、移动端底部贴边、桌面居中、最大高度、滚动区域和关闭按钮。新增弹窗时容易出现 z-index、无障碍属性、移动端高度和视觉边界不一致。
|
||||
|
||||
## 目标
|
||||
|
||||
新增统一组件 `UnifiedModal`,只负责弹窗外壳和交互边界,不接管业务内容:
|
||||
|
||||
- 统一遮罩、面板、标题区、内容区、底部区结构。
|
||||
- 支持平台风与像素风两种外观,不混用两套视觉资产。
|
||||
- 默认移动端优先,平台风移动端底部弹出、桌面居中;像素风保持游戏内居中弹窗。
|
||||
- 默认提供 `role="dialog"`、`aria-modal`、标题关联、Escape 关闭和遮罩点击关闭。
|
||||
- 支持禁用关闭,用于生成中、保存中等不可打断流程。
|
||||
- 支持 Portal 渲染到 `document.body`,避免被父层 `overflow` 裁剪。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不一次性迁移所有旧弹窗,避免运行时大面积回归。
|
||||
- 不把业务按钮、表单、状态文案放进通用组件。
|
||||
- 不改变现有主题变量、九宫格素材、平台和 RPG 的视觉风格。
|
||||
- 不新增第三方弹窗库。
|
||||
|
||||
## 组件接口
|
||||
|
||||
`UnifiedModal` 核心参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `open` | 是否显示。为 `false` 时返回 `null`。 |
|
||||
| `variant` | `platform` 或 `pixel`。默认 `platform`。 |
|
||||
| `title` | 标题,同时作为默认 `aria-label` 来源。 |
|
||||
| `description` | 可选副标题,显示在标题下方。 |
|
||||
| `children` | 主内容区。 |
|
||||
| `footer` | 可选底部操作区。 |
|
||||
| `onClose` | 关闭回调。 |
|
||||
| `closeDisabled` | 禁止遮罩、Escape 和关闭按钮触发关闭。 |
|
||||
| `closeOnBackdrop` | 是否允许点击遮罩关闭,默认允许。 |
|
||||
| `showCloseButton` | 是否显示右上关闭按钮,默认显示。 |
|
||||
| `size` | `sm`、`md`、`lg`、`xl`、`fullscreen`。 |
|
||||
| `zIndexClassName` | z-index class,默认 `z-[90]`。 |
|
||||
| `panelClassName` / `bodyClassName` / `footerClassName` | 局部样式扩展。 |
|
||||
| `portal` | 是否渲染到 `document.body`,默认开启。 |
|
||||
|
||||
## 使用边界
|
||||
|
||||
### 平台风弹窗
|
||||
|
||||
用于平台首页、登录注册、作品结果、创作工作台等非 RPG 运行时界面。
|
||||
|
||||
要求:
|
||||
|
||||
- 使用 `variant="platform"`。
|
||||
- 面板使用 `platform-modal-shell` 主题变量。
|
||||
- 移动端优先底部贴边,大屏居中。
|
||||
- 不在弹窗内放功能说明式长文案,只放任务所需信息。
|
||||
|
||||
### 像素风弹窗
|
||||
|
||||
用于 RPG 运行时、地图、背包、角色详情、NPC 交易等游戏内面板。
|
||||
|
||||
要求:
|
||||
|
||||
- 使用 `variant="pixel"`。
|
||||
- 面板使用 `pixel-nine-slice pixel-modal-shell`。
|
||||
- 默认使用 `getNineSliceStyle(UI_CHROME.modalPanel)`。
|
||||
- 标题、内容和底部仍由业务方控制,避免通用组件内写入玩法解释。
|
||||
|
||||
## 首批迁移
|
||||
|
||||
首批只迁移平台入口创作类型弹窗:
|
||||
|
||||
- 文件:`src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`
|
||||
- 目的:验证平台风布局、关闭禁用、标题区、内容区与错误区都可由统一组件承载。
|
||||
|
||||
后续可按风险由低到高迁移:
|
||||
|
||||
1. 结果页小弹窗:`PuzzleResultView`、`BigFishResultView`。
|
||||
2. 平台创作页编辑器弹窗:`RpgCreationEntityEditorShared` 内局部 `ModalShell`。
|
||||
3. RPG 运行时像素风弹窗:`RpgAdventurePanelOverlays`、`AdventureEntityModal`、`NpcModals`。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 新增弹窗优先使用 `UnifiedModal`,不再手写完整 overlay + panel 结构。
|
||||
- 迁移后的弹窗保留原有移动端和桌面布局。
|
||||
- 关闭按钮、遮罩关闭、Escape 行为一致,`closeDisabled` 时都不会关闭。
|
||||
- 类型检查、编码检查通过。
|
||||
@@ -0,0 +1,18 @@
|
||||
# Agent 空会话草稿可见性修正 2026-04-26
|
||||
|
||||
用户从创作中心点进 RPG 或大鱼吃小鱼工作台时,后端会立即创建 Agent session,并写入一条助手欢迎消息。但在用户尚未发送任何消息、也没有传入种子文本时,这个 session 只是临时工作区,不应进入“我的创作”草稿列表。
|
||||
|
||||
本次规则:
|
||||
|
||||
1. 只有存在用户消息、非空 seedText、真实草稿数据或已发布状态时,Agent session 才算作品草稿。
|
||||
2. 助手欢迎消息、默认 anchorPack、空 `{}` draftProfile 不算用户创作内容。
|
||||
3. 过滤必须落在后端 works 聚合层,前端创作中心只消费结果,不负责隐藏空草稿。
|
||||
4. RPG 仍保留已发布 profile 和孤立持久草稿 profile 的展示;未发布且仍有活跃 Agent session 的编译 profile 继续去重。
|
||||
|
||||
涉及入口:
|
||||
|
||||
- `server-rs/crates/spacetime-module/src/lib.rs`
|
||||
- `server-rs/crates/spacetime-module/src/custom_world/mod.rs`
|
||||
- `server-rs/crates/spacetime-module/src/big_fish/session.rs`
|
||||
|
||||
后续如果新增玩法创作 Agent,也必须复用同一判断:创建会话不等于创建草稿,作品列表只展示已经被用户实际开始编辑或已经生成结果的会话。
|
||||
@@ -1,4 +1,4 @@
|
||||
# UI 改动记录(供后续 Agent 阅读)
|
||||
# UI 改动记录(供后续 Agent 阅读)
|
||||
|
||||
本文档汇总 **像素 RPG UI 皮肤化** 相关实现与决策,便于新会话快速接手。更细的命名与规范见同目录上一级的 **`UI_CODING_STANDARD.md`**。
|
||||
|
||||
@@ -163,4 +163,48 @@
|
||||
|
||||
---
|
||||
|
||||
## 13. 2026-04-21 创作中心失效草稿恢复兜底
|
||||
|
||||
- `src/components/rpg-entry/useRpgEntryLibraryDetail.ts` 现在会识别 `custom-world agent session` 的 `404 NOT_FOUND` 读取失败,不再把这类错误直接冒泡成未捕获 Promise。
|
||||
- 当用户在创作中心点击“继续创作”命中失效草稿时,前端会主动清空 `customWorldSessionId` 恢复参数,并刷新一次 works 列表,避免刷新页面后反复尝试恢复同一个坏会话。
|
||||
- 当前交互已收口成平台内可见提示:用户会停留在创作中心,并看到“这份共创草稿已失效,已为你返回创作中心,请重新开始创作。”,而不是卡在空白工作区或只在控制台看到英文异常。
|
||||
- 这次兜底只处理失效会话恢复,不改变正常草稿继续创作、结果页恢复和已发布作品进入世界的主链。
|
||||
|
||||
---
|
||||
|
||||
## 14. 2026-04-24 Agent 工作区恢复指针按用户隔离
|
||||
|
||||
- `custom-world agent session` 现在由 `server-rs` Axum 路由接入 SpacetimeDB procedure,模块内按 `owner_user_id + session_id` 查询;前端恢复旧工作区前必须确认本地指针属于当前登录用户,否则旧账号残留会先请求一次 `/api/runtime/custom-world/agent/sessions/:sessionId` 并产生 404。
|
||||
- `src/services/customWorldAgentUiState.ts` 的 URL 仍只保留 `customWorldSessionId` / `customWorldOperationId`,用户归属只写入 `sessionStorage`,避免把 `userId` 暴露到可分享链接里。
|
||||
- `src/components/rpg-entry/useRpgCreationSessionController.ts` 在恢复初始工作区时会对比 `ownerUserId`,发现不是当前用户就清空恢复指针并停留在创作入口,不再打后端失效 session。
|
||||
- 后续新增 Agent 类恢复入口时,同样要区分“可分享的 URL 指针”和“仅本机使用的登录用户归属”,不要只凭 sessionId 自动恢复受保护资源。
|
||||
|
||||
---
|
||||
|
||||
## 15. 2026-04-24 多玩法 Agent 聊天顶部文案统一隐藏
|
||||
|
||||
- `CreationAgentWorkspace` 已支持在 `title` 与 `assistantSummary` 为空时只展示返回、主操作、进度与锚点区域;各玩法适配层不要再传入“世界共创 / 玩法共创”这类模块标题或引导副文案。
|
||||
- `custom-world`、`big-fish`、`puzzle` 三条 Agent 聊天工作区现在统一隐藏顶部标题与标题下方说明,避免只有 RPG / 自定义世界生效、其他玩法模板仍残留旧文案。
|
||||
- 后续新增玩法模板时,聊天页顶部模块应保持清爽:必要状态放进进度、操作横幅或聊天消息,不把功能说明类文案默认写入 UI。
|
||||
|
||||
---
|
||||
|
||||
## 16. 2026-04-24 创作结果页亮色主题细节补色
|
||||
|
||||
- RPG / 拼图 / 大鱼结果页根容器统一挂 `platform-remap-surface`,让亮色主题能接管遗留的 `text-white`、`text-zinc-*`、`bg-white/*`、`bg-black/*` 和状态色工具类。
|
||||
- 拼图与大鱼结果页顶部 Hero 只增加 `platform-result-hero` 语义类,不改变整体布局;亮色主题下由 `src/index.css` 统一换成暖白底、轻粉高光和平台主按钮色。
|
||||
- 地图弹窗新增 `map-modal-overlay`、`map-modal-shell`、`map-modal-backdrop`、`map-modal-shade`、`map-info-panel` 语义类;亮色主题通过这些类降低暗色遮罩、提亮地图背景、统一节点卡与连线颜色。
|
||||
- 结果页中保存、生成、发布等旧的 `bg-amber-600` / `bg-cyan-600` / `bg-cyan-200` 按钮,在 `platform-remap-surface` 内被映射回 `platform-button--primary` 同款渐变,避免亮色主题下按钮体系割裂。
|
||||
- 后续继续做结果页 UI 细节时,优先补语义 class + `src/index.css` 的 light remap,不要在每个结果页组件里复制一套亮色配色,也不要调整页面整体布局结构。
|
||||
|
||||
---
|
||||
|
||||
## 17. 2026-04-26 创作编辑器关闭确认弹窗亮色主题修正
|
||||
|
||||
- `RpgCreationEntityEditorShared.tsx` 的未保存关闭确认统一收口为 `CloseConfirmDialog`,弹窗只保留确认信息和两个动作,不新增说明文案。
|
||||
- `CloseConfirmDialog` 通过 `platform-close-confirm-dialog` 语义类接入平台主题 token;提示块使用 `--platform-warm-*`,确认按钮使用 `--platform-button-primary-*`,继续编辑按钮使用 `--platform-neutral-*`。
|
||||
- 后续新增关闭 / 退出确认面板时,不要继续复制 `text-amber-50`、`text-sky-50`、`bg-black/*` 这类深色 Tailwind 组合;优先复用语义类,避免亮色主题出现浅底白字和按钮文字不可读。
|
||||
|
||||
---
|
||||
|
||||
_文档目的:交接给下一个 Agent 时,优先读本文件 + `UI_CODING_STANDARD.md`,再改 `uiAssets.ts` / `App.tsx` / `index.css`。_
|
||||
|
||||
@@ -97,6 +97,12 @@
|
||||
- 当前仓库已进一步收口为:
|
||||
不再提供右上角全局账号悬浮条,统一只保留页面内入口与独立账号面板。
|
||||
|
||||
### 4.6 冒险主场景双方角色必须按画面中线镜像
|
||||
- 非滚动画面的交谈、预览、单体对峙态,主角和对面角色不能分别用左右边距、世界坐标和角色宽度重复推算。
|
||||
- 正确做法是先定义“角色容器中心距离画面中线”的统一间距,再让主角中心落在中线左侧、对面角色中心落在中线右侧。
|
||||
- 角色容器宽度、角色图片缩放和左右朝向必须各自独立处理,不能用额外 left inset 去修正角色图片,否则会破坏左右对称。
|
||||
- 自定义图片 NPC、模板角色和组合式 NPC 都要进入同一 112px 场景容器,再按各自素材锚点做场景缩放,保证视觉大小不漂移。
|
||||
|
||||
## 5. 队伍面板经验
|
||||
|
||||
### 5.1 移动端成员列表不能太“卡片化”
|
||||
@@ -197,3 +203,16 @@
|
||||
|
||||
这一轮最关键的经验是:
|
||||
**游戏 UI 的移动端优化,本质不是把元素缩小,而是重组视觉重心、固定操作锚点、让焦点内容在一屏内自然成立。**
|
||||
|
||||
### 10.1 可扮演角色形象预览保持 1:1
|
||||
- 可扮演角色的形象预览容器统一使用 1:1 方形,入口选择轮播、角色资产工坊和结果页角色卡片都不能用纵向长卡片去拉伸预览图。
|
||||
- 预览图片本身使用 `object-contain`,保证 AI 生成主形象、模板像素角色和运行时动画都在方形容器内完整显示,不裁切角色主体。
|
||||
- 卡片可以在方形预览下方放角色名、称号、状态等信息,但这些文本区不能反向影响预览区比例。
|
||||
- 编辑角色弹窗也遵循同一规则:移动端不能用固定高度压扁预览区,预览容器应随宽度保持 `aspect-square`。
|
||||
|
||||
### 10.2 运行画面怪物锚点按视觉底边校准
|
||||
- 对战预览里主角和对手要沿画面中线成对出现,但纵向不能只共用一个 `bottom` 常量。
|
||||
- 怪物精灵帧的空白、体型和脚底位置差异很大,运行画面应按帧高分档下沉,让怪物视觉底边落在主角同一条地面线上。
|
||||
- 后续新增怪物资源时,先检查红圈标注的实际落点,再调整锚点分档或单怪物偏移,避免出现“悬在地面上方”的状态。
|
||||
- 自定义世界里敌对角色已经先作为场景 NPC 存在,即使它同时携带 `characterId` 和 `monsterPresetId`,画布也不能直接沿用模板角色的 `groundOffsetY`;只要 encounter 自身有 `imageSrc` 或 `visual`,就按场景 NPC 自定义形象锚点处理。
|
||||
- 幕预览运行时还会构造“无 `characterId`、但有 `visual` 的场景 NPC”,这类和平相遇分支同样必须套用场景 NPC 自定义形象锚点,否则会停在画面中上部。
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# PC 端世界生成与草稿页布局优化说明 2026-04-24
|
||||
|
||||
## 目标
|
||||
|
||||
在移动端现有布局不变的前提下,只优化 PC 端世界生成页与世界草稿页的信息组织,让页面更紧凑、更有层次,并保留全部已有功能入口。
|
||||
|
||||
## 范围
|
||||
|
||||
- 世界生成页:`src/components/CustomWorldGenerationView.tsx`
|
||||
- 世界草稿页 / 作品页:`src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||
- 新建作品入口:`src/components/custom-world-home/CustomWorldCreationStartCard.tsx`
|
||||
- 作品卡片:`src/components/custom-world-home/CustomWorldWorkCard.tsx`
|
||||
- 筛选标签:`src/components/custom-world-home/CustomWorldWorkTabs.tsx`
|
||||
|
||||
## PC 端落地规则
|
||||
|
||||
1. 移动端默认类名保持原布局语义,只通过 `lg:` / `xl:` 断点追加 PC 布局。
|
||||
2. 世界生成页在 PC 端改为左右双栏:左侧突出进度与阶段,右侧承载玩家设定 / 结构化锚点,减少纵向滚动。
|
||||
3. 世界草稿页在 PC 端将“新建作品”和“作品列表”分区强化:顶部入口更紧凑,作品卡片网格密度提升。
|
||||
4. 不新增规则说明文案,不改变按钮、筛选、删除、体验、进入等功能行为。
|
||||
5. 中文文本只做必要保留,不因为布局调整改写已有中文内容。
|
||||
|
||||
## 视觉策略
|
||||
|
||||
- PC 端使用更明确的 `xl:grid`、固定信息侧栏和更小间距,让主内容首屏承载更多信息。
|
||||
- 卡片在 PC 端降低无效高度,操作按钮与状态信息尽量同行展示。
|
||||
- 作品卡片底部统计标签必须保留在卡片圆角范围内,不能为了压缩高度让标签贴边或被 `overflow-hidden` 裁掉。
|
||||
- 卡片正文摘要优先缩短行数来给底部标签留空间;当标题、摘要或标签变长时,允许卡片自然增高。
|
||||
- RPG 作品卡片点击行为按作品状态分流:草稿统一继续创作,已发布作品进入详情或世界;不要只依赖 `sourceType` 判断草稿可打开性。
|
||||
- 整张作品卡片需要由卡片根节点承载点击与键盘打开能力,避免透明绝对定位按钮在真实浏览器中被判定不可见,导致自动化和用户点击不稳定。
|
||||
- 保留现有 `platform-*` 视觉体系,避免引入新的 UI 系统。
|
||||
@@ -0,0 +1,17 @@
|
||||
# PC 世界档案草稿编辑页布局修正 2026-04-24
|
||||
|
||||
## 背景
|
||||
|
||||
用户反馈 PC 端世界草稿页没有明显变化。复核截图后确认实际页面是世界档案草稿编辑页中的实体目录,而不是创作首页作品列表。
|
||||
|
||||
## 本次修正范围
|
||||
|
||||
- `src/components/CustomWorldEntityCatalog.tsx`
|
||||
|
||||
## 落地要求
|
||||
|
||||
1. 移动端仍保持原来的单列滚动、顶部标签与搜索结构。
|
||||
2. PC 端把超宽单列实体列表改成卡片网格,减少横向空白,提高信息密度。
|
||||
3. PC 端顶部世界标题、标签、搜索和操作按钮更紧凑,避免首屏被空白标题区占用。
|
||||
4. 功能不变:搜索、切换标签、新增、批量删除、选择、编辑、发布入口均保持原有行为。
|
||||
5. 不新增说明类文案,不改写已有中文内容。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 平台首页 Banner 图尺寸修复记录
|
||||
|
||||
更新时间:`2026-04-25`
|
||||
|
||||
## 问题
|
||||
|
||||
首页 Hero / banner 使用作品封面作为背景图。全局 `.platform-surface > *` 会把所有直接子节点重新设为 `position: relative`,导致带有 Tailwind `absolute` 类的背景图和遮罩被覆盖,图片重新进入普通文档流后可能撑高首页,影响首屏布局。
|
||||
|
||||
## 落地
|
||||
|
||||
- 修改 `src/index.css`,将 `.platform-surface > *` 收敛为 `.platform-surface > :not(.absolute)`。
|
||||
- 保留普通内容层的 `z-index: 1`,让文字、按钮仍稳定压在背景之上。
|
||||
- 让 banner 背景图继续使用绝对定位和 `object-cover`,只在固定容器内裁切显示,不参与首页高度计算。
|
||||
|
||||
## 经验
|
||||
|
||||
- 首页、结果页、详情页的背景图都必须由固定尺寸容器承载,图片本身不要参与布局流。
|
||||
- 给通用容器写层级规则时,不能无差别覆盖 `.absolute` 节点,否则会破坏所有“背景图 + 遮罩 + 内容”的结构。
|
||||
- 后续若新增平台 Hero,优先沿用 `platform-surface--hero`,并确认背景图节点仍是 `absolute inset-0 h-full w-full object-cover`。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 可扮演角色外观模板字段删除经验
|
||||
|
||||
## 背景
|
||||
|
||||
可扮演角色曾通过 `templateCharacterId` 保存“外观模板”选择。当前角色主形象已经由 `visualDescription`、`imageSrc` 与生成资产链路承接,外观模板不应继续作为可扮演角色档案字段暴露给用户编辑或持久化。
|
||||
|
||||
## 落地边界
|
||||
|
||||
- 可扮演角色数据结构不再声明 `templateCharacterId`。
|
||||
- 可扮演角色编辑面板删除“外观模板”下拉项,保存时不再补默认模板。
|
||||
- 草稿规范化与资料库读取时丢弃旧数据中的 `templateCharacterId`,避免旧快照把字段带回新数据。
|
||||
- 运行时如需要基础动作、默认立绘或战斗标签,只能通过角色文本、参考 profile 或固定 fallback 规则临时推导模板,不再写回角色字段。
|
||||
- 资产工坊可以继续接收运行时临时模板提示,但该提示不得成为可扮演角色的持久字段。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 新建或编辑可扮演角色时界面不出现“外观模板”。
|
||||
- 保存后的 `playableNpcs` 条目不包含 `templateCharacterId`。
|
||||
- 旧存档带有 `templateCharacterId` 时,进入当前规范化链路后会被丢弃。
|
||||
- 自定义世界运行角色仍能通过推导模板获得基础动作与默认占位图,不因字段删除而中断。
|
||||
@@ -249,7 +249,19 @@
|
||||
3. 针对初始同伴流程补一份单独的状态图 / 时序图
|
||||
4. 对大 chunk 警告做代码分包
|
||||
|
||||
## 14. 一句话总结
|
||||
## 14. SpacetimeDB 绑定桥接层要做同名去重
|
||||
|
||||
`server-rs/crates/spacetime-client` 里有一部分内容是围绕 SpacetimeDB 生成绑定补的手写桥接层。
|
||||
|
||||
经验:
|
||||
|
||||
- 新增 procedure、input type 或 mapper 时,先全局确认 `module_bindings/mod.rs`、`mapper.rs`、业务封装文件里是否已经存在同名声明
|
||||
- `module_bindings/mod.rs` 同一个模块只保留一条 `pub mod` 和一条 `pub use`,不要同时放在 reducer 区和 procedure 区
|
||||
- `mapper.rs` 的字符串枚举解析函数、API 入参结构只保留一个权威定义,业务侧统一复用
|
||||
- 业务封装文件里同一个 procedure 只暴露一个客户端方法,避免 Rust 在编译期出现 E0428、E0252、E0119、E0592 这类重复定义错误
|
||||
- 修复重复绑定时优先删除后追加的重复块,不要重写整文件,避免影响中文注释和生成绑定附近的大段内容
|
||||
|
||||
## 15. 一句话总结
|
||||
|
||||
这个项目真正的开发经验不是“怎么多写一个按钮”,而是:
|
||||
|
||||
|
||||
@@ -250,13 +250,27 @@ node scripts/vite-cli.mjs --port=3000 --host=0.0.0.0
|
||||
|
||||
这类项目里,后者几乎一定导致返工。
|
||||
|
||||
## 13. 一句话总结
|
||||
## 13. 后端修改后的重启与测试
|
||||
|
||||
后端代码更新后统一执行:
|
||||
|
||||
```bash
|
||||
npm run api-server:maincloud
|
||||
```
|
||||
|
||||
执行要求:
|
||||
|
||||
- 该命令是后端更新后的默认重启入口,不再使用此前的后端重启命令。
|
||||
- 重启后必须继续执行与本次后端改动对应的自动测试;涉及 Rust workspace 时优先跑 `server-rs` 下的检查或测试脚本。
|
||||
- 若本次改动涉及 SpacetimeDB 发布、绑定生成或 Maincloud 联调,按 `spacetimedb-cli` 经验执行,并在验证记录中写清楚实际命令与结果。
|
||||
|
||||
## 14. 一句话总结
|
||||
|
||||
这个项目最重要的经验不是“做了多少页面和功能”,而是:
|
||||
|
||||
**必须把 AI 文本生成、本地规则、动画演出、场景状态、编辑器工具这几套系统严格分层,再通过 function 和统一状态流把它们重新接起来。**
|
||||
|
||||
## 14. 相关文档
|
||||
## 15. 相关文档
|
||||
|
||||
如需继续细看已有沉淀,可结合以下文档一起阅读:
|
||||
|
||||
|
||||
@@ -23,3 +23,10 @@
|
||||
- 只需要读一份时,优先看 `PROJECT_WORK_EXPERIENCE_PLAYBOOK`。
|
||||
- 做 UI 改动时,把本目录和根目录的 `UI_CODING_STANDARD.md` 对照着看。
|
||||
- 做运行时流程改动时,把本目录和 `docs/audits/engineering/README.md` 一起看,能更快发现风险边界。
|
||||
|
||||
## 近期专项记录
|
||||
|
||||
- [RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md](./RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md):记录 RPG 底稿阶段角色主形象与场景背景图并行生成约束。
|
||||
- [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。
|
||||
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
||||
- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# RPG 幕背景默认描述来源修正 2026-04-24
|
||||
|
||||
## 背景
|
||||
|
||||
草稿编辑器中“AI 生成幕背景”的“画面内容描述”曾出现类似“温馨员工宿舍第1幕背景;玩家入职后的首个落脚处;玩家会在温馨员工宿舍接住这一章的开场入口。”的默认文本。这类文本不是大模型直接写出的画面描述,而是前端或后端在缺少 `backgroundPromptText` 时,把地点名、幕标题、摘要规则句拼接出来的兜底文案。
|
||||
|
||||
## 落地约束
|
||||
|
||||
1. 幕背景图的默认画面描述必须来自草稿生成链路里的关键场景生成步骤,字段源为 `landmarks[*].actBackgroundPromptTexts[*]`。
|
||||
2. `sceneChapterBlueprints[*].acts[*].backgroundPromptText` 只承接上述幕级大模型产物,不再用 `title`、`summary`、地点描述或规则句拼接。
|
||||
3. 如果大模型漏产某一幕描述,后端规范化只保留空字符串,让后续生图前的 `backgroundPromptText` 校验暴露底稿质量问题,不能伪造可用默认文本。
|
||||
4. 前端编辑器 sanitize 只展示已有 `act.backgroundPromptText`;缺失时留空,不能在 UI 层重新拼接默认描述。
|
||||
5. 手动打开 AI 生成面板时,若字段为空,可由用户输入,但系统默认不替用户生成规则句。
|
||||
|
||||
## 当前实现
|
||||
|
||||
- `server-rs/crates/api-server/src/custom_world_foundation_draft.rs` 的关键场景框架 prompt 要求 LLM 为每个地点生成 3 条 `actBackgroundPromptTexts`。
|
||||
- 草稿合成阶段通过 `build_scene_chapter_blueprints_from_landmarks` 把这些幕级描述写入 `sceneChapterBlueprints[*].acts[*].backgroundPromptText`。
|
||||
- `normalize_scene_act_blueprint` 不再把缺失描述补成“标题 + 摘要 + 通用场景背景”格式。
|
||||
- `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx` 不再用地点名、幕标题和 `actSummary` 生成 `backgroundPromptText` fallback。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 新草稿中每一幕的 `backgroundPromptText` 应该像自然的画面描述,包含主体、前中远景、站位空间、氛围识别点。
|
||||
- 不应再出现“第1幕背景;玩家会在……”这类明显拼接句。
|
||||
- 如果 LLM 漏掉 `actBackgroundPromptTexts`,生成幕背景图阶段应失败并提示缺少 `backgroundPromptText`,而不是静默使用拼接文案。
|
||||
|
||||
## 2026-04-24 并发限流错误处理补充
|
||||
|
||||
- 批量生成幕背景图时,`JoinSet` 子任务的成功值和失败值固定承载 `(chapter_index, act_index, message)`,用于把错误精确标记回对应章节幕。
|
||||
- `Semaphore::acquire` 的 `AcquireError` 不能在子任务中转成裸 `String` 后直接使用 `?`,否则会破坏子任务统一错误类型并导致 `E0277`。
|
||||
- 限流器异常应映射为同一组三元组错误,保持后续 `mark_scene_act_background_generation_error` 和部分成功保留逻辑可复用。
|
||||
@@ -0,0 +1,23 @@
|
||||
# RPG 场景幕角色配置面板布局经验 2026-04-26
|
||||
|
||||
## 背景
|
||||
|
||||
场景多幕配置里的“配置角色”面板是高频编辑弹层,移动端和桌面端都需要快速完成选择并保存。
|
||||
|
||||
## 本次约束
|
||||
|
||||
1. 面板底部不再放“取消”按钮,关闭统一交给标题栏关闭按钮和遮罩。
|
||||
2. “保存角色”必须位于面板底部操作区,角色列表较长时只滚动内容区,不把保存按钮滚出视口。
|
||||
3. 已选角色时仍允许“移除角色”,但移动端纵向排列时保存按钮保持在最底部。
|
||||
4. 不在面板内新增功能说明文本,维持清爽编辑体验。
|
||||
5. 吸底操作区必须使用平台语义色 token,不能写死深色 Tailwind 背景,避免亮色主题下出现突兀深色底栏。
|
||||
6. 已选角色时,底部操作区保持同一行:左侧“移除角色”热区占 1/4,右侧“保存角色”占 3/4;未选角色时保存按钮占满整行。
|
||||
|
||||
## 落地位置
|
||||
|
||||
- `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx`
|
||||
- `SceneActNpcSlotPickerModal`
|
||||
|
||||
## 后续复用
|
||||
|
||||
以后新增类似独立选择弹层时,优先采用“标题栏 + 中间滚动内容 + 底部固定主动作”的结构;取消类动作不要默认占据底部按钮位,避免和主保存动作抢焦点。
|
||||
@@ -0,0 +1,46 @@
|
||||
# RPG 底稿图片并行生成说明 2026-04-24
|
||||
|
||||
## 背景
|
||||
|
||||
RPG 草稿生成进入底稿素材阶段后,角色主形象与场景幕背景图都依赖同一份结构化底稿,但二者之间没有数据依赖。旧流程先生成所有角色主形象,再生成场景背景图,导致用户需要串行等待两类图片任务。
|
||||
|
||||
## 落地约束
|
||||
|
||||
1. 角色主形象与场景背景图必须在 API 编排层并行发起,且类内每个角色、每一幕背景也必须同时调用生图接口,不能只做到“角色大类”和“背景大类”并行。这里的场景背景图指 `sceneChapterBlueprints[*].acts[*]` 中每一幕的 `backgroundImageSrc`,不是世界封面图,也不是只按章节或地点生成一张图。
|
||||
2. SpacetimeDB reducer 只负责持久化操作进度和底稿写入,不承载外部 LLM / 图片生成调用。
|
||||
3. 生图前必须已经有文本设定:角色主形象使用角色对象的 `visualDescription`;幕背景图使用对应幕的 `backgroundPromptText`。缺字段时应中断并暴露底稿质量问题,不能退回 `description`、`summary` 或通用兜底词直接生图。
|
||||
4. 并行分支各自基于同一份底稿副本写入素材字段,完成后只合并背景图生成产物字段,避免覆盖角色图片字段或其他草稿内容。
|
||||
5. 单个大类失败仍按原有失败语义终止底稿写入,保留“生成角色主形象失败”和“生成幕背景图失败”的进度提示。
|
||||
|
||||
## 当前实现
|
||||
|
||||
- `server-rs/crates/api-server/src/custom_world.rs` 在 `spawn_custom_world_draft_foundation_job` 中使用 `tokio::join!` 同时执行:
|
||||
- `generate_draft_foundation_role_visuals`
|
||||
- `generate_draft_foundation_act_backgrounds`
|
||||
- 角色分支使用 `JoinSet` 把所有角色主形象任务一次性投递,返回后再按角色位置写入 `imageSrc` 与 `generatedVisualAssetId`。
|
||||
- 背景分支使用 `JoinSet` 把 `sceneChapterBlueprints[*].acts[*]` 的每一幕背景任务一次性投递,返回后写入 `backgroundImageSrc`、`backgroundAssetId`、`generatedScenePrompt`、`generatedSceneModel`。
|
||||
- `merge_generated_act_backgrounds` 只把背景图字段合并回角色分支副本,再进入后续草稿卡编译和 SpacetimeDB 写入。
|
||||
- 幕背景 prompt 同时兼容 `backgroundPromptText`、`scenePromptText`、`visualPromptText`、`promptText`、`imagePromptText`、`backgroundPrompt`、`visualPrompt`,避免 LLM 输出字段别名导致整批背景图被误判缺失。
|
||||
- 每个角色主形象、每一幕背景图都必须独立自动重试,单项最多尝试 3 次。幕背景图允许部分成功:只要至少一幕成功,就必须保留已成功写入的 `backgroundImageSrc` 并继续生成草稿卡;全部幕都失败时才把素材阶段标记为“生成幕背景图失败”。
|
||||
- 图片任务仍然一次性投递,保证角色与幕背景两类任务不回退到串行编排;但真正请求上游生图服务时必须共用并发闸门。并发数由 `GENARRATIVE_DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS` 或 `DRAFT_ASSET_GENERATION_MAX_CONCURRENT_REQUESTS` 配置,默认 4,避免固定为 2 导致多角色、多幕草稿总耗时过长。
|
||||
- 幕背景图失败文案必须带第几章、第几幕和幕标题,不能只显示“第1幕 / 第2幕 / 第3幕”,否则多章节同名幕会被用户误认为同一失败项重复上报。
|
||||
- 中止或部分失败前必须持久化已经成功生成的部分底稿到会话 `draftProfile`,不能因为某个角色或某一幕失败而丢掉其它已生成的 `imageSrc / generatedVisualAssetId / backgroundImageSrc / backgroundAssetId`。
|
||||
- 每一幕自动生图必须记录 operation、session、第几章、第几幕、sceneId、sceneName、attempt、elapsedMs 与供应商真实错误,避免再次出现只看到“生成幕背景图失败”但无法定位哪张图、哪次请求、哪个上游原因的问题。
|
||||
- 前端看到 `draft_foundation` operation completed 后,不能只延迟一次就读取 `resultPreview`;SpacetimeDB 写入、API 读模型和前端状态同步之间可能有短暂时差,必须短轮询等待结果页 profile 可用后再自动跳转到草稿页,避免卡在“底稿已整理”。
|
||||
- 前端 `CharacterAnimator` 对带 `generatedVisualAssetId` 但尚无 `animationMap` 的自定义角色,所有状态优先渲染生成主图;只有真正发布了动作集后才按动作帧播放,避免运行或战斗状态回落到模板 sprite。
|
||||
|
||||
## 后续注意
|
||||
|
||||
如果后续图片供应商出现强限流,再在网关层做队列或供应商侧限流;不要在 RPG 底稿编排层恢复逐张串行,否则会重新退化成多张图片总耗时累加。
|
||||
|
||||
|
||||
|
||||
|
||||
## 2026-04-25 补充:开局场景也必须逐幕生成背景图
|
||||
|
||||
本次排查发现旧草稿合成只从 `landmarks` 编译 `sceneChapterBlueprints`,导致 `camp` 开局场景只有 `camp.imageSrc`,没有进入 `sceneChapterBlueprints[*].acts[*]` 的幕背景生成队列。后续实现必须遵守:
|
||||
|
||||
1. `camp` 开局场景必须作为 `sceneChapterBlueprints[0]` 写入,`sceneId` 默认使用 `camp.id`,缺失时使用 `camp-1`。
|
||||
2. `camp.actBackgroundPromptTexts` 必须包含 3 条逐幕画面描述,并和普通场景一样生成 `acts[*].backgroundImageSrc`。
|
||||
3. 结果页场景目录可用场景图兜底展示旧草稿的幕缩略图,但新草稿不能只依赖兜底,必须让开局场景真实进入幕背景图生成链路。
|
||||
4. 手动同步场景资产时,必须同时更新 `sceneChapterBlueprints` 与兼容字段 `sceneChapters`,当前主链以 `sceneChapterBlueprints` 为准。
|
||||
@@ -0,0 +1,36 @@
|
||||
# RPG 发布作品广场刷新修复 2026-04-25
|
||||
|
||||
## 背景
|
||||
|
||||
已发布 RPG 作品会进入 `custom_world_profile`,并由 SpacetimeDB 模块同步到公开读模型 `custom_world_gallery_entry`。平台首页和分类页不直接读取“我的作品”,而是共同消费 `/api/runtime/custom-world-gallery` 返回的公开作品列表。
|
||||
|
||||
本次问题表现为:结果页显示发布完成后,作品没有立即出现在平台首页和分类页。
|
||||
|
||||
## 修复原则
|
||||
|
||||
1. `publish_world` 必须写入真实作者公开信息。
|
||||
- Axum 层在执行 `publish_world` 前补充 `authorPublicUserCode` 与 `authorDisplayName`。
|
||||
- SpacetimeDB 模块发布时优先使用 payload 中的作者公开信息,再退回历史兜底。
|
||||
|
||||
2. 发布完成后必须刷新公开广场列表。
|
||||
- 前端 `executePublishWorld` 等待发布 operation 完成后,同时刷新 `refreshPublishedGallery()` 与 `refreshCustomWorldWorks()`。
|
||||
- 首页和分类页都复用 `publishedGalleryEntries`,因此刷新 gallery 后两个页面会同步看到新发布作品。
|
||||
|
||||
## 多玩法公开列表补充
|
||||
|
||||
- 平台首页 / 分类页不是只展示 RPG 作品,也需要展示已经发布到公开接口的 Puzzle 作品。
|
||||
- Puzzle 已有 `/api/runtime/puzzle/gallery` 公开接口;平台入口额外读取该接口,并把 `PuzzleWorkSummary` 映射为首页卡片模型。
|
||||
- 首页 / 分类页按 `rpg:{ownerUserId}:{profileId}` 与 `puzzle:{ownerUserId}:{profileId}` 去重后按发布时间排序。
|
||||
- 点击 RPG 卡片仍进入 RPG 公开详情;点击 Puzzle 卡片进入现有 Puzzle 广场详情,不复用 RPG 详情链路。
|
||||
|
||||
3. 历史已发布作品必须能自动补齐 gallery 投影。
|
||||
- 公开列表读取 `list_custom_world_gallery_entries` 前,会扫描 `custom_world_profile` 中已发布且未删除的 profile。
|
||||
- 若已发布 profile 缺少 `custom_world_gallery_entry`,或缺少公开作品码 / 作者叙世号,会先补齐公开字段并同步 gallery 投影。
|
||||
- 这样旧版本发布成功但未落入广场读模型的作品,在下一次首页 / 分类页读取公开列表时会自动出现。
|
||||
|
||||
## 经验
|
||||
|
||||
- 作品发布链不能只看“我的创作”列表,必须同时检查公开读模型。
|
||||
- 分类页的数据不是独立接口,修首页公开列表通常也会影响分类页。
|
||||
- Agent 发布链和普通 library profile 发布链要共享公开作者字段语义,避免同一个 gallery card 在不同发布入口下字段不一致。
|
||||
- 已发布 profile 与 gallery 投影不是同一张表,线上修复时要考虑历史数据补投影,不能只修新增发布路径。
|
||||
@@ -0,0 +1,30 @@
|
||||
# RPG 角色形象描述数据链路核查 2026-04-24
|
||||
|
||||
## 结论
|
||||
|
||||
草稿生成阶段会让大模型为每个可扮演角色和场景角色生成 `visualDescription`,该字段是角色主形象生成和资产工坊“形象描述”输入框的同一份默认文本来源。
|
||||
|
||||
本次核查发现前端 `normalizeCustomWorldProfileRecord` 曾在规范化 `playableNpcs` / `storyNpcs` 时丢弃 `visualDescription`、`actionDescription`、`sceneVisualDescription`。因此后端草稿 JSON 中有大模型生成的文字,但草稿进入前端编辑器后,资产工坊可能只能回退到 `description`,用户看不到真正的角色形象文字描述。
|
||||
|
||||
## 数据链路
|
||||
|
||||
1. 后端草稿生成:`server-rs/crates/api-server/src/custom_world_foundation_draft.rs`
|
||||
- 角色框架名单 prompt 要求 LLM 输出 `visualDescription`、`actionDescription`、`sceneVisualDescription`。
|
||||
- `visualDescription` 定义为打开角色形象图像生成面板时默认填入的角色形象描述。
|
||||
2. 后端角色主形象生成:`server-rs/crates/api-server/src/custom_world.rs`
|
||||
- `generate_draft_foundation_role_visuals` 从角色对象读取 `visualDescription`。
|
||||
- 缺失时直接失败,提示不能在角色形象设定文本生成前生图。
|
||||
- 生图成功只写回 `imageSrc` 和 `generatedVisualAssetId`,不会覆盖 `visualDescription`。
|
||||
3. 草稿持久化:草稿 profile JSON 保留角色对象字段,`visualDescription` 应与图片字段一起进入保存载荷。
|
||||
4. 前端规范化:`src/data/customWorldLibrary.ts`
|
||||
- `normalizePlayableNpc` / `normalizeStoryNpc` 必须保留三类资产描述字段。
|
||||
5. 资产工坊展示:`src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx`
|
||||
- modal 用角色对象构造 `baseRole`。
|
||||
- `buildDefaultRolePromptBundle(baseRole)` 优先把 `role.visualDescription` 转成 `visualPromptText`。
|
||||
- `RpgCreationRoleVisualSection` 的“形象描述” TextArea 展示 `visualPromptText`。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 草稿生成完毕后,打开某个角色的资产工坊,应在“形象描述”框看到 LLM 生成的 `visualDescription`。
|
||||
- 如果角色有 `visualDescription`,缓存中的旧 `visualPromptText` 不应覆盖它。
|
||||
- 如果角色缺 `visualDescription`,才允许前端回退到更弱的字段或缓存文本。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 世界草稿发布面板与测试入口设计 2026-04-24
|
||||
|
||||
## 目标
|
||||
|
||||
世界草稿页底部不再只有“发布并进入世界”一个动作,而是拆成两个明确入口:
|
||||
|
||||
1. 作品测试:跳过发布阻断项检查,直接进入当前草稿游戏体验。
|
||||
2. 发布:打开发布面板,在面板内集中处理发布阻断项与封面设置,满足条件后发布到广场。
|
||||
|
||||
## 页面范围
|
||||
|
||||
- `src/components/rpg-creation-result/RpgCreationResultActionBar.tsx`
|
||||
- `src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx`
|
||||
- `src/components/CustomWorldEntityCatalog.tsx`
|
||||
- `src/components/rpg-entry/useRpgCreationEnterWorld.ts`
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
|
||||
## 交互规则
|
||||
|
||||
1. 世界草稿页右下角显示“作品测试”和“发布”两个按钮。
|
||||
2. “作品测试”只同步当前草稿结果并进入游戏,不触发发布阻断项检查,也不执行发布动作。
|
||||
3. “发布”打开独立发布面板,不在当前页面下方展开内容。
|
||||
4. 发布面板显示当前阻断项;没有阻断项时允许执行发布。
|
||||
5. 发布面板显示封面预览与封面状态,并提供“设置封面”入口。
|
||||
6. 封面生成、封面上传与封面预览从世界档案页迁移到发布面板;世界档案页不再展示作品封面模块。
|
||||
7. 移动端仍使用底部弹层式面板,PC 端使用居中 modal。
|
||||
|
||||
## 发布后行为
|
||||
|
||||
发布成功后刷新本地结果档案,并进入正式世界;作品由既有 `publish_world` 流程同步到作品库 / 广场。
|
||||
@@ -259,7 +259,7 @@
|
||||
可复用来源:
|
||||
|
||||
- `README.md`
|
||||
- `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md`
|
||||
- `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
|
||||
|
||||
### 5.2 技术研发情况
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
- `docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md`
|
||||
- `docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md`
|
||||
- `docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md`
|
||||
- `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md`
|
||||
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md`
|
||||
- `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
- `docs/audits/engineering/README.md`
|
||||
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
|
||||
|
||||
## 5. 三个方向的共用材料包
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 当前 Agent 创作流程优化执行规划(大白话版)
|
||||
|
||||
更新时间:`2026-04-20`
|
||||
更新时间:`2026-04-21`
|
||||
|
||||
## 先把话说死
|
||||
|
||||
@@ -16,6 +16,22 @@
|
||||
|
||||
这份规划就是基于 [AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md](../audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md) 里已经确认的问题,重新收束出来的一版执行方案。
|
||||
|
||||
## 0.1 当前正式执行基线
|
||||
|
||||
从 `2026-04-21` 起,当前仓库里和这条创作主链直接对应的文件级拆分、阶段验收、工作包进度,统一以以下文档为准:
|
||||
|
||||
1. [../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)
|
||||
2. `docs/technical/CREATION_FLOW_CHAIN_REFACTOR_WORK_PACKAGE_*_PROGRESS_2026-04-21.md`
|
||||
3. [../technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md](../technical/CURRENT_AGENT_CREATION_FLOW_STAGE4_CLEANUP_CHECK_2026-04-21.md)
|
||||
|
||||
本文件继续承担的职责只剩 3 件事:
|
||||
|
||||
1. 解释为什么当前版本只收口现有 Agent 创作动线,不再扩一套新流程
|
||||
2. 冻结这轮明确不做的能力边界
|
||||
3. 给出高层执行顺序与判断标准
|
||||
|
||||
凡是涉及目录落位、命名规范、前后端真相源边界、阶段完成度和工作包状态,统一以后面的技术执行文档为准。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现在最大的问题,用大白话讲是什么
|
||||
@@ -328,17 +344,17 @@
|
||||
|
||||
**让用户走起来更顺,让系统找回内容更稳定。**
|
||||
|
||||
## 第三阶段:再处理旧 pipeline 的降级和冻结
|
||||
## 第三阶段:清理旧链残留表述与剩余兼容误导
|
||||
|
||||
再做:
|
||||
|
||||
1. 旧 `custom-world/sessions` 链降级
|
||||
2. 结果页直改 profile 的旧能力收紧
|
||||
3. 兼容链保留边界写清楚
|
||||
1. 清理旧 `custom-world/sessions` 已删除链路在文档、索引、任务清单里的残留表述
|
||||
2. 结果页直改 profile 的旧能力继续收紧
|
||||
3. 把仍保留的兼容 façade / 兼容字段边界写清楚
|
||||
|
||||
这一阶段的目标是:
|
||||
|
||||
**减少系统自己和自己打架。**
|
||||
**减少旧链文档误导和兼容边界漂移。**
|
||||
|
||||
## 第四阶段:最后做文档清理
|
||||
|
||||
@@ -382,8 +398,8 @@
|
||||
|
||||
应该看到:
|
||||
|
||||
1. 重复 pipeline 明显减少
|
||||
2. 旧链不再继续吞主流程职责
|
||||
1. 已删除旧链不再继续出现在当前执行清单里
|
||||
2. 剩余兼容层的保留边界更清楚
|
||||
3. 后续开发不会再不知道该往哪条链上接
|
||||
|
||||
## 阶段四做完
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
# 当前游戏优先迭代清单(2026-04-03)
|
||||
|
||||
## 结论先说
|
||||
|
||||
当前阶段最不该做的,是继续零散加玩法、加场景、加文案,却让主链路、规则底座和工程门禁继续处在半完成状态。
|
||||
|
||||
按现有文档和代码状态看,建议优先级顺序如下:
|
||||
|
||||
1. `P0`:先恢复工程绿色基线,并把运行时主链路继续拆开
|
||||
2. `P1`:再落统一角色属性底座,作为战斗 / 对话 / 招募 / Build / 掉落 / 任务的共同语义基础
|
||||
3. `P1`:在统一属性底座之上,重做 Build、运行时物品奖励、任务系统三条核心玩法链
|
||||
4. `P2`:最后收尾编辑器共享层、本地 API 分层、移动端体验与运行时包体优化
|
||||
|
||||
一句话判断:
|
||||
|
||||
**现在的优先级不是“继续扩玩法宽度”,而是“先把底层规则、主流程边界和工程可维护性补齐,再扩玩法深度”。**
|
||||
|
||||
---
|
||||
|
||||
## 优先级清单
|
||||
|
||||
## P0-1:恢复绿色基线,收紧质量门禁
|
||||
|
||||
### 为什么必须排第一
|
||||
|
||||
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 已明确指出:当前最值得优先优化的不是继续加功能,而是把“半完成的工程化”补齐。
|
||||
- 文档中提到过 `lint` 失败、`build` warning、核心热区文件被 ESLint ignore、部分测试未进入默认套件,这意味着当前代码库还不在真正稳定的绿色基线。
|
||||
- 在这种状态下继续叠加新玩法,只会把问题扩散到更多运行时链路和编辑器链路。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 修复现有 `lint` / `build warning` / 明确可见的门禁破口
|
||||
- 缩小高风险核心文件的 ignore 范围
|
||||
- 让 `lint + typecheck + test + build + check:content` 成为可信的统一门禁
|
||||
- 对 warning 建立“尽快清零”策略,而不是长期带病开发
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 主开发分支长期保持 `npm run check` 可稳定通过
|
||||
- 核心运行时文件不再依赖长期 ignore 才能过门禁
|
||||
- 构建 warning 收敛到零或有非常明确的短期处理计划
|
||||
|
||||
### 为什么它比新功能更优先
|
||||
|
||||
- 没有绿色基线,后续所有大改都缺少可靠回归保护
|
||||
- 这一步是后面统一属性、任务重构、物品系统重构的前置条件
|
||||
|
||||
---
|
||||
|
||||
## P0-2:继续拆运行时主链路,防止核心 hook 和壳层继续膨胀
|
||||
|
||||
### 为什么必须紧跟在 P0-1 后面
|
||||
|
||||
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`、`docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`、`docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` 都反复强调:这个项目是叙事、状态、演出、界面四条链路耦合的复合项目,不能靠大文件硬扛。
|
||||
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md` 和 `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 一致指出,`useStoryGeneration`、`useCombatFlow`、`GameShell` 仍然是当前最大的复杂度集中点。
|
||||
- 如果不先拆主链,后面的统一属性系统、任务系统、物品导演层都会继续堆进现有巨型流程控制器,技术债只会翻倍。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 把 `useStoryGeneration` 收敛为 orchestration 层,不再直接吞下 NPC、任务、背包、锻造、聊天、奖励等全部细节
|
||||
- 把 `useCombatFlow` 进一步拆成“战斗结算”与“播放/演出同步”
|
||||
- 让 `GameShell` 回到流程壳层职责,把 selection flow、overlay、scene transition 继续下沉
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 新功能接入时,不需要再跨 `story + combat + panel + modal` 四五层一起改
|
||||
- 核心流程可以按领域补测试,而不是只能做人工回归
|
||||
- 后续玩法扩展能优先加领域模块,而不是继续往大 hook 里塞逻辑
|
||||
|
||||
### 这一项的实际意义
|
||||
|
||||
- 这是“后续还能继续做大”的结构前提
|
||||
- 不做这一步,任何系统升级都会越来越难落地
|
||||
|
||||
---
|
||||
|
||||
## P1-1:落地统一角色属性系统,作为全玩法共同底座
|
||||
|
||||
### 为什么它是最优先的玩法底座
|
||||
|
||||
- `docs/prd/AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md` 已经把问题说得很清楚:当前玩家、NPC、怪物、Build、对话、掉落还没有共享同一套解释坐标。
|
||||
- 当前项目已经有 NPC 关系、怪物标签、Build 语义、自定义世界生成能力,但这些系统之间还缺一套统一的世界级属性 schema。
|
||||
- 如果先做任务、物品、Build 深化,而不先统一属性,后面很容易再次出现“每个系统各自解释角色”的分裂。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 为预设世界固化世界级属性 schema
|
||||
- 为玩家角色、怪物、关键 NPC 补 `attributeProfile`
|
||||
- 建立统一的属性解析与校验层
|
||||
- 先让对话 / 招募 / 送礼 / 详情面板开始读取这套新属性解释
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 玩家、NPC、怪物都能落到同一套属性语义里
|
||||
- 聊天、送礼、招募至少有一条链可以直接解释到属性层
|
||||
- 自定义世界也能生成并持久化自己的属性 schema
|
||||
|
||||
### 为什么这项优先于“多做内容”
|
||||
|
||||
- 这是后面 Build、物品、任务三条系统统一升级的共同前提
|
||||
- 没有这层底座,玩法会继续“能跑,但彼此不共语义”
|
||||
|
||||
---
|
||||
|
||||
## P1-2:把 Build 系统从“标签互相影响”改成“标签匹配角色属性”
|
||||
|
||||
### 为什么这里要尽快做
|
||||
|
||||
- `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md` 指出:当前 Build 更像标签网络效应,解释成本高、平衡成本高、角色差异感不够强。
|
||||
- 一旦统一角色属性系统先落地,Build 就是最适合第二个接入的玩法层,因为它最直接影响战斗反馈和角色成长感。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 为 Build 标签补属性亲和度向量
|
||||
- 改写 `buildDamage` 逻辑,让每个标签独立匹配当前角色属性画像
|
||||
- 调整 Build 面板文案,从“标签协同”转成“属性适配度”
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 玩家能理解“为什么这个标签适合当前角色”
|
||||
- 新增标签只影响自身贡献,不再扰动整张标签网络
|
||||
- Build 面板能解释收益来自哪些属性
|
||||
|
||||
### 实际收益
|
||||
|
||||
- 提高可解释性
|
||||
- 降低平衡难度
|
||||
- 让角色差异感真正进入 Build 体验
|
||||
|
||||
---
|
||||
|
||||
## P1-3:重做运行时物品奖励,让奖励真正贴合场景、NPC、最近事件和 Build 缺口
|
||||
|
||||
### 为什么它值得排在任务系统前面
|
||||
|
||||
- `docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md` 明确指出:当前宝藏、NPC、任务、锻造等入口都有物品,但缺少统一导演层,奖励与场景/NPC/事件的贴合度不够高。
|
||||
- 相比任务系统,运行时物品奖励能更快提升“世界贴脸感”和“当下反馈质量”,且可以先从宝藏入口低风险落地。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 增加运行时物品上下文采样、导演层、编译器和叙事回写层
|
||||
- 统一宝藏、NPC 奖励、怪物掉落、任务奖励的物品生成入口
|
||||
- 让奖励优先围绕 Build 标签、限时 Build Buff、少量数值补足来设计
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 至少宝藏和 NPC 奖励接入统一导演层
|
||||
- 物品能解释“为什么在这里出现、和谁有关、补的是什么方向”
|
||||
- 物品来源可以进入背包、剧情、锻造与存档的同一套结构
|
||||
|
||||
### 实际收益
|
||||
|
||||
- 奖励不再像泛用掉落池
|
||||
- 世界、人物、最近剧情与成长反馈终于真正连起来
|
||||
|
||||
---
|
||||
|
||||
## P1-4:把任务系统从“单目标单阶段”升级成“意图 -> 合约 -> 信号推进”
|
||||
|
||||
### 为什么它仍然是高优先级
|
||||
|
||||
- `docs/prd/AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md` 已经指出:当前任务闭环是成立的,但任务来源偏静态、结构偏扁平、状态过粗、奖励和关系变化也不够贴语境。
|
||||
- 当前项目已经具备任务 UI、任务奖励、NPC 交互、剧情推进链,这说明任务系统适合做“升级”,而不是推倒重来。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 新增任务生成上下文、AI 任务意图层、本地任务编译层
|
||||
- 把任务推进改成统一 signal 驱动
|
||||
- 支持多 step、阶段揭示、完成后回报、后续钩子
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- NPC 接任务不再只是静态模板,而是能根据当前局面生成任务意图
|
||||
- 运行时能用统一 signal 推进任务步骤
|
||||
- 奖励除了货币/道具,还能自然进入关系、情报、后续机会
|
||||
|
||||
### 为什么它排在物品系统之后
|
||||
|
||||
- 任务系统耦合更深,适合作为统一属性和统一奖励导演层之后的升级项
|
||||
- 先把属性和物品奖励理顺,任务系统落地时会更稳
|
||||
|
||||
---
|
||||
|
||||
## P2-1:收尾编辑器共享层与本地 API 分层,让内容扩张不再继续拖慢主项目
|
||||
|
||||
### 为什么它不是最前面,但也不能拖太久
|
||||
|
||||
- 最近几份工程审查都指出:编辑器共享层、本地 JSON 写入接口、LLM 代理、Vite 插件职责仍然处于迁移中间态。
|
||||
- 当前项目已经进入“内容工具很多、正式运行时也很重”的阶段,若不收尾这部分,后续每次扩内容都会重复踩基础设施问题。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 继续拆 editor shared 层
|
||||
- 清理迁移残留和死分支
|
||||
- 把本地 API 至少按 `llm proxy / json editor api / asset catalog` 分职责拆开
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 编辑器保存、共享组件、共享 client 不再重复实现
|
||||
- 本地 API 分工清晰,dev / preview 边界清楚
|
||||
- 编辑器扩展不再继续依赖大聚合组件
|
||||
|
||||
---
|
||||
|
||||
## P2-2:继续优化移动端冒险体验、首屏信息密度与运行时包体
|
||||
|
||||
### 为什么它放在 P2
|
||||
|
||||
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` 和 `docs/experience/MOBILE_UI_DEV_EXPERIENCE.md` 都强调:冒险页必须优先保证上方演出、一屏选项和文本区自适应。
|
||||
- 但从当前文档判断,移动端体验和包体问题更像“持续治理项”,不是当前阶段最核心的系统阻塞点。
|
||||
|
||||
### 本阶段要做什么
|
||||
|
||||
- 继续优化冒险页一屏布局与文本滚动策略
|
||||
- 拆 `GameCanvas`、`AdventurePanel` 等高热区大模块
|
||||
- 按真实交互热区继续做 chunk 拆分
|
||||
|
||||
### 做到什么算完成
|
||||
|
||||
- 手机首屏稳定容纳画布、文本和关键选项
|
||||
- 核心页面热区模块更容易维护和测试
|
||||
- 构建产物中的主 chunk 有持续下降趋势
|
||||
|
||||
---
|
||||
|
||||
## 不建议当前优先做的事
|
||||
|
||||
以下内容不是不能做,而是不建议排在当前这轮前面:
|
||||
|
||||
- 大量新增世界、场景、角色 preset
|
||||
- 继续横向扩 NPC 交互种类,但不补统一规则底座
|
||||
- 继续堆宝藏、掉落、锻造分支,但不先做统一物品导演层
|
||||
- 继续增加任务模板数量,但不升级任务 contract
|
||||
- 继续往 `useStoryGeneration` / `useCombatFlow` / `GameShell` 里直接塞新逻辑
|
||||
|
||||
原因很简单:
|
||||
|
||||
**这些工作会让表面内容变多,但不会让项目变得更稳,反而会放大当前已经存在的结构问题。**
|
||||
|
||||
---
|
||||
|
||||
## 推荐迭代顺序
|
||||
|
||||
### 第一阶段:先稳住工程与主流程
|
||||
|
||||
1. 绿色基线与门禁收紧
|
||||
2. 运行时主链拆分
|
||||
|
||||
### 第二阶段:先补统一语义底座
|
||||
|
||||
1. 统一角色属性系统
|
||||
2. Build 改为属性适配
|
||||
|
||||
### 第三阶段:再深化 AI 原生玩法闭环
|
||||
|
||||
1. 运行时物品导演层
|
||||
2. 任务意图与 contract 系统
|
||||
|
||||
### 第四阶段:最后做工具与体验收尾
|
||||
|
||||
1. 编辑器共享层 / 本地 API 分层
|
||||
2. 移动端体验与包体优化
|
||||
|
||||
---
|
||||
|
||||
## 本清单的主要依据
|
||||
|
||||
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
|
||||
- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
|
||||
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
|
||||
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md`
|
||||
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md`
|
||||
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md`
|
||||
- `docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md`
|
||||
- `docs/prd/AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md`
|
||||
- `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md`
|
||||
- `docs/prd/AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md`
|
||||
|
||||
## 最后结论
|
||||
|
||||
如果只保留一句话,那就是:
|
||||
|
||||
**当前最优先的迭代方向,不是继续堆新内容,而是先把工程基线、主流程边界和统一规则底座补齐;只有这样,AI 原生任务、物品、Build 和后续内容扩展才会真正开始越做越顺。**
|
||||
@@ -139,10 +139,10 @@ Git 分支治理可以后置做,但不能和首轮工程清洗混在一起,
|
||||
|
||||
本计划基于现有文档已经确认的结论推进,重点参考:
|
||||
|
||||
1. `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md`
|
||||
1. `docs/audits/engineering/README.md`
|
||||
2. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
|
||||
3. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
|
||||
4. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md`
|
||||
4. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||
5. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
|
||||
|
||||
按当前审计结果,首轮就应重点关注下面 3 组对象。
|
||||
|
||||
@@ -1,588 +0,0 @@
|
||||
# Express 后端化并行任务拆分规划(2026-04-08)
|
||||
|
||||
## 1. 目的
|
||||
|
||||
这份文档用于把 [EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md](./EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md) 进一步拆成可并行推进、尽量互不冲突的任务流。
|
||||
|
||||
目标不是把大重构拆成很多零碎 TODO,而是把它拆成:
|
||||
|
||||
- 可以同时开工
|
||||
- 写入边界清晰
|
||||
- 交付物明确
|
||||
- 依赖关系稳定
|
||||
- 最后容易集成
|
||||
|
||||
---
|
||||
|
||||
## 2. 并行拆分原则
|
||||
|
||||
## 2.1 基本原则
|
||||
|
||||
- 每条任务尽量拥有独占目录或独占模块,不去抢同一批热点文件。
|
||||
- 热点集成文件只由“集成岗”或最后一轮集成处理,不作为多个任务的日常编辑目标。
|
||||
- 先搭协议边界,再迁规则执行,再收缩前端。
|
||||
- 前端与后端可以并行推进,但前提是先冻结 contract。
|
||||
- 编辑器链路和正式运行时链路分开拆,避免互相阻塞。
|
||||
|
||||
## 2.2 当前最容易冲突的文件
|
||||
|
||||
以下文件建议默认只由集成岗或最后一轮联调处理:
|
||||
|
||||
- `server-node/src/context.ts`
|
||||
- `server-node/src/routes/runtimeRoutes.ts`
|
||||
- `server-node/src/app.ts`
|
||||
- `src/services/apiClient.ts`
|
||||
- `src/hooks/useStoryGeneration.ts`
|
||||
- `src/hooks/useGameFlow.ts`
|
||||
- `src/components/GameShell.tsx`
|
||||
|
||||
其他任务如果必须影响这些文件,优先通过:
|
||||
|
||||
- 新增独立模块
|
||||
- 新增 adapter
|
||||
- 新增中间层入口
|
||||
|
||||
而不是直接在热点文件中大改。
|
||||
|
||||
---
|
||||
|
||||
## 3. 建议并行批次
|
||||
|
||||
## 批次 A:可立即并行开工
|
||||
|
||||
- 任务 0:集成岗与接口冻结
|
||||
- 任务 1:共享 contract 与目录抽离
|
||||
- 任务 2:PostgreSQL 持久化基线收口
|
||||
- 任务 3:服务端 HTTP 基础设施与统一响应壳层
|
||||
- 任务 8:编辑器 API 归口与工具链隔离
|
||||
- 任务 9:测试、观测与部署基线
|
||||
|
||||
## 批次 B:在 contract 初版落地后并行开工
|
||||
|
||||
- 任务 4:服务端 AI 编排收口
|
||||
- 任务 5:运行时领域模块 A,Story / Combat / NPC
|
||||
- 任务 6:运行时领域模块 B,Inventory / Quest / Build / Runtime Item
|
||||
- 任务 7:前端 SDK、鉴权、持久化瘦身
|
||||
|
||||
## 批次 C:在服务端 action 和 view model 稳定后开工
|
||||
|
||||
- 任务 10:前端主流程壳层与大 hook 瘦身
|
||||
|
||||
---
|
||||
|
||||
## 4. 任务拆分
|
||||
|
||||
## 任务 0:集成岗与接口冻结
|
||||
|
||||
### 目标
|
||||
|
||||
负责冻结边界、维护接口文档、控制热点文件的合并节奏,避免多人同时改核心入口。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `docs/planning/**`
|
||||
- `docs/technical/**`
|
||||
- 最终集成时的热点入口文件
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 统一任务看板
|
||||
- contract 版本表
|
||||
- 热点文件编辑规则
|
||||
- 每日或每阶段集成清单
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 团队知道哪些文件不能多人同时改
|
||||
- 每条任务都有明确的上游 contract 与下游接入点
|
||||
|
||||
---
|
||||
|
||||
## 任务 1:共享 Contract 与目录抽离
|
||||
|
||||
### 目标
|
||||
|
||||
先把前后端共同识别的类型、schema、响应结构、错误结构抽出来,切断 `server-node -> src/**` 的长期反向依赖。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `packages/shared/**`
|
||||
- 新建的共享类型、schema、contract 目录
|
||||
|
||||
### 可改边界
|
||||
|
||||
- `server-node/src/**` 中的 import 替换入口
|
||||
- `src/**` 中的 import 替换入口
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 具体业务规则迁移
|
||||
- 前端页面行为调整
|
||||
- 数据库实现细节
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 统一 API envelope
|
||||
- 统一错误对象
|
||||
- 统一 action / response contract
|
||||
- 统一领域类型和状态枚举
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 新增服务端模块不需要继续直接依赖前端目录里的实现细节
|
||||
- 前后端都以共享 contract 为边界协作
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 可与任务 2、任务 3、任务 8、任务 9 同时启动
|
||||
- 是任务 4、任务 5、任务 6、任务 7 的上游基础
|
||||
|
||||
---
|
||||
|
||||
## 任务 2:PostgreSQL 持久化基线收口
|
||||
|
||||
### 目标
|
||||
|
||||
把“已经切到 PostgreSQL”的状态收成真正稳定的后端基线,清掉 SQLite 残留口径与仓储层耦合问题。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `server-node/src/config.ts`
|
||||
- `server-node/src/db.ts`
|
||||
- `server-node/src/repositories/**`
|
||||
- `server-node/src/app.test.ts`
|
||||
- `.env.example`
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 剧情规则
|
||||
- 选项结算
|
||||
- 前端状态瘦身
|
||||
|
||||
### 主要输出
|
||||
|
||||
- PostgreSQL 连接配置
|
||||
- 仓储层接口统一
|
||||
- 数据表初始化/迁移方案
|
||||
- 运行时持久化测试基线
|
||||
- 文档中的数据库现状统一
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 后端运行时数据完全以后端数据库为准
|
||||
- 配置、日志、测试、文档里不再把 SQLite 写成当前正式现状
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 可与任务 1、任务 3、任务 8、任务 9 同时启动
|
||||
- 为任务 5、任务 6、任务 7 提供稳定持久化基础
|
||||
|
||||
---
|
||||
|
||||
## 任务 3:服务端 HTTP 基础设施与统一响应壳层
|
||||
|
||||
### 目标
|
||||
|
||||
建立统一的服务端响应结构、错误结构、请求链路日志、版本字段和中间件壳层。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `server-node/src/http.ts`
|
||||
- `server-node/src/errors.ts`
|
||||
- `server-node/src/middleware/**`
|
||||
- `server-node/src/app.ts`
|
||||
|
||||
### 可改边界
|
||||
|
||||
- 为 route 层提供新的响应 helper
|
||||
- 为后续 action 接口提供统一 envelope
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 具体 story / combat / quest 业务逻辑
|
||||
- 前端页面层接入
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 统一 JSON 响应格式
|
||||
- 统一错误格式
|
||||
- `requestId`
|
||||
- `latency` 与关键日志字段
|
||||
- 路由级版本与元信息壳层
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 后端所有新接口都能套用同一层响应约定
|
||||
- 前端不需要为不同接口写多套错误解析逻辑
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 可与任务 1、任务 2、任务 8、任务 9 同时启动
|
||||
- 是任务 4、任务 5、任务 6、任务 7 的共同基础
|
||||
|
||||
---
|
||||
|
||||
## 任务 4:服务端 AI 编排收口
|
||||
|
||||
### 目标
|
||||
|
||||
把正式运行时的 prompt 组装、模型调用、容错、SSE 转发都收回后端,浏览器不再保留正式运行时 AI fallback。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `server-node/src/services/llmClient.ts`
|
||||
- `server-node/src/services/chatService.ts`
|
||||
- `server-node/src/services/storyService.ts`
|
||||
- `server-node/src/services/customWorldGenerationService.ts`
|
||||
- `server-node/src/services/questService.ts`
|
||||
- `server-node/src/services/runtimeItemService.ts`
|
||||
|
||||
### 可新增目录
|
||||
|
||||
- `server-node/src/modules/ai/**`
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 前端主流程组件
|
||||
- 数据库存储实现
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 后端统一 AI orchestration 层
|
||||
- 流式接口统一适配
|
||||
- prompt 复用策略
|
||||
- 前端 fallback 清理清单
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 正式运行时不再依赖浏览器端大体量 AI 实现作为兜底
|
||||
- AI 失败、超时、流式中断都能在后端统一处理
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 建议在任务 1、任务 3 有初版后启动
|
||||
- 可与任务 5、任务 6、任务 7 并行
|
||||
|
||||
---
|
||||
|
||||
## 任务 5:运行时领域模块 A,Story / Combat / NPC
|
||||
|
||||
### 目标
|
||||
|
||||
把剧情推进、战斗结算、NPC 交互这些最核心的运行时状态迁移到后端领域模块。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `server-node/src/modules/story/**`
|
||||
- `server-node/src/modules/combat/**`
|
||||
- `server-node/src/modules/npc/**`
|
||||
|
||||
### 可改边界
|
||||
|
||||
- 为 route/action 层提供服务接口
|
||||
- 为前端提供 view model 所需聚合结果
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 背包、Build、任务奖励编排
|
||||
- 编辑器接口
|
||||
|
||||
### 主要输出
|
||||
|
||||
- story action resolver
|
||||
- combat resolution service
|
||||
- npc interaction service
|
||||
- 统一返回给 UI 的 presentation/view model 结构
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 前端不再本地决定 function 合法性、战斗结果、NPC 关键关系变化
|
||||
- 点击选项时,后端能返回完整下一步展示结果
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 依赖任务 1、任务 3
|
||||
- 可与任务 4、任务 6、任务 7 并行
|
||||
|
||||
---
|
||||
|
||||
## 任务 6:运行时领域模块 B,Inventory / Quest / Build / Runtime Item
|
||||
|
||||
### 目标
|
||||
|
||||
把任务推进、运行时物品、背包/装备、Build 收益等剩余核心规则迁到后端。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `server-node/src/modules/inventory/**`
|
||||
- `server-node/src/modules/quest/**`
|
||||
- `server-node/src/modules/build/**`
|
||||
- `server-node/src/modules/runtime-item/**`
|
||||
|
||||
### 可改边界
|
||||
|
||||
- 调用任务 2 的仓储层
|
||||
- 使用任务 1 的共享 contract
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 前端页面层改造
|
||||
- Story / Combat / NPC 主链路
|
||||
|
||||
### 主要输出
|
||||
|
||||
- inventory mutation service
|
||||
- quest signal progression service
|
||||
- build calculation service
|
||||
- runtime item resolution service
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 背包、任务、Build、运行时物品不再由前端保留正式结算逻辑
|
||||
- 这些领域能独立测试,不依赖 UI hook
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 依赖任务 1、任务 2、任务 3
|
||||
- 可与任务 4、任务 5、任务 7 并行
|
||||
|
||||
---
|
||||
|
||||
## 任务 7:前端 SDK、鉴权、持久化瘦身
|
||||
|
||||
### 目标
|
||||
|
||||
让前端从“业务执行层”退回“API 消费层 + 表现层状态协调层”。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `src/services/apiClient.ts`
|
||||
- `src/services/authService.ts`
|
||||
- `src/services/storageService.ts`
|
||||
- `src/services/aiService.ts`
|
||||
- `src/hooks/useGamePersistence.ts`
|
||||
- `src/hooks/useGameSettings.ts`
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 页面组件大范围重构
|
||||
- `useStoryGeneration.ts` 主流程瘦身
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 轻量前端 SDK
|
||||
- 统一鉴权请求层
|
||||
- 统一错误态与重试策略
|
||||
- 远端快照/设置消费层
|
||||
- 正式运行时浏览器 fallback 下线方案
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 前端服务层不再保留完整正式规则或正式 AI 编排
|
||||
- 存档与设置以后端返回结果为准
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 依赖任务 1、任务 3
|
||||
- 可与任务 4、任务 5、任务 6 并行
|
||||
- 为任务 10 提供稳定的 API 消费层
|
||||
|
||||
---
|
||||
|
||||
## 任务 8:编辑器 API 归口与工具链隔离
|
||||
|
||||
### 目标
|
||||
|
||||
把编辑器的写盘、生成、任务查询能力从“散落接口”整理成清晰的编辑器后端模块,避免继续污染正式运行时。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `src/editor/shared/**`
|
||||
- `src/components/preset-editor/**`
|
||||
- `src/components/npcVisualEditorPersistence.ts`
|
||||
- `src/components/preset-editor/characterAssetStudioPersistence.ts`
|
||||
- `scripts/dev-server/**`
|
||||
- `server-node/src/modules/editor/**`
|
||||
- `server-node/src/modules/assets/**`
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 主游戏运行时 action 逻辑
|
||||
- 正式剧情流转
|
||||
|
||||
### 主要输出
|
||||
|
||||
- `/api/editor/*` 与 `/api/assets/*` 命名空间
|
||||
- 统一 editor client SDK
|
||||
- 写接口权限边界
|
||||
- 编辑器工具链迁移清单
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 编辑器组件不再散落直连多个写接口
|
||||
- 编辑器 API 与运行时 API 的职责边界清晰
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 可与任务 1、任务 2、任务 3、任务 9 同时启动
|
||||
- 与任务 5、任务 6、任务 10 基本不冲突
|
||||
|
||||
---
|
||||
|
||||
## 任务 9:测试、观测与部署基线
|
||||
|
||||
### 目标
|
||||
|
||||
为整个后端化改造提供自动回归、链路日志和部署基线,避免“功能迁过去了但不可验证”。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `server-node/src/**/*.test.ts`
|
||||
- `scripts/**`
|
||||
- 部署与运维相关文档
|
||||
- 反向代理与 smoke 测试脚本
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 具体业务模块实现
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 后端接口测试
|
||||
- 关键主链路 smoke
|
||||
- request/response 日志校验
|
||||
- 同域部署基线
|
||||
- 回滚、备份、迁移检查清单
|
||||
|
||||
### 验收标准
|
||||
|
||||
- `web + server` 改造过程有最小自动回归保护
|
||||
- 关键接口失败时能追踪到请求链路
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 可与任务 1、任务 2、任务 3、任务 8 同时启动
|
||||
- 后续持续跟进任务 4、任务 5、任务 6、任务 7、任务 10 的交付
|
||||
|
||||
---
|
||||
|
||||
## 任务 10:前端主流程壳层与大 Hook 瘦身
|
||||
|
||||
### 目标
|
||||
|
||||
在服务端 action 和前端 SDK 稳定后,把 `GameShell`、`useStoryGeneration` 这一层改成真正的表现层协调器。
|
||||
|
||||
### 独占范围
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts`
|
||||
- `src/hooks/story/**`
|
||||
- `src/hooks/useGameFlow.ts`
|
||||
- `src/components/GameShell.tsx`
|
||||
- `src/components/AdventurePanel.tsx`
|
||||
- `src/components/NpcModals.tsx`
|
||||
- `src/components/auth/**`
|
||||
|
||||
### 暂不负责
|
||||
|
||||
- 数据库、服务端仓储
|
||||
- 编辑器 API
|
||||
|
||||
### 主要输出
|
||||
|
||||
- 面向 action/view model 的前端流程层
|
||||
- 页面表现态与业务态分离
|
||||
- 大 hook 拆分后的协调层
|
||||
- 更容易测试和替换的主流程壳层
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 前端主流程不再直接吞下完整运行时规则
|
||||
- 页面层主要消费后端 view model,而不是本地自算结果
|
||||
|
||||
### 并行关系
|
||||
|
||||
- 依赖任务 5、任务 6、任务 7 至少有一轮稳定输出
|
||||
- 是最后一批大规模前端接入任务
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐协作顺序
|
||||
|
||||
## 第一步:先定边界
|
||||
|
||||
先启动:
|
||||
|
||||
- 任务 0
|
||||
- 任务 1
|
||||
- 任务 2
|
||||
- 任务 3
|
||||
|
||||
这一轮完成后,团队会得到:
|
||||
|
||||
- 统一 contract
|
||||
- 稳定数据库基线
|
||||
- 稳定后端响应壳层
|
||||
- 稳定任务分工边界
|
||||
|
||||
## 第二步:领域层和工具层分头推进
|
||||
|
||||
在第一步基础上并行启动:
|
||||
|
||||
- 任务 4
|
||||
- 任务 5
|
||||
- 任务 6
|
||||
- 任务 7
|
||||
- 任务 8
|
||||
- 任务 9
|
||||
|
||||
这一轮是整个改造的主生产阶段。
|
||||
|
||||
## 第三步:最后收前端主流程
|
||||
|
||||
最后启动:
|
||||
|
||||
- 任务 10
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 如果太早改 `useStoryGeneration` 和 `GameShell`,前端还没有稳定的 action contract 和 view model,会反复返工。
|
||||
|
||||
---
|
||||
|
||||
## 6. 建议的多人分工方式
|
||||
|
||||
如果是 4 人并行,建议:
|
||||
|
||||
- 1 人负责任务 1 + 任务 0 的 contract/集成
|
||||
- 1 人负责任务 2 + 任务 3 的后端基建
|
||||
- 1 人负责任务 4 + 任务 5 的运行时主链
|
||||
- 1 人负责任务 8 + 任务 9,之后转入任务 7 或任务 10
|
||||
|
||||
如果是 6 人并行,建议:
|
||||
|
||||
- 1 人负责任务 0 + 任务 1
|
||||
- 1 人负责任务 2
|
||||
- 1 人负责任务 3 + 任务 9
|
||||
- 1 人负责任务 4
|
||||
- 1 人负责任务 5
|
||||
- 1 人负责任务 6 + 任务 8
|
||||
|
||||
前端主流程任务 10 建议在第二轮由最熟悉当前 UI 壳层的人接手。
|
||||
|
||||
---
|
||||
|
||||
## 7. 合并规则建议
|
||||
|
||||
- 每条任务优先新增目录和新模块,少直接改热点文件。
|
||||
- 热点文件统一在集成窗口合并,不在多个任务里同步推进。
|
||||
- 任何任务如果需要改 `useStoryGeneration.ts`,默认先暂停并和任务 10 对齐。
|
||||
- 任何任务如果需要改 `server-node/src/routes/runtimeRoutes.ts`,默认先走任务 0 的接口冻结表。
|
||||
- 编辑器链路和正式运行时链路不要混在同一个 PR 里。
|
||||
|
||||
---
|
||||
|
||||
## 8. 一句话结论
|
||||
|
||||
这次重构最稳的并行方式不是“大家一起改前后端”,而是:
|
||||
|
||||
**先用 contract、数据库基线和 HTTP 壳层把边界钉死,再让服务端领域迁移、编辑器归口、前端瘦身分轨并行,最后由主流程壳层统一接入。**
|
||||
@@ -1,447 +0,0 @@
|
||||
# Express 后端化工程重构规划(2026-04-08)
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前项目已经引入 `Express` 后端,且 `server-node/` 已经承接了运行时鉴权、存档、设置、自定义世界、剧情生成、角色聊天、NPC 对话、运行时物品意图、任务生成等能力。
|
||||
|
||||
但从当前工程状态看,项目仍处于“后端已存在,但运行时领域层尚未完全脱前端”的过渡态,主要表现为:
|
||||
|
||||
- 前端 `src/hooks/useStoryGeneration.ts` 仍然承担了大量运行时编排、规则拼接与状态推进职责。
|
||||
- 前端 `src/services/ai.ts` 仍然保留了完整的 AI 调用、提示词拼装和本地兜底实现。
|
||||
- 前端 `src/hooks/useGamePersistence.ts` 仍在承担较重的存档恢复、schema 纠偏与归一化职责。
|
||||
- `server-node/src/**` 当前仍在直接引用 `src/types`、`src/data`、`src/services` 中的内容,分层尚未真正闭合。
|
||||
- 编辑器相关写接口仍然散落在前端组件与 `jsonClient` 中,运行时 API 与编辑器 API 还没有完全归口。
|
||||
|
||||
现在既然已经明确“前端只负责做表现,所有逻辑、数据都放到后端进行运算和存储”,就需要把这个原则升级成整个工程的硬边界,而不是只停留在一部分接口迁移完成的状态。
|
||||
|
||||
---
|
||||
|
||||
## 2. 重构总原则
|
||||
|
||||
本轮重构只坚持一个核心原则:
|
||||
|
||||
**前端不是业务执行层,而是表现层;后端才是唯一的运行时真相来源。**
|
||||
|
||||
进一步展开为:
|
||||
|
||||
- 前端只负责页面结构、动画演出、输入采集、局部交互态、加载态和错误态展示。
|
||||
- 后端负责鉴权、会话、规则计算、剧情推进、AI 编排、任务推进、道具结算、Build 结算、存档读写与持久化。
|
||||
- 浏览器内不再保留“正式运行时业务规则”的第二套实现。
|
||||
- 浏览器内允许存在少量纯表现计算,但不允许成为游戏状态真相来源。
|
||||
- 编辑器能力与正式运行时能力分离,避免 dev 工具链继续污染正式运行时边界。
|
||||
|
||||
---
|
||||
|
||||
## 3. 重构目标
|
||||
|
||||
## 3.1 目标状态
|
||||
|
||||
- 浏览器只发送“玩家意图”和必要的展示参数,不直接提交完整运行时真相。
|
||||
- `Express` 后端成为唯一的运行时状态源、规则执行源和 AI 调度源。
|
||||
- 运行时快照、任务状态、NPC 状态、背包、属性、Build、剧情历史全部以后端持久化结果为准。
|
||||
- 前端不再直接 import 正式运行时 AI 逻辑、提示词逻辑和关键规则逻辑。
|
||||
- `server-node` 不再依赖 `src/**` 中的前端实现细节,而是依赖独立共享层。
|
||||
- 编辑器 API、运行时 API、资产生成 API 形成清晰命名空间和权限边界。
|
||||
|
||||
## 3.2 非目标
|
||||
|
||||
- 本轮不追求一次性重写所有玩法系统。
|
||||
- 本轮不再讨论关系型数据库选型切换,当前后端以 `PostgreSQL` 为准。
|
||||
- 本轮不改动已有中文剧情、设定和文案方向。
|
||||
- 本轮不为了“前后端分离”牺牲移动端体验与当前主流程可玩性。
|
||||
|
||||
---
|
||||
|
||||
## 4. 职责边界
|
||||
|
||||
| 领域 | 前端职责 | 后端职责 |
|
||||
| --- | --- | --- |
|
||||
| 页面与流程壳层 | 页面切换、面板开关、布局、自适应、动效、加载态 | 不负责页面 UI |
|
||||
| 用户输入 | 收集点击、拖拽、表单输入、选项选择 | 校验输入是否合法,解释输入对应的运行时动作 |
|
||||
| 游戏状态 | 仅持有当前展示所需 view model 和局部 UI state | 持有完整游戏状态、快照、事件日志、版本号 |
|
||||
| 剧情推进 | 展示文本流、选项、动画时间线 | 生成剧情、决定选项集合、推进故事状态 |
|
||||
| 战斗与数值 | 播放攻击、受击、死亡、位移 | 计算伤害、蓝耗、CD、死亡、掉落、逃跑结果 |
|
||||
| NPC/同伴交互 | 展示面板、聊天输入框、关系反馈演出 | 计算关系变化、招募条件、交易合法性、对话结果 |
|
||||
| 背包/装备/Build | 展示背包、装备栏、Build 面板 | 计算背包变化、装备结果、Build 收益与约束 |
|
||||
| 任务系统 | 展示任务卡片、任务进度、奖励动画 | 生成任务、推进 signal、发放奖励 |
|
||||
| AI 调用 | 不直接请求正式运行时模型 | 统一做 prompt 组装、模型调用、超时重试、日志 |
|
||||
| 持久化 | 最多保留极少量表现态缓存 | 负责存档、设置、用户数据、迁移、恢复 |
|
||||
| 编辑器 | 调用 SDK、展示工具面板 | 负责写盘、生成任务、队列、权限与审计 |
|
||||
|
||||
## 4.1 前端允许保留的状态
|
||||
|
||||
- 当前面板是否打开
|
||||
- 当前动画是否播放中
|
||||
- 当前流式文本已经显示到哪一段
|
||||
- 表单草稿、搜索词、临时筛选条件
|
||||
- 与展示相关的 viewport / media / motion 状态
|
||||
|
||||
## 4.2 前端禁止继续承载的职责
|
||||
|
||||
- function 合法性判定
|
||||
- 怪物/NPC/任务/物品结算
|
||||
- 正式运行时 prompt 组装
|
||||
- 正式运行时 AI fallback
|
||||
- 存档 schema 迁移主逻辑
|
||||
- 以 `localStorage` 作为正式运行时主存储
|
||||
- 编辑器组件直接散落 `fetch('/api/...')` 访问写接口
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前工程问题归纳
|
||||
|
||||
## 5.1 运行时领域逻辑仍然偏前端中心
|
||||
|
||||
- `useStoryGeneration` 仍然是大体量编排热区,承接了剧情、NPC、战斗后续、任务和部分故事引擎逻辑。
|
||||
- `src/services/ai.ts` 体量很大,说明正式运行时 AI 编排尚未完全移出浏览器。
|
||||
- 当前“后端接口 + 前端兜底”的过渡模式,容易让正式逻辑继续双份存在。
|
||||
|
||||
## 5.2 服务端分层还没真正闭合
|
||||
|
||||
- `server-node` 当前仍直接引用 `src/types`、`src/data`、`src/services`。
|
||||
- 这意味着后端虽然有了入口,但核心领域模型仍然绑在前端目录结构上。
|
||||
- 继续沿着这条路开发,会让后端无法独立测试、独立构建和独立演进。
|
||||
|
||||
## 5.3 运行时持久化边界还不够干净
|
||||
|
||||
- 虽然正式存档已经走远端接口,但前端仍承担较重的恢复、归一化、迁移纠偏逻辑。
|
||||
- 这会导致“存档解释权”同时存在于前后端两边,后续迭代容易失配。
|
||||
|
||||
## 5.4 编辑器与运行时 API 仍然混杂
|
||||
|
||||
- 编辑器读写接口目前仍然有散落访问点。
|
||||
- 资产生成、JSON 写盘、运行时 API 还没有形成清晰的接口分域。
|
||||
- 继续混用会让权限控制、生产部署和后续多人协作变得困难。
|
||||
|
||||
## 5.5 当前协议更像“接口迁移”,还不是“后端驱动运行时”
|
||||
|
||||
- 目前很多接口是把已有前端逻辑搬成了远端调用入口。
|
||||
- 但真正理想状态应该是:玩家点击后,后端完成规则结算、状态推进、AI 调用和持久化,再把展示模型返回给前端。
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标架构
|
||||
|
||||
```text
|
||||
Browser
|
||||
├─ 页面 / 动画 / 交互 / ViewModel 渲染
|
||||
├─ 轻量前端 SDK(只负责请求与状态绑定)
|
||||
└─ 局部 UI State
|
||||
|
||||
packages/shared
|
||||
├─ contracts
|
||||
├─ schemas
|
||||
├─ domain-types
|
||||
└─ api-client-types
|
||||
|
||||
server-node
|
||||
├─ src/modules/auth
|
||||
├─ src/modules/runtime-session
|
||||
├─ src/modules/story
|
||||
├─ src/modules/combat
|
||||
├─ src/modules/npc
|
||||
├─ src/modules/inventory
|
||||
├─ src/modules/build
|
||||
├─ src/modules/quest
|
||||
├─ src/modules/custom-world
|
||||
├─ src/modules/editor
|
||||
├─ src/shared/http
|
||||
├─ src/shared/infra
|
||||
└─ src/shared/llm
|
||||
|
||||
storage
|
||||
├─ postgres
|
||||
├─ uploads
|
||||
└─ generated
|
||||
```
|
||||
|
||||
## 6.1 共享层原则
|
||||
|
||||
- `packages/shared` 只放类型、schema、协议、纯函数和序列化约定。
|
||||
- 共享层不放浏览器专属实现,也不放 Node 专属 IO。
|
||||
- 所有可执行运行时规则默认放后端,不放共享层。
|
||||
|
||||
## 6.2 前端目录目标
|
||||
|
||||
前端建议逐步收敛成下面的职责结构:
|
||||
|
||||
```text
|
||||
src/
|
||||
├─ app
|
||||
├─ pages
|
||||
├─ widgets
|
||||
├─ features
|
||||
├─ entities
|
||||
├─ shared/api
|
||||
├─ shared/ui
|
||||
└─ shared/lib
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `shared/api` 只保留面向后端 contract 的 SDK。
|
||||
- `features` 只组织交互流程和 UI 组合,不再承载正式运行时规则。
|
||||
- 超大 hook 逐步拆成“页面状态协调层 + 远端 action 调用层 + 表现层状态”。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键协议重构方向
|
||||
|
||||
当前最值得尽快统一的,不是继续加接口数量,而是把协议升级成“意图驱动”。
|
||||
|
||||
推荐核心动作协议:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "runtime-session-id",
|
||||
"clientVersion": 12,
|
||||
"action": {
|
||||
"type": "story_choice",
|
||||
"functionId": "fight_attack",
|
||||
"targetId": "npc_merchant_01",
|
||||
"payload": {
|
||||
"optionId": "opt_02"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后端统一返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "runtime-session-id",
|
||||
"serverVersion": 13,
|
||||
"viewModel": {},
|
||||
"presentation": {
|
||||
"storyText": "",
|
||||
"options": [],
|
||||
"battlePlayback": null,
|
||||
"toast": null
|
||||
},
|
||||
"patches": [],
|
||||
"meta": {
|
||||
"requestId": "req_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
协议约束:
|
||||
|
||||
- 前端不再提交完整 `gameState` 作为后端运算依据。
|
||||
- 前端提交的是“玩家意图”,不是“玩家已经算好的结果”。
|
||||
- 后端返回的是“下一帧该怎么演”的展示模型,而不是只回一个零散字段。
|
||||
|
||||
---
|
||||
|
||||
## 8. 分阶段重构路线
|
||||
|
||||
## P0:先冻结边界,建立共享协议层
|
||||
|
||||
### 本阶段目标
|
||||
|
||||
把“前端只做表现,后端负责运行时真相”从口头原则变成工程边界。
|
||||
|
||||
### 主要任务
|
||||
|
||||
- 提取 `shared contracts`,把 `server-node` 对 `src/**` 的依赖逐步迁出。
|
||||
- 固化统一的 API 响应结构、错误结构、`requestId`、版本字段。
|
||||
- 明确运行时 API 命名空间与编辑器 API 命名空间。
|
||||
- 新功能一律禁止再把正式运行时规则写回前端。
|
||||
- 为关键运行时入口补健康检查、日志字段、耗时统计。
|
||||
|
||||
### 交付物
|
||||
|
||||
- 共享类型与 schema 目录
|
||||
- 统一 API 约定文档
|
||||
- 服务端模块边界草图
|
||||
- 前端 SDK 基础层
|
||||
|
||||
### 验收标准
|
||||
|
||||
- `server-node` 可以不依赖 `src/**` 中的前端运行时实现继续编译。
|
||||
- 新增运行时需求不再允许“前端先写一版、后端再补一版”。
|
||||
|
||||
## P1:把运行时状态与持久化解释权收回后端
|
||||
|
||||
### 本阶段目标
|
||||
|
||||
让后端成为运行时状态、快照、恢复和迁移的唯一解释者。
|
||||
|
||||
### 主要任务
|
||||
|
||||
- 建立 `runtime session` / `snapshot aggregate`。
|
||||
- 将存档恢复、版本迁移、默认值补齐、schema 纠偏迁到后端。
|
||||
- 把前端 `useGamePersistence` 收敛为“拉取快照 + 触发保存 + 接收 view model”。
|
||||
- 设置、快照、自定义世界库统一归入运行时仓储接口。
|
||||
- 明确哪些内容允许本地缓存,哪些必须以后端结果为准。
|
||||
|
||||
### 交付物
|
||||
|
||||
- 统一的运行时 session API
|
||||
- 快照版本迁移服务
|
||||
- 服务端持久化 schema 文档
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 前端不再承担正式存档恢复迁移的主逻辑。
|
||||
- 同一份存档的解释权只存在于后端。
|
||||
|
||||
## P2:把核心规则结算从前端迁到后端
|
||||
|
||||
### 本阶段目标
|
||||
|
||||
把“剧情推进、战斗、NPC、任务、物品、Build”这些真正影响状态的领域结算全部后端化。
|
||||
|
||||
### 主要任务
|
||||
|
||||
- 把 function 合法性过滤迁入后端。
|
||||
- 把战斗结算、蓝耗、伤害、死亡、掉落、逃跑结果迁入后端。
|
||||
- 把 NPC 交互决策、招募条件、关系变化、交易合法性迁入后端。
|
||||
- 把任务推进 signal、奖励结算、运行时物品结果、Build 结果迁入后端。
|
||||
- 前端收到的只是一份下一步展示所需的聚合 view model 与演出计划。
|
||||
|
||||
### 交付物
|
||||
|
||||
- 运行时 action resolver
|
||||
- 统一领域服务接口
|
||||
- 面向 UI 的 view model assembler
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 前端点击一个选项时,发送的是 action,不是本地先算完再上传结果。
|
||||
- 正式运行时的数值、资源、状态迁移不再依赖浏览器逻辑。
|
||||
|
||||
## P3:把 AI 编排彻底收口到后端
|
||||
|
||||
### 本阶段目标
|
||||
|
||||
让浏览器彻底退出正式运行时 AI 调用与 prompt 组装。
|
||||
|
||||
### 主要任务
|
||||
|
||||
- 把剧情生成、角色聊天、NPC 对话、自定义世界生成、任务生成、物品意图生成等统一后端执行。
|
||||
- 清理前端正式运行时代码中的 AI fallback。
|
||||
- 将 prompt 构造、模型容错、超时、重试、日志、SSE 转发统一收口到后端。
|
||||
- 对需要复用的 prompt 纯函数进行共享层抽取,但执行权只留在后端。
|
||||
|
||||
### 交付物
|
||||
|
||||
- 后端 AI orchestration 模块
|
||||
- 统一 SSE/streaming 适配层
|
||||
- 精简后的前端 AI SDK
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 浏览器正式运行时代码不再直接 import 大体量 AI 编排模块。
|
||||
- 无后端时,正式运行时不再默默回退到另一套浏览器逻辑。
|
||||
|
||||
## P4:把编辑器与资产流程独立成正式后端模块
|
||||
|
||||
### 本阶段目标
|
||||
|
||||
让编辑器能力不再作为运行时副产物存在,而是成为有边界的工具后端模块。
|
||||
|
||||
### 主要任务
|
||||
|
||||
- 建立 `/api/editor/*`、`/api/assets/*` 等明确命名空间。
|
||||
- 给编辑器写接口补权限、环境门禁、审计日志。
|
||||
- 统一编辑器 JSON 读写、资产生成、任务查询接口。
|
||||
- 前端编辑器组件全部改走统一 SDK,不再散落直连接口。
|
||||
|
||||
### 交付物
|
||||
|
||||
- editor API contract
|
||||
- 统一 editor client SDK
|
||||
- 生成任务与写盘适配器
|
||||
|
||||
### 验收标准
|
||||
|
||||
- 编辑器写接口不再散落在多个组件内部。
|
||||
- 运行时 API 与编辑器 API 的职责边界清晰。
|
||||
|
||||
## P5:补齐质量门禁、部署路径和观测能力
|
||||
|
||||
### 本阶段目标
|
||||
|
||||
让这次后端化重构可以稳定上线,而不是只在本地联调成立。
|
||||
|
||||
### 主要任务
|
||||
|
||||
- 为后端补单测、接口测试和关键链路 smoke。
|
||||
- 为前端补 contract 测试,确保 UI 不依赖本地规则。
|
||||
- 建立 `Nginx/Caddy -> dist + /api` 的同域部署路径。
|
||||
- 为流式接口补代理配置、超时、取消和日志。
|
||||
- 为数据库迁移、备份、回滚预留脚本。
|
||||
|
||||
### 验收标准
|
||||
|
||||
- `web + server` 可以独立构建、独立测试、联合部署。
|
||||
- 关键主流程至少具备一条可自动验证的 smoke path。
|
||||
|
||||
---
|
||||
|
||||
## 9. 具体迁移清单
|
||||
|
||||
## 9.1 优先迁移对象
|
||||
|
||||
- `src/hooks/useStoryGeneration.ts`
|
||||
- `src/hooks/useGamePersistence.ts`
|
||||
- `src/services/ai.ts`
|
||||
- `src/services/aiService.ts`
|
||||
- `src/services/storageService.ts`
|
||||
- `src/services/authService.ts`
|
||||
- 编辑器持久化模块与 `src/editor/shared/jsonClient.ts`
|
||||
|
||||
## 9.2 优先抽离到共享层的内容
|
||||
|
||||
- 领域类型定义
|
||||
- zod schema 或等价校验协议
|
||||
- API 请求与响应 contract
|
||||
- 纯序列化函数
|
||||
- 前后端都要认识的 enum / id / status 常量
|
||||
|
||||
## 9.3 不建议抽到共享层的内容
|
||||
|
||||
- 依赖数据库、文件系统、LLM、日志的服务
|
||||
- 正式运行时规则执行器
|
||||
- 存档迁移执行器
|
||||
- 资产生成任务调度器
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施顺序建议
|
||||
|
||||
推荐顺序如下:
|
||||
|
||||
1. 先抽共享类型与协议,切断 `server-node -> src/**` 的反向依赖。
|
||||
2. 再把运行时 session、快照解释权、存档迁移收回后端。
|
||||
3. 再迁核心规则结算,让前端从“业务执行层”退回“表现协调层”。
|
||||
4. 然后彻底收口 AI 编排,移除正式运行时浏览器 fallback。
|
||||
5. 最后归整编辑器 API、部署路径、测试门禁和观测能力。
|
||||
|
||||
不建议的顺序:
|
||||
|
||||
1. 先零散把几个接口改成后端。
|
||||
2. 继续保留前端完整 fallback。
|
||||
3. 最后再补共享层和协议。
|
||||
|
||||
这个顺序会把“双份逻辑并存”的过渡期拖得很长,后面会越来越难收口。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与控制点
|
||||
|
||||
- 最大风险不是“迁不动”,而是长期维持双份规则。
|
||||
- 后端化期间必须避免再往前端加新的正式运行时规则。
|
||||
- 协议演进要带版本号,否则快照和 UI 很容易错位。
|
||||
- 前端瘦身不能牺牲移动端一屏体验,表现层拆分仍要遵守移动端优先。
|
||||
- 编辑器 API 必须和正式运行时隔离,不要为了方便继续走混用路径。
|
||||
|
||||
---
|
||||
|
||||
## 12. 一句话结论
|
||||
|
||||
这次重构的核心不是“把几个请求改成走 Express”,而是:
|
||||
|
||||
**把项目从“前端主导运行时、后端承接部分接口”的过渡架构,升级成“Express 后端统一持有运行时真相,前端只负责表现和交互”的正式工程架构。**
|
||||
@@ -3,10 +3,10 @@
|
||||
## 当前入口
|
||||
|
||||
- [ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md](./ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md):面向无用历史代码、隐形多数据链路和半成品实现的一轮工程大清洗执行计划,强调先建台账、再删重收口、最后恢复主工程可读性。
|
||||
- [CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md](./CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md):当前阶段最值得优先做什么、为什么,以及它和审计/PRD 的对应关系。
|
||||
- [CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md](./CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md):在不新增前端创作流程的前提下,围绕当前 Agent 创作动线做收口、删重、补通和文档收束的大白话执行规划。
|
||||
- [EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md](./EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md):基于“前端只做表现、逻辑与数据全部后端化”的工程重构规划。
|
||||
- [EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md](./EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md):将后端化重构拆成可并行推进、尽量不冲突的任务流与协作顺序。
|
||||
- [../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md):当前创作入口、Agent session、结果页自动保存、作品库与进入世界主链的正式文件级重构基线;涉及目录落位、命名规范、阶段验收与工作包拆分时优先看这一份。
|
||||
- [../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md):当前平台入口、继续游戏、角色选择、RPG runtime 与 runtime story 主链的正式文件级重构基线;涉及入口壳层、session、runtime、story、route/service/repository 拆分时优先看这一份。
|
||||
- [CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md](./CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md):创作链高层目标、冻结边界与执行顺序说明;文件级拆分与阶段验收以创作链重构执行方案为准。
|
||||
- [../technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](../technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md):当前后端唯一落地口径,后续排期涉及服务端、数据真相或 SpacetimeDB 时优先按这一份判断方向。
|
||||
- [BEIJING_POLICY_APPLICATION_OVERVIEW_13_21_24_2026-04-14.md](./BEIJING_POLICY_APPLICATION_OVERVIEW_13_21_24_2026-04-14.md):北京市方向 13 / 21 / 24 的统一判断、共用材料框架和准备顺序。
|
||||
- [BEIJING_DIRECTION13_APPLICATION_MATERIALS_2026-04-14.md](./BEIJING_DIRECTION13_APPLICATION_MATERIALS_2026-04-14.md):方向 13 软件智能化提升奖励的硬门槛、必交材料、底稿建议和证据清单。
|
||||
- [BEIJING_DIRECTION21_APPLICATION_MATERIALS_2026-04-14.md](./BEIJING_DIRECTION21_APPLICATION_MATERIALS_2026-04-14.md):方向 21 “创赢未来”成长计划的报名表、BP、Demo 和融资规划整理。
|
||||
@@ -15,4 +15,6 @@
|
||||
## 使用建议
|
||||
|
||||
- 需要排期、拆阶段、判断先修基线还是先加功能时,先看这份。
|
||||
- 当前如果要推进创作链或 RPG 运行时主链重构,先看上面的两份 `2026-04-21` 执行方案,再回来看高层优先级和冻结边界。
|
||||
- 涉及后端方案时,不再参考已删除的 Express / Node 规划文档,统一回到 Rust / SpacetimeDB 当前基线。
|
||||
- 这份文档大量引用了经验文档、工程审查和 PRD,适合作为跨文档导航页使用。
|
||||
|
||||
@@ -188,14 +188,14 @@ MVP 阶段建议采用最稳妥规则:
|
||||
|
||||
1. 用户名密码注册
|
||||
2. 游客正式入口
|
||||
3. 账号密码找回
|
||||
3. 邮箱登录
|
||||
4. 实名认证
|
||||
5. 社交好友体系
|
||||
6. 多微信绑定同一账号
|
||||
|
||||
说明:
|
||||
|
||||
当前用户名密码模式可仅保留为开发环境兜底能力,不作为正式前台入口。
|
||||
密码登录不是注册入口,也不是邮箱入口;它只作为手机号验证码登录的补充方式。用户必须先通过手机号验证码登录形成正式账号,并在已登录账号中心设置过密码后,后续才能用“手机号 + 密码”登录。
|
||||
|
||||
---
|
||||
|
||||
@@ -388,6 +388,24 @@ MVP 阶段建议采用最稳妥规则:
|
||||
|
||||
MVP 阶段不需要单独设置密码。
|
||||
|
||||
## 6.1.1 密码登录补充方式
|
||||
|
||||
密码登录只补充手机号验证码登录,不建立新的账号体系。
|
||||
|
||||
落地规则:
|
||||
|
||||
- 入参只允许 `phone` 和 `password`,不支持邮箱、用户名或叙世号。
|
||||
- 手机号不存在时,不创建账号,返回统一的登录失败。
|
||||
- 手机号存在但账号未设置过密码时,不允许密码登录。
|
||||
- 首次设置密码只能在已登录账号中心内完成;用户必须先通过手机号验证码或已绑定手机号的微信账号进入已登录态。
|
||||
- 忘记密码 / 重置密码必须先完成该手机号的短信验证码校验;手机号不存在时不创建账号。
|
||||
|
||||
前台约束:
|
||||
|
||||
- 密码页签的账号输入框文案固定为 `手机号`。
|
||||
- 密码页签主按钮固定为 `登录`,不能出现 `注册/登录`。
|
||||
- 短信验证码页签可继续承担“手机号不存在时创建正式账号并登录”的能力,但按钮文案不应暗示密码注册。
|
||||
|
||||
## 6.2 微信登录
|
||||
|
||||
微信登录按终端拆分:
|
||||
@@ -611,7 +629,7 @@ MVP 阶段建议至少提供一个轻量账号中心,包含:
|
||||
因此本期不是推翻重做,而是:
|
||||
|
||||
1. 保留 `users` 作为账号主表
|
||||
2. 废弃“用户名密码自动注册”作为正式入口
|
||||
2. 废弃“用户名密码自动注册”作为任何正式入口
|
||||
3. 增加手机号与微信身份层
|
||||
4. 增加验证码表与会话表
|
||||
|
||||
@@ -619,7 +637,7 @@ MVP 阶段建议至少提供一个轻量账号中心,包含:
|
||||
|
||||
## 8. 接口设计
|
||||
|
||||
所有接口均由 Express 后端承接。
|
||||
所有接口均由 `server-rs` 后端承接。
|
||||
|
||||
## 8.1 手机号登录相关
|
||||
|
||||
@@ -698,6 +716,28 @@ MVP 阶段建议至少提供一个轻量账号中心,包含:
|
||||
|
||||
## 8.3 会话与账号信息
|
||||
|
||||
### `POST /api/auth/entry`
|
||||
|
||||
用途:
|
||||
|
||||
- 使用已设置密码的手机号账号登录
|
||||
|
||||
入参:
|
||||
|
||||
- `phone`
|
||||
- `password`
|
||||
|
||||
出参:
|
||||
|
||||
- `token`
|
||||
- `user`
|
||||
|
||||
约束:
|
||||
|
||||
- 不支持邮箱、用户名或叙世号。
|
||||
- 不承担注册能力。
|
||||
- 只有已存在、已验证手机号、且 `passwordLoginEnabled=true` 的账号可以登录。
|
||||
|
||||
### `GET /api/auth/me`
|
||||
|
||||
返回建议扩展为:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -874,6 +874,7 @@ isCardDetailLoading: boolean;
|
||||
1. 前端步骤名优先复用服务端 `phaseLabel` 的真实语义,不再单独发明一套四段式文案。
|
||||
2. 如果服务端处于批处理阶段,顶部 `phaseLabel` / `phaseDetail` 继续直接显示当前批次信息。
|
||||
3. 自动补主形象与幕背景图也属于草稿生成链路的一部分,不能在进度 UI 中被误折叠成“已完成”后的隐藏耗时。
|
||||
4. 进度页“已耗时”必须按服务端 operation 的创建时间 `startedAt` 与当前时间计算;刷新页面、恢复轮询或前端重挂载时不能重新从本地点击时间开始计时。只有旧 operation 缺少 `startedAt` 时,才允许使用本地记录的开始时间作为兜底。
|
||||
|
||||
## 12.1 生成底稿时序
|
||||
|
||||
|
||||
@@ -307,7 +307,6 @@
|
||||
|
||||
1. `name`
|
||||
2. `description`
|
||||
3. `dangerLevel`
|
||||
|
||||
## 7.4 第四阶段不允许编辑的内容
|
||||
|
||||
@@ -361,10 +360,26 @@
|
||||
1. `id`
|
||||
2. `name`
|
||||
3. `role`
|
||||
4. `publicMask`
|
||||
5. `hiddenHook`
|
||||
6. `relationToPlayer`
|
||||
7. `summary`
|
||||
4. `description`
|
||||
5. `visualDescription`
|
||||
6. `actionDescription`
|
||||
7. `sceneVisualDescription`
|
||||
8. `publicMask`
|
||||
9. `hiddenHook`
|
||||
10. `relationToPlayer`
|
||||
11. `summary`
|
||||
|
||||
### 角色资产工坊默认文本来源
|
||||
|
||||
`visualDescription` 是角色主形象生成入口的默认形象描述主源,必须在可扮演角色 / 场景角色的草稿生成步骤中跟随角色一并生成,不允许在资产工坊打开时再用本地规则把 `description`、`role`、`tags` 拼成默认文案。
|
||||
|
||||
生成要求:
|
||||
|
||||
1. `description` 只写角色定位,控制在 8 到 18 个汉字内,用于角色卡摘要。
|
||||
2. `visualDescription` 专门写角色外观,包含轮廓、服饰 / 身体特征、携带物或材质气质,不写性格规则和玩法说明。
|
||||
3. `actionDescription` 专门写动作气质,用于动作生成默认文本。
|
||||
4. `sceneVisualDescription` 专门写角色常出现的场景氛围,用于场景图或角色场景联动默认文本。
|
||||
5. 资产工坊默认值优先读取 `visualDescription`,只有历史草稿缺失该字段时才允许回退到 `description`。
|
||||
|
||||
### 插入规则
|
||||
|
||||
@@ -409,7 +424,6 @@
|
||||
1. `id`
|
||||
2. `name`
|
||||
3. `description` 或 `summary`
|
||||
4. `dangerLevel`
|
||||
5. `purpose`
|
||||
6. `mood`
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user