@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.3

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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-user/SKILL.md +21 -12
  7. package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
  8. package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
  9. package/skills/qingflow-app-user/references/record-patterns.md +1 -1
  10. package/skills/qingflow-record-analysis/SKILL.md +44 -2
  11. package/skills/qingflow-record-insert/SKILL.md +3 -0
  12. package/skills/qingflow-record-update/SKILL.md +3 -0
  13. package/skills/qingflow-task-ops/SKILL.md +31 -10
  14. package/src/qingflow_mcp/__init__.py +33 -1
  15. package/src/qingflow_mcp/builder_facade/models.py +14 -4
  16. package/src/qingflow_mcp/builder_facade/service.py +1582 -124
  17. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  18. package/src/qingflow_mcp/cli/commands/builder.py +4 -3
  19. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  20. package/src/qingflow_mcp/cli/commands/task.py +74 -22
  21. package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
  22. package/src/qingflow_mcp/cli/formatters.py +287 -48
  23. package/src/qingflow_mcp/cli/main.py +6 -1
  24. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  25. package/src/qingflow_mcp/config.py +1 -1
  26. package/src/qingflow_mcp/errors.py +2 -2
  27. package/src/qingflow_mcp/id_utils.py +49 -0
  28. package/src/qingflow_mcp/public_surface.py +11 -1
  29. package/src/qingflow_mcp/response_trim.py +380 -9
  30. package/src/qingflow_mcp/server.py +4 -0
  31. package/src/qingflow_mcp/server_app_builder.py +11 -1
  32. package/src/qingflow_mcp/server_app_user.py +24 -0
  33. package/src/qingflow_mcp/session_store.py +69 -15
  34. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  35. package/src/qingflow_mcp/solution/executor.py +2 -2
  36. package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
  37. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  38. package/src/qingflow_mcp/tools/auth_tools.py +217 -9
  39. package/src/qingflow_mcp/tools/base.py +6 -2
  40. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  41. package/src/qingflow_mcp/tools/import_tools.py +36 -2
  42. package/src/qingflow_mcp/tools/record_tools.py +410 -156
  43. package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
  44. package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
  45. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -13,6 +13,7 @@ from mcp.server.fastmcp import FastMCP
13
13
 
14
14
  from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
15
15
  from ..errors import QingflowApiError, raise_tool_error
16
+ from ..id_utils import normalize_positive_id_int, stringify_backend_id
16
17
  from ..json_types import JSONObject, JSONScalar, JSONValue
17
18
  from ..list_type_labels import (
18
19
  SYSTEM_VIEW_DEFINITIONS,
@@ -180,6 +181,16 @@ class AccessibleViewRoute:
180
181
  view_type: str | None = None
181
182
 
182
183
 
184
+ @dataclass(slots=True)
185
+ class RecordContextRouteProbe:
186
+ route: AccessibleViewRoute
187
+ answer_list: list[JSONObject] | None
188
+ used_list_type: int | None
189
+ readable: bool
190
+ transport_error: bool
191
+ error_payload: JSONObject | None
192
+
193
+
183
194
  @dataclass(slots=True)
184
195
  class WorkflowNodeRef:
185
196
  workflow_node_id: int
@@ -274,7 +285,7 @@ class RecordTools(ToolBase):
274
285
  profile: str = DEFAULT_PROFILE,
275
286
  app_key: str = "",
276
287
  field_id: int = 0,
277
- record_id: int | None = None,
288
+ record_id: str | None = None,
278
289
  workflow_node_id: int | None = None,
279
290
  fields: JSONObject | None = None,
280
291
  keyword: str = "",
@@ -305,7 +316,7 @@ class RecordTools(ToolBase):
305
316
  profile: str = DEFAULT_PROFILE,
306
317
  app_key: str = "",
307
318
  field_id: int = 0,
308
- record_id: int | None = None,
319
+ record_id: str | None = None,
309
320
  workflow_node_id: int | None = None,
310
321
  fields: JSONObject | None = None,
311
322
  keyword: str = "",
@@ -397,7 +408,7 @@ class RecordTools(ToolBase):
397
408
  def record_get(
398
409
  profile: str = DEFAULT_PROFILE,
399
410
  app_key: str = "",
400
- record_id: int = 0,
411
+ record_id: str = "",
401
412
  columns: list[JSONObject | int] | None = None,
402
413
  view_id: str | None = None,
403
414
  workflow_node_id: int | None = None,
@@ -429,7 +440,7 @@ class RecordTools(ToolBase):
429
440
  @mcp.tool()
430
441
  def record_update_schema_get(
431
442
  app_key: str = "",
432
- record_id: int = 0,
443
+ record_id: str = "",
433
444
  output_profile: str = "normal",
434
445
  ) -> JSONObject:
435
446
  return self.record_update_schema_get_public(
@@ -469,7 +480,7 @@ class RecordTools(ToolBase):
469
480
  )
470
481
  def record_update(
471
482
  app_key: str = "",
472
- record_id: int | None = None,
483
+ record_id: str | None = None,
473
484
  fields: JSONObject | None = None,
474
485
  items: list[JSONObject] | None = None,
475
486
  dry_run: bool = False,
@@ -495,8 +506,8 @@ class RecordTools(ToolBase):
495
506
  )
496
507
  def record_delete(
497
508
  app_key: str = "",
498
- record_id: int | None = None,
499
- record_ids: list[int] | None = None,
509
+ record_id: str | None = None,
510
+ record_ids: list[str] | None = None,
500
511
  output_profile: str = "normal",
501
512
  ) -> JSONObject:
502
513
  return self.record_delete_public(
@@ -774,14 +785,13 @@ class RecordTools(ToolBase):
774
785
  *,
775
786
  profile: str = DEFAULT_PROFILE,
776
787
  app_key: str,
777
- record_id: int,
788
+ record_id: Any,
778
789
  output_profile: str = "normal",
779
790
  ) -> JSONObject:
780
791
  """执行记录相关逻辑。"""
781
792
  if not app_key:
782
793
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
783
- if record_id <= 0:
784
- raise_tool_error(QingflowApiError.config_error("record_id is required"))
794
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
785
795
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
786
796
 
787
797
  def runner(session_profile, context):
@@ -801,55 +811,29 @@ class RecordTools(ToolBase):
801
811
  index=app_index,
802
812
  question_relations=question_relations,
803
813
  )
804
- try:
805
- current_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=record_id)
806
- except QingflowApiError:
807
- return self._record_update_schema_blocked_response(
808
- profile=profile,
809
- ws_id=session_profile.selected_ws_id,
810
- request_route=request_route,
811
- app_key=app_key,
812
- record_id=record_id,
813
- blockers=["CURRENT_RECORD_CONTEXT_UNAVAILABLE"],
814
- warnings=[
815
- "update schema could not load the current record; context-sensitive lookup requirements cannot be derived safely."
816
- ],
817
- recommended_next_actions=[
818
- "Retry after the record becomes readable in the current workspace/profile context.",
819
- "If the issue persists, verify that the current profile still has read access to this record.",
820
- ],
821
- output_profile=normalized_output_profile,
822
- view_probe_summary=[],
823
- ambiguous_fields=[],
824
- )
825
-
826
814
  candidate_routes = self._candidate_update_views(profile, context, app_key)
