@josephyan/qingflow-cli 0.2.0-beta.66 → 0.2.0-beta.68

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.
@@ -12,10 +12,20 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
12
12
 
13
13
  package = builder_subparsers.add_parser("package", help="应用包")
14
14
  package_subparsers = package.add_subparsers(dest="builder_package_command", required=True)
15
+ package_list = package_subparsers.add_parser("list", help="列出应用包")
16
+ package_list.add_argument("--trial-status", default="all")
17
+ package_list.set_defaults(handler=_handle_package_list, format_hint="builder_summary")
18
+
15
19
  package_resolve = package_subparsers.add_parser("resolve", help="解析应用包")
16
20
  package_resolve.add_argument("--package-name", required=True)
17
21
  package_resolve.set_defaults(handler=_handle_package_resolve, format_hint="builder_summary")
18
22
 
23
+ package_create = package_subparsers.add_parser("create", help="创建应用包")
24
+ package_create.add_argument("--package-name", required=True)
25
+ package_create.add_argument("--icon")
26
+ package_create.add_argument("--color")
27
+ package_create.set_defaults(handler=_handle_package_create, format_hint="builder_summary")
28
+
19
29
  app = builder_subparsers.add_parser("app", help="应用")
20
30
  app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
21
31
 
@@ -119,10 +129,23 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
119
129
  publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary")
120
130
 
121
131
 
132
+ def _handle_package_list(args: argparse.Namespace, context: CliContext) -> dict:
133
+ return context.builder.package_list(profile=args.profile, trial_status=args.trial_status)
134
+
135
+
122
136
  def _handle_package_resolve(args: argparse.Namespace, context: CliContext) -> dict:
123
137
  return context.builder.package_resolve(profile=args.profile, package_name=args.package_name)
124
138
 
125
139
 
140
+ def _handle_package_create(args: argparse.Namespace, context: CliContext) -> dict:
141
+ return context.builder.package_create(
142
+ profile=args.profile,
143
+ package_name=args.package_name,
144
+ icon=args.icon,
145
+ color=args.color,
146
+ )
147
+
148
+
126
149
  def _handle_app_resolve(args: argparse.Namespace, context: CliContext) -> dict:
127
150
  return context.builder.app_resolve(
128
151
  profile=args.profile,
@@ -77,6 +77,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
77
77
  code_block.add_argument("--answers-file")
78
78
  code_block.add_argument("--fields-file")
79
79
  code_block.add_argument("--manual", action=argparse.BooleanOptionalAction, default=True)
80
+ code_block.add_argument("--apply-writeback", action=argparse.BooleanOptionalAction, default=True)
80
81
  code_block.add_argument("--verify-writeback", action=argparse.BooleanOptionalAction, default=True)
81
82
  code_block.add_argument("--force-refresh-form", action="store_true")
82
83
  code_block.set_defaults(handler=_handle_code_block_run, format_hint="")
@@ -197,6 +198,7 @@ def _handle_code_block_run(args: argparse.Namespace, context: CliContext) -> dic
197
198
  answers=load_list_arg(args.answers_file, option_name="--answers-file"),
198
199
  fields=load_object_arg(args.fields_file, option_name="--fields-file") or {},
199
200
  manual=bool(args.manual),
201
+ apply_writeback=bool(args.apply_writeback),
200
202
  verify_writeback=bool(args.verify_writeback),
201
203
  force_refresh_form=bool(args.force_refresh_form),
202
204
  )
@@ -132,8 +132,9 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
132
132
  - Always resolve the exact code-block field from `record_code_block_schema_get` first.
133
133
  - Treat code-block execution as write-capable, not read-only.
134
134
  - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
135
+ - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
135
136
  - In workflow context, pass `role=3` and the exact `workflow_node_id`.
136
- - After execution, inspect `outputs.alias_results`, `relation.target_fields`, and `writeback.verification` before claiming success.
137
+ - After execution, inspect `outputs.configured_aliases`, `outputs.alias_results`, `outputs.alias_map`, `relation.target_fields`, and `writeback.verification` before claiming success.
137
138
 
