@josephyan/qingflow-cli 0.2.0-beta.62 → 0.2.0-beta.64
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/builder_facade/models.py +225 -0
- package/src/qingflow_mcp/builder_facade/service.py +1416 -35
- package/src/qingflow_mcp/cli/commands/builder.py +58 -1
- package/src/qingflow_mcp/server_app_builder.py +25 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +244 -2
- package/src/qingflow_mcp/tools/custom_button_tools.py +177 -0
- package/src/qingflow_mcp/tools/import_tools.py +217 -90
|
@@ -361,12 +361,32 @@ class ImportTools(ToolBase):
|
|
|
361
361
|
)
|
|
362
362
|
effective_path = path
|
|
363
363
|
effective_local_check = local_check
|
|
364
|
-
auto_normalization =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
364
|
+
auto_normalization = None
|
|
365
|
+
try:
|
|
366
|
+
auto_normalization = self._maybe_auto_normalize_file(
|
|
367
|
+
source_path=path,
|
|
368
|
+
expected_columns=expected_columns,
|
|
369
|
+
template_header_profile=template_header_profile,
|
|
370
|
+
local_check=local_check,
|
|
371
|
+
)
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
effective_local_check = deepcopy(local_check)
|
|
374
|
+
effective_local_check["issues"].append(
|
|
375
|
+
_issue(
|
|
376
|
+
"IMPORT_AUTO_NORMALIZATION_FAILED",
|
|
377
|
+
f"Workbook compatibility normalization failed before backend verification: {exc}",
|
|
378
|
+
severity="error",
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
effective_local_check["warnings"].append(
|
|
382
|
+
{
|
|
383
|
+
"code": "IMPORT_AUTO_NORMALIZATION_FAILED",
|
|
384
|
+
"message": "Workbook compatibility normalization failed during local precheck; returning a structured verification failure instead of crashing.",
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
effective_local_check["local_precheck_passed"] = False
|
|
388
|
+
effective_local_check["can_import"] = False
|
|
389
|
+
effective_local_check["error_code"] = "IMPORT_VERIFICATION_FAILED"
|
|
370
390
|
if auto_normalization is not None:
|
|
371
391
|
effective_path = Path(str(auto_normalization["verified_file_path"]))
|
|
372
392
|
effective_local_check = self._local_verify(
|
|
@@ -456,7 +476,7 @@ class ImportTools(ToolBase):
|
|
|
456
476
|
return {
|
|
457
477
|
"ok": True,
|
|
458
478
|
"status": "success" if can_import else "failed",
|
|
459
|
-
"error_code": None if can_import else (local_check.get("error_code") or "IMPORT_VERIFICATION_FAILED"),
|
|
479
|
+
"error_code": None if can_import else (effective_local_check.get("error_code") or local_check.get("error_code") or "IMPORT_VERIFICATION_FAILED"),
|
|
460
480
|
"can_import": can_import,
|
|
461
481
|
"verification_id": verification_id,
|
|
462
482
|
"file_path": str(path.resolve()),
|
|
@@ -475,14 +495,14 @@ class ImportTools(ToolBase):
|
|
|
475
495
|
"import_auth_prechecked": precheck_known,
|
|
476
496
|
"import_auth_precheck_passed": True if precheck_known else None,
|
|
477
497
|
"import_auth_source": import_capability.get("auth_source"),
|
|
478
|
-
"local_precheck_passed": bool(
|
|
498
|
+
"local_precheck_passed": bool(effective_local_check["local_precheck_passed"]),
|
|
479
499
|
"backend_verification_passed": isinstance(backend_verification, dict)
|
|
480
500
|
and backend_verification.get("beingValidated", True) is not False,
|
|
481
501
|
"schema_fingerprint": schema_fingerprint,
|
|
482
502
|
"file_sha256": local_check["file_sha256"],
|
|
483
503
|
"verified_file_sha256": effective_local_check["file_sha256"] if effective_path != path else None,
|
|
484
504
|
"file_format": local_check["extension"],
|
|
485
|
-
"local_precheck_limited": bool(
|
|
505
|
+
"local_precheck_limited": bool(effective_local_check["local_precheck_limited"]),
|
|
486
506
|
"auto_normalized": effective_path != path,
|
|
487
507
|
},
|
|
488
508
|
}
|
|
@@ -911,44 +931,59 @@ class ImportTools(ToolBase):
|
|
|
911
931
|
base_result["can_import"] = False
|
|
912
932
|
base_result["error_code"] = "IMPORT_VERIFICATION_FAILED"
|
|
913
933
|
return base_result
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
base_result["issues"].extend(header_analysis["issues"])
|
|
922
|
-
base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
|
|
923
|
-
if not any(issue.get("severity") == "error" for issue in base_result["issues"]):
|
|
924
|
-
semantic_issues, semantic_warnings = self._inspect_semantic_cells(
|
|
925
|
-
profile=profile,
|
|
926
|
-
context=context,
|
|
927
|
-
sheet=sheet,
|
|
928
|
-
expected_columns=expected_columns,
|
|
929
|
-
field_index=field_index,
|
|
934
|
+
try:
|
|
935
|
+
sheet = workbook[workbook.sheetnames[0]]
|
|
936
|
+
header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
|
|
937
|
+
header_analysis = _analyze_headers(
|
|
938
|
+
header_row,
|
|
939
|
+
expected_columns,
|
|
940
|
+
allowed_titles=allowed_header_titles,
|
|
930
941
|
)
|
|
931
|
-
base_result["issues"].extend(
|
|
932
|
-
base_result["
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
942
|
+
base_result["issues"].extend(header_analysis["issues"])
|
|
943
|
+
base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
|
|
944
|
+
if not any(issue.get("severity") == "error" for issue in base_result["issues"]):
|
|
945
|
+
semantic_issues, semantic_warnings = self._inspect_semantic_cells(
|
|
946
|
+
profile=profile,
|
|
947
|
+
context=context,
|
|
948
|
+
sheet=sheet,
|
|
949
|
+
expected_columns=expected_columns,
|
|
950
|
+
field_index=field_index,
|
|
951
|
+
)
|
|
952
|
+
base_result["issues"].extend(semantic_issues)
|
|
953
|
+
base_result["warnings"].extend(semantic_warnings)
|
|
954
|
+
trailing_blank_rows = _count_trailing_blank_rows(sheet)
|
|
955
|
+
if trailing_blank_rows > 0:
|
|
956
|
+
base_result["warnings"].append(
|
|
957
|
+
{
|
|
958
|
+
"code": "TRAILING_BLANK_ROWS",
|
|
959
|
+
"message": f"Workbook contains {trailing_blank_rows} trailing blank rows that can be safely removed.",
|
|
960
|
+
}
|
|
961
|
+
)
|
|
962
|
+
base_result["repair_suggestions"].append("trim_trailing_blank_rows")
|
|
963
|
+
enum_suggestions = _find_enum_repairs(sheet, expected_columns)
|
|
964
|
+
if enum_suggestions:
|
|
965
|
+
base_result["warnings"].append(
|
|
966
|
+
{
|
|
967
|
+
"code": "ENUM_VALUE_NORMALIZATION_AVAILABLE",
|
|
968
|
+
"message": "Some enum-like cells can be normalized to exact template values without changing meaning.",
|
|
969
|
+
}
|
|
970
|
+
)
|
|
971
|
+
base_result["repair_suggestions"].append("normalize_enum_values")
|
|
972
|
+
base_result["repair_suggestions"] = sorted(set(base_result["repair_suggestions"]))
|
|
973
|
+
except Exception as exc:
|
|
974
|
+
base_result["issues"].append(
|
|
975
|
+
_issue(
|
|
976
|
+
"IMPORT_LOCAL_PRECHECK_FAILED",
|
|
977
|
+
f"Workbook content could not be fully inspected during local precheck: {exc}",
|
|
978
|
+
severity="error",
|
|
979
|
+
)
|
|
940
980
|
)
|
|
941
|
-
base_result["repair_suggestions"].append("trim_trailing_blank_rows")
|
|
942
|
-
enum_suggestions = _find_enum_repairs(sheet, expected_columns)
|
|
943
|
-
if enum_suggestions:
|
|
944
981
|
base_result["warnings"].append(
|
|
945
982
|
{
|
|
946
|
-
"code": "
|
|
947
|
-
"message": "
|
|
983
|
+
"code": "IMPORT_LOCAL_PRECHECK_FAILED",
|
|
984
|
+
"message": "Workbook local precheck encountered an unexpected compatibility problem; returning a structured verification failure instead of crashing.",
|
|
948
985
|
}
|
|
949
986
|
)
|
|
950
|
-
base_result["repair_suggestions"].append("normalize_enum_values")
|
|
951
|
-
base_result["repair_suggestions"] = sorted(set(base_result["repair_suggestions"]))
|
|
952
987
|
if any(issue.get("severity") == "error" for issue in base_result["issues"]):
|
|
953
988
|
base_result["local_precheck_passed"] = False
|
|
954
989
|
base_result["can_import"] = False
|
|
@@ -1199,58 +1234,49 @@ class ImportTools(ToolBase):
|
|
|
1199
1234
|
) -> dict[str, Any] | None:
|
|
1200
1235
|
if source_path.suffix.lower() != ".xlsx":
|
|
1201
1236
|
return None
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
return
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
normalized_workbook = Workbook()
|
|
1217
|
-
normalized_sheet = normalized_workbook.active
|
|
1218
|
-
normalized_sheet.title = sheet.title
|
|
1219
|
-
normalized_sheet.append(target_headers)
|
|
1220
|
-
last_nonblank_row = max(header_depth, sheet.max_row - trailing_blank_rows)
|
|
1221
|
-
for row_index in range(header_depth + 1, last_nonblank_row + 1):
|
|
1222
|
-
normalized_sheet.append(
|
|
1223
|
-
[sheet.cell(row=row_index, column=column_index).value for column_index in range(1, sheet.max_column + 1)]
|
|
1237
|
+
try:
|
|
1238
|
+
workbook = load_workbook(source_path, read_only=False, data_only=False)
|
|
1239
|
+
if not workbook.sheetnames:
|
|
1240
|
+
return None
|
|
1241
|
+
sheet = workbook[workbook.sheetnames[0]]
|
|
1242
|
+
rows = [list(row) for row in sheet.iter_rows(values_only=True)]
|
|
1243
|
+
header_depth = _infer_header_depth(sheet)
|
|
1244
|
+
return _build_auto_normalized_file(
|
|
1245
|
+
source_path=source_path,
|
|
1246
|
+
sheet_title=sheet.title,
|
|
1247
|
+
rows=rows,
|
|
1248
|
+
header_depth=header_depth,
|
|
1249
|
+
template_leaf_titles=template_header_profile.get("leaf_titles"),
|
|
1250
|
+
local_check=local_check,
|
|
1224
1251
|
)
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
}
|
|
1252
|
+
except Exception as exc:
|
|
1253
|
+
workbook = load_workbook(source_path, read_only=True, data_only=False)
|
|
1254
|
+
if not workbook.sheetnames:
|
|
1255
|
+
return None
|
|
1256
|
+
sheet = workbook[workbook.sheetnames[0]]
|
|
1257
|
+
rows = [list(row) for row in sheet.iter_rows(values_only=True)]
|
|
1258
|
+
header_depth = _infer_header_depth_from_rows(
|
|
1259
|
+
rows,
|
|
1260
|
+
template_header_profile=template_header_profile,
|
|
1261
|
+
local_check=local_check,
|
|
1236
1262
|
)
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1263
|
+
normalized = _build_auto_normalized_file(
|
|
1264
|
+
source_path=source_path,
|
|
1265
|
+
sheet_title=sheet.title,
|
|
1266
|
+
rows=rows,
|
|
1267
|
+
header_depth=header_depth,
|
|
1268
|
+
template_leaf_titles=template_header_profile.get("leaf_titles"),
|
|
1269
|
+
local_check=local_check,
|
|
1244
1270
|
)
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1271
|
+
if normalized is not None:
|
|
1272
|
+
normalized["warnings"].insert(
|
|
1273
|
+
0,
|
|
1274
|
+
{
|
|
1275
|
+
"code": "IMPORT_AUTO_NORMALIZATION_COMPATIBILITY_FALLBACK",
|
|
1276
|
+
"message": f"Workbook compatibility normalization retried in compatibility mode after a workbook parsing error: {exc}",
|
|
1277
|
+
},
|
|
1278
|
+
)
|
|
1279
|
+
return normalized
|
|
1254
1280
|
|
|
1255
1281
|
def _fetch_import_capability(self, context, app_key: str) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
1256
1282
|
try:
|
|
@@ -1748,6 +1774,107 @@ def _overlay_header_titles(actual_titles: list[str], template_leaf_titles: Any)
|
|
|
1748
1774
|
return normalized
|
|
1749
1775
|
|
|
1750
1776
|
|
|
1777
|
+
def _infer_header_depth_from_rows(
|
|
1778
|
+
rows: list[list[Any]],
|
|
1779
|
+
*,
|
|
1780
|
+
template_header_profile: dict[str, Any],
|
|
1781
|
+
local_check: dict[str, Any],
|
|
1782
|
+
) -> int:
|
|
1783
|
+
template_depth = max(1, int(template_header_profile.get("header_depth") or 1))
|
|
1784
|
+
header_depth = min(template_depth, max(1, len(rows)))
|
|
1785
|
+
if header_depth > 1:
|
|
1786
|
+
return header_depth
|
|
1787
|
+
if "normalize_headers" in (local_check.get("repair_suggestions") or []) and len(rows) >= 2:
|
|
1788
|
+
if any(_normalize_optional_text(value) for value in rows[1]):
|
|
1789
|
+
return 2
|
|
1790
|
+
return 1
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
def _extract_leaf_header_titles_from_rows(rows: list[list[Any]], header_depth: int) -> list[str]:
|
|
1794
|
+
titles: list[str] = []
|
|
1795
|
+
max_column = max((len(row) for row in rows[: max(1, header_depth)]), default=0)
|
|
1796
|
+
depth = max(1, min(header_depth, len(rows)))
|
|
1797
|
+
for column_index in range(max_column):
|
|
1798
|
+
selected = ""
|
|
1799
|
+
for row_index in range(depth - 1, -1, -1):
|
|
1800
|
+
value = rows[row_index][column_index] if column_index < len(rows[row_index]) else None
|
|
1801
|
+
text = _normalize_optional_text(value)
|
|
1802
|
+
if text:
|
|
1803
|
+
selected = text
|
|
1804
|
+
break
|
|
1805
|
+
titles.append(selected)
|
|
1806
|
+
return titles
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def _count_trailing_blank_rows_from_rows(rows: list[list[Any]], *, min_data_index: int = 1) -> int:
|
|
1810
|
+
count = 0
|
|
1811
|
+
for row in reversed(rows[min_data_index:]):
|
|
1812
|
+
if any(value not in (None, "") for value in row):
|
|
1813
|
+
break
|
|
1814
|
+
count += 1
|
|
1815
|
+
return count
|
|
1816
|
+
|
|
1817
|
+
|
|
1818
|
+
def _build_auto_normalized_file(
|
|
1819
|
+
*,
|
|
1820
|
+
source_path: Path,
|
|
1821
|
+
sheet_title: str,
|
|
1822
|
+
rows: list[list[Any]],
|
|
1823
|
+
header_depth: int,
|
|
1824
|
+
template_leaf_titles: Any,
|
|
1825
|
+
local_check: dict[str, Any],
|
|
1826
|
+
) -> dict[str, Any] | None:
|
|
1827
|
+
if not rows:
|
|
1828
|
+
return None
|
|
1829
|
+
normalized_header_depth = max(1, min(header_depth, len(rows)))
|
|
1830
|
+
trailing_blank_rows = _count_trailing_blank_rows_from_rows(rows, min_data_index=normalized_header_depth)
|
|
1831
|
+
if normalized_header_depth <= 1 and trailing_blank_rows <= 0:
|
|
1832
|
+
return None
|
|
1833
|
+
extracted_headers = _extract_leaf_header_titles_from_rows(rows, normalized_header_depth)
|
|
1834
|
+
target_headers = _overlay_header_titles(extracted_headers, template_leaf_titles)
|
|
1835
|
+
row_width = max(len(target_headers), max((len(row) for row in rows), default=0))
|
|
1836
|
+
if row_width <= 0:
|
|
1837
|
+
return None
|
|
1838
|
+
padded_headers = list(target_headers) + [""] * max(0, row_width - len(target_headers))
|
|
1839
|
+
verified_path = _resolve_verified_output_path(source_path)
|
|
1840
|
+
normalized_workbook = Workbook()
|
|
1841
|
+
normalized_sheet = normalized_workbook.active
|
|
1842
|
+
normalized_sheet.title = sheet_title
|
|
1843
|
+
normalized_sheet.append(padded_headers)
|
|
1844
|
+
last_nonblank_row = max(normalized_header_depth, len(rows) - trailing_blank_rows)
|
|
1845
|
+
for row in rows[normalized_header_depth:last_nonblank_row]:
|
|
1846
|
+
normalized_sheet.append(list(row) + [None] * max(0, row_width - len(row)))
|
|
1847
|
+
verified_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1848
|
+
normalized_workbook.save(verified_path)
|
|
1849
|
+
warnings: list[JSONObject] = []
|
|
1850
|
+
applied_repairs: list[str] = []
|
|
1851
|
+
if normalized_header_depth > 1:
|
|
1852
|
+
applied_repairs.append("normalize_headers")
|
|
1853
|
+
warnings.append(
|
|
1854
|
+
{
|
|
1855
|
+
"code": "IMPORT_HEADERS_AUTO_NORMALIZED",
|
|
1856
|
+
"message": f"Workbook used {normalized_header_depth} header rows; record_import_verify normalized it to a single leaf-header row automatically.",
|
|
1857
|
+
}
|
|
1858
|
+
)
|
|
1859
|
+
if trailing_blank_rows > 0:
|
|
1860
|
+
applied_repairs.append("trim_trailing_blank_rows")
|
|
1861
|
+
warnings.append(
|
|
1862
|
+
{
|
|
1863
|
+
"code": "TRAILING_BLANK_ROWS_AUTO_TRIMMED",
|
|
1864
|
+
"message": f"Removed {trailing_blank_rows} trailing blank rows before backend verification.",
|
|
1865
|
+
}
|
|
1866
|
+
)
|
|
1867
|
+
return {
|
|
1868
|
+
"verified_file_path": str(verified_path.resolve()),
|
|
1869
|
+
"header_titles": target_headers or padded_headers,
|
|
1870
|
+
"warnings": warnings,
|
|
1871
|
+
"applied_repairs": applied_repairs,
|
|
1872
|
+
"header_depth": normalized_header_depth,
|
|
1873
|
+
"trailing_blank_rows": trailing_blank_rows,
|
|
1874
|
+
"source_local_check": local_check,
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
|
|
1751
1878
|
def _count_trailing_blank_rows(sheet) -> int: # type: ignore[no-untyped-def]
|
|
1752
1879
|
count = 0
|
|
1753
1880
|
for row_index in range(sheet.max_row, 1, -1):
|