815
+ probes = self._probe_candidate_record_contexts(
816
+ context,
817
+ app_key=app_key,
818
+ apply_id=record_id_int,
819
+ candidate_routes=candidate_routes,
820
+ )
827
821
  probe_summary: list[JSONObject] = []
828
- matched_any = False
822
+ matched_probes: list[RecordContextRouteProbe] = []
829
823
  ordered_field_ids: list[int] = []
830
824
  field_payloads_by_id: dict[int, JSONObject] = {}
831
825
  title_to_field_ids: dict[str, list[int]] = {}
832
826
  ws_id = session_profile.selected_ws_id
833
827
 
834
- for candidate in candidate_routes:
835
- matched_record = self._record_matches_accessible_view(
836
- context,
837
- current_answers,
838
- resolved_view=candidate,
839
- )
840
- candidate_summary: JSONObject = {
841
- "view_id": candidate.view_id,
842
- "name": candidate.name,
843
- "kind": candidate.kind,
844
- "matched_record": matched_record,
845
- "context_complete": True,
846
- "writable_field_titles": [],
847
- }
848
- if not matched_record:
828
+ for probe in probes:
829
+ candidate = probe.route
830
+ candidate_summary = self._record_context_probe_summary_payload(probe)
831
+ candidate_summary["writable_field_titles"] = []
832
+ if not probe.readable:
849
833
  probe_summary.append(candidate_summary)
850
834
  continue
851
835
 
852
- matched_any = True
836
+ matched_probes.append(probe)
853
837
  browse_scope = self._build_browse_write_scope(
854
838
  profile,
855
839
  context,
@@ -893,19 +877,39 @@ class RecordTools(ToolBase):
893
877
  candidate_summary["writable_field_titles"] = candidate_titles
894
878
  probe_summary.append(candidate_summary)
895
879
 
896
- if not matched_any:
880
+ if not matched_probes:
881
+ blockers = (
882
+ ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
883
+ if probes and all(probe.transport_error for probe in probes)
884
+ else ["NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"]
885
+ )
886
+ warnings = (
887
+ [
888
+ "update schema could not load the current record from any candidate route; context-sensitive lookup requirements cannot be derived safely."
889
+ ]
890
+ if blockers == ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
891
+ else []
892
+ )
893
+ recommended_next_actions = (
894
+ [
895
+ "Retry after the record becomes readable in the current workspace/profile context.",
896
+ "If the issue persists, verify that the current profile still has read access to this record.",
897
+ ]
898
+ if blockers == ["CURRENT_RECORD_CONTEXT_UNAVAILABLE"]
899
+ else [
900
+ "Check whether this record is still visible in any accessible view for the current profile.",
901
+ "Use record_get or record_list to confirm the record still exists in the current workspace.",
902
+ ]
903
+ )
897
904
  return self._record_update_schema_blocked_response(
898
905
  profile=profile,
899
906
  ws_id=ws_id,
900
907
  request_route=request_route,
901
908
  app_key=app_key,
902
- record_id=record_id,
903
- blockers=["NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"],
904
- warnings=[],
905
- recommended_next_actions=[
906
- "Check whether this record is still visible in any accessible view for the current profile.",
907
- "Use record_get or record_list to confirm the record still exists in the current workspace.",
908
- ],
909
+ record_id=record_id_int,
910
+ blockers=blockers,
911
+ warnings=warnings,
912
+ recommended_next_actions=recommended_next_actions,
909
913
  output_profile=normalized_output_profile,
910
914
  view_probe_summary=probe_summary,
911
915
  ambiguous_fields=[],
@@ -913,7 +917,10 @@ class RecordTools(ToolBase):
913
917
 
914
918
  ambiguous_field_ids: set[int] = set()
915
919
  ambiguous_fields: list[JSONObject] = []
916
- warnings: list[JSONObject] = []
920
+ warnings: list[JSONObject] = [
921
+ {"code": "RECORD_CONTEXT_ROUTE_FALLBACK", "message": message}
922
+ for message in self._record_context_probe_fallback_warnings(probes)
923
+ ]
917
924
  for title, field_ids in title_to_field_ids.items():
918
925
  unique_ids = list(dict.fromkeys(field_ids))
919
926
  if len(unique_ids) <= 1:
@@ -942,7 +949,7 @@ class RecordTools(ToolBase):
942
949
  ws_id=ws_id,
943
950
  request_route=request_route,
944
951
  app_key=app_key,
945
- record_id=record_id,
952
+ record_id=record_id_int,
946
953
  blockers=["NO_WRITABLE_FIELDS_FOR_RECORD"],
947
954
  warnings=[item["message"] for item in warnings if isinstance(item.get("message"), str)],
948
955
  recommended_next_actions=[
@@ -962,7 +969,7 @@ class RecordTools(ToolBase):
962
969
  "request_route": request_route,
963
970
  "warnings": warnings,
964
971
  "app_key": app_key,
965
- "record_id": record_id,
972
+ "record_id": stringify_backend_id(record_id_int),
966
973
  "schema_scope": "update_ready",
967
974
  "writable_fields": writable_fields,
968
975
  "payload_template": {
@@ -972,6 +979,7 @@ class RecordTools(ToolBase):
972
979
  }
973
980
  if normalized_output_profile == "verbose":
974
981
  response["view_probe_summary"] = probe_summary
982
+ response["record_context_probe"] = probe_summary
975
983
  response["ambiguous_fields"] = ambiguous_fields
976
984
  return response
977
985
 
@@ -1001,7 +1009,7 @@ class RecordTools(ToolBase):
1001
1009
  "request_route": request_route,
1002
1010
  "warnings": [{"code": "PREFLIGHT_WARNING", "message": item} for item in warnings],
1003
1011
  "app_key": app_key,
1004
- "record_id": record_id,
1012
+ "record_id": stringify_backend_id(record_id),
1005
1013
  "schema_scope": "update_ready",
1006
1014
  "blockers": blockers,
1007
1015
  "writable_fields": [],
@@ -1022,6 +1030,7 @@ class RecordTools(ToolBase):
1022
1030
  ws_id: int | None,
1023
1031
  required_override: bool | None,
1024
1032
  linkage_payloads_by_field_id: dict[str, JSONObject] | None = None,
1033
+ include_field_id: bool = True,
1025
1034
  ) -> JSONObject:
1026
1035
  """执行内部辅助逻辑。"""
1027
1036
  kind = self._ready_schema_kind(field)
@@ -1042,6 +1051,8 @@ class RecordTools(ToolBase):
1042
1051
  "kind": kind,
1043
1052
  "required": required,
1044
1053
  }
1054
+ if include_field_id:
1055
+ payload["field_id"] = field.que_id
1045
1056
  if kind in {"single_select", "multi_select"} and field.options:
1046
1057
  payload["options"] = list(field.options)
1047
1058
  if kind in {"member", "department", "relation"}:
@@ -1097,6 +1108,7 @@ class RecordTools(ToolBase):
1097
1108
  ws_id=ws_id,
1098
1109
  required_override=True,
1099
1110
  linkage_payloads_by_field_id=linkage_payloads_by_field_id,
1111
+ include_field_id=False,
1100
1112
  )