138
139
  ## Import Path
139
140
 
@@ -138,8 +138,13 @@ def build_builder_server() -> FastMCP:
138
138
  return ai_builder.builder_tool_contract(tool_name=tool_name)
139
139
 
140
140
  @server.tool()
141
- def package_create(profile: str = DEFAULT_PROFILE, package_name: str = "") -> dict:
142
- return ai_builder.package_create(profile=profile, package_name=package_name)
141
+ def package_create(
142
+ profile: str = DEFAULT_PROFILE,
143
+ package_name: str = "",
144
+ icon: str | None = None,
145
+ color: str | None = None,
146
+ ) -> dict:
147
+ return ai_builder.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
143
148
 
144
149
  @server.tool()
145
150
  def member_search(
@@ -121,8 +121,9 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
121
121
  - Always resolve the exact code-block field from `record_code_block_schema_get` first.
122
122
  - Treat code-block execution as write-capable, not read-only.
123
123
  - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
124
+ - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
124
125
  - In workflow context, pass `role=3` and the exact `workflow_node_id`.
125
- - After execution, inspect `outputs.alias_results`, `relation.target_fields`, and `writeback.verification` before claiming success.
126
+ - After execution, inspect `outputs.configured_aliases`, `outputs.alias_results`, `outputs.alias_map`, `relation.target_fields`, and `writeback.verification` before claiming success.
126
127
 
127
128
  ## Import Path
128
129
 
@@ -23,6 +23,8 @@ QUESTION_TYPE_MAP = {
23
23
  FieldType.address: 21,
24
24
  FieldType.attachment: 13,
25
25
  FieldType.boolean: 10,
26
+ FieldType.q_linker: 20,
27
+ FieldType.code_block: 26,
26
28
  FieldType.relation: 25,
27
29
  FieldType.subtable: 18,
28
30
  }
@@ -243,6 +245,33 @@ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any],
243
245
  "queDefaultType": 2,
244
246
  }
245
247
  )
248
+ if field_type == FieldType.code_block:
249
+ question.update(
250
+ {
251
+ "minOpts": -1,
252
+ "maxOpts": -1,
253
+ "codeBlockConfig": {
254
+ "configMode": int(config.get("config_mode") or 1),
255
+ "codeContent": str(config.get("code_content") or ""),
256
+ "resultAliasPath": deepcopy(config.get("result_alias_path") or []),
257
+ "beingHideOnForm": bool(config.get("being_hide_on_form", False)),
258
+ },
259
+ "autoTrigger": bool(config.get("auto_trigger", False)),
260
+ "customBtnTextStatus": bool(config.get("custom_button_text_enabled", False)),
261
+ "customBtnText": str(config.get("custom_button_text") or ""),
262
+ }
263
+ )
264
+ if field_type == FieldType.q_linker:
265
+ question.update(
266
+ {
267
+ "minOpts": -1,
268
+ "maxOpts": -1,
269
+ "remoteLookupConfig": deepcopy(config.get("remote_lookup_config") or config),
270
+ "autoTrigger": bool(config.get("auto_trigger", False)),
271
+ "customBtnTextStatus": bool(config.get("custom_button_text_enabled", False)),
272
+ "customBtnText": str(config.get("custom_button_text") or ""),
273
+ }
274
+ )
246
275
  if field_type == FieldType.subtable:
247
276
  sub_questions: list[dict[str, Any]] = []
248
277
  for subfield in field.get("subfields", []):
@@ -33,6 +33,8 @@ class FieldType(str, Enum):
33
33
  address = "address"
34
34
  attachment = "attachment"
35
35
  boolean = "boolean"
36
+ q_linker = "q_linker"
37
+ code_block = "code_block"
36
38
  relation = "relation"
37
39
  subtable = "subtable"
38
40
 
@@ -82,8 +82,13 @@ class AiBuilderTools(ToolBase):
82
82
  return self.builder_tool_contract(tool_name=tool_name)
