@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.4

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.
Files changed (56) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-user/SKILL.md +21 -12
  7. package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
  8. package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
  9. package/skills/qingflow-app-user/references/record-patterns.md +1 -1
  10. package/skills/qingflow-record-analysis/SKILL.md +44 -2
  11. package/skills/qingflow-record-insert/SKILL.md +3 -0
  12. package/skills/qingflow-record-update/SKILL.md +3 -0
  13. package/skills/qingflow-task-ops/SKILL.md +31 -10
  14. package/src/qingflow_mcp/__init__.py +33 -1
  15. package/src/qingflow_mcp/backend_client.py +109 -0
  16. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  17. package/src/qingflow_mcp/builder_facade/models.py +58 -9
  18. package/src/qingflow_mcp/builder_facade/service.py +1711 -240
  19. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  20. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  21. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  22. package/src/qingflow_mcp/cli/commands/builder.py +11 -3
  23. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  24. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  25. package/src/qingflow_mcp/cli/commands/task.py +701 -27
  26. package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
  27. package/src/qingflow_mcp/cli/context.py +3 -0
  28. package/src/qingflow_mcp/cli/formatters.py +424 -50
  29. package/src/qingflow_mcp/cli/interaction.py +72 -0
  30. package/src/qingflow_mcp/cli/main.py +11 -1
  31. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  32. package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
  33. package/src/qingflow_mcp/config.py +1 -1
  34. package/src/qingflow_mcp/errors.py +4 -4
  35. package/src/qingflow_mcp/export_store.py +14 -0
  36. package/src/qingflow_mcp/id_utils.py +49 -0
  37. package/src/qingflow_mcp/public_surface.py +16 -1
  38. package/src/qingflow_mcp/response_trim.py +394 -9
  39. package/src/qingflow_mcp/server.py +26 -0
  40. package/src/qingflow_mcp/server_app_builder.py +15 -1
  41. package/src/qingflow_mcp/server_app_user.py +113 -0
  42. package/src/qingflow_mcp/session_store.py +126 -21
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  44. package/src/qingflow_mcp/solution/executor.py +2 -2
  45. package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
  46. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  47. package/src/qingflow_mcp/tools/auth_tools.py +243 -9
  48. package/src/qingflow_mcp/tools/base.py +6 -2
  49. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  50. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  51. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  52. package/src/qingflow_mcp/tools/import_tools.py +78 -4
  53. package/src/qingflow_mcp/tools/record_tools.py +551 -165
  54. package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
  55. package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
  56. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -37,6 +37,13 @@ SAFE_REPAIRS = {
37
37
  "normalize_url_cells",
38
38
  }
39
39
  EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
40
+ IMPORT_STATUS_BY_PROCESS_STATUS = {
41
+ 1: "queued",
42
+ 2: "running",
43
+ 3: "succeeded",
44
+ 4: "failed",
45
+ 5: "partially_failed",
46
+ }
40
47
 
41
48
 
42
49
  class ImportTools(ToolBase):
@@ -783,6 +790,8 @@ class ImportTools(ToolBase):
783
790
  error_code="CONFIG_ERROR",
784
791
  message="record_import_status_get accepts import_id or process_id_str, but not both at the same time",