1101
1113
  payload["reason"] = reason
1102
1114
  self._attach_ready_schema_linkage(
@@ -1177,6 +1189,7 @@ class RecordTools(ToolBase):
1177
1189
  ws_id=ws_id,
1178
1190
  required_override=bool(subfield.required),
1179
1191
  linkage_payloads_by_field_id=linkage_payloads_by_field_id,
1192
+ include_field_id=False,
1180
1193
  )
1181
1194
  )
1182
1195
  return payloads
@@ -1268,7 +1281,7 @@ class RecordTools(ToolBase):
1268
1281
  profile: str,
1269
1282
  app_key: str,
1270
1283
  field_id: int,
1271
- record_id: int | None = None,
1284
+ record_id: Any | None = None,
1272
1285
  workflow_node_id: int | None = None,
1273
1286
  fields: JSONObject | None = None,
1274
1287
  keyword: str,
@@ -1284,6 +1297,12 @@ class RecordTools(ToolBase):
1284
1297
  raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
1285
1298
  if page_size <= 0:
1286
1299
  raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
1300
+ record_id_int = (
1301
+ normalize_positive_id_int(record_id, field_name="record_id")
1302
+ if record_id is not None
1303
+ else None
1304
+ )
1305
+ record_id_text = stringify_backend_id(record_id_int) if record_id_int is not None else None
1287
1306
 
1288
1307
  def runner(session_profile, context):
1289
1308
  index = self._get_field_index(profile, context, app_key, force_refresh=False)
@@ -1303,7 +1322,7 @@ class RecordTools(ToolBase):
1303
1322
  )
1304
1323
  normalized_fields = fields if isinstance(fields, dict) else {}
1305
1324
  runtime_lookup = self._candidate_lookup_uses_runtime_scope(
1306
- record_id=record_id,
1325
+ record_id=record_id_int,
1307
1326
  workflow_node_id=workflow_node_id,
1308
1327
  fields=normalized_fields,
1309
1328
  )
@@ -1314,7 +1333,7 @@ class RecordTools(ToolBase):
1314
1333
  profile,
1315
1334
  context,
1316
1335
  app_key=app_key,
1317
- record_id=record_id,
1336
+ record_id=record_id_int,
1318
1337
  workflow_node_id=workflow_node_id,
1319
1338
  fields=normalized_fields,
1320
1339
  )
@@ -1353,7 +1372,7 @@ class RecordTools(ToolBase):
1353
1372
  "app_key": app_key,
1354
1373
  "field_id": field.que_id,
1355
1374
  "field_title": field.que_title,
1356
- "record_id": record_id,
1375
+ "record_id": record_id_text,
1357
1376
  "workflow_node_id": workflow_node_id,
1358
1377
  "fields_present": bool(normalized_fields),
1359
1378
  "keyword": keyword,
@@ -1372,7 +1391,7 @@ class RecordTools(ToolBase):
1372
1391
  profile: str,
1373
1392
  app_key: str,
1374
1393
  field_id: int,
1375
- record_id: int | None = None,
1394
+ record_id: Any | None = None,
1376
1395
  workflow_node_id: int | None = None,
1377
1396
  fields: JSONObject | None = None,
1378
1397
  keyword: str,
@@ -1388,6 +1407,12 @@ class RecordTools(ToolBase):
1388
1407
  raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
1389
1408
  if page_size <= 0:
1390
1409
  raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
1410
+ record_id_int = (
1411
+ normalize_positive_id_int(record_id, field_name="record_id")
1412
+ if record_id is not None
1413
+ else None
1414
+ )
1415
+ record_id_text = stringify_backend_id(record_id_int) if record_id_int is not None else None
1391
1416
 
1392
1417
  def runner(session_profile, context):
1393
1418
  index = self._get_field_index(profile, context, app_key, force_refresh=False)
@@ -1407,7 +1432,7 @@ class RecordTools(ToolBase):
1407
1432
  )
1408
1433
  normalized_fields = fields if isinstance(fields, dict) else {}
1409
1434
  runtime_lookup = self._candidate_lookup_uses_runtime_scope(
1410
- record_id=record_id,
1435
+ record_id=record_id_int,
1411
1436
  workflow_node_id=workflow_node_id,
1412
1437
  fields=normalized_fields,
1413
1438
  )
@@ -1418,7 +1443,7 @@ class RecordTools(ToolBase):
1418
1443
  profile,
1419
1444
  context,
1420
1445
  app_key=app_key,