83
83
 
84
84
  @mcp.tool()
85
- def package_create(profile: str = DEFAULT_PROFILE, package_name: str = "") -> JSONObject:
86
- return self.package_create(profile=profile, package_name=package_name)
85
+ def package_create(
86
+ profile: str = DEFAULT_PROFILE,
87
+ package_name: str = "",
88
+ icon: str | None = None,
89
+ color: str | None = None,
90
+ ) -> JSONObject:
91
+ return self.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
87
92
 
88
93
  @mcp.tool()
89
94
  def member_search(
@@ -425,13 +430,32 @@ class AiBuilderTools(ToolBase):
425
430
  "contract": contract,
426
431
  }
427
432
 
428
- def package_create(self, *, profile: str, package_name: str) -> JSONObject:
429
- normalized_args = {"package_name": package_name}
433
+ def package_create(
434
+ self,
435
+ *,
436
+ profile: str,
437
+ package_name: str,
438
+ icon: str | None = None,
439
+ color: str | None = None,
440
+ ) -> JSONObject:
441
+ normalized_args = {
442
+ "package_name": package_name,
443
+ **({"icon": icon} if icon else {}),
444
+ **({"color": color} if color else {}),
445
+ }
430
446
  return _safe_tool_call(
431
- lambda: self._facade.package_create(profile=profile, package_name=package_name),
447
+ lambda: self._facade.package_create(profile=profile, package_name=package_name, icon=icon, color=color),
432
448
  error_code="PACKAGE_CREATE_FAILED",
433
449
  normalized_args=normalized_args,
434
- suggested_next_call={"tool_name": "package_create", "arguments": {"profile": profile, "package_name": package_name}},
450
+ suggested_next_call={
451
+ "tool_name": "package_create",
452
+ "arguments": {
453
+ "profile": profile,
454
+ "package_name": package_name,
455
+ **({"icon": icon} if icon else {}),
456
+ **({"color": color} if color else {}),
457
+ },
458
+ },
435
459
  )
436
460
 
437
461
  def member_search(
@@ -795,15 +819,21 @@ class AiBuilderTools(ToolBase):
795
819
  "profile": profile,
796
820
  "app_key": app_key,
797
821
  "mode": "merge",
798
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
822
+ "sections": [{
823
+ "type": "paragraph",
824
+ "paragraph_id": "basic",
825
+ "title": "基础信息",
826
+ "rows": [["字段A", "字段B", "字段C", "字段D"]],
827
+ }],
799
828
  },
800
829
  },
801
830
  )
831
+ normalized_request = request.model_dump(mode="json", exclude_none=True)
802
832
  return _safe_tool_call(
803
833
  lambda: self._facade.app_layout_plan(profile=profile, request=request),
804
834
  error_code="LAYOUT_PLAN_FAILED",
805
- normalized_args=request.model_dump(mode="json"),
806
- suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **request.model_dump(mode="json")}},
835
+ normalized_args=normalized_request,
836
+ suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **normalized_request}},
807
837
  )
808
838
 
809
839
  def app_flow_plan(
@@ -1100,7 +1130,7 @@ class AiBuilderTools(ToolBase):
1100
1130
  "app_key": str(plan_args.get("app_key") or app_key),
1101
1131
  "mode": parsed_mode.value,
1102
1132
  "publish": publish,
1103
- "sections": [section.model_dump(mode="json") for section in parsed_sections],
1133
+ "sections": [section.model_dump(mode="json", exclude_none=True) for section in parsed_sections],
1104
1134
  }
