@josephyan/qingflow-cli 1.1.4 → 1.1.5

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 (154) hide show
  1. package/README.md +7 -3
  2. package/docs/local-agent-install.md +57 -6
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/bin/qingflow.mjs +1 -34
  6. package/npm/lib/runtime.mjs +21 -101
  7. package/npm/scripts/postinstall.mjs +1 -10
  8. package/package.json +3 -2
  9. package/pyproject.toml +1 -1
  10. package/skills/qingflow-cli/SKILL.md +58 -44
  11. package/skills/qingflow-cli/manifest.yaml +1 -1
  12. package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
  13. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
  14. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
  15. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
  16. package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
  17. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
  18. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
  19. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
  21. package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
  22. package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
  23. package/skills/qingflow-cli/reference/builder/README.md +41 -0
  24. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
  25. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
  26. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
  27. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
  28. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
  29. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
  30. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
  31. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
  32. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
  33. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
  34. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
  35. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
  36. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
  37. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
  38. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
  39. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
  40. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
  41. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
  42. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
  43. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
  44. package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
  45. package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
  46. package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
  47. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
  48. package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
  49. package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
  50. package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
  51. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
  52. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
  53. package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
  54. package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
  55. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
  56. package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
  57. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
  58. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
  59. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
  60. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
  61. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
  62. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
  63. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
  64. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
  65. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
  66. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
  67. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
  68. package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
  69. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
  70. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
  71. package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
  72. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
  73. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
  74. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
  75. package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
  76. package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
  77. package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
  78. package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
  79. package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
  80. package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
  81. package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
  82. package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
  83. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
  84. package/skills/qingflow-mcp-setup/SKILL.md +115 -0
  85. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  86. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  87. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  88. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  89. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  90. package/src/qingflow_mcp/__init__.py +1 -1
  91. package/src/qingflow_mcp/__main__.py +6 -2
  92. package/src/qingflow_mcp/builder_facade/models.py +282 -102
  93. package/src/qingflow_mcp/builder_facade/service.py +4166 -929
  94. package/src/qingflow_mcp/cli/commands/builder.py +316 -298
  95. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  96. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  97. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  98. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  99. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  100. package/src/qingflow_mcp/cli/commands/record.py +101 -27
  101. package/src/qingflow_mcp/cli/commands/task.py +28 -47
  102. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  103. package/src/qingflow_mcp/cli/context.py +0 -3
  104. package/src/qingflow_mcp/cli/formatters.py +784 -16
  105. package/src/qingflow_mcp/cli/main.py +117 -33
  106. package/src/qingflow_mcp/errors.py +43 -2
  107. package/src/qingflow_mcp/public_surface.py +26 -17
  108. package/src/qingflow_mcp/response_trim.py +81 -17
  109. package/src/qingflow_mcp/server.py +14 -12
  110. package/src/qingflow_mcp/server_app_builder.py +65 -21
  111. package/src/qingflow_mcp/server_app_user.py +22 -16
  112. package/src/qingflow_mcp/session_store.py +11 -7
  113. package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
  114. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  115. package/src/qingflow_mcp/solution/executor.py +245 -18
  116. package/src/qingflow_mcp/tools/ai_builder_tools.py +1780 -406
  117. package/src/qingflow_mcp/tools/app_tools.py +184 -43
  118. package/src/qingflow_mcp/tools/approval_tools.py +197 -35
  119. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  120. package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
  121. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  122. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  123. package/src/qingflow_mcp/tools/export_tools.py +244 -34
  124. package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
  125. package/src/qingflow_mcp/tools/file_tools.py +9 -3
  126. package/src/qingflow_mcp/tools/import_tools.py +336 -49
  127. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  128. package/src/qingflow_mcp/tools/package_tools.py +118 -6
  129. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  130. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  131. package/src/qingflow_mcp/tools/record_tools.py +1141 -356
  132. package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
  133. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  134. package/src/qingflow_mcp/tools/solution_tools.py +59 -45
  135. package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
  136. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  137. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  138. package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
  139. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
  140. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
  141. /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
  142. /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
  143. /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
  144. /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
  145. /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
  146. /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
  147. /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
  148. /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
  149. /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
  150. /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
  151. /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
  152. /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
  153. /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
  154. /package/skills/qingflow-cli/reference/{views_upsert_table_minimal.example.json → examples/views/views_upsert_table_minimal.example.json} +0 -0
@@ -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, backend_code_int, is_auth_like_error, message_looks_like_invalid_token
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,11 +75,12 @@ class ImportTools(ToolBase):
75
75
  """注册当前工具到 MCP 服务。"""
76
76
  @mcp.tool()
