@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26

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 (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -17,7 +17,7 @@ from mcp.server.fastmcp import FastMCP
17
17
  from openpyxl import Workbook, load_workbook
18
18
 
19
19
  from ..config import DEFAULT_PROFILE
20
- from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, message_looks_like_invalid_token
20
+ from ..errors import QingflowApiError
21
21
  from ..import_store import ImportJobStore, ImportVerificationStore
22
22
  from ..json_types import JSONObject
23
23
  from .app_tools import _derive_import_capability
@@ -75,12 +75,11 @@ class ImportTools(ToolBase):
75
75
  """注册当前工具到 MCP 服务。"""
76
76
  @mcp.tool()
77
77
  def record_import_schema_get(
78
- profile: str = DEFAULT_PROFILE,
79
78
  app_key: str = "",
80
79
  output_profile: str = "normal",
81
80
  ) -> dict[str, Any]:
82
81
  return self.record_import_schema_get(
83
- profile=profile,
82
+ profile=DEFAULT_PROFILE,
84
83
  app_key=app_key,
85
84
  output_profile=output_profile,
86
85
  )
@@ -194,22 +193,6 @@ class ImportTools(ToolBase):
194
193
 
195
194
  def runner(session_profile, context):
196
195
  import_capability, import_warnings = self._fetch_import_capability(context, app_key)
197
- if import_capability.get("can_import") is False and import_capability.get("auth_source") != "unknown":
198
- return {
199
- "ok": False,
200
- "status": "failed",
201
- "app_key": app_key,
202
- "ws_id": session_profile.selected_ws_id,
203
- "request_route": self.backend.describe_route(context),
204
- "error_code": "IMPORT_AUTH_PRECHECK_FAILED",
205
- "message": "the current user does not have import permission for this app",
206
- "warnings": import_warnings,
207
- "import_capability": import_capability,
208
- "verification": {
209
- "import_auth_prechecked": True,
210
- "import_auth_precheck_passed": False,
211
- },
212
- }
213
196
  _index, expected_columns, schema_fingerprint = self._resolve_import_schema_bundle(
214
197
  profile,
215
198
  context,
@@ -225,15 +208,8 @@ class ImportTools(ToolBase):
225
208
  }
226
209
  if isinstance(column.get("options"), list) and column.get("options"):
227
210
  payload["options"] = column["options"]
228
- if column["write_kind"] == "member":
229
- payload["import_value_format"] = "member_email"
230
- payload["format_hint"] = "Import files must use member email values."
231
- elif column["write_kind"] == "relation":
232
- payload["import_value_format"] = "target_apply_id"
233
- payload["format_hint"] = "Import files must use the target record apply_id."
234
- elif column["write_kind"] == "department":
235
- payload["import_value_format"] = "department_name"
236
- payload["format_hint"] = "Import files may use a department name within the field candidate scope."
211
+ if bool(column.get("requires_lookup")):
212
+ payload["accepts_natural_input"] = True
237
213
  if bool(column.get("requires_upload")):
238
214
  payload["requires_upload"] = True
239
215
  if isinstance(column.get("target_app_key"), str):
@@ -275,21 +251,6 @@ class ImportTools(ToolBase):
275
251
 
276
252
  def runner(session_profile, context):
277
253
  import_capability, import_warnings = self._fetch_import_capability(context, app_key)
278
- if import_capability.get("can_import") is False and import_capability.get("auth_source") != "unknown":
279
- return self._failed_template_result(
280
- app_key=app_key,
281
- error_code="IMPORT_AUTH_PRECHECK_FAILED",
282
- message="the current user does not have import permission for this app",
283
- request_route=self.backend.describe_route(context),
284
- extra={
285
- "warnings": import_warnings,
286
- "import_capability": import_capability,
287
- "verification": {
288
- "import_auth_prechecked": True,
289
- "import_auth_precheck_passed": False,
290
- },
291
- },
292
- )
293
254
  field_index, expected_columns, schema_fingerprint = self._resolve_import_schema_bundle(
294
255
  profile,
295
256
  context,
@@ -299,30 +260,12 @@ class ImportTools(ToolBase):
299
260
  try:
300
261
  payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
301
262
  except QingflowApiError as exc:
302
- can_generate_local_template = bool(expected_columns) and (
303
- import_capability.get("auth_source") == "apply_auth"
304
- or (
305
- import_capability.get("auth_source") == "unknown"
306
- and not is_auth_like_error(exc)
307
- and backend_code_int(exc) in {40002, 40027}
308
- )
309
- )
310
- if can_generate_local_template:
263
+ if import_capability.get("auth_source") == "apply_auth":
311
264
  downloaded_to_path = self._write_local_template(
312
265
  expected_columns=expected_columns,
313
266
  destination_hint=download_to_path,
314
267
  app_key=app_key,
315
268
  )
316
- template_warning = {
317
- "code": "IMPORT_TEMPLATE_LOCAL_FALLBACK",
318
- "message": "Official template download requires data management permission; MCP generated a local applicant-import template instead.",
319
- }
320
- if import_capability.get("auth_source") == "unknown":
321
- template_warning = {
322
- "code": "IMPORT_TEMPLATE_LOCAL_FALLBACK_AUTH_UNKNOWN",
323
- "message": "Official template download was permission-restricted and import permission could not be prechecked; MCP generated a local applicant-import template from readable applicant fields.",
324
- }
325
- _copy_api_error_fields(template_warning, exc)
326
269
  return {
327
270
  "ok": True,
328
271
  "status": "partial_success",
@@ -333,13 +276,18 @@ class ImportTools(ToolBase):
333
276
  "downloaded_to_path": downloaded_to_path,
334
277
  "expected_columns": expected_columns,
335
278
  "schema_fingerprint": schema_fingerprint,
336
- "warnings": import_warnings + [template_warning],
279
+ "warnings": import_warnings
280
+ + [
281
+ {
282
+ "code": "IMPORT_TEMPLATE_LOCAL_FALLBACK",
283
+ "message": "Official template download requires data management permission; MCP generated a local applicant-import template instead.",
284
+ }
285
+ ],
337
286
  "verification": {
338
287
  "schema_fingerprint": schema_fingerprint,
339
288
  "template_url_resolved": False,
340
289
  "template_downloaded": True,
341
290
  "template_source": "local_generated",
342
- "import_auth_prechecked": import_capability.get("auth_source") != "unknown",
343
291
  },
344
292
  }
345
293
  return self._failed_template_result(
@@ -500,7 +448,6 @@ class ImportTools(ToolBase):
500
448
  issues = deepcopy(effective_local_check["issues"])
501
449
  can_import = bool(effective_local_check["can_import"])
502
450
  backend_verification = None
503
- backend_failure_error_code = None
504
451
  if can_import:
505
452
  try:
506
453
  payload = self.backend.request_multipart(
@@ -531,25 +478,19 @@ class ImportTools(ToolBase):
531
478
  )
532
479
  except QingflowApiError as exc:
533
480
  can_import = False
534
- backend_error_code = _import_permission_error_code(
535
- exc,
536
- permission_code="IMPORT_VERIFICATION_UNAUTHORIZED",
537
- default="BACKEND_IMPORT_VERIFICATION_FAILED",
538
- )
539
- backend_failure_error_code = backend_error_code
540
481
  issues.append(
541
482
  _issue(
542
- backend_error_code,
483
+ "BACKEND_IMPORT_VERIFICATION_FAILED",
543
484
  exc.message or "Backend import verification failed.",
544
485
  severity="error",
545
486
  )
546
487
  )
547
- warning = {
548
- "code": backend_error_code,
549
- "message": "Backend verification failed; the file cannot be imported until verification succeeds.",
550
- }
551
- _copy_api_error_fields(warning, exc)
552
- warnings.append(warning)
488
+ warnings.append(
489
+ {
490
+ "code": "IMPORT_VERIFICATION_FAILED",
491
+ "message": "Backend verification failed; the file cannot be imported until verification succeeds.",
492
+ }
493
+ )
553
494
  verification_id = str(uuid4())
554
495
  verification_payload = {
555
496
  "id": verification_id,
@@ -576,11 +517,9 @@ class ImportTools(ToolBase):
576
517
  }
577
518
  self._verification_store.put(verification_id, verification_payload)
578
519
  return {
579
- "ok": can_import,
520
+ "ok": True,
580
521
  "status": "success" if can_import else "failed",
581
- "error_code": None
582
- if can_import
583
- else (effective_local_check.get("error_code") or local_check.get("error_code") or backend_failure_error_code or "IMPORT_VERIFICATION_FAILED"),
522
+ "error_code": None if can_import else (effective_local_check.get("error_code") or local_check.get("error_code") or "IMPORT_VERIFICATION_FAILED"),
584
523
  "can_import": can_import,
585
524
  "verification_id": verification_id,
586
525
  "file_path": str(path.resolve()),
@@ -614,11 +553,7 @@ class ImportTools(ToolBase):
614
553
  try:
615
554
  return self._run(profile, runner)
616
555
  except RuntimeError as exc:
617
- return self._runtime_error_as_result(
618
- exc,
619
- error_code="IMPORT_VERIFICATION_FAILED",
620
- extra={"can_import": _runtime_import_can_import_value(exc)},
621
- )
556
+ return self._runtime_error_as_result(exc, error_code="IMPORT_VERIFICATION_FAILED", extra={"can_import": False})
622
557
 
623
558
  @tool_cn_name("导入修复")
624
559
  def record_import_repair_local(
@@ -668,8 +603,7 @@ class ImportTools(ToolBase):
668
603
  applied_repairs: list[str] = []
669
604
  skipped_repairs: list[str] = []
670
605
  if "normalize_headers" in normalized_repairs:
671
- repair_header_columns = _repair_header_columns_from_stored_precheck(stored, expected_columns)
672
- if _repair_headers(sheet, repair_header_columns):
606
+ if _repair_headers(sheet, expected_columns):
673
607
  applied_repairs.append("normalize_headers")
674
608
  else:
675
609
  skipped_repairs.append("normalize_headers")
@@ -753,17 +687,11 @@ class ImportTools(ToolBase):
753
687
  return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verification_id does not belong to the requested app")
754
688
  if not bool(stored.get("can_import")):
755
689
  return self._failed_start_result(error_code="IMPORT_VERIFICATION_FAILED", message="verification_id is not importable", extra={"accepted": False})
756
- source_local_precheck = stored.get("source_local_precheck")
757
- source_precheck_passed = isinstance(source_local_precheck, dict) and bool(source_local_precheck.get("can_import"))
758
- if source_precheck_passed:
759
- current_path = Path(str(stored.get("source_file_path") or stored["file_path"]))
760
- expected_sha256 = stored.get("file_sha256")
761
- else:
762
- current_path = Path(str(stored.get("verified_file_path") or stored.get("source_file_path") or stored["file_path"]))
763
- expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
690
+ current_path = Path(str(stored.get("verified_file_path") or stored["file_path"]))
764
691
  if not current_path.is_file():
765
- return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified import file no longer exists")
692
+ return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified file no longer exists")
766
693
  current_sha256 = _sha256_file(current_path)
694
+ expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
767
695
  if current_sha256 != expected_sha256:
768
696
  return self._failed_start_result(
769
697
  error_code="IMPORT_FILE_CHANGED_AFTER_VERIFY",
@@ -801,14 +729,8 @@ class ImportTools(ToolBase):
801
729
  excel_name=str(stored.get("file_name") or current_path.name),
802
730
  )
803
731
  except QingflowApiError as exc:
804
- error_code = (
805
- "IMPORT_SOCKET_ACK_TIMEOUT"
806
- if exc.details and exc.details.get("error_code") == "IMPORT_SOCKET_ACK_TIMEOUT"
807
- else _import_permission_error_code(exc, permission_code="IMPORT_START_UNAUTHORIZED", default="IMPORT_START_FAILED")
808
- )
809
- details: JSONObject = {"accepted": False, "file_url": file_url}
810
- _copy_api_error_fields(details, exc)
811
- return self._failed_start_result(error_code=error_code, message=exc.message, extra=details)
732
+ error_code = "IMPORT_SOCKET_ACK_TIMEOUT" if exc.details and exc.details.get("error_code") == "IMPORT_SOCKET_ACK_TIMEOUT" else "IMPORT_VERIFICATION_FAILED"
733
+ return self._failed_start_result(error_code=error_code, message=exc.message, extra={"accepted": False, "file_url": file_url})
812
734
  import_id = str(socket_result.get("import_id") or "")
813
735
  process_id_str = _normalize_optional_text(socket_result.get("process_id_str"))
814
736
  started_at = _utc_now().isoformat()
@@ -831,11 +753,9 @@ class ImportTools(ToolBase):
831
753
  "ok": True,
832
754
  "status": "accepted",
833
755
  "accepted": True,
834
- "write_executed": True,
835
- "safe_to_retry": False,
836
756
  "import_id": import_id,
837
757
  "process_id_str": process_id_str,
838
- "source_file_name": str(stored.get("file_name") or current_path.name),
758
+ "source_file_name": str(stored.get("file_name") or current_path.name),
839
759
  "file_url": file_url,
840
760
  "warnings": warnings,
841
761
  "verification": {
@@ -918,66 +838,12 @@ class ImportTools(ToolBase):
918
838
  if local_job is None and not normalized_import_id and not normalized_process_id:
919
839
  recent = [item for item in self._job_store.list() if str(item.get("app_key")) == resolved_app_key]
920
840
  local_job = recent[0] if recent else None
921
- try:
922
- page = self.backend.request(
923
- "GET",
924
- context,
925
- "/app/apply/dataImport/record",
926
- params={"appKey": resolved_app_key, "pageNum": 1, "pageSize": 100},
927
- )
928
- except QingflowApiError as exc:
929
- error_code = _import_permission_error_code(exc, permission_code="IMPORT_STATUS_UNAUTHORIZED", default="IMPORT_STATUS_UNAVAILABLE")
930
- details: JSONObject = {
931
- "app_key": resolved_app_key,
932
- "import_id": normalized_import_id,
933
- "process_id_str": effective_process_id,
934
- "verification": {
935
- "status_lookup_completed": False,
936
- "process_id_verified": bool(effective_process_id),
937
- },
938
- }
939
- _copy_api_error_fields(details, exc)
940
- if _is_import_permission_error(exc):
941
- return {
942
- "ok": True,
943
- "status": "unknown",
944
- "error_code": error_code,
945
- "app_key": resolved_app_key,
946
- "import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
947
- "process_id_str": effective_process_id,
948
- "matched_by": "local_job" if isinstance(local_job, dict) else None,
949
- "source_file_name": local_job.get("source_file_name") if isinstance(local_job, dict) else None,
950
- "total_rows": None,
951
- "success_rows": None,
952
- "failed_rows": None,
953
- "progress": None,
954
- "error_file_urls": [],
955
- "operate_time": None,
956
- "operate_user": None,
957
- "accepted": bool(local_job or effective_process_id),
958
- "warnings": [
959
- {
960
- "code": error_code,
961
- "message": "import history is not readable; final import status is unknown",
962
- **{
963
- key: details.get(key)
964
- for key in ("category", "backend_code", "http_status", "request_id")
965
- if details.get(key) is not None
966
- },
967
- }
968
- ],
969
- "verification": details["verification"],
970
- "details": {"status_readback_error": details},
971
- "backend_code": details.get("backend_code"),
972
- "request_id": details.get("request_id"),
973
- "http_status": details.get("http_status"),
974
- "message": "import history is not readable; final import status is unknown",
975
- }
976
- return self._failed_status_result(
977
- error_code=error_code,
978
- message=exc.message,
979
- extra=details,
980
- )
841
+ page = self.backend.request(
842
+ "GET",
843
+ context,
844
+ "/app/apply/dataImport/record",
845
+ params={"appKey": resolved_app_key, "pageNum": 1, "pageSize": 100},
846
+ )
981
847
  records = _extract_import_records(page)
982
848
  matched_record, matched_by = _match_import_record(
983
849
  records,
@@ -1050,19 +916,7 @@ class ImportTools(ToolBase):
1050
916
  try:
1051
917
  return self._run(profile, runner)
1052
918
  except RuntimeError as exc:
1053
- return self._runtime_error_as_result(
1054
- exc,
1055
- error_code="IMPORT_STATUS_AMBIGUOUS",
1056
- extra={
1057
- "app_key": normalized_app_key,
1058
- "import_id": normalized_import_id,
1059
- "process_id_str": normalized_process_id,
1060
- "verification": {
1061
- "status_lookup_completed": False,
1062
- "process_id_verified": bool(normalized_process_id),
1063
- },
1064
- },
1065
- )
919
+ return self._runtime_error_as_result(exc, error_code="IMPORT_STATUS_AMBIGUOUS")
1066
920
 
1067
921
  def _resolve_import_schema_bundle(
1068
922
  self,
@@ -1157,9 +1011,6 @@ class ImportTools(ToolBase):
1157
1011
  "can_import": True,
1158
1012
  "extension": extension,
1159
1013
  "error_code": None,
1160
- "expected_header_titles": list(allowed_header_titles)
1161
- if allowed_header_titles
1162
- else [str(item["title"]) for item in expected_columns],
1163
1014
  }
1164
1015
  if extension not in SUPPORTED_IMPORT_EXTENSIONS:
1165
1016
  base_result["issues"].append(_issue("UNSUPPORTED_FILE_FORMAT", "Only .xlsx and .xls files are supported in import v1.", severity="error"))
@@ -1367,9 +1218,7 @@ class ImportTools(ToolBase):
1367
1218
  "message": f"Member candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
1368
1219
  }
1369
1220
  raise
1370
- except RuntimeError as exc:
1371
- if not _runtime_candidate_validation_skippable(exc):
1372
- raise
1221
+ except RuntimeError:
1373
1222
  return None, {
1374
1223
  "code": "MEMBER_CANDIDATE_VALIDATION_SKIPPED",
1375
1224
  "message": f"Member candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
@@ -1423,9 +1272,7 @@ class ImportTools(ToolBase):
1423
1272
  "message": f"Department candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
1424
1273
  }
1425
1274
  raise
1426
- except RuntimeError as exc:
1427
- if not _runtime_candidate_validation_skippable(exc):
1428
- raise
1275
+ except RuntimeError:
1429
1276
  return None, {
1430
1277
  "code": "DEPARTMENT_CANDIDATE_VALIDATION_SKIPPED",
1431
1278
  "message": f"Department candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
@@ -1550,31 +1397,11 @@ class ImportTools(ToolBase):
1550
1397
 
1551
1398
  def _fetch_import_capability(self, context, app_key: str) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
1552
1399
  """执行内部辅助逻辑。"""
1553
- base_info_error: QingflowApiError | None = None
1554
1400
  try:
1555
1401
  payload = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
1556
- except QingflowApiError as exc:
1557
- if not _is_optional_import_capability_error(exc):
1558
- raise
1559
- base_info_error = exc
1402
+ except QingflowApiError:
1560
1403
  payload = None
1561
- capability, warnings = _derive_import_capability(payload)
1562
- if base_info_error is not None:
1563
- for warning in warnings:
1564
- if warning.get("code") != "IMPORT_CAPABILITY_UNAVAILABLE":
1565
- continue
1566
- warning["message"] = (
1567
- "import capability precheck could not read app baseInfo; continuing with applicant schema "
1568
- "when available, but do not treat import permission as verified."
1569
- )
1570
- if base_info_error.backend_code is not None:
1571
- warning["backend_code"] = base_info_error.backend_code
1572
- if base_info_error.http_status is not None:
1573
- warning["http_status"] = base_info_error.http_status
1574
- if base_info_error.request_id is not None:
1575
- warning["request_id"] = base_info_error.request_id
1576
- break
1577
- return capability, warnings
1404
+ return _derive_import_capability(payload)
1578
1405
 
1579
1406
  def _write_local_template(
1580
1407
  self,
@@ -1603,10 +1430,9 @@ class ImportTools(ToolBase):
1603
1430
  error_code: str,
1604
1431
  message: str,
1605
1432
  request_route: JSONObject | None = None,
1606
- extra: dict[str, Any] | None = None,
1607
1433
  ) -> dict[str, Any]:
1608
1434
  """执行内部辅助逻辑。"""
1609
- payload = {
1435
+ return {
1610
1436
  "ok": False,
1611
1437
  "status": "failed",
1612
1438
  "error_code": error_code,
@@ -1620,9 +1446,6 @@ class ImportTools(ToolBase):
1620
1446
  "verification": {"template_url_resolved": False},
1621
1447
  "message": message,
1622
1448
  }
1623
- if extra:
1624
- payload.update(extra)
1625
- return payload
1626
1449
 
1627
1450
  def _failed_verify_result(
1628
1451
  self,
@@ -1635,7 +1458,7 @@ class ImportTools(ToolBase):
1635
1458
  ) -> dict[str, Any]:
1636
1459
  """执行内部辅助逻辑。"""
1637
1460
  payload = {
1638
- "ok": False,
1461
+ "ok": True,
1639
1462
  "status": "failed",
1640
1463
  "error_code": error_code,
1641
1464
  "app_key": app_key,
@@ -1750,21 +1573,18 @@ class ImportTools(ToolBase):
1750
1573
  extra: dict[str, Any] | None = None,
1751
1574
  ) -> dict[str, Any]:
1752
1575
  """执行内部辅助逻辑。"""
1753
- payload = _runtime_error_payload(error)
1754
- details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
1576
+ try:
1577
+ payload = json.loads(str(error))
1578
+ except json.JSONDecodeError:
1579
+ payload = {"message": str(error)}
1755
1580
  response = {
1756
1581
  "ok": False,
1757
1582
  "status": "failed",
1758
- "error_code": details.get("error_code") or _runtime_error_code(payload, default=error_code),
1583
+ "error_code": ((payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}).get("error_code") or error_code,
1759
1584
  "warnings": [],
1760
1585
  "verification": {},
1761
1586
  "message": payload.get("message") or str(error),
1762
1587
  }
1763
- for key in ("category", "backend_code", "request_id", "http_status"):
1764
- if key in payload:
1765
- response[key] = payload.get(key)
1766
- if details:
1767
- response["details"] = details
1768
1588
  if extra:
1769
1589
  response.update(extra)
1770
1590
  return response
@@ -2222,20 +2042,6 @@ def _sheet_header_map(sheet) -> dict[str, int]: # type: ignore[no-untyped-def]
2222
2042
  return mapping
2223
2043
 
2224
2044
 
2225
- def _repair_header_columns_from_stored_precheck(stored: JSONObject, expected_columns: list[JSONObject]) -> list[JSONObject]:
2226
- for key in ("source_local_precheck", "local_precheck"):
2227
- precheck = stored.get(key)
2228
- if not isinstance(precheck, dict):
2229
- continue
2230
- titles = precheck.get("expected_header_titles")
2231
- if not isinstance(titles, list):
2232
- continue
2233
- normalized_titles = [title for title in (_normalize_optional_text(item) for item in titles) if title]
2234
- if normalized_titles:
2235
- return [{"title": title} for title in normalized_titles]
2236
- return expected_columns
2237
-
2238
-
2239
2045
  def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type: ignore[no-untyped-def]
2240
2046
  changed = False
2241
2047
  expected_by_key = {_normalize_header_key(item["title"]): item["title"] for item in expected_columns}
@@ -2254,11 +2060,9 @@ def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type:
2254
2060
  # Fallback for template-based files where headers were edited into non-canonical
2255
2061
  # values but column order is still intact. Keep any extra trailing system columns.
2256
2062
  for index, column in enumerate(expected_columns, start=1):
2257
- expected_title = str(column["title"]).strip()
2258
2063
  if index > len(header_cells):
2259
- sheet.cell(row=1, column=index, value=expected_title)
2260
- changed = True
2261
- continue
2064
+ break
2065
+ expected_title = str(column["title"]).strip()
2262
2066
  current_title = _normalize_optional_text(header_cells[index - 1].value)
2263
2067
  if current_title != expected_title:
2264
2068
  header_cells[index - 1].value = expected_title
@@ -2453,97 +2257,6 @@ def _normalize_import_status(value: Any) -> str:
2453
2257
  return "unknown"
2454
2258
 
2455
2259
 
2456
- def _runtime_error_payload(error: RuntimeError) -> JSONObject:
2457
- try:
2458
- payload = json.loads(str(error))
2459
- except json.JSONDecodeError:
2460
- return {"message": str(error)}
2461
- return payload if isinstance(payload, dict) else {"message": str(error)}
2462
-
2463
-
2464
- def _runtime_import_can_import_value(error: RuntimeError) -> bool | None:
2465
- payload = _runtime_error_payload(error)
2466
- category = str(payload.get("category") or "").strip().lower()
2467
- if category == "auth" or _coerce_int(payload.get("http_status")) == 401 or message_looks_like_invalid_token(payload.get("message")):
2468
- return None
2469
- backend_code = backend_code_int(
2470
- QingflowApiError(
2471
- category=category,
2472
- message=str(payload.get("message") or ""),
2473
- backend_code=payload.get("backend_code"),
2474
- http_status=_coerce_int(payload.get("http_status")),
2475
- )
2476
- )
2477
- return False if backend_code in {40002, 40027} else None
2478
-
2479
-
2480
- def _runtime_candidate_validation_skippable(error: RuntimeError) -> bool:
2481
- payload = _runtime_error_payload(error)
2482
- category = str(payload.get("category") or "").strip().lower()
2483
- if category == "not_supported":
2484
- return True
2485
- if category in {"auth", "workspace"}:
2486
- return False
2487
- if message_looks_like_invalid_token(payload.get("message")):
2488
- return False
2489
- if _coerce_int(payload.get("http_status")) == 401:
2490
- return False
2491
- backend_code = backend_code_int(
2492
- QingflowApiError(
2493
- category=category,
2494
- message=str(payload.get("message") or ""),
2495
- backend_code=payload.get("backend_code"),
2496
- http_status=_coerce_int(payload.get("http_status")),
2497
- )
2498
- )
2499
- return backend_code in {40002, 40027, 404} or _coerce_int(payload.get("http_status")) == 404
2500
-
2501
-
2502
- def _runtime_error_code(payload: JSONObject, *, default: str) -> str:
2503
- category = str(payload.get("category") or "").strip().lower()
2504
- http_status = _coerce_int(payload.get("http_status"))
2505
- if category == "auth" or http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
2506
- return "AUTH_REQUIRED"
2507
- if category == "workspace":
2508
- return "WORKSPACE_NOT_SELECTED"
2509
- return default
2510
-
2511
-
2512
- def _is_import_permission_error(error: QingflowApiError) -> bool:
2513
- if is_auth_like_error(error):
2514
- return False
2515
- return backend_code_int(error) in {40002, 40027}
2516
-
2517
-
2518
- def _is_import_capability_auth_error(error: QingflowApiError) -> bool:
2519
- return is_auth_like_error(error)
2520
-
2521
-
2522
- def _is_optional_import_capability_error(error: QingflowApiError) -> bool:
2523
- if _is_import_capability_auth_error(error):
2524
- return False
2525
- return _is_import_permission_error(error) or backend_code_int(error) == 404 or error.http_status == 404
2526
-
2527
-
2528
- def _import_permission_error_code(error: QingflowApiError, *, permission_code: str, default: str) -> str:
2529
- if is_auth_like_error(error):
2530
- return "AUTH_REQUIRED"
2531
- if _is_import_permission_error(error):
2532
- return permission_code
2533
- return default
2534
-
2535
-
2536
- def _copy_api_error_fields(payload: dict[str, Any], error: QingflowApiError) -> None:
2537
- if error.category:
2538
- payload["category"] = error.category
2539
- if error.backend_code is not None:
2540
- payload["backend_code"] = error.backend_code
2541
- if error.http_status is not None:
2542
- payload["http_status"] = error.http_status
2543
- if error.request_id:
2544
- payload["request_id"] = error.request_id
2545
-
2546
-
2547
2260
  def _normalize_error_file_urls(value: Any) -> list[str]:
2548
2261
  if isinstance(value, list):
2549
2262
  return [str(item).strip() for item in value if str(item).strip()]