@josephyan/qingflow-cli 0.2.0-beta.58 → 0.2.0-beta.60

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.
Files changed (35) hide show
  1. package/README.md +3 -2
  2. package/docs/local-agent-install.md +9 -0
  3. package/npm/bin/qingflow.mjs +1 -1
  4. package/npm/lib/runtime.mjs +156 -21
  5. package/package.json +1 -1
  6. package/pyproject.toml +1 -1
  7. package/src/qingflow_mcp/builder_facade/service.py +670 -191
  8. package/src/qingflow_mcp/cli/commands/app.py +16 -16
  9. package/src/qingflow_mcp/cli/commands/auth.py +19 -16
  10. package/src/qingflow_mcp/cli/commands/builder.py +124 -162
  11. package/src/qingflow_mcp/cli/commands/common.py +21 -95
  12. package/src/qingflow_mcp/cli/commands/imports.py +42 -34
  13. package/src/qingflow_mcp/cli/commands/record.py +131 -133
  14. package/src/qingflow_mcp/cli/commands/task.py +43 -44
  15. package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
  16. package/src/qingflow_mcp/cli/context.py +35 -32
  17. package/src/qingflow_mcp/cli/formatters.py +124 -121
  18. package/src/qingflow_mcp/cli/main.py +52 -17
  19. package/src/qingflow_mcp/server_app_builder.py +122 -190
  20. package/src/qingflow_mcp/server_app_user.py +63 -662
  21. package/src/qingflow_mcp/solution/executor.py +63 -4
  22. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  23. package/src/qingflow_mcp/ops/__init__.py +0 -3
  24. package/src/qingflow_mcp/ops/apps.py +0 -64
  25. package/src/qingflow_mcp/ops/auth.py +0 -121
  26. package/src/qingflow_mcp/ops/base.py +0 -290
  27. package/src/qingflow_mcp/ops/builder.py +0 -357
  28. package/src/qingflow_mcp/ops/context.py +0 -120
  29. package/src/qingflow_mcp/ops/directory.py +0 -171
  30. package/src/qingflow_mcp/ops/feedback.py +0 -49
  31. package/src/qingflow_mcp/ops/files.py +0 -78
  32. package/src/qingflow_mcp/ops/imports.py +0 -140
  33. package/src/qingflow_mcp/ops/records.py +0 -415
  34. package/src/qingflow_mcp/ops/tasks.py +0 -171
  35. package/src/qingflow_mcp/ops/workspace.py +0 -76