77
77
  def record_import_schema_get(
78
+ profile: str = DEFAULT_PROFILE,
78
79
  app_key: str = "",
79
80
  output_profile: str = "normal",
80
81
  ) -> dict[str, Any]:
81
82
  return self.record_import_schema_get(
82
- profile=DEFAULT_PROFILE,
83
+ profile=profile,
83
84
  app_key=app_key,
84
85
  output_profile=output_profile,
85
86
  )
@@ -193,6 +194,22 @@ class ImportTools(ToolBase):
193
194
 
194
195
  def runner(session_profile, context):
195
196
  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
+ }
196
213
  _index, expected_columns, schema_fingerprint = self._resolve_import_schema_bundle(
197
214
  profile,
198
215
  context,
@@ -208,8 +225,15 @@ class ImportTools(ToolBase):
208
225
  }
209
226
  if isinstance(column.get("options"), list) and column.get("options"):
210
227
  payload["options"] = column["options"]
211
- if bool(column.get("requires_lookup")):
212
- payload["accepts_natural_input"] = True
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."
213
237
  if bool(column.get("requires_upload")):
214
238
  payload["requires_upload"] = True
215
239
  if isinstance(column.get("target_app_key"), str):
@@ -251,6 +275,21 @@ class ImportTools(ToolBase):
251
275
 
252
276
  def runner(session_profile, context):
253
277
  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
+ )
254
293
  field_index, expected_columns, schema_fingerprint = self._resolve_import_schema_bundle(
255
294
  profile,
256
295
  context,
@@ -260,12 +299,30 @@ class ImportTools(ToolBase):
260
299
  try:
261
300
  payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
262
301
  except QingflowApiError as exc:
263
- if import_capability.get("auth_source") == "apply_auth":
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:
264
311
  downloaded_to_path = self._write_local_template(
265
312
  expected_columns=expected_columns,
266
313
  destination_hint=download_to_path,
267
314
  app_key=app_key,
268
315
  )
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)
269
326
  return {
270
327
  "ok": True,
271
328
  "status": "partial_success",
@@ -276,18 +333,13 @@ class ImportTools(ToolBase):
276
333
  "downloaded_to_path": downloaded_to_path,
277
334
  "expected_columns": expected_columns,
278
335
  "schema_fingerprint": schema_fingerprint,
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
- ],
336
+ "warnings": import_warnings + [template_warning],
286
337
  "verification": {
287
338
  "schema_fingerprint": schema_fingerprint,
288
339
  "template_url_resolved": False,
289
340
  "template_downloaded": True,
290
341
  "template_source": "local_generated",
342
+ "import_auth_prechecked": import_capability.get("auth_source") != "unknown",
291
343
  },
292
344
  }
293
345
  return self._failed_template_result(
@@ -448,6 +500,7 @@ class ImportTools(ToolBase):
448
500
  issues = deepcopy(effective_local_check["issues"])
449
501
  can_import = bool(effective_local_check["can_import"])
450
502
  backend_verification = None
503
+ backend_failure_error_code = None
451
504
  if can_import:
452
505
  try:
453
506
  payload = self.backend.request_multipart(
@@ -478,19 +531,25 @@ class ImportTools(ToolBase):
478
531
  )
479
532
  except QingflowApiError as exc:
480
533
  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
481
540
  issues.append(
482
541
  _issue(
483
- "BACKEND_IMPORT_VERIFICATION_FAILED",
542
+ backend_error_code,
484
543
  exc.message or "Backend import verification failed.",
485
544
  severity="error",
486
545
  )
487
546
  )
488
- warnings.append(
489
- {
490
- "code": "IMPORT_VERIFICATION_FAILED",
491
- "message": "Backend verification failed; the file cannot be imported until verification succeeds.",
492
- }
493
- )
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)
494
553
  verification_id = str(uuid4())
495
554
  verification_payload = {
496
555
  "id": verification_id,
@@ -517,9 +576,11 @@ class ImportTools(ToolBase):
517
576
  }
518
577
  self._verification_store.put(verification_id, verification_payload)
519
578
  return {
520
- "ok": True,
579
+ "ok": can_import,
521
580
  "status": "success" if can_import else "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"),
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"),
523
584
  "can_import": can_import,
524
585
  "verification_id": verification_id,
525
586
  "file_path": str(path.resolve()),
@@ -553,7 +614,11 @@ class ImportTools(ToolBase):
553
614
  try:
554
615
  return self._run(profile, runner)
555
616
  except RuntimeError as exc:
556
- return self._runtime_error_as_result(exc, error_code="IMPORT_VERIFICATION_FAILED", extra={"can_import": False})
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
+ )
557
622
 
558
623
  @tool_cn_name("导入修复")