1421
- record_id=record_id,
1446
+ record_id=record_id_int,
1422
1447
  workflow_node_id=workflow_node_id,
1423
1448
  fields=normalized_fields,
1424
1449
  )
@@ -1474,7 +1499,7 @@ class RecordTools(ToolBase):
1474
1499
  "app_key": app_key,
1475
1500
  "field_id": field.que_id,
1476
1501
  "field_title": field.que_title,
1477
- "record_id": record_id,
1502
+ "record_id": record_id_text,
1478
1503
  "workflow_node_id": workflow_node_id,
1479
1504
  "fields_present": bool(normalized_fields),
1480
1505
  "keyword": keyword,
@@ -1740,7 +1765,7 @@ class RecordTools(ToolBase):
1740
1765
  *,
1741
1766
  profile: str,
1742
1767
  app_key: str,
1743
- record_id: int,
1768
+ record_id: Any,
1744
1769
  columns: list[JSONObject | int],
1745
1770
  view_id: str | None = None,
1746
1771
  workflow_node_id: int | None = None,
@@ -1748,8 +1773,7 @@ class RecordTools(ToolBase):
1748
1773
  ) -> JSONObject:
1749
1774
  """执行记录相关逻辑。"""
1750
1775
  normalized_output_profile = self._normalize_public_output_profile(output_profile, allow_normalized=True)
1751
- if record_id <= 0:
1752
- raise_tool_error(QingflowApiError.config_error("record_id must be positive"))
1776
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1753
1777
  normalized_columns = _normalize_public_column_selectors(columns)
1754
1778
 
1755
1779
  def runner(session_profile, context):
@@ -1778,7 +1802,7 @@ class RecordTools(ToolBase):
1778
1802
  result = self.backend.request(
1779
1803
  "GET",
1780
1804
  context,
1781
- f"/view/{resolved_view.view_selection.view_key}/apply/{record_id}",
1805
+ f"/view/{resolved_view.view_selection.view_key}/apply/{record_id_int}",
1782
1806
  )
1783
1807
  used_list_type = None
1784
1808
  else:
@@ -1795,7 +1819,7 @@ class RecordTools(ToolBase):
1795
1819
  result = self.backend.request(
1796
1820
  "GET",
1797
1821
  context,
1798
- f"/app/{app_key}/apply/{record_id}",
1822
+ f"/app/{app_key}/apply/{record_id_int}",
1799
1823
  params={"role": 1, "listType": lt},
1800
1824
  )
1801
1825
  used_list_type = lt
@@ -1810,7 +1834,7 @@ class RecordTools(ToolBase):
1810
1834
  raise last_error
1811
1835
  raise_tool_error(QingflowApiError.config_error("record_get failed: no accessible listType"))
1812
1836
  answer_list = result.get("answers") if isinstance(result, dict) and isinstance(result.get("answers"), list) else []
1813
- row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id)
1837
+ row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
1814
1838
  normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
1815
1839
  cast(list[JSONValue], answer_list),
1816
1840
  selected_fields,
@@ -1838,7 +1862,7 @@ class RecordTools(ToolBase):
1838
1862
  if normalized_columns
1839
1863
  else list(index.by_id.values())
1840
1864
  )
1841
- row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id)
1865
+ row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id_int)
1842
1866
  normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
1843
1867
  cast(list[JSONValue], answer_list),
1844
1868
  selected_fields,
@@ -1861,7 +1885,7 @@ class RecordTools(ToolBase):
1861
1885
  "output_profile": normalized_output_profile,