1105
1135
  return _safe_tool_call(
1106
1136
  lambda: self._facade.app_layout_apply(
@@ -1708,6 +1738,39 @@ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
1708
1738
 
1709
1739
 
1710
1740
  _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1741
+ "package_list": {
1742
+ "allowed_keys": ["trial_status"],
1743
+ "aliases": {"trialStatus": "trial_status"},
1744
+ "allowed_values": {"trial_status": ["all", "trial", "formal"]},
1745
+ "minimal_example": {
1746
+ "profile": "default",
1747
+ "trial_status": "all",
1748
+ },
1749
+ },
1750
+ "package_resolve": {
1751
+ "allowed_keys": ["package_name"],
1752
+ "aliases": {"packageName": "package_name"},
1753
+ "allowed_values": {},
1754
+ "minimal_example": {
1755
+ "profile": "default",
1756
+ "package_name": "PLM(备用,施工中)",
1757
+ },
1758
+ },
1759
+ "package_create": {
1760
+ "allowed_keys": ["package_name", "icon", "color"],
1761
+ "aliases": {
1762
+ "packageName": "package_name",
1763
+ "iconName": "icon",
1764
+ "iconColor": "color",
1765
+ },
1766
+ "allowed_values": {},
1767
+ "minimal_example": {
1768
+ "profile": "default",
1769
+ "package_name": "项目管理",
1770
+ "icon": "files-folder",
1771
+ "color": "azure",
1772
+ },
1773
+ },
1711
1774
  "member_search": {
1712
1775
  "allowed_keys": ["query", "page_num", "page_size", "contain_disable"],
1713
1776
  "aliases": {},
@@ -1855,6 +1918,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1855
1918
  "field.allow_multiple": "field.relation_mode",
1856
1919
  "field.optional_data_num": "field.relation_mode",
1857
1920
  "field.optionalDataNum": "field.relation_mode",
1921
+ "field.remoteLookupConfig": "field.remote_lookup_config",
1922
+ "field.qLinkerBinding": "field.q_linker_binding",
1923
+ "field.codeBlockConfig": "field.code_block_config",
1924
+ "field.codeBlockBinding": "field.code_block_binding",
1925
+ "field.autoTrigger": "field.auto_trigger",
1926
+ "field.customBtnTextStatus": "field.custom_button_text_enabled",
1927
+ "field.customBtnText": "field.custom_button_text",
1858
1928
  },
1859
1929
  "allowed_values": {
1860
1930
  "field.type": [member.value for member in PublicFieldType],
@@ -1905,6 +1975,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1905
1975
  "field.allow_multiple": "field.relation_mode",
1906
1976
  "field.optional_data_num": "field.relation_mode",
1907
1977
  "field.optionalDataNum": "field.relation_mode",
1978
+ "field.remoteLookupConfig": "field.remote_lookup_config",
1979
+ "field.qLinkerBinding": "field.q_linker_binding",
1980
+ "field.codeBlockConfig": "field.code_block_config",
1981
+ "field.codeBlockBinding": "field.code_block_binding",
1982
+ "field.autoTrigger": "field.auto_trigger",
1983
+ "field.customBtnTextStatus": "field.custom_button_text_enabled",
1984
+ "field.customBtnText": "field.custom_button_text",
1908
1985
  },
1909
1986
  "allowed_values": {
1910
1987
  "field.type": [member.value for member in PublicFieldType],
@@ -1915,6 +1992,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1915
1992
  "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
1916
1993
  "backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
1917
1994
  "relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
1995
+ "if relation target metadata lookup is blocked by 40161/40002/40027, explicit display_field.name and visible_fields[].name let builder degrade verification and still continue schema write",
1996
+ "q_linker_binding lets you declare request config, dynamic inputs, alias parsing, and target-field bindings in one step; builder writes remoteLookupConfig plus the existing backend relation-default and questionRelations structures",
1997
+ "code_block_binding lets you declare inputs, code, alias parsing, and target-field bindings in one step; builder writes codeBlockConfig plus the existing backend relation-default and questionRelations structures",
1998
+ "builder configures code blocks only; it does not execute or trigger code blocks",
1918
1999
  ],
1919
2000
  "minimal_example": {
1920
2001
  "profile": "default",
@@ -1945,34 +2026,99 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1945
2026
  "update_fields": [],
1946
2027
  "remove_fields": [],
1947
2028
  },
2029
+ "code_block_example": {
2030
+ "profile": "default",
2031
+ "app_key": "APP_SCRIPT",
2032
+ "publish": True,
2033
+ "add_fields": [
2034
+ {
2035
+ "name": "查询代码块",
2036
+ "type": "code_block",
2037
+ "code_block_binding": {
2038
+ "inputs": [
2039
+ {"field": {"name": "客户名称"}, "var": "customerName"},
2040
+ {"field": {"name": "预算金额"}, "var": "budget"},
2041
+ ],
2042
+ "code": "const qf_output = {}; qf_output.customerLevel = budget > 100000 ? 'A' : 'B'; return qf_output;",
2043
+ "auto_trigger": True,
2044
+ "custom_button_text_enabled": True,
2045
+ "custom_button_text": "评估客户",
2046
+ "outputs": [
2047
+ {"alias": "customerLevel", "path": "$.customerLevel"},
2048
+ ],
2049
+ },
2050
+ }
2051
+ ],
2052
+ "update_fields": [],
2053
+ "remove_fields": [],
2054
+ },
2055
+ "q_linker_example": {
2056
+ "profile": "default",
2057
+ "app_key": "APP_CUSTOMER",
2058
+ "publish": True,
2059
+ "add_fields": [
2060
+ {"name": "客户名称", "type": "text"},
2061
+ {"name": "企业名称", "type": "text"},
2062
+ {"name": "统一社会信用代码", "type": "text"},
2063
+ {
2064
+ "name": "企业信息查询",
2065
+ "type": "q_linker",
2066
+ "q_linker_binding": {
2067
+ "inputs": [
2068
+ {"field": {"name": "客户名称"}, "key": "keyword", "source": "query_param"},
2069
+ ],
2070
+ "request": {
2071
+ "url": "https://example.com/company/search",
2072
+ "method": "GET",
2073
+ "headers": [],
2074
+ "query_params": [],
2075
+ "body_type": 1,
2076
+ "url_encoded_value": [],
2077
+ "result_type": 1,
2078
+ "auto_trigger": True,
2079
+ "custom_button_text_enabled": True,
2080
+ "custom_button_text": "查询企业信息",
2081
+ },
2082
+ "outputs": [
2083
+ {"alias": "company_name", "path": "$.data.name", "target_field": {"name": "企业名称"}},
2084
+ {"alias": "credit_code", "path": "$.data.creditCode", "target_field": {"name": "统一社会信用代码"}},
2085
+ ],
2086
+ },
2087
+ },
2088
+ ],
2089
+ "update_fields": [],
2090
+ "remove_fields": [],
2091
+ },
1948
2092
  },
1949
2093
  "app_layout_plan": {
1950
2094
  "allowed_keys": ["app_key", "mode", "sections", "preset"],
1951
2095
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
1952
- "section_allowed_keys": ["section_id", "title", "rows"],
2096
+ "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
1953
2097
  "section_aliases": {
1954
2098
  "name": "title",
2099
+ "paragraphId": "paragraph_id",
1955
2100
  "sectionId": "section_id",
1956
2101
  "fields": "rows",
1957
2102
  "field_ids": "rows",
1958
2103
  "columns": "rows_chunk_size",
1959
2104
  },
1960
2105
  "allowed_values": {"mode": [member.value for member in LayoutApplyMode], "preset": [member.value for member in LayoutPreset]},
1961
- "minimal_section_example": {"title": "基础信息", "rows": [["字段A", "字段B"]]},
2106
+ "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
1962
2107
  "minimal_example": {
1963
2108
  "profile": "default",
1964
2109
  "app_key": "APP_KEY",
1965
2110
  "mode": "merge",
1966
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
2111
+ "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]}],
1967
2112
  },
1968
2113
  "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
1969
2114
  },
1970
2115
  "app_layout_apply": {
1971
2116
  "allowed_keys": ["app_key", "mode", "publish", "sections"],
1972
2117
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
1973
- "section_allowed_keys": ["section_id", "title", "rows"],
2118
+ "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
1974
2119
  "section_aliases": {
1975
2120
  "name": "title",
2121
+ "paragraphId": "paragraph_id",
1976
2122
  "sectionId": "section_id",
1977
2123
  "fields": "rows",
1978
2124
  "field_ids": "rows",
@@ -1983,13 +2129,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1983
2129
  "layout verification is split into layout_verified and layout_summary_verified",
1984
2130
  "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
1985
2131
  ],
1986
- "minimal_section_example": {"title": "基础信息", "rows": [["字段A", "字段B"]]},
2132
+ "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
1987
2133
  "minimal_example": {
1988
2134
  "profile": "default",
1989
2135
  "app_key": "APP_KEY",
1990
2136
  "mode": "merge",
1991
2137
  "publish": True,
1992
- "sections": [{"title": "基础信息", "rows": [["项目名称", "项目负责人"]]}],
2138
+ "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
1993
2139
  },
1994
2140
  },
1995
2141
  "app_flow_plan": {
@@ -277,6 +277,7 @@ class ApprovalTools(ToolBase):
277
277
  fields=fields,
278
278
  public_action="task_transfer",
279
279
  )
280
+ self._raise_if_self_transfer(profile=profile, payload=payload)
280
281
  raw = self.record_transfer(profile=profile, app_key=app_key, apply_id=record_id, payload=payload)
281
282
  return self._public_action_response(
282
283
  raw,
@@ -326,7 +327,9 @@ class ApprovalTools(ToolBase):
326
327
  audit_node_id=workflow_node_id,
327
328
  keyword=keyword,
328
329
  )
329
- items = _approval_page_items(raw.get("page"))
330
+ original_items = _approval_page_items(raw.get("page"))
331
+ items = self._filter_self_transfer_candidates(profile=profile, items=original_items)
332
+ filtered_count = max(len(original_items) - len(items), 0)
330
333
  return self._public_page_response(
331
334
  raw,
332
335
  items=items,
@@ -335,7 +338,7 @@ class ApprovalTools(ToolBase):
335
338
  "page_size": page_size,
336
339
  "returned_items": len(items),
337
340
  "page_amount": _approval_page_amount(raw.get("page")),
338
- "reported_total": _approval_page_total(raw.get("page")),
341
+ "reported_total": max(_approval_page_total(raw.get("page")) - filtered_count, 0),
339
342
  },
340
343
  selection={"app_key": app_key, "record_id": record_id, "workflow_node_id": workflow_node_id, "keyword": keyword},
341
344
  )