559
624
  def record_import_repair_local(
@@ -603,7 +668,8 @@ class ImportTools(ToolBase):
603
668
  applied_repairs: list[str] = []
604
669
  skipped_repairs: list[str] = []
605
670
  if "normalize_headers" in normalized_repairs:
606
- if _repair_headers(sheet, expected_columns):
671
+ repair_header_columns = _repair_header_columns_from_stored_precheck(stored, expected_columns)
672
+ if _repair_headers(sheet, repair_header_columns):
607
673
  applied_repairs.append("normalize_headers")
608
674
  else:
609
675
  skipped_repairs.append("normalize_headers")
@@ -687,11 +753,17 @@ class ImportTools(ToolBase):
687
753
  return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verification_id does not belong to the requested app")
688
754
  if not bool(stored.get("can_import")):
689
755
  return self._failed_start_result(error_code="IMPORT_VERIFICATION_FAILED", message="verification_id is not importable", extra={"accepted": False})
690
- current_path = Path(str(stored.get("verified_file_path") or stored["file_path"]))
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")
691
764
  if not current_path.is_file():
692
- return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified file no longer exists")
765
+ return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified import file no longer exists")
693
766
  current_sha256 = _sha256_file(current_path)
694
- expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
695
767
  if current_sha256 != expected_sha256:
696
768
  return self._failed_start_result(
697
769
  error_code="IMPORT_FILE_CHANGED_AFTER_VERIFY",
@@ -729,8 +801,14 @@ class ImportTools(ToolBase):
729
801
  excel_name=str(stored.get("file_name") or current_path.name),
730
802
  )
731
803
  except QingflowApiError as exc:
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})
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)
734
812
  import_id = str(socket_result.get("import_id") or "")
735
813
  process_id_str = _normalize_optional_text(socket_result.get("process_id_str"))
736
814
  started_at = _utc_now().isoformat()
@@ -753,9 +831,11 @@ class ImportTools(ToolBase):
753
831
  "ok": True,
754
832
  "status": "accepted",
755
833
  "accepted": True,
834
+ "write_executed": True,
835
+ "safe_to_retry": False,
756
836
  "import_id": import_id,
757
837
  "process_id_str": process_id_str,
758
- "source_file_name": str(stored.get("file_name") or current_path.name),
838
+ "source_file_name": str(stored.get("file_name") or current_path.name),
759
839
  "file_url": file_url,
760
840
  "warnings": warnings,
761
841
  "verification": {
@@ -838,12 +918,66 @@ class ImportTools(ToolBase):
838
918
  if local_job is None and not normalized_import_id and not normalized_process_id:
839
919
  recent = [item for item in self._job_store.list() if str(item.get("app_key")) == resolved_app_key]
840
920
  local_job = recent[0] if recent else None
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
- )
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
+ )
847
981
  records = _extract_import_records(page)
848
982
  matched_record, matched_by = _match_import_record(
849
983
  records,
@@ -916,7 +1050,19 @@ class ImportTools(ToolBase):
916
1050
  try:
917
1051
  return self._run(profile, runner)
918
1052
  except RuntimeError as exc:
919
- return self._runtime_error_as_result(exc, error_code="IMPORT_STATUS_AMBIGUOUS")
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
+ )
920
1066
 
921
1067
  def _resolve_import_schema_bundle(
922
1068
  self,
@@ -1011,6 +1157,9 @@ class ImportTools(ToolBase):
1011
1157
  "can_import": True,
1012
1158
  "extension": extension,
1013
1159
  "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],
1014
1163
  }
1015
1164
  if extension not in SUPPORTED_IMPORT_EXTENSIONS:
1016
1165
  base_result["issues"].append(_issue("UNSUPPORTED_FILE_FORMAT", "Only .xlsx and .xls files are supported in import v1.", severity="error"))
@@ -1218,7 +1367,9 @@ class ImportTools(ToolBase):
1218
1367
  "message": f"Member candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
1219
1368
  }
1220
1369
  raise
1221
- except RuntimeError:
1370
+ except RuntimeError as exc:
1371
+ if not _runtime_candidate_validation_skippable(exc):
1372
+ raise
1222
1373
  return None, {
1223
1374
  "code": "MEMBER_CANDIDATE_VALIDATION_SKIPPED",
1224
1375
  "message": f"Member candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
@@ -1272,7 +1423,9 @@ class ImportTools(ToolBase):
1272
1423
  "message": f"Department candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
1273
1424
  }
1274
1425
  raise
