@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.
- package/README.md +5 -6
- package/docs/local-agent-install.md +6 -57
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +101 -21
- package/npm/scripts/postinstall.mjs +10 -1
- package/package.json +2 -3
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +42 -104
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +11 -30
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/gotchas.md +9 -31
- package/skills/qingflow-app-builder/references/match-rules.md +0 -15
- package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
- package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
- package/skills/qingflow-app-builder/references/update-flow.md +38 -22
- package/skills/qingflow-app-builder/references/update-schema.md +0 -14
- package/skills/qingflow-app-builder/references/update-views.md +14 -83
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +2 -6
- package/src/qingflow_mcp/builder_facade/models.py +108 -485
- package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
- package/src/qingflow_mcp/cli/commands/builder.py +364 -322
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +3 -12
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +3 -3
- package/src/qingflow_mcp/cli/commands/portal.py +2 -2
- package/src/qingflow_mcp/cli/commands/record.py +27 -101
- package/src/qingflow_mcp/cli/commands/task.py +47 -28
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +16 -784
- package/src/qingflow_mcp/cli/main.py +33 -117
- package/src/qingflow_mcp/errors.py +2 -43
- package/src/qingflow_mcp/public_surface.py +17 -26
- package/src/qingflow_mcp/response_trim.py +17 -81
- package/src/qingflow_mcp/server.py +12 -14
- package/src/qingflow_mcp/server_app_builder.py +36 -79
- package/src/qingflow_mcp/server_app_user.py +16 -22
- package/src/qingflow_mcp/session_store.py +7 -11
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/executor.py +18 -245
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
- package/src/qingflow_mcp/tools/app_tools.py +43 -184
- package/src/qingflow_mcp/tools/approval_tools.py +35 -197
- package/src/qingflow_mcp/tools/auth_tools.py +16 -92
- package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
- package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
- package/src/qingflow_mcp/tools/directory_tools.py +72 -236
- package/src/qingflow_mcp/tools/export_tools.py +34 -244
- package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
- package/src/qingflow_mcp/tools/file_tools.py +3 -9
- package/src/qingflow_mcp/tools/import_tools.py +49 -336
- package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
- package/src/qingflow_mcp/tools/package_tools.py +6 -118
- package/src/qingflow_mcp/tools/portal_tools.py +3 -39
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
- package/src/qingflow_mcp/tools/record_tools.py +360 -1145
- package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
- package/src/qingflow_mcp/tools/role_tools.py +9 -80
- package/src/qingflow_mcp/tools/solution_tools.py +45 -59
- package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
- package/src/qingflow_mcp/tools/task_tools.py +29 -113
- package/src/qingflow_mcp/tools/view_tools.py +3 -106
- package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
- package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
- package/npm/bin/qingflow-skills.mjs +0 -5
- package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
- package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
- package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
- 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
|
|
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=
|
|
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
|
|
229
|
-
payload["
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
483
|
+
"BACKEND_IMPORT_VERIFICATION_FAILED",
|
|
543
484
|
exc.message or "Backend import verification failed.",
|
|
544
485
|
severity="error",
|
|
545
486
|
)
|
|
546
487
|
)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
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
|
|
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
|
|
1557
|
-
if not _is_optional_import_capability_error(exc):
|
|
1558
|
-
raise
|
|
1559
|
-
base_info_error = exc
|
|
1402
|
+
except QingflowApiError:
|
|
1560
1403
|
payload = None
|
|
1561
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
1754
|
-
|
|
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":
|
|
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
|
-
|
|
2260
|
-
|
|
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()]
|