@josephyan/qingflow-app-user-mcp 1.1.23 → 1.1.24

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-user-mcp@1.1.23
6
+ npm install @josephyan/qingflow-app-user-mcp@1.1.24
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@1.1.23 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.1.24 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "1.1.23",
3
+ "version": "1.1.24",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.1.23"
7
+ version = "1.1.24"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2259,6 +2259,7 @@ class AiBuilderTools(ToolBase):
2259
2259
  app_shell_apply_ms = 0
2260
2260
  relation_compile_ms = 0
2261
2261
  app_field_apply_ms = 0
2262
+ relation_repair_ms = 0
2262
2263
  inline_layout_fallback_ms = 0
2263
2264
  child_schema_write_ms = 0
2264
2265
  child_inline_layout_ms = 0
@@ -2307,6 +2308,7 @@ class AiBuilderTools(ToolBase):
2307
2308
  relation_compile_ms=relation_compile_ms,
2308
2309
  app_field_apply_ms=app_field_apply_ms,
2309
2310
  app_schema_parallel_wall_ms=app_field_apply_ms,
2311
+ relation_repair_parallel_wall_ms=relation_repair_ms,
2310
2312
  native_inline_layout_ms=child_inline_layout_ms,
2311
2313
  inline_layout_fallback_ms=inline_layout_fallback_ms,
2312
2314
  child_schema_write_ms=child_schema_write_ms,
@@ -2930,6 +2932,125 @@ class AiBuilderTools(ToolBase):
2930
2932
  item if isinstance(item, dict) else _multi_app_item_failure(index, apps[index], "APP_ITEM_NOT_PROCESSED", "app item was not processed")
2931
2933
  for index, item in enumerate(final_items_by_index)
2932
2934
  ]
