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

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.
@@ -470,21 +470,22 @@ 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
- fields: JSONObject | None = None,
481
+ items: list[JSONObject] | None = None,
481
482
  verify_write: bool = True,
482
483
  output_profile: str = "normal",
483
484
  ) -> JSONObject:
484
485
  return self.record_insert_public(
485
486
  profile=DEFAULT_PROFILE,
486
487
  app_key=app_key,
487
- fields=fields or {},
488
+ items=items,
488
489
  verify_write=verify_write,
489
490
  output_profile=output_profile,
490
491
  )
@@ -1064,11 +1065,16 @@ class RecordTools(ToolBase):
1064
1065
  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
1066
  else:
1066
1067
  required = bool(required_override) if required_override is not None else bool(field.required)
1068
+ write_format = _write_format_for_field(field)
1067
1069
  payload: JSONObject = {
1068
1070
  "title": field.que_title,
1069
1071
  "kind": kind,
1070
1072
  "required": required,
1073
+ "format_hint": _ready_schema_format_hint(kind, write_format),
1071
1074
  }
1075
+ example_value = _ready_schema_example_value(kind, field, write_format, row_fields=row_fields)
1076
+ if example_value is not None:
1077
+ payload["example_value"] = example_value
1072
1078
  if include_field_id:
1073
1079
  payload["field_id"] = field.que_id
1074
1080
  if kind in {"single_select", "multi_select"} and field.options:
@@ -2995,6 +3001,7 @@ class RecordTools(ToolBase):
2995
3001
  profile: str = DEFAULT_PROFILE,
2996
3002
  app_key: str,
2997
3003
  fields: JSONObject | None = None,
3004
+ items: list[JSONObject] | None = None,
2998
3005
  verify_write: bool = True,
2999
3006
  output_profile: str = "normal",
3000
3007
  ) -> JSONObject:
@@ -3002,68 +3009,477 @@ class RecordTools(ToolBase):
3002
3009
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
3003
3010
  if not app_key:
3004
3011
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
3012
+ if items is not None:
3013
+ normalized_items = self._normalize_public_record_insert_batch_items(fields=fields, items=items)
3014
+ return self._record_insert_public_batch(
3015
+ profile=profile,
3016
+ app_key=app_key,
3017
+ items=normalized_items,
3018
+ verify_write=verify_write,
3019
+ output_profile=normalized_output_profile,
3020
+ )
3005
3021
  if fields is not None and not isinstance(fields, dict):
3006
3022
  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(
3023
+ return self._record_insert_public_single(
3009
3024
  profile=profile,
3010
- operation="create",
3011
3025
  app_key=app_key,
3012
- apply_id=None,
3013
- answers=[],
3014
3026
  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),
3027
+ verify_write=verify_write,
3028
+ output_profile=normalized_output_profile,
3029
+ capture_exceptions=False,
3030
3030
  )
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
- )
3031
+
3032
+ def _record_insert_public_single(
3033
+ self,
3034
+ *,
3035
+ profile: str,
3036
+ app_key: str,
3037
+ fields: JSONObject,
3038
+ verify_write: bool,
3039
+ output_profile: str,
3040
+ capture_exceptions: bool,
3041
+ ) -> JSONObject:
3042
+ """执行内部辅助逻辑。"""
3043
+ submit_type_value = self._normalize_record_write_submit_type("submit")
3044
+ write_attempted = False
3040
3045
  try:
