@josephyan/qingflow-cli 0.2.0-beta.67 → 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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +348 -2
- package/src/qingflow_mcp/builder_facade/service.py +2006 -149
- package/src/qingflow_mcp/cli/commands/record.py +2 -0
- package/src/qingflow_mcp/server.py +2 -1
- package/src/qingflow_mcp/server_app_user.py +2 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +29 -0
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +99 -10
- package/src/qingflow_mcp/tools/approval_tools.py +31 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +91 -14
- package/src/qingflow_mcp/tools/record_tools.py +549 -185
- package/src/qingflow_mcp/tools/task_context_tools.py +26 -1
|
@@ -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", []):
|
|
@@ -819,15 +819,21 @@ class AiBuilderTools(ToolBase):
|
|
|
819
819
|
"profile": profile,
|
|
820
820
|
"app_key": app_key,
|
|
821
821
|
"mode": "merge",
|
|
822
|
-
"sections": [{
|
|
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=
|
|
830
|
-
suggested_next_call={"tool_name": "app_layout_plan", "arguments": {"profile": profile, **
|
|
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
|
-
|
|
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,
|
|
53
|
-
"relation-calculation chain to compute bound outputs and write them back
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|