@josephyan/qingflow-app-user-mcp 0.2.0-beta.41 → 0.2.0-beta.42
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@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.42
|
|
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.42 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
|
|
@@ -629,6 +632,7 @@ class ImportTools(ToolBase):
|
|
|
629
632
|
path: Path,
|
|
630
633
|
app_key: str,
|
|
631
634
|
expected_columns: list[JSONObject],
|
|
635
|
+
allowed_header_titles: list[str] | None,
|
|
632
636
|
schema_fingerprint: str,
|
|
633
637
|
) -> dict[str, Any]:
|
|
634
638
|
extension = path.suffix.lower()
|
|
@@ -681,7 +685,11 @@ class ImportTools(ToolBase):
|
|
|
681
685
|
return base_result
|
|
682
686
|
sheet = workbook[workbook.sheetnames[0]]
|
|
683
687
|
header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
|
|
684
|
-
header_analysis = _analyze_headers(
|
|
688
|
+
header_analysis = _analyze_headers(
|
|
689
|
+
header_row,
|
|
690
|
+
expected_columns,
|
|
691
|
+
allowed_titles=allowed_header_titles,
|
|
692
|
+
)
|
|
685
693
|
base_result["issues"].extend(header_analysis["issues"])
|
|
686
694
|
base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
|
|
687
695
|
trailing_blank_rows = _count_trailing_blank_rows(sheet)
|
|
@@ -709,6 +717,31 @@ class ImportTools(ToolBase):
|
|
|
709
717
|
base_result["error_code"] = "IMPORT_VERIFICATION_FAILED"
|
|
710
718
|
return base_result
|
|
711
719
|
|
|
720
|
+
def _load_template_header_titles(self, context, app_key: str) -> tuple[list[str] | None, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
721
|
+
warnings: list[JSONObject] = []
|
|
722
|
+
try:
|
|
723
|
+
payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
|
|
724
|
+
template_url = _pick_template_url(payload)
|
|
725
|
+
if not template_url:
|
|
726
|
+
return None, warnings
|
|
727
|
+
content = self.backend.download_binary(template_url)
|
|
728
|
+
workbook = load_workbook(BytesIO(content), read_only=True, data_only=False)
|
|
729
|
+
if not workbook.sheetnames:
|
|
730
|
+
return None, warnings
|
|
731
|
+
sheet = workbook[workbook.sheetnames[0]]
|
|
732
|
+
header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
|
|
733
|
+
titles = [_normalize_optional_text(value) for value in header_row]
|
|
734
|
+
normalized_titles = [title for title in titles if title]
|
|
735
|
+
return normalized_titles or None, warnings
|
|
736
|
+
except Exception:
|
|
737
|
+
warnings.append(
|
|
738
|
+
{
|
|
739
|
+
"code": "IMPORT_TEMPLATE_HEADER_UNAVAILABLE",
|
|
740
|
+
"message": "Official template headers could not be loaded during local precheck; falling back to applicant writable columns only.",
|
|
741
|
+
}
|
|
742
|
+
)
|
|
743
|
+
return None, warnings
|
|
744
|
+
|
|
712
745
|
def _failed_template_result(
|
|
713
746
|
self,
|
|
714
747
|
*,
|
|
@@ -919,8 +952,18 @@ def _issue(code: str, message: str, *, severity: str, repairable: bool = False,
|
|
|
919
952
|
return payload
|
|
920
953
|
|
|
921
954
|
|
|
922
|
-
def _analyze_headers(
|
|
955
|
+
def _analyze_headers(
|
|
956
|
+
header_row: list[Any],
|
|
957
|
+
expected_columns: list[JSONObject],
|
|
958
|
+
*,
|
|
959
|
+
allowed_titles: list[str] | None = None,
|
|
960
|
+
) -> dict[str, Any]:
|
|
923
961
|
expected_by_key = {_normalize_header_key(item["title"]): item for item in expected_columns}
|
|
962
|
+
allowed_by_key = (
|
|
963
|
+
{_normalize_header_key(title): title for title in allowed_titles if _normalize_optional_text(title)}
|
|
964
|
+
if allowed_titles
|
|
965
|
+
else {key: item["title"] for key, item in expected_by_key.items()}
|
|
966
|
+
)
|
|
924
967
|
seen: dict[str, int] = {}
|
|
925
968
|
actual_headers: list[str] = []
|
|
926
969
|
for item in header_row:
|
|
@@ -932,8 +975,8 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
|
|
|
932
975
|
key = _normalize_header_key(text)
|
|
933
976
|
seen[key] = seen.get(key, 0) + 1
|
|
934
977
|
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
|
|
978
|
+
missing = [title for key, title in allowed_by_key.items() if key not in actual_keys]
|
|
979
|
+
extra = [text for text in actual_headers if text and _normalize_header_key(text) not in allowed_by_key]
|
|
937
980
|
duplicates = [text for text in actual_headers if text and seen.get(_normalize_header_key(text), 0) > 1]
|
|
938
981
|
issues: list[JSONObject] = []
|
|
939
982
|
repair_suggestions: list[str] = []
|
|
@@ -947,9 +990,9 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
|
|
|
947
990
|
for text in actual_headers:
|
|
948
991
|
if not text:
|
|
949
992
|
continue
|
|
950
|
-
|
|
951
|
-
if
|
|
952
|
-
normalized_changes.append((text,
|
|
993
|
+
canonical = allowed_by_key.get(_normalize_header_key(text))
|
|
994
|
+
if canonical and canonical != text:
|
|
995
|
+
normalized_changes.append((text, canonical))
|
|
953
996
|
if normalized_changes:
|
|
954
997
|
repair_suggestions.append("normalize_headers")
|
|
955
998
|
return {"issues": issues, "repair_suggestions": repair_suggestions}
|