3041
- raw_apply = self.record_create(
3046
+ raw_preflight = self._preflight_record_write(
3042
3047
  profile=profile,
3048
+ operation="create",
3043
3049
  app_key=app_key,
3044
- answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3045
- fields={},
3050
+ apply_id=None,
3051
+ answers=[],
3052
+ fields=fields,
3053
+ force_refresh_form=False,
3054
+ view_id=None,
3055
+ list_type=None,
3056
+ view_key=None,
3057
+ view_name=None,
3058
+ )
3059
+ preflight_used_force_refresh = self._record_preflight_used_force_refresh(raw_preflight)
3060
+ preflight_data = cast(JSONObject, raw_preflight.get("data", {}))
3061
+ normalized_payload: JSONObject = self._record_write_normalized_payload(
3062
+ operation="insert",
3063
+ record_id=None,
3064
+ record_ids=[],
3065
+ normalized_answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3046
3066
  submit_type=submit_type_value,
3047
- verify_write=verify_write,
3048
- force_refresh_form=preflight_used_force_refresh,
3067
+ selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3049
3068
  )
3050
- except QingflowApiError as exc:
3051
- self._raise_record_write_permission_error(
3069
+ if preflight_data.get("blockers"):
3070
+ return self._record_write_blocked_response(
3071
+ raw_preflight,
3072
+ operation="insert",
3073
+ normalized_payload=normalized_payload,
3074
+ output_profile=output_profile,
3075
+ human_review=False,
3076
+ target_resource={"type": "record", "app_key": app_key, "record_id": None, "record_ids": []},
3077
+ )
3078
+ try:
3079
+ write_attempted = True
3080
+ raw_apply = self.record_create(
3081
+ profile=profile,
3082
+ app_key=app_key,
3083
+ answers=cast(list[JSONObject], preflight_data.get("normalized_answers", [])),
3084
+ fields={},
3085
+ submit_type=submit_type_value,
3086
+ verify_write=verify_write,
3087
+ force_refresh_form=preflight_used_force_refresh,
3088
+ )
3089
+ except QingflowApiError as exc:
3090
+ self._raise_record_write_permission_error(
3091
+ exc,
3092
+ operation="insert",
3093
+ app_key=app_key,
3094
+ record_id=None,
3095
+ selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3096
+ )
3097
+ raise
3098
+ return self._record_write_apply_response(
3099
+ raw_apply,
3100
+ operation="insert",
3101
+ normalized_payload=normalized_payload,
3102
+ output_profile=output_profile,
3103
+ human_review=False,
3104
+ preflight=raw_preflight,
3105
+ )
3106
+ except (QingflowApiError, RuntimeError) as exc:
3107
+ if not capture_exceptions:
3108
+ raise
3109
+ return self._record_write_exception_response(
3052
3110
  exc,
3053
3111
  operation="insert",
3112
+ profile=profile,
3054
3113
  app_key=app_key,
3055
3114
  record_id=None,
3056
- selection=cast(JSONObject | None, preflight_data.get("selection") if isinstance(preflight_data.get("selection"), dict) else None),
3115
+ output_profile=output_profile,
3116
+ human_review=False,
3117
+ write_executed=write_attempted,
3057
3118
  )
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,
3119
+
3120
+ def _normalize_public_record_insert_batch_items(
3121
+ self,
3122
+ *,
3123
+ fields: JSONObject | None,
3124
+ items: list[JSONObject] | None,
3125
+ ) -> list[JSONObject]:
3126
+ """执行内部辅助逻辑。"""
3127
+ if fields is not None:
3128
+ raise_tool_error(QingflowApiError.config_error("record_insert batch mode does not accept fields"))
3129
+ if not isinstance(items, list) or not items:
3130
+ raise_tool_error(QingflowApiError.config_error("items must be a non-empty list"))
3131
+ normalized_items: list[JSONObject] = []
3132
+ for index, item in enumerate(items):
3133
+ if not isinstance(item, dict):
3134
+ raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
3135
+ item_fields = item.get("fields")
3136
+ if not isinstance(item_fields, dict):
3137
+ raise_tool_error(QingflowApiError.config_error(f"items[{index}].fields must be an object map keyed by field title"))
3138
+ normalized_items.append({"fields": cast(JSONObject, item_fields)})
3139
+ return normalized_items
3140
+
3141
+ def _record_insert_public_batch(
3142
+ self,
3143
+ *,
3144
+ profile: str,
3145
+ app_key: str,
3146
+ items: list[JSONObject],
3147
+ verify_write: bool,
3148
+ output_profile: str,
3149
+ ) -> JSONObject:
3150
+ """执行内部辅助逻辑。"""
3151
+ responses: list[JSONObject] = []
3152
+ for item in items:
3153
+ responses.append(
3154
+ self._record_insert_public_single(
3155
+ profile=profile,
3156
+ app_key=app_key,
3157
+ fields=cast(JSONObject, item["fields"]),
3158
+ verify_write=verify_write,
3159
+ output_profile=output_profile,
3160
+ capture_exceptions=True,
3161
+ )
3162
+ )
3163
+ return self._record_insert_batch_response(
3164
+ profile=profile,
3165
+ app_key=app_key,
3166
+ responses=responses,
3167
+ output_profile=output_profile,
3168
+ )
3169
+
3170
+ def _record_insert_batch_response(
3171
+ self,
3172
+ *,
3173
+ profile: str,
3174
+ app_key: str,
3175
+ responses: list[JSONObject],
3176
+ output_profile: str,
3177
+ ) -> JSONObject:
3178
+ """执行内部辅助逻辑。"""
3179
+ items = [
3180
+ self._record_insert_batch_item_from_response(index=index, response=response, output_profile=output_profile)
3181
+ for index, response in enumerate(responses)
3182
+ ]
3183
+ summary = self._record_insert_batch_summary(items)
3184
+ status, ok, message = self._record_insert_batch_envelope_status(summary=summary)
3185
+ first_response = responses[0] if responses else {}
3186
+ created_record_ids = [
3187
+ cast(str, item["record_id"])
3188
+ for item in items
3189
+ if isinstance(item.get("record_id"), str) and item.get("record_id")
3190
+ ]
3191
+ write_executed = any(bool(item.get("write_executed")) for item in items)
3192
+ verification_status = self._record_insert_batch_verification_status(items)
3193
+ return {
3194
+ "profile": first_response.get("profile", profile),
3195
+ "ws_id": first_response.get("ws_id"),
3196
+ "ok": ok,
3197
+ "status": status,
3198
+ "mode": "batch",
3199
+ "total": summary["total"],
3200
+ "succeeded": summary["succeeded"],
3201
+ "failed": summary["failed"],
3202
+ "created_record_ids": created_record_ids,
3203
+ "write_executed": write_executed,
3204
+ "verification_status": verification_status,
3205
+ "safe_to_retry": not write_executed,
3206
+ "request_route": first_response.get("request_route"),
3207
+ "warnings": [],
3208
+ "output_profile": output_profile,
3209
+ "items": items,
3210
+ "data": {
3211
+ "app_key": app_key,
3212
+ "mode": "batch",
3213
+ "summary": summary,
3214
+ "created_record_ids": created_record_ids,
3215
+ "items": items,
3216
+ },
3217
+ "message": message,
3218
+ }
3219
+
3220
+ def _record_insert_batch_summary(self, items: list[JSONObject]) -> JSONObject:
3221
+ """执行内部辅助逻辑。"""
3222
+ created = [item for item in items if isinstance(item.get("record_id"), str) and item.get("record_id")]
3223
+ failed = [item for item in items if item.get("status") not in {"success", "verification_failed"}]
3224
+ return {
3225
+ "total": len(items),
3226
+ "succeeded": len(created),
3227
+ "failed": len(failed),
3228
+ "created_count": len(created),
3229
+ "blocked_count": sum(1 for item in items if item.get("status") == "blocked"),
3230
+ "confirmation_count": sum(1 for item in items if item.get("status") == "needs_confirmation"),
3231
+ "verification_failed_count": sum(1 for item in items if item.get("status") == "verification_failed"),
3232
+ }
3233
+
3234
+ def _record_insert_batch_envelope_status(self, *, summary: JSONObject) -> tuple[str, bool, str]:
3235
+ """执行内部辅助逻辑。"""
3236
+ succeeded = int(summary["succeeded"])
3237
+ failed = int(summary["failed"])
3238
+ if succeeded and failed:
3239
+ return "partial_success", False, "batch insert completed with partial failures"
3240
+ if succeeded and int(summary["verification_failed_count"]):
3241
+ return "verification_failed", True, "batch insert completed but verification failed for some created records"
3242
+ if succeeded:
3243
+ return "success", True, "batch insert completed"
3244
+ if int(summary["confirmation_count"]):
3245
+ return "needs_confirmation", False, "batch insert requires confirmation before retrying failed rows"
3246
+ if int(summary["blocked_count"]):
3247
+ return "blocked", False, "batch insert preflight blocked all rows"
3248
+ return "failed", False, "batch insert failed"
3249
+
3250
+ def _record_insert_batch_verification_status(self, items: list[JSONObject]) -> str:
3251
+ """执行内部辅助逻辑。"""
3252
+ statuses = {str(item.get("verification_status") or "not_requested") for item in items}
3253
+ if "failed" in statuses:
3254
+ return "failed"
3255
+ if "verified" in statuses:
3256
+ return "verified"
3257
+ return "not_requested"
3258
+
3259
+ def _record_insert_batch_item_from_response(
3260
+ self,
3261
+ *,
3262
+ index: int,
3263
+ response: JSONObject,
3264
+ output_profile: str,
3265
+ ) -> JSONObject:
3266
+ """执行内部辅助逻辑。"""
3267
+ data = cast(JSONObject, response.get("data", {})) if isinstance(response.get("data"), dict) else {}
3268
+ resource = _public_record_resource(data.get("resource"))
3269
+ record_id = _public_record_id_text(response.get("record_id"))
3270
+ apply_id = _public_record_id_text(response.get("apply_id"))
3271
+ if record_id is None and isinstance(resource, dict):
3272
+ record_id = _public_record_id_text(resource.get("record_id"))
3273
+ if apply_id is None and isinstance(resource, dict):
3274
+ apply_id = _public_record_id_text(resource.get("apply_id"))
3275
+ item: JSONObject = {
3276
+ "index": index,
3277
+ "row_number": index + 1,
3278
+ "status": response.get("status"),
3279
+ "write_executed": bool(response.get("write_executed")),
3280
+ "verification_status": response.get("verification_status", "not_requested"),
3281
+ "safe_to_retry": bool(response.get("safe_to_retry", True)),
3282
+ }
3283
+ if record_id is not None:
3284
+ item["record_id"] = record_id
3285
+ if apply_id is not None:
3286
+ item["apply_id"] = apply_id
3287
+ if resource:
3288
+ item["resource"] = resource
3289
+ verification = data.get("verification")
3290
+ if isinstance(verification, dict):
3291
+ compact_verification = {
3292
+ key: verification[key]
3293
+ for key in ("verified", "verification_mode", "field_level_verified")
3294
+ if key in verification
3295
+ }
3296
+ if compact_verification:
3297
+ item["verification"] = compact_verification
3298
+ field_errors = cast(list[JSONObject], data.get("field_errors", [])) if isinstance(data.get("field_errors"), list) else []
3299
+ confirmation_requests = cast(list[JSONObject], data.get("confirmation_requests", [])) if isinstance(data.get("confirmation_requests"), list) else []
3300
+ failed_fields = self._record_write_failed_fields(field_errors=field_errors, confirmation_requests=confirmation_requests)
3301
+ if failed_fields:
3302
+ item["failed_fields"] = failed_fields
3303
+ if confirmation_requests:
3304
+ item["confirmation_requests"] = [
3305
+ self._record_write_semantic_confirmation_request(request)
3306
+ for request in confirmation_requests
3307
+ if isinstance(request, dict)
3308
+ ]
3309
+ blockers = data.get("blockers")
3310
+ if isinstance(blockers, list) and blockers:
3311
+ item["blockers"] = blockers
3312
+ warnings = response.get("warnings")
3313
+ if isinstance(warnings, list) and warnings:
3314
+ item["warnings"] = warnings
3315
+ error = data.get("error")
3316
+ if isinstance(error, dict):
3317
+ item["error"] = error
3318
+ if output_profile == "verbose" and isinstance(data.get("debug"), dict):
3319
+ item["debug"] = data.get("debug")
3320
+ return item
3321
+
3322
+ def _record_write_failed_fields(
3323
+ self,
3324
+ *,
3325
+ field_errors: list[JSONObject],
3326
+ confirmation_requests: list[JSONObject],
3327
+ ) -> list[JSONObject]:
3328
+ """执行内部辅助逻辑。"""
3329
+ failed_fields = [
3330
+ self._record_write_semantic_field_error(error)
3331
+ for error in field_errors
3332
+ if isinstance(error, dict)
3333
+ ]
3334
+ failed_fields.extend(
3335
+ self._record_write_failed_field_from_confirmation(request)
3336
+ for request in confirmation_requests
3337
+ if isinstance(request, dict)
3066
3338
  )
3339
+ return failed_fields
3340
+
3341
+ def _record_write_semantic_field_error(self, error: JSONObject) -> JSONObject:
3342
+ """执行内部辅助逻辑。"""
3343
+ field = error.get("field")
3344
+ field_payload = cast(JSONObject, field) if isinstance(field, dict) else {}
3345
+ error_code = _normalize_optional_text(error.get("error_code")) or "INVALID_FIELD_VALUE"
3346
+ title = (
3347
+ _normalize_optional_text(field_payload.get("que_title"))
3348
+ or _normalize_optional_text(field_payload.get("title"))
3349
+ or _normalize_optional_text(error.get("location"))
3350
+ or "unknown field"
3351
+ )
3352
+ field_id = (
3353
+ field_payload.get("que_id")
3354
+ if field_payload.get("que_id") is not None
3355
+ else field_payload.get("field_id")
3356
+ )
3357
+ expected_format = error.get("expected_format") if isinstance(error.get("expected_format"), dict) else None
3358
+ if expected_format is None:
3359
+ expected_format = self._record_write_expected_format_from_field_payload(field_payload)
3360
+ payload: JSONObject = {
3361
+ "title": title,
3362
+ "field_id": field_id,
3363
+ "error_code": error_code,
3364
+ "message": self._record_write_semantic_error_message(error_code, error.get("message")),
3365
+ "next_action": self._record_write_next_action_for_error(error_code),
3366
+ }
3367
+ if expected_format is not None:
3368
+ payload["expected_format"] = expected_format
3369
+ payload["example_value"] = self._record_write_example_value_for_format(expected_format, field_payload)
3370
+ if error.get("received_value") is not None:
3371
+ payload["received_value"] = error.get("received_value")
3372
+ if error.get("fix_hint") is not None:
3373
+ payload["fix_hint"] = error.get("fix_hint")
3374
+ if error.get("details") is not None:
3375
+ payload["details"] = error.get("details")
3376
+ return payload
3377
+
3378
+ def _record_write_semantic_confirmation_request(self, request: JSONObject) -> JSONObject:
3379
+ """执行内部辅助逻辑。"""
3380
+ field_ref = request.get("field_ref")
3381
+ field_payload = cast(JSONObject, field_ref) if isinstance(field_ref, dict) else {}
3382
+ payload: JSONObject = {
3383
+ "field": request.get("field"),
3384
+ "title": _normalize_optional_text(request.get("field")) or _normalize_optional_text(field_payload.get("que_title")),
3385
+ "field_id": field_payload.get("que_id"),
3386
+ "kind": request.get("kind"),
3387
+ "input": request.get("input"),
3388
+ "candidates": request.get("candidates", []),
3389
+ "next_action": "让用户确认候选,或用显式 id/object 只重试本行。",
3390
+ }
3391
+ if request.get("parent_field") is not None:
3392
+ payload["parent_field"] = request.get("parent_field")
3393
+ if request.get("row_ordinal") is not None:
3394
+ payload["row_ordinal"] = request.get("row_ordinal")
3395
+ return payload
3396
+
3397
+ def _record_write_failed_field_from_confirmation(self, request: JSONObject) -> JSONObject:
3398
+ """执行内部辅助逻辑。"""
3399
+ semantic = self._record_write_semantic_confirmation_request(request)
3400
+ return {
3401
+ "title": semantic.get("title") or semantic.get("field"),
3402
+ "field_id": semantic.get("field_id"),
3403
+ "error_code": "LOOKUP_NEEDS_CONFIRMATION",
3404
+ "message": "候选不唯一,需要用户确认。",
3405
+ "kind": semantic.get("kind"),
3406
+ "input": semantic.get("input"),
3407
+ "candidates": semantic.get("candidates", []),
3408
+ "next_action": semantic.get("next_action"),
3409
+ }
3410
+
3411
+ def _record_write_expected_format_from_field_payload(self, field_payload: JSONObject) -> JSONObject | None:
3412
+ """执行内部辅助逻辑。"""
3413
+ que_type = _coerce_count(field_payload.get("que_type"))
3414
+ if que_type is None:
3415
+ return None
3416
+ synthetic_field = FormField(
3417
+ que_id=_coerce_count(field_payload.get("que_id")) or 0,
3418
+ que_title=_normalize_optional_text(field_payload.get("que_title")) or _normalize_optional_text(field_payload.get("title")) or "",
3419
+ que_type=que_type,
3420
+ required=False,
3421
+ readonly=False,
3422
+ system=False,
3423
+ options=[],
3424
+ aliases=[],
3425
+ target_app_key=None,
3426
+ target_app_name_hint=None,
3427
+ member_select_scope_type=None,
3428
+ member_select_scope=None,
3429
+ dept_select_scope_type=None,
3430
+ dept_select_scope=None,
3431
+ raw={},
3432
+ )
3433
+ return _write_format_for_field(synthetic_field)
3434
+
3435
+ def _record_write_example_value_for_format(self, expected_format: JSONObject, field_payload: JSONObject) -> JSONValue:
3436
+ """执行内部辅助逻辑。"""
3437
+ examples = expected_format.get("examples")
3438
+ if isinstance(examples, list) and examples:
3439
+ return cast(JSONValue, examples[0])
3440
+ kind = _normalize_optional_text(expected_format.get("kind"))
3441
+ if kind == "member_list":
3442
+ return "张三"
3443
+ if kind == "department_list":
3444
+ return "直销部"
3445
+ if kind == "relation_record":
3446
+ return {"apply_id": "5001"}
3447
+ if kind == "attachment_list":
3448
+ return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
3449
+ if kind == "subtable_rows":
3450
+ return {"rows": [{"子字段": "值"}]}
3451
+ if kind == "date_string":
3452
+ return "2026-03-13 10:00:00"
3453
+ if kind == "boolean_label":
3454
+ return "是"
3455
+ if kind in {"single_select", "multi_select"}:
3456
+ options = expected_format.get("options")
3457
+ if isinstance(options, list) and options:
3458
+ return cast(JSONValue, options[0])
3459
+ que_type = _coerce_count(field_payload.get("que_type"))
3460
+ if que_type in NUMBER_QUE_TYPES:
3461
+ return 100
3462
+ return "文本"
3463
+
3464
+ def _record_write_semantic_error_message(self, error_code: str, fallback: JSONValue) -> str:
3465
+ """执行内部辅助逻辑。"""
3466
+ if error_code == "MISSING_REQUIRED_FIELD":
3467
+ return "缺少必填字段。"
3468
+ if error_code == "FIELD_NOT_FOUND":
3469
+ return "字段不存在或字段标题不匹配。"
3470
+ if error_code == "AMBIGUOUS_FIELD":
3471
+ return "字段标题存在歧义。"
3472
+ if error_code in {"INVALID_FIELD_VALUE", "INVALID_MEMBER_VALUE", "INVALID_DEPARTMENT_VALUE", "INVALID_RELATION_VALUE"}:
3473
+ return _normalize_optional_text(fallback) or "字段值格式不正确。"
3474
+ return _normalize_optional_text(fallback) or "字段写入失败。"
3475
+
3476
+ def _record_write_next_action_for_error(self, error_code: str) -> str:
3477
+ """执行内部辅助逻辑。"""
3478
+ if error_code == "MISSING_REQUIRED_FIELD":
3479
+ return "补充该字段后只重试本行。"
3480
+ if error_code in {"FIELD_NOT_FOUND", "AMBIGUOUS_FIELD"}:
3481
+ return "重新调用 schema 工具确认字段标题或 field_id 后,只重试本行。"
3482
+ return "修正该字段值后只重试本行。"
3067
3483
 
3068
3484
  @tool_cn_name("更新记录")
3069
3485
  def record_update_public(
@@ -7357,7 +7773,7 @@ class RecordTools(ToolBase):
7357
7773
  "result": result,
7358
7774
  "normalized_answers": normalized_answers,
7359
7775
  "status": "completed" if verified else "verification_failed",
7360
- "ok": verified,
7776
+ "ok": True,
7361
7777
  "apply_id": apply_id,
7362
7778
  "record_id": apply_id,
7363
7779
  "verify_write": verify_write,
@@ -7561,7 +7977,7 @@ class RecordTools(ToolBase):
7561
7977
  "result": result,
7562
7978
  "normalized_answers": normalized_answers,
7563
7979
  "status": "completed" if verified else "verification_failed",
7564
- "ok": verified,
7980
+ "ok": True,
7565
7981
  "verify_write": verify_write,
7566
7982
  "write_verified": verified if verify_write else None,
7567
7983
  "verification": verification,
@@ -10100,6 +10516,9 @@ class RecordTools(ToolBase):
10100
10516
  "ws_id": raw_preflight.get("ws_id"),
10101
10517
  "ok": False,
10102
10518
  "status": status,
10519
+ "write_executed": False,
10520
+ "verification_status": "not_requested",
10521
+ "safe_to_retry": True,
10103
10522
  "request_route": raw_preflight.get("request_route"),
10104
10523
  "warnings": warnings,
10105
10524
  "output_profile": output_profile,
@@ -10143,6 +10562,9 @@ class RecordTools(ToolBase):
10143
10562
  "ws_id": raw_preflight.get("ws_id"),
10144
10563
  "ok": True,
10145
10564
  "status": "ready",
10565
+ "write_executed": False,
10566
+ "verification_status": "not_requested",
10567
+ "safe_to_retry": True,
10146
10568
  "request_route": raw_preflight.get("request_route"),
10147
10569
  "warnings": warnings,
10148
10570
  "output_profile": output_profile,
@@ -10185,17 +10607,45 @@ class RecordTools(ToolBase):
10185
10607
  resolved_fields = cast(list[JSONObject], preflight_data.get("lookup_resolved_fields", []))
10186
10608
  if isinstance(verification_warnings, list):
10187
10609
  warnings.extend(cast(list[JSONObject], [item for item in verification_warnings if isinstance(item, dict)]))
10610
+ resource = _public_record_resource(raw_apply.get("resource"))
10611
+ record_id = _public_record_id_text(resource.get("record_id")) if isinstance(resource, dict) else None
10612
+ apply_id = _public_record_id_text(resource.get("apply_id")) if isinstance(resource, dict) else None
10613
+ if record_id is None:
10614
+ record_id = _public_record_id_text(raw_apply.get("record_id"))
10615
+ if apply_id is None:
10616
+ apply_id = _public_record_id_text(raw_apply.get("apply_id"))
10617
+ if apply_id is None:
10618
+ apply_id = record_id
10619
+ if record_id is None:
10620
+ record_id = apply_id
10621
+ write_executed = True
10622
+ verification_requested = (
10623
+ raw_apply.get("verify_write") is True
10624
+ or raw_apply.get("write_verified") is not None
10625
+ or isinstance(raw_apply.get("verification"), dict)
10626
+ )
10627
+ if verification_requested:
10628
+ verification_status = "verified" if bool(verification.get("verified")) else "failed"
10629
+ else:
10630
+ verification_status = "not_requested"
10631
+ raw_status = _normalize_optional_text(raw_apply.get("status"))
10632
+ response_status = "verification_failed" if verification_status == "failed" else "success"
10633
+ if not bool(raw_apply.get("ok", True)) and verification_status != "failed":
10634
+ response_status = raw_status or "failed"
10188
10635
  response: JSONObject = {
10189
10636
  "profile": raw_apply.get("profile"),
10190
10637
  "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",
10638
+ "ok": True if verification_status == "failed" and write_executed else bool(raw_apply.get("ok", True)),
10639
+ "status": response_status,
10640
+ "write_executed": write_executed,
10641
+ "verification_status": verification_status,
10642
+ "safe_to_retry": False,
10193
10643
  "request_route": raw_apply.get("request_route"),
10194
10644
  "warnings": warnings,
10195
10645
  "output_profile": output_profile,
10196
10646
  "data": {
10197
10647
  "action": {"operation": operation, "executed": True},
10198
- "resource": _public_record_resource(raw_apply.get("resource")),
10648
+ "resource": resource,
10199
10649
  "verification": raw_apply.get("verification"),
10200
10650
  "normalized_payload": normalized_payload,
10201
10651
  "blockers": [],
@@ -10205,6 +10655,10 @@ class RecordTools(ToolBase):
10205
10655
  "human_review": self._record_write_human_review_payload(operation, enabled=human_review),
10206
10656
  },
10207
10657
  }
10658
+ if record_id is not None:
10659
+ response["record_id"] = record_id
10660
+ if apply_id is not None:
10661
+ response["apply_id"] = apply_id
10208
10662
  if output_profile == "verbose":
10209
10663
  debug: JSONObject = {
10210
10664
  "legacy_result": raw_apply.get("result"),
@@ -10223,9 +10677,10 @@ class RecordTools(ToolBase):
10223
10677
  operation: str,
10224
10678
  profile: str,
10225
10679
  app_key: str,
10226
- record_id: int,
10680
+ record_id: Any | None,
10227
10681
  output_profile: str,
10228
10682
  human_review: bool,
10683
+ write_executed: bool = True,
10229
10684
  ) -> JSONObject:
10230
10685
  """执行内部辅助逻辑。"""
10231
10686
  error_payload: JSONObject = {
@@ -10266,11 +10721,14 @@ class RecordTools(ToolBase):
10266
10721
  "ws_id": None,
10267
10722
  "ok": False,
10268
10723
  "status": "failed",
10724
+ "write_executed": write_executed,
10725
+ "verification_status": "failed" if write_executed else "not_requested",
10726
+ "safe_to_retry": not write_executed,
10269
10727
  "request_route": request_route,
10270
10728
  "warnings": [],
10271
10729
  "output_profile": output_profile,
10272
10730
  "data": {
10273
- "action": {"operation": operation, "executed": True},
10731
+ "action": {"operation": operation, "executed": write_executed},
10274
10732
  "resource": {"type": "record", "app_key": app_key, "record_id": record_id, "record_ids": []},
10275
10733
  "verification": None,
10276
10734
  "normalized_payload": None,
@@ -13658,11 +14116,15 @@ def _record_detail_associated_resource(raw: JSONObject) -> JSONObject:
13658
14116
  view_key = _normalize_optional_text(raw.get("viewKey", raw.get("viewgraphKey")))
13659
14117
  chart_key = _normalize_optional_text(raw.get("chartKey", raw.get("chartId")))
13660
14118
  is_view = bool(view_key) or graph_type.endswith("view") or graph_type == "view"
14119
+ if is_view and not view_key and chart_key:
14120
+ view_key = chart_key
14121
+ chart_key = None
13661
14122
  resource_type = "view" if is_view else "report"
13662
14123
  view_type = _normalize_optional_text(raw.get("viewType", raw.get("viewgraphType", raw.get("graphType"))))
13663
14124
  data_access = _record_detail_resource_data_access(resource_type=resource_type, view_type=view_type)
13664
14125
  return {
13665
14126
  "type": resource_type,
14127
+ "resource_type": resource_type,
13666
14128
  "name": _normalize_optional_text(raw.get("viewName", raw.get("chartName", raw.get("name", raw.get("title"))))),
13667
14129
  "app_key": _normalize_optional_text(raw.get("appKey", raw.get("targetAppKey"))),
13668
14130
  "app_name": _normalize_optional_text(raw.get("formTitle", raw.get("appName", raw.get("targetAppName")))),
@@ -13670,10 +14132,15 @@ def _record_detail_associated_resource(raw: JSONObject) -> JSONObject:
13670
14132
  "chart_key": chart_key,
13671
14133
  "view_type": view_type,
13672
14134
  "graph_type": raw.get("graphType"),
14135
+ "report_source": _record_detail_report_source(raw.get("sourceType")) if resource_type == "report" else None,
13673
14136
  "data_access": data_access,
13674
14137
  }
13675
14138
 
13676
14139
 
14140
+ def _record_detail_report_source(source_type: Any) -> str:
14141
+ return "dataset" if str(source_type or "").strip().upper() == "BI_DATASET" else "app"
14142
+
14143
+
13677
14144
  def _record_detail_resource_data_access(*, resource_type: str, view_type: str | None) -> JSONObject:
13678
14145
  if resource_type == "report":
13679
14146
  return {
@@ -18316,6 +18783,105 @@ def _write_format_for_field(field: FormField) -> JSONObject:
18316
18783
  return _write_support_payload(support_level="full", kind="scalar_text")
18317
18784
 
18318
18785
 
18786
+ def _ready_schema_format_hint(kind: str, write_format: JSONObject) -> str:
18787
+ if kind == "member":
18788
+ return "可直接填成员姓名;唯一匹配会自动解析,重名时会返回候选确认。也可传成员 id/value 对象。"
18789
+ if kind == "department":
18790
+ return "可直接填部门名称;唯一匹配会自动解析,重名时会返回候选确认。也可传部门 id/value 对象。"
18791
+ if kind == "relation":
18792
+ return "可传目标记录 apply_id/record_id 对象,也可填目标记录的可搜索文本;多候选时会返回确认。"
18793
+ if kind == "attachment":
18794
+ return "先调用 file_upload_local 上传文件,再写入上传返回的附件对象或 value/name。"
18795
+ if kind == "subtable":
18796
+ return "传 {'rows': [{...}]} 或直接传行对象数组;每行 key 使用子字段标题。"
18797
+ if kind == "address":
18798
+ return "传省/市/区/详细地址对象、地址明细字符串,或后端地址 parts 数组。"
18799
+ if kind == "single_select":
18800
+ return "传 options 中的一个选项文本。"
18801
+ if kind == "multi_select":
18802
+ return "传 options 中的多个选项文本数组。"
18803
+ if kind == "boolean":
18804
+ return "传 '是' 或 '否'。"
18805
+ if kind == "date":
18806
+ return "推荐传 'YYYY-MM-DD HH:MM:SS';只有日期时可传 'YYYY-MM-DD'。"
18807
+ if kind == "number":
18808
+ return "传数字或数字字符串。"
18809
+ if kind == "unsupported":
18810
+ reason = _normalize_optional_text(write_format.get("reason"))
18811
+ return reason or "该字段不支持直接写入。"
18812
+ return "传文本值。"
18813
+
18814
+
18815
+ def _ready_schema_example_value(
18816
+ kind: str,
18817
+ field: FormField,
18818
+ write_format: JSONObject,
18819
+ *,
18820
+ row_fields: list[JSONObject],
18821
+ ) -> JSONValue:
18822
+ if kind == "member":
18823
+ return "张三"
18824
+ if kind == "department":
18825
+ return "直销部"
18826
+ if kind == "relation":
18827
+ return {"apply_id": "5001"}
18828
+ if kind == "attachment":
18829
+ return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
18830
+ if kind == "subtable":
18831
+ row: JSONObject = {}
18832
+ for item in row_fields:
18833
+ if not isinstance(item, dict):
18834
+ continue
18835
+ title = _normalize_optional_text(item.get("title"))
18836
+ if not title:
18837
+ continue
18838
+ row[title] = item.get("example_value", _ready_schema_template_scalar(item.get("kind")))
18839
+ if not row:
18840
+ row = {"子字段": "值"}
18841
+ return {"rows": [row]}
18842
+ if kind == "address":
18843
+ examples = write_format.get("examples")
18844
+ if isinstance(examples, list) and examples:
18845
+ return deepcopy(cast(JSONValue, examples[0]))
18846
+ return {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"}
18847
+ if kind == "single_select":
18848
+ return field.options[0] if field.options else "选项A"
18849
+ if kind == "multi_select":
18850
+ return [field.options[0]] if field.options else ["选项A"]
18851
+ if kind == "boolean":
18852
+ return "是"
18853
+ if kind == "date":
18854
+ return "2026-03-13 10:00:00"
18855
+ if kind == "number":
18856
+ return 100
18857
+ if kind == "unsupported":
18858
+ return None
18859
+ return "示例文本"
18860
+
18861
+
18862
+ def _ready_schema_template_scalar(kind: Any) -> JSONValue:
18863
+ normalized = _normalize_optional_text(kind)
18864
+ if normalized == "number":
18865
+ return 100
18866
+ if normalized == "date":
18867
+ return "2026-03-13 10:00:00"
18868
+ if normalized == "boolean":
18869
+ return "是"
18870
+ if normalized == "member":
18871
+ return "张三"
18872
+ if normalized == "department":
18873
+ return "直销部"
18874
+ if normalized == "relation":
18875
+ return {"apply_id": "5001"}
18876
+ if normalized == "multi_select":
18877
+ return ["选项A"]
18878
+ if normalized == "attachment":
18879
+ return {"value": "<file_upload_local 返回的 value/url>", "name": "example.pdf"}
18880
+ if normalized == "address":
18881
+ return {"province": "上海市", "city": "上海市", "district": "闵行区", "detail": "浦江路99号"}
18882
+ return "值"
18883
+
18884
+
18319
18885
  def _summarize_write_support(resolved_fields: list[JSONObject]) -> JSONObject:
18320
18886
  summary: JSONObject = {
18321
18887
  "full": [],