@@ -1,290 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from typing import Any
5
-
6
- from ..errors import QingflowApiError
7
- from ..json_types import JSONObject
8
-
9
-
10
- _COMMON_KEYS = {
11
- "ok",
12
- "status",
13
- "message",
14
- "error_code",
15
- "details",
16
- "warnings",
17
- "verification",
18
- "profile",
19
- "ws_id",
20
- "request_id",
21
- "request_route",
22
- "output_profile",
23
- "recoverable",
24
- "missing_fields",
25
- "allowed_values",
26
- "suggested_next_call",
27
- "noop",
28
- "compact",
29
- "summary",
30
- }
31
-
32
- _INTERNAL_LEGACY_KEY = "__legacy__"
33
-
34
-
35
- def success_result(
36
- code: str,
37
- message: str,
38
- *,
39
- data: Any = None,
40
- warnings: list[Any] | None = None,
41
- meta: JSONObject | None = None,
42
- legacy: Any = None,
43
- ) -> JSONObject:
44
- payload: JSONObject = {
45
- "ok": True,
46
- "code": code,
47
- "message": message,
48
- "data": data if data is not None else {},
49
- "warnings": list(warnings or []),
50
- "meta": dict(meta or {}),
51
- }
52
- if legacy is not None:
53
- payload[_INTERNAL_LEGACY_KEY] = legacy
54
- return payload
55
-
56
-
57
- def error_result(
58
- code: str,
59
- message: str,
60
- *,
61
- details: Any = None,
62
- suggested_next: Any = None,
63
- meta: JSONObject | None = None,
64
- legacy: Any = None,
65
- ) -> JSONObject:
66
- payload: JSONObject = {
67
- "ok": False,
68
- "code": code,
69
- "message": message,
70
- "details": details if details is not None else {},
71
- "meta": dict(meta or {}),
72
- }
73
- if suggested_next is not None:
74
- payload["suggested_next"] = suggested_next
75
- if legacy is not None:
76
- payload[_INTERNAL_LEGACY_KEY] = legacy
77
- return payload
78
-
79
-
80
- def build_meta(raw: Any, *, extra: JSONObject | None = None) -> JSONObject:
81
- meta: JSONObject = {}
82
- if isinstance(raw, dict):
83
- if raw.get("profile") is not None:
84
- meta["profile"] = raw.get("profile")
85
- if raw.get("ws_id") is not None:
86
- meta["workspace_id"] = raw.get("ws_id")
87
- if raw.get("request_id") is not None:
88
- meta["request_id"] = raw.get("request_id")
89
- if isinstance(raw.get("request_route"), dict):
90
- meta["request_route"] = raw.get("request_route")
91
- if isinstance(raw.get("verification"), dict) and raw.get("verification"):
92
- meta["verification"] = raw.get("verification")
93
- if extra:
94
- meta.update(extra)
95
- return meta
96
-
97
-
98
- def collect_warnings(raw: Any) -> list[Any]:
99
- warnings: list[Any] = []
100
- if isinstance(raw, dict):
101
- if isinstance(raw.get("warnings"), list):
102
- warnings.extend(raw["warnings"])
103
- status = str(raw.get("status") or "").lower()
104
- if status == "partial_success":
105
- warnings.append(
106
- {
107
- "code": "PARTIAL_SUCCESS",
108
- "message": str(raw.get("message") or "operation completed with warnings"),
109
- }
110
- )
111
- return warnings
112
-
113
-
114
- def tool_payload(raw: Any) -> Any:
115
- if not isinstance(raw, dict):
116
- return raw
117
- if "data" in raw:
118
- return raw.get("data")
119
- if "items" in raw:
120
- return {
121
- "items": raw.get("items") if isinstance(raw.get("items"), list) else [],
122
- "count": raw.get("count"),
123
- }
124
- if "page" in raw:
125
- page = raw.get("page")
126
- if isinstance(page, dict):
127
- return {
128
- "items": page.get("list") if isinstance(page.get("list"), list) else [],
129
- "page": page,
130
- }
131
- if "result" in raw:
132
- return raw.get("result")
133
- return {key: value for key, value in raw.items() if key not in _COMMON_KEYS}
134
-
135
-
136
- def is_failed_tool_result(raw: Any) -> bool:
137
- if not isinstance(raw, dict):
138
- return False
139
- if raw.get("ok") is False:
140
- return True
141
- status = str(raw.get("status") or "").lower()
142
- return status in {"failed", "blocked"}
143
-
144
-
145
- def tool_result_to_cli(
146
- raw: Any,
147
- *,
148
- code: str,
149
- message: str,
150
- data: Any = None,
151
- meta: JSONObject | None = None,
152
- ) -> JSONObject:
153
- if is_failed_tool_result(raw):
154
- payload = normalize_tool_error(raw)
155
- payload["meta"] = build_meta(raw, extra=meta)
156
- return payload
157
- return success_result(
158
- code,
159
- message,
160
- data=tool_payload(raw) if data is None else data,
161
- warnings=collect_warnings(raw),
162
- meta=build_meta(raw, extra=meta),
163
- )
164
-
165
-
166
- def normalize_tool_error(raw: Any) -> JSONObject:
167
- if isinstance(raw, dict):
168
- details = raw.get("details")
169
- if not isinstance(details, dict):
170
- details = {}
171
- if raw.get("backend_code") is not None:
172
- details.setdefault("backend_code", raw.get("backend_code"))
173
- if raw.get("request_id") is not None:
174
- details.setdefault("request_id", raw.get("request_id"))
175
- if raw.get("verification") not in (None, {}):
176
- details.setdefault("verification", raw.get("verification"))
177
- return error_result(
178
- str(raw.get("error_code") or raw.get("code") or raw.get("category") or "OPERATION_FAILED"),
179
- str(raw.get("message") or "operation failed"),
180
- details=details,
181
- suggested_next=raw.get("suggested_next_call"),
182
- legacy=raw,
183
- )
184
- return error_result("OPERATION_FAILED", str(raw))
185
-
186
-
187
- def coerce_runtime_error(exc: RuntimeError) -> QingflowApiError:
188
- raw = str(exc)
189
- try:
190
- payload = json.loads(raw)
191
- except json.JSONDecodeError:
192
- return QingflowApiError(category="runtime", message=raw)
193
- if not isinstance(payload, dict):
194
- return QingflowApiError(category="runtime", message=raw)
195
- return QingflowApiError(
196
- category=str(payload.get("category") or "runtime"),
197
- message=str(payload.get("message") or raw),
198
- backend_code=payload.get("backend_code"),
199
- request_id=payload.get("request_id"),
200
- details=payload.get("details") if isinstance(payload.get("details"), dict) else None,
201
- )
202
-
203
-
204
- def normalize_exception(error: Exception) -> JSONObject:
205
- if isinstance(error, RuntimeError):
206
- api_error = coerce_runtime_error(error)
207
- elif isinstance(error, QingflowApiError):
208
- api_error = error
209
- else:
210
- return error_result("RUNTIME_ERROR", str(error))
211
-
212
- details = dict(api_error.details or {})
213
- if api_error.backend_code is not None:
214
- details.setdefault("backend_code", api_error.backend_code)
215
- if api_error.request_id is not None:
216
- details.setdefault("request_id", api_error.request_id)
217
- code = str(details.pop("error_code", None) or api_error.category or "OPERATION_FAILED").upper()
218
- if code == "CONFIG":
219
- code = "INVALID_ARGUMENT"
220
- elif code == "AUTH":
221
- code = "AUTH_REQUIRED"
222
- elif code == "WORKSPACE":
223
- code = "WORKSPACE_NOT_SELECTED"
224
- legacy_payload = None
225
- if isinstance(error, RuntimeError):
226
- raw = str(error)
227
- try:
228
- parsed = json.loads(raw)
229
- except json.JSONDecodeError:
230
- parsed = None
231
- if isinstance(parsed, dict):
232
- legacy_payload = parsed
233
- return error_result(code, api_error.message, details=details, legacy=legacy_payload)
234
-
235
-
236
- def public_result(payload: Any) -> Any:
237
- if isinstance(payload, dict):
238
- return {
239
- key: public_result(value)
240
- for key, value in payload.items()
241
- if key != _INTERNAL_LEGACY_KEY
242
- }
243
- if isinstance(payload, list):
244
- return [public_result(item) for item in payload]
245
- return payload
246
-
247
-
248
- def mcp_result_from_operation(result: Any) -> Any:
249
- if not isinstance(result, dict):
250
- return result
251
- legacy = result.get(_INTERNAL_LEGACY_KEY)
252
- if isinstance(legacy, dict):
253
- return legacy
254
- meta = result.get("meta") if isinstance(result.get("meta"), dict) else {}
255
- if result.get("ok") is False:
256
- payload: JSONObject = {
257
- "ok": False,
258
- "status": "failed",
259
- "error_code": result.get("code") or "OPERATION_FAILED",
260
- "message": result.get("message") or "operation failed",
261
- "details": result.get("details") if isinstance(result.get("details"), dict) else {},
262
- "warnings": result.get("warnings") if isinstance(result.get("warnings"), list) else [],
263
- }
264
- if isinstance(meta.get("request_route"), dict):
265
- payload["request_route"] = meta["request_route"]
266
- if isinstance(meta.get("verification"), dict):
267
- payload["verification"] = meta["verification"]
268
- if meta.get("profile") is not None:
269
- payload["profile"] = meta.get("profile")
270
- if meta.get("workspace_id") is not None:
271
- payload["ws_id"] = meta.get("workspace_id")
272
- if result.get("suggested_next") is not None:
273
- payload["suggested_next_call"] = result.get("suggested_next")
274
- return payload
275
- payload = {
276
- "ok": True,
277
- "status": "success",
278
- "message": result.get("message") or "ok",
279
- "data": result.get("data"),
280
- "warnings": result.get("warnings") if isinstance(result.get("warnings"), list) else [],
281
- }
282
- if isinstance(meta.get("request_route"), dict):
283
- payload["request_route"] = meta["request_route"]
284
- if isinstance(meta.get("verification"), dict):
285
- payload["verification"] = meta["verification"]
286
- if meta.get("profile") is not None:
287
- payload["profile"] = meta.get("profile")
288
- if meta.get("workspace_id") is not None:
289
- payload["ws_id"] = meta.get("workspace_id")
290
- return payload
@@ -1,357 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from .base import normalize_tool_error, success_result, tool_payload
6
-
7
-
8
- class BuilderOperations:
9
- def __init__(self, tools: Any) -> None:
10
- self._tools = tools
11
-
12
- def package_list(self, *, profile: str, trial_status: str) -> dict:
13
- return self._call("PACKAGES_READY", "已读取应用包列表", self._tools.builder.package_list(profile=profile, trial_status=trial_status))
14
-
15
- def package_resolve(self, *, profile: str, package_name: str) -> dict:
16
- return self._call("PACKAGE_RESOLVED", "已解析应用包", self._tools.builder.package_resolve(profile=profile, package_name=package_name))
17
-
18
- def builder_tool_contract(self, *, tool_name: str) -> dict:
19
- return self._call("BUILDER_TOOL_CONTRACT_READY", "已读取工具契约", self._tools.builder.builder_tool_contract(tool_name=tool_name))
20
-
21
- def package_create(self, *, profile: str, package_name: str) -> dict:
22
- return self._call("PACKAGE_CREATED", "已创建应用包", self._tools.builder.package_create(profile=profile, package_name=package_name))
23
-
24
- def member_search(
25
- self,
26
- *,
27
- profile: str,
28
- query: str,
29
- page_num: int,
30
- page_size: int,
31
- contain_disable: bool,
32
- ) -> dict:
33
- return self._call(
34
- "MEMBERS_READY",
35
- "已读取成员候选",
36
- self._tools.builder.member_search(
37
- profile=profile,
38
- query=query,
39
- page_num=page_num,
40
- page_size=page_size,
41
- contain_disable=contain_disable,
42
- ),
43
- )
44
-
45
- def role_search(self, *, profile: str, keyword: str, page_num: int, page_size: int) -> dict:
46
- return self._call(
47
- "ROLES_READY",
48
- "已读取角色候选",
49
- self._tools.builder.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size),
50
- )
51
-
52
- def role_create(
53
- self,
54
- *,
55
- profile: str,
56
- role_name: str,
57
- member_uids: list[int],
58
- member_emails: list[str],
59
- member_names: list[str],
60
- role_icon: str,
61
- ) -> dict:
62
- return self._call(
63
- "ROLE_CREATED",
64
- "已创建角色",
65
- self._tools.builder.role_create(
66
- profile=profile,
67
- role_name=role_name,
68
- member_uids=member_uids,
69
- member_emails=member_emails,
70
- member_names=member_names,
71
- role_icon=role_icon,
72
- ),
73
- )
74
-
75
- def package_attach_app(self, *, profile: str, tag_id: int, app_key: str, app_title: str) -> dict:
76
- return self._call(
77
- "PACKAGE_APP_ATTACHED",
78
- "已挂载应用到应用包",
79
- self._tools.builder.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title),
80
- )
81
-
82
- def release_edit_lock_if_mine(
83
- self,
84
- *,
85
- profile: str,
86
- app_key: str,
87
- lock_owner_email: str,
88
- lock_owner_name: str,
89
- ) -> dict:
90
- return self._call(
91
- "APP_EDIT_LOCK_RELEASED",
92
- "已释放编辑锁",
93
- self._tools.builder.app_release_edit_lock_if_mine(
94
- profile=profile,
95
- app_key=app_key,
96
- lock_owner_email=lock_owner_email,
97
- lock_owner_name=lock_owner_name,
98
- ),
99
- )
100
-
101
- def app_resolve(self, *, profile: str, app_key: str, app_name: str, package_tag_id: int | None) -> dict:
102
- return self._call(
103
- "APP_RESOLVED",
104
- "已解析应用",
105
- self._tools.builder.app_resolve(
106
- profile=profile,
107
- app_key=app_key,
108
- app_name=app_name,
109
- package_tag_id=package_tag_id,
110
- ),
111
- )
112
-
113
- def app_show(self, *, profile: str, app_key: str) -> dict:
114
- return self._call("APP_SUMMARY_READY", "已读取应用摘要", self._tools.builder.app_read_summary(profile=profile, app_key=app_key))
115
-
116
- def fields_show(self, *, profile: str, app_key: str) -> dict:
117
- return self._call("FIELDS_READY", "已读取字段摘要", self._tools.builder.app_read_fields(profile=profile, app_key=app_key))
118
-
119
- def layout_show(self, *, profile: str, app_key: str) -> dict:
120
- return self._call("LAYOUT_READY", "已读取布局摘要", self._tools.builder.app_read_layout_summary(profile=profile, app_key=app_key))
121
-
122
- def views_show(self, *, profile: str, app_key: str) -> dict:
123
- return self._call("VIEWS_READY", "已读取视图摘要", self._tools.builder.app_read_views_summary(profile=profile, app_key=app_key))
124
-
125
- def flow_show(self, *, profile: str, app_key: str) -> dict:
126
- return self._call("FLOW_READY", "已读取流程摘要", self._tools.builder.app_read_flow_summary(profile=profile, app_key=app_key))
127
-
128
- def charts_show(self, *, profile: str, app_key: str) -> dict:
129
- return self._call("CHARTS_READY", "已读取报表摘要", self._tools.builder.app_read_charts_summary(profile=profile, app_key=app_key))
130
-
131
- def portal_show(self, *, profile: str, dash_key: str, being_draft: bool) -> dict:
132
- return self._call(
133
- "PORTAL_READY",
134
- "已读取门户摘要",
135
- self._tools.builder.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft),
136
- )
137
-
138
- def fields_apply(
139
- self,
140
- *,
141
- profile: str,
142
- app_key: str,
143
- package_tag_id: int | None,
144
- app_name: str,
145
- app_title: str,
146
- create_if_missing: bool,
147
- publish: bool,
148
- add_fields: list[Any],
149
- update_fields: list[Any],
150
- remove_fields: list[Any],
151
- ) -> dict:
152
- return self._call(
153
- "FIELDS_APPLIED",
154
- "已执行字段变更",
155
- self._tools.builder.app_schema_apply(
156
- profile=profile,
157
- app_key=app_key,
158
- package_tag_id=package_tag_id,
159
- app_name=app_name,
160
- app_title=app_title,
161
- create_if_missing=create_if_missing,
162
- publish=publish,
163
- add_fields=[_normalize_field_spec(item) for item in add_fields],
164
- update_fields=[_normalize_field_update(item) for item in update_fields],
165
- remove_fields=[_normalize_field_selector(item) for item in remove_fields],
166
- ),
167
- )
168
-
169
- def layout_apply(
170
- self,
171
- *,
172
- profile: str,
173
- app_key: str,
174
- mode: str,
175
- publish: bool,
176
- sections: list[Any],
177
- ) -> dict:
178
- return self._call(
179
- "LAYOUT_APPLIED",
180
- "已执行布局变更",
181
- self._tools.builder.app_layout_apply(
182
- profile=profile,
183
- app_key=app_key,
184
- mode=mode,
185
- publish=publish,
186
- sections=sections,
187
- ),
188
- )
189
-
190
- def flow_apply(
191
- self,
192
- *,
193
- profile: str,
194
- app_key: str,
195
- mode: str,
196
- publish: bool,
197
- nodes: list[Any],
198
- transitions: list[Any],
199
- ) -> dict:
200
- return self._call(
201
- "FLOW_APPLIED",
202
- "已执行流程变更",
203
- self._tools.builder.app_flow_apply(
204
- profile=profile,
205
- app_key=app_key,
206
- mode=mode,
207
- publish=publish,
208
- nodes=nodes,
209
- transitions=transitions,
210
- ),
211
- )
212
-
213
- def views_apply(
214
- self,
215
- *,
216
- profile: str,
217
- app_key: str,
218
- publish: bool,
219
- upsert_views: list[Any],
220
- remove_views: list[Any],
221
- ) -> dict:
222
- return self._call(
223
- "VIEWS_APPLIED",
224
- "已执行视图变更",
225
- self._tools.builder.app_views_apply(
226
- profile=profile,
227
- app_key=app_key,
228
- publish=publish,
229
- upsert_views=upsert_views,
230
- remove_views=remove_views,
231
- ),
232
- )
233
-
234
- def charts_apply(
235
- self,
236
- *,
237
- profile: str,
238
- app_key: str,
239
- upsert_charts: list[Any],
240
- remove_chart_ids: list[Any],
241
- reorder_chart_ids: list[Any],
242
- ) -> dict:
243
- return self._call(
244
- "CHARTS_APPLIED",
245
- "已执行报表变更",
246
- self._tools.builder.app_charts_apply(
247
- profile=profile,
248
- app_key=app_key,
249
- upsert_charts=upsert_charts,
250
- remove_chart_ids=remove_chart_ids,
251
- reorder_chart_ids=reorder_chart_ids,
252
- ),
253
- )
254
-
255
- def portal_apply(
256
- self,
257
- *,
258
- profile: str,
259
- dash_key: str,
260
- dash_name: str,
261
- package_tag_id: int | None,
262
- publish: bool,
263
- sections: list[Any],
264
- auth: dict | None,
265
- icon: str | None,
266
- color: str | None,
267
- hide_copyright: bool | None,
268
- dash_global_config: dict | None,
269
- config: dict | None,
270
- ) -> dict:
271
- return self._call(
272
- "PORTAL_APPLIED",
273
- "已执行门户变更",
274
- self._tools.builder.portal_apply(
275
- profile=profile,
276
- dash_key=dash_key,
277
- dash_name=dash_name,
278
- package_tag_id=package_tag_id,
279
- publish=publish,
280
- sections=sections,
281
- auth=auth,
282
- icon=icon,
283
- color=color,
284
- hide_copyright=hide_copyright,
285
- dash_global_config=dash_global_config,
286
- config=config,
287
- ),
288
- )
289
-
290
- def publish_verify(self, *, profile: str, app_key: str, expected_package_tag_id: int | None) -> dict:
291
- return self._call(
292
- "PUBLISH_VERIFIED",
293
- "已完成发布校验",
294
- self._tools.builder.app_publish_verify(
295
- profile=profile,
296
- app_key=app_key,
297
- expected_package_tag_id=expected_package_tag_id,
298
- ),
299
- )
300
-
301
- def _call(self, code: str, message: str, raw: Any) -> dict:
302
- if isinstance(raw, dict) and (raw.get("ok") is False or str(raw.get("status") or "").lower() in {"failed", "blocked"}):
303
- return normalize_tool_error(raw)
304
- warnings = raw.get("warnings") if isinstance(raw, dict) and isinstance(raw.get("warnings"), list) else []
305
- meta = {}
306
- if isinstance(raw, dict):
307
- if raw.get("profile") is not None:
308
- meta["profile"] = raw.get("profile")
309
- if raw.get("request_id") is not None:
310
- meta["request_id"] = raw.get("request_id")
311
- if raw.get("request_route") is not None:
312
- meta["request_route"] = raw.get("request_route")
313
- if raw.get("verification") not in (None, {}):
314
- meta["verification"] = raw.get("verification")
315
- data = tool_payload(raw)
316
- if isinstance(data, dict):
317
- normalized_args = data.pop("normalized_args", None)
318
- verified = data.pop("verified", None)
319
- if normalized_args is not None:
320
- meta["normalized_args"] = normalized_args
321
- if verified is not None:
322
- meta["verified"] = verified
323
- return success_result(code, message, data=data, warnings=warnings, meta=meta, legacy=raw)
324
-
325
-
326
- def _normalize_field_spec(value: Any) -> Any:
327
- if not isinstance(value, dict):
328
- return value
329
- normalized = dict(value)
330
- if "name" not in normalized and isinstance(normalized.get("field_name"), str):
331
- normalized["name"] = normalized["field_name"]
332
- if "type" not in normalized and isinstance(normalized.get("field_type"), str):
333
- normalized["type"] = normalized["field_type"]
334
- normalized.pop("field_name", None)
335
- normalized.pop("field_type", None)
336
- return normalized
337
-
338
-
339
- def _normalize_field_selector(value: Any) -> Any:
340
- if not isinstance(value, dict):
341
- return value
342
- normalized = dict(value)
343
- if "name" not in normalized and isinstance(normalized.get("field_name"), str):
344
- normalized["name"] = normalized["field_name"]
345
- normalized.pop("field_name", None)
346
- return normalized
347
-
348
-
349
- def _normalize_field_update(value: Any) -> Any:
350
- if not isinstance(value, dict):
351
- return value
352
- normalized = dict(value)
353
- if isinstance(normalized.get("selector"), dict):
354
- normalized["selector"] = _normalize_field_selector(normalized["selector"])
355
- if isinstance(normalized.get("set"), dict):
356
- normalized["set"] = _normalize_field_spec(normalized["set"])
357
- return normalized