ue-prism 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ue_prism-1.0.0/LICENSE +21 -0
- ue_prism-1.0.0/PKG-INFO +225 -0
- ue_prism-1.0.0/README.md +198 -0
- ue_prism-1.0.0/prism/__init__.py +3 -0
- ue_prism-1.0.0/prism/__main__.py +7 -0
- ue_prism-1.0.0/prism/attributelog.py +191 -0
- ue_prism-1.0.0/prism/autostart.py +48 -0
- ue_prism-1.0.0/prism/bridge.py +119 -0
- ue_prism-1.0.0/prism/bus.py +162 -0
- ue_prism-1.0.0/prism/cli.py +287 -0
- ue_prism-1.0.0/prism/domain/__init__.py +22 -0
- ue_prism-1.0.0/prism/domain/actors.py +98 -0
- ue_prism-1.0.0/prism/domain/assets.py +467 -0
- ue_prism-1.0.0/prism/domain/metrics.py +259 -0
- ue_prism-1.0.0/prism/domain/migrate.py +303 -0
- ue_prism-1.0.0/prism/domain/ping.py +71 -0
- ue_prism-1.0.0/prism/envelope.py +28 -0
- ue_prism-1.0.0/prism/folderscan.py +85 -0
- ue_prism-1.0.0/prism/logscan.py +96 -0
- ue_prism-1.0.0/prism/registry.py +146 -0
- ue_prism-1.0.0/prism/rules.py +345 -0
- ue_prism-1.0.0/prism/server.py +534 -0
- ue_prism-1.0.0/prism/tasks.py +271 -0
- ue_prism-1.0.0/prism/uat.py +240 -0
- ue_prism-1.0.0/pyproject.toml +42 -0
- ue_prism-1.0.0/setup.cfg +4 -0
- ue_prism-1.0.0/tests/test_assets.py +40 -0
- ue_prism-1.0.0/tests/test_attribute.py +137 -0
- ue_prism-1.0.0/tests/test_bus.py +49 -0
- ue_prism-1.0.0/tests/test_buscall.py +30 -0
- ue_prism-1.0.0/tests/test_cli.py +142 -0
- ue_prism-1.0.0/tests/test_cook.py +160 -0
- ue_prism-1.0.0/tests/test_folderscan.py +42 -0
- ue_prism-1.0.0/tests/test_logscan.py +49 -0
- ue_prism-1.0.0/tests/test_metrics_degrade.py +27 -0
- ue_prism-1.0.0/tests/test_migrate.py +227 -0
- ue_prism-1.0.0/tests/test_migration_preview.py +138 -0
- ue_prism-1.0.0/tests/test_pr05_contract.py +96 -0
- ue_prism-1.0.0/tests/test_registry.py +71 -0
- ue_prism-1.0.0/tests/test_resolve.py +99 -0
- ue_prism-1.0.0/tests/test_rules.py +245 -0
- ue_prism-1.0.0/tests/test_tasks.py +144 -0
- ue_prism-1.0.0/ue_prism.egg-info/PKG-INFO +225 -0
- ue_prism-1.0.0/ue_prism.egg-info/SOURCES.txt +46 -0
- ue_prism-1.0.0/ue_prism.egg-info/dependency_links.txt +1 -0
- ue_prism-1.0.0/ue_prism.egg-info/entry_points.txt +3 -0
- ue_prism-1.0.0/ue_prism.egg-info/requires.txt +4 -0
- ue_prism-1.0.0/ue_prism.egg-info/top_level.txt +1 -0
ue_prism-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Za0Shu1
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ue_prism-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ue-prism
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 面向 Unreal Engine 项目的 MCP 诊断服务器(日志 / 性能 / 引用,结构化只读)
|
|
5
|
+
Author: Za0Shu1
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Za0Shu1/ue-prism
|
|
8
|
+
Project-URL: Repository, https://github.com/Za0Shu1/ue-prism
|
|
9
|
+
Project-URL: Issues, https://github.com/Za0Shu1/ue-prism/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/Za0Shu1/ue-prism/blob/main/docs/SETUP.md
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp<3,>=2
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# ue-prism
|
|
29
|
+
|
|
30
|
+
> 把 Unreal 项目像棱镜一样分光解析——日志、性能开销、资产引用,交给 AI agent 逐条检验。
|
|
31
|
+
|
|
32
|
+
**ue-prism** 是一个面向 Unreal Engine 项目的 MCP(Model Context Protocol)诊断服务器:让 AI agent 读取并分析**编辑器/cook/package 日志**(报错归因)、**场景性能与资产开销**(优化建议)、**资产引用链**(迁移决策),所有结果结构化返回、机器可读。
|
|
33
|
+
|
|
34
|
+
[](LICENSE)
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
> 当前进度:**v1.0 已交付**。v0.1 连通链路 → v0.2 cook/package → v0.3 性能静态规则引擎(`get_perf_report` 7 规则、`get_asset_metrics` 度量,5.4/5.8 校准一致)→ **v1.0**:连接发现(零配置 · bridge 自注册 · 全工具 `project=` 选择器 · `list_projects`)、引用链分析(`get_asset_chain` 递归 `uses`/`used_by` 闭包,按类聚合数量/体量)、迁移套件(写操作 `migrate_asset_rename`/`_move`/`_asset`〔A·同工程复制依赖闭包〕,双钥 dry_run + 成功后自动清理 redirector 桩)、发布(GitHub Releases 已就绪 · `release.yml`;PyPI 走 Trusted Publishing 待注册免费账号 · `publish.yml` 由 `PYPI_PUBLISH` 变量开关))、一条命令安装器(`prism setup --client`,幂等/可卸载/`--dry-run`)+ `server.json` 注册清单。UE 5.4.4 真机实证完毕。共 **18 工具、123 测试**。详细需求与验收见 [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md)。
|
|
39
|
+
|
|
40
|
+
## 为什么要做这个
|
|
41
|
+
|
|
42
|
+
UE 5.8 随引擎发布了实验性官方 MCP 插件(ModelContextProtocol + AllToolsets),但实测与社区反馈暴露了系统性缺陷。ue-prism 的每一条设计都是它的反面:
|
|
43
|
+
|
|
44
|
+
| 官方插件的问题 | ue-prism 的对策 |
|
|
45
|
+
|---|---|
|
|
46
|
+
| 失败以 `Ok` 文本返回,agent 无法判别成败 | 统一结构化错误信封,错误绝不伪装成成功 |
|
|
47
|
+
| 无就绪握手,早期拿到残缺工具表 | `ping` 握手,先确认编辑器在线 |
|
|
48
|
+
| 52+ toolset 数百函数,lazy 发现、schema 巨大 | ≤20 个扁平工具,签名全在文档里 |
|
|
49
|
+
| 静默截断(列表限 20 条不提示) | 所有列表携带 `total / truncated / cap` |
|
|
50
|
+
| 绑定 5.8 Experimental API,随版本漂移 | 只依赖 `unreal` Python API + 纯标准库传输,5.0–5.8+ 通吃 |
|
|
51
|
+
|
|
52
|
+
**默认只读。** 写操作(cook、迁移改名/移动)一律 dry-run 预览 + 双钥确认(`dry_run=False` 且 `confirm=True`)才落盘。
|
|
53
|
+
|
|
54
|
+
## 架构
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Claude Code / Cursor / opencode
|
|
58
|
+
│ stdio (MCP)
|
|
59
|
+
▼
|
|
60
|
+
prism server (Python 3.10+,不碰任何引擎 API)
|
|
61
|
+
│ 文件总线:cmd_*.json ⇄ res_*.json
|
|
62
|
+
▼
|
|
63
|
+
bridge (UE 编辑器内 Python,主线程 tick 轮询)
|
|
64
|
+
│ 白名单函数分发
|
|
65
|
+
▼
|
|
66
|
+
domain/*.py ── 纯 `unreal` API + 标准库
|
|
67
|
+
(日志分析 / 资产扫描 / 场景清单 / 引用查询)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
文件总线刻意做得"无聊":原子 rename、无 socket、无线程,编辑器崩溃不拖累外部进程,任何带 Python 插件的引擎版本都能用。轮询放在**主线程 tick**(UE 5.6+ 禁止在非 game thread 调用 `unreal`)。bridge 顺带限频写一个仅含时间戳的心跳标记,客户端据此对"编辑器已关/主线程停摆"立即快速失败,不再空等 30s 超时;超时崩溃残留的孤儿命令/响应文件会被自动清扫。将来可无痛替换为 socket/HTTP。
|
|
71
|
+
|
|
72
|
+
## 仓库结构
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
prism/
|
|
76
|
+
├── server.py # 协议层:MCP 工具(18 个),绝不 import unreal
|
|
77
|
+
├── bridge.py # 传输层:编辑器内白名单分发 + 信封透传 + v1.0 自注册指针
|
|
78
|
+
├── registry.py # v1.0 连接发现:固定目录指针写/读 + 现场心跳新鲜度
|
|
79
|
+
├── autostart.py # v1.0 pip 安装后 UE 侧一行自启(随包发布,无需 sys.path)
|
|
80
|
+
├── bus.py # 文件总线:原子写 / 心跳标记 / 孤儿清扫
|
|
81
|
+
├── envelope.py # 统一信封与错误码(三层通用,纯标准库)
|
|
82
|
+
├── tasks.py # v0.2 任务登记表:惰性状态推导 / BUSY 互斥
|
|
83
|
+
├── uat.py # v0.2 UAT 定位与提交:引擎路由 / 命令模板 / 跨重启合成
|
|
84
|
+
├── attributelog.py # v0.2 日志→资产归因:指纹分组 / 磁盘解析 / 批量富化
|
|
85
|
+
├── rules.py # v0.3 规则引擎:profile/TOML 阈值 + 7 规则 + 报告聚合
|
|
86
|
+
├── logscan.py # read_editor_log 离线聚合
|
|
87
|
+
├── folderscan.py # scan_folder_assets 离线审计
|
|
88
|
+
└── domain/ # 领域层:ping / actors / assets / metrics(含 describe_many、
|
|
89
|
+
# referencers_many 批量函数),纯 unreal + 标准库,守 3.7 子集
|
|
90
|
+
scripts/ # autostart_bridge.py + verify_connection.py / verify_migration.py + CLI 手测
|
|
91
|
+
tests/ # 123 项无引擎测试(总线/任务/cook/归因/规则引擎/扫描 + 连接发现 / 引用链 / 迁移)
|
|
92
|
+
docs/ # SETUP.md(操作)· REQUIREMENTS.md(契约单一事实源)· DESIGN_v0.2.md
|
|
93
|
+
.github/workflows/ # CI:push/PR 跑 pytest(3.10 / 3.12 矩阵)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 工具(18 个:只读诊断/引用链 + 双钥确认的 cook / 迁移 + 规则报告 + 连接发现)
|
|
97
|
+
|
|
98
|
+
经 bridge(需编辑器在线):
|
|
99
|
+
|
|
100
|
+
| 工具 | 说明 |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `ping` | bridge 心跳、UE 版本、工程路径、已加载地图 |
|
|
103
|
+
| `list_level_actors` | 场景清单:类名/标签过滤、transform、组件数 |
|
|
104
|
+
| `describe_asset` | 单资产元数据:类、包路径、磁盘大小、脏标记 |
|
|
105
|
+
| `get_asset_references` | 双向引用:`uses`(依赖) / `used_by`(被引用),可递归、限量 |
|
|
106
|
+
|
|
107
|
+
离线(不依赖编辑器,读磁盘):
|
|
108
|
+
|
|
109
|
+
| 工具 | 说明 |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `read_editor_log` | 日志聚合:Error/Warning 分组、计数、首现时间、样本 |
|
|
112
|
+
| `scan_folder_assets` | 目录资产审计:按大小排序、类型汇总、Top 开销元凶 |
|
|
113
|
+
|
|
114
|
+
外部进程(server 侧调 UAT,编辑器须关闭;首个非只读工具族):
|
|
115
|
+
|
|
116
|
+
| 工具 | 说明 |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `cook_package` | 触发 cook/package;**默认 dry-run,`dry_run=False`+`confirm=True` 双钥才执行**;同工程 BUSY 互斥 |
|
|
119
|
+
| `get_cook_status` | 任务状态**读时惰性推导**(done 标记/硬时限/pid/日志收束行),无守护进程 |
|
|
120
|
+
| `attribute_cook_errors` | 日志→资产归因:指纹分组 + 磁盘解析;桥在线时 `describe_many`/`referencers_many` 批量富化 |
|
|
121
|
+
|
|
122
|
+
性能规则(v0.3 · 混合通道:桥在线出全量、离线自动半份并逐条注明 skip):
|
|
123
|
+
|
|
124
|
+
| 工具 | 说明 |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `get_perf_report` | 静态性能健康报告:7 规则(资产大小/纹理尺寸/网格三角+单LOD/光源重复/WP 外部 actor 数/cook 静默丢弃/cook 空煮),top-80 大资产抽样度量 |
|
|
127
|
+
| `list_perf_rules` | 当前 profile 生效规则与阈值(可解释性入口;pc/console/mobile 三档 + 工程 prism.toml 覆盖) |
|
|
128
|
+
| `get_asset_metrics` | 批量原语度量:纹理 px / 网格 LOD 三角,带 `tried` API 证据链(5.4/5.8 双版本校准) |
|
|
129
|
+
|
|
130
|
+
连接发现 · 引用链 · 迁移(v1.0 · 去硬编码路径 + 依赖闭包分析 + 写操作):
|
|
131
|
+
|
|
132
|
+
| 工具 | 说明 |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `list_projects` | 零配置枚举已注册工程 + 活跃态(`live`/`heartbeat_age`);供 agent 先探后用 |
|
|
135
|
+
| `get_asset_chain` | 递归依赖闭包(迁移分析):`uses`=要带走的全部 `/Game` 依赖(带类别/磁盘大小,`by_class` 聚合体量);`used_by`=谁依赖它;`scope=game` 默认滤引擎噪声 |
|
|
136
|
+
| `preview_asset_migration` | 只读迁移预览:源/目标冲突检测 + `used_by` 影响面 → dry-run 计划(目标仅剩改名遗留的 `ObjectRedirector` 桩时不算冲突) |
|
|
137
|
+
| `migrate_asset_rename` | 真改名(写 · 双钥):走 `rename_asset(pkg,pkg)` 自动修引用,成功后默认清一次重定向桩 |
|
|
138
|
+
| `migrate_asset_move` | 真移动(写 · 双钥):移动到目录,可带 new_name 同时改名,默认清桩 |
|
|
139
|
+
| `migrate_asset` | 迁移 A·同工程复制(写 · 双钥):把资产 + 其 `/Game` `uses` 闭包镜像复制到目标目录;默认 dry-run 出复制计划(按类数量/体量/冲突);原件不动 |
|
|
140
|
+
|
|
141
|
+
所有桥 / 离线 / cook / 迁移工具接受可选 `project=` 选择器;未配置工程路径时按判定阶梯自动定位(详见 REQUIREMENTS §3.5)。
|
|
142
|
+
|
|
143
|
+
## 快速开始
|
|
144
|
+
|
|
145
|
+
> v0.1/v0.2 均已跑通。完整操作(`pip` 安装 / UE 开机自启 / Codex·opencode 配置 / cook 双钥)见 [docs/SETUP.md](docs/SETUP.md);设计契约见 [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md)。下面是最小三步速览(`<REPO>`=仓库绝对路径、`<PROJ>`=UE 工程根,均用正斜杠)。
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# 1. 安装 server
|
|
149
|
+
pip install git+https://github.com/Za0Shu1/ue-prism.git # 现在即可用(GitHub Releases 也已就绪)
|
|
150
|
+
# pip install ue-prism # PyPI:注册免费账号 + 配 Trusted Publishing 后启用(见 docs/SETUP.md §7)
|
|
151
|
+
|
|
152
|
+
# 2. 编辑器自启(推荐):在工程 Config/DefaultEngine.ini 注册一次后,重开编辑器即自动起 bridge:
|
|
153
|
+
# [/Script/PythonScriptPlugin.PythonScriptPluginSettings]
|
|
154
|
+
# +StartupScripts="<REPO>/scripts/autostart_bridge.py"
|
|
155
|
+
# 手动兜底(Output Log 的 [PY] 控制台):
|
|
156
|
+
import sys; sys.path.insert(0, r"<REPO>")
|
|
157
|
+
import prism.bridge; prism.bridge.start(r"<PROJ>/Saved/Prism")
|
|
158
|
+
|
|
159
|
+
# 3. 在 MCP 客户端注册(一条命令幂等写入、零工程路径;v1.0 bridge 自注册后 server 自动发现):
|
|
160
|
+
prism setup --client codex # 或 opencode / cursor / all;--dry-run 预览、--uninstall 移除
|
|
161
|
+
# 等价手写:command: python -m prism.server (或装包后 ue-prism)
|
|
162
|
+
# 多工程时 agent 先 list_projects 探测、或调用时传 project=<工程名>
|
|
163
|
+
# (仍可钉死单工程:加 env PRISM_BUS_DIR=<PROJ>/Saved/Prism PRISM_PROJECT_DIR=<PROJ>)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
先问 agent 一句验证连通:*"你现在连上的是哪个 UE 项目?里面加载了哪些地图?"*
|
|
167
|
+
接入日志工具后,可试:*"把最近编辑器日志里的 Error/Warning 归类,指出可疑原因。"*
|
|
168
|
+
cook 回路(关编辑器、双钥)可试:*"先 dry-run 看看 cook 会跑什么命令"* → *"确认执行"* → *"把这次 cook 的报错归因到资产"*
|
|
169
|
+
|
|
170
|
+
## 路线图
|
|
171
|
+
|
|
172
|
+
- **v0.1**(已发布 · v0.1.0):三层骨架 + 文件总线 + `ping` 端到端连通 + 结构化错误契约 + UE 5.4 实测 + 6 只读工具 + bridge 编辑器开机自启
|
|
173
|
+
- **v0.2**(已发布 · v0.2.0,v0.2.1 已清尾):cook/package 全回路真机实证完毕 —— cook 容错三坑、package 25s/786MB 一次通过、5.8 版式(`PRISM_ENGINE_ROOT`)同工程通过、失败注入还原闭环
|
|
174
|
+
- **v0.3**(已发布 · v0.3.0):性能静态规则引擎(7 规则全来自真机案例;纹理/网格度量 5.4 与 5.8 校准一致;桥离线自动半份报告)
|
|
175
|
+
- **v1.0**(已交付 · 版本 1.0.0,待打 tag 发 PyPI):连接发现(零配置/自注册/`project=` 选择器)+ 引用链分析(`get_asset_chain` 传递闭包)+ 迁移套件(rename/move/copy 写操作,双钥 + redirector 自动清理);UE 5.4.4 真机实证完毕
|
|
176
|
+
- 之后:bridge 打包成正式 UE 插件、socket 传输、Epic 官方 5.8 toolset 可选后端、截图验证工具
|
|
177
|
+
|
|
178
|
+
## 运行要求
|
|
179
|
+
|
|
180
|
+
- Unreal Engine 5.0+,启用 Python Editor Scripting(launcher 安装自带)
|
|
181
|
+
- 编辑器侧:引擎内嵌 Python 随 UE 版本 3.7→3.11(domain 层守 3.7 语法子集)
|
|
182
|
+
- server 侧:Python ≥3.10 + MCP Python SDK(锁 2.x),无引擎依赖
|
|
183
|
+
|
|
184
|
+
## 本机真机测试(UE 5.4)
|
|
185
|
+
|
|
186
|
+
- 工程目录:`F:/UE_Projects/TestProject/UE_PRISM_Project`(已在 DefaultEngine.ini 注册 bridge 自启;5.4.4 与 5.8.2 双版本真机验证过)
|
|
187
|
+
- 文件总线目录:`<工程>/Saved/Prism`(含 `heartbeat.json` 活性标记与 `tasks/` 任务档案)
|
|
188
|
+
- ue-prism 仓库:`F:/VibeCoding/ue-prism`;server 侧解释器:`F:/venv/Scripts/python.exe`
|
|
189
|
+
|
|
190
|
+
**1) 双击打开工程(EngineAssociation=5.4)**——bridge 已由 StartupScripts 自启,Output Log 出现 `[prism] bridge started` 即在线;`<工程>/Saved/Prism/heartbeat.json` 应每秒级更新。
|
|
191
|
+
|
|
192
|
+
**2) 另开系统终端,从总线外部验证连通与快速失败:**
|
|
193
|
+
```powershell
|
|
194
|
+
F:\venv\Scripts\python.exe F:\VibeCoding\ue-prism\scripts\ping_cli.py "F:/UE_Projects/TestProject/UE_PRISM_Project/Saved/Prism"
|
|
195
|
+
```
|
|
196
|
+
期望:`ok:true`;关编辑器再戳 → 约 1s 内 `BRIDGE_UNREACHABLE`(心跳陈旧),不再空等 30s。
|
|
197
|
+
|
|
198
|
+
**3) cook 回路(关闭编辑器后;细则见 docs/SETUP.md §4):**
|
|
199
|
+
```powershell
|
|
200
|
+
F:\venv\Scripts\python.exe -m prism.server --project-dir F:\UE_Projects\TestProject\UE_PRISM_Project
|
|
201
|
+
# 或在 agent 里:cook_package() 干跑 -> cook_package(dry_run=False, confirm=True) -> get_cook_status(task_id)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**4) 可选——无头探针(unreal API 名跨版本校核):**
|
|
205
|
+
```powershell
|
|
206
|
+
& "E:\UnrealEngine\UE_5.4\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
|
|
207
|
+
"F:\UE_Projects\TestProject\UE_PRISM_Project\UE_PRISM_Project.uproject" `
|
|
208
|
+
-run=pythonscript -script="F:\VibeCoding\ue-prism\scripts\probe_unreal.py"
|
|
209
|
+
```
|
|
210
|
+
## 相关项目
|
|
211
|
+
|
|
212
|
+
- [chongdashu/unreal-mcp](https://github.com/chongdashu/unreal-mcp)、[IvanMurzak/Unreal-MCP](https://github.com/IvanMurzak/Unreal-MCP) —— 侧重编辑器**操控**;ue-prism 侧重**诊断与分析**
|
|
213
|
+
- Epic 官方 `ModelContextProtocol` 插件(UE 5.8)—— 原生但实验性;ue-prism 覆盖 5.0–5.8 行为一致
|
|
214
|
+
|
|
215
|
+
## 设计文档
|
|
216
|
+
|
|
217
|
+
需求、契约与决策记录见 [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md);设计草案:[v0.2 cook 全回路](docs/DESIGN_v0.2.md)(已实现并真机实证)· [v0.3 性能静态规则引擎](docs/DESIGN_v0.3.md)(评审中)。
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT © [Za0Shu1](https://github.com/Za0Shu1)
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
Unreal® 与 Unreal Engine® 为 Epic Games, Inc. 注册商标。本项目与 Epic Games 无关联,亦未获其背书。
|
ue_prism-1.0.0/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# ue-prism
|
|
2
|
+
|
|
3
|
+
> 把 Unreal 项目像棱镜一样分光解析——日志、性能开销、资产引用,交给 AI agent 逐条检验。
|
|
4
|
+
|
|
5
|
+
**ue-prism** 是一个面向 Unreal Engine 项目的 MCP(Model Context Protocol)诊断服务器:让 AI agent 读取并分析**编辑器/cook/package 日志**(报错归因)、**场景性能与资产开销**(优化建议)、**资产引用链**(迁移决策),所有结果结构化返回、机器可读。
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
> 当前进度:**v1.0 已交付**。v0.1 连通链路 → v0.2 cook/package → v0.3 性能静态规则引擎(`get_perf_report` 7 规则、`get_asset_metrics` 度量,5.4/5.8 校准一致)→ **v1.0**:连接发现(零配置 · bridge 自注册 · 全工具 `project=` 选择器 · `list_projects`)、引用链分析(`get_asset_chain` 递归 `uses`/`used_by` 闭包,按类聚合数量/体量)、迁移套件(写操作 `migrate_asset_rename`/`_move`/`_asset`〔A·同工程复制依赖闭包〕,双钥 dry_run + 成功后自动清理 redirector 桩)、发布(GitHub Releases 已就绪 · `release.yml`;PyPI 走 Trusted Publishing 待注册免费账号 · `publish.yml` 由 `PYPI_PUBLISH` 变量开关))、一条命令安装器(`prism setup --client`,幂等/可卸载/`--dry-run`)+ `server.json` 注册清单。UE 5.4.4 真机实证完毕。共 **18 工具、123 测试**。详细需求与验收见 [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md)。
|
|
12
|
+
|
|
13
|
+
## 为什么要做这个
|
|
14
|
+
|
|
15
|
+
UE 5.8 随引擎发布了实验性官方 MCP 插件(ModelContextProtocol + AllToolsets),但实测与社区反馈暴露了系统性缺陷。ue-prism 的每一条设计都是它的反面:
|
|
16
|
+
|
|
17
|
+
| 官方插件的问题 | ue-prism 的对策 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| 失败以 `Ok` 文本返回,agent 无法判别成败 | 统一结构化错误信封,错误绝不伪装成成功 |
|
|
20
|
+
| 无就绪握手,早期拿到残缺工具表 | `ping` 握手,先确认编辑器在线 |
|
|
21
|
+
| 52+ toolset 数百函数,lazy 发现、schema 巨大 | ≤20 个扁平工具,签名全在文档里 |
|
|
22
|
+
| 静默截断(列表限 20 条不提示) | 所有列表携带 `total / truncated / cap` |
|
|
23
|
+
| 绑定 5.8 Experimental API,随版本漂移 | 只依赖 `unreal` Python API + 纯标准库传输,5.0–5.8+ 通吃 |
|
|
24
|
+
|
|
25
|
+
**默认只读。** 写操作(cook、迁移改名/移动)一律 dry-run 预览 + 双钥确认(`dry_run=False` 且 `confirm=True`)才落盘。
|
|
26
|
+
|
|
27
|
+
## 架构
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Claude Code / Cursor / opencode
|
|
31
|
+
│ stdio (MCP)
|
|
32
|
+
▼
|
|
33
|
+
prism server (Python 3.10+,不碰任何引擎 API)
|
|
34
|
+
│ 文件总线:cmd_*.json ⇄ res_*.json
|
|
35
|
+
▼
|
|
36
|
+
bridge (UE 编辑器内 Python,主线程 tick 轮询)
|
|
37
|
+
│ 白名单函数分发
|
|
38
|
+
▼
|
|
39
|
+
domain/*.py ── 纯 `unreal` API + 标准库
|
|
40
|
+
(日志分析 / 资产扫描 / 场景清单 / 引用查询)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
文件总线刻意做得"无聊":原子 rename、无 socket、无线程,编辑器崩溃不拖累外部进程,任何带 Python 插件的引擎版本都能用。轮询放在**主线程 tick**(UE 5.6+ 禁止在非 game thread 调用 `unreal`)。bridge 顺带限频写一个仅含时间戳的心跳标记,客户端据此对"编辑器已关/主线程停摆"立即快速失败,不再空等 30s 超时;超时崩溃残留的孤儿命令/响应文件会被自动清扫。将来可无痛替换为 socket/HTTP。
|
|
44
|
+
|
|
45
|
+
## 仓库结构
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
prism/
|
|
49
|
+
├── server.py # 协议层:MCP 工具(18 个),绝不 import unreal
|
|
50
|
+
├── bridge.py # 传输层:编辑器内白名单分发 + 信封透传 + v1.0 自注册指针
|
|
51
|
+
├── registry.py # v1.0 连接发现:固定目录指针写/读 + 现场心跳新鲜度
|
|
52
|
+
├── autostart.py # v1.0 pip 安装后 UE 侧一行自启(随包发布,无需 sys.path)
|
|
53
|
+
├── bus.py # 文件总线:原子写 / 心跳标记 / 孤儿清扫
|
|
54
|
+
├── envelope.py # 统一信封与错误码(三层通用,纯标准库)
|
|
55
|
+
├── tasks.py # v0.2 任务登记表:惰性状态推导 / BUSY 互斥
|
|
56
|
+
├── uat.py # v0.2 UAT 定位与提交:引擎路由 / 命令模板 / 跨重启合成
|
|
57
|
+
├── attributelog.py # v0.2 日志→资产归因:指纹分组 / 磁盘解析 / 批量富化
|
|
58
|
+
├── rules.py # v0.3 规则引擎:profile/TOML 阈值 + 7 规则 + 报告聚合
|
|
59
|
+
├── logscan.py # read_editor_log 离线聚合
|
|
60
|
+
├── folderscan.py # scan_folder_assets 离线审计
|
|
61
|
+
└── domain/ # 领域层:ping / actors / assets / metrics(含 describe_many、
|
|
62
|
+
# referencers_many 批量函数),纯 unreal + 标准库,守 3.7 子集
|
|
63
|
+
scripts/ # autostart_bridge.py + verify_connection.py / verify_migration.py + CLI 手测
|
|
64
|
+
tests/ # 123 项无引擎测试(总线/任务/cook/归因/规则引擎/扫描 + 连接发现 / 引用链 / 迁移)
|
|
65
|
+
docs/ # SETUP.md(操作)· REQUIREMENTS.md(契约单一事实源)· DESIGN_v0.2.md
|
|
66
|
+
.github/workflows/ # CI:push/PR 跑 pytest(3.10 / 3.12 矩阵)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 工具(18 个:只读诊断/引用链 + 双钥确认的 cook / 迁移 + 规则报告 + 连接发现)
|
|
70
|
+
|
|
71
|
+
经 bridge(需编辑器在线):
|
|
72
|
+
|
|
73
|
+
| 工具 | 说明 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `ping` | bridge 心跳、UE 版本、工程路径、已加载地图 |
|
|
76
|
+
| `list_level_actors` | 场景清单:类名/标签过滤、transform、组件数 |
|
|
77
|
+
| `describe_asset` | 单资产元数据:类、包路径、磁盘大小、脏标记 |
|
|
78
|
+
| `get_asset_references` | 双向引用:`uses`(依赖) / `used_by`(被引用),可递归、限量 |
|
|
79
|
+
|
|
80
|
+
离线(不依赖编辑器,读磁盘):
|
|
81
|
+
|
|
82
|
+
| 工具 | 说明 |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `read_editor_log` | 日志聚合:Error/Warning 分组、计数、首现时间、样本 |
|
|
85
|
+
| `scan_folder_assets` | 目录资产审计:按大小排序、类型汇总、Top 开销元凶 |
|
|
86
|
+
|
|
87
|
+
外部进程(server 侧调 UAT,编辑器须关闭;首个非只读工具族):
|
|
88
|
+
|
|
89
|
+
| 工具 | 说明 |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `cook_package` | 触发 cook/package;**默认 dry-run,`dry_run=False`+`confirm=True` 双钥才执行**;同工程 BUSY 互斥 |
|
|
92
|
+
| `get_cook_status` | 任务状态**读时惰性推导**(done 标记/硬时限/pid/日志收束行),无守护进程 |
|
|
93
|
+
| `attribute_cook_errors` | 日志→资产归因:指纹分组 + 磁盘解析;桥在线时 `describe_many`/`referencers_many` 批量富化 |
|
|
94
|
+
|
|
95
|
+
性能规则(v0.3 · 混合通道:桥在线出全量、离线自动半份并逐条注明 skip):
|
|
96
|
+
|
|
97
|
+
| 工具 | 说明 |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `get_perf_report` | 静态性能健康报告:7 规则(资产大小/纹理尺寸/网格三角+单LOD/光源重复/WP 外部 actor 数/cook 静默丢弃/cook 空煮),top-80 大资产抽样度量 |
|
|
100
|
+
| `list_perf_rules` | 当前 profile 生效规则与阈值(可解释性入口;pc/console/mobile 三档 + 工程 prism.toml 覆盖) |
|
|
101
|
+
| `get_asset_metrics` | 批量原语度量:纹理 px / 网格 LOD 三角,带 `tried` API 证据链(5.4/5.8 双版本校准) |
|
|
102
|
+
|
|
103
|
+
连接发现 · 引用链 · 迁移(v1.0 · 去硬编码路径 + 依赖闭包分析 + 写操作):
|
|
104
|
+
|
|
105
|
+
| 工具 | 说明 |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `list_projects` | 零配置枚举已注册工程 + 活跃态(`live`/`heartbeat_age`);供 agent 先探后用 |
|
|
108
|
+
| `get_asset_chain` | 递归依赖闭包(迁移分析):`uses`=要带走的全部 `/Game` 依赖(带类别/磁盘大小,`by_class` 聚合体量);`used_by`=谁依赖它;`scope=game` 默认滤引擎噪声 |
|
|
109
|
+
| `preview_asset_migration` | 只读迁移预览:源/目标冲突检测 + `used_by` 影响面 → dry-run 计划(目标仅剩改名遗留的 `ObjectRedirector` 桩时不算冲突) |
|
|
110
|
+
| `migrate_asset_rename` | 真改名(写 · 双钥):走 `rename_asset(pkg,pkg)` 自动修引用,成功后默认清一次重定向桩 |
|
|
111
|
+
| `migrate_asset_move` | 真移动(写 · 双钥):移动到目录,可带 new_name 同时改名,默认清桩 |
|
|
112
|
+
| `migrate_asset` | 迁移 A·同工程复制(写 · 双钥):把资产 + 其 `/Game` `uses` 闭包镜像复制到目标目录;默认 dry-run 出复制计划(按类数量/体量/冲突);原件不动 |
|
|
113
|
+
|
|
114
|
+
所有桥 / 离线 / cook / 迁移工具接受可选 `project=` 选择器;未配置工程路径时按判定阶梯自动定位(详见 REQUIREMENTS §3.5)。
|
|
115
|
+
|
|
116
|
+
## 快速开始
|
|
117
|
+
|
|
118
|
+
> v0.1/v0.2 均已跑通。完整操作(`pip` 安装 / UE 开机自启 / Codex·opencode 配置 / cook 双钥)见 [docs/SETUP.md](docs/SETUP.md);设计契约见 [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md)。下面是最小三步速览(`<REPO>`=仓库绝对路径、`<PROJ>`=UE 工程根,均用正斜杠)。
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 1. 安装 server
|
|
122
|
+
pip install git+https://github.com/Za0Shu1/ue-prism.git # 现在即可用(GitHub Releases 也已就绪)
|
|
123
|
+
# pip install ue-prism # PyPI:注册免费账号 + 配 Trusted Publishing 后启用(见 docs/SETUP.md §7)
|
|
124
|
+
|
|
125
|
+
# 2. 编辑器自启(推荐):在工程 Config/DefaultEngine.ini 注册一次后,重开编辑器即自动起 bridge:
|
|
126
|
+
# [/Script/PythonScriptPlugin.PythonScriptPluginSettings]
|
|
127
|
+
# +StartupScripts="<REPO>/scripts/autostart_bridge.py"
|
|
128
|
+
# 手动兜底(Output Log 的 [PY] 控制台):
|
|
129
|
+
import sys; sys.path.insert(0, r"<REPO>")
|
|
130
|
+
import prism.bridge; prism.bridge.start(r"<PROJ>/Saved/Prism")
|
|
131
|
+
|
|
132
|
+
# 3. 在 MCP 客户端注册(一条命令幂等写入、零工程路径;v1.0 bridge 自注册后 server 自动发现):
|
|
133
|
+
prism setup --client codex # 或 opencode / cursor / all;--dry-run 预览、--uninstall 移除
|
|
134
|
+
# 等价手写:command: python -m prism.server (或装包后 ue-prism)
|
|
135
|
+
# 多工程时 agent 先 list_projects 探测、或调用时传 project=<工程名>
|
|
136
|
+
# (仍可钉死单工程:加 env PRISM_BUS_DIR=<PROJ>/Saved/Prism PRISM_PROJECT_DIR=<PROJ>)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
先问 agent 一句验证连通:*"你现在连上的是哪个 UE 项目?里面加载了哪些地图?"*
|
|
140
|
+
接入日志工具后,可试:*"把最近编辑器日志里的 Error/Warning 归类,指出可疑原因。"*
|
|
141
|
+
cook 回路(关编辑器、双钥)可试:*"先 dry-run 看看 cook 会跑什么命令"* → *"确认执行"* → *"把这次 cook 的报错归因到资产"*
|
|
142
|
+
|
|
143
|
+
## 路线图
|
|
144
|
+
|
|
145
|
+
- **v0.1**(已发布 · v0.1.0):三层骨架 + 文件总线 + `ping` 端到端连通 + 结构化错误契约 + UE 5.4 实测 + 6 只读工具 + bridge 编辑器开机自启
|
|
146
|
+
- **v0.2**(已发布 · v0.2.0,v0.2.1 已清尾):cook/package 全回路真机实证完毕 —— cook 容错三坑、package 25s/786MB 一次通过、5.8 版式(`PRISM_ENGINE_ROOT`)同工程通过、失败注入还原闭环
|
|
147
|
+
- **v0.3**(已发布 · v0.3.0):性能静态规则引擎(7 规则全来自真机案例;纹理/网格度量 5.4 与 5.8 校准一致;桥离线自动半份报告)
|
|
148
|
+
- **v1.0**(已交付 · 版本 1.0.0,待打 tag 发 PyPI):连接发现(零配置/自注册/`project=` 选择器)+ 引用链分析(`get_asset_chain` 传递闭包)+ 迁移套件(rename/move/copy 写操作,双钥 + redirector 自动清理);UE 5.4.4 真机实证完毕
|
|
149
|
+
- 之后:bridge 打包成正式 UE 插件、socket 传输、Epic 官方 5.8 toolset 可选后端、截图验证工具
|
|
150
|
+
|
|
151
|
+
## 运行要求
|
|
152
|
+
|
|
153
|
+
- Unreal Engine 5.0+,启用 Python Editor Scripting(launcher 安装自带)
|
|
154
|
+
- 编辑器侧:引擎内嵌 Python 随 UE 版本 3.7→3.11(domain 层守 3.7 语法子集)
|
|
155
|
+
- server 侧:Python ≥3.10 + MCP Python SDK(锁 2.x),无引擎依赖
|
|
156
|
+
|
|
157
|
+
## 本机真机测试(UE 5.4)
|
|
158
|
+
|
|
159
|
+
- 工程目录:`F:/UE_Projects/TestProject/UE_PRISM_Project`(已在 DefaultEngine.ini 注册 bridge 自启;5.4.4 与 5.8.2 双版本真机验证过)
|
|
160
|
+
- 文件总线目录:`<工程>/Saved/Prism`(含 `heartbeat.json` 活性标记与 `tasks/` 任务档案)
|
|
161
|
+
- ue-prism 仓库:`F:/VibeCoding/ue-prism`;server 侧解释器:`F:/venv/Scripts/python.exe`
|
|
162
|
+
|
|
163
|
+
**1) 双击打开工程(EngineAssociation=5.4)**——bridge 已由 StartupScripts 自启,Output Log 出现 `[prism] bridge started` 即在线;`<工程>/Saved/Prism/heartbeat.json` 应每秒级更新。
|
|
164
|
+
|
|
165
|
+
**2) 另开系统终端,从总线外部验证连通与快速失败:**
|
|
166
|
+
```powershell
|
|
167
|
+
F:\venv\Scripts\python.exe F:\VibeCoding\ue-prism\scripts\ping_cli.py "F:/UE_Projects/TestProject/UE_PRISM_Project/Saved/Prism"
|
|
168
|
+
```
|
|
169
|
+
期望:`ok:true`;关编辑器再戳 → 约 1s 内 `BRIDGE_UNREACHABLE`(心跳陈旧),不再空等 30s。
|
|
170
|
+
|
|
171
|
+
**3) cook 回路(关闭编辑器后;细则见 docs/SETUP.md §4):**
|
|
172
|
+
```powershell
|
|
173
|
+
F:\venv\Scripts\python.exe -m prism.server --project-dir F:\UE_Projects\TestProject\UE_PRISM_Project
|
|
174
|
+
# 或在 agent 里:cook_package() 干跑 -> cook_package(dry_run=False, confirm=True) -> get_cook_status(task_id)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**4) 可选——无头探针(unreal API 名跨版本校核):**
|
|
178
|
+
```powershell
|
|
179
|
+
& "E:\UnrealEngine\UE_5.4\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
|
|
180
|
+
"F:\UE_Projects\TestProject\UE_PRISM_Project\UE_PRISM_Project.uproject" `
|
|
181
|
+
-run=pythonscript -script="F:\VibeCoding\ue-prism\scripts\probe_unreal.py"
|
|
182
|
+
```
|
|
183
|
+
## 相关项目
|
|
184
|
+
|
|
185
|
+
- [chongdashu/unreal-mcp](https://github.com/chongdashu/unreal-mcp)、[IvanMurzak/Unreal-MCP](https://github.com/IvanMurzak/Unreal-MCP) —— 侧重编辑器**操控**;ue-prism 侧重**诊断与分析**
|
|
186
|
+
- Epic 官方 `ModelContextProtocol` 插件(UE 5.8)—— 原生但实验性;ue-prism 覆盖 5.0–5.8 行为一致
|
|
187
|
+
|
|
188
|
+
## 设计文档
|
|
189
|
+
|
|
190
|
+
需求、契约与决策记录见 [docs/REQUIREMENTS.md](docs/REQUIREMENTS.md);设计草案:[v0.2 cook 全回路](docs/DESIGN_v0.2.md)(已实现并真机实证)· [v0.3 性能静态规则引擎](docs/DESIGN_v0.3.md)(评审中)。
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT © [Za0Shu1](https://github.com/Za0Shu1)
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
Unreal® 与 Unreal Engine® 为 Epic Games, Inc. 注册商标。本项目与 Epic Games 无关联,亦未获其背书。
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""v0.2 PR-3 日志→资产归因(docs/DESIGN_v0.2.md §5):纯标准库离线解析;富化单次经总线。
|
|
2
|
+
|
|
3
|
+
流水线:
|
|
4
|
+
1) 错误行识别(LogXxx: Error / Fatal / UAT "ERROR:" 三形态)
|
|
5
|
+
2) 指纹分组:路径与数字归一化,同类错误聚成 pattern
|
|
6
|
+
3) 行内提取 /Game 包路径(含 Content\\...uasset 相对形态)
|
|
7
|
+
4) 磁盘解析(存在性/大小;离线可用)
|
|
8
|
+
5) bridge 在线(心跳新鲜,PR-0.5 基建)时批量富化:describe_many + referencers_many
|
|
9
|
+
各一次总线往返(绝不逐资产 N 次 —— 设计稿洞②)
|
|
10
|
+
列表纪律:pattern ≤ top(默认 30),组内资产 ≤ 10;total/truncated/cap 全程携带。
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from . import bus, envelope
|
|
18
|
+
|
|
19
|
+
GAME_RE = re.compile(r"/Game/[A-Za-z0-9_]+(?:/[A-Za-z0-9_]+)*(?:\.[A-Za-z0-9_]+)?")
|
|
20
|
+
CONTENT_REL_RE = re.compile(r"Content[/\\]+([^\s\"']+?\.(?:uasset|umap))")
|
|
21
|
+
_TS_RE = re.compile(r"^\[[^\]]*\](\[\s*\d*\])?")
|
|
22
|
+
_LOG_ERR_RE = re.compile(r"^([A-Za-z0-9_]+):\s*(Error|Fatal error|Fatal|ERROR)\s*:?\s*(.*)$")
|
|
23
|
+
_UAT_ERR_RE = re.compile(r"^ERROR:\s*(.*)$")
|
|
24
|
+
_FATAL_ERR_RE = re.compile(r"^Fatal\s*[Ee]rror\s*[:!]\s*(.*)$")
|
|
25
|
+
_NUM_RE = re.compile(r"\d+")
|
|
26
|
+
|
|
27
|
+
ENRICH_CAP = 60 # 单次总线往返最多带走的资产数
|
|
28
|
+
PER_GROUP_CAP = 10 # 每 pattern 展示资产上限
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _line_category_message(raw):
|
|
32
|
+
"""返回 (category, message) 或 None。时间戳剥离后按三形态匹配。"""
|
|
33
|
+
line = _TS_RE.sub("", raw.strip())
|
|
34
|
+
m = _UAT_ERR_RE.match(line)
|
|
35
|
+
if m:
|
|
36
|
+
return ("UAT", m.group(1))
|
|
37
|
+
m = _LOG_ERR_RE.match(line)
|
|
38
|
+
if m:
|
|
39
|
+
return (m.group(1), m.group(3))
|
|
40
|
+
m = _FATAL_ERR_RE.match(line)
|
|
41
|
+
if m:
|
|
42
|
+
return ("FatalError", m.group(1))
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fingerprint(category, message):
|
|
47
|
+
s = GAME_RE.sub("<ASSET>", message)
|
|
48
|
+
s = CONTENT_REL_RE.sub("<ASSET>", s)
|
|
49
|
+
s = _NUM_RE.sub("#", s)
|
|
50
|
+
return (category + ": " + s.strip())[:200]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _asset_candidates(line):
|
|
54
|
+
out = []
|
|
55
|
+
for m in GAME_RE.finditer(line):
|
|
56
|
+
pkg = m.group(0).split(".", 1)[0] if "." in m.group(0).split("/")[-1] else m.group(0)
|
|
57
|
+
if pkg not in out:
|
|
58
|
+
out.append(pkg)
|
|
59
|
+
for m in CONTENT_REL_RE.finditer(line):
|
|
60
|
+
rel = m.group(1).replace("\\", "/")
|
|
61
|
+
rel = rel[: rel.rfind(".")] # 去扩展名
|
|
62
|
+
pkg = "/Game/" + rel if not rel.startswith("Game/") else rel
|
|
63
|
+
if pkg not in out:
|
|
64
|
+
out.append(pkg)
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _disk_info(content_root, pkg):
|
|
69
|
+
rel = pkg[len("/Game/"):].replace("/", os.sep) if pkg.startswith("/Game/") else None
|
|
70
|
+
if rel is None:
|
|
71
|
+
return {"path": pkg, "exists": False, "size_bytes": None}
|
|
72
|
+
for ext in (".uasset", ".umap"):
|
|
73
|
+
p = os.path.join(content_root, rel + ext)
|
|
74
|
+
if os.path.isfile(p):
|
|
75
|
+
try:
|
|
76
|
+
return {"path": pkg, "exists": True, "size_bytes": os.path.getsize(p)}
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
return {"path": pkg, "exists": False, "size_bytes": None}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_log(log_path, project_dir=None, top=30):
|
|
83
|
+
"""解析 cook/编辑器日志 -> 归因报告骨架(未富化)。编码容错 utf-8-sig + replace。"""
|
|
84
|
+
top = max(1, int(top))
|
|
85
|
+
content_root = os.path.join(project_dir, "Content") if project_dir else None
|
|
86
|
+
groups = {}
|
|
87
|
+
order = []
|
|
88
|
+
scanned = 0
|
|
89
|
+
error_lines = 0
|
|
90
|
+
with open(log_path, "r", encoding="utf-8-sig", errors="replace") as f:
|
|
91
|
+
for i, raw in enumerate(f):
|
|
92
|
+
scanned += 1
|
|
93
|
+
cm = _line_category_message(raw)
|
|
94
|
+
if cm is None:
|
|
95
|
+
continue
|
|
96
|
+
category, message = cm
|
|
97
|
+
error_lines += 1
|
|
98
|
+
fp = _fingerprint(category, message)
|
|
99
|
+
g = groups.get(fp)
|
|
100
|
+
if g is None:
|
|
101
|
+
g = groups[fp] = {"pattern": fp, "count": 0, "first_line_no": i + 1,
|
|
102
|
+
"sample_line": raw.strip()[:500], "assets": {}}
|
|
103
|
+
order.append(fp)
|
|
104
|
+
g["count"] += 1
|
|
105
|
+
for pkg in _asset_candidates(raw):
|
|
106
|
+
if pkg not in g["assets"]:
|
|
107
|
+
info = _disk_info(content_root, pkg) if content_root else {"path": pkg}
|
|
108
|
+
g["assets"][pkg] = info
|
|
109
|
+
patterns = []
|
|
110
|
+
for fp in sorted(order, key=lambda k: -groups[k]["count"]):
|
|
111
|
+
g = groups[fp]
|
|
112
|
+
assets = sorted(g["assets"].values(), key=lambda a: (-int(a.get("size_bytes") or 0), a["path"]))
|
|
113
|
+
patterns.append({
|
|
114
|
+
"pattern": g["pattern"],
|
|
115
|
+
"count": g["count"],
|
|
116
|
+
"first_line_no": g["first_line_no"],
|
|
117
|
+
"sample_line": g["sample_line"],
|
|
118
|
+
"assets": assets[:PER_GROUP_CAP],
|
|
119
|
+
"assets_total": len(assets),
|
|
120
|
+
"assets_truncated": len(assets) > PER_GROUP_CAP,
|
|
121
|
+
})
|
|
122
|
+
total = len(patterns)
|
|
123
|
+
return {
|
|
124
|
+
"log_path": os.path.abspath(log_path),
|
|
125
|
+
"scanned_lines": scanned,
|
|
126
|
+
"error_lines": error_lines,
|
|
127
|
+
"patterns": patterns[:top],
|
|
128
|
+
"total": total,
|
|
129
|
+
"truncated": total > top,
|
|
130
|
+
"cap": top,
|
|
131
|
+
"enriched": False,
|
|
132
|
+
"enrich_note": "offline",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def all_assets(report):
|
|
137
|
+
out = []
|
|
138
|
+
for g in report["patterns"]:
|
|
139
|
+
for a in g["assets"]:
|
|
140
|
+
if a["path"] not in out:
|
|
141
|
+
out.append(a["path"])
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def enrich_via_bus(bus_dir, paths):
|
|
146
|
+
"""心跳新鲜才发起(不付 30s 探测税);返回 (map|None, note)。"""
|
|
147
|
+
if not paths:
|
|
148
|
+
return None, "no_assets"
|
|
149
|
+
age = bus.heartbeat_age(bus_dir)
|
|
150
|
+
if age is None or age > bus.HB_STALE_SECONDS:
|
|
151
|
+
return None, "bridge offline or heartbeat missing/stale"
|
|
152
|
+
client = bus.BusClient(bus_dir, timeout=10)
|
|
153
|
+
want = list(paths[:ENRICH_CAP])
|
|
154
|
+
d = client.call("describe_many", {"paths": want})
|
|
155
|
+
r = client.call("referencers_many", {"paths": want, "cap": 10})
|
|
156
|
+
if not d.get("ok"):
|
|
157
|
+
return None, "describe_many failed: %s" % (d.get("error") or {}).get("code", "?")
|
|
158
|
+
emap = {}
|
|
159
|
+
for item in d["result"]["items"]:
|
|
160
|
+
if item.get("ok") is False: # 领域函数自返信封(如 UE_API_MISMATCH)
|
|
161
|
+
continue
|
|
162
|
+
key = item.get("package_name") or item.get("query")
|
|
163
|
+
if key:
|
|
164
|
+
emap.setdefault(key, {}).update(
|
|
165
|
+
{"class": item.get("class"), "found": item.get("found"),
|
|
166
|
+
"size_bytes": item.get("size_bytes")})
|
|
167
|
+
if r.get("ok"):
|
|
168
|
+
for item in r["result"]["items"]:
|
|
169
|
+
key = item.get("package_name") or item.get("query")
|
|
170
|
+
if key:
|
|
171
|
+
emap.setdefault(key, {})["used_by_count"] = item.get("used_by_count")
|
|
172
|
+
note = "enriched %d/%d assets" % (len(emap), len(want))
|
|
173
|
+
if len(paths) > ENRICH_CAP:
|
|
174
|
+
note += " (capped at %d)" % ENRICH_CAP
|
|
175
|
+
return emap, note
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def apply_enrichment(report, emap, note):
|
|
179
|
+
if not emap:
|
|
180
|
+
report["enrich_note"] = note
|
|
181
|
+
return
|
|
182
|
+
for g in report["patterns"]:
|
|
183
|
+
for a in g["assets"]:
|
|
184
|
+
e = emap.get(a["path"]) or {}
|
|
185
|
+
for k in ("class", "used_by_count", "found"):
|
|
186
|
+
if k in e and e[k] is not None:
|
|
187
|
+
a[k] = e[k]
|
|
188
|
+
g["assets"].sort(key=lambda a: (-(int(a.get("used_by_count") or 0)),
|
|
189
|
+
-int(a.get("size_bytes") or 0), a["path"]))
|
|
190
|
+
report["enriched"] = True
|
|
191
|
+
report["enrich_note"] = note
|