PyREUser3 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,304 @@
1
+ """导出阶段的枚举元数据处理逻辑。
2
+
3
+ 本模块在导出器内部维护多个运行期索引:固定枚举的“值 -> 名称”查找表、
4
+ “成员名 -> 可能所属枚举类型”的反向索引,以及字段/可序列化类型/泛型容器的
5
+ 枚举上下文。这些索引来源于显式传入的 ``il2cpp_dump.json``,用于把数值枚举
6
+ 转换成可读标签。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from ..core import ParseError
16
+ from ..rich_ui import get_console
17
+
18
+
19
+ class ExporterMetadataMixin:
20
+ """负责从 il2cpp dump 中提取和应用枚举上下文。"""
21
+
22
+ @staticmethod
23
+ def _id_formatter(key: str, value: int) -> str:
24
+ """把枚举值格式化为导出 JSON 中的可读标签。
25
+
26
+ 参数:
27
+ key (str): 枚举成员名。
28
+ value (int): 固定枚举数值。
29
+
30
+ 返回:
31
+ str: 形如 ``[123] MemberName`` 的可读标签。
32
+ """
33
+ return f"[{value}] {key}"
34
+
35
+ @staticmethod
36
+ def _to_u32(value: int) -> int:
37
+ """把整数转换到无符号 32 位范围。
38
+
39
+ 参数:
40
+ value (int): 输入整数。
41
+
42
+ 返回:
43
+ int: 无符号 32 位值(0 ~ 0xFFFFFFFF)。
44
+ """
45
+ return value & 0xFFFFFFFF
46
+
47
+ @staticmethod
48
+ def _to_s32(value: int) -> int:
49
+ """把整数转换为有符号 32 位表示。
50
+
51
+ 参数:
52
+ value (int): 输入整数。
53
+
54
+ 返回:
55
+ int: 有符号 32 位值(-0x80000000 ~ 0x7FFFFFFF)。
56
+ """
57
+ u32 = value & 0xFFFFFFFF
58
+ return u32 if u32 < 0x80000000 else u32 - 0x100000000
59
+
60
+ def _build_enum_lookup_from_enums_internal(
61
+ self, raw: Any
62
+ ) -> dict[str, dict[int, tuple[str, int]]]:
63
+ """根据 `Enums_Internal` 形状的数据建立固定枚举查找表。
64
+
65
+ 参数:
66
+ raw (Any): ``export_enums_internal`` 产出的 ``枚举类型 -> {成员名 -> 值}`` 映射。
67
+
68
+ 返回:
69
+ dict[str, dict[int, tuple[str, int]]]: ``固定枚举类型 -> {数值 -> (成员名, 原始值)}``
70
+ 的查找表,仅包含以 ``_Fixed`` 结尾的枚举类型。
71
+ """
72
+ lookup: dict[str, dict[int, tuple[str, int]]] = {}
73
+ if not isinstance(raw, dict):
74
+ return lookup
75
+
76
+ for enum_type, members in raw.items():
77
+ if (
78
+ not isinstance(enum_type, str)
79
+ or not isinstance(members, dict)
80
+ or not enum_type.endswith("_Fixed")
81
+ ):
82
+ continue
83
+ value_map: dict[int, tuple[str, int]] = {}
84
+ for member_name, raw_value in members.items():
85
+ if not isinstance(member_name, str) or not isinstance(raw_value, int):
86
+ continue
87
+ entry = (member_name, raw_value)
88
+ # 同一个底层 32 位值在 JSON 中可能以有符号或无符号形式出现,
89
+ # 两种写法都映射回同一个枚举成员,减少后处理分支。
90
+ value_map[self._to_s32(raw_value)] = entry
91
+ value_map[self._to_u32(raw_value)] = entry
92
+ if value_map:
93
+ lookup[enum_type] = value_map
94
+ return lookup
95
+
96
+ def _load_enum_lookup(self) -> dict[str, dict[int, tuple[str, int]]]:
97
+ """兼容钩子:当前版本只从显式 il2cpp 输入构建枚举表。
98
+
99
+ 返回:
100
+ dict[str, dict[int, tuple[str, int]]]: 始终为空映射;真实枚举表由
101
+ :meth:`_ensure_internal_metadata_files` 生成。
102
+ """
103
+ return {}
104
+
105
+ def _resolve_il2cpp_dump_path(self) -> Path | None:
106
+ """返回显式传入且存在的 `il2cpp_dump.json` 路径。
107
+
108
+ 返回:
109
+ Path | None: 文件存在时返回其路径,否则返回 ``None``。
110
+ """
111
+ return self.il2cpp_dump_path if self.il2cpp_dump_path.is_file() else None
112
+
113
+ def _ensure_internal_metadata_files(self) -> dict:
114
+ """根据必填 il2cpp dump 在输出目录生成 `Enums_Internal.json`。
115
+
116
+ 返回:
117
+ dict: 从 dump 中提取的内部枚举映射(``枚举类型 -> {成员名 -> 值}``)。
118
+
119
+ 异常:
120
+ FileNotFoundError: 当 ``il2cpp_dump.json`` 不存在时抛出。
121
+ ParseError: 当读取或解析 dump 失败时抛出。
122
+ """
123
+ dump_path = self._resolve_il2cpp_dump_path()
124
+ if dump_path is None:
125
+ raise FileNotFoundError(
126
+ f"il2cpp_dump.json not found: {self.il2cpp_dump_path}"
127
+ )
128
+ try:
129
+ with dump_path.open("r", encoding="utf-8") as f:
130
+ il2cpp_dump = json.load(f)
131
+ except Exception as exc:
132
+ raise ParseError(f"failed to read il2cpp dump: {dump_path}") from exc
133
+
134
+ self.output_root.mkdir(parents=True, exist_ok=True)
135
+ enums_out = self.output_root / "Enums_Internal.json"
136
+ enums_internal = self.export_enums_internal(il2cpp_dump)
137
+ # 这个文件是导出结果的一部分,方便用户排查枚举数值来源。
138
+ with enums_out.open("w", encoding="utf-8") as f:
139
+ json.dump(enums_internal, f, ensure_ascii=False, indent=2)
140
+ return enums_internal
141
+
142
+ def _rebuild_enum_member_index(self) -> None:
143
+ """建立反向索引:枚举成员名 -> 可能所属的固定枚举类型。
144
+
145
+ 遍历当前 ``enum_lookup``,把每个成员名映射到所有可能包含它的枚举类型,
146
+ 供后续根据“成员名 + 数值”反推唯一枚举类型。
147
+
148
+ 返回:
149
+ None: 直接重建 ``self.enum_member_to_types``。
150
+ """
151
+ self.enum_member_to_types = {}
152
+ for enum_type, value_map in self.enum_lookup.items():
153
+ if not isinstance(enum_type, str) or not isinstance(value_map, dict):
154
+ continue
155
+ for member_name, _entry in value_map.values():
156
+ if not isinstance(member_name, str):
157
+ continue
158
+ types = self.enum_member_to_types.setdefault(member_name, [])
159
+ if enum_type not in types:
160
+ types.append(enum_type)
161
+
162
+ def _infer_enum_type_from_member_and_value(
163
+ self, member_name: str, value: int
164
+ ) -> str | None:
165
+ """根据成员名和具体数值反推出唯一的枚举类型。
166
+
167
+ 参数:
168
+ member_name (str): 枚举成员名。
169
+ value (int): 该成员对应的数值。
170
+
171
+ 返回:
172
+ str | None: 唯一匹配时返回固定枚举类型名;无候选或多重匹配时返回 ``None``。
173
+ """
174
+ candidates = self.enum_member_to_types.get(member_name)
175
+ if not candidates:
176
+ return None
177
+ matched: list[str] = []
178
+ for enum_type in candidates:
179
+ value_map = self.enum_lookup.get(enum_type)
180
+ if value_map is None:
181
+ continue
182
+ if (
183
+ value in value_map
184
+ or self._to_s32(value) in value_map
185
+ or self._to_u32(value) in value_map
186
+ ):
187
+ matched.append(enum_type)
188
+ if len(matched) == 1:
189
+ return matched[0]
190
+ # 多个类型同时匹配时不猜测,避免把字段错误标成另一个枚举。
191
+ return None
192
+
193
+ def _apply_enum_context(self, raw: dict) -> None:
194
+ """把枚举上下文应用到导出器的运行时索引。
195
+
196
+ 参数:
197
+ raw (dict): 从 il2cpp dump 提取出的枚举上下文对象,含
198
+ ``class_field_fixed_types``、``serializable_to_fixed``、
199
+ ``generic_container_rules`` 等键。
200
+
201
+ 返回:
202
+ None: 重建 ``class_field_fixed_types`` / ``serializable_to_fixed`` /
203
+ ``generic_container_rules`` / ``param_type_default_enum`` 四个索引。
204
+ """
205
+ self.class_field_fixed_types = {}
206
+ self.serializable_to_fixed = {}
207
+ self.generic_container_rules = {}
208
+ self.param_type_default_enum = {}
209
+
210
+ class_field_fixed_types = raw.get("class_field_fixed_types")
211
+ if isinstance(class_field_fixed_types, dict):
212
+ for cls_name, field_map in class_field_fixed_types.items():
213
+ if not isinstance(cls_name, str) or not isinstance(field_map, dict):
214
+ continue
215
+ cleaned: dict[str, str] = {}
216
+ for field_name, enum_type in field_map.items():
217
+ # 只接受 `*_Fixed`,避免普通类型名误入枚举转换流程。
218
+ if (
219
+ isinstance(field_name, str)
220
+ and isinstance(enum_type, str)
221
+ and enum_type.endswith("_Fixed")
222
+ ):
223
+ cleaned[field_name] = enum_type
224
+ if cleaned:
225
+ self.class_field_fixed_types[cls_name] = cleaned
226
+
227
+ serializable_to_fixed = raw.get("serializable_to_fixed")
228
+ if isinstance(serializable_to_fixed, dict):
229
+ for serializable_name, fixed_name in serializable_to_fixed.items():
230
+ if (
231
+ isinstance(serializable_name, str)
232
+ and isinstance(fixed_name, str)
233
+ and fixed_name.endswith("_Fixed")
234
+ ):
235
+ self.serializable_to_fixed[serializable_name] = fixed_name
236
+
237
+ generic_container_rules = raw.get("generic_container_rules")
238
+ # 临时收集“参数类型 -> 枚举类型集合”,用于推断每个参数类型的默认枚举。
239
+ param_to_enum_sets: dict[str, set[str]] = {}
240
+ if isinstance(generic_container_rules, dict):
241
+ for container_name, rule in generic_container_rules.items():
242
+ if not isinstance(container_name, str) or not isinstance(rule, dict):
243
+ continue
244
+ param_type = rule.get("param_type")
245
+ enum_type = rule.get("enum_type")
246
+ if (
247
+ isinstance(param_type, str)
248
+ and isinstance(enum_type, str)
249
+ and enum_type.endswith("_Fixed")
250
+ ):
251
+ self.generic_container_rules[container_name] = (
252
+ param_type,
253
+ enum_type,
254
+ )
255
+ param_to_enum_sets.setdefault(param_type, set()).add(enum_type)
256
+
257
+ for param_type, enum_types in param_to_enum_sets.items():
258
+ if len(enum_types) == 1:
259
+ # 同一参数类型只被一个枚举容器使用时,可作为默认枚举类型。
260
+ self.param_type_default_enum[param_type] = next(iter(enum_types))
261
+
262
+ def _load_enum_context_from_il2cpp_dump(self) -> bool:
263
+ """直接从 il2cpp dump 加载枚举上下文。
264
+
265
+ 返回:
266
+ bool: 成功读取并应用上下文返回 ``True``;dump 缺失或读取失败返回 ``False``。
267
+ """
268
+ dump_path = self._resolve_il2cpp_dump_path()
269
+ if dump_path is None:
270
+ return False
271
+ try:
272
+ with dump_path.open("r", encoding="utf-8") as f:
273
+ il2cpp_dump = json.load(f)
274
+ except Exception:
275
+ return False
276
+ context = self.export_enum_context_internal(il2cpp_dump)
277
+ self._apply_enum_context(context)
278
+ return True
279
+
280
+ def _ensure_enum_lookup(self) -> None:
281
+ """检查枚举表和上下文是否可用,并在缺失时输出警告。
282
+
283
+ 枚举表缺失不会阻止导出,只会让数值保持原始整数形式。
284
+
285
+ 返回:
286
+ None: 重建成员索引;必要时通过 Rich 控制台打印告警。
287
+ """
288
+ if self.enum_lookup:
289
+ self._rebuild_enum_member_index()
290
+ return
291
+ self._rebuild_enum_member_index()
292
+ if not self.enum_lookup:
293
+ get_console().log(
294
+ "[warn] enum value formatting disabled "
295
+ f"(source: {self._resolve_il2cpp_dump_path() or 'not found'})",
296
+ style="yellow",
297
+ )
298
+ if not self.class_field_fixed_types and not self.serializable_to_fixed:
299
+ context_source = str(self._resolve_il2cpp_dump_path() or "not found")
300
+ get_console().log(
301
+ "[warn] Enum context not loaded, enum conversion may be incomplete "
302
+ f"(source: {context_source})",
303
+ style="yellow",
304
+ )
@@ -0,0 +1,346 @@
1
+ """导出 JSON 的枚举后处理和展示清理逻辑。
2
+
3
+ 原始解析结果是“紧凑但机器化”的中间形态。本模块负责把它整理成更适合人工
4
+ 阅读和编辑的 JSON:规范化类名、把固定枚举数值转换成 ``[值] 名称`` 标签、
5
+ 移除内部索引、拍平只含单值的包装对象,并圆整展示用浮点数。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ class ExporterPostprocessMixin:
14
+ """负责把原始解析结果整理成人类更容易修改的 JSON。"""
15
+
16
+ def _fixed_type_candidates(self, type_name: str) -> list[str]:
17
+ """根据类型名生成可能的固定枚举类型名。
18
+
19
+ 参数:
20
+ type_name (str): 源类型名。
21
+
22
+ 返回:
23
+ list[str]: 去重后的候选固定枚举类型名列表(含原名及可能的 ``*_Fixed`` 变体)。
24
+ """
25
+ candidates = [type_name]
26
+ if type_name.endswith("_Serializable"):
27
+ candidates.append(f"{type_name[:-13]}_Fixed")
28
+ if "Serializable" in type_name:
29
+ candidates.append(type_name.replace("Serializable", "Fixed"))
30
+ out: list[str] = []
31
+ seen: set[str] = set()
32
+ for candidate in candidates:
33
+ if candidate not in seen:
34
+ seen.add(candidate)
35
+ out.append(candidate)
36
+ return out
37
+
38
+ def _normalize_to_fixed_enum_type(self, type_name: str) -> str:
39
+ """在可能时把类型名规范化为已知固定枚举类型。
40
+
41
+ 参数:
42
+ type_name (str): 源类型名。
43
+
44
+ 返回:
45
+ str: 命中已知固定枚举类型时返回该类型名;无法匹配时返回原值。
46
+ """
47
+ if not type_name or not self.enum_lookup:
48
+ return type_name
49
+ direct = self.serializable_to_fixed.get(type_name)
50
+ if direct is not None and direct in self.enum_lookup:
51
+ return direct
52
+ for candidate in self._fixed_type_candidates(type_name):
53
+ if candidate in self.enum_lookup:
54
+ return candidate
55
+ return type_name
56
+
57
+ def _format_enum_value(self, fixed_enum_type: str, value: int) -> Any:
58
+ """把数值映射成固定枚举的可读标签。
59
+
60
+ 参数:
61
+ fixed_enum_type (str): 固定枚举类型名。
62
+ value (int): 原始数值。
63
+
64
+ 返回:
65
+ Any: 能匹配时返回 ``[值] 名称`` 字符串,否则原样返回数值。
66
+ """
67
+ if not fixed_enum_type or not self.enum_lookup:
68
+ return value
69
+ value_map = self.enum_lookup.get(fixed_enum_type)
70
+ if value_map is None:
71
+ return value
72
+ # 依次尝试原值、有符号 32 位、无符号 32 位三种形式匹配。
73
+ matched = value_map.get(value)
74
+ if matched is None:
75
+ matched = value_map.get(self._to_s32(value))
76
+ if matched is None:
77
+ matched = value_map.get(self._to_u32(value))
78
+ if matched is None:
79
+ return value
80
+ member_name, fixed_value = matched
81
+ return self._id_formatter(member_name, fixed_value)
82
+
83
+ @staticmethod
84
+ def _looks_like_class_name(text: str) -> bool:
85
+ """判断字典键是否像完整类名。
86
+
87
+ 参数:
88
+ text (str): 字典键文本。
89
+
90
+ 返回:
91
+ bool: 含命名空间点号且不以 ``_`` 开头(即不像字段名)时返回 ``True``。
92
+ """
93
+ return "." in text and not text.startswith("_")
94
+
95
+ @staticmethod
96
+ def _class_name_variants(class_name: str | None) -> list[str]:
97
+ """生成不同 dump 中常见的类名别名。
98
+
99
+ 参数:
100
+ class_name (str | None): 当前类名,可为 ``None``。
101
+
102
+ 返回:
103
+ list[str]: 类名及其 ``cData`` / ``cParam`` 互换别名;输入为空时返回空列表。
104
+ """
105
+ if not class_name:
106
+ return []
107
+ variants = [class_name]
108
+ if class_name.endswith(".cData"):
109
+ variants.append(f"{class_name[:-6]}.cParam")
110
+ elif class_name.endswith(".cParam"):
111
+ variants.append(f"{class_name[:-7]}.cData")
112
+ return variants
113
+
114
+ def _resolve_field_enum_hint(
115
+ self, current_class: str | None, field_name: str
116
+ ) -> str | None:
117
+ """解析字段对应的固定枚举类型提示。
118
+
119
+ 参数:
120
+ current_class (str | None): 当前类上下文。
121
+ field_name (str): 字段名。
122
+
123
+ 返回:
124
+ str | None: 命中时返回固定枚举类型名;无法推断时返回 ``None``。
125
+ """
126
+ for class_variant in self._class_name_variants(current_class):
127
+ class_fields = self.class_field_fixed_types.get(class_variant, {})
128
+ fixed_field_type = class_fields.get(field_name)
129
+ if fixed_field_type:
130
+ return fixed_field_type
131
+ return None
132
+
133
+ def _resolve_class_default_enum(self, class_name: str | None) -> str | None:
134
+ """解析泛型参数容器类的默认枚举类型。
135
+
136
+ 参数:
137
+ class_name (str | None): 类名。
138
+
139
+ 返回:
140
+ str | None: 命中时返回默认固定枚举类型;无法推断时返回 ``None``。
141
+ """
142
+ for class_variant in self._class_name_variants(class_name):
143
+ enum_type = self.param_type_default_enum.get(class_variant)
144
+ if enum_type is not None:
145
+ return enum_type
146
+ return None
147
+
148
+ @staticmethod
149
+ def _is_enum_value_field(field_name: str | None) -> bool:
150
+ """判断字段名是否像枚举值字段。
151
+
152
+ 参数:
153
+ field_name (str | None): 字段名。
154
+
155
+ 返回:
156
+ bool: 看起来像 ``value``、``fixedid`` 或以 ``id`` 结尾时返回 ``True``。
157
+ """
158
+ if not field_name:
159
+ return False
160
+ key = field_name.strip("_").lower()
161
+ return key in {"value", "enumvalue", "fixedid"} or key.endswith("id")
162
+
163
+ def _postprocess_enum_nodes(
164
+ self,
165
+ value: Any,
166
+ current_class: str | None = None,
167
+ scalar_enum_hint: str | None = None,
168
+ class_default_enum: str | None = None,
169
+ container_param_rule: tuple[str, str] | None = None,
170
+ field_name: str | None = None,
171
+ ) -> Any:
172
+ """递归规范化类名,并把固定枚举数值转成可读标签。
173
+
174
+ 参数:
175
+ value (Any): 当前节点(dict / list / 标量)。
176
+ current_class (str | None): 当前类上下文。
177
+ scalar_enum_hint (str | None): 当前标量值可使用的枚举类型提示。
178
+ class_default_enum (str | None): 当前类作用域的默认枚举类型。
179
+ container_param_rule (tuple[str, str] | None): 泛型容器推断出的
180
+ ``(参数类型, 枚举类型)`` 关系。
181
+ field_name (str | None): 当前字段名。
182
+
183
+ 返回:
184
+ Any: 转换后的节点;整数在有枚举提示时被替换为可读标签,结构保持不变。
185
+ """
186
+ if isinstance(value, dict):
187
+ out: dict[str, Any] = {}
188
+ dict_level_enum_hint: str | None = None
189
+ enum_name = value.get("_EnumName")
190
+ fixed_id = value.get("_FixedID")
191
+ if isinstance(enum_name, str) and isinstance(fixed_id, int):
192
+ # 一些对象同时保存 `_EnumName` 和 `_FixedID`,可用二者反推
193
+ # 字段所属的固定枚举类型。
194
+ dict_level_enum_hint = self._infer_enum_type_from_member_and_value(
195
+ enum_name, fixed_id
196
+ )
197
+ for k, v in value.items():
198
+ if (
199
+ isinstance(k, str)
200
+ and self._looks_like_class_name(k)
201
+ and isinstance(v, dict)
202
+ ):
203
+ # 类名节点是紧凑 JSON 的边界,进入子对象时刷新类上下文。
204
+ normalized_class = self._normalize_to_fixed_enum_type(k)
205
+ key_out = (
206
+ normalized_class
207
+ if normalized_class != k and k.endswith("_Serializable")
208
+ else k
209
+ )
210
+ next_scalar_hint = (
211
+ normalized_class
212
+ if normalized_class in self.enum_lookup
213
+ else None
214
+ )
215
+ next_container_rule = self.generic_container_rules.get(k)
216
+ if next_container_rule is None:
217
+ next_container_rule = self.generic_container_rules.get(
218
+ normalized_class
219
+ )
220
+ next_default_enum = self._resolve_class_default_enum(
221
+ normalized_class
222
+ )
223
+ if (
224
+ container_param_rule is not None
225
+ and normalized_class == container_param_rule[0]
226
+ ):
227
+ # 泛型容器的参数类型命中时,参数对象内部默认使用容器枚举。
228
+ next_default_enum = container_param_rule[1]
229
+
230
+ out[key_out] = self._postprocess_enum_nodes(
231
+ v,
232
+ current_class=normalized_class,
233
+ scalar_enum_hint=next_scalar_hint,
234
+ class_default_enum=next_default_enum,
235
+ container_param_rule=next_container_rule,
236
+ field_name=None,
237
+ )
238
+ continue
239
+
240
+ field_hint: str | None = None
241
+ if current_class is not None:
242
+ # 优先使用 il2cpp/RSZ 上下文明确指出的字段枚举类型。
243
+ fixed_field_type = (
244
+ self._resolve_field_enum_hint(current_class, k)
245
+ if isinstance(k, str)
246
+ else None
247
+ )
248
+ if fixed_field_type:
249
+ field_hint = fixed_field_type
250
+ if (
251
+ field_hint is None
252
+ and class_default_enum is not None
253
+ and isinstance(k, str)
254
+ and self._is_enum_value_field(k)
255
+ ):
256
+ # 其次回退到当前类作用域的默认枚举类型。
257
+ field_hint = class_default_enum
258
+ if (
259
+ field_hint is None
260
+ and scalar_enum_hint is not None
261
+ and isinstance(k, str)
262
+ and self._is_enum_value_field(k)
263
+ ):
264
+ # 再退一步使用父级传下来的标量枚举提示。
265
+ field_hint = scalar_enum_hint
266
+ if (
267
+ field_hint is None
268
+ and dict_level_enum_hint is not None
269
+ and isinstance(k, str)
270
+ and k.strip("_").lower() == "fixedid"
271
+ ):
272
+ # 最后利用本对象 _EnumName/_FixedID 推断出的提示处理 fixedid 字段。
273
+ field_hint = dict_level_enum_hint
274
+
275
+ out[k] = self._postprocess_enum_nodes(
276
+ v,
277
+ current_class=current_class,
278
+ scalar_enum_hint=field_hint,
279
+ class_default_enum=class_default_enum,
280
+ container_param_rule=container_param_rule,
281
+ field_name=k if isinstance(k, str) else None,
282
+ )
283
+ return out
284
+
285
+ if isinstance(value, list):
286
+ # 列表元素沿用父节点的所有上下文提示逐个递归处理。
287
+ return [
288
+ self._postprocess_enum_nodes(
289
+ item,
290
+ current_class=current_class,
291
+ scalar_enum_hint=scalar_enum_hint,
292
+ class_default_enum=class_default_enum,
293
+ container_param_rule=container_param_rule,
294
+ field_name=field_name,
295
+ )
296
+ for item in value
297
+ ]
298
+ if isinstance(value, int) and scalar_enum_hint is not None:
299
+ # 只有在拿到枚举提示时才把整数替换为可读标签。
300
+ return self._format_enum_value(scalar_enum_hint, value)
301
+ return value
302
+
303
+ def _finalize_export_tree(self, value: Any) -> Any:
304
+ """移除内部索引并拍平仅包含 `value` 的包装对象。
305
+
306
+ 参数:
307
+ value (Any): 经枚举后处理的节点(dict / list / 标量)。
308
+
309
+ 返回:
310
+ Any: 清理后的节点:删除 ``index`` 键,并把只含单一 ``value`` 键或
311
+ 单一枚举类型键的对象拍平为其内部值。
312
+ """
313
+ if isinstance(value, dict):
314
+ out: dict[str, Any] = {}
315
+ for k, v in value.items():
316
+ if k == "index":
317
+ # index 是解析期内部索引,对外导出时去掉。
318
+ continue
319
+ out[k] = self._finalize_export_tree(v)
320
+ if len(out) == 1:
321
+ only_key = next(iter(out))
322
+ if only_key == "value":
323
+ return out[only_key]
324
+ if isinstance(only_key, str) and only_key in self.enum_lookup:
325
+ return out[only_key]
326
+ return out
327
+ if isinstance(value, list):
328
+ return [self._finalize_export_tree(item) for item in value]
329
+ return value
330
+
331
+ def _round_export_floats(self, value: Any) -> Any:
332
+ """递归圆整浮点数,让导出的 JSON 更适合人工阅读。
333
+
334
+ 参数:
335
+ value (Any): 任意嵌套值(dict / list / 标量)。
336
+
337
+ 返回:
338
+ Any: 结构相同的值,其中所有浮点数被圆整到小数点后 4 位。
339
+ """
340
+ if isinstance(value, dict):
341
+ return {k: self._round_export_floats(v) for k, v in value.items()}
342
+ if isinstance(value, list):
343
+ return [self._round_export_floats(item) for item in value]
344
+ if isinstance(value, float):
345
+ return round(value, 4)
346
+ return value