1275
- except RuntimeError:
1426
+ except RuntimeError as exc:
1427
+ if not _runtime_candidate_validation_skippable(exc):
1428
+ raise
1276
1429
  return None, {
1277
1430
  "code": "DEPARTMENT_CANDIDATE_VALIDATION_SKIPPED",
1278
1431
  "message": f"Department candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
@@ -1397,11 +1550,31 @@ class ImportTools(ToolBase):
1397
1550
 
1398
1551
  def _fetch_import_capability(self, context, app_key: str) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
1399
1552
  """执行内部辅助逻辑。"""
1553
+ base_info_error: QingflowApiError | None = None
1400
1554
  try:
1401
1555
  payload = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
1402
- except QingflowApiError:
1556
+ except QingflowApiError as exc:
1557
+ if not _is_optional_import_capability_error(exc):
1558
+ raise
1559
+ base_info_error = exc
1403
1560
  payload = None
1404
- return _derive_import_capability(payload)
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
1405
1578
 
1406
1579
  def _write_local_template(
1407
1580
  self,
@@ -1430,9 +1603,10 @@ class ImportTools(ToolBase):
1430
1603
  error_code: str,
1431
1604
  message: str,
1432
1605
  request_route: JSONObject | None = None,
1606
+ extra: dict[str, Any] | None = None,
1433
1607
  ) -> dict[str, Any]:
1434
1608
  """执行内部辅助逻辑。"""
1435
- return {
1609
+ payload = {
1436
1610
  "ok": False,
1437
1611
  "status": "failed",
1438
1612
  "error_code": error_code,
@@ -1446,6 +1620,9 @@ class ImportTools(ToolBase):
1446
1620
  "verification": {"template_url_resolved": False},
1447
1621
  "message": message,
1448
1622
  }
1623
+ if extra:
1624
+ payload.update(extra)
1625
+ return payload
1449
1626
 
1450
1627
  def _failed_verify_result(
1451
1628
  self,
@@ -1458,7 +1635,7 @@ class ImportTools(ToolBase):
1458
1635
  ) -> dict[str, Any]:
1459
1636
  """执行内部辅助逻辑。"""
1460
1637
  payload = {
1461
- "ok": True,
1638
+ "ok": False,
1462
1639
  "status": "failed",
1463
1640
  "error_code": error_code,
1464
1641
  "app_key": app_key,
@@ -1573,18 +1750,21 @@ class ImportTools(ToolBase):
1573
1750
  extra: dict[str, Any] | None = None,
1574
1751
  ) -> dict[str, Any]:
1575
1752
  """执行内部辅助逻辑。"""
1576
- try:
1577
- payload = json.loads(str(error))
1578
- except json.JSONDecodeError:
1579
- payload = {"message": str(error)}
1753
+ payload = _runtime_error_payload(error)
1754
+ details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
1580
1755
  response = {
1581
1756
  "ok": False,
1582
1757
  "status": "failed",
1583
- "error_code": ((payload.get("details") or {}) if isinstance(payload.get("details"), dict) else {}).get("error_code") or error_code,
1758
+ "error_code": details.get("error_code") or _runtime_error_code(payload, default=error_code),
1584
1759
  "warnings": [],
1585
1760
  "verification": {},
1586
1761
  "message": payload.get("message") or str(error),
1587
1762
  }
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
1588
1768
  if extra:
1589
1769
  response.update(extra)
1590
1770
  return response
@@ -2042,6 +2222,20 @@ def _sheet_header_map(sheet) -> dict[str, int]: # type: ignore[no-untyped-def]
2042
2222
  return mapping
2043
2223
 
2044
2224
 
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
+
2045
2239
  def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type: ignore[no-untyped-def]
2046
2240
  changed = False
2047
2241
  expected_by_key = {_normalize_header_key(item["title"]): item["title"] for item in expected_columns}
@@ -2060,9 +2254,11 @@ def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type:
2060
2254
  # Fallback for template-based files where headers were edited into non-canonical
2061
2255
  # values but column order is still intact. Keep any extra trailing system columns.
2062
2256
  for index, column in enumerate(expected_columns, start=1):
2063
- if index > len(header_cells):
2064
- break
2065
2257
  expected_title = str(column["title"]).strip()
2258
+ if index > len(header_cells):
2259
+ sheet.cell(row=1, column=index, value=expected_title)
2260
+ changed = True
2261
+ continue
2066
2262
  current_title = _normalize_optional_text(header_cells[index - 1].value)
2067
2263
  if current_title != expected_title:
2068
2264
  header_cells[index - 1].value = expected_title
@@ -2257,6 +2453,97 @@ def _normalize_import_status(value: Any) -> str:
2257
2453
  return "unknown"
2258
2454
 
2259
2455
 
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
+
2260
2547
  def _normalize_error_file_urls(value: Any) -> list[str]:
2261
2548
  if isinstance(value, list):
2262
2549
  return [str(item).strip() for item in value if str(item).strip()]