@josephyan/qingflow-app-user-mcp 0.2.0-beta.25 → 0.2.0-beta.26
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
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.26
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.26 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -303,15 +303,57 @@ class DirectoryTools(ToolBase):
|
|
|
303
303
|
page_num: int,
|
|
304
304
|
page_size: int,
|
|
305
305
|
) -> dict[str, Any]:
|
|
306
|
-
if
|
|
307
|
-
raise_tool_error(QingflowApiError.config_error("
|
|
306
|
+
if page_num <= 0:
|
|
307
|
+
raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
|
|
308
|
+
if page_size <= 0:
|
|
309
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
310
|
+
normalized_keyword = keyword.strip()
|
|
311
|
+
|
|
312
|
+
if not normalized_keyword:
|
|
313
|
+
def runner(session_profile, context):
|
|
314
|
+
fetch_limit = max((page_num + 1) * page_size + 1, page_size + 1)
|
|
315
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
316
|
+
context,
|
|
317
|
+
parent_dept_id=None,
|
|
318
|
+
max_depth=20,
|
|
319
|
+
max_items=fetch_limit,
|
|
320
|
+
)
|
|
321
|
+
start = (page_num - 1) * page_size
|
|
322
|
+
page_items = items[start : start + page_size]
|
|
323
|
+
reported_total = None if truncated else len(items)
|
|
324
|
+
page_amount = None if truncated else ((len(items) + page_size - 1) // page_size if items else 0)
|
|
325
|
+
if truncated and page_items:
|
|
326
|
+
page_amount = max(page_num + 1, (start + len(page_items) + page_size - 1) // page_size)
|
|
327
|
+
return {
|
|
328
|
+
"profile": profile,
|
|
329
|
+
"ws_id": session_profile.selected_ws_id,
|
|
330
|
+
"request_route": self._request_route_payload(context),
|
|
331
|
+
"items": page_items,
|
|
332
|
+
"pagination": {
|
|
333
|
+
"page": page_num,
|
|
334
|
+
"page_size": page_size,
|
|
335
|
+
"returned_items": len(page_items),
|
|
336
|
+
"reported_total": reported_total,
|
|
337
|
+
"page_amount": page_amount,
|
|
338
|
+
"depth_scanned": deepest_depth + 1 if page_items else 0,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
raw = self._run(profile, runner)
|
|
343
|
+
items = [item for item in raw.get("items", []) if isinstance(item, dict)]
|
|
344
|
+
return self._public_directory_response(
|
|
345
|
+
raw,
|
|
346
|
+
items=items,
|
|
347
|
+
pagination=raw.get("pagination", {}),
|
|
348
|
+
selection={"keyword": None},
|
|
349
|
+
)
|
|
308
350
|
|
|
309
351
|
def runner(session_profile, context):
|
|
310
352
|
result = self.backend.request(
|
|
311
353
|
"GET",
|
|
312
354
|
context,
|
|
313
355
|
"/contact/deptByPage",
|
|
314
|
-
params={"keyword":
|
|
356
|
+
params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
|
|
315
357
|
)
|
|
316
358
|
return {
|
|
317
359
|
"profile": profile,
|
|
@@ -332,7 +374,7 @@ class DirectoryTools(ToolBase):
|
|
|
332
374
|
"reported_total": _coerce_int(_payload_value(raw.get("page"), "total")),
|
|
333
375
|
"page_amount": _coerce_int(_payload_value(raw.get("page"), "pageAmount")),
|
|
334
376
|
},
|
|
335
|
-
selection={"keyword":
|
|
377
|
+
selection={"keyword": normalized_keyword},
|
|
336
378
|
)
|
|
337
379
|
|
|
338
380
|
def directory_list_all_departments(
|
|
@@ -1686,6 +1686,9 @@ class RecordTools(ToolBase):
|
|
|
1686
1686
|
support_matrix = _summarize_write_support(resolved_fields)
|
|
1687
1687
|
invalid_fields: list[JSONObject] = []
|
|
1688
1688
|
normalized_answers: list[JSONObject] = []
|
|
1689
|
+
validation_warnings = [
|
|
1690
|
+
"record_write performs static preflight from form metadata before apply; runtime visibility and dynamic linkage can still reject writes."
|
|
1691
|
+
]
|
|
1689
1692
|
try:
|
|
1690
1693
|
normalized_answers = self._resolve_answers(
|
|
1691
1694
|
profile,
|
|
@@ -1706,6 +1709,16 @@ class RecordTools(ToolBase):
|
|
|
1706
1709
|
"received_value": error.details.get("received_value") if error.details else None,
|
|
1707
1710
|
}
|
|
1708
1711
|
)
|
|
1712
|
+
validation_answers = normalized_answers
|
|
1713
|
+
if operation == "update" and apply_id is not None and not invalid_fields:
|
|
1714
|
+
try:
|
|
1715
|
+
existing_answers = self._load_record_answers_for_preflight(context, app_key=app_key, apply_id=apply_id)
|
|
1716
|
+
except QingflowApiError:
|
|
1717
|
+
validation_warnings.append(
|
|
1718
|
+
"update preflight could not load the current record; required-field completeness was not revalidated."
|
|
1719
|
+
)
|
|
1720
|
+
else:
|
|
1721
|
+
validation_answers = self._merge_record_answers(existing_answers, normalized_answers)
|
|
1709
1722
|
readonly_or_system_fields = [
|
|
1710
1723
|
{
|
|
1711
1724
|
"que_id": entry.get("que_id"),
|
|
@@ -1721,8 +1734,9 @@ class RecordTools(ToolBase):
|
|
|
1721
1734
|
]
|
|
1722
1735
|
provided_field_ids = {
|
|
1723
1736
|
str(answer.get("queId"))
|
|
1724
|
-
for answer in
|
|
1737
|
+
for answer in validation_answers
|
|
1725
1738
|
if isinstance(answer.get("queId"), int) and int(answer["queId"]) > 0
|
|
1739
|
+
and _answer_has_meaningful_content(answer)
|
|
1726
1740
|
}
|
|
1727
1741
|
missing_required_fields = []
|
|
1728
1742
|
for field in index.by_id.values():
|
|
@@ -1738,9 +1752,6 @@ class RecordTools(ToolBase):
|
|
|
1738
1752
|
)
|
|
1739
1753
|
question_relations = _collect_question_relations(schema)
|
|
1740
1754
|
option_links = _collect_option_links(resolved_fields)
|
|
1741
|
-
validation_warnings = [
|
|
1742
|
-
"record_write performs static preflight from form metadata before apply; runtime visibility and dynamic linkage can still reject writes."
|
|
1743
|
-
]
|
|
1744
1755
|
if question_relations:
|
|
1745
1756
|
validation_warnings.append(
|
|
1746
1757
|
"form contains questionRelations; linked visibility and runtime required rules may differ at submit time."
|
|
@@ -1795,6 +1806,39 @@ class RecordTools(ToolBase):
|
|
|
1795
1806
|
"recommended_next_actions": actions,
|
|
1796
1807
|
}
|
|
1797
1808
|
|
|
1809
|
+
def _load_record_answers_for_preflight(
|
|
1810
|
+
self,
|
|
1811
|
+
context, # type: ignore[no-untyped-def]
|
|
1812
|
+
*,
|
|
1813
|
+
app_key: str,
|
|
1814
|
+
apply_id: int,
|
|
1815
|
+
) -> list[JSONObject]:
|
|
1816
|
+
record = self.backend.request(
|
|
1817
|
+
"GET",
|
|
1818
|
+
context,
|
|
1819
|
+
f"/app/{app_key}/apply/{apply_id}",
|
|
1820
|
+
params={"role": 1, "listType": DEFAULT_RECORD_LIST_TYPE},
|
|
1821
|
+
)
|
|
1822
|
+
answers = record.get("answers") if isinstance(record, dict) else None
|
|
1823
|
+
return [item for item in answers if isinstance(item, dict)] if isinstance(answers, list) else []
|
|
1824
|
+
|
|
1825
|
+
def _merge_record_answers(
|
|
1826
|
+
self,
|
|
1827
|
+
existing_answers: list[JSONObject],
|
|
1828
|
+
patch_answers: list[JSONObject],
|
|
1829
|
+
) -> list[JSONObject]:
|
|
1830
|
+
merged_by_id: dict[int, JSONObject] = {}
|
|
1831
|
+
order: list[int] = []
|
|
1832
|
+
for source in (existing_answers, patch_answers):
|
|
1833
|
+
for item in source:
|
|
1834
|
+
que_id = _coerce_count(item.get("queId")) if isinstance(item, dict) else None
|
|
1835
|
+
if que_id is None or que_id <= 0:
|
|
1836
|
+
continue
|
|
1837
|
+
if que_id not in merged_by_id:
|
|
1838
|
+
order.append(que_id)
|
|
1839
|
+
merged_by_id[que_id] = item
|
|
1840
|
+
return [merged_by_id[que_id] for que_id in order]
|
|
1841
|
+
|
|
1798
1842
|
def record_query(
|
|
1799
1843
|
self,
|
|
1800
1844
|
*,
|
|
@@ -3946,6 +3990,26 @@ def _normalize_audit_nodes(payload: JSONValue) -> list[JSONObject]:
|
|
|
3946
3990
|
return []
|
|
3947
3991
|
|
|
3948
3992
|
|
|
3993
|
+
def _answer_has_meaningful_content(answer: JSONObject) -> bool:
|
|
3994
|
+
table_values = answer.get("tableValues")
|
|
3995
|
+
if isinstance(table_values, list) and table_values:
|
|
3996
|
+
for row in table_values:
|
|
3997
|
+
if isinstance(row, list) and any(_answer_has_meaningful_content(item) for item in row if isinstance(item, dict)):
|
|
3998
|
+
return True
|
|
3999
|
+
return False
|
|
4000
|
+
values = answer.get("values")
|
|
4001
|
+
if not isinstance(values, list) or not values:
|
|
4002
|
+
return False
|
|
4003
|
+
for item in values:
|
|
4004
|
+
if isinstance(item, dict):
|
|
4005
|
+
if any(value not in (None, "", [], {}) for value in item.values()):
|
|
4006
|
+
return True
|
|
4007
|
+
continue
|
|
4008
|
+
if item not in (None, "", [], {}):
|
|
4009
|
+
return True
|
|
4010
|
+
return False
|
|
4011
|
+
|
|
4012
|
+
|
|
3949
4013
|
def _extract_applicant_node(payload: JSONValue) -> WorkflowNodeRef | None:
|
|
3950
4014
|
for item in _normalize_audit_nodes(payload):
|
|
3951
4015
|
node_type = _coerce_count(item.get("type"))
|