@josephyan/qingflow-cli 0.2.0-beta.74 → 0.2.0-beta.75
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 +121 -0
- package/src/qingflow_mcp/builder_facade/service.py +297 -66
- package/src/qingflow_mcp/cli/commands/task.py +17 -0
- package/src/qingflow_mcp/public_surface.py +231 -0
- package/src/qingflow_mcp/response_trim.py +15 -248
- package/src/qingflow_mcp/server_app_user.py +5 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +35 -5
- package/src/qingflow_mcp/tools/record_tools.py +298 -25
- package/src/qingflow_mcp/tools/task_context_tools.py +146 -7
|
@@ -14,9 +14,11 @@ from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page
|
|
|
14
14
|
from .base import ToolBase
|
|
15
15
|
from .qingbi_report_tools import _qingbi_base_url
|
|
16
16
|
from .record_tools import (
|
|
17
|
+
FieldIndex,
|
|
17
18
|
LAYOUT_ONLY_QUE_TYPES,
|
|
18
19
|
SUBTABLE_QUE_TYPES,
|
|
19
20
|
RecordTools,
|
|
21
|
+
_build_answer_backed_field_index,
|
|
20
22
|
_build_applicant_hidden_linked_top_level_field_index,
|
|
21
23
|
_build_applicant_top_level_field_index,
|
|
22
24
|
_build_static_schema_linkage_payloads,
|
|
@@ -102,6 +104,22 @@ class TaskContextTools(ToolBase):
|
|
|
102
104
|
fields=fields or {},
|
|
103
105
|
)
|
|
104
106
|
|
|
107
|
+
@mcp.tool()
|
|
108
|
+
def task_save_only(
|
|
109
|
+
profile: str = DEFAULT_PROFILE,
|
|
110
|
+
app_key: str = "",
|
|
111
|
+
record_id: int = 0,
|
|
112
|
+
workflow_node_id: int = 0,
|
|
113
|
+
fields: dict[str, Any] | None = None,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
return self.task_save_only(
|
|
116
|
+
profile=profile,
|
|
117
|
+
app_key=app_key,
|
|
118
|
+
record_id=record_id,
|
|
119
|
+
workflow_node_id=workflow_node_id,
|
|
120
|
+
fields=fields or {},
|
|
121
|
+
)
|
|
122
|
+
|
|
105
123
|
@mcp.tool()
|
|
106
124
|
def task_associated_report_detail_get(
|
|
107
125
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -228,6 +246,28 @@ class TaskContextTools(ToolBase):
|
|
|
228
246
|
|
|
229
247
|
return self._run(profile, runner)
|
|
230
248
|
|
|
249
|
+
def task_save_only(
|
|
250
|
+
self,
|
|
251
|
+
*,
|
|
252
|
+
profile: str,
|
|
253
|
+
app_key: str,
|
|
254
|
+
record_id: int,
|
|
255
|
+
workflow_node_id: int,
|
|
256
|
+
fields: dict[str, Any] | None = None,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
field_updates = dict(fields or {})
|
|
259
|
+
if not field_updates:
|
|
260
|
+
raise_tool_error(QingflowApiError.config_error("fields is required and must be non-empty for task_save_only"))
|
|
261
|
+
return self.task_action_execute(
|
|
262
|
+
profile=profile,
|
|
263
|
+
app_key=app_key,
|
|
264
|
+
record_id=record_id,
|
|
265
|
+
workflow_node_id=workflow_node_id,
|
|
266
|
+
action="save_only",
|
|
267
|
+
payload={},
|
|
268
|
+
fields=field_updates,
|
|
269
|
+
)
|
|
270
|
+
|
|
231
271
|
def task_action_execute(
|
|
232
272
|
self,
|
|
233
273
|
*,
|
|
@@ -297,6 +337,15 @@ class TaskContextTools(ToolBase):
|
|
|
297
337
|
capabilities = task_context.get("capabilities") or {}
|
|
298
338
|
available_actions = capabilities.get("available_actions") or []
|
|
299
339
|
if normalized_action not in available_actions:
|
|
340
|
+
if normalized_action == "save_only":
|
|
341
|
+
capability_warnings = capabilities.get("warnings") or []
|
|
342
|
+
message = (
|
|
343
|
+
"task action 'save_only' is not currently available for the current node; "
|
|
344
|
+
"MCP only exposes save_only when backend editableQueIds returns a non-empty result"
|
|
345
|
+
)
|
|
346
|
+
if capability_warnings:
|
|
347
|
+
message += "; backend editableQueIds is unavailable or empty for this task context"
|
|
348
|
+
raise_tool_error(QingflowApiError.config_error(message))
|
|
300
349
|
raise_tool_error(
|
|
301
350
|
QingflowApiError.config_error(
|
|
302
351
|
f"task action '{normalized_action}' is not currently available for app_key='{app_key}' record_id={record_id} workflow_node_id={workflow_node_id}"
|
|
@@ -426,10 +475,14 @@ class TaskContextTools(ToolBase):
|
|
|
426
475
|
prepared_fields: dict[str, Any] | None,
|
|
427
476
|
) -> dict[str, Any]:
|
|
428
477
|
merged_answers = None
|
|
478
|
+
normalized_answers = None
|
|
429
479
|
if isinstance(prepared_fields, dict):
|
|
430
480
|
candidate_answers = prepared_fields.get("merged_answers")
|
|
431
481
|
if isinstance(candidate_answers, list):
|
|
432
482
|
merged_answers = candidate_answers
|
|
483
|
+
candidate_normalized = prepared_fields.get("normalized_answers")
|
|
484
|
+
if isinstance(candidate_normalized, list):
|
|
485
|
+
normalized_answers = candidate_normalized
|
|
433
486
|
if normalized_action == "approve":
|
|
434
487
|
action_payload = dict(payload)
|
|
435
488
|
action_payload["nodeId"] = workflow_node_id
|
|
@@ -489,14 +542,14 @@ class TaskContextTools(ToolBase):
|
|
|
489
542
|
payload=action_payload,
|
|
490
543
|
)
|
|
491
544
|
if normalized_action == "save_only":
|
|
492
|
-
if
|
|
545
|
+
if normalized_answers is None:
|
|
493
546
|
raise_tool_error(QingflowApiError.config_error("fields is required for action 'save_only'"))
|
|
494
547
|
return self._task_save_only(
|
|
495
548
|
profile=profile,
|
|
496
549
|
app_key=app_key,
|
|
497
550
|
record_id=record_id,
|
|
498
551
|
workflow_node_id=workflow_node_id,
|
|
499
|
-
|
|
552
|
+
apply_answers=normalized_answers,
|
|
500
553
|
)
|
|
501
554
|
return self._task_tools.task_urge(
|
|
502
555
|
profile=profile,
|
|
@@ -1223,9 +1276,16 @@ class TaskContextTools(ToolBase):
|
|
|
1223
1276
|
current_answers=detail.get("answers") or [],
|
|
1224
1277
|
)
|
|
1225
1278
|
update_schema = update_schema_state["public_schema"]
|
|
1279
|
+
save_only_available, capability_warnings, save_only_source = self._resolve_task_save_only_availability(
|
|
1280
|
+
context,
|
|
1281
|
+
app_key=app_key,
|
|
1282
|
+
workflow_node_id=workflow_node_id,
|
|
1283
|
+
)
|
|
1226
1284
|
capabilities = self._build_capabilities(
|
|
1227
1285
|
node_info,
|
|
1228
|
-
allow_save_only=
|
|
1286
|
+
allow_save_only=save_only_available,
|
|
1287
|
+
warnings=capability_warnings,
|
|
1288
|
+
save_only_source=save_only_source,
|
|
1229
1289
|
)
|
|
1230
1290
|
visibility = self._build_visibility(node_info, detail)
|
|
1231
1291
|
return {
|
|
@@ -1318,7 +1378,14 @@ class TaskContextTools(ToolBase):
|
|
|
1318
1378
|
)
|
|
1319
1379
|
)
|
|
1320
1380
|
|
|
1321
|
-
def _build_capabilities(
|
|
1381
|
+
def _build_capabilities(
|
|
1382
|
+
self,
|
|
1383
|
+
node_info: dict[str, Any],
|
|
1384
|
+
*,
|
|
1385
|
+
allow_save_only: bool,
|
|
1386
|
+
warnings: list[JSONObject] | None = None,
|
|
1387
|
+
save_only_source: str = "workflow_editable_que_ids",
|
|
1388
|
+
) -> dict[str, Any]:
|
|
1322
1389
|
available_actions = ["approve"]
|
|
1323
1390
|
if self._coerce_bool(node_info.get("rejectBtnStatus")):
|
|
1324
1391
|
available_actions.append("reject")
|
|
@@ -1347,6 +1414,8 @@ class TaskContextTools(ToolBase):
|
|
|
1347
1414
|
return {
|
|
1348
1415
|
"available_actions": available_actions,
|
|
1349
1416
|
"visible_but_unimplemented_actions": visible_but_unimplemented_actions,
|
|
1417
|
+
"save_only_source": save_only_source,
|
|
1418
|
+
"warnings": list(warnings or []),
|
|
1350
1419
|
"action_constraints": {
|
|
1351
1420
|
"feedback_required_for": feedback_required_for,
|
|
1352
1421
|
"submit_check_enabled": self._coerce_bool(node_info.get("beingSubmitCheck")),
|
|
@@ -1415,6 +1484,12 @@ class TaskContextTools(ToolBase):
|
|
|
1415
1484
|
workflow_node_id=workflow_node_id,
|
|
1416
1485
|
node_info=node_info,
|
|
1417
1486
|
)
|
|
1487
|
+
index, augmentation_warnings = self._augment_task_editable_field_index(
|
|
1488
|
+
index=index,
|
|
1489
|
+
current_answers=current_answers,
|
|
1490
|
+
editable_question_ids=editable_question_ids,
|
|
1491
|
+
)
|
|
1492
|
+
schema_warnings.extend(augmentation_warnings)
|
|
1418
1493
|
effective_editable_ids = set(editable_question_ids)
|
|
1419
1494
|
for field in index.by_id.values():
|
|
1420
1495
|
if field.que_type in SUBTABLE_QUE_TYPES and (_subtable_descendant_ids(field) & set(editable_question_ids)):
|
|
@@ -1474,6 +1549,44 @@ class TaskContextTools(ToolBase):
|
|
|
1474
1549
|
"editable_question_ids_source": source,
|
|
1475
1550
|
}
|
|
1476
1551
|
|
|
1552
|
+
def _augment_task_editable_field_index(
|
|
1553
|
+
self,
|
|
1554
|
+
*,
|
|
1555
|
+
index: FieldIndex,
|
|
1556
|
+
current_answers: Any,
|
|
1557
|
+
editable_question_ids: set[int],
|
|
1558
|
+
) -> tuple[FieldIndex, list[JSONObject]]:
|
|
1559
|
+
if not editable_question_ids:
|
|
1560
|
+
return index, []
|
|
1561
|
+
missing_field_ids = {
|
|
1562
|
+
str(que_id)
|
|
1563
|
+
for que_id in editable_question_ids
|
|
1564
|
+
if que_id > 0 and str(que_id) not in index.by_id
|
|
1565
|
+
}
|
|
1566
|
+
if not missing_field_ids:
|
|
1567
|
+
return index, []
|
|
1568
|
+
answer_backed_index = _build_answer_backed_field_index(
|
|
1569
|
+
current_answers,
|
|
1570
|
+
field_id_filter=missing_field_ids,
|
|
1571
|
+
)
|
|
1572
|
+
if not answer_backed_index.by_id:
|
|
1573
|
+
return index, []
|
|
1574
|
+
augmented_index = _merge_field_indexes(index, answer_backed_index)
|
|
1575
|
+
return augmented_index, [
|
|
1576
|
+
{
|
|
1577
|
+
"code": "TASK_RUNTIME_EDITABLE_FIELDS_AUGMENTED",
|
|
1578
|
+
"message": "task update schema added backend-editable fields from current task answers because applicant schema did not expose them.",
|
|
1579
|
+
"fields": [
|
|
1580
|
+
{
|
|
1581
|
+
"field_id": field.que_id,
|
|
1582
|
+
"title": field.que_title,
|
|
1583
|
+
"que_type": field.que_type,
|
|
1584
|
+
}
|
|
1585
|
+
for field in answer_backed_index.by_id.values()
|
|
1586
|
+
],
|
|
1587
|
+
}
|
|
1588
|
+
]
|
|
1589
|
+
|
|
1477
1590
|
def _resolve_task_editable_question_ids(
|
|
1478
1591
|
self,
|
|
1479
1592
|
context: BackendRequestContext,
|
|
@@ -1504,6 +1617,31 @@ class TaskContextTools(ToolBase):
|
|
|
1504
1617
|
fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
|
|
1505
1618
|
return fallback_ids, warnings, "que_auth_setting"
|
|
1506
1619
|
|
|
1620
|
+
def _resolve_task_save_only_availability(
|
|
1621
|
+
self,
|
|
1622
|
+
context: BackendRequestContext,
|
|
1623
|
+
*,
|
|
1624
|
+
app_key: str,
|
|
1625
|
+
workflow_node_id: int,
|
|
1626
|
+
) -> tuple[bool, list[JSONObject], str]:
|
|
1627
|
+
try:
|
|
1628
|
+
payload = self.backend.request(
|
|
1629
|
+
"GET",
|
|
1630
|
+
context,
|
|
1631
|
+
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
1632
|
+
)
|
|
1633
|
+
except QingflowApiError as error:
|
|
1634
|
+
warning: JSONObject = {
|
|
1635
|
+
"code": "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
|
|
1636
|
+
"message": "save_only is hidden because backend editableQueIds is unavailable for the current node; MCP no longer infers save_only from local schema reconstruction.",
|
|
1637
|
+
}
|
|
1638
|
+
if error.backend_code is not None:
|
|
1639
|
+
warning["backend_code"] = error.backend_code
|
|
1640
|
+
if error.http_status is not None:
|
|
1641
|
+
warning["http_status"] = error.http_status
|
|
1642
|
+
return False, [warning], "backend_editable_que_ids_unavailable"
|
|
1643
|
+
return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
|
|
1644
|
+
|
|
1507
1645
|
def _extract_question_ids(self, payload: Any) -> set[int]:
|
|
1508
1646
|
candidates: list[Any] = []
|
|
1509
1647
|
if isinstance(payload, list):
|
|
@@ -1587,6 +1725,7 @@ class TaskContextTools(ToolBase):
|
|
|
1587
1725
|
},
|
|
1588
1726
|
)
|
|
1589
1727
|
)
|
|
1728
|
+
index = schema_state["index"]
|
|
1590
1729
|
preflight = self._record_tools._build_record_write_preflight(
|
|
1591
1730
|
profile=profile,
|
|
1592
1731
|
context=context,
|
|
@@ -1601,8 +1740,8 @@ class TaskContextTools(ToolBase):
|
|
|
1601
1740
|
view_key=None,
|
|
1602
1741
|
view_name=None,
|
|
1603
1742
|
existing_answers_override=current_answers,
|
|
1743
|
+
field_index_override=index,
|
|
1604
1744
|
)
|
|
1605
|
-
index = schema_state["index"]
|
|
1606
1745
|
effective_editable_ids = set(schema_state["effective_editable_question_ids"])
|
|
1607
1746
|
scoped_field_errors = self._task_scope_field_errors(
|
|
1608
1747
|
normalized_answers=preflight.get("normalized_answers") or [],
|
|
@@ -1692,14 +1831,14 @@ class TaskContextTools(ToolBase):
|
|
|
1692
1831
|
app_key: str,
|
|
1693
1832
|
record_id: int,
|
|
1694
1833
|
workflow_node_id: int,
|
|
1695
|
-
|
|
1834
|
+
apply_answers: list[dict[str, Any]],
|
|
1696
1835
|
) -> dict[str, Any]:
|
|
1697
1836
|
def runner(session_profile, context):
|
|
1698
1837
|
result = self.backend.request(
|
|
1699
1838
|
"POST",
|
|
1700
1839
|
context,
|
|
1701
1840
|
f"/app/{app_key}/apply/{record_id}",
|
|
1702
|
-
json_body={"role": 3, "auditNodeId": workflow_node_id, "answers":
|
|
1841
|
+
json_body={"role": 3, "auditNodeId": workflow_node_id, "answers": apply_answers},
|
|
1703
1842
|
)
|
|
1704
1843
|
return {
|
|
1705
1844
|
"profile": profile,
|