@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.
- 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 +282 -102
- package/src/qingflow_mcp/builder_facade/service.py +4166 -929
- package/src/qingflow_mcp/cli/commands/builder.py +316 -298
- 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 +1780 -406
- 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
|
@@ -14,7 +14,7 @@ from datetime import UTC, datetime, timedelta
|
|
|
14
14
|
from decimal import Decimal, InvalidOperation
|
|
15
15
|
from io import BytesIO
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any, cast
|
|
17
|
+
from typing import Any, Callable, cast
|
|
18
18
|
from urllib.parse import parse_qs, unquote, urlsplit
|
|
19
19
|
from uuid import uuid4
|
|
20
20
|
from xml.etree import ElementTree
|
|
@@ -22,7 +22,7 @@ from xml.etree import ElementTree
|
|
|
22
22
|
from mcp.server.fastmcp import FastMCP
|
|
23
23
|
|
|
24
24
|
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE, DEFAULT_USER_AGENT, get_mcp_home
|
|
25
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
25
|
+
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
|
|
26
26
|
from ..id_utils import normalize_positive_id_int, stringify_backend_id
|
|
27
27
|
from ..json_types import JSONObject, JSONScalar, JSONValue
|
|
28
28
|
from ..list_type_labels import (
|
|
@@ -75,6 +75,22 @@ MAX_SUMMARY_PREVIEW_COLUMN_LIMIT = 6
|
|
|
75
75
|
VERIFY_TASK_FALLBACK_PAGE_SIZE = 50
|
|
76
76
|
VERIFY_TASK_FALLBACK_MAX_PAGES = 3
|
|
77
77
|
BACKEND_LIST_SEARCH_FIELD_LIMIT = 10
|
|
78
|
+
RECORD_WRITE_SYSTEM_FIELD_NAMES = {
|
|
79
|
+
"数据ID",
|
|
80
|
+
"编号",
|
|
81
|
+
"申请人",
|
|
82
|
+
"申请时间",
|
|
83
|
+
"创建人",
|
|
84
|
+
"创建时间",
|
|
85
|
+
"提交人",
|
|
86
|
+
"提交时间",
|
|
87
|
+
"更新时间",
|
|
88
|
+
"更新人",
|
|
89
|
+
"当前流程状态",
|
|
90
|
+
"当前处理人",
|
|
91
|
+
"当前处理节点",
|
|
92
|
+
"流程标题",
|
|
93
|
+
}
|
|
78
94
|
LOOKUP_RESOLUTION_MIN_SCORE = 0.92
|
|
79
95
|
LOOKUP_RESOLUTION_MIN_MARGIN = 0.08
|
|
80
96
|
LOOKUP_CONFIRMATION_CANDIDATE_LIMIT = 5
|
|
@@ -131,6 +147,27 @@ SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE = "updating this field may auto-fill or
|
|
|
131
147
|
SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE = "this field is usually filled from an upstream reference selection or default matching logic"
|
|
132
148
|
SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE = "this field participates in reference-driven auto-fill logic"
|
|
133
149
|
SCHEMA_LINKAGE_FORMULA_MESSAGE = "this field is usually derived by formula or default auto-fill logic"
|
|
150
|
+
OPTIONAL_SCHEMA_PERMISSION_CODES = {40002, 40027, 404}
|
|
151
|
+
RECORD_PERMISSION_DENIED_CODES = {40002, 40027}
|
|
152
|
+
SYSTEM_VIEW_LIST_TYPES = {int(list_type) for _view_id, list_type, _name in SYSTEM_VIEW_DEFINITIONS}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_optional_schema_permission_error(error: QingflowApiError) -> bool:
|
|
156
|
+
if is_auth_like_error(error):
|
|
157
|
+
return False
|
|
158
|
+
return backend_code_int(error) in OPTIONAL_SCHEMA_PERMISSION_CODES or error.http_status == 404
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_record_permission_denied_error(error: QingflowApiError) -> bool:
|
|
162
|
+
if is_auth_like_error(error):
|
|
163
|
+
return False
|
|
164
|
+
return backend_code_int(error) in RECORD_PERMISSION_DENIED_CODES
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _is_optional_record_auxiliary_lookup_error(error: QingflowApiError) -> bool:
|
|
168
|
+
if is_auth_like_error(error):
|
|
169
|
+
return False
|
|
170
|
+
return backend_code_int(error) in {40002, 40027, 404} or error.http_status == 404
|
|
134
171
|
|
|
135
172
|
|
|
136
173
|
@dataclass(slots=True)
|
|
@@ -215,6 +252,13 @@ class AccessibleViewRoute:
|
|
|
215
252
|
view_type: str | None = None
|
|
216
253
|
|
|
217
254
|
|
|
255
|
+
def _prefer_custom_update_routes(routes: list[AccessibleViewRoute]) -> list[AccessibleViewRoute]:
|
|
256
|
+
return [
|
|
257
|
+
*[route for route in routes if route.kind == "custom"],
|
|
258
|
+
*[route for route in routes if route.kind != "custom"],
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
|
|
218
262
|
@dataclass(slots=True)
|
|
219
263
|
class RecordContextRouteProbe:
|
|
220
264
|
route: AccessibleViewRoute
|
|
@@ -322,11 +366,12 @@ class RecordTools(ToolBase):
|
|
|
322
366
|
"""注册当前工具到 MCP 服务。"""
|
|
323
367
|
@mcp.tool()
|
|
324
368
|
def record_insert_schema_get(
|
|
369
|
+
profile: str = DEFAULT_PROFILE,
|
|
325
370
|
app_key: str = "",
|
|
326
371
|
output_profile: str = "normal",
|
|
327
372
|
) -> JSONObject:
|
|
328
373
|
return self.record_insert_schema_get_public(
|
|
329
|
-
profile=
|
|
374
|
+
profile=profile,
|
|
330
375
|
app_key=app_key,
|
|
331
376
|
output_profile=output_profile,
|
|
332
377
|
)
|
|
@@ -409,6 +454,7 @@ class RecordTools(ToolBase):
|
|
|
409
454
|
where: list[JSONObject] | None = None,
|
|
410
455
|
order_by: list[JSONObject] | None = None,
|
|
411
456
|
page: int = 1,
|
|
457
|
+
page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
|
|
412
458
|
view_id: str | None = None,
|
|
413
459
|
output_profile: str = "normal",
|
|
414
460
|
) -> JSONObject:
|
|
@@ -421,6 +467,7 @@ class RecordTools(ToolBase):
|
|
|
421
467
|
where=where or [],
|
|
422
468
|
order_by=order_by or [],
|
|
423
469
|
page=page,
|
|
470
|
+
page_size=page_size,
|
|
424
471
|
view_id=view_id,
|
|
425
472
|
list_type=None,
|
|
426
473
|
view_key=None,
|
|
@@ -436,6 +483,7 @@ class RecordTools(ToolBase):
|
|
|
436
483
|
)
|
|
437
484
|
)
|
|
438
485
|
def record_access(
|
|
486
|
+
profile: str = DEFAULT_PROFILE,
|
|
439
487
|
app_key: str = "",
|
|
440
488
|
view_id: str = "",
|
|
441
489
|
columns: list[JSONObject | int] | None = None,
|
|
@@ -443,7 +491,7 @@ class RecordTools(ToolBase):
|
|
|
443
491
|
order_by: list[JSONObject] | None = None,
|
|
444
492
|
) -> JSONObject:
|
|
445
493
|
return self.record_access(
|
|
446
|
-
profile=
|
|
494
|
+
profile=profile,
|
|
447
495
|
app_key=app_key,
|
|
448
496
|
view_id=view_id,
|
|
449
497
|
columns=columns or [],
|
|
@@ -458,7 +506,6 @@ class RecordTools(ToolBase):
|
|
|
458
506
|
record_id: str = "",
|
|
459
507
|
columns: list[JSONObject | int] | None = None,
|
|
460
508
|
view_id: str | None = None,
|
|
461
|
-
workflow_node_id: int | None = None,
|
|
462
509
|
output_profile: str = "detail_context",
|
|
463
510
|
) -> JSONObject:
|
|
464
511
|
return self.record_get_public(
|
|
@@ -467,11 +514,11 @@ class RecordTools(ToolBase):
|
|
|
467
514
|
record_id=record_id,
|
|
468
515
|
columns=columns or [],
|
|
469
516
|
view_id=view_id,
|
|
470
|
-
workflow_node_id=
|
|
517
|
+
workflow_node_id=None,
|
|
471
518
|
output_profile=output_profile,
|
|
472
519
|
)
|
|
473
520
|
|
|
474
|
-
@mcp.tool(description="Read all visible data logs and workflow logs for one Qingflow record into local JSONL files. This tool hides pagination and returns file paths plus completeness metadata.")
|
|
521
|
+
@mcp.tool(description="Read all visible data logs and workflow logs for one Qingflow record into local JSONL files. Requires the same view_id as the frontend record context. This tool hides pagination and returns file paths plus completeness metadata.")
|
|
475
522
|
def record_logs_get(
|
|
476
523
|
profile: str = DEFAULT_PROFILE,
|
|
477
524
|
app_key: str = "",
|
|
@@ -487,12 +534,13 @@ class RecordTools(ToolBase):
|
|
|
487
534
|
|
|
488
535
|
@mcp.tool()
|
|
489
536
|
def record_browse_schema_get(
|
|
537
|
+
profile: str = DEFAULT_PROFILE,
|
|
490
538
|
app_key: str = "",
|
|
491
539
|
view_id: str = "",
|
|
492
540
|
output_profile: str = "normal",
|
|
493
541
|
) -> JSONObject:
|
|
494
542
|
return self.record_browse_schema_get_public(
|
|
495
|
-
profile=
|
|
543
|
+
profile=profile,
|
|
496
544
|
app_key=app_key,
|
|
497
545
|
view_id=view_id,
|
|
498
546
|
output_profile=output_profile,
|
|
@@ -500,14 +548,17 @@ class RecordTools(ToolBase):
|
|
|
500
548
|
|
|
501
549
|
@mcp.tool()
|
|
502
550
|
def record_update_schema_get(
|
|
551
|
+
profile: str = DEFAULT_PROFILE,
|
|
503
552
|
app_key: str = "",
|
|
504
553
|
record_id: str = "",
|
|
554
|
+
view_id: str | None = None,
|
|
505
555
|
output_profile: str = "normal",
|
|
506
556
|
) -> JSONObject:
|
|
507
557
|
return self.record_update_schema_get_public(
|
|
508
|
-
profile=
|
|
558
|
+
profile=profile,
|
|
509
559
|
app_key=app_key,
|
|
510
560
|
record_id=record_id,
|
|
561
|
+
view_id=view_id,
|
|
511
562
|
output_profile=output_profile,
|
|
512
563
|
)
|
|
513
564
|
|
|
@@ -520,13 +571,14 @@ class RecordTools(ToolBase):
|
|
|
520
571
|
)
|
|
521
572
|
)
|
|
522
573
|
def record_insert(
|
|
574
|
+
profile: str = DEFAULT_PROFILE,
|
|
523
575
|
app_key: str = "",
|
|
524
576
|
items: list[JSONObject] | None = None,
|
|
525
577
|
verify_write: bool = True,
|
|
526
578
|
output_profile: str = "normal",
|
|
527
579
|
) -> JSONObject:
|
|
528
580
|
return self.record_insert_public(
|
|
529
|
-
profile=
|
|
581
|
+
profile=profile,
|
|
530
582
|
app_key=app_key,
|
|
531
583
|
items=items,
|
|
532
584
|
verify_write=verify_write,
|
|
@@ -537,25 +589,29 @@ class RecordTools(ToolBase):
|
|
|
537
589
|
description=(
|
|
538
590
|
"Update one Qingflow record using a field map. "
|
|
539
591
|
"For simple field changes, call this tool directly after the target record is clear. "
|
|
592
|
+
"Pass view_id when the frontend detail view is known; the tool will try that view first. "
|
|
540
593
|
"It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
|
|
541
594
|
"Use record_update_schema_get for diagnostics or complex field-scope inspection."
|
|
542
595
|
)
|
|
543
596
|
)
|
|
544
597
|
def record_update(
|
|
598
|
+
profile: str = DEFAULT_PROFILE,
|
|
545
599
|
app_key: str = "",
|
|
546
600
|
record_id: str | None = None,
|
|
547
601
|
fields: JSONObject | None = None,
|
|
548
602
|
items: list[JSONObject] | None = None,
|
|
603
|
+
view_id: str | None = None,
|
|
549
604
|
dry_run: bool = False,
|
|
550
605
|
verify_write: bool = True,
|
|
551
606
|
output_profile: str = "normal",
|
|
552
607
|
) -> JSONObject:
|
|
553
608
|
return self.record_update_public(
|
|
554
|
-
profile=
|
|
609
|
+
profile=profile,
|
|
555
610
|
app_key=app_key,
|
|
556
611
|
record_id=record_id,
|
|
557
612
|
fields=fields,
|
|
558
613
|
items=items,
|
|
614
|
+
view_id=view_id,
|
|
559
615
|
dry_run=dry_run,
|
|
560
616
|
verify_write=verify_write,
|
|
561
617
|
output_profile=output_profile,
|
|
@@ -564,20 +620,23 @@ class RecordTools(ToolBase):
|
|
|
564
620
|
@mcp.tool(
|
|
565
621
|
description=(
|
|
566
622
|
"Delete Qingflow records by record_id or record_ids. "
|
|
567
|
-
"
|
|
623
|
+
"Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
|
|
568
624
|
)
|
|
569
625
|
)
|
|
570
626
|
def record_delete(
|
|
627
|
+
profile: str = DEFAULT_PROFILE,
|
|
571
628
|
app_key: str = "",
|
|
572
629
|
record_id: str | None = None,
|
|
573
630
|
record_ids: list[str] | None = None,
|
|
631
|
+
view_id: str | None = None,
|
|
574
632
|
output_profile: str = "normal",
|
|
575
633
|
) -> JSONObject:
|
|
576
634
|
return self.record_delete_public(
|
|
577
|
-
profile=
|
|
635
|
+
profile=profile,
|
|
578
636
|
app_key=app_key,
|
|
579
637
|
record_id=record_id,
|
|
580
638
|
record_ids=record_ids or [],
|
|
639
|
+
view_id=view_id,
|
|
581
640
|
output_profile=output_profile,
|
|
582
641
|
)
|
|
583
642
|
|
|
@@ -849,6 +908,7 @@ class RecordTools(ToolBase):
|
|
|
849
908
|
profile: str = DEFAULT_PROFILE,
|
|
850
909
|
app_key: str,
|
|
851
910
|
record_id: Any,
|
|
911
|
+
view_id: str | None = None,
|
|
852
912
|
output_profile: str = "normal",
|
|
853
913
|
) -> JSONObject:
|
|
854
914
|
"""执行记录相关逻辑。"""
|
|
@@ -860,21 +920,44 @@ class RecordTools(ToolBase):
|
|
|
860
920
|
def runner(session_profile, context):
|
|
861
921
|
request_route = self._request_route_payload(context)
|
|
862
922
|
self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
923
|
+
linkage_payloads_by_field_id: dict[str, JSONObject] = {}
|
|
924
|
+
try:
|
|
925
|
+
app_schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
|
|
926
|
+
except QingflowApiError as exc:
|
|
927
|
+
if not _is_optional_schema_permission_error(exc):
|
|
928
|
+
raise
|
|
929
|
+
else:
|
|
930
|
+
question_relations = _collect_question_relations(app_schema)
|
|
931
|
+
app_index = _build_applicant_top_level_field_index(app_schema)
|
|
932
|
+
linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
933
|
+
linked_field_ids.update(_collect_option_linked_field_ids(app_index))
|
|
934
|
+
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
935
|
+
app_schema,
|
|
936
|
+
linked_field_ids=linked_field_ids,
|
|
937
|
+
)
|
|
938
|
+
app_index = _merge_field_indexes(app_index, linked_hidden_index)
|
|
939
|
+
linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
|
|
940
|
+
index=app_index,
|
|
941
|
+
question_relations=question_relations,
|
|
942
|
+
)
|
|
943
|
+
preferred_view_id = _normalize_optional_text(view_id)
|
|
877
944
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
945
|
+
if preferred_view_id:
|
|
946
|
+
preferred_route = next(
|
|
947
|
+
(
|
|
948
|
+
route
|
|
949
|
+
for route in candidate_routes
|
|
950
|
+
if route.view_id == preferred_view_id
|
|
951
|
+
),
|
|
952
|
+
None,
|
|
953
|
+
)
|
|
954
|
+
if preferred_route is None:
|
|
955
|
+
raise_tool_error(
|
|
956
|
+
QingflowApiError.config_error(
|
|
957
|
+
f"view_id '{preferred_view_id}' is not an accessible update candidate"
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
candidate_routes = [preferred_route]
|
|
878
961
|
probes = self._probe_candidate_record_contexts(
|
|
879
962
|
context,
|
|
880
963
|
app_key=app_key,
|
|
@@ -976,6 +1059,7 @@ class RecordTools(ToolBase):
|
|
|
976
1059
|
output_profile=normalized_output_profile,
|
|
977
1060
|
view_probe_summary=probe_summary,
|
|
978
1061
|
ambiguous_fields=[],
|
|
1062
|
+
preferred_view_id=preferred_view_id,
|
|
979
1063
|
)
|
|
980
1064
|
|
|
981
1065
|
ambiguous_field_ids: set[int] = set()
|
|
@@ -1022,6 +1106,7 @@ class RecordTools(ToolBase):
|
|
|
1022
1106
|
output_profile=normalized_output_profile,
|
|
1023
1107
|
view_probe_summary=probe_summary,
|
|
1024
1108
|
ambiguous_fields=ambiguous_fields,
|
|
1109
|
+
preferred_view_id=preferred_view_id,
|
|
1025
1110
|
)
|
|
1026
1111
|
|
|
1027
1112
|
response: JSONObject = {
|
|
@@ -1046,6 +1131,8 @@ class RecordTools(ToolBase):
|
|
|
1046
1131
|
"message": "record_update will try data-manager direct edit first, then a matching custom-view edit route, then a unique current-user todo save-only route when the target fields are editable on that workflow node.",
|
|
1047
1132
|
},
|
|
1048
1133
|
}
|
|
1134
|
+
if preferred_view_id:
|
|
1135
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1049
1136
|
if normalized_output_profile == "verbose":
|
|
1050
1137
|
response["view_probe_summary"] = probe_summary
|
|
1051
1138
|
response["record_context_probe"] = probe_summary
|
|
@@ -1106,6 +1193,7 @@ class RecordTools(ToolBase):
|
|
|
1106
1193
|
output_profile: str,
|
|
1107
1194
|
view_probe_summary: list[JSONObject],
|
|
1108
1195
|
ambiguous_fields: list[JSONObject],
|
|
1196
|
+
preferred_view_id: str | None = None,
|
|
1109
1197
|
) -> JSONObject:
|
|
1110
1198
|
"""执行内部辅助逻辑。"""
|
|
1111
1199
|
response: JSONObject = {
|
|
@@ -1123,6 +1211,8 @@ class RecordTools(ToolBase):
|
|
|
1123
1211
|
"payload_template": {},
|
|
1124
1212
|
"recommended_next_actions": recommended_next_actions,
|
|
1125
1213
|
}
|
|
1214
|
+
if preferred_view_id:
|
|
1215
|
+
response["preferred_view_id"] = preferred_view_id
|
|
1126
1216
|
if output_profile == "verbose":
|
|
1127
1217
|
response["view_probe_summary"] = view_probe_summary
|
|
1128
1218
|
response["ambiguous_fields"] = ambiguous_fields
|
|
@@ -1440,24 +1530,58 @@ class RecordTools(ToolBase):
|
|
|
1440
1530
|
)
|
|
1441
1531
|
warnings: list[JSONObject] = []
|
|
1442
1532
|
scope_source = "static_applicant_scope"
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1533
|
+
try:
|
|
1534
|
+
if runtime_lookup:
|
|
1535
|
+
state = self._build_candidate_lookup_state(
|
|
1536
|
+
profile,
|
|
1537
|
+
context,
|
|
1538
|
+
app_key=app_key,
|
|
1539
|
+
record_id=record_id_int,
|
|
1540
|
+
workflow_node_id=workflow_node_id,
|
|
1541
|
+
fields=normalized_fields,
|
|
1542
|
+
)
|
|
1543
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1544
|
+
scope_source = "backend_runtime_scope"
|
|
1545
|
+
else:
|
|
1546
|
+
items: list[JSONObject] | None = None
|
|
1547
|
+
if self._member_candidate_static_preview_should_use_backend(field):
|
|
1548
|
+
state = self._build_candidate_lookup_state(
|
|
1549
|
+
profile,
|
|
1550
|
+
context,
|
|
1551
|
+
app_key=app_key,
|
|
1552
|
+
record_id=None,
|
|
1553
|
+
workflow_node_id=None,
|
|
1554
|
+
fields={},
|
|
1555
|
+
)
|
|
1556
|
+
items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1557
|
+
scope_source = "backend_applicant_scope"
|
|
1558
|
+
if items is None:
|
|
1559
|
+
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1560
|
+
warnings.append(
|
|
1561
|
+
{
|
|
1562
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1563
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1564
|
+
}
|
|
1565
|
+
)
|
|
1566
|
+
except (RecordInputError, QingflowApiError) as error:
|
|
1567
|
+
record_error = (
|
|
1568
|
+
error
|
|
1569
|
+
if isinstance(error, RecordInputError)
|
|
1570
|
+
else self._candidate_lookup_error(kind="member", field=field, value=keyword, error=error)
|
|
1571
|
+
)
|
|
1572
|
+
return self._candidate_lookup_failed_response(
|
|
1573
|
+
profile=profile,
|
|
1574
|
+
session_profile=session_profile,
|
|
1575
|
+
context=context,
|
|
1576
|
+
kind="member",
|
|
1577
|
+
error=record_error,
|
|
1578
|
+
field=field,
|
|
1447
1579
|
app_key=app_key,
|
|
1448
|
-
|
|
1580
|
+
record_id_text=record_id_text,
|
|
1449
1581
|
workflow_node_id=workflow_node_id,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
scope_source = "backend_runtime_scope"
|
|
1454
|
-
else:
|
|
1455
|
-
items = self._resolve_member_candidates(context, field, keyword=keyword)
|
|
1456
|
-
warnings.append(
|
|
1457
|
-
{
|
|
1458
|
-
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1459
|
-
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1460
|
-
}
|
|
1582
|
+
fields_present=bool(normalized_fields),
|
|
1583
|
+
keyword=keyword,
|
|
1584
|
+
scope_source=scope_source,
|
|
1461
1585
|
)
|
|
1462
1586
|
total = len(items)
|
|
1463
1587
|
start = (page_num - 1) * page_size
|
|
@@ -1550,41 +1674,75 @@ class RecordTools(ToolBase):
|
|
|
1550
1674
|
)
|
|
1551
1675
|
warnings: list[JSONObject] = []
|
|
1552
1676
|
scope_source = "static_applicant_scope"
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
profile,
|
|
1556
|
-
context,
|
|
1557
|
-
app_key=app_key,
|
|
1558
|
-
record_id=record_id_int,
|
|
1559
|
-
workflow_node_id=workflow_node_id,
|
|
1560
|
-
fields=normalized_fields,
|
|
1561
|
-
)
|
|
1562
|
-
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1563
|
-
scope_source = "backend_runtime_scope"
|
|
1564
|
-
else:
|
|
1565
|
-
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1566
|
-
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1567
|
-
if (
|
|
1568
|
-
not items
|
|
1569
|
-
and field.dept_select_scope_type == 2
|
|
1570
|
-
and not _scope_has_dynamic_or_external(scope)
|
|
1571
|
-
and not list(scope.get("depart") or [])
|
|
1572
|
-
):
|
|
1677
|
+
try:
|
|
1678
|
+
if runtime_lookup:
|
|
1573
1679
|
state = self._build_candidate_lookup_state(
|
|
1574
1680
|
profile,
|
|
1575
1681
|
context,
|
|
1576
1682
|
app_key=app_key,
|
|
1577
|
-
record_id=
|
|
1578
|
-
workflow_node_id=
|
|
1579
|
-
fields=
|
|
1683
|
+
record_id=record_id_int,
|
|
1684
|
+
workflow_node_id=workflow_node_id,
|
|
1685
|
+
fields=normalized_fields,
|
|
1580
1686
|
)
|
|
1581
1687
|
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1582
1688
|
scope_source = "backend_runtime_scope"
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1689
|
+
else:
|
|
1690
|
+
items: list[JSONObject] | None = None
|
|
1691
|
+
if self._department_candidate_static_preview_should_use_backend(field):
|
|
1692
|
+
state = self._build_candidate_lookup_state(
|
|
1693
|
+
profile,
|
|
1694
|
+
context,
|
|
1695
|
+
app_key=app_key,
|
|
1696
|
+
record_id=None,
|
|
1697
|
+
workflow_node_id=None,
|
|
1698
|
+
fields={},
|
|
1699
|
+
)
|
|
1700
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1701
|
+
scope_source = "backend_applicant_scope"
|
|
1702
|
+
if items is None:
|
|
1703
|
+
items = self._resolve_department_candidates(context, field, keyword=keyword)
|
|
1704
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
1705
|
+
if (
|
|
1706
|
+
not items
|
|
1707
|
+
and field.dept_select_scope_type == 2
|
|
1708
|
+
and not _scope_has_dynamic_or_external(scope)
|
|
1709
|
+
and not list(scope.get("depart") or [])
|
|
1710
|
+
):
|
|
1711
|
+
state = self._build_candidate_lookup_state(
|
|
1712
|
+
profile,
|
|
1713
|
+
context,
|
|
1714
|
+
app_key=app_key,
|
|
1715
|
+
record_id=None,
|
|
1716
|
+
workflow_node_id=None,
|
|
1717
|
+
fields={},
|
|
1718
|
+
)
|
|
1719
|
+
items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
|
|
1720
|
+
scope_source = "backend_applicant_scope"
|
|
1721
|
+
warnings.append(
|
|
1722
|
+
{
|
|
1723
|
+
"code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
|
|
1724
|
+
"message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
|
|
1725
|
+
}
|
|
1726
|
+
)
|
|
1727
|
+
except (RecordInputError, QingflowApiError) as error:
|
|
1728
|
+
record_error = (
|
|
1729
|
+
error
|
|
1730
|
+
if isinstance(error, RecordInputError)
|
|
1731
|
+
else self._candidate_lookup_error(kind="department", field=field, value=keyword, error=error)
|
|
1732
|
+
)
|
|
1733
|
+
return self._candidate_lookup_failed_response(
|
|
1734
|
+
profile=profile,
|
|
1735
|
+
session_profile=session_profile,
|
|
1736
|
+
context=context,
|
|
1737
|
+
kind="department",
|
|
1738
|
+
error=record_error,
|
|
1739
|
+
field=field,
|
|
1740
|
+
app_key=app_key,
|
|
1741
|
+
record_id_text=record_id_text,
|
|
1742
|
+
workflow_node_id=workflow_node_id,
|
|
1743
|
+
fields_present=bool(normalized_fields),
|
|
1744
|
+
keyword=keyword,
|
|
1745
|
+
scope_source=scope_source,
|
|
1588
1746
|
)
|
|
1589
1747
|
total = len(items)
|
|
1590
1748
|
start = (page_num - 1) * page_size
|
|
@@ -1646,6 +1804,21 @@ class RecordTools(ToolBase):
|
|
|
1646
1804
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
1647
1805
|
if limit <= 0:
|
|
1648
1806
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1807
|
+
if not (
|
|
1808
|
+
_normalize_optional_text(view_id)
|
|
1809
|
+
or list_type is not None
|
|
1810
|
+
or _normalize_optional_text(view_key)
|
|
1811
|
+
or _normalize_optional_text(view_name)
|
|
1812
|
+
):
|
|
1813
|
+
raise_tool_error(
|
|
1814
|
+
QingflowApiError.config_error(
|
|
1815
|
+
"record_analyze requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
1816
|
+
details={
|
|
1817
|
+
"error_code": "RECORD_ANALYZE_VIEW_REQUIRED",
|
|
1818
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_analyze with view_id.",
|
|
1819
|
+
},
|
|
1820
|
+
)
|
|
1821
|
+
)
|
|
1649
1822
|
legacy_warnings = _detect_analyze_legacy_warnings(
|
|
1650
1823
|
dimensions=dimensions,
|
|
1651
1824
|
metrics=metrics,
|
|
@@ -1662,7 +1835,7 @@ class RecordTools(ToolBase):
|
|
|
1662
1835
|
list_type=list_type,
|
|
1663
1836
|
view_key=view_key,
|
|
1664
1837
|
view_name=view_name,
|
|
1665
|
-
allow_default=
|
|
1838
|
+
allow_default=False,
|
|
1666
1839
|
)
|
|
1667
1840
|
if not _view_type_supports_analysis(resolved_view.view_type):
|
|
1668
1841
|
raise_tool_error(
|
|
@@ -1725,6 +1898,7 @@ class RecordTools(ToolBase):
|
|
|
1725
1898
|
order_by: list[JSONObject],
|
|
1726
1899
|
limit: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
|
|
1727
1900
|
page: int = 1,
|
|
1901
|
+
page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
|
|
1728
1902
|
view_id: str | None = None,
|
|
1729
1903
|
list_type: int | None = None,
|
|
1730
1904
|
view_key: str | None = None,
|
|
@@ -1743,6 +1917,23 @@ class RecordTools(ToolBase):
|
|
|
1743
1917
|
raise_tool_error(QingflowApiError.config_error("limit must be positive"))
|
|
1744
1918
|
if page <= 0:
|
|
1745
1919
|
raise_tool_error(QingflowApiError.config_error("page must be positive"))
|
|
1920
|
+
if page_size <= 0:
|
|
1921
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
1922
|
+
if not (
|
|
1923
|
+
_normalize_optional_text(view_id)
|
|
1924
|
+
or list_type is not None
|
|
1925
|
+
or _normalize_optional_text(view_key)
|
|
1926
|
+
or _normalize_optional_text(view_name)
|
|
1927
|
+
):
|
|
1928
|
+
raise_tool_error(
|
|
1929
|
+
QingflowApiError.config_error(
|
|
1930
|
+
"record_list requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
1931
|
+
details={
|
|
1932
|
+
"error_code": "RECORD_LIST_VIEW_REQUIRED",
|
|
1933
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_list with view_id.",
|
|
1934
|
+
},
|
|
1935
|
+
)
|
|
1936
|
+
)
|
|
1746
1937
|
view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
|
|
1747
1938
|
profile=profile,
|
|
1748
1939
|
app_key=app_key,
|
|
@@ -1750,7 +1941,7 @@ class RecordTools(ToolBase):
|
|
|
1750
1941
|
list_type=list_type,
|
|
1751
1942
|
view_key=view_key,
|
|
1752
1943
|
view_name=view_name,
|
|
1753
|
-
allow_default=
|
|
1944
|
+
allow_default=False,
|
|
1754
1945
|
)
|
|
1755
1946
|
if not _view_type_supports_analysis(view_route.view_type):
|
|
1756
1947
|
raise_tool_error(
|
|
@@ -1800,12 +1991,12 @@ class RecordTools(ToolBase):
|
|
|
1800
1991
|
app_key=app_key,
|
|
1801
1992
|
view_route=view_route,
|
|
1802
1993
|
page_num=page,
|
|
1803
|
-
page_size=
|
|
1994
|
+
page_size=page_size,
|
|
1804
1995
|
query_key=normalized_query,
|
|
1805
1996
|
search_que_ids=resolved_query_fields or None,
|
|
1806
1997
|
match_rules=match_rules,
|
|
1807
1998
|
sort_rules=sort_rules,
|
|
1808
|
-
max_rows=limit,
|
|
1999
|
+
max_rows=min(limit, page_size),
|
|
1809
2000
|
selected_fields=selected_fields,
|
|
1810
2001
|
output_profile="verbose" if normalized_output_profile in {"verbose", "normalized"} else DEFAULT_OUTPUT_PROFILE,
|
|
1811
2002
|
)
|
|
@@ -2224,6 +2415,7 @@ class RecordTools(ToolBase):
|
|
|
2224
2415
|
requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
|
|
2225
2416
|
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2226
2417
|
normalized_columns = _normalize_public_column_selectors(columns)
|
|
2418
|
+
explicit_view_id = _normalize_optional_text(view_id)
|
|
2227
2419
|
|
|
2228
2420
|
def runner(session_profile, context):
|
|
2229
2421
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2249,17 +2441,45 @@ class RecordTools(ToolBase):
|
|
|
2249
2441
|
"code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
|
|
2250
2442
|
"message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
|
|
2251
2443
|
})
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2444
|
+
def get_detail_for_route(route: AccessibleViewRoute, route_warnings: list[JSONObject]) -> JSONObject:
|
|
2445
|
+
return self._record_get_detail_context(
|
|
2446
|
+
profile=profile,
|
|
2447
|
+
session_profile=session_profile,
|
|
2448
|
+
context=context,
|
|
2449
|
+
app_key=app_key,
|
|
2450
|
+
record_id_int=record_id_int,
|
|
2451
|
+
resolved_view=route,
|
|
2452
|
+
requested_focus_field_ids=normalized_columns,
|
|
2453
|
+
workflow_node_id=workflow_node_id,
|
|
2454
|
+
warnings=route_warnings,
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
try:
|
|
2458
|
+
return get_detail_for_route(resolved_view, warnings)
|
|
2459
|
+
except QingflowApiError as exc:
|
|
2460
|
+
if explicit_view_id is not None:
|
|
2461
|
+
raise
|
|
2462
|
+
if not self._is_record_context_route_miss(exc):
|
|
2463
|
+
raise
|
|
2464
|
+
fallback_warnings = list(warnings)
|
|
2465
|
+
fallback_warnings.append(
|
|
2466
|
+
{
|
|
2467
|
+
"code": "DEFAULT_DETAIL_ROUTE_DENIED",
|
|
2468
|
+
"message": "record_get default system:all route was not readable; trying accessible views that match the frontend route model.",
|
|
2469
|
+
"backend_code": exc.backend_code,
|
|
2470
|
+
}
|
|
2471
|
+
)
|
|
2472
|
+
last_error = exc
|
|
2473
|
+
for candidate in self._candidate_update_views(profile, context, app_key):
|
|
2474
|
+
if candidate.view_id == resolved_view.view_id:
|
|
2475
|
+
continue
|
|
2476
|
+
try:
|
|
2477
|
+
return get_detail_for_route(candidate, fallback_warnings)
|
|
2478
|
+
except QingflowApiError as candidate_exc:
|
|
2479
|
+
if not self._is_record_context_route_miss(candidate_exc):
|
|
2480
|
+
raise
|
|
2481
|
+
last_error = candidate_exc
|
|
2482
|
+
raise last_error
|
|
2263
2483
|
|
|
2264
2484
|
return self._run_record_tool(profile, runner)
|
|
2265
2485
|
|
|
@@ -2274,6 +2494,16 @@ class RecordTools(ToolBase):
|
|
|
2274
2494
|
) -> JSONObject:
|
|
2275
2495
|
"""读取单条记录可见的全量数据日志和流程日志,写入本地 JSONL。"""
|
|
2276
2496
|
record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
|
|
2497
|
+
if not _normalize_optional_text(view_id):
|
|
2498
|
+
raise_tool_error(
|
|
2499
|
+
QingflowApiError.config_error(
|
|
2500
|
+
"record_logs_get requires view_id. Call app_get first and pass accessible_views[].view_id.",
|
|
2501
|
+
details={
|
|
2502
|
+
"error_code": "RECORD_LOGS_VIEW_REQUIRED",
|
|
2503
|
+
"fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_logs_get with view_id.",
|
|
2504
|
+
},
|
|
2505
|
+
)
|
|
2506
|
+
)
|
|
2277
2507
|
|
|
2278
2508
|
def runner(session_profile, context):
|
|
2279
2509
|
resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
|
|
@@ -2284,21 +2514,45 @@ class RecordTools(ToolBase):
|
|
|
2284
2514
|
list_type=None,
|
|
2285
2515
|
view_key=None,
|
|
2286
2516
|
view_name=None,
|
|
2287
|
-
allow_default=
|
|
2517
|
+
allow_default=False,
|
|
2288
2518
|
)
|
|
2289
2519
|
warnings: list[JSONObject] = []
|
|
2290
2520
|
warnings.extend(compatibility_warnings)
|
|
2291
2521
|
warnings.extend(_view_filter_trust_warnings(resolved_view))
|
|
2292
2522
|
unavailable_context: list[JSONObject] = []
|
|
2293
2523
|
|
|
2294
|
-
schema =
|
|
2524
|
+
schema: JSONObject = {}
|
|
2525
|
+
try:
|
|
2526
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
|
|
2527
|
+
except QingflowApiError as exc:
|
|
2528
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2529
|
+
raise
|
|
2530
|
+
unavailable_context.append(
|
|
2531
|
+
_record_detail_unavailable_context(
|
|
2532
|
+
"detail_schema",
|
|
2533
|
+
"记录日志字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
|
|
2534
|
+
exc,
|
|
2535
|
+
)
|
|
2536
|
+
)
|
|
2295
2537
|
index = _build_top_level_field_index(schema)
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2538
|
+
try:
|
|
2539
|
+
audit_info = self._record_get_audit_info(
|
|
2540
|
+
context,
|
|
2541
|
+
app_key=app_key,
|
|
2542
|
+
record_id=record_id_int,
|
|
2543
|
+
resolved_view=resolved_view,
|
|
2544
|
+
)
|
|
2545
|
+
except QingflowApiError as exc:
|
|
2546
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2547
|
+
raise
|
|
2548
|
+
audit_info = {}
|
|
2549
|
+
unavailable_context.append(
|
|
2550
|
+
_record_detail_unavailable_context(
|
|
2551
|
+
"audit_info",
|
|
2552
|
+
"记录审批节点辅助信息获取失败,已继续读取详情主数据和日志。",
|
|
2553
|
+
exc,
|
|
2554
|
+
)
|
|
2555
|
+
)
|
|
2302
2556
|
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
|
|
2303
2557
|
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2304
2558
|
context,
|
|
@@ -2308,6 +2562,17 @@ class RecordTools(ToolBase):
|
|
|
2308
2562
|
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2309
2563
|
)
|
|
2310
2564
|
answer_list = _record_detail_answers(detail_result)
|
|
2565
|
+
if not index.by_id:
|
|
2566
|
+
answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
|
|
2567
|
+
if answer_index.by_id:
|
|
2568
|
+
index = answer_index
|
|
2569
|
+
unavailable_context.append(
|
|
2570
|
+
{
|
|
2571
|
+
"section": "detail_schema",
|
|
2572
|
+
"message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
|
|
2573
|
+
"category": "partial_context",
|
|
2574
|
+
}
|
|
2575
|
+
)
|
|
2311
2576
|
selected_fields = list(index.by_id.values())
|
|
2312
2577
|
fields = [
|
|
2313
2578
|
_record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
|
|
@@ -2401,14 +2666,41 @@ class RecordTools(ToolBase):
|
|
|
2401
2666
|
warnings: list[JSONObject],
|
|
2402
2667
|
) -> JSONObject:
|
|
2403
2668
|
"""执行内部辅助逻辑。"""
|
|
2404
|
-
|
|
2669
|
+
unavailable_context: list[JSONObject] = []
|
|
2670
|
+
schema: JSONObject = {}
|
|
2671
|
+
schema_available = True
|
|
2672
|
+
try:
|
|
2673
|
+
schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
|
|
2674
|
+
except QingflowApiError as exc:
|
|
2675
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2676
|
+
raise
|
|
2677
|
+
schema_available = False
|
|
2678
|
+
unavailable_context.append(
|
|
2679
|
+
_record_detail_unavailable_context(
|
|
2680
|
+
"detail_schema",
|
|
2681
|
+
"记录详情字段结构辅助信息获取失败,已尝试使用详情主数据中的字段信息继续。",
|
|
2682
|
+
exc,
|
|
2683
|
+
)
|
|
2684
|
+
)
|
|
2405
2685
|
index = _build_top_level_field_index(schema)
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2686
|
+
try:
|
|
2687
|
+
audit_info = self._record_get_audit_info(
|
|
2688
|
+
context,
|
|
2689
|
+
app_key=app_key,
|
|
2690
|
+
record_id=record_id_int,
|
|
2691
|
+
resolved_view=resolved_view,
|
|
2692
|
+
)
|
|
2693
|
+
except QingflowApiError as exc:
|
|
2694
|
+
if not _is_optional_schema_permission_error(exc):
|
|
2695
|
+
raise
|
|
2696
|
+
audit_info = {}
|
|
2697
|
+
unavailable_context.append(
|
|
2698
|
+
_record_detail_unavailable_context(
|
|
2699
|
+
"audit_info",
|
|
2700
|
+
"记录审批节点辅助信息获取失败,已继续读取详情主数据。",
|
|
2701
|
+
exc,
|
|
2702
|
+
)
|
|
2703
|
+
)
|
|
2412
2704
|
audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
|
|
2413
2705
|
detail_result, used_list_type, used_role = self._record_get_apply_detail(
|
|
2414
2706
|
context,
|
|
@@ -2418,13 +2710,24 @@ class RecordTools(ToolBase):
|
|
|
2418
2710
|
audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
|
|
2419
2711
|
)
|
|
2420
2712
|
answer_list = _record_detail_answers(detail_result)
|
|
2713
|
+
if not index.by_id:
|
|
2714
|
+
answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
|
|
2715
|
+
if answer_index.by_id:
|
|
2716
|
+
index = answer_index
|
|
2717
|
+
unavailable_context.append(
|
|
2718
|
+
{
|
|
2719
|
+
"section": "detail_schema",
|
|
2720
|
+
"message": "字段结构由详情 answers 回退构造;候选范围、只读状态、选项和联动信息可能不完整。",
|
|
2721
|
+
"category": "partial_context",
|
|
2722
|
+
}
|
|
2723
|
+
)
|
|
2421
2724
|
selected_fields = list(index.by_id.values())
|
|
2422
2725
|
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
|
|
2423
2726
|
normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
|
|
2424
2727
|
cast(list[JSONValue], answer_list),
|
|
2425
2728
|
selected_fields,
|
|
2426
2729
|
)
|
|
2427
|
-
if self._record_get_needs_schema_refresh(
|
|
2730
|
+
if schema_available and self._record_get_needs_schema_refresh(
|
|
2428
2731
|
answer_list=cast(list[JSONValue], answer_list),
|
|
2429
2732
|
selected_fields=selected_fields,
|
|
2430
2733
|
record=row,
|
|
@@ -2440,7 +2743,6 @@ class RecordTools(ToolBase):
|
|
|
2440
2743
|
index = _build_top_level_field_index(schema)
|
|
2441
2744
|
selected_fields = list(index.by_id.values())
|
|
2442
2745
|
|
|
2443
|
-
unavailable_context: list[JSONObject] = []
|
|
2444
2746
|
dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
|
|
2445
2747
|
context,
|
|
2446
2748
|
app_key=app_key,
|
|
@@ -2599,7 +2901,20 @@ class RecordTools(ToolBase):
|
|
|
2599
2901
|
) -> JSONObject:
|
|
2600
2902
|
"""执行内部辅助逻辑。"""
|
|
2601
2903
|
if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
2602
|
-
return self.
|
|
2904
|
+
return self._get_custom_view_browse_schema(
|
|
2905
|
+
profile,
|
|
2906
|
+
context,
|
|
2907
|
+
resolved_view.view_selection.view_key,
|
|
2908
|
+
force_refresh=force_refresh,
|
|
2909
|
+
)
|
|
2910
|
+
if resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
2911
|
+
return self._get_system_browse_schema(
|
|
2912
|
+
profile,
|
|
2913
|
+
context,
|
|
2914
|
+
app_key,
|
|
2915
|
+
list_type=resolved_view.list_type,
|
|
2916
|
+
force_refresh=force_refresh,
|
|
2917
|
+
)
|
|
2603
2918
|
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
2604
2919
|
|
|
2605
2920
|
def _record_get_audit_info(
|
|
@@ -2660,7 +2975,7 @@ class RecordTools(ToolBase):
|
|
|
2660
2975
|
)
|
|
2661
2976
|
return result if isinstance(result, dict) else {"value": result}, list_type, role
|
|
2662
2977
|
except QingflowApiError as exc:
|
|
2663
|
-
if resolved_view.list_type is not None or exc
|
|
2978
|
+
if resolved_view.list_type is not None or not _is_record_permission_denied_error(exc):
|
|
2664
2979
|
raise
|
|
2665
2980
|
last_error: QingflowApiError = exc
|
|
2666
2981
|
for fallback_list_type in (14, 1, 2, 12):
|
|
@@ -2678,7 +2993,7 @@ class RecordTools(ToolBase):
|
|
|
2678
2993
|
return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
|
|
2679
2994
|
except QingflowApiError as fallback_exc:
|
|
2680
2995
|
last_error = fallback_exc
|
|
2681
|
-
if fallback_exc
|
|
2996
|
+
if _is_record_permission_denied_error(fallback_exc):
|
|
2682
2997
|
continue
|
|
2683
2998
|
raise
|
|
2684
2999
|
raise last_error
|
|
@@ -2776,6 +3091,8 @@ class RecordTools(ToolBase):
|
|
|
2776
3091
|
if target_app_key == app_key and str(target_record_id) == str(source_record_id):
|
|
2777
3092
|
reference_payload["self_reference"] = True
|
|
2778
3093
|
except QingflowApiError as exc:
|
|
3094
|
+
if is_auth_like_error(exc):
|
|
3095
|
+
raise
|
|
2779
3096
|
unavailable = _record_detail_unavailable_context(
|
|
2780
3097
|
"reference_detail",
|
|
2781
3098
|
f"引用字段「{field.que_title}」的目标记录详情获取失败。",
|
|
@@ -2873,6 +3190,8 @@ class RecordTools(ToolBase):
|
|
|
2873
3190
|
json_body=body,
|
|
2874
3191
|
)
|
|
2875
3192
|
except QingflowApiError as exc:
|
|
3193
|
+
if is_auth_like_error(exc):
|
|
3194
|
+
raise
|
|
2876
3195
|
unavailable = _record_detail_unavailable_context(
|
|
2877
3196
|
"reference_runtime_match",
|
|
2878
3197
|
"动态引用字段匹配数据获取失败。",
|
|
@@ -2927,6 +3246,8 @@ class RecordTools(ToolBase):
|
|
|
2927
3246
|
},
|
|
2928
3247
|
)
|
|
2929
3248
|
except QingflowApiError as exc:
|
|
3249
|
+
if is_auth_like_error(exc):
|
|
3250
|
+
raise
|
|
2930
3251
|
unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
|
|
2931
3252
|
return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
|
|
2932
3253
|
payload = visibility if isinstance(visibility, dict) else {}
|
|
@@ -2980,6 +3301,8 @@ class RecordTools(ToolBase):
|
|
|
2980
3301
|
source="data_logs",
|
|
2981
3302
|
)
|
|
2982
3303
|
except QingflowApiError as exc:
|
|
3304
|
+
if is_auth_like_error(exc):
|
|
3305
|
+
raise
|
|
2983
3306
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
|
|
2984
3307
|
return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")
|
|
2985
3308
|
|
|
@@ -3033,6 +3356,8 @@ class RecordTools(ToolBase):
|
|
|
3033
3356
|
source="workflow_logs",
|
|
3034
3357
|
)
|
|
3035
3358
|
except QingflowApiError as exc:
|
|
3359
|
+
if is_auth_like_error(exc):
|
|
3360
|
+
raise
|
|
3036
3361
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
|
|
3037
3362
|
return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3038
3363
|
|
|
@@ -3076,6 +3401,8 @@ class RecordTools(ToolBase):
|
|
|
3076
3401
|
deadline=deadline,
|
|
3077
3402
|
)
|
|
3078
3403
|
except QingflowApiError as exc:
|
|
3404
|
+
if is_auth_like_error(exc):
|
|
3405
|
+
raise
|
|
3079
3406
|
unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
|
|
3080
3407
|
return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")
|
|
3081
3408
|
|
|
@@ -3135,6 +3462,8 @@ class RecordTools(ToolBase):
|
|
|
3135
3462
|
deadline=deadline,
|
|
3136
3463
|
)
|
|
3137
3464
|
except QingflowApiError as exc:
|
|
3465
|
+
if is_auth_like_error(exc):
|
|
3466
|
+
raise
|
|
3138
3467
|
unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
|
|
3139
3468
|
return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")
|
|
3140
3469
|
|
|
@@ -3167,6 +3496,8 @@ class RecordTools(ToolBase):
|
|
|
3167
3496
|
params["auditNodeId"] = audit_node_id
|
|
3168
3497
|
payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
|
|
3169
3498
|
except QingflowApiError as exc:
|
|
3499
|
+
if is_auth_like_error(exc):
|
|
3500
|
+
raise
|
|
3170
3501
|
unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
|
|
3171
3502
|
return []
|
|
3172
3503
|
return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]
|
|
@@ -3204,16 +3535,17 @@ class RecordTools(ToolBase):
|
|
|
3204
3535
|
refresh_source_url=refresh_source_url,
|
|
3205
3536
|
)
|
|
3206
3537
|
except Exception as exc: # defensive: media should never break the core record detail.
|
|
3538
|
+
warning: JSONObject = {
|
|
3539
|
+
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
3540
|
+
"message": f"record_get could not collect media assets: {exc}",
|
|
3541
|
+
}
|
|
3542
|
+
if isinstance(exc, QingflowApiError):
|
|
3543
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
3207
3544
|
return {
|
|
3208
3545
|
"status": "unavailable",
|
|
3209
3546
|
"local_dir": None,
|
|
3210
3547
|
"items": [],
|
|
3211
|
-
"warnings": [
|
|
3212
|
-
{
|
|
3213
|
-
"code": "MEDIA_ASSETS_UNAVAILABLE",
|
|
3214
|
-
"message": f"record_get could not collect media assets: {exc}",
|
|
3215
|
-
}
|
|
3216
|
-
],
|
|
3548
|
+
"warnings": [warning],
|
|
3217
3549
|
}
|
|
3218
3550
|
|
|
3219
3551
|
def _record_get_file_assets(
|
|
@@ -3251,16 +3583,17 @@ class RecordTools(ToolBase):
|
|
|
3251
3583
|
refresh_source_url=refresh_source_url,
|
|
3252
3584
|
)
|
|
3253
3585
|
except Exception as exc: # defensive: file assets should never break the core record detail.
|
|
3586
|
+
warning = {
|
|
3587
|
+
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
3588
|
+
"message": f"record_get could not collect file assets: {exc}",
|
|
3589
|
+
}
|
|
3590
|
+
if isinstance(exc, QingflowApiError):
|
|
3591
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
3254
3592
|
return {
|
|
3255
3593
|
"status": "unavailable",
|
|
3256
3594
|
"local_dir": None,
|
|
3257
3595
|
"items": [],
|
|
3258
|
-
"warnings": [
|
|
3259
|
-
{
|
|
3260
|
-
"code": "FILE_ASSETS_UNAVAILABLE",
|
|
3261
|
-
"message": f"record_get could not collect file assets: {exc}",
|
|
3262
|
-
}
|
|
3263
|
-
],
|
|
3596
|
+
"warnings": [warning],
|
|
3264
3597
|
}
|
|
3265
3598
|
|
|
3266
3599
|
def _record_get_refreshed_media_source_url(
|
|
@@ -3272,7 +3605,7 @@ class RecordTools(ToolBase):
|
|
|
3272
3605
|
resolved_view: AccessibleViewRoute,
|
|
3273
3606
|
audit_node_id: int | None,
|
|
3274
3607
|
candidate: JSONObject,
|
|
3275
|
-
) ->
|
|
3608
|
+
) -> JSONValue | None:
|
|
3276
3609
|
"""Refresh the detail payload once to recover an expired attachment storage signature."""
|
|
3277
3610
|
if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
|
|
3278
3611
|
return None
|
|
@@ -3288,8 +3621,15 @@ class RecordTools(ToolBase):
|
|
|
3288
3621
|
resolved_view=resolved_view,
|
|
3289
3622
|
audit_node_id=audit_node_id,
|
|
3290
3623
|
)
|
|
3291
|
-
except QingflowApiError:
|
|
3292
|
-
return
|
|
3624
|
+
except QingflowApiError as exc:
|
|
3625
|
+
return {
|
|
3626
|
+
"source_url": None,
|
|
3627
|
+
"warning": _record_detail_unavailable_context(
|
|
3628
|
+
"asset_url_refresh",
|
|
3629
|
+
"record_get could not refresh the record detail before downloading a private asset.",
|
|
3630
|
+
exc,
|
|
3631
|
+
),
|
|
3632
|
+
}
|
|
3293
3633
|
for answer in _record_detail_answers(detail_result):
|
|
3294
3634
|
if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
|
|
3295
3635
|
continue
|
|
@@ -3325,6 +3665,7 @@ class RecordTools(ToolBase):
|
|
|
3325
3665
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
3326
3666
|
if items is not None:
|
|
3327
3667
|
normalized_items = self._normalize_public_record_insert_batch_items(fields=fields, items=items)
|
|
3668
|
+
self._reject_record_insert_system_fields([cast(JSONObject, item["fields"]) for item in normalized_items])
|
|
3328
3669
|
return self._record_insert_public_batch(
|
|
3329
3670
|
profile=profile,
|
|
3330
3671
|
app_key=app_key,
|
|
@@ -3334,15 +3675,35 @@ class RecordTools(ToolBase):
|
|
|
3334
3675
|
)
|
|
3335
3676
|
if fields is not None and not isinstance(fields, dict):
|
|
3336
3677
|
raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
|
|
3678
|
+
normalized_fields = cast(JSONObject, fields or {})
|
|
3679
|
+
self._reject_record_insert_system_fields([normalized_fields])
|
|
3337
3680
|
return self._record_insert_public_single(
|
|
3338
3681
|
profile=profile,
|
|
3339
3682
|
app_key=app_key,
|
|
3340
|
-
fields=
|
|
3683
|
+
fields=normalized_fields,
|
|
3341
3684
|
verify_write=verify_write,
|
|
3342
3685
|
output_profile=normalized_output_profile,
|
|
3343
3686
|
capture_exceptions=False,
|
|
3344
3687
|
)
|
|
3345
3688
|
|
|
3689
|
+
def _reject_record_insert_system_fields(self, field_maps: list[JSONObject]) -> None:
|
|
3690
|
+
for row_index, field_map in enumerate(field_maps):
|
|
3691
|
+
for field_name in field_map:
|
|
3692
|
+
normalized_name = str(field_name or "").strip()
|
|
3693
|
+
if normalized_name in RECORD_WRITE_SYSTEM_FIELD_NAMES:
|
|
3694
|
+
raise_tool_error(
|
|
3695
|
+
QingflowApiError.config_error(
|
|
3696
|
+
f"record_insert fields must not include built-in system field '{normalized_name}'",
|
|
3697
|
+
details={
|
|
3698
|
+
"error_code": "RESERVED_SYSTEM_FIELD_NAME",
|
|
3699
|
+
"row_number": row_index + 1,
|
|
3700
|
+
"field_name": normalized_name,
|
|
3701
|
+
"reserved_field_names": sorted(RECORD_WRITE_SYSTEM_FIELD_NAMES),
|
|
3702
|
+
"fix_hint": "Remove Qingflow built-in system fields from record_insert payload. They are generated by the platform and can be read after creation, not manually inserted.",
|
|
3703
|
+
},
|
|
3704
|
+
)
|
|
3705
|
+
)
|
|
3706
|
+
|
|
3346
3707
|
def _record_insert_public_single(
|
|
3347
3708
|
self,
|
|
3348
3709
|
*,
|
|
@@ -3676,7 +4037,11 @@ class RecordTools(ToolBase):
|
|
|
3676
4037
|
"field_id": field_id,
|
|
3677
4038
|
"error_code": error_code,
|
|
3678
4039
|
"message": self._record_write_semantic_error_message(error_code, error.get("message")),
|
|
3679
|
-
"next_action": self._record_write_next_action_for_error(
|
|
4040
|
+
"next_action": self._record_write_next_action_for_error(
|
|
4041
|
+
error_code,
|
|
4042
|
+
expected_format=expected_format,
|
|
4043
|
+
field_payload=field_payload,
|
|
4044
|
+
),
|
|
3680
4045
|
}
|
|
3681
4046
|
if expected_format is not None:
|
|
3682
4047
|
payload["expected_format"] = expected_format
|
|
@@ -3787,8 +4152,30 @@ class RecordTools(ToolBase):
|
|
|
3787
4152
|
return _normalize_optional_text(fallback) or "字段值格式不正确。"
|
|
3788
4153
|
return _normalize_optional_text(fallback) or "字段写入失败。"
|
|
3789
4154
|
|
|
3790
|
-
def _record_write_next_action_for_error(
|
|
4155
|
+
def _record_write_next_action_for_error(
|
|
4156
|
+
self,
|
|
4157
|
+
error_code: str,
|
|
4158
|
+
*,
|
|
4159
|
+
expected_format: JSONObject | None = None,
|
|
4160
|
+
field_payload: JSONObject | None = None,
|
|
4161
|
+
) -> str:
|
|
3791
4162
|
"""执行内部辅助逻辑。"""
|
|
4163
|
+
kind = _normalize_optional_text((expected_format or {}).get("kind"))
|
|
4164
|
+
field_title = (
|
|
4165
|
+
_normalize_optional_text((field_payload or {}).get("que_title"))
|
|
4166
|
+
or _normalize_optional_text((field_payload or {}).get("title"))
|
|
4167
|
+
or "该字段"
|
|
4168
|
+
)
|
|
4169
|
+
if kind == "member_list":
|
|
4170
|
+
return f"{field_title} 需要有效成员;用唯一姓名/邮箱/id,歧义时先调用 record_member_candidates 后只重试本行。"
|
|
4171
|
+
if kind == "department_list":
|
|
4172
|
+
return f"{field_title} 需要有效部门;用候选范围内的部门名/id/object,歧义或不确定时先调用 record_department_candidates 后只重试本行。"
|
|
4173
|
+
if kind == "relation_record":
|
|
4174
|
+
return f"{field_title} 需要关联记录;优先改用目标记录 record_id/apply_id,或使用可唯一命中的显示文本后只重试本行。"
|
|
4175
|
+
if kind == "attachment_list":
|
|
4176
|
+
return f"{field_title} 需要附件值;先用 file_upload_local 上传文件,再写返回的 value/url 后只重试本行。"
|
|
4177
|
+
if kind in {"single_select", "multi_select"}:
|
|
4178
|
+
return f"{field_title} 需要选项值;使用 schema options 中的标签或 option id,修正后只重试本行。"
|
|
3792
4179
|
if error_code == "MISSING_REQUIRED_FIELD":
|
|
3793
4180
|
return "补充该字段后只重试本行。"
|
|
3794
4181
|
if error_code in {"FIELD_NOT_FOUND", "AMBIGUOUS_FIELD"}:
|
|
@@ -3804,6 +4191,7 @@ class RecordTools(ToolBase):
|
|
|
3804
4191
|
record_id: Any | None,
|
|
3805
4192
|
fields: JSONObject | None = None,
|
|
3806
4193
|
items: list[JSONObject] | None = None,
|
|
4194
|
+
view_id: str | None = None,
|
|
3807
4195
|
dry_run: bool = False,
|
|
3808
4196
|
verify_write: bool = True,
|
|
3809
4197
|
output_profile: str = "normal",
|
|
@@ -3824,6 +4212,7 @@ class RecordTools(ToolBase):
|
|
|
3824
4212
|
profile=profile,
|
|
3825
4213
|
app_key=app_key,
|
|
3826
4214
|
items=normalized_items,
|
|
4215
|
+
view_id=view_id,
|
|
3827
4216
|
dry_run=dry_run,
|
|
3828
4217
|
verify_write=verify_write,
|
|
3829
4218
|
output_profile=normalized_output_profile,
|
|
@@ -3840,6 +4229,7 @@ class RecordTools(ToolBase):
|
|
|
3840
4229
|
app_key=app_key,
|
|
3841
4230
|
record_id=record_id_int,
|
|
3842
4231
|
fields=cast(JSONObject, fields or {}),
|
|
4232
|
+
view_id=view_id,
|
|
3843
4233
|
verify_write=verify_write,
|
|
3844
4234
|
output_profile=normalized_output_profile,
|
|
3845
4235
|
)
|
|
@@ -3851,17 +4241,61 @@ class RecordTools(ToolBase):
|
|
|
3851
4241
|
app_key: str,
|
|
3852
4242
|
record_id: int,
|
|
3853
4243
|
fields: JSONObject,
|
|
4244
|
+
view_id: str | None,
|
|
3854
4245
|
verify_write: bool,
|
|
3855
4246
|
output_profile: str,
|
|
4247
|
+
capture_exceptions: bool = False,
|
|
3856
4248
|
) -> JSONObject:
|
|
3857
4249
|
"""执行内部辅助逻辑。"""
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
4250
|
+
write_state = {"attempted": False}
|
|
4251
|
+
try:
|
|
4252
|
+
return self._record_update_public_single_impl(
|
|
4253
|
+
profile=profile,
|
|
4254
|
+
app_key=app_key,
|
|
4255
|
+
record_id=record_id,
|
|
4256
|
+
fields=fields,
|
|
4257
|
+
view_id=view_id,
|
|
4258
|
+
verify_write=verify_write,
|
|
4259
|
+
output_profile=output_profile,
|
|
4260
|
+
write_attempted_ref=lambda value: write_state.__setitem__("attempted", value),
|
|
4261
|
+
)
|
|
4262
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
4263
|
+
if not capture_exceptions:
|
|
4264
|
+
raise
|
|
4265
|
+
return self._record_write_exception_response(
|
|
4266
|
+
exc,
|
|
4267
|
+
operation="update",
|
|
4268
|
+
profile=profile,
|
|
4269
|
+
app_key=app_key,
|
|
4270
|
+
record_id=record_id,
|
|
4271
|
+
output_profile=output_profile,
|
|
4272
|
+
human_review=True,
|
|
4273
|
+
write_executed=write_state["attempted"],
|
|
4274
|
+
)
|
|
4275
|
+
|
|
4276
|
+
def _record_update_public_single_impl(
|
|
4277
|
+
self,
|
|
4278
|
+
*,
|
|
4279
|
+
profile: str,
|
|
4280
|
+
app_key: str,
|
|
4281
|
+
record_id: int,
|
|
4282
|
+
fields: JSONObject,
|
|
4283
|
+
view_id: str | None,
|
|
4284
|
+
verify_write: bool,
|
|
4285
|
+
output_profile: str,
|
|
4286
|
+
write_attempted_ref: Callable[[bool], None],
|
|
4287
|
+
) -> JSONObject:
|
|
4288
|
+
"""执行内部辅助逻辑。"""
|
|
4289
|
+
preflight_kwargs: dict[str, Any] = {
|
|
4290
|
+
"profile": profile,
|
|
4291
|
+
"app_key": app_key,
|
|
4292
|
+
"record_id": record_id,
|
|
4293
|
+
"fields": fields,
|
|
4294
|
+
"force_refresh_form": False,
|
|
4295
|
+
}
|
|
4296
|
+
if view_id is not None:
|
|
4297
|
+
preflight_kwargs["preferred_view_id"] = view_id
|
|
4298
|
+
raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
|
|
3865
4299
|
preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
|
|
3866
4300
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
3867
4301
|
normalized_payload = self._record_write_normalized_payload(
|
|
@@ -3881,6 +4315,7 @@ class RecordTools(ToolBase):
|
|
|
3881
4315
|
human_review=True,
|
|
3882
4316
|
target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
|
|
3883
4317
|
)
|
|
4318
|
+
write_attempted_ref(True)
|
|
3884
4319
|
route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
|
|
3885
4320
|
profile=profile,
|
|
3886
4321
|
app_key=app_key,
|
|
@@ -4161,7 +4596,9 @@ class RecordTools(ToolBase):
|
|
|
4161
4596
|
)
|
|
4162
4597
|
|
|
4163
4598
|
def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
|
|
4164
|
-
if exc
|
|
4599
|
+
if is_auth_like_error(exc):
|
|
4600
|
+
return False
|
|
4601
|
+
if backend_code_int(exc) in {40002, 40027, 40038, 404}:
|
|
4165
4602
|
return True
|
|
4166
4603
|
if exc.http_status == 404:
|
|
4167
4604
|
return True
|
|
@@ -4265,6 +4702,8 @@ class RecordTools(ToolBase):
|
|
|
4265
4702
|
},
|
|
4266
4703
|
)
|
|
4267
4704
|
except QingflowApiError as exc:
|
|
4705
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4706
|
+
raise
|
|
4268
4707
|
return unavailable(
|
|
4269
4708
|
error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
|
|
4270
4709
|
reason="current-user todo task list is unavailable",
|
|
@@ -4312,6 +4751,8 @@ class RecordTools(ToolBase):
|
|
|
4312
4751
|
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
4313
4752
|
)
|
|
4314
4753
|
except QingflowApiError as exc:
|
|
4754
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
4755
|
+
raise
|
|
4315
4756
|
return unavailable(
|
|
4316
4757
|
error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
|
|
4317
4758
|
reason="workflow node editable field list is unavailable; record_update will not guess task editability",
|
|
@@ -4462,7 +4903,7 @@ class RecordTools(ToolBase):
|
|
|
4462
4903
|
raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))
|
|
4463
4904
|
|
|
4464
4905
|
def runner(session_profile, context):
|
|
4465
|
-
index = self.
|
|
4906
|
+
index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
|
|
4466
4907
|
normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
|
|
4467
4908
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
4468
4909
|
result = self.backend.request(
|
|
@@ -4565,6 +5006,7 @@ class RecordTools(ToolBase):
|
|
|
4565
5006
|
profile: str,
|
|
4566
5007
|
app_key: str,
|
|
4567
5008
|
items: list[JSONObject],
|
|
5009
|
+
view_id: str | None,
|
|
4568
5010
|
dry_run: bool,
|
|
4569
5011
|
verify_write: bool,
|
|
4570
5012
|
output_profile: str,
|
|
@@ -4576,6 +5018,7 @@ class RecordTools(ToolBase):
|
|
|
4576
5018
|
app_key=app_key,
|
|
4577
5019
|
record_id=cast(int, item["record_id"]),
|
|
4578
5020
|
fields=cast(JSONObject, item["fields"]),
|
|
5021
|
+
view_id=view_id,
|
|
4579
5022
|
output_profile=output_profile,
|
|
4580
5023
|
)
|
|
4581
5024
|
for item in items
|
|
@@ -4604,8 +5047,10 @@ class RecordTools(ToolBase):
|
|
|
4604
5047
|
app_key=app_key,
|
|
4605
5048
|
record_id=record_id,
|
|
4606
5049
|
fields=fields,
|
|
5050
|
+
view_id=view_id,
|
|
4607
5051
|
verify_write=verify_write,
|
|
4608
5052
|
output_profile=output_profile,
|
|
5053
|
+
capture_exceptions=True,
|
|
4609
5054
|
)
|
|
4610
5055
|
)
|
|
4611
5056
|
except (QingflowApiError, RuntimeError) as exc:
|
|
@@ -4636,16 +5081,20 @@ class RecordTools(ToolBase):
|
|
|
4636
5081
|
app_key: str,
|
|
4637
5082
|
record_id: int,
|
|
4638
5083
|
fields: JSONObject,
|
|
5084
|
+
view_id: str | None,
|
|
4639
5085
|
output_profile: str,
|
|
4640
5086
|
) -> JSONObject:
|
|
4641
5087
|
"""执行内部辅助逻辑。"""
|
|
4642
|
-
|
|
4643
|
-
profile
|
|
4644
|
-
app_key
|
|
4645
|
-
record_id
|
|
4646
|
-
fields
|
|
4647
|
-
force_refresh_form
|
|
4648
|
-
|
|
5088
|
+
preflight_kwargs: dict[str, Any] = {
|
|
5089
|
+
"profile": profile,
|
|
5090
|
+
"app_key": app_key,
|
|
5091
|
+
"record_id": record_id,
|
|
5092
|
+
"fields": fields,
|
|
5093
|
+
"force_refresh_form": False,
|
|
5094
|
+
}
|
|
5095
|
+
if view_id is not None:
|
|
5096
|
+
preflight_kwargs["preferred_view_id"] = view_id
|
|
5097
|
+
raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
|
|
4649
5098
|
preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
|
|
4650
5099
|
normalized_payload = self._record_write_normalized_payload(
|
|
4651
5100
|
operation="update",
|
|
@@ -4851,6 +5300,9 @@ class RecordTools(ToolBase):
|
|
|
4851
5300
|
item: JSONObject = {
|
|
4852
5301
|
"resource": data.get("resource"),
|
|
4853
5302
|
"status": response.get("status"),
|
|
5303
|
+
"write_executed": bool(response.get("write_executed")),
|
|
5304
|
+
"safe_to_retry": bool(response.get("safe_to_retry", True)),
|
|
5305
|
+
"verification_status": response.get("verification_status", "not_requested"),
|
|
4854
5306
|
"verification": data.get("verification"),
|
|
4855
5307
|
"field_errors": cast(list[JSONObject], data.get("field_errors", [])),
|
|
4856
5308
|
"confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
|
|
@@ -4860,7 +5312,7 @@ class RecordTools(ToolBase):
|
|
|
4860
5312
|
if isinstance(update_route, dict):
|
|
4861
5313
|
item["update_route"] = update_route
|
|
4862
5314
|
tried_routes = response.get("tried_routes")
|
|
4863
|
-
if isinstance(tried_routes, list):
|
|
5315
|
+
if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
|
|
4864
5316
|
item["tried_routes"] = tried_routes
|
|
4865
5317
|
blockers = data.get("blockers")
|
|
4866
5318
|
if isinstance(blockers, list) and blockers:
|
|
@@ -4882,6 +5334,7 @@ class RecordTools(ToolBase):
|
|
|
4882
5334
|
app_key: str,
|
|
4883
5335
|
record_id: int,
|
|
4884
5336
|
fields: JSONObject,
|
|
5337
|
+
preferred_view_id: str | None = None,
|
|
4885
5338
|
force_refresh_form: bool,
|
|
4886
5339
|
) -> JSONObject:
|
|
4887
5340
|
"""执行内部辅助逻辑。"""
|
|
@@ -4889,6 +5342,25 @@ class RecordTools(ToolBase):
|
|
|
4889
5342
|
request_route = self._request_route_payload(context)
|
|
4890
5343
|
def build_once(*, effective_force_refresh: bool) -> JSONObject:
|
|
4891
5344
|
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
5345
|
+
normalized_preferred_view_id = _normalize_optional_text(preferred_view_id)
|
|
5346
|
+
if normalized_preferred_view_id:
|
|
5347
|
+
preferred_route = next(
|
|
5348
|
+
(
|
|
5349
|
+
route
|
|
5350
|
+
for route in candidate_routes
|
|
5351
|
+
if route.view_id == normalized_preferred_view_id
|
|
5352
|
+
),
|
|
5353
|
+
None,
|
|
5354
|
+
)
|
|
5355
|
+
if preferred_route is None:
|
|
5356
|
+
raise_tool_error(
|
|
5357
|
+
QingflowApiError.config_error(
|
|
5358
|
+
f"view_id '{normalized_preferred_view_id}' is not an accessible update candidate"
|
|
5359
|
+
)
|
|
5360
|
+
)
|
|
5361
|
+
candidate_routes = [preferred_route]
|
|
5362
|
+
else:
|
|
5363
|
+
candidate_routes = _prefer_custom_update_routes(candidate_routes)
|
|
4892
5364
|
probes = self._probe_candidate_record_contexts(
|
|
4893
5365
|
context,
|
|
4894
5366
|
app_key=app_key,
|
|
@@ -5102,41 +5574,24 @@ class RecordTools(ToolBase):
|
|
|
5102
5574
|
"data": first_confirmation_plan,
|
|
5103
5575
|
}
|
|
5104
5576
|
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
app_key=app_key,
|
|
5109
|
-
record_id=record_id,
|
|
5110
|
-
fields=fields,
|
|
5111
|
-
current_answers=matched_answers_for_union or [],
|
|
5112
|
-
matched_routes=matched_routes,
|
|
5113
|
-
force_refresh_form=effective_force_refresh,
|
|
5114
|
-
)
|
|
5115
|
-
if union_plan is not None:
|
|
5116
|
-
validation = union_plan.get("validation")
|
|
5117
|
-
if isinstance(validation, dict):
|
|
5118
|
-
warnings = validation.get("warnings")
|
|
5119
|
-
if not isinstance(warnings, list):
|
|
5120
|
-
warnings = []
|
|
5121
|
-
validation["warnings"] = warnings
|
|
5122
|
-
for message in fallback_warning_messages:
|
|
5123
|
-
if message not in warnings:
|
|
5124
|
-
warnings.append(message)
|
|
5125
|
-
union_plan["view_probe_summary"] = probe_summary
|
|
5126
|
-
union_plan["record_context_probe"] = probe_summary
|
|
5577
|
+
if normalized_preferred_view_id and first_blocked_plan is not None:
|
|
5578
|
+
first_blocked_plan["view_probe_summary"] = probe_summary
|
|
5579
|
+
first_blocked_plan["record_context_probe"] = probe_summary
|
|
5127
5580
|
return {
|
|
5128
5581
|
"profile": profile,
|
|
5129
5582
|
"ws_id": session_profile.selected_ws_id,
|
|
5130
5583
|
"ok": True,
|
|
5131
5584
|
"request_route": request_route,
|
|
5132
|
-
"data":
|
|
5585
|
+
"data": first_blocked_plan,
|
|
5133
5586
|
}
|
|
5134
5587
|
|
|
5135
5588
|
blocked_data = self._build_auto_view_blocked_preflight_data(
|
|
5136
5589
|
app_key=app_key,
|
|
5137
5590
|
record_id=record_id,
|
|
5138
5591
|
blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
|
|
5139
|
-
warnings=[
|
|
5592
|
+
warnings=[
|
|
5593
|
+
"record_update requires one executable frontend route for the full payload; it does not merge writable fields across multiple views."
|
|
5594
|
+
],
|
|
5140
5595
|
recommended_next_actions=[
|
|
5141
5596
|
"Call record_update_schema_get first to inspect the overall writable field set for this record.",
|
|
5142
5597
|
"Reduce the update payload until all requested fields fit inside one matched accessible view.",
|
|
@@ -5183,6 +5638,7 @@ class RecordTools(ToolBase):
|
|
|
5183
5638
|
union_writable_field_ids: set[int] = set()
|
|
5184
5639
|
union_visible_question_ids: set[int] = set()
|
|
5185
5640
|
matched_view_payloads: list[JSONObject] = []
|
|
5641
|
+
union_index: FieldIndex | None = None
|
|
5186
5642
|
|
|
5187
5643
|
for candidate in matched_routes:
|
|
5188
5644
|
browse_scope = self._build_browse_write_scope(
|
|
@@ -5192,11 +5648,13 @@ class RecordTools(ToolBase):
|
|
|
5192
5648
|
candidate,
|
|
5193
5649
|
force_refresh=force_refresh_form,
|
|
5194
5650
|
)
|
|
5651
|
+
browse_index = cast(FieldIndex, browse_scope["index"])
|
|
5652
|
+
union_index = browse_index if union_index is None else _merge_field_indexes(union_index, browse_index)
|
|
5195
5653
|
union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
|
|
5196
5654
|
union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
|
|
5197
5655
|
matched_view_payloads.append(_accessible_view_payload(candidate))
|
|
5198
5656
|
|
|
5199
|
-
if not union_writable_field_ids and not union_visible_question_ids:
|
|
5657
|
+
if union_index is None or (not union_writable_field_ids and not union_visible_question_ids):
|
|
5200
5658
|
return None
|
|
5201
5659
|
|
|
5202
5660
|
plan_data = self._build_record_write_preflight(
|
|
@@ -5213,10 +5671,9 @@ class RecordTools(ToolBase):
|
|
|
5213
5671
|
view_key=None,
|
|
5214
5672
|
view_name=None,
|
|
5215
5673
|
existing_answers_override=current_answers,
|
|
5674
|
+
field_index_override=union_index,
|
|
5216
5675
|
)
|
|
5217
5676
|
|
|
5218
|
-
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
5219
|
-
app_index = _build_applicant_top_level_field_index(schema)
|
|
5220
5677
|
validation = cast(JSONObject, plan_data.get("validation", {}))
|
|
5221
5678
|
invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
|
|
5222
5679
|
missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
|
|
@@ -5227,12 +5684,21 @@ class RecordTools(ToolBase):
|
|
|
5227
5684
|
invalid_fields.extend(
|
|
5228
5685
|
self._validate_view_scoped_subtable_answers(
|
|
5229
5686
|
normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
|
|
5230
|
-
full_index=
|
|
5231
|
-
selector_index=
|
|
5687
|
+
full_index=union_index,
|
|
5688
|
+
selector_index=union_index,
|
|
5232
5689
|
visible_question_ids=union_visible_question_ids,
|
|
5233
5690
|
)
|
|
5234
5691
|
)
|
|
5235
5692
|
|
|
5693
|
+
readonly_or_system_fields = [
|
|
5694
|
+
item
|
|
5695
|
+
for item in readonly_or_system_fields
|
|
5696
|
+
if not (
|
|
5697
|
+
isinstance(item, dict)
|
|
5698
|
+
and (que_id := _coerce_count(item.get("que_id"))) is not None
|
|
5699
|
+
and que_id in union_writable_field_ids
|
|
5700
|
+
)
|
|
5701
|
+
]
|
|
5236
5702
|
existing_readonly_ids = {
|
|
5237
5703
|
str(_coerce_count(item.get("que_id")))
|
|
5238
5704
|
for item in readonly_or_system_fields
|
|
@@ -5396,7 +5862,13 @@ class RecordTools(ToolBase):
|
|
|
5396
5862
|
view_type=None,
|
|
5397
5863
|
)
|
|
5398
5864
|
)
|
|
5399
|
-
|
|
5865
|
+
try:
|
|
5866
|
+
view_items = self._get_view_list(profile, context, app_key)
|
|
5867
|
+
except QingflowApiError as exc:
|
|
5868
|
+
if not _is_record_permission_denied_error(exc):
|
|
5869
|
+
raise
|
|
5870
|
+
view_items = []
|
|
5871
|
+
for item in view_items:
|
|
5400
5872
|
if not isinstance(item, dict):
|
|
5401
5873
|
continue
|
|
5402
5874
|
view_key = _normalize_optional_text(item.get("viewKey"))
|
|
@@ -5453,7 +5925,9 @@ class RecordTools(ToolBase):
|
|
|
5453
5925
|
return payload
|
|
5454
5926
|
|
|
5455
5927
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
5456
|
-
if error
|
|
5928
|
+
if is_auth_like_error(error):
|
|
5929
|
+
return False
|
|
5930
|
+
if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
|
|
5457
5931
|
return True
|
|
5458
5932
|
if error.http_status == 404:
|
|
5459
5933
|
return True
|
|
@@ -5483,11 +5957,12 @@ class RecordTools(ToolBase):
|
|
|
5483
5957
|
used_list_type = None
|
|
5484
5958
|
else:
|
|
5485
5959
|
used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
|
|
5960
|
+
role = _record_detail_role_for_list_type(used_list_type)
|
|
5486
5961
|
record = self.backend.request(
|
|
5487
5962
|
"GET",
|
|
5488
5963
|
context,
|
|
5489
5964
|
f"/app/{app_key}/apply/{apply_id}",
|
|
5490
|
-
params={"role":
|
|
5965
|
+
params={"role": role, "listType": used_list_type},
|
|
5491
5966
|
)
|
|
5492
5967
|
answers = record.get("answers") if isinstance(record, dict) else None
|
|
5493
5968
|
normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
@@ -5517,6 +5992,8 @@ class RecordTools(ToolBase):
|
|
|
5517
5992
|
error_payload=None,
|
|
5518
5993
|
)
|
|
5519
5994
|
except QingflowApiError as exc:
|
|
5995
|
+
if not self._is_record_context_route_miss(exc):
|
|
5996
|
+
raise
|
|
5520
5997
|
return RecordContextRouteProbe(
|
|
5521
5998
|
route=resolved_view,
|
|
5522
5999
|
answer_list=None,
|
|
@@ -5588,7 +6065,7 @@ class RecordTools(ToolBase):
|
|
|
5588
6065
|
]
|
|
5589
6066
|
|
|
5590
6067
|
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
5591
|
-
if exc
|
|
6068
|
+
if backend_code_int(exc) == 500:
|
|
5592
6069
|
return True
|
|
5593
6070
|
if exc.http_status is not None and exc.http_status >= 500:
|
|
5594
6071
|
return True
|
|
@@ -5713,12 +6190,15 @@ class RecordTools(ToolBase):
|
|
|
5713
6190
|
app_key: str,
|
|
5714
6191
|
record_id: Any | None = None,
|
|
5715
6192
|
record_ids: list[Any] | None = None,
|
|
6193
|
+
view_id: str | None = None,
|
|
6194
|
+
list_type: int | None = None,
|
|
5716
6195
|
output_profile: str = "normal",
|
|
5717
6196
|
) -> JSONObject:
|
|
5718
6197
|
"""执行记录相关逻辑。"""
|
|
5719
6198
|
normalized_output_profile = self._normalize_public_output_profile(output_profile)
|
|
5720
6199
|
if not app_key:
|
|
5721
6200
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
6201
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
5722
6202
|
normalized_record_ids: list[int] = []
|
|
5723
6203
|
for index, item in enumerate(record_ids or []):
|
|
5724
6204
|
normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
|
|
@@ -5738,21 +6218,72 @@ class RecordTools(ToolBase):
|
|
|
5738
6218
|
"record_ids": [stringify_backend_id(item) for item in delete_ids],
|
|
5739
6219
|
"answers": [],
|
|
5740
6220
|
"submit_type": 1,
|
|
6221
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
5741
6222
|
}
|
|
5742
6223
|
return self._record_delete_public_batch(
|
|
5743
6224
|
profile=profile,
|
|
5744
6225
|
app_key=app_key,
|
|
5745
6226
|
delete_ids=delete_ids,
|
|
6227
|
+
list_type=delete_list_type,
|
|
5746
6228
|
normalized_payload=normalized_payload,
|
|
5747
6229
|
output_profile=normalized_output_profile,
|
|
5748
6230
|
)
|
|
5749
6231
|
|
|
6232
|
+
def _resolve_record_delete_list_type(self, *, view_id: str | None, list_type: int | None) -> int:
|
|
6233
|
+
normalized_view_id = _normalize_optional_text(view_id)
|
|
6234
|
+
if normalized_view_id:
|
|
6235
|
+
if normalized_view_id.startswith("custom:"):
|
|
6236
|
+
raise_tool_error(
|
|
6237
|
+
QingflowApiError.config_error(
|
|
6238
|
+
"record_delete does not support custom view deletion; the backend delete route accepts system listType only",
|
|
6239
|
+
details={
|
|
6240
|
+
"error_code": "RECORD_DELETE_CUSTOM_VIEW_UNSUPPORTED",
|
|
6241
|
+
"view_id": normalized_view_id,
|
|
6242
|
+
"fix_hint": (
|
|
6243
|
+
"Use a system view_id from app_get.accessible_views, or resolve target record_ids with "
|
|
6244
|
+
"record list/get first and retry delete without a custom view selector."
|
|
6245
|
+
),
|
|
6246
|
+
},
|
|
6247
|
+
)
|
|
6248
|
+
)
|
|
6249
|
+
if not normalized_view_id.startswith("system:"):
|
|
6250
|
+
raise_tool_error(QingflowApiError.config_error("view_id must start with system: or custom:"))
|
|
6251
|
+
mapped_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE.get(normalized_view_id)
|
|
6252
|
+
if mapped_list_type is None:
|
|
6253
|
+
raise_tool_error(QingflowApiError.config_error(f"unsupported view_id '{normalized_view_id}'"))
|
|
6254
|
+
return mapped_list_type
|
|
6255
|
+
if list_type is not None:
|
|
6256
|
+
normalized_list_type = int(list_type)
|
|
6257
|
+
if normalized_list_type not in SYSTEM_VIEW_LIST_TYPES:
|
|
6258
|
+
raise_tool_error(
|
|
6259
|
+
QingflowApiError.config_error(
|
|
6260
|
+
"record_delete list_type must map to a supported system view",
|
|
6261
|
+
details={
|
|
6262
|
+
"error_code": "RECORD_DELETE_SYSTEM_VIEW_REQUIRED",
|
|
6263
|
+
"list_type": normalized_list_type,
|
|
6264
|
+
"supported_list_types": sorted(SYSTEM_VIEW_LIST_TYPES),
|
|
6265
|
+
"fix_hint": "Pass a system view_id from app_get.accessible_views instead of an arbitrary list_type.",
|
|
6266
|
+
},
|
|
6267
|
+
)
|
|
6268
|
+
)
|
|
6269
|
+
return normalized_list_type
|
|
6270
|
+
raise_tool_error(
|
|
6271
|
+
QingflowApiError.config_error(
|
|
6272
|
+
"record_delete requires a system view_id or list_type; deleting without frontend list context is ambiguous",
|
|
6273
|
+
details={
|
|
6274
|
+
"error_code": "RECORD_DELETE_VIEW_REQUIRED",
|
|
6275
|
+
"fix_hint": "Pass a system view_id from app_get.accessible_views, for example --view-id system:all. If the target came from a custom view, first confirm the record_id, then choose an accessible system view for deletion.",
|
|
6276
|
+
},
|
|
6277
|
+
)
|
|
6278
|
+
)
|
|
6279
|
+
|
|
5750
6280
|
def _record_delete_public_batch(
|
|
5751
6281
|
self,
|
|
5752
6282
|
*,
|
|
5753
6283
|
profile: str,
|
|
5754
6284
|
app_key: str,
|
|
5755
6285
|
delete_ids: list[int],
|
|
6286
|
+
list_type: int,
|
|
5756
6287
|
normalized_payload: JSONObject,
|
|
5757
6288
|
output_profile: str,
|
|
5758
6289
|
) -> JSONObject:
|
|
@@ -5762,7 +6293,7 @@ class RecordTools(ToolBase):
|
|
|
5762
6293
|
for index, delete_id in enumerate(delete_ids):
|
|
5763
6294
|
record_id_text = stringify_backend_id(delete_id)
|
|
5764
6295
|
try:
|
|
5765
|
-
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id])
|
|
6296
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=[delete_id], list_type=list_type)
|
|
5766
6297
|
request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
|
|
5767
6298
|
ws_id = raw_apply.get("ws_id", ws_id)
|
|
5768
6299
|
single_payload = {
|
|
@@ -5771,6 +6302,7 @@ class RecordTools(ToolBase):
|
|
|
5771
6302
|
"record_ids": [record_id_text],
|
|
5772
6303
|
"answers": [],
|
|
5773
6304
|
"submit_type": 1,
|
|
6305
|
+
"selection": normalized_payload.get("selection"),
|
|
5774
6306
|
}
|
|
5775
6307
|
single_response = self._record_write_apply_response(
|
|
5776
6308
|
raw_apply,
|
|
@@ -6053,12 +6585,13 @@ class RecordTools(ToolBase):
|
|
|
6053
6585
|
preflight=raw_preflight,
|
|
6054
6586
|
)
|
|
6055
6587
|
|
|
6056
|
-
if
|
|
6588
|
+
if view_key is not None or view_name is not None:
|
|
6057
6589
|
raise_tool_error(
|
|
6058
6590
|
QingflowApiError.config_error(
|
|
6059
|
-
"delete does not
|
|
6591
|
+
"delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
|
|
6060
6592
|
)
|
|
6061
6593
|
)
|
|
6594
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
|
|
6062
6595
|
if normalized_values or normalized_set:
|
|
6063
6596
|
raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
|
|
6064
6597
|
delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
|
|
@@ -6070,8 +6603,9 @@ class RecordTools(ToolBase):
|
|
|
6070
6603
|
"record_ids": delete_ids,
|
|
6071
6604
|
"answers": [],
|
|
6072
6605
|
"submit_type": submit_type_value,
|
|
6606
|
+
"selection": {"view_id": view_id, "list_type": delete_list_type},
|
|
6073
6607
|
}
|
|
6074
|
-
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids)
|
|
6608
|
+
raw_apply = self._record_delete_many(profile=profile, app_key=app_key, record_ids=delete_ids, list_type=delete_list_type)
|
|
6075
6609
|
return self._record_write_apply_response(
|
|
6076
6610
|
raw_apply,
|
|
6077
6611
|
operation="delete",
|
|
@@ -6221,7 +6755,9 @@ class RecordTools(ToolBase):
|
|
|
6221
6755
|
or _normalize_optional_text(payload.get("appName"))
|
|
6222
6756
|
or _normalize_optional_text(payload.get("appTitle"))
|
|
6223
6757
|
)
|
|
6224
|
-
except QingflowApiError:
|
|
6758
|
+
except QingflowApiError as exc:
|
|
6759
|
+
if is_auth_like_error(exc):
|
|
6760
|
+
raise
|
|
6225
6761
|
name = None
|
|
6226
6762
|
self._app_name_cache[cache_key] = name
|
|
6227
6763
|
return name
|
|
@@ -6375,7 +6911,9 @@ class RecordTools(ToolBase):
|
|
|
6375
6911
|
try:
|
|
6376
6912
|
result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
|
|
6377
6913
|
payload = result if isinstance(result, dict) else None
|
|
6378
|
-
except QingflowApiError:
|
|
6914
|
+
except QingflowApiError as exc:
|
|
6915
|
+
if is_auth_like_error(exc):
|
|
6916
|
+
raise
|
|
6379
6917
|
payload = None
|
|
6380
6918
|
self._relation_base_info_cache[cache_key] = payload or {}
|
|
6381
6919
|
return payload
|
|
@@ -6648,6 +7186,26 @@ class RecordTools(ToolBase):
|
|
|
6648
7186
|
or bool(fields)
|
|
6649
7187
|
)
|
|
6650
7188
|
|
|
7189
|
+
def _member_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
|
|
7190
|
+
"""Return true when the frontend field-scope endpoint is safer than directory expansion."""
|
|
7191
|
+
scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
|
|
7192
|
+
if field.member_select_scope_type != 2:
|
|
7193
|
+
return False
|
|
7194
|
+
return bool(
|
|
7195
|
+
_scope_has_dynamic_or_external(scope)
|
|
7196
|
+
or list(scope.get("depart") or [])
|
|
7197
|
+
or list(scope.get("role") or [])
|
|
7198
|
+
)
|
|
7199
|
+
|
|
7200
|
+
def _department_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
|
|
7201
|
+
"""Return true when static preview would otherwise need ContactAuth-only directory APIs."""
|
|
7202
|
+
scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
|
|
7203
|
+
if field.dept_select_scope_type != 2:
|
|
7204
|
+
return False
|
|
7205
|
+
if _scope_has_dynamic_or_external(scope):
|
|
7206
|
+
return True
|
|
7207
|
+
return bool(_normalize_bool(scope.get("includeSubDeparts")) or not list(scope.get("depart") or []))
|
|
7208
|
+
|
|
6651
7209
|
def _build_candidate_lookup_state(
|
|
6652
7210
|
self,
|
|
6653
7211
|
profile: str,
|
|
@@ -6666,7 +7224,9 @@ class RecordTools(ToolBase):
|
|
|
6666
7224
|
if apply_id is not None:
|
|
6667
7225
|
try:
|
|
6668
7226
|
base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
|
|
6669
|
-
except QingflowApiError:
|
|
7227
|
+
except QingflowApiError as exc:
|
|
7228
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
7229
|
+
raise
|
|
6670
7230
|
context_complete = False
|
|
6671
7231
|
state = LookupResolutionState(
|
|
6672
7232
|
operation="update" if apply_id is not None else "insert",
|
|
@@ -7156,15 +7716,16 @@ class RecordTools(ToolBase):
|
|
|
7156
7716
|
)
|
|
7157
7717
|
if configured_candidate is not None:
|
|
7158
7718
|
self._merge_department_candidate(merged, configured_candidate)
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7719
|
+
if include_sub:
|
|
7720
|
+
for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=True):
|
|
7721
|
+
normalized = _normalize_candidate_department(
|
|
7722
|
+
dept,
|
|
7723
|
+
source_kind="department",
|
|
7724
|
+
source_id=dept_id,
|
|
7725
|
+
source_value=dept_name,
|
|
7726
|
+
)
|
|
7727
|
+
if normalized is not None:
|
|
7728
|
+
self._merge_department_candidate(merged, normalized)
|
|
7168
7729
|
filtered = _filter_department_candidates(list(merged.values()), keyword)
|
|
7169
7730
|
filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
|
|
7170
7731
|
return filtered
|
|
@@ -8305,22 +8866,10 @@ class RecordTools(ToolBase):
|
|
|
8305
8866
|
field_index_override: FieldIndex | None = None,
|
|
8306
8867
|
) -> JSONObject:
|
|
8307
8868
|
"""执行内部辅助逻辑。"""
|
|
8308
|
-
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
8309
|
-
base_index = field_index_override or _build_applicant_top_level_field_index(schema)
|
|
8310
|
-
question_relations = _collect_question_relations(schema)
|
|
8311
|
-
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
8312
|
-
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
8313
|
-
index = base_index
|
|
8314
|
-
if operation == "create" and field_index_override is None:
|
|
8315
|
-
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
8316
|
-
schema,
|
|
8317
|
-
linked_field_ids=runtime_linked_field_ids,
|
|
8318
|
-
)
|
|
8319
|
-
index = _merge_field_indexes(base_index, linked_hidden_index)
|
|
8320
8869
|
normalized_fields = fields or {}
|
|
8321
8870
|
normalized_answers_input = answers or []
|
|
8322
8871
|
resolved_view: AccessibleViewRoute | None = None
|
|
8323
|
-
selector_index =
|
|
8872
|
+
selector_index: FieldIndex | None = field_index_override
|
|
8324
8873
|
browse_writable_field_ids: set[int] = set()
|
|
8325
8874
|
visible_question_ids: set[int] = set()
|
|
8326
8875
|
if any(item is not None for item in (view_id, list_type, view_key, view_name)):
|
|
@@ -8346,6 +8895,31 @@ class RecordTools(ToolBase):
|
|
|
8346
8895
|
visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
|
|
8347
8896
|
else:
|
|
8348
8897
|
compatibility_warnings = []
|
|
8898
|
+
if field_index_override is not None:
|
|
8899
|
+
base_index = field_index_override
|
|
8900
|
+
question_relations: list[JSONObject] = []
|
|
8901
|
+
runtime_linked_field_ids: set[int] = set()
|
|
8902
|
+
index = base_index
|
|
8903
|
+
elif operation == "update" and resolved_view is not None:
|
|
8904
|
+
base_index = cast(FieldIndex, selector_index)
|
|
8905
|
+
question_relations = []
|
|
8906
|
+
runtime_linked_field_ids = set()
|
|
8907
|
+
index = base_index
|
|
8908
|
+
else:
|
|
8909
|
+
schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
|
|
8910
|
+
base_index = _build_applicant_top_level_field_index(schema)
|
|
8911
|
+
question_relations = _collect_question_relations(schema)
|
|
8912
|
+
runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
8913
|
+
runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
|
|
8914
|
+
index = base_index
|
|
8915
|
+
if operation == "create":
|
|
8916
|
+
linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
|
|
8917
|
+
schema,
|
|
8918
|
+
linked_field_ids=runtime_linked_field_ids,
|
|
8919
|
+
)
|
|
8920
|
+
index = _merge_field_indexes(base_index, linked_hidden_index)
|
|
8921
|
+
if selector_index is None:
|
|
8922
|
+
selector_index = index
|
|
8349
8923
|
resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
|
|
8350
8924
|
support_matrix = _summarize_write_support(resolved_fields)
|
|
8351
8925
|
invalid_fields: list[JSONObject] = []
|
|
@@ -8389,7 +8963,9 @@ class RecordTools(ToolBase):
|
|
|
8389
8963
|
apply_id=apply_id,
|
|
8390
8964
|
)
|
|
8391
8965
|
existing_answers_loaded = True
|
|
8392
|
-
except QingflowApiError:
|
|
8966
|
+
except QingflowApiError as exc:
|
|
8967
|
+
if not _is_optional_record_auxiliary_lookup_error(exc):
|
|
8968
|
+
raise
|
|
8393
8969
|
validation_warnings.append(
|
|
8394
8970
|
"update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
|
|
8395
8971
|
)
|
|
@@ -8978,7 +9554,7 @@ class RecordTools(ToolBase):
|
|
|
8978
9554
|
break
|
|
8979
9555
|
except QingflowApiError as exc:
|
|
8980
9556
|
last_error = exc
|
|
8981
|
-
if exc
|
|
9557
|
+
if _is_record_permission_denied_error(exc):
|
|
8982
9558
|
continue
|
|
8983
9559
|
raise
|
|
8984
9560
|
if result is None:
|
|
@@ -9081,7 +9657,21 @@ class RecordTools(ToolBase):
|
|
|
9081
9657
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
9082
9658
|
|
|
9083
9659
|
def runner(session_profile, context):
|
|
9084
|
-
|
|
9660
|
+
needs_index = verify_write or bool(fields) or _answers_need_resolution(answers or [])
|
|
9661
|
+
update_index = None
|
|
9662
|
+
if needs_index:
|
|
9663
|
+
update_index = (
|
|
9664
|
+
self._get_system_browse_field_index(
|
|
9665
|
+
profile,
|
|
9666
|
+
context,
|
|
9667
|
+
app_key,
|
|
9668
|
+
list_type=DEFAULT_RECORD_LIST_TYPE,
|
|
9669
|
+
force_refresh=force_refresh_form,
|
|
9670
|
+
)
|
|
9671
|
+
if role == 1
|
|
9672
|
+
else self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
|
|
9673
|
+
)
|
|
9674
|
+
index = update_index if verify_write else None
|
|
9085
9675
|
normalized_answers = self._resolve_answers(
|
|
9086
9676
|
profile,
|
|
9087
9677
|
context,
|
|
@@ -9089,6 +9679,7 @@ class RecordTools(ToolBase):
|
|
|
9089
9679
|
answers=answers or [],
|
|
9090
9680
|
fields=fields or {},
|
|
9091
9681
|
force_refresh_form=force_refresh_form,
|
|
9682
|
+
field_index_override=update_index,
|
|
9092
9683
|
)
|
|
9093
9684
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
9094
9685
|
try:
|
|
@@ -9142,13 +9733,14 @@ class RecordTools(ToolBase):
|
|
|
9142
9733
|
def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
|
|
9143
9734
|
"""执行记录相关逻辑。"""
|
|
9144
9735
|
normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
|
|
9736
|
+
delete_list_type = self._resolve_record_delete_list_type(view_id=None, list_type=list_type)
|
|
9145
9737
|
|
|
9146
9738
|
def runner(session_profile, context):
|
|
9147
9739
|
result = self.backend.request(
|
|
9148
9740
|
"DELETE",
|
|
9149
9741
|
context,
|
|
9150
9742
|
f"/app/{app_key}/apply",
|
|
9151
|
-
json_body={"type":
|
|
9743
|
+
json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
|
|
9152
9744
|
)
|
|
9153
9745
|
return self._attach_human_review_notice(
|
|
9154
9746
|
{
|
|
@@ -9157,6 +9749,7 @@ class RecordTools(ToolBase):
|
|
|
9157
9749
|
"request_route": self._request_route_payload(context),
|
|
9158
9750
|
"app_key": app_key,
|
|
9159
9751
|
"apply_id": normalized_apply_id,
|
|
9752
|
+
"list_type": delete_list_type,
|
|
9160
9753
|
"result": result,
|
|
9161
9754
|
},
|
|
9162
9755
|
operation="delete",
|
|
@@ -9201,7 +9794,7 @@ class RecordTools(ToolBase):
|
|
|
9201
9794
|
"GET",
|
|
9202
9795
|
context,
|
|
9203
9796
|
f"/app/{app_key}/apply/{apply_id}",
|
|
9204
|
-
params={"role":
|
|
9797
|
+
params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
|
|
9205
9798
|
)
|
|
9206
9799
|
answers = result.get("answers") if isinstance(result, dict) else None
|
|
9207
9800
|
answer_list = answers if isinstance(answers, list) else []
|
|
@@ -9560,7 +10153,7 @@ class RecordTools(ToolBase):
|
|
|
9560
10153
|
used_list_type: int | None = None
|
|
9561
10154
|
if view_selection is not None:
|
|
9562
10155
|
fallback_list_types = [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
|
|
9563
|
-
elif view_route.list_type is not None
|
|
10156
|
+
elif view_route.list_type is not None:
|
|
9564
10157
|
fallback_list_types = [view_route.list_type]
|
|
9565
10158
|
else:
|
|
9566
10159
|
fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
|
|
@@ -9791,7 +10384,7 @@ class RecordTools(ToolBase):
|
|
|
9791
10384
|
try:
|
|
9792
10385
|
payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
|
|
9793
10386
|
except QingflowApiError as exc:
|
|
9794
|
-
if exc
|
|
10387
|
+
if _is_optional_schema_permission_error(exc):
|
|
9795
10388
|
self._view_config_cache[cache_key] = None
|
|
9796
10389
|
return None
|
|
9797
10390
|
raise
|
|
@@ -9912,7 +10505,12 @@ class RecordTools(ToolBase):
|
|
|
9912
10505
|
)
|
|
9913
10506
|
normalized = _normalize_data_list_base_info_schema(payload)
|
|
9914
10507
|
if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
|
|
9915
|
-
|
|
10508
|
+
try:
|
|
10509
|
+
return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
10510
|
+
except QingflowApiError as exc:
|
|
10511
|
+
if not _is_optional_schema_permission_error(exc):
|
|
10512
|
+
raise
|
|
10513
|
+
return normalized
|
|
9916
10514
|
self._form_cache[cache_key] = normalized
|
|
9917
10515
|
return normalized
|
|
9918
10516
|
|
|
@@ -9944,8 +10542,16 @@ class RecordTools(ToolBase):
|
|
|
9944
10542
|
cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
|
|
9945
10543
|
if not force_refresh and cache_key in self._form_cache:
|
|
9946
10544
|
return self._form_cache[cache_key]
|
|
9947
|
-
|
|
9948
|
-
|
|
10545
|
+
try:
|
|
10546
|
+
payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
|
|
10547
|
+
normalized = _normalize_data_list_base_info_schema(payload)
|
|
10548
|
+
form_ques = normalized.get("formQues")
|
|
10549
|
+
if not isinstance(form_ques, list) or not form_ques:
|
|
10550
|
+
normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
|
|
10551
|
+
except QingflowApiError as exc:
|
|
10552
|
+
if not _is_optional_schema_permission_error(exc):
|
|
10553
|
+
raise
|
|
10554
|
+
normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
|
|
9949
10555
|
self._form_cache[cache_key] = normalized
|
|
9950
10556
|
return normalized
|
|
9951
10557
|
|
|
@@ -10001,22 +10607,6 @@ class RecordTools(ToolBase):
|
|
|
10001
10607
|
force_refresh: bool,
|
|
10002
10608
|
) -> JSONObject:
|
|
10003
10609
|
"""Build the UI/table-view readable field scope from apply/baseInfo."""
|
|
10004
|
-
applicant_index: FieldIndex | None
|
|
10005
|
-
applicant_writable_field_ids: set[int]
|
|
10006
|
-
try:
|
|
10007
|
-
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10008
|
-
except QingflowApiError as exc:
|
|
10009
|
-
if exc.backend_code != 40002:
|
|
10010
|
-
raise
|
|
10011
|
-
applicant_index = None
|
|
10012
|
-
applicant_writable_field_ids = set()
|
|
10013
|
-
else:
|
|
10014
|
-
applicant_writable_field_ids = {
|
|
10015
|
-
field.que_id
|
|
10016
|
-
for field in applicant_index.by_id.values()
|
|
10017
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
10018
|
-
}
|
|
10019
|
-
|
|
10020
10610
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
10021
10611
|
schema = self._get_custom_view_browse_schema(
|
|
10022
10612
|
profile,
|
|
@@ -10025,6 +10615,16 @@ class RecordTools(ToolBase):
|
|
|
10025
10615
|
force_refresh=force_refresh,
|
|
10026
10616
|
)
|
|
10027
10617
|
index = _build_top_level_field_index(schema)
|
|
10618
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
10619
|
+
return {
|
|
10620
|
+
"index": index,
|
|
10621
|
+
"writable_field_ids": {
|
|
10622
|
+
field.que_id
|
|
10623
|
+
for field in index.by_id.values()
|
|
10624
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10625
|
+
},
|
|
10626
|
+
"visible_question_ids": visible_question_ids,
|
|
10627
|
+
}
|
|
10028
10628
|
elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
10029
10629
|
schema = self._get_system_browse_base_info_schema(
|
|
10030
10630
|
profile,
|
|
@@ -10034,34 +10634,26 @@ class RecordTools(ToolBase):
|
|
|
10034
10634
|
force_refresh=force_refresh,
|
|
10035
10635
|
)
|
|
10036
10636
|
index = _build_top_level_field_index(schema)
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
index = _build_top_level_field_index({"formQues": [[_form_field_to_question(field) for field in enriched_fields]]})
|
|
10637
|
+
visible_question_ids = {field.que_id for field in index.by_id.values()}
|
|
10638
|
+
return {
|
|
10639
|
+
"index": index,
|
|
10640
|
+
"writable_field_ids": {
|
|
10641
|
+
field.que_id
|
|
10642
|
+
for field in index.by_id.values()
|
|
10643
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10644
|
+
},
|
|
10645
|
+
"visible_question_ids": visible_question_ids,
|
|
10646
|
+
}
|
|
10048
10647
|
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10648
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10649
|
+
visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
|
|
10650
|
+
return {
|
|
10651
|
+
"index": applicant_index,
|
|
10652
|
+
"writable_field_ids": {
|
|
10052
10653
|
field.que_id
|
|
10053
|
-
for field in
|
|
10654
|
+
for field in applicant_index.by_id.values()
|
|
10054
10655
|
if bool(self._schema_write_hints(field)["writable"])
|
|
10055
|
-
}
|
|
10056
|
-
else:
|
|
10057
|
-
writable_field_ids = {
|
|
10058
|
-
field_id
|
|
10059
|
-
for field_id in visible_question_ids
|
|
10060
|
-
if field_id in applicant_writable_field_ids
|
|
10061
|
-
}
|
|
10062
|
-
return {
|
|
10063
|
-
"index": index,
|
|
10064
|
-
"writable_field_ids": writable_field_ids,
|
|
10656
|
+
},
|
|
10065
10657
|
"visible_question_ids": visible_question_ids,
|
|
10066
10658
|
}
|
|
10067
10659
|
|
|
@@ -10075,23 +10667,13 @@ class RecordTools(ToolBase):
|
|
|
10075
10667
|
force_refresh: bool,
|
|
10076
10668
|
) -> JSONObject:
|
|
10077
10669
|
"""执行内部辅助逻辑。"""
|
|
10078
|
-
applicant_index: FieldIndex | None
|
|
10079
|
-
applicant_writable_field_ids: set[int]
|
|
10080
|
-
try:
|
|
10081
|
-
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10082
|
-
except QingflowApiError as exc:
|
|
10083
|
-
if exc.backend_code != 40002:
|
|
10084
|
-
raise
|
|
10085
|
-
applicant_index = None
|
|
10086
|
-
applicant_writable_field_ids = set()
|
|
10087
|
-
else:
|
|
10088
|
-
applicant_writable_field_ids = {
|
|
10089
|
-
field.que_id
|
|
10090
|
-
for field in applicant_index.by_id.values()
|
|
10091
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
10092
|
-
}
|
|
10093
10670
|
if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
|
|
10094
|
-
schema = self.
|
|
10671
|
+
schema = self._get_custom_view_browse_schema(
|
|
10672
|
+
profile,
|
|
10673
|
+
context,
|
|
10674
|
+
resolved_view.view_selection.view_key,
|
|
10675
|
+
force_refresh=force_refresh,
|
|
10676
|
+
)
|
|
10095
10677
|
index = _build_top_level_field_index(schema)
|
|
10096
10678
|
visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
|
|
10097
10679
|
if not visible_question_ids:
|
|
@@ -10107,6 +10689,12 @@ class RecordTools(ToolBase):
|
|
|
10107
10689
|
index = _build_top_level_field_index(schema)
|
|
10108
10690
|
visible_question_ids = _question_ids_from_schema(schema)
|
|
10109
10691
|
else:
|
|
10692
|
+
applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
|
|
10693
|
+
applicant_writable_field_ids = {
|
|
10694
|
+
field.que_id
|
|
10695
|
+
for field in applicant_index.by_id.values()
|
|
10696
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10697
|
+
}
|
|
10110
10698
|
index = applicant_index or _build_top_level_field_index(
|
|
10111
10699
|
self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
|
|
10112
10700
|
)
|
|
@@ -10125,43 +10713,13 @@ class RecordTools(ToolBase):
|
|
|
10125
10713
|
"visible_question_ids": set(visible_question_ids),
|
|
10126
10714
|
}
|
|
10127
10715
|
|
|
10128
|
-
if applicant_index is None:
|
|
10129
|
-
return {
|
|
10130
|
-
"index": index,
|
|
10131
|
-
"writable_field_ids": {
|
|
10132
|
-
field.que_id
|
|
10133
|
-
for field in index.by_id.values()
|
|
10134
|
-
if bool(self._schema_write_hints(field)["writable"])
|
|
10135
|
-
},
|
|
10136
|
-
"visible_question_ids": visible_question_ids,
|
|
10137
|
-
}
|
|
10138
|
-
|
|
10139
|
-
augmented_fields = [
|
|
10140
|
-
_clone_form_field(applicant_index.by_id.get(str(field.que_id)) or field)
|
|
10141
|
-
for field in index.by_id.values()
|
|
10142
|
-
]
|
|
10143
|
-
augmented_field_ids = {field.que_id for field in augmented_fields}
|
|
10144
|
-
writable_field_ids = {
|
|
10145
|
-
field_id
|
|
10146
|
-
for field_id in visible_question_ids
|
|
10147
|
-
if field_id in applicant_writable_field_ids
|
|
10148
|
-
}
|
|
10149
|
-
for field in applicant_index.by_id.values():
|
|
10150
|
-
descendant_ids = _subtable_descendant_ids(field)
|
|
10151
|
-
field_visible = field.que_id in visible_question_ids
|
|
10152
|
-
descendant_visible = bool(descendant_ids and (descendant_ids & visible_question_ids))
|
|
10153
|
-
if not field_visible and not descendant_visible:
|
|
10154
|
-
continue
|
|
10155
|
-
if field.que_id not in augmented_field_ids:
|
|
10156
|
-
augmented_fields.append(_clone_form_field(field))
|
|
10157
|
-
augmented_field_ids.add(field.que_id)
|
|
10158
|
-
if descendant_visible:
|
|
10159
|
-
visible_question_ids.add(field.que_id)
|
|
10160
|
-
if field.que_id in applicant_writable_field_ids and (field_visible or descendant_visible):
|
|
10161
|
-
writable_field_ids.add(field.que_id)
|
|
10162
10716
|
return {
|
|
10163
|
-
"index":
|
|
10164
|
-
"writable_field_ids":
|
|
10717
|
+
"index": index,
|
|
10718
|
+
"writable_field_ids": {
|
|
10719
|
+
field.que_id
|
|
10720
|
+
for field in index.by_id.values()
|
|
10721
|
+
if bool(self._schema_write_hints(field)["writable"])
|
|
10722
|
+
},
|
|
10165
10723
|
"visible_question_ids": visible_question_ids,
|
|
10166
10724
|
}
|
|
10167
10725
|
|
|
@@ -10226,7 +10784,7 @@ class RecordTools(ToolBase):
|
|
|
10226
10784
|
try:
|
|
10227
10785
|
payload = self.backend.request("GET", context, f"/view/{view_key}/question")
|
|
10228
10786
|
except QingflowApiError as exc:
|
|
10229
|
-
if exc
|
|
10787
|
+
if _is_record_permission_denied_error(exc):
|
|
10230
10788
|
return set()
|
|
10231
10789
|
raise
|
|
10232
10790
|
if not isinstance(payload, list):
|
|
@@ -10270,7 +10828,7 @@ class RecordTools(ToolBase):
|
|
|
10270
10828
|
)
|
|
10271
10829
|
return True
|
|
10272
10830
|
except QingflowApiError as exc:
|
|
10273
|
-
if exc
|
|
10831
|
+
if _is_record_permission_denied_error(exc):
|
|
10274
10832
|
return False
|
|
10275
10833
|
raise
|
|
10276
10834
|
|
|
@@ -10423,7 +10981,12 @@ class RecordTools(ToolBase):
|
|
|
10423
10981
|
requested_name = _normalize_optional_text(view_name)
|
|
10424
10982
|
if requested_key is None and requested_name is None:
|
|
10425
10983
|
return None
|
|
10426
|
-
|
|
10984
|
+
try:
|
|
10985
|
+
views = self._get_view_list(profile, context, app_key)
|
|
10986
|
+
except QingflowApiError as exc:
|
|
10987
|
+
if requested_key is None or not _is_record_permission_denied_error(exc):
|
|
10988
|
+
raise
|
|
10989
|
+
views = []
|
|
10427
10990
|
selected: JSONObject | None = None
|
|
10428
10991
|
if requested_key is not None:
|
|
10429
10992
|
selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
|
|
@@ -10621,9 +11184,11 @@ class RecordTools(ToolBase):
|
|
|
10621
11184
|
|
|
10622
11185
|
def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
|
|
10623
11186
|
"""执行内部辅助逻辑。"""
|
|
10624
|
-
if error
|
|
11187
|
+
if is_auth_like_error(error):
|
|
11188
|
+
return False
|
|
11189
|
+
if backend_code_int(error) in {40002, 40027, 404}:
|
|
10625
11190
|
return True
|
|
10626
|
-
if error.http_status
|
|
11191
|
+
if error.http_status == 404:
|
|
10627
11192
|
return True
|
|
10628
11193
|
return False
|
|
10629
11194
|
|
|
@@ -11405,6 +11970,8 @@ class RecordTools(ToolBase):
|
|
|
11405
11970
|
schema: JSONObject = {}
|
|
11406
11971
|
if isinstance(raw.get("subQuestions"), list):
|
|
11407
11972
|
schema["formQues"] = [raw["subQuestions"]]
|
|
11973
|
+
elif isinstance(raw.get("subQues"), list):
|
|
11974
|
+
schema["formQues"] = [raw["subQues"]]
|
|
11408
11975
|
elif isinstance(raw.get("innerQuestions"), list):
|
|
11409
11976
|
schema["formQues"] = raw["innerQuestions"]
|
|
11410
11977
|
index = _build_field_index(schema)
|
|
@@ -11444,6 +12011,70 @@ class RecordTools(ToolBase):
|
|
|
11444
12011
|
)
|
|
11445
12012
|
)
|
|
11446
12013
|
|
|
12014
|
+
def _candidate_lookup_failed_response(
|
|
12015
|
+
self,
|
|
12016
|
+
*,
|
|
12017
|
+
profile: str,
|
|
12018
|
+
session_profile, # type: ignore[no-untyped-def]
|
|
12019
|
+
context, # type: ignore[no-untyped-def]
|
|
12020
|
+
kind: str,
|
|
12021
|
+
error: RecordInputError,
|
|
12022
|
+
field: FormField,
|
|
12023
|
+
app_key: str,
|
|
12024
|
+
record_id_text: str | None,
|
|
12025
|
+
workflow_node_id: int | None,
|
|
12026
|
+
fields_present: bool,
|
|
12027
|
+
keyword: str,
|
|
12028
|
+
scope_source: str,
|
|
12029
|
+
) -> JSONObject:
|
|
12030
|
+
"""Return a structured result when an optional field candidate lookup is unavailable."""
|
|
12031
|
+
error_payload = error.to_dict()
|
|
12032
|
+
error_details = error_payload.get("details") if isinstance(error_payload.get("details"), dict) else {}
|
|
12033
|
+
candidate_error = error_details.get("candidate_error") if isinstance(error_details.get("candidate_error"), dict) else {}
|
|
12034
|
+
warning_transport = {
|
|
12035
|
+
key: candidate_error.get(key)
|
|
12036
|
+
for key in ("backend_code", "http_status", "request_id")
|
|
12037
|
+
if candidate_error.get(key) is not None
|
|
12038
|
+
}
|
|
12039
|
+
selection: JSONObject = {
|
|
12040
|
+
"app_key": app_key,
|
|
12041
|
+
"field_id": field.que_id,
|
|
12042
|
+
"field_title": field.que_title,
|
|
12043
|
+
"record_id": record_id_text,
|
|
12044
|
+
"workflow_node_id": workflow_node_id,
|
|
12045
|
+
"fields_present": fields_present,
|
|
12046
|
+
"keyword": keyword,
|
|
12047
|
+
"permission_scope": "applicant_node",
|
|
12048
|
+
}
|
|
12049
|
+
return {
|
|
12050
|
+
"profile": profile,
|
|
12051
|
+
"ws_id": session_profile.selected_ws_id,
|
|
12052
|
+
"ok": False,
|
|
12053
|
+
"status": "failed",
|
|
12054
|
+
"error_code": error.error_code,
|
|
12055
|
+
"message": error.message,
|
|
12056
|
+
"request_route": self._request_route_payload(context),
|
|
12057
|
+
"warnings": [
|
|
12058
|
+
{
|
|
12059
|
+
"code": error.error_code,
|
|
12060
|
+
"message": error.fix_hint,
|
|
12061
|
+
"kind": kind,
|
|
12062
|
+
"field_id": field.que_id,
|
|
12063
|
+
"field_title": field.que_title,
|
|
12064
|
+
**warning_transport,
|
|
12065
|
+
}
|
|
12066
|
+
],
|
|
12067
|
+
"output_profile": "normal",
|
|
12068
|
+
"data": {
|
|
12069
|
+
"items": [],
|
|
12070
|
+
"pagination": {"returned_items": 0},
|
|
12071
|
+
"selection": selection,
|
|
12072
|
+
"scope_source": scope_source,
|
|
12073
|
+
"fix_hint": error.fix_hint,
|
|
12074
|
+
},
|
|
12075
|
+
"details": error_details,
|
|
12076
|
+
}
|
|
12077
|
+
|
|
11447
12078
|
def _request_route_payload(self, context) -> JSONObject: # type: ignore[no-untyped-def]
|
|
11448
12079
|
"""执行内部辅助逻辑。"""
|
|
11449
12080
|
describe_route = getattr(self.backend, "describe_route", None)
|
|
@@ -11617,7 +12248,7 @@ class RecordTools(ToolBase):
|
|
|
11617
12248
|
selection: JSONObject | None,
|
|
11618
12249
|
) -> None:
|
|
11619
12250
|
"""执行内部辅助逻辑。"""
|
|
11620
|
-
if exc
|
|
12251
|
+
if not _is_record_permission_denied_error(exc):
|
|
11621
12252
|
raise exc
|
|
11622
12253
|
raise_tool_error(
|
|
11623
12254
|
QingflowApiError(
|
|
@@ -11783,6 +12414,7 @@ class RecordTools(ToolBase):
|
|
|
11783
12414
|
response_status = raw_status or "failed"
|
|
11784
12415
|
update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
|
|
11785
12416
|
tried_routes = raw_apply.get("tried_routes") if isinstance(raw_apply.get("tried_routes"), list) else []
|
|
12417
|
+
expose_tried_routes = output_profile == "verbose" or response_status != "success"
|
|
11786
12418
|
response: JSONObject = {
|
|
11787
12419
|
"profile": raw_apply.get("profile"),
|
|
11788
12420
|
"ws_id": raw_apply.get("ws_id"),
|
|
@@ -11795,7 +12427,6 @@ class RecordTools(ToolBase):
|
|
|
11795
12427
|
"warnings": warnings,
|
|
11796
12428
|
"output_profile": output_profile,
|
|
11797
12429
|
"update_route": update_route,
|
|
11798
|
-
"tried_routes": tried_routes,
|
|
11799
12430
|
"data": {
|
|
11800
12431
|
"action": {"operation": operation, "executed": True},
|
|
11801
12432
|
"resource": resource,
|
|
@@ -11807,9 +12438,11 @@ class RecordTools(ToolBase):
|
|
|
11807
12438
|
"resolved_fields": resolved_fields,
|
|
11808
12439
|
"human_review": self._record_write_human_review_payload(operation, enabled=human_review),
|
|
11809
12440
|
"update_route": update_route,
|
|
11810
|
-
"tried_routes": tried_routes,
|
|
11811
12441
|
},
|
|
11812
12442
|
}
|
|
12443
|
+
if expose_tried_routes:
|
|
12444
|
+
response["tried_routes"] = tried_routes
|
|
12445
|
+
response["data"]["tried_routes"] = tried_routes
|
|
11813
12446
|
if record_id is not None:
|
|
11814
12447
|
response["record_id"] = record_id
|
|
11815
12448
|
if apply_id is not None:
|
|
@@ -11989,7 +12622,7 @@ class RecordTools(ToolBase):
|
|
|
11989
12622
|
)
|
|
11990
12623
|
return errors
|
|
11991
12624
|
|
|
11992
|
-
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int]) -> JSONObject:
|
|
12625
|
+
def _record_delete_many(self, *, profile: str, app_key: str, record_ids: list[int], list_type: int = DEFAULT_RECORD_LIST_TYPE) -> JSONObject:
|
|
11993
12626
|
"""执行内部辅助逻辑。"""
|
|
11994
12627
|
if not app_key:
|
|
11995
12628
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
@@ -12002,14 +12635,14 @@ class RecordTools(ToolBase):
|
|
|
12002
12635
|
"DELETE",
|
|
12003
12636
|
context,
|
|
12004
12637
|
f"/app/{app_key}/apply",
|
|
12005
|
-
json_body={"type":
|
|
12638
|
+
json_body={"type": list_type, "applyIds": normalized_ids},
|
|
12006
12639
|
)
|
|
12007
12640
|
return {
|
|
12008
12641
|
"profile": profile,
|
|
12009
12642
|
"ws_id": session_profile.selected_ws_id,
|
|
12010
12643
|
"request_route": self._request_route_payload(context),
|
|
12011
12644
|
"result": result,
|
|
12012
|
-
"resource": {"type": "record", "apply_ids": normalized_ids},
|
|
12645
|
+
"resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
|
|
12013
12646
|
"ok": True,
|
|
12014
12647
|
}
|
|
12015
12648
|
|
|
@@ -12583,6 +13216,30 @@ class RecordTools(ToolBase):
|
|
|
12583
13216
|
},
|
|
12584
13217
|
)
|
|
12585
13218
|
|
|
13219
|
+
def _candidate_lookup_error(
|
|
13220
|
+
self,
|
|
13221
|
+
*,
|
|
13222
|
+
kind: str,
|
|
13223
|
+
field: FormField,
|
|
13224
|
+
value: JSONValue,
|
|
13225
|
+
error: QingflowApiError,
|
|
13226
|
+
) -> RecordInputError:
|
|
13227
|
+
"""Build the standard candidate lookup failure without raising it."""
|
|
13228
|
+
field_kind = "member" if kind == "member" else "department"
|
|
13229
|
+
return RecordInputError(
|
|
13230
|
+
message=f"{field_kind} candidates for field '{field.que_title}' could not be loaded",
|
|
13231
|
+
error_code=f"{kind.upper()}_CANDIDATE_LOOKUP_FAILED",
|
|
13232
|
+
fix_hint=(
|
|
13233
|
+
f"Run record_{field_kind}_candidates again after the backend error is resolved, "
|
|
13234
|
+
"then choose one returned item exactly."
|
|
13235
|
+
),
|
|
13236
|
+
details={
|
|
13237
|
+
"field": _field_ref_payload(field),
|
|
13238
|
+
"received_value": value,
|
|
13239
|
+
"candidate_error": error.to_dict(),
|
|
13240
|
+
},
|
|
13241
|
+
)
|
|
13242
|
+
|
|
12586
13243
|
def _candidate_keyword_from_value(
|
|
12587
13244
|
self,
|
|
12588
13245
|
value: JSONValue,
|
|
@@ -12847,14 +13504,7 @@ class RecordTools(ToolBase):
|
|
|
12847
13504
|
|
|
12848
13505
|
def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject: # type: ignore[no-untyped-def]
|
|
12849
13506
|
"""执行内部辅助逻辑。"""
|
|
12850
|
-
|
|
12851
|
-
"GET",
|
|
12852
|
-
context,
|
|
12853
|
-
"/contact/deptByPage",
|
|
12854
|
-
params={"keyword": keyword, "pageNum": 1, "pageSize": 20},
|
|
12855
|
-
)
|
|
12856
|
-
rows = payload.get("list") if isinstance(payload, dict) else None
|
|
12857
|
-
items = [item for item in rows if isinstance(item, dict)] if isinstance(rows, list) else []
|
|
13507
|
+
items = self._search_workspace_departments(context, keyword=keyword)
|
|
12858
13508
|
normalized_keyword = keyword.strip()
|
|
12859
13509
|
exact = [
|
|
12860
13510
|
item for item in items
|
|
@@ -13379,6 +14029,7 @@ class RecordTools(ToolBase):
|
|
|
13379
14029
|
normalized_answers: list[JSONObject],
|
|
13380
14030
|
index: FieldIndex,
|
|
13381
14031
|
verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
|
|
14032
|
+
verify_role: int | None = None,
|
|
13382
14033
|
verify_view_key: str | None = None,
|
|
13383
14034
|
) -> JSONObject:
|
|
13384
14035
|
"""执行内部辅助逻辑。"""
|
|
@@ -13398,14 +14049,20 @@ class RecordTools(ToolBase):
|
|
|
13398
14049
|
f"/view/{verify_view_key}/apply/{apply_id}",
|
|
13399
14050
|
)
|
|
13400
14051
|
else:
|
|
14052
|
+
role = verify_role if verify_role is not None else 1
|
|
13401
14053
|
record = self.backend.request(
|
|
13402
14054
|
"GET",
|
|
13403
14055
|
context,
|
|
13404
14056
|
f"/app/{app_key}/apply/{apply_id}",
|
|
13405
|
-
params={"role":
|
|
14057
|
+
params={"role": role, "listType": verify_list_type},
|
|
13406
14058
|
)
|
|
13407
14059
|
except QingflowApiError as exc:
|
|
13408
14060
|
if verify_view_key:
|
|
14061
|
+
warning: JSONObject = {
|
|
14062
|
+
"code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
|
|
14063
|
+
"message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
|
|
14064
|
+
}
|
|
14065
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
13409
14066
|
return {
|
|
13410
14067
|
"verified": False,
|
|
13411
14068
|
"verification_mode": "custom_view_record_detail",
|
|
@@ -13414,14 +14071,9 @@ class RecordTools(ToolBase):
|
|
|
13414
14071
|
"missing_fields": [],
|
|
13415
14072
|
"empty_fields": [],
|
|
13416
14073
|
"count_mismatches": [],
|
|
13417
|
-
"warnings": [
|
|
13418
|
-
"code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
|
|
13419
|
-
"message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
|
|
13420
|
-
"backend_code": exc.backend_code,
|
|
13421
|
-
"http_status": exc.http_status,
|
|
13422
|
-
}],
|
|
14074
|
+
"warnings": [warning],
|
|
13423
14075
|
}
|
|
13424
|
-
if exc
|
|
14076
|
+
if not _is_record_permission_denied_error(exc):
|
|
13425
14077
|
raise
|
|
13426
14078
|
return self._verify_record_write_result_via_initiated_tasks(
|
|
13427
14079
|
context,
|
|
@@ -13469,6 +14121,7 @@ class RecordTools(ToolBase):
|
|
|
13469
14121
|
or len(count_mismatches) > mismatch_before
|
|
13470
14122
|
):
|
|
13471
14123
|
continue
|
|
14124
|
+
continue
|
|
13472
14125
|
expected_value = _canonicalize_answer_value_for_compare(answer, field)
|
|
13473
14126
|
actual_value = _canonicalize_answer_value_for_compare(actual, field)
|
|
13474
14127
|
if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
|
|
@@ -13715,6 +14368,8 @@ def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
|
|
|
13715
14368
|
if not isinstance(payload, dict):
|
|
13716
14369
|
return {}
|
|
13717
14370
|
que_base_infos = payload.get("queBaseInfos")
|
|
14371
|
+
if not isinstance(que_base_infos, list) and isinstance(payload.get("formQues"), list):
|
|
14372
|
+
que_base_infos = payload.get("formQues")
|
|
13718
14373
|
if not isinstance(que_base_infos, list):
|
|
13719
14374
|
return {}
|
|
13720
14375
|
return {
|
|
@@ -14101,6 +14756,44 @@ def _build_answer_backed_field_index(
|
|
|
14101
14756
|
)
|
|
14102
14757
|
|
|
14103
14758
|
|
|
14759
|
+
def _merge_subtable_parent_field(primary: FormField, extra: FormField) -> FormField:
|
|
14760
|
+
if primary.que_type not in SUBTABLE_QUE_TYPES or extra.que_type not in SUBTABLE_QUE_TYPES:
|
|
14761
|
+
return primary
|
|
14762
|
+
primary_raw = dict(primary.raw) if isinstance(primary.raw, dict) else {}
|
|
14763
|
+
extra_raw = dict(extra.raw) if isinstance(extra.raw, dict) else {}
|
|
14764
|
+
primary_subquestions = primary_raw.get("subQuestions")
|
|
14765
|
+
extra_subquestions = extra_raw.get("subQuestions")
|
|
14766
|
+
if not isinstance(primary_subquestions, list) or not isinstance(extra_subquestions, list):
|
|
14767
|
+
return primary
|
|
14768
|
+
merged_subquestions = [item for item in primary_subquestions if isinstance(item, dict)]
|
|
14769
|
+
seen_ids = {
|
|
14770
|
+
_coerce_count(item.get("queId"))
|
|
14771
|
+
for item in merged_subquestions
|
|
14772
|
+
if isinstance(item, dict) and _coerce_count(item.get("queId")) is not None
|
|
14773
|
+
}
|
|
14774
|
+
for item in extra_subquestions:
|
|
14775
|
+
if not isinstance(item, dict):
|
|
14776
|
+
continue
|
|
14777
|
+
que_id = _coerce_count(item.get("queId"))
|
|
14778
|
+
if que_id is not None and que_id in seen_ids:
|
|
14779
|
+
continue
|
|
14780
|
+
merged_subquestions.append(item)
|
|
14781
|
+
if que_id is not None:
|
|
14782
|
+
seen_ids.add(que_id)
|
|
14783
|
+
if len(merged_subquestions) == len(primary_subquestions):
|
|
14784
|
+
return primary
|
|
14785
|
+
merged_raw = dict(primary_raw)
|
|
14786
|
+
merged_raw["subQuestions"] = merged_subquestions
|
|
14787
|
+
merged_field = _clone_form_field(primary)
|
|
14788
|
+
merged_field.raw = merged_raw
|
|
14789
|
+
return merged_field
|
|
14790
|
+
|
|
14791
|
+
|
|
14792
|
+
def _replace_field_in_lookup(index: dict[str, list[FormField]], field: FormField) -> None:
|
|
14793
|
+
for key, fields in list(index.items()):
|
|
14794
|
+
index[key] = [field if existing.que_id == field.que_id else existing for existing in fields]
|
|
14795
|
+
|
|
14796
|
+
|
|
14104
14797
|
def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
14105
14798
|
by_id = dict(primary.by_id)
|
|
14106
14799
|
by_title = {key: list(value) for key, value in primary.by_title.items()}
|
|
@@ -14111,12 +14804,42 @@ def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
|
|
|
14111
14804
|
|
|
14112
14805
|
for field_id, field in extra.by_id.items():
|
|
14113
14806
|
if field_id in by_id:
|
|
14807
|
+
merged_field = _merge_subtable_parent_field(by_id[field_id], field)
|
|
14808
|
+
if merged_field is not by_id[field_id]:
|
|
14809
|
+
by_id[field_id] = merged_field
|
|
14810
|
+
_replace_field_in_lookup(by_title, merged_field)
|
|
14811
|
+
_replace_field_in_lookup(by_alias, merged_field)
|
|
14114
14812
|
continue
|
|
14115
14813
|
by_id[field_id] = field
|
|
14116
14814
|
by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
|
|
14117
14815
|
for alias in field.aliases:
|
|
14118
14816
|
by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
|
|
14119
14817
|
|
|
14818
|
+
for field_id, fields in extra.subtable_leaf_by_id.items():
|
|
14819
|
+
merged = subtable_leaf_by_id.setdefault(field_id, [])
|
|
14820
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14821
|
+
for field in fields:
|
|
14822
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14823
|
+
if key not in existing:
|
|
14824
|
+
merged.append(field)
|
|
14825
|
+
existing.add(key)
|
|
14826
|
+
for title, fields in extra.subtable_leaf_by_title.items():
|
|
14827
|
+
merged = subtable_leaf_by_title.setdefault(title, [])
|
|
14828
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14829
|
+
for field in fields:
|
|
14830
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14831
|
+
if key not in existing:
|
|
14832
|
+
merged.append(field)
|
|
14833
|
+
existing.add(key)
|
|
14834
|
+
for alias, fields in extra.subtable_leaf_by_alias.items():
|
|
14835
|
+
merged = subtable_leaf_by_alias.setdefault(alias, [])
|
|
14836
|
+
existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
|
|
14837
|
+
for field in fields:
|
|
14838
|
+
key = (field.field.que_id, field.parent_field.que_id)
|
|
14839
|
+
if key not in existing:
|
|
14840
|
+
merged.append(field)
|
|
14841
|
+
existing.add(key)
|
|
14842
|
+
|
|
14120
14843
|
return FieldIndex(
|
|
14121
14844
|
by_id=by_id,
|
|
14122
14845
|
by_title=by_title,
|
|
@@ -15507,6 +16230,28 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
15507
16230
|
"message": message,
|
|
15508
16231
|
"category": exc.category,
|
|
15509
16232
|
}
|
|
16233
|
+
if is_auth_like_error(exc):
|
|
16234
|
+
payload["auth_like"] = True
|
|
16235
|
+
payload["error_code"] = "AUTH_REQUIRED"
|
|
16236
|
+
if exc.backend_code is not None:
|
|
16237
|
+
payload["backend_code"] = exc.backend_code
|
|
16238
|
+
if exc.http_status is not None:
|
|
16239
|
+
payload["http_status"] = exc.http_status
|
|
16240
|
+
request_id = getattr(exc, "request_id", None)
|
|
16241
|
+
if request_id:
|
|
16242
|
+
payload["request_id"] = request_id
|
|
16243
|
+
details = exc.details if isinstance(exc.details, dict) else {}
|
|
16244
|
+
error_code = details.get("error_code")
|
|
16245
|
+
if error_code and not payload.get("error_code"):
|
|
16246
|
+
payload["error_code"] = error_code
|
|
16247
|
+
return payload
|
|
16248
|
+
|
|
16249
|
+
|
|
16250
|
+
def _record_detail_error_warning_fields(exc: QingflowApiError) -> JSONObject:
|
|
16251
|
+
payload: JSONObject = {"category": exc.category}
|
|
16252
|
+
if is_auth_like_error(exc):
|
|
16253
|
+
payload["auth_like"] = True
|
|
16254
|
+
payload["error_code"] = "AUTH_REQUIRED"
|
|
15510
16255
|
if exc.backend_code is not None:
|
|
15511
16256
|
payload["backend_code"] = exc.backend_code
|
|
15512
16257
|
if exc.http_status is not None:
|
|
@@ -15516,11 +16261,35 @@ def _record_detail_unavailable_context(section: str, message: str, exc: Qingflow
|
|
|
15516
16261
|
payload["request_id"] = request_id
|
|
15517
16262
|
details = exc.details if isinstance(exc.details, dict) else {}
|
|
15518
16263
|
error_code = details.get("error_code")
|
|
15519
|
-
if error_code:
|
|
16264
|
+
if error_code and not payload.get("error_code"):
|
|
15520
16265
|
payload["error_code"] = error_code
|
|
15521
16266
|
return payload
|
|
15522
16267
|
|
|
15523
16268
|
|
|
16269
|
+
def _record_detail_refreshed_source_url(refresh_result: Any) -> str | None:
|
|
16270
|
+
if isinstance(refresh_result, dict):
|
|
16271
|
+
return _normalize_optional_text(refresh_result.get("source_url"))
|
|
16272
|
+
return _normalize_optional_text(refresh_result)
|
|
16273
|
+
|
|
16274
|
+
|
|
16275
|
+
def _record_detail_append_refresh_warning(
|
|
16276
|
+
warnings: list[JSONObject],
|
|
16277
|
+
refresh_result: Any,
|
|
16278
|
+
*,
|
|
16279
|
+
id_key: str,
|
|
16280
|
+
id_value: str,
|
|
16281
|
+
) -> None:
|
|
16282
|
+
if not isinstance(refresh_result, dict):
|
|
16283
|
+
return
|
|
16284
|
+
warning = refresh_result.get("warning")
|
|
16285
|
+
if not isinstance(warning, dict):
|
|
16286
|
+
return
|
|
16287
|
+
payload: JSONObject = dict(warning)
|
|
16288
|
+
payload.setdefault("code", "ASSET_STORAGE_URL_REFRESH_FAILED")
|
|
16289
|
+
payload.setdefault(id_key, id_value)
|
|
16290
|
+
warnings.append(payload)
|
|
16291
|
+
|
|
16292
|
+
|
|
15524
16293
|
_RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
|
|
15525
16294
|
_RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
|
|
15526
16295
|
_RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
|
|
@@ -15675,7 +16444,14 @@ def _record_detail_media_assets_payload(
|
|
|
15675
16444
|
except QingflowApiError as exc:
|
|
15676
16445
|
blocked = exc.http_status in {401, 403}
|
|
15677
16446
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
15678
|
-
|
|
16447
|
+
refresh_result = refresh_source_url(candidate)
|
|
16448
|
+
_record_detail_append_refresh_warning(
|
|
16449
|
+
warnings,
|
|
16450
|
+
refresh_result,
|
|
16451
|
+
id_key="asset_id",
|
|
16452
|
+
id_value=asset_id,
|
|
16453
|
+
)
|
|
16454
|
+
refreshed_url = _record_detail_refreshed_source_url(refresh_result)
|
|
15679
16455
|
if refreshed_url and refreshed_url != source_url:
|
|
15680
16456
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
15681
16457
|
try:
|
|
@@ -15717,14 +16493,13 @@ def _record_detail_media_assets_payload(
|
|
|
15717
16493
|
"readable_by_agent": False,
|
|
15718
16494
|
}
|
|
15719
16495
|
)
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
)
|
|
16496
|
+
warning: JSONObject = {
|
|
16497
|
+
"code": warning_code,
|
|
16498
|
+
"asset_id": asset_id,
|
|
16499
|
+
"message": f"record_get could not download image asset {asset_id}: {exc.message}",
|
|
16500
|
+
}
|
|
16501
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
16502
|
+
warnings.append(warning)
|
|
15728
16503
|
continue
|
|
15729
16504
|
|
|
15730
16505
|
if not isinstance(content, bytes):
|
|
@@ -15942,7 +16717,14 @@ def _record_detail_file_assets_payload(
|
|
|
15942
16717
|
except QingflowApiError as exc:
|
|
15943
16718
|
blocked = exc.http_status in {401, 403}
|
|
15944
16719
|
if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
|
|
15945
|
-
|
|
16720
|
+
refresh_result = refresh_source_url(candidate)
|
|
16721
|
+
_record_detail_append_refresh_warning(
|
|
16722
|
+
warnings,
|
|
16723
|
+
refresh_result,
|
|
16724
|
+
id_key="file_asset_id",
|
|
16725
|
+
id_value=file_asset_id,
|
|
16726
|
+
)
|
|
16727
|
+
refreshed_url = _record_detail_refreshed_source_url(refresh_result)
|
|
15946
16728
|
if refreshed_url and refreshed_url != source_url:
|
|
15947
16729
|
refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
|
|
15948
16730
|
try:
|
|
@@ -15989,14 +16771,13 @@ def _record_detail_file_assets_payload(
|
|
|
15989
16771
|
"extraction": {"status": "failed", "text_path": None, "preview": None},
|
|
15990
16772
|
}
|
|
15991
16773
|
)
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
)
|
|
16774
|
+
warning = {
|
|
16775
|
+
"code": warning_code,
|
|
16776
|
+
"file_asset_id": file_asset_id,
|
|
16777
|
+
"message": f"record_get could not download file asset {file_asset_id}: {exc.message}",
|
|
16778
|
+
}
|
|
16779
|
+
warning.update(_record_detail_error_warning_fields(exc))
|
|
16780
|
+
warnings.append(warning)
|
|
16000
16781
|
continue
|
|
16001
16782
|
|
|
16002
16783
|
if not isinstance(content, bytes):
|
|
@@ -18115,12 +18896,14 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
|
|
|
18115
18896
|
return None
|
|
18116
18897
|
|
|
18117
18898
|
|
|
18118
|
-
def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
|
|
18899
|
+
def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
|
|
18119
18900
|
normalized: list[int] = []
|
|
18120
18901
|
for item in columns:
|
|
18121
18902
|
field_id: int | None = None
|
|
18122
18903
|
if isinstance(item, int):
|
|
18123
18904
|
field_id = item
|
|
18905
|
+
elif isinstance(item, str):
|
|
18906
|
+
field_id = _coerce_count(item)
|
|
18124
18907
|
elif isinstance(item, dict):
|
|
18125
18908
|
_ensure_allowed_record_list_keys(
|
|
18126
18909
|
item,
|
|
@@ -18132,19 +18915,21 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[
|
|
|
18132
18915
|
if field_id is None or field_id < 0:
|
|
18133
18916
|
raise_tool_error(
|
|
18134
18917
|
QingflowApiError.config_error(
|
|
18135
|
-
"columns must be a list of field_id integers or {field_id} objects"
|
|
18918
|
+
"columns must be a list of field_id integers, integer strings, or {field_id} objects"
|
|
18136
18919
|
)
|
|
18137
18920
|
)
|
|
18138
18921
|
normalized.append(field_id)
|
|
18139
18922
|
return normalized
|
|
18140
18923
|
|
|
18141
18924
|
|
|
18142
|
-
def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]) -> list[int]:
|
|
18925
|
+
def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
|
|
18143
18926
|
normalized: list[int] = []
|
|
18144
18927
|
for item in query_fields:
|
|
18145
18928
|
field_id: int | None = None
|
|
18146
18929
|
if isinstance(item, int):
|
|
18147
18930
|
field_id = item
|
|
18931
|
+
elif isinstance(item, str):
|
|
18932
|
+
field_id = _coerce_count(item)
|
|
18148
18933
|
elif isinstance(item, dict):
|
|
18149
18934
|
_ensure_allowed_record_list_keys(
|
|
18150
18935
|
item,
|
|
@@ -18156,7 +18941,7 @@ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]
|
|
|
18156
18941
|
if field_id is None or field_id < 0:
|
|
18157
18942
|
raise_tool_error(
|
|
18158
18943
|
QingflowApiError.config_error(
|
|
18159
|
-
"query_fields must be a list of field_id integers or {field_id} objects"
|
|
18944
|
+
"query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
|
|
18160
18945
|
)
|
|
18161
18946
|
)
|
|
18162
18947
|
normalized.append(field_id)
|