@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12
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 +9 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -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/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +41 -2
- package/src/qingflow_mcp/builder_facade/service.py +2743 -423
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +30 -4
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +54 -11
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +238 -8
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +24 -16
- package/src/qingflow_mcp/response_trim.py +119 -12
- package/src/qingflow_mcp/server.py +17 -14
- package/src/qingflow_mcp/server_app_builder.py +29 -7
- package/src/qingflow_mcp/server_app_user.py +23 -24
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
- package/src/qingflow_mcp/tools/app_tools.py +237 -51
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
- 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 +230 -33
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +293 -40
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +134 -8
- 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 +2305 -442
- package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- 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 +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -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
|
|
@@ -193,6 +193,22 @@ class ImportTools(ToolBase):
|
|
|
193
193
|
|
|
194
194
|
def runner(session_profile, context):
|
|
195
195
|
import_capability, import_warnings = self._fetch_import_capability(context, app_key)
|
|
196
|
+
if import_capability.get("can_import") is False and import_capability.get("auth_source") != "unknown":
|
|
197
|
+
return {
|
|
198
|
+
"ok": False,
|
|
199
|
+
"status": "failed",
|
|
200
|
+
"app_key": app_key,
|
|
201
|
+
"ws_id": session_profile.selected_ws_id,
|
|
202
|
+
"request_route": self.backend.describe_route(context),
|
|
203
|
+
"error_code": "IMPORT_AUTH_PRECHECK_FAILED",
|
|
204
|
+
"message": "the current user does not have import permission for this app",
|
|
205
|
+
"warnings": import_warnings,
|
|
206
|
+
"import_capability": import_capability,
|
|
207
|
+
"verification": {
|
|
208
|
+
"import_auth_prechecked": True,
|
|
209
|
+
"import_auth_precheck_passed": False,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
196
212
|
_index, expected_columns, schema_fingerprint = self._resolve_import_schema_bundle(
|
|
197
213
|
profile,
|
|
198
214
|
context,
|
|
@@ -251,6 +267,21 @@ class ImportTools(ToolBase):
|
|
|
251
267
|
|
|
252
268
|
def runner(session_profile, context):
|
|
253
269
|
import_capability, import_warnings = self._fetch_import_capability(context, app_key)
|
|
270
|
+
if import_capability.get("can_import") is False and import_capability.get("auth_source") != "unknown":
|
|
271
|
+
return self._failed_template_result(
|
|
272
|
+
app_key=app_key,
|
|
273
|
+
error_code="IMPORT_AUTH_PRECHECK_FAILED",
|
|
274
|
+
message="the current user does not have import permission for this app",
|
|
275
|
+
request_route=self.backend.describe_route(context),
|
|
276
|
+
extra={
|
|
277
|
+
"warnings": import_warnings,
|
|
278
|
+
"import_capability": import_capability,
|
|
279
|
+
"verification": {
|
|
280
|
+
"import_auth_prechecked": True,
|
|
281
|
+
"import_auth_precheck_passed": False,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
)
|
|
254
285
|
field_index, expected_columns, schema_fingerprint = self._resolve_import_schema_bundle(
|
|
255
286
|
profile,
|
|
256
287
|
context,
|
|
@@ -260,12 +291,30 @@ class ImportTools(ToolBase):
|
|
|
260
291
|
try:
|
|
261
292
|
payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
|
|
262
293
|
except QingflowApiError as exc:
|
|
263
|
-
|
|
294
|
+
can_generate_local_template = bool(expected_columns) and (
|
|
295
|
+
import_capability.get("auth_source") == "apply_auth"
|
|
296
|
+
or (
|
|
297
|
+
import_capability.get("auth_source") == "unknown"
|
|
298
|
+
and not is_auth_like_error(exc)
|
|
299
|
+
and backend_code_int(exc) in {40002, 40027}
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
if can_generate_local_template:
|
|
264
303
|
downloaded_to_path = self._write_local_template(
|
|
265
304
|
expected_columns=expected_columns,
|
|
266
305
|
destination_hint=download_to_path,
|
|
267
306
|
app_key=app_key,
|
|
268
307
|
)
|
|
308
|
+
template_warning = {
|
|
309
|
+
"code": "IMPORT_TEMPLATE_LOCAL_FALLBACK",
|
|
310
|
+
"message": "Official template download requires data management permission; MCP generated a local applicant-import template instead.",
|
|
311
|
+
}
|
|
312
|
+
if import_capability.get("auth_source") == "unknown":
|
|
313
|
+
template_warning = {
|
|
314
|
+
"code": "IMPORT_TEMPLATE_LOCAL_FALLBACK_AUTH_UNKNOWN",
|
|
315
|
+
"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.",
|
|
316
|
+
}
|
|
317
|
+
_copy_api_error_fields(template_warning, exc)
|
|
269
318
|
return {
|
|
270
319
|
"ok": True,
|
|
271
320
|
"status": "partial_success",
|
|
@@ -276,18 +325,13 @@ class ImportTools(ToolBase):
|
|
|
276
325
|
"downloaded_to_path": downloaded_to_path,
|
|
277
326
|
"expected_columns": expected_columns,
|
|
278
327
|
"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
|
-
],
|
|
328
|
+
"warnings": import_warnings + [template_warning],
|
|
286
329
|
"verification": {
|
|
287
330
|
"schema_fingerprint": schema_fingerprint,
|
|
288
331
|
"template_url_resolved": False,
|
|
289
332
|
"template_downloaded": True,
|
|
290
333
|
"template_source": "local_generated",
|
|
334
|
+
"import_auth_prechecked": import_capability.get("auth_source") != "unknown",
|
|
291
335
|
},
|
|
292
336
|
}
|
|
293
337
|
return self._failed_template_result(
|
|
@@ -448,6 +492,7 @@ class ImportTools(ToolBase):
|
|
|
448
492
|
issues = deepcopy(effective_local_check["issues"])
|
|
449
493
|
can_import = bool(effective_local_check["can_import"])
|
|
450
494
|
backend_verification = None
|
|
495
|
+
backend_failure_error_code = None
|
|
451
496
|
if can_import:
|
|
452
497
|
try:
|
|
453
498
|
payload = self.backend.request_multipart(
|
|
@@ -478,19 +523,25 @@ class ImportTools(ToolBase):
|
|
|
478
523
|
)
|
|
479
524
|
except QingflowApiError as exc:
|
|
480
525
|
can_import = False
|
|
526
|
+
backend_error_code = _import_permission_error_code(
|
|
527
|
+
exc,
|
|
528
|
+
permission_code="IMPORT_VERIFICATION_UNAUTHORIZED",
|
|
529
|
+
default="BACKEND_IMPORT_VERIFICATION_FAILED",
|
|
530
|
+
)
|
|
531
|
+
backend_failure_error_code = backend_error_code
|
|
481
532
|
issues.append(
|
|
482
533
|
_issue(
|
|
483
|
-
|
|
534
|
+
backend_error_code,
|
|
484
535
|
exc.message or "Backend import verification failed.",
|
|
485
536
|
severity="error",
|
|
486
537
|
)
|
|
487
538
|
)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
)
|
|
539
|
+
warning = {
|
|
540
|
+
"code": backend_error_code,
|
|
541
|
+
"message": "Backend verification failed; the file cannot be imported until verification succeeds.",
|
|
542
|
+
}
|
|
543
|
+
_copy_api_error_fields(warning, exc)
|
|
544
|
+
warnings.append(warning)
|
|
494
545
|
verification_id = str(uuid4())
|
|
495
546
|
verification_payload = {
|
|
496
547
|
"id": verification_id,
|
|
@@ -517,9 +568,11 @@ class ImportTools(ToolBase):
|
|
|
517
568
|
}
|
|
518
569
|
self._verification_store.put(verification_id, verification_payload)
|
|
519
570
|
return {
|
|
520
|
-
"ok":
|
|
571
|
+
"ok": can_import,
|
|
521
572
|
"status": "success" if can_import else "failed",
|
|
522
|
-
"error_code": None
|
|
573
|
+
"error_code": None
|
|
574
|
+
if can_import
|
|
575
|
+
else (effective_local_check.get("error_code") or local_check.get("error_code") or backend_failure_error_code or "IMPORT_VERIFICATION_FAILED"),
|
|
523
576
|
"can_import": can_import,
|
|
524
577
|
"verification_id": verification_id,
|
|
525
578
|
"file_path": str(path.resolve()),
|
|
@@ -553,7 +606,11 @@ class ImportTools(ToolBase):
|
|
|
553
606
|
try:
|
|
554
607
|
return self._run(profile, runner)
|
|
555
608
|
except RuntimeError as exc:
|
|
556
|
-
return self._runtime_error_as_result(
|
|
609
|
+
return self._runtime_error_as_result(
|
|
610
|
+
exc,
|
|
611
|
+
error_code="IMPORT_VERIFICATION_FAILED",
|
|
612
|
+
extra={"can_import": _runtime_import_can_import_value(exc)},
|
|
613
|
+
)
|
|
557
614
|
|
|
558
615
|
@tool_cn_name("导入修复")
|
|
559
616
|
def record_import_repair_local(
|
|
@@ -729,8 +786,14 @@ class ImportTools(ToolBase):
|
|
|
729
786
|
excel_name=str(stored.get("file_name") or current_path.name),
|
|
730
787
|
)
|
|
731
788
|
except QingflowApiError as exc:
|
|
732
|
-
error_code =
|
|
733
|
-
|
|
789
|
+
error_code = (
|
|
790
|
+
"IMPORT_SOCKET_ACK_TIMEOUT"
|
|
791
|
+
if exc.details and exc.details.get("error_code") == "IMPORT_SOCKET_ACK_TIMEOUT"
|
|
792
|
+
else _import_permission_error_code(exc, permission_code="IMPORT_START_UNAUTHORIZED", default="IMPORT_START_FAILED")
|
|
793
|
+
)
|
|
794
|
+
details: JSONObject = {"accepted": False, "file_url": file_url}
|
|
795
|
+
_copy_api_error_fields(details, exc)
|
|
796
|
+
return self._failed_start_result(error_code=error_code, message=exc.message, extra=details)
|
|
734
797
|
import_id = str(socket_result.get("import_id") or "")
|
|
735
798
|
process_id_str = _normalize_optional_text(socket_result.get("process_id_str"))
|
|
736
799
|
started_at = _utc_now().isoformat()
|
|
@@ -753,9 +816,11 @@ class ImportTools(ToolBase):
|
|
|
753
816
|
"ok": True,
|
|
754
817
|
"status": "accepted",
|
|
755
818
|
"accepted": True,
|
|
819
|
+
"write_executed": True,
|
|
820
|
+
"safe_to_retry": False,
|
|
756
821
|
"import_id": import_id,
|
|
757
822
|
"process_id_str": process_id_str,
|
|
758
|
-
|
|
823
|
+
"source_file_name": str(stored.get("file_name") or current_path.name),
|
|
759
824
|
"file_url": file_url,
|
|
760
825
|
"warnings": warnings,
|
|
761
826
|
"verification": {
|
|
@@ -838,12 +903,66 @@ class ImportTools(ToolBase):
|
|
|
838
903
|
if local_job is None and not normalized_import_id and not normalized_process_id:
|
|
839
904
|
recent = [item for item in self._job_store.list() if str(item.get("app_key")) == resolved_app_key]
|
|
840
905
|
local_job = recent[0] if recent else None
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
906
|
+
try:
|
|
907
|
+
page = self.backend.request(
|
|
908
|
+
"GET",
|
|
909
|
+
context,
|
|
910
|
+
"/app/apply/dataImport/record",
|
|
911
|
+
params={"appKey": resolved_app_key, "pageNum": 1, "pageSize": 100},
|
|
912
|
+
)
|
|
913
|
+
except QingflowApiError as exc:
|
|
914
|
+
error_code = _import_permission_error_code(exc, permission_code="IMPORT_STATUS_UNAUTHORIZED", default="IMPORT_STATUS_UNAVAILABLE")
|
|
915
|
+
details: JSONObject = {
|
|
916
|
+
"app_key": resolved_app_key,
|
|
917
|
+
"import_id": normalized_import_id,
|
|
918
|
+
"process_id_str": effective_process_id,
|
|
919
|
+
"verification": {
|
|
920
|
+
"status_lookup_completed": False,
|
|
921
|
+
"process_id_verified": bool(effective_process_id),
|
|
922
|
+
},
|
|
923
|
+
}
|
|
924
|
+
_copy_api_error_fields(details, exc)
|
|
925
|
+
if _is_import_permission_error(exc):
|
|
926
|
+
return {
|
|
927
|
+
"ok": True,
|
|
928
|
+
"status": "unknown",
|
|
929
|
+
"error_code": error_code,
|
|
930
|
+
"app_key": resolved_app_key,
|
|
931
|
+
"import_id": normalized_import_id or (local_job.get("import_id") if isinstance(local_job, dict) else None),
|
|
932
|
+
"process_id_str": effective_process_id,
|
|
933
|
+
"matched_by": "local_job" if isinstance(local_job, dict) else None,
|
|
934
|
+
"source_file_name": local_job.get("source_file_name") if isinstance(local_job, dict) else None,
|
|
935
|
+
"total_rows": None,
|
|
936
|
+
"success_rows": None,
|
|
937
|
+
"failed_rows": None,
|
|
938
|
+
"progress": None,
|
|
939
|
+
"error_file_urls": [],
|
|
940
|
+
"operate_time": None,
|
|
941
|
+
"operate_user": None,
|
|
942
|
+
"accepted": bool(local_job or effective_process_id),
|
|
943
|
+
"warnings": [
|
|
944
|
+
{
|
|
945
|
+
"code": error_code,
|
|
946
|
+
"message": "import history is not readable; final import status is unknown",
|
|
947
|
+
**{
|
|
948
|
+
key: details.get(key)
|
|
949
|
+
for key in ("category", "backend_code", "http_status", "request_id")
|
|
950
|
+
if details.get(key) is not None
|
|
951
|
+
},
|
|
952
|
+
}
|
|
953
|
+
],
|
|
954
|
+
"verification": details["verification"],
|
|
955
|
+
"details": {"status_readback_error": details},
|
|
956
|
+
"backend_code": details.get("backend_code"),
|
|
957
|
+
"request_id": details.get("request_id"),
|
|
958
|
+
"http_status": details.get("http_status"),
|
|
959
|
+
"message": "import history is not readable; final import status is unknown",
|
|
960
|
+
}
|
|
961
|
+
return self._failed_status_result(
|
|
962
|
+
error_code=error_code,
|
|
963
|
+
message=exc.message,
|
|
964
|
+
extra=details,
|
|
965
|
+
)
|
|
847
966
|
records = _extract_import_records(page)
|
|
848
967
|
matched_record, matched_by = _match_import_record(
|
|
849
968
|
records,
|
|
@@ -916,7 +1035,19 @@ class ImportTools(ToolBase):
|
|
|
916
1035
|
try:
|
|
917
1036
|
return self._run(profile, runner)
|
|
918
1037
|
except RuntimeError as exc:
|
|
919
|
-
return self._runtime_error_as_result(
|
|
1038
|
+
return self._runtime_error_as_result(
|
|
1039
|
+
exc,
|
|
1040
|
+
error_code="IMPORT_STATUS_AMBIGUOUS",
|
|
1041
|
+
extra={
|
|
1042
|
+
"app_key": normalized_app_key,
|
|
1043
|
+
"import_id": normalized_import_id,
|
|
1044
|
+
"process_id_str": normalized_process_id,
|
|
1045
|
+
"verification": {
|
|
1046
|
+
"status_lookup_completed": False,
|
|
1047
|
+
"process_id_verified": bool(normalized_process_id),
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
)
|
|
920
1051
|
|
|
921
1052
|
def _resolve_import_schema_bundle(
|
|
922
1053
|
self,
|
|
@@ -1218,7 +1349,9 @@ class ImportTools(ToolBase):
|
|
|
1218
1349
|
"message": f"Member candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
|
|
1219
1350
|
}
|
|
1220
1351
|
raise
|
|
1221
|
-
except RuntimeError:
|
|
1352
|
+
except RuntimeError as exc:
|
|
1353
|
+
if not _runtime_candidate_validation_skippable(exc):
|
|
1354
|
+
raise
|
|
1222
1355
|
return None, {
|
|
1223
1356
|
"code": "MEMBER_CANDIDATE_VALIDATION_SKIPPED",
|
|
1224
1357
|
"message": f"Member candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
|
|
@@ -1272,7 +1405,9 @@ class ImportTools(ToolBase):
|
|
|
1272
1405
|
"message": f"Department candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
|
|
1273
1406
|
}
|
|
1274
1407
|
raise
|
|
1275
|
-
except RuntimeError:
|
|
1408
|
+
except RuntimeError as exc:
|
|
1409
|
+
if not _runtime_candidate_validation_skippable(exc):
|
|
1410
|
+
raise
|
|
1276
1411
|
return None, {
|
|
1277
1412
|
"code": "DEPARTMENT_CANDIDATE_VALIDATION_SKIPPED",
|
|
1278
1413
|
"message": f"Department candidate scope for column '{column['title']}' could not be resolved safely during local precheck.",
|
|
@@ -1397,11 +1532,31 @@ class ImportTools(ToolBase):
|
|
|
1397
1532
|
|
|
1398
1533
|
def _fetch_import_capability(self, context, app_key: str) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
1399
1534
|
"""执行内部辅助逻辑。"""
|
|
1535
|
+
base_info_error: QingflowApiError | None = None
|
|
1400
1536
|
try:
|
|
1401
1537
|
payload = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
1402
|
-
except QingflowApiError:
|
|
1538
|
+
except QingflowApiError as exc:
|
|
1539
|
+
if not _is_optional_import_capability_error(exc):
|
|
1540
|
+
raise
|
|
1541
|
+
base_info_error = exc
|
|
1403
1542
|
payload = None
|
|
1404
|
-
|
|
1543
|
+
capability, warnings = _derive_import_capability(payload)
|
|
1544
|
+
if base_info_error is not None:
|
|
1545
|
+
for warning in warnings:
|
|
1546
|
+
if warning.get("code") != "IMPORT_CAPABILITY_UNAVAILABLE":
|
|
1547
|
+
continue
|
|
1548
|
+
warning["message"] = (
|
|
1549
|
+
"import capability precheck could not read app baseInfo; continuing with applicant schema "
|
|
1550
|
+
"when available, but do not treat import permission as verified."
|
|
1551
|
+
)
|
|
1552
|
+
if base_info_error.backend_code is not None:
|
|
1553
|
+
warning["backend_code"] = base_info_error.backend_code
|
|
1554
|
+
if base_info_error.http_status is not None:
|
|
1555
|
+
warning["http_status"] = base_info_error.http_status
|
|
1556
|
+
if base_info_error.request_id is not None:
|
|
1557
|
+
warning["request_id"] = base_info_error.request_id
|
|
1558
|
+
break
|
|
1559
|
+
return capability, warnings
|
|
1405
1560
|
|
|
1406
1561
|
def _write_local_template(
|
|
1407
1562
|
self,
|
|
@@ -1430,9 +1585,10 @@ class ImportTools(ToolBase):
|
|
|
1430
1585
|
error_code: str,
|
|
1431
1586
|
message: str,
|
|
1432
1587
|
request_route: JSONObject | None = None,
|
|
1588
|
+
extra: dict[str, Any] | None = None,
|
|
1433
1589
|
) -> dict[str, Any]:
|
|
1434
1590
|
"""执行内部辅助逻辑。"""
|
|
1435
|
-
|
|
1591
|
+
payload = {
|
|
1436
1592
|
"ok": False,
|
|
1437
1593
|
"status": "failed",
|
|
1438
1594
|
"error_code": error_code,
|
|
@@ -1446,6 +1602,9 @@ class ImportTools(ToolBase):
|
|
|
1446
1602
|
"verification": {"template_url_resolved": False},
|
|
1447
1603
|
"message": message,
|
|
1448
1604
|
}
|
|
1605
|
+
if extra:
|
|
1606
|
+
payload.update(extra)
|
|
1607
|
+
return payload
|
|
1449
1608
|
|
|
1450
1609
|
def _failed_verify_result(
|
|
1451
1610
|
self,
|
|
@@ -1458,7 +1617,7 @@ class ImportTools(ToolBase):
|
|
|
1458
1617
|
) -> dict[str, Any]:
|
|
1459
1618
|
"""执行内部辅助逻辑。"""
|
|
1460
1619
|
payload = {
|
|
1461
|
-
"ok":
|
|
1620
|
+
"ok": False,
|
|
1462
1621
|
"status": "failed",
|
|
1463
1622
|
"error_code": error_code,
|
|
1464
1623
|
"app_key": app_key,
|
|
@@ -1573,18 +1732,21 @@ class ImportTools(ToolBase):
|
|
|
1573
1732
|
extra: dict[str, Any] | None = None,
|
|
1574
1733
|
) -> dict[str, Any]:
|
|
1575
1734
|
"""执行内部辅助逻辑。"""
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
except json.JSONDecodeError:
|
|
1579
|
-
payload = {"message": str(error)}
|
|
1735
|
+
payload = _runtime_error_payload(error)
|
|
1736
|
+
details = payload.get("details") if isinstance(payload.get("details"), dict) else {}
|
|
1580
1737
|
response = {
|
|
1581
1738
|
"ok": False,
|
|
1582
1739
|
"status": "failed",
|
|
1583
|
-
"error_code":
|
|
1740
|
+
"error_code": details.get("error_code") or _runtime_error_code(payload, default=error_code),
|
|
1584
1741
|
"warnings": [],
|
|
1585
1742
|
"verification": {},
|
|
1586
1743
|
"message": payload.get("message") or str(error),
|
|
1587
1744
|
}
|
|
1745
|
+
for key in ("category", "backend_code", "request_id", "http_status"):
|
|
1746
|
+
if key in payload:
|
|
1747
|
+
response[key] = payload.get(key)
|
|
1748
|
+
if details:
|
|
1749
|
+
response["details"] = details
|
|
1588
1750
|
if extra:
|
|
1589
1751
|
response.update(extra)
|
|
1590
1752
|
return response
|
|
@@ -2257,6 +2419,97 @@ def _normalize_import_status(value: Any) -> str:
|
|
|
2257
2419
|
return "unknown"
|
|
2258
2420
|
|
|
2259
2421
|
|
|
2422
|
+
def _runtime_error_payload(error: RuntimeError) -> JSONObject:
|
|
2423
|
+
try:
|
|
2424
|
+
payload = json.loads(str(error))
|
|
2425
|
+
except json.JSONDecodeError:
|
|
2426
|
+
return {"message": str(error)}
|
|
2427
|
+
return payload if isinstance(payload, dict) else {"message": str(error)}
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
def _runtime_import_can_import_value(error: RuntimeError) -> bool | None:
|
|
2431
|
+
payload = _runtime_error_payload(error)
|
|
2432
|
+
category = str(payload.get("category") or "").strip().lower()
|
|
2433
|
+
if category == "auth" or _coerce_int(payload.get("http_status")) == 401 or message_looks_like_invalid_token(payload.get("message")):
|
|
2434
|
+
return None
|
|
2435
|
+
backend_code = backend_code_int(
|
|
2436
|
+
QingflowApiError(
|
|
2437
|
+
category=category,
|
|
2438
|
+
message=str(payload.get("message") or ""),
|
|
2439
|
+
backend_code=payload.get("backend_code"),
|
|
2440
|
+
http_status=_coerce_int(payload.get("http_status")),
|
|
2441
|
+
)
|
|
2442
|
+
)
|
|
2443
|
+
return False if backend_code in {40002, 40027} else None
|
|
2444
|
+
|
|
2445
|
+
|
|
2446
|
+
def _runtime_candidate_validation_skippable(error: RuntimeError) -> bool:
|
|
2447
|
+
payload = _runtime_error_payload(error)
|
|
2448
|
+
category = str(payload.get("category") or "").strip().lower()
|
|
2449
|
+
if category == "not_supported":
|
|
2450
|
+
return True
|
|
2451
|
+
if category in {"auth", "workspace"}:
|
|
2452
|
+
return False
|
|
2453
|
+
if message_looks_like_invalid_token(payload.get("message")):
|
|
2454
|
+
return False
|
|
2455
|
+
if _coerce_int(payload.get("http_status")) == 401:
|
|
2456
|
+
return False
|
|
2457
|
+
backend_code = backend_code_int(
|
|
2458
|
+
QingflowApiError(
|
|
2459
|
+
category=category,
|
|
2460
|
+
message=str(payload.get("message") or ""),
|
|
2461
|
+
backend_code=payload.get("backend_code"),
|
|
2462
|
+
http_status=_coerce_int(payload.get("http_status")),
|
|
2463
|
+
)
|
|
2464
|
+
)
|
|
2465
|
+
return backend_code in {40002, 40027, 404} or _coerce_int(payload.get("http_status")) == 404
|
|
2466
|
+
|
|
2467
|
+
|
|
2468
|
+
def _runtime_error_code(payload: JSONObject, *, default: str) -> str:
|
|
2469
|
+
category = str(payload.get("category") or "").strip().lower()
|
|
2470
|
+
http_status = _coerce_int(payload.get("http_status"))
|
|
2471
|
+
if category == "auth" or http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
|
|
2472
|
+
return "AUTH_REQUIRED"
|
|
2473
|
+
if category == "workspace":
|
|
2474
|
+
return "WORKSPACE_NOT_SELECTED"
|
|
2475
|
+
return default
|
|
2476
|
+
|
|
2477
|
+
|
|
2478
|
+
def _is_import_permission_error(error: QingflowApiError) -> bool:
|
|
2479
|
+
if is_auth_like_error(error):
|
|
2480
|
+
return False
|
|
2481
|
+
return backend_code_int(error) in {40002, 40027}
|
|
2482
|
+
|
|
2483
|
+
|
|
2484
|
+
def _is_import_capability_auth_error(error: QingflowApiError) -> bool:
|
|
2485
|
+
return is_auth_like_error(error)
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
def _is_optional_import_capability_error(error: QingflowApiError) -> bool:
|
|
2489
|
+
if _is_import_capability_auth_error(error):
|
|
2490
|
+
return False
|
|
2491
|
+
return _is_import_permission_error(error) or backend_code_int(error) == 404 or error.http_status == 404
|
|
2492
|
+
|
|
2493
|
+
|
|
2494
|
+
def _import_permission_error_code(error: QingflowApiError, *, permission_code: str, default: str) -> str:
|
|
2495
|
+
if is_auth_like_error(error):
|
|
2496
|
+
return "AUTH_REQUIRED"
|
|
2497
|
+
if _is_import_permission_error(error):
|
|
2498
|
+
return permission_code
|
|
2499
|
+
return default
|
|
2500
|
+
|
|
2501
|
+
|
|
2502
|
+
def _copy_api_error_fields(payload: dict[str, Any], error: QingflowApiError) -> None:
|
|
2503
|
+
if error.category:
|
|
2504
|
+
payload["category"] = error.category
|
|
2505
|
+
if error.backend_code is not None:
|
|
2506
|
+
payload["backend_code"] = error.backend_code
|
|
2507
|
+
if error.http_status is not None:
|
|
2508
|
+
payload["http_status"] = error.http_status
|
|
2509
|
+
if error.request_id:
|
|
2510
|
+
payload["request_id"] = error.request_id
|
|
2511
|
+
|
|
2512
|
+
|
|
2260
2513
|
def _normalize_error_file_urls(value: Any) -> list[str]:
|
|
2261
2514
|
if isinstance(value, list):
|
|
2262
2515
|
return [str(item).strip() for item in value if str(item).strip()]
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from mcp.server.fastmcp import FastMCP
|
|
4
4
|
|
|
5
5
|
from ..config import DEFAULT_PROFILE
|
|
6
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
6
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from .base import ToolBase, tool_cn_name
|
|
9
9
|
|
|
@@ -85,8 +85,26 @@ class NavigationTools(ToolBase):
|
|
|
85
85
|
params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
|
|
86
86
|
if query_key:
|
|
87
87
|
params["queryCondition"] = query_key
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
fallback_error: QingflowApiError | None = None
|
|
89
|
+
try:
|
|
90
|
+
result = self.backend.request("GET", context, "/navigation/page", params=params)
|
|
91
|
+
response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "status": "draft", "page": result}
|
|
92
|
+
except QingflowApiError as exc:
|
|
93
|
+
if not _is_optional_draft_navigation_read_error(exc):
|
|
94
|
+
raise
|
|
95
|
+
fallback_error = exc
|
|
96
|
+
result = self.backend.request("GET", context, "/navigation", params={"pageNum": page_num, "pageSize": page_size})
|
|
97
|
+
response = {
|
|
98
|
+
"profile": profile,
|
|
99
|
+
"ws_id": session_profile.selected_ws_id,
|
|
100
|
+
"status": "published",
|
|
101
|
+
"requested_status": "draft",
|
|
102
|
+
"page": result,
|
|
103
|
+
}
|
|
104
|
+
if fallback_error is not None:
|
|
105
|
+
response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_PAGE_UNAVAILABLE", fallback_error)]
|
|
106
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
107
|
+
return response
|
|
90
108
|
|
|
91
109
|
return self._run(profile, runner)
|
|
92
110
|
|
|
@@ -97,8 +115,26 @@ class NavigationTools(ToolBase):
|
|
|
97
115
|
params: JSONObject = {}
|
|
98
116
|
if query_key:
|
|
99
117
|
params["queryCondition"] = query_key
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
fallback_error: QingflowApiError | None = None
|
|
119
|
+
try:
|
|
120
|
+
result = self.backend.request("GET", context, "/navigation/all", params=params)
|
|
121
|
+
response = {"profile": profile, "ws_id": session_profile.selected_ws_id, "status": "draft", "items": result}
|
|
122
|
+
except QingflowApiError as exc:
|
|
123
|
+
if not _is_optional_draft_navigation_read_error(exc):
|
|
124
|
+
raise
|
|
125
|
+
fallback_error = exc
|
|
126
|
+
result = self.backend.request("GET", context, "/navigation", params={"pageNum": 1, "pageSize": 1000})
|
|
127
|
+
response = {
|
|
128
|
+
"profile": profile,
|
|
129
|
+
"ws_id": session_profile.selected_ws_id,
|
|
130
|
+
"status": "published",
|
|
131
|
+
"requested_status": "draft",
|
|
132
|
+
"items": result,
|
|
133
|
+
}
|
|
134
|
+
if fallback_error is not None:
|
|
135
|
+
response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_ALL_UNAVAILABLE", fallback_error)]
|
|
136
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
137
|
+
return response
|
|
102
138
|
|
|
103
139
|
return self._run(profile, runner)
|
|
104
140
|
|
|
@@ -108,13 +144,39 @@ class NavigationTools(ToolBase):
|
|
|
108
144
|
self._require_navigation_item_id(navigation_item_id)
|
|
109
145
|
|
|
110
146
|
def runner(session_profile, context):
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
requested_status = str(status or "draft")
|
|
148
|
+
fallback_error: QingflowApiError | None = None
|
|
149
|
+
try:
|
|
150
|
+
result = self.backend.request(
|
|
151
|
+
"GET",
|
|
152
|
+
context,
|
|
153
|
+
f"/navigation/detail/{navigation_item_id}",
|
|
154
|
+
params={"status": requested_status},
|
|
155
|
+
)
|
|
156
|
+
effective_status = requested_status
|
|
157
|
+
except QingflowApiError as exc:
|
|
158
|
+
if requested_status != "draft" or not _is_optional_draft_navigation_read_error(exc):
|
|
159
|
+
raise
|
|
160
|
+
fallback_error = exc
|
|
161
|
+
effective_status = "published"
|
|
162
|
+
result = self.backend.request(
|
|
163
|
+
"GET",
|
|
164
|
+
context,
|
|
165
|
+
f"/navigation/detail/{navigation_item_id}",
|
|
166
|
+
params={"status": effective_status},
|
|
167
|
+
)
|
|
168
|
+
response = {
|
|
169
|
+
"profile": profile,
|
|
170
|
+
"ws_id": session_profile.selected_ws_id,
|
|
171
|
+
"navigation_item_id": navigation_item_id,
|
|
172
|
+
"status": effective_status,
|
|
173
|
+
"requested_status": requested_status,
|
|
174
|
+
"result": result,
|
|
175
|
+
}
|
|
176
|
+
if fallback_error is not None:
|
|
177
|
+
response["warnings"] = [_navigation_fallback_warning("NAVIGATION_DRAFT_DETAIL_UNAVAILABLE", fallback_error)]
|
|
178
|
+
response["verification"] = {"draft_readable": False, "published_fallback_used": True}
|
|
179
|
+
return response
|
|
118
180
|
|
|
119
181
|
return self._run(profile, runner)
|
|
120
182
|
|
|
@@ -208,3 +270,20 @@ class NavigationTools(ToolBase):
|
|
|
208
270
|
"""执行内部辅助逻辑。"""
|
|
209
271
|
if navigation_item_id <= 0:
|
|
210
272
|
raise_tool_error(QingflowApiError.config_error("navigation_item_id must be positive"))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_optional_draft_navigation_read_error(error: QingflowApiError) -> bool:
|
|
276
|
+
if is_auth_like_error(error):
|
|
277
|
+
return False
|
|
278
|
+
backend_code = backend_code_int(error)
|
|
279
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _navigation_fallback_warning(code: str, error: QingflowApiError) -> JSONObject:
|
|
283
|
+
return {
|
|
284
|
+
"code": code,
|
|
285
|
+
"message": "draft navigation data is unavailable; returned published navigation data instead",
|
|
286
|
+
"backend_code": error.backend_code,
|
|
287
|
+
"http_status": error.http_status,
|
|
288
|
+
"request_id": error.request_id,
|
|
289
|
+
}
|