785
792
  extra={
793
+ "import_id": normalized_import_id,
794
+ "process_id_str": normalized_process_id,
786
795
  "details": {
787
796
  "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.",
788
797
  }
@@ -793,6 +802,8 @@ class ImportTools(ToolBase):
793
802
  error_code="CONFIG_ERROR",
794
803
  message="record_import_status_get requires at least one selector: process_id_str, import_id, or app_key",
795
804
  extra={
805
+ "import_id": normalized_import_id,
806
+ "process_id_str": normalized_process_id,
796
807
  "details": {
797
808
  "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.",
798
809
  }
@@ -806,6 +817,9 @@ class ImportTools(ToolBase):
806
817
  if local_job is None and normalized_process_id:
807
818
  matches = [item for item in self._job_store.list() if _normalize_optional_text(item.get("process_id_str")) == normalized_process_id]
808
819
  local_job = matches[0] if len(matches) == 1 else None
820
+ effective_process_id = normalized_process_id
821
+ if effective_process_id is None and isinstance(local_job, dict):
822
+ effective_process_id = _normalize_optional_text(local_job.get("process_id_str"))
809
823
  resolved_app_key = normalized_app_key
810
824
  if not resolved_app_key and isinstance(local_job, dict):
811
825
  resolved_app_key = str(local_job.get("app_key") or "").strip()
@@ -814,6 +828,8 @@ class ImportTools(ToolBase):
814
828
  error_code="CONFIG_ERROR",
815
829
  message="record_import_status_get could not determine app_key from the provided selector",
816
830
  extra={
831
+ "import_id": normalized_import_id,
832
+ "process_id_str": effective_process_id,
817
833
  "details": {
818
834
  "fix_hint": "Use the original `app_key`, or call import status with the latest-import mode: only `app_key`.",
819
835
  }
@@ -832,13 +848,18 @@ class ImportTools(ToolBase):
832
848
  matched_record, matched_by = _match_import_record(
833
849
  records,
834
850
  local_job=local_job,
835
- process_id_str=normalized_process_id,
851
+ import_id=normalized_import_id,
852
+ process_id_str=effective_process_id,
836
853
  )
837
854
  if matched_record is None:
838
855
  return self._failed_status_result(
839
856
  error_code="IMPORT_STATUS_AMBIGUOUS",
840
857
  message="could not uniquely resolve an import record from the provided identifiers",
841
- extra={"matched_by": matched_by},
858
+ extra={
859
+ "import_id": normalized_import_id,
860
+ "process_id_str": effective_process_id,
861
+ "matched_by": matched_by,
862
+ },
842
863
  )
843
864
  normalized_process = _normalize_optional_text(
844
865
  matched_record.get("processIdStr") or matched_record.get("processId") or matched_record.get("process_id_str")
@@ -852,13 +873,26 @@ class ImportTools(ToolBase):
852
873
  "process_id_str": normalized_process,
853
874
  },
854
875
  )
876
+ raw_process_status = matched_record.get("processStatus")
855
877
  total_rows = _coerce_int(matched_record.get("totalNumber") or matched_record.get("total_rows"))
856
878
  success_rows = _coerce_int(matched_record.get("successNum") or matched_record.get("success_rows"))
857
879
  failed_rows = _coerce_int(matched_record.get("errorNum") or matched_record.get("failed_rows"))
858
880
  progress = _coerce_int(matched_record.get("importPercentage") or matched_record.get("progress"))
881
+ normalized_status = _normalize_import_status(raw_process_status)
882
+ warnings: list[dict[str, str]] = []
883
+ if normalized_status in {"succeeded", "failed", "partially_failed"} and all(
884
+ value is None for value in (total_rows, success_rows, failed_rows)
885
+ ):
886
+ warnings.append(
887
+ {
888
+ "code": "IMPORT_STATUS_COUNTERS_MISSING",
889
+ "message": "backend import history returned a terminal process status without row counters",
890
+ }
891
+ )
859
892
  return {
860
893
  "ok": True,
861
- "status": _normalize_optional_text(matched_record.get("processStatus")) or "unknown",
894
+ "status": normalized_status,
895
+ "process_status": _coerce_int(raw_process_status),
862
896
  "app_key": resolved_app_key,
863
897
  "import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
864
898
  "process_id_str": normalized_process,
@@ -871,7 +905,7 @@ class ImportTools(ToolBase):
871
905
  "error_file_urls": _normalize_error_file_urls(matched_record.get("errorFileUrls")),
872
906
  "operate_time": matched_record.get("operateTime"),
873
907
  "operate_user": matched_record.get("operateUser"),
874
- "warnings": [],
908
+ "warnings": warnings,
875
909
  "verification": {
876
910
  "status_lookup_completed": True,
877
911
  "matched_by": matched_by,
@@ -2118,6 +2152,7 @@ def _match_import_record(
2118
2152
  records: list[JSONObject],
2119
2153
  *,
2120
2154
  local_job: dict[str, Any] | None,
2155
+ import_id: str | None,
2121
2156
  process_id_str: str | None,
2122
2157
  ) -> tuple[JSONObject | None, str | None]:
2123
2158
  if process_id_str:
@@ -2130,6 +2165,16 @@ def _match_import_record(
2130
2165
  return exact[0], "process_id_str"
2131
2166
  if len(exact) > 1:
2132
2167
  return None, "process_id_str"
2168
+ if import_id:
2169
+ exact = [
2170
+ item
2171
+ for item in records
2172
+ if import_id in _extract_import_record_ids(item)
2173
+ ]
2174
+ if len(exact) == 1:
2175
+ return exact[0], "import_id"
2176
+ if len(exact) > 1:
2177
+ return None, "import_id"
2133
2178
  if isinstance(local_job, dict):
2134
2179
  source_file_name = _normalize_optional_text(local_job.get("source_file_name"))
2135
2180
  started_at = _parse_utc(local_job.get("started_at"))
@@ -2160,6 +2205,15 @@ def _match_import_record(
2160
2205
  return None, None
2161
2206
 
2162
2207
 
2208
+ def _extract_import_record_ids(record: JSONObject) -> set[str]:
2209
+ identifiers: set[str] = set()
2210
+ for key in ("importId", "import_id", "dataImportId", "data_import_id"):
2211
+ normalized = _normalize_optional_text(record.get(key))
2212
+ if normalized:
2213
+ identifiers.add(normalized)
2214
+ return identifiers
2215
+
2216
+
2163
2217
  def _parse_utc(value: Any) -> datetime | None:
2164
2218
  text = _normalize_optional_text(value)
2165
2219
  if text is None:
@@ -2183,6 +2237,26 @@ def _coerce_int(value: Any) -> int | None:
2183
2237
  return None
2184
2238
 
2185
2239
 
2240
+ def _normalize_import_status(value: Any) -> str:
2241
+ status_code = _coerce_int(value)
2242
+ if status_code is not None:
2243
+ return IMPORT_STATUS_BY_PROCESS_STATUS.get(status_code, "unknown")
2244
+ text = str(value or "").strip().lower()
2245
+ if text in {"queued", "running", "succeeded", "failed", "partially_failed", "unknown"}:
2246
+ return text
2247
+ if text in {"line_up", "lineup"}:
2248
+ return "queued"
2249
+ if text in {"execute", "executing", "processing"}:
2250
+ return "running"
2251
+ if text in {"success", "completed"}:
2252
+ return "succeeded"
2253
+ if text in {"partly_fail", "partial_fail", "partially_fail", "partial_failed"}:
2254
+ return "partially_failed"
2255
+ if text in {"fail", "error"}:
2256
+ return "failed"
2257
+ return "unknown"
2258
+
2259
+
2186
2260
  def _normalize_error_file_urls(value: Any) -> list[str]:
2187
2261
  if isinstance(value, list):
2188
2262
  return [str(item).strip() for item in value if str(item).strip()]