2935
+
2936
+ def relation_repair_patches(item: JSONObject) -> list[JSONObject]:
2937
+ details = item.get("details") if isinstance(item.get("details"), dict) else {}
2938
+ repair_plan = details.get("relation_repair_plan") if isinstance(details.get("relation_repair_plan"), list) else []
2939
+ patches: list[JSONObject] = []
2940
+ for plan_item in repair_plan:
2941
+ if not isinstance(plan_item, dict):
2942
+ continue
2943
+ patch = plan_item.get("suggested_patch")
2944
+ if isinstance(patch, dict):
2945
+ patches.append(deepcopy(patch))
2946
+ return patches
2947
+
2948
+ relation_repair_jobs: list[JSONObject] = []
2949
+ for item in final_items:
2950
+ if item.get("status") != "partial_success":
2951
+ continue
2952
+ app_key = str(item.get("app_key") or "").strip()
2953
+ patches = relation_repair_patches(item)
2954
+ if app_key and patches:
2955
+ relation_repair_jobs.append({"item": item, "app_key": app_key, "patches": patches})
2956
+
2957
+ def run_relation_repair_job(job: JSONObject) -> JSONObject:
2958
+ app_key = str(job.get("app_key") or "")
2959
+ patches = list(job.get("patches") or [])
2960
+ repair_started_at = time.perf_counter()
2961
+ try:
2962
+ repair_result = self._app_schema_apply_once(
2963
+ profile=profile,
2964
+ app_key=app_key,
2965
+ package_id=None,
2966
+ app_name="",
2967
+ app_title="",
2968
+ icon="",
2969
+ color="",
2970
+ visibility=None,
2971
+ publish=publish,
2972
+ add_fields=[],
2973
+ update_fields=patches,
2974
+ remove_fields=[],
2975
+ inline_form_layout_sections=None,
2976
+ allow_inline_layout_fallback=False,
2977
+ )
2978
+ public_repair = _publicize_package_fields(repair_result)
2979
+ except Exception as error: # pragma: no cover - defensive guard for unexpected worker failures
2980
+ return {"job": job, "elapsed_ms": _elapsed_ms(repair_started_at), "exception": repr(error)}
2981
+ return {
2982
+ "job": job,
2983
+ "elapsed_ms": _elapsed_ms(repair_started_at),
2984
+ "public_repair": public_repair,
2985
+ }
2986
+
2987
+ if relation_repair_jobs:
2988
+ repair_phase_started_at = time.perf_counter()
2989
+ repair_workers = worker_count(len(relation_repair_jobs))
2990
+ repair_outputs: list[JSONObject] = []
2991
+ with ThreadPoolExecutor(max_workers=repair_workers) as executor:
2992
+ futures = [executor.submit(run_relation_repair_job, job) for job in relation_repair_jobs]
2993
+ for future in as_completed(futures):
2994
+ repair_outputs.append(future.result())
2995
+ relation_repair_ms += _elapsed_ms(repair_phase_started_at)
2996
+ for output in repair_outputs:
2997
+ job = output.get("job") if isinstance(output.get("job"), dict) else {}
2998
+ original = job.get("item") if isinstance(job.get("item"), dict) else {}
2999
+ if not original:
3000
+ continue
3001
+ index = _coerce_nonnegative_int(original.get("index"))
3002
+ if index is None or index >= len(final_items):
3003
+ continue
3004
+ repaired_item = deepcopy(original)
3005
+ exception = output.get("exception")
3006
+ if exception:
3007
+ repaired_item["relation_repair"] = {
3008
+ "attempted": True,
3009
+ "status": "failed",
3010
+ "error_code": "RELATION_REPAIR_EXCEPTION",
3011
+ "message": str(exception),
3012
+ }
3013
+ final_items[index] = repaired_item
3014
+ continue
3015
+ public_repair = output.get("public_repair") if isinstance(output.get("public_repair"), dict) else {}
3016
+ merge_child_duration(public_repair)
3017
+ if _schema_apply_result_has_write(public_repair):
3018
+ any_write_executed = True
3019
+ if _schema_apply_result_may_have_write(public_repair):
3020
+ any_write_may_have_succeeded = True
3021
+ repair_payload: JSONObject = {
3022
+ "attempted": True,
3023
+ "status": public_repair.get("status"),
3024
+ "error_code": public_repair.get("error_code"),
3025
+ "message": public_repair.get("message"),
3026
+ "verified": bool(public_repair.get("verified")),
3027
+ "field_diff": public_repair.get("field_diff") or {},
3028
+ **({"verification": public_repair.get("verification")} if isinstance(public_repair.get("verification"), dict) else {}),
3029
+ **({"details": public_repair.get("details")} if isinstance(public_repair.get("details"), dict) else {}),
3030
+ **({"duration_breakdown_ms": public_repair.get("duration_breakdown_ms")} if isinstance(public_repair.get("duration_breakdown_ms"), dict) else {}),
3031
+ }
3032
+ repaired_item["relation_repair"] = repair_payload
3033
+ repaired_item["field_diff"] = _merge_schema_field_diffs(repaired_item.get("field_diff") or {}, public_repair.get("field_diff") or {})
3034
+ repaired_item["field_diff_details"] = _merge_schema_field_diffs(
3035
+ repaired_item.get("field_diff_details") or {},
3036
+ public_repair.get("field_diff_details") or {},
3037
+ )
3038
+ if public_repair.get("status") == "success" and bool(public_repair.get("verified")):
3039
+ repaired_item["status"] = "success"
3040
+ repaired_item["error_code"] = None
3041
+ repaired_item["message"] = "applied schema patch; relation readback repaired"
3042
+ repaired_item["published"] = bool(public_repair.get("published"))
3043
+ repaired_item["verified"] = True
3044
+ original_verification = repaired_item.get("verification") if isinstance(repaired_item.get("verification"), dict) else {}
3045
+ repair_verification = public_repair.get("verification") if isinstance(public_repair.get("verification"), dict) else {}
3046
+ merged_verification = {**deepcopy(original_verification), **deepcopy(repair_verification)}
3047
+ repaired_item["verification"] = merged_verification
3048
+ details = repaired_item.get("details") if isinstance(repaired_item.get("details"), dict) else {}
3049
+ details = deepcopy(details)
3050
+ details.pop("relation_repair_plan", None)
3051
+ repaired_item["details"] = details
3052
+ final_items[index] = repaired_item
3053
+
2933
3054
  pending_readback = sum(1 for item in final_items if item.get("status") == "pending_readback")
2934
3055
  succeeded = sum(1 for item in final_items if item.get("status") in {"success", "partial_success"})
2935
3056
  failed = sum(1 for item in final_items if item.get("status") == "failed")