@josephyan/qingflow-app-user-mcp 0.2.0-beta.41 → 0.2.0-beta.43
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/tools/import_tools.py +55 -10
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.43
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.43 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,6 +4,7 @@ import hashlib
|
|
|
4
4
|
import json
|
|
5
5
|
import mimetypes
|
|
6
6
|
import shutil
|
|
7
|
+
from io import BytesIO
|
|
7
8
|
from copy import deepcopy
|
|
8
9
|
from datetime import datetime, timedelta, timezone
|
|
9
10
|
from pathlib import Path
|
|
@@ -197,13 +198,15 @@ class ImportTools(ToolBase):
|
|
|
197
198
|
|
|
198
199
|
def runner(session_profile, context):
|
|
199
200
|
expected_columns, schema_fingerprint = self._expected_import_columns(profile, context, app_key)
|
|
201
|
+
template_header_titles, header_warnings = self._load_template_header_titles(context, app_key)
|
|
200
202
|
local_check = self._local_verify(
|
|
201
203
|
path=path,
|
|
202
204
|
app_key=app_key,
|
|
203
205
|
expected_columns=expected_columns,
|
|
206
|
+
allowed_header_titles=template_header_titles,
|
|
204
207
|
schema_fingerprint=schema_fingerprint,
|
|
205
208
|
)
|
|
206
|
-
warnings = deepcopy(local_check["warnings"])
|
|
209
|
+
warnings = deepcopy(local_check["warnings"]) + header_warnings
|
|
207
210
|
issues = deepcopy(local_check["issues"])
|
|
208
211
|
can_import = bool(local_check["can_import"])
|
|
209
212
|
backend_verification = None
|
|
@@ -225,7 +228,8 @@ class ImportTools(ToolBase):
|
|
|
225
228
|
backend_verification = payload
|
|
226
229
|
else:
|
|
227
230
|
backend_verification = {}
|
|
228
|
-
|
|
231
|
+
being_validated = backend_verification.get("beingValidated", True)
|
|
232
|
+
if being_validated is False:
|
|
229
233
|
can_import = False
|
|
230
234
|
issues.append(
|
|
231
235
|
_issue(
|
|
@@ -285,7 +289,8 @@ class ImportTools(ToolBase):
|
|
|
285
289
|
"warnings": warnings,
|
|
286
290
|
"verification": {
|
|
287
291
|
"local_precheck_passed": bool(local_check["local_precheck_passed"]),
|
|
288
|
-
"backend_verification_passed":
|
|
292
|
+
"backend_verification_passed": isinstance(backend_verification, dict)
|
|
293
|
+
and backend_verification.get("beingValidated", True) is not False,
|
|
289
294
|
"schema_fingerprint": schema_fingerprint,
|
|
290
295
|
"file_sha256": local_check["file_sha256"],
|
|
291
296
|
"file_format": local_check["extension"],
|
|
@@ -629,6 +634,7 @@ class ImportTools(ToolBase):
|
|
|
629
634
|
path: Path,
|
|
630
635
|
app_key: str,
|
|
631
636
|
expected_columns: list[JSONObject],
|
|
637
|
+
allowed_header_titles: list[str] | None,
|
|
632
638
|
schema_fingerprint: str,
|
|
633
639
|
) -> dict[str, Any]:
|
|
634
640
|
extension = path.suffix.lower()
|
|
@@ -681,7 +687,11 @@ class ImportTools(ToolBase):
|
|
|
681
687
|
return base_result
|
|
682
688
|
sheet = workbook[workbook.sheetnames[0]]
|
|
683
689
|
header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
|
|
684
|
-
header_analysis = _analyze_headers(
|
|
690
|
+
header_analysis = _analyze_headers(
|
|
691
|
+
header_row,
|
|
692
|
+
expected_columns,
|
|
693
|
+
allowed_titles=allowed_header_titles,
|
|
694
|
+
)
|
|
685
695
|
base_result["issues"].extend(header_analysis["issues"])
|
|
686
696
|
base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
|
|
687
697
|
trailing_blank_rows = _count_trailing_blank_rows(sheet)
|
|
@@ -709,6 +719,31 @@ class ImportTools(ToolBase):
|
|
|
709
719
|
base_result["error_code"] = "IMPORT_VERIFICATION_FAILED"
|
|
710
720
|
return base_result
|
|
711
721
|
|
|
722
|
+
def _load_template_header_titles(self, context, app_key: str) -> tuple[list[str] | None, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
723
|
+
warnings: list[JSONObject] = []
|
|
724
|
+
try:
|
|
725
|
+
payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
|
|
726
|
+
template_url = _pick_template_url(payload)
|
|
727
|
+
if not template_url:
|
|
728
|
+
return None, warnings
|
|
729
|
+
content = self.backend.download_binary(template_url)
|
|
730
|
+
workbook = load_workbook(BytesIO(content), read_only=True, data_only=False)
|
|
731
|
+
if not workbook.sheetnames:
|
|
732
|
+
return None, warnings
|
|
733
|
+
sheet = workbook[workbook.sheetnames[0]]
|
|
734
|
+
header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
|
|
735
|
+
titles = [_normalize_optional_text(value) for value in header_row]
|
|
736
|
+
normalized_titles = [title for title in titles if title]
|
|
737
|
+
return normalized_titles or None, warnings
|
|
738
|
+
except Exception:
|
|
739
|
+
warnings.append(
|
|
740
|
+
{
|
|
741
|
+
"code": "IMPORT_TEMPLATE_HEADER_UNAVAILABLE",
|
|
742
|
+
"message": "Official template headers could not be loaded during local precheck; falling back to applicant writable columns only.",
|
|
743
|
+
}
|
|
744
|
+
)
|
|
745
|
+
return None, warnings
|
|
746
|
+
|
|
712
747
|
def _failed_template_result(
|
|
713
748
|
self,
|
|
714
749
|
*,
|
|
@@ -919,8 +954,18 @@ def _issue(code: str, message: str, *, severity: str, repairable: bool = False,
|
|
|
919
954
|
return payload
|
|
920
955
|
|
|
921
956
|
|
|
922
|
-
def _analyze_headers(
|
|
957
|
+
def _analyze_headers(
|
|
958
|
+
header_row: list[Any],
|
|
959
|
+
expected_columns: list[JSONObject],
|
|
960
|
+
*,
|
|
961
|
+
allowed_titles: list[str] | None = None,
|
|
962
|
+
) -> dict[str, Any]:
|
|
923
963
|
expected_by_key = {_normalize_header_key(item["title"]): item for item in expected_columns}
|
|
964
|
+
allowed_by_key = (
|
|
965
|
+
{_normalize_header_key(title): title for title in allowed_titles if _normalize_optional_text(title)}
|
|
966
|
+
if allowed_titles
|
|
967
|
+
else {key: item["title"] for key, item in expected_by_key.items()}
|
|
968
|
+
)
|
|
924
969
|
seen: dict[str, int] = {}
|
|
925
970
|
actual_headers: list[str] = []
|
|
926
971
|
for item in header_row:
|
|
@@ -932,8 +977,8 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
|
|
|
932
977
|
key = _normalize_header_key(text)
|
|
933
978
|
seen[key] = seen.get(key, 0) + 1
|
|
934
979
|
actual_keys = {key for key, count in seen.items() if key and count > 0}
|
|
935
|
-
missing = [
|
|
936
|
-
extra = [text for text in actual_headers if text and _normalize_header_key(text) not in
|
|
980
|
+
missing = [title for key, title in allowed_by_key.items() if key not in actual_keys]
|
|
981
|
+
extra = [text for text in actual_headers if text and _normalize_header_key(text) not in allowed_by_key]
|
|
937
982
|
duplicates = [text for text in actual_headers if text and seen.get(_normalize_header_key(text), 0) > 1]
|
|
938
983
|
issues: list[JSONObject] = []
|
|
939
984
|
repair_suggestions: list[str] = []
|
|
@@ -947,9 +992,9 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
|
|
|
947
992
|
for text in actual_headers:
|
|
948
993
|
if not text:
|
|
949
994
|
continue
|
|
950
|
-
|
|
951
|
-
if
|
|
952
|
-
normalized_changes.append((text,
|
|
995
|
+
canonical = allowed_by_key.get(_normalize_header_key(text))
|
|
996
|
+
if canonical and canonical != text:
|
|
997
|
+
normalized_changes.append((text, canonical))
|
|
953
998
|
if normalized_changes:
|
|
954
999
|
repair_suggestions.append("normalize_headers")
|
|
955
1000
|
return {"issues": issues, "repair_suggestions": repair_suggestions}
|