[English](../../README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Русский](README_ru.md)
# dsh-unreal-agentlink
### 一个把 Unreal 编辑器交给 agent 的 DSH 插件。
**纯桥接。agent 负责规划,本插件只负责翻译。**
```
DSH agent ──工具调用──► dsh-unreal-agentlink ◄──WebSocket── Unreal 编辑器
(规划、决策) (本插件:服务端、翻译) 编辑器主动连出
```
[需要什么](#需要什么) · [快速开始](#快速开始) · [12 个工具](#12-个工具) · [配置](#配置) · [文档](#文档)
npm install && npm run check
---
你的 agent 已经会读文件、跑命令了。这个插件把**编辑器本身**也交给它:查询关卡、生成 actor、编辑蓝图与材质、渲染序列、截取它能真正「看见」的图,出错时撤销自己的工作。
它**不规划任务**。任务规划属于 DSH 的原生 agent。本插件是一层带安全措施的翻译器 —— 把 agent 的意图翻成 UnrealAgentLink 命令,把编辑器的回答翻回 agent 能据以行动的形状。
---
## 需要什么
| 项 | 要求 |
|---|---|
| Unreal Engine | 5.0 – 5.8 |
| UnrealAgentLink | C++ 插件本体,放进工程的 `Plugins/` |
| Node.js | `^22.19.0 \|\| >=24.0.0` |
| DSH 运行时 | 可用即可,本插件作为 bundle 装入 |
### 不需要安装 UEBox
UEBox 桌面版**不是依赖**。它只是同一厂商的另一个客户端,恰好也连 17860,所以同时跑会抢端口 —— 关系仅此而已。依据:
- `UnrealAgentLink.uplugin:25-34` 只声明两个依赖,都是引擎自带:`IKRig` 与 `PythonScriptPlugin`。没有任何 uebox 模块。
- `UnrealAgentLink.Build.cs` 的每一项依赖都是引擎模块(UnrealEd / AssetRegistry / MovieScene / Landscape …)。没有 uebox SDK。
- 整个源码里 `uebox` 出现 3 次,都是 `// Copyright uebox.ai` 版权头。
- 没有任何代码调用 uebox.ai。唯一的 `FHttpModule` 用法是读本机代理配置(`UAL_ProxyDiagnostics.cpp:167`)。
插件放进工程后即自主运行:`StartupModule` 无条件调用 `Init("ws://127.0.0.1:17860")`(`Core/UnrealAgentLink.cpp:86-97`),连上的瞬间就推 `project.info`,无握手、无 token、无鉴权。
> 想换端口:编辑器侧 `-UALServer=ws://127.0.0.1:17861`,本插件侧改 `port` 配置。
---
## 快速开始
```bash
npm install
npm run check # typecheck(src + tests)+ 154 个测试 + catalog 对账,一条命令
```
作为 DSH bundle 装入:`package.json` 声明 `dsh.bundle.patch`,`cordis.patch.yml` 以发布包名插入一行 —— `{id: unreal-agentlink, name: dsh-unreal-agentlink}`。
> **配置分层**:后一层会**整体替换**该行的 `config`,而不是深合并。要覆盖任何键,必须把想保留的键全部重写一遍。
**端到端路径见 [教程](../zh/01-tutorial.md)** —— UE 侧安装、第一次连通、常见工作流、出错怎么读。
---
## 文档
| 文档 | 内容 |
|---|---|
| [教程](../zh/01-tutorial.md) | 端到端上手:装插件、跑通、常用工作流、故障排查 |
| [协议参考](../zh/02-protocol.md) | 报文结构、事件、13 个状态码、序列化规则、超时与并发语义 |
| [架构](../zh/03-architecture.md) | 门链、会话隔离与撤销、错误翻译、验证策略、踩过的坑 |
| [命令参考](../zh/command-reference.md) | 170 条命令的参数/必填/结果键/状态码(**自动生成**) |
以上四份在每种语言下都有对应版本(`docs//`)。
---
## 四个必须先建立的事实
这四条不是设计取舍,是 UnrealAgentLink 的既有约束,它们决定了所有操作的写法。
1. **方向是反的。** 本插件不去连编辑器 —— 是**编辑器主动连出来**。启动顺序无所谓:编辑器先开就每 5 秒重试一轮,直到插件出现。
2. **没有鉴权。** 无 token、无 header、无子协议。**端口就是凭证**,所以默认只绑 loopback。不要改成 `0.0.0.0`。
3. **命令在游戏线程上串行执行。** 而且阻塞命令运行期间**编辑器什么都不回** —— 连 `sequence.render_status` 这种进度查询也不回。
4. **没有取消。** `query_task` 与 `cancel_task` 不存在。杀掉 `ual_task` 作业只停本插件的等待,引擎照跑,结果依然会回来。
---
## 12 个工具
170 条命令如果各做一个工具,170 份 schema 会吃掉每次请求的 prompt,而绝大多数请求用不到其中任何一个。所以发现能力是两步:
| 工具 | 用途 |
|---|---|
| `ual_status` | 编辑器连接 / 熔断 / 限流 / 会话 / 孤儿命令。**出问题时先读这个** |
| `ual_tools` | 列出全部 170 条命令(22 个族),可按族或关键词过滤 |
| `ual_schema` | 单条命令的**精确键名** —— 调用前必看,UE 对未知键是静默忽略的 |
| `ual_call` | 执行一条命令,原样返回编辑器结果 |
| `ual_batch` | 一个往返执行多条(上限 `maxBatchCalls`;有一项危险则整批走审批) |
| `ual_screenshot` | 截图并返回**真实图片**,agent 能直接看到 |
| `ual_logs` | 读本地日志环(UE 侧转发默认关闭,见教程) |
| `ual_undo` / `ual_undo_history` | 会话级撤销 / 查看撤销缓冲区 |
| `ual_locks` | 声明资产锁,编辑器会画「AI 正在编辑」角标 |
| `ual_editor_bind` | 多个编辑器连着时,把会话固定到某一个 |
| `ual_task` | 把长的阻塞命令转成真实后台作业,对话可继续 |
### 风险分级
read 65 / write 89 / **destructive 2 / dangerous 14**(后 16 类走审批)。
`tools/pre-execute` 对 destructive 与 dangerous 方法(含 `ual_batch` 中任一危险项)返回 `{kind:'ask'}`,把决定权留给会话策略。**未注册的方法直接 `deny`**,不会转发进编辑器。
注意:`never` 策略下 `ask` 会被自动拒绝 —— 这正是「无人值守不得动编辑器」的正确读法。
---
## 配置
默认值是「活着但惰性」。完整表与理由见 [教程第 7 节](../zh/01-tutorial.md#7-配置)。
| 键 | 默认 | 说明 |
|---|---|---|
| `host` / `port` | `127.0.0.1` / `17860` | 保持 loopback:没有鉴权,够到端口 = 能驱动编辑器 |
| `timeoutMs` | `180000` | 阻塞类命令各自抬高下限 |
| `cacheTtlMs` / `cacheMaxEntries` | `2000` / `128` | 会话读缓存 |
| `rateLimitRps` / `rateLimitQueue` | `5` / `32` | 每秒命令数 / 排队上限(0 关闭限流) |
| `breakerThreshold` / `breakerCooldownMs` | `3` / `30000` | 连续传输失败开闸 / 冷却 |
| `logCapacity` / `logMinLevel` | `512` / `Display` | 日志环 |
| `logForwardingEnabled` | `false` | 只在编辑器内开过 `ual.ForwardLogs` 后置 `true` |
| `rollbackOnDispose` | `false` | 只有 LIFO 后缀才精确,见架构文档 |
| `notifyOnEditorChange` | `true` | 编辑器上下线时向 agent 注入一行说明 |
| `screenshotAttachments` / `screenshotMaxBytes` | `true` / 12MB | 截图转图片块 / 体积上限 |
| `maxBatchCalls` | `12` | 单次 `ual_batch` 上限 |
---
## 开发
```bash
npm run build # tsc → lib/
npm run dev # tsc --watch
npm run check # typecheck + 测试 + catalog 对账(提交前跑这个)
npm test # 仅测试
npm run typecheck # src 与 tests 分别类型检查
```
### 源码结构
```
src/
index.ts Cordis 生命周期、审批门、销毁时回滚
config.ts Schemastery schema(默认值的唯一来源)
service.ts call() 门链:catalog → requires → 读缓存 → 熔断 → 限流 → 发送 → ledger → 翻译
protocol/{bridge,serialization,types}.ts WS 服务端 / 出站校验 / wire 类型
catalog/{method-data,method-overrides,methods}.ts 170 命令目录(数据从 C++ 提取)
errors/translate.ts 错误分类(按类名/状态码,不读文案)
resilience/breaker.ts 限流 + 熔断
sessions/state.ts 会话 ledger / LIFO 判定 / 锁合并 / 缓存 / 日志过滤
tools/ 12 个工具
scripts/ catalog 提取与对账、文档生成
docs/ 教程 / 协议 / 架构 / 命令参考
tests/ 154 个测试
```
### 生成物:不要手改
`src/catalog/method-data.ts` 与 `docs//command-reference.md` 都是**生成物**:
```bash
node scripts/extract-methods.mjs .ua-ref/plugin/UnrealAgentLink/Source # 从 C++ 重建目录
npm run build && node scripts/gen-docs.mjs # 重建命令参考(全部 9 种语言)
```
手维护的表建好了却没人 import,是本项目反复出现的缺陷类型(`REQUIRED_KEYS`、`EXTRA_PARAM_KEYS`、`UAL_CODES` 都中过)。对策是让检查**去问源头**,而不是问表自己 —— 详见[架构第 9 节](../zh/03-architecture.md#9-生成物与没人-import-的表)。
### 测试与校验
```bash
npm run check # typecheck(src + tests)+ 154 passed + catalog 对账
npm test # 154 passed
node scripts/check-catalog.mjs # 170/170 零漂移;每个 requires 键都在 C++ 里出现;码集与 C++ 一致
node scripts/gen-docs.mjs --check # 9 种语言的命令参考是否为最新
```
7 个测试文件按**它们能发现的缺陷类型**划分,各有理由 —— 见[架构第 11 节](../zh/03-architecture.md#11-验证策略)。
`requires` 不是手写的:从 handler 自身的 `SendError(..., 400, "Missing field: X")` 消息推导(28 处),因为写错一个键会让桥在引擎看到之前就拒掉一个合法调用。推导结果落在 `method-data.ts` 的 `REQUIRED_KEYS`,由 `methods.ts` 的 `requiresOf()` 合并进运行时(`service.call()` 的前置拒绝、`ual_schema` 的 `REQUIRED:` 行、错误提示都走这一个函数)。
刻意**只收窄不放大**:消息里带 `or`(`"Missing field: name or path"`)整条跳过;从同文件被调函数里挖出来的键按分支局部处理丢弃 —— 见 `scripts/extract-methods.mjs` 的 `REQUIRED_DROPPED`,每个条目都写了 C++ 行号。所以「一个键都没收」是安全的默认,不是漏检。
---
## 协议要点
```
UE → 插件: { ver:"1.0", type:"req", method, id, params } // id 由服务端生成
插件 → UE: { ver:"1.0", type:"res", id, code, result } // result 为空时整键省略
UE → 插件: { ver, type:"evt", method, payload }
```
- 失败**只由 `code` 表达**,报文里没有 envelope 级 `error` 字段。
- 错误文本在 `result.message`(按编辑器文化分中英文,**不要解析**),机器可读信息在 `result.details`。
- **许多 handler 返回 `200` 但 `result.ok === false`**(`editor.undo`、`blueprint.compile`、`pie.observe`),所以插件用 `judge()` 判成败,不只看 code。C++ 里有 55 处 `SetStringField(TEXT("error"), ...)`,分布 14 个文件 —— 这是常态而非异常。
- `content.import_folder` / `content.import_assets` 是**即发即弃**的外发推送:不带 `id`、不等待回复。插件把事件转成日志与 agent 通知,**不回一个假 `res`** —— 回 `ok:false` 只会让用户看到虚假的「导入失败」对话框,比沉默更糟。
完整细节(13 个状态码、序列化规则、超时语义)见[协议参考](../zh/02-protocol.md)。
---
## License
MIT