@@ -849,6 +852,32 @@ class ApprovalTools(ToolBase):
849
852
  if payload.get("handSignImageUrl"):
850
853
  raise_tool_error(QingflowApiError.not_supported("NOT_SUPPORTED_IN_V1: handSignImageUrl is not supported"))
851
854
 
855
+ def _extract_transfer_target_uid(self, payload: dict[str, Any]) -> int | None:
856
+ for key in ("uid", "target_member_id", "targetMemberId"):
857
+ value = payload.get(key)
858
+ if isinstance(value, int) and value > 0:
859
+ return value
860
+ return None
861
+
862
+ def _raise_if_self_transfer(self, *, profile: str, payload: dict[str, Any]) -> None:
863
+ target_uid = self._extract_transfer_target_uid(payload)
864
+ if target_uid is None:
865
+ return
866
+ session_profile = self.sessions.get_profile(profile)
867
+ if session_profile is not None and target_uid == session_profile.uid:
868
+ raise_tool_error(
869
+ QingflowApiError.config_error(
870
+ "task transfer does not support transferring to the current user; choose another transfer member"
871
+ )
872
+ )
873
+
874
+ def _filter_self_transfer_candidates(self, *, profile: str, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
875
+ session_profile = self.sessions.get_profile(profile)
876
+ if session_profile is None:
877
+ return items
878
+ current_uid = session_profile.uid
879
+ return [item for item in items if item.get("uid") != current_uid]
880
+
852
881
  def _delegate_task_action(
853
882
  self,
854
883
  *,