@josephyan/qingflow-cli 1.1.3 → 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.
- package/README.md +7 -3
- package/docs/local-agent-install.md +57 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/bin/qingflow.mjs +1 -34
- package/npm/lib/runtime.mjs +21 -101
- package/npm/scripts/postinstall.mjs +1 -10
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +58 -44
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +35 -0
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +38 -0
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +39 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md → builder/30-schema-fields.md} +52 -10
- package/skills/qingflow-cli/reference/builder/40-layout.md +52 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md → builder/50-views.md} +39 -15
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_CHARTS_WORKFLOW.md → builder/60-charts.md} +36 -13
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_PORTAL_WORKFLOW.md → builder/70-portal.md} +36 -13
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +41 -0
- package/skills/qingflow-cli/reference/builder/90-workflow.md +34 -0
- package/skills/qingflow-cli/reference/builder/99-publish-verify.md +46 -0
- package/skills/qingflow-cli/reference/builder/README.md +41 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +130 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +66 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +77 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md → builder/reference/app-delivery-sop.md} +26 -16
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/README.md +293 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-complete-system.md +809 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/build-single-app.md +830 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/complete-system-development-guide.md +123 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/create-app.md +182 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/environments.md +63 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/flow-actors-and-permissions.md +142 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/gotchas.md +108 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/match-rules.md +114 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/public-surface-sync.md +75 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/single-app-development-guide.md +58 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/solution-playbooks.md +52 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/tool-selection.md +107 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-flow.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-layout.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-schema.md +7 -0
- package/skills/qingflow-cli/reference/builder/reference/legacy-playbooks/update-views.md +7 -0
- package/skills/qingflow-cli/reference/builder/workflow/01-overview.md +45 -0
- package/skills/qingflow-cli/reference/builder/workflow/02-update-mode.md +53 -0
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +57 -0
- package/skills/qingflow-cli/reference/builder/workflow/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-cli/reference/builder/workflow/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-cli/reference/builder/workflow/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-cli/reference/builder/workflow/09-stage6-summary.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/10-node-config-reference.md +93 -0
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +15 -0
- package/skills/qingflow-cli/reference/builder/workflow/README.md +88 -0
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1754 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ADMIN_CHEATSHEET.md → core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md} +3 -3
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md → core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md} +6 -6
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_EXPLORATION_REPORT.md → core/QINGFLOW_CLI_EXPLORATION_REPORT.md} +2 -2
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_FIELD_DATA_TYPES.md → core/QINGFLOW_CLI_FIELD_DATA_TYPES.md} +11 -11
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_MEMBER_CHEATSHEET.md → core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md → core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md} +3 -3
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md +31 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md} +4 -4
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md → record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md} +7 -7
- package/skills/qingflow-cli/reference/record/analysis/README.md +130 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +91 -0
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +112 -0
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +74 -0
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +69 -0
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +106 -0
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +172 -0
- package/skills/qingflow-cli/reference/record/analysis/report-format.md +76 -0
- package/skills/qingflow-cli/reference/record/insert/README.md +75 -0
- package/skills/qingflow-cli/reference/{QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md → task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md} +5 -5
- package/skills/qingflow-cli/reference/task/ops/README.md +131 -0
- package/skills/qingflow-cli/reference/task/ops/environments.md +43 -0
- package/skills/qingflow-cli/reference/task/ops/workflow-usage.md +26 -0
- package/skills/qingflow-cli/scripts/validate_system_build_summary.py +124 -0
- package/skills/qingflow-cli/scripts/workflow/diff_flow_spec.py +275 -0
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +605 -0
- package/skills/qingflow-mcp-setup/SKILL.md +115 -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 +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +287 -25
- package/src/qingflow_mcp/builder_facade/service.py +4195 -856
- package/src/qingflow_mcp/cli/commands/builder.py +316 -247
- package/src/qingflow_mcp/cli/commands/chart.py +1 -1
- package/src/qingflow_mcp/cli/commands/common.py +12 -3
- 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 +101 -27
- package/src/qingflow_mcp/cli/commands/task.py +28 -47
- package/src/qingflow_mcp/cli/commands/view.py +1 -1
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +784 -16
- package/src/qingflow_mcp/cli/main.py +117 -33
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +26 -17
- package/src/qingflow_mcp/response_trim.py +81 -17
- package/src/qingflow_mcp/server.py +14 -12
- package/src/qingflow_mcp/server_app_builder.py +65 -21
- package/src/qingflow_mcp/server_app_user.py +22 -16
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/compiler/__init__.py +3 -1
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/executor.py +245 -18
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1782 -399
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +197 -35
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/feedback_tools.py +9 -0
- package/src/qingflow_mcp/tools/file_tools.py +9 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1141 -356
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +59 -45
- package/src/qingflow_mcp/tools/task_context_tools.py +662 -158
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +48 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_MATCH_RULES.md → builder/reference/match-rules.md} +0 -0
- /package/skills/qingflow-cli/reference/{QINGFLOW_CLI_BUILDER_WORKSPACE_ICONS.md → builder/reference/workspace-icons.md} +0 -0
- /package/skills/qingflow-cli/reference/{charts_remove.example.json → examples/charts/charts_remove.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_reorder.example.json → examples/charts/charts_reorder.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_bar.example.json → examples/charts/charts_upsert_bar.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_dashboard_starter.example.json → examples/charts/charts_upsert_dashboard_starter.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{charts_upsert_minimal.example.json → examples/charts/charts_upsert_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_all_types.example.json → examples/portal/portal_sections_all_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_five_types.example.json → examples/portal/portal_sections_five_types.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{portal_sections_standard_workbench.example.json → examples/portal/portal_sections_standard_workbench.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_complex.json → examples/schema/_batch_schema_complex.json} +0 -0
- /package/skills/qingflow-cli/reference/{_batch_schema_scalar.json → examples/schema/_batch_schema_scalar.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_add_fields_minimal.example.json → examples/schema/schema_add_fields_minimal.example.json} +0 -0
- /package/skills/qingflow-cli/reference/{schema_apply_add_fields_all_types.json → examples/schema/schema_apply_add_fields_all_types.json} +0 -0
- /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=
|
|
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
|
|
212
|
-
payload["
|
|
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
|
-
|
|
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
|
-
|
|
542
|
+
backend_error_code,
|
|
484
543
|
exc.message or "Backend import verification failed.",
|
|
485
544
|
severity="error",
|
|
486
545
|
)
|
|
487
546
|
)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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":
|
|
579
|
+
"ok": can_import,
|
|
521
580
|
"status": "success" if can_import else "failed",
|
|
522
|
-
"error_code": None
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
1577
|
-
|
|
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":
|
|
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()]
|