This commit is contained in:
15
.claude/settings.local.json
Normal file
15
.claude/settings.local.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(netstat -ano)",
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"Bash(findstr :8081)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(findstr LISTENING)",
|
||||||
|
"Bash(npx tsc:*)",
|
||||||
|
"Bash(lsof -ti:8081)",
|
||||||
|
"Bash(curl -s http://localhost:8081/health)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
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
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
11
.encoding-check-ignore
Normal file
11
.encoding-check-ignore
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Temporary baseline for legacy files that already contain broken text.
|
||||||
|
# Remove a path from this list as soon as the file is repaired.
|
||||||
|
|
||||||
|
docs/audits/text/GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md
|
||||||
|
docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md
|
||||||
|
docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md
|
||||||
|
docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md
|
||||||
|
docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md
|
||||||
|
src/components/AdventurePanel.tsx
|
||||||
|
src/data/customWorldCharacterLoadout.ts
|
||||||
|
dist_check_monster_position/**
|
||||||
180
.env.example
Normal file
180
.env.example
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# Server-side OpenAI-compatible LLM endpoint base URL.
|
||||||
|
LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||||
|
|
||||||
|
# Server-side API key used by the local Vite proxy.
|
||||||
|
# Recommended: set `LLM_API_KEY` or `ARK_API_KEY`.
|
||||||
|
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
|
||||||
|
# but it should not be relied on by browser code.
|
||||||
|
LLM_API_KEY="YOUR_API_KEY"
|
||||||
|
|
||||||
|
# Optional frontend override for the local proxy path.
|
||||||
|
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"
|
||||||
|
|
||||||
|
# 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=""
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Rust api-server JWT settings.
|
||||||
|
JWT_SECRET="CHANGE_ME_FOR_PRODUCTION"
|
||||||
|
# Access token 有效期。
|
||||||
|
JWT_EXPIRES_IN="2h"
|
||||||
|
|
||||||
|
# Refresh session 配置。
|
||||||
|
AUTH_REFRESH_COOKIE_NAME="genarrative_refresh_session"
|
||||||
|
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 和短信签名/模板。
|
||||||
|
# 在 `.env.local` 或进程环境中填入 AccessKey 后会自动启用;如需强制关闭,请显式设置 `SMS_AUTH_ENABLED="false"`。
|
||||||
|
SMS_AUTH_ENABLED="false"
|
||||||
|
SMS_AUTH_PROVIDER="aliyun"
|
||||||
|
ALIYUN_SMS_ACCESS_KEY_ID=""
|
||||||
|
ALIYUN_SMS_ACCESS_KEY_SECRET=""
|
||||||
|
ALIYUN_SMS_ENDPOINT="dypnsapi.aliyuncs.com"
|
||||||
|
# 默认使用阿里云文档中的赠送测试签名/模板,可按控制台实际配置覆盖。
|
||||||
|
ALIYUN_SMS_SIGN_NAME="速通互联验证码"
|
||||||
|
ALIYUN_SMS_TEMPLATE_CODE="100001"
|
||||||
|
ALIYUN_SMS_TEMPLATE_PARAM_KEY="code"
|
||||||
|
ALIYUN_SMS_COUNTRY_CODE="86"
|
||||||
|
ALIYUN_SMS_SCHEME_NAME=""
|
||||||
|
ALIYUN_SMS_CODE_LENGTH="6"
|
||||||
|
ALIYUN_SMS_CODE_TYPE="1"
|
||||||
|
ALIYUN_SMS_VALID_TIME_SECONDS="300"
|
||||||
|
ALIYUN_SMS_INTERVAL_SECONDS="60"
|
||||||
|
ALIYUN_SMS_DUPLICATE_POLICY="1"
|
||||||
|
ALIYUN_SMS_CASE_AUTH_POLICY="1"
|
||||||
|
ALIYUN_SMS_RETURN_VERIFY_CODE="false"
|
||||||
|
SMS_AUTH_MAX_SEND_PER_PHONE_PER_DAY="20"
|
||||||
|
SMS_AUTH_MAX_SEND_PER_IP_PER_HOUR="30"
|
||||||
|
SMS_AUTH_MAX_VERIFY_FAILURES_PER_PHONE_PER_HOUR="12"
|
||||||
|
SMS_AUTH_MAX_VERIFY_FAILURES_PER_IP_PER_HOUR="24"
|
||||||
|
SMS_AUTH_CAPTCHA_TTL_SECONDS="180"
|
||||||
|
SMS_AUTH_CAPTCHA_TRIGGER_VERIFY_FAILURES_PER_PHONE="3"
|
||||||
|
SMS_AUTH_CAPTCHA_TRIGGER_VERIFY_FAILURES_PER_IP="5"
|
||||||
|
SMS_AUTH_BLOCK_PHONE_FAILURE_THRESHOLD="6"
|
||||||
|
SMS_AUTH_BLOCK_IP_FAILURE_THRESHOLD="10"
|
||||||
|
SMS_AUTH_BLOCK_PHONE_DURATION_MINUTES="30"
|
||||||
|
SMS_AUTH_BLOCK_IP_DURATION_MINUTES="30"
|
||||||
|
|
||||||
|
# 仅开发环境:允许本地开发测试自动走游客账号。
|
||||||
|
# 一旦你已经启用手机号/微信登录,建议改成 `false`,这样会直接进入真实登录界面。
|
||||||
|
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||||
|
|
||||||
|
# 微信登录配置。
|
||||||
|
# 当前实现已支持:
|
||||||
|
# 1. `WECHAT_AUTH_PROVIDER="mock"` 的本地假回调联调
|
||||||
|
# 2. `WECHAT_AUTH_PROVIDER="real"` 的真实微信 OAuth 回调
|
||||||
|
# 正式联调时除了补齐 AppID / AppSecret,还要确保微信开放平台回调域名与
|
||||||
|
# `WECHAT_CALLBACK_PATH` 拼出的完整地址一致。
|
||||||
|
WECHAT_AUTH_ENABLED="false"
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Optional model names for character asset studio.
|
||||||
|
DASHSCOPE_CHARACTER_VISUAL_MODEL="wan2.7-image-pro"
|
||||||
|
DASHSCOPE_CHARACTER_IMAGE_SEQUENCE_MODEL="wan2.7-image-pro"
|
||||||
|
DASHSCOPE_CHARACTER_REFERENCE_VIDEO_MODEL="wan2.7-r2v"
|
||||||
|
DASHSCOPE_CHARACTER_MOTION_TRANSFER_MODEL="wan2.2-animate-move"
|
||||||
|
|
||||||
|
# Optional Ark Seedance config for character animation image-to-video.
|
||||||
|
# If omitted, image-to-video will fall back to `ARK_API_KEY` / `LLM_API_KEY`
|
||||||
|
# and `ARK_BASE_URL` / `LLM_BASE_URL`.
|
||||||
|
ARK_CHARACTER_VIDEO_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
|
||||||
|
ARK_CHARACTER_VIDEO_API_KEY=""
|
||||||
|
ARK_CHARACTER_VIDEO_MODEL="doubao-seedance-2-0-fast-260128"
|
||||||
|
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS="420000"
|
||||||
|
|
||||||
|
# Optional: server-side polling timeout for custom-world scene image generation, in milliseconds.
|
||||||
|
DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS="150000"
|
||||||
|
|
||||||
|
# Optional: longer timeout for character video generation, in milliseconds.
|
||||||
|
DASHSCOPE_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS="420000"
|
||||||
|
|
||||||
|
# Optional: generic frontend timeout for regular LLM requests, in milliseconds.
|
||||||
|
VITE_LLM_REQUEST_TIMEOUT_MS="15000"
|
||||||
|
|
||||||
|
# Optional: longer timeout for custom world generation, in milliseconds.
|
||||||
|
VITE_LLM_CUSTOM_WORLD_TIMEOUT_MS="120000"
|
||||||
|
|
||||||
|
# Optional: timeout for custom-world scene image generation, in milliseconds.
|
||||||
|
VITE_SCENE_IMAGE_REQUEST_TIMEOUT_MS="150000"
|
||||||
|
|
||||||
|
# Optional: print full LLM prompts / outputs in the browser console.
|
||||||
|
# Keep this off by default for cleaner logs.
|
||||||
|
VITE_LLM_DEBUG_LOG="false"
|
||||||
|
|
||||||
|
# Optional: official VikingDB credentials for regenerating build-tag similarities
|
||||||
|
# with the Python embedding script. The script auto-loads `.env.local` and uses
|
||||||
|
# the fixed `bge-large-zh` embedding model.
|
||||||
|
VOLCENGINE_ACCESS_KEY_ID=""
|
||||||
|
VOLCENGINE_SECRET_ACCESS_KEY=""
|
||||||
|
VIKINGDB_HOST=""
|
||||||
|
VIKINGDB_REGION=""
|
||||||
|
|
||||||
|
# APP_URL: The URL where this applet is hosted.
|
||||||
|
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||||
|
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||||
|
APP_URL="MY_APP_URL"
|
||||||
55
.env.local
Normal file
55
.env.local
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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"
|
||||||
|
ARK_CHARACTER_VIDEO_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
|
||||||
|
ARK_CHARACTER_VIDEO_MODEL="doubao-seedance-2-0-fast-260128"
|
||||||
|
VITE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
|
||||||
|
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||||
|
DASHSCOPE_API_KEY="sk-65a0c6fa5e294b9887ace860f9d65990"
|
||||||
|
EMBEDDING_MODEL="doubao-embedding-text-240715"
|
||||||
|
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"
|
||||||
|
ALIYUN_SMS_ACCESS_KEY_ID="LTAI5tM6VjoixveLUNQ7x6z9"
|
||||||
|
ALIYUN_SMS_ACCESS_KEY_SECRET="w8Z8JlQKI1juGPSeirWwlvJfHp9frD"
|
||||||
|
ALIYUN_SMS_ENDPOINT="dypnsapi.aliyuncs.com"
|
||||||
|
ALIYUN_SMS_SIGN_NAME="速通互联验证码"
|
||||||
|
ALIYUN_SMS_TEMPLATE_CODE="100001"
|
||||||
|
ALIYUN_SMS_TEMPLATE_PARAM_KEY="code"
|
||||||
|
ALIYUN_SMS_COUNTRY_CODE="86"
|
||||||
|
ALIYUN_SMS_SCHEME_NAME=""
|
||||||
|
ALIYUN_SMS_CODE_LENGTH="6"
|
||||||
|
ALIYUN_SMS_CODE_TYPE="1"
|
||||||
|
ALIYUN_SMS_VALID_TIME_SECONDS="300"
|
||||||
|
ALIYUN_SMS_INTERVAL_SECONDS="60"
|
||||||
|
ALIYUN_SMS_DUPLICATE_POLICY="1"
|
||||||
|
ALIYUN_SMS_CASE_AUTH_POLICY="1"
|
||||||
|
ALIYUN_SMS_RETURN_VERIFY_CODE="false"
|
||||||
|
|
||||||
|
VITE_AUTH_ALLOW_DEV_GUEST="false"
|
||||||
|
|
||||||
|
# 启用服务端大模型调试日志(记录所有输入输出)
|
||||||
|
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"
|
||||||
92
.eslintrc.cjs
Normal file
92
.eslintrc.cjs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
const globals = require('globals');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
es2022: true,
|
||||||
|
},
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['scripts/**/*.{ts,js,mjs,cjs}'],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/components/game-canvas/**/*.tsx'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/components/GameShell.tsx', 'src/hooks/useCombatFlow.ts'],
|
||||||
|
rules: {
|
||||||
|
'simple-import-sort/imports': 'off',
|
||||||
|
'simple-import-sort/exports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
'react-hooks',
|
||||||
|
'react-refresh',
|
||||||
|
'simple-import-sort',
|
||||||
|
'unused-imports',
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
ignorePatterns: [
|
||||||
|
'dist',
|
||||||
|
'dist_check',
|
||||||
|
'dist_check_monster_position',
|
||||||
|
'node_modules',
|
||||||
|
'public/Icons',
|
||||||
|
'media',
|
||||||
|
'.codex-logs',
|
||||||
|
'*.log',
|
||||||
|
'.preview.*',
|
||||||
|
'temp-build-goal-check/**',
|
||||||
|
'tmp_*',
|
||||||
|
'tmp/**',
|
||||||
|
'npc-editor-*',
|
||||||
|
'temp-write-check.txt',
|
||||||
|
'**/__pycache__/**',
|
||||||
|
'*.timestamp-*.mjs',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-refresh/only-export-components': ['error', {allowConstantExport: true}],
|
||||||
|
'simple-import-sort/imports': 'error',
|
||||||
|
'simple-import-sort/exports': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'unused-imports/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-constant-condition': 'error',
|
||||||
|
'no-console': ['error', {allow: ['warn', 'error']}],
|
||||||
|
'no-useless-escape': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
25
.gitattributes
vendored
Normal file
25
.gitattributes
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.ts text
|
||||||
|
*.tsx text
|
||||||
|
*.js text
|
||||||
|
*.jsx text
|
||||||
|
*.mjs text
|
||||||
|
*.cjs text
|
||||||
|
*.json text
|
||||||
|
*.md text
|
||||||
|
*.txt text
|
||||||
|
*.yml text
|
||||||
|
*.yaml text
|
||||||
|
*.toml text
|
||||||
|
*.css text
|
||||||
|
*.scss text
|
||||||
|
*.html text
|
||||||
|
*.svg text
|
||||||
|
*.ps1 text
|
||||||
|
*.py text
|
||||||
|
*.sh text
|
||||||
|
*.env text
|
||||||
|
*.meta text
|
||||||
|
*.anim text
|
||||||
|
*.controller text
|
||||||
44
.github/workflows/ci.yml
vendored
Normal file
44
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.19.0
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Check encoding
|
||||||
|
run: npm run check:encoding
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint:eslint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Validate content
|
||||||
|
run: npm run check:content
|
||||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
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-rs/target/
|
||||||
|
/server-rs/.spacetimedb/
|
||||||
|
/server-rs/.data/
|
||||||
|
/public/generated-animations
|
||||||
|
/public/generated-character-drafts
|
||||||
|
/public/generated-characters
|
||||||
|
/.codex-temp
|
||||||
|
/target/
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mod.rs
|
||||||
59
.idea/codeStyles/Project.xml
generated
Normal file
59
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<HTMLCodeStyleSettings>
|
||||||
|
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
</HTMLCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="USE_DOUBLE_QUOTES" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<VueCodeStyleSettings>
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||||
|
</VueCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Vue">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
248
.idea/editor.xml
generated
Normal file
248
.idea/editor.xml
generated
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="BackendCodeEditorSettings">
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CDeclarationWithImplicitIntType/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CommentTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConstevalIfIsAlwaysConstant/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppAbstractClassWithoutSpecifier/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppAbstractFinalClass/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppAbstractVirtualFunctionCallInCtor/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppAccessSpecifierWithNoDeclarations/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppAwaiterTypeIsNotClass/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBooleanIncrementExpression/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBoostFormatBadCode/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBoostFormatLegacyCode/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBoostFormatMixedArgs/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBoostFormatTooFewArgs/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBoostFormatTooManyArgs/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppCStyleCast/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppCVQualifierCanNotBeAppliedToReference/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassCanBeFinal/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassIsIncomplete/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassNeedsConstructorBecauseOfUninitializedMember/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppCompileTimeConstantCanBeReplacedWithBooleanConstant/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppConceptNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppConditionalExpressionCanBeSimplified/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppConstParameterInDeclaration/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppConstValueFunctionReturnType/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppCoroutineCallResolveError/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAArrayIndexOutOfBounds/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAConstantConditions/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAConstantFunctionResult/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAConstantParameter/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFADeletedPointer/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAEndlessLoop/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAInfiniteRecursion/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAInvalidatedMemory/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFALocalValueEscapesFunction/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFALocalValueEscapesScope/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFALoopConditionNotUpdated/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAMemoryLeak/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFANotInitializedField/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFANullDereference/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFATimeOver/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnreachableCode/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnreachableFunctionCall/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnreadVariable/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDFAUnusedValue/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeclarationHidesLocal/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeclarationHidesUncapturedLocal/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeclarationSpecifierWithoutDeclarators/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeclaratorDisambiguatedAsFunction/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeclaratorNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeclaratorUsedBeforeInitialization/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDefaultCaseNotHandledInSwitchStatement/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDefaultInitializationWithNoUserConstructor/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDefaultIsUsedAsIdentifier/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDefaultedSpecialMemberFunctionIsImplicitlyDeleted/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeletingVoidPointer/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDependentTemplateWithoutTemplateKeyword/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDependentTypeWithoutTypenameKeyword/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeprecatedEntity/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeprecatedOverridenMethod/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDeprecatedRegisterStorageClassSpecifier/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDereferenceOperatorLimitExceeded/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDiscardedPostfixOperatorResult/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDoxygenSyntaxError/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDoxygenUndocumentedParameter/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppDoxygenUnresolvedReference/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEmptyDeclaration/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceCVQualifiersOrder/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceCVQualifiersPlacement/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceDoStatementBraces/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceForStatementBraces/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceFunctionDeclarationStyle/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceIfStatementBraces/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceNestedNamespacesStyle/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceOverridingDestructorStyle/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceOverridingFunctionStyle/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceTypeAliasCodeStyle/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnforceWhileStatementBraces/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEntityAssignedButNoRead/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEntityUsedOnlyInUnevaluatedContext/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEnumeratorNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEqualOperandsInBinaryExpression/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppEvaluationFailure/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppExplicitSpecializationInNonNamespaceScope/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppExpressionWithoutSideEffects/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFinalFunctionInFinalClass/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFinalNonOverridingVirtualFunction/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppForLoopCanBeReplacedWithWhile/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppForwardEnumDeclarationWithoutUnderlyingType/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFunctionDoesntReturnValue/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFunctionIsNotImplemented/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFunctionResultShouldBeUsed/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppFunctionalStyleCast/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppHeaderHasBeenAlreadyIncluded/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppHiddenFunction/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppHidingFunction/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIdenticalOperandsInBinaryExpression/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIfCanBeReplacedByConstexprIf/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppImplicitDefaultConstructorNotAvailable/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIncompatiblePointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIncompleteSwitchStatement/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppInconsistentNaming/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIntegralToPointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppInvalidLineContinuation/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppJoinDeclarationAndAssignment/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLambdaCaptureNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableMayBeConst/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableMightNotBeInitialized/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableWithNonTrivialDtorIsNeverUsed/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLongFloat/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMemberFunctionMayBeConst/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMemberFunctionMayBeStatic/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMemberInitializersOrder/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMismatchedClassTags/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMissingIncludeGuard/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMissingKeywordThrow/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppModulePartitionWithSeveralPartitionUnits/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMsExtAddressOfClassRValue/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMsExtBindingRValueToLvalueReference/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMsExtCopyElisionInCopyInitDeclarator/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMsExtDoubleUserConversionInCopyInit/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMsExtNotInitializedStaticConstLocalVar/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMsExtReinterpretCastFromNullptr/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMultiCharacterLiteral/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMultiCharacterWideLiteral/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMustBePublicVirtualToImplementInterface/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMutableSpecifierOnReferenceMember/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNoDiscardExpression/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNodiscardFunctionWithoutReturnValue/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNonExceptionSafeResourceAcquisition/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNonExplicitConversionOperator/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNonExplicitConvertingConstructor/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNonInlineFunctionDefinitionInHeaderFile/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNonInlineVariableDefinitionInHeaderFile/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppNotAllPathsReturnValue/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppObjectMemberMightNotBeInitialized/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppOutParameterMustBeWritten/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterMayBeConst/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterMayBeConstPtrOrRef/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterNamesMismatch/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppParameterNeverUsed/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPassValueParameterByConstReference/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPointerConversionDropsQualifiers/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPointerToIntegralConversion/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPolymorphicClassWithNonVirtualPublicDestructor/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPossiblyErroneousEmptyStatements/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPossiblyUninitializedMember/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPossiblyUnintendedObjectSlicing/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrecompiledHeaderIsNotIncluded/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrecompiledHeaderNotFound/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrintfBadFormat/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrintfExtraArg/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrintfMissedArg/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrintfRiskyFormat/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppPrivateSpecialMemberFunctionIsNotImplemented/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRangeBasedForIncompatibleReference/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedefinitionOfDefaultArgumentInOverrideFunction/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantAccessSpecifier/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantBaseClassAccessSpecifier/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantBaseClassInitializer/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantBooleanExpressionArgument/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantCastExpression/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantComplexityInComparison/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantConditionalExpression/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantConstSpecifier/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantControlFlowJump/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantDereferencingAndTakingAddress/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantElaboratedTypeSpecifier/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantElseKeyword/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantElseKeywordInsideCompoundStatement/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantEmptyDeclaration/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantEmptyStatement/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantExportKeyword/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantFwdClassOrEnumSpecifier/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantInlineSpecifier/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantLambdaParameterList/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantMemberInitializer/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantNamespaceDefinition/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantParentheses/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantQualifier/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantQualifierADL/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantStaticSpecifierOnMemberAllocationFunction/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantStaticSpecifierOnThreadLocalLocalVariable/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantTemplateArguments/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantTemplateKeyword/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantTypenameKeyword/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantVoidArgumentList/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRedundantZeroInitializerInAggregateInitialization/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppReinterpretCastFromVoidPtr/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppRemoveRedundantBraces/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppReplaceMemsetWithZeroInitialization/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppReplaceTieWithStructuredBinding/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppReturnNoValueInNonVoidFunction/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppSmartPointerVsMakeFunction/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppSomeObjectMembersMightNotBeInitialized/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppSpecialFunctionWithoutNoexceptSpecification/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticAssertFailure/@EntryIndexedValue" value="ERROR" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticDataMemberInUnnamedStruct/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticSpecifierOnAnonymousNamespaceMember/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStringLiteralToCharPointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTabsAreDisallowed/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateArgumentsCanBeDeduced/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateParameterNeverUsed/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateParameterShadowing/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppThrowExpressionCanBeReplacedWithRethrow/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTooWideScope/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTooWideScopeInitStatement/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTypeAliasNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUninitializedDependentBaseClass/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUninitializedNonStaticDataMember/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnionMemberOfReferenceType/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnmatchedPragmaEndRegionDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnmatchedPragmaRegionDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnamedNamespaceInHeaderFile/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnecessaryWhitespace/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnsignedZeroComparison/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnusedIncludeDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseAlgorithmWithCount/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseAssociativeContains/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseAuto/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseAutoForNumeric/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseElementsView/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseEraseAlgorithm/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseFamiliarTemplateSyntaxForGenericLambdas/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseRangeAlgorithm/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseStdSize/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseStructuredBinding/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseTypeTraitAlias/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUserDefinedLiteralSuffixDoesNotStartWithUnderscore/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUsingResultOfAssignmentAsCondition/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppVariableCanBeMadeConstexpr/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppVirtualFunctionCallInsideCtor/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppVirtualFunctionInFinalClass/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppVolatileParameterInDeclaration/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppWarningDirective/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppWrongIncludesOrder/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppWrongSlashesInIncludeDirective/@EntryIndexedValue" value="HINT" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppZeroConstantCanBeReplacedWithNullptr/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppZeroValuedExpressionUsedAsNullPointer/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=IdentifierTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=IfStdIsConstantEvaluatedCanBeReplaced/@EntryIndexedValue" value="SUGGESTION" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=StdIsConstantEvaluatedWillAlwaysEvaluateToConstant/@EntryIndexedValue" value="WARNING" type="string" />
|
||||||
|
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralTypo/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Genarrative.iml" filepath="$PROJECT_DIR$/.idea/Genarrative.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/prettier.xml
generated
Normal file
6
.idea/prettier.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PrettierConfiguration">
|
||||||
|
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
14
.prettierignore
Normal file
14
.prettierignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
.codex-logs
|
||||||
|
public/Icons
|
||||||
|
media
|
||||||
|
*.log
|
||||||
|
.preview.*
|
||||||
|
tmp_*
|
||||||
|
tmp/
|
||||||
|
temp-build-goal-check/
|
||||||
|
npc-editor-*
|
||||||
|
temp-write-check.txt
|
||||||
|
**/__pycache__/
|
||||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
109
AGENTS.md
Normal file
109
AGENTS.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 项目约束
|
||||||
|
- 在修改server-rs的内容时,不要去兼容server-node中的任何内容,只允许参考,以及把server-node中未迁移到server-rs的内容迁移过来
|
||||||
|
- 代码需要有完善的中文注释
|
||||||
|
- 在落地工程修改前检查是否有详细指导本次落地的文档,若没有文档或文档的完善程度仍有落地过程中编码级别的歧义优先优化文档后落地工程迭代。
|
||||||
|
- 对工程的修改不仅要落地到代码更面,还要更改对应文档,若没有生成新的文档,文档统一存在doc目录中
|
||||||
|
- 不要擅自把现有中文文案、注释、剧情、文档改写成英文,除非用户明确要求翻译。
|
||||||
|
- 看到中文乱码时,不要直接沿用乱码文本,也不要用英文替换;先确认文件真实编码,再决定是否修改。
|
||||||
|
- 在 PowerShell 5.1 中读取或写入文本时,必须显式使用 UTF-8;如果终端输出疑似乱码,要用 `Get-Content -Encoding UTF8`、Python 或 Node 再次核对原文。
|
||||||
|
- 非必要不要整文件重写,尤其是包含中文的文件;优先做局部补丁,避免把未改动的中文内容重新编码。
|
||||||
|
- 修改包含中文的文件后,优先运行仓库里的编码检查,确保没有把文本写坏。
|
||||||
|
- UI面板中不要默认写一些规则描述文案,清爽一些,按照游戏UI设计规范设计即可。
|
||||||
|
- UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。
|
||||||
|
- 不要在gitignore中添加.env.local文件。
|
||||||
|
- 严格遵循简洁的代码风格
|
||||||
|
- 前端只负责做表现,所有的逻辑、数据都放到后端工程,后端使用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` 自动重新运行后端,并执行相应自动测试;不要再使用旧的后端重启命令。
|
||||||
|
|
||||||
|
|
||||||
|
## 文档图谱
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/
|
||||||
|
├─ README.md
|
||||||
|
├─ audits/
|
||||||
|
│ ├─ README.md
|
||||||
|
│ ├─ FUNCTION_DESIGN_AUDIT_2026-04-03.md
|
||||||
|
│ ├─ ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md
|
||||||
|
│ ├─ engineering/
|
||||||
|
│ │ ├─ README.md
|
||||||
|
│ │ ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md
|
||||||
|
│ │ ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md
|
||||||
|
│ │ ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md
|
||||||
|
│ │ └─ MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md
|
||||||
|
│ └─ text/
|
||||||
|
│ ├─ README.md
|
||||||
|
│ ├─ CHINESE_MOJIBAKE_INVENTORY.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-30.md
|
||||||
|
│ ├─ GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.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-04-01.md
|
||||||
|
│ └─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md
|
||||||
|
├─ design/
|
||||||
|
│ ├─ README.md
|
||||||
|
│ ├─ AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md
|
||||||
|
│ ├─ COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md
|
||||||
|
│ ├─ CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md
|
||||||
|
│ ├─ EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md
|
||||||
|
│ └─ npc-conversation-situation-draft.md
|
||||||
|
├─ experience/
|
||||||
|
│ ├─ README.md
|
||||||
|
│ ├─ ADVENTURE_RUNTIME_DEV_EXPERIENCE.md
|
||||||
|
│ ├─ AGENT_UI_CHANGELOG.md
|
||||||
|
│ ├─ CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md
|
||||||
|
│ ├─ CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md
|
||||||
|
│ ├─ MOBILE_UI_DEV_EXPERIENCE.md
|
||||||
|
│ ├─ PROJECT_DEVELOPMENT_EXPERIENCE.md
|
||||||
|
│ └─ PROJECT_WORK_EXPERIENCE_PLAYBOOK.md
|
||||||
|
├─ planning/
|
||||||
|
│ ├─ README.md
|
||||||
|
│ └─ CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md
|
||||||
|
├─ prd/
|
||||||
|
│ ├─ AI_CHARACTER_VISUAL_ANIMATION_MVP_PRD_2026-04-04.md
|
||||||
|
│ ├─ AI_NATIVE_CLASSIC_RPG_EXPERIENCE_BENCHMARK_PRD_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_CROSS_GENRE_STORY_ENGINE_PRD_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_NARRATIVE_THREAD_ITEM_AND_WORLD_NPC_PRD_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md
|
||||||
|
│ ├─ AI_NATIVE_RUNTIME_ITEM_GENERATION_DESIGN.md
|
||||||
|
│ ├─ AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md
|
||||||
|
│ ├─ AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md
|
||||||
|
│ ├─ BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md
|
||||||
|
│ └─ RUNTIME_ITEM_GENERATION_CURRENT_SYSTEM_DESIGN.md
|
||||||
|
├─ reference/
|
||||||
|
│ ├─ README.md
|
||||||
|
│ └─ FUNCTION_SCRIPT_CATALOG_2026-04-04.md
|
||||||
|
└─ technical/
|
||||||
|
├─ README.md
|
||||||
|
├─ AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md
|
||||||
|
├─ GO_SERVER_RUNTIME_INTEGRATION_2026-04-07.md
|
||||||
|
├─ GO_SERVER_TASKLIST_2026-04-08.md
|
||||||
|
├─ NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md
|
||||||
|
├─ PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md
|
||||||
|
└─ SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md
|
||||||
|
```
|
||||||
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)
|
||||||
|
|
||||||
|
后续如继续细化任务,请优先在该目录内维护,避免根目录散落多份版本。
|
||||||
127
README.md
Normal file
127
README.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# AI Native Visual RPG
|
||||||
|
|
||||||
|
一个以“AI 叙事 + 本地规则 + 像素演出”为核心的视觉 RPG 原型。
|
||||||
|
|
||||||
|
当前已经具备这些主要能力:
|
||||||
|
|
||||||
|
- 世界与角色选择
|
||||||
|
- AI 剧情推进与流式对话
|
||||||
|
- 战斗演出、NPC 战斗、切磋
|
||||||
|
- NPC 交易、送礼、求助、招募
|
||||||
|
- 宝藏交互
|
||||||
|
- 同伴跟随与战斗
|
||||||
|
- 游戏主流程内嵌的角色资产工坊、自定义世界实体编辑与角色形象编辑
|
||||||
|
- 自动存档与继续游戏
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
前置条件:
|
||||||
|
|
||||||
|
- Node.js
|
||||||
|
- Rust / Cargo
|
||||||
|
- SpacetimeDB CLI
|
||||||
|
|
||||||
|
安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
准备环境变量:
|
||||||
|
|
||||||
|
- 复制 `.env.example` 为 `.env.local`
|
||||||
|
- 填入 `LLM_API_KEY` / `ARK_API_KEY`
|
||||||
|
- 按需设置 `VITE_LLM_MODEL`
|
||||||
|
- 如需启用阿里云短信验证码登录,填写 `ALIYUN_SMS_ACCESS_KEY_ID`、`ALIYUN_SMS_ACCESS_KEY_SECRET`,并确认 `SMS_AUTH_PROVIDER="aliyun"`
|
||||||
|
- 本地联调短信登录时,建议将 `VITE_AUTH_ALLOW_DEV_GUEST` 设为 `false`,避免开发模式自动进入游客账号而跳过登录页
|
||||||
|
- 如需打印完整 prompt/output,可把 `VITE_LLM_DEBUG_LOG` 设为 `true`
|
||||||
|
|
||||||
|
启动开发环境:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
补充说明:
|
||||||
|
|
||||||
|
- `npm run dev` 会启动 SpacetimeDB standalone、Rust `api-server` 与 Vite 前端,适合完整联调。
|
||||||
|
- 如果只想单独启动前端页面,可使用 `npm run dev:web`,默认代理到本地 Rust `api-server`。
|
||||||
|
|
||||||
|
构建生产包:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常用检查
|
||||||
|
|
||||||
|
类型检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
编码检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:encoding
|
||||||
|
```
|
||||||
|
|
||||||
|
内容引用校验:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:data
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑器 override 校验:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:overrides
|
||||||
|
```
|
||||||
|
|
||||||
|
关键内容 smoke 检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
一键内容检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check:content
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要结构
|
||||||
|
|
||||||
|
主运行时:
|
||||||
|
|
||||||
|
- [src/App.tsx](/E:/Repos/Genarrative/src/App.tsx)
|
||||||
|
- [src/components/GameShell.tsx](/E:/Repos/Genarrative/src/components/GameShell.tsx)
|
||||||
|
- [src/hooks/useCombatFlow.ts](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts)
|
||||||
|
- [src/hooks/useStoryGeneration.ts](/E:/Repos/Genarrative/src/hooks/useStoryGeneration.ts)
|
||||||
|
|
||||||
|
主流程内嵌编辑能力:
|
||||||
|
|
||||||
|
- [src/components/CustomWorldEntityEditorModal.tsx](/E:/Repos/Genarrative/src/components/CustomWorldEntityEditorModal.tsx)
|
||||||
|
- [src/components/CustomWorldNpcVisualEditor.tsx](/E:/Repos/Genarrative/src/components/CustomWorldNpcVisualEditor.tsx)
|
||||||
|
- [src/components/CustomWorldRoleAssetStudioModal.tsx](/E:/Repos/Genarrative/src/components/CustomWorldRoleAssetStudioModal.tsx)
|
||||||
|
|
||||||
|
核心数据:
|
||||||
|
|
||||||
|
- [src/data/scenePresets.ts](/E:/Repos/Genarrative/src/data/scenePresets.ts)
|
||||||
|
- [src/data/characterPresets.ts](/E:/Repos/Genarrative/src/data/characterPresets.ts)
|
||||||
|
- [src/data/monsterPresets.ts](/E:/Repos/Genarrative/src/data/monsterPresets.ts)
|
||||||
|
- [src/data/npcInteractions.ts](/E:/Repos/Genarrative/src/data/npcInteractions.ts)
|
||||||
|
- [src/data/treasureInteractions.ts](/E:/Repos/Genarrative/src/data/treasureInteractions.ts)
|
||||||
|
|
||||||
|
## 文档入口
|
||||||
|
|
||||||
|
- [docs/README.md](/E:/Repos/Genarrative/docs/README.md):文档总入口,按主题分类后的导航页
|
||||||
|
- [docs/experience/README.md](/E:/Repos/Genarrative/docs/experience/README.md):项目开发经验、UI 交接、历史实现经验
|
||||||
|
- [docs/audits/README.md](/E:/Repos/Genarrative/docs/audits/README.md):工程审查、文本审计、专项审计
|
||||||
|
- [docs/planning/README.md](/E:/Repos/Genarrative/docs/planning/README.md):当前阶段优先级与推进顺序
|
||||||
|
- [docs/design/README.md](/E:/Repos/Genarrative/docs/design/README.md):玩法、关系、物品与对话设计
|
||||||
|
- [docs/technical/README.md](/E:/Repos/Genarrative/docs/technical/README.md):技术路线、服务端方案、外部产品拆解
|
||||||
|
- [docs/reference/README.md](/E:/Repos/Genarrative/docs/reference/README.md):Function 与脚本速查
|
||||||
|
- [docs/prd/](/E:/Repos/Genarrative/docs/prd/):PRD 与阶段计划,原样保留
|
||||||
|
- [UI_CODING_STANDARD.md](/E:/Repos/Genarrative/UI_CODING_STANDARD.md):UI 资产与编码规范
|
||||||
178
UI_CODING_STANDARD.md
Normal file
178
UI_CODING_STANDARD.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# UI Coding Standard
|
||||||
|
|
||||||
|
> **会话交接 / 改动总览**:见 `docs/experience/AGENT_UI_CHANGELOG.md`(文件映射、9-slice 架构、已知坑、未收尾项)。
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This project should treat `public/UI` and `public/Icons` as the single source of truth for fantasy UI chrome and item icon semantics. New UI code should match asset naming first, then map product meaning onto those assets through a small semantic layer in code.
|
||||||
|
|
||||||
|
## Asset Folders
|
||||||
|
|
||||||
|
- `public/UI`: UI chrome, tabs, buttons, frames, arrows, HUD, equipment-slot markers, map controls.
|
||||||
|
- `public/Icons`: item and ability icons. Most files are semantic item glyphs with numeric prefixes.
|
||||||
|
|
||||||
|
## Naming Rules Observed
|
||||||
|
|
||||||
|
### `public/UI`
|
||||||
|
|
||||||
|
- `1_*`: base interaction assets and compact function glyphs.
|
||||||
|
- `11_*`: directional arrows and small action markers.
|
||||||
|
- `_s`: small size.
|
||||||
|
- `_xs`: extra-small size.
|
||||||
|
- `_pick` / `_picked`: selected, active, or pressed state.
|
||||||
|
- `_d`: disabled, dimmed, or inactive state.
|
||||||
|
- `_on` / `_off`: explicit toggle state.
|
||||||
|
- `Hud_icon_*`: HUD/tab-friendly icons.
|
||||||
|
- `Icon_Eq_*`: equipment-slot semantics.
|
||||||
|
- `Map_*`: map-only controls or chrome.
|
||||||
|
- `Frame_*`, `Popup_*`, `Dialogue_*`, `Inventory_*`, `Quest_*`, `Skill_*`: domain-specific container assets.
|
||||||
|
|
||||||
|
### `public/Icons`
|
||||||
|
|
||||||
|
- Pattern is usually `NN_name.png`.
|
||||||
|
- The numeric prefix is an atlas/catalog index, not UI meaning.
|
||||||
|
- Semantic matching should be based on the name segment, not the number.
|
||||||
|
- Prefer item-like meanings such as `sword`, `magic`, `potion`, `relic`, `treasure`, `crystal`, `shield`.
|
||||||
|
|
||||||
|
## Required Coding Pattern
|
||||||
|
|
||||||
|
- Do not hardcode random asset filenames directly in feature components.
|
||||||
|
- Put semantic mappings in `src/uiAssets.ts`.
|
||||||
|
- Render pixel UI assets through `src/components/PixelIcon.tsx`.
|
||||||
|
- Pick icons by UI meaning, not by whichever file "looks close enough" in one screen.
|
||||||
|
- If a state exists in art, wire both active and inactive assets instead of tinting one image in CSS.
|
||||||
|
- Major UI chrome should use authored textures from `public/UI`, not only plain Tailwind borders.
|
||||||
|
- Do not mix `background` shorthand with `backgroundImage` / `backgroundRepeat` / `backgroundPosition` / `backgroundSize` in the same inline `style` object; use longhand fields consistently to avoid React rerender warnings and stale paint bugs.
|
||||||
|
|
||||||
|
## Layout Rules For Icon UI
|
||||||
|
|
||||||
|
- Navigation/tab icons should be visually larger than body text.
|
||||||
|
- Preferred structure is `icon above + label below`, centered, instead of inline icon-text rows for compact nav.
|
||||||
|
- Tab labels should stay readable on mobile with `clamp()`-based sizing.
|
||||||
|
- Touch targets should remain comfortable on phones; do not ship tiny icon buttons.
|
||||||
|
|
||||||
|
## Responsive Rules For UI Images
|
||||||
|
|
||||||
|
- Do not stretch framed pixel UI with `background-size: 100% 100%`.
|
||||||
|
- Framed buttons, panels, tabs, modal shells, and title plates must use 9-slice scaling.
|
||||||
|
- Corners stay fixed, edges only stretch or repeat on one axis, and the center fills independently.
|
||||||
|
- Use `clamp()` for padding, min-height, icon size, and text size when the same control appears on desktop and mobile.
|
||||||
|
- Avoid fixed pixel-only widths for primary panels and buttons unless the element is purely decorative.
|
||||||
|
- For modal or framed panels, allow internal scrolling on small screens instead of overflowing the viewport.
|
||||||
|
- Decorative UI should never block content readability; add a dark tint layer when needed.
|
||||||
|
- Avoid non-integer `scale()` hover effects on pixel-framed controls; prefer `translateY`, brightness, or shadow shifts.
|
||||||
|
|
||||||
|
## Pixel Rendering Rules
|
||||||
|
|
||||||
|
- Pixel icons must use point-sampled rendering such as `image-rendering: pixelated`.
|
||||||
|
- Pixel-framed UI should keep consistent slice thickness on both axes; never uniformly squash the whole frame texture.
|
||||||
|
- Any large information plate inside a popup must also be skinned with UI art, not a plain dark rectangle.
|
||||||
|
|
||||||
|
## Semantic Mapping Used Now
|
||||||
|
|
||||||
|
### Tabs
|
||||||
|
|
||||||
|
- `character` -> `1_armor` / `1_armor_d`
|
||||||
|
- `adventure` -> `1_weapon` / `1_weapon_d`
|
||||||
|
- `inventory` -> `Hud_icon_inventory` / `Hud_icon_inventory_d`
|
||||||
|
|
||||||
|
### World Select
|
||||||
|
|
||||||
|
- `wuxia` -> `38_sword`
|
||||||
|
- `xianxia` -> `72_magic`
|
||||||
|
|
||||||
|
### Shared Chrome
|
||||||
|
|
||||||
|
- option arrow -> `11_right_arrow`
|
||||||
|
- map header -> `Map_icon_action`
|
||||||
|
- close action -> `1_exit_s`
|
||||||
|
|
||||||
|
### UI Shell / Panels
|
||||||
|
|
||||||
|
- app shell -> `Background_fill`
|
||||||
|
- world selection buttons -> `1_orange_button`, `1_violet_button`(极扁条形图,切片须符合尺寸约束,见「已知问题」)
|
||||||
|
- character selection card -> `pick_hero_frame` + `pick_hero_bg`
|
||||||
|
- tab shell -> `Shop_tab`, `Shop_tab_picked`
|
||||||
|
- section panel -> `Frame_bg_big_2`
|
||||||
|
- story panel -> `Dialogue_frame`
|
||||||
|
- inventory panel -> `Inventory_bg`
|
||||||
|
- stats panel -> `Stats_bar`
|
||||||
|
- choice button -> `Options_bar`
|
||||||
|
- modal shell -> `Popup_window`
|
||||||
|
- map info plate -> `Dialogue_frame`
|
||||||
|
- map node cells -> `Map_frame`
|
||||||
|
- map diagram canvas -> `Frame_bg_big_2`
|
||||||
|
- scene title -> `Title_frame_m`
|
||||||
|
- app / root chrome -> `Background_fill` + light tint(避免整块纯深蓝底)
|
||||||
|
|
||||||
|
### Equipment / Inventory
|
||||||
|
|
||||||
|
- `武器` -> `Icon_Eq_Weapon`
|
||||||
|
- `护甲` -> `Icon_Eq_Chest`
|
||||||
|
- `饰品` -> `Icon_Eq_ring`
|
||||||
|
- `消耗品` -> `12_potion`
|
||||||
|
- `稀有品` -> `68_relic`
|
||||||
|
- `专属品` -> `47_treasure`
|
||||||
|
- `材料` -> `45_crystal`
|
||||||
|
|
||||||
|
## State Handling
|
||||||
|
|
||||||
|
- Selected tab/button: prefer `_pick` or `_picked`.
|
||||||
|
- Disabled/unavailable action: prefer `_d`.
|
||||||
|
- Toggle widgets: prefer `_on` / `_off`.
|
||||||
|
- Size changes must use authored sizes like `_s` / `_xs`; avoid CSS-downscaling a large ornamental asset when a small authored version exists.
|
||||||
|
|
||||||
|
## Assets To Avoid By Default
|
||||||
|
|
||||||
|
These names are ambiguous and should not be used as standards until art is confirmed:
|
||||||
|
|
||||||
|
- `* copy`
|
||||||
|
- `Avatar_ref`
|
||||||
|
- `map_ref`
|
||||||
|
- `special_button`
|
||||||
|
- `Skill_bar_long_nopt`
|
||||||
|
- `Skill_bar_noColbs`
|
||||||
|
- `Options_button_save_p`
|
||||||
|
- `11_Q_d`
|
||||||
|
|
||||||
|
## Review Checklist For New UI
|
||||||
|
|
||||||
|
- Does the component use `src/uiAssets.ts` instead of ad-hoc file paths?
|
||||||
|
- Does the chosen icon name match the feature meaning?
|
||||||
|
- If the control has selected/disabled states, are those states backed by asset variants?
|
||||||
|
- Is the asset from `UI` for chrome and `Icons` for item semantics?
|
||||||
|
- Is pixel rendering preserved?
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { PixelIcon } from './components/PixelIcon';
|
||||||
|
import { TAB_ICONS } from './uiAssets';
|
||||||
|
|
||||||
|
<PixelIcon
|
||||||
|
src={active ? TAB_ICONS.inventory.active : TAB_ICONS.inventory.inactive}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known issues / 已知问题
|
||||||
|
|
||||||
|
### 开局「武侠 / 仙侠」按钮中部发空 / 像透明
|
||||||
|
|
||||||
|
**现象**:世界选择页上,`1_orange_button` / `1_violet_button` 套 9-slice 后,中间区域异常,像没画上或透出背景。
|
||||||
|
|
||||||
|
**正确原因(切片几何,而非文字冲突)**:
|
||||||
|
|
||||||
|
1. **图源尺寸**:`1_orange_button.png` / `1_violet_button.png` 实际为 **125×28** 的横向条(可用设计工具或脚本读 IHDR 确认)。
|
||||||
|
2. **非法切片**:若 `border-image-slice` 在竖直方向取 `top + bottom >= 高度(28)`,则图源里用于「中间横条」的像素行数为 **0**。此时 `fill` 没有可拉伸的中间带,浏览器无法正确铺满内容区,中间会发空或表现异常。
|
||||||
|
3. **错误补救**:用 `baseColor` 铺底虽能盖住背景,但**与素材本身的渐变/内描边不一致**,且中间没有原画的边框层次,视觉上「只有一块平色 + 外框」,不符合预期。
|
||||||
|
|
||||||
|
**不是**:`PixelIcon` 与 `pixel-world-button__label` 抢层级或颜色把中间「挡住」;二者在内容区内,不会导致 9-slice 中间带消失。
|
||||||
|
|
||||||
|
**正确修改方式**:
|
||||||
|
|
||||||
|
- **先按像素量体裁衣**:保证 `slice.top + slice.bottom < 图高`,`slice.left + slice.right < 图宽`。对 28px 高的条,上下 slice 宜各约 **8~10px**(需与素材圆角/浮雕厚度对齐),留出 **约 8~12px** 高的中间行供纵向拉伸。
|
||||||
|
- 横条在 UI 里被拉得很高时,中间带主要靠 **纵向 stretch**;`border-image-repeat` 对边可用 `stretch`(条形成品常见),按观感在 `round` / `stretch` 间微调。
|
||||||
|
- 仅在确认图源中心 **确实透明**(RGBA)且引擎式叠色才是需求时,再考虑 `baseColor` 或换一张中心有底的按钮图。
|
||||||
|
|
||||||
|
**维护提醒**:新增 9-slice 配置前,先读 **宽高**,再填 slice,避免 `top+bottom` 或 `left+right` 把中间切片挤没。
|
||||||
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
|
||||||
30
docs/README.md
Normal file
30
docs/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 文档总览
|
||||||
|
|
||||||
|
`docs/` 现在按主题拆成了 6 类;旧后端路线文档开始聚合和删除,后续实现以 Rust / SpacetimeDB 当前基线为准。
|
||||||
|
|
||||||
|
## 快速入口
|
||||||
|
|
||||||
|
- [经验沉淀](./experience/README.md):项目开发经验、UI 交接、历史实现经验。
|
||||||
|
- [审计与复盘](./audits/README.md):工程审查、文本/乱码审计、专项落地审计。
|
||||||
|
- [系统设计](./design/README.md):玩法、关系、物品与对话设计。
|
||||||
|
- [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。
|
||||||
|
- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级。
|
||||||
|
- [参考目录](./reference/README.md):脚本/Function 速查入口。
|
||||||
|
- [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);涉及后端先看 [当前后端实现基线](./technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)。
|
||||||
|
5. 需要对齐目标边界时再进入 [PRD](./prd)。
|
||||||
|
|
||||||
|
## 分类规则
|
||||||
|
|
||||||
|
- `experience/`:偏方法论、交接经验、长期有效的开发结论。
|
||||||
|
- `audits/`:偏“现状扫描 / 问题定位 / 是否达标”的审查类文档。
|
||||||
|
- `design/`:偏玩法机制、叙事关系、系统结构设计。
|
||||||
|
- `technical/`:偏技术选型、实现路线、竞品/产品形态拆解。
|
||||||
|
- `planning/`:偏阶段优先级与推进顺序。
|
||||||
|
- `reference/`:偏目录、速查、检索辅助。
|
||||||
417
docs/audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md
Normal file
417
docs/audits/AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# Agent 聊天到草稿生成到进入游戏世界链路审计
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 0. 审计目标
|
||||||
|
|
||||||
|
本次审计只看一条链:
|
||||||
|
|
||||||
|
`Agent 聊天 -> 世界草稿生成 -> 结果页/作品库 -> 进入游戏世界`
|
||||||
|
|
||||||
|
聚焦回答四类问题:
|
||||||
|
|
||||||
|
1. 哪些数据在链路中断掉了
|
||||||
|
2. 哪些地方在代码里同时存在多条 pipeline
|
||||||
|
3. 哪些字段、功能、组件已经变成冗余或主链弱消费
|
||||||
|
4. 哪些能力在 contract、PRD 或代码结构里已经定义,但并没有真正实装到当前游戏主流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 结论先行
|
||||||
|
|
||||||
|
当前系统还没有形成“Agent 会话是唯一真相源、发布后再进入世界”的单一主链,而是处在多条 pipeline 并存、多个桥接层临时粘合的状态。
|
||||||
|
|
||||||
|
最关键的结论有 8 条:
|
||||||
|
|
||||||
|
1. 当前至少并存 `5` 条相关 pipeline,其中真正影响可玩流程的主链至少有 `3` 条。
|
||||||
|
2. 最大的数据断点是:`CustomWorldAgentSessionSnapshot.draftProfile` 不直接进入 runtime,前端 `buildCustomWorldProfileFromAgentDraft()` 会先把它本地编译成 legacy `CustomWorldProfile`,后面的结果页、自动保存、进入世界都只认这个 legacy profile。
|
||||||
|
3. 服务端内部也存在一次“先编成 legacy runtime profile,再转回 foundation draft”的双重编译,`draftProfile.legacyResultProfile` 是这个桥接层留下来的强耦合字段。
|
||||||
|
4. `packages/shared/src/contracts/customWorldAgent.ts`、`server-node/src/routes/customWorldAgent.ts`、`server-node/src/services/customWorldAgentOrchestrator.ts` 三层定义不一致,`publish_world / generate_scene_assets / sync_scene_assets / expand_long_tail / lock_cards / unlock_cards / regenerate_scope` 等关键动作没有形成真实可用链路。
|
||||||
|
5. `CustomWorldResultView.tsx` 仍保留“直接对 legacy profile 生成角色/地点、直接编辑 profile”的旧流程,会绕过 Agent session,是当前最明显的并行 pipeline 和冗余功能源。
|
||||||
|
6. “进入世界”和“发布世界”目前是两套平行逻辑。Agent 草稿结果页可以自动保存并直接进入世界,但 `publish_world` action 仍不可用,`qualityFindings / blocker` 校验也没有真正接入。
|
||||||
|
7. `listCustomWorldWorks()` 与 `CustomWorldWorkSummaryService` 已能聚合 Agent 草稿和已发布 profile,但平台 `create` tab 仍主要展示 `myEntries`,Agent draft session 不能自然回到主入口,恢复创作主要依赖 `activeSessionId`。
|
||||||
|
8. Agent 工作区主 UI 只接了头部、进度、线程、输入框、操作横幅等极简子集,PRD 里规划的锁定条、草稿抽屉、详情面板、澄清面板、快捷动作、发布校验结果等大部分还没有真正进入当前游戏主流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目标链路
|
||||||
|
|
||||||
|
按 `docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md` 和 `docs/prd/AI_NATIVE_AGENT_FIRST_EIGHT_ANCHOR_CO_CREATION_FLOW_PRD_2026-04-16.md`,目标链路应当是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent 对话
|
||||||
|
-> Express 后端维护结构化 eight-anchor / creatorIntent / lockState / draftSnapshot
|
||||||
|
-> foundation draft
|
||||||
|
-> 角色资产工坊 / 场景资产工坊
|
||||||
|
-> sync 回 Agent session draft
|
||||||
|
-> expand long tail
|
||||||
|
-> publish_world
|
||||||
|
-> 服务端执行 quality / blocker 校验
|
||||||
|
-> 服务端编译最终 CustomWorldProfile
|
||||||
|
-> 持久化到世界库
|
||||||
|
-> 进入世界
|
||||||
|
```
|
||||||
|
|
||||||
|
这条目标链路有 4 个硬约束:
|
||||||
|
|
||||||
|
1. Express 后端才是真实状态源,前端只负责展示和输入,不负责结构化草稿编译。
|
||||||
|
2. 未发布的 Agent 草稿不应该直接污染正式世界库,主入口里应该通过“继续创作”恢复。
|
||||||
|
3. 进入世界前应先经过 `publish_world`,并由发布校验阻止缺角色资产、缺场景资产、缺主线第一幕等 blocker。
|
||||||
|
4. 结果页不再是旧自定义世界编辑器的平移副本,而应更接近“最终预览 / 发布确认 / 进入世界”的收口层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 当前真实链路
|
||||||
|
|
||||||
|
## 3.1 Agent 会话草稿链
|
||||||
|
|
||||||
|
当前新链路实际是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PreGameSelectionFlow.tsx
|
||||||
|
-> /api/runtime/custom-world/agent/sessions
|
||||||
|
-> CustomWorldAgentSessionStore
|
||||||
|
-> CustomWorldAgentOrchestrator
|
||||||
|
-> CustomWorldAgentFoundationDraftService
|
||||||
|
-> CustomWorldAgentAutoAssetService
|
||||||
|
-> session.draftProfile / draftCards / assetCoverage
|
||||||
|
-> 前端 buildCustomWorldProfileFromAgentDraft()
|
||||||
|
-> generatedCustomWorldProfile
|
||||||
|
-> upsertCustomWorldProfile()
|
||||||
|
-> handleCustomWorldSelect(profile)
|
||||||
|
-> runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
关键特点:
|
||||||
|
|
||||||
|
1. Agent session 不是 runtime 直接消费的对象。
|
||||||
|
2. Agent 草稿完成后,会在前端先转成 `CustomWorldProfile`。
|
||||||
|
3. 结果页阶段会自动调用 `upsertCustomWorldProfile()`,把当前 profile 写进 `custom-world-library`。
|
||||||
|
4. “进入世界”按钮直接把这个 profile 送给 `handleCustomWorldSelect(...)`,不需要 `publish_world`。
|
||||||
|
|
||||||
|
主要证据:
|
||||||
|
|
||||||
|
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
- `src/services/customWorldAgentDraftResult.ts`
|
||||||
|
- `src/hooks/useGameFlow.ts`
|
||||||
|
|
||||||
|
## 3.2 旧自定义世界 session 链
|
||||||
|
|
||||||
|
旧链路仍然完整存在:
|
||||||
|
|
||||||
|
```text
|
||||||
|
aiService.generateCustomWorldProfile()
|
||||||
|
-> /api/runtime/custom-world/sessions
|
||||||
|
-> answerCustomWorldSessionQuestion()
|
||||||
|
-> /generate/stream
|
||||||
|
-> generateCustomWorldProfile()
|
||||||
|
-> CustomWorldProfile
|
||||||
|
-> 结果页 / 作品库 / 进入世界
|
||||||
|
```
|
||||||
|
|
||||||
|
关键特点:
|
||||||
|
|
||||||
|
1. `src/services/aiService.ts` 里的 `generateCustomWorldProfile()` 仍然会创建旧 `custom-world/sessions`。
|
||||||
|
2. 前端会先根据 `world_hook / player_premise / opening_situation / core_conflict` 自动补默认回答,再触发流式生成。
|
||||||
|
3. 这条链已经与 Agent 八锚点链并行存在,且依然可用。
|
||||||
|
|
||||||
|
主要证据:
|
||||||
|
|
||||||
|
- `src/services/aiService.ts`
|
||||||
|
- `server-node/src/routes/runtimeRoutes.ts`
|
||||||
|
- `server-node/src/services/customWorldSessionStore.ts`
|
||||||
|
|
||||||
|
## 3.3 已保存 profile / 作品库链
|
||||||
|
|
||||||
|
当前作品库链是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
custom-world-library
|
||||||
|
-> upsert / delete / publish / unpublish
|
||||||
|
-> PlatformHomeView / saved profile detail
|
||||||
|
-> CustomWorldResultView
|
||||||
|
-> handleCustomWorldSelect(profile)
|
||||||
|
```
|
||||||
|
|
||||||
|
关键特点:
|
||||||
|
|
||||||
|
1. 这条链直接消费 `CustomWorldProfile`,不依赖 Agent session。
|
||||||
|
2. Agent 结果页自动保存后,也会落入这条链。
|
||||||
|
3. `publish/unpublish` 作用在作品库 profile 上,而不是 Agent session 上。
|
||||||
|
|
||||||
|
主要证据:
|
||||||
|
|
||||||
|
- `server-node/src/routes/runtimeRoutes.ts`
|
||||||
|
- `src/components/game-shell/PlatformHomeView.tsx`
|
||||||
|
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
|
||||||
|
## 3.4 结果页 legacy profile 直改链
|
||||||
|
|
||||||
|
`CustomWorldResultView.tsx` 仍保留旧能力:
|
||||||
|
|
||||||
|
1. `generateCustomWorldPlayableNpc({ profile })`
|
||||||
|
2. `generateCustomWorldStoryNpc({ profile })`
|
||||||
|
3. `generateCustomWorldLandmark({ profile })`
|
||||||
|
4. `CustomWorldEntityEditorModal`
|
||||||
|
|
||||||
|
这意味着结果页不仅是预览层,还是一套独立的“legacy profile 直改工作台”。这一套能力不会回写 Agent session 的结构化状态,也不会走 Agent action route。
|
||||||
|
|
||||||
|
主要证据:
|
||||||
|
|
||||||
|
- `src/components/CustomWorldResultView.tsx`
|
||||||
|
- `src/services/aiService.ts`
|
||||||
|
|
||||||
|
## 3.5 创作中心 works 聚合链
|
||||||
|
|
||||||
|
后端已经能聚合两类作品:
|
||||||
|
|
||||||
|
1. `sourceType: 'agent_session'`
|
||||||
|
2. `sourceType: 'published_profile'`
|
||||||
|
|
||||||
|
但主平台 `create` tab 现在仍主要展示 `myEntries`,没有把 `CustomWorldCreationHub.tsx` 作为主入口接上。
|
||||||
|
|
||||||
|
这导致:
|
||||||
|
|
||||||
|
1. works 聚合链存在
|
||||||
|
2. create tab 真实消费的是另一条链
|
||||||
|
3. Agent draft session 的继续创作入口没有真正收口到主平台
|
||||||
|
|
||||||
|
主要证据:
|
||||||
|
|
||||||
|
- `server-node/src/services/customWorldWorkSummaryService.ts`
|
||||||
|
- `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||||
|
- `src/components/game-shell/PlatformHomeView.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据断点
|
||||||
|
|
||||||
|
| 断点 | 当前现状 | 影响 | 主要证据 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Agent session -> runtime | `buildCustomWorldProfileFromAgentDraft()` 在前端把 `session.draftProfile` 编译成 legacy `CustomWorldProfile`,后续结果页、自动保存、进入世界都只认 profile | 后端不再是最终唯一真相源,前端承担了结构化编译与字段裁决,容易产生字段丢失、语义漂移、状态失真 | `src/components/game-shell/PreGameSelectionFlow.tsx`、`src/services/customWorldAgentDraftResult.ts` |
|
||||||
|
| foundation draft 内部双重编译 | `CustomWorldAgentFoundationDraftService` 会先 `buildCompiledCustomWorldProfile(...)`,再 `convertRuntimeProfileToFoundationDraft(...)`,并把结果塞进 `legacyResultProfile` | Agent draft 不是原生生成,而是绕了一次 legacy profile,再回 draft;后续桥接层依赖这个字段继续工作 | `server-node/src/services/customWorldAgentFoundationDraftService.ts` |
|
||||||
|
| 创作态元数据进入最终 profile | 前端桥接时会把 `anchorContent / creatorIntent / anchorPack / lockState` 一并塞进 legacy profile;同时固定写入 `generationMode: 'fast'`、`generationStatus: 'key_only'` | 创作态数据污染运行时 profile 存储;`generationMode / generationStatus` 还会覆盖真实阶段语义 | `src/services/customWorldAgentDraftResult.ts` |
|
||||||
|
| Agent session 元数据在结果页后被截断 | `draftCards / pendingClarifications / suggestedActions / qualityFindings / checkpoints / operations` 大多停留在 session 层;结果页与 runtime 只继续消费 profile | 进入结果页后,Agent 会话层的大量结构化上下文被切断,发布门槛、锁定、局部重生成等信息无法自然继承 | `packages/shared/src/contracts/customWorldAgent.ts`、`server-node/src/services/customWorldAgentSessionStore.ts`、`src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx` |
|
||||||
|
| works 聚合 -> 平台 create tab | 后端 `listCustomWorldWorkSummaries(...)` 能返回 draft 与 published,但 create tab 仍只渲染 `myEntries` | Agent draft session 无法稳定出现在主入口“我的创作”里,恢复创作入口割裂 | `server-node/src/services/customWorldWorkSummaryService.ts`、`src/components/game-shell/PlatformHomeView.tsx` |
|
||||||
|
| 发布状态 -> 可玩状态 | 结果页会自动 `upsertCustomWorldProfile()` 并允许直接 `onEnterWorld`;但 `publish_world` action 仍不可用 | “可玩”与“已发布”没有统一门槛,发布校验无法阻止未完成草稿进入世界 | `src/components/game-shell/PreGameSelectionFlow.tsx`、`server-node/src/services/customWorldAgentOrchestrator.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 多条 Pipeline
|
||||||
|
|
||||||
|
## 5.1 主链级 pipeline
|
||||||
|
|
||||||
|
| pipeline | 真相源 | 当前是否在主流程可达 | 问题 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Agent 会话草稿链 | `CustomWorldAgentSessionStore` + `draftProfile` | 是 | 后半段通过前端桥接成 legacy profile,未形成端到端单一真相源 |
|
||||||
|
| 旧 custom-world session 链 | `CustomWorldSessionStore` | 是 | 与 Agent 八锚点链重复,且前端仍在补默认回答 |
|
||||||
|
| 已保存 / 已发布 profile 链 | `custom-world-library` 中的 `CustomWorldProfile` | 是 | 与 Agent draft session 发布链平行存在 |
|
||||||
|
| 结果页 legacy profile 直改链 | 结果页本地 `profile` | 是 | 绕过 Agent session,属于并行编辑器 |
|
||||||
|
| works 创作中心链 | `listCustomWorldWorks()` 聚合数据 | 否,主平台未接主入口 | 后端已有聚合,但 UI 没真正切过去 |
|
||||||
|
|
||||||
|
## 5.2 资产子链 pipeline
|
||||||
|
|
||||||
|
资产相关还存在“自动补齐”和“人工工坊写回”并存:
|
||||||
|
|
||||||
|
1. `draft_foundation` 后,`CustomWorldAgentAutoAssetService` 会自动补角色主图和幕背景图。
|
||||||
|
2. 角色资产又存在 `generate_role_assets -> sync_role_assets` 的手动工坊写回链。
|
||||||
|
3. 场景资产在 contract 层定义了 `generate_scene_assets / sync_scene_assets`,但主 action 链未打通。
|
||||||
|
|
||||||
|
这导致当前资产链不是一条统一 pipeline,而是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
自动补角色 / 自动补幕背景
|
||||||
|
并存
|
||||||
|
手动角色工坊 -> sync_role_assets
|
||||||
|
缺失
|
||||||
|
手动场景工坊 -> sync_scene_assets
|
||||||
|
```
|
||||||
|
|
||||||
|
主要证据:
|
||||||
|
|
||||||
|
- `server-node/src/services/customWorldAgentAutoAssetService.ts`
|
||||||
|
- `server-node/src/services/customWorldAgentRoleAssetStateService.ts`
|
||||||
|
- `packages/shared/src/contracts/customWorldAgent.ts`
|
||||||
|
- `server-node/src/services/customWorldAgentOrchestrator.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 冗余字段与主链悬空字段
|
||||||
|
|
||||||
|
这里区分两类:
|
||||||
|
|
||||||
|
1. 已经明显承担桥接残留职责的冗余字段
|
||||||
|
2. 在 contract / session 里存在,但当前主流程几乎不消费的悬空字段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 当前状态 | 判断 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `draftProfile.legacyResultProfile` | 桥接残留字段 | foundation draft 服务端先生成 legacy runtime profile,再把它塞回 draft,前端桥接又优先读它 | 明显冗余,属于临时兼容字段,不应长期成为主链依赖 |
|
||||||
|
| `generationMode: 'fast'` | 固定写死字段 | `buildCustomWorldProfileFromAgentDraft()` 固定写入 | 不是草稿真实状态,更像桥接层补丁 |
|
||||||
|
| `generationStatus: 'key_only'` | 固定写死字段 | `buildCustomWorldProfileFromAgentDraft()` 固定写入 | 同上,会掩盖真实生成阶段 |
|
||||||
|
| `anchorContent / creatorIntent / anchorPack / lockState` 被直接塞进 legacy profile | 创作态元数据 | 会跟随自动保存一起写进作品库 profile,但 runtime 并不以这些字段为正式运行时输入 | 当前更像创作态元数据泄漏进运行时 profile |
|
||||||
|
| `qualityFindings` | session / contract 字段 | contract、session store、测试里存在,但没形成生成、渲染、发布阻断闭环 | 当前主链悬空 |
|
||||||
|
| `checkpoints` | session 字段 | session store 会记录,但主工作区和结果页没有真实展示入口 | 当前主链悬空 |
|
||||||
|
| `suggestedActions` | session 字段 | session 会生成,但当前正式工作区没有对应消费面;旧 `QuickActions` 面板已在 `2026-04-21` 判定退出当前版本主链并物理删除 | 当前主链悬空 |
|
||||||
|
| `pendingClarifications` | session 字段 | session 有数据,但当前正式工作区没有对应消费面;旧澄清面板已在 `2026-04-21` 判定退出当前版本主链并物理删除 | 当前主链悬空 |
|
||||||
|
| `operations` 历史 | session 字段 | 主工作区只展示当前 `activeOperation` 横幅,不展示完整历史 | 当前主链弱消费 |
|
||||||
|
| `roleAssetSummaryLabel / cover* / counts` 等 works 字段 | works 聚合字段 | 后端能返回,但主平台 create tab 没走 `works` 入口 | 当前主链弱消费 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 冗余功能与冗余组件
|
||||||
|
|
||||||
|
## 7.1 冗余功能
|
||||||
|
|
||||||
|
| 功能 | 当前状态 | 问题 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 结果页直接生成 playable/story/landmark | `CustomWorldResultView.tsx` 仍可直接调用 AI 生成 | 与 Agent 对象精修链重复,且不会同步回 session |
|
||||||
|
| 结果页直接编辑 `CustomWorldProfile` | `CustomWorldEntityEditorModal` 仍挂在结果页 | 把结果页继续维持成旧编辑器,而不是 Agent 流程的收口层 |
|
||||||
|
| 旧 `custom-world/sessions` 世界生成 | `2026-04-20` 审计时仍完整可用;`2026-04-21` 已完成物理删除 | 与 Agent 八锚点世界创建重复;当前遗留问题已转为文档口径清理 |
|
||||||
|
| 作品库 `publish/unpublish` 与 Agent `publish_world` | 两套“发布”概念并行 | 一套作用于 library profile,一套想作用于 Agent session,但后者还未打通 |
|
||||||
|
| 结果页自动保存 | `generatedCustomWorldProfile` 变化时自动 `upsertCustomWorldProfile()` | 让“草稿保存”“作品库存档”“正式发布”语义混在一起 |
|
||||||
|
|
||||||
|
## 7.2 冗余或已退出当前版本主链的组件
|
||||||
|
|
||||||
|
`src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx` 当前只真正接了:
|
||||||
|
|
||||||
|
1. `CustomWorldAgentHeader`
|
||||||
|
2. `EightAnchorProgressBar`
|
||||||
|
3. `CustomWorldAgentOperationBanner`
|
||||||
|
4. `CustomWorldAgentThread`
|
||||||
|
5. `CustomWorldAgentComposer`
|
||||||
|
|
||||||
|
在 `2026-04-21` 清理前,同目录下还存在一组未接线旧组件:
|
||||||
|
|
||||||
|
1. `CustomWorldAgentLockBar.tsx`
|
||||||
|
2. `CustomWorldAgentDraftDrawer.tsx`
|
||||||
|
3. `CustomWorldAgentDraftDetailPanel.tsx`
|
||||||
|
4. `CustomWorldAgentQuickActions.tsx`
|
||||||
|
5. `CustomWorldAgentSummaryPanel.tsx`
|
||||||
|
6. `CustomWorldAgentIntentSummaryPanel.tsx`
|
||||||
|
7. `CustomWorldAgentClarificationPanel.tsx`
|
||||||
|
8. `CustomWorldGenerateEntityModal.tsx`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
1. `CustomWorldAgentDraftDrawer.tsx` 已在批次 A 清理中删除
|
||||||
|
2. `CustomWorldAgentLockBar.tsx`
|
||||||
|
3. `CustomWorldAgentDraftDetailPanel.tsx`
|
||||||
|
4. `CustomWorldAgentQuickActions.tsx`
|
||||||
|
5. `CustomWorldAgentSummaryPanel.tsx`
|
||||||
|
6. `CustomWorldAgentIntentSummaryPanel.tsx`
|
||||||
|
7. `CustomWorldAgentClarificationPanel.tsx`
|
||||||
|
8. `CustomWorldGenerateEntityModal.tsx`
|
||||||
|
|
||||||
|
已在批次 D 清理中判定为退出当前版本主链,并完成物理删除。
|
||||||
|
|
||||||
|
因此这里的审计结论需要更新为:
|
||||||
|
|
||||||
|
1. 它们不再属于“待接线组件”
|
||||||
|
2. 它们属于已确认退场的旧副面板链
|
||||||
|
3. 当前版本如果还要补 `suggestedActions / pendingClarifications / draftCards` 的消费面,应基于新的主链设计重新定义,而不是默认把旧面板接回来
|
||||||
|
|
||||||
|
另外,`src/components/custom-world-home/CustomWorldCreationHub.tsx` 也已存在,但平台 `create` tab 还没有把它接成主入口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 当前没有真正实装到游戏主流程中的项
|
||||||
|
|
||||||
|
| 能力 | 设计 / 定义位置 | 当前状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `publish_world` 真正发布链 | contract、PRD、route、orchestrator | route 能接,orchestrator 直接 `throw badRequest('publish_world is not available in phase5')` |
|
||||||
|
| `generate_scene_assets` | contract、PRD | contract 定义了,但 action route 未接,主链无执行实现 |
|
||||||
|
| `sync_scene_assets` | contract、PRD | contract 定义了,但 action route 未接,主链无执行实现 |
|
||||||
|
| `expand_long_tail` | contract、PRD | contract 定义了,但主 action 链未接 |
|
||||||
|
| `lock_cards / unlock_cards` | contract、PRD | contract 定义了,但 route / UI / orchestrator 主链未接 |
|
||||||
|
| `regenerate_scope` | contract、PRD | contract 定义了,但 route / UI / orchestrator 主链未接 |
|
||||||
|
| `qualityFindings` 与 blocker 发布门禁 | contract、PRD、技术进度文档 | 字段存在,但没有真实的生成、展示、阻止发布闭环 |
|
||||||
|
| 场景资产工坊从 Agent workspace 打开并写回 | PRD | 主工作区未接详情面板与场景资产 action |
|
||||||
|
| 通过 works 统一恢复 Agent draft / 已发布作品 | works service + creation hub | 后端已有聚合,主平台入口未收口 |
|
||||||
|
| 发布前只允许预览、发布后再进入世界 | PRD | 当前 Agent 草稿结果页可自动保存并直接进入世界 |
|
||||||
|
|
||||||
|
补充说明:
|
||||||
|
|
||||||
|
`docs/technical/SCENE_MULTI_ACT_CREATOR_IMPLEMENTATION_PROGRESS_2026-04-20.md` 已明确写到,发布期 `qualityFindings / blocker` 正式接入仍未完成,这与当前代码状态一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 优先级建议
|
||||||
|
|
||||||
|
## P0:先收一条真正的单一主链
|
||||||
|
|
||||||
|
建议明确把下面这条定为唯一正式主链:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent session
|
||||||
|
-> 服务端 draft snapshot
|
||||||
|
-> 服务端质量检查 / 发布动作
|
||||||
|
-> 服务端编译 final CustomWorldProfile
|
||||||
|
-> 世界库
|
||||||
|
-> runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
对应动作:
|
||||||
|
|
||||||
|
1. 结果页不再承担“主编辑器”职责,至少对 Agent draft 结果页关闭 legacy profile 直改能力。
|
||||||
|
2. 用服务端 preview / compile 接口替代前端 `buildCustomWorldProfileFromAgentDraft()` 的最终裁决职责。
|
||||||
|
3. `publish_world` 打通后,再决定是否允许“发布后立即进入世界”。
|
||||||
|
|
||||||
|
## P0:把“进入世界”和“发布世界”重新绑回同一门槛
|
||||||
|
|
||||||
|
建议收口为:
|
||||||
|
|
||||||
|
1. 未发布 Agent 草稿只能继续创作或查看预览。
|
||||||
|
2. 只有 `publish_world` 成功后,才产出正式 `CustomWorldProfile` 并允许主入口进入世界。
|
||||||
|
3. `qualityFindings / blocker` 必须在 foundation draft 完成、资产写回后、publish 前持续重跑。
|
||||||
|
|
||||||
|
## P1:继续做旧 world session 链的文档收口
|
||||||
|
|
||||||
|
`2026-04-21` 更新:
|
||||||
|
|
||||||
|
旧 `custom-world/sessions` 链已经完成物理删除。
|
||||||
|
|
||||||
|
因此这里不再是“保留还是淘汰”的开放问题,而是:
|
||||||
|
|
||||||
|
1. 继续清理由这条旧链残留在审计、PRD、知识图谱中的过时口径
|
||||||
|
2. 把当前正式主链与仍保留的兼容层边界写清楚
|
||||||
|
|
||||||
|
## P1:把 works 创作中心接回主平台
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
1. 平台 `create` tab 改成消费 `listCustomWorldWorks()`。
|
||||||
|
2. 草稿 session 通过“继续创作”恢复。
|
||||||
|
3. 已发布 profile 通过“进入世界”或“查看详情”进入。
|
||||||
|
4. `myEntries` 退回为作品库子集,而不是 create tab 的唯一数据源。
|
||||||
|
|
||||||
|
## P1:为悬空 session 字段重新定义最小闭环
|
||||||
|
|
||||||
|
`2026-04-21` 更新:
|
||||||
|
|
||||||
|
原文这里建议把旧 `QuickActions / DraftDrawer / DraftDetailPanel / ClarificationPanel` 接回主工作区。
|
||||||
|
|
||||||
|
但这些旧副面板已经在当前版本收口判断中被明确认定为:
|
||||||
|
|
||||||
|
1. 不属于现行主链
|
||||||
|
2. 不再作为当前版本默认待落地项
|
||||||
|
3. 已完成物理删除
|
||||||
|
|
||||||
|
因此当前更准确的建议应该是:
|
||||||
|
|
||||||
|
1. 如果 `suggestedActions / pendingClarifications / draftCards` 仍要进入正式主流程,需要先重新定义符合当前极简工作区的消费方式
|
||||||
|
2. 不应再以“把旧副面板接回来”作为默认方案
|
||||||
|
3. 在没有新主链设计前,这些字段继续标记为“主链悬空”
|
||||||
|
|
||||||
|
## P2:等主链收口后再清桥接字段
|
||||||
|
|
||||||
|
下面这些字段不建议现在立刻删,但应在主链收口后尽快移除:
|
||||||
|
|
||||||
|
1. `draftProfile.legacyResultProfile`
|
||||||
|
2. 前端桥接里固定写死的 `generationMode / generationStatus`
|
||||||
|
3. 仅为兼容旧编辑器而塞进 legacy profile 的创作态元数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 一句话总评
|
||||||
|
|
||||||
|
当前“Agent 聊天 -> 草稿生成 -> 进入世界”已经能跑通一条可玩链,但它还不是 PRD 要求的“后端单一真相源 + 发布门禁收口”的正式链路,而是 `Agent session`、`legacy profile`、`旧 session`、`作品库` 四层并存、靠前端桥接和结果页兼容能力临时拼起来的过渡态。
|
||||||
332
docs/audits/CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md
Normal file
332
docs/audits/CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
# 角色资产 Prompt 链路审计(2026-04-20)
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 0. 本次审计回答什么问题
|
||||||
|
|
||||||
|
本次只回答角色资产相关的 4 个问题:
|
||||||
|
|
||||||
|
1. `characterAssetPrompts.ts` 里的 `visualPromptText` 和 `animationPromptText`,是不是“生成角色形象 / 动作形象的默认描述”。
|
||||||
|
2. 生成角色形象的系统提示词在哪个文件,生成默认角色形象描述文本的提示词在哪个文件。
|
||||||
|
3. 生成角色动作的系统提示词在哪个文件,生成默认角色动作描述文本的提示词在哪个文件。
|
||||||
|
4. 当前链路里是否存在冗余流程、保留接口或无效代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 先说结论
|
||||||
|
|
||||||
|
结论不是“只有一套 prompt”,而是:
|
||||||
|
|
||||||
|
**当前角色资产链路仍然有两层 prompt,但“默认描述文本”已经统一成单一主源。**
|
||||||
|
|
||||||
|
### 1.1 默认描述文本层
|
||||||
|
|
||||||
|
这层的目标是:
|
||||||
|
|
||||||
|
**先给资产工坊里的输入框一个默认可编辑文本。**
|
||||||
|
|
||||||
|
这层不直接拿去生成图片或动作视频。
|
||||||
|
|
||||||
|
当前实际主链来源:
|
||||||
|
|
||||||
|
- `src/prompts/customWorldRolePromptDefaults.ts`
|
||||||
|
|
||||||
|
它会把角色已有字段映射成:
|
||||||
|
|
||||||
|
- `visualPromptText`
|
||||||
|
- `animationPromptText`
|
||||||
|
- `scenePromptText`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `visualPromptText` 优先取 `visualDescription`
|
||||||
|
- `animationPromptText` 优先取 `actionDescription`
|
||||||
|
- `scenePromptText` 优先取 `sceneVisualDescription`
|
||||||
|
|
||||||
|
这层是**默认描述文本**,不是正式图像模型 prompt。
|
||||||
|
|
||||||
|
### 1.2 正式模型 prompt 层
|
||||||
|
|
||||||
|
这层的目标是:
|
||||||
|
|
||||||
|
**把“默认描述文本”进一步编译成正式给图像模型 / 动作模型的完整 prompt。**
|
||||||
|
|
||||||
|
当前主链来源:
|
||||||
|
|
||||||
|
- `server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
1. 前端先有一段短文本
|
||||||
|
2. 后端再用正式 prompt builder 把它扩成模型真正使用的完整 prompt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 角色形象生成链路
|
||||||
|
|
||||||
|
## 2.1 生成角色形象的系统提示词在哪
|
||||||
|
|
||||||
|
如果这里问的是“正式生成角色主图时,真正控制模型输出方向的 prompt 主源在哪”,答案是:
|
||||||
|
|
||||||
|
- `server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
|
||||||
|
更准确说:
|
||||||
|
|
||||||
|
1. `buildNpcVisualPrompt`
|
||||||
|
- 文件:`server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- 作用:把短描述文本和角色摘要合并
|
||||||
|
2. `buildMasterPrompt`
|
||||||
|
- 文件:`packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
- 作用:提供正式的角色主图 prompt 骨架
|
||||||
|
|
||||||
|
最终角色形象正式生成请求使用的是:
|
||||||
|
|
||||||
|
- `buildNpcVisualPrompt(...)`
|
||||||
|
|
||||||
|
调用位置:
|
||||||
|
|
||||||
|
- `server-node/src/modules/assets/characterAssetRoutes.ts`
|
||||||
|
|
||||||
|
即:
|
||||||
|
|
||||||
|
**角色主图正式生成的系统提示词主链,不在前端默认值文件,而在后端 `characterAssetPrompts.ts` + 共享 `qwenSprite.ts`。**
|
||||||
|
|
||||||
|
## 2.2 生成默认角色形象描述文本的提示词在哪
|
||||||
|
|
||||||
|
当前资产工坊默认输入框实际使用:
|
||||||
|
|
||||||
|
- `src/prompts/customWorldRolePromptDefaults.ts`
|
||||||
|
|
||||||
|
这不是 LLM system prompt,而是本地字段映射规则。
|
||||||
|
|
||||||
|
换句话说,当前页面上的默认“形象描述”主要来自:
|
||||||
|
|
||||||
|
- `role.visualDescription`
|
||||||
|
- 或回退到 `role.description`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 角色动作生成链路
|
||||||
|
|
||||||
|
## 3.1 生成角色动作的系统提示词在哪
|
||||||
|
|
||||||
|
当前正式动作生成主链在:
|
||||||
|
|
||||||
|
- `server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
|
||||||
|
其中分两类:
|
||||||
|
|
||||||
|
1. `buildArkCharacterAnimationPrompt`
|
||||||
|
- 当前图生视频动作链路主入口
|
||||||
|
2. `buildNpcAnimationPrompt`
|
||||||
|
- 通用动作视频 prompt builder
|
||||||
|
3. `buildImageSequencePrompt`
|
||||||
|
- 连续帧方案动作 prompt builder
|
||||||
|
4. `buildVideoActionPrompt`
|
||||||
|
- 共享动作模板骨架,在 `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
|
||||||
|
当前主动作链路更偏向:
|
||||||
|
|
||||||
|
- `buildArkCharacterAnimationPrompt`
|
||||||
|
|
||||||
|
调用位置:
|
||||||
|
|
||||||
|
- `server-node/src/modules/assets/characterAssetRoutes.ts`
|
||||||
|
|
||||||
|
## 3.2 生成默认角色动作描述文本的提示词在哪
|
||||||
|
|
||||||
|
当前资产工坊真实默认“动作描述”来源:
|
||||||
|
|
||||||
|
- `src/prompts/customWorldRolePromptDefaults.ts`
|
||||||
|
|
||||||
|
规则是:
|
||||||
|
|
||||||
|
- 优先 `actionDescription`
|
||||||
|
- 回退 `combatStyle`
|
||||||
|
|
||||||
|
这仍然是**默认描述文本层**,不是最终动作模型 prompt。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `characterAssetPrompts.ts` 里的 `visualPromptText` / `animationPromptText` 到底是什么
|
||||||
|
|
||||||
|
这两个字段容易混淆,因为它们名字里带 `Prompt`。
|
||||||
|
|
||||||
|
但当前工程里它们更准确的定位是:
|
||||||
|
|
||||||
|
**“默认描述文本 bundle 字段名”,不是最终图像模型请求体里的最终 prompt 名称。**
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
- `visualPromptText`
|
||||||
|
- 在 UI 里更像“角色形象描述默认文本”
|
||||||
|
- 之后会再被编译进正式图像 prompt
|
||||||
|
- `animationPromptText`
|
||||||
|
- 在 UI 里更像“角色动作描述默认文本”
|
||||||
|
- 之后会再被编译进正式动作 prompt
|
||||||
|
|
||||||
|
所以对你的问题可以直接回答为:
|
||||||
|
|
||||||
|
**是,它们在当前语义上确实可以看作“默认角色形象 / 动作描述文本”。**
|
||||||
|
|
||||||
|
但需要补一句:
|
||||||
|
|
||||||
|
**它们不是最终一步的正式模型系统提示词,而是正式模型 prompt 的上游输入。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 当前真实调用链
|
||||||
|
|
||||||
|
## 5.1 当前资产工坊页面初始默认值主链
|
||||||
|
|
||||||
|
当前真实主链:
|
||||||
|
|
||||||
|
1. 角色对象已有字段进入前端
|
||||||
|
2. `src/prompts/customWorldRolePromptDefaults.ts`
|
||||||
|
3. `CustomWorldRoleAssetStudioModal.tsx`
|
||||||
|
4. 输入框初始值:
|
||||||
|
- `visualPromptText`
|
||||||
|
- `animationPromptText`
|
||||||
|
|
||||||
|
这条链:
|
||||||
|
|
||||||
|
- 快
|
||||||
|
- 本地可控
|
||||||
|
- 不依赖额外一次 LLM 调用
|
||||||
|
|
||||||
|
## 5.2 当前正式角色主图生成主链
|
||||||
|
|
||||||
|
1. 前端把输入框里的 `visualPromptText` 提交到后端
|
||||||
|
2. `server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- `buildNpcVisualPrompt`
|
||||||
|
3. `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
- `buildMasterPrompt`
|
||||||
|
4. 图像模型正式生成
|
||||||
|
|
||||||
|
## 5.3 当前正式角色动作生成主链
|
||||||
|
|
||||||
|
1. 前端把输入框里的 `animationPromptText` 提交到后端
|
||||||
|
2. `server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- `buildArkCharacterAnimationPrompt`
|
||||||
|
- 或 `buildNpcAnimationPrompt`
|
||||||
|
- 或 `buildImageSequencePrompt`
|
||||||
|
3. `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
- `buildVideoActionPrompt`
|
||||||
|
4. 动作模型正式生成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 冗余流程与当前问题
|
||||||
|
|
||||||
|
## 6.1 默认描述文本双链已收口
|
||||||
|
|
||||||
|
此前默认描述文本同时存在:
|
||||||
|
|
||||||
|
1. 前端本地字段映射
|
||||||
|
2. 后端 bundle 编译接口
|
||||||
|
|
||||||
|
本轮已经统一为:
|
||||||
|
|
||||||
|
- `src/prompts/customWorldRolePromptDefaults.ts`
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
**默认描述文本现在只有一条真实主源。**
|
||||||
|
|
||||||
|
对应变化:
|
||||||
|
|
||||||
|
1. 不再保留后端独立的默认 bundle 编译接口。
|
||||||
|
2. 不再保留前端对应的 bundle 生成 API 壳层。
|
||||||
|
3. `server-node/src/prompts/characterAssetPrompts.ts` 只保留正式模型 prompt builder。
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**默认描述文本层的双份真相已经被消除。**
|
||||||
|
|
||||||
|
## 6.2 `scenePromptText` 结构存在,但当前资产工坊没有完整承接
|
||||||
|
|
||||||
|
当前这套链路里:
|
||||||
|
|
||||||
|
- `customWorldRolePromptDefaults.ts` 会返回 `scenePromptText`
|
||||||
|
- `characterAssetPrompts.ts` 也会返回 `scenePromptText`
|
||||||
|
|
||||||
|
但当前资产工坊 UI 里并没有完整对应输入框链路。
|
||||||
|
|
||||||
|
这说明:
|
||||||
|
|
||||||
|
**场景描述文本在结构层存在,但在当前角色资产工坊里没有形成完整的用户可编辑闭环。**
|
||||||
|
|
||||||
|
## 6.3 共享模板与工具模板存在相似实现,但职责不同
|
||||||
|
|
||||||
|
仓库里同时有:
|
||||||
|
|
||||||
|
- `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
- `src/prompts/qwenSpriteSheetToolPrompts.ts`
|
||||||
|
|
||||||
|
它们都提供类似的主图 / 动作模板能力。
|
||||||
|
|
||||||
|
但当前定位不同:
|
||||||
|
|
||||||
|
- `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
- 正式角色资产主链共享模板
|
||||||
|
- `src/prompts/qwenSpriteSheetToolPrompts.ts`
|
||||||
|
- Qwen 工具链 prompt
|
||||||
|
|
||||||
|
它们不是同一条业务主链里的重复实现,但确实容易让人误读为“双份正式模板”。
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**这是“职责上可解释,但认知上高混淆”的并行模板,不建议现在直接删,但需要文档明确边界。**
|
||||||
|
|
||||||
|
## 6.4 当前没有证据说明正式主图 / 动作 prompt builder 是无效代码
|
||||||
|
|
||||||
|
以下 builder 当前都有正式调用点:
|
||||||
|
|
||||||
|
- `buildNpcVisualPrompt`
|
||||||
|
- `buildNpcVisualNegativePrompt`
|
||||||
|
- `buildArkCharacterAnimationPrompt`
|
||||||
|
- `buildNpcAnimationPrompt`
|
||||||
|
- `buildImageSequencePrompt`
|
||||||
|
|
||||||
|
因此它们不能算“无效代码”。
|
||||||
|
|
||||||
|
真正已经被清理掉的保留链路,是此前未接入主 UI 的默认 bundle 接口:
|
||||||
|
|
||||||
|
- `CHARACTER_PROMPT_BUNDLE_SYSTEM_PROMPT`
|
||||||
|
- `buildCharacterPromptBundleUserPrompt`
|
||||||
|
- `/api/assets/character-prompts/generate`
|
||||||
|
|
||||||
|
这套链路已经不再保留在当前仓库主线中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 本次建议
|
||||||
|
|
||||||
|
如果后续要继续收口,建议按顺序处理:
|
||||||
|
|
||||||
|
1. 继续以前端本地映射作为默认描述文本唯一主源。
|
||||||
|
2. 对 `scenePromptText` 做完整承接,不要继续停留在结构存在但 UI 不消费的状态。
|
||||||
|
3. 继续保留 `packages/shared/src/prompts/qwenSprite.ts` 与工具链 prompt 分层,但在文档里强制写清“正式主链 / 工具链”边界。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 本次审计覆盖文件
|
||||||
|
|
||||||
|
- `server-node/src/prompts/characterAssetPrompts.ts`
|
||||||
|
- `packages/shared/src/prompts/qwenSprite.ts`
|
||||||
|
- `server-node/src/modules/assets/characterAssetRoutes.ts`
|
||||||
|
- `src/prompts/customWorldRolePromptDefaults.ts`
|
||||||
|
- `src/components/CustomWorldRoleAssetStudioModal.tsx`
|
||||||
|
- `src/components/asset-studio/characterAssetWorkflowPersistence.ts`
|
||||||
|
- `src/prompts/qwenSpriteSheetToolPrompts.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 一句话版结论
|
||||||
|
|
||||||
|
一句话总结就是:
|
||||||
|
|
||||||
|
**当前角色资产系统把“默认描述文本”和“正式模型 prompt”拆成了两层,这是合理的;默认描述文本层已经统一为前端本地映射单一主源,当前剩余主要问题不再是双主源,而是 `scenePromptText` 仍未形成完整 UI 闭环。**
|
||||||
444
docs/audits/CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md
Normal file
444
docs/audits/CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
# 自定义世界创作工具问题审计与优化建议
|
||||||
|
|
||||||
|
更新时间:`2026-04-08`
|
||||||
|
|
||||||
|
## 0. 结论先说
|
||||||
|
|
||||||
|
当前自定义世界创作工具已经有了比较强的生成骨架、锚点结构和结果编辑能力,但整体仍处在一个很明显的“半收口状态”:
|
||||||
|
|
||||||
|
**设计目标已经走到“创作者工作台”,数据结构已经支持“锚点化输入”,但实际体验仍然更像“大文本生成器 + 大型结果总表编辑器”。**
|
||||||
|
|
||||||
|
如果用一句话概括当前问题,就是:
|
||||||
|
|
||||||
|
**高杠杆创作入口还不够强,低杠杆编辑负担还偏重,局部重生成与后端收口也还没有真正闭环。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 审计范围
|
||||||
|
|
||||||
|
本次审计主要对照了三类信息:
|
||||||
|
|
||||||
|
1. 目标设计与 PRD
|
||||||
|
- `docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md`
|
||||||
|
- `docs/design/CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md`
|
||||||
|
- `docs/design/CUSTOM_WORLD_SELF_OWNED_SETTING_LAYER_OPTIMIZATION_2026-04-08.md`
|
||||||
|
- `docs/design/CUSTOM_WORLD_TEMPLATE_DECOUPLING_AND_CROSS_GENRE_GENERALIZATION_DESIGN_2026-04-08.md`
|
||||||
|
|
||||||
|
2. 当前前端主流程与工作台
|
||||||
|
- `src/components/SelectionCustomizationModals.tsx`
|
||||||
|
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
- `src/components/CustomWorldGenerationView.tsx`
|
||||||
|
- `src/components/CustomWorldResultView.tsx`
|
||||||
|
- `src/components/CustomWorldEntityCatalog.tsx`
|
||||||
|
- `src/components/CustomWorldEntityEditorModal.tsx`
|
||||||
|
|
||||||
|
3. 当前生成链与后端会话层
|
||||||
|
- `src/services/ai.ts`
|
||||||
|
- `src/services/aiService.ts`
|
||||||
|
- `src/services/customWorldCreatorIntent.ts`
|
||||||
|
- `src/services/customWorld.ts`
|
||||||
|
- `server-node/src/services/customWorldSessionStore.ts`
|
||||||
|
- `server-node/src/services/customWorldGenerationService.ts`
|
||||||
|
- `server-node/src/bridges/legacyAiRuntimeBridge.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前主要问题
|
||||||
|
|
||||||
|
## 2.1 输入层和意图结构已经脱节
|
||||||
|
|
||||||
|
当前数据结构已经支持:
|
||||||
|
|
||||||
|
- 世界一句话
|
||||||
|
- 主题关键词
|
||||||
|
- 气质约束
|
||||||
|
- 玩家身份
|
||||||
|
- 开局处境
|
||||||
|
- 核心冲突
|
||||||
|
- 关键势力
|
||||||
|
- 关键角色
|
||||||
|
- 关键地点
|
||||||
|
- 标志性要素
|
||||||
|
- 禁止事项
|
||||||
|
|
||||||
|
但实际入口 `src/components/SelectionCustomizationModals.tsx` 里,创作者弹窗仍然基本只有:
|
||||||
|
|
||||||
|
- 生成模式
|
||||||
|
- 一块大 textarea
|
||||||
|
|
||||||
|
这导致两个直接后果:
|
||||||
|
|
||||||
|
1. 设计里已经想清楚的“高杠杆锚点输入”,还没有真正变成主入口。
|
||||||
|
2. `CustomWorldCreatorIntent` 虽然已经能表达卡片化输入,但 UI 并没有把它转成真正可用的创作工作台。
|
||||||
|
|
||||||
|
目前 `card` 模式更多还停留在类型和测试层,没有成为真实用户路径。
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 把当前创建弹窗升级成“快速文本模式 / 创作卡片模式”双入口。
|
||||||
|
- 快速文本模式保留,但提交后应自动拆出建议锚点,而不是直接把整段文本原封不动送进生成链。
|
||||||
|
- 卡片模式先只做最关键的 `5~6` 张卡,不要一开始把所有高级字段都铺开。
|
||||||
|
- 允许空卡提交,但明确区分“已锁定锚点”和“允许 AI 自由补全”的内容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 澄清机制已经存在,但没有真正服务创作者
|
||||||
|
|
||||||
|
`server-node/src/services/customWorldSessionStore.ts` 已经支持:
|
||||||
|
|
||||||
|
- 根据输入缺口生成澄清问题
|
||||||
|
- 世界核心不足时追问
|
||||||
|
- 玩家身份缺失时追问
|
||||||
|
- 开局处境缺失时追问
|
||||||
|
- 核心冲突缺失时追问
|
||||||
|
|
||||||
|
但 `src/services/aiService.ts` 当前做法是:
|
||||||
|
|
||||||
|
- 创建 session
|
||||||
|
- 读取问题
|
||||||
|
- 直接用 fallback 文案自动回答
|
||||||
|
- 然后继续生成
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
|
||||||
|
**系统表面上已经有“先澄清再生成”的能力,但实际体验里,创作者并没有真正参与这一步。**
|
||||||
|
|
||||||
|
结果就是:
|
||||||
|
|
||||||
|
- 低信息量输入并没有被真正补强
|
||||||
|
- 澄清问题变成了内部兜底,而不是创作协作
|
||||||
|
- 很容易继续生成出“完整但不够像用户想要的世界”
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 把 session question 真正接到前端,作为生成前的二次确认步骤。
|
||||||
|
- 每次只问 `1~3` 个最关键问题,不要把它做成问卷。
|
||||||
|
- 支持“一键使用系统建议”,但必须让创作者可见,而不是静默自动填充。
|
||||||
|
- 把回答结果回写到 `creatorIntent`,而不是只作为一次性会话答案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.3 新建完成后的工作台闭环没有成立
|
||||||
|
|
||||||
|
设计文档里,自定义世界应该是:
|
||||||
|
|
||||||
|
`输入 -> 生成 -> 结果页确认/编辑 -> 保存并进入世界`
|
||||||
|
|
||||||
|
但当前 `src/components/game-shell/PreGameSelectionFlow.tsx` 里,新建世界生成成功后会:
|
||||||
|
|
||||||
|
- 直接保存到世界库
|
||||||
|
- 清空 `generatedCustomWorldProfile`
|
||||||
|
- 返回世界列表页
|
||||||
|
|
||||||
|
而不是进入 `custom-world-result` 结果工作台。
|
||||||
|
|
||||||
|
这会带来几个问题:
|
||||||
|
|
||||||
|
1. 新建后的第一时间确认感不够强。
|
||||||
|
2. 快速模式生成出的“关键对象预览”没有自然承接页。
|
||||||
|
3. 用户生成完后,如果想继续看结果、继续补全、继续编辑,还要再从世界列表里点一次“编辑”。
|
||||||
|
|
||||||
|
这条链路会让“创作中”与“已保存”之间的体验断开。
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 新建完成后默认进入结果工作台,而不是直接跳回世界列表。
|
||||||
|
- 保存动作留在结果页显式触发。
|
||||||
|
- 可以增加“自动保存草稿,但仍停留在结果页”的策略,兼顾安全感和连贯性。
|
||||||
|
- 世界列表更适合作为“已归档内容入口”,不适合作为新建完成页。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.4 结果页仍然偏“数据总表”,低杠杆编辑负担偏重
|
||||||
|
|
||||||
|
当前结果页已经有 `世界 / 锚点 / 可扮演角色 / 场景角色 / 场景` 五个页签,这是正确方向。
|
||||||
|
|
||||||
|
但问题在于,进入编辑器后暴露出来的字段仍然太“底层”了,例如:
|
||||||
|
|
||||||
|
- `backstoryReveal`
|
||||||
|
- 技能列表
|
||||||
|
- 初始物品
|
||||||
|
- 场景 NPC 分配
|
||||||
|
- 场景连接关系
|
||||||
|
|
||||||
|
这些字段对少数深度编辑场景有用,但不应该成为默认主编辑内容。
|
||||||
|
|
||||||
|
当前 `src/components/CustomWorldEntityEditorModal.tsx` 已经变成一个非常重的综合编辑器,里面同时承担:
|
||||||
|
|
||||||
|
- 角色完整档案编辑
|
||||||
|
- 形象编辑入口
|
||||||
|
- AI 资产生成入口
|
||||||
|
- 背景章节编辑
|
||||||
|
- 技能与初始物品编辑
|
||||||
|
- 场景内 NPC 分配
|
||||||
|
- 场景连接维护
|
||||||
|
|
||||||
|
这会带来三层问题:
|
||||||
|
|
||||||
|
1. 创作者负担过重
|
||||||
|
- 很多字段属于“系统编译层”,不属于“创作决策层”。
|
||||||
|
|
||||||
|
2. 移动端负担过重
|
||||||
|
- 大量长表单、长弹窗和多级编辑,对手机创作并不友好。
|
||||||
|
|
||||||
|
3. 工程复杂度过高
|
||||||
|
- 前端工作台承担了太多不同层级的编辑职责。
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 默认只暴露高杠杆编辑:
|
||||||
|
- 世界核心命题
|
||||||
|
- 主题与气质
|
||||||
|
- 玩家身份与开局
|
||||||
|
- 关键势力
|
||||||
|
- 关键角色
|
||||||
|
- 关键地点
|
||||||
|
- 标志性要素
|
||||||
|
- 把技能、初始物品、章节 reveal、连接网络等移到“高级模式”或“系统层编辑”。
|
||||||
|
- 结果页结构从“按对象字段堆表单”改成“按创作价值组织”。
|
||||||
|
- 移动端优先改成分段式面板或底部工作台,不要把长表单都塞进同一个大 modal。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.5 锁定与局部重生成机制还不完整
|
||||||
|
|
||||||
|
当前已经有两套看起来相关的能力:
|
||||||
|
|
||||||
|
1. `creatorIntent` 里的 `locked`
|
||||||
|
2. `lockState` 里的角色 / 地点 / 势力锁定字段
|
||||||
|
|
||||||
|
但实际重生成时,`src/components/game-shell/PreGameSelectionFlow.tsx` 里的合并逻辑只真正处理了:
|
||||||
|
|
||||||
|
- 已锁定角色
|
||||||
|
- 已锁定地点
|
||||||
|
|
||||||
|
而且还是按“名称匹配”保留,不是按稳定 id 或字段级锁定来处理。
|
||||||
|
|
||||||
|
这带来的问题很明显:
|
||||||
|
|
||||||
|
1. 角色或地点一旦重命名,锁定可能失效。
|
||||||
|
2. 势力、冲突、世界概述等高价值内容没有真正进入局部重生成保护范围。
|
||||||
|
3. 当前“锁定能力”更像一个早期过渡实现,还没有形成统一的重生成规则。
|
||||||
|
|
||||||
|
同时,结果页的“重新生成”提示文案仍然是“整世界覆盖式”的语义,这也会进一步削弱用户对重生成的信任感。
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 把锁定语义统一收口到后端,以 `lockState` 为唯一事实来源。
|
||||||
|
- 锁定粒度改成:
|
||||||
|
- 世界字段锁定
|
||||||
|
- 势力锁定
|
||||||
|
- 关键角色锁定
|
||||||
|
- 关键地点锁定
|
||||||
|
- 长尾内容可重生成
|
||||||
|
- 局部重生成至少拆成几类:
|
||||||
|
- 仅补长尾角色
|
||||||
|
- 仅补长尾场景
|
||||||
|
- 仅重做场景网络
|
||||||
|
- 仅重做支持性 NPC
|
||||||
|
- 合并逻辑不要再靠名称匹配,改成稳定 id 或锚点映射。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.6 快速模式还不够“快”,生成页也还不够“创作者视角”
|
||||||
|
|
||||||
|
当前快速模式的主要区别,是把数量降成:
|
||||||
|
|
||||||
|
- 可扮演角色 `3`
|
||||||
|
- 场景角色 `8`
|
||||||
|
- 场景 `4`
|
||||||
|
|
||||||
|
但主生成链本身仍然会继续跑:
|
||||||
|
|
||||||
|
- framework
|
||||||
|
- theme pack
|
||||||
|
- story graph
|
||||||
|
- role narrative
|
||||||
|
- role dossier
|
||||||
|
- narrative profile
|
||||||
|
- finalization
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**现在的 fast 更像“缩数量版 full”,还不是“先出关键锚点与关键对象的创作预览模式”。**
|
||||||
|
|
||||||
|
同时,`src/components/CustomWorldGenerationView.tsx` 当前展示的重点仍然是:
|
||||||
|
|
||||||
|
- 当前批次
|
||||||
|
- 预计等待
|
||||||
|
- 计时
|
||||||
|
- 模型阶段
|
||||||
|
|
||||||
|
而不是创作者真正关心的:
|
||||||
|
|
||||||
|
- 关键角色有没有成型
|
||||||
|
- 核心冲突有没有稳定
|
||||||
|
- 哪些锚点已经锁定
|
||||||
|
- 当前正在补的是关键对象还是长尾内容
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 快速模式改成真正的“关键锚点预览模式”:
|
||||||
|
- 先只生成关键角色、关键地点、核心冲突摘要
|
||||||
|
- 暂不补全所有长尾档案
|
||||||
|
- 生成页改成“创作者视角进度”:
|
||||||
|
- 世界灵魂已确定
|
||||||
|
- 关键角色已成型
|
||||||
|
- 关键地点已落地
|
||||||
|
- 长尾扩展准备开始
|
||||||
|
- 把技术批次隐藏到二级信息里,默认只展示创作状态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.7 自定义世界底层仍然没有完全脱离模板世界依赖
|
||||||
|
|
||||||
|
这部分设计文档和参考清单已经说得比较清楚,代码里也能看到对应痕迹:
|
||||||
|
|
||||||
|
- `templateWorldType`
|
||||||
|
- `WUXIA / XIANXIA` 兼容字段
|
||||||
|
- 规则层 fallback
|
||||||
|
- 视觉参考池 fallback
|
||||||
|
- 角色骨架与怪物池 fallback
|
||||||
|
|
||||||
|
当前问题不在于“还没完全去模板化”本身,而在于:
|
||||||
|
|
||||||
|
**这层依赖仍然深入到了生成、运行时规则、表现词汇和参考资源,不只是一个兼容字段。**
|
||||||
|
|
||||||
|
这会限制:
|
||||||
|
|
||||||
|
1. 跨题材表达稳定性
|
||||||
|
2. 自定义世界的自有设定层独立性
|
||||||
|
3. 后续真正做到“任何题材都能稳定跑”
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 继续按现有设计稿,把模板依赖逐步迁成:
|
||||||
|
- 语义锚层
|
||||||
|
- 规则层
|
||||||
|
- 表现层
|
||||||
|
- 原型参考层
|
||||||
|
- 兼容迁移层
|
||||||
|
- 在迁移完成前,保留兼容字段,但让新逻辑优先读取 `ownedSettingLayers`。
|
||||||
|
- 明确哪些是“兼容桥”,哪些还是“真实主依赖”,避免继续混用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.8 前后端边界仍处于过渡态,和项目约束还有距离
|
||||||
|
|
||||||
|
当前自定义世界已经有了:
|
||||||
|
|
||||||
|
- Node 路由
|
||||||
|
- session
|
||||||
|
- 流式生成
|
||||||
|
- 世界库存储接口
|
||||||
|
|
||||||
|
这是对的。
|
||||||
|
|
||||||
|
但问题是,`server-node/src/services/customWorldGenerationService.ts` 仍然通过 `server-node/src/bridges/legacyAiRuntimeBridge.ts` 去桥接 `src/services/ai.ts`。
|
||||||
|
|
||||||
|
这说明:
|
||||||
|
|
||||||
|
**核心生成逻辑虽然已经被路由包起来了,但真正的生成实现还没有完全成为 Express 侧自己的领域服务。**
|
||||||
|
|
||||||
|
同时,前端仍然承担了不少流程语义:
|
||||||
|
|
||||||
|
- 锁定内容合并
|
||||||
|
- 重生成确认
|
||||||
|
- 结果页覆盖提示
|
||||||
|
- 工作台状态切换
|
||||||
|
|
||||||
|
这和当前仓库“前端只负责表现,逻辑与数据尽量收口到 Express 后端”的方向还有距离。
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 把自定义世界生成链正式下沉到 `server-node` 领域服务。
|
||||||
|
- 把锁定、局部重生成、澄清会话、结果归档规则都放到后端。
|
||||||
|
- 前端只负责:
|
||||||
|
- 输入展示
|
||||||
|
- 进度展示
|
||||||
|
- 结果工作台展示
|
||||||
|
- 明确的用户确认动作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.9 移动端工作台仍有明显压迫感
|
||||||
|
|
||||||
|
项目文档已经明确要求移动端优先,但当前自定义世界工作台里仍有几个典型问题:
|
||||||
|
|
||||||
|
- 大量长 modal
|
||||||
|
- 多段长表单
|
||||||
|
- `window.confirm / window.alert` 原生弹框较多
|
||||||
|
- 结果页与编辑器中同时承载太多操作密度
|
||||||
|
|
||||||
|
这些在桌面端还能勉强接受,但在手机上会很容易变成:
|
||||||
|
|
||||||
|
- 滚动层级混乱
|
||||||
|
- 退出成本高
|
||||||
|
- 修改焦点不明确
|
||||||
|
- 确认感不稳定
|
||||||
|
|
||||||
|
可优化点:
|
||||||
|
|
||||||
|
- 移动端改成底部工作台 / 分步面板 / 可折叠分区。
|
||||||
|
- 用项目内统一确认弹层替换 `window.confirm / window.alert`。
|
||||||
|
- 让“保存”“继续补全”“局部重生成”这些关键动作固定在底部安全区附近。
|
||||||
|
- 把搜索、批量删除、创建动作做成更轻的操作条,而不是持续挤压正文区。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 优先级建议
|
||||||
|
|
||||||
|
### P0:先修主链路闭环
|
||||||
|
|
||||||
|
- 补卡片化输入入口,至少把关键锚点输入真正开放出来。
|
||||||
|
- 把澄清问题正式接入创作者流程,不再静默自动兜底。
|
||||||
|
- 修正“新建完成后直接回世界列表”的流程,生成后默认进入结果工作台。
|
||||||
|
- 统一锁定与局部重生成规则,先让“创作者不怕重生成”成立。
|
||||||
|
|
||||||
|
### P1:再降低工作台负担
|
||||||
|
|
||||||
|
- 结果页默认只展示高杠杆编辑。
|
||||||
|
- 低杠杆字段进入高级模式。
|
||||||
|
- 快速模式改成真正的关键对象预览模式。
|
||||||
|
- 生成页改成创作者视角进度,而不是模型批次视角。
|
||||||
|
|
||||||
|
### P2:最后做架构收口与去模板化
|
||||||
|
|
||||||
|
- 把生成链、锁定规则、会话澄清彻底收回 Express 后端。
|
||||||
|
- 持续推进 `ownedSettingLayers` 成为真实主设定层。
|
||||||
|
- 逐步去掉自定义世界对模板世界的深依赖。
|
||||||
|
- 针对移动端重做工作台的操作密度和确认路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 推荐落地顺序
|
||||||
|
|
||||||
|
如果只按“最小投入、最大体验收益”排序,建议按下面四步做:
|
||||||
|
|
||||||
|
1. 先改输入与结果闭环
|
||||||
|
- 卡片化最小入口
|
||||||
|
- 澄清问题接入
|
||||||
|
- 新建后进入结果页
|
||||||
|
|
||||||
|
2. 再改锁定与局部重生成
|
||||||
|
- 用稳定 id
|
||||||
|
- 用统一 lockState
|
||||||
|
- 增加局部重生成类型
|
||||||
|
|
||||||
|
3. 再改结果工作台结构
|
||||||
|
- 默认高杠杆
|
||||||
|
- 高级模式收纳低杠杆字段
|
||||||
|
- 移动端拆分长表单
|
||||||
|
|
||||||
|
4. 最后做后端收口与去模板化
|
||||||
|
- 服务端领域化
|
||||||
|
- 设定层自有化
|
||||||
|
- 跨题材泛化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 一句话判断
|
||||||
|
|
||||||
|
当前自定义世界创作工具最需要的,不是再继续补更多字段或更多生成步骤,而是:
|
||||||
|
|
||||||
|
**把“创作者先决定灵魂锚点,系统再稳定展开世界”这条主逻辑真正落到 UI、流程和后端边界上。**
|
||||||
621
docs/audits/CUSTOM_WORLD_PROFILE_MAPPING_AUDIT_2026-04-18.md
Normal file
621
docs/audits/CUSTOM_WORLD_PROFILE_MAPPING_AUDIT_2026-04-18.md
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
# 世界 Profile 到预设内容与实时生成规则映射审计
|
||||||
|
|
||||||
|
更新时间:`2026-04-18`
|
||||||
|
|
||||||
|
## 0. 审计目标
|
||||||
|
|
||||||
|
本次审计只回答一个问题:
|
||||||
|
|
||||||
|
**当前仓库里的世界 profile 设定,是否已经完整、合理地映射到游戏的预设内容与实时生成内容规则中。**
|
||||||
|
|
||||||
|
这里的“世界 profile”包含两层:
|
||||||
|
|
||||||
|
1. `CustomWorldProfile` 顶层世界数据
|
||||||
|
2. `ownedSettingLayers` 派生设定层
|
||||||
|
|
||||||
|
这里的“预设内容”包含:
|
||||||
|
|
||||||
|
1. 角色运行时预设
|
||||||
|
2. 场景预设
|
||||||
|
3. 默认视觉与怪物匹配
|
||||||
|
4. 初始装备 / 初始背包 / 经济与术语表现
|
||||||
|
|
||||||
|
这里的“实时生成规则”包含:
|
||||||
|
|
||||||
|
1. 主剧情 prompt
|
||||||
|
2. NPC 对话 / 招募 / 私聊 prompt
|
||||||
|
3. 任务生成
|
||||||
|
4. 运行时物品生成
|
||||||
|
5. 故事线程、可见性、叙事 QA 与推进规则
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 结论先行
|
||||||
|
|
||||||
|
结论不是“完全映射”,而是:
|
||||||
|
|
||||||
|
**已完成基础映射,但没有达到“完全且合理”的程度。**
|
||||||
|
|
||||||
|
当前状态更准确地说是:
|
||||||
|
|
||||||
|
1. `世界基础骨架 -> 角色 / 场景 / 属性 / prompt` 这条主链已经打通。
|
||||||
|
2. `叙事层 -> 主剧情/NPC 可见性规则` 已经有比较扎实的接入。
|
||||||
|
3. `规则层 -> UI术语 / 经济 / 属性` 已经接入。
|
||||||
|
4. 但 `模板兼容层` 仍然过强,跨题材世界会被粗暴压回 `WUXIA/XIANXIA`。
|
||||||
|
5. 但 `后端运行时任务/物品模块` 只拿到了瘦身版 profile,没有真正吃到完整世界叙事层。
|
||||||
|
6. 但 `世界级 items / faction / conflict` 仍然更多是文本种子,而不是可操作的游戏内容对象。
|
||||||
|
|
||||||
|
如果按结果判断:
|
||||||
|
|
||||||
|
1. **预设内容映射:部分完整,约 70%。**
|
||||||
|
2. **实时生成规则映射:前端剧情主链较完整,后端运行时子链不完整,整体约 60%。**
|
||||||
|
3. **跨题材合理性:明显不足。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本次审计覆盖的核心文件
|
||||||
|
|
||||||
|
类型与编译链:
|
||||||
|
|
||||||
|
- `src/types/customWorld.ts`
|
||||||
|
- `src/services/customWorld.ts`
|
||||||
|
- `src/services/customWorldBuilder.ts`
|
||||||
|
- `src/services/customWorldOwnedSettingLayers.ts`
|
||||||
|
- `src/services/customWorldTheme.ts`
|
||||||
|
|
||||||
|
预设内容落地:
|
||||||
|
|
||||||
|
- `src/data/characterPresets.ts`
|
||||||
|
- `src/data/scenePresets.ts`
|
||||||
|
- `src/data/customWorldCharacterLoadout.ts`
|
||||||
|
- `src/data/customWorldRuntime.ts`
|
||||||
|
- `src/data/customWorldVisuals.ts`
|
||||||
|
- `src/data/customWorldNpcMonsters.ts`
|
||||||
|
- `src/data/worldAttributeSchemas.ts`
|
||||||
|
- `src/data/economy.ts`
|
||||||
|
- `src/services/customWorldPresentation.ts`
|
||||||
|
|
||||||
|
实时生成规则:
|
||||||
|
|
||||||
|
- `src/hooks/story/storyContextBuilder.ts`
|
||||||
|
- `src/services/prompt.ts`
|
||||||
|
- `src/services/characterChatPrompt.ts`
|
||||||
|
- `src/services/questPrompt.ts`
|
||||||
|
- `src/services/questDirector.ts`
|
||||||
|
- `src/services/runtimeItemAiPrompt.ts`
|
||||||
|
- `src/data/runtimeItemNarrative.ts`
|
||||||
|
- `src/services/storyEngine/themePack.ts`
|
||||||
|
- `src/services/storyEngine/worldStoryGraph.ts`
|
||||||
|
- `src/services/storyEngine/actorNarrativeProfile.ts`
|
||||||
|
- `src/services/storyEngine/knowledgeGraph.ts`
|
||||||
|
- `src/services/storyEngine/threadContract.ts`
|
||||||
|
- `src/services/storyEngine/visibilityEngine.ts`
|
||||||
|
- `src/services/storyEngine/authorialConstraintPack.ts`
|
||||||
|
- `src/hooks/story/progressionActions.ts`
|
||||||
|
|
||||||
|
后端运行时链:
|
||||||
|
|
||||||
|
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
|
||||||
|
- `server-node/src/modules/runtime-item/runtimeItemModule.ts`
|
||||||
|
- `server-node/src/modules/quest/runtimeQuestModule.ts`
|
||||||
|
- `server-node/src/modules/runtime/runtimeSnapshotHydration.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 映射总表
|
||||||
|
|
||||||
|
| 设定层/字段 | 映射到预设内容 | 映射到实时生成规则 | 判断 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `name/subtitle/summary/tone/playerGoal` | 已映射到角色 opening、场景提示、视觉匹配、程序化物品关键词 | 已映射到主剧情 prompt、任务 prompt、物品 prompt、ThemePack/StoryGraph 派生 | 基本成立 |
|
||||||
|
| `templateWorldType/compatibilityTemplateWorldType` | 强影响角色模板、场景图参考池、怪物池、兼容 schema | 影响 ThemePack fallback 与部分运行时回退 | 已接入,但合理性不足 |
|
||||||
|
| `majorFactions/coreConflicts` | 主要进入 ThemePack / StoryGraph / tension state,未落成具体 faction 实体 | 影响 authorial constraints、线程图谱、任务与剧情语义 | 有映射,但偏文本种子 |
|
||||||
|
| `camp` | 已映射为开局 camp scene、camp 图、camp 连接 | 通过世界参考文本和开局内容进入 prompt | 成立 |
|
||||||
|
| `attributeSchema` | 已映射到角色/NPC 属性、战斗面板、属性展示 | 已映射到 prompt 属性描述与运行时计算 | 成立 |
|
||||||
|
| `ownedSettingLayers.ruleProfile.resourceLabels` | 已映射到 UI 血量/法力/货币等术语 | 主要通过 UI/经济层体现,prompt 侧间接使用 | 成立 |
|
||||||
|
| `ownedSettingLayers.ruleProfile.economyProfile.initialCurrency` | 已映射到初始货币与快照恢复 | 对运行时奖励规则影响弱,更多是初始化 | 成立但范围有限 |
|
||||||
|
| `playableNpcs` | 已映射到可玩角色预设、技能变体、初始物品、home scene | 已映射到剧情 prompt、私聊 prompt、叙事档案 | 成立 |
|
||||||
|
| `storyNpcs` | 已映射到场景 NPC、怪物判定、角色运行时预设 | 已映射到遭遇 prompt、任务发布者、叙事可见性 | 成立 |
|
||||||
|
| `landmarks` | 已映射到 scene presets、连接网络、场景视觉、treasure hints | 已映射到世界参考文本、scene residues、故事线程关联 | 成立 |
|
||||||
|
| `items` | 生成主链默认清空,世界级 item 几乎未形成正式内容层 | `knowledgeFacts` 可支持 item,但主链无内容可用 | 映射明显不足 |
|
||||||
|
| `themePack/expressionProfile` | 已映射到视觉/命名/技能名/场景语义 | 已映射到 prompt 基调、reveal 风格、故事图谱 | 成立 |
|
||||||
|
| `referenceProfile.roleArchetypes` | 已映射到角色模板骨架选择 | 运行时规则直接消费较少 | 部分成立 |
|
||||||
|
| `referenceProfile.sceneBuckets` | 已映射到场景默认图匹配 | 运行时 prompt 直接消费较少 | 部分成立 |
|
||||||
|
| `referenceProfile.creatureArchetypes` | 已映射到怪物 preset 池筛选 | 运行时规则间接消费 | 部分成立 |
|
||||||
|
| `storyGraph` | 预设层主要影响 narrative residues 与 faction tension | 已映射到 active threads、constraints、visibility、chapter/campaign | 成立 |
|
||||||
|
| `narrativeProfile` | 已映射到 scene NPC 简介和遭遇资料 | 已映射到 prompt 可见性、任务/物品关系生成 | 成立 |
|
||||||
|
| `knowledgeFacts` | 不直接生成预设内容 | 已映射到 visibility slice 与 prompt 裁剪 | 成立 |
|
||||||
|
| `threadContracts` | 不直接生成预设内容 | 已映射到 story signal / thread update / QA | 成立 |
|
||||||
|
| `creatorIntent/anchorPack/lockState/anchorContent` | 主要留在创作工作区与结果页整理 | 几乎不直接进入正式游戏运行时 | 创作层有用,运行时映射弱 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 已经成立的映射链
|
||||||
|
|
||||||
|
## 4.1 世界基础骨架已经能稳定进入角色、场景与剧情主链
|
||||||
|
|
||||||
|
`CustomWorldProfile` 的基础字段已经不是“只存档不消费”的状态。
|
||||||
|
|
||||||
|
它们已经实际进入:
|
||||||
|
|
||||||
|
1. 角色开局文案与 opening 动机
|
||||||
|
2. 角色技能变体
|
||||||
|
3. 场景预设名称、描述、连接、treasure hints
|
||||||
|
4. 主剧情 prompt 中的世界补充档案
|
||||||
|
5. 私聊 / 任务 / 运行时物品 prompt 的世界摘要
|
||||||
|
|
||||||
|
这说明:
|
||||||
|
|
||||||
|
**世界 profile 的基础文本层已经真正进入游戏主链。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 规则层已经落到真实游戏表现
|
||||||
|
|
||||||
|
`ownedSettingLayers.ruleProfile` 目前已真实影响:
|
||||||
|
|
||||||
|
1. `attributeSchema`
|
||||||
|
- 角色/NPC 属性计算
|
||||||
|
- prompt 中的属性描述
|
||||||
|
- 面板展示
|
||||||
|
2. `resourceLabels`
|
||||||
|
- HP/MP/伤害/冷却/货币等 UI 术语
|
||||||
|
3. `economyProfile.initialCurrency`
|
||||||
|
- 自定义世界初始货币
|
||||||
|
- 快照恢复时的默认初始化
|
||||||
|
|
||||||
|
这部分不是空壳。
|
||||||
|
|
||||||
|
**规则层已经从 profile 进入真实结算和 UI。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 叙事层已经进入 prompt 可见性与推进规则
|
||||||
|
|
||||||
|
`themePack -> storyGraph -> narrativeProfile -> knowledgeFacts -> visibilitySlice`
|
||||||
|
这条链已经是当前自定义世界最完整的一条映射链。
|
||||||
|
|
||||||
|
它已经支撑:
|
||||||
|
|
||||||
|
1. NPC 首遇/低披露 prompt 裁剪
|
||||||
|
2. 当前线程可见性控制
|
||||||
|
3. 当前压力、错位、禁区、已解锁章节等信息分层
|
||||||
|
4. 章节/战役/约束/QA 的继续推进
|
||||||
|
|
||||||
|
这说明:
|
||||||
|
|
||||||
|
**世界 profile 里的叙事层不只是展示文本,而是真的在控制“模型这轮能知道什么、不能知道什么”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键问题
|
||||||
|
|
||||||
|
## 5.1 高优先级问题:模板兼容层仍然是二元锚点,跨题材世界会被错误压缩
|
||||||
|
|
||||||
|
当前主生成链仍要求模型输出:
|
||||||
|
|
||||||
|
- `templateWorldType: WUXIA | XIANXIA`
|
||||||
|
|
||||||
|
而兼容解析也会把世界最终压回:
|
||||||
|
|
||||||
|
1. `arcane -> XIANXIA`
|
||||||
|
2. 其它几乎全部回到 `WUXIA`
|
||||||
|
|
||||||
|
这会直接影响:
|
||||||
|
|
||||||
|
1. 角色模板骨架选择
|
||||||
|
2. 场景默认图参考池
|
||||||
|
3. 怪物 preset 池
|
||||||
|
4. 兼容性 fallback
|
||||||
|
|
||||||
|
问题不在“有兼容字段”,而在于:
|
||||||
|
|
||||||
|
**当前兼容字段仍然过度参与真实内容映射。**
|
||||||
|
|
||||||
|
对现代金融、科幻 AI 战争、校园、都市、调查等题材来说,这种二元压缩并不合理。
|
||||||
|
|
||||||
|
仓库日志里已经出现了典型样本:
|
||||||
|
|
||||||
|
1. 股市世界被要求产出 `XIANXIA`
|
||||||
|
2. AI 战争世界也被要求产出 `XIANXIA`
|
||||||
|
3. 魔法科技融合世界被要求产出 `WUXIA`
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
|
||||||
|
**世界 profile 虽然支持跨题材文本输入,但底层预设内容映射仍带着明显的“武侠/仙侠残余偏置”。**
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
|
||||||
|
- `src/services/customWorldTheme.ts`
|
||||||
|
- `src/data/customWorldVisuals.ts`
|
||||||
|
- `src/data/customWorldNpcMonsters.ts`
|
||||||
|
- `src/data/characterPresets.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**这是当前“合理映射”最大缺口。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.2 高优先级问题:后端运行时任务/物品模块只消费了瘦身版 world profile
|
||||||
|
|
||||||
|
前端剧情主链里,`customWorldProfile` 会带着:
|
||||||
|
|
||||||
|
1. `themePack`
|
||||||
|
2. `storyGraph`
|
||||||
|
3. `knowledgeFacts`
|
||||||
|
4. `threadContracts`
|
||||||
|
5. `ownedSettingLayers`
|
||||||
|
|
||||||
|
但后端运行时模块里:
|
||||||
|
|
||||||
|
1. `runtimeItemModule` 的 `customWorldProfile` 只有 `{ name, summary }`
|
||||||
|
2. `runtimeQuestModule` 的 `customWorldProfile` 也只有 `{ name, summary }`
|
||||||
|
|
||||||
|
这直接导致后端运行时生成无法真正读取:
|
||||||
|
|
||||||
|
1. 世界线程图谱
|
||||||
|
2. 世界可见性事实
|
||||||
|
3. 参考原型层
|
||||||
|
4. 规则层
|
||||||
|
5. 表达层
|
||||||
|
|
||||||
|
结果是:
|
||||||
|
|
||||||
|
1. 主剧情/NPC prompt 已经较强依赖世界叙事层
|
||||||
|
2. 但后端任务/物品生成还只是吃世界摘要
|
||||||
|
|
||||||
|
这会把系统拆成两种强度不同的世界消费链:
|
||||||
|
|
||||||
|
1. 前端剧情链较“懂世界”
|
||||||
|
2. 后端运行时奖励链较“不懂世界”
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `src/hooks/story/storyContextBuilder.ts`
|
||||||
|
- `src/services/runtimeItemAiPrompt.ts`
|
||||||
|
- `src/services/questPrompt.ts`
|
||||||
|
- `server-node/src/modules/runtime-item/runtimeItemModule.ts`
|
||||||
|
- `server-node/src/modules/quest/runtimeQuestModule.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**世界 profile 到实时生成规则的映射,在后端链路上是不完整的。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.3 高优先级问题:世界级 `items` 没有真正接进主生成链
|
||||||
|
|
||||||
|
类型里存在:
|
||||||
|
|
||||||
|
- `CustomWorldProfile.items`
|
||||||
|
|
||||||
|
构建器也支持:
|
||||||
|
|
||||||
|
1. item 归一化
|
||||||
|
2. `attributeResonance`
|
||||||
|
3. item knowledge facts
|
||||||
|
|
||||||
|
但真正的世界生成主链里:
|
||||||
|
|
||||||
|
1. orchestrator prompt 明确要求不要预生成物品档案
|
||||||
|
2. `attachRuntimeGenerationMetadata(...)` 会把 `items` 直接压成空数组
|
||||||
|
|
||||||
|
这会带来两个结果:
|
||||||
|
|
||||||
|
1. 世界 profile 的“世界级物品层”几乎为空
|
||||||
|
2. 运行时背包、掉落、交易更多依赖程序化生成和角色初始物品
|
||||||
|
|
||||||
|
于是目前的物品系统更像:
|
||||||
|
|
||||||
|
1. 有角色初始物品
|
||||||
|
2. 有运行时程序化物品
|
||||||
|
3. 但没有稳定的“世界物品图谱”
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
|
||||||
|
**世界 profile 在物品层没有形成完整映射。**
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
|
||||||
|
- `src/services/customWorldBuilder.ts`
|
||||||
|
- `src/data/customWorldRuntime.ts`
|
||||||
|
- `src/data/customWorldCharacterLoadout.ts`
|
||||||
|
- `src/services/storyEngine/knowledgeGraph.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**这是“预设内容”和“实时生成规则”共同缺失的一块。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.4 中优先级问题:`majorFactions/coreConflicts` 仍然是文本种子,不是可操作游戏对象
|
||||||
|
|
||||||
|
当前 `majorFactions` 与 `coreConflicts` 已经被大量消费,但主要消费方式是:
|
||||||
|
|
||||||
|
1. 拼进 `ThemePack`
|
||||||
|
2. 派生 `WorldStoryGraph`
|
||||||
|
3. 派生 `AuthorialConstraintPack`
|
||||||
|
4. 派生 `FactionTensionState`
|
||||||
|
|
||||||
|
问题在于它们还没有形成:
|
||||||
|
|
||||||
|
1. 可索引 faction 实体
|
||||||
|
2. faction 与 NPC 的显式归属关系
|
||||||
|
3. faction 与场景/商店/敌对阵营的显式绑定
|
||||||
|
4. 冲突与任务/势力状态的强约束关系
|
||||||
|
|
||||||
|
当前更多是:
|
||||||
|
|
||||||
|
**“文本里提到过 -> 图谱做字符串匹配 -> 运行时拿去写 prompt”**
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
**“世界里真的存在这些派系与冲突对象,并驱动交互规则”**
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `src/services/storyEngine/themePack.ts`
|
||||||
|
- `src/services/storyEngine/worldStoryGraph.ts`
|
||||||
|
- `src/services/storyEngine/factionTensionState.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**已有映射,但离“完全落地”为具体游戏内容对象还有明显距离。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.5 中优先级问题:场景预设会额外注入模板怪物,弱化 landmark 的原始设定控制力
|
||||||
|
|
||||||
|
`buildCustomScenePresets(profile)` 在每个 landmark scene 中,除了把 `landmark.sceneNpcIds` 指向的角色放进去,还会:
|
||||||
|
|
||||||
|
1. 从怪物 preset 池按 scene index 截两只怪
|
||||||
|
2. 直接拼进 `combinedNpcs`
|
||||||
|
|
||||||
|
这意味着即使 profile 本身没有明确要求某个 landmark 出现这些敌对实体,运行时场景仍会被额外补入模板怪物。
|
||||||
|
|
||||||
|
这会导致:
|
||||||
|
|
||||||
|
1. 场景内容不完全由 `landmark + storyNpcs` 决定
|
||||||
|
2. 地标设定与实际可战斗内容之间存在偏移
|
||||||
|
3. 跨题材世界会更容易被模板怪物池拖偏
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `src/data/scenePresets.ts`
|
||||||
|
- `src/data/customWorldNpcMonsters.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**landmark 到实际场景实体池的映射,不是完全忠实映射,而是“设定 + 模板补丁”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.6 中优先级问题:后端运行时物品线程并不是真正世界线程
|
||||||
|
|
||||||
|
前端剧情链里的 `activeThreadIds` 来自:
|
||||||
|
|
||||||
|
1. `storyEngineMemory`
|
||||||
|
2. `storyGraph`
|
||||||
|
3. `knowledgeFacts`
|
||||||
|
4. `visibilitySlice`
|
||||||
|
|
||||||
|
但后端 `runtimeItemModule` 的 loose context 里,`activeThreadIds` 只是:
|
||||||
|
|
||||||
|
1. `thread:${encounter.id}`
|
||||||
|
2. 或 `thread:${scene.id}`
|
||||||
|
|
||||||
|
这不是世界线程图谱,而是临时合成 id。
|
||||||
|
|
||||||
|
结果是:
|
||||||
|
|
||||||
|
1. 名义上后端物品模块也有“active threads”
|
||||||
|
2. 实际上它拿到的并不是 `WorldStoryGraph` 中的真实线程
|
||||||
|
|
||||||
|
这会让运行时物品的“为什么现在出现”更像局部上下文推断,而不是来自世界故事结构。
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `server-node/src/modules/runtime-item/runtimeItemModule.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**这是实时生成规则层的结构性弱映射。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.7 中优先级问题:`referenceProfile.roleArchetypes` 只从 playableNpcs 派生,storyNpcs 覆盖不够
|
||||||
|
|
||||||
|
当前 `roleArchetypes` 的编译来源是:
|
||||||
|
|
||||||
|
1. `profile.playableNpcs.slice(0, 6)`
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
1. `playableNpcs + storyNpcs` 的综合原型池
|
||||||
|
|
||||||
|
这导致两个问题:
|
||||||
|
|
||||||
|
1. 世界里的长尾 story NPC 原型没有进入 reference archetype 编译
|
||||||
|
2. 某些场景角色/怪物/平民的模板骨架选择更多依赖启发式 fallback
|
||||||
|
|
||||||
|
这会让:
|
||||||
|
|
||||||
|
1. 可玩角色映射较稳定
|
||||||
|
2. 长尾场景角色映射不够稳定
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `src/services/customWorldOwnedSettingLayers.ts`
|
||||||
|
- `src/services/customWorldReferenceSignals.ts`
|
||||||
|
- `src/data/characterPresets.ts`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
**参考层映射存在明显“主角优先、长尾不足”的偏差。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.8 低优先级问题:创作元数据并未进入正式游戏运行时
|
||||||
|
|
||||||
|
`creatorIntent / anchorPack / lockState / anchorContent` 当前主要服务于:
|
||||||
|
|
||||||
|
1. 创作工作区
|
||||||
|
2. Agent session
|
||||||
|
3. 结果页和编辑器
|
||||||
|
|
||||||
|
它们对正式运行时的直接作用主要是:
|
||||||
|
|
||||||
|
1. 参与 `ownedSettingLayers` 的编译
|
||||||
|
|
||||||
|
但不会直接变成:
|
||||||
|
|
||||||
|
1. 正式战斗规则
|
||||||
|
2. 场景交互规则
|
||||||
|
3. faction 状态
|
||||||
|
4. 任务目标约束
|
||||||
|
|
||||||
|
这不一定是 bug,但如果把“世界 profile 设定”理解为所有 profile 元数据,那么:
|
||||||
|
|
||||||
|
**创作层数据目前并没有完整进入游戏运行时。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 分层判断
|
||||||
|
|
||||||
|
## 6.1 预设内容映射判断
|
||||||
|
|
||||||
|
### 已经合理接入的部分
|
||||||
|
|
||||||
|
1. 可玩角色
|
||||||
|
2. 场景角色
|
||||||
|
3. 地标场景
|
||||||
|
4. 属性 schema
|
||||||
|
5. 资源术语
|
||||||
|
6. 初始货币
|
||||||
|
7. camp 开局归处
|
||||||
|
8. 默认场景图匹配
|
||||||
|
|
||||||
|
### 仍然不足的部分
|
||||||
|
|
||||||
|
1. 世界级 items
|
||||||
|
2. faction 实体化
|
||||||
|
3. 冲突到任务/场景状态的强绑定
|
||||||
|
4. 跨题材世界的模板偏置问题
|
||||||
|
5. 地标与怪物注入之间的忠实性
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
**预设内容层是“能跑且已有骨架”,但还不是“设定完全落地”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.2 实时生成规则映射判断
|
||||||
|
|
||||||
|
### 已经合理接入的部分
|
||||||
|
|
||||||
|
1. 主剧情 prompt
|
||||||
|
2. NPC 可见性控制
|
||||||
|
3. 私聊与对话 prompt
|
||||||
|
4. 叙事线程图谱
|
||||||
|
5. 事实图谱
|
||||||
|
6. 作者性约束与 QA
|
||||||
|
|
||||||
|
### 仍然不足的部分
|
||||||
|
|
||||||
|
1. 后端任务模块 world profile 过瘦
|
||||||
|
2. 后端物品模块 world profile 过瘦
|
||||||
|
3. 后端物品线程是伪线程
|
||||||
|
4. 世界级 item 图谱为空
|
||||||
|
5. faction/conflict 仍偏语义层,不够规则化
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
**实时生成规则层呈现出“前端剧情链强、后端奖励链弱”的不均衡状态。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 最终判定
|
||||||
|
|
||||||
|
如果问题是:
|
||||||
|
|
||||||
|
**“世界 profile 设定是否已经完全地、合理地映射到游戏的预设内容、实时生成内容规则中?”**
|
||||||
|
|
||||||
|
我的结论是:
|
||||||
|
|
||||||
|
**没有。**
|
||||||
|
|
||||||
|
更准确地说:
|
||||||
|
|
||||||
|
1. 已经完成了主干映射。
|
||||||
|
2. 但还没有完成全量映射。
|
||||||
|
3. 也还没有完成跨题材下的合理映射。
|
||||||
|
|
||||||
|
当前系统最准确的状态是:
|
||||||
|
|
||||||
|
**世界 profile 已经成为真实驱动源之一,但还没有成为所有预设内容与实时规则的一致单一真相源。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 建议的修复优先级
|
||||||
|
|
||||||
|
## P1:先补“真实消费不完整”的链路
|
||||||
|
|
||||||
|
1. 让后端 `runtimeItemModule` / `runtimeQuestModule` 接收完整 `customWorldProfile` 子集
|
||||||
|
2. 至少补进:
|
||||||
|
- `ownedSettingLayers`
|
||||||
|
- `storyGraph`
|
||||||
|
- `knowledgeFacts`
|
||||||
|
- `themePack`
|
||||||
|
- `majorFactions/coreConflicts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1:把 `templateWorldType` 退回兼容字段,而不是主导字段
|
||||||
|
|
||||||
|
1. 生成期保留兼容输出
|
||||||
|
2. 运行时优先读取:
|
||||||
|
- `ownedSettingLayers`
|
||||||
|
- `themeMode`
|
||||||
|
- `referenceProfile`
|
||||||
|
3. 不再让 `WUXIA/XIANXIA` 主导现代/科幻/海洋/裂界世界的视觉与怪物选择
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1:补世界级 item 层
|
||||||
|
|
||||||
|
1. 允许世界生成阶段产出一批世界级 items seed
|
||||||
|
2. 让 `knowledgeFacts / runtime item / quest reward / treasure hint` 能挂到这些 seed 上
|
||||||
|
3. 形成“世界物件图谱”,而不是只有角色初始物品和程序化临时物品
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2:把 faction/conflict 从文本种子升级成结构对象
|
||||||
|
|
||||||
|
1. faction 实体
|
||||||
|
2. faction -> NPC 归属
|
||||||
|
3. faction -> 场景控制
|
||||||
|
4. conflict -> 任务/线程/场景压力绑定
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2:去掉地标场景里的固定模板怪物补丁式注入
|
||||||
|
|
||||||
|
1. 优先使用 landmark 自己的敌对角色设计
|
||||||
|
2. 模板怪物只作为缺口补位
|
||||||
|
3. 补位也要受 landmark/theme/thread 约束
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3:扩充 reference archetype 的来源
|
||||||
|
|
||||||
|
1. role archetypes 不只从 playableNpcs 编
|
||||||
|
2. storyNpcs 也应参与 archetype 归纳
|
||||||
|
3. 为平民、敌对、怪物、势力成员建立更细 archetype
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 一句话总评
|
||||||
|
|
||||||
|
**当前世界 profile 已经能驱动游戏,但还没有彻底收束成“所有预设内容与实时生成规则都优先读取它”的单一真相源。主链可用,边链仍散,跨题材合理性仍偏弱。**
|
||||||
437
docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md
Normal file
437
docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 当前 Function 设计审计(2026-04-03)
|
||||||
|
|
||||||
|
## 审计范围
|
||||||
|
|
||||||
|
本次审计重点阅读并对照了这些位置:
|
||||||
|
|
||||||
|
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
|
||||||
|
- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
|
||||||
|
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
|
||||||
|
- `docs/audits/engineering/README.md`
|
||||||
|
- `src/data/stateFunctions.ts`
|
||||||
|
- `src/data/npcInteractions.ts`
|
||||||
|
- `src/data/treasureInteractions.ts`
|
||||||
|
- `src/hooks/useStoryGeneration.ts`
|
||||||
|
- `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
- `src/hooks/story/npcInteraction.ts`
|
||||||
|
- `src/hooks/story/progressionActions.ts`
|
||||||
|
- `src/hooks/story/storyGenerationState.ts`
|
||||||
|
- `src/services/ai.ts`
|
||||||
|
- `src/services/prompt.ts`
|
||||||
|
- `src/components/AdventurePanel.tsx`
|
||||||
|
- `src/components/StateFunctionEditor.tsx`
|
||||||
|
|
||||||
|
## 先说结论
|
||||||
|
|
||||||
|
当前运行时的“function”不是单一体系,而是至少分成了 4 层:
|
||||||
|
|
||||||
|
1. `stateFunctions.ts` 里的基础战斗 / 空闲 function。
|
||||||
|
2. `npcInteractions.ts` 里的 NPC 交互 function。
|
||||||
|
3. `treasureInteractions.ts` 里的宝藏交互 function。
|
||||||
|
4. `useStoryGeneration.ts` / `npcInteraction.ts` / 背包装备锻造里额外加出来的“流程控制型 functionId”。
|
||||||
|
|
||||||
|
“选完选项触发 function 后没有触发剧情推理”这类现象,当前主要有 3 种来源:
|
||||||
|
|
||||||
|
1. **按设计先走本地分流,不会在第一次点击时立刻推理。**
|
||||||
|
2. **剧情推理其实已经完成,但被“继续冒险”这道 UI 中转门挡住了。**
|
||||||
|
3. **真正的可见性 bug:`story_continue_adventure` 的文案常量已经乱码,导致 UI 提示失效,用户很容易误判为没继续推理。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 当前 function 体系分层
|
||||||
|
|
||||||
|
### 1.1 基础状态 function:`src/data/stateFunctions.ts`
|
||||||
|
|
||||||
|
这一层才是严格意义上的“状态 function 注册表”,由 `resolveFunctionOption` 统一解析。
|
||||||
|
|
||||||
|
当前运行时实际启用 12 个:
|
||||||
|
|
||||||
|
- 战斗类 7 个:
|
||||||
|
- `battle_all_in_crush`
|
||||||
|
- `battle_guard_break`
|
||||||
|
- `battle_probe_pressure`
|
||||||
|
- `battle_feint_step`
|
||||||
|
- `battle_recover_breath`
|
||||||
|
- `battle_finisher_window`
|
||||||
|
- `battle_escape_breakout`
|
||||||
|
- 空闲类 5 个:
|
||||||
|
- `idle_explore_forward`
|
||||||
|
- `idle_travel_next_scene`
|
||||||
|
- `idle_rest_focus`
|
||||||
|
- `idle_observe_signs`
|
||||||
|
- `idle_call_out`
|
||||||
|
|
||||||
|
关键设计点:
|
||||||
|
|
||||||
|
- 定义源头是 `BATTLE_FUNCTIONS` / `IDLE_FUNCTIONS`。
|
||||||
|
- 最终运行时集合由 `buildStateFunctionDefinitions` 产出。
|
||||||
|
- 可执行过滤走 `getExecutableFunctions`。
|
||||||
|
- 文案和视觉包装走 `resolveFunctionOption`。
|
||||||
|
- 默认选项池走 `getDefaultFunctionIdsForContext`。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- `idle_follow_clue` 仍然留在源码和 prompt 描述里,但在 `applyRuntimeFunctionAdjustments` 中被直接过滤掉,不会进入运行时 function 集合。
|
||||||
|
- `src/components/game-shell/useSceneTransitionModel.ts` 里仍然保留了 `idle_follow_clue` 的转场映射,这说明当前 function 清单并不完全一致。
|
||||||
|
|
||||||
|
### 1.2 NPC 交互 function:`src/data/npcInteractions.ts`
|
||||||
|
|
||||||
|
这一层不是通过 `resolveFunctionOption` 生成的,而是 `buildNpcEncounterStoryMoment` 直接拼出 `StoryOption`,并挂上 `interaction.kind = 'npc'`。
|
||||||
|
|
||||||
|
按功能类型看,当前会出现这些 `functionId`:
|
||||||
|
|
||||||
|
- `npc_preview_talk`
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_fight`
|
||||||
|
- `npc_spar`
|
||||||
|
- `npc_help`
|
||||||
|
- `npc_chat`(可重复出现 2 个以上,代表不同聊天话题)
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_recruit`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_quest_turn_in`
|
||||||
|
- `npc_leave`
|
||||||
|
|
||||||
|
关键设计点:
|
||||||
|
|
||||||
|
- 这一层的 function 是“眼前 NPC 交互目录”,不是基础状态机目录。
|
||||||
|
- 真正执行分流不在 `stateFunctions.ts`,而在 `handleNpcInteraction` / `resolveNpcInteractionDecision`。
|
||||||
|
- prompt 层通过 `availableOptions` 把这些 function 当作“固定可选项列表”交给模型,要求模型保留数量和 `functionId`。
|
||||||
|
|
||||||
|
### 1.3 宝藏交互 function:`src/data/treasureInteractions.ts`
|
||||||
|
|
||||||
|
这一层同样不是 `resolveFunctionOption` 体系,而是直接构造带 `interaction.kind = 'treasure'` 的选项。
|
||||||
|
|
||||||
|
当前有 3 个:
|
||||||
|
|
||||||
|
- `treasure_secure`
|
||||||
|
- `treasure_inspect`
|
||||||
|
- `treasure_leave`
|
||||||
|
|
||||||
|
执行时由 `useTreasureFlow.ts` 接管,最终再回到 `commitGeneratedState` 继续剧情推理。
|
||||||
|
|
||||||
|
### 1.4 流程控制 / 面板动作 functionId
|
||||||
|
|
||||||
|
这类 `functionId` 会进入 `lastFunctionId` 或 `commitGeneratedState`,但不在 `stateFunctions.ts` 注册表里:
|
||||||
|
|
||||||
|
- 流程控制:
|
||||||
|
- `story_continue_adventure`
|
||||||
|
- `camp_travel_home_scene`
|
||||||
|
- `story_opening_camp_dialogue`
|
||||||
|
- 面板动作:
|
||||||
|
- `inventory_use`
|
||||||
|
- `equipment_equip`
|
||||||
|
- `equipment_unequip`
|
||||||
|
- `forge_craft`
|
||||||
|
- `forge_dismantle`
|
||||||
|
- `forge_reforge`
|
||||||
|
|
||||||
|
这说明当前项目里“functionId”已经同时承担了 3 个角色:
|
||||||
|
|
||||||
|
- 运行时基础状态动作 ID
|
||||||
|
- NPC / 宝藏交互动作 ID
|
||||||
|
- 流程 / 面板事件 ID
|
||||||
|
|
||||||
|
这能跑,但可追踪性已经开始分裂。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前剧情推理触发链路
|
||||||
|
|
||||||
|
### 2.1 会立刻触发剧情推理的主链
|
||||||
|
|
||||||
|
这类点击后会直接走 AI 续推:
|
||||||
|
|
||||||
|
- 普通战斗 / 空闲 function
|
||||||
|
- `handleChoice`
|
||||||
|
- `buildResolvedChoiceState`
|
||||||
|
- `playResolvedChoice`
|
||||||
|
- `generateNextStep`
|
||||||
|
- 这些 NPC 交互
|
||||||
|
- `npc_preview_talk`
|
||||||
|
- `npc_help`
|
||||||
|
- `npc_chat`
|
||||||
|
- `npc_fight`
|
||||||
|
- `npc_spar`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_quest_turn_in`
|
||||||
|
- `npc_leave`
|
||||||
|
- 宝藏交互确认后
|
||||||
|
- `treasure_secure`
|
||||||
|
- `treasure_inspect`
|
||||||
|
- `treasure_leave`
|
||||||
|
- 面板动作确认后
|
||||||
|
- `inventory_use`
|
||||||
|
- `equipment_equip`
|
||||||
|
- `equipment_unequip`
|
||||||
|
- `forge_*`
|
||||||
|
|
||||||
|
共性是:
|
||||||
|
|
||||||
|
- 要么直接在 `handleChoice` 里调用 `generateNextStep`。
|
||||||
|
- 要么先走 `commitGeneratedState` / `commitGeneratedStateWithEncounterEntry`,再统一调用 `generateStoryForState`。
|
||||||
|
|
||||||
|
### 2.2 第一次点击不会立刻触发剧情推理的分流
|
||||||
|
|
||||||
|
这类最容易被误判成“function 触发了,但剧情没继续”:
|
||||||
|
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_recruit`(队伍满时必进 modal;队伍未满时会先进招募对话流)
|
||||||
|
|
||||||
|
原因不是漏调,而是当前设计明确先走:
|
||||||
|
|
||||||
|
- `resolveNpcInteractionDecision`
|
||||||
|
- `trade_modal`
|
||||||
|
- `gift_modal`
|
||||||
|
- `recruit_modal`
|
||||||
|
- `recruit_immediate`
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 第一次点击只是**打开模态框 / 进入招募流程**。
|
||||||
|
- 真正推进剧情推理,要等到:
|
||||||
|
- `confirmTrade`
|
||||||
|
- `confirmGift`
|
||||||
|
- `confirmRecruit`
|
||||||
|
- `executeRecruitment`
|
||||||
|
|
||||||
|
如果产品预期是“用户每点一次选项就必须立即看到剧情继续”,这一层现在不满足。
|
||||||
|
|
||||||
|
### 2.3 `npc_chat` 是“先推理,再延迟展示选项”
|
||||||
|
|
||||||
|
`npc_chat` 不是普通的“生成下一幕 + 立刻给选项”,而是单独走这条链:
|
||||||
|
|
||||||
|
1. `commitNpcChatState` 先流式生成聊天正文。
|
||||||
|
2. 聊天结束后,再调用一次 `generateNextStep`。
|
||||||
|
3. 新一轮冒险选项不直接显示,而是塞进 `deferredOptions`。
|
||||||
|
4. 当前界面只先显示一个 `story_continue_adventure`。
|
||||||
|
5. 用户再点一次,才把 `deferredOptions` 放出来。
|
||||||
|
|
||||||
|
所以这里常见的误判是:
|
||||||
|
|
||||||
|
- **剧情推理其实已经做完了。**
|
||||||
|
- 只是 UI 先让用户“继续冒险”一次,才把新选项展示出来。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 本次排查发现的重点问题
|
||||||
|
|
||||||
|
### 3.1 真正最像“没触发剧情推理”的地方:模态框型 NPC function
|
||||||
|
|
||||||
|
定位:
|
||||||
|
|
||||||
|
- `src/hooks/story/npcEncounterActions.ts:427-449`
|
||||||
|
- `src/hooks/story/storyGenerationState.ts:41-80`
|
||||||
|
- `src/hooks/story/npcInteraction.ts:427-585`
|
||||||
|
|
||||||
|
现象:
|
||||||
|
|
||||||
|
- 点击 `npc_trade` / `npc_gift` / `npc_recruit` 后,故事文本区通常不会立刻变化。
|
||||||
|
- 当前故事也不会马上追加一段新的 `storyText`。
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些 function 的第一次点击只做本地 UI 分流。
|
||||||
|
- 直到用户在 modal 里确认,才会真正调用 `commitGeneratedState` 继续推理。
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- **这不是单纯 bug,而是当前设计本身如此。**
|
||||||
|
- 但如果玩家从“选项即剧情推进”的心智出发,会非常像“点了没反应”。
|
||||||
|
|
||||||
|
建议优先级:高。
|
||||||
|
|
||||||
|
### 3.2 最关键的实际 bug:`story_continue_adventure` 文案乱码,导致“已推理但未显式提示”
|
||||||
|
|
||||||
|
定位:
|
||||||
|
|
||||||
|
- `src/hooks/useStoryGeneration.ts:92-96`
|
||||||
|
- `src/hooks/story/npcEncounterActions.ts:281-400`
|
||||||
|
- `src/components/AdventurePanel.tsx:534`
|
||||||
|
- `src/components/AdventurePanel.tsx:860`
|
||||||
|
- `src/components/AdventurePanel.tsx:879`
|
||||||
|
|
||||||
|
现象:
|
||||||
|
|
||||||
|
- `npc_chat` 完成后,系统本应展示一个“继续冒险”按钮,提示“剧情推理完成,继续后显示新的冒险选项”。
|
||||||
|
- 但当前 `CONTINUE_ADVENTURE_ACTION_TEXT` 常量已经写成了乱码:`缁х画鍐掗櫓`。
|
||||||
|
- `AdventurePanel` 又是靠 `option.actionText === '继续冒险'` 来识别这个特殊按钮。
|
||||||
|
|
||||||
|
直接后果:
|
||||||
|
|
||||||
|
- 特殊提示 UI 不会出现。
|
||||||
|
- 玩家只会看到一个普通且乱码的单选项。
|
||||||
|
- 实际上 `deferredOptions` 已经算好了,但用户极容易误会为“剧情没有继续推理”。
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- **这是本次排查里最明确的实现级 bug。**
|
||||||
|
- 它不会阻止底层推理发生,但会严重破坏“推理已完成”的可见性。
|
||||||
|
|
||||||
|
建议优先级:最高。
|
||||||
|
|
||||||
|
### 3.3 `StateFunctionEditor` 覆盖不到真正最容易出问题的 function
|
||||||
|
|
||||||
|
定位:
|
||||||
|
|
||||||
|
- `src/components/StateFunctionEditor.tsx:13-21`
|
||||||
|
- `src/components/StateFunctionEditor.tsx:488`
|
||||||
|
- `src/components/StateFunctionEditor.tsx:772-838`
|
||||||
|
- `src/components/StateFunctionEditor.tsx:992-1211`
|
||||||
|
|
||||||
|
现象:
|
||||||
|
|
||||||
|
- 编辑器预览只接了 `buildStateFunctionDefinitions` / `getAllStateFunctionDefinitions` / `resolveFunctionOption`。
|
||||||
|
- 也就是它只能预览 `stateFunctions.ts` 那 12 个基础 function。
|
||||||
|
|
||||||
|
覆盖不到的关键分支:
|
||||||
|
|
||||||
|
- `npc_*`
|
||||||
|
- `treasure_*`
|
||||||
|
- `npc_preview_talk`
|
||||||
|
- `story_continue_adventure`
|
||||||
|
- modal 分流
|
||||||
|
- `npc_chat` 的 `deferredOptions`
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这不是运行时 bug,但它解释了为什么这类问题很难在编辑器里提前暴露。
|
||||||
|
- 当前“Function 编辑器”并没有覆盖到最复杂、最容易让用户感知为异常的 function 链路。
|
||||||
|
|
||||||
|
建议优先级:高。
|
||||||
|
|
||||||
|
### 3.4 function 清单存在“源码有、运行时无、别处还在引用”的分裂
|
||||||
|
|
||||||
|
定位:
|
||||||
|
|
||||||
|
- `src/data/stateFunctions.ts:350`
|
||||||
|
- `src/data/stateFunctions.ts:429-441`
|
||||||
|
- `src/components/game-shell/useSceneTransitionModel.ts:19-25`
|
||||||
|
|
||||||
|
现象:
|
||||||
|
|
||||||
|
- `idle_follow_clue` 仍在定义表、提示词描述、若干 `switch` 分支中存在。
|
||||||
|
- 但最终在 `applyRuntimeFunctionAdjustments` 被过滤掉,不会进入运行时 function 集合。
|
||||||
|
- `useSceneTransitionModel` 里却还保留着它的转场模式映射。
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这不是“没触发剧情推理”的直接原因。
|
||||||
|
- 但会让维护者误判当前真实 function 清单,也会增加后续继续扩 function 时的混乱。
|
||||||
|
|
||||||
|
建议优先级:中。
|
||||||
|
|
||||||
|
### 3.5 自动化测试几乎没有覆盖“最容易让人误会没推理”的链路
|
||||||
|
|
||||||
|
定位:
|
||||||
|
|
||||||
|
- 当前已有测试主要是 `src/hooks/story/storyGenerationState.test.ts`
|
||||||
|
|
||||||
|
已覆盖:
|
||||||
|
|
||||||
|
- `trade_modal`
|
||||||
|
- `recruit_modal`
|
||||||
|
- 地图切场景
|
||||||
|
|
||||||
|
未覆盖:
|
||||||
|
|
||||||
|
- `npc_chat` 的 `deferredOptions -> story_continue_adventure -> 真正显示新选项`
|
||||||
|
- `npc_trade` / `npc_gift` / `npc_recruit` 确认后是否一定调用 `commitGeneratedState`
|
||||||
|
- `story_continue_adventure` 文案与 UI 特判是否一致
|
||||||
|
- `npc_preview_talk -> enterNpcInteraction -> generateStoryForState`
|
||||||
|
- 宝藏分支确认后是否稳定续推
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这类问题之所以能长期存在,一个核心原因就是**最关键的续推分支没有测试兜底**。
|
||||||
|
|
||||||
|
建议优先级:高。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 按“首次点击是否立即触发剧情推理”整理
|
||||||
|
|
||||||
|
### 4.1 首次点击就会继续推理
|
||||||
|
|
||||||
|
- `battle_*`
|
||||||
|
- `idle_*`
|
||||||
|
- `npc_preview_talk`
|
||||||
|
- `npc_help`
|
||||||
|
- `npc_chat`
|
||||||
|
- `npc_fight`
|
||||||
|
- `npc_spar`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_quest_turn_in`
|
||||||
|
- `npc_leave`
|
||||||
|
- `treasure_*`
|
||||||
|
- `inventory_use`
|
||||||
|
- `equipment_*`
|
||||||
|
- `forge_*`
|
||||||
|
|
||||||
|
### 4.2 首次点击不会立刻继续推理
|
||||||
|
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_recruit`(至少会先进入确认 / 对话流)
|
||||||
|
- `story_continue_adventure`
|
||||||
|
|
||||||
|
这里要特别分清:
|
||||||
|
|
||||||
|
- `npc_trade` / `npc_gift` / `npc_recruit` 是**先分流,后确认,确认后再推理**。
|
||||||
|
- `story_continue_adventure` 是**推理已经完成,只是先把结果选项延后展示**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 建议的修正顺序
|
||||||
|
|
||||||
|
### P0
|
||||||
|
|
||||||
|
- 修掉 `CONTINUE_ADVENTURE_ACTION_TEXT` 的乱码。
|
||||||
|
- `AdventurePanel` 不要再靠 `actionText === '继续冒险'` 判定特殊按钮,改成按 `functionId === 'story_continue_adventure'` 判定。
|
||||||
|
|
||||||
|
### P1
|
||||||
|
|
||||||
|
- 明确产品规则:
|
||||||
|
- `npc_trade` / `npc_gift` / `npc_recruit` 第一次点击是否就应该写入一条“进入交易 / 送礼 / 招募确认”的剧情反馈。
|
||||||
|
- 如果答案是“应该”,那这些 modal 型 function 需要补一层轻量 story feedback,而不是只弹框。
|
||||||
|
|
||||||
|
### P1
|
||||||
|
|
||||||
|
- 给 `npc_chat` 补自动化测试:
|
||||||
|
- 聊天后必须出现 `story_continue_adventure`
|
||||||
|
- 第二次点击后必须能展示 `deferredOptions`
|
||||||
|
|
||||||
|
### P1
|
||||||
|
|
||||||
|
- 给 `npc_trade` / `npc_gift` / `npc_recruit` 补自动化测试:
|
||||||
|
- 第一次点击只分流
|
||||||
|
- 确认后一定触发 `commitGeneratedState`
|
||||||
|
- 后续一定能拿到新的 `StoryMoment`
|
||||||
|
|
||||||
|
### P2
|
||||||
|
|
||||||
|
- 扩展 `StateFunctionEditor` 或补新的“交互 function 预览器”,把 `npc_*` / `treasure_*` / `story_continue_adventure` 也纳入可预演范围。
|
||||||
|
|
||||||
|
### P2
|
||||||
|
|
||||||
|
- 清理 `idle_follow_clue` 这类“半退场”的 function,保证:
|
||||||
|
- 运行时集合
|
||||||
|
- prompt 描述
|
||||||
|
- 编辑器
|
||||||
|
- 转场映射
|
||||||
|
- 测试
|
||||||
|
|
||||||
|
保持一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最后结论
|
||||||
|
|
||||||
|
当前最值得优先处理的,不是再继续加新的 function,而是先把这 3 个点收拢:
|
||||||
|
|
||||||
|
1. **把“首次点击只分流、不推理”的 function 明确标出来。**
|
||||||
|
2. **把 `npc_chat -> story_continue_adventure -> deferredOptions` 这条延迟展示链做清楚。**
|
||||||
|
3. **先修掉 `story_continue_adventure` 的乱码和基于文案的 UI 判断。**
|
||||||
|
|
||||||
|
如果不先处理这几个点,用户会持续把“按设计延迟”与“真实漏调剧情推理”混在一起感知,后面 function 越多,这类问题会越难排查。
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Function 需求完整性核查(2026-04-14)
|
||||||
|
|
||||||
|
## 1. 核查范围
|
||||||
|
|
||||||
|
本次核查按当前线程上下文,聚焦 function 体系相关文档与实现,不扩大到整个项目全部 PRD。
|
||||||
|
|
||||||
|
本次实际对照了这些文档:
|
||||||
|
|
||||||
|
- `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/audits/engineering/README.md`
|
||||||
|
|
||||||
|
本次实际核对了这些实现入口:
|
||||||
|
|
||||||
|
- `src/data/functionCatalog/**`
|
||||||
|
- `src/data/stateFunctions.ts`
|
||||||
|
- `src/data/npcInteractions.ts`
|
||||||
|
- `src/components/AdventurePanel.tsx`
|
||||||
|
- `src/hooks/story/choiceActions.ts`
|
||||||
|
- `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
- `src/hooks/story/npcInteraction.ts`
|
||||||
|
- `src/services/runtimeStoryService.ts`
|
||||||
|
- `server-node/src/modules/story/**`
|
||||||
|
- `server-node/src/modules/inventory/**`
|
||||||
|
- `server-node/src/modules/runtime-item/**`
|
||||||
|
- `server-node/src/modules/quest/**`
|
||||||
|
|
||||||
|
## 2. 核查结论
|
||||||
|
|
||||||
|
结论先说:
|
||||||
|
|
||||||
|
- function 主链路需求已经基本落地,不需要再继续“为了完整而过度迭代”。
|
||||||
|
- 当前最主要的缺口不是再发明一套新 function 流程,而是把已有链路补齐回归测试,确保后续不会回退。
|
||||||
|
- 本轮核查后,已把两条仍缺直接测试兜底的核心链路补上。
|
||||||
|
|
||||||
|
## 3. 已确认已经落地的需求
|
||||||
|
|
||||||
|
### 3.1 function 目录化与分层收口
|
||||||
|
|
||||||
|
已实现:
|
||||||
|
|
||||||
|
- `state / npc / treasure / flow / panel` 五类 function 已统一收口到 `src/data/functionCatalog/`
|
||||||
|
- `src/data/functionCatalog/index.ts` 已提供统一导出
|
||||||
|
- `SERVER_RUNTIME_FUNCTION_IDS` 也已和 catalog 文档映射对齐
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这部分需求已完成,不需要继续重做结构。
|
||||||
|
|
||||||
|
### 3.2 `story_continue_adventure` 延迟展示链路
|
||||||
|
|
||||||
|
已实现:
|
||||||
|
|
||||||
|
- `npc_chat` 在 `npcEncounterActions.ts` 中先生成聊天正文,再把后续选项写入 `deferredOptions`
|
||||||
|
- `choiceActions.ts` 在点击 `story_continue_adventure` 时会直接恢复 `deferredOptions`
|
||||||
|
- `AdventurePanel.tsx` 已按 `functionId` 而不是 `actionText` 识别该特殊按钮
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这条主功能链路已经真正存在,不属于“文档写了、实现没跟上”。
|
||||||
|
|
||||||
|
### 3.3 modal 型 function 的首次点击分流
|
||||||
|
|
||||||
|
已实现:
|
||||||
|
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_recruit`
|
||||||
|
|
||||||
|
这些 function 当前都明确是“首次点击先分流,再在确认后进入真正执行链”。
|
||||||
|
|
||||||
|
当前实现路径:
|
||||||
|
|
||||||
|
- 首次点击:
|
||||||
|
- `choiceActions.ts`
|
||||||
|
- `storyGenerationState.ts`
|
||||||
|
- `npcInteraction.ts`
|
||||||
|
- 确认后:
|
||||||
|
- `trade / gift / quest` 进入 server runtime action
|
||||||
|
- `recruit` 进入本地招募对白与本地状态提交链
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这不是未实现,而是当前架构设计如此。
|
||||||
|
- 文档里“确认后要继续推进剧情/结算”的要求已经满足。
|
||||||
|
|
||||||
|
### 3.4 Task6 function 的服务端化
|
||||||
|
|
||||||
|
已实现:
|
||||||
|
|
||||||
|
- `inventory_use`
|
||||||
|
- `equipment_equip`
|
||||||
|
- `equipment_unequip`
|
||||||
|
- `forge_craft`
|
||||||
|
- `forge_dismantle`
|
||||||
|
- `forge_reforge`
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_quest_turn_in`
|
||||||
|
- `treasure_secure`
|
||||||
|
- `treasure_inspect`
|
||||||
|
- `treasure_leave`
|
||||||
|
|
||||||
|
这些 function 已经在前端 `runtimeStoryService.ts`、服务端 `story runtime / inventory / runtime-item / quest` 模块里形成闭环。
|
||||||
|
|
||||||
|
判断:
|
||||||
|
|
||||||
|
- 这部分不需要再回退到前端本地重写。
|
||||||
|
|
||||||
|
## 4. 本轮发现的真实缺口
|
||||||
|
|
||||||
|
本轮真正仍未完整落地的,不是功能行为本身,而是下面两条回归保护:
|
||||||
|
|
||||||
|
### 4.1 `npc_chat -> story_continue_adventure -> deferredOptions`
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
- 文档明确要求这条链路要清楚、可验证。
|
||||||
|
- 代码已经有,但之前没有直接测试“点击继续冒险后必须展示 deferredOptions”。
|
||||||
|
|
||||||
|
本轮已补:
|
||||||
|
|
||||||
|
- `src/hooks/story/choiceActions.test.ts`
|
||||||
|
- 新增 `reveals deferred adventure options when story_continue_adventure is selected`
|
||||||
|
|
||||||
|
### 4.2 `AdventurePanel` 对 continue option 的识别方式
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
- 文档明确要求不要再靠文案识别 continue option。
|
||||||
|
- 实现已经改成按 `functionId` 识别,但之前没有组件层测试锁住。
|
||||||
|
|
||||||
|
本轮已补:
|
||||||
|
|
||||||
|
- `src/components/AdventurePanel.test.tsx`
|
||||||
|
- 验证 `story_continue_adventure` 即使 actionText 改掉,仍然显示延迟选项提示
|
||||||
|
- 验证仅 actionText 相同但 functionId 不同,不会误触发提示
|
||||||
|
|
||||||
|
## 5. 本轮新增测试
|
||||||
|
|
||||||
|
本轮新增:
|
||||||
|
|
||||||
|
- `src/components/AdventurePanel.test.tsx`
|
||||||
|
- `src/hooks/story/choiceActions.test.ts`
|
||||||
|
- 新增 deferred options 恢复用例
|
||||||
|
|
||||||
|
## 6. 验收结论
|
||||||
|
|
||||||
|
按当前 function 相关文档要求判断:
|
||||||
|
|
||||||
|
- 核心运行时需求:已实现
|
||||||
|
- 延迟展示链路:已实现
|
||||||
|
- modal 分流架构:已实现
|
||||||
|
- Task6 服务端承接:已实现
|
||||||
|
- 回归测试盲区:本轮已补齐关键缺口
|
||||||
|
|
||||||
|
最终建议:
|
||||||
|
|
||||||
|
- 这块现在不应该继续过度迭代。
|
||||||
|
- 后续应以“新增需求再增量补测试”为主,而不是再次重构 function 主链路。
|
||||||
|
- 除非后续 PRD 明确要求“modal 首次点击也必须立刻生成一段剧情反馈”,否则不建议为了形式统一再强行改写当前分流模型。
|
||||||
212
docs/audits/FUNCTION_RUNTIME_FULL_TEST_AUDIT_2026-04-16.md
Normal file
212
docs/audits/FUNCTION_RUNTIME_FULL_TEST_AUDIT_2026-04-16.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# Function 运行时完整测试审计(2026-04-16)
|
||||||
|
|
||||||
|
## 1. 本次目标
|
||||||
|
|
||||||
|
本次不是泛泛地跑一遍前端页面,而是围绕“游戏运行中的 function 主链路”做系统化核查,重点确认下面 4 件事:
|
||||||
|
|
||||||
|
- 当前运行时 function 集合是否还能正确构建、过滤、排序和解析。
|
||||||
|
- function 命中的前端承接链路是否仍然稳定。
|
||||||
|
- 已服务端化的 runtime action function 是否还能闭环执行。
|
||||||
|
- 工程门禁是否处于可持续回归的状态。
|
||||||
|
|
||||||
|
## 2. 本次实际覆盖范围
|
||||||
|
|
||||||
|
本轮重点覆盖了这些实现入口:
|
||||||
|
|
||||||
|
- `src/data/stateFunctions.ts`
|
||||||
|
- `src/data/functionCatalog/**`
|
||||||
|
- `src/data/npcInteractions.ts`
|
||||||
|
- `src/hooks/story/**`
|
||||||
|
- `src/services/runtimeStoryService.ts`
|
||||||
|
- `src/hooks/useGameFlow.ts`
|
||||||
|
- `server-node/src/modules/story/**`
|
||||||
|
- `server-node/src/modules/inventory/**`
|
||||||
|
- `server-node/src/modules/runtime-item/**`
|
||||||
|
- `server-node/src/modules/quest/**`
|
||||||
|
- `scripts/smoke-server-node.ts`
|
||||||
|
|
||||||
|
## 3. 实际执行的测试与检查
|
||||||
|
|
||||||
|
### 3.1 前端 Vitest 全量测试
|
||||||
|
|
||||||
|
执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm.cmd test
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- `110` 个测试文件全部通过
|
||||||
|
- `276` 条测试全部通过
|
||||||
|
|
||||||
|
其中与 function 主链路直接相关、并已确认通过的测试包括:
|
||||||
|
|
||||||
|
- `src/data/stateFunctions.test.ts`
|
||||||
|
- `src/data/functionCatalog/functionCatalog.test.ts`
|
||||||
|
- `src/data/npcInteractions.test.ts`
|
||||||
|
- `src/hooks/story/choiceActions.test.ts`
|
||||||
|
- `src/hooks/story/storyGenerationState.test.ts`
|
||||||
|
- `src/hooks/story/runtimeStoryCoordinator.test.ts`
|
||||||
|
- `src/components/AdventurePanel.test.tsx`
|
||||||
|
- `src/services/runtimeStoryService.test.ts`
|
||||||
|
|
||||||
|
补充说明:
|
||||||
|
|
||||||
|
- 运行 `hostileNpcPresets.test.ts` 时会看到 “network disabled in test” 的日志。
|
||||||
|
- 这不是本轮失败项,测试已验证在 LLM 不可达时会正确走 deterministic fallback。
|
||||||
|
|
||||||
|
### 3.2 内容数据校验
|
||||||
|
|
||||||
|
执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm.cmd run check:content
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- `check:data` 通过
|
||||||
|
- `check:overrides` 通过
|
||||||
|
- `check:smoke` 通过
|
||||||
|
- 输出为:`Content validation passed. scenes=24 monsters=16 characters=5 functions=12`
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
- 当前基础内容数据、override 与 smoke 内容校验没有发现新的 function 数据层问题。
|
||||||
|
|
||||||
|
### 3.3 服务端测试
|
||||||
|
|
||||||
|
执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm.cmd run server-node:test
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- `108` 条服务端测试全部通过
|
||||||
|
|
||||||
|
本轮已确认通过的 function 相关服务端能力包括:
|
||||||
|
|
||||||
|
- `inventory_use`
|
||||||
|
- `equipment_equip`
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_quest_turn_in`
|
||||||
|
- `treasure_inspect`
|
||||||
|
- 战斗结算与 quest signal 推进
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
- 已服务端化的 runtime function 承接链路当前单测层面是稳定的。
|
||||||
|
|
||||||
|
### 3.4 服务端 smoke
|
||||||
|
|
||||||
|
执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm.cmd run server-node:smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- 失败
|
||||||
|
- 报错:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: Cannot read properties of undefined (reading 'enabled')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 TypeScript 类型检查
|
||||||
|
|
||||||
|
执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm.cmd run typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- 失败
|
||||||
|
- 失败位置:
|
||||||
|
- `src/hooks/useGameFlow.ts:339`
|
||||||
|
|
||||||
|
### 3.6 生产构建
|
||||||
|
|
||||||
|
执行命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm.cmd run build
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
- 通过
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
- 当前代码可构建,但并不代表类型门禁与 smoke 门禁同样健康。
|
||||||
|
|
||||||
|
## 4. 本轮确认的问题
|
||||||
|
|
||||||
|
## P1:`server-node:smoke` 已失效,无法完成服务端运行时冒烟验证
|
||||||
|
|
||||||
|
- 现象:
|
||||||
|
- `npm.cmd run server-node:smoke` 无法启动临时 Express 服务,直接在 `createSmsVerificationService` 处报错。
|
||||||
|
- 直接原因:
|
||||||
|
- [`scripts/smoke-server-node.ts`](../../scripts/smoke-server-node.ts) 里的 `createSmokeConfig()` 只构造了 `llm`、`dashScope` 等字段,没有补齐后续新增的 `smsAuth`、`wechatAuth`、`authSession` 配置块。
|
||||||
|
- [`server-node/src/config.ts`](../../server-node/src/config.ts) 中 `AppConfig` 已把这些字段定义为必需项。
|
||||||
|
- [`server-node/src/services/smsVerificationService.ts`](../../server-node/src/services/smsVerificationService.ts) 在 `createSmsVerificationService` 中直接读取 `config.smsAuth.enabled`,因此 smoke 配置一进入 `createAppContext` 就会崩。
|
||||||
|
- 影响:
|
||||||
|
- 本地无法再用 smoke 脚本验证“服务端真实启动 + 认证 + runtime save/settings 回路”。
|
||||||
|
- 这会让 function 服务端化之后的真实接线路径少掉一层最接近运行时的保护。
|
||||||
|
- 判断:
|
||||||
|
- 这是本轮最明确、最稳定复现的真实 bug。
|
||||||
|
|
||||||
|
## P1:`typecheck` 门禁已破,`useGameFlow` 的 starter inventory 合并存在类型漂移
|
||||||
|
|
||||||
|
- 现象:
|
||||||
|
- `npm.cmd run typecheck` 在 [`src/hooks/useGameFlow.ts`](../../src/hooks/useGameFlow.ts) 第 `339` 行失败。
|
||||||
|
- 直接原因:
|
||||||
|
- 同文件中的 `mergeStarterInventoryItems<T extends { category: string; name: string }>` 会优先从显式 starter item 推断出一个“字段更严格”的 `T`。
|
||||||
|
- 但 fallback 侧传入的是 [`InventoryItem`](../../src/types/items.ts) 数组,而 `InventoryItem.description`、`equipmentSlotId`、`runtimeMetadata` 等字段本身是可选的。
|
||||||
|
- 于是 `explicitItems` 与 `fallbackItems` 在泛型推断后不再兼容,类型门禁被打穿。
|
||||||
|
- 影响:
|
||||||
|
- 当前 starter inventory 初始化链路虽然能跑、也能通过构建,但已经失去 TypeScript 对结构一致性的保护。
|
||||||
|
- 这类问题后续很容易演变成“自定义世界显式初始物品”和“默认初始物品”在字段形态上继续分叉。
|
||||||
|
- 判断:
|
||||||
|
- 这是一个真实的工程 bug,优先级高于纯测试文案问题。
|
||||||
|
|
||||||
|
## 5. 本轮已确认通过的结论
|
||||||
|
|
||||||
|
- state function 运行时构建、过滤、优先级、选项解析当前测试全绿。
|
||||||
|
- function catalog 文档映射、helper option、trade/gift/recruit modal helper 当前测试全绿。
|
||||||
|
- 前端 function 主链路相关测试在最新工作区状态下已全部通过。
|
||||||
|
- 服务端 runtime story action、inventory、runtime-item、quest 承接链路当前测试全绿。
|
||||||
|
- 内容数据层校验通过。
|
||||||
|
- 生产构建通过。
|
||||||
|
|
||||||
|
## 6. 结论
|
||||||
|
|
||||||
|
如果只看“游戏运行中的 function 主链路”,当前结论是:
|
||||||
|
|
||||||
|
- 前端 function 运行时逻辑:通过
|
||||||
|
- 服务端 function action 承接:通过
|
||||||
|
- 内容数据与 function 基础目录:通过
|
||||||
|
- 工程回归门禁:不完整
|
||||||
|
|
||||||
|
当前最需要处理的不是再扩 function 范围,而是先修复这两个门禁缺口:
|
||||||
|
|
||||||
|
1. 修好 `server-node:smoke` 的配置构造,让服务端冒烟恢复可用。
|
||||||
|
2. 修好 `useGameFlow` 的 starter inventory 类型漂移,让 `typecheck` 回到绿色基线。
|
||||||
|
|
||||||
|
## 7. 备注
|
||||||
|
|
||||||
|
本轮没有做两类事情:
|
||||||
|
|
||||||
|
- 没有接入真实外部 LLM 做在线回归,本轮依赖的是本地测试里已有的 fallback 断言。
|
||||||
|
- 没有人手逐个点击整局游戏所有 function 的视觉回放,本轮重点是自动化测试、服务端测试、内容校验与 smoke 门禁。
|
||||||
|
|
||||||
|
因此,本审计可以说明“当前 function 系统的自动化测试层状况”,但不等于“所有视觉演出与在线模型联动都已人工验证完毕”。
|
||||||
128
docs/audits/FUNCTION_TEST_AUDIT_2026-04-14.md
Normal file
128
docs/audits/FUNCTION_TEST_AUDIT_2026-04-14.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Function 测试审计(2026-04-14)
|
||||||
|
|
||||||
|
补充更新:
|
||||||
|
|
||||||
|
- 本文记录的 2 个 bug 已在同日完成代码修复。
|
||||||
|
- 对应测试已经从“稳定复现旧行为”切换为“验证修复后行为”。
|
||||||
|
|
||||||
|
## 1. 本次新增测试
|
||||||
|
|
||||||
|
本轮新增了两组 function 相关测试:
|
||||||
|
|
||||||
|
- `src/data/stateFunctions.test.ts`
|
||||||
|
- 覆盖 state function 的运行时过滤、优先级、选项解析、排序逻辑。
|
||||||
|
- `src/data/functionCatalog/functionCatalog.test.ts`
|
||||||
|
- 覆盖 function 文档映射、flow helper、NPC helper modal 初始化逻辑。
|
||||||
|
|
||||||
|
这两组测试都直接挂在现有 `vitest` 体系里,没有新建独立测试框架。
|
||||||
|
|
||||||
|
## 2. 本次执行结果
|
||||||
|
|
||||||
|
本轮实际执行了以下测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx vitest run src/data/stateFunctions.test.ts src/data/functionCatalog/functionCatalog.test.ts
|
||||||
|
npx vitest run src/data/npcInteractions.test.ts src/hooks/story/storyGenerationState.test.ts src/services/runtimeStoryService.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
执行结果:
|
||||||
|
|
||||||
|
- 新增测试:`2` 个文件,`12` 条测试,全部通过。
|
||||||
|
- 复跑已有 function 相关测试:`3` 个文件,`17` 条测试,全部通过。
|
||||||
|
- 修复回归测试:`5` 个文件,`30` 条测试,全部通过。
|
||||||
|
- 编码检查:`1516` 个文件全部通过。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 本轮不是“测试全绿就代表没有问题”。
|
||||||
|
- 最初定位出的 2 个问题,已经在后续修复回合里转成了回归测试。
|
||||||
|
|
||||||
|
## 3. 历史 bug 与修复结果
|
||||||
|
|
||||||
|
### 3.1 `battle_recover_breath`
|
||||||
|
|
||||||
|
- 所在位置:`src/data/stateFunctions.ts`
|
||||||
|
- 状态:已修复
|
||||||
|
- 原始问题表现:
|
||||||
|
- 当 `inBattle = true`,但当前没有存活敌人时,`battle_recover_breath` 仍会留在可执行 function 列表中。
|
||||||
|
- 直接原因:
|
||||||
|
- `matchesCategory` 对 `recovery` 分类只判断了是否处于战斗态,没有像 `battle` / `escape` 分类那样额外校验 `hasAliveMonsters(context.monsters)`。
|
||||||
|
- 修复方式:
|
||||||
|
- 已在 `matchesCategory` 的 `recovery` 分支中,为战斗恢复类 function 补上 `hasAliveMonsters(context.monsters)` 判断。
|
||||||
|
- 修复前影响:
|
||||||
|
- 在“敌人已死但战斗态尚未清理干净”的边界帧里,界面仍可能出现战斗恢复类选项。
|
||||||
|
- 这会让 function 池和真实战斗状态产生残留错位。
|
||||||
|
- 当前验证方式:
|
||||||
|
- `inBattle = true`
|
||||||
|
- `monsters = [{ hp: 0, ... }]`
|
||||||
|
- 调用 `getExecutableFunctions(context)`
|
||||||
|
- 现在返回结果应为空,不再包含 `battle_recover_breath`
|
||||||
|
- 对应用例:
|
||||||
|
- `src/data/stateFunctions.test.ts`
|
||||||
|
- 用例名:`removes battle_recover_breath when combat has no living monsters`
|
||||||
|
|
||||||
|
### 3.2 `npc_trade`
|
||||||
|
|
||||||
|
- 所在位置:`src/data/functionCatalog/npc/npcTrade.ts`
|
||||||
|
- 状态:已修复
|
||||||
|
- 原始问题表现:
|
||||||
|
- trade modal 初始化时,`selectedPlayerItemId` 直接取 `state.playerInventory[0]?.id`。
|
||||||
|
- 如果玩家背包第一项数量为 `0`,modal 默认会选中一件不可出售物品。
|
||||||
|
- 直接原因:
|
||||||
|
- `buildNpcTradeModalState` 没有过滤 `quantity <= 0` 的物品,也没有寻找第一个可交易物品。
|
||||||
|
- 修复方式:
|
||||||
|
- 已改为优先选择 `quantity > 0` 的可交易物品。
|
||||||
|
- 同时对 NPC 库存和玩家背包都使用同一条筛选规则,避免默认选中空物品。
|
||||||
|
- 修复前影响:
|
||||||
|
- 交易面板第一次打开时,默认状态可能就是不可确认的。
|
||||||
|
- 用户需要手动切换到第二件物品,才会进入可提交状态。
|
||||||
|
- 当前验证方式:
|
||||||
|
- `playerInventory[0].quantity = 0`
|
||||||
|
- `playerInventory[1].quantity > 0`
|
||||||
|
- 调用 `buildNpcTradeModalState(...)`
|
||||||
|
- 现在 `selectedPlayerItemId` 应该自动落到第一件可交易物品
|
||||||
|
- 对应用例:
|
||||||
|
- `src/data/functionCatalog/functionCatalog.test.ts`
|
||||||
|
- 用例名:`prefers the first tradable player item when zero-quantity items exist`
|
||||||
|
- `src/hooks/story/storyGenerationState.test.ts`
|
||||||
|
- 用例名:`skips zero-quantity player items when opening the trade modal`
|
||||||
|
|
||||||
|
## 4. 本轮已验证通过的 function 能力
|
||||||
|
|
||||||
|
以下内容本轮已通过测试验证,没有发现新的明显问题:
|
||||||
|
|
||||||
|
- state function runtime 构建:
|
||||||
|
- `idle_follow_clue` 已正确从运行时候选池移除。
|
||||||
|
- `idle_explore_forward` 的运行时文案覆盖仍然生效。
|
||||||
|
- state function 选项行为:
|
||||||
|
- 高压战斗下 `battle_recover_breath` 会被正确提权。
|
||||||
|
- 营地场景会正确隐藏 `idle_explore_forward`。
|
||||||
|
- `idle_travel_next_scene` 会强制使用运行时建议 actionText。
|
||||||
|
- `battle_all_in_crush` 会保留外部传入的自定义 actionText。
|
||||||
|
- story option 排序仍保持“前 2 个 model 锁定 + 后续按 priority 排序”。
|
||||||
|
- flow helper:
|
||||||
|
- `story_continue_adventure`
|
||||||
|
- `camp_travel_home_scene`
|
||||||
|
- NPC helper:
|
||||||
|
- `npc_preview_talk`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_recruit`
|
||||||
|
- 文档映射:
|
||||||
|
- 当前 `SERVER_RUNTIME_FUNCTION_IDS` 全部都能在 function catalog 文档中找到对应条目。
|
||||||
|
- 本轮扫描到的 function `source` 路径全部存在,没有出现失效引用。
|
||||||
|
|
||||||
|
## 5. 本轮修复动作
|
||||||
|
|
||||||
|
- `battle_recover_breath`
|
||||||
|
- 已补充战斗恢复类 function 的存活敌人校验,避免战斗边界帧残留非法选项。
|
||||||
|
- `npc_trade`
|
||||||
|
- 已把 trade modal 默认选中逻辑改为优先寻找可交易物品,不再直接吃数组第一项。
|
||||||
|
- 回归测试
|
||||||
|
- 原先记录旧行为的测试已翻转为修复后预期,并额外补了一条 `storyGenerationState` 接入层测试。
|
||||||
|
|
||||||
|
## 6. 备注
|
||||||
|
|
||||||
|
这次测试资产的意义分两步:
|
||||||
|
|
||||||
|
- 第一步先把 bug 稳定复现出来,避免问题只停留在口头描述。
|
||||||
|
- 第二步在修复后把断言翻转成“正确行为”,让它们正式成为回归测试。
|
||||||
133
docs/audits/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md
Normal file
133
docs/audits/ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# 物品生成系统与 Build 标签系统 PRD 落地审计
|
||||||
|
|
||||||
|
审计时间:2026-04-05
|
||||||
|
|
||||||
|
审计范围:
|
||||||
|
|
||||||
|
- `docs/prd/AI_NATIVE_RUNTIME_ITEM_GENERATION_DESIGN.md`
|
||||||
|
- `docs/prd/RUNTIME_ITEM_GENERATION_CURRENT_SYSTEM_DESIGN.md`
|
||||||
|
- `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md`
|
||||||
|
- `docs/design/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md`
|
||||||
|
|
||||||
|
## 结论速览
|
||||||
|
|
||||||
|
### 1. 物品生成系统
|
||||||
|
|
||||||
|
当前状态可以判定为:**主链已补齐落地**。
|
||||||
|
|
||||||
|
已经落地的是:
|
||||||
|
|
||||||
|
- 有独立的运行时上下文层、导演层、本地编译层、叙事回写层。
|
||||||
|
- 宝藏、怪物掉落、通用 NPC 商店已经能走统一的 runtime item director。
|
||||||
|
- 永久 build 标签物品、限时 build buff 物品、少量数值物品三种骨架都已经能编译出来,并接进现有背包 / 装备 / build 结算。
|
||||||
|
|
||||||
|
本轮已补齐的是:
|
||||||
|
|
||||||
|
- 新增了 runtime item AI 意图导演与 prompt,并接入 NPC 帮助奖励主链,失败时自动回退到本地导演。
|
||||||
|
- NPC 交易库存改成按玩家当前 build 生成,并通过 `tradeStockSignature` 只在 build 变化时刷新。
|
||||||
|
- NPC 帮助奖励、委托奖励已经统一接入 runtime item director。
|
||||||
|
- 怪物掉落已经改成“基础掉落 + 语义掉落”双层叠加。
|
||||||
|
|
||||||
|
### 2. Build 标签系统
|
||||||
|
|
||||||
|
当前状态可以判定为:**核心已按 PRD 落地,且实现范围比 PRD 更大**。
|
||||||
|
|
||||||
|
已经落地的是:
|
||||||
|
|
||||||
|
- `BuildTagDefinition.attributeAffinity` 已扩展。
|
||||||
|
- `buildDamage.ts` 已改成“标签分别匹配角色属性画像”的加法模型,不再做标签两两网络效应。
|
||||||
|
- Buff / 角色固有 / 武器 / 护甲 / 饰品 / 套装标签都能进入最终倍率。
|
||||||
|
- Character 面板已经切到“属性适配度”展示,并能拆出单标签的属性贡献明细。
|
||||||
|
- 有针对新公式的测试覆盖。
|
||||||
|
|
||||||
|
还存在的尾项主要是:
|
||||||
|
|
||||||
|
- 实现用的是“世界属性 schema 六轴模型”,不是 PRD 文字里的固定四维属性。
|
||||||
|
- 旧的标签相似度辅助能力没有完全清干净,重铸仍在用 `getSimilarBuildTags` 做候选标签替换。
|
||||||
|
|
||||||
|
## 物品生成系统审计
|
||||||
|
|
||||||
|
| PRD 项 | 当前实现 | 判定 | 代码证据 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 上下文采样层 | 已有 `buildRuntimeItemGenerationContext` / `buildQuestRuntimeItemGenerationContext`,会收集场景、遭遇、关联 NPC、最近剧情、玩家 build 标签与 build gap。 | 已落地 | `src/data/runtimeItemContext.ts:157-188`、`src/data/runtimeItemContext.ts:191-252` |
|
||||||
|
| AI 意图层 | 已新增 `runtimeItemAiDirector` / `runtimeItemAiPrompt`,`buildRuntimeItemAiPromptInput` 已进入真实 prompt 组装;NPC 帮助奖励主链会先请求 AI 物品意图,再回落到本地意图导演。 | 已补齐 | `src/services/runtimeItemAiPrompt.ts`、`src/services/runtimeItemAiDirector.ts`、`src/data/runtimeItemDirector.ts`、`src/hooks/story/npcEncounterActions.ts` |
|
||||||
|
| 本地编译层 | 已按 channel / slot / permanence 做 rarity 与预算编译,并产出 `statProfile`、`useProfile.buildBuffs`、`buildProfile`、`runtimeMetadata`。 | 已落地 | `src/data/runtimeItemCompiler.ts:77-101`、`src/data/runtimeItemCompiler.ts:122-206`、`src/data/runtimeItemCompiler.ts:245-276` |
|
||||||
|
| 叙事回写层 | 会把锚点、来源理由、build 倾向回写进物品名和描述。 | 已落地 | `src/data/runtimeItemNarrative.ts:171-189` |
|
||||||
|
| 永久标签 / 限时标签 / 少量数值三类物品 | 永久物品走 `buildProfile`,限时物品走 `useProfile.buildBuffs`,并可附带少量数值。 | 已落地 | `src/data/runtimeItemCompiler.ts:104-155`、`src/data/runtimeItemCompiler.ts:157-206` |
|
||||||
|
| 宝藏入口 | 宝藏奖励已经走 `buildRuntimeItemGenerationContext + buildDirectedRuntimeReward`,并把结果写回 story hint。 | 已落地 | `src/data/treasureInteractions.ts:52-89`、`src/hooks/useTreasureFlow.ts:45-75` |
|
||||||
|
| NPC 交易入口 | 通用 NPC 商店已改成按玩家当前 build 生成;初始 NPC 状态、交易模态打开时刷新、场景预览读取都会带上完整 `GameState`,并用 `tradeStockSignature` 避免无意义重刷。角色型 NPC 仍保留既有角色装备/背包模板。 | 已补齐 | `src/data/npcInteractions.ts`、`src/hooks/story/npcInteraction.ts`、`src/hooks/useStoryGeneration.ts`、`src/components/NpcModals.tsx` |
|
||||||
|
| NPC 帮助 / 关系奖励 | `npc_reward` 已真正接入主链。帮助奖励现在会生成带 `runtimeMetadata` 的 runtime item,并保留数值恢复、冷却缩减与 story hint。 | 已补齐 | `src/data/npcInteractions.ts`、`src/hooks/story/npcEncounterActions.ts` |
|
||||||
|
| 委托奖励入口 | Quest reward 已改为走 runtime director,支持 `buildQuestRuntimeItemGenerationContext`,领取时发放的是 runtime item。主 NPC 接任务流程也已切到 `generateQuestForNpcEncounter`。 | 已补齐 | `src/data/questFlow.ts`、`src/hooks/story/npcEncounterActions.ts`、`src/services/questDirector.ts` |
|
||||||
|
| 怪物掉落双层设计 | 普通世界掉落已改成预设 `lootTable` 基础掉落与 `monster_drop` runtime 语义掉落并存,不再互相覆盖。 | 已补齐 | `src/data/hostileNpcPresets.ts`、`src/data/hostileNpcPresets.test.ts` |
|
||||||
|
|
||||||
|
### 物品系统的具体判断
|
||||||
|
|
||||||
|
#### 已经明显符合 PRD 的部分
|
||||||
|
|
||||||
|
1. 系统分层已经形成。
|
||||||
|
`runtimeItemContext -> runtimeItemDirector -> runtimeItemCompiler -> runtimeItemNarrative` 这条链已经非常接近 PRD 里“上下文采样 / AI 意图 / 本地编译 / 叙事回写”的结构。
|
||||||
|
|
||||||
|
2. build 导向优先于纯数值。
|
||||||
|
`buildRuntimeItemContext` 会先算 `playerBuildTags` 和 `playerBuildGaps`,导演层再优先把 gap tag 与现有 build tag 拼进 `targetBuildDirection`,编译层才决定数值预算。
|
||||||
|
|
||||||
|
3. 奖励已经进入真实玩法结算。
|
||||||
|
runtime item 生成出的 `buildProfile` 会进入装备 build 结算,`useProfile.buildBuffs` 会在使用物品时写入 `activeBuildBuffs`。
|
||||||
|
|
||||||
|
#### 本轮补齐后的说明
|
||||||
|
|
||||||
|
1. 运行时物品意图已经进入真实主链。
|
||||||
|
当前至少在 NPC 帮助奖励链路中,已经先走 AI 意图导演,再走本地编译与叙事回写;如果模型不可用,会回退到既有启发式导演,保证玩法不断。
|
||||||
|
|
||||||
|
2. 交易库存已经真正读取玩家当前构筑。
|
||||||
|
通用交易 NPC 的库存会基于玩家当前 build、装备标签和 build gap 生成,而不是继续依赖 NPC 自身偏好标签。
|
||||||
|
|
||||||
|
3. 帮助奖励、委托奖励、怪物掉落都已并入统一 runtime director。
|
||||||
|
现在三条链路都能产出带 relation anchor / source reason / runtime metadata 的 runtime item。
|
||||||
|
|
||||||
|
## Build 标签系统审计
|
||||||
|
|
||||||
|
| PRD 项 | 当前实现 | 判定 | 代码证据 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `BuildTagDefinition.attributeAffinity` 扩展 | 类型已扩展,标签注册表也会为每个标签注入 affinity。 | 已落地 | `src/types/build.ts:6-13`、`src/data/buildTags.ts:66-69` |
|
||||||
|
| 静态标签亲和度表 | 已有 `buildTagAttributeAffinity.ts`,提供标签到属性轴的静态 affinity 表。 | 已落地 | `src/data/buildTagAttributeAffinity.ts:127-183` |
|
||||||
|
| 从“标签互相影响”改为“标签分别匹配角色属性” | `buildDamage.ts` 已按单标签计算 `fitScore`、`bonusDelta` 和属性贡献,最后做加法累积,不再做 pair/cluster 乘法网络。 | 已落地 | `src/data/buildDamage.ts:236-318` |
|
||||||
|
| 来源系数 | Buff / 角色 / 武器 / 护甲 / 饰品 / 套装都有独立 source coefficient,和 PRD 基本一致。 | 已落地 | `src/data/buildDamage.ts:95-114` |
|
||||||
|
| 最终伤害接入 | `resolvePlayerOutgoingDamage` / `resolveCompanionOutgoingDamage` / `resolveMonsterOutgoingDamage` 都已接 `buildDamageMultiplier`。 | 已落地 | `src/data/buildDamage.ts:498-542` |
|
||||||
|
| 展示层从“标签协同”改成“属性适配度” | Character 面板已经按标签展示 bonus、来源、主导属性,并能打开明细弹窗。 | 已落地 | `src/components/CharacterPanel.tsx:205-247`、`src/components/CharacterPanel.tsx:497-539` |
|
||||||
|
| 验收测试 | 已覆盖单标签可拆分、删一个标签不重算其余标签、不同属性角色用同一套装倍率不同、Buff/套装来源正常进入等场景。 | 已落地 | `src/data/buildDamage.test.ts:125-313` |
|
||||||
|
| 四维属性口径 | PRD 写的是四维固定属性;实现已经提升为 world schema 六轴模型。目标一致,但口径不再一一对应。 | 已落地,但实现已外延扩展 | `src/types/attributes.ts:3-15`、`src/data/worldAttributeSchemas.ts:4-155` |
|
||||||
|
| 旧标签相似度清理 | 核心伤害结算已不再依赖旧矩阵,但 `getSimilarBuildTags` 仍存在,重铸继续用它挑候选标签,`generate:build-tags` 脚本也还保留。 | 收尾未完成 | `src/data/buildTags.ts:183-215`、`src/data/forgeSystem.ts:371-399`、`package.json:21-24` |
|
||||||
|
|
||||||
|
### Build 系统的具体判断
|
||||||
|
|
||||||
|
#### 已经符合 PRD 核心目标的部分
|
||||||
|
|
||||||
|
1. 可解释性已经建立。
|
||||||
|
现在每个标签都有自己的 `fitScore`、`attributeContributions` 和 `attributeModifierDeltas`,玩家可以看到“这个标签为什么强、强在哪条属性轴上”。
|
||||||
|
|
||||||
|
2. 新增标签不会反向扰动旧标签贡献。
|
||||||
|
`buildDamage.test.ts` 已专门验证“删掉一个标签,只会移除它自己的 row,不会重算其他 row”。
|
||||||
|
|
||||||
|
3. 套装标签、Buff 标签、装备标签都能统一进入同一公式。
|
||||||
|
这点和 PRD 的来源规则一致,且测试已经覆盖。
|
||||||
|
|
||||||
|
#### 需要注意但不构成核心未落地的问题
|
||||||
|
|
||||||
|
1. 实现已经超出 PRD 的四维设计。
|
||||||
|
现在不是固定 `strength / agility / intelligence / spirit` 四维,而是通过 `WorldAttributeSchema` 映射成武侠/仙侠/自定义世界都能共用的语义属性轴。这更适合当前仓库的世界化属性系统,但也意味着 PRD 文案需要同步。
|
||||||
|
|
||||||
|
2. 旧相似度能力没有完全退场。
|
||||||
|
核心伤害结算已经不用标签两两矩阵,但锻造重铸仍然会根据标签相似度挑换洗标签,所以“旧体系相关命名/脚本”还没彻底收尾。
|
||||||
|
|
||||||
|
## 综合判断
|
||||||
|
|
||||||
|
如果按“是否已经把 PRD 的主战场落到代码里”来判断:
|
||||||
|
|
||||||
|
- **Build 标签系统:可以认为已经落地。**
|
||||||
|
- **物品生成系统:可以认为主链已经落地,之前审计出的缺口已在本轮补齐。**
|
||||||
|
|
||||||
|
如果接下来继续做收尾,更建议做的是:
|
||||||
|
|
||||||
|
1. 把 runtime item AI 意图层继续扩到宝藏、怪物掉落、委托奖励以外的更多同步入口,减少启发式 fallback 的覆盖面。
|
||||||
|
2. 给 Build 系统补一版文档同步,明确当前实现已经从 PRD 四维模型升级为世界属性 schema 模型,并清理剩余旧相似度脚本/命名。
|
||||||
|
3. 评估 `origin: 'ai_compiled'` 的语义是否还要细分成“AI 意图 + 本地编译”与“纯本地 fallback”两档,方便后续观测与埋点。
|
||||||
27
docs/audits/README.md
Normal file
27
docs/audits/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 审计与复盘
|
||||||
|
|
||||||
|
这一组文档聚焦“当前状态是否健康、问题在哪里、和目标设计差多少”。
|
||||||
|
|
||||||
|
## 系列总览
|
||||||
|
|
||||||
|
- [engineering/README.md](./engineering/README.md):当前工程优化审查与历史结论聚合入口。
|
||||||
|
- [text/README.md](./text/README.md):文本、英文残留、乱码审计系列的融合入口。
|
||||||
|
|
||||||
|
## 专项审计
|
||||||
|
|
||||||
|
- [FUNCTION_DESIGN_AUDIT_2026-04-03.md](./FUNCTION_DESIGN_AUDIT_2026-04-03.md):Function 体系分层、职责边界和当前结构问题。
|
||||||
|
- [FUNCTION_REQUIREMENT_COMPLETENESS_AUDIT_2026-04-14.md](./FUNCTION_REQUIREMENT_COMPLETENESS_AUDIT_2026-04-14.md):Function 相关文档需求与当前实现对齐核查。
|
||||||
|
- [FUNCTION_TEST_AUDIT_2026-04-14.md](./FUNCTION_TEST_AUDIT_2026-04-14.md):Function 运行时测试补充、已确认 bug 与当前验证结果。
|
||||||
|
- [FUNCTION_RUNTIME_FULL_TEST_AUDIT_2026-04-16.md](./FUNCTION_RUNTIME_FULL_TEST_AUDIT_2026-04-16.md):Function 运行时完整测试、服务端承接验证与当前门禁缺口。
|
||||||
|
- [ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md](./ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md):物品生成与 Build 标签系统对 PRD 的落地情况。
|
||||||
|
- [CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md](./CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md):自定义世界创作工具当前问题、体验断层和优化优先级审计。
|
||||||
|
- [AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md](./AGENT_TO_DRAFT_TO_WORLD_PIPELINE_AUDIT_2026-04-20.md):Agent 聊天、草稿生成、作品库存储与进入世界之间的断点、多 pipeline、冗余与未实装项审计。
|
||||||
|
- [CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md](./CHARACTER_ASSET_PROMPT_CHAIN_AUDIT_2026-04-20.md):角色资产默认描述文本、正式图像/动作 prompt、共享模板与保留接口的分层与冗余审计。
|
||||||
|
- [engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md):对 `2026-04-19` 工程清理审计的当前仓库复核,区分已完成项、仍存边界问题和新的热点迁移。
|
||||||
|
- [engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md):未引用垃圾、旧入口残留、前后端双份真相与后端迁移项的专项审计。
|
||||||
|
|
||||||
|
## 推荐使用方式
|
||||||
|
|
||||||
|
1. 先读系列总览,确认“最新结论在哪一份”。
|
||||||
|
2. 再按需要进入具体日期文档,查看当时的证据和上下文。
|
||||||
|
3. 做方案设计前,优先把对应审计文档看完,避免重复踩已知问题。
|
||||||
@@ -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` 的归一化读取,而不是增加中间转换结构。
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
# 当前工程优化点盘点(2026-04-20)
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 0. 盘点目标
|
||||||
|
|
||||||
|
这份文档用于回答一个更直接的问题:
|
||||||
|
|
||||||
|
**基于当前仓库状态,接下来最值得投入工程时间的优化点是什么。**
|
||||||
|
|
||||||
|
本轮只做文档盘点,不直接修改业务代码;结论同时参考了当前工作区现状。
|
||||||
|
需要注意,仓库当前存在一批未提交改动,尤其集中在 `custom world`、`assets`、`platform shell` 相关模块,所以本文更强调“优先级与切入方式”,而不是要求做大范围整仓改写。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 当前快照
|
||||||
|
|
||||||
|
## 1.1 本轮复核方式
|
||||||
|
|
||||||
|
本轮主要复核了以下内容:
|
||||||
|
|
||||||
|
1. 现有工程优化审计文档与目录索引
|
||||||
|
2. `package.json`、`vite.config.ts`、`.eslintrc.cjs` 等门禁脚本
|
||||||
|
3. 当前前端、后端、脚本目录的大文件热点
|
||||||
|
4. 运行时、鉴权、自定义世界、资产链路的边界实现
|
||||||
|
5. 当前 `typecheck / lint / build` 状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.2 当前门禁结果
|
||||||
|
|
||||||
|
| 项目 | 结果 | 当前判断 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `npm run typecheck` | 失败 | 当前第一优先级问题,类型基线已失真 |
|
||||||
|
| `npm run lint:eslint` | 失败 | `136` 个 error、`4` 个 warning,且 `95` 个可自动修复 |
|
||||||
|
| `npm run build` | 通过 | 发布链路未红,但体积压力仍明显存在 |
|
||||||
|
|
||||||
|
### 关键说明
|
||||||
|
|
||||||
|
当前状态和 `2026-04-10` 那轮“build warning 直接拦截”的状态不同:
|
||||||
|
|
||||||
|
1. **构建现在可以通过。**
|
||||||
|
2. **真正变成第一阻塞项的是 `typecheck` 与 `lint`。**
|
||||||
|
3. **构建虽然通过,但主包、功能包、CSS 体积依然偏重,说明性能类优化仍然值得做。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1.3 当前热点文件快照
|
||||||
|
|
||||||
|
本轮按源码目录统计的大文件热点如下:
|
||||||
|
|
||||||
|
| 文件 | 当前行数 | 判断 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `6122` | 当前前端最大热点 |
|
||||||
|
| `server-node/src/app.test.ts` | `3568` | 后端测试聚合度过高 |
|
||||||
|
| `server-node/src/modules/assets/characterAssetRoutes.ts` | `2802` | 资产路由职责过重 |
|
||||||
|
| `src/services/ai.ts` | `2432` | 浏览器侧 AI 编排仍然偏重 |
|
||||||
|
| `server-node/src/modules/story/storyActionRoutes.test.ts` | `2402` | 运行时路由测试聚合度过高 |
|
||||||
|
| `src/data/npcInteractions.ts` | `2274` | NPC 规则数据仍然集中 |
|
||||||
|
| `src/prompts/storyPromptBuilders.ts` | `1728` | prompt 构造成为新的复杂度中心 |
|
||||||
|
| `server-node/src/modules/custom-world/runtimeProfile.ts` | `1623` | custom world runtime 编译热点 |
|
||||||
|
| `src/hooks/story/npcEncounterActions.ts` | `1582` | NPC 行动流仍然偏重 |
|
||||||
|
| `src/components/game-shell/PlatformHomeView.tsx` | `1474` | 平台首页壳层继续膨胀 |
|
||||||
|
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `1418` | 前置选择流程职责过多 |
|
||||||
|
| `src/services/customWorld.ts` | `1383` | 自定义世界服务虽然收缩,但仍偏大 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 结论先行
|
||||||
|
|
||||||
|
当前仓库的优化重点,已经不是“继续清旧 Vite 插件链路”或者“继续讨论前后端是否要分离”。
|
||||||
|
|
||||||
|
更准确地说,当前最值得做的优化点已经收敛成四类:
|
||||||
|
|
||||||
|
1. **先恢复可信的工程基线。**
|
||||||
|
`typecheck` 与 `lint` 当前都是红线,继续扩功能会放大返工成本。
|
||||||
|
2. **拆掉正在持续膨胀的新热点。**
|
||||||
|
热点已经从早期运行时主链,迁移到 `custom world`、`asset routes`、`platform shell`、`prompt builders`。
|
||||||
|
3. **继续把前端退出“运行时真相”和“鉴权真相”。**
|
||||||
|
当前前端仍保留本地快照镜像与自动登录凭证持久化。
|
||||||
|
4. **补一轮入口归档,减少疑似孤岛模块和大测试聚合文件。**
|
||||||
|
这部分不一定最急,但会持续拉低仓库可维护性。
|
||||||
|
|
||||||
|
一句话判断:
|
||||||
|
|
||||||
|
**当前最值得投入的不是横向加功能,而是把质量门禁重新拉绿,再把 custom world / asset / platform 这批新复杂度中心拆开。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 优化点清单
|
||||||
|
|
||||||
|
## 3.1 P0:先恢复类型基线
|
||||||
|
|
||||||
|
这是当前最优先的工程优化点。
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
`npm run typecheck` 当前失败,主要问题集中在两类:
|
||||||
|
|
||||||
|
1. `CustomWorldCampScene` 结构漂移
|
||||||
|
- `src/components/CustomWorldEntityEditorModal.test.tsx`
|
||||||
|
- `src/data/customWorldLibrary.ts`
|
||||||
|
- `src/services/customWorld.ts`
|
||||||
|
- `src/services/customWorldCamp.ts`
|
||||||
|
2. 局部实现与类型定义不同步
|
||||||
|
- `src/components/auth/AccountModal.test.tsx` 的测试数据缺少新增字段
|
||||||
|
- `src/components/game-canvas/GameCanvasShared.tsx` 引用了未定义的 `DEFAULT_IMAGE_STYLE`
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
1. 类型系统已经不能提供可信回归信号。
|
||||||
|
2. 自定义世界链路当前正在迭代,如果继续在红线状态叠加修改,后续会反复出现“改 A 崩 B”的情况。
|
||||||
|
3. 测试 fixture 与正式类型脱节,会让测试文件逐渐失去文档价值。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 先补一个统一的 `CustomWorldCampScene` 构造/归一化入口,禁止在多个文件里手写不完整字面量。
|
||||||
|
2. 把 `auth`、`custom world` 的测试 fixture 改成工厂函数,避免字段新增后多处漏改。
|
||||||
|
3. 单独清掉 `GameCanvasShared.tsx` 这类“编译即失败”的确定性问题,优先恢复 `typecheck` 绿色基线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.2 P0:恢复 lint 可信度,区分机械问题和真实问题
|
||||||
|
|
||||||
|
这项和类型基线同级。
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
`npm run lint:eslint` 当前结果是:
|
||||||
|
|
||||||
|
- `136` 个 error
|
||||||
|
- `4` 个 warning
|
||||||
|
- 其中 `95` 个问题可自动修复
|
||||||
|
|
||||||
|
当前 lint 问题明显分成两层:
|
||||||
|
|
||||||
|
1. 机械问题
|
||||||
|
- import 排序
|
||||||
|
- export 排序
|
||||||
|
- 未使用导入
|
||||||
|
2. 真实问题
|
||||||
|
- `server-node/src/modules/inventory/inventoryStoryActionService.ts` 出现 React Hook 规则错误
|
||||||
|
- `server-node/src/migrate.ts` 仍触发 `no-console`
|
||||||
|
- `packages/shared/src/http.ts` 触发 `@typescript-eslint/ban-types`
|
||||||
|
- 若干文件存在真正未使用变量、转义和规则误配问题
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
1. 当前 lint 信号噪音仍然较高,不利于 review。
|
||||||
|
2. 真实问题会被大量机械问题掩盖。
|
||||||
|
3. 团队会更倾向于跳过 lint,而不是信任 lint。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 先跑一轮仅机械修复的清理批次,优先吃掉 import sort、unused imports 这类低风险项。
|
||||||
|
2. 再单独处理 Hook 误用、共享契约类型、脚本规则豁免这类语义问题。
|
||||||
|
3. 之后把“自动可修复问题”与“必须人工处理的问题”拆成两个门禁视角,减少下次再次堆积。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.3 P1:拆 custom world / asset / platform 新热点
|
||||||
|
|
||||||
|
这是当前最有性价比的结构性优化点。
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
当前复杂度最高的业务热点,已经集中在这些模块:
|
||||||
|
|
||||||
|
1. `src/components/CustomWorldEntityEditorModal.tsx`
|
||||||
|
2. `server-node/src/modules/assets/characterAssetRoutes.ts`
|
||||||
|
3. `src/services/ai.ts`
|
||||||
|
4. `src/prompts/storyPromptBuilders.ts`
|
||||||
|
5. `server-node/src/modules/custom-world/runtimeProfile.ts`
|
||||||
|
6. `src/components/game-shell/PlatformHomeView.tsx`
|
||||||
|
7. `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
8. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
|
||||||
|
### 问题本质
|
||||||
|
|
||||||
|
这些文件并不是单纯“代码多”,而是同时承载了多类职责:
|
||||||
|
|
||||||
|
1. UI 状态
|
||||||
|
2. 领域规则
|
||||||
|
3. 请求编排
|
||||||
|
4. 文本构造
|
||||||
|
5. 运行时映射
|
||||||
|
6. 面板切换与流程控制
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. `CustomWorldEntityEditorModal.tsx`
|
||||||
|
- 先按“实体列表/表单区/资源区/高级设置/预览区”拆组件
|
||||||
|
- 再把数据准备与提交编排抽成 hook
|
||||||
|
2. `characterAssetRoutes.ts`
|
||||||
|
- 拆成 route、prompt payload、job orchestration、产物发布、错误响应五层
|
||||||
|
3. `PlatformHomeView.tsx` 与 `PreGameSelectionFlow.tsx`
|
||||||
|
- 把页面壳层、数据加载、卡片渲染、弹层控制拆开
|
||||||
|
4. `storyPromptBuilders.ts` 与 `runtimeProfile.ts`
|
||||||
|
- 把“模板片段”“上下文归一化”“规则裁剪”“最终拼接”分层
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.4 P1:继续控制构建产物体积
|
||||||
|
|
||||||
|
构建虽通过,但体积已经给出明显信号。
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
本轮 `npm run build` 输出里,几个值得关注的点是:
|
||||||
|
|
||||||
|
1. `dist/assets/AuthenticatedApp-*.js`:`794.77 kB`
|
||||||
|
2. `dist/assets/index-*.js`:`197.44 kB`
|
||||||
|
3. `dist/assets/CustomWorldResultView-*.js`:`163.38 kB`
|
||||||
|
4. `dist/assets/ai-*.js`:`131.73 kB`
|
||||||
|
5. `dist/assets/PreGameSelectionFlow-*.js`:`96.39 kB`
|
||||||
|
6. `dist/assets/index-*.css`:`201.44 kB`
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
1. 虽然还没触发新的 build gate 红线,但首屏、缓存和移动端体验会继续承压。
|
||||||
|
2. `AuthenticatedApp` 主包偏大,说明平台壳层仍然装入了过多首屏不必需能力。
|
||||||
|
3. CSS 体积继续上涨,说明样式正在跨模块相互堆叠。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 继续把 custom world、asset studio、平台详情页、角色资产工具从主壳层路径中抽离。
|
||||||
|
2. 审查 `ai.ts`、`custom world result view`、`pregame selection` 是否还能再延迟加载。
|
||||||
|
3. 对全局样式做一次按模块归属清理,减少公共样式无限增长。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.5 P1:继续收紧前端与后端边界
|
||||||
|
|
||||||
|
这项已经不是“要不要做”的问题,而是“还剩多少尾巴没收完”。
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
1. `src/services/apiClient.ts`
|
||||||
|
- 当前仍把 `access token`
|
||||||
|
- 自动登录用户名
|
||||||
|
- 自动登录密码
|
||||||
|
写入 `window.localStorage`
|
||||||
|
2. `src/hooks/story/runtimeStoryCoordinator.ts`
|
||||||
|
- 当前仍会在调用后端运行时前先 `putSaveSnapshot(...)`
|
||||||
|
- 响应后继续 `rehydrateSavedSnapshot(...)`
|
||||||
|
3. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
- 当前仍从前端动作流触发 `generateQuestForNpcEncounter(...)`
|
||||||
|
- 说明 NPC 任务“换单/重抽”分支尚未完全后端化
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
1. 前端仍保留了一部分运行时真相与鉴权真相。
|
||||||
|
2. 自动登录凭证持久化在边界和安全上都不理想。
|
||||||
|
3. 运行时快照前置写入,会让“前端镜像状态”和“后端会话状态”继续纠缠。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 优先移除自动登录用户名/密码本地持久化,收敛到服务端 session / refresh 机制。
|
||||||
|
2. 把运行时快照改为“展示缓存”而不是“提交前真相源”。
|
||||||
|
3. 把 NPC 任务更换动作补齐到后端 runtime/session 边界,不再由前端直接发起生成决策。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.6 P2:做一次疑似孤岛模块与旧入口归档
|
||||||
|
|
||||||
|
这项不一定最紧急,但现在做会明显降低后续维护噪音。
|
||||||
|
|
||||||
|
### 当前现象
|
||||||
|
|
||||||
|
从当前入口关系看,以下模块值得做一次正式复核:
|
||||||
|
|
||||||
|
1. `src/components/GameShell.tsx`
|
||||||
|
2. `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||||
|
3. `src/components/custom-world-home/CustomWorldCreationLauncherModal.tsx`
|
||||||
|
4. `src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx`
|
||||||
|
5. `src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx`
|
||||||
|
6. `src/hooks/story/storyBootstrap.ts`
|
||||||
|
7. `src/hooks/useEquipmentFlow.ts`
|
||||||
|
8. `src/hooks/useForgeFlow.ts`
|
||||||
|
9. `src/hooks/useInventoryFlow.ts`
|
||||||
|
10. `src/services/typewriter.ts`
|
||||||
|
|
||||||
|
### 当前判断
|
||||||
|
|
||||||
|
这批模块不一定全部是垃圾代码,但至少说明一件事:
|
||||||
|
|
||||||
|
**仓库里仍然存在一批“不是正式入口、也没有清晰归档标签”的过渡实现。**
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
把这类模块统一分成三类:
|
||||||
|
|
||||||
|
1. 正式保留并接回入口
|
||||||
|
2. 明确标记为实验稿
|
||||||
|
3. 直接归档或删除
|
||||||
|
|
||||||
|
这样可以减少后续开发时的误判成本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.7 P2:拆测试聚合文件,恢复测试的定位能力
|
||||||
|
|
||||||
|
当前测试文件也已经出现“大一统热点”。
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
1. `server-node/src/app.test.ts`:`3568` 行
|
||||||
|
2. `server-node/src/modules/story/storyActionRoutes.test.ts`:`2402` 行
|
||||||
|
3. `server-node/src/modules/assets/characterAssetRoutes.test.ts`:`1235` 行
|
||||||
|
4. `src/hooks/story/npcEncounterActions.test.ts`:`1199` 行
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
1. 失败定位成本高。
|
||||||
|
2. fixture 复用差,字段一变容易整片测试跟着漂移。
|
||||||
|
3. 测试文件本身开始变成新的维护热点。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 按领域动作拆测试文件,而不是继续堆到单一总测文件中。
|
||||||
|
2. 补 fixture builder / factory,减少字面量散落。
|
||||||
|
3. 对 `runtime / auth / custom world / assets` 这几条链路增加更明确的契约测试分层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 推荐执行顺序
|
||||||
|
|
||||||
|
如果只按工程收益排序,建议按下面的顺序推进:
|
||||||
|
|
||||||
|
1. 先修 `typecheck`
|
||||||
|
2. 再把 `lint` 分成机械修复和语义修复两轮
|
||||||
|
3. 然后拆 `custom world / asset / platform` 热点
|
||||||
|
4. 再继续收前端运行时与鉴权边界
|
||||||
|
5. 最后处理孤岛模块归档和测试拆分
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 当前不建议优先做的事
|
||||||
|
|
||||||
|
1. 不建议在 `typecheck` 和 `lint` 仍为红线时继续横向扩功能。
|
||||||
|
2. 不建议直接在 `CustomWorldEntityEditorModal.tsx`、`characterAssetRoutes.ts`、`PlatformHomeView.tsx` 里继续堆新逻辑。
|
||||||
|
3. 不建议把 bundle 体积问题简单理解为“先放宽阈值”,当前更适合继续拆职责和延迟加载。
|
||||||
|
4. 不建议在未确认入口关系前随手删除可疑旧模块,先做归档分类更稳。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本文依据
|
||||||
|
|
||||||
|
文档依据:
|
||||||
|
|
||||||
|
1. `docs/audits/engineering/README.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. `package.json`
|
||||||
|
2. `.eslintrc.cjs`
|
||||||
|
3. `vite.config.ts`
|
||||||
|
4. `scripts/build-gate.mjs`
|
||||||
|
5. `src/App.tsx`
|
||||||
|
6. `src/services/apiClient.ts`
|
||||||
|
7. `src/hooks/story/runtimeStoryCoordinator.ts`
|
||||||
|
8. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
9. 当前源码大文件体量扫描结果
|
||||||
|
10. `npm run typecheck`
|
||||||
|
11. `npm run lint:eslint`
|
||||||
|
12. `npm run build`
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
# 工程清理与后端边界审计(2026-04-19)
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 0.1 执行回填(2026-04-19)
|
||||||
|
|
||||||
|
本文审计项 `3.2` 与 `4.4` 已于 `2026-04-19` 当日完成首轮处置:
|
||||||
|
|
||||||
|
1. 已删除 `scripts/dev-server/localApiPlugins.ts`
|
||||||
|
2. 已删除 `scripts/dev-server/characterAssetStudioPlugins.ts`
|
||||||
|
3. 已删除 `scripts/dev-server/qwenSpriteSheetToolPlugins.ts`
|
||||||
|
4. `scripts/dev-server/` 目录仅保留迁移说明,不再保留旧 Vite 本地 API 实现代码
|
||||||
|
5. 当前正式入口统一为 `scripts/dev-node.mjs + vite proxy + server-node/src/modules/**`
|
||||||
|
|
||||||
|
本文其余段落保留为本次审计时的原始问题快照,用于解释为什么要做这轮删除。
|
||||||
|
|
||||||
|
## 0.2 执行回填(2026-04-19,仓库噪音产物)
|
||||||
|
|
||||||
|
本文审计项 `3.1` 已于 `2026-04-19` 当日完成首轮处置:
|
||||||
|
|
||||||
|
1. 已从版本库删除以下根目录历史扫描/截图产物:
|
||||||
|
- `npc-editor-dom.html`
|
||||||
|
- `npc-editor-shot.png`
|
||||||
|
- `temp-write-check.txt`
|
||||||
|
- `tmp_character_presets_scan.txt`
|
||||||
|
- `tmp_jsx_text_scan.txt`
|
||||||
|
- `tmp_runtime_text_scan.txt`
|
||||||
|
- `tmp_text_candidates.txt`
|
||||||
|
- `tmp_text_candidates_refined.txt`
|
||||||
|
- `tmp_visible_props_scan.txt`
|
||||||
|
- `tmp_volc_seedance_doc.html`
|
||||||
|
2. 已从版本库删除 `scripts/__pycache__/generate-build-tag-similarity.cpython-313.pyc`。
|
||||||
|
3. 已清理本地工作区中的 `.codex-*.log`、`.preview.*`、`npc-editor-console.log` 与 `temp-build-goal-check/`,清理前对应体量约为:
|
||||||
|
- 根目录噪音文件 `60` 个,约 `49.94 MB`
|
||||||
|
- `temp-build-goal-check/` 共 `15620` 个条目,约 `158.85 MB`
|
||||||
|
4. 已补齐 `.gitignore`、`.prettierignore` 与 `.eslintrc.cjs` 的忽略口径,显式覆盖 `tmp_*`、`tmp/`、`npc-editor-*`、`temp-write-check.txt`、`temp-build-goal-check/`、`__pycache__/`。
|
||||||
|
5. `scripts/dev-server/localApiPlugins.ts` 之外的后端边界收口项不在本轮噪音清理范围内,后续继续按本文第二至第四阶段推进。
|
||||||
|
|
||||||
|
## 0.3 执行回填(2026-04-19,运行时边界第一轮收口)
|
||||||
|
|
||||||
|
本文审计项 `4.1` 与 `5.1` 已于 `2026-04-19` 当日完成一轮工程收口:
|
||||||
|
|
||||||
|
1. `RuntimeStoryOptionView` 现在由后端直接附带 `interaction` 元数据。
|
||||||
|
2. `server-node/src/modules/story/runtimeSession.ts` 已成为 runtime option interaction 的唯一构建位置。
|
||||||
|
3. `src/services/runtimeStoryService.ts` 不再根据 `currentEncounter + functionId` 在前端本地重建一份 interaction 映射。
|
||||||
|
4. `/api/custom-world/scene-image` 已补齐服务端 prompt 兜底组装能力,允许前端只提交 `profile + landmark + userPrompt` 上下文。
|
||||||
|
5. `src/services/aiService.ts` 的场景图 SDK 已改为直接调用后端接口,不再为了该链路动态加载 `src/services/ai.ts`。
|
||||||
|
|
||||||
|
## 0.4 执行回填(2026-04-19,自定义世界后端边界第二轮收口)
|
||||||
|
|
||||||
|
本文审计项 `5.2` 与“第三阶段第 4 条:清理 `server-node -> src/**` 的反向依赖”已于 `2026-04-19` 当日完成第二轮工程收口:
|
||||||
|
|
||||||
|
1. `server-node/src/modules/custom-world/` 已新增服务端自持 runtime 模块,承接:
|
||||||
|
- `creator intent` 归一化
|
||||||
|
- `anchorPack / lockState` 推导
|
||||||
|
- custom world framework/profile compile 与 normalize
|
||||||
|
2. `server-node/src/modules/ai/customWorldOrchestrator.ts` 与 `server-node/src/services/customWorldAgentFoundationDraftService.ts` 已不再运行时依赖:
|
||||||
|
- `src/services/customWorld.js`
|
||||||
|
- `src/services/customWorldBuilder.js`
|
||||||
|
- `src/services/customWorldCreatorIntent.js`
|
||||||
|
- `src/types.js`
|
||||||
|
3. `server-node/src/prompts/customWorldPrompts.ts` 已成为后端自持的 custom world prompt source,`scene image` 与 `foundation draft` 相关 builder 不再从前端 `src/prompts/customWorldPrompts.ts` 反向 import。
|
||||||
|
4. 本轮只迁移 prompt source 位置,没有改动任何 custom world 提示词正文,也没有改动功能需求。
|
||||||
|
|
||||||
|
## 0.5 执行回填(2026-04-20,NPC 待接委托正式接取收口)
|
||||||
|
|
||||||
|
本文审计项 `5.3` 已于 `2026-04-20` 完成一轮补充收口:
|
||||||
|
|
||||||
|
1. `src/hooks/story/npcEncounterActions.ts` 中“聊天里的待接委托正式接取”已不再由前端本地直接写入:
|
||||||
|
- `quests`
|
||||||
|
- `runtimeStats.questsAccepted`
|
||||||
|
- `npcChatState.pendingQuestOffer`
|
||||||
|
2. `server-node/src/modules/quest/questStoryActionService.ts` 现在会优先读取服务端快照里已保存的 `pendingQuestOffer.quest`,按当前聊天态中已经展示给玩家的那份委托完成正式接取。
|
||||||
|
3. `server-node/src/modules/story/storyActionService.ts` 已补齐待接委托接取后的聊天态投影:
|
||||||
|
- 保留 NPC 对话展示模式
|
||||||
|
- 清空 `pendingQuestOffer`
|
||||||
|
- 回到既有的三条自由追问建议
|
||||||
|
4. 本轮没有新增任何 runtime functionId,也没有改动任务生成提示词或任务需求,只是把既有“接任务”正式结算权收回到后端。
|
||||||
|
|
||||||
|
## 0.6 执行回填(2026-04-20,NPC 聊天任务草案与浏览器 LLM fallback 收口)
|
||||||
|
|
||||||
|
本文审计项 `5.1` 与 `5.3` 已于 `2026-04-20` 完成一轮补充收口:
|
||||||
|
|
||||||
|
1. `server-node/src/modules/ai/chatOrchestrator.ts` 现在会基于 `NPC chat turn` 的运行时上下文,在后端判断是否触发 `pendingQuestOffer`,并把 quest draft 与引导文案一并回填给前端。
|
||||||
|
2. `src/hooks/story/npcEncounterActions.ts` 不再在 NPC 单轮聊天完成后本地调用 `generateQuestForNpcEncounter(...)` 再决定是否挂出待接委托。
|
||||||
|
3. `src/services/questDirector.ts` 浏览器端在后端失败时不再退回本地 LLM 生成 quest draft,而是直接走 deterministic fallback compile。
|
||||||
|
4. `src/services/runtimeItemAiDirector.ts` 浏览器端在后端失败时不再退回本地 LLM 生成 runtime item intent,而是直接返回 deterministic fallback intents。
|
||||||
|
5. 本轮仍未改动任何业务提示词正文,也没有改动 quest / runtime item 的需求能力面,只是继续清理浏览器里的正式 AI orchestration 残留。
|
||||||
|
|
||||||
|
## 0. 审计目标
|
||||||
|
|
||||||
|
本次审计只回答四类问题:
|
||||||
|
|
||||||
|
1. 项目里哪些内容已经是高置信度的垃圾、临时产物或无入口代码。
|
||||||
|
2. 哪些实现属于双份真相、重复映射或旧链路残留。
|
||||||
|
3. 哪些前端代码仍然承担了应迁移到 Express 后端的职责。
|
||||||
|
4. 哪些文件已经大到会持续拖累迭代效率,需要优先拆分。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 结论先行
|
||||||
|
|
||||||
|
当前仓库的主要问题不是“有一些小工具没人用”,而是四类结构性噪音同时存在:
|
||||||
|
|
||||||
|
1. **仓库噪音产物仍然很多。**
|
||||||
|
根目录残留了大量 `.codex-*.log`、`tmp_*`、旧截图/HTML,以及 `temp-build-goal-check/` 这类大体量检查产物,已经不是单个文件层面的脏数据,而是在持续污染工程视野。
|
||||||
|
2. **旧入口和新入口并存,形成了明显的冗余链路。**
|
||||||
|
`scripts/dev-server/localApiPlugins.ts` 已经退出当前正式开发入口,但仍保留了 LLM proxy、JSON 写盘、资产发布等整套旧 Vite 本地 API 机制。
|
||||||
|
3. **前端仍然承载了过多运行时规则与 AI 编排。**
|
||||||
|
`src/services/ai.ts`、`src/services/customWorld.ts`、`src/hooks/story/npcEncounterActions.ts` 这类文件,仍在浏览器里承担 prompt 组装、规则判定、奖励结算、剧情推进等职责。
|
||||||
|
4. **后端边界还没有真正闭合。**
|
||||||
|
`server-node` 虽然已经承接了大量路由和运行时动作,但仍直接 import `src/services/customWorld*.ts` 和 `src/types.ts`,说明后端领域层还没有完全从前端目录中独立出来。
|
||||||
|
|
||||||
|
一句话判断:
|
||||||
|
|
||||||
|
**这轮优先级不该再是继续堆功能,而是先清仓库噪音与无入口孤岛,再把前后端双份真相收口,最后拆新的巨型热点文件。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本次审计方法与口径
|
||||||
|
|
||||||
|
### 2.1 方法
|
||||||
|
|
||||||
|
本次审计结合了四类证据:
|
||||||
|
|
||||||
|
1. 文档基线:
|
||||||
|
- `docs/audits/engineering/README.md`
|
||||||
|
- `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||||
|
- `scripts/dev-server/README.md`
|
||||||
|
2. 当前入口核对:
|
||||||
|
- `src/main.tsx`
|
||||||
|
- `src/routing/appRoutes.tsx`
|
||||||
|
- `src/App.tsx`
|
||||||
|
- `package.json`
|
||||||
|
- `server-node/package.json`
|
||||||
|
3. 静态依赖扫描:
|
||||||
|
- 对 `src/`、`server-node/src/`、`packages/shared/src/`、`scripts/` 共 `650` 个 TS/JS 文件做本地依赖图扫描。
|
||||||
|
4. 定向 grep:
|
||||||
|
- 核对旧 dev 插件入口、后端跨层 import、localStorage 使用、运行时快照双写、重复映射代码。
|
||||||
|
|
||||||
|
### 2.2 口径说明
|
||||||
|
|
||||||
|
为避免误判,本次审计明确排除了两类对象:
|
||||||
|
|
||||||
|
1. **包脚本入口**:例如 `scripts/build-gate.mjs`、`scripts/check-encoding.mjs`、`server-node/build.mjs` 这类由 `package.json` 直接执行的脚本,不因“无 import”而判为垃圾。
|
||||||
|
2. **字符串路径消费的资源**:例如 `src/data/itemOverrides.json`、`src/data/monsterOverrides.json` 会被校验脚本和 editor route 以文件路径读取,不按“无 import”处理。
|
||||||
|
|
||||||
|
另外,当前工作区存在未提交改动,因此本次结论以**已纳入当前主链且能确认未接线/重复/越界的内容**为主,不把明显的当日 WIP 文件计入垃圾结论。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 高置信度垃圾、临时产物与无入口代码
|
||||||
|
|
||||||
|
## 3.1 仓库噪音产物已经到了需要集中清理的程度
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
| 项目 | 当前证据 | 判断 |
|
||||||
|
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
||||||
|
| 根目录日志/临时文件 | 根目录命中 `60` 个 `.codex-*.log`、`.preview.*`、`tmp_*`、`npc-editor-*`、`temp-write-check.txt`,合计约 `52.36 MB` | 已经不是偶发临时文件,而是长期堆积的开发残留 |
|
||||||
|
| `temp-build-goal-check/` | 当前包含 `15099` 个文件,合计约 `166.56 MB` | 大体量检查产物,应该移出主工程视野 |
|
||||||
|
| Python 缓存 | 当前存在 `scripts/__pycache__/` | 纯缓存产物,不应长期留在仓库工作区中 |
|
||||||
|
|
||||||
|
### 影响
|
||||||
|
|
||||||
|
1. 根目录信噪比明显下降,真实工程文件被大量一次性产物淹没。
|
||||||
|
2. `temp-build-goal-check/` 虽然已被 `.gitignore` 和 `vite.config.ts` 的 watch 忽略模式覆盖,但 `.eslintrc.cjs` 的 `ignorePatterns` 里没有对应口径,仍存在工具口径不一致问题。
|
||||||
|
3. 这类目录会持续干扰检索、review、lint 判断和本地扫描速度。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 把根目录临时日志、扫描 txt/html、旧截图统一迁到单独的 `tmp/` 或本地缓存目录,默认不留在仓库根目录。
|
||||||
|
2. 把 `temp-build-goal-check/` 改成真正的外置检查产物目录,或者在 lint/脚本口径上一起排除。
|
||||||
|
3. 清理 `scripts/__pycache__/`,并统一补上 Python 缓存忽略规则。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.2 旧 Vite 本地 API 插件链已经退出主入口,但仍保留整套旧实现
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
1. `scripts/dev-server/README.md` 已明确写明:`scripts/dev-server/**` 不再是当前开发入口,只保留为迁移参考。
|
||||||
|
2. `scripts/dev-server/localApiPlugins.ts` 当前仍有 `1664` 行。
|
||||||
|
3. 仓库内已经找不到 `localApiPlugins` 的实际代码入口引用,当前只剩文档引用。
|
||||||
|
4. 该文件内部仍然同时定义和拼装:
|
||||||
|
- `createLlmProxyPlugin`
|
||||||
|
- `createJsonFileEditorPlugin`
|
||||||
|
- `createCustomWorldSceneImagePlugin`
|
||||||
|
- `createCharacterVisualPublishPlugin`
|
||||||
|
- `createCharacterAnimationPublishPlugin`
|
||||||
|
- `createCharacterAssetStudioPlugins`
|
||||||
|
- `createQwenSpriteSheetToolPlugins`
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这不是“一个小工具暂时没用”,而是**整条旧 editor/assets 本地 API 链路仍然完整保留在仓库里**。它在工程上已经属于高置信度的历史残留。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 如果只保留迁移证据,建议把 `scripts/dev-server/localApiPlugins.ts` 和相关说明迁到 `docs/reference/` 或单独的 `archive/` 目录。
|
||||||
|
2. 如果确实还要保留参考代码,至少要在文件顶部加更强的“只读参考、禁止继续扩展”标识,并从主工程扫描面上进一步隔离。
|
||||||
|
3. 不建议继续在这条旧链路里新增任何 `/api/*` 能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.3 当前存在一批“无运行时入口”或“仅测试引用”的孤岛模块
|
||||||
|
|
||||||
|
### 高置信度无入口/仅测试引用清单
|
||||||
|
|
||||||
|
| 模块 | 证据 | 判断 |
|
||||||
|
| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||||
|
| `src/components/GameShell.tsx` | 文件体量 `761` 行;当前 `src/App.tsx` 只接入 `components/game-shell/GameShellRuntime.tsx`;仓库内无其它 import | 旧版壳层残留 |
|
||||||
|
| `src/components/custom-world-home/CustomWorldCreationHub.tsx` | 仅被 `CustomWorldCreationHub.test.tsx` 和 `CustomWorldCreationHub.interaction.test.tsx` 引用;`src/routing/appRoutes.tsx` 只有 `game` 和 `qwen-sprite-tool` 两条路由 | 已做出 UI,但未进入正式入口 |
|
||||||
|
| `src/components/custom-world-home/CustomWorldCreationLauncherModal.tsx` | 当前无运行时引用 | 同属未接线入口壳层 |
|
||||||
|
| `src/components/custom-world-agent/*` 中 `9` 个子模块 | 当前合计约 `826` 行;典型文件包括 `CustomWorldAgentLauncherModal.tsx`、`CustomWorldAgentDraftDrawer.tsx`、`CustomWorldAgentLockBar.tsx`、`CustomWorldAgentQuickActions.tsx`、`CustomWorldAgentSummaryPanel.tsx`;部分文件完全无引用,部分仅被测试引用 | 处于“做了一部分 UI,但未进入主链”的孤岛状态 |
|
||||||
|
| `src/hooks/story/storyBootstrap.ts` | `250` 行,仓库内只定义不消费 | 已被新流程替代的可能性高 |
|
||||||
|
| `src/hooks/useEquipmentFlow.ts` / `useForgeFlow.ts` / `useInventoryFlow.ts` | 合计约 `393` 行,当前无运行时引用 | 旧流转层残留 |
|
||||||
|
| `src/editor/shared/cloneValue.ts` / `EditorEmptyState.tsx` / `EditorSelectionCard.tsx` / `useJsonSave.ts` | 当前无运行时引用 | editor 旧共享层碎片 |
|
||||||
|
| `src/services/customWorldPresentation.stub.ts` | 当前无引用,且文件本身就是 stub | 高置信度占位残留 |
|
||||||
|
| `src/services/typewriter.ts` | 当前无引用,仅提供一个 `getTypewriterDelay` | 已被其它链路内联实现替代 |
|
||||||
|
| `src/data/buildTagSimilarity.generated.ts` | 当前 `823` 行,仅能被生成脚本自身检索到,没有消费方 | 生成产物未接入任何业务链路 |
|
||||||
|
| `src/data/customWorldCharacterLoadout.stub.ts` | 当前无引用,且实现只返回空数组 | 占位残留 |
|
||||||
|
| `src/components/DeveloperTeamModal.tsx` / `src/components/LazySkillEffectPreview.tsx` | 当前无运行时引用 | 小体量零散孤岛 |
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这批文件不一定都应该“立刻删除”,但它们已经满足两个至少其一:
|
||||||
|
|
||||||
|
1. 当前正式入口完全不消费。
|
||||||
|
2. 只剩测试在消费,本体没有真实运行时位置。
|
||||||
|
|
||||||
|
所以它们至少都应该进入以下三选一处理:
|
||||||
|
|
||||||
|
1. 立即归档/删除。
|
||||||
|
2. 明确接回正式入口。
|
||||||
|
3. 改名或迁目录,标明“实验稿/参考稿/未接线”身份。
|
||||||
|
|
||||||
|
### 特别提醒
|
||||||
|
|
||||||
|
`src/components/custom-world-home/` 和 `src/components/custom-world-agent/` 这两组文件里,存在**已经有一定 UI 完成度、但没有进入真实路由/流程**的情况。
|
||||||
|
这类文件最危险的点不是体量,而是会让后来者误以为“这块功能已经在主链上”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 冗余实现与双份真相
|
||||||
|
|
||||||
|
## 4.1 Story option interaction 映射在前后端各维护了一份
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
1. 前端 `src/services/runtimeStoryService.ts` 的 `buildRuntimeOptionInteraction` 维护了 `npcActionMap`、`treasureActionMap`。
|
||||||
|
2. 后端 `server-node/src/modules/story/storyActionService.ts` 的 `buildStoryOptionInteraction` 维护了几乎同构的一份 `npcActionMap`、`treasureActionMap`。
|
||||||
|
|
||||||
|
### 风险
|
||||||
|
|
||||||
|
1. 任何一个 functionId 增删改,前后端都要同步。
|
||||||
|
2. 一边先改、一边漏改时,表现层和运行时层会出现静默漂移。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
把 interaction/view model 映射收口到后端,前端只消费后端返回的结构,不再根据 `functionId` 本地重建一遍交互语义。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 浏览历史已经有后端接口,但前端仍维护本地真相与迁移状态
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
1. `src/components/game-shell/PreGameSelectionFlow.tsx` 中,`appendBrowseHistoryEntry` 先调用 `writePlatformBrowseHistory` 写本地,再调用 `upsertProfileBrowseHistory` 写后端。
|
||||||
|
2. 同文件启动阶段又会先读 `readPlatformBrowseHistory`,再根据 `hasPendingPlatformBrowseHistoryMigration` 把本地历史同步回后端。
|
||||||
|
3. 后端 `server-node/src/routes/runtimeRoutes.ts` 已经提供了 `/profile/browse-history` 路由,而前端 `src/services/storageService.ts` 也已有对应 API SDK。
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
当前浏览历史并不是单纯的“本地缓存”,而是**本地存储 + 远端持久化 + 迁移标记**三套状态并存。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 后端结果作为唯一真相源。
|
||||||
|
2. 前端如果要保留缓存,只保留一个明确的 cache wrapper,不再把它做成独立状态系统。
|
||||||
|
3. `markPlatformBrowseHistoryMigrated` 这种迁移标记应尽量在后端一次性收口,而不是长期停留在正式前端逻辑里。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 运行时快照依然由前端先落本地,再与后端会话互相回填
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
1. `src/hooks/story/runtimeStoryCoordinator.ts` 在读状态和提交 action 前都会先调用 `putSaveSnapshot`。
|
||||||
|
2. 同文件以及 `src/services/runtimeStoryService.ts` 又会在响应后多次 `rehydrateSavedSnapshot`。
|
||||||
|
3. 这意味着浏览器仍然在“后端 action 之前”先写一份自己的快照解释。
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这条链路说明当前运行时还处在**前端快照解释权没有完全退出**的过渡状态。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
1. 前端逐步退化为 view model 消费层。
|
||||||
|
2. 运行时快照、版本迁移、恢复解释权继续往后端收口。
|
||||||
|
3. 前端保留最小必要的离线展示缓存,但不再成为正式运行时状态真相来源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.4 旧 Vite 本地 API 与正式 Express 路由仍然形成重复能力面
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
1. `scripts/dev-server/localApiPlugins.ts` 里仍有 JSON 编辑、场景图生成、角色视觉发布、角色动作发布等插件。
|
||||||
|
2. 当前正式路径已经迁到:
|
||||||
|
- `server-node/src/modules/editor/**`
|
||||||
|
- `server-node/src/modules/assets/**`
|
||||||
|
3. `scripts/dev-server/README.md` 已明确说明旧链路只保留为迁移参考。
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这属于典型的**旧能力未删除,新能力已落地,双链路长期并存**。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
尽快把旧 Vite 本地 API 参考实现移出主工程扫描面,避免后续继续被误用或被误认为正式入口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 需要迁移到后端的代码
|
||||||
|
|
||||||
|
## 5.1 `src/services/ai.ts` 仍然承担了过多正式运行时职责
|
||||||
|
|
||||||
|
### 当前职责
|
||||||
|
|
||||||
|
`src/services/ai.ts` 当前约 `2632` 行,仍然同时承担:
|
||||||
|
|
||||||
|
1. function 可用性与 option 构造相关逻辑。
|
||||||
|
2. NPC 对话 / 招募 prompt 构造。
|
||||||
|
3. 自定义世界生成 prompt 与 JSON 修复请求。
|
||||||
|
4. 直接调用 `requestPlainTextCompletion` / `streamPlainTextCompletion`。
|
||||||
|
5. 浏览器内 fallback 与响应解析。
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这不是单纯的“前端请求 SDK”,而是**前端仍在承担正式运行时 AI orchestration**。
|
||||||
|
|
||||||
|
### 建议迁移方向
|
||||||
|
|
||||||
|
1. prompt 组装、模型调用、超时重试、JSON repair 继续收口到 `server-node/src/modules/ai/**`。
|
||||||
|
2. 前端只保留轻量 SDK 和展示态拼装。
|
||||||
|
3. fallback 如果必须保留,也应明确区分“开发兜底”与“正式运行时”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.2 `src/services/customWorld.ts` 仍然是前端侧的大型规则中心
|
||||||
|
|
||||||
|
### 当前职责
|
||||||
|
|
||||||
|
`src/services/customWorld.ts` 当前约 `2413` 行,仍然承担:
|
||||||
|
|
||||||
|
1. 世界框架与角色/地标 outline 归一化。
|
||||||
|
2. 世界属性 schema 生成。
|
||||||
|
3. `ownedSettingLayers` 归一化。
|
||||||
|
4. 最终世界 profile 校验。
|
||||||
|
5. fallback story graph/theme pack 生成。
|
||||||
|
|
||||||
|
### 当前越界证据
|
||||||
|
|
||||||
|
后端目前直接从以下文件 import 这些能力:
|
||||||
|
|
||||||
|
1. `server-node/src/modules/ai/customWorldOrchestrator.ts`
|
||||||
|
2. `server-node/src/services/customWorldAgentFoundationDraftService.ts`
|
||||||
|
|
||||||
|
它们仍直接引用:
|
||||||
|
|
||||||
|
1. `src/services/customWorld.js`
|
||||||
|
2. `src/services/customWorldBuilder.js`
|
||||||
|
3. `src/services/customWorldCreatorIntent.js`
|
||||||
|
4. `src/types.js`
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这说明自定义世界的核心领域规则仍然以**前端目录为事实源**,后端只是在反向复用。
|
||||||
|
|
||||||
|
### 建议迁移方向
|
||||||
|
|
||||||
|
1. `types/schema/contracts` 抽到 `packages/shared`。
|
||||||
|
2. 规则编译、校验、fallback 与 AI 编排迁到 `server-node`。
|
||||||
|
3. 前端只保留编辑器表现层和字段草稿态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.3 `src/hooks/story/npcEncounterActions.ts` 仍在浏览器里做任务、奖励、战斗与招募结算
|
||||||
|
|
||||||
|
### 当前职责
|
||||||
|
|
||||||
|
`src/hooks/story/npcEncounterActions.ts` 当前约 `1623` 行,仍然直接编排:
|
||||||
|
|
||||||
|
1. `quest_accept` / `quest_turn_in`
|
||||||
|
2. 招募、切磋、离开、帮助奖励
|
||||||
|
3. 掉落/背包写入
|
||||||
|
4. HP / MP / cooldown 奖励变化
|
||||||
|
5. NPC 亲和度变化
|
||||||
|
6. 战斗场景切换与遭遇状态推进
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这条链已经明显超出“前端表现协调层”的边界,仍属于**正式运行时规则在前端执行**。
|
||||||
|
|
||||||
|
### 建议迁移方向
|
||||||
|
|
||||||
|
1. quest 信号推进 -> `server-node/src/modules/quest/**`
|
||||||
|
2. 奖励与背包变更 -> `server-node/src/modules/inventory/**`
|
||||||
|
3. 招募/关系变化 -> `server-node/src/modules/npc/**`
|
||||||
|
4. 战斗结算 -> `server-node/src/modules/combat/**`
|
||||||
|
|
||||||
|
前端应该只保留选项触发、加载态、动画态和最终结果展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.4 `src/services/apiClient.ts` 仍保留了本地 token 与自动登录凭证存储
|
||||||
|
|
||||||
|
### 证据
|
||||||
|
|
||||||
|
`src/services/apiClient.ts` 当前仍把以下内容放在 `window.localStorage`:
|
||||||
|
|
||||||
|
1. access token
|
||||||
|
2. 自动登录用户名
|
||||||
|
3. 自动登录密码
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这既是安全面问题,也是边界问题。
|
||||||
|
在“后端负责鉴权、前端只做表现”的目标下,正式凭证体系不应长期依赖浏览器本地保存账号密码。
|
||||||
|
|
||||||
|
### 建议迁移方向
|
||||||
|
|
||||||
|
1. 正式态优先走服务端 session / HttpOnly cookie。
|
||||||
|
2. 自动登录不要继续保存明文用户名/密码。
|
||||||
|
3. 前端仅保留最小必要的登录态感知,不保留额外认证真相。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 需要优先优化和拆分的代码
|
||||||
|
|
||||||
|
## 6.1 `src/components/CustomWorldEntityEditorModal.tsx`
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
文件体量约 `4487` 行,已同时吞下:
|
||||||
|
|
||||||
|
1. 世界营地编辑
|
||||||
|
2. playable NPC 编辑
|
||||||
|
3. story NPC 编辑
|
||||||
|
4. 地标与世界地图布局
|
||||||
|
5. 场景图生成
|
||||||
|
6. 技能编辑
|
||||||
|
7. 初始物品编辑
|
||||||
|
8. 资产工作台串联
|
||||||
|
9. 多层 modal 开关与保存逻辑
|
||||||
|
|
||||||
|
### 判断
|
||||||
|
|
||||||
|
这是当前前端最明显的“巨型工作台单体文件”。
|
||||||
|
|
||||||
|
### 建议拆分方向
|
||||||
|
|
||||||
|
1. 按实体拆:营地 / playable NPC / story NPC / 地标。
|
||||||
|
2. 按能力拆:基础信息 / 关系 / 技能 / 初始物品 / 视觉资产。
|
||||||
|
3. 把 AI 生成与资产工作流进一步外置成独立 coordinator。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.2 `server-node/src/modules/assets/characterAssetRoutes.ts`
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
文件体量约 `3579` 行,已同时承担:
|
||||||
|
|
||||||
|
1. route 注册
|
||||||
|
2. 请求解析
|
||||||
|
3. LLM prompt bundle 生成
|
||||||
|
4. JSON 解析与修复
|
||||||
|
5. 文件系统写盘
|
||||||
|
6. visual publish
|
||||||
|
7. animation publish
|
||||||
|
8. 资产目录管理
|
||||||
|
|
||||||
|
### 直接证据
|
||||||
|
|
||||||
|
文件内同时存在:
|
||||||
|
|
||||||
|
1. `mkdir` / `writeFile`
|
||||||
|
2. `UpstreamLlmClient`
|
||||||
|
3. `parseJsonResponseText`
|
||||||
|
4. 多条 publish 路径
|
||||||
|
5. 大量本地文件落盘逻辑
|
||||||
|
|
||||||
|
### 建议拆分方向
|
||||||
|
|
||||||
|
1. route 层
|
||||||
|
2. prompt bundle service
|
||||||
|
3. file publish service
|
||||||
|
4. animation persistence service
|
||||||
|
5. asset metadata service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.3 `src/services/ai.ts`
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
文件体量约 `2632` 行,同时承载运行时 story、自定义世界、NPC 对话、招募等多条链路。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
即使短期内不能全部迁后端,也应该先按职责拆成:
|
||||||
|
|
||||||
|
1. runtime story client
|
||||||
|
2. npc dialogue client
|
||||||
|
3. recruit dialogue client
|
||||||
|
4. custom world generation client
|
||||||
|
5. parser / fallback / error helpers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.4 `src/services/customWorld.ts`
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
文件体量约 `2413` 行,已经变成世界生成、校验、归一化、fallback 的综合体。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
至少拆成:
|
||||||
|
|
||||||
|
1. 世界框架与 outline schema
|
||||||
|
2. profile normalize / validate
|
||||||
|
3. role / landmark 编译器
|
||||||
|
4. fallback builder
|
||||||
|
5. world rule helpers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.5 `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
|
||||||
|
文件体量约 `1623` 行,已经不是单纯 hook,而是前端运行时 action resolver。
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
按动作域拆开:
|
||||||
|
|
||||||
|
1. npc chat / recruit
|
||||||
|
2. npc help / affinity
|
||||||
|
3. quest accept / turn-in
|
||||||
|
4. battle entry / exit
|
||||||
|
5. async streaming / typewriter / presentation glue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 推荐执行顺序
|
||||||
|
|
||||||
|
### 第一阶段:先清仓库噪音和旧入口残留
|
||||||
|
|
||||||
|
1. 清根目录日志、扫描文件、旧截图、`__pycache__`
|
||||||
|
2. 迁出 `temp-build-goal-check/`
|
||||||
|
3. 明确处置 `scripts/dev-server/localApiPlugins.ts`
|
||||||
|
|
||||||
|
### 第二阶段:再处理无入口孤岛模块
|
||||||
|
|
||||||
|
1. 逐个确认 `GameShell.tsx`、custom-world-home、custom-world-agent、旧 flow hooks 是要接回还是归档
|
||||||
|
2. 对确认不再使用的 stub / helper / generated dead file 直接清理
|
||||||
|
|
||||||
|
### 第三阶段:把双份真相收口
|
||||||
|
|
||||||
|
1. runtime option interaction 映射只保留一份
|
||||||
|
2. 浏览历史以后端为真相源
|
||||||
|
3. 运行时快照解释权继续后移
|
||||||
|
4. 清理 `server-node -> src/**` 的反向依赖
|
||||||
|
|
||||||
|
### 第四阶段:最后拆巨型热点文件
|
||||||
|
|
||||||
|
1. `CustomWorldEntityEditorModal.tsx`
|
||||||
|
2. `characterAssetRoutes.ts`
|
||||||
|
3. `ai.ts`
|
||||||
|
4. `customWorld.ts`
|
||||||
|
5. `npcEncounterActions.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 本文依据
|
||||||
|
|
||||||
|
文档依据:
|
||||||
|
|
||||||
|
1. `docs/audits/engineering/README.md`
|
||||||
|
2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||||
|
3. `scripts/dev-server/README.md`
|
||||||
|
|
||||||
|
当前仓库扫描依据:
|
||||||
|
|
||||||
|
1. `src/main.tsx`
|
||||||
|
2. `src/routing/appRoutes.tsx`
|
||||||
|
3. `src/App.tsx`
|
||||||
|
4. `package.json`
|
||||||
|
5. `server-node/package.json`
|
||||||
|
6. `vite.config.ts`
|
||||||
|
7. `.eslintrc.cjs`
|
||||||
|
8. `git grep` 对关键模块引用、后端跨层 import、localStorage、旧 dev 插件入口的扫描结果
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
# 工程清理与后端边界复核审计(2026-04-20)
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 0. 审计目标
|
||||||
|
|
||||||
|
这份文档不是重复 `2026-04-19` 的原始扫描,而是基于当前仓库状态做一轮复核,重点回答三个问题:
|
||||||
|
|
||||||
|
1. 昨天审计里已经提出的问题,哪些今天已经真正落地。
|
||||||
|
2. 哪些结论在当前代码里仍然成立,哪些表述需要纠正。
|
||||||
|
3. 当前工程热点和边界问题有没有发生迁移。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 结论先行
|
||||||
|
|
||||||
|
和 `2026-04-19` 那份基线相比,当前仓库已经有一批明确进展:
|
||||||
|
|
||||||
|
1. **旧 Vite 本地 API 链路已经真正出清。**
|
||||||
|
`scripts/dev-server/` 当前只剩一份 `README.md`,旧的 `localApiPlugins.ts`、角色资产插件、精灵表插件都不在仓库里了。
|
||||||
|
2. **根目录噪音产物已经清理完成。**
|
||||||
|
当前根目录临时日志/扫描产物扫描结果为空,`temp-build-goal-check/` 也不存在。
|
||||||
|
3. **`server-node -> src/**` 反向依赖已经收掉。**
|
||||||
|
当前复核没有再发现 `server-node/src/**` 直接 import 前端 `src/**` 的情况。
|
||||||
|
4. **runtime option interaction 已经收口成后端单一真相。**
|
||||||
|
这部分现在由 `server-node/src/modules/story/runtimeSession.ts` 统一构造,前端 `src/services/runtimeStoryService.ts` 不再本地再建一份映射表。
|
||||||
|
|
||||||
|
但这不代表边界问题已经结束,当前剩余问题主要集中在三块:
|
||||||
|
|
||||||
|
1. **前端仍保留运行时镜像与登录凭证本地真相。**
|
||||||
|
`runtimeStoryCoordinator.ts` 仍会先写本地快照,`apiClient.ts` 仍把 token/自动登录凭证放在 `localStorage`。
|
||||||
|
2. **NPC 聊天任务链路还没有完全后端化。**
|
||||||
|
“聊天后挂出待接委托”已经移到后端,但“更换待接委托”这条分支仍由前端 `npcEncounterActions.ts` 触发 `generateQuestForNpcEncounter(...)`。
|
||||||
|
3. **未接线孤岛和热点文件问题仍然明显。**
|
||||||
|
一批 UI/Hook/Prompt 残留模块还没有正式入口;同时热点已经从已删除的旧插件链路,转移到 `CustomWorldEntityEditorModal.tsx`、`storyPromptBuilders.ts`、`runtimeProfile.ts`、`PreGameSelectionFlow.tsx`、`PlatformHomeView.tsx` 等新中心。
|
||||||
|
|
||||||
|
一句话判断:
|
||||||
|
|
||||||
|
**当前仓库已经完成“清垃圾、拆旧入口、切断后端反向依赖”的第一阶段,但还没有完成“前端退出运行时真相”和“未接线孤岛归档”的第二阶段。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 已完成项复核
|
||||||
|
|
||||||
|
## 2.1 旧 dev-server 链路已经不是“逻辑上废弃”,而是“代码上删除”
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
| 项目 | 当前状态 | 结论 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `scripts/dev-server/` | 当前只剩 `README.md` 一份说明文件 | 旧 Vite 本地 API 链路已从仓库代码层出清 |
|
||||||
|
| `scripts/dev-server/README.md` | 已明确声明当前正式入口为 `scripts/dev-node.mjs + server-node/src/modules/**` | 文档与代码状态一致 |
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
`2026-04-19` 文档里关于旧本地 API 插件链路的清理结论,在当前仓库里已经可以确认成立,不再只是“计划删除”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 根目录噪音产物已经从当前工作区移除
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
| 项目 | 当前状态 | 结论 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 根目录历史日志/扫描产物 | 本轮扫描结果为空 | 之前的 `.codex-*.log`、`tmp_*`、旧截图/HTML 不再占据当前工作区 |
|
||||||
|
| `temp-build-goal-check/` | 当前不存在 | 大体量检查产物已移出当前仓库视野 |
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
`2026-04-19` 文档中关于“仓库噪音产物”的问题,在当前工作区层面已经完成首轮治理。
|
||||||
|
这部分不再是当前工程第一优先级。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.3 `server-node -> src/**` 反向依赖已清零
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
本轮用脚本复核 `server-node/src/**` 中所有 `import` 后,当前结果为:
|
||||||
|
|
||||||
|
`NO_DIRECT_SERVER_TO_FRONTEND_SRC_IMPORTS`
|
||||||
|
|
||||||
|
同时,仓库里已经看不到类似下面这类旧反向依赖:
|
||||||
|
|
||||||
|
1. `server-node -> src/services/customWorld.js`
|
||||||
|
2. `server-node -> src/services/customWorldBuilder.js`
|
||||||
|
3. `server-node -> src/services/customWorldCreatorIntent.js`
|
||||||
|
4. `server-node -> src/types.js`
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
`2026-04-19` 文档里“清理 `server-node -> src/**` 反向依赖”的阶段性目标,在当前仓库里已经真正落地。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.4 runtime option interaction 已经收口到后端
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
1. `server-node/src/modules/story/runtimeSession.ts` 当前仍保留 `buildOptionInteraction(...)`,负责构造:
|
||||||
|
- `npcActionMap`
|
||||||
|
- `treasureActionMap`
|
||||||
|
2. `src/services/runtimeStoryService.ts` 当前只做:
|
||||||
|
- 直接读取 `option.interaction`
|
||||||
|
- 把后端返回的 interaction 投影成 `StoryOption`
|
||||||
|
3. 前端文件里已经找不到旧的 `buildRuntimeOptionInteraction` / `npcActionMap` / `treasureActionMap` 实现。
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
这项收口已经成立,当前不会再出现“前后端各维护一份 interaction 映射表”的旧问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.5 浏览器端的 quest/runtime item 本地 LLM fallback 已移除
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
1. `src/services/questDirector.ts`
|
||||||
|
- 浏览器路径先请求 `/api/runtime/quests/generate`
|
||||||
|
- 后端失败时只走 deterministic fallback compile
|
||||||
|
2. `src/services/runtimeItemAiDirector.ts`
|
||||||
|
- 浏览器路径先请求 `/api/runtime/items/runtime-intent`
|
||||||
|
- 后端失败时只返回 deterministic fallback intents
|
||||||
|
3. 这两个文件虽然仍保留 `requestChatMessageContent(...)` 分支,但那是非浏览器分支,不再是浏览器端正式兜底链路。
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
`2026-04-19` 文档里关于“浏览器本地 LLM fallback”这部分,当前应更新为:
|
||||||
|
|
||||||
|
**浏览器端本地 LLM fallback 已移除,但这两个模块仍然是双环境混合实现,还没有彻底后端化。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 需要纠正的旧文档表述
|
||||||
|
|
||||||
|
## 3.1 NPC 任务链路不是“全部后端化”,而是“挂单已后移、换单仍前触发”
|
||||||
|
|
||||||
|
### 需要纠正的点
|
||||||
|
|
||||||
|
`2026-04-19` 文档中的回填里有一条表述是:
|
||||||
|
|
||||||
|
“`src/hooks/story/npcEncounterActions.ts` 不再在 NPC 单轮聊天完成后本地调用 `generateQuestForNpcEncounter(...)` 再决定是否挂出待接委托。”
|
||||||
|
|
||||||
|
### 当前代码状态
|
||||||
|
|
||||||
|
这句话对“聊天后挂出待接委托”这条主链是成立的,因为当前后端 `server-node/src/modules/ai/chatOrchestrator.ts` 已经会回填 `pendingQuestOffer`。
|
||||||
|
|
||||||
|
但它对整条 NPC 任务链路来说并不完整,因为当前前端仍保留这条分支:
|
||||||
|
|
||||||
|
1. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
2. `replacePendingNpcQuestOffer()`
|
||||||
|
3. `generateQuestForNpcEncounter(...)`
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
**待接委托的“正式挂出”已后端化,但“更换委托”仍然由前端动作流发起。**
|
||||||
|
|
||||||
|
### 当前应改成的结论
|
||||||
|
|
||||||
|
更准确的描述应该是:
|
||||||
|
|
||||||
|
1. NPC 单轮聊天里“是否挂出待接委托”的决定权已收回后端。
|
||||||
|
2. 但待接委托的“换单/重抽”分支仍通过前端 `npcEncounterActions.ts -> questDirector.ts` 发起。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 当前仍然成立的遗留问题
|
||||||
|
|
||||||
|
## 4.1 未接线/仅测试引用孤岛模块仍然明显
|
||||||
|
|
||||||
|
本轮依赖图复核后,当前仍能确认一批高置信度孤岛模块:
|
||||||
|
|
||||||
|
| 模块 | 当前状态 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/components/GameShell.tsx` | `765` 行,无运行时引用 | 旧版壳层残留仍在 |
|
||||||
|
| `src/components/custom-world-home/CustomWorldCreationHub.tsx` | `161` 行,仅测试引用 | UI 已有完成度,但仍未进入正式入口 |
|
||||||
|
| `src/components/custom-world-home/CustomWorldCreationLauncherModal.tsx` | `147` 行,无运行时引用 | 未接线入口壳层 |
|
||||||
|
| `src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx` | `91` 行,无运行时引用 | agent UI 孤岛仍在 |
|
||||||
|
| `src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx` | `116` 行,无运行时引用 | agent UI 孤岛仍在 |
|
||||||
|
| `src/hooks/story/storyBootstrap.ts` | `250` 行,无运行时引用 | 旧 bootstrap hook 仍未归档 |
|
||||||
|
| `src/hooks/useEquipmentFlow.ts` | `134` 行,无运行时引用 | 旧 flow hook 残留 |
|
||||||
|
| `src/hooks/useForgeFlow.ts` | `159` 行,无运行时引用 | 旧 flow hook 残留 |
|
||||||
|
| `src/hooks/useInventoryFlow.ts` | `100` 行,无运行时引用 | 旧 flow hook 残留 |
|
||||||
|
| `src/services/customWorldPresentation.stub.ts` | `55` 行,无运行时引用 | 占位 stub 仍在 |
|
||||||
|
| `src/services/typewriter.ts` | `7` 行,无运行时引用 | 小型 helper 残留 |
|
||||||
|
| `src/prompts/customWorldOrchestratorPrompts.ts` | `9` 行,无运行时引用 | prompt source 已迁走后留下的孤岛 |
|
||||||
|
| `src/prompts/storyOrchestratorPrompts.ts` | `6` 行,无运行时引用 | prompt source 已迁走后留下的孤岛 |
|
||||||
|
| `src/data/buildTagSimilarity.generated.ts` | `823` 行,无运行时引用 | 生成产物未接入正式业务链路 |
|
||||||
|
|
||||||
|
### 说明
|
||||||
|
|
||||||
|
`src/data/itemOverrides.json`、`src/data/monsterOverrides.json` 这类文件虽然没有 import 引用,但会被脚本和 editor route 以路径消费,所以不计入垃圾判断。
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
仓库已经完成“删旧插件”,但还没有完成“清未接线孤岛”。
|
||||||
|
当前这批模块应该进入明确处置表:
|
||||||
|
|
||||||
|
1. 直接归档/删除
|
||||||
|
2. 正式接回入口
|
||||||
|
3. 改名/迁目录,标记为实验稿
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 前端仍保留运行时镜像真相
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
1. `src/hooks/story/runtimeStoryCoordinator.ts`
|
||||||
|
- 仍会在读状态和提交动作前先 `putSaveSnapshot(...)`
|
||||||
|
- 仍会在响应后多次 `rehydrateSavedSnapshot(...)`
|
||||||
|
2. `src/services/runtimeStoryService.ts`
|
||||||
|
- 仍对响应快照做 `rehydrateSavedSnapshot(...)`
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
当前运行时已经不是“前端主算”,但仍然是:
|
||||||
|
|
||||||
|
**前端先写一份本地镜像,再和后端会话互相回填。**
|
||||||
|
|
||||||
|
这说明前端还没有完全退出正式运行时状态解释层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 前端仍保留本地登录凭证真相
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
`src/services/apiClient.ts` 当前仍把以下内容写入 `window.localStorage`:
|
||||||
|
|
||||||
|
1. `ACCESS_TOKEN_KEY`
|
||||||
|
2. `AUTO_AUTH_USERNAME_KEY`
|
||||||
|
3. `AUTO_AUTH_PASSWORD_KEY`
|
||||||
|
|
||||||
|
对应代码仍包括:
|
||||||
|
|
||||||
|
1. `window.localStorage.getItem(...)`
|
||||||
|
2. `window.localStorage.setItem(...)`
|
||||||
|
3. `window.localStorage.removeItem(...)`
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
这一点和“前端只做表现、后端负责鉴权”的目标仍然不一致。
|
||||||
|
尤其是自动登录用户名/密码继续存本地,风险和边界问题都还在。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.4 quest/runtime item 仍是双环境混合实现
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
1. `src/services/questDirector.ts`
|
||||||
|
- 浏览器路径走 `requestJson('/api/runtime/quests/generate')`
|
||||||
|
- 非浏览器路径仍有 `requestChatMessageContent(...)`
|
||||||
|
2. `src/services/runtimeItemAiDirector.ts`
|
||||||
|
- 浏览器路径走 `requestJson('/api/runtime/items/runtime-intent')`
|
||||||
|
- 非浏览器路径仍有 `requestChatMessageContent(...)`
|
||||||
|
3. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
- 当前仍 import `generateQuestForNpcEncounter`
|
||||||
|
- `replacePendingNpcQuestOffer()` 仍会调用它
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
浏览器兜底已经收掉,但模块职责仍然是混合的:
|
||||||
|
|
||||||
|
1. 同一个文件同时承担前端 SDK 和非浏览器编排逻辑
|
||||||
|
2. NPC 换单动作仍由前端发起服务调用
|
||||||
|
|
||||||
|
这部分还不能算真正后端化完成。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.5 `src/services/ai.ts` 仍然是浏览器端正式 AI orchestration 热点
|
||||||
|
|
||||||
|
### 当前证据
|
||||||
|
|
||||||
|
`src/services/ai.ts` 当前约 `2608` 行,仍直接使用:
|
||||||
|
|
||||||
|
1. `requestChatMessageContent`
|
||||||
|
2. `requestPlainTextCompletion`
|
||||||
|
3. `streamPlainTextCompletion`
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
这说明浏览器侧的大型 AI orchestration 仍然没有真正退出主工程。
|
||||||
|
虽然部分链路已经迁走,但整体边界还没有收完。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 当前热点已经发生迁移
|
||||||
|
|
||||||
|
## 5.1 当前主要大文件快照
|
||||||
|
|
||||||
|
| 文件 | 当前行数 | 判断 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `4898` | 仍是前端最大热点 |
|
||||||
|
| `server-node/src/modules/assets/characterAssetRoutes.ts` | `3181` | 仍是后端资产链路最大热点 |
|
||||||
|
| `src/services/ai.ts` | `2608` | 浏览器 AI orchestration 热点仍在 |
|
||||||
|
| `src/data/npcInteractions.ts` | `2409` | 仍是大型规则数据中心 |
|
||||||
|
| `server-node/src/services/customWorldAgentFoundationDraftService.ts` | `1902` | custom world agent 后端热点上升 |
|
||||||
|
| `src/prompts/storyPromptBuilders.ts` | `1882` | prompt source 已成为新的前端热点 |
|
||||||
|
| `server-node/src/modules/custom-world/runtimeProfile.ts` | `1735` | custom world runtime 编译中心已转到后端 |
|
||||||
|
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `1547` | 平台/入口流程热点上升 |
|
||||||
|
| `src/components/game-shell/PlatformHomeView.tsx` | `1522` | 平台首页热点上升 |
|
||||||
|
| `src/services/customWorld.ts` | `1489` | 仍然大,但已明显缩小 |
|
||||||
|
| `src/hooks/story/npcEncounterActions.ts` | `1434` | 仍然是前端 action 热点 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.2 热点变化判断
|
||||||
|
|
||||||
|
和 `2026-04-19` 相比,当前热点不是单纯“没变”,而是出现了明显迁移:
|
||||||
|
|
||||||
|
1. `characterAssetRoutes.ts` 从 `3579` 行降到 `3181` 行,说明资产路由已经有过一轮拆分,但仍然偏大。
|
||||||
|
2. `src/services/customWorld.ts` 从 `2413` 行降到 `1489` 行,说明自定义世界规则已拆出一部分。
|
||||||
|
3. `src/hooks/story/npcEncounterActions.ts` 从 `1623` 行降到 `1434` 行,说明 NPC 运行时逻辑也有收口。
|
||||||
|
4. 新的复杂度中心开始转移到:
|
||||||
|
- `src/prompts/storyPromptBuilders.ts`
|
||||||
|
- `server-node/src/modules/custom-world/runtimeProfile.ts`
|
||||||
|
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
- `src/components/game-shell/PlatformHomeView.tsx`
|
||||||
|
|
||||||
|
### 结论
|
||||||
|
|
||||||
|
当前问题已经不再是“原来的热点完全没动”,而是:
|
||||||
|
|
||||||
|
**部分旧热点正在缩小,但复杂度正在向 prompt source、custom world runtime profile、平台入口壳层继续迁移。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 最新建议执行顺序
|
||||||
|
|
||||||
|
### 第一阶段:先清理当前仍明确无入口的孤岛
|
||||||
|
|
||||||
|
1. 处理 `GameShell.tsx`
|
||||||
|
2. 处理 `custom-world-home/*`
|
||||||
|
3. 处理 `custom-world-agent/*`
|
||||||
|
4. 处理 `storyBootstrap.ts`、`useEquipmentFlow.ts`、`useForgeFlow.ts`、`useInventoryFlow.ts`
|
||||||
|
5. 处理已脱钩的 `src/prompts/*OrchestratorPrompts.ts`
|
||||||
|
|
||||||
|
### 第二阶段:再收运行时和鉴权真相
|
||||||
|
|
||||||
|
1. 收掉 `runtimeStoryCoordinator.ts` 的本地快照前置写入
|
||||||
|
2. 收掉 `apiClient.ts` 中的自动登录用户名/密码本地持久化
|
||||||
|
3. 优先把 token/session 统一到服务端鉴权边界
|
||||||
|
|
||||||
|
### 第三阶段:补完 NPC 任务链路的后端化
|
||||||
|
|
||||||
|
1. 把“更换待接委托”从 `npcEncounterActions.ts -> questDirector.ts` 继续迁到后端
|
||||||
|
2. 把 `questDirector.ts` / `runtimeItemAiDirector.ts` 拆成明确的后端服务与前端 SDK 两层
|
||||||
|
|
||||||
|
### 第四阶段:最后拆新热点
|
||||||
|
|
||||||
|
1. `CustomWorldEntityEditorModal.tsx`
|
||||||
|
2. `characterAssetRoutes.ts`
|
||||||
|
3. `storyPromptBuilders.ts`
|
||||||
|
4. `runtimeProfile.ts`
|
||||||
|
5. `PreGameSelectionFlow.tsx`
|
||||||
|
6. `PlatformHomeView.tsx`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 本文依据
|
||||||
|
|
||||||
|
文档依据:
|
||||||
|
|
||||||
|
1. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md`
|
||||||
|
2. `docs/audits/engineering/README.md`
|
||||||
|
3. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
|
||||||
|
|
||||||
|
当前仓库复核依据:
|
||||||
|
|
||||||
|
1. `scripts/dev-server/README.md`
|
||||||
|
2. `server-node/src/modules/story/runtimeSession.ts`
|
||||||
|
3. `src/services/runtimeStoryService.ts`
|
||||||
|
4. `src/hooks/story/runtimeStoryCoordinator.ts`
|
||||||
|
5. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
6. `src/services/questDirector.ts`
|
||||||
|
7. `src/services/runtimeItemAiDirector.ts`
|
||||||
|
8. `src/services/apiClient.ts`
|
||||||
|
9. 当前依赖图扫描结果与当前大文件体量扫描结果
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 工程死分支清理执行记录 A(2026-04-21)
|
||||||
|
|
||||||
|
更新时间:`2026-04-21`
|
||||||
|
|
||||||
|
## 0. 本批次目标
|
||||||
|
|
||||||
|
这份记录对应:
|
||||||
|
|
||||||
|
- `docs/planning/ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md`
|
||||||
|
- 其中的 `P0 + 批次 A`
|
||||||
|
|
||||||
|
本批次只做一件事:
|
||||||
|
|
||||||
|
**先清理高置信度、低耦合、无正式入口的小型孤岛与残留壳子。**
|
||||||
|
|
||||||
|
这批对象有一个共同特征:
|
||||||
|
|
||||||
|
1. 当前没有正式运行时引用
|
||||||
|
2. 没有当前主链计划要接回
|
||||||
|
3. 删除后有明确替代路径,或者本身只是历史占位
|
||||||
|
|
||||||
|
因此这批次不碰运行时真相链、不碰鉴权链、不碰任务物品主链,只先做低风险去噪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 本批次已处理对象
|
||||||
|
|
||||||
|
## 1.1 已删除文件
|
||||||
|
|
||||||
|
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 | 验证口径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/services/customWorldPresentation.stub.ts` | 无引用占位 stub | 文件本身就是占位实现,且正式逻辑已由 `customWorldPresentation.ts` 承接 | `src/services/customWorldPresentation.ts` | 符号级检索确认正式调用方都指向正式实现 |
|
||||||
|
| `src/services/typewriter.ts` | 无引用 helper 残留 | 独立 helper 已失效,正式链路已在 `storyPresentation.ts` / `storyRenderingHelpers.ts` 等处内联或迁移 | `src/hooks/story/storyPresentation.ts`、`src/hooks/story/storyRenderingHelpers.ts` | `getTypewriterDelay` 调用点未指向该文件 |
|
||||||
|
| `src/prompts/customWorldOrchestratorPrompts.ts` | 前端孤岛 prompt 壳 | 当前无正式 import,正式主编排 prompt 已收口到后端 prompt 目录,前端 `ai.ts` 也保留自己的现行实现 | `server-node/src/prompts/customWorldOrchestratorPrompts.ts`、`src/services/ai.ts` | 全仓检索仅剩文档引用,无代码消费 |
|
||||||
|
| `src/prompts/storyOrchestratorPrompts.ts` | 前端孤岛 prompt 壳 | 当前无正式 import,剧情语言修复 prompt 已由后端 prompt 目录承接,前端当前执行路径不依赖该文件 | `server-node/src/prompts/storyOrchestratorPrompts.ts`、`src/services/ai.ts` | 全仓检索仅剩文档引用,无代码消费 |
|
||||||
|
| `src/components/custom-world-home/CustomWorldCreationLauncherModal.tsx` | 无入口 UI 壳层 | 最近两轮工程审计都确认无运行时引用,当前平台主流程未接这条入口 | 当前平台正式入口链 | 文件级检索确认无组件 import |
|
||||||
|
| `src/components/custom-world-agent/CustomWorldAgentLauncherModal.tsx` | 无入口 UI 壳层 | Agent 创作主流程已切到当前工作区链路,这个旧 modal 没有接线价值 | 当前 Agent 工作区主链 | 文件级检索确认无组件 import |
|
||||||
|
| `src/components/custom-world-agent/CustomWorldAgentDraftDrawer.tsx` | 无入口 UI 壳层 | 只有孤立 UI 实现,没有正式调用链,也不在当前结果页 / 工作区主链中 | 当前 Agent 工作区与结果页正式链 | 文件级检索确认无组件 import |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本批次为什么先删这 7 个
|
||||||
|
|
||||||
|
这批文件适合先处理,不是因为它们最大,而是因为它们最清晰:
|
||||||
|
|
||||||
|
1. **没有正式入口。**
|
||||||
|
本轮检索没有发现主工程 import。
|
||||||
|
2. **删除后不会形成职责空洞。**
|
||||||
|
要么已有正式替代路径,要么本身只是历史占位。
|
||||||
|
3. **不会误伤当前重点链路。**
|
||||||
|
这批不涉及运行时快照、鉴权、任务、物品、AI 正式编排主链。
|
||||||
|
4. **可以最快降低目录噪音。**
|
||||||
|
先把真假并存的壳子删掉,后面做批次 B/C/D 时判断成本会更低。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 本批次暂不处理对象
|
||||||
|
|
||||||
|
以下对象虽然已进入首轮台账,但本批次暂不删除:
|
||||||
|
|
||||||
|
1. `src/components/GameShell.tsx`
|
||||||
|
2. `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||||
|
3. `src/hooks/story/storyBootstrap.ts`
|
||||||
|
4. `src/hooks/useEquipmentFlow.ts`
|
||||||
|
5. `src/hooks/useForgeFlow.ts`
|
||||||
|
6. `src/hooks/useInventoryFlow.ts`
|
||||||
|
7. `src/data/buildTagSimilarity.generated.ts`
|
||||||
|
|
||||||
|
暂缓原因分别是:
|
||||||
|
|
||||||
|
1. 仍属于旧主流程 / 旧 flow 级别对象,删除前要先核对更多历史依赖和替代路径
|
||||||
|
2. 部分对象仍有测试引用或更大的上下文耦合
|
||||||
|
3. `buildTagSimilarity.generated.ts` 虽无正式业务 import,但属于生成产物,处理前还要确认脚本链与文档链
|
||||||
|
|
||||||
|
这批对象更适合进入:
|
||||||
|
|
||||||
|
1. `批次 B:旧 flow / 旧 shell / 旧 hook`
|
||||||
|
2. 或独立的数据产物复核批次
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 本批次同步更新的文档
|
||||||
|
|
||||||
|
本批次除了删文件,还同步做了文档回填:
|
||||||
|
|
||||||
|
1. 新增本执行记录,说明本批删了什么、为什么删、哪些对象暂缓
|
||||||
|
2. 更新 `docs/audits/engineering/README.md`,把这份执行记录加入当前审计入口
|
||||||
|
|
||||||
|
这样做的目的,是避免再次出现:
|
||||||
|
|
||||||
|
1. 代码删了
|
||||||
|
2. 但审计入口还是旧状态
|
||||||
|
3. 后续开发又从旧清单里重复判断一遍
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 验证方式
|
||||||
|
|
||||||
|
本批次验证采用两层口径:
|
||||||
|
|
||||||
|
## 5.1 删除前验证
|
||||||
|
|
||||||
|
1. 文件级检索确认无正式 import
|
||||||
|
2. 符号级检索确认关键导出没有被主链消费
|
||||||
|
3. 结合 `2026-04-20` 工程审计交叉确认这些对象已被标记为高置信度孤岛
|
||||||
|
|
||||||
|
## 5.2 删除后验证
|
||||||
|
|
||||||
|
建议至少执行:
|
||||||
|
|
||||||
|
1. `npm run check:encoding`
|
||||||
|
2. `npm run build`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前仓库已知 `typecheck` 与 `lint` 仍处于红线阶段,因此本批不把它们作为“由本批引入的新失败”判断口径
|
||||||
|
- 本批主要验证目标是:删除小残留后,不产生新的导入断裂和构建断裂
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本批次结果判断
|
||||||
|
|
||||||
|
本批次完成后,工程至少获得了 3 个直接收益:
|
||||||
|
|
||||||
|
1. `src/prompts/`、`src/services/`、`src/components/custom-world-*` 中少了一批无入口孤岛
|
||||||
|
2. 当前目录里“看起来像正式入口,其实已经废弃”的误导性对象减少
|
||||||
|
3. 后续可以把精力集中到真正高价值的批次 B/C/D,而不是继续被小残留分散判断成本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 下一批建议
|
||||||
|
|
||||||
|
建议严格按计划继续往下推进:
|
||||||
|
|
||||||
|
1. 批次 B:`GameShell`、`storyBootstrap`、`useEquipmentFlow`、`useForgeFlow`、`useInventoryFlow`
|
||||||
|
2. 批次 C:`runtimeStoryCoordinator`、`runtimeStoryService`、`apiClient`
|
||||||
|
3. 批次 D:`npcEncounterActions`、`questDirector`、`runtimeItemAiDirector`、`ai.ts`
|
||||||
|
|
||||||
|
一句话总结本批次:
|
||||||
|
|
||||||
|
**先把最确定的死分支和占位壳子清掉,让主工程少一些假入口、假主源、假能力,再进入更重的主链收口。**
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# 工程死分支清理执行记录 B(2026-04-21)
|
||||||
|
|
||||||
|
更新时间:`2026-04-21`
|
||||||
|
|
||||||
|
## 0. 本批次目标
|
||||||
|
|
||||||
|
这份记录对应清洗计划中的:
|
||||||
|
|
||||||
|
- `批次 B:旧 flow / 旧 shell / 旧 hook`
|
||||||
|
|
||||||
|
本批次聚焦的不是小型 stub,而是:
|
||||||
|
|
||||||
|
**已经退出正式主流程、但仍占着高辨识度命名和旧职责心智的壳层与流程 Hook。**
|
||||||
|
|
||||||
|
这类文件如果继续留在仓库里,问题比小 helper 更大,因为它们会持续制造误判:
|
||||||
|
|
||||||
|
1. 新人会以为它们还是正式入口
|
||||||
|
2. 后续开发会误判“应该往这里接逻辑”
|
||||||
|
3. review 时会多出一层“旧主链是不是还活着”的判断成本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 本批次已处理对象
|
||||||
|
|
||||||
|
## 1.1 已删除文件
|
||||||
|
|
||||||
|
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 | 验证口径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/GameShell.tsx` | 旧主流程壳层残留 | 当前正式壳层已由 `src/components/game-shell/GameShellRuntime.tsx` 承接,旧文件无正式 import | `src/components/game-shell/GameShellRuntime.tsx`、`src/hooks/useGameShellRuntime.ts` | 全仓检索未发现对旧 `GameShell` 组件的正式消费 |
|
||||||
|
| `src/hooks/story/storyBootstrap.ts` | 旧启动流程 Hook 残留 | 当前主剧情启动链已不再调用该 Hook,继续保留只会误导人以为它还是故事初始化入口 | 当前 story runtime / coordinator 链 | 全仓检索未发现 `useStoryBootstrap` 消费方 |
|
||||||
|
| `src/hooks/useEquipmentFlow.ts` | 旧装备流程 Hook 残留 | 当前正式背包与装备链未消费该 Hook,属于旧流程实现残留 | 当前 inventory / runtime 正式链 | 符号级检索仅命中定义文件自身 |
|
||||||
|
| `src/hooks/useForgeFlow.ts` | 旧锻造流程 Hook 残留 | 当前正式锻造入口未通过该 Hook 进入主链,保留会制造旧流程错觉 | 当前 inventory / runtime 正式链 | 符号级检索仅命中定义文件自身 |
|
||||||
|
| `src/hooks/useInventoryFlow.ts` | 旧背包使用流程 Hook 残留 | 当前主流程未消费该 Hook,属于旧状态推进实现残留 | 当前 inventory / runtime 正式链 | 符号级检索仅命中定义文件自身 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 为什么这批要紧跟批次 A 处理
|
||||||
|
|
||||||
|
批次 A 清掉的是“小型假入口”。
|
||||||
|
|
||||||
|
批次 B 清掉的是“高辨识度旧主链”。
|
||||||
|
|
||||||
|
这批必须紧跟着做,原因是:
|
||||||
|
|
||||||
|
1. 它们虽然比 stub 更大,但引用关系同样清楚
|
||||||
|
2. 它们的误导性比小残留更强
|
||||||
|
3. 不先处理这批,后面做批次 C/D 时,很容易继续有人拿旧 flow Hook 当候选接线点
|
||||||
|
|
||||||
|
一句话讲:
|
||||||
|
|
||||||
|
**批次 A 是去噪,批次 B 是拔掉旧路牌。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 本批次删除后的结构变化
|
||||||
|
|
||||||
|
本批次完成后,仓库里的流程心智会更清楚:
|
||||||
|
|
||||||
|
1. 游戏壳层正式入口继续收敛到 `src/components/game-shell/**`
|
||||||
|
2. 旧 `GameShell.tsx` 不再和 `GameShellRuntime.tsx` 并存
|
||||||
|
3. 旧的装备 / 锻造 / 背包单独 flow Hook 不再伪装成还在生效的正式实现
|
||||||
|
4. 旧 `storyBootstrap` 不再和当前 story runtime 链并存
|
||||||
|
|
||||||
|
这会直接减少两类误判:
|
||||||
|
|
||||||
|
1. “是不是还有旧主流程没迁完”
|
||||||
|
2. “我是不是应该把新逻辑继续补进这些旧 Hook”
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 本批次暂不处理对象
|
||||||
|
|
||||||
|
虽然批次 B 已经处理了旧 shell / old flow / old bootstrap,但以下对象仍暂缓:
|
||||||
|
|
||||||
|
1. `src/components/custom-world-home/CustomWorldCreationHub.tsx`
|
||||||
|
2. `src/data/buildTagSimilarity.generated.ts`
|
||||||
|
3. 批次 C 的运行时真相链:
|
||||||
|
- `src/hooks/story/runtimeStoryCoordinator.ts`
|
||||||
|
- `src/services/runtimeStoryService.ts`
|
||||||
|
- `src/services/apiClient.ts`
|
||||||
|
4. 批次 D 的混合执行层:
|
||||||
|
- `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
- `src/services/questDirector.ts`
|
||||||
|
- `src/services/runtimeItemAiDirector.ts`
|
||||||
|
- `src/services/ai.ts`
|
||||||
|
|
||||||
|
暂缓原因很明确:
|
||||||
|
|
||||||
|
1. 这些对象要么仍在当前正式链上
|
||||||
|
2. 要么涉及运行时真相与鉴权边界
|
||||||
|
3. 不能按“无引用旧壳”同一口径直接删除
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 本批次验证方式
|
||||||
|
|
||||||
|
## 5.1 删除前验证
|
||||||
|
|
||||||
|
1. 全仓检索 `GameShell` 旧组件消费方,确认当前正式壳层已切到 `game-shell/` 目录
|
||||||
|
2. 全仓检索 `useStoryBootstrap`
|
||||||
|
3. 全仓检索旧装备 / 锻造 / 背包 flow Hook 导出的 handler 名称
|
||||||
|
4. 交叉确认当前正式主链入口已存在替代实现
|
||||||
|
|
||||||
|
## 5.2 删除后验证
|
||||||
|
|
||||||
|
建议至少执行:
|
||||||
|
|
||||||
|
1. `npm run check:encoding`
|
||||||
|
2. `npm run build`
|
||||||
|
|
||||||
|
如果这两项通过,说明:
|
||||||
|
|
||||||
|
1. 删除没有引入新的导入断裂
|
||||||
|
2. 主工程构建链仍然成立
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本批次结果判断
|
||||||
|
|
||||||
|
本批次完成后,工程获得的直接收益是:
|
||||||
|
|
||||||
|
1. 旧主流程壳层不再和现行壳层并存
|
||||||
|
2. 旧流程 Hook 不再占据 `src/hooks/` 的主路径注意力
|
||||||
|
3. 当前正式入口和历史残留的边界更清楚
|
||||||
|
4. 后续开发更不容易把新逻辑接回旧流程壳子
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 下一批建议
|
||||||
|
|
||||||
|
建议下一步进入真正有结构价值的收口:
|
||||||
|
|
||||||
|
1. `批次 C:运行时真相收口`
|
||||||
|
- `runtimeStoryCoordinator`
|
||||||
|
- `runtimeStoryService`
|
||||||
|
- `apiClient`
|
||||||
|
2. `批次 D:任务 / 物品 / AI 混合执行层收口`
|
||||||
|
- `npcEncounterActions`
|
||||||
|
- `questDirector`
|
||||||
|
- `runtimeItemAiDirector`
|
||||||
|
- `ai.ts`
|
||||||
|
|
||||||
|
一句话总结本批次:
|
||||||
|
|
||||||
|
**这一步不是在“删几个没用 Hook”,而是在把已经退场的旧主流程壳层和旧 flow 路牌从主工程里真正拔掉,让现行架构不再和历史壳子并排站着。**
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# 工程死分支清理执行记录 C(2026-04-21)
|
||||||
|
|
||||||
|
更新时间:`2026-04-21`
|
||||||
|
|
||||||
|
## 0. 本批次目标
|
||||||
|
|
||||||
|
这份记录对应清洗计划中的:
|
||||||
|
|
||||||
|
- `批次 C:运行时真相收口`
|
||||||
|
|
||||||
|
但这次不是“一口气把运行时真相链全删干净”,而是先做其中最明确、风险最低、最不该继续拖的那一段:
|
||||||
|
|
||||||
|
1. **收掉前端本地自动登录用户名 / 密码真相**
|
||||||
|
2. **把登录恢复改成优先依赖服务端 session / refresh**
|
||||||
|
|
||||||
|
同时,这一批也明确记录了一件事:
|
||||||
|
|
||||||
|
**运行时快照前置写入链当前还不能直接砍。**
|
||||||
|
|
||||||
|
原因不是“不想动”,而是服务端当前 `runtime story` 动作入口仍然以远端快照作为执行基线。
|
||||||
|
在后端 contract 没先改好之前,前端不能假装自己已经退出这条链。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 本批次已处理对象
|
||||||
|
|
||||||
|
## 1.1 已收口的鉴权链
|
||||||
|
|
||||||
|
| 文件 | 处理动作 | 本批结论 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/services/apiClient.ts` | 删除本地自动登录用户名 / 密码存取逻辑 | 前端不再保存 auto auth 账号密码 |
|
||||||
|
| `src/services/authService.ts` | 去掉对本地游客凭证的读写依赖 | 自动游客登录改为仅本次生成凭证,不再长期落本地 |
|
||||||
|
| `src/components/auth/AuthGate.tsx` | 去掉“必须先有本地 access token 才尝试恢复”的前置假设 | 登录恢复改为优先尝试服务端 `getCurrentAuthUser()` / refresh session |
|
||||||
|
| `src/services/authService.test.ts` | 改写游客自动登录相关断言 | 验证改为“生成临时凭证并完成登录”,而不是“落本地账号密码” |
|
||||||
|
| `src/components/auth/AuthGate.test.tsx` | 改写登录恢复 mock | 验证改为“先尝试服务端会话恢复,再决定是否走游客兜底” |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本批次为什么先做这段
|
||||||
|
|
||||||
|
这批优先级高,是因为它同时满足 4 条:
|
||||||
|
|
||||||
|
1. **风险明确。**
|
||||||
|
浏览器保存自动登录用户名 / 密码,本身就不符合“前端只做表现、后端负责鉴权真相”的方向。
|
||||||
|
2. **替代路径已经存在。**
|
||||||
|
后端已经有 refresh session cookie 与 `getCurrentAuthUser()`,不是没有可替代能力。
|
||||||
|
3. **改动边界清楚。**
|
||||||
|
这一段主要落在前端鉴权恢复逻辑和测试,不会直接波及运行时战斗、任务、物品、剧情主链。
|
||||||
|
4. **收益直接。**
|
||||||
|
一旦收掉,前端就少了一份最不该长期保留的高风险真相。
|
||||||
|
|
||||||
|
一句话讲:
|
||||||
|
|
||||||
|
**这一步先把“浏览器记住游客账号密码再重登”这条假真相链拔掉。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 本批次明确没做的事
|
||||||
|
|
||||||
|
## 3.1 没有直接删除 `runtimeStoryCoordinator.ts` 里的前置 `putSaveSnapshot(...)`
|
||||||
|
|
||||||
|
这不是漏做,而是明确暂缓。
|
||||||
|
|
||||||
|
当前复核结果是:
|
||||||
|
|
||||||
|
1. `server-node/src/modules/story/storyActionService.ts`
|
||||||
|
2. `server-node/src/routes/runtimeRoutes.ts`
|
||||||
|
3. `server-node/src/repositories/runtimeRepository.ts`
|
||||||
|
|
||||||
|
这条后端链当前仍然通过远端快照读取运行时状态,再执行:
|
||||||
|
|
||||||
|
1. `getRuntimeStoryState`
|
||||||
|
2. `resolveRuntimeStoryAction`
|
||||||
|
|
||||||
|
也就是说,当前真实情况不是“前端多写了一份完全没用的镜像”,而是:
|
||||||
|
|
||||||
|
**前端在提交动作前先把当前状态写回远端快照,后端再基于这份快照执行业务动作。**
|
||||||
|
|
||||||
|
在这个 contract 没先升级为“前端只发 action,后端自己持有完整 session 真相”之前,前端不能直接把这一步砍掉。
|
||||||
|
|
||||||
|
否则会出现:
|
||||||
|
|
||||||
|
1. 动作请求仍在走
|
||||||
|
2. 但服务端读取到的执行基线不完整
|
||||||
|
3. 最后不是收口真相,而是把主链打断
|
||||||
|
|
||||||
|
## 3.2 没有删除 `runtimeStoryService.ts` / `runtimeStoryCoordinator.ts` 的快照再水合逻辑
|
||||||
|
|
||||||
|
这一步本轮也做了复核,结论是:
|
||||||
|
|
||||||
|
1. 我曾尝试把 `runtimeStoryCoordinator.ts` 中对服务端返回快照的重复再水合去掉
|
||||||
|
2. 但对应的 `runtimeStoryCoordinator` 测试立即暴露出:当前后端返回的快照在部分战斗场景下还不是完整水合态
|
||||||
|
3. 说明前端当前这层再水合仍然有现实职责,不是纯多余代码
|
||||||
|
|
||||||
|
所以这一步本批明确结论是:
|
||||||
|
|
||||||
|
**暂不删除,等后端快照 contract 先补完整后再做。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 本批次验证结果
|
||||||
|
|
||||||
|
本批次已完成的定向验证:
|
||||||
|
|
||||||
|
1. `npx vitest run src/services/authService.test.ts`
|
||||||
|
2. `npx vitest run src/components/auth/AuthGate.test.tsx`
|
||||||
|
3. `npx vitest run src/hooks/story/runtimeStoryCoordinator.test.ts`
|
||||||
|
4. `npm run check:encoding`
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
1. `authService` 测试通过
|
||||||
|
2. `AuthGate` 测试通过
|
||||||
|
3. `runtimeStoryCoordinator` 测试通过
|
||||||
|
4. 编码检查通过
|
||||||
|
|
||||||
|
另外执行了:
|
||||||
|
|
||||||
|
1. `npm run build`
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
构建产物生成成功,但 `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. 本批次完成后的实际收益
|
||||||
|
|
||||||
|
这一步完成后,工程在鉴权边界上有了两个明确改善:
|
||||||
|
|
||||||
|
1. **前端不再保存自动登录用户名 / 密码。**
|
||||||
|
浏览器只保留 access token,本地高风险游客凭证真相已经收掉。
|
||||||
|
2. **登录恢复逻辑更接近服务端为真相源。**
|
||||||
|
`AuthGate` 不再假设“没有本地 token 就一定还没登录”,而是优先尝试服务端会话恢复。
|
||||||
|
|
||||||
|
这意味着前端鉴权链已经从:
|
||||||
|
|
||||||
|
```text
|
||||||
|
本地用户名/密码 -> 再次 entry -> 拿 token
|
||||||
|
```
|
||||||
|
|
||||||
|
进一步收到了:
|
||||||
|
|
||||||
|
```text
|
||||||
|
refresh session / 当前会话 -> 恢复用户
|
||||||
|
兜底时才创建一次游客凭证
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 本批次后续建议
|
||||||
|
|
||||||
|
要继续完成批次 C,下一步不该直接在前端硬删,而应该先补后端 contract:
|
||||||
|
|
||||||
|
1. 让 `runtime story` 动作链逐步摆脱“前端先写远端快照”的依赖
|
||||||
|
2. 让服务端自己持有更完整的运行时 session 真相
|
||||||
|
3. 等后端返回快照已经稳定水合后,再删前端的重复再水合
|
||||||
|
|
||||||
|
换句话说,批次 C 的后半段应该拆成:
|
||||||
|
|
||||||
|
1. **C-1:鉴权真相收口**
|
||||||
|
本批已完成
|
||||||
|
2. **C-2:运行时快照 contract 后端化**
|
||||||
|
需要先改后端
|
||||||
|
3. **C-3:前端镜像写入与重复水合退场**
|
||||||
|
依赖 C-2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 一句话总结
|
||||||
|
|
||||||
|
**批次 C 这一轮已经先把“浏览器长期保存游客账号密码”这条最不该存在的鉴权假真相链收掉了;而运行时快照前置写入这条链经过复核确认仍受后端 contract 约束,不能在服务端未先补齐前硬砍。**
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# 工程死分支清理执行记录 D(2026-04-21)
|
||||||
|
|
||||||
|
更新时间:`2026-04-21`
|
||||||
|
|
||||||
|
## 0. 本批次目标
|
||||||
|
|
||||||
|
本批次继续清理上一轮复核后剩余的低风险数据产物与测试占位:
|
||||||
|
|
||||||
|
1. 未接入业务的生成产物
|
||||||
|
2. 只为测试替换真实实现的空 stub
|
||||||
|
3. 支撑这些残留的配置与脚本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 已删除对象
|
||||||
|
|
||||||
|
| 文件 | 判定 | 删除原因 | 替代路径 / 当前真相源 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `src/data/buildTagSimilarity.generated.ts` | 未接入业务的生成产物 | 运行时代码不 import;Build 相似度当前由 `buildTags.ts` 中的属性亲和度逻辑计算 | `src/data/buildTags.ts` |
|
||||||
|
| `scripts/generate-build-tag-similarity.py` | 已无输出目标的生成脚本 | 只负责生成已删除的矩阵文件,继续保留会误导后续开发恢复旧主源 | `src/data/buildTags.ts` 的手工审表逻辑 |
|
||||||
|
| `src/data/customWorldCharacterLoadout.stub.ts` | 测试专用空 stub | 只通过 `vitest.config.ts` alias 替换真实实现;真实实现已经稳定存在 | `src/data/customWorldCharacterLoadout.ts` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 同步更新
|
||||||
|
|
||||||
|
本批次同步移除了:
|
||||||
|
|
||||||
|
1. `vitest.config.ts` 中指向 `customWorldCharacterLoadout.stub.ts` 的 alias
|
||||||
|
2. `BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md` 中把旧 generated 矩阵描述为当前文件的表述
|
||||||
|
3. 清理计划里对 `buildTagSimilarity.generated.ts` 的未处理状态说明
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 验证口径
|
||||||
|
|
||||||
|
删除前已确认:
|
||||||
|
|
||||||
|
1. `buildTagSimilarity.generated.ts` 无运行时代码引用
|
||||||
|
2. `customWorldCharacterLoadout.stub.ts` 只被 `vitest.config.ts` alias 引用
|
||||||
|
3. 真实 `customWorldCharacterLoadout.ts` 仍被 `characterPresets.ts` 与 `npcInteractions.ts` 使用,不能删除
|
||||||
|
|
||||||
|
删除后建议验证:
|
||||||
|
|
||||||
|
1. `npm run check:encoding`
|
||||||
|
2. 与自定义世界开局物品相关的测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 当前结论
|
||||||
|
|
||||||
|
本批次完成后,剩余清理对象已经不再适合按“无引用直接删”推进。后续如果继续清,需要先改 contract 或主链职责:
|
||||||
|
|
||||||
|
1. 运行时快照真相链
|
||||||
|
2. 任务 / 物品 / AI 混合执行层
|
||||||
|
3. 大型主流程组件继续拆分,而不是直接删除
|
||||||
@@ -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,而不应再按文件名或历史命名直接硬删。
|
||||||
@@ -0,0 +1,553 @@
|
|||||||
|
# 前端应迁后端逻辑审计(2026-04-21)
|
||||||
|
|
||||||
|
更新时间:`2026-04-21`
|
||||||
|
|
||||||
|
## 0. 审计目标
|
||||||
|
|
||||||
|
这份文档只回答一个问题:
|
||||||
|
|
||||||
|
**当前前端代码里,哪些逻辑已经明显越过“前端只做表现,Express 后端负责逻辑、数据与存储”的边界,应该继续迁到后端。**
|
||||||
|
|
||||||
|
本轮不改业务代码,只做:
|
||||||
|
|
||||||
|
1. 基于当前仓库状态给出高置信度候选点
|
||||||
|
2. 标明代码证据
|
||||||
|
3. 给出迁移优先级
|
||||||
|
4. 说明迁移后前端应该保留什么、移走什么
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 结论先行
|
||||||
|
|
||||||
|
结合当前代码与已有边界文档,前端里仍有 7 类逻辑应该继续后移:
|
||||||
|
|
||||||
|
1. **运行时快照前置写入与本地镜像解释**
|
||||||
|
2. **鉴权 token 的浏览器本地真相**
|
||||||
|
3. **平台浏览历史的本地真相与迁移状态**
|
||||||
|
4. **NPC 待接委托“换单”仍由前端直接触发正式生成**
|
||||||
|
5. **quest/runtime item 的双环境混合编排**
|
||||||
|
6. **浏览器侧大型 AI orchestration 与 prompt/repair/fallback 主链**
|
||||||
|
7. **NPC 招募对白之后的正式结算链路**
|
||||||
|
|
||||||
|
一句话判断:
|
||||||
|
|
||||||
|
**当前前端已经不是最早那种“大量主算”的状态,但仍然保留了运行时镜像、生成编排和部分正式真相。后端边界还需要再收一轮,前端才算真正退回表现层。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 审计依据
|
||||||
|
|
||||||
|
### 2.1 文档依据
|
||||||
|
|
||||||
|
1. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.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`
|
||||||
|
|
||||||
|
### 2.2 当前代码依据
|
||||||
|
|
||||||
|
1. `src/hooks/story/runtimeStoryCoordinator.ts`
|
||||||
|
2. `src/services/apiClient.ts`
|
||||||
|
3. `src/services/platformBrowseHistory.ts`
|
||||||
|
4. `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
5. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
6. `src/services/questDirector.ts`
|
||||||
|
7. `src/services/runtimeItemAiDirector.ts`
|
||||||
|
8. `src/services/ai.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 运行时快照前置写入仍在前端
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
`src/hooks/story/runtimeStoryCoordinator.ts` 当前仍存在以下链路:
|
||||||
|
|
||||||
|
1. `syncRuntimeSnapshot(...)`
|
||||||
|
2. `syncRuntimeSnapshot(...)` 内部直接调用 `putSaveSnapshot(...)`
|
||||||
|
3. `loadServerRuntimeOptionCatalog(...)` 在请求 `getRuntimeStoryState(...)` 之前先写本地快照
|
||||||
|
4. `resolveServerRuntimeChoice(...)` 在请求 `resolveRuntimeStoryAction(...)` 之前先写本地快照
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/hooks/story/runtimeStoryCoordinator.ts:21`
|
||||||
|
2. `src/hooks/story/runtimeStoryCoordinator.ts:25`
|
||||||
|
3. `src/hooks/story/runtimeStoryCoordinator.ts:36`
|
||||||
|
4. `src/hooks/story/runtimeStoryCoordinator.ts:99`
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
这意味着运行时正式动作发起前,前端仍会先落一份自己的快照真相,再去请求后端。
|
||||||
|
|
||||||
|
这条链的问题不是“有没有缓存”,而是:
|
||||||
|
|
||||||
|
1. 前端仍在承担正式提交前的状态镜像
|
||||||
|
2. 快照解释权没有完全收回到后端
|
||||||
|
3. 运行时主链仍处于“本地镜像 + 服务端会话”并存状态
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. 运行时快照写入
|
||||||
|
2. 快照版本解释
|
||||||
|
3. 动作提交前的状态一致性校验
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. 当前展示用的 view model
|
||||||
|
2. 可选的只读恢复缓存
|
||||||
|
3. 纯表现态的 loading / transition / animation state
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.2 鉴权 token 仍由前端 localStorage 持有真相
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
`src/services/apiClient.ts` 当前仍直接访问 `window.localStorage` 保存 access token:
|
||||||
|
|
||||||
|
1. `getStoredAccessToken()`
|
||||||
|
2. `setStoredAccessToken(...)`
|
||||||
|
3. `clearStoredAccessToken(...)`
|
||||||
|
4. `withAuthorizationHeaders(...)` 直接从本地 token 组装请求头
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/services/apiClient.ts:333`
|
||||||
|
2. `src/services/apiClient.ts:341`
|
||||||
|
3. `src/services/apiClient.ts:362`
|
||||||
|
4. `src/services/apiClient.ts:382`
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
第三批清理已经收掉了“自动登录用户名/密码”本地真相,但 access token 仍然由浏览器长期持有。
|
||||||
|
|
||||||
|
这在当前项目边界下仍有两个问题:
|
||||||
|
|
||||||
|
1. 正式鉴权真相仍没有完全收回后端 session 边界
|
||||||
|
2. 前端 SDK 仍然负担 token 生命周期的关键部分
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. session / refresh / cookie 真相
|
||||||
|
2. 鉴权状态续期
|
||||||
|
3. token 更新与失效策略
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. 当前是否已登录的展示态
|
||||||
|
2. 统一的请求封装
|
||||||
|
3. 401 后的 UI 响应
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.3 平台浏览历史仍是“前端本地历史 + 后端回填”的双真相
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
`src/services/platformBrowseHistory.ts` 当前仍维护一整套本地历史真相:
|
||||||
|
|
||||||
|
1. `readPlatformBrowseHistory(...)`
|
||||||
|
2. `writePlatformBrowseHistory(...)`
|
||||||
|
3. `hasPendingPlatformBrowseHistoryMigration(...)`
|
||||||
|
4. `markPlatformBrowseHistoryMigrated(...)`
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/services/platformBrowseHistory.ts:77`
|
||||||
|
2. `src/services/platformBrowseHistory.ts:103`
|
||||||
|
3. `src/services/platformBrowseHistory.ts:151`
|
||||||
|
4. `src/services/platformBrowseHistory.ts:164`
|
||||||
|
|
||||||
|
`src/components/game-shell/PreGameSelectionFlow.tsx` 当前仍显式做:
|
||||||
|
|
||||||
|
1. 先 `writePlatformBrowseHistory(...)`
|
||||||
|
2. 再调用 `upsertProfileBrowseHistory(...)`
|
||||||
|
3. 同步成功后 `markPlatformBrowseHistoryMigrated(...)`
|
||||||
|
4. 启动阶段读取 `readPlatformBrowseHistory(...)`
|
||||||
|
5. 根据 `hasPendingPlatformBrowseHistoryMigration(...)` 决定是否补同步
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/components/game-shell/PreGameSelectionFlow.tsx:383`
|
||||||
|
2. `src/components/game-shell/PreGameSelectionFlow.tsx:392`
|
||||||
|
3. `src/components/game-shell/PreGameSelectionFlow.tsx:394`
|
||||||
|
4. `src/components/game-shell/PreGameSelectionFlow.tsx:433`
|
||||||
|
5. `src/components/game-shell/PreGameSelectionFlow.tsx:466`
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
这条链已经不是单纯缓存,而是:
|
||||||
|
|
||||||
|
1. 本地历史存储
|
||||||
|
2. 本地同步标记
|
||||||
|
3. 后端历史持久化
|
||||||
|
|
||||||
|
三套状态同时存在。
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. 浏览历史唯一持久化真相
|
||||||
|
2. 历史去重、排序、截断
|
||||||
|
3. 迁移完成标记
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. 展示缓存
|
||||||
|
2. 弱网下的临时 optimistic UI
|
||||||
|
3. 刷新后重新拉取远端结果
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.4 NPC 待接委托“换单”仍由前端直接发起正式生成
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
`src/hooks/story/npcEncounterActions.ts` 当前仍保留:
|
||||||
|
|
||||||
|
1. `replacePendingNpcQuestOffer = async () => { ... }`
|
||||||
|
2. 内部直接调用 `generateQuestForNpcEncounter(...)`
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/hooks/story/npcEncounterActions.ts:1561`
|
||||||
|
2. `src/hooks/story/npcEncounterActions.ts:1595`
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
聊天后是否挂出待接委托已经后移,但“换一份委托”这条分支仍然是:
|
||||||
|
|
||||||
|
1. 前端组装上下文
|
||||||
|
2. 前端决定调用生成
|
||||||
|
3. 前端直接把结果写回当前 story UI
|
||||||
|
|
||||||
|
这仍属于正式运行时任务编排没有收干净。
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. NPC 待接委托换单决策
|
||||||
|
2. 是否允许换单
|
||||||
|
3. 换单后的任务草案生成
|
||||||
|
4. 对应聊天态快照回填
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. 点击“换一份委托”
|
||||||
|
2. loading / error 展示
|
||||||
|
3. 消费后端返回的新 pending quest offer
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.5 questDirector 仍是前端 SDK 与生成编排混合体
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
`src/services/questDirector.ts` 当前同时承担:
|
||||||
|
|
||||||
|
1. `generateQuestForNpcEncounter(...)`
|
||||||
|
2. 浏览器路径 `requestJson('/api/runtime/quests/generate')`
|
||||||
|
3. 非浏览器路径 `requestChatMessageContent(...)`
|
||||||
|
4. 本地 `compileQuestIntentToQuest(...)` fallback
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/services/questDirector.ts:213`
|
||||||
|
2. `src/services/questDirector.ts:242`
|
||||||
|
3. `src/services/questDirector.ts:267`
|
||||||
|
4. `src/services/questDirector.ts:256`
|
||||||
|
5. `src/services/questDirector.ts:281`
|
||||||
|
6. `src/services/questDirector.ts:293`
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
这类文件虽然浏览器正式路径已经优先走后端,但职责仍混在一起:
|
||||||
|
|
||||||
|
1. 前端 SDK
|
||||||
|
2. Quest prompt 编排
|
||||||
|
3. Quest intent 解析
|
||||||
|
4. deterministic fallback compile
|
||||||
|
|
||||||
|
这会导致边界长期模糊,也让前端仍像“半个服务端”。
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. quest intent 生成
|
||||||
|
2. prompt 组装
|
||||||
|
3. JSON 解析
|
||||||
|
4. fallback compile
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. `requestGenerateQuest(...)` 这类轻量 SDK
|
||||||
|
2. 请求参数组装
|
||||||
|
3. 结果消费
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.6 runtimeItemAiDirector 仍是前端 SDK 与意图生成混合体
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
`src/services/runtimeItemAiDirector.ts` 当前同时承担:
|
||||||
|
|
||||||
|
1. `generateRuntimeItemAiIntents(...)`
|
||||||
|
2. 浏览器路径 `requestJson('/api/runtime/items/runtime-intent')`
|
||||||
|
3. 非浏览器路径 `requestChatMessageContent(...)`
|
||||||
|
4. 本地 `buildRuntimeItemAiIntent(...)` fallback
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/services/runtimeItemAiDirector.ts:84`
|
||||||
|
2. `src/services/runtimeItemAiDirector.ts:94`
|
||||||
|
3. `src/services/runtimeItemAiDirector.ts:118`
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
它和 `questDirector` 是同类问题:
|
||||||
|
|
||||||
|
1. 正式浏览器路径已经走后端
|
||||||
|
2. 但前端文件仍然承担完整生成逻辑认知
|
||||||
|
3. 文件职责仍然是双环境混合
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. runtime item intent prompt
|
||||||
|
2. 模型调用
|
||||||
|
3. 结果解析与 fallback
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. 轻量请求 SDK
|
||||||
|
2. 结果到 UI 的映射
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.7 `src/services/ai.ts` 仍是浏览器侧正式 AI orchestration 热点
|
||||||
|
|
||||||
|
### 代码证据
|
||||||
|
|
||||||
|
当前 `src/services/ai.ts` 仍直接承担以下正式链路:
|
||||||
|
|
||||||
|
1. `requestChatMessageContent(...)`
|
||||||
|
2. `requestPlainTextCompletionFromClient(...)`
|
||||||
|
3. `streamPlainTextCompletionFromClient(...)`
|
||||||
|
4. `generateCustomWorldProfile(...)`
|
||||||
|
5. `generateInitialStory(...)`
|
||||||
|
6. `generateNextStep(...)`
|
||||||
|
7. `streamNpcChatDialogue(...)`
|
||||||
|
8. `streamNpcRecruitDialogue(...)`
|
||||||
|
|
||||||
|
对应位置:
|
||||||
|
|
||||||
|
1. `src/services/ai.ts:1732`
|
||||||
|
2. `src/services/ai.ts:1868`
|
||||||
|
3. `src/services/ai.ts:2038`
|
||||||
|
4. `src/services/ai.ts:2339`
|
||||||
|
5. `src/services/ai.ts:2447`
|
||||||
|
6. `src/services/ai.ts:2487`
|
||||||
|
7. `src/services/ai.ts:2529`
|
||||||
|
8. `src/services/ai.ts:2570`
|
||||||
|
|
||||||
|
并且文件内仍保留:
|
||||||
|
|
||||||
|
1. JSON repair
|
||||||
|
2. prompt 组装
|
||||||
|
3. response normalize
|
||||||
|
4. fallback/offline 响应
|
||||||
|
5. 角色聊天建议与摘要生成
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
这说明浏览器端并不只是“请求一个后端接口”,而是还在承担:
|
||||||
|
|
||||||
|
1. prompt source
|
||||||
|
2. 生成策略
|
||||||
|
3. 错误修复
|
||||||
|
4. fallback 编排
|
||||||
|
5. 多类业务场景的正式 AI 出口
|
||||||
|
|
||||||
|
这与“前端只做表现”存在明确冲突。
|
||||||
|
|
||||||
|
### 迁移建议
|
||||||
|
|
||||||
|
后端继续承接:
|
||||||
|
|
||||||
|
1. story / npc / recruit / custom-world 的 prompt 编排
|
||||||
|
2. JSON repair
|
||||||
|
3. fallback 策略
|
||||||
|
4. streaming orchestration
|
||||||
|
5. 模型调用与日志
|
||||||
|
|
||||||
|
前端只保留:
|
||||||
|
|
||||||
|
1. 轻量 AI SDK
|
||||||
|
2. SSE 文本流展示
|
||||||
|
3. UI fallback 呈现
|
||||||
|
|
||||||
|
### 优先级
|
||||||
|
|
||||||
|
`P0`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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. 面板开关、loading、error、streaming 文本展示
|
||||||
|
2. 动画时间线、过场状态、临时 UI 回显
|
||||||
|
3. 表单草稿、筛选词、排序选项
|
||||||
|
4. 只影响表现、不影响正式真相的 view model 拼接
|
||||||
|
|
||||||
|
迁移时要注意:
|
||||||
|
|
||||||
|
**不是把所有前端代码都往后端搬,而是把“正式状态解释、规则裁决、生成编排、持久化真相”搬走。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 推荐迁移顺序
|
||||||
|
|
||||||
|
## 5.1 第一阶段
|
||||||
|
|
||||||
|
先收最危险的正式真相:
|
||||||
|
|
||||||
|
1. `runtimeStoryCoordinator.ts`
|
||||||
|
2. `apiClient.ts`
|
||||||
|
3. `npcEncounterActions.ts` 里的 quest replace 分支
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
1. 这三处最直接影响运行时真相和动作主链
|
||||||
|
2. 不先收这些,前端仍然不是纯表现层
|
||||||
|
|
||||||
|
## 5.2 第二阶段
|
||||||
|
|
||||||
|
再拆双环境混合服务:
|
||||||
|
|
||||||
|
1. `questDirector.ts`
|
||||||
|
2. `runtimeItemAiDirector.ts`
|
||||||
|
3. `platformBrowseHistory.ts`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
1. 这几处已经有后端承接基础
|
||||||
|
2. 迁移成本相对可控
|
||||||
|
|
||||||
|
## 5.3 第三阶段
|
||||||
|
|
||||||
|
最后继续压缩浏览器 AI orchestration:
|
||||||
|
|
||||||
|
1. `src/services/ai.ts`
|
||||||
|
2. 相关 prompt builder / repair helper / offline fallback
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
1. 这部分体量大
|
||||||
|
2. 链路多
|
||||||
|
3. 更适合在前两阶段把 contract 稳住后集中拆
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 建议产出物
|
||||||
|
|
||||||
|
如果后续按这份文档继续落地,建议每一批都至少同步产出:
|
||||||
|
|
||||||
|
1. 一份落地文档,说明迁移了哪条链
|
||||||
|
2. 一组 contract/route 变更说明
|
||||||
|
3. 一组前端 SDK 收缩说明
|
||||||
|
4. 一组防回退测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 一句话结论
|
||||||
|
|
||||||
|
当前前端最需要继续后移的,不是零散小工具,而是:
|
||||||
|
|
||||||
|
**运行时快照前置写入、鉴权 token、本地浏览历史真相、NPC 委托换单、quest/runtime item 双环境混合编排,以及 `src/services/ai.ts` 里仍然留在浏览器的正式 AI orchestration。**
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# 怪物-NPC 脚本统一整改审计
|
||||||
|
|
||||||
|
日期:2026-04-06
|
||||||
|
|
||||||
|
## 核心结论
|
||||||
|
|
||||||
|
当前工程仍然没有真正落实“怪物就是初始好感度为负数的 NPC”这一原则。
|
||||||
|
|
||||||
|
现状不是“NPC 脚本里支持 hostile 状态”,而是同时存在两条并行链路:
|
||||||
|
|
||||||
|
1. `npc / encounter / npcStates / npcInteraction`
|
||||||
|
2. `monster / hostileNpc / sceneMonsters / sceneHostileNpcs / hostileNpcPresets`
|
||||||
|
|
||||||
|
这会直接导致:
|
||||||
|
|
||||||
|
- 同一个敌对实体同时拥有 NPC 身份和 monster 身份。
|
||||||
|
- 场景、战斗、渲染、提示词都在维护两套入口。
|
||||||
|
- 后续修 bug 时,任何位置、死亡、血条、入场、掉落问题都要同时查两条链路。
|
||||||
|
|
||||||
|
本次文档的目标不是立刻改代码,而是先把应该删除的分叉脚本、应该降级成素材层的文件、以及必须合并的字段全部列清楚,作为后续统一改造的依据。
|
||||||
|
|
||||||
|
## 当前违背原则的根因
|
||||||
|
|
||||||
|
### 1. 场景数据仍然把“怪物”和“NPC”当成两类实体
|
||||||
|
|
||||||
|
当前场景层同时维护:
|
||||||
|
|
||||||
|
- `ScenePreset.monsterIds`
|
||||||
|
- `SceneNpc[]`
|
||||||
|
- `ScenePresetInfo.hostileNpcIds`
|
||||||
|
- `SceneNpc.monsterPresetId / hostileNpcPresetId`
|
||||||
|
|
||||||
|
这意味着场景里一个敌对单位既可以来自 `monsterIds`,也可以来自 `npcs`,甚至会被脚本再生成为 hostile scene npc。
|
||||||
|
|
||||||
|
### 2. 运行时仍然存在“怪物专用实体状态”
|
||||||
|
|
||||||
|
当前运行时仍然同时维护:
|
||||||
|
|
||||||
|
- `GameState.sceneMonsters`
|
||||||
|
- `GameState.sceneHostileNpcs`
|
||||||
|
- `SceneHostileNpc / SceneMonster`
|
||||||
|
|
||||||
|
这和“怪物本质上只是 hostile NPC”是冲突的。真正统一后,运行时只应该保留一套“场景 NPC / 战斗 NPC”状态。
|
||||||
|
|
||||||
|
### 3. 战斗脚本仍然把怪物当独立 actor
|
||||||
|
|
||||||
|
战斗层目前不是“NPC 战斗,只是 hostile 的那部分会出手”,而是显式写了:
|
||||||
|
|
||||||
|
- `TurnActor = 'player' | 'companion' | 'monster'`
|
||||||
|
- `getClosestMonster`
|
||||||
|
- `resetCombatPresentation(monsters, ...)`
|
||||||
|
- `sceneMonsters` 全链路结算
|
||||||
|
|
||||||
|
这会强制后面所有视觉、掉落、提示词、AI 上下文都跟着叫 monster。
|
||||||
|
|
||||||
|
### 4. 渲染层仍然有 monster 专属显示入口
|
||||||
|
|
||||||
|
画布层当前仍然依赖:
|
||||||
|
|
||||||
|
- `sceneMonsters`
|
||||||
|
- `sceneHostileNpcs`
|
||||||
|
- `monsterPresetId`
|
||||||
|
- `HostileNpcAnimator`
|
||||||
|
|
||||||
|
也就是“敌对 NPC 是否按 NPC 脚本渲染”这件事,到最终显示层仍然没有统一。
|
||||||
|
|
||||||
|
## 一级删除清单
|
||||||
|
|
||||||
|
下面这些文件属于“业务流程分叉脚本”,不是单纯资源适配层。后续统一时应优先删除或并入 NPC 主链路。
|
||||||
|
|
||||||
|
| 文件 | 当前分叉角色 | 处理建议 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/data/monsters.ts` | 对 `hostileNpcs.ts` 的别名出口 | 直接删除,禁止继续保留 monster 专属入口名。 |
|
||||||
|
| `src/data/hostileNpcs.ts` | 负责 monster 创建、编队、距离、朝向、变化、落位 | 按 hostile NPC 运行时工具重写并并入 NPC 体系;原文件名不应继续保留。 |
|
||||||
|
| `src/data/sceneEncounterPreviews.ts` | 单独构造 hostile encounter group、auto battle、hostile preview | 删除 monster 专线逻辑,改为“负好感 NPC 预览/入场/转战斗”。 |
|
||||||
|
| `src/hooks/combat/battlePlan.ts` | 使用 `monster` actor、`sceneMonsters`、`getClosestMonster` | 改成统一的 hostile NPC combatant 规划脚本;monster actor 概念应移除。 |
|
||||||
|
| `src/hooks/combat/playback.ts` | 使用 `sceneMonsters` 播放怪物战斗演出 | 改成统一 NPC 战斗回放;不再区分 monster 播放器。 |
|
||||||
|
| `src/components/game-canvas/GameCanvasRuntime.tsx` | 运行时按 `sceneMonsters / sceneHostileNpcs` 双数据源选敌方实体 | 删除双源兜底,统一成单一 hostile NPC 列表。 |
|
||||||
|
| `src/components/game-canvas/GameCanvasEntityLayer.tsx` | 敌方实体按 monsterPreset 分支渲染 | 改成同一套 NPC 实体渲染,视觉差异仅由 visual preset 决定。 |
|
||||||
|
| `src/components/game-canvas/GameCanvasShared.tsx` | 定义 `sceneMonsters / sceneHostileNpcs` props 与 monster 专属计算 | 删除这些字段与 helper,改成统一 NPC 画布协议。 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | 独立怪物预设编辑面板 | 并回 NPC hostile visual preset 面板,或删除该独立编辑器入口。 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetTab.tsx` | 独立怪物预设页签 | 与上面一并删除或并入 NPC preset 编辑器。 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | 仍然单独编辑 `monsterIds` | 删除 `monsterIds` 编辑项,只保留场景 NPC 列表。 |
|
||||||
|
|
||||||
|
## 二级归并清单
|
||||||
|
|
||||||
|
下面这些文件不一定需要物理删除,但它们当前仍然在放大 monster / NPC 分轨,必须在统一改造时一起收口。
|
||||||
|
|
||||||
|
| 文件 | 当前问题 | 处理建议 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/data/scenePresets.ts` | 通过 `monsterIds` 再生 hostile scene npc,并区分 `getSceneHostileNpcs / getSceneFriendlyNpcs` | 保留场景数据文件本身,但删除 `monsterIds` 体系,让敌对角色直接存在于 `npcs` 中。 |
|
||||||
|
| `src/data/customWorldNpcMonsters.ts` | 用单独脚本推导“怪物型 NPC”预设 | 可保留为 hostile visual preset 选择器,但不能再生成第二套实体语义。 |
|
||||||
|
| `src/data/hostileNpcPresets.ts` | 目前既是视觉预设库,也是独立 hostile 流程的数据源 | 降级为 hostile visual/combat preset 库;不再拥有独立实体生命周期。 |
|
||||||
|
| `src/components/HostileNpcAnimator.tsx` | 当前名字和调用语义都在暗示“独立怪物实体” | 可以保留为贴图播放器,但应改为 hostile NPC 的视觉适配组件,而不是独立物种脚本。 |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | 详情弹窗仍会优先查 monster preset / hostileNpcPreset | 统一读取 NPC 档案;视觉差异只通过 hostile preset 补充。 |
|
||||||
|
| `src/components/SkillEffectPreview.tsx` | 预览器直接使用 `sceneMonsters` 和 `createSceneMonstersFromIds` | 改成统一 hostile NPC 预览态。 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | 编辑器里仍然直接造 monster battle preview | 改成 hostile NPC preview。 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | 仍然暴露 `monsterPresetId` 字段 | 改成更明确的 hostile visual preset 字段,避免“怪物类型”和“NPC 类型”双语义。 |
|
||||||
|
| `src/hooks/story/npcEncounterActions.ts` | 虽然入口叫 npc,但内部仍然写 `sceneMonsters / sceneHostileNpcs` | 改成统一 hostile NPC 战斗状态字段。 |
|
||||||
|
| `src/hooks/story/choiceActions.ts` | 仍然有 `buildHostileNpcBattleReward` 和 `getResolvedSceneHostileNpcs` 这一层额外概念 | 统一到 hostile NPC 结算工具,不再把“敌对 NPC”和“monster”混称。 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | 给 AI/剧情层传入 `sceneMonsters` 或 `sceneHostileNpcs` | 改成统一的 hostile NPC 上下文切片。 |
|
||||||
|
| `src/services/prompt.ts` | 仍然从 `monsterIds` 和 `createSceneMonstersFromIds` 组 prompt | 改成从场景 NPC 列表中筛出 hostile NPC。 |
|
||||||
|
| `src/services/questDirector.ts` | 仍然依赖 `monsterPresetId` 推导当前敌对目标 | 统一改为基于负好感或 hostile 标记的 NPC。 |
|
||||||
|
| `src/services/ai.ts` | 仍然混用 `monsterIds`、sceneNpc 的 hostile 判定 | 与场景统一后改成只读 NPC 列表。 |
|
||||||
|
| `src/services/questTypes.ts` | 仍然把 `hostileNpcIds / monsterIds` 当作 scene 快照字段 | 删除 `monsterIds`,保留 hostile NPC 语义。 |
|
||||||
|
|
||||||
|
## 可保留但必须降级为“素材/配置层”的内容
|
||||||
|
|
||||||
|
下面这些内容不一定要消失,但不能继续作为独立业务链路存在:
|
||||||
|
|
||||||
|
| 文件/内容 | 可以保留的原因 | 必须收口的边界 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `src/components/HostileNpcAnimator.tsx` | 怪物贴图是特殊资源,需要专门 sprite sheet 播放器 | 只负责画图,不再决定实体类型、战斗身份、交互入口。 |
|
||||||
|
| `src/data/hostileNpcPresets.ts` | hostile visual/combat preset 仍然有价值 | 只能作为 hostile NPC 的 visual/combat preset 库,不再驱动另一套“monster 实体”。 |
|
||||||
|
| `src/data/hostileNpcOverrides.json` | 资源级 override 仍可继续用 | 不能再配套出独立 hostile 流程。 |
|
||||||
|
| `src/data/monsterOverrides.json` | 如果只是素材映射,可迁移到 hostile visual preset override | 不应继续以 monster 专属命名长期存在。 |
|
||||||
|
| `src/data/customWorldNpcMonsters.ts` | 自定义世界里确实需要从文本匹配 hostile visual preset | 只能产出“NPC 使用哪个 hostile visual preset”,不能产出独立 monster 身份。 |
|
||||||
|
|
||||||
|
## 字段级必须合并的内容
|
||||||
|
|
||||||
|
后续改代码时,至少要把下面这些字段和类型一起收口:
|
||||||
|
|
||||||
|
| 当前字段/类型 | 问题 | 合并方向 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ScenePreset.monsterIds` | 场景里额外保存一份怪物池 | 删除,只保留 `npcs`。 |
|
||||||
|
| `ScenePresetInfo.hostileNpcIds` | 历史遗留双字段 | 直接由 `npcs.filter(initialAffinity < 0 或 hostile)` 推导。 |
|
||||||
|
| `Encounter.monsterPresetId` | 把 hostile NPC 再次物种化 | 改成 hostile visual preset 字段,或并入统一 visualRef。 |
|
||||||
|
| `Encounter.hostileNpcPresetId` | 与 `monsterPresetId` 语义重叠 | 与上面合并为一个字段。 |
|
||||||
|
| `GameState.sceneMonsters` | 把敌对 NPC 单独塞进 monster 容器 | 改成统一 `sceneNpcCombatants` 或等价单一列表。 |
|
||||||
|
| `GameState.sceneHostileNpcs` | 历史兼容层,导致双数据源 | 删除。 |
|
||||||
|
| `SceneHostileNpc / SceneMonster` | 类型名直接固化了分轨 | 改成统一的 hostile NPC / scene combat NPC 类型。 |
|
||||||
|
| `SceneHostileNpcChange / SceneMonsterChange` | 继续复制同一套变更结构 | 合并成统一 NPC scene change。 |
|
||||||
|
| `SceneNpc.monsterPresetId / hostileNpcPresetId` | 同一实体上挂两套 preset 入口 | 收敛为一个 hostile visual/combat preset 字段。 |
|
||||||
|
|
||||||
|
## 本轮最优先的删除顺序
|
||||||
|
|
||||||
|
建议后续真正改代码时,按下面顺序删并,风险最低:
|
||||||
|
|
||||||
|
1. 先删字段入口:`monsterIds / sceneHostileNpcs / hostileNpcPresetId`
|
||||||
|
2. 再删运行时双轨:`src/data/monsters.ts`、`src/data/hostileNpcs.ts`、`src/data/sceneEncounterPreviews.ts`
|
||||||
|
3. 再删战斗双轨:`battlePlan.ts`、`playback.ts` 里的 `monster` actor 与 `sceneMonsters`
|
||||||
|
4. 再删画布双轨:`GameCanvasRuntime.tsx`、`GameCanvasEntityLayer.tsx`、`GameCanvasShared.tsx`
|
||||||
|
5. 最后清编辑器和提示词:`MonsterPresetPanel.tsx`、`MonsterPresetTab.tsx`、`prompt.ts`、`questDirector.ts`
|
||||||
|
|
||||||
|
## 改造后的目标形态
|
||||||
|
|
||||||
|
统一后应只剩下这一套语义:
|
||||||
|
|
||||||
|
- 场景中所有可见角色都放在 `npcs`
|
||||||
|
- 怪物 = `initialAffinity < 0` 或 `hostile = true` 的 NPC
|
||||||
|
- hostile 的视觉差异只来自 hostile visual preset
|
||||||
|
- 战斗中所有敌方单位都属于 hostile NPC combatant
|
||||||
|
- AI、任务、渲染、详情、掉落都只读同一套 NPC 数据
|
||||||
|
|
||||||
|
如果后面代码里还出现下面这些关键词,基本都说明分轨没有删干净:
|
||||||
|
|
||||||
|
- `sceneMonsters`
|
||||||
|
- `sceneHostileNpcs`
|
||||||
|
- `monsterIds`
|
||||||
|
- `hostileNpcPresetId`
|
||||||
|
- `createSceneMonstersFromIds`
|
||||||
|
- `getClosestMonster`
|
||||||
|
- `TurnActor = 'monster'`
|
||||||
|
|
||||||
|
## 这份文档的使用方式
|
||||||
|
|
||||||
|
后续正式开始改造时,建议把文件分成三批执行:
|
||||||
|
|
||||||
|
1. “直接删掉”的入口脚本
|
||||||
|
2. “改名并并回 NPC 主链路”的桥接脚本
|
||||||
|
3. “仅保留素材职责”的 renderer / preset 文件
|
||||||
|
|
||||||
|
不要继续接受“名字叫 NPC,但内部仍然先转成 monster 再跑”的中间态。
|
||||||
46
docs/audits/engineering/README.md
Normal file
46
docs/audits/engineering/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 工程优化审查总览
|
||||||
|
|
||||||
|
这一组只保留仍能指导当前 Rust / SpacetimeDB 主线的工程审查入口。早期连续扫描的有效结论已经合并到本 README 的“融合结论”,不再保留逐日旧稿。
|
||||||
|
|
||||||
|
## 当前推荐入口
|
||||||
|
|
||||||
|
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 与对应配置残留出清。
|
||||||
|
6. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_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 的正式出清。
|
||||||
|
8. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
|
||||||
|
这一版是第一批落地记录,聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。
|
||||||
|
9. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_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` 基线的当前仓库复核,明确哪些问题已经处理、哪些表述需要纠正、热点又迁移到了哪里。
|
||||||
|
11. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
|
||||||
|
这一版保留原始问题快照和执行回填,适合回看“为什么会有这轮清理与边界收口”。
|
||||||
|
## 融合结论
|
||||||
|
|
||||||
|
- 最新专项审计已经把“前端哪些逻辑还该后移到后端”收敛到 6 类:运行时快照、本地 token、本地浏览历史、NPC 委托换单、quest/runtime item 混合编排、浏览器 AI orchestration。
|
||||||
|
- 工程大清洗已经开始进入实际执行阶段,首批高置信度小型孤岛和残留壳子已开始清理。
|
||||||
|
- 第二批已经开始清理旧主流程壳层与旧 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。
|
||||||
|
- `2026-04-19` 这一轮把问题压实到了四类:仓库噪音、旧 dev 入口残留、前端越界运行时逻辑、巨型热点文件。
|
||||||
|
- `2026-04-20` 这一轮进一步确认:前两类已经阶段性完成,当前真正剩下的是边界尾巴和新热点迁移。
|
||||||
|
- 如果是要看当前清理和边界收口的最新状态,优先看 `ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`。
|
||||||
|
- 如果是要看“当前可执行的优化点清单”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`。
|
||||||
|
- 如果是要做长期重构方案,从 `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 专用开发入口。
|
||||||
91
docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md
Normal file
91
docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# 中文乱码位置清单
|
||||||
|
|
||||||
|
更新时间:`2026-03-24`
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 本文档用于记录仓库内已确认或高置信度疑似存在中文乱码的位置。
|
||||||
|
- 当前这份文档是重建版本;原有的 [`docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md`](/E:/Repos/Genarrative/docs/audits/text/CHINESE_MOJIBAKE_INVENTORY.md) 本身也已经乱码,因此已整体替换。
|
||||||
|
- 本次整理依据:
|
||||||
|
- 仓库内旧清单中的完整文件/行号信息
|
||||||
|
- 本轮人工复核时再次直接看到的明显乱码位置
|
||||||
|
- 由于仓库内同时存在“文件内容已写坏”和“终端/工具显示失真”两类情况,下面清单优先保留高置信位置,便于后续逐项修复。
|
||||||
|
|
||||||
|
## 扫描范围
|
||||||
|
|
||||||
|
- 已纳入:`src/`、`docs/`、根目录文档与元数据文件
|
||||||
|
- 已排除:`.git/`、`node_modules/`、`dist/`、纯图片资源目录
|
||||||
|
|
||||||
|
## 高置信位置
|
||||||
|
|
||||||
|
### 文档与元数据
|
||||||
|
|
||||||
|
- [`docs/experience/AGENT_UI_CHANGELOG.md`](/E:/Repos/Genarrative/docs/experience/AGENT_UI_CHANGELOG.md):1, 3, 7, 9, 11-18, 24, 26-28, 32-33, 37, 39, 41-43, 47, 49, 51, 53-66, 68, 72, 74, 77-79, 83, 87, 89-90, 94, 96, 98-100, 104, 106-112, 116
|
||||||
|
- [`UI_CODING_STANDARD.md`](/E:/Repos/Genarrative/UI_CODING_STANDARD.md):3, 91, 104, 108, 112, 156, 158, 160-166
|
||||||
|
- [`metadata.json`](/E:/Repos/Genarrative/metadata.json):2-3
|
||||||
|
|
||||||
|
### 组件层
|
||||||
|
|
||||||
|
- [`src/components/AdventurePanel.tsx`](/E:/Repos/Genarrative/src/components/AdventurePanel.tsx):57, 65
|
||||||
|
- [`src/components/CharacterPanel.tsx`](/E:/Repos/Genarrative/src/components/CharacterPanel.tsx):37, 65-66, 91-95, 102-103
|
||||||
|
- [`src/components/GameCanvas.tsx`](/E:/Repos/Genarrative/src/components/GameCanvas.tsx):240, 462
|
||||||
|
- [`src/components/GameShell.tsx`](/E:/Repos/Genarrative/src/components/GameShell.tsx):108, 116, 124, 138, 171, 181
|
||||||
|
- [`src/components/InventoryPanel.tsx`](/E:/Repos/Genarrative/src/components/InventoryPanel.tsx):55, 58, 82-83, 181-184, 189, 191
|
||||||
|
- [`src/components/MapModal.tsx`](/E:/Repos/Genarrative/src/components/MapModal.tsx):105, 108, 136
|
||||||
|
- [`src/components/MedievalNpcAnimator.tsx`](/E:/Repos/Genarrative/src/components/MedievalNpcAnimator.tsx):124
|
||||||
|
- [`src/components/NpcVisualEditor.tsx`](/E:/Repos/Genarrative/src/components/NpcVisualEditor.tsx):65, 69-71, 403, 440, 444, 446, 464, 467, 470, 482, 569, 571, 585, 610, 628, 662, 690, 694-695, 697, 722, 751, 759, 775, 777, 781, 824
|
||||||
|
- [`src/components/PresetEditor.tsx`](/E:/Repos/Genarrative/src/components/PresetEditor.tsx):34-37, 43-44, 94, 96, 349, 470, 472, 480, 482, 512, 516, 519, 525, 568, 612, 618, 637, 639, 643, 645, 652, 661, 677, 740, 769, 771, 779, 781, 806, 809, 820, 831, 835, 837, 840, 848, 871, 894, 916, 918, 930, 932, 950, 953, 956, 960, 962, 990, 1004, 1006, 1012, 1018, 1024, 1030, 1036, 1064, 1120, 1122, 1130, 1132, 1150, 1153, 1156, 1172-1175, 1180, 1182, 1186, 1188, 1199, 1203-1204, 1208, 1240, 1242
|
||||||
|
|
||||||
|
### 数据层
|
||||||
|
|
||||||
|
- [`src/data/characterPresets.ts`](/E:/Repos/Genarrative/src/data/characterPresets.ts):97, 102, 104, 107, 129, 132-133, 142, 144, 170, 276, 302, 470, 496, 531, 540, 566, 699-700, 729, 972
|
||||||
|
- [`src/data/medievalNpcVisuals.ts`](/E:/Repos/Genarrative/src/data/medievalNpcVisuals.ts):103, 115, 117, 119, 136, 154, 156, 161, 167, 174, 177, 189, 226, 235-236, 241, 244-245, 249-254, 256-257, 260, 262, 274, 278, 288, 451-453, 565, 568, 577, 592
|
||||||
|
- [`src/data/monsterPresets.ts`](/E:/Repos/Genarrative/src/data/monsterPresets.ts):41-42, 54, 60-61, 79-80, 92, 98-99, 117-118, 136-137, 155-156, 171-173, 185, 191-192, 204, 210-211, 229-230, 242, 248-249, 261, 267-268, 280, 286-287, 304-305, 323-324, 335
|
||||||
|
- [`src/data/monsters.ts`](/E:/Repos/Genarrative/src/data/monsters.ts):112
|
||||||
|
- [`src/data/npcInteractions.ts`](/E:/Repos/Genarrative/src/data/npcInteractions.ts):68-71, 80, 82-83, 161, 165, 173, 182, 188-190, 196, 198, 205, 231, 241, 245, 255-260, 272, 296, 319-320, 372, 444-445, 449, 451, 453, 507, 569-570, 578-579, 587-588, 597, 605-606, 615, 617-618, 626-627, 634, 641-643, 652, 661, 665, 670, 672, 676
|
||||||
|
- [`src/data/scenePresets.ts`](/E:/Repos/Genarrative/src/data/scenePresets.ts):115, 120, 122, 128, 133, 135, 141, 146, 148, 154, 159, 161, 167, 172, 174, 180, 185, 187, 192-193, 198, 200, 205-206, 211, 213, 219, 224, 226, 232, 237, 239, 245, 250, 252, 258, 263, 265, 274, 279, 281, 287, 292, 294, 299-300, 305, 307, 313, 318, 320, 326, 331, 333, 339, 344, 346, 352, 357, 359, 364-365, 370, 372, 377-378, 383, 385, 390-391, 396, 398, 404, 409, 411, 417, 422, 424, 509, 523, 525
|
||||||
|
- [`src/data/stateFunctions.ts`](/E:/Repos/Genarrative/src/data/stateFunctions.ts):72-73, 80, 95-96, 103, 117-118, 125, 139-140, 147, 161-162, 169, 186-187, 194, 209-210, 217, 237-238, 255-256, 273-274, 294, 311-312, 329-330, 420, 430-431, 433-435, 437-438, 440-442, 444-445, 447, 449, 451-452, 454-456, 458, 460-461, 464, 466, 468, 484-485, 487, 489, 491, 493, 601, 618
|
||||||
|
|
||||||
|
### Hooks 与服务层
|
||||||
|
|
||||||
|
- [`src/hooks/useCombatFlow.ts`](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts):54, 56-58, 566
|
||||||
|
- [`src/services/ai.ts`](/E:/Repos/Genarrative/src/services/ai.ts):200-201, 209-210, 234-235, 269-270, 309, 311, 317, 338, 341, 358, 382
|
||||||
|
- [`src/services/prompt.ts`](/E:/Repos/Genarrative/src/services/prompt.ts):7-8, 10, 13-15, 19-20, 25-40, 43, 55, 61-62, 64, 66, 74-76, 78-79, 83-84, 87-90, 96, 103-104, 112, 115, 157, 159, 161-162, 164-165, 167-168, 170, 172-173
|
||||||
|
|
||||||
|
### 其他源码
|
||||||
|
|
||||||
|
- [`src/uiAssets.ts`](/E:/Repos/Genarrative/src/uiAssets.ts):54, 115, 122, 129, 142, 173, 180
|
||||||
|
|
||||||
|
## 本轮人工复核补充
|
||||||
|
|
||||||
|
以下位置是在本轮实现过程中直接再次看到的明显乱码文本,建议优先复查:
|
||||||
|
|
||||||
|
- [`src/hooks/useCombatFlow.ts`](/E:/Repos/Genarrative/src/hooks/useCombatFlow.ts):1094, 1554, 1556-1557
|
||||||
|
|
||||||
|
## 处理优先级建议
|
||||||
|
|
||||||
|
### 第一批
|
||||||
|
|
||||||
|
- `src/components/GameShell.tsx`
|
||||||
|
- `src/components/InventoryPanel.tsx`
|
||||||
|
- `src/components/CharacterPanel.tsx`
|
||||||
|
- `src/data/characterPresets.ts`
|
||||||
|
- `src/data/npcInteractions.ts`
|
||||||
|
- `src/data/scenePresets.ts`
|
||||||
|
- `src/services/prompt.ts`
|
||||||
|
|
||||||
|
### 第二批
|
||||||
|
|
||||||
|
- `src/components/PresetEditor.tsx`
|
||||||
|
- `src/components/NpcVisualEditor.tsx`
|
||||||
|
- `src/data/monsterPresets.ts`
|
||||||
|
- `src/data/stateFunctions.ts`
|
||||||
|
- `docs/experience/AGENT_UI_CHANGELOG.md`
|
||||||
|
- `UI_CODING_STANDARD.md`
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 当前文档的目标是“先把位置收拢清楚”,不是直接修复乱码。
|
||||||
|
- 如果你下一步要我继续,我可以基于这份清单继续做两件事之一:
|
||||||
|
- 逐文件修复中文乱码
|
||||||
|
- 先做一个“乱码修复优先级 + 替换建议”文档
|
||||||
236
docs/audits/text/CURRENT_GAME_STORY_SOURCE_REVIEW_2026-04-07.md
Normal file
236
docs/audits/text/CURRENT_GAME_STORY_SOURCE_REVIEW_2026-04-07.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# 当前游戏剧情原文整理与质量评测
|
||||||
|
|
||||||
|
日期:`2026-04-07`
|
||||||
|
|
||||||
|
## 总结先说
|
||||||
|
- 当前游戏的剧情骨架已经能让玩家在武侠、仙侠两个世界里感到“我正在追一件事”,整体判断为:**部分达到预期**。
|
||||||
|
- 强项在于:场景残痕、地图推进、NPC 保留、线程结构已经开始互相咬合。
|
||||||
|
- 短板也很明确:强回收、强情感爆点、真正能改写后续理解的长线后果还没有完全跑起来。
|
||||||
|
|
||||||
|
## 方法
|
||||||
|
- 先把当前仓库里的可扮演角色、场景、场景 NPC、宝藏残痕原文整理出来。
|
||||||
|
- 再用现有 story engine 模块补出 ThemePack、WorldStoryGraph、ActorNarrativeProfile、KnowledgeGraph、ThreadContract、QA Report 和 Release Gate。
|
||||||
|
- 最后按“玩家真实会感受到什么剧情”重组样章,并对照 PRD 的经典 RPG 体验目标做评测。
|
||||||
|
|
||||||
|
## 武侠世界
|
||||||
|
|
||||||
|
### 说明
|
||||||
|
- “原文”部分整理的是当前仓库角色、场景、NPC 和残痕里已经存在的中文文本。
|
||||||
|
- “引擎整理”部分是根据这些原文,经过 story engine 的主题包、线程图谱、角色叙事档案和 QA 规则重新编译出的结构化结果。
|
||||||
|
|
||||||
|
### 项目内原始剧情文本整理
|
||||||
|
### 可扮演角色原文
|
||||||
|
- 剑之公主 / 王庭剑姬
|
||||||
|
角色原文:以迅疾剑技和正面压制见长,适合喜欢凌厉推进的玩家。
|
||||||
|
背景原文:王庭旁支出身,自幼被当作执剑者培养。一次宫变让她失去旧有庇护,也背上了亲手追回王室誓剑与真相的责任。
|
||||||
|
表层来意:以迅疾剑技和正面压制见长,适合喜欢凌厉推进的玩家。
|
||||||
|
- 神箭游侠 / 流风弓卫
|
||||||
|
角色原文:擅长远距离压制与精准射击,节奏灵活,机动性很强。
|
||||||
|
背景原文:曾是边境游骑与斥候,被一场伏击逼得离开旧军阵。如今他只信自己亲眼见过的风向与箭路,却仍背着守住边境故土的旧誓。
|
||||||
|
表层来意:擅长远距离压制与精准射击,节奏灵活,机动性很强。
|
||||||
|
- 双刃旅者 / 疾影斥候
|
||||||
|
角色原文:速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。
|
||||||
|
背景原文:她在暗巷与帮派追杀中长大,学会靠速度、直觉和先手活下去。表面上轻快利落,心里却一直在追查那封改变命运的密信去向。
|
||||||
|
表层来意:速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。
|
||||||
|
|
||||||
|
### 场景角色原文
|
||||||
|
- 神箭游侠 / 流风弓卫
|
||||||
|
角色原文:擅长远距离压制与精准射击,节奏灵活,机动性很强。
|
||||||
|
保留线索:曾是边境游骑与斥候,被一场伏击逼得离开旧军…
|
||||||
|
- 青鳞毒蛇 / 敌对角色
|
||||||
|
角色原文:身形细长,吐信极快,最喜欢守在草木掩映和石缝交错之地。
|
||||||
|
保留线索:青鳞毒蛇长期出没于竹林古道。身形细长,吐信…
|
||||||
|
- 枯藤伏虫 / 敌对角色
|
||||||
|
角色原文:像一截会蠕动的枯藤,贴地潜行,适合在林地和湿地里伏击。
|
||||||
|
保留线索:枯藤伏虫长期出没于竹林古道。像一截会蠕动的…
|
||||||
|
- 樵夫老周 / 樵夫
|
||||||
|
角色原文:常在竹海边缘砍柴,对附近路数和兽踪了如指掌。
|
||||||
|
保留线索:樵夫老周长期出没于竹林古道。常在竹海边缘砍…
|
||||||
|
- 玄甲战锋 / 重装先锋
|
||||||
|
角色原文:攻守兼备,推进稳健,适合喜欢扎实前排风格的玩家。
|
||||||
|
保留线索:他长期担任重装前锋,习惯站在最危险的位置替…
|
||||||
|
- 石背蜗怪 / 敌对角色
|
||||||
|
角色原文:驮着厚重石壳缓慢爬行,常盘踞在石阶、桥边与潮湿山路上。
|
||||||
|
保留线索:石背蜗怪长期出没于山门石阶。驮着厚重石壳缓…
|
||||||
|
|
||||||
|
### 场景原文整理
|
||||||
|
- 竹林古道
|
||||||
|
场景原文:风过竹叶如刀鸣,窄道蜿蜒向深处,最适合藏伏毒物和游侠。
|
||||||
|
第一残痕:竹根旁半埋的刀鞘
|
||||||
|
场景角色:神箭游侠(流风弓卫)、青鳞毒蛇(敌对角色)、枯藤伏虫(敌对角色)
|
||||||
|
- 山门石阶
|
||||||
|
场景原文:青石阶层层向上,旧山门半开半掩,守山人与伏兽都能藏得很稳。
|
||||||
|
第一残痕:裂缝里的铜钥
|
||||||
|
场景角色:玄甲战锋(重装先锋)、石背蜗怪(敌对角色)、岩甲蛛兽(敌对角色)
|
||||||
|
- 雨夜长街
|
||||||
|
场景原文:长街积水映灯,屋檐下尽是藏身空隙,最易碰见追踪者与夜行客。
|
||||||
|
第一残痕:灯檐下浸湿的布包
|
||||||
|
场景角色:双刃旅者(疾影斥候)、夜牙潜兽(敌对角色)、孢爆菇灵(敌对角色)
|
||||||
|
- 荒村断垣
|
||||||
|
场景原文:残墙和空屋挤成一团,风里总像夹着旧哭声与游荡脚步。
|
||||||
|
第一残痕:断墙后压着的木匣
|
||||||
|
场景角色:断骨祟灵(敌对角色)、孢爆菇灵(敌对角色)、守村妇人(遗民)
|
||||||
|
- 古桥渡口
|
||||||
|
场景原文:桥面潮湿,渡口雾重,来往之人不多,但每个身影都藏着故事。
|
||||||
|
第一残痕:桥柱缝里的油纸包
|
||||||
|
场景角色:双刃旅者(疾影斥候)、石背蜗怪(敌对角色)、夜牙潜兽(敌对角色)
|
||||||
|
|
||||||
|
### 引擎整理出的明线
|
||||||
|
- 旧宫旧案仍在牵动江湖局势:旧宫旧案仍在牵动江湖局势,焦点常落在竹林古道。
|
||||||
|
- 护送线:边关与地宫残痕正在把旧事重新拖回台前,焦点常落在山门石阶。
|
||||||
|
- 回收线:当前武侠世界不是单点冒险,而是一张由边关军需、渡口风声、地宫旧痕和宫苑旧案交叉拉紧的追查网络。,焦点常落在雨夜长街。
|
||||||
|
- 分歧对峙线:沿着场景残痕和人物试探,一步步追清边关与宫苑旧案背后的真相,焦点常落在荒村断垣。
|
||||||
|
|
||||||
|
### 引擎整理出的暗线
|
||||||
|
- 神箭游侠的隐线:神箭游侠并不只是流风弓卫,他与旧宫旧案仍在牵动江湖局势之间还有一段未被说破的牵连。
|
||||||
|
- 青鳞毒蛇的隐线:青鳞毒蛇并不只是敌对角色,他与护送线之间还有一段未被说破的牵连。
|
||||||
|
- 枯藤伏虫的隐线:枯藤伏虫并不只是敌对角色,他与回收线之间还有一段未被说破的牵连。
|
||||||
|
- 樵夫老周的隐线:樵夫老周并不只是樵夫,他与分歧对峙线之间还有一段未被说破的牵连。
|
||||||
|
- 玄甲战锋的隐线:玄甲战锋并不只是重装先锋,他与旧宫旧案仍在牵动江湖局势之间还有一段未被说破的牵连。
|
||||||
|
|
||||||
|
### 场景旧痕
|
||||||
|
- 竹林古道留下的旧痕:表层残痕是“风过竹叶如刀鸣,窄道蜿蜒向深处,最适合藏伏毒物和游侠。”;压着的真相是“神箭游侠并不只是流风弓卫,他与旧宫旧案仍在牵动江湖局势之间还有一段未被说破的牵连。”
|
||||||
|
- 山门石阶留下的旧痕:表层残痕是“青石阶层层向上,旧山门半开半掩,守山人与伏兽都能藏得很稳。”;压着的真相是“青鳞毒蛇并不只是敌对角色,他与护送线之间还有一段未被说破的牵连。”
|
||||||
|
- 雨夜长街留下的旧痕:表层残痕是“长街积水映灯,屋檐下尽是藏身空隙,最易碰见追踪者与夜行客。”;压着的真相是“枯藤伏虫并不只是敌对角色,他与回收线之间还有一段未被说破的牵连。”
|
||||||
|
- 荒村断垣留下的旧痕:表层残痕是“残墙和空屋挤成一团,风里总像夹着旧哭声与游荡脚步。”;压着的真相是“樵夫老周并不只是樵夫,他与分歧对峙线之间还有一段未被说破的牵连。”
|
||||||
|
- 古桥渡口留下的旧痕:表层残痕是“桥面潮湿,渡口雾重,来往之人不多,但每个身影都藏着故事。”;压着的真相是“玄甲战锋并不只是重装先锋,他与旧宫旧案仍在牵动江湖局势之间还有一段未被说破的牵连。”
|
||||||
|
|
||||||
|
### 玩家在游戏中真实感受到的剧情样章
|
||||||
|
你来到这个武侠世界,是为追查失落王庭誓剑流入江湖的踪迹。此行最重要的目标,是在诸门派与野心家之前找回誓剑,并逼出宫变幕后之人。 第一眼看到的不是纯说明,而是 边关营地 里的环境压迫:营火与旌旗都带着风沙味,士卒、斥候和异兽都可能在这里短暂停留。
|
||||||
|
走进边关营地时,玩家实际感受到的核心不是“到了新地图”,而是“营火与旌旗都带着风沙味,士卒、斥候和异兽都可能在这里短暂停留。”。这句场景原文会立刻把体验拉回到“旧宫旧案仍在牵动江湖局势”这条明线。神箭游侠表面只是流风弓卫,但他的公开面是“擅长远距离压制与精准射击,节奏灵活,机动性很强。”,真正压在肩上的却是“找出贩卖军情的人,并截回被转移的军械账册”。而像“废营帐里的箭囊”这样的场景残痕,会把玩家往“这里一定还藏着别的事”那种感觉里继续推。如果继续追下去,边关营地背后会逐渐显出“神箭游侠并不只是流风弓卫,他与旧宫旧案仍在牵动江湖局势之间还有一段未被说破的牵连。”这层旧伤。
|
||||||
|
走进雨夜长街时,玩家实际感受到的核心不是“到了新地图”,而是“长街积水映灯,屋檐下尽是藏身空隙,最易碰见追踪者与夜行客。”。这句场景原文会立刻把体验拉回到“护送线”这条明线。双刃旅者表面只是疾影斥候,但他的公开面是“速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。”,真正压在肩上的却是“夺回密信,查清究竟是谁把你推上了被追杀的路”。而像“灯檐下浸湿的布包”这样的场景残痕,会把玩家往“这里一定还藏着别的事”那种感觉里继续推。如果继续追下去,雨夜长街背后会逐渐显出“青鳞毒蛇并不只是敌对角色,他与护送线之间还有一段未被说破的牵连。”这层旧伤。
|
||||||
|
走进古桥渡口时,玩家实际感受到的核心不是“到了新地图”,而是“桥面潮湿,渡口雾重,来往之人不多,但每个身影都藏着故事。”。这句场景原文会立刻把体验拉回到“回收线”这条明线。双刃旅者表面只是疾影斥候,但他的公开面是“速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。”,真正压在肩上的却是“夺回密信,查清究竟是谁把你推上了被追杀的路”。而像“桥柱缝里的油纸包”这样的场景残痕,会把玩家往“这里一定还藏着别的事”那种感觉里继续推。如果继续追下去,古桥渡口背后会逐渐显出“枯藤伏虫并不只是敌对角色,他与回收线之间还有一段未被说破的牵连。”这层旧伤。
|
||||||
|
因此,武侠世界目前最容易让玩家产生真实剧情感的地方,不是某一句高光台词,而是“场景描述 -> 人物保留 -> 残痕线索 -> 线程压力”这条连续链路。它已经能让玩家觉得自己在追一件还没完全揭开的事,但离“经典 RPG 式的强收束和强情感爆点”还差最后一层回响回收。
|
||||||
|
|
||||||
|
### 质量评测
|
||||||
|
整体判断:**部分达成预期**
|
||||||
|
|
||||||
|
### 维度评测
|
||||||
|
- 角色记忆点:达成。当前可扮演角色的人设、背景、开局动机和首遇目标已经能形成第一轮代入,玩家能记住“谁在上路、为什么上路”。
|
||||||
|
- 低关系也有戏:达成。低好感或首遇 NPC 不再只是“更冷淡”,而是能从当前压力、错位说辞和反应钩子里带出暗线存在感。
|
||||||
|
- 世界互文与旧史厚度:达成。场景、NPC、旧痕和线程已经能互相指向同一批旧事,不再只是各自独立的设定块。
|
||||||
|
- 空间与残痕叙事:达成。地点不是纯背景图,场景描述、宝藏线索和 narrative residue 已经能共同承担“空间会说话”的职责。
|
||||||
|
- 选择后果与主线抓手:部分达成。当前任务抓手和线程合约已经存在,但真正影响关系、理解和后续回响的后果层还没有被完全跑满。
|
||||||
|
- 长线回响与收束:未达成。从 QA 结果看,当前版本最明显的短板仍是“已经埋下的线,后面有没有被稳定回收”。这一步决定它能不能真正跨到经典 RPG 质感。
|
||||||
|
|
||||||
|
### 客观检查
|
||||||
|
- Narrative QA:4 条明线 / 1 条问题。
|
||||||
|
- Release Gate:warn。当前版本可继续观察,但仍有若干 narrative 风险。
|
||||||
|
- Simulation:共跑了 3 条 simulation,ending family 1 类,单次最高 QA 问题 1 条。
|
||||||
|
|
||||||
|
### 当前主要问题
|
||||||
|
- 有线程合约尚未在 chronicle 中留下足够的回收痕迹。
|
||||||
|
|
||||||
|
## 仙侠世界
|
||||||
|
|
||||||
|
### 说明
|
||||||
|
- “原文”部分整理的是当前仓库角色、场景、NPC 和残痕里已经存在的中文文本。
|
||||||
|
- “引擎整理”部分是根据这些原文,经过 story engine 的主题包、线程图谱、角色叙事档案和 QA 规则重新编译出的结构化结果。
|
||||||
|
|
||||||
|
### 项目内原始剧情文本整理
|
||||||
|
### 可扮演角色原文
|
||||||
|
- 剑之公主 / 王庭剑姬
|
||||||
|
角色原文:以迅疾剑技和正面压制见长,适合喜欢凌厉推进的玩家。
|
||||||
|
背景原文:王庭旁支出身,自幼被当作执剑者培养。一次宫变让她失去旧有庇护,也背上了亲手追回王室誓剑与真相的责任。
|
||||||
|
表层来意:以迅疾剑技和正面压制见长,适合喜欢凌厉推进的玩家。
|
||||||
|
- 神箭游侠 / 流风弓卫
|
||||||
|
角色原文:擅长远距离压制与精准射击,节奏灵活,机动性很强。
|
||||||
|
背景原文:曾是边境游骑与斥候,被一场伏击逼得离开旧军阵。如今他只信自己亲眼见过的风向与箭路,却仍背着守住边境故土的旧誓。
|
||||||
|
表层来意:擅长远距离压制与精准射击,节奏灵活,机动性很强。
|
||||||
|
- 双刃旅者 / 疾影斥候
|
||||||
|
角色原文:速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。
|
||||||
|
背景原文:她在暗巷与帮派追杀中长大,学会靠速度、直觉和先手活下去。表面上轻快利落,心里却一直在追查那封改变命运的密信去向。
|
||||||
|
表层来意:速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。
|
||||||
|
|
||||||
|
### 场景角色原文
|
||||||
|
- 神箭游侠 / 流风弓卫
|
||||||
|
角色原文:擅长远距离压制与精准射击,节奏灵活,机动性很强。
|
||||||
|
保留线索:曾是边境游骑与斥候,被一场伏击逼得离开旧军…
|
||||||
|
- 玄甲战锋 / 重装先锋
|
||||||
|
角色原文:攻守兼备,推进稳健,适合喜欢扎实前排风格的玩家。
|
||||||
|
保留线索:他长期担任重装前锋,习惯站在最危险的位置替…
|
||||||
|
- 秘匣书妖 / 敌对角色
|
||||||
|
角色原文:像会自行翻页的秘典与宝匣,常在仙门、遗迹与禁制附近浮游。
|
||||||
|
保留线索:秘匣书妖长期出没于云海仙门。像会自行翻页的…
|
||||||
|
- 噬雾飞蛾 / 敌对角色
|
||||||
|
角色原文:借雾气遮身,飞行轨迹诡谲,喜欢围着灵光和人影打转。
|
||||||
|
保留线索:噬雾飞蛾长期出没于云海仙门。借雾气遮身,飞…
|
||||||
|
- 守门灵官 / 门官
|
||||||
|
角色原文:站在门阙侧旁观来者,像在等一份迟迟未到的回报。
|
||||||
|
保留线索:守门灵官长期出没于云海仙门。站在门阙侧旁观…
|
||||||
|
- 幽烬灵蝠 / 敌对角色
|
||||||
|
角色原文:翅翼缭绕灰烬般的灵火,常成群出没于洞天、崖壁与灵脉附近。
|
||||||
|
保留线索:幽烬灵蝠长期出没于悬空仙岛。翅翼缭绕灰烬般…
|
||||||
|
|
||||||
|
### 场景原文整理
|
||||||
|
- 云海仙门
|
||||||
|
场景原文:云阶在脚下翻涌,门阙后方灵光不断,来客与守门异物都极显眼。
|
||||||
|
第一残痕:云阶尽头的灵符匣
|
||||||
|
场景角色:神箭游侠(流风弓卫)、玄甲战锋(重装先锋)、秘匣书妖(敌对角色)
|
||||||
|
- 悬空仙岛
|
||||||
|
场景原文:浮岛边缘风大云急,灵禽与飞蛾总绕着岛沿的光带盘旋。
|
||||||
|
第一残痕:浮岛边缘的灵羽匣
|
||||||
|
场景角色:幽烬灵蝠(敌对角色)、噬雾飞蛾(敌对角色)、云栖散修(散修)
|
||||||
|
- 天宫长廊
|
||||||
|
场景原文:廊柱之间回响着空灵风声,禁制和书妖都喜欢寄在这类高处回廊里。
|
||||||
|
第一残痕:廊柱暗槽里的玉简
|
||||||
|
场景角色:剑之公主(王庭剑姬)、玄甲战锋(重装先锋)、秘匣书妖(敌对角色)
|
||||||
|
- 灵药花圃
|
||||||
|
场景原文:灵草灵花层层叠开,香气诱人,却也最容易养出食灵的怪物。
|
||||||
|
第一残痕:药圃深处的灵壶
|
||||||
|
场景角色:噬灵妖花(敌对角色)、血瞳妖眼(敌对角色)、药圃执事(药师)
|
||||||
|
- 寒玉洞天
|
||||||
|
场景原文:洞壁结着寒玉光泽,地面湿滑,水灵和阴性异物都爱停在这里。
|
||||||
|
第一残痕:寒玉裂隙里的灵髓
|
||||||
|
场景角色:青腐泥灵(敌对角色)、幽烬灵蝠(敌对角色)、澄潮灵母(敌对角色)
|
||||||
|
|
||||||
|
### 引擎整理出的明线
|
||||||
|
- 灵脉与封印正在失衡:灵脉与封印正在失衡,焦点常落在云海仙门。
|
||||||
|
- 追索线:宗门旧案与秘境争夺彼此缠住了当下局势,焦点常落在悬空仙岛。
|
||||||
|
- 封印失衡线:当前仙侠世界由宗门秩序、秘境余波、灵脉封印和古仙残迹共同推着故事前进,玩家每深入一层,都会撞上新的旧事回响。,焦点常落在天宫长廊。
|
||||||
|
- 宗门旧案线:顺着灵痕、残识和人物保留,一层层摸清宗门旧案与秘境失衡的根源,焦点常落在灵药花圃。
|
||||||
|
|
||||||
|
### 引擎整理出的暗线
|
||||||
|
- 神箭游侠的隐线:神箭游侠并不只是流风弓卫,他与灵脉与封印正在失衡之间还有一段未被说破的牵连。
|
||||||
|
- 玄甲战锋的隐线:玄甲战锋并不只是重装先锋,他与追索线之间还有一段未被说破的牵连。
|
||||||
|
- 秘匣书妖的隐线:秘匣书妖并不只是敌对角色,他与封印失衡线之间还有一段未被说破的牵连。
|
||||||
|
- 噬雾飞蛾的隐线:噬雾飞蛾并不只是敌对角色,他与宗门旧案线之间还有一段未被说破的牵连。
|
||||||
|
- 守门灵官的隐线:守门灵官并不只是门官,他与灵脉与封印正在失衡之间还有一段未被说破的牵连。
|
||||||
|
|
||||||
|
### 场景旧痕
|
||||||
|
- 云海仙门留下的旧痕:表层残痕是“云阶在脚下翻涌,门阙后方灵光不断,来客与守门异物都极显眼。”;压着的真相是“神箭游侠并不只是流风弓卫,他与灵脉与封印正在失衡之间还有一段未被说破的牵连。”
|
||||||
|
- 悬空仙岛留下的旧痕:表层残痕是“浮岛边缘风大云急,灵禽与飞蛾总绕着岛沿的光带盘旋。”;压着的真相是“玄甲战锋并不只是重装先锋,他与追索线之间还有一段未被说破的牵连。”
|
||||||
|
- 天宫长廊留下的旧痕:表层残痕是“廊柱之间回响着空灵风声,禁制和书妖都喜欢寄在这类高处回廊里。”;压着的真相是“秘匣书妖并不只是敌对角色,他与封印失衡线之间还有一段未被说破的牵连。”
|
||||||
|
- 灵药花圃留下的旧痕:表层残痕是“灵草灵花层层叠开,香气诱人,却也最容易养出食灵的怪物。”;压着的真相是“噬雾飞蛾并不只是敌对角色,他与宗门旧案线之间还有一段未被说破的牵连。”
|
||||||
|
- 寒玉洞天留下的旧痕:表层残痕是“洞壁结着寒玉光泽,地面湿滑,水灵和阴性异物都爱停在这里。”;压着的真相是“守门灵官并不只是门官,他与灵脉与封印正在失衡之间还有一段未被说破的牵连。”
|
||||||
|
|
||||||
|
### 玩家在游戏中真实感受到的剧情样章
|
||||||
|
你来到这个仙侠世界,是因为王庭圣印坠入了云海裂隙。此行最重要的目标,是寻回圣印,截断那些企图借它开启天门禁制的野心。 第一眼看到的不是纯说明,而是 星舟甲板 里的环境压迫:甲板横在高天之上,风压和星光都很强,飞行异物最爱在这里盘旋。
|
||||||
|
走进星舟甲板时,玩家实际感受到的核心不是“到了新地图”,而是“甲板横在高天之上,风压和星光都很强,飞行异物最爱在这里盘旋。”。这句场景原文会立刻把体验拉回到“灵脉与封印正在失衡”这条明线。神箭游侠表面只是流风弓卫,但他的公开面是“擅长远距离压制与精准射击,节奏灵活,机动性很强。”,真正压在肩上的却是“找回星图核心,查清是谁击落了你的船队”。而像“舵台后的星图匣”这样的场景残痕,会把玩家往“这里一定还藏着别的事”那种感觉里继续推。如果继续追下去,星舟甲板背后会逐渐显出“神箭游侠并不只是流风弓卫,他与灵脉与封印正在失衡之间还有一段未被说破的牵连。”这层旧伤。
|
||||||
|
走进悬空仙岛时,玩家实际感受到的核心不是“到了新地图”,而是“浮岛边缘风大云急,灵禽与飞蛾总绕着岛沿的光带盘旋。”。这句场景原文会立刻把体验拉回到“追索线”这条明线。云栖散修表面只是散修,但他的公开面是“常坐在浮岛边缘打坐,对天风和禁制的变化很敏感。”,真正压在肩上的却是“在悬空仙岛守住自己不愿失去的那一层秩序。”。而像“浮岛边缘的灵羽匣”这样的场景残痕,会把玩家往“这里一定还藏着别的事”那种感觉里继续推。如果继续追下去,悬空仙岛背后会逐渐显出“玄甲战锋并不只是重装先锋,他与追索线之间还有一段未被说破的牵连。”这层旧伤。
|
||||||
|
走进月湖仙洲时,玩家实际感受到的核心不是“到了新地图”,而是“湖光像铺开的镜面,水灵、章灵与花影都可能从月色里浮出来。”。这句场景原文会立刻把体验拉回到“封印失衡线”这条明线。双刃旅者表面只是疾影斥候,但他的公开面是“速度快、侵略性强,适合喜欢持续推进和连段压迫的玩家。”,真正压在肩上的却是“找到残阵核心,并弄明白信里提到的“第二个你”究竟是谁”。而像“湖岸边漂来的玉匣”这样的场景残痕,会把玩家往“这里一定还藏着别的事”那种感觉里继续推。如果继续追下去,月湖仙洲背后会逐渐显出“秘匣书妖并不只是敌对角色,他与封印失衡线之间还有一段未被说破的牵连。”这层旧伤。
|
||||||
|
因此,仙侠世界目前最容易让玩家产生真实剧情感的地方,不是某一句高光台词,而是“场景描述 -> 人物保留 -> 残痕线索 -> 线程压力”这条连续链路。它已经能让玩家觉得自己在追一件还没完全揭开的事,但离“经典 RPG 式的强收束和强情感爆点”还差最后一层回响回收。
|
||||||
|
|
||||||
|
### 质量评测
|
||||||
|
整体判断:**部分达成预期**
|
||||||
|
|
||||||
|
### 维度评测
|
||||||
|
- 角色记忆点:达成。当前可扮演角色的人设、背景、开局动机和首遇目标已经能形成第一轮代入,玩家能记住“谁在上路、为什么上路”。
|
||||||
|
- 低关系也有戏:达成。低好感或首遇 NPC 不再只是“更冷淡”,而是能从当前压力、错位说辞和反应钩子里带出暗线存在感。
|
||||||
|
- 世界互文与旧史厚度:达成。场景、NPC、旧痕和线程已经能互相指向同一批旧事,不再只是各自独立的设定块。
|
||||||
|
- 空间与残痕叙事:达成。地点不是纯背景图,场景描述、宝藏线索和 narrative residue 已经能共同承担“空间会说话”的职责。
|
||||||
|
- 选择后果与主线抓手:部分达成。当前任务抓手和线程合约已经存在,但真正影响关系、理解和后续回响的后果层还没有被完全跑满。
|
||||||
|
- 长线回响与收束:未达成。从 QA 结果看,当前版本最明显的短板仍是“已经埋下的线,后面有没有被稳定回收”。这一步决定它能不能真正跨到经典 RPG 质感。
|
||||||
|
|
||||||
|
### 客观检查
|
||||||
|
- Narrative QA:4 条明线 / 1 条问题。
|
||||||
|
- Release Gate:warn。当前版本可继续观察,但仍有若干 narrative 风险。
|
||||||
|
- Simulation:共跑了 3 条 simulation,ending family 1 类,单次最高 QA 问题 1 条。
|
||||||
|
|
||||||
|
### 当前主要问题
|
||||||
|
- 有线程合约尚未在 chronicle 中留下足够的回收痕迹。
|
||||||
|
|
||||||
|
## 最终结论
|
||||||
|
- 如果目标只是“让玩家进入游戏后立刻感觉世界里有事正在发生”,当前文本资产已经够用,且部分环节已经明显跑起来了。
|
||||||
|
- 如果目标是“对标经典单机 RPG 的强角色回响、强关系后果、强主线收束”,当前版本还只能算走到了一半,最该补的是 payoff 和长线回响。
|
||||||
|
- 也就是说:**当前剧情原文的底座已经部分达到预期,但还没到可以完全放心交给玩家沉浸式吃剧情的程度。**
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
# 游戏 UI / 预设 / 编辑器 / npcInteraction / prompt 文本深度审计
|
||||||
|
|
||||||
|
日期:`2026-04-02`
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 本文档基于当前仓库源码再次深搜,不直接沿用旧审计结论。
|
||||||
|
- 审计目标:找出“可能出现在游戏 UI、预设、编辑器 UI、生成链路中的文本”里,仍然存在的:
|
||||||
|
- `中文乱码`
|
||||||
|
- `英文直出`
|
||||||
|
- `中英混用`
|
||||||
|
- 本轮重点加查:
|
||||||
|
- `src/data/npcInteractions.ts`
|
||||||
|
- `src/services/prompt.ts`
|
||||||
|
- `src/services/characterChatPrompt.ts`
|
||||||
|
- `src/services/questPrompt.ts`
|
||||||
|
- 说明口径:
|
||||||
|
- “会显示给玩家/编辑器使用者”的文本,按高优先级记录。
|
||||||
|
- “内部英文枚举/键名,但可能泄露到 prompt、编辑器或预览”的内容,也单独记录。
|
||||||
|
- 已经转成中文且当前复查无明显问题的区域,会标记为“复查通过”。
|
||||||
|
|
||||||
|
## 结论摘要
|
||||||
|
|
||||||
|
- 当前最严重的文本污染源不是单一 UI,而是两条链路同时存在问题:
|
||||||
|
- 运行时弹窗 / 实体详情 / NPC 交易招募弹窗
|
||||||
|
- `prompt` 生成链路
|
||||||
|
- `src/services/prompt.ts` 是当前最重灾区,既有大面积中文乱码,也混有英文结构词,会直接影响 AI 生成质量。
|
||||||
|
- `src/data/npcInteractions.ts` 里“话题 actionText / detailText”大体已是正常中文,但商人来源匹配、库存种子物品类别/名称仍有明显乱码,而且阶段枚举仍是英文值。
|
||||||
|
- 预设编辑器仍然是英文和乱码高密度区,尤其是:
|
||||||
|
- `CharacterPresetPanel.tsx`
|
||||||
|
- `MonsterPresetPanel.tsx`
|
||||||
|
- `SceneNpcPresetPanel.tsx`
|
||||||
|
- `ScenePresetPanel.tsx`
|
||||||
|
- `StateFunctionEditor.tsx`
|
||||||
|
- 自定义世界 NPC/场景编辑器、NPC 视觉编辑器大体已转中文,但还残留 `NPC`、`AI`、`Shift` 等中英混用。
|
||||||
|
|
||||||
|
## 一、游戏主流程 UI 与运行时面板
|
||||||
|
|
||||||
|
### 1.1 复查通过
|
||||||
|
|
||||||
|
| 文件 | 行号 | 当前状态 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `47-49` | 通过 | 开场联系方式已改成 `QQ群`、`微信` |
|
||||||
|
| `src/components/game-shell/GameShellRuntime.tsx` | `147` | 通过 | 场景名兜底已改成 `当前区域` |
|
||||||
|
| `src/components/adventure-panel/AdventurePanelOverlays.tsx` | `137-158` | 通过 | 任务目标眉标、宝藏踪迹、切磋会话等主文案已是中文 |
|
||||||
|
|
||||||
|
### 1.2 仍有问题
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `81`、`89` | 中英混用 | `正在生成核心NPC...` | `NPC` 仍在中文进度文案里直出 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `151` | 中文乱码 | `闃熶紞` / `鑳屽寘` | 队伍/背包弹层标题已写坏 |
|
||||||
|
| `src/components/game-shell/GameShellRuntime.tsx` | `200-201` | 英文直出 | `GENARRATIVE` | 运行时头部品牌字样仍为英文 |
|
||||||
|
| `src/components/adventure-panel/AdventurePanelOverlays.tsx` | `140` | 中英混用 | `未知敌对 NPC` | 已非乱码,但仍混入 `NPC` |
|
||||||
|
|
||||||
|
## 二、实体详情、NPC 交互弹窗、交易/招募 UI
|
||||||
|
|
||||||
|
### 2.1 `AdventureEntityModal.tsx`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `111` | 中文乱码 | `鏁屽 NPC` | hostile badge 已坏,同时混入 `NPC` |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `113`、`137-146` | 英文直出 | `NPC`、`Player`、`Companion` | 标题/副标题兜底值仍为英文 |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `221`、`264`、`272`、`283`、`296`、`306` | 英文直出 | `Portrait`、`Status`、`Relation`、`Attributes`、`Encounter`、`Inventory` | 六个主区块标题都未本地化 |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `266-267` | 英文直出 | `HP`、`MP` | 状态条标签未本地化 |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `274-275` | 英文直出 | `Affinity`、`Recruited: Yes/No` | 关系区块仍是英文 |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `298-301` | 英文直出 | `Name`、`Context`、`Type`、`Battle mode` | 遭遇信息区块仍是英文 |
|
||||||
|
|
||||||
|
### 2.2 `NpcModals.tsx`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/NpcModals.tsx` | `251-252` | 英文直出 | `NPC inventory` / `Your inventory` / `items` | 交易栏标题和数量单位仍是英文 |
|
||||||
|
| `src/components/NpcModals.tsx` | `272` | 英文直出 | `This NPC has nothing to sell right now.` / `You have nothing to sell right now.` | 空状态未本地化 |
|
||||||
|
| `src/components/NpcModals.tsx` | `290` | 英文直出 | `Purchase total` / `Sale total` | 交易总价标题未本地化 |
|
||||||
|
| `src/components/NpcModals.tsx` | `297` | 中文乱码 | 大段价格不足提示 | 购买资金不足提示整段已坏 |
|
||||||
|
| `src/components/NpcModals.tsx` | `303` | 中文乱码 | 大段默认说明 | 未选中物品时的说明区整段已坏 |
|
||||||
|
| `src/components/NpcModals.tsx` | `350-399` | 英文直出 | `Item details`、`NPC item`、`Stock`、`Value`、`Purchase price`、`Buyback price`、`Slot`、`Not equippable`、`Usable immediately`、`Tags`、`None` | 物品详情弹窗几乎整屏英文 |
|
||||||
|
| `src/components/NpcModals.tsx` | `404` | 中文乱码 + 中英混用 | 含 `MP` 的恢复说明 | 物品使用效果提示已坏 |
|
||||||
|
| `src/components/NpcModals.tsx` | `465` | 中文乱码 | 好感变动提示 | 交易结果反馈文案已坏 |
|
||||||
|
| `src/components/NpcModals.tsx` | `500-501` | 英文直出 | `Manage companion slot`、`Select a current companion to rotate out before recruiting this NPC.` | 招募替换同伴弹窗未本地化 |
|
||||||
|
|
||||||
|
## 三、预设编辑器与编辑器 UI
|
||||||
|
|
||||||
|
### 3.1 复查通过或基本通过
|
||||||
|
|
||||||
|
| 文件 | 行号 | 当前状态 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `242` | 通过 | 图片路径占位文案已中文化,不再暴露 `URL` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `463` | 基本通过 | 空状态是中文,但仍混入 `NPC` |
|
||||||
|
| `src/components/CustomWorldNpcVisualEditor.tsx` | `88-91`、`552-765` | 基本通过 | 主体编辑项、装备类型、素材、姿态等基本都为中文 |
|
||||||
|
|
||||||
|
### 3.2 标签、枚举、面板标题问题
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/shared.ts` | `43` | 英文直出 | `NPC` | 预设编辑器顶层页签仍用英文 |
|
||||||
|
| `src/components/preset-editor/shared.ts` | `62-67` | 英文枚举外露 | `idle`、`move`、`attack`、`die` | 怪物动画候选仍是英文值 |
|
||||||
|
| `src/components/preset-editor/shared.ts` | `69-74` | 英文枚举外露 | `steady`、`burst`、`mobility`、`finisher`、`projectile` | 技能风格候选仍是英文值 |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `702-708` | 中英混用 | `NPC 视觉编辑器` | 中文标题中混入 `NPC` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `718` | 中英混用 | `当前 NPC` | 选择器标签混入 `NPC` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `976-977` | 中英混用 | `拖动标记微调 NPC 的预览布局。移动时按住 Shift...` | 混入 `NPC`、`Shift` |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `482-483` | 中英混用 | `AI生成NPC形象`、`NPC 形象 AI 生成功能仍在开发中。` | 模态标题与副标题仍大量混用英文术语 |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `635-636` | 中英混用 | `新增 NPC`、`编辑 NPC` | 主标题和副标题混入 `NPC` |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `734`、`762-763` | 中英混用 | `AI生成`、`AI生成场景`、`场景图片 AI 生成功能仍在开发中。` | 场景生成入口仍混入 `AI` |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `792-793` | 中英混用 | `npc-...`、`自定义NPC...` | 新建 NPC 默认 ID/名称混有英文前缀与 `NPC` |
|
||||||
|
|
||||||
|
### 3.3 `CharacterPresetPanel.tsx`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `262`、`264`、`277` | 中文乱码 | `瑙掕壊`、`淇濆瓨瑙掕壊瑕嗙洊` | 选择卡标题、下拉标签、保存按钮均已坏 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `320`、`326` | 中文乱码 | `鍔ㄧ敾`、`闁煎啿...` | 动画/世界标签已坏 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `418`、`427` | 中文乱码 | `涓栫晫`、`棰勮鎬墿` | 技能预览区字段标签已坏 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `313-314`、`402-403` | 通过 | `角色详情`、`技能预览` | 主体段落标题已是正常中文 |
|
||||||
|
|
||||||
|
### 3.4 `MonsterPresetPanel.tsx`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `49-50` | 英文直出 | `Saved monster overrides...`、`Failed to save monster overrides.` | 保存反馈未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `58-59` | 中文乱码 | 空状态整段提示 | 无怪物时的空态文案已坏 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `124-126` | 英文直出 | `Section`、`Editor section.`、`Field` | 左侧选择卡是占位英文 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `135` | 中文乱码 | `闂?` | 世界与名字之间的分隔符已坏 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `139` | 英文直出 | `Save Monster Overrides` | 保存按钮未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `158-159` | 英文直出 | `Monster Override Preview`、`Editor section.` | 右侧预览卡标题/说明未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `165`、`218`、`223`、`228`、`240`、`248`、`256` | 英文直出 | `Field`、`Name`、`Intro Action` | 多个字段标签仍是英文或占位文案 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `335` | 中文乱码 | 一段动画配置标签乱码 | 帧数相关字段标题已坏 |
|
||||||
|
|
||||||
|
### 3.5 `SceneNpcPresetPanel.tsx`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `159` | 英文直出 | `No NPC presets are available.` | 空状态未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `177-193` | 英文直出 | `NPC Library`、`Browse and select an NPC preset.`、`NPC ID`、`Save NPC Overrides` | 选择区整块未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `221-238` | 英文直出 | `Skill Preview`、`Skill`、`World` | 技能预览区未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `258-259` | 中文乱码 | 空状态/说明整段乱码 | 预览区已有损坏文本 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `264-268` | 英文直出 | `Visual Preview`、`Hostile NPCs use monster presets...`、`Narrative NPCs can preview...` | 视觉预览说明未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `310-357` | 英文直出 | `NPC Details`、`NPC ID`、`Name`、`Role`、`Avatar`、`Linked Character ID`、`Monster Preset ID`、`Initial Affinity`、`Description` | 详情字段整体未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `363` | 中文乱码 | 预览说明大段乱码 | NPC 预览描述区已坏 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `371-375` | 英文直出 | `Visual Editor`、`Hostile NPCs cannot use the visual editor...` | 可视编辑器说明未本地化 |
|
||||||
|
|
||||||
|
### 3.6 `ScenePresetPanel.tsx`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `105-108` | 英文直出 | `Treasure Ahead`、`Treasure` | 宝藏预览实体名/头像仍是英文 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `132-133` | 英文直出 | `Scene Library`、`Browse and select a scene preset.` | 左侧选择区未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `162` | 英文直出 | `Save` | 保存按钮未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `170-181` | 英文直出 | `Scene Preview`、`Preview Mode`、`Monster Preview`、`NPC Preview`、`Treasure Preview`、`Empty` | 预览模式整组未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `207-226` | 英文直出 | `Hostile NPCs`、`NPCs`、`Treasure Hint`、`None` | 场景摘要卡未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `233-299` | 英文直出 | `Scene Details`、`Scene ID`、`World`、`Name`、`Description`、`Image Source`、`Forward Scene`、`Unset`、`Connected Scene IDs`、`Monster IDs`、`Treasure Hints`、`NPCs In Scene` | 详情编辑区整块未本地化 |
|
||||||
|
|
||||||
|
### 3.7 `StateFunctionEditor.tsx` 与共享请求层
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1060-1064` | 英文直出 | `Failed to save option behavior overrides`、`Option behavior overrides saved.` | 保存结果提示未本地化 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1138` | 中英混用 | `GameState` | 预览说明里仍混入英文类型名 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1143` | 中英混用 | `敌对NPC资源` | `NPC` 混入字段标签 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1185` | 英文枚举外露 | `AnimationState` 原始值 | 玩家动作下拉会直接显示英文枚举值 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1190` | 英文占位符外露 | `{monster}` | 占位提示面向编辑器用户直接可见 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1191` | 英文枚举外露 | `idle`、`move`、`attack` | 怪物动画下拉仍使用英文值 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1213-1217` | 基本通过 | `稳扎稳打`、`爆发`、`机动` 等 | 技能权重显示标签已是中文,但底层 key 仍是英文 |
|
||||||
|
| `src/editor/shared/jsonClient.ts` | `29`、`43` | 英文直出 | `Request failed`、`Save failed` | 通用网络错误兜底未本地化,所有编辑器接口都可能透出 |
|
||||||
|
|
||||||
|
## 四、`npcInteractions.ts` 重点复查
|
||||||
|
|
||||||
|
### 4.1 复查通过
|
||||||
|
|
||||||
|
| 文件 | 行号 | 当前状态 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `src/data/npcInteractions.ts` | `507-657` | 通过 | `actionText` / `detailText` 话题库主体已是中文,适合作为后续清理基线 |
|
||||||
|
|
||||||
|
### 4.2 仍有问题
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/data/npcInteractions.ts` | `299-343` | 中文乱码 | `绋€鏈夊搧`、`鏉愭枡`、`琛屽晢鎶ょ`、`绮剧偧閾?`、`闃叉按琛屽泭`、`娌抽浘缃楃洏`、`鍏界毊`、`鐚庨拱缇藉潬`、`鎷撴湰鏂囧唽`、`娈嬬己鍦板浘`、`姝﹀櫒`、`鍒跺紡浣╁垁`、`鎶ょ敳`、`鎶よ噦`、`鏃у竷鍗?`、`闅忚韩鏃х墿` | 商人来源匹配、商品分类、商品名存在明显乱码,会直接流入交易 UI |
|
||||||
|
| `src/data/npcInteractions.ts` | `401-424` | 英文枚举外露 | `deep`、`honest`、`partial`、`guarded`、`warm`、`cooperative`、`neutral`、`distant`、`candid`、`true_but_incomplete`、`half_truth`、`situational_only` | 阶段/回答模式仍使用英文值,虽不一定直接给玩家看,但已进入 prompt 上下文 |
|
||||||
|
| `src/data/npcInteractions.ts` | `428-500` | 基本通过 | `已经愿意逐步谈到真实来历...` 等 | 描述层已是中文,可作为未来替换英文枚举时的文案来源 |
|
||||||
|
| `src/data/npcInteractions.ts` | `1199`、`1209`、`1243` | 中英混用 | `向该NPC送礼`、`邀请该NPC加入队伍`、`离开当前 NPC,重新回到探索状态。` | 选项和说明里仍混入 `NPC` |
|
||||||
|
| `src/data/npcInteractions.ts` | `1250` | 中英混用 | `当前好感为 ...` 同句包含 `NPC` | 遭遇总结文本仍有术语混入 |
|
||||||
|
|
||||||
|
## 五、`prompt` 链路重点复查
|
||||||
|
|
||||||
|
### 5.1 `src/services/prompt.ts`
|
||||||
|
|
||||||
|
这是当前最需要优先清理的文件。问题不是一两处,而是“结构性污染”:
|
||||||
|
|
||||||
|
- 前段中文描述已混入乱码。
|
||||||
|
- 中段人物/遭遇/场景/状态描述大面积乱码。
|
||||||
|
- 后段选项约束、战斗/观察/营地对话指令大面积乱码。
|
||||||
|
- 同时混有 `functionId`、`actionText`、`npc|treasure|none` 等英文结构词。
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/services/prompt.ts` | `145`、`151` | 中文乱码 | `锛堣嚜瀹氫箟...`、`鑷畾涔変笘鐣?...` | 自定义世界描述区已坏 |
|
||||||
|
| `src/services/prompt.ts` | `170-171`、`198-216` | 中文乱码 | 冒险开场理由、表层钩子、眼前顾虑、目标等整段乱码 | 角色开场信息会直接污染生成上下文 |
|
||||||
|
| `src/services/prompt.ts` | `226-233` | 中文乱码 + 英文枚举外露 | `NPC`、`deep`、`warm`、`answerMode` 等 | NPC 会话阶段控制段同时存在乱码和英文枚举 |
|
||||||
|
| `src/services/prompt.ts` | `378-451` | 中文乱码 + 中英混用 | 遭遇实体、敌对 `NPC`、玩家状态、场景说明等整段乱码 | 玩家、NPC、怪物、场景等核心提示都已受污染 |
|
||||||
|
| `src/services/prompt.ts` | `547-568` | 中文乱码 + 英文结构词外露 | `functionId`、`actionText`、`function` | 选项约束与动作重写规则段落已坏 |
|
||||||
|
| `src/services/prompt.ts` | `859-910` | 中文乱码 + 中英混用 | 空闲/遭遇函数说明、观察线索、开场营地跟进等整段乱码 | 后半段生成规则几乎不可维护 |
|
||||||
|
|
||||||
|
### 5.2 `src/services/characterChatPrompt.ts`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/services/characterChatPrompt.ts` | `42-54` | 英文直出 | `You are a companion character...`、`Generate exactly 3 player reply suggestions.`、`Summarize the evolving relationship...` | 系统 prompt 整段为英文 |
|
||||||
|
| `src/services/characterChatPrompt.ts` | `57-59` | 英文直出 | `Wuxia`、`Xianxia`、`Custom World` | 世界描述仍为英文 |
|
||||||
|
| `src/services/characterChatPrompt.ts` | `64`、`69-71`、`74-75` | 英文直出 | `Custom world reference`、`female`、`male`、`unknown`、`left`、`right` | 性别、朝向、扩展说明均为英文 |
|
||||||
|
| `src/services/characterChatPrompt.ts` | `99-112` | 英文直出 | `Recent story: none.`、`Earlier story summary`、`Most recent 3 story rounds` | 历史摘要模板为英文 |
|
||||||
|
| `src/services/characterChatPrompt.ts` | `123-155` | 英文直出 | `damage`、`mana`、`cooldown`、`arrival reason`、`current goal`、`world schema`、`top attributes` | 角色信息拼接模板整体为英文 |
|
||||||
|
|
||||||
|
### 5.3 `src/services/questPrompt.ts`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/services/questPrompt.ts` | `28-31` | 英文直出 | `issued by`、`No live quests` | 当前任务摘要模板为英文 |
|
||||||
|
| `src/services/questPrompt.ts` | `35-37` | 英文直出 | `Active companions`、`Roster companions` | 同伴摘要模板为英文 |
|
||||||
|
| `src/services/questPrompt.ts` | `41-52` | 英文直出 | `Player`、`HP`、`Mana`、`Inventory snapshot` | 玩家状态模板为英文 |
|
||||||
|
| `src/services/questPrompt.ts` | `67-95` | 英文直出 | `You are the quest director...` 等整段 | 任务意图系统 prompt 全英文 |
|
||||||
|
| `src/services/questPrompt.ts` | `107-123` | 英文直出 | `World`、`Issuer NPC`、`Encounter kind`、`Recent story moments` 等 | 最终拼接 prompt 仍是英文框架 |
|
||||||
|
|
||||||
|
### 5.4 `src/services/aiFallbacks.ts`
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/services/aiFallbacks.ts` | `5-17` | 英文直出 | `Player: ...`、`That question is not casual...`、`I accept...` | 离线 NPC 对话与招募兜底仍是英文 |
|
||||||
|
| `src/services/aiFallbacks.ts` | `28-35` | 英文直出 | `I will answer in my own way`、`I still remember...` | 离线角色私聊回复兜底为英文 |
|
||||||
|
| `src/services/aiFallbacks.ts` | `40-42` | 英文直出 | 三条候选回复 | 私聊建议兜底为英文 |
|
||||||
|
| `src/services/aiFallbacks.ts` | `52-57` | 英文直出 | `Player`、`Recent exchange`、`warmer toward the player` | 私聊摘要兜底为英文 |
|
||||||
|
|
||||||
|
## 六、其他会外溢到 UI / 预览 / 生成链路的文本源
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `578-580` | 通过 | `前往...`、`离开营地...` | 旅行选项已中文化 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `599`、`602` | 英文直出 | `approaches from a short distance away...`、`steps into view...` | 初始同伴结果文本仍是英文 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `814` | 英文直出 | `Continue the camp exchange and organize the most natural next actions.` | 营地跟进生成指令仍是英文 |
|
||||||
|
| `src/data/scenePresets.ts` | `195`、`297` | 英文枚举外露 | `trade`、`fight`、`spar`、`help`、`chat`、`recruit`、`gift` | 场景函数列表仍是英文 ID |
|
||||||
|
| `src/data/stateFunctions.ts` | `33` | 英文枚举外露 | `idle|move|attack` | 怪物动画枚举仍是英文 |
|
||||||
|
| `src/data/stateFunctions.ts` | `130`、`152`、`174`、`196`、`221`、`244` | 英文枚举外露 | `steady`、`burst`、`mobility`、`finisher`、`projectile` | 技能权重 key 仍为英文 |
|
||||||
|
| `src/data/characterPresets.ts` | `60-76` | 英文枚举外露 | `blunt`、`wary`、`evasive`、`measured`、`gentle`、`teasing`、`dry`、`steady`、`direct`、`fragmented`、`deflecting` | 对话风格推断值仍为英文,已进入 `npcInteractions` / `prompt` 描述链路 |
|
||||||
|
|
||||||
|
## 七、优先级建议
|
||||||
|
|
||||||
|
### P0:先修,否则会持续污染生成结果或直接破坏主界面
|
||||||
|
|
||||||
|
1. `src/services/prompt.ts`
|
||||||
|
2. `src/components/NpcModals.tsx`
|
||||||
|
3. `src/components/AdventureEntityModal.tsx`
|
||||||
|
4. `src/data/npcInteractions.ts` 的商店库存种子乱码段
|
||||||
|
5. `src/components/game-shell/GameShellOverlays.tsx` 的标题乱码
|
||||||
|
|
||||||
|
### P1:紧接着修,编辑器体验当前已经明显受损
|
||||||
|
|
||||||
|
1. `src/components/preset-editor/MonsterPresetPanel.tsx`
|
||||||
|
2. `src/components/preset-editor/SceneNpcPresetPanel.tsx`
|
||||||
|
3. `src/components/preset-editor/ScenePresetPanel.tsx`
|
||||||
|
4. `src/components/preset-editor/CharacterPresetPanel.tsx`
|
||||||
|
5. `src/components/StateFunctionEditor.tsx`
|
||||||
|
6. `src/editor/shared/jsonClient.ts`
|
||||||
|
|
||||||
|
### P2:统一术语与风格,减少中英混用
|
||||||
|
|
||||||
|
1. 全局统一 `NPC`、`HP`、`MP`、`AI`、`Shift` 是否保留英文缩写
|
||||||
|
2. 将 `npcInteractions.ts` / `characterPresets.ts` / `stateFunctions.ts` 的英文枚举与键名整理为“内部值 + 中文展示层”
|
||||||
|
3. 补齐 `aiFallbacks.ts`、`characterChatPrompt.ts`、`questPrompt.ts` 的中文 prompt / fallback 版本
|
||||||
|
|
||||||
|
## 八、建议的修复顺序
|
||||||
|
|
||||||
|
1. 先修 `prompt` 和 `npcInteractions`,因为这两处会同时污染 AI 输出和运行时文本。
|
||||||
|
2. 再修 `NpcModals`、`AdventureEntityModal`、`GameShellOverlays`,优先恢复玩家可见界面。
|
||||||
|
3. 再批量处理四个预设编辑器面板和 `StateFunctionEditor`,最后统一共享请求层兜底文案。
|
||||||
|
|
||||||
273
docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md
Normal file
273
docs/audits/text/GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
# 游戏 UI / 预设 / 编辑器文本二次审计(扩展重查版)
|
||||||
|
|
||||||
|
日期:`2026-04-02`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 本文档用于替换同名上一版审计。上一版确实漏掉了不少内容,尤其是:
|
||||||
|
- `AdventurePanel` 里的任务概览与奖励文案
|
||||||
|
- `npcInteractions.ts` 里的 `actionText` / `detailText`
|
||||||
|
- 自定义世界编辑器、NPC 视觉编辑器里的英文兜底和混合术语
|
||||||
|
- 预设编辑器里多组仍未本地化的标题、字段名、保存反馈
|
||||||
|
- 本次以当前仓库实际内容为准,不沿用旧结论;已经修掉的内容不再重复计入。
|
||||||
|
|
||||||
|
## 审计范围
|
||||||
|
|
||||||
|
- 扫描目录:
|
||||||
|
- `src/components/`
|
||||||
|
- `src/data/`
|
||||||
|
- `src/hooks/`
|
||||||
|
- `src/services/`
|
||||||
|
- `src/routing/`
|
||||||
|
- `src/editor/`
|
||||||
|
- 关注对象:
|
||||||
|
- 玩家在主流程、冒险面板、弹窗里会看到的文本
|
||||||
|
- 预设编辑器、自定义世界编辑器、NPC 视觉编辑器中会直接显示的文本
|
||||||
|
- 会透传到 UI、弹窗、编辑器预览中的数据层文本源
|
||||||
|
- 标记类型:
|
||||||
|
- `英文直出`:面向玩家 / 编辑器用户的英文文本仍直接显示
|
||||||
|
- `中英混用`:中文 UI 中混入 `NPC`、`HP`、`MP`、`AI`、`URL`、`Shift` 等术语
|
||||||
|
- `异常显示`:明显乱码、截断、异常问号替代、分隔符损坏
|
||||||
|
|
||||||
|
## 结论摘要
|
||||||
|
|
||||||
|
- 游戏主流程里仍有明显英文残留:`QQ Group`、`WeChat`、`GENARRATIVE`、`Current Area`。
|
||||||
|
- 游戏内任务 / 冒险面板仍有一批英文任务眉标和按钮辅助文案:`BOUNTY TARGET`、`CACHE TRACE`、`SPAR SESSION`、`Inspect reward item ...`、`Unknown monster`。
|
||||||
|
- `GameShellOverlays.tsx` 仍有整组 loading fallback 出现异常编码。
|
||||||
|
- 预设编辑器目前仍是问题最密集区域之一,`CharacterPresetPanel`、`MonsterPresetPanel`、`SceneNpcPresetPanel`、`ScenePresetPanel`、`StateFunctionEditor` 里都有明显英文直出或半成品占位。
|
||||||
|
- 数据层里仍有大量会透出到 UI / 编辑器的英文值,重点在:
|
||||||
|
- `npcInteractions.ts`
|
||||||
|
- `useStoryGeneration.ts`
|
||||||
|
- `storyGenerationState.ts`
|
||||||
|
- `npcEncounterActions.ts`
|
||||||
|
- `sceneObservation.ts`
|
||||||
|
- `characterPresets.ts`
|
||||||
|
- `stateFunctions.ts`
|
||||||
|
|
||||||
|
## 一、游戏主流程 UI
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `48-49` | 英文直出 | `QQ Group`、`WeChat` | 开场页联系方式标签仍是英文 |
|
||||||
|
| `src/components/game-shell/PreGameSelectionFlow.tsx` | `351` | 中英混用 | `contact.label === 'QQ Group' ? 'QQ群' : '微信'` | 逻辑分支仍依赖英文标签 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `123` | 异常显示 | `姝e湪鍔犺浇鍐掗櫓璇︽儏...` | 冒险详情 loading fallback 乱码 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `162` | 异常显示 | `姝e湪鍔犺浇闃熶紞闈㈡澘` | 队伍面板 loading fallback 乱码 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `187` | 异常显示 | `姝e湪鍔犺浇鑳屽寘闈㈡澘` | 背包面板 loading fallback 乱码 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `214` | 异常显示 | `姝e湪鍔犺浇闃熶紞钀ュ湴...` | 营地弹窗 loading fallback 乱码 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `229` | 异常显示 | `姝e湪鍔犺浇鍦板浘...` | 地图 loading fallback 乱码 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `248` | 异常显示 | `姝e湪鍔犺浇瑙掕壊鑱婂ぉ...` | 角色聊天 loading fallback 乱码 |
|
||||||
|
| `src/components/game-shell/GameShellOverlays.tsx` | `261` | 异常显示 | `姝e湪鍔犺浇 NPC 浜や簰...` | NPC 交互 loading fallback 同时混入 `NPC` |
|
||||||
|
| `src/components/game-shell/GameShellRuntime.tsx` | `146` | 英文直出 | `Current Area` | 当前场景名缺失时的兜底文案仍是英文 |
|
||||||
|
| `src/components/game-shell/GameShellRuntime.tsx` | `200` | 英文直出 | `GENARRATIVE` | 顶部 logo 文案仍是英文 |
|
||||||
|
| `src/components/GameShell.tsx` | `311` | 英文直出 | `Current Area` | 旧壳组件里同样保留英文兜底 |
|
||||||
|
| `src/components/GameShell.tsx` | `365` | 英文直出 | `GENARRATIVE` | 旧壳组件里同样保留英文 logo |
|
||||||
|
| `src/components/DeveloperTeamModal.tsx` | `44` | 英文直出 | `aria-label="Close developer team modal"` | 开发团队弹窗关闭按钮辅助文案未本地化 |
|
||||||
|
|
||||||
|
## 二、游戏内面板 / 弹窗
|
||||||
|
|
||||||
|
### 2.1 冒险与任务面板
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `197-198` | 异常显示 | `适合进攻型构`、`适合防御型构` | 描述文本疑似被截断,正常语义应是“构筑” |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `206` | 异常显示 | `item.name + ' 奖励物品<E789A9>?';` | 奖励物品描述兜底尾部异常 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `216` | 异常显示 | ``${hours}小时 ...<2E>?...秒`` | 时长格式字符串出现异常字符 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `219` | 异常显示 | ``${minutes}<7D>?...秒`` | 分钟格式字符串同样异常 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `248` | 英文直出 | ``aria-label={`Inspect reward item ${item.name}`}`` | 奖励物品按钮辅助文案是英文 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `279` | 英文直出 | `BOUNTY TARGET` | 任务眉标未本地化 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `283` | 英文直出 | `Unknown monster` | 目标怪物兜底文案是英文 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `290` | 英文直出 | `CACHE TRACE` | 宝藏任务眉标未本地化 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `295` | 英文直出 | `Inspect the hidden reward site` | 宝藏任务副文案未本地化 |
|
||||||
|
| `src/components/AdventurePanel.tsx` | `302` | 英文直出 | `SPAR SESSION` | 切磋任务眉标未本地化 |
|
||||||
|
|
||||||
|
### 2.2 NPC / 实体 / 交易相关弹窗
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `895` | 中英混用 | `label="HP"` | 实体状态估计面板直接显示 `HP` |
|
||||||
|
| `src/components/AdventureEntityModal.tsx` | `901` | 中英混用 | `label="MP"` | 实体状态估计面板直接显示 `MP` |
|
||||||
|
| `src/components/NpcModals.tsx` | `252` | 中英混用 | `NPC 商品列表` / `你的背包列表` | 交易列表标题混入 `NPC` |
|
||||||
|
| `src/components/NpcModals.tsx` | `273` | 中英混用 | `这个 NPC 当前没有可售商品。` | 空状态文案混入 `NPC` |
|
||||||
|
| `src/components/NpcModals.tsx` | `356` | 中英混用 | `NPC 商品` | 详情弹窗标题混入 `NPC` |
|
||||||
|
| `src/components/NpcModals.tsx` | `408` | 中英混用 | `效果预览:HP +... / MP +... / 冷却 -...` | 数值预览里直接保留 `HP`、`MP` |
|
||||||
|
|
||||||
|
### 2.3 自定义世界结果页
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/CustomWorldResultView.tsx` | `58` | 中英混用 | `新增 NPC` | 结果页新增操作标签混入 `NPC` |
|
||||||
|
|
||||||
|
## 三、自定义世界 / NPC 视觉编辑
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `242` | 英文直出 | `URL` | 图片地址输入提示词未本地化 |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `464` | 英文直出 | `MedievalFantasyCharacters` | 形象编辑副标题里直接暴露素材包英文名 |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `482-483` | 中英混用 | `AI生成NPC形象`、`NPC 形象 AI 生成功能仍在开发中。` | 同时混入 `AI`、`NPC` |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `635-636` | 中英混用 | `新增 NPC`、`编辑 NPC:...` | NPC 档案编辑标题混入英文缩写 |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `663` | 中英混用 | `修改形象` | 本行本身无问题,但上下文仍指向 `NPC` 视觉编辑入口 |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `734` | 中英混用 | `AI生成` | 按钮文案混入 `AI` |
|
||||||
|
| `src/components/CustomWorldEntityEditorModal.tsx` | `762-763` | 中英混用 | `AI生成场景`、`场景图片 AI 生成功能仍在开发中。` | 场景生成弹窗混入 `AI` |
|
||||||
|
| `src/components/CustomWorldNpcVisualEditor.tsx` | `393` | 中英混用 | `AI生成` | 自定义 NPC 视觉编辑器按钮混入 `AI` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `263-267` | 英文直出 | `Failed to load NPC visual overrides`、`Failed to load NPC layout config` | 编辑器首屏可见的加载失败兜底为英文 |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `284-308` | 英文直出 | `response was invalid, using bundled defaults` 等 | 多组降级提示未本地化 |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `463` | 中英混用 | `请先选择一个 NPC 进行编辑` | 空态文案混入 `NPC` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `539` | 英文直出 | `Save failed` | 保存失败提示仍是英文 |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `702-708` | 中英混用 | `NPC 视觉编辑器`、`选择并编辑 NPC 的外观...` | 页面标题与简介多次混入 `NPC` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `718` | 中英混用 | `当前 NPC` | 当前对象字段名混入 `NPC` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `976-977` | 中英混用 | `拖动标记微调 NPC 的预览布局。移动时按住 Shift...` | 帮助提示同时混入 `NPC` 与 `Shift` |
|
||||||
|
| `src/components/NpcVisualEditor.tsx` | `1033-1052` | 英文直出 | `Unknown headgear`、`No headgear`、`Unknown main hand`、`No main hand`、`Unknown off hand`、`No off hand` | 预览状态说明仍是英文 |
|
||||||
|
|
||||||
|
## 四、预设编辑器
|
||||||
|
|
||||||
|
### 4.1 共享配置与选项枚举
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/shared.ts` | `43` | 中英混用 | `label: 'NPC'` | 预设编辑器顶部 tab 仍直接显示 `NPC` |
|
||||||
|
| `src/components/preset-editor/shared.ts` | `63-66` | 英文直出 | `idle`、`move`、`attack`、`die` | 怪物动画可选项是原始英文值 |
|
||||||
|
| `src/components/preset-editor/shared.ts` | `70-74` | 英文直出 | `steady`、`burst`、`mobility`、`finisher`、`projectile` | 技能风格可选项是原始英文值 |
|
||||||
|
|
||||||
|
### 4.2 角色预设面板
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `91-92` | 英文直出 | `Saved character preset overrides...`、`Failed to save character preset overrides.` | 保存反馈未本地化 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `264-279` | 英文直出 | `Characters`、`Browse the character roster...`、`Field`、`Save Character Overrides` | 左侧选择卡与保存栏均为英文 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `315-322` | 英文直出 | `Character Details`、`Field` | 详情卡标题和字段名未本地化 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `404-430` | 英文直出 | `Skill Preview`、`Preview ranged skills...`、`Preview Monster` | 技能预览区英文直出 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `450-467` | 英文直出 | `Skill Setup`、`Add Skill` | 技能配置区仍是英文 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `498-558` | 英文直出 | 大量 `label="Field"` | 多数字段仍显示占位词 `Field` |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `605` | 英文直出 | `Start Frame` | 动画字段未本地化 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `629`、`729` | 英文直出 | `Section`、`Editor section.` | 两个分区仍是半成品英文占位 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `697-698` | 英文直出 | `Attributes`、`Adjust the core character attributes.` | 属性面板未本地化 |
|
||||||
|
| `src/components/preset-editor/CharacterPresetPanel.tsx` | `755` | 英文直出 | `Unset` | 场景绑定下拉空值项为英文 |
|
||||||
|
|
||||||
|
### 4.3 怪物预设面板
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `49-50` | 英文直出 | `Saved monster overrides...`、`Failed to save monster overrides.` | 保存反馈未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `124-139` | 英文直出 | `Section`、`Editor section.`、`Field`、`Save Monster Overrides` | 左侧选择区和保存栏未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `135` | 异常显示 | ``${WORLD_LABELS[monster.worldType]} 闂?${optionMonster.name}`` | 选择列表分隔符损坏 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `158-159` | 英文直出 | `Monster Override Preview`、`Editor section.` | 预览区标题仍是英文 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `192-201` | 英文直出 | `Attack Range`、`Speed`、`HP`、`Max HP` | 预览摘要未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `212-234` | 英文直出 | `Monster ID`、`Name`、`Intro Action` | 核心字段名仍是英文 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `343` | 英文直出 | `FPS` | 动画字段未本地化 |
|
||||||
|
| `src/components/preset-editor/MonsterPresetPanel.tsx` | `207`、`275`、`307` | 英文直出 | `Section`、`Editor section.` | 多个分区标题仍为占位英文 |
|
||||||
|
|
||||||
|
### 4.4 场景 NPC 预设面板
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `127-128` | 英文直出 | `Saved NPC overrides.`、`Failed to save NPC overrides.` | 保存反馈未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `159` | 英文直出 | `No NPC presets are available.` | 空态文案未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `177-193` | 英文直出 | `NPC Library`、`Browse and select an NPC preset.`、`NPC ID`、`Save NPC Overrides` | 左侧选择区英文直出 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `221-237` | 英文直出 | `Skill Preview`、`Preview ranged skills from the linked character.`、`Skill`、`World` | 技能预览区未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `258` | 异常显示 | `闂?NPC` | 空态或提示文案已损坏 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `264-268` | 英文直出 | `Visual Preview`、`Hostile NPCs use monster presets...` | 视觉预览区标题与说明均未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `310-371` | 英文直出 | `NPC Details`、`Name`、`Role`、`Avatar`、`Linked Character ID`、`Monster Preset ID`、`Initial Affinity`、`Description`、`Visual Editor` | 详情区字段名大面积英文直出 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `374-375` | 英文直出 | `Hostile NPCs cannot use the visual editor...`、`Narrative NPC visual overrides can be previewed here.` | 视觉编辑区说明未本地化 |
|
||||||
|
| `src/components/preset-editor/SceneNpcPresetPanel.tsx` | `382-384` | 异常显示 | 三整段乱码说明 + `NPC` | 视觉编辑区空态说明已严重损坏 |
|
||||||
|
|
||||||
|
### 4.5 场景预设面板
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `52-53` | 英文直出 | `Saved scene overrides.`、`Failed to save scene overrides.` | 保存反馈未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `62` | 英文直出 | `No scene presets are available.` | 空态文案未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `105-107` | 英文直出 | `Treasure Ahead`、`Treasure` | 宝藏预览实体名仍是英文 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `132-171` | 英文直出 | `Scene Library`、`Browse and select a scene preset.`、`Save`、`Scene Preview`、`Preview monsters, NPCs, and treasure...` | 场景面板主框架仍多处英文 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `175-181` | 英文直出 | `Preview Mode`、`Monster Preview`、`NPC Preview`、`Treasure Preview` | 预览模式切换未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `207-226` | 英文直出 | `Hostile NPCs`、`NPCs`、`None` | 预览摘要区未本地化 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `233-290` | 英文直出 | `Scene Details`、`Edit the selected scene preset.`、`Scene ID`、`World`、`Name`、`Description`、`Image Source`、`Forward Scene`、`Connected Scene IDs`、`Monster IDs`、`Treasure Hints` | 详情编辑区字段名几乎全部英文 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `271` | 英文直出 | `Unset` | 下拉空值项为英文 |
|
||||||
|
| `src/components/preset-editor/ScenePresetPanel.tsx` | `299` | 英文直出 | `NPCs In Scene` | 分区标题未本地化 |
|
||||||
|
|
||||||
|
### 4.6 状态函数编辑器
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `310-325` | 英文直出 | `Preview`、`Treasure` | 预览 NPC / 宝藏 context 里保留英文 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1062-1064` | 英文直出 | `Option behavior overrides saved.`、`Failed to save option behavior overrides` | 保存反馈未本地化 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1128` | 英文直出 | `无模板` 之外的模板选择逻辑仍有英文结构 | 该区主体中文,但下游模板 ID / 类型依旧偏英文 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1185` | 英文直出 | `AnimationState` 原始值直接作为选项标签 | 动画值会直接显示英文枚举 |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1190` | 英文直出 | `placeholder="可使用 {monster} 占位符"` | 占位符本身暴露英文 key |
|
||||||
|
| `src/components/StateFunctionEditor.tsx` | `1191` | 英文直出 | `idle`、`move`、`attack` | 怪物动画下拉仍使用英文值 |
|
||||||
|
|
||||||
|
## 五、数据层 / 运行时文本源
|
||||||
|
|
||||||
|
### 5.1 直接驱动交互按钮、详情文案、结果文本的源头
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/data/npcInteractions.ts` | `444-580` | 重点补录 | 大量 `actionText` / `detailText` | 这是本轮补录重点,属于 NPC 交互选项直接展示源,需要逐条审校 |
|
||||||
|
| `src/data/npcInteractions.ts` | `327-349` | 英文直出 | `deep`、`honest`、`partial`、`guarded`、`warm`、`cooperative`、`neutral`、`distant`、`candid`、`true_but_incomplete`、`half_truth`、`situational_only` | 这些关系 / 说话风格值虽然是逻辑枚举,但后续很容易在编辑器、调试面板、覆盖配置中直接暴露 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `576-578` | 英文直出 | `Travel to ...`、`Leave camp and head toward ...` | 旅行选项和详情文案未本地化 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `654-659` | 英文直出 | `Speak with ...`、`Focus on the person in front of you first...` | 开场对话选项未本地化 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `827`、`1297` | 英文直出 | `Exchange an opening judgment with ... at camp` | 营地开场交互 actionText 未本地化 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `1097` | 英文直出 | `Begin the adventure` | 初始 actionText 未本地化 |
|
||||||
|
| `src/hooks/useStoryGeneration.ts` | `1374`、`1794` | 英文直出 | `Unknown AI error` | AI 兜底错误提示仍是英文 |
|
||||||
|
| `src/hooks/story/storyGenerationState.ts` | `137-141` | 英文直出 | `You leave ...`、`Travel to ...` | 旅行结果与 actionText 未本地化 |
|
||||||
|
| `src/hooks/story/npcEncounterActions.ts` | `277` | 英文直出 | `Victory reward: ${lootText}.` | 战斗胜利奖励结算文本未本地化 |
|
||||||
|
| `src/hooks/story/npcEncounterActions.ts` | `389` | 英文直出 | `NPC dialogue AI is unavailable.` | NPC 对话 AI 失败兜底是英文 |
|
||||||
|
| `src/hooks/story/characterChat.ts` | `68` | 英文直出 | `Player` | 聊天摘要拼接里保留英文说话人名 |
|
||||||
|
| `src/hooks/story/characterChat.ts` | `84` | 英文直出 | `Tell me more clearly what you mean.` | 建议起句未本地化 |
|
||||||
|
| `src/hooks/story/characterChat.ts` | `283` | 英文直出 | `Unknown AI error` | 私聊错误兜底未本地化 |
|
||||||
|
| `src/data/sceneObservation.ts` | `9-34` | 英文直出 | `You pause to listen...`、`Possible NPCs...`、`Possible hostile NPCs...`、`Possible treasure clues...`、`Boss clue...` | 观察环境的整组文本源仍是英文 |
|
||||||
|
|
||||||
|
### 5.2 会透出到编辑器 / 预览 / 覆盖系统的预设值
|
||||||
|
|
||||||
|
| 文件 | 行号 | 类型 | 当前文本 / 字段 | 说明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `src/data/scenePresets.ts` | `141` | 中英混用 | `role: '敌对NPC'` | 场景 NPC 角色名混入 `NPC` |
|
||||||
|
| `src/data/scenePresets.ts` | `194`、`294` | 英文直出 | `['trade', 'fight', 'spar', 'help', 'chat', 'recruit', 'gift']` | 场景 NPC 功能数组是英文原始值,编辑器容易直出 |
|
||||||
|
| `src/data/stateFunctions.ts` | `33` | 英文直出 | `monsterAnimation?: 'idle' | 'move' | 'attack'` | 动画值原始英文会进入状态函数编辑器 |
|
||||||
|
| `src/data/stateFunctions.ts` | `130`、`152`、`174`、`196`、`221`、`244` | 英文直出 | `steady`、`burst`、`mobility`、`finisher`、`projectile` | 技能权重 key 为英文原始值 |
|
||||||
|
| `src/data/stateFunctions.ts` | `372`、`445`、`569`、`575-576` | 中英混用 | 描述里多次直接写 `NPC` | 虽然主体中文,但会透出到说明文字 |
|
||||||
|
| `src/data/characterPresets.ts` | `54-70` | 英文直出 | `blunt`、`wary`、`evasive`、`measured`、`gentle`、`teasing`、`dry`、`steady`、`direct`、`fragmented`、`deflecting` | 对话风格推断返回值全为英文 |
|
||||||
|
| `src/data/characterPresets.ts` | `362-382`、`519-539`、`738-758`、`833-853`、`1018-1041` | 英文直出 | `assetFolder`、`folder`、`prefix` 中大量英文,如 `Sword Princess`、`idle`、`Double Jump`、`Wall Slide`、`Attack` | 这些值会被预设编辑器动画区直接展示或编辑 |
|
||||||
|
| `src/data/characterPresets.ts` | `387-389`、`544-546`、`763-765`、`858-860`、`1046-1048` | 英文直出 | `guardStyle`、`warmStyle`、`truthStyle` 使用英文值 | 会透出到预设编辑器 / 调试视图 |
|
||||||
|
| `src/data/characterPresets.ts` | `410-501`、`575-720`、`794-1000`、`1069-1197` | 英文直出 | `style`、`delivery`、`phase`、`anchor`、`motion` 使用 `steady`、`mobility`、`ranged`、`travel`、`target`、`projectile` 等英文值 | 角色技能配置层大面积保留英文元数据 |
|
||||||
|
| `src/data/monsterPresets.ts` | `384-817` | 英文直出 | 多处 `common`、`uncommon`、`rare`、`epic` | 怪物稀有度原始值为英文,若编辑器未做映射会直接暴露 |
|
||||||
|
| `src/editor/shared/jsonClient.ts` | `29-43` | 英文直出 | `Request failed`、`Save failed` | 编辑器通用 JSON 客户端默认错误文案未本地化 |
|
||||||
|
|
||||||
|
## 六、本轮已复查、暂未记录明确问题的文件
|
||||||
|
|
||||||
|
以下文件本轮重新检查过,但当前内容里没有继续记录“英文直出 / 异常显示”的明确条目,暂可视为本轮通过:
|
||||||
|
|
||||||
|
- `src/routing/appRoutes.tsx`
|
||||||
|
- `src/components/game-shell/CharacterSelectionFlow.tsx`
|
||||||
|
- `src/components/game-shell/GameShellStoryPanels.tsx`
|
||||||
|
- `src/components/CharacterPanel.tsx`
|
||||||
|
- `src/components/InventoryPanel.tsx`
|
||||||
|
- `src/components/MapModal.tsx`
|
||||||
|
- `src/components/CharacterChatModal.tsx`
|
||||||
|
- `src/components/SelectionCustomizationModals.tsx`
|
||||||
|
- `src/components/adventure-panel/AdventurePanelOverlays.tsx`
|
||||||
|
- `src/data/sceneEncounterPreviews.ts`
|
||||||
|
- `src/data/customWorldVisuals.ts`
|
||||||
|
- `src/services/customWorldBuilder.ts`
|
||||||
|
|
||||||
|
## 建议修复顺序
|
||||||
|
|
||||||
|
1. 先修主流程直接暴露给玩家的文本:
|
||||||
|
- `PreGameSelectionFlow.tsx`
|
||||||
|
- `GameShellOverlays.tsx`
|
||||||
|
- `GameShellRuntime.tsx`
|
||||||
|
- `GameShell.tsx`
|
||||||
|
- `AdventurePanel.tsx`
|
||||||
|
2. 再修编辑器里最容易误导内容生产的面板:
|
||||||
|
- `CharacterPresetPanel.tsx`
|
||||||
|
- `MonsterPresetPanel.tsx`
|
||||||
|
- `SceneNpcPresetPanel.tsx`
|
||||||
|
- `ScenePresetPanel.tsx`
|
||||||
|
- `StateFunctionEditor.tsx`
|
||||||
|
3. 最后统一清理数据层文本源和枚举映射:
|
||||||
|
- `npcInteractions.ts`
|
||||||
|
- `useStoryGeneration.ts`
|
||||||
|
- `storyGenerationState.ts`
|
||||||
|
- `npcEncounterActions.ts`
|
||||||
|
- `sceneObservation.ts`
|
||||||
|
- `characterPresets.ts`
|
||||||
|
- `stateFunctions.ts`
|
||||||
18
docs/audits/text/README.md
Normal file
18
docs/audits/text/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 文本与乱码审计总览
|
||||||
|
|
||||||
|
这一组只保留当前仍需要执行的文本与乱码审计入口。早期逐日扫描已经聚合到当前结论,不再保留旧稿链路。
|
||||||
|
|
||||||
|
## 当前推荐入口
|
||||||
|
|
||||||
|
1. [CHINESE_MOJIBAKE_INVENTORY.md](./CHINESE_MOJIBAKE_INVENTORY.md)
|
||||||
|
偏全局库存清单,适合先确认问题分布范围。
|
||||||
|
2. [GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md](./GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md)
|
||||||
|
这是当前最完整的深度审计版本,已经扩到 `prompt`、`npcInteractions`、运行时弹窗与编辑器深层文本。
|
||||||
|
3. [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md)
|
||||||
|
适合看“扩展重查版”的 UI / 预设 / 编辑器问题面。
|
||||||
|
|
||||||
|
## 融合结论
|
||||||
|
|
||||||
|
- 早期几轮扫描已经完成使命,核心结论是:不要把乱码当成普通文案改写,先确认真实编码;文本修复要优先处理会进入玩家体验和 AI 生成链路的内容。
|
||||||
|
- `2026-04-02` 两份文档开始把重点收敛到真正会影响玩家体验和 AI 生成质量的链路。
|
||||||
|
- 现在做文本修复时,不必从最早一份开始逐篇读;优先看 `CHINESE_MOJIBAKE_INVENTORY` 和 `2026-04-02` 两份即可。
|
||||||
650
docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md
Normal file
650
docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
# AI 原生运行时物品生成系统重设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-02`
|
||||||
|
|
||||||
|
## 0. 这次重设计要解决什么
|
||||||
|
|
||||||
|
基于当前仓库已经存在的系统,这次不再把“AI 原生物品生成”设计成一套独立玩法,而是把它重做成:
|
||||||
|
|
||||||
|
**挂在现有背包、build、宝藏、NPC、任务、自定义世界之上的统一运行时物品导演系统。**
|
||||||
|
|
||||||
|
它要解决的核心问题有 4 个:
|
||||||
|
|
||||||
|
1. 当前物品入口很多,但缺少统一导演层。
|
||||||
|
2. 当前奖励能发物品,但和场景背景、相关 NPC、最近事件的贴合度不够高。
|
||||||
|
3. 当前 build 标签体系已经存在,但运行时奖励还没有围绕“永久标签 / 限时标签 / 小数值补充”形成稳定规则。
|
||||||
|
4. AI 目前只负责叙事包装,没有真正进入运行时物品生成链路。
|
||||||
|
|
||||||
|
这次重设计的目标不是“让 AI 直接生成 `InventoryItem`”,而是:
|
||||||
|
|
||||||
|
- 让 AI 负责物品的**叙事意图、关系语义、世界贴合**
|
||||||
|
- 让本地规则负责物品的**标签、数值、稀有度、存档、平衡**
|
||||||
|
|
||||||
|
## 1. 设计结论先说
|
||||||
|
|
||||||
|
新的 AI 原生物品系统建议采用:
|
||||||
|
|
||||||
|
**上下文采样器 -> AI 物品意图层 -> 本地编译器 -> 渠道分发器 -> 叙事回写器**
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `InventoryItem` 继续作为唯一成品结构
|
||||||
|
- `ItemBuildProfile` 继续承载永久 build 标签
|
||||||
|
- `ItemUseProfile.buildBuffs` 继续承载限时 build 标签
|
||||||
|
- `ItemStatProfile` 继续承载小数值加成
|
||||||
|
- `buildTags.ts / buildDamage.ts` 继续承接战斗结算
|
||||||
|
- `treasureInteractions.ts / npcInteractions.ts / forgeSystem.ts / customWorldRuntime.ts` 改为调用统一导演层
|
||||||
|
|
||||||
|
一句话:
|
||||||
|
|
||||||
|
**不要再按“宝藏怎么发、NPC 怎么卖、任务怎么奖”各写一套;而是让所有入口共用同一个运行时物品生成管线。**
|
||||||
|
|
||||||
|
## 2. 这套系统必须遵守的边界
|
||||||
|
|
||||||
|
## 2.1 AI 负责语义,不负责数值
|
||||||
|
|
||||||
|
AI 可以决定:
|
||||||
|
|
||||||
|
- 这件物品像什么
|
||||||
|
- 它为什么在这里出现
|
||||||
|
- 它和哪个 NPC / 势力 / 地标 / 怪物有关
|
||||||
|
- 它更偏向什么 build 风格
|
||||||
|
- 它应该是永久物还是限时物
|
||||||
|
|
||||||
|
AI 不应该直接决定:
|
||||||
|
|
||||||
|
- 最终 `outgoingDamageBonus` 是多少
|
||||||
|
- 能带几个标签
|
||||||
|
- 允许不允许进入装备槽
|
||||||
|
- 价值多少
|
||||||
|
- 能不能掉落
|
||||||
|
- 能不能持久化
|
||||||
|
|
||||||
|
## 2.2 所有成品都必须编译回当前系统
|
||||||
|
|
||||||
|
新的系统不是新物品结构,而是新生成过程。
|
||||||
|
|
||||||
|
最终成品仍然必须落到:
|
||||||
|
|
||||||
|
- `InventoryItem`
|
||||||
|
- `ItemStatProfile`
|
||||||
|
- `ItemUseProfile`
|
||||||
|
- `ItemBuildProfile`
|
||||||
|
|
||||||
|
否则现有:
|
||||||
|
|
||||||
|
- 背包
|
||||||
|
- 装备
|
||||||
|
- build 计算
|
||||||
|
- 锻造
|
||||||
|
- 存档
|
||||||
|
- 交易
|
||||||
|
|
||||||
|
都接不上。
|
||||||
|
|
||||||
|
## 2.3 运行时物品的主收益必须是“构筑意义”
|
||||||
|
|
||||||
|
重新设计后,运行时物品的收益优先级建议固定为:
|
||||||
|
|
||||||
|
1. `build 标签`
|
||||||
|
2. `功能性节奏收益`
|
||||||
|
3. `小数值补充`
|
||||||
|
|
||||||
|
不建议反过来做成:
|
||||||
|
|
||||||
|
1. 大数值
|
||||||
|
2. build 标签只是点缀
|
||||||
|
|
||||||
|
因为当前项目最强的系统基础已经是 build 标签和叙事关系,而不是传统数值刷装。
|
||||||
|
|
||||||
|
## 3. 新系统的整体架构
|
||||||
|
|
||||||
|
## 3.1 模块拆分
|
||||||
|
|
||||||
|
建议新增 5 个模块:
|
||||||
|
|
||||||
|
- `src/types/runtimeItem.ts`
|
||||||
|
- `src/data/runtimeItemContext.ts`
|
||||||
|
- `src/data/runtimeItemDirector.ts`
|
||||||
|
- `src/data/runtimeItemCompiler.ts`
|
||||||
|
- `src/data/runtimeItemNarrative.ts`
|
||||||
|
|
||||||
|
职责如下。
|
||||||
|
|
||||||
|
### A. `runtimeItemContext.ts`
|
||||||
|
|
||||||
|
负责统一采样生成上下文,不直接生成物品。
|
||||||
|
|
||||||
|
输入来自:
|
||||||
|
|
||||||
|
- `GameState`
|
||||||
|
- `currentEncounter`
|
||||||
|
- `currentScenePreset`
|
||||||
|
- `npcStates`
|
||||||
|
- `storyHistory`
|
||||||
|
- `playerEquipment`
|
||||||
|
- `activeBuildBuffs`
|
||||||
|
|
||||||
|
输出统一上下文对象:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RuntimeItemGenerationContext = {
|
||||||
|
worldType: WorldType | null;
|
||||||
|
customWorldProfile: CustomWorldProfile | null;
|
||||||
|
sceneId: string | null;
|
||||||
|
sceneName: string | null;
|
||||||
|
sceneDescription: string | null;
|
||||||
|
sceneTags: string[];
|
||||||
|
treasureHints: string[];
|
||||||
|
encounter: Encounter | null;
|
||||||
|
encounterNpcId: string | null;
|
||||||
|
encounterNpcName: string | null;
|
||||||
|
encounterContextText: string | null;
|
||||||
|
relatedNpcState: NpcPersistentState | null;
|
||||||
|
recentStorySummary: string;
|
||||||
|
recentActions: string[];
|
||||||
|
playerCharacterId: string;
|
||||||
|
playerBuildTags: string[];
|
||||||
|
playerBuildGaps: string[];
|
||||||
|
playerEquipmentTags: string[];
|
||||||
|
generationChannel: RuntimeItemGenerationChannel;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. `runtimeItemDirector.ts`
|
||||||
|
|
||||||
|
负责根据上下文决定:
|
||||||
|
|
||||||
|
- 这次该不该生成上下文化物品
|
||||||
|
- 生成几件
|
||||||
|
- 主奖励 / 副奖励是什么
|
||||||
|
- 是装备、消耗品、材料还是稀有物
|
||||||
|
- 偏永久 build、限时 build,还是纯功能补给
|
||||||
|
|
||||||
|
它的输出不是成品,而是“导演结果”:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RuntimeItemPlan = {
|
||||||
|
slot: "primary" | "secondary" | "support";
|
||||||
|
itemKind: "equipment" | "consumable" | "material" | "relic" | "quest";
|
||||||
|
permanence: "permanent" | "timed" | "resource";
|
||||||
|
narrativeWeight: "light" | "medium" | "heavy";
|
||||||
|
targetBuildDirection: string[];
|
||||||
|
relationAnchor: RuntimeRelationAnchor;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. `runtimeItemCompiler.ts`
|
||||||
|
|
||||||
|
负责把导演计划 + AI 意图编译成正式 `InventoryItem`。
|
||||||
|
|
||||||
|
编译职责包括:
|
||||||
|
|
||||||
|
- build 标签规范化
|
||||||
|
- 稀有度预算分配
|
||||||
|
- 永久标签上限
|
||||||
|
- 限时标签时长
|
||||||
|
- 数值预算
|
||||||
|
- 装备槽判定
|
||||||
|
- 价值计算
|
||||||
|
- metadata 生成
|
||||||
|
|
||||||
|
### D. `runtimeItemNarrative.ts`
|
||||||
|
|
||||||
|
负责把已经生成好的物品回写成叙事文本:
|
||||||
|
|
||||||
|
- 物品命名
|
||||||
|
- 物品描述
|
||||||
|
- 物品来源说明
|
||||||
|
- 宝藏/NPC/任务文案嵌入
|
||||||
|
|
||||||
|
### E. `runtimeItem.ts`
|
||||||
|
|
||||||
|
负责声明:
|
||||||
|
|
||||||
|
- 渠道类型
|
||||||
|
- 关系锚点类型
|
||||||
|
- 运行时物品 metadata
|
||||||
|
- AI 意图结构
|
||||||
|
- 编译预算结构
|
||||||
|
|
||||||
|
## 3.2 AI 在新架构里的输入输出
|
||||||
|
|
||||||
|
建议 AI 只接触两种结构:
|
||||||
|
|
||||||
|
### 输入:压缩过的生成上下文
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RuntimeItemAiPromptInput = {
|
||||||
|
worldSummary: string;
|
||||||
|
sceneSummary: string;
|
||||||
|
encounterSummary: string;
|
||||||
|
relatedNpcSummary: string;
|
||||||
|
recentStorySummary: string;
|
||||||
|
generationChannel: RuntimeItemGenerationChannel;
|
||||||
|
playerBuildDirection: string[];
|
||||||
|
playerBuildGaps: string[];
|
||||||
|
desiredItemKind: string;
|
||||||
|
permanence: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 输出:轻量意图
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RuntimeItemAiIntent = {
|
||||||
|
shortNameSeed: string;
|
||||||
|
sourcePhrase: string;
|
||||||
|
reasonToAppear: string;
|
||||||
|
relationHooks: string[];
|
||||||
|
desiredBuildTags: string[];
|
||||||
|
desiredFunctionalBias: Array<"heal" | "mana" | "cooldown" | "guard" | "damage">;
|
||||||
|
tone: "grim" | "mysterious" | "martial" | "ritual" | "survival";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
AI 输出长度必须短,目的明确,不允许直接产出成品 JSON 大对象。
|
||||||
|
|
||||||
|
## 4. 新系统里的物品收益模型
|
||||||
|
|
||||||
|
## 4.1 三种收益层
|
||||||
|
|
||||||
|
新系统建议把运行时物品分成三类收益层。
|
||||||
|
|
||||||
|
### 第一层:永久 build 标签
|
||||||
|
|
||||||
|
适用于:
|
||||||
|
|
||||||
|
- 武器
|
||||||
|
- 护甲
|
||||||
|
- 饰品
|
||||||
|
- 稀有遗物
|
||||||
|
|
||||||
|
载体:
|
||||||
|
|
||||||
|
- `buildProfile.role`
|
||||||
|
- `buildProfile.tags`
|
||||||
|
- `buildProfile.setId / setName`
|
||||||
|
|
||||||
|
建议限制:
|
||||||
|
|
||||||
|
- 普通运行时装备:`1` 个主标签
|
||||||
|
- 稀有以上:`1` 主标签 + `1` 协同标签
|
||||||
|
- 传说或剧情物:允许带 `2` 标签 + 关系锚点
|
||||||
|
|
||||||
|
### 第二层:限时 build 标签
|
||||||
|
|
||||||
|
适用于:
|
||||||
|
|
||||||
|
- 药剂
|
||||||
|
- 符箓
|
||||||
|
- 战场工具
|
||||||
|
- 一次性应急物
|
||||||
|
|
||||||
|
载体:
|
||||||
|
|
||||||
|
- `useProfile.buildBuffs`
|
||||||
|
|
||||||
|
建议限制:
|
||||||
|
|
||||||
|
- `1~2` 个标签
|
||||||
|
- `1~3` 回合
|
||||||
|
- 默认刷新持续时间,不建议无限叠层
|
||||||
|
|
||||||
|
### 第三层:少量直接数值
|
||||||
|
|
||||||
|
适用于:
|
||||||
|
|
||||||
|
- 补足 build 短板
|
||||||
|
- 强化渠道差异
|
||||||
|
- 给物品明确手感
|
||||||
|
|
||||||
|
载体:
|
||||||
|
|
||||||
|
- `statProfile`
|
||||||
|
- `useProfile`
|
||||||
|
|
||||||
|
建议数值定位:
|
||||||
|
|
||||||
|
- 永远是“辅助收益”
|
||||||
|
- 不和 build 标签争主导权
|
||||||
|
|
||||||
|
## 4.2 玩家 build 缺口驱动
|
||||||
|
|
||||||
|
建议新系统在生成物品时先判断当前玩家缺口,而不是先随机抽品类。
|
||||||
|
|
||||||
|
建议至少识别这些缺口:
|
||||||
|
|
||||||
|
- `survival_gap`
|
||||||
|
- 当前缺 `守御 / 护体 / 回复 / 续战`
|
||||||
|
- `mana_gap`
|
||||||
|
- 当前缺 `法力 / 冷却 / 节奏恢复`
|
||||||
|
- `finisher_gap`
|
||||||
|
- 当前缺 `爆发 / 重击 / 追击`
|
||||||
|
- `mobility_gap`
|
||||||
|
- 当前缺 `突进 / 快袭 / 风行 / 游击`
|
||||||
|
- `control_gap`
|
||||||
|
- 当前缺 `控场 / 符阵 / 镇邪`
|
||||||
|
|
||||||
|
运行时物品应优先:
|
||||||
|
|
||||||
|
- 补当前明显缺口
|
||||||
|
- 或强化当前已经成型的主方向
|
||||||
|
|
||||||
|
## 5. 如何让物品和背景 / NPC / 场景高度贴合
|
||||||
|
|
||||||
|
## 5.1 必须引入“关系锚点”
|
||||||
|
|
||||||
|
建议每个 AI 原生运行时物品都必须绑定至少一个 `relationAnchor`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RuntimeRelationAnchor =
|
||||||
|
| { type: "npc"; npcId?: string; npcName: string; roleText?: string }
|
||||||
|
| { type: "scene"; sceneId?: string; sceneName: string }
|
||||||
|
| { type: "landmark"; landmarkName: string }
|
||||||
|
| { type: "monster"; monsterId?: string; monsterName: string }
|
||||||
|
| { type: "faction"; factionName: string }
|
||||||
|
| { type: "quest"; questId?: string; questName: string };
|
||||||
|
```
|
||||||
|
|
||||||
|
如果没有锚点,物品最多只能作为普通补给,不进入“AI 原生重点物品”。
|
||||||
|
|
||||||
|
## 5.2 不同渠道对应不同贴合逻辑
|
||||||
|
|
||||||
|
### 宝藏
|
||||||
|
|
||||||
|
物品必须同时贴合:
|
||||||
|
|
||||||
|
- 当前场景
|
||||||
|
- `treasureHints`
|
||||||
|
- 最近故事动作
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- 在矿道拿到的不是泛用剑,而是“矿脉巡火短铳”
|
||||||
|
- 在旧祭坛拿到的不是泛用药,而是“断誓回神香”
|
||||||
|
|
||||||
|
### NPC 交易
|
||||||
|
|
||||||
|
物品必须贴合:
|
||||||
|
|
||||||
|
- NPC 身份
|
||||||
|
- NPC 库存风格
|
||||||
|
- NPC 和玩家关系
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- 黑市牙人卖的是“快袭/情报/潜行”方向的东西
|
||||||
|
- 医修给的是“回复/续战/净化”方向的东西
|
||||||
|
|
||||||
|
### 怪物掉落
|
||||||
|
|
||||||
|
物品必须贴合:
|
||||||
|
|
||||||
|
- 怪物战斗风格
|
||||||
|
- 怪物生态来源
|
||||||
|
- 所在场景
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- 重甲守卫掉 `守御精粹`
|
||||||
|
- 雾林伏击者掉 `风行羽囊`
|
||||||
|
|
||||||
|
### 任务奖励
|
||||||
|
|
||||||
|
物品必须贴合:
|
||||||
|
|
||||||
|
- 发布人
|
||||||
|
- 任务目标
|
||||||
|
- 完成方式
|
||||||
|
- 当前线索推进
|
||||||
|
|
||||||
|
它最适合发“永久关系物”。
|
||||||
|
|
||||||
|
## 5.3 命名改成“锚点命名法”
|
||||||
|
|
||||||
|
建议运行时重点物品命名遵循:
|
||||||
|
|
||||||
|
```text
|
||||||
|
来源词 + 关系词 + 功能词
|
||||||
|
```
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
稀有前缀 + 通用品类名
|
||||||
|
```
|
||||||
|
|
||||||
|
例子:
|
||||||
|
|
||||||
|
- `锁风渡缉索短刃`
|
||||||
|
- `裂界巡守压纹符`
|
||||||
|
- `药谷回岚灵露`
|
||||||
|
- `断碑旧誓护心佩`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- 来源词来自场景/地标/势力
|
||||||
|
- 关系词来自 NPC / 怪物 / 事件
|
||||||
|
- 功能词来自 build 或物品品类
|
||||||
|
|
||||||
|
## 6. 新系统里的预算规则
|
||||||
|
|
||||||
|
## 6.1 稀有度预算
|
||||||
|
|
||||||
|
建议按稀有度控制:
|
||||||
|
|
||||||
|
- 能有几个 build 标签
|
||||||
|
- 能不能带关系锚点
|
||||||
|
- 能不能有 set 倾向
|
||||||
|
- 数值范围上限
|
||||||
|
|
||||||
|
建议预算如下:
|
||||||
|
|
||||||
|
| 稀有度 | build 标签 | 限时 buff | 数值强度 | 叙事强度 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| common | 0~1 | 1 | 很小 | 轻 |
|
||||||
|
| uncommon | 1 | 1~2 | 小 | 轻 |
|
||||||
|
| rare | 1~2 | 2 | 小到中 | 中 |
|
||||||
|
| epic | 2 | 2 | 中 | 中到高 |
|
||||||
|
| legendary | 2 + 关系锚点 | 2~3 | 中 | 高 |
|
||||||
|
|
||||||
|
## 6.2 渠道预算
|
||||||
|
|
||||||
|
不同渠道不该发同样强度的物品。
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- `treasure`
|
||||||
|
- 偏单件强语义物
|
||||||
|
- `npc_trade`
|
||||||
|
- 偏稳定、可预期、可补短板
|
||||||
|
- `npc_reward`
|
||||||
|
- 偏关系定制与限时支援物
|
||||||
|
- `monster_drop`
|
||||||
|
- 偏材料 / 精粹 / 生态锚点物
|
||||||
|
- `quest_reward`
|
||||||
|
- 偏永久 build 锚点物
|
||||||
|
- `discovery`
|
||||||
|
- 偏线索物 / 过渡工具物
|
||||||
|
|
||||||
|
## 6.3 build 方向切换限制
|
||||||
|
|
||||||
|
新系统建议引入一条硬规则:
|
||||||
|
|
||||||
|
**普通运行时物品默认只能强化当前 build 或其邻近 build,不应该高频强制转流派。**
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
- 当前偏 `快剑/突进`,更容易给 `追击/风行`
|
||||||
|
- 当前偏 `守御/护体`,更容易给 `续战/回复`
|
||||||
|
- 当前偏 `法修/雷法`,更容易给 `过载/冷却`
|
||||||
|
|
||||||
|
真正能强制开新流派的物品,应只出现在:
|
||||||
|
|
||||||
|
- 高价值任务奖励
|
||||||
|
- 关键宝藏
|
||||||
|
- 重要 NPC 关系突破
|
||||||
|
|
||||||
|
## 7. 建议新增的数据结构
|
||||||
|
|
||||||
|
## 7.1 运行时 metadata
|
||||||
|
|
||||||
|
建议在 `InventoryItem` 上新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface RuntimeItemMetadata {
|
||||||
|
origin: "catalog" | "procedural" | "ai_compiled";
|
||||||
|
generationChannel: RuntimeItemGenerationChannel;
|
||||||
|
seedKey: string;
|
||||||
|
relationAnchor?: RuntimeRelationAnchor;
|
||||||
|
sourceReason: string;
|
||||||
|
recentEventHook?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 `InventoryItem` 上挂:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface InventoryItem {
|
||||||
|
runtimeMetadata?: RuntimeItemMetadata;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样后面:
|
||||||
|
|
||||||
|
- UI 可展示“来源”
|
||||||
|
- 日志可回放“为什么拿到”
|
||||||
|
- 剧情可引用“这是谁给你的”
|
||||||
|
|
||||||
|
## 7.2 运行时导演结果
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type DirectedRuntimeReward = {
|
||||||
|
primaryItem?: InventoryItem | null;
|
||||||
|
supportItems: InventoryItem[];
|
||||||
|
hp?: number;
|
||||||
|
mana?: number;
|
||||||
|
currency?: number;
|
||||||
|
storyHint?: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以统一替代宝藏、帮助奖励、任务奖励等零散结构。
|
||||||
|
|
||||||
|
## 8. 与现有模块的接入方式
|
||||||
|
|
||||||
|
## 8.1 `treasureInteractions.ts`
|
||||||
|
|
||||||
|
重构方式:
|
||||||
|
|
||||||
|
- 现在:内部直接从世界池挑物品
|
||||||
|
- 以后:调用 `runtimeItemDirector`
|
||||||
|
|
||||||
|
建议流程:
|
||||||
|
|
||||||
|
1. 从 `GameState + Encounter + ScenePreset` 组 context
|
||||||
|
2. 指定 `generationChannel = "treasure"`
|
||||||
|
3. director 产出 `DirectedRuntimeReward`
|
||||||
|
4. 再由 `buildTreasureResultText` 读 reward 回写文本
|
||||||
|
|
||||||
|
这是最适合作为第一落点的入口。
|
||||||
|
|
||||||
|
## 8.2 `npcInteractions.ts`
|
||||||
|
|
||||||
|
接入方向:
|
||||||
|
|
||||||
|
- 商店货物补货
|
||||||
|
- NPC 帮助奖励
|
||||||
|
- 高好感赠与
|
||||||
|
- 特殊 NPC 线索物
|
||||||
|
|
||||||
|
这里最适合体现“关系锚点”。
|
||||||
|
|
||||||
|
## 8.3 `forgeSystem.ts`
|
||||||
|
|
||||||
|
锻造系统不需要完全 AI 化,但应该读取 AI 原生物品遗留的 metadata:
|
||||||
|
|
||||||
|
- 拆解时保留关系信息
|
||||||
|
- 产出对应方向的精粹
|
||||||
|
- 重铸时优先在邻近 build 内滚动
|
||||||
|
|
||||||
|
这样 AI 原生物品与锻造闭环才是连通的。
|
||||||
|
|
||||||
|
## 8.4 `customWorldRuntime.ts`
|
||||||
|
|
||||||
|
这个模块不要废弃,而是改成:
|
||||||
|
|
||||||
|
- 继续负责“主题物品池”
|
||||||
|
- director 在需要 fallback 或大批量补货时调用它
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- `customWorldRuntime.ts` 保留“池”
|
||||||
|
- `runtimeItemDirector.ts` 新增“导演”
|
||||||
|
|
||||||
|
## 9. 推荐实施顺序
|
||||||
|
|
||||||
|
## 阶段 A:先加类型和 metadata
|
||||||
|
|
||||||
|
先做:
|
||||||
|
|
||||||
|
- `runtimeItem.ts`
|
||||||
|
- `InventoryItem.runtimeMetadata`
|
||||||
|
- `RuntimeRelationAnchor`
|
||||||
|
- `RuntimeItemGenerationContext`
|
||||||
|
|
||||||
|
目的:
|
||||||
|
|
||||||
|
- 不改玩法,只把类型基础搭好
|
||||||
|
|
||||||
|
## 阶段 B:先做 director + compiler
|
||||||
|
|
||||||
|
先不接所有入口,只把:
|
||||||
|
|
||||||
|
- context 采样
|
||||||
|
- AI 意图结构
|
||||||
|
- 本地编译器
|
||||||
|
|
||||||
|
跑通。
|
||||||
|
|
||||||
|
## 阶段 C:先接宝藏
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 独立
|
||||||
|
- 风险低
|
||||||
|
- 最容易观察“背景贴合”提升
|
||||||
|
|
||||||
|
## 阶段 D:再接 NPC 奖励与交易
|
||||||
|
|
||||||
|
这一步会明显提升:
|
||||||
|
|
||||||
|
- 关系系统
|
||||||
|
- 世界贴脸感
|
||||||
|
- 物品来源可解释性
|
||||||
|
|
||||||
|
## 阶段 E:最后接任务奖励与怪物语义掉落
|
||||||
|
|
||||||
|
因为这两类和现有逻辑耦合更深,适合后置。
|
||||||
|
|
||||||
|
## 10. 和旧版方案相比,这次重设计改了什么
|
||||||
|
|
||||||
|
相比之前偏概念化的 AI 原生物品方案,这次重设计更明确了 5 点:
|
||||||
|
|
||||||
|
1. **不新起物品系统**
|
||||||
|
- 直接复用 `InventoryItem` 与现有 build 结构。
|
||||||
|
2. **不让 AI 直接出成品**
|
||||||
|
- 只让 AI 出意图,交给本地编译器落地。
|
||||||
|
3. **所有渠道走同一导演层**
|
||||||
|
- 不再把宝藏、NPC、任务各写一套。
|
||||||
|
4. **把“关系锚点”正式数据化**
|
||||||
|
- 不是只写在文案里。
|
||||||
|
5. **先从宝藏接入,再逐步扩展**
|
||||||
|
- 不是一次推全仓库。
|
||||||
|
|
||||||
|
## 11. 一句话总结
|
||||||
|
|
||||||
|
新的 AI 原生运行时物品生成系统,不应该是“让 AI 随机写几个看起来很酷的装备”,而应该是:
|
||||||
|
|
||||||
|
**让 AI 根据当前世界、场景、NPC、事件和玩家 build 给出物品意图,再由本地规则把这个意图编译成能进背包、能进 build、能进锻造、能进存档、还能被剧情解释的正式物品。**
|
||||||
@@ -0,0 +1,797 @@
|
|||||||
|
# 角色首遇感、背景故事分层解锁与同伴私聊功能设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-04`
|
||||||
|
|
||||||
|
## 0. 目标
|
||||||
|
|
||||||
|
这份方案针对当前仓库,解决 3 个连在一起的问题:
|
||||||
|
|
||||||
|
1. 玩家遇见每一个角色时,在该角色当前好感度对应的关系位置上,都应该有“第一次真正接触”的感觉,而不是一上来就像已经聊过很多轮。
|
||||||
|
2. 角色背景故事目前没有按好感度分层解锁,导致尚未建立关系时,模型和界面都可能提前拿到过深信息。
|
||||||
|
3. 队伍中的高好感同伴虽然已经有聊天弹窗底子,但没有被“好感度 + 面板入口 + 上下文边界”真正串成完整功能。
|
||||||
|
|
||||||
|
这次设计不另起一套独立系统,而是基于当前仓库已有的:
|
||||||
|
|
||||||
|
- `useStoryGeneration` 的角色遭遇与对话流
|
||||||
|
- `npcInteractions.ts` 的好感度与对话阶段规则
|
||||||
|
- `prompt.ts` 的上下文注入机制
|
||||||
|
- `useCharacterChatFlow` / `CharacterChatModal` 的私聊能力
|
||||||
|
|
||||||
|
继续往前补齐。
|
||||||
|
|
||||||
|
## 1. 当前现状与问题定位
|
||||||
|
|
||||||
|
## 1.1 当前“首遇感”不足的根因
|
||||||
|
|
||||||
|
当前问题不是只出在开局同伴,而是整套角色遭遇系统缺少“第一次真正接触”的通用状态。
|
||||||
|
|
||||||
|
主要有 4 个原因:
|
||||||
|
|
||||||
|
1. `src/data/npcInteractions.ts`
|
||||||
|
- `buildInitialNpcState(...)` 会给角色型 NPC 一个初始好感,但当前系统直接把这个好感映射成普通对话阶段。
|
||||||
|
- 也就是说,系统把“当前好感是多少”和“是否已经不是第一次接触”混成了一件事。
|
||||||
|
|
||||||
|
2. `src/data/npcInteractions.ts`
|
||||||
|
- `getNpcChatTopics(...)` 目前只看 `guarded / partial / honest / deep`,不看“这是不是第一次真正交流”。
|
||||||
|
- 结果是,角色只要初始好感不低,就会直接拿到像“追一点表层理由”“开始碰真正目标”这种更像熟人后续轮的聊天切口。
|
||||||
|
|
||||||
|
3. `src/services/prompt.ts`
|
||||||
|
- `describePlayerState(...)` 直接把 `character.backstory` 注入为“主角背景”。
|
||||||
|
- `describeFrontEntity(...)` 直接把 `encounterCharacter.backstory` 注入为“背景”。
|
||||||
|
- 这样模型即使被要求“像初见”,也已经在系统层知道太多,容易写成互背设定卡。
|
||||||
|
|
||||||
|
4. 当前仓库虽然有 `initial_companion / camp_companion` 的开场流,但它只覆盖开局这一种遭遇。
|
||||||
|
- 如果只靠开场特判修正,其他角色第一次见面时仍然会缺乏首遇感。
|
||||||
|
|
||||||
|
一句话总结:
|
||||||
|
|
||||||
|
**当前缺的不是“开场对白模板”,而是“所有角色共用的首遇状态机”。**
|
||||||
|
|
||||||
|
## 1.2 背景故事过早暴露的根因
|
||||||
|
|
||||||
|
当前角色数据只有一个平铺的 `character.backstory`,而没有“公开层 / 解锁层 / 核心秘密层”的结构。
|
||||||
|
|
||||||
|
结果是:
|
||||||
|
|
||||||
|
- 界面层很容易直接把完整背景展示出来。
|
||||||
|
- prompt 层也容易直接把完整背景塞给模型。
|
||||||
|
- 好感度虽然已经控制了 `guarded / partial / honest / deep`,但它只约束“说话方式”,没有约束“哪些背景事实可以进入上下文”。
|
||||||
|
|
||||||
|
这会带来两个后果:
|
||||||
|
|
||||||
|
1. 模型写出来的对白容易像彼此已经认识很久。
|
||||||
|
2. 玩家还没建立关系,就已经在系统层“知道了太多”。
|
||||||
|
|
||||||
|
## 1.3 私聊功能现状
|
||||||
|
|
||||||
|
当前仓库其实已经有私聊底座:
|
||||||
|
|
||||||
|
- `src/hooks/story/characterChat.ts`
|
||||||
|
- 已经有 `useCharacterChatFlow(...)`
|
||||||
|
- 已经能生成建议、流式回复、聊天总结,并写回 `gameState.characterChats`
|
||||||
|
|
||||||
|
- `src/components/CharacterChatModal.tsx`
|
||||||
|
- 已经有完整聊天弹窗
|
||||||
|
|
||||||
|
- `src/components/GameShell.tsx`
|
||||||
|
- 已经挂载了 `CharacterChatModal`
|
||||||
|
- 也把 `onOpenCharacterChat` 传给了 `CharacterPanel`
|
||||||
|
|
||||||
|
但缺的 3 个点还没有补上:
|
||||||
|
|
||||||
|
1. `CharacterPanel` 当前没有真正渲染聊天按钮。
|
||||||
|
2. 游戏内实际查看同伴详情时打开的是 `AdventureEntityModal`,它也没有聊天入口。
|
||||||
|
3. 私聊没有与“队伍成员 + 好感度阈值 + 已解锁背景”绑定。
|
||||||
|
|
||||||
|
所以现在属于“能力有了,但功能还没真正成立”。
|
||||||
|
|
||||||
|
## 2. 总体设计结论
|
||||||
|
|
||||||
|
建议把这次需求拆成 3 条链,同时落地:
|
||||||
|
|
||||||
|
1. **首遇感链**
|
||||||
|
- 所有 `encounter.characterId` 的角色型 NPC,第一次真正接触时都先走通用首遇规则。
|
||||||
|
- 首遇时的亲疏、开放程度和选项排序由“当前好感度 + 是否首遇”共同决定。
|
||||||
|
- `initial_companion / camp_companion` 只保留场景与演出意义,不再承担特殊对话规则本体。
|
||||||
|
|
||||||
|
2. **背景解锁链**
|
||||||
|
- 角色背景改成按好感度分章节解锁。
|
||||||
|
- 未解锁章节既不能在 UI 明文显示,也不能进入 prompt 上下文。
|
||||||
|
|
||||||
|
3. **私聊链**
|
||||||
|
- 队伍中的高好感同伴才能解锁私聊。
|
||||||
|
- 在同伴详情面板中点击按钮打开现有聊天弹窗。
|
||||||
|
- 私聊只吃“公开信息 + 已解锁背景 + 最近共同经历”,不能越权看到锁住的背景章节。
|
||||||
|
|
||||||
|
## 3. 所有角色通用的“首遇感”方案
|
||||||
|
|
||||||
|
## 3.1 核心原则
|
||||||
|
|
||||||
|
必须把下面两件事拆开:
|
||||||
|
|
||||||
|
1. 当前好感度高低
|
||||||
|
2. 这是不是第一次真正接触
|
||||||
|
|
||||||
|
因为:
|
||||||
|
|
||||||
|
- `好感低` 不等于 `第一次见面`
|
||||||
|
- `好感不低` 也不等于 `已经聊过很多轮`
|
||||||
|
|
||||||
|
正确做法应该是“双轴控制”:
|
||||||
|
|
||||||
|
- 好感决定:
|
||||||
|
- 语气亲疏
|
||||||
|
- 允许透露的信息深度
|
||||||
|
- 哪些功能当前有机会成立
|
||||||
|
|
||||||
|
- 首遇状态决定:
|
||||||
|
- 对话必须先像“第一次对上人”
|
||||||
|
- 优先说现场判断、态度试探、短钩子
|
||||||
|
- 不能一上来就跳到“已经熟悉彼此”的后续轮节奏
|
||||||
|
|
||||||
|
## 3.2 状态设计
|
||||||
|
|
||||||
|
建议给 `NpcPersistentState` 增加一个通用字段:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface NpcPersistentState {
|
||||||
|
affinity: number;
|
||||||
|
relationState?: RoleRelationState;
|
||||||
|
helpUsed: boolean;
|
||||||
|
chattedCount: number;
|
||||||
|
giftsGiven: number;
|
||||||
|
inventory: InventoryItem[];
|
||||||
|
recruited: boolean;
|
||||||
|
revealedFacts?: string[];
|
||||||
|
knownAttributeRumors?: string[];
|
||||||
|
firstMeaningfulContactResolved?: boolean;
|
||||||
|
seenBackstoryChapterIds?: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `firstMeaningfulContactResolved`
|
||||||
|
- 适用于所有角色型 NPC
|
||||||
|
- 表示“玩家是否已经和这个角色完成过第一轮真正接触”
|
||||||
|
- 不等于 `chattedCount > 0`
|
||||||
|
- 不等于 `recruited === true`
|
||||||
|
- 不等于 `affinity` 已经较高
|
||||||
|
|
||||||
|
- `seenBackstoryChapterIds`
|
||||||
|
- 用于背景章节首次解锁时的提示去重
|
||||||
|
|
||||||
|
建议补这组 helper:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function isNpcFirstMeaningfulContact(
|
||||||
|
encounter: Encounter,
|
||||||
|
npcState: NpcPersistentState,
|
||||||
|
): boolean
|
||||||
|
|
||||||
|
function markNpcFirstMeaningfulContactResolved(
|
||||||
|
npcState: NpcPersistentState,
|
||||||
|
): NpcPersistentState
|
||||||
|
```
|
||||||
|
|
||||||
|
判断建议:
|
||||||
|
|
||||||
|
- 仅对 `encounter.characterId` 的角色型 NPC 启用
|
||||||
|
- `firstMeaningfulContactResolved !== true` 时,进入首遇模式
|
||||||
|
|
||||||
|
## 3.3 首遇时的好感度矩阵
|
||||||
|
|
||||||
|
首遇不是永远冷冰冰,而是要落在“当前好感度对应的位置”上。
|
||||||
|
|
||||||
|
建议把首遇风格做成这张矩阵:
|
||||||
|
|
||||||
|
| 当前关系站位 | 好感区间参考 | 首遇时的感觉 | 信息上限 | 选项重心 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `guarded` | `<= 14` | 明显戒备、先观察你值不值得回应 | 只谈眼前局势、模糊钩子 | 观察、试探、判断 |
|
||||||
|
| `neutral` | `15 - 34` | 可以正常交流,但还不是熟人 | 可给表层理由,不给深层秘密 | 互探来意、确认立场 |
|
||||||
|
| `cooperative` | `35 - 59` | 带基础善意或认可,但仍是第一次正式接触 | 能谈轮廓,不谈全部底牌 | 确认合作、交换判断 |
|
||||||
|
| `bonded` | `>= 60` | 明显信任,但仍应像“第一次真正见到本人/第一次正面对接” | 可谈较深动机,但不一次讲完所有旧事 | 确认并肩关系、推进共同目标 |
|
||||||
|
|
||||||
|
重点不是绝对数值,而是这句话:
|
||||||
|
|
||||||
|
**第一次见面时,角色可以按当前好感更冷或更暖,但不能直接写成“已经是后续轮”。**
|
||||||
|
|
||||||
|
## 3.4 首遇选项改法
|
||||||
|
|
||||||
|
当前 `getNpcChatTopics(...)` 和 `buildNpcEncounterStoryMoment(...)` 更像“已进入常规互动层”后的选项生成。
|
||||||
|
|
||||||
|
建议新增一层:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function getNpcFirstContactTopics(
|
||||||
|
encounter: Encounter,
|
||||||
|
npcState: NpcPersistentState,
|
||||||
|
): StoryOption[]
|
||||||
|
|
||||||
|
function buildNpcFirstContactOptionCatalog(
|
||||||
|
encounter: Encounter,
|
||||||
|
npcState: NpcPersistentState,
|
||||||
|
baseOptions: StoryOption[],
|
||||||
|
): StoryOption[]
|
||||||
|
```
|
||||||
|
|
||||||
|
规则建议:
|
||||||
|
|
||||||
|
1. 只要是 `firstMeaningfulContactResolved !== true`,前 2 个选项必须来自首遇话题池。
|
||||||
|
|
||||||
|
2. 首遇话题池只做这几类:
|
||||||
|
- 现场判断
|
||||||
|
- 你为什么在这里
|
||||||
|
- 你刚才在观察什么
|
||||||
|
- 你对我的态度和判断
|
||||||
|
- 前面那件事到底哪里不对
|
||||||
|
|
||||||
|
3. 现有其他合法功能可以继续保留,但排序必须后置:
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_help`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_recruit`
|
||||||
|
|
||||||
|
补一条实现约束:
|
||||||
|
|
||||||
|
- 首次进入 `npc_chat` 时,前端聊天状态里不允许直接塞预设对白充当首句。
|
||||||
|
- 角色第一次真正对玩家开口时说什么,必须由 `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. 首遇状态下,不允许前两项直接变成:
|
||||||
|
- 深背景追问
|
||||||
|
- 直接招募
|
||||||
|
- 直接交易
|
||||||
|
- 直接切磋
|
||||||
|
- 直接熟人式寒暄
|
||||||
|
|
||||||
|
这里的关键不是“把功能全部禁掉”,而是:
|
||||||
|
|
||||||
|
**先保证玩家看到的是首遇节奏,再决定这一轮后面还要不要承接更深功能。**
|
||||||
|
|
||||||
|
## 3.5 首遇期功能排序规则
|
||||||
|
|
||||||
|
为了兼容“不同角色当前好感不同”的需求,建议不要把首遇期做成一刀切的硬封锁,而是做成排序和表达规则:
|
||||||
|
|
||||||
|
1. `npc_chat`
|
||||||
|
- 首遇期必须优先出现
|
||||||
|
- 至少两个切口都与“眼前”和“第一判断”有关
|
||||||
|
|
||||||
|
2. `npc_recruit`
|
||||||
|
- 如果按当前好感已经合法,可以出现
|
||||||
|
- 但不能排在前两项
|
||||||
|
- 文案要像“确认是否并肩同行”,不能像已经熟到直接收编
|
||||||
|
|
||||||
|
3. `npc_trade / npc_help / npc_quest_accept`
|
||||||
|
- 角色职业允许时可以出现
|
||||||
|
- 但文案必须像“先试着谈这件事”,不是默认彼此早就熟悉流程
|
||||||
|
|
||||||
|
4. `npc_fight / npc_spar`
|
||||||
|
- 如果剧情确实需要,可以保留
|
||||||
|
- 但 storyText 里仍要先体现“第一次正面对上”的张力
|
||||||
|
|
||||||
|
这样做的好处是:
|
||||||
|
|
||||||
|
- 既满足“每个角色都按当前好感落点来写”
|
||||||
|
- 又不会把首遇感做成只能用于开局同伴的特殊模板
|
||||||
|
|
||||||
|
## 3.6 首遇状态何时结束
|
||||||
|
|
||||||
|
建议当玩家与该角色完成一次真正交互结算后,就把 `firstMeaningfulContactResolved` 设为 `true`。
|
||||||
|
|
||||||
|
推荐计入“真正交互”的动作:
|
||||||
|
|
||||||
|
- `npc_preview_talk` 后进入并完成一次正式对白
|
||||||
|
- `npc_chat`
|
||||||
|
- `npc_help`
|
||||||
|
- `npc_trade`
|
||||||
|
- `npc_gift`
|
||||||
|
- `npc_recruit`
|
||||||
|
- `npc_quest_accept`
|
||||||
|
- `npc_spar`
|
||||||
|
- `npc_fight`
|
||||||
|
|
||||||
|
不建议计入:
|
||||||
|
|
||||||
|
- 仅看了一眼就离开
|
||||||
|
- 只打开 UI 没有完成结算
|
||||||
|
|
||||||
|
这样可以保证:
|
||||||
|
|
||||||
|
- 第一次接触是独立阶段
|
||||||
|
- 第二次开始才进入当前系统已有的常规关系推进节奏
|
||||||
|
|
||||||
|
## 3.7 prompt 与 fallback 约束
|
||||||
|
|
||||||
|
建议新增一个通用的首遇指令,而不是继续把首遇逻辑绑定在开场专用 helper 上。
|
||||||
|
|
||||||
|
### 1. prompt 上下文字段
|
||||||
|
|
||||||
|
建议在 `StoryGenerationContext` 增加:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
isFirstMeaningfulContact?: boolean;
|
||||||
|
firstContactRelationStance?: 'guarded' | 'neutral' | 'cooperative' | 'bonded' | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. prompt 约束
|
||||||
|
|
||||||
|
只要 `isFirstMeaningfulContact === true`:
|
||||||
|
|
||||||
|
- 不再把 `character.backstory` 作为“主角背景”直接注入
|
||||||
|
- 不再把 `encounterCharacter.backstory` 作为“背景”直接注入
|
||||||
|
- 只允许注入:
|
||||||
|
- `publicSummary`
|
||||||
|
- `surfaceHook`
|
||||||
|
- `immediateConcern`
|
||||||
|
- 已公开的角色描述
|
||||||
|
- 现场状态
|
||||||
|
- 当前关系站位
|
||||||
|
|
||||||
|
### 3. fallback 约束
|
||||||
|
|
||||||
|
如果保留现有 `buildInitialCompanionDialogueText(...)` 一类 helper:
|
||||||
|
|
||||||
|
- 它们只能作为“某个具体场景下调用通用首遇规则”的薄包装
|
||||||
|
- 不应继续承担独立的开场规则系统
|
||||||
|
- 更不能把本地预设对白直接写进 `npc_chat` 的可见对话历史里,`npc_chat` 首个角色台词必须由 prompt 生成
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**开场营地只是首遇规则的一个调用场景,不是首遇规则本体。**
|
||||||
|
|
||||||
|
## 4. 背景故事分层解锁设计
|
||||||
|
|
||||||
|
## 4.1 核心原则
|
||||||
|
|
||||||
|
背景故事要拆成“能公开的层”和“需要建立关系后才能知道的层”。
|
||||||
|
|
||||||
|
约束是:
|
||||||
|
|
||||||
|
1. 未达到对应好感度前,该章节不能进入 prompt 上下文。
|
||||||
|
2. 未达到对应好感度前,该章节不能在角色详情中完整展示。
|
||||||
|
3. 即使模型之前已经从别的地方生成过相关话头,未解锁章节也不能被总结层写成稳定关系记忆。
|
||||||
|
|
||||||
|
## 4.2 数据结构
|
||||||
|
|
||||||
|
建议在 `Character` 上增加背景解锁配置:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CharacterBackstoryChapter {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
affinityRequired: number;
|
||||||
|
teaser: string;
|
||||||
|
content: string;
|
||||||
|
contextSnippet: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CharacterBackstoryRevealConfig {
|
||||||
|
publicSummary: string;
|
||||||
|
chapters: CharacterBackstoryChapter[];
|
||||||
|
privateChatUnlockAffinity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Character {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
backstory: string;
|
||||||
|
backstoryReveal?: CharacterBackstoryRevealConfig;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段职责:
|
||||||
|
|
||||||
|
- `backstory`
|
||||||
|
- 保留为原始完整设定文本
|
||||||
|
- 不再默认直接进入运行时 prompt
|
||||||
|
|
||||||
|
- `publicSummary`
|
||||||
|
- 永远允许展示和注入
|
||||||
|
- 只用于“第一印象”和公开层信息
|
||||||
|
|
||||||
|
- `teaser`
|
||||||
|
- 章节未解锁时给 UI 的短提示
|
||||||
|
|
||||||
|
- `content`
|
||||||
|
- 玩家真正看到的章节正文
|
||||||
|
|
||||||
|
- `contextSnippet`
|
||||||
|
- 允许注入模型上下文的精简版
|
||||||
|
- 明确只包含“系统允许此阶段知道的事实”
|
||||||
|
|
||||||
|
## 4.3 推荐阈值
|
||||||
|
|
||||||
|
建议不要直接复用 `guarded / partial / honest / deep` 作为背景章节解锁阈值,因为当前对话阶段对“已招募”有额外放宽。
|
||||||
|
|
||||||
|
尤其是:
|
||||||
|
|
||||||
|
- `getNpcDisclosureStage(...)`
|
||||||
|
- `getNpcWarmthStage(...)`
|
||||||
|
|
||||||
|
目前都会在 `recruited === true` 时直接给更高阶段。
|
||||||
|
|
||||||
|
这适合控制说话语气,但不适合控制“背景章节是否已解锁”。
|
||||||
|
|
||||||
|
因此建议新增独立阈值:
|
||||||
|
|
||||||
|
| 层级 | 建议阈值 | 含义 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 公开层 | `0` | 只展示公开印象,不算真正解锁 |
|
||||||
|
| 第一章 | `20` | 来路表层、最近为何会出现在这里 |
|
||||||
|
| 第二章 | `40` | 旧事伤痕、过去某段关键经历 |
|
||||||
|
| 第三章 | `65` | 真正目标、必须完成的事 |
|
||||||
|
| 第四章 | `85` | 最深层秘密、最不愿轻易说出的事实 |
|
||||||
|
|
||||||
|
这样做的好处是:
|
||||||
|
|
||||||
|
- 首遇阶段仍然有“先认识再深入”的节奏
|
||||||
|
- 招募后也不会立刻把全部背景放开
|
||||||
|
- 高好感的成长曲线能真正体现在“知道了多少”
|
||||||
|
|
||||||
|
## 4.4 运行时规则
|
||||||
|
|
||||||
|
建议增加这组 helper:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function getUnlockedBackstoryChapters(
|
||||||
|
character: Character,
|
||||||
|
affinity: number,
|
||||||
|
): CharacterBackstoryChapter[]
|
||||||
|
|
||||||
|
function getNextLockedBackstoryChapter(
|
||||||
|
character: Character,
|
||||||
|
affinity: number,
|
||||||
|
): CharacterBackstoryChapter | null
|
||||||
|
|
||||||
|
function buildBackstoryPromptContext(
|
||||||
|
character: Character,
|
||||||
|
affinity: number,
|
||||||
|
): string[]
|
||||||
|
```
|
||||||
|
|
||||||
|
使用规则:
|
||||||
|
|
||||||
|
- UI 展示:
|
||||||
|
- `publicSummary` 永远可见
|
||||||
|
- 已解锁章节显示全文
|
||||||
|
- 未解锁章节显示 `title + teaser + 所需好感`
|
||||||
|
|
||||||
|
- prompt 注入:
|
||||||
|
- 只能使用 `publicSummary + unlocked.contextSnippet`
|
||||||
|
- 禁止直接把 `backstory` 全文塞给模型
|
||||||
|
|
||||||
|
- 总结沉淀:
|
||||||
|
- 关系总结、私聊总结只允许总结已解锁章节
|
||||||
|
- 未解锁章节即使被模型“猜中”,也不写入稳定状态
|
||||||
|
|
||||||
|
## 4.5 解锁提示
|
||||||
|
|
||||||
|
建议当好感跨越阈值时,在运行时给一次轻提示:
|
||||||
|
|
||||||
|
- 文案示例:
|
||||||
|
- `你对 宁霜 的过去有了更多了解:旧军密图`
|
||||||
|
- `你察觉到 萧烬 愿意谈及更深的一层旧事了`
|
||||||
|
|
||||||
|
实现上不需要新增复杂通知系统,可以先:
|
||||||
|
|
||||||
|
- 在好感变化后检查本次跨越了哪些章节阈值
|
||||||
|
- 若有新章节且 `seenBackstoryChapterIds` 未记录,则插入一条轻量 UI 提示或一条短故事结果文本
|
||||||
|
|
||||||
|
## 5. “未解锁背景不能加入上下文”的具体边界
|
||||||
|
|
||||||
|
这个需求要同时卡住 4 个入口:
|
||||||
|
|
||||||
|
## 5.1 主线剧情 prompt
|
||||||
|
|
||||||
|
`buildStoryContextFromState(...)` 与 `prompt.ts` 中:
|
||||||
|
|
||||||
|
- 角色背景不能再直接读取 `character.backstory`
|
||||||
|
- 应改成读取 `buildBackstoryPromptContext(...)`
|
||||||
|
|
||||||
|
## 5.2 NPC 聊天 prompt
|
||||||
|
|
||||||
|
`buildStrictNpcChatDialoguePrompt(...)` 中:
|
||||||
|
|
||||||
|
- 只能拿 `publicSummary + 已解锁章节 + 最近共同经历`
|
||||||
|
- 未解锁章节不能出现在“当前面前实体”描述里
|
||||||
|
- 若当前还是首遇模式,还要进一步收紧到“公开层 + 当前场景判断”
|
||||||
|
|
||||||
|
## 5.3 私聊 prompt
|
||||||
|
|
||||||
|
`useCharacterChatFlow(...)` 中:
|
||||||
|
|
||||||
|
- `streamCharacterPanelChatReply(...)`
|
||||||
|
- `generateCharacterPanelChatSuggestions(...)`
|
||||||
|
- `generateCharacterPanelChatSummary(...)`
|
||||||
|
|
||||||
|
都只能吃已解锁背景。
|
||||||
|
|
||||||
|
## 5.4 关系摘要回写
|
||||||
|
|
||||||
|
`gameState.characterChats[characterId].summary` 是会反向进入主线上下文的。
|
||||||
|
|
||||||
|
所以必须再加一层约束:
|
||||||
|
|
||||||
|
- 如果某段私聊总结提到了未解锁章节内容,则不允许写回稳定 summary
|
||||||
|
- 或者更稳妥地说,summary 生成 prompt 本身就只提供已解锁背景
|
||||||
|
|
||||||
|
推荐做法是后者,因为它更简单也更可控。
|
||||||
|
|
||||||
|
## 6. 高好感同伴私聊设计
|
||||||
|
|
||||||
|
## 6.1 解锁条件
|
||||||
|
|
||||||
|
建议私聊不是“所有招募同伴自动可用”,而是满足以下条件后才解锁:
|
||||||
|
|
||||||
|
1. 角色已在队伍体系中
|
||||||
|
- `gameState.companions`
|
||||||
|
- 或 `gameState.roster`
|
||||||
|
|
||||||
|
2. 对应 `npcState.recruited === true`
|
||||||
|
|
||||||
|
3. 当前好感度达到私聊阈值
|
||||||
|
- 默认建议 `70`
|
||||||
|
- 允许角色级配置覆盖:`backstoryReveal.privateChatUnlockAffinity`
|
||||||
|
|
||||||
|
之所以不用“已招募”直接等于“可私聊”,原因很简单:
|
||||||
|
|
||||||
|
- 当前系统里已招募会把对话阶段直接推高
|
||||||
|
- 但用户要求的是“高好感同伴解锁私聊”
|
||||||
|
- 所以私聊需要独立阈值,不应绑定到 `warm / deep`
|
||||||
|
|
||||||
|
## 6.2 入口位置
|
||||||
|
|
||||||
|
建议把入口放在两个地方,但以“同伴角色信息面板”为主:
|
||||||
|
|
||||||
|
### 1. `AdventureEntityModal`
|
||||||
|
|
||||||
|
这是游戏内点击同伴详情时实际打开的面板,优先级最高。
|
||||||
|
|
||||||
|
在 `selection.kind === 'companion'` 时增加:
|
||||||
|
|
||||||
|
- 已解锁:`聊天`
|
||||||
|
- 未解锁:置灰按钮 + 提示 `好感达到 70 后解锁,当前 58`
|
||||||
|
|
||||||
|
点击逻辑:
|
||||||
|
|
||||||
|
1. 关闭 `AdventureEntityModal`
|
||||||
|
2. 调 `characterChatUi.openChat(target)`
|
||||||
|
3. 打开现有 `CharacterChatModal`
|
||||||
|
|
||||||
|
### 2. `CharacterPanel`
|
||||||
|
|
||||||
|
如果以后存在不走 `AdventureEntityModal` 的详情流,也应在成员详情面板里显示同样按钮,保持一致。
|
||||||
|
|
||||||
|
## 6.3 聊天目标结构
|
||||||
|
|
||||||
|
建议扩展 `CharacterChatTarget`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CharacterChatTarget = {
|
||||||
|
character: Character;
|
||||||
|
npcId: string | null;
|
||||||
|
roleLabel: string;
|
||||||
|
hp: number;
|
||||||
|
maxHp: number;
|
||||||
|
mana: number;
|
||||||
|
maxMana: number;
|
||||||
|
affinity?: number;
|
||||||
|
unlockedBackstoryChapterIds?: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样聊天弹窗里可以直接显示:
|
||||||
|
|
||||||
|
- 当前关系热度
|
||||||
|
- 已解锁背景章节数
|
||||||
|
- 下一段背景解锁阈值
|
||||||
|
|
||||||
|
## 6.4 私聊的上下文来源
|
||||||
|
|
||||||
|
私聊上下文建议固定由这几部分组成:
|
||||||
|
|
||||||
|
1. 角色公开信息
|
||||||
|
- `name / title / description / personality`
|
||||||
|
|
||||||
|
2. 当前关系信息
|
||||||
|
- 当前好感
|
||||||
|
- 是否出战 / 是否营地待命
|
||||||
|
- 最近共同经历
|
||||||
|
|
||||||
|
3. 已解锁背景章节
|
||||||
|
- 只传 `contextSnippet`
|
||||||
|
|
||||||
|
4. 最近私聊记录
|
||||||
|
- `characterChats[characterId].history`
|
||||||
|
- `characterChats[characterId].summary`
|
||||||
|
|
||||||
|
明确不传:
|
||||||
|
|
||||||
|
- 未解锁背景章节
|
||||||
|
- 角色完整 `backstory`
|
||||||
|
|
||||||
|
## 6.5 私聊对数值的影响
|
||||||
|
|
||||||
|
建议本期私聊先不直接改好感数值,理由是:
|
||||||
|
|
||||||
|
- 避免玩家通过无限聊天刷关系
|
||||||
|
- 先把“关系表达层”和“规则数值层”分开
|
||||||
|
- 当前已有送礼、聊天、切磋、求助、任务等好感入口,足够支撑成长
|
||||||
|
|
||||||
|
私聊本期的价值应放在:
|
||||||
|
|
||||||
|
- 沉淀角色关系摘要
|
||||||
|
- 解锁更深层的文本风格和背景信息
|
||||||
|
- 让队伍中的同伴真正像“会私下说话的人”
|
||||||
|
|
||||||
|
如果后续要给私聊轻量数值收益,建议再单独加冷却或每日首次奖励,不要在本期一起做。
|
||||||
|
|
||||||
|
## 7. UI 设计建议
|
||||||
|
|
||||||
|
## 7.1 同伴详情面板
|
||||||
|
|
||||||
|
建议增加一个“关系 / 档案”区块:
|
||||||
|
|
||||||
|
- 当前好感:`58`
|
||||||
|
- 关系阶段:`谨慎` / `熟络` / `深信`
|
||||||
|
- 首遇状态:
|
||||||
|
- `初次接触未完成`
|
||||||
|
- 或 `已完成第一次正式对接`
|
||||||
|
- 私聊状态:
|
||||||
|
- `未解锁(70)`
|
||||||
|
- 或 `已解锁`
|
||||||
|
|
||||||
|
下面接“背景档案”:
|
||||||
|
|
||||||
|
- 公开印象
|
||||||
|
- 章节卡片列表
|
||||||
|
|
||||||
|
章节卡片状态:
|
||||||
|
|
||||||
|
- 已解锁
|
||||||
|
- 标题
|
||||||
|
- 正文
|
||||||
|
|
||||||
|
- 未解锁
|
||||||
|
- 标题
|
||||||
|
- `需要好感 40`
|
||||||
|
- `teaser`
|
||||||
|
- 遮罩态
|
||||||
|
|
||||||
|
## 7.2 私聊按钮状态
|
||||||
|
|
||||||
|
按钮文案建议:
|
||||||
|
|
||||||
|
- 已解锁:`聊天`
|
||||||
|
- 未解锁:`聊天(70 解锁)`
|
||||||
|
|
||||||
|
按钮旁边可以补一行细字:
|
||||||
|
|
||||||
|
- `仅高好感同伴可进行私聊`
|
||||||
|
|
||||||
|
## 7.3 解锁反馈
|
||||||
|
|
||||||
|
建议当玩家在详情面板里刚好看到某一章解锁时:
|
||||||
|
|
||||||
|
- 章节卡片做一次轻量高亮
|
||||||
|
- 私聊按钮从置灰切到可点击时也做一次轻量强调
|
||||||
|
|
||||||
|
这样能把“关系成长”明确反馈给玩家,而不只是静态数字变化。
|
||||||
|
|
||||||
|
## 8. 落地文件建议
|
||||||
|
|
||||||
|
## 8.1 类型与数据
|
||||||
|
|
||||||
|
- `src/types/characters.ts`
|
||||||
|
- 增加 `CharacterBackstoryChapter`
|
||||||
|
- 增加 `CharacterBackstoryRevealConfig`
|
||||||
|
|
||||||
|
- `src/types/scene.ts`
|
||||||
|
- 给 `NpcPersistentState` 增加
|
||||||
|
- `firstMeaningfulContactResolved`
|
||||||
|
- `seenBackstoryChapterIds`
|
||||||
|
|
||||||
|
- `src/data/characterPresets.ts`
|
||||||
|
- 为可招募角色补 `backstoryReveal`
|
||||||
|
- 配置分章节阈值和 `privateChatUnlockAffinity`
|
||||||
|
|
||||||
|
## 8.2 关系与规则
|
||||||
|
|
||||||
|
- `src/data/npcInteractions.ts`
|
||||||
|
- 增加首遇状态 helper
|
||||||
|
- 增加首遇话题 helper
|
||||||
|
- 增加背景章节解锁 helper
|
||||||
|
- 增加私聊解锁 helper
|
||||||
|
- 调整首遇期的功能排序规则
|
||||||
|
|
||||||
|
## 8.3 剧情与 prompt
|
||||||
|
|
||||||
|
- `src/hooks/useStoryGeneration.ts`
|
||||||
|
- 新增通用首遇状态流转
|
||||||
|
- 给首遇角色注入统一的 first-contact context
|
||||||
|
- 处理好感跨章节时的解锁通知
|
||||||
|
|
||||||
|
- `src/services/aiTypes.ts`
|
||||||
|
- 给 `StoryGenerationContext` 增加
|
||||||
|
- `isFirstMeaningfulContact`
|
||||||
|
- `firstContactRelationStance`
|
||||||
|
- 已解锁背景片段字段
|
||||||
|
|
||||||
|
- `src/services/prompt.ts`
|
||||||
|
- 移除对完整 `backstory` 的直接注入
|
||||||
|
- 改为按已解锁章节构造角色背景上下文
|
||||||
|
- 首遇模式下额外收紧信息披露
|
||||||
|
|
||||||
|
## 8.4 UI 与交互
|
||||||
|
|
||||||
|
- `src/components/AdventureEntityModal.tsx`
|
||||||
|
- 显示同伴关系/档案区
|
||||||
|
- 增加聊天按钮
|
||||||
|
|
||||||
|
- `src/components/CharacterPanel.tsx`
|
||||||
|
- 补齐聊天按钮接线
|
||||||
|
- 若保留本地详情弹窗,也同步显示档案区
|
||||||
|
|
||||||
|
- `src/components/GameShell.tsx`
|
||||||
|
- 把 `characterChatUi.openChat` 透传给 `AdventureEntityModal`
|
||||||
|
|
||||||
|
- `src/hooks/story/characterChat.ts`
|
||||||
|
- 私聊 prompt 只使用已解锁章节
|
||||||
|
|
||||||
|
## 9. 推荐开发顺序
|
||||||
|
|
||||||
|
建议按这 4 步做,不要反过来:
|
||||||
|
|
||||||
|
1. 先补数据建模
|
||||||
|
- `backstoryReveal`
|
||||||
|
- `firstMeaningfulContactResolved`
|
||||||
|
- 解锁 helper
|
||||||
|
|
||||||
|
2. 再补规则
|
||||||
|
- 首遇状态判断
|
||||||
|
- 首遇选项排序
|
||||||
|
- 首遇完成后的状态流转
|
||||||
|
- 私聊解锁条件
|
||||||
|
|
||||||
|
3. 再补 prompt 上下文边界
|
||||||
|
- 禁止未解锁背景进入模型上下文
|
||||||
|
- 禁止首遇期被写成后续轮
|
||||||
|
|
||||||
|
4. 最后补 UI
|
||||||
|
- 详情面板档案区
|
||||||
|
- 聊天按钮
|
||||||
|
- 解锁提示
|
||||||
|
|
||||||
|
## 10. 验收标准
|
||||||
|
|
||||||
|
做到以下几点,才算这次需求真正完成:
|
||||||
|
|
||||||
|
1. 玩家第一次遇见任一角色型 NPC 时,对话会像第一次真正接触,而不是像已经聊过很多轮。
|
||||||
|
2. 同一个角色第一次见面时的冷暖程度,会落在该角色当前好感对应的位置上,而不是被强行写成统一冷场模板。
|
||||||
|
3. 首遇阶段的前两项选项会优先围绕“眼前局势、互相判断、来意试探”,而不是直接跳进深聊、招募或熟人功能。
|
||||||
|
4. 未达到阈值的背景章节在 UI 中不可完整查看,在 prompt 中也不可被注入。
|
||||||
|
5. 好感提高后,角色详情面板中的背景章节会逐步解锁。
|
||||||
|
6. 队伍中的高好感同伴会在详情面板中出现“聊天”按钮。
|
||||||
|
7. 点击“聊天”后,能直接弹出现有私聊弹窗。
|
||||||
|
8. 私聊内容、建议、总结都不会越权使用未解锁背景。
|
||||||
|
9. 旧存档读取后不会崩,缺省字段能平稳兜底。
|
||||||
|
|
||||||
|
## 11. 一句话收束
|
||||||
|
|
||||||
|
这次需求的关键不是“给开局补一段特判对白”,而是把:
|
||||||
|
|
||||||
|
- 首遇节奏
|
||||||
|
- 背景披露节奏
|
||||||
|
- 私聊入口
|
||||||
|
- 关系记忆
|
||||||
|
|
||||||
|
统一到同一套“好感度 + 首遇状态 + 信息解锁”的通用规则里。
|
||||||
|
|
||||||
|
只有这样,玩家遇见每一个角色时才都会有对应关系位置上的初识感,角色背景才会像一步步了解出来的,同伴私聊也才会真的有价值。
|
||||||
@@ -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 签名和过程入参漂移。
|
||||||
@@ -0,0 +1,491 @@
|
|||||||
|
# 自定义世界创作者输入与 AI 分工边界设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-06`
|
||||||
|
|
||||||
|
## 0. 目标
|
||||||
|
|
||||||
|
这份文档回答一个非常关键的问题:
|
||||||
|
|
||||||
|
**在“低创作门槛、高创作自由度”的前提下,自定义世界里哪些内容应该交给创作者直接定义,哪些内容应该交给 AI 和系统完成。**
|
||||||
|
|
||||||
|
这里默认我们的创作者:
|
||||||
|
|
||||||
|
- 不需要有专业作家背景
|
||||||
|
- 不需要有专业游戏设计背景
|
||||||
|
- 但希望作品有明显个人风格,而不是只是在用一个会自动补全设定的模板工具
|
||||||
|
|
||||||
|
一句话目标:
|
||||||
|
|
||||||
|
**让创作者把精力放在“决定这个世界为什么值得被创作”,把 AI 用在“把这个世界展开、编译、铺开、校验、补足”。**
|
||||||
|
|
||||||
|
## 1. 总体结论
|
||||||
|
|
||||||
|
自定义世界的分工边界应该遵守 3 条硬原则:
|
||||||
|
|
||||||
|
1. 灵魂归创作者,杂活归 AI。
|
||||||
|
- 凡是决定作品气质、主题、冲突、人物关系、审美方向的内容,都应由创作者掌握。
|
||||||
|
|
||||||
|
2. 重点对象归创作者,长尾铺量归 AI。
|
||||||
|
- 创作者应重点塑造少量关键角色、关键地点、关键冲突、关键意象,而不是被迫手填几十个 NPC、几十个场景、几百条描述。
|
||||||
|
|
||||||
|
3. 决策归创作者,编译归 AI / 系统。
|
||||||
|
- 创作者负责说“这个世界要成为什么样”,AI / 系统负责把它编译成可运行的数据、规则、文本、关系钩子和运行时结构。
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
|
||||||
|
- 创作者应该主要编辑“高杠杆创作锚点”
|
||||||
|
- AI 应该主要承担“批量展开 + 结构编译 + 一致性维护 + 专业执行”
|
||||||
|
|
||||||
|
## 2. 什么内容应该交给创作者
|
||||||
|
|
||||||
|
真正应该交给创作者的,不是大量表格字段,而是下面这些会显著决定作品质量、且 AI 不擅长替代的内容。
|
||||||
|
|
||||||
|
## 2.1 世界核心命题
|
||||||
|
|
||||||
|
创作者应该直接定义:
|
||||||
|
|
||||||
|
- 这个世界的一句话设定
|
||||||
|
- 这个世界最吸引人的核心幻想
|
||||||
|
- 玩家来到这个世界,最想体验的感觉是什么
|
||||||
|
- 这个世界和常规同题材作品相比,最不同的地方是什么
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这是作品的创作方向盘
|
||||||
|
- 一旦这一层是空的,后面所有 AI 扩写都会变成“像一个世界”,而不是“这个世界”
|
||||||
|
|
||||||
|
## 2.2 主题、气质与边界
|
||||||
|
|
||||||
|
创作者应该直接定义:
|
||||||
|
|
||||||
|
- 主题关键词
|
||||||
|
- 情绪基调
|
||||||
|
- 审美偏好
|
||||||
|
- 禁忌内容 / 不希望出现的表达
|
||||||
|
- 可以接受的黑暗度、浪漫度、残酷度、神秘度
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这决定了 AI 后续生成时的“味道”
|
||||||
|
- 这类判断很难靠 AI 替代,因为它本质上不是信息补全,而是审美取舍
|
||||||
|
|
||||||
|
## 2.3 玩家身份与开局处境
|
||||||
|
|
||||||
|
创作者应该直接定义:
|
||||||
|
|
||||||
|
- 玩家扮演的是什么人
|
||||||
|
- 玩家一开始最缺什么、最想要什么
|
||||||
|
- 开局时玩家被卷入什么局面
|
||||||
|
- 玩家在这个世界里天然站在哪个位置上
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这决定了整个世界的观看视角
|
||||||
|
- 同一个世界,玩家视角不同,最终体验会完全不同
|
||||||
|
|
||||||
|
## 2.4 核心冲突与关键势力
|
||||||
|
|
||||||
|
创作者应该直接定义少量高价值内容:
|
||||||
|
|
||||||
|
- 世界当前最重要的 `2~4` 条明面冲突
|
||||||
|
- 世界背后最关键的 `1~3` 条暗面问题
|
||||||
|
- `2~6` 个关键势力
|
||||||
|
- 这些势力各自想要什么、害怕什么、互相卡住了什么
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 冲突结构决定世界是否“有戏”
|
||||||
|
- 势力关系是 AI 最容易写散、写平、写成百科介绍的部分
|
||||||
|
- 这一层由创作者把握,才能真正提高作品的辨识度
|
||||||
|
|
||||||
|
## 2.5 关键角色与关系张力
|
||||||
|
|
||||||
|
创作者应该直接定义少量关键角色,而不是所有 NPC。
|
||||||
|
|
||||||
|
建议重点交给创作者的,是:
|
||||||
|
|
||||||
|
- `3~8` 个关键角色
|
||||||
|
- 玩家与这些人的潜在关系
|
||||||
|
- 这些角色彼此之间的债、仇、秘密、误解、利益绑定
|
||||||
|
- 每个关键角色“表面上像什么、实际上压着什么”
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 角色关系是最能显著提升作品质量的部分之一
|
||||||
|
- 这也是 AI 最容易写得“完整但无味”的部分
|
||||||
|
- 创作者不需要写长篇背景,但应掌握这些角色真正的关系骨架
|
||||||
|
|
||||||
|
## 2.6 关键地点与空间记忆点
|
||||||
|
|
||||||
|
创作者应该直接定义:
|
||||||
|
|
||||||
|
- `4~12` 个关键地点 / 区域 / 地标
|
||||||
|
- 这些地方为什么重要
|
||||||
|
- 这些地方承载什么冲突、危险、秘密或情绪记忆
|
||||||
|
- 玩家第一次来到这里时应该感到什么
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- “地方感”是世界质量的重要来源
|
||||||
|
- 关键地点一旦成立,AI 后续才能稳定地生成周边事件、物件、NPC 和线索
|
||||||
|
|
||||||
|
## 2.7 标志性意象、物件、怪物、制度与规则
|
||||||
|
|
||||||
|
创作者应该优先控制世界里最能代表它的东西:
|
||||||
|
|
||||||
|
- 标志性物件
|
||||||
|
- 标志性怪物 / 生物
|
||||||
|
- 标志性能力体系 / 修炼体系 / 技术体系
|
||||||
|
- 标志性社会制度 / 宗教 / 仪式 / 禁忌
|
||||||
|
- 世界的硬规则
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些内容决定世界的“手感”
|
||||||
|
- 它们不是普通细节,而是会反复影响命名、剧情、视觉、对话与玩法解释的母题
|
||||||
|
|
||||||
|
## 2.8 创作者应直接控制的“禁止事项”
|
||||||
|
|
||||||
|
创作者必须能明确锁定:
|
||||||
|
|
||||||
|
- 什么绝对不能改
|
||||||
|
- 什么不能被 AI 自动扩写到别的方向
|
||||||
|
- 哪些角色、地点、关系、设定是核心锚点
|
||||||
|
- 哪些内容允许 AI 自由发挥,哪些只能在锚点附近变体
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 高自由度不等于所有内容都开放漂移
|
||||||
|
- 如果没有“锁定机制”,AI 会把创作者真正关心的内容稀释掉
|
||||||
|
|
||||||
|
## 3. 什么内容应该交给 AI 和系统
|
||||||
|
|
||||||
|
应该交给 AI 的,不是“重要内容”,而是“重要内容之外的大量展开、编译、补缝、校验与专业执行”。
|
||||||
|
|
||||||
|
## 3.1 批量生成的长尾内容
|
||||||
|
|
||||||
|
应该主要交给 AI:
|
||||||
|
|
||||||
|
- 普通 NPC
|
||||||
|
- 路人、商贩、巡逻者、村民、杂兵
|
||||||
|
- 次级场景
|
||||||
|
- 场景支线事件
|
||||||
|
- 大量普通物品
|
||||||
|
- 世界的长尾命名与描述
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些内容数量大、重复度高
|
||||||
|
- 它们需要“贴合世界”,但不需要都由创作者逐个手写
|
||||||
|
- AI 很适合做“围绕锚点的批量铺量”
|
||||||
|
|
||||||
|
## 3.2 从创作锚点到系统结构的编译
|
||||||
|
|
||||||
|
应该交给 AI / 系统:
|
||||||
|
|
||||||
|
- 从自然语言世界设定中提取题材词汇
|
||||||
|
- 从关键冲突中编译出世界叙事图谱
|
||||||
|
- 从关键角色卡编译出角色叙事档案
|
||||||
|
- 从创作者输入里自动生成标签、钩子、隐藏线索、章节摘要
|
||||||
|
- 从地点和关系中编译出场景连接、事件触发和叙事回响
|
||||||
|
|
||||||
|
对应当前仓库,下面这些结构更适合由 AI / 系统生成,而不是让玩家直接编辑:
|
||||||
|
|
||||||
|
- `ThemePack`
|
||||||
|
- `WorldStoryGraph`
|
||||||
|
- `ActorNarrativeProfile`
|
||||||
|
- `KnowledgeFact`
|
||||||
|
- `VisibilitySlice`
|
||||||
|
- `SceneNarrativeDirective`
|
||||||
|
- `CarrierStoryFingerprint`
|
||||||
|
- `ThreadContract`
|
||||||
|
- `StorySignal`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些是运行时结构,不是创作者真正想表达的作品内容
|
||||||
|
- 直接暴露给玩家,会把创作过程变成专业数据填表
|
||||||
|
|
||||||
|
## 3.3 专业化、规则化的任务
|
||||||
|
|
||||||
|
应该交给 AI / 系统:
|
||||||
|
|
||||||
|
- 数值平衡
|
||||||
|
- 标签归纳
|
||||||
|
- 稀有度预算
|
||||||
|
- 初始技能与初始物品的批量配置
|
||||||
|
- build 方向匹配
|
||||||
|
- 地图连接补全
|
||||||
|
- 触发条件与推进信号编译
|
||||||
|
- 背景章节拆分与 teaser 生成
|
||||||
|
- 运行时物件命名与叙事描述的变体生成
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些工作要么重复、要么专业、要么容易做脏活累活
|
||||||
|
- 让非专业创作者处理,会显著提高门槛,却不一定显著提高质量
|
||||||
|
|
||||||
|
## 3.4 一致性、纠错与查漏补缺
|
||||||
|
|
||||||
|
应该交给 AI / 系统持续处理:
|
||||||
|
|
||||||
|
- 世界设定冲突检查
|
||||||
|
- 角色关系矛盾检查
|
||||||
|
- 同名 / 重复 / 设定撞车检查
|
||||||
|
- 信息越权泄露检查
|
||||||
|
- prompt 裁剪
|
||||||
|
- 风格一致性检查
|
||||||
|
- “这个角色/地点/物件是否真的和世界主线有关”的弱关联检查
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这是 AI 比人更适合做的“维护型工作”
|
||||||
|
- 它属于创作支持,不属于创作者必须亲手完成的创作
|
||||||
|
|
||||||
|
## 4. 最合理的边界不是二分法,而是三层分工
|
||||||
|
|
||||||
|
自定义世界最合理的结构,不是“玩家写”与“AI 写”的简单二选一,而是三层。
|
||||||
|
|
||||||
|
## 4.1 第一层:创作者必控层
|
||||||
|
|
||||||
|
这一层必须给创作者高自由度,且能被锁定:
|
||||||
|
|
||||||
|
- 世界核心命题
|
||||||
|
- 主题与气质
|
||||||
|
- 玩家身份与开局
|
||||||
|
- 核心冲突
|
||||||
|
- 关键势力
|
||||||
|
- 关键角色
|
||||||
|
- 关键地点
|
||||||
|
- 标志性物件 / 怪物 / 规则
|
||||||
|
- 禁止事项
|
||||||
|
|
||||||
|
这层的原则是:
|
||||||
|
|
||||||
|
**少而重。**
|
||||||
|
|
||||||
|
## 4.2 第二层:创作者可选强化层
|
||||||
|
|
||||||
|
这一层不应强制填写,但应该允许创作者继续深挖:
|
||||||
|
|
||||||
|
- 明线 / 暗线种子
|
||||||
|
- 角色之间的旧事
|
||||||
|
- 地点背后的旧伤
|
||||||
|
- 标志性物件的来历
|
||||||
|
- 关键角色的口头习惯、禁忌、执念
|
||||||
|
- 关键地点的视觉母题与情绪目标
|
||||||
|
|
||||||
|
这层的原则是:
|
||||||
|
|
||||||
|
**愿意细写的人可以拉高作品上限,不愿细写的人也不会被门槛卡住。**
|
||||||
|
|
||||||
|
## 4.3 第三层:AI 自动展开层
|
||||||
|
|
||||||
|
这一层默认交给 AI / 系统:
|
||||||
|
|
||||||
|
- 长尾 NPC
|
||||||
|
- 次级地点
|
||||||
|
- 章节拆分
|
||||||
|
- 初始技能
|
||||||
|
- 初始物品
|
||||||
|
- 标签与属性映射
|
||||||
|
- 任务 contract
|
||||||
|
- 物件叙事指纹
|
||||||
|
- 可见性裁剪
|
||||||
|
- 运行时导演指令
|
||||||
|
- 批量命名与文案变体
|
||||||
|
|
||||||
|
这层的原则是:
|
||||||
|
|
||||||
|
**AI 可以做多、做快、做杂,但不能越过第一层锁定内容。**
|
||||||
|
|
||||||
|
## 5. 具体模块的建议归属
|
||||||
|
|
||||||
|
| 模块 | 建议归属 | 创作者应控制什么 | AI / 系统应负责什么 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 世界一句话设定、核心幻想、核心卖点 | 创作者直接控制 | 直接写、直接改、可锁定 | 给出备选表述和扩展方向 |
|
||||||
|
| 主题、基调、审美、禁忌 | 创作者直接控制 | 选择 / 改写 / 锁定 | 生成风格词、避雷词、提示词约束 |
|
||||||
|
| 玩家身份、开局处境、玩家目标 | 创作者直接控制 | 直接定义 | 补足开局钩子和初始叙事包装 |
|
||||||
|
| 关键势力与核心冲突 | 创作者主控,AI 辅助 | 定义核心关系和立场 | 扩展冲突支路、生成世界线程 |
|
||||||
|
| 关键角色 | 创作者主控,AI 辅助 | 定义角色骨架、关系张力、秘密方向 | 生成长背景、章节拆分、技能、物品、叙事档案 |
|
||||||
|
| 关键地点 | 创作者主控,AI 辅助 | 定义地点意义、气氛、秘密 | 扩展场景细节、连接关系、遭遇分布 |
|
||||||
|
| 标志性物件 / 怪物 / 制度 / 规则 | 创作者主控,AI 辅助 | 定义代表性要素与硬边界 | 扩展变体、命名、说明、运行时挂钩 |
|
||||||
|
| 普通 NPC / 路人 / 杂兵 / 次级地点 | 主要交给 AI | 仅在需要时抽查或替换 | 批量生成与风格保持 |
|
||||||
|
| 角色长背景、章节 teaser、context snippet | 主要交给 AI | 创作者只改关键角色即可 | 自动拆章、压缩、解锁节奏整理 |
|
||||||
|
| 技能、初始物品、标签、构筑倾向 | 主要交给 AI / 系统 | 提供偏好或少量 override | 按角色和世界规则自动编译 |
|
||||||
|
| 世界图谱、知识事实、可见性、导演指令 | AI / 系统内部层 | 不应默认暴露给玩家 | 运行时编译与维护 |
|
||||||
|
| 一致性检查、冲突检查、越权检查 | AI / 系统内部层 | 查看报告、决定是否采纳修改 | 自动扫描并提出修正建议 |
|
||||||
|
|
||||||
|
## 6. 不应该要求玩家直接填写的字段
|
||||||
|
|
||||||
|
为了真正做到低门槛,下面这些内容不应直接以“专业字段”形式强迫玩家填写。
|
||||||
|
|
||||||
|
## 6.1 不应该要求玩家手填原始数值
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `initialAffinity`
|
||||||
|
- 精确数值型 build 倾向
|
||||||
|
- 复杂掉落预算
|
||||||
|
|
||||||
|
更合理的做法是让创作者填写直觉表达,例如:
|
||||||
|
|
||||||
|
- `初见就戒备`
|
||||||
|
- `容易合作`
|
||||||
|
- `这里非常危险`
|
||||||
|
- `偏爆发型`
|
||||||
|
|
||||||
|
再由系统编译成运行时数值。
|
||||||
|
|
||||||
|
## 6.2 不应该要求玩家手填技术型结构
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `tags`
|
||||||
|
- `attributeSchema`
|
||||||
|
- `ThemePack`
|
||||||
|
- `WorldStoryGraph`
|
||||||
|
- `VisibilitySlice`
|
||||||
|
- `SceneNarrativeDirective`
|
||||||
|
- `ThreadContract`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些字段属于系统运行结构,不属于创作者自然的创作语言
|
||||||
|
- 直接让玩家填,会把工具变成只有懂系统的人才能用
|
||||||
|
|
||||||
|
## 6.3 不应该要求玩家逐个补完所有人物设定字段
|
||||||
|
|
||||||
|
当前 `CustomWorldRoleProfile` 里这些字段:
|
||||||
|
|
||||||
|
- `backstory`
|
||||||
|
- `personality`
|
||||||
|
- `motivation`
|
||||||
|
- `combatStyle`
|
||||||
|
- `backstoryReveal`
|
||||||
|
- `skills`
|
||||||
|
- `initialItems`
|
||||||
|
|
||||||
|
更适合的做法不是全部让玩家手写,而是先让玩家填写更自然的“角色卡”:
|
||||||
|
|
||||||
|
- 这个人表面上是什么样
|
||||||
|
- 这个人真正想要什么
|
||||||
|
- 这个人最不想被提到什么
|
||||||
|
- 这个人和玩家之间最可能形成什么关系
|
||||||
|
- 这个人和哪个地点 / 物件 / 旧事绑得最紧
|
||||||
|
|
||||||
|
再由 AI / 系统编译成当前结构。
|
||||||
|
|
||||||
|
## 7. 推荐的创作输入形态
|
||||||
|
|
||||||
|
要让非专业创作者也能高自由度创作,输入形态必须改成“自然语言创作卡”,而不是“系统字段表单”。
|
||||||
|
|
||||||
|
## 7.1 世界层卡片
|
||||||
|
|
||||||
|
建议至少有这些卡片:
|
||||||
|
|
||||||
|
1. 世界一句话
|
||||||
|
2. 主题与气质
|
||||||
|
3. 玩家是谁
|
||||||
|
4. 核心冲突
|
||||||
|
5. 关键势力
|
||||||
|
6. 关键角色
|
||||||
|
7. 关键地点
|
||||||
|
8. 标志性要素
|
||||||
|
9. 禁止事项
|
||||||
|
|
||||||
|
## 7.2 每张卡片都允许 3 种输入方式
|
||||||
|
|
||||||
|
1. 一句话自由输入
|
||||||
|
- 适合低门槛创作者
|
||||||
|
|
||||||
|
2. 标签 / 选项 / 语气滑条
|
||||||
|
- 适合不想写太多字的创作者
|
||||||
|
|
||||||
|
3. 高级补充
|
||||||
|
- 适合愿意继续深挖的人
|
||||||
|
|
||||||
|
这样才能做到:
|
||||||
|
|
||||||
|
- 不会逼着用户写长文
|
||||||
|
- 但也不会限制愿意创作的人继续把世界做深
|
||||||
|
|
||||||
|
## 7.3 必须支持“锁定”与“局部重生成”
|
||||||
|
|
||||||
|
这是高创作自由度里非常关键的一点。
|
||||||
|
|
||||||
|
创作者应当能:
|
||||||
|
|
||||||
|
- 锁定一个角色
|
||||||
|
- 锁定一个地点
|
||||||
|
- 锁定一条冲突
|
||||||
|
- 只重生成未锁定部分
|
||||||
|
- 围绕锁定内容重写其余世界
|
||||||
|
|
||||||
|
否则创作者每次调用 AI,都会有“好不容易想好的东西被洗掉”的感受。
|
||||||
|
|
||||||
|
## 8. 面向当前仓库的结构映射建议
|
||||||
|
|
||||||
|
为了便于后续落实现有系统,这份边界建议可以直接映射到当前结构:
|
||||||
|
|
||||||
|
## 8.1 创作者输入层
|
||||||
|
|
||||||
|
建议主要映射到:
|
||||||
|
|
||||||
|
- `CustomWorldProfile.settingText`
|
||||||
|
- `CustomWorldProfile.name`
|
||||||
|
- `CustomWorldProfile.subtitle`
|
||||||
|
- `CustomWorldProfile.summary`
|
||||||
|
- `CustomWorldProfile.tone`
|
||||||
|
- `CustomWorldProfile.playerGoal`
|
||||||
|
- `CustomWorldProfile.majorFactions`
|
||||||
|
- `CustomWorldProfile.coreConflicts`
|
||||||
|
|
||||||
|
以及关键角色、关键地点的创作卡输入。
|
||||||
|
|
||||||
|
## 8.2 AI 编译层
|
||||||
|
|
||||||
|
由 AI / 系统从创作者输入自动补出:
|
||||||
|
|
||||||
|
- `themePack`
|
||||||
|
- `storyGraph`
|
||||||
|
- `knowledgeFacts`
|
||||||
|
- `threadContracts`
|
||||||
|
- 每个关键角色的 `narrativeProfile`
|
||||||
|
- 每个角色的 `backstoryReveal`
|
||||||
|
- 每个角色的 `skills`
|
||||||
|
- 每个角色的 `initialItems`
|
||||||
|
|
||||||
|
## 8.3 运行时支持层
|
||||||
|
|
||||||
|
运行时继续由 AI / 系统维护:
|
||||||
|
|
||||||
|
- `VisibilitySlice`
|
||||||
|
- `SceneNarrativeDirective`
|
||||||
|
- `CarrierStoryFingerprint`
|
||||||
|
- `StorySignal`
|
||||||
|
|
||||||
|
这些内容应该是“系统如何把世界跑起来”,不是“创作者必须亲手写完的创作内容”。
|
||||||
|
|
||||||
|
## 9. 产品层面的最终结论
|
||||||
|
|
||||||
|
如果我们的目标真的是“低创作门槛、高创作自由度”,那么自定义世界不应该做成一个要求用户:
|
||||||
|
|
||||||
|
- 填很多字段
|
||||||
|
- 写很多长文
|
||||||
|
- 理解很多系统结构
|
||||||
|
- 自己负责平衡、命名、拆章节、补标签、补长尾内容
|
||||||
|
|
||||||
|
的专业编辑器。
|
||||||
|
|
||||||
|
它应该做成这样:
|
||||||
|
|
||||||
|
1. 创作者决定世界的灵魂锚点。
|
||||||
|
2. 创作者重点塑造少量关键人、关键地、关键冲突、关键物。
|
||||||
|
3. AI 围绕这些锚点批量展开长尾内容。
|
||||||
|
4. 系统把这些内容编译成可运行的图谱、可见性、任务、物件和关系结构。
|
||||||
|
5. 创作者随时可以锁定核心创意,并局部重生成其余部分。
|
||||||
|
|
||||||
|
一句话收束:
|
||||||
|
|
||||||
|
**创作者应该写“这个世界为什么动人”,AI 应该负责“让这个世界长出来并跑起来”。**
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
# 自定义世界创作中手填、AI 可改与系统托管的平衡设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-12`
|
||||||
|
|
||||||
|
## 0. 文档目标
|
||||||
|
|
||||||
|
这份文档用于回答一个更具体的问题:
|
||||||
|
|
||||||
|
**参考 RPG 专业剧情策划全流程后,在自定义世界创作工具里,哪些设定必须要求创作者手动填写,哪些设定应该由 AI 先生成但允许创作者修改,哪些设定应完全交给系统托管,才能在“尽可能降低门槛”和“尽可能提高作品质量”之间取一个平衡。**
|
||||||
|
|
||||||
|
这份文档不再只回答“创作者与 AI 怎么分工”,而是进一步把创作工作台收束成一个更可执行的三层输入结构:
|
||||||
|
|
||||||
|
1. 创作者必须手填的高杠杆锚点
|
||||||
|
2. AI 先生成、创作者可修改的内容草稿层
|
||||||
|
3. 系统自动编译和运行的托管层
|
||||||
|
|
||||||
|
一句话结论:
|
||||||
|
|
||||||
|
**让创作者只负责决定作品的灵魂、视角、冲突和关系钩子,让 AI 负责把这些锚点展开成可编辑的剧情草稿,让系统负责把草稿编译成可运行的结构。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 设计目标
|
||||||
|
|
||||||
|
这套平衡设计要同时满足 5 个目标:
|
||||||
|
|
||||||
|
1. 低门槛
|
||||||
|
- 新创作者不需要写长篇设定,也不需要理解底层系统结构。
|
||||||
|
|
||||||
|
2. 高辨识度
|
||||||
|
- 创作者写出来的世界,不应该只是“像一个世界”,而应该保留明显的个人方向。
|
||||||
|
|
||||||
|
3. 高可编辑性
|
||||||
|
- AI 不能一次生成后就不可控,创作者必须能改关键对象、关键关系和关键章节。
|
||||||
|
|
||||||
|
4. 高稳定性
|
||||||
|
- 任务、章节、关系、物件和可见性等运行层结构不能依赖创作者手填专业字段。
|
||||||
|
|
||||||
|
5. 可扩展
|
||||||
|
- 愿意深挖的创作者可以继续补充世界上限,不愿深挖的人也能快速产出质量不错的作品。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心原则
|
||||||
|
|
||||||
|
## 2.1 创作者手填的必须是“高杠杆决策”,不是“高工作量字段”
|
||||||
|
|
||||||
|
应该要求创作者手填的内容,必须同时满足下面两个条件:
|
||||||
|
|
||||||
|
1. 会显著决定作品气质和辨识度
|
||||||
|
2. AI 很难替代判断
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- 世界一句话
|
||||||
|
- 玩家身份
|
||||||
|
- 核心冲突
|
||||||
|
- 关系钩子
|
||||||
|
- 禁忌边界
|
||||||
|
|
||||||
|
而不应该强制手填:
|
||||||
|
|
||||||
|
- 全量 NPC
|
||||||
|
- 全量场景
|
||||||
|
- 技能列表
|
||||||
|
- 初始物品
|
||||||
|
- 章节拆分
|
||||||
|
- 运行时信号结构
|
||||||
|
|
||||||
|
## 2.2 创作者可改层应该承接“专业策划初稿”,而不是“原始底层字段”
|
||||||
|
|
||||||
|
AI 生成后允许创作者修改的,不应该是一堆技术型字段,而应该是一批已经成形的内容卡片,例如:
|
||||||
|
|
||||||
|
- 关键角色卡
|
||||||
|
- 势力卡
|
||||||
|
- 关键地点卡
|
||||||
|
- 主线章节卡
|
||||||
|
- 支线种子卡
|
||||||
|
- 场景章节卡
|
||||||
|
- 标志性物件卡
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**AI 先给创作者一个像策划初稿的东西,而不是给一堆系统字段让创作者自己拼。**
|
||||||
|
|
||||||
|
## 2.3 系统托管层必须彻底隐藏专业运行结构
|
||||||
|
|
||||||
|
以下这类结构不应该默认要求创作者理解或编辑:
|
||||||
|
|
||||||
|
- `ThemePack`
|
||||||
|
- `WorldStoryGraph`
|
||||||
|
- `KnowledgeFact`
|
||||||
|
- `VisibilitySlice`
|
||||||
|
- `SceneNarrativeDirective`
|
||||||
|
- `StorySignal`
|
||||||
|
- `ThreadContract`
|
||||||
|
- 数值预算
|
||||||
|
- 稀有度映射
|
||||||
|
- 掉落和 build 权重
|
||||||
|
|
||||||
|
创作者应该编辑的是自然语言与内容卡,而不是运行时图结构。
|
||||||
|
|
||||||
|
## 2.4 先少量必填,再逐层展开
|
||||||
|
|
||||||
|
最合理的工作流不是“开局填一大页表”,而是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
先填最小必填卡
|
||||||
|
-> AI 生成世界初稿
|
||||||
|
-> 创作者修改关键对象
|
||||||
|
-> 系统继续展开长尾
|
||||||
|
-> 创作者决定是否进入高级补充
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.5 默认清爽,深度能力后置
|
||||||
|
|
||||||
|
结合当前项目约束,创作工作台默认不要把规则说明、底层字段、专业术语堆到 UI 面板里。
|
||||||
|
|
||||||
|
应该做到:
|
||||||
|
|
||||||
|
1. 默认只展示最有创作价值的卡片
|
||||||
|
2. 高级内容折叠到后置面板
|
||||||
|
3. 大多数系统结构不直接暴露
|
||||||
|
4. 移动端也能完成最小创作闭环
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 最终建议:三层分工
|
||||||
|
|
||||||
|
## 3.1 第一层:必须要求创作者手动填写
|
||||||
|
|
||||||
|
这一层只保留最影响作品质量的高杠杆锚点,建议默认强制填写 6 张卡。
|
||||||
|
|
||||||
|
## 3.2 第二层:AI 生成后支持创作者修改
|
||||||
|
|
||||||
|
这一层由 AI 根据第一层锚点自动展开成专业剧情策划初稿,创作者可以逐项修改、锁定、局部重生成。
|
||||||
|
|
||||||
|
## 3.3 第三层:其余都交给系统
|
||||||
|
|
||||||
|
这一层是把前两层编译成可运行游戏结构所需的系统字段、数值和运行时指令,默认不要求创作者处理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 最低门槛方案:只强制手填 6 张卡
|
||||||
|
|
||||||
|
如果目标是尽可能降低门槛,同时又保留作品辨识度,建议只强制创作者填写以下 6 张卡。
|
||||||
|
|
||||||
|
## 4.1 卡 1:世界一句话与核心幻想
|
||||||
|
|
||||||
|
创作者必须手填:
|
||||||
|
|
||||||
|
- 世界一句话设定
|
||||||
|
- 玩家来到这个世界最想体验的感觉
|
||||||
|
- 这个世界和同类题材相比最不同的一点
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这是作品的方向盘
|
||||||
|
- 这是后续 AI 所有扩写的总锚点
|
||||||
|
|
||||||
|
推荐输入形态:
|
||||||
|
|
||||||
|
- 一句话文本
|
||||||
|
- `1~3` 个体验关键词
|
||||||
|
|
||||||
|
## 4.2 卡 2:玩家身份与开局困境
|
||||||
|
|
||||||
|
创作者必须手填:
|
||||||
|
|
||||||
|
- 玩家是谁
|
||||||
|
- 玩家开局最缺什么
|
||||||
|
- 玩家为什么必须进入这场故事
|
||||||
|
- 玩家天然站在什么位置上
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 玩家视角不清,后面所有剧情都会发散
|
||||||
|
- 这是主线入口、关系入口和任务入口的共同基础
|
||||||
|
|
||||||
|
## 4.3 卡 3:主题气质与禁忌边界
|
||||||
|
|
||||||
|
创作者必须手填:
|
||||||
|
|
||||||
|
- 主题关键词
|
||||||
|
- 情绪基调
|
||||||
|
- 审美方向
|
||||||
|
- 禁止出现或尽量避免的内容
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这决定世界“是什么味道”
|
||||||
|
- 这也是避免 AI 跑偏最有效的一层
|
||||||
|
|
||||||
|
推荐输入形态:
|
||||||
|
|
||||||
|
- 标签选择
|
||||||
|
- 语气滑条
|
||||||
|
- 一小段补充说明
|
||||||
|
|
||||||
|
## 4.4 卡 4:核心冲突
|
||||||
|
|
||||||
|
创作者必须手填:
|
||||||
|
|
||||||
|
- 当前世界最重要的 `1~3` 个明面冲突
|
||||||
|
- 至少 `1` 个隐藏问题或暗面危机
|
||||||
|
- 玩家最先接触的是哪条冲突
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 没有冲突,世界就只剩设定
|
||||||
|
- 没有暗面问题,后续剧情就难以产生层次和改判
|
||||||
|
|
||||||
|
## 4.5 卡 5:关键关系钩子
|
||||||
|
|
||||||
|
这里不强制创作者一开始填写完整角色档案,只要求填写更高杠杆的“关系骨架”。
|
||||||
|
|
||||||
|
创作者必须手填:
|
||||||
|
|
||||||
|
- `2~4` 条关键关系钩子
|
||||||
|
- 每条钩子至少说明:
|
||||||
|
- 谁和谁有关
|
||||||
|
- 关系是债、仇、误解、旧情、利用还是血缘
|
||||||
|
- 这条关系里压着什么秘密或代价
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 作品的人味和记忆点主要来自关系张力
|
||||||
|
- 关系钩子比完整角色长文更容易写,也更高杠杆
|
||||||
|
|
||||||
|
## 4.6 卡 6:标志性要素与硬规则
|
||||||
|
|
||||||
|
创作者必须手填:
|
||||||
|
|
||||||
|
- `2~5` 个标志性要素
|
||||||
|
- 物件
|
||||||
|
- 怪物
|
||||||
|
- 制度
|
||||||
|
- 仪式
|
||||||
|
- 能力体系
|
||||||
|
- 社会规则
|
||||||
|
- 至少 `1~3` 条不能被 AI 擅自改写的硬规则
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这决定世界是否有独特手感
|
||||||
|
- 后续命名、剧情、物件和场景都会反复依赖这些母题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 不建议强制手填,但应该让 AI 生成后支持创作者修改的设定
|
||||||
|
|
||||||
|
这一层是平衡“低门槛”和“高质量”的关键。
|
||||||
|
|
||||||
|
创作者不需要从零填写这些内容,但 AI 生成后必须能看、能改、能锁定、能局部重生成。
|
||||||
|
|
||||||
|
## 5.1 世界外观层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 世界名称
|
||||||
|
- 副标题
|
||||||
|
- 世界简介
|
||||||
|
- 宣传短句
|
||||||
|
- 主题母题摘要
|
||||||
|
- 命名风格建议
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些内容影响观感,但不值得强制占用开局填写成本
|
||||||
|
|
||||||
|
## 5.2 势力层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- `2~6` 个关键势力
|
||||||
|
- 每个势力的公开目标
|
||||||
|
- 每个势力的隐藏目标
|
||||||
|
- 势力间的主要矛盾
|
||||||
|
- 代表人物
|
||||||
|
- 势力资源与禁忌
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 势力很重要,但让新手一开始手写完整势力表太重
|
||||||
|
- 更合理的做法是让 AI 基于核心冲突先出草稿,再由创作者修正
|
||||||
|
|
||||||
|
## 5.3 关键角色层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 关键角色姓名
|
||||||
|
- 外显身份
|
||||||
|
- 公众面具
|
||||||
|
- 当前压力
|
||||||
|
- 表面目标
|
||||||
|
- 真实目标
|
||||||
|
- 背景旧事
|
||||||
|
- 禁区
|
||||||
|
- 与玩家关系方向
|
||||||
|
- 角色个人线阶段
|
||||||
|
- 背景章节 teaser
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 创作者已经通过“关系钩子”给出最关键的人物骨架
|
||||||
|
- AI 负责把钩子展开成可编辑角色卡,创作者再做精修
|
||||||
|
|
||||||
|
## 5.4 关键地点层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- `4~10` 个关键地点
|
||||||
|
- 每个地点的功能定位
|
||||||
|
- 气氛和视觉母题
|
||||||
|
- 涉及的线程和秘密
|
||||||
|
- 首次进入时的情绪目标
|
||||||
|
- 关联角色和标志性载体
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 地点是世界感的重要来源
|
||||||
|
- 但新创作者未必能一开始就写出完整地点网络
|
||||||
|
|
||||||
|
## 5.5 世界线程层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 明线线程
|
||||||
|
- 暗线线程
|
||||||
|
- 旧事伤痕
|
||||||
|
- 误导信息
|
||||||
|
- 主要 handoff
|
||||||
|
- 阶段揭示节奏
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 线程是专业剧情结构,适合 AI 先搭骨架
|
||||||
|
- 但创作者必须有权修正哪条线更重要、哪条线该隐藏
|
||||||
|
|
||||||
|
## 5.6 主线章节层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 幕结构建议
|
||||||
|
- 章节标题
|
||||||
|
- 章节承诺
|
||||||
|
- 转折设计
|
||||||
|
- 高潮行动
|
||||||
|
- 章节 handoff
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 创作者已经给出了世界目标、冲突和关系
|
||||||
|
- AI 可以先把它们编成主线章节初稿
|
||||||
|
- 创作者再选择保留、删减或重排
|
||||||
|
|
||||||
|
## 5.7 支线、角色线、阵营线层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 支线种子
|
||||||
|
- 角色线阶段事件
|
||||||
|
- 阵营线分歧点
|
||||||
|
- 私聊或同伴互动节点
|
||||||
|
- 支线和主线的互文关系
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这是最适合 AI 拉开内容宽度的部分
|
||||||
|
- 也是最需要创作者局部精修的部分
|
||||||
|
|
||||||
|
## 5.8 场景章节层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 场景章节标题
|
||||||
|
- `opening / expansion / turning_point / climax / aftermath`
|
||||||
|
- 情感锚点 NPC
|
||||||
|
- 现场压力
|
||||||
|
- 转折信息
|
||||||
|
- 局部收束
|
||||||
|
- 下一跳 handoff
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 当前项目已经在走“场景 = 章节单元”的方向
|
||||||
|
- 这层非常适合 AI 编排出第一版,再由创作者补强记忆点
|
||||||
|
|
||||||
|
## 5.9 叙事载体层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 标志性物件
|
||||||
|
- 文书
|
||||||
|
- 残痕
|
||||||
|
- 证物
|
||||||
|
- 场景遗物
|
||||||
|
- 怪物命名及其故事指向
|
||||||
|
|
||||||
|
创作者主要修改:
|
||||||
|
|
||||||
|
- 哪些载体最重要
|
||||||
|
- 哪些载体和哪条线程绑定
|
||||||
|
- 哪些载体需要更强的个人风格
|
||||||
|
|
||||||
|
## 5.10 文案包装层
|
||||||
|
|
||||||
|
建议 AI 先生成后可改:
|
||||||
|
|
||||||
|
- 角色简介
|
||||||
|
- 地点短描述
|
||||||
|
- 势力介绍
|
||||||
|
- 章节标题候选
|
||||||
|
- 任务标题与简述
|
||||||
|
- 物件命名候选
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这些内容适合 AI 批量铺量
|
||||||
|
- 创作者只需要挑、改、锁定,不必从零起草
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 其余设定应交给系统托管
|
||||||
|
|
||||||
|
以下内容不建议默认暴露给创作者编辑,应由系统根据前两层自动编译和维护。
|
||||||
|
|
||||||
|
## 6.1 题材与术语编译层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- `ThemePack`
|
||||||
|
- 题材词汇表
|
||||||
|
- 命名模式映射
|
||||||
|
- 母题标签映射
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这是系统为了统一生成风格而维护的内部层
|
||||||
|
|
||||||
|
## 6.2 世界图谱运行层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- `WorldStoryGraph`
|
||||||
|
- `KnowledgeFact`
|
||||||
|
- 事实 id
|
||||||
|
- 线程内部关联
|
||||||
|
- 旧事与角色的细粒度映射
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 创作者要的是“故事线能对”,不是维护图数据库
|
||||||
|
|
||||||
|
## 6.3 可见性和 prompt 裁剪层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- `VisibilitySlice`
|
||||||
|
- 禁止注入信息列表
|
||||||
|
- 当前可说信息
|
||||||
|
- 推测信息
|
||||||
|
- 越权泄露检查
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这层必须稳定、严格、自动化
|
||||||
|
- 不适合依赖创作者手动维护
|
||||||
|
|
||||||
|
## 6.4 运行时导演层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- `SceneNarrativeDirective`
|
||||||
|
- 节奏推进指令
|
||||||
|
- 披露预算
|
||||||
|
- 当前主压力判断
|
||||||
|
- 当前前景角色和前景载体
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这是剧情运行时调度逻辑,不是创作表达层
|
||||||
|
|
||||||
|
## 6.5 任务编译层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- `ThreadContract`
|
||||||
|
- `StorySignal`
|
||||||
|
- step id
|
||||||
|
- step 类型映射
|
||||||
|
- 触发条件编译
|
||||||
|
- 结算条件编译
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 创作者可以编辑“任务卡”和“章节卡”
|
||||||
|
- 但不应默认编辑底层 contract 结构
|
||||||
|
|
||||||
|
## 6.6 数值与配置层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- 技能数值
|
||||||
|
- 初始物品预算
|
||||||
|
- 稀有度分布
|
||||||
|
- 掉落权重
|
||||||
|
- build 标签映射
|
||||||
|
- 关系数值初始值
|
||||||
|
- 敌对强度预算
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 创作者可以给“偏向”
|
||||||
|
- 系统负责编译成具体数值
|
||||||
|
|
||||||
|
## 6.7 QA 与一致性层
|
||||||
|
|
||||||
|
交给系统:
|
||||||
|
|
||||||
|
- 设定冲突检查
|
||||||
|
- 同名检查
|
||||||
|
- 风格漂移检查
|
||||||
|
- 关系矛盾检查
|
||||||
|
- 主线与支线弱关联检查
|
||||||
|
- 未解锁信息泄露检查
|
||||||
|
- 长尾内容覆盖率检查
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 这属于高频维护型工作,最适合系统自动做
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 按模块划分的最终边界表
|
||||||
|
|
||||||
|
| 模块 | 必须手填 | AI 生成后可改 | 系统托管 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 世界定位 | 世界一句话、核心幻想、差异点 | 世界名称、副标题、简介 | 题材词汇编译 |
|
||||||
|
| 玩家视角 | 玩家身份、开局困境、初始动机 | 开局剧情摘要、开局目标文案 | 开局状态初始化 |
|
||||||
|
| 主题边界 | 主题、气质、禁忌、硬边界 | 主题母题摘要、风格建议 | 风格约束编译 |
|
||||||
|
| 核心冲突 | 明面冲突、隐藏危机 | 线程草稿、旧事伤痕、误导设计 | 世界图谱、事实映射 |
|
||||||
|
| 关系骨架 | 关键关系钩子 | 关键角色卡、个人线阶段、背景章节 teaser | 关系数值、解锁条件 |
|
||||||
|
| 标志性要素 | 标志物、怪物、制度、规则 | 标志载体卡、命名候选、衍生变体 | 物件指纹、掉落映射 |
|
||||||
|
| 势力 | 不强制首轮手填 | 势力卡、代表人物、势力冲突 | 阵营状态映射 |
|
||||||
|
| 地点 | 不强制首轮手填 | 关键地点卡、场景网络、氛围描述 | 场景连接编译 |
|
||||||
|
| 主线 | 不强制首轮手写完整主线 | 幕结构、章节卡、高潮与 handoff | 章节状态编译 |
|
||||||
|
| 支线/角色线 | 不强制首轮手写完整矩阵 | 支线种子、角色线事件、阵营线分歧 | 任务 contract 编译 |
|
||||||
|
| 场景章节 | 不强制首轮手写全量章节 | 场景章节卡、阶段内容、章节载体 | signal 与导演层 |
|
||||||
|
| 运行时结构 | 不建议创作者接触 | 不建议默认编辑 | 可见性、导演、信号、编译、QA |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 推荐创作流程
|
||||||
|
|
||||||
|
## 8.1 第一步:只填写最小必填集
|
||||||
|
|
||||||
|
创作者只需要完成:
|
||||||
|
|
||||||
|
1. 世界一句话与核心幻想
|
||||||
|
2. 玩家身份与开局困境
|
||||||
|
3. 主题气质与禁忌边界
|
||||||
|
4. 核心冲突
|
||||||
|
5. 关键关系钩子
|
||||||
|
6. 标志性要素与硬规则
|
||||||
|
|
||||||
|
这一步应控制在:
|
||||||
|
|
||||||
|
- `5~15` 分钟
|
||||||
|
- `200~800` 字
|
||||||
|
- 或更少文字配合标签选择
|
||||||
|
|
||||||
|
## 8.2 第二步:AI 生成“策划初稿包”
|
||||||
|
|
||||||
|
系统根据最小输入,生成一份结构化初稿包,建议至少包括:
|
||||||
|
|
||||||
|
1. 世界标题与摘要
|
||||||
|
2. `3~5` 个关键角色卡
|
||||||
|
3. `2~4` 个势力卡
|
||||||
|
4. `4~8` 个关键地点卡
|
||||||
|
5. `3~5` 条世界线程
|
||||||
|
6. `3~6` 个场景章节卡
|
||||||
|
7. 一批支线种子和标志性载体
|
||||||
|
|
||||||
|
这里的重点不是一次补满全世界,而是先形成一个像样的内容骨架。
|
||||||
|
|
||||||
|
## 8.3 第三步:创作者只精修高价值卡片
|
||||||
|
|
||||||
|
建议默认优先让创作者编辑这 4 类卡片:
|
||||||
|
|
||||||
|
1. 关键角色
|
||||||
|
2. 核心冲突与线程
|
||||||
|
3. 关键地点
|
||||||
|
4. 主线第一幕或前几个场景章节
|
||||||
|
|
||||||
|
这样能以最低编辑成本,最大幅度提升作品质量。
|
||||||
|
|
||||||
|
## 8.4 第四步:系统继续展开长尾
|
||||||
|
|
||||||
|
在关键卡片被锁定后,再由系统补:
|
||||||
|
|
||||||
|
- 长尾 NPC
|
||||||
|
- 支持性地点
|
||||||
|
- 次级支线
|
||||||
|
- 普通物件
|
||||||
|
- 任务包装
|
||||||
|
- 文案变体
|
||||||
|
|
||||||
|
## 8.5 第五步:创作者按需进入高级模式
|
||||||
|
|
||||||
|
高级模式只对愿意深挖的人开放:
|
||||||
|
|
||||||
|
- 角色背景章节编辑
|
||||||
|
- 场景章节细化
|
||||||
|
- 支线矩阵补完
|
||||||
|
- 阵营线分歧补强
|
||||||
|
- 结局变量微调
|
||||||
|
|
||||||
|
这一步不是默认主流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 哪些内容应该支持“锁定 + 局部重生成”
|
||||||
|
|
||||||
|
为了既保证低门槛,又保证创作安全感,第二层内容必须支持锁定和局部重生成。
|
||||||
|
|
||||||
|
建议至少支持锁定这些对象:
|
||||||
|
|
||||||
|
1. 世界一句话与主题边界
|
||||||
|
2. 核心冲突
|
||||||
|
3. 关键角色
|
||||||
|
4. 关键地点
|
||||||
|
5. 势力卡
|
||||||
|
6. 主线章节卡
|
||||||
|
7. 场景章节卡
|
||||||
|
8. 标志性载体
|
||||||
|
|
||||||
|
建议至少支持这些局部重生成动作:
|
||||||
|
|
||||||
|
1. 仅重生成长尾角色
|
||||||
|
2. 仅重生成长尾地点
|
||||||
|
3. 仅重生成支线种子
|
||||||
|
4. 仅重生成物件与残痕
|
||||||
|
5. 仅重生成某个角色卡
|
||||||
|
6. 仅重生成某个场景章节
|
||||||
|
7. 围绕已锁定角色重做主线第一幕
|
||||||
|
|
||||||
|
原则是:
|
||||||
|
|
||||||
|
**越高价值的锚点越要支持锁定,越低价值的铺量内容越适合重生成。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 产品实现建议
|
||||||
|
|
||||||
|
## 10.1 默认 UI 只展示三段
|
||||||
|
|
||||||
|
建议工作台默认只展示:
|
||||||
|
|
||||||
|
1. 必填锚点
|
||||||
|
2. AI 初稿卡片
|
||||||
|
3. 高级模式入口
|
||||||
|
|
||||||
|
不要默认展示底层系统字段。
|
||||||
|
|
||||||
|
## 10.2 每张卡只保留自然语言输入
|
||||||
|
|
||||||
|
不要强迫创作者在首轮填写:
|
||||||
|
|
||||||
|
- tags
|
||||||
|
- ids
|
||||||
|
- 数值
|
||||||
|
- 稀有度
|
||||||
|
- 信号枚举
|
||||||
|
- step schema
|
||||||
|
|
||||||
|
更合理的做法是:
|
||||||
|
|
||||||
|
- 让创作者输入自然语言或选择直觉标签
|
||||||
|
- 再由系统编译成结构化字段
|
||||||
|
|
||||||
|
## 10.3 首轮生成后默认先看“精修建议”
|
||||||
|
|
||||||
|
AI 初稿生成后,不应该把创作者直接扔进一个大编辑器。
|
||||||
|
|
||||||
|
更好的做法是先给出:
|
||||||
|
|
||||||
|
1. 哪些卡片最值得改
|
||||||
|
2. 哪些内容已经比较稳定
|
||||||
|
3. 哪些内容仍然偏泛,需要创作者补个性
|
||||||
|
|
||||||
|
这样能明显提高创作者的修改效率。
|
||||||
|
|
||||||
|
## 10.4 移动端优先只保留高杠杆操作
|
||||||
|
|
||||||
|
移动端默认只应该支持:
|
||||||
|
|
||||||
|
1. 编辑必填卡
|
||||||
|
2. 浏览和修改关键角色卡
|
||||||
|
3. 浏览和修改关键地点卡
|
||||||
|
4. 锁定 / 重生成
|
||||||
|
5. 保存和继续创作
|
||||||
|
|
||||||
|
长表单和底层结构不要默认放在移动端主流程里。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 最后结论
|
||||||
|
|
||||||
|
如果目标是在自定义世界创作中真正平衡“降低门槛”和“提高作品质量”,最好的做法不是让创作者填更多字段,也不是把一切都交给 AI。
|
||||||
|
|
||||||
|
更合理的平衡是:
|
||||||
|
|
||||||
|
1. 创作者必须手填最小但高杠杆的 6 张卡,掌握世界灵魂。
|
||||||
|
2. AI 根据这 6 张卡生成一套可编辑的专业剧情初稿,负责把骨架展开成角色、地点、线程、章节和载体。
|
||||||
|
3. 创作者只精修最有价值的关键对象,锁定真正重要的内容。
|
||||||
|
4. 其余运行结构、数值、可见性、任务编译和 QA 检查都交给系统托管。
|
||||||
|
|
||||||
|
一句话收束:
|
||||||
|
|
||||||
|
**创作者负责决定“这个世界为什么值得被创作”,AI 负责把它整理成可修改的策划初稿,系统负责把它稳定地跑成一个游戏世界。**
|
||||||
@@ -0,0 +1,724 @@
|
|||||||
|
# 纯 Agent 式对话创作工具与结构化创作方案的对比评估及转型设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-12`
|
||||||
|
|
||||||
|
## 0. 文档目标
|
||||||
|
|
||||||
|
这份文档用于评估两种自定义世界创作形态:
|
||||||
|
|
||||||
|
1. 当前方向
|
||||||
|
- 基于“最小必填锚点 + AI 初稿卡片 + 系统托管层”的结构化创作方案
|
||||||
|
|
||||||
|
2. 纯 Agent 式方向
|
||||||
|
- 以前台对话为唯一主交互,创作者主要通过和 Agent 聊天来完成世界构建、角色塑造、剧情扩展和修改
|
||||||
|
|
||||||
|
文档需要回答 3 个问题:
|
||||||
|
|
||||||
|
1. 两种方案各自的优缺点是什么?
|
||||||
|
2. 对当前项目来说,纯 Agent 式是否更优?
|
||||||
|
3. 如果要转换成纯 Agent 式对话创作工具,应该怎么设计,才能不把质量和可控性一起丢掉?
|
||||||
|
|
||||||
|
一句话结论先说:
|
||||||
|
|
||||||
|
**纯 Agent 式对话创作工具更适合当“低门槛入口”和“陪创作过程”,但不适合把整个创作系统做成无结构、无锁定、无摘要、无编译层的纯聊天黑箱。**
|
||||||
|
|
||||||
|
更稳的方向不是“只有聊天”,而是:
|
||||||
|
|
||||||
|
**前台主交互纯 Agent,后台继续保留结构化世界模型、锁定机制、局部重生成、编译层和质量护栏。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 对比对象定义
|
||||||
|
|
||||||
|
## 1.1 当前结构化创作方案是什么
|
||||||
|
|
||||||
|
当前方案的核心是:
|
||||||
|
|
||||||
|
1. 创作者手填最小高杠杆锚点
|
||||||
|
2. AI 生成一批可编辑的剧情策划初稿卡片
|
||||||
|
3. 系统把内容编译成运行时结构
|
||||||
|
|
||||||
|
它本质上是:
|
||||||
|
|
||||||
|
**结构化工作台 + AI 协作生成。**
|
||||||
|
|
||||||
|
创作者的主要行为是:
|
||||||
|
|
||||||
|
1. 填写关键卡片
|
||||||
|
2. 修改关键角色、地点、势力、章节等内容卡
|
||||||
|
3. 锁定重要内容
|
||||||
|
4. 局部重生成次级内容
|
||||||
|
|
||||||
|
## 1.2 纯 Agent 式对话创作工具是什么
|
||||||
|
|
||||||
|
纯 Agent 式不是指“系统内部没有结构”,而是指:
|
||||||
|
|
||||||
|
**创作者前台几乎不需要面对表单和卡片编辑器,主要通过自然语言对话来完成创作。**
|
||||||
|
|
||||||
|
创作者的主要行为变成:
|
||||||
|
|
||||||
|
1. 用自然语言描述世界想法
|
||||||
|
2. 回答 Agent 的追问
|
||||||
|
3. 让 Agent 生成角色、地点、剧情和章节
|
||||||
|
4. 通过聊天指令要求修改、锁定、重做、总结和导出
|
||||||
|
|
||||||
|
它本质上是:
|
||||||
|
|
||||||
|
**对话式创作入口 + Agent 主导的协同编排。**
|
||||||
|
|
||||||
|
## 1.3 真正需要比较的不是“聊天 VS 表单”,而是“主交互模式 VS 后台结构”
|
||||||
|
|
||||||
|
很多产品会把问题误判成:
|
||||||
|
|
||||||
|
- 要么做聊天
|
||||||
|
- 要么做工作台
|
||||||
|
|
||||||
|
更准确的判断应该是:
|
||||||
|
|
||||||
|
1. 前台用户主要通过什么方式思考和输入?
|
||||||
|
2. 后台系统是否仍然有稳定的世界模型和编译层?
|
||||||
|
3. 创作者是否还能看见摘要、锁定内容和修改范围?
|
||||||
|
|
||||||
|
对当前项目来说,真正危险的不是“转成聊天”,而是:
|
||||||
|
|
||||||
|
**误把“纯 Agent 式”理解成“完全不需要结构化世界状态”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 总体结论
|
||||||
|
|
||||||
|
## 2.1 纯 Agent 式的主要优势
|
||||||
|
|
||||||
|
纯 Agent 式最大的价值,在于降低开局压力和创作焦虑。
|
||||||
|
|
||||||
|
它更擅长:
|
||||||
|
|
||||||
|
1. 帮不擅长表单和结构思考的创作者起步
|
||||||
|
2. 在创作者思路模糊时做追问和陪创作
|
||||||
|
3. 把“我要做一个世界”变成一次自然聊天
|
||||||
|
4. 动态决定追问深度,而不是一上来摆很多字段
|
||||||
|
5. 让创作者感觉自己是在和一个懂 RPG 的剧情搭档共创
|
||||||
|
|
||||||
|
## 2.2 纯 Agent 式的主要问题
|
||||||
|
|
||||||
|
纯 Agent 式最大的问题,不是能不能生成内容,而是:
|
||||||
|
|
||||||
|
**长项目一旦进入中后期,它会天然丢失可控性、可扫描性、可局部编辑性和可审计性。**
|
||||||
|
|
||||||
|
它最容易出现这些问题:
|
||||||
|
|
||||||
|
1. 聊天很多,但世界状态越来越难总览
|
||||||
|
2. 角色、地点、势力和章节信息散落在多轮消息里
|
||||||
|
3. 锁定范围不清,重生成容易误伤已有内容
|
||||||
|
4. Agent 很容易“替创作者决定太多”
|
||||||
|
5. 长会话越来越贵,越来越慢,也越来越容易漂移
|
||||||
|
|
||||||
|
## 2.3 对当前项目的判断
|
||||||
|
|
||||||
|
对当前项目而言:
|
||||||
|
|
||||||
|
1. 纯 Agent 式非常适合做创作入口
|
||||||
|
2. 纯 Agent 式也很适合做关键对象的精修与头脑风暴
|
||||||
|
3. 纯 Agent 式不适合作为唯一内容管理方式
|
||||||
|
|
||||||
|
因此更推荐的方向是:
|
||||||
|
|
||||||
|
**Agent-first,而不是 Agent-only。**
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
1. 前台以对话为主
|
||||||
|
2. 后台仍保留结构化世界状态
|
||||||
|
3. 关键内容仍然可被锁定、摘要、对比、局部重生成和导出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 纯 Agent 式对比当前方案的优缺点
|
||||||
|
|
||||||
|
## 3.1 对比表
|
||||||
|
|
||||||
|
| 维度 | 当前结构化方案 | 纯 Agent 式方案 | 更优者 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 首次上手门槛 | 比纯聊天高,需要理解少量卡片与阶段 | 最低,只需开口描述想法 | 纯 Agent |
|
||||||
|
| 创作陪伴感 | 中等,AI 更像工具 | 很强,Agent 更像搭档 | 纯 Agent |
|
||||||
|
| 思路模糊时的引导能力 | 有限,更多靠卡片提示 | 很强,可动态追问和启发 | 纯 Agent |
|
||||||
|
| 世界整体可扫描性 | 强,卡片和结构更容易总览 | 弱,聊天记录天然碎片化 | 当前方案 |
|
||||||
|
| 单对象精确编辑 | 强,适合定点修改 | 中等,容易在对话里带出额外变化 | 当前方案 |
|
||||||
|
| 锁定与局部重生成 | 容易做明确边界 | 容易模糊,需额外设计指令语义 | 当前方案 |
|
||||||
|
| 长项目稳定性 | 高,适合几十个对象持续维护 | 中等偏弱,越长越依赖摘要和记忆管理 | 当前方案 |
|
||||||
|
| 内容一致性维护 | 更容易做编译与 QA | 纯聊天很难稳定维护,需要后台隐藏编译 | 当前方案 |
|
||||||
|
| 移动端输入体验 | 表单负担偏大 | 聊天输入天然更友好 | 纯 Agent |
|
||||||
|
| 移动端结果总览 | 卡片更好浏览 | 长聊天记录不利于回看 | 当前方案 |
|
||||||
|
| 专业策划生产效率 | 中后期更高 | 前期更快,中后期容易反复确认 | 当前方案 |
|
||||||
|
| 新手用户心理压力 | 偏高,容易觉得要“填很多东西” | 低,更像在聊一个想法 | 纯 Agent |
|
||||||
|
| 实现复杂度 | 已有方向较明确 | 真正做好会更复杂,需要对话层和隐藏结构双系统 | 当前方案 |
|
||||||
|
| Token / 成本 / 延迟 | 更容易按模块调用 | 长会话上下文更重,成本更高 | 当前方案 |
|
||||||
|
| 可审计和交接 | 强,更适合多人协作 | 弱,需要额外导出和摘要机制 | 当前方案 |
|
||||||
|
|
||||||
|
## 3.2 当前结构化方案的主要优点
|
||||||
|
|
||||||
|
当前方案更强的地方在于:
|
||||||
|
|
||||||
|
1. 有明确的内容边界
|
||||||
|
2. 更容易做锁定、重生成和局部修改
|
||||||
|
3. 更适合中大型世界的长期维护
|
||||||
|
4. 更适合和后端编译层、任务层、章节层做稳定映射
|
||||||
|
5. 更容易把专业剧情策划流程映射成可执行数据
|
||||||
|
|
||||||
|
它的本质优势是:
|
||||||
|
|
||||||
|
**稳定、清楚、可扩展。**
|
||||||
|
|
||||||
|
## 3.3 当前结构化方案的主要缺点
|
||||||
|
|
||||||
|
当前方案更弱的地方在于:
|
||||||
|
|
||||||
|
1. 仍然有“我要开始填工具了”的压力
|
||||||
|
2. 对不擅长结构化思考的新手不够友好
|
||||||
|
3. 澄清、启发和陪创作感不够强
|
||||||
|
4. 很容易从“低门槛工作台”滑向“字段很多的编辑器”
|
||||||
|
5. 移动端如果处理不好,会有明显表单压迫感
|
||||||
|
|
||||||
|
## 3.4 纯 Agent 式方案的主要优点
|
||||||
|
|
||||||
|
纯 Agent 式更强的地方在于:
|
||||||
|
|
||||||
|
1. 入口极低
|
||||||
|
2. 更符合普通人“先说想法”的自然习惯
|
||||||
|
3. 更适合模糊创意逐步收束
|
||||||
|
4. 更容易把澄清问题变成真实协作
|
||||||
|
5. 更容易营造“有专业编剧陪你做世界”的体验
|
||||||
|
|
||||||
|
它的本质优势是:
|
||||||
|
|
||||||
|
**自然、轻松、像在共创。**
|
||||||
|
|
||||||
|
## 3.5 纯 Agent 式方案的主要缺点
|
||||||
|
|
||||||
|
纯 Agent 式更弱的地方在于:
|
||||||
|
|
||||||
|
1. 世界模型隐藏得太深时,创作者会失去整体掌控感
|
||||||
|
2. 多轮对话后,已确定内容不容易被清晰回看
|
||||||
|
3. 局部重做和精确编辑边界会变模糊
|
||||||
|
4. Agent 容易过度代写、过度主导
|
||||||
|
5. 没有强摘要和锁定机制时,创意很容易漂移
|
||||||
|
|
||||||
|
它的本质问题是:
|
||||||
|
|
||||||
|
**天然更擅长起步,不天然擅长收口。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 对当前项目是否值得转成纯 Agent 式的判断
|
||||||
|
|
||||||
|
## 4.1 值得转的部分
|
||||||
|
|
||||||
|
以下环节非常适合转成纯 Agent 主交互:
|
||||||
|
|
||||||
|
1. 首次创作入口
|
||||||
|
2. 世界灵魂锚点收集
|
||||||
|
3. 低信息量输入后的澄清与启发
|
||||||
|
4. 关键角色、关键地点、核心冲突的初稿展开
|
||||||
|
5. 对单个角色或单个章节做陪创作式精修
|
||||||
|
|
||||||
|
因为这些环节的关键问题不是“字段如何摆放”,而是:
|
||||||
|
|
||||||
|
**创作者有没有被真正引导出自己想做的世界。**
|
||||||
|
|
||||||
|
## 4.2 不值得直接转成纯聊天黑箱的部分
|
||||||
|
|
||||||
|
以下环节不适合彻底做成无结构纯聊天:
|
||||||
|
|
||||||
|
1. 长项目世界管理
|
||||||
|
2. 大量角色、地点、支线、章节的总览
|
||||||
|
3. 锁定与局部重生成
|
||||||
|
4. 运行时结构编译
|
||||||
|
5. 质量审计与一致性检查
|
||||||
|
6. 导出和交付
|
||||||
|
|
||||||
|
这些环节需要的是:
|
||||||
|
|
||||||
|
**稳定的结构化世界状态,而不是越来越长的聊天记录。**
|
||||||
|
|
||||||
|
## 4.3 最合理的判断
|
||||||
|
|
||||||
|
如果硬要二选一:
|
||||||
|
|
||||||
|
1. 对新手用户和移动端体验,纯 Agent 更有吸引力
|
||||||
|
2. 对专业生产、长期维护和内容质量,当前结构化方案更稳
|
||||||
|
|
||||||
|
所以真正适合当前项目的不是完全替换,而是:
|
||||||
|
|
||||||
|
**把当前方案的“结构和护栏”保留,把用户感受到的“入口和协作方式”改成纯 Agent。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 如果要转换成纯 Agent 式,对什么必须保持不变
|
||||||
|
|
||||||
|
纯 Agent 式可以改变前台交互,但不应该改变下面这些底层原则。
|
||||||
|
|
||||||
|
## 5.1 内容分层边界不能变
|
||||||
|
|
||||||
|
即使转成纯 Agent 式,也仍然要保留这三层:
|
||||||
|
|
||||||
|
1. 创作者必须确认的高杠杆锚点
|
||||||
|
2. AI 生成但允许创作者修改的策划初稿层
|
||||||
|
3. 系统托管的运行时编译层
|
||||||
|
|
||||||
|
变化的只是:
|
||||||
|
|
||||||
|
- 这些内容不一定通过卡片表单采集
|
||||||
|
- 可以通过对话逐步收集和确认
|
||||||
|
|
||||||
|
不应该变化的是:
|
||||||
|
|
||||||
|
- 谁来决定世界灵魂
|
||||||
|
- 谁来决定运行时结构
|
||||||
|
|
||||||
|
## 5.2 锁定机制不能变
|
||||||
|
|
||||||
|
纯 Agent 式必须仍然支持:
|
||||||
|
|
||||||
|
1. 锁定世界主题
|
||||||
|
2. 锁定核心冲突
|
||||||
|
3. 锁定关键角色
|
||||||
|
4. 锁定关键地点
|
||||||
|
5. 锁定主线章节
|
||||||
|
6. 锁定场景章节
|
||||||
|
7. 只重做未锁定部分
|
||||||
|
|
||||||
|
否则纯 Agent 式会很快变成:
|
||||||
|
|
||||||
|
**每次聊一句,世界都在偷偷漂移。**
|
||||||
|
|
||||||
|
## 5.3 局部重生成机制不能变
|
||||||
|
|
||||||
|
纯 Agent 式里也必须支持:
|
||||||
|
|
||||||
|
1. 只重生成长尾 NPC
|
||||||
|
2. 只重生成次级地点
|
||||||
|
3. 只重生成某个角色卡
|
||||||
|
4. 只重生成某个章节
|
||||||
|
5. 围绕锁定对象重做剩余草稿
|
||||||
|
|
||||||
|
如果这点没有做好,对话就会越来越像“整世界覆盖式重写”。
|
||||||
|
|
||||||
|
## 5.4 摘要、快照、差异对比不能变
|
||||||
|
|
||||||
|
纯 Agent 工具如果没有这些能力,后期一定失控:
|
||||||
|
|
||||||
|
1. 当前世界摘要
|
||||||
|
2. 已锁定内容清单
|
||||||
|
3. 本轮修改了什么
|
||||||
|
4. 当前有哪些待确认假设
|
||||||
|
5. 能否回退到上一版本
|
||||||
|
|
||||||
|
所以:
|
||||||
|
|
||||||
|
**前台可以纯聊天,后台不能没有版本化世界圣经。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 转成纯 Agent 式后的产品定义
|
||||||
|
|
||||||
|
## 6.1 定义
|
||||||
|
|
||||||
|
建议把转型后的工具定义为:
|
||||||
|
|
||||||
|
**以 Agent 对话为主交互的 RPG 世界共创工具。**
|
||||||
|
|
||||||
|
它不是:
|
||||||
|
|
||||||
|
- 单纯聊天框
|
||||||
|
- 一次性大文本生成器
|
||||||
|
- 没有状态的陪聊机器人
|
||||||
|
|
||||||
|
它应该是:
|
||||||
|
|
||||||
|
1. 会主动澄清
|
||||||
|
2. 会阶段性总结
|
||||||
|
3. 会把聊天结果沉淀成结构化世界状态
|
||||||
|
4. 会提醒风险和冲突
|
||||||
|
5. 会在创作者要求时进行局部重写和定向扩展
|
||||||
|
|
||||||
|
## 6.2 正确理解
|
||||||
|
|
||||||
|
最重要的一句定义是:
|
||||||
|
|
||||||
|
**界面可以纯 Agent,数据层绝不能纯会话。**
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
1. 创作者看到的是对话
|
||||||
|
2. 系统内部维护的是世界模型、锁定状态、摘要和编译结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 纯 Agent 式工具的推荐交互模型
|
||||||
|
|
||||||
|
## 7.1 阶段 A:创作意图收集
|
||||||
|
|
||||||
|
Agent 不直接要求用户填表,而是通过 `1~3` 轮自然对话收集最小锚点。
|
||||||
|
|
||||||
|
目标是确认:
|
||||||
|
|
||||||
|
1. 世界一句话
|
||||||
|
2. 玩家身份
|
||||||
|
3. 核心冲突
|
||||||
|
4. 主题气质
|
||||||
|
5. 关键关系钩子
|
||||||
|
6. 标志性要素
|
||||||
|
|
||||||
|
这实际上和当前“最小必填 6 张卡”是同一套内容,只是采集方式改成对话。
|
||||||
|
|
||||||
|
## 7.2 阶段 B:Agent 输出首轮世界底稿
|
||||||
|
|
||||||
|
Agent 首轮不应该直接铺满全世界,而应该给出一份简明底稿,例如:
|
||||||
|
|
||||||
|
1. 世界标题和摘要
|
||||||
|
2. 玩家开局定位
|
||||||
|
3. 核心冲突结构
|
||||||
|
4. `3~5` 个关键角色
|
||||||
|
5. `4~6` 个关键地点
|
||||||
|
6. `2~4` 个势力
|
||||||
|
7. 主线第一幕简稿
|
||||||
|
|
||||||
|
同时必须明确分成 3 类:
|
||||||
|
|
||||||
|
1. 已确认内容
|
||||||
|
2. 建议内容
|
||||||
|
3. 待确认内容
|
||||||
|
|
||||||
|
## 7.3 阶段 C:创作者锁定锚点
|
||||||
|
|
||||||
|
在纯 Agent 模式里,锁定行为必须被显式支持。
|
||||||
|
|
||||||
|
用户可以自然说:
|
||||||
|
|
||||||
|
- 这个世界观锁定
|
||||||
|
- 这个角色保留,不要再改
|
||||||
|
- 只把第一幕重做一下
|
||||||
|
- 势力关系别动,重新想地点
|
||||||
|
|
||||||
|
系统需要把这些自然语言翻译成正式的锁定状态和重生成范围。
|
||||||
|
|
||||||
|
## 7.4 阶段 D:按对象逐步精修
|
||||||
|
|
||||||
|
Agent 不应该每轮都继续扩全局,而应该支持“单对象工作模式”。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
1. 只精修某个角色
|
||||||
|
2. 只精修某个地点
|
||||||
|
3. 只精修某个场景章节
|
||||||
|
4. 只精修主线第一幕
|
||||||
|
5. 只精修一条支线
|
||||||
|
|
||||||
|
这样可以避免每轮修改都把整个世界重新搅动一次。
|
||||||
|
|
||||||
|
## 7.5 阶段 E:系统后台自动编译与审计
|
||||||
|
|
||||||
|
每一轮重要修改后,系统后台应自动做:
|
||||||
|
|
||||||
|
1. 世界图谱更新
|
||||||
|
2. 可见性边界更新
|
||||||
|
3. 章节和任务编译
|
||||||
|
4. 设定冲突检查
|
||||||
|
5. 弱关联检查
|
||||||
|
6. 风格一致性检查
|
||||||
|
|
||||||
|
这些结果不一定全部展示,但必须被系统持续维护。
|
||||||
|
|
||||||
|
## 7.6 阶段 F:导出世界圣经和可编辑初稿
|
||||||
|
|
||||||
|
纯 Agent 模式的最终产物不能只是一串聊天记录。
|
||||||
|
|
||||||
|
至少要能导出:
|
||||||
|
|
||||||
|
1. 世界摘要
|
||||||
|
2. 锁定锚点
|
||||||
|
3. 关键角色卡
|
||||||
|
4. 关键地点卡
|
||||||
|
5. 势力卡
|
||||||
|
6. 主线章节简稿
|
||||||
|
7. 支线种子
|
||||||
|
8. 场景章节草稿
|
||||||
|
9. 风险与待确认项
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 纯 Agent 式工具需要的后台结构
|
||||||
|
|
||||||
|
## 8.1 会话层之外必须维护的核心状态
|
||||||
|
|
||||||
|
建议后台至少维护下面这些结构:
|
||||||
|
|
||||||
|
| 结构 | 作用 |
|
||||||
|
| --- | --- |
|
||||||
|
| `creatorIntentProfile` | 当前创作者最初和最新的创作意图 |
|
||||||
|
| `lockedAnchors` | 已确认不可自动改写的内容 |
|
||||||
|
| `worldDraftSnapshot` | 当前世界底稿快照 |
|
||||||
|
| `editableDraftCards` | 角色、地点、势力、章节等可编辑初稿 |
|
||||||
|
| `pendingClarifications` | 当前还未确认的问题 |
|
||||||
|
| `changeLog` | 每轮发生了什么变化 |
|
||||||
|
| `qualityFindings` | 冲突、泄露、弱关联和风格漂移结果 |
|
||||||
|
|
||||||
|
## 8.2 每轮对话后的处理流程
|
||||||
|
|
||||||
|
建议每次用户发言后走这条后台链:
|
||||||
|
|
||||||
|
```text
|
||||||
|
用户消息
|
||||||
|
-> 意图识别
|
||||||
|
-> 判断是在回答问题 / 修改对象 / 请求重生成 / 请求总结 / 请求锁定
|
||||||
|
-> 更新 creatorIntentProfile 或 worldDraftSnapshot
|
||||||
|
-> 重新编译相关草稿对象
|
||||||
|
-> 运行质量检查
|
||||||
|
-> 生成本轮回复
|
||||||
|
-> 同步更新摘要、待确认项和 changeLog
|
||||||
|
```
|
||||||
|
|
||||||
|
这条流程说明:
|
||||||
|
|
||||||
|
**纯 Agent 的前台体验背后,实际仍然是一个结构化内容状态机。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 纯 Agent 式前台应该如何设计
|
||||||
|
|
||||||
|
## 9.1 主界面以对话为主
|
||||||
|
|
||||||
|
主界面可以只有一个核心聊天线程,但不建议只有聊天气泡。
|
||||||
|
|
||||||
|
建议保留 3 个轻量辅助区:
|
||||||
|
|
||||||
|
1. 顶部固定摘要
|
||||||
|
- 当前世界名
|
||||||
|
- 当前阶段
|
||||||
|
- 当前聚焦对象
|
||||||
|
|
||||||
|
2. 锁定内容条
|
||||||
|
- 展示已锁定的世界观、角色、地点、章节
|
||||||
|
|
||||||
|
3. 当前草稿摘要抽屉
|
||||||
|
- 展示关键角色、关键地点、主线第一幕等的简要卡片
|
||||||
|
|
||||||
|
这些区域不是表单编辑器,而是:
|
||||||
|
|
||||||
|
**对话模式下帮助用户保持掌控感的最小结构提示。**
|
||||||
|
|
||||||
|
## 9.2 支持快捷动作
|
||||||
|
|
||||||
|
为了防止用户每次都要自己组织复杂自然语言,建议保留轻量快捷动作:
|
||||||
|
|
||||||
|
1. 总结当前设定
|
||||||
|
2. 锁定当前版本
|
||||||
|
3. 只重做这一项
|
||||||
|
4. 展开主线第一幕
|
||||||
|
5. 增加一个关键角色
|
||||||
|
6. 给我 3 个更有辨识度的版本
|
||||||
|
7. 检查是否有设定冲突
|
||||||
|
|
||||||
|
这类动作按钮不破坏纯 Agent 主交互,反而能显著降低误解成本。
|
||||||
|
|
||||||
|
## 9.3 Agent 的提问规则
|
||||||
|
|
||||||
|
Agent 不能像问卷系统,也不能一次追问太多。
|
||||||
|
|
||||||
|
建议规则:
|
||||||
|
|
||||||
|
1. 一次最多追问 `1~3` 个问题
|
||||||
|
2. 问题必须是当前最缺的高杠杆信息
|
||||||
|
3. 每次追问都给默认建议方向
|
||||||
|
4. 如果创作者不想细答,允许 Agent 先代补一个版本再确认
|
||||||
|
|
||||||
|
这样才能保持“像聊天”,而不是“像客服表单”。
|
||||||
|
|
||||||
|
## 9.4 Agent 的总结规则
|
||||||
|
|
||||||
|
纯 Agent 工具必须高频做阶段性总结。
|
||||||
|
|
||||||
|
建议在这些时机自动总结:
|
||||||
|
|
||||||
|
1. 首轮世界底稿生成后
|
||||||
|
2. 锁定任意关键锚点后
|
||||||
|
3. 完成某个角色精修后
|
||||||
|
4. 主线第一幕生成后
|
||||||
|
5. 每累计 `5~8` 轮重要对话后
|
||||||
|
|
||||||
|
总结必须包含:
|
||||||
|
|
||||||
|
1. 已确认内容
|
||||||
|
2. 新增内容
|
||||||
|
3. 待确认内容
|
||||||
|
4. 潜在风险
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 纯 Agent 式下的锁定、重生成与修改语义
|
||||||
|
|
||||||
|
## 10.1 锁定语义
|
||||||
|
|
||||||
|
建议支持以下语义:
|
||||||
|
|
||||||
|
1. 锁定对象
|
||||||
|
2. 锁定字段
|
||||||
|
3. 锁定关系
|
||||||
|
4. 锁定当前版本
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- 锁定这个角色的身份和秘密,但可以重写语气
|
||||||
|
- 锁定这条冲突,不要再改动它的基本方向
|
||||||
|
- 锁定第一幕结构,只优化角色高光
|
||||||
|
|
||||||
|
## 10.2 重生成语义
|
||||||
|
|
||||||
|
建议支持以下语义:
|
||||||
|
|
||||||
|
1. 完整替换
|
||||||
|
2. 保留锚点重做
|
||||||
|
3. 仅补长尾
|
||||||
|
4. 给出多个候选版本
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- 保留世界观和角色,重做关键地点
|
||||||
|
- 保留第一幕结构,给我三个更强的转折版本
|
||||||
|
- 只补 5 个更有辨识度的路人 NPC
|
||||||
|
|
||||||
|
## 10.3 修改语义
|
||||||
|
|
||||||
|
Agent 应能识别这些常见修改类型:
|
||||||
|
|
||||||
|
1. 收紧风格
|
||||||
|
2. 增强冲突
|
||||||
|
3. 提高角色辨识度
|
||||||
|
4. 减少套路感
|
||||||
|
5. 让地点更有故事残痕
|
||||||
|
6. 把支线和主线绑定得更紧
|
||||||
|
7. 提高队友反应和选择后果
|
||||||
|
|
||||||
|
这些应该是内容层意图,而不是要求用户直接碰底层字段。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 纯 Agent 式的主要风险与防护
|
||||||
|
|
||||||
|
## 11.1 风险 1:对话越长,世界越散
|
||||||
|
|
||||||
|
防护方式:
|
||||||
|
|
||||||
|
1. 周期性强制摘要
|
||||||
|
2. 关键内容结构化落库
|
||||||
|
3. 锁定内容固定展示
|
||||||
|
4. 提供“当前世界圣经”入口
|
||||||
|
|
||||||
|
## 11.2 风险 2:Agent 过度代写,创作者失去作品归属感
|
||||||
|
|
||||||
|
防护方式:
|
||||||
|
|
||||||
|
1. 高杠杆锚点必须要求确认
|
||||||
|
2. 重要改动前先说“我准备改什么”
|
||||||
|
3. 默认优先给多个候选,而不是直接盖写
|
||||||
|
4. 允许创作者随时回退到旧版本
|
||||||
|
|
||||||
|
## 11.3 风险 3:局部修改带出全局漂移
|
||||||
|
|
||||||
|
防护方式:
|
||||||
|
|
||||||
|
1. 只在目标作用域内修改
|
||||||
|
2. 修改后自动展示影响范围
|
||||||
|
3. 对高风险改动要求二次确认
|
||||||
|
|
||||||
|
## 11.4 风险 4:看起来轻松,实际上难以收口
|
||||||
|
|
||||||
|
防护方式:
|
||||||
|
|
||||||
|
1. 阶段化工作流
|
||||||
|
2. 每阶段有明确产物
|
||||||
|
3. 不是无限聊天,而是要进入“底稿确认 -> 精修 -> 导出”
|
||||||
|
|
||||||
|
## 11.5 风险 5:成本和延迟持续上升
|
||||||
|
|
||||||
|
防护方式:
|
||||||
|
|
||||||
|
1. 长会话摘要压缩
|
||||||
|
2. 按对象加载上下文
|
||||||
|
3. 局部编译,而不是每轮重编整世界
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 推荐转型路线
|
||||||
|
|
||||||
|
不建议一步到位把当前方案彻底替换成纯聊天。
|
||||||
|
|
||||||
|
更稳的路线是分 3 步走。
|
||||||
|
|
||||||
|
## 12.1 第一步:先把纯 Agent 做成默认入口
|
||||||
|
|
||||||
|
这一阶段:
|
||||||
|
|
||||||
|
1. 用户进入后直接和 Agent 聊
|
||||||
|
2. Agent 帮用户收集最小锚点
|
||||||
|
3. 系统在后台仍然生成当前方案里的结构化初稿
|
||||||
|
4. 结果页仍保留为可选工作台
|
||||||
|
|
||||||
|
这一阶段的目标是:
|
||||||
|
|
||||||
|
**把“起步方式”改成聊天,但不动后端结构主链。**
|
||||||
|
|
||||||
|
## 12.2 第二步:让关键对象编辑也支持 Agent 化
|
||||||
|
|
||||||
|
这一阶段:
|
||||||
|
|
||||||
|
1. 角色、地点、势力、主线第一幕都支持在聊天里精修
|
||||||
|
2. Agent 支持锁定、重做、总结、对比
|
||||||
|
3. 工作台逐步退成辅助视图,而不是默认主路径
|
||||||
|
|
||||||
|
这一阶段的目标是:
|
||||||
|
|
||||||
|
**让大多数高价值修改都可以通过聊天完成。**
|
||||||
|
|
||||||
|
## 12.3 第三步:工作台只保留总览和导出
|
||||||
|
|
||||||
|
到了这一阶段,前台已经基本纯 Agent 化,但仍建议保留:
|
||||||
|
|
||||||
|
1. 世界圣经总览
|
||||||
|
2. 已锁定对象列表
|
||||||
|
3. 版本快照
|
||||||
|
4. 风险与 QA 结果
|
||||||
|
5. 导出面板
|
||||||
|
|
||||||
|
这一阶段的目标不是消灭结构,而是:
|
||||||
|
|
||||||
|
**让结构从“编辑入口”退成“掌控和收口工具”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 最后结论
|
||||||
|
|
||||||
|
纯 Agent 式对话创作工具的最大优势,是把创作入口从“填写工具”变成“和懂创作的人对话”。
|
||||||
|
|
||||||
|
它会明显提升:
|
||||||
|
|
||||||
|
1. 首次上手意愿
|
||||||
|
2. 创作陪伴感
|
||||||
|
3. 模糊想法的收束效率
|
||||||
|
4. 移动端可用性
|
||||||
|
|
||||||
|
但它也天然会削弱:
|
||||||
|
|
||||||
|
1. 世界总览
|
||||||
|
2. 精确编辑
|
||||||
|
3. 局部重生成边界
|
||||||
|
4. 长项目稳定性
|
||||||
|
5. 质量审计与交接能力
|
||||||
|
|
||||||
|
因此,对当前项目最合理的方向不是彻底放弃结构化方案,而是把它升级成:
|
||||||
|
|
||||||
|
**前台纯 Agent 主交互,后台结构化世界模型持续存在,锁定、摘要、快照、局部重生成和质量护栏全部保留。**
|
||||||
|
|
||||||
|
一句话收束:
|
||||||
|
|
||||||
|
**可以把“创作入口”彻底 Agent 化,但绝不能把“世界状态管理”也做成纯聊天。**
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
# 自定义世界自有设定层优化方案
|
||||||
|
|
||||||
|
更新时间:`2026-04-08`
|
||||||
|
|
||||||
|
## 0. 目标
|
||||||
|
|
||||||
|
这份文档要解决的问题是:
|
||||||
|
|
||||||
|
**当前自定义世界虽然已经是唯一正式可玩的世界入口,但它底层仍依赖武侠 / 仙侠模板设定。**
|
||||||
|
|
||||||
|
本次优化目标不是直接删除这些依赖,而是把它们逐步改造成:
|
||||||
|
|
||||||
|
**属于自定义世界自身的设定层,并且这套设定层必须能通用于任何题材。**
|
||||||
|
|
||||||
|
同时必须满足一条底线:
|
||||||
|
|
||||||
|
**不能破坏当前自定义世界生成流程的任何可用功能。**
|
||||||
|
|
||||||
|
一句话定义:
|
||||||
|
|
||||||
|
**让自定义世界从“借模板世界运行”,升级成“拥有自有设定层、可跨题材运行”的架构。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 设计原则
|
||||||
|
|
||||||
|
这次优化必须同时遵守 4 条原则:
|
||||||
|
|
||||||
|
1. 设定归自定义世界自身所有
|
||||||
|
- 运行时、生成期、表现层真正依赖的世界设定,应该落回 `CustomWorldProfile` 或由它直接编译出的配置。
|
||||||
|
|
||||||
|
2. 设定必须跨题材
|
||||||
|
- 新设定不能只是把“武侠 / 仙侠”换一个更抽象的名字。
|
||||||
|
- 它必须能支撑奇幻、科幻、悬疑、校园、末世、神话、现代、海洋、裂界等任何题材。
|
||||||
|
|
||||||
|
3. 优化优先做兼容迁移
|
||||||
|
- 不能先删旧字段,再补新结构。
|
||||||
|
- 必须先补新设定层,再逐步迁读,最后再让旧模板字段退化成兼容层。
|
||||||
|
|
||||||
|
4. 不能增加创作者负担
|
||||||
|
- 这次不是让创作者多填一堆底层 schema。
|
||||||
|
- 这些设定仍然应由 AI / 系统编译出来,只是所有权从模板世界转移到自定义世界自己。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前自定义世界实际依赖了什么
|
||||||
|
|
||||||
|
根据 [CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md](../reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md),当前依赖可以归纳成 6 组:
|
||||||
|
|
||||||
|
1. 模板锚点字段
|
||||||
|
- `templateWorldType`
|
||||||
|
- `WorldTemplateType`
|
||||||
|
|
||||||
|
2. 规则桥接
|
||||||
|
- `resolveRuleWorldType(...)`
|
||||||
|
|
||||||
|
3. 主题与词汇底板
|
||||||
|
- `detectCustomWorldThemeMode(...)`
|
||||||
|
- `buildThemePackFromWorldProfile(...)`
|
||||||
|
|
||||||
|
4. 属性、资源词与经济 fallback
|
||||||
|
- 预设属性 schema
|
||||||
|
- 资源命名
|
||||||
|
- 初始货币规则
|
||||||
|
|
||||||
|
5. 内容骨架
|
||||||
|
- 角色模板骨架
|
||||||
|
- 怪物模板池
|
||||||
|
- 场景视觉参考池
|
||||||
|
|
||||||
|
6. prompt 兼容字段
|
||||||
|
- `framework` 生成仍要求输出 `templateWorldType`
|
||||||
|
|
||||||
|
这些东西现在还不能直接删,因为:
|
||||||
|
|
||||||
|
**它们不是单纯的预设世界残留,而是当前自定义世界生成与运行时的真实支撑层。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 本次优化的核心思路
|
||||||
|
|
||||||
|
这次不建议新增很多彼此平行的新系统,而是把现有模板依赖统一收束成:
|
||||||
|
|
||||||
|
**自定义世界自己的 5 层设定层。**
|
||||||
|
|
||||||
|
这 5 层分别是:
|
||||||
|
|
||||||
|
1. 语义锚层
|
||||||
|
2. 规则层
|
||||||
|
3. 表现层
|
||||||
|
4. 原型参考层
|
||||||
|
5. 兼容迁移层
|
||||||
|
|
||||||
|
这样可以让现在分散在:
|
||||||
|
|
||||||
|
- 模板世界枚举
|
||||||
|
- 预设 schema
|
||||||
|
- 视觉参考池
|
||||||
|
- 怪物池
|
||||||
|
- ThemePack 底板
|
||||||
|
|
||||||
|
这些地方的依赖,被重新编译回 `CustomWorldProfile` 自己的配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 目标结构
|
||||||
|
|
||||||
|
## 4.1 语义锚层:替代 `templateWorldType`
|
||||||
|
|
||||||
|
当前 `templateWorldType` 实际在回答的不是“你是不是武侠”,而是:
|
||||||
|
|
||||||
|
1. 这个世界更接近哪类冲突结构
|
||||||
|
2. 这个世界更接近哪类制度和禁忌
|
||||||
|
3. 这个世界更接近哪类叙事载体与力量来源
|
||||||
|
|
||||||
|
所以应把它升级成一套真正属于自定义世界自己的语义锚:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldSemanticAnchor {
|
||||||
|
genreSignals: string[];
|
||||||
|
conflictForms: string[];
|
||||||
|
institutionTypes: string[];
|
||||||
|
tabooTypes: string[];
|
||||||
|
carrierTypes: string[];
|
||||||
|
forceSystemTypes: string[];
|
||||||
|
atmosphereTags: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这层应该回答:
|
||||||
|
|
||||||
|
1. 这个世界的主要矛盾像什么
|
||||||
|
2. 这个世界的秩序结构像什么
|
||||||
|
3. 这个世界的危险和禁忌像什么
|
||||||
|
4. 这个世界的线索、遗物、文书、证物、技术、仪式像什么
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
|
||||||
|
- 这里不再出现“武侠 / 仙侠”的模板世界名
|
||||||
|
- 只保留通用语义
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `institutionTypes`
|
||||||
|
- 宗门
|
||||||
|
- 公司
|
||||||
|
- 学园
|
||||||
|
- 教团
|
||||||
|
- 调查局
|
||||||
|
- 舰队
|
||||||
|
- 部族
|
||||||
|
|
||||||
|
- `forceSystemTypes`
|
||||||
|
- 灵脉
|
||||||
|
- 科技
|
||||||
|
- 仪式
|
||||||
|
- 契约
|
||||||
|
- 血统
|
||||||
|
- 神谕
|
||||||
|
- 污染
|
||||||
|
|
||||||
|
这就天然跨题材了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2 规则层:把 fallback 变成自定义世界自己的规则设定
|
||||||
|
|
||||||
|
当前很多规则仍然会回落到武侠 / 仙侠:
|
||||||
|
|
||||||
|
- 属性 schema
|
||||||
|
- 资源命名
|
||||||
|
- 经济命名
|
||||||
|
- 初始货币
|
||||||
|
|
||||||
|
优化后应由自定义世界自己持有:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldRuleProfile {
|
||||||
|
attributeSchema: WorldAttributeSchema;
|
||||||
|
resourceLabels: {
|
||||||
|
hp: string;
|
||||||
|
mp: string;
|
||||||
|
maxHp: string;
|
||||||
|
maxMp: string;
|
||||||
|
damage: string;
|
||||||
|
guard: string;
|
||||||
|
range: string;
|
||||||
|
cooldown: string;
|
||||||
|
manaCost: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
economyProfile: {
|
||||||
|
initialCurrency: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着以后运行时读取逻辑应优先变成:
|
||||||
|
|
||||||
|
1. 先读 `profile.ruleProfile`
|
||||||
|
2. 再读 `profile.attributeSchema`
|
||||||
|
3. 最后才允许读兼容 fallback
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
1. 先判断 `WUXIA / XIANXIA`
|
||||||
|
2. 再猜自定义世界应该像哪边
|
||||||
|
|
||||||
|
这层的原则是:
|
||||||
|
|
||||||
|
**规则是这个自定义世界自己的,不再借模板世界托管。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.3 表现层:把 ThemePack 变成自定义世界自身的派生物
|
||||||
|
|
||||||
|
当前 `ThemePack` 仍然是“预设底板 + 自定义词汇补丁”。
|
||||||
|
|
||||||
|
优化后应改成:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldExpressionProfile {
|
||||||
|
themePack: ThemePack;
|
||||||
|
presentationTone: string[];
|
||||||
|
namingDirectives: string[];
|
||||||
|
clueDirectives: string[];
|
||||||
|
revealDirectives: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- `ThemePack` 仍然可以保留
|
||||||
|
- 但它不再被理解为“武侠底板 / 仙侠底板的延伸”
|
||||||
|
- 它应当是:
|
||||||
|
- `semanticAnchor`
|
||||||
|
- `creatorIntent`
|
||||||
|
- `majorFactions`
|
||||||
|
- `coreConflicts`
|
||||||
|
- `world text`
|
||||||
|
共同编译出来的结果
|
||||||
|
|
||||||
|
这一步非常关键,因为它会决定:
|
||||||
|
|
||||||
|
1. 物件命名
|
||||||
|
2. 势力命名
|
||||||
|
3. 线索形式
|
||||||
|
4. 提示词词汇风格
|
||||||
|
5. reveal 风格
|
||||||
|
|
||||||
|
只有把这层做成自定义世界自己的派生物,后面跨题材才会真的稳。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.4 原型参考层:把模板骨架改成通用原型库
|
||||||
|
|
||||||
|
当前自定义世界借用模板的最深部分是:
|
||||||
|
|
||||||
|
1. 角色模板骨架
|
||||||
|
2. 场景视觉参考池
|
||||||
|
3. 怪物模板池
|
||||||
|
|
||||||
|
这三类不能粗暴删除,但可以改造成:
|
||||||
|
|
||||||
|
**通用原型参考层。**
|
||||||
|
|
||||||
|
建议统一成:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldReferenceProfile {
|
||||||
|
roleArchetypes: RoleArchetypeProfile[];
|
||||||
|
sceneBuckets: SceneArchetypeBucket[];
|
||||||
|
creatureArchetypes: CreatureArchetypeProfile[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4.1 角色原型
|
||||||
|
|
||||||
|
角色原型应描述的是:
|
||||||
|
|
||||||
|
- 正面推进型
|
||||||
|
- 远程压制型
|
||||||
|
- 控场解构型
|
||||||
|
- 续航承压型
|
||||||
|
- 潜行爆发型
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
- 剑之公主
|
||||||
|
- 神箭游侠
|
||||||
|
- 双刃旅者
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**保留战斗骨架和技能骨架,不保留模板角色人格设定。**
|
||||||
|
|
||||||
|
### 4.4.2 场景原型
|
||||||
|
|
||||||
|
场景原型应描述的是:
|
||||||
|
|
||||||
|
- 高压入口区
|
||||||
|
- 雨湿街巷区
|
||||||
|
- 临水渡口区
|
||||||
|
- 仪式神殿区
|
||||||
|
- 高空通路区
|
||||||
|
- 工业热区
|
||||||
|
- 地底遗迹区
|
||||||
|
- 群落聚居区
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
- 竹林古道
|
||||||
|
- 云海仙门
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**保留“空间语义 -> 视觉参考”的逻辑,不保留模板世界场景名作为中心。**
|
||||||
|
|
||||||
|
### 4.4.3 生物原型
|
||||||
|
|
||||||
|
生物原型应描述的是:
|
||||||
|
|
||||||
|
- 潜伏袭击者
|
||||||
|
- 重甲承压者
|
||||||
|
- 群居骚扰者
|
||||||
|
- 远程威胁者
|
||||||
|
- 异化污染体
|
||||||
|
- 灵体回响体
|
||||||
|
- 机关守卫体
|
||||||
|
|
||||||
|
而不是:
|
||||||
|
|
||||||
|
- 某个武侠怪
|
||||||
|
- 某个仙侠怪
|
||||||
|
|
||||||
|
这样之后:
|
||||||
|
|
||||||
|
- 自定义世界依赖的是“通用原型”
|
||||||
|
- 原型再映射到底层素材与 preset
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.5 兼容迁移层:旧字段继续保留一段时间
|
||||||
|
|
||||||
|
为了不破坏当前流程,短期内不能直接删除:
|
||||||
|
|
||||||
|
- `templateWorldType`
|
||||||
|
- `WorldTemplateType`
|
||||||
|
- 以及相关 normalize / save / read 逻辑
|
||||||
|
|
||||||
|
这层应被降级成:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldCompatibilityProfile {
|
||||||
|
legacyTemplateWorldType?: 'WUXIA' | 'XIANXIA' | null;
|
||||||
|
migrationVersion: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
1. 旧存档兼容
|
||||||
|
2. 旧 prompt 兼容
|
||||||
|
3. 旧测试兼容
|
||||||
|
4. 旧工具链兼容
|
||||||
|
|
||||||
|
但它不再应是新生成世界的第一真相来源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 现有每类依赖如何改造成自定义世界自己的设定
|
||||||
|
|
||||||
|
下面把现有依赖逐项映射成未来目标。
|
||||||
|
|
||||||
|
## 5.1 `templateWorldType`
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 世界锚点
|
||||||
|
- 规则 fallback
|
||||||
|
- 视觉和怪物参考入口
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 拆成:
|
||||||
|
- `semanticAnchor`
|
||||||
|
- `ruleProfile`
|
||||||
|
- `referenceProfile`
|
||||||
|
- `compatibilityProfile`
|
||||||
|
|
||||||
|
迁移方式:
|
||||||
|
|
||||||
|
1. 旧字段保留
|
||||||
|
2. 新字段生成后优先读新字段
|
||||||
|
3. 旧字段只做迁移 fallback
|
||||||
|
|
||||||
|
## 5.2 `resolveRuleWorldType(...)`
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 把 `CUSTOM` 解析回 `WUXIA / XIANXIA`
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 改成:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
resolveCustomWorldRuleProfile(profile)
|
||||||
|
```
|
||||||
|
|
||||||
|
运行时不再需要知道“它更像武侠还是仙侠”,而只需要知道:
|
||||||
|
|
||||||
|
- 这个世界的规则 profile 是什么
|
||||||
|
|
||||||
|
## 5.3 `getPresetWorldAttributeSchema(...)`
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 自定义世界 schema 的参考底板
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 把预设 schema 底板重构成:
|
||||||
|
- 通用 schema seeds
|
||||||
|
|
||||||
|
例如按功能分:
|
||||||
|
|
||||||
|
1. 承压轴
|
||||||
|
2. 机动轴
|
||||||
|
3. 洞察轴
|
||||||
|
4. 决断轴
|
||||||
|
5. 共鸣轴
|
||||||
|
6. 续航轴
|
||||||
|
|
||||||
|
然后由自定义世界的 `semanticAnchor + creatorIntent` 生成最终命名与说明。
|
||||||
|
|
||||||
|
## 5.4 `PRESET_CHARACTERS`
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 自定义世界角色的战斗模板和技能骨架
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 抽出:
|
||||||
|
- `RoleArchetypeProfile`
|
||||||
|
- `SkillArchetypeProfile`
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 还可以继续复用当前角色的战斗结构
|
||||||
|
- 但不应再让自定义世界依赖那些模板角色的人设文本
|
||||||
|
|
||||||
|
## 5.5 场景图片参考池
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 自定义世界默认场景图匹配
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 改成:
|
||||||
|
- `SceneArchetypeBucket`
|
||||||
|
|
||||||
|
每个 bucket 只表达通用空间语义,不表达模板世界名。
|
||||||
|
|
||||||
|
## 5.6 怪物池
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 自定义世界敌对单位匹配 preset
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 改成:
|
||||||
|
- `CreatureArchetypeProfile`
|
||||||
|
|
||||||
|
由 archetype 再映射到底层怪物素材与 preset。
|
||||||
|
|
||||||
|
## 5.7 `buildThemePackFromWorldProfile(...)`
|
||||||
|
|
||||||
|
当前作用:
|
||||||
|
|
||||||
|
- 以模板题材包为底生成自定义世界 ThemePack
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 变成:
|
||||||
|
- `buildThemePackFromCustomWorldSemanticAnchor(...)`
|
||||||
|
|
||||||
|
即 ThemePack 以自定义世界自己的语义锚和词汇锚为底。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 不破坏当前流程的迁移顺序
|
||||||
|
|
||||||
|
这是最关键的落地顺序。
|
||||||
|
|
||||||
|
## 阶段 A:先给 `CustomWorldProfile` 补新设定层
|
||||||
|
|
||||||
|
先补:
|
||||||
|
|
||||||
|
1. `semanticAnchor`
|
||||||
|
2. `ruleProfile`
|
||||||
|
3. `expressionProfile`
|
||||||
|
4. `referenceProfile`
|
||||||
|
5. `compatibilityProfile`
|
||||||
|
|
||||||
|
这一步只做新增,不删旧字段。
|
||||||
|
|
||||||
|
## 阶段 B:旧字段自动编译新字段
|
||||||
|
|
||||||
|
当前已有 profile、旧存档、旧生成结果,先统一经过:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
compileOwnedSettingLayersFromLegacyTemplate(profile)
|
||||||
|
```
|
||||||
|
|
||||||
|
让:
|
||||||
|
|
||||||
|
- 旧世界也拥有新设定层
|
||||||
|
- 新运行时可优先消费新字段
|
||||||
|
|
||||||
|
## 阶段 C:生成链开始直接产出新设定层
|
||||||
|
|
||||||
|
修改:
|
||||||
|
|
||||||
|
- `framework prompt`
|
||||||
|
- `normalizeCustomWorldGenerationFramework(...)`
|
||||||
|
- `customWorld.ts`
|
||||||
|
|
||||||
|
使新生成世界优先产出:
|
||||||
|
|
||||||
|
- `semanticAnchor`
|
||||||
|
- `ruleProfile hints`
|
||||||
|
- `referenceProfile hints`
|
||||||
|
|
||||||
|
而不是只产出 `templateWorldType`
|
||||||
|
|
||||||
|
## 阶段 D:运行时逐步改读新设定层
|
||||||
|
|
||||||
|
优先改:
|
||||||
|
|
||||||
|
1. 资源词与货币
|
||||||
|
2. attribute schema
|
||||||
|
3. ThemePack
|
||||||
|
4. 视觉参考
|
||||||
|
5. 怪物映射
|
||||||
|
6. 角色原型引用
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 每次只切一层
|
||||||
|
- 每层都保留 fallback
|
||||||
|
|
||||||
|
## 阶段 E:把旧模板字段降级为兼容层
|
||||||
|
|
||||||
|
当上面的读取已经都切走后:
|
||||||
|
|
||||||
|
- `templateWorldType` 就不再是主链依赖
|
||||||
|
- 只作为:
|
||||||
|
- migration
|
||||||
|
- 老存档兼容
|
||||||
|
- 老工具兼容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 推荐新增的最小字段
|
||||||
|
|
||||||
|
为了避免系统膨胀,建议只先补最小集合。
|
||||||
|
|
||||||
|
## 7.1 `CustomWorldOwnedSettingLayers`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldOwnedSettingLayers {
|
||||||
|
semanticAnchor: CustomWorldSemanticAnchor;
|
||||||
|
ruleProfile: CustomWorldRuleProfile;
|
||||||
|
expressionProfile: CustomWorldExpressionProfile;
|
||||||
|
referenceProfile: CustomWorldReferenceProfile;
|
||||||
|
compatibilityProfile?: CustomWorldCompatibilityProfile | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后挂到:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldProfile {
|
||||||
|
ownedSettingLayers?: CustomWorldOwnedSettingLayers | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样好处是:
|
||||||
|
|
||||||
|
1. 不用在 `CustomWorldProfile` 顶层堆太多字段
|
||||||
|
2. 迁移更集中
|
||||||
|
3. 后续删除兼容层更容易
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 对当前功能链的保护要求
|
||||||
|
|
||||||
|
这次优化过程中,下面这些能力不能坏:
|
||||||
|
|
||||||
|
1. 自定义世界创建
|
||||||
|
2. 自定义世界保存 / 读取
|
||||||
|
3. 自定义世界角色生成
|
||||||
|
4. 自定义世界场景生成
|
||||||
|
5. 自定义世界开局
|
||||||
|
6. 自定义世界运行时的:
|
||||||
|
- 属性
|
||||||
|
- 资源词
|
||||||
|
- 经济
|
||||||
|
- 场景图
|
||||||
|
- 敌对实体
|
||||||
|
- ThemePack
|
||||||
|
- prompt 组织
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**任何一次迭代都必须是“新层可用 + 旧层仍可兜底”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 验收标准
|
||||||
|
|
||||||
|
当下面这些标准成立时,说明这套优化开始有效:
|
||||||
|
|
||||||
|
1. 新生成的自定义世界不再必须依赖 `templateWorldType` 才能表达自身设定。
|
||||||
|
2. 运行时优先读取 `ownedSettingLayers`,而不是先问武侠 / 仙侠。
|
||||||
|
3. 自定义世界的属性、资源词、经济、视觉参考、怪物映射、ThemePack 都能从自身设定层派生出来。
|
||||||
|
4. 新设定层描述的是通用语义,不是模板世界换皮。
|
||||||
|
5. 当前自定义世界生成流程、旧存档、旧结果页工作台仍然可用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 最后结论
|
||||||
|
|
||||||
|
如果目标是:
|
||||||
|
|
||||||
|
**让这些依赖都变成属于自定义世界的设定,而且这些设定要通用于任何题材。**
|
||||||
|
|
||||||
|
那么最正确的方向不是“继续弱化武侠 / 仙侠字样”,而是:
|
||||||
|
|
||||||
|
**把模板支撑层整体迁移成自定义世界自己的设定层。**
|
||||||
|
|
||||||
|
更具体地说,就是把当前依赖重组为:
|
||||||
|
|
||||||
|
1. 自定义世界自己的语义锚
|
||||||
|
2. 自定义世界自己的规则 profile
|
||||||
|
3. 自定义世界自己的表达 profile
|
||||||
|
4. 自定义世界自己的原型参考 profile
|
||||||
|
5. 只负责兼容的旧模板字段
|
||||||
|
|
||||||
|
这样之后,自定义世界才会真正从:
|
||||||
|
|
||||||
|
**模板依赖型生成架构**
|
||||||
|
|
||||||
|
迁移成:
|
||||||
|
|
||||||
|
**跨题材、自有设定层、且兼容当前流程的生成架构。**
|
||||||
@@ -0,0 +1,656 @@
|
|||||||
|
# 自定义世界去模板依赖与跨题材通用化优化设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-08`
|
||||||
|
|
||||||
|
## 0. 目标
|
||||||
|
|
||||||
|
这份文档解决的是一个已经明确暴露出来的问题:
|
||||||
|
|
||||||
|
**当前玩家主流程虽然已经移除了武侠 / 仙侠两个预设世界,但自定义世界底层仍然依赖武侠 / 仙侠模板设定。**
|
||||||
|
|
||||||
|
本次优化的目标不是简单“删掉模板字段”,而是要在不破坏当前自定义世界生成流程的前提下,把这些依赖逐步改造成:
|
||||||
|
|
||||||
|
**属于自定义世界自身、且能通用于任何题材的设定层。**
|
||||||
|
|
||||||
|
一句话定义:
|
||||||
|
|
||||||
|
**让自定义世界从“挂靠武侠 / 仙侠模板运行”,升级成“基于通用世界设定层独立运行”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 优化原则
|
||||||
|
|
||||||
|
这次优化必须同时满足 3 条硬原则:
|
||||||
|
|
||||||
|
1. 设定归自定义世界自身所有
|
||||||
|
- 任何运行时、生成期、表现层真正依赖的设定,都应尽量落回 `CustomWorldProfile` 或由它直接编译出的通用配置,不再默认挂在 `WUXIA / XIANXIA` 两个模板世界上。
|
||||||
|
|
||||||
|
2. 设定必须跨题材通用
|
||||||
|
- 不能把“自定义世界去模板化”理解成“再做一套更抽象的武侠 / 仙侠替代字段”。
|
||||||
|
- 新结构必须能容纳奇幻、科幻、悬疑、末世、现代、神话、校园等任何题材。
|
||||||
|
|
||||||
|
3. 不能破坏当前自定义世界生成流程
|
||||||
|
- 现有 `framework -> themePack -> storyGraph -> role / landmark -> runtime` 主链必须继续能跑。
|
||||||
|
- 优化应以兼容迁移为主,而不是大爆破式重写。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前问题归纳
|
||||||
|
|
||||||
|
根据 [CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md](../reference/CUSTOM_WORLD_TEMPLATE_DEPENDENCY_INVENTORY_2026-04-08.md),当前自定义世界仍依赖模板层的地方主要有:
|
||||||
|
|
||||||
|
1. 模板锚点类型
|
||||||
|
- `templateWorldType`
|
||||||
|
- `WorldTemplateType`
|
||||||
|
- `resolveRuleWorldType(...)`
|
||||||
|
|
||||||
|
2. 主题与规则回退
|
||||||
|
- `detectCustomWorldThemeMode(...)`
|
||||||
|
- `resolveCustomWorldAnchorWorldType(...)`
|
||||||
|
- `buildThemePackFromWorldProfile(...)` 底板
|
||||||
|
|
||||||
|
3. 属性与表现
|
||||||
|
- 预设世界属性 schema
|
||||||
|
- 资源词、数值命名、货币命名
|
||||||
|
|
||||||
|
4. 角色骨架
|
||||||
|
- `PRESET_CHARACTERS`
|
||||||
|
- 模板技能定义
|
||||||
|
- 模板 opening 接口
|
||||||
|
|
||||||
|
5. 场景与视觉参考
|
||||||
|
- 武侠 / 仙侠场景图片参考池
|
||||||
|
- 模板 camp 场景映射
|
||||||
|
|
||||||
|
6. 怪物模板池
|
||||||
|
- 武侠 / 仙侠怪物 preset 池
|
||||||
|
|
||||||
|
这些依赖本质上说明:
|
||||||
|
|
||||||
|
**当前自定义世界不是完全自足,而是“先生成自定义内容,再把它映射回两套模板世界骨架”。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 这次优化不应该怎么做
|
||||||
|
|
||||||
|
先明确几个错误方向:
|
||||||
|
|
||||||
|
## 3.1 不能直接删掉 `templateWorldType`
|
||||||
|
|
||||||
|
如果直接删除:
|
||||||
|
|
||||||
|
- `templateWorldType`
|
||||||
|
- `WorldTemplateType`
|
||||||
|
- `WUXIA / XIANXIA` 相关回退
|
||||||
|
|
||||||
|
而不先补新的通用设定层,那么当前自定义世界会立刻失去:
|
||||||
|
|
||||||
|
1. 规则桥接
|
||||||
|
2. 主题底板
|
||||||
|
3. 场景图参考
|
||||||
|
4. 怪物池匹配
|
||||||
|
5. 属性 schema fallback
|
||||||
|
|
||||||
|
这会直接打断现有生成和运行链路。
|
||||||
|
|
||||||
|
## 3.2 不能把新设定继续写成“武侠 / 仙侠的抽象别名”
|
||||||
|
|
||||||
|
例如下面这种思路是不够的:
|
||||||
|
|
||||||
|
- 把 `templateWorldType` 改名成 `worldFamily`
|
||||||
|
- 但值仍然只有两类近似武侠 / 仙侠的内部枚举
|
||||||
|
|
||||||
|
这不是真正跨题材,只是换了名字。
|
||||||
|
|
||||||
|
## 3.3 不能让创作者承担更多底层配置工作
|
||||||
|
|
||||||
|
这次优化不是让创作者额外填写:
|
||||||
|
|
||||||
|
- 怪物模板表
|
||||||
|
- 场景参考池
|
||||||
|
- 属性 schema 槽位
|
||||||
|
- 规则 profile
|
||||||
|
|
||||||
|
正确方向应该是:
|
||||||
|
|
||||||
|
**这些通用设定仍由系统生成 / 编译,但所有权回到自定义世界自身。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 目标结构:把模板依赖改造成自定义世界自己的 4 层通用设定
|
||||||
|
|
||||||
|
为了避免系统越改越散,这次建议不要新增很多彼此平行的新系统,而是把现有模板依赖统一收束成 4 层通用设定。
|
||||||
|
|
||||||
|
## 4.1 第一层:世界语义锚层
|
||||||
|
|
||||||
|
这是替代 `templateWorldType` 的核心层。
|
||||||
|
|
||||||
|
它不再回答“你更像武侠还是仙侠”,而回答:
|
||||||
|
|
||||||
|
1. 这个世界的主要冲突形式是什么
|
||||||
|
2. 这个世界的制度、禁忌、叙事载体、力量来源是什么
|
||||||
|
3. 这个世界更接近哪类表现模式
|
||||||
|
|
||||||
|
建议由自定义世界自己持有一个通用结构,例如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldSemanticAnchor {
|
||||||
|
genreSignals: string[];
|
||||||
|
conflictModel: string[];
|
||||||
|
institutionHints: string[];
|
||||||
|
tabooHints: string[];
|
||||||
|
carrierHints: string[];
|
||||||
|
forceSystemHints: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 它是自定义世界自己的语义锚。
|
||||||
|
- 它可以表示:
|
||||||
|
- 宗门与灵脉
|
||||||
|
- 财团与实验体
|
||||||
|
- 学园与旧规
|
||||||
|
- 边境与裂界
|
||||||
|
- 海潮与失落群岛
|
||||||
|
- 神话与誓约
|
||||||
|
- 不再强制回落到武侠 / 仙侠二选一。
|
||||||
|
|
||||||
|
## 4.2 第二层:规则与表现配置层
|
||||||
|
|
||||||
|
这是替代:
|
||||||
|
|
||||||
|
- `resolveRuleWorldType(...)`
|
||||||
|
- 预设属性 schema fallback
|
||||||
|
- 资源命名 fallback
|
||||||
|
- 经济 fallback
|
||||||
|
|
||||||
|
建议改成由自定义世界持有一份通用 `WorldRuleProfile`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface WorldRuleProfile {
|
||||||
|
attributeSchema: WorldAttributeSchema;
|
||||||
|
resourceLabels: {
|
||||||
|
hp: string;
|
||||||
|
mp: string;
|
||||||
|
maxHp: string;
|
||||||
|
maxMp: string;
|
||||||
|
damage: string;
|
||||||
|
guard: string;
|
||||||
|
range: string;
|
||||||
|
cooldown: string;
|
||||||
|
manaCost: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
economyProfile: {
|
||||||
|
initialCurrency: number;
|
||||||
|
rarityValueScale: Record<string, number>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
|
||||||
|
1. 以后运行时不要再优先问“这是武侠还是仙侠”。
|
||||||
|
2. 应该直接问:
|
||||||
|
- `profile.ruleProfile.attributeSchema`
|
||||||
|
- `profile.ruleProfile.resourceLabels`
|
||||||
|
- `profile.ruleProfile.economyProfile`
|
||||||
|
|
||||||
|
这样:
|
||||||
|
|
||||||
|
- 奇幻世界可以叫“体魄 / 法力”
|
||||||
|
- 末世世界可以叫“生命 / 电量”
|
||||||
|
- 校园悬疑世界甚至可以弱化“mana”概念,改成“专注 / 压力”
|
||||||
|
|
||||||
|
## 4.3 第三层:内容骨架与参考层
|
||||||
|
|
||||||
|
这是替代:
|
||||||
|
|
||||||
|
- 预设角色模板骨架
|
||||||
|
- 武侠 / 仙侠场景图参考池
|
||||||
|
- 武侠 / 仙侠怪物 preset 池
|
||||||
|
|
||||||
|
这里不建议让自定义世界自己保存大批素材,而是建议让自定义世界持有:
|
||||||
|
|
||||||
|
**对内容骨架的“编译后引用配置”。**
|
||||||
|
|
||||||
|
建议统一成一个 `ContentReferenceProfile`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ContentReferenceProfile {
|
||||||
|
roleArchetypes: RoleArchetypeProfile[];
|
||||||
|
sceneReferenceBuckets: SceneReferenceBucket[];
|
||||||
|
creatureArchetypes: CreatureArchetypeProfile[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中每个子项都应是通用语义,而不是模板世界名:
|
||||||
|
|
||||||
|
1. `RoleArchetypeProfile`
|
||||||
|
- 例如:
|
||||||
|
- 正面压制型
|
||||||
|
- 远程游击型
|
||||||
|
- 灵术控制型
|
||||||
|
- 重装承压型
|
||||||
|
- 潜行刺击型
|
||||||
|
|
||||||
|
2. `SceneReferenceBucket`
|
||||||
|
- 例如:
|
||||||
|
- 高压门禁区
|
||||||
|
- 雨夜街巷区
|
||||||
|
- 高空通道区
|
||||||
|
- 神殿 / 仪式区
|
||||||
|
- 工业热区
|
||||||
|
- 潮湿临水区
|
||||||
|
|
||||||
|
3. `CreatureArchetypeProfile`
|
||||||
|
- 例如:
|
||||||
|
- 潜伏掠食者
|
||||||
|
- 重甲承压者
|
||||||
|
- 群居灵体
|
||||||
|
- 远程骚扰者
|
||||||
|
- 寄生污染体
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
|
||||||
|
- 这些 archetype 可以继续映射到底层素材和 preset。
|
||||||
|
- 但对自定义世界来说,它依赖的是“通用原型”,不是“武侠怪物池 / 仙侠怪物池”。
|
||||||
|
|
||||||
|
## 4.4 第四层:叙事与词汇编译层
|
||||||
|
|
||||||
|
这是替代:
|
||||||
|
|
||||||
|
- 以模板世界为底的 `ThemePack` fallback
|
||||||
|
- prompt 中的模板世界兼容字段
|
||||||
|
|
||||||
|
建议做法:
|
||||||
|
|
||||||
|
1. `ThemePack` 继续保留,但它的来源变成:
|
||||||
|
- `semanticAnchor + creatorIntent + world text + content reference profile`
|
||||||
|
|
||||||
|
2. 生成框架 prompt 不再要求输出:
|
||||||
|
- `templateWorldType: WUXIA|XIANXIA`
|
||||||
|
|
||||||
|
3. 改为要求输出:
|
||||||
|
- 世界语义锚
|
||||||
|
- 规则表现关键词
|
||||||
|
- 冲突和制度线索
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldGenerationFramework {
|
||||||
|
name: string;
|
||||||
|
subtitle: string;
|
||||||
|
summary: string;
|
||||||
|
tone: string;
|
||||||
|
playerGoal: string;
|
||||||
|
semanticAnchor: CustomWorldSemanticAnchor;
|
||||||
|
majorFactions: string[];
|
||||||
|
coreConflicts: string[];
|
||||||
|
camp: CampOutline;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样:
|
||||||
|
|
||||||
|
- 生成流程仍然是 `framework -> themePack -> storyGraph -> role / landmark`
|
||||||
|
- 只是 framework 的核心锚不再依赖预设世界枚举
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 现有依赖如何一一改造
|
||||||
|
|
||||||
|
## 5.1 `templateWorldType` -> `semanticAnchor + ruleProfile`
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 表示自定义世界挂靠武侠还是仙侠
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 运行时不再直接读取 `templateWorldType`
|
||||||
|
- 改为读取:
|
||||||
|
- `semanticAnchor`
|
||||||
|
- `ruleProfile`
|
||||||
|
|
||||||
|
迁移策略:
|
||||||
|
|
||||||
|
1. 先保留 `templateWorldType` 作为兼容字段
|
||||||
|
2. 新增 `semanticAnchor / ruleProfile`
|
||||||
|
3. 由旧字段自动编译出新字段
|
||||||
|
4. 所有读取逻辑逐步切到新字段
|
||||||
|
5. 最后把 `templateWorldType` 降级为 migration-only 字段
|
||||||
|
|
||||||
|
## 5.2 `resolveRuleWorldType(...)` -> `resolveWorldRuleProfile(...)`
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 把 `CUSTOM` 解析回 `WUXIA / XIANXIA`
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 不再返回模板 world type
|
||||||
|
- 直接返回自定义世界自己的 `ruleProfile`
|
||||||
|
|
||||||
|
即:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
resolveWorldRuleProfile(worldType, customWorldProfile)
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- `attributeSchema`
|
||||||
|
- `resourceLabels`
|
||||||
|
- `economyProfile`
|
||||||
|
- 其它规则信息
|
||||||
|
|
||||||
|
## 5.3 预设属性 schema -> 通用 attribute schema seed
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 用武侠 / 仙侠 schema 作为自定义世界 schema 参考底板
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 不再直接引用“武侠六脉 / 仙侠六轴”作为唯一底板
|
||||||
|
- 改为维护一组通用 `attribute schema seeds`
|
||||||
|
|
||||||
|
例如可按世界体验而不是题材命名:
|
||||||
|
|
||||||
|
1. 正面对抗型
|
||||||
|
2. 机动博弈型
|
||||||
|
3. 灵知洞察型
|
||||||
|
4. 共鸣契约型
|
||||||
|
5. 生存续航型
|
||||||
|
6. 高危推进型
|
||||||
|
|
||||||
|
然后由自定义世界的 `semanticAnchor` 决定如何组合或命名这些槽位。
|
||||||
|
|
||||||
|
## 5.4 预设角色模板 -> 通用角色原型骨架
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 从 `PRESET_CHARACTERS` 选模板角色,再覆写自定义世界内容
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 把当前模板角色骨架抽成“通用战斗原型角色”
|
||||||
|
|
||||||
|
例如保留的是:
|
||||||
|
|
||||||
|
- 技能骨架
|
||||||
|
- 动作风格
|
||||||
|
- build 倾向
|
||||||
|
- 动画资源挂载方式
|
||||||
|
|
||||||
|
而不是保留“剑之公主 / 神箭游侠”这样的预设世界人格设定。
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
|
||||||
|
- 动画素材和技能骨架可以保留
|
||||||
|
- 但运行时不应再依赖具体模板角色的人设文本
|
||||||
|
|
||||||
|
## 5.5 武侠 / 仙侠场景图参考池 -> 通用场景参考桶
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 用武侠 / 仙侠场景关键词为自定义世界匹配默认背景图
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 改成通用 `SceneReferenceBucket`
|
||||||
|
- 每个 bucket 对应一类空间语义
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
1. 落脚处 / 归舍
|
||||||
|
2. 高压入口
|
||||||
|
3. 雨湿街巷
|
||||||
|
4. 高空通路
|
||||||
|
5. 仪式空间
|
||||||
|
6. 工业热区
|
||||||
|
7. 临水空间
|
||||||
|
8. 地底遗迹
|
||||||
|
|
||||||
|
这样就能让:
|
||||||
|
|
||||||
|
- 科幻世界
|
||||||
|
- 校园世界
|
||||||
|
- 神话世界
|
||||||
|
- 末世世界
|
||||||
|
|
||||||
|
都共享同一套“空间语义 -> 视觉参考”的逻辑。
|
||||||
|
|
||||||
|
## 5.6 武侠 / 仙侠怪物池 -> 通用生物原型库
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 从武侠 / 仙侠怪物池里为自定义世界匹配怪物
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 改成通用 `CreatureArchetypeProfile`
|
||||||
|
- 再由 archetype 去映射底层 preset / 数值 / 动画素材
|
||||||
|
|
||||||
|
这样做的好处:
|
||||||
|
|
||||||
|
1. 自定义世界依赖的是“潜伏者 / 重压者 / 群居体 / 异化体”
|
||||||
|
2. 而不是“这更像武侠怪还是仙侠怪”
|
||||||
|
|
||||||
|
## 5.7 `ThemePack` 底板 -> 通用语义底板
|
||||||
|
|
||||||
|
当前用途:
|
||||||
|
|
||||||
|
- 自定义世界的 ThemePack 还是从预设题材包底板开始
|
||||||
|
|
||||||
|
目标改造:
|
||||||
|
|
||||||
|
- 让 ThemePack 直接从:
|
||||||
|
- `semanticAnchor`
|
||||||
|
- `creatorIntent`
|
||||||
|
- `majorFactions`
|
||||||
|
- `coreConflicts`
|
||||||
|
- `contentReferenceProfile`
|
||||||
|
编译出来
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**ThemePack 应变成自定义世界自己的派生物,而不是模板世界的扩写版。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 不破坏现有生成流程的迁移方案
|
||||||
|
|
||||||
|
这是这份文档最重要的部分。
|
||||||
|
|
||||||
|
正确做法不是一口气替换,而是兼容迁移。
|
||||||
|
|
||||||
|
## 阶段 A:新增通用设定字段,但不删旧字段
|
||||||
|
|
||||||
|
先做:
|
||||||
|
|
||||||
|
1. 在 `CustomWorldProfile` 上新增:
|
||||||
|
- `semanticAnchor`
|
||||||
|
- `ruleProfile`
|
||||||
|
- `contentReferenceProfile`
|
||||||
|
|
||||||
|
2. 保留:
|
||||||
|
- `templateWorldType`
|
||||||
|
|
||||||
|
3. 由当前旧字段自动编译出新字段
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 先让新结构出现
|
||||||
|
- 但现有流程完全不受影响
|
||||||
|
|
||||||
|
## 阶段 B:生成流程改为优先产出新字段
|
||||||
|
|
||||||
|
先改:
|
||||||
|
|
||||||
|
- `framework prompt`
|
||||||
|
- `customWorld.ts`
|
||||||
|
|
||||||
|
让 AI 先输出:
|
||||||
|
|
||||||
|
- 通用语义锚
|
||||||
|
- 规则提示
|
||||||
|
- 通用 archetype 线索
|
||||||
|
|
||||||
|
同时在 normalize 层继续兼容旧的 `templateWorldType`
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 新生成的自定义世界开始“原生带通用设定”
|
||||||
|
- 旧存档仍可继续读取
|
||||||
|
|
||||||
|
## 阶段 C:运行时读取切到新设定
|
||||||
|
|
||||||
|
依次改:
|
||||||
|
|
||||||
|
1. 规则层
|
||||||
|
- 从 `resolveRuleWorldType` 切到 `resolveWorldRuleProfile`
|
||||||
|
|
||||||
|
2. 属性层
|
||||||
|
- 优先读 `profile.ruleProfile.attributeSchema`
|
||||||
|
|
||||||
|
3. 资源词与经济层
|
||||||
|
- 优先读 `profile.ruleProfile.resourceLabels / economyProfile`
|
||||||
|
|
||||||
|
4. 场景图与怪物映射
|
||||||
|
- 优先读 `contentReferenceProfile`
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 让模板世界字段不再是运行时第一来源
|
||||||
|
|
||||||
|
## 阶段 D:模板世界字段退化为兼容层
|
||||||
|
|
||||||
|
这一步完成后:
|
||||||
|
|
||||||
|
1. `templateWorldType` 只用于:
|
||||||
|
- 旧存档迁移
|
||||||
|
- 老测试兼容
|
||||||
|
- 数据修复 fallback
|
||||||
|
|
||||||
|
2. 不再用于:
|
||||||
|
- 正式生成主链
|
||||||
|
- 正式运行时主链
|
||||||
|
|
||||||
|
## 阶段 E:再做深清理
|
||||||
|
|
||||||
|
只有当上面几步都完成后,才适合继续清理:
|
||||||
|
|
||||||
|
1. 非必要的模板回退逻辑
|
||||||
|
2. 非必要的主流程模板枚举消费点
|
||||||
|
3. 非必要的审计 / 工具硬编码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 对当前主链的兼容要求
|
||||||
|
|
||||||
|
这次优化过程中,下面这些链路必须始终可用:
|
||||||
|
|
||||||
|
1. `PreGameSelectionFlow -> generateCustomWorldProfile(...)`
|
||||||
|
2. `framework -> themePack -> storyGraph -> role / landmark`
|
||||||
|
3. 保存 / 读取自定义世界 profile
|
||||||
|
4. 自定义世界开局
|
||||||
|
5. 自定义世界角色与场景生成
|
||||||
|
6. 自定义世界运行时怪物 / 物品 / 场景图 /词汇表现
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
**任何迁移都必须先补新字段,再迁读,再退旧字段,不能先删旧字段。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 建议新增或改造的最小数据结构
|
||||||
|
|
||||||
|
为了避免系统膨胀,这次不建议引入很多彼此割裂的新系统,建议只在 `CustomWorldProfile` 周边增量补三块。
|
||||||
|
|
||||||
|
## 8.1 `semanticAnchor`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CustomWorldSemanticAnchor {
|
||||||
|
genreSignals: string[];
|
||||||
|
conflictModel: string[];
|
||||||
|
institutionHints: string[];
|
||||||
|
tabooHints: string[];
|
||||||
|
carrierHints: string[];
|
||||||
|
forceSystemHints: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8.2 `ruleProfile`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface WorldRuleProfile {
|
||||||
|
attributeSchema: WorldAttributeSchema;
|
||||||
|
resourceLabels: {
|
||||||
|
hp: string;
|
||||||
|
mp: string;
|
||||||
|
maxHp: string;
|
||||||
|
maxMp: string;
|
||||||
|
damage: string;
|
||||||
|
guard: string;
|
||||||
|
range: string;
|
||||||
|
cooldown: string;
|
||||||
|
manaCost: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
economyProfile: {
|
||||||
|
initialCurrency: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8.3 `contentReferenceProfile`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ContentReferenceProfile {
|
||||||
|
roleArchetypes: string[];
|
||||||
|
sceneReferenceBuckets: string[];
|
||||||
|
creatureArchetypes: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这三块足够作为第一轮去模板化的最小自定义世界设定层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 验收标准
|
||||||
|
|
||||||
|
做到以下几点,才能说明自定义世界真正开始脱离模板依赖:
|
||||||
|
|
||||||
|
1. 新生成的自定义世界框架不再需要直接输出 `WUXIA|XIANXIA` 才能工作。
|
||||||
|
2. 运行时核心逻辑优先读取 `semanticAnchor / ruleProfile / contentReferenceProfile`。
|
||||||
|
3. 自定义世界的属性 schema、资源词、经济词不再默认直接回退到武侠 / 仙侠文案。
|
||||||
|
4. 自定义世界的角色骨架、场景视觉、怪物映射能通过通用 archetype 表达。
|
||||||
|
5. 现有生成流程、存档读取、运行时体验不受破坏。
|
||||||
|
6. 旧存档仍能通过兼容层运行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 最后结论
|
||||||
|
|
||||||
|
当前自定义世界真正缺的,不是“再删一点武侠 / 仙侠字样”,而是:
|
||||||
|
|
||||||
|
**把模板世界支撑层改写成自定义世界自己的通用设定层。**
|
||||||
|
|
||||||
|
更准确地说,这次优化要完成的是:
|
||||||
|
|
||||||
|
1. 把模板锚点改成自定义世界自己的语义锚
|
||||||
|
2. 把模板回退改成自定义世界自己的规则配置
|
||||||
|
3. 把模板角色 / 场景 / 怪物参考改成通用原型引用
|
||||||
|
4. 把 ThemePack 和生成 prompt 从“依附模板世界”改成“直接从自定义世界自身编译”
|
||||||
|
|
||||||
|
同时整个过程必须遵守一条底线:
|
||||||
|
|
||||||
|
**任何优化都不能破坏当前自定义世界生成与运行主链。**
|
||||||
|
|
||||||
|
所以这不是“删模板”的问题,而是一次:
|
||||||
|
|
||||||
|
**在兼容现有流程前提下,把自定义世界从模板依赖型架构,迁移成真正跨题材、自足型架构。**
|
||||||
882
docs/design/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md
Normal file
882
docs/design/EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md
Normal file
@@ -0,0 +1,882 @@
|
|||||||
|
# 配装构筑 + 合成/锻造闭环详细设计
|
||||||
|
|
||||||
|
更新时间:`2026-03-25`
|
||||||
|
|
||||||
|
## 0. 设计前提
|
||||||
|
|
||||||
|
这份方案基于当前仓库已经存在的运行时结构来设计,不另起一套独立系统。
|
||||||
|
|
||||||
|
- 现有物品结构已经有 `InventoryItem.tags`、`statProfile`、`useProfile`、`buildProfile`。
|
||||||
|
- 现有装备位只有 `weapon / armor / relic` 三槽,因此本期套装与 build 成型必须围绕 2 件和 3 件完成。
|
||||||
|
- 现有战斗结算已经有 `functionEffect.damageMultiplier / incomingDamageMultiplier`,但 `equipmentEffects.ts` 中的装备数值聚合仍然基本为空壳。
|
||||||
|
- 现有素材库中已经出现 `setId`、`setName`、`pieceName`、`synergy` 等 build 元数据,但尚未进入真实伤害结算。
|
||||||
|
- 现有角色、怪物、消耗品、掉落、宝藏、NPC 交易都已经形成了资源入口,适合继续补成“获取 -> 拆解 -> 锻造 -> 配装 -> 战斗 -> 再获取”的闭环。
|
||||||
|
|
||||||
|
因此,本方案的目标不是“再做一层 UI”,而是补齐以下 4 层:
|
||||||
|
|
||||||
|
1. 标签规范化层:把当前中英混用、结构标签与语义标签混用的问题拆开。
|
||||||
|
2. 语义相似度层:用 embedding 相似度把“相近标签”自动组织为 build 与套装簇。
|
||||||
|
3. 伤害修正层:把标签结果稳定接入当前伤害公式。
|
||||||
|
4. 合成/锻造闭环:让掉落、材料、装备、buff、交易真正循环起来。
|
||||||
|
|
||||||
|
## 1. 结合当前系统的落地判断
|
||||||
|
|
||||||
|
### 1.1 现有可复用基础
|
||||||
|
|
||||||
|
- `src/types.ts`
|
||||||
|
- 已有 `ItemStatProfile`
|
||||||
|
- 已有 `ItemUseProfile`
|
||||||
|
- 已有 `ItemBuildProfile`
|
||||||
|
- 已有 `EquipmentLoadout`
|
||||||
|
- 已有 `GameState.playerEquipment`
|
||||||
|
- `src/data/itemDesign.ts`
|
||||||
|
- 已经能为装备自动生成 `buildProfile`
|
||||||
|
- 已经有 `setId / setName / pieceName / synergy`
|
||||||
|
- 已经有一批 role/tag 原型,例如 `assassin / caster / ward / fate`
|
||||||
|
- `src/data/monsterPresets.ts`
|
||||||
|
- 已经有 `habitatTags`
|
||||||
|
- 已经有较完整的 `lootTable`
|
||||||
|
- `src/data/treasureInteractions.ts`
|
||||||
|
- 已经有材料、消耗品、稀有品产出入口
|
||||||
|
- `src/hooks/useCombatFlow.ts`
|
||||||
|
- 玩家、同伴、怪物三条伤害结算路径已经齐备
|
||||||
|
- 只差把 build 结果统一注入 `damage`
|
||||||
|
|
||||||
|
### 1.2 当前缺口
|
||||||
|
|
||||||
|
- 当前 `InventoryItem.tags` 同时承担了“分类标签”和“战斗语义标签”,例如 `weapon / armor / relic / material / mana / healing` 混在一起。
|
||||||
|
- 当前角色正式数据结构里没有稳定的 `combatTags`,角色标签主要还停留在 UI 展示常量。
|
||||||
|
- 当前怪物只有 `habitatTags`,更适合掉落/生态,不适合直接进入伤害 build。
|
||||||
|
- 当前技能/物品可以恢复数值,但还不能稳定施加“限回合 build buff 标签”。
|
||||||
|
- 当前 `getEquipmentBonuses()` 没有真正读取 `statProfile + buildProfile`,导致 build 无法进入伤害。
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
- 现有系统已经具备 build 系统的数据骨架。
|
||||||
|
- 真正需要补的是“标签语义层”和“统一伤害入口”。
|
||||||
|
|
||||||
|
## 2. Build 标签设计原则
|
||||||
|
|
||||||
|
### 2.1 标签必须贴近实体语义
|
||||||
|
|
||||||
|
参与 build 的标签应尽量是玩家能直觉理解的“风格词”,而不是纯系统词。
|
||||||
|
|
||||||
|
推荐使用:
|
||||||
|
|
||||||
|
- 行为风格:`快剑`、`突进`、`追击`、`反击`、`蓄力`、`控场`
|
||||||
|
- 输出方式:`重击`、`连段`、`远射`、`雷法`、`符阵`、`毒雾`
|
||||||
|
- 生存节奏:`护体`、`守御`、`回复`、`续战`、`压血`
|
||||||
|
- 材料/流派气质:`寒铁`、`星砂`、`灵木`、`骨纹`、`镇邪`
|
||||||
|
- 阵营/身份风格:`先锋`、`游击`、`法修`、`命纹`、`统御`
|
||||||
|
|
||||||
|
不推荐直接把以下内容当作伤害 build 标签:
|
||||||
|
|
||||||
|
- `weapon`
|
||||||
|
- `armor`
|
||||||
|
- `relic`
|
||||||
|
- `material`
|
||||||
|
- `piece:weapon`
|
||||||
|
- `world:xianxia`
|
||||||
|
|
||||||
|
这些更适合保留为筛选、配方、掉落、编辑器过滤用的结构标签。
|
||||||
|
|
||||||
|
### 2.2 标签分层
|
||||||
|
|
||||||
|
建议把标签拆成三层:
|
||||||
|
|
||||||
|
| 层级 | 用途 | 示例 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 结构标签 | 分类、筛选、配方、存档兼容 | `weapon`、`armor`、`material`、`set:steel` |
|
||||||
|
| 战斗语义标签 | 进入 build 相似度和伤害修正 | `快剑`、`突进`、`护体`、`雷法` |
|
||||||
|
| 生态/素材标签 | 掉落、锻造、配方倾向 | `矿道`、`雾林`、`寒铁`、`星砂` |
|
||||||
|
|
||||||
|
推荐约束:
|
||||||
|
|
||||||
|
- `InventoryItem.tags` 继续保留结构标签与通用筛选标签。
|
||||||
|
- `ItemBuildProfile.tags` 只保留会进入 build 计算的语义标签。
|
||||||
|
- `MonsterPreset.habitatTags` 保留生态用途,不直接参与伤害。
|
||||||
|
- 新增 `combatTags` 给角色/怪物,用于战斗 build。
|
||||||
|
|
||||||
|
### 2.3 中英混用兼容
|
||||||
|
|
||||||
|
当前素材生成中已有较多英文 role/tag,角色 UI 和自定义世界中又偏中文标签,因此需要加一层标签规范化。
|
||||||
|
|
||||||
|
建议建立 `buildTagRegistry`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type BuildTagDefinition = {
|
||||||
|
id: string; // ASCII 稳定 id,例如 "quickblade"
|
||||||
|
label: string; // 展示名,例如 "快剑"
|
||||||
|
category: "role" | "style" | "resource" | "element" | "defense" | "craft";
|
||||||
|
aliases: string[]; // 兼容 assassin / duelist / 快剑流 等历史写法
|
||||||
|
description: string; // 供 embedding 使用
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
示例映射:
|
||||||
|
|
||||||
|
| 现有值 | 规范标签 |
|
||||||
|
| --- | --- |
|
||||||
|
| `assassin` | `快袭` / `切后` / `突进` |
|
||||||
|
| `duelist` | `快剑` / `连段` / `对拼` |
|
||||||
|
| `vanguard` | `先锋` / `稳压` / `护体` |
|
||||||
|
| `ward` | `守御` / `镇邪` / `护体` |
|
||||||
|
| `berserker` | `压血` / `重击` / `爆发` |
|
||||||
|
| `caster` | `法修` / `法力` / `远程` |
|
||||||
|
| `support` | `护持` / `回复` / `增益` |
|
||||||
|
| `fortress` | `重甲` / `格挡` / `反击` |
|
||||||
|
| `fate` | `命纹` / `机缘` / `冷却` |
|
||||||
|
| `commander` | `统御` / `均衡` / `队伍增益` |
|
||||||
|
|
||||||
|
## 3. 标签来源设计
|
||||||
|
|
||||||
|
### 3.1 装备标签
|
||||||
|
|
||||||
|
装备是 build 的主来源,但要区分“结构标签”和“build 标签”。
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 武器、护甲、饰品各自最多提供 `2` 个核心 build 标签。
|
||||||
|
- 若装备存在 `setId`,运行时根据套装件数额外生成“合成标签”。
|
||||||
|
- 同一标签在多个装备上重复出现时,只记一次,不按件数无限叠加。
|
||||||
|
|
||||||
|
装备运行时推荐结构:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type RuntimeEquipmentBuildSource = {
|
||||||
|
itemId: string;
|
||||||
|
slot: "weapon" | "armor" | "relic";
|
||||||
|
coreTags: string[];
|
||||||
|
setId?: string;
|
||||||
|
setName?: string;
|
||||||
|
forgeRank?: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 角色/怪物标签
|
||||||
|
|
||||||
|
角色和怪物都应该有自己的 `combatTags`。
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 角色固定提供 `2~3` 个核心战斗语义标签。
|
||||||
|
- 怪物固定提供 `2~3` 个核心战斗语义标签。
|
||||||
|
- `habitatTags` 不直接参与 build 伤害,而是决定掉落材料簇、锻造路线和部分弱点设计。
|
||||||
|
|
||||||
|
例子:
|
||||||
|
|
||||||
|
- 剑系角色:`快剑`、`突进`、`压制`
|
||||||
|
- 重甲怪物:`重甲`、`震击`、`守势`
|
||||||
|
- 雾林怪物生态标签:`雾林`、`湿毒`、`潜伏`
|
||||||
|
- 其中 `湿毒`、`潜伏` 可提升为 `combatTags`
|
||||||
|
- `雾林` 保留为生态/掉落标签
|
||||||
|
|
||||||
|
### 3.3 Buff 标签
|
||||||
|
|
||||||
|
buff 标签是 build 的短时放大器,也是技能与物品进入构筑闭环的关键。
|
||||||
|
|
||||||
|
buff 标签来源:
|
||||||
|
|
||||||
|
- 技能施加的限回合标签
|
||||||
|
- 消耗品施加的限回合标签
|
||||||
|
- 锻造出的铭刻/附魔效果施加的限回合标签
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type TimedBuildBuff = {
|
||||||
|
id: string;
|
||||||
|
sourceType: "skill" | "item" | "forge";
|
||||||
|
sourceId: string;
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
durationTurns: number;
|
||||||
|
maxStacks?: number;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐时长:
|
||||||
|
|
||||||
|
- 强爆发 buff:`1~2` 回合
|
||||||
|
- 节奏/机动 buff:`2~3` 回合
|
||||||
|
- 防御/续航 buff:`2~4` 回合
|
||||||
|
|
||||||
|
重复规则:
|
||||||
|
|
||||||
|
- 同名 buff 默认刷新持续时间,不新增一套完全重复标签。
|
||||||
|
- 不同来源但规范化后相同的标签,只保留一个 build 标签实例。
|
||||||
|
- 重复来源只提高优先级或刷新,不增加额外 `+1` 基础值。
|
||||||
|
|
||||||
|
## 4. 语义 embedding 与套装 build 形成方式
|
||||||
|
|
||||||
|
### 4.1 为什么要引入 embedding
|
||||||
|
|
||||||
|
如果只靠显式 `setId`,build 会很死板。
|
||||||
|
|
||||||
|
引入 embedding 后,可以让这些组合自然成型:
|
||||||
|
|
||||||
|
- `快剑` + `突进` + `追击` + `风行`
|
||||||
|
- `重甲` + `护体` + `守御` + `反击`
|
||||||
|
- `雷法` + `法器` + `过载` + `法力`
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 显式套装仍然存在
|
||||||
|
- 隐式语义套装也能成立
|
||||||
|
- 两者都走同一套 build 标签计算,不必再写第二套特殊规则
|
||||||
|
|
||||||
|
### 4.2 embedding 计算对象
|
||||||
|
|
||||||
|
不要对每个物品实例现算 embedding,而是只对“规范标签定义”计算。
|
||||||
|
|
||||||
|
推荐流程:
|
||||||
|
|
||||||
|
1. 为 `buildTagRegistry` 中每个规范标签生成一条 embedding 文本。
|
||||||
|
2. 文本内容由 `label + aliases + description` 组成。
|
||||||
|
3. 预计算标签相似度矩阵并缓存到本地数据文件。
|
||||||
|
4. 运行时只读相似度,不现算模型。
|
||||||
|
|
||||||
|
示例 embedding 文本:
|
||||||
|
|
||||||
|
```text
|
||||||
|
快剑:以高速轻兵器、连续出手、贴身追击为核心的近战风格;别名 duelist, swift blade, 快袭。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 相似度函数
|
||||||
|
|
||||||
|
建议使用余弦相似度,并且只保留正向相似。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
similarity(a, b) = max(0, cosine(embedding(a), embedding(b)))
|
||||||
|
```
|
||||||
|
|
||||||
|
建议阈值:
|
||||||
|
|
||||||
|
- `< 0.35`:视为无关,按 `0`
|
||||||
|
- `0.35 ~ 0.65`:弱协同
|
||||||
|
- `0.65 ~ 0.82`:强协同
|
||||||
|
- `> 0.82`:视为同 build 簇强关联
|
||||||
|
|
||||||
|
### 4.4 语义套装簇形成
|
||||||
|
|
||||||
|
推荐同时保留两种 build 成型路径:
|
||||||
|
|
||||||
|
#### A. 显式套装
|
||||||
|
|
||||||
|
- 依据 `setId`
|
||||||
|
- 2 件时生成一个合成标签:`套装:<setName>`
|
||||||
|
- 3 件时再生成一个进阶标签:`宗匠:<setName>`
|
||||||
|
|
||||||
|
由于当前 runtime 只有三槽,所以 2 件和 3 件阈值最合适。
|
||||||
|
|
||||||
|
#### B. 隐式语义套装
|
||||||
|
|
||||||
|
当激活标签里存在 `3` 个及以上标签两两相似度超过阈值时,形成“语义 build 簇”。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `快剑`
|
||||||
|
- `突进`
|
||||||
|
- `追击`
|
||||||
|
- `风行`
|
||||||
|
|
||||||
|
它们无需拥有相同 `setId`,也能形成一组高相似 build。
|
||||||
|
|
||||||
|
## 5. 标签修正值与伤害公式
|
||||||
|
|
||||||
|
### 5.1 核心规则
|
||||||
|
|
||||||
|
用户给出的核心要求是:
|
||||||
|
|
||||||
|
- 每个标签修正值都是 `1`
|
||||||
|
- 每多一个标签修正值加一
|
||||||
|
- 同时所有标签的 `1` 需要额外乘上与新标签的相似度
|
||||||
|
- 修正结果作用于输出伤害
|
||||||
|
|
||||||
|
将这条规则做成稳定、顺序无关的公式,可以写成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rawBuildScore(T) = |T| + Σ similarity(t_i, t_j)
|
||||||
|
(i < j)
|
||||||
|
```
|
||||||
|
|
||||||
|
其中:
|
||||||
|
|
||||||
|
- `|T|` 是激活标签数量,每个标签天然贡献 `1`
|
||||||
|
- 每对标签之间再贡献一段相似度分
|
||||||
|
|
||||||
|
这个公式与“新增一个标签时,额外 +1,并让旧标签的 1 再乘上与新标签的相似度”完全等价。
|
||||||
|
|
||||||
|
对应的增量写法:
|
||||||
|
|
||||||
|
```text
|
||||||
|
score_1 = 1
|
||||||
|
score_k = score_(k-1) + 1 + Σ similarity(t_i, t_k)
|
||||||
|
(1 <= i < k)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 激活标签集的选取
|
||||||
|
|
||||||
|
为了防止标签爆炸,建议运行时只取 `MAX_ACTIVE_BUILD_TAGS = 8`。
|
||||||
|
|
||||||
|
推荐优先级:
|
||||||
|
|
||||||
|
1. 限回合 buff 标签
|
||||||
|
2. 角色/怪物核心标签
|
||||||
|
3. 武器 build 标签
|
||||||
|
4. 饰品 build 标签
|
||||||
|
5. 护甲 build 标签
|
||||||
|
6. 2 件/3 件套装合成标签
|
||||||
|
|
||||||
|
重复标签去重后再参与计算。
|
||||||
|
|
||||||
|
### 5.3 从 raw score 到伤害倍率
|
||||||
|
|
||||||
|
`rawBuildScore` 不直接等于伤害倍率,否则会过大。建议再做一层线性缩放与封顶。
|
||||||
|
|
||||||
|
```text
|
||||||
|
buildDamageBonus = clamp((rawBuildScore - 1) * 0.03, 0, 0.45)
|
||||||
|
buildDamageMultiplier = 1 + buildDamageBonus
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐解释:
|
||||||
|
|
||||||
|
- 单标签时没有 build 成型,不给明显收益
|
||||||
|
- 2~4 标签形成初步风格,获得 5%~18% 左右伤害增益
|
||||||
|
- 5~8 标签形成成熟 build,伤害增益逐步逼近 45% 上限
|
||||||
|
|
||||||
|
### 5.4 与当前战斗公式的接法
|
||||||
|
|
||||||
|
当前战斗中,伤害基本来自:
|
||||||
|
|
||||||
|
- 技能基础伤害
|
||||||
|
- `functionEffect.damageMultiplier`
|
||||||
|
- 装备 stat 值
|
||||||
|
|
||||||
|
建议统一改成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
finalOutgoingDamage =
|
||||||
|
round(
|
||||||
|
baseSkillDamage
|
||||||
|
* functionDamageMultiplier
|
||||||
|
* equipmentStatMultiplier
|
||||||
|
* buildDamageMultiplier
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `equipmentStatMultiplier` 来自武器/护甲/饰品的 `statProfile`
|
||||||
|
- `buildDamageMultiplier` 来自标签相似度 build
|
||||||
|
- 先乘完再 `round`
|
||||||
|
|
||||||
|
本期先只影响“输出伤害”。
|
||||||
|
|
||||||
|
也就是:
|
||||||
|
|
||||||
|
- 玩家攻击怪物时用玩家侧标签
|
||||||
|
- 同伴攻击怪物时用同伴侧标签
|
||||||
|
- 怪物攻击玩家/同伴时用怪物侧标签
|
||||||
|
|
||||||
|
防御端 build 抵抗可以放到下一期,不必一开始就加复杂。
|
||||||
|
|
||||||
|
### 5.5 示例
|
||||||
|
|
||||||
|
某角色当前激活标签为:
|
||||||
|
|
||||||
|
- `快剑`
|
||||||
|
- `突进`
|
||||||
|
- `追击`
|
||||||
|
- `风行`
|
||||||
|
- `套装:百炼争锋`
|
||||||
|
|
||||||
|
假设相似度如下:
|
||||||
|
|
||||||
|
- `快剑-突进 = 0.82`
|
||||||
|
- `快剑-追击 = 0.78`
|
||||||
|
- `快剑-风行 = 0.65`
|
||||||
|
- `突进-追击 = 0.80`
|
||||||
|
- `突进-风行 = 0.72`
|
||||||
|
- `追击-风行 = 0.70`
|
||||||
|
- 套装标签与前四者平均相似度 `0.76`
|
||||||
|
|
||||||
|
则:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rawBuildScore
|
||||||
|
= 5
|
||||||
|
+ (0.82 + 0.78 + 0.65 + 0.80 + 0.72 + 0.70)
|
||||||
|
+ (0.76 * 4)
|
||||||
|
= 11.51
|
||||||
|
|
||||||
|
buildDamageBonus
|
||||||
|
= clamp((11.51 - 1) * 0.03, 0, 0.45)
|
||||||
|
= 0.3153
|
||||||
|
|
||||||
|
buildDamageMultiplier = 1.3153
|
||||||
|
```
|
||||||
|
|
||||||
|
若本次技能基础伤害为 `28`,动作倍率为 `1.2`,装备数值倍率为 `1.14`,则:
|
||||||
|
|
||||||
|
```text
|
||||||
|
finalDamage = round(28 * 1.2 * 1.14 * 1.3153) = 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 合成 / 锻造 / 回收闭环设计
|
||||||
|
|
||||||
|
### 6.1 闭环目标
|
||||||
|
|
||||||
|
让当前已有入口真正连起来:
|
||||||
|
|
||||||
|
- 怪物掉落
|
||||||
|
- 宝藏奖励
|
||||||
|
- NPC 交易
|
||||||
|
- 背包积累
|
||||||
|
- 装备更替
|
||||||
|
- 消耗品 buff
|
||||||
|
- 锻造与回收
|
||||||
|
|
||||||
|
形成闭环后,物品系统就不再只是“剧情资源池”,而是成长系统。
|
||||||
|
|
||||||
|
### 6.2 资源分层
|
||||||
|
|
||||||
|
建议把资源拆成 4 类:
|
||||||
|
|
||||||
|
| 类型 | 作用 | 当前可复用入口 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 基础材料 | 进入配方、升级、重铸 | 怪物掉落、宝藏、NPC 交易 |
|
||||||
|
| 标签精粹 | 决定 build 方向 | 拆解装备、精炼材料 |
|
||||||
|
| 套装蓝图 | 决定显式 setId 结果 | 宝藏、精英怪、任务 |
|
||||||
|
| 催化剂 | 提升稀有度、锁词条、转流派 | 稀有品、商店、Boss 掉落 |
|
||||||
|
|
||||||
|
### 6.3 推荐闭环
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["遭遇 / 战斗 / 宝藏 / 交易"] --> B["获得装备 / 材料 / 消耗品 / 蓝图"]
|
||||||
|
B --> C["直接装备"]
|
||||||
|
B --> D["拆解回收"]
|
||||||
|
D --> E["基础材料"]
|
||||||
|
D --> F["标签精粹"]
|
||||||
|
D --> G["蓝图碎片"]
|
||||||
|
E --> H["合成精炼材料"]
|
||||||
|
F --> H
|
||||||
|
G --> I["完整蓝图"]
|
||||||
|
H --> J["锻造新装备"]
|
||||||
|
I --> J
|
||||||
|
J --> K["重铸 / 淬炼 / 铭刻"]
|
||||||
|
K --> C
|
||||||
|
C --> L["形成 build 与伤害提升"]
|
||||||
|
L --> A
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 关键环节
|
||||||
|
|
||||||
|
#### A. 拆解
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 回收无用装备
|
||||||
|
- 提取 build 倾向
|
||||||
|
- 让玩家可以主动转流派
|
||||||
|
|
||||||
|
建议产出:
|
||||||
|
|
||||||
|
- 基础材料:按装备部位与稀有度产出
|
||||||
|
- 标签精粹:按 `buildProfile.tags` 产出对应流派精粹
|
||||||
|
- 套装碎片:带 `setId` 的装备有概率掉落
|
||||||
|
|
||||||
|
推荐规则:
|
||||||
|
|
||||||
|
- 普通/优秀:返还 `1~2` 基础材料 + `1` 标签精粹
|
||||||
|
- 稀有/史诗:返还 `2~4` 基础材料 + `1~2` 标签精粹
|
||||||
|
- 传说:返还 `4+` 基础材料 + `2~3` 标签精粹 + 套装碎片
|
||||||
|
|
||||||
|
#### B. 合成
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 把零散材料往更高层资源转化
|
||||||
|
|
||||||
|
推荐配方:
|
||||||
|
|
||||||
|
- `3` 个同系基础材料 -> `1` 个精炼材料
|
||||||
|
- `2` 个相近标签精粹 -> `1` 个簇催化剂
|
||||||
|
- `3` 个蓝图碎片 -> `1` 个完整蓝图
|
||||||
|
|
||||||
|
#### C. 锻造
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 制造三槽位装备
|
||||||
|
- 明确把材料生态和 build 流派联系起来
|
||||||
|
|
||||||
|
建议锻造公式:
|
||||||
|
|
||||||
|
```text
|
||||||
|
成品 = 槽位模板 + 基础材料 + 标签精粹 + 蓝图/催化剂
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐规则:
|
||||||
|
|
||||||
|
- 武器:决定主要输出 build 标签
|
||||||
|
- 护甲:决定生存/反击/续战方向
|
||||||
|
- 饰品:决定资源、冷却、机动、控场补短板
|
||||||
|
|
||||||
|
#### D. 重铸
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 保留 build 主方向
|
||||||
|
- 调整副标签
|
||||||
|
|
||||||
|
推荐规则:
|
||||||
|
|
||||||
|
- 保留主标签与 `setId`
|
||||||
|
- 只在同一语义簇内重掷副标签
|
||||||
|
- 花费标签精粹 + 货币
|
||||||
|
|
||||||
|
这样玩家不会因为一次重铸直接从 `快剑` 跳成 `重甲`。
|
||||||
|
|
||||||
|
#### E. 淬炼
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 提升已有装备而不是频繁换装
|
||||||
|
|
||||||
|
推荐效果:
|
||||||
|
|
||||||
|
- 提升 `forgeRank`
|
||||||
|
- 增加 `statProfile`
|
||||||
|
- 提高套装合成标签的优先级
|
||||||
|
|
||||||
|
#### F. 铭刻 / 附魔
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 让消耗品和锻造形成直接关系
|
||||||
|
|
||||||
|
推荐效果:
|
||||||
|
|
||||||
|
- 给装备附加可触发 buff 标签
|
||||||
|
- 或直接生产“一次性战斗铭符”
|
||||||
|
|
||||||
|
例子:
|
||||||
|
|
||||||
|
- `疾风符`:`2` 回合获得 `风行`、`突进`
|
||||||
|
- `镇岳印`:`2` 回合获得 `护体`、`守御`
|
||||||
|
- `雷纹油`:`1` 回合获得 `雷法`、`过载`
|
||||||
|
|
||||||
|
## 7. 当前三槽系统下的 build 形态
|
||||||
|
|
||||||
|
由于当前只有 `weapon / armor / relic` 三槽,建议本期 build 以“2 件成型、3 件毕业”为主。
|
||||||
|
|
||||||
|
### 7.1 套装成型方式
|
||||||
|
|
||||||
|
- 1 件:只提供本件核心标签
|
||||||
|
- 2 件:生成 `套装:<setName>` 合成标签
|
||||||
|
- 3 件:再生成 `宗匠:<setName>` 进阶标签
|
||||||
|
|
||||||
|
这两个“合成标签”本质上也是普通 build 标签,因此会自动进入 embedding 相似度和伤害公式。
|
||||||
|
|
||||||
|
### 7.2 推荐 build archetype
|
||||||
|
|
||||||
|
| build | 角色/怪物核心标签 | 装备标签方向 | buff 标签方向 | 锻造材料倾向 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 快剑追袭 | `快剑`、`突进`、`追击` | 武器补 `连段`,饰品补 `风行` | `疾风`、`破绽` | `百炼钢`、`风羽`、`轻皮` |
|
||||||
|
| 重甲反击 | `守御`、`重甲`、`反击` | 护甲补 `护体`,饰品补 `镇势` | `立壁`、`嘲压` | `寒铁`、`壳片`、`岩核` |
|
||||||
|
| 雷法过载 | `法修`、`雷法`、`法力` | 武器补 `过载`,饰品补 `聚灵` | `引雷`、`蓄放` | `星砂`、`雷纹石`、`灵晶` |
|
||||||
|
| 命纹机缘 | `命纹`、`冷却`、`机缘` | 饰品补 `连锁`,护甲补 `续战` | `转运`、`回转` | `命纹骨片`、`旧卷`、`秘印` |
|
||||||
|
| 医理续战 | `回复`、`护持`、`续战` | 护甲补 `守御`,饰品补 `凝神` | `回气`、`清心` | `灵木`、`药囊`、`泉露` |
|
||||||
|
|
||||||
|
## 8. 与当前掉落和地图生态的关系
|
||||||
|
|
||||||
|
### 8.1 怪物生态标签不直接进伤害
|
||||||
|
|
||||||
|
当前怪物已有 `habitatTags`,更适合驱动:
|
||||||
|
|
||||||
|
- 掉落材料倾向
|
||||||
|
- 可锻造流派倾向
|
||||||
|
- 宝藏/地图资源分布
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `矿道 / 铸坊 / 废城` -> `寒铁`、`锻火`、`重甲`
|
||||||
|
- `雾林 / 沼泽` -> `毒囊`、`湿毒`、`潜伏`
|
||||||
|
- `祭坛 / 遗迹 / 古迹` -> `残卷`、`纹石`、`镇邪`
|
||||||
|
- `月湖 / 灵泉 / 天河` -> `水灵`、`回气`、`法力`
|
||||||
|
|
||||||
|
### 8.2 当前掉落表可直接扩展
|
||||||
|
|
||||||
|
当前怪物掉落里已经有很多适合闭环的原型:
|
||||||
|
|
||||||
|
- `armor + material`
|
||||||
|
- `weapon + material`
|
||||||
|
- `relic + mana`
|
||||||
|
- `consumable + material`
|
||||||
|
|
||||||
|
下一步不必推翻,只要再补:
|
||||||
|
|
||||||
|
1. 掉落物的 `craftTags`
|
||||||
|
2. 掉落物的 `buildProfile`
|
||||||
|
3. 拆解产物表
|
||||||
|
|
||||||
|
就能把这些现有掉落自然接进锻造循环。
|
||||||
|
|
||||||
|
## 9. 数据结构建议
|
||||||
|
|
||||||
|
### 9.1 类型扩展
|
||||||
|
|
||||||
|
建议在现有结构上最小扩展:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type BuildTagCategory =
|
||||||
|
| "role"
|
||||||
|
| "style"
|
||||||
|
| "resource"
|
||||||
|
| "defense"
|
||||||
|
| "element"
|
||||||
|
| "craft";
|
||||||
|
|
||||||
|
interface BuildTagDefinition {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
category: BuildTagCategory;
|
||||||
|
aliases: string[];
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemBuildProfile {
|
||||||
|
role: string;
|
||||||
|
tags: string[]; // 只放战斗语义标签
|
||||||
|
setId?: string;
|
||||||
|
setName?: string;
|
||||||
|
pieceName?: string;
|
||||||
|
synergy?: string[];
|
||||||
|
craftTags?: string[]; // 新增:配方/材料倾向
|
||||||
|
forgeRank?: number; // 新增:淬炼等级
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CombatTaggedActor {
|
||||||
|
combatTags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimedBuildBuff {
|
||||||
|
id: string;
|
||||||
|
sourceType: "skill" | "item" | "forge";
|
||||||
|
sourceId: string;
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
durationTurns: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 GameState 扩展
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface GameState {
|
||||||
|
activeBuildBuffs: TimedBuildBuff[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 技能与物品扩展
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ItemUseProfile {
|
||||||
|
hpRestore?: number;
|
||||||
|
manaRestore?: number;
|
||||||
|
cooldownReduction?: number;
|
||||||
|
buildBuffs?: TimedBuildBuff[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FunctionEffectConfig {
|
||||||
|
damageMultiplier?: number;
|
||||||
|
incomingDamageMultiplier?: number;
|
||||||
|
healAmount?: number;
|
||||||
|
manaRestore?: number;
|
||||||
|
cooldownTickBonus?: number;
|
||||||
|
grantedBuildBuffs?: TimedBuildBuff[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 运行时接入点
|
||||||
|
|
||||||
|
### 10.1 必须新增的规则层
|
||||||
|
|
||||||
|
建议新增 3 个数据/规则文件:
|
||||||
|
|
||||||
|
- `src/data/buildTags.ts`
|
||||||
|
- 标签规范化
|
||||||
|
- alias 映射
|
||||||
|
- 相似度矩阵
|
||||||
|
- `src/data/buildDamage.ts`
|
||||||
|
- 聚合激活标签
|
||||||
|
- 计算 `rawBuildScore`
|
||||||
|
- 计算 `buildDamageMultiplier`
|
||||||
|
- `src/data/forgeRecipes.ts`
|
||||||
|
- 合成、锻造、拆解、重铸配方
|
||||||
|
|
||||||
|
### 10.2 当前代码里的关键接点
|
||||||
|
|
||||||
|
#### A. `src/data/equipmentEffects.ts`
|
||||||
|
|
||||||
|
需要从“空壳”升级为真正读取:
|
||||||
|
|
||||||
|
- `statProfile`
|
||||||
|
- `buildProfile`
|
||||||
|
- 套装件数
|
||||||
|
|
||||||
|
建议这里做:
|
||||||
|
|
||||||
|
- 读取三槽装备 stat 汇总
|
||||||
|
- 计算显式套装件数
|
||||||
|
- 产出装备侧 build 标签源
|
||||||
|
|
||||||
|
#### B. `src/hooks/useCombatFlow.ts`
|
||||||
|
|
||||||
|
这里是 build 进伤害的核心接点。
|
||||||
|
|
||||||
|
当前玩家、同伴、怪物都有单独 `damage = ...` 的结算片段,建议统一收敛成:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
resolveOutgoingDamage({
|
||||||
|
actor,
|
||||||
|
target,
|
||||||
|
skill,
|
||||||
|
functionEffect,
|
||||||
|
gameState,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
统一处理:
|
||||||
|
|
||||||
|
- actor 的 `combatTags`
|
||||||
|
- actor 装备 build 标签
|
||||||
|
- actor 当前限回合 buff 标签
|
||||||
|
- 显式套装合成标签
|
||||||
|
- embedding build 伤害修正
|
||||||
|
|
||||||
|
#### C. `src/data/characterPresets.ts`
|
||||||
|
|
||||||
|
角色需要正式持有 `combatTags`,不要再只放在 UI 常量里。
|
||||||
|
|
||||||
|
#### D. `src/data/monsterPresets.ts`
|
||||||
|
|
||||||
|
怪物新增:
|
||||||
|
|
||||||
|
- `combatTags`
|
||||||
|
- `craftTags`
|
||||||
|
|
||||||
|
保留:
|
||||||
|
|
||||||
|
- `habitatTags`
|
||||||
|
|
||||||
|
#### E. `src/hooks/useGamePersistence.ts`
|
||||||
|
|
||||||
|
存档兼容必须同步补:
|
||||||
|
|
||||||
|
- `activeBuildBuffs`
|
||||||
|
- 新字段默认值
|
||||||
|
- 旧存档自动补空数组
|
||||||
|
|
||||||
|
## 11. 编辑器与策划工作流
|
||||||
|
|
||||||
|
结合当前项目“先补类型和规则,再补 UI”的经验,编辑器建议按下面顺序补。
|
||||||
|
|
||||||
|
### 11.1 标签注册表
|
||||||
|
|
||||||
|
先做一个统一标签注册表,再让各编辑器引用它。
|
||||||
|
|
||||||
|
编辑器最少应支持:
|
||||||
|
|
||||||
|
- 选择规范标签
|
||||||
|
- 查看别名
|
||||||
|
- 查看所属 build 簇
|
||||||
|
- 查看与其他标签的相似度
|
||||||
|
|
||||||
|
### 11.2 Item Catalog Editor
|
||||||
|
|
||||||
|
当前它已经能展示 `buildProfile`,下一步建议补:
|
||||||
|
|
||||||
|
- 规范标签选择器
|
||||||
|
- 当前装备可形成的 build 预览
|
||||||
|
- 2 件/3 件套装合成标签预览
|
||||||
|
- 拆解产物预览
|
||||||
|
|
||||||
|
### 11.3 角色/怪物编辑
|
||||||
|
|
||||||
|
建议把角色和怪物的 `combatTags` 录入正式数据,不再只放展示文案。
|
||||||
|
|
||||||
|
### 11.4 相似度预计算脚本
|
||||||
|
|
||||||
|
建议加一个脚本,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
scripts/build-tag-similarity.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
负责:
|
||||||
|
|
||||||
|
- 读取标签注册表
|
||||||
|
- 生成 embedding
|
||||||
|
- 输出相似度矩阵 JSON
|
||||||
|
|
||||||
|
这样运行时就不需要联网或现算。
|
||||||
|
|
||||||
|
## 12. 数值与反滥用约束
|
||||||
|
|
||||||
|
为了让系统长期可控,建议一开始就加以下约束:
|
||||||
|
|
||||||
|
1. 同规范标签去重,不允许同词条多件装备无限叠加基础 `+1`。
|
||||||
|
2. 激活标签上限 `8`,避免后期词条爆炸。
|
||||||
|
3. 伤害 build 增益封顶 `45%`,防止纯标签乘爆。
|
||||||
|
4. 2 件/3 件套只生成合成标签,不再额外套一层独立乘区,避免双重膨胀。
|
||||||
|
5. `habitatTags` 不直接进伤害,避免出现“矿道标签提高输出”这种语义跑偏。
|
||||||
|
6. buff 标签以刷新时长为主,不以无限叠层为主。
|
||||||
|
7. 重铸只在语义近邻内滚动,避免 build 完全变异。
|
||||||
|
8. 拆解返还率不要超过完整锻造成本的 `60%~70%`,避免无限刷循环。
|
||||||
|
|
||||||
|
## 13. 分阶段实施建议
|
||||||
|
|
||||||
|
按照当前项目文档里已经验证过的开发经验,推荐顺序是“类型 -> 规则 -> hook -> UI”,不要反过来。
|
||||||
|
|
||||||
|
### 阶段 A:先补数据骨架
|
||||||
|
|
||||||
|
- 新增 `buildTagRegistry`
|
||||||
|
- 为角色/怪物补 `combatTags`
|
||||||
|
- 为物品补 `craftTags / forgeRank`
|
||||||
|
- 为 `GameState` 补 `activeBuildBuffs`
|
||||||
|
|
||||||
|
### 阶段 B:再补规则
|
||||||
|
|
||||||
|
- 实现标签规范化
|
||||||
|
- 实现 embedding 相似度矩阵
|
||||||
|
- 实现 `rawBuildScore`
|
||||||
|
- 实现三槽位显式套装合成标签
|
||||||
|
- 实现 buff 标签衰减
|
||||||
|
|
||||||
|
### 阶段 C:接入战斗
|
||||||
|
|
||||||
|
- `useCombatFlow.ts` 改为统一伤害入口
|
||||||
|
- 玩家 / 同伴 / 怪物统一读取 build
|
||||||
|
- `equipmentEffects.ts` 正式生效
|
||||||
|
|
||||||
|
### 阶段 D:补合成/锻造闭环
|
||||||
|
|
||||||
|
- 拆解
|
||||||
|
- 合成
|
||||||
|
- 锻造
|
||||||
|
- 重铸
|
||||||
|
- 铭刻
|
||||||
|
|
||||||
|
### 阶段 E:最后补编辑器与 UI
|
||||||
|
|
||||||
|
- 物品 build 预览
|
||||||
|
- 套装预览
|
||||||
|
- 锻造页
|
||||||
|
- 材料来源与标签簇提示
|
||||||
|
|
||||||
|
## 14. 一句话总结
|
||||||
|
|
||||||
|
本方案的核心不是单独增加“套装数值”,而是把装备、角色/怪物、限回合 buff 都转成统一的语义 build 标签,再用“每标签基础值 1 + 标签间 embedding 相似度”的方式形成构筑强度,并把这份构筑强度直接接进当前伤害输出,同时让怪物掉落、宝藏、交易、拆解、合成、锻造、铭刻围绕同一套标签体系形成闭环。
|
||||||
@@ -0,0 +1,887 @@
|
|||||||
|
# 等级成长、章节经验节奏与 NPC 自动定级设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 实现进度(2026-04-20 第一批)
|
||||||
|
|
||||||
|
当前仓库已按本设计先落地第一批稳定能力:
|
||||||
|
|
||||||
|
1. 已新增 `playerProgression` 正式成长状态,包含等级、当前等级经验、总经验与下级阈值。
|
||||||
|
2. 已新增等级基准与经验结算服务,并接入前后端存档归一化,旧存档默认回填为 `Lv.1 / 0 XP`。
|
||||||
|
3. 已给 `QuestReward` 补上 `experience`,新生成任务会按当前等级与任务结构给出任务经验。
|
||||||
|
4. 已将 Express 后端 `npc_quest_turn_in` 接入经验发放与升级处理,任务交付结果会反馈 `经验 +N` 与升级信息。
|
||||||
|
5. 已在冒险主面板补充最小等级展示:`Lv.` 与细经验条;任务奖励面板可看到经验数值。
|
||||||
|
6. 已收回任务日志里的直接领奖入口,任务奖励结算当前以 NPC 交付链路为准。
|
||||||
|
|
||||||
|
## 实现进度(2026-04-20 第二批)
|
||||||
|
|
||||||
|
当前仓库已继续落地第二批成长能力:
|
||||||
|
|
||||||
|
1. 已给运行时敌对 NPC / 战斗遭遇补上 `levelProfile` 与 `experienceReward`,前后端快照、战斗态和恢复链路会保留这组元数据。
|
||||||
|
2. 已新增敌对成长解析服务,当前先以玩家当前等级为 fallback,为 `npc_fight` / 敌对战斗入口自动生成等级、参考强度、战斗生命值与击杀经验。
|
||||||
|
3. 已将 Express 后端战斗胜利结算接入 `hostile_npc` 经验发放,击败敌对 NPC 后会直接更新 `playerProgression`,并写回 `hostileNpcsDefeated` 统计。
|
||||||
|
4. 已在战斗画布中补上敌对 NPC 的最小 `Lv.` 徽标展示,保持 UI 极简表达。
|
||||||
|
|
||||||
|
## 实现进度(2026-04-20 第三批)
|
||||||
|
|
||||||
|
当前仓库已继续落地第三批“章节预算 / 自动定级”能力:
|
||||||
|
|
||||||
|
1. 已新增服务端 `chapterProgressionPlanner`,会基于 `sceneChapterBlueprints` 编译每章的 `entry / exit pseudo level`、总经验预算、任务经验份额、敌对经验份额与预计击杀数。
|
||||||
|
2. 已新增 `npcLevelResolver`,会根据当前章节阶段和当前 act 的 `primaryNpcId` 自动区分 `hostile_standard / hostile_elite / hostile_boss / rival`,并输出 `source = chapter_auto` 的等级档案。
|
||||||
|
3. 已将 `npc_fight` / `npc_spar` 开战入口接入章节上下文解析;当运行时存在章节蓝图、当前章和当前 act 信息时,敌对 NPC 不再只跟随玩家当前等级,而会按章节自动定级并生成更贴合本章预算的经验奖励。
|
||||||
|
4. 已补上规划器、定级器与路由级验证,确认同一玩家在不同章节和不同阶段触发敌对战斗时,会得到不同的等级与经验结果。
|
||||||
|
|
||||||
|
本轮仍未落地的部分:
|
||||||
|
|
||||||
|
1. `ChapterExperienceLedger` 的正式持久化、按章实际经验记账与偏差回看还未接入。
|
||||||
|
2. 同章重复刷敌的 `repeatPenalty` 与超预算衰减还未落地,当前仍是“预算规划 + 单次掉落”版本。
|
||||||
|
3. 当前自动定级已优先接入敌对战斗入口,友方 / 环境 NPC 的更广泛等级消费链路仍待继续铺开。
|
||||||
|
|
||||||
|
## 0. 目标
|
||||||
|
|
||||||
|
这次设计解决 5 个必须同时成立的问题:
|
||||||
|
|
||||||
|
1. 玩家需要正式拥有 `等级 / 当前经验 / 总经验 / 升级` 这条成长主链。
|
||||||
|
2. 经验只从两类明确来源进入:
|
||||||
|
- 完成任务
|
||||||
|
- 击败敌对 NPC
|
||||||
|
3. 同等级实体必须具备同一档 `参考强度`,不能再靠散落在各处的静态数值各自漂移。
|
||||||
|
4. 系统需要能按章节评估玩家经验获取速度,而不是只在整体通关后回看“升太快/升太慢”。
|
||||||
|
5. 不同章节里的 NPC 需要按章节目标等级自动定级,保证这一章的敌我强度、经验产出和升级节奏互相闭合。
|
||||||
|
|
||||||
|
一句话结论:
|
||||||
|
|
||||||
|
**等级必须成为后端统一裁决的成长基线;章节必须先产出“目标玩家等级带 + 经验预算”,再由这套预算反推任务经验、击杀经验和本章 NPC 自动等级。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 基于当前仓库的判断
|
||||||
|
|
||||||
|
结合当前代码与文档,现状已经有足够好的骨架,但等级系统这一层还完全缺位。
|
||||||
|
|
||||||
|
### 1.1 已经具备的基础
|
||||||
|
|
||||||
|
1. `src/data/questFlow.ts`
|
||||||
|
|
||||||
|
- 已有 `QuestLogEntry / QuestStep / QuestProgressSignal / chapter quest`。
|
||||||
|
- 已经能把场景章节任务接到运行时主链。
|
||||||
|
|
||||||
|
2. `server-node/src/modules/quest/questStoryActionService.ts`
|
||||||
|
|
||||||
|
- 已经把 `接任务 / 交任务` 收回后端。
|
||||||
|
- 任务结算时已经集中处理货币、背包、好感变化。
|
||||||
|
|
||||||
|
3. `server-node/src/modules/quest/questRuntimeSignalService.ts`
|
||||||
|
|
||||||
|
- 已经会在 `npc_chat / 击败敌对 NPC / 宝藏 / 切磋` 后投递 quest signal。
|
||||||
|
|
||||||
|
4. `src/services/storyEngine/chapterDirector.ts`
|
||||||
|
|
||||||
|
- 已经能用当前场景章节任务推导 `opening -> expansion -> turning_point -> climax -> aftermath`。
|
||||||
|
|
||||||
|
5. `src/types/customWorld.ts`
|
||||||
|
|
||||||
|
- 已经有 `sceneChapterBlueprints`,说明章节顺序、幕推进和 NPC 编排已经有正式挂点。
|
||||||
|
|
||||||
|
6. `src/types/attributes.ts`、`src/data/hostileNpcPresets.ts`
|
||||||
|
- 已经有统一属性画像、怪物/NPC 统一实体方向。
|
||||||
|
- 当前敌对实体已有 `baseStats / attributeProfile / behaviorVectors`,可以继续向“同级同参考强度”收束。
|
||||||
|
|
||||||
|
### 1.2 当前缺口
|
||||||
|
|
||||||
|
当前最核心的缺口有 6 个:
|
||||||
|
|
||||||
|
1. `GameState` 没有玩家等级成长状态。
|
||||||
|
2. `QuestReward` 没有经验字段。
|
||||||
|
3. `SceneHostileNpc / SceneNpc` 没有正式等级和击杀经验字段。
|
||||||
|
4. 当前 hostile preset 的 `hp/maxHp` 仍是静态绝对值,不受章节节奏控制。
|
||||||
|
5. 章节系统没有“本章目标入场等级 / 出章等级 / 经验预算”的结构。
|
||||||
|
6. 没有“按章节自动定级”的编译器,也没有“本章经验是否超发/欠发”的记账面板。
|
||||||
|
|
||||||
|
一句话总结:
|
||||||
|
|
||||||
|
**现在仓库里已经有章节、任务、NPC 和属性系统,但还没有“成长预算层”,所以强度、奖励和章节节奏仍然缺少同一把尺。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心决策
|
||||||
|
|
||||||
|
## 2.1 等级、经验与 NPC 定级全部由 Express 后端裁决
|
||||||
|
|
||||||
|
必须坚持:
|
||||||
|
|
||||||
|
1. 前端只展示 `等级 / 经验条 / 升级结果 / NPC 等级徽标`。
|
||||||
|
2. 经验发放、升级、章节经验预算、NPC 自动定级全部在 Express 后端计算。
|
||||||
|
3. 前端不本地推演“这次应该升几级”“这个 NPC 应该是多少级”。
|
||||||
|
|
||||||
|
推荐新增领域目录:
|
||||||
|
|
||||||
|
- `server-node/src/modules/progression/`
|
||||||
|
|
||||||
|
建议首批模块:
|
||||||
|
|
||||||
|
- `levelBenchmarks.ts`
|
||||||
|
- `playerProgressionService.ts`
|
||||||
|
- `chapterProgressionPlanner.ts`
|
||||||
|
- `chapterExperienceLedger.ts`
|
||||||
|
- `npcLevelResolver.ts`
|
||||||
|
- `progressionRuntimeSignalService.ts`
|
||||||
|
|
||||||
|
## 2.2 MVP 经验来源只认两类事件
|
||||||
|
|
||||||
|
首版只允许两类正式经验来源:
|
||||||
|
|
||||||
|
1. `quest_turned_in`
|
||||||
|
|
||||||
|
- 任务真正交付时发经验。
|
||||||
|
- 不在“接任务”“任务 ready_to_turn_in”时发经验。
|
||||||
|
|
||||||
|
2. `hostile_npc_defeated`
|
||||||
|
- 仅限敌对 NPC / 怪物胜利结算后发经验。
|
||||||
|
- 不对 `npc_spar_completed`、普通聊天、观察、宝藏直接发经验。
|
||||||
|
|
||||||
|
这样做的原因是:
|
||||||
|
|
||||||
|
1. 最容易和当前后端任务/战斗链路接上。
|
||||||
|
2. 经验来源清晰,便于做章节预算。
|
||||||
|
3. 避免系统一开始就被碎片经验源冲散。
|
||||||
|
|
||||||
|
## 2.3 同等级 = 同参考强度
|
||||||
|
|
||||||
|
这是本次设计最重要的规则:
|
||||||
|
|
||||||
|
1. 等级是所有可比较实体共享的强度基线。
|
||||||
|
2. 同等级玩家、敌对 NPC、可战斗剧情 NPC,必须共享同一档 `参考强度`。
|
||||||
|
3. 世界属性 schema 只决定“强在哪种风格上”,不决定“同级谁天然强一截”。
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- `Lv.8` 的重甲敌人和 `Lv.8` 的迅捷刺客可以打法不同
|
||||||
|
- 但两者的 `参考强度预算` 必须是同一档
|
||||||
|
|
||||||
|
真正的强弱差只允许来自:
|
||||||
|
|
||||||
|
1. 等级差
|
||||||
|
2. 装备 / Build / Buff / Debuff
|
||||||
|
3. 章节中明确声明的 `boss / elite` 角色通过更高等级体现,而不是同级偷加隐藏倍数
|
||||||
|
|
||||||
|
## 2.4 章节先出经验预算,再反推等级
|
||||||
|
|
||||||
|
章节设计从这次开始必须按下面顺序计算:
|
||||||
|
|
||||||
|
```text
|
||||||
|
章节顺序
|
||||||
|
-> 本章玩家目标入场等级 / 出章等级
|
||||||
|
-> 本章总经验预算
|
||||||
|
-> 任务经验份额 / 击杀经验份额
|
||||||
|
-> 本章 NPC 自动等级
|
||||||
|
-> 本章实际经验记账与偏差评估
|
||||||
|
```
|
||||||
|
|
||||||
|
不能反过来先手写一堆 NPC 强度,再看玩家能不能接住。
|
||||||
|
|
||||||
|
## 2.5 UI 只做极简表达
|
||||||
|
|
||||||
|
为了符合当前项目“UI 不默认堆规则说明”的约束,前台只建议新增 4 个轻量展示:
|
||||||
|
|
||||||
|
1. 玩家信息区:
|
||||||
|
|
||||||
|
- `Lv. X`
|
||||||
|
- 一条细经验条
|
||||||
|
|
||||||
|
2. 敌对 NPC 名牌:
|
||||||
|
|
||||||
|
- `Lv. X`
|
||||||
|
|
||||||
|
3. 任务交付结果:
|
||||||
|
|
||||||
|
- `经验 +N`
|
||||||
|
|
||||||
|
4. 升级提示:
|
||||||
|
- 单条 toast 或单行系统反馈
|
||||||
|
|
||||||
|
不在界面里默认放:
|
||||||
|
|
||||||
|
- 经验公式说明
|
||||||
|
- 章节经验预算说明
|
||||||
|
- 等级规则解释文案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据结构设计
|
||||||
|
|
||||||
|
## 3.1 玩家成长状态
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface PlayerProgressionState {
|
||||||
|
level: number;
|
||||||
|
currentLevelXp: number;
|
||||||
|
totalXp: number;
|
||||||
|
xpToNextLevel: number;
|
||||||
|
pendingLevelUps?: number;
|
||||||
|
lastGrantedSource?: 'quest' | 'hostile_npc' | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
挂载位置建议:
|
||||||
|
|
||||||
|
- `src/types/game.ts`
|
||||||
|
- `GameState.playerProgression`
|
||||||
|
|
||||||
|
原则:
|
||||||
|
|
||||||
|
1. 这不是 `runtimeStats` 的一部分。
|
||||||
|
2. `runtimeStats` 继续做统计计数。
|
||||||
|
3. `playerProgression` 是正式玩法状态。
|
||||||
|
|
||||||
|
## 3.2 等级基准表
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface LevelBenchmark {
|
||||||
|
level: number;
|
||||||
|
xpToNextLevel: number;
|
||||||
|
cumulativeXpRequired: number;
|
||||||
|
referenceStrength: number;
|
||||||
|
baseHp: number;
|
||||||
|
baseMana: number;
|
||||||
|
baselineDamageScale: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
单一真相源建议放在:
|
||||||
|
|
||||||
|
- `server-node/src/modules/progression/levelBenchmarks.ts`
|
||||||
|
|
||||||
|
前端只通过后端投影拿结果,不自己保存第二份表。
|
||||||
|
|
||||||
|
## 3.3 实体等级档案
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type ProgressionRole =
|
||||||
|
| 'guide'
|
||||||
|
| 'ambient'
|
||||||
|
| 'support'
|
||||||
|
| 'hostile_standard'
|
||||||
|
| 'hostile_elite'
|
||||||
|
| 'hostile_boss'
|
||||||
|
| 'rival';
|
||||||
|
|
||||||
|
export interface EntityLevelProfile {
|
||||||
|
level: number;
|
||||||
|
referenceStrength: number;
|
||||||
|
chapterId?: string | null;
|
||||||
|
chapterIndex?: number | null;
|
||||||
|
progressionRole: ProgressionRole;
|
||||||
|
source: 'chapter_auto' | 'preset_override' | 'manual';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议接入:
|
||||||
|
|
||||||
|
- `src/types/scene.ts`
|
||||||
|
- `SceneNpc.levelProfile?: EntityLevelProfile`
|
||||||
|
- `SceneHostileNpc.levelProfile?: EntityLevelProfile`
|
||||||
|
|
||||||
|
## 3.4 任务奖励扩展
|
||||||
|
|
||||||
|
建议扩展:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface QuestReward {
|
||||||
|
affinityBonus: number;
|
||||||
|
currency: number;
|
||||||
|
experience: number;
|
||||||
|
items: InventoryItem[];
|
||||||
|
storyHint?: string;
|
||||||
|
intel?: { ... };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
1. 经验是任务奖励的一等字段。
|
||||||
|
2. 经验文本不走 story hint 兜底。
|
||||||
|
3. 任务经验由后端编译,不交给 AI 决定。
|
||||||
|
|
||||||
|
## 3.5 敌对 NPC 经验掉落
|
||||||
|
|
||||||
|
建议扩展:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface SceneHostileNpc {
|
||||||
|
...
|
||||||
|
experienceReward?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
首版只给运行时敌对 NPC 挂经验值,不强行把它沉到所有 preset 原始数据中。
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
1. 经验应该跟章节定级一起编译。
|
||||||
|
2. 同一个 hostile preset 出现在不同章节时,等级和经验都应不同。
|
||||||
|
3. 静态 preset 继续只表达“风格”和“原型”,不再表达最终强度。
|
||||||
|
|
||||||
|
## 3.6 章节成长计划
|
||||||
|
|
||||||
|
建议新增运行时编译结果:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ChapterProgressionPlan {
|
||||||
|
chapterId: string;
|
||||||
|
chapterIndex: number;
|
||||||
|
totalChapters: number;
|
||||||
|
entryPseudoLevel: number;
|
||||||
|
exitPseudoLevel: number;
|
||||||
|
entryLevel: number;
|
||||||
|
exitLevel: number;
|
||||||
|
totalXpBudget: number;
|
||||||
|
questXpBudget: number;
|
||||||
|
hostileXpBudget: number;
|
||||||
|
expectedHostileDefeatCount: number;
|
||||||
|
paceBand: 'opening_fast' | 'steady' | 'pressure' | 'finale_dense';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议作为后端运行时编译结果缓存,不作为创作者直接编辑字段。
|
||||||
|
|
||||||
|
## 3.7 章节经验记账
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface ChapterExperienceLedger {
|
||||||
|
chapterId: string;
|
||||||
|
chapterIndex: number;
|
||||||
|
levelAtEntry: number;
|
||||||
|
levelAtExit?: number | null;
|
||||||
|
plannedTotalXp: number;
|
||||||
|
plannedQuestXp: number;
|
||||||
|
plannedHostileXp: number;
|
||||||
|
actualQuestXp: number;
|
||||||
|
actualHostileXp: number;
|
||||||
|
expectedHostileDefeatCount: number;
|
||||||
|
actualHostileDefeatCount: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
1. 评估每一章经验速度。
|
||||||
|
2. 判断本章是否超发/欠发。
|
||||||
|
3. 为下一轮调参提供依据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 等级曲线与参考强度
|
||||||
|
|
||||||
|
## 4.1 首版等级目标
|
||||||
|
|
||||||
|
首版建议:
|
||||||
|
|
||||||
|
1. 系统支持 `Lv.1 ~ Lv.20`
|
||||||
|
2. 当前主线正常通章目标不是满级
|
||||||
|
3. 标准单轮战役通关目标等级建议落在 `Lv.14 ~ Lv.15`
|
||||||
|
|
||||||
|
这样做的原因是:
|
||||||
|
|
||||||
|
1. 级差足够表达章节成长
|
||||||
|
2. 不会让前期升级过细、后期又没有空间
|
||||||
|
3. 还保留后续营地、精英支线、长期养成的余量
|
||||||
|
|
||||||
|
## 4.2 升级经验公式
|
||||||
|
|
||||||
|
建议基线公式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
xpToNextLevel(level) = 60 + 20 * (level - 1) + 8 * (level - 1) * (level - 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
由此生成 `LevelBenchmark[]`,不在业务代码里散落重复公式。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
1. 前期升级快,便于建立成长反馈
|
||||||
|
2. 中后期门槛逐步拉开,避免章节尾段失控
|
||||||
|
3. 可直接序列化成常量表用于测试
|
||||||
|
|
||||||
|
## 4.3 参考强度公式
|
||||||
|
|
||||||
|
建议基线公式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
referenceStrength(level) =
|
||||||
|
100 + 16 * (level - 1) + 6 * (level - 1) * (level - 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
并同步产出:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
baseHp(level);
|
||||||
|
baseMana(level);
|
||||||
|
baselineDamageScale(level);
|
||||||
|
```
|
||||||
|
|
||||||
|
重要约束:
|
||||||
|
|
||||||
|
1. `referenceStrength` 是同级比较标尺。
|
||||||
|
2. style 只允许在同一档预算内重分布,不允许抬高总强度。
|
||||||
|
3. `elite / boss` 不允许用同级隐藏倍率偷强度,必须通过更高等级体现。
|
||||||
|
|
||||||
|
## 4.4 现有静态数值如何迁移
|
||||||
|
|
||||||
|
当前 `src/data/hostileNpcPresets.ts` 里的:
|
||||||
|
|
||||||
|
- `baseStats.hp`
|
||||||
|
- `baseStats.maxHp`
|
||||||
|
- `speed`
|
||||||
|
- `attackRange`
|
||||||
|
|
||||||
|
不建议继续全部视为最终强度。
|
||||||
|
|
||||||
|
迁移原则:
|
||||||
|
|
||||||
|
1. `attackRange / speed` 继续保留为战斗风格参数。
|
||||||
|
2. `hp / maxHp` 改为“风格形状参考”,最终值由 `等级基准 + 风格分布` 决定。
|
||||||
|
3. 现有 preset 的高血量、高机动、高压制,只用于决定“同级下怎么分布”,不改变同级总参考强度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 经验发放规则
|
||||||
|
|
||||||
|
## 5.1 任务经验
|
||||||
|
|
||||||
|
任务经验只在 `turn_in` 时发放。
|
||||||
|
|
||||||
|
建议公式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
baseQuestXp(targetLevel) = xpToNextLevel(targetLevel) * 0.45;
|
||||||
|
|
||||||
|
questXp =
|
||||||
|
baseQuestXp(targetLevel) *
|
||||||
|
stepCountMultiplier *
|
||||||
|
narrativeTypeMultiplier *
|
||||||
|
urgencyMultiplier;
|
||||||
|
```
|
||||||
|
|
||||||
|
建议倍率:
|
||||||
|
|
||||||
|
| 条件 | 倍率 |
|
||||||
|
| ------------------------------------------ | ------ |
|
||||||
|
| `steps = 1` | `0.85` |
|
||||||
|
| `steps = 2` | `1.0` |
|
||||||
|
| `steps >= 3` | `1.12` |
|
||||||
|
| `investigation / retrieval / relationship` | `1.0` |
|
||||||
|
| `trial / bounty` | `1.08` |
|
||||||
|
| `urgency = high` | `1.05` |
|
||||||
|
|
||||||
|
最终规则:
|
||||||
|
|
||||||
|
1. 结果四舍五入到 `5` 的倍数。
|
||||||
|
2. 章节主任务优先从本章 `questXpBudget` 出数。
|
||||||
|
3. 普通 NPC 支线如果不绑定章节,则按 `targetLevel` 单独计算。
|
||||||
|
|
||||||
|
## 5.2 击败敌对 NPC 经验
|
||||||
|
|
||||||
|
建议公式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
baseKillXp(targetLevel) = xpToNextLevel(targetLevel) * 0.08;
|
||||||
|
|
||||||
|
killXp =
|
||||||
|
baseKillXp(targetLevel) *
|
||||||
|
stageMultiplier *
|
||||||
|
levelDeltaMultiplier *
|
||||||
|
repeatPenalty;
|
||||||
|
```
|
||||||
|
|
||||||
|
建议倍率:
|
||||||
|
|
||||||
|
| 条件 | 倍率 |
|
||||||
|
| -------------------------------- | ----------------- |
|
||||||
|
| `opening` | `0.9` |
|
||||||
|
| `expansion` | `1.0` |
|
||||||
|
| `turning_point` | `1.05` |
|
||||||
|
| `climax` | `1.15` |
|
||||||
|
| 玩家高于目标 `2` 级 | `0.7` |
|
||||||
|
| 玩家高于目标 `4` 级 | `0.3` |
|
||||||
|
| 玩家低于目标 `2` 级 | `1.15` |
|
||||||
|
| 同章同类敌对实体超过预计击杀数后 | `0.5 -> 0.2 -> 0` |
|
||||||
|
|
||||||
|
解释:
|
||||||
|
|
||||||
|
1. 同章重复刷怪必须衰减。
|
||||||
|
2. 击杀经验要响应等级差,避免低章 farming。
|
||||||
|
3. 高潮压轴敌人可以给更多经验,但仍受章节预算约束。
|
||||||
|
|
||||||
|
## 5.3 经验发放顺序
|
||||||
|
|
||||||
|
推荐统一顺序:
|
||||||
|
|
||||||
|
```text
|
||||||
|
规则动作成功
|
||||||
|
-> 生成经验 grant
|
||||||
|
-> 写入 playerProgression.totalXp / currentLevelXp
|
||||||
|
-> 处理升级
|
||||||
|
-> 回写章节 ledger
|
||||||
|
-> 生成前端提示
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把经验结算拆在前端多个回调里各自加一次。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 章节经验速度评估
|
||||||
|
|
||||||
|
## 6.1 章节顺序来源
|
||||||
|
|
||||||
|
章节索引 `chapterIndex` 建议按下面顺序解析:
|
||||||
|
|
||||||
|
1. 有 `campaign pack` 时,优先用 campaign 正式顺序
|
||||||
|
2. 否则有 `sceneChapterBlueprints` 时,用蓝图顺序
|
||||||
|
3. 再否则,对 `landmarks` 从营地出发做最短路径排序
|
||||||
|
4. 若存在并列,则回退到稳定的 landmark 原始顺序
|
||||||
|
|
||||||
|
这样才能给每章一个稳定的“这是第几章”。
|
||||||
|
|
||||||
|
## 6.2 目标等级带
|
||||||
|
|
||||||
|
建议先计算“伪等级进度”,再换算成经验预算:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
chapterBoundaryPseudoLevel(i) =
|
||||||
|
1 + curve(i / totalChapters) * (terminalStoryLevel - 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
建议 `curve` 用轻微前快后稳的函数:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
curve(progress) = Math.pow(progress, 0.92);
|
||||||
|
```
|
||||||
|
|
||||||
|
随后:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
entryPseudoLevel = chapterBoundaryPseudoLevel(chapterIndex - 1);
|
||||||
|
exitPseudoLevel = chapterBoundaryPseudoLevel(chapterIndex);
|
||||||
|
chapterXpBudget =
|
||||||
|
xpForPseudoLevel(exitPseudoLevel) - xpForPseudoLevel(entryPseudoLevel);
|
||||||
|
```
|
||||||
|
|
||||||
|
这样做的好处是:
|
||||||
|
|
||||||
|
1. 每一章都有明确的入章/出章目标
|
||||||
|
2. 等级增幅随章节自然变慢
|
||||||
|
3. 经验速度评估可以直接落成表格
|
||||||
|
|
||||||
|
## 6.3 章节经验份额
|
||||||
|
|
||||||
|
默认建议:
|
||||||
|
|
||||||
|
| 章节类型 | 任务经验占比 | 击杀经验占比 |
|
||||||
|
| --------------- | ------------ | ------------ |
|
||||||
|
| 调查/关系型章节 | `75%` | `25%` |
|
||||||
|
| 平衡型章节 | `65%` | `35%` |
|
||||||
|
| 战斗/试炼型章节 | `55%` | `45%` |
|
||||||
|
|
||||||
|
章节类型判定可由下面几项共同决定:
|
||||||
|
|
||||||
|
1. `SceneChapterBlueprint.acts` 数量
|
||||||
|
2. 当前章节 hostile NPC 数量
|
||||||
|
3. 当前章节任务 step 中战斗目标占比
|
||||||
|
5. linked thread 是否为主线高压线程
|
||||||
|
|
||||||
|
## 6.4 实际速度评估规则
|
||||||
|
|
||||||
|
每章结束后,至少计算下面三个值:
|
||||||
|
|
||||||
|
1. `actualTotalXp / plannedTotalXp`
|
||||||
|
2. `actualHostileXp / plannedHostileXp`
|
||||||
|
3. `levelAtExit - plannedExitLevel`
|
||||||
|
|
||||||
|
建议判定:
|
||||||
|
|
||||||
|
| 偏差 | 判断 |
|
||||||
|
| ----------- | -------- |
|
||||||
|
| `±10%` 内 | 正常 |
|
||||||
|
| `10% ~ 20%` | 需观察 |
|
||||||
|
| `> 20%` | 必须调参 |
|
||||||
|
|
||||||
|
这就是“评估每一章获得经验速度”的正式口径,不再用主观感觉判断。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. NPC 自动定级规则
|
||||||
|
|
||||||
|
## 7.1 默认角色分类
|
||||||
|
|
||||||
|
建议默认按当前幕和敌我属性推导 `progressionRole`:
|
||||||
|
|
||||||
|
1. 当前幕 `primaryNpcId`
|
||||||
|
|
||||||
|
- 若 hostile:`hostile_elite` 或 `hostile_boss`
|
||||||
|
- 若非 hostile:`guide` 或 `rival`
|
||||||
|
|
||||||
|
2. 非主角色 hostile NPC
|
||||||
|
|
||||||
|
- `hostile_standard`
|
||||||
|
|
||||||
|
3. 非主角色友方 NPC
|
||||||
|
- `support` 或 `ambient`
|
||||||
|
|
||||||
|
如需修正,再允许章节蓝图加可选 override,但不要求创作者每次手填。
|
||||||
|
|
||||||
|
## 7.2 等级锚点
|
||||||
|
|
||||||
|
每章先得到:
|
||||||
|
|
||||||
|
1. `entryLevel`
|
||||||
|
2. `exitLevel`
|
||||||
|
|
||||||
|
然后按当前阶段得到阶段锚点:
|
||||||
|
|
||||||
|
| 阶段 | 目标锚点 |
|
||||||
|
| --------------- | ----------------------------- |
|
||||||
|
| `opening` | 接近 `entryLevel` |
|
||||||
|
| `expansion` | `entryLevel ~ exitLevel` 中段 |
|
||||||
|
| `turning_point` | 接近 `exitLevel` |
|
||||||
|
| `climax` | `exitLevel` |
|
||||||
|
| `aftermath` | `exitLevel - 1` 或持平 |
|
||||||
|
|
||||||
|
## 7.3 最终定级
|
||||||
|
|
||||||
|
建议公式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
baseStageLevel = interpolate(entryLevel, exitLevel, stageProgress);
|
||||||
|
|
||||||
|
npcLevel = round(baseStageLevel) + roleOffset(progressionRole);
|
||||||
|
```
|
||||||
|
|
||||||
|
建议 offset:
|
||||||
|
|
||||||
|
| role | offset |
|
||||||
|
| ------------------ | -------- |
|
||||||
|
| `ambient` | `-1` |
|
||||||
|
| `support` | `0` |
|
||||||
|
| `guide` | `0` |
|
||||||
|
| `rival` | `0 ~ +1` |
|
||||||
|
| `hostile_standard` | `0` |
|
||||||
|
| `hostile_elite` | `+1` |
|
||||||
|
| `hostile_boss` | `+2` |
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
1. 统一 clamp 到 `1 ~ terminalStoryLevel + 2`
|
||||||
|
2. 不允许出现“第 3 章普通怪高于第 6 章精英”的跨章倒挂
|
||||||
|
3. `hostile_boss` 如果需要更强,必须给更高等级,不准同级偷倍数
|
||||||
|
|
||||||
|
## 7.4 同级不同风格
|
||||||
|
|
||||||
|
NPC 等级确定后,再把 `referenceStrength` 套到具体风格:
|
||||||
|
|
||||||
|
1. 重装型:
|
||||||
|
|
||||||
|
- 生命占比更高
|
||||||
|
- 爆发占比更低
|
||||||
|
|
||||||
|
2. 迅捷型:
|
||||||
|
|
||||||
|
- 生命占比更低
|
||||||
|
- 出手与压制占比更高
|
||||||
|
|
||||||
|
3. 控场型:
|
||||||
|
- 法力/控制预算更高
|
||||||
|
|
||||||
|
但这一步只能做“分布调整”,不能改变同级总参考强度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 与当前仓库的接入点
|
||||||
|
|
||||||
|
## 8.1 第一批必须改的类型
|
||||||
|
|
||||||
|
1. `src/types/game.ts`
|
||||||
|
|
||||||
|
- 新增 `playerProgression`
|
||||||
|
|
||||||
|
2. `src/types/story.ts`
|
||||||
|
|
||||||
|
- `QuestReward.experience`
|
||||||
|
|
||||||
|
3. `src/types/scene.ts`
|
||||||
|
|
||||||
|
- `SceneNpc.levelProfile`
|
||||||
|
- `SceneHostileNpc.levelProfile`
|
||||||
|
- `SceneHostileNpc.experienceReward`
|
||||||
|
|
||||||
|
4. `packages/shared/src/contracts/story.ts`
|
||||||
|
- 如果需要让前后端合同正式共享等级展示字段,在这里补最小契约
|
||||||
|
|
||||||
|
## 8.2 第一批必须改的后端模块
|
||||||
|
|
||||||
|
1. `server-node/src/modules/quest/questStoryActionService.ts`
|
||||||
|
|
||||||
|
- `resolveQuestTurnInAction(...)` 里追加任务经验发放
|
||||||
|
|
||||||
|
2. `server-node/src/modules/quest/questRuntimeSignalService.ts`
|
||||||
|
|
||||||
|
- 保持 quest signal 职责
|
||||||
|
- 不直接负责经验裁决,只把可用信号交给 progression 模块
|
||||||
|
|
||||||
|
3. `server-node/src/modules/combat/**`
|
||||||
|
|
||||||
|
- 在胜利结算后发 hostile NPC 经验
|
||||||
|
|
||||||
|
4. `server-node/src/modules/story/**`
|
||||||
|
|
||||||
|
- 在切章、进场、恢复场景时接入章节成长计划与 ledger
|
||||||
|
|
||||||
|
5. 新增 `server-node/src/modules/progression/**`
|
||||||
|
- 成为等级、经验、章节定级唯一真相源
|
||||||
|
|
||||||
|
## 8.3 第一批不建议重写的部分
|
||||||
|
|
||||||
|
这轮不建议一开始就重写:
|
||||||
|
|
||||||
|
1. 整套前端战斗 UI
|
||||||
|
2. 整套属性系统
|
||||||
|
3. Quest UI 大面板结构
|
||||||
|
4. 所有 hostile preset 原始配置文件
|
||||||
|
|
||||||
|
更稳的做法是:
|
||||||
|
|
||||||
|
1. 先让后端算出等级与经验
|
||||||
|
2. 再把结果投影到现有运行时字段
|
||||||
|
3. 最后再逐步清理旧静态强度残留
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 迁移策略
|
||||||
|
|
||||||
|
## 9.1 旧存档兼容
|
||||||
|
|
||||||
|
旧存档没有 `playerProgression` 时:
|
||||||
|
|
||||||
|
1. 默认初始化为 `Lv.1`
|
||||||
|
2. `totalXp = 0`
|
||||||
|
3. `currentLevelXp = 0`
|
||||||
|
4. `xpToNextLevel = benchmark[1].xpToNextLevel`
|
||||||
|
|
||||||
|
如果后续希望更平滑,可在第二轮增加“按当前章节进度反推起始等级”的迁移脚本,但首版先不要让迁移复杂化。
|
||||||
|
|
||||||
|
## 9.2 旧 hostile preset 兼容
|
||||||
|
|
||||||
|
旧 preset 里的 `hp/maxHp` 首版处理建议:
|
||||||
|
|
||||||
|
1. 先保留原字段作为 style hint
|
||||||
|
2. 运行时用 level benchmark 覆盖最终 `hp/maxHp`
|
||||||
|
3. 保证当前素材和行为标签不需要重做
|
||||||
|
|
||||||
|
## 9.3 旧任务兼容
|
||||||
|
|
||||||
|
旧任务没有 `reward.experience` 时:
|
||||||
|
|
||||||
|
1. 默认按 `0` 处理
|
||||||
|
2. 仅新生成或重新编译的任务带经验
|
||||||
|
3. 章节主任务优先切到新编译链
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 开发顺序
|
||||||
|
|
||||||
|
## 阶段 A:先把等级状态立住
|
||||||
|
|
||||||
|
先做:
|
||||||
|
|
||||||
|
1. `PlayerProgressionState`
|
||||||
|
2. `LevelBenchmark[]`
|
||||||
|
3. 经验加点与升级服务
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
1. 后端能正确加经验与升级
|
||||||
|
2. 前端能稳定展示 `Lv. X / 经验条`
|
||||||
|
|
||||||
|
## 阶段 B:接任务经验
|
||||||
|
|
||||||
|
先做:
|
||||||
|
|
||||||
|
1. `QuestReward.experience`
|
||||||
|
2. `quest turn-in` 经验发放
|
||||||
|
3. 任务结果文案里补 `经验 +N`
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
1. 交付任务后能加经验
|
||||||
|
2. 升级时能正确连跳
|
||||||
|
|
||||||
|
## 阶段 C:接章节预算与 NPC 自动定级
|
||||||
|
|
||||||
|
先做:
|
||||||
|
|
||||||
|
1. `ChapterProgressionPlan`
|
||||||
|
2. `npcLevelResolver`
|
||||||
|
3. runtime hostile NPC 经验值生成
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
1. 进入不同章节时 NPC 等级自动变化
|
||||||
|
2. 同级不同风格但参考强度一致
|
||||||
|
|
||||||
|
## 阶段 D:接击败敌对 NPC 经验与章节 ledger
|
||||||
|
|
||||||
|
先做:
|
||||||
|
|
||||||
|
1. hostile defeat 经验
|
||||||
|
2. `ChapterExperienceLedger`
|
||||||
|
3. 章节偏差评估输出
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
1. 每章都能看到计划/实际经验偏差
|
||||||
|
2. 重复刷同章敌对 NPC 不会破坏曲线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 验收标准
|
||||||
|
|
||||||
|
做到下面这些,才算这次等级系统设计真正落地:
|
||||||
|
|
||||||
|
1. 玩家正式拥有 `等级 + 经验 + 升级` 主链。
|
||||||
|
2. 经验来源只通过后端发放,前端不本地算经验。
|
||||||
|
3. 同等级实体共享同一档 `参考强度`。
|
||||||
|
4. 每章都能生成 `入章等级 / 出章等级 / 经验预算`。
|
||||||
|
5. 每章的 NPC 都能按章节自动定级。
|
||||||
|
6. 完成任务、击败敌对 NPC 都能稳定获得经验。
|
||||||
|
7. 章节结束后能评估“这一章经验速度是否正常”。
|
||||||
|
8. 现有任务、章节、属性和 hostile NPC 主链不被推翻,只是在其上新增成长预算层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 最后结论
|
||||||
|
|
||||||
|
这次等级系统设计的重点,不是简单在 UI 上加一个 `Lv.1`,而是把当前仓库里已经存在的:
|
||||||
|
|
||||||
|
1. 章节闭环
|
||||||
|
2. 任务结算
|
||||||
|
3. 敌对 NPC 胜利事件
|
||||||
|
4. 统一属性与 hostile preset
|
||||||
|
|
||||||
|
收束到一条新的成长主链:
|
||||||
|
|
||||||
|
**章节先给出目标等级与经验速度,系统再按这套速度自动设置 NPC 等级,并把任务交付与击败敌对 NPC 统一变成可控的经验入口。**
|
||||||
|
|
||||||
|
这样之后,等级不再只是一个展示数字,而会真正变成:
|
||||||
|
|
||||||
|
- 玩家成长速度的刻度
|
||||||
|
- 同级参考强度的刻度
|
||||||
|
- 章节节奏是否合理的刻度
|
||||||
|
- 不同章节 NPC 强度自动落位的刻度
|
||||||
@@ -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,316 @@
|
|||||||
|
# 高好感角色聊天内委托触发与领取流程设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-19`
|
||||||
|
|
||||||
|
## 0. 目标
|
||||||
|
|
||||||
|
这次迭代解决的是一个很具体的体验断层:
|
||||||
|
|
||||||
|
1. 当前角色委托主要还是从 NPC 互动菜单里直接出现,缺少“先聊上 1-2 轮,再顺着上下文自然托付任务”的过渡。
|
||||||
|
2. 聊天态虽然已经有多轮对话、自定义输入和选项建议,但没有“临时任务 offer”这一层中间状态。
|
||||||
|
3. 现有任务详情面板已经能看任务、领奖励,但还不能承接“任务尚未正式入日志,只是对方刚提出委托”的场景。
|
||||||
|
|
||||||
|
目标不是新造一套聊天任务系统,而是把现有:
|
||||||
|
|
||||||
|
- `npc_chat` 多轮聊天流
|
||||||
|
- `generateQuestForNpcEncounter(...)` 任务生成链
|
||||||
|
- `QuestLogEntry` 任务日志结构
|
||||||
|
- `AdventurePanelOverlays` 任务详情面板
|
||||||
|
|
||||||
|
串成一条更自然的“聊天内委托”链路。
|
||||||
|
|
||||||
|
一句话目标:
|
||||||
|
|
||||||
|
**当玩家与好感度大于 0 的角色聊天时,先寒暄 1-2 轮,再由角色顺着上下文提出委托;玩家可查看、换任务、放弃任务,确认领取后任务才正式进入日志,并恢复自由聊天。**
|
||||||
|
|
||||||
|
## 1. 这次不做什么
|
||||||
|
|
||||||
|
为了避免系统边界漂移,这次明确不做下面这些事:
|
||||||
|
|
||||||
|
1. 不重写任务生成器。
|
||||||
|
- “任务”和“更换任务”都必须复用现有 `generateQuestForNpcEncounter(...)` 链路。
|
||||||
|
- 也就是说,仍然走现在的 `evaluateQuestOpportunity -> AI / fallback quest intent -> compileQuestIntentToQuest`。
|
||||||
|
|
||||||
|
2. 不把聊天态任务 offer 直接视为已接任务。
|
||||||
|
- 对方刚把委托提出来时,它还只是 `pending offer`,不应立即写进 `gameState.quests`。
|
||||||
|
- 只有玩家点击“领取任务”后,才正式调用现有任务接取写入逻辑。
|
||||||
|
|
||||||
|
3. 不在 UI 默认堆说明文字。
|
||||||
|
- 聊天态只切换三项操作:
|
||||||
|
- `查看任务`
|
||||||
|
- `更换任务`
|
||||||
|
- `放弃任务`
|
||||||
|
- 不额外在主界面堆功能说明。
|
||||||
|
|
||||||
|
4. 不改成必须走服务端聊天。
|
||||||
|
- 当前多轮 NPC 聊天仍沿用前端本地的 `handleNpcChatTurn(...)` 流程。
|
||||||
|
- 这次只是在聊天流程里插入任务 offer 状态。
|
||||||
|
|
||||||
|
## 2. 核心流程
|
||||||
|
|
||||||
|
## 2.1 触发条件
|
||||||
|
|
||||||
|
只有同时满足下面条件时,聊天中才允许提出委托:
|
||||||
|
|
||||||
|
1. 当前遭遇是角色型 NPC。
|
||||||
|
- `encounter.characterId` 存在。
|
||||||
|
|
||||||
|
2. 当前好感度大于 `0`。
|
||||||
|
|
||||||
|
3. 当前角色没有未结清任务。
|
||||||
|
- 复用 `getQuestForIssuer(...)` 判断。
|
||||||
|
|
||||||
|
4. 当前聊天里还没有待处理的任务 offer。
|
||||||
|
|
||||||
|
5. 已经完成前置寒暄轮次。
|
||||||
|
- 默认要求先完成 `1-2` 轮自然聊天。
|
||||||
|
- 建议规则:
|
||||||
|
- `affinity >= 30` 时,完成 `1` 轮后即可进入委托时机。
|
||||||
|
- `0 < affinity < 30` 时,完成 `2` 轮后再进入委托时机。
|
||||||
|
|
||||||
|
## 2.2 聊天轮次切换
|
||||||
|
|
||||||
|
正常聊天时:
|
||||||
|
|
||||||
|
- 保持现有三条 `npc_chat` 建议选项
|
||||||
|
- 保持自定义输入可用
|
||||||
|
|
||||||
|
当触发委托时:
|
||||||
|
|
||||||
|
1. 先正常生成本轮 NPC 回复。
|
||||||
|
2. 随后调用现有 `generateQuestForNpcEncounter(...)` 生成一份 `pending quest offer`。
|
||||||
|
3. 在当前轮次追加一段 NPC 委托台词。
|
||||||
|
4. 把当前轮次选项替换成:
|
||||||
|
- 第一项:`查看任务`
|
||||||
|
- 第二项:`更换任务`
|
||||||
|
- 第三项:`放弃任务`
|
||||||
|
5. 临时隐藏自定义输入。
|
||||||
|
|
||||||
|
这意味着聊天态此时进入一个短暂的“任务处理态”,直到玩家:
|
||||||
|
|
||||||
|
- 查看并领取
|
||||||
|
- 更换
|
||||||
|
- 放弃
|
||||||
|
|
||||||
|
其中任一分支结算完成后,再恢复自由聊天。
|
||||||
|
|
||||||
|
## 2.3 查看任务
|
||||||
|
|
||||||
|
点击 `查看任务` 时:
|
||||||
|
|
||||||
|
1. 不立即写入任务日志。
|
||||||
|
2. 直接复用现有任务详情弹层展示 `pending quest offer` 的详情。
|
||||||
|
3. 任务详情面板在这类任务上新增主按钮:
|
||||||
|
- `领取任务`
|
||||||
|
|
||||||
|
这一步的关键是:
|
||||||
|
|
||||||
|
**查看任务只是看,不等于接。**
|
||||||
|
|
||||||
|
## 2.4 领取任务
|
||||||
|
|
||||||
|
点击 `领取任务` 时:
|
||||||
|
|
||||||
|
1. 使用当前 `pending quest offer` 的 `QuestLogEntry`,调用现有任务接取写入逻辑,把任务正式写入 `gameState.quests`。
|
||||||
|
2. 同时把这轮动作写回聊天:
|
||||||
|
- 追加玩家一句明确接受委托的话。
|
||||||
|
- 可追加一条简短 NPC 确认回应,或直接用现有结果文案转成对话语义。
|
||||||
|
3. 更新 `storyHistory`,确保后续聊天上下文知道“这份委托已经接下”。
|
||||||
|
4. 清空 `pending quest offer`。
|
||||||
|
5. 恢复正常 `npc_chat` 建议选项与自定义输入。
|
||||||
|
|
||||||
|
## 2.5 更换任务
|
||||||
|
|
||||||
|
点击 `更换任务` 时:
|
||||||
|
|
||||||
|
1. 必须再次调用现有 `generateQuestForNpcEncounter(...)`。
|
||||||
|
2. 旧的 `pending quest offer` 被新的覆盖。
|
||||||
|
3. 当前聊天追加一条“对方换了个委托”的回应。
|
||||||
|
4. 仍然维持任务处理态:
|
||||||
|
- 继续显示
|
||||||
|
- `查看任务`
|
||||||
|
- `更换任务`
|
||||||
|
- `放弃任务`
|
||||||
|
- 自定义输入仍隐藏
|
||||||
|
|
||||||
|
这里的关键约束是:
|
||||||
|
|
||||||
|
**更换任务不是本地改标题或改描述,而是重新走现有任务生成链。**
|
||||||
|
|
||||||
|
## 2.6 放弃任务
|
||||||
|
|
||||||
|
点击 `放弃任务` 时:
|
||||||
|
|
||||||
|
1. 直接丢弃当前 `pending quest offer`。
|
||||||
|
2. 在对话里补一条“玩家暂时不接”的回应。
|
||||||
|
3. 恢复自由聊天:
|
||||||
|
- 再次显示正常 `npc_chat` 建议
|
||||||
|
- 恢复自定义输入
|
||||||
|
|
||||||
|
放弃这里只作用于“待领取委托”,不会影响已经入日志的正式任务。
|
||||||
|
|
||||||
|
## 3. 数据与状态设计
|
||||||
|
|
||||||
|
## 3.1 聊天态新增待领取任务状态
|
||||||
|
|
||||||
|
建议把这次临时状态挂在 `StoryNpcChatState` 上,而不是直接写入 `GameState.quests`。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface StoryNpcQuestOfferState {
|
||||||
|
quest: QuestLogEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoryNpcChatState {
|
||||||
|
npcId: string;
|
||||||
|
npcName: string;
|
||||||
|
turnCount: number;
|
||||||
|
customInputPlaceholder?: string;
|
||||||
|
pendingQuestOffer?: StoryNpcQuestOfferState | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样有 3 个好处:
|
||||||
|
|
||||||
|
1. 任务 offer 只属于当前聊天上下文,不污染正式任务日志。
|
||||||
|
2. AdventurePanel 可以直接从 `currentStory.npcChatState` 判断是否进入任务处理态。
|
||||||
|
3. 任务详情面板可以直接读取这份 `QuestLogEntry` 展示,而不用再造一套展示结构。
|
||||||
|
|
||||||
|
## 3.2 任务处理态的选项表达
|
||||||
|
|
||||||
|
建议不要把“查看 / 更换 / 放弃”接进服务端 runtime action。
|
||||||
|
|
||||||
|
原因是:
|
||||||
|
|
||||||
|
1. `查看任务` 只是 UI 行为,不需要服务端结算。
|
||||||
|
2. `更换任务` 和 `放弃任务` 都是当前聊天态内部状态流转。
|
||||||
|
3. 这三项更适合作为本地聊天态专用选项,由 `AdventurePanel + npcEncounterActions` 协同处理。
|
||||||
|
|
||||||
|
建议做成 3 个本地专用 `StoryOption`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
functionId: 'npc_chat_quest_offer_view',
|
||||||
|
actionText: '查看任务',
|
||||||
|
runtimePayload: { npcChatQuestOfferAction: 'view' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其余两个同理:
|
||||||
|
|
||||||
|
- `replace`
|
||||||
|
- `abandon`
|
||||||
|
|
||||||
|
## 3.3 接取后的正式写入
|
||||||
|
|
||||||
|
正式领取后才进入任务日志:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
nextState = {
|
||||||
|
...state,
|
||||||
|
quests: acceptQuest(state.quests, pendingQuest.quest),
|
||||||
|
runtimeStats: incrementGameRuntimeStats(state.runtimeStats, {
|
||||||
|
questsAccepted: 1,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- `pending offer` 不计入 `questsAccepted`
|
||||||
|
- 真正点击 `领取任务` 才计数
|
||||||
|
|
||||||
|
## 4. UI 落点
|
||||||
|
|
||||||
|
## 4.1 聊天面板
|
||||||
|
|
||||||
|
`AdventurePanel` 中增加一个判断:
|
||||||
|
|
||||||
|
1. `currentStory.npcChatState?.pendingQuestOffer` 存在时:
|
||||||
|
- 只显示三项任务处理选项
|
||||||
|
- 隐藏自定义输入
|
||||||
|
|
||||||
|
2. 不存在时:
|
||||||
|
- 保持现有聊天输入与 `npc_chat` 建议
|
||||||
|
|
||||||
|
## 4.2 任务详情弹层
|
||||||
|
|
||||||
|
`AdventurePanelOverlays` 里的任务详情弹层继续复用,但要区分两种任务来源:
|
||||||
|
|
||||||
|
1. 已在任务日志中的正式任务
|
||||||
|
- 保持现有逻辑
|
||||||
|
- 完成后仍显示 `领取奖励`
|
||||||
|
|
||||||
|
2. 聊天里的 `pending quest offer`
|
||||||
|
- 底部显示 `领取任务`
|
||||||
|
- 不显示 `领取奖励`
|
||||||
|
|
||||||
|
点击 `领取任务` 后:
|
||||||
|
|
||||||
|
- 关闭详情弹层
|
||||||
|
- 回到聊天界面
|
||||||
|
- 当前聊天追加“我愿意接下”这一步
|
||||||
|
|
||||||
|
## 5. 代码改动建议
|
||||||
|
|
||||||
|
建议落地在这些文件:
|
||||||
|
|
||||||
|
1. `src/types/story.ts`
|
||||||
|
- 扩展 `StoryNpcChatState`
|
||||||
|
|
||||||
|
2. `src/hooks/story/npcEncounterActions.ts`
|
||||||
|
- 增加聊天内任务 offer 触发判断
|
||||||
|
- 接入 `generateQuestForNpcEncounter(...)`
|
||||||
|
- 增加
|
||||||
|
- 更换任务
|
||||||
|
- 放弃任务
|
||||||
|
- 领取任务
|
||||||
|
对应的本地状态流转
|
||||||
|
|
||||||
|
3. `src/hooks/story/useStoryInteractionCoordinator.ts`
|
||||||
|
- 向上暴露聊天内任务 offer 的操作方法
|
||||||
|
|
||||||
|
4. `src/hooks/useStoryGeneration.ts`
|
||||||
|
- 把聊天内任务 offer UI 能力透传给面板
|
||||||
|
|
||||||
|
5. `src/components/AdventurePanel.tsx`
|
||||||
|
- 聊天态隐藏 / 恢复输入
|
||||||
|
- 拦截 `查看任务 / 更换任务 / 放弃任务`
|
||||||
|
- 让 pending quest 也能进入任务详情弹层
|
||||||
|
|
||||||
|
6. `src/components/adventure-panel/AdventurePanelOverlays.tsx`
|
||||||
|
- 为 pending quest 增加 `领取任务` 按钮
|
||||||
|
|
||||||
|
7. `src/components/AdventurePanel.test.tsx`
|
||||||
|
- 补聊天态输入隐藏测试
|
||||||
|
|
||||||
|
8. `src/hooks/story/npcEncounterActions.test.ts`
|
||||||
|
- 补任务 offer 触发 / 更换 / 接取测试
|
||||||
|
|
||||||
|
## 6. 验收标准
|
||||||
|
|
||||||
|
做到以下几点,才算这次需求成立:
|
||||||
|
|
||||||
|
1. 与好感度大于 `0` 的角色聊天时,不会一上来立刻塞任务,前 `1-2` 轮先正常寒暄。
|
||||||
|
2. 达到委托时机后,系统会调用现有 `generateQuestForNpcEncounter(...)` 生成一份待领取任务。
|
||||||
|
3. 当前聊天轮次会出现一段明确的委托台词。
|
||||||
|
4. 这一轮聊天选项会切成:
|
||||||
|
- `查看任务`
|
||||||
|
- `更换任务`
|
||||||
|
- `放弃任务`
|
||||||
|
5. 任务处理态下,自定义输入会被临时隐藏。
|
||||||
|
6. 点击 `查看任务` 会弹出现有任务详情面板。
|
||||||
|
7. 点击 `领取任务` 后,任务才正式进入任务日志,并在对话里体现“玩家愿意接下”。
|
||||||
|
8. 领取完成后,聊天会恢复正常输入与自由继续对话。
|
||||||
|
9. 点击 `更换任务` 时,必须重新调用现有任务生成链,而不是本地改文案。
|
||||||
|
|
||||||
|
## 7. 一句话收束
|
||||||
|
|
||||||
|
这次要做的,不是“让聊天里多一个任务按钮”,而是把:
|
||||||
|
|
||||||
|
- 高好感聊天
|
||||||
|
- 上下文化任务生成
|
||||||
|
- 临时任务 offer
|
||||||
|
- 任务详情查看
|
||||||
|
- 正式领取后回流聊天
|
||||||
|
|
||||||
|
整合成一个更自然的叙事交接过程。
|
||||||
@@ -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,不新增后端逻辑。
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# 平台首页公开浏览与登录弹窗拦截设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-19`
|
||||||
|
|
||||||
|
## 0. 背景
|
||||||
|
|
||||||
|
当前仓库里的账号 PRD 默认要求“未登录先登录,再进入平台”。
|
||||||
|
|
||||||
|
这次产品策略调整为:
|
||||||
|
|
||||||
|
- 用户进入平台后,默认可以直接浏览首页
|
||||||
|
- 只有在尝试进入作品、进入世界、开始创作等受保护动作时,才检查登录
|
||||||
|
- 登录界面不再是完整页面,而是覆盖在当前平台上的轻量弹窗
|
||||||
|
|
||||||
|
这份设计只覆盖当前一次前台入口改造,目标是把边界写清楚到可以直接编码,不再让登录策略和平台首页互相冲突。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 本次目标
|
||||||
|
|
||||||
|
1. 未登录用户可以正常进入平台首页并浏览公开内容。
|
||||||
|
2. 点击作品卡片时,若未登录,弹出登录弹窗;登录成功后继续进入刚才点击的作品。
|
||||||
|
3. 打开创作类型选择后,点击具体游戏类型开始创作时,若未登录,弹出登录弹窗;登录成功后继续刚才的创作动作。
|
||||||
|
4. 登录 UI 改成极简弹窗,只保留窗口标题、必要输入框、必要按钮、错误态与关闭能力。
|
||||||
|
5. 未登录态下不要继续请求“我的作品 / 个人看板 / 云端浏览历史 / 云端存档列表”这类受保护数据,避免首页公开态出现无意义报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 公开态与受保护动作边界
|
||||||
|
|
||||||
|
## 2.1 未登录允许访问
|
||||||
|
|
||||||
|
- 平台首页主视图
|
||||||
|
- 精选推荐
|
||||||
|
- 最新发布
|
||||||
|
- 创作类型选择弹窗本身的展示
|
||||||
|
- 本地浏览历史展示(若存在)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- “允许访问”只代表允许看,不代表允许进入作品详情、开始世界或创建内容。
|
||||||
|
- 首页公开态必须保持可读,不因账号接口 401 出现整屏报错。
|
||||||
|
|
||||||
|
## 2.2 未登录必须拦截
|
||||||
|
|
||||||
|
- 点击任意作品卡片
|
||||||
|
- 点击作品详情中的“开始游戏”
|
||||||
|
- 点击作品详情中的“继续创作 / 发布 / 下架 / 删除”等作者动作
|
||||||
|
- 点击创作类型卡片,开始进入具体创作工作台
|
||||||
|
- 其他后续新增的“进入世界 / 开始正式创作”入口
|
||||||
|
|
||||||
|
拦截方式统一为:
|
||||||
|
|
||||||
|
- 保持当前页面上下文不跳走
|
||||||
|
- 直接弹出登录弹窗
|
||||||
|
- 登录成功后自动继续刚才被拦截的动作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 登录弹窗设计
|
||||||
|
|
||||||
|
## 3.1 展示形态
|
||||||
|
|
||||||
|
- 使用居中的 modal 覆盖层
|
||||||
|
- 背景保留平台当前页面,只加遮罩和轻微模糊
|
||||||
|
- 移动端优先,弹窗宽度贴近屏幕边缘,底部和顶部留出安全边距
|
||||||
|
- 桌面端保持紧凑,不做双栏 hero,不再单独占满整页
|
||||||
|
|
||||||
|
## 3.2 内容约束
|
||||||
|
|
||||||
|
弹窗内默认只保留:
|
||||||
|
|
||||||
|
- 标题:`登录账号`
|
||||||
|
- 登录方式页签:`短信登录` / `密码登录`
|
||||||
|
- 手机号输入框
|
||||||
|
- 验证码输入框
|
||||||
|
- 获取验证码按钮
|
||||||
|
- 登录主按钮
|
||||||
|
- 微信登录按钮(当后端开放时)
|
||||||
|
- 图形验证码输入区(仅后端要求时出现)
|
||||||
|
- 错误提示
|
||||||
|
- 关闭按钮
|
||||||
|
|
||||||
|
明确不再保留:
|
||||||
|
|
||||||
|
- 品牌副标题
|
||||||
|
- 功能介绍段落
|
||||||
|
- 规则说明卡片
|
||||||
|
- “先登录再同步进度”这类描述性文案
|
||||||
|
- 占据视觉主体的装饰信息块
|
||||||
|
|
||||||
|
## 3.2.1 登录页签落地约束
|
||||||
|
|
||||||
|
账号面板需要把短信验证码登录和密码登录拆成互斥页签,避免两个登录表单在同一个面板里上下堆叠。
|
||||||
|
|
||||||
|
- 同时开放短信与密码登录时,面板顶部展示两个居中的文字页签,当前页签使用深色字重和短下划线强调。
|
||||||
|
- 只渲染当前页签对应的输入区;切换页签不弹出新面板,不展示二维码入口。
|
||||||
|
- `短信登录` 页签包含手机号、验证码、获取验证码和主按钮。
|
||||||
|
- `密码登录` 页签只包含手机号、密码、主按钮和忘记密码入口;不支持邮箱、用户名或叙世号。
|
||||||
|
- 密码登录只是手机号验证码登录的补充方式:只有已登录并设置过密码的手机号账号才能使用,不能在密码页签创建账号。
|
||||||
|
- `密码登录` 主按钮固定为 `登录`,不得使用 `注册/登录`。
|
||||||
|
- 未开放某个登录方式时不展示对应页签,避免用户进入不可用表单。
|
||||||
|
- 移动端页签保持等分点击区域,输入框与按钮宽度仍随弹窗收缩。
|
||||||
|
|
||||||
|
## 3.3 登录成功后的行为
|
||||||
|
|
||||||
|
- 手机号登录成功后,关闭弹窗
|
||||||
|
- 当前平台页面不刷新
|
||||||
|
- 若用户是被某个受保护动作拦截进入登录,则自动恢复该动作
|
||||||
|
- 若用户只是主动点“登录”按钮,则关闭弹窗并停留在当前页面
|
||||||
|
|
||||||
|
## 3.4 关闭行为
|
||||||
|
|
||||||
|
- 用户主动关闭弹窗时,只关闭弹窗,不改变当前平台页面
|
||||||
|
- 不清空首页浏览状态
|
||||||
|
- 不自动跳转到其他 tab
|
||||||
|
- 登录弹窗下次重新打开时必须恢复初始表单状态:回到默认登录页签、关闭重置密码面板、清空密码 / 验证码 / 图形验证码 / 提示 / 倒计时等本次草稿状态;只允许保留“最近一次成功登录手机号”的本地回填能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 前端状态约束
|
||||||
|
|
||||||
|
## 4.1 AuthGate
|
||||||
|
|
||||||
|
`AuthGate` 需要从“未登录整页拦截器”调整为“平台级账号状态提供器”:
|
||||||
|
|
||||||
|
- `checking / recovering`:仍可显示加载态,避免首屏闪烁
|
||||||
|
- `unauthenticated`:渲染平台内容,同时允许按需打开登录弹窗
|
||||||
|
- `ready`:渲染平台内容和账号能力
|
||||||
|
- `pending_bind_phone`:继续保留当前绑定手机号流程,不在这次入口改造里拆散
|
||||||
|
|
||||||
|
首屏之后的鉴权刷新补充约束:
|
||||||
|
|
||||||
|
- 平台内容已经渲染后,后续 access token 刷新、401 后重试、账号状态后台重算不能再把整棵平台应用卸载成 `checking / recovering` 加载页。
|
||||||
|
- 后台鉴权重算期间需要保持当前平台页与主 Tab 状态,避免用户手动切到“创作 / 存档 / 我的”后因为鉴权事件闪屏回到首页。
|
||||||
|
|
||||||
|
同时需要在 context 中提供:
|
||||||
|
|
||||||
|
- 当前用户
|
||||||
|
- 打开登录弹窗
|
||||||
|
- 打开账号面板
|
||||||
|
- `requireAuth(action)` 能力
|
||||||
|
|
||||||
|
`requireAuth(action)` 约束:
|
||||||
|
|
||||||
|
- 已登录:直接执行 `action`
|
||||||
|
- 未登录:弹出登录弹窗,并缓存 `action`
|
||||||
|
- 登录成功:自动执行缓存的 `action`
|
||||||
|
|
||||||
|
账号入口补充约束:
|
||||||
|
|
||||||
|
- 不再提供 `AuthGate` 层右上角固定悬浮的全局登录 / 账号信息入口
|
||||||
|
- 登录触发统一来自页面内受保护动作、个人页、存档页等明确入口
|
||||||
|
- 账号信息面板只通过页面内按钮打开,不在平台右上角常驻悬浮
|
||||||
|
- 未登录移动端底部导航不展示“我的”时,平台页头必须保留一个直接可点的 `登录` 入口,避免用户只能通过受保护动作被动触发弹窗
|
||||||
|
- 桌面端平台页头的账号胶囊在未登录时主文案必须直接显示 `登录`,不能只显示“进入账户”这类弱入口
|
||||||
|
|
||||||
|
## 4.2 平台首页数据加载
|
||||||
|
|
||||||
|
`PreGameSelectionFlow` 在未登录时只读取:
|
||||||
|
|
||||||
|
- 公开作品广场
|
||||||
|
- 本地浏览历史
|
||||||
|
|
||||||
|
公开作品广场前端请求约束:
|
||||||
|
|
||||||
|
- `listCustomWorldGallery`
|
||||||
|
- `getCustomWorldGalleryDetail`
|
||||||
|
|
||||||
|
这两类公开请求必须走“公开只读请求”通道:
|
||||||
|
|
||||||
|
- 不主动附带 `Authorization`
|
||||||
|
- 不因本地 access token 失效去触发 `/api/auth/refresh`
|
||||||
|
- 若当前请求本身没有携带 access token,也不允许因为返回 `401` 就额外触发 `/api/auth/refresh`
|
||||||
|
- refresh cookie 缺失、refresh 失败、账号状态过期时,不能把首页公开作品广场一起拖成错误态
|
||||||
|
|
||||||
|
受保护工作区恢复补充约束:
|
||||||
|
|
||||||
|
- 若 URL 或 `sessionStorage` 中残留 `customWorldSessionId`、`customWorldOperationId` 等共创工作区恢复标记,未登录态下不能直接请求对应的 Agent 会话或操作接口
|
||||||
|
- 这类“恢复共创工作区”场景要先弹出登录弹窗,登录成功后再恢复原本的工作区或操作上下文
|
||||||
|
- 用户未登录且关闭登录弹窗时,前端保持平台页可浏览状态,不允许持续轮询受保护接口
|
||||||
|
|
||||||
|
未登录时不读取:
|
||||||
|
|
||||||
|
- 自定义世界库
|
||||||
|
- 个人看板
|
||||||
|
- 云端浏览历史
|
||||||
|
- 云端运行时设置
|
||||||
|
- 云端存档快照
|
||||||
|
- 云端存档列表
|
||||||
|
|
||||||
|
未登录态的对应前台表现:
|
||||||
|
|
||||||
|
- “我的创作”显示空态,不显示账号接口错误
|
||||||
|
- “个人页”显示未登录态入口,可手动打开登录弹窗
|
||||||
|
- 音量等运行时设置继续使用本地缓存,不触发 `/api/runtime/settings`
|
||||||
|
- 未登录态不显示“继续远端存档”能力,也不触发 `/api/runtime/save/snapshot`
|
||||||
|
- 未登录态的“存档”Tab 只展示登录引导,不触发 `/api/runtime/profile/save-archives`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 代码落点
|
||||||
|
|
||||||
|
本次实现最少要覆盖:
|
||||||
|
|
||||||
|
- `src/components/auth/AuthGate.tsx`
|
||||||
|
- `src/components/auth/AuthUiContext.ts`
|
||||||
|
- `src/components/auth/LoginScreen.tsx`
|
||||||
|
- `src/components/game-shell/PreGameSelectionFlow.tsx`
|
||||||
|
- `src/components/game-shell/PlatformHomeView.tsx`
|
||||||
|
- `src/components/game-shell/PlatformCreationTypeModal.tsx`
|
||||||
|
|
||||||
|
测试至少覆盖:
|
||||||
|
|
||||||
|
- 未登录时平台首页仍能渲染
|
||||||
|
- 未登录点击作品卡片会打开登录弹窗
|
||||||
|
- 未登录点击创作类型卡片会打开登录弹窗
|
||||||
|
- 登录成功后会继续刚才被拦截的动作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 验收标准
|
||||||
|
|
||||||
|
1. 用户首次进入平台时,不会先看到整页登录页,而是能看到首页内容。
|
||||||
|
2. 未登录点击作品时,直接弹出登录弹窗,登录后自动进入对应作品流。
|
||||||
|
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 文案。
|
||||||
|
- 修改中文文件后通过编码检查。
|
||||||
118
docs/design/PLATFORM_UI_NON_PIXEL_REFRESH_2026-04-19.md
Normal file
118
docs/design/PLATFORM_UI_NON_PIXEL_REFRESH_2026-04-19.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# 平台层 UI 去像素化刷新设计
|
||||||
|
|
||||||
|
更新时间:`2026-04-20`
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
本次刷新只覆盖平台层功能 UI,不改游戏内 HUD、战斗、地图、剧情面板等像素风界面。
|
||||||
|
|
||||||
|
目标有 5 个:
|
||||||
|
|
||||||
|
1. 平台层正文与功能信息不再使用像素字体
|
||||||
|
2. 平台层不再使用像素九宫格边框、像素图标、像素背景纹理这类平台 chrome
|
||||||
|
3. 原有紫蓝深色方案沉淀为平台暗色主题
|
||||||
|
4. 新参考图沉淀为平台亮色主题:白色主面板、粉橘主强调、暖白背景、高亮图卡
|
||||||
|
5. 平台默认使用亮色主题,移动端保持现有布局结构不变,桌面端允许在不改变业务入口的前提下重组为控制台式平台壳层
|
||||||
|
|
||||||
|
## 2. 覆盖范围
|
||||||
|
|
||||||
|
本次统一按 `!gameState.worldType` 的平台态处理,覆盖:
|
||||||
|
|
||||||
|
- 平台首页 `PlatformHomeView`
|
||||||
|
- 作品详情 `PlatformWorldDetailView`
|
||||||
|
- 创作类型弹窗 `PlatformCreationTypeModal`
|
||||||
|
- 平台创作链路中的生成页、结果页、目录页、编辑弹窗
|
||||||
|
|
||||||
|
明确不覆盖:
|
||||||
|
|
||||||
|
- 进入世界后的游戏内 UI
|
||||||
|
- 地图、战斗、剧情面板、角色面板、背包面板等像素 RPG 界面
|
||||||
|
- 世界内容本身的数据图片、角色主图、场景图等作品内容素材
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- “不再引用像素素材”指平台 chrome 不再依赖像素框、像素按钮、像素关闭图标、像素底纹等 UI 资源
|
||||||
|
- 作品内容图仍可展示,但平台层不再用 `image-rendering: pixelated` 强化像素感
|
||||||
|
|
||||||
|
## 3. 视觉原则
|
||||||
|
|
||||||
|
### 3.1 风格来源
|
||||||
|
|
||||||
|
直接对齐现有登录页和绑定手机号页的成熟样式,并吸收本次参考图的桌面端气质:
|
||||||
|
|
||||||
|
- 暗色主题:顶部与边缘的紫蓝径向高光 + 深色纵向渐变背景
|
||||||
|
- 亮色主题:暖白控制台外壳 + 粉橘主强调 + 轻紫细节高光
|
||||||
|
- 大圆角卡片
|
||||||
|
- 半透明玻璃质感
|
||||||
|
- 平台正文与功能信息统一使用 `Inter + Noto Serif SC`
|
||||||
|
- 左上角品牌区允许使用专用像素字标组件或直接使用 `Fusion Pixel` 文本,但仅限品牌 logo,不向正文、按钮、标签扩散
|
||||||
|
- 品牌 logo 只能复用游戏现有 `Fusion Pixel`,不允许再引入第二套像素字体文件
|
||||||
|
|
||||||
|
主题基准:
|
||||||
|
|
||||||
|
- 暗色主题:
|
||||||
|
底色以深靛蓝、深紫黑为主,高光以亮紫、蓝青为主
|
||||||
|
- 亮色主题:
|
||||||
|
底色以暖白、浅粉白、浅橘白为主,强调色以高饱和粉色、橘粉色为主,局部可带少量紫色作装饰
|
||||||
|
- 平台默认主题使用亮色主题;暗色主题保留为可切换方案,不作为当前默认展示
|
||||||
|
|
||||||
|
### 3.2 排版
|
||||||
|
|
||||||
|
- 平台层正文、按钮、说明、功能标签统一使用非像素字体
|
||||||
|
- 左上角 `叙世 / GENARRATIVE` 品牌字标允许单独做成像素化 logo
|
||||||
|
- `GENARRATIVE` 与 `叙世` 都优先直接使用游戏内同款 `Fusion Pixel`
|
||||||
|
- 品牌字标默认保持正常像素字观感,禁止再叠双层粗阴影或手动加粗到影响识别
|
||||||
|
- 品牌字标直接使用字体文件内原字形,不额外做运行时描字、轮廓拼字或伪粗体处理
|
||||||
|
- 主标题保留明显层级,但不再做像素描边效果
|
||||||
|
- 微型标签维持高字距英文/中文短标签,用来保留产品感和秩序感
|
||||||
|
|
||||||
|
### 3.3 组件约束
|
||||||
|
|
||||||
|
- 面板:使用玻璃卡片,不再用九宫格像素框
|
||||||
|
- 按钮:使用圆角胶囊按钮或渐变主按钮,不再用像素按钮框
|
||||||
|
- 图标:优先使用 `lucide-react`
|
||||||
|
- Tab:移动端底部结构不变,但图标与底座改成非像素风;桌面端切换为左侧纵向导航轨道
|
||||||
|
- 弹窗:沿用登录页的圆角浮层和半透明遮罩,不再使用像素弹窗边框
|
||||||
|
- 桌面壳层:首页允许增加顶部工具栏、左侧导航轨、中央内容舞台与右侧趋势面板的组合
|
||||||
|
- 登录页、绑定手机号、账户弹窗、平台详情、创作生成页、结果页、编辑弹窗都必须共享同一套平台主题 token,禁止再各自写一套独立旧色板
|
||||||
|
- 创作中心、Agent 工作台、草稿详情抽屉、资产工坊、启动弹窗、生成弹窗这类二三级平台面板必须显式挂载平台主题壳层或平台 remap 容器,禁止直接在局部面板里写死旧深色 modal 底和旧输入框底色
|
||||||
|
- 平台“我的”页中的“设置”入口必须打开真正的设置面板;账号信息、设备管理、安全状态属于设置面板中的分区,不允许再把账号信息弹层直接充当设置页
|
||||||
|
- 设置面板必须支持平台亮色 / 暗色主题切换,并复用同一套平台 token 驱动登录页、首页、详情页与二三级面板
|
||||||
|
- 首页移动端底部 Tab 与桌面侧边导航的图标底座、图标颜色、文字状态必须全部由平台 token 驱动;暗色主题下不得出现过浅底座和错误文字色,亮色主题下不得残留旧灰蓝 inactive 状态
|
||||||
|
- 首页、存档页、作品详情这类平台主导航与局部 Tab 的 active fill、active shadow、icon shell fill 必须全部来自主题 token;暗色主题禁止继续复用亮色主题的粉橘高光、白色 active 底座
|
||||||
|
- 创作链路中的吸顶返回栏、目录 Tab 条、搜索工具条也必须走平台亮暗主题 token;暗色主题禁止继续写死暖白渐变或浅粉背景作为顶部衬底
|
||||||
|
- “我的”页账号主卡必须跟随平台亮 / 暗主题联动,不允许继续写死浅色渐变卡面与 `slate` 系按钮
|
||||||
|
|
||||||
|
## 4. 交互与布局约束
|
||||||
|
|
||||||
|
- 移动端保持原有页面布局层级、区块顺序、操作入口位置不变
|
||||||
|
- 桌面端首页允许参考图示重组为“顶部工具栏 + 左侧纵向导航 + 主 Hero 卡 + 右侧趋势列表 + 下方内容卡组”
|
||||||
|
- 桌面端的重组只改变视觉排布;自 `2026-04-19` 起平台主入口调整为“首页 / 创作 / 存档 / 我的”,四个入口的操作路径都必须保持清晰稳定
|
||||||
|
- 移动端优先,底部 tab 与主卡片点击区域不能缩小
|
||||||
|
- 不在平台 UI 面板里额外堆砌规则说明
|
||||||
|
- 所有视觉替换必须是局部补丁,不做无必要的大规模结构重写
|
||||||
|
|
||||||
|
## 5. 实现约束
|
||||||
|
|
||||||
|
- 平台态从 `fusion-pixel-app` 中隔离,避免被全局像素字体覆盖
|
||||||
|
- 品牌区禁止新增额外像素字体包;平台层只允许保留现有 `public/fusion-pixel.ttf` 这一份像素字体资源
|
||||||
|
- 平台态背景不再使用 `/UI/Background_fill.png`
|
||||||
|
- 新样式优先沉淀为平台专用 class / theme token,避免把游戏内像素 class 改坏
|
||||||
|
- 平台默认挂载亮色主题 class,旧紫蓝方案保留为暗色主题 class
|
||||||
|
- 亮色主题需要补齐统一的 overlay、progress track、status pill token,登录弹层与二三级功能面板禁止继续沿用旧深色遮罩与紫蓝强调残留
|
||||||
|
- 亮色主题下平台壳层与各个 Tab 页的 page stage 必须以暖白底为主,禁止继续让高饱和深粉底或旧深色底透成页面主背景
|
||||||
|
- 亮色主题下平台主内容区、page stage、移动端底部 Tab 容器都必须使用接近实色的暖白底,禁止继续用高透明度浅色层叠在深底上造成整体发灰
|
||||||
|
- 平台态中仍保留旧 Tailwind 深色类的历史组件,必须通过平台 remap 容器或平台专用 class 统一收口,不能放任 `bg-[#111318]`、`bg-black/*`、`bg-white/*` 这类旧类在亮色主题下直接裸露
|
||||||
|
- 编辑弹窗保留业务结构与表单逻辑,只替换壳层样式
|
||||||
|
|
||||||
|
## 6. 验收标准
|
||||||
|
|
||||||
|
达到以下结果才算完成:
|
||||||
|
|
||||||
|
1. 除左上角品牌像素字标外,平台首页、详情、登录、绑定手机号、账户弹窗、创作入口、创作结果页不再出现像素字体
|
||||||
|
2. 平台层按钮、面板、关闭按钮、底部 tab 不再依赖像素 UI 素材
|
||||||
|
3. 平台默认展示亮色主题,暗色主题保留为独立主题方案
|
||||||
|
4. 平台层二三级面板、表单、状态卡、弹窗与登录体系不再残留旧金橙 / 青蓝 / 深黑混搭方案
|
||||||
|
5. 平台层世界封面与角色预览不再使用 `pixelated` 渲染
|
||||||
|
6. 游戏内像素 UI 保持原样,不出现误改
|
||||||
|
7. 手机端布局保持稳定,桌面端在参考图方向下完成控制台化重组
|
||||||
39
docs/design/README.md
Normal file
39
docs/design/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 系统设计
|
||||||
|
|
||||||
|
这一组是玩法、关系、物品、对话等系统设计文档,偏“应该怎么设计”而不是“现在哪里出问题”。
|
||||||
|
|
||||||
|
## 文档列表
|
||||||
|
|
||||||
|
- [CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md](./CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md):自定义世界里创作者输入与 AI 分工边界设计。
|
||||||
|
- [CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md](./CUSTOM_WORLD_CREATOR_MANUAL_AI_SYSTEM_BALANCE_DESIGN_2026-04-12.md):自定义世界创作里“手填锚点 / AI 可改初稿 / 系统托管层”的平衡设计。
|
||||||
|
- [CUSTOM_WORLD_CREATOR_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 游戏全剧情的工作流程与交付模板。
|
||||||
|
- [EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md](./EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md):配装构筑与合成/锻造闭环设计。
|
||||||
|
- [COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md](./COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md):角色首遇感、关系分层解锁、私聊系统设计。
|
||||||
|
- [NPC_HIGH_AFFINITY_CHAT_QUEST_OFFER_FLOW_2026-04-19.md](./NPC_HIGH_AFFINITY_CHAT_QUEST_OFFER_FLOW_2026-04-19.md):高好感角色在聊天内自然提出委托,并支持查看、更换、放弃、领取的流程设计。
|
||||||
|
- [SCENE_CHAPTER_LOOP_AND_FIRST_ENTRY_CHAPTER_QUEST_DESIGN_2026-04-08.md](./SCENE_CHAPTER_LOOP_AND_FIRST_ENTRY_CHAPTER_QUEST_DESIGN_2026-04-08.md):把每个场景收束成章节单元,并在首进场景时开启章节任务的设计稿。
|
||||||
|
- [SCENE_CHAPTER_BENCHMARK_GAP_AND_AI_NATIVE_EXPERIENCE_SUPPLEMENT_2026-04-08.md](./SCENE_CHAPTER_BENCHMARK_GAP_AND_AI_NATIVE_EXPERIENCE_SUPPLEMENT_2026-04-08.md):对标仙剑、博德之门、黑神话,分析单场景章节的体验缺口,并给出 AI 原生补强方案。
|
||||||
|
- [npc-conversation-situation-draft.md](./npc-conversation-situation-draft.md):NPC 对话阶段和情景注入草案。
|
||||||
|
|
||||||
|
## 推荐阅读
|
||||||
|
|
||||||
|
- 做物品、Build、锻造相关需求时,先看前两份。
|
||||||
|
- 做 RPG 全剧情规划、主支线矩阵、角色线、场景章节与剧情交付模板时,先看新增的全剧情策划流程。
|
||||||
|
- 做自定义世界创作工作台、创作者输入边界、AI 分工设计时,先看第一份。
|
||||||
|
- 做“哪些内容必须让创作者手填、哪些适合 AI 先生成再改、哪些必须系统托管”这类分层设计时,优先看新增的输入平衡设计稿。
|
||||||
|
- 做“是否应该转成纯 Agent 式创作工具、转了之后前后台各该怎么收口”这类产品方向评估时,优先看新增的纯 Agent 对比与转型设计稿。
|
||||||
|
- 做自定义世界去模板依赖、跨题材泛化、兼容迁移设计时,优先看新增的去模板化优化设计稿。
|
||||||
|
- 做“模板依赖如何真正变成自定义世界自有设定层”的具体迁移方案时,优先看新增的自有设定层优化方案。
|
||||||
|
- 做角色关系、同伴互动、对话表现时,先看后两份。
|
||||||
|
- 做“高好感聊天里如何顺着上下文自然抛出委托、并让任务在聊天内领取”的需求时,优先看新增的聊天委托流程设计稿。
|
||||||
|
- 做剧情引擎章节化、场景闭环、章节任务接入时,优先看新增的场景章节设计稿。
|
||||||
|
- 做“单章节体验还缺什么、该补哪种情感 / 抉择 / 试炼模块”时,优先看新增的章节对标补强设计稿。
|
||||||
|
- 做等级成长、任务/击败敌对 NPC 发经验、章节经验速度评估、NPC 自动定级时,优先看新增的等级系统设计稿。
|
||||||
|
- 如果要判断是否符合目标,再和 `docs/prd/` 中对应 PRD 对照阅读。
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user