@josephyan/qingflow-cli 0.2.0-beta.67 → 0.2.0-beta.69

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.
@@ -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
 
@@ -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
 
@@ -819,15 +819,21 @@ class AiBuilderTools(ToolBase):
819
819
  "profile": profile,
820
820
  "app_key": app_key,
821
821
  "mode": "merge",
822
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
822
+ "sections": [{
823
+ "type": "paragraph",
824
+ "paragraph_id": "basic",
825
+ "title": "基础信息",
826
+ "rows": [["字段A", "字段B", "字段C", "字段D"]],
827
+ }],
823
828
  },
824
829
  },
825
830
  )
831
+ normalized_request = request.model_dump(mode="json", exclude_none=True)
826
832
  return _safe_tool_call(
827
833
  lambda: self._facade.app_layout_plan(profile=profile, request=request),
828
834
  error_code="LAYOUT_PLAN_FAILED",
829
- normalized_args=request.model_dump(mode="json"),
830
- 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}},
831
837
  )
832
838
 
833
839
  def app_flow_plan(
@@ -1124,7 +1130,7 @@ class AiBuilderTools(ToolBase):
1124
1130
  "app_key": str(plan_args.get("app_key") or app_key),
1125
1131
  "mode": parsed_mode.value,
1126
1132
  "publish": publish,
1127
- "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],
1128
1134
  }
1129
1135
  return _safe_tool_call(
1130
1136
  lambda: self._facade.app_layout_apply(
@@ -1912,6 +1918,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1912
1918
  "field.allow_multiple": "field.relation_mode",
1913
1919
  "field.optional_data_num": "field.relation_mode",
1914
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",
1915
1928
  },
1916
1929
  "allowed_values": {
1917
1930
  "field.type": [member.value for member in PublicFieldType],
@@ -1962,6 +1975,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1962
1975
  "field.allow_multiple": "field.relation_mode",
1963
1976
  "field.optional_data_num": "field.relation_mode",
1964
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",
1965
1985
  },
1966
1986
  "allowed_values": {
1967
1987
  "field.type": [member.value for member in PublicFieldType],
@@ -1972,6 +1992,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1972
1992
  "multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
1973
1993
  "backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
1974
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",
1975
1999
  ],
1976
2000
  "minimal_example": {
1977
2001
  "profile": "default",
@@ -2002,34 +2026,99 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2002
2026
  "update_fields": [],
2003
2027
  "remove_fields": [],
2004
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
+ },
2005
2092
  },
2006
2093
  "app_layout_plan": {
2007
2094
  "allowed_keys": ["app_key", "mode", "sections", "preset"],
2008
2095
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
2009
- "section_allowed_keys": ["section_id", "title", "rows"],
2096
+ "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
2010
2097
  "section_aliases": {
2011
2098
  "name": "title",
2099
+ "paragraphId": "paragraph_id",
2012
2100
  "sectionId": "section_id",
2013
2101
  "fields": "rows",
2014
2102
  "field_ids": "rows",
2015
2103
  "columns": "rows_chunk_size",
2016
2104
  },
2017
2105
  "allowed_values": {"mode": [member.value for member in LayoutApplyMode], "preset": [member.value for member in LayoutPreset]},
2018
- "minimal_section_example": {"title": "基础信息", "rows": [["字段A", "字段B"]]},
2106
+ "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
2019
2107
  "minimal_example": {
2020
2108
  "profile": "default",
2021
2109
  "app_key": "APP_KEY",
2022
2110
  "mode": "merge",
2023
- "sections": [{"title": "基础信息", "rows": [["字段A", "字段B"]]}],
2111
+ "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]}],
2024
2112
  },
2025
2113
  "preset_example": {"profile": "default", "app_key": "APP_KEY", "mode": "merge", "preset": "balanced", "sections": []},
2026
2114
  },
