from __future__ import annotations

import csv
import html
import mimetypes
import json
import os
import re
import time
import zipfile
from copy import deepcopy
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from decimal import Decimal, InvalidOperation
from io import BytesIO
from pathlib import Path
from typing import Any, Callable, cast
from urllib.parse import parse_qs, unquote, urlsplit
from uuid import uuid4
from xml.etree import ElementTree

from mcp.server.fastmcp import FastMCP

from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE, DEFAULT_USER_AGENT, get_mcp_home
from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
from ..id_utils import normalize_positive_id_int, stringify_backend_id
from ..json_types import JSONObject, JSONScalar, JSONValue
from ..list_type_labels import (
    SYSTEM_VIEW_DEFINITIONS,
    SYSTEM_VIEW_ID_TO_LIST_TYPE,
    get_record_list_type_label,
    get_system_view_id,
    get_system_view_name,
)
from .base import ToolBase
from .directory_tools import _directory_has_more, _directory_items


DEFAULT_QUERY_PAGE_SIZE = 50
DEFAULT_LIST_PAGE_SIZE = 200
DEFAULT_RECORD_LIST_RETURN_LIMIT = 10
BACKEND_RECORD_ACCESS_PAGE_SIZE = 1000
DEFAULT_RECORD_ACCESS_SHARD_ROWS = 20_000
RECORD_ACCESS_UNBOUNDED_ROW_THRESHOLD = 50_000
RECORD_ACCESS_TIME_BUDGET_SECONDS = 55.0
RECORD_ACCESS_MIN_REMAINING_SECONDS = 8.0
RECORD_GET_DETAIL_LOG_PAGE_SIZE = 10
RECORD_LOGS_PAGE_SIZE = 200
RECORD_LOGS_PREVIEW_LIMIT = 10
RECORD_LOGS_MAX_ITEMS = 20_000
RECORD_LOGS_TIME_BUDGET_SECONDS = 55.0
RECORD_LOGS_MIN_REMAINING_SECONDS = 8.0
RECORD_GET_MEDIA_MAX_IMAGES = 30
RECORD_GET_MEDIA_MAX_IMAGE_BYTES = 20 * 1024 * 1024
RECORD_GET_MEDIA_MAX_TOTAL_BYTES = 100 * 1024 * 1024
RECORD_GET_FILE_MAX_FILES = 50
RECORD_GET_FILE_MAX_BYTES = 50 * 1024 * 1024
RECORD_GET_FILE_MAX_TOTAL_BYTES = 200 * 1024 * 1024
RECORD_GET_FILE_TIME_BUDGET_SECONDS = 55.0
RECORD_GET_FILE_MIN_REMAINING_SECONDS = 8.0
RECORD_GET_FILE_EXTRACT_PREVIEW_CHARS = 20_000
RECORD_GET_FILE_EXTRACT_XLSX_MAX_ROWS_PER_SHEET = 200
RECORD_GET_FILE_EXTRACT_PDF_MAX_PAGES = 50
DEFAULT_ANALYSIS_PAGE_SIZE = 1000
DEFAULT_SCAN_MAX_PAGES = 10
DEFAULT_ANALYSIS_SCAN_MAX_PAGES = 100
DEFAULT_ANALYSIS_AUTO_EXPAND_PAGE_CAP = 100
DEFAULT_ROW_LIMIT = 200
DEFAULT_OUTPUT_PROFILE = "compact"
DEFAULT_MEMBER_SCOPE_FETCH_PAGE_SIZE = 200
MAX_MEMBER_SCOPE_FETCH_PAGES = 20
MAX_LIST_COLUMN_LIMIT = 20
MAX_RECORD_COLUMN_LIMIT = 20
MAX_SUMMARY_PREVIEW_COLUMN_LIMIT = 6
VERIFY_TASK_FALLBACK_PAGE_SIZE = 50
VERIFY_TASK_FALLBACK_MAX_PAGES = 3
BACKEND_LIST_SEARCH_FIELD_LIMIT = 10
RECORD_WRITE_SYSTEM_FIELD_NAMES = {
    "数据ID",
    "编号",
    "申请人",
    "申请时间",
    "创建人",
    "创建时间",
    "提交人",
    "提交时间",
    "更新时间",
    "更新人",
    "当前流程状态",
    "当前处理人",
    "当前处理节点",
    "流程标题",
}
LOOKUP_RESOLUTION_MIN_SCORE = 0.92
LOOKUP_RESOLUTION_MIN_MARGIN = 0.08
LOOKUP_CONFIRMATION_CANDIDATE_LIMIT = 5
LOOKUP_RELATION_FILTER_PAGE_SIZE = 20
MEMBER_QUE_TYPES = {5}
DEPARTMENT_QUE_TYPES = {22}
DATE_QUE_TYPES = {4}
NUMBER_QUE_TYPES = {8}
ADDRESS_QUE_TYPES = {21}
SINGLE_SELECT_QUE_TYPES = {10, 11}
MULTI_SELECT_QUE_TYPES = {12, 15}
BOOLEAN_QUE_TYPES: set[int] = set()
ATTACHMENT_QUE_TYPES = {13}
RELATION_QUE_TYPES = {25}
SUBTABLE_QUE_TYPES = {18}
VERIFY_UNSUPPORTED_WRITE_QUE_TYPES = {14, 34, 35, 36}
LAYOUT_ONLY_QUE_TYPES = {24}
DEPARTMENT_MEMBER_JUDGE_PREFIX = "deptId_"
JUDGE_EQUAL = 0
JUDGE_UNEQUAL = 1
JUDGE_GREATER = 4
JUDGE_GREATER_OR_EQUAL = 5
JUDGE_LESS = 6
JUDGE_LESS_OR_EQUAL = 7
JUDGE_EQUAL_ANY = 9
JUDGE_FUZZY_MATCH = 19
JUDGE_INCLUDE_ANY = 20
MATCH_TYPE_ACCURACY = 1
SCOPE_ALL = 1
SCOPE_NOT_EMPTY = 2
SCOPE_EMPTY = 3
LINKED_OPTIONAL_REQUIREMENT_REASON = (
    "this field may become required when linked visibility or option-driven runtime rules activate"
)
RUNTIME_LINKED_REQUIRED_REASON = (
    "required but not directly writable; this value is usually supplied by runtime linkage or upstream context"
)
LINKED_REQUIRED_FIX_HINT = (
    "this field may be activated by linked visibility or option-driven rules; provide it or adjust the upstream fields first"
)
REQUIREMENT_MODE_RUNTIME_LINKED_NON_WRITABLE = "runtime_linked_non_writable"
REQUIREMENT_MODE_LINKED_MAY_BECOME_REQUIRED = "linked_may_become_required"
SCHEMA_LINKAGE_KIND_LOGIC_VISIBILITY = "logic_visibility"
SCHEMA_LINKAGE_KIND_REFERENCE_FILL = "reference_fill"
SCHEMA_LINKAGE_KIND_FORMULA_FILL = "formula_fill"
SCHEMA_LINKAGE_ROLE_MANUAL_INPUT = "manual_input"
SCHEMA_LINKAGE_ROLE_MANUAL_INPUT_AFTER_ACTIVATION = "manual_input_after_activation"
SCHEMA_LINKAGE_ROLE_AUTO_FILL_PREFERRED = "auto_fill_preferred"
SCHEMA_LINKAGE_ROLE_AUTO_FILL_ONLY = "auto_fill_only"
SCHEMA_LINKAGE_LOGIC_SOURCE_MESSAGE = "updating this field may activate, reveal, or re-evaluate other linked fields"
SCHEMA_LINKAGE_LOGIC_TARGET_MESSAGE = "this field may appear or become required when linked visibility or option-driven rules activate"
SCHEMA_LINKAGE_LOGIC_BOTH_MESSAGE = "this field participates in linked visibility rules and may also affect other linked fields"
SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE = "updating this field may auto-fill or re-evaluate other fields from an upstream reference"
SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE = "this field is usually filled from an upstream reference selection or default matching logic"
SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE = "this field participates in reference-driven auto-fill logic"
SCHEMA_LINKAGE_FORMULA_MESSAGE = "this field is usually derived by formula or default auto-fill logic"
OPTIONAL_SCHEMA_PERMISSION_CODES = {40002, 40027, 404}
RECORD_PERMISSION_DENIED_CODES = {40002, 40027}
SYSTEM_VIEW_LIST_TYPES = {int(list_type) for _view_id, list_type, _name in SYSTEM_VIEW_DEFINITIONS}


def _is_optional_schema_permission_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    return backend_code_int(error) in OPTIONAL_SCHEMA_PERMISSION_CODES or error.http_status == 404


def _is_record_permission_denied_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    return backend_code_int(error) in RECORD_PERMISSION_DENIED_CODES


def _is_optional_record_auxiliary_lookup_error(error: QingflowApiError) -> bool:
    if is_auth_like_error(error):
        return False
    return backend_code_int(error) in {40002, 40027, 404} or error.http_status == 404


@dataclass(slots=True)
class FormField:
    que_id: int
    que_title: str
    que_type: int | None
    required: bool
    readonly: bool
    system: bool
    options: list[str]
    aliases: list[str]
    target_app_key: str | None
    target_app_name_hint: str | None
    member_select_scope_type: int | None
    member_select_scope: JSONObject | None
    dept_select_scope_type: int | None
    dept_select_scope: JSONObject | None
    raw: JSONObject


@dataclass(slots=True)
class SubtableLeafRef:
    field: FormField
    parent_field: FormField


@dataclass(slots=True)
class FieldIndex:
    by_id: dict[str, FormField]
    by_title: dict[str, list[FormField]]
    by_alias: dict[str, list[FormField]]
    subtable_leaf_by_id: dict[str, list[SubtableLeafRef]]
    subtable_leaf_by_title: dict[str, list[SubtableLeafRef]]
    subtable_leaf_by_alias: dict[str, list[SubtableLeafRef]]


@dataclass(slots=True)
class LookupResolutionState:
    operation: str
    app_key: str
    apply_id: int | None
    workflow_node_id: int | None
    force_refresh_form: bool
    context_complete: bool
    field_index: FieldIndex
    base_answers: list[JSONObject]
    normalized_answers: list[JSONObject]
    confirmation_requests: list[JSONObject]
    resolved_fields: list[JSONObject]
    unresolved_field_ids: set[int]
    unresolved_subtable_cells: set[tuple[int, int, int]]


@dataclass(slots=True)
class ViewFilterCondition:
    que_id: int | None
    que_title: str
    que_type: int | None
    judge_type: int | None
    judge_values: list[str]
    judge_value_details: list[JSONObject]
    raw: JSONObject


@dataclass(slots=True)
class ViewSelection:
    view_key: str
    view_name: str
    conditions: list[list[ViewFilterCondition]]
    view_type: str | None = None
    filter_config_loaded: bool = False


@dataclass(slots=True)
class AccessibleViewRoute:
    view_id: str
    name: str
    kind: str
    list_type: int | None
    view_selection: ViewSelection | None
    view_type: str | None = None


def _prefer_custom_update_routes(routes: list[AccessibleViewRoute]) -> list[AccessibleViewRoute]:
    return [
        *[route for route in routes if route.kind == "custom"],
        *[route for route in routes if route.kind != "custom"],
    ]


@dataclass(slots=True)
class RecordContextRouteProbe:
    route: AccessibleViewRoute
    answer_list: list[JSONObject] | None
    used_list_type: int | None
    readable: bool
    transport_error: bool
    error_payload: JSONObject | None


@dataclass(slots=True)
class WorkflowNodeRef:
    workflow_node_id: int
    name: str
    type: str
    raw: JSONObject


@dataclass(slots=True)
class RecordInputError(Exception):
    message: str
    error_code: str
    fix_hint: str
    details: JSONObject | None = None

    def __post_init__(self) -> None:
        Exception.__init__(self, self.message)

    def to_dict(self) -> JSONObject:
        payload: JSONObject = {
            "category": "config",
            "message": self.message,
            "error_code": self.error_code,
            "fix_hint": self.fix_hint,
        }
        if self.details is not None:
            payload["details"] = self.details
        return payload

    def as_json(self) -> str:
        return json.dumps(self.to_dict(), ensure_ascii=False)

    def __str__(self) -> str:
        return self.as_json()


GENERIC_FIELD_PREFIX_ALIASES = (
    "当前",
    "预计",
    "最近",
    "最新",
)
GENERIC_FIELD_ALIAS_OVERRIDES: dict[str, list[str]] = {
    "申请时间": ["新增时间", "创建时间"],
    "更新时间": ["修改时间", "最后更新时间"],
}
FIELD_LOOKUP_STRIP_RE = re.compile(r"[\s_()（）\[\]【】{}<>·/\\:：-]+")


def _pick_route_payload(payload: JSONObject) -> JSONObject:
    return {
        key: payload[key]
        for key in (
            "route_type",
            "endpoint_kind",
            "status",
            "role",
            "task_id",
            "workflow_node_id",
            "view_id",
            "view_key",
            "view_name",
            "error_code",
            "backend_code",
            "http_status",
            "request_id",
            "message",
            "reason",
        )
        if key in payload and payload[key] not in (None, "", [], {})
    }


class RecordTools(ToolBase):
    """记录工具（中文名：记录读写与分析）。

    类型：业务数据操作工具。
    主要职责：
    1. 提供记录 Schema、记录查询、记录 CRUD；
    2. 提供成员/部门候选与代码块相关能力；
    3. 提供面向分析场景的数据读取与字段归一化能力。
    """

    def __init__(self, sessions, backend) -> None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        super().__init__(sessions, backend)
        self._form_cache: dict[tuple[str, str, str, int | None], JSONObject] = {}
        self._applicant_node_cache: dict[tuple[str, str], WorkflowNodeRef] = {}
        self._view_list_cache: dict[tuple[str, str], list[JSONObject]] = {}
        self._view_config_cache: dict[tuple[str, str], JSONObject | None] = {}
        self._app_name_cache: dict[tuple[str, int | None, str], str | None] = {}
        self._relation_base_info_cache: dict[tuple[str, int], JSONObject] = {}

    def register(self, mcp: FastMCP) -> None:
        """注册当前工具到 MCP 服务。"""
        @mcp.tool()
        def record_insert_schema_get(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_insert_schema_get_public(
                profile=profile,
                app_key=app_key,
                output_profile=output_profile,
            )

        @mcp.tool(
            description=(
                "List current-user candidate members for a member field. "
                "When record_id/workflow_node_id/fields are provided, this tool uses the backend runtime candidate scope that matches writes. "
                "Without runtime context it only returns a static applicant-node preview scope. "
                "Use record_insert_schema_get or record_update_schema_get with output_profile='verbose' first, then pass the member field_id."
            )
        )
        def record_member_candidates(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            field_id: int = 0,
            record_id: str | None = None,
            workflow_node_id: int | None = None,
            fields: JSONObject | None = None,
            keyword: str = "",
            page_num: int = 1,
            page_size: int = 20,
        ) -> JSONObject:
            return self.record_member_candidates(
                profile=profile,
                app_key=app_key,
                field_id=field_id,
                record_id=record_id,
                workflow_node_id=workflow_node_id,
                fields=fields or {},
                keyword=keyword,
                page_num=page_num,
                page_size=page_size,
            )

        @mcp.tool(
            description=(
                "List current-user candidate departments for a department field. "
                "When record_id/workflow_node_id/fields are provided, this tool uses the backend runtime candidate scope that matches writes. "
                "Without runtime context it only returns a static applicant-node preview scope. "
                "Use record_insert_schema_get or record_update_schema_get with output_profile='verbose' first, then pass the department field_id."
            )
        )
        def record_department_candidates(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            field_id: int = 0,
            record_id: str | None = None,
            workflow_node_id: int | None = None,
            fields: JSONObject | None = None,
            keyword: str = "",
            page_num: int = 1,
            page_size: int = 20,
        ) -> JSONObject:
            return self.record_department_candidates(
                profile=profile,
                app_key=app_key,
                field_id=field_id,
                record_id=record_id,
                workflow_node_id=workflow_node_id,
                fields=fields or {},
                keyword=keyword,
                page_num=page_num,
                page_size=page_size,
            )

        @mcp.tool(
            description=(
                "Browse Qingflow records with a schema-first list DSL. "
                "Use record_browse_schema_get first, then pass field_id-only columns, where, and order_by clauses. "
                "This route returns up to 10 rows plus total counts for browse, sample inspection, and fuzzy record lookup; it is not for analysis."
            )
        )
        def record_list(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            columns: list[JSONObject | int] | None = None,
            query: str | None = None,
            query_fields: list[JSONObject | int] | None = None,
            where: list[JSONObject] | None = None,
            order_by: list[JSONObject] | None = None,
            page: int = 1,
            page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
            view_id: str | None = None,
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_list(
                profile=profile,
                app_key=app_key,
                columns=columns or [],
                query=query,
                query_fields=query_fields or [],
                where=where or [],
                order_by=order_by or [],
                page=page,
                page_size=page_size,
                view_id=view_id,
                list_type=None,
                view_key=None,
                view_name=None,
                output_profile=output_profile,
            )

        @mcp.tool(
            description=(
                "Access Qingflow records for analysis by writing local CSV shard files. "
                "Use app_get -> record_browse_schema_get first, then pass field_id-only columns, where, and order_by. "
                "This tool hides pagination and row limits from the caller and returns file metadata instead of record items."
            )
        )
        def record_access(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            view_id: str = "",
            columns: list[JSONObject | int] | None = None,
            where: list[JSONObject] | None = None,
            order_by: list[JSONObject] | None = None,
        ) -> JSONObject:
            return self.record_access(
                profile=profile,
                app_key=app_key,
                view_id=view_id,
                columns=columns or [],
                where=where or [],
                order_by=order_by or [],
            )

        @mcp.tool(description="Read one Qingflow record as frontend detail-page context, including visible fields, references, first-page logs, associated resources, and semantic_context.")
        def record_get(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            record_id: str = "",
            columns: list[JSONObject | int] | None = None,
            view_id: str | None = None,
            output_profile: str = "detail_context",
        ) -> JSONObject:
            return self.record_get_public(
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                columns=columns or [],
                view_id=view_id,
                workflow_node_id=None,
                output_profile=output_profile,
            )

        @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.")
        def record_logs_get(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            record_id: str = "",
            view_id: str | None = None,
        ) -> JSONObject:
            return self.record_logs_get(
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                view_id=view_id,
            )

        @mcp.tool()
        def record_browse_schema_get(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            view_id: str = "",
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_browse_schema_get_public(
                profile=profile,
                app_key=app_key,
                view_id=view_id,
                output_profile=output_profile,
            )

        @mcp.tool()
        def record_update_schema_get(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            record_id: str = "",
            view_id: str | None = None,
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_update_schema_get_public(
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                view_id=view_id,
                output_profile=output_profile,
            )

        @mcp.tool(
            description=(
                "Insert Qingflow records using applicant-node field maps. "
                "Use record_insert_schema_get first. "
                "Prefer items=[{'fields': {...}}]; a single insert is one item. "
                "Each item performs internal preflight validation before that item is written."
            )
        )
        def record_insert(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            items: list[JSONObject] | None = None,
            verify_write: bool = True,
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_insert_public(
                profile=profile,
                app_key=app_key,
                items=items,
                verify_write=verify_write,
                output_profile=output_profile,
            )

        @mcp.tool(
            description=(
                "Update one Qingflow record using a field map. "
                "For simple field changes, call this tool directly after the target record is clear. "
                "Pass view_id when the frontend detail view is known; the tool will try that view first. "
                "It first tries the data-manager direct route, then falls back to the frontend custom-view edit route when available. "
                "Use record_update_schema_get for diagnostics or complex field-scope inspection."
            )
        )
        def record_update(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            record_id: str | None = None,
            fields: JSONObject | None = None,
            items: list[JSONObject] | None = None,
            view_id: str | None = None,
            dry_run: bool = False,
            verify_write: bool = True,
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_update_public(
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                fields=fields,
                items=items,
                view_id=view_id,
                dry_run=dry_run,
                verify_write=verify_write,
                output_profile=output_profile,
            )

        @mcp.tool(
            description=(
                "Delete Qingflow records by record_id or record_ids. "
                "Pass view_id when deleting from a known system list route; custom view deletion is not supported by this backend route."
            )
        )
        def record_delete(
            profile: str = DEFAULT_PROFILE,
            app_key: str = "",
            record_id: str | None = None,
            record_ids: list[str] | None = None,
            view_id: str | None = None,
            output_profile: str = "normal",
        ) -> JSONObject:
            return self.record_delete_public(
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                record_ids=record_ids or [],
                view_id=view_id,
                output_profile=output_profile,
            )

    def record_schema_get(
        self,
        *,
        profile: str,
        app_key: str,
        schema_mode: str = "applicant",
        view_id: str | None = None,
        list_type: int | None = None,
        view_key: str | None = None,
        view_name: str | None = None,
        output_profile: str = "detail_context",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        normalized_schema_mode = schema_mode.strip().lower()
        if normalized_schema_mode not in {"applicant", "browse"}:
            raise_tool_error(QingflowApiError.config_error("schema_mode must be applicant or browse"))

        def runner(session_profile, context):
            warnings: list[JSONObject] = []
            resolved_view: AccessibleViewRoute | None = None
            browse_scope: JSONObject | None = None
            if normalized_schema_mode == "applicant":
                if any(item is not None for item in (view_id, list_type, view_key, view_name)):
                    raise_tool_error(
                        QingflowApiError.config_error(
                            "schema_mode='applicant' does not accept view selectors; use schema_mode='browse' with view_id instead"
                        )
                    )
            else:
                resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
                    profile,
                    context,
                    app_key,
                    view_id=view_id,
                    list_type=list_type,
                    view_key=view_key,
                    view_name=view_name,
                    allow_default=False,
                )
                warnings.extend(compatibility_warnings)
                if resolved_view is not None and not _view_type_supports_analysis(resolved_view.view_type):
                    warnings.append(
                        {
                            "code": "VIEW_NOT_ANALYZABLE",
                            "message": (
                                f"View '{resolved_view.name}' uses {resolved_view.view_type}; "
                                "record_access and record_list do not support boardView or ganttView."
                            ),
                        }
                    )
                browse_scope = self._build_browse_read_scope(
                    profile,
                    context,
                    app_key,
                    resolved_view,
                    force_refresh=False,
                )
            index = (
                self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=False)
                if normalized_schema_mode == "applicant"
                else cast(FieldIndex, browse_scope["index"])
            )
            browse_writable_field_ids = cast(set[int], browse_scope["writable_field_ids"]) if isinstance(browse_scope, dict) else set()
            fields = [
                self._schema_field_payload(
                    profile,
                    context,
                    field,
                    workflow_node_id=None,
                    ws_id=session_profile.selected_ws_id,
                    schema_mode=normalized_schema_mode,
                    browse_writable=field.que_id in browse_writable_field_ids if normalized_schema_mode == "browse" else None,
                )
                for field in self._schema_fields_for_mode(
                    profile,
                    context,
                    app_key,
                    index,
                    schema_mode=normalized_schema_mode,
                    resolved_view=resolved_view,
                )
            ]
            suggested_dimensions = [
                {"field_id": item["field_id"], "title": item["title"]}
                for item in fields
                if bool(cast(JSONObject, item["role_hints"]).get("dimension_candidate"))
            ]
            suggested_metrics = [
                {"field_id": item["field_id"], "title": item["title"]}
                for item in fields
                if bool(cast(JSONObject, item["role_hints"]).get("metric_candidate"))
            ]
            suggested_time_fields = [
                {"field_id": item["field_id"], "title": item["title"]}
                for item in fields
                if bool(cast(JSONObject, item["role_hints"]).get("time_candidate"))
            ]
            response: JSONObject = {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "status": "success",
                "request_route": self._request_route_payload(context),
                "warnings": warnings,
                "app_key": app_key,
                "schema_scope": "applicant_node" if normalized_schema_mode == "applicant" else "browse_view",
                "workflow_node": {
                    "workflow_node_id": None,
                    "name": None,
                    "type": "applicant",
                },
                "view_resolution": _accessible_view_payload(resolved_view),
                "fields": fields,
                "suggested_dimensions": suggested_dimensions,
                "suggested_metrics": suggested_metrics,
                "suggested_time_fields": suggested_time_fields,
            }
            if output_profile == "verbose":
                response["field_count"] = len(fields)
            return response

        return self._run_record_tool(profile, runner, tool_name="记录 Schema")

    def record_insert_schema_get_public(
        self,
        *,
        profile: str = DEFAULT_PROFILE,
        app_key: str,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        normalized_output_profile = self._normalize_public_output_profile(output_profile)

        def runner(session_profile, context):
            self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
            schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
            question_relations = _collect_question_relations(schema)
            index = _build_applicant_top_level_field_index(schema)
            runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
            runtime_linked_field_ids.update(_collect_option_linked_field_ids(index))
            linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
                schema,
                linked_field_ids=runtime_linked_field_ids,
            )
            index = _merge_field_indexes(index, linked_hidden_index)
            activation_sources_by_field_id = _collect_linked_activation_sources(question_relations, index)
            linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
                index=index,
                question_relations=question_relations,
            )
            ws_id = session_profile.selected_ws_id
            schema_fields = self._schema_fields_for_mode(
                profile,
                context,
                app_key,
                index,
                schema_mode="applicant",
                resolved_view=None,
            )
            fields: list[JSONObject] = []
            for field in schema_fields:
                write_hints = self._schema_write_hints(field)
                if not bool(write_hints.get("writable")):
                    continue
                field_id = str(field.que_id)
                is_linked_writable_target = (
                    not bool(field.system)
                    and field_id in runtime_linked_field_ids
                )
                payload = self._ready_schema_field_payload(
                    profile,
                    context,
                    field,
                    ws_id=ws_id,
                    required_override=False if is_linked_writable_target else None,
                    linkage_payloads_by_field_id=linkage_payloads_by_field_id,
                )
                if is_linked_writable_target:
                    payload["may_become_required"] = True
                    payload["requirement_reason"] = LINKED_OPTIONAL_REQUIREMENT_REASON
                    activation_sources = activation_sources_by_field_id.get(field_id, [])
                    if activation_sources:
                        payload["activation_sources"] = activation_sources
                fields.append(payload)
            required_fields = [item for item in fields if bool(item.get("required"))]
            optional_fields = [item for item in fields if not bool(item.get("required"))]
            runtime_linked_required_fields: list[JSONObject] = []
            for field in schema_fields:
                if not self._should_surface_runtime_linked_required_field(
                    field,
                    runtime_linked_field_ids=runtime_linked_field_ids,
                ):
                    continue
                payload = self._ready_schema_runtime_linked_field_payload(
                    profile,
                    context,
                    field,
                    ws_id=ws_id,
                    reason=RUNTIME_LINKED_REQUIRED_REASON,
                    linkage_payloads_by_field_id=linkage_payloads_by_field_id,
                )
                activation_sources = activation_sources_by_field_id.get(str(field.que_id), [])
                if activation_sources:
                    payload["activation_sources"] = activation_sources
                runtime_linked_required_fields.append(payload)
            payload_template_fields = list(required_fields)
            payload_template_fields.extend(
                item for item in optional_fields if bool(item.get("may_become_required"))
            )
            response: JSONObject = {
                "profile": profile,
                "ws_id": ws_id,
                "ok": True,
                "status": "success",
                "request_route": self._request_route_payload(context),
                "warnings": [],
                "app_key": app_key,
                "schema_scope": "insert_ready",
                "required_fields": required_fields,
                "optional_fields": optional_fields,
                "runtime_linked_required_fields": runtime_linked_required_fields,
                "payload_template": {
                    item["title"]: self._ready_schema_template_value(item)
                    for item in payload_template_fields
                },
            }
            if normalized_output_profile == "verbose":
                response["legacy_schema"] = self.record_schema_get(
                    profile=profile,
                    app_key=app_key,
                    schema_mode="applicant",
                    output_profile="verbose",
                )
            return response

        return self._run_record_tool(profile, runner, tool_name="新增记录 Schema")

    def record_browse_schema_get_public(
        self,
        *,
        profile: str = DEFAULT_PROFILE,
        app_key: str,
        view_id: str,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        return self.record_schema_get(
            profile=profile,
            app_key=app_key,
            schema_mode="browse",
            view_id=view_id,
            output_profile=output_profile,
        )

    def record_update_schema_get_public(
        self,
        *,
        profile: str = DEFAULT_PROFILE,
        app_key: str,
        record_id: Any,
        view_id: str | None = None,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
        normalized_output_profile = self._normalize_public_output_profile(output_profile)

        def runner(session_profile, context):
            request_route = self._request_route_payload(context)
            self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
            linkage_payloads_by_field_id: dict[str, JSONObject] = {}
            try:
                app_schema = self._get_form_schema(profile, context, app_key, force_refresh=True)
            except QingflowApiError as exc:
                if not _is_optional_schema_permission_error(exc):
                    raise
            else:
                question_relations = _collect_question_relations(app_schema)
                app_index = _build_applicant_top_level_field_index(app_schema)
                linked_field_ids = _collect_linked_required_field_ids(question_relations)
                linked_field_ids.update(_collect_option_linked_field_ids(app_index))
                linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
                    app_schema,
                    linked_field_ids=linked_field_ids,
                )
                app_index = _merge_field_indexes(app_index, linked_hidden_index)
                linkage_payloads_by_field_id = _build_static_schema_linkage_payloads(
                    index=app_index,
                    question_relations=question_relations,
                )
            preferred_view_id = _normalize_optional_text(view_id)
            candidate_routes = self._candidate_update_views(profile, context, app_key)
            if preferred_view_id:
                preferred_route = next(
                    (
                        route
                        for route in candidate_routes
                        if route.view_id == preferred_view_id
                    ),
                    None,
                )
                if preferred_route is None:
                    raise_tool_error(
                        QingflowApiError.config_error(
                            f"view_id '{preferred_view_id}' is not an accessible update candidate"
                        )
                    )
                candidate_routes = [preferred_route]
            probes = self._probe_candidate_record_contexts(
                context,
                app_key=app_key,
                apply_id=record_id_int,
                candidate_routes=candidate_routes,
            )
            probe_summary: list[JSONObject] = []
            matched_probes: list[RecordContextRouteProbe] = []
            ordered_field_ids: list[int] = []
            field_payloads_by_id: dict[int, JSONObject] = {}
            title_to_field_ids: dict[str, list[int]] = {}
            ws_id = session_profile.selected_ws_id

            for probe in probes:
                candidate = probe.route
                candidate_summary = self._record_context_probe_summary_payload(probe)
                candidate_summary["writable_field_titles"] = []
                if not probe.readable:
                    probe_summary.append(candidate_summary)
                    continue

                matched_probes.append(probe)
                browse_scope = self._build_browse_write_scope(
                    profile,
                    context,
                    app_key,
                    candidate,
                    force_refresh=True,
                )
                index = cast(FieldIndex, browse_scope["index"])
                browse_writable_field_ids = cast(set[int], browse_scope["writable_field_ids"])
                candidate_fields = [
                    field
                    for field in self._schema_fields_for_mode(
                        profile,
                        context,
                        app_key,
                        index,
                        schema_mode="browse",
                        resolved_view=candidate,
                    )
                    if field.que_id in browse_writable_field_ids
                ]
                candidate_titles: list[str] = []
                for field in candidate_fields:
                    payload = self._ready_schema_field_payload(
                        profile,
                        context,
                        field,
                        ws_id=ws_id,
                        required_override=False,
                        linkage_payloads_by_field_id=linkage_payloads_by_field_id,
                    )
                    title = _normalize_optional_text(payload.get("title"))
                    if title:
                        candidate_titles.append(title)
                        ids = title_to_field_ids.setdefault(title, [])
                        if field.que_id not in ids:
                            ids.append(field.que_id)
                    if field.que_id not in field_payloads_by_id:
                        field_payloads_by_id[field.que_id] = payload
                        ordered_field_ids.append(field.que_id)
                candidate_summary["writable_field_titles"] = candidate_titles
                probe_summary.append(candidate_summary)

            if not matched_probes:
                blockers = (
                    ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
                    if probes and all(probe.transport_error for probe in probes)
                    else ["NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"]
                )
                warnings = (
                    [
                        "update schema could not load the current record from any candidate route; context-sensitive lookup requirements cannot be derived safely."
                    ]
                    if blockers == ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
                    else []
                )
                recommended_next_actions = (
                    [
                        "Retry after the record becomes readable in the current workspace/profile context.",
                        "If the issue persists, verify that the current profile still has read access to this record.",
                    ]
                    if blockers == ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
                    else [
                        "Check whether this record is still visible in any accessible view for the current profile.",
                        "Use record_get or record_list to confirm the record still exists in the current workspace.",
                    ]
                )
                return self._record_update_schema_blocked_response(
                    profile=profile,
                    ws_id=ws_id,
                    request_route=request_route,
                    app_key=app_key,
                    record_id=record_id_int,
                    blockers=blockers,
                    warnings=warnings,
                    recommended_next_actions=recommended_next_actions,
                    output_profile=normalized_output_profile,
                    view_probe_summary=probe_summary,
                    ambiguous_fields=[],
                    preferred_view_id=preferred_view_id,
                )

            ambiguous_field_ids: set[int] = set()
            ambiguous_fields: list[JSONObject] = []
            warnings: list[JSONObject] = [
                {"code": "RECORD_CONTEXT_ROUTE_FALLBACK", "message": message}
                for message in self._record_context_probe_fallback_warnings(probes)
            ]
            for title, field_ids in title_to_field_ids.items():
                unique_ids = list(dict.fromkeys(field_ids))
                if len(unique_ids) <= 1:
                    continue
                ambiguous_field_ids.update(unique_ids)
                warnings.append(
                    {
                        "code": "AMBIGUOUS_UPDATE_FIELD_TITLE",
                        "message": f"field title '{title}' maps to multiple writable fields across matched views; it was excluded from writable_fields",
                    }
                )

            writable_fields: list[JSONObject] = []
            for field_id in ordered_field_ids:
                payload = field_payloads_by_id[field_id]
                if field_id in ambiguous_field_ids:
                    ambiguous_payload = dict(payload)
                    ambiguous_payload["field_id"] = field_id
                    ambiguous_fields.append(ambiguous_payload)
                    continue
                writable_fields.append(payload)

            if not writable_fields:
                return self._record_update_schema_blocked_response(
                    profile=profile,
                    ws_id=ws_id,
                    request_route=request_route,
                    app_key=app_key,
                    record_id=record_id_int,
                    blockers=["NO_WRITABLE_FIELDS_FOR_RECORD"],
                    warnings=[item["message"] for item in warnings if isinstance(item.get("message"), str)],
                    recommended_next_actions=[
                        "Retry with output_profile='verbose' to inspect ambiguous_fields and matched view coverage.",
                        "Reduce the update scope to fields that can be addressed unambiguously by title.",
                    ],
                    output_profile=normalized_output_profile,
                    view_probe_summary=probe_summary,
                    ambiguous_fields=ambiguous_fields,
                    preferred_view_id=preferred_view_id,
                )

            response: JSONObject = {
                "profile": profile,
                "ws_id": ws_id,
                "ok": True,
                "status": "success",
                "request_route": request_route,
                "warnings": warnings,
                "app_key": app_key,
                "record_id": stringify_backend_id(record_id_int),
                "schema_scope": "update_ready",
                "writable_fields": writable_fields,
                "payload_template": {
                    item["title"]: self._ready_schema_template_value(item)
                    for item in writable_fields
                },
                "available_update_routes": self._record_update_schema_available_routes(matched_probes),
                "recommended_update_route": {
                    "route_type": "auto",
                    "order": ["admin_direct", "view_edit", "task_save_only"],
                    "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.",
                },
            }
            if preferred_view_id:
                response["preferred_view_id"] = preferred_view_id
            if normalized_output_profile == "verbose":
                response["view_probe_summary"] = probe_summary
                response["record_context_probe"] = probe_summary
                response["ambiguous_fields"] = ambiguous_fields
                response["route_probe_summary"] = probe_summary
            return response

        return self._run_record_tool(profile, runner, tool_name="更新记录 Schema")

    def _record_update_schema_available_routes(self, matched_probes: list[RecordContextRouteProbe]) -> list[JSONObject]:
        routes: list[JSONObject] = [
            {
                "route_type": "admin_direct",
                "endpoint_kind": "app_apply_update",
                "role": 1,
                "availability": "attempted_on_update",
                "message": "Requires data-manager edit permission; record_update safely falls back if backend returns permission denied.",
            }
        ]
        for probe in matched_probes:
            if probe.route.kind != "custom":
                continue
            view_key = self._route_view_key(probe.route)
            if not view_key:
                continue
            routes.append(
                {
                    "route_type": "view_edit",
                    "endpoint_kind": "view_apply_update",
                    "view_id": probe.route.view_id,
                    "view_key": view_key,
                    "view_name": probe.route.name,
                    "availability": "candidate",
                    "message": "Uses the same custom-view detail edit route as the frontend.",
                }
            )
        routes.append(
            {
                "route_type": "task_save_only",
                "endpoint_kind": "workflow_node_save_only",
                "availability": "auto_probe_on_update",
                "message": "record_update probes the current user's todo list and uses save-only only when exactly one matching task exists and the requested fields are editable on that workflow node.",
            }
        )
        return routes

    def _record_update_schema_blocked_response(
        self,
        *,
        profile: str,
        ws_id: int | None,
        request_route: JSONObject,
        app_key: str,
        record_id: int,
        blockers: list[str],
        warnings: list[str],
        recommended_next_actions: list[str],
        output_profile: str,
        view_probe_summary: list[JSONObject],
        ambiguous_fields: list[JSONObject],
        preferred_view_id: str | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        response: JSONObject = {
            "profile": profile,
            "ws_id": ws_id,
            "ok": False,
            "status": "blocked",
            "request_route": request_route,
            "warnings": [{"code": "PREFLIGHT_WARNING", "message": item} for item in warnings],
            "app_key": app_key,
            "record_id": stringify_backend_id(record_id),
            "schema_scope": "update_ready",
            "blockers": blockers,
            "writable_fields": [],
            "payload_template": {},
            "recommended_next_actions": recommended_next_actions,
        }
        if preferred_view_id:
            response["preferred_view_id"] = preferred_view_id
        if output_profile == "verbose":
            response["view_probe_summary"] = view_probe_summary
            response["ambiguous_fields"] = ambiguous_fields
        return response

    def _ready_schema_field_payload(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        *,
        ws_id: int | None,
        required_override: bool | None,
        linkage_payloads_by_field_id: dict[str, JSONObject] | None = None,
        include_field_id: bool = True,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        kind = self._ready_schema_kind(field)
        row_fields: list[JSONObject] = []
        if kind == "subtable":
            row_fields = self._ready_schema_subtable_row_fields(
                profile,
                context,
                field,
                ws_id=ws_id,
                linkage_payloads_by_field_id=linkage_payloads_by_field_id,
            )
            required = bool(required_override) if required_override is not None else bool(field.required or any(item.get("required") for item in row_fields))
        else:
            required = bool(required_override) if required_override is not None else bool(field.required)
        write_format = _write_format_for_field(field)
        payload: JSONObject = {
            "title": field.que_title,
            "kind": kind,
            "required": required,
            "format_hint": _ready_schema_format_hint(kind, write_format),
        }
        example_value = _ready_schema_example_value(kind, field, write_format, row_fields=row_fields)
        if example_value is not None:
            payload["example_value"] = example_value
        if include_field_id:
            payload["field_id"] = field.que_id
        if kind in {"single_select", "multi_select"} and field.options:
            payload["options"] = list(field.options)
        if kind in {"member", "department", "relation"}:
            payload["accepts_natural_input"] = True
        if kind == "department":
            payload["candidate_hint"] = {
                "tool": "record_department_candidates",
                "field_id": field.que_id,
                "scope_source": "static_applicant_scope",
                "preview_only": True,
                "runtime_context_supported": True,
            }
        if kind == "attachment":
            payload["requires_upload"] = True
        if kind == "relation" and field.target_app_key:
            payload["target_app_key"] = field.target_app_key
            target_app_name = field.target_app_name_hint or self._resolve_visible_app_name(
                profile,
                context,
                target_app_key=field.target_app_key,
                ws_id=ws_id,
            )
            if target_app_name:
                payload["target_app_name"] = target_app_name
            searchable_fields = _relation_searchable_fields_for_question(field.raw)
            if searchable_fields:
                payload["searchable_fields"] = searchable_fields
        if kind == "subtable":
            payload["min_rows"] = 1 if required else 0
            payload["row_fields"] = row_fields
        self._attach_ready_schema_linkage(
            payload,
            field_id=str(field.que_id),
            linkage_payloads_by_field_id=linkage_payloads_by_field_id,
        )
        return payload

    def _ready_schema_runtime_linked_field_payload(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        *,
        ws_id: int | None,
        reason: str,
        linkage_payloads_by_field_id: dict[str, JSONObject] | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        payload = self._ready_schema_field_payload(
            profile,
            context,
            field,
            ws_id=ws_id,
            required_override=True,
            linkage_payloads_by_field_id=linkage_payloads_by_field_id,
            include_field_id=False,
        )
        payload["reason"] = reason
        self._attach_ready_schema_linkage(
            payload,
            field_id=str(field.que_id),
            linkage_payloads_by_field_id=linkage_payloads_by_field_id,
            role_override=SCHEMA_LINKAGE_ROLE_AUTO_FILL_ONLY,
        )
        return payload

    def _attach_ready_schema_linkage(
        self,
        payload: JSONObject,
        *,
        field_id: str,
        linkage_payloads_by_field_id: dict[str, JSONObject] | None,
        role_override: str | None = None,
    ) -> None:
        """执行内部辅助逻辑。"""
        if not linkage_payloads_by_field_id:
            return
        linkage_payload = linkage_payloads_by_field_id.get(field_id)
        if not isinstance(linkage_payload, dict):
            return
        linkage = deepcopy(linkage_payload)
        if role_override:
            linkage["role"] = role_override
        payload["linkage"] = linkage

    def _should_surface_runtime_linked_required_field(
        self,
        field: FormField,
        *,
        runtime_linked_field_ids: set[str],
    ) -> bool:
        """执行内部辅助逻辑。"""
        if not bool(field.required) or bool(field.system) or not bool(field.readonly):
            return False
        if str(field.que_id) not in runtime_linked_field_ids:
            return False
        write_hints = self._schema_write_hints(field)
        if bool(write_hints.get("writable")):
            return False
        if _normalize_optional_text(write_hints.get("unsupported_reason")):
            return False
        write_kind = _normalize_optional_text(write_hints.get("write_kind")) or "scalar"
        return write_kind not in {"member", "department", "relation", "attachment", "unsupported"}

    def _ready_schema_subtable_row_fields(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        *,
        ws_id: int | None,
        linkage_payloads_by_field_id: dict[str, JSONObject] | None = None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        raw = field.raw if isinstance(field.raw, dict) else {}
        columns_source = raw.get("subQuestions")
        if isinstance(columns_source, list):
            flattened = _flatten_questions(columns_source)
        else:
            inner_questions = raw.get("innerQuestions")
            flattened = _flatten_questions(inner_questions) if isinstance(inner_questions, list) else []
        payloads: list[JSONObject] = []
        for question in flattened:
            subfield = _question_to_form_field(question, is_base_question=False)
            if subfield is None:
                continue
            if not bool(self._schema_write_hints(subfield).get("writable")):
                continue
            payloads.append(
                self._ready_schema_field_payload(
                    profile,
                    context,
                    subfield,
                    ws_id=ws_id,
                    required_override=bool(subfield.required),
                    linkage_payloads_by_field_id=linkage_payloads_by_field_id,
                    include_field_id=False,
                )
            )
        return payloads

    def _ready_schema_kind(self, field: FormField) -> str:
        """执行内部辅助逻辑。"""
        write_format = _write_format_for_field(field)
        write_kind = _normalize_optional_text(write_format.get("kind")) or "scalar_text"
        if write_kind == "subtable_rows":
            return "subtable"
        if write_kind == "member_list":
            return "member"
        if write_kind == "department_list":
            return "department"
        if write_kind == "relation_record":
            return "relation"
        if write_kind == "attachment_list":
            return "attachment"
        if write_kind == "address_parts":
            return "address"
        if write_kind == "single_select":
            return "single_select"
        if write_kind == "multi_select":
            return "multi_select"
        if write_kind == "boolean_label":
            return "boolean"
        if write_kind == "date_string":
            return "date"
        field_family = self._schema_field_family(field)
        if field_family == "number":
            return "number"
        if field_family == "date":
            return "date"
        if field_family == "boolean":
            return "boolean"
        return "text"

    def _ready_schema_template_value(self, field_payload: JSONObject) -> JSONValue:
        """执行内部辅助逻辑。"""
        kind = _normalize_optional_text(field_payload.get("kind")) or "text"
        if kind == "number":
            return "<number>"
        if kind == "date":
            return "<date>"
        if kind == "boolean":
            return "<boolean>"
        if kind == "single_select":
            options = field_payload.get("options")
            if isinstance(options, list) and options:
                return _stringify_json(options[0])
            return "<option>"
        if kind == "multi_select":
            options = field_payload.get("options")
            if isinstance(options, list) and options:
                return [_stringify_json(options[0])]
            return ["<option>"]
        if kind == "member":
            return "<member>"
        if kind == "department":
            return "<department>"
        if kind == "relation":
            return "<relation>"
        if kind == "attachment":
            return [{"value": "<uploaded_file_url>", "name": "<filename>"}]
        if kind == "address":
            return {
                "province": "<text>",
                "city": "<text>",
                "district": "<text>",
                "detail": "<text>",
            }
        if kind == "subtable":
            row_fields = [item for item in cast(list[JSONValue], field_payload.get("row_fields") or []) if isinstance(item, dict)]
            required_row_fields = [item for item in row_fields if bool(item.get("required"))]
            template_fields = required_row_fields or row_fields
            return [
                {
                    str(item["title"]): self._ready_schema_template_value(cast(JSONObject, item))
                    for item in template_fields
                    if isinstance(item.get("title"), str)
                }
            ]
        return "<text>"

    def record_member_candidates(
        self,
        *,
        profile: str,
        app_key: str,
        field_id: int,
        record_id: Any | None = None,
        workflow_node_id: int | None = None,
        fields: JSONObject | None = None,
        keyword: str,
        page_num: int,
        page_size: int,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if field_id <= 0:
            raise_tool_error(QingflowApiError.config_error("field_id must be positive"))
        if page_num <= 0:
            raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
        if page_size <= 0:
            raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
        record_id_int = (
            normalize_positive_id_int(record_id, field_name="record_id")
            if record_id is not None
            else None
        )
        record_id_text = stringify_backend_id(record_id_int) if record_id_int is not None else None

        def runner(session_profile, context):
            index = self._get_field_index(profile, context, app_key, force_refresh=False)
            field = self._resolve_field_selector(field_id, index, location="field_id")
            if field.que_type not in MEMBER_QUE_TYPES:
                raise_tool_error(
                    QingflowApiError(
                        category="config",
                        message=f"field_id {field_id} is not a member field in the applicant-aware schema",
                        details={
                            "error_code": "FIELD_NOT_MEMBER",
                            "field_id": field.que_id,
                            "field_title": field.que_title,
                            "que_type": field.que_type,
                        },
                    )
                )
            normalized_fields = fields if isinstance(fields, dict) else {}
            runtime_lookup = self._candidate_lookup_uses_runtime_scope(
                record_id=record_id_int,
                workflow_node_id=workflow_node_id,
                fields=normalized_fields,
            )
            warnings: list[JSONObject] = []
            scope_source = "static_applicant_scope"
            try:
                if runtime_lookup:
                    state = self._build_candidate_lookup_state(
                        profile,
                        context,
                        app_key=app_key,
                        record_id=record_id_int,
                        workflow_node_id=workflow_node_id,
                        fields=normalized_fields,
                    )
                    items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
                    scope_source = "backend_runtime_scope"
                else:
                    items: list[JSONObject] | None = None
                    if self._member_candidate_static_preview_should_use_backend(field):
                        state = self._build_candidate_lookup_state(
                            profile,
                            context,
                            app_key=app_key,
                            record_id=None,
                            workflow_node_id=None,
                            fields={},
                        )
                        items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
                        scope_source = "backend_applicant_scope"
                    if items is None:
                        items = self._resolve_member_candidates(context, field, keyword=keyword)
                    warnings.append(
                        {
                            "code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
                            "message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
                        }
                    )
            except (RecordInputError, QingflowApiError) as error:
                record_error = (
                    error
                    if isinstance(error, RecordInputError)
                    else self._candidate_lookup_error(kind="member", field=field, value=keyword, error=error)
                )
                return self._candidate_lookup_failed_response(
                    profile=profile,
                    session_profile=session_profile,
                    context=context,
                    kind="member",
                    error=record_error,
                    field=field,
                    app_key=app_key,
                    record_id_text=record_id_text,
                    workflow_node_id=workflow_node_id,
                    fields_present=bool(normalized_fields),
                    keyword=keyword,
                    scope_source=scope_source,
                )
            total = len(items)
            start = (page_num - 1) * page_size
            end = start + page_size
            page_items = items[start:end]
            page_amount = (total + page_size - 1) // page_size if total else 0
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "request_route": self._request_route_payload(context),
                "warnings": warnings,
                "output_profile": "normal",
                "data": {
                    "items": page_items,
                    "pagination": {
                        "page": page_num,
                        "page_size": page_size,
                        "returned_items": len(page_items),
                        "reported_total": total,
                        "page_amount": page_amount,
                    },
                    "selection": {
                        "app_key": app_key,
                        "field_id": field.que_id,
                        "field_title": field.que_title,
                        "record_id": record_id_text,
                        "workflow_node_id": workflow_node_id,
                        "fields_present": bool(normalized_fields),
                        "keyword": keyword,
                        "permission_scope": "applicant_node",
                    },
                    "scope_source": scope_source,
                },
            }

        return self._run_record_tool(profile, runner, tool_name="成员候选项")

    def record_department_candidates(
        self,
        *,
        profile: str,
        app_key: str,
        field_id: int,
        record_id: Any | None = None,
        workflow_node_id: int | None = None,
        fields: JSONObject | None = None,
        keyword: str,
        page_num: int,
        page_size: int,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if field_id <= 0:
            raise_tool_error(QingflowApiError.config_error("field_id must be positive"))
        if page_num <= 0:
            raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
        if page_size <= 0:
            raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
        record_id_int = (
            normalize_positive_id_int(record_id, field_name="record_id")
            if record_id is not None
            else None
        )
        record_id_text = stringify_backend_id(record_id_int) if record_id_int is not None else None

        def runner(session_profile, context):
            index = self._get_field_index(profile, context, app_key, force_refresh=False)
            field = self._resolve_field_selector(field_id, index, location="field_id")
            if field.que_type not in DEPARTMENT_QUE_TYPES:
                raise_tool_error(
                    QingflowApiError(
                        category="config",
                        message=f"field_id {field_id} is not a department field in the applicant-aware schema",
                        details={
                            "error_code": "FIELD_NOT_DEPARTMENT",
                            "field_id": field.que_id,
                            "field_title": field.que_title,
                            "que_type": field.que_type,
                        },
                    )
                )
            normalized_fields = fields if isinstance(fields, dict) else {}
            runtime_lookup = self._candidate_lookup_uses_runtime_scope(
                record_id=record_id_int,
                workflow_node_id=workflow_node_id,
                fields=normalized_fields,
            )
            warnings: list[JSONObject] = []
            scope_source = "static_applicant_scope"
            try:
                if runtime_lookup:
                    state = self._build_candidate_lookup_state(
                        profile,
                        context,
                        app_key=app_key,
                        record_id=record_id_int,
                        workflow_node_id=workflow_node_id,
                        fields=normalized_fields,
                    )
                    items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
                    scope_source = "backend_runtime_scope"
                else:
                    items: list[JSONObject] | None = None
                    if self._department_candidate_static_preview_should_use_backend(field):
                        state = self._build_candidate_lookup_state(
                            profile,
                            context,
                            app_key=app_key,
                            record_id=None,
                            workflow_node_id=None,
                            fields={},
                        )
                        items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
                        scope_source = "backend_applicant_scope"
                    if items is None:
                        items = self._resolve_department_candidates(context, field, keyword=keyword)
                        scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
                        if (
                            not items
                            and field.dept_select_scope_type == 2
                            and not _scope_has_dynamic_or_external(scope)
                            and not list(scope.get("depart") or [])
                        ):
                            state = self._build_candidate_lookup_state(
                                profile,
                                context,
                                app_key=app_key,
                                record_id=None,
                                workflow_node_id=None,
                                fields={},
                            )
                            items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
                            scope_source = "backend_applicant_scope"
                    warnings.append(
                        {
                            "code": "CANDIDATE_SCOPE_PREVIEW_ONLY",
                            "message": "candidate scope is a static applicant-node preview; pass record_id/workflow_node_id/fields to match runtime write scope",
                        }
                    )
            except (RecordInputError, QingflowApiError) as error:
                record_error = (
                    error
                    if isinstance(error, RecordInputError)
                    else self._candidate_lookup_error(kind="department", field=field, value=keyword, error=error)
                )
                return self._candidate_lookup_failed_response(
                    profile=profile,
                    session_profile=session_profile,
                    context=context,
                    kind="department",
                    error=record_error,
                    field=field,
                    app_key=app_key,
                    record_id_text=record_id_text,
                    workflow_node_id=workflow_node_id,
                    fields_present=bool(normalized_fields),
                    keyword=keyword,
                    scope_source=scope_source,
                )
            total = len(items)
            start = (page_num - 1) * page_size
            end = start + page_size
            page_items = items[start:end]
            page_amount = (total + page_size - 1) // page_size if total else 0
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "request_route": self._request_route_payload(context),
                "warnings": warnings,
                "output_profile": "normal",
                "data": {
                    "items": page_items,
                    "pagination": {
                        "page": page_num,
                        "page_size": page_size,
                        "returned_items": len(page_items),
                        "reported_total": total,
                        "page_amount": page_amount,
                    },
                    "selection": {
                        "app_key": app_key,
                        "field_id": field.que_id,
                        "field_title": field.que_title,
                        "record_id": record_id_text,
                        "workflow_node_id": workflow_node_id,
                        "fields_present": bool(normalized_fields),
                        "keyword": keyword,
                        "permission_scope": "applicant_node",
                    },
                    "scope_source": scope_source,
                },
            }

        return self._run_record_tool(profile, runner, tool_name="部门候选项")

    def record_analyze(
        self,
        *,
        profile: str,
        app_key: str,
        dimensions: list[JSONObject],
        metrics: list[JSONObject],
        filters: list[JSONObject],
        sort: list[JSONObject],
        limit: int,
        strict_full: bool,
        view_id: str | None = None,
        list_type: int | None = None,
        view_key: str | None = None,
        view_name: str | None = None,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if limit <= 0:
            raise_tool_error(QingflowApiError.config_error("limit must be positive"))
        if not (
            _normalize_optional_text(view_id)
            or list_type is not None
            or _normalize_optional_text(view_key)
            or _normalize_optional_text(view_name)
        ):
            raise_tool_error(
                QingflowApiError.config_error(
                    "record_analyze requires view_id. Call app_get first and pass accessible_views[].view_id.",
                    details={
                        "error_code": "RECORD_ANALYZE_VIEW_REQUIRED",
                        "fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_analyze with view_id.",
                    },
                )
            )
        legacy_warnings = _detect_analyze_legacy_warnings(
            dimensions=dimensions,
            metrics=metrics,
            filters=filters,
            sort=sort,
        )

        def runner(session_profile, context):
            resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
                profile,
                context,
                app_key,
                view_id=view_id,
                list_type=list_type,
                view_key=view_key,
                view_name=view_name,
                allow_default=False,
            )
            if not _view_type_supports_analysis(resolved_view.view_type):
                raise_tool_error(
                    QingflowApiError(
                        category="not_supported",
                        message=(
                            f"record_analyze does not support view '{resolved_view.name}' "
                            f"because its type is {resolved_view.view_type}."
                        ),
                        details={
                            "error_code": "VIEW_ANALYZE_UNSUPPORTED",
                            "view_id": resolved_view.view_id,
                            "view_name": resolved_view.name,
                            "view_type": resolved_view.view_type,
                            "fix_hint": "Choose a system view or a custom table-style view from app_get.accessible_views.",
                        },
                    )
                )
            index = self._get_browse_field_index(profile, context, app_key, resolved_view, force_refresh=False)
            compiled_dimensions = self._compile_analyze_dimensions(index, dimensions)
            compiled_metrics = self._compile_analyze_metrics(index, metrics)
            duplicate_aliases = {str(item["alias"]) for item in compiled_dimensions} & {str(item["alias"]) for item in compiled_metrics}
            if duplicate_aliases:
                raise RecordInputError(
                    message=f"dimensions and metrics cannot share aliases: {sorted(duplicate_aliases)}",
                    error_code="DUPLICATE_ANALYZE_ALIAS",
                    fix_hint="Use distinct aliases across dimensions and metrics.",
                )
            compiled_filters = self._compile_analyze_filters(index, filters)
            compiled_sort = self._compile_analyze_sort(sort, compiled_dimensions, compiled_metrics)
            return self._execute_record_analyze(
                profile=profile,
                session_profile=session_profile,
                context=context,
                app_key=app_key,
                index=index,
                resolved_view=resolved_view,
                dimensions=compiled_dimensions,
                metrics=compiled_metrics,
                filters=compiled_filters,
                sort=compiled_sort,
                limit=limit,
                strict_full=strict_full,
                output_profile=output_profile,
                extra_warnings=legacy_warnings + compatibility_warnings,
            )

        return self._run_record_tool(profile, runner, tool_name="记录分析")

    def record_list(
        self,
        *,
        profile: str,
        app_key: str,
        columns: list[JSONObject | int],
        query: str | None = None,
        query_fields: list[JSONObject | int] | None = None,
        where: list[JSONObject],
        order_by: list[JSONObject],
        limit: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
        page: int = 1,
        page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
        view_id: str | None = None,
        list_type: int | None = None,
        view_key: str | None = None,
        view_name: str | None = None,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_output_profile = self._normalize_public_output_profile(output_profile, allow_normalized=True)
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        legacy_warnings = _detect_record_list_legacy_warnings(columns=columns, where=where, order_by=order_by)
        normalized_columns = _normalize_public_column_selectors(columns)
        normalized_query = _normalize_optional_text(query)
        normalized_query_field_selectors = _normalize_public_query_field_selectors(query_fields or [])
        if limit <= 0:
            raise_tool_error(QingflowApiError.config_error("limit must be positive"))
        if page <= 0:
            raise_tool_error(QingflowApiError.config_error("page must be positive"))
        if page_size <= 0:
            raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
        if not (
            _normalize_optional_text(view_id)
            or list_type is not None
            or _normalize_optional_text(view_key)
            or _normalize_optional_text(view_name)
        ):
            raise_tool_error(
                QingflowApiError.config_error(
                    "record_list requires view_id. Call app_get first and pass accessible_views[].view_id.",
                    details={
                        "error_code": "RECORD_LIST_VIEW_REQUIRED",
                        "fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_list with view_id.",
                    },
                )
            )
        view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
            profile=profile,
            app_key=app_key,
            view_id=view_id,
            list_type=list_type,
            view_key=view_key,
            view_name=view_name,
            allow_default=False,
            tool_name="记录列表",
        )
        if not _view_type_supports_analysis(view_route.view_type):
            raise_tool_error(
                QingflowApiError(
                    category="not_supported",
                    message=(
                        f"record_list does not support view '{view_route.name}' "
                        f"because its type is {view_route.view_type}."
                    ),
                    details={
                        "error_code": "VIEW_LIST_UNSUPPORTED",
                        "view_id": view_route.view_id,
                        "view_name": view_route.name,
                        "view_type": view_route.view_type,
                        "fix_hint": "Choose a system view or a custom table-style view from app_get.accessible_views.",
                    },
                )
            )
        filters = self._normalize_record_list_where(where)
        sorts = self._normalize_record_list_order_by(order_by)

        def runner(session_profile, context):
            browse_scope = self._build_browse_read_scope(
                profile,
                context,
                app_key,
                view_route,
                force_refresh=False,
            )
            index = cast(FieldIndex, browse_scope["index"])
            selected_fields = (
                self._resolve_record_list_columns(normalized_columns, index, view_route=view_route)
                if normalized_columns
                else self._derive_record_list_fields_from_index(index)
            )
            resolved_columns = [field.que_id for field in selected_fields]
            resolved_query_fields = self._resolve_record_list_query_fields(
                normalized_query_field_selectors,
                index,
                view_route=view_route,
            )
            match_rules = self._resolve_record_list_match_rules(context, filters, index, view_route=view_route)
            sort_rules = self._resolve_record_list_sort_rules(sorts, index, view_route=view_route)
            raw = self._record_list_query_view_fields(
                session_profile=session_profile,
                context=context,
                app_key=app_key,
                view_route=view_route,
                page_num=page,
                page_size=page_size,
                query_key=normalized_query,
                search_que_ids=resolved_query_fields or None,
                match_rules=match_rules,
                sort_rules=sort_rules,
                max_rows=min(limit, page_size),
                selected_fields=selected_fields,
                output_profile="verbose" if normalized_output_profile in {"verbose", "normalized"} else DEFAULT_OUTPUT_PROFILE,
            )
            list_data = cast(JSONObject, cast(JSONObject, raw["data"])["list"])
            pagination = cast(JSONObject, list_data["pagination"])
            warnings: list[JSONObject] = []
            warnings.extend(legacy_warnings)
            warnings.extend(compatibility_warnings)
            warnings.extend(_view_filter_trust_warnings(view_route))
            warning = _normalize_optional_text(list_data.get("analysis_warning"))
            if warning:
                warnings.append({"code": "BROWSE_ONLY", "message": warning})
            list_type_used = _coerce_count(pagination.get("list_type_used"))
            if list_type_used is not None and list_type_used != DEFAULT_RECORD_LIST_TYPE:
                warnings.append(
                    {
                        "code": "LIST_TYPE_FALLBACK",
                        "message": (
                            f"record_list not accessible via listType={DEFAULT_RECORD_LIST_TYPE}; "
                            f"fell back to listType={list_type_used} ({get_record_list_type_label(list_type_used)})."
                        ),
                    }
                )
            rows = list_data.get("rows", [])
            normalized_public_rows = _normalize_public_record_rows(rows if isinstance(rows, list) else [])
            lookup_payload = _build_record_list_lookup_payload(
                query=normalized_query,
                items=normalized_public_rows,
                pagination=pagination,
            )
            total_count = _coerce_count(pagination.get("result_amount"))
            returned_count = _coerce_count(pagination.get("returned_items"))
            if returned_count is None:
                returned_count = len(normalized_public_rows)
            truncated = bool(total_count is not None and total_count > returned_count)
            response: JSONObject = {
                "profile": profile,
                "ws_id": raw.get("ws_id"),
                "ok": bool(raw.get("ok", True)),
                "request_route": raw.get("request_route"),
                "warnings": warnings,
                "verification": _view_filter_verification_payload(view_route),
                "output_profile": normalized_output_profile,
                "data": {
                    "app_key": app_key,
                    "items": normalized_public_rows,
                    "pagination": {
                        "returned_count": returned_count,
                        "total_count": total_count,
                        "truncated": truncated,
                    },
                    "selection": {
                        "columns": [_column_selector_payload(field_id) for field_id in resolved_columns],
                        "query_fields": [_column_selector_payload(field_id) for field_id in resolved_query_fields],
                        "view": _accessible_view_payload(view_route),
                    },
                },
            }
            if lookup_payload is not None:
                response["lookup"] = lookup_payload
            if normalized_output_profile == "normalized":
                normalized_rows = list_data.get("normalized_rows")
                if isinstance(normalized_rows, list):
                    item_by_apply_id = {
                        _coerce_count(item.get("apply_id")): item
                        for item in cast(list[JSONObject], response["data"]["items"])
                        if isinstance(item, dict) and _coerce_count(item.get("apply_id")) is not None
                    }
                    for entry in normalized_rows:
                        if not isinstance(entry, dict):
                            continue
                        apply_id = _coerce_count(entry.get("apply_id"))
                        if apply_id is None:
                            continue
                        target = item_by_apply_id.get(apply_id)
                        if target is None:
                            continue
                        target["normalized_record"] = cast(JSONObject, entry.get("normalized_record") or {})
                        target["normalized_ambiguous_fields"] = cast(JSONObject, entry.get("normalized_ambiguous_fields") or {})
            if normalized_output_profile == "verbose":
                response["data"]["debug"] = {
                    "completeness": raw.get("completeness"),
                    "evidence": raw.get("evidence"),
                    "resolved_mappings": raw.get("resolved_mappings"),
                    "row_cap_hit": list_data.get("row_cap_hit"),
                    "sample_only": list_data.get("sample_only"),
                }
            return response

        return self._run_record_tool(profile, runner, tool_name="记录列表")

    def record_access(
        self,
        *,
        profile: str,
        app_key: str,
        view_id: str,
        columns: list[JSONObject | int],
        where: list[JSONObject],
        order_by: list[JSONObject],
    ) -> JSONObject:
        """Fetch records across pages and write readable CSV shards for local analysis."""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if not (view_id or "").strip():
            raise_tool_error(
                QingflowApiError.config_error(
                    "record_access requires view_id. Call app_get first and pass accessible_views[].view_id."
                )
            )
        legacy_warnings = _detect_record_list_legacy_warnings(columns=columns, where=where, order_by=order_by)
        normalized_columns = _normalize_public_column_selectors(columns)
        if not normalized_columns:
            raise_tool_error(
                QingflowApiError.config_error(
                    "columns is required. Call record_browse_schema_get first and pass field_id-only columns."
                )
            )
        view_route, compatibility_warnings = self._resolve_accessible_view_route_for_public(
            profile=profile,
            app_key=app_key,
            view_id=view_id,
            list_type=None,
            view_key=None,
            view_name=None,
            allow_default=False,
            tool_name="记录访问",
        )
        if not _view_type_supports_analysis(view_route.view_type):
            raise_tool_error(
                QingflowApiError(
                    category="not_supported",
                    message=(
                        f"record_access does not support view '{view_route.name}' "
                        f"because its type is {view_route.view_type}."
                    ),
                    details={
                        "error_code": "VIEW_ACCESS_UNSUPPORTED",
                        "view_id": view_route.view_id,
                        "view_name": view_route.name,
                        "view_type": view_route.view_type,
                        "fix_hint": "Choose a system view or a custom table-style view from app_get.accessible_views.",
                    },
                )
            )
        filters = self._normalize_record_list_where(where)
        sorts = self._normalize_record_list_order_by(order_by)

        def runner(session_profile, context):
            browse_scope = self._build_browse_read_scope(
                profile,
                context,
                app_key,
                view_route,
                force_refresh=False,
            )
            index = cast(FieldIndex, browse_scope["index"])
            selected_fields = self._resolve_record_access_columns(
                normalized_columns,
                index,
                view_route=view_route,
            )
            view_selection = view_route.view_selection
            match_rules = self._resolve_match_rules(context, filters, index)
            sort_rules = self._resolve_sorts(sorts, index)
            used_list_type: int | None = None
            fallback_list_types = (
                [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
                if view_selection is not None or view_route.list_type is not None
                else [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
            )
            fields_payload = [_record_access_field_payload(field) for field in selected_fields]
            scope_status = _record_access_scope_status(filters, view_route, index)

            def fetch_page(page_num: int, page_size: int) -> JSONObject:
                nonlocal used_list_type
                if used_list_type is None:
                    last_error: QingflowApiError | None = None
                    page: JSONObject | None = None
                    for candidate_list_type in fallback_list_types:
                        try:
                            page = self._search_page(
                                context,
                                app_key=app_key,
                                view_selection=view_selection,
                                page_num=page_num,
                                page_size=page_size,
                                query_key=None,
                                match_rules=match_rules,
                                sorts=sort_rules,
                                search_que_ids=None,
                                list_type=candidate_list_type,
                            )
                            used_list_type = None if view_selection is not None else candidate_list_type
                            break
                        except QingflowApiError as exc:
                            last_error = exc
                            if self._should_retry_list_type_fallback(exc) and candidate_list_type != fallback_list_types[-1]:
                                continue
                            raise
                    if page is None:
                        if last_error is not None:
                            raise last_error
                        raise_tool_error(QingflowApiError.config_error("record_access failed: no accessible listType"))
                    return page
                return self._search_page(
                    context,
                    app_key=app_key,
                    view_selection=view_selection,
                    page_num=page_num,
                    page_size=page_size,
                    query_key=None,
                    match_rules=match_rules,
                    sorts=sort_rules,
                    search_que_ids=None,
                    list_type=used_list_type,
                )

            probe_page_size = 1 if _record_access_light_probe_recommended(scope_status) else BACKEND_RECORD_ACCESS_PAGE_SIZE
            probe_page = fetch_page(1, probe_page_size)
            reported_total = _effective_total(probe_page, probe_page_size)
            estimated_pages = _record_access_estimated_pages(probe_page, reported_total, page_size=probe_page_size)
            scope_payload = _record_access_scope_payload(
                scope_status=scope_status,
                reported_total=reported_total,
                estimated_pages=estimated_pages,
                index=index,
            )
            warnings: list[JSONObject] = []
            warnings.extend(legacy_warnings)
            warnings.extend(compatibility_warnings)
            warnings.extend(_view_filter_trust_warnings(view_route))
            if used_list_type is not None and used_list_type != DEFAULT_RECORD_LIST_TYPE:
                warnings.append(
                    {
                        "code": "LIST_TYPE_FALLBACK",
                        "message": (
                            f"record_access not accessible via listType={DEFAULT_RECORD_LIST_TYPE}; "
                            f"fell back to listType={used_list_type} ({get_record_list_type_label(used_list_type)})."
                        ),
                    }
                )
            if _record_access_needs_scope(scope_status=scope_status, reported_total=reported_total):
                warnings.append(_record_access_unbounded_scan_warning(reported_total=reported_total, estimated_pages=estimated_pages))
                verification_payload: JSONObject = {
                    **_view_filter_verification_payload(view_route),
                    "reported_total": reported_total,
                    "estimated_pages": estimated_pages,
                    "fetched_pages": 1,
                    "stopped_reason": "UNBOUNDED_SCAN_TOO_LARGE",
                    "list_type_used": used_list_type,
                }
                return {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "ok": True,
                    "status": "needs_scope",
                    "app_key": app_key,
                    "view_id": view_route.view_id,
                    "format": "csv",
                    "row_count": 0,
                    "complete": False,
                    "truncated": True,
                    "safe_for_final_conclusion": False,
                    "files": [],
                    "fields": fields_payload,
                    "warnings": warnings,
                    "verification": verification_payload,
                    "scope": scope_payload,
                    "request_route": self._request_route_payload(context),
                }

            run_dir = _record_access_run_dir()
            run_dir.mkdir(parents=True, exist_ok=True)
            header = ["record_id"] + [str(field["column_name"]) for field in fields_payload]
            files: list[JSONObject] = []
            shard_part = 0
            shard_rows = 0
            row_count = 0
            file_handle: Any | None = None
            writer: Any | None = None

            def close_shard() -> None:
                nonlocal file_handle, writer, shard_rows
                if file_handle is None:
                    return
                path = Path(file_handle.name)
                file_handle.close()
                files.append(
                    {
                        "local_path": str(path),
                        "row_count": shard_rows,
                        "part": len(files) + 1,
                    }
                )
                file_handle = None
                writer = None
                shard_rows = 0

            def ensure_writer() -> Any:
                nonlocal file_handle, writer, shard_part
                if writer is not None:
                    return writer
                shard_part += 1
                path = run_dir / f"records-{shard_part:04d}.csv"
                file_handle = path.open("w", newline="", encoding="utf-8-sig")
                writer = csv.writer(file_handle)
                writer.writerow(header)
                return writer

            def write_record(apply_id: int | None, answer_list: list[JSONValue]) -> None:
                nonlocal row_count, shard_rows
                if apply_id is None:
                    return
                if shard_rows >= DEFAULT_RECORD_ACCESS_SHARD_ROWS:
                    close_shard()
                csv_writer = ensure_writer()
                values: list[str] = [_public_record_id_text(apply_id) or ""]
                for field in selected_fields:
                    answer = _find_answer_for_field(answer_list, field)
                    values.append(_record_access_csv_cell(_normalize_answer_field_value_for_output(answer, field)))
                csv_writer.writerow(values)
                row_count += 1
                shard_rows += 1

            current_page = 1
            has_more = False
            fetched_pages = 0
            stopped_reason: str | None = None
            deadline = time.monotonic() + RECORD_ACCESS_TIME_BUDGET_SECONDS
            page = probe_page if probe_page_size == BACKEND_RECORD_ACCESS_PAGE_SIZE else fetch_page(1, BACKEND_RECORD_ACCESS_PAGE_SIZE)
            try:
                while True:
                    page_rows = page.get("list")
                    items = page_rows if isinstance(page_rows, list) else []
                    has_more = _page_has_more(page, current_page, BACKEND_RECORD_ACCESS_PAGE_SIZE, len(items))
                    page_apply_order: list[int] = []
                    page_answer_map: dict[int, list[JSONValue]] = {}
                    for item in items:
                        if not isinstance(item, dict):
                            continue
                        answers = item.get("answers")
                        answer_list = answers if isinstance(answers, list) else []
                        apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
                        if apply_id is None:
                            continue
                        page_apply_order.append(apply_id)
                        page_answer_map[apply_id] = cast(list[JSONValue], answer_list)
                    for apply_id in page_apply_order:
                        write_record(apply_id, page_answer_map.get(apply_id, []))
                    fetched_pages += 1
                    if not has_more:
                        break
                    current_page += 1
                    if _record_access_time_budget_exceeded(
                        deadline,
                        fetched_pages=fetched_pages,
                        estimated_pages=estimated_pages,
                    ):
                        stopped_reason = "TIME_BUDGET_EXCEEDED"
                        break
                    page = fetch_page(current_page, BACKEND_RECORD_ACCESS_PAGE_SIZE)
            finally:
                close_shard()

            if stopped_reason == "TIME_BUDGET_EXCEEDED":
                warnings.append(_record_access_time_budget_warning(reported_total=reported_total, fetched_pages=fetched_pages))
            complete = stopped_reason is None and not has_more
            safe_for_final_conclusion = complete and not any(
                warning.get("code") == "CUSTOM_VIEW_FILTER_UNVERIFIED" for warning in warnings
            )
            verification_payload: JSONObject = {
                **_view_filter_verification_payload(view_route),
                "reported_total": reported_total,
                "estimated_pages": estimated_pages,
                "fetched_pages": fetched_pages,
                "stopped_reason": stopped_reason,
                "list_type_used": used_list_type,
            }
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "status": "success" if complete else "partial",
                "app_key": app_key,
                "view_id": view_route.view_id,
                "format": "csv",
                "local_dir": str(run_dir),
                "row_count": row_count,
                "complete": complete,
                "truncated": not complete,
                "safe_for_final_conclusion": safe_for_final_conclusion,
                "files": files,
                "fields": fields_payload,
                "warnings": warnings,
                "verification": verification_payload,
                "scope": scope_payload,
                "request_route": self._request_route_payload(context),
            }

        return self._run_record_tool(profile, runner, tool_name="记录访问")

    def record_get_public(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: Any,
        columns: list[JSONObject | int],
        view_id: str | None = None,
        workflow_node_id: int | None = None,
        output_profile: str = "detail_context",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        requested_output_profile = _normalize_record_get_detail_output_profile(output_profile)
        record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
        normalized_columns = _normalize_public_column_selectors(columns)
        explicit_view_id = _normalize_optional_text(view_id)

        def runner(session_profile, context):
            resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
                profile,
                context,
                app_key,
                view_id=view_id,
                list_type=None,
                view_key=None,
                view_name=None,
                allow_default=True,
            )
            warnings: list[JSONObject] = []
            warnings.extend(compatibility_warnings)
            warnings.extend(_view_filter_trust_warnings(resolved_view))
            if normalized_columns:
                warnings.append({
                    "code": "COLUMNS_IGNORED_FOR_DETAIL_CONTEXT",
                    "message": "record_get now mirrors the frontend detail page; columns are treated as focus hints and do not project fields.",
                })
            if requested_output_profile != "detail_context":
                warnings.append({
                    "code": "OUTPUT_PROFILE_DEPRECATED_FOR_DETAIL_CONTEXT",
                    "message": f"output_profile={requested_output_profile!r} is deprecated for record_get; detail_context is always returned.",
                })
            def get_detail_for_route(route: AccessibleViewRoute, route_warnings: list[JSONObject]) -> JSONObject:
                return self._record_get_detail_context(
                    profile=profile,
                    session_profile=session_profile,
                    context=context,
                    app_key=app_key,
                    record_id_int=record_id_int,
                    resolved_view=route,
                    requested_focus_field_ids=normalized_columns,
                    workflow_node_id=workflow_node_id,
                    warnings=route_warnings,
                )

            try:
                return get_detail_for_route(resolved_view, warnings)
            except QingflowApiError as exc:
                if explicit_view_id is not None:
                    raise
                if not self._is_record_context_route_miss(exc):
                    raise
                fallback_warnings = list(warnings)
                fallback_warnings.append(
                    {
                        "code": "DEFAULT_DETAIL_ROUTE_DENIED",
                        "message": "record_get default system:all route was not readable; trying accessible views that match the frontend route model.",
                        "backend_code": exc.backend_code,
                    }
                )
                last_error = exc
                for candidate in self._candidate_update_views(profile, context, app_key):
                    if candidate.view_id == resolved_view.view_id:
                        continue
                    try:
                        return get_detail_for_route(candidate, fallback_warnings)
                    except QingflowApiError as candidate_exc:
                        if not self._is_record_context_route_miss(candidate_exc):
                            raise
                        last_error = candidate_exc
                raise last_error

        return self._run_record_tool(profile, runner, tool_name="记录详情")

    def record_logs_get(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: Any,
        view_id: str | None = None,
    ) -> JSONObject:
        """读取单条记录可见的全量数据日志和流程日志，写入本地 JSONL。"""
        record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
        if not _normalize_optional_text(view_id):
            raise_tool_error(
                QingflowApiError.config_error(
                    "record_logs_get requires view_id. Call app_get first and pass accessible_views[].view_id.",
                    details={
                        "error_code": "RECORD_LOGS_VIEW_REQUIRED",
                        "fix_hint": "Call app_get first and choose one accessible_views[].view_id, then retry record_logs_get with view_id.",
                    },
                )
            )

        def runner(session_profile, context):
            resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
                profile,
                context,
                app_key,
                view_id=view_id,
                list_type=None,
                view_key=None,
                view_name=None,
                allow_default=False,
            )
            warnings: list[JSONObject] = []
            warnings.extend(compatibility_warnings)
            warnings.extend(_view_filter_trust_warnings(resolved_view))
            unavailable_context: list[JSONObject] = []

            schema: JSONObject = {}
            try:
                schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
            except QingflowApiError as exc:
                if not _is_optional_schema_permission_error(exc):
                    raise
                unavailable_context.append(
                    _record_detail_unavailable_context(
                        "detail_schema",
                        "记录日志字段结构辅助信息获取失败，已尝试使用详情主数据中的字段信息继续。",
                        exc,
                    )
                )
            index = _build_top_level_field_index(schema)
            try:
                audit_info = self._record_get_audit_info(
                    context,
                    app_key=app_key,
                    record_id=record_id_int,
                    resolved_view=resolved_view,
                )
            except QingflowApiError as exc:
                if not _is_optional_schema_permission_error(exc):
                    raise
                audit_info = {}
                unavailable_context.append(
                    _record_detail_unavailable_context(
                        "audit_info",
                        "记录审批节点辅助信息获取失败，已继续读取详情主数据和日志。",
                        exc,
                    )
                )
            audit_context = _record_detail_audit_context(audit_info, workflow_node_id=None)
            detail_result, used_list_type, used_role = self._record_get_apply_detail(
                context,
                app_key=app_key,
                record_id=record_id_int,
                resolved_view=resolved_view,
                audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            )
            answer_list = _record_detail_answers(detail_result)
            if not index.by_id:
                answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
                if answer_index.by_id:
                    index = answer_index
                    unavailable_context.append(
                        {
                            "section": "detail_schema",
                            "message": "字段结构由详情 answers 回退构造；候选范围、只读状态、选项和联动信息可能不完整。",
                            "category": "partial_context",
                        }
                    )
            selected_fields = list(index.by_id.values())
            fields = [
                _record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=set())
                for field in selected_fields
            ]
            app_name = self._record_get_detail_app_name(
                profile,
                context,
                app_key=app_key,
                schema=schema,
                used_list_type=used_list_type,
            )
            view_payload = _accessible_view_payload(resolved_view)
            record_payload = _record_detail_record_payload(
                app_key=app_key,
                record_id=record_id_int,
                detail=detail_result,
                answer_list=cast(list[JSONValue], answer_list),
                fields=fields,
            )
            log_visibility = self._record_get_log_visibility_context(
                context,
                app_key=app_key,
                record_id=record_id_int,
                resolved_view=resolved_view,
                role=used_role,
                audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
                unavailable_context=unavailable_context,
            )
            run_dir = _record_logs_run_dir()
            run_dir.mkdir(parents=True, exist_ok=True)
            deadline = time.monotonic() + RECORD_LOGS_TIME_BUDGET_SECONDS
            data_logs = self._record_get_full_data_logs_context(
                context,
                app_key=app_key,
                record_id=record_id_int,
                role=used_role,
                log_visibility=log_visibility,
                unavailable_context=unavailable_context,
                run_dir=run_dir,
                deadline=deadline,
            )
            workflow_logs = self._record_get_full_workflow_logs_context(
                context,
                app_key=app_key,
                record_id=record_id_int,
                resolved_view=resolved_view,
                role=used_role,
                audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
                log_visibility=log_visibility,
                unavailable_context=unavailable_context,
                run_dir=run_dir,
                deadline=deadline,
            )
            status = _record_logs_overall_status(data_logs=data_logs, workflow_logs=workflow_logs)
            context_integrity = _record_logs_context_integrity(data_logs=data_logs, workflow_logs=workflow_logs)
            payload: JSONObject = {
                "ok": True,
                "status": status,
                "output_profile": "record_logs",
                "app": {"app_key": app_key, "app_name": app_name},
                "view": view_payload,
                "record": record_payload,
                "local_dir": str(run_dir),
                "data_logs": data_logs,
                "workflow_logs": workflow_logs,
                "warnings": warnings,
                "unavailable_context": unavailable_context,
                "context_integrity": context_integrity,
            }
            summary_path = run_dir / "summary.json"
            summary_payload = deepcopy(payload)
            summary_payload.pop("request_route", None)
            summary_path.write_text(json.dumps(summary_payload, ensure_ascii=False, indent=2), encoding="utf-8")
            payload["summary_path"] = str(summary_path)
            return payload

        return self._run_record_tool(profile, runner, tool_name="记录全量日志")

    def _record_get_detail_context(
        self,
        *,
        profile: str,
        session_profile,  # type: ignore[no-untyped-def]
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        record_id_int: int,
        resolved_view: AccessibleViewRoute,
        requested_focus_field_ids: list[int],
        workflow_node_id: int | None,
        warnings: list[JSONObject],
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        unavailable_context: list[JSONObject] = []
        schema: JSONObject = {}
        schema_available = True
        try:
            schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=False)
        except QingflowApiError as exc:
            if not _is_optional_schema_permission_error(exc):
                raise
            schema_available = False
            unavailable_context.append(
                _record_detail_unavailable_context(
                    "detail_schema",
                    "记录详情字段结构辅助信息获取失败，已尝试使用详情主数据中的字段信息继续。",
                    exc,
                )
            )
        index = _build_top_level_field_index(schema)
        try:
            audit_info = self._record_get_audit_info(
                context,
                app_key=app_key,
                record_id=record_id_int,
                resolved_view=resolved_view,
            )
        except QingflowApiError as exc:
            if not _is_optional_schema_permission_error(exc):
                raise
            audit_info = {}
            unavailable_context.append(
                _record_detail_unavailable_context(
                    "audit_info",
                    "记录审批节点辅助信息获取失败，已继续读取详情主数据。",
                    exc,
                )
            )
        audit_context = _record_detail_audit_context(audit_info, workflow_node_id=workflow_node_id)
        detail_result, used_list_type, used_role = self._record_get_apply_detail(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
        )
        answer_list = _record_detail_answers(detail_result)
        if not index.by_id:
            answer_index = _build_answer_backed_field_index(cast(list[JSONObject], answer_list))
            if answer_index.by_id:
                index = answer_index
                unavailable_context.append(
                    {
                        "section": "detail_schema",
                        "message": "字段结构由详情 answers 回退构造；候选范围、只读状态、选项和联动信息可能不完整。",
                        "category": "partial_context",
                    }
                )
        selected_fields = list(index.by_id.values())
        row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
        normalized_record, _normalized_ambiguous_fields = _build_normalized_row_from_answers(
            cast(list[JSONValue], answer_list),
            selected_fields,
        )
        if schema_available and self._record_get_needs_schema_refresh(
            answer_list=cast(list[JSONValue], answer_list),
            selected_fields=selected_fields,
            record=row,
            normalized_record=normalized_record,
        ):
            self._clear_record_schema_caches(
                profile=profile,
                app_key=app_key,
                resolved_view=resolved_view,
                clear_view_caches=True,
            )
            schema = self._record_get_detail_schema(profile, context, app_key, resolved_view, force_refresh=True)
            index = _build_top_level_field_index(schema)
            selected_fields = list(index.by_id.values())

        dynamic_reference_answers, dynamic_reference_unavailable = self._record_get_dynamic_reference_answers(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            role=used_role,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            answer_list=cast(list[JSONValue], answer_list),
            selected_fields=selected_fields,
        )
        unavailable_context.extend(dynamic_reference_unavailable)
        if dynamic_reference_answers:
            answer_list = _merge_record_detail_answers(
                cast(list[JSONValue], answer_list),
                dynamic_reference_answers,
            )

        app_name = self._record_get_detail_app_name(
            profile,
            context,
            app_key=app_key,
            schema=schema,
            used_list_type=used_list_type,
        )
        view_payload = _accessible_view_payload(resolved_view)
        focus_id_set = {str(item) for item in requested_focus_field_ids}
        fields = [
            _record_detail_field_payload(field, _find_answer_for_field(cast(list[JSONValue], answer_list), field), focus_id_set=focus_id_set)
            for field in selected_fields
        ]
        record_payload = _record_detail_record_payload(
            app_key=app_key,
            record_id=record_id_int,
            detail=detail_result,
            answer_list=cast(list[JSONValue], answer_list),
            fields=fields,
        )
        summary = _record_detail_summary_payload(
            app_name=app_name,
            view_payload=view_payload,
            record=record_payload,
            fields=fields,
        )
        references, reference_unavailable = self._record_get_reference_context(
            profile,
            context,
            app_key=app_key,
            source_record_id=record_id_int,
            answer_list=cast(list[JSONValue], answer_list),
            selected_fields=selected_fields,
        )
        unavailable_context.extend(reference_unavailable)
        log_visibility = self._record_get_log_visibility_context(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            role=used_role,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            unavailable_context=unavailable_context,
        )
        data_logs = self._record_get_data_logs_context(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            role=used_role,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            log_visibility=log_visibility,
            unavailable_context=unavailable_context,
        )
        workflow_logs = self._record_get_workflow_logs_context(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            role=used_role,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            log_visibility=log_visibility,
            unavailable_context=unavailable_context,
        )
        associated_resources = self._record_get_associated_resources(
            context,
            app_key=app_key,
            resolved_view=resolved_view,
            role=used_role,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            unavailable_context=unavailable_context,
        )
        media_assets = self._record_get_media_assets(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            fields=fields,
            references=references,
        )
        file_assets = self._record_get_file_assets(
            context,
            app_key=app_key,
            record_id=record_id_int,
            resolved_view=resolved_view,
            audit_node_id=cast(int | None, audit_context.get("audit_node_id")),
            fields=fields,
            references=references,
            media_assets=media_assets,
        )
        context_integrity = _record_detail_context_integrity(
            references=references,
            data_logs=data_logs,
            workflow_logs=workflow_logs,
            associated_resources=associated_resources,
            media_assets=media_assets,
            file_assets=file_assets,
            unavailable_context=unavailable_context,
        )
        payload: JSONObject = {
            "profile": profile,
            "ws_id": session_profile.selected_ws_id,
            "ok": True,
            "status": "ok",
            "output_profile": "detail_context",
            "request_route": self._request_route_payload(context),
            "warnings": warnings,
            "verification": _view_filter_verification_payload(resolved_view),
            "app": {
                "app_key": app_key,
                "app_name": app_name,
            },
            "view": view_payload,
            "record": record_payload,
            "summary": summary,
            "fields": fields,
            "requested_focus_fields": [_column_selector_payload(field_id) for field_id in requested_focus_field_ids],
            "references": references,
            "media_assets": media_assets,
            "file_assets": file_assets,
            "data_logs": data_logs,
            "workflow_logs": workflow_logs,
            "associated_resources": associated_resources,
            "unavailable_context": unavailable_context,
            "context_integrity": context_integrity,
        }
        payload["semantic_context"] = _record_detail_semantic_context(payload)
        return payload

    def _record_get_detail_schema(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        resolved_view: AccessibleViewRoute,
        *,
        force_refresh: bool,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
            return self._get_custom_view_browse_schema(
                profile,
                context,
                resolved_view.view_selection.view_key,
                force_refresh=force_refresh,
            )
        if resolved_view.kind == "system" and resolved_view.list_type is not None:
            return self._get_system_browse_schema(
                profile,
                context,
                app_key,
                list_type=resolved_view.list_type,
                force_refresh=force_refresh,
            )
        return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)

    def _record_get_audit_info(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
            payload = self.backend.request(
                "GET",
                context,
                f"/view/{resolved_view.view_selection.view_key}/apply/{record_id}/auditInfo",
            )
            return payload if isinstance(payload, dict) else {"items": payload}
        params: JSONObject = {}
        if resolved_view.list_type is not None:
            params["type"] = resolved_view.list_type
        payload = self.backend.request(
            "GET",
            context,
            f"/app/{app_key}/apply/{record_id}/auditInfo",
            params=params or None,
        )
        return payload if isinstance(payload, dict) else {"items": payload}

    def _record_get_apply_detail(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        audit_node_id: int | None,
    ) -> tuple[JSONObject, int | None, int]:
        """执行内部辅助逻辑。"""
        if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
            result = self.backend.request(
                "GET",
                context,
                f"/view/{resolved_view.view_selection.view_key}/apply/{record_id}",
            )
            return result if isinstance(result, dict) else {"value": result}, None, 1
        list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
        role = _record_detail_role_for_list_type(list_type)
        params: JSONObject = {"role": role, "listType": list_type}
        if audit_node_id is not None:
            params["auditNodeId"] = audit_node_id
        try:
            result = self.backend.request(
                "GET",
                context,
                f"/app/{app_key}/apply/{record_id}",
                params=params,
            )
            return result if isinstance(result, dict) else {"value": result}, list_type, role
        except QingflowApiError as exc:
            if resolved_view.list_type is not None or not _is_record_permission_denied_error(exc):
                raise
            last_error: QingflowApiError = exc
            for fallback_list_type in (14, 1, 2, 12):
                role = _record_detail_role_for_list_type(fallback_list_type)
                params = {"role": role, "listType": fallback_list_type}
                if audit_node_id is not None:
                    params["auditNodeId"] = audit_node_id
                try:
                    result = self.backend.request(
                        "GET",
                        context,
                        f"/app/{app_key}/apply/{record_id}",
                        params=params,
                    )
                    return result if isinstance(result, dict) else {"value": result}, fallback_list_type, role
                except QingflowApiError as fallback_exc:
                    last_error = fallback_exc
                    if _is_record_permission_denied_error(fallback_exc):
                        continue
                    raise
            raise last_error

    def _record_get_detail_app_name(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        schema: JSONObject,
        used_list_type: int | None,
    ) -> str | None:
        """执行内部辅助逻辑。"""
        schema_name = _normalize_optional_text(schema.get("formTitle", schema.get("appName")))
        if schema_name:
            return schema_name
        return self._resolve_visible_app_name(
            profile,
            context,
            target_app_key=app_key,
            ws_id=getattr(context, "ws_id", None),
        )

    def _record_get_reference_context(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        source_record_id: int,
        answer_list: list[JSONValue],
        selected_fields: list[FormField],
    ) -> tuple[list[JSONObject], list[JSONObject]]:
        """执行内部辅助逻辑。"""
        references: list[JSONObject] = []
        unavailable_context: list[JSONObject] = []
        for field in selected_fields:
            if field.que_type not in RELATION_QUE_TYPES:
                continue
            answer = _find_answer_for_field(answer_list, field)
            if not isinstance(answer, dict):
                continue
            relation_ids = _relation_ids_from_answer(answer)
            if not relation_ids:
                continue
            normalized_value = _normalize_relation_answer_value_for_output(answer)
            relation_entries = _record_detail_relation_entries(normalized_value)
            for target_record_id in relation_ids:
                target_app_key = field.target_app_key or app_key
                target_label = _record_detail_relation_label(relation_entries, target_record_id)
                reference_payload: JSONObject = {
                    "field_id": field.que_id,
                    "field_title": field.que_title,
                    "resolution_source": _record_detail_reference_resolution_source(answer),
                    "target_app_key": target_app_key,
                    "target_record_id": target_record_id,
                    "target_title": target_label,
                    "target_detail_completeness": "unavailable",
                    "target_fields": [],
                }
                try:
                    target_schema = self._get_form_schema(profile, context, target_app_key, force_refresh=False)
                    target_index = _build_top_level_field_index(target_schema)
                    target_detail, _used_list_type, _role = self._record_get_apply_detail(
                        context,
                        app_key=target_app_key,
                        record_id=normalize_positive_id_int(target_record_id, field_name="target_record_id"),
                        resolved_view=AccessibleViewRoute(
                            view_id="system:all",
                            name=get_system_view_name("system:all"),
                            kind="system",
                            list_type=DEFAULT_RECORD_LIST_TYPE,
                            view_selection=None,
                        ),
                        audit_node_id=None,
                    )
                    target_answers = _record_detail_answers(target_detail)
                    target_fields = [
                        _record_detail_field_payload(target_field, _find_answer_for_field(target_answers, target_field), focus_id_set=set())
                        for target_field in target_index.by_id.values()
                    ]
                    reference_payload["target_title"] = (
                        _normalize_optional_text(reference_payload.get("target_title"))
                        or _normalize_optional_text(_record_detail_record_payload(
                            app_key=target_app_key,
                            record_id=normalize_positive_id_int(target_record_id, field_name="target_record_id"),
                            detail=target_detail,
                            answer_list=target_answers,
                            fields=target_fields,
                        ).get("title"))
                    )
                    reference_payload["target_fields"] = target_fields
                    reference_payload["target_detail_completeness"] = "full"
                    if target_app_key == app_key and str(target_record_id) == str(source_record_id):
                        reference_payload["self_reference"] = True
                except QingflowApiError as exc:
                    if is_auth_like_error(exc):
                        raise
                    unavailable = _record_detail_unavailable_context(
                        "reference_detail",
                        f"引用字段「{field.que_title}」的目标记录详情获取失败。",
                        exc,
                    )
                    unavailable["field_id"] = field.que_id
                    unavailable["target_app_key"] = target_app_key
                    unavailable["target_record_id"] = target_record_id
                    unavailable_context.append(unavailable)
                references.append(reference_payload)
        return references, unavailable_context

    def _record_get_dynamic_reference_answers(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        role: int,
        audit_node_id: int | None,
        answer_list: list[JSONValue],
        selected_fields: list[FormField],
    ) -> tuple[list[JSONObject], list[JSONObject]]:
        """Fetch runtime-matched reference cards that the frontend resolves lazily."""
        selected_field_by_id = {field.que_id: field for field in selected_fields}
        query_questions: list[JSONObject] = []
        key_que_values: dict[int, JSONObject] = {}
        skipped_fields: list[JSONObject] = []
        for field in selected_fields:
            if field.que_type not in RELATION_QUE_TYPES:
                continue
            existing_answer = _find_answer_for_field(answer_list, field)
            if isinstance(existing_answer, dict) and _relation_ids_from_answer(existing_answer):
                continue
            reference = field.raw.get("referenceConfig")
            if not isinstance(reference, dict):
                continue
            required_key_values, missing_sources = _record_detail_reference_match_key_values(
                reference,
                answer_list=answer_list,
                selected_field_by_id=selected_field_by_id,
            )
            if not required_key_values and not missing_sources:
                continue
            if missing_sources:
                skipped_fields.append({
                    "field_id": field.que_id,
                    "field_title": field.que_title,
                    "missing_source_field_ids": missing_sources,
                })
                continue
            query_questions.append({"queId": field.que_id, "ordinal": None})
            for key_value in required_key_values:
                key_que_id = _coerce_count(key_value.get("keyQueId"))
                if key_que_id is None:
                    continue
                existing = key_que_values.get(key_que_id)
                if existing is None:
                    key_que_values[key_que_id] = key_value
                    continue
                merged_values = _merge_string_lists(existing.get("values"), key_value.get("values"))
                existing["values"] = merged_values
        if not query_questions:
            return [], []

        body: JSONObject = {
            "appKey": app_key,
            "role": role,
            "auditNodeId": audit_node_id,
            "queryQuestions": query_questions,
            "keyQueValues": list(key_que_values.values()),
            "qlinkerValues": [],
            "codeBlockValues": [],
            "searchKey": None,
            "manual": True,
            "graphType": "VIEW" if resolved_view.kind == "custom" else "FORM",
            "graphKey": app_key,
            "pageNum": 1,
            "pageSize": 300,
        }
        try:
            if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
                payload = self.backend.request(
                    "POST",
                    context,
                    f"/view/{resolved_view.view_selection.view_key}/que",
                    json_body=body,
                )
            else:
                payload = self.backend.request(
                    "POST",
                    context,
                    f"/data/{app_key}/relation",
                    json_body=body,
                )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable = _record_detail_unavailable_context(
                "reference_runtime_match",
                "动态引用字段匹配数据获取失败。",
                exc,
            )
            unavailable["field_ids"] = [item.get("queId") for item in query_questions]
            return [], [unavailable]

        dynamic_answers = _record_detail_dynamic_reference_answers_from_payload(
            payload,
            query_field_ids={int(item["queId"]) for item in query_questions if isinstance(item.get("queId"), int)},
        )
        for answer in dynamic_answers:
            answer["_recordGetResolutionSource"] = "dynamic_match"
        unavailable_context: list[JSONObject] = []
        if skipped_fields:
            unavailable_context.append({
                "section": "reference_runtime_match",
                "message": "部分动态引用字段缺少匹配源字段值，未执行匹配查询。",
                "category": "data",
                "fields": skipped_fields,
            })
        return dynamic_answers, unavailable_context

    def _record_get_log_visibility_context(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        role: int,
        audit_node_id: int | None,
        unavailable_context: list[JSONObject],
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        channel = _record_detail_channel_payload(
            app_key=app_key,
            resolved_view=resolved_view,
            audit_node_id=audit_node_id,
            default_channel="FORM",
        )
        try:
            visibility = self.backend.request(
                "POST",
                context,
                f"/worksheet/data/log/{app_key}/{record_id}/visible",
                json_body={
                    "viewChannel": channel.get("channel"),
                    "channelKey": channel.get("channelKey"),
                    "role": role,
                },
            )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable_context.append(_record_detail_unavailable_context("log_visibility", "侧边栏日志可见性获取失败。", exc))
            return {"status": "unavailable", "channel": channel, "data_log_visible": None, "workflow_log_visible": None}
        payload = visibility if isinstance(visibility, dict) else {}
        return {
            "status": "ok",
            "channel": channel,
            "data_log_visible": _record_detail_visibility_value(
                payload,
                keys=("rowRecordLogVisible", "dataRecordLogVisible", "visible"),
                default=True,
            ),
            "workflow_log_visible": _record_detail_visibility_value(
                payload,
                keys=("auditRecordLogVisible", "workflowRecordLogVisible", "flowRecordLogVisible", "visible"),
                default=True,
            ),
        }

    def _record_get_data_logs_context(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        role: int,
        audit_node_id: int | None,
        log_visibility: JSONObject,
        unavailable_context: list[JSONObject],
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if log_visibility.get("status") == "unavailable":
            return _record_detail_log_unavailable_payload("data_logs", "visibility_unavailable")
        if log_visibility.get("data_log_visible") is False:
            return _record_detail_log_hidden_payload()
        try:
            payload = self.backend.request(
                "POST",
                context,
                f"/worksheet/data/log/{app_key}/{record_id}/page",
                json_body={
                    "viewChannel": log_visibility.get("channel"),
                    "role": role,
                    "pageNum": 1,
                    "pageSize": RECORD_GET_DETAIL_LOG_PAGE_SIZE,
                },
            )
            return _record_detail_log_page_payload(
                payload,
                normalizer=_record_detail_data_log_item,
                source="data_logs",
            )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable_context.append(_record_detail_unavailable_context("data_logs", "最近数据日志获取失败。", exc))
            return _record_detail_log_unavailable_payload("data_logs", "fetch_unavailable")

    def _record_get_workflow_logs_context(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        role: int,
        audit_node_id: int | None,
        log_visibility: JSONObject,
        unavailable_context: list[JSONObject],
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if log_visibility.get("status") == "unavailable":
            return _record_detail_log_unavailable_payload("workflow_logs", "visibility_unavailable")
        if log_visibility.get("workflow_log_visible") is False:
            return _record_detail_log_hidden_payload()
        try:
            if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
                payload = self.backend.request(
                    "POST",
                    context,
                    f"/viewGraph/{resolved_view.view_selection.view_key}/workflow/node/record",
                    json_body={
                        "key": resolved_view.view_selection.view_key,
                        "rowRecordId": str(record_id),
                        "pageNum": 1,
                        "pageSize": RECORD_GET_DETAIL_LOG_PAGE_SIZE,
                    },
                )
            else:
                payload = self.backend.request(
                    "POST",
                    context,
                    "/application/workflow/node/record",
                    json_body={
                        "key": app_key,
                        "rowRecordId": str(record_id),
                        "nodeId": audit_node_id,
                        "role": role,
                        "pageNum": 1,
                        "pageSize": RECORD_GET_DETAIL_LOG_PAGE_SIZE,
                    },
                )
            return _record_detail_log_page_payload(
                payload,
                normalizer=_record_detail_workflow_log_item,
                source="workflow_logs",
            )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "流程日志本次获取失败。", exc))
            return _record_detail_log_unavailable_payload("workflow_logs", "fetch_unavailable")

    def _record_get_full_data_logs_context(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        role: int,
        log_visibility: JSONObject,
        unavailable_context: list[JSONObject],
        run_dir: Path,
        deadline: float,
    ) -> JSONObject:
        """读取全量数据日志并写入 JSONL。"""
        if log_visibility.get("status") == "unavailable":
            return _record_logs_unavailable_payload("data_logs", "visibility_unavailable")
        if log_visibility.get("data_log_visible") is False:
            return _record_logs_hidden_payload("data_logs")

        def fetch_page(page_num: int) -> JSONValue:
            return self.backend.request(
                "POST",
                context,
                f"/worksheet/data/log/{app_key}/{record_id}/page",
                json_body={
                    "viewChannel": log_visibility.get("channel"),
                    "role": role,
                    "pageNum": page_num,
                    "pageSize": RECORD_LOGS_PAGE_SIZE,
                },
            )

        try:
            return _record_logs_fetch_all_to_jsonl(
                fetch_page=fetch_page,
                normalizer=_record_detail_data_log_item,
                source="data_logs",
                file_path=run_dir / "data-logs.jsonl",
                deadline=deadline,
            )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable_context.append(_record_detail_unavailable_context("data_logs", "全量数据日志获取失败。", exc))
            return _record_logs_unavailable_payload("data_logs", "fetch_unavailable")

    def _record_get_full_workflow_logs_context(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        role: int,
        audit_node_id: int | None,
        log_visibility: JSONObject,
        unavailable_context: list[JSONObject],
        run_dir: Path,
        deadline: float,
    ) -> JSONObject:
        """读取全量流程日志并写入 JSONL。"""
        if log_visibility.get("status") == "unavailable":
            return _record_logs_unavailable_payload("workflow_logs", "visibility_unavailable")
        if log_visibility.get("workflow_log_visible") is False:
            return _record_logs_hidden_payload("workflow_logs")

        def fetch_page(page_num: int) -> JSONValue:
            if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
                return self.backend.request(
                    "POST",
                    context,
                    f"/viewGraph/{resolved_view.view_selection.view_key}/workflow/node/record",
                    json_body={
                        "key": resolved_view.view_selection.view_key,
                        "rowRecordId": str(record_id),
                        "pageNum": page_num,
                        "pageSize": RECORD_LOGS_PAGE_SIZE,
                    },
                )
            return self.backend.request(
                "POST",
                context,
                "/application/workflow/node/record",
                json_body={
                    "key": app_key,
                    "rowRecordId": str(record_id),
                    "nodeId": audit_node_id,
                    "role": role,
                    "pageNum": page_num,
                    "pageSize": RECORD_LOGS_PAGE_SIZE,
                },
            )

        try:
            return _record_logs_fetch_all_to_jsonl(
                fetch_page=fetch_page,
                normalizer=_record_detail_workflow_log_item,
                source="workflow_logs",
                file_path=run_dir / "workflow-logs.jsonl",
                deadline=deadline,
            )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable_context.append(_record_detail_unavailable_context("workflow_logs", "全量流程日志获取失败。", exc))
            return _record_logs_unavailable_payload("workflow_logs", "fetch_unavailable")

    def _record_get_associated_resources(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        resolved_view: AccessibleViewRoute,
        role: int,
        audit_node_id: int | None,
        unavailable_context: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        try:
            if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
                payload = self.backend.request(
                    "GET",
                    context,
                    f"/view/{app_key}/asosChart",
                    params={
                        "role": role,
                        "viewgraphKey": resolved_view.view_selection.view_key,
                        "beingConfig": False,
                    },
                )
            else:
                params: JSONObject = {"role": role, "beingDraft": False}
                if audit_node_id is not None:
                    params["auditNodeId"] = audit_node_id
                payload = self.backend.request("GET", context, f"/app/{app_key}/asosChart", params=params)
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            unavailable_context.append(_record_detail_unavailable_context("associated_resources", "关联资源获取失败。", exc))
            return []
        return [_record_detail_associated_resource(item) for item in _record_detail_associated_resource_items(payload)]

    def _record_get_media_assets(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        audit_node_id: int | None,
        fields: list[JSONObject],
        references: list[JSONObject],
    ) -> JSONObject:
        """Collect and localize image assets that the frontend detail page can render."""
        try:
            def refresh_source_url(candidate: JSONObject) -> str | None:
                return self._record_get_refreshed_media_source_url(
                    context,
                    app_key=app_key,
                    record_id=record_id,
                    resolved_view=resolved_view,
                    audit_node_id=audit_node_id,
                    candidate=candidate,
                )

            return _record_detail_media_assets_payload(
                backend=self.backend,
                context=context,
                app_key=app_key,
                record_id=record_id,
                fields=fields,
                references=references,
                refresh_source_url=refresh_source_url,
            )
        except Exception as exc:  # defensive: media should never break the core record detail.
            warning: JSONObject = {
                "code": "MEDIA_ASSETS_UNAVAILABLE",
                "message": f"record_get could not collect media assets: {exc}",
            }
            if isinstance(exc, QingflowApiError):
                warning.update(_record_detail_error_warning_fields(exc))
            return {
                "status": "unavailable",
                "local_dir": None,
                "items": [],
                "warnings": [warning],
            }

    def _record_get_file_assets(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        audit_node_id: int | None,
        fields: list[JSONObject],
        references: list[JSONObject],
        media_assets: JSONObject,
    ) -> JSONObject:
        """Collect and localize file assets from the frontend detail context."""
        try:
            def refresh_source_url(candidate: JSONObject) -> str | None:
                return self._record_get_refreshed_media_source_url(
                    context,
                    app_key=app_key,
                    record_id=record_id,
                    resolved_view=resolved_view,
                    audit_node_id=audit_node_id,
                    candidate=candidate,
                )

            return _record_detail_file_assets_payload(
                backend=self.backend,
                context=context,
                app_key=app_key,
                record_id=record_id,
                fields=fields,
                references=references,
                media_assets=media_assets,
                refresh_source_url=refresh_source_url,
            )
        except Exception as exc:  # defensive: file assets should never break the core record detail.
            warning = {
                "code": "FILE_ASSETS_UNAVAILABLE",
                "message": f"record_get could not collect file assets: {exc}",
            }
            if isinstance(exc, QingflowApiError):
                warning.update(_record_detail_error_warning_fields(exc))
            return {
                "status": "unavailable",
                "local_dir": None,
                "items": [],
                "warnings": [warning],
            }

    def _record_get_refreshed_media_source_url(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int,
        resolved_view: AccessibleViewRoute,
        audit_node_id: int | None,
        candidate: JSONObject,
    ) -> JSONValue | None:
        """Refresh the detail payload once to recover an expired attachment storage signature."""
        if candidate.get("source") not in {"attachment", "image_field", "subtable"}:
            return None
        field_id = _coerce_count(candidate.get("field_id"))
        if field_id is None:
            return None
        file_name = _normalize_optional_text(candidate.get("file_name"))
        try:
            detail_result, _list_type, _role = self._record_get_apply_detail(
                context,
                app_key=app_key,
                record_id=record_id,
                resolved_view=resolved_view,
                audit_node_id=audit_node_id,
            )
        except QingflowApiError as exc:
            return {
                "source_url": None,
                "warning": _record_detail_unavailable_context(
                    "asset_url_refresh",
                    "record_get could not refresh the record detail before downloading a private asset.",
                    exc,
                ),
            }
        for answer in _record_detail_answers(detail_result):
            if not isinstance(answer, dict) or _coerce_count(answer.get("queId")) != field_id:
                continue
            values = answer.get("values") if isinstance(answer.get("values"), list) else []
            for item in values:
                attachment = _extract_attachment_item(cast(JSONValue, item))
                if not attachment:
                    continue
                refreshed_name = _normalize_optional_text(attachment.get("name"))
                refreshed_url = _normalize_optional_text(attachment.get("value"))
                if not refreshed_url:
                    continue
                if file_name and refreshed_name == file_name:
                    return refreshed_url
                if not file_name and _record_detail_url_or_name_looks_like_image(refreshed_url, refreshed_name):
                    return refreshed_url
        return None

    def record_insert_public(
        self,
        *,
        profile: str = DEFAULT_PROFILE,
        app_key: str,
        fields: JSONObject | None = None,
        items: list[JSONObject] | None = None,
        verify_write: bool = True,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_output_profile = self._normalize_public_output_profile(output_profile)
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if items is not None:
            normalized_items = self._normalize_public_record_insert_batch_items(fields=fields, items=items)
            self._reject_record_insert_system_fields([cast(JSONObject, item["fields"]) for item in normalized_items])
            return self._record_insert_public_batch(
                profile=profile,
                app_key=app_key,
                items=normalized_items,
                verify_write=verify_write,
                output_profile=normalized_output_profile,
                tool_name="新增记录",
            )
        if fields is not None and not isinstance(fields, dict):
            raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
        normalized_fields = cast(JSONObject, fields or {})
        self._reject_record_insert_system_fields([normalized_fields])
        return self._record_insert_public_single(
            profile=profile,
            app_key=app_key,
            fields=normalized_fields,
            verify_write=verify_write,
            output_profile=normalized_output_profile,
            capture_exceptions=False,
            tool_name="新增记录",
        )

    def _reject_record_insert_system_fields(self, field_maps: list[JSONObject]) -> None:
        for row_index, field_map in enumerate(field_maps):
            for field_name in field_map:
                normalized_name = str(field_name or "").strip()
                if normalized_name in RECORD_WRITE_SYSTEM_FIELD_NAMES:
                    raise_tool_error(
                        QingflowApiError.config_error(
                            f"record_insert fields must not include built-in system field '{normalized_name}'",
                            details={
                                "error_code": "RESERVED_SYSTEM_FIELD_NAME",
                                "row_number": row_index + 1,
                                "field_name": normalized_name,
                                "reserved_field_names": sorted(RECORD_WRITE_SYSTEM_FIELD_NAMES),
                                "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.",
                            },
                        )
                    )

    def _record_insert_public_single(
        self,
        *,
        profile: str,
        app_key: str,
        fields: JSONObject,
        verify_write: bool,
        output_profile: str,
        capture_exceptions: bool,
        tool_name: str = "新增记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        submit_type_value = self._normalize_record_write_submit_type("submit")
        write_attempted = False
        try:
            raw_preflight = self._preflight_record_write(
                profile=profile,
                operation="create",
                app_key=app_key,
                apply_id=None,
                answers=[],
                fields=fields,
                force_refresh_form=False,
                view_id=None,
                list_type=None,
                view_key=None,
                view_name=None,
                tool_name=tool_name,
            )
            preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
            preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
            normalized_payload: JSONObject = self._record_write_normalized_payload(
                operation="insert",
                record_id=None,
                record_ids=[],
                normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
                submit_type=submit_type_value,
                selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
            )
            if preflight_data.get("blockers"):
                return self._record_write_blocked_response(
                    raw_preflight,
                    operation="insert",
                    normalized_payload=normalized_payload,
                    output_profile=output_profile,
                    human_review=False,
                    target_resource={"type": "record", "app_key": app_key, "record_id": None, "record_ids": []},
                )
            try:
                write_attempted = True
                raw_apply = self.record_create(
                    profile=profile,
                    app_key=app_key,
                    answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
                    fields={},
                    submit_type=submit_type_value,
                    verify_write=verify_write,
                    force_refresh_form=preflight_used_force_refresh,
                )
            except QingflowApiError as exc:
                self._raise_record_write_permission_error(
                    exc,
                    operation="insert",
                    app_key=app_key,
                    record_id=None,
                    selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
                )
                raise
            return self._record_write_apply_response(
                raw_apply,
                operation="insert",
                normalized_payload=normalized_payload,
                output_profile=output_profile,
                human_review=False,
                preflight=raw_preflight,
            )
        except (QingflowApiError, RuntimeError) as exc:
            if not capture_exceptions:
                raise
            return self._record_write_exception_response(
                exc,
                operation="insert",
                profile=profile,
                app_key=app_key,
                record_id=None,
                output_profile=output_profile,
                human_review=False,
                write_executed=write_attempted,
            )

    def _normalize_public_record_insert_batch_items(
        self,
        *,
        fields: JSONObject | None,
        items: list[JSONObject] | None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if fields is not None:
            raise_tool_error(QingflowApiError.config_error("record_insert batch mode does not accept fields"))
        if not isinstance(items, list) or not items:
            raise_tool_error(QingflowApiError.config_error("items must be a non-empty list"))
        normalized_items: list[JSONObject] = []
        for index, item in enumerate(items):
            if not isinstance(item, dict):
                raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
            item_fields = item.get("fields")
            if not isinstance(item_fields, dict):
                raise_tool_error(QingflowApiError.config_error(f"items[{index}].fields must be an object map keyed by field title"))
            normalized_items.append({"fields": cast(JSONObject, item_fields)})
        return normalized_items

    def _record_insert_public_batch(
        self,
        *,
        profile: str,
        app_key: str,
        items: list[JSONObject],
        verify_write: bool,
        output_profile: str,
        tool_name: str = "新增记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        responses: list[JSONObject] = []
        for item in items:
            responses.append(
                self._record_insert_public_single(
                    profile=profile,
                    app_key=app_key,
                    fields=cast(JSONObject, item["fields"]),
                    verify_write=verify_write,
                    output_profile=output_profile,
                    capture_exceptions=True,
                    tool_name=tool_name,
                )
            )
        return self._record_insert_batch_response(
            profile=profile,
            app_key=app_key,
            responses=responses,
            output_profile=output_profile,
        )

    def _record_insert_batch_response(
        self,
        *,
        profile: str,
        app_key: str,
        responses: list[JSONObject],
        output_profile: str,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        items = [
            self._record_insert_batch_item_from_response(index=index, response=response, output_profile=output_profile)
            for index, response in enumerate(responses)
        ]
        summary = self._record_insert_batch_summary(items)
        status, ok, message = self._record_insert_batch_envelope_status(summary=summary)
        first_response = responses[0] if responses else {}
        created_record_ids = [
            cast(str, item["record_id"])
            for item in items
            if isinstance(item.get("record_id"), str) and item.get("record_id")
        ]
        write_executed = any(bool(item.get("write_executed")) for item in items)
        verification_status = self._record_insert_batch_verification_status(items)
        return {
            "profile": first_response.get("profile", profile),
            "ws_id": first_response.get("ws_id"),
            "ok": ok,
            "status": status,
            "mode": "batch",
            "total": summary["total"],
            "succeeded": summary["succeeded"],
            "failed": summary["failed"],
            "created_record_ids": created_record_ids,
            "write_executed": write_executed,
            "verification_status": verification_status,
            "safe_to_retry": not write_executed,
            "request_route": first_response.get("request_route"),
            "warnings": [],
            "output_profile": output_profile,
            "items": items,
            "data": {
                "app_key": app_key,
                "mode": "batch",
                "summary": summary,
                "created_record_ids": created_record_ids,
                "items": items,
            },
            "message": message,
        }

    def _record_insert_batch_summary(self, items: list[JSONObject]) -> JSONObject:
        """执行内部辅助逻辑。"""
        created = [item for item in items if isinstance(item.get("record_id"), str) and item.get("record_id")]
        failed = [item for item in items if item.get("status") not in {"success", "verification_failed"}]
        return {
            "total": len(items),
            "succeeded": len(created),
            "failed": len(failed),
            "created_count": len(created),
            "blocked_count": sum(1 for item in items if item.get("status") == "blocked"),
            "confirmation_count": sum(1 for item in items if item.get("status") == "needs_confirmation"),
            "verification_failed_count": sum(1 for item in items if item.get("status") == "verification_failed"),
        }

    def _record_insert_batch_envelope_status(self, *, summary: JSONObject) -> tuple[str, bool, str]:
        """执行内部辅助逻辑。"""
        succeeded = int(summary["succeeded"])
        failed = int(summary["failed"])
        if succeeded and failed:
            return "partial_success", False, "batch insert completed with partial failures"
        if succeeded and int(summary["verification_failed_count"]):
            return "verification_failed", True, "batch insert completed but verification failed for some created records"
        if succeeded:
            return "success", True, "batch insert completed"
        if int(summary["confirmation_count"]):
            return "needs_confirmation", False, "batch insert requires confirmation before retrying failed rows"
        if int(summary["blocked_count"]):
            return "blocked", False, "batch insert preflight blocked all rows"
        return "failed", False, "batch insert failed"

    def _record_insert_batch_verification_status(self, items: list[JSONObject]) -> str:
        """执行内部辅助逻辑。"""
        statuses = {str(item.get("verification_status") or "not_requested") for item in items}
        if "failed" in statuses:
            return "failed"
        if "verified" in statuses:
            return "verified"
        return "not_requested"

    def _record_insert_batch_item_from_response(
        self,
        *,
        index: int,
        response: JSONObject,
        output_profile: str,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        data = cast(JSONObject, response.get("data", {})) if isinstance(response.get("data"), dict) else {}
        resource = _public_record_resource(data.get("resource"))
        record_id = _public_record_id_text(response.get("record_id"))
        apply_id = _public_record_id_text(response.get("apply_id"))
        if record_id is None and isinstance(resource, dict):
            record_id = _public_record_id_text(resource.get("record_id"))
        if apply_id is None and isinstance(resource, dict):
            apply_id = _public_record_id_text(resource.get("apply_id"))
        item: JSONObject = {
            "index": index,
            "row_number": index + 1,
            "status": response.get("status"),
            "write_executed": bool(response.get("write_executed")),
            "verification_status": response.get("verification_status", "not_requested"),
            "safe_to_retry": bool(response.get("safe_to_retry", True)),
        }
        if record_id is not None:
            item["record_id"] = record_id
        if apply_id is not None:
            item["apply_id"] = apply_id
        if resource:
            item["resource"] = resource
        verification = data.get("verification")
        if isinstance(verification, dict):
            compact_verification = {
                key: verification[key]
                for key in ("verified", "verification_mode", "field_level_verified")
                if key in verification
            }
            if compact_verification:
                item["verification"] = compact_verification
        field_errors = cast(list[JSONObject], data.get("field_errors", [])) if isinstance(data.get("field_errors"), list) else []
        confirmation_requests = cast(list[JSONObject], data.get("confirmation_requests", [])) if isinstance(data.get("confirmation_requests"), list) else []
        failed_fields = self._record_write_failed_fields(field_errors=field_errors, confirmation_requests=confirmation_requests)
        if failed_fields:
            item["failed_fields"] = failed_fields
        if confirmation_requests:
            item["confirmation_requests"] = [
                self._record_write_semantic_confirmation_request(request)
                for request in confirmation_requests
                if isinstance(request, dict)
            ]
        blockers = data.get("blockers")
        if isinstance(blockers, list) and blockers:
            item["blockers"] = blockers
        warnings = response.get("warnings")
        if isinstance(warnings, list) and warnings:
            item["warnings"] = warnings
        error = data.get("error")
        if isinstance(error, dict):
            item["error"] = error
        if output_profile == "verbose" and isinstance(data.get("debug"), dict):
            item["debug"] = data.get("debug")
        return item

    def _record_write_failed_fields(
        self,
        *,
        field_errors: list[JSONObject],
        confirmation_requests: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        failed_fields = [
            self._record_write_semantic_field_error(error)
            for error in field_errors
            if isinstance(error, dict)
        ]
        failed_fields.extend(
            self._record_write_failed_field_from_confirmation(request)
            for request in confirmation_requests
            if isinstance(request, dict)
        )
        return failed_fields

    def _record_write_semantic_field_error(self, error: JSONObject) -> JSONObject:
        """执行内部辅助逻辑。"""
        field = error.get("field")
        field_payload = cast(JSONObject, field) if isinstance(field, dict) else {}
        error_code = _normalize_optional_text(error.get("error_code")) or "INVALID_FIELD_VALUE"
        title = (
            _normalize_optional_text(field_payload.get("que_title"))
            or _normalize_optional_text(field_payload.get("title"))
            or _normalize_optional_text(error.get("location"))
            or "unknown field"
        )
        field_id = (
            field_payload.get("que_id")
            if field_payload.get("que_id") is not None
            else field_payload.get("field_id")
        )
        expected_format = error.get("expected_format") if isinstance(error.get("expected_format"), dict) else None
        if expected_format is None:
            expected_format = self._record_write_expected_format_from_field_payload(field_payload)
        payload: JSONObject = {
            "title": title,
            "field_id": field_id,
            "error_code": error_code,
            "message": self._record_write_semantic_error_message(error_code, error.get("message")),
            "next_action": self._record_write_next_action_for_error(
                error_code,
                expected_format=expected_format,
                field_payload=field_payload,
            ),
        }
        if expected_format is not None:
            payload["expected_format"] = expected_format
            payload["example_value"] = self._record_write_example_value_for_format(expected_format, field_payload)
        if error.get("received_value") is not None:
            payload["received_value"] = error.get("received_value")
        if error.get("fix_hint") is not None:
            payload["fix_hint"] = error.get("fix_hint")
        if error.get("details") is not None:
            payload["details"] = error.get("details")
        return payload

    def _record_write_semantic_confirmation_request(self, request: JSONObject) -> JSONObject:
        """执行内部辅助逻辑。"""
        field_ref = request.get("field_ref")
        field_payload = cast(JSONObject, field_ref) if isinstance(field_ref, dict) else {}
        payload: JSONObject = {
            "field": request.get("field"),
            "title": _normalize_optional_text(request.get("field")) or _normalize_optional_text(field_payload.get("que_title")),
            "field_id": field_payload.get("que_id"),
            "kind": request.get("kind"),
            "input": request.get("input"),
            "candidates": request.get("candidates", []),
            "next_action": "让用户确认候选，或用显式 id/object 只重试本行。",
        }
        if request.get("parent_field") is not None:
            payload["parent_field"] = request.get("parent_field")
        if request.get("row_ordinal") is not None:
            payload["row_ordinal"] = request.get("row_ordinal")
        return payload

    def _record_write_failed_field_from_confirmation(self, request: JSONObject) -> JSONObject:
        """执行内部辅助逻辑。"""
        semantic = self._record_write_semantic_confirmation_request(request)
        return {
            "title": semantic.get("title") or semantic.get("field"),
            "field_id": semantic.get("field_id"),
            "error_code": "LOOKUP_NEEDS_CONFIRMATION",
            "message": "候选不唯一，需要用户确认。",
            "kind": semantic.get("kind"),
            "input": semantic.get("input"),
            "candidates": semantic.get("candidates", []),
            "next_action": semantic.get("next_action"),
        }

    def _record_write_expected_format_from_field_payload(self, field_payload: JSONObject) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        que_type = _coerce_count(field_payload.get("que_type"))
        if que_type is None:
            return None
        synthetic_field = FormField(
            que_id=_coerce_count(field_payload.get("que_id")) or 0,
            que_title=_normalize_optional_text(field_payload.get("que_title")) or _normalize_optional_text(field_payload.get("title")) or "",
            que_type=que_type,
            required=False,
            readonly=False,
            system=False,
            options=[],
            aliases=[],
            target_app_key=None,
            target_app_name_hint=None,
            member_select_scope_type=None,
            member_select_scope=None,
            dept_select_scope_type=None,
            dept_select_scope=None,
            raw={},
        )
        return _write_format_for_field(synthetic_field)

    def _record_write_example_value_for_format(self, expected_format: JSONObject, field_payload: JSONObject) -> JSONValue:
        """执行内部辅助逻辑。"""
        examples = expected_format.get("examples")
        if isinstance(examples, list) and examples:
            return cast(JSONValue, examples[0])
        kind = _normalize_optional_text(expected_format.get("kind"))
        if kind == "member_list":
            return "张三"
        if kind == "department_list":
            return "直销部"
        if kind == "relation_record":
            return {"apply_id": "5001"}
        if kind == "attachment_list":
            return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
        if kind == "subtable_rows":
            return {"rows": [{"子字段": "值"}]}
        if kind == "date_string":
            return "2026-03-13 10:00:00"
        if kind == "boolean_label":
            return "是"
        if kind in {"single_select", "multi_select"}:
            options = expected_format.get("options")
            if isinstance(options, list) and options:
                return cast(JSONValue, options[0])
        que_type = _coerce_count(field_payload.get("que_type"))
        if que_type in NUMBER_QUE_TYPES:
            return 100
        return "文本"

    def _record_write_semantic_error_message(self, error_code: str, fallback: JSONValue) -> str:
        """执行内部辅助逻辑。"""
        if error_code == "MISSING_REQUIRED_FIELD":
            return "缺少必填字段。"
        if error_code == "FIELD_NOT_FOUND":
            return "字段不存在或字段标题不匹配。"
        if error_code == "AMBIGUOUS_FIELD":
            return "字段标题存在歧义。"
        if error_code in {"INVALID_FIELD_VALUE", "INVALID_MEMBER_VALUE", "INVALID_DEPARTMENT_VALUE", "INVALID_RELATION_VALUE"}:
            return _normalize_optional_text(fallback) or "字段值格式不正确。"
        return _normalize_optional_text(fallback) or "字段写入失败。"

    def _record_write_next_action_for_error(
        self,
        error_code: str,
        *,
        expected_format: JSONObject | None = None,
        field_payload: JSONObject | None = None,
    ) -> str:
        """执行内部辅助逻辑。"""
        kind = _normalize_optional_text((expected_format or {}).get("kind"))
        field_title = (
            _normalize_optional_text((field_payload or {}).get("que_title"))
            or _normalize_optional_text((field_payload or {}).get("title"))
            or "该字段"
        )
        if kind == "member_list":
            return f"{field_title} 需要有效成员；用唯一姓名/邮箱/id，歧义时先调用 record_member_candidates 后只重试本行。"
        if kind == "department_list":
            return f"{field_title} 需要有效部门；用候选范围内的部门名/id/object，歧义或不确定时先调用 record_department_candidates 后只重试本行。"
        if kind == "relation_record":
            return f"{field_title} 需要关联记录；优先改用目标记录 record_id/apply_id，或使用可唯一命中的显示文本后只重试本行。"
        if kind == "attachment_list":
            return f"{field_title} 需要附件值；先用 file_upload_local 上传文件，再写返回的 value/url 后只重试本行。"
        if kind in {"single_select", "multi_select"}:
            return f"{field_title} 需要选项值；使用 schema options 中的标签或 option id，修正后只重试本行。"
        if error_code == "MISSING_REQUIRED_FIELD":
            return "补充该字段后只重试本行。"
        if error_code in {"FIELD_NOT_FOUND", "AMBIGUOUS_FIELD"}:
            return "重新调用 schema 工具确认字段标题或 field_id 后，只重试本行。"
        return "修正该字段值后只重试本行。"

    def record_update_public(
        self,
        *,
        profile: str = DEFAULT_PROFILE,
        app_key: str,
        record_id: Any | None,
        fields: JSONObject | None = None,
        items: list[JSONObject] | None = None,
        view_id: str | None = None,
        dry_run: bool = False,
        verify_write: bool = True,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_output_profile = self._normalize_public_output_profile(output_profile)
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if items is not None:
            if dry_run not in {True, False}:
                raise_tool_error(QingflowApiError.config_error("dry_run must be boolean"))
            normalized_items = self._normalize_public_record_update_batch_items(
                record_id=record_id,
                fields=fields,
                items=items,
            )
            return self._record_update_public_batch(
                profile=profile,
                app_key=app_key,
                items=normalized_items,
                view_id=view_id,
                dry_run=dry_run,
                verify_write=verify_write,
                output_profile=normalized_output_profile,
                tool_name="更新记录",
            )
        if dry_run:
            raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
        if record_id is None:
            raise_tool_error(QingflowApiError.config_error("record_id is required"))
        record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
        if fields is not None and not isinstance(fields, dict):
            raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
        return self._record_update_public_single(
            profile=profile,
            app_key=app_key,
            record_id=record_id_int,
            fields=cast(JSONObject, fields or {}),
            view_id=view_id,
            verify_write=verify_write,
            output_profile=normalized_output_profile,
            tool_name="更新记录",
        )

    def _record_update_public_single(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: int,
        fields: JSONObject,
        view_id: str | None,
        verify_write: bool,
        output_profile: str,
        capture_exceptions: bool = False,
        tool_name: str = "更新记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        write_state = {"attempted": False}
        try:
            return self._record_update_public_single_impl(
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                fields=fields,
                view_id=view_id,
                verify_write=verify_write,
                output_profile=output_profile,
                write_attempted_ref=lambda value: write_state.__setitem__("attempted", value),
                tool_name=tool_name,
            )
        except (QingflowApiError, RuntimeError) as exc:
            if not capture_exceptions:
                raise
            return self._record_write_exception_response(
                exc,
                operation="update",
                profile=profile,
                app_key=app_key,
                record_id=record_id,
                output_profile=output_profile,
                human_review=True,
                write_executed=write_state["attempted"],
            )

    def _record_update_public_single_impl(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: int,
        fields: JSONObject,
        view_id: str | None,
        verify_write: bool,
        output_profile: str,
        write_attempted_ref: Callable[[bool], None],
        tool_name: str = "更新记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        preflight_kwargs: dict[str, Any] = {
            "profile": profile,
            "app_key": app_key,
            "record_id": record_id,
            "fields": fields,
            "force_refresh_form": False,
        }
        if view_id is not None:
            preflight_kwargs["preferred_view_id"] = view_id
        preflight_kwargs["tool_name"] = tool_name
        raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
        preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
        preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
        normalized_payload = self._record_write_normalized_payload(
            operation="update",
            record_id=record_id,
            record_ids=[],
            normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
            submit_type=1,
            selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
        )
        if preflight_data.get("blockers"):
            return self._record_write_blocked_response(
                raw_preflight,
                operation="update",
                normalized_payload=normalized_payload,
                output_profile=output_profile,
                human_review=True,
                target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
            )
        write_attempted_ref(True)
        route_apply, tried_routes, route_blocker = self._record_update_apply_with_auto_route(
            profile=profile,
            app_key=app_key,
            record_id=record_id,
            normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
            preflight_data=preflight_data,
            verify_write=verify_write,
            force_refresh_form=preflight_used_force_refresh,
            tool_name=tool_name,
        )
        if route_blocker is not None:
            return self._record_update_route_blocked_response(
                raw_preflight=raw_preflight,
                operation="update",
                normalized_payload=normalized_payload,
                output_profile=output_profile,
                human_review=True,
                app_key=app_key,
                record_id=record_id,
                tried_routes=tried_routes,
                route_blocker=route_blocker,
            )
        raw_apply = cast(JSONObject, route_apply)
        return self._record_write_apply_response(
            raw_apply,
            operation="update",
            normalized_payload=normalized_payload,
            output_profile=output_profile,
            human_review=True,
            preflight=raw_preflight,
        )

    def _record_update_apply_with_auto_route(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: int,
        normalized_answers: list[JSONObject],
        preflight_data: JSONObject,
        verify_write: bool,
        force_refresh_form: bool,
        tool_name: str = "更新记录",
    ) -> tuple[JSONObject | None, list[JSONObject], JSONObject | None]:
        """Try record update routes in the same order a frontend user would expect."""
        tried_routes: list[JSONObject] = []
        admin_attempt = self._record_update_route_attempt(
            route_type="admin_direct",
            endpoint_kind="app_apply_update",
            role=1,
            reason="try data-manager direct edit first",
        )
        try:
            raw_apply = self.record_update(
                profile=profile,
                app_key=app_key,
                apply_id=record_id,
                answers=normalized_answers,
                fields={},
                role=1,
                verify_write=verify_write,
                force_refresh_form=force_refresh_form,
            )
            admin_attempt["status"] = "success"
            raw_apply["update_route"] = self._record_update_route_public(admin_attempt)
            raw_apply["tried_routes"] = [admin_attempt]
            return raw_apply, [admin_attempt], None
        except (QingflowApiError, RuntimeError) as exc:
            api_error = self._record_update_extract_api_error(exc)
            if api_error is None or not self._record_update_route_permission_denied(api_error):
                raise
            admin_attempt.update(self._record_update_route_error_payload(
                api_error,
                status="denied",
                error_code="ADMIN_UPDATE_PERMISSION_DENIED",
            ))
            tried_routes.append(admin_attempt)

        view_route = self._record_update_selected_custom_view_route(preflight_data)
        if view_route is None:
            tried_routes.append(
                self._record_update_route_attempt(
                    route_type="view_edit",
                    endpoint_kind="view_apply_update",
                    status="skipped",
                    error_code="VIEW_UPDATE_ROUTE_NOT_AVAILABLE",
                    reason="preflight did not select a single custom view route for this payload",
                )
            )
        else:
            view_attempt = self._record_update_route_attempt(
                route_type="view_edit",
                endpoint_kind="view_apply_update",
                view_id=cast(str, view_route.get("view_id")),
                view_key=cast(str, view_route.get("view_key")),
                view_name=_normalize_optional_text(view_route.get("name")),
                reason="fallback to frontend custom-view detail edit route",
            )
            try:
                raw_apply = self._record_update_via_custom_view(
                    profile=profile,
                    app_key=app_key,
                    apply_id=record_id,
                    view_key=cast(str, view_route["view_key"]),
                    answers=normalized_answers,
                    verify_write=verify_write,
                    force_refresh_form=force_refresh_form,
                    tool_name=tool_name,
                )
                view_attempt["status"] = "success"
                tried_routes.append(view_attempt)
                raw_apply["update_route"] = self._record_update_route_public(view_attempt)
                raw_apply["tried_routes"] = tried_routes
                return raw_apply, tried_routes, None
            except (QingflowApiError, RuntimeError) as exc:
                api_error = self._record_update_extract_api_error(exc)
                if api_error is None or not self._record_update_route_permission_denied(api_error):
                    raise
                view_attempt.update(self._record_update_route_error_payload(
                    api_error,
                    status="denied",
                    error_code="VIEW_UPDATE_PERMISSION_DENIED",
                ))
                tried_routes.append(view_attempt)

        task_route = self._record_update_task_save_only_candidate(
            profile=profile,
            app_key=app_key,
            record_id=record_id,
            normalized_answers=normalized_answers,
        )
        task_attempt = cast(JSONObject, task_route.get("attempt") if isinstance(task_route.get("attempt"), dict) else {})
        if not task_route.get("available"):
            tried_routes.append(task_attempt or self._record_update_route_attempt(
                route_type="task_save_only",
                endpoint_kind="workflow_node_save_only",
                status="skipped",
                error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
                reason="no unique current-user todo task can edit the requested fields",
            ))
        else:
            task_attempt = self._record_update_route_attempt(
                route_type="task_save_only",
                endpoint_kind="workflow_node_save_only",
                role=3,
                task_id=_normalize_optional_text(task_route.get("task_id")),
                workflow_node_id=_coerce_count(task_route.get("workflow_node_id")),
                reason="fallback to current-user workflow todo save-only route",
            )
            try:
                raw_apply = self._record_update_via_task_save_only(
                    profile=profile,
                    app_key=app_key,
                    apply_id=record_id,
                    workflow_node_id=cast(int, task_route["workflow_node_id"]),
                    answers=normalized_answers,
                    verify_write=verify_write,
                    force_refresh_form=force_refresh_form,
                    tool_name=tool_name,
                )
                task_attempt["status"] = "success"
                tried_routes.append(task_attempt)
                raw_apply["update_route"] = self._record_update_route_public(task_attempt)
                raw_apply["tried_routes"] = tried_routes
                return raw_apply, tried_routes, None
            except (QingflowApiError, RuntimeError) as exc:
                api_error = self._record_update_extract_api_error(exc)
                if api_error is None or not self._record_update_route_permission_denied(api_error):
                    raise
                task_attempt.update(self._record_update_route_error_payload(
                    api_error,
                    status="denied",
                    error_code="TASK_UPDATE_PERMISSION_DENIED",
                ))
                tried_routes.append(task_attempt)
        return None, tried_routes, {
            "error_code": "NO_AVAILABLE_UPDATE_ROUTE",
            "message": "No available record update route could execute this payload for the current user.",
            "recommended_next_actions": [
                "If this user should edit the record as a data manager, grant data edit permission and retry record_update.",
                "If the record is editable from a specific table view in the UI, make sure the target fields are visible and editable in that view.",
                "If this is workflow work, use task_list -> task_get -> task_action_execute(action='save_only') with the current task context.",
            ],
        }

    def _record_update_selected_custom_view_route(self, preflight_data: JSONObject) -> JSONObject | None:
        selection = preflight_data.get("selection")
        if not isinstance(selection, dict):
            return None
        view = selection.get("view")
        if not isinstance(view, dict):
            return None
        view_id = _normalize_optional_text(view.get("view_id"))
        if not view_id or not view_id.startswith("custom:"):
            return None
        view_key = _normalize_optional_text(view.get("view_key")) or view_id.split(":", 1)[1].strip()
        if not view_key:
            return None
        return {
            "view_id": view_id,
            "view_key": view_key,
            "name": view.get("name"),
        }

    def _record_update_route_attempt(
        self,
        *,
        route_type: str,
        endpoint_kind: str,
        status: str = "attempted",
        role: int | None = None,
        task_id: str | None = None,
        workflow_node_id: int | None = None,
        view_id: str | None = None,
        view_key: str | None = None,
        view_name: str | None = None,
        error_code: str | None = None,
        reason: str | None = None,
    ) -> JSONObject:
        payload: JSONObject = {
            "route_type": route_type,
            "endpoint_kind": endpoint_kind,
            "status": status,
        }
        if role is not None:
            payload["role"] = role
        if task_id:
            payload["task_id"] = task_id
        if workflow_node_id is not None:
            payload["workflow_node_id"] = workflow_node_id
        if view_id:
            payload["view_id"] = view_id
        if view_key:
            payload["view_key"] = view_key
        if view_name:
            payload["view_name"] = view_name
        if error_code:
            payload["error_code"] = error_code
        if reason:
            payload["reason"] = reason
        return payload

    def _record_update_route_public(self, attempt: JSONObject) -> JSONObject:
        return _pick_route_payload(attempt)

    def _record_update_route_error_payload(
        self,
        exc: QingflowApiError,
        *,
        status: str,
        error_code: str,
    ) -> JSONObject:
        payload: JSONObject = {
            "status": status,
            "error_code": error_code,
            "message": exc.message,
        }
        if exc.backend_code is not None:
            payload["backend_code"] = exc.backend_code
        if exc.http_status is not None:
            payload["http_status"] = exc.http_status
        if exc.request_id is not None:
            payload["request_id"] = exc.request_id
        return payload

    def _record_update_extract_api_error(self, exc: QingflowApiError | RuntimeError) -> QingflowApiError | None:
        if isinstance(exc, QingflowApiError):
            return exc
        try:
            payload = json.loads(str(exc))
        except json.JSONDecodeError:
            return None
        if not isinstance(payload, dict):
            return None
        return QingflowApiError(
            category=str(payload.get("category") or "backend"),
            message=str(payload.get("message") or exc),
            backend_code=payload.get("backend_code"),
            request_id=_normalize_optional_text(payload.get("request_id")),
            http_status=_coerce_count(payload.get("http_status")),
            details=cast(JSONObject | None, payload.get("details") if isinstance(payload.get("details"), dict) else None),
        )

    def _record_update_route_permission_denied(self, exc: QingflowApiError) -> bool:
        if is_auth_like_error(exc):
            return False
        if backend_code_int(exc) in {40002, 40027, 40038, 404}:
            return True
        if exc.http_status == 404:
            return True
        return False

    def _record_update_route_blocked_response(
        self,
        *,
        raw_preflight: JSONObject,
        operation: str,
        normalized_payload: JSONObject,
        output_profile: str,
        human_review: bool,
        app_key: str,
        record_id: int,
        tried_routes: list[JSONObject],
        route_blocker: JSONObject,
    ) -> JSONObject:
        plan_data = cast(JSONObject, raw_preflight.get("data", {}))
        validation = cast(JSONObject, plan_data.get("validation", {}))
        warnings_payload = validation.get("warnings", [])
        warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
        warnings.append(
            {
                "code": cast(str, route_blocker.get("error_code") or "NO_AVAILABLE_UPDATE_ROUTE"),
                "message": cast(str, route_blocker.get("message") or "No update route could execute the write."),
            }
        )
        recommended = list(route_blocker.get("recommended_next_actions") or [])
        response: JSONObject = {
            "profile": raw_preflight.get("profile"),
            "ws_id": raw_preflight.get("ws_id"),
            "ok": False,
            "status": "blocked",
            "write_executed": False,
            "verification_status": "not_requested",
            "safe_to_retry": True,
            "request_route": raw_preflight.get("request_route"),
            "warnings": warnings,
            "output_profile": output_profile,
            "update_route": None,
            "tried_routes": tried_routes,
            "error_code": route_blocker.get("error_code"),
            "data": {
                "action": {"operation": operation, "executed": False},
                "resource": {"type": "record", "app_key": app_key, "record_id": stringify_backend_id(record_id), "record_ids": []},
                "verification": None,
                "normalized_payload": normalized_payload,
                "blockers": [route_blocker.get("error_code") or "NO_AVAILABLE_UPDATE_ROUTE"],
                "field_errors": [],
                "confirmation_requests": [],
                "resolved_fields": cast(list[JSONObject], plan_data.get("lookup_resolved_fields", [])),
                "recommended_next_actions": recommended,
                "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
                "error": route_blocker,
                "update_route": None,
                "tried_routes": tried_routes,
            },
        }
        if output_profile == "verbose":
            response["data"]["debug"] = {"preflight": plan_data}
        return response

    def _record_update_task_save_only_candidate(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: int,
        normalized_answers: list[JSONObject],
    ) -> JSONObject:
        requested_question_ids = self._record_update_answer_question_ids(normalized_answers)

        def unavailable(*, status: str = "skipped", error_code: str, reason: str, extra: JSONObject | None = None) -> JSONObject:
            attempt = self._record_update_route_attempt(
                route_type="task_save_only",
                endpoint_kind="workflow_node_save_only",
                status=status,
                error_code=error_code,
                reason=reason,
            )
            if extra:
                attempt.update(extra)
            return {"available": False, "attempt": attempt}

        def runner(session_profile, context):
            matches: list[JSONObject] = []
            pages_scanned = 0
            for page_num in range(1, VERIFY_TASK_FALLBACK_MAX_PAGES + 1):
                try:
                    task_page = self.backend.request(
                        "POST",
                        context,
                        "/task/dynamic/page",
                        json_body={
                            "type": 1,
                            "processStatus": 1,
                            "appKey": app_key,
                            "pageNum": page_num,
                            "pageSize": VERIFY_TASK_FALLBACK_PAGE_SIZE,
                        },
                    )
                except QingflowApiError as exc:
                    if not _is_optional_record_auxiliary_lookup_error(exc):
                        raise
                    return unavailable(
                        error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
                        reason="current-user todo task list is unavailable",
                        extra=self._record_update_route_error_payload(
                            exc,
                            status="skipped",
                            error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
                        ),
                    )
                pages_scanned += 1
                rows = task_page.get("list") if isinstance(task_page, dict) else None
                items = [item for item in rows if isinstance(item, dict)] if isinstance(rows, list) else []
                for item in items:
                    candidate_record_id = _coerce_count(item.get("rowRecordId") or item.get("recordId") or item.get("applyId"))
                    if candidate_record_id == record_id:
                        matches.append(dict(item))
                if not _page_has_more(cast(JSONObject, task_page if isinstance(task_page, dict) else {}), page_num, VERIFY_TASK_FALLBACK_PAGE_SIZE, len(items)):
                    break

            if not matches:
                return unavailable(
                    error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
                    reason="no current-user todo task was found for this record",
                    extra={"pages_scanned": pages_scanned},
                )
            if len(matches) > 1:
                return unavailable(
                    error_code="TASK_UPDATE_ROUTE_AMBIGUOUS",
                    reason="multiple current-user todo tasks match this record; refusing to guess workflow context",
                    extra={"matched_tasks": [self._record_update_compact_task_match(item) for item in matches[:5]]},
                )

            task = matches[0]
            workflow_node_id = _coerce_count(task.get("nodeId") or task.get("auditNodeId"))
            if workflow_node_id is None:
                return unavailable(
                    error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
                    reason="matched todo task does not expose a workflow node id",
                    extra={"matched_task": self._record_update_compact_task_match(task)},
                )
            try:
                editable_payload = self.backend.request(
                    "GET",
                    context,
                    f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
                )
            except QingflowApiError as exc:
                if not _is_optional_record_auxiliary_lookup_error(exc):
                    raise
                return unavailable(
                    error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
                    reason="workflow node editable field list is unavailable; record_update will not guess task editability",
                    extra=self._record_update_route_error_payload(
                        exc,
                        status="skipped",
                        error_code="TASK_EDITABLE_FIELDS_UNAVAILABLE",
                    ),
                )
            editable_question_ids = self._record_update_extract_question_ids(editable_payload)
            if not editable_question_ids:
                return unavailable(
                    error_code="TASK_UPDATE_ROUTE_NOT_AVAILABLE",
                    reason="workflow node editable field list is empty",
                    extra={
                        "task_id": stringify_backend_id(task.get("id") or task.get("taskId")),
                        "workflow_node_id": workflow_node_id,
                    },
                )
            effective_editable_question_ids = self._record_update_effective_task_editable_ids(
                editable_question_ids,
                normalized_answers=normalized_answers,
            )
            non_editable = sorted(
                question_id for question_id in requested_question_ids
                if question_id not in effective_editable_question_ids
            )
            if non_editable:
                return unavailable(
                    status="denied",
                    error_code="TASK_UPDATE_FIELD_NOT_EDITABLE",
                    reason="one or more requested fields are not editable on the current workflow node",
                    extra={
                        "task_id": stringify_backend_id(task.get("id") or task.get("taskId")),
                        "workflow_node_id": workflow_node_id,
                        "non_editable_question_ids": non_editable,
                    },
                )
            return {
                "available": True,
                "task_id": stringify_backend_id(task.get("id") or task.get("taskId")),
                "workflow_node_id": workflow_node_id,
                "matched_task": self._record_update_compact_task_match(task),
                "editable_question_ids": sorted(editable_question_ids),
                "effective_editable_question_ids": sorted(effective_editable_question_ids),
            }

        return self._run_record_tool(profile, runner, tool_name="更新记录")

    def _record_update_compact_task_match(self, item: JSONObject) -> JSONObject:
        return {
            key: value
            for key, value in {
                "task_id": stringify_backend_id(item.get("id") or item.get("taskId")),
                "record_id": stringify_backend_id(item.get("rowRecordId") or item.get("recordId") or item.get("applyId")),
                "workflow_node_id": item.get("nodeId") or item.get("auditNodeId"),
                "workflow_node_name": item.get("nodeName") or item.get("auditNodeName"),
            }.items()
            if value not in (None, "", [], {})
        }

    def _record_update_answer_question_ids(self, answers: list[JSONObject]) -> set[int]:
        question_ids: set[int] = set()
        for answer in answers:
            if not isinstance(answer, dict):
                continue
            que_id = _coerce_count(answer.get("queId"))
            if que_id is not None and que_id > 0:
                question_ids.add(que_id)
            table_values = answer.get("tableValues")
            if not isinstance(table_values, list):
                continue
            for row in table_values:
                if not isinstance(row, list):
                    continue
                for cell in row:
                    if not isinstance(cell, dict):
                        continue
                    cell_que_id = _coerce_count(cell.get("queId"))
                    if cell_que_id is not None and cell_que_id > 0:
                        question_ids.add(cell_que_id)
        return question_ids

    def _record_update_extract_question_ids(self, payload: JSONValue) -> set[int]:
        candidates: list[Any] = []
        if isinstance(payload, list):
            candidates = payload
        elif isinstance(payload, dict):
            for key in ("editableQueIds", "editableQuestionIds", "queIds", "questionIds", "ids", "list", "result"):
                value = payload.get(key)
                if isinstance(value, list):
                    candidates = value
                    break
        question_ids: set[int] = set()
        for item in candidates:
            value: Any = item
            if isinstance(item, dict):
                value = item.get("queId", item.get("questionId", item.get("id")))
            que_id = _coerce_count(value)
            if que_id is not None and que_id > 0:
                question_ids.add(que_id)
        return question_ids

    def _record_update_effective_task_editable_ids(
        self,
        editable_question_ids: set[int],
        *,
        normalized_answers: list[JSONObject],
    ) -> set[int]:
        effective_editable_ids = set(editable_question_ids)
        for answer in normalized_answers:
            if not isinstance(answer, dict):
                continue
            parent_que_id = _coerce_count(answer.get("queId"))
            if parent_que_id is None or parent_que_id <= 0:
                continue
            table_values = answer.get("tableValues")
            if not isinstance(table_values, list) or not table_values:
                continue
            row_subfield_ids: set[int] = set()
            for row in table_values:
                if not isinstance(row, list):
                    continue
                for cell in row:
                    if not isinstance(cell, dict):
                        continue
                    cell_que_id = _coerce_count(cell.get("queId"))
                    if cell_que_id is not None and cell_que_id > 0:
                        row_subfield_ids.add(cell_que_id)
            if row_subfield_ids & editable_question_ids:
                effective_editable_ids.add(parent_que_id)
        return effective_editable_ids

    def _record_update_via_custom_view(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        view_key: str,
        answers: list[JSONObject],
        verify_write: bool,
        force_refresh_form: bool,
        tool_name: str = "更新记录",
    ) -> JSONObject:
        normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
        normalized_view_key = view_key.strip()
        if not normalized_view_key:
            raise_tool_error(QingflowApiError.config_error("view_key is required for custom view update"))

        def runner(session_profile, context):
            index = self._get_view_field_index(profile, context, normalized_view_key, force_refresh=force_refresh_form) if verify_write else None
            normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
            self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
            result = self.backend.request(
                "POST",
                context,
                f"/view/{normalized_view_key}/apply/{normalized_apply_id}",
                json_body={"answers": normalized_answers},
            )
            verification = self._verify_record_write_result(
                context,
                app_key=app_key,
                apply_id=normalized_apply_id,
                normalized_answers=normalized_answers,
                index=cast(FieldIndex, index),
                verify_view_key=normalized_view_key,
            ) if verify_write and index is not None else None
            verified = True if verification is None else bool(verification.get("verified"))
            return self._attach_human_review_notice(
                {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "request_route": self._request_route_payload(context),
                    "app_key": app_key,
                    "apply_id": normalized_apply_id,
                    "record_id": normalized_apply_id,
                    "result": result,
                    "normalized_answers": normalized_answers,
                    "status": "completed" if verified else "verification_failed",
                    "ok": True,
                    "verify_write": verify_write,
                    "write_verified": verified if verify_write else None,
                    "verification": verification,
                    "resource": _record_resource_payload(normalized_apply_id),
                },
                operation="update",
                target="record data",
            )

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _record_update_via_task_save_only(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        workflow_node_id: int,
        answers: list[JSONObject],
        verify_write: bool,
        force_refresh_form: bool,
        tool_name: str = "更新记录",
    ) -> JSONObject:
        normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
        if workflow_node_id <= 0:
            raise_tool_error(QingflowApiError.config_error("workflow_node_id must be positive for task save-only update"))

        def runner(session_profile, context):
            index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
            normalized_answers = [dict(item) for item in answers if isinstance(item, dict)]
            self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
            result = self.backend.request(
                "POST",
                context,
                f"/app/{app_key}/apply/{normalized_apply_id}",
                json_body={"role": 3, "auditNodeId": workflow_node_id, "answers": normalized_answers},
            )
            verification = self._verify_record_write_result(
                context,
                app_key=app_key,
                apply_id=normalized_apply_id,
                normalized_answers=normalized_answers,
                index=cast(FieldIndex, index),
            ) if verify_write and index is not None else None
            verified = True if verification is None else bool(verification.get("verified"))
            return self._attach_human_review_notice(
                {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "request_route": self._request_route_payload(context),
                    "app_key": app_key,
                    "apply_id": normalized_apply_id,
                    "record_id": normalized_apply_id,
                    "result": result,
                    "normalized_answers": normalized_answers,
                    "status": "completed" if verified else "verification_failed",
                    "ok": True,
                    "verify_write": verify_write,
                    "write_verified": verified if verify_write else None,
                    "verification": verification,
                    "resource": _record_resource_payload(normalized_apply_id),
                },
                operation="update",
                target="record data",
            )

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _record_update_public_batch(
        self,
        *,
        profile: str,
        app_key: str,
        items: list[JSONObject],
        view_id: str | None,
        dry_run: bool,
        verify_write: bool,
        output_profile: str,
        tool_name: str = "更新记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        preflight_responses = [
            self._record_update_public_preflight_response(
                profile=profile,
                app_key=app_key,
                record_id=cast(int, item["record_id"]),
                fields=cast(JSONObject, item["fields"]),
                view_id=view_id,
                output_profile=output_profile,
                tool_name=tool_name,
            )
            for item in items
        ]
        has_preflight_blockers = any(
            str(response.get("status") or "").lower() in {"blocked", "needs_confirmation"}
            for response in preflight_responses
        )
        if dry_run or has_preflight_blockers:
            return self._record_update_batch_response(
                profile=profile,
                app_key=app_key,
                responses=preflight_responses,
                output_profile=output_profile,
                dry_run=dry_run,
            )

        apply_responses: list[JSONObject] = []
        for item in items:
            record_id = cast(int, item["record_id"])
            fields = cast(JSONObject, item["fields"])
            try:
                apply_responses.append(
                    self._record_update_public_single(
                        profile=profile,
                        app_key=app_key,
                        record_id=record_id,
                        fields=fields,
                        view_id=view_id,
                        verify_write=verify_write,
                        output_profile=output_profile,
                        capture_exceptions=True,
                        tool_name=tool_name,
                    )
                )
            except (QingflowApiError, RuntimeError) as exc:
                apply_responses.append(
                    self._record_write_exception_response(
                        exc,
                        operation="update",
                        profile=profile,
                        app_key=app_key,
                        record_id=record_id,
                        output_profile=output_profile,
                        human_review=True,
                    )
                )

        return self._record_update_batch_response(
            profile=profile,
            app_key=app_key,
            responses=apply_responses,
            output_profile=output_profile,
            dry_run=False,
        )

    def _record_update_public_preflight_response(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: int,
        fields: JSONObject,
        view_id: str | None,
        output_profile: str,
        tool_name: str = "更新记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        preflight_kwargs: dict[str, Any] = {
            "profile": profile,
            "app_key": app_key,
            "record_id": record_id,
            "fields": fields,
            "force_refresh_form": False,
        }
        if view_id is not None:
            preflight_kwargs["preferred_view_id"] = view_id
        preflight_kwargs["tool_name"] = tool_name
        raw_preflight = self._preflight_record_update_with_auto_view(**preflight_kwargs)
        preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
        normalized_payload = self._record_write_normalized_payload(
            operation="update",
            record_id=record_id,
            record_ids=[],
            normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
            submit_type=1,
            selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
        )
        confirmation_requests = cast(list[JSONObject], preflight_data.get("confirmation_requests", []))
        if preflight_data.get("blockers") or confirmation_requests:
            return self._record_write_blocked_response(
                raw_preflight,
                operation="update",
                normalized_payload=normalized_payload,
                output_profile=output_profile,
                human_review=True,
                target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
            )
        return self._record_write_ready_response(
            raw_preflight,
            operation="update",
            normalized_payload=normalized_payload,
            output_profile=output_profile,
            human_review=True,
            target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
        )

    def _normalize_public_record_update_batch_items(
        self,
        *,
        record_id: Any | None,
        fields: JSONObject | None,
        items: list[JSONObject] | None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if record_id is not None or fields is not None:
            raise_tool_error(
                QingflowApiError.config_error("record_update batch mode does not accept record_id or fields")
            )
        if not isinstance(items, list) or not items:
            raise_tool_error(QingflowApiError.config_error("items must be a non-empty list"))
        normalized_items: list[JSONObject] = []
        seen_record_ids: set[int] = set()
        for index, item in enumerate(items):
            if not isinstance(item, dict):
                raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
            normalized_record_id = normalize_positive_id_int(
                item.get("record_id"),
                field_name=f"items[{index}].record_id",
            )
            if normalized_record_id in seen_record_ids:
                raise_tool_error(
                    QingflowApiError.config_error(f"duplicate record_id in items: {normalized_record_id}")
                )
            item_fields = item.get("fields")
            if not isinstance(item_fields, dict):
                raise_tool_error(QingflowApiError.config_error(f"items[{index}].fields must be an object map keyed by field title"))
            seen_record_ids.add(normalized_record_id)
            normalized_items.append({"record_id": normalized_record_id, "fields": cast(JSONObject, item_fields)})
        return normalized_items

    def _record_update_batch_response(
        self,
        *,
        profile: str,
        app_key: str,
        responses: list[JSONObject],
        output_profile: str,
        dry_run: bool,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        summary = self._record_update_batch_summary(responses)
        batch_items = [self._record_update_batch_item_from_response(response, output_profile=output_profile) for response in responses]
        public_items = [self._record_update_public_batch_item(item, index=index) for index, item in enumerate(batch_items)]
        status, ok, message = self._record_update_batch_envelope_status(summary=summary, dry_run=dry_run)
        first_response = responses[0] if responses else {}
        applied_count = int(summary.get("applied_count") or 0)
        ready_count = int(summary.get("ready_count") or 0)
        verified_count = int(summary.get("verified_count") or 0)
        field_level_verified_count = int(summary.get("field_level_verified_count") or 0)
        confirmation_count = int(summary.get("confirmation_count") or 0)
        blocked_count = int(summary.get("blocked_count") or 0)
        failed_count = int(summary.get("failed_count") or 0)
        write_executed = applied_count > 0
        verification_status = "not_requested"
        if write_executed:
            verification_status = "verified" if verified_count == applied_count else "failed"
        updated_record_ids = [
            str(item.get("record_id"))
            for item in public_items
            if item.get("record_id") not in (None, "") and str(item.get("status") or "").lower() == "success"
        ]
        return {
            "profile": first_response.get("profile", profile),
            "ws_id": first_response.get("ws_id"),
            "ok": ok,
            "status": status,
            "mode": "batch",
            "dry_run": dry_run,
            "app_key": app_key,
            "total": int(summary.get("total") or 0),
            "succeeded": ready_count if dry_run else applied_count,
            "failed": blocked_count + failed_count,
            "needs_confirmation": confirmation_count,
            "updated_record_ids": updated_record_ids,
            "write_executed": write_executed,
            "safe_to_retry": not write_executed,
            "verification_status": verification_status,
            "field_level_verified_count": field_level_verified_count,
            "summary": summary,
            "items": public_items,
            "request_route": first_response.get("request_route"),
            "warnings": [],
            "output_profile": output_profile,
            "data": {
                "app_key": app_key,
                "mode": "batch",
                "dry_run": dry_run,
                "summary": summary,
                "items": batch_items,
            },
            "message": message,
        }

    def _record_update_public_batch_item(self, item: JSONObject, *, index: int) -> JSONObject:
        """执行内部辅助逻辑。"""
        public = dict(item)
        public.setdefault("index", index)
        public.setdefault("row_number", index + 1)
        resource = public.get("resource")
        if isinstance(resource, dict):
            record_id = resource.get("record_id")
            apply_id = resource.get("apply_id")
            if record_id not in (None, ""):
                public["record_id"] = str(record_id)
            if apply_id not in (None, ""):
                public["apply_id"] = str(apply_id)
        status = str(public.get("status") or "").lower()
        verification = public.get("verification")
        if isinstance(verification, dict):
            if bool(verification.get("verified")):
                public.setdefault("verification_status", "verified")
            elif status == "success":
                public.setdefault("verification_status", "failed")
        public.setdefault("write_executed", status == "success")
        public.setdefault("safe_to_retry", not bool(public.get("write_executed")))
        public.setdefault("verification_status", "not_requested")
        return public

    def _record_update_batch_summary(self, responses: list[JSONObject]) -> JSONObject:
        """执行内部辅助逻辑。"""
        summary: JSONObject = {
            "total": len(responses),
            "ready_count": 0,
            "blocked_count": 0,
            "confirmation_count": 0,
            "applied_count": 0,
            "verified_count": 0,
            "field_level_verified_count": 0,
            "failed_count": 0,
        }
        for response in responses:
            status = str(response.get("status") or "").lower()
            data = cast(JSONObject, response.get("data", {}))
            action = cast(JSONObject, data.get("action", {}))
            verification = cast(JSONObject, data.get("verification", {})) if isinstance(data.get("verification"), dict) else {}
            executed = bool(action.get("executed"))
            if status == "ready":
                summary["ready_count"] = int(summary["ready_count"]) + 1
                continue
            if status == "blocked":
                summary["blocked_count"] = int(summary["blocked_count"]) + 1
                continue
            if status == "needs_confirmation":
                summary["confirmation_count"] = int(summary["confirmation_count"]) + 1
                continue
            if executed:
                summary["applied_count"] = int(summary["applied_count"]) + 1
            if bool(verification.get("verified")):
                summary["verified_count"] = int(summary["verified_count"]) + 1
            if bool(verification.get("field_level_verified")):
                summary["field_level_verified_count"] = int(summary["field_level_verified_count"]) + 1
            if status not in {"success"}:
                summary["failed_count"] = int(summary["failed_count"]) + 1
        return summary

    def _record_update_batch_envelope_status(self, *, summary: JSONObject, dry_run: bool) -> tuple[str, bool, str]:
        """执行内部辅助逻辑。"""
        if int(summary["blocked_count"]) > 0:
            return "blocked", False, "batch update preflight blocked"
        if int(summary["confirmation_count"]) > 0:
            return "needs_confirmation", False, "batch update requires confirmation before write"
        if dry_run:
            return "ready", True, "batch update dry run ready"
        if int(summary["failed_count"]) > 0:
            return "partial_success", False, "batch update completed with partial failures"
        return "success", True, "batch update completed"

    def _record_update_batch_item_from_response(self, response: JSONObject, *, output_profile: str) -> JSONObject:
        """执行内部辅助逻辑。"""
        data = cast(JSONObject, response.get("data", {}))
        item: JSONObject = {
            "resource": data.get("resource"),
            "status": response.get("status"),
            "write_executed": bool(response.get("write_executed")),
            "safe_to_retry": bool(response.get("safe_to_retry", True)),
            "verification_status": response.get("verification_status", "not_requested"),
            "verification": data.get("verification"),
            "field_errors": cast(list[JSONObject], data.get("field_errors", [])),
            "confirmation_requests": cast(list[JSONObject], data.get("confirmation_requests", [])),
            "resolved_fields": cast(list[JSONObject], data.get("resolved_fields", [])),
        }
        update_route = response.get("update_route")
        if isinstance(update_route, dict):
            item["update_route"] = update_route
        tried_routes = response.get("tried_routes")
        if isinstance(tried_routes, list) and (output_profile == "verbose" or response.get("status") != "success"):
            item["tried_routes"] = tried_routes
        blockers = data.get("blockers")
        if isinstance(blockers, list) and blockers:
            item["blockers"] = blockers
        warnings = response.get("warnings")
        if isinstance(warnings, list) and warnings:
            item["warnings"] = warnings
        error = data.get("error")
        if isinstance(error, dict):
            item["error"] = error
        if output_profile == "verbose" and isinstance(data.get("debug"), dict):
            item["debug"] = data.get("debug")
        return item

    def _preflight_record_update_with_auto_view(
        self,
        *,
        profile: str,
        app_key: str,
        record_id: int,
        fields: JSONObject,
        preferred_view_id: str | None = None,
        force_refresh_form: bool,
        tool_name: str = "更新记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        def runner(session_profile, context):
            request_route = self._request_route_payload(context)
            def build_once(*, effective_force_refresh: bool) -> JSONObject:
                candidate_routes = self._candidate_update_views(profile, context, app_key)
                normalized_preferred_view_id = _normalize_optional_text(preferred_view_id)
                if normalized_preferred_view_id:
                    preferred_route = next(
                        (
                            route
                            for route in candidate_routes
                            if route.view_id == normalized_preferred_view_id
                        ),
                        None,
                    )
                    if preferred_route is None:
                        raise_tool_error(
                            QingflowApiError.config_error(
                                f"view_id '{normalized_preferred_view_id}' is not an accessible update candidate"
                        )
                    )
                    candidate_routes = [preferred_route]
                else:
                    candidate_routes = _prefer_custom_update_routes(candidate_routes)
                probes = self._probe_candidate_record_contexts(
                    context,
                    app_key=app_key,
                    apply_id=record_id,
                    candidate_routes=candidate_routes,
                )
                matched_probes = [probe for probe in probes if probe.readable]
                if not matched_probes and probes and all(probe.transport_error for probe in probes):
                    return {
                        "profile": profile,
                        "ws_id": session_profile.selected_ws_id,
                        "ok": True,
                        "request_route": request_route,
                        "data": self._build_auto_view_blocked_preflight_data(
                            app_key=app_key,
                            record_id=record_id,
                            blockers=["CURRENT_RECORD_CONTEXT_UNAVAILABLE"],
                            warnings=[
                                "update preflight could not load the current record from any candidate route; automatic view selection was blocked to avoid resolving lookup fields against a partial patch."
                            ],
                            recommended_next_actions=[
                                "Retry after the record becomes readable in the current workspace/profile context.",
                                "Call record_update_schema_get to inspect the overall writable field set for this record after context access is restored.",
                            ],
                            view_probe_summary=[
                                self._record_context_probe_summary_payload(probe)
                                for probe in probes
                            ],
                        ),
                    }

                probe_summary: list[JSONObject] = []
                matched_routes: list[AccessibleViewRoute] = []
                matched_answers_for_union: list[JSONObject] | None = None
                first_blocked_plan: JSONObject | None = None
                first_confirmation_plan: JSONObject | None = None
                fallback_warning_messages = self._record_context_probe_fallback_warnings(probes)

                for probe in probes:
                    candidate = probe.route
                    candidate_summary = self._record_context_probe_summary_payload(probe)
                    candidate_summary["writable_field_titles"] = []
                    candidate_summary["missing_field_titles"] = []
                    candidate_summary["selected"] = False
                    if not probe.readable:
                        probe_summary.append(candidate_summary)
                        continue

                    current_answers = probe.answer_list or []
                    matched_routes.append(candidate)
                    if matched_answers_for_union is None:
                        matched_answers_for_union = current_answers
                    browse_scope = self._build_browse_write_scope(
                        profile,
                        context,
                        app_key,
                        candidate,
                        force_refresh=effective_force_refresh,
                    )
                    browse_index = cast(FieldIndex, browse_scope["index"])
                    browse_writable_field_ids = cast(set[int], browse_scope["writable_field_ids"])
                    candidate_titles = [
                        field.que_title
                        for field in self._schema_fields_for_mode(
                            profile,
                            context,
                            app_key,
                            browse_index,
                            schema_mode="browse",
                            resolved_view=candidate,
                        )
                        if field.que_id in browse_writable_field_ids and field.que_title
                    ]
                    plan_data = self._build_record_write_preflight(
                        profile=profile,
                        context=context,
                        operation="update",
                        app_key=app_key,
                        apply_id=record_id,
                        answers=[],
                        fields=fields,
                        force_refresh_form=effective_force_refresh,
                        view_id=candidate.view_id,
                        list_type=None,
                        view_key=None,
                        view_name=None,
                        existing_answers_override=current_answers,
                    )
                    readonly_entries = plan_data.get("validation", {}).get("readonly_or_system_fields", []) if isinstance(plan_data.get("validation"), dict) else []
                    invalid_entries = plan_data.get("validation", {}).get("invalid_fields", []) if isinstance(plan_data.get("validation"), dict) else []
                    missing_field_titles: list[str] = []
                    for entry in readonly_entries:
                        if not isinstance(entry, dict):
                            continue
                        title = _normalize_optional_text(entry.get("que_title"))
                        if title and title not in missing_field_titles:
                            missing_field_titles.append(title)
                    for entry in invalid_entries:
                        if not isinstance(entry, dict):
                            continue
                        if _normalize_optional_text(entry.get("error_code")) != "VIEW_SCOPE_FIELD_HIDDEN":
                            continue
                        field_payload = entry.get("field")
                        if isinstance(field_payload, dict):
                            title = _normalize_optional_text(field_payload.get("que_title"))
                            if title and title not in missing_field_titles:
                                missing_field_titles.append(title)
                    candidate_summary["writable_field_titles"] = candidate_titles
                    candidate_summary["missing_field_titles"] = missing_field_titles
                    if plan_data.get("blockers"):
                        confirmation_requests = plan_data.get("confirmation_requests")
                        if (
                            isinstance(confirmation_requests, list)
                            and confirmation_requests
                            and first_confirmation_plan is None
                        ):
                            selection = plan_data.get("selection")
                            if not isinstance(selection, dict):
                                selection = {}
                                plan_data["selection"] = selection
                            view_payload = selection.get("view")
                            if not isinstance(view_payload, dict):
                                view_payload = _accessible_view_payload(candidate)
                                selection["view"] = view_payload
                            view_payload["auto_selected"] = True
                            view_payload["selection_source"] = "first_satisfying_accessible_view"
                            candidate_summary["selected"] = True
                            validation = plan_data.get("validation")
                            if isinstance(validation, dict):
                                warnings = validation.get("warnings")
                                if not isinstance(warnings, list):
                                    warnings = []
                                    validation["warnings"] = warnings
                                for message in fallback_warning_messages:
                                    if message not in warnings:
                                        warnings.append(message)
                            first_confirmation_plan = plan_data
                        elif first_blocked_plan is None:
                            validation = plan_data.get("validation")
                            if isinstance(validation, dict):
                                warnings = validation.get("warnings")
                                if not isinstance(warnings, list):
                                    warnings = []
                                    validation["warnings"] = warnings
                                for message in fallback_warning_messages:
                                    if message not in warnings:
                                        warnings.append(message)
                            first_blocked_plan = plan_data
                        probe_summary.append(candidate_summary)
                        continue

                    selection = plan_data.get("selection")
                    if not isinstance(selection, dict):
                        selection = {}
                        plan_data["selection"] = selection
                    view_payload = selection.get("view")
                    if not isinstance(view_payload, dict):
                        view_payload = _accessible_view_payload(candidate)
                        selection["view"] = view_payload
                    view_payload["auto_selected"] = True
                    view_payload["selection_source"] = "first_satisfying_accessible_view"
                    candidate_summary["selected"] = True
                    validation = plan_data.get("validation")
                    if isinstance(validation, dict):
                        warnings = validation.get("warnings")
                        if not isinstance(warnings, list):
                            warnings = []
                            validation["warnings"] = warnings
                        for message in fallback_warning_messages:
                            if message not in warnings:
                                warnings.append(message)
                    probe_summary.append(candidate_summary)
                    plan_data["view_probe_summary"] = probe_summary
                    plan_data["record_context_probe"] = probe_summary
                    return {
                        "profile": profile,
                        "ws_id": session_profile.selected_ws_id,
                        "ok": True,
                        "request_route": request_route,
                        "data": plan_data,
                    }

                if not matched_probes:
                    blocked_data = self._build_auto_view_blocked_preflight_data(
                        app_key=app_key,
                        record_id=record_id,
                        blockers=["NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"],
                        warnings=[],
                        recommended_next_actions=[
                            "Use record_get or record_list to confirm the record still exists in the current workspace.",
                            "Call record_update_schema_get to inspect whether any accessible view still matches this record.",
                        ],
                        view_probe_summary=probe_summary,
                    )
                    return {
                        "profile": profile,
                        "ws_id": session_profile.selected_ws_id,
                        "ok": True,
                        "request_route": request_route,
                        "data": blocked_data,
                    }

                if first_confirmation_plan is not None:
                    first_confirmation_plan["view_probe_summary"] = probe_summary
                    first_confirmation_plan["record_context_probe"] = probe_summary
                    return {
                        "profile": profile,
                        "ws_id": session_profile.selected_ws_id,
                        "ok": True,
                        "request_route": request_route,
                        "data": first_confirmation_plan,
                    }

                if normalized_preferred_view_id and first_blocked_plan is not None:
                    first_blocked_plan["view_probe_summary"] = probe_summary
                    first_blocked_plan["record_context_probe"] = probe_summary
                    return {
                        "profile": profile,
                        "ws_id": session_profile.selected_ws_id,
                        "ok": True,
                        "request_route": request_route,
                        "data": first_blocked_plan,
                    }

                blocked_data = self._build_auto_view_blocked_preflight_data(
                    app_key=app_key,
                    record_id=record_id,
                    blockers=["NO_SINGLE_VIEW_CAN_UPDATE_ALL_FIELDS"],
                    warnings=[
                        "record_update requires one executable frontend route for the full payload; it does not merge writable fields across multiple views."
                    ],
                    recommended_next_actions=[
                        "Call record_update_schema_get first to inspect the overall writable field set for this record.",
                        "Reduce the update payload until all requested fields fit inside one matched accessible view.",
                    ],
                    view_probe_summary=probe_summary,
                    template=first_blocked_plan,
                )
                return {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "ok": True,
                    "request_route": request_route,
                    "data": blocked_data,
                }

            result = build_once(effective_force_refresh=force_refresh_form)
            if (
                not force_refresh_form
                and self._record_preflight_suggests_stale_schema(cast(JSONObject, result.get("data", {})))
            ):
                self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
                result = build_once(effective_force_refresh=True)
                cast(JSONObject, result.setdefault("data", {}))["schema_force_refreshed"] = True
            return result

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _build_record_update_union_preflight(
        self,
        *,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        record_id: int,
        fields: JSONObject,
        current_answers: list[JSONObject],
        matched_routes: list[AccessibleViewRoute],
        force_refresh_form: bool,
    ) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        if len(matched_routes) < 2:
            return None

        union_writable_field_ids: set[int] = set()
        union_visible_question_ids: set[int] = set()
        matched_view_payloads: list[JSONObject] = []
        union_index: FieldIndex | None = None

        for candidate in matched_routes:
            browse_scope = self._build_browse_write_scope(
                profile,
                context,
                app_key,
                candidate,
                force_refresh=force_refresh_form,
            )
            browse_index = cast(FieldIndex, browse_scope["index"])
            union_index = browse_index if union_index is None else _merge_field_indexes(union_index, browse_index)
            union_writable_field_ids.update(cast(set[int], browse_scope["writable_field_ids"]))
            union_visible_question_ids.update(cast(set[int], browse_scope["visible_question_ids"]))
            matched_view_payloads.append(_accessible_view_payload(candidate))

        if union_index is None or (not union_writable_field_ids and not union_visible_question_ids):
            return None

        plan_data = self._build_record_write_preflight(
            profile=profile,
            context=context,
            operation="update",
            app_key=app_key,
            apply_id=record_id,
            answers=[],
            fields=fields,
            force_refresh_form=force_refresh_form,
            view_id=None,
            list_type=None,
            view_key=None,
            view_name=None,
            existing_answers_override=current_answers,
            field_index_override=union_index,
        )

        validation = cast(JSONObject, plan_data.get("validation", {}))
        invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
        missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
        readonly_or_system_fields = cast(list[JSONObject], validation.get("readonly_or_system_fields", []))
        confirmation_requests = cast(list[JSONObject], plan_data.get("confirmation_requests", []))

        if union_visible_question_ids:
            invalid_fields.extend(
                self._validate_view_scoped_subtable_answers(
                    normalized_answers=cast(list[JSONObject], plan_data.get("normalized_answers", [])),
                    full_index=union_index,
                    selector_index=union_index,
                    visible_question_ids=union_visible_question_ids,
                )
            )

        readonly_or_system_fields = [
            item
            for item in readonly_or_system_fields
            if not (
                isinstance(item, dict)
                and (que_id := _coerce_count(item.get("que_id"))) is not None
                and que_id in union_writable_field_ids
            )
        ]
        existing_readonly_ids = {
            str(_coerce_count(item.get("que_id")))
            for item in readonly_or_system_fields
            if isinstance(item, dict) and _coerce_count(item.get("que_id")) is not None
        }
        for entry in cast(list[JSONObject], plan_data.get("resolved_fields", [])):
            if not isinstance(entry, dict) or not bool(entry.get("resolved")):
                continue
            que_id = _coerce_count(entry.get("que_id"))
            if que_id is None or que_id in union_writable_field_ids or str(que_id) in existing_readonly_ids:
                continue
            readonly_or_system_fields.append(
                {
                    "que_id": que_id,
                    "que_title": entry.get("que_title"),
                    "que_type": entry.get("que_type"),
                    "readonly": entry.get("readonly"),
                    "system": entry.get("system"),
                    "source": entry.get("source"),
                    "requested": entry.get("requested"),
                    "reason_code": "view_readonly",
                }
            )

        warnings_payload = validation.get("warnings")
        warnings = list(warnings_payload) if isinstance(warnings_payload, list) else []
        union_warning = "update preflight used the union of matched accessible views because no single matched view covered the full payload."
        if union_warning not in warnings:
            warnings.append(union_warning)

        blockers: list[str] = []
        if invalid_fields:
            blockers.append("payload contains invalid field values")
        if missing_required_fields:
            blockers.append("required fields are missing")
        if readonly_or_system_fields:
            blockers.append("payload writes fields that are not writable in any matched accessible view")
        if confirmation_requests:
            blockers.append("one or more lookup fields require confirmation before the write can execute")

        validation["valid"] = (
            not invalid_fields
            and not missing_required_fields
            and not readonly_or_system_fields
            and not confirmation_requests
        )
        validation["invalid_fields"] = invalid_fields
        validation["missing_required_fields"] = missing_required_fields
        validation["readonly_or_system_fields"] = readonly_or_system_fields
        validation["warnings"] = warnings
        plan_data["validation"] = validation
        plan_data["field_errors"] = self._record_write_field_errors(
            invalid_fields=invalid_fields,
            missing_required_fields=missing_required_fields,
            readonly_or_system_fields=readonly_or_system_fields,
        )
        plan_data["blockers"] = blockers

        recommended_next_actions = (
            list(plan_data.get("recommended_next_actions"))
            if isinstance(plan_data.get("recommended_next_actions"), list)
            else []
        )
        if readonly_or_system_fields:
            recommended_next_actions.append(
                "Remove fields that are not writable in any matched accessible view for this record."
            )
        if invalid_fields:
            recommended_next_actions.append("Fix invalid_fields before applying the write.")
        if missing_required_fields:
            recommended_next_actions.append("Fill missing required fields before applying the write.")
        if confirmation_requests:
            recommended_next_actions.append(
                "Review confirmation_requests and retry with explicit ids/objects for the ambiguous lookup fields."
            )
        plan_data["recommended_next_actions"] = list(dict.fromkeys(str(item) for item in recommended_next_actions))

        selection = plan_data.get("selection")
        if not isinstance(selection, dict):
            selection = {}
        selection["view"] = {
            "view_id": "virtual:matched_view_union",
            "name": "Matched accessible views (union)",
            "kind": "virtual",
            "analysis_supported": False,
            "auto_selected": True,
            "selection_source": "matched_accessible_view_union",
            "matched_views": matched_view_payloads,
        }
        plan_data["selection"] = selection

        if invalid_fields or missing_required_fields or readonly_or_system_fields:
            return None
        return plan_data

    def _build_auto_view_blocked_preflight_data(
        self,
        *,
        app_key: str,
        record_id: int,
        blockers: list[str],
        warnings: list[str],
        recommended_next_actions: list[str],
        view_probe_summary: list[JSONObject],
        template: JSONObject | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        blocked_data: JSONObject = dict(template or {})
        validation = blocked_data.get("validation")
        if not isinstance(validation, dict):
            validation = {}
            blocked_data["validation"] = validation
        existing_warnings = validation.get("warnings")
        merged_warnings = list(existing_warnings) if isinstance(existing_warnings, list) else []
        merged_warnings.extend(warnings)
        validation["warnings"] = merged_warnings
        validation.setdefault("missing_required_fields", [])
        validation.setdefault("likely_hidden_required_fields", [])
        validation.setdefault("readonly_or_system_fields", [])
        validation.setdefault("invalid_fields", [])
        validation["valid"] = False
        blocked_data.setdefault("normalized_answers", [])
        blocked_data.setdefault("resolved_fields", [])
        blocked_data.setdefault("support_matrix", _summarize_write_support([]))
        blocked_data.setdefault("confirmation_requests", [])
        blocked_data.setdefault("lookup_resolved_fields", [])
        blocked_data.setdefault(
            "dependencies",
            {
                "question_relations_present": False,
                "question_relations": [],
                "option_links": [],
            },
        )
        blocked_data["operation"] = "update"
        blocked_data["app_key"] = app_key
        blocked_data["apply_id"] = record_id
        blocked_data["selection"] = {"view": None}
        blocked_data["ready_to_submit"] = False
        blocked_data["blockers"] = blockers
        blocked_data["recommended_next_actions"] = recommended_next_actions
        blocked_data["view_probe_summary"] = view_probe_summary
        return blocked_data

    def _candidate_update_views(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
    ) -> list[AccessibleViewRoute]:
        """执行内部辅助逻辑。"""
        candidates: list[AccessibleViewRoute] = []
        for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
            candidates.append(
                AccessibleViewRoute(
                    view_id=view_id,
                    name=name,
                    kind="system",
                    list_type=list_type,
                    view_selection=None,
                    view_type=None,
                )
            )
        try:
            view_items = self._get_view_list(profile, context, app_key)
        except QingflowApiError as exc:
            if not _is_record_permission_denied_error(exc):
                raise
            view_items = []
        for item in view_items:
            if not isinstance(item, dict):
                continue
            view_key = _normalize_optional_text(item.get("viewKey"))
            if not view_key:
                continue
            view_name = _normalize_optional_text(item.get("viewName")) or view_key
            view_type = _normalize_optional_text(item.get("viewType") or item.get("viewgraphType"))
            filter_config: JSONObject | None = None
            if isinstance(item.get("viewgraphLimit"), list):
                filter_config = item
            else:
                raw_view_config = item.get("viewConfig")
                if isinstance(raw_view_config, dict):
                    filter_config = raw_view_config
            view_selection = ViewSelection(
                view_key=view_key,
                view_name=view_name,
                conditions=_compile_view_conditions(filter_config or {}),
                view_type=view_type,
                filter_config_loaded=isinstance(filter_config, dict),
            )
            candidates.append(
                AccessibleViewRoute(
                    view_id=f"custom:{view_key}",
                    name=view_name,
                    kind="custom",
                    list_type=None,
                    view_selection=view_selection,
                    view_type=view_type,
                )
            )
        return candidates

    def _route_view_key(self, resolved_view: AccessibleViewRoute) -> str | None:
        if resolved_view.view_selection is not None:
            view_key = _normalize_optional_text(resolved_view.view_selection.view_key)
            if view_key:
                return view_key
        if resolved_view.kind == "custom" and resolved_view.view_id.startswith("custom:"):
            view_key = resolved_view.view_id.split(":", 1)[1].strip()
            return view_key or None
        return None

    def _record_context_route_error_payload(self, error: QingflowApiError) -> JSONObject:
        payload: JSONObject = {"message": error.message}
        if error.category:
            payload["category"] = error.category
        if error.backend_code is not None:
            payload["backend_code"] = error.backend_code
        if error.request_id is not None:
            payload["request_id"] = error.request_id
        if error.http_status is not None:
            payload["http_status"] = error.http_status
        return payload

    def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
        if is_auth_like_error(error):
            return False
        if backend_code_int(error) in {40002, 40023, 40027, 40038, 404}:
            return True
        if error.http_status == 404:
            return True
        return False

    def _load_record_answers_for_accessible_route(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int,
        resolved_view: AccessibleViewRoute,
    ) -> tuple[list[JSONObject], int | None]:
        if resolved_view.kind == "custom":
            view_key = self._route_view_key(resolved_view)
            if not view_key:
                raise_tool_error(
                    QingflowApiError.config_error(
                        f"cannot resolve custom view route for '{resolved_view.view_id}'"
                    )
                )
            record = self.backend.request(
                "GET",
                context,
                f"/view/{view_key}/apply/{apply_id}",
            )
            used_list_type = None
        else:
            used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
            role = _record_detail_role_for_list_type(used_list_type)
            record = self.backend.request(
                "GET",
                context,
                f"/app/{app_key}/apply/{apply_id}",
                params={"role": role, "listType": used_list_type},
            )
        answers = record.get("answers") if isinstance(record, dict) else None
        normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
        return normalized_answers, used_list_type

    def _probe_record_context_route(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int,
        resolved_view: AccessibleViewRoute,
    ) -> RecordContextRouteProbe:
        try:
            answer_list, used_list_type = self._load_record_answers_for_accessible_route(
                context,
                app_key=app_key,
                apply_id=apply_id,
                resolved_view=resolved_view,
            )
            return RecordContextRouteProbe(
                route=resolved_view,
                answer_list=answer_list,
                used_list_type=used_list_type,
                readable=True,
                transport_error=False,
                error_payload=None,
            )
        except QingflowApiError as exc:
            if not self._is_record_context_route_miss(exc):
                raise
            return RecordContextRouteProbe(
                route=resolved_view,
                answer_list=None,
                used_list_type=resolved_view.list_type if resolved_view.kind == "system" else None,
                readable=False,
                transport_error=not self._is_record_context_route_miss(exc),
                error_payload=self._record_context_route_error_payload(exc),
            )

    def _probe_candidate_record_contexts(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int,
        candidate_routes: list[AccessibleViewRoute],
    ) -> list[RecordContextRouteProbe]:
        return [
            self._probe_record_context_route(
                context,
                app_key=app_key,
                apply_id=apply_id,
                resolved_view=candidate,
            )
            for candidate in candidate_routes
        ]

    def _record_context_probe_summary_payload(self, probe: RecordContextRouteProbe) -> JSONObject:
        payload: JSONObject = {
            "view_id": probe.route.view_id,
            "name": probe.route.name,
            "kind": probe.route.kind,
            "matched_record": probe.readable,
            "readable": probe.readable,
            "context_complete": probe.readable,
            "used_list_type": probe.used_list_type,
        }
        if probe.error_payload is not None:
            payload["error"] = probe.error_payload
            payload["transport_error"] = probe.transport_error
        return payload

    def _record_context_probe_fallback_warnings(
        self,
        probes: list[RecordContextRouteProbe],
    ) -> list[str]:
        matched_probes = [probe for probe in probes if probe.readable]
        if not matched_probes:
            return []
        if any(
            probe.route.kind == "system" and probe.used_list_type == DEFAULT_RECORD_LIST_TYPE
            for probe in matched_probes
        ):
            return []
        first_probe = matched_probes[0]
        if first_probe.route.kind == "custom":
            return [
                "current record context was not accessible via listType="
                f"{DEFAULT_RECORD_LIST_TYPE}; preflight resolved it via custom view "
                f"'{first_probe.route.name}'."
            ]
        used_list_type = first_probe.used_list_type
        if used_list_type is None:
            return []
        return [
            "current record context was not accessible via listType="
            f"{DEFAULT_RECORD_LIST_TYPE}; preflight resolved it via listType={used_list_type} "
            f"({get_record_list_type_label(used_list_type)})."
        ]

    def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
        if backend_code_int(exc) == 500:
            return True
        if exc.http_status is not None and exc.http_status >= 500:
            return True
        normalized_message = exc.message.strip().lower()
        return normalized_message in {"unknown error", "internal server error"}

    def _remap_record_update_target_context_error(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int,
        exc: QingflowApiError,
    ) -> None:
        if not self._looks_like_generic_record_update_backend_failure(exc):
            return
        try:
            candidate_routes = self._candidate_update_views(profile, context, app_key)
            probes = self._probe_candidate_record_contexts(
                context,
                app_key=app_key,
                apply_id=apply_id,
                candidate_routes=candidate_routes,
            )
        except (QingflowApiError, RuntimeError):
            return
        if not probes or any(probe.readable for probe in probes):
            return

        blocker = (
            "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
            if all(probe.transport_error for probe in probes)
            else "NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"
        )
        recommended_next_actions = (
            [
                "Retry after the record becomes readable in the current workspace/profile context.",
                "If the issue persists, verify that the current profile still has read access to this record.",
            ]
            if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
            else [
                "Use record_get or record_list to confirm the record still exists in the current workspace.",
                "Call record_update_schema_get to inspect whether any accessible view still matches this record.",
            ]
        )
        first_error_payload = next(
            (
                cast(JSONObject, probe.error_payload)
                for probe in probes
                if isinstance(probe.error_payload, dict)
            ),
            None,
        )
        backend_code = (
            cast(int, first_error_payload.get("backend_code"))
            if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("backend_code"), int)
            else exc.backend_code
        )
        request_id = (
            _normalize_optional_text(first_error_payload.get("request_id"))
            if isinstance(first_error_payload, dict)
            else None
        ) or exc.request_id
        http_status = (
            cast(int, first_error_payload.get("http_status"))
            if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("http_status"), int)
            else exc.http_status
        )
        raise_tool_error(
            QingflowApiError(
                category="backend",
                message=(
                    "Direct record edit was blocked because the current record context could not be loaded from any candidate route."
                    if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
                    else "Direct record edit was blocked because the target record is no longer accessible in any readable view for the current profile."
                ),
                backend_code=backend_code,
                request_id=request_id,
                http_status=http_status,
                details={
                    "error_code": blocker,
                    "operation": "update",
                    "app_key": app_key,
                    "record_id": apply_id,
                    "blockers": [blocker],
                    "request_route": self._request_route_payload(context),
                    "view_probe_summary": [
                        self._record_context_probe_summary_payload(probe)
                        for probe in probes
                    ],
                    "recommended_next_actions": recommended_next_actions,
                    "fix_hint": (
                        "Confirm the target record still exists and remains visible in at least one accessible view before retrying the update."
                    ),
                },
            )
        )

    def _record_matches_accessible_view(
        self,
        context,  # type: ignore[no-untyped-def]
        answer_list: list[JSONObject],
        *,
        resolved_view: AccessibleViewRoute,
    ) -> bool:
        """执行内部辅助逻辑。"""
        if resolved_view.kind == "system":
            return True
        return self._matches_view_selection(
            context,
            answer_list,
            view_selection=resolved_view.view_selection,
            dept_member_cache={},
        )

    def record_delete_public(
        self,
        *,
        profile: str = DEFAULT_PROFILE,
        app_key: str,
        record_id: Any | None = None,
        record_ids: list[Any] | None = None,
        view_id: str | None = None,
        list_type: int | None = None,
        output_profile: str = "normal",
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_output_profile = self._normalize_public_output_profile(output_profile)
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
        normalized_record_ids: list[int] = []
        for index, item in enumerate(record_ids or []):
            normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
        delete_ids = normalized_record_ids
        if not delete_ids and record_id is not None:
            delete_ids = [normalize_positive_id_int(record_id, field_name="record_id")]
        if not delete_ids:
            raise_tool_error(QingflowApiError.config_error("record_id or record_ids is required"))
        seen_delete_ids: set[int] = set()
        for item in delete_ids:
            if item in seen_delete_ids:
                raise_tool_error(QingflowApiError.config_error(f"duplicate record id in delete payload: {stringify_backend_id(item)}"))
            seen_delete_ids.add(item)
        normalized_payload = {
            "operation": "delete",
            "record_id": stringify_backend_id(record_id) if record_id is not None else None,
            "record_ids": [stringify_backend_id(item) for item in delete_ids],
            "answers": [],
            "submit_type": 1,
            "selection": {"view_id": view_id, "list_type": delete_list_type},
        }
        return self._record_delete_public_batch(
            profile=profile,
            app_key=app_key,
            delete_ids=delete_ids,
            list_type=delete_list_type,
            normalized_payload=normalized_payload,
            output_profile=normalized_output_profile,
            tool_name="删除记录",
        )

    def _resolve_record_delete_list_type(self, *, view_id: str | None, list_type: int | None) -> int:
        normalized_view_id = _normalize_optional_text(view_id)
        if normalized_view_id:
            if normalized_view_id.startswith("custom:"):
                raise_tool_error(
                    QingflowApiError.config_error(
                        "record_delete does not support custom view deletion; the backend delete route accepts system listType only",
                        details={
                            "error_code": "RECORD_DELETE_CUSTOM_VIEW_UNSUPPORTED",
                            "view_id": normalized_view_id,
                            "fix_hint": (
                                "Use a system view_id from app_get.accessible_views, or resolve target record_ids with "
                                "record list/get first and retry delete without a custom view selector."
                            ),
                        },
                    )
                )
            if not normalized_view_id.startswith("system:"):
                raise_tool_error(QingflowApiError.config_error("view_id must start with system: or custom:"))
            mapped_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE.get(normalized_view_id)
            if mapped_list_type is None:
                raise_tool_error(QingflowApiError.config_error(f"unsupported view_id '{normalized_view_id}'"))
            return mapped_list_type
        if list_type is not None:
            normalized_list_type = int(list_type)
            if normalized_list_type not in SYSTEM_VIEW_LIST_TYPES:
                raise_tool_error(
                    QingflowApiError.config_error(
                        "record_delete list_type must map to a supported system view",
                        details={
                            "error_code": "RECORD_DELETE_SYSTEM_VIEW_REQUIRED",
                            "list_type": normalized_list_type,
                            "supported_list_types": sorted(SYSTEM_VIEW_LIST_TYPES),
                            "fix_hint": "Pass a system view_id from app_get.accessible_views instead of an arbitrary list_type.",
                        },
                    )
                )
            return normalized_list_type
        raise_tool_error(
            QingflowApiError.config_error(
                "record_delete requires a system view_id or list_type; deleting without frontend list context is ambiguous",
                details={
                    "error_code": "RECORD_DELETE_VIEW_REQUIRED",
                    "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.",
                },
            )
        )

    def _record_delete_public_batch(
        self,
        *,
        profile: str,
        app_key: str,
        delete_ids: list[int],
        list_type: int,
        normalized_payload: JSONObject,
        output_profile: str,
        tool_name: str = "删除记录",
    ) -> JSONObject:
        items: list[JSONObject] = []
        request_route: JSONObject | None = None
        ws_id: object = None
        for index, delete_id in enumerate(delete_ids):
            record_id_text = stringify_backend_id(delete_id)
            try:
                raw_apply = self._record_delete_many(
                    profile=profile,
                    app_key=app_key,
                    record_ids=[delete_id],
                    list_type=list_type,
                    tool_name=tool_name,
                )
                request_route = cast(JSONObject, raw_apply.get("request_route")) if isinstance(raw_apply.get("request_route"), dict) else request_route
                ws_id = raw_apply.get("ws_id", ws_id)
                single_payload = {
                    "operation": "delete",
                    "record_id": record_id_text,
                    "record_ids": [record_id_text],
                    "answers": [],
                    "submit_type": 1,
                    "selection": normalized_payload.get("selection"),
                }
                single_response = self._record_write_apply_response(
                    raw_apply,
                    operation="delete",
                    normalized_payload=single_payload,
                    output_profile=output_profile,
                    human_review=True,
                    preflight=None,
                )
                item_status = str(single_response.get("status") or "success")
                item: JSONObject = {
                    "index": index,
                    "row_number": index + 1,
                    "record_id": record_id_text,
                    "status": item_status,
                    "write_executed": bool(single_response.get("write_executed")),
                    "verification_status": single_response.get("verification_status", "not_requested"),
                    "safe_to_retry": bool(single_response.get("safe_to_retry", False)),
                }
                if item_status != "success":
                    item["error"] = (single_response.get("data") or {}).get("error") if isinstance(single_response.get("data"), dict) else None
                items.append(item)
            except (QingflowApiError, RuntimeError) as exc:
                error_response = self._record_write_exception_response(
                    exc,
                    operation="delete",
                    profile=profile,
                    app_key=app_key,
                    record_id=record_id_text,
                    output_profile=output_profile,
                    human_review=True,
                    write_executed=False,
                )
                request_route = cast(JSONObject, error_response.get("request_route")) if isinstance(error_response.get("request_route"), dict) else request_route
                item = {
                    "index": index,
                    "row_number": index + 1,
                    "record_id": record_id_text,
                    "status": "failed",
                    "write_executed": False,
                    "verification_status": "not_requested",
                    "safe_to_retry": True,
                    "error": (error_response.get("data") or {}).get("error") if isinstance(error_response.get("data"), dict) else {"message": str(exc)},
                }
                items.append(item)
        deleted_ids = [
            str(item["record_id"])
            for item in items
            if str(item.get("status") or "") == "success"
        ]
        failed_ids = [
            str(item["record_id"])
            for item in items
            if str(item.get("status") or "") != "success"
        ]
        total = len(items)
        succeeded = len(deleted_ids)
        failed = len(failed_ids)
        if succeeded and failed:
            status = "partial_success"
            ok = False
        elif succeeded:
            status = "success"
            ok = True
        else:
            status = "failed"
            ok = False
        write_executed = any(bool(item.get("write_executed")) for item in items)
        return {
            "profile": profile,
            "ws_id": ws_id,
            "ok": ok,
            "status": status,
            "mode": "batch",
            "total": total,
            "succeeded": succeeded,
            "failed": failed,
            "deleted_ids": deleted_ids,
            "failed_ids": failed_ids,
            "write_executed": write_executed,
            "verification_status": "not_requested",
            "safe_to_retry": False if write_executed else True,
            "request_route": request_route,
            "warnings": [],
            "output_profile": output_profile,
            "items": items,
            "data": {
                "action": {"operation": "delete", "executed": write_executed},
                "resource": {"type": "record", "app_key": app_key, "record_id": None, "record_ids": [stringify_backend_id(item) for item in delete_ids]},
                "normalized_payload": normalized_payload,
                "deleted_ids": deleted_ids,
                "failed_ids": failed_ids,
                "items": items,
            },
        }

    def record_write(
        self,
        *,
        profile: str,
        app_key: str,
        operation: str = "insert",
        record_id: int | None = None,
        record_ids: list[int] | None = None,
        values: list[JSONObject] | None = None,
        set: list[JSONObject] | None = None,
        view_id: str | None = None,
        list_type: int | None = None,
        view_key: str | None = None,
        view_name: str | None = None,
        submit_type: str | int = "submit",
        verify_write: bool = True,
        output_profile: str = "normal",
        mode: str | None = None,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_output_profile = self._normalize_public_output_profile(output_profile)
        normalized_operation = operation.strip().lower()
        if normalized_operation not in {"insert", "update", "delete"}:
            raise_tool_error(QingflowApiError.config_error("operation must be insert, update, or delete"))
        if mode is not None:
            raise_tool_error(
                QingflowApiError.config_error(
                    "record_write no longer accepts mode; it now runs internal preflight automatically before apply"
                )
            )
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        normalized_values = list(values or [])
        normalized_set = list(set or [])
        normalized_record_ids = [item for item in (record_ids or []) if isinstance(item, int) and item > 0]
        submit_type_value = self._normalize_record_write_submit_type(submit_type)
        uses_view_scope = any(item is not None for item in (view_id, list_type, view_key, view_name))

        if normalized_operation == "insert":
            if uses_view_scope:
                raise_tool_error(
                    QingflowApiError.config_error(
                        "insert strictly follows applicant-node write scope and does not accept view selectors; use record_insert_schema_get first"
                    )
                )
            if record_id is not None or normalized_record_ids:
                raise_tool_error(QingflowApiError.config_error("insert must not include record_id or record_ids"))
            if normalized_set:
                raise_tool_error(QingflowApiError.config_error("insert must use values, not set"))
            normalized_answers = self._normalize_record_write_clauses(normalized_values, location="values")
            raw_preflight = self._preflight_record_write(
                profile=profile,
                operation="create",
                app_key=app_key,
                apply_id=None,
                answers=normalized_answers,
                fields={},
                force_refresh_form=False,
                view_id=view_id,
                list_type=list_type,
                view_key=view_key,
                view_name=view_name,
                tool_name="写入记录",
            )
            preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
            preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
            normalized_payload: JSONObject = self._record_write_normalized_payload(
                operation="insert",
                record_id=None,
                record_ids=[],
                normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
                submit_type=submit_type_value,
                selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
            )
            if preflight_data.get("blockers"):
                return self._record_write_blocked_response(
                    raw_preflight,
                    operation="insert",
                    normalized_payload=normalized_payload,
                    output_profile=normalized_output_profile,
                    human_review=False,
                    target_resource={"type": "record", "app_key": app_key, "record_id": None, "record_ids": []},
                )
            try:
                raw_apply = self.record_create(
                    profile=profile,
                    app_key=app_key,
                    answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
                    fields={},
                    submit_type=submit_type_value,
                    verify_write=verify_write,
                    force_refresh_form=preflight_used_force_refresh,
                )
            except QingflowApiError as exc:
                self._raise_record_write_permission_error(
                    exc,
                    operation="insert",
                    app_key=app_key,
                    record_id=None,
                    selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
                )
                raise
            return self._record_write_apply_response(
                raw_apply,
                operation="insert",
                normalized_payload=normalized_payload,
                output_profile=normalized_output_profile,
                human_review=False,
                preflight=raw_preflight,
            )

        if normalized_operation == "update":
            if not uses_view_scope:
                raise_tool_error(
                    QingflowApiError.config_error(
                        "update requires a writable browse scope; use record_update_schema_get or record_update_public instead of legacy record_write"
                    )
                )
            if record_id is None or record_id <= 0:
                raise_tool_error(QingflowApiError.config_error("update requires record_id"))
            if normalized_record_ids:
                raise_tool_error(QingflowApiError.config_error("update does not support record_ids"))
            if normalized_values:
                raise_tool_error(QingflowApiError.config_error("update must use set, not values"))
            normalized_answers = self._normalize_record_write_clauses(normalized_set, location="set")
            raw_preflight = self._preflight_record_write(
                profile=profile,
                operation="update",
                app_key=app_key,
                apply_id=record_id,
                answers=normalized_answers,
                fields={},
                force_refresh_form=False,
                view_id=view_id,
                list_type=list_type,
                view_key=view_key,
                view_name=view_name,
                tool_name="写入记录",
            )
            preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
            preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
            normalized_payload = self._record_write_normalized_payload(
                operation="update",
                record_id=record_id,
                record_ids=[],
                normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
                submit_type=submit_type_value,
                selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
            )
            if preflight_data.get("blockers"):
                return self._record_write_blocked_response(
                    raw_preflight,
                    operation="update",
                    normalized_payload=normalized_payload,
                    output_profile=normalized_output_profile,
                    human_review=True,
                    target_resource={"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
                )
            try:
                raw_apply = self.record_update(
                    profile=profile,
                    app_key=app_key,
                    apply_id=record_id,
                    answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
                    fields={},
                    role=1,
                    verify_write=verify_write,
                    force_refresh_form=preflight_used_force_refresh,
                )
            except QingflowApiError as exc:
                self._raise_record_write_permission_error(
                    exc,
                    operation="update",
                    app_key=app_key,
                    record_id=record_id,
                    selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
                )
                raise
            return self._record_write_apply_response(
                raw_apply,
                operation="update",
                normalized_payload=normalized_payload,
                output_profile=normalized_output_profile,
                human_review=True,
                preflight=raw_preflight,
            )

        if view_key is not None or view_name is not None:
            raise_tool_error(
                QingflowApiError.config_error(
                    "delete does not support custom view selectors; use a system view_id/list_type or resolve target record_ids first"
                )
            )
        delete_list_type = self._resolve_record_delete_list_type(view_id=view_id, list_type=list_type)
        if normalized_values or normalized_set:
            raise_tool_error(QingflowApiError.config_error("delete must not include values or set"))
        delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
        if not delete_ids:
            raise_tool_error(QingflowApiError.config_error("delete requires record_id or record_ids"))
        normalized_payload = {
            "operation": "delete",
            "record_id": record_id,
            "record_ids": delete_ids,
            "answers": [],
            "submit_type": submit_type_value,
            "selection": {"view_id": view_id, "list_type": delete_list_type},
        }
        raw_apply = self._record_delete_many(
            profile=profile,
            app_key=app_key,
            record_ids=delete_ids,
            list_type=delete_list_type,
            tool_name="写入记录",
        )
        return self._record_write_apply_response(
            raw_apply,
            operation="delete",
            normalized_payload=normalized_payload,
            output_profile=normalized_output_profile,
            human_review=True,
            preflight=None,
        )

    def _schema_field_payload(
        self,
        profile: str,
        context,
        field: FormField,
        *,
        workflow_node_id: int | None,
        ws_id: int | None,
        schema_mode: str = "applicant",
        browse_writable: bool | None = None,
    ) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        write_hints = self._schema_write_hints(field)
        if schema_mode == "applicant":
            writable = write_hints["writable"]
            supported_write_ops = write_hints["supported_write_ops"]
        else:
            writable = bool(browse_writable)
            supported_write_ops = ["update"] if writable else []
        payload = {
            "field_id": field.que_id,
            "title": field.que_title,
            "que_type": field.que_type,
            "system": field.system,
            "readonly": field.readonly,
            "options": field.options,
            "aliases": field.aliases,
            "role_hints": self._schema_role_hints(field),
            "readable": True,
            "writable": writable,
            "permission_scope": "applicant_node" if schema_mode == "applicant" else "browse_view",
            "workflow_node_id": workflow_node_id,
            "write_kind": write_hints["write_kind"],
            "supported_read_ops": write_hints["supported_read_ops"],
            "supported_write_ops": supported_write_ops,
            "requires_lookup": write_hints["requires_lookup"],
            "requires_upload": write_hints["requires_upload"],
            "requires_existing_row_id": write_hints["requires_existing_row_id"],
            "unsupported_reason": write_hints["unsupported_reason"],
        }
        if field.que_type in RELATION_QUE_TYPES and field.target_app_key:
            payload["target_app_key"] = field.target_app_key
            target_app_name = field.target_app_name_hint or self._resolve_visible_app_name(
                profile,
                context,
                target_app_key=field.target_app_key,
                ws_id=ws_id,
            )
            if target_app_name:
                payload["target_app_name"] = target_app_name
            searchable_fields = _relation_searchable_fields_for_question(field.raw)
            if searchable_fields:
                payload["searchable_fields"] = searchable_fields
        return payload

    def _schema_role_hints(self, field: FormField) -> JSONObject:
        """执行内部辅助逻辑。"""
        field_family = self._schema_field_family(field)
        time_candidate = field.que_type in DATE_QUE_TYPES
        identifier_like = self._schema_is_identifier_like(field, field_family=field_family)
        metric_candidate = bool(field.que_type == 8 and not field.system and not field.readonly and not identifier_like)
        dimension_candidate = bool(
            field.que_type not in ATTACHMENT_QUE_TYPES | RELATION_QUE_TYPES | SUBTABLE_QUE_TYPES | VERIFY_UNSUPPORTED_WRITE_QUE_TYPES
            and not field.system
        )
        return {
            "dimension_candidate": dimension_candidate,
            "metric_candidate": metric_candidate,
            "time_candidate": time_candidate,
            "field_family": field_family,
            "supported_metric_ops": self._schema_supported_metric_ops(field, field_family=field_family),
            "semantic_hints": self._schema_semantic_hint(field, field_family=field_family),
        }

    def _schema_write_hints(self, field: FormField) -> JSONObject:
        """执行内部辅助逻辑。"""
        write_format = _write_format_for_field(field)
        kind = _normalize_optional_text(write_format.get("kind")) or "scalar_text"
        support_level = _normalize_optional_text(write_format.get("support_level")) or "full"
        write_kind = self._schema_write_kind(kind)
        writable = bool(not field.system and not field.readonly and support_level != "unsupported")
        supported_write_ops = ["insert", "update"] if writable else []
        requires_lookup = write_kind in {"member", "department", "relation"}
        requires_upload = write_kind == "attachment"
        requires_existing_row_id = write_kind == "subtable"
        unsupported_reason = _normalize_optional_text(write_format.get("reason"))
        supported_read_ops = ["select"]
        if field.que_type not in ATTACHMENT_QUE_TYPES | SUBTABLE_QUE_TYPES:
            supported_read_ops.append("filter")
        if field.que_type not in ATTACHMENT_QUE_TYPES | RELATION_QUE_TYPES | SUBTABLE_QUE_TYPES:
            supported_read_ops.append("sort")
        return {
            "writable": writable,
            "write_kind": write_kind,
            "supported_read_ops": supported_read_ops,
            "supported_write_ops": supported_write_ops,
            "requires_lookup": requires_lookup,
            "requires_upload": requires_upload,
            "requires_existing_row_id": requires_existing_row_id,
            "unsupported_reason": unsupported_reason,
        }

    def _schema_write_kind(self, kind: str) -> str:
        """执行内部辅助逻辑。"""
        mapping = {
            "single_select": "select",
            "multi_select": "select",
            "member_list": "member",
            "department_list": "department",
            "relation_record": "relation",
            "attachment_list": "attachment",
            "subtable_rows": "subtable",
            "unsupported_direct_write": "unsupported",
            "boolean_label": "scalar",
            "date_string": "scalar",
            "scalar_text": "scalar",
        }
        return mapping.get(kind, "scalar")

    def _resolve_visible_app_name(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        *,
        target_app_key: str,
        ws_id: int | None,
    ) -> str | None:
        """执行内部辅助逻辑。"""
        cache_key = (profile, ws_id, target_app_key)
        if cache_key in self._app_name_cache:
            return self._app_name_cache[cache_key]
        name: str | None = None
        try:
            payload = self.backend.request("GET", context, f"/app/{target_app_key}/baseInfo")
            if isinstance(payload, dict):
                name = (
                    _normalize_optional_text(payload.get("formTitle"))
                    or _normalize_optional_text(payload.get("appName"))
                    or _normalize_optional_text(payload.get("appTitle"))
                )
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            name = None
        self._app_name_cache[cache_key] = name
        return name

    def _lookup_context_answers(self, state: LookupResolutionState) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        merged: dict[int, JSONObject] = {}
        ordered_ids: list[int] = []
        for answer in state.base_answers:
            if not isinstance(answer, dict):
                continue
            que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
            if que_id is None or que_id <= 0:
                continue
            if que_id not in merged:
                ordered_ids.append(que_id)
            merged[que_id] = answer
        for answer in state.normalized_answers:
            if not isinstance(answer, dict):
                continue
            que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
            if que_id is None or que_id <= 0:
                continue
            if que_id not in merged:
                ordered_ids.append(que_id)
            merged[que_id] = answer
        return [merged[que_id] for que_id in ordered_ids]

    def _answers_to_lookup_key_values(self, answers: list[JSONObject], index: FieldIndex) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        values: list[JSONObject] = []
        for answer in answers:
            if not isinstance(answer, dict):
                continue
            open_match = self._answer_to_lookup_key_value(answer, index)
            if open_match is None:
                continue
            values.append(open_match)
        return values

    def _answer_to_lookup_key_value(self, answer: JSONObject, index: FieldIndex) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
        if que_id is None or que_id <= 0:
            return None
        field = index.by_id.get(str(que_id))
        if field is None:
            return None
        ordinal = _coerce_count(answer.get("ordinal"))
        if field.que_type in SUBTABLE_QUE_TYPES:
            rows = answer.get("tableValues")
            row_values: list[list[JSONObject]] = []
            subtable_index = self._subtable_field_index_optional(field)
            if isinstance(rows, list) and subtable_index is not None:
                for row in rows:
                    if not isinstance(row, list):
                        continue
                    normalized_row: list[JSONObject] = []
                    for cell in row:
                        if not isinstance(cell, dict):
                            continue
                        converted = self._answer_to_lookup_key_value(cell, subtable_index)
                        if converted is not None:
                            normalized_row.append(converted)
                    row_values.append(normalized_row)
            return {"keyQueId": field.que_id, "ordinal": ordinal, "values": [], "tableValues": row_values}
        return {
            "keyQueId": field.que_id,
            "ordinal": ordinal,
            "values": self._answer_values_to_lookup_values(answer, field),
        }

    def _answer_values_to_lookup_values(self, answer: JSONObject, field: FormField) -> list[str]:
        """执行内部辅助逻辑。"""
        if field.que_type in RELATION_QUE_TYPES:
            return _relation_ids_from_answer(answer)
        raw_values = answer.get("values")
        if not isinstance(raw_values, list):
            return []
        normalized: list[str] = []
        for item in raw_values:
            value: str | None = None
            if isinstance(item, dict):
                if field.que_type in MEMBER_QUE_TYPES:
                    member_id = _coerce_count(item.get("id", item.get("uid")))
                    value = (
                        str(member_id)
                        if member_id is not None
                        else _normalize_optional_text(item.get("userId"))
                        or _normalize_optional_text(item.get("email"))
                        or _normalize_optional_text(item.get("value"))
                    )
                elif field.que_type in DEPARTMENT_QUE_TYPES:
                    dept_id = _coerce_count(item.get("id", item.get("deptId")))
                    value = (
                        str(dept_id)
                        if dept_id is not None
                        else _normalize_optional_text(item.get("value"))
                        or _normalize_optional_text(item.get("deptName"))
                    )
                else:
                    extracted = _extract_value_item(item)
                    value = _normalize_optional_text(extracted) or (_stringify_json(extracted) if extracted is not None else None)
            else:
                value = _normalize_optional_text(item) or (_stringify_json(item) if item is not None else None)
            if value is not None:
                normalized.append(value)
        return normalized

    def _lookup_key_que_values(self, state: LookupResolutionState) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        answers = self._lookup_context_answers(state)
        if not answers:
            return []
        return self._answers_to_lookup_key_values(answers, state.field_index)

    def _build_lookup_scope_payload(
        self,
        state: LookupResolutionState,
        *,
        keyword: str,
        que_id: int,
        page_num: int = 1,
        page_size: int = LOOKUP_RELATION_FILTER_PAGE_SIZE,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        payload: JSONObject = {
            "role": 1,
            "pageNum": page_num,
            "pageSize": page_size,
            "queId": que_id,
            "searchKey": keyword,
            "beingDraft": False,
        }
        if state.apply_id is not None and state.apply_id > 0:
            payload["applyId"] = state.apply_id
        key_que_values = self._lookup_key_que_values(state)
        if key_que_values:
            payload["keyQueValues"] = key_que_values
        context_answers = self._lookup_context_answers(state)
        if context_answers:
            payload["answers"] = context_answers
        return payload

    def _relation_base_info(self, context, *, app_key: str, field: FormField) -> JSONObject | None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        cache_key = (app_key, field.que_id)
        if cache_key in self._relation_base_info_cache:
            return self._relation_base_info_cache[cache_key]
        payload: JSONObject | None
        try:
            result = self.backend.request("GET", context, "/data/baseInfo", params={"queId": field.que_id})
            payload = result if isinstance(result, dict) else None
        except QingflowApiError as exc:
            if is_auth_like_error(exc):
                raise
            payload = None
        self._relation_base_info_cache[cache_key] = payload or {}
        return payload

    def _relation_searchable_fields_for_field(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        field: FormField,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        direct = _relation_searchable_fields_for_question(field.raw)
        return direct

    def _relation_search_field_defs(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        field: FormField,
        app_key: str,
    ) -> list[tuple[JSONObject, FormField]]:
        """执行内部辅助逻辑。"""
        searchable_fields = self._relation_searchable_fields_for_field(context, app_key=app_key, field=field)
        if not searchable_fields:
            return []
        questions_by_id = {
            cast(int, item["queId"]): item
            for item in _relation_searchable_questions(field.raw)
            if isinstance(item.get("queId"), int)
        }
        field_defs: list[tuple[JSONObject, FormField]] = []
        for item in searchable_fields:
            if not isinstance(item, dict):
                continue
            field_id = _coerce_count(item.get("field_id"))
            title = _normalize_optional_text(item.get("title"))
            if field_id is None or not title:
                continue
            question = questions_by_id.get(field_id) or {
                "queId": field_id,
                "queTitle": title,
                "queType": 2,
            }
            candidate_field = _question_to_form_field(cast(JSONObject, question), is_base_question=False)
            if candidate_field is None:
                candidate_field = FormField(
                    que_id=field_id,
                    que_title=title,
                    que_type=_coerce_count(cast(JSONObject, question).get("queType")) or 2,
                    required=False,
                    readonly=False,
                    system=False,
                    options=[],
                    aliases=[],
                    target_app_key=None,
                    target_app_name_hint=None,
                    member_select_scope_type=None,
                    member_select_scope=None,
                    dept_select_scope_type=None,
                    dept_select_scope=None,
                    raw=cast(JSONObject, question),
                )
            field_defs.append((item, candidate_field))
        return field_defs

    def _build_relation_filter_payload(
        self,
        state: LookupResolutionState,
        *,
        field: FormField,
        keyword: str,
        searchable_fields: list[JSONObject],
        page_num: int = 1,
        page_size: int = LOOKUP_RELATION_FILTER_PAGE_SIZE,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        payload: JSONObject = {
            "appKey": state.app_key,
            "queId": field.que_id,
            "type": 8,
            "pageNum": page_num,
            "pageSize": page_size,
            "queryKey": keyword,
        }
        search_que_ids = [
            cast(int, item["field_id"])
            for item in searchable_fields
            if isinstance(item, dict) and isinstance(item.get("field_id"), int)
        ]
        if search_que_ids:
            payload["searchQueIds"] = search_que_ids
        key_que_values = self._lookup_key_que_values(state)
        if key_que_values:
            payload["keyQueValues"] = key_que_values
        return payload

    def _normalize_relation_candidate_value(self, value: JSONValue) -> JSONValue:
        """执行内部辅助逻辑。"""
        if value is None:
            return None
        if isinstance(value, dict):
            if "rows" in value and isinstance(value.get("rows"), list):
                return _stringify_json(value)
            primary = value.get("value", value.get("name", value.get("label", value.get("email"))))
            normalized = _normalize_optional_text(primary)
            return normalized if normalized is not None else _stringify_json(value)
        if isinstance(value, list):
            normalized_items = [
                self._normalize_relation_candidate_value(item)
                for item in value
            ]
            normalized_items = [item for item in normalized_items if item not in (None, "")]
            if not normalized_items:
                return None
            if len(normalized_items) == 1:
                return normalized_items[0]
            return " / ".join(_stringify_json(item) for item in normalized_items)
        normalized = _normalize_optional_text(value)
        return normalized if normalized is not None else _stringify_json(value)

    def _normalize_relation_candidates(
        self,
        payload: JSONValue,
        *,
        field_defs: list[tuple[JSONObject, FormField]],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if not isinstance(payload, dict):
            return []
        rows = payload.get("list")
        if not isinstance(rows, list):
            return []
        candidates: list[JSONObject] = []
        for item in rows:
            if not isinstance(item, dict):
                continue
            apply_id = item.get("applyId", item.get("apply_id", item.get("id")))
            normalized_apply_id = _normalize_optional_text(apply_id) or (_stringify_json(apply_id) if apply_id is not None else None)
            if not normalized_apply_id:
                continue
            answers = item.get("answers")
            answer_map: dict[int, JSONObject] = {}
            if isinstance(answers, list):
                for answer in answers:
                    if not isinstance(answer, dict):
                        continue
                    answer_que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
                    if answer_que_id is not None:
                        answer_map[answer_que_id] = answer
            fields_payload: JSONObject = {}
            label: str | None = None
            primary_value: str | None = None
            subtitle_parts: list[str] = []
            for search_field, candidate_field in field_defs:
                title = _normalize_optional_text(search_field.get("title")) or candidate_field.que_title
                answer = answer_map.get(candidate_field.que_id)
                if answer is None:
                    continue
                extracted = self._normalize_relation_candidate_value(_extract_answer_field_value(answer, candidate_field))
                if extracted in (None, ""):
                    continue
                fields_payload[title] = extracted
                display_text = _normalize_optional_text(extracted) or _stringify_json(extracted)
                if search_field.get("primary") is True and display_text:
                    primary_value = display_text
                elif display_text:
                    subtitle_parts.append(f"{title}: {display_text}")
            label = primary_value or next(
                (
                    _normalize_optional_text(value) or _stringify_json(value)
                    for value in fields_payload.values()
                    if value not in (None, "")
                ),
                None,
            )
            if not label:
                label = next(
                    (
                        _normalize_optional_text(item.get(key))
                        for key in ("applyName", "applyTitle", "name", "title", "value", "label", "code")
                        if _normalize_optional_text(item.get(key))
                    ),
                    normalized_apply_id,
                )
            if not fields_payload and label:
                fields_payload = {"记录": label}
            if primary_value:
                subtitle_parts = [
                    part for part in subtitle_parts
                    if not part.endswith(primary_value)
                ]
            candidates.append(
                {
                    "apply_id": normalized_apply_id,
                    "label": label,
                    "subtitle": " | ".join(subtitle_parts),
                    "fields": fields_payload,
                }
            )
        return candidates

    def _relation_confirmation_candidates(self, candidates: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        payloads: list[JSONObject] = []
        for candidate in candidates[:LOOKUP_CONFIRMATION_CANDIDATE_LIMIT]:
            payloads.append(
                {
                    "apply_id": candidate.get("apply_id"),
                    "label": candidate.get("label"),
                    "subtitle": candidate.get("subtitle"),
                    "fields": candidate.get("fields"),
                    "score": candidate.get("score"),
                    "match_reason": candidate.get("match_reason"),
                }
            )
        return payloads

    def _resolve_relation_candidates_backend(
        self,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        *,
        keyword: str,
        state: LookupResolutionState,
    ) -> tuple[list[JSONObject], list[JSONObject]]:
        """执行内部辅助逻辑。"""
        field_defs = self._relation_search_field_defs(context, field=field, app_key=state.app_key)
        searchable_fields = [item for item, _ in field_defs]
        try:
            result = self.backend.request(
                "POST",
                context,
                "/data/filter",
                json_body=self._build_relation_filter_payload(
                    state,
                    field=field,
                    keyword=keyword,
                    searchable_fields=searchable_fields,
                ),
            )
        except QingflowApiError as exc:
            raise RecordInputError(
                message=f"relation auto resolution for field '{field.que_title}' is unsupported because backend lookup failed",
                error_code="RELATION_AUTO_RESOLUTION_UNSUPPORTED",
                fix_hint="Retry with an explicit apply_id/applyId object for this field.",
                details={
                    "field": _field_ref_payload(field),
                    "received_value": keyword,
                    "candidate_error": exc.to_dict(),
                },
            )
        return self._normalize_relation_candidates(result, field_defs=field_defs), searchable_fields

    def _lookup_context_is_incomplete(self, state: LookupResolutionState | None) -> bool:
        """执行内部辅助逻辑。"""
        return bool(state is not None and not state.context_complete)

    def _candidate_lookup_uses_runtime_scope(
        self,
        *,
        record_id: int | None,
        workflow_node_id: int | None,
        fields: JSONObject | None,
    ) -> bool:
        """执行内部辅助逻辑。"""
        return bool(
            (record_id is not None and record_id > 0)
            or (workflow_node_id is not None and workflow_node_id > 0)
            or bool(fields)
        )

    def _member_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
        """Return true when the frontend field-scope endpoint is safer than directory expansion."""
        scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
        if field.member_select_scope_type != 2:
            return False
        return bool(
            _scope_has_dynamic_or_external(scope)
            or list(scope.get("depart") or [])
            or list(scope.get("role") or [])
        )

    def _department_candidate_static_preview_should_use_backend(self, field: FormField) -> bool:
        """Return true when static preview would otherwise need ContactAuth-only directory APIs."""
        scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
        if field.dept_select_scope_type != 2:
            return False
        if _scope_has_dynamic_or_external(scope):
            return True
        return bool(_normalize_bool(scope.get("includeSubDeparts")) or not list(scope.get("depart") or []))

    def _build_candidate_lookup_state(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        record_id: int | None,
        workflow_node_id: int | None,
        fields: JSONObject,
    ) -> LookupResolutionState:
        """执行内部辅助逻辑。"""
        index = self._get_field_index(profile, context, app_key, force_refresh=False)
        apply_id = record_id if isinstance(record_id, int) and record_id > 0 else None
        base_answers: list[JSONObject] = []
        context_complete = True
        if apply_id is not None:
            try:
                base_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
            except QingflowApiError as exc:
                if not _is_optional_record_auxiliary_lookup_error(exc):
                    raise
                context_complete = False
        state = LookupResolutionState(
            operation="update" if apply_id is not None else "insert",
            app_key=app_key,
            apply_id=apply_id,
            workflow_node_id=workflow_node_id if isinstance(workflow_node_id, int) and workflow_node_id > 0 else None,
            force_refresh_form=False,
            context_complete=context_complete,
            field_index=index,
            base_answers=base_answers,
            normalized_answers=[],
            confirmation_requests=[],
            resolved_fields=[],
            unresolved_field_ids=set(),
            unresolved_subtable_cells=set(),
        )
        if fields:
            state.normalized_answers = self._resolve_answers(
                profile,
                context,
                app_key,
                answers=[],
                fields=fields,
                force_refresh_form=False,
                resolution_state=state,
            )
        return state

    def _raise_lookup_context_unavailable(
        self,
        *,
        field: FormField,
        kind: str,
        value: JSONValue,
        location: str,
        explicit_fix_hint: str,
    ) -> None:
        """执行内部辅助逻辑。"""
        raise RecordInputError(
            message=(
                f"{kind} auto resolution for field '{field.que_title}' requires the current record context, "
                "but update preflight could not load that record"
            ),
            error_code=f"{kind.upper()}_LOOKUP_CONTEXT_UNAVAILABLE",
            fix_hint=explicit_fix_hint,
            details={
                "field": _field_ref_payload(field),
                "location": location,
                "received_value": value,
            },
        )

    def _normalize_backend_member_candidates(self, payload: JSONValue, *, external: bool) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        candidates: list[JSONObject] = []
        if not isinstance(payload, dict):
            return candidates
        buckets: list[tuple[str, JSONValue]] = []
        if isinstance(payload.get("member"), list):
            buckets.append(("member_scope", payload.get("member")))
        if external and isinstance(payload.get("externalMemberList"), list):
            buckets.append(("external_member_scope", payload.get("externalMemberList")))
        for source_kind, items in buckets:
            for item in cast(list[JSONValue], items):
                normalized = _normalize_candidate_member(item, source_kind=source_kind)
                if normalized is not None:
                    candidates.append(normalized)
        return candidates

    def _normalize_backend_department_candidates(self, payload: JSONValue, *, external: bool) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        candidates: list[JSONObject] = []
        if not isinstance(payload, dict):
            return candidates
        rows = payload.get("list")
        if not isinstance(rows, list):
            return candidates
        source_kind = "external_department_scope" if external else "department_scope"
        for item in rows:
            normalized = _normalize_candidate_department(item, source_kind=source_kind)
            if normalized is None:
                continue
            path = self._department_candidate_path(item)
            if path:
                normalized["path"] = path
            candidates.append(normalized)
        return candidates

    def _department_candidate_path(self, value: JSONValue) -> str | None:
        """执行内部辅助逻辑。"""
        if not isinstance(value, dict):
            return None
        name = _normalize_optional_text(value.get("deptName", value.get("value", value.get("name"))))
        parent_names = [
            _normalize_optional_text(item)
            for item in cast(list[JSONValue], value.get("parentDeptNameList") or [])
            if _normalize_optional_text(item)
        ]
        if not name:
            return None
        if not parent_names:
            return name
        return " / ".join(list(reversed(parent_names)) + [name])

    def _rank_member_candidates(self, keyword: str, candidates: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        requested = _normalize_field_lookup_key(keyword)
        ranked: list[JSONObject] = []
        for candidate in candidates:
            variants = [
                ("name", _normalize_optional_text(candidate.get("value"))),
                ("user_id", _normalize_optional_text(candidate.get("userId"))),
                ("email", _normalize_optional_text(candidate.get("email"))),
            ]
            best_score = 0.0
            best_reason = "none"
            for label, text in variants:
                if not text:
                    continue
                score, reason = _score_candidate_text(requested, text, fuzzy=True, label="title")
                if label in {"user_id", "email"} and reason == "title_exact":
                    score = 1.0
                    reason = f"{label}_exact"
                elif label in {"user_id", "email"} and reason == "title_prefix":
                    score = max(score, 0.94)
                    reason = f"{label}_prefix"
                elif label in {"user_id", "email"} and reason == "title_contains":
                    score = max(score, 0.9)
                    reason = f"{label}_contains"
                elif label in {"user_id", "email"} and reason == "title_fuzzy":
                    reason = f"{label}_fuzzy"
                if score > best_score:
                    best_score = score
                    best_reason = reason
            if best_score <= 0:
                continue
            payload = dict(candidate)
            payload["score"] = round(best_score, 4)
            payload["match_reason"] = best_reason
            ranked.append(payload)
        ranked.sort(
            key=lambda item: (
                float(item.get("score", 0.0)),
                _normalize_optional_text(item.get("value")) or "",
                _coerce_count(item.get("id")) or 0,
            ),
            reverse=True,
        )
        return ranked

    def _rank_department_candidates(self, keyword: str, candidates: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        requested = _normalize_field_lookup_key(keyword)
        ranked: list[JSONObject] = []
        for candidate in candidates:
            variants = [
                ("path", _normalize_optional_text(candidate.get("path"))),
                ("name", _normalize_optional_text(candidate.get("value"))),
            ]
            best_score = 0.0
            best_reason = "none"
            for label, text in variants:
                if not text:
                    continue
                score, reason = _score_candidate_text(requested, text, fuzzy=True, label="title")
                if label == "path" and score > 0:
                    score += 0.02
                    reason = reason.replace("title_", "path_")
                if score > best_score:
                    best_score = score
                    best_reason = reason
            if best_score <= 0:
                continue
            payload = dict(candidate)
            payload["score"] = round(min(best_score, 1.0), 4)
            payload["match_reason"] = best_reason
            ranked.append(payload)
        ranked.sort(
            key=lambda item: (
                float(item.get("score", 0.0)),
                _normalize_optional_text(item.get("path")) or _normalize_optional_text(item.get("value")) or "",
                _coerce_count(item.get("id")) or 0,
            ),
            reverse=True,
        )
        return ranked

    def _rank_relation_candidates(
        self,
        keyword: str,
        candidates: list[JSONObject],
        *,
        searchable_fields: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        requested = _normalize_field_lookup_key(keyword)
        primary_titles = {
            _normalize_field_lookup_key(_stringify_json(item.get("title")))
            for item in searchable_fields
            if isinstance(item, dict) and item.get("primary") is True
        }
        ranked: list[JSONObject] = []
        for candidate in candidates:
            fields_payload = candidate.get("fields")
            best_score = 0.0
            best_reason = "none"
            if isinstance(fields_payload, dict) and fields_payload:
                for title, raw_text in fields_payload.items():
                    text = _normalize_optional_text(raw_text) or (_stringify_json(raw_text) if raw_text is not None else None)
                    if not text:
                        continue
                    score, reason = _score_candidate_text(requested, text, fuzzy=True, label="title")
                    if score > 0 and _normalize_field_lookup_key(title) in primary_titles:
                        score = min(score + 0.03, 1.0)
                        reason = f"primary_{reason}"
                    if score > best_score:
                        best_score = score
                        best_reason = reason
            else:
                label = _normalize_optional_text(candidate.get("label"))
                if label:
                    best_score, best_reason = _score_candidate_text(requested, label, fuzzy=True, label="title")
            if best_score <= 0:
                continue
            payload = dict(candidate)
            payload["score"] = round(best_score, 4)
            payload["match_reason"] = best_reason
            ranked.append(payload)
        ranked.sort(
            key=lambda item: (
                float(item.get("score", 0.0)),
                _normalize_optional_text(item.get("label")) or "",
                _normalize_optional_text(item.get("apply_id")) or "",
            ),
            reverse=True,
        )
        return ranked

    def _resolve_ranked_lookup_candidate(self, ranked_candidates: list[JSONObject]) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        if not ranked_candidates:
            return None
        top = ranked_candidates[0]
        top_score = float(top.get("score", 0.0))
        second_score = float(ranked_candidates[1].get("score", 0.0)) if len(ranked_candidates) > 1 else 0.0
        if top_score >= LOOKUP_RESOLUTION_MIN_SCORE and (len(ranked_candidates) == 1 or top_score - second_score >= LOOKUP_RESOLUTION_MIN_MARGIN):
            return top
        return None

    def _append_lookup_resolution(
        self,
        state: LookupResolutionState | None,
        *,
        field: FormField,
        kind: str,
        input_value: JSONValue,
        resolved_to: JSONObject,
        method: str,
        confidence: float,
    ) -> None:
        """执行内部辅助逻辑。"""
        if state is None:
            return
        state.resolved_fields.append(
            {
                "field": field.que_title,
                "kind": kind,
                "input": input_value,
                "resolved_to": resolved_to,
                "method": method,
                "confidence": round(confidence, 4),
            }
        )

    def _build_confirmation_request(
        self,
        *,
        field: FormField,
        kind: str,
        input_value: JSONValue,
        candidates: list[JSONObject],
        parent_field: FormField | None = None,
        row_ordinal: int | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        payload: JSONObject = {
            "field": field.que_title,
            "kind": kind,
            "input": input_value,
            "candidates": candidates[:LOOKUP_CONFIRMATION_CANDIDATE_LIMIT],
            "retry_hint": "Choose one candidate and retry with an explicit id/object for this field.",
            "field_ref": _field_ref_payload(field),
        }
        if parent_field is not None:
            payload["parent_field"] = _field_ref_payload(parent_field)
        if row_ordinal is not None:
            payload["row_ordinal"] = row_ordinal
        return payload

    def _mark_lookup_confirmation(
        self,
        state: LookupResolutionState | None,
        *,
        field: FormField,
        request: JSONObject,
        parent_field: FormField | None = None,
        row_ordinal: int | None = None,
    ) -> None:
        """执行内部辅助逻辑。"""
        if state is None:
            return
        state.confirmation_requests.append(request)
        state.unresolved_field_ids.add(field.que_id)
        if parent_field is not None and row_ordinal is not None:
            state.unresolved_subtable_cells.add((parent_field.que_id, row_ordinal, field.que_id))

    def _candidate_resolution_method(self, candidate: JSONObject, *, explicit_method: str | None = None) -> str:
        """执行内部辅助逻辑。"""
        if explicit_method:
            return explicit_method
        reason = _normalize_optional_text(candidate.get("match_reason")) or "resolved"
        if "exact" in reason:
            return "exact_unique_match"
        if "fuzzy" in reason:
            return "fuzzy_unique_high_confidence"
        if "prefix" in reason:
            return "prefix_unique_match"
        if "contains" in reason:
            return "contains_unique_match"
        return reason

    def _candidate_scope_fix_hint(
        self,
        *,
        kind: str,
        state: LookupResolutionState | None,
        explicit_guidance: str,
    ) -> str:
        """执行内部辅助逻辑。"""
        if state is not None and self._candidate_lookup_uses_runtime_scope(
            record_id=state.apply_id,
            workflow_node_id=state.workflow_node_id,
            fields={item.get("queId"): True for item in state.normalized_answers if isinstance(item, dict)},
        ):
            return (
                f"Use record_{kind}_candidates with the same record_id/workflow_node_id/fields context to inspect the "
                "backend runtime candidate scope and choose one returned item exactly. "
                f"{explicit_guidance}"
            )
        return (
            f"Use record_{kind}_candidates to inspect the allowed candidate scope and choose one returned item exactly. "
            f"{explicit_guidance}"
        )

    def _resolve_member_candidates(self, context, field: FormField, *, keyword: str) -> list[JSONObject]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        scope_type = field.member_select_scope_type
        scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
        if _scope_is_default_all(scope_type, scope, keys=("member", "depart", "role")):
            candidates = [
                _normalize_candidate_member(item, source_kind="workspace")
                for item in self._search_workspace_members(context, keyword=keyword)
            ]
            filtered = [item for item in candidates if item is not None]
            filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
            return filtered
        if scope_type != 2:
            raise_tool_error(
                QingflowApiError(
                    category="not_supported",
                    message="record_member_candidates only supports explicit member scopes on the applicant-node form",
                    details={
                        "error_code": "MEMBER_CANDIDATES_SCOPE_UNSUPPORTED",
                        "field_id": field.que_id,
                        "field_title": field.que_title,
                        "member_select_scope_type": scope_type,
                    },
                )
            )
        if _scope_has_dynamic_or_external(scope):
            raise_tool_error(
                QingflowApiError(
                    category="not_supported",
                    message="record_member_candidates does not support dynamic or external member scopes safely",
                    details={
                        "error_code": "MEMBER_CANDIDATES_SCOPE_UNSUPPORTED",
                        "field_id": field.que_id,
                        "field_title": field.que_title,
                        "member_select_scope_type": scope_type,
                    },
                )
            )

        merged: dict[str, JSONObject] = {}
        for item in cast(list[JSONValue], scope.get("member") or []):
            normalized = _normalize_candidate_member(item, source_kind="member")
            if normalized is None:
                continue
            self._merge_member_candidate(merged, normalized)

        include_sub = _normalize_bool(scope.get("includeSubDeparts"))
        for item in cast(list[JSONValue], scope.get("depart") or []):
            dept_id = _coerce_count(item.get("deptId", item.get("id")) if isinstance(item, dict) else item)
            if dept_id is None:
                continue
            dept_name = _normalize_optional_text(item.get("deptName") if isinstance(item, dict) else None)
            for member in self._list_members_by_department(context, dept_id=dept_id, include_sub_departments=include_sub):
                normalized = _normalize_candidate_member(
                    member,
                    source_kind="department",
                    source_id=dept_id,
                    source_value=dept_name,
                )
                if normalized is None:
                    continue
                self._merge_member_candidate(merged, normalized)

        for item in cast(list[JSONValue], scope.get("role") or []):
            role_id = _coerce_count(item.get("roleId", item.get("id")) if isinstance(item, dict) else item)
            if role_id is None:
                continue
            role_name = _normalize_optional_text(item.get("roleName") if isinstance(item, dict) else None)
            for member in self._list_members_by_role(context, role_id=role_id):
                normalized = _normalize_candidate_member(
                    member,
                    source_kind="role",
                    source_id=role_id,
                    source_value=role_name,
                )
                if normalized is None:
                    continue
                self._merge_member_candidate(merged, normalized)

        candidates = list(merged.values())
        filtered = _filter_member_candidates(candidates, keyword)
        filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
        return filtered

    def _resolve_department_candidates(self, context, field: FormField, *, keyword: str) -> list[JSONObject]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        scope_type = field.dept_select_scope_type
        scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
        if _scope_has_dynamic_or_external(scope):
            raise_tool_error(
                QingflowApiError(
                    category="not_supported",
                    message="record_department_candidates does not support dynamic or external department scopes safely",
                    details={
                        "error_code": "DEPARTMENT_CANDIDATES_SCOPE_UNSUPPORTED",
                        "field_id": field.que_id,
                        "field_title": field.que_title,
                        "dept_select_scope_type": scope_type,
                    },
                )
            )
        merged: dict[str, JSONObject] = {}
        include_sub = _normalize_bool(scope.get("includeSubDeparts"))
        if _scope_is_default_all(scope_type, scope, keys=("depart",)):
            for item in self._search_workspace_departments(context, keyword=keyword):
                normalized = _normalize_candidate_department(item, source_kind="workspace")
                if normalized is not None:
                    self._merge_department_candidate(merged, normalized)
        else:
            if scope_type != 2:
                raise_tool_error(
                    QingflowApiError(
                        category="not_supported",
                        message="record_department_candidates only supports explicit or default-all department scopes on the applicant-node form",
                        details={
                            "error_code": "DEPARTMENT_CANDIDATES_SCOPE_UNSUPPORTED",
                            "field_id": field.que_id,
                            "field_title": field.que_title,
                            "dept_select_scope_type": scope_type,
                        },
                    )
                )
            for item in cast(list[JSONValue], scope.get("depart") or []):
                dept_id = _coerce_count(item.get("deptId", item.get("id")) if isinstance(item, dict) else item)
                if dept_id is None:
                    continue
                dept_name = _normalize_optional_text(item.get("deptName", item.get("value")) if isinstance(item, dict) else None)
                configured_candidate = _normalize_candidate_department(
                    {"deptId": dept_id, "deptName": dept_name},
                    source_kind="department",
                    source_id=dept_id,
                    source_value=dept_name,
                )
                if configured_candidate is not None:
                    self._merge_department_candidate(merged, configured_candidate)
                if include_sub:
                    for dept in self._list_departments_by_scope(context, dept_id=dept_id, include_sub_departments=True):
                        normalized = _normalize_candidate_department(
                            dept,
                            source_kind="department",
                            source_id=dept_id,
                            source_value=dept_name,
                        )
                        if normalized is not None:
                            self._merge_department_candidate(merged, normalized)
        filtered = _filter_department_candidates(list(merged.values()), keyword)
        filtered.sort(key=lambda item: (_normalize_optional_text(item.get("value")) or "", _coerce_count(item.get("id")) or 0))
        return filtered

    def _merge_member_candidate(self, merged: dict[str, JSONObject], candidate: JSONObject) -> None:
        """执行内部辅助逻辑。"""
        key = _member_candidate_key(candidate)
        if not key:
            return
        existing = merged.get(key)
        if existing is None:
            merged[key] = candidate
            return
        if not existing.get("userId") and candidate.get("userId"):
            existing["userId"] = candidate["userId"]
        if not existing.get("email") and candidate.get("email"):
            existing["email"] = candidate["email"]
        if (not _normalize_optional_text(existing.get("value"))) and _normalize_optional_text(candidate.get("value")):
            existing["value"] = candidate["value"]
        existing_sources = existing.setdefault("sources", [])
        candidate_sources = candidate.get("sources")
        if isinstance(existing_sources, list) and isinstance(candidate_sources, list):
            seen = {json.dumps(item, ensure_ascii=False, sort_keys=True) for item in existing_sources if isinstance(item, dict)}
            for source in candidate_sources:
                if not isinstance(source, dict):
                    continue
                marker = json.dumps(source, ensure_ascii=False, sort_keys=True)
                if marker in seen:
                    continue
                existing_sources.append(source)
                seen.add(marker)

    def _list_members_by_department(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        dept_id: int,
        include_sub_departments: bool,
    ) -> list[dict[str, Any]]:
        """执行内部辅助逻辑。"""
        dept_ids = {dept_id}
        if include_sub_departments:
            dept_ids.update(self._expand_department_ids(context, root_dept_id=dept_id))
        merged: dict[str, dict[str, Any]] = {}
        for current_dept_id in dept_ids:
            for item in self._fetch_internal_members(context, department_id=current_dept_id, role_id=None):
                member_key = _member_candidate_key(item)
                if member_key and member_key not in merged:
                    merged[member_key] = item
        return list(merged.values())

    def _list_members_by_role(self, context, *, role_id: int) -> list[dict[str, Any]]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        return self._fetch_internal_members(context, department_id=None, role_id=role_id)

    def _list_departments_by_scope(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        dept_id: int,
        include_sub_departments: bool,
    ) -> list[dict[str, Any]]:
        """执行内部辅助逻辑。"""
        dept_ids = {dept_id}
        if include_sub_departments:
            dept_ids.update(self._expand_department_ids(context, root_dept_id=dept_id))
        merged: dict[str, dict[str, Any]] = {}
        for item in self._list_all_departments(context):
            department_key = _department_candidate_key(item)
            current_dept_id = _coerce_count(item.get("deptId", item.get("id")))
            if not department_key or current_dept_id is None or current_dept_id not in dept_ids or department_key in merged:
                continue
            merged[department_key] = item
        return list(merged.values())

    def _list_all_departments(self, context) -> list[dict[str, Any]]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        queue: list[int | None] = [None]
        seen_ids: set[int] = set()
        requested_parents: set[int | None] = set()
        items: list[dict[str, Any]] = []
        while queue:
            current_parent = queue.pop(0)
            if current_parent in requested_parents:
                continue
            requested_parents.add(current_parent)
            params: JSONObject = {}
            if current_parent is not None:
                params["parentDeptId"] = current_parent
            result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
            page_items = _directory_items(result)
            if not page_items and current_parent is None:
                result = self.backend.request("GET", context, "/contact/subDeptList", params={"parentDeptId": 0})
                page_items = _directory_items(result)
            for item in page_items:
                if not isinstance(item, dict):
                    continue
                dept_id = _coerce_count(item.get("deptId", item.get("id")))
                if dept_id is None or dept_id in seen_ids:
                    continue
                seen_ids.add(dept_id)
                normalized = dict(item)
                if current_parent is not None and "parentDeptId" not in normalized:
                    normalized["parentDeptId"] = current_parent
                items.append(normalized)
                queue.append(dept_id)
        return items

    def _merge_department_candidate(self, merged: dict[str, JSONObject], candidate: JSONObject) -> None:
        """执行内部辅助逻辑。"""
        key = _department_candidate_key(candidate)
        if not key:
            return
        existing = merged.get(key)
        if existing is None:
            merged[key] = candidate
            return
        if (not _normalize_optional_text(existing.get("value"))) and _normalize_optional_text(candidate.get("value")):
            existing["value"] = candidate["value"]
        existing_sources = existing.setdefault("sources", [])
        candidate_sources = candidate.get("sources")
        if isinstance(existing_sources, list) and isinstance(candidate_sources, list):
            seen = {json.dumps(item, ensure_ascii=False, sort_keys=True) for item in existing_sources if isinstance(item, dict)}
            for source in candidate_sources:
                if not isinstance(source, dict):
                    continue
                marker = json.dumps(source, ensure_ascii=False, sort_keys=True)
                if marker in seen:
                    continue
                existing_sources.append(source)
                seen.add(marker)

    def _expand_department_ids(self, context, *, root_dept_id: int) -> set[int]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        seen: set[int] = set()
        queue: list[int] = [root_dept_id]
        while queue:
            current_dept_id = queue.pop(0)
            if current_dept_id in seen:
                continue
            seen.add(current_dept_id)
            result = self.backend.request("GET", context, "/contact/subDeptList", params={"parentDeptId": current_dept_id})
            for item in _directory_items(result):
                if not isinstance(item, dict):
                    continue
                child_id = _coerce_count(item.get("deptId", item.get("id")))
                if child_id is None or child_id in seen:
                    continue
                queue.append(child_id)
        return seen

    def _fetch_internal_members(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        department_id: int | None,
        role_id: int | None,
    ) -> list[dict[str, Any]]:
        """执行内部辅助逻辑。"""
        current_page = 1
        fetched_pages = 0
        seen: dict[str, dict[str, Any]] = {}
        while fetched_pages < MAX_MEMBER_SCOPE_FETCH_PAGES:
            params: JSONObject = {
                "pageNum": current_page,
                "pageSize": DEFAULT_MEMBER_SCOPE_FETCH_PAGE_SIZE,
                "containDisable": False,
            }
            if department_id is not None:
                params["deptId"] = department_id
            if role_id is not None:
                params["roleId"] = role_id
            result = self.backend.request("GET", context, "/contact", params=params)
            page_items = _directory_items(result)
            for item in page_items:
                if not isinstance(item, dict):
                    continue
                normalized = dict(item)
                member_key = _member_candidate_key(normalized)
                if not member_key or member_key in seen:
                    continue
                seen[member_key] = normalized
            fetched_pages += 1
            if not _directory_has_more(
                result,
                current_page=current_page,
                page_size=DEFAULT_MEMBER_SCOPE_FETCH_PAGE_SIZE,
                returned_items=len(page_items),
            ):
                break
            current_page += 1
        return list(seen.values())

    def _search_workspace_members(self, context, *, keyword: str) -> list[dict[str, Any]]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        return self._search_workspace_directory_dimension(context, dimension="MEMBER", bucket_key="member", keyword=keyword)

    def _search_workspace_departments(self, context, *, keyword: str) -> list[dict[str, Any]]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        return self._search_workspace_directory_dimension(context, dimension="DEPT", bucket_key="dept", keyword=keyword)

    def _search_workspace_directory_dimension(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        dimension: str,
        bucket_key: str,
        keyword: str,
    ) -> list[dict[str, Any]]:
        """执行内部辅助逻辑。"""
        current_page = 1
        fetched_pages = 0
        seen: dict[str, dict[str, Any]] = {}
        while fetched_pages < MAX_MEMBER_SCOPE_FETCH_PAGES:
            result = self.backend.request(
                "POST",
                context,
                "/member/search",
                json_body={
                    "dimensions": [dimension],
                    "searchKey": keyword,
                    "pageNum": current_page,
                    "pageSize": DEFAULT_MEMBER_SCOPE_FETCH_PAGE_SIZE,
                },
            )
            bucket_payload = _member_search_bucket_payload(result, bucket_key=bucket_key)
            page_items = _directory_items(bucket_payload)
            for item in page_items:
                if not isinstance(item, dict):
                    continue
                normalized = dict(item)
                item_key = _member_candidate_key(normalized) if bucket_key == "member" else _department_candidate_key(normalized)
                if not item_key or item_key in seen:
                    continue
                seen[item_key] = normalized
            fetched_pages += 1
            if not _directory_has_more(
                bucket_payload,
                current_page=current_page,
                page_size=DEFAULT_MEMBER_SCOPE_FETCH_PAGE_SIZE,
                returned_items=len(page_items),
            ):
                break
            current_page += 1
        return list(seen.values())

    def _schema_field_family(self, field: FormField) -> str:
        """执行内部辅助逻辑。"""
        if self._schema_is_identifier_like(field):
            return "text"
        que_type = field.que_type
        if que_type == 8:
            return "number"
        if que_type in DATE_QUE_TYPES:
            return "date"
        if que_type in SINGLE_SELECT_QUE_TYPES | MULTI_SELECT_QUE_TYPES:
            return "category"
        if que_type in MEMBER_QUE_TYPES:
            return "member"
        if que_type in DEPARTMENT_QUE_TYPES:
            return "department"
        if que_type in BOOLEAN_QUE_TYPES:
            return "boolean"
        if que_type in ATTACHMENT_QUE_TYPES | RELATION_QUE_TYPES | SUBTABLE_QUE_TYPES:
            return "unknown"
        if que_type is None:
            return "unknown"
        return "text"

    def _schema_is_identifier_like(self, field: FormField, *, field_family: str | None = None) -> bool:
        """执行内部辅助逻辑。"""
        normalized_title = _normalize_field_lookup_key(field.que_title)
        if field.que_id == 0:
            return True
        if any(
            token in normalized_title for token in ("编号", "单号", "流水号", "编码", "序号", "uid", "id", "code")
        ):
            return True
        return False

    def _schema_supported_metric_ops(self, field: FormField, *, field_family: str) -> list[str]:
        """执行内部辅助逻辑。"""
        if field.que_type in ATTACHMENT_QUE_TYPES | RELATION_QUE_TYPES | SUBTABLE_QUE_TYPES:
            return []
        if self._schema_is_identifier_like(field, field_family=field_family):
            return ["distinct_count"]
        if field_family == "number":
            return ["sum", "avg", "min", "max", "distinct_count"]
        if field_family in {"date", "category", "member", "department", "text", "boolean", "unknown"}:
            return ["distinct_count"]
        return []

    def _schema_semantic_hint(self, field: FormField, *, field_family: str) -> str:
        """执行内部辅助逻辑。"""
        if self._schema_is_identifier_like(field, field_family=field_family):
            return "unknown"
        if field_family != "number":
            return "unknown"
        normalized_title = _normalize_field_lookup_key(field.que_title)
        if any(token in normalized_title for token in ("比例", "比率", "占比", "转化率", "渗透率", "毛利率", "折扣率", "百分比")):
            return "ratio_like"
        if any(token in normalized_title for token in ("金额", "arr", "mrr", "收入", "成本", "价格", "单价", "费用", "预算", "报价", "应收", "实收", "总额", "价税")):
            return "money_like"
        if any(token in normalized_title for token in ("数量", "人数", "单量", "个数", "次数", "件数", "台数", "天数", "笔数", "数")):
            return "quantity_like"
        return "unknown"

    def _resolve_field_by_id(self, field_id: int | None, index: FieldIndex, *, location: str) -> FormField:
        """执行内部辅助逻辑。"""
        if field_id is None:
            raise RecordInputError(
                message=f"{location} requires field_id",
                error_code="MISSING_FIELD_ID",
                fix_hint="Use record_browse_schema_get and pass a valid field_id from the schema response.",
            )
        field = index.by_id.get(str(field_id))
        if field is None:
            raise RecordInputError(
                message=f"{location} references unknown field_id '{field_id}'",
                error_code="FIELD_NOT_FOUND",
                fix_hint="Use record_browse_schema_get to confirm the exact field_id before calling record_analyze.",
                details={"location": location, "field_id": field_id},
            )
        return field

    def _ensure_allowed_analyze_keys(
        self,
        item: JSONObject,
        *,
        location: str,
        allowed_keys: set[str],
        example: str,
    ) -> None:
        """执行内部辅助逻辑。"""
        unexpected_keys = sorted(str(key) for key in item.keys() if str(key) not in allowed_keys)
        if unexpected_keys:
            raise RecordInputError(
                message=f"{location} contains unsupported keys: {unexpected_keys}",
                error_code="UNSUPPORTED_ANALYZE_DSL_KEY",
                fix_hint=f"Use {location} in the documented DSL shape only, for example {example}.",
                details={
                    "location": location,
                    "unexpected_keys": unexpected_keys,
                    "allowed_keys": sorted(allowed_keys),
                },
            )

    def _validate_analyze_filter_value(
        self,
        *,
        field: FormField,
        op: str,
        value: JSONValue,
        location: str,
    ) -> JSONValue:
        """执行内部辅助逻辑。"""
        if op in {"is_null", "not_null"}:
            if value is not None:
                raise RecordInputError(
                    message=f"{location} with op '{op}' must omit value",
                    error_code="INVALID_ANALYZE_FILTER_VALUE",
                    fix_hint="Remove value when using is_null or not_null.",
                    details={"location": location, "op": op, "field": _field_ref_payload(field)},
                )
            return None

        if op in {"in", "not_in"}:
            if not isinstance(value, list) or not value:
                raise RecordInputError(
                    message=f"{location} with op '{op}' requires a non-empty array value",
                    error_code="INVALID_ANALYZE_FILTER_VALUE",
                    fix_hint="Use an array for in/not_in, for example ['A', 'B'].",
                    details={"location": location, "op": op, "field": _field_ref_payload(field), "received_value": value},
                )
            if field.que_type in DATE_QUE_TYPES:
                for idx, item in enumerate(value):
                    self._validate_strict_date_filter_value(item, location=f"{location}.value[{idx}]")
            return value

        if op == "between":
            lower, upper = _coerce_filter_range(value)
            if lower is None and upper is None:
                raise RecordInputError(
                    message=f"{location} with op 'between' requires a two-bound range",
                    error_code="INVALID_ANALYZE_FILTER_VALUE",
                    fix_hint="Use value like ['2026-03-01', '2026-03-31'] or [100, 200].",
                    details={"location": location, "op": op, "field": _field_ref_payload(field), "received_value": value},
                )
            if field.que_type == 8:
                lower_amount = _coerce_amount(lower) if lower is not None else None
                upper_amount = _coerce_amount(upper) if upper is not None else None
                if lower is not None and lower_amount is None:
                    raise RecordInputError(
                        message=f"{location} lower bound is not a valid numeric value",
                        error_code="INVALID_ANALYZE_FILTER_VALUE",
                        fix_hint="Use numeric bounds when filtering a numeric field.",
                        details={"location": location, "bound": "lower", "field": _field_ref_payload(field), "received_value": lower},
                    )
                if upper is not None and upper_amount is None:
                    raise RecordInputError(
                        message=f"{location} upper bound is not a valid numeric value",
                        error_code="INVALID_ANALYZE_FILTER_VALUE",
                        fix_hint="Use numeric bounds when filtering a numeric field.",
                        details={"location": location, "bound": "upper", "field": _field_ref_payload(field), "received_value": upper},
                    )
                if lower_amount is not None and upper_amount is not None and lower_amount > upper_amount:
                    raise RecordInputError(
                        message=f"{location} lower bound cannot be greater than upper bound",
                        error_code="INVALID_ANALYZE_FILTER_RANGE",
                        fix_hint="Swap the between bounds so the lower value comes first.",
                        details={"location": location, "field": _field_ref_payload(field), "received_value": value},
                    )
                return [lower, upper]
            if field.que_type in DATE_QUE_TYPES:
                lower_dt = self._validate_strict_date_filter_value(lower, location=f"{location}.value[0]") if lower is not None else None
                upper_dt = self._validate_strict_date_filter_value(upper, location=f"{location}.value[1]") if upper is not None else None
                if lower_dt is not None and upper_dt is not None and lower_dt > upper_dt:
                    raise RecordInputError(
                        message=f"{location} lower bound cannot be later than upper bound",
                        error_code="INVALID_ANALYZE_FILTER_RANGE",
                        fix_hint="Swap the date bounds so the earlier date comes first.",
                        details={"location": location, "field": _field_ref_payload(field), "received_value": value},
                    )
                return [lower, upper]
            raise RecordInputError(
                message=f"{location} with op 'between' requires a numeric or date/time field",
                error_code="INVALID_ANALYZE_FILTER_FIELD_TYPE",
                fix_hint="Use between only on numeric or date/time fields.",
                details={"location": location, "field": _field_ref_payload(field), "op": op},
            )

        if op in {"gt", "gte", "lt", "lte"}:
            if value is None or isinstance(value, (list, dict)):
                raise RecordInputError(
                    message=f"{location} with op '{op}' requires a single scalar value",
                    error_code="INVALID_ANALYZE_FILTER_VALUE",
                    fix_hint="Use a single number or date string for comparison filters.",
                    details={"location": location, "op": op, "field": _field_ref_payload(field), "received_value": value},
                )
            if field.que_type == 8:
                if _coerce_amount(value) is None:
                    raise RecordInputError(
                        message=f"{location} requires a numeric comparison value",
                        error_code="INVALID_ANALYZE_FILTER_VALUE",
                        fix_hint="Use numeric values with gt/gte/lt/lte on numeric fields.",
                        details={"location": location, "field": _field_ref_payload(field), "received_value": value},
                    )
                return value
            if field.que_type in DATE_QUE_TYPES:
                self._validate_strict_date_filter_value(value, location=f"{location}.value")
                return value
            raise RecordInputError(
                message=f"{location} with op '{op}' requires a numeric or date/time field",
                error_code="INVALID_ANALYZE_FILTER_FIELD_TYPE",
                fix_hint="Use gt/gte/lt/lte only on numeric or date/time fields.",
                details={"location": location, "field": _field_ref_payload(field), "op": op},
            )

        if value is None:
            raise RecordInputError(
                message=f"{location} with op '{op}' requires value",
                error_code="INVALID_ANALYZE_FILTER_VALUE",
                fix_hint="Provide value for this filter, or use is_null / not_null.",
                details={"location": location, "op": op, "field": _field_ref_payload(field)},
            )

        if op == "contains" and isinstance(value, (list, dict)):
            raise RecordInputError(
                message=f"{location} with op 'contains' requires a single text value",
                error_code="INVALID_ANALYZE_FILTER_VALUE",
                fix_hint="Use a single string for contains filters.",
                details={"location": location, "field": _field_ref_payload(field), "received_value": value},
            )

        if field.que_type in DATE_QUE_TYPES and op in {"eq", "neq"}:
            if isinstance(value, list):
                raise RecordInputError(
                    message=f"{location} with op '{op}' requires a single date value",
                    error_code="INVALID_ANALYZE_FILTER_VALUE",
                    fix_hint="Use a single date string for eq/neq on date fields.",
                    details={"location": location, "field": _field_ref_payload(field), "received_value": value},
                )
            self._validate_strict_date_filter_value(value, location=f"{location}.value")
        return value

    def _validate_strict_date_filter_value(self, value: JSONValue, *, location: str) -> datetime:
        """执行内部辅助逻辑。"""
        text = _normalize_optional_text(value)
        if text is None:
            raise RecordInputError(
                message=f"{location} requires a concrete date or datetime string",
                error_code="INVALID_DATE_FILTER_VALUE",
                fix_hint="Use a valid ISO-like date such as 2026-03-01 or 2026-03-01 00:00:00.",
                details={"location": location, "received_value": value},
            )
        parsed = _parse_datetime_like(text)
        if parsed is None:
            raise RecordInputError(
                message=f"{location} uses an invalid date value '{text}'",
                error_code="INVALID_DATE_FILTER_VALUE",
                fix_hint="Normalize relative time phrases into a real date range, and avoid impossible dates like 2026-02-29.",
                details={"location": location, "received_value": value},
            )
        return parsed

    def _compile_analyze_dimensions(self, index: FieldIndex, dimensions: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        supported_buckets = {None, "day", "week", "month", "quarter", "year"}
        compiled: list[JSONObject] = []
        used_aliases: set[str] = set()
        for idx, item in enumerate(dimensions):
            if not isinstance(item, dict):
                raise RecordInputError(
                    message=f"dimensions[{idx}] must be an object",
                    error_code="INVALID_ANALYZE_DIMENSION",
                    fix_hint="Pass dimensions like {'field_id': 2, 'alias': '状态'}",
                )
            self._ensure_allowed_analyze_keys(
                item,
                location=f"dimensions[{idx}]",
                allowed_keys={"field_id", "fieldId", "alias", "bucket"},
                example="{'field_id': 2, 'alias': '状态', 'bucket': 'month'}",
            )
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
            field = self._resolve_field_by_id(field_id, index, location=f"dimensions[{idx}]")
            bucket = _normalize_optional_text(item.get("bucket"))
            if bucket not in supported_buckets:
                raise RecordInputError(
                    message=f"dimensions[{idx}] uses unsupported bucket '{bucket}'",
                    error_code="UNSUPPORTED_TIME_BUCKET",
                    fix_hint="Use one of day/week/month/quarter/year, or omit bucket.",
                )
            if bucket is not None and field.que_type not in DATE_QUE_TYPES:
                raise RecordInputError(
                    message=f"dimensions[{idx}] bucket requires a date/time field",
                    error_code="INVALID_TIME_BUCKET_FIELD",
                    fix_hint="Use bucket only on fields returned in suggested_time_fields by record_browse_schema_get.",
                    details={"field": _field_ref_payload(field), "bucket": bucket},
                )
            alias = _normalize_optional_text(item.get("alias")) or field.que_title
            if alias in used_aliases:
                raise RecordInputError(
                    message=f"dimensions[{idx}] alias '{alias}' is duplicated",
                    error_code="DUPLICATE_ANALYZE_ALIAS",
                    fix_hint="Use unique aliases across dimensions and metrics.",
                )
            used_aliases.add(alias)
            compiled.append({"field": field, "alias": alias, "bucket": bucket})
        return compiled

    def _compile_analyze_metrics(self, index: FieldIndex, metrics: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        requested_metrics = metrics or [{"op": "count", "alias": "记录数"}]
        supported_ops = {"count", "sum", "avg", "min", "max", "distinct_count"}
        compiled: list[JSONObject] = []
        used_aliases: set[str] = set()
        for idx, item in enumerate(requested_metrics):
            if not isinstance(item, dict):
                raise RecordInputError(
                    message=f"metrics[{idx}] must be an object",
                    error_code="INVALID_ANALYZE_METRIC",
                    fix_hint="Pass metrics like {'op': 'count', 'alias': '记录数'}",
                )
            self._ensure_allowed_analyze_keys(
                item,
                location=f"metrics[{idx}]",
                allowed_keys={"op", "type", "agg", "aggregation", "field_id", "fieldId", "alias"},
                example="{'op': 'sum', 'field_id': 7, 'alias': '总金额'}",
            )
            op = _normalize_optional_text(
                item.get("op") or item.get("type") or item.get("agg") or item.get("aggregation")
            )
            if op not in supported_ops:
                raise RecordInputError(
                    message=f"metrics[{idx}] uses unsupported op '{op}'",
                    error_code="UNSUPPORTED_ANALYZE_METRIC",
                    fix_hint="Use one of count/sum/avg/min/max/distinct_count.",
                )
            field: FormField | None = None
            if op != "count":
                field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
                field = self._resolve_field_by_id(field_id, index, location=f"metrics[{idx}]")
                if op in {"sum", "avg", "min", "max"} and field.que_type != 8:
                    raise RecordInputError(
                        message=f"metrics[{idx}] with op '{op}' requires a numeric field",
                        error_code="INVALID_METRIC_FIELD_TYPE",
                        fix_hint="Use sum/avg/min/max only on numeric fields returned by record_browse_schema_get.",
                        details={"location": f"metrics[{idx}]", "field": _field_ref_payload(field), "op": op},
                    )
            elif item.get("field_id", item.get("fieldId")) is not None:
                raise RecordInputError(
                    message=f"metrics[{idx}] with op 'count' must not include field_id",
                    error_code="INVALID_ANALYZE_METRIC",
                    fix_hint="For count, omit field_id and use only {'op': 'count', 'alias': '记录数'}.",
                    details={"location": f"metrics[{idx}]", "op": op},
                )
            alias = _normalize_optional_text(item.get("alias"))
            if alias is None:
                if op == "count":
                    alias = "count"
                elif field is not None:
                    alias = f"{field.que_title}_{op}"
                else:
                    alias = op
            if alias in used_aliases:
                raise RecordInputError(
                    message=f"metrics[{idx}] alias '{alias}' is duplicated",
                    error_code="DUPLICATE_ANALYZE_ALIAS",
                    fix_hint="Use unique aliases across dimensions and metrics.",
                )
            used_aliases.add(alias)
            compiled.append({"op": op, "field": field, "alias": alias})
        return compiled

    def _compile_analyze_filters(self, index: FieldIndex, filters: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        supported_ops = {"eq", "neq", "in", "not_in", "gt", "gte", "lt", "lte", "between", "contains", "is_null", "not_null"}
        compiled: list[JSONObject] = []
        for idx, item in enumerate(filters):
            if not isinstance(item, dict):
                raise RecordInputError(
                    message=f"filters[{idx}] must be an object",
                    error_code="INVALID_ANALYZE_FILTER",
                    fix_hint="Pass filters like {'field_id': 2, 'op': 'eq', 'value': '进行中'}.",
                )
            self._ensure_allowed_analyze_keys(
                item,
                location=f"filters[{idx}]",
                allowed_keys={"field_id", "fieldId", "op", "operator", "value", "values"},
                example="{'field_id': 2, 'op': 'eq', 'value': '进行中'}",
            )
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
            op = _normalize_optional_text(item.get("op", item.get("operator"))) or "eq"
            if op not in supported_ops:
                raise RecordInputError(
                    message=f"filters[{idx}] uses unsupported op '{op}'",
                    error_code="UNSUPPORTED_ANALYZE_FILTER_OP",
                    fix_hint="Use one of eq/neq/in/not_in/gt/gte/lt/lte/between/contains/is_null/not_null.",
                )
            field = self._resolve_field_by_id(field_id, index, location=f"filters[{idx}]")
            normalized_value = self._validate_analyze_filter_value(
                field=field,
                op=op,
                value=item.get("value", item.get("values")),
                location=f"filters[{idx}]",
            )
            compiled.append({"field": field, "field_id": field_id, "op": op, "value": normalized_value})
        return compiled

    def _compile_analyze_sort(self, sort: list[JSONObject], dimensions: list[JSONObject], metrics: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        dimension_aliases = {str(item["alias"]) for item in dimensions}
        metric_aliases = {str(item["alias"]) for item in metrics}
        compiled: list[JSONObject] = []
        for idx, item in enumerate(sort):
            if not isinstance(item, dict):
                raise RecordInputError(
                    message=f"sort[{idx}] must be an object",
                    error_code="INVALID_ANALYZE_SORT",
                    fix_hint="Pass sort like {'by': '记录数', 'order': 'desc'}.",
                )
            self._ensure_allowed_analyze_keys(
                item,
                location=f"sort[{idx}]",
                allowed_keys={"by", "order"},
                example="{'by': '记录数', 'order': 'desc'}",
            )
            by = _normalize_optional_text(item.get("by"))
            if by is None:
                raise RecordInputError(
                    message=f"sort[{idx}] requires by",
                    error_code="MISSING_SORT_KEY",
                    fix_hint="Use a dimension alias or metric alias in sort.by.",
                )
            order = (_normalize_optional_text(item.get("order")) or "asc").lower()
            if order not in {"asc", "desc"}:
                raise RecordInputError(
                    message=f"sort[{idx}] uses unsupported order '{order}'",
                    error_code="INVALID_SORT_ORDER",
                    fix_hint="Use asc or desc.",
                )
            if by in dimension_aliases:
                compiled.append({"by": by, "order": order, "kind": "dimension"})
                continue
            if by in metric_aliases:
                compiled.append({"by": by, "order": order, "kind": "metric"})
                continue
            raise RecordInputError(
                message=f"sort[{idx}] references unknown alias '{by}'",
                error_code="UNKNOWN_ANALYZE_SORT_KEY",
                fix_hint="Use a dimension alias or metric alias defined in the same record_analyze request.",
            )
        return compiled

    def _execute_record_analyze(
        self,
        *,
        profile: str,
        session_profile,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        index: FieldIndex,
        resolved_view: AccessibleViewRoute,
        dimensions: list[JSONObject],
        metrics: list[JSONObject],
        filters: list[JSONObject],
        sort: list[JSONObject],
        limit: int,
        strict_full: bool,
        output_profile: str,
        extra_warnings: list[JSONObject] | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        started_at = time.perf_counter()
        analysis_paging = _fixed_analysis_scan_policy()
        page_size = int(analysis_paging["page_size"])
        requested_pages = int(analysis_paging["requested_pages"])
        scan_max_pages = int(analysis_paging["scan_max_pages"])
        auto_expand_pages = bool(analysis_paging["auto_expand_pages"])
        pages_to_scan = min(max(requested_pages, 1), max(scan_max_pages, 1))
        current_page = 1
        scanned_pages = 0
        source_pages: list[int] = []
        result_amount: int | None = None
        has_more = False
        view_selection = resolved_view.view_selection
        local_filtering = bool(filters)
        group_stats: dict[tuple[tuple[str, object], ...], JSONObject] = {}
        overall_metrics = self._initialize_metric_states(metrics)
        matched_rows = 0
        scan_control: JSONObject = {
            "requested_pages": max(requested_pages, 1),
            "scan_max_pages": max(scan_max_pages, 1),
            "auto_expand_pages": auto_expand_pages,
            "auto_expand_applied": False,
            "auto_expand_target_pages": pages_to_scan,
            "auto_expand_page_cap": DEFAULT_ANALYSIS_AUTO_EXPAND_PAGE_CAP,
        }

        while scanned_pages < pages_to_scan:
            page = self._search_page(
                context,
                app_key=app_key,
                view_selection=view_selection,
                page_num=current_page,
                page_size=page_size,
                query_key=None,
                match_rules=[],
                sorts=[],
                search_que_ids=None,
                list_type=resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE,
            )
            scanned_pages += 1
            source_pages.append(current_page)
            items = page.get("list") if isinstance(page.get("list"), list) else []
            if result_amount is None:
                result_amount = _effective_total(page, page_size)
                pages_to_scan, scan_control = _compute_scan_limit(
                    requested_pages=requested_pages,
                    scan_max_pages=scan_max_pages,
                    auto_expand_pages=auto_expand_pages,
                    page=page,
                    page_size=page_size,
                )
            has_more = _page_has_more(page, current_page, page_size, len(items))
            for item in items:
                if not isinstance(item, dict):
                    continue
                answers = item.get("answers")
                answer_list = answers if isinstance(answers, list) else []
                if not self._matches_analyze_filters(answer_list, filters):
                    continue
                matched_rows += 1
                self._apply_metric_states(overall_metrics, metrics, answer_list)
                if not dimensions:
                    continue
                group_payload = self._build_analyze_group_payload(answer_list, dimensions)
                group_key = self._analysis_group_key(group_payload)
                bucket = group_stats.get(group_key)
                if bucket is None:
                    bucket = {
                        "dimensions": group_payload,
                        "metrics_state": self._initialize_metric_states(metrics),
                    }
                    group_stats[group_key] = bucket
                bucket_metrics = cast(dict[str, JSONObject], bucket["metrics_state"])
                self._apply_metric_states(bucket_metrics, metrics, answer_list)
            if not has_more:
                break
            current_page += 1

        metric_totals = self._render_metric_values(overall_metrics, metrics)
        if dimensions:
            all_rows = [
                {
                    "dimensions": cast(JSONObject, bucket["dimensions"]),
                    "metrics": self._render_metric_values(cast(dict[str, JSONObject], bucket["metrics_state"]), metrics),
                }
                for bucket in group_stats.values()
            ]
            all_rows = self._sort_analyze_rows(all_rows, sort, dimensions, metrics)
            rows_truncated = len(all_rows) > limit
            rows = all_rows[:limit]
            group_count = len(all_rows)
            statement_scope = "returned_groups_only" if rows_truncated else "full_population"
        else:
            rows_truncated = False
            rows = [{"dimensions": {}, "metrics": metric_totals}]
            group_count = 1
            statement_scope = "full_population"
        raw_scan_complete = not has_more
        completeness_status = "complete" if raw_scan_complete else "incomplete"
        reason_code = "LOCAL_VIEW_FILTERING" if local_filtering and raw_scan_complete else ("SOURCE_EXHAUSTED" if raw_scan_complete else "SCAN_LIMIT_HIT")
        query_payload: JSONObject = {
            "app_key": app_key,
            "dimensions": [
                {
                    "field_id": cast(FormField, item["field"]).que_id,
                    "alias": item["alias"],
                    "bucket": item["bucket"],
                }
                for item in dimensions
            ],
            "metrics": [
                {
                    "op": item["op"],
                    **(
                        {"field_id": cast(FormField, item["field"]).que_id}
                        if item["field"] is not None
                        else {}
                    ),
                    "alias": item["alias"],
                }
                for item in metrics
            ],
            "filters": [
                {
                    "field_id": cast(FormField, item["field"]).que_id,
                    "op": item["op"],
                    **({"value": item.get("value")} if item.get("value") is not None else {}),
                }
                for item in filters
            ],
            "sort": [{"by": item["by"], "order": item["order"]} for item in sort],
            "limit": limit,
            "strict_full": strict_full,
        }
        view_payload = _accessible_view_payload(resolved_view)
        if view_payload:
            query_payload["view"] = view_payload

        ranking: JSONObject | None = None
        if dimensions and sort:
            primary_sort = sort[0]
            ranking = {
                "order_by": primary_sort["by"],
                "order_direction": primary_sort["order"],
                "ranked": True,
            }
            rows = [
                {
                    "dimensions": cast(JSONObject, row["dimensions"]),
                    "metrics": cast(JSONObject, row["metrics"]),
                    "rank": idx,
                }
                for idx, row in enumerate(rows, start=1)
            ]

        warnings = self._build_analyze_warnings(
            local_filtering=local_filtering,
            rows_truncated=rows_truncated,
            extra_warnings=(extra_warnings or []) + _view_filter_trust_warnings(resolved_view),
        )
        completeness: JSONObject = {
            "status": completeness_status,
            "safe_for_final_conclusion": completeness_status == "complete",
            "rows_truncated": rows_truncated,
            "statement_scope": statement_scope,
            "warnings": warnings,
        }
        if completeness_status != "complete":
            completeness["reason_code"] = reason_code

        response: JSONObject = {
            "ok": True,
            "status": "success" if raw_scan_complete else "partial",
            "query": query_payload,
            "result": {
                "totals": {
                    "metric_totals": metric_totals,
                },
                "rows": rows,
                "row_count": len(rows),
            },
            "ranking": ranking,
            "ratios": None,
            "completeness": completeness,
            "warnings": warnings,
            "verification": _view_filter_verification_payload(resolved_view),
            "presentation": {
                "returned_group_count": len(rows),
                "total_group_count": group_count,
            },
        }
        if strict_full and not raw_scan_complete:
            response["ok"] = False
            response["status"] = "error"
            response["error"] = {
                "code": "INCOMPLETE_SCAN",
                "message": "record_analyze could not complete the scan within the fixed internal analysis budget.",
                "fix_hint": "Narrow the scope with view/filter constraints, or retry after reducing the dataset size.",
            }
        if output_profile == "verbose":
            response["debug"] = {
                "request_route": self._request_route_payload(context),
                "elapsed_ms": int((time.perf_counter() - started_at) * 1000),
                "backend_total_hint": scan_control.get("backend_total_count", result_amount),
                "backend_page_amount": scan_control.get("backend_page_amount"),
                "source_pages": source_pages,
                "scan_control": scan_control,
                "reason_code": reason_code,
                "local_filtering_applied": local_filtering,
            }
        return response

    def _build_analyze_group_payload(self, answer_list: list[JSONValue], dimensions: list[JSONObject]) -> JSONObject:
        """执行内部辅助逻辑。"""
        if not dimensions:
            return {}
        payload: JSONObject = {}
        for item in dimensions:
            field = cast(FormField, item["field"])
            alias = cast(str, item["alias"])
            bucket = cast(str | None, item["bucket"])
            value = _extract_field_value(answer_list, field)
            if bucket is not None:
                value = _to_time_bucket(value, bucket)
            payload[alias] = value
        return payload

    def _initialize_metric_states(self, metrics: list[JSONObject]) -> dict[str, JSONObject]:
        """执行内部辅助逻辑。"""
        states: dict[str, JSONObject] = {}
        for item in metrics:
            states[str(item["alias"])] = {
                "count": 0,
                "sum": 0.0,
                "min": None,
                "max": None,
                "seen": set(),
            }
        return states

    def _analysis_group_key(self, payload: JSONObject) -> tuple[tuple[str, object], ...]:
        """执行内部辅助逻辑。"""
        return tuple((key, self._freeze_group_key_value(value)) for key, value in payload.items())

    def _freeze_group_key_value(self, value: JSONValue) -> object:
        """执行内部辅助逻辑。"""
        if isinstance(value, dict):
            return tuple((key, self._freeze_group_key_value(item)) for key, item in sorted(value.items()))
        if isinstance(value, list):
            return tuple(self._freeze_group_key_value(item) for item in value)
        return value

    def _apply_metric_states(self, states: dict[str, JSONObject], metrics: list[JSONObject], answer_list: list[JSONValue]) -> None:
        """执行内部辅助逻辑。"""
        for item in metrics:
            alias = cast(str, item["alias"])
            op = cast(str, item["op"])
            field = cast(FormField | None, item["field"])
            state = states[alias]
            if op == "count":
                state["count"] = int(state["count"]) + 1
                continue
            value = _extract_field_value(answer_list, field)
            if op == "distinct_count":
                for entry in _flatten_distinct_values(value):
                    cast(set[str], state["seen"]).add(entry)
                continue
            amount = _coerce_amount(value)
            if amount is None:
                continue
            state["count"] = int(state["count"]) + 1
            state["sum"] = float(state["sum"]) + amount
            state["min"] = amount if state["min"] is None else min(float(state["min"]), amount)
            state["max"] = amount if state["max"] is None else max(float(state["max"]), amount)

    def _render_metric_values(self, states: dict[str, JSONObject], metrics: list[JSONObject]) -> JSONObject:
        """执行内部辅助逻辑。"""
        rendered: JSONObject = {}
        for item in metrics:
            alias = cast(str, item["alias"])
            op = cast(str, item["op"])
            state = states[alias]
            count = int(state["count"] or 0)
            amount_sum = float(state["sum"] or 0.0)
            if op == "count":
                rendered[alias] = count
            elif op == "sum":
                rendered[alias] = amount_sum
            elif op == "avg":
                rendered[alias] = (amount_sum / count) if count else None
            elif op == "min":
                rendered[alias] = state["min"]
            elif op == "max":
                rendered[alias] = state["max"]
            elif op == "distinct_count":
                rendered[alias] = len(cast(set[str], state["seen"]))
        return rendered

    def _matches_analyze_filters(self, answer_list: list[JSONValue], filters: list[JSONObject]) -> bool:
        """执行内部辅助逻辑。"""
        for item in filters:
            field = cast(FormField, item["field"])
            if not _match_analyze_filter(_extract_field_value(answer_list, field), cast(str, item["op"]), item.get("value")):
                return False
        return True

    def _sort_analyze_rows(
        self,
        rows: list[JSONObject],
        sort: list[JSONObject],
        dimensions: list[JSONObject],
        metrics: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if not rows or not sort:
            if dimensions and any(item.get("bucket") for item in dimensions):
                return sorted(rows, key=lambda item: json.dumps(item.get("dimensions", {}), ensure_ascii=False, sort_keys=True))
            return rows
        sorted_rows = list(rows)
        for sort_item in reversed(sort):
            by = cast(str, sort_item["by"])
            reverse = cast(str, sort_item["order"]) == "desc"
            kind = cast(str, sort_item["kind"])
            sorted_rows.sort(
                key=lambda item: _sortable_value(
                    cast(JSONObject, item["metrics" if kind == "metric" else "dimensions"]).get(by)
                ),
                reverse=reverse,
            )
        return sorted_rows

    def _build_analyze_warnings(
        self,
        *,
        local_filtering: bool,
        rows_truncated: bool,
        extra_warnings: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        warnings: list[JSONObject] = []
        warnings.extend(extra_warnings)
        if local_filtering:
            warnings.append({"code": "LOCAL_VIEW_FILTERING"})
        if rows_truncated:
            warnings.append({"code": "ROWS_TRUNCATED"})
        return warnings

    def _preflight_record_write(
        self,
        *,
        profile: str,
        operation: str,
        app_key: str,
        apply_id: int | None,
        answers: list[JSONObject],
        fields: JSONObject,
        force_refresh_form: bool,
        view_id: str | None,
        list_type: int | None,
        view_key: str | None,
        view_name: str | None,
        tool_name: str = "写入记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))

        def runner(session_profile, context):
            plan_data = self._build_record_write_preflight(
                profile=profile,
                context=context,
                operation=operation,
                app_key=app_key,
                apply_id=apply_id,
                answers=answers,
                fields=fields,
                force_refresh_form=force_refresh_form,
                view_id=view_id,
                list_type=list_type,
                view_key=view_key,
                view_name=view_name,
            )
            if not force_refresh_form and self._record_preflight_suggests_stale_schema(plan_data):
                self._clear_record_schema_caches(profile=profile, app_key=app_key, clear_view_caches=True)
                plan_data = self._build_record_write_preflight(
                    profile=profile,
                    context=context,
                    operation=operation,
                    app_key=app_key,
                    apply_id=apply_id,
                    answers=answers,
                    fields=fields,
                    force_refresh_form=True,
                    view_id=view_id,
                    list_type=list_type,
                    view_key=view_key,
                    view_name=view_name,
                )
                plan_data["schema_force_refreshed"] = True
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "request_route": self._request_route_payload(context),
                "data": plan_data,
            }

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _build_record_write_preflight(
        self,
        *,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        operation: str,
        app_key: str,
        apply_id: int | None,
        answers: list[JSONObject],
        fields: JSONObject,
        force_refresh_form: bool,
        view_id: str | None,
        list_type: int | None,
        view_key: str | None,
        view_name: str | None,
        existing_answers_override: list[JSONObject] | None = None,
        field_index_override: FieldIndex | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        normalized_fields = fields or {}
        normalized_answers_input = answers or []
        resolved_view: AccessibleViewRoute | None = None
        selector_index: FieldIndex | None = field_index_override
        browse_writable_field_ids: set[int] = set()
        visible_question_ids: set[int] = set()
        if any(item is not None for item in (view_id, list_type, view_key, view_name)):
            resolved_view, compatibility_warnings = self._resolve_accessible_view_route(
                profile,
                context,
                app_key,
                view_id=view_id,
                list_type=list_type,
                view_key=view_key,
                view_name=view_name,
                allow_default=False,
            )
            browse_scope = self._build_browse_write_scope(
                profile,
                context,
                app_key,
                resolved_view,
                force_refresh=force_refresh_form,
            )
            selector_index = cast(FieldIndex, browse_scope["index"])
            browse_writable_field_ids = cast(set[int], browse_scope["writable_field_ids"])
            visible_question_ids = cast(set[int], browse_scope["visible_question_ids"])
        else:
            compatibility_warnings = []
        if field_index_override is not None:
            base_index = field_index_override
            question_relations: list[JSONObject] = []
            runtime_linked_field_ids: set[int] = set()
            index = base_index
        elif operation == "update" and resolved_view is not None:
            base_index = cast(FieldIndex, selector_index)
            question_relations = []
            runtime_linked_field_ids = set()
            index = base_index
        else:
            schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
            base_index = _build_applicant_top_level_field_index(schema)
            question_relations = _collect_question_relations(schema)
            runtime_linked_field_ids = _collect_linked_required_field_ids(question_relations)
            runtime_linked_field_ids.update(_collect_option_linked_field_ids(base_index))
            index = base_index
            if operation == "create":
                linked_hidden_index = _build_applicant_hidden_linked_top_level_field_index(
                    schema,
                    linked_field_ids=runtime_linked_field_ids,
                )
                index = _merge_field_indexes(base_index, linked_hidden_index)
        if selector_index is None:
            selector_index = index
        resolved_fields = self._collect_write_plan_field_refs(fields=normalized_fields, answers=normalized_answers_input, index=selector_index)
        support_matrix = _summarize_write_support(resolved_fields)
        invalid_fields: list[JSONObject] = []
        normalized_answers: list[JSONObject] = []
        validation_warnings = [
            "Direct record edits perform static preflight from form metadata before apply; runtime visibility and dynamic linkage can still reject writes."
        ]
        if resolved_view is not None:
            validation_warnings.append(
                "view-scoped writes only narrow selectable fields; they do not grant record edit permission."
            )
        validation_warnings.extend(
            str(item.get("message"))
            for item in compatibility_warnings
            if isinstance(item, dict) and item.get("message")
        )
        validation_warnings.extend(
            str(item.get("message"))
            for item in _view_filter_trust_warnings(resolved_view)
            if isinstance(item, dict) and item.get("message")
        )
        existing_answers_for_update: list[JSONObject] = []
        existing_answers_loaded = False
        if operation == "update" and apply_id is not None:
            if existing_answers_override is not None:
                existing_answers_for_update = [item for item in existing_answers_override if isinstance(item, dict)]
                existing_answers_loaded = True
            else:
                try:
                    if resolved_view is not None:
                        existing_answers_for_update, _used_list_type = self._load_record_answers_for_accessible_route(
                            context,
                            app_key=app_key,
                            apply_id=apply_id,
                            resolved_view=resolved_view,
                        )
                    else:
                        existing_answers_for_update = self._load_record_answers_for_preflight(
                            context,
                            app_key=app_key,
                            apply_id=apply_id,
                        )
                    existing_answers_loaded = True
                except QingflowApiError as exc:
                    if not _is_optional_record_auxiliary_lookup_error(exc):
                        raise
                    validation_warnings.append(
                        "update preflight could not load the current record; required-field completeness and dynamic lookup context were not fully revalidated."
                    )
        lookup_resolution = LookupResolutionState(
            operation=operation,
            app_key=app_key,
            apply_id=apply_id,
            workflow_node_id=None,
            force_refresh_form=force_refresh_form,
            context_complete=(operation != "update" or existing_answers_loaded),
            field_index=index,
            base_answers=existing_answers_for_update,
            normalized_answers=[],
            confirmation_requests=[],
            resolved_fields=[],
            unresolved_field_ids=set(),
            unresolved_subtable_cells=set(),
        )
        try:
            scoped_answers_input, scoped_fields = self._canonicalize_write_scope_selectors(
                answers=normalized_answers_input,
                fields=normalized_fields,
                selector_index=selector_index,
            )
            normalized_answers = self._resolve_answers(
                profile,
                context,
                app_key,
                answers=scoped_answers_input,
                fields=scoped_fields,
                force_refresh_form=force_refresh_form,
                resolution_state=lookup_resolution,
                field_index_override=index,
            )
        except RecordInputError as error:
            normalized_answers = list(lookup_resolution.normalized_answers)
            invalid_fields.append(
                {
                    "location": _stringify_json(error.details.get("location") if error.details else None),
                    "message": error.message,
                    "error_code": error.error_code,
                    "fix_hint": error.fix_hint,
                    "details": error.details,
                    "field": error.details.get("field") if error.details and isinstance(error.details.get("field"), dict) else None,
                    "expected_format": error.details.get("expected_format") if error.details and isinstance(error.details.get("expected_format"), dict) else None,
                    "received_value": error.details.get("received_value") if error.details else None,
                }
            )
        validation_answers = normalized_answers
        if operation == "update" and apply_id is not None and not invalid_fields and existing_answers_loaded:
            validation_answers = self._merge_record_answers(existing_answers_for_update, normalized_answers)
        readonly_or_system_fields = [
            {
                "que_id": entry.get("que_id"),
                "que_title": entry.get("que_title"),
                "que_type": entry.get("que_type"),
                "readonly": entry.get("readonly"),
                "system": entry.get("system"),
                "source": entry.get("source"),
                "requested": entry.get("requested"),
                "reason_code": (
                    "system"
                    if bool(entry.get("system"))
                    else "view_readonly"
                    if resolved_view is not None
                    else "readonly"
                ),
            }
            for entry in resolved_fields
            if bool(entry.get("resolved"))
            and (
                bool(entry.get("system"))
                or (
                    resolved_view is not None
                    and _coerce_count(entry.get("que_id")) is not None
                    and _coerce_count(entry.get("que_id")) not in browse_writable_field_ids
                )
                or (resolved_view is None and bool(entry.get("readonly")))
            )
        ]
        if resolved_view is not None and visible_question_ids:
            invalid_fields.extend(
                self._validate_view_scoped_subtable_answers(
                    normalized_answers=normalized_answers,
                    full_index=index,
                    selector_index=selector_index,
                    visible_question_ids=visible_question_ids,
                )
            )
        provided_field_ids = {
            str(answer.get("queId"))
            for answer in validation_answers
            if isinstance(answer.get("queId"), int) and int(answer["queId"]) > 0
            and _answer_has_meaningful_content(answer)
        }
        provided_field_ids.update(str(item) for item in lookup_resolution.unresolved_field_ids)
        activation_sources_by_field_id = _collect_linked_activation_sources(question_relations, index)
        active_question_relation_field_ids, inactive_question_relation_field_ids = _classify_question_relation_target_ids(
            validation_answers,
            question_relations,
        )
        conditional_option_link_field_ids = _collect_option_linked_field_ids(index)
        active_option_link_field_ids = _collect_active_option_linked_field_ids(validation_answers, index)
        missing_required_fields = []
        likely_hidden_required_fields = []
        for field in index.by_id.values():
            if not field.required or str(field.que_id) in provided_field_ids:
                continue
            field_id = str(field.que_id)
            is_actively_linked = field_id in active_question_relation_field_ids or field_id in active_option_link_field_ids
            is_conditionally_linked = field_id in inactive_question_relation_field_ids or field_id in conditional_option_link_field_ids
            target_bucket = missing_required_fields if is_actively_linked or not is_conditionally_linked else likely_hidden_required_fields
            payload: JSONObject = {
                "que_id": field.que_id,
                "que_title": field.que_title,
                "que_type": field.que_type,
                "reason": "required field not provided" if target_bucket is missing_required_fields else "required field may be hidden by linked visibility rules",
            }
            if target_bucket is missing_required_fields and is_actively_linked:
                activation_sources = activation_sources_by_field_id.get(field_id, [])
                if activation_sources:
                    payload["activation_sources"] = activation_sources
                payload["requirement_mode"] = (
                    REQUIREMENT_MODE_RUNTIME_LINKED_NON_WRITABLE
                    if self._should_surface_runtime_linked_required_field(
                        field,
                        runtime_linked_field_ids=runtime_linked_field_ids,
                    )
                    else REQUIREMENT_MODE_LINKED_MAY_BECOME_REQUIRED
                )
                payload["fix_hint"] = LINKED_REQUIRED_FIX_HINT
            target_bucket.append(payload)
        missing_required_fields.extend(
            self._collect_missing_required_subtable_fields(
                operation=operation,
                normalized_answers=normalized_answers,
                full_index=index,
                skip_cells=lookup_resolution.unresolved_subtable_cells,
            )
        )
        option_links = _collect_option_links(resolved_fields)
        if question_relations:
            validation_warnings.append(
                "form contains questionRelations; linked visibility and runtime required rules may differ at submit time."
            )
        if conditional_option_link_field_ids:
            validation_warnings.append(
                "form contains option-linked fields; linked visibility and runtime required rules may differ at submit time."
            )
        if likely_hidden_required_fields:
            titles = ", ".join(str(item.get("que_title")) for item in likely_hidden_required_fields if item.get("que_title"))
            validation_warnings.append(
                "static preflight did not block linked required fields because dynamic visibility may hide them at runtime: "
                + titles
            )
        validation = {
            "valid": (
                not invalid_fields
                and not missing_required_fields
                and not readonly_or_system_fields
                and not lookup_resolution.confirmation_requests
            ),
            "missing_required_fields": missing_required_fields,
            "likely_hidden_required_fields": likely_hidden_required_fields,
            "readonly_or_system_fields": readonly_or_system_fields,
            "invalid_fields": invalid_fields,
            "warnings": validation_warnings,
        }
        field_errors = self._record_write_field_errors(
            invalid_fields=invalid_fields,
            missing_required_fields=missing_required_fields,
            readonly_or_system_fields=readonly_or_system_fields,
        )
        blockers = []
        if invalid_fields:
            blockers.append("payload contains invalid field values")
        if missing_required_fields:
            blockers.append("required fields are missing")
        if readonly_or_system_fields:
            blockers.append(
                "payload writes fields that are not writable in the selected view"
                if resolved_view is not None
                else "payload writes readonly or system-managed fields"
            )
        actions = ["Use the relevant schema-get tool when field titles or field ids are ambiguous."]
        if support_matrix["restricted"]:
            actions.append("Review write_format.required_presteps for restricted fields before submit.")
        if invalid_fields:
            actions.append("Fix invalid_fields before applying the write.")
        if missing_required_fields:
            actions.append("Fill missing required fields before applying the write.")
        if readonly_or_system_fields:
            actions.append(
                "Remove fields that the selected view does not allow for update, or choose a view that exposes those fields as writable."
                if resolved_view is not None
                else "Remove readonly/system fields from payload before applying the write."
            )
        if lookup_resolution.confirmation_requests:
            blockers.append("one or more lookup fields require confirmation before the write can execute")
            actions.append("Review confirmation_requests and retry with explicit ids/objects for the ambiguous lookup fields.")
        if question_relations:
            actions.append("Treat static preflight as conservative only because linked fields can still appear at runtime.")
        return {
            "operation": operation if operation in {"create", "update"} else ("update" if apply_id else "create"),
            "app_key": app_key,
            "apply_id": apply_id,
            "normalized_answers": normalized_answers,
            "resolved_fields": resolved_fields,
            "support_matrix": support_matrix,
            "validation": validation,
            "field_errors": field_errors,
            "confirmation_requests": lookup_resolution.confirmation_requests,
            "lookup_resolved_fields": lookup_resolution.resolved_fields,
            "dependencies": {
                "question_relations_present": bool(question_relations),
                "question_relations": question_relations,
                "option_links": option_links,
            },
            "selection": {
                "view": _accessible_view_payload(resolved_view),
            },
            "ready_to_submit": validation["valid"],
            "blockers": blockers,
            "recommended_next_actions": actions,
        }

    def _load_record_answers_for_preflight(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        record = self.backend.request(
            "GET",
            context,
            f"/app/{app_key}/apply/{apply_id}",
            params={"role": 1, "listType": DEFAULT_RECORD_LIST_TYPE},
        )
        answers = record.get("answers") if isinstance(record, dict) else None
        return [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []

    def _validate_view_scoped_subtable_answers(
        self,
        *,
        normalized_answers: list[JSONObject],
        full_index: FieldIndex,
        selector_index: FieldIndex,
        visible_question_ids: set[int],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        invalid_fields: list[JSONObject] = []
        if not visible_question_ids:
            return invalid_fields
        for answer in normalized_answers:
            table_values = answer.get("tableValues") if isinstance(answer.get("tableValues"), list) else []
            if not table_values:
                continue
            parent_que_id = _coerce_count(answer.get("queId"))
            if parent_que_id is None:
                continue
            parent_field = full_index.by_id.get(str(parent_que_id)) or selector_index.by_id.get(str(parent_que_id))
            if parent_field is None:
                continue
            subtable_index = self._subtable_field_index_optional(parent_field)
            for row_ordinal, row in enumerate(table_values, start=1):
                row_cells = [item for item in row if isinstance(item, dict)] if isinstance(row, list) else []
                for cell in row_cells:
                    que_id = _coerce_count(cell.get("queId"))
                    if que_id is None or que_id in visible_question_ids:
                        continue
                    subfield = subtable_index.by_id.get(str(que_id)) if subtable_index is not None else None
                    invalid_fields.append(
                        {
                            "location": f"{parent_field.que_title}[{row_ordinal}].{subfield.que_title if subfield is not None else que_id}",
                            "message": (
                                f"subtable field '{subfield.que_title if subfield is not None else que_id}' is not visible in the selected view"
                            ),
                            "error_code": "VIEW_SCOPE_FIELD_HIDDEN",
                            "field": _field_ref_payload(subfield) if subfield is not None else {"que_id": que_id},
                            "expected_format": _write_format_for_field(parent_field),
                            "received_value": cell.get("values"),
                        }
                    )
        return invalid_fields

    def _collect_missing_required_subtable_fields(
        self,
        *,
        operation: str,
        normalized_answers: list[JSONObject],
        full_index: FieldIndex,
        skip_cells: set[tuple[int, int, int]] | None = None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        missing_required_fields: list[JSONObject] = []
        skipped = skip_cells or set()
        answers_by_field_id = {
            str(que_id): answer
            for answer in normalized_answers
            if isinstance(answer, dict) and (que_id := _coerce_count(answer.get("queId"))) is not None and que_id > 0
        }
        for field in full_index.by_id.values():
            if field.que_type not in SUBTABLE_QUE_TYPES or field.system or field.readonly:
                continue
            subtable_index = self._subtable_field_index_optional(field)
            if subtable_index is None:
                continue
            required_subfields = [
                subfield
                for subfield in subtable_index.by_id.values()
                if subfield.required and not subfield.readonly and not subfield.system
            ]
            if not required_subfields:
                continue
            answer = answers_by_field_id.get(str(field.que_id))
            if answer is None:
                if operation == "create":
                    missing_required_fields.append(
                        {
                            "que_id": field.que_id,
                            "que_title": field.que_title,
                            "que_type": field.que_type,
                            "reason": (
                                "required subtable rows not provided; at least one row must include: "
                                + "、".join(subfield.que_title for subfield in required_subfields)
                            ),
                            "location": "fields",
                            "required_subfields": [
                                _field_ref_payload(subfield) for subfield in required_subfields
                            ],
                        }
                    )
                continue
            table_values = answer.get("tableValues") if isinstance(answer.get("tableValues"), list) else []
            if not table_values:
                if operation == "create":
                    missing_required_fields.append(
                        {
                            "que_id": field.que_id,
                            "que_title": field.que_title,
                            "que_type": field.que_type,
                            "reason": (
                                "required subtable rows not provided; at least one row must include: "
                                + "、".join(subfield.que_title for subfield in required_subfields)
                            ),
                            "location": "fields",
                            "required_subfields": [
                                _field_ref_payload(subfield) for subfield in required_subfields
                            ],
                        }
                    )
                continue
            for row_ordinal, row in enumerate(table_values, start=1):
                row_cells = [item for item in row if isinstance(item, dict)] if isinstance(row, list) else []
                if not row_cells:
                    continue
                row_id = _subtable_row_id(row_cells)
                if operation == "update" and row_id is not None:
                    continue
                provided_subfield_ids = {
                    str(que_id)
                    for cell in row_cells
                    if isinstance(cell, dict)
                    and (que_id := _coerce_count(cell.get("queId"))) is not None
                    and que_id > 0
                    and _answer_has_meaningful_content(cell)
                }
                for subfield in required_subfields:
                    if str(subfield.que_id) in provided_subfield_ids:
                        continue
                    if (field.que_id, row_ordinal, subfield.que_id) in skipped:
                        continue
                    missing_required_fields.append(
                        {
                            "que_id": subfield.que_id,
                            "que_title": subfield.que_title,
                            "que_type": subfield.que_type,
                            "reason": "required subtable field not provided",
                            "location": f"{field.que_title}[{row_ordinal}].{subfield.que_title}",
                            "parent_que_id": field.que_id,
                            "parent_que_title": field.que_title,
                            "row_ordinal": row_ordinal,
                        }
                    )
        return missing_required_fields

    def _merge_record_answers(
        self,
        existing_answers: list[JSONObject],
        patch_answers: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        merged_by_id: dict[int, JSONObject] = {}
        order: list[int] = []
        for source in (existing_answers, patch_answers):
            for item in source:
                que_id = _coerce_count(item.get("queId")) if isinstance(item, dict) else None
                if que_id is None or que_id <= 0:
                    continue
                if que_id not in merged_by_id:
                    order.append(que_id)
                merged_by_id[que_id] = item
        return [merged_by_id[que_id] for que_id in order]

    def record_query(
        self,
        *,
        profile: str,
        query_mode: str,
        app_key: str,
        apply_id: int | None,
        page_num: int,
        page_size: int,
        requested_pages: int,
        scan_max_pages: int,
        auto_expand_pages: bool = False,
        query_key: str | None,
        filters: list[JSONObject],
        sorts: list[JSONObject],
        max_rows: int,
        max_columns: int | None,
        select_columns: list[str | int],
        amount_column: str | int | None,
        time_range: JSONObject,
        stat_policy: JSONObject,
        strict_full: bool,
        output_profile: str,
        list_type: int,
        search_que_ids: list[int] | None = None,
        view_key: str | None = None,
        view_name: str | None = None,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        resolved_mode = _resolve_query_mode(query_mode, apply_id=apply_id, amount_column=amount_column, time_range=time_range, stat_policy=stat_policy)
        if resolved_mode == "summary":
            raise_tool_error(
                QingflowApiError(
                    category="config",
                    message="query_mode='summary' is not supported",
                    details={
                        "error_code": "UNSUPPORTED_QUERY_MODE",
                        "allowed_modes": ["auto", "list", "record"],
                        "fix_hint": "Use record_browse_schema_get followed by record_access, then analyze the CSV files with Python.",
                    },
                )
            )
        if resolved_mode == "list":
            list_paging = _fixed_list_scan_policy()
            page_size = int(list_paging["page_size"])
            requested_pages = int(list_paging["requested_pages"])
            scan_max_pages = int(list_paging["scan_max_pages"])
            auto_expand_pages = bool(list_paging["auto_expand_pages"])
        if resolved_mode == "record":
            return self._record_query_record(
                profile=profile,
                app_key=app_key,
                apply_id=apply_id,
                select_columns=select_columns,
                max_columns=max_columns,
                output_profile=output_profile,
                list_type=list_type,
                tool_name="查询记录",
            )
        return self._record_query_list(
            profile=profile,
            app_key=app_key,
            page_num=page_num,
            page_size=page_size,
            requested_pages=requested_pages,
            scan_max_pages=scan_max_pages,
            query_key=query_key,
            search_que_ids=search_que_ids,
            filters=filters,
            sorts=sorts,
            max_rows=max_rows,
            max_columns=max_columns,
            select_columns=select_columns,
            time_range=time_range,
            output_profile=output_profile,
            list_type=list_type,
            view_key=view_key,
            view_name=view_name,
            tool_name="查询记录",
        )

    def record_create(
        self,
        *,
        profile: str,
        app_key: str,
        answers: list[JSONObject] | None = None,
        fields: JSONObject | None = None,
        submit_type: int = 1,
        verify_write: bool = False,
        force_refresh_form: bool = False,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if submit_type not in (0, 1):
            raise_tool_error(QingflowApiError.config_error("submit_type must be 0 or 1"))

        def runner(session_profile, context):
            index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form) if verify_write else None
            normalized_answers = self._resolve_answers(
                profile,
                context,
                app_key,
                answers=answers or [],
                fields=fields or {},
                force_refresh_form=force_refresh_form,
            )
            self._validate_record_write(app_key, normalized_answers)
            result = self.backend.request("POST", context, f"/app/{app_key}/apply", json_body={"type": submit_type, "answers": normalized_answers})
            apply_id = _coerce_count(result.get("applyId")) if isinstance(result, dict) else None
            verification = self._verify_record_write_result(
                context,
                app_key=app_key,
                apply_id=apply_id,
                normalized_answers=normalized_answers,
                index=cast(FieldIndex, index),
                verify_list_type=14,
            ) if verify_write and index is not None else None
            verified = True if verification is None else bool(verification.get("verified"))
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "request_route": self._request_route_payload(context),
                "app_key": app_key,
                "result": result,
                "normalized_answers": normalized_answers,
                "status": "completed" if verified else "verification_failed",
                "ok": True,
                "apply_id": apply_id,
                "record_id": apply_id,
                "verify_write": verify_write,
                "write_verified": verified if verify_write else None,
                "verification": verification,
                "resource": _record_resource_payload(apply_id),
            }

        return self._run_record_tool(profile, runner, tool_name="创建记录")

    # listType 降级顺序（内部 record_get）
    _INTERNAL_GET_LIST_TYPE_FALLBACKS = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]

    def record_get(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        role: int,
        list_type: int | None,
        audit_node_id: int | None,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_apply_id = self._validate_app_and_record(app_key, apply_id)

        def runner(session_profile, context):
            base_params: JSONObject = {"role": role}
            if audit_node_id is not None:
                base_params["auditNodeId"] = audit_node_id

            # 如果调用方指定了 list_type，直接用，不降级
            if list_type is not None:
                base_params["listType"] = list_type
                result = self.backend.request("GET", context, f"/app/{app_key}/apply/{normalized_apply_id}", params=base_params)
                return {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "request_route": self._request_route_payload(context),
                    "app_key": app_key,
                    "apply_id": normalized_apply_id,
                    "result": result,
                }

            # 未指定 list_type 时自动降级
            result: JSONObject | None = None
            last_error: QingflowApiError | None = None
            for lt in self._INTERNAL_GET_LIST_TYPE_FALLBACKS:
                try:
                    params = {**base_params, "listType": lt}
                    result = self.backend.request("GET", context, f"/app/{app_key}/apply/{normalized_apply_id}", params=params)
                    break
                except QingflowApiError as exc:
                    last_error = exc
                    if _is_record_permission_denied_error(exc):
                        continue
                    raise
            if result is None:
                if last_error is not None:
                    raise last_error
                raise_tool_error(QingflowApiError.config_error("record_get failed: no accessible listType"))
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "request_route": self._request_route_payload(context),
                "app_key": app_key,
                "apply_id": normalized_apply_id,
                "result": result,
            }

        return self._run_record_tool(profile, runner, tool_name="获取记录")

    def record_search(
        self,
        *,
        profile: str,
        app_key: str,
        page_num: int,
        page_size: int,
        query_key: str | None,
        match_rules: list[JSONObject],
        sorts: list[JSONObject],
        search_que_ids: list[int] | None,
        list_type: int,
        view_key: str | None = None,
        view_name: str | None = None,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))

        def runner(session_profile, context):
            view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=view_name)
            index = (
                self._get_view_field_index(profile, context, view_selection.view_key, force_refresh=False)
                if view_selection is not None
                else self._get_field_index(profile, context, app_key, force_refresh=False)
            )
            result = self._search_page(
                context,
                app_key=app_key,
                view_selection=view_selection,
                page_num=page_num,
                page_size=page_size,
                query_key=query_key,
                match_rules=match_rules,
                sorts=sorts,
                search_que_ids=search_que_ids,
                list_type=list_type,
            )
            rows = result.get("list")
            raw_rows = rows if isinstance(rows, list) else []
            filtered_rows = [item for item in raw_rows if isinstance(item, dict)]
            if isinstance(rows, list):
                result = dict(result)
                result["list"] = filtered_rows
            returned_rows = len(filtered_rows)
            reported_total = _coerce_count(result.get("total"))
            if reported_total is None:
                reported_total = _coerce_count(result.get("count"))
            effective_count = returned_rows if view_selection is not None else (max(reported_total or 0, returned_rows) if reported_total is not None else returned_rows)
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "request_route": self._request_route_payload(context),
                "app_key": app_key,
                "page": result,
                "list_type": list_type,
                "list_type_label": get_record_list_type_label(list_type),
                "view": _view_selection_payload(view_selection),
                "page_num": page_num,
                "page_size": page_size,
                "reported_total": reported_total,
                "returned_rows": returned_rows,
                "effective_count": effective_count,
            }

        return self._run_record_tool(profile, runner, tool_name="搜索记录")

    def record_update(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int,
        answers: list[JSONObject] | None = None,
        fields: JSONObject | None = None,
        role: int = 1,
        verify_write: bool = False,
        force_refresh_form: bool = False,
    ) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_apply_id = self._validate_app_and_record(app_key, apply_id)

        def runner(session_profile, context):
            needs_index = verify_write or bool(fields) or _answers_need_resolution(answers or [])
            update_index = None
            if needs_index:
                update_index = (
                    self._get_system_browse_field_index(
                        profile,
                        context,
                        app_key,
                        list_type=DEFAULT_RECORD_LIST_TYPE,
                        force_refresh=force_refresh_form,
                    )
                    if role == 1
                    else self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
                )
            index = update_index if verify_write else None
            normalized_answers = self._resolve_answers(
                profile,
                context,
                app_key,
                answers=answers or [],
                fields=fields or {},
                force_refresh_form=force_refresh_form,
                field_index_override=update_index,
            )
            self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
            try:
                result = self.backend.request(
                    "POST",
                    context,
                    f"/app/{app_key}/apply/{normalized_apply_id}",
                    json_body={"role": role, "answers": normalized_answers},
                )
            except QingflowApiError as exc:
                self._remap_record_update_target_context_error(
                    profile,
                    context,
                    app_key=app_key,
                    apply_id=normalized_apply_id,
                    exc=exc,
                )
                raise
            verification = self._verify_record_write_result(
                context,
                app_key=app_key,
                apply_id=normalized_apply_id,
                normalized_answers=normalized_answers,
                index=cast(FieldIndex, index),
            ) if verify_write and index is not None else None
            verified = True if verification is None else bool(verification.get("verified"))
            return self._attach_human_review_notice(
                {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "request_route": self._request_route_payload(context),
                    "app_key": app_key,
                    "apply_id": normalized_apply_id,
                    "record_id": normalized_apply_id,
                    "result": result,
                    "normalized_answers": normalized_answers,
                    "status": "completed" if verified else "verification_failed",
                    "ok": True,
                    "verify_write": verify_write,
                    "write_verified": verified if verify_write else None,
                    "verification": verification,
                    "resource": _record_resource_payload(normalized_apply_id),
                },
                operation="update",
                target="record data",
            )

        return self._run_record_tool(profile, runner, tool_name="修改记录")

    def record_delete(self, *, profile: str, app_key: str, apply_id: int, list_type: int) -> JSONObject:
        """执行记录相关逻辑。"""
        normalized_apply_id = self._validate_app_and_record(app_key, apply_id)
        delete_list_type = self._resolve_record_delete_list_type(view_id=None, list_type=list_type)

        def runner(session_profile, context):
            result = self.backend.request(
                "DELETE",
                context,
                f"/app/{app_key}/apply",
                json_body={"type": delete_list_type, "applyIds": [normalized_apply_id]},
            )
            return self._attach_human_review_notice(
                {
                    "profile": profile,
                    "ws_id": session_profile.selected_ws_id,
                    "request_route": self._request_route_payload(context),
                    "app_key": app_key,
                    "apply_id": normalized_apply_id,
                    "list_type": delete_list_type,
                    "result": result,
                },
                operation="delete",
                target="record data",
            )

        return self._run_record_tool(profile, runner, tool_name="删除记录（兼容）")

    def _record_query_record(
        self,
        *,
        profile: str,
        app_key: str,
        apply_id: int | None,
        select_columns: list[str | int],
        max_columns: int | None,
        output_profile: str,
        list_type: int,
        tool_name: str = "查询记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if apply_id is None or apply_id <= 0:
            raise_tool_error(QingflowApiError.config_error("apply_id is required"))
        if not select_columns:
            raise_tool_error(QingflowApiError.config_error("select_columns is required in record mode"))

        def runner(session_profile, context):
            index = self._get_field_index(profile, context, app_key, force_refresh=False)
            resolved_column_cap = _bounded_column_limit(
                max_columns,
                default_limit=MAX_RECORD_COLUMN_LIMIT,
                hard_limit=MAX_RECORD_COLUMN_LIMIT,
            )
            selected_fields = self._resolve_select_columns(
                select_columns,
                index,
                max_columns=max_columns,
                default_limit=MAX_RECORD_COLUMN_LIMIT,
            )
            result = self.backend.request(
                "GET",
                context,
                f"/app/{app_key}/apply/{apply_id}",
                params={"role": _record_detail_role_for_list_type(list_type), "listType": list_type},
            )
            answers = result.get("answers") if isinstance(result, dict) else None
            answer_list = answers if isinstance(answers, list) else []
            row = _build_flat_row(answer_list, selected_fields, apply_id=apply_id)
            completeness = _build_completeness(
                result_amount=1,
                returned_items=1,
                fetched_pages=1,
                requested_pages=1,
                has_more=False,
                next_page_token=None,
                is_complete=True,
                omitted_items=0,
                extra={},
            )
            evidence = {
                "query_id": _query_id(),
                "app_key": app_key,
                "filters": [],
                "selected_columns": [field.que_title for field in selected_fields],
                "time_range": None,
                "source_pages": [1],
            }
            response: JSONObject = {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "request_route": self._request_route_payload(context),
                "data": {
                    "mode": "record",
                    "source_tool": "record_get",
                    "record": {
                        "apply_id": apply_id,
                        "row": row,
                        "applied_limits": {
                            "column_cap": resolved_column_cap,
                            "selected_columns": [field.que_title for field in selected_fields],
                        },
                    },
                },
                "output_profile": output_profile,
                "next_page_token": None,
            }
            if output_profile == "verbose":
                response["completeness"] = completeness
                response["evidence"] = evidence
                response["resolved_mappings"] = {
                    "select_columns": [_field_mapping_entry("row", field, requested=field.que_title) for field in selected_fields]
                }
            return response

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _record_query_list(
        self,
        *,
        profile: str,
        app_key: str,
        page_num: int,
        page_size: int,
        requested_pages: int,
        scan_max_pages: int,
        query_key: str | None,
        search_que_ids: list[int] | None,
        filters: list[JSONObject],
        sorts: list[JSONObject],
        max_rows: int,
        max_columns: int | None,
        select_columns: list[str | int],
        time_range: JSONObject,
        output_profile: str,
        list_type: int,
        view_key: str | None = None,
        view_name: str | None = None,
        tool_name: str = "查询记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if not select_columns:
            raise_tool_error(QingflowApiError.config_error("select_columns is required in list mode"))
        if max_rows <= 0:
            raise_tool_error(QingflowApiError.config_error("max_rows must be positive"))

        def runner(session_profile, context):
            index = self._get_field_index(profile, context, app_key, force_refresh=False)
            view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=view_name)
            resolved_column_cap = _bounded_column_limit(
                max_columns,
                default_limit=MAX_LIST_COLUMN_LIMIT,
                hard_limit=MAX_LIST_COLUMN_LIMIT,
            )
            selected_fields = self._resolve_select_columns(
                select_columns,
                index,
                max_columns=max_columns,
                default_limit=MAX_LIST_COLUMN_LIMIT,
            )
            explicit_search_scope = search_que_ids is not None
            if explicit_search_scope:
                primary_search_que_ids = list(dict.fromkeys(search_que_ids or [])) or None
                remaining_field_batches: list[list[FormField]] = []
                selected_fields_from_primary = selected_fields
            else:
                selected_field_batches = _chunk_fields(selected_fields, BACKEND_LIST_SEARCH_FIELD_LIMIT)
                primary_search_que_ids = [field.que_id for field in selected_field_batches[0]]
                if view_selection is not None and not _view_selection_supported_by_search_ids(view_selection, primary_search_que_ids):
                    primary_search_que_ids = None
                    remaining_field_batches = []
                    selected_fields_from_primary = selected_fields
                else:
                    remaining_field_batches = selected_field_batches[1:]
                    primary_search_que_ids = primary_search_que_ids or None
                    primary_que_ids = set(primary_search_que_ids or [])
                    selected_fields_from_primary = [field for field in selected_fields if field.que_id in primary_que_ids]
            time_field = self._resolve_time_range_column(time_range, index)
            match_rules = self._resolve_match_rules(context, filters, index)
            sort_rules = self._resolve_sorts(sorts, index)
            match_rules = self._append_time_range_filter(match_rules, time_range, time_field)
            scan_limit = min(max(requested_pages, 1), max(scan_max_pages, 1))
            current_page = max(page_num, 1)
            scanned_pages = 0
            rows: list[JSONObject] = []
            normalized_rows: list[JSONObject] = []
            matched_records = 0
            result_amount: int | None = None
            reported_total: int | None = None
            has_more = False
            source_pages: list[int] = []
            used_list_type: int | None = None
            fallback_list_types = (
                [list_type]
                if view_selection is not None or list_type != DEFAULT_RECORD_LIST_TYPE
                else [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
            )
            while scanned_pages < scan_limit and len(rows) < max_rows:
                if used_list_type is None:
                    last_error: QingflowApiError | None = None
                    page: JSONObject | None = None
                    for candidate_list_type in fallback_list_types:
                        try:
                            page = self._search_page(
                                context,
                                app_key=app_key,
                                view_selection=view_selection,
                                page_num=current_page,
                                page_size=page_size,
                                query_key=query_key,
                                match_rules=match_rules,
                                sorts=sort_rules,
                                search_que_ids=primary_search_que_ids,
                                list_type=candidate_list_type,
                            )
                            used_list_type = None if view_selection is not None else candidate_list_type
                            break
                        except QingflowApiError as exc:
                            last_error = exc
                            if (
                                self._should_retry_list_type_fallback(exc)
                                and candidate_list_type != fallback_list_types[-1]
                            ):
                                continue
                            raise
                    if page is None:
                        if last_error is not None:
                            raise last_error
                        raise_tool_error(QingflowApiError.config_error("record_list failed: no accessible listType"))
                else:
                    page = self._search_page(
                        context,
                        app_key=app_key,
                        view_selection=view_selection,
                        page_num=current_page,
                        page_size=page_size,
                        query_key=query_key,
                        match_rules=match_rules,
                        sorts=sort_rules,
                        search_que_ids=primary_search_que_ids,
                        list_type=used_list_type,
                    )
                scanned_pages += 1
                source_pages.append(current_page)
                page_rows = page.get("list")
                items = page_rows if isinstance(page_rows, list) else []
                if result_amount is None:
                    reported_total = _coerce_count(page.get("total"))
                    if reported_total is None:
                        reported_total = _coerce_count(page.get("count"))
                    result_amount = _effective_total(page, page_size)
                has_more = _page_has_more(page, current_page, page_size, len(items))
                page_output_rows: list[JSONObject] = []
                page_apply_order: list[int] = []
                page_answer_map: dict[int, list[JSONValue]] = {}
                for item in items:
                    if not isinstance(item, dict):
                        continue
                    answers = item.get("answers")
                    answer_list = answers if isinstance(answers, list) else []
                    matched_records += 1
                    apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
                    row = _build_flat_row(answer_list, selected_fields_from_primary, apply_id=apply_id)
                    rows.append(row)
                    page_output_rows.append(row)
                    if apply_id is not None:
                        page_apply_order.append(apply_id)
                        page_answer_map[apply_id] = cast(list[JSONValue], answer_list)
                    if len(rows) >= max_rows:
                        break
                if page_output_rows and remaining_field_batches:
                    page_row_map = {
                        _coerce_count(row.get("apply_id")): row
                        for row in page_output_rows
                        if isinstance(row, dict) and _coerce_count(row.get("apply_id")) is not None
                    }
                    for batch in remaining_field_batches:
                        extra_page = self._search_page(
                            context,
                            app_key=app_key,
                            view_selection=view_selection,
                            page_num=current_page,
                            page_size=page_size,
                            query_key=query_key,
                            match_rules=match_rules,
                            sorts=sort_rules,
                            search_que_ids=[field.que_id for field in batch],
                            list_type=used_list_type or list_type,
                        )
                        extra_rows = extra_page.get("list")
                        extra_items = extra_rows if isinstance(extra_rows, list) else []
                        for extra_item in extra_items:
                            if not isinstance(extra_item, dict):
                                continue
                            apply_id = _coerce_count(extra_item.get("applyId")) or _coerce_count(extra_item.get("id"))
                            if apply_id is None or apply_id not in page_row_map:
                                continue
                            extra_answers = extra_item.get("answers")
                            extra_answer_list = extra_answers if isinstance(extra_answers, list) else []
                            partial_row = _build_flat_row(extra_answer_list, batch, apply_id=apply_id)
                            partial_row.pop("apply_id", None)
                            page_row_map[apply_id].update(partial_row)
                            page_answer_map[apply_id] = _merge_answer_lists_by_field_id(
                                page_answer_map.get(apply_id, []),
                                cast(list[JSONValue], extra_answer_list),
                            )
                if output_profile == "verbose" and page_apply_order:
                    for apply_id in page_apply_order:
                        normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
                            page_answer_map.get(apply_id, []),
                            selected_fields,
                        )
                        normalized_rows.append(
                            {
                                "apply_id": apply_id,
                                "normalized_record": normalized_record,
                                "normalized_ambiguous_fields": normalized_ambiguous_fields,
                            }
                        )
                if not has_more:
                    break
                current_page += 1
            effective_result_amount = matched_records if view_selection is not None else (result_amount or len(rows))
            completeness = _build_completeness(
                result_amount=effective_result_amount,
                returned_items=len(rows),
                fetched_pages=scanned_pages,
                requested_pages=scan_limit,
                has_more=has_more,
                next_page_token=None,
                is_complete=not has_more and len(rows) < max_rows,
                omitted_items=max(0, effective_result_amount - len(rows)),
                extra={},
            )
            evidence = {
                "query_id": _query_id(),
                "app_key": app_key,
                "filters": _echo_filters(match_rules),
                "selected_columns": [field.que_title for field in selected_fields],
                "time_range": time_range or None,
                "source_pages": source_pages,
                "view": _view_selection_payload(view_selection),
            }
            response: JSONObject = {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "ok": True,
                "request_route": self._request_route_payload(context),
                "data": {
                    "mode": "list",
                    "source_tool": "record_search",
                    "view": _view_selection_payload(view_selection),
                    "list": {
                        "rows": rows,
                        "row_cap_hit": _list_row_cap_hit(returned_items=len(rows), row_cap=max_rows),
                        "sample_only": _list_sample_only(
                            returned_items=len(rows),
                            row_cap=max_rows,
                            result_amount=effective_result_amount,
                        ),
                        "safe_for_final_conclusion": False,
                        "analysis_warning": _list_sample_warning(
                            returned_items=len(rows),
                            row_cap=max_rows,
                            result_amount=effective_result_amount,
                        ),
                        "pagination": {
                            "page_num": page_num,
                            "page_size": page_size,
                            "requested_pages": scan_limit,
                            "result_amount": effective_result_amount,
                            "returned_items": len(rows),
                            "list_type_used": used_list_type,
                        },
                        "applied_limits": {
                            "row_cap": max_rows,
                            "column_cap": resolved_column_cap,
                            "selected_columns": [field.que_title for field in selected_fields],
                        },
                    },
                },
                "output_profile": output_profile,
                "next_page_token": None,
            }
            if output_profile == "verbose":
                cast(JSONObject, cast(JSONObject, response["data"])["list"])["normalized_rows"] = normalized_rows
                response["completeness"] = completeness
                evidence["backend_reported_total"] = reported_total
                response["evidence"] = evidence
                response["resolved_mappings"] = {
                    "select_columns": [_field_mapping_entry("row", field, requested=field.que_title) for field in selected_fields],
                    "filters": [_field_mapping_entry("filter", entry["field"], requested=entry["requested"]) for entry in self._resolve_filter_field_entries(filters, index)],
                    "time_range": _field_mapping_entry("time", time_field, requested=time_field.que_title) if time_field is not None else None,
                }
            return response

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _record_list_query_view_fields(
        self,
        *,
        session_profile,
        context,
        app_key: str,
        view_route: AccessibleViewRoute,
        page_num: int,
        page_size: int,
        query_key: str | None,
        search_que_ids: list[int] | None,
        match_rules: list[JSONObject],
        sort_rules: list[JSONObject],
        max_rows: int,
        selected_fields: list[FormField],
        output_profile: str,
    ) -> JSONObject:
        """Run public record_list with fields already resolved from the selected view schema."""
        view_selection = view_route.view_selection
        current_page = max(page_num, 1)
        used_list_type: int | None = None
        if view_selection is not None:
            fallback_list_types = [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
        elif view_route.list_type is not None:
            fallback_list_types = [view_route.list_type]
        else:
            fallback_list_types = [DEFAULT_RECORD_LIST_TYPE, 14, 1, 2, 12]
        last_error: QingflowApiError | None = None
        page: JSONObject | None = None
        for candidate_list_type in fallback_list_types:
            try:
                page = self._search_page(
                    context,
                    app_key=app_key,
                    view_selection=view_selection,
                    page_num=current_page,
                    page_size=page_size,
                    query_key=query_key,
                    match_rules=match_rules,
                    sorts=sort_rules,
                    search_que_ids=search_que_ids,
                    list_type=candidate_list_type,
                )
                used_list_type = None if view_selection is not None else candidate_list_type
                break
            except QingflowApiError as exc:
                last_error = exc
                if self._should_retry_list_type_fallback(exc) and candidate_list_type != fallback_list_types[-1]:
                    continue
                raise
        if page is None:
            if last_error is not None:
                raise last_error
            raise_tool_error(QingflowApiError.config_error("record_list failed: no accessible listType"))

        page_rows = page.get("list")
        items = page_rows if isinstance(page_rows, list) else []
        reported_total = _coerce_count(page.get("total"))
        if reported_total is None:
            reported_total = _coerce_count(page.get("count"))
        result_amount = _effective_total(page, page_size)
        has_more = _page_has_more(page, current_page, page_size, len(items))
        rows: list[JSONObject] = []
        normalized_rows: list[JSONObject] = []
        page_apply_order: list[int] = []
        page_answer_map: dict[int, list[JSONValue]] = {}
        for item in items:
            if not isinstance(item, dict):
                continue
            answers = item.get("answers")
            answer_list = answers if isinstance(answers, list) else []
            apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
            row = _build_flat_row(answer_list, selected_fields, apply_id=apply_id)
            rows.append(row)
            if apply_id is not None:
                page_apply_order.append(apply_id)
                page_answer_map[apply_id] = cast(list[JSONValue], answer_list)
            if len(rows) >= max_rows:
                break
        if output_profile == "verbose" and page_apply_order:
            for apply_id in page_apply_order:
                normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
                    page_answer_map.get(apply_id, []),
                    selected_fields,
                )
                normalized_rows.append(
                    {
                        "apply_id": apply_id,
                        "normalized_record": normalized_record,
                        "normalized_ambiguous_fields": normalized_ambiguous_fields,
                    }
                )
        effective_result_amount = result_amount if result_amount is not None else len(rows)
        completeness = _build_completeness(
            result_amount=effective_result_amount,
            returned_items=len(rows),
            fetched_pages=1,
            requested_pages=1,
            has_more=has_more,
            next_page_token=None,
            is_complete=not has_more and len(rows) < max_rows,
            omitted_items=max(0, effective_result_amount - len(rows)),
            extra={},
        )
        evidence = {
            "query_id": _query_id(),
            "app_key": app_key,
            "filters": _echo_filters(match_rules),
            "selected_columns": [field.que_title for field in selected_fields],
            "time_range": None,
            "source_pages": [current_page],
            "view": _view_selection_payload(view_selection),
            "backend_reported_total": reported_total,
        }
        response: JSONObject = {
            "profile": session_profile.profile,
            "ws_id": session_profile.selected_ws_id,
            "ok": True,
            "request_route": self._request_route_payload(context),
            "data": {
                "mode": "list",
                "source_tool": "record_list",
                "view": _view_selection_payload(view_selection),
                "list": {
                    "rows": rows,
                    "row_cap_hit": _list_row_cap_hit(returned_items=len(rows), row_cap=max_rows),
                    "sample_only": _list_sample_only(
                        returned_items=len(rows),
                        row_cap=max_rows,
                        result_amount=effective_result_amount,
                    ),
                    "safe_for_final_conclusion": False,
                    "analysis_warning": _list_sample_warning(
                        returned_items=len(rows),
                        row_cap=max_rows,
                        result_amount=effective_result_amount,
                    ),
                    "pagination": {
                        "page_num": current_page,
                        "page_size": page_size,
                        "requested_pages": 1,
                        "result_amount": effective_result_amount,
                        "returned_items": len(rows),
                        "list_type_used": used_list_type,
                    },
                    "applied_limits": {
                        "row_cap": max_rows,
                        "column_cap": len(selected_fields),
                        "selected_columns": [field.que_title for field in selected_fields],
                    },
                },
            },
            "output_profile": output_profile,
            "next_page_token": None,
        }
        if output_profile == "verbose":
            cast(JSONObject, cast(JSONObject, response["data"])["list"])["normalized_rows"] = normalized_rows
            response["completeness"] = completeness
            response["evidence"] = evidence
            response["resolved_mappings"] = {
                "select_columns": [_field_mapping_entry("row", field, requested=field.que_title) for field in selected_fields],
                "filters": [],
                "time_range": None,
            }
        return response

    def _get_form_schema(self, profile: str, context, app_key: str, *, force_refresh: bool) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        cache_key = (profile, app_key, "applicant_node", None)
        if not force_refresh and cache_key in self._form_cache:
            return self._form_cache[cache_key]
        schema = self.backend.request(
            "GET",
            context,
            f"/app/{app_key}/form",
            params={"type": 2, "beingApply": True},
        )
        normalized = _normalize_form_schema(schema)
        self._form_cache[cache_key] = normalized
        return normalized

    def _get_field_index(self, profile: str, context, app_key: str, *, force_refresh: bool) -> FieldIndex:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        return _build_field_index(self._get_form_schema(profile, context, app_key, force_refresh=force_refresh))

    def _get_applicant_top_level_field_index(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        force_refresh: bool,
    ) -> FieldIndex:
        """执行内部辅助逻辑。"""
        return _build_applicant_top_level_field_index(
            self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
        )

    def _resolve_applicant_node(self, profile: str, context, app_key: str, *, force_refresh: bool) -> WorkflowNodeRef:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        cache_key = (profile, app_key)
        if not force_refresh and cache_key in self._applicant_node_cache:
            return self._applicant_node_cache[cache_key]
        payload = self.backend.request("GET", context, f"/app/{app_key}/auditNodes")
        applicant_node = _extract_applicant_node(payload)
        if applicant_node is None:
            raise_tool_error(
                QingflowApiError(
                    category="config",
                    message=f"cannot resolve applicant node for app {app_key}",
                    details={
                        "error_code": "APPLICANT_NODE_NOT_FOUND",
                        "fix_hint": "Ensure the app has a workflow applicant node before using user-side record tools.",
                    },
                )
            )
        self._applicant_node_cache[cache_key] = applicant_node
        return applicant_node

    def _get_view_list(self, profile: str, context, app_key: str) -> list[JSONObject]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        cache_key = (profile, app_key)
        if cache_key in self._view_list_cache:
            return self._view_list_cache[cache_key]
        payload = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
        normalized = _normalize_view_list(payload)
        self._view_list_cache[cache_key] = normalized
        return normalized

    def _get_view_form_schema(self, profile: str, context, view_key: str, *, force_refresh: bool) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        cache_key = (profile, f"view:{view_key}", "browse_view", None)
        if not force_refresh and cache_key in self._form_cache:
            return self._form_cache[cache_key]
        schema = self.backend.request("GET", context, f"/view/{view_key}/form")
        normalized = _normalize_form_schema(schema)
        self._form_cache[cache_key] = normalized
        return normalized

    def _get_view_config(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        view_key: str,
        *,
        force_refresh: bool = False,
    ) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        cache_key = (profile, view_key)
        if not force_refresh and cache_key in self._view_config_cache:
            return self._view_config_cache[cache_key]
        try:
            payload = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
        except QingflowApiError as exc:
            if _is_optional_schema_permission_error(exc):
                self._view_config_cache[cache_key] = None
                return None
            raise
        if isinstance(payload, dict) and isinstance(payload.get("result"), dict):
            config = payload.get("result")
        elif isinstance(payload, dict) and (
            isinstance(payload.get("viewgraphLimit"), list)
            or isinstance(payload.get("viewConfig"), dict)
            or isinstance(payload.get("viewgraphConfig"), dict)
            or isinstance(payload.get("viewgraphQuestions"), list)
            or isinstance(payload.get("viewgraphQueIds"), list)
        ):
            config = payload
        else:
            config = None
        normalized = config if isinstance(config, dict) else None
        self._view_config_cache[cache_key] = normalized
        return normalized

    def _clear_record_schema_caches(
        self,
        *,
        profile: str,
        app_key: str,
        resolved_view: AccessibleViewRoute | None = None,
        clear_view_caches: bool = False,
    ) -> None:
        """执行内部辅助逻辑。"""
        view_key = (
            resolved_view.view_selection.view_key
            if resolved_view is not None and resolved_view.view_selection is not None
            else None
        )
        for cache_key in list(self._form_cache.keys()):
            cache_profile, cache_scope, *_ = cache_key
            if cache_profile != profile:
                continue
            if cache_scope == app_key:
                self._form_cache.pop(cache_key, None)
                continue
            if view_key is not None and cache_scope == f"view:{view_key}":
                self._form_cache.pop(cache_key, None)
                continue
            if clear_view_caches and isinstance(cache_scope, str) and cache_scope.startswith("view:"):
                self._form_cache.pop(cache_key, None)
        self._applicant_node_cache.pop((profile, app_key), None)
        self._view_list_cache.pop((profile, app_key), None)
        if view_key is not None:
            self._view_config_cache.pop((profile, view_key), None)
        elif clear_view_caches:
            for cache_key in [item for item in self._view_config_cache.keys() if item[0] == profile]:
                self._view_config_cache.pop(cache_key, None)

    def _record_preflight_used_force_refresh(self, raw_preflight: JSONObject) -> bool:
        """执行内部辅助逻辑。"""
        data = raw_preflight.get("data")
        return bool(isinstance(data, dict) and data.get("schema_force_refreshed"))

    def _record_preflight_suggests_stale_schema(self, plan_data: JSONObject) -> bool:
        """执行内部辅助逻辑。"""
        validation = plan_data.get("validation")
        invalid_fields = validation.get("invalid_fields") if isinstance(validation, dict) else None
        if not isinstance(invalid_fields, list):
            return False
        return any(self._field_error_suggests_stale_schema(entry) for entry in invalid_fields if isinstance(entry, dict))

    def _field_error_suggests_stale_schema(self, entry: JSONObject) -> bool:
        """执行内部辅助逻辑。"""
        error_code = _normalize_optional_text(entry.get("error_code"))
        if error_code == "VIEW_SCOPE_FIELD_HIDDEN":
            return False
        if error_code == "FIELD_NOT_FOUND":
            return True
        expected_format = entry.get("expected_format")
        if isinstance(expected_format, dict) and _normalize_optional_text(expected_format.get("kind")) == "subtable_rows":
            return True
        location = _normalize_optional_text(entry.get("location"))
        return bool(location and "[" in location and "." in location)

    def _record_get_needs_schema_refresh(
        self,
        *,
        answer_list: list[JSONValue],
        selected_fields: list[FormField],
        record: JSONObject,
        normalized_record: JSONObject,
    ) -> bool:
        """执行内部辅助逻辑。"""
        for field in selected_fields:
            if field.que_type not in SUBTABLE_QUE_TYPES:
                continue
            answer = _find_answer_for_field(answer_list, field)
            if not isinstance(answer, dict):
                continue
            if not _subtable_answer_has_unmapped_cells(answer, field):
                continue
            return True
        return False

    def _get_system_browse_schema(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        list_type: int,
        force_refresh: bool,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        cache_key = (profile, app_key, "browse_system", list_type)
        if not force_refresh and cache_key in self._form_cache:
            return self._form_cache[cache_key]
        payload = self.backend.request(
            "GET",
            context,
            f"/app/{app_key}/apply/baseInfo",
            params={"type": list_type},
        )
        normalized = _normalize_data_list_base_info_schema(payload)
        if not isinstance(normalized.get("formQues"), list) or not normalized.get("formQues"):
            try:
                return self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
            except QingflowApiError as exc:
                if not _is_optional_schema_permission_error(exc):
                    raise
                return normalized
        self._form_cache[cache_key] = normalized
        return normalized

    def _get_system_browse_base_info_schema(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        list_type: int,
        force_refresh: bool,
    ) -> JSONObject:
        """Return system view apply/baseInfo without falling back to app form metadata."""
        cache_key = (profile, app_key, "browse_system_base_info", list_type)
        if not force_refresh and cache_key in self._form_cache:
            return self._form_cache[cache_key]
        payload = self.backend.request(
            "GET",
            context,
            f"/app/{app_key}/apply/baseInfo",
            params={"type": list_type},
        )
        normalized = _normalize_data_list_base_info_schema(payload)
        self._form_cache[cache_key] = normalized
        return normalized

    def _get_custom_view_browse_schema(self, profile: str, context, view_key: str, *, force_refresh: bool) -> JSONObject:  # type: ignore[no-untyped-def]
        """Return the same baseInfo schema used by the Qingflow table view UI."""
        cache_key = (profile, f"view:{view_key}", "browse_view_base_info", None)
        if not force_refresh and cache_key in self._form_cache:
            return self._form_cache[cache_key]
        try:
            payload = self.backend.request("GET", context, f"/view/{view_key}/apply/baseInfo")
            normalized = _normalize_data_list_base_info_schema(payload)
            form_ques = normalized.get("formQues")
            if not isinstance(form_ques, list) or not form_ques:
                normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
        except QingflowApiError as exc:
            if not _is_optional_schema_permission_error(exc):
                raise
            normalized = self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh)
        self._form_cache[cache_key] = normalized
        return normalized

    def _get_view_field_index(self, profile: str, context, view_key: str, *, force_refresh: bool) -> FieldIndex:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        return _build_field_index(self._get_view_form_schema(profile, context, view_key, force_refresh=force_refresh))

    def _get_system_browse_field_index(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        list_type: int,
        force_refresh: bool,
    ) -> FieldIndex:
        """执行内部辅助逻辑。"""
        return _build_field_index(
            self._get_system_browse_schema(
                profile,
                context,
                app_key,
                list_type=list_type,
                force_refresh=force_refresh,
            )
        )

    def _get_browse_field_index(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        resolved_view: AccessibleViewRoute | None,
        *,
        force_refresh: bool,
    ) -> FieldIndex:
        """执行内部辅助逻辑。"""
        return self._build_browse_write_scope(
            profile,
            context,
            app_key,
            resolved_view,
            force_refresh=force_refresh,
        )["index"]

    def _build_browse_read_scope(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        resolved_view: AccessibleViewRoute | None,
        *,
        force_refresh: bool,
    ) -> JSONObject:
        """Build the UI/table-view readable field scope from apply/baseInfo."""
        if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
            schema = self._get_custom_view_browse_schema(
                profile,
                context,
                resolved_view.view_selection.view_key,
                force_refresh=force_refresh,
            )
            index = _build_top_level_field_index(schema)
            visible_question_ids = {field.que_id for field in index.by_id.values()}
            return {
                "index": index,
                "writable_field_ids": {
                    field.que_id
                    for field in index.by_id.values()
                    if bool(self._schema_write_hints(field)["writable"])
                },
                "visible_question_ids": visible_question_ids,
            }
        elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
            schema = self._get_system_browse_base_info_schema(
                profile,
                context,
                app_key,
                list_type=resolved_view.list_type,
                force_refresh=force_refresh,
            )
            index = _build_top_level_field_index(schema)
            visible_question_ids = {field.que_id for field in index.by_id.values()}
            return {
                "index": index,
                "writable_field_ids": {
                    field.que_id
                    for field in index.by_id.values()
                    if bool(self._schema_write_hints(field)["writable"])
                },
                "visible_question_ids": visible_question_ids,
            }

        applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
        visible_question_ids = {field.que_id for field in applicant_index.by_id.values()}
        return {
            "index": applicant_index,
            "writable_field_ids": {
                field.que_id
                for field in applicant_index.by_id.values()
                if bool(self._schema_write_hints(field)["writable"])
            },
            "visible_question_ids": visible_question_ids,
        }

    def _build_browse_write_scope(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        resolved_view: AccessibleViewRoute | None,
        *,
        force_refresh: bool,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if resolved_view is not None and resolved_view.kind == "custom" and resolved_view.view_selection is not None:
            schema = self._get_custom_view_browse_schema(
                profile,
                context,
                resolved_view.view_selection.view_key,
                force_refresh=force_refresh,
            )
            index = _build_top_level_field_index(schema)
            visible_question_ids = self._get_view_question_ids(profile, context, resolved_view.view_selection.view_key)
            if not visible_question_ids:
                visible_question_ids = _question_ids_from_schema(schema)
        elif resolved_view is not None and resolved_view.kind == "system" and resolved_view.list_type is not None:
            schema = self._get_system_browse_schema(
                profile,
                context,
                app_key,
                list_type=resolved_view.list_type,
                force_refresh=force_refresh,
            )
            index = _build_top_level_field_index(schema)
            visible_question_ids = _question_ids_from_schema(schema)
        else:
            applicant_index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=force_refresh)
            applicant_writable_field_ids = {
                field.que_id
                for field in applicant_index.by_id.values()
                if bool(self._schema_write_hints(field)["writable"])
            }
            index = applicant_index or _build_top_level_field_index(
                self._get_form_schema(profile, context, app_key, force_refresh=force_refresh)
            )
            visible_question_ids = {field.que_id for field in index.by_id.values()}
            return {
                "index": index,
                "writable_field_ids": (
                    set(applicant_writable_field_ids)
                    if applicant_index is not None
                    else {
                        field.que_id
                        for field in index.by_id.values()
                        if bool(self._schema_write_hints(field)["writable"])
                    }
                ),
                "visible_question_ids": set(visible_question_ids),
            }

        return {
            "index": index,
            "writable_field_ids": {
                field.que_id
                for field in index.by_id.values()
                if bool(self._schema_write_hints(field)["writable"])
            },
            "visible_question_ids": visible_question_ids,
        }

    def _derive_public_list_columns_for_public(
        self,
        *,
        profile: str,
        app_key: str,
        resolved_view: AccessibleViewRoute,
        tool_name: str = "记录列表",
    ) -> list[int]:
        """执行内部辅助逻辑。"""
        def runner(_session_profile, context):
            return self._derive_public_list_columns(profile, context, app_key, resolved_view)

        return cast(list[int], self._run_record_tool(profile, runner, tool_name=tool_name))

    def _resolve_record_list_query_fields_for_public(
        self,
        *,
        profile: str,
        app_key: str,
        resolved_view: AccessibleViewRoute,
        selectors: list[int],
        tool_name: str = "记录列表",
    ) -> list[int]:
        """Resolve record_list query_fields against the selected view-readable schema."""
        if not selectors:
            return []

        def runner(_session_profile, context):
            browse_scope = self._build_browse_read_scope(
                profile,
                context,
                app_key,
                resolved_view,
                force_refresh=False,
            )
            index = cast(FieldIndex, browse_scope["index"])
            return self._resolve_record_list_query_fields(selectors, index, view_route=resolved_view)

        return cast(list[int], self._run_record_tool(profile, runner, tool_name=tool_name))

    def _derive_public_list_columns(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        resolved_view: AccessibleViewRoute,
    ) -> list[int]:
        """执行内部辅助逻辑。"""
        browse_scope = self._build_browse_read_scope(
            profile,
            context,
            app_key,
            resolved_view,
            force_refresh=False,
        )
        index = cast(FieldIndex, browse_scope["index"])
        return [field.que_id for field in self._derive_record_list_fields_from_index(index)]

    def _get_view_question_ids(self, profile: str, context, view_key: str) -> set[int]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        try:
            payload = self.backend.request("GET", context, f"/view/{view_key}/question")
        except QingflowApiError as exc:
            if _is_record_permission_denied_error(exc):
                return set()
            raise
        if not isinstance(payload, list):
            return set()
        question_ids: set[int] = set()
        for item in payload:
            if not isinstance(item, dict):
                continue
            question_id = _coerce_count(item.get("queId", item.get("questionId", item.get("id"))))
            if question_id is not None:
                question_ids.add(question_id)
        return question_ids

    def _schema_fields_for_mode(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        index: FieldIndex,
        *,
        schema_mode: str,
        resolved_view: AccessibleViewRoute | None,
    ) -> list[FormField]:
        """执行内部辅助逻辑。"""
        return list(index.by_id.values())

    def _probe_list_type_access(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        list_type: int,
    ) -> bool:
        """执行内部辅助逻辑。"""
        try:
            self.backend.request(
                "POST",
                context,
                f"/app/{app_key}/apply/filter",
                json_body={"type": list_type, "pageNum": 1, "pageSize": 1},
            )
            return True
        except QingflowApiError as exc:
            if _is_record_permission_denied_error(exc):
                return False
            raise

    def _resolve_accessible_view_route_for_public(
        self,
        *,
        profile: str,
        app_key: str,
        view_id: str | None,
        list_type: int | None,
        view_key: str | None,
        view_name: str | None,
        allow_default: bool,
        tool_name: str = "记录列表",
    ) -> tuple[AccessibleViewRoute, list[JSONObject]]:
        """执行内部辅助逻辑。"""
        def runner(_session_profile, context):
            return self._resolve_accessible_view_route(
                profile,
                context,
                app_key,
                view_id=view_id,
                list_type=list_type,
                view_key=view_key,
                view_name=view_name,
                allow_default=allow_default,
            )

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _resolve_accessible_view_route(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        view_id: str | None,
        list_type: int | None,
        view_key: str | None,
        view_name: str | None,
        allow_default: bool,
    ) -> tuple[AccessibleViewRoute, list[JSONObject]]:
        """执行内部辅助逻辑。"""
        warnings: list[JSONObject] = []
        normalized_view_id = _normalize_optional_text(view_id)
        if normalized_view_id:
            if normalized_view_id.startswith("system:"):
                mapped_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE.get(normalized_view_id)
                if mapped_list_type is None:
                    raise_tool_error(QingflowApiError.config_error(f"unsupported view_id '{normalized_view_id}'"))
                return (
                    AccessibleViewRoute(
                        view_id=normalized_view_id,
                        name=get_system_view_name(normalized_view_id) or normalized_view_id,
                        kind="system",
                        list_type=mapped_list_type,
                        view_selection=None,
                        view_type=None,
                    ),
                    warnings,
                )
            if normalized_view_id.startswith("custom:"):
                resolved_key = normalized_view_id.split(":", 1)[1].strip()
                if not resolved_key:
                    raise_tool_error(QingflowApiError.config_error("custom view_id must include a non-empty view key"))
                view_selection = self._resolve_view_selection(profile, context, app_key, view_key=resolved_key, view_name=None)
                if view_selection is None:
                    raise_tool_error(QingflowApiError.config_error(f"cannot resolve custom view '{normalized_view_id}'"))
                return (
                    AccessibleViewRoute(
                        view_id=normalized_view_id,
                        name=view_selection.view_name,
                        kind="custom",
                        list_type=None,
                        view_selection=view_selection,
                        view_type=view_selection.view_type,
                    ),
                    warnings,
                )
            raise_tool_error(QingflowApiError.config_error(f"view_id '{normalized_view_id}' must start with system: or custom:"))

        if list_type is not None:
            legacy_view_id = get_system_view_id(list_type) or f"system:list_type:{list_type}"
            warnings.append(
                {
                    "code": "LEGACY_VIEW_ROUTE_DSL",
                    "message": "list_type is compatibility-only; prefer app_get.accessible_views followed by view_id.",
                }
            )
            return (
                AccessibleViewRoute(
                    view_id=legacy_view_id,
                    name=get_record_list_type_label(list_type) or str(list_type),
                    kind="system",
                    list_type=list_type,
                    view_selection=None,
                    view_type=None,
                ),
                warnings,
            )

        if view_key is not None or view_name is not None:
            view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=view_name)
            if view_selection is None:
                raise_tool_error(QingflowApiError.config_error("cannot resolve view from view_key/view_name"))
            warnings.append(
                {
                    "code": "LEGACY_VIEW_ROUTE_DSL",
                    "message": "view_key/view_name are compatibility-only; prefer app_get.accessible_views followed by view_id.",
                }
            )
            return (
                AccessibleViewRoute(
                    view_id=f"custom:{view_selection.view_key}",
                    name=view_selection.view_name,
                    kind="custom",
                    list_type=None,
                    view_selection=view_selection,
                    view_type=view_selection.view_type,
                ),
                warnings,
            )

        if not allow_default:
            raise_tool_error(QingflowApiError.config_error("view_id is required; call app_get first to inspect accessible_views"))

        system_all_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE["system:all"]
        return (
            AccessibleViewRoute(
                view_id="system:all",
                name=get_system_view_name("system:all") or "全部数据",
                kind="system",
                list_type=system_all_list_type,
                view_selection=None,
                view_type=None,
            ),
            warnings,
        )

    def _resolve_view_selection(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        view_key: str | None,
        view_name: str | None,
    ) -> ViewSelection | None:
        """执行内部辅助逻辑。"""
        requested_key = _normalize_optional_text(view_key)
        requested_name = _normalize_optional_text(view_name)
        if requested_key is None and requested_name is None:
            return None
        try:
            views = self._get_view_list(profile, context, app_key)
        except QingflowApiError as exc:
            if requested_key is None or not _is_record_permission_denied_error(exc):
                raise
            views = []
        selected: JSONObject | None = None
        if requested_key is not None:
            selected = next((item for item in views if _normalize_optional_text(item.get("viewKey")) == requested_key), None)
        if selected is None and requested_name is not None:
            exact_matches = [item for item in views if _normalize_optional_text(item.get("viewName")) == requested_name]
            if len(exact_matches) > 1:
                raise_tool_error(QingflowApiError.config_error(f"view_name '{requested_name}' is ambiguous; pass view_key instead"))
            selected = exact_matches[0] if exact_matches else None
        resolved_view_key = requested_key or _normalize_optional_text(selected.get("viewKey") if isinstance(selected, dict) else None)
        if not resolved_view_key:
            raise_tool_error(QingflowApiError.config_error(f"cannot resolve view '{requested_name or requested_key}' for app {app_key}"))
        resolved_view_name = (
            _normalize_optional_text(selected.get("viewName") if isinstance(selected, dict) else None)
            or requested_name
            or resolved_view_key
        )
        resolved_view_type = (
            _normalize_optional_text(selected.get("viewType") if isinstance(selected, dict) else None)
            or _normalize_optional_text(selected.get("viewgraphType") if isinstance(selected, dict) else None)
        )
        filter_config: JSONObject | None = None
        if isinstance(selected, dict):
            if isinstance(selected.get("viewgraphLimit"), list):
                filter_config = selected
            else:
                raw_view_config = selected.get("viewConfig")
                if isinstance(raw_view_config, dict):
                    filter_config = raw_view_config
        if filter_config is None:
            filter_config = self._get_view_config(profile, context, resolved_view_key)
        return ViewSelection(
            view_key=resolved_view_key,
            view_name=resolved_view_name,
            conditions=_compile_view_conditions(filter_config or {}),
            view_type=resolved_view_type,
            filter_config_loaded=isinstance(filter_config, dict),
        )

    def _get_department_member_ids(self, context, dept_id: int) -> set[int]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        page_num = 1
        page_size = 200
        member_ids: set[int] = set()
        while True:
            payload = self.backend.request(
                "GET",
                context,
                "/contact",
                params={"pageNum": page_num, "pageSize": page_size, "deptId": dept_id, "containDisable": False},
            )
            result = payload.get("result") if isinstance(payload, dict) else None
            rows = result if isinstance(result, list) else []
            for item in rows:
                if not isinstance(item, dict):
                    continue
                member_id = _coerce_count(item.get("uid", item.get("id")))
                if member_id is not None:
                    member_ids.add(member_id)
            page_amount = _coerce_count(payload.get("pageAmount")) if isinstance(payload, dict) else None
            if page_amount is not None:
                if page_num >= page_amount:
                    break
            elif len(rows) < page_size:
                break
            page_num += 1
        return member_ids

    def _matches_view_selection(
        self,
        context,  # type: ignore[no-untyped-def]
        answer_list: list[JSONValue],
        *,
        view_selection: ViewSelection | None,
        dept_member_cache: dict[int, set[int]],
    ) -> bool:
        """执行内部辅助逻辑。"""
        if view_selection is None or not view_selection.conditions:
            return True
        for group in view_selection.conditions:
            if all(
                _match_view_condition(
                    answer_list,
                    condition,
                    dept_member_cache=dept_member_cache,
                    dept_member_resolver=lambda dept_id: self._get_department_member_ids(context, dept_id),
                )
                for condition in group
            ):
                return True
        return False

    def _build_analysis_probe(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        arguments: JSONObject,
        view_selection: ViewSelection | None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        routed_mode = _resolve_query_mode(
            str(arguments.get("query_mode", "auto")),
            apply_id=_coerce_count(arguments.get("apply_id")),
            amount_column=arguments.get("amount_column"),
            time_range=cast(JSONObject, arguments.get("time_range") if isinstance(arguments.get("time_range"), dict) else {}),
            stat_policy=cast(JSONObject, arguments.get("stat_policy") if isinstance(arguments.get("stat_policy"), dict) else {}),
        )
        fixed_policy = _fixed_analysis_scan_policy() if routed_mode == "summary" else _fixed_list_scan_policy()
        page_size = int(fixed_policy["page_size"])
        query_key = _normalize_optional_text(arguments.get("query_key"))
        filters = _as_object_list(arguments.get("filters"))
        sorts = _as_object_list(arguments.get("sorts"))
        app_form = (
            self._get_view_field_index(profile, context, view_selection.view_key, force_refresh=False)
            if view_selection is not None
            else self._get_field_index(profile, context, app_key, force_refresh=False)
        )
        time_range = cast(JSONObject, arguments.get("time_range") if isinstance(arguments.get("time_range"), dict) else {})
        match_rules = self._resolve_match_rules(context, filters, app_form)
        sort_rules = self._resolve_sorts(sorts, app_form)
        time_field = self._resolve_time_range_column(time_range, app_form)
        match_rules = self._append_time_range_filter(match_rules, time_range, time_field)
        probe_page = self._search_page(
            context,
            app_key=app_key,
            view_selection=view_selection,
            page_num=max(_coerce_count(arguments.get("page_num")) or 1, 1),
            page_size=page_size,
            query_key=query_key,
            match_rules=match_rules,
            sorts=sort_rules,
            search_que_ids=None,
            list_type=_coerce_count(arguments.get("list_type")) or DEFAULT_RECORD_LIST_TYPE,
        )
        backend_total_count = _effective_total(probe_page, page_size)
        page_amount = _coerce_count(probe_page.get("pageAmount"))
        estimated_full_scan_pages = page_amount
        if estimated_full_scan_pages is None and backend_total_count > 0:
            estimated_full_scan_pages = (backend_total_count + page_size - 1) // page_size
        current_budget = min(int(fixed_policy["requested_pages"]), int(fixed_policy["scan_max_pages"]))
        return {
            "page_size": page_size,
            "backend_total_count": backend_total_count,
            "backend_page_amount": page_amount,
            "estimated_full_scan_pages": estimated_full_scan_pages,
            "current_scan_budget": current_budget,
            "would_exceed_current_budget": bool(estimated_full_scan_pages is not None and estimated_full_scan_pages > current_budget),
            "local_filtering": bool(view_selection.conditions) if view_selection is not None else False,
        }

    def _search_page(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        view_selection: ViewSelection | None,
        page_num: int,
        page_size: int,
        query_key: str | None,
        match_rules: list[JSONObject],
        sorts: list[JSONObject],
        search_que_ids: list[int] | None,
        list_type: int,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if view_selection is not None and _is_board_view_type(view_selection.view_type):
            return self._search_board_view_page(
                context,
                view_selection=view_selection,
                page_num=page_num,
                page_size=page_size,
                query_key=query_key,
                match_rules=match_rules,
                sorts=sorts,
                search_que_ids=search_que_ids,
            )
        body = self._build_view_filter_payload(
            page_num=page_num,
            page_size=page_size,
            query_key=query_key,
            match_rules=match_rules,
            sorts=sorts,
            search_que_ids=search_que_ids,
            list_type=list_type,
            include_list_type=True,
        )
        if view_selection is not None:
            route = f"/view/{view_selection.view_key}/apply/filter"
            body = {"filter": body, "viewgraphKey": view_selection.view_key, "equipmentType": 0}
        else:
            route = f"/app/{app_key}/apply/filter"
        result = self.backend.request("POST", context, route, json_body=body)
        return result if isinstance(result, dict) else {}

    def _should_retry_list_type_fallback(self, error: QingflowApiError) -> bool:
        """执行内部辅助逻辑。"""
        if is_auth_like_error(error):
            return False
        if backend_code_int(error) in {40002, 40027, 404}:
            return True
        if error.http_status == 404:
            return True
        return False

    def _build_view_filter_payload(
        self,
        *,
        page_num: int,
        page_size: int,
        query_key: str | None,
        match_rules: list[JSONObject],
        sorts: list[JSONObject],
        search_que_ids: list[int] | None,
        list_type: int,
        include_list_type: bool,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        body: JSONObject = {"pageNum": page_num, "pageSize": page_size}
        if include_list_type:
            body["type"] = list_type
        if query_key:
            body["queryKey"] = query_key
        if match_rules:
            queries = []
            normalized_match_rules = []
            for item in match_rules:
                if not isinstance(item, dict):
                    continue
                item_queries = _normalize_list_query_rules(item)
                if item_queries:
                    queries.extend(item_queries)
                    continue
                normalized_match_rules.extend(_normalize_list_match_rules(item))
            if queries:
                body["queries"] = queries
            if normalized_match_rules:
                body["matchRules"] = normalized_match_rules
        if sorts:
            normalized_sorts = [rule for rule in (_normalize_list_sort_rule(item) for item in sorts if isinstance(item, dict)) if rule]
            if normalized_sorts:
                body["sorts"] = normalized_sorts
        if search_que_ids:
            body["searchQueIds"] = search_que_ids
        return body

    def _get_board_lane_ids(self, context, *, view_key: str) -> list[int]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        payload = self.backend.request(
            "GET",
            context,
            f"/view/{view_key}/lane/baseInfo",
            params={"pageNum": 1, "pageSize": 200},
        )
        if not isinstance(payload, dict):
            return []
        lane_base_info_list = payload.get("laneBaseInfoList")
        if not isinstance(lane_base_info_list, list):
            return []
        lane_ids: list[int] = []
        for item in lane_base_info_list:
            if not isinstance(item, dict):
                continue
            if item.get("beingVisible") is False:
                continue
            lane_id = _coerce_count(item.get("laneId"))
            if lane_id is not None:
                lane_ids.append(lane_id)
        return lane_ids

    def _fetch_board_lane_slice(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        view_key: str,
        lane_id: int,
        filter_payload: JSONObject,
        offset: int,
        limit: int,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if limit <= 0:
            return []
        request_page_size = max(_coerce_count(filter_payload.get("pageSize")) or 1, 1)
        lane_page_num = max(offset // request_page_size + 1, 1)
        lane_skip = max(offset % request_page_size, 0)
        rows: list[JSONObject] = []
        while len(rows) < limit:
            lane_filter = dict(filter_payload)
            lane_filter["pageNum"] = lane_page_num
            lane_filter["pageSize"] = request_page_size
            payload = self.backend.request(
                "POST",
                context,
                f"/view/{view_key}/lane/{lane_id}/boardViewFilter",
                json_body={"filter": lane_filter, "viewgraphKey": view_key, "equipmentType": 0},
            )
            if not isinstance(payload, dict):
                break
            raw_items = payload.get("result")
            page_items = raw_items if isinstance(raw_items, list) else []
            sliced_items = page_items[lane_skip:] if lane_skip else page_items
            lane_skip = 0
            if not sliced_items:
                break
            rows.extend(item for item in sliced_items if isinstance(item, dict))
            if len(rows) >= limit:
                break
            page_amount = _coerce_count(payload.get("pageAmount"))
            if page_amount is not None and lane_page_num >= page_amount:
                break
            if len(page_items) < request_page_size:
                break
            lane_page_num += 1
        return rows[:limit]

    def _search_board_view_page(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        view_selection: ViewSelection,
        page_num: int,
        page_size: int,
        query_key: str | None,
        match_rules: list[JSONObject],
        sorts: list[JSONObject],
        search_que_ids: list[int] | None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        filter_payload = self._build_view_filter_payload(
            page_num=page_num,
            page_size=page_size,
            query_key=query_key,
            match_rules=match_rules,
            sorts=sorts,
            search_que_ids=search_que_ids,
            list_type=DEFAULT_RECORD_LIST_TYPE,
            include_list_type=False,
        )
        lane_ids = self._get_board_lane_ids(context, view_key=view_selection.view_key)
        if not lane_ids:
            return {"list": [], "pageNum": page_num, "pageSize": page_size, "pageAmount": 0, "total": 0}

        summary_filter = dict(filter_payload)
        summary_filter["pageNum"] = 1
        summary_filter["pageSize"] = 1
        summary = self.backend.request(
            "POST",
            context,
            f"/view/{view_selection.view_key}/lane/boardViewFilter",
            json_body={"filter": summary_filter, "laneList": lane_ids},
        )
        if not isinstance(summary, dict):
            return {"list": [], "pageNum": page_num, "pageSize": page_size, "pageAmount": 0, "total": 0}
        lane_results_raw = summary.get("boardViewApplyResult")
        lane_results = lane_results_raw if isinstance(lane_results_raw, list) else []
        total = _coerce_count(summary.get("resultAmount"))
        if total is None:
            total = sum(
                _coerce_count(item.get("resultAmount")) or len(item.get("result") if isinstance(item.get("result"), list) else [])
                for item in lane_results
                if isinstance(item, dict)
            )

        start = max(page_num - 1, 0) * page_size
        end = start + page_size
        cursor = 0
        collected: list[JSONObject] = []
        for lane_result in lane_results:
            if not isinstance(lane_result, dict):
                continue
            lane_id = _coerce_count(lane_result.get("laneId"))
            raw_lane_items = lane_result.get("result")
            lane_items = raw_lane_items if isinstance(raw_lane_items, list) else []
            lane_total = _coerce_count(lane_result.get("resultAmount"))
            if lane_total is None:
                lane_total = len(lane_items)
            if lane_id is None or lane_total <= 0:
                cursor += max(lane_total or 0, 0)
                continue

            overlap_start = max(start, cursor)
            overlap_end = min(end, cursor + lane_total)
            if overlap_start < overlap_end:
                lane_offset = overlap_start - cursor
                lane_limit = overlap_end - overlap_start
                if page_num == 1 and lane_offset == 0 and len(lane_items) >= lane_limit:
                    collected.extend(item for item in lane_items[:lane_limit] if isinstance(item, dict))
                else:
                    collected.extend(
                        self._fetch_board_lane_slice(
                            context,
                            view_key=view_selection.view_key,
                            lane_id=lane_id,
                            filter_payload=filter_payload,
                            offset=lane_offset,
                            limit=lane_limit,
                        )
                    )

            cursor += lane_total
            if cursor >= end:
                break

        page_amount = (total + page_size - 1) // page_size if total > 0 and page_size > 0 else 0
        return {
            "list": collected[:page_size],
            "pageNum": page_num,
            "pageSize": page_size,
            "pageAmount": page_amount,
            "total": total,
        }

    def _resolve_answers(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        app_key: str,
        *,
        answers: list[JSONObject],
        fields: JSONObject,
        force_refresh_form: bool,
        resolution_state: LookupResolutionState | None = None,
        field_index_override: FieldIndex | None = None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if not answers and not fields:
            raise_tool_error(QingflowApiError.config_error("either answers or fields is required"))
        if not answers and fields:
            index = field_index_override or self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
            normalized: list[JSONObject] = []
            for key, value in fields.items():
                if value is None:
                    continue
                answer = self._build_field_answer(profile, context, index, key, value, resolution_state=resolution_state)
                if answer is not None:
                    normalized.append(answer)
                    if resolution_state is not None:
                        resolution_state.normalized_answers = list(normalized)
            return normalized
        if answers and not _answers_need_resolution(answers) and not fields:
            return answers
        index = field_index_override or self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
        normalized: list[JSONObject] = []
        for item in answers:
            answer = self._normalize_answer_item(profile, context, index, item, resolution_state=resolution_state)
            if answer is not None:
                normalized.append(answer)
                if resolution_state is not None:
                    resolution_state.normalized_answers = list(normalized)
        if fields:
            for key, value in fields.items():
                if value is None:
                    continue
                answer = self._build_field_answer(profile, context, index, key, value, resolution_state=resolution_state)
                if answer is not None:
                    normalized.append(answer)
                    if resolution_state is not None:
                        resolution_state.normalized_answers = list(normalized)
        return normalized

    def _normalize_answer_item(
        self,
        profile: str,
        context,
        index: FieldIndex,
        item: JSONObject,
        *,
        resolution_state: LookupResolutionState | None = None,
    ) -> JSONObject | None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        field = self._resolve_field_from_answer_item(item, index)
        if field.que_type in SUBTABLE_QUE_TYPES:
            table_values_input: JSONValue = item.get("tableValues")
            if table_values_input is None:
                table_values_input = item.get("rows", item.get("value", item.get("values")))
            normalized_rows = self._normalize_subtable_rows(
                profile,
                context,
                field,
                table_values_input,
                location=field.que_title,
                resolution_state=resolution_state,
            )
            payload: JSONObject = {
                "queId": field.que_id,
                "queType": field.que_type or 18,
                "values": [],
                "tableValues": normalized_rows,
            }
            previous_row_ordinals = item.get("previousTableRowOrdinalList")
            if isinstance(previous_row_ordinals, list):
                payload["previousTableRowOrdinalList"] = previous_row_ordinals
            return payload
        self._raise_if_verify_unsupported_write_field(field, item, location=field.que_title)
        if "values" in item and isinstance(item["values"], list):
            values = item["values"]
        elif field.que_type in RELATION_QUE_TYPES and isinstance(item.get("referValues"), list):
            values = item["referValues"]
        elif "value" in item:
            values = [item["value"]]
        else:
            raise RecordInputError(
                message=f"answer for field '{field.que_title}' requires value or values",
                error_code="MISSING_VALUE",
                fix_hint="Pass value for scalar fields, or values for multi-value fields.",
                details={"location": field.que_title, "field": _field_ref_payload(field), "expected_format": _write_format_for_field(field)},
            )
        if field.que_type in RELATION_QUE_TYPES:
            return self._build_relation_answer(
                profile,
                context,
                field,
                values,
                resolution_state=resolution_state,
                location=field.que_title,
            )
        normalized_values = self._normalize_field_values(
            profile,
            context,
            field,
            values,
            resolution_state=resolution_state,
            location=field.que_title,
        )
        if not normalized_values and resolution_state is not None and field.que_type in MEMBER_QUE_TYPES | DEPARTMENT_QUE_TYPES:
            return None
        return {
            "queId": field.que_id,
            "queType": field.que_type or 2,
            "values": normalized_values,
            "tableValues": item.get("tableValues") if isinstance(item.get("tableValues"), list) else [],
        }

    def _build_field_answer(
        self,
        profile: str,
        context,
        index: FieldIndex,
        field_selector: str,
        raw_value: JSONValue,
        *,
        resolution_state: LookupResolutionState | None = None,
    ) -> JSONObject | None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        field = self._resolve_field_selector(field_selector, index, location="fields")
        if field.que_type in SUBTABLE_QUE_TYPES:
            return {
                "queId": field.que_id,
                "queType": field.que_type or 18,
                "values": [],
                "tableValues": self._normalize_subtable_rows(
                    profile,
                    context,
                    field,
                    raw_value,
                    location=str(field_selector),
                    resolution_state=resolution_state,
                ),
            }
        self._raise_if_verify_unsupported_write_field(field, raw_value, location=str(field_selector))
        values = raw_value if isinstance(raw_value, list) and field.que_type in MULTI_SELECT_QUE_TYPES else [raw_value]
        if field.que_type in RELATION_QUE_TYPES:
            return self._build_relation_answer(
                profile,
                context,
                field,
                values,
                resolution_state=resolution_state,
                location=str(field_selector),
            )
        normalized_values = self._normalize_field_values(
            profile,
            context,
            field,
            values,
            resolution_state=resolution_state,
            location=str(field_selector),
        )
        if not normalized_values and resolution_state is not None and field.que_type in MEMBER_QUE_TYPES | DEPARTMENT_QUE_TYPES:
            return None
        return {
            "queId": field.que_id,
            "queType": field.que_type or 2,
            "values": normalized_values,
            "tableValues": [],
        }

    def _build_relation_answer(
        self,
        profile: str,
        context,
        field: FormField,
        raw_values: list[JSONValue],
        *,
        resolution_state: LookupResolutionState | None = None,
        location: str,
        parent_field: FormField | None = None,
        row_ordinal: int | None = None,
    ) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        values: list[JSONObject] = []
        refer_values: list[JSONObject] = []
        for raw_value in _expand_values(raw_values):
            value_payload, refer_payload = self._relation_value_payload(
                profile,
                context,
                field,
                raw_value,
                resolution_state=resolution_state,
                location=location,
                parent_field=parent_field,
                row_ordinal=row_ordinal,
            )
            if value_payload is None or refer_payload is None:
                continue
            values.append(value_payload)
            refer_values.append(refer_payload)
        if not values and resolution_state is not None:
            return None
        return {
            "queId": field.que_id,
            "queType": field.que_type or 25,
            "values": values,
            "referValues": refer_values,
            "tableValues": [],
        }

    def _raise_if_verify_unsupported_write_field(self, field: FormField, raw_value: JSONValue, *, location: str) -> None:
        """执行内部辅助逻辑。"""
        if field.que_type not in VERIFY_UNSUPPORTED_WRITE_QUE_TYPES:
            return
        raise RecordInputError(
            message=f"field '{field.que_title}' uses unsupported direct writes",
            error_code="UNSUPPORTED_WRITE_FORMAT",
            fix_hint=_unsupported_write_fix_hint(field.que_type),
            details={
                "location": location,
                "field": _field_ref_payload(field),
                "expected_format": _write_format_for_field(field),
                "received_value": raw_value,
            },
        )

    def _normalize_field_values(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        raw_values: list[JSONValue],
        *,
        resolution_state: LookupResolutionState | None = None,
        location: str = "field",
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if field.que_type in SINGLE_SELECT_QUE_TYPES:
            return [_option_value(raw_values[0], field)]
        if field.que_type in MULTI_SELECT_QUE_TYPES:
            return [_option_value(value, field) for value in raw_values]
        if field.que_type in BOOLEAN_QUE_TYPES:
            return [{"value": _boolean_display(raw_values[0])}]
        if field.que_type in MEMBER_QUE_TYPES:
            values: list[JSONObject] = []
            for value in _expand_values(raw_values):
                resolved = self._member_value_from_selector(
                    profile,
                    context,
                    field,
                    value,
                    resolution_state=resolution_state,
                    location=location,
                )
                if resolved is not None:
                    values.append(resolved)
            return values
        if field.que_type in DEPARTMENT_QUE_TYPES:
            values: list[JSONObject] = []
            for value in _expand_values(raw_values):
                resolved = self._department_value_from_selector(
                    profile,
                    context,
                    field,
                    value,
                    resolution_state=resolution_state,
                    location=location,
                )
                if resolved is not None:
                    values.append(resolved)
            return values
        if field.que_type in ATTACHMENT_QUE_TYPES:
            return [_attachment_value(value) for value in _expand_values(raw_values)]
        if field.que_type in ADDRESS_QUE_TYPES:
            return _address_value_payloads(raw_values)
        if field.que_type == 8:
            return [{"value": _normalize_amount_value_for_write(field, raw_values[0])}]
        return [{"value": _stringify_json(raw_values[0])}]

    def _normalize_subtable_rows(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        table_field: FormField,
        raw_rows: JSONValue,
        *,
        location: str,
        resolution_state: LookupResolutionState | None = None,
    ) -> list[list[JSONObject]]:
        """执行内部辅助逻辑。"""
        row_values = raw_rows
        if isinstance(raw_rows, dict):
            if "rows" in raw_rows:
                row_values = raw_rows.get("rows")
            elif "tableValues" in raw_rows:
                row_values = raw_rows.get("tableValues")
        if row_values is None:
            return []
        if not isinstance(row_values, list):
            raise RecordInputError(
                message=f"field '{table_field.que_title}' requires subtable rows",
                error_code="INVALID_SUBTABLE_VALUE",
                fix_hint="Pass subtables as a list of row objects or a native tableValues array.",
                details={"location": location, "field": _field_ref_payload(table_field), "expected_format": _write_format_for_field(table_field), "received_value": raw_rows},
            )
        subtable_index = self._subtable_field_index(table_field)
        normalized_rows: list[list[JSONObject]] = []
        for row_ordinal, row in enumerate(row_values):
            normalized_rows.append(
                self._normalize_subtable_row(
                    profile,
                    context,
                    table_field,
                    subtable_index,
                    row,
                    row_ordinal=row_ordinal,
                    location=location,
                    resolution_state=resolution_state,
                )
            )
        return normalized_rows

    def _normalize_subtable_row(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        table_field: FormField,
        subtable_index: FieldIndex,
        row: JSONValue,
        *,
        row_ordinal: int,
        location: str,
        resolution_state: LookupResolutionState | None = None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        row_id: int | None = None
        normalized_cells: list[JSONObject] = []
        if isinstance(row, dict):
            row_id = _coerce_count(row.get("__row_id__", row.get("row_id", row.get("rowId"))))
            if isinstance(row.get("answers"), list):
                confirmation_count_before = len(resolution_state.confirmation_requests) if resolution_state is not None else 0
                normalized_cells = [
                    self._normalize_subtable_answer_item(
                        profile,
                        context,
                        subtable_index,
                        item,
                        location=f"{location}[{row_ordinal}]",
                        resolution_state=resolution_state,
                        parent_field=table_field,
                        row_ordinal=row_ordinal + 1,
                    )
                    for item in row["answers"]
                    if isinstance(item, dict)
                ]
                normalized_cells = [cell for cell in normalized_cells if cell is not None]
                if not normalized_cells and resolution_state is not None and len(resolution_state.confirmation_requests) > confirmation_count_before:
                    return []
            elif isinstance(row.get("fields"), dict):
                confirmation_count_before = len(resolution_state.confirmation_requests) if resolution_state is not None else 0
                normalized_cells = [
                    self._build_subtable_field_answer(
                        profile,
                        context,
                        subtable_index,
                        key,
                        value,
                        location=f"{location}[{row_ordinal}]",
                        resolution_state=resolution_state,
                        parent_field=table_field,
                        row_ordinal=row_ordinal + 1,
                    )
                    for key, value in row["fields"].items()
                    if value is not None
                ]
                normalized_cells = [cell for cell in normalized_cells if cell is not None]
                if not normalized_cells and resolution_state is not None and len(resolution_state.confirmation_requests) > confirmation_count_before:
                    return []
            else:
                row_fields = {
                    key: value
                    for key, value in row.items()
                    if key not in {"__row_id__", "row_id", "rowId", "answers", "fields", "rows", "tableValues"}
                }
                confirmation_count_before = len(resolution_state.confirmation_requests) if resolution_state is not None else 0
                normalized_cells = [
                    self._build_subtable_field_answer(
                        profile,
                        context,
                        subtable_index,
                        key,
                        value,
                        location=f"{location}[{row_ordinal}]",
                        resolution_state=resolution_state,
                        parent_field=table_field,
                        row_ordinal=row_ordinal + 1,
                    )
                    for key, value in row_fields.items()
                    if value is not None
                ]
                normalized_cells = [cell for cell in normalized_cells if cell is not None]
                if not normalized_cells and resolution_state is not None and len(resolution_state.confirmation_requests) > confirmation_count_before:
                    return []
        elif isinstance(row, list):
            confirmation_count_before = len(resolution_state.confirmation_requests) if resolution_state is not None else 0
            normalized_cells = [
                self._normalize_subtable_answer_item(
                    profile,
                    context,
                    subtable_index,
                    item,
                    location=f"{location}[{row_ordinal}]",
                    resolution_state=resolution_state,
                    parent_field=table_field,
                    row_ordinal=row_ordinal + 1,
                )
                for item in row
                if isinstance(item, dict)
            ]
            normalized_cells = [cell for cell in normalized_cells if cell is not None]
            if not normalized_cells and resolution_state is not None and len(resolution_state.confirmation_requests) > confirmation_count_before:
                return []
        else:
            raise RecordInputError(
                message=f"field '{table_field.que_title}' row {row_ordinal + 1} has unsupported shape",
                error_code="INVALID_SUBTABLE_ROW",
                fix_hint="Pass each subtable row as an object keyed by subfield title, or a list of native answer objects.",
                details={"location": location, "field": _field_ref_payload(table_field), "row_ordinal": row_ordinal, "received_value": row},
            )
        if not normalized_cells:
            raise RecordInputError(
                message=f"field '{table_field.que_title}' row {row_ordinal + 1} is empty",
                error_code="EMPTY_SUBTABLE_ROW",
                fix_hint="Provide at least one subfield value in each subtable row, or omit the row entirely.",
                details={"location": location, "field": _field_ref_payload(table_field), "row_ordinal": row_ordinal, "received_value": row},
            )
        if row_id is not None:
            for cell in normalized_cells:
                cell.setdefault("rowId", row_id)
        return normalized_cells

    def _normalize_subtable_answer_item(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        subtable_index: FieldIndex,
        item: JSONObject,
        *,
        location: str,
        resolution_state: LookupResolutionState | None = None,
        parent_field: FormField | None = None,
        row_ordinal: int | None = None,
    ) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        field = self._resolve_field_from_answer_item(item, subtable_index)
        if field.que_type in SUBTABLE_QUE_TYPES:
            raise RecordInputError(
                message=f"field '{field.que_title}' uses unsupported nested subtable writes",
                error_code="UNSUPPORTED_WRITE_FORMAT",
                fix_hint="Nested subtable writes are not supported in app-user tools.",
                details={"location": location, "field": _field_ref_payload(field), "expected_format": _write_format_for_field(field), "received_value": item},
            )
        self._raise_if_verify_unsupported_write_field(field, item, location=location)
        if "values" in item and isinstance(item["values"], list):
            values = item["values"]
        elif field.que_type in RELATION_QUE_TYPES and isinstance(item.get("referValues"), list):
            values = item["referValues"]
        elif "value" in item:
            values = [item["value"]]
        else:
            raise RecordInputError(
                message=f"subtable field '{field.que_title}' requires value or values",
                error_code="MISSING_VALUE",
                fix_hint="Pass value for scalar subtable fields, or values for multi-value subtable fields.",
                details={"location": location, "field": _field_ref_payload(field), "expected_format": _write_format_for_field(field)},
            )
        payload = self._build_relation_answer(
            profile,
            context,
            field,
            values,
            resolution_state=resolution_state,
            location=location,
            parent_field=parent_field,
            row_ordinal=row_ordinal,
        ) if field.que_type in RELATION_QUE_TYPES else {
            "queId": field.que_id,
            "queType": field.que_type or 2,
            "values": self._normalize_field_values(
                profile,
                context,
                field,
                values,
                resolution_state=resolution_state,
                location=location,
            ),
            "tableValues": [],
        }
        if payload is None:
            return None
        row_id = _coerce_count(item.get("rowId", item.get("row_id")))
        if row_id is not None:
            payload["rowId"] = row_id
        return payload

    def _build_subtable_field_answer(
        self,
        profile: str,
        context,  # type: ignore[no-untyped-def]
        subtable_index: FieldIndex,
        field_selector: str,
        raw_value: JSONValue,
        *,
        location: str,
        resolution_state: LookupResolutionState | None = None,
        parent_field: FormField | None = None,
        row_ordinal: int | None = None,
    ) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        field = self._resolve_field_selector(field_selector, subtable_index, location=location)
        if field.que_type in SUBTABLE_QUE_TYPES:
            raise RecordInputError(
                message=f"field '{field.que_title}' uses unsupported nested subtable writes",
                error_code="UNSUPPORTED_WRITE_FORMAT",
                fix_hint="Nested subtable writes are not supported in app-user tools.",
                details={"location": location, "field": _field_ref_payload(field), "expected_format": _write_format_for_field(field), "received_value": raw_value},
            )
        self._raise_if_verify_unsupported_write_field(field, raw_value, location=location)
        values = raw_value if isinstance(raw_value, list) and field.que_type in MULTI_SELECT_QUE_TYPES else [raw_value]
        if field.que_type in RELATION_QUE_TYPES:
            return self._build_relation_answer(
                profile,
                context,
                field,
                values,
                resolution_state=resolution_state,
                location=location,
                parent_field=parent_field,
                row_ordinal=row_ordinal,
            )
        normalized_values = self._normalize_field_values(
            profile,
            context,
            field,
            values,
            resolution_state=resolution_state,
            location=location,
        )
        if not normalized_values and resolution_state is not None and field.que_type in MEMBER_QUE_TYPES | DEPARTMENT_QUE_TYPES:
            return None
        return {
            "queId": field.que_id,
            "queType": field.que_type or 2,
            "values": normalized_values,
            "tableValues": [],
        }

    def _subtable_field_index(self, table_field: FormField) -> FieldIndex:
        """执行内部辅助逻辑。"""
        raw = table_field.raw if isinstance(table_field.raw, dict) else {}
        schema: JSONObject = {}
        if isinstance(raw.get("subQuestions"), list):
            schema["formQues"] = [raw["subQuestions"]]
        elif isinstance(raw.get("subQues"), list):
            schema["formQues"] = [raw["subQues"]]
        elif isinstance(raw.get("innerQuestions"), list):
            schema["formQues"] = raw["innerQuestions"]
        index = _build_field_index(schema)
        if not index.by_id:
            raise RecordInputError(
                message=f"field '{table_field.que_title}' does not expose subtable column metadata",
                error_code="SUBTABLE_SCHEMA_UNAVAILABLE",
                fix_hint="Refresh the form schema and ensure the subtable columns are visible before writing.",
                details={"field": _field_ref_payload(table_field), "raw": raw},
            )
        return index

    def _subtable_field_index_optional(self, table_field: FormField | None) -> FieldIndex | None:
        """执行内部辅助逻辑。"""
        if table_field is None:
            return None
        try:
            return self._subtable_field_index(table_field)
        except RecordInputError:
            return None

    def _run_record_tool(
        self,
        profile: str,
        func,
        *,
        require_workspace: bool = True,
        tool_name: str,
    ):  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        try:
            return self._run(profile, func, require_workspace=require_workspace, tool_name=tool_name)
        except RecordInputError as error:
            raise_tool_error(
                QingflowApiError(
                    category="config",
                    message=error.message,
                    backend_code=error.error_code,
                    details={
                        "error_code": error.error_code,
                        "fix_hint": error.fix_hint,
                        "record_input": error.to_dict(),
                    },
                )
            )

    def _candidate_lookup_failed_response(
        self,
        *,
        profile: str,
        session_profile,  # type: ignore[no-untyped-def]
        context,  # type: ignore[no-untyped-def]
        kind: str,
        error: RecordInputError,
        field: FormField,
        app_key: str,
        record_id_text: str | None,
        workflow_node_id: int | None,
        fields_present: bool,
        keyword: str,
        scope_source: str,
    ) -> JSONObject:
        """Return a structured result when an optional field candidate lookup is unavailable."""
        error_payload = error.to_dict()
        error_details = error_payload.get("details") if isinstance(error_payload.get("details"), dict) else {}
        candidate_error = error_details.get("candidate_error") if isinstance(error_details.get("candidate_error"), dict) else {}
        warning_transport = {
            key: candidate_error.get(key)
            for key in ("backend_code", "http_status", "request_id")
            if candidate_error.get(key) is not None
        }
        selection: JSONObject = {
            "app_key": app_key,
            "field_id": field.que_id,
            "field_title": field.que_title,
            "record_id": record_id_text,
            "workflow_node_id": workflow_node_id,
            "fields_present": fields_present,
            "keyword": keyword,
            "permission_scope": "applicant_node",
        }
        return {
            "profile": profile,
            "ws_id": session_profile.selected_ws_id,
            "ok": False,
            "status": "failed",
            "error_code": error.error_code,
            "message": error.message,
            "request_route": self._request_route_payload(context),
            "warnings": [
                {
                    "code": error.error_code,
                    "message": error.fix_hint,
                    "kind": kind,
                    "field_id": field.que_id,
                    "field_title": field.que_title,
                    **warning_transport,
                }
            ],
            "output_profile": "normal",
            "data": {
                "items": [],
                "pagination": {"returned_items": 0},
                "selection": selection,
                "scope_source": scope_source,
                "fix_hint": error.fix_hint,
            },
            "details": error_details,
        }

    def _request_route_payload(self, context) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        describe_route = getattr(self.backend, "describe_route", None)
        if callable(describe_route):
            payload = describe_route(context)
            if isinstance(payload, dict):
                return payload
        return {
            "base_url": getattr(context, "base_url", None),
            "qf_version": getattr(context, "qf_version", None),
            "qf_version_source": getattr(context, "qf_version_source", None) or ("context" if getattr(context, "qf_version", None) else "unknown"),
        }

    def _normalize_public_output_profile(self, output_profile: str, *, allow_normalized: bool = False) -> str:
        """执行内部辅助逻辑。"""
        normalized = (output_profile or "normal").strip().lower()
        allowed = {"normal", "verbose"}
        if allow_normalized:
            allowed.add("normalized")
        if normalized not in allowed:
            expected = "normal, verbose, or normalized" if allow_normalized else "normal or verbose"
            raise_tool_error(QingflowApiError.config_error(f"output_profile must be {expected}"))
        return normalized

    def _normalize_record_list_where(self, where: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        normalized: list[JSONObject] = []
        for idx, item in enumerate(where):
            if not isinstance(item, dict):
                raise_tool_error(QingflowApiError.config_error(f"where[{idx}] must be an object"))
            _ensure_allowed_record_list_keys(
                item,
                location=f"where[{idx}]",
                allowed_keys={"field_id", "fieldId", "op", "operator", "value", "values"},
                example="{'field_id': 12, 'op': 'eq', 'value': '进行中'}",
            )
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
            if field_id is None:
                raise_tool_error(QingflowApiError.config_error(f"where[{idx}] requires field_id"))
            payload: JSONObject = {"field_id": field_id}
            op = item.get("op", item.get("operator"))
            if op is not None:
                payload["op"] = op
            if "value" in item:
                payload["value"] = item["value"]
            elif "values" in item:
                payload["value"] = item["values"]
            normalized.append(payload)
        return normalized

    def _normalize_record_list_order_by(self, order_by: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        normalized: list[JSONObject] = []
        for idx, item in enumerate(order_by):
            if not isinstance(item, dict):
                raise_tool_error(QingflowApiError.config_error(f"order_by[{idx}] must be an object"))
            _ensure_allowed_record_list_keys(
                item,
                location=f"order_by[{idx}]",
                allowed_keys={"field_id", "fieldId", "direction", "order"},
                example="{'field_id': 18, 'direction': 'desc'}",
            )
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
            if field_id is None:
                raise_tool_error(QingflowApiError.config_error(f"order_by[{idx}] requires field_id"))
            direction = _normalize_optional_text(item.get("direction", item.get("order"))) or "asc"
            if direction not in {"asc", "desc"}:
                raise_tool_error(QingflowApiError.config_error(f"order_by[{idx}].direction must be asc or desc"))
            normalized.append({"field_id": field_id, "direction": direction})
        return normalized

    def _normalize_record_write_submit_type(self, submit_type: str | int) -> int:
        """执行内部辅助逻辑。"""
        if isinstance(submit_type, int):
            if submit_type in {0, 1}:
                return submit_type
            raise_tool_error(QingflowApiError.config_error("submit_type must be 0, 1, save, or submit"))
        normalized = (submit_type or "submit").strip().lower()
        if normalized == "submit":
            return 1
        if normalized in {"save", "draft"}:
            return 0
        raise_tool_error(QingflowApiError.config_error("submit_type must be save or submit"))

    def _normalize_record_write_clauses(self, clauses: list[JSONObject], *, location: str) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        normalized: list[JSONObject] = []
        for idx, item in enumerate(clauses):
            if not isinstance(item, dict):
                raise_tool_error(QingflowApiError.config_error(f"{location}[{idx}] must be an object"))
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
            if field_id is None:
                raise_tool_error(QingflowApiError.config_error(f"{location}[{idx}] requires field_id"))
            payload: JSONObject = {"field_id": field_id}
            if "value" in item:
                payload["value"] = item["value"]
            elif "values" in item:
                payload["values"] = item["values"]
            else:
                raise_tool_error(QingflowApiError.config_error(f"{location}[{idx}] requires value"))
            normalized.append(payload)
        return normalized

    def _record_write_normalized_payload(
        self,
        *,
        operation: str,
        record_id: int | None,
        record_ids: list[int],
        normalized_answers: list[JSONObject],
        submit_type: int,
        selection: JSONObject | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        payload: JSONObject = {
            "operation": operation,
            "record_id": stringify_backend_id(record_id) if record_id is not None else None,
            "record_ids": [stringify_backend_id(item) for item in record_ids],
            "answers": normalized_answers,
            "submit_type": submit_type,
        }
        if selection:
            payload["selection"] = selection
        return payload

    def _canonicalize_write_scope_selectors(
        self,
        *,
        answers: list[JSONObject],
        fields: JSONObject,
        selector_index: FieldIndex,
    ) -> tuple[list[JSONObject], JSONObject]:
        """执行内部辅助逻辑。"""
        canonical_answers: list[JSONObject] = []
        for item in answers:
            if not isinstance(item, dict):
                raise RecordInputError(
                    message="answer item must be an object",
                    error_code="INVALID_ANSWER_ITEM",
                    fix_hint="Provide each answer as an object with a field selector and value.",
                )
            field = self._resolve_field_from_answer_item(item, selector_index)
            payload = dict(item)
            payload["queId"] = field.que_id
            for key in ("field_id", "fieldId", "que_id", "queTitle", "que_title"):
                payload.pop(key, None)
            canonical_answers.append(payload)
        canonical_fields: JSONObject = {}
        for requested_key, value in fields.items():
            field = self._resolve_field_selector(cast(str | int, requested_key), selector_index, location="fields")
            canonical_fields[str(field.que_id)] = value
        return canonical_answers, canonical_fields

    def _record_write_human_review_payload(self, operation: str, *, enabled: bool) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        if not enabled:
            return None
        return {
            "required": True,
            "operation": operation,
            "message": "Read the current record first and confirm the exact target before applying this high-risk write.",
        }

    def _raise_record_write_permission_error(
        self,
        exc: QingflowApiError,
        *,
        operation: str,
        app_key: str,
        record_id: int | None,
        selection: JSONObject | None,
    ) -> None:
        """执行内部辅助逻辑。"""
        if not _is_record_permission_denied_error(exc):
            raise exc
        raise_tool_error(
            QingflowApiError(
                category="permission",
                message="Direct record edit was blocked because the current user does not have permission to edit this record.",
                backend_code=exc.backend_code,
                request_id=exc.request_id,
                http_status=exc.http_status,
                details={
                    "error_code": "WRITE_PERMISSION_DENIED",
                    "operation": operation,
                    "app_key": app_key,
                    "record_id": record_id,
                    "selection": selection,
                    "fix_hint": (
                        "View visibility only narrows field scope and does not grant edit rights. "
                        "If this is workflow work, prefer task_list -> task_get -> task_action_execute; "
                        "otherwise ask an operator/admin account with record edit permission to perform the update."
                    ),
                },
            )
        )

    def _record_write_blocked_response(
        self,
        raw_preflight: JSONObject,
        *,
        operation: str,
        normalized_payload: JSONObject,
        output_profile: str,
        human_review: bool,
        target_resource: JSONObject,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        plan_data = cast(JSONObject, raw_preflight.get("data", {}))
        validation = cast(JSONObject, plan_data.get("validation", {}))
        field_errors = self._record_write_public_field_errors(plan_data)
        confirmation_requests = cast(list[JSONObject], plan_data.get("confirmation_requests", []))
        resolved_fields = cast(list[JSONObject], plan_data.get("lookup_resolved_fields", []))
        warnings_payload = validation.get("warnings", [])
        warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
        status = "needs_confirmation" if confirmation_requests else "blocked"
        response: JSONObject = {
            "profile": raw_preflight.get("profile"),
            "ws_id": raw_preflight.get("ws_id"),
            "ok": False,
            "status": status,
            "write_executed": False,
            "verification_status": "not_requested",
            "safe_to_retry": True,
            "request_route": raw_preflight.get("request_route"),
            "warnings": warnings,
            "output_profile": output_profile,
            "data": {
                "action": {"operation": operation, "executed": False},
                "resource": target_resource,
                "verification": None,
                "normalized_payload": normalized_payload,
                "blockers": plan_data.get("blockers", []),
                "field_errors": field_errors,
                "confirmation_requests": confirmation_requests,
                "resolved_fields": resolved_fields,
                "recommended_next_actions": cast(list[JSONValue], plan_data.get("recommended_next_actions", [])),
                "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
            },
        }
        if output_profile == "verbose":
            response["data"]["debug"] = {
                "preflight": plan_data,
            }
        return response

    def _record_write_ready_response(
        self,
        raw_preflight: JSONObject,
        *,
        operation: str,
        normalized_payload: JSONObject,
        output_profile: str,
        human_review: bool,
        target_resource: JSONObject,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        plan_data = cast(JSONObject, raw_preflight.get("data", {}))
        validation = cast(JSONObject, plan_data.get("validation", {}))
        resolved_fields = cast(list[JSONObject], plan_data.get("lookup_resolved_fields", []))
        warnings_payload = validation.get("warnings", [])
        warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
        response: JSONObject = {
            "profile": raw_preflight.get("profile"),
            "ws_id": raw_preflight.get("ws_id"),
            "ok": True,
            "status": "ready",
            "write_executed": False,
            "verification_status": "not_requested",
            "safe_to_retry": True,
            "request_route": raw_preflight.get("request_route"),
            "warnings": warnings,
            "output_profile": output_profile,
            "data": {
                "action": {"operation": operation, "executed": False},
                "resource": target_resource,
                "verification": None,
                "normalized_payload": normalized_payload,
                "blockers": [],
                "field_errors": [],
                "confirmation_requests": [],
                "resolved_fields": resolved_fields,
                "recommended_next_actions": cast(list[JSONValue], plan_data.get("recommended_next_actions", [])),
                "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
            },
        }
        if output_profile == "verbose":
            response["data"]["debug"] = {
                "preflight": plan_data,
            }
        return response

    def _record_write_apply_response(
        self,
        raw_apply: JSONObject,
        *,
        operation: str,
        normalized_payload: JSONObject,
        output_profile: str,
        human_review: bool,
        preflight: JSONObject | None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        preflight_data = cast(JSONObject, preflight.get("data", {})) if isinstance(preflight, dict) else {}
        validation = cast(JSONObject, preflight_data.get("validation", {}))
        warnings_payload = validation.get("warnings", [])
        warnings = [{"code": "PREFLIGHT_WARNING", "message": str(item)} for item in warnings_payload] if isinstance(warnings_payload, list) else []
        verification = cast(JSONObject, raw_apply.get("verification", {})) if isinstance(raw_apply.get("verification"), dict) else {}
        verification_warnings = verification.get("warnings", [])
        resolved_fields = cast(list[JSONObject], preflight_data.get("lookup_resolved_fields", []))
        if isinstance(verification_warnings, list):
            warnings.extend(cast(list[JSONObject], [item for item in verification_warnings if isinstance(item, dict)]))
        resource = _public_record_resource(raw_apply.get("resource"))
        record_id = _public_record_id_text(resource.get("record_id")) if isinstance(resource, dict) else None
        apply_id = _public_record_id_text(resource.get("apply_id")) if isinstance(resource, dict) else None
        if record_id is None:
            record_id = _public_record_id_text(raw_apply.get("record_id"))
        if apply_id is None:
            apply_id = _public_record_id_text(raw_apply.get("apply_id"))
        if apply_id is None:
            apply_id = record_id
        if record_id is None:
            record_id = apply_id
        write_executed = True
        verification_requested = (
            raw_apply.get("verify_write") is True
            or raw_apply.get("write_verified") is not None
            or isinstance(raw_apply.get("verification"), dict)
        )
        if verification_requested:
            verification_status = "verified" if bool(verification.get("verified")) else "failed"
        else:
            verification_status = "not_requested"
        raw_status = _normalize_optional_text(raw_apply.get("status"))
        response_status = "verification_failed" if verification_status == "failed" else "success"
        if not bool(raw_apply.get("ok", True)) and verification_status != "failed":
            response_status = raw_status or "failed"
        update_route = raw_apply.get("update_route") if isinstance(raw_apply.get("update_route"), dict) else None
        tried_routes = raw_apply.get("tried_routes") if isinstance(raw_apply.get("tried_routes"), list) else []
        expose_tried_routes = output_profile == "verbose" or response_status != "success"
        response: JSONObject = {
            "profile": raw_apply.get("profile"),
            "ws_id": raw_apply.get("ws_id"),
            "ok": True if verification_status == "failed" and write_executed else bool(raw_apply.get("ok", True)),
            "status": response_status,
            "write_executed": write_executed,
            "verification_status": verification_status,
            "safe_to_retry": False,
            "request_route": raw_apply.get("request_route"),
            "warnings": warnings,
            "output_profile": output_profile,
            "update_route": update_route,
            "data": {
                "action": {"operation": operation, "executed": True},
                "resource": resource,
                "verification": raw_apply.get("verification"),
                "normalized_payload": normalized_payload,
                "blockers": [],
                "field_errors": [],
                "confirmation_requests": [],
                "resolved_fields": resolved_fields,
                "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
                "update_route": update_route,
            },
        }
        if expose_tried_routes:
            response["tried_routes"] = tried_routes
            response["data"]["tried_routes"] = tried_routes
        if record_id is not None:
            response["record_id"] = record_id
        if apply_id is not None:
            response["apply_id"] = apply_id
        if output_profile == "verbose":
            debug: JSONObject = {
                "legacy_result": raw_apply.get("result"),
                "status": raw_apply.get("status"),
                "write_verified": raw_apply.get("write_verified"),
            }
            if preflight_data:
                debug["preflight"] = preflight_data
            response["data"]["debug"] = debug
        return response

    def _record_write_exception_response(
        self,
        exc: QingflowApiError | RuntimeError,
        *,
        operation: str,
        profile: str,
        app_key: str,
        record_id: Any | None,
        output_profile: str,
        human_review: bool,
        write_executed: bool = True,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        error_payload: JSONObject = {
            "error_code": "RECORD_WRITE_EXECUTION_FAILED",
            "message": str(exc),
        }
        request_route: JSONObject | None = None
        if isinstance(exc, QingflowApiError):
            error_payload["message"] = exc.message
            if exc.backend_code is not None:
                error_payload["backend_code"] = exc.backend_code
            if exc.request_id is not None:
                error_payload["request_id"] = exc.request_id
        else:
            try:
                parsed = json.loads(str(exc))
            except json.JSONDecodeError:
                parsed = None
            if isinstance(parsed, dict):
                parsed_details = parsed.get("details")
                details_payload = cast(JSONObject, parsed_details) if isinstance(parsed_details, dict) else None
                error_payload["error_code"] = (
                    parsed.get("error_code")
                    or (details_payload.get("error_code") if details_payload is not None else None)
                    or error_payload["error_code"]
                )
                error_payload["message"] = parsed.get("message") or error_payload["message"]
                if parsed.get("backend_code") is not None:
                    error_payload["backend_code"] = parsed.get("backend_code")
                if parsed.get("request_id") is not None:
                    error_payload["request_id"] = parsed.get("request_id")
                if isinstance(parsed.get("request_route"), dict):
                    request_route = cast(JSONObject, parsed.get("request_route"))
                elif details_payload is not None and isinstance(details_payload.get("request_route"), dict):
                    request_route = cast(JSONObject, details_payload.get("request_route"))
        response: JSONObject = {
            "profile": profile,
            "ws_id": None,
            "ok": False,
            "status": "failed",
            "write_executed": write_executed,
            "verification_status": "failed" if write_executed else "not_requested",
            "safe_to_retry": not write_executed,
            "request_route": request_route,
            "warnings": [],
            "output_profile": output_profile,
            "data": {
                "action": {"operation": operation, "executed": write_executed},
                "resource": {"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
                "verification": None,
                "normalized_payload": None,
                "blockers": [],
                "field_errors": [],
                "confirmation_requests": [],
                "resolved_fields": [],
                "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
                "error": error_payload,
            },
        }
        if output_profile == "verbose":
            response["data"]["debug"] = {"exception": error_payload}
        return response

    def _record_write_public_field_errors(self, plan_data: JSONObject) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        existing = plan_data.get("field_errors")
        if isinstance(existing, list):
            return [cast(JSONObject, item) for item in existing if isinstance(item, dict)]
        validation = cast(JSONObject, plan_data.get("validation", {}))
        invalid_fields = cast(list[JSONObject], validation.get("invalid_fields", []))
        missing_required_fields = cast(list[JSONObject], validation.get("missing_required_fields", []))
        readonly_or_system_fields = cast(list[JSONObject], validation.get("readonly_or_system_fields", []))
        return self._record_write_field_errors(
            invalid_fields=invalid_fields,
            missing_required_fields=missing_required_fields,
            readonly_or_system_fields=readonly_or_system_fields,
        )

    def _record_write_field_errors(
        self,
        *,
        invalid_fields: list[JSONObject],
        missing_required_fields: list[JSONObject],
        readonly_or_system_fields: list[JSONObject],
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        errors: list[JSONObject] = []
        for item in invalid_fields:
            payload: JSONObject = {
                "error_code": item.get("error_code") or "INVALID_FIELD_VALUE",
                "message": item.get("message") or "invalid field value",
                "field": item.get("field"),
                "location": item.get("location"),
                "expected_format": item.get("expected_format"),
                "received_value": item.get("received_value"),
            }
            if "fix_hint" in item:
                payload["fix_hint"] = item.get("fix_hint")
            if "details" in item:
                payload["details"] = item.get("details")
            errors.append(payload)
        for item in missing_required_fields:
            payload: JSONObject = {
                "error_code": "MISSING_REQUIRED_FIELD",
                "message": item.get("reason") or "required field not provided",
                "field": {
                    "que_id": item.get("que_id"),
                    "que_title": item.get("que_title"),
                    "que_type": item.get("que_type"),
                },
                "location": item.get("location") or "fields",
                "expected_format": None,
                "received_value": None,
            }
            if "fix_hint" in item:
                payload["fix_hint"] = item.get("fix_hint")
            activation_sources = item.get("activation_sources")
            requirement_mode = item.get("requirement_mode")
            if isinstance(activation_sources, list) or requirement_mode is not None:
                payload["details"] = {
                    "activation_sources": activation_sources if isinstance(activation_sources, list) else [],
                    "requirement_mode": requirement_mode,
                }
            errors.append(payload)
        for item in readonly_or_system_fields:
            reason_code = _normalize_optional_text(item.get("reason_code")) or "readonly"
            if reason_code == "view_readonly":
                error_code = "VIEW_FIELD_READONLY"
                message = "field is not writable in the selected view"
            elif reason_code == "system":
                error_code = "READONLY_OR_SYSTEM_FIELD"
                message = "field is system-managed"
            else:
                error_code = "READONLY_OR_SYSTEM_FIELD"
                message = "field is readonly or system-managed"
            errors.append(
                {
                    "error_code": error_code,
                    "message": message,
                    "field": {
                        "que_id": item.get("que_id"),
                        "que_title": item.get("que_title"),
                        "que_type": item.get("que_type"),
                        "readonly": item.get("readonly"),
                        "system": item.get("system"),
                    },
                    "location": item.get("source") or "fields",
                    "expected_format": None,
                    "received_value": item.get("requested"),
                }
            )
        return errors

    def _record_delete_many(
        self,
        *,
        profile: str,
        app_key: str,
        record_ids: list[int],
        list_type: int = DEFAULT_RECORD_LIST_TYPE,
        tool_name: str = "删除记录",
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        normalized_ids = [item for item in record_ids if isinstance(item, int) and item > 0]
        if not normalized_ids:
            raise_tool_error(QingflowApiError.config_error("record_ids must contain at least one positive id"))

        def runner(session_profile, context):
            result = self.backend.request(
                "DELETE",
                context,
                f"/app/{app_key}/apply",
                json_body={"type": list_type, "applyIds": normalized_ids},
            )
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "request_route": self._request_route_payload(context),
                "result": result,
                "resource": {"type": "record", "apply_ids": normalized_ids, "list_type": list_type},
                "ok": True,
            }

        return self._run_record_tool(profile, runner, tool_name=tool_name)

    def _resolve_field_selector(self, selector: str | int | None, index: FieldIndex, *, location: str) -> FormField:
        """执行内部辅助逻辑。"""
        if selector is None:
            raise RecordInputError(
                message=f"{location} requires a field selector",
                error_code="MISSING_FIELD_SELECTOR",
                fix_hint="Pass an exact field title or queId.",
            )
        requested = str(selector).strip()
        requested_key = _normalize_field_lookup_key(requested)
        if not requested:
            raise RecordInputError(
                message=f"{location} contains an empty field selector",
                error_code="EMPTY_FIELD_SELECTOR",
                fix_hint="Pass an exact field title or queId.",
            )
        if requested.isdigit():
            field = index.by_id.get(str(int(requested)))
            if field is not None:
                return field
            leaf_matches = index.subtable_leaf_by_id.get(str(int(requested)), [])
            if leaf_matches:
                raise self._subtable_leaf_field_error(location=location, requested=requested, matches=leaf_matches, matched_via="id")
            raise RecordInputError(
                message=f"{location} references unknown queId '{requested}'",
                error_code="FIELD_NOT_FOUND",
                fix_hint="Use record_browse_schema_get to confirm the exact field_id.",
                details={"location": location, "requested": requested, "requested_key": requested_key},
            )
        matches = index.by_title.get(requested_key, [])
        if len(matches) == 1:
            return matches[0]
        if len(matches) > 1:
            raise RecordInputError(
                message=f"{location} field '{requested}' is ambiguous",
                error_code="AMBIGUOUS_FIELD",
                fix_hint="Use numeric queId, or inspect record_browse_schema_get output to disambiguate the field.",
                details={
                    "location": location,
                    "requested": requested,
                    "requested_key": requested_key,
                    "matched_via": "title",
                    "candidates": [_field_ref_payload(item) for item in matches],
                },
            )
        leaf_matches = index.subtable_leaf_by_title.get(requested_key, [])
        if leaf_matches:
            raise self._subtable_leaf_field_error(location=location, requested=requested, matches=leaf_matches, matched_via="title")
        alias_matches = index.by_alias.get(requested_key, [])
        if len(alias_matches) == 1:
            return alias_matches[0]
        if len(alias_matches) > 1:
            raise RecordInputError(
                message=f"{location} field '{requested}' is ambiguous",
                error_code="AMBIGUOUS_FIELD",
                fix_hint="Use a more specific field title, or inspect the relevant schema-get output to disambiguate the field.",
                details={
                    "location": location,
                    "requested": requested,
                    "requested_key": requested_key,
                    "matched_via": "alias",
                    "candidates": [_field_ref_payload(item) for item in alias_matches],
                },
            )
        leaf_alias_matches = index.subtable_leaf_by_alias.get(requested_key, [])
        if leaf_alias_matches:
            raise self._subtable_leaf_field_error(location=location, requested=requested, matches=leaf_alias_matches, matched_via="alias")
        raise RecordInputError(
            message=f"{location} cannot resolve field '{requested}'",
            error_code="FIELD_NOT_FOUND",
            fix_hint="Use the relevant schema-get tool to confirm the exact field title or field_id.",
            details={
                "location": location,
                "requested": requested,
                "requested_key": requested_key,
                "suggestions": self._score_field_matches(requested, index, fuzzy=True, top_k=5),
            },
        )

    def _subtable_leaf_field_error(
        self,
        *,
        location: str,
        requested: str,
        matches: list[SubtableLeafRef],
        matched_via: str,
    ) -> RecordInputError:
        """执行内部辅助逻辑。"""
        if len(matches) > 1:
            return RecordInputError(
                message=f"{location} field '{requested}' matches subtable leaf fields under multiple parent tables",
                error_code="AMBIGUOUS_SUBTABLE_LEAF_FIELD",
                fix_hint="Use the parent subtable field with a row array, or inspect record_insert_schema_get / record_update_schema_get to choose the correct parent table.",
                details={
                    "location": location,
                    "requested": requested,
                    "matched_via": matched_via,
                    "candidates": [
                        {
                            "parent_field": _field_ref_payload(item.parent_field),
                            "subfield": _field_ref_payload(item.field),
                        }
                        for item in matches
                    ],
                },
            )
        match = matches[0]
        return RecordInputError(
            message=(
                f"{location} field '{requested}' is a subtable leaf field and cannot be written at the top level; "
                f"use parent field '{match.parent_field.que_title}' with a row array instead"
            ),
            error_code="SUBTABLE_LEAF_REQUIRES_PARENT_ROWS",
            fix_hint=(
                f"Write subtable leaf '{match.field.que_title}' under parent field '{match.parent_field.que_title}', "
                "for example {'"
                + match.parent_field.que_title
                + "': [{'"
                + match.field.que_title
                + "': '值'}]}."
            ),
            details={
                "location": location,
                "requested": requested,
                "matched_via": matched_via,
                "field": _field_ref_payload(match.field),
                "parent_field": _field_ref_payload(match.parent_field),
                "expected_format": _write_format_for_field(match.parent_field),
            },
        )

    def _resolve_field_from_answer_item(self, item: JSONObject, index: FieldIndex) -> FormField:
        """执行内部辅助逻辑。"""
        for key in ("queId", "que_id", "queTitle", "que_title", "field_id", "fieldId"):
            if key in item:
                return self._resolve_field_selector(cast(str | int, item[key]), index, location="answers")
        raise RecordInputError(
            message="answer item requires queId/que_id, queTitle/que_title, or field_id",
            error_code="MISSING_FIELD_SELECTOR",
            fix_hint="Provide a field selector in each answer item.",
        )

    def _resolve_select_columns(
        self,
        selectors: list[str | int],
        index: FieldIndex,
        *,
        max_columns: int | None,
        default_limit: int,
    ) -> list[FormField]:
        """执行内部辅助逻辑。"""
        if not selectors:
            raise_tool_error(QingflowApiError.config_error("select_columns is required"))
        limit = _bounded_column_limit(max_columns, default_limit=default_limit, hard_limit=default_limit)
        fields: list[FormField] = []
        seen: set[int] = set()
        for selector in selectors:
            field = self._resolve_field_selector(selector, index, location="select_columns")
            if field.que_id in seen:
                continue
            fields.append(field)
            seen.add(field.que_id)
            if len(fields) >= limit:
                break
        return fields

    def _resolve_record_access_columns(
        self,
        selectors: list[int],
        index: FieldIndex,
        *,
        view_route: AccessibleViewRoute,
    ) -> list[FormField]:
        """Resolve record_access columns against the selected view's baseInfo schema."""
        if not selectors:
            raise_tool_error(QingflowApiError.config_error("columns is required"))
        fields: list[FormField] = []
        seen: set[int] = set()
        for selector in selectors:
            try:
                field = self._resolve_field_selector(selector, index, location="record_access.columns")
            except RecordInputError as exc:
                if exc.error_code == "FIELD_NOT_FOUND":
                    raise RecordInputError(
                        message=(
                            f"record_access column field_id '{selector}' is not in the selected view schema "
                            f"({view_route.view_id})."
                        ),
                        error_code="FIELD_NOT_IN_VIEW_SCHEMA",
                        fix_hint="Call record_browse_schema_get for this exact view_id and pass only field_id values from its fields[].",
                        details={
                            "location": "record_access.columns",
                            "requested": selector,
                            "view_id": view_route.view_id,
                            "view_name": view_route.name,
                        },
                    ) from exc
                raise
            if field.que_id in seen:
                continue
            fields.append(field)
            seen.add(field.que_id)
        return fields

    def _derive_record_list_fields_from_index(self, index: FieldIndex) -> list[FormField]:
        fields = [
            field
            for field in index.by_id.values()
            if field.que_type not in LAYOUT_ONLY_QUE_TYPES
        ][:MAX_LIST_COLUMN_LIMIT]
        if not fields:
            raise_tool_error(
                QingflowApiError.config_error(
                    "record_list could not determine readable columns for the selected view"
                )
            )
        return fields

    def _resolve_record_list_columns(
        self,
        selectors: list[int],
        index: FieldIndex,
        *,
        view_route: AccessibleViewRoute,
    ) -> list[FormField]:
        if not selectors:
            raise_tool_error(QingflowApiError.config_error("columns is required"))
        fields: list[FormField] = []
        seen: set[int] = set()
        for selector in selectors:
            try:
                field = self._resolve_field_selector(selector, index, location="record_list.columns")
            except RecordInputError as exc:
                if exc.error_code == "FIELD_NOT_FOUND":
                    raise self._record_list_field_not_in_view_error(
                        exc,
                        location="record_list.columns",
                        error_code="FIELD_NOT_IN_VIEW_SCHEMA",
                        view_route=view_route,
                    ) from exc
                raise
            if field.que_id in seen:
                continue
            fields.append(field)
            seen.add(field.que_id)
        return fields

    def _resolve_record_list_query_fields(
        self,
        selectors: list[int],
        index: FieldIndex,
        *,
        view_route: AccessibleViewRoute,
    ) -> list[int]:
        resolved: list[int] = []
        seen: set[int] = set()
        for selector in selectors:
            try:
                field = self._resolve_field_selector(selector, index, location="record_list.query_fields")
            except RecordInputError as exc:
                if exc.error_code == "FIELD_NOT_FOUND":
                    raise self._record_list_field_not_in_view_error(
                        exc,
                        location="record_list.query_fields",
                        error_code="QUERY_FIELD_NOT_IN_VIEW_SCHEMA",
                        view_route=view_route,
                    ) from exc
                raise
            if field.que_id in seen:
                continue
            resolved.append(field.que_id)
            seen.add(field.que_id)
        if len(resolved) > BACKEND_LIST_SEARCH_FIELD_LIMIT:
            raise RecordInputError(
                message=(
                    f"record_list query_fields supports at most {BACKEND_LIST_SEARCH_FIELD_LIMIT} fields."
                ),
                error_code="QUERY_FIELDS_TOO_MANY",
                fix_hint="Narrow query_fields to the most likely title/name/customer/number fields, or omit query_fields to use the backend default search scope.",
                details={
                    "location": "record_list.query_fields",
                    "max_fields": BACKEND_LIST_SEARCH_FIELD_LIMIT,
                    "received": len(resolved),
                },
            )
        return resolved

    def _resolve_record_list_match_rules(
        self,
        context,  # type: ignore[no-untyped-def]
        filters: list[JSONObject],
        index: FieldIndex,
        *,
        view_route: AccessibleViewRoute,
    ) -> list[JSONObject]:
        try:
            return self._resolve_match_rules(context, filters, index)
        except RecordInputError as exc:
            if exc.error_code == "FIELD_NOT_FOUND":
                raise self._record_list_field_not_in_view_error(
                    exc,
                    location="record_list.where",
                    error_code="FILTER_FIELD_NOT_IN_VIEW_SCHEMA",
                    view_route=view_route,
                ) from exc
            raise

    def _resolve_record_list_sort_rules(
        self,
        sorts: list[JSONObject],
        index: FieldIndex,
        *,
        view_route: AccessibleViewRoute,
    ) -> list[JSONObject]:
        try:
            return self._resolve_sorts(sorts, index)
        except RecordInputError as exc:
            if exc.error_code == "FIELD_NOT_FOUND":
                raise self._record_list_field_not_in_view_error(
                    exc,
                    location="record_list.order_by",
                    error_code="SORT_FIELD_NOT_IN_VIEW_SCHEMA",
                    view_route=view_route,
                ) from exc
            raise

    def _record_list_field_not_in_view_error(
        self,
        exc: RecordInputError,
        *,
        location: str,
        error_code: str,
        view_route: AccessibleViewRoute,
    ) -> RecordInputError:
        details = exc.details if isinstance(exc.details, dict) else {}
        requested = details.get("requested")
        return RecordInputError(
            message=(
                f"{location} field_id '{requested}' is not in the selected view schema "
                f"({view_route.view_id})."
            ),
            error_code=error_code,
            fix_hint="Call record_browse_schema_get for this exact view_id and pass only field_id values from its fields[].",
            details={
                "location": location,
                "requested": requested,
                "view_id": view_route.view_id,
                "view_name": view_route.name,
            },
        )

    def _resolve_summary_preview_fields(
        self,
        selectors: list[str | int],
        index: FieldIndex,
        amount_field: FormField | None,
        time_field: FormField | None,
        *,
        max_columns: int | None,
    ) -> list[FormField]:
        """执行内部辅助逻辑。"""
        if selectors:
            return self._resolve_select_columns(
                selectors,
                index,
                max_columns=max_columns,
                default_limit=MAX_SUMMARY_PREVIEW_COLUMN_LIMIT,
            )
        limit = _bounded_column_limit(
            max_columns,
            default_limit=MAX_SUMMARY_PREVIEW_COLUMN_LIMIT,
            hard_limit=MAX_SUMMARY_PREVIEW_COLUMN_LIMIT,
        )
        candidates: list[FormField] = []
        title_candidate = _pick_title_field(index)
        for field in (title_candidate, amount_field, time_field):
            if field is None or any(existing.que_id == field.que_id for existing in candidates):
                continue
            candidates.append(field)
        if not candidates:
            candidates = list(index.by_id.values())[:limit]
        return candidates[:limit]

    def _resolve_match_rules(self, context, filters: list[JSONObject], index: FieldIndex) -> list[JSONObject]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        rules: list[JSONObject] = []
        for item in filters:
            if not isinstance(item, dict):
                continue
            selector = _extract_filter_selector(item)
            field = self._resolve_field_selector(cast(str | int | None, selector), index, location="filters") if selector is not None else None
            if field is not None and field.que_type in DEPARTMENT_QUE_TYPES:
                department_rule = self._build_department_filter_rule(context, field, item)
                if department_rule:
                    rules.append(department_rule)
                    continue
            rule: JSONObject = {}
            if field is not None:
                rule["queId"] = field.que_id
                if field.que_type is not None:
                    rule["queType"] = field.que_type
            for source, target in (
                ("search_key", "searchKey"),
                ("searchKey", "searchKey"),
                ("search_keys", "searchKeys"),
                ("searchKeys", "searchKeys"),
                ("min_value", "minValue"),
                ("minValue", "minValue"),
                ("max_value", "maxValue"),
                ("maxValue", "maxValue"),
                ("scope", "scope"),
                ("search_options", "searchOptions"),
                ("searchOptions", "searchOptions"),
                ("search_user_ids", "searchUserIds"),
                ("searchUserIds", "searchUserIds"),
            ):
                if source in item:
                    rule[target] = item[source]
            operator = _stringify_json(item.get("operator", item.get("op"))).strip().lower()
            if "searchKey" not in rule and "searchKeys" not in rule and "searchOptions" not in rule and "searchUserIds" not in rule and "minValue" not in rule and "maxValue" not in rule:
                value = item.get("value", item.get("values"))
                if operator in {"gte", "gt"} and value is not None:
                    rule["minValue"] = _stringify_json(value)
                elif operator in {"lte", "lt"} and value is not None:
                    rule["maxValue"] = _stringify_json(value)
                elif operator == "between":
                    lower, upper = _coerce_filter_range(value)
                    if lower is not None:
                        rule["minValue"] = lower
                    if upper is not None:
                        rule["maxValue"] = upper
                elif value is not None:
                    if field is not None and field.que_type in SINGLE_SELECT_QUE_TYPES | MULTI_SELECT_QUE_TYPES:
                        option_ids = _normalize_option_filter_ids(value)
                        if option_ids:
                            rule["searchOptions"] = option_ids
                        elif isinstance(value, list):
                            rule["searchKeys"] = [_stringify_json(entry) for entry in value]
                        else:
                            rule["searchKey"] = _stringify_json(value)
                    elif field is not None and field.que_type in MEMBER_QUE_TYPES:
                        member_ids = _normalize_member_filter_ids(value)
                        if member_ids:
                            rule["searchUids"] = member_ids
                    elif isinstance(value, list):
                        rule["searchKeys"] = [_stringify_json(entry) for entry in value]
                    else:
                        rule["searchKey"] = _stringify_json(value)
            if rule:
                rules.append(rule)
        return rules

    def _build_department_filter_rule(self, context, field: FormField, item: JSONObject) -> JSONObject | None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        raw_value: JSONValue | None = None
        for key in ("search_options", "searchOptions", "search_keys", "searchKeys", "search_key", "searchKey", "value", "values"):
            if key in item:
                raw_value = cast(JSONValue, item[key])
                break
        if raw_value is None:
            return None
        details = self._resolve_department_filter_details(context, raw_value)
        if not details:
            return None
        judge_type = JUDGE_EQUAL if len(details) == 1 else JUDGE_EQUAL_ANY
        return _department_filter_rule(field.que_id, field.que_type, details, judge_type=judge_type)

    def _resolve_department_filter_details(self, context, raw_value: JSONValue) -> list[JSONObject]:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        values = raw_value if isinstance(raw_value, list) else [raw_value]
        details: list[JSONObject] = []
        seen: set[int] = set()
        for item in _expand_values(values):
            detail = self._resolve_department_filter_detail(context, item)
            dept_id = _coerce_count(detail.get("id"))
            if dept_id is None or dept_id in seen:
                continue
            seen.add(dept_id)
            details.append(detail)
        return details

    def _resolve_department_filter_detail(self, context, raw_value: JSONValue) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        if isinstance(raw_value, dict):
            dept_id = _coerce_count(raw_value.get("id", raw_value.get("deptId")))
            if dept_id is not None:
                return {
                    "id": dept_id,
                    "value": _stringify_json(raw_value.get("value", raw_value.get("name", raw_value.get("deptName", dept_id)))),
                }
            keyword = _normalize_optional_text(raw_value.get("value", raw_value.get("name", raw_value.get("deptName"))))
            if keyword:
                return self._lookup_department_filter_detail(context, keyword)
            raise RecordInputError(
                message="department filters require id/deptId or a department name",
                error_code="INVALID_DEPARTMENT_FILTER",
                fix_hint="Pass department filters like {'field': '所在部门', 'value': {'id': 11, 'value': '示例部门'}} or use an exact department name.",
                details={"received_value": raw_value},
            )
        dept_id = _coerce_count(raw_value)
        if dept_id is not None:
            return {"id": dept_id, "value": str(dept_id)}
        keyword = _normalize_optional_text(raw_value)
        if keyword is None:
            raise RecordInputError(
                message="department filters require a non-empty department selector",
                error_code="INVALID_DEPARTMENT_FILTER",
                fix_hint="Pass a numeric dept id, a department object, or an exact department name.",
                details={"received_value": raw_value},
            )
        return self._lookup_department_filter_detail(context, keyword)

    def _lookup_department_filter_detail(self, context, keyword: str) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        return self._lookup_department_detail(context, keyword, purpose="filter")

    def _candidate_lookup_sample(self, candidates: list[JSONObject], *, kind: str) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        sample: list[JSONObject] = []
        for item in candidates[:10]:
            if kind == "relation":
                payload = {
                    "apply_id": item.get("apply_id"),
                    "label": item.get("label"),
                    "subtitle": item.get("subtitle"),
                    "fields": item.get("fields"),
                }
            else:
                payload = {
                    "value": item.get("value"),
                }
            if kind == "member":
                if item.get("id") is not None:
                    payload["id"] = item.get("id")
                if item.get("userId") is not None:
                    payload["userId"] = item.get("userId")
                if item.get("email") is not None:
                    payload["email"] = item.get("email")
            elif kind == "department":
                if item.get("id") is not None:
                    payload["id"] = item.get("id")
            sample.append(payload)
        return sample

    def _raise_candidate_lookup_failed(
        self,
        *,
        kind: str,
        field: FormField,
        value: JSONValue,
        error: QingflowApiError,
    ) -> None:
        """执行内部辅助逻辑。"""
        field_kind = "member" if kind == "member" else "department"
        raise RecordInputError(
            message=f"{field_kind} candidates for field '{field.que_title}' could not be loaded",
            error_code=f"{kind.upper()}_CANDIDATE_LOOKUP_FAILED",
            fix_hint=(
                f"Run record_{field_kind}_candidates again after the backend error is resolved, "
                "then choose one returned item exactly."
            ),
            details={
                "field": _field_ref_payload(field),
                "received_value": value,
                "candidate_error": error.to_dict(),
            },
        )

    def _candidate_lookup_error(
        self,
        *,
        kind: str,
        field: FormField,
        value: JSONValue,
        error: QingflowApiError,
    ) -> RecordInputError:
        """Build the standard candidate lookup failure without raising it."""
        field_kind = "member" if kind == "member" else "department"
        return RecordInputError(
            message=f"{field_kind} candidates for field '{field.que_title}' could not be loaded",
            error_code=f"{kind.upper()}_CANDIDATE_LOOKUP_FAILED",
            fix_hint=(
                f"Run record_{field_kind}_candidates again after the backend error is resolved, "
                "then choose one returned item exactly."
            ),
            details={
                "field": _field_ref_payload(field),
                "received_value": value,
                "candidate_error": error.to_dict(),
            },
        )

    def _candidate_keyword_from_value(
        self,
        value: JSONValue,
        *,
        text_keys: tuple[str, ...],
        fallback_id_keys: tuple[str, ...] = (),
    ) -> str:
        """执行内部辅助逻辑。"""
        if isinstance(value, dict):
            for key in text_keys:
                text = _normalize_optional_text(value.get(key))
                if text:
                    return text
            for key in fallback_id_keys:
                text = _normalize_optional_text(value.get(key))
                if text:
                    return text
            return ""
        text = _normalize_optional_text(value)
        if text is None:
            return ""
        return text

    def _resolve_member_candidates_backend(
        self,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        *,
        keyword: str,
        state: LookupResolutionState,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
        use_external = bool(
            isinstance(scope.get("externalMemberList"), list) and scope.get("externalMemberList")
            or isinstance(scope.get("externalDepartList"), list) and scope.get("externalDepartList")
        )
        payload = self._build_lookup_scope_payload(state, keyword=keyword, que_id=field.que_id)
        route = f"/data/que/{field.que_id}/external/memberSelectScope" if use_external else f"/data/que/{field.que_id}/memberSelectScope"
        try:
            result = self.backend.request("POST", context, route, json_body=payload)
        except QingflowApiError as exc:
            if exc.category == "not_supported":
                raise RecordInputError(
                    message=f"member auto resolution for field '{field.que_title}' is unsupported in the current candidate scope",
                    error_code="MEMBER_AUTO_RESOLUTION_UNSUPPORTED",
                    fix_hint="Retry with an explicit member id/userId/email object for this field.",
                    details={"field": _field_ref_payload(field), "received_value": keyword, "candidate_error": exc.to_dict()},
                )
            self._raise_candidate_lookup_failed(kind="member", field=field, value=keyword, error=exc)
        return self._normalize_backend_member_candidates(result, external=use_external)

    def _member_confirmation_candidates(self, candidates: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        payloads: list[JSONObject] = []
        for candidate in candidates[:LOOKUP_CONFIRMATION_CANDIDATE_LIMIT]:
            source_labels = []
            for source in cast(list[JSONValue], candidate.get("sources") or []):
                if not isinstance(source, dict):
                    continue
                source_value = _normalize_optional_text(source.get("value"))
                if source_value:
                    source_labels.append(source_value)
            subtitle_parts = []
            if source_labels:
                subtitle_parts.append(" / ".join(source_labels))
            email = _normalize_optional_text(candidate.get("email"))
            if email:
                subtitle_parts.append(email)
            payload: JSONObject = {
                "id": candidate.get("id"),
                "userId": candidate.get("userId"),
                "email": candidate.get("email"),
                "label": candidate.get("value"),
                "subtitle": " | ".join(part for part in subtitle_parts if part),
                "score": candidate.get("score"),
                "match_reason": candidate.get("match_reason"),
            }
            payloads.append(payload)
        return payloads

    def _member_value_from_selector(
        self,
        profile: str,
        context,
        field: FormField,
        value: JSONValue,
        *,
        resolution_state: LookupResolutionState | None = None,
        location: str,
    ) -> JSONObject | None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        explicit_member_id: int | None = None
        explicit_user_id: str | None = None
        explicit_email: str | None = None
        explicit_label: str | None = None
        if isinstance(value, dict):
            explicit_member_id = _coerce_count(value.get("id", value.get("uid")))
            explicit_user_id = _normalize_optional_text(value.get("userId"))
            explicit_email = _normalize_optional_text(value.get("email"))
            explicit_label = _normalize_optional_text(value.get("value", value.get("name", value.get("userName"))))
        else:
            explicit_member_id = _coerce_count(value)
            if explicit_member_id is None:
                explicit_label = _normalize_optional_text(value)
        has_explicit_selector = bool(explicit_member_id is not None or explicit_user_id or explicit_email)
        keyword = explicit_email or explicit_user_id or explicit_label or self._candidate_keyword_from_value(
            value,
            text_keys=("value", "name", "userName", "email", "userId"),
            fallback_id_keys=("id", "uid"),
        )
        if not keyword:
            raise RecordInputError(
                message=f"member value for field '{field.que_title}' requires a non-empty selector",
                error_code="INVALID_MEMBER_VALUE",
                fix_hint="Pass a member id, userId, email, or a non-empty name string.",
                details={"field": _field_ref_payload(field), "location": location, "received_value": value},
            )
        if resolution_state is None:
            if has_explicit_selector:
                payload = _member_value(profile, value)
                self._append_lookup_resolution(
                    resolution_state,
                    field=field,
                    kind="member",
                    input_value=value,
                    resolved_to={
                        "id": payload.get("id"),
                        "userId": payload.get("userId"),
                        "email": payload.get("email"),
                        "value": payload.get("value"),
                    },
                    method="explicit_selector",
                    confidence=1.0,
                )
                return payload
            return _member_value(profile, value)
        if has_explicit_selector:
            payload = _member_value(profile, value)
            self._append_lookup_resolution(
                resolution_state,
                field=field,
                kind="member",
                input_value=value,
                resolved_to={
                    "id": payload.get("id"),
                    "userId": payload.get("userId"),
                    "email": payload.get("email"),
                    "value": payload.get("value"),
                },
                method="explicit_selector",
                confidence=1.0,
            )
            return payload
        if self._lookup_context_is_incomplete(resolution_state):
            self._raise_lookup_context_unavailable(
                field=field,
                kind="member",
                value=value,
                location=location,
                explicit_fix_hint="Retry with an explicit member id/userId/email object for this field.",
            )
        candidates = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=resolution_state)
        ranked = self._rank_member_candidates(keyword, candidates)
        resolved = self._resolve_ranked_lookup_candidate(ranked)
        if resolved is not None:
            payload: JSONObject = {
                "value": _stringify_json(resolved.get("value")),
            }
            member_id = _coerce_count(resolved.get("id"))
            user_id = _normalize_optional_text(resolved.get("userId"))
            email = _normalize_optional_text(resolved.get("email"))
            if member_id is not None:
                payload["id"] = member_id
            elif user_id:
                payload["userId"] = user_id
            if email:
                payload["email"] = email
            self._append_lookup_resolution(
                resolution_state,
                field=field,
                kind="member",
                input_value=value,
                resolved_to={
                    "id": payload.get("id"),
                    "userId": user_id,
                    "email": payload.get("email"),
                    "value": payload.get("value"),
                },
                method=self._candidate_resolution_method(resolved),
                confidence=float(resolved.get("score", 1.0)),
            )
            return payload
        if not ranked:
            raise RecordInputError(
                message=f"member value for field '{field.que_title}' could not be resolved in the current candidate scope",
                error_code="MEMBER_NOT_IN_CANDIDATE_SCOPE",
                fix_hint=self._candidate_scope_fix_hint(
                    kind="member",
                    state=resolution_state,
                    explicit_guidance="Explicit member id/userId/email values do not bypass candidate scope.",
                ),
                details={
                    "field": _field_ref_payload(field),
                    "location": location,
                    "received_value": value,
                    "candidate_count": len(candidates),
                    "candidate_sample": self._candidate_lookup_sample(candidates, kind="member"),
                    "scope_limited": True,
                },
            )
        request = self._build_confirmation_request(
            field=field,
            kind="member",
            input_value=value,
            candidates=self._member_confirmation_candidates(ranked),
        )
        self._mark_lookup_confirmation(resolution_state, field=field, request=request)
        return None

    def _match_member_candidates(self, candidates: list[JSONObject], value: JSONValue) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        member_id: int | None = None
        user_id: str | None = None
        email: str | None = None
        display: str | None = None
        if isinstance(value, dict):
            member_id = _coerce_count(value.get("id", value.get("uid", value.get("userId"))))
            user_id = _normalize_optional_text(value.get("userId"))
            email = _normalize_optional_text(value.get("email"))
            display = _normalize_optional_text(value.get("value", value.get("name", value.get("userName"))))
        else:
            member_id = _coerce_count(value)
            if member_id is None:
                display = _normalize_optional_text(value)
                user_id = display
        matches: list[JSONObject] = []
        normalized_display = display.strip() if isinstance(display, str) else None
        normalized_user_id = user_id.strip() if isinstance(user_id, str) else None
        normalized_email = email.strip().lower() if isinstance(email, str) else None
        for candidate in candidates:
            candidate_id = _coerce_count(candidate.get("id"))
            candidate_user_id = _normalize_optional_text(candidate.get("userId"))
            candidate_email = _normalize_optional_text(candidate.get("email"))
            candidate_value = _normalize_optional_text(candidate.get("value"))
            if member_id is not None and candidate_id == member_id:
                matches.append(candidate)
                continue
            if normalized_user_id and candidate_user_id == normalized_user_id:
                matches.append(candidate)
                continue
            if normalized_email and candidate_email and candidate_email.lower() == normalized_email:
                matches.append(candidate)
                continue
            if normalized_display and (
                candidate_value == normalized_display
                or candidate_user_id == normalized_display
                or (candidate_email and candidate_email.lower() == normalized_display.lower())
            ):
                matches.append(candidate)
        return matches

    def _lookup_department_detail(self, context, keyword: str, *, purpose: str) -> JSONObject:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        items = self._search_workspace_departments(context, keyword=keyword)
        normalized_keyword = keyword.strip()
        exact = [
            item for item in items
            if _normalize_optional_text(item.get("deptName", item.get("value", item.get("name")))) == normalized_keyword
        ]
        matches = exact or items
        if len(matches) != 1:
            raise RecordInputError(
                message=f"department {purpose} '{keyword}' is ambiguous or not found",
                error_code="DEPARTMENT_NOT_RESOLVED",
                fix_hint="Pass an exact department name or a numeric dept id.",
                details={"keyword": keyword, "matches": [{"id": item.get('deptId', item.get('id')), "value": item.get('deptName', item.get('value', item.get('name')))} for item in items[:10]]},
            )
        matched = matches[0]
        dept_id = _coerce_count(matched.get("deptId", matched.get("id")))
        if dept_id is None:
            raise RecordInputError(
                message=f"department {purpose} '{keyword}' resolved to an invalid department payload",
                error_code="DEPARTMENT_NOT_RESOLVED",
                fix_hint="Pass a numeric dept id directly.",
                details={"keyword": keyword, "matched": matched},
            )
        return {"id": dept_id, "value": _stringify_json(matched.get("deptName", matched.get("value", matched.get("name", dept_id))))}

    def _match_department_candidates(self, candidates: list[JSONObject], value: JSONValue) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        dept_id: int | None = None
        display: str | None = None
        if isinstance(value, dict):
            dept_id = _coerce_count(value.get("id", value.get("deptId")))
            display = _normalize_optional_text(value.get("value", value.get("name", value.get("deptName"))))
        else:
            dept_id = _coerce_count(value)
            if dept_id is None:
                display = _normalize_optional_text(value)
        normalized_display = display.strip() if isinstance(display, str) else None
        matches: list[JSONObject] = []
        for candidate in candidates:
            candidate_id = _coerce_count(candidate.get("id", candidate.get("deptId")))
            candidate_value = _normalize_optional_text(candidate.get("value", candidate.get("deptName", candidate.get("name"))))
            if dept_id is not None and candidate_id == dept_id:
                matches.append(candidate)
                continue
            if normalized_display and candidate_value == normalized_display:
                matches.append(candidate)
        return matches

    def _resolve_department_candidates_backend(
        self,
        context,  # type: ignore[no-untyped-def]
        field: FormField,
        *,
        keyword: str,
        state: LookupResolutionState,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
        external = bool(
            isinstance(scope.get("externalDepartList"), list) and scope.get("externalDepartList")
        )
        payload = self._build_lookup_scope_payload(state, keyword=keyword, que_id=field.que_id)
        payload["deptScope"] = "EXTERNAL" if external else "INTERNAL"
        result = self.backend.request("POST", context, "/data/deptSelectScope/search", json_body=payload)
        return self._normalize_backend_department_candidates(result, external=external)

    def _department_confirmation_candidates(self, candidates: list[JSONObject]) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        payloads: list[JSONObject] = []
        for candidate in candidates[:LOOKUP_CONFIRMATION_CANDIDATE_LIMIT]:
            payloads.append(
                {
                    "id": candidate.get("id"),
                    "label": candidate.get("value"),
                    "subtitle": candidate.get("path") or candidate.get("value"),
                    "path": candidate.get("path"),
                    "score": candidate.get("score"),
                    "match_reason": candidate.get("match_reason"),
                }
            )
        return payloads

    def _department_value_from_selector(
        self,
        profile: str,
        context,
        field: FormField,
        value: JSONValue,
        *,
        resolution_state: LookupResolutionState | None = None,
        location: str,
    ) -> JSONObject | None:  # type: ignore[no-untyped-def]
        """执行内部辅助逻辑。"""
        explicit_dept_id: int | None = None
        explicit_label: str | None = None
        if isinstance(value, dict):
            explicit_dept_id = _coerce_count(value.get("id", value.get("deptId")))
            explicit_label = _normalize_optional_text(value.get("value", value.get("name", value.get("deptName"))))
        else:
            explicit_dept_id = _coerce_count(value)
            if explicit_dept_id is None:
                explicit_label = _normalize_optional_text(value)
        has_explicit_selector = explicit_dept_id is not None
        keyword = explicit_label or self._candidate_keyword_from_value(
            value,
            text_keys=("value", "name", "deptName"),
            fallback_id_keys=("id", "deptId"),
        )
        if not keyword:
            raise RecordInputError(
                message=f"department value for field '{field.que_title}' requires a non-empty selector",
                error_code="INVALID_DEPARTMENT_VALUE",
                fix_hint="Pass a department id/deptId, full path, or a non-empty department name.",
                details={"field": _field_ref_payload(field), "location": location, "received_value": value},
            )
        if resolution_state is None:
            if has_explicit_selector:
                payload = _department_value(value)
                self._append_lookup_resolution(
                    resolution_state,
                    field=field,
                    kind="department",
                    input_value=value,
                    resolved_to=payload,
                    method="explicit_selector",
                    confidence=1.0,
                )
                return payload
            return self._lookup_department_detail(context, keyword, purpose="value")
        if has_explicit_selector:
            payload = _department_value(value)
            self._append_lookup_resolution(
                resolution_state,
                field=field,
                kind="department",
                input_value=value,
                resolved_to=payload,
                method="explicit_selector",
                confidence=1.0,
            )
            return payload
        if self._lookup_context_is_incomplete(resolution_state):
            self._raise_lookup_context_unavailable(
                field=field,
                kind="department",
                value=value,
                location=location,
                explicit_fix_hint="Retry with an explicit department id/object for this field.",
            )
        try:
            candidates = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=resolution_state)
        except QingflowApiError as exc:
            if exc.category == "not_supported":
                raise RecordInputError(
                    message=f"department auto resolution for field '{field.que_title}' is unsupported in the current candidate scope",
                    error_code="DEPARTMENT_AUTO_RESOLUTION_UNSUPPORTED",
                    fix_hint="Retry with an explicit department id/object for this field.",
                    details={"field": _field_ref_payload(field), "received_value": value, "candidate_error": exc.to_dict()},
                )
            self._raise_candidate_lookup_failed(kind="department", field=field, value=value, error=exc)
        ranked = self._rank_department_candidates(keyword, candidates)
        resolved = self._resolve_ranked_lookup_candidate(ranked)
        if resolved is not None:
            dept_id = _coerce_count(resolved.get("id", resolved.get("deptId")))
            if dept_id is None:
                raise RecordInputError(
                    message=f"department value for field '{field.que_title}' resolved to an invalid candidate payload",
                    error_code="INVALID_DEPARTMENT_VALUE",
                    fix_hint="Retry with an explicit dept id for this field.",
                    details={"field": _field_ref_payload(field), "location": location, "received_value": value, "matched": resolved},
                )
            payload = {"id": dept_id, "value": _stringify_json(resolved.get("value", dept_id))}
            self._append_lookup_resolution(
                resolution_state,
                field=field,
                kind="department",
                input_value=value,
                resolved_to=payload,
                method=self._candidate_resolution_method(resolved),
                confidence=float(resolved.get("score", 1.0)),
            )
            return payload
        if not ranked:
            scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
            has_scope_config = field.dept_select_scope_type is not None or bool(scope)
            if not has_scope_config:
                payload = self._lookup_department_detail(context, keyword, purpose="value")
                self._append_lookup_resolution(
                    resolution_state,
                    field=field,
                    kind="department",
                    input_value=value,
                    resolved_to=payload,
                    method="workspace_exact_lookup",
                    confidence=1.0,
                )
                return payload
            raise RecordInputError(
                message=f"department value for field '{field.que_title}' could not be resolved in the current candidate scope",
                error_code="DEPARTMENT_NOT_IN_CANDIDATE_SCOPE",
                fix_hint=self._candidate_scope_fix_hint(
                    kind="department",
                    state=resolution_state,
                    explicit_guidance="Explicit department ids do not bypass candidate scope.",
                ),
                details={
                    "field": _field_ref_payload(field),
                    "location": location,
                    "received_value": value,
                    "candidate_count": len(candidates),
                    "candidate_sample": self._candidate_lookup_sample(candidates, kind="department"),
                    "scope_limited": True,
                },
            )
        request = self._build_confirmation_request(
            field=field,
            kind="department",
            input_value=value,
            candidates=self._department_confirmation_candidates(ranked),
        )
        self._mark_lookup_confirmation(resolution_state, field=field, request=request)
        return None

    def _relation_value_payload(
        self,
        profile: str,
        context,
        field: FormField,
        value: JSONValue,
        *,
        resolution_state: LookupResolutionState | None,
        location: str,
        parent_field: FormField | None = None,
        row_ordinal: int | None = None,
    ) -> tuple[JSONObject | None, JSONObject | None]:
        """执行内部辅助逻辑。"""
        explicit_apply_id: str | None = None
        keyword: str | None = None
        if isinstance(value, dict):
            target_app_key = _normalize_optional_text(
                value.get("target_app_key", value.get("targetAppKey", value.get("app_key", value.get("appKey"))))
            )
            if field.target_app_key and target_app_key and target_app_key != field.target_app_key:
                raise RecordInputError(
                    message=f"relation field '{field.que_title}' points to a different target app",
                    error_code="RELATION_TARGET_APP_MISMATCH",
                    fix_hint=f"Use a record from target app '{field.target_app_key}'.",
                    details={
                        "field": _field_ref_payload(field),
                        "expected_target_app_key": field.target_app_key,
                        "received_target_app_key": target_app_key,
                        "received_value": value,
                    },
                )
            apply_id = value.get("apply_id", value.get("applyId", value.get("id")))
            if apply_id is not None:
                explicit_apply_id = _stringify_json(apply_id)
            else:
                keyword = self._candidate_keyword_from_value(
                    value,
                    text_keys=("query", "value", "name", "title", "label", "code"),
                )
        else:
            explicit_apply_id = str(_coerce_count(value)) if _coerce_count(value) is not None else None
            if explicit_apply_id is None:
                keyword = _normalize_optional_text(value)
        if explicit_apply_id:
            value_payload: JSONObject = {"value": explicit_apply_id}
            refer_payload: JSONObject = {"applyId": explicit_apply_id}
            self._append_lookup_resolution(
                resolution_state,
                field=field,
                kind="relation",
                input_value=value,
                resolved_to={"apply_id": explicit_apply_id},
                method="explicit_apply_id",
                confidence=1.0,
            )
            return value_payload, refer_payload
        if not keyword:
            raise RecordInputError(
                message=f"relation value for field '{field.que_title}' requires a non-empty selector",
                error_code="INVALID_RELATION_VALUE",
                fix_hint="Pass an apply_id/applyId object or a non-empty string that can be resolved inside the relation field searchable_fields.",
                details={"field": _field_ref_payload(field), "location": location, "received_value": value},
            )
        if resolution_state is None:
            raise RecordInputError(
                message=f"relation value for field '{field.que_title}' requires an explicit apply_id in this write path",
                error_code="RELATION_AUTO_RESOLUTION_UNSUPPORTED",
                fix_hint="Retry with an explicit apply_id/applyId object for this field.",
                details={"field": _field_ref_payload(field), "location": location, "received_value": value},
            )
        if self._lookup_context_is_incomplete(resolution_state):
            self._raise_lookup_context_unavailable(
                field=field,
                kind="relation",
                value=value,
                location=location,
                explicit_fix_hint="Retry with an explicit apply_id/applyId object for this field.",
            )
        candidates, searchable_fields = self._resolve_relation_candidates_backend(
            context,
            field,
            keyword=keyword,
            state=resolution_state,
        )
        ranked = self._rank_relation_candidates(keyword, candidates, searchable_fields=searchable_fields)
        resolved = self._resolve_ranked_lookup_candidate(ranked)
        if resolved is not None:
            apply_id = _normalize_optional_text(resolved.get("apply_id")) or _stringify_json(resolved.get("apply_id"))
            value_payload = {"value": apply_id}
            refer_payload = {"applyId": apply_id}
            self._append_lookup_resolution(
                resolution_state,
                field=field,
                kind="relation",
                input_value=value,
                resolved_to={"apply_id": apply_id, "label": resolved.get("label")},
                method=self._candidate_resolution_method(resolved),
                confidence=float(resolved.get("score", 1.0)),
            )
            return value_payload, refer_payload
        if not ranked:
            searchable_field_titles = [
                _normalize_optional_text(item.get("title"))
                for item in searchable_fields
                if isinstance(item, dict)
            ]
            raise RecordInputError(
                message=(
                    f"relation value for field '{field.que_title}' did not match any record inside the relation searchable_fields"
                ),
                error_code="RELATION_NOT_RESOLVED",
                fix_hint=(
                    "Use a value that matches one of the relation searchable_fields, "
                    "or retry with an explicit apply_id/applyId object."
                ),
                details={
                    "field": _field_ref_payload(field),
                    "location": location,
                    "received_value": value,
                    "searchable_fields": searchable_fields,
                    "searchable_field_titles": [item for item in searchable_field_titles if item],
                    "candidate_count": len(candidates),
                    "candidate_sample": self._candidate_lookup_sample(candidates, kind="relation"),
                },
            )
        request = self._build_confirmation_request(
            field=field,
            kind="relation",
            input_value=value,
            candidates=self._relation_confirmation_candidates(ranked),
            parent_field=parent_field,
            row_ordinal=row_ordinal,
        )
        self._mark_lookup_confirmation(
            resolution_state,
            field=field,
            request=request,
            parent_field=parent_field,
            row_ordinal=row_ordinal,
        )
        return None, None

    def _resolve_filter_field_entries(self, filters: list[JSONObject], index: FieldIndex) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        entries: list[JSONObject] = []
        for item in filters:
            if not isinstance(item, dict):
                continue
            selector = _extract_filter_selector(item)
            if selector is None:
                continue
            field = self._resolve_field_selector(cast(str | int | None, selector), index, location="filters")
            entries.append({"requested": str(selector), "field": field})
        return entries

    def _resolve_sorts(self, sorts: list[JSONObject], index: FieldIndex) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        resolved: list[JSONObject] = []
        for item in sorts:
            if not isinstance(item, dict):
                continue
            selector = _extract_sort_selector(item)
            if selector is None:
                continue
            field = self._resolve_field_selector(cast(str | int | None, selector), index, location="sorts")
            ascend = _resolve_sort_ascend(item)
            resolved.append({"queId": field.que_id, "isAscend": ascend})
        return resolved

    def _resolve_time_range_column(self, time_range: JSONObject, index: FieldIndex) -> FormField | None:
        """执行内部辅助逻辑。"""
        if not time_range or not isinstance(time_range, dict):
            return None
        column = time_range.get("column")
        if column is None:
            return None
        return self._resolve_field_selector(cast(str | int, column), index, location="time_range.column")

    def _build_time_range_filter(self, time_range: JSONObject, field: FormField) -> JSONObject | None:
        """执行内部辅助逻辑。"""
        if not time_range:
            return None
        value_from = time_range.get("from")
        value_to = time_range.get("to")
        if value_from is None and value_to is None:
            return None
        rule: JSONObject = {"queId": field.que_id}
        if field.que_type is not None:
            rule["queType"] = field.que_type
        if value_from is not None:
            rule["minValue"] = _stringify_json(value_from)
        if value_to is not None:
            rule["maxValue"] = _stringify_json(value_to)
        return rule

    def _append_time_range_filter(
        self,
        match_rules: list[JSONObject],
        time_range: JSONObject,
        field: FormField | None,
    ) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        if field is None:
            return match_rules
        time_rule = self._build_time_range_filter(time_range, field)
        if time_rule is None:
            return match_rules
        for item in match_rules:
            if not isinstance(item, dict):
                continue
            if _coerce_count(item.get("queId")) != field.que_id:
                continue
            if item.get("minValue") is not None or item.get("maxValue") is not None:
                return match_rules
        return [*match_rules, time_rule]

    def _collect_write_plan_field_refs(self, *, fields: JSONObject, answers: list[JSONObject], index: FieldIndex) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        refs: list[JSONObject] = []
        for field_key in fields.keys():
            refs.append(self._resolve_write_plan_field_ref("fields", field_key, index))
        for item in answers:
            if not isinstance(item, dict):
                continue
            selector = item.get(
                "field_id",
                item.get("fieldId", item.get("queId", item.get("que_id", item.get("queTitle", item.get("que_title"))))),
            )
            if selector is None:
                continue
            refs.append(self._resolve_write_plan_field_ref("answers", str(selector), index))
        return refs

    def _resolve_write_plan_field_ref(self, source: str, requested: str, index: FieldIndex) -> JSONObject:
        """执行内部辅助逻辑。"""
        try:
            field = self._resolve_field_selector(requested, index, location=source)
            return {
                "source": source,
                "requested": requested,
                "resolved": True,
                "que_id": field.que_id,
                "que_title": field.que_title,
                "que_type": field.que_type,
                "required": field.required,
                "readonly": field.readonly,
                "system": field.system,
                "write_format": _write_format_for_field(field),
                "reason": None,
            }
        except RecordInputError as error:
            return {
                "source": source,
                "requested": requested,
                "resolved": False,
                "que_id": None,
                "que_title": None,
                "que_type": None,
                "required": None,
                "readonly": None,
                "system": None,
                "write_format": {"kind": "unknown"},
                "reason": error.message,
            }

    def _validate_app_and_record(self, app_key: str, apply_id: int | str) -> int:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        return self._normalize_internal_backend_id(apply_id, field_name="apply_id")

    def _normalize_internal_backend_id(self, value: Any, *, field_name: str) -> int:
        """Normalize backend/apply ids after the public boundary has already preserved long string ids."""
        if value in (None, "") or isinstance(value, bool):
            raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
        if isinstance(value, int):
            if value <= 0:
                raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
            return value
        text = stringify_backend_id(value)
        if text is None or not text.isdecimal() or int(text) <= 0:
            raise_tool_error(QingflowApiError.config_error(f"{field_name} must be positive"))
        return int(text)

    def _validate_record_write(self, app_key: str, answers: list[JSONObject], apply_id: int | None = None) -> None:
        """执行内部辅助逻辑。"""
        if not app_key:
            raise_tool_error(QingflowApiError.config_error("app_key is required"))
        if apply_id is not None and apply_id <= 0:
            raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))
        if not isinstance(answers, list) or not answers:
            raise_tool_error(QingflowApiError.config_error("answers must be a non-empty array"))

    def _verify_record_write_result(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int | None,
        normalized_answers: list[JSONObject],
        index: FieldIndex,
        verify_list_type: int = DEFAULT_RECORD_LIST_TYPE,
        verify_role: int | None = None,
        verify_view_key: str | None = None,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        if apply_id is None:
            return {
                "verified": False,
                "error": "missing_apply_id",
                "missing_fields": [],
                "empty_fields": [],
                "count_mismatches": [],
            }
        try:
            if verify_view_key:
                record = self.backend.request(
                    "GET",
                    context,
                    f"/view/{verify_view_key}/apply/{apply_id}",
                )
            else:
                role = verify_role if verify_role is not None else 1
                record = self.backend.request(
                    "GET",
                    context,
                    f"/app/{app_key}/apply/{apply_id}",
                    params={"role": role, "listType": verify_list_type},
                )
        except QingflowApiError as exc:
            if verify_view_key:
                warning: JSONObject = {
                    "code": "WRITE_VERIFY_CUSTOM_VIEW_READBACK_FAILED",
                    "message": "Write was sent through a custom view route, but the same view could not be re-read for field-level verification.",
                }
                warning.update(_record_detail_error_warning_fields(exc))
                return {
                    "verified": False,
                    "verification_mode": "custom_view_record_detail",
                    "field_level_verified": False,
                    "error": "custom_view_readback_failed",
                    "missing_fields": [],
                    "empty_fields": [],
                    "count_mismatches": [],
                    "warnings": [warning],
                }
            if not _is_record_permission_denied_error(exc):
                raise
            return self._verify_record_write_result_via_initiated_tasks(
                context,
                app_key=app_key,
                apply_id=apply_id,
                original_error=exc,
            )
        answers = record.get("answers") if isinstance(record, dict) else None
        answer_list = answers if isinstance(answers, list) else []
        actual_by_id = {
            que_id: item
            for item in answer_list
            if isinstance(item, dict) and (que_id := _coerce_count(item.get("queId"))) is not None
        }
        missing_fields: list[JSONObject] = []
        empty_fields: list[JSONObject] = []
        count_mismatches: list[JSONObject] = []
        for answer in normalized_answers:
            que_id = _coerce_count(answer.get("queId"))
            if que_id is None or que_id <= 0:
                continue
            actual = actual_by_id.get(que_id)
            field = index.by_id.get(str(que_id))
            field_payload = _field_ref_payload(field) if field is not None else {"que_id": que_id}
            if actual is None:
                missing_fields.append(field_payload)
                continue
            expected_rows = answer.get("tableValues") if isinstance(answer.get("tableValues"), list) else []
            if field is not None and field.que_type in SUBTABLE_QUE_TYPES and expected_rows:
                missing_before = len(missing_fields)
                empty_before = len(empty_fields)
                mismatch_before = len(count_mismatches)
                actual_rows = actual.get("tableValues") if isinstance(actual.get("tableValues"), list) else []
                self._verify_subtable_write_result(
                    field=field,
                    expected_rows=expected_rows,
                    actual_rows=actual_rows,
                    missing_fields=missing_fields,
                    empty_fields=empty_fields,
                    count_mismatches=count_mismatches,
                )
                if (
                    len(missing_fields) > missing_before
                    or len(empty_fields) > empty_before
                    or len(count_mismatches) > mismatch_before
                ):
                    continue
                continue
            expected_value = _canonicalize_answer_value_for_compare(answer, field)
            actual_value = _canonicalize_answer_value_for_compare(actual, field)
            if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
                empty_fields.append(field_payload)
                continue
            if expected_value != actual_value:
                count_mismatches.append(
                    {
                        **field_payload,
                        "expected_value": expected_value,
                        "actual_value": actual_value,
                    }
                )
        return {
            "verified": not missing_fields and not empty_fields and not count_mismatches,
            "verification_mode": "custom_view_record_detail" if verify_view_key else "initiated_record_view",
            "field_level_verified": True,
            "missing_fields": missing_fields,
            "empty_fields": empty_fields,
            "count_mismatches": count_mismatches,
        }

    def _verify_record_write_result_via_initiated_tasks(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        app_key: str,
        apply_id: int,
        original_error: QingflowApiError,
    ) -> JSONObject:
        """执行内部辅助逻辑。"""
        readback_error = _record_write_readback_error_payload(original_error)
        for page_num in range(1, VERIFY_TASK_FALLBACK_MAX_PAGES + 1):
            task_page = self.backend.request(
                "POST",
                context,
                "/task/dynamic/page",
                json_body={
                    "type": 2,
                    "processStatus": 1,
                    "appKey": app_key,
                    "pageNum": page_num,
                    "pageSize": VERIFY_TASK_FALLBACK_PAGE_SIZE,
                },
            )
            items = task_page.get("list") if isinstance(task_page, dict) else None
            if not isinstance(items, list):
                items = []
            for item in items:
                if not isinstance(item, dict):
                    continue
                candidate_apply_id = _coerce_count(item.get("rowRecordId") or item.get("recordId") or item.get("applyId"))
                if candidate_apply_id != apply_id:
                    continue
                warning: JSONObject = {
                    "code": "WRITE_VERIFIED_VIA_TASK_BOX",
                    "message": "Immediate field-level readback was unavailable; write was confirmed via the initiated task box fallback.",
                    "readback_error": readback_error,
                }
                warning.update(_record_detail_error_warning_fields(original_error))
                return {
                    "verified": True,
                    "verification_mode": "initiated_task_box",
                    "field_level_verified": False,
                    "missing_fields": [],
                    "empty_fields": [],
                    "count_mismatches": [],
                    "warnings": [warning],
                    "readback_error": readback_error,
                    "task_match": {
                        "task_id": item.get("id") or item.get("taskId"),
                        "record_id": candidate_apply_id,
                        "workflow_node_id": item.get("nodeId") or item.get("auditNodeId"),
                    },
                }
            if len(items) < VERIFY_TASK_FALLBACK_PAGE_SIZE:
                break
        return {
            "verified": False,
            "verification_mode": "initiated_task_box",
            "field_level_verified": False,
            "error": "initiated_task_box_missing_record",
            "missing_fields": [],
            "empty_fields": [],
            "count_mismatches": [],
            "warnings": [{
                "code": "WRITE_VERIFY_FALLBACK_FAILED",
                "message": "Immediate field-level readback was unavailable, and the record was not found in the initiated task box fallback.",
                "readback_error": readback_error,
                **_record_detail_error_warning_fields(original_error),
            }],
            "readback_error": readback_error,
            "original_error": readback_error,
        }

    def _verify_subtable_write_result(
        self,
        *,
        field: FormField | None,
        expected_rows: list[JSONValue],
        actual_rows: list[JSONValue],
        missing_fields: list[JSONObject],
        empty_fields: list[JSONObject],
        count_mismatches: list[JSONObject],
    ) -> None:
        """执行内部辅助逻辑。"""
        field_payload = _field_ref_payload(field) if field is not None else {"que_id": None}
        if not actual_rows:
            empty_fields.append(field_payload)
            return
        if len(actual_rows) < len(expected_rows):
            count_mismatches.append(
                {
                    **field_payload,
                    "expected_count": len(expected_rows),
                    "actual_count": len(actual_rows),
                    "unit": "rows",
                }
            )
        subtable_index = self._subtable_field_index_optional(field)
        actual_rows_by_row_id = _index_subtable_rows_by_row_id(actual_rows)
        for row_ordinal, raw_expected_row in enumerate(expected_rows):
            expected_row = [item for item in raw_expected_row if isinstance(item, dict)] if isinstance(raw_expected_row, list) else []
            expected_row_id = _subtable_row_id(expected_row)
            actual_row = None
            if expected_row_id is not None:
                actual_row = actual_rows_by_row_id.get(expected_row_id)
            if actual_row is None and row_ordinal < len(actual_rows):
                candidate = actual_rows[row_ordinal]
                actual_row = candidate if isinstance(candidate, list) else None
            if actual_row is None:
                missing_fields.append({**field_payload, "row_ordinal": row_ordinal})
                continue
            actual_cells_by_id = {
                que_id: item
                for item in actual_row
                if isinstance(item, dict) and (que_id := _coerce_count(item.get("queId"))) is not None
            }
            for expected_cell in expected_row:
                que_id = _coerce_count(expected_cell.get("queId"))
                if que_id is None or que_id <= 0:
                    continue
                subfield = subtable_index.by_id.get(str(que_id)) if subtable_index is not None else None
                cell_payload = _subtable_cell_ref_payload(field, subfield, que_id=que_id, row_ordinal=row_ordinal, row_id=expected_row_id)
                actual_cell = actual_cells_by_id.get(que_id)
                if actual_cell is None:
                    missing_fields.append(cell_payload)
                    continue
                if subfield is not None and subfield.que_type in RELATION_QUE_TYPES:
                    expected_relation_ids = _relation_ids_from_answer(expected_cell)
                    actual_relation_ids = _relation_ids_from_answer(actual_cell)
                    if expected_relation_ids and not actual_relation_ids:
                        empty_fields.append(cell_payload)
                        continue
                    if expected_relation_ids:
                        actual_id_set = set(actual_relation_ids)
                        missing_ids = [value for value in expected_relation_ids if value not in actual_id_set]
                        if missing_ids:
                            count_mismatches.append(
                                {
                                    **cell_payload,
                                    "expected_ids": expected_relation_ids,
                                    "actual_ids": actual_relation_ids,
                                    "missing_ids": missing_ids,
                                }
                            )
                    continue
                actual_values = actual_cell.get("values") if isinstance(actual_cell.get("values"), list) else []
                if not actual_values:
                    empty_fields.append(cell_payload)
                    continue
                expected_values = expected_cell.get("values") if isinstance(expected_cell.get("values"), list) else []
                if expected_values and len(actual_values) < len(expected_values):
                    count_mismatches.append(
                        {
                            **cell_payload,
                            "expected_count": len(expected_values),
                            "actual_count": len(actual_values),
                            "unit": "values",
                        }
                    )

    def _score_field_matches(self, requested: str, index: FieldIndex, *, fuzzy: bool, top_k: int) -> list[JSONObject]:
        """执行内部辅助逻辑。"""
        requested_text = requested.strip()
        requested_key = _normalize_field_lookup_key(requested_text)
        if not requested_key:
            return []
        matches: list[JSONObject] = []
        for field in index.by_id.values():
            best_score = 0.0
            match_type = "none"
            matched_alias: str | None = None
            if requested_text.isdigit() and field.que_id == int(requested_text):
                best_score = 1.0
                match_type = "id_exact"
            else:
                best_score, match_type = _score_candidate_text(requested_key, field.que_title, fuzzy=fuzzy, label="title")
                for alias in field.aliases:
                    alias_score, alias_match_type = _score_candidate_text(requested_key, alias, fuzzy=fuzzy, label="alias")
                    if alias_score > best_score:
                        best_score = alias_score
                        match_type = alias_match_type
                        matched_alias = alias
            if best_score <= 0:
                continue
            payload = {
                "que_id": field.que_id,
                "que_title": field.que_title,
                "que_type": field.que_type,
                "score": round(best_score, 4),
                "match_type": match_type,
            }
            if matched_alias is not None:
                payload["matched_alias"] = matched_alias
            matches.append(payload)
        matches.sort(key=lambda item: float(item["score"]), reverse=True)
        return matches[: max(top_k, 1)]

    def _raise_need_more_data(self, completeness: JSONObject, evidence: JSONObject, message: str) -> None:
        """执行内部辅助逻辑。"""
        raise RuntimeError(
            json.dumps(
                {
                    "ok": False,
                    "code": "NEED_MORE_DATA",
                    "status": "need_more_data",
                    "message": message,
                    "details": {"completeness": completeness, "evidence": evidence},
                },
                ensure_ascii=False,
            )
        )


def _normalize_form_schema(payload: JSONValue) -> JSONObject:
    if isinstance(payload, dict) and isinstance(payload.get("formQues"), list):
        return payload
    if isinstance(payload, list):
        for item in payload:
            if isinstance(item, dict) and isinstance(item.get("formQues"), list):
                return item
    return {}


def _normalize_data_list_base_info_schema(payload: JSONValue) -> JSONObject:
    if not isinstance(payload, dict):
        return {}
    que_base_infos = payload.get("queBaseInfos")
    if not isinstance(que_base_infos, list) and isinstance(payload.get("formQues"), list):
        que_base_infos = payload.get("formQues")
    if not isinstance(que_base_infos, list):
        return {}
    return {
        "formTitle": payload.get("formTitle"),
        "baseQues": [],
        "formQues": [item for item in que_base_infos if isinstance(item, dict)],
        "questionRelations": [],
    }


def _normalize_view_list(payload: JSONValue) -> list[JSONObject]:
    if not isinstance(payload, list):
        return []
    flattened: list[JSONObject] = []
    for group in payload:
        if not isinstance(group, dict):
            continue
        view_list = group.get("viewList")
        if not isinstance(view_list, list):
            continue
        for item in view_list:
            if isinstance(item, dict) and item.get("viewKey"):
                flattened.append(item)
    return flattened


def _normalize_audit_nodes(payload: JSONValue) -> list[JSONObject]:
    if isinstance(payload, list):
        return [item for item in payload if isinstance(item, dict)]
    if isinstance(payload, dict):
        return [item for item in payload.values() if isinstance(item, dict)]
    return []


def _answer_has_meaningful_content(answer: JSONObject) -> bool:
    table_values = answer.get("tableValues")
    if isinstance(table_values, list) and table_values:
        for row in table_values:
            if isinstance(row, list) and any(_answer_has_meaningful_content(item) for item in row if isinstance(item, dict)):
                return True
        return False
    values = answer.get("values")
    if not isinstance(values, list) or not values:
        return False
    for item in values:
        if isinstance(item, dict):
            if any(value not in (None, "", [], {}) for value in item.values()):
                return True
            continue
        if item not in (None, "", [], {}):
            return True
    return False


def _extract_applicant_node(payload: JSONValue) -> WorkflowNodeRef | None:
    for item in _normalize_audit_nodes(payload):
        node_type = _coerce_count(item.get("type"))
        deal_type = _coerce_count(item.get("dealType"))
        workflow_node_id = _coerce_count(item.get("auditNodeId"))
        if workflow_node_id is None or node_type != 0 or deal_type != 3:
            continue
        return WorkflowNodeRef(
            workflow_node_id=workflow_node_id,
            name=_normalize_optional_text(item.get("auditNodeName")) or str(workflow_node_id),
            type="applicant",
            raw=item,
        )
    return None


def _compile_view_conditions(config: JSONObject) -> list[list[ViewFilterCondition]]:
    raw_limit = config.get("viewgraphLimit")
    if not isinstance(raw_limit, list):
        return []
    compiled: list[list[ViewFilterCondition]] = []
    for raw_group in raw_limit:
        group_items = raw_group if isinstance(raw_group, list) else [raw_group]
        group: list[ViewFilterCondition] = []
        for raw_condition in group_items:
            if not isinstance(raw_condition, dict):
                continue
            group.append(
                ViewFilterCondition(
                    que_id=_coerce_count(raw_condition.get("queId")),
                    que_title=_stringify_json(raw_condition.get("queTitle")).strip(),
                    que_type=_coerce_count(raw_condition.get("queType")),
                    judge_type=_coerce_count(raw_condition.get("judgeType")),
                    judge_values=[_stringify_json(item).strip() for item in cast(list[JSONValue], raw_condition.get("judgeValues") or []) if _stringify_json(item).strip()],
                    judge_value_details=[item for item in cast(list[JSONValue], raw_condition.get("judgeValueDetails") or []) if isinstance(item, dict)],
                    raw=raw_condition,
                )
            )
        if group:
            compiled.append(group)
    return compiled


def _collect_top_level_questions(
    payload: JSONValue,
    *,
    allow_hidden: bool = False,
    hidden_only: bool = False,
) -> list[JSONObject]:
    questions: list[JSONObject] = []
    if isinstance(payload, dict):
        is_question = "queId" in payload or "queTitle" in payload
        if is_question:
            que_type = _coerce_count(payload.get("queType"))
            if _should_index_question(payload, allow_hidden=allow_hidden):
                is_hidden = bool(payload.get("beingHide") or payload.get("hidden"))
                if not hidden_only or is_hidden:
                    questions.append(payload)
                if que_type in SUBTABLE_QUE_TYPES:
                    return questions
            if not _question_is_container_wrapper(payload, allow_hidden=allow_hidden):
                return questions
        for key in ("baseQues", "formQues"):
            value = payload.get(key)
            if isinstance(value, list):
                questions.extend(
                    _collect_top_level_questions(
                        value,
                        allow_hidden=allow_hidden,
                        hidden_only=hidden_only,
                    )
                )
        for key in ("innerQuestions", "subQues", "subQuestions"):
            value = payload.get(key)
            if isinstance(value, list):
                questions.extend(
                    _collect_top_level_questions(
                        value,
                        allow_hidden=allow_hidden,
                        hidden_only=hidden_only,
                    )
                )
        return questions
    if isinstance(payload, list):
        for item in payload:
            questions.extend(
                _collect_top_level_questions(
                    item,
                    allow_hidden=allow_hidden,
                    hidden_only=hidden_only,
                )
            )
    return questions


def _question_to_form_field(
    question: JSONObject,
    *,
    is_base_question: bool,
    allow_hidden: bool = False,
) -> FormField | None:
    if not _should_index_question(question, allow_hidden=allow_hidden):
        return None
    que_id = _coerce_count(question.get("queId"))
    title = _stringify_json(question.get("queTitle")).strip()
    if que_id is None or que_id < 0 or not title:
        return None
    can_edit = question.get("canEdit")
    field = FormField(
        que_id=que_id,
        que_title=title,
        que_type=_coerce_count(question.get("queType")),
        required=bool(question.get("required") or question.get("beingRequired")),
        readonly=bool(question.get("readonly") or question.get("beingReadonly") or is_base_question or can_edit is False),
        system=bool(question.get("system") or question.get("beingSystem") or is_base_question),
        options=_extract_question_options(question),
        aliases=[],
        target_app_key=_extract_relation_target_app_key(question),
        target_app_name_hint=_extract_relation_target_app_name_hint(question),
        member_select_scope_type=_coerce_count(question.get("memberSelectScopeType")),
        member_select_scope=_normalize_scope_payload(question.get("memberSelectScope")),
        dept_select_scope_type=_coerce_count(question.get("deptSelectScopeType")),
        dept_select_scope=_normalize_scope_payload(question.get("deptSelectScope")),
        raw=question,
    )
    field.aliases = sorted(_field_alias_candidates(field))
    return field


def _add_subtable_leaf_ref(
    target: dict[str, list[SubtableLeafRef]],
    key: str,
    ref: SubtableLeafRef,
) -> None:
    if not key:
        return
    refs = target.setdefault(key, [])
    if any(existing.parent_field.que_id == ref.parent_field.que_id and existing.field.que_id == ref.field.que_id for existing in refs):
        return
    refs.append(ref)


def _subtable_question_payload(question: JSONObject) -> list[JSONObject]:
    for key in ("subQuestions", "innerQuestions", "subQues"):
        value = question.get(key)
        if isinstance(value, list):
            return [item for item in _flatten_questions(value) if isinstance(item, dict)]
    return []


def _build_field_index(
    schema: JSONObject,
    *,
    include_subtable_leaf_fields: bool = True,
    force_subtable_parents_writable: bool = False,
    allow_hidden: bool = False,
    hidden_only: bool = False,
    field_id_filter: set[str] | None = None,
) -> FieldIndex:
    by_id: dict[str, FormField] = {}
    by_title: dict[str, list[FormField]] = {}
    by_alias: dict[str, list[FormField]] = {}
    subtable_leaf_by_id: dict[str, list[SubtableLeafRef]] = {}
    subtable_leaf_by_title: dict[str, list[SubtableLeafRef]] = {}
    subtable_leaf_by_alias: dict[str, list[SubtableLeafRef]] = {}
    all_questions = [
        *[
            (question, True)
            for question in (
                _flatten_questions(schema.get("baseQues"))
                if include_subtable_leaf_fields
                else _collect_top_level_questions(
                    schema.get("baseQues"),
                    allow_hidden=allow_hidden,
                    hidden_only=hidden_only,
                )
            )
        ],
        *[
            (question, False)
            for question in (
                _flatten_questions(schema.get("formQues"))
                if include_subtable_leaf_fields
                else _collect_top_level_questions(
                    schema.get("formQues"),
                    allow_hidden=allow_hidden,
                    hidden_only=hidden_only,
                )
            )
        ],
    ]
    for question, is_base_question in all_questions:
        field = _question_to_form_field(
            question,
            is_base_question=is_base_question,
            allow_hidden=allow_hidden,
        )
        if field is None:
            continue
        if field_id_filter is not None and str(field.que_id) not in field_id_filter:
            continue
        if force_subtable_parents_writable and field.que_type in SUBTABLE_QUE_TYPES and not field.system:
            field = _clone_form_field(field, readonly=False)
        if str(field.que_id) in by_id:
            continue
        by_id[str(field.que_id)] = field
        by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
        for alias in field.aliases:
            by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
        if field.que_type not in SUBTABLE_QUE_TYPES:
            continue
        for sub_question in _subtable_question_payload(question):
            sub_field = _question_to_form_field(cast(JSONObject, sub_question), is_base_question=False)
            if sub_field is None:
                continue
            ref = SubtableLeafRef(field=sub_field, parent_field=field)
            _add_subtable_leaf_ref(subtable_leaf_by_id, str(sub_field.que_id), ref)
            _add_subtable_leaf_ref(subtable_leaf_by_title, _normalize_field_lookup_key(sub_field.que_title), ref)
            for alias in sub_field.aliases:
                _add_subtable_leaf_ref(subtable_leaf_by_alias, _normalize_field_lookup_key(alias), ref)
    return FieldIndex(
        by_id=by_id,
        by_title=by_title,
        by_alias=by_alias,
        subtable_leaf_by_id=subtable_leaf_by_id,
        subtable_leaf_by_title=subtable_leaf_by_title,
        subtable_leaf_by_alias=subtable_leaf_by_alias,
    )


def _build_top_level_field_index(schema: JSONObject) -> FieldIndex:
    return _build_field_index(schema, include_subtable_leaf_fields=False)


def _build_applicant_top_level_field_index(schema: JSONObject) -> FieldIndex:
    return _build_field_index(
        schema,
        include_subtable_leaf_fields=False,
        force_subtable_parents_writable=True,
    )


def _build_applicant_hidden_linked_top_level_field_index(
    schema: JSONObject,
    *,
    linked_field_ids: set[str],
) -> FieldIndex:
    return _build_field_index(
        schema,
        include_subtable_leaf_fields=False,
        force_subtable_parents_writable=True,
        allow_hidden=True,
        hidden_only=True,
        field_id_filter=linked_field_ids,
    )


def _answer_to_form_field(answer: JSONObject) -> FormField | None:
    que_id = _coerce_count(answer.get("queId", answer.get("que_id")))
    title = _normalize_optional_text(answer.get("queTitle", answer.get("que_title")))
    que_type = _coerce_count(answer.get("queType", answer.get("que_type")))
    if que_id is None or que_id < 0 or not title or que_type in LAYOUT_ONLY_QUE_TYPES:
        return None
    raw: JSONObject = {
        "queId": que_id,
        "queTitle": title,
    }
    if que_type is not None:
        raw["queType"] = que_type
    field = FormField(
        que_id=que_id,
        que_title=title,
        que_type=que_type,
        required=False,
        readonly=False,
        system=False,
        options=[],
        aliases=[],
        target_app_key=None,
        target_app_name_hint=None,
        member_select_scope_type=None,
        member_select_scope=None,
        dept_select_scope_type=None,
        dept_select_scope=None,
        raw=raw,
    )
    field.aliases = sorted(_field_alias_candidates(field))
    return field


def _build_answer_backed_field_index(
    answers: list[JSONObject] | JSONValue,
    *,
    field_id_filter: set[str] | None = None,
) -> FieldIndex:
    by_id: dict[str, FormField] = {}
    by_title: dict[str, list[FormField]] = {}
    by_alias: dict[str, list[FormField]] = {}
    if not isinstance(answers, list):
        return FieldIndex(
            by_id=by_id,
            by_title=by_title,
            by_alias=by_alias,
            subtable_leaf_by_id={},
            subtable_leaf_by_title={},
            subtable_leaf_by_alias={},
        )
    for item in answers:
        if not isinstance(item, dict):
            continue
        field = _answer_to_form_field(item)
        if field is None:
            continue
        field_id = str(field.que_id)
        if field_id_filter is not None and field_id not in field_id_filter:
            continue
        if field_id in by_id:
            continue
        by_id[field_id] = field
        by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
        for alias in field.aliases:
            by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)
    return FieldIndex(
        by_id=by_id,
        by_title=by_title,
        by_alias=by_alias,
        subtable_leaf_by_id={},
        subtable_leaf_by_title={},
        subtable_leaf_by_alias={},
    )


def _merge_subtable_parent_field(primary: FormField, extra: FormField) -> FormField:
    if primary.que_type not in SUBTABLE_QUE_TYPES or extra.que_type not in SUBTABLE_QUE_TYPES:
        return primary
    primary_raw = dict(primary.raw) if isinstance(primary.raw, dict) else {}
    extra_raw = dict(extra.raw) if isinstance(extra.raw, dict) else {}
    primary_subquestions = primary_raw.get("subQuestions")
    extra_subquestions = extra_raw.get("subQuestions")
    if not isinstance(primary_subquestions, list) or not isinstance(extra_subquestions, list):
        return primary
    merged_subquestions = [item for item in primary_subquestions if isinstance(item, dict)]
    seen_ids = {
        _coerce_count(item.get("queId"))
        for item in merged_subquestions
        if isinstance(item, dict) and _coerce_count(item.get("queId")) is not None
    }
    for item in extra_subquestions:
        if not isinstance(item, dict):
            continue
        que_id = _coerce_count(item.get("queId"))
        if que_id is not None and que_id in seen_ids:
            continue
        merged_subquestions.append(item)
        if que_id is not None:
            seen_ids.add(que_id)
    if len(merged_subquestions) == len(primary_subquestions):
        return primary
    merged_raw = dict(primary_raw)
    merged_raw["subQuestions"] = merged_subquestions
    merged_field = _clone_form_field(primary)
    merged_field.raw = merged_raw
    return merged_field


def _replace_field_in_lookup(index: dict[str, list[FormField]], field: FormField) -> None:
    for key, fields in list(index.items()):
        index[key] = [field if existing.que_id == field.que_id else existing for existing in fields]


def _merge_field_indexes(primary: FieldIndex, extra: FieldIndex) -> FieldIndex:
    by_id = dict(primary.by_id)
    by_title = {key: list(value) for key, value in primary.by_title.items()}
    by_alias = {key: list(value) for key, value in primary.by_alias.items()}
    subtable_leaf_by_id = {key: list(value) for key, value in primary.subtable_leaf_by_id.items()}
    subtable_leaf_by_title = {key: list(value) for key, value in primary.subtable_leaf_by_title.items()}
    subtable_leaf_by_alias = {key: list(value) for key, value in primary.subtable_leaf_by_alias.items()}

    for field_id, field in extra.by_id.items():
        if field_id in by_id:
            merged_field = _merge_subtable_parent_field(by_id[field_id], field)
            if merged_field is not by_id[field_id]:
                by_id[field_id] = merged_field
                _replace_field_in_lookup(by_title, merged_field)
                _replace_field_in_lookup(by_alias, merged_field)
            continue
        by_id[field_id] = field
        by_title.setdefault(_normalize_field_lookup_key(field.que_title), []).append(field)
        for alias in field.aliases:
            by_alias.setdefault(_normalize_field_lookup_key(alias), []).append(field)

    for field_id, fields in extra.subtable_leaf_by_id.items():
        merged = subtable_leaf_by_id.setdefault(field_id, [])
        existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
        for field in fields:
            key = (field.field.que_id, field.parent_field.que_id)
            if key not in existing:
                merged.append(field)
                existing.add(key)
    for title, fields in extra.subtable_leaf_by_title.items():
        merged = subtable_leaf_by_title.setdefault(title, [])
        existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
        for field in fields:
            key = (field.field.que_id, field.parent_field.que_id)
            if key not in existing:
                merged.append(field)
                existing.add(key)
    for alias, fields in extra.subtable_leaf_by_alias.items():
        merged = subtable_leaf_by_alias.setdefault(alias, [])
        existing = {(ref.field.que_id, ref.parent_field.que_id) for ref in merged}
        for field in fields:
            key = (field.field.que_id, field.parent_field.que_id)
            if key not in existing:
                merged.append(field)
                existing.add(key)

    return FieldIndex(
        by_id=by_id,
        by_title=by_title,
        by_alias=by_alias,
        subtable_leaf_by_id=subtable_leaf_by_id,
        subtable_leaf_by_title=subtable_leaf_by_title,
        subtable_leaf_by_alias=subtable_leaf_by_alias,
    )


def _clone_form_field(field: FormField, *, readonly: bool | None = None) -> FormField:
    return FormField(
        que_id=field.que_id,
        que_title=field.que_title,
        que_type=field.que_type,
        required=field.required,
        readonly=field.readonly if readonly is None else readonly,
        system=field.system,
        options=list(field.options),
        aliases=list(field.aliases),
        target_app_key=field.target_app_key,
        target_app_name_hint=field.target_app_name_hint,
        member_select_scope_type=field.member_select_scope_type,
        member_select_scope=field.member_select_scope,
        dept_select_scope_type=field.dept_select_scope_type,
        dept_select_scope=field.dept_select_scope,
        raw=field.raw,
    )


def _enrich_read_field_from_applicant(field: FormField, applicant_field: FormField | None) -> FormField:
    if applicant_field is None:
        return field
    raw = dict(applicant_field.raw) if isinstance(applicant_field.raw, dict) else {}
    raw.update(dict(field.raw) if isinstance(field.raw, dict) else {})
    raw["queId"] = field.que_id
    raw["queTitle"] = field.que_title
    que_type = field.que_type if field.que_type is not None else applicant_field.que_type
    if que_type is not None:
        raw["queType"] = que_type
    readonly_source = field if any(key in field.raw for key in ("readonly", "beingReadonly", "canEdit")) else applicant_field
    enriched = FormField(
        que_id=field.que_id,
        que_title=field.que_title,
        que_type=que_type,
        required=field.required or applicant_field.required,
        readonly=readonly_source.readonly,
        system=field.system or applicant_field.system,
        options=list(field.options or applicant_field.options),
        aliases=[],
        target_app_key=field.target_app_key or applicant_field.target_app_key,
        target_app_name_hint=field.target_app_name_hint or applicant_field.target_app_name_hint,
        member_select_scope_type=field.member_select_scope_type if field.member_select_scope_type is not None else applicant_field.member_select_scope_type,
        member_select_scope=field.member_select_scope or applicant_field.member_select_scope,
        dept_select_scope_type=field.dept_select_scope_type if field.dept_select_scope_type is not None else applicant_field.dept_select_scope_type,
        dept_select_scope=field.dept_select_scope or applicant_field.dept_select_scope,
        raw=raw,
    )
    enriched.aliases = sorted(_field_alias_candidates(enriched))
    return enriched


def _form_field_to_question(field: FormField) -> JSONObject:
    question = dict(field.raw) if isinstance(field.raw, dict) else {}
    question.setdefault("queId", field.que_id)
    question.setdefault("queTitle", field.que_title)
    if field.que_type is not None:
        question.setdefault("queType", field.que_type)
    question["readonly"] = field.readonly
    question["beingReadonly"] = field.readonly
    question["required"] = field.required
    question["beingRequired"] = field.required
    question["system"] = field.system
    question["beingSystem"] = field.system
    return question


def _question_ids_from_schema(schema: JSONObject) -> set[int]:
    question_ids: set[int] = set()
    for question in _flatten_questions(schema):
        que_id = _coerce_count(question.get("queId"))
        if que_id is not None and que_id >= 0:
            question_ids.add(que_id)
    return question_ids


def _subtable_descendant_ids(field: FormField) -> set[int]:
    return {
        item["que_id"]
        for item in _subtable_columns_for_field(field)
        if isinstance(item.get("que_id"), int)
    }


def _extract_relation_target_app_key(question: JSONObject) -> str | None:
    if _coerce_count(question.get("queType")) not in RELATION_QUE_TYPES:
        return None
    reference = question.get("referenceConfig")
    if not isinstance(reference, dict):
        return None
    return _normalize_optional_text(reference.get("referAppKey"))


def _extract_relation_target_app_name_hint(question: JSONObject) -> str | None:
    if _coerce_count(question.get("queType")) not in RELATION_QUE_TYPES:
        return None
    reference = question.get("referenceConfig")
    if not isinstance(reference, dict):
        return None
    for key in ("referAppName", "referAppTitle", "referFormTitle", "appName", "appTitle"):
        text = _normalize_optional_text(reference.get(key))
        if text:
            return text
    return None


def _relation_searchable_questions(question: JSONObject) -> list[JSONObject]:
    if _coerce_count(question.get("queType")) not in RELATION_QUE_TYPES:
        return []
    reference = question.get("referenceConfig")
    if not isinstance(reference, dict):
        return []
    refer_questions = reference.get("referQuestions")
    if not isinstance(refer_questions, list):
        return []
    return [item for item in refer_questions if isinstance(item, dict)]


def _relation_searchable_fields_for_question(question: JSONObject) -> list[JSONObject]:
    reference = question.get("referenceConfig")
    primary_que_id = (
        _coerce_count(reference.get("referQueId"))
        if isinstance(reference, dict)
        else None
    )
    fields: list[JSONObject] = []
    for item in _relation_searchable_questions(question):
        que_id = _coerce_count(item.get("queId"))
        title = _normalize_optional_text(item.get("queTitle"))
        if que_id is None or not title:
            continue
        payload: JSONObject = {
            "field_id": que_id,
            "title": title,
            "primary": que_id == primary_que_id,
        }
        fields.append(payload)
    return fields


def _normalize_scope_payload(value: JSONValue) -> JSONObject | None:
    return value if isinstance(value, dict) else None


def _flatten_questions(payload: JSONValue) -> list[JSONObject]:
    flattened: list[JSONObject] = []
    if isinstance(payload, dict):
        is_question = "queId" in payload or "queTitle" in payload
        if is_question:
            flattened.append(payload)
        for key in ("subQuestions", "innerQuestions", "subQues"):
            value = payload.get(key)
            if isinstance(value, list):
                flattened.extend(_flatten_questions(value))
        if not is_question:
            for key in ("baseQues", "formQues"):
                value = payload.get(key)
                if isinstance(value, list):
                    flattened.extend(_flatten_questions(value))
    elif isinstance(payload, list):
        for item in payload:
            flattened.extend(_flatten_questions(item))
    return flattened


def _question_is_container_wrapper(question: JSONObject, *, allow_hidden: bool = False) -> bool:
    return not _should_index_question(question, allow_hidden=allow_hidden)


def _should_index_question(question: JSONObject, *, allow_hidden: bool = False) -> bool:
    if not allow_hidden and bool(question.get("beingHide") or question.get("hidden")):
        return False
    if _coerce_count(question.get("quoteId")) is not None:
        return False
    que_type = _coerce_count(question.get("queType"))
    if que_type in LAYOUT_ONLY_QUE_TYPES:
        return False
    return True


def _extract_question_options(question: JSONObject) -> list[str]:
    options = question.get("options")
    if not isinstance(options, list):
        return []
    values: list[str] = []
    for item in options:
        if isinstance(item, dict):
            value = item.get("optValue", item.get("value"))
            if value is not None:
                values.append(_stringify_json(value))
        elif item is not None:
            values.append(_stringify_json(item))
    return values


def _fixed_list_scan_policy() -> JSONObject:
    return {
        "page_size": DEFAULT_LIST_PAGE_SIZE,
        "requested_pages": 1,
        "scan_max_pages": 1,
        "auto_expand_pages": False,
        "public_inputs_exposed": False,
    }


def _fixed_analysis_scan_policy() -> JSONObject:
    return {
        "page_size": DEFAULT_ANALYSIS_PAGE_SIZE,
        "requested_pages": DEFAULT_ANALYSIS_SCAN_MAX_PAGES,
        "scan_max_pages": DEFAULT_ANALYSIS_SCAN_MAX_PAGES,
        "auto_expand_pages": True,
        "public_inputs_exposed": False,
    }


def _list_row_cap_hit(*, returned_items: int, row_cap: int) -> bool:
    return row_cap > 0 and returned_items >= row_cap


def _list_sample_only(*, returned_items: int, row_cap: int, result_amount: int | None) -> bool:
    if _list_row_cap_hit(returned_items=returned_items, row_cap=row_cap):
        return True
    if result_amount is None:
        return False
    return result_amount > returned_items


def _list_sample_warning(*, returned_items: int, row_cap: int, result_amount: int | None) -> str:
    if _list_sample_only(returned_items=returned_items, row_cap=row_cap, result_amount=result_amount):
        return "当前仅返回样本，不适合最终统计结论。"
    return "record_list 适合浏览或样本检查；最终统计结论请改用 record_browse_schema_get -> record_access -> Python。"


def _resolve_query_mode(
    query_mode: str,
    *,
    apply_id: int | None,
    amount_column: JSONValue,
    time_range: JSONObject,
    stat_policy: JSONObject,
) -> str:
    if query_mode in {"list", "record", "summary"}:
        return query_mode
    if apply_id is not None and apply_id > 0:
        return "record"
    return "list"


def _bounded_column_limit(max_columns: int | None, *, default_limit: int, hard_limit: int) -> int:
    if max_columns is None:
        return default_limit
    return max(1, min(max_columns, hard_limit))


def _chunk_fields(fields: list[FormField], chunk_size: int) -> list[list[FormField]]:
    if chunk_size <= 0:
        raise ValueError("chunk_size must be positive")
    if not fields:
        return []
    return [fields[index : index + chunk_size] for index in range(0, len(fields), chunk_size)]


def _record_access_run_dir() -> Path:
    custom_home = os.getenv("QINGFLOW_MCP_RECORD_ACCESS_HOME")
    base_dir = Path(custom_home).expanduser() if custom_home else get_mcp_home() / "record-access"
    run_id = f"{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}-{uuid4().hex[:8]}"
    return base_dir / run_id


def _record_logs_run_dir() -> Path:
    custom_home = os.getenv("QINGFLOW_MCP_RECORD_LOGS_HOME")
    base_dir = Path(custom_home).expanduser() if custom_home else get_mcp_home() / "record-logs"
    run_id = f"{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}-{uuid4().hex[:8]}"
    return base_dir / run_id


def _record_access_field_payload(field: FormField) -> JSONObject:
    return {
        "field_id": field.que_id,
        "title": field.que_title,
        "column_name": _record_access_column_name(field),
        "type": _record_access_field_type(field),
    }


def _record_access_column_name(field: FormField) -> str:
    title = re.sub(r"\s+", " ", field.que_title or "").strip()
    if not title:
        return f"field_{field.que_id}"
    return f"{title}__field_{field.que_id}"


def _record_access_scope_status(filters: list[JSONObject], view_route: AccessibleViewRoute, index: FieldIndex) -> JSONObject:
    has_time_filter = False
    has_explicit_business_filter = False
    for item in filters:
        if not isinstance(item, dict):
            continue
        field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
        field = index.by_id.get(str(field_id)) if field_id is not None else None
        if field is not None and field.que_type in DATE_QUE_TYPES:
            has_time_filter = True
        else:
            has_explicit_business_filter = True
    view_selection = view_route.view_selection
    has_saved_view_filter = bool(view_selection is not None and view_selection.conditions)
    return {
        "filter_count": len(filters),
        "has_explicit_where": bool(filters),
        "has_time_filter": has_time_filter,
        "has_business_filter": bool(has_explicit_business_filter or has_saved_view_filter),
        "has_saved_view_filter": has_saved_view_filter,
    }


def _record_access_light_probe_recommended(scope_status: JSONObject) -> bool:
    return not bool(scope_status.get("has_time_filter")) and not bool(scope_status.get("has_business_filter"))


def _record_access_estimated_pages(page: JSONObject, reported_total: int | None, *, page_size: int) -> int | None:
    page_amount = _coerce_count(page.get("pageAmount"))
    if page_size == BACKEND_RECORD_ACCESS_PAGE_SIZE and page_amount is not None:
        return page_amount
    if reported_total is None or reported_total <= 0:
        return page_amount
    return (reported_total + BACKEND_RECORD_ACCESS_PAGE_SIZE - 1) // BACKEND_RECORD_ACCESS_PAGE_SIZE


def _record_access_suggested_time_fields(index: FieldIndex) -> list[JSONObject]:
    fields: list[JSONObject] = []
    for field in index.by_id.values():
        if field.que_type in DATE_QUE_TYPES:
            fields.append(_record_access_field_payload(field))
    return fields


def _record_access_recommended_where_examples(suggested_time_fields: list[JSONObject]) -> list[JSONObject]:
    if not suggested_time_fields:
        return []
    now = datetime.now(UTC)
    month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    if month_start.month == 12:
        next_month = month_start.replace(year=month_start.year + 1, month=1)
    else:
        next_month = month_start.replace(month=month_start.month + 1)
    month_end = next_month - timedelta(seconds=1)
    examples: list[JSONObject] = []
    for field in suggested_time_fields[:3]:
        field_id = field.get("field_id")
        examples.append(
            {
                "field_id": field_id,
                "title": field.get("title"),
                "where": [
                    {
                        "field_id": field_id,
                        "op": "between",
                        "value": [
                            month_start.strftime("%Y-%m-%d"),
                            month_end.strftime("%Y-%m-%d %H:%M:%S"),
                        ],
                    }
                ],
            }
        )
    return examples


def _record_access_scope_payload(
    *,
    scope_status: JSONObject,
    reported_total: int | None,
    estimated_pages: int | None,
    index: FieldIndex,
) -> JSONObject:
    suggested_time_fields = _record_access_suggested_time_fields(index)
    return {
        "reported_total": reported_total,
        "estimated_pages": estimated_pages,
        "has_time_filter": bool(scope_status.get("has_time_filter")),
        "has_business_filter": bool(scope_status.get("has_business_filter")),
        "suggested_time_fields": suggested_time_fields,
        "recommended_where_examples": _record_access_recommended_where_examples(suggested_time_fields),
    }


def _record_access_needs_scope(*, scope_status: JSONObject, reported_total: int | None) -> bool:
    if reported_total is None or reported_total <= RECORD_ACCESS_UNBOUNDED_ROW_THRESHOLD:
        return False
    return not bool(scope_status.get("has_time_filter")) and not bool(scope_status.get("has_business_filter"))


def _record_access_unbounded_scan_warning(*, reported_total: int | None, estimated_pages: int | None) -> JSONObject:
    page_text = f" across about {estimated_pages} pages" if estimated_pages else ""
    return {
        "code": "UNBOUNDED_SCAN_TOO_LARGE",
        "message": (
            "record_access stopped before writing CSV because this query has no time/business boundary "
            f"and the backend reports {reported_total or 'unknown'} rows{page_text}. "
            "Add a where filter, preferably on a time field, then retry."
        ),
        "row_threshold": RECORD_ACCESS_UNBOUNDED_ROW_THRESHOLD,
        "reported_total": reported_total,
        "estimated_pages": estimated_pages,
    }


def _record_access_time_budget_exceeded(
    deadline: float,
    *,
    fetched_pages: int,
    estimated_pages: int | None,
) -> bool:
    now = time.monotonic()
    if now >= deadline:
        return True
    if estimated_pages is not None and estimated_pages - fetched_pages <= 1:
        return False
    return now + RECORD_ACCESS_MIN_REMAINING_SECONDS >= deadline


def _record_access_time_budget_warning(*, reported_total: int | None, fetched_pages: int) -> JSONObject:
    return {
        "code": "TIME_BUDGET_EXCEEDED",
        "message": (
            "record_access stopped early to return partial CSV files before the caller timeout. "
            "Narrow the query with a time or business filter for a complete result."
        ),
        "reported_total": reported_total,
        "fetched_pages": fetched_pages,
        "time_budget_seconds": RECORD_ACCESS_TIME_BUDGET_SECONDS,
    }


def _record_access_field_type(field: FormField) -> str:
    if field.que_type in DATE_QUE_TYPES:
        return "datetime"
    if field.que_type in NUMBER_QUE_TYPES:
        return "number"
    if field.que_type in MEMBER_QUE_TYPES:
        return "member"
    if field.que_type in DEPARTMENT_QUE_TYPES:
        return "department"
    if field.que_type in ADDRESS_QUE_TYPES:
        return "address"
    if field.que_type in RELATION_QUE_TYPES:
        return "relation"
    if field.que_type in ATTACHMENT_QUE_TYPES:
        return "attachment"
    if field.que_type in SINGLE_SELECT_QUE_TYPES:
        return "single_select"
    if field.que_type in MULTI_SELECT_QUE_TYPES:
        return "multi_select"
    if field.que_type in SUBTABLE_QUE_TYPES:
        return "subtable"
    return "value"


def _record_access_csv_cell(value: JSONValue) -> str:
    if value is None:
        return ""
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, int | float | str):
        return str(value)
    return json.dumps(value, ensure_ascii=False)


def _normalize_record_get_detail_output_profile(output_profile: str | None) -> str:
    normalized = (output_profile or "normal").strip().lower()
    if normalized in {"", "normal"}:
        return "normal"
    if normalized in {"normalized", "verbose", "detail_context"}:
        return normalized
    raise_tool_error(
        QingflowApiError.config_error(
            "record_get output_profile is unsupported.",
            details={
                "fix_hint": "Use output_profile='detail_context' or omit it. Legacy normal/normalized/verbose are accepted but ignored.",
            },
        )
    )
    return "normal"


def _record_detail_role_for_list_type(list_type: int | None) -> int:
    if list_type in {1, 2, 12}:
        return 3
    if list_type in {8, 9, 10, 11, 15}:
        return 1
    if list_type == 13:
        return 5
    return 2


def _record_detail_answers(payload: JSONObject) -> list[JSONValue]:
    answers = payload.get("answers")
    if isinstance(answers, list):
        return cast(list[JSONValue], answers)
    data = payload.get("data")
    if isinstance(data, dict) and isinstance(data.get("answers"), list):
        return cast(list[JSONValue], data.get("answers"))
    result = payload.get("result")
    if isinstance(result, dict) and isinstance(result.get("answers"), list):
        return cast(list[JSONValue], result.get("answers"))
    return []


def _record_detail_field_payload(field: FormField, answer: JSONObject | None, *, focus_id_set: set[str]) -> JSONObject:
    value = _normalize_answer_field_value_for_output(answer, field)
    display_source = _extract_answer_field_value(answer, field) if isinstance(answer, dict) else value
    return {
        "field_id": field.que_id,
        "title": field.que_title,
        "type": _record_access_field_type(field),
        "type_code": field.que_type,
        "value": value,
        "display_value": _record_detail_display_value(display_source if display_source is not None else value),
        "required": field.required,
        "readonly": field.readonly,
        "system": field.system,
        "requested_focus": str(field.que_id) in focus_id_set,
    }


def _record_detail_record_payload(
    *,
    app_key: str,
    record_id: int,
    detail: JSONObject,
    answer_list: list[JSONValue],
    fields: list[JSONObject],
) -> JSONObject:
    field_by_id = {str(item.get("field_id")): item for item in fields if isinstance(item, dict)}
    title = _first_text(
        detail,
        "title",
        "dataTitle",
        "applyTitle",
        "applyName",
        "formTitle",
        "name",
    )
    if not title:
        title = _record_detail_pick_title_from_fields(fields)
    serial_number = _first_text(detail, "applyNum", "serialNumber", "serialNo", "number", "rowNo")
    if not serial_number and isinstance(field_by_id.get("0"), dict):
        serial_number = _normalize_optional_text(cast(JSONObject, field_by_id["0"]).get("display_value"))
    workflow_status = _first_text(detail, "statusText", "applyStatusText", "workflowStatus", "flowStatus", "stateName")
    if not workflow_status:
        workflow_status = _record_detail_status_label(detail.get("applyStatus", detail.get("status")))
    applicant = _record_detail_user_display_name(
        detail.get("applyUser", detail.get("applicant", detail.get("creator", detail.get("user"))))
    )
    if not applicant and isinstance(field_by_id.get("1"), dict):
        applicant = _normalize_optional_text(cast(JSONObject, field_by_id["1"]).get("display_value"))
    apply_time = _record_detail_time_text(
        detail.get("applyTime", detail.get("createTime", detail.get("createdAt", detail.get("gmtCreate"))))
    )
    if not apply_time and isinstance(field_by_id.get("2"), dict):
        apply_time = _normalize_optional_text(cast(JSONObject, field_by_id["2"]).get("display_value"))
    update_time = _record_detail_time_text(
        detail.get("updateTime", detail.get("lastUpdateTime", detail.get("modifiedAt", detail.get("gmtModified"))))
    )
    if not update_time and isinstance(field_by_id.get("3"), dict):
        update_time = _normalize_optional_text(cast(JSONObject, field_by_id["3"]).get("display_value"))
    return {
        "app_key": app_key,
        "record_id": _public_record_id_text(record_id),
        "apply_id": _public_record_id_text(record_id),
        "title": title,
        "serial_number": serial_number,
        "workflow_status": workflow_status,
        "applicant": applicant,
        "apply_time": apply_time,
        "update_time": update_time,
    }


def _record_detail_summary_payload(
    *,
    app_name: str | None,
    view_payload: JSONObject,
    record: JSONObject,
    fields: list[JSONObject],
) -> JSONObject:
    core_fields = _record_detail_core_fields(fields, limit=6)
    summary_text = (
        f"当前记录来自应用「{app_name or record.get('app_key') or '-'}」，"
        f"视图「{view_payload.get('name') or view_payload.get('view_id') or '-'}」。"
        f"记录标题「{record.get('title') or '-'}」，recordId={record.get('record_id') or '-'}。"
    )
    if record.get("serial_number") not in (None, ""):
        summary_text += f"编号 {record.get('serial_number')}。"
    if record.get("workflow_status") not in (None, ""):
        summary_text += f"流程状态「{record.get('workflow_status')}」。"
    if record.get("applicant") not in (None, "") or record.get("apply_time") not in (None, ""):
        summary_text += f"申请人「{record.get('applicant') or '-'}」，申请时间 {record.get('apply_time') or '-'}。"
    if core_fields:
        core_text = "；".join(f"{item.get('title')}为「{item.get('display_value')}」" for item in core_fields if item.get("display_value") not in (None, ""))
        if core_text:
            summary_text += f"核心字段：{core_text}。"
    return {
        "text": summary_text,
        "core_fields": core_fields,
    }


def _record_detail_core_fields(fields: list[JSONObject], *, limit: int) -> list[JSONObject]:
    priority_keywords = ("名称", "编号", "状态", "阶段", "优先级", "金额", "类型", "负责人", "客户", "商机", "项目")
    selected: list[JSONObject] = []
    for field in fields:
        title = str(field.get("title") or "")
        display = field.get("display_value")
        if display in (None, ""):
            continue
        if any(keyword in title for keyword in priority_keywords):
            selected.append(_record_detail_compact_field(field))
        if len(selected) >= limit:
            return selected
    for field in fields:
        display = field.get("display_value")
        if display in (None, ""):
            continue
        compact = _record_detail_compact_field(field)
        if any(item.get("field_id") == compact.get("field_id") for item in selected):
            continue
        selected.append(compact)
        if len(selected) >= limit:
            break
    return selected


def _record_detail_compact_field(field: JSONObject) -> JSONObject:
    return {
        "field_id": field.get("field_id"),
        "title": field.get("title"),
        "type": field.get("type"),
        "display_value": field.get("display_value"),
        "value": field.get("value"),
    }


def _record_detail_pick_title_from_fields(fields: list[JSONObject]) -> str | None:
    priority_keywords = ("名称", "标题", "主题", "客户", "商机", "项目")
    for keyword in priority_keywords:
        for field in fields:
            title = str(field.get("title") or "")
            if keyword in title and field.get("display_value") not in (None, ""):
                return _normalize_optional_text(field.get("display_value"))
    for field in fields:
        if field.get("display_value") not in (None, ""):
            return _normalize_optional_text(field.get("display_value"))
    return None


def _record_detail_display_value(value: JSONValue) -> str | None:
    if value is None:
        return None
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, int | float | str):
        text = str(value).strip()
        return text or None
    if isinstance(value, dict):
        for key in ("label", "name", "title", "display_value", "value"):
            text = _normalize_optional_text(value.get(key))
            if text:
                return text
        return json.dumps(value, ensure_ascii=False)
    if isinstance(value, list):
        parts = [_record_detail_display_value(cast(JSONValue, item)) for item in value]
        parts = [part for part in parts if part]
        return "；".join(parts) if parts else None
    return _stringify_json(value)


def _record_detail_time_text(value: JSONValue) -> str | None:
    if isinstance(value, (int, float)) and value > 10_000_000_000:
        try:
            return datetime.fromtimestamp(float(value) / 1000).strftime("%Y-%m-%d %H:%M:%S")
        except (OverflowError, OSError, ValueError):
            return str(value)
    if isinstance(value, (int, float)) and value > 0:
        try:
            return datetime.fromtimestamp(float(value)).strftime("%Y-%m-%d %H:%M:%S")
        except (OverflowError, OSError, ValueError):
            return str(value)
    return _normalize_optional_text(value)


def _record_detail_user_display_name(value: JSONValue) -> str | None:
    if isinstance(value, dict):
        for key in ("nickName", "nickname", "name", "userName", "realName", "email"):
            text = _normalize_optional_text(value.get(key))
            if text:
                return text
        return _normalize_optional_text(value.get("value"))
    if isinstance(value, list):
        parts = [_record_detail_user_display_name(cast(JSONValue, item)) for item in value]
        parts = [part for part in parts if part]
        return "、".join(parts) if parts else None
    return _normalize_optional_text(value)


def _record_detail_status_label(value: JSONValue) -> str | None:
    text = _normalize_optional_text(value)
    if text and not text.isdecimal():
        return text
    code = _coerce_count(value)
    return {
        1: "草稿",
        2: "流程中",
        3: "已通过",
        4: "已拒绝",
        5: "待完善",
        6: "已通过",
        7: "流程中",
        8: "已结束",
    }.get(code)


def _first_text(payload: JSONObject, *keys: str) -> str | None:
    for key in keys:
        text = _normalize_optional_text(payload.get(key))
        if text:
            return text
    return None


def _first_present(payload: JSONObject, *keys: str) -> JSONValue:
    for key in keys:
        value = payload.get(key)
        if value not in (None, ""):
            return cast(JSONValue, value)
    return None


def _record_detail_audit_context(audit_info: JSONObject, *, workflow_node_id: int | None) -> JSONObject:
    nodes = _record_detail_audit_nodes(audit_info)
    selected: JSONObject | None = None
    if workflow_node_id is not None:
        for node in nodes:
            node_id = _coerce_count(node.get("auditNodeId", node.get("nodeId", node.get("id"))))
            if node_id == workflow_node_id:
                selected = node
                break
    if selected is None and nodes:
        selected = nodes[0]
    audit_node_id = workflow_node_id
    if selected is not None:
        audit_node_id = _coerce_count(selected.get("auditNodeId", selected.get("nodeId", selected.get("id")))) or audit_node_id
    return {
        "raw": audit_info,
        "items_loaded": len(nodes),
        "selected_node": _record_detail_compact_audit_node(selected),
        "audit_node_id": audit_node_id,
    }


def _record_detail_audit_nodes(payload: JSONValue) -> list[JSONObject]:
    if isinstance(payload, list):
        return [item for item in payload if isinstance(item, dict)]
    if not isinstance(payload, dict):
        return []
    for key in ("auditInfos", "auditInfo", "auditNodes", "nodes", "items", "list"):
        value = payload.get(key)
        if isinstance(value, list):
            return [item for item in value if isinstance(item, dict)]
    for key in ("data", "result"):
        value = payload.get(key)
        if isinstance(value, (dict, list)):
            nested = _record_detail_audit_nodes(cast(JSONValue, value))
            if nested:
                return nested
    return []


def _record_detail_compact_audit_node(node: JSONObject | None) -> JSONObject | None:
    if not isinstance(node, dict):
        return None
    node_id = _coerce_count(node.get("auditNodeId", node.get("nodeId", node.get("id"))))
    name = _normalize_optional_text(node.get("auditNodeName", node.get("nodeName", node.get("name"))))
    payload: JSONObject = {}
    if node_id is not None:
        payload["audit_node_id"] = node_id
    if name:
        payload["name"] = name
    if node.get("type") is not None:
        payload["type"] = node.get("type")
    return payload or None


def _record_detail_relation_entries(value: JSONValue) -> list[JSONObject]:
    if isinstance(value, dict):
        return [value]
    if isinstance(value, list):
        return [item for item in value if isinstance(item, dict)]
    return []


def _record_detail_relation_label(entries: list[JSONObject], target_record_id: str) -> str | None:
    for entry in entries:
        apply_id = _normalize_optional_text(entry.get("apply_id", entry.get("applyId")))
        if apply_id == target_record_id:
            return _normalize_optional_text(entry.get("label", entry.get("title", entry.get("name"))))
    return _normalize_optional_text(entries[0].get("label")) if entries else None


def _record_detail_reference_resolution_source(answer: JSONObject) -> str:
    return _normalize_optional_text(answer.get("_recordGetResolutionSource")) or "saved_answer"


def _merge_record_detail_answers(answer_list: list[JSONValue], dynamic_answers: list[JSONObject]) -> list[JSONValue]:
    merged = list(answer_list)
    for dynamic_answer in dynamic_answers:
        dynamic_que_id = _coerce_count(dynamic_answer.get("queId"))
        if dynamic_que_id is None:
            merged.append(dynamic_answer)
            continue
        replace_index: int | None = None
        for index, existing_answer in enumerate(merged):
            if not isinstance(existing_answer, dict):
                continue
            if _coerce_count(existing_answer.get("queId")) != dynamic_que_id:
                continue
            if not _relation_ids_from_answer(existing_answer):
                replace_index = index
            break
        if replace_index is None:
            merged.append(dynamic_answer)
        else:
            merged[replace_index] = dynamic_answer
    return merged


def _record_detail_reference_match_key_values(
    reference: JSONObject,
    *,
    answer_list: list[JSONValue],
    selected_field_by_id: dict[int, FormField],
) -> tuple[list[JSONObject], list[int]]:
    source_field_ids = _record_detail_reference_match_source_field_ids(reference.get("referMatchRules"))
    if not source_field_ids:
        return [], []
    key_values: list[JSONObject] = []
    missing_sources: list[int] = []
    for source_field_id in source_field_ids:
        source_field = selected_field_by_id.get(source_field_id)
        answer = _find_answer_by_que_id(answer_list, source_field_id)
        values = _record_detail_answer_key_values(answer, source_field)
        if not values:
            missing_sources.append(source_field_id)
            continue
        key_values.append({"keyQueId": source_field_id, "values": values, "ordinal": None})
    return key_values, missing_sources


def _record_detail_reference_match_source_field_ids(raw_rules: JSONValue) -> list[int]:
    source_field_ids: list[int] = []
    seen: set[int] = set()

    def visit(node: JSONValue) -> None:
        if isinstance(node, list):
            for item in node:
                visit(item)
            return
        if not isinstance(node, dict):
            return
        if _coerce_count(node.get("matchType")) != 2:
            return
        judge_detail = node.get("judgeQueDetail")
        source_field_id = _coerce_count(judge_detail.get("queId")) if isinstance(judge_detail, dict) else None
        if source_field_id is None:
            judge_values = node.get("judgeValues")
            if isinstance(judge_values, list) and judge_values:
                source_field_id = _coerce_count(judge_values[0])
        if source_field_id is None or source_field_id in seen:
            return
        seen.add(source_field_id)
        source_field_ids.append(source_field_id)

    visit(raw_rules)
    return source_field_ids


def _find_answer_by_que_id(answer_list: list[JSONValue], que_id: int) -> JSONObject | None:
    for answer in answer_list:
        if not isinstance(answer, dict):
            continue
        if _coerce_count(answer.get("queId")) == que_id:
            return answer
    return None


def _record_detail_answer_key_values(answer: JSONObject | None, field: FormField | None) -> list[str]:
    if not isinstance(answer, dict):
        return []
    if (field is not None and field.que_type in RELATION_QUE_TYPES) or isinstance(answer.get("referValues"), list):
        return _relation_ids_from_answer(answer)
    values = answer.get("values")
    normalized_values: list[str] = []
    seen: set[str] = set()
    if isinstance(values, list):
        for item in values:
            raw_value: JSONValue
            if isinstance(item, dict):
                raw_value = item.get("value")
                if raw_value in (None, ""):
                    raw_value = item.get("dataValue")
                if raw_value in (None, ""):
                    raw_value = item.get("id")
            else:
                raw_value = item
            text = _normalize_optional_text(raw_value) or (_stringify_json(raw_value) if raw_value is not None else None)
            if not text or text in seen:
                continue
            seen.add(text)
            normalized_values.append(text)
    if normalized_values:
        return normalized_values
    source_value = answer.get("sourceValue")
    text = _normalize_optional_text(source_value) or (_stringify_json(source_value) if source_value is not None else None)
    return [text] if text else []


def _merge_string_lists(left: JSONValue, right: JSONValue) -> list[str]:
    merged: list[str] = []
    seen: set[str] = set()
    for values in (left, right):
        if not isinstance(values, list):
            continue
        for value in values:
            text = _normalize_optional_text(value) or (_stringify_json(value) if value is not None else None)
            if not text or text in seen:
                continue
            seen.add(text)
            merged.append(text)
    return merged


def _record_detail_dynamic_reference_answers_from_payload(payload: JSONValue, *, query_field_ids: set[int]) -> list[JSONObject]:
    answers: list[JSONObject] = []
    seen: set[tuple[int, tuple[str, ...]]] = set()

    def add_answer(answer: JSONObject) -> None:
        que_id = _coerce_count(answer.get("queId"))
        if que_id is None or que_id not in query_field_ids:
            return
        relation_ids = tuple(_relation_ids_from_answer(answer))
        if not relation_ids:
            return
        identity = (que_id, relation_ids)
        if identity in seen:
            return
        seen.add(identity)
        answers.append(dict(answer))

    def visit(node: JSONValue) -> None:
        if isinstance(node, list):
            for item in node:
                visit(item)
            return
        if not isinstance(node, dict):
            return
        if "queId" in node and ("referValues" in node or "values" in node):
            add_answer(node)
        raw_answers = node.get("answers")
        if isinstance(raw_answers, list):
            for raw_answer in raw_answers:
                if isinstance(raw_answer, dict):
                    add_answer(raw_answer)
        for key in ("result", "list", "applyResultList", "data"):
            child = node.get(key)
            if isinstance(child, (dict, list)):
                visit(child)

    visit(payload)
    return answers


def _record_detail_channel_payload(
    *,
    app_key: str,
    resolved_view: AccessibleViewRoute,
    audit_node_id: int | None,
    default_channel: str,
) -> JSONObject:
    if audit_node_id is not None:
        return {"channel": "NODE", "channelKey": str(audit_node_id)}
    if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
        return {"channel": "VIEW", "channelKey": resolved_view.view_selection.view_key}
    return {"channel": default_channel, "channelKey": app_key}


def _record_detail_visibility_value(payload: JSONObject, *, keys: tuple[str, ...], default: bool) -> bool:
    for key in keys:
        value = payload.get(key)
        if isinstance(value, bool):
            return value
        text = _normalize_optional_text(value)
        if text is not None:
            lowered = text.lower()
            if lowered in {"true", "1", "yes"}:
                return True
            if lowered in {"false", "0", "no"}:
                return False
    data = payload.get("data")
    if isinstance(data, dict):
        return _record_detail_visibility_value(data, keys=keys, default=default)
    result = payload.get("result")
    if isinstance(result, dict):
        return _record_detail_visibility_value(result, keys=keys, default=default)
    return default


def _record_detail_log_hidden_payload() -> JSONObject:
    return {
        "status": "hidden",
        "visible": False,
        "page": 1,
        "page_size": RECORD_GET_DETAIL_LOG_PAGE_SIZE,
        "items_loaded": 0,
        "has_more": False,
        "complete": False,
        "items": [],
    }


def _record_detail_log_unavailable_payload(source: str, reason: str) -> JSONObject:
    return {
        "status": "unavailable",
        "visible": None,
        "source": source,
        "reason": reason,
        "page": 1,
        "page_size": RECORD_GET_DETAIL_LOG_PAGE_SIZE,
        "items_loaded": 0,
        "has_more": None,
        "complete": False,
        "items": [],
    }


def _record_logs_hidden_payload(source: str) -> JSONObject:
    return {
        "status": "hidden",
        "visible": False,
        "source": source,
        "complete": False,
        "items_count": 0,
        "pages_fetched": 0,
        "reported_total": None,
        "local_path": None,
        "preview_items": [],
        "warnings": [],
    }


def _record_logs_unavailable_payload(source: str, reason: str) -> JSONObject:
    return {
        "status": "unavailable",
        "visible": None,
        "source": source,
        "reason": reason,
        "complete": False,
        "items_count": 0,
        "pages_fetched": 0,
        "reported_total": None,
        "local_path": None,
        "preview_items": [],
        "warnings": [],
    }


def _record_logs_fetch_all_to_jsonl(
    *,
    fetch_page,
    normalizer,
    source: str,
    file_path: Path,
    deadline: float,
) -> JSONObject:  # type: ignore[no-untyped-def]
    file_path.parent.mkdir(parents=True, exist_ok=True)
    page_num = 1
    pages_fetched = 0
    items_count = 0
    reported_total: int | None = None
    preview_items: list[JSONObject] = []
    warnings: list[JSONObject] = []
    stopped_reason: str | None = None
    complete = True

    with file_path.open("w", encoding="utf-8") as handle:
        while True:
            if _record_logs_time_budget_exceeded(deadline=deadline):
                complete = False
                stopped_reason = "time_budget_exceeded"
                warnings.append(_record_logs_time_budget_warning(source=source, pages_fetched=pages_fetched, items_count=items_count))
                break
            payload = fetch_page(page_num)
            pages_fetched += 1
            items = _record_detail_page_items(payload)
            if reported_total is None:
                reported_total = _record_detail_page_total(payload)
            if not items:
                break
            for item in items:
                normalized = normalizer(item)
                handle.write(json.dumps(normalized, ensure_ascii=False) + "\n")
                items_count += 1
                if len(preview_items) < RECORD_LOGS_PREVIEW_LIMIT:
                    preview_items.append(normalized)
                if items_count >= RECORD_LOGS_MAX_ITEMS:
                    complete = False
                    stopped_reason = "item_limit_exceeded"
                    warnings.append(_record_logs_item_limit_warning(source=source, item_limit=RECORD_LOGS_MAX_ITEMS))
                    break
            if stopped_reason:
                break
            if reported_total is not None and items_count >= reported_total:
                break
            if reported_total is None and len(items) < RECORD_LOGS_PAGE_SIZE:
                break
            page_num += 1

    return {
        "status": "ok" if complete else "partial",
        "visible": True,
        "source": source,
        "complete": complete,
        "items_count": items_count,
        "pages_fetched": pages_fetched,
        "page_size": RECORD_LOGS_PAGE_SIZE,
        "reported_total": reported_total,
        "local_path": str(file_path),
        "preview_items": preview_items,
        "warnings": warnings,
        "stopped_reason": stopped_reason,
    }


def _record_logs_time_budget_exceeded(*, deadline: float) -> bool:
    return time.monotonic() + RECORD_LOGS_MIN_REMAINING_SECONDS >= deadline


def _record_logs_time_budget_warning(*, source: str, pages_fetched: int, items_count: int) -> JSONObject:
    return {
        "code": "RECORD_LOGS_TIME_BUDGET_EXCEEDED",
        "source": source,
        "message": "record_logs_get stopped early to return partial JSONL files before the caller timeout.",
        "pages_fetched": pages_fetched,
        "items_count": items_count,
    }


def _record_logs_item_limit_warning(*, source: str, item_limit: int) -> JSONObject:
    return {
        "code": "RECORD_LOGS_ITEM_LIMIT_EXCEEDED",
        "source": source,
        "message": f"record_logs_get stopped after the internal {item_limit} item limit.",
        "item_limit": item_limit,
    }


def _record_logs_overall_status(*, data_logs: JSONObject, workflow_logs: JSONObject) -> str:
    statuses = {str(data_logs.get("status") or ""), str(workflow_logs.get("status") or "")}
    if statuses == {"unavailable"}:
        return "unavailable"
    if "partial" in statuses or "unavailable" in statuses:
        return "partial"
    return "success"


def _record_logs_context_integrity(*, data_logs: JSONObject, workflow_logs: JSONObject) -> JSONObject:
    data_integrity = _record_logs_section_integrity(data_logs)
    workflow_integrity = _record_logs_section_integrity(workflow_logs)
    return {
        "data_logs": data_integrity,
        "workflow_logs": workflow_integrity,
        "safe_for_full_log_conclusion": data_integrity == "full" and workflow_integrity == "full",
    }


def _record_logs_section_integrity(section: JSONObject) -> str:
    status = str(section.get("status") or "")
    if status == "ok" and section.get("complete") is True:
        return "full"
    if status == "hidden":
        return "hidden"
    if status == "partial":
        return "partial"
    if status == "unavailable":
        return "unavailable"
    return "unknown"


def _record_detail_log_page_payload(payload: JSONValue, *, normalizer, source: str) -> JSONObject:  # type: ignore[no-untyped-def]
    items = _record_detail_page_items(payload)
    total = _record_detail_page_total(payload)
    has_more = _record_detail_page_has_more(total=total, loaded=len(items))
    return {
        "status": "ok",
        "visible": True,
        "source": source,
        "page": 1,
        "page_size": RECORD_GET_DETAIL_LOG_PAGE_SIZE,
        "items_loaded": len(items),
        "reported_total": total,
        "has_more": has_more,
        "complete": False,
        "items": [normalizer(item) for item in items[:RECORD_GET_DETAIL_LOG_PAGE_SIZE]],
    }


def _record_detail_page_items(payload: JSONValue) -> list[JSONObject]:
    if isinstance(payload, list):
        return [item for item in payload if isinstance(item, dict)]
    if not isinstance(payload, dict):
        return []
    for key in ("items", "list", "records", "logs", "rows", "content"):
        value = payload.get(key)
        if isinstance(value, list):
            return [item for item in value if isinstance(item, dict)]
    for key in ("data", "result", "page"):
        value = payload.get(key)
        if isinstance(value, (dict, list)):
            nested = _record_detail_page_items(cast(JSONValue, value))
            if nested:
                return nested
    return []


def _record_detail_page_total(payload: JSONValue) -> int | None:
    if not isinstance(payload, dict):
        return None
    for key in ("total", "totalCount", "resultAmount", "result_amount", "amount", "count"):
        value = _coerce_count(payload.get(key))
        if value is not None:
            return value
    for key in ("data", "result", "page"):
        value = payload.get(key)
        if isinstance(value, dict):
            total = _record_detail_page_total(value)
            if total is not None:
                return total
    return None


def _record_detail_page_has_more(*, total: int | None, loaded: int) -> bool:
    if total is None:
        return loaded >= RECORD_GET_DETAIL_LOG_PAGE_SIZE
    return total > loaded


def _record_detail_data_log_item(item: JSONObject) -> JSONObject:
    changed_fields = _record_detail_changed_fields(item)
    return {
        "time": _record_detail_time_text(_first_present(item, "time", "createTime", "operateTime", "gmtCreate")),
        "operator": _record_detail_user_display_name(_first_present(item, "operator", "creator", "user", "createUser")),
        "channel": _normalize_optional_text(_first_present(item, "channel", "source", "from")),
        "action": _normalize_optional_text(_first_present(item, "action", "operation", "operateType", "type")),
        "summary": _normalize_optional_text(_first_present(item, "summary", "content", "description")),
        "changed_fields": changed_fields,
    }


def _record_detail_workflow_log_item(item: JSONObject) -> JSONObject:
    return {
        "time": _record_detail_time_text(_first_present(item, "time", "createTime", "operateTime", "gmtCreate")),
        "operator": _record_detail_user_display_name(_first_present(item, "operator", "creator", "user", "auditor")),
        "node_name": _normalize_optional_text(_first_present(item, "nodeName", "auditNodeName", "name")),
        "action": _normalize_optional_text(_first_present(item, "action", "operation", "dealTypeName", "type")),
        "summary": _normalize_optional_text(_first_present(item, "summary", "content", "remark", "comment")),
    }


def _record_detail_changed_fields(item: JSONObject) -> list[JSONObject]:
    candidates: list[JSONValue] = []
    for key in ("changedFields", "changeFields", "changeQues", "details", "fieldLogs", "values"):
        value = item.get(key)
        if isinstance(value, list):
            candidates.extend(value)
    fields: list[JSONObject] = []
    for raw in candidates:
        if not isinstance(raw, dict):
            continue
        field_id = _coerce_count(raw.get("queId", raw.get("fieldId", raw.get("field_id"))))
        title = _normalize_optional_text(raw.get("queTitle", raw.get("title", raw.get("fieldTitle"))))
        old_value = raw.get("oldValue", raw.get("before", raw.get("from")))
        new_value = raw.get("newValue", raw.get("after", raw.get("to", raw.get("value"))))
        payload: JSONObject = {}
        if field_id is not None:
            payload["field_id"] = field_id
        if title:
            payload["title"] = title
        if old_value is not None:
            payload["old_value"] = _record_detail_display_value(cast(JSONValue, old_value))
        if new_value is not None:
            payload["new_value"] = _record_detail_display_value(cast(JSONValue, new_value))
        if payload:
            fields.append(payload)
    return fields


def _record_detail_associated_resource_items(payload: JSONValue) -> list[JSONObject]:
    if isinstance(payload, list):
        return [item for item in payload if isinstance(item, dict)]
    if not isinstance(payload, dict):
        return []
    for key in ("asosCharts", "items", "list", "resources"):
        value = payload.get(key)
        if isinstance(value, list):
            return [item for item in value if isinstance(item, dict)]
    for key in ("data", "result"):
        value = payload.get(key)
        if isinstance(value, (dict, list)):
            nested = _record_detail_associated_resource_items(cast(JSONValue, value))
            if nested:
                return nested
    return []


def _record_detail_associated_resource(raw: JSONObject) -> JSONObject:
    graph_type = str(raw.get("graphType") or "").strip().lower()
    view_key = _normalize_optional_text(raw.get("viewKey", raw.get("viewgraphKey")))
    chart_key = _normalize_optional_text(raw.get("chartKey", raw.get("chartId")))
    is_view = bool(view_key) or graph_type.endswith("view") or graph_type == "view"
    if is_view and not view_key and chart_key:
        view_key = chart_key
        chart_key = None
    resource_type = "view" if is_view else "report"
    view_type = _normalize_optional_text(raw.get("viewType", raw.get("viewgraphType", raw.get("graphType"))))
    data_access = _record_detail_resource_data_access(resource_type=resource_type, view_type=view_type)
    return {
        "type": resource_type,
        "resource_type": resource_type,
        "name": _normalize_optional_text(raw.get("viewName", raw.get("chartName", raw.get("name", raw.get("title"))))),
        "app_key": _normalize_optional_text(raw.get("appKey", raw.get("targetAppKey"))),
        "app_name": _normalize_optional_text(raw.get("formTitle", raw.get("appName", raw.get("targetAppName")))),
        "view_key": view_key,
        "chart_key": chart_key,
        "view_type": view_type,
        "graph_type": raw.get("graphType"),
        "report_source": _record_detail_report_source(raw.get("sourceType")) if resource_type == "report" else None,
        "data_access": data_access,
    }


def _record_detail_report_source(source_type: Any) -> str:
    return "dataset" if str(source_type or "").strip().upper() == "BI_DATASET" else "app"


def _record_detail_resource_data_access(*, resource_type: str, view_type: str | None) -> JSONObject:
    if resource_type == "report":
        return {
            "record_readable": False,
            "read_mode": "aggregate",
            "description": "可读取报表配置和聚合结果；明细数据需根据报表数据源继续查询。",
        }
    normalized_view_type = (view_type or "").lower()
    if "gantt" in normalized_view_type or "board" in normalized_view_type or "kanban" in normalized_view_type:
        return {
            "record_readable": False,
            "read_mode": "config_only",
            "description": "仅可读取视图配置，不支持直接读取记录列表；如需明细数据，请改用同应用的表格视图或全部数据视图。",
        }
    return {
        "record_readable": True,
        "read_mode": "record_access",
        "description": "可读取该视图下的记录数据。",
    }


def _record_detail_unavailable_context(section: str, message: str, exc: QingflowApiError) -> JSONObject:
    payload: JSONObject = {
        "section": section,
        "message": message,
        "category": exc.category,
    }
    if is_auth_like_error(exc):
        payload["auth_like"] = True
        payload["error_code"] = "AUTH_REQUIRED"
    if exc.backend_code is not None:
        payload["backend_code"] = exc.backend_code
    if exc.http_status is not None:
        payload["http_status"] = exc.http_status
    request_id = getattr(exc, "request_id", None)
    if request_id:
        payload["request_id"] = request_id
    details = exc.details if isinstance(exc.details, dict) else {}
    error_code = details.get("error_code")
    if error_code and not payload.get("error_code"):
        payload["error_code"] = error_code
    return payload


def _record_detail_error_warning_fields(exc: QingflowApiError) -> JSONObject:
    payload: JSONObject = {"category": exc.category}
    if is_auth_like_error(exc):
        payload["auth_like"] = True
        payload["error_code"] = "AUTH_REQUIRED"
    if exc.backend_code is not None:
        payload["backend_code"] = exc.backend_code
    if exc.http_status is not None:
        payload["http_status"] = exc.http_status
    request_id = getattr(exc, "request_id", None)
    if request_id:
        payload["request_id"] = request_id
    details = exc.details if isinstance(exc.details, dict) else {}
    error_code = details.get("error_code")
    if error_code and not payload.get("error_code"):
        payload["error_code"] = error_code
    return payload


def _record_write_readback_error_payload(exc: QingflowApiError) -> JSONObject:
    payload: JSONObject = {
        "category": exc.category,
        "message": exc.message,
    }
    payload.update(_record_detail_error_warning_fields(exc))
    return payload


def _record_detail_refreshed_source_url(refresh_result: Any) -> str | None:
    if isinstance(refresh_result, dict):
        return _normalize_optional_text(refresh_result.get("source_url"))
    return _normalize_optional_text(refresh_result)


def _record_detail_append_refresh_warning(
    warnings: list[JSONObject],
    refresh_result: Any,
    *,
    id_key: str,
    id_value: str,
) -> None:
    if not isinstance(refresh_result, dict):
        return
    warning = refresh_result.get("warning")
    if not isinstance(warning, dict):
        return
    payload: JSONObject = dict(warning)
    payload.setdefault("code", "ASSET_STORAGE_URL_REFRESH_FAILED")
    payload.setdefault(id_key, id_value)
    warnings.append(payload)


_RECORD_MEDIA_IMG_SRC_RE = re.compile(r"""<img\b[^>]*\bsrc\s*=\s*["']?([^"'\s>]+)""", re.IGNORECASE)
_RECORD_MEDIA_MD_IMAGE_RE = re.compile(r"""!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)""")
_RECORD_MEDIA_URL_RE = re.compile(r"""https?://[^\s<>"')\]]+""", re.IGNORECASE)
_RECORD_MEDIA_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"}
_RECORD_FILE_EXTENSIONS = _RECORD_MEDIA_IMAGE_EXTENSIONS | {
    ".csv",
    ".doc",
    ".docx",
    ".json",
    ".md",
    ".pdf",
    ".text",
    ".txt",
    ".xls",
    ".xlsm",
    ".xlsx",
}
_RECORD_MEDIA_IMAGE_URL_KEYS = {
    "image",
    "imageurl",
    "image_url",
    "img",
    "imgurl",
    "img_url",
    "preview",
    "previewurl",
    "preview_url",
    "thumbnail",
    "thumbnailurl",
    "thumbnail_url",
    "fileurl",
    "file_url",
    "src",
    "url",
    "value",
}
_RECORD_FILE_URL_KEYS = _RECORD_MEDIA_IMAGE_URL_KEYS | {
    "downloadurl",
    "download_url",
    "file",
    "href",
    "link",
    "path",
}
_RECORD_FILE_NAME_KEYS = {"name", "otherinfo", "filename", "file_name", "title"}


def _record_detail_media_assets_payload(
    *,
    backend: Any,
    context: BackendRequestContext,
    app_key: str,
    record_id: int,
    fields: list[JSONObject],
    references: list[JSONObject],
    refresh_source_url: Any | None = None,
) -> JSONObject:
    candidates: list[JSONObject] = []
    source_record_id = _public_record_id_text(record_id)
    for field in fields:
        if isinstance(field, dict):
            candidates.extend(
                _record_detail_media_candidates_from_field(
                    field,
                    source_app_key=app_key,
                    source_record_id=source_record_id,
                    forced_source=None,
                )
            )
    for reference in references:
        if not isinstance(reference, dict):
            continue
        target_fields = reference.get("target_fields") if isinstance(reference.get("target_fields"), list) else []
        target_app_key = _normalize_optional_text(reference.get("target_app_key")) or app_key
        target_record_id = _normalize_optional_text(reference.get("target_record_id"))
        for field in target_fields:
            if isinstance(field, dict):
                candidates.extend(
                    _record_detail_media_candidates_from_field(
                        field,
                        source_app_key=target_app_key,
                        source_record_id=target_record_id,
                        forced_source="reference_target",
                    )
                )
    if not candidates:
        return {"status": "none", "local_dir": None, "items": [], "warnings": []}

    local_dir = _record_detail_media_assets_dir(uuid4().hex)
    local_dir_created = False
    items: list[JSONObject] = []
    warnings: list[JSONObject] = []
    asset_by_url: dict[str, str] = {}
    total_bytes = 0
    image_count = 0
    environment_prefix_cache: dict[str, str] = {}

    def ensure_local_dir() -> None:
        nonlocal local_dir_created
        if not local_dir_created:
            local_dir.mkdir(parents=True, exist_ok=True)
            local_dir_created = True

    for candidate in candidates:
        source_url = _normalize_optional_text(candidate.get("source_url"))
        owner = candidate.get("_owner")
        if not source_url or not isinstance(owner, dict):
            continue
        existing_asset_id = asset_by_url.get(source_url)
        if existing_asset_id:
            _record_detail_attach_asset_id(owner, existing_asset_id)
            continue
        asset_id = f"img_{len(items) + 1:04d}"
        asset_by_url[source_url] = asset_id
        _record_detail_attach_asset_id(owner, asset_id)
        base_item = _record_detail_media_asset_base_item(candidate, asset_id=asset_id)

        if image_count >= RECORD_GET_MEDIA_MAX_IMAGES:
            items.append(
                {
                    **base_item,
                    "local_path": None,
                    "mime_type": None,
                    "size_bytes": None,
                    "access_status": "too_large",
                    "download_strategy": "skipped_limit",
                    "readable_by_agent": False,
                }
            )
            warnings.append(
                {
                    "code": "MEDIA_ASSET_LIMIT_EXCEEDED",
                    "message": f"record_get stopped downloading images after {RECORD_GET_MEDIA_MAX_IMAGES} assets.",
                }
            )
            continue

        download_strategy = _record_detail_media_download_strategy(source_url)
        download_succeeded = False
        content: bytes = b""
        download_meta: JSONObject = {}
        try:
            content, download_meta = _record_detail_download_media_content(
                backend=backend,
                context=context,
                source_url=source_url,
                warnings=warnings,
                environment_prefix_cache=environment_prefix_cache,
                requested_strategy=download_strategy,
            )
            download_succeeded = True
        except QingflowApiError as exc:
            blocked = exc.http_status in {401, 403}
            if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
                refresh_result = refresh_source_url(candidate)
                _record_detail_append_refresh_warning(
                    warnings,
                    refresh_result,
                    id_key="asset_id",
                    id_value=asset_id,
                )
                refreshed_url = _record_detail_refreshed_source_url(refresh_result)
                if refreshed_url and refreshed_url != source_url:
                    refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
                    try:
                        content, download_meta = _record_detail_download_media_content(
                            backend=backend,
                            context=context,
                            source_url=refreshed_url,
                            warnings=warnings,
                            environment_prefix_cache=environment_prefix_cache,
                            requested_strategy=download_strategy if download_strategy == "decrypted_file_url_then_storage_cookie_redirect" else refreshed_strategy,
                        )
                        source_url = refreshed_url
                        base_item["source_url"] = refreshed_url
                        download_succeeded = True
                    except QingflowApiError as refreshed_exc:
                        exc = refreshed_exc
                        blocked = exc.http_status in {401, 403}
                    else:
                        warnings.append(
                            {
                                "code": "MEDIA_ASSET_STORAGE_URL_REFRESHED",
                                "asset_id": asset_id,
                                "message": "record_get refreshed the record detail once before downloading this media asset.",
                            }
                        )
            if not download_succeeded:
                warning_code = "STORAGE_COOKIE_AUTH_FAILED" if blocked and download_strategy != "referer_acl" else "MEDIA_ASSET_DOWNLOAD_FAILED"
                items.append(
                    {
                        **base_item,
                        "storage_auth_type": _record_detail_storage_auth_type(source_url),
                        "storage_cookie_prefix": environment_prefix_cache.get("value"),
                        "redirected": False,
                        "local_path": None,
                        "mime_type": None,
                        "size_bytes": None,
                        "access_status": "blocked_private_url" if blocked else "download_failed",
                        "download_strategy": download_strategy,
                        "readable_by_agent": False,
                    }
                )
                warning: JSONObject = {
                    "code": warning_code,
                    "asset_id": asset_id,
                    "message": f"record_get could not download image asset {asset_id}: {exc.message}",
                }
                warning.update(_record_detail_error_warning_fields(exc))
                warnings.append(warning)
                continue

        if not isinstance(content, bytes):
            content = bytes(content or b"")
        mime_type = _record_detail_image_mime_from_bytes(content)
        if not mime_type:
            items.append(
                {
                    **base_item,
                    **download_meta,
                    "local_path": None,
                    "mime_type": _record_detail_mime_from_url(source_url),
                    "size_bytes": len(content),
                    "access_status": "skipped_non_image",
                    "readable_by_agent": False,
                }
            )
            continue
        if len(content) > RECORD_GET_MEDIA_MAX_IMAGE_BYTES or total_bytes + len(content) > RECORD_GET_MEDIA_MAX_TOTAL_BYTES:
            items.append(
                {
                    **base_item,
                    **download_meta,
                    "local_path": None,
                    "mime_type": mime_type,
                    "size_bytes": len(content),
                    "access_status": "too_large",
                    "readable_by_agent": False,
                }
            )
            warnings.append(
                {
                    "code": "MEDIA_ASSET_SIZE_LIMIT_EXCEEDED",
                    "asset_id": asset_id,
                    "message": "record_get skipped an image asset because it exceeded the internal media size budget.",
                }
            )
            continue

        ensure_local_dir()
        extension = _record_detail_image_extension(mime_type, source_url)
        local_path = local_dir / f"{asset_id}{extension}"
        local_path.write_bytes(content)
        total_bytes += len(content)
        image_count += 1
        items.append(
            {
                **base_item,
                **download_meta,
                "local_path": str(local_path),
                "mime_type": mime_type,
                "size_bytes": len(content),
                "access_status": "downloaded",
                "readable_by_agent": True,
            }
        )

    if not items:
        status = "none"
    elif any(item.get("access_status") != "downloaded" for item in items):
        status = "partial"
    else:
        status = "ok"
    return {"status": status, "local_dir": str(local_dir) if items else None, "items": items, "warnings": warnings}


def _record_detail_file_assets_payload(
    *,
    backend: Any,
    context: BackendRequestContext,
    app_key: str,
    record_id: int,
    fields: list[JSONObject],
    references: list[JSONObject],
    media_assets: JSONObject,
    refresh_source_url: Any | None = None,
) -> JSONObject:
    candidates: list[JSONObject] = []
    source_record_id = _public_record_id_text(record_id)
    for field in fields:
        if isinstance(field, dict):
            candidates.extend(
                _record_detail_file_candidates_from_field(
                    field,
                    source_app_key=app_key,
                    source_record_id=source_record_id,
                    forced_source=None,
                )
            )
    for reference in references:
        if not isinstance(reference, dict):
            continue
        target_fields = reference.get("target_fields") if isinstance(reference.get("target_fields"), list) else []
        target_app_key = _normalize_optional_text(reference.get("target_app_key")) or app_key
        target_record_id = _normalize_optional_text(reference.get("target_record_id"))
        for field in target_fields:
            if isinstance(field, dict):
                candidates.extend(
                    _record_detail_file_candidates_from_field(
                        field,
                        source_app_key=target_app_key,
                        source_record_id=target_record_id,
                        forced_source="reference_target",
                    )
                )
    if not candidates:
        return {"status": "none", "local_dir": None, "items": [], "warnings": []}

    local_dir = _record_detail_file_assets_dir(uuid4().hex)
    local_dir_created = False
    items: list[JSONObject] = []
    warnings: list[JSONObject] = []
    file_by_url: dict[str, str] = {}
    media_by_url = _record_detail_media_assets_by_url(media_assets)
    media_by_asset_id = _record_detail_media_assets_by_asset_id(media_assets)
    total_bytes = 0
    downloaded_count = 0
    deadline = time.monotonic() + RECORD_GET_FILE_TIME_BUDGET_SECONDS
    stopped_for_time_budget = False
    environment_prefix_cache: dict[str, str] = {}

    def ensure_local_dir() -> None:
        nonlocal local_dir_created
        if not local_dir_created:
            local_dir.mkdir(parents=True, exist_ok=True)
            local_dir_created = True

    for candidate in candidates:
        if items and time.monotonic() + RECORD_GET_FILE_MIN_REMAINING_SECONDS >= deadline:
            stopped_for_time_budget = True
            warnings.append(
                {
                    "code": "FILE_ASSET_TIME_BUDGET_EXCEEDED",
                    "message": "record_get stopped downloading additional file assets to stay within the internal time budget.",
                    "time_budget_seconds": RECORD_GET_FILE_TIME_BUDGET_SECONDS,
                }
            )
            break
        source_url = _normalize_optional_text(candidate.get("source_url"))
        owner = candidate.get("_owner")
        if not source_url or not isinstance(owner, dict):
            continue
        existing_asset_id = file_by_url.get(source_url)
        if existing_asset_id:
            _record_detail_attach_file_asset_id(owner, existing_asset_id)
            continue
        file_asset_id = f"file_{len(items) + 1:04d}"
        file_by_url[source_url] = file_asset_id
        _record_detail_attach_file_asset_id(owner, file_asset_id)
        base_item = _record_detail_file_asset_base_item(candidate, file_asset_id=file_asset_id)

        media_item = media_by_url.get(source_url)
        if media_item is None:
            media_item = _record_detail_media_item_from_owner_asset_ids(owner, media_by_asset_id, candidate)
        if isinstance(media_item, dict) and media_item.get("asset_id") not in (None, ""):
            base_item["media_asset_id"] = media_item.get("asset_id")
        if downloaded_count >= RECORD_GET_FILE_MAX_FILES:
            items.append(
                {
                    **base_item,
                    "local_path": None,
                    "mime_type": None,
                    "size_bytes": None,
                    "access_status": "too_large",
                    "download_strategy": "skipped_limit",
                    "readable_by_agent": False,
                    "extraction": {"status": "skipped_too_large", "text_path": None, "preview": None},
                }
            )
            warnings.append(
                {
                    "code": "FILE_ASSET_LIMIT_EXCEEDED",
                    "message": f"record_get stopped downloading files after {RECORD_GET_FILE_MAX_FILES} assets.",
                }
            )
            continue

        reused_media_path = _normalize_optional_text(media_item.get("local_path")) if isinstance(media_item, dict) else None
        if reused_media_path and media_item.get("access_status") == "downloaded":
            file_name = _record_detail_file_name_from_candidate(candidate, source_url=source_url, fallback_id=file_asset_id)
            mime_type = _normalize_optional_text(media_item.get("mime_type")) or _record_detail_mime_from_url(source_url)
            items.append(
                {
                    **base_item,
                    "download_strategy": media_item.get("download_strategy"),
                    "storage_auth_type": media_item.get("storage_auth_type"),
                    "storage_cookie_prefix": media_item.get("storage_cookie_prefix"),
                    "redirected": media_item.get("redirected"),
                    "file_name": file_name,
                    "local_path": reused_media_path,
                    "mime_type": mime_type,
                    "size_bytes": media_item.get("size_bytes"),
                    "access_status": "downloaded",
                    "readable_by_agent": True,
                    "extraction": {"status": "unsupported", "text_path": None, "preview": None},
                }
            )
            downloaded_count += 1
            continue

        download_strategy = _record_detail_media_download_strategy(source_url)
        download_succeeded = False
        content: bytes = b""
        download_meta: JSONObject = {}
        try:
            content, download_meta = _record_detail_download_media_content(
                backend=backend,
                context=context,
                source_url=source_url,
                warnings=warnings,
                environment_prefix_cache=environment_prefix_cache,
                requested_strategy=download_strategy,
            )
            download_succeeded = True
        except QingflowApiError as exc:
            blocked = exc.http_status in {401, 403}
            if blocked and download_strategy != "referer_acl" and callable(refresh_source_url):
                refresh_result = refresh_source_url(candidate)
                _record_detail_append_refresh_warning(
                    warnings,
                    refresh_result,
                    id_key="file_asset_id",
                    id_value=file_asset_id,
                )
                refreshed_url = _record_detail_refreshed_source_url(refresh_result)
                if refreshed_url and refreshed_url != source_url:
                    refreshed_strategy = _record_detail_media_download_strategy(refreshed_url)
                    try:
                        content, download_meta = _record_detail_download_media_content(
                            backend=backend,
                            context=context,
                            source_url=refreshed_url,
                            warnings=warnings,
                            environment_prefix_cache=environment_prefix_cache,
                            requested_strategy=(
                                download_strategy
                                if download_strategy == "decrypted_file_url_then_storage_cookie_redirect"
                                else refreshed_strategy
                            ),
                        )
                        source_url = refreshed_url
                        base_item["source_url"] = refreshed_url
                        download_succeeded = True
                    except QingflowApiError as refreshed_exc:
                        exc = refreshed_exc
                        blocked = exc.http_status in {401, 403}
                    else:
                        warnings.append(
                            {
                                "code": "FILE_ASSET_STORAGE_URL_REFRESHED",
                                "file_asset_id": file_asset_id,
                                "message": "record_get refreshed the record detail once before downloading this file asset.",
                            }
                        )
            if not download_succeeded:
                warning_code = "STORAGE_COOKIE_AUTH_FAILED" if blocked and download_strategy != "referer_acl" else "FILE_ASSET_DOWNLOAD_FAILED"
                items.append(
                    {
                        **base_item,
                        "storage_auth_type": _record_detail_storage_auth_type(source_url),
                        "storage_cookie_prefix": environment_prefix_cache.get("value"),
                        "redirected": False,
                        "local_path": None,
                        "mime_type": _record_detail_mime_from_url(source_url),
                        "size_bytes": None,
                        "access_status": "blocked_private_url" if blocked else "download_failed",
                        "download_strategy": download_strategy,
                        "readable_by_agent": False,
                        "extraction": {"status": "failed", "text_path": None, "preview": None},
                    }
                )
                warning = {
                    "code": warning_code,
                    "file_asset_id": file_asset_id,
                    "message": f"record_get could not download file asset {file_asset_id}: {exc.message}",
                }
                warning.update(_record_detail_error_warning_fields(exc))
                warnings.append(warning)
                continue

        if not isinstance(content, bytes):
            content = bytes(content or b"")
        file_name = _record_detail_file_name_from_candidate(candidate, source_url=source_url, fallback_id=file_asset_id)
        mime_type = _record_detail_file_mime_from_content_or_name(content, source_url=source_url, file_name=file_name)
        size_bytes = len(content)
        if size_bytes > RECORD_GET_FILE_MAX_BYTES or total_bytes + size_bytes > RECORD_GET_FILE_MAX_TOTAL_BYTES:
            items.append(
                {
                    **base_item,
                    **download_meta,
                    "file_name": file_name,
                    "local_path": None,
                    "mime_type": mime_type,
                    "size_bytes": size_bytes,
                    "access_status": "too_large",
                    "readable_by_agent": False,
                    "extraction": {"status": "skipped_too_large", "text_path": None, "preview": None},
                }
            )
            warnings.append(
                {
                    "code": "FILE_ASSET_SIZE_LIMIT_EXCEEDED",
                    "file_asset_id": file_asset_id,
                    "message": "record_get skipped a file asset because it exceeded the internal file size budget.",
                }
            )
            continue

        ensure_local_dir()
        extension = _record_detail_file_extension(mime_type, source_url=source_url, file_name=file_name)
        local_path = local_dir / f"{file_asset_id}{extension}"
        local_path.write_bytes(content)
        extraction = _record_detail_extract_file_asset_text(
            content,
            mime_type=mime_type,
            file_name=file_name,
            local_dir=local_dir,
            file_asset_id=file_asset_id,
        )
        if extraction.get("status") == "failed":
            warnings.append(
                {
                    "code": "FILE_ASSET_EXTRACTION_FAILED",
                    "file_asset_id": file_asset_id,
                    "message": f"record_get downloaded file asset {file_asset_id}, but text extraction failed.",
                }
            )
        total_bytes += size_bytes
        downloaded_count += 1
        items.append(
            {
                **base_item,
                **download_meta,
                "file_name": file_name,
                "local_path": str(local_path),
                "mime_type": mime_type,
                "size_bytes": size_bytes,
                "access_status": "downloaded",
                "readable_by_agent": extraction.get("status") == "ok" or _record_detail_image_mime_from_bytes(content) is not None,
                "extraction": extraction,
            }
        )

    if not items:
        status = "none"
    elif stopped_for_time_budget or any(item.get("access_status") != "downloaded" or cast(JSONObject, item.get("extraction", {})).get("status") == "failed" for item in items):
        status = "partial"
    else:
        status = "ok"
    return {"status": status, "local_dir": str(local_dir) if items else None, "items": items, "warnings": warnings}


def _record_detail_media_candidates_from_field(
    field: JSONObject,
    *,
    source_app_key: str | None,
    source_record_id: str | None,
    forced_source: str | None,
) -> list[JSONObject]:
    field_id = _coerce_count(field.get("field_id"))
    field_title = _normalize_optional_text(field.get("title"))
    field_type = _normalize_optional_text(field.get("type"))
    candidates: list[JSONObject] = []
    seen_urls: set[str] = set()

    def add_candidate(url: str | None, *, source: str, path: str, name: str | None = None, image_hint: bool = False) -> None:
        normalized_url = _record_detail_normalize_media_url(url)
        if not normalized_url or normalized_url in seen_urls:
            return
        if not _record_detail_supported_media_url(normalized_url):
            return
        if not image_hint and not _record_detail_url_or_name_looks_like_image(normalized_url, name):
            return
        seen_urls.add(normalized_url)
        candidates.append(
            {
                "_owner": field,
                "kind": "image",
                "source": forced_source or source,
                "source_path": path,
                "field_id": field_id,
                "field_title": field_title,
                "source_app_key": source_app_key,
                "source_record_id": source_record_id,
                "source_url": normalized_url,
                "file_name": name,
            }
        )

    def scan_text(value: str, *, path: str, source: str) -> None:
        for match in _RECORD_MEDIA_IMG_SRC_RE.finditer(value):
            add_candidate(match.group(1), source="rich_text", path=path, image_hint=True)
        for match in _RECORD_MEDIA_MD_IMAGE_RE.finditer(value):
            add_candidate(match.group(1), source="rich_text", path=path, image_hint=True)
        for match in _RECORD_MEDIA_URL_RE.finditer(value):
            add_candidate(match.group(0), source=source, path=path, image_hint=False)

    def scan_value(value: JSONValue, *, path: str, source: str) -> None:
        if isinstance(value, str):
            scan_text(value, path=path, source=source)
            return
        if isinstance(value, list):
            for index, item in enumerate(value):
                scan_value(cast(JSONValue, item), path=f"{path}[{index}]", source=source)
            return
        if not isinstance(value, dict):
            return
        candidate_name = _normalize_optional_text(value.get("name", value.get("otherInfo", value.get("fileName"))))
        for key, item in value.items():
            normalized_key = _record_detail_media_key(key)
            item_text = _normalize_optional_text(item) if not isinstance(item, (dict, list)) else None
            key_source = source
            image_hint = False
            if normalized_key in _RECORD_MEDIA_IMAGE_URL_KEYS:
                key_source = "attachment" if source == "attachment" else "image_field"
                image_hint = normalized_key not in {"value", "url", "fileurl", "file_url"}
                if item_text:
                    add_candidate(item_text, source=key_source, path=f"{path}.{key}", name=candidate_name, image_hint=image_hint)
            scan_value(cast(JSONValue, item), path=f"{path}.{key}", source=key_source)

    value = cast(JSONValue, field.get("value"))
    display_value = cast(JSONValue, field.get("display_value"))
    if field_type == "attachment":
        scan_value(value, path="value", source="attachment")
    elif field_type == "subtable":
        scan_value(value, path="value", source="subtable")
    else:
        scan_value(value, path="value", source="image_field")
    scan_value(display_value, path="display_value", source="rich_text")
    return candidates


def _record_detail_file_candidates_from_field(
    field: JSONObject,
    *,
    source_app_key: str | None,
    source_record_id: str | None,
    forced_source: str | None,
) -> list[JSONObject]:
    field_id = _coerce_count(field.get("field_id"))
    field_title = _normalize_optional_text(field.get("title"))
    field_type = _normalize_optional_text(field.get("type"))
    candidates: list[JSONObject] = []
    seen_urls: set[str] = set()

    def add_candidate(url: str | None, *, source: str, path: str, name: str | None = None, file_hint: bool = False) -> None:
        normalized_url = _record_detail_normalize_media_url(url)
        if not normalized_url or normalized_url in seen_urls:
            return
        if not _record_detail_supported_file_url(normalized_url):
            return
        if not file_hint and not _record_detail_url_or_name_looks_like_file(normalized_url, name):
            return
        seen_urls.add(normalized_url)
        candidates.append(
            {
                "_owner": field,
                "kind": "file",
                "source": forced_source or source,
                "source_path": path,
                "field_id": field_id,
                "field_title": field_title,
                "source_app_key": source_app_key,
                "source_record_id": source_record_id,
                "source_url": normalized_url,
                "file_name": name,
            }
        )

    def candidate_name_from_mapping(value: dict[Any, Any]) -> str | None:
        for key, item in value.items():
            if _record_detail_media_key(key) in _RECORD_FILE_NAME_KEYS:
                text = _normalize_optional_text(item) if not isinstance(item, (dict, list)) else None
                if text:
                    return text
        return None

    def scan_text(value: str, *, path: str, source: str, file_hint: bool = False) -> None:
        for match in _RECORD_MEDIA_IMG_SRC_RE.finditer(value):
            add_candidate(match.group(1), source="rich_text", path=path, file_hint=True)
        for match in _RECORD_MEDIA_MD_IMAGE_RE.finditer(value):
            add_candidate(match.group(1), source="rich_text", path=path, file_hint=True)
        for match in _RECORD_MEDIA_URL_RE.finditer(value):
            add_candidate(match.group(0), source=source, path=path, file_hint=file_hint)

    def scan_value(value: JSONValue, *, path: str, source: str, file_hint: bool = False) -> None:
        if isinstance(value, str):
            scan_text(value, path=path, source=source, file_hint=file_hint)
            return
        if isinstance(value, list):
            for index, item in enumerate(value):
                scan_value(cast(JSONValue, item), path=f"{path}[{index}]", source=source, file_hint=file_hint)
            return
        if not isinstance(value, dict):
            return

        attachment = _extract_attachment_item(cast(JSONValue, value))
        if attachment:
            add_candidate(
                _normalize_optional_text(attachment.get("value")),
                source="attachment" if source == "attachment" else source,
                path=path,
                name=_normalize_optional_text(attachment.get("name")),
                file_hint=True,
            )
        candidate_name = candidate_name_from_mapping(value)
        for key, item in value.items():
            normalized_key = _record_detail_media_key(key)
            item_text = _normalize_optional_text(item) if not isinstance(item, (dict, list)) else None
            key_source = source
            key_file_hint = file_hint
            if normalized_key in _RECORD_FILE_URL_KEYS:
                key_source = "attachment" if source == "attachment" else ("image_field" if source != "subtable" else "subtable")
                key_file_hint = source == "attachment" or normalized_key not in {"value", "url"}
                if item_text:
                    add_candidate(item_text, source=key_source, path=f"{path}.{key}", name=candidate_name, file_hint=key_file_hint)
            scan_value(cast(JSONValue, item), path=f"{path}.{key}", source=key_source, file_hint=key_file_hint)

    value = cast(JSONValue, field.get("value"))
    display_value = cast(JSONValue, field.get("display_value"))
    if field_type == "attachment":
        scan_value(value, path="value", source="attachment", file_hint=True)
    elif field_type == "subtable":
        scan_value(value, path="value", source="subtable", file_hint=True)
    else:
        scan_value(value, path="value", source="image_field", file_hint=False)
    scan_value(display_value, path="display_value", source="rich_text", file_hint=False)
    return candidates


def _record_detail_attach_asset_id(field: JSONObject, asset_id: str) -> None:
    asset_ids = field.get("asset_ids")
    if not isinstance(asset_ids, list):
        asset_ids = []
        field["asset_ids"] = asset_ids
    if asset_id not in asset_ids:
        asset_ids.append(asset_id)


def _record_detail_attach_file_asset_id(field: JSONObject, file_asset_id: str) -> None:
    asset_ids = field.get("file_asset_ids")
    if not isinstance(asset_ids, list):
        asset_ids = []
        field["file_asset_ids"] = asset_ids
    if file_asset_id not in asset_ids:
        asset_ids.append(file_asset_id)


def _record_detail_media_asset_base_item(candidate: JSONObject, *, asset_id: str) -> JSONObject:
    payload: JSONObject = {
        "asset_id": asset_id,
        "kind": candidate.get("kind") or "image",
        "source": candidate.get("source") or "image_field",
        "field_id": candidate.get("field_id"),
        "field_title": candidate.get("field_title"),
        "source_url": candidate.get("source_url"),
    }
    for key in ("source_path", "source_app_key", "source_record_id", "file_name"):
        if candidate.get(key) not in (None, ""):
            payload[key] = candidate.get(key)
    return payload


def _record_detail_file_asset_base_item(candidate: JSONObject, *, file_asset_id: str) -> JSONObject:
    payload: JSONObject = {
        "file_asset_id": file_asset_id,
        "kind": candidate.get("kind") or "file",
        "source": candidate.get("source") or "attachment",
        "field_id": candidate.get("field_id"),
        "field_title": candidate.get("field_title"),
        "source_url": candidate.get("source_url"),
    }
    for key in ("source_path", "source_app_key", "source_record_id", "file_name"):
        if candidate.get(key) not in (None, ""):
            payload[key] = candidate.get(key)
    return payload


def _record_detail_media_assets_by_url(media_assets: JSONObject) -> dict[str, JSONObject]:
    items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
    result: dict[str, JSONObject] = {}
    for item in items:
        if not isinstance(item, dict):
            continue
        source_url = _normalize_optional_text(item.get("source_url"))
        if source_url and source_url not in result:
            result[source_url] = cast(JSONObject, item)
    return result


def _record_detail_media_assets_by_asset_id(media_assets: JSONObject) -> dict[str, JSONObject]:
    items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
    result: dict[str, JSONObject] = {}
    for item in items:
        if not isinstance(item, dict):
            continue
        asset_id = _normalize_optional_text(item.get("asset_id"))
        if asset_id and asset_id not in result:
            result[asset_id] = cast(JSONObject, item)
    return result


def _record_detail_media_item_from_owner_asset_ids(
    owner: JSONObject,
    media_by_asset_id: dict[str, JSONObject],
    candidate: JSONObject,
) -> JSONObject | None:
    asset_ids = owner.get("asset_ids") if isinstance(owner.get("asset_ids"), list) else []
    if len(asset_ids) != 1:
        return None
    media_item = media_by_asset_id.get(str(asset_ids[0]))
    if not isinstance(media_item, dict):
        return None
    candidate_name = _normalize_optional_text(candidate.get("file_name"))
    media_name = _normalize_optional_text(media_item.get("file_name"))
    if candidate_name and media_name and candidate_name != media_name:
        return None
    if candidate.get("field_id") not in (None, media_item.get("field_id")):
        return None
    return media_item


def _record_detail_media_assets_dir(run_id: str) -> Path:
    custom_home = os.environ.get("QINGFLOW_MCP_RECORD_ASSETS_HOME")
    base_dir = Path(custom_home).expanduser() if custom_home else get_mcp_home() / "record-assets"
    return base_dir / run_id


def _record_detail_file_assets_dir(run_id: str) -> Path:
    custom_home = os.environ.get("QINGFLOW_MCP_RECORD_FILES_HOME")
    base_dir = Path(custom_home).expanduser() if custom_home else get_mcp_home() / "record-files"
    return base_dir / run_id


def _record_detail_media_download_headers(context: BackendRequestContext) -> dict[str, str]:
    origin = _record_detail_context_origin(context)
    return {"User-Agent": DEFAULT_USER_AGENT, "Referer": f"{origin}/", "Origin": origin}


def _record_detail_download_media_content(
    *,
    backend: Any,
    context: BackendRequestContext,
    source_url: str,
    warnings: list[JSONObject],
    environment_prefix_cache: dict[str, str],
    requested_strategy: str,
) -> tuple[bytes, JSONObject]:
    if requested_strategy == "decrypted_file_url_then_storage_cookie_redirect":
        decrypted_url = _record_detail_decrypt_file_url(backend, context, source_url)
        content, meta = _record_detail_download_resolved_media_content(
            backend=backend,
            context=context,
            source_url=decrypted_url,
            warnings=warnings,
            environment_prefix_cache=environment_prefix_cache,
            requested_strategy=requested_strategy,
        )
        meta["download_strategy"] = requested_strategy
        return content, meta
    return _record_detail_download_resolved_media_content(
        backend=backend,
        context=context,
        source_url=source_url,
        warnings=warnings,
        environment_prefix_cache=environment_prefix_cache,
        requested_strategy=requested_strategy,
    )


def _record_detail_download_resolved_media_content(
    *,
    backend: Any,
    context: BackendRequestContext,
    source_url: str,
    warnings: list[JSONObject],
    environment_prefix_cache: dict[str, str],
    requested_strategy: str,
) -> tuple[bytes, JSONObject]:
    storage_auth_type = _record_detail_storage_auth_type(source_url)
    if _record_detail_should_use_storage_cookie(source_url, storage_auth_type):
        storage_cookie_prefix = _record_detail_environment_prefix(
            backend,
            context,
            warnings=warnings,
            environment_prefix_cache=environment_prefix_cache,
        )
        cookie_name = f"{storage_cookie_prefix}token"
        headers = _record_detail_media_download_headers(context)
        if hasattr(backend, "download_binary_with_cookie"):
            response = backend.download_binary_with_cookie(context, source_url, cookie_name=cookie_name, headers=headers)
            return response.content, {
                "download_strategy": requested_strategy,
                "storage_auth_type": storage_auth_type,
                "storage_cookie_prefix": storage_cookie_prefix,
                "redirected": bool(getattr(response, "redirected", False)),
            }
        cookie_headers = dict(headers)
        cookie_headers["Cookie"] = f"{cookie_name}={context.token}"
        content = backend.download_binary(source_url, headers=cookie_headers)
        return content, {
            "download_strategy": requested_strategy,
            "storage_auth_type": storage_auth_type,
            "storage_cookie_prefix": storage_cookie_prefix,
            "redirected": False,
        }
    content = backend.download_binary(source_url, headers=_record_detail_media_download_headers(context))
    return content, {
        "download_strategy": requested_strategy,
        "storage_auth_type": storage_auth_type,
        "storage_cookie_prefix": None,
        "redirected": False,
    }


def _record_detail_media_download_strategy(source_url: str) -> str:
    if _record_detail_is_download_file_url(source_url):
        return "decrypted_file_url_then_storage_cookie_redirect"
    if _record_detail_should_use_storage_cookie(source_url, _record_detail_storage_auth_type(source_url)):
        return "qingflow_storage_cookie_redirect"
    return "referer_acl"


def _record_detail_should_use_storage_cookie(source_url: str, storage_auth_type: str | None) -> bool:
    if not _record_detail_is_qingflow_storage_url(source_url):
        return False
    if not storage_auth_type:
        return True
    return storage_auth_type.upper() != "ANONYMOUS"


def _record_detail_is_qingflow_storage_url(source_url: str) -> bool:
    parsed = urlsplit(source_url)
    path = parsed.path.lower()
    if "/storage/file/" in path:
        return True
    query = parse_qs(parsed.query)
    return any(key.lower() == "qingflow-auth_type" for key in query)


def _record_detail_storage_auth_type(source_url: str) -> str | None:
    query = parse_qs(urlsplit(source_url).query)
    for key, values in query.items():
        if key.lower() == "qingflow-auth_type" and values:
            return _normalize_optional_text(values[0])
    return None


def _record_detail_is_download_file_url(source_url: str) -> bool:
    parsed = urlsplit(source_url)
    path = parsed.path.strip("/").lower()
    return path == "download-file" or path.endswith("/download-file")


def _record_detail_decrypt_file_url(backend: Any, context: BackendRequestContext, source_url: str) -> str:
    parsed = urlsplit(source_url)
    query = parse_qs(parsed.query)
    app_key = _record_detail_query_value(query, "appKey")
    apply_id = _coerce_count(_record_detail_query_value(query, "applyId"))
    que_id = _coerce_count(_record_detail_query_value(query, "queId"))
    list_type = _coerce_count(_record_detail_query_value(query, "listType"))
    uuid = _record_detail_query_value(query, "uuid")
    if not app_key or apply_id is None or que_id is None or list_type is None or not uuid:
        raise QingflowApiError.config_error(
            "download-file attachment URL is missing appKey, applyId, listType, uuid, or queId",
            details={"source_url": source_url},
        )
    payload = backend.request(
        "POST",
        context,
        f"/app/{app_key}/apply/decryptedFileUrl",
        json_body={"applyId": apply_id, "queId": que_id, "listType": list_type, "uuid": uuid},
    )
    if not isinstance(payload, dict):
        raise QingflowApiError.config_error("decryptedFileUrl returned an unexpected response", details={"source_url": source_url})
    decrypted_url = _normalize_optional_text(payload.get("url"))
    if not decrypted_url:
        raise QingflowApiError.config_error("decryptedFileUrl did not return a downloadable url", details={"source_url": source_url})
    return decrypted_url


def _record_detail_query_value(query: dict[str, list[str]], key: str) -> str | None:
    for raw_key, values in query.items():
        if raw_key == key and values:
            return _normalize_optional_text(values[0])
    return None


def _record_detail_environment_prefix(
    backend: Any,
    context: BackendRequestContext,
    *,
    warnings: list[JSONObject],
    environment_prefix_cache: dict[str, str],
) -> str:
    if "value" in environment_prefix_cache:
        return environment_prefix_cache["value"]
    try:
        payload = backend.request("GET", context, "/environment/properties")
        environment = payload.get("environmentProperty") if isinstance(payload, dict) else None
        if not isinstance(environment, dict):
            environment = payload.get("environment_property") if isinstance(payload, dict) else None
        prefix = _normalize_optional_text(environment.get("environmentPrefix")) if isinstance(environment, dict) else None
        if not prefix and isinstance(environment, dict):
            prefix = _normalize_optional_text(environment.get("environment_prefix"))
    except QingflowApiError as exc:
        prefix = None
        warnings.append(
            {
                "code": "MEDIA_ASSET_ENVIRONMENT_PREFIX_UNAVAILABLE",
                "message": f"record_get could not read /environment/properties; falling back to qf_ token cookie prefix: {exc.message}",
                "http_status": exc.http_status,
            }
        )
    prefix = prefix if prefix is not None else "qf_"
    environment_prefix_cache["value"] = prefix
    return prefix


def _record_detail_context_origin(context: BackendRequestContext) -> str:
    raw_base_url = _normalize_optional_text(getattr(context, "base_url", None)) or "https://qingflow.com"
    parsed = urlsplit(raw_base_url)
    if parsed.scheme and parsed.netloc:
        return f"{parsed.scheme}://{parsed.netloc}"
    return "https://qingflow.com"


def _record_detail_normalize_media_url(value: str | None) -> str | None:
    text = html.unescape(value or "").strip().strip("\"'")
    text = text.rstrip(".,;")
    return text or None


def _record_detail_supported_media_url(url: str) -> bool:
    parsed = urlsplit(url)
    return parsed.scheme.lower() in {"http", "https"} or _record_detail_is_download_file_url(url)


def _record_detail_supported_file_url(url: str) -> bool:
    parsed = urlsplit(url)
    return parsed.scheme.lower() in {"http", "https"} or _record_detail_is_download_file_url(url)


def _record_detail_media_key(key: Any) -> str:
    return str(key or "").strip().replace("-", "_").lower()


def _record_detail_url_or_name_looks_like_image(url: str, name: str | None = None) -> bool:
    for value in (url, name or ""):
        path = unquote(urlsplit(value).path).lower()
        if any(path.endswith(extension) for extension in _RECORD_MEDIA_IMAGE_EXTENSIONS):
            return True
    return False


def _record_detail_url_or_name_looks_like_file(url: str, name: str | None = None) -> bool:
    if _record_detail_is_download_file_url(url) or _record_detail_is_qingflow_storage_url(url):
        return True
    for value in (url, name or ""):
        if not value:
            continue
        path = unquote(urlsplit(value).path).lower() or value.lower()
        if any(path.endswith(extension) for extension in _RECORD_FILE_EXTENSIONS):
            return True
    return False


def _record_detail_mime_from_url(url: str) -> str | None:
    path = unquote(urlsplit(url).path).lower()
    if path.endswith(".png"):
        return "image/png"
    if path.endswith(".jpg") or path.endswith(".jpeg"):
        return "image/jpeg"
    if path.endswith(".gif"):
        return "image/gif"
    if path.endswith(".webp"):
        return "image/webp"
    if path.endswith(".bmp"):
        return "image/bmp"
    if path.endswith(".svg"):
        return "image/svg+xml"
    if path.endswith(".pdf"):
        return "application/pdf"
    if path.endswith(".docx"):
        return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    if path.endswith(".xlsx"):
        return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    if path.endswith(".xlsm"):
        return "application/vnd.ms-excel.sheet.macroEnabled.12"
    if path.endswith(".csv"):
        return "text/csv"
    if path.endswith(".txt") or path.endswith(".text"):
        return "text/plain"
    if path.endswith(".md"):
        return "text/markdown"
    if path.endswith(".json"):
        return "application/json"
    return None


def _record_detail_file_name_from_candidate(candidate: JSONObject, *, source_url: str, fallback_id: str) -> str:
    raw_name = _normalize_optional_text(candidate.get("file_name"))
    if raw_name:
        return raw_name
    path_name = Path(unquote(urlsplit(source_url).path)).name
    if path_name:
        return path_name
    return fallback_id


def _record_detail_file_mime_from_content_or_name(content: bytes, *, source_url: str, file_name: str) -> str | None:
    image_mime = _record_detail_image_mime_from_bytes(content)
    if image_mime:
        return image_mime
    if content.startswith(b"%PDF"):
        return "application/pdf"
    guessed = mimetypes.guess_type(file_name or source_url)[0] or _record_detail_mime_from_url(source_url)
    if guessed:
        return guessed
    lowered = (file_name or source_url).lower()
    if lowered.endswith(".docx"):
        return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    if lowered.endswith(".xlsx"):
        return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    if lowered.endswith(".xlsm"):
        return "application/vnd.ms-excel.sheet.macroEnabled.12"
    if lowered.endswith(".csv"):
        return "text/csv"
    if lowered.endswith(".json"):
        return "application/json"
    if _record_detail_bytes_look_like_text(content):
        return "text/plain"
    return None


def _record_detail_file_extension(mime_type: str | None, *, source_url: str, file_name: str) -> str:
    for value in (file_name, unquote(urlsplit(source_url).path)):
        suffix = Path(value).suffix.lower()
        if suffix and re.fullmatch(r"\.[a-z0-9]{1,10}", suffix):
            return suffix
    if mime_type:
        extension = mimetypes.guess_extension(mime_type)
        if extension:
            return ".jpg" if extension == ".jpe" else extension
    return ".bin"


def _record_detail_bytes_look_like_text(content: bytes) -> bool:
    if not content:
        return True
    sample = content[:4096]
    if b"\x00" in sample:
        return False
    try:
        sample.decode("utf-8")
        return True
    except UnicodeDecodeError:
        try:
            sample.decode("gb18030")
            return True
        except UnicodeDecodeError:
            return False


def _record_detail_extract_file_asset_text(
    content: bytes,
    *,
    mime_type: str | None,
    file_name: str,
    local_dir: Path,
    file_asset_id: str,
) -> JSONObject:
    normalized_name = file_name.lower()
    try:
        text: str | None
        if normalized_name.endswith(".docx") or mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
            text = _record_detail_extract_docx_text(content)
        elif normalized_name.endswith((".xlsx", ".xlsm")) or mime_type in {
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            "application/vnd.ms-excel.sheet.macroEnabled.12",
        }:
            text = _record_detail_extract_xlsx_text(content)
        elif normalized_name.endswith(".pdf") or mime_type == "application/pdf":
            text = _record_detail_extract_pdf_text(content)
        elif normalized_name.endswith(".json") or mime_type == "application/json":
            text = _record_detail_decode_json_text(content)
        elif normalized_name.endswith((".csv", ".txt", ".text", ".md")) or (mime_type or "").startswith("text/"):
            text = _record_detail_decode_text(content)
        else:
            text = None
    except Exception as exc:
        return {"status": "failed", "text_path": None, "preview": None, "error": str(exc)}
    if text is None:
        return {"status": "unsupported", "text_path": None, "preview": None}
    text_path = local_dir / f"{file_asset_id}.txt"
    text_path.write_text(text, encoding="utf-8")
    preview = text[:RECORD_GET_FILE_EXTRACT_PREVIEW_CHARS]
    return {
        "status": "ok",
        "text_path": str(text_path),
        "preview": preview,
        "preview_truncated": len(text) > RECORD_GET_FILE_EXTRACT_PREVIEW_CHARS,
    }


def _record_detail_decode_text(content: bytes) -> str:
    for encoding in ("utf-8-sig", "utf-8", "gb18030"):
        try:
            return content.decode(encoding)
        except UnicodeDecodeError:
            continue
    return content.decode("utf-8", errors="replace")


def _record_detail_decode_json_text(content: bytes) -> str:
    text = _record_detail_decode_text(content)
    try:
        return json.dumps(json.loads(text), ensure_ascii=False, indent=2)
    except ValueError:
        return text


def _record_detail_extract_docx_text(content: bytes) -> str:
    with zipfile.ZipFile(BytesIO(content)) as archive:
        document_xml = archive.read("word/document.xml")
    root = ElementTree.fromstring(document_xml)
    ns = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
    body = root.find(f"{ns}body")
    if body is None:
        return ""

    def node_text(node: ElementTree.Element) -> str:
        return "".join(text_node.text or "" for text_node in node.iter(f"{ns}t")).strip()

    lines: list[str] = []
    for child in list(body):
        if child.tag == f"{ns}p":
            line = node_text(child)
            if line:
                lines.append(line)
        elif child.tag == f"{ns}tbl":
            for row in child.iter(f"{ns}tr"):
                cells = [node_text(cell) for cell in row.iter(f"{ns}tc")]
                cells = [cell for cell in cells if cell]
                if cells:
                    lines.append(" | ".join(cells))
    return "\n".join(lines)


def _record_detail_extract_xlsx_text(content: bytes) -> str:
    from openpyxl import load_workbook

    workbook = load_workbook(BytesIO(content), read_only=True, data_only=True)
    try:
        parts: list[str] = []
        for sheet in workbook.worksheets:
            parts.append(f"# {sheet.title}")
            row_count = 0
            for row in sheet.iter_rows(values_only=True):
                row_count += 1
                if row_count > RECORD_GET_FILE_EXTRACT_XLSX_MAX_ROWS_PER_SHEET:
                    parts.append(f"... skipped rows after {RECORD_GET_FILE_EXTRACT_XLSX_MAX_ROWS_PER_SHEET}")
                    break
                cells = ["" if cell is None else str(cell) for cell in row]
                if any(cell for cell in cells):
                    parts.append("\t".join(cells).rstrip())
        return "\n".join(parts)
    finally:
        workbook.close()


def _record_detail_extract_pdf_text(content: bytes) -> str:
    from pypdf import PdfReader

    reader = PdfReader(BytesIO(content))
    lines: list[str] = []
    for index, page in enumerate(reader.pages[:RECORD_GET_FILE_EXTRACT_PDF_MAX_PAGES], start=1):
        page_text = page.extract_text() or ""
        if page_text.strip():
            lines.append(f"# Page {index}\n{page_text.strip()}")
    if len(reader.pages) > RECORD_GET_FILE_EXTRACT_PDF_MAX_PAGES:
        lines.append(f"... skipped pages after {RECORD_GET_FILE_EXTRACT_PDF_MAX_PAGES}")
    return "\n\n".join(lines)


def _record_detail_image_mime_from_bytes(content: bytes) -> str | None:
    if content.startswith(b"\x89PNG\r\n\x1a\n"):
        return "image/png"
    if content.startswith(b"\xff\xd8\xff"):
        return "image/jpeg"
    if content.startswith((b"GIF87a", b"GIF89a")):
        return "image/gif"
    if len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP":
        return "image/webp"
    if content.startswith(b"BM"):
        return "image/bmp"
    stripped = content.lstrip().lower()
    if stripped.startswith(b"<svg") or stripped.startswith(b"<?xml"):
        return "image/svg+xml" if b"<svg" in stripped[:200] else None
    return None


def _record_detail_image_extension(mime_type: str, source_url: str) -> str:
    source_path = unquote(urlsplit(source_url).path).lower()
    for extension in _RECORD_MEDIA_IMAGE_EXTENSIONS:
        if source_path.endswith(extension):
            return ".jpg" if extension == ".jpeg" else extension
    return {
        "image/png": ".png",
        "image/jpeg": ".jpg",
        "image/gif": ".gif",
        "image/webp": ".webp",
        "image/bmp": ".bmp",
        "image/svg+xml": ".svg",
    }.get(mime_type, ".img")


def _record_detail_context_integrity(
    *,
    references: list[JSONObject],
    data_logs: JSONObject,
    workflow_logs: JSONObject,
    associated_resources: list[JSONObject],
    media_assets: JSONObject,
    file_assets: JSONObject,
    unavailable_context: list[JSONObject],
) -> JSONObject:
    reference_unavailable = any(item.get("target_detail_completeness") != "full" for item in references)
    return {
        "record_detail": "full",
        "fields": "full",
        "references": "partial" if reference_unavailable else "full",
        "data_logs": data_logs.get("status") or "unknown",
        "workflow_logs": workflow_logs.get("status") or "unknown",
        "associated_resources": "full" if associated_resources or not any(item.get("section") == "associated_resources" for item in unavailable_context) else "unavailable",
        "media_assets": media_assets.get("status") or "unknown",
        "file_assets": file_assets.get("status") or "unknown",
        "unavailable_count": len(unavailable_context),
        "safe_for_record_fact_conclusion": True,
        "safe_for_full_log_conclusion": False,
    }


def _record_detail_semantic_context(payload: JSONObject) -> str:
    app = payload.get("app") if isinstance(payload.get("app"), dict) else {}
    view = payload.get("view") if isinstance(payload.get("view"), dict) else {}
    record = payload.get("record") if isinstance(payload.get("record"), dict) else {}
    summary = payload.get("summary") if isinstance(payload.get("summary"), dict) else {}
    fields = payload.get("fields") if isinstance(payload.get("fields"), list) else []
    references = payload.get("references") if isinstance(payload.get("references"), list) else []
    media_assets = payload.get("media_assets") if isinstance(payload.get("media_assets"), dict) else {}
    file_assets = payload.get("file_assets") if isinstance(payload.get("file_assets"), dict) else {}
    data_logs = payload.get("data_logs") if isinstance(payload.get("data_logs"), dict) else {}
    workflow_logs = payload.get("workflow_logs") if isinstance(payload.get("workflow_logs"), dict) else {}
    associated_resources = payload.get("associated_resources") if isinstance(payload.get("associated_resources"), list) else []
    unavailable_context = payload.get("unavailable_context") if isinstance(payload.get("unavailable_context"), list) else []
    lines = [
        '<qingflow_context source="qingflow_record_get" version="detail_context">',
        f'  <record_context appKey="{_semantic_escape(app.get("app_key"))}" appName="{_semantic_escape(app.get("app_name"))}" viewId="{_semantic_escape(view.get("view_id"))}" viewName="{_semantic_escape(view.get("name"))}" recordId="{_semantic_escape(record.get("record_id"))}" title="{_semantic_escape(record.get("title"))}" status="ok">',
        "摘要：",
        _semantic_escape(summary.get("text")) or "暂无摘要。",
        "",
        "字段：",
    ]
    for field in fields:
        if not isinstance(field, dict):
            continue
        lines.append(f"- {_semantic_escape(field.get('title'))}（fieldId={_semantic_escape(field.get('field_id'))}）：{_semantic_escape(field.get('display_value'))}")
    if references:
        lines.extend(["", "引用："])
        for ref in references:
            if not isinstance(ref, dict):
                continue
            lines.append(
                f"- 引用字段「{_semantic_escape(ref.get('field_title'))}」（fieldId={_semantic_escape(ref.get('field_id'))}）"
                f"指向目标记录「{_semantic_escape(ref.get('target_title'))}」："
                f"targetAppKey={_semantic_escape(ref.get('target_app_key'))}；"
                f"targetRecordId={_semantic_escape(ref.get('target_record_id'))}。"
            )
            target_fields = ref.get("target_fields") if isinstance(ref.get("target_fields"), list) else []
            if target_fields:
                compact = []
                for field in target_fields[:20]:
                    if isinstance(field, dict):
                        compact.append(f"{field.get('title')}（fieldId={field.get('field_id')}）={field.get('display_value')}")
                lines.append(f"  目标记录字段：{_semantic_escape('；'.join(compact))}。")
            lines.append(f"  目标详情完整性：{_semantic_escape(ref.get('target_detail_completeness'))}。")
    media_items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
    if media_items:
        lines.extend(["", "图片与附件："])
        for item in media_items:
            if not isinstance(item, dict):
                continue
            readable_text = "可由智能体读取" if item.get("readable_by_agent") else f"不可直接读取（{item.get('access_status') or 'unknown'}）"
            lines.append(
                f"- 图片 {_semantic_escape(item.get('asset_id'))} 来自字段「{_semantic_escape(item.get('field_title'))}」"
                f"（fieldId={_semantic_escape(item.get('field_id'))}），"
                f"本地路径：{_semantic_escape(item.get('local_path')) or '无'}，{_semantic_escape(readable_text)}。"
            )
    file_items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
    if file_items:
        lines.extend(["", "文件附件："])
        for item in file_items:
            if not isinstance(item, dict):
                continue
            extraction = item.get("extraction") if isinstance(item.get("extraction"), dict) else {}
            readable_text = "可由智能体读取" if item.get("readable_by_agent") else f"不可直接读取（{item.get('access_status') or 'unknown'}）"
            lines.append(
                f"- 文件 {_semantic_escape(item.get('file_asset_id'))}「{_semantic_escape(item.get('file_name'))}」"
                f"来自字段「{_semantic_escape(item.get('field_title'))}」（fieldId={_semantic_escape(item.get('field_id'))}），"
                f"本地路径：{_semantic_escape(item.get('local_path')) or '无'}，"
                f"提取文本：{_semantic_escape(extraction.get('text_path')) or '无'}，{_semantic_escape(readable_text)}。"
            )
    lines.extend(["", "最近数据日志："])
    _append_semantic_log_lines(lines, data_logs)
    lines.extend(["", "最近流程日志："])
    _append_semantic_log_lines(lines, workflow_logs)
    if associated_resources:
        lines.extend(["", "关联资源："])
        for item in associated_resources:
            if not isinstance(item, dict):
                continue
            data_access = item.get("data_access") if isinstance(item.get("data_access"), dict) else {}
            label = "报表" if item.get("type") == "report" else "视图"
            lines.append(
                f"- {label}「{_semantic_escape(item.get('name'))}」："
                f"appKey={_semantic_escape(item.get('app_key'))}，"
                f"viewKey={_semantic_escape(item.get('view_key'))}，"
                f"chartKey={_semantic_escape(item.get('chart_key'))}。"
                f"数据读取：{_semantic_escape(data_access.get('description'))}"
            )
    if unavailable_context:
        lines.extend(["", "不可用上下文："])
        for item in unavailable_context:
            if isinstance(item, dict):
                lines.append(f"- {_semantic_escape(item.get('message'))}（section={_semantic_escape(item.get('section'))}）。")
    lines.extend(["  </record_context>", "</qingflow_context>"])
    return "\n".join(lines)


def _append_semantic_log_lines(lines: list[str], payload: JSONObject) -> None:
    if payload.get("status") != "ok":
        lines.append(f"- {payload.get('status') or 'unavailable'}：{payload.get('reason') or '未加载'}。")
        return
    items = payload.get("items") if isinstance(payload.get("items"), list) else []
    if not items:
        lines.append("- 暂无首屏日志。")
        return
    for item in items:
        if not isinstance(item, dict):
            continue
        summary = item.get("summary") or item.get("action") or "记录了一次操作"
        lines.append(
            f"- {_semantic_escape(item.get('time'))}，{_semantic_escape(item.get('operator'))}"
            f"：{_semantic_escape(summary)}。"
        )


def _semantic_escape(value: Any) -> str:
    text = "" if value is None else str(value)
    return text.replace("&", "&amp;").replace("<", "＜").replace(">", "＞")


def _is_board_view_type(view_type: str | None) -> bool:
    normalized = _normalize_optional_text(view_type)
    return normalized is not None and normalized.lower() == "boardview"


def _is_gantt_view_type(view_type: str | None) -> bool:
    normalized = _normalize_optional_text(view_type)
    return normalized is not None and normalized.lower() == "ganttview"


def _view_type_supports_analysis(view_type: str | None) -> bool:
    return not (_is_board_view_type(view_type) or _is_gantt_view_type(view_type))


def _view_selection_supported_by_search_ids(view_selection: ViewSelection, search_que_ids: list[int] | None) -> bool:
    if not view_selection.conditions:
        return True
    if not search_que_ids:
        return False
    allowed_ids = set(search_que_ids)
    for group in view_selection.conditions:
        for condition in group:
            if condition.que_id is None or condition.que_id not in allowed_ids:
                return False
    return True


def _build_flat_row(answer_list: list[JSONValue], fields: list[FormField], *, apply_id: int | None) -> JSONObject:
    public_record_id = _public_record_id_text(apply_id)
    row: JSONObject = {"apply_id": public_record_id, "record_id": public_record_id}
    for field in fields:
        row[field.que_title] = _extract_field_value(answer_list, field)
    return row


def _public_record_id_text(record_id: Any) -> str | None:
    if record_id is None or isinstance(record_id, bool):
        return None
    if isinstance(record_id, int) and record_id <= 0:
        return None
    if isinstance(record_id, str) and (not record_id.strip() or not record_id.strip().isdecimal()):
        return None
    return stringify_backend_id(record_id)


def _normalize_public_record_rows(rows: list[JSONValue]) -> list[JSONObject]:
    normalized_rows: list[JSONObject] = []
    for item in rows:
        if not isinstance(item, dict):
            continue
        row = dict(item)
        normalized_record_id = _coerce_count(row.get("apply_id"))
        if normalized_record_id is None:
            normalized_record_id = _coerce_count(row.get("record_id"))
        if normalized_record_id is not None and normalized_record_id > 0:
            public_record_id = _public_record_id_text(normalized_record_id)
            row["apply_id"] = public_record_id
            row["record_id"] = public_record_id
        normalized_rows.append(row)
    return normalized_rows


def _build_record_list_lookup_payload(
    *,
    query: str | None,
    items: list[JSONObject],
    pagination: JSONObject,
) -> JSONObject | None:
    if not query:
        return None
    reported_total = _coerce_count(pagination.get("result_amount"))
    returned_items = _coerce_count(pagination.get("returned_items"))
    if returned_items is None:
        returned_items = len(items)
    truncated = bool(reported_total is not None and reported_total > returned_items)
    confidence = _record_list_lookup_confidence(returned_items=returned_items, reported_total=reported_total, truncated=truncated)
    next_action = {
        "single_high": "record_get",
        "multiple": "ask_user",
        "none": "broaden_query",
        "truncated": "refine_query",
    }[confidence]
    return {
        "mode": "candidate_locator",
        "query": query,
        "total_count": reported_total,
        "returned_count": returned_items,
        "truncated": truncated,
        "confidence": confidence,
        "next_action": next_action,
    }


def _record_list_lookup_confidence(*, returned_items: int, reported_total: int | None, truncated: bool) -> str:
    if returned_items <= 0:
        return "none"
    if truncated:
        return "truncated"
    effective_total = reported_total if reported_total is not None else returned_items
    if effective_total == 1:
        return "single_high"
    return "multiple"


def _truncate_text(value: str, limit: int) -> str:
    if len(value) <= limit:
        return value
    return value[: max(0, limit - 1)] + "…"


def _merge_answer_lists_by_field_id(existing: list[JSONValue], extra: list[JSONValue]) -> list[JSONValue]:
    merged: dict[str, JSONValue] = {}
    order: list[str] = []
    for answer in list(existing) + list(extra):
        if not isinstance(answer, dict):
            continue
        que_id = _coerce_count(answer.get("queId"))
        title = _normalize_optional_text(answer.get("queTitle"))
        key = f"id:{que_id}" if que_id is not None and que_id > 0 else f"title:{title or ''}"
        if key not in merged:
            order.append(key)
        merged[key] = answer
    return [merged[key] for key in order]


def _build_normalized_row_from_answers(
    answer_list: list[JSONValue],
    fields: list[FormField],
) -> tuple[JSONObject, JSONObject]:
    grouped: dict[str, list[JSONObject]] = {}
    for field in fields:
        answer = _find_answer_for_field(answer_list, field)
        normalized_value = _normalize_answer_field_value_for_output(answer, field)
        grouped.setdefault(field.que_title, []).append(
            {
                "que_id": field.que_id,
                "que_type": field.que_type,
                "value": normalized_value,
            }
        )
    normalized_record: JSONObject = {}
    normalized_ambiguous_fields: JSONObject = {}
    for title, entries in grouped.items():
        if len(entries) == 1:
            normalized_record[title] = entries[0].get("value")
        else:
            normalized_ambiguous_fields[title] = entries
    return normalized_record, normalized_ambiguous_fields


def _find_answer_for_field(answer_list: list[JSONValue], field: FormField | None) -> JSONObject | None:
    if field is None:
        return None
    for answer in answer_list:
        if not isinstance(answer, dict):
            continue
        answer_que_id = _coerce_count(answer.get("queId"))
        if answer_que_id == field.que_id:
            return answer
    for answer in answer_list:
        if not isinstance(answer, dict):
            continue
        answer_title = _stringify_json(answer.get("queTitle")).strip()
        if answer_title and answer_title == field.que_title:
            return answer
    return None


def _normalize_answer_field_value_for_output(answer: JSONObject | None, field: FormField) -> JSONValue:
    if answer is None:
        return None
    if field.que_type in SUBTABLE_QUE_TYPES:
        return _normalize_subtable_answer_value_for_output(answer, field)
    return _normalize_answer_value_for_output_by_que_type(answer, field.que_type)


def _normalize_answer_value_for_output_by_que_type(answer: JSONObject, que_type: int | None) -> JSONValue:
    if que_type in ADDRESS_QUE_TYPES:
        return _normalize_address_output_value(_extract_address_value(answer))
    if que_type in RELATION_QUE_TYPES:
        return _normalize_relation_answer_value_for_output(answer)
    if que_type in MEMBER_QUE_TYPES or que_type in DEPARTMENT_QUE_TYPES:
        return _normalize_member_or_department_answer_value_for_output(answer)
    if que_type in ATTACHMENT_QUE_TYPES:
        values = answer.get("values")
        attachments = [_extract_attachment_item(item) for item in values] if isinstance(values, list) else []
        attachments = [item for item in attachments if item is not None]
        if not attachments:
            return None
        return attachments[0] if len(attachments) == 1 else attachments
    values = answer.get("values")
    if not isinstance(values, list) or not values:
        return None
    extracted = [_extract_value_item(item) for item in values]
    if que_type in MULTI_SELECT_QUE_TYPES:
        normalized_items: list[JSONValue] = []
        for item in extracted:
            parsed_item = _parse_json_like(item)
            if isinstance(parsed_item, list):
                normalized_items.extend(candidate for candidate in parsed_item if candidate not in (None, ""))
            elif parsed_item not in (None, ""):
                normalized_items.append(parsed_item)
        return normalized_items
    if len(extracted) == 1:
        return _normalize_scalar_output_value(extracted[0], que_type)
    normalized_items = [_normalize_scalar_output_value(item, que_type) for item in extracted]
    return [item for item in normalized_items if item is not None]


def _normalize_scalar_output_value(value: JSONValue, que_type: int | None) -> JSONValue:
    parsed_value = _parse_json_like(value)
    if que_type in DATE_QUE_TYPES:
        return _normalize_date_output_value(parsed_value)
    if que_type in MULTI_SELECT_QUE_TYPES:
        if isinstance(parsed_value, list):
            return [item for item in parsed_value]
        return [] if parsed_value in (None, "") else [parsed_value]
    if que_type in SINGLE_SELECT_QUE_TYPES:
        return parsed_value
    if que_type == 8 and _looks_like_number_value(parsed_value):
        return _normalize_number_output_value(parsed_value)
    return parsed_value


def _normalize_date_output_value(value: JSONValue) -> JSONValue:
    text = _normalize_optional_text(value) or (_stringify_json(value) if value is not None else None)
    if not text:
        return None
    if re.fullmatch(r"\d{4}-\d{2}-\d{2} 00:00:00", text):
        return text[:10]
    return text


def _looks_like_number_value(value: JSONValue) -> bool:
    if isinstance(value, (int, float)):
        return True
    if isinstance(value, str):
        text = value.strip()
        if not text:
            return False
        try:
            Decimal(text)
            return True
        except InvalidOperation:
            return False
    return False


def _normalize_number_output_value(value: JSONValue) -> JSONValue:
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        decimal_value = Decimal(str(value))
    elif isinstance(value, str):
        text = value.strip()
        if not text:
            return None
        try:
            decimal_value = Decimal(text)
        except InvalidOperation:
            return value
    else:
        return value
    if decimal_value == decimal_value.to_integral():
        return int(decimal_value)
    return float(decimal_value)


def _normalize_member_or_department_answer_value_for_output(answer: JSONObject) -> JSONValue:
    values = answer.get("values")
    if not isinstance(values, list) or not values:
        return None
    labels: list[str] = []
    for item in values:
        if not isinstance(item, dict):
            text = _normalize_optional_text(item) or (_stringify_json(item) if item is not None else None)
            if text:
                labels.append(text)
            continue
        label = (
            _normalize_optional_text(item.get("value"))
            or _normalize_optional_text(item.get("name"))
            or _normalize_optional_text(item.get("email"))
            or (_stringify_json(item.get("id")) if item.get("id") is not None else None)
        )
        if label:
            labels.append(label)
    if not labels:
        return None
    return labels[0] if len(labels) == 1 else labels


def _normalize_relation_answer_value_for_output(answer: JSONObject) -> JSONValue:
    values: list[JSONObject] = []
    for key in ("referValues", "values"):
        raw_values = answer.get(key)
        if isinstance(raw_values, list):
            values.extend(item for item in raw_values if isinstance(item, dict))
    normalized_entries: list[JSONObject] = []
    seen: set[tuple[str | None, str | None]] = set()
    for item in values:
        apply_id = _normalize_optional_text(item.get("applyId", item.get("apply_id", item.get("value", item.get("id")))))
        if apply_id is None and item.get("applyId", item.get("apply_id", item.get("value", item.get("id")))) is not None:
            apply_id = _stringify_json(item.get("applyId", item.get("apply_id", item.get("value", item.get("id")))))
        label = (
            _normalize_optional_text(item.get("label"))
            or _normalize_optional_text(item.get("name"))
            or _normalize_optional_text(item.get("title"))
            or _normalize_optional_text(item.get("displayValue"))
            or _normalize_optional_text(item.get("otherInfo"))
        )
        if label == apply_id:
            label = None
        key = (apply_id, label)
        if key in seen or (apply_id is None and label is None):
            continue
        seen.add(key)
        entry: JSONObject = {"apply_id": apply_id, "label": label}
        normalized_entries.append(entry)
    if not normalized_entries:
        return None
    return normalized_entries[0] if len(normalized_entries) == 1 else normalized_entries


def _normalize_address_output_value(value: JSONValue) -> JSONObject | None:
    parsed_value = _parse_json_like(value)
    if parsed_value in (None, "", []):
        return None
    if isinstance(parsed_value, dict):
        direct = {
            "province": _normalize_optional_text(parsed_value.get("province")),
            "city": _normalize_optional_text(parsed_value.get("city")),
            "district": _normalize_optional_text(parsed_value.get("district")),
            "detail": _normalize_optional_text(parsed_value.get("detail")),
        }
        if any(item is not None for item in direct.values()):
            return direct
        nested = parsed_value.get("values", parsed_value.get("parts"))
        if nested is not None:
            return _normalize_address_output_value(cast(JSONValue, nested))
    parts: list[str] = []
    indexed_parts: dict[int, str] = {}
    if isinstance(parsed_value, list):
        for item in parsed_value:
            if isinstance(item, dict):
                text = (
                    _normalize_optional_text(item.get("value"))
                    or _normalize_optional_text(item.get("name"))
                    or _normalize_optional_text(item.get("detail"))
                )
                if not text:
                    continue
                part_id = _coerce_count(item.get("id"))
                if part_id is not None and 1 <= part_id <= 4:
                    indexed_parts[part_id] = text
                else:
                    parts.append(text)
                continue
            text = _normalize_optional_text(item) or (_stringify_json(item) if item is not None else None)
            if text:
                parts.append(text)
    elif isinstance(parsed_value, str):
        parts.append(parsed_value)
    ordered = [
        indexed_parts.get(1),
        indexed_parts.get(2),
        indexed_parts.get(3),
        indexed_parts.get(4),
    ]
    remaining_parts = [item for item in parts if item]
    for idx, current in enumerate(ordered):
        if current is None and remaining_parts:
            ordered[idx] = remaining_parts.pop(0)
    if not any(item is not None for item in ordered):
        return None
    if ordered[0] is None and ordered[1] is None and ordered[2] is None and ordered[3] is None and remaining_parts:
        ordered[3] = remaining_parts[0]
    if ordered[0] is None and ordered[1] is None and ordered[2] is None and len(parts) == 1:
        ordered[3] = parts[0]
    return {
        "province": ordered[0],
        "city": ordered[1],
        "district": ordered[2],
        "detail": ordered[3],
    }


def _normalize_subtable_answer_value_for_output(answer: JSONObject, field: FormField) -> JSONValue:
    table_values = answer.get("tableValues")
    if not isinstance(table_values, list) or not table_values:
        return None
    subtable_columns = {
        cast(int, item["que_id"]): item
        for item in _subtable_columns_for_field(field)
        if isinstance(item.get("que_id"), int)
    }
    normalized_rows: list[JSONObject] = []
    for raw_row in table_values:
        if not isinstance(raw_row, list):
            continue
        row_payload: JSONObject = {}
        for cell in raw_row:
            if not isinstance(cell, dict):
                continue
            que_id = _coerce_count(cell.get("queId"))
            subfield = subtable_columns.get(que_id) if que_id is not None else None
            title = _normalize_optional_text(subfield.get("que_title")) if isinstance(subfield, dict) else _normalize_optional_text(cell.get("queTitle"))
            if not title or title.startswith("-"):
                continue
            if isinstance(subfield, dict) and bool(subfield.get("system")):
                continue
            row_payload[title] = _normalize_answer_value_for_output_by_que_type(
                cell,
                _coerce_count(subfield.get("que_type")) if isinstance(subfield, dict) else _coerce_count(cell.get("queType")),
            )
        if row_payload:
            normalized_rows.append(row_payload)
    return normalized_rows or None


def _subtable_answer_has_unmapped_cells(answer: JSONObject, field: FormField) -> bool:
    table_values = answer.get("tableValues")
    if not isinstance(table_values, list) or not table_values:
        return False
    subtable_column_ids = {
        cast(int, item["que_id"])
        for item in _subtable_columns_for_field(field)
        if isinstance(item.get("que_id"), int)
    }
    if not subtable_column_ids:
        return False
    for raw_row in table_values:
        if not isinstance(raw_row, list):
            continue
        for cell in raw_row:
            if not isinstance(cell, dict):
                continue
            que_id = _coerce_count(cell.get("queId"))
            if que_id is None or que_id in subtable_column_ids:
                continue
            if _answer_has_meaningful_content(cell):
                return True
    return False


def _subtable_output_effectively_empty(value: JSONValue) -> bool:
    if value in (None, "", [], {}):
        return True
    if isinstance(value, dict):
        rows = value.get("rows")
        if not isinstance(rows, list) or not rows:
            return True
        return all(
            isinstance(row, dict) and set(row.keys()) <= {"__row_id__"}
            for row in rows
        )
    return False


def _canonicalize_answer_value_for_compare(answer: JSONObject | None, field: FormField | None) -> JSONValue:
    if answer is None or field is None:
        return None
    if field.que_type in MEMBER_QUE_TYPES:
        return _canonicalize_entity_answer_value(answer, id_keys=("id", "uid", "userId"), label_keys=("value", "name", "email"))
    if field.que_type in DEPARTMENT_QUE_TYPES:
        return _canonicalize_entity_answer_value(answer, id_keys=("id", "deptId"), label_keys=("value", "name", "deptName"))
    if field.que_type in RELATION_QUE_TYPES:
        return _canonicalize_relation_answer_value(answer)
    if field.que_type in MULTI_SELECT_QUE_TYPES:
        normalized = _normalize_answer_value_for_output_by_que_type(answer, field.que_type)
        if isinstance(normalized, list):
            return sorted(_stringify_json(item) for item in normalized)
        if normalized is None:
            return []
        return sorted([_stringify_json(normalized)])
    return _normalize_answer_field_value_for_output(answer, field)


def _canonicalize_entity_answer_value(
    answer: JSONObject,
    *,
    id_keys: tuple[str, ...],
    label_keys: tuple[str, ...],
) -> list[JSONObject]:
    values = answer.get("values")
    if not isinstance(values, list):
        return []
    canonical: list[JSONObject] = []
    seen: set[tuple[str | None, str | None]] = set()
    for item in values:
        if isinstance(item, dict):
            entity_id: str | None = None
            for key in id_keys:
                raw_id = item.get(key)
                entity_id = _normalize_optional_text(raw_id) or (_stringify_json(raw_id) if raw_id is not None else None)
                if entity_id:
                    break
            label: str | None = None
            for key in label_keys:
                raw_label = item.get(key)
                label = _normalize_optional_text(raw_label) or (_stringify_json(raw_label) if raw_label is not None else None)
                if label:
                    break
            identity = (entity_id, label)
            if identity in seen or (entity_id is None and label is None):
                continue
            seen.add(identity)
            canonical.append({"id": entity_id, "label": label})
            continue
        label = _normalize_optional_text(item) or (_stringify_json(item) if item is not None else None)
        if label is None:
            continue
        identity = (None, label)
        if identity in seen:
            continue
        seen.add(identity)
        canonical.append({"id": None, "label": label})
    canonical.sort(key=lambda item: ((item.get("id") or ""), (item.get("label") or "")))
    return canonical


def _canonicalize_relation_answer_value(answer: JSONObject) -> list[JSONObject]:
    normalized = _normalize_relation_answer_value_for_output(answer)
    if normalized is None:
        return []
    entries = normalized if isinstance(normalized, list) else [normalized]
    canonical: list[JSONObject] = []
    seen: set[tuple[str | None, str | None]] = set()
    for item in entries:
        if not isinstance(item, dict):
            continue
        apply_id = _normalize_optional_text(item.get("apply_id")) or (_stringify_json(item.get("apply_id")) if item.get("apply_id") is not None else None)
        label = _normalize_optional_text(item.get("label")) or (_stringify_json(item.get("label")) if item.get("label") is not None else None)
        identity = (apply_id, label)
        if identity in seen or (apply_id is None and label is None):
            continue
        seen.add(identity)
        canonical.append({"apply_id": apply_id, "label": label})
    canonical.sort(key=lambda item: ((item.get("apply_id") or ""), (item.get("label") or "")))
    return canonical


def _canonical_value_is_empty(value: JSONValue) -> bool:
    return value in (None, "", [], {})


def _extract_field_value(answer_list: list[JSONValue], field: FormField | None) -> JSONValue:
    answer = _find_answer_for_field(answer_list, field)
    if answer is None:
        return None
    return _extract_answer_field_value(answer, field)


def _extract_answer_field_value(answer: JSONObject, field: FormField | None) -> JSONValue:
    if field is not None and field.que_type in SUBTABLE_QUE_TYPES:
        return _extract_subtable_value(answer, field)
    if field is not None and field.que_type in ADDRESS_QUE_TYPES:
        return _extract_address_value(answer)
    if field is not None and field.que_type in RELATION_QUE_TYPES:
        return _extract_relation_value(answer)
    values = answer.get("values")
    if not isinstance(values, list) or not values:
        return None
    if field is not None and field.que_type in ATTACHMENT_QUE_TYPES:
        extracted_attachments = [_extract_attachment_item(item) for item in values]
        extracted_attachments = [item for item in extracted_attachments if item is not None]
        if not extracted_attachments:
            return None
        return extracted_attachments[0] if len(extracted_attachments) == 1 else extracted_attachments
    extracted = [_extract_value_item(item) for item in values]
    return extracted[0] if len(extracted) == 1 else extracted


def _extract_subtable_value(answer: JSONObject, field: FormField) -> JSONValue:
    table_values = answer.get("tableValues")
    if not isinstance(table_values, list) or not table_values:
        return None
    subtable_columns = {
        cast(int, item["que_id"]): item
        for item in _subtable_columns_for_field(field)
        if isinstance(item.get("que_id"), int)
    }
    extracted_rows: list[JSONObject] = []
    for raw_row in table_values:
        if not isinstance(raw_row, list):
            continue
        normalized_row = [item for item in raw_row if isinstance(item, dict)]
        if not normalized_row:
            continue
        row_payload: JSONObject = {}
        row_id = _subtable_row_id(normalized_row)
        if row_id is not None:
            row_payload["__row_id__"] = row_id
        for cell in normalized_row:
            que_id = _coerce_count(cell.get("queId"))
            subfield = subtable_columns.get(que_id) if que_id is not None else None
            title = _normalize_optional_text(subfield.get("que_title")) if isinstance(subfield, dict) else _normalize_optional_text(cell.get("queTitle"))
            if not title:
                continue
            values = cell.get("values")
            subfield_que_type = _coerce_count(subfield.get("que_type")) if isinstance(subfield, dict) else None
            if subfield_que_type in ATTACHMENT_QUE_TYPES:
                attachments = [_extract_attachment_item(item) for item in values] if isinstance(values, list) else []
                attachments = [item for item in attachments if item is not None]
                row_payload[title] = None if not attachments else (attachments[0] if len(attachments) == 1 else attachments)
            elif subfield_que_type in ADDRESS_QUE_TYPES:
                row_payload[title] = _extract_address_value(cell)
            elif subfield_que_type in RELATION_QUE_TYPES:
                row_payload[title] = _extract_relation_value(cell)
            else:
                extracted = [_extract_value_item(item) for item in values] if isinstance(values, list) else []
                row_payload[title] = None if not extracted else (extracted[0] if len(extracted) == 1 else extracted)
        if row_payload:
            extracted_rows.append(row_payload)
    if not extracted_rows:
        return None
    return {"rows": extracted_rows}


def _extract_value_item(value: JSONValue) -> JSONValue:
    if isinstance(value, dict):
        if "value" in value:
            return value["value"]
        if "name" in value:
            return value["name"]
        if "email" in value:
            return value["email"]
        if "id" in value:
            return value["id"]
    return value


def _field_mapping_entry(role: str, field: FormField | None, *, requested: str) -> JSONObject:
    return {
        "role": role,
        "requested": requested,
        "resolved": field is not None,
        "que_id": field.que_id if field is not None else None,
        "que_title": field.que_title if field is not None else None,
        "que_type": field.que_type if field is not None else None,
    }


def _record_resource_payload(record_id: Any) -> JSONObject | None:
    public_record_id = _public_record_id_text(record_id)
    if public_record_id is None:
        return None
    return {"type": "record", "record_id": public_record_id, "apply_id": public_record_id}


def _public_record_resource(resource: Any) -> Any:
    if not isinstance(resource, dict) or resource.get("type") != "record":
        return resource
    payload = dict(resource)
    if "record_id" in payload:
        payload["record_id"] = _public_record_id_text(payload.get("record_id"))
    if "apply_id" in payload:
        payload["apply_id"] = _public_record_id_text(payload.get("apply_id"))
    record_ids = payload.get("record_ids")
    if isinstance(record_ids, list):
        payload["record_ids"] = [
            stringify_backend_id(item)
            for item in record_ids
            if stringify_backend_id(item) is not None
        ]
    return payload


def _query_id() -> str:
    return datetime.now(UTC).isoformat()


def _view_selection_payload(view_selection: ViewSelection | None) -> JSONObject | None:
    if view_selection is None:
        return None
    return {
        "view_key": view_selection.view_key,
        "view_name": view_selection.view_name,
        "local_filtering": bool(view_selection.conditions),
        "condition_group_count": len(view_selection.conditions),
        "filter_config_loaded": view_selection.filter_config_loaded,
    }


def _accessible_view_payload(view_route: AccessibleViewRoute | None) -> JSONObject | None:
    if view_route is None:
        return None
    payload: JSONObject = {
        "view_id": view_route.view_id,
        "name": view_route.name,
        "kind": view_route.kind,
        "analysis_supported": _view_type_supports_analysis(view_route.view_type),
    }
    if view_route.view_type is not None:
        payload["view_type"] = view_route.view_type
    if view_route.list_type is not None:
        payload["list_type"] = view_route.list_type
    selection_payload = _view_selection_payload(view_route.view_selection)
    if selection_payload is not None:
        payload.update(selection_payload)
    return payload


def _view_filter_verification_payload(view_route: AccessibleViewRoute | None) -> JSONObject:
    if view_route is None:
        return {"scope_source": "unknown", "view_filter_verified": False}
    if view_route.kind == "custom":
        return {
            "scope_source": "custom_view",
            "view_filter_verified": bool(view_route.view_selection and view_route.view_selection.filter_config_loaded),
        }
    return {
        "scope_source": "system_view",
        "view_filter_verified": True,
    }


def _view_filter_trust_warnings(view_route: AccessibleViewRoute | None) -> list[JSONObject]:
    if (
        view_route is None
        or view_route.kind != "custom"
        or (view_route.view_selection is not None and view_route.view_selection.filter_config_loaded)
    ):
        return []
    return [
        {
            "code": "CUSTOM_VIEW_FILTER_UNVERIFIED",
            "message": (
                "custom view results are usable, but MCP cannot verify that the backend "
                "applied the saved view filters exactly as configured."
            ),
        }
    ]


def _compute_scan_limit(
    *,
    requested_pages: int,
    scan_max_pages: int,
    auto_expand_pages: bool,
    page: JSONObject | None = None,
    page_size: int | None = None,
) -> tuple[int, JSONObject]:
    base_requested = max(requested_pages, 1)
    base_scan_max = max(scan_max_pages, 1)
    initial_limit = min(base_requested, base_scan_max)
    meta: JSONObject = {
        "requested_pages": base_requested,
        "scan_max_pages": base_scan_max,
        "auto_expand_pages": auto_expand_pages,
        "auto_expand_applied": False,
        "auto_expand_target_pages": initial_limit,
        "auto_expand_page_cap": DEFAULT_ANALYSIS_AUTO_EXPAND_PAGE_CAP,
    }
    if not auto_expand_pages or not isinstance(page, dict):
        return initial_limit, meta
    effective_page_size = max(page_size or 0, 1)
    backend_total = _effective_total(page, effective_page_size)
    page_amount = _coerce_count(page.get("pageAmount"))
    target_pages = page_amount
    if target_pages is None and backend_total > 0:
        target_pages = (backend_total + effective_page_size - 1) // effective_page_size
    if target_pages is None:
        return initial_limit, meta
    expanded_limit = min(max(initial_limit, target_pages), DEFAULT_ANALYSIS_AUTO_EXPAND_PAGE_CAP)
    if expanded_limit > initial_limit:
        meta["auto_expand_applied"] = True
    meta["auto_expand_target_pages"] = target_pages
    meta["backend_total_count"] = backend_total
    meta["backend_page_amount"] = page_amount
    return expanded_limit, meta


def _build_analysis_counts(
    *,
    backend_total_count: int | None,
    scanned_count: int,
    grouped_count: int,
    local_filtering: bool,
) -> JSONObject:
    unscanned_count: int | None = None
    if backend_total_count is not None and not local_filtering:
        unscanned_count = max(backend_total_count - scanned_count, 0)
    return {
        "backend_total_count": backend_total_count,
        "scanned_count": scanned_count,
        "grouped_count": grouped_count,
        "unscanned_count": unscanned_count,
        "local_filtering": local_filtering,
    }


def _analysis_status_from_completeness(completeness: JSONObject) -> tuple[str, bool]:
    raw_scan_complete = bool(completeness.get("raw_scan_complete"))
    status = "success" if raw_scan_complete else "partial_success"
    return status, raw_scan_complete


def _build_completeness(
    *,
    result_amount: int,
    returned_items: int,
    fetched_pages: int,
    requested_pages: int,
    has_more: bool,
    next_page_token: str | None,
    is_complete: bool,
    omitted_items: int,
    extra: JSONObject,
) -> JSONObject:
    payload: JSONObject = {
        "result_amount": result_amount,
        "returned_items": returned_items,
        "fetched_pages": fetched_pages,
        "requested_pages": requested_pages,
        "actual_scanned_pages": fetched_pages,
        "has_more": has_more,
        "next_page_token": next_page_token,
        "is_complete": is_complete,
        "partial": not is_complete,
        "omitted_items": omitted_items,
        "omitted_chars": 0,
    }
    payload.update(extra)
    return payload


def _page_has_more(page: JSONObject, current_page: int, page_size: int, returned_rows: int) -> bool:
    page_amount = _coerce_count(page.get("pageAmount"))
    if page_amount is not None:
        return current_page < page_amount
    return returned_rows >= page_size


def _effective_total(page: JSONObject, page_size: int) -> int:
    rows = page.get("list")
    returned_rows = len(rows) if isinstance(rows, list) else 0
    reported = _coerce_count(page.get("total"))
    if reported is None:
        reported = _coerce_count(page.get("count"))
    if reported is not None:
        return max(reported, returned_rows)
    page_amount = _coerce_count(page.get("pageAmount"))
    if page_amount is not None:
        return page_amount * page_size
    return returned_rows


def _pick_title_field(index: FieldIndex) -> FormField | None:
    priority_keywords = ("名称", "标题", "主题", "name", "title", "subject")
    for field in index.by_id.values():
        lowered = field.que_title.lower()
        if any(keyword in lowered for keyword in priority_keywords):
            return field
    return next(iter(index.by_id.values()), None)


def _normalize_metrics(metrics: list[str], *, include_sum: bool) -> list[str]:
    normalized = [item for item in metrics if item in {"count", "sum", "avg", "min", "max"}]
    if not normalized:
        return ["count", "sum"] if include_sum else ["count"]
    if not include_sum:
        return [item for item in normalized if item == "count"] or ["count"]
    return normalized


def _coerce_count(value: JSONValue) -> int | None:
    if isinstance(value, bool) or value is None:
        return None
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return int(value)
    if isinstance(value, str):
        text = value.strip()
        if not text:
            return None
        try:
            return int(text)
        except ValueError:
            return None
    return None


def _coerce_amount(value: JSONValue) -> float | None:
    if isinstance(value, bool) or value is None:
        return None
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, str):
        text = value.strip()
        if not text:
            return None
        negative = False
        if text.startswith("(") and text.endswith(")"):
            negative = True
            text = text[1:-1].strip()
        for symbol in ("¥", "￥", "$"):
            text = text.replace(symbol, "")
        text = text.replace(",", "").replace(" ", "")
        if negative and text and not text.startswith("-"):
            text = f"-{text}"
        try:
            return float(text)
        except ValueError:
            return None
    return None


def _normalize_amount_value_for_write(field: FormField, value: JSONValue) -> str:
    if isinstance(value, bool) or value is None:
        raise RecordInputError(
            message=f"field '{field.que_title}' requires a numeric amount",
            error_code="INVALID_AMOUNT_VALUE",
            fix_hint="Pass a numeric value or numeric string for the amount field.",
            details={"field": _field_ref_payload(field), "received_value": value},
        )
    if isinstance(value, int):
        return str(value)
    if isinstance(value, float):
        decimal_value = Decimal(str(value))
    elif isinstance(value, str):
        text = value.strip()
        if not text:
            raise RecordInputError(
                message=f"field '{field.que_title}' requires a numeric amount",
                error_code="INVALID_AMOUNT_VALUE",
                fix_hint="Pass a numeric value or numeric string for the amount field.",
                details={"field": _field_ref_payload(field), "received_value": value},
            )
        negative = False
        if text.startswith("(") and text.endswith(")"):
            negative = True
            text = text[1:-1].strip()
        for symbol in ("¥", "￥", "$"):
            text = text.replace(symbol, "")
        text = text.replace(",", "").replace(" ", "")
        if negative and text and not text.startswith("-"):
            text = f"-{text}"
        try:
            decimal_value = Decimal(text)
        except InvalidOperation as exc:
            raise RecordInputError(
                message=f"field '{field.que_title}' requires a numeric amount",
                error_code="INVALID_AMOUNT_VALUE",
                fix_hint="Pass a numeric value or numeric string for the amount field.",
                details={"field": _field_ref_payload(field), "received_value": value},
            ) from exc
    else:
        raise RecordInputError(
            message=f"field '{field.que_title}' requires a numeric amount",
            error_code="INVALID_AMOUNT_VALUE",
            fix_hint="Pass a numeric value or numeric string for the amount field.",
            details={"field": _field_ref_payload(field), "received_value": value},
        )

    allow_decimal = bool((field.raw or {}).get("canDecimal"))
    if not allow_decimal:
        integral_value = decimal_value.to_integral_value()
        if decimal_value != integral_value:
            raise RecordInputError(
                message=f"field '{field.que_title}' requires an integer amount",
                error_code="INVALID_AMOUNT_VALUE",
                fix_hint="Pass an integer value for this amount field, or remove the decimal part.",
                details={"field": _field_ref_payload(field), "received_value": value},
            )
        return format(integral_value, "f").split(".")[0]

    normalized = format(decimal_value, "f")
    if "." in normalized:
        normalized = normalized.rstrip("0").rstrip(".")
    return normalized or "0"


def _to_time_bucket(value: JSONValue, bucket: str) -> str:
    text = _stringify_json(value).strip()
    if not text:
        return "unknown"
    parsed = _parse_datetime_like(text)
    if parsed is None:
        return text[:10] if bucket == "day" and len(text) >= 10 else text
    if bucket == "month":
        return parsed.strftime("%Y-%m")
    if bucket == "week":
        iso_year, iso_week, _ = parsed.isocalendar()
        return f"{iso_year}-W{iso_week:02d}"
    if bucket == "quarter":
        quarter = ((parsed.month - 1) // 3) + 1
        return f"{parsed.year}-Q{quarter}"
    if bucket == "year":
        return parsed.strftime("%Y")
    return parsed.strftime("%Y-%m-%d")


def _parse_datetime_like(text: str) -> datetime | None:
    cleaned = text.strip().replace("/", "-")
    for candidate in (cleaned, cleaned.replace(" ", "T")):
        try:
            return datetime.fromisoformat(candidate)
        except ValueError:
            continue
    return None


def _flatten_distinct_values(value: JSONValue) -> list[str]:
    if value is None:
        return []
    if isinstance(value, list):
        items = value
    else:
        items = [value]
    flattened: list[str] = []
    for item in items:
        if item is None:
            continue
        if isinstance(item, (dict, list)):
            flattened.append(json.dumps(item, ensure_ascii=False, sort_keys=True))
        else:
            flattened.append(_stringify_json(item))
    return flattened


def _coerce_comparable(value: JSONValue) -> JSONValue:
    amount = _coerce_amount(value)
    if amount is not None:
        return amount
    text = _normalize_optional_text(value)
    if text is None:
        return None
    parsed = _parse_datetime_like(text)
    if parsed is not None:
        return parsed
    return text


def _match_analyze_filter(field_value: JSONValue, op: str, expected: JSONValue) -> bool:
    if op == "is_null":
        return field_value is None or field_value == [] or field_value == ""
    if op == "not_null":
        return not _match_analyze_filter(field_value, "is_null", expected)

    values = field_value if isinstance(field_value, list) else [field_value]
    normalized_values = [_coerce_comparable(item) for item in values if item is not None]

    if op == "contains":
        needle = _normalize_optional_text(expected)
        if needle is None:
            return False
        return any(needle in _stringify_json(item) for item in values if item is not None)

    if op in {"in", "not_in"}:
        expected_values = expected if isinstance(expected, list) else [expected]
        matched = any(
            _analyze_values_equal(actual=item, expected=expected_item)
            for item in values
            if item is not None
            for expected_item in expected_values
            if expected_item is not None
        )
        return matched if op == "in" else not matched

    if op in {"eq", "neq"}:
        matched = any(_analyze_values_equal(actual=item, expected=expected) for item in values if item is not None)
        return matched if op == "eq" else not matched

    if op == "between":
        lower, upper = _coerce_filter_range(expected)
        lower_value = _coerce_comparable(lower)
        upper_value = _coerce_comparable(upper)
        for item in normalized_values:
            if item is None:
                continue
            try:
                if lower_value is not None and item < lower_value:
                    continue
                if upper_value is not None and item > upper_value:
                    continue
                return True
            except TypeError:
                continue
        return False

    target = _coerce_comparable(expected)
    for item in normalized_values:
        if item is None or target is None:
            continue
        try:
            if op == "gt" and item > target:
                return True
            if op == "gte" and item >= target:
                return True
            if op == "lt" and item < target:
                return True
            if op == "lte" and item <= target:
                return True
        except TypeError:
            continue
    return False


def _analyze_values_equal(*, actual: JSONValue, expected: JSONValue) -> bool:
    actual_comparable = _coerce_comparable(actual)
    expected_comparable = _coerce_comparable(expected)
    if actual_comparable is not None and expected_comparable is not None and type(actual_comparable) is type(expected_comparable):
        return actual_comparable == expected_comparable
    return _stringify_json(actual) == _stringify_json(expected)


def _sortable_value(value: JSONValue) -> tuple[int, JSONValue]:
    if value is None:
        return (1, "")
    comparable = _coerce_comparable(value)
    return (0, comparable if comparable is not None else "")


def _echo_filters(filters: list[JSONObject]) -> list[JSONObject]:
    return [dict(item) for item in filters]


def _extract_filter_selector(item: JSONObject) -> JSONValue:
    for key in ("que_id", "queId", "field", "field_id", "fieldId", "column", "queTitle", "que_title"):
        if key in item:
            return item[key]
    return None


def _extract_sort_selector(item: JSONObject) -> JSONValue:
    for key in ("que_id", "queId", "field", "field_id", "fieldId", "column", "queTitle", "que_title"):
        if key in item:
            return item[key]
    return None


def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
    normalized: list[int] = []
    for item in columns:
        field_id: int | None = None
        if isinstance(item, int):
            field_id = item
        elif isinstance(item, str):
            field_id = _coerce_count(item)
        elif isinstance(item, dict):
            _ensure_allowed_record_list_keys(
                item,
                location="columns[]",
                allowed_keys={"field_id", "fieldId"},
                example="{'field_id': 12}",
            )
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
        if field_id is None or field_id < 0:
            raise_tool_error(
                QingflowApiError.config_error(
                    "columns must be a list of field_id integers, integer strings, or {field_id} objects"
                )
            )
        normalized.append(field_id)
    return normalized


def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
    normalized: list[int] = []
    for item in query_fields:
        field_id: int | None = None
        if isinstance(item, int):
            field_id = item
        elif isinstance(item, str):
            field_id = _coerce_count(item)
        elif isinstance(item, dict):
            _ensure_allowed_record_list_keys(
                item,
                location="query_fields[]",
                allowed_keys={"field_id", "fieldId"},
                example="{'field_id': 12}",
            )
            field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
        if field_id is None or field_id < 0:
            raise_tool_error(
                QingflowApiError.config_error(
                    "query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
                )
            )
        normalized.append(field_id)
    return normalized


def _column_selector_payload(field_id: int) -> JSONObject:
    return {"field_id": field_id}


def _ensure_allowed_record_list_keys(
    item: JSONObject,
    *,
    location: str,
    allowed_keys: set[str],
    example: str,
) -> None:
    unexpected_keys = sorted(str(key) for key in item.keys() if str(key) not in allowed_keys)
    if unexpected_keys:
        raise_tool_error(
            QingflowApiError.config_error(
                f"{location} contains unsupported keys: {unexpected_keys}. Use {example}."
            )
        )


def _detect_record_list_legacy_warnings(
    *,
    columns: list[JSONObject | int],
    where: list[JSONObject],
    order_by: list[JSONObject],
) -> list[JSONObject]:
    warnings: list[JSONObject] = []
    if any(isinstance(item, int) or (isinstance(item, dict) and "fieldId" in item) for item in columns):
        warnings.append(
            {
                "code": "LEGACY_LIST_COLUMNS_DSL",
                "message": "Use columns as [{field_id}] objects. Bare integers and fieldId are compatibility-only.",
            }
        )
    if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "operator", "values")) for item in where):
        warnings.append(
            {
                "code": "LEGACY_LIST_FILTER_DSL",
                "message": "Use where items as {field_id, op, value}. fieldId/operator/values are compatibility-only.",
            }
        )
    if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "order")) for item in order_by):
        warnings.append(
            {
                "code": "LEGACY_LIST_SORT_DSL",
                "message": "Use order_by items as {field_id, direction}. fieldId/order are compatibility-only.",
            }
        )
    return warnings


def _detect_analyze_legacy_warnings(
    *,
    dimensions: list[JSONObject],
    metrics: list[JSONObject],
    filters: list[JSONObject],
    sort: list[JSONObject],
) -> list[JSONObject]:
    warnings: list[JSONObject] = []
    if any(isinstance(item, dict) and "fieldId" in item for item in dimensions):
        warnings.append(
            {
                "code": "LEGACY_ANALYZE_DIMENSION_DSL",
                "message": "Use dimensions as {field_id, alias, bucket}. fieldId is compatibility-only.",
            }
        )
    if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "type", "agg", "aggregation")) for item in metrics):
        warnings.append(
            {
                "code": "LEGACY_ANALYZE_METRIC_DSL",
                "message": "Use metrics as {op, field_id, alias}. fieldId/type/agg/aggregation are compatibility-only.",
            }
        )
    if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "operator", "values")) for item in filters):
        warnings.append(
            {
                "code": "LEGACY_ANALYZE_FILTER_DSL",
                "message": "Use filters as {field_id, op, value}. fieldId/operator/values are compatibility-only.",
            }
        )
    if any(isinstance(item, dict) and "direction" in item for item in sort):
        warnings.append(
            {
                "code": "LEGACY_ANALYZE_SORT_DSL",
                "message": "Use sort items as {by, order}. direction is compatibility-only.",
            }
        )
    return warnings


def _resolve_sort_ascend(item: JSONObject) -> bool:
    if "isAscend" in item:
        return bool(item["isAscend"])
    if "ascend" in item:
        return bool(item["ascend"])
    direction = _stringify_json(item.get("direction", item.get("order"))).strip().lower()
    return direction not in {"desc", "descending", "-1"}


def _coerce_filter_range(value: JSONValue) -> tuple[str | None, str | None]:
    if isinstance(value, dict):
        lower = value.get("from", value.get("min", value.get("start")))
        upper = value.get("to", value.get("max", value.get("end")))
        return _stringify_json(lower) if lower is not None else None, _stringify_json(upper) if upper is not None else None
    if isinstance(value, list) and value:
        lower = value[0] if len(value) >= 1 else None
        upper = value[1] if len(value) >= 2 else None
        return _stringify_json(lower) if lower is not None else None, _stringify_json(upper) if upper is not None else None
    return None, None


def _normalize_filter_values(value: JSONValue) -> list[str]:
    if isinstance(value, list):
        return [_stringify_json(item) for item in value]
    return [_stringify_json(value)]


def _normalize_optional_text(value: JSONValue) -> str | None:
    if isinstance(value, str):
        text = value.strip()
        return text or None
    if isinstance(value, (int, float)):
        text = str(value).strip()
        return text or None
    return None


def _match_view_condition(
    answer_list: list[JSONValue],
    condition: ViewFilterCondition,
    *,
    dept_member_cache: dict[int, set[int]],
    dept_member_resolver,
) -> bool:  # type: ignore[no-untyped-def]
    answer = _find_answer_for_condition(answer_list, condition)
    if answer is None:
        return False
    values = answer.get("values")
    answer_values = values if isinstance(values, list) else []
    if not answer_values:
        return condition.judge_type == JUDGE_UNEQUAL and not bool(condition.judge_values or condition.judge_value_details)
    if condition.que_type in MEMBER_QUE_TYPES:
        dept_ids = _extract_condition_dept_ids(condition)
        if dept_ids:
            allowed_ids: set[int] = set()
            for dept_id in dept_ids:
                if dept_id not in dept_member_cache:
                    dept_member_cache[dept_id] = set(dept_member_resolver(dept_id))
                allowed_ids.update(dept_member_cache[dept_id])
            return any(
                member_id in allowed_ids
                for member_id in (_coerce_count(item.get("id")) if isinstance(item, dict) else None for item in answer_values)
                if member_id is not None
            )
    condition_ids = _extract_condition_ids(condition)
    if condition_ids:
        answer_ids = [
            member_id
            for member_id in (_coerce_count(item.get("id")) if isinstance(item, dict) else None for item in answer_values)
            if member_id is not None
        ]
        if any(answer_id in condition_ids for answer_id in answer_ids):
            return True
    expected_values: list[JSONValue] = []
    if isinstance(condition.judge_value_details, list):
        for item in condition.judge_value_details:
            if not isinstance(item, dict):
                continue
            for candidate in _extract_answer_probe_values(item):
                comparable = _coerce_comparable(candidate)
                if comparable is not None:
                    expected_values.append(comparable)
    if condition.judge_values:
        for item in condition.judge_values:
            comparable = _coerce_comparable(item)
            if comparable is not None:
                expected_values.append(comparable)
    if expected_values:
        state = _evaluate_question_relation_value_match(answer_values, expected_values, condition.judge_type)
        if state is not None:
            return state
    condition_texts = _extract_condition_texts(condition)
    if not condition_texts:
        return False
    answer_texts = [_normalize_answer_value_text(item) for item in answer_values]
    if condition.judge_type == JUDGE_UNEQUAL:
        return not any(answer_text in condition_texts for answer_text in answer_texts if answer_text)
    return any(answer_text in condition_texts for answer_text in answer_texts if answer_text)


def _find_answer_for_condition(answer_list: list[JSONValue], condition: ViewFilterCondition) -> JSONObject | None:
    target_title = condition.que_title.strip()
    for answer in answer_list:
        if not isinstance(answer, dict):
            continue
        answer_que_id = _coerce_count(answer.get("queId"))
        if condition.que_id is not None and answer_que_id == condition.que_id:
            return answer
        answer_title = _stringify_json(answer.get("queTitle")).strip()
        if target_title and answer_title == target_title:
            return answer
    return None


def _extract_condition_dept_ids(condition: ViewFilterCondition) -> list[int]:
    dept_ids: list[int] = []
    for raw in condition.judge_values:
        if raw.startswith(DEPARTMENT_MEMBER_JUDGE_PREFIX):
            dept_id = _coerce_count(raw[len(DEPARTMENT_MEMBER_JUDGE_PREFIX):])
            if dept_id is not None:
                dept_ids.append(dept_id)
    return dept_ids


def _extract_condition_ids(condition: ViewFilterCondition) -> set[int]:
    ids: set[int] = set()
    for raw in condition.judge_values:
        if raw.startswith(DEPARTMENT_MEMBER_JUDGE_PREFIX):
            continue
        value = _coerce_count(raw)
        if value is not None:
            ids.add(value)
    for item in condition.judge_value_details:
        value = _coerce_count(item.get("id", item.get("uid")))
        if value is not None:
            ids.add(value)
    return ids


def _extract_condition_texts(condition: ViewFilterCondition) -> set[str]:
    values: set[str] = set()
    for raw in condition.judge_values:
        if raw.startswith(DEPARTMENT_MEMBER_JUDGE_PREFIX):
            continue
        normalized = raw.strip()
        if normalized:
            values.add(normalized)
    for item in condition.judge_value_details:
        for key in ("value", "dataValue", "name"):
            normalized = _normalize_optional_text(item.get(key))
            if normalized:
                values.add(normalized)
    return values


def _normalize_answer_value_text(value: JSONValue) -> str | None:
    if isinstance(value, dict):
        for key in ("value", "dataValue", "name", "email"):
            normalized = _normalize_optional_text(value.get(key))
            if normalized:
                return normalized
        value_id = _coerce_count(value.get("id", value.get("uid")))
        return str(value_id) if value_id is not None else None
    return _normalize_optional_text(value)


def _normalize_list_query_rules(item: JSONObject) -> list[JSONObject]:
    if _looks_like_backend_match_rule(item):
        rule = _match_rule_to_query_rule(item)
        return [rule] if rule else []
    que_id = item.get("queId", item.get("que_id"))
    que_type = _coerce_count(item.get("queType", item.get("que_type")))
    if que_type in DEPARTMENT_QUE_TYPES and _department_filter_details_from_item(item):
        return []
    rule: JSONObject = {}
    if que_id is not None:
        rule["queId"] = que_id
    if que_type is not None:
        rule["queType"] = que_type
    for source, target in (
        ("searchKey", "searchKey"),
        ("search_key", "searchKey"),
        ("searchKeys", "searchKeys"),
        ("search_keys", "searchKeys"),
        ("minValue", "minValue"),
        ("min_value", "minValue"),
        ("maxValue", "maxValue"),
        ("max_value", "maxValue"),
        ("scope", "scope"),
        ("searchOptions", "searchOptions"),
        ("search_options", "searchOptions"),
        ("searchUserIds", "searchUserIds"),
        ("search_user_ids", "searchUserIds"),
        ("searchUids", "searchUids"),
        ("search_uids", "searchUids"),
    ):
        if source in item:
            rule[target] = item[source]
    return [rule] if rule else []


def _normalize_list_match_rules(item: JSONObject) -> list[JSONObject]:
    if _looks_like_backend_match_rule(item):
        normalized = _normalize_backend_match_rule(item)
        return [normalized] if normalized else []
    que_id = _coerce_count(item.get("queId", item.get("que_id")))
    que_type = _coerce_count(item.get("queType", item.get("que_type")))
    if que_id is None:
        return []
    if que_type in DEPARTMENT_QUE_TYPES:
        details = _department_filter_details_from_item(item)
        if details:
            judge_type = JUDGE_EQUAL if len(details) == 1 else JUDGE_EQUAL_ANY
            return [_department_filter_rule(que_id, que_type, details, judge_type=judge_type)]
    min_value = item.get("minValue", item.get("min_value"))
    max_value = item.get("maxValue", item.get("max_value"))
    if que_type in DATE_QUE_TYPES and (min_value is not None or max_value is not None):
        return []
    match_rules: list[JSONObject] = []
    scope = _coerce_count(item.get("scope"))
    if scope is not None and scope != SCOPE_ALL:
        match_rule: JSONObject = {
            "queId": que_id,
            "judgeType": JUDGE_UNEQUAL if scope == SCOPE_NOT_EMPTY else JUDGE_EQUAL,
            "judgeValues": [],
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    search_key = _normalize_optional_text(item.get("searchKey", item.get("search_key")))
    if search_key is not None:
        match_rule = {
            "queId": que_id,
            "judgeType": JUDGE_FUZZY_MATCH,
            "judgeValues": [search_key],
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    search_keys = item.get("searchKeys", item.get("search_keys"))
    normalized_search_keys = [_stringify_json(entry) for entry in search_keys] if isinstance(search_keys, list) else []
    if normalized_search_keys:
        judge_type = JUDGE_INCLUDE_ANY if que_type in MULTI_SELECT_QUE_TYPES | MEMBER_QUE_TYPES else JUDGE_EQUAL_ANY
        match_rule = {
            "queId": que_id,
            "judgeType": judge_type,
            "judgeValues": normalized_search_keys,
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    if min_value is not None:
        match_rule = {
            "queId": que_id,
            "judgeType": JUDGE_GREATER_OR_EQUAL,
            "judgeValues": [_stringify_json(min_value)],
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    if max_value is not None:
        match_rule = {
            "queId": que_id,
            "judgeType": JUDGE_LESS_OR_EQUAL,
            "judgeValues": [_stringify_json(max_value)],
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    search_options = item.get("searchOptions", item.get("search_options"))
    normalized_search_options = [_stringify_json(entry) for entry in search_options] if isinstance(search_options, list) else []
    if normalized_search_options:
        judge_type = JUDGE_INCLUDE_ANY if que_type in MULTI_SELECT_QUE_TYPES else JUDGE_EQUAL_ANY
        match_rule = {
            "queId": que_id,
            "judgeType": judge_type,
            "judgeValues": normalized_search_options,
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    search_uids = item.get("searchUids", item.get("search_uids", item.get("searchUserIds", item.get("search_user_ids"))))
    normalized_search_uids = [str(member_id) for member_id in _normalize_member_filter_ids(search_uids)] if search_uids is not None else []
    if normalized_search_uids:
        match_rule = {
            "queId": que_id,
            "judgeType": JUDGE_INCLUDE_ANY,
            "judgeValues": normalized_search_uids,
            "matchType": MATCH_TYPE_ACCURACY,
        }
        if que_type is not None:
            match_rule["queType"] = que_type
        match_rules.append(match_rule)
    return match_rules


def _looks_like_backend_match_rule(item: JSONObject) -> bool:
    return any(key in item for key in ("judgeType", "judge_type", "judgeValues", "judge_values", "judgeValueDetails", "judge_value_details"))


def _normalize_backend_match_rule(item: JSONObject) -> JSONObject:
    que_id = _coerce_count(item.get("queId", item.get("que_id")))
    if que_id is None:
        return {}
    rule: JSONObject = {"queId": que_id}
    que_title = _normalize_optional_text(item.get("queTitle", item.get("que_title")))
    if que_title is not None:
        rule["queTitle"] = que_title
    que_type = _coerce_count(item.get("queType", item.get("que_type")))
    if que_type is not None:
        rule["queType"] = que_type
    judge_type = _coerce_count(item.get("judgeType", item.get("judge_type")))
    if judge_type is not None:
        rule["judgeType"] = judge_type
    judge_values = item.get("judgeValues", item.get("judge_values"))
    if isinstance(judge_values, list):
        rule["judgeValues"] = [_stringify_json(value) for value in judge_values]
    judge_value_details = item.get("judgeValueDetails", item.get("judge_value_details"))
    if isinstance(judge_value_details, list):
        rule["judgeValueDetails"] = [value for value in judge_value_details if isinstance(value, dict)]
    control_value = _coerce_count(item.get("controlValue", item.get("control_value")))
    if control_value is not None:
        rule["controlValue"] = control_value
    control_time_start_value = item.get("controlTimeStartValue", item.get("control_time_start_value"))
    if control_time_start_value is not None:
        rule["controlTimeStartValue"] = _stringify_json(control_time_start_value)
    control_time_end_value = item.get("controlTimeEndValue", item.get("control_time_end_value"))
    if control_time_end_value is not None:
        rule["controlTimeEndValue"] = _stringify_json(control_time_end_value)
    match_type = _coerce_count(item.get("matchType", item.get("match_type")))
    rule["matchType"] = match_type if match_type is not None else MATCH_TYPE_ACCURACY
    return rule


def _department_filter_rule(
    que_id: int,
    que_type: int | None,
    details: list[JSONObject],
    *,
    judge_type: int,
) -> JSONObject:
    rule: JSONObject = {
        "queId": que_id,
        "judgeType": judge_type,
        "judgeValues": [str(detail["id"]) for detail in details if detail.get("id") is not None],
        "judgeValueDetails": [
            {"id": detail["id"], "value": _stringify_json(detail.get("value", detail["id"]))}
            for detail in details
            if detail.get("id") is not None
        ],
        "matchType": MATCH_TYPE_ACCURACY,
    }
    if que_type is not None:
        rule["queType"] = que_type
    return rule


def _department_filter_details_from_item(item: JSONObject) -> list[JSONObject]:
    values: list[JSONValue] = []
    for key in ("searchOptions", "search_options", "searchKeys", "search_keys", "searchKey", "search_key", "value", "values"):
        if key in item:
            raw = item[key]
            values = raw if isinstance(raw, list) else [raw]
            break
    details: list[JSONObject] = []
    seen: set[int] = set()
    for value in _expand_values(values):
        detail = _department_filter_detail_from_value(value)
        if detail is None:
            continue
        dept_id = _coerce_count(detail.get("id"))
        if dept_id is None or dept_id in seen:
            continue
        seen.add(dept_id)
        details.append(detail)
    return details


def _department_filter_detail_from_value(value: JSONValue) -> JSONObject | None:
    if isinstance(value, dict):
        dept_id = _coerce_count(value.get("id", value.get("deptId")))
        if dept_id is None:
            return None
        return {"id": dept_id, "value": _stringify_json(value.get("value", value.get("name", value.get("deptName", dept_id))))}
    dept_id = _coerce_count(value)
    if dept_id is None:
        return None
    return {"id": dept_id, "value": str(dept_id)}


def _match_rule_to_query_rule(item: JSONObject) -> JSONObject:
    que_id = _coerce_count(item.get("queId", item.get("que_id")))
    if que_id is None:
        return {}
    que_type = _coerce_count(item.get("queType", item.get("que_type")))
    judge_type = _coerce_count(item.get("judgeType", item.get("judge_type")))
    judge_values = item.get("judgeValues", item.get("judge_values"))
    values = [_stringify_json(value) for value in judge_values] if isinstance(judge_values, list) else []
    rule: JSONObject = {"queId": que_id}
    if que_type is not None:
        rule["queType"] = que_type
    if judge_type == JUDGE_FUZZY_MATCH and values:
        rule["searchKey"] = values[0]
    elif judge_type == JUDGE_GREATER_OR_EQUAL and values:
        rule["minValue"] = values[0]
    elif judge_type == JUDGE_LESS_OR_EQUAL and values:
        rule["maxValue"] = values[0]
    elif judge_type == JUDGE_EQUAL_ANY and values:
        if que_type in SINGLE_SELECT_QUE_TYPES | MULTI_SELECT_QUE_TYPES | DEPARTMENT_QUE_TYPES:
            rule["searchOptions"] = values
        else:
            rule["searchKeys"] = values
    elif judge_type == JUDGE_INCLUDE_ANY and values:
        if que_type in MEMBER_QUE_TYPES:
            numeric_ids = [member_id for member_id in (_coerce_count(value) for value in values) if member_id is not None]
            if numeric_ids:
                rule["searchUids"] = numeric_ids
            else:
                rule["searchUserIds"] = values
        elif que_type in MULTI_SELECT_QUE_TYPES:
            rule["searchOptions"] = values
        else:
            rule["searchKeys"] = values
    elif judge_type == JUDGE_UNEQUAL and not values:
        rule["scope"] = SCOPE_NOT_EMPTY
    elif judge_type == JUDGE_EQUAL and not values:
        rule["scope"] = SCOPE_EMPTY
    query_keys = {"searchKey", "searchKeys", "minValue", "maxValue", "searchOptions", "searchUids", "searchUserIds", "scope"}
    return rule if any(key in rule for key in query_keys) else {}


def _normalize_list_sort_rule(item: JSONObject) -> JSONObject:
    rule: JSONObject = {}
    que_id = item.get("queId", item.get("que_id"))
    if que_id is not None:
        rule["queId"] = que_id
    if "isAscend" in item:
        rule["isAscend"] = bool(item["isAscend"])
    elif "ascend" in item or "direction" in item or "order" in item:
        rule["isAscend"] = _resolve_sort_ascend(item)
    return rule


def _normalize_member_filter_ids(value: JSONValue) -> list[int]:
    values = value if isinstance(value, list) else [value]
    member_ids: list[int] = []
    for item in values:
        if isinstance(item, dict):
            candidate = item.get("id", item.get("uid", item.get("userId")))
        else:
            candidate = item
        member_id = _coerce_count(candidate)
        if member_id is not None:
            member_ids.append(member_id)
    return member_ids


def _normalize_option_filter_ids(value: JSONValue) -> list[int]:
    values = value if isinstance(value, list) else [value]
    option_ids: list[int] = []
    for item in values:
        candidate: JSONValue
        if isinstance(item, dict):
            candidate = item.get("optionId", item.get("optId", item.get("id")))
        else:
            candidate = item
        option_id = _coerce_count(candidate)
        if option_id is not None:
            option_ids.append(option_id)
    return option_ids


def _collect_question_relations(schema: JSONObject) -> list[JSONObject]:
    relations = schema.get("questionRelations")
    if not isinstance(relations, list):
        return []
    return [item for item in relations if isinstance(item, dict)]


def _classify_question_relation_target_ids(
    validation_answers: list[JSONObject],
    question_relations: list[JSONObject],
) -> tuple[set[str], set[str]]:
    active_ids: set[str] = set()
    inactive_ids: set[str] = set()
    answer_probe_by_field = _collect_answer_probe_by_field(validation_answers)
    for relation in question_relations:
        target_ids = _question_relation_target_field_ids(relation)
        if not target_ids:
            continue
        state = _evaluate_question_relation_state(relation, answer_probe_by_field)
        if state is True:
            active_ids.update(target_ids)
            inactive_ids.difference_update(target_ids)
        elif state is False:
            inactive_ids.update(target_id for target_id in target_ids if target_id not in active_ids)
    return active_ids, inactive_ids


def _collect_linked_required_field_ids(question_relations: list[JSONObject]) -> set[str]:
    linked_ids: set[str] = set()
    for item in question_relations:
        for key in ("displayedQueId", "targetQueId"):
            value = _coerce_count(item.get(key))
            if value is not None and value > 0:
                linked_ids.add(str(value))
        displayed_info = item.get("displayedQueInfo")
        if isinstance(displayed_info, dict):
            value = _coerce_count(displayed_info.get("queId"))
            if value is not None and value > 0:
                linked_ids.add(str(value))
    return linked_ids


def _question_relation_source_field_ids(relation: JSONObject) -> set[str]:
    source_ids: set[str] = set()
    source_que_id = _coerce_count(relation.get("sourceQueId", relation.get("queId")))
    if source_que_id is not None and source_que_id > 0:
        source_ids.add(str(source_que_id))
    match_rules = relation.get("matchRules")
    pending_rules: list[JSONValue] = []
    if isinstance(match_rules, list):
        pending_rules.extend(match_rules)
    elif isinstance(match_rules, dict):
        pending_rules.append(match_rules)
    while pending_rules:
        item = pending_rules.pop()
        if isinstance(item, list):
            pending_rules.extend(item)
            continue
        if not isinstance(item, dict):
            continue
        que_id = _coerce_count(item.get("queId"))
        if que_id is not None and que_id > 0:
            source_ids.add(str(que_id))
    return source_ids


def _collect_question_relation_activation_sources(
    question_relations: list[JSONObject],
    index: FieldIndex,
) -> dict[str, set[str]]:
    activation_sources: dict[str, set[str]] = {}
    for relation in question_relations:
        target_ids = _question_relation_target_field_ids(relation)
        if not target_ids:
            continue
        source_titles = {
            field.que_title
            for source_id in _question_relation_source_field_ids(relation)
            if (field := index.by_id.get(source_id)) is not None and field.que_title
        }
        if not source_titles:
            continue
        for target_id in target_ids:
            activation_sources.setdefault(target_id, set()).update(source_titles)
    return activation_sources
    

def _collect_option_link_activation_sources(index: FieldIndex) -> dict[str, set[str]]:
    activation_sources: dict[str, set[str]] = {}
    for field in index.by_id.values():
        options = field.raw.get("options")
        if not isinstance(options, list):
            continue
        for option in options:
            if not isinstance(option, dict):
                continue
            link_ids = option.get("linkQueIds")
            if not isinstance(link_ids, list):
                continue
            for candidate in link_ids:
                value = _coerce_count(candidate)
                if value is None or value <= 0:
                    continue
                activation_sources.setdefault(str(value), set()).add(field.que_title)
    return activation_sources


def _collect_linked_activation_sources(
    question_relations: list[JSONObject],
    index: FieldIndex,
) -> dict[str, list[str]]:
    activation_sources = _collect_question_relation_activation_sources(question_relations, index)
    option_activation_sources = _collect_option_link_activation_sources(index)
    for field_id, titles in option_activation_sources.items():
        activation_sources.setdefault(field_id, set()).update(titles)
    return {
        field_id: sorted(title for title in titles if title)
        for field_id, titles in activation_sources.items()
        if titles
    }


def _collect_question_relation_affected_fields(
    question_relations: list[JSONObject],
    index: FieldIndex,
) -> dict[str, set[str]]:
    affects_fields: dict[str, set[str]] = {}
    for relation in question_relations:
        target_titles = {
            field.que_title
            for target_id in _question_relation_target_field_ids(relation)
            if (field := index.by_id.get(target_id)) is not None and field.que_title
        }
        if not target_titles:
            continue
        for source_id in _question_relation_source_field_ids(relation):
            if source_id not in index.by_id:
                continue
            affects_fields.setdefault(source_id, set()).update(target_titles)
    return affects_fields


def _collect_option_link_affected_fields(index: FieldIndex) -> dict[str, set[str]]:
    affects_fields: dict[str, set[str]] = {}
    for field in index.by_id.values():
        options = field.raw.get("options")
        if not isinstance(options, list):
            continue
        target_titles: set[str] = set()
        for option in options:
            if not isinstance(option, dict):
                continue
            link_ids = option.get("linkQueIds")
            if not isinstance(link_ids, list):
                continue
            for candidate in link_ids:
                value = _coerce_count(candidate)
                if value is None or value <= 0:
                    continue
                target_field = index.by_id.get(str(value))
                if target_field is not None and target_field.que_title:
                    target_titles.add(target_field.que_title)
        if target_titles:
            affects_fields[str(field.que_id)] = target_titles
    return affects_fields


def _collect_linked_affected_fields(
    question_relations: list[JSONObject],
    index: FieldIndex,
) -> dict[str, list[str]]:
    affects_fields = _collect_question_relation_affected_fields(question_relations, index)
    option_affected_fields = _collect_option_link_affected_fields(index)
    for field_id, titles in option_affected_fields.items():
        affects_fields.setdefault(field_id, set()).update(titles)
    return {
        field_id: sorted(title for title in titles if title)
        for field_id, titles in affects_fields.items()
        if titles
    }


def _collect_match_rule_field_titles(
    payload: JSONValue,
    index: FieldIndex,
) -> set[str]:
    titles: set[str] = set()
    pending: list[JSONValue] = [payload]
    while pending:
        item = pending.pop()
        if isinstance(item, list):
            pending.extend(item)
            continue
        if not isinstance(item, dict):
            continue
        que_id = _coerce_count(item.get("queId", item.get("que_id")))
        if que_id is not None and que_id > 0:
            field = index.by_id.get(str(que_id))
            if field is not None and field.que_title:
                titles.add(field.que_title)
        for value in item.values():
            if isinstance(value, (list, dict)):
                pending.append(cast(JSONValue, value))
    return titles


def _collect_reference_fill_linkage(
    index: FieldIndex,
) -> tuple[dict[str, list[str]], dict[str, list[str]], dict[str, list[str]]]:
    target_sources: dict[str, set[str]] = {}
    source_affects_fields: dict[str, set[str]] = {}
    source_dependency_sources: dict[str, set[str]] = {}
    for field in index.by_id.values():
        reference = field.raw.get("referenceConfig")
        if not isinstance(reference, dict):
            continue
        source_id = str(field.que_id)
        source_title = field.que_title
        fill_target_titles: set[str] = set()
        for fill_rule in reference.get("referFillRules", []):
            if not isinstance(fill_rule, dict):
                continue
            target_id = _coerce_count(fill_rule.get("queId", fill_rule.get("field_id")))
            if target_id is None or target_id <= 0:
                continue
            target_field = index.by_id.get(str(target_id))
            if target_field is None or not target_field.que_title:
                continue
            fill_target_titles.add(target_field.que_title)
            target_sources.setdefault(str(target_id), set()).add(source_title)
        if fill_target_titles:
            source_affects_fields.setdefault(source_id, set()).update(fill_target_titles)
        dependency_titles: set[str] = set()
        dependency_titles.update(_collect_match_rule_field_titles(reference.get("referMatchRules"), index))
        dependency_titles.update(_collect_match_rule_field_titles(reference.get("defaultValueMatchRules"), index))
        dependency_titles.discard(source_title)
        if dependency_titles:
            source_dependency_sources.setdefault(source_id, set()).update(dependency_titles)
    return (
        {
            field_id: sorted(title for title in titles if title)
            for field_id, titles in target_sources.items()
            if titles
        },
        {
            field_id: sorted(title for title in titles if title)
            for field_id, titles in source_affects_fields.items()
            if titles
        },
        {
            field_id: sorted(title for title in titles if title)
            for field_id, titles in source_dependency_sources.items()
            if titles
        },
    )


def _question_has_formula_fill(question: JSONObject) -> bool:
    formula = question.get("formula")
    if isinstance(formula, str):
        if formula.strip():
            return True
    elif formula not in (None, "", [], {}):
        return True
    reference = question.get("referenceConfig")
    return bool(isinstance(reference, dict) and reference.get("beingDefaultFormulaAutoFillEnabled"))


def _build_static_schema_linkage_payloads(
    *,
    index: FieldIndex,
    question_relations: list[JSONObject],
) -> dict[str, JSONObject]:
    logic_sources_by_field_id = _collect_linked_activation_sources(question_relations, index)
    logic_affects_by_field_id = _collect_linked_affected_fields(question_relations, index)
    (
        reference_sources_by_field_id,
        reference_affects_by_field_id,
        reference_dependency_sources_by_field_id,
    ) = _collect_reference_fill_linkage(index)
    linkage_payloads: dict[str, JSONObject] = {}
    for field_id, field in index.by_id.items():
        logic_sources = logic_sources_by_field_id.get(field_id, [])
        logic_affects = logic_affects_by_field_id.get(field_id, [])
        reference_sources = reference_sources_by_field_id.get(field_id, [])
        reference_affects = reference_affects_by_field_id.get(field_id, [])
        reference_dependency_sources = reference_dependency_sources_by_field_id.get(field_id, [])
        payload: JSONObject | None = None

        if logic_sources or logic_affects:
            payload = {
                "kind": SCHEMA_LINKAGE_KIND_LOGIC_VISIBILITY,
                "role": (
                    SCHEMA_LINKAGE_ROLE_MANUAL_INPUT_AFTER_ACTIVATION
                    if logic_sources
                    else SCHEMA_LINKAGE_ROLE_MANUAL_INPUT
                ),
                "message": (
                    SCHEMA_LINKAGE_LOGIC_BOTH_MESSAGE
                    if logic_sources and logic_affects
                    else SCHEMA_LINKAGE_LOGIC_TARGET_MESSAGE
                    if logic_sources
                    else SCHEMA_LINKAGE_LOGIC_SOURCE_MESSAGE
                ),
            }
            if logic_sources:
                payload["sources"] = logic_sources
            if logic_affects:
                payload["affects_fields"] = logic_affects
        elif reference_sources or reference_affects or reference_dependency_sources:
            payload = {
                "kind": SCHEMA_LINKAGE_KIND_REFERENCE_FILL,
                "role": (
                    SCHEMA_LINKAGE_ROLE_AUTO_FILL_ONLY
                    if reference_sources and field.readonly
                    else SCHEMA_LINKAGE_ROLE_AUTO_FILL_PREFERRED
                    if reference_sources
                    else SCHEMA_LINKAGE_ROLE_MANUAL_INPUT
                ),
                "message": (
                    SCHEMA_LINKAGE_REFERENCE_BOTH_MESSAGE
                    if (reference_sources or reference_dependency_sources) and reference_affects
                    else SCHEMA_LINKAGE_REFERENCE_TARGET_MESSAGE
                    if reference_sources
                    else SCHEMA_LINKAGE_REFERENCE_SOURCE_MESSAGE
                ),
            }
            if reference_sources:
                payload["sources"] = reference_sources
            elif reference_dependency_sources:
                payload["sources"] = reference_dependency_sources
            if reference_affects:
                payload["affects_fields"] = reference_affects
        elif _question_has_formula_fill(field.raw):
            payload = {
                "kind": SCHEMA_LINKAGE_KIND_FORMULA_FILL,
                "role": (
                    SCHEMA_LINKAGE_ROLE_AUTO_FILL_ONLY
                    if field.readonly
                    else SCHEMA_LINKAGE_ROLE_AUTO_FILL_PREFERRED
                ),
                "message": SCHEMA_LINKAGE_FORMULA_MESSAGE,
            }

        if payload is not None:
            linkage_payloads[field_id] = payload
    return linkage_payloads


def _collect_option_linked_field_ids(index: FieldIndex) -> set[str]:
    linked_ids: set[str] = set()
    for field in index.by_id.values():
        options = field.raw.get("options")
        if not isinstance(options, list):
            continue
        for option in options:
            if not isinstance(option, dict):
                continue
            link_ids = option.get("linkQueIds")
            if not isinstance(link_ids, list):
                continue
            for candidate in link_ids:
                value = _coerce_count(candidate)
                if value is not None and value > 0:
                    linked_ids.add(str(value))
    return linked_ids


def _collect_active_option_linked_field_ids(validation_answers: list[JSONObject], index: FieldIndex) -> set[str]:
    linked_ids: set[str] = set()
    for answer in validation_answers:
        que_id = _coerce_count(answer.get("queId"))
        if que_id is None or que_id <= 0:
            continue
        field = index.by_id.get(str(que_id))
        if field is None:
            continue
        options = field.raw.get("options")
        if not isinstance(options, list) or not options:
            continue
        selected_value_keys: set[str] = set()
        selected_option_ids: set[int] = set()
        for item in _iter_answer_values(answer):
            if not isinstance(item, dict):
                continue
            raw_value = item.get("value")
            if raw_value is not None:
                selected_value_keys.add(_normalize_option_key(str(raw_value)))
            option_id = _coerce_count(item.get("id", item.get("optId", item.get("optionId"))))
            if option_id is not None and option_id > 0:
                selected_option_ids.add(option_id)
        if not selected_value_keys and not selected_option_ids:
            continue
        for option in options:
            if not isinstance(option, dict):
                continue
            option_id = _coerce_count(option.get("optId", option.get("optionId", option.get("id"))))
            option_value = option.get("optValue", option.get("value"))
            option_matches = (
                option_id is not None and option_id in selected_option_ids
            ) or (
                option_value is not None and _normalize_option_key(str(option_value)) in selected_value_keys
            )
            if not option_matches:
                continue
            link_ids = option.get("linkQueIds")
            if not isinstance(link_ids, list):
                continue
            for candidate in link_ids:
                value = _coerce_count(candidate)
                if value is not None and value > 0:
                    linked_ids.add(str(value))
    return linked_ids


def _collect_answer_probe_by_field(validation_answers: list[JSONObject]) -> dict[str, JSONObject]:
    answer_probe_by_field: dict[str, JSONObject] = {}
    for answer in validation_answers:
        que_id = _coerce_count(answer.get("queId"))
        if que_id is None or que_id <= 0:
            continue
        comparable_values: list[JSONValue] = []
        text_tokens: set[str] = set()
        for item in _iter_answer_values(answer):
            for candidate in _extract_answer_probe_values(item):
                comparable = _coerce_comparable(candidate)
                if comparable is not None:
                    comparable_values.append(comparable)
                normalized_text = _normalize_optional_text(candidate)
                if normalized_text:
                    text_tokens.add(normalized_text)
                numeric_id = _coerce_count(candidate)
                if numeric_id is not None:
                    text_tokens.add(str(numeric_id))
        answer_probe_by_field[str(que_id)] = {
            "answered": _answer_has_meaningful_content(answer),
            "values": comparable_values,
            "tokens": sorted(text_tokens),
        }
    return answer_probe_by_field


def _extract_answer_probe_values(value: JSONValue) -> list[JSONValue]:
    if isinstance(value, dict):
        extracted: list[JSONValue] = []
        for key in ("value", "dataValue", "name", "email", "id", "uid", "userId", "optionId", "optId"):
            if key in value and value.get(key) is not None:
                extracted.append(cast(JSONValue, value.get(key)))
        return extracted
    return [value]


def _question_relation_target_field_ids(relation: JSONObject) -> set[str]:
    target_ids: set[str] = set()
    for key in ("displayedQueId", "targetQueId"):
        value = _coerce_count(relation.get(key))
        if value is not None and value > 0:
            target_ids.add(str(value))
    displayed_info = relation.get("displayedQueInfo")
    if isinstance(displayed_info, dict):
        value = _coerce_count(displayed_info.get("queId"))
        if value is not None and value > 0:
            target_ids.add(str(value))
    elif isinstance(displayed_info, list):
        for item in displayed_info:
            if not isinstance(item, dict):
                continue
            value = _coerce_count(item.get("queId"))
            if value is not None and value > 0:
                target_ids.add(str(value))
    return target_ids


def _evaluate_question_relation_state(
    relation: JSONObject,
    answer_probe_by_field: dict[str, JSONObject],
) -> bool | None:
    match_rules = relation.get("matchRules")
    if isinstance(match_rules, list) and match_rules:
        return _evaluate_question_relation_match_groups(match_rules, answer_probe_by_field)
    source_que_id = _coerce_count(relation.get("sourceQueId", relation.get("queId")))
    if source_que_id is None or source_que_id <= 0:
        return None
    source_probe = answer_probe_by_field.get(str(source_que_id))
    if not isinstance(source_probe, dict):
        return False
    return bool(source_probe.get("answered"))


def _evaluate_question_relation_match_groups(
    match_rules: list[JSONValue],
    answer_probe_by_field: dict[str, JSONObject],
) -> bool | None:
    groups: list[list[JSONObject]] = []
    for item in match_rules:
        if isinstance(item, list):
            group = [rule for rule in item if isinstance(rule, dict)]
            if group:
                groups.append(group)
        elif isinstance(item, dict):
            groups.append([item])
    if not groups:
        return None
    saw_unknown = False
    for group in groups:
        state = _evaluate_question_relation_match_group(group, answer_probe_by_field)
        if state is True:
            return True
        if state is None:
            saw_unknown = True
    return None if saw_unknown else False


def _evaluate_question_relation_match_group(
    group: list[JSONObject],
    answer_probe_by_field: dict[str, JSONObject],
) -> bool | None:
    saw_unknown = False
    for rule in group:
        state = _evaluate_question_relation_match_rule(rule, answer_probe_by_field)
        if state is False:
            return False
        if state is None:
            saw_unknown = True
    return None if saw_unknown else True


def _evaluate_question_relation_match_rule(
    rule: JSONObject,
    answer_probe_by_field: dict[str, JSONObject],
) -> bool | None:
    source_que_id = _coerce_count(rule.get("queId"))
    if source_que_id is None or source_que_id <= 0:
        return None
    source_probe = answer_probe_by_field.get(str(source_que_id))
    if not isinstance(source_probe, dict):
        return False
    if not bool(source_probe.get("answered")):
        return False
    actual_values = cast(list[JSONValue], source_probe.get("values") or [])
    judge_type = _coerce_count(rule.get("judgeType"))
    match_type = _coerce_count(rule.get("matchType"))
    expected_values: list[JSONValue] = []
    if match_type == 2:
        judge_que_id = _coerce_count(rule.get("judgeQueId"))
        if judge_que_id is None:
            judge_detail = rule.get("judgeQueDetail")
            if isinstance(judge_detail, dict):
                judge_que_id = _coerce_count(judge_detail.get("queId"))
        if judge_que_id is None or judge_que_id <= 0:
            return None
        target_probe = answer_probe_by_field.get(str(judge_que_id))
        if not isinstance(target_probe, dict):
            return False
        if not bool(target_probe.get("answered")):
            return False
        expected_values = cast(list[JSONValue], target_probe.get("values") or [])
    else:
        judge_values = rule.get("judgeValues")
        if isinstance(judge_values, list):
            expected_values = [_coerce_comparable(item) for item in judge_values if _coerce_comparable(item) is not None]
        judge_value_details = rule.get("judgeValueDetails")
        if isinstance(judge_value_details, list):
            for item in judge_value_details:
                if not isinstance(item, dict):
                    continue
                for candidate in _extract_answer_probe_values(item):
                    comparable = _coerce_comparable(candidate)
                    if comparable is not None:
                        expected_values.append(comparable)
    return _evaluate_question_relation_value_match(actual_values, expected_values, judge_type)


def _evaluate_question_relation_value_match(
    actual_values: list[JSONValue],
    expected_values: list[JSONValue],
    judge_type: int | None,
) -> bool | None:
    expanded_actual_values: list[JSONValue] = []
    for item in actual_values:
        expanded_actual_values.extend(_extract_answer_probe_values(item))
    if not expanded_actual_values:
        return False
    if judge_type in {JUDGE_EQUAL, JUDGE_EQUAL_ANY, JUDGE_INCLUDE_ANY}:
        if not expected_values:
            return None
        return any(_analyze_values_equal(actual=item, expected=expected) for item in expanded_actual_values for expected in expected_values)
    if judge_type == JUDGE_UNEQUAL:
        if not expected_values:
            return None
        return not any(_analyze_values_equal(actual=item, expected=expected) for item in expanded_actual_values for expected in expected_values)
    if judge_type in {JUDGE_GREATER, JUDGE_GREATER_OR_EQUAL, JUDGE_LESS, JUDGE_LESS_OR_EQUAL}:
        if not expected_values:
            return None
        comparator = {
            JUDGE_GREATER: "gt",
            JUDGE_GREATER_OR_EQUAL: "gte",
            JUDGE_LESS: "lt",
            JUDGE_LESS_OR_EQUAL: "lte",
        }[judge_type]
        return any(_match_numeric_comparison(item, comparator, expected) for item in expanded_actual_values for expected in expected_values)
    return None


def _match_numeric_comparison(actual: JSONValue, operator: str, expected: JSONValue) -> bool:
    actual_value = _coerce_comparable(actual)
    expected_value = _coerce_comparable(expected)
    if actual_value is None or expected_value is None:
        return False
    try:
        if operator == "gt":
            return actual_value > expected_value
        if operator == "gte":
            return actual_value >= expected_value
        if operator == "lt":
            return actual_value < expected_value
        if operator == "lte":
            return actual_value <= expected_value
    except TypeError:
        return False
    return False


def _iter_answer_values(answer: JSONObject) -> list[JSONObject]:
    values = answer.get("values")
    if not isinstance(values, list):
        return []
    return [item for item in values if isinstance(item, dict)]


def _normalize_option_key(value: str | None) -> str:
    return _normalize_optional_text(value)


def _collect_option_links(resolved_fields: list[JSONObject]) -> list[JSONObject]:
    links: list[JSONObject] = []
    for entry in resolved_fields:
        write_format = entry.get("write_format")
        if not isinstance(write_format, dict):
            continue
        option_links = write_format.get("option_links")
        if not isinstance(option_links, list):
            continue
        for item in option_links:
            if isinstance(item, dict):
                links.append(item)
    return links


def _answers_need_resolution(answers: list[JSONObject]) -> bool:
    for item in answers:
        if not isinstance(item, dict):
            return True
        if "queId" not in item or "queType" not in item:
            return True
        if "values" not in item and "value" in item:
            return True
        if "queTitle" in item or "que_title" in item or "que_id" in item:
            return True
        if _subtable_answer_needs_resolution(item):
            return True
    return False


def _subtable_answer_needs_resolution(item: JSONObject) -> bool:
    if any(key in item for key in ("rows", "fields")):
        return True
    que_type = _coerce_count(item.get("queType"))
    table_values = item.get("tableValues")
    if que_type not in SUBTABLE_QUE_TYPES:
        return False
    if table_values is None:
        return True
    if not isinstance(table_values, list):
        return True
    for row in table_values:
        if isinstance(row, dict):
            return True
        if not isinstance(row, list):
            return True
        for cell in row:
            if not isinstance(cell, dict):
                return True
            if "queId" not in cell or "queType" not in cell:
                return True
            if "values" not in cell and "value" in cell:
                return True
            if any(key in cell for key in ("queTitle", "que_title", "que_id", "fields", "answers", "rows")):
                return True
    return False


def _expand_values(values: list[JSONValue]) -> list[JSONValue]:
    if len(values) == 1 and isinstance(values[0], list):
        nested = values[0]
        return [item for item in nested]
    return values


def _option_value(raw_value: JSONValue, field: FormField) -> JSONObject:
    if isinstance(raw_value, dict):
        value = raw_value.get("value", raw_value.get("optValue"))
        payload: JSONObject = {"value": _stringify_json(value)}
        if "optionId" in raw_value:
            payload["optionId"] = raw_value["optionId"]
        return payload
    text = _stringify_json(raw_value)
    if field.options and text not in field.options:
        raise RecordInputError(
            message=f"field '{field.que_title}' uses unknown option '{text}'",
            error_code="OPTION_NOT_FOUND",
            fix_hint="Use the relevant schema-get tool or inspect the form to confirm allowed option values.",
            details={"field": _field_ref_payload(field), "expected_format": _write_format_for_field(field), "received_value": raw_value},
        )
    return {"value": text}


def _boolean_display(value: JSONValue) -> str:
    if isinstance(value, bool):
        return "是" if value else "否"
    text = _stringify_json(value).strip().lower()
    if text in {"true", "1", "yes", "y", "是"}:
        return "是"
    if text in {"false", "0", "no", "n", "否"}:
        return "否"
    return _stringify_json(value)


def _member_value(profile: str, value: JSONValue) -> JSONObject:
    if isinstance(value, dict):
        member_id = _coerce_count(value.get("id", value.get("uid", value.get("userId"))))
        user_id = value.get("userId")
        if member_id is None and not (isinstance(user_id, str) and user_id.strip()):
            raise RecordInputError(
                message="member values require id, uid, or userId",
                error_code="INVALID_MEMBER_VALUE",
                fix_hint="Pass member values like {'id': 2, 'value': '张三'} or {'userId': 'u_123', 'userName': '张三'}.",
                details={"received_value": value, "location": profile},
            )
        payload: JSONObject = {
            "value": _stringify_json(value.get("value", value.get("name", value.get("userName", member_id if member_id is not None else user_id))))
        }
        if member_id is not None:
            payload["id"] = member_id
        elif isinstance(user_id, str) and user_id.strip():
            payload["userId"] = user_id.strip()
        if value.get("email") is not None:
            payload["email"] = value["email"]
        if value.get("otherInfo") is not None:
            payload["otherInfo"] = value["otherInfo"]
        return payload
    member_id = _coerce_count(value)
    if member_id is None and not (isinstance(value, str) and value.strip()):
        raise RecordInputError(
            message="member values require numeric ids, userIds, or member objects",
            error_code="INVALID_MEMBER_VALUE",
            fix_hint="Pass member ids like 2, userIds like 'u_123', or objects like {'id': 2, 'value': '张三'}.",
            details={"received_value": value, "location": profile},
        )
    if member_id is not None:
        return {"id": member_id, "value": str(member_id)}
    return {"userId": str(value).strip(), "value": str(value).strip()}


def _department_value(value: JSONValue) -> JSONObject:
    if isinstance(value, dict):
        dept_id = _coerce_count(value.get("id", value.get("deptId")))
        if dept_id is None:
            raise RecordInputError(
                message="department values require id or deptId",
                error_code="INVALID_DEPARTMENT_VALUE",
                fix_hint="Pass department values like {'id': 11, 'value': '示例部门'} or {'deptId': 11, 'deptName': '示例部门'}.",
                details={"received_value": value},
            )
        return {"id": dept_id, "value": _stringify_json(value.get("value", value.get("name", value.get("deptName", dept_id))))}
    dept_id = _coerce_count(value)
    if dept_id is None:
        raise RecordInputError(
            message="department values require numeric ids or department objects",
            error_code="INVALID_DEPARTMENT_VALUE",
            fix_hint="Pass department ids like 11 or objects like {'id': 11, 'value': '示例部门'}.",
            details={"received_value": value},
        )
    return {"id": dept_id, "value": str(dept_id)}


def _attachment_value(value: JSONValue) -> JSONObject:
    if isinstance(value, dict):
        if "value" not in value and "url" not in value:
            raise RecordInputError(
                message="attachment values require value/url",
                error_code="INVALID_ATTACHMENT_VALUE",
                fix_hint="Pass attachments like {'value': 'https://.../a.pdf', 'name': 'a.pdf'}.",
                details={"received_value": value},
            )
        payload: JSONObject = {"value": value.get("value", value.get("url"))}
        file_name = value.get("otherInfo", value.get("name", value.get("fileName")))
        if file_name is not None:
            payload["otherInfo"] = file_name
        return payload
    return {"value": _stringify_json(value)}


def _extract_attachment_item(value: JSONValue) -> JSONObject | None:
    if not isinstance(value, dict):
        text = _normalize_optional_text(value)
        return {"value": text, "name": None} if text else None
    file_url = value.get("value", value.get("url"))
    normalized_url = _normalize_optional_text(file_url) or (_stringify_json(file_url) if file_url is not None else None)
    if not normalized_url:
        return None
    file_name = value.get("otherInfo", value.get("name", value.get("fileName")))
    normalized_name = _normalize_optional_text(file_name) or (_stringify_json(file_name) if file_name is not None else None)
    payload: JSONObject = {"value": normalized_url}
    if normalized_name:
        payload["name"] = normalized_name
    return payload


def _relation_value(value: JSONValue) -> JSONObject:
    return _relation_value_payload(None, value)[0]


def _extract_relation_value(answer: JSONObject) -> JSONValue:
    relation_ids = _relation_ids_from_answer(answer)
    if not relation_ids:
        return None
    return relation_ids[0] if len(relation_ids) == 1 else relation_ids


def _extract_address_value(answer: JSONObject) -> JSONValue:
    values = answer.get("values")
    if not isinstance(values, list) or not values:
        return None
    normalized_parts: list[tuple[int, JSONObject]] = []
    for ordinal, item in enumerate(values):
        if isinstance(item, dict):
            raw_value = item.get("value")
            text = _normalize_optional_text(raw_value) or (_stringify_json(raw_value) if raw_value is not None else None)
            if not text:
                continue
            payload: JSONObject = {"value": text}
            part_id = _coerce_count(item.get("id"))
            if part_id is not None:
                payload["id"] = part_id
            other_info = _normalize_optional_text(item.get("otherInfo"))
            if other_info:
                payload["otherInfo"] = other_info
            sort_key = part_id if part_id is not None else 100 + ordinal
            normalized_parts.append((sort_key, payload))
            continue
        text = _normalize_optional_text(item) or (_stringify_json(item) if item is not None else None)
        if text:
            normalized_parts.append((100 + ordinal, {"value": text}))
    if not normalized_parts:
        return None
    normalized_parts.sort(key=lambda item: item[0])
    return [payload for _, payload in normalized_parts]


def _address_value_payloads(raw_values: list[JSONValue]) -> list[JSONObject]:
    payloads: list[JSONObject] = []
    for item in _expand_values(raw_values):
        payloads.extend(_coerce_address_value_items(item))
    if payloads:
        return payloads
    raise RecordInputError(
        message="address fields require a detail string, ordered address parts, or a province/city/district/detail object",
        error_code="INVALID_ADDRESS_VALUE",
        fix_hint="Pass a detail string, a list like [{'id': 1, 'value': '上海市'}], or an object like {'province': '上海市', 'city': '上海市', 'district': '闵行区', 'detail': '浦江路99号'}.",
        details={"received_value": raw_values},
    )


def _coerce_address_value_items(value: JSONValue) -> list[JSONObject]:
    if value in (None, "", []):
        return []
    if isinstance(value, list):
        payloads: list[JSONObject] = []
        for item in value:
            payloads.extend(_coerce_address_value_items(item))
        return payloads
    if isinstance(value, dict):
        nested_values = value.get("values", value.get("parts"))
        if isinstance(nested_values, list):
            return _coerce_address_value_items(cast(JSONValue, nested_values))
        if any(
            key in value
            for key in (
                "province",
                "city",
                "district",
                "area",
                "detail",
                "address",
                "详细地址",
                "省",
                "市",
                "区",
                "县",
            )
        ):
            mapped_fields = [
                (1, value.get("province", value.get("省")), value.get("province_apcode", value.get("provinceAdcode", value.get("provinceCode", value.get("省编码"))))),
                (2, value.get("city", value.get("市")), value.get("city_apcode", value.get("cityAdcode", value.get("cityCode", value.get("市编码"))))),
                (3, value.get("district", value.get("area", value.get("区", value.get("县")))), value.get("district_apcode", value.get("districtAdcode", value.get("districtCode", value.get("区编码", value.get("县编码")))))),
                (4, value.get("detail", value.get("address", value.get("detailed_address", value.get("详细地址")))), None),
            ]
            payloads: list[JSONObject] = []
            for part_id, part_value, other_info in mapped_fields:
                text = _normalize_optional_text(part_value) or (_stringify_json(part_value) if part_value is not None else None)
                if not text:
                    continue
                payload: JSONObject = {"id": part_id, "value": text}
                normalized_other = _normalize_optional_text(other_info) or (_stringify_json(other_info) if other_info is not None else None)
                if normalized_other:
                    payload["otherInfo"] = normalized_other
                payloads.append(payload)
            return payloads
        part_id = _coerce_count(value.get("id", value.get("partId", value.get("level"))))
        text = (
            _normalize_optional_text(value.get("value"))
            or _normalize_optional_text(value.get("name"))
            or _normalize_optional_text(value.get("label"))
            or _normalize_optional_text(value.get("text"))
        )
        if part_id is None:
            if text:
                return [{"id": 4, "value": text}]
            return []
        if not text:
            return []
        payload: JSONObject = {"id": part_id, "value": text}
        other_info = _normalize_optional_text(value.get("otherInfo", value.get("apcode", value.get("adcode", value.get("code")))))
        if other_info:
            payload["otherInfo"] = other_info
        return [payload]
    return [{"id": 4, "value": _stringify_json(value)}]


def _relation_value_payload(field: FormField | None, value: JSONValue) -> tuple[JSONObject, JSONObject]:
    if isinstance(value, dict):
        target_app_key = _normalize_optional_text(value.get("target_app_key", value.get("targetAppKey", value.get("app_key", value.get("appKey")))))
        if field is not None and field.target_app_key and target_app_key and target_app_key != field.target_app_key:
            raise RecordInputError(
                message=f"relation field '{field.que_title}' points to a different target app",
                error_code="RELATION_TARGET_APP_MISMATCH",
                fix_hint=f"Use a record from target app '{field.target_app_key}'.",
                details={
                    "field": _field_ref_payload(field),
                    "expected_target_app_key": field.target_app_key,
                    "received_target_app_key": target_app_key,
                    "received_value": value,
                },
            )
        apply_id = value.get("apply_id", value.get("applyId", value.get("value", value.get("id"))))
        if apply_id is None:
            raise RecordInputError(
                message="relation values require apply_id/applyId/value/id",
                error_code="INVALID_RELATION_VALUE",
                fix_hint="Pass relation values like {'apply_id': 5001} or numeric apply ids.",
                details={"received_value": value},
            )
        normalized_apply_id = _stringify_json(apply_id)
        return ({"value": normalized_apply_id}, {"applyId": normalized_apply_id})
    normalized_apply_id = _stringify_json(value)
    return ({"value": normalized_apply_id}, {"applyId": normalized_apply_id})


def _relation_ids_from_answer(answer: JSONObject) -> list[str]:
    ids: list[str] = []
    seen: set[str] = set()
    for key in ("referValues", "values"):
        values = answer.get(key)
        if not isinstance(values, list):
            continue
        for item in values:
            if not isinstance(item, dict):
                continue
            relation_id = item.get("applyId", item.get("apply_id", item.get("value", item.get("id"))))
            normalized = _normalize_optional_text(relation_id) or (_stringify_json(relation_id) if relation_id is not None else None)
            if not normalized or normalized in seen:
                continue
            ids.append(normalized)
            seen.add(normalized)
    return ids


def _member_search_bucket_payload(payload: JSONValue, *, bucket_key: str) -> JSONValue:
    if isinstance(payload, dict):
        bucket = payload.get(bucket_key)
        if isinstance(bucket, (dict, list)):
            return bucket
    return payload


def _normalize_candidate_member(
    value: JSONValue,
    *,
    source_kind: str,
    source_id: int | None = None,
    source_value: str | None = None,
) -> JSONObject | None:
    if not isinstance(value, dict):
        return None
    member_id = _coerce_count(value.get("uid", value.get("id")))
    user_id = _normalize_optional_text(value.get("userId"))
    if member_id is None and not user_id:
        return None
    display_value = (
        _normalize_optional_text(value.get("nickName"))
        or _normalize_optional_text(value.get("name"))
        or _normalize_optional_text(value.get("userName"))
        or _normalize_optional_text(value.get("value"))
        or (str(member_id) if member_id is not None else user_id)
    )
    if not display_value:
        return None
    candidate: JSONObject = {"value": display_value, "sources": [{"kind": source_kind}]}
    if member_id is not None:
        candidate["id"] = member_id
    if user_id:
        candidate["userId"] = user_id
    email = _normalize_optional_text(value.get("email"))
    if email:
        candidate["email"] = email
    if source_id is not None:
        cast(list[JSONObject], candidate["sources"])[0]["id"] = source_id
    if source_value:
        cast(list[JSONObject], candidate["sources"])[0]["value"] = source_value
    return candidate


def _normalize_candidate_department(
    value: JSONValue,
    *,
    source_kind: str,
    source_id: int | None = None,
    source_value: str | None = None,
) -> JSONObject | None:
    if not isinstance(value, dict):
        return None
    dept_id = _coerce_count(value.get("deptId", value.get("id")))
    if dept_id is None:
        return None
    display_value = (
        _normalize_optional_text(value.get("deptName"))
        or _normalize_optional_text(value.get("value"))
        or _normalize_optional_text(value.get("name"))
        or str(dept_id)
    )
    if not display_value:
        return None
    candidate: JSONObject = {"id": dept_id, "value": display_value, "sources": [{"kind": source_kind}]}
    parent_dept_id = _coerce_count(value.get("parentDeptId"))
    if parent_dept_id is not None:
        candidate["parentDeptId"] = parent_dept_id
    if source_id is not None:
        cast(list[JSONObject], candidate["sources"])[0]["id"] = source_id
    if source_value:
        cast(list[JSONObject], candidate["sources"])[0]["value"] = source_value
    return candidate


def _member_candidate_key(value: JSONValue) -> str | None:
    if not isinstance(value, dict):
        return None
    member_id = _coerce_count(value.get("id", value.get("uid")))
    if member_id is not None:
        return f"id:{member_id}"
    user_id = _normalize_optional_text(value.get("userId"))
    if user_id:
        return f"userId:{user_id}"
    return None


def _department_candidate_key(value: JSONValue) -> str | None:
    if not isinstance(value, dict):
        return None
    dept_id = _coerce_count(value.get("id", value.get("deptId")))
    if dept_id is None:
        return None
    return f"id:{dept_id}"


def _filter_member_candidates(items: list[JSONObject], keyword: str) -> list[JSONObject]:
    normalized_keyword = keyword.strip().lower()
    if not normalized_keyword:
        return items
    filtered: list[JSONObject] = []
    for item in items:
        haystacks = [
            _normalize_optional_text(item.get("value")) or "",
            _normalize_optional_text(item.get("userId")) or "",
            _normalize_optional_text(item.get("email")) or "",
        ]
        if any(normalized_keyword in haystack.lower() for haystack in haystacks if haystack):
            filtered.append(item)
    return filtered


def _filter_department_candidates(items: list[JSONObject], keyword: str) -> list[JSONObject]:
    normalized_keyword = keyword.strip().lower()
    if not normalized_keyword:
        return items
    filtered: list[JSONObject] = []
    for item in items:
        haystacks = [
            _normalize_optional_text(item.get("value")) or "",
            _stringify_json(item.get("id")),
        ]
        if any(normalized_keyword in haystack.lower() for haystack in haystacks if haystack):
            filtered.append(item)
    return filtered


def _coerce_runtime_api_error(error: Exception) -> QingflowApiError | None:
    if isinstance(error, QingflowApiError):
        return error
    if not isinstance(error, RuntimeError):
        return None
    try:
        payload = json.loads(str(error))
    except json.JSONDecodeError:
        return None
    if not isinstance(payload, dict) or not payload.get("category") or not payload.get("message"):
        return None
    details = payload.get("details")
    return QingflowApiError(
        category=str(payload.get("category")),
        message=str(payload.get("message")),
        backend_code=payload.get("backend_code"),
        request_id=payload.get("request_id"),
        http_status=payload.get("http_status"),
        details=details if isinstance(details, dict) else None,
    )


def _scope_has_dynamic_or_external(scope: JSONObject) -> bool:
    dynamic_items = scope.get("dynamic")
    external_members = scope.get("externalMemberList")
    external_departs = scope.get("externalDepartList")
    return bool(
        isinstance(dynamic_items, list)
        and dynamic_items
        or isinstance(external_members, list)
        and external_members
        or isinstance(external_departs, list)
        and external_departs
    )


def _scope_is_default_all(scope_type: int | None, scope: JSONObject, *, keys: tuple[str, ...]) -> bool:
    if scope_type != 1:
        return False
    if _scope_has_dynamic_or_external(scope):
        return False
    for key in keys:
        items = scope.get(key)
        if isinstance(items, list) and items:
            return False
    return True


def _normalize_bool(value: JSONValue) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return bool(value)
    if isinstance(value, str):
        return value.strip().lower() in {"1", "true", "yes", "y", "是"}
    return False


def _field_ref_payload(field: FormField) -> JSONObject:
    payload = {"que_id": field.que_id, "que_title": field.que_title, "que_type": field.que_type}
    if field.aliases:
        payload["aliases"] = field.aliases[:8]
    return payload


def _normalize_field_lookup_key(value: str | int | None) -> str:
    text = _stringify_json(value).strip().lower()
    if not text:
        return ""
    return FIELD_LOOKUP_STRIP_RE.sub("", text)


def _field_alias_candidates(field: FormField) -> set[str]:
    title = field.que_title.strip()
    aliases = set(GENERIC_FIELD_ALIAS_OVERRIDES.get(title, []))
    for prefix in GENERIC_FIELD_PREFIX_ALIASES:
        if title.startswith(prefix) and len(title) > len(prefix) + 1:
            aliases.add(title[len(prefix):])
    if title.endswith("所在部门"):
        aliases.add("部门")
        aliases.add(f"{title.removesuffix('所在部门')}部门")
    if title.endswith("阶段"):
        aliases.add("阶段")
    if title.endswith("来源"):
        aliases.add("来源")
    if title.endswith("类型"):
        aliases.add("类型")
    if title.endswith("等级"):
        aliases.add("等级")
    if title.endswith("名称"):
        aliases.add("名称")
    if title.endswith("编号"):
        aliases.add("编号")
    if title.endswith("金额"):
        aliases.add("金额")
    if field.que_type in MEMBER_QUE_TYPES:
        if "负责人" in title:
            aliases.add("负责人")
    if field.que_type in DEPARTMENT_QUE_TYPES:
        aliases.add("部门")
    if field.que_type in DATE_QUE_TYPES:
        if title.endswith("时间"):
            aliases.add("时间")
        if title.endswith("日期"):
            aliases.add("日期")
    normalized_title = _normalize_field_lookup_key(title)
    return {
        alias.strip()
        for alias in aliases
        if alias and _normalize_field_lookup_key(alias) and _normalize_field_lookup_key(alias) != normalized_title
    }


def _subtable_columns_for_field(field: FormField) -> list[JSONObject]:
    raw = field.raw if isinstance(field.raw, dict) else {}
    columns_source = raw.get("subQuestions")
    if isinstance(columns_source, list):
        flattened = _flatten_questions(columns_source)
    else:
        inner_questions = raw.get("innerQuestions")
        flattened = _flatten_questions(inner_questions) if isinstance(inner_questions, list) else []
    columns: list[JSONObject] = []
    for question in flattened:
        que_id = _coerce_count(question.get("queId"))
        que_title = _normalize_optional_text(question.get("queTitle"))
        if que_id is None or que_title is None:
            continue
        columns.append(
            {
                "que_id": que_id,
                "que_title": que_title,
                "que_type": _coerce_count(question.get("queType")),
                "required": bool(question.get("required") or question.get("beingRequired")),
                "readonly": bool(question.get("readonly") or question.get("beingReadonly")),
                "system": bool(question.get("system") or question.get("beingSystem")),
            }
        )
    return columns


def _write_support_payload(
    *,
    support_level: str,
    kind: str,
    examples: JSONValue | None = None,
    accepted_aliases: list[str] | None = None,
    options: list[str] | None = None,
    reason: str | None = None,
    fix_hint: str | None = None,
    required_presteps: list[str] | None = None,
    subfields: list[JSONObject] | None = None,
    row_shape: str | None = None,
) -> JSONObject:
    payload: JSONObject = {
        "support_level": support_level,
        "kind": kind,
    }
    if examples is not None:
        payload["examples"] = examples
    if accepted_aliases:
        payload["accepted_aliases"] = accepted_aliases
    if options is not None:
        payload["options"] = options
    if reason is not None:
        payload["reason"] = reason
    if fix_hint is not None:
        payload["fix_hint"] = fix_hint
    if required_presteps:
        payload["required_presteps"] = required_presteps
    if subfields:
        payload["subfields"] = subfields
    if row_shape is not None:
        payload["row_shape"] = row_shape
    return payload


def _score_candidate_text(requested_key: str, candidate_text: str, *, fuzzy: bool, label: str) -> tuple[float, str]:
    candidate_key = _normalize_field_lookup_key(candidate_text)
    if not candidate_key:
        return 0.0, "none"
    if candidate_key == requested_key:
        return (0.98 if label == "title" else 0.95), f"{label}_exact"
    if candidate_key.startswith(requested_key):
        return (0.9 if label == "title" else 0.87), f"{label}_prefix"
    if requested_key in candidate_key:
        return (0.84 if label == "title" else 0.8), f"{label}_contains"
    if fuzzy:
        similarity = _normalized_text_similarity(requested_key, candidate_key)
        if similarity >= 0.3:
            return similarity, f"{label}_fuzzy"
    return 0.0, "none"


def _write_format_for_field(field: FormField) -> JSONObject:
    if field.que_type in VERIFY_UNSUPPORTED_WRITE_QUE_TYPES:
        return _write_support_payload(
            support_level="unsupported",
            kind="unsupported_direct_write",
            reason=_unsupported_write_reason(field.que_type),
            fix_hint=_unsupported_write_fix_hint(field.que_type),
        )
    if field.que_type in SUBTABLE_QUE_TYPES:
        return _write_support_payload(
            support_level="restricted",
            kind="subtable_rows",
            accepted_aliases=["rows", "tableValues", "rowId", "row_id", "__row_id__"],
            row_shape="list of row objects keyed by subfield title, or native tableValues rows",
            required_presteps=[
                "Use the current subfield titles from the form schema.",
                "When updating existing rows, include rowId/row_id only if the source record already exposes it.",
            ],
            subfields=_subtable_columns_for_field(field),
            examples=[{"rows": [{"子字段1": "值", "子字段2": 1}]}],
        )
    if field.que_type in MEMBER_QUE_TYPES:
        return _write_support_payload(
            support_level="restricted",
            kind="member_list",
            examples=[{"id": 2, "value": "张三"}],
            accepted_aliases=["uid", "userId", "userName"],
            required_presteps=[
                "Pass a member id/userId/member object, or a natural string that resolves uniquely inside the backend candidate scope.",
                "If multiple members match, the write stops and returns candidates for confirmation.",
            ],
        )
    if field.que_type in DEPARTMENT_QUE_TYPES:
        return _write_support_payload(
            support_level="restricted",
            kind="department_list",
            examples=[{"id": 11, "value": "示例部门"}],
            accepted_aliases=["deptId", "deptName"],
            required_presteps=[
                "Pass a dept id/object, or a natural string that resolves uniquely inside the backend department scope.",
                "If multiple departments match, the write stops and returns candidates for confirmation.",
            ],
        )
    if field.que_type in ATTACHMENT_QUE_TYPES:
        return _write_support_payload(
            support_level="restricted",
            kind="attachment_list",
            examples=[{"value": "https://files.example.com/a.pdf", "name": "a.pdf"}],
            required_presteps=["Upload the file first with file_upload_local, then write the returned attachment value/url."],
        )
    if field.que_type in RELATION_QUE_TYPES:
        return _write_support_payload(
            support_level="restricted",
            kind="relation_record",
            examples=[{"apply_id": 5001}],
            required_presteps=[
                "Pass an apply_id/object, or a natural string that resolves uniquely inside the relation field's backend searchable fields.",
                "If multiple referenced records match, the write stops and returns candidates for confirmation.",
            ],
        )
    if field.que_type in ADDRESS_QUE_TYPES:
        return _write_support_payload(
            support_level="restricted",
            kind="address_parts",
            accepted_aliases=["province", "city", "district", "detail", "address", "values", "parts"],
            examples=[
                {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"},
                [{"id": 1, "value": "上海市", "otherInfo": "310000"}, {"id": 4, "value": "浦江路99号"}],
            ],
            required_presteps=["Pass a detail string, ordered address parts, or a province/city/district/detail object."],
        )
    if field.que_type in SINGLE_SELECT_QUE_TYPES:
        return _write_support_payload(support_level="full", kind="single_select", options=field.options)
    if field.que_type in MULTI_SELECT_QUE_TYPES:
        return _write_support_payload(support_level="full", kind="multi_select", options=field.options)
    if field.que_type in BOOLEAN_QUE_TYPES:
        return _write_support_payload(support_level="full", kind="boolean_label", examples=["是", "否"])
    if field.que_type in DATE_QUE_TYPES:
        return _write_support_payload(support_level="full", kind="date_string", examples=["2026-03-13 10:00:00"])
    if field.que_type == 8:
        allow_decimal = bool((field.raw or {}).get("canDecimal"))
        payload = _write_support_payload(
            support_level="full",
            kind="amount_number",
            examples=[100.5 if allow_decimal else 100],
        )
        payload["allow_decimal"] = allow_decimal
        return payload
    return _write_support_payload(support_level="full", kind="scalar_text")


def _ready_schema_format_hint(kind: str, write_format: JSONObject) -> str:
    if kind == "member":
        return "可直接填成员姓名；唯一匹配会自动解析，重名时会返回候选确认。也可传成员 id/value 对象。"
    if kind == "department":
        return "可直接填部门名称；唯一匹配会自动解析，重名时会返回候选确认。也可传部门 id/value 对象。"
    if kind == "relation":
        return "可传目标记录 apply_id/record_id 对象，也可填目标记录的可搜索文本；多候选时会返回确认。"
    if kind == "attachment":
        return "先调用 file_upload_local 上传文件，再写入上传返回的附件对象或 value/name。"
    if kind == "subtable":
        return "传 {'rows': [{...}]} 或直接传行对象数组；每行 key 使用子字段标题。"
    if kind == "address":
        return "传省/市/区/详细地址对象、地址明细字符串，或后端地址 parts 数组。"
    if kind == "single_select":
        return "传 options 中的一个选项文本。"
    if kind == "multi_select":
        return "传 options 中的多个选项文本数组。"
    if kind == "boolean":
        return "传 '是' 或 '否'。"
    if kind == "date":
        return "推荐传 'YYYY-MM-DD HH:MM:SS'；只有日期时可传 'YYYY-MM-DD'。"
    if kind == "number":
        write_kind = _normalize_optional_text(write_format.get("kind"))
        if write_kind == "amount_number":
            if bool(write_format.get("allow_decimal")):
                return "传数字或数字字符串，支持小数。"
            return "传整数或整数字符串；该字段后端不接受小数。"
        return "传数字或数字字符串。"
    if kind == "unsupported":
        reason = _normalize_optional_text(write_format.get("reason"))
        return reason or "该字段不支持直接写入。"
    return "传文本值。"


def _ready_schema_example_value(
    kind: str,
    field: FormField,
    write_format: JSONObject,
    *,
    row_fields: list[JSONObject],
) -> JSONValue:
    if kind == "member":
        return "张三"
    if kind == "department":
        return "直销部"
    if kind == "relation":
        return {"apply_id": "5001"}
    if kind == "attachment":
        return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
    if kind == "subtable":
        row: JSONObject = {}
        for item in row_fields:
            if not isinstance(item, dict):
                continue
            title = _normalize_optional_text(item.get("title"))
            if not title:
                continue
            row[title] = item.get("example_value", _ready_schema_template_scalar(item.get("kind")))
        if not row:
            row = {"子字段": "值"}
        return {"rows": [row]}
    if kind == "address":
        examples = write_format.get("examples")
        if isinstance(examples, list) and examples:
            return deepcopy(cast(JSONValue, examples[0]))
        return {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"}
    if kind == "single_select":
        return field.options[0] if field.options else "选项A"
    if kind == "multi_select":
        return [field.options[0]] if field.options else ["选项A"]
    if kind == "boolean":
        return "是"
    if kind == "date":
        return "2026-03-13 10:00:00"
    if kind == "number":
        return 100
    if kind == "unsupported":
        return None
    return "示例文本"


def _ready_schema_template_scalar(kind: Any) -> JSONValue:
    normalized = _normalize_optional_text(kind)
    if normalized == "number":
        return 100
    if normalized == "date":
        return "2026-03-13 10:00:00"
    if normalized == "boolean":
        return "是"
    if normalized == "member":
        return "张三"
    if normalized == "department":
        return "直销部"
    if normalized == "relation":
        return {"apply_id": "5001"}
    if normalized == "multi_select":
        return ["选项A"]
    if normalized == "attachment":
        return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
    if normalized == "address":
        return {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"}
    return "值"


def _summarize_write_support(resolved_fields: list[JSONObject]) -> JSONObject:
    summary: JSONObject = {
        "full": [],
        "restricted": [],
        "unsupported": [],
    }
    for entry in resolved_fields:
        if not bool(entry.get("resolved")):
            continue
        write_format = entry.get("write_format")
        if not isinstance(write_format, dict):
            continue
        support_level = str(write_format.get("support_level", "full"))
        bucket = summary.get(support_level)
        if not isinstance(bucket, list):
            continue
        bucket.append(
            {
                "source": entry.get("source"),
                "requested": entry.get("requested"),
                "que_id": entry.get("que_id"),
                "que_title": entry.get("que_title"),
                "que_type": entry.get("que_type"),
                "kind": write_format.get("kind"),
            }
        )
    return summary


def _subtable_row_id(row: list[JSONObject]) -> int | None:
    for cell in row:
        if not isinstance(cell, dict):
            continue
        row_id = _coerce_count(cell.get("rowId", cell.get("row_id")))
        if row_id is not None:
            return row_id
    return None


def _index_subtable_rows_by_row_id(rows: list[JSONValue]) -> dict[int, list[JSONObject]]:
    indexed: dict[int, list[JSONObject]] = {}
    for row in rows:
        if not isinstance(row, list):
            continue
        normalized_row = [item for item in row if isinstance(item, dict)]
        row_id = _subtable_row_id(normalized_row)
        if row_id is not None:
            indexed[row_id] = normalized_row
    return indexed


def _subtable_cell_ref_payload(
    table_field: FormField | None,
    subfield: FormField | None,
    *,
    que_id: int,
    row_ordinal: int,
    row_id: int | None,
) -> JSONObject:
    payload: JSONObject = {
        "parent_que_id": table_field.que_id if table_field is not None else None,
        "parent_que_title": table_field.que_title if table_field is not None else None,
        "row_ordinal": row_ordinal,
        "que_id": que_id,
        "que_title": subfield.que_title if subfield is not None else None,
        "que_type": subfield.que_type if subfield is not None else None,
    }
    if row_id is not None:
        payload["row_id"] = row_id
    return payload


def _unsupported_write_reason(que_type: int | None) -> str:
    if que_type == 14:
        return "time range fields require a backend-specific native payload shape that app-user tools do not build yet"
    if que_type == 34:
        return "image recognition fields are runtime AI fields and direct writes are not reliably persisted"
    if que_type == 35:
        return "image generation fields are runtime AI fields and direct writes are not reliably persisted"
    if que_type == 36:
        return "document parsing fields are runtime AI fields and direct writes are not reliably persisted"
    return "direct writes are not supported for this field type"


def _unsupported_write_fix_hint(que_type: int | None) -> str:
    if que_type == 14:
        return "Avoid direct writes for time range fields until native payload support is added."
    if que_type in {34, 35, 36}:
        return "Avoid direct writes for AI/runtime fields, or submit through the product UI/workflow path that populates them."
    return "Avoid direct writes for this field type."


def _parse_json_like(value: JSONValue) -> JSONValue:
    if isinstance(value, dict):
        return {key: _parse_json_like(item) for key, item in value.items()}
    if isinstance(value, list):
        return [_parse_json_like(item) for item in value]
    if isinstance(value, str):
        text = value.strip()
        if not text:
            return value
        if (text.startswith("{") and text.endswith("}")) or (text.startswith("[") and text.endswith("]")):
            try:
                return _parse_json_like(cast(JSONValue, json.loads(text)))
            except json.JSONDecodeError:
                return value
        if text.lower() in {"true", "false"}:
            return text.lower() == "true"
    return value


def _as_selector_list(value: JSONValue) -> list[str | int]:
    if isinstance(value, list):
        return [cast(str | int, item) for item in value if isinstance(item, (str, int))]
    return []


def _as_object_list(value: JSONValue) -> list[JSONObject]:
    if isinstance(value, list):
        return [cast(JSONObject, item) for item in value if isinstance(item, dict)]
    return []


def _stringify_json(value: JSONValue) -> str:
    if value is None:
        return ""
    if isinstance(value, str):
        return value
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (int, float)):
        return str(value)
    return json.dumps(value, ensure_ascii=False)


def _normalized_text_similarity(left: str, right: str) -> float:
    if not left or not right:
        return 0.0
    if left == right:
        return 1.0
    left_bigrams = _bigrams(left)
    right_bigrams = _bigrams(right)
    if not left_bigrams or not right_bigrams:
        return 0.0
    intersection = len(left_bigrams & right_bigrams)
    union = len(left_bigrams | right_bigrams)
    return (intersection / union) if union else 0.0


def _bigrams(text: str) -> set[str]:
    normalized = text.replace(" ", "")
    if len(normalized) <= 1:
        return {normalized}
    return {normalized[index : index + 2] for index in range(len(normalized) - 1)}
