# AGENTS.md

这份文件定义 AI agent 在本仓库，以及类似的 Zust 服务端 + 一个或多个客户端项目中工作时的长期规则。具体业务规则、产品计划、数据模型和接口语义，应放在 `README.md`、功能文档或对应模块文档里。

规范本身不是目的。真正重要的是规范背后的思想：让系统更简单、更短、更直接，让读代码和改代码的人能把注意力集中在当前问题上。

## 1. 基本原则和设计思路

### 核心行为

改代码前，先理解用户请求、附近代码和现有架构。重要假设要显式说出来。需求有多个合理解释时，先指出分歧，不要静默选择一个高风险方向。

优先使用能正确解决问题的最小改动。不要添加未被要求的功能、配置层、兼容分支或单次使用的抽象。如果实现开始明显大于问题本身，就收缩它。

保持改动外科手术式精准。只碰完成任务必须修改的文件，匹配本地风格，不要顺手重构邻近代码。只删除由你本次改动制造出的无用 import、变量、函数或生成产物。发现无关问题时可以说明，不要静默改写。

把任务转成可验证目标。修 bug 时，实际可行的话先复现或隔离失败，再修复。做功能时，明确可观察行为并检查。做重构时，如果项目有合理命令，尽量在前后验证行为。

### Simple / Short / Straight

Simple：简单是跨越语言、框架和设计模式的最高原则。小即是美。能用一个清楚模块解决的问题，不要拆成多层概念；能用直接数据结构表达的问题，不要引入协议、模板或抽象工厂。

Short：简短不是压缩代码，而是在保证可读性的前提下减少不必要内容。代码越短越好，作用域越短越好，生命周期越短越好。变量、对象、连接、事务和临时状态都应尽早创建、尽快使用、及时结束。

Straight：直接意味着读者不需要记住很远处的定义。假设代码读者只有很短的工作记忆，也应该能顺着当前文件、当前函数、当前作用域看懂。描述和定义尽量靠近使用点；不言自明的代码才是合格代码。

Helper 不是天然的好设计。很多 helper 是 Java 式工程习惯带来的过度间接：一个函数、一个表达式就能说清楚的事情，不要拆成一堆只用过一次两次的小 helper。helper 会把读者的注意力从当前逻辑拉走，只有当它真正减少重复、隔离复杂性或表达稳定概念时才值得存在。

整体方向应从“方便计算机”转向“方便人”。现代编辑器已经提供实时提示、类型跳转、补全和搜索，不要再为了机器或陈旧工具习惯牺牲代码本身的可读性。

设计权衡的最终目标是帮助程序员 focus。人在聚焦时非常强大，所以语言选择、模块边界、命名、复用方式和接口形状，都应减少读者在无关细节上的记忆负担。

### 命名原则

被淘汰的命名习惯不要继承。例如匈牙利命名法中的 `lpsz` 一类前缀，来自指针和编辑器能力都很弱的时代。今天更重要的是表达领域含义、对象职责和动作意图。

静态定义对象、类型、仓库、映射表等名词概念使用首字母大写。推荐“名词类型 + 容器/角色”的形式，例如 `ProductStore`、`IDNameMap`。

方法和函数表示动作，首字母小写。可以使用 `addMember`，也可以按语言习惯使用 `add_member`。独立函数应完整说明动作和对象，例如 `addMember` 表示“增加一个成员”。如果函数已经处在明确对象或作用域中，可以简化，例如 `ProductStore.add`。

命名需要在清晰和短小之间权衡。更长的名字更容易理解，更短的名字能让视觉范围内容纳更多有效信息。短单词不需要缩写；惯用缩写可以保留，例如 `ID`、`pt`。缩写以具有足够区分度为原则，不要制造只有作者知道的暗号。

### 复用原则

DRY 的目标不是消灭所有重复，而是消灭会导致理解和修改成本上升的重复。少量平铺、清楚、稳定、可生成的重复，往往比过早抽象更容易维护。

过程/函数复用只复用算法，不包含环境和数据结构。它简单明了，但隐含环境要求容易失控。只有依赖少、副作用少、输入输出清楚时，才优先使用这种复用。

对象复用把算法和数据结构放在一起，适合表达有稳定状态和行为边界的概念。

继承复用是基于设计的复用。它适用于设计模式固定、需求简单固定的情况，但要求先有父类再有派生类，容易逼迫工程反复重构父类。能不用就不用；必须使用时，类层次不要超过五层。

组合复用是基于工程的复用。它把功能块以对象方式组装起来，更符合人类逐步认识问题的过程，但也会增加装配复杂度。优先使用组合代替继承，但不要为了组合而制造过多小对象。

