diff --git a/plan.md b/plan.md index a1645a6..5782f04 100644 --- a/plan.md +++ b/plan.md @@ -28,6 +28,11 @@ ## 1. 用户设想的场景(2026-09-26,本计划的新方向) +★ **这是抽象模型的一个用例,不是全部**(用户 2026-09-26: +「机位这个只是一个具体应用场景,而不是只有这一个场景」)。 +它用来验证 §1b 那套「状态 + 事件 + 操作」是否立得住 —— +若拍照能跑通,agent 的日常感知/交互也就能跑通。 + > 我设想的场景是玩家可以在聊天框给 agent 发消息,然后比如让 agent 给他**拍照**, > 如果是他和某个景物的**合照**,agent 可以先通过 **ass 粗渲染**,找好角度 > 再去**渲染实图**。 @@ -38,48 +43,127 @@ ① 玩家在聊天框发消息 → agent 通过统一接口收到 ② agent 要"给玩家拍合照" → 需要:玩家位置/朝向 + 景物位置 + 相机可摆放范围 ③ 选角度 → 用 **ass 粗渲染**(低成本、快)试若干机位, - 挑一个好的,再用**真渲染**出图 + 挑一个好的,再用**细渲染**出图 ``` -### ★ ass 粗渲染是什么(这里是关键设计点) +★ 注意:这只是粗渲染的**一个用例**。它的本职是 **agent 的常驻感知层** +(日常游玩),见 §4。 -「ass」= **ASCII / 低保真近似**(用户 2026-09-26 提出)。 -它要能回答「这个角度拍出来大概是什么样」,但**不启动 MC**: +### ★ 粗渲染与细渲染的分工(用户 2026-09-26 明确,关键) -- **不是**把真渲染结果降采样 —— 那还是要跑 MC,没省。 -- **是**用一个**纯数据的近似模型**:从方块数据直接推 - 「视线方向上有哪些方块、遮挡关系、大致明暗」, - 在文本/低分辨率网格上画出来。 +> **粗渲染是 agent 日常游玩用的**;**细渲染是出图和 agent 判断精细状态用的**; +> **细渲染是单次调用的而非常驻的**。 -**为什么它值得做**(三个具体收益): +| | 粗渲染(ass) | 细渲染 | +|---|---|---| +| 用途 | **agent 日常游玩** | **出图 + 判断精细状态** | +| 生命周期 | **常驻**(随 agent 会话) | **单次调用**(起 → 渲染 → 销毁) | +| 频率 | 高(agent 每一步都可能问) | 低(要图 / 需要精细判断时) | +| 延迟预算 | 毫秒级 | 20-40s 可接受 | +| 依赖 | 纯 Python(**无 JVM**) | JVM + 完整 MC 栈 + 加载器/光影 | +| 数据源 | 轻量世界快照 | 完整渲染管线 | -1. **试错便宜**:一次真渲染要起 JVM(~20-40s),粗渲染是毫秒级 - ⇒ agent 可以试几十个机位,只把最好的那一个送进真渲染。 -2. **给 agent 可读的中间结果**:ASCII 图能被 LLM 直接「看」和理解, - 而 PNG 需要多模态输入。agent 可以在文本空间里推理"该往哪挪"。 -3. **失败模式友好**:机位选错时,粗渲染已经暴露(比如"镜头埋在地里"、 - "景物被树挡住"),不用浪费一次完整渲染。 +★ **这条分工决定了架构**: +- 粗渲染**不是**细渲染的降级,而是 agent 的**常驻感知层** —— + 它要回答 agent 平时的所有感知问题(周围有什么、挡不挡视线、 + 前面走得通吗、那边是什么),这些都必须廉价。 +- 细渲染**按需起、用完走**(JVM 启停成本不敏感)⇒ 慢一点没关系。 +- ⇒ 所以**「粗渲染↔细渲染一致性」不是刚需**:两者服务于不同问题, + 不要求粗渲染预测细渲染的像素。真正要测的是 + **各自对各自用途的正确性**(见 4.5)。 -### mineflayer 的参考价值(用户 2026-09-26 点名) +--- -> 记得参考 mineflayer 的实现,我觉得它对于**行为触发条件**与**展示方式** -> 以及 **ass 粗略绘图**的形式很优秀。 +## 1b. 抽象模型:参考 mineflayer 的「状态 + 事件 + 操作」三层 -mineflayer 相关能力(本机已有:`/home/newqqagent/mcbridge/node/node_modules/mineflayer`): +> 至于事件更新通报与操作抽象,我觉得可以参考 mineflayer, +> 使用它的抽象模型调用我们的后端。(用户 2026-09-26) -| mineflayer 的做法 | 对我们的启发 | -|---|---| -| **事件驱动**:`bot.on('chat')` / `'entitySpawn'` / `'physicsTick'` | 行为触发条件:聊天消息 → 意图识别 → 动作(拍照)| -| **`bot.chat()` / `bot.entity.position`** | agent 拿到玩家位置/朝向;回复走聊天 | -| **`prismarine-viewer`**:三种视角(first_person / third_person / fixed)| ★ 相机摆放的**现成语义**:合照 = third_person + 朝向景物 | -| **`.saveScreenshot()`** | 截图接口的形态 | -| **blockAt / findBlock / raycast** | ★ **粗渲染的核心原语**:从方块数据做视线求交,算遮挡 | -| **minecraft-data**:权威方块/物品数据 | 服务端交互的注册表来源之一 | +★ **注意**:机位/拍照只是这个抽象的**一个用例**,不是目的。 +下面这套模型要覆盖 agent 的日常:感知、移动、交互、观察、问答。 -**⚠️ 但要注意 mineflayer 与我们的根本差别**: -mineflayer 是**真连服务端**、有完整世界状态; -我们是**离屏渲染 + 自喂数据**。所以它的**行为触发条件**能直接借鉴, -而**世界访问**要走我们自己的数据通路(存档 / 服务端注入)。 +### 1b.1 mineflayer 的实际形态(已读源码核实) + +``` +① 状态(挂在 bot 上的对象,agent 直接读) + world 方块/生物群系/光照 entities {id: Entity} + players {username: Player} time / game / controlState + inventory / experience / tablist / teamMap / spawnPoint / settings + +② 事件(bot.emit(name, ...),细分层用 ':') + 消息类 message / messagestr / chat / chat: / actionBar + 世界类 blockUpdate / chunkColumnLoad / weatherUpdate + 实体类 entitySpawn / entityMoved / playerJoined / playerLeft + 玩家类 moved / heldItemChanged / lookingAt / physicsTick + 流程类 login / spawn / health / death / kicked / error + +③ 操作(bot.(...)) + 查询 blockAt / findBlocks / raycast / nearestEntity + 移动 move / look / setControlState / simpleClick + 交互 dig / placeBlock / activateBlock / attackEntity / equip + 社交 chat / whisper / tell + 观察 saveScreenshot + 容器 openContainer / clickWindow / closeWindow / … +``` + +**★ 三个值得直接照搬的设计**: + +1. **状态是「对象」而不是「方法调用」** —— agent 读 `bot.entities` / + `bot.world` 就拿到快照,不必每次查询。 + ⇒ 我们的粗渲染(常驻)就该是这个角色。 + +2. **事件分「粗粒度 + 细分层」**(`message` 与 `chat:` 并存) + —— agent 可以只订阅粗粒度(省 token),需要时再挂细分。 + ⇒ 避免「每个方块变化都推给 agent」把上下文淹掉。 + +3. **操作是动词,参数是结构化对象** + —— `bot.findBlocks({ matching, maxDistance, count })` + ⇒ 不是「一条命令 + 位置参数」,而是「意图 + 可选约束」。 + +### 1b.2 映射到我们的后端 + +``` +mineflayer 概念 我们的实现 备注 +───────────────────────────────────────────────────────────────── +bot.world mcfold 的 World(已有 block_at) ★ 粗渲染的底座 +bot.entities 待加:EntityStore 从 chunk/实体数据来 +bot.on(...) 事件总线(待加) 粗/细分层同 mineflayer +bot.findBlocks({...}) rough.find_blocks(matching=…, …) 纯 Python,毫秒级 +bot.raycast rough.cast_ray(…) DDA +bot.move / bot.look 待定:见 1b.4(我们不做移动) +bot.chat 走 agentmail / 服务端聊天 已有通路 +bot.saveScreenshot WorldRenderer.screenshot() 细渲染(单次调用) +``` + +★ **最重要的一条**:`bot` 那些方法最终落到 `bot._client`(协议层), +而我们落到 **jar 端**(用户定调:「jar 端接受操作意图,输出包」)。 +所以**抽象形状照抄 mineflayer,落地换到 jar 端** —— 这正是用户那句话的含义。 + +### 1b.3 我们的三层(与 mineflayer 同形) + +``` +状态层 常驻、无 JVM、毫秒级 + World / EntityStore / 玩家与相机状态 / 时间天气 + ★ 这层就是「粗渲染」 + +操作层 意图式动词 + find_blocks / cast_ray / look / say / place / dig … + 多数落粗渲染(快);需精细视觉的转细渲染(慢,单次) + +事件层 粗粒度 + 细分层 + moved / block_changed / entity_appeared / message / chat: + ★ 分层是为省 token,不是为性能 +``` + +### 1b.4 一个必须先定的边界 + +⚠️ 我们**不做移动/战斗**(用户:范围是「渲染 + 与服务端 mod 物品交互」)。 +所以 mineflayer 的 `bot.move` / `dig` / `attackEntity` **不照抄** —— +它们要么不需要,要么该由「服务端交互」那条线以别的方式提供。 + +⇒ 待定:`bot.move` 这类「改变世界状态」的操作,在我们的定位里 + 究竟**不做**、还是**转交服务端**(agent 在真实世界里动,我们在旁边看/记)。 + 这决定 `操作层` 的边界。 --- @@ -224,9 +308,9 @@ protodef 描述**不随版本变** ⇒ `ProtocolVersion` 只影响少数结构 已有 `WorldRenderer` / `Instance` / `create_instance`,缺一个把 「渲染」+「服务端交互」串起来的门面。 -### ⬜ G4. ass 粗渲染(§1,新方向) +### ⬜ G4. 粗渲染(ass):agent 的常驻感知层(§1、§4) -见 §4 设计。 +见 §4 设计(**常驻**,与细渲染的**单次调用**互补)。 ### ⬜ G5. 聊天框 → agent → 拍照 的行为链(§1,新方向) @@ -234,57 +318,86 @@ protodef 描述**不随版本变** ⇒ `ProtocolVersion` 只影响少数结构 --- -## 4. ass 粗渲染设计(⬜ 未开始) +## 4. 粗渲染(ass)设计(⬜ 未开始) -### 4.1 定位 +### 4.1 定位:agent 的**常驻感知层** + +按 §1 的分工,粗渲染服务于**日常游玩**,所以它不是一个"拍照辅助", +而是一个**常驻的、无 JVM 的世界感知接口**。 ``` -真渲染: JVM + 完整 MC 渲染栈 + (可选)Iris 光影 ~20-40s ~1MB PNG -粗渲染: 纯 Python,从方块数据做视线近似 <100ms 文本网格 +细渲染: JVM + 完整 MC 栈 + Iris 单次调用(起→渲→销) ~20-40s +粗渲染: 纯 Python + 世界快照 常驻 <100ms ``` -**两者不是精度不同,是用途不同**: -- 粗渲染用来**搜索机位**(试几十个) -- 真渲染用来**出最终图**(只跑选中的那个) +### 4.2 它要回答的问题(agent 游玩时会问的) -### 4.2 核心原语(借鉴 mineflayer / prismarine 的做法) +| 问法 | 需要的能力 | +|---|---| +| 「我面前是什么?」 | 视线求交(DDA)| +| 「这堵墙后面有没有通路?」 | 射线 + 邻域查询 | +| 「我站的地方是空的吗?」 | `block_at`(已有)| +| 「那边那座山是什么?」 | 远距离扫描 / 俯瞰索引 | +| 「周围一圈都有什么?」 | 局部切面(ASCII 最有用的场景)| +| 「从这里能不能看到它?」 | 可见性测试(遮挡判定)| + +**其中「ASCII 网格」只是其中一种展示形态** —— 它的价值在于 +让 LLM 能直接读,而 PNG 需要多模态。 + +### 4.3 核心原语(借鉴 mineflayer / prismarine 的做法) mineflayer 的 `raycast` / `blockAt` 是纯数据的视线求交 —— 我们不连服务端, -但**同样的计算**可以跑在存档数据上: +但**同样的计算**可以跑在存档/注入的世界快照上。 ```python -# 输入:世界的方块数据(已能拿到,见 render.py 的 SaveChunk) -# 输出:ASCII 网格 + 结构化线索 +# 基础已有(mcfold/world.py): +# World.block_at(x,y,z) -> BlockHit(name, state_id, loaded, source) +# ★ 它已能区分 unloaded / unknown-state ⇒ mod 方块不会被当成空气 -for 每条视线(从相机出发,过每个网格像素): - 沿射线步进(DDA,与 MC 的方块网格对齐) - 记录第一个命中的非空气方块 - → 得到 depth / block / 明暗(按法线与光方向) +# 待加(mcfold/rough.py): +def cast_ray(world, origin, direction, max_dist) -> RayHit + """沿方向步进(DDA,与 MC 方块网格对齐),返回首个非空气方块""" + +def visibility(world, eye, target) -> Visibility + """两点之间是否通视(被几个方块挡住)""" + +def slice_view(world, eye, yaw, pitch, w, h) -> AsciiGrid + """把视野切成 w×h 的 ASCII 网格(agent 直接读)""" + +def scene_summary(world, center, radius) -> dict + """结构化线索:方块分布 / 开阔度 / 遮挡率(LLM 读数字比读 ASCII 准)""" ``` -### 4.3 输出形态(三件,服务于"让 agent 能推理") +### 4.4 「ass」的三种展示形态(服务于不同用途) ``` -① ASCII 网格 相机看到的大致形状("镜头埋地里"一眼可见) -② 结构化线索 视野内方块分布 / 主景物的屏幕占比 / 遮挡率 -③ 建议 "往左挪 3 格可让景物居中"(可选,规则驱动) +① ASCII 网格 视野切面 —— 人看 + LLM 直接读 +② 结构化线索 开阔度/遮挡率/最近景物/可通行方向 —— LLM 推理用(比①准) +③ 可选:俯视 ASCII 局部地图(w×h 格,每格一个地标字符)—— 定位用 ``` -**②比①更重要** —— LLM 读数字比读 ASCII 准; -ASCII 主要是给人看和做快速否决。 +### 4.5 验证口径(★ 改成"各自对各自用途",不是互相预测) -### 4.4 与真渲染的一致性(关键风险) +``` +粗渲染的判据(不需要细渲染参与): + · block_at 已区分 unloaded / unknown-state ⇒ 不能把未加载当空气 + · cast_ray 的结果与人工推算一致(可构造的简单场景:单方块、空走廊、斜坡) + · 局部切面的 ASCII 与直接读方块数据一致 -⚠️ 粗渲染的结论必须**能预测**真渲染。要测: -- 同一机位,粗渲染说"看得见主景物" ⇒ 真渲染确实看得见 -- 建立**一致性判据**(不是"看起来差不多",而是可判的: - 「主景物的屏幕包围盒重叠率 > 阈值」) +细渲染的判据(不需要粗渲染参与): + · 现有:注入地形 → distinctColors / chunksRendered 达阈 + · 现有:Sodium tracker 通知后 chunkReady > 0 -### 4.5 实现落点 +★ **不再要求**「粗渲染能预测细渲染」—— 两者服务不同问题 + (用户口径:粗=日常游玩,细=出图与精细判断) + 唯一的交集是「拍照选机位」这一个用例,那里是**用法**上的配合, + 不是**正确性**上的依赖。 +``` -`mcfold/rough.py`(新模块)+ `tests/test_rough.py` -- 输入:`WorldRenderer` 的方块数据(或直接读存档) -- 不依赖 JVM ⇒ 可在默认测试套件里跑(秒级) +### 4.6 实现落点 + +`mcfold/rough.py`(新模块,纯 Python、**不依赖 JVM**) +`tests/test_rough.py`(默认套件里跑,秒级) --- @@ -301,9 +414,9 @@ ASCII 主要是给人看和做快速否决。 ↓ ③ 需要什么 玩家位置/朝向 + 景物位置 + 可用机位 ↓ -④ 规划 粗渲染试 N 个机位 → 挑最好的 +④ 规划 粗渲染(常驻)试 N 个机位 → 挑最好的 ↓ -⑤ 出图 真渲染(Iris + 光影) +⑤ 出图 细渲染(单次调用:起 JVM → Iris+光影 → 销毁) ↓ ⑥ 送达 回聊天 / 发邮件(走现有 agentmail 通路) ``` @@ -332,7 +445,7 @@ shot = capture_photo( subject_scale=20, # 景物半径 with_player=True, # 是否要合照 player_pos=(x, y, z), # 合照时的"玩家"位置 - samples=20, # 粗渲染试几个机位 + samples=20, # 粗渲染试几个机位(常驻,毫秒级) ) shot.png → /path/out.png shot.rough → ASCII + 线索(给 agent 看它为什么选了这个角度) @@ -345,14 +458,20 @@ shot.rough → ASCII + 线索(给 agent 看它为什么选了这个角度) | # | 内容 | 依赖 | 状态 | |---|---|---|---| | M1 | 修 G1(build.sh 编译 CP 加 Fabric 库),1.21.1 构建跑通 | — | 🔄 | -| M2 | `rough.py` 粗渲染(纯 Python,DDA 视线) | — | ⬜ | -| M3 | 粗渲染 ↔ 真渲染一致性判据 | M2 | ⬜ | -| M4 | `capture_photo()` 机位搜索 + 统一接口门面 | M2, M3 | ⬜ | +| M2 | **抽象模型骨架**:状态/事件/操作三层(照 mineflayer 形状) | — | ⬜ | +| M2a | └ `rough.py` 常驻粗渲染(World 视图 + DDA 视线 + ASCII/线索) | M2 | ⬜ | +| M2b | └ 事件总线(粗粒度 + 细分层) | M2 | ⬜ | +| M3 | 粗渲染自身判据(构造场景;**不要求预测细渲染**) | M2a | ⬜ | +| M4 | `capture_photo()`(**作为第一个用例**:粗搜机位 → 细出图) | M2a, M2b, M3 | ⬜ | | M5 | 服务端交互(configuration / registry_data / 物品表) | M1 | ⬜ | -| M6 | 聊天框 → agent → 拍照 全链 | M1, M4, M5 | ⬜ | +| M6 | 聊天框 → agent → 拍照 全链(§5) | M1, M4, M5 | ⬜ | -**建议先做 M1 + M2**:M1 解阻断,M2 是用户新设想里最有独立价值的一块 -(纯 Python、不依赖 JVM、可秒级验证)。 +**建议先做 M1 + M2**:M1 解阻断;M2 是**主线**(抽象模型骨架), +拍照只是它的第一个用例。M2 做完后 agent 就有常驻感知能力了, +而这正是"日常游玩"的地基。 +★ M3 特意**不叫**"一致性判据":按用户口径粗/细服务于不同问题, + 不要求粗渲染预测细渲染的像素。M3 只验粗渲染**自己**对日常感知问题的正确性 + (构造简单场景,人工可推算)。 ---