1862
1886
  "data": {
1863
1887
  "app_key": app_key,
1864
- "record_id": _public_record_id_text(record_id),
1888
+ "record_id": _public_record_id_text(record_id_int),
1865
1889
  "record": row,
1866
1890
  "selection": {
1867
1891
  "columns": [_column_selector_payload(field_id) for field_id in normalized_columns] if normalized_columns else [],
@@ -1962,7 +1986,7 @@ class RecordTools(ToolBase):
1962
1986
  *,
1963
1987
  profile: str = DEFAULT_PROFILE,
1964
1988
  app_key: str,
1965
- record_id: int | None,
1989
+ record_id: Any | None,
1966
1990
  fields: JSONObject | None = None,
1967
1991
  items: list[JSONObject] | None = None,
1968
1992
  dry_run: bool = False,
@@ -1991,14 +2015,15 @@ class RecordTools(ToolBase):
1991
2015
  )
1992
2016
  if dry_run:
1993
2017
  raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
1994
- if record_id is None or record_id <= 0:
2018
+ if record_id is None:
1995
2019
  raise_tool_error(QingflowApiError.config_error("record_id is required"))
2020
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1996
2021
  if fields is not None and not isinstance(fields, dict):
1997
2022
  raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
1998
2023
  return self._record_update_public_single(
1999
2024
  profile=profile,
2000
2025
  app_key=app_key,
2001
- record_id=record_id,
2026
+ record_id=record_id_int,
2002
2027
  fields=cast(JSONObject, fields or {}),
2003
2028
  verify_write=verify_write,
2004
2029
  output_profile=normalized_output_profile,
@@ -2188,7 +2213,7 @@ class RecordTools(ToolBase):
2188
2213
  def _normalize_public_record_update_batch_items(
2189
2214
  self,
2190
2215
  *,
2191
- record_id: int | None,
2216
+ record_id: Any | None,
2192
2217
  fields: JSONObject | None,
2193
2218
  items: list[JSONObject] | None,
2194
2219
  ) -> list[JSONObject]:
@@ -2204,9 +2229,10 @@ class RecordTools(ToolBase):
2204
2229
  for index, item in enumerate(items):
2205
2230
  if not isinstance(item, dict):
2206
2231
  raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
2207
- normalized_record_id = _coerce_count(item.get("record_id"))
2208
- if normalized_record_id is None or normalized_record_id <= 0:
2209
- raise_tool_error(QingflowApiError.config_error(f"items[{index}].record_id must be a positive integer"))
2232
+ normalized_record_id = normalize_positive_id_int(
2233
+ item.get("record_id"),
2234
+ field_name=f"items[{index}].record_id",
2235
+ )
2210
2236
  if normalized_record_id in seen_record_ids:
2211
2237
  raise_tool_error(
2212
2238
  QingflowApiError.config_error(f"duplicate record_id in items: {normalized_record_id}")
@@ -2336,9 +2362,15 @@ class RecordTools(ToolBase):
2336
2362
  def runner(session_profile, context):
2337
2363
  request_route = self._request_route_payload(context)
2338
2364
  def build_once(*, effective_force_refresh: bool) -> JSONObject:
2339
- try:
2340
- current_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=record_id)
2341
- except QingflowApiError:
2365
+ candidate_routes = self._candidate_update_views(profile, context, app_key)
2366
+ probes = self._probe_candidate_record_contexts(
2367
+ context,
2368
+ app_key=app_key,
2369
+ apply_id=record_id,
2370
+ candidate_routes=candidate_routes,
2371
+ )
2372
+ matched_probes = [probe for probe in probes if probe.readable]
2373
+ if not matched_probes and probes and all(probe.transport_error for probe in probes):
2342
2374
  return {
2343
2375
  "profile": profile,
2344
2376
  "ws_id": session_profile.selected_ws_id,
@@ -2349,46 +2381,40 @@ class RecordTools(ToolBase):
2349
2381
  record_id=record_id,
2350
2382
  blockers=["CURRENT_RECORD_CONTEXT_UNAVAILABLE"],
2351
2383
  warnings=[
2352
- "update preflight could not load the current record; automatic view selection was blocked to avoid resolving lookup fields against a partial patch."
2384
+ "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."
2353
2385
  ],
2354
2386
  recommended_next_actions=[
2355
2387
  "Retry after the record becomes readable in the current workspace/profile context.",
2356
2388
  "Call record_update_schema_get to inspect the overall writable field set for this record after context access is restored.",
2357
2389
  ],
2358
- view_probe_summary=[],
2390
+ view_probe_summary=[
2391
+ self._record_context_probe_summary_payload(probe)
2392
+ for probe in probes
2393
+ ],
2359
2394
  ),
2360
2395
  }
2361
2396
 
2362
- candidate_routes = self._candidate_update_views(profile, context, app_key)
2363
2397
  probe_summary: list[JSONObject] = []
2364
- matched_any = False
2365
2398
  matched_routes: list[AccessibleViewRoute] = []
2399
+ matched_answers_for_union: list[JSONObject] | None = None
2366
2400
  first_blocked_plan: JSONObject | None = None
2367
2401
  first_confirmation_plan: JSONObject | None = None
2368
-
2369
- for candidate in candidate_routes:
2370
- matched_record = self._record_matches_accessible_view(
2371
- context,
2372
- current_answers,
2373
- resolved_view=candidate,
2374
- )
2375
- if not matched_record:
2376
- probe_summary.append(
2377
- {
2378
- "view_id": candidate.view_id,
2379
- "name": candidate.name,
2380
- "kind": candidate.kind,
2381
- "matched_record": False,
2382
- "writable_field_titles": [],
2383
- "missing_field_titles": [],
2384
- "context_complete": True,
2385
- "selected": False,
2386
- }
2387
- )
2402
+ fallback_warning_messages = self._record_context_probe_fallback_warnings(probes)
2403
+
2404
+ for probe in probes:
2405
+ candidate = probe.route
2406
+ candidate_summary = self._record_context_probe_summary_payload(probe)
2407
+ candidate_summary["writable_field_titles"] = []
2408
+ candidate_summary["missing_field_titles"] = []
2409
+ candidate_summary["selected"] = False
2410
+ if not probe.readable:
2411
+ probe_summary.append(candidate_summary)
2388
2412
  continue
2389
2413
 
2390
- matched_any = True
2414
+ current_answers = probe.answer_list or []
2391
2415
  matched_routes.append(candidate)
2416
+ if matched_answers_for_union is None:
2417
+ matched_answers_for_union = current_answers
2392
2418
  browse_scope = self._build_browse_write_scope(
2393
2419
  profile,
2394
2420
  context,
@@ -2444,16 +2470,8 @@ class RecordTools(ToolBase):
2444
2470
  title = _normalize_optional_text(field_payload.get("que_title"))
2445
2471
  if title and title not in missing_field_titles:
2446
2472
  missing_field_titles.append(title)
2447
- candidate_summary: JSONObject = {
2448
- "view_id": candidate.view_id,
2449
- "name": candidate.name,
2450
- "kind": candidate.kind,
2451
- "matched_record": True,
2452
- "writable_field_titles": candidate_titles,
2453
- "missing_field_titles": missing_field_titles,
2454
- "context_complete": True,
2455
- "selected": False,
2456
- }
2473
+ candidate_summary["writable_field_titles"] = candidate_titles
2474
+ candidate_summary["missing_field_titles"] = missing_field_titles
2457
2475
  if plan_data.get("blockers"):
2458
2476
  confirmation_requests = plan_data.get("confirmation_requests")
2459
2477
  if (
@@ -2472,8 +2490,26 @@ class RecordTools(ToolBase):
2472
2490
  view_payload["auto_selected"] = True
2473
2491
  view_payload["selection_source"] = "first_satisfying_accessible_view"
2474
2492
  candidate_summary["selected"] = True
2493
+ validation = plan_data.get("validation")
2494
+ if isinstance(validation, dict):
2495
+ warnings = validation.get("warnings")
2496
+ if not isinstance(warnings, list):
2497
+ warnings = []
2498
+ validation["warnings"] = warnings
2499
+ for message in fallback_warning_messages:
2500
+ if message not in warnings:
2501
+ warnings.append(message)
2475
2502
  first_confirmation_plan = plan_data
2476
2503
  elif first_blocked_plan is None:
2504
+ validation = plan_data.get("validation")
2505
+ if isinstance(validation, dict):
2506
+ warnings = validation.get("warnings")
2507
+ if not isinstance(warnings, list):
2508
+ warnings = []
2509
+ validation["warnings"] = warnings
2510
+ for message in fallback_warning_messages:
2511
+ if message not in warnings:
2512
+ warnings.append(message)
2477
2513
  first_blocked_plan = plan_data
2478
2514
  probe_summary.append(candidate_summary)
2479
2515
  continue
@@ -2489,8 +2525,18 @@ class RecordTools(ToolBase):
2489
2525
  view_payload["auto_selected"] = True
2490
2526
  view_payload["selection_source"] = "first_satisfying_accessible_view"
2491
2527
  candidate_summary["selected"] = True
2528
+ validation = plan_data.get("validation")
2529
+ if isinstance(validation, dict):
2530
+ warnings = validation.get("warnings")
2531
+ if not isinstance(warnings, list):
2532
+ warnings = []
2533
+ validation["warnings"] = warnings
2534
+ for message in fallback_warning_messages:
2535
+ if message not in warnings:
2536
+ warnings.append(message)
2492
2537
  probe_summary.append(candidate_summary)
2493
2538
  plan_data["view_probe_summary"] = probe_summary
2539
+ plan_data["record_context_probe"] = probe_summary
2494
2540
  return {
2495
2541
  "profile": profile,
2496
2542
  "ws_id": session_profile.selected_ws_id,
@@ -2499,7 +2545,7 @@ class RecordTools(ToolBase):
2499
2545
  "data": plan_data,
2500
2546
  }
2501
2547
 
2502
- if not matched_any:
2548
+ if not matched_probes:
2503
2549
  blocked_data = self._build_auto_view_blocked_preflight_data(
2504
2550
  app_key=app_key,
2505
2551
  record_id=record_id,
@@ -2521,6 +2567,7 @@ class RecordTools(ToolBase):
2521
2567
 
2522
2568
  if first_confirmation_plan is not None:
2523
2569
  first_confirmation_plan["view_probe_summary"] = probe_summary
2570
+ first_confirmation_plan["record_context_probe"] = probe_summary
2524
2571
  return {
2525
2572
  "profile": profile,
2526
2573
  "ws_id": session_profile.selected_ws_id,
@@ -2535,12 +2582,22 @@ class RecordTools(ToolBase):
2535
2582
  app_key=app_key,
2536
2583
  record_id=record_id,
2537
2584
  fields=fields,
2538
- current_answers=current_answers,
2585
+ current_answers=matched_answers_for_union or [],
2539
2586
  matched_routes=matched_routes,
2540
2587
  force_refresh_form=effective_force_refresh,
2541
2588
  )
2542
2589
  if union_plan is not None:
2590
+ validation = union_plan.get("validation")
2591
+ if isinstance(validation, dict):
2592
+ warnings = validation.get("warnings")
2593
+ if not isinstance(warnings, list):
2594
+ warnings = []
2595
+ validation["warnings"] = warnings
2596
+ for message in fallback_warning_messages:
2597
+ if message not in warnings:
2598
+ warnings.append(message)
2543
2599
  union_plan["view_probe_summary"] = probe_summary
2600
+ union_plan["record_context_probe"] = probe_summary
2544
2601
  return {
2545
2602
  "profile": profile,
2546
2603
  "ws_id": session_profile.selected_ws_id,
@@ -2803,8 +2860,6 @@ class RecordTools(ToolBase):
2803
2860
  """执行内部辅助逻辑。"""
2804
2861
  candidates: list[AccessibleViewRoute] = []
2805
2862
  for view_id, list_type, name in SYSTEM_VIEW_DEFINITIONS:
2806
- if not self._probe_list_type_access(context, app_key=app_key, list_type=list_type):
2807
- continue
2808
2863
  candidates.append(
2809
2864
  AccessibleViewRoute(
2810
2865
  view_id=view_id,
@@ -2821,11 +2876,22 @@ class RecordTools(ToolBase):
2821
2876
  view_key = _normalize_optional_text(item.get("viewKey"))
2822
2877
  if not view_key:
2823
2878
  continue
2824
- view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=None)
2825
- if view_selection is None:
2826
- continue
2827
- view_name = _normalize_optional_text(item.get("viewName")) or view_selection.view_name or view_key
2828
- view_type = _normalize_optional_text(item.get("viewType") or item.get("viewgraphType")) or view_selection.view_type
2879
+ view_name = _normalize_optional_text(item.get("viewName")) or view_key
2880
+ view_type = _normalize_optional_text(item.get("viewType") or item.get("viewgraphType"))
2881
+ filter_config: JSONObject | None = None
2882
+ if isinstance(item.get("viewgraphLimit"), list):
2883
+ filter_config = item
2884
+ else:
2885
+ raw_view_config = item.get("viewConfig")
2886
+ if isinstance(raw_view_config, dict):
2887
+ filter_config = raw_view_config
2888
+ view_selection = ViewSelection(
2889
+ view_key=view_key,
2890
+ view_name=view_name,
2891
+ conditions=_compile_view_conditions(filter_config or {}),
2892
+ view_type=view_type,
2893
+ filter_config_loaded=isinstance(filter_config, dict),
2894
+ )
2829
2895
  candidates.append(
2830
2896
  AccessibleViewRoute(
2831
2897
  view_id=f"custom:{view_key}",
@@ -2838,6 +2904,161 @@ class RecordTools(ToolBase):
2838
2904
  )
2839
2905
  return candidates
2840
2906
 
2907
+ def _route_view_key(self, resolved_view: AccessibleViewRoute) -> str | None:
2908
+ if resolved_view.view_selection is not None:
2909
+ view_key = _normalize_optional_text(resolved_view.view_selection.view_key)
2910
+ if view_key:
2911
+ return view_key
2912
+ if resolved_view.kind == "custom" and resolved_view.view_id.startswith("custom:"):
2913
+ view_key = resolved_view.view_id.split(":", 1)[1].strip()
2914
+ return view_key or None
2915
+ return None
2916
+
2917
+ def _record_context_route_error_payload(self, error: QingflowApiError) -> JSONObject:
2918
+ payload: JSONObject = {"message": error.message}
2919
+ if error.category:
2920
+ payload["category"] = error.category
2921
+ if error.backend_code is not None:
2922
+ payload["backend_code"] = error.backend_code
2923
+ if error.http_status is not None:
2924
+ payload["http_status"] = error.http_status
2925
+ return payload
2926
+
2927
+ def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
2928
+ if error.backend_code in {40002, 40027, 40038, 404}:
2929
+ return True
2930
+ if error.http_status == 404:
2931
+ return True
2932
+ return False
2933
+
2934
+ def _load_record_answers_for_accessible_route(
2935
+ self,
2936
+ context, # type: ignore[no-untyped-def]
2937
+ *,
2938
+ app_key: str,
2939
+ apply_id: int,
2940
+ resolved_view: AccessibleViewRoute,
2941
+ ) -> tuple[list[JSONObject], int | None]:
2942
+ if resolved_view.kind == "custom":
2943
+ view_key = self._route_view_key(resolved_view)
2944
+ if not view_key:
2945
+ raise_tool_error(
2946
+ QingflowApiError.config_error(
2947
+ f"cannot resolve custom view route for '{resolved_view.view_id}'"
2948
+ )
2949
+ )
2950
+ record = self.backend.request(
2951
+ "GET",
2952
+ context,
2953
+ f"/view/{view_key}/apply/{apply_id}",
2954
+ )
2955
+ used_list_type = None
2956
+ else:
2957
+ used_list_type = resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE
2958
+ record = self.backend.request(
2959
+ "GET",
2960
+ context,
2961
+ f"/app/{app_key}/apply/{apply_id}",
2962
+ params={"role": 1, "listType": used_list_type},
2963
+ )
2964
+ answers = record.get("answers") if isinstance(record, dict) else None
2965
+ normalized_answers = [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
2966
+ return normalized_answers, used_list_type
2967
+
2968
+ def _probe_record_context_route(
2969
+ self,
2970
+ context, # type: ignore[no-untyped-def]
2971
+ *,
2972
+ app_key: str,
2973
+ apply_id: int,
2974
+ resolved_view: AccessibleViewRoute,
2975
+ ) -> RecordContextRouteProbe:
2976
+ try:
2977
+ answer_list, used_list_type = self._load_record_answers_for_accessible_route(
2978
+ context,
2979
+ app_key=app_key,
2980
+ apply_id=apply_id,
2981
+ resolved_view=resolved_view,
2982
+ )
2983
+ return RecordContextRouteProbe(
2984
+ route=resolved_view,
2985
+ answer_list=answer_list,
2986
+ used_list_type=used_list_type,
2987
+ readable=True,
2988
+ transport_error=False,
2989
+ error_payload=None,
2990
+ )
2991
+ except QingflowApiError as exc:
2992
+ return RecordContextRouteProbe(
2993
+ route=resolved_view,
2994
+ answer_list=None,
2995
+ used_list_type=resolved_view.list_type if resolved_view.kind == "system" else None,
2996
+ readable=False,
2997
+ transport_error=not self._is_record_context_route_miss(exc),
2998
+ error_payload=self._record_context_route_error_payload(exc),
2999
+ )
3000
+
3001
+ def _probe_candidate_record_contexts(
3002
+ self,
3003
+ context, # type: ignore[no-untyped-def]
3004
+ *,
3005
+ app_key: str,
3006
+ apply_id: int,
3007
+ candidate_routes: list[AccessibleViewRoute],
3008
+ ) -> list[RecordContextRouteProbe]:
3009
+ return [
3010
+ self._probe_record_context_route(
3011
+ context,
3012
+ app_key=app_key,
3013
+ apply_id=apply_id,
3014
+ resolved_view=candidate,
3015
+ )
3016
+ for candidate in candidate_routes
3017
+ ]
3018
+
3019
+ def _record_context_probe_summary_payload(self, probe: RecordContextRouteProbe) -> JSONObject:
3020
+ payload: JSONObject = {
3021
+ "view_id": probe.route.view_id,
3022
+ "name": probe.route.name,
3023
+ "kind": probe.route.kind,
3024
+ "matched_record": probe.readable,
3025
+ "readable": probe.readable,
3026
+ "context_complete": probe.readable,
3027
+ "used_list_type": probe.used_list_type,
3028
+ }
3029
+ if probe.error_payload is not None:
3030
+ payload["error"] = probe.error_payload
3031
+ payload["transport_error"] = probe.transport_error
3032
+ return payload
3033
+
3034
+ def _record_context_probe_fallback_warnings(
3035
+ self,
3036
+ probes: list[RecordContextRouteProbe],
3037
+ ) -> list[str]:
3038
+ matched_probes = [probe for probe in probes if probe.readable]
3039
+ if not matched_probes:
3040
+ return []
3041
+ if any(
3042
+ probe.route.kind == "system" and probe.used_list_type == DEFAULT_RECORD_LIST_TYPE
3043
+ for probe in matched_probes
3044
+ ):
3045
+ return []
3046
+ first_probe = matched_probes[0]
3047
+ if first_probe.route.kind == "custom":
3048
+ return [
3049
+ "current record context was not accessible via listType="
3050
+ f"{DEFAULT_RECORD_LIST_TYPE}; preflight resolved it via custom view "
3051
+ f"'{first_probe.route.name}'."
3052
+ ]
3053
+ used_list_type = first_probe.used_list_type
3054
+ if used_list_type is None:
3055
+ return []
3056
+ return [
3057
+ "current record context was not accessible via listType="
3058
+ f"{DEFAULT_RECORD_LIST_TYPE}; preflight resolved it via listType={used_list_type} "
3059
+ f"({get_record_list_type_label(used_list_type)})."
3060
+ ]
3061
+
2841
3062
  def _record_matches_accessible_view(
2842
3063
  self,
2843
3064
  context, # type: ignore[no-untyped-def]
@@ -2861,22 +3082,26 @@ class RecordTools(ToolBase):
2861
3082
  *,
2862
3083
  profile: str = DEFAULT_PROFILE,
2863
3084
  app_key: str,
2864
- record_id: int | None = None,
2865
- record_ids: list[int] | None = None,
3085
+ record_id: Any | None = None,
3086
+ record_ids: list[Any] | None = None,
2866
3087
  output_profile: str = "normal",
2867
3088
  ) -> JSONObject:
2868
3089
  """执行记录相关逻辑。"""
2869
3090
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
2870
3091
  if not app_key:
2871
3092
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
2872
- normalized_record_ids = [item for item in (record_ids or []) if isinstance(item, int) and item > 0]
2873
- delete_ids = normalized_record_ids or ([record_id] if record_id is not None and record_id > 0 else [])
3093
+ normalized_record_ids: list[int] = []
3094
+ for index, item in enumerate(record_ids or []):
3095
+ normalized_record_ids.append(normalize_positive_id_int(item, field_name=f"record_ids[{index}]"))
3096
+ delete_ids = normalized_record_ids
3097
+ if not delete_ids and record_id is not None:
3098
+ delete_ids = [normalize_positive_id_int(record_id, field_name="record_id")]
2874
3099
  if not delete_ids:
2875
3100
  raise_tool_error(QingflowApiError.config_error("record_id or record_ids is required"))
2876
3101
  normalized_payload = {
2877
3102
  "operation": "delete",
2878
- "record_id": record_id,
2879
- "record_ids": delete_ids,
3103
+ "record_id": stringify_backend_id(record_id) if record_id is not None else None,
3104
+ "record_ids": [stringify_backend_id(item) for item in delete_ids],
2880
3105
  "answers": [],
2881
3106
  "submit_type": 1,
2882
3107
  }
@@ -5406,7 +5631,19 @@ class RecordTools(ToolBase):
5406
5631
  existing_answers_loaded = True
5407
5632
  else:
5408
5633
  try:
5409
- existing_answers_for_update = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
5634
+ if resolved_view is not None:
5635
+ existing_answers_for_update, _used_list_type = self._load_record_answers_for_accessible_route(
5636
+ context,
5637
+ app_key=app_key,
5638
+ apply_id=apply_id,
5639
+ resolved_view=resolved_view,
5640
+ )
5641
+ else:
5642
+ existing_answers_for_update = self._load_record_answers_for_preflight(
5643
+ context,
5644
+ app_key=app_key,
5645
+ apply_id=apply_id,
5646
+ )
5410
5647
  existing_answers_loaded = True
5411
5648
  except QingflowApiError:
5412
5649
  validation_warnings.append(
@@ -7144,13 +7381,6 @@ class RecordTools(ToolBase):
7144
7381
  raise_tool_error(QingflowApiError.config_error("view_id is required; call app_get first to inspect accessible_views"))
7145
7382
 
7146
7383
  system_all_list_type = SYSTEM_VIEW_ID_TO_LIST_TYPE["system:all"]
7147
- if not self._probe_list_type_access(context, app_key=app_key, list_type=system_all_list_type):
7148
- raise_tool_error(
7149
- QingflowApiError.config_error(
7150
- "view_id is required because system:all is not accessible; call app_get first to inspect accessible_views"
7151
- )
7152
- )
7153
-
7154
7384
  return (
7155
7385
  AccessibleViewRoute(
7156
7386
  view_id="system:all",
@@ -8314,8 +8544,8 @@ class RecordTools(ToolBase):
8314
8544
  """执行内部辅助逻辑。"""
8315
8545
  payload: JSONObject = {
8316
8546
  "operation": operation,
8317
- "record_id": record_id,
8318
- "record_ids": record_ids,
8547
+ "record_id": stringify_backend_id(record_id) if record_id is not None else None,
8548
+ "record_ids": [stringify_backend_id(item) for item in record_ids],
8319
8549
  "answers": normalized_answers,
8320
8550
  "submit_type": submit_type,
8321
8551
  }
@@ -8514,7 +8744,7 @@ class RecordTools(ToolBase):
8514
8744
  "output_profile": output_profile,
8515
8745
  "data": {
8516
8746
  "action": {"operation": operation, "executed": True},
8517
- "resource": raw_apply.get("resource"),
8747
+ "resource": _public_record_resource(raw_apply.get("resource")),
8518
8748
  "verification": raw_apply.get("verification"),
8519
8749
  "normalized_payload": normalized_payload,
8520
8750
  "blockers": [],
@@ -9859,8 +10089,9 @@ class RecordTools(ToolBase):
9859
10089
  """执行内部辅助逻辑。"""
9860
10090
  if not app_key:
9861
10091
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
9862
- normalized_apply_id = _coerce_count(apply_id)
9863
- if normalized_apply_id is None or normalized_apply_id <= 0:
10092
+ try:
10093
+ normalized_apply_id = normalize_positive_id_int(apply_id, field_name="apply_id")
10094
+ except QingflowApiError:
9864
10095
  raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))
9865
10096
  return normalized_apply_id
9866
10097
 
@@ -10871,10 +11102,14 @@ def _build_flat_row(answer_list: list[JSONValue], fields: list[FormField], *, ap
10871
11102
  return row
10872
11103
 
10873
11104
 
10874
- def _public_record_id_text(record_id: int | None) -> str | None:
10875
- if record_id is None or record_id <= 0:
11105
+ def _public_record_id_text(record_id: Any) -> str | None:
11106
+ if record_id is None or isinstance(record_id, bool):
11107
+ return None
11108
+ if isinstance(record_id, int) and record_id <= 0:
11109
+ return None
11110
+ if isinstance(record_id, str) and (not record_id.strip() or not record_id.strip().isdecimal()):
10876
11111
  return None
10877
- return str(record_id)
11112
+ return stringify_backend_id(record_id)
10878
11113
 
10879
11114
 
10880
11115
  def _normalize_public_record_rows(rows: list[JSONValue]) -> list[JSONObject]:
@@ -11427,10 +11662,29 @@ def _field_mapping_entry(role: str, field: FormField | None, *, requested: str)
11427
11662
  }
11428
11663
 
11429
11664
 
11430
- def _record_resource_payload(record_id: int | None) -> JSONObject | None:
11431
- if record_id is None or record_id <= 0:
11665
+ def _record_resource_payload(record_id: Any) -> JSONObject | None:
11666
+ public_record_id = _public_record_id_text(record_id)
11667
+ if public_record_id is None:
11432
11668
  return None
11433
- return {"type": "record", "record_id": record_id, "apply_id": record_id}
11669
+ return {"type": "record", "record_id": public_record_id, "apply_id": public_record_id}
11670
+
11671
+
11672
+ def _public_record_resource(resource: Any) -> Any:
11673
+ if not isinstance(resource, dict) or resource.get("type") != "record":
11674
+ return resource
11675
+ payload = dict(resource)
11676
+ if "record_id" in payload:
11677
+ payload["record_id"] = _public_record_id_text(payload.get("record_id"))
11678
+ if "apply_id" in payload:
11679
+ payload["apply_id"] = _public_record_id_text(payload.get("apply_id"))
11680
+ record_ids = payload.get("record_ids")
11681
+ if isinstance(record_ids, list):
11682
+ payload["record_ids"] = [
11683
+ stringify_backend_id(item)
11684
+ for item in record_ids
11685
+ if stringify_backend_id(item) is not None
11686
+ ]
11687
+ return payload
11434
11688
 
11435
11689
 
11436
11690
  def _query_id() -> str: