@qingflow-tech/qingflow-app-builder-mcp 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.5
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.6
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.5 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.6 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.5"
7
+ version = "1.0.6"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -82,7 +82,8 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
82
82
 
83
83
  insert = record_subparsers.add_parser("insert", help="新增记录")
84
84
  insert.add_argument("--app-key", required=True)
85
- insert.add_argument("--fields-file", required=True)
85
+ insert.add_argument("--fields-file")
86
+ insert.add_argument("--items-file")
86
87
  insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
87
88
  insert.set_defaults(handler=_handle_insert, format_hint="")
88
89
 
@@ -274,6 +275,23 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
274
275
 
275
276
 
276
277
  def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
278
+ if args.items_file:
279
+ if args.fields_file:
280
+ raise_config_error(
281
+ "record insert batch mode does not accept --fields-file.",
282
+ fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json` for batch inserts.",
283
+ )
284
+ return context.record.record_insert_public(
285
+ profile=args.profile,
286
+ app_key=args.app_key,
287
+ items=require_list_arg(args.items_file, option_name="--items-file"),
288
+ verify_write=bool(args.verify_write),
289
+ )
290
+ if not args.fields_file:
291
+ raise_config_error(
292
+ "record insert requires --items-file or --fields-file.",
293
+ fix_hint="Prefer `record insert --app-key APP_KEY --items-file ITEMS.json`; use --fields-file only for legacy single inserts.",
294
+ )
277
295
  return context.record.record_insert_public(
278
296
  profile=args.profile,
279
297
  app_key=args.app_key,
@@ -57,6 +57,13 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
57
57
  if not isinstance(payload, dict):
58
58
  return payload
59
59
  if _looks_like_failure_payload(payload):
60
+ status = str(payload.get("status") or "").lower()
61
+ if tool_name in {"user:record_insert", "user:record_update"} and status in {
62
+ "blocked",
63
+ "needs_confirmation",
64
+ "partial_success",
65
+ }:
66
+ return trim_success_response(tool_name, payload)
60
67
  return _trim_returned_failure(payload)
61
68
  return trim_success_response(tool_name, payload)
62
69
 
@@ -140,7 +147,7 @@ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
140
147
  if payload.get("ok") is False:
141
148
  return True
142
149
  status = str(payload.get("status") or "").lower()
143
- return status in {"failed", "blocked", "verification_failed"}
150
+ return status in {"failed", "blocked"}
144
151
 
145
152
 
146
153
  def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
@@ -368,6 +375,9 @@ def _trim_record_write(payload: JSONObject) -> None:
368
375
  data = payload.get("data")
369
376
  if not isinstance(data, dict):
370
377
  return
378
+ if payload.get("mode") == "batch" or data.get("mode") == "batch":
379
+ _trim_record_write_batch(payload, data)
380
+ return
371
381
  data.pop("debug", None)
372
382
  data.pop("normalized_payload", None)
373
383
  data.pop("human_review", None)
@@ -397,6 +407,44 @@ def _trim_record_write(payload: JSONObject) -> None:
397
407
  data.pop(key, None)
398
408
 
399
409
 
410
+ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
411
+ data.pop("items", None)
412
+ data.pop("debug", None)
413
+ for key in ("summary", "created_record_ids", "app_key", "mode"):
414
+ if data.get(key) in (None, [], {}, ""):
415
+ data.pop(key, None)
416
+ items = payload.get("items")
417
+ if isinstance(items, list):
418
+ payload["items"] = [
419
+ _pick(
420
+ item,
421
+ (
422
+ "index",
423
+ "row_number",
424
+ "status",
425
+ "record_id",
426
+ "apply_id",
427
+ "write_executed",
428
+ "verification_status",
429
+ "safe_to_retry",
430
+ "failed_fields",
431
+ "confirmation_requests",
432
+ "blockers",
433
+ "error",
434
+ "warnings",
435
+ "resource",
436
+ "verification",
437
+ ),
438
+ )
439
+ for item in items
440
+ if isinstance(item, dict)
441
+ ]
442
+ for key in ("items", "created_record_ids"):
443
+ value = payload.get(key)
444
+ if value in (None, [], {}, ""):
445
+ payload.pop(key, None)
446
+
447
+
400
448
  def _trim_record_get(payload: JSONObject) -> None:
401
449
  if payload.get("fields") is not None or payload.get("semantic_context") is not None:
402
450
  _trim_detail_context_record_get(payload)
@@ -730,6 +778,19 @@ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) ->
730
778
  compact["required"] = bool(item.get("required"))
731
779
  if template_map is not None and isinstance(title, str) and title in template_map:
732
780
  compact["template"] = template_map.get(title)
781
+ for key in (
782
+ "format_hint",
783
+ "example_value",
784
+ "linkage",
785
+ "may_become_required",
786
+ "activation_sources",
787
+ "requirement_reason",
788
+ "accepts_natural_input",
789
+ "requires_upload",
790
+ ):
791
+ value = item.get(key)
792
+ if value not in (None, [], {}, ""):
793
+ compact[key] = value
733
794
  candidate_hint = item.get("candidate_hint")
734
795
  if isinstance(candidate_hint, dict):
735
796
  compact["candidate_hint"] = candidate_hint
@@ -104,7 +104,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
104
104
  ## Record CRUD Path
105
105
 
106
106
  `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
107
- `record_insert_schema_get -> record_insert`
107
+ `record_insert_schema_get -> record_insert(items)`
108
108
  `record_update_schema_get -> record_update`
109
109
  `record_list / record_get -> record_delete`
110
110
  `record_code_block_schema_get -> record_code_block_run`
@@ -115,7 +115,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
115
115
  - Use `order_by` items as `{{field_id, direction}}`
116
116
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
117
117
 
118
- - `record_insert` uses an applicant-node `fields` map keyed by field title.
118
+ - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
119
119
  - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
120
120
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
121
121
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
@@ -130,7 +130,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
130
130
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
131
131
 
132
132
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
133
- - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
133
+ - Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
134
+ - For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
134
135
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
135
136
 
136
137
  ## Code Block Path
@@ -103,7 +103,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
103
103
  ## Record CRUD Path
104
104
 
105
105
  `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
106
- `record_insert_schema_get -> record_insert`
106
+ `record_insert_schema_get -> record_insert(items)`
107
107
  `record_update_schema_get -> record_update`
108
108
  `record_list / record_get -> record_delete`
109
109
  `record_code_block_schema_get -> record_code_block_run`
@@ -116,7 +116,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
116
116
  - Use `order_by` items as `{{field_id, direction}}`
117
117
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
118
118
 
119
- - `record_insert` uses an applicant-node `fields` map keyed by field title.
119
+ - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
120
120
  - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
121
121
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
122
122
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
@@ -131,7 +131,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
131
131
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
132
132
 
133
133
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
134
- - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
134
+ - Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
135
+ - For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
135
136
  - When candidate browsing must match a real update/write scope, pass `record_id`, `workflow_node_id`, and any pending `fields` context to the candidate tool; otherwise the candidate result is only a static applicant-node preview.
136
137
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
137
138
 
@@ -470,21 +470,24 @@ class RecordTools(ToolBase):
470
470
 
471
471
  @mcp.tool(
472
472
  description=(
473
- "Insert one Qingflow record using an applicant-node field map. "
473
+ "Insert Qingflow records using applicant-node field maps. "
474
474
  "Use record_insert_schema_get first. "
475
- "This tool performs internal preflight validation before any write is applied."
475
+ "Prefer items=[{'fields': {...}}]; a single insert is one item. "
476
+ "Each item performs internal preflight validation before that item is written."
476
477
  )
477
478
  )
478
479
  def record_insert(
479
480
  app_key: str = "",
480
481
  fields: JSONObject | None = None,
482
+ items: list[JSONObject] | None = None,
481
483
  verify_write: bool = True,
482
484
  output_profile: str = "normal",
483
485
  ) -> JSONObject:
484
486
  return self.record_insert_public(
485
487
  profile=DEFAULT_PROFILE,
486
488
  app_key=app_key,
487
- fields=fields or {},
489
+ fields=fields,
490
+ items=items,
488
491
  verify_write=verify_write,
489
492
  output_profile=output_profile,
490
493
  )
@@ -1064,11 +1067,16 @@ class RecordTools(ToolBase):
1064
1067
  required = bool(required_override) if required_override is not None else bool(field.required or any(item.get("required") for item in row_fields))
1065
1068
  else:
1066
1069
  required = bool(required_override) if required_override is not None else bool(field.required)
1070
+ write_format = _write_format_for_field(field)
1067
1071
  payload: JSONObject = {
1068
1072
  "title": field.que_title,
1069
1073
  "kind": kind,
1070
1074
  "required": required,
1075
+ "format_hint": _ready_schema_format_hint(kind, write_format),
1071
1076
  }
1077
+ example_value = _ready_schema_example_value(kind, field, write_format, row_fields=row_fields)
1078
+ if example_value is not None:
1079
+ payload["example_value"] = example_value
1072
1080
  if include_field_id:
1073
1081
  payload["field_id"] = field.que_id
1074
1082
  if kind in {"single_select", "multi_select"} and field.options:
@@ -2995,6 +3003,7 @@ class RecordTools(ToolBase):
2995
3003
  profile: str = DEFAULT_PROFILE,
2996
3004
  app_key: str,
2997
3005
  fields: JSONObject | None = None,
3006
+ items: list[JSONObject] | None = None,
2998
3007
  verify_write: bool = True,
2999
3008
  output_profile: str = "normal",
3000
3009
  ) -> JSONObject:
@@ -3002,69 +3011,478 @@ class RecordTools(ToolBase):
3002
3011
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
3003
3012
  if not app_key:
3004
3013
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
3014
+ if items is not None:
3015
+ normalized_items = self._normalize_public_record_insert_batch_items(fields=fields, items=items)
3016
+ return self._record_insert_public_batch(
3017
+ profile=profile,
3018
+ app_key=app_key,
3019
+ items=normalized_items,
3020
+ verify_write=verify_write,
3021
+ output_profile=normalized_output_profile,
3022
+ )
3005
3023
  if fields is not None and not isinstance(fields, dict):
3006
3024
  raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
3007
- submit_type_value = self._normalize_record_write_submit_type("submit")
3008
- raw_preflight = self._preflight_record_write(
3025
+ return self._record_insert_public_single(
3009
3026
  profile=profile,
3010
- operation="create",
3011
3027
  app_key=app_key,
3012
- apply_id=None,
3013
- answers=[],
3014
3028
  fields=cast(JSONObject, fields or {}),
3015
- force_refresh_form=False,
3016
- view_id=None,
3017
- list_type=None,
3018
- view_key=None,
3019
- view_name=None,
3020
- )
3021
- preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
3022
- preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
3023
- normalized_payload: JSONObject = self._record_write_normalized_payload(
3024
- operation="insert",
3025
- record_id=None,
3026
- record_ids=[],
3027
- normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3028
- submit_type=submit_type_value,
3029
- selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3029
+ verify_write=verify_write,
3030
+ output_profile=normalized_output_profile,
3031
+ capture_exceptions=False,
3030
3032
  )
3031
- if preflight_data.get("blockers"):
3032
- return self._record_write_blocked_response(
3033
- raw_preflight,
3034
- operation="insert",
3035
- normalized_payload=normalized_payload,
3036
- output_profile=normalized_output_profile,
3037
- human_review=False,
3038
- target_resource={"type": "record", "app_key": app_key, "record_id": None, "record_ids": []},
3039
- )
3033
+
3034
+ def _record_insert_public_single(
3035
+ self,
3036
+ *,
3037
+ profile: str,
3038
+ app_key: str,
3039
+ fields: JSONObject,
3040
+ verify_write: bool,
3041
+ output_profile: str,
3042
+ capture_exceptions: bool,
3043
+ ) -> JSONObject:
3044
+ """执行内部辅助逻辑。"""
3045
+ submit_type_value = self._normalize_record_write_submit_type("submit")
3046
+ write_attempted = False
3040
3047
  try:
3041
- raw_apply = self.record_create(
3048
+ raw_preflight = self._preflight_record_write(
3042
3049
  profile=profile,
3050
+ operation="create",
3043
3051
  app_key=app_key,
3044
- answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3045
- fields={},
3052
+ apply_id=None,
3053
+ answers=[],
3054
+ fields=fields,
3055
+ force_refresh_form=False,
3056
+ view_id=None,
3057
+ list_type=None,
3058
+ view_key=None,
3059
+ view_name=None,
3060
+ )
3061
+ preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
3062
+ preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
3063
+ normalized_payload: JSONObject = self._record_write_normalized_payload(
3064
+ operation="insert",
3065
+ record_id=None,
3066
+ record_ids=[],
3067
+ normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3046
3068
  submit_type=submit_type_value,
3047
- verify_write=verify_write,
3048
- force_refresh_form=preflight_used_force_refresh,
3069
+ selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3049
3070
  )
3050
- except QingflowApiError as exc:
3051
- self._raise_record_write_permission_error(
3071
+ if preflight_data.get("blockers"):
3072
+ return self._record_write_blocked_response(
3073
+ raw_preflight,
3074
+ operation="insert",
3075
+ normalized_payload=normalized_payload,
3076
+ output_profile=output_profile,
3077
+ human_review=False,
3078
+ target_resource={"type": "record", "app_key": app_key, "record_id": None, "record_ids": []},
3079
+ )
3080
+ try:
3081
+ write_attempted = True
3082
+ raw_apply = self.record_create(
3083
+ profile=profile,
3084
+ app_key=app_key,
3085
+ answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3086
+ fields={},
3087
+ submit_type=submit_type_value,
3088
+ verify_write=verify_write,
3089
+ force_refresh_form=preflight_used_force_refresh,
3090
+ )
3091
+ except QingflowApiError as exc:
3092
+ self._raise_record_write_permission_error(
3093
+ exc,
3094
+ operation="insert",
3095
+ app_key=app_key,
3096
+ record_id=None,
3097
+ selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3098
+ )
3099
+ raise
3100
+ return self._record_write_apply_response(
3101
+ raw_apply,
3102
+ operation="insert",
3103
+ normalized_payload=normalized_payload,
3104
+ output_profile=output_profile,
3105
+ human_review=False,
3106
+ preflight=raw_preflight,
3107
+ )
3108
+ except (QingflowApiError, RuntimeError) as exc:
3109
+ if not capture_exceptions:
3110
+ raise
3111
+ return self._record_write_exception_response(
3052
3112
  exc,
3053
3113
  operation="insert",
3114
+ profile=profile,
3054
3115
  app_key=app_key,
3055
3116
  record_id=None,
3056
- selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3117
+ output_profile=output_profile,
3118
+ human_review=False,
3119
+ write_executed=write_attempted,
3057
3120
  )
3058
- raise
3059
- return self._record_write_apply_response(
3060
- raw_apply,
3061
- operation="insert",
3062
- normalized_payload=normalized_payload,
3063
- output_profile=normalized_output_profile,
3064
- human_review=False,
3065
- preflight=raw_preflight,
3121
+
3122
+ def _normalize_public_record_insert_batch_items(
3123
+ self,
3124
+ *,
3125
+ fields: JSONObject | None,
3126
+ items: list[JSONObject] | None,
3127
+ ) -> list[JSONObject]:
3128
+ """执行内部辅助逻辑。"""
3129
+ if fields is not None:
3130
+ raise_tool_error(QingflowApiError.config_error("record_insert batch mode does not accept fields"))
3131
+ if not isinstance(items, list) or not items:
3132
+ raise_tool_error(QingflowApiError.config_error("items must be a non-empty list"))
3133
+ normalized_items: list[JSONObject] = []
3134
+ for index, item in enumerate(items):
3135
+ if not isinstance(item, dict):
3136
+ raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
3137
+ item_fields = item.get("fields")
3138
+ if not isinstance(item_fields, dict):
3139
+ raise_tool_error(QingflowApiError.config_error(f"items[{index}].fields must be an object map keyed by field title"))
3140
+ normalized_items.append({"fields": cast(JSONObject, item_fields)})
3141
+ return normalized_items
3142
+
3143
+ def _record_insert_public_batch(
3144
+ self,
3145
+ *,
3146
+ profile: str,
3147
+ app_key: str,
3148
+ items: list[JSONObject],
3149
+ verify_write: bool,
3150
+ output_profile: str,
3151
+ ) -> JSONObject:
3152
+ """执行内部辅助逻辑。"""
3153
+ responses: list[JSONObject] = []
3154
+ for item in items:
3155
+ responses.append(
3156
+ self._record_insert_public_single(
3157
+ profile=profile,
3158
+ app_key=app_key,
3159
+ fields=cast(JSONObject, item["fields"]),
3160
+ verify_write=verify_write,
3161
+ output_profile=output_profile,
3162
+ capture_exceptions=True,
3163
+ )
3164
+ )
3165
+ return self._record_insert_batch_response(
3166
+ profile=profile,
3167
+ app_key=app_key,
3168
+ responses=responses,
3169
+ output_profile=output_profile,
3066
3170
  )
3067
3171
 
3172
+ def _record_insert_batch_response(
3173
+ self,
3174
+ *,
3175
+ profile: str,
3176
+ app_key: str,
3177
+ responses: list[JSONObject],
3178
+ output_profile: str,
3179
+ ) -> JSONObject:
3180
+ """执行内部辅助逻辑。"""
3181
+ items = [
3182
+ self._record_insert_batch_item_from_response(index=index, response=response, output_profile=output_profile)
3183
+ for index, response in enumerate(responses)
3184
+ ]
3185
+ summary = self._record_insert_batch_summary(items)
3186
+ status, ok, message = self._record_insert_batch_envelope_status(summary=summary)
3187
+ first_response = responses[0] if responses else {}
3188
+ created_record_ids = [
3189
+ cast(str, item["record_id"])
3190
+ for item in items
3191
+ if isinstance(item.get("record_id"), str) and item.get("record_id")
3192
+ ]
3193
+ write_executed = any(bool(item.get("write_executed")) for item in items)
3194
+ verification_status = self._record_insert_batch_verification_status(items)
3195
+ return {
3196
+ "profile": first_response.get("profile", profile),
3197
+ "ws_id": first_response.get("ws_id"),
3198
+ "ok": ok,
3199
+ "status": status,
3200
+ "mode": "batch",
3201
+ "total": summary["total"],
3202
+ "succeeded": summary["succeeded"],
3203
+ "failed": summary["failed"],
3204
+ "created_record_ids": created_record_ids,
3205
+ "write_executed": write_executed,
3206
+ "verification_status": verification_status,
3207
+ "safe_to_retry": not write_executed,
3208
+ "request_route": first_response.get("request_route"),
3209
+ "warnings": [],
3210
+ "output_profile": output_profile,
3211
+ "items": items,
3212
+ "data": {
3213
+ "app_key": app_key,
3214
+ "mode": "batch",
3215
+ "summary": summary,
3216
+ "created_record_ids": created_record_ids,
3217
+ "items": items,
3218
+ },
3219
+ "message": message,
3220
+ }
3221
+
3222
+ def _record_insert_batch_summary(self, items: list[JSONObject]) -> JSONObject:
3223
+ """执行内部辅助逻辑。"""
3224
+ created = [item for item in items if isinstance(item.get("record_id"), str) and item.get("record_id")]
3225
+ failed = [item for item in items if item.get("status") not in {"success", "verification_failed"}]
3226
+ return {
3227
+ "total": len(items),
3228
+ "succeeded": len(created),
3229
+ "failed": len(failed),
3230
+ "created_count": len(created),
3231
+ "blocked_count": sum(1 for item in items if item.get("status") == "blocked"),
3232
+ "confirmation_count": sum(1 for item in items if item.get("status") == "needs_confirmation"),
3233
+ "verification_failed_count": sum(1 for item in items if item.get("status") == "verification_failed"),
3234
+ }
3235
+
3236
+ def _record_insert_batch_envelope_status(self, *, summary: JSONObject) -> tuple[str, bool, str]:
3237
+ """执行内部辅助逻辑。"""
3238
+ succeeded = int(summary["succeeded"])
3239
+ failed = int(summary["failed"])
3240
+ if succeeded and failed:
3241
+ return "partial_success", False, "batch insert completed with partial failures"
3242
+ if succeeded and int(summary["verification_failed_count"]):
3243
+ return "verification_failed", True, "batch insert completed but verification failed for some created records"
3244
+ if succeeded:
3245
+ return "success", True, "batch insert completed"
3246
+ if int(summary["confirmation_count"]):
3247
+ return "needs_confirmation", False, "batch insert requires confirmation before retrying failed rows"
3248
+ if int(summary["blocked_count"]):
3249
+ return "blocked", False, "batch insert preflight blocked all rows"
3250
+ return "failed", False, "batch insert failed"
3251
+
3252
+ def _record_insert_batch_verification_status(self, items: list[JSONObject]) -> str:
3253
+ """执行内部辅助逻辑。"""
3254
+ statuses = {str(item.get("verification_status") or "not_requested") for item in items}
3255
+ if "failed" in statuses:
3256
+ return "failed"
3257
+ if "verified" in statuses:
3258
+ return "verified"
3259
+ return "not_requested"
3260
+
3261
+ def _record_insert_batch_item_from_response(
3262
+ self,
3263
+ *,
3264
+ index: int,
3265
+ response: JSONObject,
3266
+ output_profile: str,
3267
+ ) -> JSONObject:
3268
+ """执行内部辅助逻辑。"""
3269
+ data = cast(JSONObject, response.get("data", {})) if isinstance(response.get("data"), dict) else {}
3270
+ resource = _public_record_resource(data.get("resource"))
3271
+ record_id = _public_record_id_text(response.get("record_id"))
3272
+ apply_id = _public_record_id_text(response.get("apply_id"))
3273
+ if record_id is None and isinstance(resource, dict):
3274
+ record_id = _public_record_id_text(resource.get("record_id"))
3275
+ if apply_id is None and isinstance(resource, dict):
3276
+ apply_id = _public_record_id_text(resource.get("apply_id"))
3277
+ item: JSONObject = {
3278
+ "index": index,
3279
+ "row_number": index + 1,
3280
+ "status": response.get("status"),
3281
+ "write_executed": bool(response.get("write_executed")),
3282
+ "verification_status": response.get("verification_status", "not_requested"),
3283
+ "safe_to_retry": bool(response.get("safe_to_retry", True)),
3284
+ }
3285
+ if record_id is not None:
3286
+ item["record_id"] = record_id
3287
+ if apply_id is not None:
3288
+ item["apply_id"] = apply_id
3289
+ if resource:
3290
+ item["resource"] = resource
3291
+ verification = data.get("verification")
3292
+ if isinstance(verification, dict):
3293
+ compact_verification = {
3294
+ key: verification[key]
3295
+ for key in ("verified", "verification_mode", "field_level_verified")
3296
+ if key in verification
3297
+ }
3298
+ if compact_verification:
3299
+ item["verification"] = compact_verification
3300
+ field_errors = cast(list[JSONObject], data.get("field_errors", [])) if isinstance(data.get("field_errors"), list) else []
3301
+ confirmation_requests = cast(list[JSONObject], data.get("confirmation_requests", [])) if isinstance(data.get("confirmation_requests"), list) else []
3302
+ failed_fields = self._record_write_failed_fields(field_errors=field_errors, confirmation_requests=confirmation_requests)
3303
+ if failed_fields:
3304
+ item["failed_fields"] = failed_fields
3305
+ if confirmation_requests:
3306
+ item["confirmation_requests"] = [
3307
+ self._record_write_semantic_confirmation_request(request)
3308
+ for request in confirmation_requests
3309
+ if isinstance(request, dict)
3310
+ ]
3311
+ blockers = data.get("blockers")
3312
+ if isinstance(blockers, list) and blockers:
3313
+ item["blockers"] = blockers
3314
+ warnings = response.get("warnings")
3315
+ if isinstance(warnings, list) and warnings:
3316
+ item["warnings"] = warnings
3317
+ error = data.get("error")
3318
+ if isinstance(error, dict):
3319
+ item["error"] = error
3320
+ if output_profile == "verbose" and isinstance(data.get("debug"), dict):
3321
+ item["debug"] = data.get("debug")
3322
+ return item
3323
+
3324
+ def _record_write_failed_fields(
3325
+ self,
3326
+ *,
3327
+ field_errors: list[JSONObject],
3328
+ confirmation_requests: list[JSONObject],
3329
+ ) -> list[JSONObject]:
3330
+ """执行内部辅助逻辑。"""
3331
+ failed_fields = [
3332
+ self._record_write_semantic_field_error(error)
3333
+ for error in field_errors
3334
+ if isinstance(error, dict)
3335
+ ]
3336
+ failed_fields.extend(
3337
+ self._record_write_failed_field_from_confirmation(request)
3338
+ for request in confirmation_requests
3339
+ if isinstance(request, dict)
3340
+ )
3341
+ return failed_fields
3342
+
3343
+ def _record_write_semantic_field_error(self, error: JSONObject) -> JSONObject:
3344
+ """执行内部辅助逻辑。"""
3345
+ field = error.get("field")
3346
+ field_payload = cast(JSONObject, field) if isinstance(field, dict) else {}
3347
+ error_code = _normalize_optional_text(error.get("error_code")) or "INVALID_FIELD_VALUE"
3348
+ title = (
3349
+ _normalize_optional_text(field_payload.get("que_title"))
3350
+ or _normalize_optional_text(field_payload.get("title"))
3351
+ or _normalize_optional_text(error.get("location"))
3352
+ or "unknown field"
3353
+ )
3354
+ field_id = (
3355
+ field_payload.get("que_id")
3356
+ if field_payload.get("que_id") is not None
3357
+ else field_payload.get("field_id")
3358
+ )
3359
+ expected_format = error.get("expected_format") if isinstance(error.get("expected_format"), dict) else None
3360
+ if expected_format is None:
3361
+ expected_format = self._record_write_expected_format_from_field_payload(field_payload)
3362
+ payload: JSONObject = {
3363
+ "title": title,
3364
+ "field_id": field_id,
3365
+ "error_code": error_code,
3366
+ "message": self._record_write_semantic_error_message(error_code, error.get("message")),
3367
+ "next_action": self._record_write_next_action_for_error(error_code),
3368
+ }
3369
+ if expected_format is not None:
3370
+ payload["expected_format"] = expected_format
3371
+ payload["example_value"] = self._record_write_example_value_for_format(expected_format, field_payload)
3372
+ if error.get("received_value") is not None:
3373
+ payload["received_value"] = error.get("received_value")
3374
+ if error.get("fix_hint") is not None:
3375
+ payload["fix_hint"] = error.get("fix_hint")
3376
+ if error.get("details") is not None:
3377
+ payload["details"] = error.get("details")
3378
+ return payload
3379
+
3380
+ def _record_write_semantic_confirmation_request(self, request: JSONObject) -> JSONObject:
3381
+ """执行内部辅助逻辑。"""
3382
+ field_ref = request.get("field_ref")
3383
+ field_payload = cast(JSONObject, field_ref) if isinstance(field_ref, dict) else {}
3384
+ payload: JSONObject = {
3385
+ "field": request.get("field"),
3386
+ "title": _normalize_optional_text(request.get("field")) or _normalize_optional_text(field_payload.get("que_title")),
3387
+ "field_id": field_payload.get("que_id"),
3388
+ "kind": request.get("kind"),
3389
+ "input": request.get("input"),
3390
+ "candidates": request.get("candidates", []),
3391
+ "next_action": "让用户确认候选,或用显式 id/object 只重试本行。",
3392
+ }
3393
+ if request.get("parent_field") is not None:
3394
+ payload["parent_field"] = request.get("parent_field")
3395
+ if request.get("row_ordinal") is not None:
3396
+ payload["row_ordinal"] = request.get("row_ordinal")
3397
+ return payload
3398
+
3399
+ def _record_write_failed_field_from_confirmation(self, request: JSONObject) -> JSONObject:
3400
+ """执行内部辅助逻辑。"""
3401
+ semantic = self._record_write_semantic_confirmation_request(request)
3402
+ return {
3403
+ "title": semantic.get("title") or semantic.get("field"),
3404
+ "field_id": semantic.get("field_id"),
3405
+ "error_code": "LOOKUP_NEEDS_CONFIRMATION",
3406
+ "message": "候选不唯一,需要用户确认。",
3407
+ "kind": semantic.get("kind"),
3408
+ "input": semantic.get("input"),
3409
+ "candidates": semantic.get("candidates", []),
3410
+ "next_action": semantic.get("next_action"),
3411
+ }
3412
+
3413
+ def _record_write_expected_format_from_field_payload(self, field_payload: JSONObject) -> JSONObject | None:
3414
+ """执行内部辅助逻辑。"""
3415
+ que_type = _coerce_count(field_payload.get("que_type"))
3416
+ if que_type is None:
3417
+ return None
3418
+ synthetic_field = FormField(
3419
+ que_id=_coerce_count(field_payload.get("que_id")) or 0,
3420
+ que_title=_normalize_optional_text(field_payload.get("que_title")) or _normalize_optional_text(field_payload.get("title")) or "",
3421
+ que_type=que_type,
3422
+ required=False,
3423
+ readonly=False,
3424
+ system=False,
3425
+ options=[],
3426
+ aliases=[],
3427
+ target_app_key=None,
3428
+ target_app_name_hint=None,
3429
+ member_select_scope_type=None,
3430
+ member_select_scope=None,
3431
+ dept_select_scope_type=None,
3432
+ dept_select_scope=None,
3433
+ raw={},
3434
+ )
3435
+ return _write_format_for_field(synthetic_field)
3436
+
3437
+ def _record_write_example_value_for_format(self, expected_format: JSONObject, field_payload: JSONObject) -> JSONValue:
3438
+ """执行内部辅助逻辑。"""
3439
+ examples = expected_format.get("examples")
3440
+ if isinstance(examples, list) and examples:
3441
+ return cast(JSONValue, examples[0])
3442
+ kind = _normalize_optional_text(expected_format.get("kind"))
3443
+ if kind == "member_list":
3444
+ return "张三"
3445
+ if kind == "department_list":
3446
+ return "直销部"
3447
+ if kind == "relation_record":
3448
+ return {"apply_id": "5001"}
3449
+ if kind == "attachment_list":
3450
+ return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
3451
+ if kind == "subtable_rows":
3452
+ return {"rows": [{"子字段": "值"}]}
3453
+ if kind == "date_string":
3454
+ return "2026-03-13 10:00:00"
3455
+ if kind == "boolean_label":
3456
+ return "是"
3457
+ if kind in {"single_select", "multi_select"}:
3458
+ options = expected_format.get("options")
3459
+ if isinstance(options, list) and options:
3460
+ return cast(JSONValue, options[0])
3461
+ que_type = _coerce_count(field_payload.get("que_type"))
3462
+ if que_type in NUMBER_QUE_TYPES:
3463
+ return 100
3464
+ return "文本"
3465
+
3466
+ def _record_write_semantic_error_message(self, error_code: str, fallback: JSONValue) -> str:
3467
+ """执行内部辅助逻辑。"""
3468
+ if error_code == "MISSING_REQUIRED_FIELD":
3469
+ return "缺少必填字段。"
3470
+ if error_code == "FIELD_NOT_FOUND":
3471
+ return "字段不存在或字段标题不匹配。"
3472
+ if error_code == "AMBIGUOUS_FIELD":
3473
+ return "字段标题存在歧义。"
3474
+ if error_code in {"INVALID_FIELD_VALUE", "INVALID_MEMBER_VALUE", "INVALID_DEPARTMENT_VALUE", "INVALID_RELATION_VALUE"}:
3475
+ return _normalize_optional_text(fallback) or "字段值格式不正确。"
3476
+ return _normalize_optional_text(fallback) or "字段写入失败。"
3477
+
3478
+ def _record_write_next_action_for_error(self, error_code: str) -> str:
3479
+ """执行内部辅助逻辑。"""
3480
+ if error_code == "MISSING_REQUIRED_FIELD":
3481
+ return "补充该字段后只重试本行。"
3482
+ if error_code in {"FIELD_NOT_FOUND", "AMBIGUOUS_FIELD"}:
3483
+ return "重新调用 schema 工具确认字段标题或 field_id 后,只重试本行。"
3484
+ return "修正该字段值后只重试本行。"
3485
+
3068
3486
  @tool_cn_name("更新记录")
3069
3487
  def record_update_public(
3070
3488
  self,
@@ -7357,7 +7775,7 @@ class RecordTools(ToolBase):
7357
7775
  "result": result,
7358
7776
  "normalized_answers": normalized_answers,
7359
7777
  "status": "completed" if verified else "verification_failed",
7360
- "ok": verified,
7778
+ "ok": True,
7361
7779
  "apply_id": apply_id,
7362
7780
  "record_id": apply_id,
7363
7781
  "verify_write": verify_write,
@@ -7561,7 +7979,7 @@ class RecordTools(ToolBase):
7561
7979
  "result": result,
7562
7980
  "normalized_answers": normalized_answers,
7563
7981
  "status": "completed" if verified else "verification_failed",
7564
- "ok": verified,
7982
+ "ok": True,
7565
7983
  "verify_write": verify_write,
7566
7984
  "write_verified": verified if verify_write else None,
7567
7985
  "verification": verification,
@@ -10100,6 +10518,9 @@ class RecordTools(ToolBase):
10100
10518
  "ws_id": raw_preflight.get("ws_id"),
10101
10519
  "ok": False,
10102
10520
  "status": status,
10521
+ "write_executed": False,
10522
+ "verification_status": "not_requested",
10523
+ "safe_to_retry": True,
10103
10524
  "request_route": raw_preflight.get("request_route"),
10104
10525
  "warnings": warnings,
10105
10526
  "output_profile": output_profile,
@@ -10143,6 +10564,9 @@ class RecordTools(ToolBase):
10143
10564
  "ws_id": raw_preflight.get("ws_id"),
10144
10565
  "ok": True,
10145
10566
  "status": "ready",
10567
+ "write_executed": False,
10568
+ "verification_status": "not_requested",
10569
+ "safe_to_retry": True,
10146
10570
  "request_route": raw_preflight.get("request_route"),
10147
10571
  "warnings": warnings,
10148
10572
  "output_profile": output_profile,
@@ -10185,17 +10609,45 @@ class RecordTools(ToolBase):
10185
10609
  resolved_fields = cast(list[JSONObject], preflight_data.get("lookup_resolved_fields", []))
10186
10610
  if isinstance(verification_warnings, list):
10187
10611
  warnings.extend(cast(list[JSONObject], [item for item in verification_warnings if isinstance(item, dict)]))
10612
+ resource = _public_record_resource(raw_apply.get("resource"))
10613
+ record_id = _public_record_id_text(resource.get("record_id")) if isinstance(resource, dict) else None
10614
+ apply_id = _public_record_id_text(resource.get("apply_id")) if isinstance(resource, dict) else None
10615
+ if record_id is None:
10616
+ record_id = _public_record_id_text(raw_apply.get("record_id"))
10617
+ if apply_id is None:
10618
+ apply_id = _public_record_id_text(raw_apply.get("apply_id"))
10619
+ if apply_id is None:
10620
+ apply_id = record_id
10621
+ if record_id is None:
10622
+ record_id = apply_id
10623
+ write_executed = True
10624
+ verification_requested = (
10625
+ raw_apply.get("verify_write") is True
10626
+ or raw_apply.get("write_verified") is not None
10627
+ or isinstance(raw_apply.get("verification"), dict)
10628
+ )
10629
+ if verification_requested:
10630
+ verification_status = "verified" if bool(verification.get("verified")) else "failed"
10631
+ else:
10632
+ verification_status = "not_requested"
10633
+ raw_status = _normalize_optional_text(raw_apply.get("status"))
10634
+ response_status = "verification_failed" if verification_status == "failed" else "success"
10635
+ if not bool(raw_apply.get("ok", True)) and verification_status != "failed":
10636
+ response_status = raw_status or "failed"
10188
10637
  response: JSONObject = {
10189
10638
  "profile": raw_apply.get("profile"),
10190
10639
  "ws_id": raw_apply.get("ws_id"),
10191
- "ok": bool(raw_apply.get("ok", True)),
10192
- "status": "success" if bool(raw_apply.get("ok", True)) else _normalize_optional_text(raw_apply.get("status")) or "failed",
10640
+ "ok": True if verification_status == "failed" and write_executed else bool(raw_apply.get("ok", True)),
10641
+ "status": response_status,
10642
+ "write_executed": write_executed,
10643
+ "verification_status": verification_status,
10644
+ "safe_to_retry": False,
10193
10645
  "request_route": raw_apply.get("request_route"),
10194
10646
  "warnings": warnings,
10195
10647
  "output_profile": output_profile,
10196
10648
  "data": {
10197
10649
  "action": {"operation": operation, "executed": True},
10198
- "resource": _public_record_resource(raw_apply.get("resource")),
10650
+ "resource": resource,
10199
10651
  "verification": raw_apply.get("verification"),
10200
10652
  "normalized_payload": normalized_payload,
10201
10653
  "blockers": [],
@@ -10205,6 +10657,10 @@ class RecordTools(ToolBase):
10205
10657
  "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
10206
10658
  },
10207
10659
  }
10660
+ if record_id is not None:
10661
+ response["record_id"] = record_id
10662
+ if apply_id is not None:
10663
+ response["apply_id"] = apply_id
10208
10664
  if output_profile == "verbose":
10209
10665
  debug: JSONObject = {
10210
10666
  "legacy_result": raw_apply.get("result"),
@@ -10223,9 +10679,10 @@ class RecordTools(ToolBase):
10223
10679
  operation: str,
10224
10680
  profile: str,
10225
10681
  app_key: str,
10226
- record_id: int,
10682
+ record_id: Any | None,
10227
10683
  output_profile: str,
10228
10684
  human_review: bool,
10685
+ write_executed: bool = True,
10229
10686
  ) -> JSONObject:
10230
10687
  """执行内部辅助逻辑。"""
10231
10688
  error_payload: JSONObject = {
@@ -10266,11 +10723,14 @@ class RecordTools(ToolBase):
10266
10723
  "ws_id": None,
10267
10724
  "ok": False,
10268
10725
  "status": "failed",
10726
+ "write_executed": write_executed,
10727
+ "verification_status": "failed" if write_executed else "not_requested",
10728
+ "safe_to_retry": not write_executed,
10269
10729
  "request_route": request_route,
10270
10730
  "warnings": [],
10271
10731
  "output_profile": output_profile,
10272
10732
  "data": {
10273
- "action": {"operation": operation, "executed": True},
10733
+ "action": {"operation": operation, "executed": write_executed},
10274
10734
  "resource": {"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
10275
10735
  "verification": None,
10276
10736
  "normalized_payload": None,
@@ -18316,6 +18776,105 @@ def _write_format_for_field(field: FormField) -> JSONObject:
18316
18776
  return _write_support_payload(support_level="full", kind="scalar_text")
18317
18777
 
18318
18778
 
18779
+ def _ready_schema_format_hint(kind: str, write_format: JSONObject) -> str:
18780
+ if kind == "member":
18781
+ return "可直接填成员姓名;唯一匹配会自动解析,重名时会返回候选确认。也可传成员 id/value 对象。"
18782
+ if kind == "department":
18783
+ return "可直接填部门名称;唯一匹配会自动解析,重名时会返回候选确认。也可传部门 id/value 对象。"
18784
+ if kind == "relation":
18785
+ return "可传目标记录 apply_id/record_id 对象,也可填目标记录的可搜索文本;多候选时会返回确认。"
18786
+ if kind == "attachment":
18787
+ return "先调用 file_upload_local 上传文件,再写入上传返回的附件对象或 value/name。"
18788
+ if kind == "subtable":
18789
+ return "传 {'rows': [{...}]} 或直接传行对象数组;每行 key 使用子字段标题。"
18790
+ if kind == "address":
18791
+ return "传省/市/区/详细地址对象、地址明细字符串,或后端地址 parts 数组。"
18792
+ if kind == "single_select":
18793
+ return "传 options 中的一个选项文本。"
18794
+ if kind == "multi_select":
18795
+ return "传 options 中的多个选项文本数组。"
18796
+ if kind == "boolean":
18797
+ return "传 '是' 或 '否'。"
18798
+ if kind == "date":
18799
+ return "推荐传 'YYYY-MM-DD HH:MM:SS';只有日期时可传 'YYYY-MM-DD'。"
18800
+ if kind == "number":
18801
+ return "传数字或数字字符串。"
18802
+ if kind == "unsupported":
18803
+ reason = _normalize_optional_text(write_format.get("reason"))
18804
+ return reason or "该字段不支持直接写入。"
18805
+ return "传文本值。"
18806
+
18807
+
18808
+ def _ready_schema_example_value(
18809
+ kind: str,
18810
+ field: FormField,
18811
+ write_format: JSONObject,
18812
+ *,
18813
+ row_fields: list[JSONObject],
18814
+ ) -> JSONValue:
18815
+ if kind == "member":
18816
+ return "张三"
18817
+ if kind == "department":
18818
+ return "直销部"
18819
+ if kind == "relation":
18820
+ return {"apply_id": "5001"}
18821
+ if kind == "attachment":
18822
+ return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
18823
+ if kind == "subtable":
18824
+ row: JSONObject = {}
18825
+ for item in row_fields:
18826
+ if not isinstance(item, dict):
18827
+ continue
18828
+ title = _normalize_optional_text(item.get("title"))
18829
+ if not title:
18830
+ continue
18831
+ row[title] = item.get("example_value", _ready_schema_template_scalar(item.get("kind")))
18832
+ if not row:
18833
+ row = {"子字段": "值"}
18834
+ return {"rows": [row]}
18835
+ if kind == "address":
18836
+ examples = write_format.get("examples")
18837
+ if isinstance(examples, list) and examples:
18838
+ return deepcopy(cast(JSONValue, examples[0]))
18839
+ return {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"}
18840
+ if kind == "single_select":
18841
+ return field.options[0] if field.options else "选项A"
18842
+ if kind == "multi_select":
18843
+ return [field.options[0]] if field.options else ["选项A"]
18844
+ if kind == "boolean":
18845
+ return "是"
18846
+ if kind == "date":
18847
+ return "2026-03-13 10:00:00"
18848
+ if kind == "number":
18849
+ return 100
18850
+ if kind == "unsupported":
18851
+ return None
18852
+ return "示例文本"
18853
+
18854
+
18855
+ def _ready_schema_template_scalar(kind: Any) -> JSONValue:
18856
+ normalized = _normalize_optional_text(kind)
18857
+ if normalized == "number":
18858
+ return 100
18859
+ if normalized == "date":
18860
+ return "2026-03-13 10:00:00"
18861
+ if normalized == "boolean":
18862
+ return "是"
18863
+ if normalized == "member":
18864
+ return "张三"
18865
+ if normalized == "department":
18866
+ return "直销部"
18867
+ if normalized == "relation":
18868
+ return {"apply_id": "5001"}
18869
+ if normalized == "multi_select":
18870
+ return ["选项A"]
18871
+ if normalized == "attachment":
18872
+ return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
18873
+ if normalized == "address":
18874
+ return {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"}
18875
+ return "值"
18876
+
18877
+
18319
18878
  def _summarize_write_support(resolved_fields: list[JSONObject]) -> JSONObject:
18320
18879
  summary: JSONObject = {
18321
18880
  "full": [],