2027
2115
  "app_layout_apply": {
2028
2116
  "allowed_keys": ["app_key", "mode", "publish", "sections"],
2029
2117
  "aliases": {"overwrite": "replace", "sectionId": "section_id"},
2030
- "section_allowed_keys": ["section_id", "title", "rows"],
2118
+ "section_allowed_keys": ["type", "paragraph_id", "section_id", "title", "rows"],
2031
2119
  "section_aliases": {
2032
2120
  "name": "title",
2121
+ "paragraphId": "paragraph_id",
2033
2122
  "sectionId": "section_id",
2034
2123
  "fields": "rows",
2035
2124
  "field_ids": "rows",
@@ -2040,13 +2129,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2040
2129
  "layout verification is split into layout_verified and layout_summary_verified",
2041
2130
  "LAYOUT_SUMMARY_UNVERIFIED means raw form readback is stronger than the compact summary",
2042
2131
  ],
2043
- "minimal_section_example": {"title": "基础信息", "rows": [["字段A", "字段B"]]},
2132
+ "minimal_section_example": {"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["字段A", "字段B", "字段C", "字段D"]]},
2044
2133
  "minimal_example": {
2045
2134
  "profile": "default",
2046
2135
  "app_key": "APP_KEY",
2047
2136
  "mode": "merge",
2048
2137
  "publish": True,
2049
- "sections": [{"title": "基础信息", "rows": [["项目名称", "项目负责人"]]}],
2138
+ "sections": [{"type": "paragraph", "paragraph_id": "basic", "title": "基础信息", "rows": [["项目名称", "项目负责人", "项目阶段", "优先级"]]}],
2050
2139
  },
2051
2140
  },
2052
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
  *,
@@ -33,6 +33,27 @@ SUPPORTED_CODE_BLOCK_ROLES = {1, 2, 3, 5}
33
33
 
34
34
 
35
35
  class CodeBlockTools(RecordTools):
36
+ def _get_code_block_relation_schema(
37
+ self,
38
+ profile: str,
39
+ context, # type: ignore[no-untyped-def]
40
+ app_key: str,
41
+ *,
42
+ force_refresh: bool,
43
+ ) -> JSONObject:
44
+ cache_key = (profile, app_key, "code_block_relation_form", 1)
45
+ if not force_refresh and cache_key in self._form_cache:
46
+ return self._form_cache[cache_key]
47
+ schema = self.backend.request(
48
+ "GET",
49
+ context,
50
+ f"/app/{app_key}/form",
51
+ params={"type": 1},
52
+ )
53
+ normalized = schema if isinstance(schema, dict) else {}
54
+ self._form_cache[cache_key] = normalized
55
+ return normalized
56
+
36
57
  def register(self, mcp: FastMCP) -> None:
37
58
  super().register(mcp)
38
59
 
@@ -49,9 +70,10 @@ class CodeBlockTools(RecordTools):
49
70
 
50
71
  @mcp.tool(
51
72
  description=(
52
- "Run a form code-block field against the current record data, then reuse Qingflow's existing "
53
- "relation-calculation chain to compute bound outputs and write them back automatically. "
54
- "Use record_code_block_schema_get first and choose an exact code-block field selector."
73
+ "Run a form code-block field against the current record data, parse alias results, and optionally "
74
+ "reuse Qingflow's existing relation-calculation chain to compute bound outputs and write them back. "
75
+ "Use record_code_block_schema_get first and choose an exact code-block field selector. "
76
+ "For safe debugging, pass apply_writeback=false to inspect parsed results without writing back."
55
77
  )
56
78
  )
57
79
  def record_code_block_run(
@@ -64,6 +86,7 @@ class CodeBlockTools(RecordTools):
64
86
  answers: list[JSONObject] | None = None,
65
87
  fields: JSONObject | None = None,
66
88
  manual: bool = True,
89
+ apply_writeback: bool = True,
67
90
  verify_writeback: bool = True,
68
91
  force_refresh_form: bool = False,
69
92
  output_profile: str = "normal",
@@ -78,6 +101,7 @@ class CodeBlockTools(RecordTools):
78
101
  answers=answers or [],
79
102
  fields=fields or {},
80
103
  manual=manual,
104
+ apply_writeback=apply_writeback,
81
105
  verify_writeback=verify_writeback,
82
106
  force_refresh_form=force_refresh_form,
83
107
  output_profile=output_profile,
@@ -95,7 +119,7 @@ class CodeBlockTools(RecordTools):
95
119
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
96
120
 
97
121
  def runner(session_profile, context):
98
- schema = self._get_form_schema(profile, context, app_key, force_refresh=False)
122
+ relation_schema = self._get_code_block_relation_schema(profile, context, app_key, force_refresh=False)
99
123
  index = self._get_applicant_top_level_field_index(profile, context, app_key, force_refresh=False)
100
124
  input_fields = [
101
125
  self._ready_schema_field_payload(
@@ -113,7 +137,7 @@ class CodeBlockTools(RecordTools):
113
137
  if field.que_type != CODE_BLOCK_QUE_TYPE:
114
138
  continue
115
139
  targets = _collect_code_block_relation_targets(
116
- _collect_question_relations(schema),
140
+ _collect_question_relations(relation_schema),
117
141
  code_block_que_id=field.que_id,
118
142
  )
119
143
  bound_output_fields = [
@@ -127,6 +151,7 @@ class CodeBlockTools(RecordTools):
127
151
  "title": field.que_title,
128
152
  "selector": field.que_title,
129
153
  "bound_output_fields": bound_output_fields,
154
+ "configured_aliases": _extract_code_block_configured_aliases(field),
130
155
  }
131
156
  )
132
157
  response: JSONObject = {
@@ -164,6 +189,7 @@ class CodeBlockTools(RecordTools):
164
189
  answers: list[JSONObject] | None = None,
165
190
  fields: JSONObject | None = None,
166
191
  manual: bool = True,
192
+ apply_writeback: bool = True,
167
193
  verify_writeback: bool = True,
168
194
  force_refresh_form: bool = False,
169
195
  output_profile: str = "normal",
@@ -178,7 +204,12 @@ class CodeBlockTools(RecordTools):
178
204
  raise_tool_error(QingflowApiError.config_error("code_block_field is required"))
179
205
 
180
206
  def runner(session_profile, context):
181
- schema = self._get_form_schema(profile, context, app_key, force_refresh=force_refresh_form)
207
+ relation_schema = self._get_code_block_relation_schema(
208
+ profile,
209
+ context,
210
+ app_key,
211
+ force_refresh=force_refresh_form,
212
+ )
182
213
  index = self._get_field_index(profile, context, app_key, force_refresh=force_refresh_form)
183
214
  code_block = self._resolve_field_selector(code_block_field, index, location="code_block_field")
184
215
  if code_block.que_type != CODE_BLOCK_QUE_TYPE:
@@ -233,8 +264,9 @@ class CodeBlockTools(RecordTools):
233
264
  json_body=run_body,
234
265
  )
235
266
  alias_results = _normalize_code_block_alias_results(run_result)
267
+ configured_aliases = _extract_code_block_configured_aliases(code_block)
236
268
  relation_target_fields = _collect_code_block_relation_targets(
237
- _collect_question_relations(schema),
269
+ _collect_question_relations(relation_schema),
238
270
  code_block_que_id=code_block.que_id,
239
271
  )
240
272
  relation_errors: list[JSONObject] = []
@@ -277,7 +309,12 @@ class CodeBlockTools(RecordTools):
277
309
  writeback_applied = False
278
310
  status = "completed"
279
311
  ok = True
280
- if relation_target_fields and calculated_answers:
312
+ if relation_errors:
313
+ status = "relation_failed"
314
+ ok = False
315
+ elif not apply_writeback:
316
+ status = "debug_completed"
317
+ elif relation_target_fields and calculated_answers:
281
318
  write_body: JSONObject = {"role": role, "answers": calculated_answers}
282
319
  if workflow_node_id is not None:
283
320
  write_body["auditNodeId"] = workflow_node_id
@@ -305,9 +342,6 @@ class CodeBlockTools(RecordTools):
305
342
  if not bool(verification.get("verified")):
306
343
  status = "verification_failed"
307
344
  ok = False
308
- elif relation_errors:
309
- status = "relation_failed"
310
- ok = False
311
345
  else:
312
346
  status = "no_writeback"
313
347
  response: JSONObject = {
@@ -325,9 +359,11 @@ class CodeBlockTools(RecordTools):
325
359
  "role": role,
326
360
  "workflow_node_id": workflow_node_id,
327
361
  "manual": bool(manual),
362
+ "apply_writeback": bool(apply_writeback),
328
363
  "result_count": len(alias_results),
329
364
  },
330
365
  "outputs": {
366
+ "configured_aliases": configured_aliases,
331
367
  "alias_results": alias_results,
332
368
  "alias_map": _build_alias_result_map(alias_results),
333
369
  },
@@ -335,11 +371,14 @@ class CodeBlockTools(RecordTools):
335
371
  "target_fields": relation_target_fields,
336
372
  "result_item_count": len(relation_items),
337
373
  "calculated_answer_count": len(calculated_answers),
374
+ "calculated_answers_preview": calculated_answers,
338
375
  "errors": relation_errors,
339
376
  },
340
377
  "writeback": {
378
+ "enabled": bool(apply_writeback),
341
379
  "attempted": writeback_attempted,
342
380
  "applied": writeback_applied,
381
+ "skipped_reason": "apply_writeback_disabled" if not apply_writeback else None,
343
382
  "verify_writeback": verify_writeback,
344
383
  "write_verified": bool(verification.get("verified")) if verification is not None else None,
345
384
  "result": write_result,
@@ -606,6 +645,38 @@ def _normalize_code_block_alias_results(payload: JSONValue) -> list[JSONObject]:
606
645
  return results
607
646
 
608
647
 
648
+ def _extract_code_block_configured_aliases(field: FormField) -> list[JSONObject]:
649
+ raw_config = field.raw.get("codeBlockConfig")
650
+ if not isinstance(raw_config, dict):
651
+ return []
652
+ raw_aliases = raw_config.get("resultAliasPath")
653
+ if not isinstance(raw_aliases, list):
654
+ return []
655
+ return [item for item in (_normalize_code_block_alias_item(alias) for alias in raw_aliases) if item is not None]
656
+
657
+
658
+ def _normalize_code_block_alias_item(value: JSONValue) -> JSONObject | None:
659
+ if not isinstance(value, dict):
660
+ return None
661
+ alias_name = _normalize_optional_text(value.get("aliasName", value.get("alias_name")))
662
+ alias_path = _normalize_optional_text(value.get("aliasPath", value.get("alias_path")))
663
+ if alias_name is None and alias_path is None:
664
+ return None
665
+ raw_sub_alias = value.get("subAlias", value.get("sub_alias"))
666
+ sub_alias = (
667
+ [item for item in (_normalize_code_block_alias_item(entry) for entry in raw_sub_alias) if item is not None]
668
+ if isinstance(raw_sub_alias, list)
669
+ else []
670
+ )
671
+ return {
672
+ "alias_id": _coerce_count(value.get("aliasId", value.get("alias_id"))),
673
+ "alias_name": alias_name,
674
+ "alias_path": alias_path,
675
+ "alias_type": _coerce_count(value.get("aliasType", value.get("alias_type"))) or 1,
676
+ "sub_alias": sub_alias,
677
+ }
678
+
679
+
609
680
  def _build_alias_result_map(alias_results: list[JSONObject]) -> JSONObject:
610
681
  alias_map: JSONObject = {}
611
682
  for item in alias_results:
@@ -625,7 +696,11 @@ def _collect_code_block_relation_targets(question_relations: list[JSONObject], *
625
696
  for relation in question_relations:
626
697
  if _coerce_count(relation.get("relationType")) != CODE_BLOCK_RELATION_TYPE:
627
698
  continue
628
- if _coerce_count(relation.get("qlinkerQueId")) != code_block_que_id:
699
+ alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
700
+ relation_code_block_que_id = _coerce_count(relation.get("qlinkerQueId"))
701
+ if relation_code_block_que_id is None:
702
+ relation_code_block_que_id = _coerce_count(alias_config.get("queId"))
703
+ if relation_code_block_que_id != code_block_que_id:
629
704
  continue
630
705
  target_id = _coerce_count(
631
706
  relation.get("queId", relation.get("targetQueId", relation.get("displayedQueId")))
@@ -636,8 +711,10 @@ def _collect_code_block_relation_targets(question_relations: list[JSONObject], *
636
711
  targets.append(
637
712
  {
638
713
  "que_id": target_id,
639
- "alias_id": _coerce_count(relation.get("aliasId")),
640
- "qlinker_que_id": _coerce_count(relation.get("qlinkerQueId")),
714
+ "alias_id": _coerce_count(relation.get("aliasId")) or _coerce_count(alias_config.get("aliasId")),
715
+ "alias_name": _normalize_optional_text(relation.get("qlinkerAlias"))
716
+ or _normalize_optional_text(alias_config.get("qlinkerAlias")),
717
+ "qlinker_que_id": relation_code_block_que_id,
641
718
  }
642
719
  )
643
720
  return targets