@josephyan/qingflow-cli 0.2.0-beta.74 → 0.2.0-beta.76
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/public_surface.py +230 -0
- package/src/qingflow_mcp/response_trim.py +14 -248
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +35 -5
- package/src/qingflow_mcp/tools/approval_tools.py +0 -16
- package/src/qingflow_mcp/tools/record_tools.py +298 -25
- package/src/qingflow_mcp/tools/task_context_tools.py +130 -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,
|
|
@@ -228,6 +230,28 @@ class TaskContextTools(ToolBase):
|
|
|
228
230
|
|
|
229
231
|
return self._run(profile, runner)
|
|
230
232
|
|
|
233
|
+
def task_save_only(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
profile: str,
|
|
237
|
+
app_key: str,
|
|
238
|
+
record_id: int,
|
|
239
|
+
workflow_node_id: int,
|
|
240
|
+
fields: dict[str, Any] | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
field_updates = dict(fields or {})
|
|
243
|
+
if not field_updates:
|
|
244
|
+
raise_tool_error(QingflowApiError.config_error("fields is required and must be non-empty for task_save_only"))
|
|
245
|
+
return self.task_action_execute(
|
|
246
|
+
profile=profile,
|
|
247
|
+
app_key=app_key,
|
|
248
|
+
record_id=record_id,
|
|
249
|
+
workflow_node_id=workflow_node_id,
|
|
250
|
+
action="save_only",
|
|
251
|
+
payload={},
|
|
252
|
+
fields=field_updates,
|
|
253
|
+
)
|
|
254
|
+
|
|
231
255
|
def task_action_execute(
|
|
232
256
|
self,
|
|
233
257
|
*,
|
|
@@ -297,6 +321,15 @@ class TaskContextTools(ToolBase):
|
|
|
297
321
|
capabilities = task_context.get("capabilities") or {}
|
|
298
322
|
available_actions = capabilities.get("available_actions") or []
|
|
299
323
|
if normalized_action not in available_actions:
|
|
324
|
+
if normalized_action == "save_only":
|
|
325
|
+
capability_warnings = capabilities.get("warnings") or []
|
|
326
|
+
message = (
|
|
327
|
+
"task action 'save_only' is not currently available for the current node; "
|
|
328
|
+
"MCP only exposes save_only when backend editableQueIds returns a non-empty result"
|
|
329
|
+
)
|
|
330
|
+
if capability_warnings:
|
|
331
|
+
message += "; backend editableQueIds is unavailable or empty for this task context"
|
|
332
|
+
raise_tool_error(QingflowApiError.config_error(message))
|
|
300
333
|
raise_tool_error(
|
|
301
334
|
QingflowApiError.config_error(
|
|
302
335
|
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 +459,14 @@ class TaskContextTools(ToolBase):
|
|
|
426
459
|
prepared_fields: dict[str, Any] | None,
|
|
427
460
|
) -> dict[str, Any]:
|
|
428
461
|
merged_answers = None
|
|
462
|
+
normalized_answers = None
|
|
429
463
|
if isinstance(prepared_fields, dict):
|
|
430
464
|
candidate_answers = prepared_fields.get("merged_answers")
|
|
431
465
|
if isinstance(candidate_answers, list):
|
|
432
466
|
merged_answers = candidate_answers
|
|
467
|
+
candidate_normalized = prepared_fields.get("normalized_answers")
|
|
468
|
+
if isinstance(candidate_normalized, list):
|
|
469
|
+
normalized_answers = candidate_normalized
|
|
433
470
|
if normalized_action == "approve":
|
|
434
471
|
action_payload = dict(payload)
|
|
435
472
|
action_payload["nodeId"] = workflow_node_id
|
|
@@ -489,14 +526,14 @@ class TaskContextTools(ToolBase):
|
|
|
489
526
|
payload=action_payload,
|
|
490
527
|
)
|
|
491
528
|
if normalized_action == "save_only":
|
|
492
|
-
if
|
|
529
|
+
if normalized_answers is None:
|
|
493
530
|
raise_tool_error(QingflowApiError.config_error("fields is required for action 'save_only'"))
|
|
494
531
|
return self._task_save_only(
|
|
495
532
|
profile=profile,
|
|
496
533
|
app_key=app_key,
|
|
497
534
|
record_id=record_id,
|
|
498
535
|
workflow_node_id=workflow_node_id,
|
|
499
|
-
|
|
536
|
+
apply_answers=normalized_answers,
|
|
500
537
|
)
|
|
501
538
|
return self._task_tools.task_urge(
|
|
502
539
|
profile=profile,
|
|
@@ -1223,9 +1260,16 @@ class TaskContextTools(ToolBase):
|
|
|
1223
1260
|
current_answers=detail.get("answers") or [],
|
|
1224
1261
|
)
|
|
1225
1262
|
update_schema = update_schema_state["public_schema"]
|
|
1263
|
+
save_only_available, capability_warnings, save_only_source = self._resolve_task_save_only_availability(
|
|
1264
|
+
context,
|
|
1265
|
+
app_key=app_key,
|
|
1266
|
+
workflow_node_id=workflow_node_id,
|
|
1267
|
+
)
|
|
1226
1268
|
capabilities = self._build_capabilities(
|
|
1227
1269
|
node_info,
|
|
1228
|
-
allow_save_only=
|
|
1270
|
+
allow_save_only=save_only_available,
|
|
1271
|
+
warnings=capability_warnings,
|
|
1272
|
+
save_only_source=save_only_source,
|
|
1229
1273
|
)
|
|
1230
1274
|
visibility = self._build_visibility(node_info, detail)
|
|
1231
1275
|
return {
|
|
@@ -1318,7 +1362,14 @@ class TaskContextTools(ToolBase):
|
|
|
1318
1362
|
)
|
|
1319
1363
|
)
|
|
1320
1364
|
|
|
1321
|
-
def _build_capabilities(
|
|
1365
|
+
def _build_capabilities(
|
|
1366
|
+
self,
|
|
1367
|
+
node_info: dict[str, Any],
|
|
1368
|
+
*,
|
|
1369
|
+
allow_save_only: bool,
|
|
1370
|
+
warnings: list[JSONObject] | None = None,
|
|
1371
|
+
save_only_source: str = "workflow_editable_que_ids",
|
|
1372
|
+
) -> dict[str, Any]:
|
|
1322
1373
|
available_actions = ["approve"]
|
|
1323
1374
|
if self._coerce_bool(node_info.get("rejectBtnStatus")):
|
|
1324
1375
|
available_actions.append("reject")
|
|
@@ -1347,6 +1398,8 @@ class TaskContextTools(ToolBase):
|
|
|
1347
1398
|
return {
|
|
1348
1399
|
"available_actions": available_actions,
|
|
1349
1400
|
"visible_but_unimplemented_actions": visible_but_unimplemented_actions,
|
|
1401
|
+
"save_only_source": save_only_source,
|
|
1402
|
+
"warnings": list(warnings or []),
|
|
1350
1403
|
"action_constraints": {
|
|
1351
1404
|
"feedback_required_for": feedback_required_for,
|
|
1352
1405
|
"submit_check_enabled": self._coerce_bool(node_info.get("beingSubmitCheck")),
|
|
@@ -1415,6 +1468,12 @@ class TaskContextTools(ToolBase):
|
|
|
1415
1468
|
workflow_node_id=workflow_node_id,
|
|
1416
1469
|
node_info=node_info,
|
|
1417
1470
|
)
|
|
1471
|
+
index, augmentation_warnings = self._augment_task_editable_field_index(
|
|
1472
|
+
index=index,
|
|
1473
|
+
current_answers=current_answers,
|
|
1474
|
+
editable_question_ids=editable_question_ids,
|
|
1475
|
+
)
|
|
1476
|
+
schema_warnings.extend(augmentation_warnings)
|
|
1418
1477
|
effective_editable_ids = set(editable_question_ids)
|
|
1419
1478
|
for field in index.by_id.values():
|
|
1420
1479
|
if field.que_type in SUBTABLE_QUE_TYPES and (_subtable_descendant_ids(field) & set(editable_question_ids)):
|
|
@@ -1474,6 +1533,44 @@ class TaskContextTools(ToolBase):
|
|
|
1474
1533
|
"editable_question_ids_source": source,
|
|
1475
1534
|
}
|
|
1476
1535
|
|
|
1536
|
+
def _augment_task_editable_field_index(
|
|
1537
|
+
self,
|
|
1538
|
+
*,
|
|
1539
|
+
index: FieldIndex,
|
|
1540
|
+
current_answers: Any,
|
|
1541
|
+
editable_question_ids: set[int],
|
|
1542
|
+
) -> tuple[FieldIndex, list[JSONObject]]:
|
|
1543
|
+
if not editable_question_ids:
|
|
1544
|
+
return index, []
|
|
1545
|
+
missing_field_ids = {
|
|
1546
|
+
str(que_id)
|
|
1547
|
+
for que_id in editable_question_ids
|
|
1548
|
+
if que_id > 0 and str(que_id) not in index.by_id
|
|
1549
|
+
}
|
|
1550
|
+
if not missing_field_ids:
|
|
1551
|
+
return index, []
|
|
1552
|
+
answer_backed_index = _build_answer_backed_field_index(
|
|
1553
|
+
current_answers,
|
|
1554
|
+
field_id_filter=missing_field_ids,
|
|
1555
|
+
)
|
|
1556
|
+
if not answer_backed_index.by_id:
|
|
1557
|
+
return index, []
|
|
1558
|
+
augmented_index = _merge_field_indexes(index, answer_backed_index)
|
|
1559
|
+
return augmented_index, [
|
|
1560
|
+
{
|
|
1561
|
+
"code": "TASK_RUNTIME_EDITABLE_FIELDS_AUGMENTED",
|
|
1562
|
+
"message": "task update schema added backend-editable fields from current task answers because applicant schema did not expose them.",
|
|
1563
|
+
"fields": [
|
|
1564
|
+
{
|
|
1565
|
+
"field_id": field.que_id,
|
|
1566
|
+
"title": field.que_title,
|
|
1567
|
+
"que_type": field.que_type,
|
|
1568
|
+
}
|
|
1569
|
+
for field in answer_backed_index.by_id.values()
|
|
1570
|
+
],
|
|
1571
|
+
}
|
|
1572
|
+
]
|
|
1573
|
+
|
|
1477
1574
|
def _resolve_task_editable_question_ids(
|
|
1478
1575
|
self,
|
|
1479
1576
|
context: BackendRequestContext,
|
|
@@ -1504,6 +1601,31 @@ class TaskContextTools(ToolBase):
|
|
|
1504
1601
|
fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
|
|
1505
1602
|
return fallback_ids, warnings, "que_auth_setting"
|
|
1506
1603
|
|
|
1604
|
+
def _resolve_task_save_only_availability(
|
|
1605
|
+
self,
|
|
1606
|
+
context: BackendRequestContext,
|
|
1607
|
+
*,
|
|
1608
|
+
app_key: str,
|
|
1609
|
+
workflow_node_id: int,
|
|
1610
|
+
) -> tuple[bool, list[JSONObject], str]:
|
|
1611
|
+
try:
|
|
1612
|
+
payload = self.backend.request(
|
|
1613
|
+
"GET",
|
|
1614
|
+
context,
|
|
1615
|
+
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
1616
|
+
)
|
|
1617
|
+
except QingflowApiError as error:
|
|
1618
|
+
warning: JSONObject = {
|
|
1619
|
+
"code": "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
|
|
1620
|
+
"message": "save_only is hidden because backend editableQueIds is unavailable for the current node; MCP no longer infers save_only from local schema reconstruction.",
|
|
1621
|
+
}
|
|
1622
|
+
if error.backend_code is not None:
|
|
1623
|
+
warning["backend_code"] = error.backend_code
|
|
1624
|
+
if error.http_status is not None:
|
|
1625
|
+
warning["http_status"] = error.http_status
|
|
1626
|
+
return False, [warning], "backend_editable_que_ids_unavailable"
|
|
1627
|
+
return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
|
|
1628
|
+
|
|
1507
1629
|
def _extract_question_ids(self, payload: Any) -> set[int]:
|
|
1508
1630
|
candidates: list[Any] = []
|
|
1509
1631
|
if isinstance(payload, list):
|
|
@@ -1587,6 +1709,7 @@ class TaskContextTools(ToolBase):
|
|
|
1587
1709
|
},
|
|
1588
1710
|
)
|
|
1589
1711
|
)
|
|
1712
|
+
index = schema_state["index"]
|
|
1590
1713
|
preflight = self._record_tools._build_record_write_preflight(
|
|
1591
1714
|
profile=profile,
|
|
1592
1715
|
context=context,
|
|
@@ -1601,8 +1724,8 @@ class TaskContextTools(ToolBase):
|
|
|
1601
1724
|
view_key=None,
|
|
1602
1725
|
view_name=None,
|
|
1603
1726
|
existing_answers_override=current_answers,
|
|
1727
|
+
field_index_override=index,
|
|
1604
1728
|
)
|
|
1605
|
-
index = schema_state["index"]
|
|
1606
1729
|
effective_editable_ids = set(schema_state["effective_editable_question_ids"])
|
|
1607
1730
|
scoped_field_errors = self._task_scope_field_errors(
|
|
1608
1731
|
normalized_answers=preflight.get("normalized_answers") or [],
|
|
@@ -1692,14 +1815,14 @@ class TaskContextTools(ToolBase):
|
|
|
1692
1815
|
app_key: str,
|
|
1693
1816
|
record_id: int,
|
|
1694
1817
|
workflow_node_id: int,
|
|
1695
|
-
|
|
1818
|
+
apply_answers: list[dict[str, Any]],
|
|
1696
1819
|
) -> dict[str, Any]:
|
|
1697
1820
|
def runner(session_profile, context):
|
|
1698
1821
|
result = self.backend.request(
|
|
1699
1822
|
"POST",
|
|
1700
1823
|
context,
|
|
1701
1824
|
f"/app/{app_key}/apply/{record_id}",
|
|
1702
|
-
json_body={"role": 3, "auditNodeId": workflow_node_id, "answers":
|
|
1825
|
+
json_body={"role": 3, "auditNodeId": workflow_node_id, "answers": apply_answers},
|
|
1703
1826
|
)
|
|
1704
1827
|
return {
|
|
1705
1828
|
"profile": profile,
|