@josephyan/qingflow-app-builder-mcp 0.2.0-beta.98 → 0.2.0-beta.982
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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +2 -2
- package/src/qingflow_mcp/builder_facade/service.py +84 -27
- package/src/qingflow_mcp/cli/commands/builder.py +3 -2
- package/src/qingflow_mcp/cli/formatters.py +23 -14
- package/src/qingflow_mcp/response_trim.py +364 -7
- package/src/qingflow_mcp/server_app_builder.py +1 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +19 -17
- package/src/qingflow_mcp/tools/import_tools.py +36 -2
- package/src/qingflow_mcp/tools/record_tools.py +1 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +13 -15
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.982
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.982 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1536,8 +1536,8 @@ class PortalApplyRequest(StrictModel):
|
|
|
1536
1536
|
raise ValueError("package_tag_id is required when dash_key is empty")
|
|
1537
1537
|
if not self.dash_key and not self.dash_name:
|
|
1538
1538
|
raise ValueError("dash_name is required when creating a portal")
|
|
1539
|
-
if not self.sections:
|
|
1540
|
-
raise ValueError("portal apply requires a non-empty sections list")
|
|
1539
|
+
if not self.dash_key and not self.sections:
|
|
1540
|
+
raise ValueError("portal apply requires a non-empty sections list when creating a portal")
|
|
1541
1541
|
if self.visibility is not None and self.auth is not None:
|
|
1542
1542
|
raise ValueError("visibility and auth cannot be provided together")
|
|
1543
1543
|
return self
|
|
@@ -1413,6 +1413,8 @@ class AiBuilderFacade:
|
|
|
1413
1413
|
issues: list[dict[str, Any]] = []
|
|
1414
1414
|
resolved: list[dict[str, Any]] = []
|
|
1415
1415
|
seen_ids: set[int] = set()
|
|
1416
|
+
if not dept_ids and not dept_names:
|
|
1417
|
+
return {"department_entries": resolved, "issues": issues}
|
|
1416
1418
|
listed = self.directory.directory_list_all_departments(
|
|
1417
1419
|
profile=profile,
|
|
1418
1420
|
parent_dept_id=None,
|
|
@@ -6706,6 +6708,7 @@ class AiBuilderFacade:
|
|
|
6706
6708
|
|
|
6707
6709
|
for patch in request.upsert_charts:
|
|
6708
6710
|
try:
|
|
6711
|
+
config_update_requested = _chart_patch_updates_chart_config(patch)
|
|
6709
6712
|
chart_visible_auth = (
|
|
6710
6713
|
self._compile_visibility_to_chart_visible_auth(profile=profile, visibility=patch.visibility)
|
|
6711
6714
|
if patch.visibility is not None
|
|
@@ -6809,18 +6812,17 @@ class AiBuilderFacade:
|
|
|
6809
6812
|
existing_by_name.pop(old_name, None)
|
|
6810
6813
|
existing_by_name.setdefault(patch.name, []).append(deepcopy(updated_chart))
|
|
6811
6814
|
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
if existing is not None and chart_id not in updated_ids:
|
|
6815
|
+
config_updated = False
|
|
6816
|
+
if existing is None or config_update_requested:
|
|
6817
|
+
config_payload = _build_public_chart_config_payload(
|
|
6818
|
+
patch=patch,
|
|
6819
|
+
app_key=app_key,
|
|
6820
|
+
field_lookup=field_lookup,
|
|
6821
|
+
qingbi_fields_by_id=qingbi_fields_by_id,
|
|
6822
|
+
)
|
|
6823
|
+
self.charts.qingbi_report_update_config(profile=profile, chart_id=chart_id, payload=config_payload)
|
|
6824
|
+
config_updated = True
|
|
6825
|
+
if existing is not None and chart_id not in updated_ids and config_updated:
|
|
6824
6826
|
updated_ids.append(chart_id)
|
|
6825
6827
|
if patch.question_config:
|
|
6826
6828
|
self._request_backend(
|
|
@@ -6836,6 +6838,8 @@ class AiBuilderFacade:
|
|
|
6836
6838
|
path=f"/chart/{chart_id}/user/config",
|
|
6837
6839
|
json_body=patch.user_config,
|
|
6838
6840
|
)
|
|
6841
|
+
if existing is not None and chart_id not in updated_ids and (patch.question_config or patch.user_config):
|
|
6842
|
+
updated_ids.append(chart_id)
|
|
6839
6843
|
chart_results.append(
|
|
6840
6844
|
{
|
|
6841
6845
|
"chart_id": chart_id,
|
|
@@ -6992,11 +6996,12 @@ class AiBuilderFacade:
|
|
|
6992
6996
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
6993
6997
|
dash_key = str(request.dash_key or "").strip()
|
|
6994
6998
|
creating = not dash_key
|
|
6999
|
+
sections_requested = creating or bool(request.sections)
|
|
6995
7000
|
verify_dash_name = creating or request.dash_name is not None
|
|
6996
7001
|
verify_dash_icon = bool(request.icon or request.color)
|
|
6997
7002
|
verify_auth = request.visibility is not None or request.auth is not None
|
|
6998
|
-
verify_hide_copyright = request.hide_copyright is not None
|
|
6999
|
-
verify_dash_global_config = request.dash_global_config is not None
|
|
7003
|
+
verify_hide_copyright = request.hide_copyright is not None and sections_requested
|
|
7004
|
+
verify_dash_global_config = request.dash_global_config is not None and sections_requested
|
|
7000
7005
|
verify_tags = creating or request.package_tag_id is not None
|
|
7001
7006
|
requested_visibility = request.visibility
|
|
7002
7007
|
if requested_visibility is None and isinstance(request.auth, dict) and request.auth:
|
|
@@ -7073,6 +7078,25 @@ class AiBuilderFacade:
|
|
|
7073
7078
|
if package_edit_outcome.block is not None:
|
|
7074
7079
|
return package_edit_outcome.block
|
|
7075
7080
|
permission_outcomes.append(package_edit_outcome)
|
|
7081
|
+
if not sections_requested:
|
|
7082
|
+
unsupported_base_only_keys: list[str] = []
|
|
7083
|
+
if request.hide_copyright is not None:
|
|
7084
|
+
unsupported_base_only_keys.append("hide_copyright")
|
|
7085
|
+
if request.dash_global_config is not None:
|
|
7086
|
+
unsupported_base_only_keys.append("dash_global_config")
|
|
7087
|
+
if request.config:
|
|
7088
|
+
unsupported_base_only_keys.append("config")
|
|
7089
|
+
if unsupported_base_only_keys:
|
|
7090
|
+
return _failed(
|
|
7091
|
+
"PORTAL_SECTIONS_REQUIRED",
|
|
7092
|
+
"editing a portal without sections only supports base-info updates",
|
|
7093
|
+
normalized_args=normalized_args,
|
|
7094
|
+
details={
|
|
7095
|
+
"unsupported_without_sections": unsupported_base_only_keys,
|
|
7096
|
+
"fix_hint": "Pass sections when changing layout or global portal config, or omit those keys for visibility/icon/package updates.",
|
|
7097
|
+
},
|
|
7098
|
+
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7099
|
+
)
|
|
7076
7100
|
try:
|
|
7077
7101
|
if creating:
|
|
7078
7102
|
create_payload = _build_public_portal_base_payload(
|
|
@@ -7098,7 +7122,6 @@ class AiBuilderFacade:
|
|
|
7098
7122
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
7099
7123
|
)
|
|
7100
7124
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
7101
|
-
component_payload = self._build_portal_components_from_sections(profile=profile, sections=request.sections)
|
|
7102
7125
|
update_payload = _build_public_portal_base_payload(
|
|
7103
7126
|
dash_name=request.dash_name or str(base_payload.get("dashName") or "").strip() or "未命名门户",
|
|
7104
7127
|
package_tag_id=target_package_tag_id,
|
|
@@ -7110,8 +7133,10 @@ class AiBuilderFacade:
|
|
|
7110
7133
|
config=request.config,
|
|
7111
7134
|
base_payload=base_payload,
|
|
7112
7135
|
)
|
|
7113
|
-
|
|
7114
|
-
|
|
7136
|
+
if sections_requested:
|
|
7137
|
+
component_payload = self._build_portal_components_from_sections(profile=profile, sections=request.sections)
|
|
7138
|
+
update_payload["components"] = component_payload
|
|
7139
|
+
self.portals.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
|
|
7115
7140
|
self.portals.portal_update_base_info(
|
|
7116
7141
|
profile=profile,
|
|
7117
7142
|
dash_key=dash_key,
|
|
@@ -7148,11 +7173,14 @@ class AiBuilderFacade:
|
|
|
7148
7173
|
publish_failed = True
|
|
7149
7174
|
|
|
7150
7175
|
draft_components = draft_result.get("components") if isinstance(draft_result, dict) else None
|
|
7151
|
-
expected_count = len(request.sections)
|
|
7152
|
-
draft_verified = isinstance(
|
|
7176
|
+
expected_count = len(request.sections) if sections_requested else None
|
|
7177
|
+
draft_verified = isinstance(draft_result, dict) and (
|
|
7178
|
+
expected_count is None or (isinstance(draft_components, list) and len(draft_components) == expected_count)
|
|
7179
|
+
)
|
|
7153
7180
|
draft_meta_verified, draft_meta_mismatches = _verify_portal_readback(
|
|
7154
7181
|
actual=draft_result,
|
|
7155
7182
|
expected_payload=update_payload,
|
|
7183
|
+
expected_visibility=requested_visibility.model_dump(mode="json") if requested_visibility is not None else None,
|
|
7156
7184
|
expected_section_count=expected_count,
|
|
7157
7185
|
requested_config_keys=set((request.config or {}).keys()),
|
|
7158
7186
|
verify_dash_name=verify_dash_name,
|
|
@@ -7168,12 +7196,18 @@ class AiBuilderFacade:
|
|
|
7168
7196
|
if request.publish:
|
|
7169
7197
|
live_verified = (
|
|
7170
7198
|
isinstance(live_result, dict)
|
|
7171
|
-
and
|
|
7172
|
-
|
|
7199
|
+
and (
|
|
7200
|
+
expected_count is None
|
|
7201
|
+
or (
|
|
7202
|
+
isinstance(live_result.get("components"), list)
|
|
7203
|
+
and len(live_result.get("components")) == expected_count
|
|
7204
|
+
)
|
|
7205
|
+
)
|
|
7173
7206
|
)
|
|
7174
7207
|
live_meta_verified, live_meta_mismatches = _verify_portal_readback(
|
|
7175
7208
|
actual=live_result,
|
|
7176
7209
|
expected_payload=update_payload,
|
|
7210
|
+
expected_visibility=requested_visibility.model_dump(mode="json") if requested_visibility is not None else None,
|
|
7177
7211
|
expected_section_count=expected_count,
|
|
7178
7212
|
requested_config_keys=set((request.config or {}).keys()),
|
|
7179
7213
|
verify_dash_name=verify_dash_name,
|
|
@@ -7207,7 +7241,15 @@ class AiBuilderFacade:
|
|
|
7207
7241
|
"status": status,
|
|
7208
7242
|
"error_code": error_code,
|
|
7209
7243
|
"recoverable": not verified,
|
|
7210
|
-
"message":
|
|
7244
|
+
"message": (
|
|
7245
|
+
"updated portal base info"
|
|
7246
|
+
if verified and not sections_requested
|
|
7247
|
+
else "applied portal"
|
|
7248
|
+
if verified
|
|
7249
|
+
else "updated portal base info; draft/live verification pending"
|
|
7250
|
+
if not sections_requested
|
|
7251
|
+
else "applied portal; draft/live verification pending"
|
|
7252
|
+
),
|
|
7211
7253
|
"normalized_args": normalized_args,
|
|
7212
7254
|
"missing_fields": [],
|
|
7213
7255
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
@@ -8733,6 +8775,11 @@ def _build_public_chart_config_payload(
|
|
|
8733
8775
|
return payload
|
|
8734
8776
|
|
|
8735
8777
|
|
|
8778
|
+
def _chart_patch_updates_chart_config(patch: ChartUpsertPatch) -> bool:
|
|
8779
|
+
explicit_fields = set(getattr(patch, "model_fields_set", set()) or set())
|
|
8780
|
+
return bool({"dimension_field_ids", "indicator_field_ids", "filters", "config"} & explicit_fields)
|
|
8781
|
+
|
|
8782
|
+
|
|
8736
8783
|
def _build_public_portal_base_payload(
|
|
8737
8784
|
*,
|
|
8738
8785
|
dash_name: str,
|
|
@@ -8962,7 +9009,8 @@ def _verify_portal_readback(
|
|
|
8962
9009
|
*,
|
|
8963
9010
|
actual: Any,
|
|
8964
9011
|
expected_payload: dict[str, Any],
|
|
8965
|
-
|
|
9012
|
+
expected_visibility: dict[str, Any] | None,
|
|
9013
|
+
expected_section_count: int | None,
|
|
8966
9014
|
requested_config_keys: set[str],
|
|
8967
9015
|
verify_dash_name: bool,
|
|
8968
9016
|
verify_dash_icon: bool,
|
|
@@ -8975,14 +9023,19 @@ def _verify_portal_readback(
|
|
|
8975
9023
|
if not isinstance(actual, dict):
|
|
8976
9024
|
return False, ["portal readback payload is unavailable"]
|
|
8977
9025
|
components = actual.get("components")
|
|
8978
|
-
if not isinstance(components, list) or len(components) != expected_section_count:
|
|
9026
|
+
if expected_section_count is not None and (not isinstance(components, list) or len(components) != expected_section_count):
|
|
8979
9027
|
mismatches.append(f"components expected {expected_section_count}, got {len(components) if isinstance(components, list) else 'unavailable'}")
|
|
8980
9028
|
if verify_dash_name and str(actual.get("dashName") or "").strip() != str(expected_payload.get("dashName") or "").strip():
|
|
8981
9029
|
mismatches.append("dash_name")
|
|
8982
9030
|
if verify_dash_icon and str(actual.get("dashIcon") or "") != str(expected_payload.get("dashIcon") or ""):
|
|
8983
9031
|
mismatches.append("dash_icon")
|
|
8984
|
-
if verify_auth
|
|
8985
|
-
|
|
9032
|
+
if verify_auth:
|
|
9033
|
+
if expected_visibility is not None:
|
|
9034
|
+
actual_visibility = _public_visibility_from_member_auth(actual.get("auth"))
|
|
9035
|
+
if not _visibility_matches_expected(actual_visibility, expected_visibility):
|
|
9036
|
+
mismatches.append("auth")
|
|
9037
|
+
elif not _mapping_contains(actual.get("auth"), expected_payload.get("auth")):
|
|
9038
|
+
mismatches.append("auth")
|
|
8986
9039
|
if verify_hide_copyright and bool(actual.get("hideCopyright", False)) != bool(expected_payload.get("hideCopyright", False)):
|
|
8987
9040
|
mismatches.append("hide_copyright")
|
|
8988
9041
|
if verify_dash_global_config and not _mapping_contains(actual.get("dashGlobalConfig") or {}, expected_payload.get("dashGlobalConfig") or {}):
|
|
@@ -9190,7 +9243,11 @@ def _visibility_matches_expected(actual: Any, expected: Any) -> bool:
|
|
|
9190
9243
|
if expected_text and sorted_values(actual_group, text_key) != expected_text:
|
|
9191
9244
|
return False
|
|
9192
9245
|
|
|
9193
|
-
if
|
|
9246
|
+
if (
|
|
9247
|
+
"include_sub_departs" in expected_selectors
|
|
9248
|
+
and expected_selectors.get("include_sub_departs") is not None
|
|
9249
|
+
and actual_selectors.get("include_sub_departs") != expected_selectors.get("include_sub_departs")
|
|
9250
|
+
):
|
|
9194
9251
|
return False
|
|
9195
9252
|
return True
|
|
9196
9253
|
|
|
@@ -160,7 +160,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
160
160
|
portal_apply.add_argument("--dash-name", default="")
|
|
161
161
|
portal_apply.add_argument("--package-id", type=int)
|
|
162
162
|
portal_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
163
|
-
portal_apply.add_argument("--sections-file"
|
|
163
|
+
portal_apply.add_argument("--sections-file")
|
|
164
164
|
portal_apply.add_argument("--visibility-file")
|
|
165
165
|
portal_apply.add_argument("--auth-file")
|
|
166
166
|
portal_apply.add_argument("--icon")
|
|
@@ -513,13 +513,14 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
513
513
|
"portal apply requires either --dash-key, or --package-id together with --dash-name.",
|
|
514
514
|
fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
|
|
515
515
|
)
|
|
516
|
+
sections = [] if not args.sections_file else require_list_arg(args.sections_file, option_name="--sections-file")
|
|
516
517
|
return context.builder.portal_apply(
|
|
517
518
|
profile=args.profile,
|
|
518
519
|
dash_key=args.dash_key,
|
|
519
520
|
dash_name=args.dash_name,
|
|
520
521
|
package_id=args.package_id,
|
|
521
522
|
publish=bool(args.publish),
|
|
522
|
-
sections=
|
|
523
|
+
sections=sections,
|
|
523
524
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
524
525
|
auth=load_object_arg(args.auth_file, option_name="--auth-file"),
|
|
525
526
|
icon=args.icon,
|
|
@@ -177,10 +177,10 @@ def _format_task_list(result: dict[str, Any]) -> str:
|
|
|
177
177
|
str(item.get("app_key") or ""),
|
|
178
178
|
str(item.get("record_id") or ""),
|
|
179
179
|
str(item.get("workflow_node_id") or ""),
|
|
180
|
-
str(item.get("
|
|
180
|
+
str(item.get("workflow_node_name") or ""),
|
|
181
181
|
]
|
|
182
182
|
)
|
|
183
|
-
output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "
|
|
183
|
+
output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "node_name"], rows)
|
|
184
184
|
lines = output.rstrip("\n").split("\n")
|
|
185
185
|
_append_warnings(lines, result.get("warnings"))
|
|
186
186
|
return "\n".join(lines) + "\n"
|
|
@@ -193,11 +193,13 @@ def _format_task_get(result: dict[str, Any]) -> str:
|
|
|
193
193
|
editable_fields = data.get("editable_fields") if isinstance(data.get("editable_fields"), list) else []
|
|
194
194
|
available_actions = data.get("available_actions") if isinstance(data.get("available_actions"), list) else []
|
|
195
195
|
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
196
|
+
initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
|
|
197
|
+
initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
|
|
196
198
|
lines = [
|
|
197
199
|
f"Task: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
|
|
198
200
|
f"Node: {task.get('workflow_node_name') or '-'}",
|
|
199
201
|
f"App: {task.get('app_name') or '-'}",
|
|
200
|
-
f"Initiator: {
|
|
202
|
+
f"Initiator: {initiator_label}",
|
|
201
203
|
f"Apply Status: {record_summary.get('apply_status')}",
|
|
202
204
|
f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
|
|
203
205
|
f"Editable Fields: {len(editable_fields)}",
|
|
@@ -238,17 +240,22 @@ def _format_import_verify(result: dict[str, Any]) -> str:
|
|
|
238
240
|
f"App Key: {result.get('app_key') or '-'}",
|
|
239
241
|
f"File: {result.get('file_name') or result.get('file_path') or '-'}",
|
|
240
242
|
f"Can Import: {result.get('can_import')}",
|
|
241
|
-
f"Apply Rows: {result.get('apply_rows')}",
|
|
242
243
|
f"Verification ID: {result.get('verification_id') or '-'}",
|
|
243
244
|
]
|
|
244
|
-
|
|
245
|
-
if
|
|
246
|
-
lines.append(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
issue_summary = result.get("issue_summary") if isinstance(result.get("issue_summary"), dict) else {}
|
|
246
|
+
if issue_summary:
|
|
247
|
+
lines.append(
|
|
248
|
+
"Issues: "
|
|
249
|
+
f"total={issue_summary.get('total', 0)}, "
|
|
250
|
+
f"errors={issue_summary.get('errors', 0)}, "
|
|
251
|
+
f"warnings={issue_summary.get('warnings', 0)}"
|
|
252
|
+
)
|
|
253
|
+
sample = issue_summary.get("sample") if isinstance(issue_summary.get("sample"), list) else []
|
|
254
|
+
if sample:
|
|
255
|
+
lines.append("Issue Samples:")
|
|
256
|
+
for item in sample:
|
|
257
|
+
if isinstance(item, dict):
|
|
258
|
+
lines.append(f"- {item.get('code') or 'ISSUE'}: {item.get('message') or ''}".rstrip())
|
|
252
259
|
_append_warnings(lines, result.get("warnings"))
|
|
253
260
|
_append_verification(lines, result.get("verification"))
|
|
254
261
|
return "\n".join(lines) + "\n"
|
|
@@ -259,8 +266,10 @@ def _format_import_status(result: dict[str, Any]) -> str:
|
|
|
259
266
|
f"Status: {result.get('status') or '-'}",
|
|
260
267
|
f"Import ID: {result.get('import_id') or '-'}",
|
|
261
268
|
f"Process ID: {result.get('process_id_str') or '-'}",
|
|
262
|
-
f"
|
|
263
|
-
f"
|
|
269
|
+
f"Total Rows: {result.get('total') or 0}",
|
|
270
|
+
f"Finished Rows: {result.get('finished') or 0}",
|
|
271
|
+
f"Succeeded Rows: {result.get('succeeded') or 0}",
|
|
272
|
+
f"Failed Rows: {result.get('failed') or 0}",
|
|
264
273
|
f"Progress: {result.get('progress') or '-'}",
|
|
265
274
|
]
|
|
266
275
|
_append_warnings(lines, result.get("warnings"))
|
|
@@ -293,34 +293,175 @@ def _trim_file_upload_local(payload: JSONObject) -> None:
|
|
|
293
293
|
|
|
294
294
|
|
|
295
295
|
def _trim_import_schema(payload: JSONObject) -> None:
|
|
296
|
-
|
|
296
|
+
columns: list[JSONObject] | None = None
|
|
297
|
+
if isinstance(payload.get("columns"), list):
|
|
298
|
+
columns = [item for item in payload.get("columns", []) if isinstance(item, dict)]
|
|
299
|
+
elif isinstance(payload.get("expected_columns"), list):
|
|
300
|
+
columns = [item for item in payload.get("expected_columns", []) if isinstance(item, dict)]
|
|
301
|
+
if columns is not None:
|
|
302
|
+
payload["columns"] = [_compact_import_column(item) for item in columns]
|
|
303
|
+
payload.pop("expected_columns", None)
|
|
304
|
+
payload.pop("schema_fingerprint", None)
|
|
305
|
+
payload.pop("import_capability", None)
|
|
306
|
+
payload.pop("request_route", None)
|
|
307
|
+
payload.pop("verification", None)
|
|
308
|
+
|
|
309
|
+
if _looks_like_import_verify(payload):
|
|
310
|
+
_trim_import_verify_payload(payload)
|
|
311
|
+
return
|
|
312
|
+
if "applied_repairs" in payload or "repaired_file_path" in payload:
|
|
313
|
+
_trim_import_repair_payload(payload)
|
|
314
|
+
return
|
|
315
|
+
if "template_url" in payload or "downloaded_to_path" in payload:
|
|
316
|
+
_trim_import_template_payload(payload)
|
|
317
|
+
return
|
|
318
|
+
if "import_id" in payload or "process_id_str" in payload:
|
|
319
|
+
_trim_import_status_payload(payload)
|
|
320
|
+
return
|
|
297
321
|
|
|
298
322
|
|
|
299
323
|
def _trim_record_schema(payload: JSONObject) -> None:
|
|
300
324
|
payload.pop("legacy_schema", None)
|
|
325
|
+
template_map = payload.get("payload_template")
|
|
326
|
+
if not isinstance(template_map, dict):
|
|
327
|
+
template_map = None
|
|
328
|
+
|
|
329
|
+
if "writable_fields" in payload:
|
|
330
|
+
writable_fields = payload.get("writable_fields")
|
|
331
|
+
payload.pop("writable_fields", None)
|
|
332
|
+
required_fields: list[JSONObject] = []
|
|
333
|
+
optional_fields: list[JSONObject] = []
|
|
334
|
+
if isinstance(writable_fields, list):
|
|
335
|
+
for item in writable_fields:
|
|
336
|
+
compact = _compact_schema_field(item, template_map=template_map)
|
|
337
|
+
if not compact:
|
|
338
|
+
continue
|
|
339
|
+
if compact.get("required") is True:
|
|
340
|
+
required_fields.append(compact)
|
|
341
|
+
else:
|
|
342
|
+
optional_fields.append(compact)
|
|
343
|
+
payload["required_fields"] = required_fields
|
|
344
|
+
payload["optional_fields"] = optional_fields
|
|
345
|
+
|
|
346
|
+
for key in ("required_fields", "optional_fields", "runtime_linked_required_fields", "fields", "ambiguous_fields"):
|
|
347
|
+
if key in payload:
|
|
348
|
+
payload[key] = _compact_schema_fields(payload.get(key), template_map=template_map)
|
|
349
|
+
|
|
350
|
+
for key in ("suggested_dimensions", "suggested_metrics", "suggested_time_fields"):
|
|
351
|
+
if isinstance(payload.get(key), list):
|
|
352
|
+
payload[key] = [
|
|
353
|
+
_pick(item, ("field_id", "title")) for item in payload.get(key) if isinstance(item, dict)
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
for key in ("workflow_node", "view_resolution", "field_count"):
|
|
357
|
+
payload.pop(key, None)
|
|
301
358
|
|
|
302
359
|
|
|
303
360
|
def _trim_record_write(payload: JSONObject) -> None:
|
|
361
|
+
payload.pop("verification", None)
|
|
304
362
|
data = payload.get("data")
|
|
305
363
|
if not isinstance(data, dict):
|
|
306
364
|
return
|
|
365
|
+
data.pop("debug", None)
|
|
307
366
|
data.pop("normalized_payload", None)
|
|
308
367
|
data.pop("human_review", None)
|
|
309
368
|
data.pop("action", None)
|
|
369
|
+
resource = _compact_record_resource(data.get("resource"))
|
|
370
|
+
if resource:
|
|
371
|
+
data["resource"] = resource
|
|
372
|
+
else:
|
|
373
|
+
data.pop("resource", None)
|
|
374
|
+
verification = data.get("verification")
|
|
375
|
+
if isinstance(verification, dict):
|
|
376
|
+
compact_verification = _pick(
|
|
377
|
+
verification,
|
|
378
|
+
(
|
|
379
|
+
"verified",
|
|
380
|
+
"verification_mode",
|
|
381
|
+
"field_level_verified",
|
|
382
|
+
),
|
|
383
|
+
)
|
|
384
|
+
if compact_verification:
|
|
385
|
+
data["verification"] = compact_verification
|
|
386
|
+
else:
|
|
387
|
+
data.pop("verification", None)
|
|
388
|
+
for key in ("blockers", "field_errors", "confirmation_requests", "resolved_fields"):
|
|
389
|
+
value = data.get(key)
|
|
390
|
+
if value in (None, [], {}, ""):
|
|
391
|
+
data.pop(key, None)
|
|
310
392
|
|
|
311
393
|
|
|
312
394
|
def _trim_record_get(payload: JSONObject) -> None:
|
|
313
|
-
|
|
314
|
-
|
|
395
|
+
data = payload.get("data")
|
|
396
|
+
if not isinstance(data, dict):
|
|
397
|
+
return
|
|
398
|
+
compact: dict[str, Any] = {}
|
|
399
|
+
app_key = data.get("app_key")
|
|
400
|
+
if app_key:
|
|
401
|
+
compact["app_key"] = app_key
|
|
402
|
+
record_id = data.get("record_id")
|
|
403
|
+
if record_id not in (None, ""):
|
|
404
|
+
compact["record_id"] = str(record_id)
|
|
405
|
+
record = data.get("record")
|
|
406
|
+
if isinstance(record, dict):
|
|
407
|
+
compact["record"] = record
|
|
408
|
+
normalized_record = data.get("normalized_record")
|
|
409
|
+
if isinstance(normalized_record, dict):
|
|
410
|
+
compact["normalized_record"] = normalized_record
|
|
411
|
+
normalized_ambiguous_fields = data.get("normalized_ambiguous_fields")
|
|
412
|
+
if isinstance(normalized_ambiguous_fields, dict):
|
|
413
|
+
compact["normalized_ambiguous_fields"] = normalized_ambiguous_fields
|
|
414
|
+
payload["data"] = compact
|
|
315
415
|
|
|
316
416
|
|
|
317
417
|
def _trim_record_list(payload: JSONObject) -> None:
|
|
318
|
-
|
|
319
|
-
|
|
418
|
+
data = payload.get("data")
|
|
419
|
+
if not isinstance(data, dict):
|
|
420
|
+
return
|
|
421
|
+
pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
|
|
422
|
+
returned_items = pagination.get("returned_items")
|
|
423
|
+
result_amount = pagination.get("result_amount")
|
|
424
|
+
limit = pagination.get("limit")
|
|
425
|
+
truncated = False
|
|
426
|
+
if isinstance(result_amount, int) and isinstance(returned_items, int):
|
|
427
|
+
truncated = result_amount > returned_items
|
|
428
|
+
compact_pagination = {
|
|
429
|
+
"loaded": True,
|
|
430
|
+
"page_size": limit,
|
|
431
|
+
"fetched_pages": 1,
|
|
432
|
+
"reported_total": result_amount,
|
|
433
|
+
"truncated": truncated,
|
|
434
|
+
}
|
|
435
|
+
selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
|
|
436
|
+
view = selection.get("view") if isinstance(selection.get("view"), dict) else {}
|
|
437
|
+
compact: dict[str, Any] = {
|
|
438
|
+
"app_key": data.get("app_key"),
|
|
439
|
+
"items": data.get("items") if isinstance(data.get("items"), list) else [],
|
|
440
|
+
"pagination": compact_pagination,
|
|
441
|
+
}
|
|
442
|
+
if view:
|
|
443
|
+
compact["view"] = _pick(view, ("view_id", "name"))
|
|
444
|
+
payload["data"] = compact
|
|
320
445
|
|
|
321
446
|
|
|
322
447
|
def _trim_record_analyze(payload: JSONObject) -> None:
|
|
323
|
-
|
|
448
|
+
summary: dict[str, Any] = {}
|
|
449
|
+
completeness = payload.get("completeness")
|
|
450
|
+
if isinstance(completeness, dict):
|
|
451
|
+
summary["completeness"] = completeness
|
|
452
|
+
presentation = payload.get("presentation")
|
|
453
|
+
if isinstance(presentation, dict):
|
|
454
|
+
summary["presentation"] = presentation
|
|
455
|
+
ranking = payload.get("ranking")
|
|
456
|
+
if isinstance(ranking, dict):
|
|
457
|
+
summary["ranking"] = ranking
|
|
458
|
+
error = payload.get("error")
|
|
459
|
+
if isinstance(error, dict):
|
|
460
|
+
summary["error"] = error
|
|
461
|
+
if summary:
|
|
462
|
+
payload["summary"] = summary
|
|
463
|
+
for key in ("query", "ranking", "ratios", "completeness", "presentation", "error", "debug"):
|
|
464
|
+
payload.pop(key, None)
|
|
324
465
|
|
|
325
466
|
|
|
326
467
|
def _trim_code_block_schema(payload: JSONObject) -> None:
|
|
@@ -344,6 +485,222 @@ def _trim_task_context_detail(payload: JSONObject) -> None:
|
|
|
344
485
|
_drop_deep_keys(payload, {"request_route", "output_profile"})
|
|
345
486
|
|
|
346
487
|
|
|
488
|
+
def _trim_record_delete(payload: JSONObject) -> None:
|
|
489
|
+
data = payload.get("data")
|
|
490
|
+
if not isinstance(data, dict):
|
|
491
|
+
return
|
|
492
|
+
resource = data.get("resource")
|
|
493
|
+
deleted_ids: list[str] = []
|
|
494
|
+
if isinstance(resource, dict):
|
|
495
|
+
raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
|
|
496
|
+
if isinstance(raw_ids, list):
|
|
497
|
+
deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
|
|
498
|
+
data["deleted_ids"] = deleted_ids
|
|
499
|
+
data.setdefault("failed_ids", [])
|
|
500
|
+
for key in (
|
|
501
|
+
"resource",
|
|
502
|
+
"action",
|
|
503
|
+
"normalized_payload",
|
|
504
|
+
"human_review",
|
|
505
|
+
"verification",
|
|
506
|
+
"blockers",
|
|
507
|
+
"field_errors",
|
|
508
|
+
"confirmation_requests",
|
|
509
|
+
"resolved_fields",
|
|
510
|
+
"debug",
|
|
511
|
+
):
|
|
512
|
+
data.pop(key, None)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _compact_record_resource(resource: Any) -> dict[str, Any] | None:
|
|
516
|
+
if not isinstance(resource, dict):
|
|
517
|
+
return None
|
|
518
|
+
compact: dict[str, Any] = {}
|
|
519
|
+
if resource.get("type") not in (None, ""):
|
|
520
|
+
compact["type"] = resource.get("type")
|
|
521
|
+
app_key = resource.get("app_key") or resource.get("appKey")
|
|
522
|
+
if app_key not in (None, ""):
|
|
523
|
+
compact["app_key"] = app_key
|
|
524
|
+
record_id = resource.get("record_id")
|
|
525
|
+
if record_id not in (None, ""):
|
|
526
|
+
compact["record_id"] = str(record_id)
|
|
527
|
+
apply_id = resource.get("apply_id") or resource.get("applyId")
|
|
528
|
+
if apply_id not in (None, "") and "record_id" not in compact:
|
|
529
|
+
compact["record_id"] = str(apply_id)
|
|
530
|
+
record_ids = resource.get("record_ids")
|
|
531
|
+
if isinstance(record_ids, list):
|
|
532
|
+
compact["record_ids"] = [str(item) for item in record_ids if item not in (None, "")]
|
|
533
|
+
apply_ids = resource.get("apply_ids") or resource.get("applyIds")
|
|
534
|
+
if isinstance(apply_ids, list) and "record_ids" not in compact:
|
|
535
|
+
compact["record_ids"] = [str(item) for item in apply_ids if item not in (None, "")]
|
|
536
|
+
return compact or None
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _compact_schema_fields(items: Any, *, template_map: dict[str, Any] | None) -> list[JSONObject]:
|
|
540
|
+
if not isinstance(items, list):
|
|
541
|
+
return []
|
|
542
|
+
compacted: list[JSONObject] = []
|
|
543
|
+
for item in items:
|
|
544
|
+
compact = _compact_schema_field(item, template_map=template_map)
|
|
545
|
+
if compact:
|
|
546
|
+
compacted.append(compact)
|
|
547
|
+
return compacted
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) -> JSONObject | None:
|
|
551
|
+
if not isinstance(item, dict):
|
|
552
|
+
return None
|
|
553
|
+
compact: dict[str, Any] = {}
|
|
554
|
+
field_id = item.get("field_id")
|
|
555
|
+
if field_id not in (None, ""):
|
|
556
|
+
compact["field_id"] = field_id
|
|
557
|
+
title = item.get("title")
|
|
558
|
+
if title not in (None, ""):
|
|
559
|
+
compact["title"] = title
|
|
560
|
+
kind = item.get("kind") or item.get("write_kind")
|
|
561
|
+
if kind not in (None, ""):
|
|
562
|
+
compact["kind"] = kind
|
|
563
|
+
if "required" in item:
|
|
564
|
+
compact["required"] = bool(item.get("required"))
|
|
565
|
+
if template_map is not None and isinstance(title, str) and title in template_map:
|
|
566
|
+
compact["template"] = template_map.get(title)
|
|
567
|
+
candidate_hint = item.get("candidate_hint")
|
|
568
|
+
if isinstance(candidate_hint, dict):
|
|
569
|
+
compact["candidate_hint"] = candidate_hint
|
|
570
|
+
options = item.get("options")
|
|
571
|
+
if isinstance(options, list) and options:
|
|
572
|
+
compact["options"] = options
|
|
573
|
+
target_app_key = item.get("target_app_key")
|
|
574
|
+
if isinstance(target_app_key, str) and target_app_key:
|
|
575
|
+
compact["target_app_key"] = target_app_key
|
|
576
|
+
searchable_fields = item.get("searchable_fields")
|
|
577
|
+
if isinstance(searchable_fields, list) and searchable_fields:
|
|
578
|
+
compact["searchable_fields"] = searchable_fields
|
|
579
|
+
row_fields = item.get("row_fields")
|
|
580
|
+
if isinstance(row_fields, list) and row_fields:
|
|
581
|
+
compact["row_fields"] = _compact_schema_fields(row_fields, template_map=None)
|
|
582
|
+
return compact or None
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
|
|
586
|
+
compact: dict[str, Any] = {}
|
|
587
|
+
title = item.get("title")
|
|
588
|
+
if title not in (None, ""):
|
|
589
|
+
compact["title"] = title
|
|
590
|
+
kind = item.get("kind") or item.get("write_kind")
|
|
591
|
+
if kind not in (None, ""):
|
|
592
|
+
compact["kind"] = kind
|
|
593
|
+
compact["required"] = bool(item.get("required"))
|
|
594
|
+
options = item.get("options")
|
|
595
|
+
if isinstance(options, list) and options:
|
|
596
|
+
compact["options"] = options
|
|
597
|
+
if bool(item.get("accepts_natural_input")):
|
|
598
|
+
compact["accepts_natural_input"] = True
|
|
599
|
+
if bool(item.get("requires_upload")):
|
|
600
|
+
compact["requires_upload"] = True
|
|
601
|
+
target_app_key = item.get("target_app_key")
|
|
602
|
+
if isinstance(target_app_key, str) and target_app_key:
|
|
603
|
+
compact["target_app_key"] = target_app_key
|
|
604
|
+
target_app_name = item.get("target_app_name")
|
|
605
|
+
if isinstance(target_app_name, str) and target_app_name:
|
|
606
|
+
compact["target_app_name"] = target_app_name
|
|
607
|
+
searchable_fields = item.get("searchable_fields")
|
|
608
|
+
if isinstance(searchable_fields, list) and searchable_fields:
|
|
609
|
+
compact["searchable_fields"] = searchable_fields
|
|
610
|
+
return compact
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _looks_like_import_verify(payload: JSONObject) -> bool:
|
|
614
|
+
return "verification_id" in payload and "can_import" in payload
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _trim_import_verify_payload(payload: JSONObject) -> None:
|
|
618
|
+
issues = payload.get("issues") if isinstance(payload.get("issues"), list) else []
|
|
619
|
+
issue_summary = _summarize_import_issues(issues)
|
|
620
|
+
payload["issue_summary"] = issue_summary
|
|
621
|
+
columns = payload.get("columns")
|
|
622
|
+
if "expected_columns" not in payload and isinstance(columns, list):
|
|
623
|
+
payload["expected_columns"] = columns
|
|
624
|
+
file_name = payload.get("file_name")
|
|
625
|
+
if not file_name:
|
|
626
|
+
file_path = payload.get("file_path")
|
|
627
|
+
if isinstance(file_path, str) and file_path:
|
|
628
|
+
payload["file_name"] = file_path.split("/")[-1]
|
|
629
|
+
for key in ("apply_rows", "schema_fingerprint", "import_capability", "file_sha256", "verified_file_sha256", "file_format", "local_precheck_limited"):
|
|
630
|
+
payload.pop(key, None)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _trim_import_repair_payload(payload: JSONObject) -> None:
|
|
634
|
+
payload["verification_id"] = payload.get("new_verification_id") or payload.get("verification_id")
|
|
635
|
+
post_repair_issues = payload.get("post_repair_issues")
|
|
636
|
+
if isinstance(post_repair_issues, list):
|
|
637
|
+
payload["post_repair_issue_summary"] = _summarize_import_issues(post_repair_issues)
|
|
638
|
+
for key in ("new_verification_id", "verification"):
|
|
639
|
+
payload.pop(key, None)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _trim_import_template_payload(payload: JSONObject) -> None:
|
|
643
|
+
for key in ("schema_fingerprint", "verification"):
|
|
644
|
+
payload.pop(key, None)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _trim_import_status_payload(payload: JSONObject) -> None:
|
|
648
|
+
total_rows = payload.get("total_rows")
|
|
649
|
+
success_rows = payload.get("success_rows")
|
|
650
|
+
failed_rows = payload.get("failed_rows")
|
|
651
|
+
payload["total"] = total_rows
|
|
652
|
+
if isinstance(success_rows, int) and isinstance(failed_rows, int):
|
|
653
|
+
payload["finished"] = success_rows + failed_rows
|
|
654
|
+
elif isinstance(success_rows, int):
|
|
655
|
+
payload["finished"] = success_rows
|
|
656
|
+
else:
|
|
657
|
+
payload["finished"] = None
|
|
658
|
+
payload["succeeded"] = success_rows
|
|
659
|
+
payload["failed"] = failed_rows
|
|
660
|
+
for key in (
|
|
661
|
+
"matched_by",
|
|
662
|
+
"source_file_name",
|
|
663
|
+
"total_rows",
|
|
664
|
+
"success_rows",
|
|
665
|
+
"failed_rows",
|
|
666
|
+
"error_file_urls",
|
|
667
|
+
"operate_time",
|
|
668
|
+
"operate_user",
|
|
669
|
+
"verification",
|
|
670
|
+
):
|
|
671
|
+
payload.pop(key, None)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _summarize_import_issues(issues: list[Any]) -> dict[str, Any]:
|
|
675
|
+
total = 0
|
|
676
|
+
error_count = 0
|
|
677
|
+
warning_count = 0
|
|
678
|
+
sample: list[dict[str, Any]] = []
|
|
679
|
+
for item in issues:
|
|
680
|
+
if not isinstance(item, dict):
|
|
681
|
+
continue
|
|
682
|
+
total += 1
|
|
683
|
+
severity = str(item.get("severity") or "").lower()
|
|
684
|
+
if severity == "error":
|
|
685
|
+
error_count += 1
|
|
686
|
+
if severity == "warning":
|
|
687
|
+
warning_count += 1
|
|
688
|
+
if len(sample) < 3:
|
|
689
|
+
sample.append(
|
|
690
|
+
{
|
|
691
|
+
"code": item.get("code"),
|
|
692
|
+
"message": item.get("message"),
|
|
693
|
+
"severity": item.get("severity"),
|
|
694
|
+
}
|
|
695
|
+
)
|
|
696
|
+
return {
|
|
697
|
+
"total": total,
|
|
698
|
+
"errors": error_count,
|
|
699
|
+
"warnings": warning_count,
|
|
700
|
+
"sample": sample,
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
|
|
347
704
|
def _trim_directory(payload: JSONObject) -> None:
|
|
348
705
|
pass
|
|
349
706
|
|
|
@@ -442,10 +799,10 @@ _register_policy(
|
|
|
442
799
|
(
|
|
443
800
|
"record_member_candidates",
|
|
444
801
|
"record_department_candidates",
|
|
445
|
-
"record_delete",
|
|
446
802
|
),
|
|
447
803
|
_trim_builder_list_like,
|
|
448
804
|
)
|
|
805
|
+
_register_policy((USER_DOMAIN,), ("record_delete",), _trim_record_delete)
|
|
449
806
|
_register_policy(
|
|
450
807
|
(BUILDER_DOMAIN,),
|
|
451
808
|
(
|
|
@@ -38,7 +38,7 @@ def build_builder_server() -> FastMCP:
|
|
|
38
38
|
"If creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use package_get and app_resolve to locate resources, "
|
|
39
39
|
"app_get/app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
|
|
40
40
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
41
|
-
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates
|
|
41
|
+
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
42
42
|
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
43
43
|
"Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
|
|
44
44
|
"For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
|
|
@@ -1763,7 +1763,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1763
1763
|
dash_name: str = "",
|
|
1764
1764
|
package_id: int | None = None,
|
|
1765
1765
|
publish: bool = True,
|
|
1766
|
-
sections: list[JSONObject],
|
|
1766
|
+
sections: list[JSONObject] | None = None,
|
|
1767
1767
|
visibility: JSONObject | None = None,
|
|
1768
1768
|
auth: JSONObject | None = None,
|
|
1769
1769
|
icon: str | None = None,
|
|
@@ -3179,14 +3179,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3179
3179
|
"chart.filter.operator": [member.value for member in ViewFilterOperator],
|
|
3180
3180
|
**deepcopy(_VISIBILITY_ALLOWED_VALUES),
|
|
3181
3181
|
},
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3182
|
+
"execution_notes": [
|
|
3183
|
+
"app_charts_apply is immediate-live and does not publish",
|
|
3184
|
+
"chart matching precedence is chart_id first, then exact unique chart name",
|
|
3185
|
+
"when chart names are not unique, supply chart_id instead of guessing by name",
|
|
3186
|
+
"successful create results must return a real backend chart_id",
|
|
3187
|
+
"upsert_charts[].visibility compiles to QingBI base visibleAuth only",
|
|
3188
|
+
"visibility-only updates keep the existing chart config and do not rewrite rawDataConfigDTO.authInfo",
|
|
3189
|
+
*_VISIBILITY_EXECUTION_NOTES,
|
|
3190
|
+
],
|
|
3190
3191
|
"minimal_example": {
|
|
3191
3192
|
"profile": "default",
|
|
3192
3193
|
"app_key": "APP_KEY",
|
|
@@ -3242,14 +3243,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
3242
3243
|
"dashStyleConfigBO": "dash_style_config",
|
|
3243
3244
|
},
|
|
3244
3245
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"], **deepcopy(_VISIBILITY_ALLOWED_VALUES)},
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3246
|
+
"execution_notes": [
|
|
3247
|
+
"use exactly one resource mode",
|
|
3248
|
+
"update mode: dash_key",
|
|
3249
|
+
"create mode: package_id + dash_name",
|
|
3250
|
+
"portal_apply uses replace semantics for sections",
|
|
3251
|
+
"when editing an existing portal, sections may be omitted to update only base info such as visibility, icon, or package",
|
|
3252
|
+
"remove a section by omitting it from the new sections list",
|
|
3253
|
+
"package_id is required when creating a new portal",
|
|
3254
|
+
"publish=false only guarantees draft and base-info updates; it does not claim live has changed",
|
|
3253
3255
|
"chart_ref resolves by chart_id first, then exact unique chart_name",
|
|
3254
3256
|
"view_ref resolves by view_key first, then exact unique view_name",
|
|
3255
3257
|
"position.pc/mobile is the canonical portal layout shape",
|
|
@@ -760,6 +760,8 @@ class ImportTools(ToolBase):
|
|
|
760
760
|
error_code="CONFIG_ERROR",
|
|
761
761
|
message="record_import_status_get accepts import_id or process_id_str, but not both at the same time",
|
|
762
762
|
extra={
|
|
763
|
+
"import_id": normalized_import_id,
|
|
764
|
+
"process_id_str": normalized_process_id,
|
|
763
765
|
"details": {
|
|
764
766
|
"fix_hint": "Use only one of `import_id` or `process_id_str`. You may pass `app_key` as an optional routing hint for direct method compatibility.",
|
|
765
767
|
}
|
|
@@ -770,6 +772,8 @@ class ImportTools(ToolBase):
|
|
|
770
772
|
error_code="CONFIG_ERROR",
|
|
771
773
|
message="record_import_status_get requires at least one selector: process_id_str, import_id, or app_key",
|
|
772
774
|
extra={
|
|
775
|
+
"import_id": normalized_import_id,
|
|
776
|
+
"process_id_str": normalized_process_id,
|
|
773
777
|
"details": {
|
|
774
778
|
"fix_hint": "Use `process_id_str` or `import_id` for a known import, or use only `app_key` to inspect the latest import in that app.",
|
|
775
779
|
}
|
|
@@ -783,6 +787,9 @@ class ImportTools(ToolBase):
|
|
|
783
787
|
if local_job is None and normalized_process_id:
|
|
784
788
|
matches = [item for item in self._job_store.list() if _normalize_optional_text(item.get("process_id_str")) == normalized_process_id]
|
|
785
789
|
local_job = matches[0] if len(matches) == 1 else None
|
|
790
|
+
effective_process_id = normalized_process_id
|
|
791
|
+
if effective_process_id is None and isinstance(local_job, dict):
|
|
792
|
+
effective_process_id = _normalize_optional_text(local_job.get("process_id_str"))
|
|
786
793
|
resolved_app_key = normalized_app_key
|
|
787
794
|
if not resolved_app_key and isinstance(local_job, dict):
|
|
788
795
|
resolved_app_key = str(local_job.get("app_key") or "").strip()
|
|
@@ -791,6 +798,8 @@ class ImportTools(ToolBase):
|
|
|
791
798
|
error_code="CONFIG_ERROR",
|
|
792
799
|
message="record_import_status_get could not determine app_key from the provided selector",
|
|
793
800
|
extra={
|
|
801
|
+
"import_id": normalized_import_id,
|
|
802
|
+
"process_id_str": effective_process_id,
|
|
794
803
|
"details": {
|
|
795
804
|
"fix_hint": "Use the original `app_key`, or call import status with the latest-import mode: only `app_key`.",
|
|
796
805
|
}
|
|
@@ -809,13 +818,18 @@ class ImportTools(ToolBase):
|
|
|
809
818
|
matched_record, matched_by = _match_import_record(
|
|
810
819
|
records,
|
|
811
820
|
local_job=local_job,
|
|
812
|
-
|
|
821
|
+
import_id=normalized_import_id,
|
|
822
|
+
process_id_str=effective_process_id,
|
|
813
823
|
)
|
|
814
824
|
if matched_record is None:
|
|
815
825
|
return self._failed_status_result(
|
|
816
826
|
error_code="IMPORT_STATUS_AMBIGUOUS",
|
|
817
827
|
message="could not uniquely resolve an import record from the provided identifiers",
|
|
818
|
-
extra={
|
|
828
|
+
extra={
|
|
829
|
+
"import_id": normalized_import_id,
|
|
830
|
+
"process_id_str": effective_process_id,
|
|
831
|
+
"matched_by": matched_by,
|
|
832
|
+
},
|
|
819
833
|
)
|
|
820
834
|
normalized_process = _normalize_optional_text(
|
|
821
835
|
matched_record.get("processIdStr") or matched_record.get("processId") or matched_record.get("process_id_str")
|
|
@@ -2079,6 +2093,7 @@ def _match_import_record(
|
|
|
2079
2093
|
records: list[JSONObject],
|
|
2080
2094
|
*,
|
|
2081
2095
|
local_job: dict[str, Any] | None,
|
|
2096
|
+
import_id: str | None,
|
|
2082
2097
|
process_id_str: str | None,
|
|
2083
2098
|
) -> tuple[JSONObject | None, str | None]:
|
|
2084
2099
|
if process_id_str:
|
|
@@ -2091,6 +2106,16 @@ def _match_import_record(
|
|
|
2091
2106
|
return exact[0], "process_id_str"
|
|
2092
2107
|
if len(exact) > 1:
|
|
2093
2108
|
return None, "process_id_str"
|
|
2109
|
+
if import_id:
|
|
2110
|
+
exact = [
|
|
2111
|
+
item
|
|
2112
|
+
for item in records
|
|
2113
|
+
if import_id in _extract_import_record_ids(item)
|
|
2114
|
+
]
|
|
2115
|
+
if len(exact) == 1:
|
|
2116
|
+
return exact[0], "import_id"
|
|
2117
|
+
if len(exact) > 1:
|
|
2118
|
+
return None, "import_id"
|
|
2094
2119
|
if isinstance(local_job, dict):
|
|
2095
2120
|
source_file_name = _normalize_optional_text(local_job.get("source_file_name"))
|
|
2096
2121
|
started_at = _parse_utc(local_job.get("started_at"))
|
|
@@ -2121,6 +2146,15 @@ def _match_import_record(
|
|
|
2121
2146
|
return None, None
|
|
2122
2147
|
|
|
2123
2148
|
|
|
2149
|
+
def _extract_import_record_ids(record: JSONObject) -> set[str]:
|
|
2150
|
+
identifiers: set[str] = set()
|
|
2151
|
+
for key in ("importId", "import_id", "dataImportId", "data_import_id"):
|
|
2152
|
+
normalized = _normalize_optional_text(record.get(key))
|
|
2153
|
+
if normalized:
|
|
2154
|
+
identifiers.add(normalized)
|
|
2155
|
+
return identifiers
|
|
2156
|
+
|
|
2157
|
+
|
|
2124
2158
|
def _parse_utc(value: Any) -> datetime | None:
|
|
2125
2159
|
text = _normalize_optional_text(value)
|
|
2126
2160
|
if text is None:
|
|
@@ -1025,6 +1025,7 @@ class RecordTools(ToolBase):
|
|
|
1025
1025
|
else:
|
|
1026
1026
|
required = bool(required_override) if required_override is not None else bool(field.required)
|
|
1027
1027
|
payload: JSONObject = {
|
|
1028
|
+
"field_id": field.que_id,
|
|
1028
1029
|
"title": field.que_title,
|
|
1029
1030
|
"kind": kind,
|
|
1030
1031
|
"required": required,
|
|
@@ -1371,7 +1371,7 @@ class TaskContextTools(ToolBase):
|
|
|
1371
1371
|
"record_id": task.get("record_id"),
|
|
1372
1372
|
"workflow_node_id": task.get("workflow_node_id"),
|
|
1373
1373
|
"workflow_node_name": task.get("workflow_node_name"),
|
|
1374
|
-
"initiator": record.get("apply_user"),
|
|
1374
|
+
"initiator": self._compact_initiator(record.get("apply_user")),
|
|
1375
1375
|
"actionable": task.get("actionable"),
|
|
1376
1376
|
},
|
|
1377
1377
|
"record_summary": {
|
|
@@ -1416,12 +1416,6 @@ class TaskContextTools(ToolBase):
|
|
|
1416
1416
|
},
|
|
1417
1417
|
},
|
|
1418
1418
|
}
|
|
1419
|
-
action_metadata = self._compact_task_action_metadata(capabilities)
|
|
1420
|
-
if action_metadata:
|
|
1421
|
-
compact["action_metadata"] = action_metadata
|
|
1422
|
-
editable_metadata = self._compact_task_editable_metadata(update_schema)
|
|
1423
|
-
if editable_metadata:
|
|
1424
|
-
compact["editable_metadata"] = editable_metadata
|
|
1425
1419
|
return compact
|
|
1426
1420
|
|
|
1427
1421
|
def _compact_task_action_metadata(self, capabilities: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1449,6 +1443,18 @@ class TaskContextTools(ToolBase):
|
|
|
1449
1443
|
metadata["warnings"] = warnings
|
|
1450
1444
|
return metadata
|
|
1451
1445
|
|
|
1446
|
+
def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
|
|
1447
|
+
if not isinstance(payload, dict):
|
|
1448
|
+
return None
|
|
1449
|
+
compact = {
|
|
1450
|
+
"uid": payload.get("uid"),
|
|
1451
|
+
"displayName": payload.get("displayName") or payload.get("name") or payload.get("nickName"),
|
|
1452
|
+
"email": payload.get("email"),
|
|
1453
|
+
"mobile": payload.get("mobile"),
|
|
1454
|
+
"headImg": payload.get("headImg"),
|
|
1455
|
+
}
|
|
1456
|
+
return {key: value for key, value in compact.items() if value not in (None, "", [])} or None
|
|
1457
|
+
|
|
1452
1458
|
def _task_app_name(self, detail: dict[str, Any], node_info: dict[str, Any]) -> Any:
|
|
1453
1459
|
for source in (detail, node_info):
|
|
1454
1460
|
for key in ("formTitle", "appName", "worksheetName", "appTitle"):
|
|
@@ -1550,12 +1556,6 @@ class TaskContextTools(ToolBase):
|
|
|
1550
1556
|
app_key = raw.get("appKey") or raw.get("app_key")
|
|
1551
1557
|
record_id = raw.get("rowRecordId") or raw.get("recordId") or raw.get("applyId")
|
|
1552
1558
|
workflow_node_id = raw.get("nodeId") or raw.get("auditNodeId")
|
|
1553
|
-
apply_user = raw.get("applyUser")
|
|
1554
|
-
if apply_user is None:
|
|
1555
|
-
user_uid = raw.get("applyUserUid")
|
|
1556
|
-
user_name = raw.get("applyUserName")
|
|
1557
|
-
if user_uid is not None or user_name is not None:
|
|
1558
|
-
apply_user = {"uid": user_uid, "name": user_name}
|
|
1559
1559
|
return {
|
|
1560
1560
|
"task_id": raw.get("id") or raw.get("taskId") or record_id,
|
|
1561
1561
|
"app_key": app_key,
|
|
@@ -1563,8 +1563,6 @@ class TaskContextTools(ToolBase):
|
|
|
1563
1563
|
"record_id": record_id,
|
|
1564
1564
|
"workflow_node_id": workflow_node_id,
|
|
1565
1565
|
"workflow_node_name": raw.get("nodeName") or raw.get("auditNodeName"),
|
|
1566
|
-
"title": raw.get("title") or raw.get("applyTitle") or raw.get("name") or raw.get("formTitle"),
|
|
1567
|
-
"apply_user": apply_user,
|
|
1568
1566
|
"apply_time": raw.get("applyTime") or raw.get("receiveTime"),
|
|
1569
1567
|
"task_box": task_box,
|
|
1570
1568
|
"flow_status": flow_status,
|