@josephyan/qingflow-app-user-mcp 0.2.0-beta.987 → 0.2.0-beta.989

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 @josephyan/qingflow-app-user-mcp@0.2.0-beta.987
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.989
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.987 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.989 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.987",
3
+ "version": "0.2.0-beta.989",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory 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 = "0.2.0b987"
7
+ version = "0.2.0b989"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b987"
8
+ _FALLBACK_VERSION = "0.2.0b989"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -13454,6 +13454,7 @@ def _canonicalize_reference_questions_for_save(
13454
13454
  source: dict[str, Any],
13455
13455
  field: dict[str, Any],
13456
13456
  ) -> list[dict[str, Any]]:
13457
+ relation_config_explicit = bool(field.get("_relation_config_explicit"))
13457
13458
  normalized_source_questions = [
13458
13459
  item
13459
13460
  for item in (
@@ -13462,6 +13463,8 @@ def _canonicalize_reference_questions_for_save(
13462
13463
  )
13463
13464
  if item is not None
13464
13465
  ]
13466
+ if not relation_config_explicit:
13467
+ return normalized_source_questions
13465
13468
 
13466
13469
  display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13467
13470
  visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
@@ -13498,14 +13501,19 @@ def _canonicalize_reference_questions_for_save(
13498
13501
  if matched_index is not None:
13499
13502
  used_source_indexes.add(matched_index)
13500
13503
 
13501
- next_ordinal = len(canonical_questions) + 1
13502
- for index, item in enumerate(normalized_source_questions):
13503
- if index in used_source_indexes:
13504
- continue
13505
- remaining_item = deepcopy(item)
13506
- remaining_item["ordinal"] = next_ordinal
13507
- next_ordinal += 1
13508
- canonical_questions.append(remaining_item)
13504
+ source_target_app_key = str(source.get("referAppKey") or "").strip()
13505
+ target_app_key = str(field.get("target_app_key") or "").strip()
13506
+ preserve_remaining_source_questions = not source_target_app_key or source_target_app_key == target_app_key
13507
+
13508
+ if preserve_remaining_source_questions:
13509
+ next_ordinal = len(canonical_questions) + 1
13510
+ for index, item in enumerate(normalized_source_questions):
13511
+ if index in used_source_indexes:
13512
+ continue
13513
+ remaining_item = deepcopy(item)
13514
+ remaining_item["ordinal"] = next_ordinal
13515
+ next_ordinal += 1
13516
+ canonical_questions.append(remaining_item)
13509
13517
 
13510
13518
  return canonical_questions
13511
13519
 
@@ -13514,6 +13522,7 @@ def _canonicalize_reference_auth_questions_for_save(
13514
13522
  *,
13515
13523
  source: dict[str, Any],
13516
13524
  refer_questions: list[dict[str, Any]],
13525
+ relation_config_explicit: bool,
13517
13526
  ) -> list[dict[str, Any]]:
13518
13527
  source_auth_questions = [
13519
13528
  item
@@ -13530,6 +13539,40 @@ def _canonicalize_reference_auth_questions_for_save(
13530
13539
  continue
13531
13540
  source_auth_by_que_id[que_id] = item
13532
13541
 
13542
+ if not relation_config_explicit:
13543
+ auth_questions: list[dict[str, Any]] = []
13544
+ seen_que_ids: set[int] = set()
13545
+ refer_question_auth_by_que_id: dict[int, int] = {}
13546
+ for item in refer_questions:
13547
+ que_id = _coerce_any_int(item.get("queId"))
13548
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13549
+ if que_id is None or que_auth is None or que_id in refer_question_auth_by_que_id:
13550
+ continue
13551
+ refer_question_auth_by_que_id[que_id] = que_auth
13552
+
13553
+ for item in source_auth_questions:
13554
+ que_id = _coerce_any_int(item.get("queId"))
13555
+ if que_id is None or que_id in seen_que_ids:
13556
+ continue
13557
+ payload = deepcopy(item)
13558
+ if que_id in refer_question_auth_by_que_id:
13559
+ payload["queAuth"] = refer_question_auth_by_que_id[que_id]
13560
+ auth_questions.append(payload)
13561
+ seen_que_ids.add(que_id)
13562
+
13563
+ for item in refer_questions:
13564
+ que_id = _coerce_any_int(item.get("queId"))
13565
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13566
+ if que_id is None or que_auth is None or que_id in seen_que_ids:
13567
+ continue
13568
+ payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
13569
+ payload["queId"] = que_id
13570
+ payload["queAuth"] = que_auth
13571
+ auth_questions.append(payload)
13572
+ seen_que_ids.add(que_id)
13573
+
13574
+ return _dedupe_reference_auth_questions(auth_questions)
13575
+
13533
13576
  auth_questions: list[dict[str, Any]] = []
13534
13577
  for item in refer_questions:
13535
13578
  que_id = _coerce_any_int(item.get("queId"))
@@ -13548,6 +13591,7 @@ def _enforce_reference_config_consistency_for_save(
13548
13591
  *,
13549
13592
  field: dict[str, Any],
13550
13593
  ) -> dict[str, Any]:
13594
+ relation_config_explicit = bool(field.get("_relation_config_explicit"))
13551
13595
  refer_questions = [
13552
13596
  item
13553
13597
  for item in (
@@ -13583,28 +13627,29 @@ def _enforce_reference_config_consistency_for_save(
13583
13627
  if display_field_que_id is not None:
13584
13628
  payload["referQueId"] = display_field_que_id
13585
13629
 
13586
- if display_field_que_id is not None and not any(
13587
- _coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
13588
- ):
13589
- display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13590
- display_question = (
13591
- _build_reference_question_from_visible_selector(display_selector, ordinal=1)
13592
- if display_selector is not None
13593
- else None
13594
- )
13595
- if display_question is not None:
13596
- display_question["queId"] = display_field_que_id
13597
- display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
13598
- refer_questions = [display_question, *refer_questions]
13630
+ if relation_config_explicit:
13631
+ if display_field_que_id is not None and not any(
13632
+ _coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
13633
+ ):
13634
+ display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13635
+ display_question = (
13636
+ _build_reference_question_from_visible_selector(display_selector, ordinal=1)
13637
+ if display_selector is not None
13638
+ else None
13639
+ )
13640
+ if display_question is not None:
13641
+ display_question["queId"] = display_field_que_id
13642
+ display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
13643
+ refer_questions = [display_question, *refer_questions]
13599
13644
 
13600
- if display_field_que_id is not None:
13601
- display_questions = [
13602
- item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
13603
- ]
13604
- trailing_questions = [
13605
- item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
13606
- ]
13607
- refer_questions = [*display_questions, *trailing_questions]
13645
+ if display_field_que_id is not None:
13646
+ display_questions = [
13647
+ item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
13648
+ ]
13649
+ trailing_questions = [
13650
+ item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
13651
+ ]
13652
+ refer_questions = [*display_questions, *trailing_questions]
13608
13653
 
13609
13654
  for ordinal, item in enumerate(refer_questions, start=1):
13610
13655
  que_id = _coerce_any_int(item.get("queId"))
@@ -13622,6 +13667,7 @@ def _enforce_reference_config_consistency_for_save(
13622
13667
  payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
13623
13668
  source={"referAuthQues": refer_auth_ques},
13624
13669
  refer_questions=refer_questions,
13670
+ relation_config_explicit=relation_config_explicit,
13625
13671
  )
13626
13672
  return payload
13627
13673
 
@@ -13661,7 +13707,11 @@ def _normalize_reference_config_for_save(
13661
13707
  if refer_fill_rules or "referFillRules" in source:
13662
13708
  payload["referFillRules"] = refer_fill_rules
13663
13709
 
13664
- refer_auth_ques = _canonicalize_reference_auth_questions_for_save(source=source, refer_questions=refer_questions)
13710
+ refer_auth_ques = _canonicalize_reference_auth_questions_for_save(
13711
+ source=source,
13712
+ refer_questions=refer_questions,
13713
+ relation_config_explicit=bool(field.get("_relation_config_explicit")),
13714
+ )
13665
13715
  if not refer_auth_ques:
13666
13716
  refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
13667
13717
  if refer_auth_ques or "referAuthQues" in source:
@@ -13917,6 +13967,13 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13917
13967
  if isinstance(built_question.get("referenceConfig"), dict)
13918
13968
  else {}
13919
13969
  )
13970
+ original_target_app_key = str(existing_reference.get("referAppKey") or "").strip()
13971
+ next_target_app_key = str(field.get("target_app_key") or "").strip()
13972
+ preserve_existing_reference_questions = (
13973
+ relation_config_explicit
13974
+ and bool(original_target_app_key)
13975
+ and original_target_app_key == next_target_app_key
13976
+ )
13920
13977
  if relation_config_explicit:
13921
13978
  for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
13922
13979
  reference.pop(stale_key, None)
@@ -13928,7 +13985,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13928
13985
  "fieldNameShow",
13929
13986
  "_targetFieldId",
13930
13987
  ):
13931
- if relation_config_explicit and key in {"referQuestions", "referAuthQues"}:
13988
+ if preserve_existing_reference_questions and key in {"referQuestions", "referAuthQues"}:
13932
13989
  continue
13933
13990
  if key in built_reference:
13934
13991
  reference[key] = deepcopy(built_reference[key])
@@ -32,7 +32,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
32
32
 
33
33
  schema_update = schema_subparsers.add_parser("update", help="读取更新记录表结构")
34
34
  schema_update.add_argument("--app-key", required=True)
35
- schema_update.add_argument("--record-id", required=True, type=int)
35
+ schema_update.add_argument("--record-id", required=True)
36
36
  schema_update.set_defaults(handler=_handle_schema_update, format_hint="")
37
37
 
38
38
  schema_import = schema_subparsers.add_parser("import", help="读取导入表结构")
@@ -59,7 +59,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
59
59
 
60
60
  get = record_subparsers.add_parser("get", help="读取单条记录")
61
61
  get.add_argument("--app-key", required=True)
62
- get.add_argument("--record-id", required=True, type=int)
62
+ get.add_argument("--record-id", required=True)
63
63
  get.add_argument("--column", dest="columns", action="append", type=int, default=[])
64
64
  get.add_argument("--columns-file")
65
65
  get.add_argument("--view-id")
@@ -73,7 +73,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
73
73
 
74
74
  update = record_subparsers.add_parser("update", help="更新记录")
75
75
  update.add_argument("--app-key", required=True)
76
- update.add_argument("--record-id", type=int)
76
+ update.add_argument("--record-id")
77
77
  update.add_argument("--fields-file")
78
78
  update.add_argument("--items-file")
79
79
  update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
@@ -82,7 +82,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
82
82
 
83
83
  delete = record_subparsers.add_parser("delete", help="删除记录")
84
84
  delete.add_argument("--app-key", required=True)
85
- delete.add_argument("--record-id", type=int)
85
+ delete.add_argument("--record-id")
86
86
  delete.add_argument("--record-ids-file")
87
87
  delete.set_defaults(handler=_handle_delete, format_hint="")
88
88
 
@@ -102,7 +102,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
102
102
 
103
103
  code_block = record_subparsers.add_parser("code-block-run", help="执行代码块字段")
104
104
  code_block.add_argument("--app-key", required=True)
105
- code_block.add_argument("--record-id", required=True, type=int)
105
+ code_block.add_argument("--record-id", required=True)
106
106
  code_block.add_argument("--code-block-field", required=True)
107
107
  code_block.add_argument("--role", type=int, default=1)
108
108
  code_block.add_argument("--workflow-node-id", type=int)
@@ -25,7 +25,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
25
25
 
26
26
  get = task_subparsers.add_parser("get", help="读取待办详情")
27
27
  get.add_argument("--app-key", required=True)
28
- get.add_argument("--record-id", required=True, type=int)
28
+ get.add_argument("--record-id", required=True)
29
29
  get.add_argument("--workflow-node-id", required=True, type=int)
30
30
  get.add_argument("--include-candidates", action=argparse.BooleanOptionalAction, default=True)
31
31
  get.add_argument("--include-associated-reports", action=argparse.BooleanOptionalAction, default=True)
@@ -33,7 +33,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
33
33
 
34
34
  action = task_subparsers.add_parser("action", help="执行待办动作")
35
35
  action.add_argument("--app-key", required=True)
36
- action.add_argument("--record-id", required=True, type=int)
36
+ action.add_argument("--record-id", required=True)
37
37
  action.add_argument("--workflow-node-id", required=True, type=int)
38
38
  action.add_argument("--action", required=True)
39
39
  action.add_argument("--payload-file")
@@ -42,7 +42,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
42
42
 
43
43
  log = task_subparsers.add_parser("log", help="读取流程日志")
44
44
  log.add_argument("--app-key", required=True)
45
- log.add_argument("--record-id", required=True, type=int)
45
+ log.add_argument("--record-id", required=True)
46
46
  log.add_argument("--workflow-node-id", required=True, type=int)
47
47
  log.set_defaults(handler=_handle_log, format_hint="")
48
48
 
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .errors import QingflowApiError
6
+
7
+
8
+ JS_MAX_SAFE_INTEGER = 9_007_199_254_740_991
9
+
10
+
11
+ def stringify_backend_id(value: Any) -> str | None:
12
+ """Return an exact public id string for backend-originated identifiers."""
13
+ if value in (None, ""):
14
+ return None
15
+ if isinstance(value, bool):
16
+ return None
17
+ text = str(value).strip()
18
+ return text or None
19
+
20
+
21
+ def normalize_positive_id_text(value: Any, *, field_name: str) -> str:
22
+ """Normalize a user-supplied id while rejecting JS-unsafe numeric input."""
23
+ if value in (None, "") or isinstance(value, bool):
24
+ raise QingflowApiError.config_error(f"{field_name} must be positive")
25
+ if isinstance(value, int):
26
+ if value <= 0:
27
+ raise QingflowApiError.config_error(f"{field_name} must be positive")
28
+ if value > JS_MAX_SAFE_INTEGER:
29
+ raise QingflowApiError.config_error(
30
+ f"{field_name} exceeds JavaScript's safe integer range; pass it as a string to avoid precision loss"
31
+ )
32
+ return str(value)
33
+ if isinstance(value, str):
34
+ text = value.strip()
35
+ if not text.isdecimal() or int(text) <= 0:
36
+ raise QingflowApiError.config_error(f"{field_name} must be a positive integer string")
37
+ return text
38
+ raise QingflowApiError.config_error(f"{field_name} must be a positive integer string")
39
+
40
+
41
+ def normalize_positive_id_int(value: Any, *, field_name: str) -> int:
42
+ """Normalize an id to Python int after the public boundary preserves it as text."""
43
+ return int(normalize_positive_id_text(value, field_name=field_name))
44
+
45
+
46
+ def ids_equal(left: Any, right: Any) -> bool:
47
+ left_text = stringify_backend_id(left)
48
+ right_text = stringify_backend_id(right)
49
+ return left_text is not None and right_text is not None and left_text == right_text
@@ -263,6 +263,15 @@ def _trim_workspace_list(payload: JSONObject) -> None:
263
263
  _trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
264
264
 
265
265
 
266
+ def _trim_workspace_get(payload: JSONObject) -> None:
267
+ workspace = payload.get("workspace")
268
+ if isinstance(workspace, dict):
269
+ payload["workspace"] = _pick(
270
+ workspace,
271
+ allowed=("wsId", "workspaceName", "remark", "systemVersion", "auth"),
272
+ )
273
+
274
+
266
275
  def _trim_app_search_like(payload: JSONObject) -> None:
267
276
  payload.pop("apps", None)
268
277
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
@@ -728,6 +737,7 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
728
737
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
729
738
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
730
739
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
740
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get",), _trim_workspace_get)
731
741
  _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
732
742
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
733
743
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
@@ -93,6 +93,16 @@ def build_builder_server() -> FastMCP:
93
93
  include_external=include_external,
94
94
  )
95
95
 
96
+ @server.tool()
97
+ def workspace_get(
98
+ profile: str = DEFAULT_PROFILE,
99
+ ws_id: int | None = None,
100
+ ) -> dict:
101
+ return workspace.workspace_get(
102
+ profile=profile,
103
+ ws_id=ws_id,
104
+ )
105
+
96
106
  @server.tool()
97
107
  def file_upload_local(
98
108
  profile: str = DEFAULT_PROFILE,
@@ -228,6 +228,16 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
228
228
  include_external=include_external,
229
229
  )
230
230
 
231
+ @server.tool()
232
+ def workspace_get(
233
+ profile: str = DEFAULT_PROFILE,
234
+ ws_id: int | None = None,
235
+ ) -> dict:
236
+ return workspace.workspace_get(
237
+ profile=profile,
238
+ ws_id=ws_id,
239
+ )
240
+
231
241
  @server.tool()
232
242
  def app_list(profile: str = DEFAULT_PROFILE) -> dict:
233
243
  return apps.app_list(profile=profile)
@@ -91,7 +91,7 @@ class CodeBlockTools(RecordTools):
91
91
  def record_code_block_run(
92
92
  profile: str = DEFAULT_PROFILE,
93
93
  app_key: str = "",
94
- record_id: int = 0,
94
+ record_id: str = "",
95
95
  code_block_field: str = "",
96
96
  role: int = 1,
97
97
  workflow_node_id: int | None = None,
@@ -197,7 +197,7 @@ class CodeBlockTools(RecordTools):
197
197
  *,
198
198
  profile: str,
199
199
  app_key: str,
200
- record_id: int,
200
+ record_id: int | str,
201
201
  code_block_field: str,
202
202
  role: int = 1,
203
203
  workflow_node_id: int | None = None,
@@ -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,
@@ -284,7 +285,7 @@ class RecordTools(ToolBase):
284
285
  profile: str = DEFAULT_PROFILE,
285
286
  app_key: str = "",
286
287
  field_id: int = 0,
287
- record_id: int | None = None,
288
+ record_id: str | None = None,
288
289
  workflow_node_id: int | None = None,
289
290
  fields: JSONObject | None = None,
290
291
  keyword: str = "",
@@ -315,7 +316,7 @@ class RecordTools(ToolBase):
315
316
  profile: str = DEFAULT_PROFILE,
316
317
  app_key: str = "",
317
318
  field_id: int = 0,
318
- record_id: int | None = None,
319
+ record_id: str | None = None,
319
320
  workflow_node_id: int | None = None,
320
321
  fields: JSONObject | None = None,
321
322
  keyword: str = "",
@@ -407,7 +408,7 @@ class RecordTools(ToolBase):
407
408
  def record_get(
408
409
  profile: str = DEFAULT_PROFILE,
409
410
  app_key: str = "",
410
- record_id: int = 0,
411
+ record_id: str = "",
411
412
  columns: list[JSONObject | int] | None = None,
412
413
  view_id: str | None = None,
413
414
  workflow_node_id: int | None = None,
@@ -439,7 +440,7 @@ class RecordTools(ToolBase):
439
440
  @mcp.tool()
440
441
  def record_update_schema_get(
441
442
  app_key: str = "",
442
- record_id: int = 0,
443
+ record_id: str = "",
443
444
  output_profile: str = "normal",
444
445
  ) -> JSONObject:
445
446
  return self.record_update_schema_get_public(
@@ -479,7 +480,7 @@ class RecordTools(ToolBase):
479
480
  )
480
481
  def record_update(
481
482
  app_key: str = "",
482
- record_id: int | None = None,
483
+ record_id: str | None = None,
483
484
  fields: JSONObject | None = None,
484
485
  items: list[JSONObject] | None = None,
485
486
  dry_run: bool = False,
@@ -505,8 +506,8 @@ class RecordTools(ToolBase):
505
506
  )
506
507
  def record_delete(
507
508
  app_key: str = "",
508
- record_id: int | None = None,
509
- record_ids: list[int] | None = None,
509
+ record_id: str | None = None,
510
+ record_ids: list[str] | None = None,
510
511
  output_profile: str = "normal",
511
512
  ) -> JSONObject:
512
513
  return self.record_delete_public(
@@ -784,14 +785,13 @@ class RecordTools(ToolBase):
784
785
  *,
785
786
  profile: str = DEFAULT_PROFILE,
786
787
  app_key: str,
787
- record_id: int,
788
+ record_id: Any,
788
789
  output_profile: str = "normal",
789
790
  ) -> JSONObject:
790
791
  """执行记录相关逻辑。"""
791
792
  if not app_key:
792
793
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
793
- if record_id <= 0:
794
- raise_tool_error(QingflowApiError.config_error("record_id is required"))
794
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
795
795
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
796
796
 
797
797
  def runner(session_profile, context):
@@ -815,7 +815,7 @@ class RecordTools(ToolBase):
815
815
  probes = self._probe_candidate_record_contexts(
816
816
  context,
817
817
  app_key=app_key,
818
- apply_id=record_id,
818
+ apply_id=record_id_int,
819
819
  candidate_routes=candidate_routes,
820
820
  )
821
821
  probe_summary: list[JSONObject] = []
@@ -906,7 +906,7 @@ class RecordTools(ToolBase):
906
906
  ws_id=ws_id,
907
907
  request_route=request_route,
908
908
  app_key=app_key,
909
- record_id=record_id,
909
+ record_id=record_id_int,
910
910
  blockers=blockers,
911
911
  warnings=warnings,
912
912
  recommended_next_actions=recommended_next_actions,
@@ -949,7 +949,7 @@ class RecordTools(ToolBase):
949
949
  ws_id=ws_id,
950
950
  request_route=request_route,
951
951
  app_key=app_key,
952
- record_id=record_id,
952
+ record_id=record_id_int,
953
953
  blockers=["NO_WRITABLE_FIELDS_FOR_RECORD"],
954
954
  warnings=[item["message"] for item in warnings if isinstance(item.get("message"), str)],
955
955
  recommended_next_actions=[
@@ -969,7 +969,7 @@ class RecordTools(ToolBase):
969
969
  "request_route": request_route,
970
970
  "warnings": warnings,
971
971
  "app_key": app_key,
972
- "record_id": record_id,
972
+ "record_id": stringify_backend_id(record_id_int),
973
973
  "schema_scope": "update_ready",
974
974
  "writable_fields": writable_fields,
975
975
  "payload_template": {
@@ -1009,7 +1009,7 @@ class RecordTools(ToolBase):
1009
1009
  "request_route": request_route,
1010
1010
  "warnings": [{"code": "PREFLIGHT_WARNING", "message": item} for item in warnings],
1011
1011
  "app_key": app_key,
1012
- "record_id": record_id,
1012
+ "record_id": stringify_backend_id(record_id),
1013
1013
  "schema_scope": "update_ready",
1014
1014
  "blockers": blockers,
1015
1015
  "writable_fields": [],
@@ -1281,7 +1281,7 @@ class RecordTools(ToolBase):
1281
1281
  profile: str,
1282
1282
  app_key: str,
1283
1283
  field_id: int,
1284
- record_id: int | None = None,
1284
+ record_id: Any | None = None,
1285
1285
  workflow_node_id: int | None = None,
1286
1286
  fields: JSONObject | None = None,
1287
1287
  keyword: str,
@@ -1297,6 +1297,12 @@ class RecordTools(ToolBase):
1297
1297
  raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
1298
1298
  if page_size <= 0:
1299
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
1300
1306
 
1301
1307
  def runner(session_profile, context):
1302
1308
  index = self._get_field_index(profile, context, app_key, force_refresh=False)
@@ -1316,7 +1322,7 @@ class RecordTools(ToolBase):
1316
1322
  )
1317
1323
  normalized_fields = fields if isinstance(fields, dict) else {}
1318
1324
  runtime_lookup = self._candidate_lookup_uses_runtime_scope(
1319
- record_id=record_id,
1325
+ record_id=record_id_int,
1320
1326
  workflow_node_id=workflow_node_id,
1321
1327
  fields=normalized_fields,
1322
1328
  )
@@ -1327,7 +1333,7 @@ class RecordTools(ToolBase):
1327
1333
  profile,
1328
1334
  context,
1329
1335
  app_key=app_key,
1330
- record_id=record_id,
1336
+ record_id=record_id_int,
1331
1337
  workflow_node_id=workflow_node_id,
1332
1338
  fields=normalized_fields,
1333
1339
  )
@@ -1366,7 +1372,7 @@ class RecordTools(ToolBase):
1366
1372
  "app_key": app_key,
1367
1373
  "field_id": field.que_id,
1368
1374
  "field_title": field.que_title,
1369
- "record_id": record_id,
1375
+ "record_id": record_id_text,
1370
1376
  "workflow_node_id": workflow_node_id,
1371
1377
  "fields_present": bool(normalized_fields),
1372
1378
  "keyword": keyword,
@@ -1385,7 +1391,7 @@ class RecordTools(ToolBase):
1385
1391
  profile: str,
1386
1392
  app_key: str,
1387
1393
  field_id: int,
1388
- record_id: int | None = None,
1394
+ record_id: Any | None = None,
1389
1395
  workflow_node_id: int | None = None,
1390
1396
  fields: JSONObject | None = None,
1391
1397
  keyword: str,
@@ -1401,6 +1407,12 @@ class RecordTools(ToolBase):
1401
1407
  raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
1402
1408
  if page_size <= 0:
1403
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
1404
1416
 
1405
1417
  def runner(session_profile, context):
1406
1418
  index = self._get_field_index(profile, context, app_key, force_refresh=False)
@@ -1420,7 +1432,7 @@ class RecordTools(ToolBase):
1420
1432
  )
1421
1433
  normalized_fields = fields if isinstance(fields, dict) else {}
1422
1434
  runtime_lookup = self._candidate_lookup_uses_runtime_scope(
1423
- record_id=record_id,
1435
+ record_id=record_id_int,
1424
1436
  workflow_node_id=workflow_node_id,
1425
1437
  fields=normalized_fields,
1426
1438
  )
@@ -1431,7 +1443,7 @@ class RecordTools(ToolBase):
1431
1443
  profile,
1432
1444
  context,
1433
1445
  app_key=app_key,
1434
- record_id=record_id,
1446
+ record_id=record_id_int,
1435
1447
  workflow_node_id=workflow_node_id,
1436
1448
  fields=normalized_fields,
1437
1449
  )
@@ -1487,7 +1499,7 @@ class RecordTools(ToolBase):
1487
1499
  "app_key": app_key,
1488
1500
  "field_id": field.que_id,
1489
1501
  "field_title": field.que_title,
1490
- "record_id": record_id,
1502
+ "record_id": record_id_text,
1491
1503
  "workflow_node_id": workflow_node_id,
1492
1504
  "fields_present": bool(normalized_fields),
1493
1505
  "keyword": keyword,
@@ -1753,7 +1765,7 @@ class RecordTools(ToolBase):
1753
1765
  *,
1754
1766
  profile: str,
1755
1767
  app_key: str,
1756
- record_id: int,
1768
+ record_id: Any,
1757
1769
  columns: list[JSONObject | int],
1758
1770
  view_id: str | None = None,
1759
1771
  workflow_node_id: int | None = None,
@@ -1761,8 +1773,7 @@ class RecordTools(ToolBase):
1761
1773
  ) -> JSONObject:
1762
1774
  """执行记录相关逻辑。"""
1763
1775
  normalized_output_profile = self._normalize_public_output_profile(output_profile, allow_normalized=True)
1764
- if record_id <= 0:
1765
- 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")
1766
1777
  normalized_columns = _normalize_public_column_selectors(columns)
1767
1778
 
1768
1779
  def runner(session_profile, context):
@@ -1791,7 +1802,7 @@ class RecordTools(ToolBase):
1791
1802
  result = self.backend.request(
1792
1803
  "GET",
1793
1804
  context,
1794
- f"/view/{resolved_view.view_selection.view_key}/apply/{record_id}",
1805
+ f"/view/{resolved_view.view_selection.view_key}/apply/{record_id_int}",
1795
1806
  )
1796
1807
  used_list_type = None
1797
1808
  else:
@@ -1808,7 +1819,7 @@ class RecordTools(ToolBase):
1808
1819
  result = self.backend.request(
1809
1820
  "GET",
1810
1821
  context,
1811
- f"/app/{app_key}/apply/{record_id}",
1822
+ f"/app/{app_key}/apply/{record_id_int}",
1812
1823
  params={"role": 1, "listType": lt},
1813
1824
  )
1814
1825
  used_list_type = lt
@@ -1823,7 +1834,7 @@ class RecordTools(ToolBase):
1823
1834
  raise last_error
1824
1835
  raise_tool_error(QingflowApiError.config_error("record_get failed: no accessible listType"))
1825
1836
  answer_list = result.get("answers") if isinstance(result, dict) and isinstance(result.get("answers"), list) else []
1826
- 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)
1827
1838
  normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
1828
1839
  cast(list[JSONValue], answer_list),
1829
1840
  selected_fields,
@@ -1851,7 +1862,7 @@ class RecordTools(ToolBase):
1851
1862
  if normalized_columns
1852
1863
  else list(index.by_id.values())
1853
1864
  )
1854
- 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)
1855
1866
  normalized_record, normalized_ambiguous_fields = _build_normalized_row_from_answers(
1856
1867
  cast(list[JSONValue], answer_list),
1857
1868
  selected_fields,
@@ -1874,7 +1885,7 @@ class RecordTools(ToolBase):
1874
1885
  "output_profile": normalized_output_profile,
1875
1886
  "data": {
1876
1887
  "app_key": app_key,
1877
- "record_id": _public_record_id_text(record_id),
1888
+ "record_id": _public_record_id_text(record_id_int),
1878
1889
  "record": row,
1879
1890
  "selection": {
1880
1891
  "columns": [_column_selector_payload(field_id) for field_id in normalized_columns] if normalized_columns else [],
@@ -1975,7 +1986,7 @@ class RecordTools(ToolBase):
1975
1986
  *,
1976
1987
  profile: str = DEFAULT_PROFILE,
1977
1988
  app_key: str,
1978
- record_id: int | None,
1989
+ record_id: Any | None,
1979
1990
  fields: JSONObject | None = None,
1980
1991
  items: list[JSONObject] | None = None,
1981
1992
  dry_run: bool = False,
@@ -2004,14 +2015,15 @@ class RecordTools(ToolBase):
2004
2015
  )
2005
2016
  if dry_run:
2006
2017
  raise_tool_error(QingflowApiError.config_error("dry_run currently requires items"))
2007
- if record_id is None or record_id <= 0:
2018
+ if record_id is None:
2008
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")
2009
2021
  if fields is not None and not isinstance(fields, dict):
2010
2022
  raise_tool_error(QingflowApiError.config_error("fields must be an object map keyed by field title"))
2011
2023
  return self._record_update_public_single(
2012
2024
  profile=profile,
2013
2025
  app_key=app_key,
2014
- record_id=record_id,
2026
+ record_id=record_id_int,
2015
2027
  fields=cast(JSONObject, fields or {}),
2016
2028
  verify_write=verify_write,
2017
2029
  output_profile=normalized_output_profile,
@@ -2201,7 +2213,7 @@ class RecordTools(ToolBase):
2201
2213
  def _normalize_public_record_update_batch_items(
2202
2214
  self,
2203
2215
  *,
2204
- record_id: int | None,
2216
+ record_id: Any | None,
2205
2217
  fields: JSONObject | None,
2206
2218
  items: list[JSONObject] | None,
2207
2219
  ) -> list[JSONObject]:
@@ -2217,9 +2229,10 @@ class RecordTools(ToolBase):
2217
2229
  for index, item in enumerate(items):
2218
2230
  if not isinstance(item, dict):
2219
2231
  raise_tool_error(QingflowApiError.config_error(f"items[{index}] must be an object"))
2220
- normalized_record_id = _coerce_count(item.get("record_id"))
2221
- if normalized_record_id is None or normalized_record_id <= 0:
2222
- 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
+ )
2223
2236
  if normalized_record_id in seen_record_ids:
2224
2237
  raise_tool_error(
2225
2238
  QingflowApiError.config_error(f"duplicate record_id in items: {normalized_record_id}")
@@ -3069,22 +3082,26 @@ class RecordTools(ToolBase):
3069
3082
  *,
3070
3083
  profile: str = DEFAULT_PROFILE,
3071
3084
  app_key: str,
3072
- record_id: int | None = None,
3073
- record_ids: list[int] | None = None,
3085
+ record_id: Any | None = None,
3086
+ record_ids: list[Any] | None = None,
3074
3087
  output_profile: str = "normal",
3075
3088
  ) -> JSONObject:
3076
3089
  """执行记录相关逻辑。"""
3077
3090
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
3078
3091
  if not app_key:
3079
3092
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
3080
- normalized_record_ids = [item for item in (record_ids or []) if isinstance(item, int) and item > 0]
3081
- 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")]
3082
3099
  if not delete_ids:
3083
3100
  raise_tool_error(QingflowApiError.config_error("record_id or record_ids is required"))
3084
3101
  normalized_payload = {
3085
3102
  "operation": "delete",
3086
- "record_id": record_id,
3087
- "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],
3088
3105
  "answers": [],
3089
3106
  "submit_type": 1,
3090
3107
  }
@@ -8527,8 +8544,8 @@ class RecordTools(ToolBase):
8527
8544
  """执行内部辅助逻辑。"""
8528
8545
  payload: JSONObject = {
8529
8546
  "operation": operation,
8530
- "record_id": record_id,
8531
- "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],
8532
8549
  "answers": normalized_answers,
8533
8550
  "submit_type": submit_type,
8534
8551
  }
@@ -8727,7 +8744,7 @@ class RecordTools(ToolBase):
8727
8744
  "output_profile": output_profile,
8728
8745
  "data": {
8729
8746
  "action": {"operation": operation, "executed": True},
8730
- "resource": raw_apply.get("resource"),
8747
+ "resource": _public_record_resource(raw_apply.get("resource")),
8731
8748
  "verification": raw_apply.get("verification"),
8732
8749
  "normalized_payload": normalized_payload,
8733
8750
  "blockers": [],
@@ -10072,8 +10089,9 @@ class RecordTools(ToolBase):
10072
10089
  """执行内部辅助逻辑。"""
10073
10090
  if not app_key:
10074
10091
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
10075
- normalized_apply_id = _coerce_count(apply_id)
10076
- 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:
10077
10095
  raise_tool_error(QingflowApiError.config_error("apply_id must be positive"))
10078
10096
  return normalized_apply_id
10079
10097
 
@@ -11084,10 +11102,14 @@ def _build_flat_row(answer_list: list[JSONValue], fields: list[FormField], *, ap
11084
11102
  return row
11085
11103
 
11086
11104
 
11087
- def _public_record_id_text(record_id: int | None) -> str | None:
11088
- 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):
11089
11107
  return None
11090
- return str(record_id)
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()):
11111
+ return None
11112
+ return stringify_backend_id(record_id)
11091
11113
 
11092
11114
 
11093
11115
  def _normalize_public_record_rows(rows: list[JSONValue]) -> list[JSONObject]:
@@ -11640,10 +11662,29 @@ def _field_mapping_entry(role: str, field: FormField | None, *, requested: str)
11640
11662
  }
11641
11663
 
11642
11664
 
11643
- def _record_resource_payload(record_id: int | None) -> JSONObject | None:
11644
- 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:
11645
11668
  return None
11646
- 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
11647
11688
 
11648
11689
 
11649
11690
  def _query_id() -> str:
@@ -10,6 +10,7 @@ from mcp.server.fastmcp import FastMCP
10
10
  from ..backend_client import BackendRequestContext
11
11
  from ..config import DEFAULT_PROFILE
12
12
  from ..errors import QingflowApiError, raise_tool_error
13
+ from ..id_utils import ids_equal, normalize_positive_id_int, stringify_backend_id
13
14
  from ..json_types import JSONObject
14
15
  from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page_items, _approval_page_total
15
16
  from .base import ToolBase, tool_cn_name
@@ -87,7 +88,7 @@ class TaskContextTools(ToolBase):
87
88
  def task_get(
88
89
  profile: str = DEFAULT_PROFILE,
89
90
  app_key: str = "",
90
- record_id: int = 0,
91
+ record_id: str = "",
91
92
  workflow_node_id: int = 0,
92
93
  include_candidates: bool = True,
93
94
  include_associated_reports: bool = True,
@@ -105,7 +106,7 @@ class TaskContextTools(ToolBase):
105
106
  def task_action_execute(
106
107
  profile: str = DEFAULT_PROFILE,
107
108
  app_key: str = "",
108
- record_id: int = 0,
109
+ record_id: str = "",
109
110
  workflow_node_id: int = 0,
110
111
  action: str = "",
111
112
  payload: dict[str, Any] | None = None,
@@ -125,7 +126,7 @@ class TaskContextTools(ToolBase):
125
126
  def task_associated_report_detail_get(
126
127
  profile: str = DEFAULT_PROFILE,
127
128
  app_key: str = "",
128
- record_id: int = 0,
129
+ record_id: str = "",
129
130
  workflow_node_id: int = 0,
130
131
  report_id: int = 0,
131
132
  page: int = 1,
@@ -145,7 +146,7 @@ class TaskContextTools(ToolBase):
145
146
  def task_workflow_log_get(
146
147
  profile: str = DEFAULT_PROFILE,
147
148
  app_key: str = "",
148
- record_id: int = 0,
149
+ record_id: str = "",
149
150
  workflow_node_id: int = 0,
150
151
  ) -> dict[str, Any]:
151
152
  return self.task_workflow_log_get(
@@ -249,20 +250,21 @@ class TaskContextTools(ToolBase):
249
250
  *,
250
251
  profile: str,
251
252
  app_key: str,
252
- record_id: int,
253
+ record_id: Any,
253
254
  workflow_node_id: int,
254
255
  include_candidates: bool,
255
256
  include_associated_reports: bool,
256
257
  ) -> dict[str, Any]:
257
258
  """执行任务相关逻辑。"""
258
- self._require_app_record_and_node(app_key, record_id, workflow_node_id)
259
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
260
+ self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
259
261
 
260
262
  def runner(session_profile, context):
261
263
  data = self._build_task_context(
262
264
  profile=profile,
263
265
  context=context,
264
266
  app_key=app_key,
265
- record_id=record_id,
267
+ record_id=record_id_int,
266
268
  workflow_node_id=workflow_node_id,
267
269
  include_candidates=include_candidates,
268
270
  include_associated_reports=include_associated_reports,
@@ -287,7 +289,7 @@ class TaskContextTools(ToolBase):
287
289
  *,
288
290
  profile: str,
289
291
  app_key: str,
290
- record_id: int,
292
+ record_id: Any,
291
293
  workflow_node_id: int,
292
294
  fields: dict[str, Any] | None = None,
293
295
  ) -> dict[str, Any]:
@@ -311,14 +313,16 @@ class TaskContextTools(ToolBase):
311
313
  *,
312
314
  profile: str,
313
315
  app_key: str,
314
- record_id: int,
316
+ record_id: Any,
315
317
  workflow_node_id: int,
316
318
  action: str,
317
319
  payload: dict[str, Any],
318
320
  fields: dict[str, Any] | None = None,
319
321
  ) -> dict[str, Any]:
320
322
  """执行任务相关逻辑。"""
321
- self._require_app_record_and_node(app_key, record_id, workflow_node_id)
323
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
324
+ record_id_text = stringify_backend_id(record_id_int)
325
+ self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
322
326
  normalized_action = (action or "").strip().lower()
323
327
  if normalized_action not in {"approve", "reject", "rollback", "transfer", "urge", "save_only"}:
324
328
  raise_tool_error(
@@ -341,7 +345,7 @@ class TaskContextTools(ToolBase):
341
345
  profile=profile,
342
346
  context=context,
343
347
  app_key=app_key,
344
- record_id=record_id,
348
+ record_id=record_id_int,
345
349
  workflow_node_id=workflow_node_id,
346
350
  include_candidates=False,
347
351
  include_associated_reports=False,
@@ -354,7 +358,7 @@ class TaskContextTools(ToolBase):
354
358
  session_profile=session_profile,
355
359
  context=context,
356
360
  app_key=app_key,
357
- record_id=record_id,
361
+ record_id=record_id_int,
358
362
  workflow_node_id=workflow_node_id,
359
363
  action=normalized_action,
360
364
  source_error=error,
@@ -387,7 +391,7 @@ class TaskContextTools(ToolBase):
387
391
  raise_tool_error(QingflowApiError.config_error(message))
388
392
  raise_tool_error(
389
393
  QingflowApiError.config_error(
390
- f"task action '{normalized_action}' is not currently available for app_key='{app_key}' record_id={record_id} workflow_node_id={workflow_node_id}"
394
+ f"task action '{normalized_action}' is not currently available for app_key='{app_key}' record_id={record_id_text} workflow_node_id={workflow_node_id}"
391
395
  )
392
396
  )
393
397
  feedback_required_for = capabilities.get("action_constraints", {}).get("feedback_required_for") or []
@@ -409,7 +413,7 @@ class TaskContextTools(ToolBase):
409
413
  profile=profile,
410
414
  context=context,
411
415
  app_key=app_key,
412
- record_id=record_id,
416
+ record_id=record_id_int,
413
417
  workflow_node_id=workflow_node_id,
414
418
  task_context=task_context,
415
419
  fields=field_updates,
@@ -421,14 +425,14 @@ class TaskContextTools(ToolBase):
421
425
  profile=profile,
422
426
  context=context,
423
427
  app_key=app_key,
424
- record_id=record_id,
428
+ record_id=record_id_int,
425
429
  workflow_node_id=workflow_node_id,
426
430
  )
427
431
  try:
428
432
  raw = self._execute_task_action(
429
433
  profile=profile,
430
434
  app_key=app_key,
431
- record_id=record_id,
435
+ record_id=record_id_int,
432
436
  workflow_node_id=workflow_node_id,
433
437
  normalized_action=normalized_action,
434
438
  payload=body,
@@ -441,7 +445,7 @@ class TaskContextTools(ToolBase):
441
445
  session_profile=session_profile,
442
446
  context=context,
443
447
  app_key=app_key,
444
- record_id=record_id,
448
+ record_id=record_id_int,
445
449
  workflow_node_id=workflow_node_id,
446
450
  action=normalized_action,
447
451
  source_error=error,
@@ -453,7 +457,7 @@ class TaskContextTools(ToolBase):
453
457
  verification, warnings = self._verify_task_save_only(
454
458
  context=context,
455
459
  app_key=app_key,
456
- record_id=record_id,
460
+ record_id=record_id_int,
457
461
  workflow_node_id=workflow_node_id,
458
462
  before_apply_status=before_apply_status,
459
463
  expected_answers=((prepared_fields or {}).get("normalized_answers") or []),
@@ -467,7 +471,7 @@ class TaskContextTools(ToolBase):
467
471
  profile=profile,
468
472
  context=context,
469
473
  app_key=app_key,
470
- record_id=record_id,
474
+ record_id=record_id_int,
471
475
  workflow_node_id=workflow_node_id,
472
476
  action=normalized_action,
473
477
  before_apply_status=before_apply_status,
@@ -490,7 +494,7 @@ class TaskContextTools(ToolBase):
490
494
  "action": normalized_action,
491
495
  "resource": {
492
496
  "app_key": app_key,
493
- "record_id": record_id,
497
+ "record_id": record_id_text,
494
498
  "workflow_node_id": workflow_node_id,
495
499
  },
496
500
  "selection": {"action": normalized_action},
@@ -507,7 +511,7 @@ class TaskContextTools(ToolBase):
507
511
  *,
508
512
  profile: str,
509
513
  app_key: str,
510
- record_id: int,
514
+ record_id: Any,
511
515
  workflow_node_id: int,
512
516
  normalized_action: str,
513
517
  payload: dict[str, Any],
@@ -671,12 +675,12 @@ class TaskContextTools(ToolBase):
671
675
  todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
672
676
  initiated_items = self._safe_task_list_items(profile=profile, task_box="initiated", app_key=app_key)
673
677
  downstream_todo_detected = any(
674
- int(item.get("record_id") or 0) == record_id and int(item.get("workflow_node_id") or 0) != workflow_node_id
678
+ ids_equal(item.get("record_id"), record_id) and int(item.get("workflow_node_id") or 0) != workflow_node_id
675
679
  for item in todo_items
676
680
  if isinstance(item, dict)
677
681
  )
678
682
  initiated_visible = any(
679
- int(item.get("record_id") or 0) == record_id
683
+ ids_equal(item.get("record_id"), record_id)
680
684
  for item in initiated_items
681
685
  if isinstance(item, dict)
682
686
  )
@@ -693,7 +697,7 @@ class TaskContextTools(ToolBase):
693
697
  int(item.get("workflow_node_id") or 0)
694
698
  for item in todo_items
695
699
  if isinstance(item, dict)
696
- and int(item.get("record_id") or 0) == record_id
700
+ and ids_equal(item.get("record_id"), record_id)
697
701
  and int(item.get("workflow_node_id") or 0) != workflow_node_id
698
702
  }
699
703
  workflow_log_digest = self._workflow_log_digest(log_items)
@@ -898,7 +902,7 @@ class TaskContextTools(ToolBase):
898
902
  int(item.get("workflow_node_id") or 0)
899
903
  for item in todo_items
900
904
  if isinstance(item, dict)
901
- and int(item.get("record_id") or 0) == record_id
905
+ and ids_equal(item.get("record_id"), record_id)
902
906
  and int(item.get("workflow_node_id") or 0) != workflow_node_id
903
907
  }
904
908
  )
@@ -918,6 +922,7 @@ class TaskContextTools(ToolBase):
918
922
  before_apply_status: Any,
919
923
  ) -> dict[str, Any]:
920
924
  """执行内部辅助逻辑。"""
925
+ record_id_text = stringify_backend_id(record_id)
921
926
  verification, warnings = self._verify_task_action_runtime(
922
927
  profile=profile,
923
928
  context=context,
@@ -950,7 +955,7 @@ class TaskContextTools(ToolBase):
950
955
  "action": action,
951
956
  "resource": {
952
957
  "app_key": app_key,
953
- "record_id": record_id,
958
+ "record_id": record_id_text,
954
959
  "workflow_node_id": workflow_node_id,
955
960
  },
956
961
  "selection": {"action": action},
@@ -978,7 +983,7 @@ class TaskContextTools(ToolBase):
978
983
  "action": action,
979
984
  "resource": {
980
985
  "app_key": app_key,
981
- "record_id": record_id,
986
+ "record_id": record_id_text,
982
987
  "workflow_node_id": workflow_node_id,
983
988
  },
984
989
  "selection": {"action": action},
@@ -1101,7 +1106,9 @@ class TaskContextTools(ToolBase):
1101
1106
  page_size: int,
1102
1107
  ) -> dict[str, Any]:
1103
1108
  """执行任务相关逻辑。"""
1104
- self._require_app_record_and_node(app_key, record_id, workflow_node_id)
1109
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1110
+ record_id_text = stringify_backend_id(record_id_int)
1111
+ self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
1105
1112
  if report_id <= 0:
1106
1113
  raise_tool_error(QingflowApiError.config_error("report_id must be positive"))
1107
1114
  if page <= 0 or page_size <= 0:
@@ -1112,7 +1119,7 @@ class TaskContextTools(ToolBase):
1112
1119
  profile=profile,
1113
1120
  context=context,
1114
1121
  app_key=app_key,
1115
- record_id=record_id,
1122
+ record_id=record_id_int,
1116
1123
  workflow_node_id=workflow_node_id,
1117
1124
  include_candidates=False,
1118
1125
  include_associated_reports=True,
@@ -1122,7 +1129,7 @@ class TaskContextTools(ToolBase):
1122
1129
  if report_item is None:
1123
1130
  raise_tool_error(
1124
1131
  QingflowApiError.config_error(
1125
- f"report_id={report_id} is not visible for app_key='{app_key}' record_id={record_id} workflow_node_id={workflow_node_id}"
1132
+ f"report_id={report_id} is not visible for app_key='{app_key}' record_id={record_id_text} workflow_node_id={workflow_node_id}"
1126
1133
  )
1127
1134
  )
1128
1135
  association_query = self._build_association_query(
@@ -1131,7 +1138,7 @@ class TaskContextTools(ToolBase):
1131
1138
  )
1132
1139
  selection = {
1133
1140
  "app_key": app_key,
1134
- "record_id": record_id,
1141
+ "record_id": record_id_text,
1135
1142
  "workflow_node_id": workflow_node_id,
1136
1143
  "report_id": report_id,
1137
1144
  "target_app_key": report_item.get("target_app_key"),
@@ -1286,18 +1293,20 @@ class TaskContextTools(ToolBase):
1286
1293
  *,
1287
1294
  profile: str,
1288
1295
  app_key: str,
1289
- record_id: int,
1296
+ record_id: Any,
1290
1297
  workflow_node_id: int,
1291
1298
  ) -> dict[str, Any]:
1292
1299
  """执行任务相关逻辑。"""
1293
- self._require_app_record_and_node(app_key, record_id, workflow_node_id)
1300
+ record_id_int = normalize_positive_id_int(record_id, field_name="record_id")
1301
+ record_id_text = stringify_backend_id(record_id_int)
1302
+ self._require_app_record_and_node(app_key, record_id_int, workflow_node_id)
1294
1303
 
1295
1304
  def runner(session_profile, context):
1296
1305
  task_context = self._build_task_context(
1297
1306
  profile=profile,
1298
1307
  context=context,
1299
1308
  app_key=app_key,
1300
- record_id=record_id,
1309
+ record_id=record_id_int,
1301
1310
  workflow_node_id=workflow_node_id,
1302
1311
  include_candidates=False,
1303
1312
  include_associated_reports=False,
@@ -1307,7 +1316,7 @@ class TaskContextTools(ToolBase):
1307
1316
  if not visibility.get("audit_record_visible"):
1308
1317
  raise_tool_error(
1309
1318
  QingflowApiError.config_error(
1310
- f"workflow logs are not visible for app_key='{app_key}' record_id={record_id} workflow_node_id={workflow_node_id}"
1319
+ f"workflow logs are not visible for app_key='{app_key}' record_id={record_id_text} workflow_node_id={workflow_node_id}"
1311
1320
  )
1312
1321
  )
1313
1322
  page = self.backend.request(
@@ -1316,7 +1325,7 @@ class TaskContextTools(ToolBase):
1316
1325
  "/application/workflow/node/record",
1317
1326
  json_body={
1318
1327
  "key": app_key,
1319
- "rowRecordId": record_id,
1328
+ "rowRecordId": record_id_int,
1320
1329
  "nodeId": workflow_node_id,
1321
1330
  "role": 3,
1322
1331
  "pageNum": 1,
@@ -1334,7 +1343,7 @@ class TaskContextTools(ToolBase):
1334
1343
  "data": {
1335
1344
  "selection": {
1336
1345
  "app_key": app_key,
1337
- "record_id": record_id,
1346
+ "record_id": record_id_text,
1338
1347
  "workflow_node_id": workflow_node_id,
1339
1348
  },
1340
1349
  "visibility": {
@@ -1443,11 +1452,12 @@ class TaskContextTools(ToolBase):
1443
1452
  save_only_source=save_only_source,
1444
1453
  )
1445
1454
  visibility = self._build_visibility(node_info, detail)
1455
+ record_id_text = stringify_backend_id(record_id)
1446
1456
  return {
1447
1457
  "task": {
1448
1458
  "app_key": app_key,
1449
1459
  "app_name": app_name,
1450
- "record_id": record_id,
1460
+ "record_id": record_id_text,
1451
1461
  "workflow_node_id": workflow_node_id,
1452
1462
  "workflow_node_name": node_info.get("auditNodeName") or node_info.get("nodeName"),
1453
1463
  "actionable": True,
@@ -1458,7 +1468,7 @@ class TaskContextTools(ToolBase):
1458
1468
  "raw": dict(node_info),
1459
1469
  },
1460
1470
  "record": {
1461
- "apply_id": detail.get("applyId", record_id),
1471
+ "apply_id": stringify_backend_id(detail.get("applyId") or record_id),
1462
1472
  "apply_status": detail.get("applyStatus"),
1463
1473
  "apply_num": detail.get("applyNum"),
1464
1474
  "custom_apply_num": detail.get("customApplyNum"),
@@ -1526,7 +1536,7 @@ class TaskContextTools(ToolBase):
1526
1536
  "task": {
1527
1537
  "app_key": task.get("app_key"),
1528
1538
  "app_name": task.get("app_name"),
1529
- "record_id": task.get("record_id"),
1539
+ "record_id": stringify_backend_id(task.get("record_id")),
1530
1540
  "workflow_node_id": task.get("workflow_node_id"),
1531
1541
  "workflow_node_name": task.get("workflow_node_name"),
1532
1542
  "initiator": self._compact_initiator(record.get("apply_user")),
@@ -1722,10 +1732,10 @@ class TaskContextTools(ToolBase):
1722
1732
  record_id = raw.get("rowRecordId") or raw.get("recordId") or raw.get("applyId")
1723
1733
  workflow_node_id = raw.get("nodeId") or raw.get("auditNodeId")
1724
1734
  return {
1725
- "task_id": raw.get("id") or raw.get("taskId") or record_id,
1735
+ "task_id": stringify_backend_id(raw.get("id") or raw.get("taskId") or record_id),
1726
1736
  "app_key": app_key,
1727
1737
  "app_name": raw.get("formTitle") or raw.get("worksheetName") or raw.get("appName"),
1728
- "record_id": record_id,
1738
+ "record_id": stringify_backend_id(record_id),
1729
1739
  "workflow_node_id": workflow_node_id,
1730
1740
  "workflow_node_name": raw.get("nodeName") or raw.get("auditNodeName"),
1731
1741
  "apply_time": raw.get("applyTime") or raw.get("receiveTime"),
@@ -1816,6 +1826,7 @@ class TaskContextTools(ToolBase):
1816
1826
  current_answers: Any,
1817
1827
  ) -> dict[str, Any]:
1818
1828
  """执行内部辅助逻辑。"""
1829
+ record_id_text = stringify_backend_id(record_id)
1819
1830
  try:
1820
1831
  app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
1821
1832
  except QingflowApiError as error:
@@ -1832,7 +1843,7 @@ class TaskContextTools(ToolBase):
1832
1843
  ],
1833
1844
  "selection": {
1834
1845
  "app_key": app_key,
1835
- "record_id": record_id,
1846
+ "record_id": record_id_text,
1836
1847
  "workflow_node_id": workflow_node_id,
1837
1848
  },
1838
1849
  "transport_error": {
@@ -1917,7 +1928,7 @@ class TaskContextTools(ToolBase):
1917
1928
  "warnings": schema_warnings,
1918
1929
  "selection": {
1919
1930
  "app_key": app_key,
1920
- "record_id": record_id,
1931
+ "record_id": record_id_text,
1921
1932
  "workflow_node_id": workflow_node_id,
1922
1933
  },
1923
1934
  }