基于 Interface/Trait 的复用，也可以理解为基于视角、剖面或能力的复用。它抽取的是“这个对象在某个场景下能做什么”，比继承更接近一维重构。语言支持时，优先使用接口或 trait 表达可替换能力。

### 编码、存储和日志

源代码和所有文本文件统一使用 UTF-8 编码。严禁使用带 BOM 的 UTF-8。MySQL 字符集统一使用 `utf8mb4`。

Redis 适合高性能运行期状态、缓存和相对复杂的内存数据结构，例如 KV、HashMap、HashSet、会话、短期索引和热点数据。不要把 Redis 当成没有边界的持久数据库；需要强一致关系查询时，应回到关系数据库或明确的持久层。

MySQL 适合关系数据和有大范围查询需求的业务数据。应用场景中的查询必须建立在索引基础上。所有表和字段都需要注释；表必须有主键；统一使用 InnoDB；字符集使用 `utf8mb4`。禁止使用分区表，优先从业务侧分表。禁止使用外键，关系一致性由业务层实现。禁止在数据库中存储图片、文件等大数据。

消息队列 MQ 用于异步、削峰和应用解耦。只有当业务确实需要跨进程异步处理、失败重试、缓冲压力或隔离上下游时才引入 MQ。可选技术包括 Kafka、RocketMQ、Redis Stream 等；不要为了“架构完整”提前引入队列。

日志要服务于定位问题，而不是制造噪声。关键入口、鉴权失败、参数错误、状态转移、事务失败、外部系统调用失败都应记录足够上下文。不要记录密码、token、私钥、完整个人敏感信息或大体积 payload。日志字段应稳定，便于搜索和聚合。

## 2. Zust 语言的特性和规则

把 Zust 当成 Rust-like 的动态强类型语言，不要当成 JavaScript 或 Python。显式处理控制流、运行时类型、可变性边界和返回值。

不要把同一个变量复用成互不相关的运行时类型。如果 `root::get(...)` 可能返回空值，必须显式分支：

```zs
let existing = root::get(path);
if existing.is_list() {
    // 使用已有 list
} else {
    // 创建新的 list
}
```

不要写 JavaScript 风格的 truthy fallback：

```zs
let items = req.items || []
let user_id = req.user_id || ""
let count = req.count || 0
```

应校验具体类型：

```zs
let items = req.items;
if !items.is_list() {
    return { ok: false, error: "items must be list" };
}
```

结构化输入就使用结构化数据。可以传 map/list 时，不要把列表或记录编码成逗号分隔字符串。

ROOT 是运行期状态树，按这个事实直接使用它。

- `root::add` 和 `root::add_fn` 写入 ROOT 路径。
- `start.zs` 或项目启动脚本应初始化配置、地图、数据库、路由、handler 和必要运行期节点。
- 简单项目可以由 `start.zs` 直接注册 HTTP/WebSocket/console 入口；复杂项目应由 `start.zs` 调用各系统的 `start`/`init` 函数，再让系统内部注册自己的 ROOT 节点和分发入口。
- 固定的模块内部调用应直接使用模块函数。
- 只有真正需要运行时分发的边界才使用 `root::add_fn`，例如 HTTP、WebSocket、console/admin 入口、动态世界 handler 或插件式扩展点。
- 不要把简单 ROOT 路径藏进 helper。规则很直观时，优先写可见字符串，例如 `"redis/users/" + account_id`。一个表达式能看懂的路径，不要为了“看起来抽象”拆成只用过一次两次的 helper。
- 只有路径规则足够复杂、helper 能减少真实错误时，才添加 path helper。

Zust 很容易被生成或机械重复。清晰、稳定、易生成的一致结构，比聪明的复用更重要。

不要用跨地图、跨业务、跨模块的默认值掩盖必需配置缺失。全局 fallback 会让坏数据继续流动，并让 bug 更难定位。

错误模式：

```zs
if !world.contains("spawns") {
    return default_spawn();
}
```

更好的模式：

```zs
if !world.contains("spawns") {
    return { ok: false, error: "world spawns missing", world: world.world_name };
}
```

默认值只允许属于当前对象自身，并且语义明确，例如当前 panel 的可选 placement、当前模型的 idle animation。必需数据缺失时，应在启动或请求阶段明确失败。

遇到 Zust compiler、JIT、VM 或 ROOT 错误时，先定位具体脚本、函数、语句和运行时错误含义，再改业务逻辑。判断修复点属于 VM/parser/runtime，还是脚本。不要用无关改写绕过 VM 错误。

有服务端 console 时，优先用 console 调试 ROOT 和脚本。通过 `root::get("node/path")`、`module::function(...)` 等命令确认注册、运行期状态和 handler 输出，再进入完整客户端验证。

## 3. Zust + Rust 的服务器架构设计

典型 Zust + Rust 服务端项目通常分为这些层：

```text
client(s)
  -> HTTP / WebSocket / platform bridge
  -> Rust server
  -> Zust route dispatch
  -> Zust business scripts
  -> database / ROOT / runtime state
```

常见目录职责：

```text
server/
  src/              # HTTP、WebSocket、静态文件、上传处理、分发
  scripts/          # Zust 启动、路由、系统、世界、UI、业务逻辑
  static/           # 被服务端托管的资源，或构建后的客户端产物

client/
  shared/           # 跨端 DTO、API 契约、共享状态/页面
  apps/*            # Web、Android、小程序、桌面端、管理端或其他壳工程

docs/
  *.md              # 架构说明和接入文档
```

服务端拥有游戏或业务状态的权威性。客户端输入应被视为意图，而不是真实状态。移动、碰撞、战斗、任务进度、经济、背包、鉴权和状态推进默认都属于服务端，除非项目文档另有说明。

Rust 服务端负责基础设施：

- HTTP 路由、headers、body 限制、静态文件、上传和 WebSocket 建连。
- 把请求 payload 规范化后交给 Zust。
- 脚本加载、ROOT 初始化、路由分发和响应转换。
- 处理平台传输约束，例如 WebSocket 鉴权限制。

Zust 脚本负责领域行为：

- 启动配置和路由注册。
- 业务校验、权限检查、状态变更和响应数据。
- 服务端驱动 UI 时的 UI、dialog、panel 结构。
- ROOT 中的运行期状态和数据库中的持久状态。

### HTTP 分发

HTTP 层应只做直接、可预测的映射：`POST /api/foo` 映射到 `local/http/post/foo`，`GET /api/foo` 映射到 `local/http/get/foo`，不要在 Rust HTTP 层塞业务分支。

简单项目可以在 `start.zs` 里直接注册这些 `local/http/*` handler 并完成分发。复杂项目则让 `start.zs` 调用各系统自己的启动函数，由系统内部注册和分发自己的 HTTP 路径。

```text
HTTP method + path
  -> local/http/{method}/{path}
  -> root::send_msg(...) 或等价分发
  -> Zust handler
  -> map/list response
  -> HTTP response conversion
```

开发环境可以直接使用 HTTP；HTTPS 应由统一网关、反向代理或部署层提供，不要让每个业务服务重复实现证书和 TLS 细节。

简单、公开、对后台完全只读的接口可以使用 GET 语义。其他情况使用 POST 语义；没有把握时也使用 POST。公开图片获取、公开产品信息查询等不需要考虑参数安全的场景，可以使用 query 参数。其他大部分接口使用 POST + JSON body。

请求 payload 应在保留字段中保存传输元数据，例如 `@method`、`@path`、`@header`、`@query`，再合并通过校验的 JSON body 字段。

响应 map 可以使用 `@status`、`@content-type`、`@body` 等保留字段。没有自定义响应需求时，普通 map/list 可直接作为 JSON 返回。

HTTP 状态码 `200` 表示调用路径正确、请求参数格式正确、服务端成功返回业务结构。业务返回尽量使用 JSON 数据结构；一般使用 `code` 表示业务返回码，`0` 表示正确，使用 `message` 或 `msg` 表示返回信息。

multipart 上传通常应先在 Rust 层处理。文件名、content-type、字节数据和业务字段解析完成后，再打包成有明确类型的 payload 交给 Zust。不要假设上传路由和普通 JSON 路由完全一样。

### WebSocket 分发

WebSocket 分发应拆开 auth、connect、message、disconnect handler。推荐注册稳定路径，例如：

```text
local/ws_handlers/auth
local/ws_handlers/connect
local/ws_handlers/message
local/ws_handlers/disconnect
```

### 事务和调试

任何包含多次持久化写入的业务动作，都需要一个事务，或等价的原子边界。

通用顺序：

1. 校验输入和权限。
2. 锁定或读取必须保持一致的状态。
3. 一次性完成状态、流水、索引和缓存更新。
4. 只提交一次。
5. 失败时整体回滚。

不要让业务流程停留在半成功状态。

服务端问题的调试顺序：

1. 确认路由注册在预期 ROOT 路径。
2. 确认 method/path 或 WebSocket message type 与注册一致。
3. 确认 payload 字段和运行时类型。
4. 运行或隔离 Zust handler。
5. 确认数据库写入和事务行为。
6. 确认响应结构符合 Rust 转换层约定。

### 验证命令

使用能证明本次改动的最窄命令。

```bash
cd server
cargo test
cargo check
cargo run
cargo run --bin import_tmx_maps
```

如果命令不可用、太慢或被环境限制阻塞，要说明原因，并说明实际验证了什么。

## 4. 客户端的基本原则

这里的“客户端”指任何 UI 或平台层：共享跨端代码、Web、Android、iOS、小程序、桌面端、管理端、测试壳或其他 runtime。

客户端职责应保持窄边界：

- 渲染服务端状态。
- 收集用户输入并发送意图。
- 维护真正属于表现层的本地 UI 状态。
- 为网络、存储、图片、文件、上传、状态栏、权限和 native view 实现平台 adapter。

如果项目存在共享客户端层，DTO、API 契约、消息解析和核心页面/状态逻辑应放在那里。平台壳负责适配平台能力，不要 fork 业务行为。

生成产物不是源码。除非用户明确要求修改某个生成产物，并且知道它会被构建流程覆盖，否则不要手改构建输出。

### 服务端驱动 UI

当服务端下发 UI 数据时，客户端只渲染这些数据。客户端不得发明缺失的视觉、文字、动作或默认值来掩盖协议错误。

- `images` 描述外观。
- `hotspots` 描述点击/触摸区域，并回传 `panel_id + action_id`。
- 文字、图片、选中态和可见性应来自服务端数据。
- 所有后续可能变化的 UI 元素都需要稳定 `id`。
- 首包应发送完整 panel/dialog 结构。
- 后续更新应使用稳定 patch 协议，例如 `panel_id.element_id -> value`。

如果服务端没有下发必需图片、hotspot、id 或文字，调试阶段应暴露缺失信息，而不是静默画一个替代品。

### 响应式和跨端

对任何响应式客户端框架，都不要先把动态列表算成一次性局部快照，再长期依赖它。应在 render/update 边界内读取响应式状态，或使用该框架已验证的列表重建模式。

UI 不更新时，按顺序检查请求是否成功、回调是否执行、状态是否变更、渲染是否失效/重建、平台 adapter 是否正确。

Web 可用不代表 native 或小程序可用。需要分别确认平台 API base URL、网络权限、token 存储、图片 adapter、上传路径、状态栏处理和 WebSocket 鉴权约束。

mock connector 和测试账号也应走与真实连接一致的鉴权、刷新、解析和更新路径，除非用户明确要求纯 UI mock。

### 资源和 CDN

资源要尽可能复用，不要重复下载。图片、Spine/动画资源、地图、脚本包、字体和构建产物都应有稳定 URL、缓存策略和复用路径。

可长期缓存的资源应使用内容哈希、版本号或改名方式发布。AI 时代生成和批量改名的成本更低，因此更新资源时优先通过新文件名或新版本路径触发刷新，而不是让客户端反复下载同名资源。

静态资源应尽量走 CDN 提速。服务端只负责生成、引用、鉴权或回源控制；高频访问的大图片、地图、动画、构建包和公共库应交给 CDN 缓存分发。

图片不显示时，先调试真实路径，不要先加 placeholder 逻辑。

检查顺序：

1. 状态中是否包含预期 URL 或资源路径。
2. URL 是否可直接访问。
3. 响应字节是否与 content-type 匹配。
4. 图片 source 是否在 render/update 边界内读取实时状态。
5. 动态列表是否真的重建或 patch。
6. native 或平台 image adapter 是否已注册。

placeholder 可以是最终产品体验的一部分，但不能作为调试捷径掩盖坏数据。

### 客户端验证

客户端问题的调试顺序：

1. 确认请求或 WebSocket 消息已发送。
2. 确认响应已到达并解析成功。
3. 确认回调或消息处理已执行。
4. 确认状态已更新。
5. 确认 UI 已失效、重建或 patch。
6. 确认平台 actual/adapter 行为与共享语义一致。

常用客户端命令：

```bash
cd client
gradle :apps:miniapp:jsMiniAppDevelopmentWebpack
./gradlew build
```

## 不要做的事

- 不要添加超出请求范围的业务功能。
- 不要为尚不明确的未来需求引入宽泛抽象。
- 不要在 Zust 中使用 JS 风格 fallback。
- 不要把结构化 payload 表达成逗号分隔字符串。
- 不要给必需配置添加全局兜底。
- 不要用 placeholder 掩盖图片、UI、协议或响应式状态 bug。
- 不要把生成的客户端输出当源码。
- 不要把一个业务事务拆成多个独立提交。
- 不要假设一个客户端 runtime 可用就代表所有 runtime 可用。
- 不要用客户端业务逻辑取代服务端权威状态。
- 不要让可复用静态资源因为同名更新或缺少缓存策略而被反复下载。
