@josephyan/qingflow-cli 0.2.0-beta.1000

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 (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
@@ -0,0 +1,841 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from copy import deepcopy
5
+ from functools import wraps
6
+ from typing import Any, Callable
7
+
8
+ from .public_surface import (
9
+ BUILDER_DOMAIN,
10
+ USER_DOMAIN,
11
+ cli_trim_key_from_namespace,
12
+ server_method_map,
13
+ tool_key,
14
+ )
15
+
16
+
17
+ JSONObject = dict[str, Any]
18
+ TransformFn = Callable[[JSONObject], None]
19
+
20
+
21
+ COMMON_SUCCESS_DROP_TOP = {
22
+ "request_route",
23
+ "profile",
24
+ "ws_id",
25
+ "output_profile",
26
+ "qf_version_source",
27
+ "persisted",
28
+ "base_url",
29
+ "normalized_args",
30
+ "suggested_next_call",
31
+ "noop",
32
+ "allowed_values",
33
+ "missing_fields",
34
+ "ok",
35
+ }
36
+
37
+ COMMON_ERROR_DROP_TOP = {
38
+ "request_route",
39
+ "base_url",
40
+ "normalized_args",
41
+ "suggested_next_call",
42
+ "profile",
43
+ "ws_id",
44
+ "output_profile",
45
+ "qf_version_source",
46
+ "persisted",
47
+ "allowed_values",
48
+ "missing_fields",
49
+ "noop",
50
+ "ok",
51
+ }
52
+
53
+ SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
54
+
55
+
56
+ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
57
+ if not isinstance(payload, dict):
58
+ return payload
59
+ if _looks_like_failure_payload(payload):
60
+ return _trim_returned_failure(payload)
61
+ return trim_success_response(tool_name, payload)
62
+
63
+
64
+ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
65
+ if not isinstance(payload, dict):
66
+ return payload
67
+ trimmed = deepcopy(payload)
68
+ _drop_top_keys(trimmed, COMMON_SUCCESS_DROP_TOP)
69
+ transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
70
+ if transformer is not None:
71
+ transformer(trimmed)
72
+ _drop_empty_optional_keys(trimmed)
73
+ return trimmed
74
+
75
+
76
+ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
77
+ if not isinstance(payload, dict):
78
+ return payload
79
+ trimmed = deepcopy(payload)
80
+ _drop_top_keys(trimmed, COMMON_ERROR_DROP_TOP)
81
+ _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
82
+ details = trimmed.get("details")
83
+ if isinstance(details, dict):
84
+ compact_details = _compact_scalar_dict(details)
85
+ if compact_details:
86
+ trimmed["details"] = compact_details
87
+ else:
88
+ trimmed.pop("details", None)
89
+ if trimmed.get("backend_code") is not None:
90
+ trimmed.pop("http_status", None)
91
+ _drop_empty_optional_keys(trimmed)
92
+ return trimmed
93
+
94
+
95
+ def wrap_trimmed_methods(instance: object, method_map: dict[str, str]) -> object:
96
+ for method_name, tool_name in method_map.items():
97
+ original = getattr(instance, method_name, None)
98
+ if original is None or not callable(original):
99
+ continue
100
+ setattr(instance, method_name, _wrap_callable(original, tool_name))
101
+ return instance
102
+
103
+
104
+ def resolve_cli_tool_name(args: Any) -> str | None:
105
+ return cli_trim_key_from_namespace(args)
106
+
107
+
108
+ USER_SERVER_METHOD_MAP = server_method_map(USER_DOMAIN)
109
+ BUILDER_SERVER_METHOD_MAP = server_method_map(BUILDER_DOMAIN)
110
+
111
+
112
+ def _wrap_callable(original: Callable[..., Any], tool_name: str) -> Callable[..., Any]:
113
+ @wraps(original)
114
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
115
+ try:
116
+ result = original(*args, **kwargs)
117
+ except RuntimeError as exc:
118
+ payload = _parse_runtime_error_payload(exc)
119
+ if payload is None:
120
+ raise
121
+ raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
122
+ return trim_public_response(tool_name, result) if isinstance(result, dict) else result
123
+
124
+ return wrapped
125
+
126
+
127
+ def _parse_runtime_error_payload(exc: RuntimeError) -> dict[str, Any] | None:
128
+ raw = str(exc)
129
+ try:
130
+ payload = json.loads(raw)
131
+ except json.JSONDecodeError:
132
+ return None
133
+ return payload if isinstance(payload, dict) else None
134
+
135
+
136
+ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
137
+ if payload.get("ok") is False:
138
+ return True
139
+ status = str(payload.get("status") or "").lower()
140
+ return status in {"failed", "blocked", "verification_failed"}
141
+
142
+
143
+ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
144
+ trimmed = deepcopy(payload)
145
+ _drop_top_keys(trimmed, COMMON_ERROR_DROP_TOP)
146
+ _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
147
+ details = trimmed.get("details")
148
+ if isinstance(details, dict):
149
+ compact_details = _compact_scalar_dict(details)
150
+ if compact_details:
151
+ trimmed["details"] = compact_details
152
+ else:
153
+ trimmed.pop("details", None)
154
+ if trimmed.get("backend_code") is not None:
155
+ trimmed.pop("http_status", None)
156
+ _drop_empty_optional_keys(trimmed)
157
+ return trimmed
158
+
159
+
160
+ def _drop_top_keys(payload: JSONObject, keys: set[str]) -> None:
161
+ for key in keys:
162
+ payload.pop(key, None)
163
+
164
+
165
+ def _drop_empty_optional_keys(payload: JSONObject) -> None:
166
+ for key in ("warnings", "verification", "details"):
167
+ value = payload.get(key)
168
+ if value in (None, [], {}, ""):
169
+ payload.pop(key, None)
170
+
171
+
172
+ def _compact_scalar_dict(payload: dict[str, Any]) -> dict[str, Any]:
173
+ compact: dict[str, Any] = {}
174
+ for key, value in payload.items():
175
+ if isinstance(value, (str, int, float, bool)) or value is None:
176
+ compact[key] = value
177
+ continue
178
+ if isinstance(value, dict):
179
+ nested = {
180
+ nested_key: nested_value
181
+ for nested_key, nested_value in value.items()
182
+ if isinstance(nested_value, (str, int, float, bool)) or nested_value is None
183
+ }
184
+ if nested:
185
+ compact[key] = nested
186
+ return compact
187
+
188
+
189
+ def _trim_item_list(payload: JSONObject, key: str, *, allowed: tuple[str, ...]) -> None:
190
+ items = payload.get(key)
191
+ if not isinstance(items, list):
192
+ return
193
+ payload[key] = [_pick(item, allowed) for item in items if isinstance(item, dict)]
194
+
195
+
196
+ def _trim_nested_item_list(payload: JSONObject, path: tuple[str, ...], *, allowed: tuple[str, ...]) -> None:
197
+ parent = _nested_dict(payload, path[:-1])
198
+ if not isinstance(parent, dict):
199
+ return
200
+ key = path[-1]
201
+ items = parent.get(key)
202
+ if not isinstance(items, list):
203
+ return
204
+ parent[key] = [_pick(item, allowed) for item in items if isinstance(item, dict)]
205
+
206
+
207
+ def _keep_nested_keys(payload: JSONObject, path: tuple[str, ...], *, allowed: tuple[str, ...]) -> None:
208
+ parent = _nested_dict(payload, path[:-1])
209
+ if not isinstance(parent, dict):
210
+ return
211
+ node = parent.get(path[-1])
212
+ if isinstance(node, dict):
213
+ parent[path[-1]] = _pick(node, allowed)
214
+
215
+
216
+ def _drop_nested_keys(payload: JSONObject, path: tuple[str, ...], *, keys: tuple[str, ...]) -> None:
217
+ parent = _nested_dict(payload, path[:-1])
218
+ if not isinstance(parent, dict):
219
+ return
220
+ node = parent.get(path[-1])
221
+ if not isinstance(node, dict):
222
+ return
223
+ for key in keys:
224
+ node.pop(key, None)
225
+
226
+
227
+ def _drop_deep_keys(payload: Any, keys: set[str]) -> None:
228
+ if isinstance(payload, dict):
229
+ for key in list(payload.keys()):
230
+ if key in keys:
231
+ payload.pop(key, None)
232
+ continue
233
+ _drop_deep_keys(payload.get(key), keys)
234
+ elif isinstance(payload, list):
235
+ for item in payload:
236
+ _drop_deep_keys(item, keys)
237
+
238
+
239
+ def _nested_dict(payload: JSONObject, path: tuple[str, ...]) -> JSONObject | None:
240
+ node: Any = payload
241
+ for key in path:
242
+ if not isinstance(node, dict):
243
+ return None
244
+ node = node.get(key)
245
+ return node if isinstance(node, dict) else None
246
+
247
+
248
+ def _pick(payload: JSONObject, allowed: tuple[str, ...]) -> JSONObject:
249
+ return {key: payload.get(key) for key in allowed if key in payload}
250
+
251
+
252
+ def _trim_auth_payload(payload: JSONObject) -> None:
253
+ pass
254
+
255
+
256
+ def _trim_auth_logout(payload: JSONObject) -> None:
257
+ pass
258
+
259
+
260
+ def _trim_workspace_list(payload: JSONObject) -> None:
261
+ page = payload.get("page")
262
+ if isinstance(page, dict):
263
+ _trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
264
+
265
+
266
+ def _trim_workspace_get(payload: JSONObject) -> None:
267
+ workspace = payload.get("workspace")
268
+ if isinstance(workspace, dict):
269
+ payload["workspace"] = _pick(
270
+ workspace,
271
+ allowed=("wsId", "workspaceName", "remark", "systemVersion", "auth"),
272
+ )
273
+
274
+
275
+ def _trim_app_search_like(payload: JSONObject) -> None:
276
+ payload.pop("apps", None)
277
+ _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
278
+
279
+
280
+ def _trim_app_get(payload: JSONObject) -> None:
281
+ _trim_builder_envelope(payload)
282
+ _trim_nested_item_list(
283
+ payload,
284
+ ("data", "accessible_views"),
285
+ allowed=("view_id", "name", "analysis_supported", "list_supported", "kind"),
286
+ )
287
+
288
+
289
+ def _trim_file_upload_info(payload: JSONObject) -> None:
290
+ pass
291
+
292
+
293
+ def _trim_file_upload_local(payload: JSONObject) -> None:
294
+ payload.pop("result", None)
295
+ payload.pop("upload_result", None)
296
+
297
+
298
+ def _trim_import_schema(payload: JSONObject) -> None:
299
+ columns: list[JSONObject] | None = None
300
+ if isinstance(payload.get("columns"), list):
301
+ columns = [item for item in payload.get("columns", []) if isinstance(item, dict)]
302
+ elif isinstance(payload.get("expected_columns"), list):
303
+ columns = [item for item in payload.get("expected_columns", []) if isinstance(item, dict)]
304
+ if columns is not None:
305
+ payload["columns"] = [_compact_import_column(item) for item in columns]
306
+ payload.pop("expected_columns", None)
307
+ payload.pop("schema_fingerprint", None)
308
+ payload.pop("import_capability", None)
309
+ payload.pop("request_route", None)
310
+ payload.pop("verification", None)
311
+
312
+ if _looks_like_import_verify(payload):
313
+ _trim_import_verify_payload(payload)
314
+ return
315
+ if "applied_repairs" in payload or "repaired_file_path" in payload:
316
+ _trim_import_repair_payload(payload)
317
+ return
318
+ if "template_url" in payload or "downloaded_to_path" in payload:
319
+ _trim_import_template_payload(payload)
320
+ return
321
+ if "import_id" in payload or "process_id_str" in payload:
322
+ _trim_import_status_payload(payload)
323
+ return
324
+
325
+
326
+ def _trim_record_schema(payload: JSONObject) -> None:
327
+ payload.pop("legacy_schema", None)
328
+ template_map = payload.get("payload_template")
329
+ if not isinstance(template_map, dict):
330
+ template_map = None
331
+
332
+ if "writable_fields" in payload:
333
+ writable_fields = payload.get("writable_fields")
334
+ payload.pop("writable_fields", None)
335
+ required_fields: list[JSONObject] = []
336
+ optional_fields: list[JSONObject] = []
337
+ if isinstance(writable_fields, list):
338
+ for item in writable_fields:
339
+ compact = _compact_schema_field(item, template_map=template_map)
340
+ if not compact:
341
+ continue
342
+ if compact.get("required") is True:
343
+ required_fields.append(compact)
344
+ else:
345
+ optional_fields.append(compact)
346
+ payload["required_fields"] = required_fields
347
+ payload["optional_fields"] = optional_fields
348
+
349
+ for key in ("required_fields", "optional_fields", "runtime_linked_required_fields", "fields", "ambiguous_fields"):
350
+ if key in payload:
351
+ payload[key] = _compact_schema_fields(payload.get(key), template_map=template_map)
352
+
353
+ for key in ("suggested_dimensions", "suggested_metrics", "suggested_time_fields"):
354
+ if isinstance(payload.get(key), list):
355
+ payload[key] = [
356
+ _pick(item, ("field_id", "title")) for item in payload.get(key) if isinstance(item, dict)
357
+ ]
358
+
359
+ for key in ("workflow_node", "view_resolution", "field_count"):
360
+ payload.pop(key, None)
361
+
362
+
363
+ def _trim_record_write(payload: JSONObject) -> None:
364
+ payload.pop("verification", None)
365
+ data = payload.get("data")
366
+ if not isinstance(data, dict):
367
+ return
368
+ data.pop("debug", None)
369
+ data.pop("normalized_payload", None)
370
+ data.pop("human_review", None)
371
+ data.pop("action", None)
372
+ resource = _compact_record_resource(data.get("resource"))
373
+ if resource:
374
+ data["resource"] = resource
375
+ else:
376
+ data.pop("resource", None)
377
+ verification = data.get("verification")
378
+ if isinstance(verification, dict):
379
+ compact_verification = _pick(
380
+ verification,
381
+ (
382
+ "verified",
383
+ "verification_mode",
384
+ "field_level_verified",
385
+ ),
386
+ )
387
+ if compact_verification:
388
+ data["verification"] = compact_verification
389
+ else:
390
+ data.pop("verification", None)
391
+ for key in ("blockers", "field_errors", "confirmation_requests", "resolved_fields"):
392
+ value = data.get(key)
393
+ if value in (None, [], {}, ""):
394
+ data.pop(key, None)
395
+
396
+
397
+ def _trim_record_get(payload: JSONObject) -> None:
398
+ data = payload.get("data")
399
+ if not isinstance(data, dict):
400
+ return
401
+ compact: dict[str, Any] = {}
402
+ app_key = data.get("app_key")
403
+ if app_key:
404
+ compact["app_key"] = app_key
405
+ record_id = data.get("record_id")
406
+ if record_id not in (None, ""):
407
+ compact["record_id"] = str(record_id)
408
+ record = data.get("record")
409
+ if isinstance(record, dict):
410
+ compact["record"] = record
411
+ normalized_record = data.get("normalized_record")
412
+ if isinstance(normalized_record, dict):
413
+ compact["normalized_record"] = normalized_record
414
+ normalized_ambiguous_fields = data.get("normalized_ambiguous_fields")
415
+ if isinstance(normalized_ambiguous_fields, dict):
416
+ compact["normalized_ambiguous_fields"] = normalized_ambiguous_fields
417
+ payload["data"] = compact
418
+
419
+
420
+ def _trim_record_list(payload: JSONObject) -> None:
421
+ data = payload.get("data")
422
+ if not isinstance(data, dict):
423
+ return
424
+ pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
425
+ returned_items = pagination.get("returned_items")
426
+ result_amount = pagination.get("result_amount")
427
+ limit = pagination.get("limit")
428
+ truncated = False
429
+ if isinstance(result_amount, int) and isinstance(returned_items, int):
430
+ truncated = result_amount > returned_items
431
+ compact_pagination = {
432
+ "loaded": True,
433
+ "page_size": limit,
434
+ "fetched_pages": 1,
435
+ "reported_total": result_amount,
436
+ "truncated": truncated,
437
+ }
438
+ selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
439
+ view = selection.get("view") if isinstance(selection.get("view"), dict) else {}
440
+ compact: dict[str, Any] = {
441
+ "app_key": data.get("app_key"),
442
+ "items": data.get("items") if isinstance(data.get("items"), list) else [],
443
+ "pagination": compact_pagination,
444
+ }
445
+ if view:
446
+ compact["view"] = _pick(view, ("view_id", "name"))
447
+ payload["data"] = compact
448
+
449
+
450
+ def _trim_record_analyze(payload: JSONObject) -> None:
451
+ summary: dict[str, Any] = {}
452
+ completeness = payload.get("completeness")
453
+ if isinstance(completeness, dict):
454
+ summary["completeness"] = completeness
455
+ presentation = payload.get("presentation")
456
+ if isinstance(presentation, dict):
457
+ summary["presentation"] = presentation
458
+ ranking = payload.get("ranking")
459
+ if isinstance(ranking, dict):
460
+ summary["ranking"] = ranking
461
+ error = payload.get("error")
462
+ if isinstance(error, dict):
463
+ summary["error"] = error
464
+ if summary:
465
+ payload["summary"] = summary
466
+ for key in ("query", "ranking", "ratios", "completeness", "presentation", "error", "debug"):
467
+ payload.pop(key, None)
468
+
469
+
470
+ def _trim_code_block_schema(payload: JSONObject) -> None:
471
+ payload.pop("legacy_schema", None)
472
+ _trim_nested_item_list(payload, ("code_block_fields",), allowed=("title", "selector", "bound_output_fields", "configured_aliases"))
473
+
474
+
475
+ def _trim_code_block_run(payload: JSONObject) -> None:
476
+ _drop_deep_keys(payload, {"debug", "request_route"})
477
+
478
+
479
+ def _trim_task_list(payload: JSONObject) -> None:
480
+ _drop_nested_keys(payload, ("data", "selection"), keys=("query",))
481
+
482
+
483
+ def _trim_task_get(payload: JSONObject) -> None:
484
+ _drop_deep_keys(payload, {"request_route", "output_profile"})
485
+
486
+
487
+ def _trim_task_context_detail(payload: JSONObject) -> None:
488
+ _drop_deep_keys(payload, {"request_route", "output_profile"})
489
+
490
+
491
+ def _trim_record_delete(payload: JSONObject) -> None:
492
+ data = payload.get("data")
493
+ if not isinstance(data, dict):
494
+ return
495
+ resource = data.get("resource")
496
+ deleted_ids: list[str] = []
497
+ if isinstance(resource, dict):
498
+ raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
499
+ if isinstance(raw_ids, list):
500
+ deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
501
+ data["deleted_ids"] = deleted_ids
502
+ data.setdefault("failed_ids", [])
503
+ for key in (
504
+ "resource",
505
+ "action",
506
+ "normalized_payload",
507
+ "human_review",
508
+ "verification",
509
+ "blockers",
510
+ "field_errors",
511
+ "confirmation_requests",
512
+ "resolved_fields",
513
+ "debug",
514
+ ):
515
+ data.pop(key, None)
516
+
517
+
518
+ def _compact_record_resource(resource: Any) -> dict[str, Any] | None:
519
+ if not isinstance(resource, dict):
520
+ return None
521
+ compact: dict[str, Any] = {}
522
+ if resource.get("type") not in (None, ""):
523
+ compact["type"] = resource.get("type")
524
+ app_key = resource.get("app_key") or resource.get("appKey")
525
+ if app_key not in (None, ""):
526
+ compact["app_key"] = app_key
527
+ record_id = resource.get("record_id")
528
+ if record_id not in (None, ""):
529
+ compact["record_id"] = str(record_id)
530
+ apply_id = resource.get("apply_id") or resource.get("applyId")
531
+ if apply_id not in (None, "") and "record_id" not in compact:
532
+ compact["record_id"] = str(apply_id)
533
+ record_ids = resource.get("record_ids")
534
+ if isinstance(record_ids, list):
535
+ compact["record_ids"] = [str(item) for item in record_ids if item not in (None, "")]
536
+ apply_ids = resource.get("apply_ids") or resource.get("applyIds")
537
+ if isinstance(apply_ids, list) and "record_ids" not in compact:
538
+ compact["record_ids"] = [str(item) for item in apply_ids if item not in (None, "")]
539
+ return compact or None
540
+
541
+
542
+ def _compact_schema_fields(items: Any, *, template_map: dict[str, Any] | None) -> list[JSONObject]:
543
+ if not isinstance(items, list):
544
+ return []
545
+ compacted: list[JSONObject] = []
546
+ for item in items:
547
+ compact = _compact_schema_field(item, template_map=template_map)
548
+ if compact:
549
+ compacted.append(compact)
550
+ return compacted
551
+
552
+
553
+ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) -> JSONObject | None:
554
+ if not isinstance(item, dict):
555
+ return None
556
+ compact: dict[str, Any] = {}
557
+ field_id = item.get("field_id")
558
+ if field_id not in (None, ""):
559
+ compact["field_id"] = field_id
560
+ title = item.get("title")
561
+ if title not in (None, ""):
562
+ compact["title"] = title
563
+ kind = item.get("kind") or item.get("write_kind")
564
+ if kind not in (None, ""):
565
+ compact["kind"] = kind
566
+ if "required" in item:
567
+ compact["required"] = bool(item.get("required"))
568
+ if template_map is not None and isinstance(title, str) and title in template_map:
569
+ compact["template"] = template_map.get(title)
570
+ candidate_hint = item.get("candidate_hint")
571
+ if isinstance(candidate_hint, dict):
572
+ compact["candidate_hint"] = candidate_hint
573
+ options = item.get("options")
574
+ if isinstance(options, list) and options:
575
+ compact["options"] = options
576
+ target_app_key = item.get("target_app_key")
577
+ if isinstance(target_app_key, str) and target_app_key:
578
+ compact["target_app_key"] = target_app_key
579
+ searchable_fields = item.get("searchable_fields")
580
+ if isinstance(searchable_fields, list) and searchable_fields:
581
+ compact["searchable_fields"] = searchable_fields
582
+ row_fields = item.get("row_fields")
583
+ if isinstance(row_fields, list) and row_fields:
584
+ compact["row_fields"] = _compact_schema_fields(row_fields, template_map=None)
585
+ return compact or None
586
+
587
+
588
+ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
589
+ compact: dict[str, Any] = {}
590
+ title = item.get("title")
591
+ if title not in (None, ""):
592
+ compact["title"] = title
593
+ kind = item.get("kind") or item.get("write_kind")
594
+ if kind not in (None, ""):
595
+ compact["kind"] = kind
596
+ compact["required"] = bool(item.get("required"))
597
+ options = item.get("options")
598
+ if isinstance(options, list) and options:
599
+ compact["options"] = options
600
+ if bool(item.get("accepts_natural_input")):
601
+ compact["accepts_natural_input"] = True
602
+ if bool(item.get("requires_upload")):
603
+ compact["requires_upload"] = True
604
+ target_app_key = item.get("target_app_key")
605
+ if isinstance(target_app_key, str) and target_app_key:
606
+ compact["target_app_key"] = target_app_key
607
+ target_app_name = item.get("target_app_name")
608
+ if isinstance(target_app_name, str) and target_app_name:
609
+ compact["target_app_name"] = target_app_name
610
+ searchable_fields = item.get("searchable_fields")
611
+ if isinstance(searchable_fields, list) and searchable_fields:
612
+ compact["searchable_fields"] = searchable_fields
613
+ return compact
614
+
615
+
616
+ def _looks_like_import_verify(payload: JSONObject) -> bool:
617
+ return "verification_id" in payload and "can_import" in payload
618
+
619
+
620
+ def _trim_import_verify_payload(payload: JSONObject) -> None:
621
+ issues = payload.get("issues") if isinstance(payload.get("issues"), list) else []
622
+ issue_summary = _summarize_import_issues(issues)
623
+ payload["issue_summary"] = issue_summary
624
+ columns = payload.get("columns")
625
+ if "expected_columns" not in payload and isinstance(columns, list):
626
+ payload["expected_columns"] = columns
627
+ file_name = payload.get("file_name")
628
+ if not file_name:
629
+ file_path = payload.get("file_path")
630
+ if isinstance(file_path, str) and file_path:
631
+ payload["file_name"] = file_path.split("/")[-1]
632
+ for key in ("apply_rows", "schema_fingerprint", "import_capability", "file_sha256", "verified_file_sha256", "file_format", "local_precheck_limited"):
633
+ payload.pop(key, None)
634
+
635
+
636
+ def _trim_import_repair_payload(payload: JSONObject) -> None:
637
+ payload["verification_id"] = payload.get("new_verification_id") or payload.get("verification_id")
638
+ post_repair_issues = payload.get("post_repair_issues")
639
+ if isinstance(post_repair_issues, list):
640
+ payload["post_repair_issue_summary"] = _summarize_import_issues(post_repair_issues)
641
+ for key in ("new_verification_id", "verification"):
642
+ payload.pop(key, None)
643
+
644
+
645
+ def _trim_import_template_payload(payload: JSONObject) -> None:
646
+ for key in ("schema_fingerprint", "verification"):
647
+ payload.pop(key, None)
648
+
649
+
650
+ def _trim_import_status_payload(payload: JSONObject) -> None:
651
+ total_rows = payload.get("total_rows")
652
+ success_rows = payload.get("success_rows")
653
+ failed_rows = payload.get("failed_rows")
654
+ payload["total"] = total_rows
655
+ if isinstance(success_rows, int) and isinstance(failed_rows, int):
656
+ payload["finished"] = success_rows + failed_rows
657
+ elif isinstance(success_rows, int):
658
+ payload["finished"] = success_rows
659
+ else:
660
+ payload["finished"] = None
661
+ payload["succeeded"] = success_rows
662
+ payload["failed"] = failed_rows
663
+ for key in (
664
+ "matched_by",
665
+ "source_file_name",
666
+ "total_rows",
667
+ "success_rows",
668
+ "failed_rows",
669
+ "error_file_urls",
670
+ "operate_time",
671
+ "operate_user",
672
+ "verification",
673
+ ):
674
+ payload.pop(key, None)
675
+
676
+
677
+ def _summarize_import_issues(issues: list[Any]) -> dict[str, Any]:
678
+ total = 0
679
+ error_count = 0
680
+ warning_count = 0
681
+ sample: list[dict[str, Any]] = []
682
+ for item in issues:
683
+ if not isinstance(item, dict):
684
+ continue
685
+ total += 1
686
+ severity = str(item.get("severity") or "").lower()
687
+ if severity == "error":
688
+ error_count += 1
689
+ if severity == "warning":
690
+ warning_count += 1
691
+ if len(sample) < 3:
692
+ sample.append(
693
+ {
694
+ "code": item.get("code"),
695
+ "message": item.get("message"),
696
+ "severity": item.get("severity"),
697
+ }
698
+ )
699
+ return {
700
+ "total": total,
701
+ "errors": error_count,
702
+ "warnings": warning_count,
703
+ "sample": sample,
704
+ }
705
+
706
+
707
+ def _trim_directory(payload: JSONObject) -> None:
708
+ pass
709
+
710
+
711
+ def _trim_feedback(payload: JSONObject) -> None:
712
+ payload.pop("normalized_payload", None)
713
+
714
+
715
+ def _trim_builder_envelope(payload: JSONObject) -> None:
716
+ if str(payload.get("status") or "").lower() == "success":
717
+ details = payload.get("details")
718
+ if isinstance(details, dict):
719
+ _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
720
+ compact = _compact_scalar_dict(details)
721
+ if compact:
722
+ payload["details"] = compact
723
+ else:
724
+ payload.pop("details", None)
725
+
726
+
727
+ def _trim_builder_list_like(payload: JSONObject) -> None:
728
+ _trim_builder_envelope(payload)
729
+
730
+
731
+ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform: TransformFn) -> None:
732
+ for domain in domains:
733
+ for name in names:
734
+ SUCCESS_POLICY_BY_TOOL[tool_key(domain, name)] = transform
735
+
736
+
737
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
738
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
739
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
740
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
741
+ _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
742
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
743
+ _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
744
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
745
+ _register_policy((USER_DOMAIN,), ("file_get_upload_info",), _trim_file_upload_info)
746
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("file_upload_local",), _trim_file_upload_local)
747
+ _register_policy(
748
+ (USER_DOMAIN,),
749
+ (
750
+ "record_import_schema_get",
751
+ "record_import_template_get",
752
+ "record_import_verify",
753
+ "record_import_repair_local",
754
+ "record_import_start",
755
+ "record_import_status_get",
756
+ ),
757
+ _trim_import_schema,
758
+ )
759
+ _register_policy(
760
+ (USER_DOMAIN,),
761
+ (
762
+ "record_schema_get",
763
+ "record_insert_schema_get",
764
+ "record_browse_schema_get",
765
+ "record_update_schema_get",
766
+ "record_code_block_schema_get",
767
+ ),
768
+ _trim_record_schema,
769
+ )
770
+ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_record_write)
771
+ _register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
772
+ _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
773
+ _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
774
+ _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
775
+ _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
776
+ _register_policy((USER_DOMAIN,), ("task_get",), _trim_task_get)
777
+ _register_policy(
778
+ (USER_DOMAIN,),
779
+ (
780
+ "task_action_execute",
781
+ "task_associated_report_detail_get",
782
+ "task_workflow_log_get",
783
+ ),
784
+ _trim_task_context_detail,
785
+ )
786
+ _register_policy(
787
+ (USER_DOMAIN,),
788
+ (
789
+ "directory_search",
790
+ "directory_list_internal_users",
791
+ "directory_list_all_internal_users",
792
+ "directory_list_internal_departments",
793
+ "directory_list_all_departments",
794
+ "directory_list_sub_departments",
795
+ "directory_list_external_members",
796
+ ),
797
+ _trim_directory,
798
+ )
799
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("feedback_submit",), _trim_feedback)
800
+ _register_policy(
801
+ (USER_DOMAIN,),
802
+ (
803
+ "record_member_candidates",
804
+ "record_department_candidates",
805
+ ),
806
+ _trim_builder_list_like,
807
+ )
808
+ _register_policy((USER_DOMAIN,), ("record_delete",), _trim_record_delete)
809
+ _register_policy(
810
+ (BUILDER_DOMAIN,),
811
+ (
812
+ "builder_tool_contract",
813
+ "package_get",
814
+ "package_apply",
815
+ "solution_install",
816
+ "member_search",
817
+ "role_search",
818
+ "role_create",
819
+ "app_release_edit_lock_if_mine",
820
+ "app_resolve",
821
+ "app_custom_button_list",
822
+ "app_custom_button_get",
823
+ "app_custom_button_create",
824
+ "app_custom_button_update",
825
+ "app_custom_button_delete",
826
+ "app_get_fields",
827
+ "app_repair_code_blocks",
828
+ "app_get_layout",
829
+ "app_get_views",
830
+ "app_get_flow",
831
+ "app_get_charts",
832
+ "app_schema_apply",
833
+ "app_layout_apply",
834
+ "app_flow_apply",
835
+ "app_views_apply",
836
+ "app_charts_apply",
837
+ "portal_apply",
838
+ "app_publish_verify",
839
+ ),
840
+ _trim_builder_list_like,
841
+ )